ashpd/desktop/
settings.rs

1//! ```rust,no_run
2//! use ashpd::desktop::settings::Settings;
3//! use futures_util::StreamExt;
4//!
5//! async fn run() -> ashpd::Result<()> {
6//!     let proxy = Settings::new().await?;
7//!
8//!     let clock_format = proxy
9//!         .read::<String>("org.gnome.desktop.interface", "clock-format")
10//!         .await?;
11//!     println!("{:#?}", clock_format);
12//!
13//!     let settings = proxy.read_all(&["org.gnome.desktop.interface"]).await?;
14//!     println!("{:#?}", settings);
15//!
16//!     let setting = proxy
17//!         .receive_setting_changed()
18//!         .await?
19//!         .next()
20//!         .await
21//!         .expect("Stream exhausted");
22//!     println!("{}", setting.namespace());
23//!     println!("{}", setting.key());
24//!     println!("{:#?}", setting.value());
25//!
26//!     Ok(())
27//! }
28//! ```
29
30use std::{collections::HashMap, convert::TryFrom, fmt::Debug, future::ready};
31
32use futures_util::{Stream, StreamExt};
33use serde::{Deserialize, Serialize};
34use zbus::zvariant::{OwnedValue, Type, Value};
35
36use crate::{desktop::Color, proxy::Proxy, Error};
37
38/// A HashMap of the <key, value> settings found on a specific namespace.
39pub type Namespace = HashMap<String, OwnedValue>;
40
41#[derive(Deserialize, Type)]
42/// A specific `namespace.key = value` setting.
43pub struct Setting(String, String, OwnedValue);
44
45impl Setting {
46    /// The setting namespace.
47    pub fn namespace(&self) -> &str {
48        &self.0
49    }
50
51    /// The setting key.
52    pub fn key(&self) -> &str {
53        &self.1
54    }
55
56    /// The setting value.
57    pub fn value(&self) -> &OwnedValue {
58        &self.2
59    }
60}
61
62impl std::fmt::Debug for Setting {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Setting")
65            .field("namespace", &self.namespace())
66            .field("key", &self.key())
67            .field("value", self.value())
68            .finish()
69    }
70}
71
72/// The system's preferred color scheme
73#[cfg_attr(feature = "glib", derive(glib::Enum))]
74#[cfg_attr(feature = "glib", enum_type(name = "AshpdColorScheme"))]
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
76pub enum ColorScheme {
77    /// No preference
78    #[default]
79    NoPreference,
80    /// Prefers dark appearance
81    PreferDark,
82    /// Prefers light appearance
83    PreferLight,
84}
85
86impl From<ColorScheme> for OwnedValue {
87    fn from(value: ColorScheme) -> Self {
88        match value {
89            ColorScheme::PreferDark => 1,
90            ColorScheme::PreferLight => 2,
91            _ => 0,
92        }
93        .into()
94    }
95}
96
97impl TryFrom<OwnedValue> for ColorScheme {
98    type Error = Error;
99
100    fn try_from(value: OwnedValue) -> Result<Self, Self::Error> {
101        TryFrom::<Value>::try_from(value.into())
102    }
103}
104
105impl TryFrom<Value<'_>> for ColorScheme {
106    type Error = Error;
107
108    fn try_from(value: Value) -> Result<Self, Self::Error> {
109        Ok(match u32::try_from(value)? {
110            1 => Self::PreferDark,
111            2 => Self::PreferLight,
112            _ => Self::NoPreference,
113        })
114    }
115}
116
117/// The system's preferred contrast level
118#[cfg_attr(feature = "glib", derive(glib::Enum))]
119#[cfg_attr(feature = "glib", enum_type(name = "AshpdContrast"))]
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
121pub enum Contrast {
122    /// No preference
123    #[default]
124    NoPreference,
125    /// Higher contrast
126    High,
127}
128
129impl From<Contrast> for OwnedValue {
130    fn from(value: Contrast) -> Self {
131        match value {
132            Contrast::High => 1,
133            _ => 0,
134        }
135        .into()
136    }
137}
138
139impl TryFrom<OwnedValue> for Contrast {
140    type Error = Error;
141
142    fn try_from(value: OwnedValue) -> Result<Self, Self::Error> {
143        TryFrom::<Value>::try_from(value.into())
144    }
145}
146
147impl TryFrom<Value<'_>> for Contrast {
148    type Error = Error;
149
150    fn try_from(value: Value) -> Result<Self, Self::Error> {
151        Ok(match u32::try_from(value)? {
152            1 => Self::High,
153            _ => Self::NoPreference,
154        })
155    }
156}
157
158/// Appearance namespace
159pub const APPEARANCE_NAMESPACE: &str = "org.freedesktop.appearance";
160/// Color scheme key
161pub const COLOR_SCHEME_KEY: &str = "color-scheme";
162/// Accent color key
163pub const ACCENT_COLOR_SCHEME_KEY: &str = "accent-color";
164/// Contrast key
165pub const CONTRAST_KEY: &str = "contrast";
166
167/// The interface provides read-only access to a small number of host settings
168/// required for toolkits similar to XSettings. It is not for general purpose
169/// settings.
170///
171/// Wrapper of the DBus interface: [`org.freedesktop.portal.Settings`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html).
172#[derive(Debug)]
173#[doc(alias = "org.freedesktop.portal.Settings")]
174pub struct Settings<'a>(Proxy<'a>);
175
176impl<'a> Settings<'a> {
177    /// Create a new instance of [`Settings`].
178    pub async fn new() -> Result<Settings<'a>, Error> {
179        let proxy = Proxy::new_desktop("org.freedesktop.portal.Settings").await?;
180        Ok(Self(proxy))
181    }
182
183    /// Reads a single value. Returns an error on any unknown namespace or key.
184    ///
185    /// # Arguments
186    ///
187    /// * `namespaces` - List of namespaces to filter results by.
188    ///
189    /// If `namespaces` is an empty array or contains an empty string it matches
190    /// all. Globing is supported but only for trailing sections, e.g.
191    /// `org.example.*`.
192    ///
193    /// # Returns
194    ///
195    /// A `HashMap` of namespaces to its keys and values.
196    ///
197    /// # Specifications
198    ///
199    /// See also [`ReadAll`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html#org-freedesktop-portal-settings-readall).
200    #[doc(alias = "ReadAll")]
201    pub async fn read_all(
202        &self,
203        namespaces: &[impl AsRef<str> + Type + Serialize + Debug],
204    ) -> Result<HashMap<String, Namespace>, Error> {
205        self.0.call("ReadAll", &(namespaces)).await
206    }
207
208    /// Reads a single value. Returns an error on any unknown namespace or key.
209    ///
210    /// # Arguments
211    ///
212    /// * `namespace` - Namespace to look up key in.
213    /// * `key` - The key to get.
214    ///
215    /// # Returns
216    ///
217    /// The value for `key` as a `zvariant::OwnedValue`.
218    ///
219    /// # Specifications
220    ///
221    /// See also [`Read`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html#org-freedesktop-portal-settings-read).
222    #[doc(alias = "Read")]
223    #[doc(alias = "ReadOne")]
224    pub async fn read<T>(&self, namespace: &str, key: &str) -> Result<T, Error>
225    where
226        T: TryFrom<OwnedValue>,
227        Error: From<<T as TryFrom<OwnedValue>>::Error>,
228    {
229        let value = self.0.call::<OwnedValue>("Read", &(namespace, key)).await?;
230        if let Ok(v) = value.downcast_ref::<Value>() {
231            T::try_from(v.try_to_owned()?).map_err(From::from)
232        } else {
233            T::try_from(value).map_err(From::from)
234        }
235    }
236
237    /// Retrieves the system's preferred accent color
238    pub async fn accent_color(&self) -> Result<Color, Error> {
239        self.read::<(f64, f64, f64)>(APPEARANCE_NAMESPACE, ACCENT_COLOR_SCHEME_KEY)
240            .await
241            .map(Color::from)
242    }
243
244    /// Retrieves the system's preferred color scheme
245    pub async fn color_scheme(&self) -> Result<ColorScheme, Error> {
246        self.read::<ColorScheme>(APPEARANCE_NAMESPACE, COLOR_SCHEME_KEY)
247            .await
248    }
249
250    /// Retrieves the system's preferred contrast level
251    pub async fn contrast(&self) -> Result<Contrast, Error> {
252        self.read::<Contrast>(APPEARANCE_NAMESPACE, CONTRAST_KEY)
253            .await
254    }
255
256    /// Listen to changes of the system's preferred color scheme
257    pub async fn receive_color_scheme_changed(
258        &self,
259    ) -> Result<impl Stream<Item = ColorScheme>, Error> {
260        Ok(self
261            .receive_setting_changed_with_args(APPEARANCE_NAMESPACE, COLOR_SCHEME_KEY)
262            .await?
263            .filter_map(|t| ready(t.ok())))
264    }
265
266    /// Listen to changes of the system's accent color
267    pub async fn receive_accent_color_changed(&self) -> Result<impl Stream<Item = Color>, Error> {
268        Ok(self
269            .receive_setting_changed_with_args::<(f64, f64, f64)>(
270                APPEARANCE_NAMESPACE,
271                ACCENT_COLOR_SCHEME_KEY,
272            )
273            .await?
274            .filter_map(|t| ready(t.ok().map(Color::from))))
275    }
276
277    /// Listen to changes of the system's contrast level
278    pub async fn receive_contrast_changed(&self) -> Result<impl Stream<Item = Contrast>, Error> {
279        Ok(self
280            .receive_setting_changed_with_args(APPEARANCE_NAMESPACE, CONTRAST_KEY)
281            .await?
282            .filter_map(|t| ready(t.ok())))
283    }
284
285    /// Signal emitted when a setting changes.
286    ///
287    /// # Specifications
288    ///
289    /// See also [`SettingChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html#org-freedesktop-portal-settings-settingchanged).
290    #[doc(alias = "SettingChanged")]
291    pub async fn receive_setting_changed(&self) -> Result<impl Stream<Item = Setting>, Error> {
292        self.0.signal("SettingChanged").await
293    }
294
295    /// Similar to [Self::receive_setting_changed]
296    /// but allows you to filter specific settings.
297    ///
298    /// # Example
299    /// ```rust,no_run
300    /// use ashpd::desktop::settings::{ColorScheme, Settings};
301    /// use futures_util::StreamExt;
302    ///
303    /// # async fn run() -> ashpd::Result<()> {
304    /// let settings = Settings::new().await?;
305    /// while let Some(Ok(scheme)) = settings
306    ///     .receive_setting_changed_with_args::<ColorScheme>(
307    ///         "org.freedesktop.appearance",
308    ///         "color-scheme",
309    ///     )
310    ///     .await?
311    ///     .next()
312    ///     .await
313    /// {
314    ///     println!("{:#?}", scheme);
315    /// }
316    /// #    Ok(())
317    /// # }
318    /// ```
319    pub async fn receive_setting_changed_with_args<T>(
320        &self,
321        namespace: &str,
322        key: &str,
323    ) -> Result<impl Stream<Item = Result<T, Error>>, Error>
324    where
325        T: TryFrom<OwnedValue>,
326        Error: From<<T as TryFrom<OwnedValue>>::Error>,
327    {
328        Ok(self
329            .0
330            .signal_with_args::<Setting>("SettingChanged", &[(0, namespace), (1, key)])
331            .await?
332            .map(|x| T::try_from(x.2).map_err(From::from)))
333    }
334}
335
336impl<'a> std::ops::Deref for Settings<'a> {
337    type Target = zbus::Proxy<'a>;
338
339    fn deref(&self) -> &Self::Target {
340        &self.0
341    }
342}