generated from PlexSheep/rs-base
implement countdown timer #16
This commit is contained in:
parent
d01683ff2d
commit
18a69a1c7a
3 changed files with 127 additions and 0 deletions
74
src/app.rs
74
src/app.rs
|
@ -1,4 +1,10 @@
|
||||||
|
use chrono::DateTime;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use chrono::Local;
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use chrono::NaiveTime;
|
||||||
|
use chrono::TimeZone;
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -8,6 +14,7 @@ use tui::style::Style;
|
||||||
use tui::Frame;
|
use tui::Frame;
|
||||||
|
|
||||||
use self::modes::Clock;
|
use self::modes::Clock;
|
||||||
|
use self::modes::Countdown;
|
||||||
use self::modes::DurationFormat;
|
use self::modes::DurationFormat;
|
||||||
use self::modes::Stopwatch;
|
use self::modes::Stopwatch;
|
||||||
use self::modes::Timer;
|
use self::modes::Timer;
|
||||||
|
@ -48,6 +55,24 @@ pub(crate) enum Mode {
|
||||||
},
|
},
|
||||||
/// The stopwatch mode displays the elapsed time since it was started.
|
/// The stopwatch mode displays the elapsed time since it was started.
|
||||||
Stopwatch,
|
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)]
|
#[derive(clap::Parser)]
|
||||||
|
@ -70,6 +95,8 @@ pub(crate) struct App {
|
||||||
timer: Option<Timer>,
|
timer: Option<Timer>,
|
||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
stopwatch: Option<Stopwatch>,
|
stopwatch: Option<Stopwatch>,
|
||||||
|
#[clap(skip)]
|
||||||
|
countdown: Option<Countdown>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for widgets that can be paused
|
/// Trait for widgets that can be paused
|
||||||
|
@ -134,6 +161,22 @@ impl App {
|
||||||
Mode::Stopwatch => {
|
Mode::Stopwatch => {
|
||||||
self.stopwatch = Some(Stopwatch::new(self.size, style));
|
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());
|
f.render_widget(w, f.size());
|
||||||
} else if let Some(ref w) = self.stopwatch {
|
} else if let Some(ref w) = self.stopwatch {
|
||||||
f.render_widget(w, f.size());
|
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());
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod clock;
|
mod clock;
|
||||||
|
mod countdown;
|
||||||
mod stopwatch;
|
mod stopwatch;
|
||||||
mod timer;
|
mod timer;
|
||||||
|
|
||||||
|
@ -7,6 +8,7 @@ use std::cmp::min;
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
pub(crate) use clock::Clock;
|
pub(crate) use clock::Clock;
|
||||||
use clock_tui::bricks_text::BricksText;
|
use clock_tui::bricks_text::BricksText;
|
||||||
|
pub(crate) use countdown::Countdown;
|
||||||
pub(crate) use stopwatch::Stopwatch;
|
pub(crate) use stopwatch::Stopwatch;
|
||||||
pub(crate) use timer::Timer;
|
pub(crate) use timer::Timer;
|
||||||
use tui::{
|
use tui::{
|
||||||
|
@ -26,12 +28,18 @@ pub(crate) enum DurationFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_duration(duration: Duration, format: DurationFormat) -> String {
|
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 millis = duration.num_milliseconds();
|
||||||
let seconds = millis / 1000;
|
let seconds = millis / 1000;
|
||||||
let minutes = seconds / 60;
|
let minutes = seconds / 60;
|
||||||
let hours = minutes / 60;
|
let hours = minutes / 60;
|
||||||
let days = hours / 24;
|
let days = hours / 24;
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
|
if is_neg {
|
||||||
|
result.push('-');
|
||||||
|
}
|
||||||
if days > 0 {
|
if days > 0 {
|
||||||
result.push_str(&format!("{}:", days));
|
result.push_str(&format!("{}:", days));
|
||||||
}
|
}
|
||||||
|
|
45
src/app/modes/countdown.rs
Normal file
45
src/app/modes/countdown.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue