1use std::sync::Mutex;
6
7#[derive(Debug, Clone)]
16pub struct ThemeColors {
17 pub text: Color,
19 pub muted: Color,
21 pub dim: Color,
23 pub accent: Color,
25 pub error: Color,
27 pub success: Color,
29 pub warning: Color,
31 pub info: Color,
33 pub background: Color,
35 pub surface: Color,
37 pub border: Color,
39 pub border_accent: Color,
41 pub border_muted: Color,
43 pub selection: Color,
45 pub cursor: Color,
47 pub thinking_text: Color,
49
50 pub md_heading: Color,
53 pub md_link: Color,
55 pub md_link_url: Color,
57 pub md_code: Color,
59 pub md_code_bg: Color,
62 pub md_code_block: Color,
64 pub md_code_block_bg: Color,
66 pub md_code_block_border: Color,
68 pub md_quote: Color,
70 pub md_quote_border: Color,
72 pub md_hr: Color,
74 pub md_list_bullet: Color,
76
77 pub tool_pending_bg: Color,
80 pub tool_success_bg: Color,
82 pub tool_error_bg: Color,
84 pub tool_title: Color,
86 pub tool_output: Color,
88 pub bash_mode: Color,
90
91 pub custom_message_bg: Color,
94 pub custom_message_label: Color,
96 pub custom_message_text: Color,
98
99 pub tool_diff_added: Color,
102 pub tool_diff_removed: Color,
104 pub tool_diff_context: Color,
106}
107
108impl Default for ThemeColors {
109 fn default() -> Self {
110 Self {
117 text: Color::Default,
118 muted: Color::Ansi256(244), dim: Color::Ansi256(242), accent: Color::Ansi256(108), error: Color::Ansi256(131), success: Color::Ansi256(107), warning: Color::Ansi256(226), info: Color::Ansi256(81), background: Color::Default,
126 surface: Color::Ansi256(236), border: Color::Ansi256(67), border_accent: Color::Ansi256(45), border_muted: Color::Ansi256(239), selection: Color::Ansi256(60), cursor: Color::Ansi256(81),
132 thinking_text: Color::Ansi256(244), md_heading: Color::Ansi256(179), md_link: Color::Ansi256(110), md_link_url: Color::Ansi256(242), md_code: Color::Rgb(138, 190, 183), md_code_bg: Color::Rgb(48, 52, 59), md_code_block: Color::Rgb(212, 212, 212), md_code_block_bg: Color::Rgb(40, 44, 52), md_code_block_border: Color::Ansi256(244), md_quote: Color::Ansi256(244), md_quote_border: Color::Ansi256(244),
145 md_hr: Color::Ansi256(244),
146 md_list_bullet: Color::Ansi256(108), tool_pending_bg: Color::Rgb(40, 44, 52),
153 tool_success_bg: Color::Rgb(38, 48, 52), tool_error_bg: Color::Rgb(55, 40, 45), tool_title: Color::Default,
156 tool_output: Color::Ansi256(244),
157 bash_mode: Color::Ansi256(107), custom_message_bg: Color::Rgb(45, 40, 56),
162 custom_message_label: Color::Rgb(149, 117, 205),
163 custom_message_text: Color::Default,
164
165 tool_diff_added: Color::Ansi256(107), tool_diff_removed: Color::Ansi256(131), tool_diff_context: Color::Ansi256(244), }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum Color {
176 Default,
178 Ansi16(u8),
180 Ansi256(u8),
182 Rgb(u8, u8, u8),
184}
185
186impl Color {
187 pub fn to_fg(&self) -> String {
189 match self {
190 Color::Default => "\x1b[39m".to_string(),
191 Color::Ansi16(c) => format!("\x1b[{}m", c),
192 Color::Ansi256(c) => format!("\x1b[38;5;{}m", c),
193 Color::Rgb(r, g, b) => format!("\x1b[38;2;{};{};{}m", r, g, b),
194 }
195 }
196
197 pub fn to_bg(&self) -> String {
199 match self {
200 Color::Default => "\x1b[49m".to_string(),
201 Color::Ansi16(c) => format!("\x1b[{}m", c + 10),
202 Color::Ansi256(c) => format!("\x1b[48;5;{}m", c),
203 Color::Rgb(r, g, b) => format!("\x1b[48;2;{};{};{}m", r, g, b),
204 }
205 }
206
207 pub fn fg(&self, text: &str) -> String {
209 format!("{}{}\x1b[0m", self.to_fg(), text)
210 }
211
212 pub fn bg(&self, text: &str) -> String {
214 let bg = self.to_bg();
215 let nested = text.replace("\x1b[0m", &format!("\x1b[0m{bg}"));
220 format!("{bg}{nested}\x1b[0m")
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct Theme {
227 pub colors: ThemeColors,
229 pub border_style: BorderStyle,
231 pub corner_style: CornerStyle,
233}
234
235impl Default for Theme {
236 fn default() -> Self {
237 Self {
238 colors: ThemeColors::default(),
239 border_style: BorderStyle::Rounded,
240 corner_style: CornerStyle::Rounded,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy, Default)]
247pub enum BorderStyle {
248 #[default]
249 Rounded,
250 Sharp,
251 Double,
252 Thick,
253 None,
254}
255
256impl BorderStyle {
257 pub fn chars(&self) -> BorderChars {
259 match self {
260 BorderStyle::Rounded => BorderChars {
261 top_left: "╭",
262 top_right: "╮",
263 bottom_left: "╰",
264 bottom_right: "╯",
265 horizontal: "─",
266 vertical: "│",
267 },
268 BorderStyle::Sharp => BorderChars {
269 top_left: "┌",
270 top_right: "┐",
271 bottom_left: "└",
272 bottom_right: "┘",
273 horizontal: "─",
274 vertical: "│",
275 },
276 BorderStyle::Double => BorderChars {
277 top_left: "╔",
278 top_right: "╗",
279 bottom_left: "╚",
280 bottom_right: "╝",
281 horizontal: "═",
282 vertical: "║",
283 },
284 BorderStyle::Thick => BorderChars {
285 top_left: "┏",
286 top_right: "┓",
287 bottom_left: "┗",
288 bottom_right: "┛",
289 horizontal: "━",
290 vertical: "┃",
291 },
292 BorderStyle::None => BorderChars {
293 top_left: " ",
294 top_right: " ",
295 bottom_left: " ",
296 bottom_right: " ",
297 horizontal: " ",
298 vertical: " ",
299 },
300 }
301 }
302}
303
304pub struct BorderChars {
306 pub top_left: &'static str,
307 pub top_right: &'static str,
308 pub bottom_left: &'static str,
309 pub bottom_right: &'static str,
310 pub horizontal: &'static str,
311 pub vertical: &'static str,
312}
313
314#[derive(Debug, Clone, Copy, Default)]
316pub enum CornerStyle {
317 #[default]
318 Rounded,
319 Sharp,
320}
321
322pub struct ThemeManager {
324 current: Mutex<Theme>,
325}
326
327static THEME_REVISION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
334
335pub fn theme_revision() -> u64 {
337 THEME_REVISION.load(std::sync::atomic::Ordering::Acquire)
338}
339
340impl ThemeManager {
341 pub fn new() -> Self {
343 Self {
344 current: Mutex::new(Theme::default()),
345 }
346 }
347
348 pub fn get(&self) -> Theme {
350 self.current.lock().map(|t| t.clone()).unwrap_or_default()
351 }
352
353 pub fn set(&self, theme: Theme) {
355 if let Ok(mut current) = self.current.lock() {
356 *current = theme;
357 }
358 THEME_REVISION.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
359 }
360
361 pub fn apply_preset(&self, preset: ThemePreset) {
363 self.set(theme_for_preset(preset));
364 }
365}
366
367fn theme_for_preset(preset: ThemePreset) -> Theme {
372 match preset {
373 ThemePreset::Dark => Theme::default(),
374 ThemePreset::Light => {
375 let mut colors = ThemeColors::default();
376 colors.text = Color::Default;
377 colors.muted = Color::Ansi256(241); colors.dim = Color::Ansi256(243); colors.accent = Color::Ansi256(66); colors.error = Color::Ansi256(131); colors.success = Color::Ansi256(65); colors.warning = Color::Ansi256(136); colors.info = Color::Ansi256(67); colors.background = Color::Default;
385 colors.surface = Color::Ansi256(254); colors.border = Color::Ansi256(67); colors.border_accent = Color::Ansi256(66); colors.border_muted = Color::Ansi256(249); colors.selection = Color::Ansi256(189); colors.cursor = Color::Ansi256(67);
391 colors.thinking_text = Color::Ansi256(241);
392 colors.md_heading = Color::Ansi256(136); colors.md_link = Color::Ansi256(67); colors.md_link_url = Color::Ansi256(243);
395 colors.md_code = Color::Rgb(90, 128, 128); colors.md_code_bg = Color::Rgb(232, 234, 237);
397 colors.md_code_block = Color::Rgb(31, 35, 40);
398 colors.md_code_block_bg = Color::Rgb(246, 248, 250); colors.md_code_block_border = Color::Ansi256(241);
400 colors.md_quote = Color::Ansi256(241);
401 colors.md_quote_border = Color::Ansi256(241);
402 colors.md_hr = Color::Ansi256(241);
403 colors.md_list_bullet = Color::Ansi256(65); colors.tool_pending_bg = Color::Ansi256(189);
405 colors.tool_success_bg = Color::Ansi256(151);
406 colors.tool_error_bg = Color::Ansi256(181);
407 colors.tool_title = Color::Default;
408 colors.tool_output = Color::Ansi256(241);
409 colors.bash_mode = Color::Ansi256(65);
410 colors.custom_message_bg = Color::Rgb(237, 231, 246);
413 colors.custom_message_label = Color::Rgb(126, 87, 194);
414 colors.custom_message_text = Color::Ansi256(237);
415 colors.tool_diff_added = Color::Ansi256(65);
416 colors.tool_diff_removed = Color::Ansi256(131);
417 colors.tool_diff_context = Color::Ansi256(241);
418 Theme {
419 colors,
420 ..Default::default()
421 }
422 }
423 ThemePreset::Monochrome => {
424 let mut colors = ThemeColors::default();
425 for f in [
426 &mut colors.accent,
427 &mut colors.border_accent,
428 &mut colors.md_code,
429 &mut colors.md_list_bullet,
430 &mut colors.bash_mode,
431 ] {
432 *f = Color::Ansi256(15);
433 }
434 colors.muted = Color::Ansi256(244);
435 colors.dim = Color::Ansi256(242);
436 colors.error = Color::Ansi256(9);
437 colors.success = Color::Ansi256(15);
438 colors.warning = Color::Ansi256(15);
439 colors.info = Color::Ansi256(15);
440 colors.surface = Color::Ansi256(236);
441 colors.border = Color::Ansi256(244);
442 colors.border_muted = Color::Ansi256(240);
443 colors.selection = Color::Ansi256(244);
444 colors.cursor = Color::Ansi256(15);
445 colors.thinking_text = Color::Ansi256(244);
446 colors.md_heading = Color::Ansi256(15);
447 colors.md_link = Color::Ansi256(15);
448 colors.md_link_url = Color::Ansi256(242);
449 colors.md_code_bg = Color::Ansi256(237);
450 colors.md_code_block = Color::Ansi256(15);
451 colors.md_code_block_bg = Color::Ansi256(235);
452 colors.md_code_block_border = Color::Ansi256(244);
453 colors.md_quote = Color::Ansi256(244);
454 colors.md_quote_border = Color::Ansi256(244);
455 colors.md_hr = Color::Ansi256(244);
456 colors.custom_message_bg = Color::Ansi256(236);
457 colors.custom_message_label = Color::Ansi256(15);
458 colors.custom_message_text = Color::Ansi256(244);
459 colors.tool_title = Color::Default;
460 colors.tool_output = Color::Ansi256(244);
461 colors.tool_diff_added = Color::Ansi256(15);
462 colors.tool_diff_removed = Color::Ansi256(9);
463 colors.tool_diff_context = Color::Ansi256(244);
464 Theme {
465 colors,
466 ..Default::default()
467 }
468 }
469 }
470}
471
472impl Default for ThemeManager {
473 fn default() -> Self {
474 Self::new()
475 }
476}
477
478#[derive(Debug, Clone, Copy)]
480pub enum ThemePreset {
481 Dark,
482 Light,
483 Monochrome,
484}
485
486pub fn theme() -> Theme {
488 global_theme_manager().get()
489}
490
491pub fn apply_theme_preset(preset: ThemePreset) {
496 global_theme_manager().apply_preset(preset);
497}
498
499pub fn global_theme_manager() -> &'static ThemeManager {
501 THEME_MANAGER.get_or_init(ThemeManager::new)
502}
503
504static THEME_MANAGER: std::sync::OnceLock<ThemeManager> = std::sync::OnceLock::new();
506
507pub fn themed_text(text: &str, color_fn: fn(&ThemeColors) -> Color) -> String {
509 let theme = theme();
510 let color = color_fn(&theme.colors);
511 color.fg(text)
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn test_color_ansi() {
520 let color = Color::Ansi256(196);
521 let text = color.fg("Hello");
522 assert!(text.contains("Hello"));
523 }
524
525 #[test]
526 fn test_theme_manager() {
527 let manager = ThemeManager::new();
528 manager.apply_preset(ThemePreset::Dark);
529 let theme = manager.get();
530 assert!(matches!(theme.colors.accent, Color::Ansi256(108)));
531 }
532}