Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24fe43b6d5 |
Generated
+287
-25
@@ -26,9 +26,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@@ -607,9 +607,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.73"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
@@ -665,6 +665,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "better-panic"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fa9e1d11a268684cbd90ed36370d7577afb6c62d912ddff5c15fc34343e5036"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"console",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
version = "0.1.4"
|
||||
@@ -943,6 +953,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
@@ -955,6 +971,15 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
|
||||
|
||||
[[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.1.18"
|
||||
@@ -1158,6 +1183,33 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
@@ -1193,7 +1245,22 @@ dependencies = [
|
||||
"crossterm 0.27.0",
|
||||
"strum 0.26.3",
|
||||
"strum_macros 0.26.4",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
|
||||
dependencies = [
|
||||
"castaway 0.2.3",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1214,7 +1281,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -1634,6 +1701,23 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"mio 1.0.1",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
@@ -1915,12 +1999,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.9"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core 0.20.9",
|
||||
"darling_macro 0.20.9",
|
||||
"darling_core 0.20.10",
|
||||
"darling_macro 0.20.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1939,9 +2023,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.9"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
@@ -1964,11 +2048,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.9"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core 0.20.9",
|
||||
"darling_core 0.20.10",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
@@ -2634,6 +2718,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -2798,6 +2888,15 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gcc"
|
||||
version = "0.3.55"
|
||||
@@ -2875,9 +2974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
@@ -3028,6 +3127,17 @@ dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.8.4"
|
||||
@@ -3512,9 +3622,15 @@ dependencies = [
|
||||
"instant",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
|
||||
[[package]]
|
||||
name = "inlinable_string"
|
||||
version = "0.1.15"
|
||||
@@ -3564,7 +3680,21 @@ dependencies = [
|
||||
"newline-converter",
|
||||
"thiserror",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e"
|
||||
dependencies = [
|
||||
"darling 0.20.10",
|
||||
"indoc",
|
||||
"pretty_assertions",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3642,7 +3772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"castaway",
|
||||
"castaway 0.1.2",
|
||||
"crossbeam-utils",
|
||||
"curl",
|
||||
"curl-sys",
|
||||
@@ -3914,6 +4044,15 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macro_rules_attribute"
|
||||
version = "0.1.3"
|
||||
@@ -4099,6 +4238,7 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -6674,6 +6814,27 @@ dependencies = [
|
||||
"wasm-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-tui-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"better-panic",
|
||||
"color-eyre",
|
||||
"crossterm 0.28.1",
|
||||
"humantime-serde",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tui-logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-tun"
|
||||
version = "0.1.0"
|
||||
@@ -6961,9 +7122,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.0"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -7145,6 +7306,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.23.0"
|
||||
@@ -7804,6 +7971,28 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm 0.28.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru",
|
||||
"paste",
|
||||
"serde",
|
||||
"strum 0.26.3",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.2.0"
|
||||
@@ -8798,7 +8987,7 @@ version = "3.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
|
||||
dependencies = [
|
||||
"darling 0.20.9",
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
@@ -8911,12 +9100,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio 1.0.1",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
@@ -9347,6 +9537,15 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strip-ansi-escapes"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa"
|
||||
dependencies = [
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@@ -10174,6 +10373,16 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-futures"
|
||||
version = "0.2.5"
|
||||
@@ -10355,6 +10564,22 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-logger"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdf8f5ebd2c83a5176c69b150ea7f2a855ec8dc2a2774e7f198d1b50feda5745"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fxhash",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"ratatui",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.20.1"
|
||||
@@ -10480,12 +10705,29 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.4"
|
||||
@@ -10817,6 +11059,26 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
"vte_generate_state_changes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte_generate_state_changes"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.2.0"
|
||||
|
||||
+10
-1
@@ -149,7 +149,7 @@ members = [
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract", "common/nym-tui-common",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -412,6 +412,15 @@ wasm-bindgen-futures = "0.4.45"
|
||||
wasmtimer = "0.2.0"
|
||||
web-sys = "0.3.72"
|
||||
|
||||
# tui related
|
||||
better-panic = "0.3.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
ratatui = "0.29.0"
|
||||
tui-logger = "0.14.0"
|
||||
|
||||
|
||||
# Profile settings for individual crates
|
||||
|
||||
# Compile-time verified queries do quite a bit of work at compile time. Incremental
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "nym-tui-common"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "rt-multi-thread", "signal", "macros"] }
|
||||
tokio-stream = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# tui
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
ratatui = { workspace = true, features = ["serde", "macros", "crossterm"] }
|
||||
tui-logger = { workspace = true, optional = true, features = ["tracing-support"], default-features = false }
|
||||
|
||||
# panic handlers
|
||||
better-panic = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
strip-ansi-escapes = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["logger"]
|
||||
|
||||
|
||||
|
||||
|
||||
[features]
|
||||
logger = ["tui-logger"]
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use color_eyre::eyre;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use nym_tui_common::tui::config::keybindings::{KeyBinding, LoggerKeybindings};
|
||||
use nym_tui_common::{
|
||||
run_tui, Action, ActionDispatcher, ActionSender, AppAction, Component, DebugHistory, Logger,
|
||||
LoggerProps, State,
|
||||
};
|
||||
use ratatui::layout::{Layout, Rect};
|
||||
use ratatui::prelude::Constraint;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
use tracing::log::LevelFilter;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
// useful pattern for tabs, etc:
|
||||
/*
|
||||
fn get_active_page_component_mut(&mut self) -> &mut dyn Component {
|
||||
match self.props.active_tab {
|
||||
ActiveTab::Tab1 => &mut self.tab1,
|
||||
ActiveTab::Tab2 => &mut self.tab2,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
struct Props {
|
||||
custom_quit: KeyBinding,
|
||||
}
|
||||
|
||||
impl From<&HelloStore> for Props {
|
||||
fn from(store: &HelloStore) -> Self {
|
||||
Props {
|
||||
custom_quit: store.config.custom_quit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HelloRootApp {
|
||||
props: Props,
|
||||
|
||||
action_sender: ActionSender<HelloActions>,
|
||||
logger: Logger<HelloStore, HelloActions>,
|
||||
debug_history: DebugHistory<HelloStore, HelloActions>,
|
||||
}
|
||||
|
||||
impl Component for HelloRootApp {
|
||||
type State = HelloStore;
|
||||
type Actions = HelloActions;
|
||||
|
||||
fn new(state: &HelloStore, action_sender: ActionSender<HelloActions>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
HelloRootApp {
|
||||
props: Props::from(state),
|
||||
action_sender: action_sender.clone(),
|
||||
logger: Logger::new(state, action_sender.clone()),
|
||||
debug_history: DebugHistory::new(state, action_sender),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(self, state: &Self::State) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
HelloRootApp {
|
||||
logger: self.logger.update(state),
|
||||
debug_history: self.debug_history.update(state),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self) -> bool {
|
||||
let logger_tick = self.logger.tick();
|
||||
let debug_history_tick = self.debug_history.tick();
|
||||
|
||||
logger_tick || debug_history_tick
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> eyre::Result<()> {
|
||||
let maybe_binding = KeyBinding::from(key);
|
||||
if maybe_binding == self.props.custom_quit {
|
||||
self.action_sender.send(Action::Quit);
|
||||
}
|
||||
|
||||
self.logger.handle_key(key)?;
|
||||
self.debug_history.handle_key(key)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn view(&mut self, frame: &mut Frame, rect: Rect) {
|
||||
let [logs, hello_rect] =
|
||||
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(rect);
|
||||
|
||||
self.logger.view(frame, logs);
|
||||
|
||||
frame.render_widget(Paragraph::new("Hello world!").centered(), hello_rect);
|
||||
|
||||
self.debug_history.view(frame, rect);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HelloActions {}
|
||||
|
||||
impl AppAction for HelloActions {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HelloConfig {
|
||||
pub custom_quit: KeyBinding,
|
||||
pub logger_keybindings: LoggerKeybindings,
|
||||
}
|
||||
|
||||
impl Default for HelloConfig {
|
||||
fn default() -> Self {
|
||||
HelloConfig {
|
||||
custom_quit: KeyBinding::new(KeyCode::Char('x')),
|
||||
logger_keybindings: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HelloStore {
|
||||
config: HelloConfig,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a HelloStore> for LoggerProps {
|
||||
fn from(store: &'a HelloStore) -> LoggerProps {
|
||||
LoggerProps {
|
||||
keybindings: store.config.logger_keybindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State for HelloStore {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HelloDispatcher {}
|
||||
|
||||
#[async_trait]
|
||||
impl ActionDispatcher for HelloDispatcher {
|
||||
type Store = HelloStore;
|
||||
type Actions = HelloActions;
|
||||
|
||||
async fn handle_app_action(
|
||||
&mut self,
|
||||
action: Self::Actions,
|
||||
store: &mut Self::Store,
|
||||
) -> eyre::Result<()> {
|
||||
let _ = action;
|
||||
let _ = store;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
let filter: EnvFilter = "trace,mio=warn".parse()?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tui_logger::tracing_subscriber_layer().with_filter(filter))
|
||||
.init();
|
||||
tui_logger::init_logger(LevelFilter::Trace)?;
|
||||
|
||||
run_tui::<HelloRootApp, _>(Default::default(), HelloDispatcher {}, Default::default()).await
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::char::ParseCharError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NymTuiError {
|
||||
#[error("failed to abort tui processing task within specified duration")]
|
||||
TuiTaskAbortFailure,
|
||||
|
||||
#[error("{str} could not be parsed into a character: {source}")]
|
||||
InvalidCharacter {
|
||||
str: String,
|
||||
#[source]
|
||||
source: ParseCharError,
|
||||
},
|
||||
|
||||
#[error("could not process an unknown keybinding: '{value}'")]
|
||||
UnknownKeyBinding { value: String },
|
||||
|
||||
#[error("could not process an unknown key modifier: '{value}'")]
|
||||
UnknownKeyModifier { value: String },
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![warn(clippy::todo)]
|
||||
#![warn(clippy::dbg_macro)]
|
||||
#![warn(clippy::panic)]
|
||||
|
||||
use crate::tui::manager::TuiManager;
|
||||
use color_eyre::eyre;
|
||||
|
||||
pub mod error;
|
||||
pub mod tui;
|
||||
|
||||
pub use crate::tui::config::TuiConfig;
|
||||
pub use tui::action::{Action, AppAction};
|
||||
pub use tui::dispatcher::store::State;
|
||||
pub use tui::dispatcher::{ActionDispatcher, ActionSender};
|
||||
pub use tui::initialize_panic_handler;
|
||||
pub use tui::ui::components::Component;
|
||||
|
||||
// components:
|
||||
pub use tui::ui::components::common::DebugHistory;
|
||||
#[cfg(feature = "logger")]
|
||||
pub use tui::ui::components::common::{Logger, LoggerProps};
|
||||
|
||||
pub async fn run_tui<C, D>(
|
||||
config: TuiConfig,
|
||||
action_dispatcher: D,
|
||||
initial_state: D::Store,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
C: Component + Send + Sync + 'static,
|
||||
C::State: Send + Sync + 'static,
|
||||
C::Actions: Send + Sync + Clone + 'static,
|
||||
D: ActionDispatcher<Store = C::State, Actions = C::Actions> + Send + Sync + 'static,
|
||||
{
|
||||
initialize_panic_handler()?;
|
||||
|
||||
TuiManager::<C>::build_new(config, action_dispatcher, initial_state)?
|
||||
.wait_for_exit_or_signal()
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Action<T: AppAction> {
|
||||
Quit,
|
||||
|
||||
AppDefined(T),
|
||||
}
|
||||
|
||||
pub trait AppAction: Debug {}
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::error::NymTuiError;
|
||||
#[cfg(feature = "logger")]
|
||||
use tui_logger::TuiWidgetEvent;
|
||||
|
||||
static KEY_MODIFIERS: LazyLock<HashMap<&'static str, KeyModifiers>> = LazyLock::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("shift", KeyModifiers::SHIFT);
|
||||
m.insert("ctrl", KeyModifiers::CONTROL);
|
||||
m.insert("alt", KeyModifiers::ALT);
|
||||
m.insert("super", KeyModifiers::SUPER);
|
||||
m.insert("hyper", KeyModifiers::HYPER);
|
||||
m.insert("meta", KeyModifiers::META);
|
||||
m
|
||||
});
|
||||
|
||||
static SPECIAL_KEYS: LazyLock<HashMap<&'static str, KeyCode>> = LazyLock::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("backspace", KeyCode::Backspace);
|
||||
m.insert("enter", KeyCode::Enter);
|
||||
m.insert("left", KeyCode::Left);
|
||||
m.insert("right", KeyCode::Right);
|
||||
m.insert("up", KeyCode::Up);
|
||||
m.insert("down", KeyCode::Down);
|
||||
m.insert("home", KeyCode::Home);
|
||||
m.insert("end", KeyCode::End);
|
||||
m.insert("pageup", KeyCode::PageUp);
|
||||
m.insert("pagedown", KeyCode::PageDown);
|
||||
m.insert("tab", KeyCode::Tab);
|
||||
m.insert("backtab", KeyCode::BackTab);
|
||||
m.insert("delete", KeyCode::Delete);
|
||||
m.insert("insert", KeyCode::Insert);
|
||||
m.insert("null", KeyCode::Null);
|
||||
m.insert("esc", KeyCode::Esc);
|
||||
m.insert("space", KeyCode::Char(' '));
|
||||
m.insert("f1", KeyCode::F(1));
|
||||
m.insert("f2", KeyCode::F(2));
|
||||
m.insert("f3", KeyCode::F(3));
|
||||
m.insert("f4", KeyCode::F(4));
|
||||
m.insert("f5", KeyCode::F(5));
|
||||
m.insert("f6", KeyCode::F(6));
|
||||
m.insert("f7", KeyCode::F(7));
|
||||
m.insert("f8", KeyCode::F(8));
|
||||
m.insert("f9", KeyCode::F(9));
|
||||
m.insert("f10", KeyCode::F(10));
|
||||
m.insert("f11", KeyCode::F(11));
|
||||
m.insert("f12", KeyCode::F(12));
|
||||
m
|
||||
});
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
|
||||
pub struct LoggerKeybindings {
|
||||
// TODO: give them better names
|
||||
tui_logger_space_key: KeyBinding,
|
||||
tui_logger_up_key: KeyBinding,
|
||||
tui_logger_down_key: KeyBinding,
|
||||
tui_logger_left_key: KeyBinding,
|
||||
tui_logger_right_key: KeyBinding,
|
||||
tui_logger_plus_key: KeyBinding,
|
||||
tui_logger_minus_key: KeyBinding,
|
||||
tui_logger_hide_key: KeyBinding,
|
||||
tui_logger_focus_key: KeyBinding,
|
||||
tui_logger_prev_page_key: KeyBinding,
|
||||
tui_logger_next_page_key: KeyBinding,
|
||||
tui_logger_escape_key: KeyBinding,
|
||||
}
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
impl Default for LoggerKeybindings {
|
||||
fn default() -> Self {
|
||||
LoggerKeybindings {
|
||||
tui_logger_space_key: KeyBinding::new(KeyCode::Char(' ')),
|
||||
tui_logger_up_key: KeyBinding::new(KeyCode::Up),
|
||||
tui_logger_down_key: KeyBinding::new(KeyCode::Down),
|
||||
tui_logger_left_key: KeyBinding::new(KeyCode::Left),
|
||||
tui_logger_right_key: KeyBinding::new(KeyCode::Right),
|
||||
tui_logger_plus_key: KeyBinding::new(KeyCode::Char('+')),
|
||||
tui_logger_minus_key: KeyBinding::new(KeyCode::Char('-')),
|
||||
tui_logger_hide_key: KeyBinding::new(KeyCode::Char('h')),
|
||||
tui_logger_focus_key: KeyBinding::new(KeyCode::Char('f')),
|
||||
tui_logger_prev_page_key: KeyBinding::new(KeyCode::PageUp),
|
||||
tui_logger_next_page_key: KeyBinding::new(KeyCode::PageDown),
|
||||
tui_logger_escape_key: KeyBinding::new(KeyCode::Esc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
impl LoggerKeybindings {
|
||||
pub fn tui_logger_event(&self, key: KeyBinding) -> Option<TuiWidgetEvent> {
|
||||
if key == self.tui_logger_space_key {
|
||||
Some(TuiWidgetEvent::SpaceKey)
|
||||
} else if key == self.tui_logger_up_key {
|
||||
Some(TuiWidgetEvent::UpKey)
|
||||
} else if key == self.tui_logger_down_key {
|
||||
Some(TuiWidgetEvent::DownKey)
|
||||
} else if key == self.tui_logger_left_key {
|
||||
Some(TuiWidgetEvent::LeftKey)
|
||||
} else if key == self.tui_logger_right_key {
|
||||
Some(TuiWidgetEvent::RightKey)
|
||||
} else if key == self.tui_logger_plus_key {
|
||||
Some(TuiWidgetEvent::PlusKey)
|
||||
} else if key == self.tui_logger_minus_key {
|
||||
Some(TuiWidgetEvent::MinusKey)
|
||||
} else if key == self.tui_logger_hide_key {
|
||||
Some(TuiWidgetEvent::HideKey)
|
||||
} else if key == self.tui_logger_focus_key {
|
||||
Some(TuiWidgetEvent::FocusKey)
|
||||
} else if key == self.tui_logger_prev_page_key {
|
||||
Some(TuiWidgetEvent::PrevPageKey)
|
||||
} else if key == self.tui_logger_next_page_key {
|
||||
Some(TuiWidgetEvent::NextPageKey)
|
||||
} else if key == self.tui_logger_escape_key {
|
||||
Some(TuiWidgetEvent::EscapeKey)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[serde(try_from = "String")]
|
||||
#[serde(into = "String")]
|
||||
pub struct KeyBinding {
|
||||
pub code: KeyCode,
|
||||
pub modifier: KeyModifiers,
|
||||
}
|
||||
|
||||
impl KeyBinding {
|
||||
pub fn new(code: KeyCode) -> Self {
|
||||
KeyBinding {
|
||||
code,
|
||||
modifier: KeyModifiers::NONE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_modifier(code: KeyCode, modifier: KeyModifiers) -> Self {
|
||||
KeyBinding { code, modifier }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyEvent> for KeyBinding {
|
||||
fn from(value: KeyEvent) -> Self {
|
||||
KeyBinding {
|
||||
code: value.code,
|
||||
modifier: value.modifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KeyBinding {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if self.modifier.contains(KeyModifiers::SHIFT) {
|
||||
write!(f, "shift+")?;
|
||||
}
|
||||
if self.modifier.contains(KeyModifiers::CONTROL) {
|
||||
write!(f, "ctrl+")?;
|
||||
}
|
||||
if self.modifier.contains(KeyModifiers::ALT) {
|
||||
write!(f, "alt+")?;
|
||||
}
|
||||
if self.modifier.contains(KeyModifiers::SUPER) {
|
||||
write!(f, "super+")?;
|
||||
}
|
||||
if self.modifier.contains(KeyModifiers::HYPER) {
|
||||
write!(f, "hyper+")?;
|
||||
}
|
||||
if self.modifier.contains(KeyModifiers::META) {
|
||||
write!(f, "meta+")?;
|
||||
}
|
||||
match self.code {
|
||||
KeyCode::Backspace => write!(f, "backspace"),
|
||||
KeyCode::Enter => write!(f, "enter"),
|
||||
KeyCode::Left => write!(f, "left"),
|
||||
KeyCode::Right => write!(f, "right"),
|
||||
KeyCode::Up => write!(f, "up"),
|
||||
KeyCode::Down => write!(f, "down"),
|
||||
KeyCode::Home => write!(f, "home"),
|
||||
KeyCode::End => write!(f, "end"),
|
||||
KeyCode::PageUp => write!(f, "pageup"),
|
||||
KeyCode::PageDown => write!(f, "pagedown"),
|
||||
KeyCode::Tab => write!(f, "tab"),
|
||||
KeyCode::BackTab => write!(f, "backtab"),
|
||||
KeyCode::Delete => write!(f, "delete"),
|
||||
KeyCode::Insert => write!(f, "insert"),
|
||||
KeyCode::Char(c) => write!(f, "{c}"),
|
||||
KeyCode::Null => write!(f, "null"),
|
||||
KeyCode::Esc => write!(f, "esc"),
|
||||
_ => write!(f, "unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyBinding> for String {
|
||||
fn from(value: KeyBinding) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for KeyBinding {
|
||||
type Error = NymTuiError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for KeyBinding {
|
||||
type Err = NymTuiError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.rsplit_once('+') {
|
||||
Some((modifiers, value)) => Ok(Self {
|
||||
code: parse_keycode(value)?,
|
||||
modifier: parse_modifiers(modifiers)?,
|
||||
}),
|
||||
None => Ok(Self {
|
||||
code: parse_keycode(s)?,
|
||||
modifier: KeyModifiers::NONE,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_keycode(value: &str) -> Result<KeyCode, NymTuiError> {
|
||||
Ok(if value.len() == 1 {
|
||||
KeyCode::Char(
|
||||
char::from_str(value)
|
||||
.map_err(|source| NymTuiError::InvalidCharacter {
|
||||
str: value.to_string(),
|
||||
source,
|
||||
})?
|
||||
.to_ascii_lowercase(),
|
||||
)
|
||||
} else {
|
||||
SPECIAL_KEYS
|
||||
.get(value)
|
||||
.cloned()
|
||||
.ok_or_else(|| NymTuiError::UnknownKeyBinding {
|
||||
value: value.to_string(),
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_modifiers(modifiers: &str) -> Result<KeyModifiers, NymTuiError> {
|
||||
modifiers
|
||||
.split('+')
|
||||
.try_fold(KeyModifiers::NONE, |modifiers, token| {
|
||||
KEY_MODIFIERS
|
||||
.get(token)
|
||||
.map(|modifier| modifiers | *modifier)
|
||||
.ok_or_else(|| NymTuiError::UnknownKeyModifier {
|
||||
value: token.to_string(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||
KeyBinding::from(*key_event).to_string()
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod keybindings;
|
||||
|
||||
pub const DEFAULT_TICK_RATE: Duration = Duration::from_millis(200);
|
||||
|
||||
const DEFAULT_SHUTDOWN_GRACE: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_CANCEL_GRACE: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_ABORT_GRACE: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TuiConfig {
|
||||
pub tui: Tui,
|
||||
|
||||
// #[serde(default)]
|
||||
// pub key_bindings: KeyBindings,
|
||||
#[serde(default)]
|
||||
pub debug: Debug,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct Debug {
|
||||
pub debug_mode_enabled: bool,
|
||||
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub shutdown_grace: Duration,
|
||||
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub cancel_grace: Duration,
|
||||
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub abort_grace: Duration,
|
||||
}
|
||||
|
||||
impl Default for Debug {
|
||||
fn default() -> Self {
|
||||
Debug {
|
||||
debug_mode_enabled: true,
|
||||
shutdown_grace: DEFAULT_SHUTDOWN_GRACE,
|
||||
cancel_grace: DEFAULT_CANCEL_GRACE,
|
||||
abort_grace: DEFAULT_ABORT_GRACE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct Tui {
|
||||
#[serde(default, flatten)]
|
||||
pub debug: TuiDebug,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct TuiDebug {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub tick_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for TuiDebug {
|
||||
fn default() -> Self {
|
||||
TuiDebug {
|
||||
tick_rate: DEFAULT_TICK_RATE,
|
||||
// frame_rate: DEFAULT_FRAME_RATE,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::action::Action;
|
||||
use crate::tui::dispatcher::store::State;
|
||||
use crate::AppAction;
|
||||
use async_trait::async_trait;
|
||||
use color_eyre::eyre;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
pub mod store;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActionSender<T: AppAction>(UnboundedSender<Action<T>>);
|
||||
|
||||
impl<T> From<UnboundedSender<Action<T>>> for ActionSender<T>
|
||||
where
|
||||
T: AppAction,
|
||||
{
|
||||
fn from(value: UnboundedSender<Action<T>>) -> Self {
|
||||
ActionSender(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ActionSender<T>
|
||||
where
|
||||
T: AppAction,
|
||||
{
|
||||
pub fn send(&self, action: impl Into<Action<T>>) {
|
||||
if let Err(unsent) = self.0.send(action.into()) {
|
||||
error!("failed to send {:?} action to the dispatcher", unsent.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ActionReceiver<T> = UnboundedReceiverStream<Action<T>>;
|
||||
|
||||
pub type StateUpdateSender<S> = UnboundedSender<S>;
|
||||
pub type StateUpdateReceiver<S> = UnboundedReceiverStream<S>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait ActionDispatcher {
|
||||
type Store: State;
|
||||
type Actions: AppAction;
|
||||
|
||||
async fn handle_app_action(
|
||||
&mut self,
|
||||
action: Self::Actions,
|
||||
store: &mut Self::Store,
|
||||
) -> eyre::Result<()>;
|
||||
}
|
||||
|
||||
pub struct DispatcherLoop<D: ActionDispatcher> {
|
||||
dispatcher: D,
|
||||
store: D::Store,
|
||||
|
||||
// to be used with async actions
|
||||
#[allow(dead_code)]
|
||||
action_sender: ActionSender<D::Actions>,
|
||||
action_receiver: ActionReceiver<D::Actions>,
|
||||
state_update_sender: StateUpdateSender<D::Store>,
|
||||
cancellation_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl<D> DispatcherLoop<D>
|
||||
where
|
||||
D: ActionDispatcher + Send + Sync + 'static,
|
||||
D::Store: Send + Sync + 'static,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
dispatcher: D,
|
||||
store: D::Store,
|
||||
action_sender: impl Into<ActionSender<D::Actions>>,
|
||||
action_receiver: impl Into<ActionReceiver<D::Actions>>,
|
||||
state_update_sender: StateUpdateSender<D::Store>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> DispatcherLoop<D> {
|
||||
DispatcherLoop {
|
||||
dispatcher,
|
||||
store,
|
||||
action_sender: action_sender.into(),
|
||||
action_receiver: action_receiver.into(),
|
||||
state_update_sender,
|
||||
cancellation_token,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_action(&mut self, action: Option<Action<D::Actions>>) -> eyre::Result<()> {
|
||||
let Some(action) = action else {
|
||||
warn!("the dispatcher channel has closed! we're probably already in shutdown!");
|
||||
// but if we're not, make sure to kick it off...
|
||||
self.cancellation_token.cancel();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match action {
|
||||
Action::Quit => {
|
||||
debug!("attempting to handle the QUIT action");
|
||||
self.cancellation_token.cancel();
|
||||
// no need to send any state updates here
|
||||
return Ok(());
|
||||
}
|
||||
Action::AppDefined(action) => {
|
||||
debug!("attempting to handle the following action: {:?}", action);
|
||||
self.dispatcher
|
||||
.handle_app_action(action, &mut self.store)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.state_update_sender.send(self.store.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> eyre::Result<()> {
|
||||
info!("starting the dispatcher loop");
|
||||
|
||||
// issue initial state
|
||||
self.state_update_sender.send(self.store.clone())?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.cancellation_token.cancelled() => {
|
||||
info!("received cancellation token");
|
||||
break;
|
||||
}
|
||||
maybe_action = self.action_receiver.next() => {
|
||||
self.handle_action(maybe_action).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub trait State: Clone {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use color_eyre::eyre;
|
||||
use crossterm::event::EventStream;
|
||||
use ratatui::backend::CrosstermBackend as Backend;
|
||||
use ratatui::crossterm::{
|
||||
self, cursor,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio_stream::Stream;
|
||||
|
||||
pub struct TuiHandle {
|
||||
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
|
||||
pub crossterm_events: EventStream,
|
||||
}
|
||||
|
||||
impl TuiHandle {
|
||||
pub fn new() -> Result<TuiHandle, eyre::Error> {
|
||||
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
|
||||
let crossterm_events = EventStream::new();
|
||||
|
||||
Ok(TuiHandle {
|
||||
terminal,
|
||||
crossterm_events,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enter(&self) -> Result<(), eyre::Error> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exit(&mut self) -> eyre::Result<()> {
|
||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||
self.terminal.flush()?;
|
||||
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for TuiHandle {
|
||||
type Item = <EventStream as Stream>::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.crossterm_events).poll_next(cx)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.crossterm_events.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TuiHandle {
|
||||
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for TuiHandle {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TuiHandle {
|
||||
fn drop(&mut self) {
|
||||
// well. at this point we can't do much, we'll just go straight into the panic handler
|
||||
#[allow(clippy::expect_used)]
|
||||
self.exit().expect("failed to teardown the terminal")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::action::Action;
|
||||
use crate::tui::config::TuiConfig;
|
||||
use crate::tui::dispatcher::{ActionDispatcher, ActionSender, DispatcherLoop};
|
||||
use crate::tui::ui::UiEventLoop;
|
||||
use crate::Component;
|
||||
use color_eyre::eyre;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::TaskTracker;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub struct TuiManager<C: Component> {
|
||||
shutdown_grace: Duration,
|
||||
cancel_grace: Duration,
|
||||
abort_grace: Duration,
|
||||
|
||||
task_tracker: TaskTracker,
|
||||
cancellation_token: CancellationToken,
|
||||
|
||||
dispatcher_handle: JoinHandle<eyre::Result<()>>,
|
||||
ui_event_loop_handle: JoinHandle<eyre::Result<()>>,
|
||||
|
||||
action_sender: ActionSender<C::Actions>,
|
||||
}
|
||||
|
||||
impl<C> TuiManager<C>
|
||||
where
|
||||
C: Component + Send + Sync + 'static,
|
||||
C::State: Send + Sync + 'static,
|
||||
C::Actions: Send + Sync + Clone + 'static,
|
||||
{
|
||||
pub(crate) fn build_new<D>(
|
||||
config: TuiConfig,
|
||||
action_dispatcher: D,
|
||||
initial_state: D::Store,
|
||||
) -> eyre::Result<TuiManager<C>>
|
||||
where
|
||||
D: ActionDispatcher<Store = C::State, Actions = C::Actions> + Send + Sync + 'static,
|
||||
{
|
||||
let task_tracker = TaskTracker::new();
|
||||
let cancellation_token = CancellationToken::new();
|
||||
|
||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||
let (state_tx, state_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let mut dispatcher_loop = DispatcherLoop::new(
|
||||
action_dispatcher,
|
||||
initial_state,
|
||||
action_tx.clone(),
|
||||
action_rx,
|
||||
state_tx,
|
||||
cancellation_token.clone(),
|
||||
);
|
||||
let mut ui_event_loop = UiEventLoop::<C>::new(
|
||||
config.tui.debug.tick_rate,
|
||||
cancellation_token.clone(),
|
||||
state_rx,
|
||||
action_tx.clone(),
|
||||
)?;
|
||||
|
||||
let dispatcher_handle = task_tracker.spawn(async move { dispatcher_loop.run().await });
|
||||
let ui_event_loop_handle = task_tracker.spawn(async move { ui_event_loop.run().await });
|
||||
|
||||
task_tracker.close();
|
||||
|
||||
Ok(TuiManager {
|
||||
shutdown_grace: config.debug.shutdown_grace,
|
||||
cancel_grace: config.debug.cancel_grace,
|
||||
abort_grace: config.debug.abort_grace,
|
||||
task_tracker,
|
||||
cancellation_token,
|
||||
dispatcher_handle,
|
||||
ui_event_loop_handle,
|
||||
action_sender: action_tx.into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn graceful_shutdown(&self) {
|
||||
// 1. try to send quit action to handle it the most gracefully
|
||||
self.action_sender.send(Action::Quit);
|
||||
if timeout(self.shutdown_grace, self.task_tracker.wait())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
error!("timed out while waiting for graceful shutdown");
|
||||
|
||||
// 2. if that doesn't work, issue cancellation token
|
||||
self.cancellation_token.cancel();
|
||||
if timeout(self.cancel_grace, self.task_tracker.wait())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
error!("timed out while attempting to resolve cancellation token shutdown");
|
||||
|
||||
// 3. finally go with nuclear option and just abort the tasks
|
||||
self.dispatcher_handle.abort();
|
||||
self.ui_event_loop_handle.abort();
|
||||
|
||||
if timeout(self.abort_grace, self.task_tracker.wait())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
error!("somehow we still failed to shutdown our tasks! we might end up in a dirty state... oh well")
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_exit_or_signal(&self) {
|
||||
tokio::select! {
|
||||
_ = self.task_tracker.wait() => {
|
||||
// user decided to quit with 'normal' action
|
||||
}
|
||||
_ = wait_for_signal() => {
|
||||
self.graceful_shutdown().await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[allow(clippy::expect_used)]
|
||||
pub async fn wait_for_signal() {
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut sigterm = signal(SignalKind::terminate()).expect("failed to setup SIGTERM channel");
|
||||
let mut sigquit = signal(SignalKind::quit()).expect("failed to setup SIGQUIT channel");
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!("Received SIGINT");
|
||||
},
|
||||
_ = sigterm.recv() => {
|
||||
info!("Received SIGTERM");
|
||||
}
|
||||
_ = sigquit.recv() => {
|
||||
info!("Received SIGQUIT");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub async fn wait_for_signal() {
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!("Received SIGINT");
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::handle::TuiHandle;
|
||||
use color_eyre::eyre;
|
||||
use tracing::error;
|
||||
|
||||
pub mod action;
|
||||
pub mod config;
|
||||
pub(crate) mod dispatcher;
|
||||
pub mod handle;
|
||||
pub(crate) mod manager;
|
||||
pub(crate) mod ui;
|
||||
|
||||
pub fn initialize_panic_handler() -> eyre::Result<()> {
|
||||
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
||||
.display_location_section(true)
|
||||
.display_env_section(true)
|
||||
.into_hooks();
|
||||
eyre_hook.install()?;
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
if let Err(r) = TuiHandle::new().and_then(|mut h| h.exit()) {
|
||||
error!("Unable to exit Terminal: {:?}", r);
|
||||
}
|
||||
|
||||
let msg = format!("{}", panic_hook.panic_report(panic_info));
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
eprintln!("{}", msg); // prints color-eyre stack trace to stderr
|
||||
}
|
||||
error!("error: {}", strip_ansi_escapes::strip_str(msg));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Better Panic stacktrace that is only enabled when debugging.
|
||||
better_panic::Settings::auto()
|
||||
.most_recent_first(false)
|
||||
.lineno_suffix(true)
|
||||
.verbosity(better_panic::Verbosity::Full)
|
||||
.create_panic_handler()(panic_info);
|
||||
}
|
||||
|
||||
std::process::exit(1);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::config::keybindings::key_event_to_string;
|
||||
use crate::tui::dispatcher::ActionSender;
|
||||
use crate::{AppAction, Component, State};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
use ratatui::prelude::{Line, Style, Stylize};
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::Frame;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub struct DebugHistory<S, A> {
|
||||
last_tick_key_events: Vec<KeyEvent>,
|
||||
|
||||
phantom_state: PhantomData<S>,
|
||||
phantom_action: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<S, A> Component for DebugHistory<S, A>
|
||||
where
|
||||
S: State,
|
||||
A: AppAction,
|
||||
{
|
||||
type State = S;
|
||||
type Actions = A;
|
||||
|
||||
fn new(_state: &Self::State, _action_sender: ActionSender<Self::Actions>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
DebugHistory {
|
||||
last_tick_key_events: vec![],
|
||||
phantom_state: PhantomData,
|
||||
phantom_action: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self) -> bool {
|
||||
let was_empty = self.last_tick_key_events.is_empty();
|
||||
self.last_tick_key_events.drain(..);
|
||||
|
||||
!was_empty
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> color_eyre::Result<()> {
|
||||
self.last_tick_key_events.push(key);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn view(&mut self, frame: &mut Frame, rect: Rect) {
|
||||
frame.render_widget(
|
||||
Block::default()
|
||||
.title_top(
|
||||
Line::from(format!(
|
||||
"{:?}",
|
||||
&self
|
||||
.last_tick_key_events
|
||||
.iter()
|
||||
.map(key_event_to_string)
|
||||
.collect::<Vec<_>>()
|
||||
))
|
||||
.alignment(Alignment::Right),
|
||||
)
|
||||
.title_style(Style::default().bold()),
|
||||
rect,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::config::keybindings::LoggerKeybindings;
|
||||
use crate::tui::dispatcher::store::State;
|
||||
use crate::tui::dispatcher::ActionSender;
|
||||
use crate::tui::ui::components::Component;
|
||||
use crate::AppAction;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::Frame;
|
||||
use std::marker::PhantomData;
|
||||
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetState};
|
||||
|
||||
pub struct Props {
|
||||
pub keybindings: LoggerKeybindings,
|
||||
}
|
||||
|
||||
pub struct Logger<S, A> {
|
||||
props: Props,
|
||||
widget_state: TuiWidgetState,
|
||||
|
||||
phantom_state: PhantomData<S>,
|
||||
phantom_action: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<S, A> Component for Logger<S, A>
|
||||
where
|
||||
S: State,
|
||||
for<'a> Props: From<&'a S>,
|
||||
A: AppAction,
|
||||
{
|
||||
type State = S;
|
||||
type Actions = A;
|
||||
|
||||
fn new(state: &Self::State, _action_sender: ActionSender<Self::Actions>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Logger {
|
||||
props: Props::from(state),
|
||||
widget_state: TuiWidgetState::new(),
|
||||
phantom_state: PhantomData,
|
||||
phantom_action: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> color_eyre::Result<()> {
|
||||
if let Some(tui_event) = self.props.keybindings.tui_logger_event(key.into()) {
|
||||
self.widget_state.transition(tui_event)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn view(&mut self, frame: &mut Frame, rect: Rect) {
|
||||
let border = Block::bordered();
|
||||
let inner_area = border.inner(rect);
|
||||
frame.render_widget(border, rect);
|
||||
|
||||
let tui_sm = TuiLoggerSmartWidget::default()
|
||||
.style_error(Style::default().fg(Color::Red))
|
||||
.style_warn(Style::default().fg(Color::Yellow))
|
||||
.style_info(Style::default().fg(Color::Green))
|
||||
.style_debug(Style::default().fg(Color::Cyan))
|
||||
.style_trace(Style::default().fg(Color::Magenta))
|
||||
.output_separator(':')
|
||||
.output_timestamp(Some("%F %H:%M:%S%.3f".to_string()))
|
||||
.output_level(Some(TuiLoggerLevelOutput::Long))
|
||||
.output_target(true)
|
||||
.output_file(true)
|
||||
.output_line(true)
|
||||
.state(&self.widget_state);
|
||||
frame.render_widget(tui_sm, inner_area);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod debug_history;
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
pub mod logger;
|
||||
|
||||
pub use debug_history::DebugHistory;
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
pub use logger::{Logger, Props as LoggerProps};
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::dispatcher::ActionSender;
|
||||
use crate::{AppAction, State};
|
||||
use color_eyre::eyre;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::prelude::Rect;
|
||||
use ratatui::Frame;
|
||||
|
||||
pub mod common;
|
||||
|
||||
// pub trait Props<'a>: From<&'a Self::State> {
|
||||
// type State: State;
|
||||
// }
|
||||
|
||||
pub trait Component {
|
||||
type State: State;
|
||||
type Actions: AppAction;
|
||||
|
||||
fn new(state: &Self::State, action_sender: ActionSender<Self::Actions>) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn update(self, state: &Self::State) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let _ = state;
|
||||
self
|
||||
}
|
||||
|
||||
// returns boolean indicating whether a rerender is needed
|
||||
// fn tick(&mut self) -> bool;
|
||||
fn tick(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> eyre::Result<()> {
|
||||
let _ = key;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn view(&mut self, frame: &mut Frame, rect: Rect);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tui::dispatcher::{ActionSender, StateUpdateReceiver};
|
||||
use crate::tui::handle::TuiHandle;
|
||||
use crate::tui::ui::components::Component;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::eyre;
|
||||
use crossterm::event::Event;
|
||||
use humantime_serde::re::humantime;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
use tokio::time::{timeout, Instant};
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
pub mod components;
|
||||
|
||||
pub struct UiEventLoop<C: Component> {
|
||||
tick_rate: Duration,
|
||||
cancellation_token: CancellationToken,
|
||||
state_receiver: StateUpdateReceiver<C::State>,
|
||||
|
||||
// only to be used to construct root 'App' instance
|
||||
action_sender: ActionSender<C::Actions>,
|
||||
tui_handle: TuiHandle,
|
||||
|
||||
root_component: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C> UiEventLoop<C>
|
||||
where
|
||||
C: Component,
|
||||
{
|
||||
pub fn new(
|
||||
tick_rate: Duration,
|
||||
cancellation_token: CancellationToken,
|
||||
state_receiver: impl Into<StateUpdateReceiver<C::State>>,
|
||||
action_sender: impl Into<ActionSender<C::Actions>>,
|
||||
) -> eyre::Result<Self> {
|
||||
Ok(UiEventLoop {
|
||||
tick_rate,
|
||||
cancellation_token,
|
||||
state_receiver: state_receiver.into(),
|
||||
action_sender: action_sender.into(),
|
||||
tui_handle: TuiHandle::new()?,
|
||||
root_component: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_ui_event(
|
||||
&mut self,
|
||||
event: Option<io::Result<Event>>,
|
||||
root_app: &mut C,
|
||||
) -> eyre::Result<()> {
|
||||
let Some(event) = event else {
|
||||
warn!("the crossterm event channel has closed! we're probably already in shutdown!");
|
||||
// but if we're not, make sure to kick it off...
|
||||
self.cancellation_token.cancel();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match event? {
|
||||
Event::FocusGained => {}
|
||||
Event::FocusLost => {}
|
||||
Event::Key(key_event) => root_app.handle_key(key_event)?,
|
||||
Event::Mouse(_) => {}
|
||||
Event::Paste(_) => {}
|
||||
Event::Resize(_, _) => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_updated_state(&mut self, state: Option<C::State>, root_app: C) -> C
|
||||
where
|
||||
C: Component,
|
||||
{
|
||||
let Some(updated_state) = state else {
|
||||
warn!("the state update channel has closed! we're probably already in shutdown!");
|
||||
// but if we're not, make sure to kick it off...
|
||||
self.cancellation_token.cancel();
|
||||
return root_app;
|
||||
};
|
||||
|
||||
root_app.update(&updated_state)
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> eyre::Result<()>
|
||||
where
|
||||
// this clone shouldn't really be needed...
|
||||
C::Actions: Clone,
|
||||
{
|
||||
info!("starting the ui loop");
|
||||
|
||||
// wait for initial state...
|
||||
let initial_state = timeout(Duration::from_secs(1), self.state_receiver.next())
|
||||
.await?
|
||||
.ok_or_else(|| eyre!("did not receive initial state!"))?;
|
||||
|
||||
let mut root_app = C::new(&initial_state, self.action_sender.clone());
|
||||
|
||||
let mut tick_rate = self.tick_rate;
|
||||
let mut tick_interval = tokio::time::interval(tick_rate);
|
||||
self.tui_handle.enter()?;
|
||||
|
||||
let mut draw = true;
|
||||
|
||||
loop {
|
||||
if draw {
|
||||
let draw_start = Instant::now();
|
||||
|
||||
trace!("redrawing the UI");
|
||||
self.tui_handle
|
||||
.draw(|frame| root_app.view(frame, frame.area()))?;
|
||||
|
||||
let taken = humantime::format_duration(draw_start.elapsed()).to_string();
|
||||
trace!(time_taken = taken, "UI drawing");
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.cancellation_token.cancelled() => {
|
||||
info!("received cancellation token");
|
||||
break;
|
||||
}
|
||||
maybe_ui_event = self.tui_handle.next() => {
|
||||
self.handle_ui_event(maybe_ui_event, &mut root_app).await?;
|
||||
draw = true;
|
||||
}
|
||||
state_update = self.state_receiver.next() => {
|
||||
root_app = self.handle_updated_state(state_update, root_app).await;
|
||||
// the tick rate has changed
|
||||
if self.tick_rate != tick_rate {
|
||||
tick_rate = self.tick_rate;
|
||||
tick_interval = tokio::time::interval(tick_rate);
|
||||
}
|
||||
draw = true;
|
||||
}
|
||||
_ = tick_interval.tick() => {
|
||||
let tick_start = Instant::now();
|
||||
draw = root_app.tick();
|
||||
|
||||
let taken = humantime::format_duration(tick_start.elapsed()).to_string();
|
||||
trace!(time_taken = taken, will_redraw = draw, "app tick");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user