feat(cli): ✨ tui framework
This commit is contained in:
parent
6702ba8240
commit
d621a0a657
18
.github/img/logo-cli-tui.svg
vendored
Normal file
18
.github/img/logo-cli-tui.svg
vendored
Normal 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
692
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
35
Cargo.toml
35
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]]
|
||||
|
||||
@ -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
24
lib/cli/tui/README.md
Normal 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.
|
||||
184
lib/cli/tui/framework/app.rs
Normal file
184
lib/cli/tui/framework/app.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
351
lib/cli/tui/framework/component.rs
Normal file
351
lib/cli/tui/framework/component.rs
Normal 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
|
||||
}
|
||||
}
|
||||
44
lib/cli/tui/framework/events.rs
Normal file
44
lib/cli/tui/framework/events.rs
Normal 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),
|
||||
}
|
||||
217
lib/cli/tui/framework/keyboard.rs
Normal file
217
lib/cli/tui/framework/keyboard.rs
Normal 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()
|
||||
}
|
||||
233
lib/cli/tui/framework/tui.rs
Normal file
233
lib/cli/tui/framework/tui.rs
Normal 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
46
lib/cli/tui/mod.rs
Normal 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))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user