use std::{
sync::{Arc, LazyLock, OnceLock},
thread::{self, JoinHandle},
};
use tokio::sync::Notify;
use zbus::{
blocking::{fdo::DBusProxy, Connection, Proxy},
names::BusName,
zvariant::OwnedValue,
};
use crate::{error::Error, ThemeColor, ThemeContrast, ThemeKind, ThemeScheme};
const DESKTOP_PORTAL_DEST: &str = "org.freedesktop.portal.Desktop";
const DESKTOP_PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
const SETTINGS_INTERFACE: &str = "org.freedesktop.portal.Settings";
const READ_METHOD: &str = "ReadOne";
const CHANGE_SIGNAL: &str = "SettingChanged";
const APPERANCE_NAMESPACE: &str = "org.freedesktop.appearance";
const COLOR_SCHEME_KEY: &str = "color-scheme";
const CONTRAST_KEY: &str = "contrast";
const ACCENT_COLOR_KEY: &str = "accent-color";
const PORTAL_NOT_FOUND: &str = "org.freedesktop.portal.Error.NotFound";
const DBUS_UNKNOWN_SERVICE: &str = "org.freedesktop.DBus.Error.ServiceUnknown";
const DBUS_UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
const GTK_PORTAL_IMPL: &str = "org.freedesktop.impl.portal.desktop.gtk";
static WATCHER_NOTIFY: LazyLock<Arc<Notify>> = LazyLock::new(|| Arc::new(Notify::new()));
static WATCHER_HANDLE: OnceLock<JoinHandle<()>> = OnceLock::new();
impl From<zbus::Error> for Error {
fn from(value: zbus::Error) -> Self {
match &value {
zbus::Error::InterfaceNotFound => Error::Unsupported,
zbus::Error::Unsupported => Error::Unsupported,
zbus::Error::MethodError(name, _, _) => {
let name_str = name.as_str();
if name_str == PORTAL_NOT_FOUND
|| name_str == DBUS_UNKNOWN_SERVICE
|| name_str == DBUS_UNKNOWN_METHOD
{
Error::Unsupported
} else {
Error::from_platform(value)
}
}
_ => Error::from_platform(value),
}
}
}
fn check_color_component(component: f64) -> bool {
(0.0..=1.0).contains(&component)
}
pub struct Platform {
conn: Connection,
}
impl Platform {
pub fn new() -> Result<Self, Error> {
let conn = Connection::session()?;
let conn_cloned = conn.clone();
if let Ok(proxy) = Proxy::new(
&conn_cloned,
DESKTOP_PORTAL_DEST,
DESKTOP_PORTAL_PATH,
SETTINGS_INTERFACE,
) {
if let Ok(signal) = proxy.receive_signal(CHANGE_SIGNAL) {
WATCHER_HANDLE.get_or_init(|| {
thread::spawn(move || {
for _ in signal {
(*WATCHER_NOTIFY).notify_waiters();
}
})
});
}
}
Ok(Self { conn })
}
pub fn theme_kind(&self) -> Result<ThemeKind, Error> {
if self.check_has_owner(
GTK_PORTAL_IMPL
.try_into()
.expect("Failed to convert GTK_PORTAL_IMPL"),
)? {
Ok(ThemeKind::Gtk)
} else {
Ok(ThemeKind::Qt)
}
}
pub fn theme_scheme(&self) -> Result<ThemeScheme, Error> {
let scheme: u32 = self.get_settings_apperance(COLOR_SCHEME_KEY)?;
if scheme == 1 {
Ok(ThemeScheme::Dark)
} else {
Ok(ThemeScheme::Light)
}
}
pub fn theme_contrast(&self) -> Result<ThemeContrast, Error> {
let contrast: u32 = self.get_settings_apperance(CONTRAST_KEY)?;
if contrast == 1 {
Ok(ThemeContrast::High)
} else {
Ok(ThemeContrast::Normal)
}
}
pub fn theme_accent(&self) -> Result<ThemeColor, Error> {
let accent: (f64, f64, f64) = self.get_settings_apperance(ACCENT_COLOR_KEY)?;
if !check_color_component(accent.0)
|| !check_color_component(accent.1)
|| !check_color_component(accent.2)
{
return Err(Error::Unavailable);
}
Ok(ThemeColor {
red: accent.0 as f32,
green: accent.1 as f32,
blue: accent.2 as f32,
})
}
pub fn get_notify(&self) -> Arc<Notify> {
(*WATCHER_NOTIFY).clone()
}
fn check_has_owner(&self, name: BusName<'_>) -> Result<bool, Error> {
let proxy = DBusProxy::new(&self.conn)?;
match proxy.get_name_owner(name) {
Ok(_) => Ok(true),
Err(zbus::fdo::Error::NameHasNoOwner(_)) => Ok(false),
Err(e) => Err(Error::from_platform(e)),
}
}
fn get_settings_apperance<T: TryFrom<OwnedValue>>(&self, key: &str) -> Result<T, Error> {
let response = self.conn.call_method(
Some(DESKTOP_PORTAL_DEST),
DESKTOP_PORTAL_PATH,
Some(SETTINGS_INTERFACE),
READ_METHOD,
&(APPERANCE_NAMESPACE, key),
)?;
let value = response
.body()
.deserialize::<OwnedValue>()
.map_err(Error::from_platform)?;
value.try_into().map_err(|_| Error::Unavailable)
}
}