Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split sync and async API #50

Merged
merged 7 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ description = "Detect if dark mode or light mode is enabled"
readme = "README.md"
build = "build.rs"

[[example]]
name = "sync"
path = "examples/sync.rs"
required-features = ["sync"]

[features]
sync = ["pollster"]

[dependencies]
futures = "0.3.30"
log = "0.4.22"
Expand All @@ -18,23 +26,14 @@ 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]
ashpd = "0.9.1"
pollster = "0.3.0"
pollster = { version = "0.3.0", optional = true }

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

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.5.1"
objc2-foundation = { version = "0.2.0", features = [
"NSArray",
"NSObject",
"NSString",
] }
objc2-app-kit = { version = "0.2.0", features = [
"NSAppearance",
"NSApplication",
"NSResponder",
] }
cocoa = "0.26.0"
objc = "0.2.7"
edfloreshz marked this conversation as resolved.
Show resolved Hide resolved

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
detect().await;
subscribe().await;
Ok(())
}

async fn detect() {
println!("Current mode: {:?}", dark_light::detect().await);
}

async fn subscribe() {
let mut stream = dark_light::subscribe().await;
while let Some(mode) = stream.next().await {
println!("System mode changed: {:?}", mode);
}
}
3 changes: 0 additions & 3 deletions examples/detect.rs

This file was deleted.

13 changes: 0 additions & 13 deletions examples/notify.rs

This file was deleted.

16 changes: 16 additions & 0 deletions examples/sync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
detect();
subscribe();
Ok(())
}

fn detect() {
println!("Current mode: {:?}", dark_light::detect());
}

fn subscribe() {
let stream = dark_light::subscribe();
while let Ok(mode) = stream.recv() {
println!("System theme changed: {:?}", mode);
}
}
28 changes: 4 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,12 @@
//! }
//! ```

mod mode;
mod platforms;
use platforms::platform;

/// 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
}
}
}
pub use mode::Mode;

/// 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;
pub use platforms::platform::detect::detect;
/// Notifies the user if the system theme has been changed.
pub use platform::notify::subscribe;
pub use platforms::platform::subscribe::subscribe;
21 changes: 21 additions & 0 deletions src/mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// 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)]
pub fn from_bool(b: bool) -> Self {
if b {
Mode::Dark
} else {
Mode::Light
}
}
}
6 changes: 6 additions & 0 deletions src/platforms/freedesktop/detect.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
use crate::Mode;

#[cfg(feature = "sync")]
pub fn detect() -> Mode {
pollster::block_on(super::get_color_scheme())
}

#[cfg(not(feature = "sync"))]
pub async fn detect() -> Mode {
super::get_color_scheme().await
}
2 changes: 1 addition & 1 deletion src/platforms/freedesktop/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub mod detect;
pub mod notify;
pub mod subscribe;

use crate::Mode;

Expand Down
17 changes: 0 additions & 17 deletions src/platforms/freedesktop/notify.rs

This file was deleted.

56 changes: 56 additions & 0 deletions src/platforms/freedesktop/subscribe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::error::Error;
use std::pin::Pin;

use ashpd::desktop::settings::Settings;
use futures::{stream, Stream, StreamExt};

use crate::Mode;

#[cfg(feature = "sync")]
pub fn subscribe() -> std::sync::mpsc::Receiver<Mode> {
let (tx, rx) = std::sync::mpsc::channel();

std::thread::spawn(move || {
pollster::block_on(async {
let stream = match color_scheme_stream().await {
Ok(stream) => stream,
Err(err) => {
log::error!("Failed to subscribe to color scheme changes: {}", err);
Box::pin(Box::new(stream::empty()))
}
};

stream
.for_each(|mode| {
let _ = tx.send(mode);
async {}
})
.await;
});
});

rx
}

#[cfg(not(feature = "sync"))]
pub async fn subscribe() -> Pin<Box<dyn Stream<Item = Mode> + Send>> {
match color_scheme_stream().await {
Ok(stream) => stream,
Err(err) => {
log::error!("Failed to subscribe to color scheme changes: {}", err);
Box::pin(Box::new(stream::empty()))
}
}
}

pub async fn color_scheme_stream(
) -> Result<Pin<Box<dyn Stream<Item = Mode> + Send>>, Box<dyn Error>> {
let initial = stream::once(super::get_color_scheme()).boxed();
let later_updates = Settings::new()
.await?
.receive_color_scheme_changed()
.await?
.map(Mode::from)
.boxed();
Ok(Box::pin(Box::new(initial.chain(later_updates))))
}
45 changes: 6 additions & 39 deletions src/platforms/macos/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,12 @@

use crate::Mode;

use objc2::sel;
use objc2_app_kit::{
NSAppearance, NSAppearanceNameAccessibilityHighContrastAqua,
NSAppearanceNameAccessibilityHighContrastDarkAqua, NSAppearanceNameAqua,
NSAppearanceNameDarkAqua, NSApplication,
};
use objc2_foundation::{MainThreadMarker, NSArray, NSCopying, NSObjectProtocol};

fn is_dark_mode_enabled() -> bool {
// SAFETY: TODO, only perform this function on the main thread.
let mtm = unsafe { MainThreadMarker::new_unchecked() };

unsafe {
#[allow(deprecated)]
let appearance = NSAppearance::currentAppearance()
.unwrap_or_else(|| NSApplication::sharedApplication(mtm).effectiveAppearance());

let names = NSArray::from_id_slice(&[
NSAppearanceNameAqua.copy(),
NSAppearanceNameAccessibilityHighContrastAqua.copy(),
NSAppearanceNameDarkAqua.copy(),
NSAppearanceNameAccessibilityHighContrastDarkAqua.copy(),
]);

// `bestMatchFromAppearancesWithNames` is only available in macOS 10.14+.
// Gracefully handle earlier versions.
if !appearance.respondsToSelector(sel!(bestMatchFromAppearancesWithNames:)) {
return false;
}

if let Some(style) = appearance.bestMatchFromAppearancesWithNames(&names) {
*style == *NSAppearanceNameDarkAqua
|| *style == *NSAppearanceNameAccessibilityHighContrastDarkAqua
} else {
false
}
}
#[cfg(feature = "sync")]
pub fn detect() -> crate::Mode {
Mode::from_bool(super::is_dark_mode())
}

pub fn detect() -> crate::Mode {
Mode::from_bool(is_dark_mode_enabled())
#[cfg(not(feature = "sync"))]
pub async fn detect() -> crate::Mode {
Mode::from_bool(super::is_dark_mode())
}
25 changes: 24 additions & 1 deletion src/platforms/macos/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
pub mod detect;
pub mod notify;

pub mod subscribe;

use cocoa::base::{id, nil};
use cocoa::foundation::NSString;
use objc::{class, msg_send, sel, sel_impl};

fn is_dark_mode() -> bool {
unsafe {
let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults];
let dict: id = msg_send![user_defaults, persistentDomainForName:
NSString::alloc(nil).init_str("Apple Global Domain")];
let key = NSString::alloc(nil).init_str("AppleInterfaceStyle");
let style: id = msg_send![dict, objectForKey: key];

if style != nil {
let dark_mode = NSString::alloc(nil).init_str("Dark");
let is_dark: bool = msg_send![style, isEqualToString: dark_mode];
is_dark
} else {
false
}
}
}
24 changes: 0 additions & 24 deletions src/platforms/macos/notify.rs

This file was deleted.

38 changes: 38 additions & 0 deletions src/platforms/macos/subscribe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::Mode;

#[cfg(feature = "sync")]
pub fn subscribe() -> std::sync::mpsc::Receiver<Mode> {
let (tx, rx) = std::sync::mpsc::channel();
let mut last_mode = crate::detect();

tx.send(last_mode).unwrap();

std::thread::spawn(move || loop {
let current_mode = crate::detect();

if current_mode != last_mode {
if tx.send(current_mode).is_err() {
break;
}
last_mode = current_mode;
}
});

rx
}

#[cfg(not(feature = "sync"))]
pub async fn subscribe() -> impl futures::Stream<Item = Mode> {
Box::pin(futures::stream::unfold(
crate::detect().await,
|last_mode| async move {
loop {
let current_mode = crate::detect().await;

if current_mode != last_mode {
return Some((current_mode, current_mode));
}
}
},
))
}
Loading
Loading