11 Commits

Author SHA1 Message Date
Jens Krause
ec18da0664 fix(ci): remove magic nix cache action (#57) 2025-01-22 09:56:28 +01:00
Jens Krause
59c99f4f5c fix(build): statically linked binaries for linux (#55) 2025-01-22 09:43:51 +01:00
Jens Krause
6d2bf5ac09 Edit countdown by local time (#49) 2025-01-13 18:44:56 +01:00
Jens Krause
b1efb1eb62 rust 1.84, update deps (#48)
- `nix flake update`
- `cargo update`
2025-01-11 16:48:37 +01:00
Jens Krause
4ee5d7b4e9 Prepare v1.0.0 (#47)
* update README
* update CHANGELOG
* update demo
* bump v1.0.0
2025-01-10 16:36:57 +01:00
Jens Krause
9ea9f88266 feat(countdown): persist elapsed time (#46) 2025-01-10 16:01:03 +01:00
Jens Krause
c8af76c9e5 feat(countdown): rocket countdown (#45) 2025-01-08 18:52:18 +01:00
Jens Krause
468b4a5abf simplify style settings, improve naming (#44)
* simplify style settings by passing `style` directly to Widgets. No need to store it in `state` of widgets.
* remove unneeded things
* naming (state vs. widgets)
2025-01-07 19:02:57 +01:00
Jens Krause
8603a823e4 fix(footer): 12-hour format incl. tests (#43) 2025-01-06 19:22:02 +01:00
Jens Krause
94bdeeab11 feat(footer): show local time (#42) 2025-01-06 18:31:22 +01:00
Orhun Parmaksız
66c6d7fc46 docs: Add instructions for installing from the AUR (#41) 2025-01-06 09:06:15 +01:00
27 changed files with 1389 additions and 414 deletions

View File

@@ -12,7 +12,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Check formatting - name: Check formatting
run: nix develop --command cargo fmt --all -- --check run: nix develop --command cargo fmt --all -- --check
- name: Run clippy - name: Run clippy
@@ -25,7 +24,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Run tests - name: Run tests
run: nix develop --command cargo test run: nix develop --command cargo test
@@ -34,6 +32,5 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build project - name: Build project
run: nix build .#timr run: nix build .#timr

View File

@@ -31,7 +31,7 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
os_target: linux os_target: linux
binary_name: timr-tui binary_name: timr-tui
arch: x86_64 # `x86_64` by default arch: x86_64 # based on target 'x86_64-unknown-linux-musl' defined by `CARGO_BUILD_TARGET` in flake.nix
- os: ubuntu-latest - os: ubuntu-latest
os_target: windows os_target: windows
binary_name: timr-tui.exe binary_name: timr-tui.exe
@@ -43,14 +43,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build (windows) - name: Build (windows)
if: matrix.os_target == 'windows' if: matrix.os_target == 'windows'
run: nix build .#windows run: nix build .#windows
- name: Build (linux/macos) - name: Build (linux)
if: matrix.os_target != 'windows' if: matrix.os_target == 'linux'
run: nix build .#linuxStatic
- name: Build (macos)
if: matrix.os_target == 'macos'
run: nix build run: nix build
- name: Copy artifact - name: Copy artifact

View File

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

348
Cargo.lock generated
View File

@@ -28,9 +28,9 @@ dependencies = [
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.20" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "anstream" name = "anstream"
@@ -104,9 +104,9 @@ dependencies = [
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -134,9 +134,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.1" version = "1.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -149,9 +149,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.23" version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -159,9 +159,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.23" version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -171,9 +171,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.18" version = "4.5.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -222,9 +222,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]] [[package]]
name = "compact_str" name = "compact_str"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [ dependencies = [
"castaway", "castaway",
"cfg-if", "cfg-if",
@@ -297,10 +297,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "diff" name = "deranged"
version = "0.1.13" version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "directories" name = "directories"
@@ -336,13 +339,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "erased-serde"
version = "0.3.9" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -363,9 +376,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]] [[package]]
name = "futures" name = "futures"
@@ -490,12 +503,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@@ -516,13 +523,12 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]] [[package]]
name = "instability" name = "instability"
version = "0.3.3" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d"
dependencies = [ dependencies = [
"darling", "darling",
"indoc", "indoc",
"pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -557,9 +563,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.166" version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]] [[package]]
name = "libredox" name = "libredox"
@@ -573,9 +579,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -589,9 +595,12 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d"
dependencies = [
"value-bag",
]
[[package]] [[package]]
name = "lru" name = "lru"
@@ -628,11 +637,10 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"hermit-abi",
"libc", "libc",
"log", "log",
"wasi", "wasi",
@@ -649,6 +657,21 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.32.2"
@@ -713,9 +736,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@@ -724,29 +747,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pretty_assertions" name = "powerfmt"
version = "1.4.1" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
dependencies = [
"diff",
"yansi",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.92" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -774,9 +793,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.7" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@@ -844,22 +863,22 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.41" version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.18" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "ryu" name = "ryu"
@@ -875,18 +894,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.215" version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.215" version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -894,10 +913,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_fmt"
version = "1.0.134" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4"
dependencies = [
"serde",
]
[[package]]
name = "serde_json"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1010,10 +1038,88 @@ dependencies = [
] ]
[[package]] [[package]]
name = "syn" name = "sval"
version = "2.0.89" version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8"
[[package]]
name = "sval_buffer"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f"
dependencies = [
"sval",
"sval_ref",
]
[[package]]
name = "sval_dynamic"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4"
dependencies = [
"sval",
]
[[package]]
name = "sval_fmt"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94"
dependencies = [
"itoa",
"ryu",
"sval",
]
[[package]]
name = "sval_json"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155"
dependencies = [
"itoa",
"ryu",
"sval",
]
[[package]]
name = "sval_nested"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f"
dependencies = [
"sval",
"sval_buffer",
"sval_ref",
]
[[package]]
name = "sval_ref"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa"
dependencies = [
"sval",
]
[[package]]
name = "sval_serde"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a"
dependencies = [
"serde",
"sval",
"sval_nested",
]
[[package]]
name = "syn"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1050,9 +1156,42 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "timr-tui" name = "timr-tui"
version = "0.9.0" version = "1.1.0-alpha"
dependencies = [ dependencies = [
"clap", "clap",
"color-eyre", "color-eyre",
@@ -1063,6 +1202,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"strum", "strum",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
@@ -1072,9 +1212,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.41.1" version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -1090,9 +1230,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1101,9 +1241,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.16" version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@@ -1112,9 +1252,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.12" version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@@ -1194,6 +1334,12 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "typeid"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.14"
@@ -1241,6 +1387,42 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
dependencies = [
"value-bag-serde1",
"value-bag-sval2",
]
[[package]]
name = "value-bag-serde1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b"
dependencies = [
"erased-serde",
"serde",
"serde_fmt",
]
[[package]]
name = "value-bag-sval2"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a"
dependencies = [
"sval",
"sval_buffer",
"sval_dynamic",
"sval_fmt",
"sval_json",
"sval_ref",
"sval_serde",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@@ -1416,9 +1598,3 @@ name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

View File

@@ -1,9 +1,9 @@
[package] [package]
name = "timr-tui" name = "timr-tui"
version = "0.9.0" version = "1.1.0-alpha"
description = "TUI to organize your time: Pomodoro, Countdown, Timer." description = "TUI to organize your time: Pomodoro, Countdown, Timer."
edition = "2021" edition = "2021"
rust-version = "1.82.0" rust-version = "1.84.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"
@@ -26,3 +26,4 @@ tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
directories = "5.0.1" directories = "5.0.1"
clap = { version = "4.5.23", features = ["derive"] } clap = { version = "4.5.23", features = ["derive"] }
time = { version = "0.3.37", features = ["formatting", "local-offset"] }

View File

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

BIN
demo/countdown-met.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

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

@@ -0,0 +1,22 @@
Output demo/countdown-met.gif
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
Set Theme "iceberg-light"
Set FontSize 14
Set Width 800
Set Height 400
Set Padding 0
Set Margin 1
# --- START ---
Set LoopOffset 4
Hide
Type "cargo run -- -m c -c 3"
Enter
Sleep 0.2
Show
Type "s"
Sleep 6
Type "r"
Sleep 1

BIN
demo/local-time.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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

@@ -0,0 +1,22 @@
Output demo/local-time.gif
# https://github.com/charmbracelet/vhs/blob/main/THEMES.md
Set Theme "AtomOneLight"
Set FontSize 14
Set Width 800
Set Height 400
Set Padding 0
Set Margin 1
# --- START ---
Set LoopOffset 4
Hide
Type "cargo run -- -m c"
Enter
Sleep 0.2
Show
Sleep 1
# --- toggle local time ---
Type@1.5s ":::"
Sleep 1.5

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

24
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1733286231, "lastModified": 1736566337,
"narHash": "sha256-mlIDSv1/jqWnH8JTiOV7GMUNPCXL25+6jmD+7hdxx5o=", "narHash": "sha256-SC0eDcZPqISVt6R0UfGPyQLrI0+BppjjtQ3wcSlk0oI=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "af1556ecda8bcf305820f68ec2f9d77b41d9cc80", "rev": "9172acc1ee6c7e1cbafc3044ff850c568c75a5a3",
"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": 1732689334, "lastModified": 1736577158,
"narHash": "sha256-yKI1KiZ0+bvDvfPTQ1ZT3oP/nIu3jPYm4dnbRd6hYg4=", "narHash": "sha256-ngnAENZ+vmzOFgnj0EDtHj22nuH7MQB+EqzUmdbvaqA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "a8a983027ca02b363dfc82fbe3f7d9548a8d3dce", "rev": "05dcdb02ea657f81b13d99bd0ca36b09d25f4c43",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -56,11 +56,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1733212471, "lastModified": 1736344531,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", "narHash": "sha256-8YVQ9ZbSfuUk2bUf2KRj60NRraLPKPS0Q4QFTbc+c2c=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", "rev": "bffc22eb12172e6db3c5dde9e3e5628f8e3e7912",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -81,11 +81,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1732633904, "lastModified": 1736517563,
"narHash": "sha256-7VKcoLug9nbAN2txqVksWHHJplqK9Ou8dXjIZAIYSGc=", "narHash": "sha256-YJ5ajpMsyXITc91ZfnI0Mdocd+tmCFkZ3BLozUkB44M=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "8d5e91c94f80c257ce6dbdfba7bd63a5e8a03fa6", "rev": "4f35021ca9a8e7f9ed4344139b9eaf770a2e5725",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,7 +1,5 @@
{ {
inputs = { inputs = {
# Disable `nixos-unstable` for now, it introduced some `VScode` related errors:
# error: function 'buildVscodeExtension' called without required argument 'pname'
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
crane.url = "github:ipetkov/crane"; crane.url = "github:ipetkov/crane";
@@ -20,12 +18,12 @@
}: }:
flake-utils.lib.eachDefaultSystem (system: let flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
# Using stable toolchain as base
toolchain = with fenix.packages.${system}; toolchain = with fenix.packages.${system};
combine [ combine [
minimal.rustc minimal.rustc
minimal.cargo minimal.cargo
targets.x86_64-pc-windows-gnu.latest.rust-std targets.x86_64-pc-windows-gnu.latest.rust-std
targets.x86_64-unknown-linux-musl.latest.rust-std
]; ];
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain; craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
@@ -35,19 +33,24 @@
cargoArtifacts = craneLib.buildDepsOnly { cargoArtifacts = craneLib.buildDepsOnly {
src = craneLib.cleanCargoSource ./.; src = craneLib.cleanCargoSource ./.;
}; };
strictDeps = true;
doCheck = false; # skip tests during nix build doCheck = false; # skip tests during nix build
}; };
# Native build # Native build
timr = craneLib.buildPackage commonArgs; timr = craneLib.buildPackage commonArgs;
# Linux build w/ statically linked binaries
staticLinuxBuild = craneLib.buildPackage (commonArgs
// {
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
});
# Windows cross-compilation build # Windows cross-compilation build
# @see https://crane.dev/examples/cross-windows.html # @see https://crane.dev/examples/cross-windows.html
crossBuild = craneLib.buildPackage { windowsBuild = craneLib.buildPackage {
src = craneLib.cleanCargoSource ./.; inherit (commonArgs) src strictDeps doCheck;
strictDeps = true;
doCheck = false;
CARGO_BUILD_TARGET = "x86_64-pc-windows-gnu"; CARGO_BUILD_TARGET = "x86_64-pc-windows-gnu";
@@ -68,7 +71,8 @@
packages = { packages = {
inherit timr; inherit timr;
default = timr; default = timr;
windows = crossBuild; linuxStatic = staticLinuxBuild;
windows = windowsBuild;
}; };
# Development shell with all necessary tools # Development shell with all necessary tools

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,10 @@ 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;
// max. 99:59:59
pub const MAX_DURATION: Duration =
Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND);
#[derive(Debug, Clone, Copy, PartialOrd)] #[derive(Debug, Clone, Copy, PartialOrd)]
pub struct DurationEx { pub struct DurationEx {
inner: Duration, inner: Duration,
@@ -86,6 +90,10 @@ impl DurationEx {
let inner = self.inner.saturating_sub(ex.inner); let inner = self.inner.saturating_sub(ex.inner);
Self { inner } Self { inner }
} }
pub fn to_string_with_decis(self) -> String {
format!("{}.{}", self, self.decis())
}
} }
impl fmt::Display for DurationEx { impl fmt::Display for DurationEx {

View File

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

View File

@@ -5,6 +5,7 @@ pub mod clock_elements_test;
#[cfg(test)] #[cfg(test)]
pub mod clock_test; pub mod clock_test;
pub mod countdown; pub mod countdown;
pub mod edit_time;
pub mod footer; pub mod footer;
pub mod header; pub mod header;
pub mod pomodoro; pub mod pomodoro;

View File

@@ -11,20 +11,13 @@ use ratatui::{
use crate::{ use crate::{
common::Style, common::Style,
duration::{ duration::{DurationEx, MAX_DURATION, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
DurationEx, MINS_PER_HOUR, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND,
SECS_PER_MINUTE,
},
utils::center_horizontal, utils::center_horizontal,
widgets::clock_elements::{ widgets::clock_elements::{
Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_WIDTH, DOT_WIDTH, Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_SPACE_WIDTH, DIGIT_WIDTH, DOT_WIDTH,
}, },
}; };
// max. 99:59:59
const MAX_DURATION: Duration =
Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND);
#[derive(Debug, Copy, Clone, Display, PartialEq, Eq)] #[derive(Debug, Copy, Clone, Display, PartialEq, Eq)]
pub enum Time { pub enum Time {
Decis, Decis,
@@ -73,26 +66,45 @@ pub enum Format {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Clock<T> { pub struct ClockState<T> {
initial_value: DurationEx, initial_value: DurationEx,
current_value: DurationEx, current_value: DurationEx,
tick_value: DurationEx, tick_value: DurationEx,
mode: Mode, mode: Mode,
format: Format, format: Format,
pub style: Style,
pub with_decis: bool, pub with_decis: bool,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
pub struct ClockArgs { pub struct ClockStateArgs {
pub initial_value: Duration, pub initial_value: Duration,
pub current_value: Duration, pub current_value: Duration,
pub tick_value: Duration, pub tick_value: Duration,
pub style: Style,
pub with_decis: bool, pub with_decis: bool,
} }
impl<T> Clock<T> { impl<T> ClockState<T> {
pub fn with_mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
pub fn get_mode(&self) -> &Mode {
&self.mode
}
pub fn is_initial(&self) -> bool {
self.mode == Mode::Initial
}
pub fn run(&mut self) {
self.mode = Mode::Tick
}
pub fn is_running(&self) -> bool {
self.mode == Mode::Tick
}
pub fn toggle_pause(&mut self) { pub fn toggle_pause(&mut self) {
self.mode = if self.mode == Mode::Tick { self.mode = if self.mode == Mode::Tick {
Mode::Pause Mode::Pause
@@ -109,6 +121,11 @@ impl<T> Clock<T> {
&self.current_value &self.current_value
} }
pub fn set_current_value(&mut self, duration: DurationEx) {
self.current_value = duration;
self.update_format();
}
pub fn toggle_edit(&mut self) { pub fn toggle_edit(&mut self) {
self.mode = match self.mode.clone() { self.mode = match self.mode.clone() {
Mode::Editable(_, prev) => { Mode::Editable(_, prev) => {
@@ -187,6 +204,7 @@ impl<T> Clock<T> {
}; };
self.update_format(); self.update_format();
} }
pub fn edit_current_down(&mut self) { pub fn edit_current_down(&mut self) {
self.current_value = match self.mode { self.current_value = match self.mode {
Mode::Editable(Time::Decis, _) => { Mode::Editable(Time::Decis, _) => {
@@ -205,14 +223,6 @@ impl<T> Clock<T> {
self.update_mode(); self.update_mode();
} }
pub fn get_mode(&self) -> &Mode {
&self.mode
}
pub fn is_running(&self) -> bool {
self.mode == Mode::Tick
}
pub fn is_edit_mode(&self) -> bool { pub fn is_edit_mode(&self) -> bool {
matches!(self.mode, Mode::Editable(_, _)) matches!(self.mode, Mode::Editable(_, _))
} }
@@ -324,13 +334,12 @@ impl<T> Clock<T> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Countdown {} pub struct Countdown {}
impl Clock<Countdown> { impl ClockState<Countdown> {
pub fn new(args: ClockArgs) -> Self { pub fn new(args: ClockStateArgs) -> Self {
let ClockArgs { let ClockStateArgs {
initial_value, initial_value,
current_value, current_value,
tick_value, tick_value,
style,
with_decis, with_decis,
} = args; } = args;
let mut instance = Self { let mut instance = Self {
@@ -345,7 +354,6 @@ impl Clock<Countdown> {
Mode::Pause Mode::Pause
}, },
format: Format::S, format: Format::S,
style,
with_decis, with_decis,
phantom: PhantomData, phantom: PhantomData,
}; };
@@ -394,13 +402,12 @@ impl Clock<Countdown> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Timer {} pub struct Timer {}
impl Clock<Timer> { impl ClockState<Timer> {
pub fn new(args: ClockArgs) -> Self { pub fn new(args: ClockStateArgs) -> Self {
let ClockArgs { let ClockStateArgs {
initial_value, initial_value,
current_value, current_value,
tick_value, tick_value,
style,
with_decis, with_decis,
} = args; } = args;
let mut instance = Self { let mut instance = Self {
@@ -416,7 +423,6 @@ impl Clock<Timer> {
}, },
format: Format::S, format: Format::S,
phantom: PhantomData, phantom: PhantomData,
style,
with_decis, with_decis,
}; };
// update format once // update format once
@@ -455,12 +461,11 @@ impl Clock<Timer> {
} }
} }
const SPACE_WIDTH: u16 = 1;
pub struct ClockWidget<T> pub struct ClockWidget<T>
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
style: Style,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
@@ -468,8 +473,9 @@ impl<T> ClockWidget<T>
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
pub fn new() -> Self { pub fn new(style: Style) -> Self {
Self { Self {
style,
phantom: PhantomData, phantom: PhantomData,
} }
} }
@@ -489,15 +495,15 @@ where
Format::HhMmSs => add_decis( Format::HhMmSs => add_decis(
vec![ vec![
DIGIT_WIDTH, // h DIGIT_WIDTH, // h
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // h DIGIT_WIDTH, // h
COLON_WIDTH, // : COLON_WIDTH, // :
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
COLON_WIDTH, // : COLON_WIDTH, // :
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
], ],
with_decis, with_decis,
@@ -507,11 +513,11 @@ where
DIGIT_WIDTH, // h DIGIT_WIDTH, // h
COLON_WIDTH, // : COLON_WIDTH, // :
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
COLON_WIDTH, // : COLON_WIDTH, // :
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
], ],
with_decis, with_decis,
@@ -519,11 +525,11 @@ where
Format::MmSs => add_decis( Format::MmSs => add_decis(
vec![ vec![
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
COLON_WIDTH, // : COLON_WIDTH, // :
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
], ],
with_decis, with_decis,
@@ -533,7 +539,7 @@ where
DIGIT_WIDTH, // m DIGIT_WIDTH, // m
COLON_WIDTH, // : COLON_WIDTH, // :
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
], ],
with_decis, with_decis,
@@ -541,7 +547,7 @@ where
Format::Ss => add_decis( Format::Ss => add_decis(
vec![ vec![
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
SPACE_WIDTH, // (space) DIGIT_SPACE_WIDTH, // (space)
DIGIT_WIDTH, // s DIGIT_WIDTH, // s
], ],
with_decis, with_decis,
@@ -568,12 +574,12 @@ impl<T> StatefulWidget for ClockWidget<T>
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
type State = Clock<T>; type State = ClockState<T>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let with_decis = state.with_decis; let with_decis = state.with_decis;
let format = state.format; let format = state.format;
let symbol = state.style.get_digit_symbol(); let symbol = self.style.get_digit_symbol();
let widths = self.get_horizontal_lengths(&format, with_decis); let widths = self.get_horizontal_lengths(&format, with_decis);
let area = center_horizontal( let area = center_horizontal(
area, area,

View File

@@ -9,6 +9,7 @@ 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 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
#[rustfmt::skip] #[rustfmt::skip]
const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [

View File

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

View File

@@ -1,3 +1,15 @@
use crate::{
common::{AppTime, Style},
constants::TICK_VALUE_MS,
duration::{DurationEx, MAX_DURATION},
events::{Event, EventHandler},
utils::center,
widgets::{
clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode},
edit_time::EditTimeState,
},
};
use crossterm::event::KeyModifiers;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::KeyCode, crossterm::event::KeyCode,
@@ -5,69 +17,238 @@ use ratatui::{
text::Line, text::Line,
widgets::{StatefulWidget, Widget}, widgets::{StatefulWidget, Widget},
}; };
use std::cmp::max;
use crate::{ use std::ops::Sub;
common::Style, use std::{cmp::max, time::Duration};
events::{Event, EventHandler}, use time::OffsetDateTime;
utils::center,
widgets::clock::{self, Clock, ClockWidget},
};
use super::edit_time::{EditTimeStateArgs, EditTimeWidget};
/// State for Countdown Widget
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Countdown { pub struct CountdownState {
clock: Clock<clock::Countdown>, /// clock to count down
clock: ClockState<clock::Countdown>,
/// clock to count time after `DONE` - similar to Mission Elapsed Time (MET)
elapsed_clock: ClockState<clock::Timer>,
app_time: AppTime,
/// Edit by local time
edit_time: Option<EditTimeState>,
} }
impl Countdown { impl CountdownState {
pub const fn new(clock: Clock<clock::Countdown>) -> Self { pub fn new(
Self { clock } clock: ClockState<clock::Countdown>,
elapsed_value: Duration,
app_time: AppTime,
) -> Self {
Self {
clock,
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO,
current_value: elapsed_value,
tick_value: Duration::from_millis(TICK_VALUE_MS),
with_decis: false,
})
// A previous `elapsed_value > 0` means the `Clock` was running before,
// but not in `Initial` state anymore. Updating `Mode` here
// is needed to handle `Event::Tick` in `EventHandler::update` properly
.with_mode(if elapsed_value.gt(&Duration::ZERO) {
ClockMode::Pause
} else {
ClockMode::Initial
}),
app_time,
edit_time: None,
} }
pub fn set_style(&mut self, style: Style) {
self.clock.style = style;
} }
pub fn set_with_decis(&mut self, with_decis: bool) { pub fn set_with_decis(&mut self, with_decis: bool) {
self.clock.with_decis = with_decis; self.clock.with_decis = with_decis;
self.elapsed_clock.with_decis = with_decis;
} }
pub fn get_clock(&self) -> &Clock<clock::Countdown> { pub fn get_clock(&self) -> &ClockState<clock::Countdown> {
&self.clock &self.clock
} }
pub fn is_running(&self) -> bool {
self.clock.is_running() || self.elapsed_clock.is_running()
}
pub fn get_elapsed_value(&self) -> &DurationEx {
self.elapsed_clock.get_current_value()
}
pub fn set_app_time(&mut self, app_time: AppTime) {
self.app_time = app_time;
}
fn time_to_edit(&self) -> OffsetDateTime {
// get current value
let d: Duration = (*self.clock.get_current_value()).into();
// transform
let dd = time::Duration::try_from(d).unwrap_or(time::Duration::ZERO);
// substract from `app_time`
OffsetDateTime::from(self.app_time).saturating_add(dd)
}
pub fn min_time_to_edit(&self) -> OffsetDateTime {
OffsetDateTime::from(self.app_time)
}
fn max_time_to_edit(&self) -> OffsetDateTime {
OffsetDateTime::from(self.app_time)
.saturating_add(time::Duration::try_from(MAX_DURATION).unwrap_or(time::Duration::ZERO))
}
fn edit_time_done(&mut self, edit_time: &mut EditTimeState) {
// get diff
let d: time::Duration = edit_time
.get_time()
.sub(OffsetDateTime::from(self.app_time));
// transfrom
let dx: DurationEx = Duration::try_from(d).unwrap_or(Duration::ZERO).into();
// update clock
self.clock.set_current_value(dx);
// remove `edit_time`
self.edit_time = None;
}
pub fn is_clock_edit_mode(&self) -> bool {
self.clock.is_edit_mode()
}
pub fn is_time_edit_mode(&self) -> bool {
self.edit_time.is_some()
}
} }
impl EventHandler for Countdown { impl EventHandler for CountdownState {
fn update(&mut self, event: Event) -> Option<Event> { fn update(&mut self, event: Event) -> Option<Event> {
let edit_mode = self.clock.is_edit_mode(); let is_edit_clock = self.clock.is_edit_mode();
let is_edit_time = self.edit_time.is_some();
match event { match event {
Event::Tick => { Event::Tick => {
if !self.clock.is_done() {
self.clock.tick(); self.clock.tick();
} else {
self.elapsed_clock.tick();
if self.elapsed_clock.is_initial() {
self.elapsed_clock.run();
}
}
let min_time = self.min_time_to_edit();
let max_time = self.max_time_to_edit();
if let Some(edit_time) = &mut self.edit_time {
edit_time.set_min_time(min_time);
edit_time.set_max_time(max_time);
} }
Event::Key(key) if key.code == KeyCode::Char('r') => {
self.clock.reset();
} }
Event::Key(key) => match key.code { Event::Key(key) => match key.code {
KeyCode::Char('r') => { KeyCode::Char('r') => {
// reset both clocks to use intial values
self.clock.reset(); self.clock.reset();
self.elapsed_clock.reset();
// reset `edit_time` back initial value
let time = self.time_to_edit();
if let Some(edit_time) = &mut self.edit_time {
edit_time.set_time(time);
}
} }
KeyCode::Char('s') => { KeyCode::Char('s') => {
// toggle pause status depending on which clock is running
if !self.clock.is_done() {
self.clock.toggle_pause(); self.clock.toggle_pause();
} else {
self.elapsed_clock.toggle_pause();
} }
KeyCode::Char('e') => {
// finish `edit_time` and continue for using `clock`
if let Some(edit_time) = &mut self.edit_time.clone() {
self.edit_time_done(edit_time);
}
}
// STRG + e => toggle edit time
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
// stop editing clock
if self.clock.is_edit_mode() {
// toggle edit mode
self.clock.toggle_edit(); self.clock.toggle_edit();
} }
KeyCode::Left if edit_mode => {
if let Some(edit_time) = &mut self.edit_time.clone() {
self.edit_time_done(edit_time)
} else {
// update `edit_time`
self.edit_time = Some(EditTimeState::new(EditTimeStateArgs {
time: self.time_to_edit(),
min: self.min_time_to_edit(),
max: self.max_time_to_edit(),
}));
}
// stop `clock`
if self.clock.is_running() {
self.clock.toggle_pause();
}
// stop `elapsed_clock`
if self.elapsed_clock.is_running() {
self.elapsed_clock.toggle_pause();
}
}
// STRG + e => toggle edit clock
KeyCode::Char('e') => {
// toggle edit mode
self.clock.toggle_edit();
// stop `elapsed_clock`
if self.elapsed_clock.is_running() {
self.elapsed_clock.toggle_pause();
}
// finish `edit_time` and continue for using `clock`
if let Some(edit_time) = &mut self.edit_time.clone() {
self.edit_time_done(edit_time);
}
}
KeyCode::Left if is_edit_clock => {
self.clock.edit_next(); self.clock.edit_next();
} }
KeyCode::Right if edit_mode => { KeyCode::Left if is_edit_time => {
// safe unwrap because of previous check in `is_edit_time`
self.edit_time.as_mut().unwrap().next();
}
KeyCode::Right if is_edit_clock => {
self.clock.edit_prev(); self.clock.edit_prev();
} }
KeyCode::Up if edit_mode => { KeyCode::Right if is_edit_time => {
self.clock.edit_up(); // safe unwrap because of previous check in `is_edit_time`
self.edit_time.as_mut().unwrap().prev();
} }
KeyCode::Down if edit_mode => { KeyCode::Up if is_edit_clock => {
self.clock.edit_up();
// whenever `clock`'s value is changed, reset `elapsed_clock`
self.elapsed_clock.reset();
}
KeyCode::Up if is_edit_time => {
// safe unwrap because of previous check in `is_edit_time`
self.edit_time.as_mut().unwrap().up();
// whenever `clock`'s value is changed, reset `elapsed_clock`
self.elapsed_clock.reset();
}
KeyCode::Down if is_edit_clock => {
self.clock.edit_down(); self.clock.edit_down();
// whenever clock value is changed, reset timer
self.elapsed_clock.reset();
}
KeyCode::Down if is_edit_time => {
// safe unwrap because of previous check in `is_edit_time`
self.edit_time.as_mut().unwrap().down();
// whenever clock value is changed, reset timer
self.elapsed_clock.reset();
} }
_ => return Some(event), _ => return Some(event),
}, },
@@ -77,26 +258,81 @@ impl EventHandler for Countdown {
} }
} }
pub struct CountdownWidget; pub struct Countdown {
pub style: Style,
}
impl StatefulWidget for CountdownWidget { fn human_days_diff(a: &OffsetDateTime, b: &OffsetDateTime) -> String {
type State = Countdown; let days_diff = (a.date() - b.date()).whole_days();
match days_diff {
0 => "today".to_owned(),
1 => "tomorrow".to_owned(),
n => format!("+{}days", n),
}
}
impl StatefulWidget for Countdown {
type State = CountdownState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let clock = ClockWidget::new(); // render `edit_time` OR `clock`
let label = Line::raw((format!("Countdown {}", state.clock.get_mode())).to_uppercase()); if let Some(edit_time) = &mut state.edit_time {
let label = Line::raw(
format!(
"Countdown {} {}",
edit_time.get_selected().clone(),
human_days_diff(edit_time.get_time(), &state.app_time.into())
)
.to_uppercase(),
);
let widget = EditTimeWidget::new(self.style);
let area = center(
area,
Constraint::Length(max(widget.get_width(), label.width() as u16)),
Constraint::Length(widget.get_height() + 1 /* height of label */),
);
let [v1, v2] =
Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area);
widget.render(v1, buf, edit_time);
label.centered().render(v2, buf);
} else {
let label = Line::raw(
if state.clock.is_done() {
if state.clock.with_decis {
format!(
"Countdown {} +{}",
state.clock.get_mode(),
state
.elapsed_clock
.get_current_value()
.to_string_with_decis()
)
} else {
format!(
"Countdown {} +{}",
state.clock.get_mode(),
state.elapsed_clock.get_current_value()
)
}
} else {
format!("Countdown {}", state.clock.get_mode())
}
.to_uppercase(),
);
let widget = ClockWidget::new(self.style);
let area = center( let area = center(
area, area,
Constraint::Length(max( Constraint::Length(max(
clock.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(clock.get_height() + 1 /* height of label */), Constraint::Length(widget.get_height() + 1 /* height of label */),
); );
let [v1, v2] = let [v1, v2] =
Layout::vertical(Constraint::from_lengths([clock.get_height(), 1])).areas(area); Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area);
clock.render(v1, buf, &mut state.clock); widget.render(v1, buf, &mut state.clock);
label.centered().render(v2, buf); label.centered().render(v2, buf);
} }
}
} }

231
src/widgets/edit_time.rs Normal file
View File

@@ -0,0 +1,231 @@
use std::fmt;
use time::OffsetDateTime;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
widgets::{StatefulWidget, Widget},
};
use crate::{
common::Style,
widgets::clock_elements::{Colon, Digit, COLON_WIDTH, DIGIT_SPACE_WIDTH, DIGIT_WIDTH},
};
use super::clock_elements::DIGIT_HEIGHT;
#[derive(Debug, Clone)]
pub enum Selected {
Seconds,
Minutes,
Hours,
}
impl Selected {
pub fn next(&self) -> Self {
match self {
Selected::Seconds => Selected::Minutes,
Selected::Minutes => Selected::Hours,
Selected::Hours => Selected::Seconds,
}
}
pub fn prev(&self) -> Self {
match self {
Selected::Seconds => Selected::Hours,
Selected::Minutes => Selected::Seconds,
Selected::Hours => Selected::Minutes,
}
}
}
impl fmt::Display for Selected {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Selected::Seconds => write!(f, "[edit seconds]"),
Selected::Minutes => write!(f, "[edit minutes]"),
Selected::Hours => write!(f, "[edit hours]"),
}
}
}
#[derive(Debug, Clone)]
pub struct EditTimeState {
selected: Selected,
time: OffsetDateTime,
min: OffsetDateTime,
max: OffsetDateTime,
}
#[derive(Debug, Clone)]
pub struct EditTimeStateArgs {
pub time: OffsetDateTime,
pub min: OffsetDateTime,
pub max: OffsetDateTime,
}
impl EditTimeState {
pub fn new(args: EditTimeStateArgs) -> Self {
EditTimeState {
time: args.time,
min: args.min,
max: args.max,
selected: Selected::Minutes,
}
}
pub fn set_time(&mut self, time: OffsetDateTime) {
self.time = time;
}
pub fn set_min_time(&mut self, min: OffsetDateTime) {
self.min = min;
}
pub fn set_max_time(&mut self, min: OffsetDateTime) {
self.max = min;
}
pub fn get_time(&mut self) -> &OffsetDateTime {
&self.time
}
pub fn get_selected(&mut self) -> &Selected {
&self.selected
}
pub fn next(&mut self) {
self.selected = self.selected.next();
}
pub fn prev(&mut self) {
self.selected = self.selected.prev();
}
pub fn up(&mut self) {
self.time = match self.selected {
Selected::Seconds => {
if self
.time
.lt(&self.max.saturating_sub(time::Duration::new(1, 0)))
{
self.time.saturating_add(time::Duration::new(1, 0))
} else {
self.time
}
}
Selected::Minutes => {
if self
.time
.lt(&self.max.saturating_sub(time::Duration::new(60, 0)))
{
self.time.saturating_add(time::Duration::new(60, 0))
} else {
self.time
}
}
Selected::Hours => {
if self
.time
.lt(&self.max.saturating_sub(time::Duration::new(60 * 60, 0)))
{
self.time.saturating_add(time::Duration::new(60 * 60, 0))
} else {
self.time
}
}
}
}
pub fn down(&mut self) {
self.time = match self.selected {
Selected::Seconds => {
if self
.time
.ge(&self.min.saturating_add(time::Duration::new(1, 0)))
{
self.time.saturating_sub(time::Duration::new(1, 0))
} else {
self.time
}
}
Selected::Minutes => {
if self
.time
.ge(&self.min.saturating_add(time::Duration::new(60, 0)))
{
self.time.saturating_sub(time::Duration::new(60, 0))
} else {
self.time
}
}
Selected::Hours => {
if self
.time
.ge(&self.min.saturating_add(time::Duration::new(60 * 60, 0)))
{
self.time.saturating_sub(time::Duration::new(60 * 60, 0))
} else {
self.time
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct EditTimeWidget {
style: Style,
}
impl EditTimeWidget {
pub fn new(style: Style) -> Self {
Self { style }
}
fn get_horizontal_lengths(&self) -> Vec<u16> {
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
]
}
pub fn get_width(&self) -> u16 {
self.get_horizontal_lengths().iter().sum()
}
pub fn get_height(&self) -> u16 {
DIGIT_HEIGHT
}
}
impl StatefulWidget for EditTimeWidget {
type State = EditTimeState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let symbol = self.style.get_digit_symbol();
let edit_hours = matches!(state.selected, Selected::Hours);
let edit_minutes = matches!(state.selected, Selected::Minutes);
let edit_secs = matches!(state.selected, Selected::Seconds);
let [hh, _, h, c_hm, mm, _, m, c_ms, ss, _, s] =
Layout::horizontal(Constraint::from_lengths(self.get_horizontal_lengths())).areas(area);
Digit::new((state.time.hour() as u64) / 10, edit_hours, symbol).render(hh, buf);
Digit::new((state.time.hour() as u64) % 10, edit_hours, symbol).render(h, buf);
Colon::new(symbol).render(c_hm, buf);
Digit::new((state.time.minute() as u64) / 10, edit_minutes, symbol).render(mm, buf);
Digit::new((state.time.minute() as u64) % 10, edit_minutes, symbol).render(m, buf);
Colon::new(symbol).render(c_ms, buf);
Digit::new((state.time.second() as u64) / 10, edit_secs, symbol).render(ss, buf);
Digit::new((state.time.second() as u64) % 10, edit_secs, symbol).render(s, buf);
}
}

View File

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

View File

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

View File

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