implement countdown timer #16

This commit is contained in:
race604 2022-08-29 10:29:41 +08:00
parent d01683ff2d
commit 18a69a1c7a
3 changed files with 127 additions and 0 deletions

View file

@ -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<Local>,
/// Title or description for countdown show in header
#[clap(long, short = 'T')]
title: Option<String>,
/// 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<Timer>,
#[clap(skip)]
stopwatch: Option<Stopwatch>,
#[clap(skip)]
countdown: Option<Countdown>,
}
/// 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<Color, String> {
}
}
}
fn parse_datetime(s: &str) -> Result<DateTime<Local>, 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());
}

View file

@ -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));
}

View file

@ -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<Local>,
pub title: Option<String>,
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);
}
}