npm_run_scripts/tui/
theme.rs

1//! Color theme for the TUI.
2
3use ratatui::style::{Color, Modifier, Style};
4
5use crate::config::Theme as ThemeConfig;
6
7/// Color theme for the TUI.
8#[derive(Debug, Clone)]
9pub struct Theme {
10    // Header
11    header_bg: Color,
12    header_fg: Color,
13
14    // Filter
15    filter_fg: Color,
16    filter_placeholder_fg: Color,
17
18    // Scripts
19    number_fg: Color,
20    script_fg: Color,
21    selected_bg: Color,
22    selected_fg: Color,
23    cursor_fg: Color,
24    multiselect_fg: Color,
25
26    // Description
27    description_fg: Color,
28    command_fg: Color,
29    separator_fg: Color,
30
31    // Footer
32    footer_fg: Color,
33    key_fg: Color,
34
35    // Status
36    error_fg: Color,
37    success_fg: Color,
38    warning_fg: Color,
39}
40
41impl Default for Theme {
42    fn default() -> Self {
43        Self::new(&ThemeConfig::Default)
44    }
45}
46
47impl Theme {
48    /// Create a theme from configuration.
49    pub fn new(config: &ThemeConfig) -> Self {
50        match config {
51            ThemeConfig::Default => Self::default_theme(),
52            ThemeConfig::Minimal => Self::minimal_theme(),
53            ThemeConfig::None => Self::no_color_theme(),
54        }
55    }
56
57    /// Default full-color theme.
58    fn default_theme() -> Self {
59        Self {
60            header_bg: Color::Blue,
61            header_fg: Color::White,
62
63            filter_fg: Color::Yellow,
64            filter_placeholder_fg: Color::DarkGray,
65
66            number_fg: Color::Cyan,
67            script_fg: Color::White,
68            selected_bg: Color::Blue,
69            selected_fg: Color::White,
70            cursor_fg: Color::Green,
71            multiselect_fg: Color::Magenta,
72
73            description_fg: Color::Gray,
74            command_fg: Color::DarkGray,
75            separator_fg: Color::DarkGray,
76
77            footer_fg: Color::DarkGray,
78            key_fg: Color::Cyan,
79
80            error_fg: Color::Red,
81            success_fg: Color::Green,
82            warning_fg: Color::Yellow,
83        }
84    }
85
86    /// Minimal color theme (fewer colors, less bold).
87    fn minimal_theme() -> Self {
88        Self {
89            header_bg: Color::Reset,
90            header_fg: Color::White,
91
92            filter_fg: Color::White,
93            filter_placeholder_fg: Color::DarkGray,
94
95            number_fg: Color::Gray,
96            script_fg: Color::White,
97            selected_bg: Color::Reset,
98            selected_fg: Color::Cyan,
99            cursor_fg: Color::White,
100            multiselect_fg: Color::White,
101
102            description_fg: Color::Gray,
103            command_fg: Color::DarkGray,
104            separator_fg: Color::DarkGray,
105
106            footer_fg: Color::DarkGray,
107            key_fg: Color::Gray,
108
109            error_fg: Color::Red,
110            success_fg: Color::Green,
111            warning_fg: Color::Yellow,
112        }
113    }
114
115    /// No-color theme (monochrome).
116    fn no_color_theme() -> Self {
117        Self {
118            header_bg: Color::Reset,
119            header_fg: Color::Reset,
120
121            filter_fg: Color::Reset,
122            filter_placeholder_fg: Color::Reset,
123
124            number_fg: Color::Reset,
125            script_fg: Color::Reset,
126            selected_bg: Color::Reset,
127            selected_fg: Color::Reset,
128            cursor_fg: Color::Reset,
129            multiselect_fg: Color::Reset,
130
131            description_fg: Color::Reset,
132            command_fg: Color::Reset,
133            separator_fg: Color::Reset,
134
135            footer_fg: Color::Reset,
136            key_fg: Color::Reset,
137
138            error_fg: Color::Reset,
139            success_fg: Color::Reset,
140            warning_fg: Color::Reset,
141        }
142    }
143
144    // ==================== Header Styles ====================
145
146    /// Get the header style.
147    pub fn header(&self) -> Style {
148        Style::default()
149            .fg(self.header_fg)
150            .bg(self.header_bg)
151            .add_modifier(Modifier::BOLD)
152    }
153
154    /// Get the header project name style.
155    pub fn header_project(&self) -> Style {
156        Style::default()
157            .fg(self.header_fg)
158            .bg(self.header_bg)
159            .add_modifier(Modifier::BOLD)
160    }
161
162    /// Get the header runner style.
163    pub fn header_runner(&self) -> Style {
164        Style::default().fg(self.header_fg).bg(self.header_bg)
165    }
166
167    // ==================== Filter Styles ====================
168
169    /// Get the filter text style.
170    pub fn filter(&self) -> Style {
171        Style::default().fg(self.filter_fg)
172    }
173
174    /// Get the filter active style (when in filter mode).
175    pub fn filter_active(&self) -> Style {
176        Style::default()
177            .fg(self.filter_fg)
178            .add_modifier(Modifier::BOLD)
179    }
180
181    /// Get the filter placeholder style.
182    pub fn filter_placeholder(&self) -> Style {
183        Style::default()
184            .fg(self.filter_placeholder_fg)
185            .add_modifier(Modifier::ITALIC)
186    }
187
188    // ==================== Script Styles ====================
189
190    /// Get the script number style.
191    pub fn number(&self) -> Style {
192        Style::default()
193            .fg(self.number_fg)
194            .add_modifier(Modifier::DIM)
195    }
196
197    /// Get the script name style.
198    pub fn script(&self) -> Style {
199        Style::default().fg(self.script_fg)
200    }
201
202    /// Get the selected script style.
203    pub fn selected(&self) -> Style {
204        if self.selected_bg == Color::Reset {
205            Style::default()
206                .fg(self.selected_fg)
207                .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
208        } else {
209            Style::default()
210                .fg(self.selected_fg)
211                .bg(self.selected_bg)
212                .add_modifier(Modifier::BOLD)
213        }
214    }
215
216    /// Get the cursor style.
217    pub fn cursor(&self) -> Style {
218        Style::default()
219            .fg(self.cursor_fg)
220            .add_modifier(Modifier::BOLD)
221    }
222
223    /// Get the multiselect marker style.
224    pub fn multiselect(&self) -> Style {
225        Style::default()
226            .fg(self.multiselect_fg)
227            .add_modifier(Modifier::BOLD)
228    }
229
230    // ==================== Description Styles ====================
231
232    /// Get the description style.
233    pub fn description(&self) -> Style {
234        Style::default().fg(self.description_fg)
235    }
236
237    /// Get the command preview style.
238    pub fn command(&self) -> Style {
239        Style::default()
240            .fg(self.command_fg)
241            .add_modifier(Modifier::ITALIC)
242    }
243
244    /// Get the separator style.
245    pub fn separator(&self) -> Style {
246        Style::default().fg(self.separator_fg)
247    }
248
249    // ==================== Footer Styles ====================
250
251    /// Get the footer style.
252    pub fn footer(&self) -> Style {
253        Style::default().fg(self.footer_fg)
254    }
255
256    /// Get the keybinding style.
257    pub fn key(&self) -> Style {
258        Style::default()
259            .fg(self.key_fg)
260            .add_modifier(Modifier::BOLD)
261    }
262
263    // ==================== Status Styles ====================
264
265    /// Get the error style.
266    pub fn error(&self) -> Style {
267        Style::default()
268            .fg(self.error_fg)
269            .add_modifier(Modifier::BOLD)
270    }
271
272    /// Get the success style.
273    pub fn success(&self) -> Style {
274        Style::default().fg(self.success_fg)
275    }
276
277    /// Get the warning style.
278    pub fn warning(&self) -> Style {
279        Style::default().fg(self.warning_fg)
280    }
281
282    // ==================== Utility Styles ====================
283
284    /// Get style for dimmed/muted text.
285    pub fn dim(&self) -> Style {
286        Style::default().add_modifier(Modifier::DIM)
287    }
288
289    /// Get bold style.
290    pub fn bold(&self) -> Style {
291        Style::default().add_modifier(Modifier::BOLD)
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_default_theme() {
301        let theme = Theme::default();
302        assert_eq!(theme.header_bg, Color::Blue);
303        assert_eq!(theme.header_fg, Color::White);
304    }
305
306    #[test]
307    fn test_theme_from_config() {
308        let theme = Theme::new(&ThemeConfig::Default);
309        assert_eq!(theme.header_bg, Color::Blue);
310
311        let theme = Theme::new(&ThemeConfig::Minimal);
312        assert_eq!(theme.header_bg, Color::Reset);
313
314        let theme = Theme::new(&ThemeConfig::None);
315        assert_eq!(theme.header_fg, Color::Reset);
316    }
317
318    #[test]
319    fn test_header_style() {
320        let theme = Theme::default();
321        let style = theme.header();
322
323        // Should have bold modifier
324        assert!(style.add_modifier.contains(Modifier::BOLD));
325    }
326
327    #[test]
328    fn test_selected_style_minimal() {
329        let theme = Theme::new(&ThemeConfig::Minimal);
330        let style = theme.selected();
331
332        // Minimal theme uses underline instead of background
333        assert!(style.add_modifier.contains(Modifier::UNDERLINED));
334    }
335
336    #[test]
337    fn test_no_color_theme() {
338        let theme = Theme::new(&ThemeConfig::None);
339
340        // All colors should be Reset
341        assert_eq!(theme.header_bg, Color::Reset);
342        assert_eq!(theme.filter_fg, Color::Reset);
343        assert_eq!(theme.number_fg, Color::Reset);
344    }
345}