is_dark_theme/
lib.rs

1//! Check whether the system is set to a "dark" theme.
2//!
3//! This checks a *global* system default. It is intended for headless tools or programs that can't access their windows.
4//! It may not be accurate when specific screens or applications have a different theme set.
5//! If you control your GUI, please check window-specific properties instead (on macOS that is `NSAppearance` protocol).
6//!
7//! On macOS this crate uses Core Foundation to read a `AppleInterfaceStyle` global setting, which is equivalent of:
8//!
9//! ```bash
10//! defaults read -g AppleInterfaceStyle
11//! ```
12//!
13//! On other platforms only `None` is returned. Please submit pull requests for more OSes!
14
15#[derive(Debug, Copy, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum Theme {
18    /// Standard appearance (possibly light), or themes are not supported
19    Default,
20
21    /// The theme has a "Dark" appearance (light text on dark background)
22    Dark,
23}
24
25/// Reads the current system-wide default setting for application theme.
26///
27/// Returns `None` if the platform is not supported.
28pub fn global_default_theme() -> Option<Theme> {
29    #[cfg(target_vendor = "apple")]
30    return crate::apple::get();
31
32    #[cfg(not(target_vendor = "apple"))]
33    return None;
34}
35
36#[cfg(target_vendor = "apple")]
37mod apple {
38    use crate::Theme;
39    use core_foundation_sys::preferences::kCFPreferencesAnyApplication;
40    use core::ptr;
41    use core_foundation_sys::base::CFRelease;
42    use core_foundation_sys::base::kCFAllocatorNull;
43    use core_foundation_sys::preferences::CFPreferencesCopyAppValue;
44    use core_foundation_sys::string::CFStringCreateWithBytesNoCopy;
45    use core_foundation_sys::string::CFStringHasPrefix;
46    use core_foundation_sys::string::CFStringRef;
47    use core_foundation_sys::string::kCFStringEncodingUTF8;
48
49    fn static_cf_string(string: &'static str) -> CFStringRef {
50        unsafe {
51            CFStringCreateWithBytesNoCopy(
52                ptr::null_mut(),
53                string.as_ptr(),
54                string.len() as _,
55                kCFStringEncodingUTF8,
56                false as _,
57                kCFAllocatorNull,
58            )
59        }
60    }
61
62    pub(crate) fn get() -> Option<Theme> {
63        #[cfg(target_vendor = "apple")]
64        unsafe {
65            let interface_style = static_cf_string("AppleInterfaceStyle");
66            if interface_style.is_null() {
67                return None;
68            }
69            let dark = static_cf_string("Dark");
70
71            let value = CFPreferencesCopyAppValue(interface_style, kCFPreferencesAnyApplication);
72            let is_dark = !value.is_null() && !dark.is_null() && 0 != CFStringHasPrefix(value.cast(), dark);
73
74            CFRelease(dark.cast());
75            CFRelease(interface_style.cast());
76
77            Some(if is_dark { Theme::Dark } else { Theme::Default })
78        }
79    }
80}
81
82#[test]
83fn test() {
84    #[cfg(target_vendor = "apple")]
85    assert!(global_default_theme().is_some());
86
87    global_default_theme();
88}