Compare commits

...

No commits in common. "8fbb3ca7e03fdf18c81cbfd117492831c4e44959" and "22a0cf6938c866b9c08fe42b1321582d4e85ebe6" have entirely different histories.

30 changed files with 2143 additions and 60 deletions

3
.cargo/config Normal file
View file

@ -0,0 +1,3 @@
[alias]
xtask = "run --package xtask --"
main = "run --package clock-tui --"

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/assets/gen
/.idea

797
Cargo.lock generated Normal file
View 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",
]

View file

@ -1,16 +1,5 @@
[package] [workspace]
name = "crock" members = [
version = "0.1.0" "clock-tui",
edition = "2021" "xtask",
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]

20
LICENSE
View file

@ -1,9 +1,21 @@
MIT License 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
View file

@ -1,3 +1,127 @@
# Crock # clock-tui (tclock)
Clock TUI A clock app in terminal. It support the following modes:
## Clock
![clock](./assets/demo-clock-mode.gif)
## Timer
![timer](./assets/demo-timer-mode.gif)
## Stopwatch
![stopwatch](./assets/demo-stopwatch-mode.gif)
## Countdown
![countdown](./assets/demo-countdown-mode.gif)
# 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

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
View file

@ -0,0 +1 @@
/target

29
clock-tui/Cargo.toml Normal file
View 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
View file

@ -0,0 +1 @@
../README.md

345
clock-tui/src/app.rs Normal file
View 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
View 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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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());
}
}
}

View 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
View 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(())
}

View 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;
}
}
}
}

View 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);
}

View 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![]]),
_ => {}
}
}
}

View 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
View file

@ -0,0 +1,4 @@
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
pub mod app;
pub mod clock_text;

View file

@ -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

View file

@ -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"

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

12
xtask/Cargo.toml Normal file
View 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
View 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(())
}