Skip to main content

stakpak_shared/
terminal_theme.rs

1//! Terminal theme detection (light/dark background)
2//!
3//! Provides a single source of truth for detecting the terminal's color scheme.
4//! Used by both CLI and TUI crates to ensure consistent theme detection.
5
6use std::sync::OnceLock;
7
8/// Terminal color theme (light or dark background)
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum Theme {
11    #[default]
12    Dark,
13    Light,
14}
15
16/// Global theme state - detected once at startup and cached
17static CURRENT_THEME: OnceLock<Theme> = OnceLock::new();
18
19/// Initialize the theme detection. Call this once at startup.
20/// If `override_theme` is Some, use that instead of auto-detection.
21pub fn init_theme(override_theme: Option<Theme>) {
22    let theme = override_theme.unwrap_or_else(detect_theme);
23    // OnceLock::set returns Err if already set, which we ignore
24    let _ = CURRENT_THEME.set(theme);
25}
26
27/// Get the current theme. Returns Dark if not initialized.
28pub fn current_theme() -> Theme {
29    *CURRENT_THEME.get_or_init(detect_theme)
30}
31
32/// Check if we're in light mode
33pub fn is_light_mode() -> bool {
34    current_theme() == Theme::Light
35}
36
37/// Detect terminal theme using terminal-light crate
38/// Falls back to Dark if detection fails
39fn detect_theme() -> Theme {
40    // First check environment variable override
41    if let Ok(theme_env) = std::env::var("STAKPAK_THEME") {
42        match theme_env.to_lowercase().as_str() {
43            "light" => return Theme::Light,
44            "dark" => return Theme::Dark,
45            _ => {} // Fall through to detection
46        }
47    }
48
49    // Use terminal-light for detection (only on unix, Windows falls back)
50    #[cfg(unix)]
51    {
52        // Use a thread with timeout to avoid blocking on slow/unresponsive terminals
53        // (e.g., SSH connections, terminals that don't respond to OSC queries)
54        use std::sync::mpsc;
55        use std::time::Duration;
56
57        let (tx, rx) = mpsc::channel();
58        std::thread::spawn(move || {
59            let _ = tx.send(terminal_light::luma());
60        });
61
62        match rx.recv_timeout(Duration::from_millis(100)) {
63            Ok(Ok(luma)) if luma > 0.5 => return Theme::Light,
64            Ok(Ok(_)) => return Theme::Dark,
65            Ok(Err(_)) | Err(_) => {
66                // Detection failed or timed out - try COLORFGBG fallback
67            }
68        }
69    }
70
71    // Fallback: COLORFGBG environment variable
72    detect_theme_from_colorfgbg()
73}
74
75/// Fallback theme detection using COLORFGBG environment variable
76fn detect_theme_from_colorfgbg() -> Theme {
77    // COLORFGBG format: "fg;bg" where bg is ANSI color code
78    // 0 = black (dark), 15 = white (light)
79    if let Ok(colorfgbg) = std::env::var("COLORFGBG")
80        && let Some(bg_str) = colorfgbg.split(';').next_back()
81        && let Ok(bg) = bg_str.trim().parse::<u8>()
82    {
83        // ANSI colors: 0-7 are dark variants, 8-15 are light variants
84        // White (15) and light gray (7) typically indicate light background
85        if bg == 15 || bg == 7 {
86            return Theme::Light;
87        }
88    }
89    Theme::Dark // Default to dark
90}