diff --git a/src/clock.rs b/src/clock.rs index 7d9f961..a0cde82 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -15,12 +15,12 @@ use ratatui::widgets::{Block, LineGauge, Padding, Paragraph}; use ratatui::Terminal; use std::collections::HashMap; use std::io::{Cursor, Stdout, Write}; -use std::time::{Duration, Instant}; +use std::time::Instant; pub mod timebar; -pub mod uidata; +pub mod ui; use timebar::TimeBarLength; -use uidata::UiData; +use ui::UiData; /// Make your terminal into a big clock #[derive(Parser, Debug, Clone)] @@ -78,13 +78,11 @@ impl Clock { } else if self.hour { Some(TimeBarLength::Hour) } else if self.countdown.is_some() { - Some(TimeBarLength::Countup(i128::from( - self.countdown.unwrap().as_secs(), - ))) + Some(TimeBarLength::Countup( + self.countdown.unwrap().as_secs() as i64 + )) } else if self.custom.is_some() { - Some(TimeBarLength::Custom(i128::from( - self.custom.unwrap().as_secs(), - ))) + Some(TimeBarLength::Custom(self.custom.unwrap().as_secs() as i64)) } else { None } @@ -113,7 +111,7 @@ impl Clock { } TimeBarLength::Custom(_) => { if since_last_reset.num_seconds() >= 1 - && i128::from(since_last_reset.num_seconds()) >= len.as_secs() + && i64::from(since_last_reset.num_seconds()) >= len.as_secs() { self.last_reset = Some(Local::now()); } @@ -190,7 +188,7 @@ impl Clock { mut self, terminal: &mut Terminal>, ) -> anyhow::Result<()> { - let tick_rate = Duration::from_millis(100); + let tick_rate = std::time::Duration::from_millis(100); let mut last_tick = Instant::now(); let mut uidata: UiData = UiData::default(); self.setup()?; @@ -219,10 +217,12 @@ impl Clock { // 01:30 is 50%, 01:59 is 98%, 01:60 does not exist because that's how counting from // 0 works. + let now = raw_time + chrono::Duration::seconds(1); uidata.update( + now, splits[0].clone(), splits[1].clone(), - self.timebar_ratio(raw_time + chrono::Duration::seconds(1)), + self.timebar_ratio(now), ); if uidata.changed() { self.ui(terminal, &uidata)?; @@ -267,12 +267,12 @@ impl Clock { .title_bottom(env!("CARGO_PKG_VERSION")) .title_alignment(Alignment::Center) .title_style(Style::new().bold()); - let a = space.inner(root); + let inner_rect = space.inner(root); frame.render_widget(space, root); - let parts = Self::partition(a); + let parts = Self::partition(inner_rect); let mut clockw = tui_big_text::BigText::builder(); - if a.width > 80 { + if inner_rect.width > 80 { clockw.pixel_size(tui_big_text::PixelSize::Full); } else { clockw.pixel_size(tui_big_text::PixelSize::Quadrant); @@ -288,48 +288,13 @@ impl Clock { // render the timebar which counts up to the full minute and so on // // Will not be rendered if it is None - let timebarw: Option = if self.timebar_len().is_some() { - debug!("time bar ration: {:?}", data.timebar_ratio()); - let ratio = data.timebar_ratio().unwrap(); - - if !self.did_notify && (ratio - 1.0).abs() < 0.000_001 { - if let Some(TimeBarLength::Countup(_)) = self.timebar_len() { - let _ = self.notify().inspect_err(|e| { - error!("could not notify: {e}"); - debug!("complete error: {e:#?}"); - }); - self.did_notify = true; - } - } - - #[allow(clippy::cast_sign_loss)] - #[allow(clippy::cast_possible_truncation)] - let padding = [ - (f32::from(parts["timebarw"].width) * 0.43) as u16, - (f32::from(parts["timebarw"].width) * 0.25) as u16, - ]; - let timebarw = LineGauge::default() - .filled_style(if self.did_notify { - Style::default() - .slow_blink() - .bold() - .underlined() - .yellow() - .crossed_out() - } else { - Style::default().blue() - }) - .unfilled_style(Style::default()) - .block(Block::default().padding(Padding::right(if a.width > 80 { - padding[0] - } else { - padding[1] - }))) - .ratio(ratio); - Some(timebarw) - } else { - None - }; + let timebarw_padding = [ + (f32::from(parts["timebarw"].width) * 0.43) as u16, + (f32::from(parts["timebarw"].width) * 0.25) as u16, + ]; + let timebarw = ui::timebarw(self, data, &timebarw_padding, inner_rect); + let timebarw_label: Option = + ui::timebarw_label(self, data, &timebarw_padding, inner_rect); // render the small date let datew = Paragraph::new(data.fdate()) @@ -337,6 +302,7 @@ impl Clock { .block(Block::default().padding(Padding::right(2))) .alignment(Alignment::Right); frame.render_widget(&timebarw, parts["timebarw"]); + frame.render_widget(&timebarw_label, parts["timebarw_label"]); frame.render_widget(datew, parts["datew"]); // render the clock frame.render_widget(clockw, parts["clockw"]); @@ -432,9 +398,15 @@ impl Clock { ]) .split(part[0]); + let timebarw_spaces = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Length(1)]) + .split(subparts[1]); + HashMap::from([ ("clockw", part[1]), - ("timebarw", subparts[1]), + ("timebarw", timebarw_spaces[0]), + ("timebarw_label", timebarw_spaces[1]), ("datew", subparts[0]), ]) } diff --git a/src/clock/timebar.rs b/src/clock/timebar.rs index 5886308..410f1d6 100644 --- a/src/clock/timebar.rs +++ b/src/clock/timebar.rs @@ -1,16 +1,20 @@ +use std::fmt::Display; + +use chrono::{Duration, TimeDelta}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeBarLength { Minute, Hour, - Custom(i128), + Custom(i64), /// implementing a bar that would grow smaller would be weird, so it's a count up instead of /// a countdown - Countup(i128), + Countup(i64), Day, } impl TimeBarLength { - pub(crate) const fn as_secs(self) -> i128 { + pub(crate) const fn as_secs(self) -> i64 { match self { Self::Minute => 60, Self::Day => 24 * 60 * 60, @@ -25,3 +29,31 @@ impl Default for TimeBarLength { Self::Minute } } + +impl Display for TimeBarLength { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let buf = match self { + Self::Minute => humantime::Duration::from( + Duration::minutes(1) + .to_std() + .expect("could not convert chrono time to std time"), + ), + Self::Hour => humantime::Duration::from( + Duration::hours(1) + .to_std() + .expect("could not convert chrono time to std time"), + ), + Self::Day => humantime::Duration::from( + Duration::days(1) + .to_std() + .expect("could not convert chrono time to std time"), + ), + Self::Custom(secs) | Self::Countup(secs) => humantime::Duration::from( + Duration::seconds(*secs) + .to_std() + .expect("could not convert chrono time to std time"), + ), + }; + write!(f, "{buf}") + } +} diff --git a/src/clock/ui.rs b/src/clock/ui.rs new file mode 100644 index 0000000..2f5f72e --- /dev/null +++ b/src/clock/ui.rs @@ -0,0 +1,163 @@ +use chrono::{DateTime, Local, SubsecRound}; +use libpt::log::{debug, error, trace}; +use ratatui::layout::{Alignment, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::widgets::{Block, LineGauge, Padding, Paragraph}; + +use crate::clock::timebar::TimeBarLength; + +use super::Clock; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct UiData { + now: [DateTime; 2], + fdate: [String; 2], + ftime: [String; 2], + timebar_ratio: [Option; 2], + + data_idx: usize, +} + +impl UiData { + pub fn update( + &mut self, + now: DateTime, + fdate: String, + ftime: String, + timebar_ratio: Option, + ) { + self.data_idx ^= 1; + self.now[self.data_idx] = now; + self.fdate[self.data_idx] = fdate; + self.ftime[self.data_idx] = ftime; + self.timebar_ratio[self.data_idx] = timebar_ratio; + #[cfg(debug_assertions)] + if self.changed() { + trace!("update with change: {:#?}", self); + } + } + + /// did the data change with the last update? + #[must_use] + #[inline] + pub fn changed(&self) -> bool { + // the timebar ratio is discarded, so that we only render the ui when the time + // (second) changes + self.fdate[0] != self.fdate[1] || self.ftime[0] != self.ftime[1] + } + + #[must_use] + #[inline] + pub fn fdate(&self) -> &str { + &self.fdate[self.data_idx] + } + + #[must_use] + #[inline] + pub fn ftime(&self) -> &str { + &self.ftime[self.data_idx] + } + + #[must_use] + #[inline] + pub fn now(&self) -> &DateTime { + &self.now[self.data_idx] + } + + #[must_use] + #[inline] + #[allow(clippy::missing_const_for_fn)] // no it's not const + pub fn timebar_ratio(&self) -> Option { + self.timebar_ratio[self.data_idx] + } +} + +pub fn timebarw<'a>( + clock: &mut Clock, + data: &UiData, + timebarw_padding: &[u16], + inner_rect: Rect, +) -> Option> { + if clock.timebar_len().is_some() { + debug!("time bar ration: {:?}", data.timebar_ratio()); + let ratio = data.timebar_ratio().unwrap(); + + if !clock.did_notify && (ratio - 1.0).abs() < 0.000_001 { + if let Some(TimeBarLength::Countup(_)) = clock.timebar_len() { + let _ = clock.notify().inspect_err(|e| { + error!("could not notify: {e}"); + debug!("complete error: {e:#?}"); + }); + clock.did_notify = true; + } + } + + #[allow(clippy::cast_sign_loss)] + #[allow(clippy::cast_possible_truncation)] + let timebarw = LineGauge::default() + .filled_style(if clock.did_notify { + Style::default() + .slow_blink() + .bold() + .underlined() + .yellow() + .crossed_out() + } else { + Style::default().blue() + }) + .unfilled_style(Style::default()) + .block( + Block::default().padding(Padding::right(if inner_rect.width > 80 { + timebarw_padding[0] + } else { + timebarw_padding[1] + })), + ) + .ratio(ratio); + Some(timebarw) + } else { + None + } +} + +pub fn timebarw_label<'a>( + clock: &Clock, + data: &UiData, + timebarw_padding: &[u16], + inner_rect: Rect, +) -> Option> { + clock.timebar_len().map(|len| { + let last_reset = clock.last_reset.unwrap().round_subsecs(0); + let time_now = match clock.timebar_len().unwrap() { + TimeBarLength::Countup(secs) => { + if clock.did_notify { + humantime::Duration::from(chrono::Duration::seconds(secs).to_std().unwrap()) + } else { + humantime::Duration::from( + data.now() + .round_subsecs(0) + .signed_duration_since(last_reset) + .to_std() + .unwrap(), + ) + } + } + _ => humantime::Duration::from( + data.now() + .round_subsecs(0) + .signed_duration_since(last_reset) + .to_std() + .unwrap(), + ), + }; + Paragraph::new(format!("{} / {}", time_now, len)) + .alignment(Alignment::Center) + .block( + Block::default().padding(Padding::right(if inner_rect.width > 80 { + timebarw_padding[0] + } else { + timebarw_padding[1] + })), + ) + }) +} diff --git a/src/clock/uidata.rs b/src/clock/uidata.rs deleted file mode 100644 index c801c6e..0000000 --- a/src/clock/uidata.rs +++ /dev/null @@ -1,51 +0,0 @@ -use libpt::log::trace; - -#[derive(Debug, Clone, PartialEq, Default)] -pub struct UiData { - fdate: [String; 2], - ftime: [String; 2], - timebar_ratio: [Option; 2], - - data_idx: usize, -} - -impl UiData { - pub fn update(&mut self, fdate: String, ftime: String, timebar_ratio: Option) { - self.data_idx ^= 1; - self.fdate[self.data_idx] = fdate; - self.ftime[self.data_idx] = ftime; - self.timebar_ratio[self.data_idx] = timebar_ratio; - #[cfg(debug_assertions)] - if self.changed() { - trace!("update with change: {:#?}", self); - } - } - - /// did the data change with the last update? - #[must_use] - #[inline] - pub fn changed(&self) -> bool { - // the timebar ratio is discarded, so that we only render the ui when the time - // (second) changes - self.fdate[0] != self.fdate[1] || self.ftime[0] != self.ftime[1] - } - - #[must_use] - #[inline] - pub fn fdate(&self) -> &str { - &self.fdate[self.data_idx] - } - - #[must_use] - #[inline] - pub fn ftime(&self) -> &str { - &self.ftime[self.data_idx] - } - - #[must_use] - #[inline] - #[allow(clippy::missing_const_for_fn)] // no it's not const - pub fn timebar_ratio(&self) -> Option { - self.timebar_ratio[self.data_idx] - } -} diff --git a/src/main.rs b/src/main.rs index c933f04..6f44694 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,7 +63,7 @@ fn mock_tests() { use chrono::{Local, Timelike}; use libpt::log::info; - use crate::clock::uidata::UiData; + use crate::clock::ui::UiData; info!("doing the mock tests"); { let mut c = Clock::parse_from(["some exec", "-mvvv"]); @@ -82,11 +82,12 @@ fn mock_tests() { } { let mut data = UiData::default(); - data.update("date".to_owned(), "time".to_owned(), Some(0.1)); + let now = Local::now(); + data.update(now, "date".to_owned(), "time".to_owned(), Some(0.1)); assert_eq!(data.timebar_ratio(), Some(0.1)); - data.update("date".to_owned(), "time".to_owned(), Some(0.2)); + data.update(now, "date".to_owned(), "time".to_owned(), Some(0.2)); assert_eq!(data.timebar_ratio(), Some(0.2)); - data.update("date".to_owned(), "time".to_owned(), Some(0.3)); + data.update(now, "date".to_owned(), "time".to_owned(), Some(0.3)); assert_eq!(data.timebar_ratio(), Some(0.3)); } info!("finished the mock tests");