From 323e8a55b2bfe34b6b47fb4e85f0991a10ea924d Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sat, 17 Aug 2024 00:40:10 -0400 Subject: [PATCH] fix(time): Update applet timezone on change Closes: #582 The chrono crate caches the local timezone but doesn't update it. This makes sense because it'd be inefficient to constantly evaluate the local timezone. Instead of using the local timezone, this patch changes the applet to use a fixed offset internally which is updated if the external timezone changes. --- Cargo.lock | 52 +++++++++++++++++++ cosmic-applet-time/Cargo.toml | 5 +- cosmic-applet-time/src/window.rs | 87 ++++++++++++++++++++++++++++++-- 3 files changed, 139 insertions(+), 5 deletions(-) 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) + } } }