1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Appearance {
22 Dark,
23 Light,
24 Unknown,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum ThemeChoice {
34 #[default]
36 Auto,
37 Dark,
39 Light,
41}
42
43impl ThemeChoice {
44 pub fn from_config_str(s: &str) -> Self {
48 match s.trim().to_ascii_lowercase().as_str() {
49 "dark" => Self::Dark,
50 "light" => Self::Light,
51 _ => Self::Auto,
53 }
54 }
55
56 pub fn as_str(self) -> &'static str {
57 match self {
58 Self::Auto => "auto",
59 Self::Dark => "dark",
60 Self::Light => "light",
61 }
62 }
63
64 pub fn resolve(self, os_hint: Appearance) -> Appearance {
68 match self {
69 Self::Dark => Appearance::Dark,
70 Self::Light => Appearance::Light,
71 Self::Auto => match os_hint {
72 Appearance::Dark | Appearance::Light => os_hint,
73 Appearance::Unknown => Appearance::Dark,
74 },
75 }
76 }
77}
78
79pub fn detect() -> Appearance {
85 if std::env::var_os("NO_COLOR").is_some() {
86 return Appearance::Unknown;
87 }
88
89 if let Some(a) = from_colorfgbg() {
90 return a;
91 }
92
93 #[cfg(target_os = "windows")]
94 {
95 if let Some(a) = windows::detect() {
96 return a;
97 }
98 }
99 #[cfg(target_os = "macos")]
100 {
101 if let Some(a) = macos::detect() {
102 return a;
103 }
104 }
105 #[cfg(all(unix, not(target_os = "macos")))]
106 {
107 if let Some(a) = linux::detect() {
108 return a;
109 }
110 }
111
112 Appearance::Unknown
113}
114
115fn from_colorfgbg() -> Option<Appearance> {
119 let raw = std::env::var("COLORFGBG").ok()?;
120 let bg: u8 = raw.rsplit(';').next()?.trim().parse().ok()?;
121 Some(if matches!(bg, 0..=6 | 8) {
122 Appearance::Dark
123 } else {
124 Appearance::Light
125 })
126}
127
128#[cfg(target_os = "windows")]
132mod windows {
133 use super::Appearance;
134 use std::ffi::{c_void, OsStr};
135 use std::os::windows::ffi::OsStrExt;
136
137 type Hkey = *mut c_void;
143 const HKEY_CURRENT_USER: Hkey = 0x8000_0001 as Hkey;
144 const RRF_RT_REG_DWORD: u32 = 0x0000_0010;
145 const ERROR_SUCCESS: i32 = 0;
146
147 #[link(name = "advapi32")]
148 unsafe extern "system" {
149 fn RegGetValueW(
150 hkey: Hkey,
151 lp_subkey: *const u16,
152 lp_value: *const u16,
153 dw_flags: u32,
154 pdw_type: *mut u32,
155 pv_data: *mut c_void,
156 pcb_data: *mut u32,
157 ) -> i32;
158 }
159
160 fn wide(s: &str) -> Vec<u16> {
161 OsStr::new(s).encode_wide().chain(Some(0)).collect()
162 }
163
164 pub fn detect() -> Option<Appearance> {
165 let subkey = wide(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
168 let value = wide("AppsUseLightTheme");
169 let mut data: u32 = 0;
170 let mut data_len: u32 = std::mem::size_of::<u32>() as u32;
171 let rc = unsafe {
172 RegGetValueW(
173 HKEY_CURRENT_USER,
174 subkey.as_ptr(),
175 value.as_ptr(),
176 RRF_RT_REG_DWORD,
177 std::ptr::null_mut(),
178 &mut data as *mut u32 as *mut c_void,
179 &mut data_len,
180 )
181 };
182 if rc != ERROR_SUCCESS {
183 return None;
184 }
185 Some(if data == 1 {
186 Appearance::Light
187 } else {
188 Appearance::Dark
189 })
190 }
191}
192
193#[cfg(target_os = "macos")]
197mod macos {
198 use super::Appearance;
199 use std::process::Command;
200
201 pub fn detect() -> Option<Appearance> {
202 let out = Command::new("defaults")
206 .args(["read", "-g", "AppleInterfaceStyle"])
207 .output()
208 .ok()?;
209 if out.status.success() && String::from_utf8_lossy(&out.stdout).contains("Dark") {
210 Some(Appearance::Dark)
211 } else {
212 Some(Appearance::Light)
214 }
215 }
216}
217
218#[cfg(all(unix, not(target_os = "macos")))]
222mod linux {
223 use super::Appearance;
224 use std::process::Command;
225
226 pub fn detect() -> Option<Appearance> {
227 if let Some(a) = gsettings("org.gnome.desktop.interface", "color-scheme") {
229 return Some(a);
230 }
231 if let Some(a) = kde_kdeglobals() {
234 return Some(a);
235 }
236 None
238 }
239
240 fn gsettings(schema: &str, key: &str) -> Option<Appearance> {
241 let out = Command::new("gsettings")
242 .args(["get", schema, key])
243 .output()
244 .ok()?;
245 if !out.status.success() {
246 return None;
247 }
248 let s = String::from_utf8_lossy(&out.stdout).to_lowercase();
249 if s.contains("dark") {
250 Some(Appearance::Dark)
251 } else if s.contains("light") || s.contains("default") {
252 Some(Appearance::Light)
253 } else {
254 None
255 }
256 }
257
258 fn kde_kdeglobals() -> Option<Appearance> {
259 let home = dirs::config_dir()?;
260 let path = home.join("kdeglobals");
261 let text = std::fs::read_to_string(path).ok()?;
262 for line in text.lines() {
263 if let Some(v) = line.strip_prefix("ColorScheme=") {
264 let v = v.trim().to_lowercase();
265 return Some(if v.contains("dark") {
266 Appearance::Dark
267 } else {
268 Appearance::Light
269 });
270 }
271 }
272 None
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
284 fn theme_choice_parses_and_resolves() {
285 assert_eq!(ThemeChoice::from_config_str("dark"), ThemeChoice::Dark);
286 assert_eq!(ThemeChoice::from_config_str("LIGHT"), ThemeChoice::Light);
287 assert_eq!(ThemeChoice::from_config_str("auto"), ThemeChoice::Auto);
288 assert_eq!(ThemeChoice::from_config_str(""), ThemeChoice::Auto);
290 assert_eq!(
291 ThemeChoice::from_config_str("high-contrast"),
292 ThemeChoice::Auto
293 );
294
295 assert_eq!(
297 ThemeChoice::Auto.resolve(Appearance::Dark),
298 Appearance::Dark
299 );
300 assert_eq!(
301 ThemeChoice::Auto.resolve(Appearance::Light),
302 Appearance::Light
303 );
304 assert_eq!(
305 ThemeChoice::Auto.resolve(Appearance::Unknown),
306 Appearance::Dark
307 );
308 assert_eq!(
309 ThemeChoice::Dark.resolve(Appearance::Light),
310 Appearance::Dark
311 );
312 assert_eq!(
313 ThemeChoice::Light.resolve(Appearance::Dark),
314 Appearance::Light
315 );
316
317 for choice in [ThemeChoice::Auto, ThemeChoice::Dark, ThemeChoice::Light] {
319 assert_eq!(ThemeChoice::from_config_str(choice.as_str()), choice);
320 }
321 }
322
323 #[test]
324 fn colorfgbg_parsing() {
325 unsafe {
328 std::env::set_var("COLORFGBG", "15;0");
329 assert_eq!(from_colorfgbg(), Some(Appearance::Dark), "bg=0 -> dark");
330
331 std::env::set_var("COLORFGBG", "0;15");
332 assert_eq!(from_colorfgbg(), Some(Appearance::Light), "bg=15 -> light");
333
334 std::env::set_var("COLORFGBG", "7;8");
336 assert_eq!(from_colorfgbg(), Some(Appearance::Dark), "bg=8 -> dark");
337
338 std::env::set_var("COLORFGBG", "nonsense");
340 assert_eq!(from_colorfgbg(), None);
341
342 std::env::remove_var("COLORFGBG");
343 assert_eq!(from_colorfgbg(), None);
344 }
345 }
346}