redbear-power: theme system refactor

Move all palette colors into Theme fields so dark/light/high-contrast/nord/gruvbox/solarized-dark are fully usable. Convert NiceEdit and AffinityEditor to regular render_dialog methods that take a theme. Update all render_* helpers to bind let theme = &app.theme and reference theme fields. All 224 tests pass; clippy and fmt clean.
This commit is contained in:
Sisyphus Agent
2026-07-07 13:42:46 +03:00
parent 2728c2e408
commit b331285661
5 changed files with 550 additions and 409 deletions
@@ -5,7 +5,7 @@ use ratatui::{
widgets::{Block, Borders, Clear, Widget},
};
use crate::theme;
use crate::theme::Theme;
pub struct AffinityEditor {
pub open: bool,
@@ -123,8 +123,8 @@ impl AffinityEditor {
}
}
impl Widget for &mut AffinityEditor {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
impl AffinityEditor {
pub fn render_dialog(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer, theme: &Theme) {
Clear.render(area, buf);
let cols = self.grid_cols();
let rows = self.num_cpus.div_ceil(cols);
@@ -136,7 +136,7 @@ impl Widget for &mut AffinityEditor {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme::BORDER_FOCUSED)
.border_style(theme.border_focused)
.title(format!(" Affinity: {} (PID {}) ", self.comm, self.pid));
let inner = block.inner(darea);
block.render(darea, buf);
@@ -161,9 +161,9 @@ impl Widget for &mut AffinityEditor {
ratatui::style::Modifier::BOLD | ratatui::style::Modifier::REVERSED,
)
} else if is_on {
theme::VALUE
theme.value
} else {
theme::VALUE_OFF
theme.value_off
};
let marker = if is_on { "+" } else { "-" };
spans.push(Span::styled(format!("{marker}{label} "), style));
@@ -179,16 +179,16 @@ impl Widget for &mut AffinityEditor {
Err(e) => format!("Error: {e}"),
};
let style = if result.is_ok() {
theme::STATUS_OK
theme.status_ok
} else {
theme::STATUS_ERR
theme.status_err
};
let para = ratatui::widgets::Paragraph::new(Line::from(Span::styled(msg, style)));
para.render(footer_area, buf);
} else {
let hint = "Space:toggle Enter:apply Esc:cancel Arrows:move";
let para =
ratatui::widgets::Paragraph::new(Line::from(Span::styled(hint, theme::VALUE_OFF)));
ratatui::widgets::Paragraph::new(Line::from(Span::styled(hint, theme.value_off)));
para.render(footer_area, buf);
}
}
@@ -602,11 +602,14 @@ fn main() -> io::Result<()> {
}
f.render_widget(&app.kill_dialog, full);
if app.nice_edit.open {
f.render_widget(&app.nice_edit, full);
app.nice_edit
.render_dialog(full, f.buffer_mut(), &app.theme);
}
if app.affinity_editor.open {
f.render_widget(&mut app.affinity_editor, full);
app.affinity_editor
.render_dialog(full, f.buffer_mut(), &app.theme);
}
if app.show_open_files
&& let Some(files) = &app.open_files_result
{
@@ -5,7 +5,7 @@ use ratatui::{
widgets::{Block, Borders, Clear, Paragraph, Widget},
};
use crate::theme;
use crate::theme::Theme;
const MIN_NICE: i32 = -20;
const MAX_NICE: i32 = 19;
@@ -55,8 +55,8 @@ impl NiceEdit {
}
}
impl Widget for &NiceEdit {
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
impl NiceEdit {
pub fn render_dialog(&self, area: Rect, buf: &mut ratatui::buffer::Buffer, theme: &Theme) {
let width = 48u16.min(area.width);
let height = 9u16.min(area.height);
let x = area.x + (area.width - width) / 2;
@@ -87,7 +87,7 @@ impl Widget for &NiceEdit {
let info = Paragraph::new(vec![
Line::from(Span::styled(
format!(" PID {} {}", self.pid, self.comm),
theme::VALUE,
theme.value,
)),
Line::from(""),
]);
@@ -109,11 +109,11 @@ impl Widget for &NiceEdit {
Span::styled(
bar,
if self.value < 0 {
Style::default().fg(theme::STATUS_OK.fg.unwrap_or_default())
Style::default().fg(theme.status_ok.fg.unwrap_or_default())
} else if self.value > 0 {
Style::default().fg(theme::STATUS_WARN.fg.unwrap_or_default())
Style::default().fg(theme.status_warn.fg.unwrap_or_default())
} else {
theme::VALUE
theme.value
},
),
Span::raw("]"),
@@ -128,14 +128,14 @@ impl Widget for &NiceEdit {
match res {
Ok(()) => Line::from(Span::styled(
" Applied \u{2014} press Esc to close",
theme::STATUS_OK,
theme.status_ok,
)),
Err(e) => Line::from(Span::styled(format!(" Error: {}", e), theme::STATUS_ERR)),
Err(e) => Line::from(Span::styled(format!(" Error: {}", e), theme.status_err)),
}
} else {
Line::from(Span::styled(
" \u{2191}\u{2193}/+- adjust, Enter=apply, Esc=close",
theme::VALUE_OFF,
theme.value_off,
))
};
Widget::render(Paragraph::new(msg), msg_area, buf);
File diff suppressed because it is too large Load Diff
@@ -1,56 +1,43 @@
//! Centralized color and style palette for redbear-power.
//!
//! Every visible color and recurring style lives here as a `const`,
//! so changing a color is a one-line edit and tests can snapshot
//! against stable references. The constants use the ratatui 0.30
//! `Stylize` shorthand (`Style::new().red().bold()`) which is
//! stable across all builds.
//!
//! v1.44: Added `Theme` struct with Dark / Light / HighContrast
//! presets for border styles and key highlights. The `const` values
//! below are the Dark theme defaults. The active `Theme` is stored
//! in `App` and selectable via `--theme` / config `[theme].mode`.
//! Every visible color and recurring style lives in the `Theme` struct,
//! so each preset (Dark, Light, HighContrast, Nord, Gruvbox, SolarizedDark)
//! can define its own palette. The active `Theme` is stored in `App` and
//! selectable via `--theme` / config `[theme].mode`.
#[allow(unused_imports)]
use ratatui::style::{Color, Style, Stylize};
pub const LABEL: Style = Style::new().cyan();
pub const LABEL_BOLD: Style = Style::new().cyan().bold();
pub const VALUE: Style = Style::new();
pub const VALUE_OFF: Style = Style::new().dark_gray();
pub const VALUE_OK: Style = Style::new().green();
pub const VALUE_WARM: Style = Style::new().yellow();
pub const VALUE_HOT: Style = Style::new().red().bold();
pub const VALUE_HOT_LIGHT: Style = Style::new().light_red();
pub const BORDER_FOCUSED: Style = Style::new().yellow().bold();
pub const BORDER_DIM: Style = Style::new().dark_gray();
pub const HEADER_GOVERNOR: Style = Style::new().magenta().bold();
pub const HEADER_THROTTLE_AUTO: Style = Style::new().green();
pub const HEADER_THROTTLE_USER: Style = Style::new().blue();
pub const HEADER_THROTTLE_FORCED: Style = Style::new().red().bold();
pub const STATUS_OK: Style = Style::new().green().bold();
pub const STATUS_WARN: Style = Style::new().yellow().bold();
pub const STATUS_ERR: Style = Style::new().red().bold();
pub const PROCHOT_PULSE: Style = Style::new().red().bold();
pub const PROCHOT_FLAG: Style = Style::new().light_red();
pub const POWER_LIMIT_FLAG: Style = Style::new().yellow();
pub const NO_FLAG: Style = Style::new().dark_gray();
pub const CURSOR: Style = Style::new().bold();
/// Active theme with overridable border/header/cursor styles.
/// Three built-in presets: Dark (default), Light, HighContrast.
/// Active theme palette.
#[derive(Clone, Debug)]
pub struct Theme {
pub border_focused: Style,
pub border_dim: Style,
pub header_governor: Style,
pub cursor_highlight: Style,
pub label: Style,
pub label_bold: Style,
pub value: Style,
pub value_off: Style,
pub value_ok: Style,
pub value_warm: Style,
pub value_hot: Style,
pub value_hot_light: Style,
pub header_throttle_auto: Style,
pub header_throttle_user: Style,
pub header_throttle_forced: Style,
pub status_ok: Style,
pub status_warn: Style,
pub status_err: Style,
pub prochot_pulse: Style,
pub prochot_flag: Style,
pub power_limit_flag: Style,
pub no_flag: Style,
}
impl Theme {
@@ -67,10 +54,28 @@ impl Theme {
pub fn dark() -> Self {
Self {
border_focused: BORDER_FOCUSED,
border_dim: BORDER_DIM,
header_governor: HEADER_GOVERNOR,
cursor_highlight: CURSOR,
border_focused: Style::new().yellow().bold(),
border_dim: Style::new().dark_gray(),
header_governor: Style::new().magenta().bold(),
cursor_highlight: Style::new().bold(),
label: Style::new().cyan(),
label_bold: Style::new().cyan().bold(),
value: Style::new(),
value_off: Style::new().dark_gray(),
value_ok: Style::new().green(),
value_warm: Style::new().yellow(),
value_hot: Style::new().red().bold(),
value_hot_light: Style::new().light_red(),
header_throttle_auto: Style::new().green(),
header_throttle_user: Style::new().blue(),
header_throttle_forced: Style::new().red().bold(),
status_ok: Style::new().green().bold(),
status_warn: Style::new().yellow().bold(),
status_err: Style::new().red().bold(),
prochot_pulse: Style::new().red().bold(),
prochot_flag: Style::new().light_red(),
power_limit_flag: Style::new().yellow(),
no_flag: Style::new().dark_gray(),
}
}
@@ -80,6 +85,24 @@ impl Theme {
border_dim: Style::new().gray(),
header_governor: Style::new().blue().bold(),
cursor_highlight: Style::new().bold(),
label: Style::new().cyan(),
label_bold: Style::new().cyan().bold(),
value: Style::new().black(),
value_off: Style::new().gray(),
value_ok: Style::new().green(),
value_warm: Style::new().yellow(),
value_hot: Style::new().red().bold(),
value_hot_light: Style::new().red(),
header_throttle_auto: Style::new().green(),
header_throttle_user: Style::new().blue(),
header_throttle_forced: Style::new().red().bold(),
status_ok: Style::new().green().bold(),
status_warn: Style::new().yellow().bold(),
status_err: Style::new().red().bold(),
prochot_pulse: Style::new().red().bold(),
prochot_flag: Style::new().red(),
power_limit_flag: Style::new().yellow(),
no_flag: Style::new().gray(),
}
}
@@ -89,6 +112,24 @@ impl Theme {
border_dim: Style::new().white(),
header_governor: Style::new().white().bold(),
cursor_highlight: Style::new().white().on_black().bold(),
label: Style::new().white().bold(),
label_bold: Style::new().white().bold(),
value: Style::new().white(),
value_off: Style::new().white(),
value_ok: Style::new().green().bold(),
value_warm: Style::new().yellow().bold(),
value_hot: Style::new().red().bold(),
value_hot_light: Style::new().red().bold(),
header_throttle_auto: Style::new().green().bold(),
header_throttle_user: Style::new().cyan().bold(),
header_throttle_forced: Style::new().red().bold(),
status_ok: Style::new().green().bold(),
status_warn: Style::new().yellow().bold(),
status_err: Style::new().red().bold(),
prochot_pulse: Style::new().red().bold(),
prochot_flag: Style::new().red().bold(),
power_limit_flag: Style::new().yellow().bold(),
no_flag: Style::new().white(),
}
}
@@ -98,6 +139,24 @@ impl Theme {
border_dim: Style::new().dark_gray(),
header_governor: Style::new().light_blue().bold(),
cursor_highlight: Style::new().bold(),
label: Style::new().light_cyan(),
label_bold: Style::new().light_cyan().bold(),
value: Style::new().white(),
value_off: Style::new().dark_gray(),
value_ok: Style::new().light_green(),
value_warm: Style::new().yellow(),
value_hot: Style::new().light_red().bold(),
value_hot_light: Style::new().light_red(),
header_throttle_auto: Style::new().light_green(),
header_throttle_user: Style::new().light_blue(),
header_throttle_forced: Style::new().light_red().bold(),
status_ok: Style::new().light_green().bold(),
status_warn: Style::new().yellow().bold(),
status_err: Style::new().light_red().bold(),
prochot_pulse: Style::new().light_red().bold(),
prochot_flag: Style::new().light_red(),
power_limit_flag: Style::new().yellow(),
no_flag: Style::new().dark_gray(),
}
}
@@ -107,6 +166,24 @@ impl Theme {
border_dim: Style::new().dark_gray(),
header_governor: Style::new().yellow().bold(),
cursor_highlight: Style::new().bold(),
label: Style::new().light_blue(),
label_bold: Style::new().light_blue().bold(),
value: Style::new().white(),
value_off: Style::new().dark_gray(),
value_ok: Style::new().green(),
value_warm: Style::new().yellow(),
value_hot: Style::new().red().bold(),
value_hot_light: Style::new().red(),
header_throttle_auto: Style::new().green(),
header_throttle_user: Style::new().blue(),
header_throttle_forced: Style::new().red().bold(),
status_ok: Style::new().green().bold(),
status_warn: Style::new().yellow().bold(),
status_err: Style::new().red().bold(),
prochot_pulse: Style::new().red().bold(),
prochot_flag: Style::new().red(),
power_limit_flag: Style::new().yellow(),
no_flag: Style::new().dark_gray(),
}
}
@@ -116,6 +193,24 @@ impl Theme {
border_dim: Style::new().dark_gray(),
header_governor: Style::new().magenta().bold(),
cursor_highlight: Style::new().bold(),
label: Style::new().cyan(),
label_bold: Style::new().cyan().bold(),
value: Style::new().white(),
value_off: Style::new().dark_gray(),
value_ok: Style::new().green(),
value_warm: Style::new().yellow(),
value_hot: Style::new().red().bold(),
value_hot_light: Style::new().light_red(),
header_throttle_auto: Style::new().green(),
header_throttle_user: Style::new().blue(),
header_throttle_forced: Style::new().red().bold(),
status_ok: Style::new().green().bold(),
status_warn: Style::new().yellow().bold(),
status_err: Style::new().red().bold(),
prochot_pulse: Style::new().red().bold(),
prochot_flag: Style::new().light_red(),
power_limit_flag: Style::new().yellow(),
no_flag: Style::new().dark_gray(),
}
}
}
@@ -161,3 +256,26 @@ pub fn flags_color(prochot: bool, critical: bool, power_limit: bool) -> Color {
Color::Green
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn named_dispatches_to_dark_by_default() {
let t = Theme::named("unknown");
assert_eq!(t.label, Theme::dark().label);
}
#[test]
fn light_has_dark_foreground() {
let t = Theme::light();
assert_eq!(t.value, Style::new().black());
}
#[test]
fn nord_label_is_not_dark_default() {
let t = Theme::nord();
assert_ne!(t.label, Theme::dark().label);
}
}