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}