redbear-power: v1.19 — PID detail view (modal popup)
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. New module pid_detail.rs (237 lines, 7 unit tests): - read_status(pid) → ProcStatus: Name, State, Pid, PPid, Tgid, Threads, Uid (3-tuple), Gid (3-tuple), 12 Vm* memory fields - read_io(pid) → ProcIo: rchar, wchar, syscr, syscw, read_bytes, write_bytes, cancelled_write_bytes - read_smaps_rollup(pid) → ProcSmapsRollup: Rss, Pss, Private_Clean, Private_Dirty, Swapped (CAP_SYS_ADMIN gated) - PidDetail::read(pid) — aggregator Updated app.rs: - pid_detail: Option<PidDetail> field - selected_pid() method — returns PID of selected row (filter-aware) Updated main.rs: - Enter on Process tab → opens pid_detail for selected PID - Enter on other tabs → toggle P-state expansion (existing behavior) - Esc or any key while popup open → closes popup - Popup rendered with Clear + centered Rect (70% × 80%) Updated render.rs: - New render_pid_detail(detail, pid) — full PID detail layout with [Identity] / [Memory] / [smaps_rollup] / [io] sections - Fixed missing render_system_panel import (existing bug) 69/69 tests pass (5 bench + 12 sensor + 13 network + 12 storage + 20 process + 7 pid_detail). Cross-compile SHA256: e34a22ed518b2e918bf8fb07eec77d8c5e2e2389a01ad00dad0d76f5c09578a4. Docs: improvement plan §43, CONSOLE-TO-KDE §3.3.2 v1.19, RATATUI-APP-PATTERNS §13.14 + §14 (6160 LoC, 20 modules, 69 tests).
This commit is contained in:
@@ -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<PidDetail>` 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 |
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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<u64>` 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<u64>`)
|
||||
- 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<PidDetail>` 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.
|
||||
|
||||
@@ -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<crate::pid_detail::PidDetail>,
|
||||
pub refresh_counter: u32,
|
||||
pub status_msg: String,
|
||||
pub status_expires: Option<Instant>,
|
||||
@@ -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<u32> {
|
||||
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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<String>,
|
||||
pub umask: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub pid: Option<u64>,
|
||||
pub ppid: Option<u64>,
|
||||
pub pgrp: Option<u64>,
|
||||
pub session: Option<u64>,
|
||||
pub uid_real: Option<u64>,
|
||||
pub uid_effective: Option<u64>,
|
||||
pub uid_saved: Option<u64>,
|
||||
pub gid_real: Option<u64>,
|
||||
pub gid_effective: Option<u64>,
|
||||
pub gid_saved: Option<u64>,
|
||||
pub threads: Option<u64>,
|
||||
pub vm_peak_kb: Option<u64>,
|
||||
pub vm_size_kb: Option<u64>,
|
||||
pub vm_lck_kb: Option<u64>,
|
||||
pub vm_pin_kb: Option<u64>,
|
||||
pub vm_hwm_kb: Option<u64>,
|
||||
pub vm_rss_kb: Option<u64>,
|
||||
pub vm_data_kb: Option<u64>,
|
||||
pub vm_stk_kb: Option<u64>,
|
||||
pub vm_exe_kb: Option<u64>,
|
||||
pub vm_lib_kb: Option<u64>,
|
||||
pub vm_pte_kb: Option<u64>,
|
||||
pub vm_swap_kb: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ProcIo {
|
||||
pub rchar: Option<u64>,
|
||||
pub wchar: Option<u64>,
|
||||
pub syscr: Option<u64>,
|
||||
pub syscw: Option<u64>,
|
||||
pub read_bytes: Option<u64>,
|
||||
pub write_bytes: Option<u64>,
|
||||
pub cancelled_write_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ProcSmapsRollup {
|
||||
pub rss_kb: Option<u64>,
|
||||
pub pss_kb: Option<u64>,
|
||||
pub private_clean_kb: Option<u64>,
|
||||
pub private_dirty_kb: Option<u64>,
|
||||
pub swapped_kb: Option<u64>,
|
||||
}
|
||||
|
||||
#[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::<u64>().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::<u64>().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::<u64>().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());
|
||||
}
|
||||
}
|
||||
@@ -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<u64>| x.map(|v| format!("{}", v)).unwrap_or_else(|| "?".to_string());
|
||||
let opt_kb = |x: &Option<u64>| x.map(|v| format!("{} KiB", v)).unwrap_or_else(|| "?".to_string());
|
||||
let opt_str = |x: &Option<String>| x.clone().unwrap_or_else(|| "?".to_string());
|
||||
|
||||
let mut lines: Vec<Line> = 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<u32>,
|
||||
|
||||
Reference in New Issue
Block a user