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