diff --git a/README.md b/README.md index e7e11e0..2fc1fc1 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ nix build .#usage Each gauge fills against an auto-scaling peak (nice 1/2/5×10ⁿ ceiling) so idle and busy machines both stay readable. Heat colors shift accent → warning → destructive as the arc fills. -Below the gauges: cards listing individual disks and network interfaces with mini read/write bars, plus a **process table** (name, disk write rate, write syscall frequency) from `/proc/[pid]/io`. +Below the gauges: cards listing individual disks and network interfaces with mini read/write bars, plus a **process table** (name, path, disk write rate, write syscall frequency) from `/proc/[pid]/{io,exe}`. ## Requirements diff --git a/src/app.rs b/src/app.rs index 33558cd..51c5b78 100644 --- a/src/app.rs +++ b/src/app.rs @@ -258,42 +258,15 @@ impl UsageApp { ui.horizontal(|ui| { ui.set_min_height(row_h); ui.add_space(th.spacing.sm); - - // Name (flex) - let name_w = (ui.available_width() - 220.0).max(80.0); - let name_label = truncate_name(&p.name, 48); - ui.add_sized( - [name_w, row_h], - egui::Label::new( - RichText::new(name_label) - .size(th.type_scale.body) - .color(th.palette.text), - ) - .truncate(), - ); - - // Write rate - let write_txt = format_bps(p.write_bps); - ui.add_sized( - [100.0, row_h], - egui::Label::new( - RichText::new(write_txt) - .size(th.type_scale.body) - .color(th.palette.text), - ) - .halign(Align::RIGHT), - ); - - // Write frequency (syscalls/s) - let freq_txt = format_hz(p.write_freq); - ui.add_sized( - [90.0, row_h], - egui::Label::new( - RichText::new(freq_txt) - .size(th.type_scale.body) - .color(th.palette.text_secondary), - ) - .halign(Align::RIGHT), + proc_row( + ui, + th, + row_h, + false, + &p.name, + &p.path, + &format_bps(p.write_bps), + &format_hz(p.write_freq), ); }); }); @@ -324,50 +297,147 @@ fn header_row(ui: &mut egui::Ui, th: &Theme, row_h: f32) { ui.horizontal(|ui| { ui.set_min_height(row_h); ui.add_space(th.spacing.sm); - let name_w = (ui.available_width() - 220.0).max(80.0); - ui.add_sized( - [name_w, row_h], - egui::Label::new( - RichText::new("Name") - .size(th.type_scale.caption) - .strong() - .color(th.palette.text_secondary), - ), - ); - ui.add_sized( - [100.0, row_h], - egui::Label::new( - RichText::new("Write") - .size(th.type_scale.caption) - .strong() - .color(th.palette.text_secondary), - ) - .halign(Align::RIGHT), - ); - ui.add_sized( - [90.0, row_h], - egui::Label::new( - RichText::new("Write freq") - .size(th.type_scale.caption) - .strong() - .color(th.palette.text_secondary), - ) - .halign(Align::RIGHT), + proc_row( + ui, + th, + row_h, + true, + "Name", + "Path", + "Write", + "Write freq", ); }); }); } -fn truncate_name(name: &str, max_chars: usize) -> String { - let count = name.chars().count(); - if count <= max_chars { +/// Shared layout: Name | Path | Write | Write freq. +fn proc_row( + ui: &mut egui::Ui, + th: &Theme, + row_h: f32, + is_header: bool, + name: &str, + path: &str, + write: &str, + freq: &str, +) { + const NAME_W: f32 = 140.0; + const WRITE_W: f32 = 100.0; + const FREQ_W: f32 = 90.0; + + let name_style = |s: &str| { + if is_header { + RichText::new(s) + .size(th.type_scale.caption) + .strong() + .color(th.palette.text_secondary) + } else { + RichText::new(s) + .size(th.type_scale.body) + .color(th.palette.text) + } + }; + let path_style = |s: &str| { + if is_header { + RichText::new(s) + .size(th.type_scale.caption) + .strong() + .color(th.palette.text_secondary) + } else { + RichText::new(s) + .size(th.type_scale.caption) + .color(th.palette.text_secondary) + } + }; + let metric_style = |s: &str| { + if is_header { + RichText::new(s) + .size(th.type_scale.caption) + .strong() + .color(th.palette.text_secondary) + } else { + RichText::new(s) + .size(th.type_scale.body) + .color(th.palette.text) + } + }; + + let name_label = if is_header { name.to_string() } else { - let t: String = name.chars().take(max_chars.saturating_sub(1)).collect(); + truncate_chars(name, 28) + }; + ui.add_sized( + [NAME_W, row_h], + egui::Label::new(name_style(&name_label)).truncate(), + ); + + let path_w = (ui.available_width() - (WRITE_W + FREQ_W + 8.0)).max(80.0); + let path_label = if is_header { + path.to_string() + } else if path.is_empty() { + "—".into() + } else { + truncate_middle(path, 64) + }; + ui.add_sized( + [path_w, row_h], + egui::Label::new(path_style(&path_label)).truncate(), + ); + + ui.add_sized( + [WRITE_W, row_h], + egui::Label::new(metric_style(write)).halign(Align::RIGHT), + ); + ui.add_sized( + [FREQ_W, row_h], + egui::Label::new( + if is_header { + metric_style(freq) + } else { + RichText::new(freq) + .size(th.type_scale.body) + .color(th.palette.text_secondary) + }, + ) + .halign(Align::RIGHT), + ); +} + +fn truncate_chars(s: &str, max_chars: usize) -> String { + let count = s.chars().count(); + if count <= max_chars { + s.to_string() + } else { + let t: String = s.chars().take(max_chars.saturating_sub(1)).collect(); format!("{t}…") } } +/// Keep start and end of a long path: `/very/long/…/file`. +fn truncate_middle(s: &str, max_chars: usize) -> String { + let count = s.chars().count(); + if count <= max_chars { + return s.to_string(); + } + if max_chars < 8 { + return truncate_chars(s, max_chars); + } + let head = (max_chars - 1) / 2; + let tail = max_chars - 1 - head; + let start: String = s.chars().take(head).collect(); + let end: String = s + .chars() + .rev() + .take(tail) + .collect::() + .chars() + .rev() + .collect(); + format!("{start}…{end}") +} + fn device_card( ui: &mut egui::Ui, th: &Theme, diff --git a/src/io.rs b/src/io.rs index 58107b1..7772fd1 100644 --- a/src/io.rs +++ b/src/io.rs @@ -30,6 +30,8 @@ pub struct DeviceIo { pub struct ProcIo { pub pid: u32, pub name: String, + /// Executable path (`/proc/[pid]/exe` or cmdline argv0). + pub path: String, /// Storage write rate (`write_bytes` delta / s). pub write_bps: f64, /// Write syscall rate (`syscw` delta / s) — write frequency. @@ -41,6 +43,7 @@ struct ProcCounters { write_bytes: u64, syscw: u64, name: String, + path: String, } /// Full system snapshot of I/O rates. @@ -265,6 +268,7 @@ fn proc_rates( ProcIo { pid, name: c.name.clone(), + path: c.path.clone(), write_bps, write_freq, } @@ -314,27 +318,47 @@ fn read_proc_counters() -> HashMap { let comm = fs::read_to_string(base.join("comm")) .map(|s| s.trim().to_string()) .unwrap_or_else(|_| format!("[{pid}]")); - // Prefer a short cmdline base when available (no null noise). - let name = match fs::read(base.join("cmdline")) { + + // Full path: prefer resolved /proc/[pid]/exe, else argv0 from cmdline. + let exe_path = fs::read_link(base.join("exe")) + .ok() + .map(|p| p.to_string_lossy().into_owned()) + .filter(|s| !s.is_empty()); + let cmdline0 = match fs::read(base.join("cmdline")) { Ok(bytes) if !bytes.is_empty() => { let first = bytes.split(|&b| b == 0).next().unwrap_or(&bytes); - let s = String::from_utf8_lossy(first); - let leaf = s.rsplit('/').next().unwrap_or(&s); - if leaf.is_empty() { - comm + let s = String::from_utf8_lossy(first).trim().to_string(); + if s.is_empty() { + None } else { - // Keep args off the table — name column only. - leaf.split_whitespace().next().unwrap_or(leaf).to_string() + Some(s) } } - _ => comm, + _ => None, + }; + let path = exe_path + .or_else(|| cmdline0.clone()) + .unwrap_or_default(); + // Short name for the Name column (basename of path or comm). + let name = if !path.is_empty() { + path.rsplit('/').next().unwrap_or(&path).to_string() + } else { + comm }; + // If basename still has spaces (rare), take first token. + let name = name + .split_whitespace() + .next() + .unwrap_or(&name) + .to_string(); + map.insert( pid, ProcCounters { write_bytes, syscw, name, + path, }, ); }