Clock<T> (#6)
This commit is contained in:
parent
2f587c97b5
commit
4f66ea86d4
57
src/app.rs
57
src/app.rs
@ -1,4 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
clock::{self, Clock},
|
||||||
|
constants::TICK_VALUE_MS,
|
||||||
events::{Event, Events},
|
events::{Event, Events},
|
||||||
terminal::Terminal,
|
terminal::Terminal,
|
||||||
utils::center,
|
utils::center,
|
||||||
@ -32,7 +34,8 @@ pub struct App {
|
|||||||
content: Content,
|
content: Content,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
show_menu: bool,
|
show_menu: bool,
|
||||||
tick: u128,
|
clock_countdown: Clock<clock::Countdown>,
|
||||||
|
clock_timer: Clock<clock::Timer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for App {
|
impl Default for App {
|
||||||
@ -41,7 +44,11 @@ impl Default for App {
|
|||||||
mode: Mode::Running,
|
mode: Mode::Running,
|
||||||
content: Content::Countdown,
|
content: Content::Countdown,
|
||||||
show_menu: false,
|
show_menu: false,
|
||||||
tick: 0,
|
clock_countdown: Clock::<clock::Countdown>::new(
|
||||||
|
10 * 60 * 1000, /* 10min in milliseconds */
|
||||||
|
TICK_VALUE_MS,
|
||||||
|
),
|
||||||
|
clock_timer: Clock::<clock::Timer>::new(0, TICK_VALUE_MS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +66,7 @@ impl App {
|
|||||||
self.draw(&mut terminal)?;
|
self.draw(&mut terminal)?;
|
||||||
}
|
}
|
||||||
Event::Tick => {
|
Event::Tick => {
|
||||||
self.tick = self.tick.saturating_add(1);
|
self.tick();
|
||||||
}
|
}
|
||||||
Event::Key(key) => self.handle_key_event(key),
|
Event::Key(key) => self.handle_key_event(key),
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -77,9 +84,11 @@ impl App {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
|
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
|
||||||
KeyCode::Char('c') => self.content = Content::Countdown,
|
KeyCode::Char('c') => self.content = Content::Countdown,
|
||||||
|
KeyCode::Char('s') => self.toggle(),
|
||||||
KeyCode::Char('t') => self.content = Content::Timer,
|
KeyCode::Char('t') => self.content = Content::Timer,
|
||||||
KeyCode::Char('p') => self.content = Content::Pomodoro,
|
KeyCode::Char('p') => self.content = Content::Pomodoro,
|
||||||
KeyCode::Char('m') => self.show_menu = !self.show_menu,
|
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) {
|
fn render_content(&self, area: Rect, buf: &mut Buffer) {
|
||||||
// center content
|
// 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 {
|
match self.content {
|
||||||
Content::Timer => Timer::new(200, "Timer".into()).render(area, buf),
|
Content::Timer => {
|
||||||
Content::Countdown => Countdown::new("Countdown".into()).render(area, buf),
|
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),
|
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 {
|
impl Widget for &App {
|
||||||
@ -109,11 +146,11 @@ impl Widget for &App {
|
|||||||
Constraint::Fill(0),
|
Constraint::Fill(0),
|
||||||
Constraint::Length(if self.show_menu { 2 } else { 1 }),
|
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);
|
Block::new().render(area, buf);
|
||||||
Header::new(self.tick).render(header_area, buf);
|
Header::new(true).render(v0, buf);
|
||||||
self.render_content(content_area, buf);
|
self.render_content(v1, buf);
|
||||||
Footer::new(self.show_menu, self.content).render(footer_area, buf);
|
Footer::new(self.show_menu, self.content).render(v4, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
100
src/clock.rs
Normal file
100
src/clock.rs
Normal file
@ -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<T> {
|
||||||
|
initial_value: u64,
|
||||||
|
tick_value: u64,
|
||||||
|
current_value: u64,
|
||||||
|
mode: Mode,
|
||||||
|
phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clock<T> {
|
||||||
|
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<Countdown> {
|
||||||
|
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<Timer> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/constants.rs
Normal file
2
src/constants.rs
Normal file
@ -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
|
||||||
@ -4,6 +4,8 @@ use std::{pin::Pin, time::Duration};
|
|||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
use tokio_stream::{wrappers::IntervalStream, StreamMap};
|
use tokio_stream::{wrappers::IntervalStream, StreamMap};
|
||||||
|
|
||||||
|
use crate::constants::{FPS_VALUE_MS, TICK_VALUE_MS};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
enum StreamKey {
|
enum StreamKey {
|
||||||
Ticks,
|
Ticks,
|
||||||
@ -48,12 +50,12 @@ impl Events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn tick_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
|
fn tick_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
|
||||||
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))
|
Box::pin(IntervalStream::new(tick_interval).map(|_| Event::Tick))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
|
fn render_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
|
||||||
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))
|
Box::pin(IntervalStream::new(render_interval).map(|_| Event::Render))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
mod app;
|
mod app;
|
||||||
|
mod clock;
|
||||||
|
mod constants;
|
||||||
mod events;
|
mod events;
|
||||||
mod terminal;
|
mod terminal;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|||||||
13
src/utils.rs
13
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);
|
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,23 +1,30 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::{Constraint, Layout, Rect},
|
||||||
widgets::{Paragraph, Widget},
|
widgets::{Paragraph, Widget},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
use crate::clock::{self, Clock};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Countdown {
|
pub struct Countdown {
|
||||||
headline: String,
|
headline: String,
|
||||||
|
clock: Clock<clock::Countdown>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Countdown {
|
impl Countdown {
|
||||||
pub const fn new(headline: String) -> Self {
|
pub const fn new(headline: String, clock: Clock<clock::Countdown>) -> Self {
|
||||||
Self { headline }
|
Self { headline, clock }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Countdown {
|
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();
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,28 +5,26 @@ use ratatui::{
|
|||||||
widgets::Widget,
|
widgets::Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::utils::format_ms;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
tick: u128,
|
show_fps: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
impl Header {
|
||||||
pub fn new(tick: u128) -> Self {
|
pub fn new(show_fps: bool) -> Self {
|
||||||
Self { tick }
|
Self { show_fps }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Header {
|
impl Widget for Header {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let time_string = format_ms(self.tick * 100, true);
|
let fps_txt = if self.show_fps { "FPS (soon)" } else { "" };
|
||||||
let tick_span = Span::raw(time_string);
|
let fps_span = Span::raw(fps_txt);
|
||||||
let tick_width = tick_span.width().try_into().unwrap_or(0);
|
let fps_width = fps_span.width().try_into().unwrap_or(0);
|
||||||
let [h1, h2] =
|
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);
|
Span::raw("tim:r").render(h1, buf);
|
||||||
tick_span.render(h2, buf);
|
fps_span.render(h2, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,30 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::{Constraint, Layout, Rect},
|
||||||
widgets::{Paragraph, Widget},
|
widgets::{Paragraph, Widget},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
use crate::clock::{self, Clock};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Timer {
|
pub struct Timer {
|
||||||
value: u64,
|
|
||||||
headline: String,
|
headline: String,
|
||||||
|
clock: Clock<clock::Timer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timer {
|
impl Timer {
|
||||||
pub const fn new(value: u64, headline: String) -> Self {
|
pub const fn new(headline: String, clock: Clock<clock::Timer>) -> Self {
|
||||||
Self { value, headline }
|
Self { headline, clock }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Timer {
|
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();
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user