diff --git a/Cargo.toml b/Cargo.toml index 0ad97cf..381c1f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,19 +10,18 @@ readme = "README.md" build = "build.rs" [dependencies] -futures = "0.3.30" -anyhow = "1.0.79" +futures = "0.3" [dev-dependencies] -tokio = { version = "1.23.0", features = ["full"] } +tokio = { version = "1.40.0", features = ["full"] } [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies] -detect-desktop-environment = "1.0.0" +detect-desktop-environment = "1.1.0" dconf_rs = "0.3" -zbus = "3.0" -rust-ini = "0.20" -ashpd = "0.7.0" -xdg = "2.4.1" +zbus = { version = "4.4", optional = true } +ashpd = { version = "0.9", optional = true } +rust-ini = "0.21" +xdg = "2.5" [target.'cfg(windows)'.dependencies] winreg = "0.52.0" @@ -32,3 +31,6 @@ objc = "0.2" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["MediaQueryList", "Window"] } + +[features] +zbus = ["dep:zbus", "dep:ashpd"] diff --git a/README.md b/README.md index 490561b..cc6e45c 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,14 @@ fn main() { // Light mode dark_light::Mode::Light => {}, // Unspecified - dark_light::Mode::Default => {}, + dark_light::Mode::NoPreference => {}, } } ``` +On platforms which make use of xdg-desktop-portals, by default this crate uses the `dbus-send` and `dbus-monitor` commands to avoid heavy dependencies. +If you already depend on `zbus` or `ashpd`, you should enable the `zbus` feature. + ## Example ``` @@ -35,5 +38,3 @@ Licensed under either of * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. - - diff --git a/examples/notify.rs b/examples/notify.rs index 3efa3d5..fcf8bb5 100644 --- a/examples/notify.rs +++ b/examples/notify.rs @@ -1,8 +1,11 @@ +use std::error::Error; + use futures::StreamExt; #[tokio::main] -async fn main() -> anyhow::Result<()> { - while let Some(mode) = dark_light::subscribe().await?.next().await { +async fn main() -> Result<(), Box> { + let mut stream = dark_light::subscribe().await?; + while let Some(mode) = stream.next().await { println!("System theme changed: {:?}", mode); } diff --git a/src/freedesktop.rs b/src/freedesktop.rs index e40a56b..71faba5 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -24,7 +24,7 @@ fn get_freedesktop_color_scheme() -> Option { if theme.is_err() { return None; } - + match theme.unwrap() { 1 => Some(Mode::Dark), 2 => Some(Mode::Light), @@ -86,7 +86,7 @@ pub fn detect() -> Mode { DesktopEnvironment::Gnome => detect_gtk("/org/gnome/desktop/interface/gtk-theme"), DesktopEnvironment::Mate => detect_gtk("/org/mate/desktop/interface/gtk-theme"), DesktopEnvironment::Unity => detect_gtk("/org/gnome/desktop/interface/gtk-theme"), - _ => Mode::Default, + _ => Mode::NoPreference, }, } } diff --git a/src/lib.rs b/src/lib.rs index 069faee..633ff2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,11 +11,12 @@ //! // Light mode //! dark_light::Mode::Light => {}, //! // Unspecified -//! dark_light::Mode::Default => {}, +//! dark_light::Mode::NoPreference => {}, //! } //! ``` mod platforms; + use platforms::platform; mod utils; @@ -29,14 +30,15 @@ mod utils; use utils::rgb::Rgb; /// Enum representing dark mode, light mode, or unspecified. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub enum Mode { /// Dark mode Dark, /// Light mode Light, /// Unspecified - Default, + #[default] + NoPreference, } impl Mode { @@ -65,9 +67,17 @@ impl Mode { Self::Light } } + + fn concrete(self) -> Option { + match self { + Mode::Dark => Some(Mode::Dark), + Mode::Light => Some(Mode::Light), + Mode::NoPreference => None, + } + } } -/// Detect if light mode or dark mode is enabled. If the mode can’t be detected, fall back to [`Mode::Default`]. +/// Detect if light mode or dark mode is enabled. If the mode can’t be detected, fall back to [`Mode::NoPreference`]. pub use platform::detect::detect; /// Notifies the user if the system theme has been changed. pub use platform::notify::subscribe; diff --git a/src/platforms/freedesktop/detect.rs b/src/platforms/freedesktop/detect.rs index 738acff..40f69b7 100644 --- a/src/platforms/freedesktop/detect.rs +++ b/src/platforms/freedesktop/detect.rs @@ -1,3 +1,5 @@ +use std::process::Command; + use detect_desktop_environment::DesktopEnvironment; use crate::Mode; @@ -6,6 +8,8 @@ use super::{dconf_detect, kde_detect, CINNAMON, GNOME, MATE}; pub fn detect() -> Mode { NonFreeDesktop::detect() + .concrete() + .unwrap_or_else(FreeDesktop::detect) } /// Detects the color scheme on a platform. @@ -22,7 +26,34 @@ struct NonFreeDesktop; /// Detects the color scheme on FreeDesktop platforms. It makes use of the DBus interface. impl ColorScheme for FreeDesktop { fn detect() -> Mode { - todo!() + let Ok(output) = Command::new("dbus-send") + .args([ + "--print-reply=literal", + "--dest=org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings.Read", + "string:org.freedesktop.appearance", + "string:color-scheme", + ]) + .output() + .map(|output| output.stdout) + else { + return Mode::NoPreference; + }; + const PREFIX: &[u8] = b"uint32 "; + if let Some(index) = output + .windows(PREFIX.len()) + .position(|bytes| bytes == PREFIX) + { + match output.get(index + PREFIX.len()) { + Some(b'0') => Mode::NoPreference, + Some(b'1') => Mode::Dark, + Some(b'2') => Mode::Light, + _ => Mode::NoPreference, + } + } else { + Mode::NoPreference + } } } @@ -31,17 +62,14 @@ impl ColorScheme for NonFreeDesktop { fn detect() -> Mode { match DesktopEnvironment::detect() { Some(mode) => match mode { - DesktopEnvironment::Kde => match kde_detect() { - Ok(mode) => mode, - Err(_) => Mode::Default, - }, + DesktopEnvironment::Kde => kde_detect(), DesktopEnvironment::Cinnamon => dconf_detect(CINNAMON), DesktopEnvironment::Gnome => dconf_detect(GNOME), DesktopEnvironment::Mate => dconf_detect(MATE), DesktopEnvironment::Unity => dconf_detect(GNOME), - _ => Mode::Default, + _ => Mode::NoPreference, }, - None => Mode::Default, + None => Mode::NoPreference, } } } diff --git a/src/platforms/freedesktop/mod.rs b/src/platforms/freedesktop/mod.rs index cd65e05..6911c19 100644 --- a/src/platforms/freedesktop/mod.rs +++ b/src/platforms/freedesktop/mod.rs @@ -1,6 +1,5 @@ use std::str::FromStr; -use anyhow::Context; use ini::Ini; use crate::{utils::rgb::Rgb, Mode}; @@ -21,30 +20,28 @@ fn dconf_detect(path: &str) -> Mode { Mode::Light } } - Err(_) => Mode::Default, + Err(_) => Mode::NoPreference, } } -fn kde_detect() -> anyhow::Result { - let xdg = xdg::BaseDirectories::new()?; - let path = xdg - .find_config_file("kdeglobals") - .context("Path not found")?; - let cfg = Ini::load_from_file(path)?; - let properties = cfg - .section(Some("Colors:Window")) - .context("Failed to get section Colors:Window")?; - let background = properties - .get("BackgroundNormal") - .context("Failed to get BackgroundNormal inside Colors:Window")?; - let rgb = Rgb::from_str(background).unwrap(); - Ok(Mode::from_rgb(rgb)) +fn kde_detect() -> Mode { + fn kde_detect() -> Option { + let xdg = xdg::BaseDirectories::new().ok()?; + let path = xdg.find_config_file("kdeglobals")?; + let cfg = Ini::load_from_file(path).ok()?; + let properties = cfg.section(Some("Colors:Window"))?; + let background = properties.get("BackgroundNormal")?; + let rgb = Rgb::from_str(background).ok()?; + Some(Mode::from_rgb(rgb)) + } + kde_detect().unwrap_or_default() } +#[cfg(feature = "zbus")] impl From for Mode { fn from(value: ashpd::desktop::settings::ColorScheme) -> Self { match value { - ashpd::desktop::settings::ColorScheme::NoPreference => Mode::Default, + ashpd::desktop::settings::ColorScheme::NoPreference => Mode::NoPreference, ashpd::desktop::settings::ColorScheme::PreferDark => Mode::Dark, ashpd::desktop::settings::ColorScheme::PreferLight => Mode::Light, } diff --git a/src/platforms/freedesktop/notify.rs b/src/platforms/freedesktop/notify.rs index c63fdc1..b097a77 100644 --- a/src/platforms/freedesktop/notify.rs +++ b/src/platforms/freedesktop/notify.rs @@ -1,12 +1,46 @@ -use ashpd::desktop::settings::{ColorScheme, Settings}; -use futures::{stream, Stream, StreamExt}; -use std::task::Poll; +use crate::Mode; +use futures::Stream; +use std::error::Error; -use crate::{detect, Mode}; +#[cfg(not(feature = "zbus"))] +pub async fn subscribe() -> Result + Send, Box> { + use futures::stream; + use std::{ + io::{BufRead, BufReader}, + process::{Command, Stdio}, + }; + let mut process = Command::new("dbus-monitor") + .arg( + "type='signal',\ + sender='org.freedesktop.portal.Desktop',\ + path='/org/freedesktop/portal/desktop',\ + interface='org.freedesktop.portal.Settings',\ + member='SettingChanged',\ + arg0='org.freedesktop.appearance',\ + arg1='color-scheme'", + ) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?; + let stdout = process.stdout.take().unwrap(); + let lines = BufReader::new(stdout).lines(); + Ok(stream::iter(lines.filter_map( + |line| match line.ok()?.chars().last()? { + '0' => Some(Mode::NoPreference), + '1' => Some(Mode::Dark), + '2' => Some(Mode::Light), + _ => None, + }, + ))) +} -pub async fn subscribe() -> anyhow::Result + Send> { +#[cfg(feature = "zbus")] +pub async fn subscribe() -> Result + Send, Box> { + use crate::detect; + use futures::{stream, StreamExt}; + use std::task::Poll; let stream = if get_freedesktop_color_scheme().await.is_ok() { - let proxy = Settings::new().await?; + let proxy = ashpd::desktop::settings::Settings::new().await?; proxy .receive_color_scheme_changed() .await? @@ -30,13 +64,10 @@ pub async fn subscribe() -> anyhow::Result + Send> { Ok(stream) } -async fn get_freedesktop_color_scheme() -> anyhow::Result { - let proxy = Settings::new().await?; +#[cfg(feature = "zbus")] +async fn get_freedesktop_color_scheme() -> Result> { + let proxy = ashpd::desktop::settings::Settings::new().await?; let color_scheme = proxy.color_scheme().await?; - let mode = match color_scheme { - ColorScheme::PreferDark => Mode::Dark, - ColorScheme::PreferLight => Mode::Light, - ColorScheme::NoPreference => Mode::Default, - }; + let mode = color_scheme.into(); Ok(mode) } diff --git a/src/platforms/macos/notify.rs b/src/platforms/macos/notify.rs index 305205f..8c3608c 100644 --- a/src/platforms/macos/notify.rs +++ b/src/platforms/macos/notify.rs @@ -1,10 +1,11 @@ +use std::error::Error; use std::task::Poll; use futures::{stream, Stream}; use crate::{detect, Mode}; -pub async fn subscribe() -> anyhow::Result + Send> { +pub async fn subscribe() -> Result + Send, Box> { let mut last_mode = detect(); let stream = stream::poll_fn(move |ctx| -> Poll> { diff --git a/src/platforms/mod.rs b/src/platforms/mod.rs index 35037de..6a518e8 100644 --- a/src/platforms/mod.rs +++ b/src/platforms/mod.rs @@ -43,6 +43,6 @@ pub use websys as platform; )))] pub mod platform { pub fn detect() -> crate::Mode { - super::Mode::Light + super::Mode::NoPreference } } diff --git a/src/platforms/websys/notify.rs b/src/platforms/websys/notify.rs index 305205f..8c3608c 100644 --- a/src/platforms/websys/notify.rs +++ b/src/platforms/websys/notify.rs @@ -1,10 +1,11 @@ +use std::error::Error; use std::task::Poll; use futures::{stream, Stream}; use crate::{detect, Mode}; -pub async fn subscribe() -> anyhow::Result + Send> { +pub async fn subscribe() -> Result + Send, Box> { let mut last_mode = detect(); let stream = stream::poll_fn(move |ctx| -> Poll> { diff --git a/src/platforms/windows/notify.rs b/src/platforms/windows/notify.rs index 22a91fd..7a3621a 100644 --- a/src/platforms/windows/notify.rs +++ b/src/platforms/windows/notify.rs @@ -1,10 +1,11 @@ +use std::error::Error; use std::task::Poll; use futures::{stream, Stream}; use crate::{detect, Mode}; -pub async fn subscribe() -> anyhow::Result + Send> { +pub async fn subscribe() -> Result + Send, Box> { let mut last_mode = detect(); let stream = stream::poll_fn(move |ctx| -> Poll> { diff --git a/src/utils/rgb.rs b/src/utils/rgb.rs index d43f62b..43798a8 100644 --- a/src/utils/rgb.rs +++ b/src/utils/rgb.rs @@ -3,8 +3,10 @@ use std::str::FromStr; /// Struct representing an RGB color pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32); +pub struct ParseError; + impl FromStr for Rgb { - type Err = anyhow::Error; + type Err = ParseError; fn from_str(s: &str) -> Result { let rgb = s @@ -15,7 +17,7 @@ impl FromStr for Rgb { acc.push(x); Ok(acc) } else { - Err(anyhow::anyhow!("RGB format is invalid")) + Err(ParseError) } })?; Ok(Rgb(rgb[0], rgb[1], rgb[2]))