1#![forbid(unsafe_code)]
2
3use crate::color::Color;
25use std::env;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum AdaptiveColor {
30 Fixed(Color),
32 Adaptive {
34 light: Color,
36 dark: Color,
38 },
39}
40
41impl AdaptiveColor {
42 #[inline]
44 pub const fn fixed(color: Color) -> Self {
45 Self::Fixed(color)
46 }
47
48 #[inline]
50 pub const fn adaptive(light: Color, dark: Color) -> Self {
51 Self::Adaptive { light, dark }
52 }
53
54 #[inline]
59 pub const fn resolve(&self, is_dark: bool) -> Color {
60 match self {
61 Self::Fixed(c) => *c,
62 Self::Adaptive { light, dark } => {
63 if is_dark {
64 *dark
65 } else {
66 *light
67 }
68 }
69 }
70 }
71
72 #[inline]
74 pub const fn is_adaptive(&self) -> bool {
75 matches!(self, Self::Adaptive { .. })
76 }
77}
78
79impl Default for AdaptiveColor {
80 fn default() -> Self {
81 Self::Fixed(Color::rgb(128, 128, 128))
82 }
83}
84
85impl From<Color> for AdaptiveColor {
86 fn from(color: Color) -> Self {
87 Self::Fixed(color)
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Theme {
97 pub primary: AdaptiveColor,
100 pub secondary: AdaptiveColor,
102 pub accent: AdaptiveColor,
104
105 pub background: AdaptiveColor,
108 pub surface: AdaptiveColor,
110 pub overlay: AdaptiveColor,
112
113 pub text: AdaptiveColor,
116 pub text_muted: AdaptiveColor,
118 pub text_subtle: AdaptiveColor,
120
121 pub success: AdaptiveColor,
124 pub warning: AdaptiveColor,
126 pub error: AdaptiveColor,
128 pub info: AdaptiveColor,
130
131 pub border: AdaptiveColor,
134 pub border_focused: AdaptiveColor,
136
137 pub selection_bg: AdaptiveColor,
140 pub selection_fg: AdaptiveColor,
142
143 pub scrollbar_track: AdaptiveColor,
146 pub scrollbar_thumb: AdaptiveColor,
148}
149
150impl Default for Theme {
151 fn default() -> Self {
152 themes::dark()
153 }
154}
155
156impl Theme {
157 pub fn builder() -> ThemeBuilder {
159 ThemeBuilder::new()
160 }
161
162 #[must_use]
171 pub fn detect_dark_mode() -> bool {
172 Self::detect_dark_mode_from_colorfgbg(env::var("COLORFGBG").ok().as_deref())
173 }
174
175 fn detect_dark_mode_from_colorfgbg(colorfgbg: Option<&str>) -> bool {
176 if let Some(colorfgbg) = colorfgbg
179 && let Some(bg_part) = colorfgbg.split(';').next_back()
180 && let Ok(bg) = bg_part.trim().parse::<u8>()
181 {
182 return bg != 7 && bg != 15;
184 }
185
186 true
188 }
189
190 #[must_use]
194 pub fn resolve(&self, is_dark: bool) -> ResolvedTheme {
195 ResolvedTheme {
196 primary: self.primary.resolve(is_dark),
197 secondary: self.secondary.resolve(is_dark),
198 accent: self.accent.resolve(is_dark),
199 background: self.background.resolve(is_dark),
200 surface: self.surface.resolve(is_dark),
201 overlay: self.overlay.resolve(is_dark),
202 text: self.text.resolve(is_dark),
203 text_muted: self.text_muted.resolve(is_dark),
204 text_subtle: self.text_subtle.resolve(is_dark),
205 success: self.success.resolve(is_dark),
206 warning: self.warning.resolve(is_dark),
207 error: self.error.resolve(is_dark),
208 info: self.info.resolve(is_dark),
209 border: self.border.resolve(is_dark),
210 border_focused: self.border_focused.resolve(is_dark),
211 selection_bg: self.selection_bg.resolve(is_dark),
212 selection_fg: self.selection_fg.resolve(is_dark),
213 scrollbar_track: self.scrollbar_track.resolve(is_dark),
214 scrollbar_thumb: self.scrollbar_thumb.resolve(is_dark),
215 }
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct ResolvedTheme {
224 pub primary: Color,
226 pub secondary: Color,
228 pub accent: Color,
230 pub background: Color,
232 pub surface: Color,
234 pub overlay: Color,
236 pub text: Color,
238 pub text_muted: Color,
240 pub text_subtle: Color,
242 pub success: Color,
244 pub warning: Color,
246 pub error: Color,
248 pub info: Color,
250 pub border: Color,
252 pub border_focused: Color,
254 pub selection_bg: Color,
256 pub selection_fg: Color,
258 pub scrollbar_track: Color,
260 pub scrollbar_thumb: Color,
262}
263
264#[derive(Debug, Clone)]
266pub struct ThemeBuilder {
267 theme: Theme,
268}
269
270impl ThemeBuilder {
271 pub fn new() -> Self {
273 Self {
274 theme: themes::dark(),
275 }
276 }
277
278 pub fn from_theme(theme: Theme) -> Self {
280 Self { theme }
281 }
282
283 pub fn primary(mut self, color: impl Into<AdaptiveColor>) -> Self {
285 self.theme.primary = color.into();
286 self
287 }
288
289 pub fn secondary(mut self, color: impl Into<AdaptiveColor>) -> Self {
291 self.theme.secondary = color.into();
292 self
293 }
294
295 pub fn accent(mut self, color: impl Into<AdaptiveColor>) -> Self {
297 self.theme.accent = color.into();
298 self
299 }
300
301 pub fn background(mut self, color: impl Into<AdaptiveColor>) -> Self {
303 self.theme.background = color.into();
304 self
305 }
306
307 pub fn surface(mut self, color: impl Into<AdaptiveColor>) -> Self {
309 self.theme.surface = color.into();
310 self
311 }
312
313 pub fn overlay(mut self, color: impl Into<AdaptiveColor>) -> Self {
315 self.theme.overlay = color.into();
316 self
317 }
318
319 pub fn text(mut self, color: impl Into<AdaptiveColor>) -> Self {
321 self.theme.text = color.into();
322 self
323 }
324
325 pub fn text_muted(mut self, color: impl Into<AdaptiveColor>) -> Self {
327 self.theme.text_muted = color.into();
328 self
329 }
330
331 pub fn text_subtle(mut self, color: impl Into<AdaptiveColor>) -> Self {
333 self.theme.text_subtle = color.into();
334 self
335 }
336
337 pub fn success(mut self, color: impl Into<AdaptiveColor>) -> Self {
339 self.theme.success = color.into();
340 self
341 }
342
343 pub fn warning(mut self, color: impl Into<AdaptiveColor>) -> Self {
345 self.theme.warning = color.into();
346 self
347 }
348
349 pub fn error(mut self, color: impl Into<AdaptiveColor>) -> Self {
351 self.theme.error = color.into();
352 self
353 }
354
355 pub fn info(mut self, color: impl Into<AdaptiveColor>) -> Self {
357 self.theme.info = color.into();
358 self
359 }
360
361 pub fn border(mut self, color: impl Into<AdaptiveColor>) -> Self {
363 self.theme.border = color.into();
364 self
365 }
366
367 pub fn border_focused(mut self, color: impl Into<AdaptiveColor>) -> Self {
369 self.theme.border_focused = color.into();
370 self
371 }
372
373 pub fn selection_bg(mut self, color: impl Into<AdaptiveColor>) -> Self {
375 self.theme.selection_bg = color.into();
376 self
377 }
378
379 pub fn selection_fg(mut self, color: impl Into<AdaptiveColor>) -> Self {
381 self.theme.selection_fg = color.into();
382 self
383 }
384
385 pub fn scrollbar_track(mut self, color: impl Into<AdaptiveColor>) -> Self {
387 self.theme.scrollbar_track = color.into();
388 self
389 }
390
391 pub fn scrollbar_thumb(mut self, color: impl Into<AdaptiveColor>) -> Self {
393 self.theme.scrollbar_thumb = color.into();
394 self
395 }
396
397 pub fn build(self) -> Theme {
399 self.theme
400 }
401}
402
403impl Default for ThemeBuilder {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409pub mod themes {
411 use super::*;
412
413 #[must_use]
415 pub fn default() -> Theme {
416 dark()
417 }
418
419 #[must_use]
421 pub fn dark() -> Theme {
422 Theme {
423 primary: AdaptiveColor::fixed(Color::rgb(88, 166, 255)), secondary: AdaptiveColor::fixed(Color::rgb(163, 113, 247)), accent: AdaptiveColor::fixed(Color::rgb(255, 123, 114)), background: AdaptiveColor::fixed(Color::rgb(22, 27, 34)), surface: AdaptiveColor::fixed(Color::rgb(33, 38, 45)), overlay: AdaptiveColor::fixed(Color::rgb(48, 54, 61)), text: AdaptiveColor::fixed(Color::rgb(230, 237, 243)), text_muted: AdaptiveColor::fixed(Color::rgb(139, 148, 158)), text_subtle: AdaptiveColor::fixed(Color::rgb(110, 118, 129)), success: AdaptiveColor::fixed(Color::rgb(63, 185, 80)), warning: AdaptiveColor::fixed(Color::rgb(210, 153, 34)), error: AdaptiveColor::fixed(Color::rgb(248, 81, 73)), info: AdaptiveColor::fixed(Color::rgb(88, 166, 255)), border: AdaptiveColor::fixed(Color::rgb(48, 54, 61)), border_focused: AdaptiveColor::fixed(Color::rgb(88, 166, 255)), selection_bg: AdaptiveColor::fixed(Color::rgb(56, 139, 253)), selection_fg: AdaptiveColor::fixed(Color::rgb(255, 255, 255)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(33, 38, 45)),
447 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(72, 79, 88)),
448 }
449 }
450
451 #[must_use]
453 pub fn light() -> Theme {
454 Theme {
455 primary: AdaptiveColor::fixed(Color::rgb(9, 105, 218)), secondary: AdaptiveColor::fixed(Color::rgb(130, 80, 223)), accent: AdaptiveColor::fixed(Color::rgb(207, 34, 46)), background: AdaptiveColor::fixed(Color::rgb(255, 255, 255)), surface: AdaptiveColor::fixed(Color::rgb(246, 248, 250)), overlay: AdaptiveColor::fixed(Color::rgb(255, 255, 255)), text: AdaptiveColor::fixed(Color::rgb(31, 35, 40)), text_muted: AdaptiveColor::fixed(Color::rgb(87, 96, 106)), text_subtle: AdaptiveColor::fixed(Color::rgb(140, 149, 159)), success: AdaptiveColor::fixed(Color::rgb(26, 127, 55)), warning: AdaptiveColor::fixed(Color::rgb(158, 106, 3)), error: AdaptiveColor::fixed(Color::rgb(207, 34, 46)), info: AdaptiveColor::fixed(Color::rgb(9, 105, 218)), border: AdaptiveColor::fixed(Color::rgb(208, 215, 222)), border_focused: AdaptiveColor::fixed(Color::rgb(9, 105, 218)), selection_bg: AdaptiveColor::fixed(Color::rgb(221, 244, 255)), selection_fg: AdaptiveColor::fixed(Color::rgb(31, 35, 40)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(246, 248, 250)),
479 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(175, 184, 193)),
480 }
481 }
482
483 #[must_use]
485 pub fn nord() -> Theme {
486 Theme {
487 primary: AdaptiveColor::fixed(Color::rgb(136, 192, 208)), secondary: AdaptiveColor::fixed(Color::rgb(180, 142, 173)), accent: AdaptiveColor::fixed(Color::rgb(191, 97, 106)), background: AdaptiveColor::fixed(Color::rgb(46, 52, 64)), surface: AdaptiveColor::fixed(Color::rgb(59, 66, 82)), overlay: AdaptiveColor::fixed(Color::rgb(67, 76, 94)), text: AdaptiveColor::fixed(Color::rgb(236, 239, 244)), text_muted: AdaptiveColor::fixed(Color::rgb(216, 222, 233)), text_subtle: AdaptiveColor::fixed(Color::rgb(129, 161, 193)), success: AdaptiveColor::fixed(Color::rgb(163, 190, 140)), warning: AdaptiveColor::fixed(Color::rgb(235, 203, 139)), error: AdaptiveColor::fixed(Color::rgb(191, 97, 106)), info: AdaptiveColor::fixed(Color::rgb(129, 161, 193)), border: AdaptiveColor::fixed(Color::rgb(76, 86, 106)), border_focused: AdaptiveColor::fixed(Color::rgb(136, 192, 208)), selection_bg: AdaptiveColor::fixed(Color::rgb(76, 86, 106)), selection_fg: AdaptiveColor::fixed(Color::rgb(236, 239, 244)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(59, 66, 82)),
511 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(76, 86, 106)),
512 }
513 }
514
515 #[must_use]
517 pub fn dracula() -> Theme {
518 Theme {
519 primary: AdaptiveColor::fixed(Color::rgb(189, 147, 249)), secondary: AdaptiveColor::fixed(Color::rgb(255, 121, 198)), accent: AdaptiveColor::fixed(Color::rgb(139, 233, 253)), background: AdaptiveColor::fixed(Color::rgb(40, 42, 54)), surface: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), overlay: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), text: AdaptiveColor::fixed(Color::rgb(248, 248, 242)), text_muted: AdaptiveColor::fixed(Color::rgb(188, 188, 188)), text_subtle: AdaptiveColor::fixed(Color::rgb(98, 114, 164)), success: AdaptiveColor::fixed(Color::rgb(80, 250, 123)), warning: AdaptiveColor::fixed(Color::rgb(255, 184, 108)), error: AdaptiveColor::fixed(Color::rgb(255, 85, 85)), info: AdaptiveColor::fixed(Color::rgb(139, 233, 253)), border: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), border_focused: AdaptiveColor::fixed(Color::rgb(189, 147, 249)), selection_bg: AdaptiveColor::fixed(Color::rgb(68, 71, 90)), selection_fg: AdaptiveColor::fixed(Color::rgb(248, 248, 242)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(40, 42, 54)),
543 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(68, 71, 90)),
544 }
545 }
546
547 #[must_use]
549 pub fn solarized_dark() -> Theme {
550 Theme {
551 primary: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), secondary: AdaptiveColor::fixed(Color::rgb(108, 113, 196)), accent: AdaptiveColor::fixed(Color::rgb(203, 75, 22)), background: AdaptiveColor::fixed(Color::rgb(0, 43, 54)), surface: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), overlay: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), text: AdaptiveColor::fixed(Color::rgb(131, 148, 150)), text_muted: AdaptiveColor::fixed(Color::rgb(101, 123, 131)), text_subtle: AdaptiveColor::fixed(Color::rgb(88, 110, 117)), success: AdaptiveColor::fixed(Color::rgb(133, 153, 0)), warning: AdaptiveColor::fixed(Color::rgb(181, 137, 0)), error: AdaptiveColor::fixed(Color::rgb(220, 50, 47)), info: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), border: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), border_focused: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), selection_bg: AdaptiveColor::fixed(Color::rgb(7, 54, 66)), selection_fg: AdaptiveColor::fixed(Color::rgb(147, 161, 161)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(0, 43, 54)),
575 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(7, 54, 66)),
576 }
577 }
578
579 #[must_use]
581 pub fn solarized_light() -> Theme {
582 Theme {
583 primary: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), secondary: AdaptiveColor::fixed(Color::rgb(108, 113, 196)), accent: AdaptiveColor::fixed(Color::rgb(203, 75, 22)), background: AdaptiveColor::fixed(Color::rgb(253, 246, 227)), surface: AdaptiveColor::fixed(Color::rgb(238, 232, 213)), overlay: AdaptiveColor::fixed(Color::rgb(253, 246, 227)), text: AdaptiveColor::fixed(Color::rgb(101, 123, 131)), text_muted: AdaptiveColor::fixed(Color::rgb(88, 110, 117)), text_subtle: AdaptiveColor::fixed(Color::rgb(147, 161, 161)), success: AdaptiveColor::fixed(Color::rgb(133, 153, 0)), warning: AdaptiveColor::fixed(Color::rgb(181, 137, 0)), error: AdaptiveColor::fixed(Color::rgb(220, 50, 47)), info: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), border: AdaptiveColor::fixed(Color::rgb(238, 232, 213)), border_focused: AdaptiveColor::fixed(Color::rgb(38, 139, 210)), selection_bg: AdaptiveColor::fixed(Color::rgb(238, 232, 213)), selection_fg: AdaptiveColor::fixed(Color::rgb(88, 110, 117)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(253, 246, 227)),
607 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(238, 232, 213)),
608 }
609 }
610
611 #[must_use]
613 pub fn monokai() -> Theme {
614 Theme {
615 primary: AdaptiveColor::fixed(Color::rgb(102, 217, 239)), secondary: AdaptiveColor::fixed(Color::rgb(174, 129, 255)), accent: AdaptiveColor::fixed(Color::rgb(249, 38, 114)), background: AdaptiveColor::fixed(Color::rgb(39, 40, 34)), surface: AdaptiveColor::fixed(Color::rgb(60, 61, 54)), overlay: AdaptiveColor::fixed(Color::rgb(60, 61, 54)), text: AdaptiveColor::fixed(Color::rgb(248, 248, 242)), text_muted: AdaptiveColor::fixed(Color::rgb(189, 189, 189)), text_subtle: AdaptiveColor::fixed(Color::rgb(117, 113, 94)), success: AdaptiveColor::fixed(Color::rgb(166, 226, 46)), warning: AdaptiveColor::fixed(Color::rgb(230, 219, 116)), error: AdaptiveColor::fixed(Color::rgb(249, 38, 114)), info: AdaptiveColor::fixed(Color::rgb(102, 217, 239)), border: AdaptiveColor::fixed(Color::rgb(60, 61, 54)), border_focused: AdaptiveColor::fixed(Color::rgb(102, 217, 239)), selection_bg: AdaptiveColor::fixed(Color::rgb(73, 72, 62)), selection_fg: AdaptiveColor::fixed(Color::rgb(248, 248, 242)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(39, 40, 34)),
639 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(60, 61, 54)),
640 }
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647
648 #[test]
649 fn adaptive_color_fixed() {
650 let color = AdaptiveColor::fixed(Color::rgb(255, 0, 0));
651 assert_eq!(color.resolve(true), Color::rgb(255, 0, 0));
652 assert_eq!(color.resolve(false), Color::rgb(255, 0, 0));
653 assert!(!color.is_adaptive());
654 }
655
656 #[test]
657 fn adaptive_color_adaptive() {
658 let color = AdaptiveColor::adaptive(
659 Color::rgb(255, 255, 255), Color::rgb(0, 0, 0), );
662 assert_eq!(color.resolve(true), Color::rgb(0, 0, 0)); assert_eq!(color.resolve(false), Color::rgb(255, 255, 255)); assert!(color.is_adaptive());
665 }
666
667 #[test]
668 fn theme_default_is_dark() {
669 let theme = Theme::default();
670 let bg = theme.background.resolve(true);
672 if let Color::Rgb(rgb) = bg {
673 assert!(rgb.luminance_u8() < 50);
675 }
676 }
677
678 #[test]
679 fn theme_light_has_light_background() {
680 let theme = themes::light();
681 let bg = theme.background.resolve(false);
682 if let Color::Rgb(rgb) = bg {
683 assert!(rgb.luminance_u8() > 200);
685 }
686 }
687
688 #[test]
689 fn theme_has_all_slots() {
690 let theme = Theme::default();
691 let _ = theme.primary.resolve(true);
693 let _ = theme.secondary.resolve(true);
694 let _ = theme.accent.resolve(true);
695 let _ = theme.background.resolve(true);
696 let _ = theme.surface.resolve(true);
697 let _ = theme.overlay.resolve(true);
698 let _ = theme.text.resolve(true);
699 let _ = theme.text_muted.resolve(true);
700 let _ = theme.text_subtle.resolve(true);
701 let _ = theme.success.resolve(true);
702 let _ = theme.warning.resolve(true);
703 let _ = theme.error.resolve(true);
704 let _ = theme.info.resolve(true);
705 let _ = theme.border.resolve(true);
706 let _ = theme.border_focused.resolve(true);
707 let _ = theme.selection_bg.resolve(true);
708 let _ = theme.selection_fg.resolve(true);
709 let _ = theme.scrollbar_track.resolve(true);
710 let _ = theme.scrollbar_thumb.resolve(true);
711 }
712
713 #[test]
714 fn theme_builder_works() {
715 let theme = Theme::builder()
716 .primary(Color::rgb(255, 0, 0))
717 .background(Color::rgb(0, 0, 0))
718 .build();
719
720 assert_eq!(theme.primary.resolve(true), Color::rgb(255, 0, 0));
721 assert_eq!(theme.background.resolve(true), Color::rgb(0, 0, 0));
722 }
723
724 #[test]
725 fn theme_resolve_flattens() {
726 let theme = themes::dark();
727 let resolved = theme.resolve(true);
728
729 assert_eq!(resolved.primary, theme.primary.resolve(true));
731 assert_eq!(resolved.text, theme.text.resolve(true));
732 assert_eq!(resolved.background, theme.background.resolve(true));
733 }
734
735 #[test]
736 fn all_presets_exist() {
737 let _ = themes::default();
738 let _ = themes::dark();
739 let _ = themes::light();
740 let _ = themes::nord();
741 let _ = themes::dracula();
742 let _ = themes::solarized_dark();
743 let _ = themes::solarized_light();
744 let _ = themes::monokai();
745 }
746
747 #[test]
748 fn presets_have_different_colors() {
749 let dark = themes::dark();
750 let light = themes::light();
751 let nord = themes::nord();
752
753 assert_ne!(
755 dark.background.resolve(true),
756 light.background.resolve(false)
757 );
758 assert_ne!(dark.background.resolve(true), nord.background.resolve(true));
759 }
760
761 #[test]
762 fn detect_dark_mode_returns_bool() {
763 let _ = Theme::detect_dark_mode();
765 }
766
767 #[test]
768 fn color_converts_to_adaptive() {
769 let color = Color::rgb(100, 150, 200);
770 let adaptive: AdaptiveColor = color.into();
771 assert_eq!(adaptive.resolve(true), color);
772 assert_eq!(adaptive.resolve(false), color);
773 }
774
775 #[test]
776 fn builder_from_theme() {
777 let base = themes::nord();
778 let modified = ThemeBuilder::from_theme(base.clone())
779 .primary(Color::rgb(255, 0, 0))
780 .build();
781
782 assert_eq!(modified.primary.resolve(true), Color::rgb(255, 0, 0));
784 assert_eq!(modified.secondary, base.secondary);
786 }
787
788 #[test]
790 fn has_at_least_15_semantic_slots() {
791 let theme = Theme::default();
792 let slot_count = 19; assert!(slot_count >= 15);
794
795 let _slots = [
797 &theme.primary,
798 &theme.secondary,
799 &theme.accent,
800 &theme.background,
801 &theme.surface,
802 &theme.overlay,
803 &theme.text,
804 &theme.text_muted,
805 &theme.text_subtle,
806 &theme.success,
807 &theme.warning,
808 &theme.error,
809 &theme.info,
810 &theme.border,
811 &theme.border_focused,
812 &theme.selection_bg,
813 &theme.selection_fg,
814 &theme.scrollbar_track,
815 &theme.scrollbar_thumb,
816 ];
817 }
818
819 #[test]
820 fn adaptive_color_default_is_gray() {
821 let color = AdaptiveColor::default();
822 assert!(!color.is_adaptive());
823 assert_eq!(color.resolve(true), Color::rgb(128, 128, 128));
824 assert_eq!(color.resolve(false), Color::rgb(128, 128, 128));
825 }
826
827 #[test]
828 fn theme_builder_default() {
829 let builder = ThemeBuilder::default();
830 let theme = builder.build();
831 assert_eq!(theme, themes::dark());
833 }
834
835 #[test]
836 fn resolved_theme_has_all_19_slots() {
837 let theme = themes::dark();
838 let resolved = theme.resolve(true);
839 let _colors = [
841 resolved.primary,
842 resolved.secondary,
843 resolved.accent,
844 resolved.background,
845 resolved.surface,
846 resolved.overlay,
847 resolved.text,
848 resolved.text_muted,
849 resolved.text_subtle,
850 resolved.success,
851 resolved.warning,
852 resolved.error,
853 resolved.info,
854 resolved.border,
855 resolved.border_focused,
856 resolved.selection_bg,
857 resolved.selection_fg,
858 resolved.scrollbar_track,
859 resolved.scrollbar_thumb,
860 ];
861 }
862
863 #[test]
864 fn dark_and_light_resolve_differently() {
865 let theme = Theme {
866 text: AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255)),
867 ..themes::dark()
868 };
869 let dark_resolved = theme.resolve(true);
870 let light_resolved = theme.resolve(false);
871 assert_ne!(dark_resolved.text, light_resolved.text);
872 assert_eq!(dark_resolved.text, Color::rgb(255, 255, 255));
873 assert_eq!(light_resolved.text, Color::rgb(0, 0, 0));
874 }
875
876 #[test]
877 fn all_dark_presets_have_dark_backgrounds() {
878 for (name, theme) in [
879 ("dark", themes::dark()),
880 ("nord", themes::nord()),
881 ("dracula", themes::dracula()),
882 ("solarized_dark", themes::solarized_dark()),
883 ("monokai", themes::monokai()),
884 ] {
885 let bg = theme.background.resolve(true);
886 if let Color::Rgb(rgb) = bg {
887 assert!(
888 rgb.luminance_u8() < 100,
889 "{name} background too bright: {}",
890 rgb.luminance_u8()
891 );
892 }
893 }
894 }
895
896 #[test]
897 fn all_light_presets_have_light_backgrounds() {
898 for (name, theme) in [
899 ("light", themes::light()),
900 ("solarized_light", themes::solarized_light()),
901 ] {
902 let bg = theme.background.resolve(false);
903 if let Color::Rgb(rgb) = bg {
904 assert!(
905 rgb.luminance_u8() > 150,
906 "{name} background too dark: {}",
907 rgb.luminance_u8()
908 );
909 }
910 }
911 }
912
913 #[test]
914 fn theme_default_equals_dark() {
915 assert_eq!(Theme::default(), themes::dark());
916 assert_eq!(themes::default(), themes::dark());
917 }
918
919 #[test]
920 fn builder_all_setters_chain() {
921 let theme = Theme::builder()
922 .primary(Color::rgb(1, 0, 0))
923 .secondary(Color::rgb(2, 0, 0))
924 .accent(Color::rgb(3, 0, 0))
925 .background(Color::rgb(4, 0, 0))
926 .surface(Color::rgb(5, 0, 0))
927 .overlay(Color::rgb(6, 0, 0))
928 .text(Color::rgb(7, 0, 0))
929 .text_muted(Color::rgb(8, 0, 0))
930 .text_subtle(Color::rgb(9, 0, 0))
931 .success(Color::rgb(10, 0, 0))
932 .warning(Color::rgb(11, 0, 0))
933 .error(Color::rgb(12, 0, 0))
934 .info(Color::rgb(13, 0, 0))
935 .border(Color::rgb(14, 0, 0))
936 .border_focused(Color::rgb(15, 0, 0))
937 .selection_bg(Color::rgb(16, 0, 0))
938 .selection_fg(Color::rgb(17, 0, 0))
939 .scrollbar_track(Color::rgb(18, 0, 0))
940 .scrollbar_thumb(Color::rgb(19, 0, 0))
941 .build();
942 assert_eq!(theme.primary.resolve(true), Color::rgb(1, 0, 0));
943 assert_eq!(theme.scrollbar_thumb.resolve(true), Color::rgb(19, 0, 0));
944 }
945
946 #[test]
947 fn resolved_theme_is_copy() {
948 let theme = themes::dark();
949 let resolved = theme.resolve(true);
950 let copy = resolved;
951 assert_eq!(resolved, copy);
952 }
953
954 #[test]
955 fn detect_dark_mode_with_colorfgbg_dark() {
956 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;0"));
958 assert!(result, "bg=0 should be dark mode");
959 }
960
961 #[test]
962 fn detect_dark_mode_with_colorfgbg_light_15() {
963 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;15"));
965 assert!(!result, "bg=15 should be light mode");
966 }
967
968 #[test]
969 fn detect_dark_mode_with_colorfgbg_light_7() {
970 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;7"));
972 assert!(!result, "bg=7 should be light mode");
973 }
974
975 #[test]
976 fn detect_dark_mode_without_env_defaults_dark() {
977 let result = Theme::detect_dark_mode_from_colorfgbg(None);
978 assert!(result, "missing COLORFGBG should default to dark");
979 }
980
981 #[test]
982 fn detect_dark_mode_with_empty_string() {
983 let result = Theme::detect_dark_mode_from_colorfgbg(Some(""));
984 assert!(result, "empty COLORFGBG should default to dark");
985 }
986
987 #[test]
988 fn detect_dark_mode_with_no_semicolon() {
989 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0"));
990 assert!(result, "COLORFGBG without semicolon should default to dark");
991 }
992
993 #[test]
994 fn detect_dark_mode_with_multiple_semicolons() {
995 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;0;extra"));
997 assert!(result, "COLORFGBG with extra parts should use last as bg");
998 }
999
1000 #[test]
1001 fn detect_dark_mode_with_whitespace() {
1002 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0; 15 "));
1003 assert!(!result, "COLORFGBG with whitespace should parse correctly");
1004 }
1005
1006 #[test]
1007 fn detect_dark_mode_with_invalid_number() {
1008 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;abc"));
1009 assert!(
1010 result,
1011 "COLORFGBG with invalid number should default to dark"
1012 );
1013 }
1014
1015 #[test]
1016 fn theme_clone_produces_equal_theme() {
1017 let theme = themes::nord();
1018 let cloned = theme.clone();
1019 assert_eq!(theme, cloned);
1020 }
1021
1022 #[test]
1023 fn theme_equality_different_themes() {
1024 let dark = themes::dark();
1025 let light = themes::light();
1026 assert_ne!(dark, light);
1027 }
1028
1029 #[test]
1030 fn resolved_theme_different_modes_differ() {
1031 let theme = Theme {
1033 text: AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255)),
1034 background: AdaptiveColor::adaptive(Color::rgb(255, 255, 255), Color::rgb(0, 0, 0)),
1035 ..themes::dark()
1036 };
1037 let dark_resolved = theme.resolve(true);
1038 let light_resolved = theme.resolve(false);
1039 assert_ne!(dark_resolved, light_resolved);
1040 }
1041
1042 #[test]
1043 fn resolved_theme_equality_same_mode() {
1044 let theme = themes::dark();
1045 let resolved1 = theme.resolve(true);
1046 let resolved2 = theme.resolve(true);
1047 assert_eq!(resolved1, resolved2);
1048 }
1049
1050 #[test]
1051 fn preset_nord_has_characteristic_colors() {
1052 let nord = themes::nord();
1053 let primary = nord.primary.resolve(true);
1055 if let Color::Rgb(rgb) = primary {
1056 assert!(rgb.b > rgb.r, "Nord primary should be bluish");
1057 }
1058 }
1059
1060 #[test]
1061 fn preset_dracula_has_characteristic_colors() {
1062 let dracula = themes::dracula();
1063 let primary = dracula.primary.resolve(true);
1065 if let Color::Rgb(rgb) = primary {
1066 assert!(
1067 rgb.r > 100 && rgb.b > 200,
1068 "Dracula primary should be purple"
1069 );
1070 }
1071 }
1072
1073 #[test]
1074 fn preset_monokai_has_characteristic_colors() {
1075 let monokai = themes::monokai();
1076 let primary = monokai.primary.resolve(true);
1078 if let Color::Rgb(rgb) = primary {
1079 assert!(rgb.g > 200 && rgb.b > 200, "Monokai primary should be cyan");
1080 }
1081 }
1082
1083 #[test]
1084 fn preset_solarized_dark_and_light_share_accent_colors() {
1085 let sol_dark = themes::solarized_dark();
1086 let sol_light = themes::solarized_light();
1087 assert_eq!(
1089 sol_dark.primary.resolve(true),
1090 sol_light.primary.resolve(true),
1091 "Solarized dark and light should share primary accent"
1092 );
1093 }
1094
1095 #[test]
1096 fn builder_accepts_adaptive_color_directly() {
1097 let adaptive = AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255));
1098 let theme = Theme::builder().text(adaptive).build();
1099 assert!(theme.text.is_adaptive());
1100 }
1101
1102 #[test]
1103 fn all_presets_have_distinct_error_colors_from_info() {
1104 for (name, theme) in [
1105 ("dark", themes::dark()),
1106 ("light", themes::light()),
1107 ("nord", themes::nord()),
1108 ("dracula", themes::dracula()),
1109 ("solarized_dark", themes::solarized_dark()),
1110 ("monokai", themes::monokai()),
1111 ] {
1112 let error = theme.error.resolve(true);
1113 let info = theme.info.resolve(true);
1114 assert_ne!(
1115 error, info,
1116 "{name} should have distinct error and info colors"
1117 );
1118 }
1119 }
1120
1121 #[test]
1122 fn adaptive_color_debug_impl() {
1123 let fixed = AdaptiveColor::fixed(Color::rgb(255, 0, 0));
1124 let adaptive = AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255));
1125 let _ = format!("{:?}", fixed);
1127 let _ = format!("{:?}", adaptive);
1128 }
1129
1130 #[test]
1131 fn theme_debug_impl() {
1132 let theme = themes::dark();
1133 let debug = format!("{:?}", theme);
1135 assert!(debug.contains("Theme"));
1136 }
1137
1138 #[test]
1139 fn resolved_theme_debug_impl() {
1140 let resolved = themes::dark().resolve(true);
1141 let debug = format!("{:?}", resolved);
1142 assert!(debug.contains("ResolvedTheme"));
1143 }
1144}