diff --git a/local/docs/archived/USB-VALIDATION-RUNBOOK.md b/local/docs/USB-VALIDATION-RUNBOOK.md similarity index 100% rename from local/docs/archived/USB-VALIDATION-RUNBOOK.md rename to local/docs/USB-VALIDATION-RUNBOOK.md diff --git a/local/docs/USB-IMPLEMENTATION-PLAN.md b/local/docs/archived/USB-IMPLEMENTATION-PLAN-v1-2026-04.md similarity index 100% rename from local/docs/USB-IMPLEMENTATION-PLAN.md rename to local/docs/archived/USB-IMPLEMENTATION-PLAN-v1-2026-04.md diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index 7b9d3eb930..224d8ebfc6 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -239,6 +239,9 @@ pub struct App { pub refresh_counter: u32, pub status_msg: String, pub status_expires: Option, + /// Toast notification: (message, expires_at). None = no active toast. + pub toast: Option, + pub toast_expires: Option, pub bench_line: String, pub interval_input: Option, pub current_tab: TabId, @@ -262,6 +265,8 @@ pub struct App { pub storage_graph_history: crate::graph::RingHistory, /// Process kill dialog. Opened by `k` key in the Process tab. pub kill_dialog: crate::kill::KillDialog, + /// Help dialog scroll offset (lines from top). + pub help_scroll: usize, /// When true, the current tab is rendered full-screen (widget /// expansion). Toggled by the `e` key. pub expanded: bool, @@ -411,6 +416,8 @@ impl App { hybrid_summary, status_msg: String::new(), status_expires: None, + toast: None, + toast_expires: None, bench_line: String::new(), interval_input: None, current_tab: TabId::PerCpu, @@ -470,6 +477,7 @@ impl App { net_graph_history: crate::graph::RingHistory::new(120), storage_graph_history: crate::graph::RingHistory::new(120), kill_dialog: crate::kill::KillDialog::new(), + help_scroll: 0, expanded: false, }; // v1.40: load persisted session state and apply. @@ -976,6 +984,21 @@ impl App { self.status_expires = Some(Instant::now() + Duration::from_secs(3)); } + /// tlc-style toast notification. Renders as floating bottom-right box + /// for 3 seconds, then auto-dismisses. Distinct from flash_status + /// (keybar text) — toasts are for notable events. + pub fn flash_toast(&mut self, msg: impl Into) { + self.toast = Some(msg.into()); + self.toast_expires = Some(std::time::Instant::now() + std::time::Duration::from_secs(3)); + } + + pub fn active_toast(&self) -> Option<&str> { + match self.toast_expires { + Some(deadline) if std::time::Instant::now() < deadline => self.toast.as_deref(), + _ => None, + } + } + pub fn status_text(&self) -> Option<&str> { match self.status_expires { Some(deadline) if Instant::now() < deadline => Some(&self.status_msg), diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index 8a9c234109..945750be9d 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -558,7 +558,7 @@ fn main() -> io::Result<()> { let area = full.centered(Constraint::Percentage(70), Constraint::Percentage(80)); f.render_widget(Clear, area); - f.render_widget(render_help(), area); + f.render_widget(render_help(app.help_scroll, &app.theme), area); } if let Some(detail) = app.pid_detail.as_ref() { let pid = detail.status.pid.unwrap_or(0) as u32; @@ -750,7 +750,7 @@ fn main() -> io::Result<()> { if show_help { let area = full.centered(Constraint::Percentage(70), Constraint::Percentage(80)); f.render_widget(Clear, area); - f.render_widget(render_help(), area); + f.render_widget(render_help(app.help_scroll, &app.theme), area); } if let Some(detail) = app.pid_detail.as_ref() { let pid = detail.status.pid.unwrap_or(0) as u32; @@ -857,6 +857,24 @@ fn main() -> io::Result<()> { Key::Char('9') => app.set_tab(app::TabId::Process), Key::Char('T') => app.set_tab(app.current_tab.next()), Key::Char('?') => show_help = !show_help, + Key::Down | Key::Char('j') if show_help => { + app.help_scroll = app.help_scroll.saturating_add(1); + } + Key::Up | Key::Char('k') if show_help => { + app.help_scroll = app.help_scroll.saturating_sub(1); + } + Key::PageDown if show_help => { + app.help_scroll = app.help_scroll.saturating_add(10); + } + Key::PageUp if show_help => { + app.help_scroll = app.help_scroll.saturating_sub(10); + } + Key::Home if show_help => { + app.help_scroll = 0; + } + Key::End | Key::Char('G') if show_help => { + app.help_scroll = 200; + } Key::Char('g') => app.cycle_governor(), Key::Char('p') => app.step_selected_pstate(-1), Key::Char('P') => app.step_selected_pstate(1), diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index 035954a658..357dee0349 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -305,10 +305,27 @@ fn mem_bar_line<'a>( for _ in filled..bar_width { bar.push('\u{2591}'); // light shade } + // Threshold gradient: green <50%, yellow 50-75%, red >75%. + let threshold_color = if percent < 50.0 { + theme::VALUE_OK + } else if percent < 75.0 { + theme::STATUS_WARN + } else { + theme::VALUE_HOT + }; + // Size-based tint: apply Modifiers (BOLD at 50%, BOLD|REVERSED at 90%+) + // for visual emphasis at high pressure — tlc size_tint pattern. + let mut percent_style = theme::VALUE; + if percent >= 90.0 { + percent_style = percent_style.add_modifier(ratatui::style::Modifier::BOLD); + } + if percent >= 95.0 { + percent_style = percent_style.add_modifier(ratatui::style::Modifier::REVERSED); + } Line::from(vec![ label.set_style(theme::LABEL), - format!("[{}] ", bar).set_style(color), - format!("{:5.1}% ", percent).set_style(theme::VALUE), + format!("[{}] ", bar).set_style(if filled > 0 { threshold_color } else { color }), + format!("{:5.1}% ", percent).set_style(percent_style), format!( "{} / {}", crate::render::format_kib(value_kib), @@ -1701,8 +1718,15 @@ pub fn render_cpu_table<'a>( let pct = t.min(100) as u8; let bar = horizontal_bar(pct, 4); let color = theme::temp_color(cpu.temp_c); + let mut temp_style = Style::new().fg(color); + if t >= 85 { + temp_style = temp_style.add_modifier(ratatui::style::Modifier::BOLD); + } + if t >= 95 { + temp_style = temp_style.add_modifier(ratatui::style::Modifier::REVERSED); + } Cell::from(Line::from(vec![ - format!("{t:>3} ").set_style(Style::new().fg(color)), + format!("{t:>3} ").set_style(temp_style), bar.set_style(Style::new().fg(color)), ])) } @@ -2017,10 +2041,25 @@ NOTES: - Snapshot (c key) saves Per-CPU + System + Network + Storage + graphs to /tmp/redbear-power-snapshot.txt."; -pub fn render_help() -> Paragraph<'static> { - Paragraph::new(HELP_TEXT) - .block(Block::default().borders(Borders::ALL).title(" Help ")) +pub fn render_help<'a>(help_scroll: usize, theme: &Theme) -> Paragraph<'a> { + let lines: Vec<&str> = HELP_TEXT.lines().collect(); + let total = lines.len(); + let start = help_scroll.min(total); + let visible: String = lines[start..].join("\n"); + let pct = if total > 0 { start * 100 / total } else { 0 }; + Paragraph::new(visible) + .style(theme::VALUE) .wrap(Wrap { trim: true }) + .scroll((help_scroll as u16, 0)) + .block( + Block::default() + .borders(Borders::ALL) + .title(Line::styled( + format!(" Help ({}%, j/k to scroll) ", pct), + theme::LABEL_BOLD, + )) + .border_style(theme.border_focused), + ) } /// Render the full TUI into a fixed-size buffer and return the