standout_render/theme/adaptive.rs
1//! Color mode detection for adaptive themes.
2//!
3//! This module provides color mode detection for themes that adapt to the
4//! user's OS display mode (light/dark).
5//!
6//! # Usage
7//!
8//! Color mode detection is typically handled automatically by the render
9//! functions. Use [`set_theme_detector`] to override detection for testing.
10//!
11//! ```rust
12//! use standout_render::{Theme, ColorMode, set_theme_detector};
13//! use console::Style;
14//!
15//! // Create an adaptive theme
16//! let theme = Theme::new()
17//! .add_adaptive(
18//! "panel",
19//! Style::new(),
20//! Some(Style::new().fg(console::Color::Black)), // Light mode
21//! Some(Style::new().fg(console::Color::White)), // Dark mode
22//! );
23//!
24//! // For testing, override the detector
25//! set_theme_detector(|| ColorMode::Dark);
26//! ```
27
28use dark_light::{detect as detect_os_theme, Mode as OsThemeMode};
29use once_cell::sync::Lazy;
30use std::sync::Mutex;
31
32/// The user's preferred color mode.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ColorMode {
35 /// Light mode (light background, dark text).
36 Light,
37 /// Dark mode (dark background, light text).
38 Dark,
39}
40
41type ThemeDetector = fn() -> ColorMode;
42
43static THEME_DETECTOR: Lazy<Mutex<ThemeDetector>> = Lazy::new(|| Mutex::new(os_theme_detector));
44
45/// Overrides the detector used to determine whether the user prefers a light or dark theme.
46///
47/// This is useful for testing or when you want to force a specific color mode.
48///
49/// # Example
50///
51/// ```rust
52/// use standout_render::{ColorMode, set_theme_detector};
53///
54/// // Force dark mode for testing
55/// set_theme_detector(|| ColorMode::Dark);
56///
57/// // Reset to OS detection (if needed)
58/// // Note: There's no direct way to reset to OS detection,
59/// // but tests should restore their changes.
60/// ```
61pub fn set_theme_detector(detector: ThemeDetector) {
62 let mut guard = THEME_DETECTOR.lock().unwrap();
63 *guard = detector;
64}
65
66/// Detects the user's preferred color mode from the OS.
67///
68/// Uses the `dark-light` crate to query the OS for the current theme preference.
69/// The detector can be overridden via [`set_theme_detector`] for testing.
70///
71/// # Returns
72///
73/// - [`ColorMode::Light`] if the OS is in light mode
74/// - [`ColorMode::Dark`] if the OS is in dark mode
75pub fn detect_color_mode() -> ColorMode {
76 let detector = THEME_DETECTOR.lock().unwrap();
77 (*detector)()
78}
79
80fn os_theme_detector() -> ColorMode {
81 match detect_os_theme() {
82 OsThemeMode::Dark => ColorMode::Dark,
83 OsThemeMode::Light => ColorMode::Light,
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::{render_with_output, OutputMode, Theme};
91 use console::Style;
92 use serde::Serialize;
93 use serial_test::serial;
94
95 #[derive(Serialize)]
96 struct SimpleData {
97 message: String,
98 }
99
100 #[test]
101 #[serial]
102 fn test_adaptive_theme_uses_detector() {
103 console::set_colors_enabled(true);
104
105 // Create an adaptive theme with different colors for light/dark modes
106 let theme = Theme::new().add_adaptive(
107 "tone",
108 Style::new(), // base (unused since we always have overrides)
109 Some(Style::new().green().force_styling(true)), // Light mode
110 Some(Style::new().red().force_styling(true)), // Dark mode
111 );
112
113 let data = SimpleData {
114 message: "hi".into(),
115 };
116
117 // Test dark mode
118 set_theme_detector(|| ColorMode::Dark);
119 let dark_output = render_with_output(
120 r#"[tone]{{ message }}[/tone]"#,
121 &data,
122 &theme,
123 OutputMode::Term,
124 )
125 .unwrap();
126 assert!(
127 dark_output.contains("\x1b[31"),
128 "Expected red color in dark mode, got: {}",
129 dark_output
130 );
131
132 // Test light mode
133 set_theme_detector(|| ColorMode::Light);
134 let light_output = render_with_output(
135 r#"[tone]{{ message }}[/tone]"#,
136 &data,
137 &theme,
138 OutputMode::Term,
139 )
140 .unwrap();
141 assert!(
142 light_output.contains("\x1b[32"),
143 "Expected green color in light mode, got: {}",
144 light_output
145 );
146
147 // Reset to light for other tests
148 set_theme_detector(|| ColorMode::Light);
149 }
150
151 #[test]
152 #[serial]
153 fn test_detect_color_mode_uses_override() {
154 set_theme_detector(|| ColorMode::Dark);
155 assert_eq!(detect_color_mode(), ColorMode::Dark);
156
157 set_theme_detector(|| ColorMode::Light);
158 assert_eq!(detect_color_mode(), ColorMode::Light);
159 }
160}