refactor(footer): AppTimeFormat data handling (#89)

- Extract local state of `app_time_format` from `footer` to have it globally available
- Add a deserialization fallback for deprecated `AppTimeFormat::Hidden`
- Persist `footer_app_time` toggle state
This commit is contained in:
Jens Krause 2025-08-27 19:44:02 +02:00 committed by GitHub
parent 637c1da21b
commit c494f0e829
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 93 additions and 39 deletions

View File

@ -44,6 +44,7 @@ pub struct App {
#[allow(dead_code)] // w/ `--features sound` available only #[allow(dead_code)] // w/ `--features sound` available only
sound_path: Option<PathBuf>, sound_path: Option<PathBuf>,
app_time: AppTime, app_time: AppTime,
app_time_format: AppTimeFormat,
countdown: CountdownState, countdown: CountdownState,
timer: TimerState, timer: TimerState,
pomodoro: PomodoroState, pomodoro: PomodoroState,
@ -72,6 +73,7 @@ pub struct AppArgs {
pub current_value_timer: Duration, pub current_value_timer: Duration,
pub app_tx: events::AppEventTx, pub app_tx: events::AppEventTx,
pub sound_path: Option<PathBuf>, pub sound_path: Option<PathBuf>,
pub footer_toggle_app_time: Toggle,
} }
pub struct FromAppArgs { pub struct FromAppArgs {
@ -131,6 +133,7 @@ impl From<FromAppArgs> for App {
sound_path: args.sound, sound_path: args.sound,
#[cfg(not(feature = "sound"))] #[cfg(not(feature = "sound"))]
sound_path: None, sound_path: None,
footer_toggle_app_time: stg.footer_app_time,
}) })
} }
} }
@ -164,6 +167,7 @@ impl App {
blink, blink,
sound_path, sound_path,
app_tx, app_tx,
footer_toggle_app_time,
} = args; } = args;
let app_time = get_app_time(); let app_time = get_app_time();
@ -174,6 +178,7 @@ impl App {
sound_path, sound_path,
content, content,
app_time, app_time,
app_time_format,
style, style,
with_decis, with_decis,
countdown: CountdownState::new(CountdownStateArgs { countdown: CountdownState::new(CountdownStateArgs {
@ -204,7 +209,14 @@ impl App {
round: pomodoro_round, round: pomodoro_round,
app_tx: app_tx.clone(), app_tx: app_tx.clone(),
}), }),
footer: FooterState::new(show_menu, app_time_format), footer: FooterState::new(
show_menu,
if footer_toggle_app_time == Toggle::On {
Some(app_time_format)
} else {
None
},
),
} }
} }
@ -222,7 +234,26 @@ impl App {
KeyCode::Char('t') => app.content = Content::Timer, KeyCode::Char('t') => app.content = Content::Timer,
KeyCode::Char('p') => app.content = Content::Pomodoro, KeyCode::Char('p') => app.content = Content::Pomodoro,
// toogle app time format // toogle app time format
KeyCode::Char(':') => app.footer.toggle_app_time_format(), KeyCode::Char(':') => {
//
// TODO: Check content != LocalClock
let new_format = match app.footer.app_time_format() {
// footer is hidden in footer ->
None => Some(AppTimeFormat::first()),
Some(v) => {
if v != &AppTimeFormat::last() {
Some(v.next())
} else {
None
}
}
};
if let Some(format) = new_format {
app.app_time_format = format;
}
app.footer.set_app_time_format(new_format);
}
// toogle menu // toogle menu
KeyCode::Char('m') => app.footer.set_show_menu(!app.footer.get_show_menu()), KeyCode::Char('m') => app.footer.set_show_menu(!app.footer.get_show_menu()),
KeyCode::Char(',') => { KeyCode::Char(',') => {
@ -374,7 +405,7 @@ impl App {
show_menu: self.footer.get_show_menu(), show_menu: self.footer.get_show_menu(),
notification: self.notification, notification: self.notification,
blink: self.blink, blink: self.blink,
app_time_format: *self.footer.app_time_format(), app_time_format: self.app_time_format,
style: self.style, style: self.style,
with_decis: self.with_decis, with_decis: self.with_decis,
pomodoro_mode: self.pomodoro.get_mode().clone(), pomodoro_mode: self.pomodoro.get_mode().clone(),
@ -393,6 +424,7 @@ impl App {
), ),
elapsed_value_countdown: Duration::from(*self.countdown.get_elapsed_value()), elapsed_value_countdown: Duration::from(*self.countdown.get_elapsed_value()),
current_value_timer: Duration::from(*self.timer.get_clock().get_current_value()), current_value_timer: Duration::from(*self.timer.get_clock().get_current_value()),
footer_app_time: self.footer.app_time_format().is_some().into(),
} }
} }
} }

View File

@ -1,6 +1,7 @@
use clap::ValueEnum; use clap::ValueEnum;
use ratatui::symbols::shade; use ratatui::symbols::shade;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::EnumString;
use time::OffsetDateTime; use time::OffsetDateTime;
use time::format_description; use time::format_description;
@ -15,6 +16,8 @@ pub enum Content {
Timer, Timer,
#[value(name = "pomodoro", alias = "p")] #[value(name = "pomodoro", alias = "p")]
Pomodoro, Pomodoro,
// #[value(name = "localclock", alias = "l")]
// LocalClock,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -71,7 +74,7 @@ impl Style {
} }
} }
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, EnumString, Serialize, Deserialize)]
pub enum AppTimeFormat { pub enum AppTimeFormat {
/// `hh:mm:ss` /// `hh:mm:ss`
#[default] #[default]
@ -80,17 +83,22 @@ pub enum AppTimeFormat {
HhMm, HhMm,
/// `hh:mm AM` (or PM) /// `hh:mm AM` (or PM)
Hh12Mm, Hh12Mm,
/// `` (empty)
Hidden,
} }
impl AppTimeFormat { impl AppTimeFormat {
pub const fn first() -> Self {
Self::HhMmSs
}
pub const fn last() -> Self {
Self::Hh12Mm
}
pub fn next(&self) -> Self { pub fn next(&self) -> Self {
match self { match self {
AppTimeFormat::HhMmSs => AppTimeFormat::HhMm, AppTimeFormat::HhMmSs => AppTimeFormat::HhMm,
AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm, AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm,
AppTimeFormat::Hh12Mm => AppTimeFormat::Hidden, AppTimeFormat::Hh12Mm => AppTimeFormat::HhMmSs,
AppTimeFormat::Hidden => AppTimeFormat::HhMmSs,
} }
} }
} }
@ -113,24 +121,19 @@ impl From<AppTime> for OffsetDateTime {
impl AppTime { impl AppTime {
pub fn format(&self, app_format: &AppTimeFormat) -> String { pub fn format(&self, app_format: &AppTimeFormat) -> String {
let parse_str = match app_format { let parse_str = match app_format {
AppTimeFormat::HhMmSs => Some("[hour]:[minute]:[second]"), AppTimeFormat::HhMmSs => "[hour]:[minute]:[second]",
AppTimeFormat::HhMm => Some("[hour]:[minute]"), AppTimeFormat::HhMm => "[hour]:[minute]",
AppTimeFormat::Hh12Mm => Some("[hour repr:12 padding:none]:[minute] [period]"), AppTimeFormat::Hh12Mm => "[hour repr:12 padding:none]:[minute] [period]",
AppTimeFormat::Hidden => None,
}; };
if let Some(str) = parse_str { format_description::parse(parse_str)
format_description::parse(str) .map_err(|_| "parse error")
.map_err(|_| "parse error") .and_then(|fd| {
.and_then(|fd| { OffsetDateTime::from(*self)
OffsetDateTime::from(*self) .format(&fd)
.format(&fd) .map_err(|_| "format error")
.map_err(|_| "format error") })
}) .unwrap_or_else(|e| e.to_string())
.unwrap_or_else(|e| e.to_string())
} else {
"".to_owned()
}
} }
} }
@ -150,6 +153,15 @@ pub enum Toggle {
Off, Off,
} }
impl From<bool> for Toggle {
fn from(value: bool) -> Self {
match value {
true => Toggle::On,
false => Toggle::Off,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -196,12 +208,5 @@ mod tests {
"6:06 PM", "6:06 PM",
"local" "local"
); );
// hidden
assert_eq!(AppTime::Utc(dt).format(&AppTimeFormat::Hidden), "", "utc");
assert_eq!(
AppTime::Local(dt).format(&AppTimeFormat::Hidden),
"",
"local"
);
} }
} }

View File

@ -3,17 +3,30 @@ use crate::{
widgets::pomodoro::Mode as PomodoroMode, widgets::pomodoro::Mode as PomodoroMode,
}; };
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
fn deserialize_app_time_format<'de, D>(deserializer: D) -> Result<AppTimeFormat, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
// Hidden is deprecated - use `default` value instead
"Hidden" => Ok(AppTimeFormat::default()),
_ => s.parse().map_err(serde::de::Error::custom),
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct AppStorage { pub struct AppStorage {
pub content: Content, pub content: Content,
pub show_menu: bool, pub show_menu: bool,
pub notification: Toggle, pub notification: Toggle,
pub blink: Toggle, pub blink: Toggle,
#[serde(deserialize_with = "deserialize_app_time_format")]
pub app_time_format: AppTimeFormat, pub app_time_format: AppTimeFormat,
pub style: Style, pub style: Style,
pub with_decis: bool, pub with_decis: bool,
@ -31,6 +44,8 @@ pub struct AppStorage {
pub elapsed_value_countdown: Duration, pub elapsed_value_countdown: Duration,
// timer // timer
pub current_value_timer: Duration, pub current_value_timer: Duration,
// footer
pub footer_app_time: Toggle,
} }
impl Default for AppStorage { impl Default for AppStorage {
@ -60,6 +75,8 @@ impl Default for AppStorage {
elapsed_value_countdown: Duration::ZERO, elapsed_value_countdown: Duration::ZERO,
// timer // timer
current_value_timer: Duration::ZERO, current_value_timer: Duration::ZERO,
// footer
footer_app_time: Toggle::Off,
} }
} }
} }

View File

@ -13,11 +13,11 @@ use ratatui::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FooterState { pub struct FooterState {
show_menu: bool, show_menu: bool,
app_time_format: AppTimeFormat, app_time_format: Option<AppTimeFormat>,
} }
impl FooterState { impl FooterState {
pub const fn new(show_menu: bool, app_time_format: AppTimeFormat) -> Self { pub const fn new(show_menu: bool, app_time_format: Option<AppTimeFormat>) -> Self {
Self { Self {
show_menu, show_menu,
app_time_format, app_time_format,
@ -32,12 +32,12 @@ impl FooterState {
self.show_menu self.show_menu
} }
pub const fn app_time_format(&self) -> &AppTimeFormat { pub const fn app_time_format(&self) -> &Option<AppTimeFormat> {
&self.app_time_format &self.app_time_format
} }
pub fn toggle_app_time_format(&mut self) { pub const fn set_app_time_format(&mut self, value: Option<AppTimeFormat>) {
self.app_time_format = self.app_time_format.next(); self.app_time_format = value;
} }
} }
@ -73,9 +73,9 @@ impl StatefulWidget for Footer {
Line::from( Line::from(
match state.app_time_format { match state.app_time_format {
// `Hidden` -> no (empty) title // `Hidden` -> no (empty) title
AppTimeFormat::Hidden => "".into(), None => "".into(),
// others -> add some space around // others -> add some space around
_ => format!(" {} ", self.app_time.format(&state.app_time_format)) Some(v) => format!(" {} ", self.app_time.format(&v))
} }
).right_aligned()) ).right_aligned())
.border_set(border::PLAIN) .border_set(border::PLAIN)