From a1d363627aa6ea24e06bd07c8210ffaaa1a83fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliaksandr=20Tru=C5=A1?= Date: Wed, 19 Nov 2025 16:17:28 +0100 Subject: [PATCH 1/2] WIP add theme applet --- Cargo.lock | 59 ++++- Cargo.toml | 1 + cosmic-applet-theme/Cargo.toml | 17 ++ .../com.system76.CosmicAppletTheme.desktop | 16 ++ ...om.system76.CosmicAppletTheme-symbolic.svg | 5 + cosmic-applet-theme/i18n.toml | 4 + .../i18n/be/cosmic_applet_theme.ftl | 4 + .../i18n/en/cosmic_applet_theme.ftl | 4 + cosmic-applet-theme/src/lib.rs | 203 ++++++++++++++++++ cosmic-applet-theme/src/localize.rs | 48 +++++ cosmic-applet-theme/src/main.rs | 13 ++ cosmic-applets/Cargo.toml | 1 + cosmic-applets/src/main.rs | 1 + justfile | 2 +- 14 files changed, 372 insertions(+), 6 deletions(-) create mode 100644 cosmic-applet-theme/Cargo.toml create mode 100644 cosmic-applet-theme/data/com.system76.CosmicAppletTheme.desktop create mode 100644 cosmic-applet-theme/data/icons/scalable/apps/com.system76.CosmicAppletTheme-symbolic.svg create mode 100644 cosmic-applet-theme/i18n.toml create mode 100644 cosmic-applet-theme/i18n/be/cosmic_applet_theme.ftl create mode 100644 cosmic-applet-theme/i18n/en/cosmic_applet_theme.ftl create mode 100644 cosmic-applet-theme/src/lib.rs create mode 100644 cosmic-applet-theme/src/localize.rs create mode 100644 cosmic-applet-theme/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9b160617a..08d1f6ac0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,6 +1340,22 @@ dependencies = [ "zbus 5.12.0", ] +[[package]] +name = "cosmic-applet-theme" +version = "0.1.0" +dependencies = [ + "dirs 5.0.1", + "i18n-embed", + "i18n-embed-fl", + "libcosmic", + "rust-embed", + "tokio", + "tracing", + "tracing-log", + "tracing-subscriber", + "zbus 5.12.0", +] + [[package]] name = "cosmic-applet-tiling" version = "0.1.0" @@ -1413,6 +1429,7 @@ dependencies = [ "cosmic-applet-notifications", "cosmic-applet-power", "cosmic-applet-status-area", + "cosmic-applet-theme", "cosmic-applet-tiling", "cosmic-applet-time", "cosmic-applet-workspaces", @@ -1462,7 +1479,7 @@ dependencies = [ "atomicwrites", "cosmic-config-derive", "cosmic-settings-daemon", - "dirs", + "dirs 6.0.0", "futures-util", "iced_futures", "known-folders", @@ -1510,7 +1527,7 @@ name = "cosmic-freedesktop-icons" version = "0.4.0" source = "git+https://github.com/pop-os/freedesktop-icons#689c60d428f46dc59316eafa22297e196afa4b15" dependencies = [ - "dirs", + "dirs 6.0.0", "ini_core", "memmap2 0.9.9", "thiserror 2.0.17", @@ -1660,7 +1677,7 @@ dependencies = [ "almost", "cosmic-config", "csscolorparser", - "dirs", + "dirs 6.0.0", "palette", "ron", "serde", @@ -1972,13 +1989,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -1989,7 +2027,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -5533,6 +5571,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 6aaf267b4..f31689a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "cosmic-applet-status-area", "cosmic-applet-tiling", "cosmic-applet-time", + "cosmic-applet-theme", "cosmic-applet-workspaces", "cosmic-panel-button", "cosmic-applet-input-sources", diff --git a/cosmic-applet-theme/Cargo.toml b/cosmic-applet-theme/Cargo.toml new file mode 100644 index 000000000..eb2969b72 --- /dev/null +++ b/cosmic-applet-theme/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cosmic-applet-theme" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" + +[dependencies] +dirs = "5.0" +i18n-embed-fl.workspace = true +i18n-embed.workspace = true +libcosmic.workspace = true +rust-embed.workspace = true +tokio.workspace = true +tracing-log.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true +zbus.workspace = true diff --git a/cosmic-applet-theme/data/com.system76.CosmicAppletTheme.desktop b/cosmic-applet-theme/data/com.system76.CosmicAppletTheme.desktop new file mode 100644 index 000000000..c185dfb87 --- /dev/null +++ b/cosmic-applet-theme/data/com.system76.CosmicAppletTheme.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Type=Application +Name=Theme Toggle Applet +Comment=Toggle system light/dark theme +Exec=cosmic-applet-theme +Icon=com.system76.CosmicAppletTheme-symbolic +Terminal=false +Categories=COSMIC; +Keywords=COSMIC;Iced; +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +StartupNotify=true +NoDisplay=true +X-CosmicApplet=true +X-CosmicShrinkable=true +X-CosmicHoverPopup=Auto +X-OverflowPriority=10 diff --git a/cosmic-applet-theme/data/icons/scalable/apps/com.system76.CosmicAppletTheme-symbolic.svg b/cosmic-applet-theme/data/icons/scalable/apps/com.system76.CosmicAppletTheme-symbolic.svg new file mode 100644 index 000000000..20074917f --- /dev/null +++ b/cosmic-applet-theme/data/icons/scalable/apps/com.system76.CosmicAppletTheme-symbolic.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/cosmic-applet-theme/i18n.toml b/cosmic-applet-theme/i18n.toml new file mode 100644 index 000000000..76f7c3103 --- /dev/null +++ b/cosmic-applet-theme/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en" + +[fluent] +assets_dir = "i18n" diff --git a/cosmic-applet-theme/i18n/be/cosmic_applet_theme.ftl b/cosmic-applet-theme/i18n/be/cosmic_applet_theme.ftl new file mode 100644 index 000000000..fbbe44cfa --- /dev/null +++ b/cosmic-applet-theme/i18n/be/cosmic_applet_theme.ftl @@ -0,0 +1,4 @@ +applet-theme-title = Theme Toggle +applet-theme-description = An applet to toggle between light and dark themes. + +toggle-theme = Пераключыць тэму diff --git a/cosmic-applet-theme/i18n/en/cosmic_applet_theme.ftl b/cosmic-applet-theme/i18n/en/cosmic_applet_theme.ftl new file mode 100644 index 000000000..d5a1ec7c6 --- /dev/null +++ b/cosmic-applet-theme/i18n/en/cosmic_applet_theme.ftl @@ -0,0 +1,4 @@ +applet-theme-title = Theme Toggle +applet-theme-description = An applet to toggle between light and dark themes. + +toggle-theme = Toggle Theme diff --git a/cosmic-applet-theme/src/lib.rs b/cosmic-applet-theme/src/lib.rs new file mode 100644 index 000000000..8555d4261 --- /dev/null +++ b/cosmic-applet-theme/src/lib.rs @@ -0,0 +1,203 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::{ + Element, Task, app, + cosmic_theme::Spacing, + iced::{ + Alignment, Length, + platform_specific::shell::commands::popup::{destroy_popup, get_popup}, + widget::{self, column, row}, + window, + }, + surface, theme, + widget::{icon, text}, +}; +use std::{fs, path::PathBuf, sync::LazyLock}; + +use crate::localize::localize; + +pub mod localize; + +static SUBSURFACE_ID: LazyLock = + LazyLock::new(|| cosmic::widget::Id::new("subsurface")); + +pub fn run() -> cosmic::iced::Result { + localize(); + + cosmic::applet::run::(()) +} + +struct Theme { + core: cosmic::app::Core, + icon_name: String, + popup: Option, + subsurface_id: window::Id, + is_dark: bool, + theme_file: PathBuf, +} + +#[derive(Debug, Clone)] +enum ThemeAction { + ToggleTheme, +} + +#[derive(Debug, Clone)] +enum Message { + Action(ThemeAction), + TogglePopup, + Closed(window::Id), + Surface(surface::Action), +} + +impl cosmic::Application for Theme { + type Executor = cosmic::SingleThreadExecutor; + type Flags = (); + type Message = Message; + const APP_ID: &'static str = "com.system76.CosmicAppletTheme"; + + fn core(&self) -> &cosmic::app::Core { + &self.core + } + + fn core_mut(&mut self) -> &mut cosmic::app::Core { + &mut self.core + } + + fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task) { + let theme_file = dirs::config_dir() + .expect("Failed to find config dir") + .join("cosmic/com.system76.CosmicTheme.Mode/v1/is_dark"); + + let is_dark = fs::read_to_string(&theme_file) + .map(|s| s.trim() == "true") + .unwrap_or(true); // Default to dark mode + + // Ensure file exists + if !theme_file.exists() { + if let Some(parent) = theme_file.parent() { + fs::create_dir_all(parent).expect("Failed to create theme config directory"); + } + fs::write(&theme_file, is_dark.to_string()).expect("Failed to write initial theme state"); + } + + let icon_name = if is_dark { + "weather-clear-night-symbolic" + } else { + "weather-sunny-symbolic" + }.to_string(); + + ( + Self { + core, + icon_name, + subsurface_id: window::Id::unique(), + popup: Option::default(), + is_dark, + theme_file, + }, + Task::none(), + ) + } + + fn on_close_requested(&self, id: window::Id) -> Option { + Some(Message::Closed(id)) + } + + fn update(&mut self, message: Message) -> app::Task { + match message { + Message::TogglePopup => { + if let Some(p) = self.popup.take() { + destroy_popup(p) + } else { + let new_id = window::Id::unique(); + self.popup.replace(new_id); + + let popup_settings = self.core.applet.get_popup_settings( + self.core.main_window_id().unwrap(), + new_id, + None, + None, + None, + ); + + get_popup(popup_settings) + } + } + Message::Action(action) => { + match action { + ThemeAction::ToggleTheme => { + self.is_dark = !self.is_dark; + fs::write(&self.theme_file, self.is_dark.to_string()).expect("Failed to write theme state"); + self.icon_name = if self.is_dark { + "weather-clear-night-symbolic" + } else { + "weather-sunny-symbolic" + }.to_string(); + Task::none() + } + } + } + + Message::Closed(id) => { + if self.popup == Some(id) { + self.popup = None; + } + Task::none() + } + Message::Surface(a) => { + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(a), + )); + } + } + } + + fn view(&self) -> Element<'_, Message> { + self.core + .applet + .icon_button(&self.icon_name) + .on_press_down(Message::Action(ThemeAction::ToggleTheme)) + .into() + } + + fn view_window(&self, id: window::Id) -> Element<'_, Message> { + let Spacing { + space_xxs, + space_s, + space_m, + .. + } = theme::active().cosmic().spacing; + + if matches!(self.popup, Some(p) if p == id) { + let toggle = cosmic::widget::button::custom( + widget::container( + row![ + icon::from_name(if self.is_dark { "weather-sunny-symbolic" } else { "weather-clear-night-symbolic" }).size(24).symbolic(true).icon(), + text::body(fl!("toggle-theme")), + ] + .align_y(Alignment::Center) + .spacing(space_xxs) + ) + .center(Length::Fill), + ) + .on_press(Message::Action(ThemeAction::ToggleTheme)) + .height(Length::Fixed(40.0)) + .class(theme::Button::Text); + + let content = column![toggle] + .align_x(Alignment::Start) + .padding([8, 0]); + + self.core.applet.popup_container(content).into() + } else { + widget::text("").into() + } + } + + fn style(&self) -> Option { + Some(cosmic::applet::style()) + } +} + + diff --git a/cosmic-applet-theme/src/localize.rs b/cosmic-applet-theme/src/localize.rs new file mode 100644 index 000000000..c1fca2531 --- /dev/null +++ b/cosmic-applet-theme/src/localize.rs @@ -0,0 +1,48 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use i18n_embed::{ + DefaultLocalizer, LanguageLoader, Localizer, + fluent::{FluentLanguageLoader, fluent_language_loader}, +}; +use rust_embed::RustEmbed; +use std::sync::LazyLock; + +#[derive(RustEmbed)] +#[folder = "i18n/"] +struct Localizations; + +pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { + let loader: FluentLanguageLoader = fluent_language_loader!(); + + loader + .load_fallback_language(&Localizations) + .expect("Error while loading fallback language"); + + loader +}); + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) + }}; + + ($message_id:literal, $($args:expr),*) => {{ + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) + }}; +} + +// Get the `Localizer` to be used for localizing this library. +pub fn localizer() -> Box { + Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) +} + +pub fn localize() { + let localizer = localizer(); + let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); + + if let Err(error) = localizer.select(&requested_languages) { + eprintln!("Error while loading language for App List {error}"); + } +} diff --git a/cosmic-applet-theme/src/main.rs b/cosmic-applet-theme/src/main.rs new file mode 100644 index 000000000..d1a3fb534 --- /dev/null +++ b/cosmic-applet-theme/src/main.rs @@ -0,0 +1,13 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: GPL-3.0-only + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn main() -> cosmic::iced::Result { + tracing_subscriber::fmt::init(); + let _ = tracing_log::LogTracer::init(); + + tracing::info!("Starting theme applet with version {VERSION}"); + + cosmic_applet_theme::run() +} diff --git a/cosmic-applets/Cargo.toml b/cosmic-applets/Cargo.toml index c39ad7208..cf7429134 100644 --- a/cosmic-applets/Cargo.toml +++ b/cosmic-applets/Cargo.toml @@ -19,6 +19,7 @@ cosmic-applet-tiling = { path = "../cosmic-applet-tiling" } cosmic-applet-time = { path = "../cosmic-applet-time" } cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" } cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" } +cosmic-applet-theme = { path = "../cosmic-applet-theme" } cosmic-panel-button = { path = "../cosmic-panel-button" } libcosmic.workspace = true tracing.workspace = true diff --git a/cosmic-applets/src/main.rs b/cosmic-applets/src/main.rs index f62bbf210..f5c8ee293 100644 --- a/cosmic-applets/src/main.rs +++ b/cosmic-applets/src/main.rs @@ -31,6 +31,7 @@ fn main() -> cosmic::iced::Result { "cosmic-applet-time" => cosmic_applet_time::run(), "cosmic-applet-workspaces" => cosmic_applet_workspaces::run(), "cosmic-applet-input-sources" => cosmic_applet_input_sources::run(), + "cosmic-applet-theme" => cosmic_applet_theme::run(), "cosmic-panel-button" => cosmic_panel_button::run(), _ => Ok(()), } diff --git a/justfile b/justfile index 54e110267..591baff46 100644 --- a/justfile +++ b/justfile @@ -58,7 +58,7 @@ _install_metainfo: install -Dm0644 {{metainfo-src}} {{metainfo-dst}} # Installs files into the system -install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo) +install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletTheme' 'cosmic-applet-theme') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo) # Vendor Cargo dependencies locally vendor: From caa8bcabea73a39b969a463ea25946d6b493e455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliaksandr=20Tru=C5=A1?= Date: Thu, 20 Nov 2025 10:36:08 +0100 Subject: [PATCH 2/2] Add subscription for theme file --- Cargo.lock | 72 ++++++++++- cosmic-applet-theme/Cargo.toml | 1 + cosmic-applet-theme/src/lib.rs | 113 ++++++++---------- cosmic-applet-theme/src/theme_subscription.rs | 62 ++++++++++ 4 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 cosmic-applet-theme/src/theme_subscription.rs diff --git a/Cargo.lock b/Cargo.lock index 08d1f6ac0..36c99f489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1348,6 +1348,7 @@ dependencies = [ "i18n-embed", "i18n-embed-fl", "libcosmic", + "notify 6.1.1", "rust-embed", "tokio", "tracing", @@ -1483,7 +1484,7 @@ dependencies = [ "futures-util", "iced_futures", "known-folders", - "notify", + "notify 8.2.0", "ron", "serde", "tokio", @@ -1712,6 +1713,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -2334,6 +2344,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-crate" version = "0.6.3" @@ -3743,6 +3765,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + [[package]] name = "inotify" version = "0.11.0" @@ -4394,6 +4427,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.1.0" @@ -4553,6 +4598,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.10.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify 0.9.6", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "notify" version = "8.2.0" @@ -4561,11 +4625,11 @@ checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ "bitflags 2.10.0", "fsevent-sys", - "inotify", + "inotify 0.11.0", "kqueue", "libc", "log", - "mio", + "mio 1.1.0", "notify-types", "walkdir", "windows-sys 0.60.2", @@ -6579,7 +6643,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", - "mio", + "mio 1.1.0", "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", diff --git a/cosmic-applet-theme/Cargo.toml b/cosmic-applet-theme/Cargo.toml index eb2969b72..00c9c4653 100644 --- a/cosmic-applet-theme/Cargo.toml +++ b/cosmic-applet-theme/Cargo.toml @@ -15,3 +15,4 @@ tracing-log.workspace = true tracing-subscriber.workspace = true tracing.workspace = true zbus.workspace = true +notify = "6.1" diff --git a/cosmic-applet-theme/src/lib.rs b/cosmic-applet-theme/src/lib.rs index 8555d4261..a38fa8a72 100644 --- a/cosmic-applet-theme/src/lib.rs +++ b/cosmic-applet-theme/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023 System76 +// Copyright 2025 System76 // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ @@ -6,21 +6,18 @@ use cosmic::{ cosmic_theme::Spacing, iced::{ Alignment, Length, - platform_specific::shell::commands::popup::{destroy_popup, get_popup}, widget::{self, column, row}, window, }, - surface, theme, + theme, widget::{icon, text}, }; -use std::{fs, path::PathBuf, sync::LazyLock}; +use std::{fs, path::PathBuf}; use crate::localize::localize; pub mod localize; - -static SUBSURFACE_ID: LazyLock = - LazyLock::new(|| cosmic::widget::Id::new("subsurface")); +mod theme_subscription; pub fn run() -> cosmic::iced::Result { localize(); @@ -32,7 +29,6 @@ struct Theme { core: cosmic::app::Core, icon_name: String, popup: Option, - subsurface_id: window::Id, is_dark: bool, theme_file: PathBuf, } @@ -45,9 +41,8 @@ enum ThemeAction { #[derive(Debug, Clone)] enum Message { Action(ThemeAction), - TogglePopup, Closed(window::Id), - Surface(surface::Action), + ThemeUpdate(theme_subscription::ThemeUpdate), } impl cosmic::Application for Theme { @@ -78,20 +73,21 @@ impl cosmic::Application for Theme { if let Some(parent) = theme_file.parent() { fs::create_dir_all(parent).expect("Failed to create theme config directory"); } - fs::write(&theme_file, is_dark.to_string()).expect("Failed to write initial theme state"); + fs::write(&theme_file, is_dark.to_string()) + .expect("Failed to write initial theme state"); } let icon_name = if is_dark { "weather-clear-night-symbolic" } else { "weather-sunny-symbolic" - }.to_string(); + } + .to_string(); ( Self { core, icon_name, - subsurface_id: window::Id::unique(), popup: Option::default(), is_dark, theme_file, @@ -106,38 +102,20 @@ impl cosmic::Application for Theme { fn update(&mut self, message: Message) -> app::Task { match message { - Message::TogglePopup => { - if let Some(p) = self.popup.take() { - destroy_popup(p) - } else { - let new_id = window::Id::unique(); - self.popup.replace(new_id); - - let popup_settings = self.core.applet.get_popup_settings( - self.core.main_window_id().unwrap(), - new_id, - None, - None, - None, - ); - - get_popup(popup_settings) - } - } - Message::Action(action) => { - match action { - ThemeAction::ToggleTheme => { - self.is_dark = !self.is_dark; - fs::write(&self.theme_file, self.is_dark.to_string()).expect("Failed to write theme state"); - self.icon_name = if self.is_dark { - "weather-clear-night-symbolic" - } else { - "weather-sunny-symbolic" - }.to_string(); - Task::none() + Message::Action(action) => match action { + ThemeAction::ToggleTheme => { + self.is_dark = !self.is_dark; + fs::write(&self.theme_file, self.is_dark.to_string()) + .expect("Failed to write theme state"); + self.icon_name = if self.is_dark { + "weather-clear-night-symbolic" + } else { + "weather-sunny-symbolic" } + .to_string(); + Task::none() } - } + }, Message::Closed(id) => { if self.popup == Some(id) { @@ -145,11 +123,19 @@ impl cosmic::Application for Theme { } Task::none() } - Message::Surface(a) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(a), - )); - } + + Message::ThemeUpdate(update) => match update { + theme_subscription::ThemeUpdate::Changed(is_dark) => { + self.is_dark = is_dark; + self.icon_name = if is_dark { + "weather-clear-night-symbolic" + } else { + "weather-sunny-symbolic" + } + .to_string(); + Task::none() + } + }, } } @@ -162,22 +148,24 @@ impl cosmic::Application for Theme { } fn view_window(&self, id: window::Id) -> Element<'_, Message> { - let Spacing { - space_xxs, - space_s, - space_m, - .. - } = theme::active().cosmic().spacing; + let Spacing { space_xxs, .. } = theme::active().cosmic().spacing; if matches!(self.popup, Some(p) if p == id) { let toggle = cosmic::widget::button::custom( widget::container( row![ - icon::from_name(if self.is_dark { "weather-sunny-symbolic" } else { "weather-clear-night-symbolic" }).size(24).symbolic(true).icon(), + icon::from_name(if self.is_dark { + "weather-sunny-symbolic" + } else { + "weather-clear-night-symbolic" + }) + .size(24) + .symbolic(true) + .icon(), text::body(fl!("toggle-theme")), ] .align_y(Alignment::Center) - .spacing(space_xxs) + .spacing(space_xxs), ) .center(Length::Fill), ) @@ -185,9 +173,7 @@ impl cosmic::Application for Theme { .height(Length::Fixed(40.0)) .class(theme::Button::Text); - let content = column![toggle] - .align_x(Alignment::Start) - .padding([8, 0]); + let content = column![toggle].align_x(Alignment::Start).padding([8, 0]); self.core.applet.popup_container(content).into() } else { @@ -198,6 +184,11 @@ impl cosmic::Application for Theme { fn style(&self) -> Option { Some(cosmic::applet::style()) } -} - + fn subscription(&self) -> cosmic::iced::Subscription { + cosmic::iced::Subscription::batch(vec![ + theme_subscription::theme_subscription(self.theme_file.clone()) + .map(Message::ThemeUpdate), + ]) + } +} diff --git a/cosmic-applet-theme/src/theme_subscription.rs b/cosmic-applet-theme/src/theme_subscription.rs new file mode 100644 index 000000000..70801a9c9 --- /dev/null +++ b/cosmic-applet-theme/src/theme_subscription.rs @@ -0,0 +1,62 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic::iced::{Subscription, futures::SinkExt, stream}; +use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::PathBuf; +use tokio::sync::mpsc; + +#[derive(Debug, Clone)] +pub enum ThemeUpdate { + Changed(bool), // is_dark +} + +pub fn theme_subscription(theme_file: PathBuf) -> Subscription { + Subscription::run_with_id( + "theme-file-watch", + stream::channel(1, move |mut output| async move { + let (tx, mut rx) = mpsc::channel(20); + + let mut watcher = match RecommendedWatcher::new( + move |res: Result| { + if let Ok(event) = res { + if matches!(event.kind, notify::EventKind::Modify(_)) { + let _ = tx.try_send(()); + } + } + }, + Config::default(), + ) { + Ok(watcher) => watcher, + Err(e) => { + tracing::error!("Failed to create file watcher: {}", e); + return; + } + }; + + if let Err(e) = watcher.watch(&theme_file, RecursiveMode::NonRecursive) { + tracing::error!("Failed to watch theme file: {}", e); + return; + } + + // Send initial state + if let Ok(content) = std::fs::read_to_string(&theme_file) { + let is_dark = content.trim() == "true"; + tracing::debug!("Theme subscription: initial state is_dark={}", is_dark); + let _ = output.send(ThemeUpdate::Changed(is_dark)).await; + } else { + tracing::warn!("Theme subscription: failed to read initial theme file"); + } + + while let Some(_) = rx.recv().await { + if let Ok(content) = std::fs::read_to_string(&theme_file) { + let is_dark = content.trim() == "true"; + tracing::debug!("Theme subscription: detected change, is_dark={}", is_dark); + let _ = output.send(ThemeUpdate::Changed(is_dark)).await; + } else { + tracing::warn!("Theme subscription: failed to read theme file on change"); + } + } + }), + ) +}