refactor: use icu for datetime
This commit is contained in:
parent
0600931abf
commit
37cff18672
7 changed files with 686 additions and 89 deletions
|
|
@ -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<Dropdown>,
|
||||
window_size: HashMap<SurfaceId, Size>,
|
||||
heartbeat_handle: Option<cosmic::iced::task::Handle>,
|
||||
time: crate::time::Time,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn menu(&self, id: SurfaceId) -> Element<Message> {
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@ mod networkmanager;
|
|||
|
||||
#[cfg(feature = "upower")]
|
||||
mod upower;
|
||||
|
||||
mod time;
|
||||
|
|
|
|||
|
|
@ -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<FluentLanguageLoader> = OnceLock::new();
|
||||
pub static LANGUAGE_CHRONO: LazyLock<chrono::Locale> = 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 {
|
||||
|
|
|
|||
|
|
@ -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<PathBuf> {
|
||||
|
|
@ -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<mpsc::Sender<String>>,
|
||||
prompt_opt: Option<(String, bool, Option<String>)>,
|
||||
error_opt: Option<String>,
|
||||
time: crate::time::Time,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn menu(&self, surface_id: SurfaceId) -> Element<Message> {
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
189
src/time.rs
Normal file
189
src/time.rs
Normal file
|
|
@ -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<chrono_tz::Tz>,
|
||||
now: chrono::DateTime<chrono::FixedOffset>,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
pub fn new() -> Self {
|
||||
fn get_local() -> Result<Locale, Box<dyn std::error::Error>> {
|
||||
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<D: Datelike>(&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<chrono_tz::Tz> {
|
||||
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<chrono_tz::Tz>) -> 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::<chrono_tz::Tz>() else {
|
||||
bail!("Failed to parse timezone.");
|
||||
};
|
||||
emitter.emit(tz).await;
|
||||
}
|
||||
bail!("Timezone property stream ended.");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue