From 37cff1867284360d4ed3630865e0d38fa71997da Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 10 Apr 2025 16:39:32 -0400 Subject: [PATCH] refactor: use icu for datetime --- Cargo.lock | 440 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 9 + src/greeter.rs | 68 ++++---- src/lib.rs | 2 + src/localize.rs | 20 +-- src/locker.rs | 47 +++--- src/time.rs | 189 +++++++++++++++++++++ 7 files changed, 686 insertions(+), 89 deletions(-) create mode 100644 src/time.rs diff --git a/Cargo.lock b/Cargo.lock index 8c04323..7da7890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,6 +327,16 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fn-stream" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e71711442f1016c768c259bec59300a10efe753bc3e686ec19e2c6a54a97c29b" +dependencies = [ + "futures-util", + "pin-project-lite", +] + [[package]] name = "async-fs" version = "2.1.2" @@ -722,6 +732,16 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "calendrical_calculations" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97f73e95d668625c9b28a3072e6326773785a0cf807de9f3d632778438f3d38" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "calloop" version = "0.13.0" @@ -820,6 +840,27 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1019,6 +1060,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cosmic-bg-config" version = "0.1.0" @@ -1118,7 +1168,10 @@ dependencies = [ name = "cosmic-greeter" version = "0.1.0" dependencies = [ + "anyhow", + "async-fn-stream", "chrono", + "chrono-tz", "clap_lex", "cosmic-bg-config", "cosmic-comp-config", @@ -1133,6 +1186,7 @@ dependencies = [ "greetd_ipc", "i18n-embed", "i18n-embed-fl", + "icu", "libcosmic", "log", "logind-zbus", @@ -1142,6 +1196,7 @@ dependencies = [ "ron 0.10.1", "rust-embed", "shlex", + "timedate-zbus", "tokio", "upower_dbus", "vergen", @@ -1699,7 +1754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1816,6 +1871,17 @@ dependencies = [ "toml", ] +[[package]] +name = "fixed_decimal" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0febbeb1118a9ecdee6e4520ead6b54882e843dd0592ad233247dbee84c53db8" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + [[package]] name = "flate2" version = "1.1.0" @@ -2726,6 +2792,100 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "icu" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff5e3018d703f168b00dcefa540a65f1bbc50754ae32f3f5f0e43fe5ee51502" +dependencies = [ + "icu_calendar", + "icu_casemap", + "icu_collator", + "icu_collections", + "icu_datetime", + "icu_decimal", + "icu_experimental", + "icu_list", + "icu_locid", + "icu_locid_transform", + "icu_normalizer", + "icu_plurals", + "icu_properties", + "icu_provider", + "icu_segmenter", + "icu_timezone", +] + +[[package]] +name = "icu_calendar" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7265b2137f9a36f7634a308d91f984574bbdba8cfd95ceffe1c345552275a8ff" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locid", + "icu_locid_transform", + "icu_provider", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820499e77e852162190608b4f444e7b4552619150eafc39a9e39333d9efae9e1" + +[[package]] +name = "icu_casemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff0c8ae9f8d31b12e27fc385ff9ab1f3cd9b17417c665c49e4ec958c37da75f" +dependencies = [ + "displaydoc", + "icu_casemap_data", + "icu_collections", + "icu_locid", + "icu_properties", + "icu_provider", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_casemap_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd9f6276270c85a5cd54611adbbf94e993ec464a2a86a452a6c565b7ded5d9" + +[[package]] +name = "icu_collator" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d370371887d31d56f361c3eaa15743e54f13bc677059c9191c77e099ed6966b2" +dependencies = [ + "displaydoc", + "icu_collator_data", + "icu_collections", + "icu_locid_transform", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b353986d77d28991eca4dea5ef2b8982f639342ae19ca81edc44f048bc38ebb" + [[package]] name = "icu_collections" version = "1.5.0" @@ -2738,6 +2898,112 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_datetime" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d115efb85e08df3fd77e77f52e7e087545a783fffba8be80bfa2102f306b1780" +dependencies = [ + "displaydoc", + "either", + "fixed_decimal", + "icu_calendar", + "icu_datetime_data", + "icu_decimal", + "icu_locid", + "icu_locid_transform", + "icu_plurals", + "icu_provider", + "icu_timezone", + "litemap", + "smallvec", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_datetime_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef5f04076123cab1b7a926a7083db27fe0d7a0e575adb984854aae3f3a6507d" + +[[package]] +name = "icu_decimal" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8fd98f86ec0448d85e1edf8884e4e318bb2e121bd733ec929a05c0a5e8b0eb" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locid_transform", + "icu_provider", + "writeable", +] + +[[package]] +name = "icu_decimal_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c95dd97f5ccf6d837a9c115496ec7d36646fa86ca18e7f1412115b4c820ae2" + +[[package]] +name = "icu_experimental" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844ad7b682a165c758065d694bc4d74ac67f176da1c499a04d85d492c0f193b7" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_collections", + "icu_decimal", + "icu_experimental_data", + "icu_locid", + "icu_locid_transform", + "icu_normalizer", + "icu_pattern", + "icu_plurals", + "icu_properties", + "icu_provider", + "litemap", + "num-bigint", + "num-rational", + "num-traits", + "smallvec", + "tinystr", + "writeable", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_experimental_data" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121df92eafb8f5286d4e8ff401c1e7db8384377f806db3f8db77b91e5b7bd4dd" + +[[package]] +name = "icu_list" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfeda1d7775b6548edd4e8b7562304a559a91ed56ab56e18961a053f367c365" +dependencies = [ + "displaydoc", + "icu_list_data", + "icu_locid_transform", + "icu_provider", + "regex-automata 0.2.0", + "writeable", +] + +[[package]] +name = "icu_list_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b1a7fbdbf3958f1be8354cb59ac73f165b7b7082d447ff2090355c9a069120" + [[package]] name = "icu_locid" version = "1.5.0" @@ -2795,6 +3061,39 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +[[package]] +name = "icu_pattern" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f36aafd098d6717de34e668a8120822275c1fba22b936e757b7de8a2fd7e4" +dependencies = [ + "displaydoc", + "either", + "writeable", + "yoke", + "zerofrom", +] + +[[package]] +name = "icu_plurals" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a70e7c025dbd5c501b0a5c188cd11666a424f0dadcd4f0a95b7dafde3b114" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_locid_transform", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a483403238cb7d6a876a77a5f8191780336d80fe7b8b00bfdeb20be6abbfd112" + [[package]] name = "icu_properties" version = "1.5.1" @@ -2844,6 +3143,49 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "icu_segmenter" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de" +dependencies = [ + "core_maths", + "displaydoc", + "icu_collections", + "icu_locid", + "icu_provider", + "icu_segmenter_data", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e52775179941363cc594e49ce99284d13d6948928d8e72c755f55e98caa1eb" + +[[package]] +name = "icu_timezone" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa91ba6a585939a020c787235daa8aee856d9bceebd6355e283c0c310bc6de96" +dependencies = [ + "displaydoc", + "icu_calendar", + "icu_provider", + "icu_timezone_data", + "tinystr", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_timezone_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adcf7b613a268af025bc2a2532b4b9ee294e6051c5c0832d8bff20ac0232e68" + [[package]] name = "ident_case" version = "1.0.1" @@ -3005,7 +3347,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3214,7 +3556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3611,12 +3953,42 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3652,7 +4024,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.100", @@ -4075,6 +4447,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" @@ -4097,6 +4478,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -4504,10 +4895,19 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.9", "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9368763f5a9b804326f3af749e16f9abf378d227bcdee7634b13d8f17793782" +dependencies = [ + "memchr", +] + [[package]] name = "regex-automata" version = "0.4.9" @@ -4711,7 +5111,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4724,7 +5124,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.3", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5189,7 +5589,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.3", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5274,6 +5674,14 @@ dependencies = [ "time-core", ] +[[package]] +name = "timedate-zbus" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#0eee63a96c8b1f6555ca797b5c12545c372b1a1b" +dependencies = [ + "zbus 4.4.0", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -6081,7 +6489,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -6486,6 +6894,9 @@ name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +dependencies = [ + "either", +] [[package]] name = "x11-dl" @@ -6823,6 +7234,17 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerotrie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index 21f3500..67e6795 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,14 @@ edition = "2024" vergen = { version = "8", features = ["git", "gitcl"] } [dependencies] +anyhow = "1" +async-fn-stream = "0.2.2" +icu = { version = "1.5.0", features = [ + "experimental", + "compiled_data", + "icu_datetime_experimental", +] } +chrono-tz = "0.10" chrono = { version = "0.4", features = ["unstable-locales"] } cosmic-bg-config.workspace = true cosmic-comp-config.workspace = true @@ -55,6 +63,7 @@ i18n-embed = { version = "0.14", features = [ i18n-embed-fl = "0.7" rust-embed = "8" futures-util = "0.3.30" +timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" } [dependencies.greetd_ipc] version = "0.10.3" diff --git a/src/greeter.rs b/src/greeter.rs index 7ca4731..550701c 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -29,7 +29,7 @@ use cosmic::{ }, }, iced_runtime::core::window::Id as SurfaceId, - style, theme, widget, + theme, widget, }; use cosmic_comp_config::CosmicCompConfig; use cosmic_greeter_config::Config as CosmicGreeterConfig; @@ -417,6 +417,8 @@ pub enum Message { Socket(SocketState), Surface(surface::Action), Suspend, + Tick, + Tz(chrono_tz::Tz), Username(String), } @@ -444,47 +446,23 @@ pub struct App { dropdown_opt: Option, window_size: HashMap, heartbeat_handle: Option, + time: crate::time::Time, } impl App { fn menu(&self, id: SurfaceId) -> Element { let left_element = { - let date_time_column = { - let mut column = widget::column::with_capacity(2).padding(16.0).spacing(12.0); - - let dt = chrono::Local::now(); - let locale = *crate::localize::LANGUAGE_CHRONO; - - let date = dt.format_localized("%A, %B %-d", locale); - column = column - .push(widget::text::title2(format!("{}", date)).class(style::Text::Accent)); - - let (time, time_size) = if self - .selected_username - .data_idx - .and_then(|i| { - self.flags - .user_datas - .get(i) - .map(|user| user.clock_military_time) - }) - .unwrap_or_default() - { - (dt.format_localized("%R", locale), 112.0) - } else { - // xxx format_localized doesn't seem to show am/pm for some languages, such as - // French or Hungarian. This is apparently correct - // Also, time size needs to be reduced a bit here so that it fits on one line - (dt.format_localized("%I:%M %p", locale), 75.0) - }; - column = column.push( - widget::text(format!("{}", time)) - .size(time_size) - .class(style::Text::Accent), - ); - - column - }; + let military_time = self + .selected_username + .data_idx + .and_then(|i| { + self.flags + .user_datas + .get(i) + .map(|user| user.clock_military_time) + }) + .unwrap_or_default(); + let date_time_column = self.time.date_time_widget(military_time); let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0); @@ -1071,8 +1049,15 @@ impl cosmic::Application for App { dropdown_opt: None, window_size: HashMap::new(), heartbeat_handle: None, + time: crate::time::Time::new(), }; - (app, Task::none()) + ( + app, + Task::batch(vec![ + crate::time::tick().map(|_| cosmic::Action::App(Message::Tick)), + crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz))), + ]), + ) } /// Handle application events here. @@ -1489,7 +1474,6 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(a), )); } - Message::Focus(surface_id) => { self.active_surface_id_opt = Some(surface_id); if let Some(text_input_id) = self @@ -1500,6 +1484,12 @@ impl cosmic::Application for App { return widget::text_input::focus(text_input_id.clone()); } } + Message::Tick => { + self.time.tick(); + } + Message::Tz(tz) => { + self.time.set_tz(tz); + } } Task::none() } diff --git a/src/lib.rs b/src/lib.rs index cb2473f..31d008e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,3 +14,5 @@ mod networkmanager; #[cfg(feature = "upower")] mod upower; + +mod time; diff --git a/src/localize.rs b/src/localize.rs index c012f86..c3deeda 100644 --- a/src/localize.rs +++ b/src/localize.rs @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only -use std::{ - str::FromStr, - sync::{LazyLock, OnceLock}, -}; +use std::sync::OnceLock; use i18n_embed::{ - fluent::{fluent_language_loader, FluentLanguageLoader}, DefaultLocalizer, LanguageLoader, Localizer, + fluent::{FluentLanguageLoader, fluent_language_loader}, }; use rust_embed::RustEmbed; @@ -16,19 +13,6 @@ use rust_embed::RustEmbed; struct Localizations; pub static LANGUAGE_LOADER: OnceLock = OnceLock::new(); -pub static LANGUAGE_CHRONO: LazyLock = LazyLock::new(|| { - std::env::var("LC_TIME") - .ok() - .or_else(|| std::env::var("LANG").ok()) - .and_then(|locale_full| { - // Split LANG because it may be set to a locale such as en_US.UTF8 - locale_full - .split('.') - .next() - .and_then(|locale| chrono::Locale::from_str(locale).ok()) - }) - .unwrap_or(chrono::Locale::en_US) -}); #[macro_export] macro_rules! fl { diff --git a/src/locker.rs b/src/locker.rs index d5ec67c..d524646 100644 --- a/src/locker.rs +++ b/src/locker.rs @@ -20,7 +20,7 @@ use cosmic::{ }, }, iced_runtime::core::window::Id as SurfaceId, - style, widget, + widget, }; use cosmic_config::CosmicConfigEntry; use std::time::Duration; @@ -35,7 +35,7 @@ use std::{ process, sync::Arc, }; -use tokio::{sync::mpsc, task}; +use tokio::{sync::mpsc, task, time}; use wayland_client::{Proxy, protocol::wl_output::WlOutput}; fn lockfile_opt() -> Option { @@ -232,6 +232,8 @@ pub enum Message { Suspend, Error(String), Lock, + Tick, + Tz(chrono_tz::Tz), Unlock, } @@ -273,30 +275,15 @@ pub struct App { value_tx_opt: Option>, prompt_opt: Option<(String, bool, Option)>, error_opt: Option, + time: crate::time::Time, } impl App { fn menu(&self, surface_id: SurfaceId) -> Element { let left_element = { - let date_time_column = { - let mut column = widget::column::with_capacity(2).padding(16.0); - - let dt = chrono::Local::now(); - let locale = *crate::localize::LANGUAGE_CHRONO; - - let date = dt.format_localized("%A, %B %-d", locale); - column = column - .push(widget::text::title2(format!("{}", date)).class(style::Text::Accent)); - - let time = dt.format_localized("%R", locale); - column = column.push( - widget::text(format!("{}", time)) - .size(112.0) - .class(style::Text::Accent), - ); - - column - }; + // TODO how should we get user preference for military time here? + let military_time = false; + let date_time_column = self.time.date_time_widget(military_time); let mut status_row = widget::row::with_capacity(2).padding(16.0).spacing(12.0); @@ -555,9 +542,10 @@ impl cosmic::Application for App { value_tx_opt: None, prompt_opt: None, error_opt: None, + time: crate::time::Time::new(), }; - let command = if cfg!(feature = "logind") { + let task = if cfg!(feature = "logind") { if already_locked { // Recover previously locked state log::info!("recovering previous locked state"); @@ -574,7 +562,14 @@ impl cosmic::Application for App { lock() }; - (app, command) + ( + app, + Task::batch(vec![ + task, + crate::time::tick().map(|_| cosmic::Action::App(Message::Tick)), + crate::time::tz_updates().map(|tz| cosmic::Action::App(Message::Tz(tz))), + ]), + ) } /// Handle application events here. @@ -998,6 +993,12 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(a), )); } + Message::Tick => { + self.time.tick(); + } + Message::Tz(tz) => { + self.time.set_tz(tz); + } } Task::none() } diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..4373d8b --- /dev/null +++ b/src/time.rs @@ -0,0 +1,189 @@ +use std::{str::FromStr, time::Duration}; + +use anyhow::bail; +use async_fn_stream::StreamEmitter; +use chrono::{Datelike, Timelike}; +use cosmic::{ + Task, + iced_core::Element, + style, + widget::{self, column, text::title2}, +}; +use futures_util::StreamExt; +use icu::{ + calendar::DateTime, + datetime::{ + DateTimeFormatter, DateTimeFormatterOptions, + options::{ + components::{self, Bag}, + preferences, + }, + }, + locid::Locale, +}; +use timedate_zbus::TimeDateProxy; +use tokio::time; + +#[derive(Debug, Clone)] +pub struct Time { + locale: Locale, + timezone: Option, + now: chrono::DateTime, +} + +impl Time { + pub fn new() -> Self { + fn get_local() -> Result> { + let locale = std::env::var("LC_TIME").or_else(|_| std::env::var("LANG"))?; + let locale = locale + .split('.') + .next() + .ok_or(format!("Can't split the locale {locale}"))?; + + let locale = Locale::from_str(locale).map_err(|e| format!("{e:?}"))?; + Ok(locale) + } + + let locale = match get_local() { + Ok(locale) => locale, + Err(e) => { + log::error!("can't get locale {e}"); + Locale::default() + } + }; + let now = chrono::Local::now().fixed_offset(); + + Self { + locale, + timezone: None, + now, + } + } + + pub fn set_tz(&mut self, tz: chrono_tz::Tz) { + self.timezone = Some(tz); + self.tick(); + } + + pub fn tick(&mut self) { + self.now = self + .timezone + .map(|tz| chrono::Local::now().with_timezone(&tz).fixed_offset()) + .unwrap_or_else(|| chrono::Local::now().into()); + } + + pub fn format(&self, bag: Bag, date: &D) -> String { + let options = DateTimeFormatterOptions::Components(bag); + + let dtf = + DateTimeFormatter::try_new_experimental(&self.locale.clone().into(), options).unwrap(); + + let datetime = DateTime::try_new_gregorian_datetime( + date.year(), + date.month() as u8, + date.day() as u8, + // hack cause we know that we will only use "now" + // when we need hours (NaiveDate don't support this functions) + self.now.hour() as u8, + self.now.minute() as u8, + self.now.second() as u8, + ) + .unwrap() + .to_iso() + .to_any(); + + dtf.format(&datetime) + .expect("can't format value") + .to_string() + } + + pub fn date_time_widget<'a, M: 'a>(&self, military_time: bool) -> cosmic::Element<'a, M> { + let mut top_bag = Bag::empty(); + + top_bag.weekday = Some(components::Text::Long); + + top_bag.day = Some(components::Day::NumericDayOfMonth); + top_bag.month = Some(components::Month::Long); + + let mut bottom_bag = Bag::empty(); + + bottom_bag.hour = Some(components::Numeric::Numeric); + bottom_bag.minute = Some(components::Numeric::Numeric); + + let hour_cycle = if military_time { + preferences::HourCycle::H23 + } else { + preferences::HourCycle::H12 + }; + + top_bag.preferences = Some(preferences::Bag::from_hour_cycle(hour_cycle)); + + Element::from( + column() + .padding(16.) + .spacing(12.0) + .push(title2(self.format(top_bag, &self.now)).class(style::Text::Accent)) + .push( + widget::text(self.format(bottom_bag, &self.now)) + .size(if military_time { 112. } else { 75. }) + .class(style::Text::Accent), + ), + ) + } +} + +pub fn tz_updates() -> Task { + Task::stream(async_fn_stream::fn_stream(|emitter| async move { + loop { + if let Err(err) = tz_stream(&emitter).await { + log::error!("{err:?}"); + } + _ = time::sleep(Duration::from_secs(60)).await; + } + })) +} + +pub fn tick() -> Task<()> { + Task::stream(async_fn_stream::fn_stream(|emitter| async move { + let mut timer = time::interval(time::Duration::from_secs(60)); + timer.set_missed_tick_behavior(time::MissedTickBehavior::Skip); + + emitter.emit(()).await; + loop { + timer.tick().await; + emitter.emit(()).await; + + // Calculate a delta if we're ticking per minute to keep ticks stable + // Based on i3status-rust + let current = chrono::Local::now().second() as u64 % 60; + if current != 0 { + timer.reset_after(time::Duration::from_secs(60 - current)); + } + } + })) +} + +pub async fn tz_stream(emitter: &StreamEmitter) -> anyhow::Result<()> { + let Ok(conn) = zbus::Connection::system().await else { + bail!("No zbus system connection."); + }; + let Ok(proxy) = TimeDateProxy::new(&conn).await else { + bail!("No timezone proxy"); + }; + + // The stream always returns the current timezone as its first item even if it wasn't + // updated. If the proxy is recreated in a loop somehow, the resulting stream will + // always yield an update immediately which could lead to spammed false updates. + let mut s = proxy.receive_timezone_changed().await; + + while let Some(property) = s.next().await { + let Ok(tz) = property.get().await else { + bail!("Failed to get property"); + }; + let Ok(tz) = tz.parse::() else { + bail!("Failed to parse timezone."); + }; + emitter.emit(tz).await; + } + bail!("Timezone property stream ended."); +}