feat(time): formattable datetime strings with strftime

This commit is contained in:
Joshua Megnauth 2025-11-07 13:42:36 -05:00 • committed by GitHub
parent ba1e379a75
commit bd98de8228
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 58 deletions

View file

@ -43,6 +43,11 @@ use icu::{
static AUTOSIZE_MAIN_ID: LazyLock<Id> = LazyLock::new(|| Id::new("autosize-main")); static AUTOSIZE_MAIN_ID: LazyLock<Id> = LazyLock::new(|| Id::new("autosize-main"));
// Specifiers for strftime that indicate seconds. Subsecond precision isn't supported by the applet
// so those specifiers aren't listed here. This list is non-exhaustive, and it's possible that %X
// and other specifiers have to be added depending on locales.
const STRFTIME_SECONDS: &[char] = &['S', 'T', '+', 's'];
fn get_system_locale() -> Locale { fn get_system_locale() -> Locale {
for var in ["LC_TIME", "LC_ALL", "LANG"] { for var in ["LC_TIME", "LC_ALL", "LANG"] {
if let Ok(locale_str) = std::env::var(var) { if let Ok(locale_str) = std::env::var(var) {
@ -159,45 +164,58 @@ impl Window {
} }
fn vertical_layout(&self) -> Element<'_, Message> { fn vertical_layout(&self) -> Element<'_, Message> {
let mut elements = Vec::new(); let elements: Vec<Element<'_, Message>> = if !self.config.format_strftime.is_empty() {
let date = self.now.naive_local(); // strftime formatter may override locale specific elements so it stands alone rather
let datetime = self.create_datetime(&date); // than using ICU to determine a format.
let mut prefs = DateTimeFormatterPreferences::from(self.locale.clone()); self.now
prefs.hour_cycle = Some(if self.config.military_time { .format(&self.config.format_strftime)
HourCycle::H23 .to_string()
.split_whitespace()
.map(|piece| self.core.applet.text(piece.to_owned()).into())
.collect()
} else { } else {
HourCycle::H12 let mut elements = Vec::new();
}); let date = self.now.naive_local();
let datetime = self.create_datetime(&date);
let mut prefs = DateTimeFormatterPreferences::from(self.locale.clone());
prefs.hour_cycle = Some(if self.config.military_time {
HourCycle::H23
} else {
HourCycle::H12
});
if self.config.show_date_in_top_panel { if self.config.show_date_in_top_panel {
let formatted_date = DateTimeFormatter::try_new(prefs, fieldsets::MD::medium()) let formatted_date = DateTimeFormatter::try_new(prefs, fieldsets::MD::medium())
.unwrap()
.format(&datetime)
.to_string();
for p in formatted_date.split_whitespace() {
elements.push(self.core.applet.text(p.to_owned()).into());
}
elements.push(
horizontal_rule(2)
.width(self.core.applet.suggested_size(true).0)
.into(),
);
}
let mut fs = fieldsets::T::medium();
if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute);
}
let formatted_time = DateTimeFormatter::try_new(prefs, fs)
.unwrap() .unwrap()
.format(&datetime) .format(&datetime)
.to_string(); .to_string();
for p in formatted_date.split_whitespace() { // todo: split using formatToParts when it is implemented
// https://github.com/unicode-org/icu4x/issues/4936#issuecomment-2128812667
for p in formatted_time.split_whitespace().flat_map(|s| s.split(':')) {
elements.push(self.core.applet.text(p.to_owned()).into()); elements.push(self.core.applet.text(p.to_owned()).into());
} }
elements.push(
horizontal_rule(2)
.width(self.core.applet.suggested_size(true).0)
.into(),
);
}
let mut fs = fieldsets::T::medium();
if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute);
}
let formatted_time = DateTimeFormatter::try_new(prefs, fs)
.unwrap()
.format(&datetime)
.to_string();
// todo: split using formatToParts when it is implemented elements
// https://github.com/unicode-org/icu4x/issues/4936#issuecomment-2128812667 };
for p in formatted_time.split_whitespace().flat_map(|s| s.split(':')) {
elements.push(self.core.applet.text(p.to_owned()).into());
}
let date_time_col = Column::with_children(elements) let date_time_col = Column::with_children(elements)
.align_x(Alignment::Center) .align_x(Alignment::Center)
@ -216,26 +234,39 @@ impl Window {
} }
fn horizontal_layout(&self) -> Element<'_, Message> { fn horizontal_layout(&self) -> Element<'_, Message> {
let datetime = self.create_datetime(&self.now); let formatted_date = if !self.config.format_strftime.is_empty() {
let mut prefs = DateTimeFormatterPreferences::from(self.locale.clone()); self.now.format(&self.config.format_strftime).to_string()
prefs.hour_cycle = Some(if self.config.military_time {
HourCycle::H23
} else { } else {
HourCycle::H12 let datetime = self.create_datetime(&self.now);
}); let mut prefs = DateTimeFormatterPreferences::from(self.locale.clone());
prefs.hour_cycle = Some(if self.config.military_time {
let formatted_date = if self.config.show_date_in_top_panel { HourCycle::H23
if self.config.show_weekday {
let mut fs = fieldsets::MDET::long();
if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute);
}
DateTimeFormatter::try_new(prefs, fs)
.unwrap()
.format(&datetime)
.to_string()
} else { } else {
let mut fs = fieldsets::MDT::long(); HourCycle::H12
});
if self.config.show_date_in_top_panel {
if self.config.show_weekday {
let mut fs = fieldsets::MDET::long();
if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute);
}
DateTimeFormatter::try_new(prefs, fs)
.unwrap()
.format(&datetime)
.to_string()
} else {
let mut fs = fieldsets::MDT::long();
if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute);
}
DateTimeFormatter::try_new(prefs, fs)
.unwrap()
.format(&datetime)
.to_string()
}
} else {
let mut fs = fieldsets::T::medium();
if !self.config.show_seconds { if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute); fs = fs.with_time_precision(TimePrecision::Minute);
} }
@ -244,15 +275,6 @@ impl Window {
.format(&datetime) .format(&datetime)
.to_string() .to_string()
} }
} else {
let mut fs = fieldsets::T::medium();
if !self.config.show_seconds {
fs = fs.with_time_precision(TimePrecision::Minute);
}
DateTimeFormatter::try_new(prefs, fs)
.unwrap()
.format(&datetime)
.to_string()
}; };
Element::from( Element::from(
@ -587,7 +609,20 @@ impl cosmic::Application for Window {
Message::ConfigChanged(c) => { Message::ConfigChanged(c) => {
// Don't interrupt the tick subscription unless necessary // Don't interrupt the tick subscription unless necessary
self.show_seconds_tx.send_if_modified(|show_seconds| { self.show_seconds_tx.send_if_modified(|show_seconds| {
if *show_seconds == c.show_seconds { if !c.format_strftime.is_empty() {
if c.format_strftime.split('%').any(|s| {
STRFTIME_SECONDS.contains(&s.chars().next().unwrap_or_default())
}) && !*show_seconds
{
// The strftime formatter contains a seconds specifier. Force enable
// ticking per seconds internally regardless of the user setting.
// This does not change the user's setting. It's invisible to the user.
*show_seconds = true;
true
} else {
false
}
} else if *show_seconds == c.show_seconds {
false false
} else { } else {
*show_seconds = c.show_seconds; *show_seconds = c.show_seconds;

View file

@ -11,6 +11,8 @@ pub struct TimeAppletConfig {
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,
#[serde(default, skip_serializing_if = "str::is_empty")]
pub format_strftime: String,
} }
impl Default for TimeAppletConfig { impl Default for TimeAppletConfig {
@ -21,6 +23,7 @@ impl Default for TimeAppletConfig {
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,
format_strftime: Default::default(),
} }
} }
} }