Skip to content

Commit ccbbe8a

Browse files
committed
Streams 🌊
- Switched notify implementation to use streams.
1 parent f5e7a95 commit ccbbe8a

File tree

12 files changed

+228
-393
lines changed

12 files changed

+228
-393
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ readme = "README.md"
1010
build = "build.rs"
1111

1212
[dependencies]
13-
anyhow = "1.0.52"
14-
tokio = "1.23.0"
13+
futures = "0.3.30"
14+
anyhow = "1.0.79"
1515

1616
[dev-dependencies]
1717
tokio = { version = "1.23.0", features = ["full"] }

examples/notify.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use dark_light::ThemeWatcher;
1+
use dark_light::{subscribe, Event};
2+
use futures::StreamExt;
23

34
#[tokio::main]
4-
async fn main() {
5-
let mut receiver = ThemeWatcher::new().lock().await.subscribe();
5+
async fn main() -> anyhow::Result<()> {
6+
let mut stream = subscribe().await?;
67

7-
while let Ok(mode) = receiver.recv().await {
8-
println!("New mode: {:?}", mode);
8+
while let Some(event) = stream.next().await {
9+
if let Event::ThemeChanged(mode) = event {
10+
println!("System theme changed: {:?}", mode);
11+
}
912
}
13+
14+
Ok(())
1015
}

src/lib.rs

Lines changed: 73 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,73 @@
1-
//! Detect if dark mode or light mode is enabled.
2-
//!
3-
//! # Examples
4-
//!
5-
//! ```
6-
//! let mode = dark_light::detect();
7-
//!
8-
//! match mode {
9-
//! // Dark mode
10-
//! dark_light::Mode::Dark => {},
11-
//! // Light mode
12-
//! dark_light::Mode::Light => {},
13-
//! // Unspecified
14-
//! dark_light::Mode::Default => {},
15-
//! }
16-
//! ```
17-
18-
mod platforms;
19-
use platforms::platform;
20-
21-
mod utils;
22-
#[cfg(any(
23-
target_os = "linux",
24-
target_os = "freebsd",
25-
target_os = "dragonfly",
26-
target_os = "netbsd",
27-
target_os = "openbsd"
28-
))]
29-
use utils::rgb::Rgb;
30-
31-
/// Enum representing dark mode, light mode, or unspecified.
32-
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
33-
pub enum Mode {
34-
/// Dark mode
35-
Dark,
36-
/// Light mode
37-
Light,
38-
/// Unspecified
39-
Default,
40-
}
41-
42-
impl Mode {
43-
/// Convert a boolean to [`Mode`]. `true` is [`Mode::Dark`], `false` is [`Mode::Light`].
44-
fn from(mode: bool) -> Self {
45-
if mode {
46-
Mode::Dark
47-
} else {
48-
Mode::Light
49-
}
50-
}
51-
52-
#[cfg(any(
53-
target_os = "linux",
54-
target_os = "freebsd",
55-
target_os = "dragonfly",
56-
target_os = "netbsd",
57-
target_os = "openbsd"
58-
))]
59-
/// 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.
60-
fn from_rgb(rgb: Rgb) -> Self {
61-
let window_background_gray = (rgb.0 * 11 + rgb.1 * 16 + rgb.2 * 5) / 32;
62-
if window_background_gray < 192 {
63-
Self::Dark
64-
} else {
65-
Self::Light
66-
}
67-
}
68-
}
69-
70-
pub use platform::notify::ThemeWatcher;
71-
72-
/// Detect if light mode or dark mode is enabled. If the mode can’t be detected, fall back to [`Mode::Default`].
73-
pub fn detect() -> Mode {
74-
platform::detect::detect()
75-
}
1+
//! Detect if dark mode or light mode is enabled.
2+
//!
3+
//! # Examples
4+
//!
5+
//! ```
6+
//! let mode = dark_light::detect();
7+
//!
8+
//! match mode {
9+
//! // Dark mode
10+
//! dark_light::Mode::Dark => {},
11+
//! // Light mode
12+
//! dark_light::Mode::Light => {},
13+
//! // Unspecified
14+
//! dark_light::Mode::Default => {},
15+
//! }
16+
//! ```
17+
18+
mod platforms;
19+
use platforms::platform;
20+
21+
mod utils;
22+
#[cfg(any(
23+
target_os = "linux",
24+
target_os = "freebsd",
25+
target_os = "dragonfly",
26+
target_os = "netbsd",
27+
target_os = "openbsd"
28+
))]
29+
use utils::rgb::Rgb;
30+
31+
/// Enum representing dark mode, light mode, or unspecified.
32+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
33+
pub enum Mode {
34+
/// Dark mode
35+
Dark,
36+
/// Light mode
37+
Light,
38+
/// Unspecified
39+
Default,
40+
}
41+
42+
impl Mode {
43+
fn from_bool(b: bool) -> Self {
44+
if b {
45+
Mode::Dark
46+
} else {
47+
Mode::Light
48+
}
49+
}
50+
51+
#[cfg(any(
52+
target_os = "linux",
53+
target_os = "freebsd",
54+
target_os = "dragonfly",
55+
target_os = "netbsd",
56+
target_os = "openbsd"
57+
))]
58+
/// 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.
59+
fn from_rgb(rgb: Rgb) -> Self {
60+
let window_background_gray = (rgb.0 * 11 + rgb.1 * 16 + rgb.2 * 5) / 32;
61+
if window_background_gray < 192 {
62+
Self::Dark
63+
} else {
64+
Self::Light
65+
}
66+
}
67+
}
68+
69+
/// Detect if light mode or dark mode is enabled. If the mode can’t be detected, fall back to [`Mode::Default`].
70+
pub use platform::detect::detect;
71+
/// Notifies the user if the system theme has been changed.
72+
pub use platform::notify::subscribe;
73+
pub use platforms::Event;

src/platforms/freedesktop/mod.rs

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,52 @@
1-
use std::str::FromStr;
2-
3-
use anyhow::Context;
4-
use ini::Ini;
5-
6-
use crate::{utils::rgb::Rgb, Mode};
7-
8-
pub mod detect;
9-
pub mod notify;
10-
11-
const MATE: &str = "/org/mate/desktop/interface/gtk-theme";
12-
const GNOME: &str = "/org/gnome/desktop/interface/gtk-theme";
13-
const CINNAMON: &str = "/org/cinnamon/desktop/interface/gtk-theme";
14-
15-
fn dconf_detect(path: &str) -> Mode {
16-
match dconf_rs::get_string(path) {
17-
Ok(theme) => Mode::from(theme.to_lowercase().contains("dark")),
18-
Err(_) => Mode::Default,
19-
}
20-
}
21-
22-
fn kde_detect() -> anyhow::Result<Mode> {
23-
let xdg = xdg::BaseDirectories::new()?;
24-
let path = xdg
25-
.find_config_file("kdeglobals")
26-
.context("Path not found")?;
27-
let cfg = Ini::load_from_file(path)?;
28-
let properties = cfg
29-
.section(Some("Colors:Window"))
30-
.context("Failed to get section Colors:Window")?;
31-
let background = properties
32-
.get("BackgroundNormal")
33-
.context("Failed to get BackgroundNormal inside Colors:Window")?;
34-
let rgb = Rgb::from_str(background)?;
35-
Ok(Mode::from_rgb(rgb))
36-
}
1+
use std::str::FromStr;
2+
3+
use anyhow::Context;
4+
use ini::Ini;
5+
6+
use crate::{utils::rgb::Rgb, Mode};
7+
8+
pub mod detect;
9+
pub mod notify;
10+
11+
const MATE: &str = "/org/mate/desktop/interface/gtk-theme";
12+
const GNOME: &str = "/org/gnome/desktop/interface/gtk-theme";
13+
const CINNAMON: &str = "/org/cinnamon/desktop/interface/gtk-theme";
14+
15+
fn dconf_detect(path: &str) -> Mode {
16+
match dconf_rs::get_string(path) {
17+
Ok(theme) => {
18+
if theme.to_lowercase().contains("dark") {
19+
Mode::Dark
20+
} else {
21+
Mode::Light
22+
}
23+
}
24+
Err(_) => Mode::Default,
25+
}
26+
}
27+
28+
fn kde_detect() -> anyhow::Result<Mode> {
29+
let xdg = xdg::BaseDirectories::new()?;
30+
let path = xdg
31+
.find_config_file("kdeglobals")
32+
.context("Path not found")?;
33+
let cfg = Ini::load_from_file(path)?;
34+
let properties = cfg
35+
.section(Some("Colors:Window"))
36+
.context("Failed to get section Colors:Window")?;
37+
let background = properties
38+
.get("BackgroundNormal")
39+
.context("Failed to get BackgroundNormal inside Colors:Window")?;
40+
let rgb = Rgb::from_str(background).unwrap();
41+
Ok(Mode::from_rgb(rgb))
42+
}
43+
44+
impl From<ashpd::desktop::settings::ColorScheme> for Mode {
45+
fn from(value: ashpd::desktop::settings::ColorScheme) -> Self {
46+
match value {
47+
ashpd::desktop::settings::ColorScheme::NoPreference => Mode::Default,
48+
ashpd::desktop::settings::ColorScheme::PreferDark => Mode::Dark,
49+
ashpd::desktop::settings::ColorScheme::PreferLight => Mode::Light,
50+
}
51+
}
52+
}

src/platforms/freedesktop/notify.rs

Lines changed: 29 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,35 @@
1-
use std::sync::Arc;
2-
3-
use tokio::sync::{broadcast, Mutex};
4-
51
use ashpd::desktop::settings::{ColorScheme, Settings};
6-
use zbus::export::futures_util::StreamExt;
7-
8-
use crate::{detect, Mode};
9-
10-
pub struct ThemeWatcher {
11-
sender: broadcast::Sender<Mode>,
12-
current_mode: Mutex<Mode>,
13-
}
14-
15-
/// Theme watcher implementation for subscriptions.
16-
impl ThemeWatcher {
17-
pub fn new() -> Arc<Mutex<Self>> {
18-
let (sender, _) = broadcast::channel::<Mode>(256);
19-
20-
let theme_watcher = ThemeWatcher {
21-
sender,
22-
current_mode: Mutex::new(detect()),
23-
};
24-
25-
let arc_watcher = Arc::new(Mutex::new(theme_watcher));
26-
27-
tokio::spawn({
28-
let arc_watcher = Arc::clone(&arc_watcher);
29-
async move {
30-
arc_watcher.lock().await.monitor_theme_changes().await;
2+
use futures::{stream, Stream, StreamExt};
3+
use std::task::Poll;
4+
5+
use crate::{detect, platforms::Event, Mode};
6+
7+
pub async fn subscribe() -> anyhow::Result<impl Stream<Item = Event<Mode>> + Send> {
8+
let mut last_mode = detect();
9+
10+
let stream = if get_freedesktop_color_scheme().await.is_ok() {
11+
let proxy = Settings::new().await?;
12+
proxy
13+
.receive_color_scheme_changed()
14+
.await?
15+
.map(Mode::from)
16+
.map(|mode| Event::ThemeChanged(mode))
17+
.boxed()
18+
} else {
19+
stream::poll_fn(move |_| -> Poll<Option<Event<Mode>>> {
20+
let current_mode = detect();
21+
22+
if current_mode != last_mode {
23+
last_mode = current_mode;
24+
Poll::Ready(Some(Event::ThemeChanged(current_mode)))
25+
} else {
26+
Poll::Ready(Some(Event::Waiting))
3127
}
32-
});
33-
34-
arc_watcher
35-
}
36-
37-
/// Method to get the current theme mode
38-
pub async fn get_current_mode(&self) -> Mode {
39-
let current_mode = self.current_mode.lock().await;
40-
current_mode.clone()
41-
}
42-
43-
/// Method to subscribe to theme change events
44-
pub fn subscribe(&self) -> broadcast::Receiver<Mode> {
45-
self.sender.subscribe()
46-
}
47-
48-
/// The asynchronous method to monitor theme changes
49-
async fn monitor_theme_changes(&self) {
50-
if get_freedesktop_color_scheme().await.is_ok() {
51-
let proxy = Settings::new().await.unwrap();
52-
if let Ok(mut color_scheme) = proxy.receive_color_scheme_changed().await {
53-
while let Some(color_scheme) = color_scheme.next().await {
54-
// Compare the current value with the stored value
55-
let mut current_mode = self.current_mode.lock().await;
56-
57-
let mode = match color_scheme {
58-
ColorScheme::NoPreference => Mode::Default,
59-
ColorScheme::PreferDark => Mode::Dark,
60-
ColorScheme::PreferLight => Mode::Light,
61-
};
28+
})
29+
.boxed()
30+
};
6231

63-
if *current_mode != mode {
64-
*current_mode = mode;
65-
let _ = self.sender.send(current_mode.clone());
66-
}
67-
}
68-
}
69-
} else {
70-
eprintln!("Unable to start freedesktop proxy, falling back to legacy...");
71-
loop {
72-
let mut current_mode = self.current_mode.lock().await;
73-
let new_mode = detect();
74-
if *current_mode != new_mode {
75-
*current_mode = new_mode;
76-
let _ = self.sender.send(current_mode.clone());
77-
}
78-
}
79-
}
80-
}
32+
Ok(stream)
8133
}
8234

8335
async fn get_freedesktop_color_scheme() -> anyhow::Result<Mode> {

src/platforms/macos/detect.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ fn is_dark_mode_enabled() -> bool {
5252
}
5353

5454
pub fn detect() -> crate::Mode {
55-
Mode::from(is_dark_mode_enabled())
55+
Mode::from_bool(is_dark_mode_enabled())
5656
}

0 commit comments

Comments
 (0)