system_theme/platform/
xdg.rs1use std::{
2 sync::{Arc, LazyLock, OnceLock},
3 thread::{self, JoinHandle},
4};
5use tokio::sync::Notify;
6use zbus::{
7 blocking::{fdo::DBusProxy, Connection, Proxy},
8 names::BusName,
9 zvariant::OwnedValue,
10};
11
12use crate::{error::Error, ThemeColor, ThemeContrast, ThemeKind, ThemeScheme};
13
14const DESKTOP_PORTAL_DEST: &str = "org.freedesktop.portal.Desktop";
15const DESKTOP_PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
16const SETTINGS_INTERFACE: &str = "org.freedesktop.portal.Settings";
17const READ_METHOD: &str = "ReadOne";
18const CHANGE_SIGNAL: &str = "SettingChanged";
19const APPERANCE_NAMESPACE: &str = "org.freedesktop.appearance";
20
21const COLOR_SCHEME_KEY: &str = "color-scheme";
22const CONTRAST_KEY: &str = "contrast";
23const ACCENT_COLOR_KEY: &str = "accent-color";
24
25const PORTAL_NOT_FOUND: &str = "org.freedesktop.portal.Error.NotFound";
26const DBUS_UNKNOWN_SERVICE: &str = "org.freedesktop.DBus.Error.ServiceUnknown";
27const DBUS_UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
28
29const GTK_PORTAL_IMPL: &str = "org.freedesktop.impl.portal.desktop.gtk";
30
31static WATCHER_NOTIFY: LazyLock<Arc<Notify>> = LazyLock::new(|| Arc::new(Notify::new()));
32static WATCHER_HANDLE: OnceLock<JoinHandle<()>> = OnceLock::new();
33
34impl From<zbus::Error> for Error {
35 fn from(value: zbus::Error) -> Self {
36 match &value {
37 zbus::Error::InterfaceNotFound => Error::Unsupported,
38 zbus::Error::Unsupported => Error::Unsupported,
39 zbus::Error::MethodError(name, _, _) => {
40 let name_str = name.as_str();
41 if name_str == PORTAL_NOT_FOUND
43 || name_str == DBUS_UNKNOWN_SERVICE
44 || name_str == DBUS_UNKNOWN_METHOD
45 {
46 Error::Unsupported
47 } else {
48 Error::from_platform(value)
49 }
50 }
51 _ => Error::from_platform(value),
52 }
53 }
54}
55
56fn check_color_component(component: f64) -> bool {
58 (0.0..=1.0).contains(&component)
59}
60
61pub struct Platform {
62 conn: Connection,
63}
64
65impl Platform {
66 pub fn new() -> Result<Self, Error> {
67 let conn = Connection::session()?;
68
69 let conn_cloned = conn.clone();
71 if let Ok(proxy) = Proxy::new(
72 &conn_cloned,
73 DESKTOP_PORTAL_DEST,
74 DESKTOP_PORTAL_PATH,
75 SETTINGS_INTERFACE,
76 ) {
77 if let Ok(signal) = proxy.receive_signal(CHANGE_SIGNAL) {
78 WATCHER_HANDLE.get_or_init(|| {
80 thread::spawn(move || {
81 for _ in signal {
82 (*WATCHER_NOTIFY).notify_waiters();
83 }
84 })
85 });
86 }
87 }
88
89 Ok(Self { conn })
90 }
91
92 pub fn theme_kind(&self) -> Result<ThemeKind, Error> {
93 if self.check_has_owner(
94 GTK_PORTAL_IMPL
95 .try_into()
96 .expect("Failed to convert GTK_PORTAL_IMPL"),
97 )? {
98 Ok(ThemeKind::Gtk)
100 } else {
101 Ok(ThemeKind::Qt)
103 }
104 }
105
106 pub fn theme_scheme(&self) -> Result<ThemeScheme, Error> {
107 let scheme: u32 = self.get_settings_apperance(COLOR_SCHEME_KEY)?;
108
109 if scheme == 1 {
111 Ok(ThemeScheme::Dark)
112 } else {
113 Ok(ThemeScheme::Light)
114 }
115 }
116
117 pub fn theme_contrast(&self) -> Result<ThemeContrast, Error> {
118 let contrast: u32 = self.get_settings_apperance(CONTRAST_KEY)?;
119
120 if contrast == 1 {
122 Ok(ThemeContrast::High)
123 } else {
124 Ok(ThemeContrast::Normal)
125 }
126 }
127
128 pub fn theme_accent(&self) -> Result<ThemeColor, Error> {
129 let accent: (f64, f64, f64) = self.get_settings_apperance(ACCENT_COLOR_KEY)?;
130
131 if !check_color_component(accent.0)
133 || !check_color_component(accent.1)
134 || !check_color_component(accent.2)
135 {
136 return Err(Error::Unavailable);
137 }
138
139 Ok(ThemeColor {
140 red: accent.0 as f32,
141 green: accent.1 as f32,
142 blue: accent.2 as f32,
143 })
144 }
145
146 pub fn get_notify(&self) -> Arc<Notify> {
147 (*WATCHER_NOTIFY).clone()
148 }
149
150 fn check_has_owner(&self, name: BusName<'_>) -> Result<bool, Error> {
151 let proxy = DBusProxy::new(&self.conn)?;
152
153 match proxy.get_name_owner(name) {
154 Ok(_) => Ok(true),
155 Err(zbus::fdo::Error::NameHasNoOwner(_)) => Ok(false),
156 Err(e) => Err(Error::from_platform(e)),
157 }
158 }
159
160 fn get_settings_apperance<T: TryFrom<OwnedValue>>(&self, key: &str) -> Result<T, Error> {
161 let response = self.conn.call_method(
163 Some(DESKTOP_PORTAL_DEST),
164 DESKTOP_PORTAL_PATH,
165 Some(SETTINGS_INTERFACE),
166 READ_METHOD,
167 &(APPERANCE_NAMESPACE, key),
168 )?;
169
170 let value = response
172 .body()
173 .deserialize::<OwnedValue>()
174 .map_err(Error::from_platform)?;
175
176 value.try_into().map_err(|_| Error::Unavailable)
178 }
179}