7 Commits

Author SHA1 Message Date
Jens Krause
4ee5d7b4e9 Prepare v1.0.0 (#47)
* update README
* update CHANGELOG
* update demo
* bump v1.0.0
2025-01-10 16:36:57 +01:00
Jens Krause
9ea9f88266 feat(countdown): persist elapsed time (#46) 2025-01-10 16:01:03 +01:00
Jens Krause
c8af76c9e5 feat(countdown): rocket countdown (#45) 2025-01-08 18:52:18 +01:00
Jens Krause
468b4a5abf simplify style settings, improve naming (#44)
* simplify style settings by passing `style` directly to Widgets. No need to store it in `state` of widgets.
* remove unneeded things
* naming (state vs. widgets)
2025-01-07 19:02:57 +01:00
Jens Krause
8603a823e4 fix(footer): 12-hour format incl. tests (#43) 2025-01-06 19:22:02 +01:00
Jens Krause
94bdeeab11 feat(footer): show local time (#42) 2025-01-06 18:31:22 +01:00
Orhun Parmaksız
66c6d7fc46 docs: Add instructions for installing from the AUR (#41) 2025-01-06 09:06:15 +01:00
20 changed files with 615 additions and 196 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # Changelog
## v1.0.0 - 2025-01-10
Happy `v1.0.0` 🎉
### Features
- (countdown) Mission Elapsed Time ([MET](https://en.wikipedia.org/wiki/Mission_Elapsed_Time)). [#45](https://github.com/sectore/timr-tui/pull/45), [#46](https://github.com/sectore/timr-tui/pull/46)
- (footer) Local time. Optional and with custom formats. [#42](https://github.com/sectore/timr-tui/pull/42), [#43](https://github.com/sectore/timr-tui/pull/43)
- (docs) More installation instructions: Cargo, AUR (Arch Linux) [#41](https://github.com/sectore/timr-tui/pull/41), pre-built release binaries (Linux, macOS, Windows) [#47](https://github.com/sectore/timr-tui/pull/47)
## v0.9.0 - 2025-01-03 ## v0.9.0 - 2025-01-03
Initial version. Initial version.
@@ -8,5 +18,6 @@ Initial version.
- Add `Pomodoro`, `Timer`, `Countdown` - Add `Pomodoro`, `Timer`, `Countdown`
- Persist application state - Persist application state
- Change styles - Custom styles for digits
- Toggle deciseconds
- CLI - CLI

66
Cargo.lock generated
View File

@@ -296,6 +296,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@@ -649,6 +658,21 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@@ -723,6 +747,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "1.4.1" version = "1.4.1"
@@ -1050,9 +1080,42 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "timr-tui" name = "timr-tui"
version = "0.9.0" version = "1.0.0"
dependencies = [ dependencies = [
"clap", "clap",
"color-eyre", "color-eyre",
@@ -1063,6 +1126,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"strum", "strum",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "timr-tui" name = "timr-tui"
version = "0.9.0" version = "1.0.0"
description = "TUI to organize your time: Pomodoro, Countdown, Timer." description = "TUI to organize your time: Pomodoro, Countdown, Timer."
edition = "2021" edition = "2021"
rust-version = "1.82.0" rust-version = "1.82.0"
@@ -26,3 +26,4 @@ tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
directories = "5.0.1" directories = "5.0.1"
clap = { version = "4.5.23", features = ["derive"] } clap = { version = "4.5.23", features = ["derive"] }
time = { version = "0.3.37", features = ["formatting", "local-offset"] }

View File

@@ -6,9 +6,9 @@ TUI to organize your time: Pomodoro, Countdown, Timer.
- `[c]ountdown` Use it for your workout, yoga session, meditation, handstand or whatever. - `[c]ountdown` Use it for your workout, yoga session, meditation, handstand or whatever.
- `[p]omodoro` Organize your working time to be focused all the time by following the [Pomodoro Technique](https://en.wikipedia.org/wiki/Pomodoro_Technique). - `[p]omodoro` Organize your working time to be focused all the time by following the [Pomodoro Technique](https://en.wikipedia.org/wiki/Pomodoro_Technique).
It's built with [`Ratatui`](https://ratatui.rs/) written in [Rust 🦀](https://www.rust-lang.org/). Built with [Ratatui](https://ratatui.rs/) / [Rust 🦀](https://www.rust-lang.org/).
# Preview # Features
_Side note:_ Theme colors depend on your terminal preferences. _Side note:_ Theme colors depend on your terminal preferences.
@@ -48,6 +48,18 @@ _Side note:_ Theme colors depend on your terminal preferences.
<img alt="menu" src="demo/menu.gif" /> <img alt="menu" src="demo/menu.gif" />
</a> </a>
## Local time
<a href="demo/local-time.gif">
<img alt="menu" src="demo/local-time.gif" />
</a>
## Mission Elapsed Time ([MET](https://en.wikipedia.org/wiki/Mission_Elapsed_Time))
<a href="demo/countdown-met.gif">
<img alt="menu" src="demo/countdown-met.gif" />
</a>
# CLI # CLI
```sh ```sh
@@ -70,19 +82,35 @@ Options:
# Installation # Installation
From [crates.io](https://crates.io/crates/timr-tui) run: ## Cargo
### From [crates.io](https://crates.io/crates/timr-tui)
```sh ```sh
cargo install timr-tui cargo install timr-tui
``` ```
Latest version from git repository: ### From GitHub repository
```sh ```sh
cargo install --git https://github.com/sectore/timr-tui cargo install --git https://github.com/sectore/timr-tui
``` ```
# Build from source 🔧 ## Arch Linux
Install [from the AUR](https://aur.archlinux.org/packages/timr/):
```sh
paru -S timr
```
## Release binaries
Pre-built artifacts are available to download from [latest GitHub release](https://github.com/sectore/timr-tui/releases).
# Development
## Requirements ## Requirements
@@ -152,7 +180,7 @@ In `debug` mode only. Locations:
```sh ```sh
# Linux # Linux
~/.local/state/timr/logs/app.log ~/.local/state/timr-tui/logs/app.log
# macOS # macOS
/Users/{user}/Library/Application Support/timr-tui/logs/app.log /Users/{user}/Library/Application Support/timr-tui/logs/app.log
# `Windows` # `Windows`

BIN
demo/countdown-met.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

22
demo/countdown-met.tape Normal file
View File

@@ -0,0 +1,22 @@
Output demo/countdown-met.gif
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
Set Theme "iceberg-light"
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

BIN
demo/local-time.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

22
demo/local-time.tape Normal file
View File

@@ -0,0 +1,22 @@
Output demo/local-time.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"
Enter
Sleep 0.2
Show
Sleep 1
# --- toggle local time ---
Type@1.5s ":::"
Sleep 1.5

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -49,6 +49,11 @@ alias dc := demo-countdown
demo-countdown: demo-countdown:
vhs demo/countdown.tape vhs demo/countdown.tape
alias dcm := demo-countdown-met
demo-countdown-met:
vhs demo/countdown-met.tape
alias ds := demo-style alias ds := demo-style
demo-style: demo-style:
@@ -63,3 +68,13 @@ alias dm := demo-menu
demo-menu: demo-menu:
vhs demo/menu.tape vhs demo/menu.tape
alias dlt := demo-local-time
demo-local-time:
vhs demo/local-time.tape
alias drc := demo-rocket-countdown
demo-rocket-countdown:
vhs demo/met.tape

View File

@@ -1,17 +1,17 @@
use crate::{ use crate::{
args::Args, args::Args,
common::{Content, Style}, common::{AppTime, AppTimeFormat, Content, Style},
constants::TICK_VALUE_MS, constants::TICK_VALUE_MS,
events::{Event, EventHandler, Events}, events::{Event, EventHandler, Events},
storage::AppStorage, storage::AppStorage,
terminal::Terminal, terminal::Terminal,
widgets::{ widgets::{
clock::{self, Clock, ClockArgs}, clock::{self, ClockState, ClockStateArgs},
countdown::{Countdown, CountdownWidget}, countdown::{Countdown, CountdownState},
footer::Footer, footer::{Footer, FooterState},
header::Header, header::Header,
pomodoro::{Mode as PomodoroMode, Pomodoro, PomodoroArgs, PomodoroWidget}, pomodoro::{Mode as PomodoroMode, PomodoroState, PomodoroStateArgs, PomodoroWidget},
timer::{Timer, TimerWidget}, timer::{Timer, TimerState},
}, },
}; };
use color_eyre::Result; use color_eyre::Result;
@@ -22,6 +22,7 @@ use ratatui::{
widgets::{StatefulWidget, Widget}, widgets::{StatefulWidget, Widget},
}; };
use std::time::Duration; use std::time::Duration;
use time::OffsetDateTime;
use tracing::debug; use tracing::debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -34,18 +35,20 @@ enum Mode {
pub struct App { pub struct App {
content: Content, content: Content,
mode: Mode, mode: Mode,
show_menu: bool, app_time: AppTime,
countdown: Countdown, countdown: CountdownState,
timer: Timer, timer: TimerState,
pomodoro: Pomodoro, pomodoro: PomodoroState,
style: Style, style: Style,
with_decis: bool, with_decis: bool,
footer: FooterState,
} }
pub struct AppArgs { pub struct AppArgs {
pub style: Style, pub style: Style,
pub with_decis: bool, pub with_decis: bool,
pub show_menu: bool, pub show_menu: bool,
pub app_time_format: AppTimeFormat,
pub content: Content, pub content: Content,
pub pomodoro_mode: PomodoroMode, pub pomodoro_mode: PomodoroMode,
pub initial_value_work: Duration, pub initial_value_work: Duration,
@@ -54,6 +57,7 @@ pub struct AppArgs {
pub current_value_pause: Duration, pub current_value_pause: Duration,
pub initial_value_countdown: Duration, pub initial_value_countdown: Duration,
pub current_value_countdown: Duration, pub current_value_countdown: Duration,
pub elapsed_value_countdown: Duration,
pub current_value_timer: Duration, pub current_value_timer: Duration,
} }
@@ -64,6 +68,7 @@ impl From<(Args, AppStorage)> for AppArgs {
AppArgs { AppArgs {
with_decis: args.decis || stg.with_decis, with_decis: args.decis || stg.with_decis,
show_menu: args.menu || stg.show_menu, show_menu: args.menu || stg.show_menu,
app_time_format: stg.app_time_format,
content: args.mode.unwrap_or(stg.content), content: args.mode.unwrap_or(stg.content),
style: args.style.unwrap_or(stg.style), style: args.style.unwrap_or(stg.style),
pomodoro_mode: stg.pomodoro_mode, pomodoro_mode: stg.pomodoro_mode,
@@ -76,22 +81,32 @@ impl From<(Args, AppStorage)> for AppArgs {
initial_value_countdown: args.countdown.unwrap_or(stg.inital_value_countdown), initial_value_countdown: args.countdown.unwrap_or(stg.inital_value_countdown),
// invalidate `current_value_countdown` if an initial value is set via args // invalidate `current_value_countdown` if an initial value is set via args
current_value_countdown: args.countdown.unwrap_or(stg.current_value_countdown), current_value_countdown: args.countdown.unwrap_or(stg.current_value_countdown),
elapsed_value_countdown: stg.elapsed_value_countdown,
current_value_timer: stg.current_value_timer, current_value_timer: stg.current_value_timer,
} }
} }
} }
fn get_app_time() -> AppTime {
match OffsetDateTime::now_local() {
Ok(t) => AppTime::Local(t),
Err(_) => AppTime::Utc(OffsetDateTime::now_utc()),
}
}
impl App { impl App {
pub fn new(args: AppArgs) -> Self { pub fn new(args: AppArgs) -> Self {
let AppArgs { let AppArgs {
style, style,
show_menu, show_menu,
app_time_format,
initial_value_work, initial_value_work,
initial_value_pause, initial_value_pause,
initial_value_countdown, initial_value_countdown,
current_value_work, current_value_work,
current_value_pause, current_value_pause,
current_value_countdown, current_value_countdown,
elapsed_value_countdown,
current_value_timer, current_value_timer,
content, content,
with_decis, with_decis,
@@ -100,38 +115,43 @@ impl App {
Self { Self {
mode: Mode::Running, mode: Mode::Running,
content, content,
show_menu, app_time: get_app_time(),
style, style,
with_decis, with_decis,
countdown: Countdown::new(Clock::<clock::Countdown>::new(ClockArgs { countdown: CountdownState::new(
initial_value: initial_value_countdown, ClockState::<clock::Countdown>::new(ClockStateArgs {
current_value: current_value_countdown, initial_value: initial_value_countdown,
tick_value: Duration::from_millis(TICK_VALUE_MS), current_value: current_value_countdown,
style, tick_value: Duration::from_millis(TICK_VALUE_MS),
with_decis, with_decis,
})), }),
timer: Timer::new(Clock::<clock::Timer>::new(ClockArgs { elapsed_value_countdown,
),
timer: TimerState::new(ClockState::<clock::Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: current_value_timer, current_value: current_value_timer,
tick_value: Duration::from_millis(TICK_VALUE_MS), tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis, with_decis,
})), })),
pomodoro: Pomodoro::new(PomodoroArgs { pomodoro: PomodoroState::new(PomodoroStateArgs {
mode: pomodoro_mode, mode: pomodoro_mode,
initial_value_work, initial_value_work,
current_value_work, current_value_work,
initial_value_pause, initial_value_pause,
current_value_pause, current_value_pause,
style,
with_decis, with_decis,
}), }),
footer: FooterState::new(show_menu, app_time_format),
} }
} }
pub async fn run(mut self, mut terminal: Terminal, mut events: Events) -> Result<Self> { pub async fn run(mut self, mut terminal: Terminal, mut events: Events) -> Result<Self> {
while self.is_running() { while self.is_running() {
if let Some(event) = events.next().await { if let Some(event) = events.next().await {
if matches!(event, Event::Tick) {
self.app_time = get_app_time();
}
// Pipe events into subviews and handle only 'unhandled' events afterwards // Pipe events into subviews and handle only 'unhandled' events afterwards
if let Some(unhandled) = match self.content { if let Some(unhandled) = match self.content {
Content::Countdown => self.countdown.update(event.clone()), Content::Countdown => self.countdown.update(event.clone()),
@@ -165,7 +185,7 @@ impl App {
fn clock_is_running(&self) -> bool { fn clock_is_running(&self) -> bool {
match self.content { 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::Timer => self.timer.get_clock().is_running(),
Content::Pomodoro => self.pomodoro.get_clock().is_running(), Content::Pomodoro => self.pomodoro.get_clock().is_running(),
} }
@@ -186,13 +206,12 @@ impl App {
KeyCode::Char('c') => self.content = Content::Countdown, KeyCode::Char('c') => self.content = Content::Countdown,
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, // toogle app time format
KeyCode::Char(':') => self.footer.toggle_app_time_format(),
// toogle menu
KeyCode::Char('m') => self.footer.set_show_menu(!self.footer.get_show_menu()),
KeyCode::Char(',') => { KeyCode::Char(',') => {
self.style = self.style.next(); self.style = self.style.next();
// update clocks
self.timer.set_style(self.style);
self.countdown.set_style(self.style);
self.pomodoro.set_style(self.style);
} }
KeyCode::Char('.') => { KeyCode::Char('.') => {
self.with_decis = !self.with_decis; self.with_decis = !self.with_decis;
@@ -201,8 +220,8 @@ impl App {
self.countdown.set_with_decis(self.with_decis); self.countdown.set_with_decis(self.with_decis);
self.pomodoro.set_with_decis(self.with_decis); self.pomodoro.set_with_decis(self.with_decis);
} }
KeyCode::Up => self.show_menu = true, KeyCode::Up => self.footer.set_show_menu(true),
KeyCode::Down => self.show_menu = false, KeyCode::Down => self.footer.set_show_menu(false),
_ => {} _ => {}
}; };
} }
@@ -217,7 +236,8 @@ impl App {
pub fn to_storage(&self) -> AppStorage { pub fn to_storage(&self) -> AppStorage {
AppStorage { AppStorage {
content: self.content, content: self.content,
show_menu: self.show_menu, show_menu: self.footer.get_show_menu(),
app_time_format: *self.footer.app_time_format(),
style: self.style, style: self.style,
with_decis: self.with_decis, with_decis: self.with_decis,
pomodoro_mode: self.pomodoro.get_mode().clone(), pomodoro_mode: self.pomodoro.get_mode().clone(),
@@ -233,6 +253,7 @@ impl App {
current_value_countdown: Duration::from( current_value_countdown: Duration::from(
*self.countdown.get_clock().get_current_value(), *self.countdown.get_clock().get_current_value(),
), ),
elapsed_value_countdown: Duration::from(*self.countdown.get_elapsed_value()),
current_value_timer: Duration::from(*self.timer.get_clock().get_current_value()), current_value_timer: Duration::from(*self.timer.get_clock().get_current_value()),
} }
} }
@@ -243,9 +264,15 @@ struct AppWidget;
impl AppWidget { impl AppWidget {
fn render_content(&self, area: Rect, buf: &mut Buffer, state: &mut App) { fn render_content(&self, area: Rect, buf: &mut Buffer, state: &mut App) {
match state.content { match state.content {
Content::Timer => TimerWidget.render(area, buf, &mut state.timer.clone()), Content::Timer => {
Content::Countdown => CountdownWidget.render(area, buf, &mut state.countdown.clone()), Timer { style: state.style }.render(area, buf, &mut state.timer);
Content::Pomodoro => PomodoroWidget.render(area, buf, &mut state.pomodoro.clone()), }
Content::Countdown => {
Countdown { style: state.style }.render(area, buf, &mut state.countdown)
}
Content::Pomodoro => {
PomodoroWidget { style: state.style }.render(area, buf, &mut state.pomodoro)
}
}; };
} }
} }
@@ -256,7 +283,7 @@ impl StatefulWidget for AppWidget {
let [v0, v1, v2] = Layout::vertical([ let [v0, v1, v2] = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
Constraint::Percentage(100), Constraint::Percentage(100),
Constraint::Length(if state.show_menu { 4 } else { 1 }), Constraint::Length(if state.footer.get_show_menu() { 4 } else { 1 }),
]) ])
.areas(area); .areas(area);
@@ -269,11 +296,11 @@ impl StatefulWidget for AppWidget {
self.render_content(v1, buf, state); self.render_content(v1, buf, state);
// footer // footer
Footer { Footer {
show_menu: state.show_menu,
running_clock: state.clock_is_running(), running_clock: state.clock_is_running(),
selected_content: state.content, selected_content: state.content,
edit_mode: state.is_edit_mode(), edit_mode: state.is_edit_mode(),
app_time: state.app_time,
} }
.render(v2, buf); .render(v2, buf, &mut state.footer);
} }
} }

View File

@@ -1,6 +1,8 @@
use clap::ValueEnum; use clap::ValueEnum;
use ratatui::symbols::shade; use ratatui::symbols::shade;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::format_description;
use time::OffsetDateTime;
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize,
@@ -62,3 +64,122 @@ impl Style {
} }
} }
} }
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub enum AppTimeFormat {
/// `hh:mm:ss`
#[default]
HhMmSs,
/// `hh:mm`
HhMm,
/// `hh:mm AM` (or PM)
Hh12Mm,
/// `` (empty)
Hidden,
}
impl AppTimeFormat {
pub fn next(&self) -> Self {
match self {
AppTimeFormat::HhMmSs => AppTimeFormat::HhMm,
AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm,
AppTimeFormat::Hh12Mm => AppTimeFormat::Hidden,
AppTimeFormat::Hidden => AppTimeFormat::HhMmSs,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum AppTime {
Local(OffsetDateTime),
Utc(OffsetDateTime),
}
impl From<AppTime> for OffsetDateTime {
fn from(app_time: AppTime) -> Self {
match app_time {
AppTime::Local(t) => t,
AppTime::Utc(t) => t,
}
}
}
impl AppTime {
pub fn format(&self, app_format: &AppTimeFormat) -> String {
let parse_str = match app_format {
AppTimeFormat::HhMmSs => Some("[hour]:[minute]:[second]"),
AppTimeFormat::HhMm => Some("[hour]:[minute]"),
AppTimeFormat::Hh12Mm => Some("[hour repr:12 padding:none]:[minute] [period]"),
AppTimeFormat::Hidden => None,
};
if let Some(str) = parse_str {
format_description::parse(str)
.map_err(|_| "parse error")
.and_then(|fd| {
OffsetDateTime::from(*self)
.format(&fd)
.map_err(|_| "format error")
})
.unwrap_or_else(|e| e.to_string())
} else {
"".to_owned()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::{Date, Month, PrimitiveDateTime, Time};
#[test]
fn test_format_app_time() {
let dt = PrimitiveDateTime::new(
Date::from_calendar_date(2025, Month::January, 6).unwrap(),
Time::from_hms(18, 6, 10).unwrap(),
)
.assume_utc();
// hh:mm:ss
assert_eq!(
AppTime::Utc(dt).format(&AppTimeFormat::HhMmSs),
"18:06:10",
"utc"
);
assert_eq!(
AppTime::Local(dt).format(&AppTimeFormat::HhMmSs),
"18:06:10",
"local"
);
// hh:mm
assert_eq!(
AppTime::Utc(dt).format(&AppTimeFormat::HhMm),
"18:06",
"utc"
);
assert_eq!(
AppTime::Local(dt).format(&AppTimeFormat::HhMm),
"18:06",
"local"
);
// hh:mm period
assert_eq!(
AppTime::Utc(dt).format(&AppTimeFormat::Hh12Mm),
"6:06 PM",
"utc"
);
assert_eq!(
AppTime::Local(dt).format(&AppTimeFormat::Hh12Mm),
"6:06 PM",
"local"
);
// hidden
assert_eq!(AppTime::Utc(dt).format(&AppTimeFormat::Hidden), "", "utc");
assert_eq!(
AppTime::Local(dt).format(&AppTimeFormat::Hidden),
"",
"local"
);
}
}

View File

@@ -86,6 +86,10 @@ impl DurationEx {
let inner = self.inner.saturating_sub(ex.inner); let inner = self.inner.saturating_sub(ex.inner);
Self { inner } Self { inner }
} }
pub fn to_string_with_decis(self) -> String {
format!("{}.{}", self, self.decis())
}
} }
impl fmt::Display for DurationEx { impl fmt::Display for DurationEx {

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
common::{Content, Style}, common::{AppTimeFormat, Content, Style},
widgets::pomodoro::Mode as PomodoroMode, widgets::pomodoro::Mode as PomodoroMode,
}; };
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
@@ -12,6 +12,7 @@ use std::time::Duration;
pub struct AppStorage { pub struct AppStorage {
pub content: Content, pub content: Content,
pub show_menu: bool, pub show_menu: bool,
pub app_time_format: AppTimeFormat,
pub style: Style, pub style: Style,
pub with_decis: bool, pub with_decis: bool,
pub pomodoro_mode: PomodoroMode, pub pomodoro_mode: PomodoroMode,
@@ -24,6 +25,7 @@ pub struct AppStorage {
// countdown // countdown
pub inital_value_countdown: Duration, pub inital_value_countdown: Duration,
pub current_value_countdown: Duration, pub current_value_countdown: Duration,
pub elapsed_value_countdown: Duration,
// timer // timer
pub current_value_timer: Duration, pub current_value_timer: Duration,
} }
@@ -36,6 +38,7 @@ impl Default for AppStorage {
AppStorage { AppStorage {
content: Content::default(), content: Content::default(),
show_menu: true, show_menu: true,
app_time_format: AppTimeFormat::default(),
style: Style::default(), style: Style::default(),
with_decis: false, with_decis: false,
pomodoro_mode: PomodoroMode::Work, pomodoro_mode: PomodoroMode::Work,
@@ -48,6 +51,7 @@ impl Default for AppStorage {
// countdown // countdown
inital_value_countdown: DEFAULT_COUNTDOWN, inital_value_countdown: DEFAULT_COUNTDOWN,
current_value_countdown: DEFAULT_COUNTDOWN, current_value_countdown: DEFAULT_COUNTDOWN,
elapsed_value_countdown: Duration::ZERO,
// timer // timer
current_value_timer: Duration::ZERO, current_value_timer: Duration::ZERO,
} }

View File

@@ -73,26 +73,45 @@ pub enum Format {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Clock<T> { pub struct ClockState<T> {
initial_value: DurationEx, initial_value: DurationEx,
current_value: DurationEx, current_value: DurationEx,
tick_value: DurationEx, tick_value: DurationEx,
mode: Mode, mode: Mode,
format: Format, format: Format,
pub style: Style,
pub with_decis: bool, pub with_decis: bool,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
pub struct ClockArgs { pub struct ClockStateArgs {
pub initial_value: Duration, pub initial_value: Duration,
pub current_value: Duration, pub current_value: Duration,
pub tick_value: Duration, pub tick_value: Duration,
pub style: Style,
pub with_decis: bool, pub with_decis: bool,
} }
impl<T> Clock<T> { impl<T> ClockState<T> {
pub fn with_mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
pub fn get_mode(&self) -> &Mode {
&self.mode
}
pub fn is_initial(&self) -> bool {
self.mode == Mode::Initial
}
pub fn run(&mut self) {
self.mode = Mode::Tick
}
pub fn is_running(&self) -> bool {
self.mode == Mode::Tick
}
pub fn toggle_pause(&mut self) { pub fn toggle_pause(&mut self) {
self.mode = if self.mode == Mode::Tick { self.mode = if self.mode == Mode::Tick {
Mode::Pause Mode::Pause
@@ -187,6 +206,7 @@ impl<T> Clock<T> {
}; };
self.update_format(); self.update_format();
} }
pub fn edit_current_down(&mut self) { pub fn edit_current_down(&mut self) {
self.current_value = match self.mode { self.current_value = match self.mode {
Mode::Editable(Time::Decis, _) => { Mode::Editable(Time::Decis, _) => {
@@ -205,14 +225,6 @@ impl<T> Clock<T> {
self.update_mode(); self.update_mode();
} }
pub fn get_mode(&self) -> &Mode {
&self.mode
}
pub fn is_running(&self) -> bool {
self.mode == Mode::Tick
}
pub fn is_edit_mode(&self) -> bool { pub fn is_edit_mode(&self) -> bool {
matches!(self.mode, Mode::Editable(_, _)) matches!(self.mode, Mode::Editable(_, _))
} }
@@ -324,13 +336,12 @@ impl<T> Clock<T> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Countdown {} pub struct Countdown {}
impl Clock<Countdown> { impl ClockState<Countdown> {
pub fn new(args: ClockArgs) -> Self { pub fn new(args: ClockStateArgs) -> Self {
let ClockArgs { let ClockStateArgs {
initial_value, initial_value,
current_value, current_value,
tick_value, tick_value,
style,
with_decis, with_decis,
} = args; } = args;
let mut instance = Self { let mut instance = Self {
@@ -345,7 +356,6 @@ impl Clock<Countdown> {
Mode::Pause Mode::Pause
}, },
format: Format::S, format: Format::S,
style,
with_decis, with_decis,
phantom: PhantomData, phantom: PhantomData,
}; };
@@ -394,13 +404,12 @@ impl Clock<Countdown> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Timer {} pub struct Timer {}
impl Clock<Timer> { impl ClockState<Timer> {
pub fn new(args: ClockArgs) -> Self { pub fn new(args: ClockStateArgs) -> Self {
let ClockArgs { let ClockStateArgs {
initial_value, initial_value,
current_value, current_value,
tick_value, tick_value,
style,
with_decis, with_decis,
} = args; } = args;
let mut instance = Self { let mut instance = Self {
@@ -416,7 +425,6 @@ impl Clock<Timer> {
}, },
format: Format::S, format: Format::S,
phantom: PhantomData, phantom: PhantomData,
style,
with_decis, with_decis,
}; };
// update format once // update format once
@@ -461,6 +469,7 @@ pub struct ClockWidget<T>
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
style: Style,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
@@ -468,8 +477,9 @@ impl<T> ClockWidget<T>
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
pub fn new() -> Self { pub fn new(style: Style) -> Self {
Self { Self {
style,
phantom: PhantomData, phantom: PhantomData,
} }
} }
@@ -568,12 +578,12 @@ impl<T> StatefulWidget for ClockWidget<T>
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
type State = Clock<T>; type State = ClockState<T>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let with_decis = state.with_decis; let with_decis = state.with_decis;
let format = state.format; let format = state.format;
let symbol = state.style.get_digit_symbol(); let symbol = self.style.get_digit_symbol();
let widths = self.get_horizontal_lengths(&format, with_decis); let widths = self.get_horizontal_lengths(&format, with_decis);
let area = center_horizontal( let area = center_horizontal(
area, area,

View File

@@ -1,5 +1,4 @@
use crate::{ use crate::{
common::Style,
duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND}, duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
widgets::clock::*, widgets::clock::*,
}; };
@@ -7,11 +6,10 @@ use std::time::Duration;
#[test] #[test]
fn test_toggle_edit() { fn test_toggle_edit() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
// off by default // off by default
@@ -26,11 +24,10 @@ fn test_toggle_edit() {
#[test] #[test]
fn test_default_edit_mode_hhmmss() { fn test_default_edit_mode_hhmmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -41,11 +38,10 @@ fn test_default_edit_mode_hhmmss() {
#[test] #[test]
fn test_default_edit_mode_mmss() { fn test_default_edit_mode_mmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_MINUTE, initial_value: ONE_MINUTE,
current_value: ONE_MINUTE, current_value: ONE_MINUTE,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
// toggle on // toggle on
@@ -55,11 +51,10 @@ fn test_default_edit_mode_mmss() {
#[test] #[test]
fn test_default_edit_mode_ss() { fn test_default_edit_mode_ss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_SECOND, initial_value: ONE_SECOND,
current_value: ONE_SECOND, current_value: ONE_SECOND,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
// toggle on // toggle on
@@ -69,11 +64,10 @@ fn test_default_edit_mode_ss() {
#[test] #[test]
fn test_edit_next_hhmmssd() { fn test_edit_next_hhmmssd() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -91,11 +85,10 @@ fn test_edit_next_hhmmssd() {
#[test] #[test]
fn test_edit_next_hhmmss() { fn test_edit_next_hhmmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -111,11 +104,10 @@ fn test_edit_next_hhmmss() {
#[test] #[test]
fn test_edit_next_mmssd() { fn test_edit_next_mmssd() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_MINUTE, initial_value: ONE_MINUTE,
current_value: ONE_MINUTE, current_value: ONE_MINUTE,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -131,11 +123,10 @@ fn test_edit_next_mmssd() {
#[test] #[test]
fn test_edit_next_mmss() { fn test_edit_next_mmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_MINUTE, initial_value: ONE_MINUTE,
current_value: ONE_MINUTE, current_value: ONE_MINUTE,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -149,11 +140,10 @@ fn test_edit_next_mmss() {
#[test] #[test]
fn test_edit_next_ssd() { fn test_edit_next_ssd() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_SECOND * 3, initial_value: ONE_SECOND * 3,
current_value: ONE_SECOND * 3, current_value: ONE_SECOND * 3,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -165,11 +155,10 @@ fn test_edit_next_ssd() {
#[test] #[test]
fn test_edit_next_ss() { fn test_edit_next_ss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_SECOND * 3, initial_value: ONE_SECOND * 3,
current_value: ONE_SECOND * 3, current_value: ONE_SECOND * 3,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -182,11 +171,10 @@ fn test_edit_next_ss() {
#[test] #[test]
fn test_edit_prev_hhmmssd() { fn test_edit_prev_hhmmssd() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -203,11 +191,10 @@ fn test_edit_prev_hhmmssd() {
#[test] #[test]
fn test_edit_prev_hhmmss() { fn test_edit_prev_hhmmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -222,11 +209,10 @@ fn test_edit_prev_hhmmss() {
#[test] #[test]
fn test_edit_prev_mmssd() { fn test_edit_prev_mmssd() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_MINUTE, initial_value: ONE_MINUTE,
current_value: ONE_MINUTE, current_value: ONE_MINUTE,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -243,11 +229,10 @@ fn test_edit_prev_mmssd() {
#[test] #[test]
fn test_edit_prev_mmss() { fn test_edit_prev_mmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_MINUTE, initial_value: ONE_MINUTE,
current_value: ONE_MINUTE, current_value: ONE_MINUTE,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -262,11 +247,10 @@ fn test_edit_prev_mmss() {
#[test] #[test]
fn test_edit_prev_ssd() { fn test_edit_prev_ssd() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_SECOND, initial_value: ONE_SECOND,
current_value: ONE_SECOND, current_value: ONE_SECOND,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: true, with_decis: true,
}); });
@@ -281,11 +265,10 @@ fn test_edit_prev_ssd() {
#[test] #[test]
fn test_edit_prev_ss() { fn test_edit_prev_ss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_SECOND, initial_value: ONE_SECOND,
current_value: ONE_SECOND, current_value: ONE_SECOND,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -298,11 +281,10 @@ fn test_edit_prev_ss() {
#[test] #[test]
fn test_edit_up_ss() { fn test_edit_up_ss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: Duration::ZERO, current_value: Duration::ZERO,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -315,11 +297,10 @@ fn test_edit_up_ss() {
#[test] #[test]
fn test_edit_up_mmss() { fn test_edit_up_mmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: Duration::from_secs(60), current_value: Duration::from_secs(60),
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -335,11 +316,10 @@ fn test_edit_up_mmss() {
#[test] #[test]
fn test_edit_up_hhmmss() { fn test_edit_up_hhmmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: Duration::from_secs(3600), current_value: Duration::from_secs(3600),
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -357,11 +337,10 @@ fn test_edit_up_hhmmss() {
#[test] #[test]
fn test_edit_down_ss() { fn test_edit_down_ss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: ONE_SECOND, current_value: ONE_SECOND,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -378,11 +357,10 @@ fn test_edit_down_ss() {
#[test] #[test]
fn test_edit_down_mmss() { fn test_edit_down_mmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: Duration::from_secs(120), current_value: Duration::from_secs(120),
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });
@@ -401,11 +379,10 @@ fn test_edit_down_mmss() {
#[test] #[test]
fn test_edit_down_hhmmss() { fn test_edit_down_hhmmss() {
let mut c = Clock::<Timer>::new(ClockArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
current_value: Duration::from_secs(3600), current_value: Duration::from_secs(3600),
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
style: Style::default(),
with_decis: false, with_decis: false,
}); });

View File

@@ -5,57 +5,99 @@ use ratatui::{
text::Line, text::Line,
widgets::{StatefulWidget, Widget}, widgets::{StatefulWidget, Widget},
}; };
use std::cmp::max; use std::{cmp::max, time::Duration};
use crate::{ use crate::{
common::Style, common::Style,
constants::TICK_VALUE_MS,
duration::DurationEx,
events::{Event, EventHandler}, events::{Event, EventHandler},
utils::center, utils::center,
widgets::clock::{self, Clock, ClockWidget}, widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode},
}; };
/// State for Countdown Widget
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Countdown { pub struct CountdownState {
clock: Clock<clock::Countdown>, /// clock to count down
clock: ClockState<clock::Countdown>,
/// clock to count time after `DONE` - similar to Mission Elapsed Time (MET)
elapsed_clock: ClockState<clock::Timer>,
} }
impl Countdown { impl CountdownState {
pub const fn new(clock: Clock<clock::Countdown>) -> Self { pub fn new(clock: ClockState<clock::Countdown>, elapsed_value: Duration) -> Self {
Self { clock } Self {
} clock,
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
pub fn set_style(&mut self, style: Style) { initial_value: Duration::ZERO,
self.clock.style = style; current_value: elapsed_value,
tick_value: Duration::from_millis(TICK_VALUE_MS),
with_decis: false,
})
// A previous `elapsed_value > 0` means the `Clock` was running before,
// but not in `Initial` state anymore. Updating `Mode` here
// is needed to handle `Event::Tick` in `EventHandler::update` properly
.with_mode(if elapsed_value.gt(&Duration::ZERO) {
ClockMode::Pause
} else {
ClockMode::Initial
}),
}
} }
pub fn set_with_decis(&mut self, with_decis: bool) { pub fn set_with_decis(&mut self, with_decis: bool) {
self.clock.with_decis = with_decis; self.clock.with_decis = with_decis;
self.elapsed_clock.with_decis = with_decis;
} }
pub fn get_clock(&self) -> &Clock<clock::Countdown> { pub fn get_clock(&self) -> &ClockState<clock::Countdown> {
&self.clock &self.clock
} }
pub fn is_running(&self) -> bool {
self.clock.is_running() || self.elapsed_clock.is_running()
}
pub fn get_elapsed_value(&self) -> &DurationEx {
self.elapsed_clock.get_current_value()
}
} }
impl EventHandler for Countdown { impl EventHandler for CountdownState {
fn update(&mut self, event: Event) -> Option<Event> { fn update(&mut self, event: Event) -> Option<Event> {
let edit_mode = self.clock.is_edit_mode(); let edit_mode = self.clock.is_edit_mode();
match event { match event {
Event::Tick => { Event::Tick => {
self.clock.tick(); if !self.clock.is_done() {
} self.clock.tick();
Event::Key(key) if key.code == KeyCode::Char('r') => { } else {
self.clock.reset(); self.elapsed_clock.tick();
if self.elapsed_clock.is_initial() {
self.elapsed_clock.run();
}
}
} }
Event::Key(key) => match key.code { Event::Key(key) => match key.code {
KeyCode::Char('r') => { KeyCode::Char('r') => {
// reset both clocks
self.clock.reset(); self.clock.reset();
self.elapsed_clock.reset();
} }
KeyCode::Char('s') => { KeyCode::Char('s') => {
self.clock.toggle_pause(); // toggle pause status depending on which clock is running
if !self.clock.is_done() {
self.clock.toggle_pause();
} else {
self.elapsed_clock.toggle_pause();
}
} }
KeyCode::Char('e') => { KeyCode::Char('e') => {
self.clock.toggle_edit(); self.clock.toggle_edit();
// stop + reset timer entering `edit` mode
if self.elapsed_clock.is_running() {
self.elapsed_clock.toggle_pause();
}
} }
KeyCode::Left if edit_mode => { KeyCode::Left if edit_mode => {
self.clock.edit_next(); self.clock.edit_next();
@@ -65,9 +107,13 @@ impl EventHandler for Countdown {
} }
KeyCode::Up if edit_mode => { KeyCode::Up if edit_mode => {
self.clock.edit_up(); self.clock.edit_up();
// whenever `clock`'s value is changed, reset `elapsed_clock`
self.elapsed_clock.reset();
} }
KeyCode::Down if edit_mode => { KeyCode::Down if edit_mode => {
self.clock.edit_down(); self.clock.edit_down();
// whenever clock value is changed, reset timer
self.elapsed_clock.reset();
} }
_ => return Some(event), _ => return Some(event),
}, },
@@ -77,13 +123,38 @@ impl EventHandler for Countdown {
} }
} }
pub struct CountdownWidget; pub struct Countdown {
pub style: Style,
}
impl StatefulWidget for CountdownWidget { impl StatefulWidget for Countdown {
type State = Countdown; type State = CountdownState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let clock = ClockWidget::new(); 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
.elapsed_clock
.get_current_value()
.to_string_with_decis()
)
} else {
format!(
"Countdown {} +{}",
state.clock.get_mode(),
state.elapsed_clock.get_current_value()
)
}
} else {
format!("Countdown {}", state.clock.get_mode())
}
.to_uppercase(),
);
let area = center( let area = center(
area, area,

View File

@@ -1,25 +1,57 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use crate::common::Content; use crate::common::{AppTime, AppTimeFormat, Content};
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::{Modifier, Style}, style::{Modifier, Style},
symbols::{border, scrollbar}, symbols::{border, scrollbar},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, Cell, Row, Table, Widget}, widgets::{Block, Borders, Cell, Row, StatefulWidget, Table, Widget},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FooterState {
show_menu: bool,
app_time_format: AppTimeFormat,
}
impl FooterState {
pub const fn new(show_menu: bool, app_time_format: AppTimeFormat) -> Self {
Self {
show_menu,
app_time_format,
}
}
pub fn set_show_menu(&mut self, value: bool) {
self.show_menu = value;
}
pub const fn get_show_menu(&self) -> bool {
self.show_menu
}
pub const fn app_time_format(&self) -> &AppTimeFormat {
&self.app_time_format
}
pub fn toggle_app_time_format(&mut self) {
self.app_time_format = self.app_time_format.next();
}
}
#[derive(Debug)]
pub struct Footer { pub struct Footer {
pub show_menu: bool,
pub running_clock: bool, pub running_clock: bool,
pub selected_content: Content, pub selected_content: Content,
pub edit_mode: bool, pub edit_mode: bool,
pub app_time: AppTime,
} }
impl Widget for Footer { impl StatefulWidget for Footer {
fn render(self, area: Rect, buf: &mut Buffer) { type State = FooterState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let content_labels: BTreeMap<Content, &str> = BTreeMap::from([ let content_labels: BTreeMap<Content, &str> = BTreeMap::from([
(Content::Countdown, "[c]ountdown"), (Content::Countdown, "[c]ountdown"),
(Content::Timer, "[t]imer"), (Content::Timer, "[t]imer"),
@@ -31,15 +63,25 @@ impl Widget for Footer {
let [border_area, menu_area] = let [border_area, menu_area] =
Layout::vertical([Constraint::Length(1), Constraint::Percentage(100)]).areas(area); Layout::vertical([Constraint::Length(1), Constraint::Percentage(100)]).areas(area);
Block::new() Block::new()
.borders(Borders::TOP) .borders(Borders::TOP)
.title( .title(
format! {"[m]enu {:} ", if self.show_menu {scrollbar::VERTICAL.end} else {scrollbar::VERTICAL.begin}}, format! {"[m]enu {:} ", if state.show_menu {scrollbar::VERTICAL.end} else {scrollbar::VERTICAL.begin}},
) )
.title(
Line::from(
match state.app_time_format {
// `Hidden` -> no (empty) title
AppTimeFormat::Hidden => "".into(),
// others -> add some space around
_ => format!(" {} ", self.app_time.format(&state.app_time_format))
}
).right_aligned())
.border_set(border::PLAIN) .border_set(border::PLAIN)
.render(border_area, buf); .render(border_area, buf);
// show menu // show menu
if self.show_menu { if state.show_menu {
let content_labels: Vec<Span> = content_labels let content_labels: Vec<Span> = content_labels
.iter() .iter()
.enumerate() .enumerate()
@@ -60,7 +102,7 @@ impl Widget for Footer {
const SPACE: &str = " "; // 2 empty spaces const SPACE: &str = " "; // 2 empty spaces
let widths = [Constraint::Length(12), Constraint::Percentage(100)]; let widths = [Constraint::Length(12), Constraint::Percentage(100)];
Table::new( let table = Table::new(
[ [
// content // content
Row::new(vec![ Row::new(vec![
@@ -80,6 +122,14 @@ impl Widget for Footer {
Span::from("[,]change style"), Span::from("[,]change style"),
Span::from(SPACE), Span::from(SPACE),
Span::from("[.]toggle deciseconds"), Span::from("[.]toggle deciseconds"),
Span::from(SPACE),
Span::from(format!(
"[:]toggle {} time",
match self.app_time {
AppTime::Local(_) => "local",
AppTime::Utc(_) => "utc",
}
)),
])), ])),
]), ]),
// edit // edit
@@ -128,8 +178,9 @@ impl Widget for Footer {
], ],
widths, widths,
) )
.column_spacing(1) .column_spacing(1);
.render(menu_area, buf);
Widget::render(table, menu_area, buf);
} }
} }
} }

View File

@@ -3,7 +3,7 @@ use crate::{
constants::TICK_VALUE_MS, constants::TICK_VALUE_MS,
events::{Event, EventHandler}, events::{Event, EventHandler},
utils::center, utils::center,
widgets::clock::{Clock, ClockWidget, Countdown}, widgets::clock::{ClockState, ClockWidget, Countdown},
}; };
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
@@ -18,7 +18,7 @@ use strum::Display;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::clock::ClockArgs; use super::clock::ClockStateArgs;
#[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)]
pub enum Mode { pub enum Mode {
@@ -28,18 +28,18 @@ pub enum Mode {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ClockMap { pub struct ClockMap {
work: Clock<Countdown>, work: ClockState<Countdown>,
pause: Clock<Countdown>, pause: ClockState<Countdown>,
} }
impl ClockMap { impl ClockMap {
fn get_mut(&mut self, mode: &Mode) -> &mut Clock<Countdown> { fn get_mut(&mut self, mode: &Mode) -> &mut ClockState<Countdown> {
match mode { match mode {
Mode::Work => &mut self.work, Mode::Work => &mut self.work,
Mode::Pause => &mut self.pause, Mode::Pause => &mut self.pause,
} }
} }
fn get(&self, mode: &Mode) -> &Clock<Countdown> { fn get(&self, mode: &Mode) -> &ClockState<Countdown> {
match mode { match mode {
Mode::Work => &self.work, Mode::Work => &self.work,
Mode::Pause => &self.pause, Mode::Pause => &self.pause,
@@ -48,66 +48,62 @@ impl ClockMap {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Pomodoro { pub struct PomodoroState {
mode: Mode, mode: Mode,
clock_map: ClockMap, clock_map: ClockMap,
} }
pub struct PomodoroArgs { pub struct PomodoroStateArgs {
pub mode: Mode, pub mode: Mode,
pub initial_value_work: Duration, pub initial_value_work: Duration,
pub current_value_work: Duration, pub current_value_work: Duration,
pub initial_value_pause: Duration, pub initial_value_pause: Duration,
pub current_value_pause: Duration, pub current_value_pause: Duration,
pub style: Style,
pub with_decis: bool, pub with_decis: bool,
} }
impl Pomodoro { impl PomodoroState {
pub fn new(args: PomodoroArgs) -> Self { pub fn new(args: PomodoroStateArgs) -> Self {
let PomodoroArgs { let PomodoroStateArgs {
mode, mode,
initial_value_work, initial_value_work,
current_value_work, current_value_work,
initial_value_pause, initial_value_pause,
current_value_pause, current_value_pause,
style,
with_decis, with_decis,
} = args; } = args;
Self { Self {
mode, mode,
clock_map: ClockMap { clock_map: ClockMap {
work: Clock::<Countdown>::new(ClockArgs { work: ClockState::<Countdown>::new(ClockStateArgs {
initial_value: initial_value_work, initial_value: initial_value_work,
current_value: current_value_work, current_value: current_value_work,
tick_value: Duration::from_millis(TICK_VALUE_MS), tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis, with_decis,
}), }),
pause: Clock::<Countdown>::new(ClockArgs { pause: ClockState::<Countdown>::new(ClockStateArgs {
initial_value: initial_value_pause, initial_value: initial_value_pause,
current_value: current_value_pause, current_value: current_value_pause,
tick_value: Duration::from_millis(TICK_VALUE_MS), tick_value: Duration::from_millis(TICK_VALUE_MS),
style,
with_decis, with_decis,
}), }),
}, },
} }
} }
fn get_clock_mut(&mut self) -> &mut Clock<Countdown> { fn get_clock_mut(&mut self) -> &mut ClockState<Countdown> {
self.clock_map.get_mut(&self.mode) self.clock_map.get_mut(&self.mode)
} }
pub fn get_clock(&self) -> &Clock<Countdown> { pub fn get_clock(&self) -> &ClockState<Countdown> {
self.clock_map.get(&self.mode) self.clock_map.get(&self.mode)
} }
pub fn get_clock_work(&self) -> &Clock<Countdown> { pub fn get_clock_work(&self) -> &ClockState<Countdown> {
&self.clock_map.work &self.clock_map.work
} }
pub fn get_clock_pause(&self) -> &Clock<Countdown> { pub fn get_clock_pause(&self) -> &ClockState<Countdown> {
&self.clock_map.pause &self.clock_map.pause
} }
@@ -115,11 +111,6 @@ impl Pomodoro {
&self.mode &self.mode
} }
pub fn set_style(&mut self, style: Style) {
self.clock_map.work.style = style;
self.clock_map.pause.style = style;
}
pub fn set_with_decis(&mut self, with_decis: bool) { pub fn set_with_decis(&mut self, with_decis: bool) {
self.clock_map.work.with_decis = with_decis; self.clock_map.work.with_decis = with_decis;
self.clock_map.pause.with_decis = with_decis; self.clock_map.pause.with_decis = with_decis;
@@ -133,7 +124,7 @@ impl Pomodoro {
} }
} }
impl EventHandler for Pomodoro { impl EventHandler for PomodoroState {
fn update(&mut self, event: Event) -> Option<Event> { fn update(&mut self, event: Event) -> Option<Event> {
let edit_mode = self.get_clock().is_edit_mode(); let edit_mode = self.get_clock().is_edit_mode();
match event { match event {
@@ -177,12 +168,14 @@ impl EventHandler for Pomodoro {
} }
} }
pub struct PomodoroWidget; pub struct PomodoroWidget {
pub style: Style,
}
impl StatefulWidget for PomodoroWidget { impl StatefulWidget for PomodoroWidget {
type State = Pomodoro; type State = PomodoroState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let clock_widget = ClockWidget::new(); let clock_widget = ClockWidget::new(self.style);
let label = Line::raw( let label = Line::raw(
(format!( (format!(
"Pomodoro {} {}", "Pomodoro {} {}",

View File

@@ -2,7 +2,7 @@ use crate::{
common::Style, common::Style,
events::{Event, EventHandler}, events::{Event, EventHandler},
utils::center, utils::center,
widgets::clock::{self, Clock, ClockWidget}, widgets::clock::{self, ClockState, ClockWidget},
}; };
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
@@ -14,29 +14,25 @@ use ratatui::{
use std::cmp::max; use std::cmp::max;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Timer { pub struct TimerState {
clock: Clock<clock::Timer>, clock: ClockState<clock::Timer>,
} }
impl Timer { impl TimerState {
pub const fn new(clock: Clock<clock::Timer>) -> Self { pub const fn new(clock: ClockState<clock::Timer>) -> Self {
Self { clock } Self { clock }
} }
pub fn set_style(&mut self, style: Style) {
self.clock.style = style;
}
pub fn set_with_decis(&mut self, with_decis: bool) { pub fn set_with_decis(&mut self, with_decis: bool) {
self.clock.with_decis = with_decis; self.clock.with_decis = with_decis;
} }
pub fn get_clock(&self) -> &Clock<clock::Timer> { pub fn get_clock(&self) -> &ClockState<clock::Timer> {
&self.clock &self.clock
} }
} }
impl EventHandler for Timer { impl EventHandler for TimerState {
fn update(&mut self, event: Event) -> Option<Event> { fn update(&mut self, event: Event) -> Option<Event> {
let edit_mode = self.clock.is_edit_mode(); let edit_mode = self.clock.is_edit_mode();
match event { match event {
@@ -73,13 +69,15 @@ impl EventHandler for Timer {
} }
} }
pub struct TimerWidget; pub struct Timer {
pub style: Style,
}
impl StatefulWidget for &TimerWidget { impl StatefulWidget for Timer {
type State = Timer; type State = TimerState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let clock = &mut state.clock; let clock = &mut state.clock;
let clock_widget = ClockWidget::new(); let clock_widget = ClockWidget::new(self.style);
let label = Line::raw((format!("Timer {}", clock.get_mode())).to_uppercase()); let label = Line::raw((format!("Timer {}", clock.get_mode())).to_uppercase());
let area = center( let area = center(