use crossterm::event::{KeyCode, KeyModifiers};
use std::time::Duration;
use sysinfo::{
CpuRefreshKind, Disks, MemoryRefreshKind, Networks, ProcessRefreshKind, ProcessesToUpdate,
System, Users,
};
use super::config::{DetailLevel, FilesViewMode, PanelType, PtopConfig, SignalType};
use super::ui::{read_gpu_info, GpuInfo};
#[cfg(target_os = "linux")]
fn parse_meminfo_line(line: &str) -> Option<u64> {
let parts: Vec<&str> = line.split_whitespace().collect();
parts
.get(1)
.and_then(|s| s.parse::<u64>().ok())
.map(|kb| kb * 1024)
}
#[cfg(target_os = "linux")]
fn is_cached_line(line: &str) -> bool {
line.starts_with("Cached:") && !line.starts_with("CachedSwap")
}
#[cfg(target_os = "linux")]
fn read_cached_memory() -> u64 {
std::fs::read_to_string("/proc/meminfo")
.ok()
.and_then(|contents| {
contents
.lines()
.find(|line| is_cached_line(line))
.and_then(parse_meminfo_line)
})
.unwrap_or(0)
}
#[cfg(not(target_os = "linux"))]
fn read_cached_memory() -> u64 {
0
}
#[cfg(target_os = "linux")]
fn map_ccd_temps_to_cores(ccd_temps: &std::collections::HashMap<String, f32>, temps: &mut [f32]) {
let core_count = temps.len();
let cores_per_ccd = core_count / 4;
for (ccd_idx, label) in ["Tccd1", "Tccd2", "Tccd3", "Tccd4"].iter().enumerate() {
if let Some(&temp) = ccd_temps.get(*label) {
let start = cores_per_ccd * ccd_idx;
let end = if ccd_idx == 3 {
core_count
} else {
(cores_per_ccd * (ccd_idx + 1)).min(core_count)
};
for i in start..end {
temps[i] = temp;
}
}
}
if temps.iter().all(|&t| t == 0.0) {
if let Some(&tctl) = ccd_temps.get("Tctl") {
temps.fill(tctl);
}
}
}
#[cfg(target_os = "linux")]
fn read_amd_temps(path: &std::path::Path, temps: &mut [f32]) -> bool {
use std::collections::HashMap;
use std::fs;
let mut ccd_temps: HashMap<String, f32> = HashMap::new();
for i in 1..=10 {
let label_path = path.join(format!("temp{i}_label"));
let input_path = path.join(format!("temp{i}_input"));
if let (Ok(label), Ok(input)) = (
fs::read_to_string(&label_path),
fs::read_to_string(&input_path),
) {
let label = label.trim().to_string();
if let Ok(millidegrees) = input.trim().parse::<i64>() {
ccd_temps.insert(label, millidegrees as f32 / 1000.0);
}
}
}
if ccd_temps.is_empty() {
return false;
}
map_ccd_temps_to_cores(&ccd_temps, temps);
true
}
#[cfg(target_os = "linux")]
fn read_intel_temps(path: &std::path::Path, temps: &mut [f32]) -> bool {
use std::fs;
for (i, temp) in temps.iter_mut().enumerate() {
let temp_file = path.join(format!("temp{}_input", i + 2));
if let Ok(temp_str) = fs::read_to_string(&temp_file) {
if let Ok(millidegrees) = temp_str.trim().parse::<i64>() {
*temp = millidegrees as f32 / 1000.0;
}
}
}
if temps.iter().all(|&t| t == 0.0) {
let temp_file = path.join("temp1_input");
if let Ok(temp_str) = fs::read_to_string(&temp_file) {
if let Ok(millidegrees) = temp_str.trim().parse::<i64>() {
temps.fill(millidegrees as f32 / 1000.0);
return true;
}
}
return false;
}
true
}
#[cfg(target_os = "linux")]
fn try_read_hwmon_temps(path: &std::path::Path, temps: &mut [f32]) -> bool {
use std::fs;
let Ok(name) = fs::read_to_string(path.join("name")) else {
return false;
};
match name.trim() {
"k10temp" | "zenpower" => read_amd_temps(path, temps),
"coretemp" => read_intel_temps(path, temps),
_ => false,
}
}
#[cfg(target_os = "linux")]
fn read_core_temperatures(core_count: usize) -> Vec<f32> {
use std::fs;
use std::path::Path;
let mut temps = vec![0.0f32; core_count];
let hwmon_dir = Path::new("/sys/class/hwmon");
let Ok(entries) = fs::read_dir(hwmon_dir) else {
return temps;
};
for entry in entries.flatten() {
if try_read_hwmon_temps(&entry.path(), &mut temps) {
return temps;
}
}
temps
}
#[cfg(not(target_os = "linux"))]
fn read_core_temperatures(core_count: usize) -> Vec<f32> {
vec![0.0f32; core_count]
}
use super::analyzers::{
AnalyzerRegistry, ConnectionsData, DiskEntropyData, DiskIoData, FileAnalyzerData, PsiData,
SensorHealthData, TreemapData,
};
use crate::{AsyncCollector, Snapshot};
#[derive(Clone)]
pub struct MetricsSnapshot {
pub cpu_avg: f64,
pub per_core_percent: Vec<f64>,
pub per_core_freq: Vec<u64>, pub per_core_temp: Vec<f32>, pub load_avg: sysinfo::LoadAvg,
pub mem_total: u64,
pub mem_used: u64,
pub mem_available: u64,
pub mem_cached: u64,
pub swap_total: u64,
pub swap_used: u64,
pub net_rx: u64,
pub net_tx: u64,
pub gpu_info: Option<GpuInfo>,
pub processes: Vec<ProcessInfo>,
pub disk_info: Vec<DiskInfo>,
pub network_info: Vec<NetworkInfo>,
pub psi_data: Option<PsiData>,
pub connections_data: Option<ConnectionsData>,
pub treemap_data: Option<TreemapData>,
pub sensor_health_data: Option<SensorHealthData>,
pub disk_io_data: Option<DiskIoData>,
pub disk_entropy_data: Option<DiskEntropyData>,
pub file_analyzer_data: Option<FileAnalyzerData>,
}
#[derive(Clone)]
pub struct ProcessInfo {
pub pid: u32,
pub name: String,
pub cpu_usage: f32,
pub memory: u64,
pub user: String,
pub cmd: String,
}
#[derive(Clone)]
pub struct DiskInfo {
pub name: String,
pub mount_point: String,
pub total_space: u64,
pub available_space: u64,
pub file_system: String,
}
#[derive(Clone)]
pub struct NetworkInfo {
pub name: String,
pub received: u64,
pub transmitted: u64,
}
impl Snapshot for MetricsSnapshot {
fn empty() -> Self {
Self {
cpu_avg: 0.0,
per_core_percent: Vec::new(),
per_core_freq: Vec::new(),
per_core_temp: Vec::new(),
load_avg: sysinfo::LoadAvg {
one: 0.0,
five: 0.0,
fifteen: 0.0,
},
mem_total: 0,
mem_used: 0,
mem_available: 0,
mem_cached: 0,
swap_total: 0,
swap_used: 0,
net_rx: 0,
net_tx: 0,
gpu_info: None,
processes: Vec::new(),
disk_info: Vec::new(),
network_info: Vec::new(),
psi_data: None,
connections_data: None,
treemap_data: None,
sensor_health_data: None,
disk_io_data: None,
disk_entropy_data: None,
file_analyzer_data: None,
}
}
}
pub struct MetricsCollector {
system: System,
disks: Disks,
networks: Networks,
analyzers: AnalyzerRegistry,
deterministic: bool,
frame_id: u64,
}
impl MetricsCollector {
pub fn new(deterministic: bool) -> Self {
let mut system = System::new();
system.refresh_cpu_specifics(CpuRefreshKind::everything());
let disks = Disks::new_with_refreshed_list();
let networks = Networks::new_with_refreshed_list();
let analyzers = AnalyzerRegistry::new();
Self {
system,
disks,
networks,
analyzers,
deterministic,
frame_id: 0,
}
}
pub fn has_psi(&self) -> bool {
self.analyzers.psi.is_some()
}
pub fn has_gpu(&self) -> bool {
self.analyzers.gpu_procs.is_some()
}
pub fn has_sensors(&self) -> bool {
self.analyzers.sensor_health.is_some()
}
pub fn has_connections(&self) -> bool {
self.analyzers.connections.is_some()
}
pub fn has_treemap(&self) -> bool {
self.analyzers.treemap.is_some()
}
}
impl AsyncCollector for MetricsCollector {
type Snapshot = MetricsSnapshot;
fn collect(&mut self) -> MetricsSnapshot {
self.frame_id += 1;
if self.deterministic {
return MetricsSnapshot::empty();
}
self.system
.refresh_cpu_specifics(CpuRefreshKind::everything());
let cpu_total: f32 = self
.system
.cpus()
.iter()
.map(sysinfo::Cpu::cpu_usage)
.sum::<f32>()
/ self.system.cpus().len().max(1) as f32;
let per_core_percent: Vec<f64> = self
.system
.cpus()
.iter()
.map(|c| c.cpu_usage() as f64)
.collect();
let per_core_freq: Vec<u64> = self
.system
.cpus()
.iter()
.map(sysinfo::Cpu::frequency)
.collect();
let per_core_temp: Vec<f32> = read_core_temperatures(self.system.cpus().len());
let load_avg = System::load_average();
self.system
.refresh_memory_specifics(MemoryRefreshKind::everything());
let mem_total = self.system.total_memory();
let mem_used = self.system.used_memory();
let mem_available = self.system.available_memory();
let mem_cached = read_cached_memory();
let swap_total = self.system.total_swap();
let swap_used = self.system.used_swap();
let process_count = self.system.processes().len();
let needs_initial = process_count == 0;
let needs_periodic = self.frame_id > 0 && self.frame_id % 60 == 0;
if needs_initial || needs_periodic {
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_user(sysinfo::UpdateKind::OnlyIfNotSet),
);
} else if self.frame_id > 0 {
let top_pids: Vec<_> = self
.system
.processes()
.iter()
.filter(|(_, p)| p.cpu_usage() > 0.1)
.take(50)
.map(|(pid, _)| *pid)
.collect();
if !top_pids.is_empty() {
self.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&top_pids),
true,
ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_user(sysinfo::UpdateKind::OnlyIfNotSet),
);
}
}
let processes: Vec<ProcessInfo> = self
.system
.processes()
.iter()
.map(|(pid, proc)| ProcessInfo {
pid: pid.as_u32(),
name: proc.name().to_string_lossy().to_string(),
cpu_usage: proc.cpu_usage(),
memory: proc.memory(),
user: proc.user_id().map(|u| u.to_string()).unwrap_or_default(),
cmd: proc
.cmd()
.iter()
.map(|s| s.to_string_lossy())
.collect::<Vec<_>>()
.join(" "),
})
.collect();
self.disks.refresh(true);
let disk_info: Vec<DiskInfo> = self
.disks
.iter()
.map(|d| DiskInfo {
name: d.name().to_string_lossy().to_string(),
mount_point: d.mount_point().to_string_lossy().to_string(),
total_space: d.total_space(),
available_space: d.available_space(),
file_system: d.file_system().to_string_lossy().to_string(),
})
.collect();
self.networks.refresh(true);
let (net_rx, net_tx) = self
.networks
.iter()
.fold((0u64, 0u64), |acc, (_name, data)| {
(acc.0 + data.received(), acc.1 + data.transmitted())
});
let network_info: Vec<NetworkInfo> = self
.networks
.iter()
.map(|(name, data)| NetworkInfo {
name: name.clone(),
received: data.received(),
transmitted: data.transmitted(),
})
.collect();
let gpu_info = read_gpu_info();
self.analyzers.collect_all();
let psi_data = self.analyzers.psi.as_ref().map(|p| p.data().clone());
let connections_data = self
.analyzers
.connections
.as_ref()
.map(|c| c.data().clone());
let treemap_data = self.analyzers.treemap.as_ref().map(|t| t.data().clone());
let sensor_health_data = self
.analyzers
.sensor_health
.as_ref()
.map(|s| s.data().clone());
let disk_io_data = self.analyzers.disk_io.as_ref().map(|d| d.data().clone());
let disk_entropy_data = self
.analyzers
.disk_entropy
.as_ref()
.map(|d| d.data().clone());
let file_analyzer_data = self
.analyzers
.file_analyzer
.as_ref()
.map(|f| f.data().clone());
MetricsSnapshot {
cpu_avg: cpu_total as f64 / 100.0,
per_core_percent,
per_core_freq,
per_core_temp,
load_avg,
mem_total,
mem_used,
mem_available,
mem_cached,
swap_total,
swap_used,
net_rx,
net_tx,
gpu_info,
processes,
disk_info,
network_info,
psi_data,
connections_data,
treemap_data,
sensor_health_data,
disk_io_data,
disk_entropy_data,
file_analyzer_data,
}
}
}
pub struct RingBuffer<T> {
data: Vec<T>,
capacity: usize,
}
impl<T: Clone> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
Self {
data: Vec::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, value: T) {
if self.data.len() >= self.capacity {
self.data.remove(0);
}
self.data.push(value);
}
pub fn as_slice(&self) -> &[T] {
&self.data
}
pub fn last(&self) -> Option<&T> {
self.data.last()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessSortColumn {
Pid,
User,
Cpu,
Mem,
Command,
}
impl ProcessSortColumn {
pub const COUNT: usize = 5;
pub fn next(self) -> Self {
match self {
Self::Pid => Self::User,
Self::User => Self::Cpu,
Self::Cpu => Self::Mem,
Self::Mem => Self::Command,
Self::Command => Self::Pid,
}
}
pub fn prev(self) -> Self {
match self {
Self::Pid => Self::Command,
Self::User => Self::Pid,
Self::Cpu => Self::User,
Self::Mem => Self::Cpu,
Self::Command => Self::Mem,
}
}
pub fn from_index(idx: usize) -> Self {
match idx % Self::COUNT {
0 => Self::Pid,
1 => Self::User,
2 => Self::Cpu,
3 => Self::Mem,
_ => Self::Command,
}
}
pub fn to_index(self) -> usize {
match self {
Self::Pid => 0,
Self::User => 1,
Self::Cpu => 2,
Self::Mem => 3,
Self::Command => 4,
}
}
pub fn header(self, is_sorted: bool, descending: bool) -> String {
let base = match self {
Self::Pid => "PID",
Self::User => "USER",
Self::Cpu => "CPU%",
Self::Mem => "MEM%",
Self::Command => "COMMAND",
};
if is_sorted {
format!("{}{}", base, if descending { "â–¼" } else { "â–²" })
} else {
base.to_string()
}
}
}
#[derive(Debug, Clone, Copy)]
#[allow(clippy::struct_excessive_bools)]
pub struct PanelVisibility {
pub cpu: bool,
pub memory: bool,
pub disk: bool,
pub network: bool,
pub process: bool,
pub gpu: bool,
pub sensors: bool,
pub psi: bool,
pub connections: bool,
pub battery: bool,
pub sensors_compact: bool,
pub system: bool,
pub treemap: bool,
pub files: bool,
}
impl Default for PanelVisibility {
fn default() -> Self {
Self {
cpu: true,
memory: true,
disk: true,
network: true,
process: true,
gpu: false,
sensors: false,
psi: false,
connections: false,
battery: false,
sensors_compact: false,
system: false,
treemap: false,
files: false,
}
}
}
#[allow(clippy::struct_excessive_bools)]
pub struct App {
pub system: System,
pub disks: Disks,
pub networks: Networks,
pub users: Users,
pub analyzers: AnalyzerRegistry,
pub cpu_history: RingBuffer<f64>,
pub mem_history: RingBuffer<f64>,
pub net_rx_history: RingBuffer<f64>,
pub net_tx_history: RingBuffer<f64>,
pub net_iface_history: std::collections::HashMap<String, (RingBuffer<f64>, RingBuffer<f64>)>,
pub selected_interface_index: usize,
pub gpu_history: RingBuffer<f64>,
pub vram_history: RingBuffer<f64>,
pub gpu_info: Option<GpuInfo>,
pub per_core_percent: Vec<f64>,
pub per_core_freq: Vec<u64>,
pub per_core_temp: Vec<f32>,
pub mem_total: u64,
pub mem_used: u64,
pub mem_available: u64,
pub mem_cached: u64,
pub swap_total: u64,
pub swap_used: u64,
pub panels: PanelVisibility,
pub process_selected: usize,
pub process_scroll_offset: usize,
pub sort_column: ProcessSortColumn,
pub sort_descending: bool,
pub filter: String,
pub show_filter_input: bool,
pub show_help: bool,
pub running: bool,
pub pending_signal: Option<(u32, String, SignalType)>,
pub signal_result: Option<(bool, String, std::time::Instant)>,
pub focused_panel: Option<PanelType>,
pub exploded_panel: Option<PanelType>,
pub selected_column: usize,
pub files_view_mode: FilesViewMode,
pub collapse_memory: Option<PanelType>,
pub config: PtopConfig,
pub frame_id: u64,
pub avg_frame_time_us: u64,
pub show_fps: bool,
pub deterministic: bool,
pub fixed_uptime: u64,
pub load_avg: sysinfo::LoadAvg,
pub hostname: String,
pub kernel_version: String,
pub in_container: bool,
pub system_capabilities: crate::widgets::SystemCapabilities,
pub snapshot_processes: Vec<ProcessInfo>,
pub snapshot_disks: Vec<DiskInfo>,
pub snapshot_networks: Vec<NetworkInfo>,
pub snapshot_psi: Option<PsiData>,
pub snapshot_connections: Option<ConnectionsData>,
pub snapshot_treemap: Option<TreemapData>,
pub snapshot_sensor_health: Option<SensorHealthData>,
pub snapshot_disk_io: Option<DiskIoData>,
pub snapshot_disk_entropy: Option<DiskEntropyData>,
pub snapshot_file_analyzer: Option<FileAnalyzerData>,
}
impl App {
pub fn new(deterministic: bool) -> Self {
Self::with_config(deterministic, PtopConfig::load())
}
pub fn with_config(deterministic: bool, config: PtopConfig) -> Self {
Self::with_config_options(deterministic, config, false)
}
pub fn with_config_lightweight(deterministic: bool, config: PtopConfig) -> Self {
Self::with_config_options(deterministic, config, true)
}
fn with_config_options(deterministic: bool, config: PtopConfig, lightweight: bool) -> Self {
let mut system = System::new();
system.refresh_cpu_specifics(CpuRefreshKind::everything());
std::thread::sleep(Duration::from_millis(50));
system.refresh_cpu_specifics(CpuRefreshKind::everything());
system.refresh_memory_specifics(MemoryRefreshKind::everything());
if !lightweight {
system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::everything()
.with_cpu()
.with_memory()
.with_user(sysinfo::UpdateKind::OnlyIfNotSet),
);
std::thread::sleep(Duration::from_millis(50));
system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing().with_cpu(),
);
}
let disks = Disks::new_with_refreshed_list();
let networks = Networks::new_with_refreshed_list();
let core_count = if deterministic {
48
} else {
system.cpus().len()
};
let analyzers = AnalyzerRegistry::new();
let mut panels = PanelVisibility::default();
let has_sensors = analyzers
.sensor_health
.as_ref()
.is_some_and(|sh| !sh.data().sensors.is_empty());
if has_sensors {
panels.sensors = true;
} else if analyzers.psi.is_some() {
panels.psi = true;
}
if analyzers.gpu_procs.is_some() {
panels.gpu = true;
}
if analyzers.connections.is_some() {
panels.connections = true;
}
if analyzers.treemap.is_some() {
panels.files = true;
}
if deterministic {
panels = PanelVisibility {
cpu: true,
memory: true,
disk: true,
network: true,
process: true,
gpu: true,
sensors: true,
psi: false, connections: true,
battery: false,
sensors_compact: false,
system: false,
treemap: false,
files: true,
};
}
let users = Users::new_with_refreshed_list();
let mut app = Self {
system,
disks,
networks,
users,
analyzers,
cpu_history: RingBuffer::new(60),
mem_history: RingBuffer::new(60),
net_rx_history: RingBuffer::new(60),
net_tx_history: RingBuffer::new(60),
net_iface_history: std::collections::HashMap::new(),
selected_interface_index: 0, gpu_history: RingBuffer::new(60),
vram_history: RingBuffer::new(60),
gpu_info: None,
per_core_percent: vec![0.0; core_count],
per_core_freq: vec![0; core_count], per_core_temp: if deterministic {
vec![0.0; core_count]
} else {
read_core_temperatures(core_count)
},
mem_total: 0,
mem_used: 0,
mem_available: 0,
mem_cached: 0,
swap_total: 0,
swap_used: 0,
panels,
process_selected: 0,
process_scroll_offset: 0,
sort_column: ProcessSortColumn::Cpu,
sort_descending: true,
filter: String::new(),
show_filter_input: false,
show_help: false,
running: true,
pending_signal: None,
signal_result: None,
focused_panel: Some(PanelType::Cpu), exploded_panel: None,
selected_column: 0, files_view_mode: FilesViewMode::default(), collapse_memory: None, config,
frame_id: 0,
avg_frame_time_us: 0,
show_fps: false,
deterministic,
fixed_uptime: 0,
load_avg: if deterministic {
sysinfo::LoadAvg {
one: 0.0,
five: 0.0,
fifteen: 0.0,
}
} else {
System::load_average()
},
hostname: read_hostname(),
kernel_version: read_kernel_version(),
in_container: detect_container(),
system_capabilities: crate::widgets::SystemCapabilities::detect(),
snapshot_processes: Vec::new(),
snapshot_disks: Vec::new(),
snapshot_networks: Vec::new(),
snapshot_psi: None,
snapshot_connections: None,
snapshot_treemap: None,
snapshot_sensor_health: None,
snapshot_disk_io: None,
snapshot_disk_entropy: None,
snapshot_file_analyzer: None,
};
if deterministic {
app.init_deterministic_data();
}
app.gpu_info = read_gpu_info();
app
}
fn init_deterministic_data(&mut self) {
self.per_core_percent = vec![0.0; 48];
self.mem_total = 0;
self.mem_used = 0;
self.mem_available = 0;
self.mem_cached = 0;
self.swap_total = 0;
self.swap_used = 0;
self.fixed_uptime = 5 * 86400 + 3 * 3600 + 47 * 60;
for _ in 0..60 {
self.cpu_history.push(0.0);
self.mem_history.push(0.0);
self.net_rx_history.push(0.0);
self.net_tx_history.push(0.0);
}
}
pub fn psi_data(&self) -> Option<&PsiData> {
self.snapshot_psi.as_ref()
}
pub fn collect_metrics(&mut self) {
self.frame_id += 1;
debug_assert!(
self.frame_id > 0,
"frame_id must be positive after increment"
);
if self.frame_id % 10 == 0 {
if let Some(new_config) = self.config.check_reload() {
eprintln!("[ptop] config reloaded");
self.config = new_config;
}
}
if self.deterministic {
return;
}
self.load_avg = System::load_average();
self.system
.refresh_cpu_specifics(CpuRefreshKind::everything());
let cpu_total: f32 = self
.system
.cpus()
.iter()
.map(sysinfo::Cpu::cpu_usage)
.sum::<f32>()
/ self.system.cpus().len().max(1) as f32;
self.cpu_history.push(cpu_total as f64 / 100.0);
self.per_core_percent = self
.system
.cpus()
.iter()
.map(|c| c.cpu_usage() as f64)
.collect();
debug_assert_eq!(
self.per_core_percent.len(),
self.system.cpus().len(),
"per_core_percent must have one entry per CPU"
);
self.per_core_freq = self
.system
.cpus()
.iter()
.map(sysinfo::Cpu::frequency)
.collect();
self.per_core_temp = read_core_temperatures(self.system.cpus().len());
self.system
.refresh_memory_specifics(MemoryRefreshKind::everything());
self.mem_total = self.system.total_memory();
self.mem_used = self.system.used_memory();
self.mem_available = self.system.available_memory();
self.mem_cached = read_cached_memory();
self.swap_total = self.system.total_swap();
self.swap_used = self.system.used_swap();
if self.mem_total > 0 {
self.mem_history
.push(self.mem_used as f64 / self.mem_total as f64);
}
let process_count = self.system.processes().len();
let needs_initial_data = process_count == 0;
let needs_periodic_refresh = self.frame_id > 0 && self.frame_id % 60 == 0;
let needs_delta_calculation = self.frame_id <= 3;
if needs_initial_data || needs_periodic_refresh || needs_delta_calculation {
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_user(sysinfo::UpdateKind::OnlyIfNotSet),
);
} else if self.frame_id > 0 {
let top_pids: Vec<_> = self
.system
.processes()
.iter()
.filter(|(_, p)| p.cpu_usage() > 0.1)
.take(50)
.map(|(pid, _)| *pid)
.collect();
if !top_pids.is_empty() {
self.system.refresh_processes_specifics(
ProcessesToUpdate::Some(&top_pids),
true,
ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_user(sysinfo::UpdateKind::OnlyIfNotSet),
);
}
}
self.disks.refresh(true);
self.networks.refresh(true);
let (rx, tx) = self
.networks
.iter()
.fold((0u64, 0u64), |acc, (_name, data)| {
(acc.0 + data.received(), acc.1 + data.transmitted())
});
self.net_rx_history
.push((rx as f64 / 1_000_000_000.0).min(1.0));
self.net_tx_history
.push((tx as f64 / 1_000_000_000.0).min(1.0));
if !self.deterministic {
self.gpu_info = read_gpu_info();
if let Some(ref gpu) = self.gpu_info {
self.gpu_history.push(gpu.utilization.unwrap_or(0) as f64);
let vram_pct = match (gpu.vram_used, gpu.vram_total) {
(Some(used), Some(total)) if total > 0 => (used as f64 / total as f64) * 100.0,
_ => 0.0,
};
self.vram_history.push(vram_pct);
}
}
self.analyzers.collect_all();
self.snapshot_psi = self.analyzers.psi.as_ref().map(|p| p.data().clone());
self.snapshot_connections = self
.analyzers
.connections
.as_ref()
.map(|c| c.data().clone());
self.snapshot_treemap = self.analyzers.treemap.as_ref().map(|t| t.data().clone());
self.snapshot_sensor_health = self
.analyzers
.sensor_health
.as_ref()
.map(|s| s.data().clone());
self.snapshot_disk_io = self.analyzers.disk_io.as_ref().map(|d| d.data().clone());
self.snapshot_disk_entropy = self
.analyzers
.disk_entropy
.as_ref()
.map(|d| d.data().clone());
self.snapshot_file_analyzer = self
.analyzers
.file_analyzer
.as_ref()
.map(|f| f.data().clone());
}
pub fn update_frame_stats(&mut self, frame_times: &[Duration]) {
if frame_times.is_empty() {
return;
}
let total: u128 = frame_times.iter().map(std::time::Duration::as_micros).sum();
self.avg_frame_time_us = (total / frame_times.len() as u128) as u64;
}
pub fn apply_snapshot(&mut self, snapshot: MetricsSnapshot) {
self.frame_id += 1;
self.per_core_percent = snapshot.per_core_percent;
self.per_core_freq = snapshot.per_core_freq;
self.per_core_temp = snapshot.per_core_temp;
self.load_avg = snapshot.load_avg;
self.cpu_history.push(snapshot.cpu_avg);
self.mem_total = snapshot.mem_total;
self.mem_used = snapshot.mem_used;
self.mem_available = snapshot.mem_available;
self.mem_cached = snapshot.mem_cached;
self.swap_total = snapshot.swap_total;
self.swap_used = snapshot.swap_used;
if self.mem_total > 0 {
self.mem_history
.push(self.mem_used as f64 / self.mem_total as f64);
}
self.net_rx_history
.push((snapshot.net_rx as f64 / 1_000_000_000.0).min(1.0));
self.net_tx_history
.push((snapshot.net_tx as f64 / 1_000_000_000.0).min(1.0));
self.gpu_info = snapshot.gpu_info.clone();
if let Some(ref gpu) = snapshot.gpu_info {
self.gpu_history.push(gpu.utilization.unwrap_or(0) as f64);
let vram_pct = match (gpu.vram_used, gpu.vram_total) {
(Some(used), Some(total)) if total > 0 => (used as f64 / total as f64) * 100.0,
_ => 0.0,
};
self.vram_history.push(vram_pct);
}
self.snapshot_processes = snapshot.processes;
self.snapshot_disks = snapshot.disk_info;
for net in &snapshot.network_info {
let entry = self
.net_iface_history
.entry(net.name.clone())
.or_insert_with(|| (RingBuffer::new(60), RingBuffer::new(60)));
entry.0.push(net.received as f64);
entry.1.push(net.transmitted as f64);
}
self.snapshot_networks = snapshot.network_info;
self.snapshot_psi = snapshot.psi_data;
self.snapshot_connections = snapshot.connections_data;
self.snapshot_treemap = snapshot.treemap_data;
self.snapshot_sensor_health = snapshot.sensor_health_data;
self.snapshot_disk_io = snapshot.disk_io_data;
self.snapshot_disk_entropy = snapshot.disk_entropy_data;
self.snapshot_file_analyzer = snapshot.file_analyzer_data;
}
pub fn data_availability(&self) -> crate::widgets::DataAvailability {
crate::widgets::DataAvailability {
psi_available: self.snapshot_psi.as_ref().is_some_and(|psi| {
psi.available
&& (psi.cpu.some.avg10 > 0.01
|| psi.io.some.avg10 > 0.01
|| psi.memory.some.avg10 > 0.01)
}),
sensors_available: self.snapshot_sensor_health.is_some(),
sensor_count: self
.snapshot_sensor_health
.as_ref()
.map_or(0, |s| s.sensors.len()),
gpu_available: self.gpu_info.is_some(),
battery_available: self
.analyzers
.battery_data()
.is_some_and(|b| !b.batteries.is_empty()),
treemap_ready: self
.snapshot_treemap
.as_ref()
.is_some_and(|t| !t.top_items.is_empty()),
connections_available: self.snapshot_connections.is_some(),
connection_count: self
.snapshot_connections
.as_ref()
.map_or(0, |c| c.connections.len()),
}
}
pub fn evaluate_panel_display(&self, panel: PanelType) -> crate::widgets::DisplayAction {
use crate::widgets::{
BatteryDisplayRules, DisplayContext, DisplayRules, DisplayTerminalSize,
GpuDisplayRules, PsiDisplayRules, SensorsDisplayRules,
};
let ctx = DisplayContext {
system: &self.system_capabilities,
terminal: DisplayTerminalSize {
width: 0,
height: 0,
}, data: self.data_availability(),
};
match panel {
PanelType::Psi => PsiDisplayRules.evaluate(&ctx),
PanelType::Sensors => SensorsDisplayRules.evaluate(&ctx),
PanelType::Gpu => GpuDisplayRules.evaluate(&ctx),
PanelType::Battery => BatteryDisplayRules.evaluate(&ctx),
_ => crate::widgets::DisplayAction::Show,
}
}
pub fn handle_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
if self.show_help {
return self.handle_help_mode_key(code, modifiers);
}
if self.pending_signal.is_some() {
return self.handle_signal_confirmation_key(code, modifiers);
}
if self.exploded_panel.is_some() {
return self.handle_exploded_mode_key(code, modifiers);
}
if self.show_filter_input {
return self.handle_filter_input_key(code);
}
self.handle_normal_mode_key(code, modifiers)
}
fn handle_help_mode_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
match code {
KeyCode::Esc | KeyCode::Char('?' | 'h') | KeyCode::F(1) => {
self.show_help = false;
}
KeyCode::Char('q') => return true,
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => return true,
_ => {} }
false
}
fn handle_signal_confirmation_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
match code {
KeyCode::Char('Y') | KeyCode::Enter => self.confirm_signal(),
KeyCode::Char('n' | 'N') | KeyCode::Esc => self.cancel_signal(),
KeyCode::Char('q') => return true,
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => return true,
KeyCode::Char('x') => self.request_signal(SignalType::Term),
KeyCode::Char('K') => self.request_signal(SignalType::Kill),
KeyCode::Char('H') => self.request_signal(SignalType::Hup),
KeyCode::Char('i') => self.request_signal(SignalType::Int),
KeyCode::Char('p') => self.request_signal(SignalType::Stop),
_ => {} }
false
}
fn handle_exploded_mode_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
match code {
KeyCode::Esc | KeyCode::Char('z') => self.exploded_panel = None,
KeyCode::Char('q') => return true,
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => return true,
KeyCode::Left | KeyCode::Char('h') => self.navigate_column_left(),
KeyCode::Right | KeyCode::Char('l') => self.navigate_column_right(),
KeyCode::Enter | KeyCode::Char(' ') => self.sort_by_selected_column(),
KeyCode::Up | KeyCode::Char('k') => self.navigate_process(-1),
KeyCode::Down | KeyCode::Char('j') => self.navigate_process(1),
KeyCode::PageUp => self.navigate_process(-10),
KeyCode::PageDown => self.navigate_process(10),
KeyCode::Home | KeyCode::Char('g') => self.process_selected = 0,
KeyCode::End | KeyCode::Char('G') => self.select_last_process(),
KeyCode::Char('c') => self.quick_sort(ProcessSortColumn::Cpu, true),
KeyCode::Char('m') => self.quick_sort(ProcessSortColumn::Mem, true),
KeyCode::Char('p') => self.quick_sort(ProcessSortColumn::Pid, false),
KeyCode::Char('n') => self.quick_sort(ProcessSortColumn::Command, false),
KeyCode::Char('r') => self.sort_descending = !self.sort_descending,
KeyCode::Char('/' | 'f') => self.show_filter_input = true,
_ => {} }
false
}
fn handle_filter_input_key(&mut self, code: KeyCode) -> bool {
match code {
KeyCode::Esc => {
self.show_filter_input = false;
self.filter.clear();
}
KeyCode::Enter => self.show_filter_input = false,
KeyCode::Backspace => {
self.filter.pop();
}
KeyCode::Char(c) => self.filter.push(c),
_ => {}
}
false
}
#[allow(clippy::match_same_arms)]
fn handle_normal_mode_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
match code {
KeyCode::Char('q') | KeyCode::Esc => return true,
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => return true,
KeyCode::Enter | KeyCode::Char('z') => {
if let Some(panel) = self.focused_panel {
self.exploded_panel = Some(panel);
}
}
KeyCode::Tab if !modifiers.contains(KeyModifiers::SHIFT) => {
if self.focused_panel == Some(PanelType::Network) {
self.cycle_interface();
} else {
self.navigate_panel_forward();
}
}
KeyCode::BackTab => self.navigate_panel_backward(),
KeyCode::Char('l') if !self.show_filter_input => self.navigate_panel_forward(),
KeyCode::Char('H') => self.navigate_panel_backward(),
KeyCode::Char('?' | 'h') | KeyCode::F(1) => {
self.show_help = !self.show_help;
}
KeyCode::Char('1') => self.toggle_panel(PanelType::Cpu),
KeyCode::Char('2') => self.toggle_panel(PanelType::Memory),
KeyCode::Char('3') => self.toggle_panel(PanelType::Disk),
KeyCode::Char('4') => self.toggle_panel(PanelType::Network),
KeyCode::Char('5') => self.toggle_panel(PanelType::Process),
KeyCode::Char('6') => self.toggle_panel(PanelType::Gpu),
KeyCode::Char('7') => self.toggle_panel(PanelType::Sensors),
KeyCode::Char('8') => self.toggle_panel(PanelType::Connections),
KeyCode::Char('9') => self.toggle_panel(PanelType::Psi),
KeyCode::Char('v') if self.focused_panel == Some(PanelType::Files) => {
self.cycle_files_view_mode();
}
KeyCode::Down | KeyCode::Char('j') => self.navigate_process(1),
KeyCode::Up | KeyCode::Char('k') => self.navigate_process(-1),
KeyCode::PageDown => self.navigate_process(10),
KeyCode::PageUp => self.navigate_process(-10),
KeyCode::Home | KeyCode::Char('g') => self.process_selected = 0,
KeyCode::End | KeyCode::Char('G') => self.select_last_process(),
KeyCode::Char('c') => {
self.sort_column = ProcessSortColumn::Cpu;
self.sort_descending = true;
}
KeyCode::Char('m') => {
self.sort_column = ProcessSortColumn::Mem;
self.sort_descending = true;
}
KeyCode::Char('p') => {
self.sort_column = ProcessSortColumn::Pid;
self.sort_descending = false;
}
KeyCode::Char('s') => self.sort_column = self.sort_column.next(),
KeyCode::Char('r') => self.sort_descending = !self.sort_descending,
KeyCode::Char('/' | 'f') => self.show_filter_input = true,
KeyCode::Delete => self.filter.clear(),
KeyCode::Char('0') => self.panels = PanelVisibility::default(),
KeyCode::Char('x') => self.request_signal(SignalType::Term),
KeyCode::Char('X') => self.request_signal(SignalType::Kill),
_ => {}
}
false
}
fn navigate_column_left(&mut self) {
if self.selected_column > 0 {
self.selected_column -= 1;
} else {
self.selected_column = ProcessSortColumn::COUNT - 1;
}
}
fn navigate_column_right(&mut self) {
self.selected_column = (self.selected_column + 1) % ProcessSortColumn::COUNT;
}
fn sort_by_selected_column(&mut self) {
let new_col = ProcessSortColumn::from_index(self.selected_column);
if self.sort_column == new_col {
self.sort_descending = !self.sort_descending;
} else {
self.sort_column = new_col;
self.sort_descending =
matches!(new_col, ProcessSortColumn::Cpu | ProcessSortColumn::Mem);
}
}
fn quick_sort(&mut self, column: ProcessSortColumn, descending: bool) {
self.sort_column = column;
self.selected_column = column.to_index();
self.sort_descending = descending;
}
fn select_last_process(&mut self) {
let count = self.process_count();
if count > 0 {
self.process_selected = count - 1;
}
}
fn navigate_panel_forward(&mut self) {
let visible = self.visible_panels();
if visible.is_empty() {
return;
}
let current_idx = self
.focused_panel
.and_then(|p| visible.iter().position(|&v| v == p))
.unwrap_or(0);
let next_idx = (current_idx + 1) % visible.len();
self.focused_panel = Some(visible[next_idx]);
}
fn navigate_panel_backward(&mut self) {
let visible = self.visible_panels();
if visible.is_empty() {
return;
}
let current_idx = self
.focused_panel
.and_then(|p| visible.iter().position(|&v| v == p))
.unwrap_or(0);
let prev_idx = if current_idx == 0 {
visible.len() - 1
} else {
current_idx - 1
};
self.focused_panel = Some(visible[prev_idx]);
}
pub fn visible_panels(&self) -> Vec<PanelType> {
let mut visible = Vec::new();
if self.panels.cpu {
visible.push(PanelType::Cpu);
}
if self.panels.memory {
visible.push(PanelType::Memory);
}
if self.panels.disk {
visible.push(PanelType::Disk);
}
if self.panels.network {
visible.push(PanelType::Network);
}
if self.panels.process {
visible.push(PanelType::Process);
}
if self.panels.gpu {
visible.push(PanelType::Gpu);
}
if self.panels.sensors {
visible.push(PanelType::Sensors);
}
if self.panels.connections {
visible.push(PanelType::Connections);
}
if self.panels.psi {
visible.push(PanelType::Psi);
}
if self.panels.files {
visible.push(PanelType::Files);
}
visible
}
pub fn is_panel_focused(&self, panel: PanelType) -> bool {
self.focused_panel == Some(panel)
}
pub fn detail_level_for_panel(&self, _panel: PanelType, height: u16) -> DetailLevel {
DetailLevel::for_height(height)
}
#[allow(clippy::cast_possible_wrap)]
fn navigate_process(&mut self, delta: isize) {
let count = self.process_count();
if count == 0 {
return;
}
let current = self.process_selected as isize;
let new = (current + delta).clamp(0, (count - 1) as isize) as usize;
self.process_selected = new;
}
pub fn process_count(&self) -> usize {
if self.deterministic {
return 0;
}
self.system
.processes()
.values()
.filter(|p| {
if self.filter.is_empty() {
true
} else {
let name = p.name().to_string_lossy().to_lowercase();
name.contains(&self.filter.to_lowercase())
}
})
.count()
}
fn get_selected_process(&self) -> Option<(u32, String)> {
let procs = self.sorted_processes();
if self.process_selected < procs.len() {
let p = procs[self.process_selected];
Some((p.pid().as_u32(), p.name().to_string_lossy().to_string()))
} else {
None
}
}
pub fn request_signal(&mut self, signal: SignalType) {
if let Some((pid, name)) = self.get_selected_process() {
self.pending_signal = Some((pid, name, signal));
}
}
pub fn confirm_signal(&mut self) {
if let Some((pid, _name, signal)) = self.pending_signal.take() {
let (success, message) = self.send_signal(pid, signal);
self.signal_result = Some((success, message, std::time::Instant::now()));
}
}
pub fn cancel_signal(&mut self) {
self.pending_signal = None;
}
pub fn clear_old_signal_result(&mut self) {
if let Some((_, _, timestamp)) = &self.signal_result {
if timestamp.elapsed() > std::time::Duration::from_secs(3) {
self.signal_result = None;
}
}
}
pub fn cycle_interface(&mut self) {
let iface_count = self.snapshot_networks.len();
if iface_count == 0 {
self.selected_interface_index = 0;
return;
}
self.selected_interface_index = (self.selected_interface_index + 1) % iface_count;
}
#[must_use]
pub fn selected_interface_name(&self) -> Option<&str> {
self.snapshot_networks
.get(self.selected_interface_index)
.map(|info| info.name.as_str())
}
#[must_use]
pub fn selected_interface_data(&self) -> Option<&NetworkInfo> {
self.snapshot_networks.get(self.selected_interface_index)
}
pub fn cycle_files_view_mode(&mut self) {
self.files_view_mode = self.files_view_mode.next();
}
pub fn toggle_panel(&mut self, panel: PanelType) {
let is_visible = self.is_panel_visible(panel);
if is_visible {
if self.focused_panel == Some(panel) {
self.collapse_memory = Some(panel);
self.set_panel_visible(panel, false);
let visible = self.visible_panels();
self.focused_panel = visible.first().copied();
} else {
self.set_panel_visible(panel, false);
}
} else {
self.set_panel_visible(panel, true);
if self.collapse_memory == Some(panel) {
self.focused_panel = Some(panel);
self.collapse_memory = None;
}
}
}
fn is_panel_visible(&self, panel: PanelType) -> bool {
match panel {
PanelType::Cpu => self.panels.cpu,
PanelType::Memory => self.panels.memory,
PanelType::Disk => self.panels.disk,
PanelType::Network => self.panels.network,
PanelType::Process => self.panels.process,
PanelType::Gpu => self.panels.gpu,
PanelType::Sensors => self.panels.sensors,
PanelType::Connections => self.panels.connections,
PanelType::Psi => self.panels.psi,
PanelType::Battery => self.panels.battery,
PanelType::Files => self.panels.files,
PanelType::Containers => false, }
}
fn set_panel_visible(&mut self, panel: PanelType, visible: bool) {
match panel {
PanelType::Cpu => self.panels.cpu = visible,
PanelType::Memory => self.panels.memory = visible,
PanelType::Disk => self.panels.disk = visible,
PanelType::Network => self.panels.network = visible,
PanelType::Process => self.panels.process = visible,
PanelType::Gpu => self.panels.gpu = visible,
PanelType::Sensors => self.panels.sensors = visible,
PanelType::Connections => self.panels.connections = visible,
PanelType::Psi => self.panels.psi = visible,
PanelType::Battery => self.panels.battery = visible,
PanelType::Files => self.panels.files = visible,
PanelType::Containers => {} }
}
#[cfg(unix)]
fn send_signal(&self, pid: u32, signal: SignalType) -> (bool, String) {
use std::process::Command;
let output = Command::new("kill")
.arg(format!("-{}", signal.number()))
.arg(pid.to_string())
.output();
match output {
Ok(result) if result.status.success() => {
(true, format!("Sent SIG{} to PID {}", signal.name(), pid))
}
Ok(result) => {
let stderr = String::from_utf8_lossy(&result.stderr);
(
false,
format!(
"Failed to send SIG{} to {}: {}",
signal.name(),
pid,
stderr.trim()
),
)
}
Err(e) => (
false,
format!("Failed to send SIG{} to {}: {}", signal.name(), pid, e),
),
}
}
#[cfg(not(unix))]
fn send_signal(&self, pid: u32, signal: SignalType) -> (bool, String) {
(
false,
format!(
"Signal {} not supported on this platform (PID {})",
signal.name(),
pid
),
)
}
pub fn sorted_processes(&self) -> Vec<&sysinfo::Process> {
if self.deterministic {
return Vec::new();
}
let mut procs: Vec<_> = self
.system
.processes()
.values()
.filter(|p| {
if self.filter.is_empty() {
true
} else {
let name = p.name().to_string_lossy().to_lowercase();
name.contains(&self.filter.to_lowercase())
}
})
.collect();
procs.sort_by(|a, b| {
let cmp = match self.sort_column {
ProcessSortColumn::Pid => a.pid().as_u32().cmp(&b.pid().as_u32()),
ProcessSortColumn::User => {
let ua = a.user_id().map(|u| u.to_string()).unwrap_or_default();
let ub = b.user_id().map(|u| u.to_string()).unwrap_or_default();
ua.cmp(&ub)
}
ProcessSortColumn::Cpu => a
.cpu_usage()
.partial_cmp(&b.cpu_usage())
.unwrap_or(std::cmp::Ordering::Equal),
ProcessSortColumn::Mem => a.memory().cmp(&b.memory()),
ProcessSortColumn::Command => {
let na = a.name().to_string_lossy();
let nb = b.name().to_string_lossy();
na.cmp(&nb)
}
};
if self.sort_descending {
cmp.reverse()
} else {
cmp
}
});
procs
}
pub fn disk_io_data(&self) -> Option<&super::analyzers::DiskIoData> {
self.snapshot_disk_io.as_ref()
}
pub fn uptime(&self) -> u64 {
if self.deterministic {
self.fixed_uptime
} else {
System::uptime()
}
}
}
fn read_hostname() -> String {
std::fs::read_to_string("/etc/hostname")
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
fn read_kernel_version() -> String {
std::fs::read_to_string("/proc/version")
.map(|s| s.split_whitespace().take(3).collect::<Vec<_>>().join(" "))
.unwrap_or_else(|_| "Linux".to_string())
}
fn detect_container() -> bool {
std::path::Path::new("/.dockerenv").exists()
|| std::fs::read_to_string("/proc/1/cgroup")
.map(|s| s.contains("docker") || s.contains("containerd"))
.unwrap_or(false)
}