[desktop] Add rust-dark-light as direct dependency

This commit is contained in:
Vladimir Stoilov
2025-02-25 11:34:21 +02:00
parent 9ff7ec96d1
commit b2907e9475
23 changed files with 654 additions and 2 deletions

View File

@@ -0,0 +1,92 @@
use detect_desktop_environment::DesktopEnvironment;
use ini::Ini;
use std::path::{Path, PathBuf};
use zbus::blocking::Connection;
use crate::Mode;
const XDG_KDEGLOBALS: &str = "/etc/xdg/kdeglobals";
fn get_freedesktop_color_scheme() -> Option<Mode> {
let conn = Connection::session();
if conn.is_err() {
return None;
}
let reply = conn.unwrap().call_method(
Some("org.freedesktop.portal.Desktop"),
"/org/freedesktop/portal/desktop",
Some("org.freedesktop.portal.Settings"),
"Read",
&("org.freedesktop.appearance", "color-scheme"),
);
if let Ok(reply) = &reply {
let theme = reply.body().deserialize::<u32>();
if theme.is_err() {
return None;
}
match theme.unwrap() {
1 => Some(Mode::Dark),
2 => Some(Mode::Light),
_ => None,
}
} else {
None
}
}
fn detect_gtk(pattern: &str) -> Mode {
match dconf_rs::get_string(pattern) {
Ok(theme) => Mode::from(theme.to_lowercase().contains("dark")),
Err(_) => Mode::Light,
}
}
fn detect_kde(path: &str) -> Mode {
match Ini::load_from_file(path) {
Ok(cfg) => {
let section = match cfg.section(Some("Colors:Window")) {
Some(section) => section,
None => return Mode::Light,
};
let values = match section.get("BackgroundNormal") {
Some(string) => string,
None => return Mode::Light,
};
let rgb = values
.split(',')
.map(|s| s.parse::<u32>().unwrap_or(255))
.collect::<Vec<u32>>();
let rgb = if rgb.len() > 2 {
rgb
} else {
vec![255, 255, 255]
};
let (r, g, b) = (rgb[0], rgb[1], rgb[2]);
Mode::rgb(r, g, b)
}
Err(_) => Mode::Light,
}
}
pub fn detect() -> Mode {
match get_freedesktop_color_scheme() {
Some(mode) => mode,
// Other desktop environments are still being worked on, fow now, only the following implementations work.
None => match DesktopEnvironment::detect() {
DesktopEnvironment::Kde => {
let path = if Path::new(XDG_KDEGLOBALS).exists() {
PathBuf::from(XDG_KDEGLOBALS)
} else {
dirs::home_dir().unwrap().join(".config/kdeglobals")
};
detect_kde(path.to_str().unwrap())
}
DesktopEnvironment::Cinnamon => detect_gtk("/org/cinnamon/desktop/interface/gtk-theme"),
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,
},
}
}

View File

@@ -0,0 +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 => {},
//! }
//! ```
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 cant 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;

View File

@@ -0,0 +1,47 @@
use detect_desktop_environment::DesktopEnvironment;
use crate::Mode;
use super::{dconf_detect, gsetting_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 => gsetting_detect(),
DesktopEnvironment::Mate => dconf_detect(MATE),
DesktopEnvironment::Unity => dconf_detect(GNOME),
_ => Mode::Default,
},
None => Mode::Default,
}
}
}

View File

@@ -0,0 +1,88 @@
use std::{process::Command, 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) => {
println!("dconf output: {}", theme);
if theme.is_empty() {
Mode::Default
} else {
if theme.to_lowercase().contains("dark") {
Mode::Dark
} else {
Mode::Light
}
}
}
Err(_) => Mode::Default,
}
}
pub fn gsetting_detect() -> Mode {
let mode = match Command::new("gsettings")
.arg("get")
.arg("org.gnome.desktop.interface")
.arg("color-scheme")
.output()
{
Ok(output) => {
if let Ok(scheme) = String::from_utf8(output.stdout) {
if scheme.contains("prefer-dark") {
Mode::Dark
} else if scheme.contains("prefer-light") {
Mode::Dark
} else {
Mode::Default
}
} else {
Mode::Default
}
}
Err(_) => Mode::Default,
};
// Fallback to dconf
if mode == Mode::Default {
return dconf_detect(GNOME);
}
mode
}
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,
}
}
}

View File

@@ -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)
}

View File

@@ -0,0 +1,56 @@
// Dark/light mode detection on macOS.
// Written with help from Ryan McGrath (https://rymc.io/).
use crate::Mode;
use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl};
extern "C" {
static NSAppearanceNameAqua: *const Object;
static NSAppearanceNameAccessibilityHighContrastAqua: *const Object;
static NSAppearanceNameDarkAqua: *const Object;
static NSAppearanceNameAccessibilityHighContrastDarkAqua: *const Object;
}
fn is_dark_mode_enabled() -> bool {
unsafe {
let mut appearance: *const Object = msg_send![class!(NSAppearance), currentAppearance];
if appearance.is_null() {
appearance = msg_send![class!(NSApp), effectiveAppearance];
}
let objects = [
NSAppearanceNameAqua,
NSAppearanceNameAccessibilityHighContrastAqua,
NSAppearanceNameDarkAqua,
NSAppearanceNameAccessibilityHighContrastDarkAqua,
];
let names: *const Object = msg_send![
class!(NSArray),
arrayWithObjects:objects.as_ptr()
count:objects.len()
];
// `bestMatchFromAppearancesWithNames` is only available in macOS 10.14+.
// Gracefully handle earlier versions.
let responds_to_selector: objc::runtime::BOOL = msg_send![
appearance,
respondsToSelector: sel!(bestMatchFromAppearancesWithNames:)
];
if responds_to_selector == objc::runtime::NO {
return false;
}
let style: *const Object = msg_send![
appearance,
bestMatchFromAppearancesWithNames:&*names
];
style == NSAppearanceNameDarkAqua
|| style == NSAppearanceNameAccessibilityHighContrastDarkAqua
}
}
pub fn detect() -> crate::Mode {
Mode::from_bool(is_dark_mode_enabled())
}

View File

@@ -0,0 +1,2 @@
pub mod detect;
pub mod notify;

View File

@@ -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)
}

View File

@@ -0,0 +1,48 @@
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "macos")]
pub use macos as platform;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub use windows as platform;
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
pub mod freedesktop;
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
))]
pub use freedesktop as platform;
#[cfg(target_arch = "wasm32")]
pub mod websys;
#[cfg(target_arch = "wasm32")]
pub 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"
)))]
pub mod platform {
pub fn detect() -> crate::Mode {
super::Mode::Light
}
}

View File

@@ -0,0 +1,11 @@
use crate::Mode;
pub fn detect() -> crate::Mode {
if let Some(window) = web_sys::window() {
let query_result = window.match_media("(prefers-color-scheme: dark)");
if let Ok(Some(mql)) = query_result {
return Mode::from_bool(mql.matches());
}
}
Mode::Light
}

View File

@@ -0,0 +1,2 @@
pub mod detect;
pub mod notify;

View File

@@ -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)
}

View File

@@ -0,0 +1,15 @@
use crate::Mode;
use winreg::RegKey;
const SUBKEY: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
const VALUE: &str = "AppsUseLightTheme";
pub fn detect() -> Mode {
let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER);
if let Ok(subkey) = hkcu.open_subkey(SUBKEY) {
if let Ok(dword) = subkey.get_value::<u32, _>(VALUE) {
return Mode::from_bool(dword == 0);
}
}
Mode::Light
}

View File

@@ -0,0 +1,2 @@
pub mod detect;
pub mod notify;

View File

@@ -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)
}

View File

@@ -0,0 +1 @@
pub mod rgb;

View File

@@ -0,0 +1,23 @@
use std::str::FromStr;
/// Struct representing an RGB color
pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32);
impl FromStr for Rgb {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rgb = s
.split(',')
.map(|s| s.parse::<u32>().unwrap_or(255))
.try_fold(vec![], |mut acc, x| {
if acc.len() < 3 {
acc.push(x);
Ok(acc)
} else {
Err(anyhow::anyhow!("RGB format is invalid"))
}
})?;
Ok(Rgb(rgb[0], rgb[1], rgb[2]))
}
}