system_theme/platform/
xdg.rs1use 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 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
47fn 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 Ok(ThemeKind::Gtk)
70 } else {
71 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 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 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 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 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 let value = response
138 .body()
139 .deserialize::<OwnedValue>()
140 .map_err(Error::from_platform)?;
141
142 value.try_into().map_err(|_| Error::Unavailable)
144 }
145}