Skip to main content

dark_light/platforms/
freedesktop.rs

1use std::sync::mpsc;
2use std::thread::JoinHandle;
3
4use crate::{Error, Mode, Watcher};
5use zbus::proxy;
6use zbus::zvariant::OwnedValue;
7
8const APPEARANCE: &str = "org.freedesktop.appearance";
9const COLOR_SCHEME: &str = "color-scheme";
10const PORTAL_DESTINATION: &str = "org.freedesktop.portal.Desktop";
11
12#[proxy(
13    interface = "org.freedesktop.portal.Settings",
14    default_path = "/org/freedesktop/portal/desktop"
15)]
16pub trait XdgPortalSettings {
17    fn read_one(&self, namespace: &str, key: &str) -> zbus::Result<OwnedValue>;
18
19    #[zbus(signal)]
20    fn setting_changed(&self, namespace: &str, key: &str, value: OwnedValue) -> zbus::Result<()>;
21}
22
23pub fn detect() -> Result<Mode, Error> {
24    let conn = zbus::blocking::Connection::session()
25        .map_err(|e| Error::XdgDesktopPortal(e.to_string()))?;
26    let portal = XdgPortalSettingsProxyBlocking::new(&conn, PORTAL_DESTINATION)
27        .map_err(|e| Error::XdgDesktopPortal(e.to_string()))?;
28    let mode_value: u32 = portal
29        .read_one(APPEARANCE, COLOR_SCHEME)
30        .map_err(|e| Error::XdgDesktopPortal(e.to_string()))?
31        .try_into()
32        .map_err(|_| Error::XdgDesktopPortal("type convert failed".to_string()))?;
33    Ok(match mode_value {
34        1 => Mode::Dark,
35        2 => Mode::Light,
36        _ => Mode::Unspecified,
37    })
38}
39
40/// Background handle for a Linux/BSD theme watcher.
41///
42/// The watcher thread blocks on the D-Bus session connection's signal stream, which has
43/// no external cancellation mechanism; it is detached rather than joined on drop and will
44/// exit the next time it observes that the channel receiver has been dropped.
45pub struct WatchGuard(#[allow(dead_code)] Option<JoinHandle<()>>);
46
47pub fn subscribe() -> Result<Watcher, Error> {
48    let (sender, receiver) = mpsc::channel();
49
50    let conn = zbus::blocking::Connection::session()
51        .map_err(|e| Error::XdgDesktopPortal(e.to_string()))?;
52    let portal = XdgPortalSettingsProxyBlocking::new(&conn, PORTAL_DESTINATION)
53        .map_err(|e| Error::XdgDesktopPortal(e.to_string()))?;
54    let changes = portal
55        .receive_setting_changed()
56        .map_err(|e| Error::XdgDesktopPortal(e.to_string()))?;
57
58    let handle = std::thread::spawn(move || {
59        let mut last = None;
60        for signal in changes {
61            let Ok(args) = signal.args() else { continue };
62            if args.namespace != APPEARANCE || args.key != COLOR_SCHEME {
63                continue;
64            }
65            let Ok(value): Result<u32, _> = args.value.try_into() else {
66                continue;
67            };
68            let mode = match value {
69                1 => Mode::Dark,
70                2 => Mode::Light,
71                _ => Mode::Unspecified,
72            };
73            if Some(mode) == last {
74                continue;
75            }
76            last = Some(mode);
77            if sender.send(mode).is_err() {
78                break;
79            }
80        }
81    });
82
83    Ok(Watcher {
84        receiver,
85        guard: WatchGuard(Some(handle)),
86    })
87}