generated from PlexSheep/rs-base
Compare commits
No commits in common. "8fbb3ca7e03fdf18c81cbfd117492831c4e44959" and "22a0cf6938c866b9c08fe42b1321582d4e85ebe6" have entirely different histories.
8fbb3ca7e0
...
22a0cf6938
30 changed files with 2143 additions and 60 deletions
3
.cargo/config
Normal file
3
.cargo/config
Normal file
|
@ -0,0 +1,3 @@
|
|||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
main = "run --package clock-tui --"
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/assets/gen
|
||||
/.idea
|
797
Cargo.lock
generated
Normal file
797
Cargo.lock
generated
Normal file
|
@ -0,0 +1,797 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "3.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4179da71abd56c26b54dd0c248cc081c1f43b0a1a7e8448e28e57a29baa993d"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_mangen"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "105180c05a72388d5f5e4e4f6c79eecb92497bda749fa8f963a16647c5d5377f"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"roff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clock-tui"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"crossterm 0.24.0",
|
||||
"regex",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab9f7409c70a38a56216480fba371ee460207dd8926ccf5b4160591759559170"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"crossterm 0.23.2",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"clock-tui",
|
||||
]
|
21
Cargo.toml
21
Cargo.toml
|
@ -1,16 +1,5 @@
|
|||
[package]
|
||||
name = "crock"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
||||
license = "MIT"
|
||||
description = "clock tui"
|
||||
readme = "README.md"
|
||||
homepage = "https://git.cscherr.de/PlexSheep/crock"
|
||||
repository = "https://git.cscherr.de/PlexSheep/crock"
|
||||
keywords = ["time", "clock", "tui"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"clock-tui",
|
||||
"xtask",
|
||||
]
|
||||
|
|
20
LICENSE
20
LICENSE
|
@ -1,9 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 PlexSheep
|
||||
Copyright (c) 2022 Race604
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
128
README.md
128
README.md
|
@ -1,3 +1,127 @@
|
|||
# Crock
|
||||
# clock-tui (tclock)
|
||||
|
||||
Clock TUI
|
||||
A clock app in terminal. It support the following modes:
|
||||
|
||||
## Clock
|
||||
|
||||

|
||||
|
||||
## Timer
|
||||
|
||||

|
||||
|
||||
## Stopwatch
|
||||
|
||||

|
||||
|
||||
## Countdown
|
||||
|
||||

|
||||
|
||||
# Usage
|
||||
|
||||
## Install
|
||||
|
||||
Install executable by `cargo`:
|
||||
|
||||
```shell
|
||||
$ cargo install clock-tui
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
```shell
|
||||
$ tclock
|
||||
```
|
||||
Run this command to start a clock, and press `q` to exit.
|
||||
|
||||
You can always use `-h` or `--help` to show help message, for exmaple
|
||||
|
||||
```shell
|
||||
$ tclock --help
|
||||
|
||||
# or
|
||||
$ tclock clock -h
|
||||
```
|
||||
|
||||
## Clock mode, this it the default mode
|
||||
|
||||
```shell
|
||||
$ tclock clock
|
||||
|
||||
# Or just run
|
||||
$ tclock
|
||||
```
|
||||
|
||||
For more details, run `tclock clock -h` to show usage.
|
||||
|
||||
## Run timer
|
||||
|
||||
```shell
|
||||
# Start timer for 5 minutes
|
||||
$ tclock timer -d 5m
|
||||
```
|
||||
|
||||
The option `-d` or `--duration` to set time, for example `100s`, `5m`, `1h`, etc.
|
||||
|
||||
You can press `Space` key to _pause_ and _resume_ the timer.
|
||||
|
||||
The timer mode also accept additional command to run when the timer ends, for example:
|
||||
|
||||
```
|
||||
tclock timer -d 25m -e terminal-notifier -title tclock -message "'Time is up!'"
|
||||
```
|
||||
|
||||
Here we use [terminal-notifier](https://github.com/julienXX/terminal-notifier) to fire a notification when time is up.
|
||||
|
||||
For more details, run `tclock timer -h` to show usage.
|
||||
|
||||
## Run stopwatch
|
||||
|
||||
```shell
|
||||
$ tclock stopwatch
|
||||
```
|
||||
|
||||
For more details, run `tclock stopwatch -h` to show usage.
|
||||
|
||||
## Run countdown
|
||||
|
||||
```shell
|
||||
$ tclock countdown --time 2023-01-01 --title 'New Year 2023'`
|
||||
```
|
||||
|
||||
You can use `-t` or `--time` to specify time, for example: `2023-01-01`, `20:00`, `'2022-12-25 20:00:00'` or `2022-12-25T20:00:00-04:00`.
|
||||
|
||||
You can use `-r` or `--reverse` to run in count-up mode, it counts up duration since the specific time.
|
||||
|
||||
For more details, run `tclock countdown -h` to show usage.
|
||||
|
||||
## Customize style
|
||||
|
||||
You can customize the styles.
|
||||
|
||||
### Size
|
||||
|
||||
You can use `-s` or `--size` option to custome clock size, for example:
|
||||
|
||||
```shell
|
||||
$ tclock -s 2
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
You can use `-c` or `--color` to set clock forground color, for exmaple:
|
||||
|
||||
```shell
|
||||
# color name, any one of:
|
||||
# Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed,
|
||||
# LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, White
|
||||
$ tclock -c yellow
|
||||
|
||||
# or hex color
|
||||
$ tclock -c '#e63946'
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
MIT License, refer to [LICENSE](./LICENSE) for detail.
|
||||
|
|
BIN
assets/demo-clock-mode.gif
Normal file
BIN
assets/demo-clock-mode.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 224 KiB |
BIN
assets/demo-countdown-mode.gif
Normal file
BIN
assets/demo-countdown-mode.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
BIN
assets/demo-stopwatch-mode.gif
Normal file
BIN
assets/demo-stopwatch-mode.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
BIN
assets/demo-timer-mode.gif
Normal file
BIN
assets/demo-timer-mode.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 117 KiB |
1
clock-tui/.gitignore
vendored
Normal file
1
clock-tui/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
29
clock-tui/Cargo.toml
Normal file
29
clock-tui/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "clock-tui"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "A clock app in terminal"
|
||||
homepage = "https://github.com/race604/clock-tui"
|
||||
repository = "https://github.com/race604/clock-tui"
|
||||
readme = "README.md"
|
||||
authors = ["Race604 <race604@gmail.com>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tui = "0.18.0"
|
||||
crossterm = "0.24"
|
||||
chrono = "0.4"
|
||||
clap = { version = "3.2.12", features = ["derive"] }
|
||||
regex = "1.6.0"
|
||||
chrono-tz = { version = "0.6.3", features = ["serde"] }
|
||||
|
||||
[lib]
|
||||
name = "clock_tui"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tclock"
|
||||
path = "src/bin/main.rs"
|
||||
bench = false
|
1
clock-tui/README.md
Symbolic link
1
clock-tui/README.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../README.md
|
345
clock-tui/src/app.rs
Normal file
345
clock-tui/src/app.rs
Normal file
|
@ -0,0 +1,345 @@
|
|||
use chrono::DateTime;
|
||||
use chrono::Duration;
|
||||
use chrono::Local;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::NaiveTime;
|
||||
use chrono::TimeZone;
|
||||
use chrono_tz::Tz;
|
||||
use clap::Subcommand;
|
||||
use crossterm::event::KeyCode;
|
||||
use regex::Regex;
|
||||
use tui::backend::Backend;
|
||||
use tui::style::Color;
|
||||
use tui::style::Style;
|
||||
use tui::Frame;
|
||||
|
||||
use self::modes::Clock;
|
||||
use self::modes::Countdown;
|
||||
use self::modes::DurationFormat;
|
||||
use self::modes::Stopwatch;
|
||||
use self::modes::Timer;
|
||||
|
||||
pub mod modes;
|
||||
|
||||
/// This application does not only offer a clock, but also a few other modes. This enum allows to
|
||||
/// specify on the command line, which mode should be used and what additional options it should
|
||||
/// receive.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Mode {
|
||||
/// The clock mode displays the current time, the default mode.
|
||||
Clock {
|
||||
/// Custome timezone, for example "America/New_York", use local timezone if not specificed
|
||||
#[clap(short = 'z', long, value_parser=parse_timezone)]
|
||||
timezone: Option<Tz>,
|
||||
/// Do not show date
|
||||
#[clap(short = 'D', long, takes_value = false)]
|
||||
no_date: bool,
|
||||
/// Do not show seconds
|
||||
#[clap(short = 'S', long, takes_value = false)]
|
||||
no_seconds: bool,
|
||||
/// Show milliseconds
|
||||
#[clap(short, long, takes_value = false)]
|
||||
millis: bool,
|
||||
},
|
||||
/// The timer mode displays the remaining time until the timer is finished.
|
||||
Timer {
|
||||
/// Initial duration for timer, value can be 10s for 10 seconds, 1m for 1 minute, etc.
|
||||
/// Also accept mulitple duration value and run the timers sequentially, eg. 25m 5m
|
||||
#[clap(short, long="duration", value_parser = parse_duration, min_values=1, default_value = "5m")]
|
||||
durations: Vec<Duration>,
|
||||
|
||||
/// Set the title for the timer, also accept mulitple titles for each durations correspondingly
|
||||
#[clap(short, long = "title", min_values = 0)]
|
||||
titles: Vec<String>,
|
||||
|
||||
/// Restart the timer when timer is over
|
||||
#[clap(long, short, takes_value = false)]
|
||||
repeat: bool,
|
||||
|
||||
/// Hide milliseconds
|
||||
#[clap(long = "no-millis", short = 'M', takes_value = false)]
|
||||
no_millis: bool,
|
||||
|
||||
/// Start the timer paused
|
||||
#[clap(long = "paused", short = 'P', takes_value = false)]
|
||||
paused: bool,
|
||||
|
||||
/// Auto quit when time is up
|
||||
#[clap(long = "quit", short = 'Q', takes_value = false)]
|
||||
auto_quit: bool,
|
||||
|
||||
/// Command to run when the timer ends
|
||||
#[clap(long, short, multiple = true, allow_hyphen_values = true)]
|
||||
execute: Vec<String>,
|
||||
},
|
||||
/// The stopwatch mode displays the elapsed time since it was started.
|
||||
Stopwatch,
|
||||
/// The countdown timer mode shows the duration to a specific time
|
||||
Countdown {
|
||||
/// The target time to countdown to, eg. "2023-01-01", "20:00", "2022-12-25 20:00:00" or "2022-12-25T20:00:00-04:00"
|
||||
#[clap(long, short, value_parser = parse_datetime)]
|
||||
time: DateTime<Local>,
|
||||
|
||||
/// Title or description for countdown show in header
|
||||
#[clap(long, short = 'T')]
|
||||
title: Option<String>,
|
||||
|
||||
/// Continue to countdown after pass the target time
|
||||
#[clap(long = "continue", short = 'c', takes_value = false)]
|
||||
continue_on_zero: bool,
|
||||
|
||||
/// Reverse the countdown, a.k.a. countup
|
||||
#[clap(long, short, takes_value = false)]
|
||||
reverse: bool,
|
||||
|
||||
/// Show milliseconds
|
||||
#[clap(short, long, takes_value = false)]
|
||||
millis: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[clap(name = "tclock", about = "A clock app in terminal", long_about = None)]
|
||||
/// Represents the TUI Application that can be
|
||||
pub struct App {
|
||||
#[clap(subcommand)]
|
||||
pub mode: Option<Mode>,
|
||||
/// Foreground color of the clock, possible values are:
|
||||
/// a) Any one of: Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, White.
|
||||
/// b) Hexadecimal color code: #RRGGBB.
|
||||
#[clap(short, long, value_parser = parse_color, default_value = "green")]
|
||||
pub color: Color,
|
||||
/// Size of the clock, should be a positive integer (>=1).
|
||||
#[clap(short, long, value_parser, default_value = "1")]
|
||||
pub size: u16,
|
||||
|
||||
#[clap(skip)]
|
||||
clock: Option<Clock>,
|
||||
#[clap(skip)]
|
||||
timer: Option<Timer>,
|
||||
#[clap(skip)]
|
||||
stopwatch: Option<Stopwatch>,
|
||||
#[clap(skip)]
|
||||
countdown: Option<Countdown>,
|
||||
}
|
||||
|
||||
/// Trait for widgets that can be paused
|
||||
pub(crate) trait Pause {
|
||||
fn is_paused(&self) -> bool;
|
||||
|
||||
fn pause(&mut self);
|
||||
|
||||
fn resume(&mut self);
|
||||
|
||||
fn toggle_paused(&mut self) {
|
||||
if self.is_paused() {
|
||||
self.resume()
|
||||
} else {
|
||||
self.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn init_app(&mut self) {
|
||||
let style = Style::default().fg(self.color);
|
||||
let mode = self.mode.as_ref().unwrap_or(&Mode::Clock {
|
||||
no_date: false,
|
||||
millis: false,
|
||||
no_seconds: false,
|
||||
timezone: None,
|
||||
});
|
||||
match mode {
|
||||
Mode::Clock {
|
||||
no_date,
|
||||
no_seconds,
|
||||
millis,
|
||||
timezone,
|
||||
} => {
|
||||
self.clock = Some(Clock {
|
||||
size: self.size,
|
||||
style,
|
||||
show_date: !no_date,
|
||||
show_millis: *millis,
|
||||
show_secs: !no_seconds,
|
||||
timezone: *timezone,
|
||||
});
|
||||
}
|
||||
Mode::Timer {
|
||||
durations,
|
||||
titles,
|
||||
repeat,
|
||||
no_millis,
|
||||
paused,
|
||||
auto_quit,
|
||||
execute,
|
||||
} => {
|
||||
let format = if *no_millis {
|
||||
DurationFormat::HourMinSec
|
||||
} else {
|
||||
DurationFormat::HourMinSecDeci
|
||||
};
|
||||
self.timer = Some(Timer::new(
|
||||
self.size,
|
||||
style,
|
||||
durations.to_owned(),
|
||||
titles.to_owned(),
|
||||
*repeat,
|
||||
format,
|
||||
*paused,
|
||||
*auto_quit,
|
||||
execute.to_owned(),
|
||||
));
|
||||
}
|
||||
Mode::Stopwatch => {
|
||||
self.stopwatch = Some(Stopwatch::new(self.size, style));
|
||||
}
|
||||
Mode::Countdown {
|
||||
time,
|
||||
title,
|
||||
continue_on_zero,
|
||||
reverse,
|
||||
millis,
|
||||
} => {
|
||||
self.countdown = Some(Countdown {
|
||||
size: self.size,
|
||||
style,
|
||||
time: *time,
|
||||
title: title.to_owned(),
|
||||
continue_on_zero: *continue_on_zero,
|
||||
reverse: *reverse,
|
||||
format: if *millis {
|
||||
DurationFormat::HourMinSecDeci
|
||||
} else {
|
||||
DurationFormat::HourMinSec
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui<B: Backend>(&self, f: &mut Frame<B>) {
|
||||
if let Some(ref w) = self.clock {
|
||||
f.render_widget(w, f.size());
|
||||
} else if let Some(ref w) = self.timer {
|
||||
f.render_widget(w, f.size());
|
||||
} else if let Some(ref w) = self.stopwatch {
|
||||
f.render_widget(w, f.size());
|
||||
} else if let Some(ref w) = self.countdown {
|
||||
f.render_widget(w, f.size());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_key(&mut self, key: KeyCode) {
|
||||
if let Some(_w) = self.clock.as_mut() {
|
||||
} else if let Some(w) = self.timer.as_mut() {
|
||||
handle_key(w, key);
|
||||
} else if let Some(w) = self.stopwatch.as_mut() {
|
||||
handle_key(w, key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ended(&self) -> bool {
|
||||
if let Some(ref w) = self.timer {
|
||||
return w.is_finished();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key<T: Pause>(widget: &mut T, key: KeyCode) {
|
||||
if let KeyCode::Char(' ') = key {
|
||||
widget.toggle_paused()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> Result<Duration, String> {
|
||||
let reg = Regex::new(r"^(\d+)([smhdSMHD])$").unwrap();
|
||||
let cap = reg
|
||||
.captures(s)
|
||||
.ok_or_else(|| format!("{} is not a valid duration", s))?;
|
||||
|
||||
let num = cap.get(1).unwrap().as_str().parse::<i64>().unwrap();
|
||||
let unit = cap.get(2).unwrap().as_str().to_lowercase();
|
||||
|
||||
match unit.as_str() {
|
||||
"s" => Ok(Duration::seconds(num)),
|
||||
"m" => Ok(Duration::minutes(num)),
|
||||
"h" => Ok(Duration::hours(num)),
|
||||
"d" => Ok(Duration::days(num)),
|
||||
_ => Err(format!("Invalid duration: {}", s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_color(s: &str) -> Result<Color, String> {
|
||||
let s = s.to_lowercase();
|
||||
let reg = Regex::new(r"^#([0-9a-f]{6})$").unwrap();
|
||||
match s.as_str() {
|
||||
"black" => Ok(Color::Black),
|
||||
"red" => Ok(Color::Red),
|
||||
"green" => Ok(Color::Green),
|
||||
"yellow" => Ok(Color::Yellow),
|
||||
"blue" => Ok(Color::Blue),
|
||||
"magenta" => Ok(Color::Magenta),
|
||||
"cyan" => Ok(Color::Cyan),
|
||||
"gray" => Ok(Color::Gray),
|
||||
"darkgray" => Ok(Color::DarkGray),
|
||||
"lightred" => Ok(Color::LightRed),
|
||||
"lightgreen" => Ok(Color::LightGreen),
|
||||
"lightyellow" => Ok(Color::LightYellow),
|
||||
"lightblue" => Ok(Color::LightBlue),
|
||||
"lightmagenta" => Ok(Color::LightMagenta),
|
||||
"lightcyan" => Ok(Color::LightCyan),
|
||||
"white" => Ok(Color::White),
|
||||
s => {
|
||||
let cap = reg
|
||||
.captures(s)
|
||||
.ok_or_else(|| format!("Invalid color: {}", s))?;
|
||||
let hex = cap.get(1).unwrap().as_str();
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap();
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap();
|
||||
let b = u8::from_str_radix(&hex[4..], 16).unwrap();
|
||||
Ok(Color::Rgb(r, g, b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_datetime(s: &str) -> Result<DateTime<Local>, String> {
|
||||
let s = s.trim();
|
||||
let today = Local::today();
|
||||
|
||||
let time = NaiveTime::parse_from_str(s, "%H:%M");
|
||||
if let Ok(time) = time {
|
||||
let time = NaiveDateTime::new(today.naive_local(), time);
|
||||
return Ok(Local.from_local_datetime(&time).unwrap());
|
||||
}
|
||||
|
||||
let time = NaiveTime::parse_from_str(s, "%H:%M:%S");
|
||||
if let Ok(time) = time {
|
||||
let time = NaiveDateTime::new(today.naive_local(), time);
|
||||
return Ok(Local.from_local_datetime(&time).unwrap());
|
||||
}
|
||||
|
||||
let date = NaiveDate::parse_from_str(s, "%Y-%m-%d");
|
||||
if let Ok(date) = date {
|
||||
let time = NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0));
|
||||
return Ok(Local.from_local_datetime(&time).unwrap());
|
||||
}
|
||||
|
||||
let date_time = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S");
|
||||
if let Ok(date_time) = date_time {
|
||||
return Ok(Local.from_local_datetime(&date_time).unwrap());
|
||||
}
|
||||
|
||||
let rfc_time = DateTime::parse_from_rfc3339(s);
|
||||
if let Ok(rfc_time) = rfc_time {
|
||||
return Ok(rfc_time.with_timezone(&Local));
|
||||
}
|
||||
|
||||
Err("Invalid time format".to_string())
|
||||
}
|
||||
|
||||
fn parse_timezone(s: &str) -> Result<Tz, String> {
|
||||
s.parse()
|
||||
}
|
141
clock-tui/src/app/modes.rs
Normal file
141
clock-tui/src/app/modes.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
mod clock;
|
||||
mod countdown;
|
||||
mod stopwatch;
|
||||
mod timer;
|
||||
|
||||
use std::cmp::min;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::clock_text::BricksText;
|
||||
use chrono::Duration;
|
||||
pub(crate) use clock::Clock;
|
||||
pub(crate) use countdown::Countdown;
|
||||
pub(crate) use stopwatch::Stopwatch;
|
||||
pub(crate) use timer::Timer;
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::Span,
|
||||
widgets::{Paragraph, Widget},
|
||||
};
|
||||
|
||||
/// Describes how a [`Duration`] should be displayed.
|
||||
///
|
||||
/// For now, the only difference is if the deciseconds should be shown or not.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
|
||||
pub enum DurationFormat {
|
||||
/// Hours, minutes, seconds, deciseconds
|
||||
HourMinSecDeci,
|
||||
/// Hours, minutes, seconds
|
||||
HourMinSec,
|
||||
}
|
||||
|
||||
/// Format a Duration, converting it into a String
|
||||
///
|
||||
/// The formatting can be configured with the [`DurationFormat`] passed into the function.
|
||||
///
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chrono::Duration;
|
||||
/// # use clock_tui::app::modes::*;
|
||||
/// # fn main() {
|
||||
/// let time = Duration::seconds(5555);
|
||||
/// let formatted_time = format_duration(time, DurationFormat::HourMinSecDeci);
|
||||
/// assert_eq!("1:32:35.0", &formatted_time);
|
||||
/// let formatted_time = format_duration(time, DurationFormat::HourMinSec);
|
||||
/// assert_eq!("1:32:35", &formatted_time);
|
||||
///
|
||||
/// let time = Duration::days(255);
|
||||
/// let formatted_time = format_duration(time, DurationFormat::HourMinSec);
|
||||
/// assert_eq!("255:00:00:00", &formatted_time);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn format_duration(duration: Duration, format: DurationFormat) -> String {
|
||||
let is_neg = duration < Duration::zero();
|
||||
let duration = if is_neg { -duration } else { duration };
|
||||
|
||||
let millis = duration.num_milliseconds();
|
||||
let seconds = millis / 1000;
|
||||
let minutes = seconds / 60;
|
||||
let hours = minutes / 60;
|
||||
let days = hours / 24;
|
||||
let mut result = String::new();
|
||||
|
||||
fn append_number(s: &mut String, num: i64) {
|
||||
if s.is_empty() {
|
||||
let _ = write!(s, "{}", num);
|
||||
} else {
|
||||
let _ = write!(s, "{:02}", num);
|
||||
}
|
||||
}
|
||||
|
||||
if days > 0 {
|
||||
let _ = write!(result, "{}:", days);
|
||||
}
|
||||
if hours > 0 {
|
||||
append_number(&mut result, hours % 24);
|
||||
result.push(':');
|
||||
}
|
||||
append_number(&mut result, minutes % 60);
|
||||
result.push(':');
|
||||
|
||||
// prepend a - if the duration is negative
|
||||
if is_neg {
|
||||
result.insert(0, '-');
|
||||
}
|
||||
match format {
|
||||
DurationFormat::HourMinSecDeci => {
|
||||
let _ = write!(result, "{:02}.{}", seconds % 60, (millis % 1000) / 100);
|
||||
}
|
||||
DurationFormat::HourMinSec => {
|
||||
let _ = write!(result, "{:02}", seconds % 60);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn render_centered(
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
text: &BricksText,
|
||||
header: Option<String>,
|
||||
footer: Option<String>,
|
||||
) {
|
||||
let text_size = text.size();
|
||||
let text_area = Rect {
|
||||
x: area.x + (area.width.saturating_sub(text_size.0)) / 2,
|
||||
y: area.y + (area.height.saturating_sub(text_size.1)) / 2,
|
||||
width: min(text_size.0, area.width),
|
||||
height: min(text_size.1, area.height),
|
||||
};
|
||||
text.render(text_area, buf);
|
||||
|
||||
let render_text_center = |text: &str, top: u16, buf: &mut Buffer| {
|
||||
let text_len = text.len() as u16;
|
||||
let paragrahp = Paragraph::new(Span::from(text)).style(Style::default());
|
||||
|
||||
let para_area = Rect {
|
||||
x: area.left() + (area.width.saturating_sub(text_len)) / 2,
|
||||
y: top,
|
||||
width: min(text_len, area.width),
|
||||
height: min(1, area.height),
|
||||
};
|
||||
paragrahp.render(para_area, buf);
|
||||
};
|
||||
|
||||
if let Some(text) = header {
|
||||
if area.top() + 2 <= text_area.top() {
|
||||
render_text_center(text.as_str(), text_area.top() - 2, buf);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(text) = footer {
|
||||
if area.bottom() >= text_area.bottom() + 2 {
|
||||
render_text_center(text.as_str(), text_area.bottom() + 1, buf);
|
||||
}
|
||||
}
|
||||
}
|
46
clock-tui/src/app/modes/clock.rs
Normal file
46
clock-tui/src/app/modes/clock.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use crate::clock_text::BricksText;
|
||||
use chrono::{Local, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use tui::{layout::Rect, style::Style, widgets::Widget};
|
||||
|
||||
use super::render_centered;
|
||||
|
||||
pub(crate) struct Clock {
|
||||
pub size: u16,
|
||||
pub style: Style,
|
||||
pub show_date: bool,
|
||||
pub show_millis: bool,
|
||||
pub show_secs: bool,
|
||||
pub timezone: Option<Tz>,
|
||||
}
|
||||
|
||||
impl Widget for &Clock {
|
||||
fn render(self, area: Rect, buf: &mut tui::buffer::Buffer) {
|
||||
let now = if let Some(ref tz) = self.timezone {
|
||||
Utc::now().with_timezone(tz).naive_local()
|
||||
} else {
|
||||
Local::now().naive_local()
|
||||
};
|
||||
let mut time_str = now.format("%H:%M:%S%.3f").to_string();
|
||||
if self.show_millis {
|
||||
time_str.truncate(time_str.len() - 2);
|
||||
} else if self.show_secs {
|
||||
time_str.truncate(time_str.len() - 4);
|
||||
} else {
|
||||
time_str.truncate(time_str.len() - 7);
|
||||
};
|
||||
let time_str = time_str.as_str();
|
||||
let text = BricksText::new(time_str, self.size, self.size, self.style);
|
||||
let header = if self.show_date {
|
||||
let mut title = now.format("%Y-%m-%d").to_string();
|
||||
if let Some(tz) = self.timezone {
|
||||
title.push(' ');
|
||||
title.push_str(tz.name());
|
||||
}
|
||||
Some(title)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
render_centered(area, buf, &text, header, None);
|
||||
}
|
||||
}
|
45
clock-tui/src/app/modes/countdown.rs
Normal file
45
clock-tui/src/app/modes/countdown.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use crate::clock_text::BricksText;
|
||||
use chrono::{DateTime, Duration, Local};
|
||||
use tui::{style::Style, widgets::Widget};
|
||||
|
||||
use super::{format_duration, render_centered, DurationFormat};
|
||||
|
||||
pub struct Countdown {
|
||||
pub size: u16,
|
||||
pub style: Style,
|
||||
pub time: DateTime<Local>,
|
||||
pub title: Option<String>,
|
||||
pub continue_on_zero: bool,
|
||||
pub(crate) reverse: bool,
|
||||
pub(crate) format: DurationFormat,
|
||||
}
|
||||
|
||||
impl Countdown {
|
||||
pub(crate) fn remaining_time(&self) -> Duration {
|
||||
let now = Local::now();
|
||||
let result = self.time.signed_duration_since(now);
|
||||
if self.reverse {
|
||||
-result
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Countdown {
|
||||
fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) {
|
||||
let remaining_time = self.remaining_time();
|
||||
let time_str = if remaining_time < Duration::zero() && !self.continue_on_zero {
|
||||
if (remaining_time.num_milliseconds()).abs() % 1000 < 500 {
|
||||
return;
|
||||
} else {
|
||||
format_duration(Duration::zero(), self.format)
|
||||
}
|
||||
} else {
|
||||
format_duration(remaining_time, self.format)
|
||||
};
|
||||
|
||||
let text = BricksText::new(time_str.as_str(), self.size, self.size, self.style);
|
||||
render_centered(area, buf, &text, self.title.to_owned(), None);
|
||||
}
|
||||
}
|
67
clock-tui/src/app/modes/stopwatch.rs
Normal file
67
clock-tui/src/app/modes/stopwatch.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use crate::clock_text::BricksText;
|
||||
use chrono::{DateTime, Duration, Local};
|
||||
use tui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
|
||||
|
||||
use crate::app::Pause;
|
||||
|
||||
use super::{format_duration, render_centered, DurationFormat};
|
||||
|
||||
pub struct Stopwatch {
|
||||
pub size: u16,
|
||||
pub style: Style,
|
||||
duration: Duration,
|
||||
started_at: Option<DateTime<Local>>,
|
||||
}
|
||||
|
||||
impl Stopwatch {
|
||||
pub(crate) fn new(size: u16, style: Style) -> Self {
|
||||
Self {
|
||||
size,
|
||||
style,
|
||||
duration: Duration::zero(),
|
||||
started_at: Some(Local::now()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn total_time(&self) -> Duration {
|
||||
if let Some(start_at) = self.started_at {
|
||||
let now = Local::now();
|
||||
self.duration + now.signed_duration_since(start_at)
|
||||
} else {
|
||||
self.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Stopwatch {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let time_str = format_duration(self.total_time(), DurationFormat::HourMinSecDeci);
|
||||
let text = BricksText::new(time_str.as_str(), self.size, self.size, self.style);
|
||||
let footer = if self.is_paused() {
|
||||
Some("PAUSED (press <SPACE> to resume)".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
render_centered(area, buf, &text, None, footer);
|
||||
}
|
||||
}
|
||||
|
||||
impl Pause for Stopwatch {
|
||||
fn is_paused(&self) -> bool {
|
||||
self.started_at.is_none()
|
||||
}
|
||||
|
||||
fn pause(&mut self) {
|
||||
if let Some(start_at) = self.started_at {
|
||||
let now = Local::now();
|
||||
self.duration = self.duration + now.signed_duration_since(start_at);
|
||||
self.started_at = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn resume(&mut self) {
|
||||
if self.started_at.is_none() {
|
||||
self.started_at = Some(Local::now());
|
||||
}
|
||||
}
|
||||
}
|
152
clock-tui/src/app/modes/timer.rs
Normal file
152
clock-tui/src/app/modes/timer.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
use std::{cell::RefCell, cmp::min, process::Command};
|
||||
|
||||
use crate::clock_text::BricksText;
|
||||
use chrono::{DateTime, Duration, Local};
|
||||
use tui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
|
||||
|
||||
use crate::app::Pause;
|
||||
|
||||
use super::{format_duration, render_centered, DurationFormat};
|
||||
|
||||
pub struct Timer {
|
||||
pub size: u16,
|
||||
pub style: Style,
|
||||
pub repeat: bool,
|
||||
pub durations: Vec<Duration>,
|
||||
pub titles: Vec<String>,
|
||||
pub execute: Vec<String>,
|
||||
auto_quit: bool,
|
||||
format: DurationFormat,
|
||||
passed: Duration,
|
||||
started_at: Option<DateTime<Local>>,
|
||||
execute_result: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
size: u16,
|
||||
style: Style,
|
||||
durations: Vec<Duration>,
|
||||
titles: Vec<String>,
|
||||
repeat: bool,
|
||||
format: DurationFormat,
|
||||
paused: bool,
|
||||
auto_quit: bool,
|
||||
execute: Vec<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
size,
|
||||
style,
|
||||
durations,
|
||||
titles,
|
||||
repeat,
|
||||
execute,
|
||||
auto_quit,
|
||||
format,
|
||||
passed: Duration::zero(),
|
||||
started_at: (!paused).then(Local::now),
|
||||
execute_result: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remaining_time(&self) -> (Duration, usize) {
|
||||
let total_passed = if let Some(started_at) = self.started_at {
|
||||
self.passed + (Local::now() - started_at)
|
||||
} else {
|
||||
self.passed
|
||||
};
|
||||
|
||||
let mut idx = 0;
|
||||
let mut next_checkpoint = self.durations[idx];
|
||||
while next_checkpoint < total_passed {
|
||||
if idx >= self.durations.len() - 1 && !self.repeat {
|
||||
break;
|
||||
}
|
||||
idx = (idx + 1) % self.durations.len();
|
||||
next_checkpoint = next_checkpoint + self.durations[idx];
|
||||
}
|
||||
|
||||
(next_checkpoint - total_passed, idx)
|
||||
}
|
||||
|
||||
pub(crate) fn is_finished(&self) -> bool {
|
||||
return self.auto_quit && !self.execute_result.borrow().is_none();
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(execute: &[String]) -> String {
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c");
|
||||
let cmd_str = execute.join(" ");
|
||||
cmd.arg(cmd_str);
|
||||
let output = cmd.output();
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
format!("[ERROR] {}", String::from_utf8_lossy(&output.stderr))
|
||||
} else {
|
||||
format!("[SUCCEED] {}", String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
format!("[FAILED] {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Timer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let (remaining_time, idx) = self.remaining_time();
|
||||
let time_str = if remaining_time < Duration::zero() {
|
||||
if self.execute_result.borrow().is_none() {
|
||||
if !self.execute.is_empty() {
|
||||
let result = execute(&self.execute);
|
||||
*self.execute_result.borrow_mut() = Some(result);
|
||||
} else {
|
||||
*self.execute_result.borrow_mut() = Some("".to_owned())
|
||||
}
|
||||
}
|
||||
if remaining_time.num_milliseconds().abs() % 1000 < 500 {
|
||||
return;
|
||||
} else {
|
||||
format_duration(Duration::zero(), self.format)
|
||||
}
|
||||
} else {
|
||||
format_duration(remaining_time, self.format)
|
||||
};
|
||||
|
||||
let header = if self.titles.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.titles[min(idx, self.titles.len() - 1)].clone())
|
||||
};
|
||||
|
||||
let text = BricksText::new(time_str.as_str(), self.size, self.size, self.style);
|
||||
let footer = if self.is_paused() {
|
||||
Some("PAUSED (press <SPACE> to resume)".to_string())
|
||||
} else {
|
||||
self.execute_result.borrow().clone()
|
||||
};
|
||||
render_centered(area, buf, &text, header, footer);
|
||||
}
|
||||
}
|
||||
|
||||
impl Pause for Timer {
|
||||
fn is_paused(&self) -> bool {
|
||||
self.started_at.is_none()
|
||||
}
|
||||
|
||||
fn pause(&mut self) {
|
||||
if let Some(started_at) = self.started_at {
|
||||
self.passed = self.passed + (Local::now() - started_at);
|
||||
self.started_at = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn resume(&mut self) {
|
||||
if self.started_at.is_none() {
|
||||
self.started_at = Some(Local::now());
|
||||
}
|
||||
}
|
||||
}
|
107
clock-tui/src/bin/main.rs
Normal file
107
clock-tui/src/bin/main.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use clock_tui::app::Mode;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use clock_tui::app::App;
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
app: &mut App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
|
||||
loop {
|
||||
if app.is_ended() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// draw the TUI using the `tui` library
|
||||
terminal.draw(|f| app.ui(f))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed()) // substraction, is None if would underflow (be negative)
|
||||
.unwrap_or(Duration::ZERO); // if it was `None`, it was longer than `tick_rate` since the
|
||||
// `last_tick`. We set the timeout to `DURATION::ZERO` so
|
||||
// that `event::poll` will return instantly with success
|
||||
|
||||
// wait up to 100 ms for the timeout Duration. This ensures that we do not loop very fast,
|
||||
// which would cause high cpu load. If the Duration is less than 100 ms, we enter the if
|
||||
// clause. An event describes some kind of user interaction: Mouse actions, Key Presses, or
|
||||
// Resizing of the Terminal
|
||||
if event::poll(timeout)? {
|
||||
// we only care about pressed keys
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
// exit the application if the user presses the 'q' key
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
// otherwise, let the app handle it the key that was pressed
|
||||
key => app.on_key(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// has it been longer since the last tick than the duration of the `tick_rate`?
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
// if so, we want to substract with NOW when we calculate the new timeout.
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// entry point into the program
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Parse the cli arguments
|
||||
let mut app = App::parse();
|
||||
app.init_app();
|
||||
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen /*EnableMouseCapture*/,)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let low_rate = match app.mode {
|
||||
Some(Mode::Clock {
|
||||
millis, no_seconds, ..
|
||||
}) => !millis || no_seconds,
|
||||
Some(Mode::Timer { no_millis, .. }) => no_millis,
|
||||
Some(Mode::Countdown { millis, .. }) => !millis,
|
||||
_ => false,
|
||||
};
|
||||
let tick_rate = Duration::from_millis(if low_rate { 200 } else { 20 });
|
||||
let res = run_app(&mut terminal, &mut app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
// DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
eprintln!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
48
clock-tui/src/clock_text.rs
Normal file
48
clock-tui/src/clock_text.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use tui::{style::Style, widgets::Widget};
|
||||
|
||||
use self::font::bricks::Bricks;
|
||||
use self::font::Font;
|
||||
use self::point::Point;
|
||||
|
||||
mod font;
|
||||
mod point;
|
||||
|
||||
pub struct BricksText {
|
||||
text: String,
|
||||
space: u16,
|
||||
style: Style,
|
||||
font: Bricks,
|
||||
}
|
||||
|
||||
impl BricksText {
|
||||
pub fn new(text: &str, size: u16, space: u16, style: Style) -> BricksText {
|
||||
BricksText {
|
||||
text: text.to_string(),
|
||||
space,
|
||||
style,
|
||||
font: Bricks { size },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> (u16, u16) {
|
||||
let Point(w, h) = self.font.size();
|
||||
let n_chars = self.text.chars().count() as u16;
|
||||
(w * n_chars + self.space * (n_chars - 1), h)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &BricksText {
|
||||
fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) {
|
||||
let mut area = area;
|
||||
for char in self.text.chars() {
|
||||
let Point(w, _) = self.font.size();
|
||||
self.font.render(char, self.style, area, buf);
|
||||
let l = w + self.space;
|
||||
area.x += l;
|
||||
area.width = area.width.saturating_sub(l);
|
||||
if area.area() == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
clock-tui/src/clock_text/font.rs
Normal file
10
clock-tui/src/clock_text/font.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use tui::{buffer::Buffer, layout::Rect, style::Style};
|
||||
|
||||
use super::point::Point;
|
||||
|
||||
pub mod bricks;
|
||||
|
||||
pub(crate) trait Font {
|
||||
fn size(&self) -> Point;
|
||||
fn render(&self, char: char, style: Style, area: Rect, buf: &mut Buffer);
|
||||
}
|
121
clock-tui/src/clock_text/font/bricks.rs
Normal file
121
clock-tui/src/clock_text/font/bricks.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use std::cmp::min;
|
||||
|
||||
use tui::{buffer::Buffer, layout::Rect, style::Style};
|
||||
|
||||
use super::Font;
|
||||
use crate::clock_text::point::Point;
|
||||
|
||||
pub struct Bricks {
|
||||
pub size: u16,
|
||||
}
|
||||
|
||||
impl Bricks {
|
||||
const UNIT_SIZE: Point = Point(6, 5);
|
||||
|
||||
/// each row is represented with a vector of numbers:
|
||||
/// the odd indexed items represent the lenght of "off",
|
||||
/// the even indexed items represent the lenght of "on".
|
||||
/// For exmaple:
|
||||
/// vec![0, 6] is "██████"
|
||||
/// vec![2, 2] is " ██"
|
||||
/// vec![0, 2, 2, 2] is "██ ██"
|
||||
fn draw_row(
|
||||
start: Point,
|
||||
row: Vec<u16>,
|
||||
size: u16,
|
||||
style: Style,
|
||||
area: &Rect,
|
||||
buf: &mut Buffer,
|
||||
) {
|
||||
let mut p = start;
|
||||
let mut on = false;
|
||||
for len in row {
|
||||
let len = len * size;
|
||||
if p.0 > area.right() {
|
||||
break;
|
||||
}
|
||||
|
||||
if on {
|
||||
let s = min(len, area.right() - p.0 + 1);
|
||||
let line = "█".repeat(s as usize);
|
||||
for r in 0..size {
|
||||
if p.1 > area.bottom() {
|
||||
break;
|
||||
}
|
||||
buf.set_string(p.0, p.1 + r, line.as_str(), style);
|
||||
}
|
||||
}
|
||||
|
||||
p.0 += len;
|
||||
on = !on;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_matrix(mat: [Vec<u16>; 5], size: u16, style: Style, area: &Rect, buf: &mut Buffer) {
|
||||
let mut start = Point(area.x, area.y);
|
||||
for row in mat {
|
||||
Self::draw_row(start, row, size, style, area, buf);
|
||||
start.1 += size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Font for Bricks {
|
||||
fn size(&self) -> Point {
|
||||
Self::UNIT_SIZE * self.size
|
||||
}
|
||||
|
||||
fn render(&self, char: char, style: Style, area: Rect, buf: &mut Buffer) {
|
||||
let size = self.size;
|
||||
let mut render_matrix = |mat: [Vec<u16>; 5]| {
|
||||
Bricks::draw_matrix(mat, size, style, &area, buf);
|
||||
};
|
||||
|
||||
match char {
|
||||
'0' => render_matrix([
|
||||
vec![0, 6],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 6],
|
||||
]),
|
||||
'1' => render_matrix([vec![0, 4], vec![2, 2], vec![2, 2], vec![2, 2], vec![0, 6]]),
|
||||
'2' => render_matrix([vec![0, 6], vec![4, 2], vec![0, 6], vec![0, 2], vec![0, 6]]),
|
||||
'3' => render_matrix([vec![0, 6], vec![4, 2], vec![0, 6], vec![4, 2], vec![0, 6]]),
|
||||
'4' => render_matrix([
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 6],
|
||||
vec![4, 2],
|
||||
vec![4, 2],
|
||||
]),
|
||||
'5' => render_matrix([vec![0, 6], vec![0, 2], vec![0, 6], vec![4, 2], vec![0, 6]]),
|
||||
'6' => render_matrix([
|
||||
vec![0, 6],
|
||||
vec![0, 2],
|
||||
vec![0, 6],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 6],
|
||||
]),
|
||||
'7' => render_matrix([vec![0, 6], vec![4, 2], vec![4, 2], vec![4, 2], vec![4, 2]]),
|
||||
'8' => render_matrix([
|
||||
vec![0, 6],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 6],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 6],
|
||||
]),
|
||||
'9' => render_matrix([
|
||||
vec![0, 6],
|
||||
vec![0, 2, 2, 2],
|
||||
vec![0, 6],
|
||||
vec![4, 2],
|
||||
vec![0, 6],
|
||||
]),
|
||||
':' => render_matrix([vec![], vec![2, 2], vec![], vec![2, 2], vec![]]),
|
||||
'.' => render_matrix([vec![], vec![], vec![], vec![], vec![2, 2]]),
|
||||
'-' => render_matrix([vec![], vec![], vec![0, 6], vec![], vec![]]),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
20
clock-tui/src/clock_text/point.rs
Normal file
20
clock-tui/src/clock_text/point.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use std::ops;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Point(pub u16, pub u16);
|
||||
|
||||
impl ops::Add<&Point> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn add(self, other: &Point) -> Point {
|
||||
Point(self.0 + other.0, self.1 + other.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<u16> for Point {
|
||||
type Output = Point;
|
||||
|
||||
fn mul(self, other: u16) -> Point {
|
||||
Point(self.0 * other, self.1 * other)
|
||||
}
|
||||
}
|
4
clock-tui/src/lib.rs
Normal file
4
clock-tui/src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#![warn(missing_docs)]
|
||||
#![warn(missing_debug_implementations)]
|
||||
pub mod app;
|
||||
pub mod clock_text;
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
cargo check --all-features
|
||||
echo ">>>>>>>> PUBLISHING RELEASE FOR REPO"
|
||||
bash scripts/release.sh
|
||||
echo ">>>>>>>> PUBLISHING TO CRATES.IO NEXT"
|
||||
sleep 2
|
||||
cargo publish
|
||||
echo ">>>>>>>> PUBLISHING TO CSCHERR.DE NEXT"
|
||||
sleep 2
|
||||
cargo publish --registry cscherr
|
|
@ -1,24 +0,0 @@
|
|||
#!/bin/bash
|
||||
TOKEN=$(cat ~/.git-credentials | grep 'git.cscherr.de' | grep -P '(?:)[^:]*(?=@)' -o)
|
||||
NEW_VERSION=$(cat Cargo.toml | rg '^\s*version\s*=\s*"([^"]*)"\s*$' -or '$1')
|
||||
GIT_COMMIT_SHA=$(git rev-parse HEAD)
|
||||
REPO=${PWD##*/} # name of cwd
|
||||
BODY="
|
||||
$(git log $(git describe --tags --abbrev=0)..HEAD --pretty="- %s" --oneline --decorate)
|
||||
"
|
||||
USER=PlexSheep
|
||||
git tag "v$NEW_VERSION" || echo "could not tag"
|
||||
curl -X 'POST' \
|
||||
'https://git.cscherr.de/api/v1/repos/PlexSheep/'$REPO'/releases' \
|
||||
-H 'accept: application/json' \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"body": "'"$BODY"'",
|
||||
"draft": false,
|
||||
"name": "v'$NEW_VERSION'",
|
||||
"prerelease": true,
|
||||
"tag_name": "v'$NEW_VERSION'",
|
||||
"target_commitish": "'$GIT_COMMIT_SHA'"
|
||||
}' | python -m json.tool
|
||||
git push || echo "could not push"
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
12
xtask/Cargo.toml
Normal file
12
xtask/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "3.2.12", features = ["derive"] }
|
||||
clap_mangen = "0.1"
|
||||
clap_complete = "3.2.4"
|
||||
clock-tui = { path = "../clock-tui" }
|
44
xtask/src/main.rs
Normal file
44
xtask/src/main.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use clap::{ArgEnum, IntoApp};
|
||||
use clap_complete::{generate_to, Shell};
|
||||
use clap_mangen::Man;
|
||||
use clock_tui::app::App;
|
||||
use std::fs::File;
|
||||
use std::io::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs};
|
||||
|
||||
const BIN_NAME: &str = "tclock";
|
||||
|
||||
fn build_shell_completion(outdir: &Path) -> Result<()> {
|
||||
let mut app = App::into_app();
|
||||
let shells = Shell::value_variants();
|
||||
|
||||
for shell in shells {
|
||||
generate_to(*shell, &mut app, BIN_NAME, &outdir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_manpages(outdir: &Path) -> Result<()> {
|
||||
let app = App::into_app();
|
||||
|
||||
let file = Path::new(&outdir).join(format!("{}.1", BIN_NAME));
|
||||
let mut file = File::create(&file)?;
|
||||
|
||||
Man::new(app).render(&mut file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let out_dir = env!("CARGO_MANIFEST_DIR");
|
||||
|
||||
let out_path = PathBuf::from(out_dir).join("../assets/gen");
|
||||
fs::create_dir_all(&out_path).unwrap();
|
||||
|
||||
build_shell_completion(&out_path)?;
|
||||
build_manpages(&out_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
Reference in a new issue