feat: add tui
This commit is contained in:
361
Cargo.lock
generated
361
Cargo.lock
generated
@@ -11,6 +11,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
@@ -55,12 +61,41 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[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.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
@@ -82,6 +117,31 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"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 = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -92,6 +152,40 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -102,12 +196,24 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -124,6 +230,12 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
@@ -136,6 +248,15 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy-matcher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||
dependencies = [
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -158,12 +279,44 @@ dependencies = [
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.46.0"
|
||||
@@ -176,6 +329,28 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"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.17"
|
||||
@@ -245,6 +420,12 @@ version = "0.2.179"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
@@ -266,6 +447,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -288,6 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -301,6 +492,7 @@ dependencies = [
|
||||
"hex",
|
||||
"insta",
|
||||
"jiff",
|
||||
"nocontrol-tui",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -313,6 +505,19 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nocontrol-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossterm",
|
||||
"fuzzy-matcher",
|
||||
"nocontrol",
|
||||
"ratatui",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -351,6 +556,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -434,6 +645,27 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -460,6 +692,19 @@ version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
@@ -469,7 +714,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -479,6 +724,12 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -548,6 +799,27 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -580,6 +852,40 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[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.113"
|
||||
@@ -600,7 +906,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 1.1.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -749,6 +1055,35 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[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 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.19.0"
|
||||
@@ -833,6 +1168,28 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[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-link"
|
||||
version = "0.2.1"
|
||||
|
||||
21
crates/nocontrol-tui/Cargo.toml
Normal file
21
crates/nocontrol-tui/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "nocontrol-tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "nocontrol_tui"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
nocontrol = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
serde_json = "1.0"
|
||||
|
||||
# TUI dependencies
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
fuzzy-matcher = "0.3"
|
||||
647
crates/nocontrol-tui/src/lib.rs
Normal file
647
crates/nocontrol-tui/src/lib.rs
Normal file
@@ -0,0 +1,647 @@
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use nocontrol::{manifests::ManifestState, ControlPlane, Operator, Specification};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum InputMode {
|
||||
Normal,
|
||||
Search,
|
||||
}
|
||||
|
||||
struct App<TOperator: Operator> {
|
||||
control_plane: ControlPlane<TOperator>,
|
||||
manifests: Vec<ManifestState<TOperator::Specifications>>,
|
||||
filtered_indices: Vec<usize>,
|
||||
list_state: ListState,
|
||||
|
||||
input_mode: InputMode,
|
||||
command_input: String,
|
||||
search_query: String,
|
||||
|
||||
command_history: Vec<String>,
|
||||
history_index: Option<usize>,
|
||||
|
||||
messages: Vec<String>,
|
||||
should_quit: bool,
|
||||
|
||||
fuzzy_matcher: SkimMatcherV2,
|
||||
}
|
||||
|
||||
impl<TOperator: Operator> App<TOperator> {
|
||||
fn new(control_plane: ControlPlane<TOperator>) -> Self {
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select(Some(0));
|
||||
|
||||
Self {
|
||||
control_plane,
|
||||
manifests: Vec::new(),
|
||||
filtered_indices: Vec::new(),
|
||||
list_state,
|
||||
|
||||
input_mode: InputMode::Normal,
|
||||
command_input: String::new(),
|
||||
search_query: String::new(),
|
||||
|
||||
command_history: Vec::new(),
|
||||
history_index: None,
|
||||
|
||||
messages: vec![
|
||||
"Welcome to nocontrol TUI".to_string(),
|
||||
"Press / to search, type commands, ↑↓ for history/navigation".to_string(),
|
||||
],
|
||||
should_quit: false,
|
||||
|
||||
fuzzy_matcher: SkimMatcherV2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_manifests(&mut self) {
|
||||
if let Ok(manifests) = self.control_plane.get_manifests().await {
|
||||
self.manifests = manifests;
|
||||
self.update_filtered_list();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_filtered_list(&mut self) {
|
||||
if self.input_mode == InputMode::Search && !self.search_query.is_empty() {
|
||||
// Fuzzy search through manifest names and specs
|
||||
let mut scored_indices: Vec<(usize, i64)> = self
|
||||
.manifests
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, manifest)| {
|
||||
let search_text = format!(
|
||||
"{} {:?} {:?}",
|
||||
manifest.manifest.name,
|
||||
manifest.status.status,
|
||||
manifest.manifest.spec.kind()
|
||||
);
|
||||
|
||||
self.fuzzy_matcher
|
||||
.fuzzy_match(&search_text, &self.search_query)
|
||||
.map(|score| (idx, score))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by score (highest first)
|
||||
scored_indices.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
self.filtered_indices = scored_indices.into_iter().map(|(idx, _)| idx).collect();
|
||||
} else {
|
||||
// No filtering, show all manifests
|
||||
self.filtered_indices = (0..self.manifests.len()).collect();
|
||||
}
|
||||
|
||||
// Reset selection to first item if list changed
|
||||
if !self.filtered_indices.is_empty() {
|
||||
self.list_state.select(Some(0));
|
||||
} else {
|
||||
self.list_state.select(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_manifest(&self) -> Option<&ManifestState<TOperator::Specifications>> {
|
||||
self.list_state
|
||||
.selected()
|
||||
.and_then(|selected_idx| self.filtered_indices.get(selected_idx))
|
||||
.and_then(|&manifest_idx| self.manifests.get(manifest_idx))
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
if self.filtered_indices.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.filtered_indices.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
if self.filtered_indices.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = match self.list_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.filtered_indices.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.list_state.select(Some(i));
|
||||
}
|
||||
|
||||
fn history_previous(&mut self) {
|
||||
if self.command_history.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_index = match self.history_index {
|
||||
None => Some(self.command_history.len() - 1),
|
||||
Some(0) => Some(0), // Stay at oldest
|
||||
Some(i) => Some(i - 1),
|
||||
};
|
||||
|
||||
if let Some(idx) = new_index {
|
||||
self.history_index = Some(idx);
|
||||
self.command_input = self.command_history[idx].clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn history_next(&mut self) {
|
||||
if self.command_history.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_index = match self.history_index {
|
||||
None => None,
|
||||
Some(i) if i >= self.command_history.len() - 1 => {
|
||||
// Clear input when going past newest
|
||||
self.command_input.clear();
|
||||
None
|
||||
}
|
||||
Some(i) => Some(i + 1),
|
||||
};
|
||||
|
||||
self.history_index = new_index;
|
||||
if let Some(idx) = new_index {
|
||||
self.command_input = self.command_history[idx].clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_search_mode(&mut self) {
|
||||
self.input_mode = match self.input_mode {
|
||||
InputMode::Normal => {
|
||||
self.search_query.clear();
|
||||
InputMode::Search
|
||||
}
|
||||
InputMode::Search => {
|
||||
self.search_query.clear();
|
||||
self.update_filtered_list();
|
||||
InputMode::Normal
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn update_search(&mut self) {
|
||||
if self.input_mode == InputMode::Search {
|
||||
self.search_query = self.command_input.clone();
|
||||
self.update_filtered_list();
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_command(&mut self) {
|
||||
let cmd = self.command_input.trim().to_string();
|
||||
if cmd.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to history
|
||||
if self.command_history.last() != Some(&cmd) {
|
||||
self.command_history.push(cmd.clone());
|
||||
}
|
||||
self.history_index = None;
|
||||
|
||||
self.messages.push(format!("> {}", cmd));
|
||||
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
match parts.as_slice() {
|
||||
["get"] | ["list"] => {
|
||||
self.refresh_manifests().await;
|
||||
self.messages.push(format!(
|
||||
"Found {} manifest(s), showing {}",
|
||||
self.manifests.len(),
|
||||
self.filtered_indices.len()
|
||||
));
|
||||
}
|
||||
["describe"] => {
|
||||
if let Some(manifest) = self.get_selected_manifest() {
|
||||
let json = serde_json::to_string_pretty(manifest).unwrap_or_default();
|
||||
self.messages.push(json);
|
||||
} else {
|
||||
self.messages.push("No manifest selected".to_string());
|
||||
}
|
||||
}
|
||||
["quit"] | ["exit"] | ["q"] => {
|
||||
self.should_quit = true;
|
||||
}
|
||||
["clear"] => {
|
||||
self.messages.clear();
|
||||
self.messages.push("Screen cleared".to_string());
|
||||
}
|
||||
["history"] => {
|
||||
self.messages.push("Command history:".to_string());
|
||||
for (i, cmd) in self.command_history.iter().enumerate() {
|
||||
self.messages.push(format!(" {}: {}", i + 1, cmd));
|
||||
}
|
||||
}
|
||||
["help"] => {
|
||||
self.messages.push("Commands:".to_string());
|
||||
self.messages.push(" get, list - Refresh manifest list".to_string());
|
||||
self.messages.push(" describe - Show selected manifest details".to_string());
|
||||
self.messages.push(" history - Show command history".to_string());
|
||||
self.messages.push(" clear - Clear output messages".to_string());
|
||||
self.messages.push(" quit, exit, q - Exit application".to_string());
|
||||
self.messages.push(" help - Show this help".to_string());
|
||||
self.messages.push("".to_string());
|
||||
self.messages.push("Keys:".to_string());
|
||||
self.messages.push(" / - Toggle search mode".to_string());
|
||||
self.messages.push(" ↑/↓ - Navigate list OR cycle command history".to_string());
|
||||
self.messages.push(" Enter - Execute command (or exit search)".to_string());
|
||||
self.messages.push(" Esc - Clear input / exit search".to_string());
|
||||
self.messages.push(" q - Quick quit (when input empty)".to_string());
|
||||
}
|
||||
_ => {
|
||||
self.messages.push(format!("Unknown command: {}. Type 'help' for commands.", cmd));
|
||||
}
|
||||
}
|
||||
|
||||
self.command_input.clear();
|
||||
|
||||
// Keep only last 100 messages
|
||||
if self.messages.len() > 100 {
|
||||
self.messages.drain(0..self.messages.len() - 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_app<B: Backend, TOperator: Operator + Send + Sync + 'static>(
|
||||
terminal: &mut Terminal<B>,
|
||||
app: Arc<Mutex<App<TOperator>>>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
TOperator::Specifications: Send + Sync,
|
||||
{
|
||||
// Spawn refresh task
|
||||
let app_clone = app.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
app_clone.lock().await.refresh_manifests().await;
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
// Draw UI
|
||||
{
|
||||
let mut app = app.lock().await;
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle input with timeout
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
let mut app = app.lock().await;
|
||||
|
||||
match (key.code, key.modifiers, app.input_mode) {
|
||||
// Quick quit with 'q' when input is empty
|
||||
(KeyCode::Char('q'), KeyModifiers::NONE, InputMode::Normal)
|
||||
if app.command_input.is_empty() => {
|
||||
app.should_quit = true;
|
||||
}
|
||||
|
||||
// Toggle search mode with '/'
|
||||
(KeyCode::Char('/'), KeyModifiers::NONE, _) => {
|
||||
app.toggle_search_mode();
|
||||
}
|
||||
|
||||
// Character input
|
||||
(KeyCode::Char(c), _, _) => {
|
||||
app.command_input.push(c);
|
||||
app.update_search();
|
||||
}
|
||||
|
||||
// Backspace
|
||||
(KeyCode::Backspace, _, _) => {
|
||||
app.command_input.pop();
|
||||
app.update_search();
|
||||
}
|
||||
|
||||
// Enter key
|
||||
(KeyCode::Enter, _, InputMode::Search) => {
|
||||
// Exit search mode but keep filter
|
||||
app.input_mode = InputMode::Normal;
|
||||
app.command_input.clear();
|
||||
}
|
||||
(KeyCode::Enter, _, InputMode::Normal) => {
|
||||
app.execute_command().await;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
(KeyCode::Up, _, InputMode::Normal) if !app.command_input.is_empty() => {
|
||||
// Navigate history when typing a command
|
||||
app.history_previous();
|
||||
}
|
||||
(KeyCode::Down, _, InputMode::Normal) if !app.command_input.is_empty() => {
|
||||
// Navigate history when typing a command
|
||||
app.history_next();
|
||||
}
|
||||
(KeyCode::Up, _, _) => {
|
||||
// Navigate manifest list
|
||||
app.previous();
|
||||
}
|
||||
(KeyCode::Down, _, _) => {
|
||||
// Navigate manifest list
|
||||
app.next();
|
||||
}
|
||||
|
||||
// Escape key
|
||||
(KeyCode::Esc, _, InputMode::Search) => {
|
||||
app.toggle_search_mode();
|
||||
app.command_input.clear();
|
||||
}
|
||||
(KeyCode::Esc, _, InputMode::Normal) => {
|
||||
app.command_input.clear();
|
||||
app.history_index = None;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<TOperator: Operator>(f: &mut Frame, app: &mut App<TOperator>) {
|
||||
// Create layout
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Title
|
||||
Constraint::Min(10), // Main content
|
||||
Constraint::Length(10), // Messages
|
||||
Constraint::Length(3), // Command input
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Title
|
||||
let mode_text = match app.input_mode {
|
||||
InputMode::Normal => "NORMAL",
|
||||
InputMode::Search => "SEARCH",
|
||||
};
|
||||
let title = Paragraph::new(vec![
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
"NoControl - Kubernetes-like Control Plane",
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("[{}]", mode_text),
|
||||
Style::default()
|
||||
.fg(if app.input_mode == InputMode::Search { Color::Yellow } else { Color::Green })
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]),
|
||||
])
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
// Main content area - split into list and details
|
||||
let main_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
|
||||
.split(chunks[1]);
|
||||
|
||||
// Manifest list
|
||||
render_manifest_list(f, app, main_chunks[0]);
|
||||
|
||||
// Manifest details
|
||||
render_manifest_details(f, app, main_chunks[1]);
|
||||
|
||||
// Messages area
|
||||
render_messages(f, app, chunks[2]);
|
||||
|
||||
// Command input
|
||||
render_command_input(f, app, chunks[3]);
|
||||
}
|
||||
|
||||
fn render_manifest_list<TOperator: Operator>(f: &mut Frame, app: &mut App<TOperator>, area: Rect) {
|
||||
// Collect filtered manifests data before borrowing list_state
|
||||
let filtered_data: Vec<_> = app.filtered_indices
|
||||
.iter()
|
||||
.filter_map(|&idx| app.manifests.get(idx))
|
||||
.map(|m| (m.manifest.name.clone(), m.status.status.clone()))
|
||||
.collect();
|
||||
|
||||
let items: Vec<ListItem> = filtered_data
|
||||
.iter()
|
||||
.map(|(name, status)| {
|
||||
let status_color = match status {
|
||||
nocontrol::manifests::ManifestStatusState::Running => Color::Green,
|
||||
nocontrol::manifests::ManifestStatusState::Started => Color::Yellow,
|
||||
nocontrol::manifests::ManifestStatusState::Pending => Color::Gray,
|
||||
nocontrol::manifests::ManifestStatusState::Stopping => Color::Magenta,
|
||||
nocontrol::manifests::ManifestStatusState::Deleting => Color::Red,
|
||||
};
|
||||
|
||||
let status_text = format!("{:?}", status);
|
||||
let content = Line::from(vec![
|
||||
Span::styled(
|
||||
"● ",
|
||||
Style::default().fg(status_color),
|
||||
),
|
||||
Span::raw(name),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("[{}]", status_text),
|
||||
Style::default().fg(status_color),
|
||||
),
|
||||
]);
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let title = if app.input_mode == InputMode::Search && !app.search_query.is_empty() {
|
||||
format!(
|
||||
" Manifests ({}/{}) - Filtered ",
|
||||
filtered_data.len(),
|
||||
app.manifests.len()
|
||||
)
|
||||
} else {
|
||||
format!(" Manifests ({}) ", app.manifests.len())
|
||||
};
|
||||
|
||||
let list = List::new(items)
|
||||
.block(Block::default().title(title).borders(Borders::ALL))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::DarkGray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
f.render_stateful_widget(list, area, &mut app.list_state);
|
||||
}
|
||||
|
||||
fn render_manifest_details<TOperator: Operator>(f: &mut Frame, app: &App<TOperator>, area: Rect) {
|
||||
let content = if let Some(manifest) = app.get_selected_manifest() {
|
||||
let mut lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled("Name: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(&manifest.manifest.name),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Kind: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(manifest.manifest.spec.kind()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Status: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(format!("{:?}", manifest.status.status)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Generation: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(format!("{}", manifest.generation)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Created: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(format!("{}", manifest.created)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Updated: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(format!("{}", manifest.updated)),
|
||||
]),
|
||||
];
|
||||
|
||||
if !manifest.status.events.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled("Events:", Style::default().add_modifier(Modifier::BOLD))));
|
||||
for event in manifest.status.events.iter().rev().take(5) {
|
||||
lines.push(Line::from(format!(" • {}", event.message)));
|
||||
}
|
||||
}
|
||||
|
||||
if !manifest.status.changes.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled("Changes:", Style::default().add_modifier(Modifier::BOLD))));
|
||||
for change in manifest.status.changes.iter().rev().take(5) {
|
||||
lines.push(Line::from(format!(
|
||||
" • {:?} at {}",
|
||||
change.event, change.created
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Text::from(lines)
|
||||
} else {
|
||||
Text::from("No manifest selected")
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(content)
|
||||
.block(Block::default().title(" Details ").borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_messages<TOperator: Operator>(f: &mut Frame, app: &App<TOperator>, area: Rect) {
|
||||
let messages: Vec<Line> = app
|
||||
.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.take(8)
|
||||
.rev()
|
||||
.map(|m| Line::from(m.clone()))
|
||||
.collect();
|
||||
|
||||
let paragraph = Paragraph::new(messages)
|
||||
.block(Block::default().title(" Output ").borders(Borders::ALL))
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_command_input<TOperator: Operator>(f: &mut Frame, app: &App<TOperator>, area: Rect) {
|
||||
let (title, input_text, style) = match app.input_mode {
|
||||
InputMode::Normal => {
|
||||
let hist_info = if let Some(idx) = app.history_index {
|
||||
format!(" [History {}/{}]", idx + 1, app.command_history.len())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
(
|
||||
format!(" Command{} (/ to search, ↑↓ for history, 'help' for commands) ", hist_info),
|
||||
app.command_input.as_str(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
)
|
||||
}
|
||||
InputMode::Search => (
|
||||
" Search (fuzzy) - Enter to apply, Esc to cancel ".to_string(),
|
||||
app.command_input.as_str(),
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
};
|
||||
|
||||
let input = Paragraph::new(input_text)
|
||||
.style(style)
|
||||
.block(Block::default().title(title).borders(Borders::ALL));
|
||||
|
||||
f.render_widget(input, area);
|
||||
}
|
||||
|
||||
/// Run the TUI with the given control plane
|
||||
pub async fn run<TOperator>(control_plane: ControlPlane<TOperator>) -> anyhow::Result<()>
|
||||
where
|
||||
TOperator: Operator + Send + Sync + 'static,
|
||||
TOperator::Specifications: Send + Sync,
|
||||
{
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Create app state
|
||||
let app = Arc::new(Mutex::new(App::new(control_plane)));
|
||||
|
||||
// Run TUI
|
||||
let res = run_app(&mut terminal, app).await;
|
||||
|
||||
// Restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
res
|
||||
}
|
||||
@@ -18,6 +18,7 @@ tracing.workspace = true
|
||||
uuid = { version = "1.19.0", features = ["serde", "v4", "v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
nocontrol-tui = { path = "../nocontrol-tui" }
|
||||
insta = "1.46.0"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
tracing-test = { version = "0.2.5", features = ["no-env-filter"] }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::io::{BufRead, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nocontrol::{
|
||||
@@ -10,10 +10,10 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Setup logging to file
|
||||
let output_file = std::fs::File::create("target/nocontrol.log")?;
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
// .pretty()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(output_file)
|
||||
.with_file(false)
|
||||
@@ -23,109 +23,57 @@ async fn main() -> anyhow::Result<()> {
|
||||
.init();
|
||||
|
||||
let operator = MyOperator {};
|
||||
let control_plane = nocontrol::ControlPlane::new(operator);
|
||||
|
||||
let mut control_plane = nocontrol::ControlPlane::new(operator);
|
||||
// control_plane.with_deadline(std::time::Duration::from_secs(4));
|
||||
// Add initial manifest
|
||||
control_plane
|
||||
.add_manifest(Manifest {
|
||||
name: "initial-deployment".into(),
|
||||
metadata: ManifestMetadata {},
|
||||
spec: Specifications::Deployment(DeploymentControllerManifest {
|
||||
name: "initial-app".into(),
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Spawn random manifest updater
|
||||
tokio::spawn({
|
||||
let control_plane = control_plane.clone();
|
||||
async move {
|
||||
control_plane
|
||||
.add_manifest(Manifest {
|
||||
name: "some-manifest".into(),
|
||||
metadata: ManifestMetadata {},
|
||||
spec: Specifications::Deployment(DeploymentControllerManifest {
|
||||
name: "some-name".into(),
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let rand = {
|
||||
use rand::prelude::*;
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
rng.random_range(2..5)
|
||||
rng.random_range(3..8)
|
||||
};
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(rand)).await;
|
||||
tokio::time::sleep(Duration::from_secs(rand)).await;
|
||||
|
||||
let random = uuid::Uuid::now_v7();
|
||||
|
||||
control_plane
|
||||
let _ = control_plane
|
||||
.add_manifest(Manifest {
|
||||
name: "some-manifest".into(),
|
||||
name: "initial-deployment".into(),
|
||||
metadata: ManifestMetadata {},
|
||||
spec: Specifications::Deployment(DeploymentControllerManifest {
|
||||
name: format!("some-changed-name: {}", random),
|
||||
name: format!("app-{}", &random.to_string()[..8]),
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Debugging shell
|
||||
// Spawn control plane
|
||||
tokio::spawn({
|
||||
let control_plane = control_plane.clone();
|
||||
|
||||
async move {
|
||||
let ui = Ui {};
|
||||
|
||||
loop {
|
||||
ui.write("> ");
|
||||
let cmd = ui.read_line();
|
||||
|
||||
let items = cmd.split(" ").map(|t| t.to_string()).collect::<Vec<_>>();
|
||||
|
||||
let (command, args) = match &items[..] {
|
||||
[first, rest @ ..] => (first, rest.to_vec()),
|
||||
//[first] => (first, vec![]),
|
||||
_ => {
|
||||
ui.writeln("invalid command");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match (command.as_str(), args.as_slice()) {
|
||||
("get", _) => {
|
||||
// get all for now
|
||||
let manifests = control_plane
|
||||
.get_manifests()
|
||||
.await
|
||||
.inspect_err(|e| ui.writeln(format!("get failed: {e:#}")))
|
||||
.unwrap();
|
||||
|
||||
ui.writeln("listing manifests");
|
||||
|
||||
for manifest in manifests {
|
||||
ui.writeln(format!(" - {}", manifest.manifest.name));
|
||||
}
|
||||
}
|
||||
("describe", [manifest_name, ..]) => {
|
||||
let manifests = control_plane
|
||||
.get_manifests()
|
||||
.await
|
||||
.inspect_err(|e| ui.writeln(format!("get failed: {e:#}")))
|
||||
.unwrap();
|
||||
|
||||
if let Some(manifest) =
|
||||
manifests.iter().find(|m| &m.manifest.name == manifest_name)
|
||||
{
|
||||
let output = serde_json::to_string_pretty(&manifest).unwrap();
|
||||
ui.writeln(output);
|
||||
}
|
||||
}
|
||||
(cmd, _) => ui.writeln(format!("command is not implemented: {}", cmd)),
|
||||
}
|
||||
|
||||
ui.writeln("");
|
||||
}
|
||||
let _ = control_plane.execute().await;
|
||||
}
|
||||
});
|
||||
|
||||
control_plane.execute().await?;
|
||||
// Run TUI
|
||||
nocontrol_tui::run(control_plane).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -180,29 +128,3 @@ impl Specification for Specifications {
|
||||
pub struct DeploymentControllerManifest {
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub struct Ui {}
|
||||
|
||||
impl Ui {
|
||||
pub fn write(&self, msg: &str) {
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
stderr.write_all(msg.as_bytes()).unwrap();
|
||||
stderr.flush().unwrap()
|
||||
}
|
||||
pub fn writeln(&self, msg: impl AsRef<str>) {
|
||||
let msg = msg.as_ref();
|
||||
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
stderr.write_all(msg.as_bytes()).unwrap();
|
||||
writeln!(stderr).unwrap();
|
||||
stderr.flush().unwrap()
|
||||
}
|
||||
pub fn read_line(&self) -> String {
|
||||
let mut stdin = std::io::stdin().lock();
|
||||
let mut output = String::new();
|
||||
|
||||
stdin.read_line(&mut output).unwrap();
|
||||
|
||||
output.trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ impl<T: Specification> BackingStore<T> {
|
||||
events: Vec::default(),
|
||||
changes: vec![ManifestChangeEvent {
|
||||
created: now,
|
||||
handled: false,
|
||||
event: ManifestChangeEventType::Changed,
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -60,11 +60,22 @@ impl<T: Operator> Reconciler<T> {
|
||||
}
|
||||
|
||||
// 4. Check desired vs actual
|
||||
for manifest in our_manifests.iter_mut() {
|
||||
'manifest: for manifest in our_manifests.iter_mut() {
|
||||
// Currently periodic sync,
|
||||
// TODO: this should also be made event based
|
||||
|
||||
if let Some(change) = manifest.status.changes.first() {
|
||||
if change.handled {
|
||||
continue 'manifest;
|
||||
}
|
||||
}
|
||||
|
||||
self.operator.reconcile(manifest).await?;
|
||||
self.store.update_state(manifest).await?;
|
||||
|
||||
if let Some(change) = manifest.status.changes.first_mut() {
|
||||
change.handled = true
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
@@ -58,6 +58,7 @@ pub enum ManifestStatusState {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ManifestChangeEvent {
|
||||
pub created: jiff::Timestamp,
|
||||
pub handled: bool,
|
||||
pub event: ManifestChangeEventType,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user