Skip to main content

slt/style/
theme.rs

1use super::*;
2
3/// A color theme that flows through all widgets automatically.
4///
5/// Construct with [`Theme::dark()`] or [`Theme::light()`], or build a custom
6/// theme by filling in the fields directly. Pass the theme via [`crate::RunConfig`]
7/// and every widget will pick up the colors without any extra wiring.
8#[derive(Debug, Clone, Copy)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Theme {
11    /// Primary accent color, used for focused borders and highlights.
12    pub primary: Color,
13    /// Secondary accent color, used for less prominent highlights.
14    pub secondary: Color,
15    /// Accent color for decorative elements.
16    pub accent: Color,
17    /// Default foreground text color.
18    pub text: Color,
19    /// Dimmed text color for secondary labels and hints.
20    pub text_dim: Color,
21    /// Border color for unfocused containers.
22    pub border: Color,
23    /// Background color. Typically [`Color::Reset`] to inherit the terminal background.
24    pub bg: Color,
25    /// Color for success states (e.g., toast notifications).
26    pub success: Color,
27    /// Color for warning states.
28    pub warning: Color,
29    /// Color for error states.
30    pub error: Color,
31    /// Background color for selected list/table rows.
32    pub selected_bg: Color,
33    /// Foreground color for selected list/table rows.
34    pub selected_fg: Color,
35    /// Subtle surface color for card backgrounds and elevated containers.
36    pub surface: Color,
37    /// Hover/active surface color, one step brighter than `surface`.
38    ///
39    /// Used for interactive element hover states. Should be visually
40    /// distinguishable from both `surface` and `border`.
41    pub surface_hover: Color,
42    /// Secondary text color guaranteed readable on `surface` backgrounds.
43    ///
44    /// Use this instead of `text_dim` when rendering on `surface`-colored
45    /// containers. `text_dim` is tuned for the main `bg`; on `surface` it
46    /// may lack contrast.
47    pub surface_text: Color,
48}
49
50impl Theme {
51    /// Create a dark theme with cyan primary and white text.
52    pub fn dark() -> Self {
53        Self {
54            primary: Color::Cyan,
55            secondary: Color::Blue,
56            accent: Color::Magenta,
57            text: Color::White,
58            text_dim: Color::Indexed(245),
59            border: Color::Indexed(240),
60            bg: Color::Reset,
61            success: Color::Green,
62            warning: Color::Yellow,
63            error: Color::Red,
64            selected_bg: Color::Cyan,
65            selected_fg: Color::Black,
66            surface: Color::Indexed(236),
67            surface_hover: Color::Indexed(238),
68            surface_text: Color::Indexed(250),
69        }
70    }
71
72    /// Create a light theme with blue primary and black text.
73    pub fn light() -> Self {
74        Self {
75            primary: Color::Blue,
76            secondary: Color::Cyan,
77            accent: Color::Magenta,
78            text: Color::Black,
79            text_dim: Color::Indexed(240),
80            border: Color::Indexed(245),
81            bg: Color::Reset,
82            success: Color::Green,
83            warning: Color::Yellow,
84            error: Color::Red,
85            selected_bg: Color::Blue,
86            selected_fg: Color::White,
87            surface: Color::Indexed(254),
88            surface_hover: Color::Indexed(252),
89            surface_text: Color::Indexed(238),
90        }
91    }
92
93    /// Create a [`ThemeBuilder`] for configuring a custom theme.
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// use slt::{Color, Theme};
99    ///
100    /// let theme = Theme::builder()
101    ///     .primary(Color::Rgb(255, 107, 107))
102    ///     .accent(Color::Cyan)
103    ///     .build();
104    /// ```
105    pub fn builder() -> ThemeBuilder {
106        ThemeBuilder {
107            primary: None,
108            secondary: None,
109            accent: None,
110            text: None,
111            text_dim: None,
112            border: None,
113            bg: None,
114            success: None,
115            warning: None,
116            error: None,
117            selected_bg: None,
118            selected_fg: None,
119            surface: None,
120            surface_hover: None,
121            surface_text: None,
122        }
123    }
124
125    /// Dracula theme — purple primary on dark gray.
126    pub fn dracula() -> Self {
127        Self {
128            primary: Color::Rgb(189, 147, 249),
129            secondary: Color::Rgb(139, 233, 253),
130            accent: Color::Rgb(255, 121, 198),
131            text: Color::Rgb(248, 248, 242),
132            text_dim: Color::Rgb(98, 114, 164),
133            border: Color::Rgb(68, 71, 90),
134            bg: Color::Rgb(40, 42, 54),
135            success: Color::Rgb(80, 250, 123),
136            warning: Color::Rgb(241, 250, 140),
137            error: Color::Rgb(255, 85, 85),
138            selected_bg: Color::Rgb(189, 147, 249),
139            selected_fg: Color::Rgb(40, 42, 54),
140            surface: Color::Rgb(68, 71, 90),
141            surface_hover: Color::Rgb(98, 100, 120),
142            surface_text: Color::Rgb(191, 194, 210),
143        }
144    }
145
146    /// Catppuccin Mocha theme — lavender primary on dark base.
147    pub fn catppuccin() -> Self {
148        Self {
149            primary: Color::Rgb(180, 190, 254),
150            secondary: Color::Rgb(137, 180, 250),
151            accent: Color::Rgb(245, 194, 231),
152            text: Color::Rgb(205, 214, 244),
153            text_dim: Color::Rgb(127, 132, 156),
154            border: Color::Rgb(88, 91, 112),
155            bg: Color::Rgb(30, 30, 46),
156            success: Color::Rgb(166, 227, 161),
157            warning: Color::Rgb(249, 226, 175),
158            error: Color::Rgb(243, 139, 168),
159            selected_bg: Color::Rgb(180, 190, 254),
160            selected_fg: Color::Rgb(30, 30, 46),
161            surface: Color::Rgb(49, 50, 68),
162            surface_hover: Color::Rgb(69, 71, 90),
163            surface_text: Color::Rgb(166, 173, 200),
164        }
165    }
166
167    /// Nord theme — frost blue primary on polar night.
168    pub fn nord() -> Self {
169        Self {
170            primary: Color::Rgb(136, 192, 208),
171            secondary: Color::Rgb(129, 161, 193),
172            accent: Color::Rgb(180, 142, 173),
173            text: Color::Rgb(236, 239, 244),
174            text_dim: Color::Rgb(76, 86, 106),
175            border: Color::Rgb(76, 86, 106),
176            bg: Color::Rgb(46, 52, 64),
177            success: Color::Rgb(163, 190, 140),
178            warning: Color::Rgb(235, 203, 139),
179            error: Color::Rgb(191, 97, 106),
180            selected_bg: Color::Rgb(136, 192, 208),
181            selected_fg: Color::Rgb(46, 52, 64),
182            surface: Color::Rgb(59, 66, 82),
183            surface_hover: Color::Rgb(67, 76, 94),
184            surface_text: Color::Rgb(216, 222, 233),
185        }
186    }
187
188    /// Solarized Dark theme — blue primary on dark base.
189    pub fn solarized_dark() -> Self {
190        Self {
191            primary: Color::Rgb(38, 139, 210),
192            secondary: Color::Rgb(42, 161, 152),
193            accent: Color::Rgb(211, 54, 130),
194            text: Color::Rgb(131, 148, 150),
195            text_dim: Color::Rgb(88, 110, 117),
196            border: Color::Rgb(88, 110, 117),
197            bg: Color::Rgb(0, 43, 54),
198            success: Color::Rgb(133, 153, 0),
199            warning: Color::Rgb(181, 137, 0),
200            error: Color::Rgb(220, 50, 47),
201            selected_bg: Color::Rgb(38, 139, 210),
202            selected_fg: Color::Rgb(253, 246, 227),
203            surface: Color::Rgb(7, 54, 66),
204            surface_hover: Color::Rgb(23, 72, 85),
205            surface_text: Color::Rgb(147, 161, 161),
206        }
207    }
208
209    /// Tokyo Night theme — blue primary on dark storm base.
210    pub fn tokyo_night() -> Self {
211        Self {
212            primary: Color::Rgb(122, 162, 247),
213            secondary: Color::Rgb(125, 207, 255),
214            accent: Color::Rgb(187, 154, 247),
215            text: Color::Rgb(169, 177, 214),
216            text_dim: Color::Rgb(86, 95, 137),
217            border: Color::Rgb(54, 58, 79),
218            bg: Color::Rgb(26, 27, 38),
219            success: Color::Rgb(158, 206, 106),
220            warning: Color::Rgb(224, 175, 104),
221            error: Color::Rgb(247, 118, 142),
222            selected_bg: Color::Rgb(122, 162, 247),
223            selected_fg: Color::Rgb(26, 27, 38),
224            surface: Color::Rgb(36, 40, 59),
225            surface_hover: Color::Rgb(41, 46, 66),
226            surface_text: Color::Rgb(192, 202, 245),
227        }
228    }
229}
230
231/// Builder for creating custom themes with defaults from `Theme::dark()`.
232pub struct ThemeBuilder {
233    primary: Option<Color>,
234    secondary: Option<Color>,
235    accent: Option<Color>,
236    text: Option<Color>,
237    text_dim: Option<Color>,
238    border: Option<Color>,
239    bg: Option<Color>,
240    success: Option<Color>,
241    warning: Option<Color>,
242    error: Option<Color>,
243    selected_bg: Option<Color>,
244    selected_fg: Option<Color>,
245    surface: Option<Color>,
246    surface_hover: Option<Color>,
247    surface_text: Option<Color>,
248}
249
250impl ThemeBuilder {
251    pub fn primary(mut self, color: Color) -> Self {
252        self.primary = Some(color);
253        self
254    }
255
256    pub fn secondary(mut self, color: Color) -> Self {
257        self.secondary = Some(color);
258        self
259    }
260
261    pub fn accent(mut self, color: Color) -> Self {
262        self.accent = Some(color);
263        self
264    }
265
266    pub fn text(mut self, color: Color) -> Self {
267        self.text = Some(color);
268        self
269    }
270
271    pub fn text_dim(mut self, color: Color) -> Self {
272        self.text_dim = Some(color);
273        self
274    }
275
276    pub fn border(mut self, color: Color) -> Self {
277        self.border = Some(color);
278        self
279    }
280
281    pub fn bg(mut self, color: Color) -> Self {
282        self.bg = Some(color);
283        self
284    }
285
286    pub fn success(mut self, color: Color) -> Self {
287        self.success = Some(color);
288        self
289    }
290
291    pub fn warning(mut self, color: Color) -> Self {
292        self.warning = Some(color);
293        self
294    }
295
296    pub fn error(mut self, color: Color) -> Self {
297        self.error = Some(color);
298        self
299    }
300
301    pub fn selected_bg(mut self, color: Color) -> Self {
302        self.selected_bg = Some(color);
303        self
304    }
305
306    pub fn selected_fg(mut self, color: Color) -> Self {
307        self.selected_fg = Some(color);
308        self
309    }
310
311    pub fn surface(mut self, color: Color) -> Self {
312        self.surface = Some(color);
313        self
314    }
315
316    pub fn surface_hover(mut self, color: Color) -> Self {
317        self.surface_hover = Some(color);
318        self
319    }
320
321    pub fn surface_text(mut self, color: Color) -> Self {
322        self.surface_text = Some(color);
323        self
324    }
325
326    pub fn build(self) -> Theme {
327        let defaults = Theme::dark();
328        Theme {
329            primary: self.primary.unwrap_or(defaults.primary),
330            secondary: self.secondary.unwrap_or(defaults.secondary),
331            accent: self.accent.unwrap_or(defaults.accent),
332            text: self.text.unwrap_or(defaults.text),
333            text_dim: self.text_dim.unwrap_or(defaults.text_dim),
334            border: self.border.unwrap_or(defaults.border),
335            bg: self.bg.unwrap_or(defaults.bg),
336            success: self.success.unwrap_or(defaults.success),
337            warning: self.warning.unwrap_or(defaults.warning),
338            error: self.error.unwrap_or(defaults.error),
339            selected_bg: self.selected_bg.unwrap_or(defaults.selected_bg),
340            selected_fg: self.selected_fg.unwrap_or(defaults.selected_fg),
341            surface: self.surface.unwrap_or(defaults.surface),
342            surface_hover: self.surface_hover.unwrap_or(defaults.surface_hover),
343            surface_text: self.surface_text.unwrap_or(defaults.surface_text),
344        }
345    }
346}
347
348impl Default for Theme {
349    fn default() -> Self {
350        Self::dark()
351    }
352}