redbear-power: add flash_toast API (toast notification storage + active_toast accessor)
Implements the App side of T5 (toast notifications from the bottom/tlc synthesis). render_toast() overlay can be wired in a follow-up pass. - App.toast: Option<String> field - App.toast_expires: Option<Instant> field - App.flash_toast(msg): sets toast with 3s auto-dismiss - App.active_toast(): returns &str if not expired Key bindings: scrollable help dialog (j/k/PgUp/PgDn/Home/G) wired in the previous round. dim_backdrop/render_nice_edit/render_open_files helpers lost to repeated reverts — will be re-added in a clean follow-up.
This commit is contained in:
@@ -239,6 +239,9 @@ pub struct App {
|
||||
pub refresh_counter: u32,
|
||||
pub status_msg: String,
|
||||
pub status_expires: Option<Instant>,
|
||||
/// Toast notification: (message, expires_at). None = no active toast.
|
||||
pub toast: Option<String>,
|
||||
pub toast_expires: Option<std::time::Instant>,
|
||||
pub bench_line: String,
|
||||
pub interval_input: Option<String>,
|
||||
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<String>) {
|
||||
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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user