Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d542799f6 | ||
|
|
cc656b6ffe | ||
|
|
936d82eb94 |
4
.gitignore
vendored
@@ -21,7 +21,3 @@ result/**/*
|
|||||||
|
|
||||||
# ignore (possible) sound files
|
# ignore (possible) sound files
|
||||||
**/*.{mp3,wav}
|
**/*.{mp3,wav}
|
||||||
|
|
||||||
|
|
||||||
CLAUDE.md
|
|
||||||
.claude
|
|
||||||
|
|||||||
16
CHANGELOG.md
@@ -1,21 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [unreleased]
|
|
||||||
|
|
||||||
### Misc.
|
|
||||||
|
|
||||||
- (deps) Rust 1.90.0 [#95](https://github.com/sectore/timr-tui/pull/95)
|
|
||||||
|
|
||||||
## v1.4.0 - 2025-09-02
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- (screen): Local Time [#89](https://github.com/sectore/timr-tui/pull/89), [#90](https://github.com/sectore/timr-tui/pull/90), [#91](https://github.com/sectore/timr-tui/pull/91)
|
|
||||||
|
|
||||||
### Misc.
|
|
||||||
|
|
||||||
- (deps) Rust 1.89.0 [#87](https://github.com/sectore/timr-tui/pull/87)
|
|
||||||
|
|
||||||
## v1.3.1 - 2025-07-03
|
## v1.3.1 - 2025-07-03
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
Any feedback / contribution are welcome. Just open an `issue`, a `PR` or start a `discussion`.
|
|
||||||
|
|
||||||
## Code style / conventions
|
|
||||||
|
|
||||||
- Try to write [clean, idiomatic Rust code](https://github.com/mre/idiomatic-rust).
|
|
||||||
- Keep code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) whenever it makes sense.
|
|
||||||
- Before pushing any code make sure to run `clippy` and `fmt`. Check provided [`just`](./jusfile) file to run such commands, [CI](https://github.com/sectore/timr-tui/blob/main/.github/workflows/ci.yml) will do the same.
|
|
||||||
- Have fun to write code.
|
|
||||||
|
|
||||||
## Design files
|
|
||||||
|
|
||||||
Use [Figma Design file](https://www.figma.com/community/file/1553076532392275586/timr-tui) to suggest design changes
|
|
||||||
|
|
||||||
## AI
|
|
||||||
|
|
||||||
Always understand what AI provides to you. Never push any code based on [`vibe coding`](https://en.wikipedia.org/wiki/Vibe_coding) you or anybody else can't follow. Make sure your agent still follows all code styles and conventions suggested above. Use AI for better, not for worse code.
|
|
||||||
699
Cargo.lock
generated
25
Cargo.toml
@@ -1,24 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "timr-tui"
|
name = "timr-tui"
|
||||||
version = "1.4.0"
|
version = "1.3.1"
|
||||||
description = "TUI to organize your time: Pomodoro, Countdown, Timer."
|
description = "TUI to organize your time: Pomodoro, Countdown, Timer."
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
# Reminder: Always keep `channel` in `rust-toolchain.toml` in sync with `rust-version`.
|
# Reminder: Always keep `channel` in `rust-toolchain.toml` in sync with `rust-version`.
|
||||||
rust-version = "1.90.0"
|
rust-version = "1.88.0"
|
||||||
homepage = "https://github.com/sectore/timr-tui"
|
homepage = "https://github.com/sectore/timr-tui"
|
||||||
repository = "https://github.com/sectore/timr-tui"
|
repository = "https://github.com/sectore/timr-tui"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = ["tui", "timer", "countdown", "pomodoro"]
|
keywords = ["tui", "timer", "countdown", "pomodoro"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
exclude = [
|
exclude = [".github/*", "demo/*.tape", "result/*", "*.mp3"]
|
||||||
".github/*",
|
|
||||||
"demo/*.tape",
|
|
||||||
"result/*",
|
|
||||||
"*.mp3",
|
|
||||||
".claude",
|
|
||||||
"CLAUDE.md",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
@@ -28,20 +21,20 @@ futures = "0.3"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
strum = { version = "0.26.3", features = ["derive"] }
|
strum = { version = "0.26.3", features = ["derive"] }
|
||||||
tokio = { version = "1.47.1", features = ["full"] }
|
tokio = { version = "1.45.1", features = ["full"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.16"
|
tokio-util = "0.7.15"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
clap = { version = "4.5.48", features = ["derive"] }
|
clap = { version = "4.5.40", features = ["derive"] }
|
||||||
time = { version = "0.3.44", features = ["formatting", "local-offset", "parsing", "macros"] }
|
time = { version = "0.3.41", features = ["formatting", "local-offset"] }
|
||||||
notify-rust = "4.11.7"
|
notify-rust = "4.11.7"
|
||||||
rodio = { version = "0.20.1", features = [
|
rodio = { version = "0.20.1", features = [
|
||||||
"symphonia-mp3",
|
"symphonia-mp3",
|
||||||
"symphonia-wav",
|
"symphonia-wav",
|
||||||
], default-features = false, optional = true }
|
], default-features = false, optional = true }
|
||||||
thiserror = { version = "2.0.17", optional = true }
|
thiserror = { version = "2.0.12", optional = true }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
99
README.md
@@ -17,7 +17,6 @@ Built with [Ratatui](https://ratatui.rs/) / [Rust 🦀](https://www.rust-lang.or
|
|||||||
- [Installation](./#installation)
|
- [Installation](./#installation)
|
||||||
- [Development](./#development)
|
- [Development](./#development)
|
||||||
- [Misc](./#misc)
|
- [Misc](./#misc)
|
||||||
- [Contributing](./#contributing)
|
|
||||||
- [License](./#license)
|
- [License](./#license)
|
||||||
|
|
||||||
# Preview
|
# Preview
|
||||||
@@ -42,23 +41,10 @@ _(theme depends on your terminal preferences)_
|
|||||||
<img alt="countdown" src="demo/countdown.gif" />
|
<img alt="countdown" src="demo/countdown.gif" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Countdown: Mission Elapsed Time ([MET](https://en.wikipedia.org/wiki/Mission_Elapsed_Time))
|
## Change style
|
||||||
|
|
||||||
<a href="demo/countdown-met.gif">
|
<a href="demo/style.gif">
|
||||||
<img alt="menu" src="demo/countdown-met.gif" />
|
<img alt="style" src="demo/style.gif" />
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
## Local time
|
|
||||||
|
|
||||||
<a href="demo/local-time.gif">
|
|
||||||
<img alt="menu" src="demo/local-time.gif" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Local time (footer)
|
|
||||||
|
|
||||||
<a href="demo/local-time-footer.gif">
|
|
||||||
<img alt="menu" src="demo/local-time-footer.gif" />
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Toggle deciseconds
|
## Toggle deciseconds
|
||||||
@@ -67,18 +53,24 @@ _(theme depends on your terminal preferences)_
|
|||||||
<img alt="deciseconds" src="demo/decis.gif" />
|
<img alt="deciseconds" src="demo/decis.gif" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Change style
|
|
||||||
|
|
||||||
<a href="demo/style.gif">
|
|
||||||
<img alt="style" src="demo/style.gif" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Menu
|
## Menu
|
||||||
|
|
||||||
<a href="demo/menu.gif">
|
<a href="demo/menu.gif">
|
||||||
<img alt="menu" src="demo/menu.gif" />
|
<img alt="menu" src="demo/menu.gif" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## Local time (footer)
|
||||||
|
|
||||||
|
<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
|
||||||
@@ -87,34 +79,19 @@ timr-tui --help
|
|||||||
Usage: timr-tui [OPTIONS]
|
Usage: timr-tui [OPTIONS]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-c, --countdown <COUNTDOWN>
|
-c, --countdown <COUNTDOWN> Countdown time to start from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'
|
||||||
Countdown time to start from. Formats: 'Yy Dd hh:mm:ss', 'Dd hh:mm:ss', 'Yy mm:ss', 'Dd mm:ss', 'Yy ss', 'Dd ss', 'hh:mm:ss', 'mm:ss', 'ss'. Examples: '1y 5d 10:30:00', '2d 4:00', '1d 10', '5:03'.
|
-w, --work <WORK> Work time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'
|
||||||
--countdown-target <COUNTDOWN_TARGET>
|
-p, --pause <PAUSE> Pause time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'
|
||||||
Countdown targeting a specific time in the future or past. Formats: 'yyyy-mm-dd hh:mm:ss', 'yyyy-mm-dd hh:mm', 'hh:mm:ss', 'hh:mm', 'mm' [aliases: --ct]
|
-d, --decis Show deciseconds.
|
||||||
-w, --work <WORK>
|
-m, --mode <MODE> Mode to start with. [possible values: countdown, timer, pomodoro]
|
||||||
Work time to count down from. Formats: 'ss', 'mm:ss', 'hh:mm:ss'
|
-s, --style <STYLE> Style to display time with. [possible values: full, light, medium, dark, thick, cross, braille]
|
||||||
-p, --pause <PAUSE>
|
--menu Open the menu.
|
||||||
Pause time to count down from. Formats: 'ss', 'mm:ss', 'hh:mm:ss'
|
-r, --reset Reset stored values to default values.
|
||||||
-d, --decis
|
-n, --notification <NOTIFICATION> Toggle desktop notifications. Experimental. [possible values: on, off]
|
||||||
Show deciseconds.
|
--blink <BLINK> Toggle blink mode to animate a clock when it reaches its finished mode. [possible values: on, off]
|
||||||
-m, --mode <MODE>
|
--log [<LOG>] Directory to store log file. If not set, standard application log directory is used (check README for details).
|
||||||
Mode to start with. [possible values: countdown, timer, pomodoro, localtime]
|
-h, --help Print help
|
||||||
-s, --style <STYLE>
|
-V, --version Print version
|
||||||
Style to display time with. [possible values: full, light, medium, dark, thick, cross, braille]
|
|
||||||
--menu
|
|
||||||
Open menu.
|
|
||||||
-r, --reset
|
|
||||||
Reset stored values to defaults.
|
|
||||||
-n, --notification <NOTIFICATION>
|
|
||||||
Toggle desktop notifications. Experimental. [possible values: on, off]
|
|
||||||
--blink <BLINK>
|
|
||||||
Toggle blink mode to animate a clock when it reaches its finished mode. [possible values: on, off]
|
|
||||||
--log [<LOG>]
|
|
||||||
Directory for log file. If not set, standard application log directory is used (check README for details).
|
|
||||||
-h, --help
|
|
||||||
Print help
|
|
||||||
-V, --version
|
|
||||||
Print version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Extra option (if `--features sound` is enabled by local build only):
|
Extra option (if `--features sound` is enabled by local build only):
|
||||||
@@ -138,7 +115,6 @@ Extra option (if `--features sound` is enabled by local build only):
|
|||||||
| <kbd>p</kbd> | Pomodoro |
|
| <kbd>p</kbd> | Pomodoro |
|
||||||
| <kbd>c</kbd> | Countdown |
|
| <kbd>c</kbd> | Countdown |
|
||||||
| <kbd>t</kbd> | Timer |
|
| <kbd>t</kbd> | Timer |
|
||||||
| <kbd>l</kbd> | Local Time |
|
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
@@ -157,24 +133,22 @@ Extra option (if `--features sound` is enabled by local build only):
|
|||||||
| <kbd>Esc</kbd> | skip changes |
|
| <kbd>Esc</kbd> | skip changes |
|
||||||
| <kbd>←</kbd> or <kbd>→</kbd> | change selection |
|
| <kbd>←</kbd> or <kbd>→</kbd> | change selection |
|
||||||
| <kbd>↑</kbd> | edit to go up |
|
| <kbd>↑</kbd> | edit to go up |
|
||||||
| <kbd>ctrl+↑</kbd> | edit to go up 10x |
|
|
||||||
| <kbd>↓</kbd> | edit to go down |
|
| <kbd>↓</kbd> | edit to go down |
|
||||||
| <kbd>ctrl+↓</kbd> | edit to go down 10x |
|
|
||||||
|
|
||||||
**In `Pomodoro` screen only:**
|
**In `Pomodoro` screen only**
|
||||||
|
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| <kbd>←</kbd> or <kbd>→</kbd> | switch work/pause |
|
| <kbd>←</kbd> or <kbd>→</kbd> | switch work/pause |
|
||||||
| <kbd>ctrl+r</kbd> | reset round |
|
| <kbd>^r</kbd> | reset round |
|
||||||
| <kbd>ctrl+s</kbd> | save initial value |
|
| <kbd>^s</kbd> | save initial value |
|
||||||
|
|
||||||
**In `Countdown` screen only:**
|
**In `Countdown` screen only:**
|
||||||
|
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| <kbd>ctrl+e</kbd> | edit by local time |
|
| <kbd>^e</kbd> | edit by local time |
|
||||||
| <kbd>ctrl+s</kbd> | save initial value |
|
| <kbd>^s</kbd> | save initial value |
|
||||||
|
|
||||||
## Appearance
|
## Appearance
|
||||||
|
|
||||||
@@ -182,7 +156,7 @@ Extra option (if `--features sound` is enabled by local build only):
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| <kbd>,</kbd> | toggle styles |
|
| <kbd>,</kbd> | toggle styles |
|
||||||
| <kbd>.</kbd> | toggle deciseconds |
|
| <kbd>.</kbd> | toggle deciseconds |
|
||||||
| <kbd>:</kbd> | toggle local time |
|
| <kbd>:</kbd> | toggle local time in footer |
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@@ -246,7 +220,6 @@ Available recipes:
|
|||||||
demo-countdown-met # build demo: countdown + met [alias: dcm]
|
demo-countdown-met # build demo: countdown + met [alias: dcm]
|
||||||
demo-decis # build demo: deciseconds [alias: dd]
|
demo-decis # build demo: deciseconds [alias: dd]
|
||||||
demo-local-time # build demo: local time [alias: dlt]
|
demo-local-time # build demo: local time [alias: dlt]
|
||||||
demo-local-time-footer # build demo: local time (footer) [alias: dltf]
|
|
||||||
demo-menu # build demo: menu [alias: dm]
|
demo-menu # build demo: menu [alias: dm]
|
||||||
demo-pomodoro # build demo: pomodoro [alias: dp]
|
demo-pomodoro # build demo: pomodoro [alias: dp]
|
||||||
demo-rocket-countdown # build demo: rocket countdown [alias: drc]
|
demo-rocket-countdown # build demo: rocket countdown [alias: drc]
|
||||||
@@ -321,10 +294,6 @@ C:/Users/{user}/AppData/Local/timr-tui/logs/app.log
|
|||||||
|
|
||||||
Optional: You can use a custom directory by passing it via `--log` arg.
|
Optional: You can use a custom directory by passing it via `--log` arg.
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
[CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
[MIT License](./LICENSE)
|
[MIT License](./LICENSE)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,20 +0,0 @@
|
|||||||
Output demo/countdown-target-future.gif
|
|
||||||
|
|
||||||
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
|
|
||||||
Set Theme "SeaShells"
|
|
||||||
|
|
||||||
Set FontSize 14
|
|
||||||
Set Width 1000
|
|
||||||
Set Height 500
|
|
||||||
Set Padding 0
|
|
||||||
Set Margin 1
|
|
||||||
|
|
||||||
# --- START ---
|
|
||||||
Set LoopOffset 4
|
|
||||||
Hide
|
|
||||||
Type "cargo run -- -r -m c --ct '2030-01-10 18:00'"
|
|
||||||
Enter
|
|
||||||
Type "m"
|
|
||||||
Sleep 0.2
|
|
||||||
Show
|
|
||||||
Sleep 0.1
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,20 +0,0 @@
|
|||||||
Output demo/countdown-target-past.gif
|
|
||||||
|
|
||||||
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
|
|
||||||
Set Theme "seoulbones_light"
|
|
||||||
|
|
||||||
Set FontSize 14
|
|
||||||
Set Width 1000
|
|
||||||
Set Height 500
|
|
||||||
Set Padding 0
|
|
||||||
Set Margin 1
|
|
||||||
|
|
||||||
# --- START ---
|
|
||||||
Set LoopOffset 4
|
|
||||||
Hide
|
|
||||||
Type "cargo run -- -r -m c --ct '2024-01-10 18:00'"
|
|
||||||
Enter
|
|
||||||
Type "m"
|
|
||||||
Sleep 0.2
|
|
||||||
Show
|
|
||||||
Sleep 0.1
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,20 +0,0 @@
|
|||||||
Output demo/local-time-date.gif
|
|
||||||
|
|
||||||
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
|
|
||||||
Set Theme "WarmNeon"
|
|
||||||
|
|
||||||
Set FontSize 14
|
|
||||||
Set Width 1000
|
|
||||||
Set Height 500
|
|
||||||
Set Padding 0
|
|
||||||
Set Margin 1
|
|
||||||
|
|
||||||
# --- START ---
|
|
||||||
Set LoopOffset 4
|
|
||||||
Hide
|
|
||||||
Type "cargo run -- -r -m l"
|
|
||||||
Enter
|
|
||||||
Type "m"
|
|
||||||
Sleep 0.2
|
|
||||||
Show
|
|
||||||
Sleep 0.1
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,20 +0,0 @@
|
|||||||
Output demo/local-time-footer.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
|
|
||||||
# --- toggle local time ---
|
|
||||||
Type@1.5s ":::"
|
|
||||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,7 +1,7 @@
|
|||||||
Output demo/local-time.gif
|
Output demo/local-time.gif
|
||||||
|
|
||||||
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
|
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
|
||||||
Set Theme "Atom"
|
Set Theme "AtomOneLight"
|
||||||
|
|
||||||
Set FontSize 14
|
Set FontSize 14
|
||||||
Set Width 800
|
Set Width 800
|
||||||
@@ -12,9 +12,11 @@ Set Margin 1
|
|||||||
# --- START ---
|
# --- START ---
|
||||||
Set LoopOffset 4
|
Set LoopOffset 4
|
||||||
Hide
|
Hide
|
||||||
Type "cargo run -- -m l"
|
Type "cargo run -- -m c"
|
||||||
Enter
|
Enter
|
||||||
Sleep 0.2
|
Sleep 0.2
|
||||||
Show
|
Show
|
||||||
|
Sleep 1
|
||||||
# --- toggle local time ---
|
# --- toggle local time ---
|
||||||
Type@1.5s ":::"
|
Type@1.5s ":::"
|
||||||
|
Sleep 1.5
|
||||||
|
|||||||
BIN
demo/maximum.gif
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,41 +0,0 @@
|
|||||||
Output demo/maximum.gif
|
|
||||||
|
|
||||||
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
|
|
||||||
Set Theme "C64"
|
|
||||||
|
|
||||||
Set FontSize 14
|
|
||||||
Set Width 1000
|
|
||||||
Set Height 500
|
|
||||||
Set Padding 0
|
|
||||||
Set Margin 1
|
|
||||||
|
|
||||||
# --- START ---
|
|
||||||
Set LoopOffset 4
|
|
||||||
Hide
|
|
||||||
Type "cargo run -- -r -m timer"
|
|
||||||
Enter
|
|
||||||
Sleep 0.2
|
|
||||||
Type "m"
|
|
||||||
Type "e"
|
|
||||||
# secs
|
|
||||||
Up@1ms 60
|
|
||||||
Left
|
|
||||||
# mins
|
|
||||||
Up@1ms 59
|
|
||||||
Left
|
|
||||||
# hours
|
|
||||||
Up@1ms 23
|
|
||||||
Left
|
|
||||||
# days
|
|
||||||
Up@1ms 364
|
|
||||||
Right@1ms 3
|
|
||||||
Down@1ms 1
|
|
||||||
Left@1ms 4
|
|
||||||
# years
|
|
||||||
Up@1ms 998
|
|
||||||
Right
|
|
||||||
# days
|
|
||||||
Up@1ms 365
|
|
||||||
Type@1ms "s"
|
|
||||||
Show
|
|
||||||
Sleep 0.1
|
|
||||||
24
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1758758545,
|
"lastModified": 1750266157,
|
||||||
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
|
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
|
"rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1758782550,
|
"lastModified": 1751092526,
|
||||||
"narHash": "sha256-olCvyP5r6+HQTl2EUudtjlA5UammsBpkzAl0l9+utZc=",
|
"narHash": "sha256-vmbu97JXqr9/sTWR5XRh646jkp8a0J9m0o6JIQTdjE4=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "32f4e350c03cc5762be811e9c700e8696cd13c02",
|
"rev": "6643d56d9a78afa157b577862c220298c09b891d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1758690382,
|
"lastModified": 1750776420,
|
||||||
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
|
"narHash": "sha256-/CG+w0o0oJ5itVklOoLbdn2dGB0wbZVOoDm4np6w09A=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
|
"rev": "30a61f056ac492e3b7cdcb69c1e6abdcf00e39cf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -81,11 +81,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1758620797,
|
"lastModified": 1750871759,
|
||||||
"narHash": "sha256-Ly4rHgrixFMBnkbMursVt74mxnntnE6yVdF5QellJ+A=",
|
"narHash": "sha256-hMNZXMtlhfjQdu1F4Fa/UFiMoXdZag4cider2R9a648=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "905641f3520230ad6ef421bcf5da9c6b49f2479b",
|
"rev": "317542c1e4a3ec3467d21d1c25f6a43b80d83e7d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
13
flake.nix
@@ -24,7 +24,7 @@
|
|||||||
{
|
{
|
||||||
file = ./rust-toolchain.toml;
|
file = ./rust-toolchain.toml;
|
||||||
# sha256 = nixpkgs.lib.fakeSha256;
|
# sha256 = nixpkgs.lib.fakeSha256;
|
||||||
sha256 = "sha256-SJwZ8g0zF2WrKDVmHrVG3pD2RGoQeo24MEXnNx5FyuI=";
|
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
|
||||||
};
|
};
|
||||||
|
|
||||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||||
@@ -85,18 +85,15 @@
|
|||||||
pkgs.nixd
|
pkgs.nixd
|
||||||
pkgs.alejandra
|
pkgs.alejandra
|
||||||
]
|
]
|
||||||
# pkgs needed to play sound on Linux
|
# some extra pkgs needed to play sound on Linux
|
||||||
++ lib.optionals stdenv.isLinux [
|
++ lib.optionals stdenv.isLinux [
|
||||||
pkgs.pkg-config
|
pkgs.pkg-config
|
||||||
pkgs.pipewire
|
(pkgs.alsa-lib-with-plugins.override {
|
||||||
pkgs.alsa-lib
|
plugins = [pkgs.alsa-plugins pkgs.pipewire];
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
inherit (commonArgs) src;
|
inherit (commonArgs) src;
|
||||||
|
|
||||||
# Environment variables needed discover ALSA/PipeWire properly on Linux
|
|
||||||
LD_LIBRARY_PATH = lib.optionalString stdenv.isLinux "${pkgs.alsa-lib}/lib:${pkgs.pipewire}/lib";
|
|
||||||
ALSA_PLUGIN_DIR = lib.optionalString stdenv.isLinux "${pkgs.pipewire}/lib/alsa-lib";
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
7
justfile
@@ -121,13 +121,6 @@ alias dlt := demo-local-time
|
|||||||
demo-local-time:
|
demo-local-time:
|
||||||
vhs demo/local-time.tape
|
vhs demo/local-time.tape
|
||||||
|
|
||||||
alias dltf := demo-local-time-footer
|
|
||||||
|
|
||||||
# build demo: local time (footer)
|
|
||||||
[group('demo')]
|
|
||||||
demo-local-time-footer:
|
|
||||||
vhs demo/local-time-footer.tape
|
|
||||||
|
|
||||||
alias drc := demo-rocket-countdown
|
alias drc := demo-rocket-countdown
|
||||||
|
|
||||||
# build demo: rocket countdown
|
# build demo: rocket countdown
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
# Reminder: Always keep `rust-version` in `Cargo.toml` in sync with `channel`.
|
# Reminder: Always keep `rust-version` in `Cargo.toml` in sync with `channel`.
|
||||||
channel = "1.90.0"
|
channel = "1.88.0"
|
||||||
components = ["clippy", "rustfmt", "rust-src", "rust-analyzer"]
|
components = ["clippy", "rustfmt", "rust-src", "rust-analyzer"]
|
||||||
targets = ["x86_64-pc-windows-gnu", "x86_64-unknown-linux-musl"]
|
targets = ["x86_64-pc-windows-gnu", "x86_64-unknown-linux-musl"]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
|
|||||||
107
src/app.rs
@@ -2,7 +2,6 @@ use crate::{
|
|||||||
args::Args,
|
args::Args,
|
||||||
common::{AppEditMode, AppTime, AppTimeFormat, ClockTypeId, Content, Style, Toggle},
|
common::{AppEditMode, AppTime, AppTimeFormat, ClockTypeId, Content, Style, Toggle},
|
||||||
constants::TICK_VALUE_MS,
|
constants::TICK_VALUE_MS,
|
||||||
duration::DirectedDuration,
|
|
||||||
events::{self, TuiEventHandler},
|
events::{self, TuiEventHandler},
|
||||||
storage::AppStorage,
|
storage::AppStorage,
|
||||||
terminal::Terminal,
|
terminal::Terminal,
|
||||||
@@ -11,7 +10,6 @@ use crate::{
|
|||||||
countdown::{Countdown, CountdownState, CountdownStateArgs},
|
countdown::{Countdown, CountdownState, CountdownStateArgs},
|
||||||
footer::{Footer, FooterState},
|
footer::{Footer, FooterState},
|
||||||
header::Header,
|
header::Header,
|
||||||
local_time::{LocalTimeState, LocalTimeStateArgs, LocalTimeWidget},
|
|
||||||
pomodoro::{Mode as PomodoroMode, PomodoroState, PomodoroStateArgs, PomodoroWidget},
|
pomodoro::{Mode as PomodoroMode, PomodoroState, PomodoroStateArgs, PomodoroWidget},
|
||||||
timer::{Timer, TimerState},
|
timer::{Timer, TimerState},
|
||||||
},
|
},
|
||||||
@@ -29,6 +27,7 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use time::OffsetDateTime;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -45,11 +44,9 @@ pub struct App {
|
|||||||
#[allow(dead_code)] // w/ `--features sound` available only
|
#[allow(dead_code)] // w/ `--features sound` available only
|
||||||
sound_path: Option<PathBuf>,
|
sound_path: Option<PathBuf>,
|
||||||
app_time: AppTime,
|
app_time: AppTime,
|
||||||
app_time_format: AppTimeFormat,
|
|
||||||
countdown: CountdownState,
|
countdown: CountdownState,
|
||||||
timer: TimerState,
|
timer: TimerState,
|
||||||
pomodoro: PomodoroState,
|
pomodoro: PomodoroState,
|
||||||
local_time: LocalTimeState,
|
|
||||||
style: Style,
|
style: Style,
|
||||||
with_decis: bool,
|
with_decis: bool,
|
||||||
footer: FooterState,
|
footer: FooterState,
|
||||||
@@ -75,7 +72,6 @@ pub struct AppArgs {
|
|||||||
pub current_value_timer: Duration,
|
pub current_value_timer: Duration,
|
||||||
pub app_tx: events::AppEventTx,
|
pub app_tx: events::AppEventTx,
|
||||||
pub sound_path: Option<PathBuf>,
|
pub sound_path: Option<PathBuf>,
|
||||||
pub footer_toggle_app_time: Toggle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FromAppArgs {
|
pub struct FromAppArgs {
|
||||||
@@ -103,7 +99,7 @@ impl From<FromAppArgs> for App {
|
|||||||
None => {
|
None => {
|
||||||
if args.work.is_some() || args.pause.is_some() {
|
if args.work.is_some() || args.pause.is_some() {
|
||||||
Content::Pomodoro
|
Content::Pomodoro
|
||||||
} else if args.countdown.is_some() || args.countdown_target.is_some() {
|
} else if args.countdown.is_some() {
|
||||||
Content::Countdown
|
Content::Countdown
|
||||||
}
|
}
|
||||||
// in other case just use latest stored state
|
// in other case just use latest stored state
|
||||||
@@ -121,28 +117,13 @@ impl From<FromAppArgs> for App {
|
|||||||
initial_value_pause: args.pause.unwrap_or(stg.inital_value_pause),
|
initial_value_pause: args.pause.unwrap_or(stg.inital_value_pause),
|
||||||
// invalidate `current_value_pause` if an initial value is set via args
|
// invalidate `current_value_pause` if an initial value is set via args
|
||||||
current_value_pause: args.pause.unwrap_or(stg.current_value_pause),
|
current_value_pause: args.pause.unwrap_or(stg.current_value_pause),
|
||||||
initial_value_countdown: match (&args.countdown, &args.countdown_target) {
|
initial_value_countdown: args.countdown.unwrap_or(stg.inital_value_countdown),
|
||||||
(Some(d), _) => *d,
|
|
||||||
(None, Some(DirectedDuration::Until(d))) => *d,
|
|
||||||
// reset for values from "past"
|
|
||||||
(None, Some(DirectedDuration::Since(_))) => Duration::ZERO,
|
|
||||||
(None, None) => 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: match (&args.countdown, &args.countdown_target) {
|
current_value_countdown: args.countdown.unwrap_or(stg.current_value_countdown),
|
||||||
(Some(d), _) => *d,
|
elapsed_value_countdown: match args.countdown {
|
||||||
(None, Some(DirectedDuration::Until(d))) => *d,
|
// reset value if countdown is set by arguments
|
||||||
// `zero` makes values from `past` marked as `DONE`
|
Some(_) => Duration::ZERO,
|
||||||
(None, Some(DirectedDuration::Since(_))) => Duration::ZERO,
|
None => stg.elapsed_value_countdown,
|
||||||
(None, None) => stg.inital_value_countdown,
|
|
||||||
},
|
|
||||||
elapsed_value_countdown: match (args.countdown, args.countdown_target) {
|
|
||||||
// use `Since` duration
|
|
||||||
(_, Some(DirectedDuration::Since(d))) => d,
|
|
||||||
// reset values
|
|
||||||
(_, Some(_)) => Duration::ZERO,
|
|
||||||
(Some(_), _) => Duration::ZERO,
|
|
||||||
(_, _) => stg.elapsed_value_countdown,
|
|
||||||
},
|
},
|
||||||
current_value_timer: stg.current_value_timer,
|
current_value_timer: stg.current_value_timer,
|
||||||
app_tx,
|
app_tx,
|
||||||
@@ -150,11 +131,17 @@ impl From<FromAppArgs> for App {
|
|||||||
sound_path: args.sound,
|
sound_path: args.sound,
|
||||||
#[cfg(not(feature = "sound"))]
|
#[cfg(not(feature = "sound"))]
|
||||||
sound_path: None,
|
sound_path: None,
|
||||||
footer_toggle_app_time: stg.footer_app_time,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -177,9 +164,8 @@ impl App {
|
|||||||
blink,
|
blink,
|
||||||
sound_path,
|
sound_path,
|
||||||
app_tx,
|
app_tx,
|
||||||
footer_toggle_app_time,
|
|
||||||
} = args;
|
} = args;
|
||||||
let app_time = AppTime::new();
|
let app_time = get_app_time();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
mode: Mode::Running,
|
mode: Mode::Running,
|
||||||
@@ -188,7 +174,6 @@ impl App {
|
|||||||
sound_path,
|
sound_path,
|
||||||
content,
|
content,
|
||||||
app_time,
|
app_time,
|
||||||
app_time_format,
|
|
||||||
style,
|
style,
|
||||||
with_decis,
|
with_decis,
|
||||||
countdown: CountdownState::new(CountdownStateArgs {
|
countdown: CountdownState::new(CountdownStateArgs {
|
||||||
@@ -219,18 +204,7 @@ impl App {
|
|||||||
round: pomodoro_round,
|
round: pomodoro_round,
|
||||||
app_tx: app_tx.clone(),
|
app_tx: app_tx.clone(),
|
||||||
}),
|
}),
|
||||||
local_time: LocalTimeState::new(LocalTimeStateArgs {
|
footer: FooterState::new(show_menu, app_time_format),
|
||||||
app_time,
|
|
||||||
app_time_format,
|
|
||||||
}),
|
|
||||||
footer: FooterState::new(
|
|
||||||
show_menu,
|
|
||||||
if footer_toggle_app_time == Toggle::On {
|
|
||||||
Some(app_time_format)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,39 +221,8 @@ impl App {
|
|||||||
KeyCode::Char('c') => app.content = Content::Countdown,
|
KeyCode::Char('c') => app.content = Content::Countdown,
|
||||||
KeyCode::Char('t') => app.content = Content::Timer,
|
KeyCode::Char('t') => app.content = Content::Timer,
|
||||||
KeyCode::Char('p') => app.content = Content::Pomodoro,
|
KeyCode::Char('p') => app.content = Content::Pomodoro,
|
||||||
KeyCode::Char('l') => app.content = Content::LocalTime,
|
|
||||||
// toogle app time format
|
// toogle app time format
|
||||||
KeyCode::Char(':') => {
|
KeyCode::Char(':') => app.footer.toggle_app_time_format(),
|
||||||
if app.content == Content::LocalTime {
|
|
||||||
// For LocalTime content: just cycle through formats
|
|
||||||
app.app_time_format = app.app_time_format.next();
|
|
||||||
app.local_time.set_app_time_format(app.app_time_format);
|
|
||||||
// Only update footer if it's currently showing time
|
|
||||||
if app.footer.app_time_format().is_some() {
|
|
||||||
app.footer.set_app_time_format(Some(app.app_time_format));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For other content: allow footer to toggle between formats and None
|
|
||||||
let new_format = match app.footer.app_time_format() {
|
|
||||||
// footer is hidden -> show first format
|
|
||||||
None => Some(AppTimeFormat::first()),
|
|
||||||
Some(v) => {
|
|
||||||
if v != &AppTimeFormat::last() {
|
|
||||||
Some(v.next())
|
|
||||||
} else {
|
|
||||||
// reached last format -> hide footer time
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(format) = new_format {
|
|
||||||
app.app_time_format = format;
|
|
||||||
app.local_time.set_app_time_format(format);
|
|
||||||
}
|
|
||||||
app.footer.set_app_time_format(new_format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// toogle menu
|
// toogle menu
|
||||||
KeyCode::Char('m') => app.footer.set_show_menu(!app.footer.get_show_menu()),
|
KeyCode::Char('m') => app.footer.set_show_menu(!app.footer.get_show_menu()),
|
||||||
KeyCode::Char(',') => {
|
KeyCode::Char(',') => {
|
||||||
@@ -300,9 +243,8 @@ impl App {
|
|||||||
// Closure to handle `TuiEvent`'s
|
// Closure to handle `TuiEvent`'s
|
||||||
let mut handle_tui_events = |app: &mut Self, event: events::TuiEvent| -> Result<()> {
|
let mut handle_tui_events = |app: &mut Self, event: events::TuiEvent| -> Result<()> {
|
||||||
if matches!(event, events::TuiEvent::Tick) {
|
if matches!(event, events::TuiEvent::Tick) {
|
||||||
app.app_time = AppTime::new();
|
app.app_time = get_app_time();
|
||||||
app.countdown.set_app_time(app.app_time);
|
app.countdown.set_app_time(app.app_time);
|
||||||
app.local_time.set_app_time(app.app_time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
||||||
@@ -310,7 +252,6 @@ impl App {
|
|||||||
Content::Countdown => app.countdown.update(event.clone()),
|
Content::Countdown => app.countdown.update(event.clone()),
|
||||||
Content::Timer => app.timer.update(event.clone()),
|
Content::Timer => app.timer.update(event.clone()),
|
||||||
Content::Pomodoro => app.pomodoro.update(event.clone()),
|
Content::Pomodoro => app.pomodoro.update(event.clone()),
|
||||||
Content::LocalTime => app.local_time.update(event.clone()),
|
|
||||||
} {
|
} {
|
||||||
match unhandled {
|
match unhandled {
|
||||||
events::TuiEvent::Render | events::TuiEvent::Resize => {
|
events::TuiEvent::Render | events::TuiEvent::Resize => {
|
||||||
@@ -401,7 +342,6 @@ impl App {
|
|||||||
AppEditMode::None
|
AppEditMode::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Content::LocalTime => AppEditMode::None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,8 +350,6 @@ impl App {
|
|||||||
Content::Countdown => self.countdown.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(),
|
||||||
// `LocalTime` does not use a `Clock`
|
|
||||||
Content::LocalTime => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +358,6 @@ impl App {
|
|||||||
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
||||||
Content::Timer => None,
|
Content::Timer => None,
|
||||||
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
||||||
Content::LocalTime => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +374,7 @@ impl App {
|
|||||||
show_menu: self.footer.get_show_menu(),
|
show_menu: self.footer.get_show_menu(),
|
||||||
notification: self.notification,
|
notification: self.notification,
|
||||||
blink: self.blink,
|
blink: self.blink,
|
||||||
app_time_format: self.app_time_format,
|
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(),
|
||||||
@@ -456,7 +393,6 @@ impl App {
|
|||||||
),
|
),
|
||||||
elapsed_value_countdown: Duration::from(*self.countdown.get_elapsed_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()),
|
||||||
footer_app_time: self.footer.app_time_format().is_some().into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,9 +419,6 @@ impl AppWidget {
|
|||||||
blink: state.blink == Toggle::On,
|
blink: state.blink == Toggle::On,
|
||||||
}
|
}
|
||||||
.render(area, buf, &mut state.pomodoro),
|
.render(area, buf, &mut state.pomodoro),
|
||||||
Content::LocalTime => {
|
|
||||||
LocalTimeWidget { style: state.style }.render(area, buf, &mut state.local_time);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/args.rs
@@ -13,23 +13,18 @@ pub const LOG_DIRECTORY_DEFAULT_MISSING_VALUE: &str = " "; // empty string
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[arg(long, short, value_parser = duration::parse_long_duration,
|
#[arg(long, short, value_parser = duration::parse_duration,
|
||||||
help = "Countdown time to start from. Formats: 'Yy Dd hh:mm:ss', 'Dd hh:mm:ss', 'Yy mm:ss', 'Dd mm:ss', 'Yy ss', 'Dd ss', 'hh:mm:ss', 'mm:ss', 'ss'. Examples: '1y 5d 10:30:00', '2d 4:00', '1d 10', '5:03'."
|
help = "Countdown time to start from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'"
|
||||||
)]
|
)]
|
||||||
pub countdown: Option<Duration>,
|
pub countdown: Option<Duration>,
|
||||||
|
|
||||||
#[arg(long, visible_alias = "ct", value_parser = duration::parse_duration_by_time,
|
|
||||||
help = "Countdown targeting a specific time in the future or past. Formats: 'yyyy-mm-dd hh:mm:ss', 'yyyy-mm-dd hh:mm', 'hh:mm:ss', 'hh:mm', 'mm'"
|
|
||||||
)]
|
|
||||||
pub countdown_target: Option<duration::DirectedDuration>,
|
|
||||||
|
|
||||||
#[arg(long, short, value_parser = duration::parse_duration,
|
#[arg(long, short, value_parser = duration::parse_duration,
|
||||||
help = "Work time to count down from. Formats: 'ss', 'mm:ss', 'hh:mm:ss'"
|
help = "Work time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'"
|
||||||
)]
|
)]
|
||||||
pub work: Option<Duration>,
|
pub work: Option<Duration>,
|
||||||
|
|
||||||
#[arg(long, short, value_parser = duration::parse_duration,
|
#[arg(long, short, value_parser = duration::parse_duration,
|
||||||
help = "Pause time to count down from. Formats: 'ss', 'mm:ss', 'hh:mm:ss'"
|
help = "Pause time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'"
|
||||||
)]
|
)]
|
||||||
pub pause: Option<Duration>,
|
pub pause: Option<Duration>,
|
||||||
|
|
||||||
@@ -42,10 +37,10 @@ pub struct Args {
|
|||||||
#[arg(long, short = 's', value_enum, help = "Style to display time with.")]
|
#[arg(long, short = 's', value_enum, help = "Style to display time with.")]
|
||||||
pub style: Option<Style>,
|
pub style: Option<Style>,
|
||||||
|
|
||||||
#[arg(long, value_enum, help = "Open menu.")]
|
#[arg(long, value_enum, help = "Open the menu.")]
|
||||||
pub menu: bool,
|
pub menu: bool,
|
||||||
|
|
||||||
#[arg(long, short = 'r', help = "Reset stored values to defaults.")]
|
#[arg(long, short = 'r', help = "Reset stored values to default values.")]
|
||||||
pub reset: bool,
|
pub reset: bool,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
@@ -81,7 +76,7 @@ pub struct Args {
|
|||||||
// this value will be checked later in `main`
|
// this value will be checked later in `main`
|
||||||
// to use another (default) log directory instead
|
// to use another (default) log directory instead
|
||||||
default_missing_value=LOG_DIRECTORY_DEFAULT_MISSING_VALUE,
|
default_missing_value=LOG_DIRECTORY_DEFAULT_MISSING_VALUE,
|
||||||
help = "Directory for log file. If not set, standard application log directory is used (check README for details).",
|
help = "Directory to store log file. If not set, standard application log directory is used (check README for details).",
|
||||||
value_hint = clap::ValueHint::DirPath,
|
value_hint = clap::ValueHint::DirPath,
|
||||||
)]
|
)]
|
||||||
pub log: Option<PathBuf>,
|
pub log: Option<PathBuf>,
|
||||||
|
|||||||
@@ -1,8 +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 strum::EnumString;
|
use time::OffsetDateTime;
|
||||||
use time::{OffsetDateTime, format_description};
|
use time::format_description;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize,
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize,
|
||||||
@@ -15,8 +15,6 @@ pub enum Content {
|
|||||||
Timer,
|
Timer,
|
||||||
#[value(name = "pomodoro", alias = "p")]
|
#[value(name = "pomodoro", alias = "p")]
|
||||||
Pomodoro,
|
Pomodoro,
|
||||||
#[value(name = "localtime", alias = "l")]
|
|
||||||
LocalTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -73,7 +71,7 @@ impl Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, EnumString, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||||
pub enum AppTimeFormat {
|
pub enum AppTimeFormat {
|
||||||
/// `hh:mm:ss`
|
/// `hh:mm:ss`
|
||||||
#[default]
|
#[default]
|
||||||
@@ -82,22 +80,17 @@ pub enum AppTimeFormat {
|
|||||||
HhMm,
|
HhMm,
|
||||||
/// `hh:mm AM` (or PM)
|
/// `hh:mm AM` (or PM)
|
||||||
Hh12Mm,
|
Hh12Mm,
|
||||||
|
/// `` (empty)
|
||||||
|
Hidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppTimeFormat {
|
impl AppTimeFormat {
|
||||||
pub const fn first() -> Self {
|
|
||||||
Self::HhMmSs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn last() -> Self {
|
|
||||||
Self::Hh12Mm
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(&self) -> Self {
|
pub fn next(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
AppTimeFormat::HhMmSs => AppTimeFormat::HhMm,
|
AppTimeFormat::HhMmSs => AppTimeFormat::HhMm,
|
||||||
AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm,
|
AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm,
|
||||||
AppTimeFormat::Hh12Mm => AppTimeFormat::HhMmSs,
|
AppTimeFormat::Hh12Mm => AppTimeFormat::Hidden,
|
||||||
|
AppTimeFormat::Hidden => AppTimeFormat::HhMmSs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,22 +111,16 @@ impl From<AppTime> for OffsetDateTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppTime {
|
impl AppTime {
|
||||||
#[allow(clippy::new_without_default)]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
match OffsetDateTime::now_local() {
|
|
||||||
Ok(t) => AppTime::Local(t),
|
|
||||||
Err(_) => AppTime::Utc(OffsetDateTime::now_utc()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format(&self, app_format: &AppTimeFormat) -> String {
|
pub fn format(&self, app_format: &AppTimeFormat) -> String {
|
||||||
let parse_str = match app_format {
|
let parse_str = match app_format {
|
||||||
AppTimeFormat::HhMmSs => "[hour]:[minute]:[second]",
|
AppTimeFormat::HhMmSs => Some("[hour]:[minute]:[second]"),
|
||||||
AppTimeFormat::HhMm => "[hour]:[minute]",
|
AppTimeFormat::HhMm => Some("[hour]:[minute]"),
|
||||||
AppTimeFormat::Hh12Mm => "[hour repr:12 padding:none]:[minute] [period]",
|
AppTimeFormat::Hh12Mm => Some("[hour repr:12 padding:none]:[minute] [period]"),
|
||||||
|
AppTimeFormat::Hidden => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
format_description::parse(parse_str)
|
if let Some(str) = parse_str {
|
||||||
|
format_description::parse(str)
|
||||||
.map_err(|_| "parse error")
|
.map_err(|_| "parse error")
|
||||||
.and_then(|fd| {
|
.and_then(|fd| {
|
||||||
OffsetDateTime::from(*self)
|
OffsetDateTime::from(*self)
|
||||||
@@ -141,41 +128,9 @@ impl AppTime {
|
|||||||
.map_err(|_| "format error")
|
.map_err(|_| "format error")
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|e| e.to_string())
|
.unwrap_or_else(|e| e.to_string())
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_date(&self) -> String {
|
|
||||||
format_description::parse("[year]-[month]-[day]")
|
|
||||||
.map_err(|_| "parse error")
|
|
||||||
.and_then(|fd| {
|
|
||||||
OffsetDateTime::from(*self)
|
|
||||||
.format(&fd)
|
|
||||||
.map_err(|_| "format error")
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_period(&self) -> String {
|
|
||||||
format_description::parse("[period]")
|
|
||||||
.map_err(|_| "parse error")
|
|
||||||
.and_then(|fd| {
|
|
||||||
OffsetDateTime::from(*self)
|
|
||||||
.format(&fd)
|
|
||||||
.map_err(|_| "format error")
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts `AppTime` into a `Duration` representing elapsed time since midnight (today).
|
|
||||||
pub fn as_duration_of_today(&self) -> std::time::Duration {
|
|
||||||
let dt = OffsetDateTime::from(*self);
|
|
||||||
let time = dt.time();
|
|
||||||
|
|
||||||
let total_nanos = u64::from(time.hour()) * 3_600_000_000_000
|
|
||||||
+ u64::from(time.minute()) * 60_000_000_000
|
|
||||||
+ u64::from(time.second()) * 1_000_000_000
|
|
||||||
+ u64::from(time.nanosecond());
|
|
||||||
|
|
||||||
std::time::Duration::from_nanos(total_nanos)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,15 +150,6 @@ pub enum Toggle {
|
|||||||
Off,
|
Off,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bool> for Toggle {
|
|
||||||
fn from(value: bool) -> Self {
|
|
||||||
match value {
|
|
||||||
true => Toggle::On,
|
|
||||||
false => Toggle::Off,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
@@ -250,5 +196,12 @@ mod tests {
|
|||||||
"6:06 PM",
|
"6:06 PM",
|
||||||
"local"
|
"local"
|
||||||
);
|
);
|
||||||
|
// hidden
|
||||||
|
assert_eq!(AppTime::Utc(dt).format(&AppTimeFormat::Hidden), "", "utc");
|
||||||
|
assert_eq!(
|
||||||
|
AppTime::Local(dt).format(&AppTimeFormat::Hidden),
|
||||||
|
"",
|
||||||
|
"local"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
487
src/duration.rs
@@ -2,11 +2,13 @@ use color_eyre::{
|
|||||||
Report,
|
Report,
|
||||||
eyre::{ensure, eyre},
|
eyre::{ensure, eyre},
|
||||||
};
|
};
|
||||||
use std::cmp::min;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::common::AppTime;
|
pub const ONE_DECI_SECOND: Duration = Duration::from_millis(100);
|
||||||
|
pub const ONE_SECOND: Duration = Duration::from_secs(1);
|
||||||
|
pub const ONE_MINUTE: Duration = Duration::from_secs(SECS_PER_MINUTE);
|
||||||
|
pub const ONE_HOUR: Duration = Duration::from_secs(MINS_PER_HOUR * SECS_PER_MINUTE);
|
||||||
|
|
||||||
// unstable
|
// unstable
|
||||||
// https://doc.rust-lang.org/src/core/time.rs.html#32
|
// https://doc.rust-lang.org/src/core/time.rs.html#32
|
||||||
@@ -18,33 +20,9 @@ pub const MINS_PER_HOUR: u64 = 60;
|
|||||||
// https://doc.rust-lang.org/src/core/time.rs.html#36
|
// https://doc.rust-lang.org/src/core/time.rs.html#36
|
||||||
const HOURS_PER_DAY: u64 = 24;
|
const HOURS_PER_DAY: u64 = 24;
|
||||||
|
|
||||||
pub const ONE_DECI_SECOND: Duration = Duration::from_millis(100);
|
// max. 99:59:59
|
||||||
pub const ONE_SECOND: Duration = Duration::from_secs(1);
|
pub const MAX_DURATION: Duration =
|
||||||
pub const ONE_MINUTE: Duration = Duration::from_secs(SECS_PER_MINUTE);
|
Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND);
|
||||||
pub const ONE_HOUR: Duration = Duration::from_secs(MINS_PER_HOUR * SECS_PER_MINUTE);
|
|
||||||
pub const ONE_DAY: Duration = Duration::from_secs(HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE);
|
|
||||||
pub const ONE_YEAR: Duration =
|
|
||||||
Duration::from_secs(DAYS_PER_YEAR * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE);
|
|
||||||
|
|
||||||
// Days per year
|
|
||||||
// "There are 365 days in a year in a common year of the Gregorian calendar and 366 days in a leap year.
|
|
||||||
// Leap years occur every four years. The average number of days in a year is 365.2425 days."
|
|
||||||
// ^ https://www.math.net/days-in-a-year
|
|
||||||
const DAYS_PER_YEAR: u64 = 365; // ignore leap year of 366 days
|
|
||||||
|
|
||||||
// max. 999y 364d 23:59:59.9 (1000 years - 1 decisecond)
|
|
||||||
pub const MAX_DURATION: Duration = ONE_YEAR
|
|
||||||
.saturating_mul(1000)
|
|
||||||
.saturating_sub(ONE_DECI_SECOND);
|
|
||||||
|
|
||||||
/// `Duration` with direction in time (past or future)
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum DirectedDuration {
|
|
||||||
/// Time `until` a future moment (positive `Duration`)
|
|
||||||
Until(Duration),
|
|
||||||
/// Time `since` a past moment (negative duration, but still represented as positive `Duration`)
|
|
||||||
Since(Duration),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialOrd)]
|
||||||
pub struct DurationEx {
|
pub struct DurationEx {
|
||||||
@@ -70,36 +48,22 @@ impl From<DurationEx> for Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DurationEx {
|
impl DurationEx {
|
||||||
pub fn years(&self) -> u64 {
|
pub fn seconds(&self) -> u64 {
|
||||||
self.days() / DAYS_PER_YEAR
|
self.inner.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn days(&self) -> u64 {
|
pub fn seconds_mod(&self) -> u64 {
|
||||||
self.hours() / HOURS_PER_DAY
|
self.seconds() % SECS_PER_MINUTE
|
||||||
}
|
|
||||||
|
|
||||||
/// Days in a year
|
|
||||||
pub fn days_mod(&self) -> u64 {
|
|
||||||
self.days() % DAYS_PER_YEAR
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hours(&self) -> u64 {
|
pub fn hours(&self) -> u64 {
|
||||||
self.seconds() / (SECS_PER_MINUTE * MINS_PER_HOUR)
|
self.seconds() / (SECS_PER_MINUTE * MINS_PER_HOUR)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hours as 24-hour clock
|
|
||||||
pub fn hours_mod(&self) -> u64 {
|
pub fn hours_mod(&self) -> u64 {
|
||||||
self.hours() % HOURS_PER_DAY
|
self.hours() % HOURS_PER_DAY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hours as 12-hour clock
|
|
||||||
pub fn hours_mod_12(&self) -> u64 {
|
|
||||||
// 0 => 12,
|
|
||||||
// 1..=12 => hours,
|
|
||||||
// 13..=23 => hours - 12,
|
|
||||||
(self.hours_mod() + 11) % 12 + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn minutes(&self) -> u64 {
|
pub fn minutes(&self) -> u64 {
|
||||||
self.seconds() / MINS_PER_HOUR
|
self.seconds() / MINS_PER_HOUR
|
||||||
}
|
}
|
||||||
@@ -108,14 +72,6 @@ impl DurationEx {
|
|||||||
self.minutes() % SECS_PER_MINUTE
|
self.minutes() % SECS_PER_MINUTE
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seconds(&self) -> u64 {
|
|
||||||
self.inner.as_secs()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seconds_mod(&self) -> u64 {
|
|
||||||
self.seconds() % SECS_PER_MINUTE
|
|
||||||
}
|
|
||||||
|
|
||||||
// deciseconds
|
// deciseconds
|
||||||
pub fn decis(&self) -> u64 {
|
pub fn decis(&self) -> u64 {
|
||||||
(self.inner.subsec_millis() / 100) as u64
|
(self.inner.subsec_millis() / 100) as u64
|
||||||
@@ -142,26 +98,7 @@ impl DurationEx {
|
|||||||
|
|
||||||
impl fmt::Display for DurationEx {
|
impl fmt::Display for DurationEx {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if self.years() >= 1 {
|
if self.hours() >= 10 {
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}y {}d {:02}:{:02}:{:02}",
|
|
||||||
self.years(),
|
|
||||||
self.days_mod(),
|
|
||||||
self.hours_mod(),
|
|
||||||
self.minutes_mod(),
|
|
||||||
self.seconds_mod(),
|
|
||||||
)
|
|
||||||
} else if self.hours() >= HOURS_PER_DAY {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}d {:02}:{:02}:{:02}",
|
|
||||||
self.days_mod(),
|
|
||||||
self.hours_mod(),
|
|
||||||
self.minutes_mod(),
|
|
||||||
self.seconds_mod(),
|
|
||||||
)
|
|
||||||
} else if self.hours() >= 10 {
|
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{:02}:{:02}:{:02}",
|
"{:02}:{:02}:{:02}",
|
||||||
@@ -189,183 +126,45 @@ impl fmt::Display for DurationEx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse seconds (must be < 60)
|
|
||||||
fn parse_seconds(s: &str) -> Result<u8, Report> {
|
|
||||||
let secs = s.parse::<u8>().map_err(|_| eyre!("Invalid seconds"))?;
|
|
||||||
ensure!(secs < 60, "Seconds must be less than 60.");
|
|
||||||
Ok(secs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse minutes (must be < 60)
|
|
||||||
fn parse_minutes(m: &str) -> Result<u8, Report> {
|
|
||||||
let mins = m.parse::<u8>().map_err(|_| eyre!("Invalid minutes"))?;
|
|
||||||
ensure!(mins < 60, "Minutes must be less than 60.");
|
|
||||||
Ok(mins)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse hours
|
|
||||||
fn parse_hours(h: &str) -> Result<u8, Report> {
|
|
||||||
let hours = h.parse::<u8>().map_err(|_| eyre!("Invalid hours"))?;
|
|
||||||
Ok(hours)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses `DirectedDuration` from following formats:
|
|
||||||
/// - `yyyy-mm-dd hh:mm:ss`
|
|
||||||
/// - `yyyy-mm-dd hh:mm`
|
|
||||||
/// - `hh:mm:ss`
|
|
||||||
/// - `hh:mm`
|
|
||||||
/// - `mm`
|
|
||||||
///
|
|
||||||
/// Returns `DirectedDuration::Until` for future times, `DirectedDuration::Since` for past times
|
|
||||||
pub fn parse_duration_by_time(arg: &str) -> Result<DirectedDuration, Report> {
|
|
||||||
use time::{OffsetDateTime, PrimitiveDateTime, macros::format_description};
|
|
||||||
|
|
||||||
let now: OffsetDateTime = AppTime::new().into();
|
|
||||||
|
|
||||||
let target_time = if arg.contains('-') {
|
|
||||||
// First: `YYYY-MM-DD HH:MM:SS`
|
|
||||||
// Then: `YYYY-MM-DD HH:MM`
|
|
||||||
let format_with_seconds =
|
|
||||||
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
|
||||||
let format_without_seconds = format_description!("[year]-[month]-[day] [hour]:[minute]");
|
|
||||||
|
|
||||||
let pdt = PrimitiveDateTime::parse(arg, format_with_seconds)
|
|
||||||
.or_else(|_| PrimitiveDateTime::parse(arg, format_without_seconds))
|
|
||||||
.map_err(|e| {
|
|
||||||
eyre!("Invalid datetime '{}'. Use format 'yyyy-mm-dd hh:mm:ss' or 'yyyy-mm-dd hh:mm'. Error: {}", arg, e)
|
|
||||||
})?;
|
|
||||||
pdt.assume_offset(now.offset())
|
|
||||||
} else {
|
|
||||||
// Parse time parts: interpret as HH:MM:SS, HH:MM, or SS
|
|
||||||
let parts: Vec<&str> = arg.split(':').collect();
|
|
||||||
|
|
||||||
let (hour, minute, second) = match parts.as_slice() {
|
|
||||||
[mm] => {
|
|
||||||
// Single part: treat as minutes in current hour
|
|
||||||
let m = parse_minutes(mm)?;
|
|
||||||
(now.hour(), m, 0)
|
|
||||||
}
|
|
||||||
[hh, mm] => {
|
|
||||||
// Two parts: treat as HH:MM (time of day)
|
|
||||||
let h = parse_hours(hh)?;
|
|
||||||
let m = parse_minutes(mm)?;
|
|
||||||
(h, m, 0)
|
|
||||||
}
|
|
||||||
[hh, mm, ss] => {
|
|
||||||
// Three parts: HH:MM:SS
|
|
||||||
let h = parse_hours(hh)?;
|
|
||||||
let m = parse_minutes(mm)?;
|
|
||||||
let s = parse_seconds(ss)?;
|
|
||||||
(h, m, s)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(eyre!(
|
|
||||||
"Invalid time format. Use 'hh:mm:ss', 'hh:mm', or 'mm'"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
now.replace_time(
|
|
||||||
time::Time::from_hms(hour, minute, second).map_err(|_| eyre!("Invalid time"))?,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut duration_secs = (target_time - now).whole_seconds();
|
|
||||||
|
|
||||||
// `Since` for past times
|
|
||||||
if duration_secs < 0 {
|
|
||||||
duration_secs *= -1;
|
|
||||||
Ok(DirectedDuration::Since(Duration::from_secs(
|
|
||||||
duration_secs as u64,
|
|
||||||
)))
|
|
||||||
} else
|
|
||||||
// `Until` for future times,
|
|
||||||
{
|
|
||||||
Ok(DirectedDuration::Until(Duration::from_secs(
|
|
||||||
duration_secs as u64,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses `Duration` from `hh:mm:ss`, `mm:ss` or `ss`
|
/// Parses `Duration` from `hh:mm:ss`, `mm:ss` or `ss`
|
||||||
pub fn parse_duration(arg: &str) -> Result<Duration, Report> {
|
pub fn parse_duration(arg: &str) -> Result<Duration, Report> {
|
||||||
let parts: Vec<&str> = arg.split(':').collect();
|
let parts: Vec<&str> = arg.split(':').rev().collect();
|
||||||
|
|
||||||
let (hours, minutes, seconds) = match parts.as_slice() {
|
let parse_seconds = |s: &str| -> Result<u64, Report> {
|
||||||
[ss] => {
|
let secs = s.parse::<u64>().map_err(|_| eyre!("Invalid seconds"))?;
|
||||||
// Single part: seconds only
|
ensure!(secs < 60, "Seconds must be less than 60.");
|
||||||
let s = parse_seconds(ss)?;
|
Ok(secs)
|
||||||
(0u64, 0u64, s as u64)
|
|
||||||
}
|
|
||||||
[mm, ss] => {
|
|
||||||
// Two parts: MM:SS
|
|
||||||
let m = parse_minutes(mm)?;
|
|
||||||
let s = parse_seconds(ss)?;
|
|
||||||
(0u64, m as u64, s as u64)
|
|
||||||
}
|
|
||||||
[hh, mm, ss] => {
|
|
||||||
// Three parts: HH:MM:SS
|
|
||||||
let h = parse_hours(hh)?;
|
|
||||||
let m = parse_minutes(mm)?;
|
|
||||||
let s = parse_seconds(ss)?;
|
|
||||||
(h as u64, m as u64, s as u64)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(eyre!(
|
|
||||||
"Invalid time format. Use 'ss', 'mm:ss', or 'hh:mm:ss'"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let total_seconds = hours * 3600 + minutes * 60 + seconds;
|
let parse_minutes = |m: &str| -> Result<u64, Report> {
|
||||||
Ok(Duration::from_secs(total_seconds))
|
let mins = m.parse::<u64>().map_err(|_| eyre!("Invalid minutes"))?;
|
||||||
}
|
ensure!(mins < 60, "Minutes must be less than 60.");
|
||||||
|
Ok(mins)
|
||||||
|
};
|
||||||
|
|
||||||
/// Similar to `parse_duration`, but it parses `years` and `days` in addition
|
let parse_hours = |h: &str| -> Result<u64, Report> {
|
||||||
/// Formats: `Yy Dd`, `Yy` or `Dd` in any combination to other time formats
|
let hours = h.parse::<u64>().map_err(|_| eyre!("Invalid hours"))?;
|
||||||
/// Examples: `10y 3d 12:10:03`, `2d 10:00`, `101y 33`, `5:30`
|
ensure!(hours < 100, "Hours must be less than 100.");
|
||||||
pub fn parse_long_duration(arg: &str) -> Result<Duration, Report> {
|
Ok(hours)
|
||||||
let arg = arg.trim();
|
};
|
||||||
|
|
||||||
// parts are separated by whitespaces:
|
let seconds = match parts.as_slice() {
|
||||||
// 3 parts: years, days, time
|
[ss] => parse_seconds(ss)?,
|
||||||
let parts: Vec<&str> = arg.split_whitespace().collect();
|
[ss, mm] => {
|
||||||
ensure!(parts.len() <= 3, "Invalid format. Too many parts.");
|
let s = parse_seconds(ss)?;
|
||||||
|
let m = parse_minutes(mm)?;
|
||||||
let mut total_duration = Duration::ZERO;
|
m * 60 + s
|
||||||
let mut time_part: Option<&str> = None;
|
|
||||||
|
|
||||||
for part in parts {
|
|
||||||
// years
|
|
||||||
if let Some(years_str) = part.strip_suffix('y') {
|
|
||||||
let years = years_str
|
|
||||||
.parse::<u64>()
|
|
||||||
.map_err(|_| eyre!("Invalid years value: '{}'", years_str))?;
|
|
||||||
total_duration = total_duration.saturating_add(ONE_YEAR.saturating_mul(years as u32));
|
|
||||||
}
|
|
||||||
// days
|
|
||||||
else if let Some(days_str) = part.strip_suffix('d') {
|
|
||||||
let days = days_str
|
|
||||||
.parse::<u64>()
|
|
||||||
.map_err(|_| eyre!("Invalid days value: '{}'", days_str))?;
|
|
||||||
total_duration = total_duration.saturating_add(ONE_DAY.saturating_mul(days as u32));
|
|
||||||
}
|
|
||||||
// possible time format
|
|
||||||
else {
|
|
||||||
time_part = Some(part);
|
|
||||||
}
|
}
|
||||||
|
[ss, mm, hh] => {
|
||||||
|
let s = parse_seconds(ss)?;
|
||||||
|
let m = parse_minutes(mm)?;
|
||||||
|
let h = parse_hours(hh)?;
|
||||||
|
h * 60 * 60 + m * 60 + s
|
||||||
}
|
}
|
||||||
|
_ => return Err(eyre!("Invalid time format. Use 'ss', mm:ss, or hh:mm:ss")),
|
||||||
|
};
|
||||||
|
|
||||||
// time format
|
Ok(Duration::from_secs(seconds))
|
||||||
if let Some(time) = time_part {
|
|
||||||
let time_duration = parse_duration(time)?;
|
|
||||||
total_duration = total_duration.saturating_add(time_duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid overflow
|
|
||||||
total_duration = min(MAX_DURATION, total_duration);
|
|
||||||
|
|
||||||
Ok(total_duration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -374,51 +173,19 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const MINUTE_IN_SECONDS: u64 = ONE_MINUTE.as_secs();
|
|
||||||
const HOUR_IN_SECONDS: u64 = ONE_HOUR.as_secs();
|
|
||||||
const DAY_IN_SECONDS: u64 = ONE_DAY.as_secs();
|
|
||||||
const YEAR_IN_SECONDS: u64 = ONE_YEAR.as_secs();
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fmt() {
|
fn test_fmt() {
|
||||||
// 1y Dd hh:mm:ss (single year)
|
|
||||||
let ex: DurationEx =
|
|
||||||
Duration::from_secs(YEAR_IN_SECONDS + 10 * DAY_IN_SECONDS + 36001).into();
|
|
||||||
assert_eq!(format!("{ex}"), "1y 10d 10:00:01");
|
|
||||||
// 5y Dd hh:mm:ss (multiple years)
|
|
||||||
let ex: DurationEx = Duration::from_secs(
|
|
||||||
5 * YEAR_IN_SECONDS + 100 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1,
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
assert_eq!(format!("{ex}"), "5y 100d 10:00:01");
|
|
||||||
// 150y Dd hh:mm:ss (more than 100 years)
|
|
||||||
let ex: DurationEx = Duration::from_secs(
|
|
||||||
150 * YEAR_IN_SECONDS + 200 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1,
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
assert_eq!(format!("{ex}"), "150y 200d 10:00:01");
|
|
||||||
// 366d hh:mm:ss (days more than a year)
|
|
||||||
let ex: DurationEx =
|
|
||||||
Duration::from_secs(366 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1).into();
|
|
||||||
assert_eq!(format!("{ex}"), "1y 1d 10:00:01");
|
|
||||||
// 1d hh:mm:ss (single day)
|
|
||||||
let ex: DurationEx = Duration::from_secs(DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1).into();
|
|
||||||
assert_eq!(format!("{ex}"), "1d 10:00:01");
|
|
||||||
// 2d hh:mm:ss (multiple days)
|
|
||||||
let ex: DurationEx =
|
|
||||||
Duration::from_secs(2 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1).into();
|
|
||||||
assert_eq!(format!("{ex}"), "2d 10:00:01");
|
|
||||||
// hh:mm:ss
|
// hh:mm:ss
|
||||||
let ex: DurationEx = Duration::from_secs(10 * HOUR_IN_SECONDS + 1).into();
|
let ex: DurationEx = Duration::from_secs(36001).into();
|
||||||
assert_eq!(format!("{ex}"), "10:00:01");
|
assert_eq!(format!("{ex}"), "10:00:01");
|
||||||
// h:mm:ss
|
// h:mm:ss
|
||||||
let ex: DurationEx = Duration::from_secs(HOUR_IN_SECONDS + 1).into();
|
let ex: DurationEx = Duration::from_secs(3601).into();
|
||||||
assert_eq!(format!("{ex}"), "1:00:01");
|
assert_eq!(format!("{ex}"), "1:00:01");
|
||||||
// mm:ss
|
// mm:ss
|
||||||
let ex: DurationEx = Duration::from_secs(MINUTE_IN_SECONDS + 11).into();
|
let ex: DurationEx = Duration::from_secs(71).into();
|
||||||
assert_eq!(format!("{ex}"), "1:11");
|
assert_eq!(format!("{ex}"), "1:11");
|
||||||
// m:ss
|
// m:ss
|
||||||
let ex: DurationEx = Duration::from_secs(MINUTE_IN_SECONDS + 1).into();
|
let ex: DurationEx = Duration::from_secs(61).into();
|
||||||
assert_eq!(format!("{ex}"), "1:01");
|
assert_eq!(format!("{ex}"), "1:01");
|
||||||
// ss
|
// ss
|
||||||
let ex: DurationEx = Duration::from_secs(11).into();
|
let ex: DurationEx = Duration::from_secs(11).into();
|
||||||
@@ -444,34 +211,6 @@ mod tests {
|
|||||||
assert_eq!(format!("{ex3}"), "11");
|
assert_eq!(format!("{ex3}"), "11");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_hours_mod_12() {
|
|
||||||
// 24 -> 12
|
|
||||||
let ex: DurationEx = ONE_HOUR.saturating_mul(24).into();
|
|
||||||
let result = ex.hours_mod_12();
|
|
||||||
assert_eq!(result, 12);
|
|
||||||
|
|
||||||
// 12 -> 12
|
|
||||||
let ex: DurationEx = ONE_HOUR.saturating_mul(12).into();
|
|
||||||
let result = ex.hours_mod_12();
|
|
||||||
assert_eq!(result, 12);
|
|
||||||
|
|
||||||
// 0 -> 12
|
|
||||||
let ex: DurationEx = ONE_SECOND.into();
|
|
||||||
let result = ex.hours_mod_12();
|
|
||||||
assert_eq!(result, 12);
|
|
||||||
|
|
||||||
// 13 -> 1
|
|
||||||
let ex: DurationEx = ONE_HOUR.saturating_mul(13).into();
|
|
||||||
let result = ex.hours_mod_12();
|
|
||||||
assert_eq!(result, 1);
|
|
||||||
|
|
||||||
// 1 -> 1
|
|
||||||
let ex: DurationEx = ONE_HOUR.saturating_mul(1).into();
|
|
||||||
let result = ex.hours_mod_12();
|
|
||||||
assert_eq!(result, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_duration() {
|
fn test_parse_duration() {
|
||||||
// ss
|
// ss
|
||||||
@@ -489,142 +228,8 @@ mod tests {
|
|||||||
// errors
|
// errors
|
||||||
assert!(parse_duration("1:60").is_err()); // invalid seconds
|
assert!(parse_duration("1:60").is_err()); // invalid seconds
|
||||||
assert!(parse_duration("60:00").is_err()); // invalid minutes
|
assert!(parse_duration("60:00").is_err()); // invalid minutes
|
||||||
|
assert!(parse_duration("100:00:00").is_err()); // invalid hours
|
||||||
assert!(parse_duration("abc").is_err()); // invalid input
|
assert!(parse_duration("abc").is_err()); // invalid input
|
||||||
assert!(parse_duration("01:02:03:04").is_err()); // too many parts
|
assert!(parse_duration("01:02:03:04").is_err()); // too many parts
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_duration_by_time() {
|
|
||||||
// YYYY-MM-DD HH:MM:SS - future
|
|
||||||
assert!(matches!(
|
|
||||||
parse_duration_by_time("2050-06-15 14:30:45"),
|
|
||||||
Ok(DirectedDuration::Until(_))
|
|
||||||
));
|
|
||||||
|
|
||||||
// YYYY-MM-DD HH:MM - future
|
|
||||||
assert!(matches!(
|
|
||||||
parse_duration_by_time("2050-06-15 14:30"),
|
|
||||||
Ok(DirectedDuration::Until(_))
|
|
||||||
));
|
|
||||||
|
|
||||||
// HH:MM:SS - past
|
|
||||||
assert!(matches!(
|
|
||||||
parse_duration_by_time("2000-01-01 23:59:59"),
|
|
||||||
Ok(DirectedDuration::Since(_))
|
|
||||||
));
|
|
||||||
|
|
||||||
// HH:MM - Until or Since depending on current time
|
|
||||||
assert!(parse_duration_by_time("18:00").is_ok());
|
|
||||||
|
|
||||||
// MM - Until or Since depending on current time
|
|
||||||
assert!(parse_duration_by_time("45").is_ok());
|
|
||||||
|
|
||||||
// errors
|
|
||||||
assert!(parse_duration_by_time("60").is_err()); // invalid minutes
|
|
||||||
assert!(parse_duration_by_time("24:00").is_err()); // invalid hours
|
|
||||||
assert!(parse_duration_by_time("24:00:00").is_err()); // invalid hours
|
|
||||||
assert!(parse_duration_by_time("2030-13-01 12:00:00").is_err()); // invalid month
|
|
||||||
assert!(parse_duration_by_time("2030-06-32 12:00:00").is_err()); // invalid day
|
|
||||||
assert!(parse_duration_by_time("abc").is_err()); // invalid input
|
|
||||||
assert!(parse_duration_by_time("01:02:03:04").is_err()); // too many parts
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_long_duration() {
|
|
||||||
// `Yy`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("10y").unwrap(),
|
|
||||||
Duration::from_secs(10 * YEAR_IN_SECONDS)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("101y").unwrap(),
|
|
||||||
Duration::from_secs(101 * YEAR_IN_SECONDS)
|
|
||||||
);
|
|
||||||
|
|
||||||
// `Dd`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("2d").unwrap(),
|
|
||||||
Duration::from_secs(2 * DAY_IN_SECONDS)
|
|
||||||
);
|
|
||||||
|
|
||||||
// `Yy Dd`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("10y 3d").unwrap(),
|
|
||||||
Duration::from_secs(10 * YEAR_IN_SECONDS + 3 * DAY_IN_SECONDS)
|
|
||||||
);
|
|
||||||
|
|
||||||
// `Yy Dd hh:mm:ss`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("10y 3d 12:10:03").unwrap(),
|
|
||||||
Duration::from_secs(
|
|
||||||
10 * YEAR_IN_SECONDS
|
|
||||||
+ 3 * DAY_IN_SECONDS
|
|
||||||
+ 12 * HOUR_IN_SECONDS
|
|
||||||
+ 10 * MINUTE_IN_SECONDS
|
|
||||||
+ 3
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// `Dd hh:mm`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("2d 10:00").unwrap(),
|
|
||||||
Duration::from_secs(2 * DAY_IN_SECONDS + 10 * 60)
|
|
||||||
);
|
|
||||||
|
|
||||||
// `Yy ss`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("101y 33").unwrap(),
|
|
||||||
Duration::from_secs(101 * YEAR_IN_SECONDS + 33)
|
|
||||||
);
|
|
||||||
|
|
||||||
// time formats (backward compatibility with `parse_duration`)
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("5:30").unwrap(),
|
|
||||||
Duration::from_secs(5 * MINUTE_IN_SECONDS + 30)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("01:30:45").unwrap(),
|
|
||||||
Duration::from_secs(HOUR_IN_SECONDS + 30 * MINUTE_IN_SECONDS + 45)
|
|
||||||
);
|
|
||||||
assert_eq!(parse_long_duration("42").unwrap(), Duration::from_secs(42));
|
|
||||||
|
|
||||||
// `Dd ss`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("5d 30").unwrap(),
|
|
||||||
Duration::from_secs(5 * DAY_IN_SECONDS + 30)
|
|
||||||
);
|
|
||||||
|
|
||||||
// `Yy hh:mm:ss`
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("1y 01:30:00").unwrap(),
|
|
||||||
Duration::from_secs(YEAR_IN_SECONDS + HOUR_IN_SECONDS + 30 * MINUTE_IN_SECONDS)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Whitespace handling
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration(" 2d 10:00 ").unwrap(),
|
|
||||||
Duration::from_secs(2 * DAY_IN_SECONDS + 10 * MINUTE_IN_SECONDS)
|
|
||||||
);
|
|
||||||
|
|
||||||
// MAX_DURATION clamping
|
|
||||||
assert_eq!(parse_long_duration("1000y").unwrap(), MAX_DURATION);
|
|
||||||
assert_eq!(
|
|
||||||
parse_long_duration("999y 364d 23:59:59").unwrap(),
|
|
||||||
Duration::from_secs(
|
|
||||||
999 * YEAR_IN_SECONDS
|
|
||||||
+ 364 * DAY_IN_SECONDS
|
|
||||||
+ 23 * HOUR_IN_SECONDS
|
|
||||||
+ 59 * MINUTE_IN_SECONDS
|
|
||||||
+ 59
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// errors
|
|
||||||
assert!(parse_long_duration("10x").is_err()); // invalid unit
|
|
||||||
assert!(parse_long_duration("abc").is_err()); // invalid input
|
|
||||||
assert!(parse_long_duration("10y 60:00").is_err()); // invalid minutes in time part
|
|
||||||
assert!(parse_long_duration("5d 1:60").is_err()); // invalid seconds in time part
|
|
||||||
assert!(parse_long_duration("1y 2d 3d 4:00").is_err()); // too many parts (4 parts)
|
|
||||||
assert!(parse_long_duration("1y 2d 3h 4m 5s").is_err()); // too many parts (5 parts)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,17 @@ use crate::{
|
|||||||
widgets::pomodoro::Mode as PomodoroMode,
|
widgets::pomodoro::Mode as PomodoroMode,
|
||||||
};
|
};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
fn deserialize_app_time_format<'de, D>(deserializer: D) -> Result<AppTimeFormat, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
match s.as_str() {
|
|
||||||
// Hidden is deprecated - use `default` value instead
|
|
||||||
"Hidden" => Ok(AppTimeFormat::default()),
|
|
||||||
_ => s.parse().map_err(serde::de::Error::custom),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AppStorage {
|
pub struct AppStorage {
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
pub show_menu: bool,
|
pub show_menu: bool,
|
||||||
pub notification: Toggle,
|
pub notification: Toggle,
|
||||||
pub blink: Toggle,
|
pub blink: Toggle,
|
||||||
#[serde(deserialize_with = "deserialize_app_time_format")]
|
|
||||||
pub app_time_format: AppTimeFormat,
|
pub app_time_format: AppTimeFormat,
|
||||||
pub style: Style,
|
pub style: Style,
|
||||||
pub with_decis: bool,
|
pub with_decis: bool,
|
||||||
@@ -44,8 +31,6 @@ pub struct AppStorage {
|
|||||||
pub elapsed_value_countdown: Duration,
|
pub elapsed_value_countdown: Duration,
|
||||||
// timer
|
// timer
|
||||||
pub current_value_timer: Duration,
|
pub current_value_timer: Duration,
|
||||||
// footer
|
|
||||||
pub footer_app_time: Toggle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppStorage {
|
impl Default for AppStorage {
|
||||||
@@ -75,8 +60,6 @@ impl Default for AppStorage {
|
|||||||
elapsed_value_countdown: Duration::ZERO,
|
elapsed_value_countdown: Duration::ZERO,
|
||||||
// timer
|
// timer
|
||||||
current_value_timer: Duration::ZERO,
|
current_value_timer: Duration::ZERO,
|
||||||
// footer
|
|
||||||
footer_app_time: Toggle::Off,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ pub mod countdown;
|
|||||||
pub mod edit_time;
|
pub mod edit_time;
|
||||||
pub mod footer;
|
pub mod footer;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod local_time;
|
|
||||||
pub mod pomodoro;
|
pub mod pomodoro;
|
||||||
pub mod progressbar;
|
pub mod progressbar;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
|
|||||||
1118
src/widgets/clock.rs
@@ -7,13 +7,9 @@ use ratatui::{
|
|||||||
pub const DIGIT_SIZE: usize = 5;
|
pub const DIGIT_SIZE: usize = 5;
|
||||||
pub const DIGIT_WIDTH: u16 = DIGIT_SIZE as u16;
|
pub const DIGIT_WIDTH: u16 = DIGIT_SIZE as u16;
|
||||||
pub const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */;
|
pub const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */;
|
||||||
pub const TWO_DIGITS_WIDTH: u16 = DIGIT_WIDTH + DIGIT_SPACE_WIDTH + DIGIT_WIDTH; // digit-space-digit
|
|
||||||
pub const THREE_DIGITS_WIDTH: u16 =
|
|
||||||
DIGIT_WIDTH + DIGIT_SPACE_WIDTH + DIGIT_WIDTH + DIGIT_SPACE_WIDTH + DIGIT_WIDTH; // digit-space-digit-space-digit
|
|
||||||
pub const COLON_WIDTH: u16 = 4; // incl. padding left + padding right
|
pub const COLON_WIDTH: u16 = 4; // incl. padding left + padding right
|
||||||
pub const DOT_WIDTH: u16 = 4; // incl. padding left + padding right
|
pub const DOT_WIDTH: u16 = 4; // incl. padding left + padding right
|
||||||
pub const DIGIT_SPACE_WIDTH: u16 = 1; // space between digits
|
pub const DIGIT_SPACE_WIDTH: u16 = 1; // space between digits
|
||||||
pub const DIGIT_LABEL_WIDTH: u16 = 3; // label (single char) incl. padding left + padding right
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [
|
const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::ClockTypeId,
|
common::ClockTypeId,
|
||||||
duration::{
|
duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
|
||||||
MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND, ONE_YEAR,
|
|
||||||
},
|
|
||||||
widgets::clock::*,
|
widgets::clock::*,
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -25,230 +23,6 @@ fn test_type_id() {
|
|||||||
assert!(matches!(c.get_type_id(), ClockTypeId::Countdown));
|
assert!(matches!(c.get_type_id(), ClockTypeId::Countdown));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_format_seconds() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_SECOND * 5,
|
|
||||||
current_value: ONE_SECOND * 5,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
// S
|
|
||||||
assert_eq!(c.get_format(), &Format::S);
|
|
||||||
// Ss
|
|
||||||
c.set_current_value(Duration::from_secs(15).into());
|
|
||||||
assert_eq!(c.get_format(), &Format::Ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_format_minutes() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_MINUTE,
|
|
||||||
current_value: ONE_MINUTE,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
// MSs
|
|
||||||
assert_eq!(c.get_format(), &Format::MSs);
|
|
||||||
// MmSs
|
|
||||||
c.set_current_value((ONE_MINUTE * 11).into()); // 10+ minutes
|
|
||||||
assert_eq!(c.get_format(), &Format::MmSs);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_format_hours() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_HOUR,
|
|
||||||
current_value: ONE_HOUR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
// HMmSS
|
|
||||||
assert_eq!(c.get_format(), &Format::HMmSs);
|
|
||||||
// HhMmSs
|
|
||||||
c.set_current_value((10 * ONE_HOUR).into());
|
|
||||||
assert_eq!(c.get_format(), &Format::HhMmSs);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_by_duration_boundaries() {
|
|
||||||
// S
|
|
||||||
assert_eq!(format_by_duration(&(ONE_SECOND * 9).into()), Format::S);
|
|
||||||
// Ss
|
|
||||||
assert_eq!(format_by_duration(&(10 * ONE_SECOND).into()), Format::Ss);
|
|
||||||
// Ss
|
|
||||||
assert_eq!(format_by_duration(&(59 * ONE_SECOND).into()), Format::Ss);
|
|
||||||
// MSs
|
|
||||||
assert_eq!(format_by_duration(&ONE_MINUTE.into()), Format::MSs);
|
|
||||||
// HhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_DAY.saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::HhMmSs
|
|
||||||
);
|
|
||||||
// DHhMmSs
|
|
||||||
assert_eq!(format_by_duration(&ONE_DAY.into()), Format::DHhMmSs);
|
|
||||||
// DHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&((10 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::DHhMmSs
|
|
||||||
);
|
|
||||||
// DdHhMmSs
|
|
||||||
assert_eq!(format_by_duration(&(10 * ONE_DAY).into()), Format::DdHhMmSs);
|
|
||||||
// DdHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&((100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::DdHhMmSs
|
|
||||||
);
|
|
||||||
// DddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_DAY).into()),
|
|
||||||
Format::DddHhMmSs
|
|
||||||
);
|
|
||||||
// DddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_YEAR.saturating_sub(ONE_SECOND).into())),
|
|
||||||
Format::DddHhMmSs
|
|
||||||
);
|
|
||||||
// YDHhMmSs
|
|
||||||
assert_eq!(format_by_duration(&ONE_YEAR.into()), Format::YDHhMmSs);
|
|
||||||
// YDdHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::YDdHhMmSs
|
|
||||||
);
|
|
||||||
// YDddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_YEAR + 100 * ONE_DAY).into()),
|
|
||||||
Format::YDddHhMmSs
|
|
||||||
);
|
|
||||||
// YDddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&((10 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::YDddHhMmSs
|
|
||||||
);
|
|
||||||
// YyDHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR).into()),
|
|
||||||
Format::YyDHhMmSs
|
|
||||||
);
|
|
||||||
// YyDdHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
|
|
||||||
Format::YyDdHhMmSs
|
|
||||||
);
|
|
||||||
// YyDdHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::YyDdHhMmSs
|
|
||||||
);
|
|
||||||
// YyDddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
|
|
||||||
Format::YyDddHhMmSs
|
|
||||||
);
|
|
||||||
// YyDddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&((100 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::YyDddHhMmSs
|
|
||||||
);
|
|
||||||
// YyyDHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR).into()),
|
|
||||||
Format::YyyDHhMmSs
|
|
||||||
);
|
|
||||||
// YyyDdHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
|
|
||||||
Format::YyyDdHhMmSs
|
|
||||||
);
|
|
||||||
// YyyDdHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
|
||||||
Format::YyyDdHhMmSs
|
|
||||||
);
|
|
||||||
// YyyDddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
|
|
||||||
Format::YyyDddHhMmSs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_by_duration_days() {
|
|
||||||
// DHhMmSs
|
|
||||||
assert_eq!(format_by_duration(&ONE_DAY.into()), Format::DHhMmSs);
|
|
||||||
// DdHhMmSs
|
|
||||||
assert_eq!(format_by_duration(&(10 * ONE_DAY).into()), Format::DdHhMmSs);
|
|
||||||
// DddHhMmSs
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(101 * ONE_DAY).into()),
|
|
||||||
Format::DddHhMmSs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_by_duration_years() {
|
|
||||||
// YDHhMmSs (1 year, 0 days)
|
|
||||||
assert_eq!(format_by_duration(&ONE_YEAR.into()), Format::YDHhMmSs);
|
|
||||||
|
|
||||||
// YDHhMmSs (1 year, 1 day)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_YEAR + ONE_DAY).into()),
|
|
||||||
Format::YDHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YDdHhMmSs (1 year, 10 days)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_YEAR + 10 * ONE_DAY).into()),
|
|
||||||
Format::YDdHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YDddHhMmSs (1 year, 100 days)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(ONE_YEAR + 100 * ONE_DAY).into()),
|
|
||||||
Format::YDddHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YyDHhMmSs (10 years)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR).into()),
|
|
||||||
Format::YyDHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YyDdHhMmSs (10 years, 10 days)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
|
|
||||||
Format::YyDdHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YyDddHhMmSs (10 years, 100 days)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
|
|
||||||
Format::YyDddHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YyyDHhMmSs (100 years)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR).into()),
|
|
||||||
Format::YyyDHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YyyDdHhMmSs (100 years, 10 days)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
|
|
||||||
Format::YyyDdHhMmSs
|
|
||||||
);
|
|
||||||
|
|
||||||
// YyyDddHhMmSs (100 years, 100 days)
|
|
||||||
assert_eq!(
|
|
||||||
format_by_duration(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
|
|
||||||
Format::YyyDddHhMmSs
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_default_edit_mode_hhmmss() {
|
fn test_default_edit_mode_hhmmss() {
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||||
@@ -289,278 +63,6 @@ fn test_default_edit_mode_ss() {
|
|||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_up_stays_in_seconds() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_MINUTE - ONE_SECOND,
|
|
||||||
current_value: ONE_MINUTE - ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_up();
|
|
||||||
// Edit mode should stay on seconds
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_up_stays_in_minutes() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_HOUR - ONE_SECOND,
|
|
||||||
current_value: ONE_HOUR - ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
c.edit_up();
|
|
||||||
// Edit mode should stay on minutes
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_up_stays_in_hours() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_DAY - ONE_SECOND,
|
|
||||||
current_value: ONE_DAY - ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_up();
|
|
||||||
// Edit mode should stay on hours
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_up_stays_in_days() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_YEAR - ONE_DAY,
|
|
||||||
current_value: ONE_YEAR - ONE_DAY,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next(); // Hours
|
|
||||||
c.edit_next(); // Days
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_up();
|
|
||||||
// Edit mode should stay on days
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_up_overflow_protection() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: MAX_DURATION.saturating_sub(ONE_SECOND),
|
|
||||||
current_value: MAX_DURATION.saturating_sub(ONE_SECOND),
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next(); // Hours
|
|
||||||
c.edit_next(); // Days
|
|
||||||
c.edit_next(); // Years
|
|
||||||
c.edit_up(); // +1y
|
|
||||||
assert!(Duration::from(*c.get_current_value()) <= MAX_DURATION);
|
|
||||||
c.edit_prev(); // Days
|
|
||||||
c.edit_up(); // +1d
|
|
||||||
assert!(Duration::from(*c.get_current_value()) <= MAX_DURATION);
|
|
||||||
c.edit_prev(); // Hours
|
|
||||||
c.edit_up(); // +1h
|
|
||||||
assert!(Duration::from(*c.get_current_value()) <= MAX_DURATION);
|
|
||||||
c.edit_prev(); // Minutes
|
|
||||||
c.edit_up(); // +1m
|
|
||||||
assert!(Duration::from(*c.get_current_value()) <= MAX_DURATION);
|
|
||||||
c.edit_prev(); // Sec.
|
|
||||||
c.edit_up(); // +1s
|
|
||||||
c.edit_up(); // +1s
|
|
||||||
c.edit_up(); // +1s
|
|
||||||
assert!(Duration::from(*c.get_current_value()) <= MAX_DURATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_down_years_to_days() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_YEAR + ONE_DAY,
|
|
||||||
current_value: ONE_YEAR + ONE_DAY,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next(); // Hours
|
|
||||||
c.edit_next(); // Days
|
|
||||||
c.edit_next(); // Years
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_down();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_down_days_to_hours() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_DAY + ONE_HOUR,
|
|
||||||
current_value: ONE_DAY + ONE_HOUR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next(); // Hours
|
|
||||||
c.edit_next(); // Days
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_down();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_down_hours_to_minutes() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_HOUR + ONE_MINUTE,
|
|
||||||
current_value: ONE_HOUR + ONE_MINUTE,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next(); // Hours
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_down();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_down_minutes_to_seconds() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_MINUTE,
|
|
||||||
current_value: ONE_MINUTE,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
c.edit_down();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_next_ydddhhmmssd() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_YEAR,
|
|
||||||
current_value: ONE_YEAR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: true,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on - should start at Minutes
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_hours_in_dhhmmss_format() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_DAY + ONE_HOUR,
|
|
||||||
current_value: ONE_DAY + ONE_HOUR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next(); // Move to Hours
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
|
|
||||||
// Increment hours - should stay in Hours edit mode
|
|
||||||
c.edit_up();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
assert_eq!(
|
|
||||||
Duration::from(*c.get_current_value()),
|
|
||||||
ONE_DAY + 2 * ONE_HOUR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_next_ydddhhmmss() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_YEAR,
|
|
||||||
current_value: ONE_YEAR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on - should start at Minutes
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_next_dhhmmssd() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_DAY,
|
|
||||||
current_value: ONE_DAY,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: true,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on - should start at Minutes (following existing pattern)
|
|
||||||
c.toggle_edit();
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit_next_hhmmssd() {
|
fn test_edit_next_hhmmssd() {
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||||
@@ -576,10 +78,6 @@ fn test_edit_next_hhmmssd() {
|
|||||||
c.edit_next();
|
c.edit_next();
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
||||||
c.edit_next();
|
c.edit_next();
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
||||||
c.edit_next();
|
c.edit_next();
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
||||||
@@ -602,10 +100,6 @@ fn test_edit_next_hhmmss() {
|
|||||||
c.edit_next();
|
c.edit_next();
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
||||||
c.edit_next();
|
c.edit_next();
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
||||||
c.edit_next();
|
c.edit_next();
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
||||||
@@ -665,25 +159,6 @@ fn test_edit_next_ssd() {
|
|||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_next_sd() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_SECOND,
|
|
||||||
current_value: ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: true,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit_next_ss() {
|
fn test_edit_next_ss() {
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||||
@@ -697,103 +172,10 @@ fn test_edit_next_ss() {
|
|||||||
// toggle on
|
// toggle on
|
||||||
c.toggle_edit();
|
c.toggle_edit();
|
||||||
c.edit_next();
|
c.edit_next();
|
||||||
|
println!("mode -> {:?}", c.get_mode());
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_next_s() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_SECOND,
|
|
||||||
current_value: ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_next();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_prev_ydddhhmmssd() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_YEAR,
|
|
||||||
current_value: ONE_YEAR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: true,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on - should start at Minutes
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_prev_ydddhhmmss() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_YEAR,
|
|
||||||
current_value: ONE_YEAR,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on - should start at Minutes
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Years, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_prev_dhhmmssd() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_DAY,
|
|
||||||
current_value: ONE_DAY,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: true,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on - should start at Minutes
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Days, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Minutes, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit_prev_hhmmssd() {
|
fn test_edit_prev_hhmmssd() {
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||||
@@ -893,25 +275,6 @@ fn test_edit_prev_ssd() {
|
|||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_prev_sd() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_SECOND,
|
|
||||||
current_value: ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: true,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Decis, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit_prev_ss() {
|
fn test_edit_prev_ss() {
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||||
@@ -929,23 +292,6 @@ fn test_edit_prev_ss() {
|
|||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edit_prev_s() {
|
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
|
||||||
initial_value: ONE_SECOND,
|
|
||||||
current_value: ONE_SECOND,
|
|
||||||
tick_value: ONE_DECI_SECOND,
|
|
||||||
with_decis: false,
|
|
||||||
app_tx: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// toggle on
|
|
||||||
c.toggle_edit();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
c.edit_prev();
|
|
||||||
assert!(matches!(c.get_mode(), Mode::Editable(Time::Seconds, _)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_edit_up_ss() {
|
fn test_edit_up_ss() {
|
||||||
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
let mut c = ClockState::<Timer>::new(ClockStateArgs {
|
||||||
|
|||||||
@@ -194,15 +194,9 @@ impl TuiEventHandler for CountdownState {
|
|||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
self.clock.edit_next();
|
self.clock.edit_next();
|
||||||
}
|
}
|
||||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
||||||
self.clock.edit_jump_up();
|
|
||||||
}
|
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
self.clock.edit_up();
|
self.clock.edit_up();
|
||||||
}
|
}
|
||||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
||||||
self.clock.edit_jump_down();
|
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
self.clock.edit_down();
|
self.clock.edit_down();
|
||||||
}
|
}
|
||||||
@@ -378,11 +372,10 @@ impl StatefulWidget for Countdown {
|
|||||||
.to_uppercase(),
|
.to_uppercase(),
|
||||||
);
|
);
|
||||||
let widget = ClockWidget::new(self.style, self.blink);
|
let widget = ClockWidget::new(self.style, self.blink);
|
||||||
|
|
||||||
let area = center(
|
let area = center(
|
||||||
area,
|
area,
|
||||||
Constraint::Length(max(
|
Constraint::Length(max(
|
||||||
widget.get_width(state.clock.get_format(), state.clock.with_decis),
|
widget.get_width(&state.clock.get_format(), state.clock.with_decis),
|
||||||
label.width() as u16,
|
label.width() as u16,
|
||||||
)),
|
)),
|
||||||
Constraint::Length(widget.get_height() + 1 /* height of label */),
|
Constraint::Length(widget.get_height() + 1 /* height of label */),
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ use ratatui::{
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FooterState {
|
pub struct FooterState {
|
||||||
show_menu: bool,
|
show_menu: bool,
|
||||||
app_time_format: Option<AppTimeFormat>,
|
app_time_format: AppTimeFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FooterState {
|
impl FooterState {
|
||||||
pub const fn new(show_menu: bool, app_time_format: Option<AppTimeFormat>) -> Self {
|
pub const fn new(show_menu: bool, app_time_format: AppTimeFormat) -> Self {
|
||||||
Self {
|
Self {
|
||||||
show_menu,
|
show_menu,
|
||||||
app_time_format,
|
app_time_format,
|
||||||
@@ -32,12 +32,12 @@ impl FooterState {
|
|||||||
self.show_menu
|
self.show_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn app_time_format(&self) -> &Option<AppTimeFormat> {
|
pub const fn app_time_format(&self) -> &AppTimeFormat {
|
||||||
&self.app_time_format
|
&self.app_time_format
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn set_app_time_format(&mut self, value: Option<AppTimeFormat>) {
|
pub fn toggle_app_time_format(&mut self) {
|
||||||
self.app_time_format = value;
|
self.app_time_format = self.app_time_format.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ impl StatefulWidget for Footer {
|
|||||||
(Content::Countdown, "[c]ountdown"),
|
(Content::Countdown, "[c]ountdown"),
|
||||||
(Content::Timer, "[t]imer"),
|
(Content::Timer, "[t]imer"),
|
||||||
(Content::Pomodoro, "[p]omodoro"),
|
(Content::Pomodoro, "[p]omodoro"),
|
||||||
(Content::LocalTime, "[l]ocal time"),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let [_, area] =
|
let [_, area] =
|
||||||
@@ -72,12 +71,11 @@ impl StatefulWidget for Footer {
|
|||||||
)
|
)
|
||||||
.title(
|
.title(
|
||||||
Line::from(
|
Line::from(
|
||||||
match (state.app_time_format, self.selected_content) {
|
match state.app_time_format {
|
||||||
// Show time
|
// `Hidden` -> no (empty) title
|
||||||
(Some(v), content) if content != Content::LocalTime => format!(" {} " // add some space around
|
AppTimeFormat::Hidden => "".into(),
|
||||||
, self.app_time.format(&v)),
|
// others -> add some space around
|
||||||
// Hide time -> empty
|
_ => format!(" {} ", self.app_time.format(&state.app_time_format))
|
||||||
_ => "".into(),
|
|
||||||
}
|
}
|
||||||
).right_aligned())
|
).right_aligned())
|
||||||
.border_set(border::PLAIN)
|
.border_set(border::PLAIN)
|
||||||
@@ -104,7 +102,8 @@ impl StatefulWidget 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)];
|
||||||
let mut table_rows = vec![
|
let table = Table::new(
|
||||||
|
[
|
||||||
// screens
|
// screens
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(Span::styled(
|
Cell::from(Span::styled(
|
||||||
@@ -133,10 +132,6 @@ impl StatefulWidget for Footer {
|
|||||||
)),
|
)),
|
||||||
])),
|
])),
|
||||||
]),
|
]),
|
||||||
];
|
|
||||||
|
|
||||||
if self.selected_content != Content::LocalTime {
|
|
||||||
table_rows.extend_from_slice(&[
|
|
||||||
// controls - 1. row
|
// controls - 1. row
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(Span::styled(
|
Cell::from(Span::styled(
|
||||||
@@ -220,31 +215,19 @@ impl StatefulWidget for Footer {
|
|||||||
scrollbar::VERTICAL.begin
|
scrollbar::VERTICAL.begin
|
||||||
)),
|
)),
|
||||||
Span::from(SPACE),
|
Span::from(SPACE),
|
||||||
Span::from(format!(
|
|
||||||
// ctrl + ↑
|
|
||||||
"[^{}]edit up 10x",
|
|
||||||
scrollbar::VERTICAL.begin
|
|
||||||
)),
|
|
||||||
Span::from(SPACE),
|
|
||||||
Span::from(format!(
|
Span::from(format!(
|
||||||
// ↓
|
// ↓
|
||||||
"[{}]edit up",
|
"[{}]edit up",
|
||||||
scrollbar::VERTICAL.end
|
scrollbar::VERTICAL.end
|
||||||
)),
|
)),
|
||||||
Span::from(SPACE),
|
|
||||||
Span::from(format!(
|
|
||||||
// ctrl + ↓
|
|
||||||
"[^{}]edit up 10x",
|
|
||||||
scrollbar::VERTICAL.end
|
|
||||||
)),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
]),
|
]),
|
||||||
])
|
],
|
||||||
}
|
widths,
|
||||||
|
)
|
||||||
let table = Table::new(table_rows, widths).column_spacing(1);
|
.column_spacing(1);
|
||||||
|
|
||||||
Widget::render(table, menu_area, buf);
|
Widget::render(table, menu_area, buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
use ratatui::{
|
|
||||||
buffer::Buffer,
|
|
||||||
layout::{Constraint, Layout, Rect},
|
|
||||||
style::{Modifier, Style},
|
|
||||||
text::{Line, Span},
|
|
||||||
widgets::{StatefulWidget, Widget},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
common::{AppTime, AppTimeFormat, Style as DigitStyle},
|
|
||||||
duration::DurationEx,
|
|
||||||
events::{TuiEvent, TuiEventHandler},
|
|
||||||
utils::center,
|
|
||||||
widgets::clock_elements::{
|
|
||||||
COLON_WIDTH, Colon, DIGIT_HEIGHT, DIGIT_SPACE_WIDTH, DIGIT_WIDTH, Digit,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use std::cmp::max;
|
|
||||||
|
|
||||||
/// State for `LocalTimeWidget`
|
|
||||||
pub struct LocalTimeState {
|
|
||||||
time: AppTime,
|
|
||||||
format: AppTimeFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LocalTimeStateArgs {
|
|
||||||
pub app_time: AppTime,
|
|
||||||
pub app_time_format: AppTimeFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LocalTimeState {
|
|
||||||
pub fn new(args: LocalTimeStateArgs) -> Self {
|
|
||||||
let LocalTimeStateArgs {
|
|
||||||
app_time,
|
|
||||||
app_time_format,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
Self {
|
|
||||||
time: app_time,
|
|
||||||
format: app_time_format,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_app_time(&mut self, app_time: AppTime) {
|
|
||||||
self.time = app_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_app_time_format(&mut self, format: AppTimeFormat) {
|
|
||||||
self.format = format;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TuiEventHandler for LocalTimeState {
|
|
||||||
fn update(&mut self, event: TuiEvent) -> Option<TuiEvent> {
|
|
||||||
Some(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct LocalTimeWidget {
|
|
||||||
pub style: DigitStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LocalTimeWidget {
|
|
||||||
fn get_horizontal_lengths(&self, format: &AppTimeFormat) -> Vec<u16> {
|
|
||||||
const PERIOD_WIDTH: u16 = 2; // PM or AM
|
|
||||||
|
|
||||||
match format {
|
|
||||||
AppTimeFormat::HhMmSs => vec![
|
|
||||||
DIGIT_WIDTH, // H
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // h
|
|
||||||
COLON_WIDTH, // :
|
|
||||||
DIGIT_WIDTH, // M
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // m
|
|
||||||
COLON_WIDTH, // :
|
|
||||||
DIGIT_WIDTH, // S
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // s
|
|
||||||
],
|
|
||||||
AppTimeFormat::HhMm => vec![
|
|
||||||
DIGIT_WIDTH, // H
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // h
|
|
||||||
COLON_WIDTH, // :
|
|
||||||
DIGIT_WIDTH, // M
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // m
|
|
||||||
],
|
|
||||||
AppTimeFormat::Hh12Mm => vec![
|
|
||||||
DIGIT_SPACE_WIDTH + PERIOD_WIDTH, // (space) + (empty period) to center everything well horizontally
|
|
||||||
DIGIT_WIDTH, // H
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // h
|
|
||||||
COLON_WIDTH, // :
|
|
||||||
DIGIT_WIDTH, // M
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
DIGIT_WIDTH, // m
|
|
||||||
DIGIT_SPACE_WIDTH, // (space)
|
|
||||||
PERIOD_WIDTH, // period
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatefulWidget for LocalTimeWidget {
|
|
||||||
type State = LocalTimeState;
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
||||||
let current_value: DurationEx = state.time.as_duration_of_today().into();
|
|
||||||
let hours = current_value.hours_mod();
|
|
||||||
let hours12 = current_value.hours_mod_12();
|
|
||||||
let minutes = current_value.minutes_mod();
|
|
||||||
let seconds = current_value.seconds_mod();
|
|
||||||
let symbol = self.style.get_digit_symbol();
|
|
||||||
|
|
||||||
let label = Line::raw("Local Time".to_uppercase());
|
|
||||||
let label_date = Line::raw(state.time.format_date().to_uppercase());
|
|
||||||
let mut content_width = max(label.width(), label_date.width()) as u16;
|
|
||||||
|
|
||||||
let format = state.format;
|
|
||||||
let widths = self.get_horizontal_lengths(&format);
|
|
||||||
let mut widths = widths;
|
|
||||||
// Special case for `Hh12Mm`
|
|
||||||
// It might be `h:Mm` OR `Hh:Mm` depending on `hours12`
|
|
||||||
if state.format == AppTimeFormat::Hh12Mm && hours12 < 10 {
|
|
||||||
// single digit means, no (zero) width's for `H` and `space`
|
|
||||||
widths[1] = 0; // `H`
|
|
||||||
widths[2] = 0; // `space`
|
|
||||||
}
|
|
||||||
|
|
||||||
content_width = max(widths.iter().sum(), content_width);
|
|
||||||
let v_heights = [
|
|
||||||
1, // empty (offset) to keep everything centered vertically comparing to "clock" widgets with one label only
|
|
||||||
DIGIT_HEIGHT, // local time
|
|
||||||
1, // label
|
|
||||||
1, // date
|
|
||||||
];
|
|
||||||
|
|
||||||
let area = center(
|
|
||||||
area,
|
|
||||||
Constraint::Length(content_width),
|
|
||||||
Constraint::Length(v_heights.iter().sum()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let [_, v1, v2, v3] = Layout::vertical(Constraint::from_lengths(v_heights)).areas(area);
|
|
||||||
|
|
||||||
match state.format {
|
|
||||||
AppTimeFormat::HhMmSs => {
|
|
||||||
let [hh, _, h, c_hm, mm, _, m, c_ms, ss, _, s] =
|
|
||||||
Layout::horizontal(Constraint::from_lengths(widths)).areas(v1);
|
|
||||||
Digit::new(hours / 10, false, symbol).render(hh, buf);
|
|
||||||
Digit::new(hours % 10, false, symbol).render(h, buf);
|
|
||||||
Colon::new(symbol).render(c_hm, buf);
|
|
||||||
Digit::new(minutes / 10, false, symbol).render(mm, buf);
|
|
||||||
Digit::new(minutes % 10, false, symbol).render(m, buf);
|
|
||||||
Colon::new(symbol).render(c_ms, buf);
|
|
||||||
Digit::new(seconds / 10, false, symbol).render(ss, buf);
|
|
||||||
Digit::new(seconds % 10, false, symbol).render(s, buf);
|
|
||||||
}
|
|
||||||
AppTimeFormat::HhMm => {
|
|
||||||
let [hh, _, h, c_hm, mm, _, m] =
|
|
||||||
Layout::horizontal(Constraint::from_lengths(widths)).areas(v1);
|
|
||||||
Digit::new(hours / 10, false, symbol).render(hh, buf);
|
|
||||||
Digit::new(hours % 10, false, symbol).render(h, buf);
|
|
||||||
Colon::new(symbol).render(c_hm, buf);
|
|
||||||
Digit::new(minutes / 10, false, symbol).render(mm, buf);
|
|
||||||
Digit::new(minutes % 10, false, symbol).render(m, buf);
|
|
||||||
}
|
|
||||||
AppTimeFormat::Hh12Mm => {
|
|
||||||
let [_, hh, _, h, c_hm, mm, _, m, _, p] =
|
|
||||||
Layout::horizontal(Constraint::from_lengths(widths)).areas(v1);
|
|
||||||
// Hh
|
|
||||||
if hours12 >= 10 {
|
|
||||||
Digit::new(hours12 / 10, false, symbol).render(hh, buf);
|
|
||||||
Digit::new(hours12 % 10, false, symbol).render(h, buf);
|
|
||||||
}
|
|
||||||
// h
|
|
||||||
else {
|
|
||||||
Digit::new(hours12, false, symbol).render(h, buf);
|
|
||||||
}
|
|
||||||
Colon::new(symbol).render(c_hm, buf);
|
|
||||||
Digit::new(minutes / 10, false, symbol).render(mm, buf);
|
|
||||||
Digit::new(minutes % 10, false, symbol).render(m, buf);
|
|
||||||
Span::styled(
|
|
||||||
state.time.get_period().to_uppercase(),
|
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
|
||||||
)
|
|
||||||
.render(p, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
label.centered().render(v2, buf);
|
|
||||||
label_date.centered().render(v3, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -250,8 +250,10 @@ impl StatefulWidget for PomodoroWidget {
|
|||||||
let area = center(
|
let area = center(
|
||||||
area,
|
area,
|
||||||
Constraint::Length(max(
|
Constraint::Length(max(
|
||||||
clock_widget
|
clock_widget.get_width(
|
||||||
.get_width(state.get_clock().get_format(), state.get_clock().with_decis),
|
&state.get_clock().get_format(),
|
||||||
|
state.get_clock().with_decis,
|
||||||
|
),
|
||||||
label.width() as u16,
|
label.width() as u16,
|
||||||
)),
|
)),
|
||||||
Constraint::Length(
|
Constraint::Length(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use crate::{
|
|||||||
utils::center,
|
utils::center,
|
||||||
widgets::clock::{self, ClockState, ClockWidget},
|
widgets::clock::{self, ClockState, ClockWidget},
|
||||||
};
|
};
|
||||||
use crossterm::event::KeyModifiers;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::event::KeyCode,
|
crossterm::event::KeyCode,
|
||||||
@@ -61,17 +60,11 @@ impl TuiEventHandler for TimerState {
|
|||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
self.clock.edit_prev();
|
self.clock.edit_prev();
|
||||||
}
|
}
|
||||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
||||||
self.clock.edit_jump_up();
|
|
||||||
}
|
|
||||||
// change value up
|
// change value up
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
self.clock.edit_up();
|
self.clock.edit_up();
|
||||||
}
|
}
|
||||||
// change value down
|
// change value down
|
||||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
||||||
self.clock.edit_jump_down();
|
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
self.clock.edit_down();
|
self.clock.edit_down();
|
||||||
}
|
}
|
||||||
@@ -114,7 +107,7 @@ impl StatefulWidget for Timer {
|
|||||||
let area = center(
|
let area = center(
|
||||||
area,
|
area,
|
||||||
Constraint::Length(max(
|
Constraint::Length(max(
|
||||||
clock_widget.get_width(clock.get_format(), clock.with_decis),
|
clock_widget.get_width(&clock.get_format(), clock.with_decis),
|
||||||
label.width() as u16,
|
label.width() as u16,
|
||||||
)),
|
)),
|
||||||
Constraint::Length(clock_widget.get_height() + 1 /* height of label */),
|
Constraint::Length(clock_widget.get_height() + 1 /* height of label */),
|
||||||
|
|||||||