diff --git a/Cargo.lock b/Cargo.lock index 61bb0f6b..de8a768a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -706,6 +706,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -1141,16 +1163,19 @@ name = "cosmic-applet-time" version = "0.1.0" dependencies = [ "chrono", + "chrono-tz", "i18n-embed 0.14.1", "i18n-embed-fl 0.8.0", "icu", "libcosmic", "once_cell", "rust-embed 8.5.0", + "timedate-zbus", "tokio", "tracing", "tracing-log", "tracing-subscriber", + "zbus 4.3.1", ] [[package]] @@ -4436,6 +4461,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -4458,6 +4492,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.2" @@ -5705,6 +5749,14 @@ dependencies = [ "time-core", ] +[[package]] +name = "timedate-zbus" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#cd21ddcb1b5cbfc80ab84b34d3c8b1ff3d81179a" +dependencies = [ + "zbus 4.3.1", +] + [[package]] name = "tiny-keccak" version = "2.0.2" diff --git a/cosmic-applet-time/Cargo.toml b/cosmic-applet-time/Cargo.toml index 9ae0ea76..212ee893 100644 --- a/cosmic-applet-time/Cargo.toml +++ b/cosmic-applet-time/Cargo.toml @@ -6,6 +6,7 @@ license = "GPL-3.0" [dependencies] chrono = { version = "0.4.35", features = ["clock"] } +chrono-tz = "0.9" i18n-embed-fl.workspace = true i18n-embed.workspace = true libcosmic.workspace = true @@ -15,4 +16,6 @@ tokio = { version = "1.36.0", features = ["time"] } tracing-log.workspace = true tracing-subscriber.workspace = true tracing.workspace = true -icu = { version = "1.4.0", features = ["experimental", "compiled_data", "icu_datetime_experimental"]} \ No newline at end of file +icu = { version = "1.4.0", features = ["experimental", "compiled_data", "icu_datetime_experimental"]} +zbus.workspace = true +timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" } diff --git a/cosmic-applet-time/src/window.rs b/cosmic-applet-time/src/window.rs index 408046ae..ef425ec6 100644 --- a/cosmic-applet-time/src/window.rs +++ b/cosmic-applet-time/src/window.rs @@ -9,12 +9,14 @@ use cosmic::{ applet::{cosmic_panel_config::PanelAnchor, menu_button, padded_control}, cctk::sctk::reexports::calloop, iced::{ + futures::{FutureExt, TryFutureExt}, subscription, wayland::popup::{destroy_popup, get_popup}, widget::{column, row, text, vertical_space}, window, Alignment, Length, Rectangle, Subscription, }, iced_core::alignment::{Horizontal, Vertical}, + iced_runtime::command, iced_style::application, iced_widget::{horizontal_rule, Column}, widget::{ @@ -23,6 +25,8 @@ use cosmic::{ }, Command, Element, Theme, }; +use timedate_zbus::TimeDateProxy; +use zbus::Connection; use icu::{ calendar::DateTime, @@ -47,17 +51,20 @@ use cosmic::applet::token::subscription::{ pub struct Window { core: cosmic::app::Core, popup: Option, - now: chrono::DateTime, + now: chrono::DateTime, + timezone: Option, date_selected: chrono::NaiveDate, rectangle_tracker: Option>, rectangle: Rectangle, token_tx: Option>, config: TimeAppletConfig, locale: Locale, + conn: Option, } #[derive(Debug, Clone)] pub enum Message { + Init(Option), TogglePopup, CloseRequested(window::Id), Tick, @@ -68,6 +75,7 @@ pub enum Message { OpenDateTimeSettings, Token(TokenUpdate), ConfigChanged(TimeAppletConfig), + TimezoneUpdate(Option), } impl Window { @@ -126,21 +134,28 @@ impl cosmic::Application for Window { } }; - let now: chrono::prelude::DateTime = chrono::Local::now(); + // Chrono evaluates the local timezone once whereby it's stored in a thread local + // variable but never updated + // Instead of using the local timezone, we will store an offset that is updated if the + // timezone is ever externally changed + let now: chrono::DateTime = chrono::Local::now().fixed_offset(); ( Self { core, popup: None, now, + timezone: None, date_selected: chrono::NaiveDate::from(now.naive_local()), rectangle_tracker: None, rectangle: Rectangle::default(), token_tx: None, config: TimeAppletConfig::default(), locale, + conn: None, }, - Command::none(), + Command::single(command::Action::Future(Box::pin(Connection::system()))) + .map(|res| Message::Init(res.ok()).into()), ) } @@ -170,10 +185,57 @@ impl cosmic::Application for Window { }) } + let conn = self.conn.clone(); + fn timezone_subscription(conn: Option) -> Subscription { + use cosmic::iced_futures::futures::StreamExt; + let Some(conn) = conn else { + return Subscription::none(); + }; + + // Update applet's timezone if the system's timezone changes + subscription::unfold("timezone-sub", (), move |_| { + let conn = conn.clone(); + async move { + TimeDateProxy::new(&conn) + .inspect_err(|e| { + tracing::error!("Failed to connect to timedate endpoint: {e:?}") + }) + .then(|proxy| async move { + let Ok(proxy) = proxy else { + return (Message::TimezoneUpdate(None), ()); + }; + proxy + .receive_timezone_changed() + .then(|stream| stream.into_future()) + .then(|(prop_opt, _)| async move { + if let Some(property) = prop_opt { + property + .get() + .await + .inspect_err(|e| { + tracing::error!( + "Failed to receive time zone update: {e:?}" + ) + }) + .ok() + .map(|tz| (Message::TimezoneUpdate(Some(tz)), ())) + .unwrap_or((Message::TimezoneUpdate(None), ())) + } else { + (Message::TimezoneUpdate(None), ()) + } + }) + .await + }) + .await + } + }) + } + Subscription::batch(vec![ rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)), time_subscription().map(|_| Message::Tick), activation_token_subscription(0).map(Message::Token), + timezone_subscription(conn), self.core.watch_config(Self::APP_ID).map(|u| { for err in u.errors { tracing::error!(?err, "Error watching config"); @@ -188,6 +250,10 @@ impl cosmic::Application for Window { message: Self::Message, ) -> cosmic::iced::Command> { match message { + Message::Init(conn) => { + self.conn = conn; + Command::none() + } Message::TogglePopup => { if let Some(p) = self.popup.take() { destroy_popup(p) @@ -220,7 +286,10 @@ impl cosmic::Application for Window { } } Message::Tick => { - self.now = chrono::Local::now(); + self.now = self + .timezone + .map(|tz| chrono::Local::now().with_timezone(&tz).fixed_offset()) + .unwrap_or_else(|| chrono::Local::now().into()); Command::none() } Message::Rectangle(u) => { @@ -306,6 +375,16 @@ impl cosmic::Application for Window { self.config = c; Command::none() } + Message::TimezoneUpdate(timezone) => { + if let Some(timezone) = + timezone.and_then(|timezone| timezone.parse::().ok()) + { + self.now = chrono::Local::now().with_timezone(&timezone).fixed_offset(); + self.timezone = Some(timezone); + } + + self.update(Message::Tick) + } } }