feat(cli): tui framework

This commit is contained in:
Lucas Colombo 2024-09-17 03:43:44 -03:00
parent 6702ba8240
commit d621a0a657
Signed by: lucas
GPG Key ID: EF34786CFEFFAE35
11 changed files with 1824 additions and 23 deletions

18
.github/img/logo-cli-tui.svg vendored Normal file
View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 142 182">
<style>
.a { fill: #000000; }
@media (prefers-color-scheme: dark) {
.a { fill: #ffffff; }
}
</style>
<path class="a" d="M4.91962,1.516H0V0H20.42877V61.6416h4.6695v1.51648H.3335V61.6416H4.91962ZM32.68567,58.60962q-5.33725-5.3865-5.33649-15.45264,0-10.06072,5.71161-15.49371,5.71014-5.432,16.051-5.432,10.33767,0,15.42578,5.01056Q69.62248,32.25306,69.624,42.5265,69.624,63.999,48.44489,64,38.02069,64,32.68567,58.60962ZM43.35858,39.83124v6.56811q0,9.18109.54187,11.03144a18.93828,18.93828,0,0,0,1.12549,3.032,3.54886,3.54886,0,0,0,3.58575,2.02167q3.252,0,4.25214-3.79071.751-2.69324.75079-12.12592v-7.1575a57.79357,57.79357,0,0,0-.87579-11.916q-.87543-3.74541-4.04382-3.74713a4.16107,4.16107,0,0,0-2.96,1.05243,6.4872,6.4872,0,0,0-1.62616,3.495A64.699,64.699,0,0,0,43.35858,39.83124ZM137.33051,61.6416V0H116.90173V1.516h4.91962V61.6416h-4.58618v1.51648H142V61.6416Zm-59.36792-3.032Q72.62468,53.2231,72.62518,43.157q0-10.06072,5.7116-15.49371,5.71161-5.432,16.051-5.432,10.33839,0,15.42669,5.01056,5.08347,5.01123,5.08538,15.28473Q114.8999,63.999,93.721,64q-10.423,0-15.75841-5.39038ZM88.6355,39.83124v6.56811q0,9.18109.541,11.03144a18.93282,18.93282,0,0,0,1.12641,3.032,3.54715,3.54715,0,0,0,3.58477,2.02167q3.25276,0,4.25269-3.79071.75165-2.69324.75122-12.12592h.00006v-7.1575a57.81892,57.81892,0,0,0-.87634-11.916q-.87533-3.74541-4.04425-3.74713a4.16011,4.16011,0,0,0-2.95954,1.05243,6.48466,6.48466,0,0,0-1.62567,3.495A64.70833,64.70833,0,0,0,88.6355,39.83124Z" />
<path class="a" d="M0,80H142V78H0Z" />
<path class="a" d="M94.54443,99.29932a2.3565,2.3565,0,0,0,.6631-1.6836v-.98536a2.33921,2.33921,0,0,0-.6631-1.70018,4.44567,4.44567,0,0,0-4.72656,0,2.33846,2.33846,0,0,0-.66211,1.70018v.98536a2.35572,2.35572,0,0,0,.66211,1.6836A4.35366,4.35366,0,0,0,94.54443,99.29932Zm-46.23925,7.54785a3.46851,3.46851,0,0,1,2.66894-1.00293,3.19824,3.19824,0,0,1,2.19336.67969,5.16812,5.16812,0,0,1,1.24072,1.70018l3.876-2.10839a7.58028,7.58028,0,0,0-2.669-3.11036,8.21,8.21,0,0,0-4.64111-1.17285,10.26889,10.26889,0,0,0-3.689.62891,7.50149,7.50149,0,0,0-2.78809,1.81836,8.0855,8.0855,0,0,0-1.751,2.89061,12.368,12.368,0,0,0,0,7.6836,8.0687,8.0687,0,0,0,1.751,2.89063A7.48955,7.48955,0,0,0,47.2851,119.563a10.37091,10.37091,0,0,0,3.72314.6289A8.69136,8.69136,0,0,0,55.7851,119.019a7.65872,7.65872,0,0,0,2.771-3.11035L54.748,113.73289a5.69171,5.69171,0,0,1-1.41064,1.76758,4.22674,4.22674,0,0,1-5.03222-.32324,3.85523,3.85523,0,0,1-.93506-2.73731v-2.85547A3.85523,3.85523,0,0,1,48.30518,106.84717Zm14.501,8.99316v3.94336H78.58154v-3.94336H73.21V94.62354H62.80615v3.94532h5.37207v17.27147Zm21.28321,0v3.94336H99.86572v-3.94336h-5.168V102.24072H84.08936v3.94336h5.57617v9.65625Z" />
<path class="a" d="M.1125,136h142v-2h-142Z" />
<path class="a" d="M113.99849,150.75006l-.00338,29.82263c0,1.40576-.00076,1.43286-1.437,1.42682L107.99972,182l-2-4,.01227-24,1.98773-4,5.2824-.00049C113.80174,149.99066,114.0182,150.22613,113.99849,150.75006ZM71.09006,150H65.99637l-2.025,7.84283V172l2,4,2.02783,2h2l2-2V150.95184C72.01657,150.23389,71.738,149.97736,71.09006,150Zm-19.09288.93536c.01294-.62616-.12866-.93237-.83526-.9129L46,150l-8,4v28l6.38239-.00488c1.71179-.00659,1.53778.20123,1.54071-1.50843.014-8.012.06695-16.47442.0769-24.48669l5.137.07477c.67347.0177.87873-.2204.86206-.88092C51.96044,153.66315,51.96538,152.46637,51.99718,150.93536Zm42.00043-.10565c.00116-.6178-.20532-.86523-.83746-.85425H88l-4,10v10.00379l-1.98486,5.99621L80,178v2l2,2,5.01953-.00586c.82953.02411.99329-.287.97974-.95581-.0365-1.79535-.008-3.24231-.00525-5.03833l.00006-.04834.00592.0238h5.09876c.667.01355.90283-.21789.90124-.89441C93.98125,167.11383,93.9829,158.79694,93.99761,150.82971Z" />
<path d="M28.61473,150c-.51233-.00214-.62006.2132-.61444.67511.02069,1.70349.02887,3.07172,0,4.775-.00873.51246.18084.55726.64862.54987H32.76c1.08905-.07819,1.24628.3313,1.2403,1.36353v23.64331c-.00354.82281.24206,1.01861,1.02051.9906L40.00865,182c.00555-8.14337.02985-16.66907-.0083-24.81226-.00452-.9696.28625-1.23071,1.18017-1.18774h4.81977v-6Z" style="fill:#39c976" />
<path d="M82,150.80725l-.0108,25.18726L66.80128,176c-.86212.037-.8302-.45453-.829-1.0451L66,150h-5.4176c-.579-.01239-.58271.30975-.5824.72021v24.49018c-.00122.57733.16406.8031.75885.78961h4.18066c.83344-.03284,1.09235-.16266,1.06049.61487v4.49988c-.01325.60809-.01215.89416.73822.88525H82l.01392-6H88V150H82.7843C82.11127,149.98059,81.99829,150.20361,82,150.80725Z" style="fill:#39c976" />
<path d="M102.55548,150c-.545-.01221-.55181.3-.55157.715L102,181.27057c-.00055.49359.07032.74328.63251.72943h5.35156L108,150Z" style="fill:#39c976" />
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

692
Cargo.lock generated
View File

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

View File

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

View File

@ -1,2 +1,5 @@
#[cfg(feature = "cli.stylize")]
pub mod stylize;
#[cfg(feature = "cli.tui")]
pub mod tui;

24
lib/cli/tui/README.md Normal file
View File

@ -0,0 +1,24 @@
<p align="center"><img src="../../../.github/img/logo-cli-tui.svg" width="142"></p>
<br>
<br>
<br>
<p align="center"><b>lool » <code>cli.stylize</code></b> is a set of utilities for colorizing console outputs.
</p>
<br>
<br>
<br>
# Installation
This crate is for internal use. It's only published privately.
```bash
cargo add lool --registry=lugit --features cli cli.tui
```
# Usage
Pending.

View File

@ -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<Box<dyn Component>>,
pub should_quit: bool,
pub should_suspend: bool,
pub keybindings: KeyBindings,
pub last_tick_key_events: Vec<KeyEvent>,
action_tx: mpsc::UnboundedSender<String>,
action_rx: mpsc::UnboundedReceiver<String>,
}
impl App {
pub fn new(kb: Kb, components: Vec<Box<dyn Component>>) -> Result<Self> {
let keybindings = KeyBindings::new(kb);
let (action_tx, action_rx) = mpsc::unbounded_channel::<String>();
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<String, TryRecvError> {
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(())
}
}

View File

@ -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<String, Box<dyn Component>>;
// 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<String>) -> 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<Option<Action>>` - An action to be processed or none.
fn handle_events(&mut self, event: Option<Event>) -> Result<Vec<Action>> {
let mut actions = vec![];
let action = match event {
Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?,
_ => None,
};
if let Some(action) = action {
actions.push(action);
}
if let Some(children) = self.get_children() {
for child in children.values_mut() {
let child_actions = child.handle_events(event.clone())?;
actions.extend(child_actions);
}
}
Ok(actions)
}
/// Handle key events and produce actions if necessary.
///
/// # Arguments
///
/// * `key` - A key event to be processed.
///
/// # Returns
///
/// * `Result<Option<Action>>` - An action to be processed or none.
#[allow(unused_variables)]
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
Ok(None)
}
/// Handle mouse events and produce actions if necessary.
///
/// # Arguments
///
/// * `mouse` - A mouse event to be processed.
///
/// # Returns
///
/// * `Result<Option<Action>>` - An action to be processed or none.
#[allow(unused_variables)]
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Option<Action>> {
Ok(None)
}
/// Update the state of the component based on a received action. If you want to override this
/// method, you will lose calling the children's update methods. That's why we provide a helper
/// function to update the children's state. Something like the following is recommended:
///
/// ```rust
/// fn update(&mut self, action: Action) -> Result<()> {
/// // Do something with the action
///
/// // Update the children
/// update_children(self, action)
/// }
/// ```
///
/// # Arguments
///
/// * `action` - An action that may modify the state of the component.
///
/// # Returns
///
/// * `Result<Option<Action>>` - An action to be processed or none.
#[allow(unused_variables)]
fn update(&mut self, action: Action) -> Result<()> {
update_children(self, action)
}
/// Receive a custom message, probably from another component.
/// If you want to override this method, you will lose calling the children's `receive_message`
/// method. That's why we provide a helper function to pass the message. Something like the
/// following is recommended:
///
/// ```rust
/// fn receive_message(&mut self, message: String) -> Result<()> {
/// // Do something with the message
///
/// // Pass the message to the children
/// pass_message_to_children(self, message)
/// }
/// ```
/// # Arguments
///
/// * `message` - A string message to be processed.
///
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
#[allow(unused_variables)]
fn receive_message(&mut self, message: String) -> Result<()> {
pass_message_to_children(self, message)
}
/// Render the component on the screen. (REQUIRED)
///
/// # Arguments
///
/// * `f` - A frame used for rendering.
/// * `area` - The area in which the component should be drawn.
///
/// # Returns
///
/// * `Result<()>` - An Ok result or an error.
fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()>;
/// Get a child component by name as a mutable reference.
///
/// # Arguments
/// * `name` - The name of the child component.
///
/// # Returns
/// * `Option<&mut Box<dyn Component>>` - A mutable reference to the child component or none.
fn child_mut(&mut self, name: &str) -> Option<&mut Box<dyn Component>> {
if let Some(children) = self.get_children() {
children.get_mut(name)
} else {
None
}
}
/// Get a child component by name as an immutable reference.
///
/// # Arguments
/// * `name` - The name of the child component.
///
/// # Returns
/// * `Option<&Box<dyn Component>>` - A reference to the child component or none.
fn child(&mut self, name: &str) -> Option<&Box<dyn Component>> {
if let Some(children) = self.get_children() {
children.get(name)
} else {
None
}
}
/// get all child components. This is necessary if the component has children, as will be
/// used by other functions to have knowledge of the children.
///
/// # Attributes
/// * `children`: `HashMap<String, Box<dyn Component>>` - All child components.
///
/// # Returns
///
/// * `Vec[&mut Box<dyn Component>]` - A vector of mutable references to the child components.
fn get_children(&mut self) -> Option<&mut Children> {
None
}
/// gets the active state of the component.
///
/// # Returns
/// * `bool` - The active state of the component.
fn is_active(&self) -> bool {
true
}
/// Set the active state of the component.
///
/// # Arguments
/// * `active` - The active state of the component.
fn set_active(&mut self, _active: bool) {}
}
impl_downcast!(Component);
/// Update the children's state based on a received action.
///
/// This helper function is used to update the children's state based on a received action. It was
/// created to allow to easily override the default `update` method of a component implementation and
/// be able to call the children's `update` method.
pub fn update_children<T: Component + ?Sized>(this: &mut T, action: Action) -> Result<()> {
if let Some(children) = this.get_children() {
for child in children.values_mut() {
child.update(action.clone())?;
}
}
Ok(())
}
/// Pass a message to the children of a component.
///
/// This helper function is used to pass a message to the children of a component. It was created
/// to allow to easily override the default `receive_message` method of a component implementation
/// and be able pass the call to the children's `receive_message` method.
pub fn pass_message_to_children<T: Component + ?Sized>(
this: &mut T,
message: String,
) -> Result<()> {
if let Some(children) = this.get_children() {
for child in children.values_mut() {
child.receive_message(message.clone())?;
}
}
Ok(())
}
/// Initialize the children of a component.
///
/// This helper function is used to initialize the children of a component. It was created to
/// allow to easily override the default `init` method of a component implementation and be able
/// to call the children's `init` method.
pub fn init_children<T: Component + ?Sized>(this: &mut T, area: Size) -> Result<()> {
if let Some(children) = this.get_children() {
for child in children.values_mut() {
child.init(area)?;
}
}
Ok(())
}
/// Pass the action handler to the children of a component.
///
/// This helper function is used to pass the action handler to the children of a component. It was
/// created to allow to easily override the default `register_action_handler` method of a component
/// implementation and be able to call the children's `register_action_handler` method.
pub fn pass_action_handler_to_children<T: Component + ?Sized>(
this: &mut T,
tx: UnboundedSender<String>,
) -> Result<()> {
if let Some(children) = this.get_children() {
for child in children.values_mut() {
child.register_action_handler(tx.clone())?;
}
}
Ok(())
}
/// Get a child downcasted to a specific type by name as a mutable reference.
///
/// # Arguments
/// * `name` - The name of the child component.
///
/// # Returns
/// * `Option<&mut T>` - A mutable reference to the child component or none.
pub fn child_downcast_mut<'a, CastTo: Component, This: Component + ?Sized>(
this: &'a mut This,
name: &str,
) -> Option<&'a mut CastTo> {
if let Some(child) = this.child_mut(name) {
child.downcast_mut::<CastTo>()
} else {
None
}
}
/// Get a child downcasted to a specific type by name as an immutable reference.
///
/// # Arguments
/// * `name` - The name of the child component.
///
/// # Returns
/// * `Option<&T>` - A reference to the child component or none.
pub fn child_downcast<'a, CastTo: Component, This: Component + ?Sized>(
this: &'a mut This,
name: &str,
) -> Option<&'a CastTo> {
if let Some(child) = this.child(name) {
child.downcast_ref::<CastTo>()
} else {
None
}
}

View File

@ -0,0 +1,44 @@
use {
crossterm::event::{KeyEvent, MouseEvent},
std::fmt::{Display, Formatter, Result},
strum::EnumString,
};
#[derive(Debug, PartialEq, Eq, Clone, EnumString)]
pub enum Action {
Tick,
Render,
Resize(u16, u16),
Suspend,
Resume,
Quit,
Refresh,
Error(String),
Help,
Version,
AppAction(String),
Key(String),
}
impl Display for Action {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let enum_str = write!(f, "{:?}", self);
enum_str
}
}
#[derive(Clone, Debug)]
pub enum Event {
Init,
Quit,
Error,
Closed,
Tick,
Render,
FocusGained,
FocusLost,
Paste(String),
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
}

View File

@ -0,0 +1,217 @@
use {
super::events::Action,
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
eyre::Result,
std::{collections::HashMap, str::FromStr},
};
#[derive(Clone, Debug, Default)]
pub struct Config {
pub keybindings: KeyBindings,
}
#[derive(Clone, Debug, Default)]
pub struct KeyBindings(pub HashMap<Vec<KeyEvent>, Action>);
impl KeyBindings {
pub fn new(raw: HashMap<&str, String>) -> Self {
let keybindings = raw
.into_iter()
.map(|(key_str, cmd)| {
let action = Action::from_str(&cmd);
match action {
Ok(action) => (parse_key_sequence(&key_str).unwrap(), action),
Err(_) => (parse_key_sequence(&key_str).unwrap(), Action::AppAction(cmd)),
}
})
.collect();
KeyBindings(keybindings)
}
pub fn get(&self, key_events: &[KeyEvent]) -> Option<&Action> {
self.0.get(key_events)
}
}
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
let raw_lower = raw.to_ascii_lowercase();
let (remaining, modifiers) = extract_modifiers(&raw_lower);
parse_key_code_with_modifiers(remaining, modifiers)
}
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
let mut modifiers = KeyModifiers::empty();
let mut current = raw;
loop {
match current {
rest if rest.starts_with("ctrl-") => {
modifiers.insert(KeyModifiers::CONTROL);
current = &rest[5..];
}
rest if rest.starts_with("alt-") => {
modifiers.insert(KeyModifiers::ALT);
current = &rest[4..];
}
rest if rest.starts_with("shift-") => {
modifiers.insert(KeyModifiers::SHIFT);
current = &rest[6..];
}
_ => break, // break out of the loop if no known prefix is detected
};
}
(current, modifiers)
}
fn parse_key_code_with_modifiers(
raw: &str,
mut modifiers: KeyModifiers,
) -> Result<KeyEvent, String> {
let c = match raw {
"esc" => KeyCode::Esc,
"enter" => KeyCode::Enter,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"backtab" => {
modifiers.insert(KeyModifiers::SHIFT);
KeyCode::BackTab
}
"backspace" => KeyCode::Backspace,
"delete" => KeyCode::Delete,
"insert" => KeyCode::Insert,
"f1" => KeyCode::F(1),
"f2" => KeyCode::F(2),
"f3" => KeyCode::F(3),
"f4" => KeyCode::F(4),
"f5" => KeyCode::F(5),
"f6" => KeyCode::F(6),
"f7" => KeyCode::F(7),
"f8" => KeyCode::F(8),
"f9" => KeyCode::F(9),
"f10" => KeyCode::F(10),
"f11" => KeyCode::F(11),
"f12" => KeyCode::F(12),
"space" => KeyCode::Char(' '),
"hyphen" => KeyCode::Char('-'),
"minus" => KeyCode::Char('-'),
"tab" => KeyCode::Tab,
c if c.len() == 1 => {
let mut c = c.chars().next().unwrap();
if modifiers.contains(KeyModifiers::SHIFT) {
c = c.to_ascii_uppercase();
}
KeyCode::Char(c)
}
_ => return Err(format!("Unable to parse {raw}")),
};
Ok(KeyEvent::new(c, modifiers))
}
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
let char;
let key_code = match key_event.code {
KeyCode::Backspace => "backspace",
KeyCode::Enter => "enter",
KeyCode::Left => "left",
KeyCode::Right => "right",
KeyCode::Up => "up",
KeyCode::Down => "down",
KeyCode::Home => "home",
KeyCode::End => "end",
KeyCode::PageUp => "pageup",
KeyCode::PageDown => "pagedown",
KeyCode::Tab => "tab",
KeyCode::BackTab => "backtab",
KeyCode::Delete => "delete",
KeyCode::Insert => "insert",
KeyCode::F(c) => {
char = format!("f({c})");
&char
}
KeyCode::Char(c) if c == ' ' => "space",
KeyCode::Char(c) => {
char = c.to_string();
&char
}
KeyCode::Esc => "esc",
KeyCode::Null => "",
KeyCode::CapsLock => "",
KeyCode::Menu => "",
KeyCode::ScrollLock => "",
KeyCode::Media(_) => "",
KeyCode::NumLock => "",
KeyCode::PrintScreen => "",
KeyCode::Pause => "",
KeyCode::KeypadBegin => "",
KeyCode::Modifier(_) => "",
};
let mut modifiers = Vec::with_capacity(3);
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
modifiers.push("ctrl");
}
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
modifiers.push("shift");
}
if key_event.modifiers.intersects(KeyModifiers::ALT) {
modifiers.push("alt");
}
// if the modifiers is "shift" and the key code is a letter, we just return the letter
// otherwise we return the modifiers joined by a dash and the key code
if modifiers.len() == 1
&& key_code.chars().count() == 1
&& key_code.chars().all(char::is_alphabetic)
{
return key_code.to_string();
}
let mut key = modifiers.join("-");
if !key.is_empty() {
key.push('-');
}
key.push_str(key_code);
key
}
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
return Err(format!("Unable to parse `{}`", raw));
}
let raw = if !raw.contains("><") {
let raw = raw.strip_prefix('<').unwrap_or(raw);
let raw = raw.strip_prefix('>').unwrap_or(raw);
raw
} else {
raw
};
let sequences = raw
.split("><")
.map(|seq| {
if let Some(s) = seq.strip_prefix('<') {
s
} else if let Some(s) = seq.strip_suffix('>') {
s
} else {
seq
}
})
.collect::<Vec<_>>();
sequences.into_iter().map(parse_key_event).collect()
}

View File

@ -0,0 +1,233 @@
use {
super::events::Event,
crossterm::{
cursor,
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
Event as CrosstermEvent, KeyEventKind,
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
},
eyre::Result,
futures::{FutureExt, StreamExt},
ratatui::backend::CrosstermBackend as Backend,
std::{
ops::{Deref, DerefMut},
time::Duration,
},
tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
task::JoinHandle,
},
tokio_util::sync::CancellationToken,
};
pub use ratatui::prelude::{Color, Constraint, Direction, Layout, Rect, Size};
pub type IO = std::io::Stdout;
pub fn io() -> IO {
std::io::stdout()
}
pub type Frame<'a> = ratatui::Frame<'a>;
pub struct Tui {
pub terminal: ratatui::Terminal<Backend<IO>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
pub frame_rate: f64,
pub tick_rate: f64,
pub mouse: bool,
pub paste: bool,
}
impl Tui {
pub fn new() -> Result<Self> {
let tick_rate = 4.0;
let frame_rate = 60.0;
let terminal = ratatui::Terminal::new(Backend::new(io()))?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
let mouse = false;
let paste = false;
Ok(Self {
terminal,
task,
cancellation_token,
event_rx,
event_tx,
frame_rate,
tick_rate,
mouse,
paste,
})
}
pub fn tick_rate(mut self, tick_rate: f64) -> Self {
self.tick_rate = tick_rate;
self
}
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
self.frame_rate = frame_rate;
self
}
pub fn mouse(mut self, mouse: bool) -> Self {
self.mouse = mouse;
self
}
pub fn paste(mut self, paste: bool) -> Self {
self.paste = paste;
self
}
pub fn start(&mut self) {
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
self.cancel();
self.cancellation_token = CancellationToken::new();
let _cancellation_token = self.cancellation_token.clone();
let _event_tx = self.event_tx.clone();
self.task = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut tick_interval = tokio::time::interval(tick_delay);
let mut render_interval = tokio::time::interval(render_delay);
_event_tx.send(Event::Init).unwrap();
loop {
let tick_delay = tick_interval.tick();
let render_delay = render_interval.tick();
let crossterm_event = reader.next().fuse();
tokio::select! {
_ = _cancellation_token.cancelled() => {
break;
}
maybe_event = crossterm_event => {
match maybe_event {
Some(Ok(evt)) => {
match evt {
CrosstermEvent::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Event::Key(key)).unwrap();
}
},
CrosstermEvent::Mouse(mouse) => {
_event_tx.send(Event::Mouse(mouse)).unwrap();
},
CrosstermEvent::Resize(x, y) => {
_event_tx.send(Event::Resize(x, y)).unwrap();
},
CrosstermEvent::FocusLost => {
_event_tx.send(Event::FocusLost).unwrap();
},
CrosstermEvent::FocusGained => {
_event_tx.send(Event::FocusGained).unwrap();
},
CrosstermEvent::Paste(s) => {
_event_tx.send(Event::Paste(s)).unwrap();
},
}
}
Some(Err(_)) => {
_event_tx.send(Event::Error).unwrap();
}
None => {},
}
},
_ = tick_delay => {
_event_tx.send(Event::Tick).unwrap();
},
_ = render_delay => {
_event_tx.send(Event::Render).unwrap();
},
}
}
});
}
pub fn stop(&self) -> Result<()> {
self.cancel();
let mut counter = 0;
while !self.task.is_finished() {
std::thread::sleep(Duration::from_millis(1));
counter += 1;
if counter > 50 {
self.task.abort();
}
if counter > 100 {
eprintln!("Failed to abort task in 100 milliseconds for unknown reason");
break;
}
}
Ok(())
}
pub fn enter(&mut self) -> Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?;
if self.mouse {
crossterm::execute!(io(), EnableMouseCapture)?;
}
if self.paste {
crossterm::execute!(io(), EnableBracketedPaste)?;
}
self.start();
Ok(())
}
pub fn exit(&mut self) -> Result<()> {
self.stop()?;
if crossterm::terminal::is_raw_mode_enabled()? {
self.flush()?;
if self.paste {
crossterm::execute!(io(), DisableBracketedPaste)?;
}
if self.mouse {
crossterm::execute!(io(), DisableMouseCapture)?;
}
crossterm::execute!(io(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
}
Ok(())
}
pub fn cancel(&self) {
self.cancellation_token.cancel();
}
pub fn suspend(&mut self) -> Result<()> {
self.exit()
}
pub fn resume(&mut self) -> Result<()> {
self.enter()?;
Ok(())
}
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
}
}
impl Deref for Tui {
type Target = ratatui::Terminal<Backend<IO>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for Tui {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for Tui {
fn drop(&mut self) {
self.exit().unwrap();
}
}

46
lib/cli/tui/mod.rs Normal file
View File

@ -0,0 +1,46 @@
use {eyre::Result, palette::rgb::Rgb, ratatui::style::Color, std::str::FromStr};
pub mod framework {
pub mod app;
pub mod component;
pub mod events;
pub mod keyboard;
pub mod tui;
}
#[macro_export]
macro_rules! components {
( $( $x:expr $( => $t:ty )* ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push(
Box::new($x)
as Box<dyn tui::framework::component::Component $( $t + )* >
);
)*
temp_vec
}
};
}
#[macro_export]
macro_rules! children {
( $( $name:expr => $value:expr ),* ) => {
{
let mut map = std::collections::HashMap::new();
$(
map.insert(
$name.to_string(),
Box::new($value) as Box<dyn tui::framework::component::Component>
);
)*
map
}
};
}
pub fn rgb(hex: &str) -> Result<Color> {
let rgb: Rgb<u8, u8> = Rgb::from_str(hex)?;
Ok(Color::Rgb(rgb.red, rgb.green, rgb.blue))
}