diff --git a/src/app.rs b/src/app.rs index 100c16b..75eb34e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,10 @@ +use chrono::DateTime; use chrono::Duration; +use chrono::Local; +use chrono::NaiveDate; +use chrono::NaiveDateTime; +use chrono::NaiveTime; +use chrono::TimeZone; use clap::Subcommand; use crossterm::event::KeyCode; use regex::Regex; @@ -8,6 +14,7 @@ use tui::style::Style; use tui::Frame; use self::modes::Clock; +use self::modes::Countdown; use self::modes::DurationFormat; use self::modes::Stopwatch; use self::modes::Timer; @@ -48,6 +55,24 @@ pub(crate) enum Mode { }, /// The stopwatch mode displays the elapsed time since it was started. Stopwatch, + /// The countdown timer mode shows the duration to a specific time + Countdown { + /// The target time to countdown to, eg. "2023-01-01", or "20:00" + #[clap(long, short, value_parser = parse_datetime)] + time: DateTime, + + /// Title or description for countdown show in header + #[clap(long, short = 'T')] + title: Option, + + /// Continue to countdown after pass the target time + #[clap(long = "continue", short = 'c', takes_value = false)] + continue_on_zero: bool, + + /// Reverse the countdown, a.k.a. countup + #[clap(long, short, takes_value = false)] + reverse: bool, + }, } #[derive(clap::Parser)] @@ -70,6 +95,8 @@ pub(crate) struct App { timer: Option, #[clap(skip)] stopwatch: Option, + #[clap(skip)] + countdown: Option, } /// Trait for widgets that can be paused @@ -134,6 +161,22 @@ impl App { Mode::Stopwatch => { self.stopwatch = Some(Stopwatch::new(self.size, style)); } + Mode::Countdown { + time, + title, + continue_on_zero, + reverse, + } => { + self.countdown = Some(Countdown { + size: self.size, + style, + time: *time, + title: title.to_owned(), + continue_on_zero: *continue_on_zero, + reverse: *reverse, + format: DurationFormat::HourMinSec, + }) + } } } @@ -144,6 +187,8 @@ impl App { f.render_widget(w, f.size()); } else if let Some(ref w) = self.stopwatch { f.render_widget(w, f.size()); + } else if let Some(ref w) = self.countdown { + f.render_widget(w, f.size()); } } @@ -213,3 +258,32 @@ fn parse_color(s: &str) -> Result { } } } + +fn parse_datetime(s: &str) -> Result, String> { + let s = s.trim(); + let today = Local::today(); + + let time = NaiveTime::parse_from_str(s, "%H:%M:%S"); + if time.is_ok() { + let time = NaiveDateTime::new(today.naive_local(), time.unwrap()); + return Ok(Local.from_local_datetime(&time).unwrap()); + } + + let date = NaiveDate::parse_from_str(s, "%Y-%m-%d"); + if date.is_ok() { + let time = NaiveDateTime::new(date.unwrap(), NaiveTime::from_hms(0, 0, 0)); + return Ok(Local.from_local_datetime(&time).unwrap()); + } + + let date_time = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S"); + if date_time.is_ok() { + return Ok(Local.from_local_datetime(&date_time.unwrap()).unwrap()); + } + + let rfc_time = DateTime::parse_from_rfc3339(s); + if rfc_time.is_ok() { + return Ok(rfc_time.unwrap().with_timezone(&Local)); + } + + return Err("Invalid time format".to_string()); +} diff --git a/src/app/modes.rs b/src/app/modes.rs index 843ad9f..66ac6ba 100644 --- a/src/app/modes.rs +++ b/src/app/modes.rs @@ -1,4 +1,5 @@ mod clock; +mod countdown; mod stopwatch; mod timer; @@ -7,6 +8,7 @@ use std::cmp::min; use chrono::Duration; pub(crate) use clock::Clock; use clock_tui::bricks_text::BricksText; +pub(crate) use countdown::Countdown; pub(crate) use stopwatch::Stopwatch; pub(crate) use timer::Timer; use tui::{ @@ -26,12 +28,18 @@ pub(crate) enum DurationFormat { } fn format_duration(duration: Duration, format: DurationFormat) -> String { + let is_neg = duration < Duration::zero(); + let duration = if is_neg { -duration } else { duration }; + let millis = duration.num_milliseconds(); let seconds = millis / 1000; let minutes = seconds / 60; let hours = minutes / 60; let days = hours / 24; let mut result = String::new(); + if is_neg { + result.push('-'); + } if days > 0 { result.push_str(&format!("{}:", days)); } diff --git a/src/app/modes/countdown.rs b/src/app/modes/countdown.rs new file mode 100644 index 0000000..9fce3e7 --- /dev/null +++ b/src/app/modes/countdown.rs @@ -0,0 +1,45 @@ +use chrono::{DateTime, Duration, Local}; +use clock_tui::bricks_text::BricksText; +use tui::{style::Style, widgets::Widget}; + +use super::{format_duration, render_centered, DurationFormat}; + +pub struct Countdown { + pub size: u16, + pub style: Style, + pub time: DateTime, + pub title: Option, + pub continue_on_zero: bool, + pub(crate) reverse: bool, + pub(crate) format: DurationFormat, +} + +impl Countdown { + pub(crate) fn remaining_time(&self) -> Duration { + let now = Local::now(); + let result = self.time.signed_duration_since(now); + if self.reverse { + -result + } else { + result + } + } +} + +impl Widget for &Countdown { + fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) { + let remaining_time = self.remaining_time(); + let time_str = if remaining_time < Duration::zero() && !self.continue_on_zero { + if (remaining_time.num_milliseconds()).abs() % 1000 < 500 { + return; + } else { + format_duration(Duration::zero(), self.format) + } + } else { + format_duration(remaining_time, self.format) + }; + + let text = BricksText::new(time_str.as_str(), self.size, self.size, self.style); + render_centered(area, buf, &text, self.title.to_owned(), None); + } +}