Skip to main content

void/theme/
mod.rs

1use ratatui::style::Color;
2
3use crate::canvas_timer::SceneStyle;
4
5mod builtin;
6mod catalog;
7mod color;
8mod file;
9mod tokens;
10
11pub use catalog::{themes_dir, ThemeCatalog, ThemeEntry};
12pub use tokens::{ThemeTokens, NAMES as TOKEN_NAMES};
13
14use anyhow::{Context, Result};
15
16use self::builtin::builtin_tokens;
17use self::catalog::ThemeSource;
18use self::file::ThemeFile;
19
20pub struct Theme {
21    pub bg: Color,
22    pub text: Color,
23    pub dim: Color,
24    pub accent: Color,
25    pub on_accent: Color,
26    pub success: Color,
27    pub warning: Color,
28    pub error: Color,
29    pub info: Color,
30    pub progress_dim: Color,
31    pub task_track: Color,
32    pub panel: Color,
33    pub panel_border: Color,
34    pub select_bg: Color,
35    pub select_fg: Color,
36    pub active_bg: Color,
37    pub active_fg: Color,
38}
39
40impl Theme {
41    pub fn from_tokens(tokens: ThemeTokens) -> Self {
42        Self {
43            bg: tokens.bg,
44            text: tokens.text,
45            dim: tokens.dim,
46            accent: tokens.accent,
47            on_accent: tokens.on_accent,
48            success: tokens.success,
49            warning: tokens.warning,
50            error: tokens.error,
51            info: tokens.info,
52            progress_dim: tokens.progress_dim,
53            task_track: tokens.task_track,
54            panel: tokens.panel,
55            panel_border: tokens.panel_border,
56            select_bg: tokens.select_bg,
57            select_fg: tokens.select_fg,
58            active_bg: tokens.active_bg,
59            active_fg: tokens.active_fg,
60        }
61    }
62
63    pub fn into_tokens(self) -> ThemeTokens {
64        ThemeTokens {
65            bg: self.bg,
66            text: self.text,
67            dim: self.dim,
68            accent: self.accent,
69            on_accent: self.on_accent,
70            success: self.success,
71            warning: self.warning,
72            error: self.error,
73            info: self.info,
74            progress_dim: self.progress_dim,
75            task_track: self.task_track,
76            panel: self.panel,
77            panel_border: self.panel_border,
78            select_bg: self.select_bg,
79            select_fg: self.select_fg,
80            active_bg: self.active_bg,
81            active_fg: self.active_fg,
82        }
83    }
84
85    pub fn dark() -> Self {
86        Self {
87            bg: Color::Rgb(15, 15, 20),
88            text: Color::Rgb(225, 225, 230),
89            dim: Color::Rgb(90, 90, 100),
90            accent: Color::Rgb(100, 180, 255),
91            on_accent: Color::Rgb(10, 10, 15),
92            success: Color::Rgb(80, 210, 130),
93            warning: Color::Rgb(245, 185, 70),
94            error: Color::Rgb(245, 85, 85),
95            info: Color::Rgb(170, 140, 250),
96            progress_dim: Color::Rgb(45, 45, 55),
97            task_track: Color::Rgb(35, 35, 42),
98            panel: Color::Rgb(20, 22, 30),
99            panel_border: Color::Rgb(55, 60, 75),
100            select_bg: Color::Rgb(38, 52, 78),
101            select_fg: Color::Rgb(230, 235, 245),
102            active_bg: Color::Rgb(32, 48, 72),
103            active_fg: Color::Rgb(170, 210, 255),
104        }
105    }
106
107    pub fn light() -> Self {
108        Self {
109            bg: Color::Rgb(250, 250, 252),
110            text: Color::Rgb(30, 30, 35),
111            dim: Color::Rgb(140, 140, 150),
112            accent: Color::Rgb(25, 110, 200),
113            on_accent: Color::Rgb(255, 255, 255),
114            success: Color::Rgb(30, 150, 80),
115            warning: Color::Rgb(200, 130, 20),
116            error: Color::Rgb(200, 50, 50),
117            info: Color::Rgb(110, 70, 190),
118            progress_dim: Color::Rgb(200, 205, 215),
119            task_track: Color::Rgb(220, 225, 232),
120            panel: Color::Rgb(242, 245, 250),
121            panel_border: Color::Rgb(190, 198, 210),
122            select_bg: Color::Rgb(210, 225, 245),
123            select_fg: Color::Rgb(20, 40, 70),
124            active_bg: Color::Rgb(195, 218, 245),
125            active_fg: Color::Rgb(15, 60, 120),
126        }
127    }
128
129    pub fn polaris() -> Self {
130        Self {
131            bg: Color::Rgb(10, 14, 30),
132            text: Color::Rgb(215, 225, 250),
133            dim: Color::Rgb(100, 120, 160),
134            accent: Color::Rgb(90, 200, 255),
135            on_accent: Color::Rgb(10, 14, 30),
136            success: Color::Rgb(80, 235, 180),
137            warning: Color::Rgb(255, 160, 75),
138            error: Color::Rgb(255, 95, 120),
139            info: Color::Rgb(180, 135, 255),
140            progress_dim: Color::Rgb(40, 50, 80),
141            task_track: Color::Rgb(25, 35, 60),
142            panel: Color::Rgb(14, 20, 38),
143            panel_border: Color::Rgb(55, 70, 110),
144            select_bg: Color::Rgb(28, 42, 78),
145            select_fg: Color::Rgb(210, 225, 255),
146            active_bg: Color::Rgb(22, 38, 68),
147            active_fg: Color::Rgb(140, 210, 255),
148        }
149    }
150
151    pub fn matrix() -> Self {
152        Self {
153            bg: Color::Rgb(3, 12, 5),
154            text: Color::Rgb(150, 240, 140),
155            dim: Color::Rgb(60, 110, 65),
156            accent: Color::Rgb(80, 230, 90),
157            on_accent: Color::Rgb(0, 0, 0),
158            success: Color::Rgb(70, 220, 110),
159            warning: Color::Rgb(220, 200, 60),
160            error: Color::Rgb(255, 80, 90),
161            info: Color::Rgb(100, 190, 240),
162            progress_dim: Color::Rgb(15, 40, 18),
163            task_track: Color::Rgb(8, 28, 10),
164            panel: Color::Rgb(5, 16, 7),
165            panel_border: Color::Rgb(35, 85, 40),
166            select_bg: Color::Rgb(10, 32, 14),
167            select_fg: Color::Rgb(160, 255, 165),
168            active_bg: Color::Rgb(14, 42, 18),
169            active_fg: Color::Rgb(110, 255, 120),
170        }
171    }
172
173    pub fn scene_style(&self, mode: Color) -> SceneStyle {
174        SceneStyle {
175            mode,
176            track: self.progress_dim,
177            task: self.success,
178            task_dim: self.task_track,
179            bg: self.bg,
180            bg_mid: mix(self.bg, self.panel, 160),
181            bg_light: self.panel,
182            wave: self.accent,
183            core: mode,
184            glow: self.accent,
185            particle: self.dim,
186            text: self.text,
187            session_on: self.accent,
188            session_off: self.dim,
189        }
190    }
191}
192
193pub fn resolve(id: &str, catalog: &ThemeCatalog) -> Result<Theme> {
194    if let Some(tokens) = builtin_tokens(id) {
195        return Ok(Theme::from_tokens(tokens));
196    }
197
198    let entry = catalog.resolve_entry(id)?;
199    let tokens = match &entry.source {
200        ThemeSource::Builtin => builtin_tokens(id).context("builtin theme missing tokens")?,
201        ThemeSource::Embedded(source) => ThemeFile::from_str(source)?.into_tokens()?,
202        ThemeSource::File(path) => ThemeFile::from_path(path)?.into_tokens()?,
203    };
204    Ok(Theme::from_tokens(tokens))
205}
206
207pub fn normalize_theme_id(raw: &str) -> String {
208    raw.trim().to_ascii_lowercase()
209}
210
211fn mix(a: Color, b: Color, t: u8) -> Color {
212    let (ar, ag, ab) = rgb(a);
213    let (br, bg, bb) = rgb(b);
214    let t = t as u16;
215    let inv = 255 - t;
216    Color::Rgb(
217        ((ar as u16 * inv + br as u16 * t) / 255) as u8,
218        ((ag as u16 * inv + bg as u16 * t) / 255) as u8,
219        ((ab as u16 * inv + bb as u16 * t) / 255) as u8,
220    )
221}
222
223fn rgb(c: Color) -> (u8, u8, u8) {
224    match c {
225        Color::Rgb(r, g, b) => (r, g, b),
226        Color::Black => (0, 0, 0),
227        Color::White => (255, 255, 255),
228        _ => (128, 128, 128),
229    }
230}