Skip to main content

gitkraft_core/features/theme/
system.rs

1//! "Follow system" appearance: detect the OS dark/light setting and map it to a preset.
2
3/// Persisted `theme_name` meaning "follow the operating system's light/dark setting".
4pub const SYSTEM_THEME_NAME: &str = "System";
5
6/// Preset used while the system is in dark mode (index into `THEME_NAMES`).
7pub const SYSTEM_DARK_INDEX: usize = 0; // "Default"
8/// Preset used while the system is in light mode (index into `THEME_NAMES`).
9pub const SYSTEM_LIGHT_INDEX: usize = 1; // "Default Light"
10
11/// Whether `name` is the "follow system" pseudo-theme (case-insensitive).
12pub fn is_system_name(name: &str) -> bool {
13    name.eq_ignore_ascii_case(SYSTEM_THEME_NAME)
14}
15
16/// The preset index to show for a given system appearance.
17pub fn system_theme_index(dark: bool) -> usize {
18    if dark {
19        SYSTEM_DARK_INDEX
20    } else {
21        SYSTEM_LIGHT_INDEX
22    }
23}
24
25/// Parse `defaults read -g AppleInterfaceStyle` output (macOS): prints `Dark` in dark mode.
26pub fn parse_macos(out: &str) -> bool {
27    out.trim().eq_ignore_ascii_case("dark")
28}
29
30/// Parse `gsettings get org.gnome.desktop.interface color-scheme` (`'prefer-dark'`, …).
31pub fn parse_gsettings(out: &str) -> Option<bool> {
32    let v = out.trim().trim_matches('\'');
33    match v {
34        "prefer-dark" => Some(true),
35        "prefer-light" | "default" => Some(false),
36        _ => None,
37    }
38}
39
40/// Parse `reg query … /v AppsUseLightTheme` output (Windows): `0x0` means dark.
41pub fn parse_windows(out: &str) -> Option<bool> {
42    let v = out.split_whitespace().last()?;
43    match v {
44        "0x0" => Some(true),
45        "0x1" => Some(false),
46        _ => None,
47    }
48}
49
50#[allow(dead_code)]
51fn run(cmd: &str, args: &[&str]) -> Option<String> {
52    let o = std::process::Command::new(cmd).args(args).output().ok()?;
53    Some(String::from_utf8_lossy(&o.stdout).into_owned())
54}
55
56/// Is the OS in dark mode? `None` when it can't be determined (callers pick a default).
57pub fn detect_system_dark() -> Option<bool> {
58    #[cfg(target_os = "macos")]
59    {
60        // Key is absent in light mode (command fails, empty stdout).
61        Some(parse_macos(
62            &run("defaults", &["read", "-g", "AppleInterfaceStyle"]).unwrap_or_default(),
63        ))
64    }
65    #[cfg(target_os = "windows")]
66    {
67        parse_windows(&run(
68            "reg",
69            &[
70                "query",
71                r"HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
72                "/v",
73                "AppsUseLightTheme",
74            ],
75        )?)
76    }
77    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
78    {
79        if let Some(d) = run(
80            "gsettings",
81            &["get", "org.gnome.desktop.interface", "color-scheme"],
82        )
83        .and_then(|o| parse_gsettings(&o))
84        {
85            return Some(d);
86        }
87        std::env::var("GTK_THEME")
88            .ok()
89            .map(|t| t.to_ascii_lowercase().contains("dark"))
90    }
91}
92
93/// Preset index matching the current system appearance (dark if undetectable).
94pub fn current_system_theme_index() -> usize {
95    system_theme_index(detect_system_dark().unwrap_or(true))
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn system_name_is_case_insensitive_and_not_a_preset() {
104        assert!(is_system_name("System") && is_system_name("system"));
105        assert!(!is_system_name("Nord"));
106        assert!(!super::super::THEME_NAMES.iter().any(|n| is_system_name(n)));
107    }
108
109    #[test]
110    fn dark_and_light_map_to_matching_presets() {
111        assert!(super::super::theme_by_index(system_theme_index(true)).is_dark);
112        assert!(!super::super::theme_by_index(system_theme_index(false)).is_dark);
113    }
114
115    #[test]
116    fn parsers() {
117        assert!(parse_macos("Dark\n"));
118        assert!(!parse_macos(""));
119        assert_eq!(parse_gsettings("'prefer-dark'\n"), Some(true));
120        assert_eq!(parse_gsettings("'default'"), Some(false));
121        assert_eq!(parse_gsettings("garbage"), None);
122        assert_eq!(
123            parse_windows("    AppsUseLightTheme    REG_DWORD    0x0\r\n"),
124            Some(true)
125        );
126        assert_eq!(
127            parse_windows("AppsUseLightTheme REG_DWORD 0x1"),
128            Some(false)
129        );
130        assert_eq!(parse_windows(""), None);
131    }
132}