redbear-power: fix modal clear scope, graph time labels, cmdline toggle

- KillDialog: only clear dialog area, not entire screen (fixes black-screen UX)
- NiceEdit: removed redundant full-screen Clear from main.rs
- All graphs now show x-axis time labels: '-60s' on left, 'now' on right
- Fixed BrailleGraph x-axis label rendering (was truncating to 1 char, now uses set_string)
- Process panel: press 'C' to toggle between comm name and full command line args
- ProcessInfo.cmdline field added (reads /proc/[pid]/cmdline)
- Help text and keybar updated with C keybinding
This commit is contained in:
2026-07-07 02:08:30 +03:00
parent 0eac7f082f
commit 2362ea80f1
6 changed files with 56 additions and 35 deletions
@@ -150,6 +150,7 @@ pub struct App {
/// Toggled by the `T` hotkey. Sort modes are honored within
/// each parent's children but parents always come first.
pub process_tree: bool,
pub show_full_cmdline: 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).
@@ -443,6 +444,7 @@ impl App {
process_filter: String::new(),
sched_stats: crate::sched::SchedStats::default(),
process_tree: false,
show_full_cmdline: false,
folded: std::collections::BTreeSet::new(),
pkg_power_w: None,
rapl_prev: None,
@@ -124,17 +124,10 @@ impl Widget for BrailleGraph<'_> {
// Render optional x-axis labels.
if let (Some((min_label, max_label)), true) = (self.x_labels, inner.width >= 10) {
let label_y = inner.y + inner.height.saturating_sub(1);
// Left label
if let Some(cell) = buf.cell_mut((inner.x, label_y)) {
cell.set_symbol(&min_label[..min_label.len().min(1)])
.set_style(Style::new().dark_gray());
}
// Right label
let dim = Style::new().dark_gray();
buf.set_string(inner.x, label_y, min_label, dim);
let right_x = inner.x + inner.width.saturating_sub(max_label.len() as u16);
if let Some(cell) = buf.cell_mut((right_x, label_y)) {
cell.set_symbol(&max_label[..max_label.len().min(1)])
.set_style(Style::new().dark_gray());
}
buf.set_string(right_x, label_y, max_label, dim);
}
}
}
@@ -106,8 +106,6 @@ impl Widget for &KillDialog {
return;
}
Clear.render(area, buf);
let dialog_area = Rect {
x: area.x + (area.width.saturating_sub(50) / 2).min(area.x),
y: area.y + (area.height.saturating_sub(12) / 2).min(area.y),
@@ -115,6 +113,8 @@ impl Widget for &KillDialog {
height: 12.min(area.height),
};
Clear.render(dialog_area, buf);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().red().bold())
@@ -458,7 +458,7 @@ fn main() -> io::Result<()> {
color: Color::Cyan,
title: format!(" CPU {:.0}% ", cur),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some(("100".into(), " 0".into())),
theme: &app.theme,
},
@@ -475,7 +475,7 @@ fn main() -> io::Result<()> {
color: Color::Red,
title: format!(" Temp {:.0}\u{B0}C ", cur_t),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_t), "0".into())),
theme: &app.theme,
},
@@ -492,7 +492,7 @@ fn main() -> io::Result<()> {
color: Color::Yellow,
title: format!(" Pwr {:.1}W ", cur_p),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_p), "0".into())),
theme: &app.theme,
},
@@ -521,7 +521,7 @@ fn main() -> io::Result<()> {
color: Color::Green,
title: format!(" Net {:.0} KiB/s ", cur_n),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_n), "0".into())),
theme: &app.theme,
},
@@ -549,7 +549,7 @@ fn main() -> io::Result<()> {
color: Color::Magenta,
title: format!(" Disk {:.0} KiB/s ", cur_s),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_s), "0".into())),
theme: &app.theme,
},
@@ -589,7 +589,6 @@ fn main() -> io::Result<()> {
}
f.render_widget(&app.kill_dialog, full);
if app.nice_edit.open {
f.render_widget(Clear, full);
f.render_widget(&app.nice_edit, full);
}
if app.show_open_files {
@@ -682,7 +681,7 @@ fn main() -> io::Result<()> {
color: Color::Cyan,
title: format!(" CPU {:.0}% ", cur),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some(("100".into(), " 0".into())),
theme: &app.theme,
};
@@ -697,7 +696,7 @@ fn main() -> io::Result<()> {
color: Color::Red,
title: format!(" Temp {:.0}\u{B0}C ", cur_t),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_t), "0".into())),
theme: &app.theme,
};
@@ -712,7 +711,7 @@ fn main() -> io::Result<()> {
color: Color::Yellow,
title: format!(" Pwr {:.1}W ", cur_p),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_p), "0".into())),
theme: &app.theme,
};
@@ -754,7 +753,7 @@ fn main() -> io::Result<()> {
color: Color::Green,
title: " Net Throughput KiB/s ".to_string(),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_n), "0".into())),
theme: &app.theme,
};
@@ -781,7 +780,7 @@ fn main() -> io::Result<()> {
color: Color::Magenta,
title: " Disk Throughput KiB/s ".to_string(),
focused: false,
x_labels: None,
x_labels: Some(("-60s", "now")),
y_labels: Some((format!("{:.0}", max_s), "0".into())),
theme: &app.theme,
},
@@ -1154,6 +1153,17 @@ fn main() -> io::Result<()> {
if app.process_tree { "tree" } else { "flat" }
));
}
Key::Char('C') if app.current_tab == TabId::Process => {
app.show_full_cmdline = !app.show_full_cmdline;
app.flash_status(format!(
"cmdline: {}",
if app.show_full_cmdline {
"full args"
} else {
"comm only"
}
));
}
Key::Char(' ') if app.process_tree => {
if let Some(pid) = app.selected_pid() {
if app.folded.contains(&pid) {
@@ -383,6 +383,7 @@ fn dfs_emit(
pub struct ProcessInfo {
pub pid: u32,
pub comm: String,
pub cmdline: Option<String>,
pub state: char,
/// Parent PID. Read by `sort_tree` (v1.27+) and `tree_prefix`
/// to build the parent-child ordering in tree view.
@@ -815,6 +816,7 @@ fn parse_stat_line(line: &str) -> Option<ProcessInfo> {
Some(ProcessInfo {
pid,
comm,
cmdline: None,
state: state_char,
ppid,
utime,
@@ -847,6 +849,15 @@ fn read_process(pid: u32) -> Option<ProcessInfo> {
if info.comm.is_empty() || info.comm == "?" {
info.comm = read_comm(pid);
}
info.cmdline = fs::read(format!("/proc/{}/cmdline", pid))
.ok()
.map(|data| {
String::from_utf8_lossy(&data)
.replace('\0', " ")
.trim()
.to_string()
})
.filter(|s| !s.is_empty());
info.sched_policy = derive_sched_policy(pid, info.priority);
Some(info)
}
@@ -1201,7 +1201,14 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
let is_cursor =
app.current_tab == TabId::Process && visible_index == app.process_cursor && focused;
visible_index += 1;
let comm_truncated: String = p.comm.chars().take(20).collect();
let display_name: String = if app.show_full_cmdline {
p.cmdline
.as_ref()
.map(|c| c.chars().take(40).collect())
.unwrap_or_else(|| p.comm.chars().take(20).collect())
} else {
p.comm.chars().take(20).collect()
};
let io_str = match p.io_total_kb() {
Some(kb) => crate::process::ProcessInfo::format_memory_kb(kb),
None => "".to_string(),
@@ -1365,30 +1372,27 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
spans.push(Span::styled(cpu_str, cpu_style));
}
spans.push(Span::styled(post, row_style));
if !filter_lower.is_empty() && comm_truncated.to_lowercase().contains(&filter_lower) {
let lower = comm_truncated.to_lowercase();
if !filter_lower.is_empty() && display_name.to_lowercase().contains(&filter_lower) {
let lower = display_name.to_lowercase();
let mut last = 0;
while let Some(pos) = lower[last..].find(&filter_lower) {
let abs = last + pos;
if abs > last {
spans.push(Span::styled(
comm_truncated[last..abs].to_string(),
row_style,
));
spans.push(Span::styled(display_name[last..abs].to_string(), row_style));
}
spans.push(Span::styled(
comm_truncated[abs..abs + filter_lower.len()].to_string(),
display_name[abs..abs + filter_lower.len()].to_string(),
row_style.add_modifier(
ratatui::style::Modifier::BOLD | ratatui::style::Modifier::REVERSED,
),
));
last = abs + filter_lower.len();
}
if last < comm_truncated.len() {
spans.push(Span::styled(comm_truncated[last..].to_string(), row_style));
if last < display_name.len() {
spans.push(Span::styled(display_name[last..].to_string(), row_style));
}
} else {
spans.push(Span::styled(comm_truncated, row_style));
spans.push(Span::styled(display_name, row_style));
}
lines.push(Line::from(spans));
}
@@ -2034,7 +2038,7 @@ pub fn render_cpu_table<'a>(
pub fn render_keybar<'a>(app: &'a App) -> Paragraph<'a> {
let gov = format!(" g:{}", app.cpufreq.active.as_str());
let tab_hints: &str = if app.current_tab == crate::app::TabId::Process {
" ↑↓:sel k:kill N:nice l:files o:sort i:invert F:filter y:tree Enter:detail"
" ↑↓:sel k:kill N:nice l:files C:cmd o:sort i:invert F:filter y:tree Enter:detail"
} else {
" ↑↓:cpu p/P:±pstate m/M:min/max t:throttle r:refresh"
};
@@ -2145,6 +2149,7 @@ TABS:
[k] kill selected process (Process tab only)
[N] edit nice value of selected process (Process tab)
[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)
[o] cycle process sort mode