From 3056e4fa36a9b5d5aaec08eddfed385df1b8651a Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 12 Jul 2024 19:14:12 +0200 Subject: [PATCH 01/20] chore: git merge shenanigans --- Cargo.toml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 639252d..bbd6632 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crock" -version = "0.1.5" +version = "0.2.0" edition = "2021" publish = true authors = ["Christoph J. Scherr "] @@ -12,10 +12,22 @@ repository = "https://git.cscherr.de/PlexSheep/crock" keywords = ["time", "clock", "tui"] categories = ["date-and-time"] +[features] +default = ["desktop", "sound"] +desktop = ["dep:notify-rust"] +sound = ["dep:rodio"] + [dependencies] anyhow = "1.0.86" chrono = "0.4.38" +humantime = "2.1.0" libpt = { version = "0.6.0", features = ["cli"] } +notify-rust = { version = "4.11.0", default-features = false, features = [ + "d", +], optional = true } ratatui = "0.27.0" +rodio = { version = "0.19.0", optional = true, default-features = false, features = [ + "mp3", +] } tui-big-text = "0.4.5" From daa764cb96f09f2feab2f1efd778826fac7df8cc Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 12 Jul 2024 19:15:14 +0200 Subject: [PATCH 02/20] feat(notify): make sound be on by default #15 --- src/clock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clock.rs b/src/clock.rs index 162eb0f..c074e8c 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -78,7 +78,7 @@ pub struct Clock { #[clap(short = 'u', long, value_parser = humantime::parse_duration)] pub countdown: Option, /// Play a notification sound when the countdown is up - #[clap(short, long)] + #[clap(short, long, default_value_t = true)] pub sound: bool, // internal variables From d62fd468ffd04fe2f1e551d42f7e782df42368ea Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 12 Jul 2024 19:15:58 +0200 Subject: [PATCH 03/20] refactor: hide the sound option behind the sound feature --- src/clock.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clock.rs b/src/clock.rs index c074e8c..27d20f8 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -78,6 +78,7 @@ pub struct Clock { #[clap(short = 'u', long, value_parser = humantime::parse_duration)] pub countdown: Option, /// Play a notification sound when the countdown is up + #[cfg(feature = "sound")] #[clap(short, long, default_value_t = true)] pub sound: bool, From 6863448974c471fed99f35999bc0d08db8a0a4d7 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 12 Jul 2024 19:25:08 +0200 Subject: [PATCH 04/20] chore: address clippy findings --- src/clock.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index 27d20f8..22fa865 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -374,6 +374,12 @@ impl Clock { } } + #[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() @@ -387,9 +393,9 @@ impl Clock { }) .unfilled_style(Style::default()) .block(Block::default().padding(Padding::right(if a.width > 80 { - (f32::from(parts[2].width) * 0.43) as u16 + padding[0] } else { - (f32::from(parts[2].width) * 0.25) as u16 + padding[1] }))) .ratio(ratio); Some(timebarw) @@ -490,6 +496,8 @@ impl Clock { Constraint::Length(if r.width > 80 { 8 } else { 5 }), ]) .split(r); + #[allow(clippy::cast_sign_loss)] + #[allow(clippy::cast_possible_truncation)] let hlen_date: u16 = (f32::from(part[1].width) * 0.35) as u16; let subparts = Layout::default() .direction(Direction::Horizontal) From 27d135b1c833530f4d2fd37b13ff1d28e77d5049 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 12 Jul 2024 19:25:16 +0200 Subject: [PATCH 05/20] chore: remove an old todo comment --- src/clock.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index 22fa865..97b948c 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -452,9 +452,6 @@ impl Clock { // NOTE: sadly, notify_rust does not (yet) support KDE plasma, because // they have a weird way of making sounds and notifications in general // work. At least we get a little notification. - // - // TODO: add something to make a sound without the notification system, - // as that is not reliable but the user might depend on it. // only play this when we don't use built in sound, this // isn't as consistent From d6102dffa2cdcabd74d03c25332a54dc1a02c958 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 17 Jul 2024 16:30:39 +0200 Subject: [PATCH 06/20] refactor: move clock parts to submodules --- src/clock.rs | 81 +++----------------------------------------- src/clock/timebar.rs | 27 +++++++++++++++ src/clock/uidata.rs | 51 ++++++++++++++++++++++++++++ src/main.rs | 2 +- 4 files changed, 83 insertions(+), 78 deletions(-) create mode 100644 src/clock/timebar.rs create mode 100644 src/clock/uidata.rs diff --git a/src/clock.rs b/src/clock.rs index 97b948c..5d90bd0 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -16,33 +16,10 @@ use ratatui::Terminal; use std::io::{Cursor, Stdout, Write}; use std::time::{Duration, 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 uidata; +use timebar::TimeBarLength; +use uidata::UiData; /// Make your terminal into a big clock #[derive(Parser, Debug, Clone)] @@ -89,56 +66,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)] diff --git a/src/clock/timebar.rs b/src/clock/timebar.rs new file mode 100644 index 0000000..5886308 --- /dev/null +++ b/src/clock/timebar.rs @@ -0,0 +1,27 @@ +#[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 + } +} diff --git a/src/clock/uidata.rs b/src/clock/uidata.rs new file mode 100644 index 0000000..c801c6e --- /dev/null +++ b/src/clock/uidata.rs @@ -0,0 +1,51 @@ +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 065a46b..c933f04 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::uidata::UiData; info!("doing the mock tests"); { let mut c = Clock::parse_from(["some exec", "-mvvv"]); From 700f236649c7dc0a1ee5f31078afc8daa4b6677b Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 17 Jul 2024 16:50:32 +0200 Subject: [PATCH 07/20] refactor: partition to a HashMap instead of a Vec --- src/clock.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index 5d90bd0..7d9f961 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -13,6 +13,7 @@ use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; 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}; @@ -304,8 +305,8 @@ impl Clock { #[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, + (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 { @@ -335,10 +336,10 @@ impl Clock { .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(datew, parts["datew"]); // render the clock - frame.render_widget(clockw, parts[0]); + frame.render_widget(clockw, parts["clockw"]); })?; debug!("done rendering the ui"); Ok(()) @@ -412,7 +413,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([ @@ -431,6 +432,10 @@ impl Clock { ]) .split(part[0]); - vec![part[1], subparts[0], subparts[1]] + HashMap::from([ + ("clockw", part[1]), + ("timebarw", subparts[1]), + ("datew", subparts[0]), + ]) } } From 74e39a08dc223fd80f1c6ba1e3674ea219520dfc Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 17 Jul 2024 17:46:22 +0200 Subject: [PATCH 08/20] feat(timebar): show mode and counter for timebar --- src/clock.rs | 88 ++++++++--------------- src/clock/timebar.rs | 38 +++++++++- src/clock/ui.rs | 163 +++++++++++++++++++++++++++++++++++++++++++ src/clock/uidata.rs | 51 -------------- src/main.rs | 9 +-- 5 files changed, 233 insertions(+), 116 deletions(-) create mode 100644 src/clock/ui.rs delete mode 100644 src/clock/uidata.rs 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"); From 290704de46cb7be95bf924f2ebfcf037c3669040 Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Wed, 17 Jul 2024 15:48:15 +0000 Subject: [PATCH 09/20] automatic cargo CI changes --- src/clock.rs | 4 ++-- src/clock/timebar.rs | 2 +- src/clock/ui.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index a0cde82..9f45cc9 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -11,7 +11,7 @@ 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}; @@ -111,7 +111,7 @@ impl Clock { } TimeBarLength::Custom(_) => { if since_last_reset.num_seconds() >= 1 - && i64::from(since_last_reset.num_seconds()) >= len.as_secs() + && since_last_reset.num_seconds() >= len.as_secs() { self.last_reset = Some(Local::now()); } diff --git a/src/clock/timebar.rs b/src/clock/timebar.rs index 410f1d6..3d761d2 100644 --- a/src/clock/timebar.rs +++ b/src/clock/timebar.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use chrono::{Duration, TimeDelta}; +use chrono::Duration; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeBarLength { diff --git a/src/clock/ui.rs b/src/clock/ui.rs index 2f5f72e..fe6dc7b 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -150,7 +150,7 @@ pub fn timebarw_label<'a>( .unwrap(), ), }; - Paragraph::new(format!("{} / {}", time_now, len)) + Paragraph::new(format!("{time_now} / {len}")) .alignment(Alignment::Center) .block( Block::default().padding(Padding::right(if inner_rect.width > 80 { From 19cc0d5d1adab41151a996480139817b623115c2 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Wed, 17 Jul 2024 17:52:35 +0200 Subject: [PATCH 10/20] chore: bump version to alpha --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bbd6632..c0b0bc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crock" -version = "0.2.0" +version = "0.2.1-alpha.0" edition = "2021" publish = true authors = ["Christoph J. Scherr "] From 3a4c3d7c00dd77b4a7488b2a22254c174d3e581f Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 09:42:33 +0200 Subject: [PATCH 11/20] feat(modeshow): add a timestamp that shows when the timer ends #14 --- src/clock/timebar.rs | 6 ++++++ src/clock/ui.rs | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/clock/timebar.rs b/src/clock/timebar.rs index 3d761d2..4be656e 100644 --- a/src/clock/timebar.rs +++ b/src/clock/timebar.rs @@ -24,6 +24,12 @@ impl TimeBarLength { } } +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 diff --git a/src/clock/ui.rs b/src/clock/ui.rs index fe6dc7b..0027ab0 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Local, SubsecRound}; +use chrono::{DateTime, Local, SubsecRound, Timelike}; use libpt::log::{debug, error, trace}; use ratatui::layout::{Alignment, Rect}; use ratatui::style::{Style, Stylize}; @@ -150,7 +150,12 @@ pub fn timebarw_label<'a>( .unwrap(), ), }; - Paragraph::new(format!("{time_now} / {len}")) + let until = last_reset + .checked_add_signed(len.into()) + .expect("could not calculate when the countdown finishes"); + let timestamp_until: String = + format!("{}:{}:{}", 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 { From 4ec8be1e1be8124c54d82e598c6728c91680ab8d Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 09:58:54 +0200 Subject: [PATCH 12/20] chore: add bug comment for #17 --- src/clock/ui.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clock/ui.rs b/src/clock/ui.rs index 0027ab0..91453b0 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -151,6 +151,8 @@ pub fn timebarw_label<'a>( ), }; 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 = From 252677102660fbf8bc77b14f2ce674673515f837 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 09:59:18 +0200 Subject: [PATCH 13/20] refactor(uidata): change name of the struct and the idx --- src/clock.rs | 6 +++--- src/clock/ui.rs | 28 ++++++++++++++-------------- src/main.rs | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index 9f45cc9..99ad61b 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -20,7 +20,7 @@ use std::time::Instant; pub mod timebar; pub mod ui; use timebar::TimeBarLength; -use ui::UiData; +use ui::Data; /// Make your terminal into a big clock #[derive(Parser, Debug, Clone)] @@ -190,7 +190,7 @@ impl Clock { ) -> anyhow::Result<()> { 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); @@ -251,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"); diff --git a/src/clock/ui.rs b/src/clock/ui.rs index 91453b0..0aa3ebd 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -9,16 +9,16 @@ use crate::clock::timebar::TimeBarLength; use super::Clock; #[derive(Debug, Clone, PartialEq, Default)] -pub struct UiData { +pub struct Data { now: [DateTime; 2], fdate: [String; 2], ftime: [String; 2], timebar_ratio: [Option; 2], - data_idx: usize, + idx: usize, } -impl UiData { +impl Data { pub fn update( &mut self, now: DateTime, @@ -26,11 +26,11 @@ impl UiData { 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; + 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); @@ -49,32 +49,32 @@ impl UiData { #[must_use] #[inline] pub fn fdate(&self) -> &str { - &self.fdate[self.data_idx] + &self.fdate[self.idx] } #[must_use] #[inline] pub fn ftime(&self) -> &str { - &self.ftime[self.data_idx] + &self.ftime[self.idx] } #[must_use] #[inline] pub fn now(&self) -> &DateTime { - &self.now[self.data_idx] + &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.data_idx] + self.timebar_ratio[self.idx] } } pub fn timebarw<'a>( clock: &mut Clock, - data: &UiData, + data: &Data, timebarw_padding: &[u16], inner_rect: Rect, ) -> Option> { @@ -122,7 +122,7 @@ pub fn timebarw<'a>( pub fn timebarw_label<'a>( clock: &Clock, - data: &UiData, + data: &Data, timebarw_padding: &[u16], inner_rect: Rect, ) -> Option> { diff --git a/src/main.rs b/src/main.rs index 6f44694..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 crate::clock::ui::UiData; + use crate::clock::ui::Data; info!("doing the mock tests"); { let mut c = Clock::parse_from(["some exec", "-mvvv"]); @@ -81,7 +81,7 @@ fn mock_tests() { info!("0s=0.0"); } { - let mut data = UiData::default(); + 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)); From 01c3f44026c2b52c7485a7efb490e5eb1e257c01 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 10:00:48 +0200 Subject: [PATCH 14/20] fix(modeshow): force all segments of the time to have 2 digits --- src/clock/ui.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/clock/ui.rs b/src/clock/ui.rs index 0aa3ebd..3fd8480 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -155,8 +155,12 @@ pub fn timebarw_label<'a>( // example with `-o` #17 .checked_add_signed(len.into()) .expect("could not calculate when the countdown finishes"); - let timestamp_until: String = - format!("{}:{}:{}", until.hour(), until.minute(), until.second()); + 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( From b0d0651cc79bf9f0370dd050146a01498ac385a1 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 10:03:33 +0200 Subject: [PATCH 15/20] chore: bump alpha version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c0b0bc6..ef338fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crock" -version = "0.2.1-alpha.0" +version = "0.2.1-alpha.1" edition = "2021" publish = true authors = ["Christoph J. Scherr "] From 48f88ee17e82abc556a72e00d65f0ef1169d2978 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 10:16:22 +0200 Subject: [PATCH 16/20] feat(panic): add human_panic --- Cargo.toml | 1 + src/main.rs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ef338fc..fd21eb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ sound = ["dep:rodio"] [dependencies] anyhow = "1.0.86" chrono = "0.4.38" +human-panic = "2.0.0" humantime = "2.1.0" libpt = { version = "0.6.0", features = ["cli"] } notify-rust = { version = "4.11.0", default-features = false, features = [ diff --git a/src/main.rs b/src/main.rs index 2294a51..0186fa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,13 @@ use self::clock::Clock; mod clock; fn main() -> anyhow::Result<()> { + human_panic::setup_panic!(human_panic::Metadata::new( + env!("CARGO_BIN_NAME"), + env!("CARGO_PKG_VERSION") + ) + .authors(env!("CARGO_PKG_AUTHORS")) + .homepage(env!("CARGO_PKG_HOMEPAGE"))); + // setup the cli let clock = Clock::parse(); if clock.verbose.level() >= Level::DEBUG { From 19c1939cfb60b90923d522dbb386f669ab86cf96 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 19 Jul 2024 10:26:45 +0200 Subject: [PATCH 17/20] fix(modeshow): fix #17 by conditionally setting the seconds to 0 --- src/clock/ui.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/clock/ui.rs b/src/clock/ui.rs index 3fd8480..5d2d1db 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -150,18 +150,20 @@ pub fn timebarw_label<'a>( .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})")) + let until = { + // we need to cut off the seconds if we're not in custom and countup mode, otherwise, + // the timestamp will not be correct. This fixes #17 + match len { + TimeBarLength::Custom(_) | TimeBarLength::Countup(_) => last_reset, + _ => last_reset.with_second(0).unwrap(), + } + } + // 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") + .format("%H:%M:%S"); + Paragraph::new(format!("{time_now} / {len} ({until})")) .alignment(Alignment::Center) .block( Block::default().padding(Padding::right(if inner_rect.width > 80 { From 4957c601ea72187099e37b333c0929b7a7ecf3cb Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 26 Jul 2024 10:38:37 +0200 Subject: [PATCH 18/20] fix(modeshow): display correct durations for hour,minute,day #19 --- src/clock.rs | 21 +++++++++++++++------ src/clock/ui.rs | 7 +++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/clock.rs b/src/clock.rs index 99ad61b..e17b5ce 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -1,7 +1,7 @@ #![warn(clippy::pedantic, clippy::style, clippy::nursery)] #![allow(clippy::question_mark_used)] -use chrono::{DateTime, Local, SubsecRound, Timelike}; +use chrono::{DateTime, Local, SubsecRound, TimeDelta, Timelike}; use clap::Parser; use libpt::cli::args::HELP_TEMPLATE; use libpt::cli::clap::ArgGroup; @@ -113,24 +113,24 @@ impl Clock { if since_last_reset.num_seconds() >= 1 && since_last_reset.num_seconds() >= len.as_secs() { - self.last_reset = Some(Local::now()); + self.last_reset = Some(Local::now().round_subsecs(0)); } } TimeBarLength::Minute => { if since_last_reset.num_seconds() >= 1 && Local::now().second() == 0 { - self.last_reset = Some(Local::now()); + self.last_reset = Some(Local::now().round_subsecs(0)); debug!("reset the time of the time bar (minute)"); } } TimeBarLength::Hour => { if since_last_reset.num_minutes() >= 1 && Local::now().minute() == 0 { - self.last_reset = Some(Local::now()); + self.last_reset = Some(Local::now().round_subsecs(0)); debug!("reset the time of the time bar (hour)"); } } TimeBarLength::Day => { if since_last_reset.num_hours() >= 1 && Local::now().hour() == 0 { - self.last_reset = Some(Local::now()); + self.last_reset = Some(Local::now().round_subsecs(0)); debug!("reset the time of the time bar (day)"); } } @@ -148,13 +148,17 @@ impl Clock { TimeBarLength::Minute => { self.last_reset = Some( Local::now() - .with_second(0) + .round_subsecs(0) + .with_second(1) .expect("tried to use a time that does not exist"), ); } TimeBarLength::Hour => { self.last_reset = Some( Local::now() + .round_subsecs(0) + .with_second(1) + .expect("tried to use a time that does not exist") .with_minute(0) .expect("tried to use a time that does not exist"), ); @@ -162,6 +166,11 @@ impl Clock { TimeBarLength::Day => { self.last_reset = Some( Local::now() + .round_subsecs(0) + .with_second(1) + .expect("tried to use a time that does not exist") + .with_minute(0) + .expect("tried to use a time that does not exist") .with_hour(0) .expect("tried to use a time that does not exist"), ); diff --git a/src/clock/ui.rs b/src/clock/ui.rs index 5d2d1db..f13b98d 100644 --- a/src/clock/ui.rs +++ b/src/clock/ui.rs @@ -8,6 +8,7 @@ use crate::clock::timebar::TimeBarLength; use super::Clock; +// TODO: make this a ringbuffer with a custom struct inside? #[derive(Debug, Clone, PartialEq, Default)] pub struct Data { now: [DateTime; 2], @@ -142,6 +143,12 @@ pub fn timebarw_label<'a>( ) } } + TimeBarLength::Hour => humantime::Duration::from( + data.now() + .signed_duration_since(last_reset) + .to_std() + .unwrap(), + ), _ => humantime::Duration::from( data.now() .round_subsecs(0) From 28d149db42238aaa49d88e59ca9a37c6bb6896fe Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Fri, 26 Jul 2024 08:40:32 +0000 Subject: [PATCH 19/20] automatic cargo CI changes --- src/clock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clock.rs b/src/clock.rs index e17b5ce..c8cdf92 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -1,7 +1,7 @@ #![warn(clippy::pedantic, clippy::style, clippy::nursery)] #![allow(clippy::question_mark_used)] -use chrono::{DateTime, Local, SubsecRound, TimeDelta, Timelike}; +use chrono::{DateTime, Local, SubsecRound, Timelike}; use clap::Parser; use libpt::cli::args::HELP_TEMPLATE; use libpt::cli::clap::ArgGroup; From 4d12f268d9dce03fd125626ef6d7d3f917762137 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 26 Jul 2024 10:42:57 +0200 Subject: [PATCH 20/20] chore: bump version to v0.2.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fd21eb8..91389ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crock" -version = "0.2.1-alpha.1" +version = "0.2.2" edition = "2021" publish = true authors = ["Christoph J. Scherr "]