system_theme/platform/
xdg.rs

1use zbus::{
2    blocking::{fdo::DBusProxy, Connection},
3    names::BusName,
4    zvariant::OwnedValue,
5};
6
7use crate::{error::Error, ThemeColor, ThemeContrast, ThemeKind, ThemeScheme};
8
9const DESKTOP_PORTAL_DEST: &str = "org.freedesktop.portal.Desktop";
10const DESKTOP_PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
11const SETTINGS_INTERFACE: &str = "org.freedesktop.portal.Settings";
12const READ_METHOD: &str = "ReadOne";
13const APPERANCE_NAMESPACE: &str = "org.freedesktop.appearance";
14
15const COLOR_SCHEME_KEY: &str = "color-scheme";
16const CONTRAST_KEY: &str = "contrast";
17const ACCENT_COLOR_KEY: &str = "accent-color";
18
19const PORTAL_NOT_FOUND: &str = "org.freedesktop.portal.Error.NotFound";
20const DBUS_UNKNOWN_SERVICE: &str = "org.freedesktop.DBus.Error.ServiceUnknown";
21const DBUS_UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
22
23const GTK_PORTAL_IMPL: &str = "org.freedesktop.impl.portal.desktop.gtk";
24
25impl From<zbus::Error> for Error {
26    fn from(value: zbus::Error) -> Self {
27        match &value {
28            zbus::Error::InterfaceNotFound => Error::Unsupported,
29            zbus::Error::Unsupported => Error::Unsupported,
30            zbus::Error::MethodError(name, _, _) => {
31                let name_str = name.as_str();
32                // Errors that can be returned if not supported by the platform
33                if name_str == PORTAL_NOT_FOUND
34                    || name_str == DBUS_UNKNOWN_SERVICE
35                    || name_str == DBUS_UNKNOWN_METHOD
36                {
37                    Error::Unsupported
38                } else {
39                    Error::from_platform(value)
40                }
41            }
42            _ => Error::from_platform(value),
43        }
44    }
45}
46
47/// Check if color component is valid
48fn check_color_component(component: f64) -> bool {
49    (0.0..=1.0).contains(&component)
50}
51
52pub struct Platform {
53    conn: Connection,
54}
55
56impl Platform {
57    pub fn new() -> Result<Self, Error> {
58        let conn = Connection::session()?;
59        Ok(Self { conn })
60    }
61
62    pub fn theme_kind(&self) -> Result<ThemeKind, Error> {
63        if self.check_has_owner(
64            GTK_PORTAL_IMPL
65                .try_into()
66                .expect("Failed to convert GTK_PORTAL_IMPL"),
67        )? {
68            // If we have GTK Portal, we're using GTK
69            Ok(ThemeKind::Gtk)
70        } else {
71            // Anything else should be Qt
72            Ok(ThemeKind::Qt)
73        }
74    }
75
76    pub fn theme_scheme(&self) -> Result<ThemeScheme, Error> {
77        let scheme: u32 = self.get_settings_apperance(COLOR_SCHEME_KEY)?;
78
79        // 1 = dark, 2 = light
80        match scheme {
81            1 => Ok(ThemeScheme::Dark),
82            2 => Ok(ThemeScheme::Light),
83            _ => Err(Error::Unavailable),
84        }
85    }
86
87    pub fn theme_contrast(&self) -> Result<ThemeContrast, Error> {
88        let contrast: u32 = self.get_settings_apperance(CONTRAST_KEY)?;
89
90        // 0 = normal, 1 = high
91        match contrast {
92            0 => Ok(ThemeContrast::Normal),
93            1 => Ok(ThemeContrast::High),
94            _ => Err(Error::Unavailable),
95        }
96    }
97
98    pub fn theme_accent(&self) -> Result<ThemeColor, Error> {
99        let accent: (f64, f64, f64) = self.get_settings_apperance(ACCENT_COLOR_KEY)?;
100
101        // Check color components range (invalid -> not configured)
102        if !check_color_component(accent.0)
103            || !check_color_component(accent.1)
104            || !check_color_component(accent.2)
105        {
106            return Err(Error::Unavailable);
107        }
108
109        Ok(ThemeColor {
110            red: accent.0 as f32,
111            green: accent.1 as f32,
112            blue: accent.2 as f32,
113        })
114    }
115
116    fn check_has_owner(&self, name: BusName<'_>) -> Result<bool, Error> {
117        let proxy = DBusProxy::new(&self.conn)?;
118
119        match proxy.get_name_owner(name) {
120            Ok(_) => Ok(true),
121            Err(zbus::fdo::Error::NameHasNoOwner(_)) => Ok(false),
122            Err(e) => Err(Error::from_platform(e)),
123        }
124    }
125
126    fn get_settings_apperance<T: TryFrom<OwnedValue>>(&self, key: &str) -> Result<T, Error> {
127        // Call method to read a settings appearance
128        let response = self.conn.call_method(
129            Some(DESKTOP_PORTAL_DEST),
130            DESKTOP_PORTAL_PATH,
131            Some(SETTINGS_INTERFACE),
132            READ_METHOD,
133            &(APPERANCE_NAMESPACE, key),
134        )?;
135
136        // As the result is a variant, convert it to a value first
137        let value = response
138            .body()
139            .deserialize::<OwnedValue>()
140            .map_err(Error::from_platform)?;
141
142        // Now try to convert it to the desired type (invalid -> not configured)
143        value.try_into().map_err(|_| Error::Unavailable)
144    }
145}