1use ratatui::style::Color;
2
3fn rgb(hex: u32) -> Color {
4 Color::Rgb(
5 ((hex >> 16) & 0xFF) as u8,
6 ((hex >> 8) & 0xFF) as u8,
7 (hex & 0xFF) as u8,
8 )
9}
10
11fn darken(hex: u32, factor: u8) -> Color {
12 let r = ((hex >> 16) & 0xFF) as u16;
13 let g = ((hex >> 8) & 0xFF) as u16;
14 let b = (hex & 0xFF) as u16;
15 let f = factor as u16;
16 Color::Rgb(
17 (r * f / 100) as u8,
18 (g * f / 100) as u8,
19 (b * f / 100) as u8,
20 )
21}
22
23fn muted(neutral: u32, ink: u32) -> Color {
24 let nr = (neutral >> 16) & 0xFF;
25 let ng = (neutral >> 8) & 0xFF;
26 let nb = neutral & 0xFF;
27 let ir = (ink >> 16) & 0xFF;
28 let ig = (ink >> 8) & 0xFF;
29 let ib = ink & 0xFF;
30 let r = ((nr as u16 * 60 + ir as u16 * 40) / 100) as u8;
31 let g = ((ng as u16 * 60 + ig as u16 * 40) / 100) as u8;
32 let b = ((nb as u16 * 60 + ib as u16 * 40) / 100) as u8;
33 Color::Rgb(r, g, b)
34}
35
36#[derive(Clone, Debug)]
37pub struct Theme {
38 pub accent: Color,
39 pub highlight: Color,
40 pub logo: Color,
41 pub text: Color,
42 pub text_muted: Color,
43 pub background: Color,
44 pub background_panel: Color,
45 pub background_overlay: Color,
46 pub border: Color,
47 pub success: Color,
48 pub error: Color,
49 pub inverted_text: Color,
50}
51
52struct ThemeDef {
53 name: &'static str,
54 neutral: u32,
55 ink: u32,
56 primary: u32,
57 accent: u32,
58 success: u32,
59 error: u32,
60}
61
62const THEMES: &[ThemeDef] = &[
63 ThemeDef {
64 name: "OpenCode",
65 neutral: 0x0a0a0a,
66 ink: 0xeeeeee,
67 primary: 0xfab283,
68 accent: 0x9d7cd8,
69 success: 0x7fd88f,
70 error: 0xe06c75,
71 },
72 ThemeDef {
73 name: "Santui",
74 neutral: 0x141414,
75 ink: 0xffffff,
76 primary: 0xffb900,
77 accent: 0x9d7cd8,
78 success: 0x7fd88f,
79 error: 0xe06c75,
80 },
81 ThemeDef {
82 name: "AMOLED",
83 neutral: 0x000000,
84 ink: 0xffffff,
85 primary: 0xb388ff,
86 accent: 0xff4081,
87 success: 0x00ff88,
88 error: 0xff1744,
89 },
90 ThemeDef {
91 name: "Aura",
92 neutral: 0x15141b,
93 ink: 0xedecee,
94 primary: 0xa277ff,
95 accent: 0xff6767,
96 success: 0x61ffca,
97 error: 0xff6767,
98 },
99 ThemeDef {
100 name: "Ayu",
101 neutral: 0x0f1419,
102 ink: 0xd6dae0,
103 primary: 0x3fb7e3,
104 accent: 0xf2856f,
105 success: 0x78d05c,
106 error: 0xf58572,
107 },
108 ThemeDef {
109 name: "Carbonfox",
110 neutral: 0x393939,
111 ink: 0xf2f4f8,
112 primary: 0x33b1ff,
113 accent: 0xff8389,
114 success: 0x42be65,
115 error: 0xff8389,
116 },
117 ThemeDef {
118 name: "Catppuccin Frappe",
119 neutral: 0x303446,
120 ink: 0xc6d0f5,
121 primary: 0x8da4e2,
122 accent: 0xf4b8e4,
123 success: 0xa6d189,
124 error: 0xe78284,
125 },
126 ThemeDef {
127 name: "Catppuccin Macchiato",
128 neutral: 0x24273a,
129 ink: 0xcad3f5,
130 primary: 0x8aadf4,
131 accent: 0xf5bde6,
132 success: 0xa6da95,
133 error: 0xed8796,
134 },
135 ThemeDef {
136 name: "Catppuccin",
137 neutral: 0x1e1e2e,
138 ink: 0xcdd6f4,
139 primary: 0xb4befe,
140 accent: 0xf38ba8,
141 success: 0xa6d189,
142 error: 0xf38ba8,
143 },
144 ThemeDef {
145 name: "Cobalt2",
146 neutral: 0x193549,
147 ink: 0xffffff,
148 primary: 0x0088ff,
149 accent: 0x2affdf,
150 success: 0x9eff80,
151 error: 0xff0088,
152 },
153 ThemeDef {
154 name: "Cursor",
155 neutral: 0x181818,
156 ink: 0xe4e4e4,
157 primary: 0x88c0d0,
158 accent: 0x88c0d0,
159 success: 0x3fa266,
160 error: 0xe34671,
161 },
162 ThemeDef {
163 name: "Dracula",
164 neutral: 0x1d1e28,
165 ink: 0xf8f8f2,
166 primary: 0xbd93f9,
167 accent: 0xff79c6,
168 success: 0x50fa7b,
169 error: 0xff5555,
170 },
171 ThemeDef {
172 name: "Everforest",
173 neutral: 0x2d353b,
174 ink: 0xd3c6aa,
175 primary: 0xa7c080,
176 accent: 0xd699b6,
177 success: 0xa7c080,
178 error: 0xe67e80,
179 },
180 ThemeDef {
181 name: "Flexoki",
182 neutral: 0x100f0f,
183 ink: 0xcecdc3,
184 primary: 0xda702c,
185 accent: 0x8b7ec8,
186 success: 0x879a39,
187 error: 0xd14d41,
188 },
189 ThemeDef {
190 name: "GitHub",
191 neutral: 0x0d1117,
192 ink: 0xc9d1d9,
193 primary: 0x58a6ff,
194 accent: 0x39c5cf,
195 success: 0x3fb950,
196 error: 0xf85149,
197 },
198 ThemeDef {
199 name: "Gruvbox",
200 neutral: 0x282828,
201 ink: 0xebdbb2,
202 primary: 0x83a598,
203 accent: 0xfb4934,
204 success: 0xb8bb26,
205 error: 0xfb4934,
206 },
207 ThemeDef {
208 name: "Kanagawa",
209 neutral: 0x1f1f28,
210 ink: 0xdcd7ba,
211 primary: 0x7e9cd8,
212 accent: 0xd27e99,
213 success: 0x98bb6c,
214 error: 0xe82424,
215 },
216 ThemeDef {
217 name: "Lucent Orng",
218 neutral: 0x2a1a15,
219 ink: 0xeeeeee,
220 primary: 0xec5b2b,
221 accent: 0xfff7f1,
222 success: 0x6ba1e6,
223 error: 0xe06c75,
224 },
225 ThemeDef {
226 name: "Material",
227 neutral: 0x263238,
228 ink: 0xeeffff,
229 primary: 0x82aaff,
230 accent: 0x89ddff,
231 success: 0xc3e88d,
232 error: 0xf07178,
233 },
234 ThemeDef {
235 name: "Matrix",
236 neutral: 0x0a0e0a,
237 ink: 0x62ff94,
238 primary: 0x2eff6a,
239 accent: 0xc770ff,
240 success: 0x62ff94,
241 error: 0xff4b4b,
242 },
243 ThemeDef {
244 name: "Mercury",
245 neutral: 0x171721,
246 ink: 0xdddde5,
247 primary: 0x8da4f5,
248 accent: 0x8da4f5,
249 success: 0x77c599,
250 error: 0xfc92b4,
251 },
252 ThemeDef {
253 name: "Monokai",
254 neutral: 0x272822,
255 ink: 0xf8f8f2,
256 primary: 0xae81ff,
257 accent: 0xf92672,
258 success: 0xa6e22e,
259 error: 0xf92672,
260 },
261 ThemeDef {
262 name: "Night Owl",
263 neutral: 0x011627,
264 ink: 0xd6deeb,
265 primary: 0x82aaff,
266 accent: 0xf78c6c,
267 success: 0xc5e478,
268 error: 0xef5350,
269 },
270 ThemeDef {
271 name: "Nord",
272 neutral: 0x2e3440,
273 ink: 0xe5e9f0,
274 primary: 0x88c0d0,
275 accent: 0xd57780,
276 success: 0xa3be8c,
277 error: 0xbf616a,
278 },
279 ThemeDef {
280 name: "OC-2",
281 neutral: 0x1f1f1f,
282 ink: 0xf1ece8,
283 primary: 0xfab283,
284 accent: 0xfab283,
285 success: 0x12c905,
286 error: 0xfc533a,
287 },
288 ThemeDef {
289 name: "One Dark",
290 neutral: 0x282c34,
291 ink: 0xabb2bf,
292 primary: 0x61afef,
293 accent: 0x56b6c2,
294 success: 0x98c379,
295 error: 0xe06c75,
296 },
297 ThemeDef {
298 name: "One Dark Pro",
299 neutral: 0x1e222a,
300 ink: 0xabb2bf,
301 primary: 0x61afef,
302 accent: 0xe06c75,
303 success: 0x98c379,
304 error: 0xe06c75,
305 },
306 ThemeDef {
307 name: "Orng",
308 neutral: 0x0a0a0a,
309 ink: 0xeeeeee,
310 primary: 0xec5b2b,
311 accent: 0xfff7f1,
312 success: 0x6ba1e6,
313 error: 0xe06c75,
314 },
315 ThemeDef {
316 name: "Osaka Jade",
317 neutral: 0x111c18,
318 ink: 0xc1c497,
319 primary: 0x2dd5b7,
320 accent: 0x549e6a,
321 success: 0x549e6a,
322 error: 0xff5345,
323 },
324 ThemeDef {
325 name: "Palenight",
326 neutral: 0x292d3e,
327 ink: 0xa6accd,
328 primary: 0x82aaff,
329 accent: 0x89ddff,
330 success: 0xc3e88d,
331 error: 0xf07178,
332 },
333 ThemeDef {
334 name: "Rose Pine",
335 neutral: 0x191724,
336 ink: 0xe0def4,
337 primary: 0x9ccfd8,
338 accent: 0xebbcba,
339 success: 0x31748f,
340 error: 0xeb6f92,
341 },
342 ThemeDef {
343 name: "Shades of Purple",
344 neutral: 0x1a102b,
345 ink: 0xf5f0ff,
346 primary: 0xc792ff,
347 accent: 0xff7ac6,
348 success: 0x7be0b0,
349 error: 0xff7ac6,
350 },
351 ThemeDef {
352 name: "Solarized",
353 neutral: 0x002b36,
354 ink: 0x93a1a1,
355 primary: 0x6c71c4,
356 accent: 0xd33682,
357 success: 0x859900,
358 error: 0xdc322f,
359 },
360 ThemeDef {
361 name: "Synthwave '84",
362 neutral: 0x262335,
363 ink: 0xffffff,
364 primary: 0x36f9f6,
365 accent: 0xb084eb,
366 success: 0x72f1b8,
367 error: 0xfe4450,
368 },
369 ThemeDef {
370 name: "Tokyonight",
371 neutral: 0x1a1b26,
372 ink: 0xc0caf5,
373 primary: 0x7aa2f7,
374 accent: 0xff9e64,
375 success: 0x9ece6a,
376 error: 0xf7768e,
377 },
378 ThemeDef {
379 name: "Vercel",
380 neutral: 0x000000,
381 ink: 0xededed,
382 primary: 0x0070f3,
383 accent: 0x8e4ec6,
384 success: 0x46a758,
385 error: 0xe5484d,
386 },
387 ThemeDef {
388 name: "Vesper",
389 neutral: 0x101010,
390 ink: 0xffffff,
391 primary: 0xffc799,
392 accent: 0xff8080,
393 success: 0x99ffe4,
394 error: 0xff8080,
395 },
396 ThemeDef {
397 name: "Zenburn",
398 neutral: 0x3f3f3f,
399 ink: 0xdcdccc,
400 primary: 0x8cd0d3,
401 accent: 0x93e0e3,
402 success: 0x7f9f7f,
403 error: 0xcc9393,
404 },
405];
406
407impl Theme {
408 pub fn all() -> Vec<(&'static str, Self)> {
409 THEMES
410 .iter()
411 .map(|d| {
412 (
413 d.name,
414 Self {
415 accent: rgb(d.accent),
416 highlight: rgb(d.primary),
417 logo: rgb(d.primary),
418 text: rgb(d.ink),
419 text_muted: muted(d.neutral, d.ink),
420 background: Color::Reset,
421 background_panel: rgb(d.neutral),
422 background_overlay: darken(d.neutral, 40),
423 border: rgb(d.primary),
424 success: rgb(d.success),
425 error: rgb(d.error),
426 inverted_text: rgb(d.neutral),
427 },
428 )
429 })
430 .collect()
431 }
432}
433
434impl Default for Theme {
435 fn default() -> Self {
436 let d = &THEMES[1];
437 Self {
438 accent: rgb(d.accent),
439 highlight: rgb(d.primary),
440 logo: rgb(d.primary),
441 text: rgb(d.ink),
442 text_muted: muted(d.neutral, d.ink),
443 background: Color::Reset,
444 background_panel: rgb(d.neutral),
445 background_overlay: darken(d.neutral, 40),
446 border: rgb(d.primary),
447 success: rgb(d.success),
448 error: rgb(d.error),
449 inverted_text: rgb(d.neutral),
450 }
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn rgb_creates_correct_color() {
460 let c = rgb(0xff8800);
461 assert_eq!(c, Color::Rgb(255, 136, 0));
462 }
463
464 #[test]
465 fn rgb_black() {
466 let c = rgb(0x000000);
467 assert_eq!(c, Color::Rgb(0, 0, 0));
468 }
469
470 #[test]
471 fn rgb_white() {
472 let c = rgb(0xffffff);
473 assert_eq!(c, Color::Rgb(255, 255, 255));
474 }
475
476 #[test]
477 fn darken_reduces_brightness() {
478 let c = darken(0xffffff, 50);
479 assert_eq!(c, Color::Rgb(127, 127, 127));
480 }
481
482 #[test]
483 fn darken_full_brightness() {
484 let c = darken(0xffffff, 100);
485 assert_eq!(c, Color::Rgb(255, 255, 255));
486 }
487
488 #[test]
489 fn darken_minimum() {
490 let c = darken(0xffffff, 0);
491 assert_eq!(c, Color::Rgb(0, 0, 0));
492 }
493
494 #[test]
495 fn muted_creates_mixed_color() {
496 let c = muted(0x000000, 0xffffff);
497 assert_eq!(c, Color::Rgb(102, 102, 102));
498 }
499
500 #[test]
501 fn theme_all_returns_all_themes() {
502 let themes = Theme::all();
503 assert_eq!(themes.len(), THEMES.len());
504 for (i, (name, _)) in themes.iter().enumerate() {
505 assert_eq!(*name, THEMES[i].name);
506 }
507 }
508
509 #[test]
510 fn theme_default_is_santui() {
511 let default = Theme::default();
512 let themes = Theme::all();
513 let santui = &themes[1].1;
514 assert_eq!(default.accent, santui.accent);
515 assert_eq!(default.highlight, santui.highlight);
516 assert_eq!(default.text, santui.text);
517 }
518
519 #[test]
520 fn theme_has_background_reset() {
521 let default = Theme::default();
522 assert_eq!(default.background, Color::Reset);
523 }
524}