Skip to content

Commit

Permalink
Notifications (#26)
Browse files Browse the repository at this point in the history
* Subscriptions

Implemented callback functions for freedesktop and non-freedesktop.
Partitioned freedesktop into a module.

New module structure

Implemented new module structure to macOS and Windows

Fix for doc test

Ensured that all possible cases are being handled within `Mode` match.

* Async notify

`notify` is now an async function.
Modified example to use `tokio`
Renamed `Mode::rgb` to `Mode::from_rgb`
`freedesktop_watch` now uses `ashpd` beta.
Changed signature for `notify` function in macOS and Windows files.

* Converted examples to individual crates

* Example implementation

* Re-include ashpd

* Fix merge issue

* Implemented channels for Linux notify

- Updated ashpd

* Code improvements

- Removed Settings struct as it was not necessary.
- Removed `Settings` parameter from `freedesktop_watch`.
- `get_freedesktop_color_scheme` matches `ColorScheme` instead of `u32`

* Using legacy fallback

When the settings proxy is unable to init, legacy is used as fallback.

* Upgraded ashpd

- Upgraded to the master branch of ashpd
- Moved the thread spawning to `notify`
- Made the async methods return `anyhow::Result<T>`

* Using `xdg` crate.

- Using xdg to find config files on Linux/BSD systems.
- Quality of life improvements

* Removed tokio dependency

- Cleaned Cargo.toml
- Modified notify to take a closure.

* Formatting applied

* Fix todo methods for macOS and Windows

* Move websys to new module structure

* Discard unused parameter.

- Fix wasm issue.

* Improvements

- Moved RGB logic to a single file.
- Added documentation.

* Subscriptions 📰

- Added ThemeWatcher, a new struct which allows users to subscribe and receive notifications when the theme changes.

- New crate organization.

* Currently only Windows has been implemented.
* Currently only tokio has been implemented, we can add new implementations as features in the future.

* Subscriptions 📰

- Added support for Freedesktop.

* Added default implementaton for other platforms.

* Subscriptions ⚡

- Added generic implementation for all platforms except freedesktop.

* Updated dependencies 🎁

* Streams 🌊

- Switched notify implementation to use streams.

* Update example.

* Stream can now wait

- The stream is now using Poll::Pending to wait for a new event instead of returning a value when waiting.
  • Loading branch information
edfloreshz authored Apr 13, 2024
1 parent 0b41b1c commit 77ae434
Show file tree
Hide file tree
Showing 19 changed files with 390 additions and 106 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/target
/examples/*/target
Cargo.lock
.vscode
20 changes: 14 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,26 @@ description = "Detect if dark mode or light mode is enabled"
readme = "README.md"
build = "build.rs"

[dependencies]
futures = "0.3.30"
anyhow = "1.0.79"

[dev-dependencies]
tokio = { version = "1.23.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 = "0.2"
detect-desktop-environment = "1.0.0"
dconf_rs = "0.3"
dirs = "4.0"
zbus = "4.1.2"
rust-ini = "0.18"
zbus = "3.0"
rust-ini = "0.20"
ashpd = "0.7.0"
xdg = "2.4.1"

[target.'cfg(windows)'.dependencies]
winreg = "0.10"
winreg = "0.52.0"

[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = [ "MediaQueryList", "Window" ] }
web-sys = { version = "0.3", features = ["MediaQueryList", "Window"] }
10 changes: 10 additions & 0 deletions examples/notify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use futures::StreamExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
while let Some(mode) = dark_light::subscribe().await?.next().await {
println!("System theme changed: {:?}", mode);
}

Ok(())
}
170 changes: 73 additions & 97 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,97 +1,73 @@
//! Detect if dark mode or light mode is enabled.
//!
//! # Examples
//!
//! ```
//! let mode = dark_light::detect();
//!
//! match mode {
//! // Dark mode
//! dark_light::Mode::Dark => {},
//! // Light mode
//! dark_light::Mode::Light => {},
//! // Unspecified
//! dark_light::Mode::Default => {},
//! }
//! ```
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos as platform;

#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
use windows as platform;

#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
mod freedesktop;
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
use freedesktop as platform;

#[cfg(target_arch = "wasm32")]
mod websys;
#[cfg(target_arch = "wasm32")]
use websys as platform;

#[cfg(not(any(
target_os = "macos",
target_os = "windows",
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd",
target_arch = "wasm32"
)))]
mod platform {
pub fn detect() -> crate::Mode {
super::Mode::Light
}
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Mode {
/// Dark mode
Dark,
/// Light mode
Light,
/// Unspecified
Default,
}

impl Mode {
fn from(b: bool) -> Self {
if b {
Mode::Dark
} else {
Mode::Light
}
}
fn rgb(r: u32, g: u32, b: u32) -> Self {
let window_background_gray = (r * 11 + g * 16 + b * 5) / 32;
if window_background_gray < 192 {
Self::Dark
} else {
Self::Light
}
}
}

/// Detect if light mode or dark mode is enabled. If the mode can’t be detected, fall back to [`Mode::Default`].
pub fn detect() -> Mode {
platform::detect()
}
//! Detect if dark mode or light mode is enabled.
//!
//! # Examples
//!
//! ```
//! let mode = dark_light::detect();
//!
//! match mode {
//! // Dark mode
//! dark_light::Mode::Dark => {},
//! // Light mode
//! dark_light::Mode::Light => {},
//! // Unspecified
//! dark_light::Mode::Default => {},
//! }
//! ```
mod platforms;
use platforms::platform;

mod utils;
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
use utils::rgb::Rgb;

/// Enum representing dark mode, light mode, or unspecified.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Mode {
/// Dark mode
Dark,
/// Light mode
Light,
/// Unspecified
Default,
}

impl Mode {
#[allow(dead_code)]
fn from_bool(b: bool) -> Self {
if b {
Mode::Dark
} else {
Mode::Light
}
}

#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
/// Convert an RGB color to [`Mode`]. The color is converted to grayscale, and if the grayscale value is less than 192, [`Mode::Dark`] is returned. Otherwise, [`Mode::Light`] is returned.
fn from_rgb(rgb: Rgb) -> Self {
let window_background_gray = (rgb.0 * 11 + rgb.1 * 16 + rgb.2 * 5) / 32;
if window_background_gray < 192 {
Self::Dark
} else {
Self::Light
}
}
}

/// Detect if light mode or dark mode is enabled. If the mode can’t be detected, fall back to [`Mode::Default`].
pub use platform::detect::detect;
/// Notifies the user if the system theme has been changed.
pub use platform::notify::subscribe;
47 changes: 47 additions & 0 deletions src/platforms/freedesktop/detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use detect_desktop_environment::DesktopEnvironment;

use crate::Mode;

use super::{dconf_detect, kde_detect, CINNAMON, GNOME, MATE};

pub fn detect() -> Mode {
NonFreeDesktop::detect()
}

/// Detects the color scheme on a platform.
trait ColorScheme {
fn detect() -> Mode;
}

/// Represents the FreeDesktop platform.
struct FreeDesktop;

/// Represents non FreeDesktop platforms.
struct NonFreeDesktop;

/// Detects the color scheme on FreeDesktop platforms. It makes use of the DBus interface.
impl ColorScheme for FreeDesktop {
fn detect() -> Mode {
todo!()
}
}

/// Detects the color scheme on non FreeDesktop platforms, having a custom implementation for each desktop environment.
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::Cinnamon => dconf_detect(CINNAMON),
DesktopEnvironment::Gnome => dconf_detect(GNOME),
DesktopEnvironment::Mate => dconf_detect(MATE),
DesktopEnvironment::Unity => dconf_detect(GNOME),
_ => Mode::Default,
},
None => Mode::Default,
}
}
}
52 changes: 52 additions & 0 deletions src/platforms/freedesktop/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::str::FromStr;

use anyhow::Context;
use ini::Ini;

use crate::{utils::rgb::Rgb, Mode};

pub mod detect;
pub mod notify;

const MATE: &str = "/org/mate/desktop/interface/gtk-theme";
const GNOME: &str = "/org/gnome/desktop/interface/gtk-theme";
const CINNAMON: &str = "/org/cinnamon/desktop/interface/gtk-theme";

fn dconf_detect(path: &str) -> Mode {
match dconf_rs::get_string(path) {
Ok(theme) => {
if theme.to_lowercase().contains("dark") {
Mode::Dark
} else {
Mode::Light
}
}
Err(_) => Mode::Default,
}
}

fn kde_detect() -> anyhow::Result<Mode> {
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))
}

impl From<ashpd::desktop::settings::ColorScheme> for Mode {
fn from(value: ashpd::desktop::settings::ColorScheme) -> Self {
match value {
ashpd::desktop::settings::ColorScheme::NoPreference => Mode::Default,
ashpd::desktop::settings::ColorScheme::PreferDark => Mode::Dark,
ashpd::desktop::settings::ColorScheme::PreferLight => Mode::Light,
}
}
}
42 changes: 42 additions & 0 deletions src/platforms/freedesktop/notify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use ashpd::desktop::settings::{ColorScheme, Settings};
use futures::{stream, Stream, StreamExt};
use std::task::Poll;

use crate::{detect, Mode};

pub async fn subscribe() -> anyhow::Result<impl Stream<Item = Mode> + Send> {
let stream = if get_freedesktop_color_scheme().await.is_ok() {
let proxy = Settings::new().await?;
proxy
.receive_color_scheme_changed()
.await?
.map(Mode::from)
.boxed()
} else {
let mut last_mode = detect();
stream::poll_fn(move |ctx| -> Poll<Option<Mode>> {
let current_mode = detect();
if current_mode != last_mode {
last_mode = current_mode;
Poll::Ready(Some(current_mode))
} else {
ctx.waker().wake_by_ref();
Poll::Pending
}
})
.boxed()
};

Ok(stream)
}

async fn get_freedesktop_color_scheme() -> anyhow::Result<Mode> {
let proxy = 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,
};
Ok(mode)
}
2 changes: 1 addition & 1 deletion src/macos.rs → src/platforms/macos/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ fn is_dark_mode_enabled() -> bool {
}

pub fn detect() -> crate::Mode {
Mode::from(is_dark_mode_enabled())
Mode::from_bool(is_dark_mode_enabled())
}
2 changes: 2 additions & 0 deletions src/platforms/macos/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod detect;
pub mod notify;
23 changes: 23 additions & 0 deletions src/platforms/macos/notify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::task::Poll;

use futures::{stream, Stream};

use crate::{detect, Mode};

pub async fn subscribe() -> anyhow::Result<impl Stream<Item = Mode> + Send> {
let mut last_mode = detect();

let stream = stream::poll_fn(move |ctx| -> Poll<Option<Mode>> {
let current_mode = detect();

if current_mode != last_mode {
last_mode = current_mode;
Poll::Ready(Some(current_mode))
} else {
ctx.waker().wake_by_ref();
Poll::Pending
}
});

Ok(stream)
}
Loading

0 comments on commit 77ae434

Please sign in to comment.