diff --git a/demo/rocket-countdown.gif b/demo/rocket-countdown.gif new file mode 100644 index 0000000..f0b448d Binary files /dev/null and b/demo/rocket-countdown.gif differ diff --git a/demo/rocket-countdown.tape b/demo/rocket-countdown.tape new file mode 100644 index 0000000..e562a5c --- /dev/null +++ b/demo/rocket-countdown.tape @@ -0,0 +1,22 @@ +Output demo/rocket-countdown.gif + +# https://github.com/charmbracelet/vhs/blob/main/THEMES.md +Set Theme "AtomOneLight" + +Set FontSize 14 +Set Width 800 +Set Height 400 +Set Padding 0 +Set Margin 1 + +# --- START --- +Set LoopOffset 4 +Hide +Type "cargo run -- -m c -c 3" +Enter +Sleep 0.2 +Show +Type "s" +Sleep 6 +Type "r" +Sleep 1 diff --git a/justfile b/justfile index 15f7a1d..d08d9fa 100644 --- a/justfile +++ b/justfile @@ -68,3 +68,8 @@ alias dlt := demo-local-time demo-local-time: vhs demo/local-time.tape + +alias drc := demo-rocket-countdown + +demo-rocket-countdown: + vhs demo/rocket-countdown.tape diff --git a/src/app.rs b/src/app.rs index 7113711..bc693e3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -179,7 +179,7 @@ impl App { fn clock_is_running(&self) -> bool { match self.content { - Content::Countdown => self.countdown.get_clock().is_running(), + Content::Countdown => self.countdown.is_running(), Content::Timer => self.timer.get_clock().is_running(), Content::Pomodoro => self.pomodoro.get_clock().is_running(), } diff --git a/src/duration.rs b/src/duration.rs index 2dc388e..77a4821 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -86,6 +86,10 @@ impl DurationEx { let inner = self.inner.saturating_sub(ex.inner); Self { inner } } + + pub fn to_string_with_decis(self) -> String { + format!("{}.{}", self, self.decis()) + } } impl fmt::Display for DurationEx { diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index 827d498..759badb 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -207,10 +207,18 @@ impl ClockState { &self.mode } + pub fn run(&mut self) { + self.mode = Mode::Tick + } + pub fn is_running(&self) -> bool { self.mode == Mode::Tick } + pub fn is_initial(&self) -> bool { + self.mode == Mode::Initial + } + pub fn is_edit_mode(&self) -> bool { matches!(self.mode, Mode::Editable(_, _)) } @@ -294,6 +302,10 @@ impl ClockState { self.update_format(); } + pub fn is_done(&self) -> bool { + self.mode == Mode::Done + } + fn update_format(&mut self) { self.format = self.get_format(); } diff --git a/src/widgets/countdown.rs b/src/widgets/countdown.rs index f1e9b62..967b890 100644 --- a/src/widgets/countdown.rs +++ b/src/widgets/countdown.rs @@ -5,32 +5,50 @@ use ratatui::{ text::Line, widgets::{StatefulWidget, Widget}, }; -use std::cmp::max; +use std::{cmp::max, time::Duration}; use crate::{ common::Style, + constants::TICK_VALUE_MS, events::{Event, EventHandler}, utils::center, - widgets::clock::{self, ClockState, ClockWidget}, + widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget}, }; +/// State for Countdown Widget #[derive(Debug, Clone)] pub struct CountdownState { + /// clock to count down clock: ClockState, + /// clock to count up afterwards + timer: ClockState, } impl CountdownState { - pub const fn new(clock: ClockState) -> Self { - Self { clock } + pub fn new(clock: ClockState) -> Self { + Self { + clock, + timer: ClockState::::new(ClockStateArgs { + initial_value: Duration::ZERO, + current_value: Duration::ZERO, + tick_value: Duration::from_millis(TICK_VALUE_MS), + with_decis: false, + }), + } } pub fn set_with_decis(&mut self, with_decis: bool) { self.clock.with_decis = with_decis; + self.timer.with_decis = with_decis; } pub fn get_clock(&self) -> &ClockState { &self.clock } + + pub fn is_running(&self) -> bool { + self.clock.is_running() || self.timer.is_running() + } } impl EventHandler for CountdownState { @@ -38,20 +56,35 @@ impl EventHandler for CountdownState { let edit_mode = self.clock.is_edit_mode(); match event { Event::Tick => { - self.clock.tick(); - } - Event::Key(key) if key.code == KeyCode::Char('r') => { - self.clock.reset(); + if !self.clock.is_done() { + self.clock.tick(); + } else { + self.timer.tick(); + if self.timer.is_initial() { + self.timer.run(); + } + } } Event::Key(key) => match key.code { KeyCode::Char('r') => { + // reset both clocks self.clock.reset(); + self.timer.reset(); } KeyCode::Char('s') => { - self.clock.toggle_pause(); + // toggle pause status depending on who is running + if !self.clock.is_done() { + self.clock.toggle_pause(); + } else { + self.timer.toggle_pause(); + } } KeyCode::Char('e') => { self.clock.toggle_edit(); + // stop + reset timer entering `edit` mode + if self.timer.is_running() { + self.timer.toggle_pause(); + } } KeyCode::Left if edit_mode => { self.clock.edit_next(); @@ -61,9 +94,13 @@ impl EventHandler for CountdownState { } KeyCode::Up if edit_mode => { self.clock.edit_up(); + // whenever clock value is changed, reset timer + self.timer.reset(); } KeyCode::Down if edit_mode => { self.clock.edit_down(); + // whenever clock value is changed, reset timer + self.timer.reset(); } _ => return Some(event), }, @@ -81,7 +118,27 @@ impl StatefulWidget for Countdown { type State = CountdownState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let clock = ClockWidget::new(self.style); - let label = Line::raw((format!("Countdown {}", state.clock.get_mode())).to_uppercase()); + + let label = Line::raw( + if state.clock.is_done() { + if state.clock.with_decis { + format!( + "Countdown {} +{}", + state.clock.get_mode(), + state.timer.get_current_value().to_string_with_decis() + ) + } else { + format!( + "Countdown {} +{}", + state.clock.get_mode(), + state.timer.get_current_value() + ) + } + } else { + format!("Countdown {}", state.clock.get_mode()) + } + .to_uppercase(), + ); let area = center( area,