is_dark_theme/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//! Check whether the system is set to a "dark" theme.
//!
//! This checks a *global* system default. It is intended for headless tools or programs that can't access their windows.
//! It may not be accurate when specific screens or applications have a different theme set.
//! If you control your GUI, please check window-specific properties instead (on macOS that is `NSAppearance` protocol).
//!
//! On macOS this crate uses Core Foundation to read a `AppleInterfaceStyle` global setting, which is equivalent of:
//!
//! ```bash
//! defaults read -g AppleInterfaceStyle
//! ```
//!
//! On other platforms only `None` is returned. Please submit pull requests for more OSes!

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Theme {
    /// Standard appearance (possibly light), or themes are not supported
    Default,

    /// The theme has a "Dark" appearance (light text on dark background)
    Dark,
}

/// Reads the current system-wide default setting for application theme.
///
/// Returns `None` if the platform is not supported.
pub fn global_default_theme() -> Option<Theme> {
    #[cfg(target_vendor = "apple")]
    return crate::apple::get();

    #[cfg(not(target_vendor = "apple"))]
    return None;
}

#[cfg(target_vendor = "apple")]
mod apple {
    use crate::Theme;
    use core_foundation_sys::preferences::kCFPreferencesAnyApplication;
    use core::ptr;
    use core_foundation_sys::base::CFRelease;
    use core_foundation_sys::base::kCFAllocatorNull;
    use core_foundation_sys::preferences::CFPreferencesCopyAppValue;
    use core_foundation_sys::string::CFStringCreateWithBytesNoCopy;
    use core_foundation_sys::string::CFStringHasPrefix;
    use core_foundation_sys::string::CFStringRef;
    use core_foundation_sys::string::kCFStringEncodingUTF8;

    fn static_cf_string(string: &'static str) -> CFStringRef {
        unsafe {
            CFStringCreateWithBytesNoCopy(
                ptr::null_mut(),
                string.as_ptr(),
                string.len() as _,
                kCFStringEncodingUTF8,
                false as _,
                kCFAllocatorNull,
            )
        }
    }

    pub(crate) fn get() -> Option<Theme> {
        #[cfg(target_vendor = "apple")]
        unsafe {
            let interface_style = static_cf_string("AppleInterfaceStyle");
            if interface_style.is_null() {
                return None;
            }
            let dark = static_cf_string("Dark");

            let value = CFPreferencesCopyAppValue(interface_style, kCFPreferencesAnyApplication);
            let is_dark = !value.is_null() && !dark.is_null() && 0 != CFStringHasPrefix(value.cast(), dark);

            CFRelease(dark.cast());
            CFRelease(interface_style.cast());

            Some(if is_dark { Theme::Dark } else { Theme::Default })
        }
    }
}

#[test]
fn test() {
    #[cfg(target_vendor = "apple")]
    assert!(global_default_theme().is_some());

    global_default_theme();
}