Skip to main content

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::{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::{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}