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}