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}