From 4f66ea86d402d1e0035378ccb785c46bb49f2103 Mon Sep 17 00:00:00 2001 From: "Jens K." <47693+sectore@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:54:47 +0100 Subject: [PATCH] Clock (#6) --- src/app.rs | 57 ++++++++++++++++++---- src/clock.rs | 100 +++++++++++++++++++++++++++++++++++++++ src/constants.rs | 2 + src/events.rs | 6 ++- src/main.rs | 2 + src/utils.rs | 13 ----- src/widgets/countdown.rs | 19 +++++--- src/widgets/header.rs | 18 ++++--- src/widgets/timer.rs | 20 +++++--- 9 files changed, 189 insertions(+), 48 deletions(-) create mode 100644 src/clock.rs create mode 100644 src/constants.rs diff --git a/src/app.rs b/src/app.rs index 2380ad0..7023f06 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,6 @@ use crate::{ + clock::{self, Clock}, + constants::TICK_VALUE_MS, events::{Event, Events}, terminal::Terminal, utils::center, @@ -32,7 +34,8 @@ pub struct App { content: Content, mode: Mode, show_menu: bool, - tick: u128, + clock_countdown: Clock, + clock_timer: Clock, } impl Default for App { @@ -41,7 +44,11 @@ impl Default for App { mode: Mode::Running, content: Content::Countdown, show_menu: false, - tick: 0, + clock_countdown: Clock::::new( + 10 * 60 * 1000, /* 10min in milliseconds */ + TICK_VALUE_MS, + ), + clock_timer: Clock::::new(0, TICK_VALUE_MS), } } } @@ -59,7 +66,7 @@ impl App { self.draw(&mut terminal)?; } Event::Tick => { - self.tick = self.tick.saturating_add(1); + self.tick(); } Event::Key(key) => self.handle_key_event(key), _ => {} @@ -77,9 +84,11 @@ impl App { match key.code { KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit, KeyCode::Char('c') => self.content = Content::Countdown, + KeyCode::Char('s') => self.toggle(), KeyCode::Char('t') => self.content = Content::Timer, KeyCode::Char('p') => self.content = Content::Pomodoro, KeyCode::Char('m') => self.show_menu = !self.show_menu, + KeyCode::Char('r') => self.reset(), _ => {} }; } @@ -93,13 +102,41 @@ impl App { fn render_content(&self, area: Rect, buf: &mut Buffer) { // center content - let area = center(area, Constraint::Length(50), Constraint::Length(1)); + let area = center(area, Constraint::Length(50), Constraint::Length(2)); match self.content { - Content::Timer => Timer::new(200, "Timer".into()).render(area, buf), - Content::Countdown => Countdown::new("Countdown".into()).render(area, buf), + Content::Timer => { + Timer::new("Timer".into(), self.clock_timer.clone()).render(area, buf) + } + Content::Countdown => { + Countdown::new("Countdown".into(), self.clock_countdown.clone()).render(area, buf) + } Content::Pomodoro => Pomodoro::new("Pomodoro".into()).render(area, buf), }; } + + fn reset(&mut self) { + match self.content { + Content::Timer => self.clock_timer.reset(), + Content::Countdown => self.clock_countdown.reset(), + _ => {} + }; + } + + fn toggle(&mut self) { + match self.content { + Content::Timer => self.clock_timer.toggle_pause(), + Content::Countdown => self.clock_countdown.toggle_pause(), + _ => {} + }; + } + + fn tick(&mut self) { + match self.content { + Content::Timer => self.clock_timer.tick(), + Content::Countdown => self.clock_countdown.tick(), + _ => {} + }; + } } impl Widget for &App { @@ -109,11 +146,11 @@ impl Widget for &App { Constraint::Fill(0), Constraint::Length(if self.show_menu { 2 } else { 1 }), ]); - let [header_area, content_area, footer_area] = vertical.areas(area); + let [v0, v1, v4] = vertical.areas(area); Block::new().render(area, buf); - Header::new(self.tick).render(header_area, buf); - self.render_content(content_area, buf); - Footer::new(self.show_menu, self.content).render(footer_area, buf); + Header::new(true).render(v0, buf); + self.render_content(v1, buf); + Footer::new(self.show_menu, self.content).render(v4, buf); } } diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 0000000..3cc2051 --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,100 @@ +use std::marker::PhantomData; + +use strum::Display; + +#[derive(Debug, Copy, Clone, Display, PartialEq, Eq)] +pub enum Mode { + Initial, + Tick, + Pause, + Done, +} + +#[derive(Debug, Clone, Copy)] +pub struct Clock { + initial_value: u64, + tick_value: u64, + current_value: u64, + mode: Mode, + phantom: PhantomData, +} + +impl Clock { + pub fn toggle_pause(&mut self) { + self.mode = if self.mode == Mode::Tick { + Mode::Pause + } else { + Mode::Tick + } + } + + pub fn reset(&mut self) { + self.mode = Mode::Initial; + self.current_value = self.initial_value; + } + + pub fn format(&mut self) -> String { + let ms = self.current_value; + + let minutes = (ms % 3600000) / 60000; + let seconds = (ms % 60000) / 1000; + let tenths = (ms % 1000) / 100; + + format!("{:02}:{:02}.{}", minutes, seconds, tenths) + } +} + +#[derive(Debug, Clone)] +pub struct Countdown {} + +#[derive(Debug, Clone)] +pub struct Timer {} + +impl Clock { + pub fn new(initial_value: u64, tick_value: u64) -> Self { + Self { + initial_value, + tick_value, + current_value: initial_value, + mode: Mode::Initial, + phantom: PhantomData, + } + } + + pub fn tick(&mut self) { + if self.mode == Mode::Tick { + self.current_value = self.current_value.saturating_sub(self.tick_value); + self.check_done(); + } + } + + fn check_done(&mut self) { + if self.current_value == 0 { + self.mode = Mode::Done; + } + } +} +impl Clock { + pub fn new(initial_value: u64, tick_value: u64) -> Self { + Self { + initial_value, + tick_value, + current_value: 0, + mode: Mode::Initial, + phantom: PhantomData, + } + } + + pub fn tick(&mut self) { + if self.mode == Mode::Tick { + self.current_value = self.current_value.saturating_add(self.tick_value); + self.check_done(); + } + } + + fn check_done(&mut self) { + if self.current_value == self.initial_value { + self.mode = Mode::Done; + } + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..91850fe --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,2 @@ +pub static TICK_VALUE_MS: u64 = 1000 / 10; // 0.1 sec in milliseconds +pub static FPS_VALUE_MS: u64 = 1000 / 60; // 60 FPS in milliseconds diff --git a/src/events.rs b/src/events.rs index 30d8135..7ac6ea6 100644 --- a/src/events.rs +++ b/src/events.rs @@ -4,6 +4,8 @@ use std::{pin::Pin, time::Duration}; use tokio::time::interval; use tokio_stream::{wrappers::IntervalStream, StreamMap}; +use crate::constants::{FPS_VALUE_MS, TICK_VALUE_MS}; + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] enum StreamKey { Ticks, @@ -48,12 +50,12 @@ impl Events { } fn tick_stream() -> Pin>> { - let tick_interval = interval(Duration::from_secs_f64(1.0 / 10.0)); + let tick_interval = interval(Duration::from_millis(TICK_VALUE_MS)); Box::pin(IntervalStream::new(tick_interval).map(|_| Event::Tick)) } fn render_stream() -> Pin>> { - let render_interval = interval(Duration::from_secs_f64(1.0 / 60.0)); // 60 FPS + let render_interval = interval(Duration::from_millis(FPS_VALUE_MS)); Box::pin(IntervalStream::new(render_interval).map(|_| Event::Render)) } diff --git a/src/main.rs b/src/main.rs index e2c4bb4..e34cc0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ mod app; +mod clock; +mod constants; mod events; mod terminal; mod utils; diff --git a/src/utils.rs b/src/utils.rs index 518c64d..8f6b8db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,16 +9,3 @@ pub fn center(base_area: Rect, horizontal: Constraint, vertical: Constraint) -> let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); area } - -pub fn format_ms(ms: u128, show_tenths: bool) -> String { - // let hours = ms / 3600000; - let minutes = (ms % 3600000) / 60000; - let seconds = (ms % 60000) / 1000; - let tenths = (ms % 1000) / 100; - - if show_tenths { - format!("{:02}:{:02}.{}", minutes, seconds, tenths) - } else { - format!("{:02}:{:02}", minutes, seconds) - } -} diff --git a/src/widgets/countdown.rs b/src/widgets/countdown.rs index 2b0a165..8f7fba4 100644 --- a/src/widgets/countdown.rs +++ b/src/widgets/countdown.rs @@ -1,23 +1,30 @@ use ratatui::{ buffer::Buffer, - layout::Rect, + layout::{Constraint, Layout, Rect}, widgets::{Paragraph, Widget}, }; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +use crate::clock::{self, Clock}; + +#[derive(Debug)] pub struct Countdown { headline: String, + clock: Clock, } impl Countdown { - pub const fn new(headline: String) -> Self { - Self { headline } + pub const fn new(headline: String, clock: Clock) -> Self { + Self { headline, clock } } } impl Widget for Countdown { - fn render(self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let h = Paragraph::new(self.headline).centered(); - h.render(area, buf); + let c = Paragraph::new(self.clock.format()).centered(); + let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); + + h.render(v1, buf); + c.render(v2, buf) } } diff --git a/src/widgets/header.rs b/src/widgets/header.rs index c03fd6d..d82802c 100644 --- a/src/widgets/header.rs +++ b/src/widgets/header.rs @@ -5,28 +5,26 @@ use ratatui::{ widgets::Widget, }; -use crate::utils::format_ms; - #[derive(Debug, Clone)] pub struct Header { - tick: u128, + show_fps: bool, } impl Header { - pub fn new(tick: u128) -> Self { - Self { tick } + pub fn new(show_fps: bool) -> Self { + Self { show_fps } } } impl Widget for Header { fn render(self, area: Rect, buf: &mut Buffer) { - let time_string = format_ms(self.tick * 100, true); - let tick_span = Span::raw(time_string); - let tick_width = tick_span.width().try_into().unwrap_or(0); + let fps_txt = if self.show_fps { "FPS (soon)" } else { "" }; + let fps_span = Span::raw(fps_txt); + let fps_width = fps_span.width().try_into().unwrap_or(0); let [h1, h2] = - Layout::horizontal([Constraint::Fill(1), Constraint::Length(tick_width)]).areas(area); + Layout::horizontal([Constraint::Fill(1), Constraint::Length(fps_width)]).areas(area); Span::raw("tim:r").render(h1, buf); - tick_span.render(h2, buf); + fps_span.render(h2, buf); } } diff --git a/src/widgets/timer.rs b/src/widgets/timer.rs index ccd7f64..037046c 100644 --- a/src/widgets/timer.rs +++ b/src/widgets/timer.rs @@ -1,24 +1,30 @@ use ratatui::{ buffer::Buffer, - layout::Rect, + layout::{Constraint, Layout, Rect}, widgets::{Paragraph, Widget}, }; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +use crate::clock::{self, Clock}; + +#[derive(Debug)] pub struct Timer { - value: u64, headline: String, + clock: Clock, } impl Timer { - pub const fn new(value: u64, headline: String) -> Self { - Self { value, headline } + pub const fn new(headline: String, clock: Clock) -> Self { + Self { headline, clock } } } impl Widget for Timer { - fn render(self, area: Rect, buf: &mut Buffer) { + fn render(mut self, area: Rect, buf: &mut Buffer) { let h = Paragraph::new(self.headline).centered(); - h.render(area, buf); + let c = Paragraph::new(self.clock.format()).centered(); + let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); + + h.render(v1, buf); + c.render(v2, buf) } }