diff --git a/Cargo.lock b/Cargo.lock index 2ff24be..fc9b179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,7 +36,7 @@ dependencies = [ "accesskit", "accesskit_consumer 0.36.0", "atspi-common", - "phf 0.13.1", + "phf", "serde", "zvariant", ] @@ -199,56 +199,6 @@ dependencies = [ "vte", ] -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -395,7 +345,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -430,16 +380,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", + "syn", ] [[package]] @@ -491,36 +432,15 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ - "bit-vec 0.9.1", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bit-vec" version = "0.9.1" @@ -539,15 +459,6 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block2" version = "0.5.1" @@ -612,7 +523,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -678,15 +589,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.63" @@ -720,46 +622,6 @@ dependencies = [ "libc", ] -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "clipboard-win" version = "5.4.1" @@ -789,12 +651,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "combine" version = "4.6.7" @@ -805,20 +661,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compact_str" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -828,15 +670,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -887,15 +720,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -905,177 +729,24 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.12.1", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.12.1", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.1.4", - "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 = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf 0.11.3", -] - [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[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 2.0.117", -] - -[[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 2.0.117", -] - -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -1100,7 +771,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1306,7 +977,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1400,16 +1071,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set 0.5.3", - "regex", -] - [[package]] name = "fastrand" version = "2.4.1" @@ -1440,35 +1101,12 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.1.9" @@ -1479,12 +1117,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -1524,7 +1156,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1611,7 +1243,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1643,16 +1275,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" version = "1.1.0" @@ -1961,12 +1583,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -2014,28 +1630,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "io-kit-sys" version = "0.4.1" @@ -2046,12 +1640,6 @@ dependencies = [ "mach2", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itertools" version = "0.14.0" @@ -2094,7 +1682,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", + "syn", ] [[package]] @@ -2122,7 +1710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2147,17 +1735,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - [[package]] name = "khronos-egl" version = "6.0.0" @@ -2187,12 +1764,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - [[package]] name = "lazy_static" version = "1.5.0" @@ -2259,15 +1830,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "line-clipping" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" -dependencies = [ - "bitflags 2.12.1", -] - [[package]] name = "linebender_resource_handle" version = "0.1.1" @@ -2313,15 +1875,6 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" -[[package]] -name = "lru" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "lua-src" version = "550.0.0" @@ -2341,16 +1894,6 @@ dependencies = [ "which", ] -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix 0.29.0", - "winapi", -] - [[package]] name = "mach2" version = "0.4.3" @@ -2384,12 +1927,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - [[package]] name = "memoffset" version = "0.9.1" @@ -2487,7 +2024,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.117", + "syn", ] [[package]] @@ -2507,7 +2044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079" dependencies = [ "arrayvec", - "bit-set 0.9.1", + "bit-set", "bitflags 2.12.1", "cfg-if", "cfg_aliases", @@ -2577,7 +2114,6 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset", ] [[package]] @@ -2614,23 +2150,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2660,16 +2179,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", + "syn", ] [[package]] @@ -2987,12 +2497,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "orbclient" version = "0.3.55" @@ -3012,15 +2516,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "5.3.0" @@ -3097,90 +2592,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros", + "phf_shared", "serde", ] -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand", -] - [[package]] name = "phf_generator" version = "0.13.1" @@ -3188,20 +2610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", + "phf_shared", ] [[package]] @@ -3210,20 +2619,11 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", + "syn", ] [[package]] @@ -3252,7 +2652,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3321,27 +2721,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "pipeview-tui" -version = "0.1.0" -dependencies = [ - "ansitok", - "clap", - "crossterm 0.28.1", - "hex", - "image", - "mlua", - "pipeview-client", - "pipeview-core", - "ratatui", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - [[package]] name = "pkg-config" version = "0.3.33" @@ -3420,12 +2799,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "presser" version = "0.3.1" @@ -3439,7 +2812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn", ] [[package]] @@ -3470,7 +2843,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3531,112 +2904,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "range-alloc" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termwiz", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.12.1", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools", - "kasuari", - "lru", - "strum", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm 0.29.0", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.12.1", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools", - "line-clipping", - "ratatui-core", - "strum", - "time", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3780,12 +3053,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "same-file" version = "1.0.6" @@ -3869,7 +3136,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3893,7 +3160,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3915,17 +3182,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -3941,27 +3197,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[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" @@ -4140,50 +3375,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -4203,7 +3394,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4228,69 +3419,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom 7.1.3", - "phf 0.11.3", - "phf_codegen", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64", - "bitflags 2.12.1", - "fancy-regex", - "filedescriptor", - "finl_unicode", - "fixedbitset", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix 0.29.0", - "num-derive", - "num-traits", - "ordered-float 4.6.0", - "pest", - "pest_derive", - "phf 0.11.3", - "sha2", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -4317,7 +3445,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4328,7 +3456,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4354,39 +3482,6 @@ dependencies = [ "zune-jpeg", ] -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tiny-skia" version = "0.11.4" @@ -4447,7 +3542,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4506,19 +3601,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-appender" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" -dependencies = [ - "crossbeam-channel", - "symlink", - "thiserror 2.0.18", - "time", - "tracing-subscriber", -] - [[package]] name = "tracing-attributes" version = "0.1.31" @@ -4527,7 +3609,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4590,18 +3672,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "uds_windows" version = "1.2.1" @@ -4634,17 +3704,6 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicode-width" version = "0.2.2" @@ -4675,20 +3734,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ - "atomic", - "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -4742,15 +3793,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -4827,7 +3869,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -5051,78 +4093,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float 4.6.0", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - [[package]] name = "wgpu" version = "29.0.3" @@ -5160,8 +4130,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7" dependencies = [ "arrayvec", - "bit-set 0.9.1", - "bit-vec 0.9.1", + "bit-set", + "bit-vec", "bitflags 2.12.1", "bytemuck", "cfg_aliases", @@ -5232,7 +4202,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set 0.9.1", + "bit-set", "bitflags 2.12.1", "block2 0.6.2", "bytemuck", @@ -5394,7 +4364,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5405,7 +4375,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5713,7 +4683,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -5729,7 +4699,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5859,7 +4829,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -5916,7 +4886,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "zbus-lockstep", "zbus_xml", "zvariant", @@ -5931,7 +4901,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "zbus_names", "zvariant", "zvariant_utils", @@ -5977,7 +4947,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -5997,7 +4967,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -6031,7 +5001,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -6078,7 +5048,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "zvariant_utils", ] @@ -6091,6 +5061,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", + "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index b11d33b..8b44858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ resolver = "2" members = [ "crates/pipeview-core", "crates/pipeview-client", - "crates/pipeview-tui", "crates/pipeview-gui", ] @@ -30,21 +29,10 @@ thiserror = "2" # ── 日志 ── tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-appender = "0.2" # ── 工具 ── hex = "0.4" -# ── 图像 ── -image = { version = "0.25", default-features = false, features = ["png"] } - -# ── CLI ── -clap = { version = "4", features = ["derive"] } - -# ── TUI ── -ratatui = "0.30" -crossterm = "0.28" - # ── GUI ── egui = "0.34" eframe = "0.34" diff --git a/README.md b/README.md index 4dede8f..c7d3d9c 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ return { ``` ┌──────────────────────────────────────────────────┐ -│ pipeview-gui (egui) pipeview-tui (ratatui) │ +│ pipeview-gui (egui) │ ├──────────────────────────────────────────────────┤ │ pipeview-client │ │ SessionManager · Session · Config · History │ @@ -293,9 +293,8 @@ return { | `pipeview-core` | library | 传输层(Serial/TCP/UDP)、分帧器(Line/Fixed/Length/COBS/Mixed/Lua)、协议解码器(Text/Hex/Plot)、MultiPipeline | | `pipeview-client` | library | Session 生命周期管理、SessionManager、事件广播(tokio broadcast)、RingBuffer 历史、Lua 运行时及会话 API | | `pipeview-gui` | binary | egui 桌面应用,包含 sidebar/config/console/text/hex/plot 面板、键盘快捷键、字体管理、性能分析 | -| `pipeview-tui` | binary | ratatui 终端应用(功能未对齐 GUI,仍在开发中) | -**依赖方向:** `core ← client ← {gui, tui}` +**依赖方向:** `core ← client ← gui` **数据流:** @@ -365,7 +364,6 @@ crates/ pipeview-core/ # 传输、分帧、协议 pipeview-client/ # 会话管理、Lua 运行时 pipeview-gui/ # egui 桌面应用 - pipeview-tui/ # ratatui 终端应用 examples/ # Lua 脚本示例 drone_plot.lua # 飞控遥测波形解码器 drone_text.lua # 飞控遥测文本解码器 diff --git a/README_en.md b/README_en.md index 205d63c..5c66867 100644 --- a/README_en.md +++ b/README_en.md @@ -265,7 +265,7 @@ More examples in `examples/`. ``` ┌──────────────────────────────────────────────────┐ -│ pipeview-gui (egui) pipeview-tui (ratatui) │ +│ pipeview-gui (egui) │ ├──────────────────────────────────────────────────┤ │ pipeview-client │ │ SessionManager · Session · Config · History │ @@ -281,9 +281,8 @@ More examples in `examples/`. | `pipeview-core` | library | Transport (Serial/TCP/UDP), framers (Line/Fixed/Length/COBS/Mixed/Lua), decoders (Text/Hex/Plot), MultiPipeline | | `pipeview-client` | library | Session lifecycle, SessionManager, event broadcast (tokio broadcast), RingBuffer history, Lua runtime & session API | | `pipeview-gui` | binary | egui desktop app: sidebar, config, console, text/hex/plot panels, keyboard shortcuts, font management, profiling | -| `pipeview-tui` | binary | ratatui terminal app (feature-incomplete, under development) | -**Dependency flow:** `core ← client ← {gui, tui}` +**Dependency flow:** `core ← client ← gui` **Data flow:** @@ -349,7 +348,6 @@ crates/ pipeview-core/ # Transport, framing, protocol pipeview-client/ # Session management, Lua runtime pipeview-gui/ # egui desktop application - pipeview-tui/ # ratatui terminal application examples/ # Lua script examples drone_plot.lua # Drone telemetry plot decoder drone_text.lua # Drone telemetry text decoder diff --git a/crates/pipeview-tui/Cargo.toml b/crates/pipeview-tui/Cargo.toml deleted file mode 100644 index acf9c6f..0000000 --- a/crates/pipeview-tui/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "pipeview-tui" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "pipeview-tui" -path = "src/main.rs" - -[dependencies] -pipeview-core = { path = "../pipeview-core" } -pipeview-client = { path = "../pipeview-client" } -tokio = { workspace = true } -ratatui = { workspace = true } -crossterm = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -tracing-appender = { workspace = true } -clap = { workspace = true } -hex = { workspace = true } -image = { workspace = true } -mlua = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -ansitok = "0.3" diff --git a/crates/pipeview-tui/src/ansi.rs b/crates/pipeview-tui/src/ansi.rs deleted file mode 100644 index e32dbd3..0000000 --- a/crates/pipeview-tui/src/ansi.rs +++ /dev/null @@ -1,295 +0,0 @@ -use ansitok::{AnsiColor, ElementKind, VisualAttribute, parse_ansi, parse_ansi_sgr}; -use ratatui::style::{Color, Modifier, Style}; -use ratatui::text::Span; - -/// Parse ANSI-encoded text and return a Vec of styled Spans. -/// -/// ANSI SGR sequences (colors, bold, italic, underline, strikethrough) -/// are converted to ratatui `Style` applied on top of `base_style`. -/// Non-SGR sequences (cursor movement, etc.) are silently ignored. -pub fn ansi_to_spans(text: &str, base_style: Style) -> Vec> { - let mut spans = Vec::new(); - let mut style = AnsiStyleStack::default(); - - for element in parse_ansi(text) { - match element.kind() { - ElementKind::Text => { - let slice = &text[element.start()..element.end()]; - if !slice.is_empty() { - spans.push(Span::styled( - slice.to_string(), - style.merge(base_style), - )); - } - } - ElementKind::Sgr => { - let sgr = &text[element.start()..element.end()]; - apply_sgr_sequence(&mut style, sgr); - } - _ => {} - } - } - - spans -} - -/// Extract visible text from ANSI-encoded string (strips escape sequences). -/// -/// This is the text that a user would actually see on a terminal — -/// useful for search, copy, and line counting. -pub fn ansi_visible_text(text: &str) -> String { - let mut visible = String::new(); - for element in parse_ansi(text) { - if element.kind() == ElementKind::Text { - visible.push_str(&text[element.start()..element.end()]); - } - } - visible -} - -// ── internal style stack ── - -#[derive(Default, Clone)] -struct AnsiStyleStack { - fg: Option, - bg: Option, - bold: bool, - italic: bool, - underline: bool, - strikethrough: bool, -} - -impl AnsiStyleStack { - fn apply_sgr_attr(&mut self, attr: VisualAttribute) { - match attr { - VisualAttribute::Reset(0) => *self = Self::default(), - VisualAttribute::Reset(22) => self.bold = false, - VisualAttribute::Reset(23) => self.italic = false, - VisualAttribute::Reset(24) => self.underline = false, - VisualAttribute::Reset(29) => self.strikethrough = false, - VisualAttribute::Reset(39) => self.fg = None, - VisualAttribute::Reset(49) => self.bg = None, - VisualAttribute::Reset(_) => {} - VisualAttribute::Bold => self.bold = true, - VisualAttribute::Faint => self.bold = false, - VisualAttribute::Italic => self.italic = true, - VisualAttribute::Underline => self.underline = true, - VisualAttribute::Crossedout => self.strikethrough = true, - VisualAttribute::FgColor(c) => self.fg = Some(ansi_color_to_ratatui(c)), - VisualAttribute::BgColor(c) => self.bg = Some(ansi_color_to_ratatui(c)), - _ => {} - } - } - - fn apply_sgr(&mut self, sgr: &str) { - // Strip ESC[ ... m wrapper and delegate to ansitok's SGR parser - let params = sgr - .strip_prefix("\x1b[") - .and_then(|s| s.strip_suffix('m')) - .unwrap_or(sgr); - - if params.is_empty() { - *self = Self::default(); - return; - } - - for output in parse_ansi_sgr(sgr) { - if let Some(attr) = output.as_escape() { - self.apply_sgr_attr(attr); - } - } - } - - fn merge(&self, base: Style) -> Style { - let mut style = base; - - if let Some(fg) = self.fg { - if self.bold { - style = style.fg(brighten(fg)); - } else { - style = style.fg(fg); - } - } - if let Some(bg) = self.bg { - style = style.bg(bg); - } - if self.italic { - style = style.add_modifier(Modifier::ITALIC); - } - if self.underline { - style = style.add_modifier(Modifier::UNDERLINED); - } - if self.strikethrough { - style = style.add_modifier(Modifier::CROSSED_OUT); - } - if self.bold { - // Bold applied only when there's no explicit fg (brighten handles it otherwise) - style = style.add_modifier(Modifier::BOLD); - } - - style - } -} - -fn apply_sgr_sequence(style: &mut AnsiStyleStack, sgr: &str) { - style.apply_sgr(sgr); -} - -// ── color conversion ── - -fn ansi_color_to_ratatui(color: AnsiColor) -> Color { - match color { - AnsiColor::Bit4(c) => ansi_4bit(c), - AnsiColor::Bit8(c) => ansi_256(c), - AnsiColor::Bit24 { r, g, b } => Color::Rgb(r, g, b), - } -} - -fn ansi_4bit(code: u8) -> Color { - match code { - 30 => Color::Rgb(0, 0, 0), - 31 => Color::Rgb(194, 54, 33), - 32 => Color::Rgb(37, 188, 36), - 33 => Color::Rgb(173, 173, 39), - 34 => Color::Rgb(73, 46, 225), - 35 => Color::Rgb(211, 56, 211), - 36 => Color::Rgb(51, 187, 200), - 37 => Color::Rgb(203, 204, 205), - - 90 => Color::Rgb(129, 131, 131), - 91 => Color::Rgb(252, 57, 31), - 92 => Color::Rgb(49, 231, 34), - 93 => Color::Rgb(234, 236, 35), - 94 => Color::Rgb(88, 51, 255), - 95 => Color::Rgb(249, 53, 248), - 96 => Color::Rgb(20, 240, 240), - 97 => Color::Rgb(233, 235, 235), - - // Background colors (same values as foreground but offset by 10) - 40 => Color::Rgb(0, 0, 0), - 41 => Color::Rgb(194, 54, 33), - 42 => Color::Rgb(37, 188, 36), - 43 => Color::Rgb(173, 173, 39), - 44 => Color::Rgb(73, 46, 225), - 45 => Color::Rgb(211, 56, 211), - 46 => Color::Rgb(51, 187, 200), - 47 => Color::Rgb(203, 204, 205), - - 100 => Color::Rgb(129, 131, 131), - 101 => Color::Rgb(252, 57, 31), - 102 => Color::Rgb(49, 231, 34), - 103 => Color::Rgb(234, 236, 35), - 104 => Color::Rgb(88, 51, 255), - 105 => Color::Rgb(249, 53, 248), - 106 => Color::Rgb(20, 240, 240), - 107 => Color::Rgb(233, 235, 235), - - _ => Color::Rgb(255, 255, 255), - } -} - -fn ansi_256(code: u8) -> Color { - match code { - 0..=15 => ansi_4bit(if code < 8 { code + 30 } else { code + 82 }), - 16..=231 => { - let idx = code - 16; - let cube = [0, 95, 135, 175, 215, 255]; - let r = cube[(idx / 36) as usize]; - let g = cube[((idx / 6) % 6) as usize]; - let b = cube[(idx % 6) as usize]; - Color::Rgb(r, g, b) - } - 232..=255 => { - let v = (code - 232) * 10 + 8; - Color::Rgb(v, v, v) - } - } -} - -fn brighten(color: Color) -> Color { - match color { - Color::Rgb(r, g, b) => Color::Rgb( - ((r as u32) * 13 / 10).min(255) as u8, - ((g as u32) * 13 / 10).min(255) as u8, - ((b as u32) * 13 / 10).min(255) as u8, - ), - _ => color, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ratatui::style::Style; - - fn visible<'a>(spans: &[Span<'a>]) -> String { - spans.iter().map(|s| s.content.as_ref()).collect::() - } - - #[test] - fn plain_text_no_ansi() { - let spans = ansi_to_spans("hello", Style::default()); - assert_eq!(spans.len(), 1); - assert_eq!(spans[0].content, "hello"); - } - - #[test] - fn fg_red_reset() { - let spans = ansi_to_spans("\x1b[31mred\x1b[0m plain", Style::default()); - assert!(spans.len() >= 2); - assert_eq!(visible(&spans), "red plain"); - } - - #[test] - fn empty_sgr_resets_all() { - let base = Style::default().fg(Color::Rgb(1, 2, 3)); - let spans = ansi_to_spans("\x1b[31mred\x1b[m plain", base); - assert_eq!(visible(&spans), "red plain"); - assert!(spans.len() >= 2); - // First span should have red foreground - assert_ne!(spans[0].style.fg, Some(Color::Rgb(1, 2, 3))); - // Last span should have base foreground - assert_eq!(spans.last().unwrap().style.fg, Some(Color::Rgb(1, 2, 3))); - } - - #[test] - fn bold_brightens_color() { - let spans = ansi_to_spans("\x1b[1;31mbold red\x1b[0m", Style::default()); - assert_eq!(spans.len(), 1); - assert_eq!(visible(&spans), "bold red"); - } - - #[test] - fn strip_ansi_codes_from_output() { - let spans = ansi_to_spans("\x1b[32mgreen\x1b[0m", Style::default()); - assert_eq!(visible(&spans), "green"); - } - - #[test] - fn visible_text_strips_ansi() { - assert_eq!(ansi_visible_text("plain"), "plain"); - assert_eq!(ansi_visible_text("\x1b[31mred\x1b[0m"), "red"); - assert_eq!( - ansi_visible_text("\x1b[1;32mbold green\x1b[0m plain"), - "bold green plain" - ); - } - - #[test] - fn xterm_256_color_cube() { - assert_eq!(ansi_256(16), Color::Rgb(0, 0, 0)); - assert_eq!(ansi_256(21), Color::Rgb(0, 0, 255)); - assert_eq!(ansi_256(52), Color::Rgb(95, 0, 0)); - assert_eq!(ansi_256(67), Color::Rgb(95, 135, 175)); - } - - #[test] - fn true_color_24bit() { - let spans = ansi_to_spans( - "\x1b[38;2;100;200;50mtruecolor\x1b[0m", - Style::default(), - ); - assert_eq!(visible(&spans), "truecolor"); - assert_eq!(spans[0].style.fg, Some(Color::Rgb(100, 200, 50))); - } -} diff --git a/crates/pipeview-tui/src/app.rs b/crates/pipeview-tui/src/app.rs deleted file mode 100644 index 5e12e2f..0000000 --- a/crates/pipeview-tui/src/app.rs +++ /dev/null @@ -1,1782 +0,0 @@ -use std::io; -use std::time::Duration; - -use crossterm::event::{ - self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, - MouseEventKind, -}; -use pipeview_client::{ - DecoderConfig, FramerConfig, PipelineConfig, SessionConfig, SessionEvent, SessionHandle, - SessionId, SessionManager, -}; -use pipeview_core::protocol::DecodedData; -use pipeview_core::protocol::Endian; -use pipeview_core::protocol::plot::{PlotFormat, SampleType}; -use pipeview_core::protocol::text::TextEncoding; -use pipeview_core::transport::TransportConfig; -use pipeview_core::transport::serial::{ - SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits, SerialTransport, -}; -use ratatui::Terminal; -use ratatui::backend::CrosstermBackend; -use ratatui::layout::Rect; -use tokio::sync::broadcast; - -use crate::ansi::ansi_visible_text; -use crate::app_state; -use crate::buffers::{HexBuffer, PlotBuffer, TextBuffer}; - -const TICK_RATE: Duration = Duration::from_millis(33); -const DEFAULT_HISTORY_LIMIT: usize = 10_000; - -#[derive(Clone)] -pub enum ConnectionStatus { - Connected, - Disconnected, - Connecting, - Error(String), -} - -impl ConnectionStatus { - pub fn label(&self) -> &'static str { - match self { - Self::Connected => "CONNECTED", - Self::Disconnected => "OFFLINE", - Self::Connecting => "CONNECTING", - Self::Error(_) => "ERROR", - } - } - - pub fn is_connectedish(&self) -> bool { - matches!(self, Self::Connected | Self::Connecting) - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum View { - Text, - Hex, - Plot, -} - -impl View { - pub const ALL: [Self; 3] = [Self::Text, Self::Hex, Self::Plot]; - - pub fn label(self) -> &'static str { - match self { - Self::Text => "Text", - Self::Hex => "Hex", - Self::Plot => "Plot", - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum SendMode { - Text, - Hex, -} - -impl SendMode { - pub fn label(self) -> &'static str { - match self { - Self::Text => "Text", - Self::Hex => "Hex", - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -#[allow(clippy::upper_case_acronyms)] -pub enum LineEnding { - None, - LF, - CR, - CRLF, -} - -impl LineEnding { - pub const ALL: [Self; 4] = [Self::None, Self::LF, Self::CR, Self::CRLF]; - - pub fn label(self) -> &'static str { - match self { - Self::None => "None", - Self::LF => "LF", - Self::CR => "CR", - Self::CRLF => "CRLF", - } - } - - fn bytes(self) -> &'static [u8] { - match self { - Self::None => b"", - Self::LF => b"\n", - Self::CR => b"\r", - Self::CRLF => b"\r\n", - } - } - - fn next(self) -> Self { - match self { - Self::None => Self::LF, - Self::LF => Self::CR, - Self::CR => Self::CRLF, - Self::CRLF => Self::None, - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum FocusPane { - Controls, - Main, - Composer, -} - -impl FocusPane { - fn next(self) -> Self { - match self { - Self::Controls => Self::Main, - Self::Main => Self::Composer, - Self::Composer => Self::Controls, - } - } - - fn prev(self) -> Self { - match self { - Self::Controls => Self::Composer, - Self::Main => Self::Controls, - Self::Composer => Self::Main, - } - } -} - -#[derive(Clone, Copy)] -pub struct DisplayOptions { - pub show_timestamp: bool, - pub show_direction: bool, - pub show_pipeline: bool, -} - -#[derive(Default)] -pub struct SearchState { - pub query: String, - pub matches: Vec, - pub current_match: usize, - pub case_sensitive: bool, - pub active: bool, -} - -impl SearchState { - pub fn current_line(&self) -> Option { - self.matches.get(self.current_match).copied() - } - - pub fn count(&self) -> usize { - self.matches.len() - } - - pub fn display_index(&self) -> usize { - if self.matches.is_empty() { - 0 - } else { - self.current_match + 1 - } - } -} - -#[derive(Clone, Copy)] -pub enum AppMode { - Normal, - EditingSend, - Search, - Config, - Help, -} - -pub struct SingleSession { - pub id: SessionId, - pub config: SessionConfig, - pub status: ConnectionStatus, - pub console: TextBuffer, - pub hex: HexBuffer, - pub plot: PlotBuffer, - pub view: View, - pub send_input: String, - pub input_cursor: usize, - pub send_mode: SendMode, - pub line_ending: LineEnding, - pub send_status: String, - pub auto_reconnect: bool, - pub dtr: bool, - pub rts: bool, - pub received_messages: usize, - pub sent_messages: usize, -} - -impl SingleSession { - fn new(id: SessionId, config: SessionConfig, view: View) -> Self { - let history_limit = config.history_limit; - let (dtr, rts) = match &config.transport { - TransportConfig::Serial { dtr, rts, .. } => (*dtr, *rts), - _ => (false, false), - }; - - Self { - id, - auto_reconnect: config.auto_reconnect, - config, - status: ConnectionStatus::Connecting, - console: TextBuffer::new(history_limit), - hex: HexBuffer::new(history_limit), - plot: PlotBuffer::new(history_limit), - view, - send_input: String::new(), - input_cursor: 0, - send_mode: SendMode::Text, - line_ending: LineEnding::LF, - send_status: String::from("idle"), - dtr, - rts, - received_messages: 0, - sent_messages: 0, - } - } -} - -#[derive(Clone, Copy)] -pub enum TransportChoice { - Serial, - Tcp, - Udp, -} - -impl TransportChoice { - fn label(self) -> &'static str { - match self { - Self::Serial => "Serial", - Self::Tcp => "TCP", - Self::Udp => "UDP", - } - } - - fn next(self, forward: bool) -> Self { - match (self, forward) { - (Self::Serial, true) => Self::Tcp, - (Self::Tcp, true) => Self::Udp, - (Self::Udp, true) => Self::Serial, - (Self::Serial, false) => Self::Udp, - (Self::Tcp, false) => Self::Serial, - (Self::Udp, false) => Self::Tcp, - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum PipelinePreset { - TextHexLines, - TextLines, - HexLines, - MixedTextPlot, - PlotF32Fixed, -} - -impl PipelinePreset { - fn label(self) -> &'static str { - match self { - Self::TextHexLines => "Text + Hex lines", - Self::TextLines => "Text lines", - Self::HexLines => "Hex lines", - Self::MixedTextPlot => "Mixed text/plot", - Self::PlotF32Fixed => "Plot f32 fixed", - } - } - - fn next(self, forward: bool) -> Self { - match (self, forward) { - (Self::TextHexLines, true) => Self::TextLines, - (Self::TextLines, true) => Self::HexLines, - (Self::HexLines, true) => Self::MixedTextPlot, - (Self::MixedTextPlot, true) => Self::PlotF32Fixed, - (Self::PlotF32Fixed, true) => Self::TextHexLines, - (Self::TextHexLines, false) => Self::PlotF32Fixed, - (Self::TextLines, false) => Self::TextHexLines, - (Self::HexLines, false) => Self::TextLines, - (Self::MixedTextPlot, false) => Self::HexLines, - (Self::PlotF32Fixed, false) => Self::MixedTextPlot, - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum ConfigField { - Transport, - TcpAddr, - UdpBind, - UdpRemote, - SerialPort, - SerialBaud, - SerialDataBits, - SerialParity, - SerialStopBits, - SerialFlowControl, - PipelinePreset, - PipelineName, - HistoryLimit, - AutoReconnect, - PlotFrameLen, - PlotChannels, - Apply, -} - -pub struct ConfigRow { - pub field: ConfigField, - pub label: &'static str, - pub value: String, - pub editable: bool, -} - -pub struct ConfigForm { - pub transport: TransportChoice, - pub tcp_addr: String, - pub udp_bind_addr: String, - pub udp_remote_addr: String, - pub serial_port: String, - pub serial_baud_rate: String, - pub serial_data_bits: SerialDataBits, - pub serial_parity: SerialParity, - pub serial_stop_bits: SerialStopBits, - pub serial_flow_control: SerialFlowControl, - pub pipeline_preset: PipelinePreset, - pub pipeline_name: String, - pub history_limit: String, - pub auto_reconnect: bool, - pub plot_frame_len: String, - pub plot_channels: String, - pub focused: usize, - pub available_serial_ports: Vec, -} - -impl ConfigForm { - fn from_session(config: &SessionConfig) -> Self { - let available_serial_ports = available_serial_ports(); - let mut form = Self { - transport: TransportChoice::Tcp, - tcp_addr: String::from("127.0.0.1:8080"), - udp_bind_addr: String::from("0.0.0.0:9000"), - udp_remote_addr: String::new(), - serial_port: available_serial_ports.first().cloned().unwrap_or_default(), - serial_baud_rate: String::from("115200"), - serial_data_bits: SerialDataBits::Eight, - serial_parity: SerialParity::None, - serial_stop_bits: SerialStopBits::One, - serial_flow_control: SerialFlowControl::None, - pipeline_preset: infer_pipeline_preset(config), - pipeline_name: config - .pipelines - .first() - .map(|pipeline| pipeline.name.clone()) - .unwrap_or_else(|| String::from("text")), - history_limit: config.history_limit.to_string(), - auto_reconnect: config.auto_reconnect, - plot_frame_len: String::from("4"), - plot_channels: String::from("1"), - focused: 0, - available_serial_ports, - }; - - match &config.transport { - TransportConfig::Tcp { addr } => { - form.transport = TransportChoice::Tcp; - form.tcp_addr = addr.clone(); - } - TransportConfig::Udp { - bind_addr, - remote_addr, - } => { - form.transport = TransportChoice::Udp; - form.udp_bind_addr = bind_addr.clone(); - form.udp_remote_addr = remote_addr.clone().unwrap_or_default(); - } - TransportConfig::Serial { - port, - baud_rate, - data_bits, - parity, - stop_bits, - flow_control, - .. - } => { - form.transport = TransportChoice::Serial; - form.serial_port = port.clone(); - form.serial_baud_rate = baud_rate.to_string(); - form.serial_data_bits = *data_bits; - form.serial_parity = *parity; - form.serial_stop_bits = *stop_bits; - form.serial_flow_control = *flow_control; - } - } - - if let Some(pipeline) = config.pipelines.first() { - if let FramerConfig::Fixed { frame_len } = &pipeline.framer { - form.plot_frame_len = frame_len.to_string(); - } - if let DecoderConfig::Plot { channels, .. } = &pipeline.decoder { - form.plot_channels = channels.to_string(); - } - } - - form - } - - pub fn rows(&self) -> Vec { - self.fields() - .into_iter() - .map(|field| ConfigRow { - field, - label: field.label(), - value: self.field_value(field), - editable: self.is_text_field(field), - }) - .collect() - } - - pub fn focused_field(&self) -> ConfigField { - let fields = self.fields(); - fields[self.focused.min(fields.len().saturating_sub(1))] - } - - fn fields(&self) -> Vec { - let mut fields = vec![ConfigField::Transport]; - match self.transport { - TransportChoice::Tcp => fields.push(ConfigField::TcpAddr), - TransportChoice::Udp => { - fields.push(ConfigField::UdpBind); - fields.push(ConfigField::UdpRemote); - } - TransportChoice::Serial => { - fields.push(ConfigField::SerialPort); - fields.push(ConfigField::SerialBaud); - fields.push(ConfigField::SerialDataBits); - fields.push(ConfigField::SerialParity); - fields.push(ConfigField::SerialStopBits); - fields.push(ConfigField::SerialFlowControl); - } - } - fields.push(ConfigField::PipelinePreset); - fields.push(ConfigField::PipelineName); - fields.push(ConfigField::HistoryLimit); - fields.push(ConfigField::AutoReconnect); - if self.pipeline_preset == PipelinePreset::PlotF32Fixed { - fields.push(ConfigField::PlotFrameLen); - fields.push(ConfigField::PlotChannels); - } - fields.push(ConfigField::Apply); - fields - } - - fn field_value(&self, field: ConfigField) -> String { - match field { - ConfigField::Transport => self.transport.label().to_string(), - ConfigField::TcpAddr => self.tcp_addr.clone(), - ConfigField::UdpBind => self.udp_bind_addr.clone(), - ConfigField::UdpRemote => { - if self.udp_remote_addr.is_empty() { - String::from("") - } else { - self.udp_remote_addr.clone() - } - } - ConfigField::SerialPort => { - if self.serial_port.is_empty() { - String::from("") - } else { - self.serial_port.clone() - } - } - ConfigField::SerialBaud => self.serial_baud_rate.clone(), - ConfigField::SerialDataBits => serial_data_bits_name(self.serial_data_bits).to_string(), - ConfigField::SerialParity => serial_parity_name(self.serial_parity).to_string(), - ConfigField::SerialStopBits => serial_stop_bits_name(self.serial_stop_bits).to_string(), - ConfigField::SerialFlowControl => { - serial_flow_control_name(self.serial_flow_control).to_string() - } - ConfigField::PipelinePreset => self.pipeline_preset.label().to_string(), - ConfigField::PipelineName => self.pipeline_name.clone(), - ConfigField::HistoryLimit => self.history_limit.clone(), - ConfigField::AutoReconnect => bool_label(self.auto_reconnect).to_string(), - ConfigField::PlotFrameLen => self.plot_frame_len.clone(), - ConfigField::PlotChannels => self.plot_channels.clone(), - ConfigField::Apply => String::from("Apply and reconnect"), - } - } - - fn is_text_field(&self, field: ConfigField) -> bool { - matches!( - field, - ConfigField::TcpAddr - | ConfigField::UdpBind - | ConfigField::UdpRemote - | ConfigField::SerialPort - | ConfigField::SerialBaud - | ConfigField::PipelineName - | ConfigField::HistoryLimit - | ConfigField::PlotFrameLen - | ConfigField::PlotChannels - ) - } - - fn focus_field(&mut self, field: ConfigField) { - if let Some(index) = self.fields().into_iter().position(|item| item == field) { - self.focused = index; - } - } - - fn next_field(&mut self) { - let count = self.fields().len(); - self.focused = (self.focused + 1) % count; - } - - fn prev_field(&mut self) { - let count = self.fields().len(); - self.focused = if self.focused == 0 { - count.saturating_sub(1) - } else { - self.focused - 1 - }; - } - - fn cycle_current_option(&mut self, forward: bool) { - match self.focused_field() { - ConfigField::Transport => { - self.transport = self.transport.next(forward); - self.focused = self.focused.min(self.fields().len().saturating_sub(1)); - } - ConfigField::SerialPort => self.cycle_serial_port(forward), - ConfigField::SerialDataBits => { - self.serial_data_bits = next_serial_data_bits(self.serial_data_bits, forward); - } - ConfigField::SerialParity => { - self.serial_parity = next_serial_parity(self.serial_parity, forward); - } - ConfigField::SerialStopBits => { - self.serial_stop_bits = next_serial_stop_bits(self.serial_stop_bits, forward); - } - ConfigField::SerialFlowControl => { - self.serial_flow_control = - next_serial_flow_control(self.serial_flow_control, forward); - } - ConfigField::PipelinePreset => { - self.pipeline_preset = self.pipeline_preset.next(forward); - self.focused = self.focused.min(self.fields().len().saturating_sub(1)); - } - ConfigField::AutoReconnect => self.auto_reconnect = !self.auto_reconnect, - _ => {} - } - } - - fn cycle_serial_port(&mut self, forward: bool) { - if self.available_serial_ports.is_empty() { - return; - } - - let next_index = self - .available_serial_ports - .iter() - .position(|port| port == &self.serial_port) - .map(|index| { - if forward { - (index + 1) % self.available_serial_ports.len() - } else if index == 0 { - self.available_serial_ports.len() - 1 - } else { - index - 1 - } - }) - .unwrap_or(0); - self.serial_port = self.available_serial_ports[next_index].clone(); - } - - fn focused_text_field_mut(&mut self) -> Option<&mut String> { - match self.focused_field() { - ConfigField::TcpAddr => Some(&mut self.tcp_addr), - ConfigField::UdpBind => Some(&mut self.udp_bind_addr), - ConfigField::UdpRemote => Some(&mut self.udp_remote_addr), - ConfigField::SerialPort => Some(&mut self.serial_port), - ConfigField::SerialBaud => Some(&mut self.serial_baud_rate), - ConfigField::PipelineName => Some(&mut self.pipeline_name), - ConfigField::HistoryLimit => Some(&mut self.history_limit), - ConfigField::PlotFrameLen => Some(&mut self.plot_frame_len), - ConfigField::PlotChannels => Some(&mut self.plot_channels), - _ => None, - } - } - - fn accept_char(&mut self, c: char) { - let numeric = matches!( - self.focused_field(), - ConfigField::SerialBaud - | ConfigField::HistoryLimit - | ConfigField::PlotFrameLen - | ConfigField::PlotChannels - ); - if let Some(field) = self.focused_text_field_mut() { - if numeric && !c.is_ascii_digit() { - return; - } - field.push(c); - } else if c == ' ' { - self.cycle_current_option(true); - } - } - - fn backspace(&mut self) { - if let Some(field) = self.focused_text_field_mut() { - field.pop(); - } - } - - fn to_session_config(&self, dtr: bool, rts: bool) -> Result { - let history_limit = parse_usize(&self.history_limit, "history limit")?.max(1); - let pipeline_name = self.pipeline_name.trim(); - if pipeline_name.is_empty() { - return Err(String::from("pipeline name cannot be empty")); - } - - let transport = match self.transport { - TransportChoice::Tcp => { - let addr = self.tcp_addr.trim(); - if addr.is_empty() { - return Err(String::from("tcp addr cannot be empty")); - } - TransportConfig::Tcp { - addr: addr.to_string(), - } - } - TransportChoice::Udp => { - let bind_addr = self.udp_bind_addr.trim(); - if bind_addr.is_empty() { - return Err(String::from("udp bind addr cannot be empty")); - } - let remote_addr = if self.udp_remote_addr.trim().is_empty() { - None - } else { - Some(self.udp_remote_addr.trim().to_string()) - }; - TransportConfig::Udp { - bind_addr: bind_addr.to_string(), - remote_addr, - } - } - TransportChoice::Serial => { - let port = self.serial_port.trim(); - if port.is_empty() { - return Err(String::from("serial port cannot be empty")); - } - TransportConfig::Serial { - port: port.to_string(), - baud_rate: parse_u32(&self.serial_baud_rate, "serial baud rate")?, - data_bits: self.serial_data_bits, - parity: self.serial_parity, - stop_bits: self.serial_stop_bits, - flow_control: self.serial_flow_control, - dtr, - rts, - } - } - }; - - Ok(SessionConfig { - transport, - pipelines: build_pipelines( - self.pipeline_preset, - pipeline_name, - parse_usize(&self.plot_frame_len, "plot frame length")?, - parse_usize(&self.plot_channels, "plot channels")?.max(1), - ), - history_limit, - auto_reconnect: self.auto_reconnect, - }) - } -} - -impl ConfigField { - fn label(self) -> &'static str { - match self { - Self::Transport => "Transport", - Self::TcpAddr => "TCP address", - Self::UdpBind => "UDP bind", - Self::UdpRemote => "UDP remote", - Self::SerialPort => "Serial port", - Self::SerialBaud => "Baud rate", - Self::SerialDataBits => "Data bits", - Self::SerialParity => "Parity", - Self::SerialStopBits => "Stop bits", - Self::SerialFlowControl => "Flow control", - Self::PipelinePreset => "Pipeline", - Self::PipelineName => "Pipeline name", - Self::HistoryLimit => "History", - Self::AutoReconnect => "Auto reconnect", - Self::PlotFrameLen => "Plot frame bytes", - Self::PlotChannels => "Plot channels", - Self::Apply => "Commit", - } - } -} - -pub struct PlotViewState { - pub follow_latest: bool, - pub x_window: f64, -} - -impl Default for PlotViewState { - fn default() -> Self { - Self { - follow_latest: true, - x_window: 500.0, - } - } -} - -#[derive(Clone, Copy)] -pub enum HotAction { - Focus(FocusPane), - View(View), - ToggleConnection, - Reconnect, - Clear, - OpenConfig, - OpenSearch, - OpenHelp, - Send, - ToggleAutoReconnect, - ToggleDtr, - ToggleRts, - ToggleTimestamp, - ToggleDirection, - TogglePipeline, - SendMode(SendMode), - LineEnding(LineEnding), - PlotFollow, - PlotZoomIn, - PlotZoomOut, - ConfigFocus(ConfigField), - ConfigSubmit, - ConfigCancel, - CloseModal, -} - -#[derive(Clone, Copy)] -pub struct HotZone { - pub area: Rect, - pub action: HotAction, -} - -pub struct App { - pub should_quit: bool, - pub mode: AppMode, - pub manager: SessionManager, - pub handle: SessionHandle, - pub session: SingleSession, - pub display: DisplayOptions, - pub search: SearchState, - pub config_form: ConfigForm, - pub focus: FocusPane, - pub scroll_from_bottom: usize, - pub plot: PlotViewState, - pub notice: String, - pub hot_zones: Vec, - session_rx: broadcast::Receiver, -} - -impl App { - pub fn new() -> Self { - let saved = app_state::load_tui_state(); - let manager = SessionManager::new(); - let session_rx = manager.subscribe(); - let handle = manager.create(saved.session.clone()); - let display = DisplayOptions { - show_timestamp: saved.show_timestamp, - show_direction: saved.show_direction, - show_pipeline: saved.show_pipeline, - }; - let session = - SingleSession::new(handle.id(), saved.session.clone(), saved.active_view.into()); - let config_form = ConfigForm::from_session(&saved.session); - - Self { - should_quit: false, - mode: AppMode::Normal, - manager, - handle, - session, - display, - search: SearchState::default(), - config_form, - focus: FocusPane::Main, - scroll_from_bottom: 0, - plot: PlotViewState::default(), - notice: String::from("Ready"), - hot_zones: Vec::new(), - session_rx, - } - } - - pub async fn run( - &mut self, - terminal: &mut Terminal>, - ) -> io::Result<()> { - loop { - self.drain_events(); - terminal.draw(|frame| crate::ui::render(frame, self))?; - - if self.should_quit { - break; - } - - if event::poll(TICK_RATE)? { - match event::read()? { - Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key(key), - Event::Mouse(mouse) => self.on_mouse(mouse), - Event::Resize(_, _) => {} - _ => {} - } - } - } - - Ok(()) - } - - pub async fn shutdown(&mut self) { - let _ = self.handle.close().await; - self.manager.remove(self.session.id); - } - - pub fn reset_hot_zones(&mut self) { - self.hot_zones.clear(); - } - - pub fn add_hot_zone(&mut self, area: Rect, action: HotAction) { - if area.width > 0 && area.height > 0 { - self.hot_zones.push(HotZone { area, action }); - } - } - - pub fn active_line_count(&self) -> usize { - match self.session.view { - View::Text => self.session.console.len(), - View::Hex => self.session.hex.len(), - View::Plot => 0, - } - } - - pub fn visible_start(&self, total: usize, viewport_height: usize) -> usize { - if viewport_height == 0 || total <= viewport_height { - return 0; - } - total - .saturating_sub(viewport_height) - .saturating_sub(self.scroll_from_bottom) - } - - pub fn persist_state(&self) { - app_state::save_tui_state(&self.session.config, self.session.view, self.display); - } - - pub fn transport_summary(&self) -> String { - transport_summary(&self.session.config.transport) - } - - pub fn pipeline_summary(&self) -> String { - let names = self - .session - .config - .pipelines - .iter() - .map(|pipeline| pipeline.name.as_str()) - .collect::>(); - if names.is_empty() { - String::from("no pipelines") - } else { - names.join(", ") - } - } - - pub fn set_notice(&mut self, message: impl Into) { - self.notice = message.into(); - } - - pub fn plot_bounds(&mut self) -> Option<([f64; 2], [f64; 2])> { - let (min, max) = self.session.plot.bounds()?; - if self.plot.follow_latest && max[0].is_finite() && self.plot.x_window > 0.0 { - let min_x = (max[0] - self.plot.x_window).max(min[0]); - Some(([min_x, min[1]], max)) - } else { - Some((min, max)) - } - } - - fn drain_events(&mut self) { - while let Ok(event) = self.session_rx.try_recv() { - match event { - SessionEvent::Connected(id) if id == self.session.id => { - self.session.status = ConnectionStatus::Connected; - self.set_notice("Connected"); - } - SessionEvent::Disconnected(id) | SessionEvent::Closed(id) - if id == self.session.id => - { - self.session.status = ConnectionStatus::Disconnected; - self.set_notice("Disconnected"); - } - SessionEvent::Error(id, message) if id == self.session.id => { - self.session.status = ConnectionStatus::Error(message.clone()); - self.set_notice(message); - } - SessionEvent::Data(id, entry) if id == self.session.id => { - self.session.received_messages += 1; - match &entry.data { - DecodedData::Text(_) => self.session.console.push(&entry), - DecodedData::Hex(_) => self.session.hex.push(&entry), - DecodedData::Plot(_) => self.session.plot.push(&entry), - DecodedData::Binary(bytes) => { - self.session.send_status = - format!("received binary frame: {} bytes", bytes.len()); - } - } - if self.search.active && !self.search.query.is_empty() { - self.rebuild_search(); - } - } - _ => {} - } - } - } - - fn on_key(&mut self, key: KeyEvent) { - match self.mode { - AppMode::Normal => self.on_normal_key(key), - AppMode::EditingSend => self.on_send_key(key), - AppMode::Search => self.on_search_key(key), - AppMode::Config => self.on_config_key(key), - AppMode::Help => { - if matches!(key.code, KeyCode::Esc | KeyCode::Char('?') | KeyCode::Enter) { - self.mode = AppMode::Normal; - } - } - } - } - - fn on_normal_key(&mut self, key: KeyEvent) { - if key.modifiers == KeyModifiers::CONTROL { - match key.code { - KeyCode::Char('s') => self.send_active_input(), - KeyCode::Char('l') => self.clear_buffers(), - KeyCode::Char('f') => self.open_search(), - _ => {} - } - return; - } - - match key.code { - KeyCode::Char('q') => self.should_quit = true, - KeyCode::Char('?') => self.mode = AppMode::Help, - KeyCode::Esc => { - self.search.active = false; - self.set_notice("Search cleared"); - } - KeyCode::Tab => self.focus = self.focus.next(), - KeyCode::BackTab => self.focus = self.focus.prev(), - KeyCode::Char('1') => self.set_view(View::Text), - KeyCode::Char('2') => self.set_view(View::Hex), - KeyCode::Char('3') => self.set_view(View::Plot), - KeyCode::Char('c') => self.toggle_connection(), - KeyCode::Char('r') => self.reconnect(), - KeyCode::Char('e') => self.open_config(), - KeyCode::Char('/') => self.open_search(), - KeyCode::Char('x') => self.clear_buffers(), - KeyCode::Char('m') => self.toggle_send_mode(), - KeyCode::Char('l') => self.cycle_line_ending(), - KeyCode::Char('a') => self.toggle_auto_reconnect(), - KeyCode::Char('d') => self.toggle_dtr(), - KeyCode::Char('s') => self.toggle_rts(), - KeyCode::Char('t') => self.toggle_timestamp(), - KeyCode::Char('o') => self.toggle_direction(), - KeyCode::Char('p') => self.toggle_pipeline_display(), - KeyCode::Char('f') if self.session.view == View::Plot => self.toggle_plot_follow(), - KeyCode::Char('+') | KeyCode::Char('=') if self.session.view == View::Plot => { - self.zoom_plot(true); - } - KeyCode::Char('-') if self.session.view == View::Plot => self.zoom_plot(false), - KeyCode::Char('n') if self.search.active => self.next_match(), - KeyCode::Char('N') if self.search.active => self.prev_match(), - KeyCode::Up | KeyCode::Char('k') => self.scroll_up(1), - KeyCode::Down | KeyCode::Char('j') => self.scroll_down(1), - KeyCode::PageUp => self.scroll_up(10), - KeyCode::PageDown => self.scroll_down(10), - KeyCode::Home => self.scroll_to_top(), - KeyCode::End => self.scroll_to_bottom(), - KeyCode::Enter if self.focus == FocusPane::Composer => self.mode = AppMode::EditingSend, - KeyCode::Char(c) if self.focus == FocusPane::Composer && key.modifiers.is_empty() => { - self.mode = AppMode::EditingSend; - self.insert_send_char(c); - } - _ => {} - } - } - - fn on_send_key(&mut self, key: KeyEvent) { - match key.code { - KeyCode::Esc => self.mode = AppMode::Normal, - KeyCode::Enter if key.modifiers == KeyModifiers::CONTROL => self.insert_send_char('\n'), - KeyCode::Enter => { - self.send_active_input(); - self.mode = AppMode::Normal; - } - KeyCode::Backspace => self.backspace_send_input(), - KeyCode::Delete => self.delete_send_input(), - KeyCode::Left => self.move_send_cursor_left(), - KeyCode::Right => self.move_send_cursor_right(), - KeyCode::Home => self.session.input_cursor = 0, - KeyCode::End => self.session.input_cursor = self.send_input_char_len(), - KeyCode::Tab => self.insert_send_char('\t'), - KeyCode::Char(c) - if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => - { - self.insert_send_char(c); - } - _ => {} - } - } - - fn on_search_key(&mut self, key: KeyEvent) { - match key.code { - KeyCode::Esc => self.mode = AppMode::Normal, - KeyCode::Enter => { - self.search.active = !self.search.query.is_empty(); - self.mode = AppMode::Normal; - self.focus_current_match(); - } - KeyCode::Backspace => { - self.search.query.pop(); - self.rebuild_search(); - } - KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { - self.search.case_sensitive = !self.search.case_sensitive; - self.rebuild_search(); - } - KeyCode::Char(c) - if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => - { - self.search.query.push(c); - self.search.active = true; - self.rebuild_search(); - } - KeyCode::Down => self.next_match(), - KeyCode::Up => self.prev_match(), - _ => {} - } - } - - fn on_config_key(&mut self, key: KeyEvent) { - match key.code { - KeyCode::Esc => self.mode = AppMode::Normal, - KeyCode::Tab | KeyCode::Down => self.config_form.next_field(), - KeyCode::BackTab | KeyCode::Up => self.config_form.prev_field(), - KeyCode::Left => self.config_form.cycle_current_option(false), - KeyCode::Right => self.config_form.cycle_current_option(true), - KeyCode::Backspace => self.config_form.backspace(), - KeyCode::Char(c) - if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT => - { - self.config_form.accept_char(c); - } - KeyCode::Enter => self.apply_config(), - _ => {} - } - } - - fn on_mouse(&mut self, mouse: MouseEvent) { - match mouse.kind { - MouseEventKind::ScrollUp => { - if self.session.view == View::Plot { - self.zoom_plot(true); - } else { - self.scroll_up(3); - } - } - MouseEventKind::ScrollDown => { - if self.session.view == View::Plot { - self.zoom_plot(false); - } else { - self.scroll_down(3); - } - } - MouseEventKind::Down(MouseButton::Left) => { - let action = self - .hot_zones - .iter() - .rev() - .find(|zone| point_in_rect(mouse.column, mouse.row, zone.area)) - .map(|zone| zone.action); - if let Some(action) = action { - self.execute_hot_action(action); - } - } - _ => {} - } - } - - fn execute_hot_action(&mut self, action: HotAction) { - match action { - HotAction::Focus(focus) => self.focus = focus, - HotAction::View(view) => self.set_view(view), - HotAction::ToggleConnection => self.toggle_connection(), - HotAction::Reconnect => self.reconnect(), - HotAction::Clear => self.clear_buffers(), - HotAction::OpenConfig => self.open_config(), - HotAction::OpenSearch => self.open_search(), - HotAction::OpenHelp => self.mode = AppMode::Help, - HotAction::Send => self.send_active_input(), - HotAction::ToggleAutoReconnect => self.toggle_auto_reconnect(), - HotAction::ToggleDtr => self.toggle_dtr(), - HotAction::ToggleRts => self.toggle_rts(), - HotAction::ToggleTimestamp => self.toggle_timestamp(), - HotAction::ToggleDirection => self.toggle_direction(), - HotAction::TogglePipeline => self.toggle_pipeline_display(), - HotAction::SendMode(mode) => self.session.send_mode = mode, - HotAction::LineEnding(ending) => self.session.line_ending = ending, - HotAction::PlotFollow => self.toggle_plot_follow(), - HotAction::PlotZoomIn => self.zoom_plot(true), - HotAction::PlotZoomOut => self.zoom_plot(false), - HotAction::ConfigFocus(field) => { - self.mode = AppMode::Config; - self.config_form.focus_field(field); - } - HotAction::ConfigSubmit => self.apply_config(), - HotAction::ConfigCancel | HotAction::CloseModal => self.mode = AppMode::Normal, - } - } - - fn set_view(&mut self, view: View) { - self.session.view = view; - self.scroll_from_bottom = 0; - self.rebuild_search(); - self.persist_state(); - } - - fn toggle_connection(&mut self) { - let handle = self.handle.clone(); - if self.session.status.is_connectedish() { - self.session.status = ConnectionStatus::Disconnected; - self.set_notice("Disconnect requested"); - tokio::spawn(async move { - let _ = handle.disconnect().await; - }); - } else { - self.session.status = ConnectionStatus::Connecting; - self.set_notice("Connect requested"); - tokio::spawn(async move { - let _ = handle.connect().await; - }); - } - } - - fn reconnect(&mut self) { - self.session.status = ConnectionStatus::Connecting; - self.set_notice("Reconnect requested"); - let handle = self.handle.clone(); - tokio::spawn(async move { - let _ = handle.reconnect().await; - }); - } - - fn clear_buffers(&mut self) { - self.session.console.clear(); - self.session.hex.clear(); - self.session.plot.clear(); - self.scroll_from_bottom = 0; - self.search.matches.clear(); - self.session.send_status = String::from("cleared"); - self.set_notice("Buffers cleared"); - } - - fn open_config(&mut self) { - self.config_form = ConfigForm::from_session(&self.session.config); - self.mode = AppMode::Config; - } - - fn open_search(&mut self) { - self.mode = AppMode::Search; - self.search.active = true; - self.rebuild_search(); - } - - fn toggle_send_mode(&mut self) { - self.session.send_mode = match self.session.send_mode { - SendMode::Text => SendMode::Hex, - SendMode::Hex => SendMode::Text, - }; - } - - fn cycle_line_ending(&mut self) { - self.session.line_ending = self.session.line_ending.next(); - } - - fn toggle_auto_reconnect(&mut self) { - self.session.auto_reconnect = !self.session.auto_reconnect; - self.session.config.auto_reconnect = self.session.auto_reconnect; - let enabled = self.session.auto_reconnect; - self.persist_state(); - - let handle = self.handle.clone(); - tokio::spawn(async move { - let _ = handle.set_auto_reconnect(enabled).await; - }); - } - - fn toggle_dtr(&mut self) { - if !matches!( - self.session.config.transport, - TransportConfig::Serial { .. } - ) { - self.set_notice("DTR is only available for serial sessions"); - return; - } - - self.session.dtr = !self.session.dtr; - if let TransportConfig::Serial { dtr, .. } = &mut self.session.config.transport { - *dtr = self.session.dtr; - } - self.persist_state(); - - let handle = self.handle.clone(); - let state = self.session.dtr; - tokio::spawn(async move { - let _ = handle.set_dtr(state).await; - }); - } - - fn toggle_rts(&mut self) { - if !matches!( - self.session.config.transport, - TransportConfig::Serial { .. } - ) { - self.set_notice("RTS is only available for serial sessions"); - return; - } - - self.session.rts = !self.session.rts; - if let TransportConfig::Serial { rts, .. } = &mut self.session.config.transport { - *rts = self.session.rts; - } - self.persist_state(); - - let handle = self.handle.clone(); - let state = self.session.rts; - tokio::spawn(async move { - let _ = handle.set_rts(state).await; - }); - } - - fn toggle_timestamp(&mut self) { - self.display.show_timestamp = !self.display.show_timestamp; - self.persist_state(); - } - - fn toggle_direction(&mut self) { - self.display.show_direction = !self.display.show_direction; - self.persist_state(); - } - - fn toggle_pipeline_display(&mut self) { - self.display.show_pipeline = !self.display.show_pipeline; - self.persist_state(); - } - - fn toggle_plot_follow(&mut self) { - self.plot.follow_latest = !self.plot.follow_latest; - } - - fn zoom_plot(&mut self, zoom_in: bool) { - self.plot.x_window = if zoom_in { - (self.plot.x_window * 0.75).max(20.0) - } else { - (self.plot.x_window * 1.35).min(100_000.0) - }; - } - - fn apply_config(&mut self) { - let config = match self - .config_form - .to_session_config(self.session.dtr, self.session.rts) - { - Ok(config) => config, - Err(message) => { - self.set_notice(message); - return; - } - }; - - self.session.config = config.clone(); - self.session.auto_reconnect = config.auto_reconnect; - self.session.console.set_limit(config.history_limit); - self.session.hex.set_limit(config.history_limit); - self.session.plot.set_limit(config.history_limit); - self.session.status = ConnectionStatus::Connecting; - self.session.send_status = String::from("session reconfigured"); - self.mode = AppMode::Normal; - self.persist_state(); - - let handle = self.handle.clone(); - tokio::spawn(async move { - let _ = handle.reconfigure(config).await; - }); - } - - fn send_active_input(&mut self) { - let payload = match self.build_payload() { - Ok(Some(payload)) => payload, - Ok(None) => { - self.session.send_status = String::from("nothing to send"); - return; - } - Err(message) => { - self.session.send_status = format!("send failed: {message}"); - return; - } - }; - - match self.session.send_mode { - SendMode::Text => { - let text = self.session.send_input.clone(); - if !text.is_empty() { - self.session.console.push_outbound(text); - } - } - SendMode::Hex => { - let compact = self - .session - .send_input - .split_whitespace() - .collect::>() - .join(" "); - if !compact.is_empty() { - self.session.hex.push_outbound(compact); - } - } - } - - self.session.sent_messages += 1; - self.session.send_input.clear(); - self.session.input_cursor = 0; - self.session.send_status = String::from("sent"); - self.scroll_from_bottom = 0; - - let handle = self.handle.clone(); - tokio::spawn(async move { - let _ = handle.send(payload).await; - }); - } - - fn build_payload(&self) -> Result>, String> { - if self.session.send_input.trim().is_empty() { - return Ok(None); - } - - match self.session.send_mode { - SendMode::Text => { - let mut payload = self.session.send_input.clone().into_bytes(); - payload.extend_from_slice(self.session.line_ending.bytes()); - Ok(Some(payload)) - } - SendMode::Hex => { - let compact: String = self - .session - .send_input - .chars() - .filter(|ch| !ch.is_ascii_whitespace()) - .collect(); - if !compact.len().is_multiple_of(2) { - return Err(String::from("hex input needs an even number of digits")); - } - hex::decode(compact) - .map(Some) - .map_err(|err| format!("invalid hex input ({err})")) - } - } - } - - fn rebuild_search(&mut self) { - if self.search.query.is_empty() { - self.search.matches.clear(); - self.search.current_match = 0; - return; - } - - let query = if self.search.case_sensitive { - self.search.query.clone() - } else { - self.search.query.to_lowercase() - }; - - let total = self.active_line_count(); - self.search.matches = (0..total) - .filter(|index| { - let line = match self.session.view { - View::Text => self - .session - .console - .get(*index) - .map(|line| ansi_visible_text(&line.text)) - .unwrap_or_default(), - View::Hex => self - .session - .hex - .get(*index) - .map(|line| format!("{} {}", line.hex, line.ascii)) - .unwrap_or_default(), - View::Plot => String::new(), - }; - - if self.search.case_sensitive { - line.contains(&query) - } else { - line.to_lowercase().contains(&query) - } - }) - .collect(); - - if self.search.current_match >= self.search.matches.len() { - self.search.current_match = self.search.matches.len().saturating_sub(1); - } - } - - fn next_match(&mut self) { - if self.search.matches.is_empty() { - return; - } - self.search.current_match = (self.search.current_match + 1) % self.search.matches.len(); - self.focus_current_match(); - } - - fn prev_match(&mut self) { - if self.search.matches.is_empty() { - return; - } - self.search.current_match = if self.search.current_match == 0 { - self.search.matches.len() - 1 - } else { - self.search.current_match - 1 - }; - self.focus_current_match(); - } - - fn focus_current_match(&mut self) { - let Some(index) = self.search.current_line() else { - return; - }; - let total = self.active_line_count(); - self.scroll_from_bottom = total.saturating_sub(index + 1); - } - - fn scroll_up(&mut self, lines: usize) { - let total = self.active_line_count(); - self.scroll_from_bottom = (self.scroll_from_bottom + lines).min(total.saturating_sub(1)); - } - - fn scroll_down(&mut self, lines: usize) { - self.scroll_from_bottom = self.scroll_from_bottom.saturating_sub(lines); - } - - fn scroll_to_top(&mut self) { - self.scroll_from_bottom = self.active_line_count().saturating_sub(1); - } - - fn scroll_to_bottom(&mut self) { - self.scroll_from_bottom = 0; - } - - fn insert_send_char(&mut self, c: char) { - let byte_index = byte_index_for_char(&self.session.send_input, self.session.input_cursor); - self.session.send_input.insert(byte_index, c); - self.session.input_cursor += 1; - } - - fn backspace_send_input(&mut self) { - if self.session.input_cursor == 0 { - return; - } - let remove_at = - byte_index_for_char(&self.session.send_input, self.session.input_cursor - 1); - self.session.send_input.remove(remove_at); - self.session.input_cursor -= 1; - } - - fn delete_send_input(&mut self) { - if self.session.input_cursor >= self.send_input_char_len() { - return; - } - let remove_at = byte_index_for_char(&self.session.send_input, self.session.input_cursor); - self.session.send_input.remove(remove_at); - } - - fn move_send_cursor_left(&mut self) { - self.session.input_cursor = self.session.input_cursor.saturating_sub(1); - } - - fn move_send_cursor_right(&mut self) { - self.session.input_cursor = (self.session.input_cursor + 1).min(self.send_input_char_len()); - } - - fn send_input_char_len(&self) -> usize { - self.session.send_input.chars().count() - } -} - -pub fn default_session_config() -> SessionConfig { - SessionConfig { - transport: TransportConfig::Tcp { - addr: String::from("127.0.0.1:8080"), - }, - pipelines: build_pipelines(PipelinePreset::TextHexLines, "text", 4, 1), - history_limit: DEFAULT_HISTORY_LIMIT, - auto_reconnect: false, - } -} - -fn build_pipelines( - preset: PipelinePreset, - pipeline_name: &str, - plot_frame_len: usize, - plot_channels: usize, -) -> Vec { - match preset { - PipelinePreset::TextHexLines => vec![ - PipelineConfig { - name: String::from("text"), - framer: FramerConfig::Line { - strip_cr: true, - max_line_len: 1_048_576, - }, - decoder: DecoderConfig::Text { - encoding: TextEncoding::Utf8, - }, - }, - PipelineConfig { - name: String::from("hex"), - framer: FramerConfig::Line { - strip_cr: true, - max_line_len: 1_048_576, - }, - decoder: DecoderConfig::Hex { - uppercase: false, - separator: String::from(" "), - bytes_per_group: 1, - endian: Endian::Big, - }, - }, - ], - PipelinePreset::TextLines => vec![PipelineConfig { - name: pipeline_name.to_string(), - framer: FramerConfig::Line { - strip_cr: true, - max_line_len: 1_048_576, - }, - decoder: DecoderConfig::Text { - encoding: TextEncoding::Utf8, - }, - }], - PipelinePreset::HexLines => vec![PipelineConfig { - name: pipeline_name.to_string(), - framer: FramerConfig::Line { - strip_cr: true, - max_line_len: 1_048_576, - }, - decoder: DecoderConfig::Hex { - uppercase: false, - separator: String::from(" "), - bytes_per_group: 1, - endian: Endian::Big, - }, - }], - PipelinePreset::MixedTextPlot => vec![PipelineConfig { - name: pipeline_name.to_string(), - framer: FramerConfig::MixedTextPlot { - strip_cr: true, - max_line_len: 1_048_576, - max_plot_frame: 1_048_576, - }, - decoder: DecoderConfig::MixedTextPlot { - encoding: TextEncoding::Utf8, - }, - }], - PipelinePreset::PlotF32Fixed => vec![PipelineConfig { - name: pipeline_name.to_string(), - framer: FramerConfig::Fixed { - frame_len: plot_frame_len.max(4), - }, - decoder: DecoderConfig::Plot { - sample_type: SampleType::F32, - endian: Endian::Little, - channels: plot_channels.max(1), - format: PlotFormat::Interleaved, - }, - }], - } -} - -fn infer_pipeline_preset(config: &SessionConfig) -> PipelinePreset { - let has_text = config - .pipelines - .iter() - .any(|pipeline| matches!(pipeline.decoder, DecoderConfig::Text { .. })); - let has_hex = config - .pipelines - .iter() - .any(|pipeline| matches!(pipeline.decoder, DecoderConfig::Hex { .. })); - if has_text && has_hex { - return PipelinePreset::TextHexLines; - } - - config - .pipelines - .first() - .map(|pipeline| match pipeline.decoder { - DecoderConfig::Hex { .. } => PipelinePreset::HexLines, - DecoderConfig::Plot { .. } => PipelinePreset::PlotF32Fixed, - DecoderConfig::MixedTextPlot { .. } => PipelinePreset::MixedTextPlot, - DecoderConfig::Text { .. } | DecoderConfig::Lua { .. } => PipelinePreset::TextLines, - }) - .unwrap_or(PipelinePreset::TextHexLines) -} - -fn transport_summary(config: &TransportConfig) -> String { - match config { - TransportConfig::Serial { - port, baud_rate, .. - } => format!("Serial {port} @ {baud_rate}"), - TransportConfig::Tcp { addr } => format!("TCP {addr}"), - TransportConfig::Udp { - bind_addr, - remote_addr, - } => match remote_addr { - Some(remote) => format!("UDP {bind_addr} -> {remote}"), - None => format!("UDP {bind_addr}"), - }, - } -} - -fn available_serial_ports() -> Vec { - let mut ports: Vec<_> = SerialTransport::list_ports() - .into_iter() - .map(|port| port.port_name) - .collect(); - ports.sort(); - ports.dedup(); - ports -} - -fn point_in_rect(x: u16, y: u16, rect: Rect) -> bool { - x >= rect.x - && x < rect.x.saturating_add(rect.width) - && y >= rect.y - && y < rect.y.saturating_add(rect.height) -} - -fn byte_index_for_char(text: &str, char_index: usize) -> usize { - text.char_indices() - .nth(char_index) - .map(|(index, _)| index) - .unwrap_or(text.len()) -} - -fn parse_usize(text: &str, label: &str) -> Result { - text.trim() - .parse::() - .map_err(|_| format!("{label} must be a positive integer")) -} - -fn parse_u32(text: &str, label: &str) -> Result { - text.trim() - .parse::() - .map_err(|_| format!("{label} must be a positive integer")) -} - -fn bool_label(value: bool) -> &'static str { - if value { "on" } else { "off" } -} - -fn serial_data_bits_name(value: SerialDataBits) -> &'static str { - match value { - SerialDataBits::Five => "5", - SerialDataBits::Six => "6", - SerialDataBits::Seven => "7", - SerialDataBits::Eight => "8", - } -} - -fn serial_parity_name(value: SerialParity) -> &'static str { - match value { - SerialParity::None => "None", - SerialParity::Odd => "Odd", - SerialParity::Even => "Even", - } -} - -fn serial_stop_bits_name(value: SerialStopBits) -> &'static str { - match value { - SerialStopBits::One => "1", - SerialStopBits::Two => "2", - } -} - -fn serial_flow_control_name(value: SerialFlowControl) -> &'static str { - match value { - SerialFlowControl::None => "None", - SerialFlowControl::Software => "Software", - SerialFlowControl::Hardware => "Hardware", - } -} - -fn next_serial_data_bits(value: SerialDataBits, forward: bool) -> SerialDataBits { - match (value, forward) { - (SerialDataBits::Five, true) => SerialDataBits::Six, - (SerialDataBits::Six, true) => SerialDataBits::Seven, - (SerialDataBits::Seven, true) => SerialDataBits::Eight, - (SerialDataBits::Eight, true) => SerialDataBits::Five, - (SerialDataBits::Five, false) => SerialDataBits::Eight, - (SerialDataBits::Six, false) => SerialDataBits::Five, - (SerialDataBits::Seven, false) => SerialDataBits::Six, - (SerialDataBits::Eight, false) => SerialDataBits::Seven, - } -} - -fn next_serial_parity(value: SerialParity, forward: bool) -> SerialParity { - match (value, forward) { - (SerialParity::None, true) => SerialParity::Odd, - (SerialParity::Odd, true) => SerialParity::Even, - (SerialParity::Even, true) => SerialParity::None, - (SerialParity::None, false) => SerialParity::Even, - (SerialParity::Odd, false) => SerialParity::None, - (SerialParity::Even, false) => SerialParity::Odd, - } -} - -fn next_serial_stop_bits(value: SerialStopBits, _forward: bool) -> SerialStopBits { - match value { - SerialStopBits::One => SerialStopBits::Two, - SerialStopBits::Two => SerialStopBits::One, - } -} - -fn next_serial_flow_control(value: SerialFlowControl, forward: bool) -> SerialFlowControl { - match (value, forward) { - (SerialFlowControl::None, true) => SerialFlowControl::Software, - (SerialFlowControl::Software, true) => SerialFlowControl::Hardware, - (SerialFlowControl::Hardware, true) => SerialFlowControl::None, - (SerialFlowControl::None, false) => SerialFlowControl::Hardware, - (SerialFlowControl::Software, false) => SerialFlowControl::None, - (SerialFlowControl::Hardware, false) => SerialFlowControl::Software, - } -} diff --git a/crates/pipeview-tui/src/app_state.rs b/crates/pipeview-tui/src/app_state.rs deleted file mode 100644 index d409086..0000000 --- a/crates/pipeview-tui/src/app_state.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::env; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -use pipeview_client::SessionConfig; -use serde::{Deserialize, Serialize}; -use tracing::warn; - -use crate::app::{DisplayOptions, View, default_session_config}; - -const TUI_STATE_FILE_NAME: &str = "tui-state.json"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PersistedTuiState { - #[serde(default = "default_session")] - pub session: SessionConfig, - #[serde(default)] - pub active_view: PersistedView, - #[serde(default = "default_true")] - pub show_timestamp: bool, - #[serde(default = "default_true")] - pub show_direction: bool, - #[serde(default = "default_true")] - pub show_pipeline: bool, -} - -impl Default for PersistedTuiState { - fn default() -> Self { - Self { - session: default_session(), - active_view: PersistedView::Text, - show_timestamp: true, - show_direction: true, - show_pipeline: true, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] -pub enum PersistedView { - #[default] - Text, - Hex, - Plot, -} - -impl From for PersistedView { - fn from(value: View) -> Self { - match value { - View::Text => Self::Text, - View::Hex => Self::Hex, - View::Plot => Self::Plot, - } - } -} - -impl From for View { - fn from(value: PersistedView) -> Self { - match value { - PersistedView::Text => View::Text, - PersistedView::Hex => View::Hex, - PersistedView::Plot => View::Plot, - } - } -} - -pub fn load_tui_state() -> PersistedTuiState { - match load_tui_state_from_path(&tui_state_path()) { - Ok(mut state) => { - if state.session.pipelines.is_empty() { - state.session = default_session(); - } - state - } - Err(err) if err.kind() == io::ErrorKind::NotFound => PersistedTuiState::default(), - Err(err) => { - warn!(error = %err, "Failed to load persisted TUI state"); - PersistedTuiState::default() - } - } -} - -pub fn save_tui_state(session: &SessionConfig, view: View, display: DisplayOptions) { - let state = PersistedTuiState { - session: session.clone(), - active_view: view.into(), - show_timestamp: display.show_timestamp, - show_direction: display.show_direction, - show_pipeline: display.show_pipeline, - }; - - if let Err(err) = save_tui_state_to_path(&state, &tui_state_path()) { - warn!(error = %err, "Failed to persist TUI state"); - } -} - -fn default_session() -> SessionConfig { - default_session_config() -} - -fn tui_state_path() -> PathBuf { - state_path_for_os_and_env(env::consts::OS, |key| env::var(key).ok()) -} - -pub fn config_dir() -> PathBuf { - tui_state_path() - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(|| PathBuf::from(".")) -} - -fn state_path_for_os_and_env(os: &str, get_env: impl Fn(&str) -> Option) -> PathBuf { - match os { - "windows" => { - if let Some(path) = get_env("APPDATA").filter(|path| !path.trim().is_empty()) { - return PathBuf::from(path) - .join("pipeview") - .join(TUI_STATE_FILE_NAME); - } - if let Some(path) = get_env("LOCALAPPDATA").filter(|path| !path.trim().is_empty()) { - return PathBuf::from(path) - .join("pipeview") - .join(TUI_STATE_FILE_NAME); - } - } - "macos" => { - if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) { - return PathBuf::from(home) - .join("Library") - .join("Application Support") - .join("pipeview") - .join(TUI_STATE_FILE_NAME); - } - } - _ => { - if let Some(path) = get_env("XDG_CONFIG_HOME").filter(|path| !path.trim().is_empty()) { - return PathBuf::from(path) - .join("pipeview") - .join(TUI_STATE_FILE_NAME); - } - if let Some(home) = get_env("HOME").filter(|path| !path.trim().is_empty()) { - return PathBuf::from(home) - .join(".config") - .join("pipeview") - .join(TUI_STATE_FILE_NAME); - } - } - } - - PathBuf::from(TUI_STATE_FILE_NAME) -} - -fn load_tui_state_from_path(path: &Path) -> io::Result { - let text = fs::read_to_string(path)?; - serde_json::from_str(&text).map_err(io::Error::other) -} - -fn save_tui_state_to_path(state: &PersistedTuiState, path: &Path) -> io::Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - let json = serde_json::to_string_pretty(state).map_err(io::Error::other)?; - fs::write(path, json) -} - -const fn default_true() -> bool { - true -} diff --git a/crates/pipeview-tui/src/buffers.rs b/crates/pipeview-tui/src/buffers.rs deleted file mode 100644 index 08a63a4..0000000 --- a/crates/pipeview-tui/src/buffers.rs +++ /dev/null @@ -1,637 +0,0 @@ -use std::collections::VecDeque; -use std::time::{Duration, Instant}; - -use pipeview_client::{DecodedEntry, RingBuffer}; -use pipeview_core::protocol::DecodedData; -use pipeview_core::protocol::plot::{PlotFormat, PlotFrame}; - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum LineDirection { - In, - Out, -} - -#[derive(Clone)] -pub struct ConsoleLine { - pub elapsed: Duration, - pub pipeline: String, - pub text: String, - pub direction: LineDirection, -} - -pub struct TextBuffer { - started_at: Instant, - lines: RingBuffer, -} - -impl TextBuffer { - pub fn new(limit: usize) -> Self { - Self { - started_at: Instant::now(), - lines: RingBuffer::new(limit), - } - } - - pub fn push(&mut self, entry: &DecodedEntry) { - if let DecodedData::Text(text) = &entry.data { - self.lines.push(ConsoleLine { - elapsed: self.started_at.elapsed(), - pipeline: entry.pipeline_name.clone(), - text: text.clone(), - direction: LineDirection::In, - }); - } - } - - pub fn push_outbound(&mut self, text: String) { - self.lines.push(ConsoleLine { - elapsed: self.started_at.elapsed(), - pipeline: String::from("OUT"), - text, - direction: LineDirection::Out, - }); - } - - pub fn get(&self, index: usize) -> Option<&ConsoleLine> { - self.lines.get(index) - } - - pub fn len(&self) -> usize { - self.lines.len() - } - - pub fn clear(&mut self) { - self.lines.clear(); - } - - pub fn set_limit(&mut self, limit: usize) { - self.lines.set_limit(limit); - } -} - -#[derive(Clone)] -pub struct HexLine { - pub elapsed: Duration, - pub pipeline: String, - pub hex: String, - pub ascii: String, - pub direction: LineDirection, -} - -pub struct HexBuffer { - started_at: Instant, - lines: RingBuffer, -} - -impl HexBuffer { - pub fn new(limit: usize) -> Self { - Self { - started_at: Instant::now(), - lines: RingBuffer::new(limit), - } - } - - pub fn push(&mut self, entry: &DecodedEntry) { - if let DecodedData::Hex(hex) = &entry.data { - self.lines.push(HexLine { - elapsed: self.started_at.elapsed(), - pipeline: entry.pipeline_name.clone(), - ascii: decode_ascii(hex), - hex: hex.clone(), - direction: LineDirection::In, - }); - } - } - - pub fn push_outbound(&mut self, hex: String) { - self.lines.push(HexLine { - elapsed: self.started_at.elapsed(), - pipeline: String::from("OUT"), - ascii: decode_ascii(&hex), - hex, - direction: LineDirection::Out, - }); - } - - pub fn get(&self, index: usize) -> Option<&HexLine> { - self.lines.get(index) - } - - pub fn len(&self) -> usize { - self.lines.len() - } - - pub fn clear(&mut self) { - self.lines.clear(); - } - - pub fn set_limit(&mut self, limit: usize) { - self.lines.set_limit(limit); - } -} - -pub struct PlotSeries { - pub name: String, - points: VecDeque<[f64; 2]>, - next_x: f64, -} - -impl PlotSeries { - fn new(name: String) -> Self { - Self { - name, - points: VecDeque::new(), - next_x: 0.0, - } - } - - fn push_samples(&mut self, samples: &[f64], limit: usize) { - for sample in samples { - if !sample.is_finite() { - continue; - } - self.push_point([self.next_x, *sample], limit); - self.next_x += 1.0; - } - } - - fn push_point(&mut self, point: [f64; 2], limit: usize) -> Option<[f64; 2]> { - self.points.push_back(point); - if self.points.len() > limit { - self.points.pop_front() - } else { - None - } - } - - pub fn points(&self) -> impl Iterator + '_ { - self.points.iter().copied() - } - - pub fn len(&self) -> usize { - self.points.len() - } - - fn first_point(&self) -> Option<[f64; 2]> { - self.points.front().copied() - } - - fn last_point(&self) -> Option<[f64; 2]> { - self.points.back().copied() - } - - pub fn render_points_time_series( - &self, - x_min: f64, - x_max: f64, - max_points: usize, - ) -> Vec<[f64; 2]> { - if self.points.is_empty() || max_points == 0 { - return Vec::new(); - } - - let mut exact_visible = Vec::with_capacity(max_points.min(self.points.len())); - for point in self.points.iter().copied() { - if point[0] < x_min { - continue; - } - if point[0] > x_max { - break; - } - exact_visible.push(point); - if exact_visible.len() > max_points { - exact_visible.clear(); - break; - } - } - if !exact_visible.is_empty() { - return exact_visible; - } - - let bucket_count = (max_points / 2).max(1); - let width = (x_max - x_min).max(1.0); - let bucket_width = width / bucket_count as f64; - let mut rendered = Vec::with_capacity(bucket_count * 2); - let mut points = self - .points - .iter() - .copied() - .skip_while(|point| point[0] < x_min) - .peekable(); - - for bucket_index in 0..bucket_count { - let bucket_start = x_min + bucket_width * bucket_index as f64; - let bucket_end = if bucket_index + 1 == bucket_count { - x_max - } else { - bucket_start + bucket_width - }; - - let mut min_point: Option<[f64; 2]> = None; - let mut max_point: Option<[f64; 2]> = None; - - while let Some(point) = points.peek().copied() { - if point[0] > bucket_end { - break; - } - if point[0] >= bucket_start { - match min_point { - Some(current) if current[1] <= point[1] => {} - _ => min_point = Some(point), - } - match max_point { - Some(current) if current[1] >= point[1] => {} - _ => max_point = Some(point), - } - } - points.next(); - } - - match (min_point, max_point) { - (Some(a), Some(b)) if a[0] <= b[0] => { - rendered.push(a); - if a != b { - rendered.push(b); - } - } - (Some(a), Some(b)) => { - rendered.push(b); - if a != b { - rendered.push(a); - } - } - (Some(a), None) | (None, Some(a)) => rendered.push(a), - (None, None) => {} - } - } - - if rendered.len() > max_points { - let stride = rendered.len().div_ceil(max_points); - rendered.into_iter().step_by(stride).collect() - } else { - rendered - } - } - - pub fn render_points_xy(&self, max_points: usize) -> Vec<[f64; 2]> { - if self.points.is_empty() || max_points == 0 { - return Vec::new(); - } - if self.points.len() <= max_points { - return self.points.iter().copied().collect(); - } - - let stride = self.points.len().div_ceil(max_points); - self.points.iter().copied().step_by(stride).collect() - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum PlotSeriesKind { - TimeSeries, - XY, -} - -#[derive(Clone, Copy)] -struct BoundsRect { - min_x: f64, - max_x: f64, - min_y: f64, - max_y: f64, -} - -impl BoundsRect { - fn from_point([x, y]: [f64; 2]) -> Option { - if !(x.is_finite() && y.is_finite()) { - return None; - } - Some(Self { - min_x: x, - max_x: x, - min_y: y, - max_y: y, - }) - } - - fn extend_with_point(&mut self, [x, y]: [f64; 2]) { - if !(x.is_finite() && y.is_finite()) { - return; - } - self.min_x = self.min_x.min(x); - self.max_x = self.max_x.max(x); - self.min_y = self.min_y.min(y); - self.max_y = self.max_y.max(y); - } - - fn touches(&self, [x, y]: [f64; 2]) -> bool { - x == self.min_x || x == self.max_x || y == self.min_y || y == self.max_y - } -} - -pub struct PlotBuffer { - limit: usize, - kind: PlotSeriesKind, - series: Vec, - time_series_y_bounds: Option<(f64, f64)>, - time_series_y_dirty: bool, - xy_bounds: Option, - xy_bounds_dirty: bool, -} - -impl PlotBuffer { - pub fn new(limit: usize) -> Self { - Self { - limit, - kind: PlotSeriesKind::TimeSeries, - series: Vec::new(), - time_series_y_bounds: None, - time_series_y_dirty: false, - xy_bounds: None, - xy_bounds_dirty: false, - } - } - - pub fn push(&mut self, entry: &DecodedEntry) { - if let DecodedData::Plot(frame) = &entry.data { - self.push_frame(&entry.pipeline_name, frame); - } - } - - fn push_frame(&mut self, pipeline_name: &str, frame: &PlotFrame) { - let next_kind = match frame.format { - PlotFormat::XY => PlotSeriesKind::XY, - PlotFormat::Interleaved | PlotFormat::Block => PlotSeriesKind::TimeSeries, - }; - if self.kind != next_kind { - self.invalidate_bounds(); - } - self.kind = next_kind; - - if matches!(frame.format, PlotFormat::XY) { - self.push_xy_frame(pipeline_name, frame); - return; - } - - for (index, channel) in frame.channels.iter().enumerate() { - let series_name = if frame.channels.len() == 1 { - pipeline_name.to_owned() - } else { - format!("{pipeline_name}:ch{}", index + 1) - }; - - if let Some(series_index) = self - .series - .iter() - .position(|series| series.name == series_name) - { - for sample in channel { - if !sample.is_finite() { - continue; - } - let (point, removed) = { - let series = &mut self.series[series_index]; - let point = [series.next_x, *sample]; - let removed = series.push_point(point, self.limit); - series.next_x += 1.0; - (point, removed) - }; - if let Some(removed) = removed { - self.note_time_series_removed(removed); - } - self.note_time_series_point(point); - } - } else { - let mut series = PlotSeries::new(series_name); - series.push_samples(channel, self.limit); - for point in series.points() { - self.note_time_series_point(point); - } - self.series.push(series); - } - } - } - - fn push_xy_frame(&mut self, pipeline_name: &str, frame: &PlotFrame) { - if frame.channels.len() < 2 { - return; - } - - let x = &frame.channels[0]; - let y = &frame.channels[1]; - let len = x.len().min(y.len()); - let series_name = format!("{pipeline_name}:xy"); - - let series_index = if let Some(index) = self - .series - .iter() - .position(|series| series.name == series_name) - { - index - } else { - self.series.push(PlotSeries::new(series_name)); - self.series.len() - 1 - }; - - for index in 0..len { - if !x[index].is_finite() || !y[index].is_finite() { - continue; - } - let point = [x[index], y[index]]; - let removed = { - let series = &mut self.series[series_index]; - series.push_point(point, self.limit) - }; - if let Some(removed) = removed { - self.note_xy_removed(removed); - } - self.note_xy_point(point); - } - } - - pub fn clear(&mut self) { - self.series.clear(); - self.invalidate_bounds(); - } - - pub fn set_limit(&mut self, limit: usize) { - self.limit = limit; - for series in &mut self.series { - while series.points.len() > self.limit { - series.points.pop_front(); - } - } - self.invalidate_bounds(); - } - - pub fn is_empty(&self) -> bool { - self.series.is_empty() - } - - pub fn kind(&self) -> PlotSeriesKind { - self.kind - } - - pub fn iter(&self) -> impl Iterator { - self.series.iter() - } - - pub fn series_len(&self) -> usize { - self.series.len() - } - - pub fn total_points(&self) -> usize { - self.series.iter().map(PlotSeries::len).sum() - } - - pub fn bounds(&mut self) -> Option<([f64; 2], [f64; 2])> { - match self.kind { - PlotSeriesKind::TimeSeries => self.time_series_bounds(), - PlotSeriesKind::XY => self.xy_bounds(), - } - } - - fn invalidate_bounds(&mut self) { - self.time_series_y_bounds = None; - self.time_series_y_dirty = false; - self.xy_bounds = None; - self.xy_bounds_dirty = false; - } - - fn note_time_series_point(&mut self, point: [f64; 2]) { - if !point[1].is_finite() { - return; - } - match &mut self.time_series_y_bounds { - Some((min_y, max_y)) => { - *min_y = min_y.min(point[1]); - *max_y = max_y.max(point[1]); - } - None => self.time_series_y_bounds = Some((point[1], point[1])), - } - } - - fn note_time_series_removed(&mut self, removed: [f64; 2]) { - if let Some((min_y, max_y)) = self.time_series_y_bounds - && (removed[1] == min_y || removed[1] == max_y) - { - self.time_series_y_dirty = true; - } - } - - fn note_xy_point(&mut self, point: [f64; 2]) { - match &mut self.xy_bounds { - Some(bounds) => bounds.extend_with_point(point), - None => self.xy_bounds = BoundsRect::from_point(point), - } - } - - fn note_xy_removed(&mut self, removed: [f64; 2]) { - if let Some(bounds) = self.xy_bounds - && bounds.touches(removed) - { - self.xy_bounds_dirty = true; - } - } - - fn time_series_bounds(&mut self) -> Option<([f64; 2], [f64; 2])> { - let mut min_x = f64::INFINITY; - let mut max_x = f64::NEG_INFINITY; - - for series in &self.series { - if let Some([x, _]) = series.first_point() { - min_x = min_x.min(x); - } - if let Some([x, _]) = series.last_point() { - max_x = max_x.max(x); - } - } - - if !(min_x.is_finite() && max_x.is_finite()) { - return None; - } - - if self.time_series_y_dirty { - self.recompute_time_series_y_bounds(); - } - let (mut min_y, mut max_y) = self.time_series_y_bounds?; - if min_y == max_y { - min_y -= 1.0; - max_y += 1.0; - } - Some(([min_x, min_y], [max_x, max_y])) - } - - fn xy_bounds(&mut self) -> Option<([f64; 2], [f64; 2])> { - if self.xy_bounds_dirty { - self.recompute_xy_bounds(); - } - let bounds = self.xy_bounds?; - let mut min_x = bounds.min_x; - let mut max_x = bounds.max_x; - let mut min_y = bounds.min_y; - let mut max_y = bounds.max_y; - if min_x == max_x { - min_x -= 1.0; - max_x += 1.0; - } - if min_y == max_y { - min_y -= 1.0; - max_y += 1.0; - } - Some(([min_x, min_y], [max_x, max_y])) - } - - fn recompute_time_series_y_bounds(&mut self) { - let mut min_y = f64::INFINITY; - let mut max_y = f64::NEG_INFINITY; - - for series in &self.series { - for [_, y] in series.points() { - if y.is_finite() { - min_y = min_y.min(y); - max_y = max_y.max(y); - } - } - } - - self.time_series_y_bounds = if min_y.is_finite() && max_y.is_finite() { - Some((min_y, max_y)) - } else { - None - }; - self.time_series_y_dirty = false; - } - - fn recompute_xy_bounds(&mut self) { - let mut bounds: Option = None; - - for series in &self.series { - for point in series.points() { - match &mut bounds { - Some(existing) => existing.extend_with_point(point), - None => bounds = BoundsRect::from_point(point), - } - } - } - - self.xy_bounds = bounds; - self.xy_bounds_dirty = false; - } -} - -fn decode_ascii(hex: &str) -> String { - hex::decode(hex.replace(' ', "")) - .map(|bytes| { - bytes - .into_iter() - .map(|byte| { - if byte.is_ascii_graphic() || byte == b' ' { - byte as char - } else { - '.' - } - }) - .collect() - }) - .unwrap_or_else(|_| String::from("[invalid hex]")) -} diff --git a/crates/pipeview-tui/src/main.rs b/crates/pipeview-tui/src/main.rs deleted file mode 100644 index 137aacd..0000000 --- a/crates/pipeview-tui/src/main.rs +++ /dev/null @@ -1,65 +0,0 @@ -mod ansi; -mod app; -mod app_state; -mod buffers; -mod ui; - -use std::io; - -use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, -}; -use ratatui::{Terminal, backend::CrosstermBackend}; -use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::{EnvFilter, fmt}; - -use crate::app::App; - -#[tokio::main] -async fn main() -> io::Result<()> { - let _log_guard = init_tracing(); - - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let result = async { - let mut app = App::new(); - app.run(&mut terminal).await?; - app.shutdown().await; - io::Result::Ok(()) - } - .await; - - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - result -} - -fn init_tracing() -> Option { - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("pipeview_tui=info,pipeview_client=info")); - let log_dir = app_state::config_dir(); - let _ = std::fs::create_dir_all(&log_dir); - let file_appender = tracing_appender::rolling::never(log_dir, "tui.log"); - let (writer, guard) = tracing_appender::non_blocking(file_appender); - - fmt() - .with_env_filter(filter) - .with_target(false) - .with_ansi(false) - .with_writer(writer) - .try_init() - .ok() - .map(|_| guard) -} diff --git a/crates/pipeview-tui/src/ui.rs b/crates/pipeview-tui/src/ui.rs deleted file mode 100644 index 4037ad9..0000000 --- a/crates/pipeview-tui/src/ui.rs +++ /dev/null @@ -1,989 +0,0 @@ -use std::time::Duration; - -use ratatui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - symbols::Marker, - text::{Line, Span}, - widgets::{ - Axis, Block, Borders, Chart, Clear, Dataset, GraphType, List, ListItem, Paragraph, Wrap, - }, -}; - -use crate::ansi::ansi_to_spans; -use crate::app::{ - App, AppMode, ConfigField, ConnectionStatus, DisplayOptions, FocusPane, HotAction, LineEnding, - SendMode, View, -}; -use crate::buffers::{ConsoleLine, HexLine, LineDirection, PlotSeriesKind}; - -const BG: Color = Color::Rgb(8, 12, 18); -const PANEL: Color = Color::Rgb(12, 20, 28); -const PANEL_ALT: Color = Color::Rgb(17, 27, 38); -const CYAN: Color = Color::Rgb(55, 214, 230); -const GREEN: Color = Color::Rgb(72, 211, 137); -const AMBER: Color = Color::Rgb(244, 184, 74); -const MAGENTA: Color = Color::Rgb(219, 111, 220); -const RED: Color = Color::Rgb(238, 92, 107); -const BLUE: Color = Color::Rgb(96, 165, 250); -const MUTED: Color = Color::Rgb(120, 134, 153); -const TEXT: Color = Color::Rgb(224, 234, 244); - -pub fn render(frame: &mut Frame, app: &mut App) { - app.reset_hot_zones(); - let area = frame.area(); - frame.render_widget(Block::default().style(Style::default().bg(BG)), area); - - let root = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(4), - Constraint::Min(10), - Constraint::Length(6), - Constraint::Length(2), - ]) - .split(area); - - render_header(frame, app, root[0]); - - let body = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Length(34), Constraint::Min(30)]) - .split(root[1]); - render_controls(frame, app, body[0]); - render_main(frame, app, body[1]); - render_composer(frame, app, root[2]); - render_footer(frame, app, root[3]); - - match app.mode { - AppMode::Search => render_search_modal(frame, app), - AppMode::Config => render_config_modal(frame, app), - AppMode::Help => render_help_modal(frame, app), - AppMode::Normal | AppMode::EditingSend => {} - } -} - -fn render_header(frame: &mut Frame, app: &App, area: Rect) { - let status = &app.session.status; - let status_color = status_color(status); - let error = match status { - ConnectionStatus::Error(message) => format!(" {message}"), - _ => String::new(), - }; - - let header = vec![ - Line::from(vec![ - Span::styled( - " PIPEVIEW ", - Style::default() - .fg(Color::Black) - .bg(CYAN) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::styled( - "single session telemetry console", - Style::default().fg(TEXT).add_modifier(Modifier::BOLD), - ), - Span::raw(" "), - Span::styled( - status.label(), - Style::default() - .fg(Color::Black) - .bg(status_color) - .add_modifier(Modifier::BOLD), - ), - Span::styled(error, Style::default().fg(RED)), - ]), - Line::from(vec![ - Span::styled(app.transport_summary(), Style::default().fg(AMBER)), - Span::raw(" | pipelines: "), - Span::styled(app.pipeline_summary(), Style::default().fg(MAGENTA)), - Span::raw(" | view: "), - Span::styled( - app.session.view.label(), - Style::default().fg(view_color(app.session.view)), - ), - Span::raw(" | rx/tx: "), - Span::styled( - format!( - "{}/{}", - app.session.received_messages, app.session.sent_messages - ), - Style::default().fg(GREEN), - ), - ]), - ]; - - frame.render_widget( - Paragraph::new(header) - .block(panel_block("Status", false, CYAN)) - .wrap(Wrap { trim: false }), - area, - ); -} - -fn render_controls(frame: &mut Frame, app: &mut App, area: Rect) { - app.add_hot_zone(area, HotAction::Focus(FocusPane::Controls)); - let focused = app.focus == FocusPane::Controls; - let block = panel_block("Control Deck", focused, AMBER); - let inner = block.inner(area); - frame.render_widget(block, area); - - let connect_label = if app.session.status.is_connectedish() { - "[ Disconnect ]" - } else { - "[ Connect ]" - }; - let ending = next_line_ending(app.session.line_ending); - let rows: Vec<(Line<'static>, Option)> = vec![ - section_line("SESSION"), - button_line( - connect_label, - status_color(&app.session.status), - Some(HotAction::ToggleConnection), - ), - button_line("[ Reconnect ]", BLUE, Some(HotAction::Reconnect)), - button_line("[ Clear Buffers ]", RED, Some(HotAction::Clear)), - button_line("[ Configure ]", MAGENTA, Some(HotAction::OpenConfig)), - spacer_line(), - section_line("LINES"), - toggle_line( - "Auto reconnect", - app.session.auto_reconnect, - Some(HotAction::ToggleAutoReconnect), - ), - toggle_line("DTR", app.session.dtr, Some(HotAction::ToggleDtr)), - toggle_line("RTS", app.session.rts, Some(HotAction::ToggleRts)), - spacer_line(), - section_line("DISPLAY"), - toggle_line( - "Timestamp", - app.display.show_timestamp, - Some(HotAction::ToggleTimestamp), - ), - toggle_line( - "Direction", - app.display.show_direction, - Some(HotAction::ToggleDirection), - ), - toggle_line( - "Pipeline", - app.display.show_pipeline, - Some(HotAction::TogglePipeline), - ), - spacer_line(), - section_line("SEND"), - value_line( - "Mode", - app.session.send_mode.label(), - Some(HotAction::SendMode(match app.session.send_mode { - SendMode::Text => SendMode::Hex, - SendMode::Hex => SendMode::Text, - })), - ), - value_line( - "Ending", - app.session.line_ending.label(), - Some(HotAction::LineEnding(ending)), - ), - button_line("[ Search ]", CYAN, Some(HotAction::OpenSearch)), - button_line("[ Help ]", MUTED, Some(HotAction::OpenHelp)), - ]; - - for (index, (_, action)) in rows.iter().enumerate() { - if let Some(action) = action { - let y = inner.y.saturating_add(index as u16); - if y < inner.y.saturating_add(inner.height) { - app.add_hot_zone( - Rect { - x: inner.x, - y, - width: inner.width, - height: 1, - }, - *action, - ); - } - } - } - - frame.render_widget( - Paragraph::new(rows.into_iter().map(|(line, _)| line).collect::>()) - .style(Style::default().fg(TEXT).bg(PANEL)), - inner, - ); -} - -fn render_main(frame: &mut Frame, app: &mut App, area: Rect) { - app.add_hot_zone(area, HotAction::Focus(FocusPane::Main)); - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(5)]) - .split(area); - - render_tabs(frame, app, layout[0]); - match app.session.view { - View::Text => render_text_view(frame, app, layout[1]), - View::Hex => render_hex_view(frame, app, layout[1]), - View::Plot => render_plot_view(frame, app, layout[1]), - } -} - -fn render_tabs(frame: &mut Frame, app: &mut App, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(34), - Constraint::Percentage(33), - Constraint::Percentage(33), - ]) - .split(area); - - for (idx, view) in View::ALL.into_iter().enumerate() { - let selected = app.session.view == view; - app.add_hot_zone(chunks[idx], HotAction::View(view)); - let color = view_color(view); - let block = panel_block(view.label(), selected, color); - frame.render_widget( - Paragraph::new(Line::from(vec![ - Span::styled( - view.label(), - Style::default() - .fg(if selected { Color::Black } else { color }) - .bg(if selected { color } else { PANEL }) - .add_modifier(Modifier::BOLD), - ), - Span::styled( - match view { - View::Text => format!(" {} lines", app.session.console.len()), - View::Hex => format!(" {} lines", app.session.hex.len()), - View::Plot => format!( - " {} series / {} pts", - app.session.plot.series_len(), - app.session.plot.total_points() - ), - }, - Style::default().fg(MUTED), - ), - ])) - .block(block) - .alignment(Alignment::Center), - chunks[idx], - ); - } -} - -fn render_text_view(frame: &mut Frame, app: &mut App, area: Rect) { - let focused = app.focus == FocusPane::Main; - let block = panel_block("Text Stream", focused, CYAN); - let inner = block.inner(area); - let height = inner.height as usize; - let total = app.session.console.len(); - let start = app.visible_start(total, height); - let end = start.saturating_add(height).min(total); - - let lines = if total == 0 { - vec![Line::styled("no text data", Style::default().fg(MUTED))] - } else { - (start..end) - .filter_map(|index| { - app.session - .console - .get(index) - .map(|line| format_console_line(index, line, app.display, app)) - }) - .collect() - }; - - frame.render_widget( - Paragraph::new(lines) - .block(block) - .style(Style::default().bg(PANEL_ALT)) - .wrap(Wrap { trim: false }), - area, - ); -} - -fn render_hex_view(frame: &mut Frame, app: &mut App, area: Rect) { - let focused = app.focus == FocusPane::Main; - let block = panel_block("Hex Stream", focused, AMBER); - let inner = block.inner(area); - let height = inner.height as usize; - let total = app.session.hex.len(); - let start = app.visible_start(total, height); - let end = start.saturating_add(height).min(total); - - let lines = if total == 0 { - vec![Line::styled("no hex data", Style::default().fg(MUTED))] - } else { - (start..end) - .filter_map(|index| { - app.session - .hex - .get(index) - .map(|line| format_hex_line(index, line, app.display, app)) - }) - .collect() - }; - - frame.render_widget( - Paragraph::new(lines) - .block(block) - .style(Style::default().bg(PANEL_ALT)) - .wrap(Wrap { trim: false }), - area, - ); -} - -fn render_plot_view(frame: &mut Frame, app: &mut App, area: Rect) { - let focused = app.focus == FocusPane::Main; - let block = panel_block("Plot View", focused, GREEN); - let inner = block.inner(area); - app.add_hot_zone(inner, HotAction::Focus(FocusPane::Main)); - - if app.session.plot.is_empty() { - frame.render_widget( - Paragraph::new(vec![ - Line::styled("no plot data", Style::default().fg(MUTED)), - Line::from(vec![ - Span::raw("pipeline: "), - Span::styled(app.pipeline_summary(), Style::default().fg(MAGENTA)), - ]), - ]) - .block(block) - .style(Style::default().bg(PANEL_ALT)) - .alignment(Alignment::Center), - area, - ); - return; - } - - let Some((min, max)) = app.plot_bounds() else { - frame.render_widget( - Paragraph::new("plot bounds unavailable") - .block(block) - .style(Style::default().fg(MUTED).bg(PANEL_ALT)), - area, - ); - return; - }; - - let max_points = (inner.width as usize).saturating_mul(2).max(32); - let kind = app.session.plot.kind(); - let series_points: Vec<(String, Vec<(f64, f64)>)> = app - .session - .plot - .iter() - .map(|series| { - let points = match kind { - PlotSeriesKind::TimeSeries => { - series.render_points_time_series(min[0], max[0], max_points) - } - PlotSeriesKind::XY => series.render_points_xy(max_points), - }; - ( - series.name.clone(), - points.into_iter().map(|[x, y]| (x, y)).collect(), - ) - }) - .collect(); - - let palette = [GREEN, CYAN, AMBER, MAGENTA, BLUE, RED]; - let datasets = series_points - .iter() - .enumerate() - .map(|(index, (name, points))| { - Dataset::default() - .name(name.as_str()) - .marker(Marker::Braille) - .graph_type(GraphType::Line) - .style(Style::default().fg(palette[index % palette.len()])) - .data(points) - }) - .collect::>(); - - let chart = Chart::new(datasets) - .block(block) - .style(Style::default().bg(PANEL_ALT)) - .x_axis(axis("X", min[0], max[0], CYAN)) - .y_axis(axis("Y", min[1], max[1], GREEN)); - - frame.render_widget(chart, area); - - let overlay = Rect { - x: inner.x.saturating_add(1), - y: inner.y, - width: inner.width.saturating_sub(2).min(72), - height: 1, - }; - let controls = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(16), - Constraint::Length(10), - Constraint::Length(10), - Constraint::Min(1), - ]) - .split(overlay); - app.add_hot_zone(controls[0], HotAction::PlotFollow); - app.add_hot_zone(controls[1], HotAction::PlotZoomIn); - app.add_hot_zone(controls[2], HotAction::PlotZoomOut); - let mode = match kind { - PlotSeriesKind::TimeSeries => "time", - PlotSeriesKind::XY => "xy", - }; - frame.render_widget( - Paragraph::new(Line::from(vec![ - Span::styled( - format!( - "[follow {}] ", - if app.plot.follow_latest { "on" } else { "off" } - ), - Style::default() - .fg(Color::Black) - .bg(if app.plot.follow_latest { GREEN } else { MUTED }) - .add_modifier(Modifier::BOLD), - ), - Span::styled("[zoom +] ", Style::default().fg(Color::Black).bg(CYAN)), - Span::styled("[zoom -] ", Style::default().fg(Color::Black).bg(AMBER)), - Span::styled(format!("{mode} "), Style::default().fg(MUTED)), - Span::styled( - format!( - "{} series / {} pts", - app.session.plot.series_len(), - app.session.plot.total_points() - ), - Style::default().fg(TEXT), - ), - ])) - .style(Style::default().bg(PANEL_ALT)), - overlay, - ); -} - -fn render_composer(frame: &mut Frame, app: &mut App, area: Rect) { - app.add_hot_zone(area, HotAction::Focus(FocusPane::Composer)); - let focused = app.focus == FocusPane::Composer || matches!(app.mode, AppMode::EditingSend); - let block = panel_block("Composer", focused, MAGENTA); - let inner = block.inner(area); - frame.render_widget(block, area); - - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(20), Constraint::Length(28)]) - .split(inner); - - let input_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(if focused { MAGENTA } else { MUTED })) - .style(Style::default().bg(PANEL_ALT)); - - frame.render_widget( - Paragraph::new(input_line(app)) - .block(input_block) - .wrap(Wrap { trim: false }), - chunks[0], - ); - - let side_rows = vec![ - value_line( - "Mode", - app.session.send_mode.label(), - Some(HotAction::SendMode(match app.session.send_mode { - SendMode::Text => SendMode::Hex, - SendMode::Hex => SendMode::Text, - })), - ), - value_line( - "Ending", - app.session.line_ending.label(), - Some(HotAction::LineEnding(next_line_ending( - app.session.line_ending, - ))), - ), - button_line("[ Send ]", GREEN, Some(HotAction::Send)), - ( - Line::from(vec![ - Span::styled("Status ", Style::default().fg(MUTED)), - Span::styled(app.session.send_status.clone(), Style::default().fg(TEXT)), - ]), - None, - ), - ]; - - for (index, (_, action)) in side_rows.iter().enumerate() { - if let Some(action) = action { - app.add_hot_zone( - Rect { - x: chunks[1].x, - y: chunks[1].y.saturating_add(index as u16), - width: chunks[1].width, - height: 1, - }, - *action, - ); - } - } - - frame.render_widget( - Paragraph::new( - side_rows - .into_iter() - .map(|(line, _)| line) - .collect::>(), - ) - .style(Style::default().bg(PANEL)), - chunks[1], - ); -} - -fn render_footer(frame: &mut Frame, app: &App, area: Rect) { - let search = if app.search.active && !app.search.query.is_empty() { - format!( - " search {}/{} '{}'", - app.search.display_index(), - app.search.count(), - app.search.query - ) - } else { - String::new() - }; - let shortcuts = "1/2/3 view c connect r reconnect e config / search Ctrl-S send q quit"; - frame.render_widget( - Paragraph::new(Line::from(vec![ - Span::styled( - format!(" {} ", app.notice), - Style::default().fg(TEXT).bg(PANEL_ALT), - ), - Span::styled(search, Style::default().fg(AMBER).bg(PANEL_ALT)), - Span::styled(" ", Style::default().bg(PANEL_ALT)), - Span::styled(shortcuts, Style::default().fg(MUTED).bg(PANEL_ALT)), - ])), - area, - ); -} - -fn render_search_modal(frame: &mut Frame, app: &mut App) { - let area = centered_rect(70, 7, frame.area()); - app.add_hot_zone(area, HotAction::CloseModal); - frame.render_widget(Clear, area); - let block = panel_block("Search", true, CYAN); - let inner = block.inner(area); - frame.render_widget(block, area); - - let body = vec![ - Line::from(vec![ - Span::styled("Query: ", Style::default().fg(MUTED)), - Span::styled(app.search.query.clone(), Style::default().fg(TEXT)), - Span::styled( - "_", - Style::default().fg(CYAN).add_modifier(Modifier::SLOW_BLINK), - ), - ]), - Line::from(vec![ - Span::styled("Matches: ", Style::default().fg(MUTED)), - Span::styled( - format!("{}/{}", app.search.display_index(), app.search.count()), - Style::default().fg(AMBER), - ), - Span::raw(" "), - Span::styled( - if app.search.case_sensitive { - "case sensitive" - } else { - "case insensitive" - }, - Style::default().fg(MAGENTA), - ), - ]), - Line::styled( - "Enter close Up/Down navigate Ctrl-C case", - Style::default().fg(MUTED), - ), - ]; - - frame.render_widget( - Paragraph::new(body) - .style(Style::default().bg(PANEL)) - .wrap(Wrap { trim: false }), - inner, - ); -} - -fn render_config_modal(frame: &mut Frame, app: &mut App) { - let rows = app.config_form.rows(); - let height = (rows.len() as u16 + 6).min(frame.area().height.saturating_sub(2)); - let area = centered_rect(88, height, frame.area()); - frame.render_widget(Clear, area); - let block = panel_block("Session Config", true, MAGENTA); - let inner = block.inner(area); - frame.render_widget(block, area); - - let footer_height = 3; - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(3), Constraint::Length(footer_height)]) - .split(inner); - - let items = rows - .iter() - .enumerate() - .map(|(index, row)| { - let focused = index == app.config_form.focused; - let value_style = if row.editable { - Style::default().fg(CYAN) - } else { - Style::default().fg(AMBER) - }; - ListItem::new(Line::from(vec![ - Span::styled( - format!("{:<18}", row.label), - Style::default().fg(if focused { Color::Black } else { MUTED }), - ), - Span::styled( - row.value.clone(), - if focused { - value_style.bg(MAGENTA).fg(Color::Black) - } else { - value_style - }, - ), - ])) - .style(if focused { - Style::default().bg(MAGENTA) - } else { - Style::default().bg(PANEL) - }) - }) - .collect::>(); - - for (index, row) in rows.iter().enumerate() { - let y = layout[0].y.saturating_add(index as u16); - if y >= layout[0].y.saturating_add(layout[0].height) { - break; - } - app.add_hot_zone( - Rect { - x: layout[0].x, - y, - width: layout[0].width, - height: 1, - }, - if row.field == ConfigField::Apply { - HotAction::ConfigSubmit - } else { - HotAction::ConfigFocus(row.field) - }, - ); - } - - frame.render_widget( - List::new(items).style(Style::default().bg(PANEL)), - layout[0], - ); - app.add_hot_zone(layout[1], HotAction::ConfigCancel); - frame.render_widget( - Paragraph::new(vec![ - Line::styled( - "Tab move Left/Right change options Enter apply Esc cancel", - Style::default().fg(MUTED), - ), - Line::styled(serial_ports_hint(app), Style::default().fg(BLUE)), - ]) - .style(Style::default().bg(PANEL)), - layout[1], - ); -} - -fn render_help_modal(frame: &mut Frame, app: &mut App) { - let area = centered_rect(74, 12, frame.area()); - app.add_hot_zone(area, HotAction::CloseModal); - frame.render_widget(Clear, area); - let block = panel_block("Help", true, BLUE); - let inner = block.inner(area); - frame.render_widget(block, area); - frame.render_widget( - Paragraph::new(vec![ - Line::styled( - "Keyboard", - Style::default().fg(CYAN).add_modifier(Modifier::BOLD), - ), - Line::raw("1/2/3 switch views, Tab shift focus, q quit"), - Line::raw("c connect, r reconnect, e config, / search, x clear"), - Line::raw("Ctrl-S send, Enter edit/send composer, Esc close modal"), - Line::styled( - "Mouse", - Style::default().fg(AMBER).add_modifier(Modifier::BOLD), - ), - Line::raw("Click tabs, buttons, toggles, config rows, and composer."), - Line::raw("Wheel scrolls Text/Hex and zooms Plot."), - ]) - .style(Style::default().fg(TEXT).bg(PANEL)) - .wrap(Wrap { trim: false }), - inner, - ); -} - -fn format_console_line( - index: usize, - line: &ConsoleLine, - display: DisplayOptions, - app: &App, -) -> Line<'static> { - let mut spans = prefix_spans(line.elapsed, line.direction, &line.pipeline, display); - let base_style = Style::default().fg(TEXT); - spans.extend(ansi_to_spans(&line.text, base_style)); - apply_search_style(index, Line::from(spans), app) -} - -fn format_hex_line( - index: usize, - line: &HexLine, - display: DisplayOptions, - app: &App, -) -> Line<'static> { - let mut spans = prefix_spans(line.elapsed, line.direction, &line.pipeline, display); - spans.push(Span::styled(line.hex.clone(), Style::default().fg(AMBER))); - spans.push(Span::styled(" |", Style::default().fg(MUTED))); - spans.push(Span::styled(line.ascii.clone(), Style::default().fg(TEXT))); - spans.push(Span::styled("|", Style::default().fg(MUTED))); - apply_search_style(index, Line::from(spans), app) -} - -fn prefix_spans( - elapsed: Duration, - direction: LineDirection, - pipeline: &str, - display: DisplayOptions, -) -> Vec> { - let mut spans = Vec::new(); - if display.show_timestamp { - spans.push(Span::styled( - format!("[{}] ", format_elapsed(elapsed)), - Style::default().fg(MUTED), - )); - } - if display.show_direction { - let (label, color) = match direction { - LineDirection::In => ("IN", CYAN), - LineDirection::Out => ("OUT", AMBER), - }; - spans.push(Span::styled( - format!("[{label}] "), - Style::default().fg(color).add_modifier(Modifier::BOLD), - )); - } - if display.show_pipeline { - spans.push(Span::styled( - format!("[{pipeline}] "), - Style::default().fg(MAGENTA), - )); - } - spans -} - -fn apply_search_style(index: usize, line: Line<'static>, app: &App) -> Line<'static> { - if !app.search.active || app.search.query.is_empty() { - return line; - } - - if app.search.current_line() == Some(index) { - line.style(Style::default().bg(AMBER)) - } else if app.search.matches.contains(&index) { - line.style(Style::default().bg(Color::Rgb(47, 61, 91))) - } else { - line - } -} - -fn input_line(app: &App) -> Line<'static> { - if app.session.send_input.is_empty() { - return Line::styled( - match app.session.send_mode { - SendMode::Text => "type text payload", - SendMode::Hex => "hex bytes, e.g. 48 65 6c 6c 6f", - }, - Style::default().fg(MUTED), - ); - } - - if !matches!(app.mode, AppMode::EditingSend) { - return Line::styled(app.session.send_input.clone(), Style::default().fg(TEXT)); - } - - let mut spans = Vec::new(); - let cursor = app.session.input_cursor; - for (index, ch) in app.session.send_input.chars().enumerate() { - if index == cursor { - spans.push(Span::styled( - ch.to_string(), - Style::default().fg(Color::Black).bg(MAGENTA), - )); - } else { - spans.push(Span::styled(ch.to_string(), Style::default().fg(TEXT))); - } - } - if cursor >= app.session.send_input.chars().count() { - spans.push(Span::styled(" ", Style::default().bg(MAGENTA))); - } - Line::from(spans) -} - -fn section_line(label: &'static str) -> (Line<'static>, Option) { - ( - Line::styled( - format!("-- {label} "), - Style::default().fg(MUTED).add_modifier(Modifier::BOLD), - ), - None, - ) -} - -fn spacer_line() -> (Line<'static>, Option) { - (Line::raw(""), None) -} - -fn button_line( - label: &'static str, - color: Color, - action: Option, -) -> (Line<'static>, Option) { - ( - Line::from(vec![Span::styled( - label, - Style::default() - .fg(Color::Black) - .bg(color) - .add_modifier(Modifier::BOLD), - )]), - action, - ) -} - -fn toggle_line( - label: &'static str, - enabled: bool, - action: Option, -) -> (Line<'static>, Option) { - ( - Line::from(vec![ - Span::styled( - if enabled { "[x] " } else { "[ ] " }, - Style::default().fg(if enabled { GREEN } else { MUTED }), - ), - Span::styled(label, Style::default().fg(TEXT)), - ]), - action, - ) -} - -fn value_line( - label: &'static str, - value: &'static str, - action: Option, -) -> (Line<'static>, Option) { - ( - Line::from(vec![ - Span::styled(format!("{label:<8}"), Style::default().fg(MUTED)), - Span::styled(value.to_string(), Style::default().fg(CYAN)), - ]), - action, - ) -} - -fn panel_block(title: &str, focused: bool, color: Color) -> Block<'_> { - Block::default() - .borders(Borders::ALL) - .title(Span::styled( - format!(" {title} "), - Style::default() - .fg(if focused { Color::Black } else { color }) - .bg(if focused { color } else { PANEL }) - .add_modifier(Modifier::BOLD), - )) - .border_style(Style::default().fg(if focused { - color - } else { - Color::Rgb(45, 58, 74) - })) - .style(Style::default().bg(PANEL)) -} - -fn axis(title: &'static str, min: f64, max: f64, color: Color) -> Axis<'static> { - Axis::default() - .title(title) - .style(Style::default().fg(color)) - .bounds([min, max]) - .labels(vec![ - Span::styled(format!("{min:.2}"), Style::default().fg(MUTED)), - Span::styled(format!("{max:.2}"), Style::default().fg(MUTED)), - ]) -} - -fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { - let vertical = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Fill(1), - Constraint::Length(height.min(area.height)), - Constraint::Fill(1), - ]) - .split(area); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Fill(1), - Constraint::Length(width.min(area.width)), - Constraint::Fill(1), - ]) - .split(vertical[1])[1] -} - -fn status_color(status: &ConnectionStatus) -> Color { - match status { - ConnectionStatus::Connected => GREEN, - ConnectionStatus::Disconnected => MUTED, - ConnectionStatus::Connecting => AMBER, - ConnectionStatus::Error(_) => RED, - } -} - -fn view_color(view: View) -> Color { - match view { - View::Text => CYAN, - View::Hex => AMBER, - View::Plot => GREEN, - } -} - -fn format_elapsed(elapsed: Duration) -> String { - let secs = elapsed.as_secs_f64(); - if secs < 60.0 { - format!("{secs:05.2}") - } else { - format!("{:02}:{:02}", (secs / 60.0) as u64, (secs % 60.0) as u64) - } -} - -fn next_line_ending(current: LineEnding) -> LineEnding { - let endings = LineEnding::ALL; - let index = endings - .iter() - .position(|ending| *ending == current) - .unwrap_or(0); - endings[(index + 1) % endings.len()] -} - -fn serial_ports_hint(app: &App) -> String { - if app.config_form.available_serial_ports.is_empty() { - String::from("Serial ports: none detected") - } else { - format!( - "Serial ports: {}", - app.config_form.available_serial_ports.join(", ") - ) - } -}