From 9e08f7ed05dcc691dc0d8bf118f3db1958dd10fd Mon Sep 17 00:00:00 2001 From: vasilito Date: Tue, 7 Jul 2026 08:54:59 +0300 Subject: [PATCH] redbear-power: PID jump, process filter matches PID, narrow process view - Press F, type a number, Enter: jump cursor directly to that PID in the visible list. - Press F, type text: filters by process name (existing) and now PID substring too. - Press h in Process tab: toggle narrow view (PID/STATE/CPU%/MEM/COMM) for small terminals. - Add process_narrow to App and SessionState; persist across sessions. - Update help text and status messages to document the new keybindings. - Fix unused super::* warning in process filter unit tests. --- .../system/redbear-power/source/src/app.rs | 8 +++ .../system/redbear-power/source/src/main.rs | 59 +++++++++++++------ .../redbear-power/source/src/process.rs | 1 - .../system/redbear-power/source/src/render.rs | 37 ++++++++---- .../redbear-power/source/src/session.rs | 8 +++ 5 files changed, 83 insertions(+), 30 deletions(-) diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index c61412178f..4515fd749d 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -154,6 +154,9 @@ pub struct App { /// each parent's children but parents always come first. pub process_tree: bool, pub show_full_cmdline: bool, + /// When true, the Process tab renders a narrow layout + /// (PID, STATE, CPU%, MEM, COMM) for small terminals. + pub process_narrow: bool, /// PIDs whose subtrees are collapsed in tree view. When a PID /// is in this set, its descendants are not rendered. Toggled /// by the `Space` hotkey (Process tab, tree mode only). @@ -463,6 +466,7 @@ impl App { theme_idx: 0, process_tree: false, show_full_cmdline: false, + process_narrow: false, folded: std::collections::BTreeSet::new(), pkg_power_w: None, rapl_prev: None, @@ -520,6 +524,8 @@ impl App { app.process_sort = session.process_sort; app.sort_ascending = session.sort_ascending; app.process_tree = session.process_tree; + app.show_full_cmdline = session.show_full_cmdline; + app.process_narrow = session.process_narrow; app.folded = session.folded.into_iter().collect(); app.process_filter = session.process_filter; app.show_full_cmdline = session.show_full_cmdline; @@ -544,6 +550,7 @@ impl App { folded: self.folded.iter().copied().collect(), process_filter: self.process_filter.clone(), show_full_cmdline: self.show_full_cmdline, + process_narrow: self.process_narrow, }; session.save(); } @@ -1872,6 +1879,7 @@ mod tests { folded: app.folded.iter().copied().collect(), process_filter: app.process_filter.clone(), show_full_cmdline: app.show_full_cmdline, + process_narrow: app.process_narrow, }; let serialized = toml::to_string(&session).unwrap(); let parsed: crate::session::SessionState = toml::from_str(&serialized).unwrap(); diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index f040c8199a..00570eb149 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -1207,6 +1207,17 @@ fn main() -> io::Result<()> { if app.process_tree { "tree" } else { "flat" } )); } + Key::Char('h') if app.current_tab == TabId::Process => { + app.process_narrow = !app.process_narrow; + app.flash_status(format!( + "process view: {}", + if app.process_narrow { + "narrow (PID/STATE/CPU/MEM/COMM)" + } else { + "full" + } + )); + } Key::Char('C') if app.current_tab == TabId::Process => { app.show_full_cmdline = !app.show_full_cmdline; app.flash_status(format!( @@ -1247,7 +1258,7 @@ fn main() -> io::Result<()> { Key::Char('F') => { process_filter_input = Some(app.process_filter.clone()); app.flash_status( - "process filter: type chars + Enter to apply, Esc to clear", + "process filter: text filters by name/PID, number jumps to PID, Esc clears", ); } Key::Char(c) if process_filter_input.is_some() && interval_input.is_none() => { @@ -1266,23 +1277,37 @@ fn main() -> io::Result<()> { if process_filter_input.is_some() && interval_input.is_none() => { let new_filter = process_filter_input.take().unwrap_or_default(); - app.process_filter = new_filter.clone(); - let matches = if app.process_filter.is_empty() { - app.processes.count() + if let Ok(pid) = new_filter.parse::() { + if let Some(idx) = + app.visible_processes().iter().position(|p| p.pid == pid) + { + app.process_cursor = idx; + app.flash_status(format!("jumped to PID {pid}")); + } else { + app.flash_status(format!("PID {pid} not found in current view")); + } } else { - let needle = app.process_filter.to_lowercase(); - app.processes - .processes - .iter() - .filter(|p| p.comm.to_lowercase().contains(&needle)) - .count() - }; - app.flash_status(format!( - "process filter applied: \"{}\" ({} match{})", - new_filter, - matches, - if matches == 1 { "" } else { "es" } - )); + app.process_filter = new_filter.clone(); + let matches = if app.process_filter.is_empty() { + app.processes.count() + } else { + let needle = app.process_filter.to_lowercase(); + app.processes + .processes + .iter() + .filter(|p| { + p.comm.to_lowercase().contains(&needle) + || p.pid.to_string().contains(&needle) + }) + .count() + }; + app.flash_status(format!( + "process filter applied: \"{}\" ({} match{})", + new_filter, + matches, + if matches == 1 { "" } else { "es" } + )); + } } Key::Esc if process_filter_input.is_some() => { process_filter_input = None; diff --git a/local/recipes/system/redbear-power/source/src/process.rs b/local/recipes/system/redbear-power/source/src/process.rs index 53b8d6769b..da120955e1 100644 --- a/local/recipes/system/redbear-power/source/src/process.rs +++ b/local/recipes/system/redbear-power/source/src/process.rs @@ -1187,7 +1187,6 @@ mod sort_unit_tests { #[cfg(test)] mod filter_unit_tests { - use super::*; #[test] fn filter_case_insensitive() { diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index 2a7844d1d3..ade05ccd22 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -1183,13 +1183,14 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { } let state_summary = format!("R:{running} S:{sleeping} Z:{zombie} T:{stopped}"); lines.push(Line::from(format!( - "Showing top {} of {} process(es) [{state_summary}]; total RSS: {}; sort: {} {}{}{} (press 'o' to cycle, 'y' for tree, 'F' to filter)", + "Showing top {} of {} process(es) [{state_summary}]; total RSS: {}; sort: {} {}{}{}{} (press 'o' to cycle, 'y' for tree, 'h' for narrow, 'F' to filter)", proc.count(), proc.total_count, crate::process::ProcessInfo::format_memory_kb(proc.total_memory_kb), app.process_sort.name(), sort_arrow, if app.process_tree { "; view: tree" } else { "" }, + if app.process_narrow { "; view: narrow" } else { "" }, filter_indicator, ).set_style(theme::LABEL_BOLD))); lines.push(Line::from("")); @@ -1205,10 +1206,14 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { crate::process::SortMode::WChar => "WChr", _ => "RSS", }; - let header_str = format!( - " PID STATE SCHED PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM", - mem_header - ); + let header_str = if app.process_narrow { + format!(" PID STATE CPU% {:<11} COMM", mem_header) + } else { + format!( + " PID STATE SCHED PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM", + mem_header + ) + }; lines.push(Line::from(vec![header_str.set_style(theme::LABEL)])); let filter_lower = app.process_filter.to_lowercase(); let mut visible_index: usize = 0; @@ -1373,23 +1378,30 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> { state_style }, ), - Span::styled(pre1b, row_style), - Span::styled( + ]; + if !app.process_narrow { + spans.push(Span::styled(pre1b, row_style)); + spans.push(Span::styled( nice_str, if is_cursor { app.theme.cursor_highlight } else { nice_style }, - ), - Span::styled(pre2, row_style), - ]; + )); + spans.push(Span::styled(pre2, row_style)); + } if is_cursor { spans.push(Span::styled(cpu_str, app.theme.cursor_highlight)); } else { spans.push(Span::styled(cpu_str, cpu_style)); } - spans.push(Span::styled(post, row_style)); + if app.process_narrow { + let narrow_post = format!(" {:<11}{:<7} ", mem_str, mem_pct); + spans.push(Span::styled(narrow_post, row_style)); + } else { + spans.push(Span::styled(post, row_style)); + } if !filter_lower.is_empty() && display_name.to_lowercase().contains(&filter_lower) { let lower = display_name.to_lowercase(); let mut last = 0; @@ -2178,7 +2190,8 @@ INTERACTIVE CONTROLS: [l] list open file descriptors of selected process (Process tab) [C] toggle full command line vs comm name (Process tab) [y] toggle process tree view (Process tab) - [F] filter processes by name (Process tab) + [F] filter processes by name or PID; number alone jumps to PID (Process tab) + [h] toggle narrow process view (Process tab) [o] cycle process sort mode [i] invert sort direction [Space] fold/unfold process subtree (tree mode) diff --git a/local/recipes/system/redbear-power/source/src/session.rs b/local/recipes/system/redbear-power/source/src/session.rs index 0b2a5b2c5c..5f941eda47 100644 --- a/local/recipes/system/redbear-power/source/src/session.rs +++ b/local/recipes/system/redbear-power/source/src/session.rs @@ -45,6 +45,9 @@ pub struct SessionState { /// Show full command line instead of comm name. #[serde(default)] pub show_full_cmdline: bool, + /// Narrow process view (hide IO columns for small terminals). + #[serde(default)] + pub process_narrow: bool, } impl Default for SessionState { @@ -57,6 +60,7 @@ impl Default for SessionState { folded: Vec::new(), process_filter: String::new(), show_full_cmdline: false, + process_narrow: false, } } } @@ -150,6 +154,8 @@ mod tests { assert!(!s.process_tree); assert!(s.folded.is_empty()); assert!(s.process_filter.is_empty()); + assert!(!s.show_full_cmdline); + assert!(!s.process_narrow); } #[test] @@ -165,6 +171,7 @@ mod tests { folded: vec![100, 200, 300], process_filter: "kworker".to_string(), show_full_cmdline: true, + process_narrow: true, }; let serialized = toml::to_string(&s).unwrap(); let parsed: SessionState = toml::from_str(&serialized).unwrap(); @@ -224,6 +231,7 @@ mod tests { folded: vec![42], process_filter: "bash".to_string(), show_full_cmdline: false, + process_narrow: true, }; let serialized = toml::to_string(&s).unwrap(); // Mimic the temp+rename flow with a different name