#[cfg(target_os = "linux")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scheme {
Dark,
Light,
}
#[cfg(target_os = "linux")]
#[must_use]
pub fn scheme_of(color_scheme: u32) -> Scheme {
match color_scheme {
1 => Scheme::Dark,
_ => Scheme::Light,
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::{Scheme, scheme_of};
const PORTAL: &str = "org.freedesktop.portal.Desktop";
const PATH: &str = "/org/freedesktop/portal/desktop";
const SETTINGS: &str = "org.freedesktop.portal.Settings";
const NAMESPACE: &str = "org.freedesktop.appearance";
const KEY: &str = "color-scheme";
fn number_inside(value: &zbus::zvariant::Value<'_>) -> Option<u32> {
let mut here = value;
for _ in 0..4 {
match here {
zbus::zvariant::Value::U32(n) => return Some(*n),
zbus::zvariant::Value::Value(inner) => here = inner,
_ => return None,
}
}
None
}
fn proxy(
connection: &zbus::blocking::Connection,
) -> Result<zbus::blocking::Proxy<'static>, zbus::Error> {
zbus::blocking::Proxy::new(connection, PORTAL, PATH, SETTINGS)
}
pub fn ask() -> Option<Scheme> {
let connection = zbus::blocking::Connection::session().ok()?;
let proxy = proxy(&connection).ok()?;
let value: zbus::zvariant::OwnedValue =
proxy.call("Read", &(NAMESPACE, KEY)).ok()?;
number_inside(&value).map(scheme_of)
}
pub fn watch(ctx: &eframe::egui::Context, mut apply: impl FnMut(Scheme) + Send + 'static) {
let ctx = ctx.clone();
std::thread::Builder::new()
.name("filebase-theme".to_owned())
.spawn(move || {
let Ok(connection) = zbus::blocking::Connection::session() else {
return;
};
let Ok(proxy) = proxy(&connection) else { return };
let Ok(changes) = proxy.receive_signal("SettingChanged") else {
return;
};
for message in changes {
let body = message.body();
let Ok((namespace, key, value)) =
body.deserialize::<(String, String, zbus::zvariant::Value<'_>)>()
else {
continue;
};
if namespace != NAMESPACE || key != KEY {
continue;
}
if let Some(number) = number_inside(&value) {
apply(scheme_of(number));
ctx.request_repaint();
}
}
})
.ok();
}
}
#[cfg(target_os = "linux")]
pub fn follow(ctx: &eframe::egui::Context) {
use eframe::egui::ThemePreference;
fn preference(scheme: Scheme) -> ThemePreference {
match scheme {
Scheme::Dark => ThemePreference::Dark,
Scheme::Light => ThemePreference::Light,
}
}
if let Some(scheme) = linux::ask() {
ctx.set_theme(preference(scheme));
}
let watcher = ctx.clone();
linux::watch(ctx, move |scheme| {
watcher.set_theme(preference(scheme));
});
}
#[cfg(not(target_os = "linux"))]
pub fn follow(_ctx: &eframe::egui::Context) {}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::{Scheme, scheme_of};
#[test]
fn no_preference_is_light_because_that_is_what_gnome_sends_for_light() {
assert_eq!(scheme_of(0), Scheme::Light);
}
#[test]
fn one_is_dark_and_two_is_light() {
assert_eq!(scheme_of(1), Scheme::Dark);
assert_eq!(scheme_of(2), Scheme::Light);
}
#[test]
fn a_value_the_specification_does_not_define_is_light() {
for unknown in [3, 4, 99, u32::MAX] {
assert_eq!(
scheme_of(unknown),
Scheme::Light,
"color-scheme {unknown} should not darken the window"
);
}
}
}