Skip to main content

dot/tui/
theme.rs

1use ratatui::style::{Color, Modifier, Style};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub enum TerminalBackground {
5    Dark,
6    Light,
7}
8
9pub fn detect_terminal_background() -> TerminalBackground {
10    if let Ok(val) = std::env::var("TERM_BACKGROUND") {
11        match val.to_lowercase().as_str() {
12            "light" => return TerminalBackground::Light,
13            "dark" => return TerminalBackground::Dark,
14            _ => {}
15        }
16    }
17
18    if let Ok(val) = std::env::var("COLORFGBG")
19        && let Some(bg_str) = val.rsplit(';').next()
20        && let Ok(bg) = bg_str.trim().parse::<u8>()
21    {
22        return if bg < 8 || (232..232 + 12).contains(&bg) {
23            TerminalBackground::Dark
24        } else if (8..=15).contains(&bg) {
25            TerminalBackground::Light
26        } else {
27            TerminalBackground::Dark
28        };
29    }
30
31    if let Ok(term_program) = std::env::var("TERM_PROGRAM")
32        && term_program.to_lowercase().contains("apple_terminal")
33    {
34        return TerminalBackground::Light;
35    }
36
37    TerminalBackground::Dark
38}
39
40#[derive(Debug, Clone, Copy)]
41pub struct SyntaxStyles {
42    pub keyword: Style,
43    pub string: Style,
44    pub comment: Style,
45    pub function: Style,
46    pub type_name: Style,
47    pub number: Style,
48    pub constant: Style,
49    pub attribute: Style,
50}
51
52pub struct Theme {
53    pub bg: Color,
54    pub fg: Color,
55    pub dim: Style,
56    pub accent: Color,
57    pub user_label: Style,
58    pub assistant_label: Style,
59    pub border: Style,
60    pub input_prompt: Style,
61    pub status_bar: Style,
62    pub code_bg: Color,
63    pub inline_code: Style,
64    pub error: Style,
65    pub tool_name: Style,
66    pub tool_output: Style,
67    pub heading: Style,
68    pub bold: Style,
69    pub italic: Style,
70    pub blockquote: Style,
71    pub link: Style,
72    pub list_bullet: Style,
73    pub scrollbar_track: Style,
74    pub scrollbar_thumb: Style,
75    pub tool_success: Style,
76    pub highlight: Style,
77    pub muted_fg: Color,
78    pub tool_file_read: Style,
79    pub tool_file_write: Style,
80    pub tool_directory: Style,
81    pub tool_search: Style,
82    pub tool_command: Style,
83    pub tool_mcp: Style,
84    pub tool_skill: Style,
85    pub tool_badge_bg: Color,
86    pub tool_path: Style,
87    pub thinking: Style,
88    pub mode_normal_fg: Color,
89    pub mode_normal_bg: Color,
90    pub mode_insert_fg: Color,
91    pub mode_insert_bg: Color,
92    pub cost: Style,
93    pub user_text: Style,
94    pub tool_action: Style,
95    pub separator: Style,
96    pub tool_exit_ok: Style,
97    pub tool_exit_err: Style,
98    pub syntax: Option<SyntaxStyles>,
99    pub syntect_theme: Option<&'static str>,
100    pub diff_add: Style,
101    pub diff_remove: Style,
102    pub diff_hunk: Style,
103}
104
105impl Theme {
106    pub fn from_config(name: &str) -> Self {
107        match name {
108            "light" => Self::light(),
109            "terminal" => Self::terminal(),
110            "auto" => match detect_terminal_background() {
111                TerminalBackground::Light => Self::light(),
112                TerminalBackground::Dark => Self::dark(),
113            },
114            _ => Self::dark(),
115        }
116    }
117
118    pub fn dark() -> Self {
119        let muted = Color::Rgb(88, 91, 112);
120        let surface = Color::Rgb(49, 50, 68);
121        let accent = Color::Rgb(137, 180, 250);
122        let green = Color::Rgb(166, 227, 161);
123        let peach = Color::Rgb(250, 179, 135);
124        let red = Color::Rgb(243, 139, 168);
125        let mauve = Color::Rgb(203, 166, 247);
126        let yellow = Color::Rgb(249, 226, 175);
127        let teal = Color::Rgb(148, 226, 213);
128        let sapphire = Color::Rgb(116, 199, 236);
129        let base = Color::Rgb(30, 30, 46);
130
131        Self {
132            bg: Color::Reset,
133            fg: Color::White,
134            dim: Style::default().fg(muted),
135            accent,
136            muted_fg: muted,
137            user_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
138            assistant_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
139            border: Style::default().fg(surface),
140            input_prompt: Style::default().fg(accent),
141            status_bar: Style::default().fg(muted),
142            code_bg: surface,
143            inline_code: Style::default().fg(peach),
144            error: Style::default().fg(red),
145            tool_name: Style::default().fg(yellow).add_modifier(Modifier::BOLD),
146            tool_output: Style::default().fg(muted),
147            tool_success: Style::default().fg(green),
148            heading: Style::default().fg(accent).add_modifier(Modifier::BOLD),
149            bold: Style::default().add_modifier(Modifier::BOLD),
150            italic: Style::default().add_modifier(Modifier::ITALIC),
151            blockquote: Style::default().fg(muted),
152            link: Style::default()
153                .fg(accent)
154                .add_modifier(Modifier::UNDERLINED),
155            list_bullet: Style::default().fg(muted),
156            scrollbar_track: Style::default().fg(surface),
157            scrollbar_thumb: Style::default().fg(muted),
158            highlight: Style::default().fg(base).bg(accent),
159            tool_file_read: Style::default().fg(sapphire),
160            tool_file_write: Style::default().fg(peach),
161            tool_directory: Style::default().fg(accent),
162            tool_search: Style::default().fg(mauve),
163            tool_command: Style::default().fg(green),
164            tool_mcp: Style::default().fg(teal),
165            tool_skill: Style::default().fg(mauve),
166            tool_badge_bg: surface,
167            tool_path: Style::default()
168                .fg(Color::White)
169                .add_modifier(Modifier::UNDERLINED),
170            thinking: Style::default().fg(muted),
171            mode_normal_fg: base,
172            mode_normal_bg: muted,
173            mode_insert_fg: base,
174            mode_insert_bg: accent,
175            cost: Style::default().fg(Color::Rgb(180, 150, 90)),
176            user_text: Style::default().fg(Color::Rgb(205, 214, 244)),
177            tool_action: Style::default().fg(muted),
178            separator: Style::default().fg(Color::Rgb(60, 62, 80)),
179            tool_exit_ok: Style::default().fg(green),
180            tool_exit_err: Style::default().fg(red),
181            syntax: None,
182            syntect_theme: Some("base16-ocean.dark"),
183            diff_add: Style::default().fg(green),
184            diff_remove: Style::default().fg(red),
185            diff_hunk: Style::default().fg(accent),
186        }
187    }
188
189    pub fn light() -> Self {
190        let muted = Color::Rgb(140, 143, 161);
191        let surface = Color::Rgb(204, 208, 218);
192        let accent = Color::Rgb(30, 102, 245);
193        let green = Color::Rgb(64, 160, 43);
194        let peach = Color::Rgb(254, 100, 11);
195        let red = Color::Rgb(210, 15, 57);
196        let mauve = Color::Rgb(136, 57, 239);
197        let yellow = Color::Rgb(223, 142, 29);
198        let teal = Color::Rgb(23, 146, 153);
199        let sapphire = Color::Rgb(32, 159, 181);
200        let text = Color::Rgb(76, 79, 105);
201
202        Self {
203            bg: Color::Reset,
204            fg: text,
205            dim: Style::default().fg(muted),
206            accent,
207            muted_fg: muted,
208            user_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
209            assistant_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
210            border: Style::default().fg(surface),
211            input_prompt: Style::default().fg(accent),
212            status_bar: Style::default().fg(muted),
213            code_bg: surface,
214            inline_code: Style::default().fg(peach),
215            error: Style::default().fg(red),
216            tool_name: Style::default().fg(yellow).add_modifier(Modifier::BOLD),
217            tool_output: Style::default().fg(muted),
218            tool_success: Style::default().fg(green),
219            heading: Style::default().fg(accent).add_modifier(Modifier::BOLD),
220            bold: Style::default().add_modifier(Modifier::BOLD),
221            italic: Style::default().add_modifier(Modifier::ITALIC),
222            blockquote: Style::default().fg(muted),
223            link: Style::default()
224                .fg(accent)
225                .add_modifier(Modifier::UNDERLINED),
226            list_bullet: Style::default().fg(muted),
227            scrollbar_track: Style::default().fg(surface),
228            scrollbar_thumb: Style::default().fg(muted),
229            highlight: Style::default().fg(Color::White).bg(accent),
230            tool_file_read: Style::default().fg(sapphire),
231            tool_file_write: Style::default().fg(peach),
232            tool_directory: Style::default().fg(accent),
233            tool_search: Style::default().fg(mauve),
234            tool_command: Style::default().fg(green),
235            tool_mcp: Style::default().fg(teal),
236            tool_skill: Style::default().fg(mauve),
237            tool_badge_bg: surface,
238            tool_path: Style::default().fg(text).add_modifier(Modifier::UNDERLINED),
239            thinking: Style::default().fg(muted),
240            mode_normal_fg: Color::White,
241            mode_normal_bg: muted,
242            mode_insert_fg: Color::White,
243            mode_insert_bg: accent,
244            cost: Style::default().fg(Color::Rgb(160, 120, 40)),
245            user_text: Style::default().fg(text),
246            tool_action: Style::default().fg(muted),
247            separator: Style::default().fg(surface),
248            tool_exit_ok: Style::default().fg(green),
249            tool_exit_err: Style::default().fg(red),
250            syntax: None,
251            syntect_theme: Some("base16-ocean.light"),
252            diff_add: Style::default().fg(green),
253            diff_remove: Style::default().fg(red),
254            diff_hunk: Style::default().fg(accent),
255        }
256    }
257
258    pub fn terminal() -> Self {
259        let red = Color::Indexed(1);
260        let green = Color::Indexed(2);
261        let yellow = Color::Indexed(3);
262        let magenta = Color::Indexed(5);
263        let cyan = Color::Indexed(6);
264        let bright_black = Color::Indexed(8);
265
266        let fg = Color::Reset;
267        let dim = Style::default().fg(bright_black);
268        let bold = Style::default().add_modifier(Modifier::BOLD);
269
270        Self {
271            bg: Color::Reset,
272            fg,
273            dim,
274            accent: fg,
275            muted_fg: bright_black,
276            user_label: Style::default().fg(magenta).add_modifier(Modifier::BOLD),
277            assistant_label: bold,
278            border: dim,
279            input_prompt: bold,
280            status_bar: dim,
281            code_bg: Color::Indexed(0),
282            inline_code: Style::default().fg(yellow),
283            error: Style::default().fg(red),
284            tool_name: Style::default().add_modifier(Modifier::BOLD),
285            tool_output: dim,
286            tool_success: Style::default().fg(green),
287            heading: Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
288            bold,
289            italic: Style::default().add_modifier(Modifier::ITALIC),
290            blockquote: dim,
291            link: Style::default().fg(cyan).add_modifier(Modifier::UNDERLINED),
292            list_bullet: dim,
293            scrollbar_track: dim,
294            scrollbar_thumb: Style::default().fg(fg),
295            highlight: Style::default().add_modifier(Modifier::REVERSED),
296            tool_file_read: Style::default().fg(cyan),
297            tool_file_write: Style::default().fg(yellow),
298            tool_directory: bold,
299            tool_search: Style::default().fg(magenta),
300            tool_command: Style::default().fg(green),
301            tool_mcp: Style::default().fg(cyan),
302            tool_skill: Style::default().fg(magenta),
303            tool_badge_bg: bright_black,
304            tool_path: Style::default().add_modifier(Modifier::UNDERLINED),
305            thinking: dim,
306            mode_normal_fg: Color::Reset,
307            mode_normal_bg: bright_black,
308            mode_insert_fg: Color::Indexed(0),
309            mode_insert_bg: Color::Reset,
310            cost: dim,
311            user_text: Style::default(),
312            tool_action: dim,
313            separator: dim,
314            tool_exit_ok: Style::default().fg(green),
315            tool_exit_err: Style::default().fg(red),
316            syntax: Some(SyntaxStyles {
317                keyword: Style::default().fg(magenta),
318                string: Style::default().fg(green),
319                comment: Style::default()
320                    .fg(bright_black)
321                    .add_modifier(Modifier::ITALIC),
322                function: Style::default().fg(yellow),
323                type_name: Style::default().fg(cyan),
324                number: Style::default().fg(cyan),
325                constant: Style::default().fg(red),
326                attribute: Style::default().fg(yellow),
327            }),
328            syntect_theme: None,
329            diff_add: Style::default().fg(green),
330            diff_remove: Style::default().fg(red),
331            diff_hunk: Style::default().fg(cyan),
332        }
333    }
334}
335
336impl Default for Theme {
337    fn default() -> Self {
338        Self::terminal()
339    }
340}