From 94bdeeab11bace1d36029f548762bbcd01f364c1 Mon Sep 17 00:00:00 2001 From: Jens Krause <47693+sectore@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:31:22 +0100 Subject: [PATCH] feat(footer): show local time (#42) --- Cargo.lock | 64 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + demo/local-time.gif | Bin 0 -> 17153 bytes demo/local-time.tape | 22 +++++++++++++ justfile | 5 +++ src/app.rs | 53 +++++++++++++++++++++++-------- src/common.rs | 65 ++++++++++++++++++++++++++++++++++++++ src/storage.rs | 4 ++- src/widgets/footer.rs | 71 ++++++++++++++++++++++++++++++++++++------ 9 files changed, 261 insertions(+), 24 deletions(-) create mode 100644 demo/local-time.gif create mode 100644 demo/local-time.tape diff --git a/Cargo.lock b/Cargo.lock index 0b5d39a..d07bf9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,6 +296,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "diff" version = "0.1.13" @@ -649,6 +658,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -723,6 +747,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -1050,6 +1080,39 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "timr-tui" version = "0.9.0" @@ -1063,6 +1126,7 @@ dependencies = [ "serde", "serde_json", "strum", + "time", "tokio", "tokio-stream", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index 5ac1402..7ea1bf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } directories = "5.0.1" clap = { version = "4.5.23", features = ["derive"] } +time = { version = "0.3.37", features = ["formatting", "local-offset"] } diff --git a/demo/local-time.gif b/demo/local-time.gif new file mode 100644 index 0000000000000000000000000000000000000000..b1da6d2183ec9ba7482380f95aa3461ec3790bb8 GIT binary patch literal 17153 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 E0JF&HaR2}S literal 0 HcmV?d00001 diff --git a/demo/local-time.tape b/demo/local-time.tape new file mode 100644 index 0000000..7bafe99 --- /dev/null +++ b/demo/local-time.tape @@ -0,0 +1,22 @@ +Output demo/local-time.gif + +# https://github.com/charmbracelet/vhs/blob/main/THEMES.md +Set Theme "AtomOneLight" + +Set FontSize 14 +Set Width 800 +Set Height 400 +Set Padding 0 +Set Margin 1 + +# --- START --- +Set LoopOffset 4 +Hide +Type "cargo run -- -m c" +Enter +Sleep 0.2 +Show +Sleep 1 +# --- toggle local time --- +Type@1.5s ":::" +Sleep 1.5 diff --git a/justfile b/justfile index 9361b0f..15f7a1d 100644 --- a/justfile +++ b/justfile @@ -63,3 +63,8 @@ alias dm := demo-menu demo-menu: vhs demo/menu.tape + +alias dlt := demo-local-time + +demo-local-time: + vhs demo/local-time.tape diff --git a/src/app.rs b/src/app.rs index b45467a..ec9beef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use crate::{ args::Args, - common::{Content, Style}, + common::{AppTime, AppTimeFormat, Content, Style}, constants::TICK_VALUE_MS, events::{Event, EventHandler, Events}, storage::AppStorage, @@ -8,7 +8,7 @@ use crate::{ widgets::{ clock::{self, Clock, ClockArgs}, countdown::{Countdown, CountdownWidget}, - footer::Footer, + footer::{Footer, FooterState}, header::Header, pomodoro::{Mode as PomodoroMode, Pomodoro, PomodoroArgs, PomodoroWidget}, timer::{Timer, TimerWidget}, @@ -22,6 +22,7 @@ use ratatui::{ widgets::{StatefulWidget, Widget}, }; use std::time::Duration; +use time::OffsetDateTime; use tracing::debug; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -34,18 +35,20 @@ enum Mode { pub struct App { content: Content, mode: Mode, - show_menu: bool, + app_time: AppTime, countdown: Countdown, timer: Timer, pomodoro: Pomodoro, style: Style, with_decis: bool, + footer_state: FooterState, } pub struct AppArgs { pub style: Style, pub with_decis: bool, pub show_menu: bool, + pub app_time_format: AppTimeFormat, pub content: Content, pub pomodoro_mode: PomodoroMode, pub initial_value_work: Duration, @@ -64,6 +67,7 @@ impl From<(Args, AppStorage)> for AppArgs { AppArgs { with_decis: args.decis || stg.with_decis, show_menu: args.menu || stg.show_menu, + app_time_format: stg.app_time_format, content: args.mode.unwrap_or(stg.content), style: args.style.unwrap_or(stg.style), pomodoro_mode: stg.pomodoro_mode, @@ -81,11 +85,19 @@ impl From<(Args, AppStorage)> for AppArgs { } } +fn get_app_time() -> AppTime { + match OffsetDateTime::now_local() { + Ok(t) => AppTime::Local(t), + Err(_) => AppTime::Utc(OffsetDateTime::now_utc()), + } +} + impl App { pub fn new(args: AppArgs) -> Self { let AppArgs { style, show_menu, + app_time_format, initial_value_work, initial_value_pause, initial_value_countdown, @@ -100,7 +112,7 @@ impl App { Self { mode: Mode::Running, content, - show_menu, + app_time: get_app_time(), style, with_decis, countdown: Countdown::new(Clock::::new(ClockArgs { @@ -126,12 +138,17 @@ impl App { style, with_decis, }), + footer_state: FooterState::new(show_menu, app_time_format), } } pub async fn run(mut self, mut terminal: Terminal, mut events: Events) -> Result { while self.is_running() { if let Some(event) = events.next().await { + if matches!(event, Event::Tick) { + self.app_time = get_app_time(); + } + // Pipe events into subviews and handle only 'unhandled' events afterwards if let Some(unhandled) = match self.content { Content::Countdown => self.countdown.update(event.clone()), @@ -186,7 +203,12 @@ impl App { KeyCode::Char('c') => self.content = Content::Countdown, KeyCode::Char('t') => self.content = Content::Timer, KeyCode::Char('p') => self.content = Content::Pomodoro, - KeyCode::Char('m') => self.show_menu = !self.show_menu, + // toogle app time format + KeyCode::Char(':') => self.footer_state.toggle_app_time_format(), + // toogle menu + KeyCode::Char('m') => self + .footer_state + .set_show_menu(!self.footer_state.get_show_menu()), KeyCode::Char(',') => { self.style = self.style.next(); // update clocks @@ -201,8 +223,8 @@ impl App { self.countdown.set_with_decis(self.with_decis); self.pomodoro.set_with_decis(self.with_decis); } - KeyCode::Up => self.show_menu = true, - KeyCode::Down => self.show_menu = false, + KeyCode::Up => self.footer_state.set_show_menu(true), + KeyCode::Down => self.footer_state.set_show_menu(false), _ => {} }; } @@ -217,7 +239,8 @@ impl App { pub fn to_storage(&self) -> AppStorage { AppStorage { content: self.content, - show_menu: self.show_menu, + show_menu: self.footer_state.get_show_menu(), + app_time_format: *self.footer_state.app_time_format(), style: self.style, with_decis: self.with_decis, pomodoro_mode: self.pomodoro.get_mode().clone(), @@ -256,7 +279,11 @@ impl StatefulWidget for AppWidget { let [v0, v1, v2] = Layout::vertical([ Constraint::Length(1), Constraint::Percentage(100), - Constraint::Length(if state.show_menu { 4 } else { 1 }), + Constraint::Length(if state.footer_state.get_show_menu() { + 4 + } else { + 1 + }), ]) .areas(area); @@ -268,12 +295,12 @@ impl StatefulWidget for AppWidget { // content self.render_content(v1, buf, state); // footer - Footer { - show_menu: state.show_menu, + let footer = Footer { running_clock: state.clock_is_running(), selected_content: state.content, edit_mode: state.is_edit_mode(), - } - .render(v2, buf); + app_time: state.app_time, + }; + StatefulWidget::render(footer, v2, buf, &mut state.footer_state); } } diff --git a/src/common.rs b/src/common.rs index 8217d5a..c85fe45 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,8 @@ use clap::ValueEnum; use ratatui::symbols::shade; use serde::{Deserialize, Serialize}; +use time::format_description; +use time::OffsetDateTime; #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize, @@ -62,3 +64,66 @@ impl Style { } } } + +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub enum AppTimeFormat { + /// `hh:mm:ss` + #[default] + HhMmSs, + /// `hh:mm` + HhMm, + /// `hh:mm AM` (or PM) + Hh12Mm, + /// `` (empty) + Hidden, +} + +impl AppTimeFormat { + pub fn next(&self) -> Self { + match self { + AppTimeFormat::HhMmSs => AppTimeFormat::HhMm, + AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm, + AppTimeFormat::Hh12Mm => AppTimeFormat::Hidden, + AppTimeFormat::Hidden => AppTimeFormat::HhMmSs, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum AppTime { + Local(OffsetDateTime), + Utc(OffsetDateTime), +} + +impl From for OffsetDateTime { + fn from(app_time: AppTime) -> Self { + match app_time { + AppTime::Local(t) => t, + AppTime::Utc(t) => t, + } + } +} + +impl AppTime { + pub fn format(&self, app_format: &AppTimeFormat) -> String { + let parse_str = match app_format { + AppTimeFormat::HhMmSs => Some("[hour]:[minute]:[second]"), + AppTimeFormat::HhMm => Some("[hour]:[minute]"), + AppTimeFormat::Hh12Mm => Some("[hour]:[minute] [period]"), + AppTimeFormat::Hidden => None, + }; + + if let Some(str) = parse_str { + format_description::parse(str) + .map_err(|_| "parse error") + .and_then(|fd| { + OffsetDateTime::from(*self) + .format(&fd) + .map_err(|_| "format error") + }) + .unwrap_or_else(|e| e.to_string()) + } else { + "".to_owned() + } + } +} diff --git a/src/storage.rs b/src/storage.rs index a30383f..a805cfa 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,5 +1,5 @@ use crate::{ - common::{Content, Style}, + common::{AppTimeFormat, Content, Style}, widgets::pomodoro::Mode as PomodoroMode, }; use color_eyre::eyre::Result; @@ -12,6 +12,7 @@ use std::time::Duration; pub struct AppStorage { pub content: Content, pub show_menu: bool, + pub app_time_format: AppTimeFormat, pub style: Style, pub with_decis: bool, pub pomodoro_mode: PomodoroMode, @@ -36,6 +37,7 @@ impl Default for AppStorage { AppStorage { content: Content::default(), show_menu: true, + app_time_format: AppTimeFormat::default(), style: Style::default(), with_decis: false, pomodoro_mode: PomodoroMode::Work, diff --git a/src/widgets/footer.rs b/src/widgets/footer.rs index c623dc0..8a22f66 100644 --- a/src/widgets/footer.rs +++ b/src/widgets/footer.rs @@ -1,25 +1,57 @@ use std::collections::BTreeMap; -use crate::common::Content; +use crate::common::{AppTime, AppTimeFormat, Content}; use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Modifier, Style}, symbols::{border, scrollbar}, text::{Line, Span}, - widgets::{Block, Borders, Cell, Row, Table, Widget}, + widgets::{Block, Borders, Cell, Row, StatefulWidget, Table, Widget}, }; #[derive(Debug, Clone)] +pub struct FooterState { + show_menu: bool, + app_time_format: AppTimeFormat, +} + +impl FooterState { + pub const fn new(show_menu: bool, app_time_format: AppTimeFormat) -> Self { + Self { + show_menu, + app_time_format, + } + } + + pub fn set_show_menu(&mut self, value: bool) { + self.show_menu = value; + } + + pub const fn get_show_menu(&self) -> bool { + self.show_menu + } + + pub const fn app_time_format(&self) -> &AppTimeFormat { + &self.app_time_format + } + + pub fn toggle_app_time_format(&mut self) { + self.app_time_format = self.app_time_format.next(); + } +} + +#[derive(Debug)] pub struct Footer { - pub show_menu: bool, pub running_clock: bool, pub selected_content: Content, pub edit_mode: bool, + pub app_time: AppTime, } -impl Widget for Footer { - fn render(self, area: Rect, buf: &mut Buffer) { +impl StatefulWidget for Footer { + type State = FooterState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let content_labels: BTreeMap = BTreeMap::from([ (Content::Countdown, "[c]ountdown"), (Content::Timer, "[t]imer"), @@ -31,15 +63,25 @@ impl Widget for Footer { let [border_area, menu_area] = Layout::vertical([Constraint::Length(1), Constraint::Percentage(100)]).areas(area); + Block::new() .borders(Borders::TOP) .title( - format! {"[m]enu {:} ", if self.show_menu {scrollbar::VERTICAL.end} else {scrollbar::VERTICAL.begin}}, + format! {"[m]enu {:} ", if state.show_menu {scrollbar::VERTICAL.end} else {scrollbar::VERTICAL.begin}}, ) + .title( + Line::from( + match state.app_time_format { + // `Hidden` -> no (empty) title + AppTimeFormat::Hidden => "".into(), + // others -> add some space around + _ => format!(" {} ", self.app_time.format(&state.app_time_format)) + } + ).right_aligned()) .border_set(border::PLAIN) .render(border_area, buf); // show menu - if self.show_menu { + if state.show_menu { let content_labels: Vec = content_labels .iter() .enumerate() @@ -60,7 +102,7 @@ impl Widget for Footer { const SPACE: &str = " "; // 2 empty spaces let widths = [Constraint::Length(12), Constraint::Percentage(100)]; - Table::new( + let table = Table::new( [ // content Row::new(vec![ @@ -80,6 +122,14 @@ impl Widget for Footer { Span::from("[,]change style"), Span::from(SPACE), Span::from("[.]toggle deciseconds"), + Span::from(SPACE), + Span::from(format!( + "[:]toggle {} time", + match self.app_time { + AppTime::Local(_) => "local", + AppTime::Utc(_) => "utc", + } + )), ])), ]), // edit @@ -128,8 +178,9 @@ impl Widget for Footer { ], widths, ) - .column_spacing(1) - .render(menu_area, buf); + .column_spacing(1); + + Widget::render(table, menu_area, buf); } } }