feat(time): Option to show seconds on the clock

Closes: #496
Requires: pop-os/cosmic-settings#509

I based some of this code off of `i3status-rust` to double check that I
handled it efficiently.
This commit is contained in:
Josh Megnauth 2024-08-19 03:13:51 -04:00 • committed by Michael Murphy
parent f9c01f54d2
commit 93e3b0f8f3
2 changed files with 74 additions and 12 deletions

View file

@ -7,6 +7,7 @@ use cosmic::cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, Cosmi
#[version = 1] #[version = 1]
pub struct TimeAppletConfig { pub struct TimeAppletConfig {
pub military_time: bool, pub military_time: bool,
pub show_seconds: bool,
pub first_day_of_week: u8, pub first_day_of_week: u8,
pub show_date_in_top_panel: bool, pub show_date_in_top_panel: bool,
pub show_weekday: bool, pub show_weekday: bool,
@ -16,6 +17,7 @@ impl Default for TimeAppletConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
military_time: false, military_time: false,
show_seconds: false,
first_day_of_week: 6, first_day_of_week: 6,
show_date_in_top_panel: true, show_date_in_top_panel: true,
show_weekday: false, show_weekday: false,

View file

@ -3,7 +3,7 @@
use std::{borrow::Cow, str::FromStr}; use std::{borrow::Cow, str::FromStr};
use chrono::{Datelike, DurationRound, Timelike}; use chrono::{Datelike, Timelike};
use cosmic::{ use cosmic::{
app, app,
applet::{cosmic_panel_config::PanelAnchor, menu_button, padded_control}, applet::{cosmic_panel_config::PanelAnchor, menu_button, padded_control},
@ -25,6 +25,7 @@ use cosmic::{
Command, Element, Theme, Command, Element, Theme,
}; };
use timedate_zbus::TimeDateProxy; use timedate_zbus::TimeDateProxy;
use tokio::{sync::watch, time};
use icu::{ use icu::{
calendar::DateTime, calendar::DateTime,
@ -56,6 +57,7 @@ pub struct Window {
rectangle: Rectangle, rectangle: Rectangle,
token_tx: Option<calloop::channel::Sender<TokenRequest>>, token_tx: Option<calloop::channel::Sender<TokenRequest>>,
config: TimeAppletConfig, config: TimeAppletConfig,
show_seconds_tx: watch::Sender<bool>,
locale: Locale, locale: Locale,
} }
@ -134,7 +136,10 @@ impl cosmic::Application for Window {
// variable but never updated // variable but never updated
// Instead of using the local timezone, we will store an offset that is updated if the // Instead of using the local timezone, we will store an offset that is updated if the
// timezone is ever externally changed // timezone is ever externally changed
let now: chrono::DateTime<chrono::FixedOffset> = chrono::Local::now().fixed_offset(); let now = chrono::Local::now().fixed_offset();
// Synch `show_seconds` from the config within the time subscription
let (show_seconds_tx, _) = watch::channel(true);
( (
Self { Self {
@ -147,6 +152,7 @@ impl cosmic::Application for Window {
rectangle: Rectangle::default(), rectangle: Rectangle::default(),
token_tx: None, token_tx: None,
config: TimeAppletConfig::default(), config: TimeAppletConfig::default(),
show_seconds_tx,
locale, locale,
}, },
Command::none(), Command::none(),
@ -166,16 +172,56 @@ impl cosmic::Application for Window {
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
fn time_subscription() -> Subscription<()> { fn time_subscription(mut show_seconds: watch::Receiver<bool>) -> Subscription<Message> {
subscription::unfold("time-sub", (), move |()| async move { subscription::channel("time-sub", 1, |mut output| async move {
let now = chrono::Local::now(); // Mark this receiver's state as changed so that it always receives an initial
let update_delay = chrono::TimeDelta::minutes(1); // update during the loop below
// This allows us to avoid duplicating code from the loop
show_seconds.mark_changed();
let mut period = 1;
let mut timer = time::interval(time::Duration::from_secs(period));
timer.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
let duration = ((now + update_delay).duration_trunc(update_delay).unwrap() - now) loop {
.to_std() tokio::select! {
.unwrap(); _ = timer.tick() => {
tokio::time::sleep(duration).await; #[cfg(debug_assertions)]
((), ()) if let Err(err) = output.send(Message::Tick).await {
tracing::error!(?err, "Failed sending tick request to applet");
}
#[cfg(not(debug_assertions))]
let _ = output.send(Message::Tick).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 % period;
if current != 0 {
timer.reset_after(time::Duration::from_secs(period - current));
}
},
// Update timer if the user toggles show_seconds
Ok(()) = show_seconds.changed() => {
let seconds = *show_seconds.borrow_and_update();
if seconds {
period = 1;
// Subsecond precision isn't needed; skip calculating offset
let period = time::Duration::from_secs(period);
let start = time::Instant::now() + period;
timer = time::interval_at(start, period);
} else {
period = 60;
let delta = time::Duration::from_secs(period - chrono::Utc::now().second() as u64 % period);
let now = time::Instant::now();
// Start ticking from the next minute to update the time properly
let start = now + delta;
let period = time::Duration::from_secs(period);
timer = time::interval_at(start, period);
}
timer.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
}
}
}
}) })
} }
@ -219,9 +265,10 @@ impl cosmic::Application for Window {
}) })
} }
let show_seconds_rx = self.show_seconds_tx.subscribe();
Subscription::batch(vec![ Subscription::batch(vec![
rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)), rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)),
time_subscription().map(|_| Message::Tick), time_subscription(show_seconds_rx),
activation_token_subscription(0).map(Message::Token), activation_token_subscription(0).map(Message::Token),
timezone_subscription(), timezone_subscription(),
self.core.watch_config(Self::APP_ID).map(|u| { self.core.watch_config(Self::APP_ID).map(|u| {
@ -356,6 +403,15 @@ impl cosmic::Application for Window {
Command::none() Command::none()
} }
Message::ConfigChanged(c) => { Message::ConfigChanged(c) => {
// Don't interrupt the tick subscription unless necessary
self.show_seconds_tx.send_if_modified(|show_seconds| {
if *show_seconds == c.show_seconds {
false
} else {
*show_seconds = c.show_seconds;
true
}
});
self.config = c; self.config = c;
Command::none() Command::none()
} }
@ -396,6 +452,10 @@ impl cosmic::Application for Window {
time_bag.hour = Some(components::Numeric::Numeric); time_bag.hour = Some(components::Numeric::Numeric);
time_bag.minute = Some(components::Numeric::Numeric); time_bag.minute = Some(components::Numeric::Numeric);
time_bag.second = self
.config
.show_seconds
.then_some(components::Numeric::Numeric);
let hour_cycle = if self.config.military_time { let hour_cycle = if self.config.military_time {
preferences::HourCycle::H23 preferences::HourCycle::H23