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    pub input_bg: Color,
104    pub input_fg: Color,
105    pub input_dim_fg: Color,
106    pub progress_bar_filled: Style,
107    pub progress_bar_empty: Style,
108    pub streaming_dot: Style,
109    pub user_text_bg: Color,
110    pub subagent_border: Style,
111    pub subagent_header: Style,
112    pub subagent_done: Style,
113    pub subagent_working: Style,
114    pub diff_add_bg: Color,
115    pub diff_remove_bg: Color,
116    pub assistant_marker: Style,
117    pub user_role_label: Style,
118    pub assistant_role_label: Style,
119    pub message_separator: Style,
120}
121
122impl Theme {
123    pub fn from_config(name: &str) -> Self {
124        match name {
125            "light" => Self::light(),
126            "terminal" => Self::terminal(),
127            "auto" => match detect_terminal_background() {
128                TerminalBackground::Light => Self::light(),
129                TerminalBackground::Dark => Self::dark(),
130            },
131            _ => Self::dark(),
132        }
133    }
134
135    pub fn dark() -> Self {
136        let muted = Color::Rgb(88, 91, 112);
137        let surface = Color::Rgb(42, 44, 60);
138        let accent = Color::Rgb(110, 150, 215);
139        let green = Color::Rgb(140, 190, 135);
140        let peach = Color::Rgb(210, 155, 115);
141        let red = Color::Rgb(200, 120, 145);
142        let mauve = Color::Rgb(170, 140, 210);
143        let yellow = Color::Rgb(210, 190, 150);
144        let teal = Color::Rgb(120, 185, 175);
145        let sapphire = Color::Rgb(95, 165, 200);
146        let base = Color::Rgb(30, 30, 46);
147
148        Self {
149            bg: Color::Reset,
150            fg: Color::White,
151            dim: Style::default().fg(muted),
152            accent,
153            muted_fg: muted,
154            user_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
155            assistant_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
156            border: Style::default().fg(surface),
157            input_prompt: Style::default().fg(accent),
158            status_bar: Style::default().fg(muted),
159            code_bg: surface,
160            inline_code: Style::default().fg(peach),
161            error: Style::default().fg(red),
162            tool_name: Style::default().fg(yellow).add_modifier(Modifier::BOLD),
163            tool_output: Style::default().fg(muted),
164            tool_success: Style::default().fg(green),
165            heading: Style::default().fg(accent).add_modifier(Modifier::BOLD),
166            bold: Style::default().add_modifier(Modifier::BOLD),
167            italic: Style::default().add_modifier(Modifier::ITALIC),
168            blockquote: Style::default().fg(muted),
169            link: Style::default()
170                .fg(accent)
171                .add_modifier(Modifier::UNDERLINED),
172            list_bullet: Style::default().fg(muted),
173            scrollbar_track: Style::default().fg(surface),
174            scrollbar_thumb: Style::default().fg(muted),
175            highlight: Style::default().fg(base).bg(accent),
176            tool_file_read: Style::default().fg(sapphire),
177            tool_file_write: Style::default().fg(peach),
178            tool_directory: Style::default().fg(accent),
179            tool_search: Style::default().fg(mauve),
180            tool_command: Style::default().fg(green),
181            tool_mcp: Style::default().fg(teal),
182            tool_skill: Style::default().fg(mauve),
183            tool_badge_bg: surface,
184            tool_path: Style::default()
185                .fg(Color::White)
186                .add_modifier(Modifier::UNDERLINED),
187            thinking: Style::default().fg(muted),
188            mode_normal_fg: base,
189            mode_normal_bg: muted,
190            mode_insert_fg: base,
191            mode_insert_bg: accent,
192            cost: Style::default().fg(Color::Rgb(165, 135, 80)),
193            user_text: Style::default().fg(Color::Rgb(205, 214, 244)),
194            tool_action: Style::default().fg(muted),
195            separator: Style::default().fg(Color::Rgb(52, 54, 72)),
196            tool_exit_ok: Style::default().fg(green),
197            tool_exit_err: Style::default().fg(red),
198            syntax: None,
199            syntect_theme: Some("base16-ocean.dark"),
200            diff_add: Style::default().fg(green),
201            diff_remove: Style::default().fg(red),
202            diff_hunk: Style::default().fg(accent),
203            input_bg: Color::Rgb(36, 38, 55),
204            input_fg: Color::White,
205            input_dim_fg: muted,
206            progress_bar_filled: Style::default().fg(accent).add_modifier(Modifier::BOLD),
207            progress_bar_empty: Style::default().fg(surface),
208            streaming_dot: Style::default().fg(accent),
209            user_text_bg: Color::Rgb(38, 40, 58),
210            subagent_border: Style::default().fg(surface),
211            subagent_header: Style::default().fg(accent).add_modifier(Modifier::BOLD),
212            subagent_done: Style::default().fg(green),
213            subagent_working: Style::default().fg(accent),
214            diff_add_bg: Color::Rgb(25, 45, 30),
215            diff_remove_bg: Color::Rgb(55, 25, 30),
216            assistant_marker: Style::default().fg(accent).add_modifier(Modifier::BOLD),
217            user_role_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
218            assistant_role_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
219            message_separator: Style::default().fg(Color::Rgb(45, 47, 65)),
220        }
221    }
222
223    pub fn light() -> Self {
224        let muted = Color::Rgb(140, 143, 161);
225        let surface = Color::Rgb(204, 208, 218);
226        let accent = Color::Rgb(35, 90, 210);
227        let green = Color::Rgb(55, 135, 40);
228        let peach = Color::Rgb(210, 90, 20);
229        let red = Color::Rgb(175, 30, 60);
230        let mauve = Color::Rgb(110, 55, 190);
231        let yellow = Color::Rgb(185, 120, 30);
232        let teal = Color::Rgb(25, 125, 130);
233        let sapphire = Color::Rgb(30, 130, 155);
234        let text = Color::Rgb(76, 79, 105);
235
236        Self {
237            bg: Color::Reset,
238            fg: text,
239            dim: Style::default().fg(muted),
240            accent,
241            muted_fg: muted,
242            user_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
243            assistant_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
244            border: Style::default().fg(surface),
245            input_prompt: Style::default().fg(accent),
246            status_bar: Style::default().fg(muted),
247            code_bg: surface,
248            inline_code: Style::default().fg(peach),
249            error: Style::default().fg(red),
250            tool_name: Style::default().fg(yellow).add_modifier(Modifier::BOLD),
251            tool_output: Style::default().fg(muted),
252            tool_success: Style::default().fg(green),
253            heading: Style::default().fg(accent).add_modifier(Modifier::BOLD),
254            bold: Style::default().add_modifier(Modifier::BOLD),
255            italic: Style::default().add_modifier(Modifier::ITALIC),
256            blockquote: Style::default().fg(muted),
257            link: Style::default()
258                .fg(accent)
259                .add_modifier(Modifier::UNDERLINED),
260            list_bullet: Style::default().fg(muted),
261            scrollbar_track: Style::default().fg(surface),
262            scrollbar_thumb: Style::default().fg(muted),
263            highlight: Style::default().fg(Color::White).bg(accent),
264            tool_file_read: Style::default().fg(sapphire),
265            tool_file_write: Style::default().fg(peach),
266            tool_directory: Style::default().fg(accent),
267            tool_search: Style::default().fg(mauve),
268            tool_command: Style::default().fg(green),
269            tool_mcp: Style::default().fg(teal),
270            tool_skill: Style::default().fg(mauve),
271            tool_badge_bg: surface,
272            tool_path: Style::default().fg(text).add_modifier(Modifier::UNDERLINED),
273            thinking: Style::default().fg(muted),
274            mode_normal_fg: Color::White,
275            mode_normal_bg: muted,
276            mode_insert_fg: Color::White,
277            mode_insert_bg: accent,
278            cost: Style::default().fg(Color::Rgb(150, 110, 35)),
279            user_text: Style::default().fg(text),
280            tool_action: Style::default().fg(muted),
281            separator: Style::default().fg(surface),
282            tool_exit_ok: Style::default().fg(green),
283            tool_exit_err: Style::default().fg(red),
284            syntax: None,
285            syntect_theme: Some("base16-ocean.light"),
286            diff_add: Style::default().fg(green),
287            diff_remove: Style::default().fg(red),
288            diff_hunk: Style::default().fg(accent),
289            input_bg: Color::Rgb(210, 214, 225),
290            input_fg: text,
291            input_dim_fg: muted,
292            progress_bar_filled: Style::default().fg(accent).add_modifier(Modifier::BOLD),
293            progress_bar_empty: Style::default().fg(surface),
294            streaming_dot: Style::default().fg(accent),
295            user_text_bg: Color::Rgb(218, 222, 232),
296            subagent_border: Style::default().fg(surface),
297            subagent_header: Style::default().fg(accent).add_modifier(Modifier::BOLD),
298            subagent_done: Style::default().fg(green),
299            subagent_working: Style::default().fg(accent),
300            diff_add_bg: Color::Rgb(220, 245, 220),
301            diff_remove_bg: Color::Rgb(255, 225, 225),
302            assistant_marker: Style::default().fg(accent).add_modifier(Modifier::BOLD),
303            user_role_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
304            assistant_role_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
305            message_separator: Style::default().fg(Color::Rgb(195, 200, 212)),
306        }
307    }
308
309    pub fn terminal() -> Self {
310        let dim = Style::default().add_modifier(Modifier::DIM);
311        let bold = Style::default().add_modifier(Modifier::BOLD);
312        let muted = Color::Indexed(8);
313
314        Self {
315            bg: Color::Reset,
316            fg: Color::Reset,
317            dim,
318            accent: Color::Reset,
319            muted_fg: muted,
320            user_label: bold,
321            assistant_label: Style::default(),
322            border: dim,
323            input_prompt: bold,
324            status_bar: dim,
325            code_bg: Color::Indexed(0),
326            inline_code: Style::default().fg(muted),
327            error: Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED),
328            tool_name: bold,
329            tool_output: dim,
330            tool_success: bold,
331            heading: Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
332            bold,
333            italic: Style::default().add_modifier(Modifier::ITALIC),
334            blockquote: dim,
335            link: Style::default().add_modifier(Modifier::UNDERLINED),
336            list_bullet: dim,
337            scrollbar_track: dim,
338            scrollbar_thumb: Style::default(),
339            highlight: Style::default().add_modifier(Modifier::REVERSED),
340            tool_file_read: Style::default().fg(muted),
341            tool_file_write: Style::default().fg(muted),
342            tool_directory: bold,
343            tool_search: Style::default().fg(muted),
344            tool_command: Style::default().fg(muted),
345            tool_mcp: Style::default().fg(muted),
346            tool_skill: Style::default().fg(muted),
347            tool_badge_bg: muted,
348            tool_path: Style::default().add_modifier(Modifier::UNDERLINED),
349            thinking: dim,
350            mode_normal_fg: Color::Reset,
351            mode_normal_bg: muted,
352            mode_insert_fg: Color::Indexed(0),
353            mode_insert_bg: Color::Reset,
354            cost: dim,
355            user_text: bold,
356            tool_action: dim,
357            separator: dim,
358            tool_exit_ok: bold,
359            tool_exit_err: Style::default().add_modifier(Modifier::BOLD),
360            syntax: Some(SyntaxStyles {
361                keyword: bold,
362                string: Style::default().fg(muted),
363                comment: dim.add_modifier(Modifier::ITALIC),
364                function: bold,
365                type_name: Style::default().fg(muted),
366                number: Style::default().fg(muted),
367                constant: bold,
368                attribute: Style::default().fg(muted),
369            }),
370            syntect_theme: None,
371            diff_add: bold,
372            diff_remove: dim,
373            diff_hunk: Style::default().fg(muted),
374            input_bg: muted,
375            input_fg: Color::Reset,
376            input_dim_fg: muted,
377            progress_bar_filled: bold,
378            progress_bar_empty: dim,
379            streaming_dot: dim,
380            user_text_bg: Color::Indexed(0),
381            subagent_border: dim,
382            subagent_header: bold,
383            subagent_done: bold,
384            subagent_working: dim,
385            diff_add_bg: Color::Reset,
386            diff_remove_bg: Color::Reset,
387            assistant_marker: bold,
388            user_role_label: bold,
389            assistant_role_label: Style::default(),
390            message_separator: dim,
391        }
392    }
393}
394
395impl Default for Theme {
396    fn default() -> Self {
397        Self::terminal()
398    }
399}