From a08b687a878fea3607dac83fcd1bec674065920c Mon Sep 17 00:00:00 2001 From: race604 Date: Tue, 19 Jul 2022 13:25:30 +0800 Subject: [PATCH] initial implement clock-tui --- .gitignore | 1 + Cargo.lock | 633 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 + src/app.rs | 148 +++++++++ src/app/modes.rs | 42 +++ src/app/modes/clock.rs | 50 +++ src/app/modes/stopwatch.rs | 58 ++++ src/app/modes/timer.rs | 66 ++++ src/bricks_text.rs | 46 +++ src/bricks_text/chars.rs | 265 ++++++++++++++++ src/lib.rs | 1 + src/main.rs | 79 +++++ 12 files changed, 1403 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/app.rs create mode 100644 src/app/modes.rs create mode 100644 src/app/modes/clock.rs create mode 100644 src/app/modes/stopwatch.rs create mode 100644 src/app/modes/timer.rs create mode 100644 src/bricks_text.rs create mode 100644 src/bricks_text/chars.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e1ab5c1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,633 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[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 = "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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "chrono-tz" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1878c18b5b01b9978d5f130fe366d434022004d12fb87c182e8459b427c4a3" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + +[[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_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 = "clock-tui" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "clap", + "crossterm 0.24.0", + "regex 1.6.0", + "tui", +] + +[[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 = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ee19a3656dadae35a33467f9714f1228dd34766dbe49e10e656b5296867aea" +dependencies = [ + "regex 0.2.11", +] + +[[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 = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local", + "utf8-ranges", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick 0.7.18", + "memchr", + "regex-syntax 0.6.27", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[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 = "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 = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static", +] + +[[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 = "ucd-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bfcbf611b122f2c10eb1bb6172fbc4c2e25df9970330e4d75ce2b5201c9bfc" + +[[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 = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[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 = "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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..feff33f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "clock-tui" +version = "0.1.0" +edition = "2021" + +# 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" +chrono-tz = "0.4" +clap = { version = "3.2.12", features = ["derive"] } +regex = "1.6.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..07311e5 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,148 @@ +use chrono::Duration; +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::Stopwatch; +use self::modes::Timer; + +pub(crate) mod modes; + +#[derive(clap::ArgEnum, Clone)] +pub(crate) enum Mode { + Clock, + Timer, + Stopwatch, +} + +#[derive(clap::Parser)] +pub(crate) struct App { + #[clap(short, long, value_parser, arg_enum, default_value = "clock")] + pub mode: Mode, + #[clap(short, long, value_parser = parse_color, default_value = "green")] + pub color: Color, + #[clap(short, long, value_parser = parse_duration, default_value = "5m")] + pub duration: Duration, + #[clap(short, long, value_parser, default_value = "1")] + pub size: u16, + + #[clap(skip)] + clock: Option, + #[clap(skip)] + timer: Option, + #[clap(skip)] + stopwatch: Option, +} + +impl App { + pub fn init_app(&mut self) { + let style = Style::default().fg(self.color); + match self.mode { + Mode::Clock => { + self.clock = Some(Clock { + size: self.size, + style, + long: false, + }); + } + Mode::Timer => { + self.timer = Some(Timer::new(self.duration, self.size, style)); + } + Mode::Stopwatch => { + self.stopwatch = Some(Stopwatch::new(self.size, style)); + } + } + } + + pub fn ui(&self, f: &mut Frame) { + if let Some(w) = self.clock.as_ref() { + f.render_widget(w, f.size()); + } else if let Some(w) = self.timer.as_ref() { + f.render_widget(w, f.size()); + } else if let Some(w) = self.stopwatch.as_ref() { + 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() { + match key { + KeyCode::Char(' ') => { + if w.is_paused() { + w.resume(); + } else { + w.pause(); + } + } + _ => {} + } + } else if let Some(w) = self.stopwatch.as_mut() { + match key { + KeyCode::Char(' ') => { + if w.is_paused() { + w.resume(); + } else { + w.pause(); + } + } + _ => {} + } + } + } +} + +fn parse_duration(s: &str) -> Result { + 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::().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).into()), + } +} + +fn parse_color(s: &str) -> Result { + 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 = hex[1..3].parse::().unwrap(); + let g = hex[3..5].parse::().unwrap(); + let b = hex[5..7].parse::().unwrap(); + Ok(Color::Rgb(r, g, b)) + } + } +} diff --git a/src/app/modes.rs b/src/app/modes.rs new file mode 100644 index 0000000..9762976 --- /dev/null +++ b/src/app/modes.rs @@ -0,0 +1,42 @@ +mod clock; +mod stopwatch; +mod timer; + +use std::cmp::min; + +use chrono::Duration; +pub(crate) use clock::Clock; +use clock_tui::bricks_text::BricksText; +pub(crate) use stopwatch::Stopwatch; +pub(crate) use timer::Timer; +use tui::{buffer::Buffer, layout::Rect, widgets::Widget}; + +fn format_duration(duration: Duration) -> String { + 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(); + if days > 0 { + result.push_str(&format!("{}:", days)); + } + if hours > 0 { + result.push_str(&format!("{}:", hours % 24)); + } + result.push_str(&format!("{}:", minutes % 60)); + result.push_str(&format!("{:02}.{}", seconds % 60, (millis % 1000) / 100)); + + result +} + +fn render_centered(area: Rect, buf: &mut Buffer, text: &BricksText) { + 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.0, area.height), + }; + text.render(text_area, buf); +} diff --git a/src/app/modes/clock.rs b/src/app/modes/clock.rs new file mode 100644 index 0000000..299c0e0 --- /dev/null +++ b/src/app/modes/clock.rs @@ -0,0 +1,50 @@ +use std::cmp::min; + +use chrono::Local; +use clock_tui::bricks_text::BricksText; +use tui::{ + layout::Rect, + style::Style, + text::Span, + widgets::{Paragraph, Widget}, +}; + +pub(crate) struct Clock { + pub size: u16, + pub style: Style, + pub long: bool, +} + +impl Widget for &Clock { + fn render(self, area: Rect, buf: &mut tui::buffer::Buffer) { + let now = Local::now(); + let time_str = if self.long { + let mut str = now.format("%H:%M:%S%.3f").to_string(); + str.truncate(str.len() - 2); + str + } else { + now.format("%H:%M:%S").to_string() + }; + let time_str = time_str.as_str(); + let text = BricksText::new(time_str, self.size, self.size, self.style); + 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.0, area.height), + }; + text.render(text_area, buf); + let text = now.format("%Y-%m-%d %Z").to_string(); + let text_len = text.as_str().len() as u16; + let paragrahp = Paragraph::new(Span::from(text)).style(Style::default()); + + let para_area = Rect { + x: area.x + (area.width.saturating_sub(text_len)) / 2, + y: text_area.y.saturating_sub(2), + width: min(text_len, area.width), + height: min(1, area.height), + }; + paragrahp.render(para_area, buf); + } +} diff --git a/src/app/modes/stopwatch.rs b/src/app/modes/stopwatch.rs new file mode 100644 index 0000000..b9d1583 --- /dev/null +++ b/src/app/modes/stopwatch.rs @@ -0,0 +1,58 @@ +use chrono::{DateTime, Duration, Local}; +use clock_tui::bricks_text::BricksText; +use tui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget}; + +use super::{format_duration, render_centered}; + +pub struct Stopwatch { + pub size: u16, + pub style: Style, + duration: Duration, + started_at: Option>, +} + +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 is_paused(&self) -> bool { + self.started_at.is_none() + } + + pub(crate) 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; + } + } + + pub(crate) fn resume(&mut self) { + if self.started_at.is_none() { + self.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()); + let text = BricksText::new(time_str.as_str(), self.size, self.size, self.style); + render_centered(area, buf, &text); + } +} diff --git a/src/app/modes/timer.rs b/src/app/modes/timer.rs new file mode 100644 index 0000000..673ae12 --- /dev/null +++ b/src/app/modes/timer.rs @@ -0,0 +1,66 @@ +use chrono::{DateTime, Duration, Local}; +use clock_tui::bricks_text::BricksText; +use tui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget}; + +use super::{format_duration, render_centered}; + +pub struct Timer { + pub size: u16, + pub style: Style, + duration: Duration, + ended_at: Option>, +} + +impl Timer { + pub(crate) fn new(duration: Duration, size: u16, style: Style) -> Self { + Self { + duration, + size, + style, + ended_at: Some(Local::now() + duration), + } + } + + pub(crate) fn is_paused(&self) -> bool { + self.ended_at.is_none() + } + + pub(crate) fn pause(&mut self) { + if let Some(end_at) = self.ended_at { + if end_at <= Local::now() { + self.duration = Duration::zero(); + } else { + self.duration = end_at - Local::now(); + } + self.ended_at = None; + } + } + + pub(crate) fn resume(&mut self) { + if self.ended_at.is_none() { + self.ended_at = Some(Local::now() + self.duration); + } + } + + pub(crate) fn remaining_time(&self) -> Duration { + if let Some(end_at) = self.ended_at { + let now = Local::now(); + if end_at <= now { + Duration::zero() + } else { + end_at.signed_duration_since(now) + } + } else { + self.duration + } + } +} + +impl Widget for &Timer { + fn render(self, area: Rect, buf: &mut Buffer) { + let time_str = format_duration(self.remaining_time()); + // println!("{}", time_str); + let text = BricksText::new(time_str.as_str(), self.size, self.size, self.style); + render_centered(area, buf, &text); + } +} diff --git a/src/bricks_text.rs b/src/bricks_text.rs new file mode 100644 index 0000000..ebcf70a --- /dev/null +++ b/src/bricks_text.rs @@ -0,0 +1,46 @@ +use tui::{style::Style, widgets::Widget}; + +use self::chars::{BrickChar, Point}; + +mod chars; + +pub struct BricksText { + text: String, + size: u16, + space: u16, + style: Style, +} + +impl BricksText { + pub fn new(text: &str, size: u16, space: u16, style: Style) -> BricksText { + BricksText { + text: text.to_string(), + size, + space, + style, + } + } + + pub fn size(&self) -> (u16, u16) { + let Point(w, h) = BrickChar::size(self.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.clone(); + for char in self.text.chars() { + let Point(w, _) = BrickChar::size(self.size); + let char = BrickChar::from(char); + char.render(self.size, self.style, area, buf); + let l = w + self.space; + area.x += l; + area.width = area.width.saturating_sub(l); + if area.area() == 0 { + break; + } + } + } +} diff --git a/src/bricks_text/chars.rs b/src/bricks_text/chars.rs new file mode 100644 index 0000000..54e5084 --- /dev/null +++ b/src/bricks_text/chars.rs @@ -0,0 +1,265 @@ +use std::{cmp::max, ops}; +use tui::{buffer::Buffer, layout::Rect, style::Style}; + +pub(crate) trait BricksChar { + fn render(&self, size: u16, style: Style, area: Rect, buf: &mut Buffer); +} + +pub struct BrickChar(char); + +impl BrickChar { + const H_UNIT: u16 = 2; + const V_UNIT: u16 = 1; + const UNIT_SIZE: Point = Point(3 * Self::H_UNIT, 5 * Self::V_UNIT); + + pub(crate) fn size(size: u16) -> Point { + Self::UNIT_SIZE.clone() * size + } + + pub(crate) fn from(char: char) -> BrickChar { + BrickChar(char) + } + + pub(crate) fn render(&self, size: u16, style: Style, area: Rect, buf: &mut Buffer) { + let char_size = BrickChar::size(size); + match self.0 { + '0'..='9' => Self::draw_digital(self.0, size, style, area, buf), + ':' => { + let start_x = area.x + size * Self::H_UNIT; + let end_x = area.x + char_size.0 - size * Self::H_UNIT; + let start_y = area.y + size * Self::V_UNIT; + let start_y2 = area.y + (char_size.1 + size * Self::V_UNIT) / 2; + let len = (char_size.1 - 3 * size * Self::V_UNIT) / 2; + for x in (start_x..end_x).step_by((size * Self::H_UNIT) as usize) { + Self::draw_line( + size, + Point(x, start_y), + len, + LineDir::Vertical, + style, + &area, + buf, + ); + Self::draw_line( + size, + Point(x, start_y2), + len, + LineDir::Vertical, + style, + &area, + buf, + ); + } + } + '-' => { + let x = area.x; + let y = area.y + (char_size.1 - size * Self::V_UNIT) / 2; + Self::draw_line( + size, + Point(x, y), + char_size.1, + LineDir::Horizontal, + style, + &area, + buf, + ); + } + '.' => { + let x = area.x + char_size.0 - size * Self::H_UNIT; + let y = area.y + char_size.1 - size * Self::V_UNIT; + Self::draw_line( + size, + Point(x, y), + size * Self::H_UNIT, + LineDir::Horizontal, + style, + &area, + buf, + ); + } + _ => {} + } + } + + fn draw_digital(d: char, size: u16, style: Style, area: Rect, buf: &mut Buffer) { + let char_size = BrickChar::size(size); + let mut draw_line = + |x, y, len, dir| Self::draw_line(size, Point(x, y), len, dir, style, &area, buf); + let x_start = area.x; + let x_end = area.x + char_size.0 - size * Self::H_UNIT; + let y_start = area.y; + let y_end = area.y + char_size.1 - size * Self::V_UNIT; + let y_center = area.y + (char_size.1 - size * Self::V_UNIT) / 2; + let half_h = (char_size.1 + size * Self::V_UNIT) / 2; + match d { + '0' => { + draw_line(x_start, y_start, half_h, LineDir::Vertical); + draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + // draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '1' => { + // draw_line(x_start, y_start, half_h, LineDir::Vertical); + // draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + // draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + // draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + // draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '2' => { + // draw_line(x_start, y_start, half_h, LineDir::Vertical); + draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + // draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '3' => { + // draw_line(x_start, y_start, half_h, LineDir::Vertical); + // draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '4' => { + draw_line(x_start, y_start, half_h, LineDir::Vertical); + // draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + // draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + // draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '5' => { + draw_line(x_start, y_start, half_h, LineDir::Vertical); + // draw_line(x_start, y_center, half_h, LineDir::Vertical); + // draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '6' => { + draw_line(x_start, y_start, half_h, LineDir::Vertical); + draw_line(x_start, y_center, half_h, LineDir::Vertical); + // draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '7' => { + // draw_line(x_start, y_start, half_h, LineDir::Vertical); + // draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + // draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + // draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '8' => { + draw_line(x_start, y_start, half_h, LineDir::Vertical); + draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + '9' => { + draw_line(x_start, y_start, half_h, LineDir::Vertical); + // draw_line(x_start, y_center, half_h, LineDir::Vertical); + draw_line(x_end, y_start, half_h, LineDir::Vertical); + draw_line(x_end, y_center, half_h, LineDir::Vertical); + + draw_line(x_start, y_start, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_center, char_size.0, LineDir::Horizontal); + draw_line(x_start, y_end, char_size.0, LineDir::Horizontal); + } + _ => {} + } + } + + fn draw_line( + size: u16, + start: Point, + len: u16, + dir: LineDir, + style: Style, + area: &Rect, + buf: &mut Buffer, + ) { + let step = match dir { + LineDir::Horizontal => Point(Self::H_UNIT, 0), + LineDir::Vertical => Point(0, Self::V_UNIT), + }; + + let line = match dir { + LineDir::Horizontal => Point(0, Self::V_UNIT), + LineDir::Vertical => Point(Self::H_UNIT, 0), + }; + + let mut from = start; + for _ in 0..size { + let mut p = from; + for _ in (0..len).step_by(max(step.0, step.1).into()) { + if !p.in_area(&area) { + break; + } + // println!("p = {:?} area = {:?}", p, area); + buf.get_mut(p.0, p.1).set_symbol("██").set_style(style); + p = p + &step; + } + from = from + &line; + } + } +} + +enum LineDir { + Horizontal, + Vertical, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Point(pub u16, pub u16); + +impl Point { + pub(crate) fn in_area(&self, area: &Rect) -> bool { + area.left() <= self.0 + && self.0 < area.right() + && area.top() <= self.1 + && self.1 < area.bottom() + } +} + +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 for Point { + type Output = Point; + + fn mul(self, other: u16) -> Point { + Point(self.0 * other, self.1 * other) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2b82456 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod bricks_text; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cd3bea8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,79 @@ +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; + +use clap::Parser; +use crossterm::{ + event::{self, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use tui::{ + backend::{Backend, CrosstermBackend}, + Terminal, +}; + +mod app; + +fn run_app( + terminal: &mut Terminal, + app: &mut app::App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + + loop { + terminal.draw(|f| app.ui(f))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or(Duration::from_secs(0)); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => { + return Ok(()); + } + key => app.on_key(key), + } + } + } + + if last_tick.elapsed() >= tick_rate { + last_tick = Instant::now(); + } + } +} + +fn main() -> Result<(), Box> { + let mut app = 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 tick_rate = Duration::from_millis(100); + 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(()) +}