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.
This commit is contained in:
2026-07-07 08:54:59 +03:00
parent affb6690c5
commit 9e08f7ed05
5 changed files with 83 additions and 30 deletions
@@ -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();
@@ -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::<u32>() {
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;
@@ -1187,7 +1187,6 @@ mod sort_unit_tests {
#[cfg(test)]
mod filter_unit_tests {
use super::*;
#[test]
fn filter_case_insensitive() {
@@ -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)
@@ -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