diff --git a/.github/img/logo-cli-tui.svg b/.github/img/logo-cli-tui.svg
new file mode 100644
index 0000000..5741f04
--- /dev/null
+++ b/.github/img/logo-cli-tui.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 11e0971..8c91779 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,24 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -32,6 +50,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "autocfg"
version = "1.2.0"
@@ -55,9 +82,9 @@ dependencies = [
[[package]]
name = "bitflags"
-version = "2.5.0"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
@@ -65,6 +92,33 @@ version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
+[[package]]
+name = "by_address"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
+
+[[package]]
+name = "bytes"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
+dependencies = [
+ "rustversion",
+]
+
[[package]]
name = "cc"
version = "1.0.90"
@@ -79,9 +133,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.37"
+version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -91,6 +145,20 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "compact_str"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@@ -99,13 +167,61 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "croner"
-version = "2.0.4"
+version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "516aad5374ea0ea75a0f0f4512fb4e7ad46c5eeff9971cb8ebc8fd74f1cd16c1"
+checksum = "eba3aaaafb3c313b352ff02626adb2dfaf9663159d339a878f6e5b2f6259a97c"
dependencies = [
"chrono",
]
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
[[package]]
name = "eyre"
version = "0.6.12"
@@ -116,6 +232,101 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "fast-srgb8"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
[[package]]
name = "gimli"
version = "0.28.1"
@@ -128,6 +339,28 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d"
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
[[package]]
name = "iana-time-zone"
version = "0.1.60"
@@ -157,6 +390,31 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
+[[package]]
+name = "instability"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
[[package]]
name = "js-sys"
version = "0.3.69"
@@ -168,15 +426,31 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.153"
+version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
[[package]]
name = "log"
-version = "0.4.21"
+version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lool"
@@ -185,11 +459,27 @@ dependencies = [
"bitflags",
"chrono",
"croner",
+ "crossterm",
+ "downcast-rs",
"eyre",
+ "futures",
"glob-match",
"log",
"num-traits",
+ "palette",
+ "ratatui",
+ "strum",
"tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "lru"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
+dependencies = [
+ "hashbrown",
]
[[package]]
@@ -208,10 +498,23 @@ dependencies = [
]
[[package]]
-name = "num-traits"
-version = "0.2.18"
+name = "mio"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
@@ -231,12 +534,113 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+[[package]]
+name = "palette"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
+dependencies = [
+ "approx",
+ "fast-srgb8",
+ "palette_derive",
+ "phf",
+]
+
+[[package]]
+name = "palette_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
+dependencies = [
+ "by_address",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
[[package]]
name = "proc-macro2"
version = "1.0.79"
@@ -255,12 +659,167 @@ 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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "ratatui"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d"
+dependencies = [
+ "bitflags",
+ "cassowary",
+ "compact_str",
+ "crossterm",
+ "instability",
+ "itertools",
+ "lru",
+ "paste",
+ "strum",
+ "strum_macros",
+ "unicode-segmentation",
+ "unicode-truncate",
+ "unicode-width",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+[[package]]
+name = "rustix"
+version = "0.38.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
[[package]]
name = "syn"
version = "2.0.58"
@@ -274,9 +833,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.37.0"
+version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
+checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"pin-project-lite",
@@ -285,21 +844,69 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.2.0"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "tokio-util"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-truncate"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
+dependencies = [
+ "itertools",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[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.92"
@@ -354,6 +961,28 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+[[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-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -363,6 +992,15 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-targets"
version = "0.52.4"
@@ -419,3 +1057,23 @@ name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 5dd7cc4..aede35d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,9 +22,25 @@ debug-assertions = false
path = "lib/lib.rs"
[features]
-# cli utilities
+# cli/tui utilities
"cli" = []
"cli.stylize" = ["cli", "dep:bitflags"]
+"cli.tui" = [
+ "cli",
+ "dep:ratatui",
+ "dep:palette",
+ "dep:crossterm",
+ "dep:strum",
+ "dep:downcast-rs",
+ "dep:futures",
+ "dep:tokio",
+ "crossterm?/event-stream",
+ "strum?/derive",
+ "tokio?/tokio-macros",
+ "tokio?/macros",
+ "tokio?/sync",
+ "tokio?/time",
+]
# logging
"logger" = ["dep:log", "dep:glob-match"]
# macros
@@ -52,13 +68,20 @@ path = "lib/lib.rs"
eyre = { version = "0.6.12", default-features = false }
# optional
-bitflags = { version = "2.5.0", optional = true }
+bitflags = { version = "2.6.0", optional = true }
chrono = { version = "0.4.37", optional = true }
-log = { version = "0.4.21", optional = true }
-tokio = { version = "1.37.0", optional = true }
-croner = { version = "2.0.4", optional = true }
-num-traits = { version = "0.2.18", optional = true }
+log = { version = "0.4.22", optional = true }
+tokio = { version = "1.40.0", optional = true }
+croner = { version = "2.0.5", optional = true }
+num-traits = { version = "0.2.19", optional = true }
glob-match = { version = "0.2.1", optional = true }
+tokio-util = { version = "0.7.12", optional = true }
+ratatui = { version = "0.28.1", optional = true }
+palette = { version = "0.7.6", optional = true }
+crossterm = { version="0.28.1", optional = true}
+strum = { version="0.26.1", optional = true }
+downcast-rs = { version="1.2.1", optional = true}
+futures = { version = "0.3.30", optional = true }
[[example]]
diff --git a/lib/cli/mod.rs b/lib/cli/mod.rs
index 07e9900..39f2d96 100644
--- a/lib/cli/mod.rs
+++ b/lib/cli/mod.rs
@@ -1,2 +1,5 @@
#[cfg(feature = "cli.stylize")]
pub mod stylize;
+
+#[cfg(feature = "cli.tui")]
+pub mod tui;
diff --git a/lib/cli/tui/README.md b/lib/cli/tui/README.md
new file mode 100644
index 0000000..74dbea7
--- /dev/null
+++ b/lib/cli/tui/README.md
@@ -0,0 +1,24 @@
+

+
+
+
+
+
+lool ยป cli.stylize is a set of utilities for colorizing console outputs.
+
+
+
+
+
+
+# Installation
+
+This crate is for internal use. It's only published privately.
+
+```bash
+cargo add lool --registry=lugit --features cli cli.tui
+```
+
+# Usage
+
+Pending.
\ No newline at end of file
diff --git a/lib/cli/tui/framework/app.rs b/lib/cli/tui/framework/app.rs
new file mode 100644
index 0000000..34830fa
--- /dev/null
+++ b/lib/cli/tui/framework/app.rs
@@ -0,0 +1,184 @@
+use {
+ super::{
+ component::Component,
+ events::{Action, Event},
+ keyboard::KeyBindings,
+ tui::Tui,
+ },
+ crossterm::event::{KeyCode, KeyEvent},
+ eyre::Result,
+ ratatui::prelude::Rect,
+ std::{collections::HashMap, str::FromStr},
+ tokio::sync::mpsc::{self, error::TryRecvError},
+};
+
+pub type Kb<'a> = HashMap<&'a str, String>;
+
+pub struct App {
+ pub tick_rate: f64,
+ pub frame_rate: f64,
+ pub components: Vec>,
+ pub should_quit: bool,
+ pub should_suspend: bool,
+ pub keybindings: KeyBindings,
+ pub last_tick_key_events: Vec,
+ action_tx: mpsc::UnboundedSender,
+ action_rx: mpsc::UnboundedReceiver,
+}
+
+impl App {
+ pub fn new(kb: Kb, components: Vec>) -> Result {
+ let keybindings = KeyBindings::new(kb);
+ let (action_tx, action_rx) = mpsc::unbounded_channel::();
+
+ Ok(Self {
+ tick_rate: 1.into(),
+ frame_rate: 4.into(),
+ action_tx,
+ action_rx,
+ components,
+ keybindings,
+ should_quit: false,
+ should_suspend: false,
+ last_tick_key_events: Vec::new(),
+ })
+ }
+
+ fn send(&self, action: Action) -> Result<()> {
+ match action {
+ Action::AppAction(cmd) => self.action_tx.send(cmd)?,
+ Action::Key(key) => self.action_tx.send(key)?,
+ action => self.action_tx.send(action.to_string())?,
+ }
+
+ Ok(())
+ }
+
+ fn try_recv(&mut self) -> Result {
+ self.action_rx.try_recv()
+ }
+
+ pub async fn run(&mut self) -> Result<()> {
+ let mut tui = Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate);
+
+ tui.enter()?;
+
+ for component in self.components.iter_mut() {
+ component.register_action_handler(self.action_tx.clone())?;
+ }
+
+ for component in self.components.iter_mut() {
+ component.init(tui.size()?)?;
+ }
+
+ loop {
+ if let Some(e) = tui.next().await {
+ match e {
+ Event::Quit => self.send(Action::Quit)?,
+ Event::Tick => self.send(Action::Tick)?,
+ Event::Render => self.send(Action::Render)?,
+ // Event::Resize(x, y) => self.send(Action::Resize(x, y))?,
+ Event::Key(key) => {
+ if let Some(action) = self.keybindings.get(&vec![key]) {
+ self.send(action.clone())?;
+ } else {
+ // If the key was not handled as a single key action,
+ // then consider it for multi-key combinations.
+ self.last_tick_key_events.push(key);
+
+ // Check for multi-key combinations
+ if let Some(action) = self.keybindings.get(&self.last_tick_key_events) {
+ self.send(action.clone())?;
+ }
+ }
+
+ // send the key event as simple key event too (not as action) if it's a
+ // single alphanumeric char key
+ if let KeyCode::Char(c) = key.code {
+ if c.is_alphanumeric() {
+ self.send(Action::Key(c.to_string()))?;
+ }
+ }
+ }
+ _ => {}
+ }
+ let mut actions = Vec::new();
+
+ for component in self.components.iter_mut() {
+ let component_actions = component.handle_events(Some(e.clone()))?;
+ actions.extend(component_actions);
+ }
+
+ for action in actions {
+ self.send(action)?;
+ }
+ }
+
+ while let Ok(action) = self.try_recv() {
+ let enum_action = Action::from_str(&action).ok();
+ if let Some(a) = enum_action {
+ match a {
+ Action::Tick => {
+ self.last_tick_key_events.drain(..);
+ }
+ Action::Quit => self.should_quit = true,
+ Action::Suspend => self.should_suspend = true,
+ Action::Resume => self.should_suspend = false,
+ Action::Resize(w, h) => {
+ tui.resize(Rect::new(0, 0, w, h))?;
+ let mut errors = Vec::new();
+ tui.draw(|f| {
+ for component in self.components.iter_mut() {
+ let r = component.draw(f, f.area());
+ if let Err(e) = r {
+ errors.push(format!("Failed to draw: {:?}", e));
+ }
+ }
+ })?;
+ for error in errors {
+ self.send(Action::Error(error)).unwrap();
+ }
+ }
+ Action::Render => {
+ let mut errors = Vec::new();
+ tui.draw(|f| {
+ for component in self.components.iter_mut() {
+ let r = component.draw(f, f.area());
+ if let Err(e) = r {
+ errors.push(format!("Failed to draw: {:?}", e));
+ }
+ }
+ })?;
+ for error in errors {
+ self.send(Action::Error(error)).unwrap();
+ }
+ }
+ _ => {}
+ }
+
+ for component in self.components.iter_mut() {
+ component.update(a.clone())?;
+ }
+ } else {
+ // unrecognized action, might be a custom component action
+ // send it to all components as a raw string
+ for component in self.components.iter_mut() {
+ let _ = component.receive_message(action.clone());
+ }
+ }
+ }
+ if self.should_suspend {
+ tui.suspend()?;
+ self.send(Action::Resume)?;
+ tui = Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate);
+ // tui.mouse(true);
+ tui.enter()?;
+ } else if self.should_quit {
+ tui.stop()?;
+ break;
+ }
+ }
+ tui.exit()?;
+ Ok(())
+ }
+}
diff --git a/lib/cli/tui/framework/component.rs b/lib/cli/tui/framework/component.rs
new file mode 100644
index 0000000..602df7d
--- /dev/null
+++ b/lib/cli/tui/framework/component.rs
@@ -0,0 +1,351 @@
+use {
+ super::{
+ events::{Action, Event},
+ tui::Frame,
+ },
+ crossterm::event::{KeyEvent, MouseEvent},
+ downcast_rs::{impl_downcast, Downcast},
+ eyre::Result,
+ ratatui::layout::{Rect, Size},
+ std::collections::HashMap,
+ tokio::sync::mpsc::UnboundedSender,
+};
+
+pub type Children = HashMap>;
+
+// TODO: create a component! macro to simplify the creation of components, adding the boilerplate
+// code, similar to what we do with the `rustler!` macro in the `rustler` crate.
+
+/// `Component` is a trait that represents a visual and interactive element of the user interface.
+/// Implementors of this trait can be registered with the main application loop and will be able to
+/// receive events,
+/// update state, and be rendered on the screen.
+pub trait Component: Downcast {
+ /// Register an action handler that can send actions for processing if necessary.
+ ///
+ /// # Arguments
+ ///
+ /// * `tx` - An unbounded sender that can send actions.
+ ///
+ /// # Returns
+ ///
+ /// * `Result<()>` - An Ok result or an error.
+ #[allow(unused_variables)]
+ fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> {
+ pass_action_handler_to_children(self, tx)
+ }
+
+ /// Initialize the component with a specified area if necessary.
+ /// By default, this method will pass the initialization to the children. If you want to
+ /// override this method, you will lose calling the children's `init` method. That's why we
+ /// provide a helper function to initialize the children. Something like the following is
+ /// recommended:
+ ///
+ /// ```rust
+ /// fn init(&mut self, area: Rect) -> Result<()> {
+ /// // Do something with the area
+ ///
+ /// // Initialize the children
+ /// init_children(self, area)
+ /// }
+ /// ```
+ ///
+ /// # Arguments
+ ///
+ /// * `area` - Rectangular area to initialize the component within.
+ ///
+ /// # Returns
+ ///
+ /// * `Result<()>` - An Ok result or an error.
+ #[allow(unused)]
+ fn init(&mut self, area: Size) -> Result<()> {
+ init_children(self, area)
+ }
+
+ /// Handle incoming events and produce actions if necessary.
+ /// In most cases, you should avoid overriding this method, as it will handle the children's
+ /// events and also rerout actions to `self.handle_key_events` and `self.handle_mouse_events`.
+ ///
+ /// In most cases, you might want to implement those two methods instead of this one.
+ ///
+ /// # Arguments
+ ///
+ /// * `event` - An optional event to be processed.
+ ///
+ /// # Returns
+ ///
+ /// * `Result