gravityfile_tui/
theme.rs

1//! Color theme for the TUI.
2//!
3//! Provides a comprehensive theming system with dark and light themes,
4//! using a semantic color palette based on Tailwind CSS colors.
5
6use ratatui::style::{Color, Modifier, Style};
7
8/// Theme variant (dark or light).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ThemeVariant {
11    #[default]
12    Dark,
13    Light,
14}
15
16/// Color theme for the TUI.
17#[derive(Debug, Clone)]
18pub struct Theme {
19    /// Current theme variant.
20    pub variant: ThemeVariant,
21
22    // Base colors
23    pub background: Color,
24    pub foreground: Color,
25    pub muted: Color,
26
27    // Interactive elements
28    pub selected: Style,
29    pub hover: Style,
30
31    // Size indicators (gradient by proportion)
32    pub size_huge: Color,   // > 50%
33    pub size_large: Color,  // > 25%
34    pub size_medium: Color, // > 10%
35    pub size_small: Color,  // > 1%
36    pub size_tiny: Color,   // <= 1%
37
38    // Status colors
39    pub success: Color,
40    pub warning: Color,
41    pub error: Color,
42    pub info: Color,
43
44    // UI elements
45    pub border: Style,
46    pub title: Style,
47    pub help_key: Style,
48    pub help_desc: Style,
49
50    // Tree elements
51    pub tree_lines: Style,
52    pub directory: Style,
53    pub file: Style,
54    pub symlink: Style,
55    pub executable: Style,
56
57    // Progress
58    pub progress_bar: Style,
59    pub progress_bg: Style,
60
61    // Header/Footer
62    pub header: Style,
63    pub footer: Style,
64
65    // Footer keybinding hints (polished pill/badge style)
66    pub help_key_bg: Color,
67    pub help_key_fg: Color,
68    pub help_desc_fg: Color,
69    pub footer_bg: Color,
70
71    // Command palette
72    pub command_prompt: Style,
73    pub command_input: Style,
74    pub command_cursor: Style,
75
76    // Marked items
77    pub marked: Style,
78}
79
80impl Theme {
81    /// Dark theme using a slate-based palette.
82    pub fn dark() -> Self {
83        // Slate palette (Tailwind CSS)
84        let slate_50 = Color::Rgb(248, 250, 252);
85        let slate_100 = Color::Rgb(241, 245, 249);
86        let slate_300 = Color::Rgb(203, 213, 225);
87        let slate_400 = Color::Rgb(148, 163, 184);
88        let slate_500 = Color::Rgb(100, 116, 139);
89        let slate_600 = Color::Rgb(71, 85, 105);
90        let slate_700 = Color::Rgb(51, 65, 85);
91        let slate_800 = Color::Rgb(30, 41, 59);
92        let slate_900 = Color::Rgb(15, 23, 42);
93
94        // Accent colors (Tailwind CSS)
95        let blue_400 = Color::Rgb(96, 165, 250);
96        let blue_500 = Color::Rgb(59, 130, 246);
97        let green_500 = Color::Rgb(34, 197, 94);
98        let yellow_500 = Color::Rgb(234, 179, 8);
99        let orange_500 = Color::Rgb(249, 115, 22);
100        let red_500 = Color::Rgb(239, 68, 68);
101        let cyan_400 = Color::Rgb(34, 211, 238);
102        let amber_500 = Color::Rgb(245, 158, 11);
103
104        Self {
105            variant: ThemeVariant::Dark,
106            background: slate_900,
107            foreground: slate_100,
108            muted: slate_500,
109
110            selected: Style::new().bg(slate_700).fg(slate_50).add_modifier(Modifier::BOLD),
111            hover: Style::new().bg(slate_800),
112
113            size_huge: red_500,
114            size_large: orange_500,
115            size_medium: yellow_500,
116            size_small: green_500,
117            size_tiny: slate_600,
118
119            success: green_500,
120            warning: yellow_500,
121            error: red_500,
122            info: blue_400,
123
124            border: Style::new().fg(slate_600),
125            title: Style::new().fg(blue_400).add_modifier(Modifier::BOLD),
126            help_key: Style::new().fg(blue_400).add_modifier(Modifier::BOLD),
127            help_desc: Style::new().fg(slate_400),
128
129            tree_lines: Style::new().fg(slate_600),
130            directory: Style::new().fg(blue_500).add_modifier(Modifier::BOLD),
131            file: Style::new().fg(slate_300),
132            symlink: Style::new().fg(cyan_400),
133            executable: Style::new().fg(green_500),
134
135            progress_bar: Style::new().fg(blue_500),
136            progress_bg: Style::new().fg(slate_700),
137
138            header: Style::new().bg(slate_800).fg(slate_100),
139            footer: Style::new().bg(slate_800).fg(slate_400),
140
141            // Footer keybinding hints - polished pill/badge style
142            help_key_bg: slate_700,
143            help_key_fg: blue_400,
144            help_desc_fg: slate_400,
145            footer_bg: slate_800,
146
147            command_prompt: Style::new().fg(blue_400).add_modifier(Modifier::BOLD),
148            command_input: Style::new().fg(slate_100),
149            command_cursor: Style::new().add_modifier(Modifier::REVERSED),
150
151            marked: Style::new().fg(amber_500).add_modifier(Modifier::BOLD),
152        }
153    }
154
155    /// Light theme using a slate-based palette.
156    pub fn light() -> Self {
157        // Slate palette (Tailwind CSS)
158        let slate_50 = Color::Rgb(248, 250, 252);
159        let slate_100 = Color::Rgb(241, 245, 249);
160        let slate_200 = Color::Rgb(226, 232, 240);
161        let slate_400 = Color::Rgb(148, 163, 184);
162        let slate_500 = Color::Rgb(100, 116, 139);
163        let slate_600 = Color::Rgb(71, 85, 105);
164        let slate_700 = Color::Rgb(51, 65, 85);
165        let slate_800 = Color::Rgb(30, 41, 59);
166        let slate_900 = Color::Rgb(15, 23, 42);
167
168        // Accent colors (Tailwind CSS - darker variants for light theme)
169        let blue_600 = Color::Rgb(37, 99, 235);
170        let blue_700 = Color::Rgb(29, 78, 216);
171        let green_600 = Color::Rgb(22, 163, 74);
172        let yellow_600 = Color::Rgb(202, 138, 4);
173        let orange_600 = Color::Rgb(234, 88, 12);
174        let red_600 = Color::Rgb(220, 38, 38);
175        let cyan_600 = Color::Rgb(8, 145, 178);
176        let amber_600 = Color::Rgb(217, 119, 6);
177
178        Self {
179            variant: ThemeVariant::Light,
180            background: slate_50,
181            foreground: slate_900,
182            muted: slate_500,
183
184            selected: Style::new().bg(slate_200).fg(slate_900).add_modifier(Modifier::BOLD),
185            hover: Style::new().bg(slate_100),
186
187            size_huge: red_600,
188            size_large: orange_600,
189            size_medium: yellow_600,
190            size_small: green_600,
191            size_tiny: slate_400,
192
193            success: green_600,
194            warning: yellow_600,
195            error: red_600,
196            info: blue_600,
197
198            border: Style::new().fg(slate_400),
199            title: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
200            help_key: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
201            help_desc: Style::new().fg(slate_600),
202
203            tree_lines: Style::new().fg(slate_400),
204            directory: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
205            file: Style::new().fg(slate_700),
206            symlink: Style::new().fg(cyan_600),
207            executable: Style::new().fg(green_600),
208
209            progress_bar: Style::new().fg(blue_600),
210            progress_bg: Style::new().fg(slate_200),
211
212            header: Style::new().bg(slate_100).fg(slate_800),
213            footer: Style::new().bg(slate_100).fg(slate_600),
214
215            // Footer keybinding hints - polished pill/badge style
216            help_key_bg: slate_200,
217            help_key_fg: blue_700,
218            help_desc_fg: slate_600,
219            footer_bg: slate_100,
220
221            command_prompt: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
222            command_input: Style::new().fg(slate_900),
223            command_cursor: Style::new().add_modifier(Modifier::REVERSED),
224
225            marked: Style::new().fg(amber_600).add_modifier(Modifier::BOLD),
226        }
227    }
228
229    /// Create theme from variant.
230    pub fn from_variant(variant: ThemeVariant) -> Self {
231        match variant {
232            ThemeVariant::Dark => Self::dark(),
233            ThemeVariant::Light => Self::light(),
234        }
235    }
236
237    /// Toggle between dark and light themes.
238    pub fn toggle(&self) -> Self {
239        match self.variant {
240            ThemeVariant::Dark => Self::light(),
241            ThemeVariant::Light => Self::dark(),
242        }
243    }
244
245    /// Get color for a size ratio (0.0 to 1.0).
246    pub fn size_color(&self, ratio: f64) -> Color {
247        match ratio {
248            r if r > 0.50 => self.size_huge,
249            r if r > 0.25 => self.size_large,
250            r if r > 0.10 => self.size_medium,
251            r if r > 0.01 => self.size_small,
252            _ => self.size_tiny,
253        }
254    }
255
256    /// Get style for a size bar at given ratio.
257    pub fn size_bar_style(&self, ratio: f64) -> Style {
258        Style::new().fg(self.size_color(ratio))
259    }
260
261    /// Style for keybinding keys in footer (pill/badge style).
262    pub fn help_key_style(&self) -> Style {
263        Style::new()
264            .bg(self.help_key_bg)
265            .fg(self.help_key_fg)
266            .add_modifier(Modifier::BOLD)
267    }
268
269    /// Style for keybinding descriptions in footer.
270    pub fn help_desc_style(&self) -> Style {
271        Style::new().fg(self.help_desc_fg)
272    }
273
274    /// Style for footer background.
275    pub fn footer_style(&self) -> Style {
276        Style::new().bg(self.footer_bg)
277    }
278}
279
280impl Default for Theme {
281    fn default() -> Self {
282        Self::dark()
283    }
284}