diff --git a/Cargo.toml b/Cargo.toml index c404206..bca811b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ xdg-portal = ["ashpd"] applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] applet-token = [] single-instance = ["dep:zbus", "serde", "ron"] +dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"] [dependencies] apply = "0.3.0" @@ -68,6 +69,8 @@ css-color = "0.2.5" nix = { version = "0.27", features = ["process"], optional = true } zbus = {version = "3.14.1", default-features = false, optional = true} serde = { version = "1.0.180", optional = true } +cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } + [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.4" diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 76db752..69fd6e9 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{self}; -#[proc_macro_derive(CosmicConfigEntry)] +#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -13,6 +13,24 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { } fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { + let attributes = &ast.attrs; + let version = attributes + .iter() + .find_map(|attr| { + if attr.path.is_ident("version") { + match attr.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Int(lit_int), + .. + })) => Some(lit_int.base10_parse::().unwrap()), + _ => None, + } + } else { + None + } + }) + .unwrap_or(0); + let name = &ast.ident; // Get the fields of the struct @@ -64,6 +82,8 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let gen = quote! { impl CosmicConfigEntry for #name { + const VERSION: u64 = #version; + fn write_entry(&self, config: &cosmic_config::Config) -> Result<(), cosmic_config::Error> { let tx = config.transaction(); #(#write_each_config_field)* @@ -83,7 +103,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { } } - fn update_keys>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec, Vec<&str>){ + fn update_keys>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec, Vec<&'static str>){ let mut keys = Vec::with_capacity(changed_keys.len()); let mut errors = Vec::new(); for key in changed_keys.iter() { diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 3593ae3..154183e 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -5,11 +5,13 @@ edition = "2021" [features] default = ["macro", "subscription"] +dbus = ["dep:zbus", "cosmic-settings-daemon", "futures-util", "subscription"] macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] # For redox support +zbus = { version = "3.14.1", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.12.2", optional = true } dirs = "5.0.1" @@ -19,4 +21,6 @@ serde = "1.0.152" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } - +once_cell = "1.19.0" +cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } +futures-util = { version = "0.3", optional = true } diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs new file mode 100644 index 0000000..b048660 --- /dev/null +++ b/cosmic-config/src/dbus.rs @@ -0,0 +1,137 @@ +use std::ops::Deref; + +use crate::CosmicConfigEntry; +use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy, Ping}; +use futures_util::SinkExt; +use iced_futures::futures::{future::pending, StreamExt}; +pub async fn settings_daemon_proxy() -> zbus::Result> { + let conn = zbus::Connection::session().await?; + CosmicSettingsDaemonProxy::new(&conn).await +} + +#[derive(Debug)] +pub struct Watcher { + proxy: ConfigProxy<'static>, +} + +impl Deref for Watcher { + type Target = ConfigProxy<'static>; + fn deref(&self) -> &Self::Target { + &self.proxy + } +} + +impl Watcher { + pub async fn new_config( + settings_daemon_proxy: &CosmicSettingsDaemonProxy<'static>, + id: &str, + version: u64, + ) -> zbus::Result { + let (path, name) = settings_daemon_proxy.watch_config(id, version).await?; + ConfigProxy::builder(settings_daemon_proxy.connection()) + .path(path)? + .destination(name)? + .build() + .await + .map(|proxy| Self { proxy }) + } +} + +#[derive(Debug)] +pub struct ConfigUpdate { + pub errors: Vec, + pub keys: Vec<&'static str>, + pub config: T, +} + +pub fn watcher_subscription( + settings_daemon: CosmicSettingsDaemonProxy<'static>, + config_id: &'static str, +) -> iced_futures::Subscription> { + let id = std::any::TypeId::of::(); + iced_futures::subscription::channel((config_id, id), 5, move |mut tx| async move { + let version = T::VERSION; + let Ok(cosmic_config) = crate::Config::new(config_id, version) else { + pending::<()>().await; + unreachable!(); + }; + dbg!(config_id, version, &cosmic_config); + let mut config = match T::get_entry(&cosmic_config) { + Ok(config) => config, + Err((errors, default)) => { + if !errors.is_empty() { + eprintln!("Failed to get config: {errors:?}"); + } + default + } + }; + if let Err(err) = tx + .send(ConfigUpdate { + errors: Vec::new(), + keys: Vec::new(), + config: config.clone(), + }) + .await + { + eprintln!("Failed to send config: {err}"); + } + + dbg!("sent init"); + + let Ok(watcher) = Watcher::new_config(&settings_daemon, config_id, version).await else { + dbg!("failed to create watcher"); + pending::<()>().await; + unreachable!(); + }; + + dbg!("watcher created"); + + loop { + let Ok(changes) = watcher.receive_changed().await else { + pending::<()>().await; + unreachable!(); + }; + let Ok(pings) = watcher.receive_ping().await else { + pending::<()>().await; + unreachable!(); + }; + let mut streams = futures_util::stream_select!( + changes.map(Message::ConfigChanged), + pings.map(Message::ConfigPing) + ); + while let Some(v) = streams.next().await { + match v { + Message::ConfigChanged(change) => { + let Ok(args) = change.args() else { + continue; + }; + let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); + if !keys.is_empty() { + if let Err(err) = tx + .send(ConfigUpdate { + errors, + keys, + config: config.clone(), + }) + .await + { + eprintln!("Failed to send config update: {err}"); + } + } + } + Message::ConfigPing(_) => { + // send pong + if let Err(err) = watcher.pong().await { + eprintln!("Failed to send pong: {err}"); + } + } + } + } + } + }) +} + +pub enum Message { + ConfigChanged(Changed), + ConfigPing(Ping), +} diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 203b6da..e7c7f5b 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,20 +1,22 @@ -use iced_futures::futures::SinkExt; -#[cfg(feature = "subscription")] -use iced_futures::{futures::channel::mpsc, subscription}; use notify::{ event::{EventKind, ModifyKind}, - RecommendedWatcher, Watcher, + Watcher, }; use serde::{de::DeserializeOwned, Serialize}; use std::{ - borrow::Cow, fmt, fs, - hash::Hash, io::Write, path::{Path, PathBuf}, sync::Mutex, }; +#[cfg(feature = "subscription")] +mod subscription; +pub use subscription::*; + +#[cfg(all(feature = "dbus", feature = "subscription"))] +pub mod dbus; + #[cfg(feature = "macro")] pub use cosmic_config_derive; @@ -322,24 +324,12 @@ impl<'a> ConfigSet for ConfigTransaction<'a> { } } -#[cfg(feature = "subscription")] -pub enum ConfigState { - Init(Cow<'static, str>, u64, bool), - Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), - Failed, -} - -#[cfg(feature = "subscription")] -pub enum ConfigUpdate { - Update(T), - UpdateError(T, Vec), - Failed, -} - pub trait CosmicConfigEntry where Self: Sized, { + const VERSION: u64; + fn write_entry(&self, config: &Config) -> Result<(), crate::Error>; fn get_entry(config: &Config) -> Result, Self)>; /// Returns the keys that were updated @@ -347,108 +337,5 @@ where &mut self, config: &Config, changed_keys: &[T], - ) -> (Vec, Vec<&str>); -} - -#[cfg(feature = "subscription")] -pub fn config_subscription< - I: 'static + Copy + Send + Sync + Hash, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - id: I, - config_id: Cow<'static, str>, - config_version: u64, -) -> iced_futures::Subscription<(I, Result, T)>)> { - subscription::channel(id, 100, move |mut output| { - let config_id = config_id.clone(); - async move { - let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, false); - - loop { - state = start_listening(state, &mut output, id).await; - } - } - }) -} - -#[cfg(feature = "subscription")] -pub fn config_state_subscription< - I: 'static + Copy + Send + Sync + Hash, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - id: I, - config_id: Cow<'static, str>, - config_version: u64, -) -> iced_futures::Subscription<(I, Result, T)>)> { - subscription::channel(id, 100, move |mut output| { - let config_id = config_id.clone(); - async move { - let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, true); - - loop { - state = start_listening(state, &mut output, id).await; - } - } - }) -} - -async fn start_listening< - I: Copy, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - state: ConfigState, - output: &mut mpsc::Sender<(I, Result, T)>)>, - id: I, -) -> ConfigState { - use iced_futures::futures::{future::pending, StreamExt}; - - match state { - ConfigState::Init(config_id, version, is_state) => { - let (tx, rx) = mpsc::channel(100); - let config = match if is_state { - Config::new_state(&config_id, version) - } else { - Config::new(&config_id, version) - } { - Ok(c) => c, - Err(_) => return ConfigState::Failed, - }; - let watcher = match config.watch(move |_helper, keys| { - let mut tx = tx.clone(); - let _ = tx.try_send(keys.to_vec()); - }) { - Ok(w) => w, - Err(_) => return ConfigState::Failed, - }; - - match T::get_entry(&config) { - Ok(t) => { - _ = output.send((id, Ok(t.clone()))).await; - ConfigState::Waiting(t, watcher, rx, config) - } - Err((errors, t)) => { - _ = output.send((id, Err((errors, t.clone())))).await; - ConfigState::Waiting(t, watcher, rx, config) - } - } - } - ConfigState::Waiting(mut conf_data, watcher, mut rx, config) => match rx.next().await { - Some(keys) => { - let (errors, changed) = conf_data.update_keys(&config, &keys); - - if !changed.is_empty() { - if errors.is_empty() { - _ = output.send((id, Ok(conf_data.clone()))).await; - } else { - _ = output.send((id, Err((errors, conf_data.clone())))).await; - } - } - ConfigState::Waiting(conf_data, watcher, rx, config) - } - None => ConfigState::Failed, - }, - ConfigState::Failed => pending().await, - } + ) -> (Vec, Vec<&'static str>); } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs new file mode 100644 index 0000000..8c4f3c6 --- /dev/null +++ b/cosmic-config/src/subscription.rs @@ -0,0 +1,119 @@ +use iced_futures::futures::SinkExt; +use iced_futures::{futures::channel::mpsc, subscription}; +use notify::RecommendedWatcher; +use std::{borrow::Cow, hash::Hash}; + +use crate::{Config, CosmicConfigEntry}; + +pub enum ConfigState { + Init(Cow<'static, str>, u64, bool), + Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), + Failed, +} + +pub enum ConfigUpdate { + Update(T), + UpdateError(T, Vec), + Failed, +} + +pub fn config_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced_futures::Subscription<(I, Result, T)>)> { + subscription::channel(id, 100, move |mut output| { + let config_id = config_id.clone(); + async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, false); + + loop { + state = start_listening(state, &mut output, id).await; + } + } + }) +} + +pub fn config_state_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced_futures::Subscription<(I, Result, T)>)> { + subscription::channel(id, 100, move |mut output| { + let config_id = config_id.clone(); + async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, true); + + loop { + state = start_listening(state, &mut output, id).await; + } + } + }) +} + +async fn start_listening< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + state: ConfigState, + output: &mut mpsc::Sender<(I, Result, T)>)>, + id: I, +) -> ConfigState { + use iced_futures::futures::{future::pending, StreamExt}; + + match state { + ConfigState::Init(config_id, version, is_state) => { + let (tx, rx) = mpsc::channel(100); + let config = match if is_state { + Config::new_state(&config_id, version) + } else { + Config::new(&config_id, version) + } { + Ok(c) => c, + Err(_) => return ConfigState::Failed, + }; + let watcher = match config.watch(move |_helper, keys| { + let mut tx = tx.clone(); + let _ = tx.try_send(keys.to_vec()); + }) { + Ok(w) => w, + Err(_) => return ConfigState::Failed, + }; + + match T::get_entry(&config) { + Ok(t) => { + _ = output.send((id, Ok(t.clone()))).await; + ConfigState::Waiting(t, watcher, rx, config) + } + Err((errors, t)) => { + _ = output.send((id, Err((errors, t.clone())))).await; + ConfigState::Waiting(t, watcher, rx, config) + } + } + } + ConfigState::Waiting(mut conf_data, watcher, mut rx, config) => match rx.next().await { + Some(keys) => { + let (errors, changed) = conf_data.update_keys(&config, &keys); + + if !changed.is_empty() { + if errors.is_empty() { + _ = output.send((id, Ok(conf_data.clone()))).await; + } else { + _ = output.send((id, Err((errors, conf_data.clone())))).await; + } + } + ConfigState::Waiting(conf_data, watcher, rx, config) + } + None => ConfigState::Failed, + }, + ConfigState::Failed => pending().await, + } +} diff --git a/cosmic-theme/src/model/mode.rs b/cosmic-theme/src/model/mode.rs index 85853dd..d9a9b14 100644 --- a/cosmic-theme/src/model/mode.rs +++ b/cosmic-theme/src/model/mode.rs @@ -7,6 +7,7 @@ pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode"; #[derive( Debug, Clone, Copy, PartialEq, Eq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] +#[version = 1] pub struct ThemeMode { /// The theme dark mode setting. pub is_dark: bool, diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index d454f30..987314d 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -40,6 +40,7 @@ pub enum Layer { PartialEq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] +#[version = 1] pub struct Theme { /// name of the theme pub name: String, @@ -388,6 +389,7 @@ impl From for Theme { cosmic_config::cosmic_config_derive::CosmicConfigEntry, PartialEq, )] +#[version = 1] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 7e7b466..7332a69 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.14.0" -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config"] } once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" diff --git a/src/app/core.rs b/src/app/core.rs index 37a5fad..219fc59 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -69,6 +69,9 @@ pub struct Core { #[cfg(feature = "single-instance")] pub(crate) single_instance: bool, + + #[cfg(feature = "dbus-config")] + pub(crate) settings_daemon: Option>, } impl Default for Core { @@ -114,6 +117,8 @@ impl Default for Core { applet: crate::applet::Context::default(), #[cfg(feature = "single-instance")] single_instance: false, + #[cfg(feature = "dbus-config")] + settings_daemon: None, } } } @@ -208,4 +213,16 @@ impl Core { pub fn system_theme_mode(&self) -> ThemeMode { self.system_theme_mode } + + #[cfg(feature = "dbus-config")] + pub fn watch_config( + &self, + config_id: &'static str, + ) -> iced::Subscription> { + if let Some(settings_daemon) = self.settings_daemon.clone() { + cosmic_config::dbus::watcher_subscription(settings_daemon, config_id) + } else { + iced::Subscription::none() + } + } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 2b405f5..4da3654 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use std::sync::Arc; + use super::{command, Application, ApplicationExt, Core, Subscription}; use crate::theme::{self, Theme, ThemeType, THEME}; use crate::widget::nav_bar; @@ -64,6 +66,9 @@ pub enum Message { WmCapabilities(window::Id, WindowManagerCapabilities), /// Activate the application Activate(String), + #[cfg(feature = "dbus-config")] + /// dbus settings daemon setup + SettingsDaemon(zbus::Result>), } #[derive(Default)] @@ -83,6 +88,13 @@ where fn new((core, flags): Self::Flags) -> (Self, iced::Command) { let (model, command) = T::init(core, flags); + #[cfg(feature = "dbus-config")] + let command = iced::Command::batch(vec![ + command, + iced::Command::perform(cosmic_config::dbus::settings_daemon_proxy(), |p| { + super::Message::Cosmic(super::cosmic::Message::SettingsDaemon(p)) + }), + ]); (Self::new(model), command) } @@ -164,12 +176,26 @@ where keyboard_nav::subscription() .map(Message::KeyboardNav) .map(super::Message::Cosmic), - theme::subscription( - self.app.core().theme_sub_counter, - self.app.core().system_theme_mode.is_dark, - ) - .map(Message::SystemThemeChange) - .map(super::Message::Cosmic), + #[cfg(feature = "dbus-config")] + self.app + .core() + .watch_config::(if self.app.core().system_theme_mode.is_dark { + cosmic_theme::DARK_THEME_ID + } else { + cosmic_theme::LIGHT_THEME_ID + }) + .map(|update| { + for e in update.errors { + tracing::error!("{e}"); + } + Message::SystemThemeChange(crate::theme::Theme::system(Arc::new(update.config))) + }) + .map(super::Message::Cosmic), + #[cfg(not(feature = "dbus-config"))] + theme::subscription(self.app.core().system_theme_mode.is_dark) + .map(Message::SystemThemeChange) + .map(super::Message::Cosmic), + #[cfg(not(feature = "dbus-config"))] cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( 0, cosmic_theme::THEME_MODE_ID.into(), @@ -185,6 +211,17 @@ where } }) .map(super::Message::Cosmic), + #[cfg(feature = "dbus-config")] + self.app + .core() + .watch_config::(cosmic_theme::THEME_MODE_ID) + .map(|update| { + for e in update.errors { + tracing::error!("{e}"); + } + Message::SystemThemeModeChange(update.config) + }) + .map(super::Message::Cosmic), window_events.map(super::Message::Cosmic), #[cfg(feature = "single-instance")] self.app @@ -384,6 +421,15 @@ impl Cosmic { _token, ); } + #[cfg(feature = "dbus-config")] + Message::SettingsDaemon(p) => match p { + Ok(p) => { + self.app.core_mut().settings_daemon = Some(p); + } + Err(e) => { + tracing::error!("Failed to connect to settings daemon: {e}"); + } + }, } iced::Command::none() diff --git a/src/app/mod.rs b/src/app/mod.rs index 4921300..5877d45 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -551,7 +551,7 @@ impl ApplicationExt for App { #[cfg(any(feature = "multi-window", feature = "wayland"))] fn title(&self, id: window::Id) -> &str { - self.core().title.get(&id).map(|s| s.as_str()).unwrap_or("") + self.core().title.get(&id).map_or("", |s| s.as_str()) } #[cfg(not(any(feature = "multi-window", feature = "wayland")))] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index dcfd271..45d769f 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -153,11 +153,11 @@ impl Context { Container::::new(Container::::new(content).style( theme::Container::custom(|theme| { let cosmic = theme.cosmic(); - + let corners = cosmic.corner_radii.clone(); Appearance { text_color: Some(cosmic.background.on.into()), background: Some(Color::from(cosmic.background.base).into()), - border_radius: cosmic.corner_radii.radius_m.into(), + border_radius: corners.radius_m.into(), border_width: 1.0, border_color: cosmic.background.divider.into(), icon_color: Some(cosmic.background.on.into()), diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 0208dd6..42a344a 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -16,6 +16,9 @@ use iced_futures::Subscription; use std::cell::RefCell; use std::sync::Arc; +#[cfg(feature = "dbus-config")] +use cosmic_config::dbus; + pub type CosmicColor = ::palette::rgb::Srgba; pub type CosmicComponent = cosmic_theme::Component; pub type CosmicTheme = cosmic_theme::Theme; @@ -68,9 +71,12 @@ pub fn is_high_contrast() -> bool { } /// Watches for changes to the system's theme preference. -pub fn subscription(id: u64, is_dark: bool) -> Subscription { +pub fn subscription(is_dark: bool) -> Subscription { config_subscription::<_, crate::cosmic_theme::Theme>( - (id, is_dark), + ( + std::any::TypeId::of::(), + is_dark, + ), if is_dark { cosmic_theme::DARK_THEME_ID } else {