1use crate::border::Border;
36use crate::color::{AdaptiveColor, Color, ansi256_to_rgb};
37use crate::position::{Position, Sides};
38use crate::renderer::Renderer;
39use crate::style::Style;
40use serde::{Deserialize, Serialize};
41use std::collections::HashMap;
42use std::fmt;
43#[cfg(feature = "native")]
44use std::fs;
45use std::panic::{AssertUnwindSafe, catch_unwind};
46#[cfg(feature = "native")]
47use std::path::Path;
48use std::sync::atomic::{AtomicU64, Ordering};
49use std::sync::{Arc, LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
50use thiserror::Error;
51use tracing::{debug, info, trace, warn};
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Theme {
60 #[serde(default)]
62 name: String,
63
64 #[serde(default)]
66 is_dark: bool,
67
68 #[serde(default)]
70 colors: ThemeColors,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 description: Option<String>,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 author: Option<String>,
79
80 #[serde(default, skip_serializing_if = "ThemeMeta::is_empty")]
82 meta: ThemeMeta,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ThemeColors {
92 pub primary: Color,
97
98 pub secondary: Color,
100
101 pub accent: Color,
103
104 pub background: Color,
109
110 pub surface: Color,
112
113 pub surface_alt: Color,
115
116 pub text: Color,
121
122 pub text_muted: Color,
124
125 pub text_disabled: Color,
127
128 pub success: Color,
133
134 pub warning: Color,
136
137 pub error: Color,
139
140 pub info: Color,
142
143 pub border: Color,
148
149 pub border_muted: Color,
151
152 pub separator: Color,
154
155 pub focus: Color,
160
161 pub selection: Color,
163
164 pub hover: Color,
166
167 pub code_keyword: Color,
172
173 pub code_string: Color,
175
176 pub code_number: Color,
178
179 pub code_comment: Color,
181
182 pub code_function: Color,
184
185 pub code_type: Color,
187
188 pub code_variable: Color,
190
191 pub code_operator: Color,
193
194 #[serde(default, flatten)]
196 pub custom: HashMap<String, Color>,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
201pub enum ColorSlot {
202 Primary,
204 Secondary,
206 Accent,
208 Background,
210 Foreground,
212 Text,
214 TextMuted,
216 TextDisabled,
218 Surface,
220 SurfaceAlt,
222 Success,
224 Warning,
226 Error,
228 Info,
230 Border,
232 BorderMuted,
234 Separator,
236 Focus,
238 Selection,
240 Hover,
242 CodeKeyword,
244 CodeString,
246 CodeNumber,
248 CodeComment,
250 CodeFunction,
252 CodeType,
254 CodeVariable,
256 CodeOperator,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
262pub enum ThemeRole {
263 Primary,
265 Success,
267 Warning,
269 Error,
271 Muted,
273 Inverted,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
279pub enum ThemePreset {
280 Dark,
281 Light,
282 Dracula,
283 Nord,
284 Catppuccin(CatppuccinFlavor),
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289pub enum CatppuccinFlavor {
290 Latte,
292 Frappe,
294 Macchiato,
296 Mocha,
298}
299
300impl fmt::Display for CatppuccinFlavor {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 let name = match self {
303 Self::Latte => "Latte",
304 Self::Frappe => "Frappe",
305 Self::Macchiato => "Macchiato",
306 Self::Mocha => "Mocha",
307 };
308 f.write_str(name)
309 }
310}
311
312impl fmt::Display for ThemePreset {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 match self {
315 Self::Dark => f.write_str("Dark"),
316 Self::Light => f.write_str("Light"),
317 Self::Dracula => f.write_str("Dracula"),
318 Self::Nord => f.write_str("Nord"),
319 Self::Catppuccin(flavor) => write!(f, "Catppuccin {flavor}"),
320 }
321 }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
326#[serde(rename_all = "lowercase")]
327pub enum ThemeVariant {
328 Light,
329 Dark,
330}
331
332#[derive(Debug, Clone, Default, Serialize, Deserialize)]
334pub struct ThemeMeta {
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub version: Option<String>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub variant: Option<ThemeVariant>,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub source: Option<String>,
341}
342
343impl ThemeMeta {
344 fn is_empty(&self) -> bool {
345 self.version.is_none() && self.variant.is_none() && self.source.is_none()
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
351pub struct ListenerId(u64);
352
353pub trait ThemeChangeListener: Send + Sync {
355 fn on_theme_change(&self, theme: &Theme);
356}
357
358impl<F> ThemeChangeListener for F
359where
360 F: Fn(&Theme) + Send + Sync,
361{
362 fn on_theme_change(&self, theme: &Theme) {
363 self(theme);
364 }
365}
366
367#[derive(Clone)]
369pub struct ThemeContext {
370 current: Arc<RwLock<Theme>>,
371 listeners: Arc<RwLock<HashMap<ListenerId, Arc<dyn ThemeChangeListener>>>>,
372 next_listener_id: Arc<AtomicU64>,
373}
374
375fn read_lock_or_recover<'a, T>(lock: &'a RwLock<T>, lock_name: &str) -> RwLockReadGuard<'a, T> {
376 match lock.read() {
377 Ok(guard) => guard,
378 Err(poisoned) => {
379 warn!(lock = lock_name, "Recovering from poisoned read lock");
380 poisoned.into_inner()
381 }
382 }
383}
384
385fn write_lock_or_recover<'a, T>(lock: &'a RwLock<T>, lock_name: &str) -> RwLockWriteGuard<'a, T> {
386 match lock.write() {
387 Ok(guard) => guard,
388 Err(poisoned) => {
389 warn!(lock = lock_name, "Recovering from poisoned write lock");
390 poisoned.into_inner()
391 }
392 }
393}
394
395impl fmt::Debug for ThemeContext {
396 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397 let listener_count = read_lock_or_recover(&self.listeners, "theme.listeners").len();
398 f.debug_struct("ThemeContext")
399 .field("current", &"<RwLock<Theme>>")
400 .field("listeners", &format!("{listener_count} listeners"))
401 .field("next_listener_id", &self.next_listener_id)
402 .finish()
403 }
404}
405
406impl ThemeContext {
407 pub fn new(initial: Theme) -> Self {
409 Self {
410 current: Arc::new(RwLock::new(initial)),
411 listeners: Arc::new(RwLock::new(HashMap::new())),
412 next_listener_id: Arc::new(AtomicU64::new(1)),
413 }
414 }
415
416 pub fn from_preset(preset: ThemePreset) -> Self {
418 Self::new(preset.to_theme())
419 }
420
421 pub fn current(&self) -> std::sync::RwLockReadGuard<'_, Theme> {
423 let guard = read_lock_or_recover(&self.current, "theme.current");
424 trace!(theme.name = %guard.name(), "Theme read");
425 guard
426 }
427
428 pub fn set_theme(&self, theme: Theme) {
430 let from = {
431 let current = read_lock_or_recover(&self.current, "theme.current");
432 current.name().to_string()
433 };
434 let to = theme.name().to_string();
435 let snapshot = theme.clone();
436 {
437 let mut current = write_lock_or_recover(&self.current, "theme.current");
438 *current = theme;
439 }
440
441 info!(theme.from = %from, theme.to = %to, "Theme switched");
442 self.notify_listeners(&snapshot);
443 }
444
445 pub fn set_preset(&self, preset: ThemePreset) {
447 self.set_theme(preset.to_theme());
448 }
449
450 pub fn on_change<F>(&self, callback: F) -> ListenerId
452 where
453 F: Fn(&Theme) + Send + Sync + 'static,
454 {
455 let id = ListenerId(self.next_listener_id.fetch_add(1, Ordering::Relaxed));
456 write_lock_or_recover(&self.listeners, "theme.listeners").insert(id, Arc::new(callback));
457 debug!(theme.listener_id = id.0, "Theme listener registered");
458 id
459 }
460
461 pub fn remove_listener(&self, id: ListenerId) {
463 let mut listeners = write_lock_or_recover(&self.listeners, "theme.listeners");
464 if listeners.remove(&id).is_some() {
465 debug!(theme.listener_id = id.0, "Theme listener removed");
466 }
467 }
468
469 fn notify_listeners(&self, theme: &Theme) {
470 let listeners: Vec<(ListenerId, Arc<dyn ThemeChangeListener>)> = {
471 let listeners = read_lock_or_recover(&self.listeners, "theme.listeners");
472 listeners
473 .iter()
474 .map(|(id, listener)| (*id, Arc::clone(listener)))
475 .collect()
476 };
477
478 for (id, listener) in listeners {
479 let result = catch_unwind(AssertUnwindSafe(|| listener.on_theme_change(theme)));
480 if result.is_err() {
481 warn!(
482 theme.listener_id = id.0,
483 theme.name = %theme.name(),
484 "Theme listener panicked"
485 );
486 }
487 }
488 }
489}
490
491static GLOBAL_THEME_CONTEXT: LazyLock<ThemeContext> =
492 LazyLock::new(|| ThemeContext::from_preset(ThemePreset::Dark));
493
494pub fn global_theme() -> &'static ThemeContext {
496 &GLOBAL_THEME_CONTEXT
497}
498
499pub fn set_global_theme(theme: Theme) {
501 GLOBAL_THEME_CONTEXT.set_theme(theme);
502}
503
504pub fn set_global_preset(preset: ThemePreset) {
506 GLOBAL_THEME_CONTEXT.set_preset(preset);
507}
508
509#[derive(Clone, Debug)]
515pub struct ThemedStyle {
516 context: Arc<ThemeContext>,
517 foreground: Option<ThemedColor>,
518 background: Option<ThemedColor>,
519 border_foreground: Option<ThemedColor>,
520 border_background: Option<ThemedColor>,
521 base_style: Style,
522}
523
524#[derive(Clone, Debug)]
526pub enum ThemedColor {
527 Fixed(Color),
529 Slot(ColorSlot),
531 Computed(ColorSlot, ColorTransform),
533}
534
535#[derive(Clone, Copy, Debug)]
537pub enum ColorTransform {
538 Lighten(f32),
540 Darken(f32),
542 Saturate(f32),
544 Desaturate(f32),
546 Alpha(f32),
548}
549
550impl ThemedStyle {
551 pub fn new(context: Arc<ThemeContext>) -> Self {
553 Self {
554 context,
555 foreground: None,
556 background: None,
557 border_foreground: None,
558 border_background: None,
559 base_style: Style::new(),
560 }
561 }
562
563 pub fn global() -> Self {
565 Self::new(Arc::new(global_theme().clone()))
566 }
567
568 pub fn resolve(&self) -> Style {
570 let Ok(theme) = catch_unwind(AssertUnwindSafe(|| self.context.current())) else {
571 warn!("themed_style.resolve called without a valid theme context");
572 return self.base_style.clone();
573 };
574
575 let mut style = self.base_style.clone();
576
577 if let Some(ref fg) = self.foreground {
578 let color = Self::resolve_color(fg, &theme);
579 style = style.foreground_color(color);
580 }
581 if let Some(ref bg) = self.background {
582 let color = Self::resolve_color(bg, &theme);
583 style = style.background_color(color);
584 }
585 if let Some(ref bfg) = self.border_foreground {
586 let color = Self::resolve_color(bfg, &theme);
587 style = style.border_foreground(color.0);
588 }
589 if let Some(ref bbg) = self.border_background {
590 let color = Self::resolve_color(bbg, &theme);
591 style = style.border_background(color.0);
592 }
593
594 drop(theme);
595 style
596 }
597
598 pub fn render(&self, text: &str) -> String {
600 self.resolve().render(text)
601 }
602
603 fn resolve_color(themed: &ThemedColor, theme: &Theme) -> Color {
604 match themed {
605 ThemedColor::Fixed(color) => {
606 debug!(themed_style.resolve = true, color_kind = "fixed", color = %color.0);
607 color.clone()
608 }
609 ThemedColor::Slot(slot) => {
610 let color = theme.get(*slot);
611 debug!(
612 themed_style.resolve = true,
613 color_kind = "slot",
614 color_slot = ?slot,
615 color = %color.0
616 );
617 color
618 }
619 ThemedColor::Computed(slot, transform) => {
620 let base = theme.get(*slot);
621 let color = transform.apply(base);
622 debug!(
623 themed_style.resolve = true,
624 color_kind = "computed",
625 color_slot = ?slot,
626 transform = ?transform,
627 color = %color.0
628 );
629 color
630 }
631 }
632 }
633
634 pub fn foreground(mut self, slot: ColorSlot) -> Self {
640 self.foreground = Some(ThemedColor::Slot(slot));
641 self
642 }
643
644 pub fn foreground_fixed(mut self, color: impl Into<Color>) -> Self {
646 self.foreground = Some(ThemedColor::Fixed(color.into()));
647 self
648 }
649
650 pub fn foreground_computed(mut self, slot: ColorSlot, transform: ColorTransform) -> Self {
652 self.foreground = Some(ThemedColor::Computed(slot, transform));
653 self
654 }
655
656 pub fn no_foreground(mut self) -> Self {
658 self.foreground = None;
659 self.base_style = self.base_style.no_foreground();
660 self
661 }
662
663 pub fn background(mut self, slot: ColorSlot) -> Self {
665 self.background = Some(ThemedColor::Slot(slot));
666 self
667 }
668
669 pub fn background_fixed(mut self, color: impl Into<Color>) -> Self {
671 self.background = Some(ThemedColor::Fixed(color.into()));
672 self
673 }
674
675 pub fn background_computed(mut self, slot: ColorSlot, transform: ColorTransform) -> Self {
677 self.background = Some(ThemedColor::Computed(slot, transform));
678 self
679 }
680
681 pub fn no_background(mut self) -> Self {
683 self.background = None;
684 self.base_style = self.base_style.no_background();
685 self
686 }
687
688 pub fn border_foreground(mut self, slot: ColorSlot) -> Self {
690 self.border_foreground = Some(ThemedColor::Slot(slot));
691 self
692 }
693
694 pub fn border_foreground_fixed(mut self, color: impl Into<Color>) -> Self {
696 self.border_foreground = Some(ThemedColor::Fixed(color.into()));
697 self
698 }
699
700 pub fn border_foreground_computed(
702 mut self,
703 slot: ColorSlot,
704 transform: ColorTransform,
705 ) -> Self {
706 self.border_foreground = Some(ThemedColor::Computed(slot, transform));
707 self
708 }
709
710 pub fn border_background(mut self, slot: ColorSlot) -> Self {
712 self.border_background = Some(ThemedColor::Slot(slot));
713 self
714 }
715
716 pub fn border_background_fixed(mut self, color: impl Into<Color>) -> Self {
718 self.border_background = Some(ThemedColor::Fixed(color.into()));
719 self
720 }
721
722 pub fn border_background_computed(
724 mut self,
725 slot: ColorSlot,
726 transform: ColorTransform,
727 ) -> Self {
728 self.border_background = Some(ThemedColor::Computed(slot, transform));
729 self
730 }
731
732 pub fn set_string(mut self, s: impl Into<String>) -> Self {
738 self.base_style = self.base_style.set_string(s);
739 self
740 }
741
742 pub fn value(&self) -> &str {
744 self.base_style.value()
745 }
746
747 pub fn bold(mut self) -> Self {
749 self.base_style = self.base_style.bold();
750 self
751 }
752
753 pub fn italic(mut self) -> Self {
755 self.base_style = self.base_style.italic();
756 self
757 }
758
759 pub fn underline(mut self) -> Self {
761 self.base_style = self.base_style.underline();
762 self
763 }
764
765 pub fn strikethrough(mut self) -> Self {
767 self.base_style = self.base_style.strikethrough();
768 self
769 }
770
771 pub fn reverse(mut self) -> Self {
773 self.base_style = self.base_style.reverse();
774 self
775 }
776
777 pub fn blink(mut self) -> Self {
779 self.base_style = self.base_style.blink();
780 self
781 }
782
783 pub fn faint(mut self) -> Self {
785 self.base_style = self.base_style.faint();
786 self
787 }
788
789 pub fn underline_spaces(mut self, v: bool) -> Self {
791 self.base_style = self.base_style.underline_spaces(v);
792 self
793 }
794
795 pub fn strikethrough_spaces(mut self, v: bool) -> Self {
797 self.base_style = self.base_style.strikethrough_spaces(v);
798 self
799 }
800
801 pub fn width(mut self, w: u16) -> Self {
803 self.base_style = self.base_style.width(w);
804 self
805 }
806
807 pub fn height(mut self, h: u16) -> Self {
809 self.base_style = self.base_style.height(h);
810 self
811 }
812
813 pub fn max_width(mut self, w: u16) -> Self {
815 self.base_style = self.base_style.max_width(w);
816 self
817 }
818
819 pub fn max_height(mut self, h: u16) -> Self {
821 self.base_style = self.base_style.max_height(h);
822 self
823 }
824
825 pub fn align(mut self, p: Position) -> Self {
827 self.base_style = self.base_style.align(p);
828 self
829 }
830
831 pub fn align_horizontal(mut self, p: Position) -> Self {
833 self.base_style = self.base_style.align_horizontal(p);
834 self
835 }
836
837 pub fn align_vertical(mut self, p: Position) -> Self {
839 self.base_style = self.base_style.align_vertical(p);
840 self
841 }
842
843 pub fn padding(mut self, sides: impl Into<Sides<u16>>) -> Self {
845 self.base_style = self.base_style.padding(sides);
846 self
847 }
848
849 pub fn padding_top(mut self, n: u16) -> Self {
851 self.base_style = self.base_style.padding_top(n);
852 self
853 }
854
855 pub fn padding_right(mut self, n: u16) -> Self {
857 self.base_style = self.base_style.padding_right(n);
858 self
859 }
860
861 pub fn padding_bottom(mut self, n: u16) -> Self {
863 self.base_style = self.base_style.padding_bottom(n);
864 self
865 }
866
867 pub fn padding_left(mut self, n: u16) -> Self {
869 self.base_style = self.base_style.padding_left(n);
870 self
871 }
872
873 pub fn margin(mut self, sides: impl Into<Sides<u16>>) -> Self {
875 self.base_style = self.base_style.margin(sides);
876 self
877 }
878
879 pub fn margin_top(mut self, n: u16) -> Self {
881 self.base_style = self.base_style.margin_top(n);
882 self
883 }
884
885 pub fn margin_right(mut self, n: u16) -> Self {
887 self.base_style = self.base_style.margin_right(n);
888 self
889 }
890
891 pub fn margin_bottom(mut self, n: u16) -> Self {
893 self.base_style = self.base_style.margin_bottom(n);
894 self
895 }
896
897 pub fn margin_left(mut self, n: u16) -> Self {
899 self.base_style = self.base_style.margin_left(n);
900 self
901 }
902
903 pub fn margin_background(mut self, color: impl Into<String>) -> Self {
905 self.base_style = self.base_style.margin_background(color);
906 self
907 }
908
909 pub fn border(mut self, border: Border) -> Self {
911 self.base_style = self.base_style.border(border);
912 self
913 }
914
915 pub fn border_style(mut self, border: Border) -> Self {
917 self.base_style = self.base_style.border_style(border);
918 self
919 }
920
921 pub fn border_top(mut self, v: bool) -> Self {
923 self.base_style = self.base_style.border_top(v);
924 self
925 }
926
927 pub fn border_right(mut self, v: bool) -> Self {
929 self.base_style = self.base_style.border_right(v);
930 self
931 }
932
933 pub fn border_bottom(mut self, v: bool) -> Self {
935 self.base_style = self.base_style.border_bottom(v);
936 self
937 }
938
939 pub fn border_left(mut self, v: bool) -> Self {
941 self.base_style = self.base_style.border_left(v);
942 self
943 }
944
945 pub fn inline(mut self) -> Self {
947 self.base_style = self.base_style.inline();
948 self
949 }
950
951 pub fn tab_width(mut self, n: i8) -> Self {
953 self.base_style = self.base_style.tab_width(n);
954 self
955 }
956
957 pub fn transform<F>(mut self, f: F) -> Self
959 where
960 F: Fn(&str) -> String + Send + Sync + 'static,
961 {
962 self.base_style = self.base_style.transform(f);
963 self
964 }
965
966 pub fn renderer(mut self, r: Arc<Renderer>) -> Self {
968 self.base_style = self.base_style.renderer(r);
969 self
970 }
971
972 pub fn is_set(&self, prop: crate::style::Props) -> bool {
974 self.base_style.is_set(prop)
975 }
976}
977
978#[allow(clippy::many_single_char_names)]
979impl ColorTransform {
980 fn apply(self, color: Color) -> Color {
981 let (r, g, b) = if let Some((r, g, b)) = color.as_rgb() {
982 (r, g, b)
983 } else if let Some(n) = color.as_ansi() {
984 ansi256_to_rgb(n)
985 } else {
986 return color;
987 };
988
989 let (h, mut s, mut l) = rgb_to_hsl(r, g, b);
990 let amount = |v: f32| v.clamp(0.0, 1.0);
991
992 match self {
993 ColorTransform::Lighten(a) => l = (l + amount(a)).min(1.0),
994 ColorTransform::Darken(a) => l = (l - amount(a)).max(0.0),
995 ColorTransform::Saturate(a) => s = (s + amount(a)).min(1.0),
996 ColorTransform::Desaturate(a) => s = (s - amount(a)).max(0.0),
997 ColorTransform::Alpha(a) => {
998 let a = amount(a);
999 l = (l * a).min(1.0);
1000 }
1001 }
1002
1003 let (nr, ng, nb) = hsl_to_rgb(h, s, l);
1004 Color::from(format!("#{:02x}{:02x}{:02x}", nr, ng, nb))
1005 }
1006}
1007
1008#[allow(clippy::many_single_char_names)]
1009fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
1010 let r = r as f32 / 255.0;
1011 let g = g as f32 / 255.0;
1012 let b = b as f32 / 255.0;
1013
1014 let max = r.max(g).max(b);
1015 let min = r.min(g).min(b);
1016 let l = f32::midpoint(max, min);
1017
1018 if (max - min).abs() < f32::EPSILON {
1019 return (0.0, 0.0, l);
1020 }
1021
1022 let d = max - min;
1023 let s = if l > 0.5 {
1024 d / (2.0 - max - min)
1025 } else {
1026 d / (max + min)
1027 };
1028
1029 let mut h = if (max - r).abs() < f32::EPSILON {
1030 (g - b) / d + if g < b { 6.0 } else { 0.0 }
1031 } else if (max - g).abs() < f32::EPSILON {
1032 (b - r) / d + 2.0
1033 } else {
1034 (r - g) / d + 4.0
1035 };
1036
1037 h /= 6.0;
1038 (h * 360.0, s, l)
1039}
1040
1041#[allow(clippy::many_single_char_names, clippy::suboptimal_flops)]
1042fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
1043 if s == 0.0 {
1044 let v = (l * 255.0).round() as u8;
1045 return (v, v, v);
1046 }
1047
1048 let h = h / 360.0;
1049 let q = if l < 0.5 {
1050 l * (1.0 + s)
1051 } else {
1052 l + s - l * s
1053 };
1054 let p = 2.0 * l - q;
1055
1056 fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
1057 if t < 0.0 {
1058 t += 1.0;
1059 }
1060 if t > 1.0 {
1061 t -= 1.0;
1062 }
1063 if t < 1.0 / 6.0 {
1064 return p + (q - p) * 6.0 * t;
1065 }
1066 if t < 1.0 / 2.0 {
1067 return q;
1068 }
1069 if t < 2.0 / 3.0 {
1070 return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
1071 }
1072 p
1073 }
1074
1075 let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
1076 let g = hue_to_rgb(p, q, h);
1077 let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
1078
1079 (
1080 (r * 255.0).round() as u8,
1081 (g * 255.0).round() as u8,
1082 (b * 255.0).round() as u8,
1083 )
1084}
1085
1086pub struct CachedThemedStyle {
1088 themed: ThemedStyle,
1089 cache: Arc<RwLock<Option<Style>>>,
1090 listener_id: ListenerId,
1091}
1092
1093impl CachedThemedStyle {
1094 pub fn new(themed: ThemedStyle) -> Self {
1096 let cache = Arc::new(RwLock::new(None));
1097 let cache_ref = Arc::clone(&cache);
1098 let listener_id = themed.context.on_change(move |_theme| {
1099 if let Ok(mut guard) = cache_ref.write() {
1100 *guard = None;
1101 }
1102 trace!("cached_themed_style cache invalidated");
1103 });
1104
1105 Self {
1106 themed,
1107 cache,
1108 listener_id,
1109 }
1110 }
1111
1112 pub fn resolve(&self) -> Style {
1114 if let Ok(cache) = self.cache.read() {
1115 if let Some(style) = cache.as_ref() {
1116 trace!("cached_themed_style cache hit");
1117 return style.clone();
1118 }
1119 }
1120
1121 trace!("cached_themed_style cache miss");
1122 let resolved = self.themed.resolve();
1123 if let Ok(mut cache) = self.cache.write() {
1124 *cache = Some(resolved.clone());
1125 }
1126 resolved
1127 }
1128
1129 pub fn render(&self, text: &str) -> String {
1131 self.resolve().render(text)
1132 }
1133
1134 pub fn invalidate(&self) {
1136 if let Ok(mut cache) = self.cache.write() {
1137 *cache = None;
1138 }
1139 }
1140}
1141
1142impl Drop for CachedThemedStyle {
1143 fn drop(&mut self) {
1144 self.themed.context.remove_listener(self.listener_id);
1145 }
1146}
1147
1148#[cfg(feature = "tokio")]
1150pub struct AsyncThemeContext {
1151 sender: tokio::sync::watch::Sender<Theme>,
1152 receiver: tokio::sync::watch::Receiver<Theme>,
1153}
1154
1155#[cfg(feature = "tokio")]
1156impl AsyncThemeContext {
1157 pub fn new(initial: Theme) -> Self {
1159 let (sender, receiver) = tokio::sync::watch::channel(initial);
1160 Self { sender, receiver }
1161 }
1162
1163 pub fn from_preset(preset: ThemePreset) -> Self {
1165 Self::new(preset.to_theme())
1166 }
1167
1168 pub fn current(&self) -> Theme {
1170 self.receiver.borrow().clone()
1171 }
1172
1173 pub fn set_theme(&self, theme: Theme) {
1175 let from = self.receiver.borrow().name().to_string();
1176 let to = theme.name().to_string();
1177 let _ = self.sender.send(theme);
1178 info!(theme.from = %from, theme.to = %to, "Theme switched (async)");
1179 }
1180
1181 pub fn set_preset(&self, preset: ThemePreset) {
1183 self.set_theme(preset.to_theme());
1184 }
1185
1186 pub fn subscribe(&self) -> tokio::sync::watch::Receiver<Theme> {
1188 self.receiver.clone()
1189 }
1190
1191 pub async fn changed(&mut self) -> Result<(), tokio::sync::watch::error::RecvError> {
1197 self.receiver.changed().await
1198 }
1199}
1200
1201impl Theme {
1202 pub fn new(name: impl Into<String>, is_dark: bool, colors: ThemeColors) -> Self {
1204 let meta = ThemeMeta {
1205 variant: Some(if is_dark {
1206 ThemeVariant::Dark
1207 } else {
1208 ThemeVariant::Light
1209 }),
1210 ..ThemeMeta::default()
1211 };
1212 Self {
1213 name: name.into(),
1214 is_dark,
1215 colors,
1216 description: None,
1217 author: None,
1218 meta,
1219 }
1220 }
1221
1222 pub fn name(&self) -> &str {
1224 &self.name
1225 }
1226
1227 pub fn is_dark(&self) -> bool {
1229 self.is_dark
1230 }
1231
1232 pub fn description(&self) -> Option<&str> {
1234 self.description.as_deref()
1235 }
1236
1237 pub fn author(&self) -> Option<&str> {
1239 self.author.as_deref()
1240 }
1241
1242 pub fn meta(&self) -> &ThemeMeta {
1244 &self.meta
1245 }
1246
1247 pub fn meta_mut(&mut self) -> &mut ThemeMeta {
1249 &mut self.meta
1250 }
1251
1252 pub fn colors(&self) -> &ThemeColors {
1254 &self.colors
1255 }
1256
1257 pub fn colors_mut(&mut self) -> &mut ThemeColors {
1259 &mut self.colors
1260 }
1261
1262 pub fn get(&self, slot: ColorSlot) -> Color {
1264 self.colors.get(slot).clone()
1265 }
1266
1267 pub fn style(&self) -> Style {
1272 Style::new()
1273 }
1274
1275 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1281 self.name = name.into();
1282 self
1283 }
1284
1285 pub fn with_dark(mut self, is_dark: bool) -> Self {
1287 self.is_dark = is_dark;
1288 self.meta.variant = Some(if is_dark {
1289 ThemeVariant::Dark
1290 } else {
1291 ThemeVariant::Light
1292 });
1293 self
1294 }
1295
1296 pub fn with_colors(mut self, colors: ThemeColors) -> Self {
1298 self.colors = colors;
1299 self
1300 }
1301
1302 pub fn with_description(mut self, description: impl Into<String>) -> Self {
1304 self.description = Some(description.into());
1305 self
1306 }
1307
1308 pub fn with_author(mut self, author: impl Into<String>) -> Self {
1310 self.author = Some(author.into());
1311 self
1312 }
1313
1314 pub fn with_meta(mut self, meta: ThemeMeta) -> Self {
1316 self.meta = meta;
1317 self
1318 }
1319
1320 pub fn validate(&self) -> Result<(), ThemeValidationError> {
1325 self.colors.validate()?;
1326 let _ = self.check_contrast_aa(ColorSlot::Foreground, ColorSlot::Background);
1327 Ok(())
1328 }
1329
1330 pub fn contrast_ratio(&self, fg: ColorSlot, bg: ColorSlot) -> f64 {
1332 let fg_lum = self.get(fg).relative_luminance();
1333 let bg_lum = self.get(bg).relative_luminance();
1334 let lighter = fg_lum.max(bg_lum);
1335 let darker = fg_lum.min(bg_lum);
1336 (lighter + 0.05) / (darker + 0.05)
1337 }
1338
1339 pub fn check_contrast_aa(&self, fg: ColorSlot, bg: ColorSlot) -> bool {
1341 let ratio = self.contrast_ratio(fg, bg);
1342 let ok = ratio >= 4.5;
1343 if !ok {
1344 warn!(
1345 theme.contrast_ratio = ratio,
1346 theme.fg = ?fg,
1347 theme.bg = ?bg,
1348 theme.name = %self.name(),
1349 "Theme contrast below WCAG AA"
1350 );
1351 }
1352 ok
1353 }
1354
1355 pub fn check_contrast_aaa(&self, fg: ColorSlot, bg: ColorSlot) -> bool {
1357 self.contrast_ratio(fg, bg) >= 7.0
1358 }
1359
1360 fn normalize(&mut self) {
1361 if let Some(variant) = self.meta.variant {
1362 self.is_dark = matches!(variant, ThemeVariant::Dark);
1363 } else {
1364 self.meta.variant = Some(if self.is_dark {
1365 ThemeVariant::Dark
1366 } else {
1367 ThemeVariant::Light
1368 });
1369 }
1370 }
1371
1372 pub fn from_json(json: &str) -> Result<Self, ThemeLoadError> {
1377 let mut theme: Theme = serde_json::from_str(json)?;
1378 theme.normalize();
1379 theme.validate()?;
1380 Ok(theme)
1381 }
1382
1383 pub fn from_toml(toml: &str) -> Result<Self, ThemeLoadError> {
1388 let mut theme: Theme = toml::from_str(toml)?;
1389 theme.normalize();
1390 theme.validate()?;
1391 Ok(theme)
1392 }
1393
1394 #[cfg(feature = "yaml")]
1399 pub fn from_yaml(yaml: &str) -> Result<Self, ThemeLoadError> {
1400 let mut theme: Theme = serde_yaml::from_str(yaml)?;
1401 theme.normalize();
1402 theme.validate()?;
1403 Ok(theme)
1404 }
1405
1406 #[cfg(feature = "native")]
1414 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ThemeLoadError> {
1415 let path = path.as_ref();
1416 let content = fs::read_to_string(path)?;
1417 match path.extension().and_then(|e| e.to_str()) {
1418 Some("json") => Self::from_json(&content),
1419 Some("toml") => Self::from_toml(&content),
1420 Some("yaml" | "yml") => {
1421 #[cfg(feature = "yaml")]
1422 {
1423 Self::from_yaml(&content)
1424 }
1425 #[cfg(not(feature = "yaml"))]
1426 {
1427 Err(ThemeLoadError::UnsupportedFormat("yaml".into()))
1428 }
1429 }
1430 Some(ext) => Err(ThemeLoadError::UnsupportedFormat(ext.into())),
1431 None => Err(ThemeLoadError::UnsupportedFormat("unknown".into())),
1432 }
1433 }
1434
1435 pub fn to_json(&self) -> Result<String, ThemeSaveError> {
1440 serde_json::to_string_pretty(self).map_err(ThemeSaveError::Json)
1441 }
1442
1443 pub fn to_toml(&self) -> Result<String, ThemeSaveError> {
1448 toml::to_string_pretty(self).map_err(ThemeSaveError::Toml)
1449 }
1450
1451 #[cfg(feature = "yaml")]
1456 pub fn to_yaml(&self) -> Result<String, ThemeSaveError> {
1457 serde_yaml::to_string(self).map_err(ThemeSaveError::Yaml)
1458 }
1459
1460 #[cfg(feature = "native")]
1468 pub fn to_file(&self, path: impl AsRef<Path>) -> Result<(), ThemeSaveError> {
1469 let path = path.as_ref();
1470 let content = match path.extension().and_then(|e| e.to_str()) {
1471 Some("json") | None => self.to_json()?,
1472 Some("toml") => self.to_toml()?,
1473 Some("yaml" | "yml") => {
1474 #[cfg(feature = "yaml")]
1475 {
1476 self.to_yaml()?
1477 }
1478 #[cfg(not(feature = "yaml"))]
1479 {
1480 return Err(ThemeSaveError::UnsupportedFormat("yaml".into()));
1481 }
1482 }
1483 Some(ext) => return Err(ThemeSaveError::UnsupportedFormat(ext.into())),
1484 };
1485
1486 fs::write(path, content).map_err(ThemeSaveError::Io)
1487 }
1488
1489 pub fn dark() -> Self {
1497 Self::new("Dark", true, ThemeColors::dark())
1498 }
1499
1500 pub fn light() -> Self {
1504 Self::new("Light", false, ThemeColors::light())
1505 }
1506
1507 pub fn dracula() -> Self {
1512 Self::new("Dracula", true, ThemeColors::dracula())
1513 }
1514
1515 pub fn nord() -> Self {
1520 Self::new("Nord", true, ThemeColors::nord())
1521 }
1522
1523 pub fn catppuccin(flavor: CatppuccinFlavor) -> Self {
1528 match flavor {
1529 CatppuccinFlavor::Latte => {
1530 Self::new("Catppuccin Latte", false, ThemeColors::catppuccin_latte())
1531 }
1532 CatppuccinFlavor::Frappe => {
1533 Self::new("Catppuccin Frappe", true, ThemeColors::catppuccin_frappe())
1534 }
1535 CatppuccinFlavor::Macchiato => Self::new(
1536 "Catppuccin Macchiato",
1537 true,
1538 ThemeColors::catppuccin_macchiato(),
1539 ),
1540 CatppuccinFlavor::Mocha => {
1541 Self::new("Catppuccin Mocha", true, ThemeColors::catppuccin_mocha())
1542 }
1543 }
1544 }
1545
1546 pub fn catppuccin_latte() -> Self {
1548 Self::catppuccin(CatppuccinFlavor::Latte)
1549 }
1550
1551 pub fn catppuccin_frappe() -> Self {
1553 Self::catppuccin(CatppuccinFlavor::Frappe)
1554 }
1555
1556 pub fn catppuccin_macchiato() -> Self {
1558 Self::catppuccin(CatppuccinFlavor::Macchiato)
1559 }
1560
1561 pub fn catppuccin_mocha() -> Self {
1563 Self::catppuccin(CatppuccinFlavor::Mocha)
1564 }
1565}
1566
1567impl Default for Theme {
1568 fn default() -> Self {
1569 Self::dark()
1570 }
1571}
1572
1573impl ThemePreset {
1574 pub fn to_theme(&self) -> Theme {
1576 let theme = match *self {
1577 ThemePreset::Dark => Theme::dark(),
1578 ThemePreset::Light => Theme::light(),
1579 ThemePreset::Dracula => Theme::dracula(),
1580 ThemePreset::Nord => Theme::nord(),
1581 ThemePreset::Catppuccin(flavor) => Theme::catppuccin(flavor),
1582 };
1583
1584 info!(theme.preset = %self, theme.name = %theme.name(), "Loaded theme preset");
1585 theme
1586 }
1587}
1588
1589impl ThemeColors {
1590 pub fn get(&self, slot: ColorSlot) -> &Color {
1592 let color = match slot {
1593 ColorSlot::Primary => &self.primary,
1594 ColorSlot::Secondary => &self.secondary,
1595 ColorSlot::Accent => &self.accent,
1596 ColorSlot::Background => &self.background,
1597 ColorSlot::Foreground | ColorSlot::Text => &self.text,
1598 ColorSlot::TextMuted => &self.text_muted,
1599 ColorSlot::TextDisabled => &self.text_disabled,
1600 ColorSlot::Surface => &self.surface,
1601 ColorSlot::SurfaceAlt => &self.surface_alt,
1602 ColorSlot::Success => &self.success,
1603 ColorSlot::Warning => &self.warning,
1604 ColorSlot::Error => &self.error,
1605 ColorSlot::Info => &self.info,
1606 ColorSlot::Border => &self.border,
1607 ColorSlot::BorderMuted => &self.border_muted,
1608 ColorSlot::Separator => &self.separator,
1609 ColorSlot::Focus => &self.focus,
1610 ColorSlot::Selection => &self.selection,
1611 ColorSlot::Hover => &self.hover,
1612 ColorSlot::CodeKeyword => &self.code_keyword,
1613 ColorSlot::CodeString => &self.code_string,
1614 ColorSlot::CodeNumber => &self.code_number,
1615 ColorSlot::CodeComment => &self.code_comment,
1616 ColorSlot::CodeFunction => &self.code_function,
1617 ColorSlot::CodeType => &self.code_type,
1618 ColorSlot::CodeVariable => &self.code_variable,
1619 ColorSlot::CodeOperator => &self.code_operator,
1620 };
1621
1622 debug!(theme.slot = ?slot, theme.value = %color.0, "Theme color lookup");
1623 color
1624 }
1625
1626 pub fn custom(&self) -> &HashMap<String, Color> {
1628 &self.custom
1629 }
1630
1631 pub fn custom_mut(&mut self) -> &mut HashMap<String, Color> {
1633 &mut self.custom
1634 }
1635
1636 pub fn get_custom(&self, name: &str) -> Option<&Color> {
1638 self.custom.get(name)
1639 }
1640
1641 pub fn validate(&self) -> Result<(), ThemeValidationError> {
1646 fn validate_color(slot: &'static str, color: &Color) -> Result<(), ThemeValidationError> {
1647 if color.0.trim().is_empty() {
1648 return Err(ThemeValidationError::EmptyColor(slot));
1649 }
1650 if !color.is_valid() {
1651 return Err(ThemeValidationError::InvalidColor {
1652 slot,
1653 value: color.0.clone(),
1654 });
1655 }
1656 Ok(())
1657 }
1658
1659 validate_color("primary", &self.primary)?;
1660 validate_color("secondary", &self.secondary)?;
1661 validate_color("accent", &self.accent)?;
1662 validate_color("background", &self.background)?;
1663 validate_color("surface", &self.surface)?;
1664 validate_color("surface_alt", &self.surface_alt)?;
1665 validate_color("text", &self.text)?;
1666 validate_color("text_muted", &self.text_muted)?;
1667 validate_color("text_disabled", &self.text_disabled)?;
1668 validate_color("success", &self.success)?;
1669 validate_color("warning", &self.warning)?;
1670 validate_color("error", &self.error)?;
1671 validate_color("info", &self.info)?;
1672 validate_color("border", &self.border)?;
1673 validate_color("border_muted", &self.border_muted)?;
1674 validate_color("separator", &self.separator)?;
1675 validate_color("focus", &self.focus)?;
1676 validate_color("selection", &self.selection)?;
1677 validate_color("hover", &self.hover)?;
1678 validate_color("code_keyword", &self.code_keyword)?;
1679 validate_color("code_string", &self.code_string)?;
1680 validate_color("code_number", &self.code_number)?;
1681 validate_color("code_comment", &self.code_comment)?;
1682 validate_color("code_function", &self.code_function)?;
1683 validate_color("code_type", &self.code_type)?;
1684 validate_color("code_variable", &self.code_variable)?;
1685 validate_color("code_operator", &self.code_operator)?;
1686
1687 for (name, color) in &self.custom {
1688 if name.trim().is_empty() {
1689 return Err(ThemeValidationError::InvalidCustomName);
1690 }
1691 if !color.is_valid() {
1692 return Err(ThemeValidationError::InvalidCustomColor {
1693 name: name.clone(),
1694 value: color.0.clone(),
1695 });
1696 }
1697 }
1698
1699 Ok(())
1700 }
1701
1702 pub fn uniform(color: impl Into<Color>) -> Self {
1706 let c = color.into();
1707 Self {
1708 primary: c.clone(),
1709 secondary: c.clone(),
1710 accent: c.clone(),
1711 background: c.clone(),
1712 surface: c.clone(),
1713 surface_alt: c.clone(),
1714 text: c.clone(),
1715 text_muted: c.clone(),
1716 text_disabled: c.clone(),
1717 success: c.clone(),
1718 warning: c.clone(),
1719 error: c.clone(),
1720 info: c.clone(),
1721 border: c.clone(),
1722 border_muted: c.clone(),
1723 separator: c.clone(),
1724 focus: c.clone(),
1725 selection: c.clone(),
1726 hover: c.clone(),
1727 code_keyword: c.clone(),
1728 code_string: c.clone(),
1729 code_number: c.clone(),
1730 code_comment: c.clone(),
1731 code_function: c.clone(),
1732 code_type: c.clone(),
1733 code_variable: c.clone(),
1734 code_operator: c,
1735 custom: HashMap::new(),
1736 }
1737 }
1738
1739 pub fn dark() -> Self {
1741 Self {
1742 primary: Color::from("#7c3aed"), secondary: Color::from("#6366f1"), accent: Color::from("#22d3ee"), background: Color::from("#0f0f0f"), surface: Color::from("#1a1a1a"), surface_alt: Color::from("#262626"), text: Color::from("#fafafa"), text_muted: Color::from("#a1a1aa"), text_disabled: Color::from("#52525b"), success: Color::from("#22c55e"), warning: Color::from("#f59e0b"), error: Color::from("#ef4444"), info: Color::from("#3b82f6"), border: Color::from("#3f3f46"), border_muted: Color::from("#27272a"), separator: Color::from("#27272a"), focus: Color::from("#7c3aed"), selection: Color::from("#4c1d95"), hover: Color::from("#27272a"), code_keyword: Color::from("#c678dd"), code_string: Color::from("#98c379"), code_number: Color::from("#d19a66"), code_comment: Color::from("#5c6370"), code_function: Color::from("#61afef"), code_type: Color::from("#e5c07b"), code_variable: Color::from("#e06c75"), code_operator: Color::from("#56b6c2"), custom: HashMap::new(),
1783 }
1784 }
1785
1786 pub fn light() -> Self {
1788 Self {
1789 primary: Color::from("#7c3aed"), secondary: Color::from("#4f46e5"), accent: Color::from("#0891b2"), background: Color::from("#ffffff"), surface: Color::from("#f4f4f5"), surface_alt: Color::from("#e4e4e7"), text: Color::from("#18181b"), text_muted: Color::from("#71717a"), text_disabled: Color::from("#a1a1aa"), success: Color::from("#16a34a"), warning: Color::from("#d97706"), error: Color::from("#dc2626"), info: Color::from("#2563eb"), border: Color::from("#d4d4d8"), border_muted: Color::from("#e4e4e7"), separator: Color::from("#e4e4e7"), focus: Color::from("#7c3aed"), selection: Color::from("#ddd6fe"), hover: Color::from("#f4f4f5"), code_keyword: Color::from("#a626a4"), code_string: Color::from("#50a14f"), code_number: Color::from("#986801"), code_comment: Color::from("#a0a1a7"), code_function: Color::from("#4078f2"), code_type: Color::from("#c18401"), code_variable: Color::from("#e45649"), code_operator: Color::from("#0184bc"), custom: HashMap::new(),
1830 }
1831 }
1832
1833 pub fn dracula() -> Self {
1835 Self {
1837 primary: Color::from("#bd93f9"), secondary: Color::from("#ff79c6"), accent: Color::from("#8be9fd"), background: Color::from("#282a36"), surface: Color::from("#44475a"), surface_alt: Color::from("#6272a4"), text: Color::from("#f8f8f2"), text_muted: Color::from("#6272a4"), text_disabled: Color::from("#44475a"), success: Color::from("#50fa7b"), warning: Color::from("#ffb86c"), error: Color::from("#ff5555"), info: Color::from("#8be9fd"), border: Color::from("#44475a"), border_muted: Color::from("#282a36"), separator: Color::from("#44475a"), focus: Color::from("#bd93f9"), selection: Color::from("#44475a"), hover: Color::from("#44475a"), code_keyword: Color::from("#ff79c6"), code_string: Color::from("#f1fa8c"), code_number: Color::from("#bd93f9"), code_comment: Color::from("#6272a4"), code_function: Color::from("#50fa7b"), code_type: Color::from("#8be9fd"), code_variable: Color::from("#f8f8f2"), code_operator: Color::from("#ff79c6"), custom: HashMap::new(),
1871 }
1872 }
1873
1874 pub fn nord() -> Self {
1876 Self {
1878 primary: Color::from("#88c0d0"), secondary: Color::from("#81a1c1"), accent: Color::from("#b48ead"), background: Color::from("#2e3440"), surface: Color::from("#3b4252"), surface_alt: Color::from("#434c5e"), text: Color::from("#eceff4"), text_muted: Color::from("#d8dee9"), text_disabled: Color::from("#4c566a"), success: Color::from("#a3be8c"), warning: Color::from("#ebcb8b"), error: Color::from("#bf616a"), info: Color::from("#5e81ac"), border: Color::from("#4c566a"), border_muted: Color::from("#3b4252"), separator: Color::from("#3b4252"), focus: Color::from("#88c0d0"), selection: Color::from("#434c5e"), hover: Color::from("#3b4252"), code_keyword: Color::from("#81a1c1"), code_string: Color::from("#a3be8c"), code_number: Color::from("#b48ead"), code_comment: Color::from("#616e88"), code_function: Color::from("#88c0d0"), code_type: Color::from("#8fbcbb"), code_variable: Color::from("#d8dee9"), code_operator: Color::from("#81a1c1"), custom: HashMap::new(),
1912 }
1913 }
1914
1915 pub fn catppuccin_mocha() -> Self {
1917 Self {
1919 primary: Color::from("#cba6f7"), secondary: Color::from("#89b4fa"), accent: Color::from("#f5c2e7"), background: Color::from("#1e1e2e"), surface: Color::from("#313244"), surface_alt: Color::from("#45475a"), text: Color::from("#cdd6f4"), text_muted: Color::from("#a6adc8"), text_disabled: Color::from("#6c7086"), success: Color::from("#a6e3a1"), warning: Color::from("#f9e2af"), error: Color::from("#f38ba8"), info: Color::from("#89dceb"), border: Color::from("#45475a"), border_muted: Color::from("#313244"), separator: Color::from("#313244"), focus: Color::from("#cba6f7"), selection: Color::from("#45475a"), hover: Color::from("#313244"), code_keyword: Color::from("#cba6f7"), code_string: Color::from("#a6e3a1"), code_number: Color::from("#fab387"), code_comment: Color::from("#6c7086"), code_function: Color::from("#89b4fa"), code_type: Color::from("#f9e2af"), code_variable: Color::from("#f5c2e7"), code_operator: Color::from("#89dceb"), custom: HashMap::new(),
1953 }
1954 }
1955
1956 pub fn catppuccin_latte() -> Self {
1958 Self {
1960 primary: Color::from("#8839ef"), secondary: Color::from("#1e66f5"), accent: Color::from("#ea76cb"), background: Color::from("#eff1f5"), surface: Color::from("#ccd0da"), surface_alt: Color::from("#bcc0cc"), text: Color::from("#4c4f69"), text_muted: Color::from("#6c6f85"), text_disabled: Color::from("#9ca0b0"), success: Color::from("#40a02b"), warning: Color::from("#df8e1d"), error: Color::from("#d20f39"), info: Color::from("#04a5e5"), border: Color::from("#bcc0cc"), border_muted: Color::from("#ccd0da"), separator: Color::from("#ccd0da"), focus: Color::from("#8839ef"), selection: Color::from("#bcc0cc"), hover: Color::from("#ccd0da"), code_keyword: Color::from("#8839ef"), code_string: Color::from("#40a02b"), code_number: Color::from("#fe640b"), code_comment: Color::from("#9ca0b0"), code_function: Color::from("#1e66f5"), code_type: Color::from("#df8e1d"), code_variable: Color::from("#ea76cb"), code_operator: Color::from("#04a5e5"), custom: HashMap::new(),
1994 }
1995 }
1996
1997 pub fn catppuccin_frappe() -> Self {
1999 Self {
2001 primary: Color::from("#ca9ee6"), secondary: Color::from("#8caaee"), accent: Color::from("#f4b8e4"), background: Color::from("#303446"), surface: Color::from("#414559"), surface_alt: Color::from("#51576d"), text: Color::from("#c6d0f5"), text_muted: Color::from("#a5adce"), text_disabled: Color::from("#737994"), success: Color::from("#a6d189"), warning: Color::from("#e5c890"), error: Color::from("#e78284"), info: Color::from("#99d1db"), border: Color::from("#51576d"), border_muted: Color::from("#414559"), separator: Color::from("#414559"), focus: Color::from("#ca9ee6"), selection: Color::from("#51576d"), hover: Color::from("#414559"), code_keyword: Color::from("#ca9ee6"), code_string: Color::from("#a6d189"), code_number: Color::from("#ef9f76"), code_comment: Color::from("#737994"), code_function: Color::from("#8caaee"), code_type: Color::from("#e5c890"), code_variable: Color::from("#f4b8e4"), code_operator: Color::from("#99d1db"), custom: HashMap::new(),
2035 }
2036 }
2037
2038 pub fn catppuccin_macchiato() -> Self {
2040 Self {
2042 primary: Color::from("#c6a0f6"), secondary: Color::from("#8aadf4"), accent: Color::from("#f5bde6"), background: Color::from("#24273a"), surface: Color::from("#363a4f"), surface_alt: Color::from("#494d64"), text: Color::from("#cad3f5"), text_muted: Color::from("#a5adcb"), text_disabled: Color::from("#6e738d"), success: Color::from("#a6da95"), warning: Color::from("#eed49f"), error: Color::from("#ed8796"), info: Color::from("#91d7e3"), border: Color::from("#494d64"), border_muted: Color::from("#363a4f"), separator: Color::from("#363a4f"), focus: Color::from("#c6a0f6"), selection: Color::from("#494d64"), hover: Color::from("#363a4f"), code_keyword: Color::from("#c6a0f6"), code_string: Color::from("#a6da95"), code_number: Color::from("#f5a97f"), code_comment: Color::from("#6e738d"), code_function: Color::from("#8aadf4"), code_type: Color::from("#eed49f"), code_variable: Color::from("#f5bde6"), code_operator: Color::from("#91d7e3"), custom: HashMap::new(),
2076 }
2077 }
2078}
2079
2080impl Default for ThemeColors {
2081 fn default() -> Self {
2082 Self::dark()
2083 }
2084}
2085
2086pub fn adaptive(
2091 light: &ThemeColors,
2092 dark: &ThemeColors,
2093 slot: impl Fn(&ThemeColors) -> &Color,
2094) -> AdaptiveColor {
2095 AdaptiveColor {
2096 light: slot(light).clone(),
2097 dark: slot(dark).clone(),
2098 }
2099}
2100
2101#[derive(Error, Debug)]
2103pub enum ThemeValidationError {
2104 #[error("Color slot '{0}' is empty")]
2105 EmptyColor(&'static str),
2106 #[error("Invalid color value '{value}' for slot '{slot}'")]
2107 InvalidColor { slot: &'static str, value: String },
2108 #[error("Custom color name cannot be empty")]
2109 InvalidCustomName,
2110 #[error("Invalid custom color '{value}' for '{name}'")]
2111 InvalidCustomColor { name: String, value: String },
2112}
2113
2114#[derive(Error, Debug)]
2116pub enum ThemeLoadError {
2117 #[error("JSON error: {0}")]
2118 Json(#[from] serde_json::Error),
2119 #[error("TOML error: {0}")]
2120 Toml(#[from] toml::de::Error),
2121 #[cfg(feature = "yaml")]
2122 #[error("YAML error: {0}")]
2123 Yaml(#[from] serde_yaml::Error),
2124 #[error("IO error: {0}")]
2125 Io(#[from] std::io::Error),
2126 #[error("Unsupported format: {0}")]
2127 UnsupportedFormat(String),
2128 #[error("Validation error: {0}")]
2129 Validation(#[from] ThemeValidationError),
2130}
2131
2132#[derive(Error, Debug)]
2134pub enum ThemeSaveError {
2135 #[error("JSON error: {0}")]
2136 Json(#[from] serde_json::Error),
2137 #[error("TOML error: {0}")]
2138 Toml(#[from] toml::ser::Error),
2139 #[cfg(feature = "yaml")]
2140 #[error("YAML error: {0}")]
2141 Yaml(#[from] serde_yaml::Error),
2142 #[error("IO error: {0}")]
2143 Io(#[from] std::io::Error),
2144 #[error("Unsupported format: {0}")]
2145 UnsupportedFormat(String),
2146}
2147
2148#[cfg(test)]
2149mod tests {
2150 use super::*;
2151 use crate::renderer::Renderer;
2152 use std::sync::Arc;
2153 use std::sync::atomic::{AtomicUsize, Ordering};
2154
2155 #[test]
2156 fn test_theme_dark_default() {
2157 let theme = Theme::dark();
2158 assert!(theme.is_dark());
2159 assert_eq!(theme.name(), "Dark");
2160 }
2161
2162 #[test]
2163 fn test_theme_light_default() {
2164 let theme = Theme::light();
2165 assert!(!theme.is_dark());
2166 assert_eq!(theme.name(), "Light");
2167 }
2168
2169 #[test]
2170 fn test_theme_dracula() {
2171 let theme = Theme::dracula();
2172 assert!(theme.is_dark());
2173 assert_eq!(theme.name(), "Dracula");
2174 assert_eq!(theme.colors().background.0, "#282a36");
2176 }
2177
2178 #[test]
2179 fn test_theme_nord() {
2180 let theme = Theme::nord();
2181 assert!(theme.is_dark());
2182 assert_eq!(theme.name(), "Nord");
2183 assert_eq!(theme.colors().background.0, "#2e3440");
2185 }
2186
2187 #[test]
2188 fn test_theme_catppuccin() {
2189 let theme = Theme::catppuccin_mocha();
2190 assert!(theme.is_dark());
2191 assert_eq!(theme.name(), "Catppuccin Mocha");
2192 assert_eq!(theme.colors().background.0, "#1e1e2e");
2194 }
2195
2196 #[test]
2197 fn test_theme_catppuccin_latte() {
2198 let theme = Theme::catppuccin_latte();
2199 assert!(!theme.is_dark());
2200 assert_eq!(theme.name(), "Catppuccin Latte");
2201 assert_eq!(theme.colors().background.0, "#eff1f5");
2202 assert_eq!(theme.colors().primary.0, "#8839ef");
2203 }
2204
2205 #[test]
2206 fn test_theme_catppuccin_frappe() {
2207 let theme = Theme::catppuccin_frappe();
2208 assert!(theme.is_dark());
2209 assert_eq!(theme.name(), "Catppuccin Frappe");
2210 assert_eq!(theme.colors().background.0, "#303446");
2211 assert_eq!(theme.colors().primary.0, "#ca9ee6");
2212 }
2213
2214 #[test]
2215 fn test_theme_catppuccin_macchiato() {
2216 let theme = Theme::catppuccin_macchiato();
2217 assert!(theme.is_dark());
2218 assert_eq!(theme.name(), "Catppuccin Macchiato");
2219 assert_eq!(theme.colors().background.0, "#24273a");
2220 assert_eq!(theme.colors().primary.0, "#c6a0f6");
2221 }
2222
2223 #[test]
2224 fn test_theme_preset_to_theme() {
2225 let theme = ThemePreset::Catppuccin(CatppuccinFlavor::Latte).to_theme();
2226 assert_eq!(theme.name(), "Catppuccin Latte");
2227 assert!(!theme.is_dark());
2228 }
2229
2230 #[test]
2231 fn test_theme_contrast_aa() {
2232 let theme = Theme::dark();
2233 assert!(theme.check_contrast_aa(ColorSlot::Foreground, ColorSlot::Background));
2234 }
2235
2236 #[test]
2237 fn test_theme_context_switch() {
2238 let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2239 assert_eq!(ctx.current().name(), "Dark");
2240 ctx.set_preset(ThemePreset::Light);
2241 assert_eq!(ctx.current().name(), "Light");
2242 }
2243
2244 #[test]
2245 fn test_theme_context_listener() {
2246 let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2247 let hits = Arc::new(AtomicUsize::new(0));
2248 let hits_ref = Arc::clone(&hits);
2249 let id = ctx.on_change(move |_theme| {
2250 hits_ref.fetch_add(1, Ordering::SeqCst);
2251 });
2252
2253 ctx.set_preset(ThemePreset::Light);
2254 assert_eq!(hits.load(Ordering::SeqCst), 1);
2255
2256 ctx.remove_listener(id);
2257 ctx.set_preset(ThemePreset::Dark);
2258 assert_eq!(hits.load(Ordering::SeqCst), 1);
2259 }
2260
2261 #[test]
2262 fn test_theme_context_thread_safe() {
2263 use std::thread;
2264
2265 let ctx = Arc::new(ThemeContext::from_preset(ThemePreset::Dark));
2266 let handles: Vec<_> = (0..8)
2267 .map(|i| {
2268 let ctx = Arc::clone(&ctx);
2269 thread::spawn(move || {
2270 if i % 2 == 0 {
2271 ctx.set_preset(ThemePreset::Light);
2272 } else {
2273 ctx.set_preset(ThemePreset::Dark);
2274 }
2275 let _current = ctx.current();
2276 })
2277 })
2278 .collect();
2279
2280 for handle in handles {
2281 handle.join().expect("thread join");
2282 }
2283 }
2284
2285 #[test]
2286 fn test_theme_context_recovers_from_poisoned_current_lock() {
2287 let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2288 let current = Arc::clone(&ctx.current);
2289
2290 let poison_result = std::thread::spawn(move || {
2291 let _guard = current.write().expect("write lock should be acquired");
2292 std::panic::resume_unwind(Box::new("poison current lock"));
2293 })
2294 .join();
2295 assert!(poison_result.is_err(), "poisoning thread should panic");
2296
2297 assert_eq!(ctx.current().name(), "Dark");
2299 ctx.set_preset(ThemePreset::Light);
2300 assert_eq!(ctx.current().name(), "Light");
2301 }
2302
2303 #[test]
2304 fn test_theme_context_recovers_from_poisoned_listeners_lock() {
2305 let ctx = ThemeContext::from_preset(ThemePreset::Dark);
2306 let listeners = Arc::clone(&ctx.listeners);
2307
2308 let poison_result = std::thread::spawn(move || {
2309 let _guard = listeners.write().expect("write lock should be acquired");
2310 std::panic::resume_unwind(Box::new("poison listeners lock"));
2311 })
2312 .join();
2313 assert!(poison_result.is_err(), "poisoning thread should panic");
2314
2315 let hits = Arc::new(AtomicUsize::new(0));
2316 let hits_ref = Arc::clone(&hits);
2317 let id = ctx.on_change(move |_theme| {
2318 hits_ref.fetch_add(1, Ordering::SeqCst);
2319 });
2320
2321 ctx.set_preset(ThemePreset::Light);
2323 assert_eq!(hits.load(Ordering::SeqCst), 1);
2324 ctx.remove_listener(id);
2325 }
2326
2327 #[test]
2328 fn test_theme_builder() {
2329 let theme = Theme::dark().with_name("Custom Dark").with_dark(true);
2330 assert_eq!(theme.name(), "Custom Dark");
2331 assert!(theme.is_dark());
2332 }
2333
2334 #[test]
2335 fn test_theme_colors_uniform() {
2336 let colors = ThemeColors::uniform("#ff0000");
2337 assert_eq!(colors.primary.0, "#ff0000");
2338 assert_eq!(colors.background.0, "#ff0000");
2339 assert_eq!(colors.text.0, "#ff0000");
2340 }
2341
2342 #[test]
2343 fn test_adaptive_color() {
2344 let light = ThemeColors::light();
2345 let dark = ThemeColors::dark();
2346
2347 let adaptive_text = adaptive(&light, &dark, |c| &c.text);
2348
2349 assert_eq!(adaptive_text.light.0, light.text.0);
2351 assert_eq!(adaptive_text.dark.0, dark.text.0);
2352 }
2353
2354 #[test]
2355 fn test_theme_style() {
2356 let theme = Theme::dark();
2357 let style = theme.style();
2358 assert!(style.value().is_empty());
2360 }
2361
2362 #[test]
2363 fn test_theme_get_slot() {
2364 let theme = Theme::dark();
2365 assert_eq!(theme.get(ColorSlot::Primary).0, theme.colors().primary.0);
2366 assert_eq!(
2367 theme.get(ColorSlot::TextMuted).0,
2368 theme.colors().text_muted.0
2369 );
2370 assert_eq!(theme.get(ColorSlot::Foreground).0, theme.colors().text.0);
2371 assert_eq!(theme.get(ColorSlot::Text).0, theme.colors().text.0);
2372 }
2373
2374 #[test]
2375 fn test_theme_json_roundtrip() {
2376 let theme = Theme::dark()
2377 .with_description("A dark theme")
2378 .with_author("charmed_rust");
2379 let json = theme.to_json().expect("serialize theme");
2380 let loaded = Theme::from_json(&json).expect("deserialize theme");
2381 assert_eq!(loaded.colors().primary.0, theme.colors().primary.0);
2382 assert_eq!(loaded.description(), Some("A dark theme"));
2383 assert_eq!(loaded.author(), Some("charmed_rust"));
2384 assert!(loaded.is_dark());
2385 }
2386
2387 #[test]
2388 fn test_theme_toml_roundtrip() {
2389 let theme = Theme::dark().with_description("TOML theme");
2390 let toml = theme.to_toml().expect("serialize theme to toml");
2391 let loaded = Theme::from_toml(&toml).expect("deserialize theme from toml");
2392 assert_eq!(loaded.colors().primary.0, theme.colors().primary.0);
2393 assert_eq!(loaded.description(), Some("TOML theme"));
2394 assert!(loaded.is_dark());
2395 }
2396
2397 #[test]
2398 fn test_theme_custom_colors_serde() {
2399 let mut theme = Theme::dark();
2400 theme
2401 .colors_mut()
2402 .custom_mut()
2403 .insert("brand".to_string(), Color::from("#123456"));
2404 let json = theme.to_json().expect("serialize theme");
2405 let loaded = Theme::from_json(&json).expect("deserialize theme");
2406 assert_eq!(
2407 loaded.colors().get_custom("brand").expect("custom color"),
2408 &Color::from("#123456")
2409 );
2410 }
2411
2412 #[test]
2413 fn test_color_slots_all_defined() {
2414 for theme in [
2416 Theme::dark(),
2417 Theme::light(),
2418 Theme::dracula(),
2419 Theme::nord(),
2420 Theme::catppuccin_mocha(),
2421 Theme::catppuccin_latte(),
2422 Theme::catppuccin_frappe(),
2423 Theme::catppuccin_macchiato(),
2424 ] {
2425 let c = theme.colors();
2426
2427 assert!(!c.primary.0.is_empty(), "{}: primary empty", theme.name());
2429 assert!(
2430 !c.secondary.0.is_empty(),
2431 "{}: secondary empty",
2432 theme.name()
2433 );
2434 assert!(!c.accent.0.is_empty(), "{}: accent empty", theme.name());
2435 assert!(
2436 !c.background.0.is_empty(),
2437 "{}: background empty",
2438 theme.name()
2439 );
2440 assert!(!c.surface.0.is_empty(), "{}: surface empty", theme.name());
2441 assert!(!c.text.0.is_empty(), "{}: text empty", theme.name());
2442 assert!(!c.error.0.is_empty(), "{}: error empty", theme.name());
2443 }
2444 }
2445
2446 #[test]
2447 fn test_color_transform_lighten_darken() {
2448 let black = Color::from("#000000");
2449 let lighter = ColorTransform::Lighten(0.2).apply(black);
2450 assert_eq!(lighter.0, "#333333");
2451
2452 let white = Color::from("#ffffff");
2453 let darker = ColorTransform::Darken(0.2).apply(white);
2454 assert_eq!(darker.0, "#cccccc");
2455 }
2456
2457 #[test]
2458 fn test_color_transform_desaturate_and_alpha() {
2459 let red = Color::from("#ff0000");
2460 let gray = ColorTransform::Desaturate(1.0).apply(red);
2461 assert_eq!(gray.0, "#808080");
2462
2463 let white = Color::from("#ffffff");
2464 let alpha = ColorTransform::Alpha(0.5).apply(white);
2465 assert_eq!(alpha.0, "#808080");
2466 }
2467
2468 #[test]
2469 fn test_cached_themed_style_invalidation() {
2470 let ctx = Arc::new(ThemeContext::from_preset(ThemePreset::Dark));
2471 let themed = ThemedStyle::new(Arc::clone(&ctx))
2472 .background(ColorSlot::Background)
2473 .renderer(Arc::new(Renderer::DEFAULT));
2474 let cached = CachedThemedStyle::new(themed);
2475
2476 let first = cached.render("x");
2477 ctx.set_preset(ThemePreset::Light);
2478 let second = cached.render("x");
2479
2480 assert_ne!(first, second);
2481 }
2482}