Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ee5d7b4e9 | ||
|
|
9ea9f88266 | ||
|
|
c8af76c9e5 | ||
|
|
468b4a5abf | ||
|
|
8603a823e4 | ||
|
|
94bdeeab11 | ||
|
|
66c6d7fc46 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# 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
|
||||
|
||||
Initial version.
|
||||
@@ -8,5 +18,6 @@ Initial version.
|
||||
|
||||
- Add `Pomodoro`, `Timer`, `Countdown`
|
||||
- Persist application state
|
||||
- Change styles
|
||||
- Custom styles for digits
|
||||
- Toggle deciseconds
|
||||
- CLI
|
||||
|
||||
66
Cargo.lock
generated
66
Cargo.lock
generated
@@ -296,6 +296,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -649,6 +658,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
@@ -723,6 +747,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
@@ -1050,9 +1080,42 @@ dependencies = [
|
||||
"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]]
|
||||
name = "timr-tui"
|
||||
version = "0.9.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"color-eyre",
|
||||
@@ -1063,6 +1126,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "timr-tui"
|
||||
version = "0.9.0"
|
||||
version = "1.0.0"
|
||||
description = "TUI to organize your time: Pomodoro, Countdown, Timer."
|
||||
edition = "2021"
|
||||
rust-version = "1.82.0"
|
||||
@@ -26,3 +26,4 @@ tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
directories = "5.0.1"
|
||||
clap = { version = "4.5.23", features = ["derive"] }
|
||||
time = { version = "0.3.37", features = ["formatting", "local-offset"] }
|
||||
|
||||
40
README.md
40
README.md
@@ -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.
|
||||
- `[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.
|
||||
|
||||
@@ -48,6 +48,18 @@ _Side note:_ Theme colors depend on your terminal preferences.
|
||||
<img alt="menu" src="demo/menu.gif" />
|
||||
</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
|
||||
|
||||
```sh
|
||||
@@ -70,19 +82,35 @@ Options:
|
||||
|
||||
# Installation
|
||||
|
||||
From [crates.io](https://crates.io/crates/timr-tui) run:
|
||||
## Cargo
|
||||
|
||||
### From [crates.io](https://crates.io/crates/timr-tui)
|
||||
|
||||
```sh
|
||||
cargo install timr-tui
|
||||
```
|
||||
|
||||
Latest version from git repository:
|
||||
### From GitHub repository
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
@@ -152,7 +180,7 @@ In `debug` mode only. Locations:
|
||||
|
||||
```sh
|
||||
# Linux
|
||||
~/.local/state/timr/logs/app.log
|
||||
~/.local/state/timr-tui/logs/app.log
|
||||
# macOS
|
||||
/Users/{user}/Library/Application Support/timr-tui/logs/app.log
|
||||
# `Windows`
|
||||
|
||||
BIN
demo/countdown-met.gif
Normal file
BIN
demo/countdown-met.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
22
demo/countdown-met.tape
Normal file
22
demo/countdown-met.tape
Normal 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
BIN
demo/local-time.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
22
demo/local-time.tape
Normal file
22
demo/local-time.tape
Normal 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
|
||||
BIN
demo/rocket-countdown_no-ds.gif
Normal file
BIN
demo/rocket-countdown_no-ds.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
15
justfile
15
justfile
@@ -49,6 +49,11 @@ alias dc := demo-countdown
|
||||
demo-countdown:
|
||||
vhs demo/countdown.tape
|
||||
|
||||
alias dcm := demo-countdown-met
|
||||
|
||||
demo-countdown-met:
|
||||
vhs demo/countdown-met.tape
|
||||
|
||||
alias ds := demo-style
|
||||
|
||||
demo-style:
|
||||
@@ -63,3 +68,13 @@ alias dm := demo-menu
|
||||
|
||||
demo-menu:
|
||||
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
|
||||
|
||||
101
src/app.rs
101
src/app.rs
@@ -1,17 +1,17 @@
|
||||
use crate::{
|
||||
args::Args,
|
||||
common::{Content, Style},
|
||||
common::{AppTime, AppTimeFormat, Content, Style},
|
||||
constants::TICK_VALUE_MS,
|
||||
events::{Event, EventHandler, Events},
|
||||
storage::AppStorage,
|
||||
terminal::Terminal,
|
||||
widgets::{
|
||||
clock::{self, Clock, ClockArgs},
|
||||
countdown::{Countdown, CountdownWidget},
|
||||
footer::Footer,
|
||||
clock::{self, ClockState, ClockStateArgs},
|
||||
countdown::{Countdown, CountdownState},
|
||||
footer::{Footer, FooterState},
|
||||
header::Header,
|
||||
pomodoro::{Mode as PomodoroMode, Pomodoro, PomodoroArgs, PomodoroWidget},
|
||||
timer::{Timer, TimerWidget},
|
||||
pomodoro::{Mode as PomodoroMode, PomodoroState, PomodoroStateArgs, PomodoroWidget},
|
||||
timer::{Timer, TimerState},
|
||||
},
|
||||
};
|
||||
use color_eyre::Result;
|
||||
@@ -22,6 +22,7 @@ use ratatui::{
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -34,18 +35,20 @@ enum Mode {
|
||||
pub struct App {
|
||||
content: Content,
|
||||
mode: Mode,
|
||||
show_menu: bool,
|
||||
countdown: Countdown,
|
||||
timer: Timer,
|
||||
pomodoro: Pomodoro,
|
||||
app_time: AppTime,
|
||||
countdown: CountdownState,
|
||||
timer: TimerState,
|
||||
pomodoro: PomodoroState,
|
||||
style: Style,
|
||||
with_decis: bool,
|
||||
footer: FooterState,
|
||||
}
|
||||
|
||||
pub struct AppArgs {
|
||||
pub style: Style,
|
||||
pub with_decis: bool,
|
||||
pub show_menu: bool,
|
||||
pub app_time_format: AppTimeFormat,
|
||||
pub content: Content,
|
||||
pub pomodoro_mode: PomodoroMode,
|
||||
pub initial_value_work: Duration,
|
||||
@@ -54,6 +57,7 @@ pub struct AppArgs {
|
||||
pub current_value_pause: Duration,
|
||||
pub initial_value_countdown: Duration,
|
||||
pub current_value_countdown: Duration,
|
||||
pub elapsed_value_countdown: Duration,
|
||||
pub current_value_timer: Duration,
|
||||
}
|
||||
|
||||
@@ -64,6 +68,7 @@ impl From<(Args, AppStorage)> for AppArgs {
|
||||
AppArgs {
|
||||
with_decis: args.decis || stg.with_decis,
|
||||
show_menu: args.menu || stg.show_menu,
|
||||
app_time_format: stg.app_time_format,
|
||||
content: args.mode.unwrap_or(stg.content),
|
||||
style: args.style.unwrap_or(stg.style),
|
||||
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),
|
||||
// invalidate `current_value_countdown` if an initial value is set via args
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_app_time() -> AppTime {
|
||||
match OffsetDateTime::now_local() {
|
||||
Ok(t) => AppTime::Local(t),
|
||||
Err(_) => AppTime::Utc(OffsetDateTime::now_utc()),
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(args: AppArgs) -> Self {
|
||||
let AppArgs {
|
||||
style,
|
||||
show_menu,
|
||||
app_time_format,
|
||||
initial_value_work,
|
||||
initial_value_pause,
|
||||
initial_value_countdown,
|
||||
current_value_work,
|
||||
current_value_pause,
|
||||
current_value_countdown,
|
||||
elapsed_value_countdown,
|
||||
current_value_timer,
|
||||
content,
|
||||
with_decis,
|
||||
@@ -100,38 +115,43 @@ impl App {
|
||||
Self {
|
||||
mode: Mode::Running,
|
||||
content,
|
||||
show_menu,
|
||||
app_time: get_app_time(),
|
||||
style,
|
||||
with_decis,
|
||||
countdown: Countdown::new(Clock::<clock::Countdown>::new(ClockArgs {
|
||||
initial_value: initial_value_countdown,
|
||||
current_value: current_value_countdown,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
style,
|
||||
with_decis,
|
||||
})),
|
||||
timer: Timer::new(Clock::<clock::Timer>::new(ClockArgs {
|
||||
countdown: CountdownState::new(
|
||||
ClockState::<clock::Countdown>::new(ClockStateArgs {
|
||||
initial_value: initial_value_countdown,
|
||||
current_value: current_value_countdown,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
with_decis,
|
||||
}),
|
||||
elapsed_value_countdown,
|
||||
),
|
||||
timer: TimerState::new(ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: current_value_timer,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
style,
|
||||
with_decis,
|
||||
})),
|
||||
pomodoro: Pomodoro::new(PomodoroArgs {
|
||||
pomodoro: PomodoroState::new(PomodoroStateArgs {
|
||||
mode: pomodoro_mode,
|
||||
initial_value_work,
|
||||
current_value_work,
|
||||
initial_value_pause,
|
||||
current_value_pause,
|
||||
style,
|
||||
with_decis,
|
||||
}),
|
||||
footer: FooterState::new(show_menu, app_time_format),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(mut self, mut terminal: Terminal, mut events: Events) -> Result<Self> {
|
||||
while self.is_running() {
|
||||
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
|
||||
if let Some(unhandled) = match self.content {
|
||||
Content::Countdown => self.countdown.update(event.clone()),
|
||||
@@ -165,7 +185,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(),
|
||||
}
|
||||
@@ -186,13 +206,12 @@ impl App {
|
||||
KeyCode::Char('c') => self.content = Content::Countdown,
|
||||
KeyCode::Char('t') => self.content = Content::Timer,
|
||||
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(',') => {
|
||||
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('.') => {
|
||||
self.with_decis = !self.with_decis;
|
||||
@@ -201,8 +220,8 @@ impl App {
|
||||
self.countdown.set_with_decis(self.with_decis);
|
||||
self.pomodoro.set_with_decis(self.with_decis);
|
||||
}
|
||||
KeyCode::Up => self.show_menu = true,
|
||||
KeyCode::Down => self.show_menu = false,
|
||||
KeyCode::Up => self.footer.set_show_menu(true),
|
||||
KeyCode::Down => self.footer.set_show_menu(false),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -217,7 +236,8 @@ impl App {
|
||||
pub fn to_storage(&self) -> AppStorage {
|
||||
AppStorage {
|
||||
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,
|
||||
with_decis: self.with_decis,
|
||||
pomodoro_mode: self.pomodoro.get_mode().clone(),
|
||||
@@ -233,6 +253,7 @@ impl App {
|
||||
current_value_countdown: Duration::from(
|
||||
*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()),
|
||||
}
|
||||
}
|
||||
@@ -243,9 +264,15 @@ struct AppWidget;
|
||||
impl AppWidget {
|
||||
fn render_content(&self, area: Rect, buf: &mut Buffer, state: &mut App) {
|
||||
match state.content {
|
||||
Content::Timer => TimerWidget.render(area, buf, &mut state.timer.clone()),
|
||||
Content::Countdown => CountdownWidget.render(area, buf, &mut state.countdown.clone()),
|
||||
Content::Pomodoro => PomodoroWidget.render(area, buf, &mut state.pomodoro.clone()),
|
||||
Content::Timer => {
|
||||
Timer { style: state.style }.render(area, buf, &mut state.timer);
|
||||
}
|
||||
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([
|
||||
Constraint::Length(1),
|
||||
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);
|
||||
|
||||
@@ -269,11 +296,11 @@ impl StatefulWidget for AppWidget {
|
||||
self.render_content(v1, buf, state);
|
||||
// footer
|
||||
Footer {
|
||||
show_menu: state.show_menu,
|
||||
running_clock: state.clock_is_running(),
|
||||
selected_content: state.content,
|
||||
edit_mode: state.is_edit_mode(),
|
||||
app_time: state.app_time,
|
||||
}
|
||||
.render(v2, buf);
|
||||
.render(v2, buf, &mut state.footer);
|
||||
}
|
||||
}
|
||||
|
||||
121
src/common.rs
121
src/common.rs
@@ -1,6 +1,8 @@
|
||||
use clap::ValueEnum;
|
||||
use ratatui::symbols::shade;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::format_description;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
common::{Content, Style},
|
||||
common::{AppTimeFormat, Content, Style},
|
||||
widgets::pomodoro::Mode as PomodoroMode,
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
@@ -12,6 +12,7 @@ use std::time::Duration;
|
||||
pub struct AppStorage {
|
||||
pub content: Content,
|
||||
pub show_menu: bool,
|
||||
pub app_time_format: AppTimeFormat,
|
||||
pub style: Style,
|
||||
pub with_decis: bool,
|
||||
pub pomodoro_mode: PomodoroMode,
|
||||
@@ -24,6 +25,7 @@ pub struct AppStorage {
|
||||
// countdown
|
||||
pub inital_value_countdown: Duration,
|
||||
pub current_value_countdown: Duration,
|
||||
pub elapsed_value_countdown: Duration,
|
||||
// timer
|
||||
pub current_value_timer: Duration,
|
||||
}
|
||||
@@ -36,6 +38,7 @@ impl Default for AppStorage {
|
||||
AppStorage {
|
||||
content: Content::default(),
|
||||
show_menu: true,
|
||||
app_time_format: AppTimeFormat::default(),
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
pomodoro_mode: PomodoroMode::Work,
|
||||
@@ -48,6 +51,7 @@ impl Default for AppStorage {
|
||||
// countdown
|
||||
inital_value_countdown: DEFAULT_COUNTDOWN,
|
||||
current_value_countdown: DEFAULT_COUNTDOWN,
|
||||
elapsed_value_countdown: Duration::ZERO,
|
||||
// timer
|
||||
current_value_timer: Duration::ZERO,
|
||||
}
|
||||
|
||||
@@ -73,26 +73,45 @@ pub enum Format {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Clock<T> {
|
||||
pub struct ClockState<T> {
|
||||
initial_value: DurationEx,
|
||||
current_value: DurationEx,
|
||||
tick_value: DurationEx,
|
||||
mode: Mode,
|
||||
format: Format,
|
||||
pub style: Style,
|
||||
pub with_decis: bool,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
pub struct ClockArgs {
|
||||
pub struct ClockStateArgs {
|
||||
pub initial_value: Duration,
|
||||
pub current_value: Duration,
|
||||
pub tick_value: Duration,
|
||||
pub style: Style,
|
||||
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) {
|
||||
self.mode = if self.mode == Mode::Tick {
|
||||
Mode::Pause
|
||||
@@ -187,6 +206,7 @@ impl<T> Clock<T> {
|
||||
};
|
||||
self.update_format();
|
||||
}
|
||||
|
||||
pub fn edit_current_down(&mut self) {
|
||||
self.current_value = match self.mode {
|
||||
Mode::Editable(Time::Decis, _) => {
|
||||
@@ -205,14 +225,6 @@ impl<T> Clock<T> {
|
||||
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 {
|
||||
matches!(self.mode, Mode::Editable(_, _))
|
||||
}
|
||||
@@ -324,13 +336,12 @@ impl<T> Clock<T> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Countdown {}
|
||||
|
||||
impl Clock<Countdown> {
|
||||
pub fn new(args: ClockArgs) -> Self {
|
||||
let ClockArgs {
|
||||
impl ClockState<Countdown> {
|
||||
pub fn new(args: ClockStateArgs) -> Self {
|
||||
let ClockStateArgs {
|
||||
initial_value,
|
||||
current_value,
|
||||
tick_value,
|
||||
style,
|
||||
with_decis,
|
||||
} = args;
|
||||
let mut instance = Self {
|
||||
@@ -345,7 +356,6 @@ impl Clock<Countdown> {
|
||||
Mode::Pause
|
||||
},
|
||||
format: Format::S,
|
||||
style,
|
||||
with_decis,
|
||||
phantom: PhantomData,
|
||||
};
|
||||
@@ -394,13 +404,12 @@ impl Clock<Countdown> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timer {}
|
||||
|
||||
impl Clock<Timer> {
|
||||
pub fn new(args: ClockArgs) -> Self {
|
||||
let ClockArgs {
|
||||
impl ClockState<Timer> {
|
||||
pub fn new(args: ClockStateArgs) -> Self {
|
||||
let ClockStateArgs {
|
||||
initial_value,
|
||||
current_value,
|
||||
tick_value,
|
||||
style,
|
||||
with_decis,
|
||||
} = args;
|
||||
let mut instance = Self {
|
||||
@@ -416,7 +425,6 @@ impl Clock<Timer> {
|
||||
},
|
||||
format: Format::S,
|
||||
phantom: PhantomData,
|
||||
style,
|
||||
with_decis,
|
||||
};
|
||||
// update format once
|
||||
@@ -461,6 +469,7 @@ pub struct ClockWidget<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
style: Style,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -468,8 +477,9 @@ impl<T> ClockWidget<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
pub fn new(style: Style) -> Self {
|
||||
Self {
|
||||
style,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -568,12 +578,12 @@ impl<T> StatefulWidget for ClockWidget<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
type State = Clock<T>;
|
||||
type State = ClockState<T>;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let with_decis = state.with_decis;
|
||||
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 area = center_horizontal(
|
||||
area,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::{
|
||||
common::Style,
|
||||
duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
|
||||
widgets::clock::*,
|
||||
};
|
||||
@@ -7,11 +6,10 @@ use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_toggle_edit() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_HOUR,
|
||||
current_value: ONE_HOUR,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
// off by default
|
||||
@@ -26,11 +24,10 @@ fn test_toggle_edit() {
|
||||
|
||||
#[test]
|
||||
fn test_default_edit_mode_hhmmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_HOUR,
|
||||
current_value: ONE_HOUR,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -41,11 +38,10 @@ fn test_default_edit_mode_hhmmss() {
|
||||
|
||||
#[test]
|
||||
fn test_default_edit_mode_mmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_MINUTE,
|
||||
current_value: ONE_MINUTE,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
// toggle on
|
||||
@@ -55,11 +51,10 @@ fn test_default_edit_mode_mmss() {
|
||||
|
||||
#[test]
|
||||
fn test_default_edit_mode_ss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_SECOND,
|
||||
current_value: ONE_SECOND,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
// toggle on
|
||||
@@ -69,11 +64,10 @@ fn test_default_edit_mode_ss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_next_hhmmssd() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_HOUR,
|
||||
current_value: ONE_HOUR,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -91,11 +85,10 @@ fn test_edit_next_hhmmssd() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_next_hhmmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_HOUR,
|
||||
current_value: ONE_HOUR,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -111,11 +104,10 @@ fn test_edit_next_hhmmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_next_mmssd() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_MINUTE,
|
||||
current_value: ONE_MINUTE,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -131,11 +123,10 @@ fn test_edit_next_mmssd() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_next_mmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_MINUTE,
|
||||
current_value: ONE_MINUTE,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -149,11 +140,10 @@ fn test_edit_next_mmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_next_ssd() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_SECOND * 3,
|
||||
current_value: ONE_SECOND * 3,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -165,11 +155,10 @@ fn test_edit_next_ssd() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_next_ss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_SECOND * 3,
|
||||
current_value: ONE_SECOND * 3,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -182,11 +171,10 @@ fn test_edit_next_ss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_prev_hhmmssd() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_HOUR,
|
||||
current_value: ONE_HOUR,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -203,11 +191,10 @@ fn test_edit_prev_hhmmssd() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_prev_hhmmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_HOUR,
|
||||
current_value: ONE_HOUR,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -222,11 +209,10 @@ fn test_edit_prev_hhmmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_prev_mmssd() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_MINUTE,
|
||||
current_value: ONE_MINUTE,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -243,11 +229,10 @@ fn test_edit_prev_mmssd() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_prev_mmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_MINUTE,
|
||||
current_value: ONE_MINUTE,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -262,11 +247,10 @@ fn test_edit_prev_mmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_prev_ssd() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_SECOND,
|
||||
current_value: ONE_SECOND,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: true,
|
||||
});
|
||||
|
||||
@@ -281,11 +265,10 @@ fn test_edit_prev_ssd() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_prev_ss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: ONE_SECOND,
|
||||
current_value: ONE_SECOND,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -298,11 +281,10 @@ fn test_edit_prev_ss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_up_ss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: Duration::ZERO,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -315,11 +297,10 @@ fn test_edit_up_ss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_up_mmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: Duration::from_secs(60),
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -335,11 +316,10 @@ fn test_edit_up_mmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_up_hhmmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: Duration::from_secs(3600),
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -357,11 +337,10 @@ fn test_edit_up_hhmmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_down_ss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: ONE_SECOND,
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -378,11 +357,10 @@ fn test_edit_down_ss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_down_mmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: Duration::from_secs(120),
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
@@ -401,11 +379,10 @@ fn test_edit_down_mmss() {
|
||||
|
||||
#[test]
|
||||
fn test_edit_down_hhmmss() {
|
||||
let mut c = Clock::<Timer>::new(ClockArgs {
|
||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: Duration::from_secs(3600),
|
||||
tick_value: ONE_DECI_SECOND,
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,57 +5,99 @@ 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,
|
||||
duration::DurationEx,
|
||||
events::{Event, EventHandler},
|
||||
utils::center,
|
||||
widgets::clock::{self, Clock, ClockWidget},
|
||||
widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode},
|
||||
};
|
||||
|
||||
/// State for Countdown Widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Countdown {
|
||||
clock: Clock<clock::Countdown>,
|
||||
pub struct CountdownState {
|
||||
/// 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 {
|
||||
pub const fn new(clock: Clock<clock::Countdown>) -> Self {
|
||||
Self { clock }
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: Style) {
|
||||
self.clock.style = style;
|
||||
impl CountdownState {
|
||||
pub fn new(clock: ClockState<clock::Countdown>, elapsed_value: Duration) -> Self {
|
||||
Self {
|
||||
clock,
|
||||
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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> {
|
||||
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.elapsed_clock.tick();
|
||||
if self.elapsed_clock.is_initial() {
|
||||
self.elapsed_clock.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Key(key) => match key.code {
|
||||
KeyCode::Char('r') => {
|
||||
// reset both clocks
|
||||
self.clock.reset();
|
||||
self.elapsed_clock.reset();
|
||||
}
|
||||
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') => {
|
||||
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 => {
|
||||
self.clock.edit_next();
|
||||
@@ -65,9 +107,13 @@ impl EventHandler for Countdown {
|
||||
}
|
||||
KeyCode::Up if edit_mode => {
|
||||
self.clock.edit_up();
|
||||
// whenever `clock`'s value is changed, reset `elapsed_clock`
|
||||
self.elapsed_clock.reset();
|
||||
}
|
||||
KeyCode::Down if edit_mode => {
|
||||
self.clock.edit_down();
|
||||
// whenever clock value is changed, reset timer
|
||||
self.elapsed_clock.reset();
|
||||
}
|
||||
_ => return Some(event),
|
||||
},
|
||||
@@ -77,13 +123,38 @@ impl EventHandler for Countdown {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CountdownWidget;
|
||||
pub struct Countdown {
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl StatefulWidget for CountdownWidget {
|
||||
type State = Countdown;
|
||||
impl StatefulWidget for Countdown {
|
||||
type State = CountdownState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let clock = ClockWidget::new();
|
||||
let label = Line::raw((format!("Countdown {}", state.clock.get_mode())).to_uppercase());
|
||||
let clock = ClockWidget::new(self.style);
|
||||
|
||||
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(
|
||||
area,
|
||||
|
||||
@@ -1,25 +1,57 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::common::Content;
|
||||
use crate::common::{AppTime, AppTimeFormat, Content};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
symbols::{border, scrollbar},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Cell, Row, Table, Widget},
|
||||
widgets::{Block, Borders, Cell, Row, StatefulWidget, Table, Widget},
|
||||
};
|
||||
|
||||
#[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 show_menu: bool,
|
||||
pub running_clock: bool,
|
||||
pub selected_content: Content,
|
||||
pub edit_mode: bool,
|
||||
pub app_time: AppTime,
|
||||
}
|
||||
|
||||
impl Widget for Footer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
impl StatefulWidget for Footer {
|
||||
type State = FooterState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let content_labels: BTreeMap<Content, &str> = BTreeMap::from([
|
||||
(Content::Countdown, "[c]ountdown"),
|
||||
(Content::Timer, "[t]imer"),
|
||||
@@ -31,15 +63,25 @@ impl Widget for Footer {
|
||||
|
||||
let [border_area, menu_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Percentage(100)]).areas(area);
|
||||
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.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)
|
||||
.render(border_area, buf);
|
||||
// show menu
|
||||
if self.show_menu {
|
||||
if state.show_menu {
|
||||
let content_labels: Vec<Span> = content_labels
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -60,7 +102,7 @@ impl Widget for Footer {
|
||||
|
||||
const SPACE: &str = " "; // 2 empty spaces
|
||||
let widths = [Constraint::Length(12), Constraint::Percentage(100)];
|
||||
Table::new(
|
||||
let table = Table::new(
|
||||
[
|
||||
// content
|
||||
Row::new(vec![
|
||||
@@ -80,6 +122,14 @@ impl Widget for Footer {
|
||||
Span::from("[,]change style"),
|
||||
Span::from(SPACE),
|
||||
Span::from("[.]toggle deciseconds"),
|
||||
Span::from(SPACE),
|
||||
Span::from(format!(
|
||||
"[:]toggle {} time",
|
||||
match self.app_time {
|
||||
AppTime::Local(_) => "local",
|
||||
AppTime::Utc(_) => "utc",
|
||||
}
|
||||
)),
|
||||
])),
|
||||
]),
|
||||
// edit
|
||||
@@ -128,8 +178,9 @@ impl Widget for Footer {
|
||||
],
|
||||
widths,
|
||||
)
|
||||
.column_spacing(1)
|
||||
.render(menu_area, buf);
|
||||
.column_spacing(1);
|
||||
|
||||
Widget::render(table, menu_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
constants::TICK_VALUE_MS,
|
||||
events::{Event, EventHandler},
|
||||
utils::center,
|
||||
widgets::clock::{Clock, ClockWidget, Countdown},
|
||||
widgets::clock::{ClockState, ClockWidget, Countdown},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
@@ -18,7 +18,7 @@ use strum::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::clock::ClockArgs;
|
||||
use super::clock::ClockStateArgs;
|
||||
|
||||
#[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub enum Mode {
|
||||
@@ -28,18 +28,18 @@ pub enum Mode {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClockMap {
|
||||
work: Clock<Countdown>,
|
||||
pause: Clock<Countdown>,
|
||||
work: ClockState<Countdown>,
|
||||
pause: ClockState<Countdown>,
|
||||
}
|
||||
|
||||
impl ClockMap {
|
||||
fn get_mut(&mut self, mode: &Mode) -> &mut Clock<Countdown> {
|
||||
fn get_mut(&mut self, mode: &Mode) -> &mut ClockState<Countdown> {
|
||||
match mode {
|
||||
Mode::Work => &mut self.work,
|
||||
Mode::Pause => &mut self.pause,
|
||||
}
|
||||
}
|
||||
fn get(&self, mode: &Mode) -> &Clock<Countdown> {
|
||||
fn get(&self, mode: &Mode) -> &ClockState<Countdown> {
|
||||
match mode {
|
||||
Mode::Work => &self.work,
|
||||
Mode::Pause => &self.pause,
|
||||
@@ -48,66 +48,62 @@ impl ClockMap {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pomodoro {
|
||||
pub struct PomodoroState {
|
||||
mode: Mode,
|
||||
clock_map: ClockMap,
|
||||
}
|
||||
|
||||
pub struct PomodoroArgs {
|
||||
pub struct PomodoroStateArgs {
|
||||
pub mode: Mode,
|
||||
pub initial_value_work: Duration,
|
||||
pub current_value_work: Duration,
|
||||
pub initial_value_pause: Duration,
|
||||
pub current_value_pause: Duration,
|
||||
pub style: Style,
|
||||
pub with_decis: bool,
|
||||
}
|
||||
|
||||
impl Pomodoro {
|
||||
pub fn new(args: PomodoroArgs) -> Self {
|
||||
let PomodoroArgs {
|
||||
impl PomodoroState {
|
||||
pub fn new(args: PomodoroStateArgs) -> Self {
|
||||
let PomodoroStateArgs {
|
||||
mode,
|
||||
initial_value_work,
|
||||
current_value_work,
|
||||
initial_value_pause,
|
||||
current_value_pause,
|
||||
style,
|
||||
with_decis,
|
||||
} = args;
|
||||
Self {
|
||||
mode,
|
||||
clock_map: ClockMap {
|
||||
work: Clock::<Countdown>::new(ClockArgs {
|
||||
work: ClockState::<Countdown>::new(ClockStateArgs {
|
||||
initial_value: initial_value_work,
|
||||
current_value: current_value_work,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
style,
|
||||
with_decis,
|
||||
}),
|
||||
pause: Clock::<Countdown>::new(ClockArgs {
|
||||
pause: ClockState::<Countdown>::new(ClockStateArgs {
|
||||
initial_value: initial_value_pause,
|
||||
current_value: current_value_pause,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
style,
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn get_clock(&self) -> &Clock<Countdown> {
|
||||
pub fn get_clock(&self) -> &ClockState<Countdown> {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn get_clock_pause(&self) -> &Clock<Countdown> {
|
||||
pub fn get_clock_pause(&self) -> &ClockState<Countdown> {
|
||||
&self.clock_map.pause
|
||||
}
|
||||
|
||||
@@ -115,11 +111,6 @@ impl Pomodoro {
|
||||
&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) {
|
||||
self.clock_map.work.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> {
|
||||
let edit_mode = self.get_clock().is_edit_mode();
|
||||
match event {
|
||||
@@ -177,12 +168,14 @@ impl EventHandler for Pomodoro {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PomodoroWidget;
|
||||
pub struct PomodoroWidget {
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl StatefulWidget for PomodoroWidget {
|
||||
type State = Pomodoro;
|
||||
type State = PomodoroState;
|
||||
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(
|
||||
(format!(
|
||||
"Pomodoro {} {}",
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
common::Style,
|
||||
events::{Event, EventHandler},
|
||||
utils::center,
|
||||
widgets::clock::{self, Clock, ClockWidget},
|
||||
widgets::clock::{self, ClockState, ClockWidget},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
@@ -14,29 +14,25 @@ use ratatui::{
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timer {
|
||||
clock: Clock<clock::Timer>,
|
||||
pub struct TimerState {
|
||||
clock: ClockState<clock::Timer>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub const fn new(clock: Clock<clock::Timer>) -> Self {
|
||||
impl TimerState {
|
||||
pub const fn new(clock: ClockState<clock::Timer>) -> Self {
|
||||
Self { clock }
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: Style) {
|
||||
self.clock.style = style;
|
||||
}
|
||||
|
||||
pub fn set_with_decis(&mut self, with_decis: bool) {
|
||||
self.clock.with_decis = with_decis;
|
||||
}
|
||||
|
||||
pub fn get_clock(&self) -> &Clock<clock::Timer> {
|
||||
pub fn get_clock(&self) -> &ClockState<clock::Timer> {
|
||||
&self.clock
|
||||
}
|
||||
}
|
||||
|
||||
impl EventHandler for Timer {
|
||||
impl EventHandler for TimerState {
|
||||
fn update(&mut self, event: Event) -> Option<Event> {
|
||||
let edit_mode = self.clock.is_edit_mode();
|
||||
match event {
|
||||
@@ -73,13 +69,15 @@ impl EventHandler for Timer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TimerWidget;
|
||||
pub struct Timer {
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl StatefulWidget for &TimerWidget {
|
||||
type State = Timer;
|
||||
impl StatefulWidget for Timer {
|
||||
type State = TimerState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
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 area = center(
|
||||
|
||||
Reference in New Issue
Block a user