1use std::sync::atomic::{AtomicU8, Ordering};
2use std::sync::{OnceLock, RwLock};
3
4use ratatui::style::{Color, Modifier, Style};
5
6static COLOR_MODE: AtomicU8 = AtomicU8::new(1);
8
9static COLORED_UNDERLINE: AtomicU8 = AtomicU8::new(1);
15
16static THEME: OnceLock<RwLock<ThemeDef>> = OnceLock::new();
18
19#[derive(Debug, Clone)]
21pub struct ColorSlot {
22 pub truecolor: Option<Color>,
23 pub ansi16: Option<Color>,
24 pub add_modifier: Option<Modifier>,
25 pub remove_modifier: Option<Modifier>,
26}
27
28impl ColorSlot {
29 pub(crate) const fn new() -> Self {
30 Self {
31 truecolor: None,
32 ansi16: None,
33 add_modifier: None,
34 remove_modifier: None,
35 }
36 }
37
38 pub const fn new_with_modifier(m: Modifier) -> Self {
39 Self {
40 truecolor: None,
41 ansi16: None,
42 add_modifier: Some(m),
43 remove_modifier: None,
44 }
45 }
46
47 pub fn to_style(&self, mode: u8) -> Style {
49 let mut style = Style::default();
50 match mode {
51 0 => {} 2 => {
53 if let Some(c) = self.truecolor {
54 style = style.fg(c);
55 }
56 }
57 _ => {
58 if let Some(c) = self.ansi16 {
59 style = style.fg(c);
60 }
61 }
62 }
63 if let Some(m) = self.add_modifier {
64 style = style.add_modifier(m);
65 }
66 if let Some(m) = self.remove_modifier {
67 style = style.remove_modifier(m);
68 }
69 style
70 }
71
72 #[allow(dead_code)]
73 pub fn to_style_bg(&self, mode: u8) -> Style {
74 let mut style = Style::default();
75 match mode {
76 0 => {}
77 2 => {
78 if let Some(c) = self.truecolor {
79 style = style.bg(c);
80 }
81 }
82 _ => {
83 if let Some(c) = self.ansi16 {
84 style = style.bg(c);
85 }
86 }
87 }
88 if let Some(m) = self.add_modifier {
89 style = style.add_modifier(m);
90 }
91 if let Some(m) = self.remove_modifier {
92 style = style.remove_modifier(m);
93 }
94 style
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct ThemeDef {
101 pub name: String,
102 pub accent: ColorSlot,
103 pub accent_bg: ColorSlot,
104 pub success: ColorSlot,
105 pub success_dim: ColorSlot,
106 pub warning: ColorSlot,
107 pub error: ColorSlot,
108 pub highlight: ColorSlot,
109 pub border: ColorSlot,
110 pub border_active: ColorSlot,
111 pub fg_muted: ColorSlot,
112 pub fg_bold: ColorSlot,
113 pub footer_key: ColorSlot,
114 pub badge: ColorSlot,
115 pub selected_fg: ColorSlot,
116 pub footer_key_fg: ColorSlot,
117 pub logo_dot: ColorSlot,
122}
123
124impl ThemeDef {
125 pub fn purple() -> Self {
126 Self {
127 name: "Purple".to_string(),
128 accent: ColorSlot {
129 truecolor: Some(Color::Rgb(147, 51, 234)),
130 ansi16: Some(Color::Magenta),
131 add_modifier: None,
132 remove_modifier: None,
133 },
134 accent_bg: ColorSlot {
135 truecolor: Some(Color::Rgb(147, 51, 234)),
136 ansi16: Some(Color::Magenta),
137 add_modifier: Some(Modifier::BOLD),
138 remove_modifier: Some(Modifier::DIM),
139 },
140 success: ColorSlot {
141 truecolor: Some(Color::Rgb(34, 197, 94)),
142 ansi16: Some(Color::Green),
143 add_modifier: Some(Modifier::BOLD),
144 remove_modifier: None,
145 },
146 success_dim: ColorSlot {
147 truecolor: Some(Color::Rgb(34, 197, 94)),
148 ansi16: Some(Color::Green),
149 add_modifier: Some(Modifier::DIM),
150 remove_modifier: None,
151 },
152 warning: ColorSlot {
153 truecolor: Some(Color::Rgb(234, 179, 8)),
154 ansi16: Some(Color::Yellow),
155 add_modifier: Some(Modifier::BOLD),
156 remove_modifier: None,
157 },
158 error: ColorSlot {
159 truecolor: Some(Color::Rgb(239, 68, 68)),
160 ansi16: Some(Color::Red),
161 add_modifier: Some(Modifier::BOLD),
162 remove_modifier: None,
163 },
164 highlight: ColorSlot {
165 truecolor: None,
166 ansi16: None,
167 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
168 remove_modifier: None,
169 },
170 border: ColorSlot {
171 truecolor: None,
172 ansi16: None,
173 add_modifier: Some(Modifier::DIM),
174 remove_modifier: None,
175 },
176 border_active: ColorSlot {
177 truecolor: Some(Color::Rgb(147, 51, 234)),
178 ansi16: Some(Color::Magenta),
179 add_modifier: None,
180 remove_modifier: None,
181 },
182 fg_muted: ColorSlot {
183 truecolor: None,
184 ansi16: None,
185 add_modifier: Some(Modifier::DIM),
186 remove_modifier: None,
187 },
188 fg_bold: ColorSlot {
189 truecolor: None,
190 ansi16: None,
191 add_modifier: Some(Modifier::BOLD),
192 remove_modifier: None,
193 },
194 footer_key: ColorSlot {
195 truecolor: Some(Color::Rgb(88, 88, 88)),
196 ansi16: Some(Color::DarkGray),
197 add_modifier: None,
198 remove_modifier: None,
199 },
200 badge: ColorSlot {
201 truecolor: Some(Color::Rgb(147, 51, 234)),
202 ansi16: Some(Color::Magenta),
203 add_modifier: Some(Modifier::BOLD),
204 remove_modifier: Some(Modifier::DIM),
205 },
206 selected_fg: ColorSlot {
207 truecolor: Some(Color::White),
208 ansi16: Some(Color::White),
209 add_modifier: Some(Modifier::BOLD),
210 remove_modifier: None,
211 },
212 footer_key_fg: ColorSlot {
213 truecolor: Some(Color::White),
214 ansi16: Some(Color::White),
215 add_modifier: None,
216 remove_modifier: None,
217 },
218 logo_dot: ColorSlot {
219 truecolor: Some(Color::Rgb(0, 240, 255)),
220 ansi16: Some(Color::Cyan),
221 add_modifier: Some(Modifier::BOLD),
222 remove_modifier: None,
223 },
224 }
225 }
226
227 pub fn purple_purple() -> Self {
228 Self {
229 name: "Purple Purple".to_string(),
230 accent: ColorSlot {
231 truecolor: Some(Color::Rgb(147, 51, 234)),
232 ansi16: Some(Color::Magenta),
233 add_modifier: None,
234 remove_modifier: None,
235 },
236 accent_bg: ColorSlot {
237 truecolor: Some(Color::Rgb(147, 51, 234)),
238 ansi16: Some(Color::Magenta),
239 add_modifier: Some(Modifier::BOLD),
240 remove_modifier: Some(Modifier::DIM),
241 },
242 success: ColorSlot {
243 truecolor: Some(Color::Rgb(34, 197, 94)),
244 ansi16: Some(Color::Green),
245 add_modifier: Some(Modifier::BOLD),
246 remove_modifier: None,
247 },
248 success_dim: ColorSlot {
249 truecolor: Some(Color::Rgb(34, 197, 94)),
250 ansi16: Some(Color::Green),
251 add_modifier: Some(Modifier::DIM),
252 remove_modifier: None,
253 },
254 warning: ColorSlot {
255 truecolor: Some(Color::Rgb(234, 179, 8)),
256 ansi16: Some(Color::Yellow),
257 add_modifier: Some(Modifier::BOLD),
258 remove_modifier: None,
259 },
260 error: ColorSlot {
261 truecolor: Some(Color::Rgb(239, 68, 68)),
262 ansi16: Some(Color::Red),
263 add_modifier: Some(Modifier::BOLD),
264 remove_modifier: None,
265 },
266 highlight: ColorSlot {
267 truecolor: None,
268 ansi16: None,
269 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
270 remove_modifier: None,
271 },
272 border: ColorSlot {
273 truecolor: Some(Color::Rgb(147, 51, 234)),
274 ansi16: Some(Color::Magenta),
275 add_modifier: Some(Modifier::DIM),
276 remove_modifier: None,
277 },
278 border_active: ColorSlot {
279 truecolor: Some(Color::Rgb(147, 51, 234)),
280 ansi16: Some(Color::Magenta),
281 add_modifier: None,
282 remove_modifier: None,
283 },
284 fg_muted: ColorSlot {
285 truecolor: None,
286 ansi16: None,
287 add_modifier: Some(Modifier::DIM),
288 remove_modifier: None,
289 },
290 fg_bold: ColorSlot {
291 truecolor: None,
292 ansi16: None,
293 add_modifier: Some(Modifier::BOLD),
294 remove_modifier: None,
295 },
296 footer_key: ColorSlot {
297 truecolor: Some(Color::Rgb(88, 88, 88)),
298 ansi16: Some(Color::DarkGray),
299 add_modifier: None,
300 remove_modifier: None,
301 },
302 badge: ColorSlot {
303 truecolor: Some(Color::Rgb(147, 51, 234)),
304 ansi16: Some(Color::Magenta),
305 add_modifier: Some(Modifier::BOLD),
306 remove_modifier: Some(Modifier::DIM),
307 },
308 selected_fg: ColorSlot {
309 truecolor: Some(Color::White),
310 ansi16: Some(Color::White),
311 add_modifier: Some(Modifier::BOLD),
312 remove_modifier: None,
313 },
314 footer_key_fg: ColorSlot {
315 truecolor: Some(Color::White),
316 ansi16: Some(Color::White),
317 add_modifier: None,
318 remove_modifier: None,
319 },
320 logo_dot: ColorSlot {
321 truecolor: Some(Color::Rgb(0, 240, 255)),
322 ansi16: Some(Color::Cyan),
323 add_modifier: Some(Modifier::BOLD),
324 remove_modifier: None,
325 },
326 }
327 }
328
329 pub fn catppuccin_mocha() -> Self {
330 Self {
331 name: "Catppuccin Mocha".to_string(),
332 accent: ColorSlot {
333 truecolor: Some(Color::Rgb(137, 180, 250)),
334 ansi16: Some(Color::Blue),
335 add_modifier: None,
336 remove_modifier: None,
337 },
338 accent_bg: ColorSlot {
339 truecolor: Some(Color::Rgb(137, 180, 250)),
340 ansi16: Some(Color::Blue),
341 add_modifier: Some(Modifier::BOLD),
342 remove_modifier: Some(Modifier::DIM),
343 },
344 success: ColorSlot {
345 truecolor: Some(Color::Rgb(166, 227, 161)),
346 ansi16: Some(Color::Green),
347 add_modifier: Some(Modifier::BOLD),
348 remove_modifier: None,
349 },
350 success_dim: ColorSlot {
351 truecolor: Some(Color::Rgb(166, 227, 161)),
352 ansi16: Some(Color::Green),
353 add_modifier: Some(Modifier::DIM),
354 remove_modifier: None,
355 },
356 warning: ColorSlot {
357 truecolor: Some(Color::Rgb(249, 226, 175)),
358 ansi16: Some(Color::Yellow),
359 add_modifier: Some(Modifier::BOLD),
360 remove_modifier: None,
361 },
362 error: ColorSlot {
363 truecolor: Some(Color::Rgb(243, 139, 168)),
364 ansi16: Some(Color::Red),
365 add_modifier: Some(Modifier::BOLD),
366 remove_modifier: None,
367 },
368 highlight: ColorSlot {
369 truecolor: None,
370 ansi16: None,
371 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
372 remove_modifier: None,
373 },
374 border: ColorSlot {
375 truecolor: Some(Color::Rgb(88, 91, 112)),
376 ansi16: None,
377 add_modifier: Some(Modifier::DIM),
378 remove_modifier: None,
379 },
380 border_active: ColorSlot {
381 truecolor: Some(Color::Rgb(137, 180, 250)),
382 ansi16: Some(Color::Blue),
383 add_modifier: None,
384 remove_modifier: None,
385 },
386 fg_muted: ColorSlot {
387 truecolor: Some(Color::Rgb(108, 112, 134)),
388 ansi16: None,
389 add_modifier: Some(Modifier::DIM),
390 remove_modifier: None,
391 },
392 fg_bold: ColorSlot {
393 truecolor: None,
394 ansi16: None,
395 add_modifier: Some(Modifier::BOLD),
396 remove_modifier: None,
397 },
398 footer_key: ColorSlot {
399 truecolor: Some(Color::Rgb(69, 71, 90)),
400 ansi16: Some(Color::DarkGray),
401 add_modifier: None,
402 remove_modifier: None,
403 },
404 badge: ColorSlot {
405 truecolor: Some(Color::Rgb(137, 180, 250)),
406 ansi16: Some(Color::Blue),
407 add_modifier: Some(Modifier::BOLD),
408 remove_modifier: Some(Modifier::DIM),
409 },
410 selected_fg: ColorSlot {
411 truecolor: Some(Color::Rgb(30, 30, 46)), ansi16: Some(Color::Black),
413 add_modifier: Some(Modifier::BOLD),
414 remove_modifier: None,
415 },
416 footer_key_fg: ColorSlot {
417 truecolor: Some(Color::White),
418 ansi16: Some(Color::White),
419 add_modifier: None,
420 remove_modifier: None,
421 },
422 logo_dot: ColorSlot {
423 truecolor: Some(Color::Rgb(0, 240, 255)),
424 ansi16: Some(Color::Cyan),
425 add_modifier: Some(Modifier::BOLD),
426 remove_modifier: None,
427 },
428 }
429 }
430
431 pub fn dracula() -> Self {
432 Self {
433 name: "Dracula".to_string(),
434 accent: ColorSlot {
435 truecolor: Some(Color::Rgb(189, 147, 249)),
436 ansi16: Some(Color::Magenta),
437 add_modifier: None,
438 remove_modifier: None,
439 },
440 accent_bg: ColorSlot {
441 truecolor: Some(Color::Rgb(189, 147, 249)),
442 ansi16: Some(Color::Magenta),
443 add_modifier: Some(Modifier::BOLD),
444 remove_modifier: Some(Modifier::DIM),
445 },
446 success: ColorSlot {
447 truecolor: Some(Color::Rgb(80, 250, 123)),
448 ansi16: Some(Color::Green),
449 add_modifier: Some(Modifier::BOLD),
450 remove_modifier: None,
451 },
452 success_dim: ColorSlot {
453 truecolor: Some(Color::Rgb(80, 250, 123)),
454 ansi16: Some(Color::Green),
455 add_modifier: Some(Modifier::DIM),
456 remove_modifier: None,
457 },
458 warning: ColorSlot {
459 truecolor: Some(Color::Rgb(241, 250, 140)),
460 ansi16: Some(Color::Yellow),
461 add_modifier: Some(Modifier::BOLD),
462 remove_modifier: None,
463 },
464 error: ColorSlot {
465 truecolor: Some(Color::Rgb(255, 85, 85)),
466 ansi16: Some(Color::Red),
467 add_modifier: Some(Modifier::BOLD),
468 remove_modifier: None,
469 },
470 highlight: ColorSlot {
471 truecolor: None,
472 ansi16: None,
473 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
474 remove_modifier: None,
475 },
476 border: ColorSlot {
477 truecolor: Some(Color::Rgb(68, 71, 90)),
478 ansi16: None,
479 add_modifier: Some(Modifier::DIM),
480 remove_modifier: None,
481 },
482 border_active: ColorSlot {
483 truecolor: Some(Color::Rgb(189, 147, 249)),
484 ansi16: Some(Color::Magenta),
485 add_modifier: None,
486 remove_modifier: None,
487 },
488 fg_muted: ColorSlot {
489 truecolor: Some(Color::Rgb(98, 114, 164)),
490 ansi16: None,
491 add_modifier: Some(Modifier::DIM),
492 remove_modifier: None,
493 },
494 fg_bold: ColorSlot {
495 truecolor: None,
496 ansi16: None,
497 add_modifier: Some(Modifier::BOLD),
498 remove_modifier: None,
499 },
500 footer_key: ColorSlot {
501 truecolor: Some(Color::Rgb(68, 71, 90)),
502 ansi16: Some(Color::DarkGray),
503 add_modifier: None,
504 remove_modifier: None,
505 },
506 badge: ColorSlot {
507 truecolor: Some(Color::Rgb(189, 147, 249)),
508 ansi16: Some(Color::Magenta),
509 add_modifier: Some(Modifier::BOLD),
510 remove_modifier: Some(Modifier::DIM),
511 },
512 selected_fg: ColorSlot {
513 truecolor: Some(Color::Rgb(40, 42, 54)), ansi16: Some(Color::Black),
515 add_modifier: Some(Modifier::BOLD),
516 remove_modifier: None,
517 },
518 footer_key_fg: ColorSlot {
519 truecolor: Some(Color::White),
520 ansi16: Some(Color::White),
521 add_modifier: None,
522 remove_modifier: None,
523 },
524 logo_dot: ColorSlot {
525 truecolor: Some(Color::Rgb(0, 240, 255)),
526 ansi16: Some(Color::Cyan),
527 add_modifier: Some(Modifier::BOLD),
528 remove_modifier: None,
529 },
530 }
531 }
532
533 pub fn gruvbox_dark() -> Self {
534 Self {
535 name: "Gruvbox Dark".to_string(),
536 accent: ColorSlot {
537 truecolor: Some(Color::Rgb(215, 153, 33)),
538 ansi16: Some(Color::LightYellow),
539 add_modifier: None,
540 remove_modifier: None,
541 },
542 accent_bg: ColorSlot {
543 truecolor: Some(Color::Rgb(215, 153, 33)),
544 ansi16: Some(Color::LightYellow),
545 add_modifier: Some(Modifier::BOLD),
546 remove_modifier: Some(Modifier::DIM),
547 },
548 success: ColorSlot {
549 truecolor: Some(Color::Rgb(152, 151, 26)),
550 ansi16: Some(Color::Green),
551 add_modifier: Some(Modifier::BOLD),
552 remove_modifier: None,
553 },
554 success_dim: ColorSlot {
555 truecolor: Some(Color::Rgb(152, 151, 26)),
556 ansi16: Some(Color::Green),
557 add_modifier: Some(Modifier::DIM),
558 remove_modifier: None,
559 },
560 warning: ColorSlot {
561 truecolor: Some(Color::Rgb(250, 189, 47)),
562 ansi16: Some(Color::Yellow),
563 add_modifier: Some(Modifier::BOLD),
564 remove_modifier: None,
565 },
566 error: ColorSlot {
567 truecolor: Some(Color::Rgb(251, 73, 52)), ansi16: Some(Color::Red),
569 add_modifier: Some(Modifier::BOLD),
570 remove_modifier: None,
571 },
572 highlight: ColorSlot {
573 truecolor: None,
574 ansi16: None,
575 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
576 remove_modifier: None,
577 },
578 border: ColorSlot {
579 truecolor: Some(Color::Rgb(80, 73, 69)),
580 ansi16: None,
581 add_modifier: Some(Modifier::DIM),
582 remove_modifier: None,
583 },
584 border_active: ColorSlot {
585 truecolor: Some(Color::Rgb(215, 153, 33)),
586 ansi16: Some(Color::LightYellow),
587 add_modifier: None,
588 remove_modifier: None,
589 },
590 fg_muted: ColorSlot {
591 truecolor: Some(Color::Rgb(146, 131, 116)),
592 ansi16: None,
593 add_modifier: Some(Modifier::DIM),
594 remove_modifier: None,
595 },
596 fg_bold: ColorSlot {
597 truecolor: None,
598 ansi16: None,
599 add_modifier: Some(Modifier::BOLD),
600 remove_modifier: None,
601 },
602 footer_key: ColorSlot {
603 truecolor: Some(Color::Rgb(80, 73, 69)),
604 ansi16: Some(Color::DarkGray),
605 add_modifier: None,
606 remove_modifier: None,
607 },
608 badge: ColorSlot {
609 truecolor: Some(Color::Rgb(215, 153, 33)),
610 ansi16: Some(Color::LightYellow),
611 add_modifier: Some(Modifier::BOLD),
612 remove_modifier: Some(Modifier::DIM),
613 },
614 selected_fg: ColorSlot {
615 truecolor: Some(Color::Rgb(40, 40, 40)), ansi16: Some(Color::Black),
617 add_modifier: Some(Modifier::BOLD),
618 remove_modifier: None,
619 },
620 footer_key_fg: ColorSlot {
621 truecolor: Some(Color::White),
622 ansi16: Some(Color::White),
623 add_modifier: None,
624 remove_modifier: None,
625 },
626 logo_dot: ColorSlot {
627 truecolor: Some(Color::Rgb(0, 240, 255)),
628 ansi16: Some(Color::Cyan),
629 add_modifier: Some(Modifier::BOLD),
630 remove_modifier: None,
631 },
632 }
633 }
634
635 pub fn nord() -> Self {
636 Self {
637 name: "Nord".to_string(),
638 accent: ColorSlot {
639 truecolor: Some(Color::Rgb(136, 192, 208)),
640 ansi16: Some(Color::Cyan),
641 add_modifier: None,
642 remove_modifier: None,
643 },
644 accent_bg: ColorSlot {
645 truecolor: Some(Color::Rgb(136, 192, 208)),
646 ansi16: Some(Color::Cyan),
647 add_modifier: Some(Modifier::BOLD),
648 remove_modifier: Some(Modifier::DIM),
649 },
650 success: ColorSlot {
651 truecolor: Some(Color::Rgb(163, 190, 140)),
652 ansi16: Some(Color::Green),
653 add_modifier: Some(Modifier::BOLD),
654 remove_modifier: None,
655 },
656 success_dim: ColorSlot {
657 truecolor: Some(Color::Rgb(163, 190, 140)),
658 ansi16: Some(Color::Green),
659 add_modifier: Some(Modifier::DIM),
660 remove_modifier: None,
661 },
662 warning: ColorSlot {
663 truecolor: Some(Color::Rgb(235, 203, 139)),
664 ansi16: Some(Color::Yellow),
665 add_modifier: Some(Modifier::BOLD),
666 remove_modifier: None,
667 },
668 error: ColorSlot {
669 truecolor: Some(Color::Rgb(191, 97, 106)),
670 ansi16: Some(Color::Red),
671 add_modifier: Some(Modifier::BOLD),
672 remove_modifier: None,
673 },
674 highlight: ColorSlot {
675 truecolor: None,
676 ansi16: None,
677 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
678 remove_modifier: None,
679 },
680 border: ColorSlot {
681 truecolor: Some(Color::Rgb(76, 86, 106)),
682 ansi16: None,
683 add_modifier: Some(Modifier::DIM),
684 remove_modifier: None,
685 },
686 border_active: ColorSlot {
687 truecolor: Some(Color::Rgb(136, 192, 208)),
688 ansi16: Some(Color::Cyan),
689 add_modifier: None,
690 remove_modifier: None,
691 },
692 fg_muted: ColorSlot {
693 truecolor: Some(Color::Rgb(76, 86, 106)),
694 ansi16: None,
695 add_modifier: Some(Modifier::DIM),
696 remove_modifier: None,
697 },
698 fg_bold: ColorSlot {
699 truecolor: None,
700 ansi16: None,
701 add_modifier: Some(Modifier::BOLD),
702 remove_modifier: None,
703 },
704 footer_key: ColorSlot {
705 truecolor: Some(Color::Rgb(76, 86, 106)),
706 ansi16: Some(Color::DarkGray),
707 add_modifier: None,
708 remove_modifier: None,
709 },
710 badge: ColorSlot {
711 truecolor: Some(Color::Rgb(136, 192, 208)),
712 ansi16: Some(Color::Cyan),
713 add_modifier: Some(Modifier::BOLD),
714 remove_modifier: Some(Modifier::DIM),
715 },
716 selected_fg: ColorSlot {
717 truecolor: Some(Color::Rgb(46, 52, 64)), ansi16: Some(Color::Black),
719 add_modifier: Some(Modifier::BOLD),
720 remove_modifier: None,
721 },
722 footer_key_fg: ColorSlot {
723 truecolor: Some(Color::White),
724 ansi16: Some(Color::White),
725 add_modifier: None,
726 remove_modifier: None,
727 },
728 logo_dot: ColorSlot {
729 truecolor: Some(Color::Rgb(0, 240, 255)),
730 ansi16: Some(Color::Cyan),
731 add_modifier: Some(Modifier::BOLD),
732 remove_modifier: None,
733 },
734 }
735 }
736
737 pub fn tokyo_night() -> Self {
738 Self {
739 name: "Tokyo Night".to_string(),
740 accent: ColorSlot {
741 truecolor: Some(Color::Rgb(122, 162, 247)),
742 ansi16: Some(Color::Blue),
743 add_modifier: None,
744 remove_modifier: None,
745 },
746 accent_bg: ColorSlot {
747 truecolor: Some(Color::Rgb(122, 162, 247)),
748 ansi16: Some(Color::Blue),
749 add_modifier: Some(Modifier::BOLD),
750 remove_modifier: Some(Modifier::DIM),
751 },
752 success: ColorSlot {
753 truecolor: Some(Color::Rgb(158, 206, 106)),
754 ansi16: Some(Color::Green),
755 add_modifier: Some(Modifier::BOLD),
756 remove_modifier: None,
757 },
758 success_dim: ColorSlot {
759 truecolor: Some(Color::Rgb(158, 206, 106)),
760 ansi16: Some(Color::Green),
761 add_modifier: Some(Modifier::DIM),
762 remove_modifier: None,
763 },
764 warning: ColorSlot {
765 truecolor: Some(Color::Rgb(224, 175, 104)),
766 ansi16: Some(Color::Yellow),
767 add_modifier: Some(Modifier::BOLD),
768 remove_modifier: None,
769 },
770 error: ColorSlot {
771 truecolor: Some(Color::Rgb(247, 118, 142)),
772 ansi16: Some(Color::Red),
773 add_modifier: Some(Modifier::BOLD),
774 remove_modifier: None,
775 },
776 highlight: ColorSlot {
777 truecolor: None,
778 ansi16: None,
779 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
780 remove_modifier: None,
781 },
782 border: ColorSlot {
783 truecolor: Some(Color::Rgb(61, 89, 161)),
784 ansi16: None,
785 add_modifier: Some(Modifier::DIM),
786 remove_modifier: None,
787 },
788 border_active: ColorSlot {
789 truecolor: Some(Color::Rgb(122, 162, 247)),
790 ansi16: Some(Color::Blue),
791 add_modifier: None,
792 remove_modifier: None,
793 },
794 fg_muted: ColorSlot {
795 truecolor: Some(Color::Rgb(86, 95, 137)),
796 ansi16: None,
797 add_modifier: Some(Modifier::DIM),
798 remove_modifier: None,
799 },
800 fg_bold: ColorSlot {
801 truecolor: None,
802 ansi16: None,
803 add_modifier: Some(Modifier::BOLD),
804 remove_modifier: None,
805 },
806 footer_key: ColorSlot {
807 truecolor: Some(Color::Rgb(61, 89, 161)),
808 ansi16: Some(Color::DarkGray),
809 add_modifier: None,
810 remove_modifier: None,
811 },
812 badge: ColorSlot {
813 truecolor: Some(Color::Rgb(122, 162, 247)),
814 ansi16: Some(Color::Blue),
815 add_modifier: Some(Modifier::BOLD),
816 remove_modifier: Some(Modifier::DIM),
817 },
818 selected_fg: ColorSlot {
819 truecolor: Some(Color::Rgb(26, 27, 38)), ansi16: Some(Color::Black),
821 add_modifier: Some(Modifier::BOLD),
822 remove_modifier: None,
823 },
824 footer_key_fg: ColorSlot {
825 truecolor: Some(Color::White),
826 ansi16: Some(Color::White),
827 add_modifier: None,
828 remove_modifier: None,
829 },
830 logo_dot: ColorSlot {
831 truecolor: Some(Color::Rgb(0, 240, 255)),
832 ansi16: Some(Color::Cyan),
833 add_modifier: Some(Modifier::BOLD),
834 remove_modifier: None,
835 },
836 }
837 }
838
839 pub fn one_dark() -> Self {
840 Self {
841 name: "One Dark".to_string(),
842 accent: ColorSlot {
843 truecolor: Some(Color::Rgb(97, 175, 239)),
844 ansi16: Some(Color::Blue),
845 add_modifier: None,
846 remove_modifier: None,
847 },
848 accent_bg: ColorSlot {
849 truecolor: Some(Color::Rgb(97, 175, 239)),
850 ansi16: Some(Color::Blue),
851 add_modifier: Some(Modifier::BOLD),
852 remove_modifier: Some(Modifier::DIM),
853 },
854 success: ColorSlot {
855 truecolor: Some(Color::Rgb(152, 195, 121)),
856 ansi16: Some(Color::Green),
857 add_modifier: Some(Modifier::BOLD),
858 remove_modifier: None,
859 },
860 success_dim: ColorSlot {
861 truecolor: Some(Color::Rgb(152, 195, 121)),
862 ansi16: Some(Color::Green),
863 add_modifier: Some(Modifier::DIM),
864 remove_modifier: None,
865 },
866 warning: ColorSlot {
867 truecolor: Some(Color::Rgb(229, 192, 123)),
868 ansi16: Some(Color::Yellow),
869 add_modifier: Some(Modifier::BOLD),
870 remove_modifier: None,
871 },
872 error: ColorSlot {
873 truecolor: Some(Color::Rgb(224, 108, 117)),
874 ansi16: Some(Color::Red),
875 add_modifier: Some(Modifier::BOLD),
876 remove_modifier: None,
877 },
878 highlight: ColorSlot {
879 truecolor: None,
880 ansi16: None,
881 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
882 remove_modifier: None,
883 },
884 border: ColorSlot {
885 truecolor: Some(Color::Rgb(62, 68, 81)),
886 ansi16: None,
887 add_modifier: Some(Modifier::DIM),
888 remove_modifier: None,
889 },
890 border_active: ColorSlot {
891 truecolor: Some(Color::Rgb(97, 175, 239)),
892 ansi16: Some(Color::Blue),
893 add_modifier: None,
894 remove_modifier: None,
895 },
896 fg_muted: ColorSlot {
897 truecolor: Some(Color::Rgb(92, 99, 112)),
898 ansi16: None,
899 add_modifier: Some(Modifier::DIM),
900 remove_modifier: None,
901 },
902 fg_bold: ColorSlot {
903 truecolor: None,
904 ansi16: None,
905 add_modifier: Some(Modifier::BOLD),
906 remove_modifier: None,
907 },
908 footer_key: ColorSlot {
909 truecolor: Some(Color::Rgb(62, 68, 81)),
910 ansi16: Some(Color::DarkGray),
911 add_modifier: None,
912 remove_modifier: None,
913 },
914 badge: ColorSlot {
915 truecolor: Some(Color::Rgb(97, 175, 239)),
916 ansi16: Some(Color::Blue),
917 add_modifier: Some(Modifier::BOLD),
918 remove_modifier: Some(Modifier::DIM),
919 },
920 selected_fg: ColorSlot {
921 truecolor: Some(Color::Rgb(40, 44, 52)), ansi16: Some(Color::Black),
923 add_modifier: Some(Modifier::BOLD),
924 remove_modifier: None,
925 },
926 footer_key_fg: ColorSlot {
927 truecolor: Some(Color::White),
928 ansi16: Some(Color::White),
929 add_modifier: None,
930 remove_modifier: None,
931 },
932 logo_dot: ColorSlot {
933 truecolor: Some(Color::Rgb(0, 240, 255)),
934 ansi16: Some(Color::Cyan),
935 add_modifier: Some(Modifier::BOLD),
936 remove_modifier: None,
937 },
938 }
939 }
940
941 pub fn catppuccin_latte() -> Self {
942 Self {
943 name: "Catppuccin Latte".to_string(),
944 accent: ColorSlot {
945 truecolor: Some(Color::Rgb(30, 102, 245)),
946 ansi16: Some(Color::Blue),
947 add_modifier: None,
948 remove_modifier: None,
949 },
950 accent_bg: ColorSlot {
951 truecolor: Some(Color::Rgb(30, 102, 245)),
952 ansi16: Some(Color::Blue),
953 add_modifier: Some(Modifier::BOLD),
954 remove_modifier: Some(Modifier::DIM),
955 },
956 success: ColorSlot {
957 truecolor: Some(Color::Rgb(64, 160, 43)),
958 ansi16: Some(Color::Green),
959 add_modifier: Some(Modifier::BOLD),
960 remove_modifier: None,
961 },
962 success_dim: ColorSlot {
963 truecolor: Some(Color::Rgb(64, 160, 43)),
964 ansi16: Some(Color::Green),
965 add_modifier: Some(Modifier::DIM),
966 remove_modifier: None,
967 },
968 warning: ColorSlot {
969 truecolor: Some(Color::Rgb(223, 142, 29)),
970 ansi16: Some(Color::Yellow),
971 add_modifier: Some(Modifier::BOLD),
972 remove_modifier: None,
973 },
974 error: ColorSlot {
975 truecolor: Some(Color::Rgb(210, 15, 57)),
976 ansi16: Some(Color::Red),
977 add_modifier: Some(Modifier::BOLD),
978 remove_modifier: None,
979 },
980 highlight: ColorSlot {
981 truecolor: None,
982 ansi16: None,
983 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
984 remove_modifier: None,
985 },
986 border: ColorSlot {
987 truecolor: Some(Color::Rgb(172, 176, 190)),
988 ansi16: None,
989 add_modifier: None, remove_modifier: None,
991 },
992 border_active: ColorSlot {
993 truecolor: Some(Color::Rgb(30, 102, 245)),
994 ansi16: Some(Color::Blue),
995 add_modifier: None,
996 remove_modifier: None,
997 },
998 fg_muted: ColorSlot {
999 truecolor: Some(Color::Rgb(140, 143, 161)),
1000 ansi16: None,
1001 add_modifier: Some(Modifier::DIM),
1002 remove_modifier: None,
1003 },
1004 fg_bold: ColorSlot {
1005 truecolor: None,
1006 ansi16: None,
1007 add_modifier: Some(Modifier::BOLD),
1008 remove_modifier: None,
1009 },
1010 footer_key: ColorSlot {
1011 truecolor: Some(Color::Rgb(172, 176, 190)),
1012 ansi16: Some(Color::DarkGray),
1013 add_modifier: None,
1014 remove_modifier: None,
1015 },
1016 badge: ColorSlot {
1017 truecolor: Some(Color::Rgb(30, 102, 245)),
1018 ansi16: Some(Color::Blue),
1019 add_modifier: Some(Modifier::BOLD),
1020 remove_modifier: Some(Modifier::DIM),
1021 },
1022 selected_fg: ColorSlot {
1023 truecolor: Some(Color::White),
1024 ansi16: Some(Color::White),
1025 add_modifier: Some(Modifier::BOLD),
1026 remove_modifier: None,
1027 },
1028 footer_key_fg: ColorSlot {
1029 truecolor: Some(Color::Rgb(76, 79, 105)), ansi16: Some(Color::Black),
1031 add_modifier: None,
1032 remove_modifier: None,
1033 },
1034 logo_dot: ColorSlot {
1035 truecolor: Some(Color::Rgb(0, 240, 255)),
1036 ansi16: Some(Color::Cyan),
1037 add_modifier: Some(Modifier::BOLD),
1038 remove_modifier: None,
1039 },
1040 }
1041 }
1042
1043 pub fn solarized_light() -> Self {
1044 Self {
1045 name: "Solarized Light".to_string(),
1046 accent: ColorSlot {
1047 truecolor: Some(Color::Rgb(38, 139, 210)),
1048 ansi16: Some(Color::Blue),
1049 add_modifier: None,
1050 remove_modifier: None,
1051 },
1052 accent_bg: ColorSlot {
1053 truecolor: Some(Color::Rgb(38, 139, 210)),
1054 ansi16: Some(Color::Blue),
1055 add_modifier: Some(Modifier::BOLD),
1056 remove_modifier: Some(Modifier::DIM),
1057 },
1058 success: ColorSlot {
1059 truecolor: Some(Color::Rgb(133, 153, 0)),
1060 ansi16: Some(Color::Green),
1061 add_modifier: Some(Modifier::BOLD),
1062 remove_modifier: None,
1063 },
1064 success_dim: ColorSlot {
1065 truecolor: Some(Color::Rgb(133, 153, 0)),
1066 ansi16: Some(Color::Green),
1067 add_modifier: Some(Modifier::DIM),
1068 remove_modifier: None,
1069 },
1070 warning: ColorSlot {
1071 truecolor: Some(Color::Rgb(181, 137, 0)),
1072 ansi16: Some(Color::Yellow),
1073 add_modifier: Some(Modifier::BOLD),
1074 remove_modifier: None,
1075 },
1076 error: ColorSlot {
1077 truecolor: Some(Color::Rgb(220, 50, 47)),
1078 ansi16: Some(Color::Red),
1079 add_modifier: Some(Modifier::BOLD),
1080 remove_modifier: None,
1081 },
1082 highlight: ColorSlot {
1083 truecolor: None,
1084 ansi16: None,
1085 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
1086 remove_modifier: None,
1087 },
1088 border: ColorSlot {
1089 truecolor: Some(Color::Rgb(147, 161, 161)),
1090 ansi16: None,
1091 add_modifier: None, remove_modifier: None,
1093 },
1094 border_active: ColorSlot {
1095 truecolor: Some(Color::Rgb(38, 139, 210)),
1096 ansi16: Some(Color::Blue),
1097 add_modifier: None,
1098 remove_modifier: None,
1099 },
1100 fg_muted: ColorSlot {
1101 truecolor: Some(Color::Rgb(147, 161, 161)),
1102 ansi16: None,
1103 add_modifier: None, remove_modifier: None,
1105 },
1106 fg_bold: ColorSlot {
1107 truecolor: None,
1108 ansi16: None,
1109 add_modifier: Some(Modifier::BOLD),
1110 remove_modifier: None,
1111 },
1112 footer_key: ColorSlot {
1113 truecolor: Some(Color::Rgb(7, 54, 66)), ansi16: Some(Color::DarkGray),
1115 add_modifier: None,
1116 remove_modifier: None,
1117 },
1118 badge: ColorSlot {
1119 truecolor: Some(Color::Rgb(38, 139, 210)),
1120 ansi16: Some(Color::Blue),
1121 add_modifier: Some(Modifier::BOLD),
1122 remove_modifier: Some(Modifier::DIM),
1123 },
1124 selected_fg: ColorSlot {
1125 truecolor: Some(Color::White),
1126 ansi16: Some(Color::White),
1127 add_modifier: Some(Modifier::BOLD),
1128 remove_modifier: None,
1129 },
1130 footer_key_fg: ColorSlot {
1131 truecolor: Some(Color::Rgb(238, 232, 213)), ansi16: Some(Color::White),
1133 add_modifier: None,
1134 remove_modifier: None,
1135 },
1136 logo_dot: ColorSlot {
1137 truecolor: Some(Color::Rgb(0, 240, 255)),
1138 ansi16: Some(Color::Cyan),
1139 add_modifier: Some(Modifier::BOLD),
1140 remove_modifier: None,
1141 },
1142 }
1143 }
1144
1145 pub fn no_color() -> Self {
1146 Self {
1147 name: "No Color".to_string(),
1148 accent: ColorSlot::new_with_modifier(Modifier::BOLD),
1149 accent_bg: ColorSlot {
1150 truecolor: None,
1151 ansi16: None,
1152 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
1153 remove_modifier: Some(Modifier::DIM),
1154 },
1155 success: ColorSlot::new_with_modifier(Modifier::BOLD),
1156 success_dim: ColorSlot::new(),
1157 warning: ColorSlot::new_with_modifier(Modifier::BOLD),
1158 error: ColorSlot::new_with_modifier(Modifier::BOLD),
1159 highlight: ColorSlot {
1160 truecolor: None,
1161 ansi16: None,
1162 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
1163 remove_modifier: None,
1164 },
1165 border: ColorSlot::new_with_modifier(Modifier::DIM),
1166 border_active: ColorSlot::new_with_modifier(Modifier::BOLD),
1167 fg_muted: ColorSlot::new_with_modifier(Modifier::DIM),
1168 fg_bold: ColorSlot::new_with_modifier(Modifier::BOLD),
1169 footer_key: ColorSlot::new_with_modifier(Modifier::REVERSED),
1170 badge: ColorSlot {
1171 truecolor: None,
1172 ansi16: None,
1173 add_modifier: Some(Modifier::BOLD | Modifier::REVERSED),
1174 remove_modifier: Some(Modifier::DIM),
1175 },
1176 selected_fg: ColorSlot::new_with_modifier(Modifier::BOLD),
1177 footer_key_fg: ColorSlot::new_with_modifier(Modifier::REVERSED),
1178 logo_dot: ColorSlot::new_with_modifier(Modifier::BOLD),
1179 }
1180 }
1181
1182 pub fn builtins() -> Vec<ThemeDef> {
1183 vec![
1184 Self::purple(),
1185 Self::purple_purple(),
1186 Self::catppuccin_mocha(),
1187 Self::dracula(),
1188 Self::gruvbox_dark(),
1189 Self::nord(),
1190 Self::tokyo_night(),
1191 Self::one_dark(),
1192 Self::catppuccin_latte(),
1193 Self::solarized_light(),
1194 Self::no_color(),
1195 ]
1196 }
1197
1198 pub fn find_builtin(name: &str) -> Option<ThemeDef> {
1199 Self::builtins()
1200 .into_iter()
1201 .find(|t| t.name.eq_ignore_ascii_case(name))
1202 }
1203
1204 pub fn parse_toml(content: &str) -> Option<Self> {
1205 let mut values: std::collections::HashMap<String, String> =
1206 std::collections::HashMap::new();
1207 for line in content.lines() {
1208 let line = line.trim();
1209 if line.is_empty() || line.starts_with('#') {
1210 continue;
1211 }
1212 if let Some((key, val)) = line.split_once('=') {
1213 let key = key.trim().to_string();
1214 let val = val.trim();
1215 let val = if let Some(idx) = val.find(" #") {
1216 &val[..idx]
1217 } else {
1218 val
1219 };
1220 let val = val.trim().trim_matches('"').to_string();
1221 values.insert(key, val);
1222 }
1223 }
1224 let name = values.get("name")?.to_string();
1225 let theme_name = name.clone();
1226 let fallback = Self::purple();
1227 let resolve_slot = |key: &str, fb: &ColorSlot| -> ColorSlot {
1228 let truecolor = match values.get(key) {
1229 Some(v) => match parse_hex(v) {
1230 Some(c) => Some(c),
1231 None => {
1232 log::warn!(
1233 "[config] theme '{}' field '{}' has invalid hex value '{}'; falling back",
1234 theme_name,
1235 key,
1236 v
1237 );
1238 fb.truecolor
1239 }
1240 },
1241 None => fb.truecolor,
1242 };
1243 let ansi_key = format!("{key}_ansi");
1244 let ansi16 = match values.get(&ansi_key) {
1245 Some(v) => match parse_ansi_name(v) {
1246 Some(c) => Some(c),
1247 None => {
1248 log::warn!(
1249 "[config] theme '{}' field '{}' has invalid colour name '{}'; falling back",
1250 theme_name,
1251 ansi_key,
1252 v
1253 );
1254 truecolor.and_then(auto_ansi16).or(fb.ansi16)
1255 }
1256 },
1257 None => truecolor.and_then(auto_ansi16).or(fb.ansi16),
1258 };
1259 ColorSlot {
1260 truecolor,
1261 ansi16,
1262 add_modifier: fb.add_modifier,
1263 remove_modifier: fb.remove_modifier,
1264 }
1265 };
1266 Some(Self {
1267 name,
1268 accent: resolve_slot("accent", &fallback.accent),
1269 accent_bg: resolve_slot("accent_bg", &fallback.accent_bg),
1270 success: resolve_slot("success", &fallback.success),
1271 success_dim: resolve_slot("success_dim", &fallback.success_dim),
1272 warning: resolve_slot("warning", &fallback.warning),
1273 error: resolve_slot("error", &fallback.error),
1274 highlight: fallback.highlight,
1275 border: resolve_slot("border", &fallback.border),
1276 border_active: resolve_slot("border_active", &fallback.border_active),
1277 fg_muted: resolve_slot("fg_muted", &fallback.fg_muted),
1278 fg_bold: fallback.fg_bold,
1279 footer_key: resolve_slot("footer_key_bg", &fallback.footer_key),
1280 badge: resolve_slot("badge_bg", &fallback.badge),
1281 selected_fg: resolve_slot("selected_fg", &fallback.selected_fg),
1282 footer_key_fg: resolve_slot("footer_key_fg", &fallback.footer_key_fg),
1283 logo_dot: resolve_slot("logo_dot", &fallback.logo_dot),
1284 })
1285 }
1286
1287 pub fn load_custom() -> Vec<ThemeDef> {
1288 let Some(home) = dirs::home_dir() else {
1289 return Vec::new();
1290 };
1291 let dir = home.join(".purple").join("themes");
1292 let Ok(entries) = std::fs::read_dir(&dir) else {
1293 return Vec::new();
1294 };
1295 let mut themes = Vec::new();
1296 for entry in entries.flatten() {
1297 let path = entry.path();
1298 if path.extension().is_some_and(|e| e == "toml") {
1299 if let Ok(content) = std::fs::read_to_string(&path) {
1300 if let Some(theme) = Self::parse_toml(&content) {
1301 themes.push(theme);
1302 } else {
1303 log::warn!("[config] Invalid theme file: {}", path.display());
1304 }
1305 }
1306 }
1307 }
1308 themes.sort_by(|a, b| a.name.cmp(&b.name));
1309 themes
1310 }
1311}
1312
1313fn parse_hex(s: &str) -> Option<Color> {
1318 let s = s.trim().strip_prefix('#')?;
1319 if s.len() != 6 {
1320 return None;
1321 }
1322 let r = u8::from_str_radix(&s[0..2], 16).ok()?;
1323 let g = u8::from_str_radix(&s[2..4], 16).ok()?;
1324 let b = u8::from_str_radix(&s[4..6], 16).ok()?;
1325 Some(Color::Rgb(r, g, b))
1326}
1327
1328fn parse_ansi_name(s: &str) -> Option<Color> {
1329 match s.to_ascii_lowercase().as_str() {
1330 "black" => Some(Color::Black),
1331 "red" => Some(Color::Red),
1332 "green" => Some(Color::Green),
1333 "yellow" => Some(Color::Yellow),
1334 "blue" => Some(Color::Blue),
1335 "magenta" => Some(Color::Magenta),
1336 "cyan" => Some(Color::Cyan),
1337 "white" => Some(Color::White),
1338 "darkgray" | "dark_gray" => Some(Color::DarkGray),
1339 "lightred" | "light_red" => Some(Color::LightRed),
1340 "lightgreen" | "light_green" => Some(Color::LightGreen),
1341 "lightyellow" | "light_yellow" => Some(Color::LightYellow),
1342 "lightblue" | "light_blue" => Some(Color::LightBlue),
1343 "lightmagenta" | "light_magenta" => Some(Color::LightMagenta),
1344 "lightcyan" | "light_cyan" => Some(Color::LightCyan),
1345 "gray" => Some(Color::Gray),
1346 _ => None,
1347 }
1348}
1349
1350fn auto_ansi16(color: Color) -> Option<Color> {
1351 let Color::Rgb(r, g, b) = color else {
1352 return Some(color);
1353 };
1354 let max = r.max(g).max(b);
1355 if max < 50 {
1356 return Some(Color::Black);
1357 }
1358 let is_bright = max > 170;
1359 if r > g && r > b {
1360 return Some(if is_bright {
1361 Color::LightRed
1362 } else {
1363 Color::Red
1364 });
1365 }
1366 if g > r && g > b {
1367 return Some(if is_bright {
1368 Color::LightGreen
1369 } else {
1370 Color::Green
1371 });
1372 }
1373 if b > r && b > g {
1374 return Some(if is_bright {
1375 Color::LightBlue
1376 } else {
1377 Color::Blue
1378 });
1379 }
1380 if r > 150 && g > 150 && b < 100 {
1381 return Some(Color::Yellow);
1382 }
1383 if r > 150 && b > 150 && g < 100 {
1384 return Some(Color::Magenta);
1385 }
1386 if g > 150 && b > 150 && r < 100 {
1387 return Some(Color::Cyan);
1388 }
1389 if r > 200 && g > 200 && b > 200 {
1390 return Some(Color::White);
1391 }
1392 if r > 100 && g > 100 && b > 100 {
1393 return Some(Color::Gray);
1394 }
1395 Some(Color::DarkGray)
1396}
1397
1398fn active_theme() -> std::sync::RwLockReadGuard<'static, ThemeDef> {
1403 THEME
1404 .get_or_init(|| RwLock::new(ThemeDef::purple()))
1405 .read()
1406 .unwrap_or_else(|e| e.into_inner())
1407}
1408
1409pub fn set_theme(theme: ThemeDef) {
1410 let lock = THEME.get_or_init(|| RwLock::new(ThemeDef::purple()));
1411 *lock.write().unwrap_or_else(|e| e.into_inner()) = theme;
1412}
1413
1414pub fn current_theme() -> ThemeDef {
1415 active_theme().clone()
1416}
1417
1418pub fn color_mode() -> u8 {
1419 COLOR_MODE.load(Ordering::Acquire)
1420}
1421
1422fn mode() -> u8 {
1424 COLOR_MODE.load(Ordering::Acquire)
1425}
1426
1427pub fn supports_colored_underline() -> bool {
1431 COLORED_UNDERLINE.load(Ordering::Acquire) != 0
1432}
1433
1434fn detects_colored_underline(term_program: Option<&str>) -> bool {
1438 !matches!(term_program, Some("Apple_Terminal"))
1440}
1441
1442fn detect_terminal_caps() {
1444 let term_program = std::env::var("TERM_PROGRAM").ok();
1445 let supports = detects_colored_underline(term_program.as_deref());
1446 COLORED_UNDERLINE.store(u8::from(supports), Ordering::Release);
1447}
1448
1449pub fn init() {
1451 detect_terminal_caps();
1452 if std::env::var_os("NO_COLOR").is_some() {
1453 COLOR_MODE.store(0, Ordering::Release);
1454 set_theme(ThemeDef::no_color());
1455 return;
1456 }
1457 if std::env::var("COLORTERM")
1458 .map(|v| v == "truecolor" || v == "24bit")
1459 .unwrap_or(false)
1460 {
1461 COLOR_MODE.store(2, Ordering::Release);
1462 }
1463 if let Some(name) = crate::preferences::load_theme() {
1464 if let Some(theme) = ThemeDef::find_builtin(&name) {
1465 set_theme(theme);
1466 } else {
1467 let custom = ThemeDef::load_custom();
1468 if let Some(theme) = custom
1469 .into_iter()
1470 .find(|t| t.name.eq_ignore_ascii_case(&name))
1471 {
1472 set_theme(theme);
1473 }
1474 }
1475 }
1476}
1477
1478#[cfg(test)]
1479pub(crate) fn init_with_mode(m: u8) {
1480 COLOR_MODE.store(m, Ordering::Release);
1481 let _ = THEME.get_or_init(|| RwLock::new(ThemeDef::purple()));
1482}
1483
1484#[cfg(test)]
1485pub(crate) fn set_colored_underline_support(v: bool) {
1486 COLORED_UNDERLINE.store(u8::from(v), Ordering::Release);
1487}
1488
1489pub fn brand_badge() -> Style {
1497 let m = mode();
1498 let t = active_theme();
1499 if m == 0 {
1500 Style::default()
1501 .add_modifier(Modifier::BOLD | Modifier::REVERSED)
1502 .remove_modifier(Modifier::DIM)
1503 } else {
1504 let mut style = t.selected_fg.to_style(m);
1505 style = match m {
1506 2 => {
1507 if let Some(c) = t.badge.truecolor {
1508 style.bg(c)
1509 } else {
1510 style
1511 }
1512 }
1513 _ => {
1514 if let Some(c) = t.badge.ansi16 {
1515 style.bg(c)
1516 } else {
1517 style
1518 }
1519 }
1520 };
1521 if let Some(add) = t.badge.add_modifier {
1522 style = style.add_modifier(add);
1523 }
1524 if let Some(rm) = t.badge.remove_modifier {
1525 style = style.remove_modifier(rm);
1526 }
1527 style
1528 }
1529}
1530
1531pub fn brand() -> Style {
1534 Style::default()
1535 .add_modifier(Modifier::BOLD)
1536 .remove_modifier(Modifier::DIM)
1537}
1538
1539pub fn nav_active() -> Style {
1551 let mut style = bold().add_modifier(Modifier::UNDERLINED);
1552 if !supports_colored_underline() {
1553 return style;
1554 }
1555 let m = mode();
1556 let accent_color = match m {
1557 2 => active_theme().accent.truecolor,
1558 1 => active_theme().accent.ansi16,
1559 _ => None,
1560 };
1561 if let Some(c) = accent_color {
1562 style = style.underline_color(c);
1563 }
1564 style
1565}
1566
1567pub fn border_dim() -> Style {
1578 active_theme().border.to_style(mode())
1579}
1580
1581pub fn accent() -> Style {
1588 active_theme().accent.to_style(mode())
1589}
1590
1591pub fn accent_bold() -> Style {
1593 let mut style = active_theme().accent.to_style(mode());
1594 style = style.add_modifier(Modifier::BOLD);
1595 style
1596}
1597
1598pub fn highlight_bold() -> Style {
1600 active_theme().highlight.to_style(mode())
1601}
1602
1603pub fn footer_key() -> Style {
1608 let m = mode();
1609 if m == 0 {
1610 return Style::default().add_modifier(Modifier::REVERSED);
1611 }
1612 let t = active_theme();
1613 let mut style = t.footer_key_fg.to_style(m);
1614 style = match m {
1615 2 => {
1616 if let Some(c) = t.footer_key.truecolor {
1617 style.bg(c)
1618 } else {
1619 style
1620 }
1621 }
1622 _ => {
1623 if let Some(c) = t.footer_key.ansi16 {
1624 style.bg(c)
1625 } else {
1626 style
1627 }
1628 }
1629 };
1630 style
1631}
1632
1633pub fn muted() -> Style {
1635 active_theme().fg_muted.to_style(mode())
1636}
1637
1638pub fn section_header() -> Style {
1640 active_theme().fg_bold.to_style(mode())
1641}
1642
1643pub fn error() -> Style {
1645 active_theme().error.to_style(mode())
1646}
1647
1648pub fn success() -> Style {
1650 active_theme().success.to_style(mode())
1651}
1652
1653pub fn online_dot() -> Style {
1656 active_theme().success_dim.to_style(mode())
1657}
1658
1659pub fn healthy() -> Style {
1670 active_theme().success_dim.to_style(mode())
1671}
1672
1673pub fn tunnel_active() -> Style {
1677 active_theme().accent.to_style(mode())
1678}
1679
1680pub fn tag_user() -> Style {
1684 active_theme().accent.to_style(mode())
1685}
1686
1687pub fn online_dot_pulsing(tick: u64) -> Style {
1701 use ratatui::style::Modifier;
1702 const PERIOD: u64 = 30;
1703 let phase = (tick % PERIOD) as f32 * std::f32::consts::TAU / PERIOD as f32;
1704 let alpha = 0.40 + 0.60 * (phase.sin() * 0.5 + 0.5);
1710 let m = mode();
1711 if m == 2 {
1712 let base = active_theme().success.truecolor;
1717 if let Some(ratatui::style::Color::Rgb(r, g, b)) = base {
1718 let lerp = |c: u8| -> u8 {
1719 let f = c as f32 / 255.0;
1720 let dim_f = f * 0.55;
1724 ((dim_f + (f - dim_f) * alpha).clamp(0.0, 1.0) * 255.0).round() as u8
1725 };
1726 return ratatui::style::Style::default().fg(ratatui::style::Color::Rgb(
1727 lerp(r),
1728 lerp(g),
1729 lerp(b),
1730 ));
1731 }
1732 }
1734 if alpha > 0.85 {
1739 active_theme().success.to_style(m)
1740 } else if alpha < 0.55 {
1741 active_theme().success_dim.to_style(m)
1742 } else {
1743 active_theme()
1744 .success
1745 .to_style(m)
1746 .remove_modifier(Modifier::BOLD)
1747 }
1748}
1749
1750pub fn warning() -> Style {
1752 active_theme().warning.to_style(mode())
1753}
1754
1755pub fn toast_border_success() -> Style {
1757 let m = mode();
1758 let t = active_theme();
1759 let mut style = t.success.to_style(m);
1760 if m == 0 {
1761 style = Style::default().add_modifier(Modifier::BOLD);
1762 }
1763 style
1764}
1765
1766pub fn toast_border_error() -> Style {
1768 let m = mode();
1769 let t = active_theme();
1770 let mut style = t.error.to_style(m);
1771 if m == 0 {
1772 style = Style::default().add_modifier(Modifier::BOLD);
1773 }
1774 style
1775}
1776
1777pub fn toast_border_warning() -> Style {
1781 let m = mode();
1782 let t = active_theme();
1783 let mut style = t.warning.to_style(m);
1784 if m == 0 {
1785 style = Style::default().add_modifier(Modifier::BOLD);
1786 }
1787 style
1788}
1789
1790pub fn danger() -> Style {
1792 active_theme().error.to_style(mode())
1793}
1794
1795pub fn border() -> Style {
1797 active_theme().border.to_style(mode())
1798}
1799
1800pub fn version() -> Style {
1802 active_theme().accent.to_style(mode())
1803}
1804
1805pub fn border_search() -> Style {
1807 active_theme().border_active.to_style(mode())
1808}
1809
1810pub fn logo_dot() -> Style {
1814 active_theme().logo_dot.to_style(mode())
1815}
1816
1817pub fn selected_row() -> Style {
1819 let m = mode();
1820 let t = active_theme();
1821 if m == 0 {
1822 return Style::default()
1823 .add_modifier(Modifier::REVERSED)
1824 .remove_modifier(Modifier::DIM);
1825 }
1826 let mut style = t.selected_fg.to_style(m);
1827 style = match m {
1828 2 => {
1829 if let Some(c) = t.accent_bg.truecolor {
1830 style.bg(c)
1831 } else {
1832 style
1833 }
1834 }
1835 _ => {
1836 if let Some(c) = t.accent_bg.ansi16 {
1837 style.bg(c)
1838 } else {
1839 style
1840 }
1841 }
1842 };
1843 if let Some(add) = t.accent_bg.add_modifier {
1844 style = style.add_modifier(add);
1845 }
1846 if let Some(rm) = t.accent_bg.remove_modifier {
1847 style = style.remove_modifier(rm);
1848 }
1849 style
1850}
1851
1852pub fn border_danger() -> Style {
1854 active_theme().error.to_style(mode())
1855}
1856
1857pub fn bold() -> Style {
1859 active_theme().fg_bold.to_style(mode())
1860}
1861
1862pub fn update_badge() -> Style {
1864 brand_badge()
1865}
1866
1867#[cfg(test)]
1872static TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
1873
1874#[cfg(test)]
1875#[path = "theme_tests.rs"]
1876mod tests;