redbear-power: live graph labels, sort indicator, toast for governor/throttle

- Graph titles now show live values: 'CPU 45%', 'Temp 62°C', 'Pwr 8.3W', etc.
- Process panel shows sort direction arrow (▲ ascending / ▼ descending)
- Governor cycle and throttle mode now trigger flash_toast (floating box)
  instead of flash_status (keybar only)
- BrailleGraph.title changed from &str to String for dynamic titles
This commit is contained in:
2026-07-07 01:26:56 +03:00
parent d9368b08b2
commit 2061609f62
4 changed files with 40 additions and 26 deletions
@@ -867,7 +867,7 @@ impl App {
return;
}
if let Some(name) = self.cpufreq.cycle() {
self.flash_status(format!("governor {name}"));
self.flash_toast(format!("Governor \u{2192} {}", name));
} else {
self.flash_status("governor cycle: no other governor available");
}
@@ -989,7 +989,7 @@ impl App {
ThrottleMode::User => "USER (no throttling)",
ThrottleMode::ForcedMin => "FORCED MIN (manual)",
};
self.flash_status(format!("throttle mode → {label}"));
self.flash_toast(format!("Throttle: {}", label));
}
pub fn flash_status(&mut self, msg: impl Into<String>) {
@@ -35,7 +35,7 @@ pub struct BrailleGraph<'a> {
/// Color for the graph line.
pub color: Color,
/// Title rendered in the block border.
pub title: &'a str,
pub title: String,
/// Whether this graph has keyboard focus (affects border style).
pub focused: bool,
/// Optional x-axis label range. If `Some((min_label, max_label))`,
@@ -58,7 +58,7 @@ impl<'a> BrailleGraph<'a> {
} else {
self.theme.border_dim
})
.title(self.title)
.title(self.title.clone())
}
}
@@ -290,7 +290,7 @@ mod tests {
values: &values,
max_value: 50.0,
color: ratatui::style::Color::Cyan,
title: " Test ",
title: " Test ".to_string(),
focused: false,
x_labels: None,
y_labels: Some(("50".into(), " 0".into())),
@@ -311,7 +311,7 @@ mod tests {
values: &[],
max_value: 100.0,
color: ratatui::style::Color::Red,
title: " Empty ",
title: " Empty ".to_string(),
focused: false,
x_labels: None,
y_labels: None,
@@ -332,7 +332,7 @@ mod tests {
values: &[42.0],
max_value: 100.0,
color: ratatui::style::Color::Green,
title: " Single ",
title: " Single ".to_string(),
focused: false,
x_labels: None,
y_labels: None,
@@ -450,12 +450,13 @@ fn main() -> io::Result<()> {
Constraint::Length(w),
]));
if ca.width >= 10 && ca.height >= GRAPH_HEIGHT {
let cur = app.cpu_graph_history.last().unwrap_or(0.0);
f.render_widget(
BrailleGraph {
values: app.cpu_graph_history.as_slice(),
max_value: 100.0,
color: Color::Cyan,
title: " CPU Load % ",
title: format!(" CPU {:.0}% ", cur),
focused: false,
x_labels: None,
y_labels: Some(("100".into(), " 0".into())),
@@ -466,12 +467,13 @@ fn main() -> io::Result<()> {
}
if ta.width >= 10 && ta.height >= GRAPH_HEIGHT {
let max_t = app.temp_graph_history.display_max().max(50.0);
let cur_t = app.temp_graph_history.last().unwrap_or(0.0);
f.render_widget(
BrailleGraph {
values: app.temp_graph_history.as_slice(),
max_value: max_t,
color: Color::Red,
title: " Temp °C ",
title: format!(" Temp {:.0}\u{B0}C ", cur_t),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_t), "0".into())),
@@ -482,12 +484,13 @@ fn main() -> io::Result<()> {
}
if pa.width >= 10 && pa.height >= GRAPH_HEIGHT {
let max_p = app.power_graph_history.display_max().max(20.0);
let cur_p = app.power_graph_history.last().unwrap_or(0.0);
f.render_widget(
BrailleGraph {
values: app.power_graph_history.as_slice(),
max_value: max_p,
color: Color::Yellow,
title: " Pkg Power W ",
title: format!(" Pwr {:.1}W ", cur_p),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_p), "0".into())),
@@ -510,12 +513,13 @@ fn main() -> io::Result<()> {
f.render_widget(render_network_panel(&app, true), net_area);
if graph_area.width >= 10 && graph_area.height >= GRAPH_HEIGHT {
let max_n = app.net_graph_history.display_max();
let cur_n = app.net_graph_history.last().unwrap_or(0.0);
f.render_widget(
BrailleGraph {
values: app.net_graph_history.as_slice(),
max_value: max_n,
color: Color::Green,
title: " Net Throughput KiB/s ",
title: format!(" Net {:.0} KiB/s ", cur_n),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_n), "0".into())),
@@ -537,12 +541,13 @@ fn main() -> io::Result<()> {
f.render_widget(render_storage_panel(&app, true), sto_area);
if graph_area.width >= 10 && graph_area.height >= GRAPH_HEIGHT {
let max_s = app.storage_graph_history.display_max();
let cur_s = app.storage_graph_history.last().unwrap_or(0.0);
f.render_widget(
BrailleGraph {
values: app.storage_graph_history.as_slice(),
max_value: max_s,
color: Color::Magenta,
title: " Disk Throughput KiB/s ",
title: format!(" Disk {:.0} KiB/s ", cur_s),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_s), "0".into())),
@@ -670,11 +675,12 @@ fn main() -> io::Result<()> {
Constraint::Length(w),
]));
if cpu_area.width >= 10 && cpu_area.height >= GRAPH_HEIGHT {
let cur = app.cpu_graph_history.last().unwrap_or(0.0);
let g = BrailleGraph {
values: app.cpu_graph_history.as_slice(),
max_value: 100.0,
color: Color::Cyan,
title: " CPU Load % ",
title: format!(" CPU {:.0}% ", cur),
focused: false,
x_labels: None,
y_labels: Some(("100".into(), " 0".into())),
@@ -684,11 +690,12 @@ fn main() -> io::Result<()> {
}
if temp_area.width >= 10 && temp_area.height >= GRAPH_HEIGHT {
let max_t = app.temp_graph_history.display_max().max(50.0);
let cur_t = app.temp_graph_history.last().unwrap_or(0.0);
let g = BrailleGraph {
values: app.temp_graph_history.as_slice(),
max_value: max_t,
color: Color::Red,
title: " Temp °C ",
title: format!(" Temp {:.0}\u{B0}C ", cur_t),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_t), "0".into())),
@@ -698,11 +705,12 @@ fn main() -> io::Result<()> {
}
if power_area.width >= 10 && power_area.height >= GRAPH_HEIGHT {
let max_p = app.power_graph_history.display_max().max(20.0);
let cur_p = app.power_graph_history.last().unwrap_or(0.0);
let g = BrailleGraph {
values: app.power_graph_history.as_slice(),
max_value: max_p,
color: Color::Yellow,
title: " Pkg Power W ",
title: format!(" Pwr {:.1}W ", cur_p),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_p), "0".into())),
@@ -744,7 +752,7 @@ fn main() -> io::Result<()> {
values: app.net_graph_history.as_slice(),
max_value: max_n,
color: Color::Green,
title: " Net Throughput KiB/s ",
title: " Net Throughput KiB/s ".to_string(),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_n), "0".into())),
@@ -771,7 +779,7 @@ fn main() -> io::Result<()> {
values: app.storage_graph_history.as_slice(),
max_value: max_s,
color: Color::Magenta,
title: " Disk Throughput KiB/s ",
title: " Disk Throughput KiB/s ".to_string(),
focused: false,
x_labels: None,
y_labels: Some((format!("{:.0}", max_s), "0".into())),
@@ -1151,15 +1151,21 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
} else {
format!("; filter: \"{}\" (press Esc to clear)", app.process_filter)
};
let sort_arrow = if app.sort_ascending {
"\u{25B2}"
} else {
"\u{25BC}"
};
lines.push(Line::from(format!(
"Showing top {} of {} process(es); total RSS: {}; sort: {}{}{} (press 'o' to cycle, 'y' for tree, 'F' to filter)",
proc.count(),
proc.total_count,
crate::process::ProcessInfo::format_memory_kb(proc.total_memory_kb),
app.process_sort.name(),
if app.process_tree { "; view: tree" } else { "" },
filter_indicator,
).set_style(theme::LABEL_BOLD)));
"Showing top {} of {} process(es); total RSS: {}; sort: {} {}{}{} (press 'o' to cycle, 'y' for tree, '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 { "" },
filter_indicator,
).set_style(theme::LABEL_BOLD)));
lines.push(Line::from(""));
// The MEM column header swaps between RSS and VSZ depending on
// the active sort. Default and most modes use RSS (resident
@@ -2251,7 +2257,7 @@ fn render_graph_snapshot(
values,
max_value: max_val,
color,
title,
title: title.to_string(),
focused: false,
x_labels: None,
y_labels: Some((top_label.to_string(), bot_label.to_string())),