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    // Command palette
66    pub command_prompt: Style,
67    pub command_input: Style,
68    pub command_cursor: Style,
69
70    // Marked items
71    pub marked: Style,
72}
73
74impl Theme {
75    /// Dark theme using a slate-based palette.
76    pub fn dark() -> Self {
77        // Slate palette (Tailwind CSS)
78        let slate_50 = Color::Rgb(248, 250, 252);
79        let slate_100 = Color::Rgb(241, 245, 249);
80        let slate_300 = Color::Rgb(203, 213, 225);
81        let slate_400 = Color::Rgb(148, 163, 184);
82        let slate_500 = Color::Rgb(100, 116, 139);
83        let slate_600 = Color::Rgb(71, 85, 105);
84        let slate_700 = Color::Rgb(51, 65, 85);
85        let slate_800 = Color::Rgb(30, 41, 59);
86        let slate_900 = Color::Rgb(15, 23, 42);
87
88        // Accent colors (Tailwind CSS)
89        let blue_400 = Color::Rgb(96, 165, 250);
90        let blue_500 = Color::Rgb(59, 130, 246);
91        let green_500 = Color::Rgb(34, 197, 94);
92        let yellow_500 = Color::Rgb(234, 179, 8);
93        let orange_500 = Color::Rgb(249, 115, 22);
94        let red_500 = Color::Rgb(239, 68, 68);
95        let cyan_400 = Color::Rgb(34, 211, 238);
96        let amber_500 = Color::Rgb(245, 158, 11);
97
98        Self {
99            variant: ThemeVariant::Dark,
100            background: slate_900,
101            foreground: slate_100,
102            muted: slate_500,
103
104            selected: Style::new().bg(slate_700).fg(slate_50).add_modifier(Modifier::BOLD),
105            hover: Style::new().bg(slate_800),
106
107            size_huge: red_500,
108            size_large: orange_500,
109            size_medium: yellow_500,
110            size_small: green_500,
111            size_tiny: slate_600,
112
113            success: green_500,
114            warning: yellow_500,
115            error: red_500,
116            info: blue_400,
117
118            border: Style::new().fg(slate_600),
119            title: Style::new().fg(blue_400).add_modifier(Modifier::BOLD),
120            help_key: Style::new().fg(blue_400).add_modifier(Modifier::BOLD),
121            help_desc: Style::new().fg(slate_400),
122
123            tree_lines: Style::new().fg(slate_600),
124            directory: Style::new().fg(blue_500).add_modifier(Modifier::BOLD),
125            file: Style::new().fg(slate_300),
126            symlink: Style::new().fg(cyan_400),
127            executable: Style::new().fg(green_500),
128
129            progress_bar: Style::new().fg(blue_500),
130            progress_bg: Style::new().fg(slate_700),
131
132            header: Style::new().bg(slate_800).fg(slate_100),
133            footer: Style::new().bg(slate_800).fg(slate_400),
134
135            command_prompt: Style::new().fg(blue_400).add_modifier(Modifier::BOLD),
136            command_input: Style::new().fg(slate_100),
137            command_cursor: Style::new().add_modifier(Modifier::REVERSED),
138
139            marked: Style::new().fg(amber_500).add_modifier(Modifier::BOLD),
140        }
141    }
142
143    /// Light theme using a slate-based palette.
144    pub fn light() -> Self {
145        // Slate palette (Tailwind CSS)
146        let slate_50 = Color::Rgb(248, 250, 252);
147        let slate_100 = Color::Rgb(241, 245, 249);
148        let slate_200 = Color::Rgb(226, 232, 240);
149        let slate_400 = Color::Rgb(148, 163, 184);
150        let slate_500 = Color::Rgb(100, 116, 139);
151        let slate_600 = Color::Rgb(71, 85, 105);
152        let slate_700 = Color::Rgb(51, 65, 85);
153        let slate_800 = Color::Rgb(30, 41, 59);
154        let slate_900 = Color::Rgb(15, 23, 42);
155
156        // Accent colors (Tailwind CSS - darker variants for light theme)
157        let blue_600 = Color::Rgb(37, 99, 235);
158        let blue_700 = Color::Rgb(29, 78, 216);
159        let green_600 = Color::Rgb(22, 163, 74);
160        let yellow_600 = Color::Rgb(202, 138, 4);
161        let orange_600 = Color::Rgb(234, 88, 12);
162        let red_600 = Color::Rgb(220, 38, 38);
163        let cyan_600 = Color::Rgb(8, 145, 178);
164        let amber_600 = Color::Rgb(217, 119, 6);
165
166        Self {
167            variant: ThemeVariant::Light,
168            background: slate_50,
169            foreground: slate_900,
170            muted: slate_500,
171
172            selected: Style::new().bg(slate_200).fg(slate_900).add_modifier(Modifier::BOLD),
173            hover: Style::new().bg(slate_100),
174
175            size_huge: red_600,
176            size_large: orange_600,
177            size_medium: yellow_600,
178            size_small: green_600,
179            size_tiny: slate_400,
180
181            success: green_600,
182            warning: yellow_600,
183            error: red_600,
184            info: blue_600,
185
186            border: Style::new().fg(slate_400),
187            title: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
188            help_key: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
189            help_desc: Style::new().fg(slate_600),
190
191            tree_lines: Style::new().fg(slate_400),
192            directory: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
193            file: Style::new().fg(slate_700),
194            symlink: Style::new().fg(cyan_600),
195            executable: Style::new().fg(green_600),
196
197            progress_bar: Style::new().fg(blue_600),
198            progress_bg: Style::new().fg(slate_200),
199
200            header: Style::new().bg(slate_100).fg(slate_800),
201            footer: Style::new().bg(slate_100).fg(slate_600),
202
203            command_prompt: Style::new().fg(blue_700).add_modifier(Modifier::BOLD),
204            command_input: Style::new().fg(slate_900),
205            command_cursor: Style::new().add_modifier(Modifier::REVERSED),
206
207            marked: Style::new().fg(amber_600).add_modifier(Modifier::BOLD),
208        }
209    }
210
211    /// Create theme from variant.
212    pub fn from_variant(variant: ThemeVariant) -> Self {
213        match variant {
214            ThemeVariant::Dark => Self::dark(),
215            ThemeVariant::Light => Self::light(),
216        }
217    }
218
219    /// Toggle between dark and light themes.
220    pub fn toggle(&self) -> Self {
221        match self.variant {
222            ThemeVariant::Dark => Self::light(),
223            ThemeVariant::Light => Self::dark(),
224        }
225    }
226
227    /// Get color for a size ratio (0.0 to 1.0).
228    pub fn size_color(&self, ratio: f64) -> Color {
229        match ratio {
230            r if r > 0.50 => self.size_huge,
231            r if r > 0.25 => self.size_large,
232            r if r > 0.10 => self.size_medium,
233            r if r > 0.01 => self.size_small,
234            _ => self.size_tiny,
235        }
236    }
237
238    /// Get style for a size bar at given ratio.
239    pub fn size_bar_style(&self, ratio: f64) -> Style {
240        Style::new().fg(self.size_color(ratio))
241    }
242}
243
244impl Default for Theme {
245    fn default() -> Self {
246        Self::dark()
247    }
248}