diff --git a/Cargo.toml b/Cargo.toml index bbd6632..ef338fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crock" -version = "0.2.0" +version = "0.2.1-alpha.1" edition = "2021" publish = true authors = ["Christoph J. Scherr "] diff --git a/src/clock.rs b/src/clock.rs index 97b948c..99ad61b 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -11,38 +11,16 @@ use ratatui::backend::CrosstermBackend; use ratatui::crossterm::event::{self, poll, Event, KeyCode, KeyModifiers}; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; -use ratatui::widgets::{Block, LineGauge, Padding, Paragraph}; +use ratatui::widgets::{Block, Padding, Paragraph}; use ratatui::Terminal; +use std::collections::HashMap; use std::io::{Cursor, Stdout, Write}; -use std::time::{Duration, Instant}; +use std::time::Instant; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TimeBarLength { - Minute, - Hour, - Custom(i128), - /// implementing a bar that would grow smaller would be weird, so it's a count up instead of - /// a countdown - Countup(i128), - Day, -} - -impl TimeBarLength { - pub(crate) const fn as_secs(self) -> i128 { - match self { - Self::Minute => 60, - Self::Day => 24 * 60 * 60, - Self::Hour => 60 * 60, - Self::Custom(secs) | Self::Countup(secs) => secs, - } - } -} - -impl Default for TimeBarLength { - fn default() -> Self { - Self::Minute - } -} +pub mod timebar; +pub mod ui; +use timebar::TimeBarLength; +use ui::Data; /// Make your terminal into a big clock #[derive(Parser, Debug, Clone)] @@ -89,56 +67,6 @@ pub struct Clock { pub(crate) did_notify: bool, } -#[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] - } -} - impl Clock { #[must_use] #[allow(clippy::missing_const_for_fn)] @@ -150,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 } @@ -185,7 +111,7 @@ impl Clock { } TimeBarLength::Custom(_) => { if since_last_reset.num_seconds() >= 1 - && i128::from(since_last_reset.num_seconds()) >= len.as_secs() + && since_last_reset.num_seconds() >= len.as_secs() { self.last_reset = Some(Local::now()); } @@ -262,9 +188,9 @@ 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(); + let mut uidata: Data = Data::default(); self.setup()?; loop { let raw_time = chrono::Local::now().round_subsecs(0); @@ -291,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)?; @@ -323,7 +251,7 @@ impl Clock { fn ui( &mut self, terminal: &mut Terminal>, - data: &UiData, + data: &Data, ) -> anyhow::Result<()> { terminal.draw(|frame| { debug!("rendering the ui"); @@ -339,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); @@ -360,58 +288,24 @@ 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[2].width) * 0.43) as u16, - (f32::from(parts[2].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()) .blue() .block(Block::default().padding(Padding::right(2))) .alignment(Alignment::Right); - frame.render_widget(&timebarw, parts[2]); - frame.render_widget(datew, parts[1]); + 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[0]); + frame.render_widget(clockw, parts["clockw"]); })?; debug!("done rendering the ui"); Ok(()) @@ -485,7 +379,7 @@ impl Clock { std::io::stdout().flush()?; Ok(()) } - fn partition(r: Rect) -> Vec { + fn partition(r: Rect) -> HashMap<&'static str, Rect> { let part = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -504,6 +398,16 @@ impl Clock { ]) .split(part[0]); - vec![part[1], subparts[0], subparts[1]] + let timebarw_spaces = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Length(1)]) + .split(subparts[1]); + + HashMap::from([ + ("clockw", part[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 new file mode 100644 index 0000000..4be656e --- /dev/null +++ b/src/clock/timebar.rs @@ -0,0 +1,65 @@ +use std::fmt::Display; + +use chrono::Duration; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TimeBarLength { + Minute, + Hour, + Custom(i64), + /// implementing a bar that would grow smaller would be weird, so it's a count up instead of + /// a countdown + Countup(i64), + Day, +} + +impl TimeBarLength { + pub(crate) const fn as_secs(self) -> i64 { + match self { + Self::Minute => 60, + Self::Day => 24 * 60 * 60, + Self::Hour => 60 * 60, + Self::Custom(secs) | Self::Countup(secs) => secs, + } + } +} + +impl From for chrono::Duration { + fn from(value: TimeBarLength) -> Self { + Self::new(value.as_secs(), 0).expect("seconds out of bounds, cannot create duration") + } +} + +impl Default for TimeBarLength { + fn default() -> Self { + 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..3fd8480 --- /dev/null +++ b/src/clock/ui.rs @@ -0,0 +1,174 @@ +use chrono::{DateTime, Local, SubsecRound, Timelike}; +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 Data { + now: [DateTime; 2], + fdate: [String; 2], + ftime: [String; 2], + timebar_ratio: [Option; 2], + + idx: usize, +} + +impl Data { + pub fn update( + &mut self, + now: DateTime, + fdate: String, + ftime: String, + timebar_ratio: Option, + ) { + self.idx ^= 1; + self.now[self.idx] = now; + self.fdate[self.idx] = fdate; + self.ftime[self.idx] = ftime; + self.timebar_ratio[self.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.idx] + } + + #[must_use] + #[inline] + pub fn ftime(&self) -> &str { + &self.ftime[self.idx] + } + + #[must_use] + #[inline] + pub fn now(&self) -> &DateTime { + &self.now[self.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.idx] + } +} + +pub fn timebarw<'a>( + clock: &mut Clock, + data: &Data, + 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: &Data, + 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(), + ), + }; + let until = last_reset + // BUG: seconds are sometimes a little too much, for + // example with `-o` #17 + .checked_add_signed(len.into()) + .expect("could not calculate when the countdown finishes"); + let timestamp_until: String = format!( + "{:02}:{:02}:{:02}", + until.hour(), + until.minute(), + until.second() + ); + Paragraph::new(format!("{time_now} / {len} ({timestamp_until})")) + .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/main.rs b/src/main.rs index 065a46b..2294a51 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 self::clock::UiData; + use crate::clock::ui::Data; info!("doing the mock tests"); { let mut c = Clock::parse_from(["some exec", "-mvvv"]); @@ -81,12 +81,13 @@ fn mock_tests() { info!("0s=0.0"); } { - let mut data = UiData::default(); - data.update("date".to_owned(), "time".to_owned(), Some(0.1)); + let mut data = Data::default(); + 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");