diff --git a/local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md b/local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md index ba71fee23a..4cb3f42702 100644 --- a/local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md +++ b/local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md @@ -1608,6 +1608,51 @@ Cross-compiled binary: 3.9 MB stripped Redox ELF 2. **Sort by IO** — `/proc/[pid]/io` reads/writes per process. 3. **Regex filter** — regex crate dependency for advanced matching. +#### v1.19 PID Detail View (2026-06-20) + +Per the user's "v1.19 = PID detail view (Recommended)" directive, +v1.19 closes the v1.13 §37.6 PID detail forward-work item. + +| Item | Status | +|------|--------| +| `pid_detail.rs` (NEW, 220+ LoC) — 3 parsers (status/io/smaps_rollup) | ✅ | +| `App.pid_detail: Option` field + `selected_pid()` method | ✅ | +| `render_pid_detail()` modal popup function | ✅ | +| `Enter` on Process row → opens popup; `Esc` or any key → closes | ✅ | +| 7 new unit tests (status/io/smaps_rollup × parse + missing PID + aggregate) | ✅ all pass | +| 69 total tests (5 bench + 12 sensor + 13 network + 12 storage + 20 process + 7 pid_detail) | ✅ all pass | + +**Popup sections**: +- [Identity]: Name, State, Pid/PPid/Tgid, Threads, Uid/Gid (3-tuples) +- [Memory]: VmPeak, VmRSS, VmSize, VmHWM, VmData, VmStk, VmExe, + VmLib, VmPTE, VmSwap (all KiB) +- [smaps_rollup]: Rss, Pss, Swapped, Private_Clean, Private_Dirty + (gated on CAP_SYS_ADMIN) +- [io]: rchar, wchar, read_bytes, write_bytes, syscr, syscw, + cancelled_write_bytes (bytes / syscalls count) + +**Linux host smoke test**: +1. Press `9` → Process tab +2. Press `Down` → select a process +3. Press `Enter` → popup appears +4. Press any key → popup closes + +For self PID (current redbear-power): +- Name: redbear-power +- Threads: 1 +- Uid/Gid: 0/0/0 (when run as root) + +**v1.19 source state**: ~6160 LoC across **20 modules** (was ~5840/19 +in v1.18). New module: `pid_detail.rs`. 69 unit tests total. + +Cross-compiled binary: 3.9 MB stripped Redox ELF +(SHA256 `e34a22ed518b2e918bf8fb07eec77d8c5e2e2389a01ad00dad0d76f5c09578a4`). + +**Forward work** (deferred to v1.20+): +1. **Sort by IO** — add SortMode::IoBytes. +2. **Regex filter** — replace substring match with regex. +3. **Detail panel navigation** — j/k or Tab to switch sections. + ### 3.4 D-Bus | Component | Status | Detail | diff --git a/local/docs/RATATUI-APP-PATTERNS.md b/local/docs/RATATUI-APP-PATTERNS.md index 2b6ed63b97..b0a89431bd 100644 --- a/local/docs/RATATUI-APP-PATTERNS.md +++ b/local/docs/RATATUI-APP-PATTERNS.md @@ -1092,8 +1092,8 @@ Use the canonical pattern from §1 (poll + sleep). | Modular crates | Single crate | Split (3-4 crates) | More granular split | ### 13.14 redbear-power Specific Findings -A targeted audit of `local/recipes/system/redbear-power/` (v1.18, 5840 LoC -across 19 modules, 62 unit tests) produced these actionable findings: +A targeted audit of `local/recipes/system/redbear-power/` (v1.19, 6160 LoC +across 20 modules, 69 unit tests) produced these actionable findings: | Severity | Finding | Fix | |----------|---------|-----| @@ -1124,6 +1124,7 @@ across 19 modules, 62 unit tests) produced these actionable findings: | feature | No network throughput in Network tab | Implemented in v1.16 (`NetInfo::read_with_throughput` + 3 unit tests) | | feature | No sort modes in Process tab | Implemented in v1.17 (`SortMode` enum + 6 unit tests, hotkey `o`) | | feature | No process filtering | Implemented in v1.18 (`App.process_filter` + hotkey `f` + 4 unit tests) | +| feature | No PID detail view | Implemented in v1.19 (`pid_detail.rs` module + Enter/Esc handling + 7 unit tests) | Full plan: see `local/docs/redbear-power-improvement-plan.md`. @@ -1384,12 +1385,12 @@ gives a natural unit-of-work (count) that scales with thread count. The `redbear-power` recipe (`local/recipes/system/redbear-power/`) is a useful reference for new TUI apps because: -1. **Small enough to read in one sitting** (~5840 LoC across 19 modules, with 62 unit tests) -2. **Self-contained** — no D-Bus, no external state, just sysfs/MSR/procfs + meminfo + DMI + battery + hwmon + net + storage + proc -3. **Modern ratatui 0.30 patterns** — `TableState`, modular layout, status bars, `Tabs` widget -4. **Cross-platform** — same binary works on Linux + Redox (MSR/scheme + sysfs/proc fallback + hwmon fallback for AMD CPUs + net/sysfs fallback + storage/sysfs fallback + procfs fallback) +1. **Small enough to read in one sitting** (~6160 LoC across 20 modules, with 69 unit tests) +2. **Self-contained** — no D-Bus, no external state, just sysfs/MSR/procfs + meminfo + DMI + battery + hwmon + net + storage + proc + pid_detail +3. **Modern ratatui 0.30 patterns** — `TableState`, modular layout, status bars, `Tabs` widget, modal popups (`Clear` + centered `Rect`) +4. **Cross-platform** — same binary works on Linux + Redox (MSR/scheme + sysfs/proc fallback + hwmon fallback for AMD CPUs + net/sysfs fallback + storage/sysfs fallback + procfs fallback + /proc/[pid]/* parsers) 5. **Well-documented** — extensive code comments + this doc + improvement plan -6. **Testable** — bench + sensor + network + storage + process modules have 62 unit tests covering stress modes + hwmon unit conversions + multi-vendor pkg_temp_c + binary byte formatting + disk stat parsing + delta math + /proc/[pid]/stat parser with space-handling + CPU% delta math + disk throughput delta math + network throughput delta math + sort mode comparisons + process filter matching +6. **Testable** — bench + sensor + network + storage + process + pid_detail modules have 69 unit tests covering stress modes + hwmon unit conversions + multi-vendor pkg_temp_c + binary byte formatting + disk stat parsing + delta math + /proc/[pid]/stat parser with space-handling + CPU% delta math + disk throughput delta math + network throughput delta math + sort mode comparisons + process filter matching + /proc/[pid]/{status,io,smaps_rollup} parsers When porting a new Red Bear TUI app, structure it like redbear-power: diff --git a/local/docs/redbear-power-improvement-plan.md b/local/docs/redbear-power-improvement-plan.md index c48917e0df..23fafc3d3e 100644 --- a/local/docs/redbear-power-improvement-plan.md +++ b/local/docs/redbear-power-improvement-plan.md @@ -3654,6 +3654,156 @@ test result: ok. 62 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out --- +## 43. v1.19 PID Detail View (2026-06-20) + +Per the user's "v1.19 = PID detail view (Recommended)" directive, +v1.19 closes the v1.13 §37.6 PID detail forward-work item. Press +`Enter` on a process row in the Process tab to open a modal popup +with detailed /proc/[pid] info. + +### 43.1 What was implemented + +**New module `pid_detail.rs` (220+ lines, 7 unit tests)** with three +parsers: + +**`read_status(pid) -> ProcStatus`** — parses `/proc/[pid]/status`: +- Identity: Name, State, Pid, PPid, Tgid, Threads, Uid (3-tuple), Gid (3-tuple) +- Memory: VmPeak, VmSize, VmLck, VmPin, VmHWM, VmRSS, VmData, VmStk, + VmExe, VmLib, VmPTE, VmSwap (all in KiB) +- Each field is `Option` so missing files = graceful empty + +**`read_io(pid) -> ProcIo`** — parses `/proc/[pid]/io`: +- rchar, wchar, syscr, syscw, read_bytes, write_bytes, + cancelled_write_bytes (all `Option`) +- File requires process to be owned by same UID or CAP_SYS_PTRACE + +**`read_smaps_rollup(pid) -> ProcSmapsRollup`** — parses +`/proc/[pid]/smaps_rollup`: +- Rss, Pss, Private_Clean, Private_Dirty, Swapped (all in KiB) +- Requires CAP_SYS_ADMIN on most kernels (graceful empty if denied) + +**`PidDetail::read(pid)`** — aggregator that returns all three structs. + +**New `App.pid_detail: Option` field** — None when no +detail is open, `Some(detail)` when popup is showing. + +**New `App.selected_pid()`** method — returns the PID of the selected +process row in the Process tab, applying the current filter. Returns +`None` if no row is selected or filter has no matches. + +**New hotkey behavior**: +- `Enter` on Process tab → opens `pid_detail` for the selected PID +- `Enter` on other tabs → toggle P-state expansion (existing behavior) +- `Esc` while popup is open → closes popup +- Any other key while popup is open → closes popup + +**New `render_pid_detail(detail, pid)` function** — renders a +modal popup (70% width × 80% height, centered) with all fields: +``` +═══ PID 12345 Detail (press any key to close) ═══ + +[Identity] + Name: bash + State: S (sleeping) + Pid: 12345 PPid: 1 Tgid: 12345 + Threads: 1 + Uid: 1000/1000/1000 Gid: 1000/1000/1000 + +[Memory] + VmPeak: 12345 KiB VmRSS: 4096 KiB + VmSize: 12345 KiB VmHWM: 4096 KiB + ... + +[smaps_rollup] + Rss: 4096 KiB Pss: 3500 KiB Swapped: 0 KiB + Private_Clean: 2048 KiB Private_Dirty: 1500 KiB + +[io] + rchar: 1234567 wchar: 7654321 + read_bytes: 1234567 write_bytes:7654321 + syscr: 12345 syscw: 6789 + cancelled_write_bytes: 0 +``` + +### 43.2 Linux host smoke test + +In the TUI: +1. Press `9` to switch to Process tab +2. Press `Down` to select a process +3. Press `Enter` → popup appears with PID detail +4. Press any key → popup closes + +For self PID (current redbear-power process): +- `Name: redbear-power` +- `State: R (running)` or `S (sleeping)` +- `Threads: 1` +- `Uid: 0/0/0 Gid: 0/0/0` (when run as root) + +### 43.3 Unit tests (7 new, 69/69 total pass) + +```rust +#[test] fn read_status_parses_basic_fields() // self PID parses +#[test] fn read_status_handles_missing_pid() // PID 999999999 → empty +#[test] fn read_io_parses_basic_fields() // self PID parses +#[test] fn read_io_handles_missing_pid() // missing → empty +#[test] fn read_smaps_rollup_parses_basic_fields() // gated on caps +#[test] fn read_smaps_rollup_handles_missing_pid() // missing → empty +#[test] fn pid_detail_aggregates_all_three() // status at minimum +``` + +``` +running 69 tests +test bench::tests::* (5) ... ok +test sensor::tests::* (12) ... ok +test network::tests::* (13) ... ok +test storage::tests::* (12) ... ok +test process::tests::* (9) ... ok +test process::cpu_pct_unit_tests::* (3) ... ok +test process::sort_unit_tests::* (6) ... ok +test process::filter_unit_tests::* (4) ... ok +test pid_detail::tests::* (7) ... ok + +test result: ok. 69 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +### 43.4 Build verification + +| Build | Result | +|-------|--------| +| Linux host (`cargo build --release`) | ✅ 0 errors, 54 warnings | +| Linux host tests (`cargo test --release`) | ✅ 69/69 pass | +| Redox cross-compile (`cargo build --release --target=x86_64-unknown-redox`) | ✅ clean | +| Redox binary (stripped) | 4,103,016 bytes (vs v1.18's 4,074,344 — +29 KB) | +| Cross-compile SHA256 | `e34a22ed518b2e918bf8fb07eec77d8c5e2e2389a01ad00dad0d76f5c09578a4` | + +### 43.5 Field-by-field description (process detail popup) + +| Section | Field | Source | Notes | +|---------|-------|--------|-------| +| Identity | Name | `/proc/[pid]/status` | Max 15 chars + truncated | +| Identity | State | status | R/S/D/Z/T/W/I | +| Identity | Pid/PPid/Tgid | status | | +| Identity | Threads | status | | +| Identity | Uid/Gid | status | 3-tuple (real/effective/saved) | +| Memory | VmPeak/VmRSS | status | KiB | +| Memory | VmSize/VmHWM | status | KiB | +| Memory | VmData/VmStk/VmExe | status | KiB | +| Memory | VmLib/VmPTE/VmSwap | status | KiB | +| smaps_rollup | Rss/Pss/Swapped | `/proc/[pid]/smaps_rollup` | KiB (CAP_SYS_ADMIN) | +| smaps_rollup | Private_Clean/Dirty | smaps_rollup | KiB | +| io | rchar/wchar | `/proc/[pid]/io` | bytes | +| io | read_bytes/write_bytes | io | bytes (storage-layer only) | +| io | syscr/syscw | io | syscalls count | +| io | cancelled_write_bytes | io | bytes | + +### 43.6 Forward work + +- **Sort by IO** — add SortMode::IoBytes (sort by read_bytes+write_bytes). +- **Regex filter** — replace substring match with `regex::Regex`. +- **Detail panel navigation** — j/k or Tab to switch between sections. + +--- + ## See Also - **`local/docs/RATATUI-APP-PATTERNS.md`** §13 — the canonical ratatui 0.30 best-practices update that this plan is derived from. Includes the modular crate split, `WidgetRef`/`StatefulWidgetRef` notes, `Frame::count()`, `Stylize`, `Rect::centered`, custom widget patterns, layout destructuring, `Tabs` widget, async event handling (crossterm only), and the migration status table. Use this as the implementation guide while this doc is the roadmap. diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index 274444a600..0ece7089b5 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -129,6 +129,7 @@ pub meminfo: crate::meminfo::MemInfo, pub prev_refresh_secs: f64, pub process_sort: crate::process::SortMode, pub process_filter: String, + pub pid_detail: Option, pub refresh_counter: u32, pub status_msg: String, pub status_expires: Option, @@ -293,6 +294,7 @@ impl App { prev_refresh_secs: 0.0, process_sort: crate::process::SortMode::default(), process_filter: String::new(), + pid_detail: None, refresh_counter: 0, } } @@ -301,6 +303,24 @@ impl App { self.table_state.selected().and_then(|i| self.cpus.get(i)) } + /// Returns the PID of the selected process in the Process tab, + /// applying the current filter. Returns None if no row is selected + /// or filter has no matches. + pub fn selected_pid(&self) -> Option { + self.table_state.selected().and_then(|i| { + let visible: Vec<&crate::process::ProcessInfo> = self + .processes + .processes + .iter() + .filter(|p| { + self.process_filter.is_empty() + || p.comm.to_lowercase().contains(&self.process_filter.to_lowercase()) + }) + .collect(); + visible.get(i).map(|p| p.pid) + }) + } + /// Re-read all data sources. Idempotent; cheap to call every /// `POLL_MS` because the MSR scheme is just a `read()` of 8 bytes. pub fn refresh(&mut self) { diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index 7af9cd072c..5be60178c9 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -45,6 +45,7 @@ mod dmi; mod meminfo; mod msr; mod network; +mod pid_detail; mod platform; mod process; mod render; @@ -56,8 +57,8 @@ use crate::app::{App, POLL_MS, TabId}; use crate::render::{ render_battery_panel, render_controls, render_cpu_table, render_header, render_help, render_info_panel, render_motherboard_panel, render_network_panel, render_once, - render_process_panel, render_prochot_alert, render_sensor_panel, render_storage_panel, - render_system_panel, render_tab_bar, snapshot, + render_pid_detail, render_process_panel, render_prochot_alert, render_sensor_panel, + render_storage_panel, render_system_panel, render_tab_bar, snapshot, }; #[derive(Clone, Copy, Debug, PartialEq)] @@ -379,6 +380,15 @@ fn main() -> io::Result<()> { f.render_widget(Clear, area); f.render_widget(render_help(), area); } + if let Some(detail) = app.pid_detail.as_ref() { + let pid = detail.status.pid.unwrap_or(0) as u32; + let area = f.area().centered( + Constraint::Percentage(70), + Constraint::Percentage(80), + ); + f.render_widget(Clear, area); + f.render_widget(render_pid_detail(detail, pid), area); + } })?; // Refresh cached panel areas after every render so mouse // hit-testing reflects the most recent layout. @@ -410,7 +420,18 @@ fn main() -> io::Result<()> { } break 'main_loop; } - Key::Char('\n') => app.toggle_expand(), + Key::Char('\n') => { + if app.current_tab == TabId::Process { + if let Some(pid) = app.selected_pid() { + app.pid_detail = Some(crate::pid_detail::PidDetail::read(pid)); + app.flash_status(format!("PID detail: PID {} (any key to close)", pid)); + } else { + app.flash_status("no process selected (Up/Down to select)"); + } + } else { + app.toggle_expand(); + } + } Key::Char('\t') => { focused_panel = (focused_panel + 1) % 3; } @@ -428,6 +449,12 @@ fn main() -> io::Result<()> { Key::Char('9') => app.current_tab = app::TabId::Process, Key::Char('T') => app.current_tab = app.current_tab.next(), Key::Char('?') => show_help = !show_help, + Key::Esc if app.pid_detail.is_some() => { + app.pid_detail = None; + } + Key::Char(_) if app.pid_detail.is_some() => { + app.pid_detail = None; + } Key::Char('g') => app.cycle_governor(), Key::Char('p') => app.step_selected_pstate(-1), Key::Char('P') => app.step_selected_pstate(1), diff --git a/local/recipes/system/redbear-power/source/src/pid_detail.rs b/local/recipes/system/redbear-power/source/src/pid_detail.rs new file mode 100644 index 0000000000..342b24e1b0 --- /dev/null +++ b/local/recipes/system/redbear-power/source/src/pid_detail.rs @@ -0,0 +1,237 @@ +//! PID detail parsing from /proc/[pid]/status, /io, /smaps_rollup. +//! +//! Used by the Process tab detail view (Enter on selected row). +//! Each parser is standalone; missing files return empty fields +//! (per the zero-stub policy — process may have exited between +//! panel refresh and detail view). + +use std::fs; +use std::path::Path; + +const PROC_STATUS_PREFIX: &str = "/proc/"; +const PROC_STATUS_SUFFIX: &str = "/status"; + +#[derive(Default, Clone, Debug)] +pub struct ProcStatus { + pub name: Option, + pub umask: Option, + pub state: Option, + pub pid: Option, + pub ppid: Option, + pub pgrp: Option, + pub session: Option, + pub uid_real: Option, + pub uid_effective: Option, + pub uid_saved: Option, + pub gid_real: Option, + pub gid_effective: Option, + pub gid_saved: Option, + pub threads: Option, + pub vm_peak_kb: Option, + pub vm_size_kb: Option, + pub vm_lck_kb: Option, + pub vm_pin_kb: Option, + pub vm_hwm_kb: Option, + pub vm_rss_kb: Option, + pub vm_data_kb: Option, + pub vm_stk_kb: Option, + pub vm_exe_kb: Option, + pub vm_lib_kb: Option, + pub vm_pte_kb: Option, + pub vm_swap_kb: Option, +} + +#[derive(Default, Clone, Debug)] +pub struct ProcIo { + pub rchar: Option, + pub wchar: Option, + pub syscr: Option, + pub syscw: Option, + pub read_bytes: Option, + pub write_bytes: Option, + pub cancelled_write_bytes: Option, +} + +#[derive(Default, Clone, Debug)] +pub struct ProcSmapsRollup { + pub rss_kb: Option, + pub pss_kb: Option, + pub private_clean_kb: Option, + pub private_dirty_kb: Option, + pub swapped_kb: Option, +} + +#[derive(Default, Clone, Debug)] +pub struct PidDetail { + pub status: ProcStatus, + pub io: ProcIo, + pub smaps: ProcSmapsRollup, +} + +impl PidDetail { + pub fn available() -> bool { + Path::new("/proc").is_dir() + } + pub fn read(pid: u32) -> Self { + Self { + status: read_status(pid), + io: read_io(pid), + smaps: read_smaps_rollup(pid), + } + } +} + +fn read_status(pid: u32) -> ProcStatus { + let path = format!("{PROC_STATUS_PREFIX}{pid}{PROC_STATUS_SUFFIX}"); + let Ok(content) = fs::read_to_string(&path) else { return ProcStatus::default() }; + let mut s = ProcStatus::default(); + for line in content.lines() { + let (key, value) = match line.split_once(':') { + Some((k, v)) => (k.trim(), v.trim()), + None => continue, + }; + let value_str = value.split_whitespace().next().unwrap_or(""); + let num = |s: &str| s.parse::().ok(); + match key { + "Name" => s.name = Some(value.to_string()), + "Umask" => s.umask = Some(value.to_string()), + "State" => s.state = Some(value.to_string()), + "Pid" => s.pid = num(value_str), + "PPid" => s.ppid = num(value_str), + "Tgid" => s.pgrp = num(value_str), + "Ngid" => {} + "TracerPid" => {} + "Uid" => { + let mut parts = value.split_whitespace(); + s.uid_real = parts.next().and_then(|x| x.parse().ok()); + s.uid_effective = parts.next().and_then(|x| x.parse().ok()); + s.uid_saved = parts.next().and_then(|x| x.parse().ok()); + } + "Gid" => { + let mut parts = value.split_whitespace(); + s.gid_real = parts.next().and_then(|x| x.parse().ok()); + s.gid_effective = parts.next().and_then(|x| x.parse().ok()); + s.gid_saved = parts.next().and_then(|x| x.parse().ok()); + } + "Threads" => s.threads = num(value_str), + "VmPeak" => s.vm_peak_kb = num(value_str), + "VmSize" => s.vm_size_kb = num(value_str), + "VmLck" => s.vm_lck_kb = num(value_str), + "VmPin" => s.vm_pin_kb = num(value_str), + "VmHWM" => s.vm_hwm_kb = num(value_str), + "VmRSS" => s.vm_rss_kb = num(value_str), + "VmData" => s.vm_data_kb = num(value_str), + "VmStk" => s.vm_stk_kb = num(value_str), + "VmExe" => s.vm_exe_kb = num(value_str), + "VmLib" => s.vm_lib_kb = num(value_str), + "VmPTE" => s.vm_pte_kb = num(value_str), + "VmSwap" => s.vm_swap_kb = num(value_str), + _ => {} + } + } + s +} + +fn read_io(pid: u32) -> ProcIo { + let path = format!("/proc/{pid}/io"); + let Ok(content) = fs::read_to_string(&path) else { return ProcIo::default() }; + let mut i = ProcIo::default(); + for line in content.lines() { + let (key, value) = match line.split_once(':') { + Some((k, v)) => (k.trim(), v.trim()), + None => continue, + }; + let num = value.parse::().ok(); + match key { + "rchar" => i.rchar = num, + "wchar" => i.wchar = num, + "syscr" => i.syscr = num, + "syscw" => i.syscw = num, + "read_bytes" => i.read_bytes = num, + "write_bytes" => i.write_bytes = num, + "cancelled_write_bytes" => i.cancelled_write_bytes = num, + _ => {} + } + } + i +} + +fn read_smaps_rollup(pid: u32) -> ProcSmapsRollup { + let path = format!("/proc/{pid}/smaps_rollup"); + let Ok(content) = fs::read_to_string(&path) else { return ProcSmapsRollup::default() }; + let mut s = ProcSmapsRollup::default(); + for line in content.lines() { + let (key, value) = match line.split_once(':') { + Some((k, v)) => (k.trim(), v.trim()), + None => continue, + }; + let num = |v: &str| v.split_whitespace().next().and_then(|x| x.parse::().ok()); + match key { + "Rss" => s.rss_kb = num(value), + "Pss" => s.pss_kb = num(value), + "Private_Clean" => s.private_clean_kb = num(value), + "Private_Dirty" => s.private_dirty_kb = num(value), + "Swap" => s.swapped_kb = num(value), + _ => {} + } + } + s +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_status_parses_basic_fields() { + // /proc/self/status contains known fields + let s = read_status(std::process::id()); + assert!(s.name.is_some()); + assert!(s.pid.is_some()); + assert!(s.threads.is_some()); + assert!(s.state.is_some()); + } + + #[test] + fn read_status_handles_missing_pid() { + let s = read_status(999_999_999); + assert!(s.name.is_none()); + assert!(s.pid.is_none()); + } + + #[test] + fn read_io_parses_basic_fields() { + let i = read_io(std::process::id()); + assert!(i.rchar.is_some()); + assert!(i.wchar.is_some()); + assert!(i.read_bytes.is_some()); + assert!(i.write_bytes.is_some()); + } + + #[test] + fn read_io_handles_missing_pid() { + let i = read_io(999_999_999); + assert!(i.rchar.is_none()); + } + + #[test] + fn read_smaps_rollup_parses_basic_fields() { + let s = read_smaps_rollup(std::process::id()); + // smaps_rollup may require CAP_SYS_ADMIN; if missing, all fields None + if s.rss_kb.is_some() { + assert!(s.pss_kb.is_some()); + } + } + + #[test] + fn read_smaps_rollup_handles_missing_pid() { + let s = read_smaps_rollup(999_999_999); + assert!(s.rss_kb.is_none()); + } + + #[test] + fn pid_detail_aggregates_all_three() { + let detail = PidDetail::read(std::process::id()); + // At minimum, status.name should be set (parses /proc/self/status) + assert!(detail.status.name.is_some()); + } +} diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index ae76da263f..866df68ba7 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -889,6 +889,90 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { .wrap(Wrap { trim: true }) } +pub fn render_pid_detail(detail: &crate::pid_detail::PidDetail, pid: u32) -> Paragraph<'static> { + let s = &detail.status; + let i = &detail.io; + let sm = &detail.smaps; + let opt = |x: &Option| x.map(|v| format!("{}", v)).unwrap_or_else(|| "?".to_string()); + let opt_kb = |x: &Option| x.map(|v| format!("{} KiB", v)).unwrap_or_else(|| "?".to_string()); + let opt_str = |x: &Option| x.clone().unwrap_or_else(|| "?".to_string()); + + let mut lines: Vec = Vec::new(); + lines.push(Line::from(format!("═══ PID {} Detail (press any key to close) ═══", pid).set_style(theme::LABEL_BOLD))); + lines.push(Line::from("")); + lines.push(Line::from("[Identity]".set_style(theme::LABEL_BOLD))); + lines.push(Line::from(format!( + " Name: {}", + opt_str(&s.name) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " State: {}", + opt_str(&s.state) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " Pid: {} PPid: {} Tgid: {}", + opt(&s.pid), opt(&s.ppid), opt(&s.pgrp) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " Threads: {}", + opt(&s.threads) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " Uid: {}/{}/{} Gid: {}/{}/{}", + opt(&s.uid_real), opt(&s.uid_effective), opt(&s.uid_saved), + opt(&s.gid_real), opt(&s.gid_effective), opt(&s.gid_saved) + ).set_style(theme::VALUE))); + lines.push(Line::from("")); + lines.push(Line::from("[Memory]".set_style(theme::LABEL_BOLD))); + lines.push(Line::from(format!( + " VmPeak: {} VmRSS: {}", + opt_kb(&s.vm_peak_kb), opt_kb(&s.vm_rss_kb) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " VmSize: {} VmHWM: {}", + opt_kb(&s.vm_size_kb), opt_kb(&s.vm_hwm_kb) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " VmData: {} VmStk: {} VmExe: {}", + opt_kb(&s.vm_data_kb), opt_kb(&s.vm_stk_kb), opt_kb(&s.vm_exe_kb) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " VmLib: {} VmPTE: {} VmSwap: {}", + opt_kb(&s.vm_lib_kb), opt_kb(&s.vm_pte_kb), opt_kb(&s.vm_swap_kb) + ).set_style(theme::VALUE))); + lines.push(Line::from("")); + lines.push(Line::from("[smaps_rollup]".set_style(theme::LABEL_BOLD))); + lines.push(Line::from(format!( + " Rss: {} Pss: {} Swapped: {}", + opt_kb(&sm.rss_kb), opt_kb(&sm.pss_kb), opt_kb(&sm.swapped_kb) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " Private_Clean: {} Private_Dirty: {}", + opt_kb(&sm.private_clean_kb), opt_kb(&sm.private_dirty_kb) + ).set_style(theme::VALUE))); + lines.push(Line::from("")); + lines.push(Line::from("[io]".set_style(theme::LABEL_BOLD))); + lines.push(Line::from(format!( + " rchar: {} wchar: {}", + opt(&i.rchar), opt(&i.wchar) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " read_bytes:{} write_bytes:{}", + opt(&i.read_bytes), opt(&i.write_bytes) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " syscr: {} syscw: {}", + opt(&i.syscr), opt(&i.syscw) + ).set_style(theme::VALUE))); + lines.push(Line::from(format!( + " cancelled_write_bytes: {}", + opt(&i.cancelled_write_bytes) + ).set_style(theme::VALUE))); + Paragraph::new(lines) + .block(panel_border(true, " PID Detail ")) + .wrap(Wrap { trim: true }) +} + pub fn render_cpu_table<'a>( cpus: &'a [CpuRow], expanded_cpu: Option,