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)]
266#[must_use]
267pub struct ThemeBuilder {
268 theme: Theme,
269}
270
271impl ThemeBuilder {
272 pub fn new() -> Self {
274 Self {
275 theme: themes::dark(),
276 }
277 }
278
279 pub fn from_theme(theme: Theme) -> Self {
281 Self { theme }
282 }
283
284 pub fn primary(mut self, color: impl Into<AdaptiveColor>) -> Self {
286 self.theme.primary = color.into();
287 self
288 }
289
290 pub fn secondary(mut self, color: impl Into<AdaptiveColor>) -> Self {
292 self.theme.secondary = color.into();
293 self
294 }
295
296 pub fn accent(mut self, color: impl Into<AdaptiveColor>) -> Self {
298 self.theme.accent = color.into();
299 self
300 }
301
302 pub fn background(mut self, color: impl Into<AdaptiveColor>) -> Self {
304 self.theme.background = color.into();
305 self
306 }
307
308 pub fn surface(mut self, color: impl Into<AdaptiveColor>) -> Self {
310 self.theme.surface = color.into();
311 self
312 }
313
314 pub fn overlay(mut self, color: impl Into<AdaptiveColor>) -> Self {
316 self.theme.overlay = color.into();
317 self
318 }
319
320 pub fn text(mut self, color: impl Into<AdaptiveColor>) -> Self {
322 self.theme.text = color.into();
323 self
324 }
325
326 pub fn text_muted(mut self, color: impl Into<AdaptiveColor>) -> Self {
328 self.theme.text_muted = color.into();
329 self
330 }
331
332 pub fn text_subtle(mut self, color: impl Into<AdaptiveColor>) -> Self {
334 self.theme.text_subtle = color.into();
335 self
336 }
337
338 pub fn success(mut self, color: impl Into<AdaptiveColor>) -> Self {
340 self.theme.success = color.into();
341 self
342 }
343
344 pub fn warning(mut self, color: impl Into<AdaptiveColor>) -> Self {
346 self.theme.warning = color.into();
347 self
348 }
349
350 pub fn error(mut self, color: impl Into<AdaptiveColor>) -> Self {
352 self.theme.error = color.into();
353 self
354 }
355
356 pub fn info(mut self, color: impl Into<AdaptiveColor>) -> Self {
358 self.theme.info = color.into();
359 self
360 }
361
362 pub fn border(mut self, color: impl Into<AdaptiveColor>) -> Self {
364 self.theme.border = color.into();
365 self
366 }
367
368 pub fn border_focused(mut self, color: impl Into<AdaptiveColor>) -> Self {
370 self.theme.border_focused = color.into();
371 self
372 }
373
374 pub fn selection_bg(mut self, color: impl Into<AdaptiveColor>) -> Self {
376 self.theme.selection_bg = color.into();
377 self
378 }
379
380 pub fn selection_fg(mut self, color: impl Into<AdaptiveColor>) -> Self {
382 self.theme.selection_fg = color.into();
383 self
384 }
385
386 pub fn scrollbar_track(mut self, color: impl Into<AdaptiveColor>) -> Self {
388 self.theme.scrollbar_track = color.into();
389 self
390 }
391
392 pub fn scrollbar_thumb(mut self, color: impl Into<AdaptiveColor>) -> Self {
394 self.theme.scrollbar_thumb = color.into();
395 self
396 }
397
398 pub fn build(self) -> Theme {
400 self.theme
401 }
402}
403
404impl Default for ThemeBuilder {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410pub mod themes {
412 use super::*;
413
414 #[must_use]
416 pub fn default() -> Theme {
417 dark()
418 }
419
420 #[must_use]
422 pub fn dark() -> Theme {
423 Theme {
424 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)),
448 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(72, 79, 88)),
449 }
450 }
451
452 #[must_use]
454 pub fn light() -> Theme {
455 Theme {
456 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)),
480 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(175, 184, 193)),
481 }
482 }
483
484 #[must_use]
486 pub fn nord() -> Theme {
487 Theme {
488 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)),
512 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(76, 86, 106)),
513 }
514 }
515
516 #[must_use]
518 pub fn dracula() -> Theme {
519 Theme {
520 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)),
544 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(68, 71, 90)),
545 }
546 }
547
548 #[must_use]
550 pub fn solarized_dark() -> Theme {
551 Theme {
552 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)),
576 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(7, 54, 66)),
577 }
578 }
579
580 #[must_use]
582 pub fn solarized_light() -> Theme {
583 Theme {
584 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)),
608 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(238, 232, 213)),
609 }
610 }
611
612 #[must_use]
614 pub fn monokai() -> Theme {
615 Theme {
616 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)),
640 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(60, 61, 54)),
641 }
642 }
643
644 #[must_use]
646 pub fn doom() -> Theme {
647 Theme {
648 primary: AdaptiveColor::fixed(Color::rgb(178, 34, 34)), secondary: AdaptiveColor::fixed(Color::rgb(50, 205, 50)), accent: AdaptiveColor::fixed(Color::rgb(255, 255, 0)), background: AdaptiveColor::fixed(Color::rgb(26, 26, 26)), surface: AdaptiveColor::fixed(Color::rgb(47, 47, 47)), overlay: AdaptiveColor::fixed(Color::rgb(64, 64, 64)), text: AdaptiveColor::fixed(Color::rgb(211, 211, 211)), text_muted: AdaptiveColor::fixed(Color::rgb(128, 128, 128)), text_subtle: AdaptiveColor::fixed(Color::rgb(105, 105, 105)), success: AdaptiveColor::fixed(Color::rgb(50, 205, 50)), warning: AdaptiveColor::fixed(Color::rgb(255, 215, 0)), error: AdaptiveColor::fixed(Color::rgb(139, 0, 0)), info: AdaptiveColor::fixed(Color::rgb(65, 105, 225)), border: AdaptiveColor::fixed(Color::rgb(105, 105, 105)), border_focused: AdaptiveColor::fixed(Color::rgb(178, 34, 34)), selection_bg: AdaptiveColor::fixed(Color::rgb(139, 0, 0)), selection_fg: AdaptiveColor::fixed(Color::rgb(255, 255, 255)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(26, 26, 26)),
672 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(178, 34, 34)),
673 }
674 }
675
676 #[must_use]
678 pub fn quake() -> Theme {
679 Theme {
680 primary: AdaptiveColor::fixed(Color::rgb(139, 69, 19)), secondary: AdaptiveColor::fixed(Color::rgb(85, 107, 47)), accent: AdaptiveColor::fixed(Color::rgb(205, 133, 63)), background: AdaptiveColor::fixed(Color::rgb(28, 28, 28)), surface: AdaptiveColor::fixed(Color::rgb(46, 39, 34)), overlay: AdaptiveColor::fixed(Color::rgb(62, 54, 48)), text: AdaptiveColor::fixed(Color::rgb(210, 180, 140)), text_muted: AdaptiveColor::fixed(Color::rgb(139, 115, 85)), text_subtle: AdaptiveColor::fixed(Color::rgb(101, 84, 61)), success: AdaptiveColor::fixed(Color::rgb(85, 107, 47)), warning: AdaptiveColor::fixed(Color::rgb(210, 105, 30)), error: AdaptiveColor::fixed(Color::rgb(128, 0, 0)), info: AdaptiveColor::fixed(Color::rgb(70, 130, 180)), border: AdaptiveColor::fixed(Color::rgb(93, 64, 55)), border_focused: AdaptiveColor::fixed(Color::rgb(205, 133, 63)), selection_bg: AdaptiveColor::fixed(Color::rgb(139, 69, 19)), selection_fg: AdaptiveColor::fixed(Color::rgb(255, 222, 173)), scrollbar_track: AdaptiveColor::fixed(Color::rgb(28, 28, 28)),
704 scrollbar_thumb: AdaptiveColor::fixed(Color::rgb(139, 69, 19)),
705 }
706 }
707}
708
709pub struct SharedResolvedTheme {
737 inner: arc_swap::ArcSwap<ResolvedTheme>,
738}
739
740impl SharedResolvedTheme {
741 pub fn new(theme: ResolvedTheme) -> Self {
743 Self {
744 inner: arc_swap::ArcSwap::from_pointee(theme),
745 }
746 }
747
748 #[inline]
750 pub fn load(&self) -> ResolvedTheme {
751 let guard = self.inner.load();
752 **guard
753 }
754
755 #[inline]
757 pub fn store(&self, theme: ResolvedTheme) {
758 self.inner.store(std::sync::Arc::new(theme));
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765
766 #[test]
767 fn adaptive_color_fixed() {
768 let color = AdaptiveColor::fixed(Color::rgb(255, 0, 0));
769 assert_eq!(color.resolve(true), Color::rgb(255, 0, 0));
770 assert_eq!(color.resolve(false), Color::rgb(255, 0, 0));
771 assert!(!color.is_adaptive());
772 }
773
774 #[test]
775 fn adaptive_color_adaptive() {
776 let color = AdaptiveColor::adaptive(
777 Color::rgb(255, 255, 255), Color::rgb(0, 0, 0), );
780 assert_eq!(color.resolve(true), Color::rgb(0, 0, 0)); assert_eq!(color.resolve(false), Color::rgb(255, 255, 255)); assert!(color.is_adaptive());
783 }
784
785 #[test]
786 fn theme_default_is_dark() {
787 let theme = Theme::default();
788 let bg = theme.background.resolve(true);
790 if let Color::Rgb(rgb) = bg {
791 assert!(rgb.luminance_u8() < 50);
793 }
794 }
795
796 #[test]
797 fn theme_light_has_light_background() {
798 let theme = themes::light();
799 let bg = theme.background.resolve(false);
800 if let Color::Rgb(rgb) = bg {
801 assert!(rgb.luminance_u8() > 200);
803 }
804 }
805
806 #[test]
807 fn theme_has_all_slots() {
808 let theme = Theme::default();
809 let _ = theme.primary.resolve(true);
811 let _ = theme.secondary.resolve(true);
812 let _ = theme.accent.resolve(true);
813 let _ = theme.background.resolve(true);
814 let _ = theme.surface.resolve(true);
815 let _ = theme.overlay.resolve(true);
816 let _ = theme.text.resolve(true);
817 let _ = theme.text_muted.resolve(true);
818 let _ = theme.text_subtle.resolve(true);
819 let _ = theme.success.resolve(true);
820 let _ = theme.warning.resolve(true);
821 let _ = theme.error.resolve(true);
822 let _ = theme.info.resolve(true);
823 let _ = theme.border.resolve(true);
824 let _ = theme.border_focused.resolve(true);
825 let _ = theme.selection_bg.resolve(true);
826 let _ = theme.selection_fg.resolve(true);
827 let _ = theme.scrollbar_track.resolve(true);
828 let _ = theme.scrollbar_thumb.resolve(true);
829 }
830
831 #[test]
832 fn theme_builder_works() {
833 let theme = Theme::builder()
834 .primary(Color::rgb(255, 0, 0))
835 .background(Color::rgb(0, 0, 0))
836 .build();
837
838 assert_eq!(theme.primary.resolve(true), Color::rgb(255, 0, 0));
839 assert_eq!(theme.background.resolve(true), Color::rgb(0, 0, 0));
840 }
841
842 #[test]
843 fn theme_resolve_flattens() {
844 let theme = themes::dark();
845 let resolved = theme.resolve(true);
846
847 assert_eq!(resolved.primary, theme.primary.resolve(true));
849 assert_eq!(resolved.text, theme.text.resolve(true));
850 assert_eq!(resolved.background, theme.background.resolve(true));
851 }
852
853 #[test]
854 fn all_presets_exist() {
855 let _ = themes::default();
856 let _ = themes::dark();
857 let _ = themes::light();
858 let _ = themes::nord();
859 let _ = themes::dracula();
860 let _ = themes::solarized_dark();
861 let _ = themes::solarized_light();
862 let _ = themes::monokai();
863 }
864
865 #[test]
866 fn presets_have_different_colors() {
867 let dark = themes::dark();
868 let light = themes::light();
869 let nord = themes::nord();
870
871 assert_ne!(
873 dark.background.resolve(true),
874 light.background.resolve(false)
875 );
876 assert_ne!(dark.background.resolve(true), nord.background.resolve(true));
877 }
878
879 #[test]
880 fn detect_dark_mode_returns_bool() {
881 let _ = Theme::detect_dark_mode();
883 }
884
885 #[test]
886 fn color_converts_to_adaptive() {
887 let color = Color::rgb(100, 150, 200);
888 let adaptive: AdaptiveColor = color.into();
889 assert_eq!(adaptive.resolve(true), color);
890 assert_eq!(adaptive.resolve(false), color);
891 }
892
893 #[test]
894 fn builder_from_theme() {
895 let base = themes::nord();
896 let modified = ThemeBuilder::from_theme(base.clone())
897 .primary(Color::rgb(255, 0, 0))
898 .build();
899
900 assert_eq!(modified.primary.resolve(true), Color::rgb(255, 0, 0));
902 assert_eq!(modified.secondary, base.secondary);
904 }
905
906 #[test]
908 fn has_at_least_15_semantic_slots() {
909 let theme = Theme::default();
910 let slot_count = 19; assert!(slot_count >= 15);
912
913 let _slots = [
915 &theme.primary,
916 &theme.secondary,
917 &theme.accent,
918 &theme.background,
919 &theme.surface,
920 &theme.overlay,
921 &theme.text,
922 &theme.text_muted,
923 &theme.text_subtle,
924 &theme.success,
925 &theme.warning,
926 &theme.error,
927 &theme.info,
928 &theme.border,
929 &theme.border_focused,
930 &theme.selection_bg,
931 &theme.selection_fg,
932 &theme.scrollbar_track,
933 &theme.scrollbar_thumb,
934 ];
935 }
936
937 #[test]
938 fn adaptive_color_default_is_gray() {
939 let color = AdaptiveColor::default();
940 assert!(!color.is_adaptive());
941 assert_eq!(color.resolve(true), Color::rgb(128, 128, 128));
942 assert_eq!(color.resolve(false), Color::rgb(128, 128, 128));
943 }
944
945 #[test]
946 fn theme_builder_default() {
947 let builder = ThemeBuilder::default();
948 let theme = builder.build();
949 assert_eq!(theme, themes::dark());
951 }
952
953 #[test]
954 fn resolved_theme_has_all_19_slots() {
955 let theme = themes::dark();
956 let resolved = theme.resolve(true);
957 let _colors = [
959 resolved.primary,
960 resolved.secondary,
961 resolved.accent,
962 resolved.background,
963 resolved.surface,
964 resolved.overlay,
965 resolved.text,
966 resolved.text_muted,
967 resolved.text_subtle,
968 resolved.success,
969 resolved.warning,
970 resolved.error,
971 resolved.info,
972 resolved.border,
973 resolved.border_focused,
974 resolved.selection_bg,
975 resolved.selection_fg,
976 resolved.scrollbar_track,
977 resolved.scrollbar_thumb,
978 ];
979 }
980
981 #[test]
982 fn dark_and_light_resolve_differently() {
983 let theme = Theme {
984 text: AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255)),
985 ..themes::dark()
986 };
987 let dark_resolved = theme.resolve(true);
988 let light_resolved = theme.resolve(false);
989 assert_ne!(dark_resolved.text, light_resolved.text);
990 assert_eq!(dark_resolved.text, Color::rgb(255, 255, 255));
991 assert_eq!(light_resolved.text, Color::rgb(0, 0, 0));
992 }
993
994 #[test]
995 fn all_dark_presets_have_dark_backgrounds() {
996 for (name, theme) in [
997 ("dark", themes::dark()),
998 ("nord", themes::nord()),
999 ("dracula", themes::dracula()),
1000 ("solarized_dark", themes::solarized_dark()),
1001 ("monokai", themes::monokai()),
1002 ] {
1003 let bg = theme.background.resolve(true);
1004 if let Color::Rgb(rgb) = bg {
1005 assert!(
1006 rgb.luminance_u8() < 100,
1007 "{name} background too bright: {}",
1008 rgb.luminance_u8()
1009 );
1010 }
1011 }
1012 }
1013
1014 #[test]
1015 fn all_light_presets_have_light_backgrounds() {
1016 for (name, theme) in [
1017 ("light", themes::light()),
1018 ("solarized_light", themes::solarized_light()),
1019 ] {
1020 let bg = theme.background.resolve(false);
1021 if let Color::Rgb(rgb) = bg {
1022 assert!(
1023 rgb.luminance_u8() > 150,
1024 "{name} background too dark: {}",
1025 rgb.luminance_u8()
1026 );
1027 }
1028 }
1029 }
1030
1031 #[test]
1032 fn theme_default_equals_dark() {
1033 assert_eq!(Theme::default(), themes::dark());
1034 assert_eq!(themes::default(), themes::dark());
1035 }
1036
1037 #[test]
1038 fn builder_all_setters_chain() {
1039 let theme = Theme::builder()
1040 .primary(Color::rgb(1, 0, 0))
1041 .secondary(Color::rgb(2, 0, 0))
1042 .accent(Color::rgb(3, 0, 0))
1043 .background(Color::rgb(4, 0, 0))
1044 .surface(Color::rgb(5, 0, 0))
1045 .overlay(Color::rgb(6, 0, 0))
1046 .text(Color::rgb(7, 0, 0))
1047 .text_muted(Color::rgb(8, 0, 0))
1048 .text_subtle(Color::rgb(9, 0, 0))
1049 .success(Color::rgb(10, 0, 0))
1050 .warning(Color::rgb(11, 0, 0))
1051 .error(Color::rgb(12, 0, 0))
1052 .info(Color::rgb(13, 0, 0))
1053 .border(Color::rgb(14, 0, 0))
1054 .border_focused(Color::rgb(15, 0, 0))
1055 .selection_bg(Color::rgb(16, 0, 0))
1056 .selection_fg(Color::rgb(17, 0, 0))
1057 .scrollbar_track(Color::rgb(18, 0, 0))
1058 .scrollbar_thumb(Color::rgb(19, 0, 0))
1059 .build();
1060 assert_eq!(theme.primary.resolve(true), Color::rgb(1, 0, 0));
1061 assert_eq!(theme.scrollbar_thumb.resolve(true), Color::rgb(19, 0, 0));
1062 }
1063
1064 #[test]
1065 fn resolved_theme_is_copy() {
1066 let theme = themes::dark();
1067 let resolved = theme.resolve(true);
1068 let copy = resolved;
1069 assert_eq!(resolved, copy);
1070 }
1071
1072 #[test]
1073 fn detect_dark_mode_with_colorfgbg_dark() {
1074 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;0"));
1076 assert!(result, "bg=0 should be dark mode");
1077 }
1078
1079 #[test]
1080 fn detect_dark_mode_with_colorfgbg_light_15() {
1081 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;15"));
1083 assert!(!result, "bg=15 should be light mode");
1084 }
1085
1086 #[test]
1087 fn detect_dark_mode_with_colorfgbg_light_7() {
1088 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;7"));
1090 assert!(!result, "bg=7 should be light mode");
1091 }
1092
1093 #[test]
1094 fn detect_dark_mode_without_env_defaults_dark() {
1095 let result = Theme::detect_dark_mode_from_colorfgbg(None);
1096 assert!(result, "missing COLORFGBG should default to dark");
1097 }
1098
1099 #[test]
1100 fn detect_dark_mode_with_empty_string() {
1101 let result = Theme::detect_dark_mode_from_colorfgbg(Some(""));
1102 assert!(result, "empty COLORFGBG should default to dark");
1103 }
1104
1105 #[test]
1106 fn detect_dark_mode_with_no_semicolon() {
1107 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0"));
1108 assert!(result, "COLORFGBG without semicolon should default to dark");
1109 }
1110
1111 #[test]
1112 fn detect_dark_mode_with_multiple_semicolons() {
1113 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;0;extra"));
1115 assert!(result, "COLORFGBG with extra parts should use last as bg");
1116 }
1117
1118 #[test]
1119 fn detect_dark_mode_with_whitespace() {
1120 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0; 15 "));
1121 assert!(!result, "COLORFGBG with whitespace should parse correctly");
1122 }
1123
1124 #[test]
1125 fn detect_dark_mode_with_invalid_number() {
1126 let result = Theme::detect_dark_mode_from_colorfgbg(Some("0;abc"));
1127 assert!(
1128 result,
1129 "COLORFGBG with invalid number should default to dark"
1130 );
1131 }
1132
1133 #[test]
1134 fn theme_clone_produces_equal_theme() {
1135 let theme = themes::nord();
1136 let cloned = theme.clone();
1137 assert_eq!(theme, cloned);
1138 }
1139
1140 #[test]
1141 fn theme_equality_different_themes() {
1142 let dark = themes::dark();
1143 let light = themes::light();
1144 assert_ne!(dark, light);
1145 }
1146
1147 #[test]
1148 fn resolved_theme_different_modes_differ() {
1149 let theme = Theme {
1151 text: AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255)),
1152 background: AdaptiveColor::adaptive(Color::rgb(255, 255, 255), Color::rgb(0, 0, 0)),
1153 ..themes::dark()
1154 };
1155 let dark_resolved = theme.resolve(true);
1156 let light_resolved = theme.resolve(false);
1157 assert_ne!(dark_resolved, light_resolved);
1158 }
1159
1160 #[test]
1161 fn resolved_theme_equality_same_mode() {
1162 let theme = themes::dark();
1163 let resolved1 = theme.resolve(true);
1164 let resolved2 = theme.resolve(true);
1165 assert_eq!(resolved1, resolved2);
1166 }
1167
1168 #[test]
1169 fn preset_nord_has_characteristic_colors() {
1170 let nord = themes::nord();
1171 let primary = nord.primary.resolve(true);
1173 if let Color::Rgb(rgb) = primary {
1174 assert!(rgb.b > rgb.r, "Nord primary should be bluish");
1175 }
1176 }
1177
1178 #[test]
1179 fn preset_dracula_has_characteristic_colors() {
1180 let dracula = themes::dracula();
1181 let primary = dracula.primary.resolve(true);
1183 if let Color::Rgb(rgb) = primary {
1184 assert!(
1185 rgb.r > 100 && rgb.b > 200,
1186 "Dracula primary should be purple"
1187 );
1188 }
1189 }
1190
1191 #[test]
1192 fn preset_monokai_has_characteristic_colors() {
1193 let monokai = themes::monokai();
1194 let primary = monokai.primary.resolve(true);
1196 if let Color::Rgb(rgb) = primary {
1197 assert!(rgb.g > 200 && rgb.b > 200, "Monokai primary should be cyan");
1198 }
1199 }
1200
1201 #[test]
1202 fn preset_solarized_dark_and_light_share_accent_colors() {
1203 let sol_dark = themes::solarized_dark();
1204 let sol_light = themes::solarized_light();
1205 assert_eq!(
1207 sol_dark.primary.resolve(true),
1208 sol_light.primary.resolve(true),
1209 "Solarized dark and light should share primary accent"
1210 );
1211 }
1212
1213 #[test]
1214 fn builder_accepts_adaptive_color_directly() {
1215 let adaptive = AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255));
1216 let theme = Theme::builder().text(adaptive).build();
1217 assert!(theme.text.is_adaptive());
1218 }
1219
1220 #[test]
1221 fn all_presets_have_distinct_error_colors_from_info() {
1222 for (name, theme) in [
1223 ("dark", themes::dark()),
1224 ("light", themes::light()),
1225 ("nord", themes::nord()),
1226 ("dracula", themes::dracula()),
1227 ("solarized_dark", themes::solarized_dark()),
1228 ("monokai", themes::monokai()),
1229 ] {
1230 let error = theme.error.resolve(true);
1231 let info = theme.info.resolve(true);
1232 assert_ne!(
1233 error, info,
1234 "{name} should have distinct error and info colors"
1235 );
1236 }
1237 }
1238
1239 #[test]
1240 fn adaptive_color_debug_impl() {
1241 let fixed = AdaptiveColor::fixed(Color::rgb(255, 0, 0));
1242 let adaptive = AdaptiveColor::adaptive(Color::rgb(0, 0, 0), Color::rgb(255, 255, 255));
1243 let _ = format!("{:?}", fixed);
1245 let _ = format!("{:?}", adaptive);
1246 }
1247
1248 #[test]
1249 fn theme_debug_impl() {
1250 let theme = themes::dark();
1251 let debug = format!("{:?}", theme);
1253 assert!(debug.contains("Theme"));
1254 }
1255
1256 #[test]
1257 fn resolved_theme_debug_impl() {
1258 let resolved = themes::dark().resolve(true);
1259 let debug = format!("{:?}", resolved);
1260 assert!(debug.contains("ResolvedTheme"));
1261 }
1262
1263 #[test]
1266 fn shared_theme_load_returns_initial() {
1267 let dark = themes::dark().resolve(true);
1268 let shared = SharedResolvedTheme::new(dark);
1269 assert_eq!(shared.load(), dark);
1270 }
1271
1272 #[test]
1273 fn shared_theme_store_replaces_value() {
1274 let original = themes::dark().resolve(true);
1275 let mut updated = original;
1277 updated.primary = Color::rgb(0, 0, 0);
1278 assert_ne!(original.primary, updated.primary);
1279
1280 let shared = SharedResolvedTheme::new(original);
1281 shared.store(updated);
1282 assert_eq!(shared.load(), updated);
1283 assert_ne!(shared.load(), original);
1284 }
1285
1286 #[test]
1287 fn shared_theme_concurrent_read_write() {
1288 use std::sync::{Arc, Barrier};
1289 use std::thread;
1290
1291 let dark = themes::dark().resolve(true);
1292 let light = themes::dark().resolve(false);
1293 let shared = Arc::new(SharedResolvedTheme::new(dark));
1294 let barrier = Arc::new(Barrier::new(5));
1295
1296 let readers: Vec<_> = (0..4)
1297 .map(|_| {
1298 let s = Arc::clone(&shared);
1299 let b = Arc::clone(&barrier);
1300 let dark_copy = dark;
1301 let light_copy = light;
1302 thread::spawn(move || {
1303 b.wait();
1304 for _ in 0..10_000 {
1305 let theme = s.load();
1306 assert!(
1308 theme == dark_copy || theme == light_copy,
1309 "torn read detected"
1310 );
1311 }
1312 })
1313 })
1314 .collect();
1315
1316 let writer = {
1317 let s = Arc::clone(&shared);
1318 let b = Arc::clone(&barrier);
1319 thread::spawn(move || {
1320 b.wait();
1321 for i in 0..1_000 {
1322 if i % 2 == 0 {
1323 s.store(light);
1324 } else {
1325 s.store(dark);
1326 }
1327 }
1328 })
1329 };
1330
1331 writer.join().unwrap();
1332 for h in readers {
1333 h.join().unwrap();
1334 }
1335 }
1336}