initial implement clock-tui

This commit is contained in:
race604 2022-07-19 13:25:30 +08:00
commit a08b687a87
12 changed files with 1403 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

633
Cargo.lock generated Normal file
View file

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

14
Cargo.toml Normal file
View file

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

148
src/app.rs Normal file
View file

@ -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<Clock>,
#[clap(skip)]
timer: Option<Timer>,
#[clap(skip)]
stopwatch: Option<Stopwatch>,
}
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<B: Backend>(&self, f: &mut Frame<B>) {
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<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).into()),
}
}
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 = hex[1..3].parse::<u8>().unwrap();
let g = hex[3..5].parse::<u8>().unwrap();
let b = hex[5..7].parse::<u8>().unwrap();
Ok(Color::Rgb(r, g, b))
}
}
}

42
src/app/modes.rs Normal file
View file

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

50
src/app/modes/clock.rs Normal file
View file

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

View file

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

66
src/app/modes/timer.rs Normal file
View file

@ -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<DateTime<Local>>,
}
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);
}
}

46
src/bricks_text.rs Normal file
View file

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

265
src/bricks_text/chars.rs Normal file
View file

@ -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<u16> for Point {
type Output = Point;
fn mul(self, other: u16) -> Point {
Point(self.0 * other, self.1 * other)
}
}

1
src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod bricks_text;

79
src/main.rs Normal file
View file

@ -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<B: Backend>(
terminal: &mut Terminal<B>,
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<dyn Error>> {
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(())
}