Skip to main content

santui_core/
theme.rs

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}