From c6dff58d8a86e3a7c78d7c0917bbed0257fbfcff Mon Sep 17 00:00:00 2001 From: Trevor Merritt Date: Tue, 22 Apr 2025 08:56:18 -0400 Subject: [PATCH] Adds colour to the text display optimized main loop splits code into structs to better structure tui shows hosts from the app state single display shows the stats for a passed TargetState multi display is woefully incomplete --- Cargo.lock | 636 +++++++++++++++++++++++++++++++- Cargo.toml | 5 + src/bin/pp.rs | 152 ++------ src/bin/rat.rs | 139 +++++++ src/lib.rs | 3 +- src/manager.rs | 57 ++- src/ppstate.rs | 145 ++++++++ src/target_state.rs | 13 + src/tui/mod.rs | 2 + src/tui/multi_target_display.rs | 15 + src/tui/target_display.rs | 29 ++ 11 files changed, 1049 insertions(+), 147 deletions(-) create mode 100644 src/bin/rat.rs create mode 100644 src/ppstate.rs create mode 100644 src/tui/mod.rs create mode 100644 src/tui/multi_target_display.rs create mode 100644 src/tui/target_display.rs diff --git a/Cargo.lock b/Cargo.lock index 52b4efa..4abb7ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "ansi_term" version = "0.12.1" @@ -56,7 +77,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -67,9 +88,66 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", - "windows-sys", + "windows-sys 0.59.0", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.5.36" @@ -110,12 +188,78 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "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 = "csv" version = "1.3.1" @@ -137,6 +281,47 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "env_filter" version = "0.1.3" @@ -160,18 +345,113 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -202,24 +482,132 @@ dependencies = [ "syn", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "portable-atomic" version = "1.11.0" @@ -241,9 +629,12 @@ version = "0.1.1" dependencies = [ "ansi_term", "clap", + "color-eyre", + "crossterm", "csv", "env_logger", "log", + "ratatui", ] [[package]] @@ -264,6 +655,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.11.1" @@ -293,12 +714,43 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -319,12 +771,91 @@ dependencies = [ "syn", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.100" @@ -336,18 +867,110 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -370,6 +993,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 706ebab..c8d40e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,11 @@ ansi_term = "0.12" clap = { version = "4.5", features = ["derive"] } csv = "1.3" +# Ratatui +color-eyre = "0.6.3" +crossterm = "0.28.1" +ratatui = "0.29.0" + [[bin]] name = "duration_to_string" diff --git a/src/bin/pp.rs b/src/bin/pp.rs index 06cde4c..39791b4 100644 --- a/src/bin/pp.rs +++ b/src/bin/pp.rs @@ -14,101 +14,10 @@ use pp::manager::Manager; use pp::SECONDS_BETWEEN_DISPLAY; use pp::target_state::TargetState; use std::{env, error::Error, ffi::OsString, process}; - -fn get_default_targets() -> BTreeMap { - let mut working = BTreeMap::new(); - working.insert("Localhost".to_string(), - TargetState { - name: "Localhost".to_string(), - target: Ipv4Addr::new(127, 0, 0, 1), - ..TargetState::default() - }, - ); - - working.insert("Home Gateway".to_string(), - TargetState { - name: "Home Gateway".to_string(), - target: Ipv4Addr::new(172, 24, 0, 1), - ..TargetState::default() - }, - ); - - working.insert("1111 DNS".to_string(), - TargetState { - name: "1111 DNS".to_string(), - target: Ipv4Addr::new(1, 1, 1, 1), - ..TargetState::default() - }, - ); - working.insert("Google DNS".to_string(), - TargetState { - name: "Google DNS".to_string(), - target: Ipv4Addr::new(8, 8, 8, 8), - ..TargetState::default() - }, - ); - - working.insert("Site IP 1".to_string(), - TargetState { - name: "Site IP 1".to_string(), - target: Ipv4Addr::new(216, 121, 247, 231), - ..TargetState::default() - }, - ); - working.insert("Site IP 2".to_string(), - TargetState { - name: "Site IP 2".to_string(), - target: Ipv4Addr::new(216, 234, 202, 122), - ..TargetState::default() - }, - ); - working.insert("Site IP 3".to_string(), - TargetState { - name: "Site IP 3".to_string(), - target: Ipv4Addr::new(24, 143, 184, 98), - ..TargetState::default() - }, - ); - working -} - -fn build_targets_from_file(filename: Option) -> BTreeMap { - let mut hosts: BTreeMap = get_default_targets(); - - if let Some(file) = filename { - hosts = if !&file.exists() { - println!("Cant load hosts from {:?}. Using default host list.", file.clone().as_os_str()); - // use - get_default_targets() - } else { - println!("LOADING HOSTS FROM {:?}", file.to_str()); - let file = File::open(file); - let mut rdr = csv::Reader::from_reader(file.unwrap()); - for result in rdr.records() { - let record = result.unwrap(); - hosts.insert(record[1].to_string(), - TargetState { - name: record[1].to_string(), - target: Ipv4Addr::from_str(&record[0]).unwrap(), - alive: false, - last_alive_change: SystemTime::now(), - last_rtt: 0, - }); - } - hosts - } - } - - hosts -} - -fn ips_from_state(to_read_from: BTreeMap) -> Vec { - let mut working: Vec = vec![]; - for current in to_read_from { - working.push(current.1.target); - } - working -} +use color_eyre::owo_colors::OwoColorize; +use crossterm::style::Stylize; +use log::debug; +use pp::ppstate::PPState; /// Simple program to greet a person #[derive(Parser, Debug)] @@ -123,59 +32,52 @@ fn main() { // Get App Settings let settings = AppSettings::parse(); - print!("Prep to load targets..."); - let mut targets = build_targets_from_file(settings.ping_host_file); + print!("Creating new State..."); + let mut state = PPState::new_from_file(settings.ping_host_file.unwrap_or(PathBuf::from("INVALID FILE"))); // channel to send requests to ping let (ping_response_sender, ping_response_listener) = mpsc::channel::(); - println!("Setting up requests for {} hosts.", targets.len()); - Manager::spawn_manager_thread(ips_from_state(targets.clone()), ping_response_sender.clone()); + println!("Setting up requests for {} hosts.", state.ips_from_state().len()); + Manager::spawn_manager_thread(PPState::targets_states_from_state(&state), ping_response_sender.clone()); let mut display_loop_start = SystemTime::now(); let mut duration_since_last_loop = SystemTime::now().duration_since(display_loop_start).unwrap(); loop { - if let Ok(response) = ping_response_listener.recv_timeout(Duration::from_millis(100)) { - for (_, (name, current_state)) in targets.clone().iter().enumerate() { - if current_state.target == response.target { - let last_alive_change = if response.success == current_state.alive { - current_state.last_alive_change - } else { - SystemTime::now() - }; + state.check_for_ping(&ping_response_listener); - let new_state = TargetState { - name: current_state.name.clone(), - target: current_state.target, - alive: response.success, - last_rtt: response.rtt, - last_alive_change, - }; - targets.insert(name.clone(), new_state); - } - } - } - duration_since_last_loop = SystemTime::now() + let now = SystemTime::now(); + + + duration_since_last_loop = now .duration_since(display_loop_start) .expect("unable to figure out how long ago we displayed stuff"); if duration_since_last_loop.as_secs() > SECONDS_BETWEEN_DISPLAY as u64 { println!("DISPLAY LOOP"); println!("Host \t\t\t\t\t | Alive \t | RTT \t\t"); - for (name, current_result) in targets.clone() { - let mut target_string = String::new(); - let time_since_last_change = SystemTime::now().duration_since(current_result.last_alive_change).unwrap(); - target_string = format!("{} ({})", name, current_result.target); + for current_result in state.targets_states_from_state() { + let time_since_last_change = now + .duration_since(current_result.last_alive_change) + .unwrap_or(Duration::from_secs(0)); + let mut target_string = format!("{} ({})", current_result.name, current_result.target); while target_string.len() < 34 { - target_string = format!("{} ", target_string); + target_string.push(' '); + // target_string = format!("{} ", target_string); } + target_string = if current_result.alive { + target_string.green().to_string() + } else { + target_string.red().to_string() + }; + println!("{} \t | {} \t | {}\t | Changed {}s ago", target_string, current_result.alive, current_result.last_rtt, time_since_last_change.as_secs() ); } - display_loop_start = SystemTime::now(); + display_loop_start = now; } } } diff --git a/src/bin/rat.rs b/src/bin/rat.rs new file mode 100644 index 0000000..6b43b3c --- /dev/null +++ b/src/bin/rat.rs @@ -0,0 +1,139 @@ +use pp::tui::target_display::TargetDisplay; +use std::net::Ipv4Addr; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; +use std::time::Duration; + +use color_eyre::Result; +use crossterm::event; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use pp::ping_result::PingResult; +use pp::target_state::TargetState; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::{DefaultTerminal, Frame}; +use std::collections::BTreeMap; +use std::time::SystemTime; + +#[derive(Default)] +pub struct App { + running: bool, + counter: u32, + known_hosts: Vec, + targets: BTreeMap, + result_tx: Option>, + result_rx: Option>, + log_events: Vec, +} + +impl App { + pub fn new() -> Self { + let mut working = Self::default(); + working.known_hosts.push(Ipv4Addr::new(127, 0, 0, 1)); + working.known_hosts.push(Ipv4Addr::new(8, 8, 8, 8)); + working.known_hosts.push(Ipv4Addr::new(1, 1, 1, 1)); + + working + .targets + .insert("Test Host 1".to_string(), TargetState { + name: "Test Host 1".to_string(), + target: Ipv4Addr::new(127, 0, 0, 1), + ..Default::default() + }); + + working + .targets + .insert("Test Host 2".to_string(), TargetState { + target: Ipv4Addr::new(1, 1, 1, 1), + name: "Test Host 2".to_string(), + ..Default::default() + }); + + let (sender, receiver) = mpsc::channel::(); + working.result_tx = Some(sender); + working.result_rx = Some(receiver); + working + } + + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + self.running = true; + while self.running { + // check if we have any waiting ping responses + if let Ok(response) = self + .result_rx + .as_mut() + .unwrap() + .recv_timeout(Duration::from_millis(50)) + { + let local_targets = self.targets.clone(); + for (name, current_state) in local_targets { + if current_state.target == response.target { + let last_alive_change = if response.success == current_state.alive { + current_state.last_alive_change + } else { + SystemTime::now() + }; + + let new_state = TargetState { + name: name.clone(), + target: current_state.target, + alive: response.success, + last_rtt: response.rtt, + last_alive_change, + }; + self.targets.insert(name.clone(), new_state); + //self.set_target_state(current_state.name.clone(), new_state); + } + } + } + terminal.draw(|frame| self.render(frame))?; + self.handle_crossterm_events()?; + } + Ok(()) + } + + fn render(&mut self, frame: &mut Frame) { + let title = Line::from("PP Status").bold().centered().blue(); + + let mut body_string = String::new(); + for (name, target) in &self.targets { + frame.render_widget(TargetDisplay::new(target.clone()), frame.area()); + body_string = format!("{}\n{} = {}", body_string, target.name, target.target); + } + + frame.render_widget(Paragraph::new(body_string), frame.area()); + } + + fn handle_crossterm_events(&mut self) -> Result<()> { + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_event(key), + Event::Mouse(_) => {} + Event::Resize(_, _) => {} + _ => {} + } + Ok(()) + } + + fn on_key_event(&mut self, key: KeyEvent) { + match (key.modifiers, key.code) { + (_, KeyCode::Esc | KeyCode::Char('q')) + | (KeyModifiers::CONTROL, KeyCode::Char('c') | KeyCode::Char('C')) => self.quit(), + _ => {} + } + } + + fn quit(&mut self) { + self.running = false; + } +} + +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let result = App::new().run(terminal); + ratatui::restore(); + println!("Exited."); + result +} diff --git a/src/lib.rs b/src/lib.rs index d006f5c..61ca8da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ pub mod manager; pub mod ping_request; pub mod ping_result; +pub mod ppstate; pub mod target_state; - +pub mod tui; pub const SECONDS_BETWEEN_DISPLAY: u32 = 1; pub const SECONDS_BETWEEN_PING: u32 = 2; diff --git a/src/manager.rs b/src/manager.rs index 700c25f..e7d6516 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -1,11 +1,12 @@ +use crate::SECONDS_BETWEEN_PING; +use crate::ping_request::PingRequest; +use crate::ping_result::PingResult; +use crate::target_state::TargetState; use std::net::Ipv4Addr; use std::process::Command; use std::sync::mpsc::Sender; use std::thread; use std::time::{Duration, SystemTime}; -use crate::ping_request::PingRequest; -use crate::ping_result::PingResult; -use crate::SECONDS_BETWEEN_PING; pub struct Manager; @@ -13,12 +14,17 @@ const WIN_PING_SUCCESS: i32 = 1; const LIN_PING_SUCCESS: i32 = 0; impl Manager { - pub fn spawn_manager_thread(targets: Vec, sender: Sender) { + pub fn spawn_manager_thread(targets: Vec, sender: Sender) { let local_targets = targets.clone(); thread::spawn(move || { loop { for target in &local_targets { - Manager::spawn_single_ping(PingRequest { target: *target }, sender.clone()); + Manager::spawn_single_ping( + PingRequest { + target: target.target, + }, + sender.clone(), + ); } thread::sleep(Duration::from_secs(SECONDS_BETWEEN_PING as u64)); } @@ -26,40 +32,53 @@ impl Manager { } // Spawns a thread that pings a target and sends the response back to the main thread -// via the sender + // via the sender fn spawn_single_ping(request: PingRequest, sender: Sender) { let local_request = request.clone(); thread::spawn(move || { - let mut result = 0; let mut success = true; let start_time = SystemTime::now(); - #[cfg(any(target_os="windows"))] { - result = Command::new("c:/windows/system32/ping.exe").arg(request.target.to_string()) + #[cfg(any(target_os = "windows"))] + { + result = Command::new("c:/windows/system32/ping.exe") + .arg(request.target.to_string()) .arg("-n 1") .arg(format!("-w {}", crate::SECONDS_BETWEEN_PING)) .arg("-4") - .output().unwrap().status.code().unwrap_or(255); - success = result == WIN_PING_SUCCESS as i32;; + .output() + .unwrap() + .status + .code() + .unwrap_or(255); + success = result == WIN_PING_SUCCESS as i32; } - #[cfg(any(target_os="linux"))] { - result = Command::new("/usr/bin/ping").arg(request.target.to_string()) + #[cfg(any(target_os = "linux"))] + { + result = Command::new("/usr/bin/ping") + .arg(request.target.to_string()) .arg("-c 1") .arg("-4") .arg(format!("-w {}", SECONDS_BETWEEN_PING)) .arg("-W 1") - .output().unwrap().status.code().unwrap_or(255); + .output() + .unwrap() + .status + .code() + .unwrap_or(255); success = result == LIN_PING_SUCCESS as i32; } let ping_duration = SystemTime::now() .duration_since(start_time) .unwrap_or(Duration::from_secs(600)); let success = ping_duration.as_millis() >= SECONDS_BETWEEN_PING as u128 && success; - sender.send(PingResult { - target: local_request.target, - success, - rtt: ping_duration.as_millis() as u32 - }).expect("Unable to send response"); + sender + .send(PingResult { + target: local_request.target, + success, + rtt: ping_duration.as_millis() as u32, + }) + .expect("Unable to send response"); // println!("Attempt for for {}", local_request.target); }); diff --git a/src/ppstate.rs b/src/ppstate.rs new file mode 100644 index 0000000..e1ffbd3 --- /dev/null +++ b/src/ppstate.rs @@ -0,0 +1,145 @@ +use crate::ping_result::PingResult; +use crate::target_state::TargetState; +use log::debug; +use std::collections::BTreeMap; +use std::fs::File; +use std::net::Ipv4Addr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::mpsc::Receiver; +use std::time::{Duration, SystemTime}; + +#[derive(Default)] +pub struct PPState { + targets: BTreeMap, +} + +impl PPState { + pub fn check_for_ping(&mut self, channel: &Receiver) { + if let Ok(response) = channel.recv_timeout(Duration::from_millis(100)) { + let local_targets = self.targets_states_from_state().clone(); + for current_state in local_targets { + if current_state.target == response.target { + let last_alive_change = if response.success == current_state.alive { + current_state.last_alive_change + } else { + SystemTime::now() + }; + + let new_state = TargetState { + name: current_state.name.clone(), + target: current_state.target, + alive: response.success, + last_rtt: response.rtt, + last_alive_change, + }; + self.set_target_state(current_state.name.clone(), new_state); + } + } + } + } + + pub fn new_from_file(file_to_load_from: PathBuf) -> Self { + let mut working = Self::new(); + working.targets = PPState::build_targets_from_file(Some(file_to_load_from)); + working + } + pub fn new() -> Self { + Self::default() + } + pub fn get_default_targets() -> BTreeMap { + let mut working = BTreeMap::new(); + working.insert("Localhost".to_string(), TargetState { + name: "Localhost".to_string(), + target: Ipv4Addr::new(127, 0, 0, 1), + ..TargetState::default() + }); + + working.insert("Home Gateway".to_string(), TargetState { + name: "Home Gateway".to_string(), + target: Ipv4Addr::new(172, 24, 0, 1), + ..TargetState::default() + }); + + working.insert("1111 DNS".to_string(), TargetState { + name: "1111 DNS".to_string(), + target: Ipv4Addr::new(1, 1, 1, 1), + ..TargetState::default() + }); + working.insert("Google DNS".to_string(), TargetState { + name: "Google DNS".to_string(), + target: Ipv4Addr::new(8, 8, 8, 8), + ..TargetState::default() + }); + + working.insert("Site IP 1".to_string(), TargetState { + name: "Site IP 1".to_string(), + target: Ipv4Addr::new(216, 121, 247, 231), + ..TargetState::default() + }); + working.insert("Site IP 2".to_string(), TargetState { + name: "Site IP 2".to_string(), + target: Ipv4Addr::new(216, 234, 202, 122), + ..TargetState::default() + }); + working.insert("Site IP 3".to_string(), TargetState { + name: "Site IP 3".to_string(), + target: Ipv4Addr::new(24, 143, 184, 98), + ..TargetState::default() + }); + working + } + + pub fn build_targets_from_file(filename: Option) -> BTreeMap { + PPState::get_default_targets(); + if let Some(file) = filename { + let mut working = BTreeMap::new(); + if !&file.exists() { + debug!( + "Cant load hosts from {:?}. Using default host list.", + file.clone().as_os_str() + ); + // use + PPState::get_default_targets() + } else { + debug!("LOADING HOSTS FROM {:?}", file.to_str()); + let file = File::open(file); + let mut rdr = csv::Reader::from_reader(file.unwrap()); + for result in rdr.records() { + let record = result.unwrap(); + working.insert(record[1].to_string(), TargetState { + name: record[1].to_string(), + target: Ipv4Addr::from_str(&record[0]).unwrap(), + alive: false, + last_alive_change: SystemTime::now(), + last_rtt: 0, + }); + } + working + } + } else { + PPState::get_default_targets() + } + } + + pub fn ips_from_state(&self) -> Vec { + let mut working: Vec = vec![]; + for (name, current) in &self.targets { + working.push(current.target); + } + working + } + + pub fn targets_states_from_state(&self) -> Vec { + let mut working: Vec = vec![]; + for (_, target) in &self.targets { + working.push(target.clone()); + } + + working + } + + pub fn set_target_state(&mut self, key: String, value: TargetState) { + self.targets.insert(key, value); + } +} diff --git a/src/target_state.rs b/src/target_state.rs index c306962..d1a7f10 100644 --- a/src/target_state.rs +++ b/src/target_state.rs @@ -1,5 +1,10 @@ use std::net::Ipv4Addr; use std::time::SystemTime; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::{StatefulWidget, Style}; +use ratatui::style::Color; +use ratatui::widgets::Widget; #[derive(Clone, Debug)] pub struct TargetState { @@ -21,3 +26,11 @@ impl Default for TargetState { } } } +struct TargetStateWidget; + +impl StatefulWidget for TargetStateWidget { + type State = TargetStateWidget; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + buf.set_string(area.left(),area.top(),"This is the string set ", Style::default().fg(Color::Red)); + } +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..597c943 --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,2 @@ +pub mod target_display; +mod multi_target_display; diff --git a/src/tui/multi_target_display.rs b/src/tui/multi_target_display.rs new file mode 100644 index 0000000..616c8dc --- /dev/null +++ b/src/tui/multi_target_display.rs @@ -0,0 +1,15 @@ +use ratatui::buffer::{Buffer, Cell}; +use ratatui::layout::Rect; +use ratatui::text::Line; +use ratatui::widgets::{Widget}; + +pub struct MultiTargetDisplay { + +} + +impl Widget for MultiTargetDisplay { + fn render(self, area: Rect, buf: &mut Buffer) + { + buf.content.push(Cell::from("Widgets go here")); + } +} diff --git a/src/tui/target_display.rs b/src/tui/target_display.rs new file mode 100644 index 0000000..ecd7031 --- /dev/null +++ b/src/tui/target_display.rs @@ -0,0 +1,29 @@ +use crate::target_state::TargetState; +use ratatui::prelude::Buffer; +use ratatui::prelude::Rect; +use ratatui::style::Style; +use ratatui::widgets::Widget; + +#[derive(Default)] +pub struct TargetDisplay { + target: TargetState, +} + +impl TargetDisplay { + pub fn new(state: TargetState) -> Self { + let mut working = TargetDisplay::default(); + working.target = state; + working + } +} + +impl Widget for TargetDisplay { + fn render(self, area: Rect, buf: &mut Buffer) { + let row = format!( + "{} -> {} / {} / {}", + self.target.name, self.target.target, self.target.alive, self.target.last_rtt + ); + + buf.set_string(area.x, area.y, row, Style::default()); + } +}