Skip to main content

mockforge_tui/
theme.rs

1//! Color palette and style helpers with dark and light theme support.
2
3use std::sync::OnceLock;
4
5use ratatui::style::{Color, Modifier, Style};
6
7/// Active palette, set once at startup via [`Theme::init`].
8static PALETTE: OnceLock<Palette> = OnceLock::new();
9
10/// A complete colour palette.
11#[derive(Debug, Clone, Copy)]
12pub struct Palette {
13    pub bg: Color,
14    pub fg: Color,
15    pub dim: Color,
16    pub surface: Color,
17    pub overlay: Color,
18    pub blue: Color,
19    pub green: Color,
20    pub red: Color,
21    pub yellow: Color,
22    pub peach: Color,
23    pub mauve: Color,
24    pub teal: Color,
25    pub pink: Color,
26}
27
28impl Palette {
29    /// Catppuccin Mocha (dark).
30    pub const DARK: Self = Self {
31        bg: Color::Rgb(22, 22, 30),
32        fg: Color::Rgb(205, 214, 244),
33        dim: Color::Rgb(108, 112, 134),
34        surface: Color::Rgb(30, 30, 46),
35        overlay: Color::Rgb(49, 50, 68),
36        blue: Color::Rgb(137, 180, 250),
37        green: Color::Rgb(166, 227, 161),
38        red: Color::Rgb(243, 139, 168),
39        yellow: Color::Rgb(249, 226, 175),
40        peach: Color::Rgb(250, 179, 135),
41        mauve: Color::Rgb(203, 166, 247),
42        teal: Color::Rgb(148, 226, 213),
43        pink: Color::Rgb(245, 194, 231),
44    };
45
46    /// Catppuccin Latte (light).
47    pub const LIGHT: Self = Self {
48        bg: Color::Rgb(239, 241, 245),
49        fg: Color::Rgb(76, 79, 105),
50        dim: Color::Rgb(140, 143, 161),
51        surface: Color::Rgb(230, 233, 239),
52        overlay: Color::Rgb(204, 208, 218),
53        blue: Color::Rgb(30, 102, 245),
54        green: Color::Rgb(64, 160, 43),
55        red: Color::Rgb(210, 15, 57),
56        yellow: Color::Rgb(223, 142, 29),
57        peach: Color::Rgb(254, 100, 11),
58        mauve: Color::Rgb(136, 57, 239),
59        teal: Color::Rgb(23, 146, 153),
60        pink: Color::Rgb(234, 118, 203),
61    };
62}
63
64/// Theme accessor. All colour/style lookups go through here.
65pub struct Theme;
66
67impl Theme {
68    /// Initialise the global palette. Call once at startup.
69    /// If not called, defaults to dark.
70    pub fn init(light: bool) {
71        let palette = if light { Palette::LIGHT } else { Palette::DARK };
72        let _ = PALETTE.set(palette);
73    }
74
75    #[inline]
76    fn p() -> &'static Palette {
77        PALETTE.get().unwrap_or(&Palette::DARK)
78    }
79
80    // ── Colour accessors ────────────────────────────────────────────
81    #[inline]
82    pub fn fg() -> Color {
83        Self::p().fg
84    }
85    #[inline]
86    pub fn bg() -> Color {
87        Self::p().bg
88    }
89    #[inline]
90    pub fn dim_color() -> Color {
91        Self::p().dim
92    }
93    #[inline]
94    pub fn surface_color() -> Color {
95        Self::p().surface
96    }
97    #[inline]
98    pub fn overlay_color() -> Color {
99        Self::p().overlay
100    }
101    #[inline]
102    pub fn blue() -> Color {
103        Self::p().blue
104    }
105    #[inline]
106    pub fn green() -> Color {
107        Self::p().green
108    }
109    #[inline]
110    pub fn red() -> Color {
111        Self::p().red
112    }
113    #[inline]
114    pub fn yellow() -> Color {
115        Self::p().yellow
116    }
117    #[inline]
118    pub fn peach() -> Color {
119        Self::p().peach
120    }
121    #[inline]
122    pub fn mauve() -> Color {
123        Self::p().mauve
124    }
125    #[inline]
126    pub fn teal() -> Color {
127        Self::p().teal
128    }
129    #[inline]
130    pub fn pink() -> Color {
131        Self::p().pink
132    }
133
134    // ── Backward-compatible `const` aliases (dark defaults) ─────────
135    // These exist only for code that uses `Theme::FG` etc. in const contexts.
136    // Prefer the functions above for theme-aware colours.
137    pub const FG: Color = Color::Rgb(205, 214, 244);
138    pub const BG: Color = Color::Rgb(22, 22, 30);
139    pub const DIM: Color = Color::Rgb(108, 112, 134);
140    pub const SURFACE: Color = Color::Rgb(30, 30, 46);
141    pub const OVERLAY: Color = Color::Rgb(49, 50, 68);
142    pub const BLUE: Color = Color::Rgb(137, 180, 250);
143    pub const GREEN: Color = Color::Rgb(166, 227, 161);
144    pub const RED: Color = Color::Rgb(243, 139, 168);
145    pub const YELLOW: Color = Color::Rgb(249, 226, 175);
146    pub const PEACH: Color = Color::Rgb(250, 179, 135);
147    pub const MAUVE: Color = Color::Rgb(203, 166, 247);
148    pub const TEAL: Color = Color::Rgb(148, 226, 213);
149    pub const PINK: Color = Color::Rgb(245, 194, 231);
150
151    pub const STATUS_UP: Color = Self::GREEN;
152    pub const STATUS_DOWN: Color = Self::RED;
153    pub const STATUS_WARN: Color = Self::YELLOW;
154
155    // ── Composed styles (theme-aware) ───────────────────────────────
156
157    pub fn base() -> Style {
158        Style::default().fg(Self::fg()).bg(Self::bg())
159    }
160
161    pub fn surface() -> Style {
162        Style::default().fg(Self::fg()).bg(Self::surface_color())
163    }
164
165    pub fn title() -> Style {
166        Style::default().fg(Self::blue()).add_modifier(Modifier::BOLD)
167    }
168
169    pub fn highlight() -> Style {
170        Style::default().fg(Self::bg()).bg(Self::blue())
171    }
172
173    pub fn tab_active() -> Style {
174        Style::default().fg(Self::bg()).bg(Self::blue()).add_modifier(Modifier::BOLD)
175    }
176
177    pub fn tab_inactive() -> Style {
178        Style::default().fg(Self::dim_color()).bg(Self::surface_color())
179    }
180
181    pub fn status_bar() -> Style {
182        Style::default().fg(Self::fg()).bg(Self::overlay_color())
183    }
184
185    pub fn key_hint() -> Style {
186        Style::default().fg(Self::yellow()).add_modifier(Modifier::BOLD)
187    }
188
189    pub fn error() -> Style {
190        Style::default().fg(Self::red())
191    }
192
193    pub fn success() -> Style {
194        Style::default().fg(Self::green())
195    }
196
197    pub fn dim() -> Style {
198        Style::default().fg(Self::dim_color())
199    }
200
201    pub fn http_method(method: &str) -> Style {
202        let color = match method.to_uppercase().as_str() {
203            "GET" => Self::green(),
204            "POST" => Self::blue(),
205            "PUT" => Self::yellow(),
206            "PATCH" => Self::peach(),
207            "DELETE" => Self::red(),
208            "HEAD" | "OPTIONS" => Self::dim_color(),
209            _ => Self::fg(),
210        };
211        Style::default().fg(color).add_modifier(Modifier::BOLD)
212    }
213
214    pub fn status_code(code: u16) -> Style {
215        let color = match code {
216            200..=299 => Self::green(),
217            300..=399 => Self::blue(),
218            400..=499 => Self::yellow(),
219            500..=599 => Self::red(),
220            _ => Self::dim_color(),
221        };
222        Style::default().fg(color)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn dark_palette_default() {
232        // Without init, should return dark palette colours.
233        let p = Theme::p();
234        assert_eq!(p.bg, Palette::DARK.bg);
235        assert_eq!(p.fg, Palette::DARK.fg);
236    }
237
238    #[test]
239    fn composed_styles_use_palette() {
240        let base = Theme::base();
241        assert_eq!(base.fg, Some(Theme::fg()));
242        assert_eq!(base.bg, Some(Theme::bg()));
243    }
244
245    #[test]
246    fn http_method_styles() {
247        let get_style = Theme::http_method("GET");
248        assert_eq!(get_style.fg, Some(Theme::green()));
249
250        let delete_style = Theme::http_method("DELETE");
251        assert_eq!(delete_style.fg, Some(Theme::red()));
252    }
253
254    #[test]
255    fn status_code_styles() {
256        assert_eq!(Theme::status_code(200).fg, Some(Theme::green()));
257        assert_eq!(Theme::status_code(404).fg, Some(Theme::yellow()));
258        assert_eq!(Theme::status_code(500).fg, Some(Theme::red()));
259    }
260
261    #[test]
262    fn const_aliases_match_dark_palette() {
263        assert_eq!(Theme::FG, Palette::DARK.fg);
264        assert_eq!(Theme::BG, Palette::DARK.bg);
265        assert_eq!(Theme::BLUE, Palette::DARK.blue);
266        assert_eq!(Theme::RED, Palette::DARK.red);
267    }
268
269    #[test]
270    fn light_palette_differs_from_dark() {
271        assert_ne!(Palette::DARK.bg, Palette::LIGHT.bg);
272        assert_ne!(Palette::DARK.fg, Palette::LIGHT.fg);
273        assert_ne!(Palette::DARK.blue, Palette::LIGHT.blue);
274    }
275}