ai_agent/utils/
system_theme.rs1use crate::utils::config::ThemeSetting;
14use once_cell::sync::Lazy;
15use std::env;
16use std::sync::Mutex;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SystemTheme {
21 Dark,
22 Light,
23}
24
25static CACHED_SYSTEM_THEME: Lazy<Mutex<Option<SystemTheme>>> = Lazy::new(|| Mutex::new(None));
26
27pub fn get_system_theme_name() -> SystemTheme {
30 let mut cached = CACHED_SYSTEM_THEME.lock().unwrap();
31 if cached.is_none() {
32 *cached = detect_from_color_fg_bg().or(Some(SystemTheme::Dark));
33 }
34 cached.unwrap_or(SystemTheme::Dark)
35}
36
37pub fn set_cached_system_theme(theme: SystemTheme) {
40 let mut cached = CACHED_SYSTEM_THEME.lock().unwrap();
41 *cached = Some(theme);
42}
43
44pub fn resolve_theme_setting(setting: &ThemeSetting) -> &'static str {
46 match setting {
47 ThemeSetting::System => {
48 if get_system_theme_name() == SystemTheme::Light {
49 "light"
50 } else {
51 "dark"
52 }
53 }
54 ThemeSetting::Dark => "dark",
55 ThemeSetting::Light => "light",
56 }
57}
58
59pub fn theme_from_osc_color(data: &str) -> Option<SystemTheme> {
69 let rgb = parse_osc_rgb(data)?;
70 let luminance = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
72 if luminance > 0.5 {
73 Some(SystemTheme::Light)
74 } else {
75 Some(SystemTheme::Dark)
76 }
77}
78
79#[derive(Debug, Clone, Copy)]
80struct Rgb {
81 r: f64,
82 g: f64,
83 b: f64,
84}
85
86fn parse_osc_rgb(data: &str) -> Option<Rgb> {
87 let data_lower = data.to_lowercase();
90 if let Some(caps) = regex::Regex::new(r"^rgba?:([0-9a-f]{1,4})/([0-9a-f]{1,4})/([0-9a-f]{1,4})")
91 .ok()?
92 .captures(&data_lower)
93 {
94 return Some(Rgb {
95 r: hex_component(&caps[1]),
96 g: hex_component(&caps[2]),
97 b: hex_component(&caps[3]),
98 });
99 }
100 if let Some(caps) = regex::Regex::new(r"^#([0-9a-f]+)$")
102 .ok()?
103 .captures(&data_lower)
104 {
105 let hex = &caps[1];
106 if hex.len() % 3 == 0 {
107 let n = hex.len() / 3;
108 return Some(Rgb {
109 r: hex_component(&hex[..n]),
110 g: hex_component(&hex[n..2 * n]),
111 b: hex_component(&hex[2 * n..]),
112 });
113 }
114 }
115 None
116}
117
118fn hex_component(hex: &str) -> f64 {
120 let max = 16_f64.powi(hex.len() as i32) - 1.0;
121 let value = u64::from_str_radix(hex, 16).unwrap_or(0) as f64;
122 value / max
123}
124
125fn detect_from_color_fg_bg() -> Option<SystemTheme> {
131 let colorfgbg = env::var("COLORFGBG").ok()?;
132 let parts: Vec<&str> = colorfgbg.split(';').collect();
133 let bg = parts.last()?;
134 if bg.is_empty() {
135 return None;
136 }
137 let bg_num: i32 = bg.parse().ok()?;
138 if bg_num < 0 || bg_num > 15 {
139 return None;
140 }
141 if bg_num <= 6 || bg_num == 8 {
143 Some(SystemTheme::Dark)
144 } else {
145 Some(SystemTheme::Light)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_theme_from_osc_color_rgb() {
155 assert_eq!(theme_from_osc_color("rgb:0/0/0"), Some(SystemTheme::Dark));
157 assert_eq!(
159 theme_from_osc_color("rgb:ff/ff/ff"),
160 Some(SystemTheme::Light)
161 );
162 assert_eq!(
164 theme_from_osc_color("rgb:80/80/80"),
165 Some(SystemTheme::Light)
166 );
167 }
168
169 #[test]
170 fn test_theme_from_osc_color_hash() {
171 assert_eq!(theme_from_osc_color("#000000"), Some(SystemTheme::Dark));
172 assert_eq!(theme_from_osc_color("#ffffff"), Some(SystemTheme::Light));
173 }
174
175 #[test]
176 fn test_resolve_theme_setting() {
177 assert_eq!(resolve_theme_setting(&ThemeSetting::Dark), "dark");
178 assert_eq!(resolve_theme_setting(&ThemeSetting::Light), "light");
179 let _ = resolve_theme_setting(&ThemeSetting::System);
181 }
182}