├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── colors.rs ├── info.rs ├── list.rs ├── main.rs ├── search.rs ├── utils.rs └── widget.rs └── static └── mdns.gif /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .cargo-ok 3 | .DS_Store 4 | /.idea 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - v0.1.0 Initial implementation. 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project adheres to the Rust Code of Conduct, which can be found [here](https://www.rust-lang.org/conduct.html). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | First off, thank you for considering contributing to discovery-rs. 4 | 5 | If your contribution is not straightforward, please first discuss the change you 6 | wish to make by creating a new issue before making the change. 7 | 8 | ## Reporting issues 9 | 10 | Before reporting an issue on the 11 | [issue tracker](https://github.com/JustPretender/discovery-rs/issues), 12 | please check that it has not already been reported by searching for some related 13 | keywords. 14 | 15 | ## Pull requests 16 | 17 | Try to do one pull request per change. 18 | 19 | ### Updating the changelog 20 | 21 | Update the changes you have made in 22 | [CHANGELOG](https://github.com/JustPretender/discovery-rs/blob/main/CHANGELOG.md) 23 | file under the **Unreleased** section. 24 | 25 | Add the changes of your pull request to one of the following subsections, 26 | depending on the types of changes defined by 27 | [Keep a changelog](https://keepachangelog.com/en/1.0.0/): 28 | 29 | - `Added` for new features. 30 | - `Changed` for changes in existing functionality. 31 | - `Deprecated` for soon-to-be removed features. 32 | - `Removed` for now removed features. 33 | - `Fixed` for any bug fixes. 34 | - `Security` in case of vulnerabilities. 35 | 36 | If the required subsection does not exist yet under **Unreleased**, create it! 37 | 38 | ## Developing 39 | 40 | ### Set up 41 | 42 | This is no different than other Rust projects. 43 | 44 | ```shell 45 | git clone https://github.com/JustPretender/discovery-rs 46 | cd discovery-rs 47 | cargo test 48 | ``` 49 | 50 | ### Useful Commands 51 | 52 | - Build and run release version: 53 | 54 | ```shell 55 | cargo build --release && cargo run --release 56 | ``` 57 | 58 | - Run Clippy: 59 | 60 | ```shell 61 | cargo clippy --all-targets --all-features --workspace 62 | ``` 63 | 64 | - Run all tests: 65 | 66 | ```shell 67 | cargo test --all-features --workspace 68 | ``` 69 | 70 | - Check to see if there are code formatting issues 71 | 72 | ```shell 73 | cargo fmt --all -- --check 74 | ``` 75 | 76 | - Format the code in the project 77 | 78 | ```shell 79 | cargo fmt --all 80 | ``` 81 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.18" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 46 | 47 | [[package]] 48 | name = "anstream" 49 | version = "0.6.14" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 52 | dependencies = [ 53 | "anstyle", 54 | "anstyle-parse", 55 | "anstyle-query", 56 | "anstyle-wincon", 57 | "colorchoice", 58 | "is_terminal_polyfill", 59 | "utf8parse", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle" 64 | version = "1.0.7" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 67 | 68 | [[package]] 69 | name = "anstyle-parse" 70 | version = "0.2.4" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 73 | dependencies = [ 74 | "utf8parse", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle-query" 79 | version = "1.0.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 82 | dependencies = [ 83 | "windows-sys 0.52.0", 84 | ] 85 | 86 | [[package]] 87 | name = "anstyle-wincon" 88 | version = "3.0.3" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 91 | dependencies = [ 92 | "anstyle", 93 | "windows-sys 0.52.0", 94 | ] 95 | 96 | [[package]] 97 | name = "anyhow" 98 | version = "1.0.86" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 101 | 102 | [[package]] 103 | name = "autocfg" 104 | version = "1.3.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 107 | 108 | [[package]] 109 | name = "backtrace" 110 | version = "0.3.71" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 113 | dependencies = [ 114 | "addr2line", 115 | "cc", 116 | "cfg-if", 117 | "libc", 118 | "miniz_oxide", 119 | "object", 120 | "rustc-demangle", 121 | ] 122 | 123 | [[package]] 124 | name = "bitflags" 125 | version = "1.3.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 128 | 129 | [[package]] 130 | name = "bitflags" 131 | version = "2.5.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 134 | 135 | [[package]] 136 | name = "bumpalo" 137 | version = "3.16.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 140 | 141 | [[package]] 142 | name = "cassowary" 143 | version = "0.3.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 146 | 147 | [[package]] 148 | name = "castaway" 149 | version = "0.2.2" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 152 | dependencies = [ 153 | "rustversion", 154 | ] 155 | 156 | [[package]] 157 | name = "cc" 158 | version = "1.0.98" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 161 | 162 | [[package]] 163 | name = "cfg-if" 164 | version = "1.0.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 167 | 168 | [[package]] 169 | name = "clap" 170 | version = "4.5.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 173 | dependencies = [ 174 | "clap_builder", 175 | ] 176 | 177 | [[package]] 178 | name = "clap_builder" 179 | version = "4.5.2" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 182 | dependencies = [ 183 | "anstream", 184 | "anstyle", 185 | "clap_lex", 186 | "strsim", 187 | ] 188 | 189 | [[package]] 190 | name = "clap_derive" 191 | version = "4.5.4" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 194 | dependencies = [ 195 | "heck 0.5.0", 196 | "proc-macro2", 197 | "quote", 198 | "syn", 199 | ] 200 | 201 | [[package]] 202 | name = "clap_lex" 203 | version = "0.7.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 206 | 207 | [[package]] 208 | name = "color-eyre" 209 | version = "0.6.3" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 212 | dependencies = [ 213 | "backtrace", 214 | "color-spantrace", 215 | "eyre", 216 | "indenter", 217 | "once_cell", 218 | "owo-colors", 219 | "tracing-error", 220 | ] 221 | 222 | [[package]] 223 | name = "color-spantrace" 224 | version = "0.2.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 227 | dependencies = [ 228 | "once_cell", 229 | "owo-colors", 230 | "tracing-core", 231 | "tracing-error", 232 | ] 233 | 234 | [[package]] 235 | name = "colorchoice" 236 | version = "1.0.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 239 | 240 | [[package]] 241 | name = "compact_str" 242 | version = "0.7.1" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 245 | dependencies = [ 246 | "castaway", 247 | "cfg-if", 248 | "itoa", 249 | "ryu", 250 | "static_assertions", 251 | ] 252 | 253 | [[package]] 254 | name = "concurrent-queue" 255 | version = "2.5.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 258 | dependencies = [ 259 | "crossbeam-utils", 260 | ] 261 | 262 | [[package]] 263 | name = "crossbeam-channel" 264 | version = "0.5.13" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 267 | dependencies = [ 268 | "crossbeam-utils", 269 | ] 270 | 271 | [[package]] 272 | name = "crossbeam-utils" 273 | version = "0.8.20" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 276 | 277 | [[package]] 278 | name = "crossterm" 279 | version = "0.27.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 282 | dependencies = [ 283 | "bitflags 2.5.0", 284 | "crossterm_winapi", 285 | "libc", 286 | "mio", 287 | "parking_lot", 288 | "signal-hook", 289 | "signal-hook-mio", 290 | "winapi", 291 | ] 292 | 293 | [[package]] 294 | name = "crossterm_winapi" 295 | version = "0.9.1" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 298 | dependencies = [ 299 | "winapi", 300 | ] 301 | 302 | [[package]] 303 | name = "deranged" 304 | version = "0.3.11" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 307 | dependencies = [ 308 | "powerfmt", 309 | ] 310 | 311 | [[package]] 312 | name = "discovery-rs" 313 | version = "0.1.1" 314 | dependencies = [ 315 | "anyhow", 316 | "clap", 317 | "clap_derive", 318 | "color-eyre", 319 | "crossterm", 320 | "flume", 321 | "mdns-sd", 322 | "parking_lot", 323 | "ratatui", 324 | "regex", 325 | "textwrap", 326 | "tracing", 327 | "tracing-appender", 328 | "tracing-subscriber", 329 | ] 330 | 331 | [[package]] 332 | name = "either" 333 | version = "1.12.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" 336 | 337 | [[package]] 338 | name = "eyre" 339 | version = "0.6.12" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 342 | dependencies = [ 343 | "indenter", 344 | "once_cell", 345 | ] 346 | 347 | [[package]] 348 | name = "flume" 349 | version = "0.11.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" 352 | dependencies = [ 353 | "futures-core", 354 | "futures-sink", 355 | "nanorand", 356 | "spin", 357 | ] 358 | 359 | [[package]] 360 | name = "futures-core" 361 | version = "0.3.30" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 364 | 365 | [[package]] 366 | name = "futures-sink" 367 | version = "0.3.30" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 370 | 371 | [[package]] 372 | name = "getrandom" 373 | version = "0.2.15" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 376 | dependencies = [ 377 | "cfg-if", 378 | "js-sys", 379 | "libc", 380 | "wasi", 381 | "wasm-bindgen", 382 | ] 383 | 384 | [[package]] 385 | name = "gimli" 386 | version = "0.28.1" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 389 | 390 | [[package]] 391 | name = "hashbrown" 392 | version = "0.14.5" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 395 | dependencies = [ 396 | "ahash", 397 | "allocator-api2", 398 | ] 399 | 400 | [[package]] 401 | name = "heck" 402 | version = "0.4.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 405 | 406 | [[package]] 407 | name = "heck" 408 | version = "0.5.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 411 | 412 | [[package]] 413 | name = "if-addrs" 414 | version = "0.10.2" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" 417 | dependencies = [ 418 | "libc", 419 | "windows-sys 0.48.0", 420 | ] 421 | 422 | [[package]] 423 | name = "indenter" 424 | version = "0.3.3" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 427 | 428 | [[package]] 429 | name = "is_terminal_polyfill" 430 | version = "1.70.0" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 433 | 434 | [[package]] 435 | name = "itertools" 436 | version = "0.12.1" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 439 | dependencies = [ 440 | "either", 441 | ] 442 | 443 | [[package]] 444 | name = "itoa" 445 | version = "1.0.11" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 448 | 449 | [[package]] 450 | name = "js-sys" 451 | version = "0.3.69" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 454 | dependencies = [ 455 | "wasm-bindgen", 456 | ] 457 | 458 | [[package]] 459 | name = "lazy_static" 460 | version = "1.4.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 463 | 464 | [[package]] 465 | name = "libc" 466 | version = "0.2.155" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 469 | 470 | [[package]] 471 | name = "lock_api" 472 | version = "0.4.12" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 475 | dependencies = [ 476 | "autocfg", 477 | "scopeguard", 478 | ] 479 | 480 | [[package]] 481 | name = "log" 482 | version = "0.4.21" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 485 | 486 | [[package]] 487 | name = "lru" 488 | version = "0.12.3" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 491 | dependencies = [ 492 | "hashbrown", 493 | ] 494 | 495 | [[package]] 496 | name = "matchers" 497 | version = "0.1.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 500 | dependencies = [ 501 | "regex-automata 0.1.10", 502 | ] 503 | 504 | [[package]] 505 | name = "mdns-sd" 506 | version = "0.11.1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "807457e493076539ff8f202806f9dc2eaa9f13f69701da7ed38eec7a9afd1616" 509 | dependencies = [ 510 | "flume", 511 | "if-addrs", 512 | "log", 513 | "polling", 514 | "socket2", 515 | ] 516 | 517 | [[package]] 518 | name = "memchr" 519 | version = "2.7.2" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 522 | 523 | [[package]] 524 | name = "miniz_oxide" 525 | version = "0.7.3" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" 528 | dependencies = [ 529 | "adler", 530 | ] 531 | 532 | [[package]] 533 | name = "mio" 534 | version = "0.8.11" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 537 | dependencies = [ 538 | "libc", 539 | "log", 540 | "wasi", 541 | "windows-sys 0.48.0", 542 | ] 543 | 544 | [[package]] 545 | name = "nanorand" 546 | version = "0.7.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 549 | dependencies = [ 550 | "getrandom", 551 | ] 552 | 553 | [[package]] 554 | name = "nu-ansi-term" 555 | version = "0.46.0" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 558 | dependencies = [ 559 | "overload", 560 | "winapi", 561 | ] 562 | 563 | [[package]] 564 | name = "num-conv" 565 | version = "0.1.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 568 | 569 | [[package]] 570 | name = "object" 571 | version = "0.32.2" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 574 | dependencies = [ 575 | "memchr", 576 | ] 577 | 578 | [[package]] 579 | name = "once_cell" 580 | version = "1.19.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 583 | 584 | [[package]] 585 | name = "overload" 586 | version = "0.1.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 589 | 590 | [[package]] 591 | name = "owo-colors" 592 | version = "3.5.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 595 | 596 | [[package]] 597 | name = "parking_lot" 598 | version = "0.12.3" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 601 | dependencies = [ 602 | "lock_api", 603 | "parking_lot_core", 604 | ] 605 | 606 | [[package]] 607 | name = "parking_lot_core" 608 | version = "0.9.10" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 611 | dependencies = [ 612 | "cfg-if", 613 | "libc", 614 | "redox_syscall", 615 | "smallvec", 616 | "windows-targets 0.52.5", 617 | ] 618 | 619 | [[package]] 620 | name = "paste" 621 | version = "1.0.15" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 624 | 625 | [[package]] 626 | name = "pin-project-lite" 627 | version = "0.2.14" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 630 | 631 | [[package]] 632 | name = "polling" 633 | version = "2.8.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" 636 | dependencies = [ 637 | "autocfg", 638 | "bitflags 1.3.2", 639 | "cfg-if", 640 | "concurrent-queue", 641 | "libc", 642 | "log", 643 | "pin-project-lite", 644 | "windows-sys 0.48.0", 645 | ] 646 | 647 | [[package]] 648 | name = "powerfmt" 649 | version = "0.2.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 652 | 653 | [[package]] 654 | name = "proc-macro2" 655 | version = "1.0.83" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" 658 | dependencies = [ 659 | "unicode-ident", 660 | ] 661 | 662 | [[package]] 663 | name = "quote" 664 | version = "1.0.36" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 667 | dependencies = [ 668 | "proc-macro2", 669 | ] 670 | 671 | [[package]] 672 | name = "ratatui" 673 | version = "0.26.3" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" 676 | dependencies = [ 677 | "bitflags 2.5.0", 678 | "cassowary", 679 | "compact_str", 680 | "crossterm", 681 | "itertools", 682 | "lru", 683 | "paste", 684 | "stability", 685 | "strum", 686 | "unicode-segmentation", 687 | "unicode-truncate", 688 | "unicode-width", 689 | ] 690 | 691 | [[package]] 692 | name = "redox_syscall" 693 | version = "0.5.1" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 696 | dependencies = [ 697 | "bitflags 2.5.0", 698 | ] 699 | 700 | [[package]] 701 | name = "regex" 702 | version = "1.10.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 705 | dependencies = [ 706 | "aho-corasick", 707 | "memchr", 708 | "regex-automata 0.4.6", 709 | "regex-syntax 0.8.3", 710 | ] 711 | 712 | [[package]] 713 | name = "regex-automata" 714 | version = "0.1.10" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 717 | dependencies = [ 718 | "regex-syntax 0.6.29", 719 | ] 720 | 721 | [[package]] 722 | name = "regex-automata" 723 | version = "0.4.6" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 726 | dependencies = [ 727 | "aho-corasick", 728 | "memchr", 729 | "regex-syntax 0.8.3", 730 | ] 731 | 732 | [[package]] 733 | name = "regex-syntax" 734 | version = "0.6.29" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 737 | 738 | [[package]] 739 | name = "regex-syntax" 740 | version = "0.8.3" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 743 | 744 | [[package]] 745 | name = "rustc-demangle" 746 | version = "0.1.24" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 749 | 750 | [[package]] 751 | name = "rustversion" 752 | version = "1.0.17" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 755 | 756 | [[package]] 757 | name = "ryu" 758 | version = "1.0.18" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 761 | 762 | [[package]] 763 | name = "scopeguard" 764 | version = "1.2.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 767 | 768 | [[package]] 769 | name = "serde" 770 | version = "1.0.204" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 773 | dependencies = [ 774 | "serde_derive", 775 | ] 776 | 777 | [[package]] 778 | name = "serde_derive" 779 | version = "1.0.204" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 782 | dependencies = [ 783 | "proc-macro2", 784 | "quote", 785 | "syn", 786 | ] 787 | 788 | [[package]] 789 | name = "sharded-slab" 790 | version = "0.1.7" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 793 | dependencies = [ 794 | "lazy_static", 795 | ] 796 | 797 | [[package]] 798 | name = "signal-hook" 799 | version = "0.3.17" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 802 | dependencies = [ 803 | "libc", 804 | "signal-hook-registry", 805 | ] 806 | 807 | [[package]] 808 | name = "signal-hook-mio" 809 | version = "0.2.3" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 812 | dependencies = [ 813 | "libc", 814 | "mio", 815 | "signal-hook", 816 | ] 817 | 818 | [[package]] 819 | name = "signal-hook-registry" 820 | version = "1.4.2" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 823 | dependencies = [ 824 | "libc", 825 | ] 826 | 827 | [[package]] 828 | name = "smallvec" 829 | version = "1.13.2" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 832 | 833 | [[package]] 834 | name = "smawk" 835 | version = "0.3.2" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 838 | 839 | [[package]] 840 | name = "socket2" 841 | version = "0.5.7" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 844 | dependencies = [ 845 | "libc", 846 | "windows-sys 0.52.0", 847 | ] 848 | 849 | [[package]] 850 | name = "spin" 851 | version = "0.9.8" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 854 | dependencies = [ 855 | "lock_api", 856 | ] 857 | 858 | [[package]] 859 | name = "stability" 860 | version = "0.2.0" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" 863 | dependencies = [ 864 | "quote", 865 | "syn", 866 | ] 867 | 868 | [[package]] 869 | name = "static_assertions" 870 | version = "1.1.0" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 873 | 874 | [[package]] 875 | name = "strsim" 876 | version = "0.11.1" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 879 | 880 | [[package]] 881 | name = "strum" 882 | version = "0.26.2" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 885 | dependencies = [ 886 | "strum_macros", 887 | ] 888 | 889 | [[package]] 890 | name = "strum_macros" 891 | version = "0.26.2" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 894 | dependencies = [ 895 | "heck 0.4.1", 896 | "proc-macro2", 897 | "quote", 898 | "rustversion", 899 | "syn", 900 | ] 901 | 902 | [[package]] 903 | name = "syn" 904 | version = "2.0.66" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 907 | dependencies = [ 908 | "proc-macro2", 909 | "quote", 910 | "unicode-ident", 911 | ] 912 | 913 | [[package]] 914 | name = "textwrap" 915 | version = "0.16.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 918 | dependencies = [ 919 | "smawk", 920 | "unicode-linebreak", 921 | "unicode-width", 922 | ] 923 | 924 | [[package]] 925 | name = "thiserror" 926 | version = "1.0.61" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 929 | dependencies = [ 930 | "thiserror-impl", 931 | ] 932 | 933 | [[package]] 934 | name = "thiserror-impl" 935 | version = "1.0.61" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 938 | dependencies = [ 939 | "proc-macro2", 940 | "quote", 941 | "syn", 942 | ] 943 | 944 | [[package]] 945 | name = "thread_local" 946 | version = "1.1.8" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 949 | dependencies = [ 950 | "cfg-if", 951 | "once_cell", 952 | ] 953 | 954 | [[package]] 955 | name = "time" 956 | version = "0.3.36" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 959 | dependencies = [ 960 | "deranged", 961 | "itoa", 962 | "num-conv", 963 | "powerfmt", 964 | "serde", 965 | "time-core", 966 | "time-macros", 967 | ] 968 | 969 | [[package]] 970 | name = "time-core" 971 | version = "0.1.2" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 974 | 975 | [[package]] 976 | name = "time-macros" 977 | version = "0.2.18" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 980 | dependencies = [ 981 | "num-conv", 982 | "time-core", 983 | ] 984 | 985 | [[package]] 986 | name = "tracing" 987 | version = "0.1.40" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 990 | dependencies = [ 991 | "pin-project-lite", 992 | "tracing-attributes", 993 | "tracing-core", 994 | ] 995 | 996 | [[package]] 997 | name = "tracing-appender" 998 | version = "0.2.3" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" 1001 | dependencies = [ 1002 | "crossbeam-channel", 1003 | "thiserror", 1004 | "time", 1005 | "tracing-subscriber", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "tracing-attributes" 1010 | version = "0.1.27" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1013 | dependencies = [ 1014 | "proc-macro2", 1015 | "quote", 1016 | "syn", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "tracing-core" 1021 | version = "0.1.32" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1024 | dependencies = [ 1025 | "once_cell", 1026 | "valuable", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "tracing-error" 1031 | version = "0.2.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 1034 | dependencies = [ 1035 | "tracing", 1036 | "tracing-subscriber", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "tracing-log" 1041 | version = "0.2.0" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1044 | dependencies = [ 1045 | "log", 1046 | "once_cell", 1047 | "tracing-core", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "tracing-subscriber" 1052 | version = "0.3.18" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 1055 | dependencies = [ 1056 | "matchers", 1057 | "nu-ansi-term", 1058 | "once_cell", 1059 | "regex", 1060 | "sharded-slab", 1061 | "smallvec", 1062 | "thread_local", 1063 | "tracing", 1064 | "tracing-core", 1065 | "tracing-log", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "unicode-ident" 1070 | version = "1.0.12" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1073 | 1074 | [[package]] 1075 | name = "unicode-linebreak" 1076 | version = "0.1.5" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1079 | 1080 | [[package]] 1081 | name = "unicode-segmentation" 1082 | version = "1.11.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1085 | 1086 | [[package]] 1087 | name = "unicode-truncate" 1088 | version = "1.0.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" 1091 | dependencies = [ 1092 | "itertools", 1093 | "unicode-width", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "unicode-width" 1098 | version = "0.1.12" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 1101 | 1102 | [[package]] 1103 | name = "utf8parse" 1104 | version = "0.2.1" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1107 | 1108 | [[package]] 1109 | name = "valuable" 1110 | version = "0.1.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1113 | 1114 | [[package]] 1115 | name = "version_check" 1116 | version = "0.9.4" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1119 | 1120 | [[package]] 1121 | name = "wasi" 1122 | version = "0.11.0+wasi-snapshot-preview1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1125 | 1126 | [[package]] 1127 | name = "wasm-bindgen" 1128 | version = "0.2.92" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1131 | dependencies = [ 1132 | "cfg-if", 1133 | "wasm-bindgen-macro", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "wasm-bindgen-backend" 1138 | version = "0.2.92" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1141 | dependencies = [ 1142 | "bumpalo", 1143 | "log", 1144 | "once_cell", 1145 | "proc-macro2", 1146 | "quote", 1147 | "syn", 1148 | "wasm-bindgen-shared", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "wasm-bindgen-macro" 1153 | version = "0.2.92" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1156 | dependencies = [ 1157 | "quote", 1158 | "wasm-bindgen-macro-support", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "wasm-bindgen-macro-support" 1163 | version = "0.2.92" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1166 | dependencies = [ 1167 | "proc-macro2", 1168 | "quote", 1169 | "syn", 1170 | "wasm-bindgen-backend", 1171 | "wasm-bindgen-shared", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "wasm-bindgen-shared" 1176 | version = "0.2.92" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1179 | 1180 | [[package]] 1181 | name = "winapi" 1182 | version = "0.3.9" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1185 | dependencies = [ 1186 | "winapi-i686-pc-windows-gnu", 1187 | "winapi-x86_64-pc-windows-gnu", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "winapi-i686-pc-windows-gnu" 1192 | version = "0.4.0" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1195 | 1196 | [[package]] 1197 | name = "winapi-x86_64-pc-windows-gnu" 1198 | version = "0.4.0" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1201 | 1202 | [[package]] 1203 | name = "windows-sys" 1204 | version = "0.48.0" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1207 | dependencies = [ 1208 | "windows-targets 0.48.5", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "windows-sys" 1213 | version = "0.52.0" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1216 | dependencies = [ 1217 | "windows-targets 0.52.5", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "windows-targets" 1222 | version = "0.48.5" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1225 | dependencies = [ 1226 | "windows_aarch64_gnullvm 0.48.5", 1227 | "windows_aarch64_msvc 0.48.5", 1228 | "windows_i686_gnu 0.48.5", 1229 | "windows_i686_msvc 0.48.5", 1230 | "windows_x86_64_gnu 0.48.5", 1231 | "windows_x86_64_gnullvm 0.48.5", 1232 | "windows_x86_64_msvc 0.48.5", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "windows-targets" 1237 | version = "0.52.5" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1240 | dependencies = [ 1241 | "windows_aarch64_gnullvm 0.52.5", 1242 | "windows_aarch64_msvc 0.52.5", 1243 | "windows_i686_gnu 0.52.5", 1244 | "windows_i686_gnullvm", 1245 | "windows_i686_msvc 0.52.5", 1246 | "windows_x86_64_gnu 0.52.5", 1247 | "windows_x86_64_gnullvm 0.52.5", 1248 | "windows_x86_64_msvc 0.52.5", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "windows_aarch64_gnullvm" 1253 | version = "0.48.5" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1256 | 1257 | [[package]] 1258 | name = "windows_aarch64_gnullvm" 1259 | version = "0.52.5" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1262 | 1263 | [[package]] 1264 | name = "windows_aarch64_msvc" 1265 | version = "0.48.5" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1268 | 1269 | [[package]] 1270 | name = "windows_aarch64_msvc" 1271 | version = "0.52.5" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1274 | 1275 | [[package]] 1276 | name = "windows_i686_gnu" 1277 | version = "0.48.5" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1280 | 1281 | [[package]] 1282 | name = "windows_i686_gnu" 1283 | version = "0.52.5" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1286 | 1287 | [[package]] 1288 | name = "windows_i686_gnullvm" 1289 | version = "0.52.5" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1292 | 1293 | [[package]] 1294 | name = "windows_i686_msvc" 1295 | version = "0.48.5" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1298 | 1299 | [[package]] 1300 | name = "windows_i686_msvc" 1301 | version = "0.52.5" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1304 | 1305 | [[package]] 1306 | name = "windows_x86_64_gnu" 1307 | version = "0.48.5" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1310 | 1311 | [[package]] 1312 | name = "windows_x86_64_gnu" 1313 | version = "0.52.5" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1316 | 1317 | [[package]] 1318 | name = "windows_x86_64_gnullvm" 1319 | version = "0.48.5" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1322 | 1323 | [[package]] 1324 | name = "windows_x86_64_gnullvm" 1325 | version = "0.52.5" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1328 | 1329 | [[package]] 1330 | name = "windows_x86_64_msvc" 1331 | version = "0.48.5" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1334 | 1335 | [[package]] 1336 | name = "windows_x86_64_msvc" 1337 | version = "0.52.5" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1340 | 1341 | [[package]] 1342 | name = "zerocopy" 1343 | version = "0.7.34" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" 1346 | dependencies = [ 1347 | "zerocopy-derive", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "zerocopy-derive" 1352 | version = "0.7.34" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" 1355 | dependencies = [ 1356 | "proc-macro2", 1357 | "quote", 1358 | "syn", 1359 | ] 1360 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discovery-rs" 3 | version = "0.1.1" 4 | edition = "2021" 5 | description = "mDNS-SD TUI browser" 6 | repository = "https://github.com/JustPretender/discovery-rs" 7 | license = "MIT OR Apache-2.0" 8 | categories = ["network-programming", "command-line-utilities"] 9 | exclude = ["static/*"] 10 | homepage = "https://github.com/JustPretender/discovery-rs" 11 | keywords = ["networking", "mdns", "discovery", "cli"] 12 | readme = "README.md" 13 | rust-version = "1.74.0" 14 | 15 | [dependencies] 16 | anyhow = "1.0.86" 17 | crossterm = "0.27.0" 18 | ratatui = "0.26.3" 19 | color-eyre = "0.6.3" 20 | mdns-sd = "0.11.1" 21 | flume = { version = "0.11.0", features = ["default", "select"] } 22 | clap = "4.5.4" 23 | clap_derive = "4.5.4" 24 | regex = "1.10.5" 25 | textwrap = "0.16.1" 26 | tracing = "0.1.40" 27 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 28 | tracing-appender = "0.2.3" 29 | parking_lot = "0.12.3" 30 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rostyslav Khudolii 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discovery-rs 2 | 3 | [![Build](https://github.com/keepsimple1/mdns-sd/actions/workflows/build.yml/badge.svg)](https://github.com/JustPretender/discovery-rs/actions)[![Rust version: 1.74+](https://img.shields.io/badge/rust%20version-1.74-orange)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) 4 | 5 | A cli utility to discover **mDNS** services on your network. Built with [Ratatui](https://ratatui.rs/) and [mDNS-SD](https://github.com/keepsimple1/mdns-sd). 6 | 7 | ![Example](./static/mdns.gif) 8 | 9 | ## Installation 10 | 11 | ### Cargo 12 | 13 | * Install the rust toolchain in order to have cargo installed by following 14 | [this](https://www.rust-lang.org/tools/install) guide. 15 | * run `cargo install discovery-rs` 16 | 17 | ## License 18 | 19 | Licensed under either of 20 | 21 | * Apache License, Version 2.0 22 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 23 | * MIT license 24 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 25 | 26 | at your option. 27 | 28 | ## Contribution 29 | 30 | Unless you explicitly state otherwise, any contribution intentionally submitted 31 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 32 | dual licensed as above, without any additional terms or conditions. 33 | 34 | See [CONTRIBUTING.md](CONTRIBUTING.md). 35 | 36 | ## Acknowledgments 37 | 38 | * [Slumber](https://github.com/LucasPickering/slumber). List navigation code. 39 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::palette::tailwind; 2 | use ratatui::style::Color; 3 | 4 | pub const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950; 5 | pub const ALT_ROW_COLOR: Color = tailwind::SLATE.c900; 6 | pub const TEXT_COLOR: Color = tailwind::SLATE.c200; 7 | pub const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300; 8 | pub const HEADER_BG: Color = tailwind::BLUE.c950; 9 | pub const SEARCH_STYLE_BORDER: Color = tailwind::YELLOW.c300; 10 | -------------------------------------------------------------------------------- /src/info.rs: -------------------------------------------------------------------------------- 1 | use crate::colors::*; 2 | use crate::list::ListEntry; 3 | use crate::widget::DiscoveryWidget; 4 | use crossterm::event::KeyEvent; 5 | use mdns_sd::ServiceInfo; 6 | use ratatui::{prelude::*, widgets::*}; 7 | 8 | /// [`ServiceInfo`] wrapper. 9 | /// 10 | /// Implements traits, necessary for the [`ServiceInfo`] to be 11 | /// rendered either as a [`Widget`] or simply as an entry in the [`List`] 12 | #[derive(Debug)] 13 | pub struct Info { 14 | pub info: ServiceInfo, 15 | } 16 | 17 | impl PartialEq for Info { 18 | fn eq(&self, other: &Self) -> bool { 19 | self.info.get_hostname() == other.info.get_hostname() 20 | } 21 | } 22 | 23 | impl ListEntry for Info { 24 | fn entry(&self) -> Line { 25 | Line::styled(format!("{}", self.info.get_hostname()), TEXT_COLOR) 26 | } 27 | 28 | fn id(&self) -> String { 29 | self.info.get_hostname().to_string() 30 | } 31 | } 32 | 33 | impl DiscoveryWidget for &Info { 34 | fn title(&self) -> String { 35 | self.id() 36 | } 37 | 38 | fn controls(&self) -> String { 39 | "".to_string() 40 | } 41 | 42 | fn process_key_event(&mut self, _key_event: &KeyEvent) {} 43 | 44 | fn render(&self, area: Rect, buf: &mut Buffer, selected: bool) { 45 | let outer_block = Block::new() 46 | .borders(Borders::ALL) 47 | .border_style(if selected { 48 | Style::new().fg(SELECTED_STYLE_FG) 49 | } else { 50 | Style::default() 51 | }) 52 | .title_alignment(Alignment::Center) 53 | .title(self.title()) 54 | .title_style(Style::new().bold()) 55 | .fg(TEXT_COLOR) 56 | .bg(HEADER_BG); 57 | let inner_area = outer_block.inner(area); 58 | outer_block.render(area, buf); 59 | 60 | let inner_block = Block::new() 61 | .borders(Borders::NONE) 62 | .padding(Padding::horizontal(1)) 63 | .bg(NORMAL_ROW_COLOR); 64 | let properties = textwrap::wrap( 65 | &self.info.get_properties().to_string(), 66 | // Fit to end, minus "properties" and cell spacing 67 | textwrap::Options::new((area.width as usize).saturating_sub(10 + 1)), 68 | ) 69 | .join("\n"); 70 | let rows = [ 71 | Row::new([ 72 | Cell::new("Hostname").bold().light_cyan(), 73 | self.info.get_hostname().into(), 74 | ]), 75 | Row::new([ 76 | Cell::new("Addresses").bold().light_cyan(), 77 | self.info 78 | .get_addresses() 79 | .into_iter() 80 | .map(|addr| addr.to_string()) 81 | .fold(String::new(), |acc, addr| acc + &addr + " ") 82 | .into(), 83 | ]), 84 | Row::new([ 85 | Cell::new("Port").bold().light_cyan(), 86 | self.info.get_port().to_string().into(), 87 | ]), 88 | Row::new([ 89 | Cell::new("Host TTL").bold().light_cyan(), 90 | self.info.get_host_ttl().to_string().into(), 91 | ]), 92 | Row::new([ 93 | Cell::new("Other TTL").bold().light_cyan(), 94 | self.info.get_other_ttl().to_string().into(), 95 | ]), 96 | Row::new([ 97 | Cell::new("Priority").bold().light_cyan(), 98 | self.info.get_priority().to_string().into(), 99 | ]), 100 | Row::new([ 101 | Cell::new("Weight").bold().light_cyan(), 102 | self.info.get_weight().to_string().into(), 103 | ]), 104 | Row::new([ 105 | Cell::new("Properties").bold().light_cyan(), 106 | Cell::new(properties), 107 | ]) 108 | .height(2), 109 | ]; 110 | let widths = [Constraint::Percentage(10), Constraint::Percentage(90)]; 111 | 112 | let table = Table::new(rows, widths) 113 | .block(inner_block) 114 | .column_spacing(1) 115 | .highlight_spacing(HighlightSpacing::Always) 116 | .style(Style::new().white()) 117 | .on_black(); 118 | 119 | Widget::render(table, inner_area, buf); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent}; 2 | use ratatui::{prelude::*, widgets::*}; 3 | use regex::Regex; 4 | use std::cell::RefCell; 5 | use std::fmt::Display; 6 | use tracing::instrument; 7 | 8 | use crate::colors::*; 9 | use crate::search::Search; 10 | use crate::utils::centered_rect; 11 | use crate::widget::DiscoveryWidget; 12 | 13 | #[derive(Debug, Default)] 14 | enum Mode { 15 | #[default] 16 | Display, 17 | Search, 18 | } 19 | 20 | /// Custom [`List`] entry trait 21 | /// 22 | /// Implementing this trait for a type will make it possible 23 | /// for the type to be rendered as a line in the [`List`]. 24 | pub trait ListEntry { 25 | fn entry(&self) -> Line; 26 | fn id(&self) -> String; 27 | } 28 | 29 | impl ListEntry for D { 30 | fn entry(&self) -> Line { 31 | Line::styled(format!("{}", self), TEXT_COLOR) 32 | } 33 | 34 | fn id(&self) -> String { 35 | format!("{}", self) 36 | } 37 | } 38 | 39 | /// Custom [`List`] widget. 40 | /// 41 | /// Keeps track of the list elements and implements [`Widget`] so 42 | /// that the list can be rendered as part of the TUI. 43 | #[derive(Debug)] 44 | pub struct ListWidget { 45 | name: String, 46 | items: Vec, 47 | state: RefCell, 48 | search_regex: Option, 49 | search: Search, 50 | current_mode: Mode, 51 | } 52 | 53 | impl Default for ListWidget { 54 | fn default() -> Self { 55 | Self { 56 | name: "ListWidget".to_string(), 57 | items: Default::default(), 58 | state: RefCell::new(ListState::default()), 59 | search: Search::default(), 60 | search_regex: None, 61 | current_mode: Mode::default(), 62 | } 63 | } 64 | } 65 | 66 | impl ListWidget 67 | where 68 | Item: ListEntry + PartialEq + std::fmt::Debug, 69 | { 70 | pub fn name(mut self, name: String) -> Self { 71 | self.name = name; 72 | self 73 | } 74 | 75 | pub fn selected(&self) -> Option<&Item> { 76 | let filtered = self.filtered(); 77 | filtered 78 | .get(self.state.borrow().selected().unwrap_or(0)) 79 | .copied() 80 | } 81 | 82 | pub fn push(&mut self, item: Item) { 83 | if !self.items.contains(&item) { 84 | self.items.push(item); 85 | } 86 | 87 | // Select the first item once we have at least one 88 | let state = self.state.get_mut(); 89 | if state.selected().is_none() { 90 | state.select(Some(0)); 91 | } 92 | } 93 | 94 | pub fn remove(&mut self, id: &String) { 95 | if let Some((index, _)) = self.items.iter().enumerate().find(|(_, el)| el.id() == *id) { 96 | self.items.remove(index); 97 | } 98 | 99 | // Deselect when all the items are gone 100 | if self.items.is_empty() { 101 | self.state.get_mut().select(None); 102 | } 103 | } 104 | 105 | pub fn next(&mut self) { 106 | self.select_delta(1); 107 | } 108 | 109 | pub fn prev(&mut self) { 110 | self.select_delta(-1); 111 | } 112 | 113 | pub fn top(&mut self) { 114 | let s = self.state.get_mut().selected().unwrap_or(0) as isize; 115 | self.select_delta(-1 * s); 116 | } 117 | 118 | pub fn bottom(&mut self) { 119 | let s = self.state.get_mut().selected().unwrap_or(0) as isize; 120 | self.select_delta(s); 121 | } 122 | 123 | /// Move some number of items up or down the list. Selection will wrap if 124 | /// it underflows/overflows. 125 | #[instrument] 126 | fn select_delta(&mut self, delta: isize) { 127 | tracing::trace!( 128 | "List state before the update: {:?}", 129 | self.state.get_mut().selected() 130 | ); 131 | let filtered = self.filtered(); 132 | // If there's nothing in the list, we can't do anything 133 | if !filtered.is_empty() { 134 | let len = filtered.len() as isize; 135 | let index = match self.state.get_mut().selected() { 136 | Some(i) => (i as isize + delta).rem_euclid(len) as usize, 137 | // Nothing selected yet, pick the first item 138 | None => 0, 139 | }; 140 | self.state.get_mut().select(Some(index)); 141 | } 142 | tracing::trace!( 143 | "List state after the update: {:?}", 144 | self.state.get_mut().selected() 145 | ); 146 | } 147 | 148 | fn filtered(&self) -> Vec<&Item> { 149 | if let Some(regex) = self.search_regex.as_ref() { 150 | self.items 151 | .iter() 152 | .filter(|item| regex.is_match(&item.id())) 153 | .collect() 154 | } else { 155 | self.items.iter().collect() 156 | } 157 | } 158 | 159 | #[instrument] 160 | fn update_filter(&mut self, regex: Option) { 161 | self.search_regex = regex; 162 | let filtered = self.filtered(); 163 | if !filtered.is_empty() { 164 | self.state.get_mut().select(Some(0)); 165 | } 166 | tracing::debug!("Filter has been updated"); 167 | } 168 | } 169 | 170 | impl DiscoveryWidget for ListWidget 171 | where 172 | Item: ListEntry + PartialEq + std::fmt::Debug, 173 | { 174 | fn title(&self) -> String { 175 | format!( 176 | "{}{}", 177 | self.name, 178 | if let Some(regex) = self.search_regex.as_ref() { 179 | format!("(/{}/)", regex.to_string()) 180 | } else { 181 | "".to_string() 182 | } 183 | ) 184 | } 185 | 186 | fn controls(&self) -> String { 187 | "Use ↓↑ to select next/prev, g/G to go top/bottom, / to search".to_string() 188 | } 189 | 190 | fn process_key_event(&mut self, event: &KeyEvent) { 191 | match self.current_mode { 192 | Mode::Search => match event.code { 193 | KeyCode::Esc => { 194 | self.current_mode = Mode::Display; 195 | } 196 | KeyCode::Enter => { 197 | self.current_mode = Mode::Display; 198 | self.update_filter(self.search.compile_regex().ok().flatten()); 199 | } 200 | KeyCode::Char(_) | KeyCode::Backspace => { 201 | self.search.process_key_event(event); 202 | } 203 | _ => {} 204 | }, 205 | Mode::Display => match event.code { 206 | KeyCode::Down => self.next(), 207 | KeyCode::Up => self.prev(), 208 | KeyCode::Char('g') => self.top(), 209 | KeyCode::Char('G') => self.bottom(), 210 | KeyCode::Char('/') => self.current_mode = Mode::Search, 211 | _ => {} 212 | }, 213 | } 214 | } 215 | 216 | fn render(&self, area: Rect, buf: &mut Buffer, selected: bool) { 217 | let outer_block = Block::new() 218 | .borders(Borders::ALL) 219 | .border_style(if selected { 220 | Style::new().fg(SELECTED_STYLE_FG) 221 | } else { 222 | Style::default() 223 | }) 224 | .title_alignment(Alignment::Center) 225 | .title(self.title()) 226 | .title_style(Style::new().bold()) 227 | .fg(TEXT_COLOR) 228 | .bg(HEADER_BG); 229 | let inner_area = outer_block.inner(area); 230 | outer_block.render(area, buf); 231 | 232 | let inner_block = Block::new() 233 | .borders(Borders::NONE) 234 | .fg(TEXT_COLOR) 235 | .bg(NORMAL_ROW_COLOR); 236 | 237 | let items: Vec<_> = self 238 | .items 239 | .iter() 240 | .filter(|item| { 241 | if let Some(regex) = self.search_regex.as_ref() { 242 | regex.is_match(&item.id()) 243 | } else { 244 | true 245 | } 246 | }) 247 | .enumerate() 248 | .map(|(index, item)| { 249 | ListItem::new(item.entry()).bg(if (index % 2) == 0 { 250 | NORMAL_ROW_COLOR 251 | } else { 252 | ALT_ROW_COLOR 253 | }) 254 | }) 255 | .collect(); 256 | let list = List::new(items) 257 | .block(inner_block) 258 | .highlight_style( 259 | Style::default() 260 | .add_modifier(Modifier::BOLD) 261 | .add_modifier(Modifier::REVERSED) 262 | .fg(SELECTED_STYLE_FG), 263 | ) 264 | .highlight_symbol(">") 265 | .highlight_spacing(HighlightSpacing::Always); 266 | StatefulWidget::render(list, inner_area, buf, &mut self.state.borrow_mut()); 267 | 268 | if matches!(self.current_mode, Mode::Search) { 269 | let search_area = centered_rect(60, (5. / area.height as f64 * 100.) as u16, area); 270 | Clear.render(search_area, buf); 271 | self.search.render(search_area, buf, true); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use std::cell::RefCell; 3 | use std::collections::HashMap; 4 | use std::fs::File; 5 | use std::rc::Rc; 6 | use std::sync::Arc; 7 | use std::thread::JoinHandle; 8 | use std::time::Duration; 9 | use std::{error::Error, io::stdout}; 10 | 11 | use clap::Parser; 12 | use clap_derive::Parser; 13 | use color_eyre::config::HookBuilder; 14 | use crossterm::event::KeyModifiers; 15 | use crossterm::{ 16 | event::{self, poll, Event, KeyCode, KeyEventKind}, 17 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 18 | ExecutableCommand, 19 | }; 20 | use flume::{Selector, Sender}; 21 | use mdns_sd::{IfKind, ServiceDaemon, ServiceEvent}; 22 | use parking_lot::Mutex; 23 | use ratatui::{prelude::*, widgets::*}; 24 | use tracing::{instrument, Level}; 25 | use tracing_appender::non_blocking; 26 | use tracing_appender::non_blocking::WorkerGuard; 27 | use tracing_subscriber::EnvFilter; 28 | 29 | use crate::info::Info; 30 | use crate::list::ListWidget; 31 | use crate::widget::DiscoveryWidget; 32 | 33 | mod colors; 34 | mod info; 35 | mod list; 36 | mod search; 37 | mod utils; 38 | mod widget; 39 | 40 | #[derive(Parser, Debug, Default)] 41 | #[command(author, version, about, long_about = None)] 42 | #[command(propagate_version = true)] 43 | struct CliOpts { 44 | #[arg(long)] 45 | /// mDNS service query, default: _services._dns-sd._udp.local. 46 | query: Option, 47 | #[arg(long)] 48 | /// Interface to perform discovery on, default: All 49 | interface: Option, 50 | #[arg(long, action)] 51 | /// Enable tracing and debug logging 52 | tracing: bool, 53 | } 54 | 55 | const K_SERVICE_TYPE_ENUMERATION: &'static str = "_services._dns-sd._udp.local."; 56 | const K_REFRESH_RATE: u8 = 24; 57 | 58 | fn main() -> Result<(), Box> { 59 | let opts = CliOpts::parse(); 60 | 61 | init_error_hooks()?; 62 | 63 | // setup tracing and keep its guard 64 | let mut _tracing_guard = None; 65 | if opts.tracing { 66 | _tracing_guard = Some(init_tracing()?); 67 | } 68 | 69 | let terminal = init_terminal()?; 70 | 71 | // create app and run it 72 | let mut app = App::new( 73 | opts.query 74 | .as_ref() 75 | .map(|q| q.as_str()) 76 | .unwrap_or(K_SERVICE_TYPE_ENUMERATION), 77 | opts.interface.unwrap_or(IfKind::All), 78 | )?; 79 | app.run(terminal)?; 80 | app.shutdown()?; 81 | 82 | restore_terminal()?; 83 | 84 | Ok(()) 85 | } 86 | 87 | fn init_error_hooks() -> color_eyre::Result<()> { 88 | let (panic, error) = HookBuilder::default().into_hooks(); 89 | let panic = panic.into_panic_hook(); 90 | let error = error.into_eyre_hook(); 91 | color_eyre::eyre::set_hook(Box::new(move |e| { 92 | let _ = restore_terminal(); 93 | error(e) 94 | }))?; 95 | std::panic::set_hook(Box::new(move |info| { 96 | let _ = restore_terminal(); 97 | panic(info); 98 | })); 99 | Ok(()) 100 | } 101 | 102 | fn init_terminal() -> color_eyre::Result> { 103 | enable_raw_mode()?; 104 | stdout().execute(EnterAlternateScreen)?; 105 | let backend = CrosstermBackend::new(stdout()); 106 | let terminal = Terminal::new(backend)?; 107 | Ok(terminal) 108 | } 109 | 110 | fn restore_terminal() -> color_eyre::Result<()> { 111 | disable_raw_mode()?; 112 | stdout().execute(LeaveAlternateScreen)?; 113 | Ok(()) 114 | } 115 | 116 | /// Initialize the tracing subscriber to log to a file 117 | /// 118 | /// This function initializes the tracing subscriber to log to a file named `tracing.log` in the 119 | /// current directory. The function returns a [`WorkerGuard`] that must be kept alive for the 120 | /// duration of the program to ensure that logs are flushed to the file on shutdown. The logs are 121 | /// written in a non-blocking fashion to ensure that the logs do not block the main thread. 122 | fn init_tracing() -> anyhow::Result { 123 | let file = File::create("tracing.log").context("Failed to create tracing.log")?; 124 | let (non_blocking, guard) = non_blocking(file); 125 | 126 | // By default, the subscriber is configured to log all events with a level of `DEBUG` or higher, 127 | // but this can be changed by setting the `RUST_LOG` environment variable. 128 | let env_filter = EnvFilter::builder() 129 | .with_default_directive(Level::INFO.into()) 130 | .from_env_lossy(); 131 | 132 | tracing_subscriber::fmt() 133 | .with_writer(non_blocking) 134 | .with_env_filter(env_filter) 135 | .init(); 136 | Ok(guard) 137 | } 138 | 139 | #[derive(Debug, Default)] 140 | enum Tab { 141 | #[default] 142 | Services, 143 | Instances, 144 | } 145 | 146 | #[derive(Debug, Default)] 147 | enum State { 148 | #[default] 149 | Running, 150 | Exit, 151 | } 152 | 153 | struct App { 154 | stop: Sender<()>, 155 | services: Arc>>, 156 | instances: Arc>>>, 157 | current_tab: Tab, 158 | worker_handle: Option>>, 159 | } 160 | 161 | impl App { 162 | #[instrument] 163 | fn new + std::fmt::Debug>(query: T, interface: IfKind) -> anyhow::Result { 164 | let mdns = ServiceDaemon::new()?; 165 | let mdns = Arc::new(Mutex::new(mdns)); 166 | let services = Arc::new(Mutex::new( 167 | ListWidget::default().name("Services".to_string()), 168 | )); 169 | let instances = Arc::new(Mutex::new(HashMap::new())); 170 | let (stop_tx, stop_rx) = flume::bounded(1); 171 | 172 | let worker = { 173 | let mdns = mdns.clone(); 174 | let services = services.clone(); 175 | let instances = instances.clone(); 176 | let query = query.as_ref().to_string(); 177 | std::thread::spawn(move || -> anyhow::Result<()> { 178 | let _span = tracing::span!(Level::TRACE, "mDNS worker").entered(); 179 | 180 | let base = { 181 | let mdns = mdns.lock(); 182 | mdns.enable_interface(interface.clone())?; 183 | mdns.browse(query.as_str())? 184 | }; 185 | 186 | tracing::info!("Started the mDNS browsing"); 187 | 188 | let receivers = Rc::new(RefCell::new(vec![base])); 189 | let event_handler = { 190 | let receivers = receivers.clone(); 191 | let mdns = mdns.clone(); 192 | move |event| -> anyhow::Result<()> { 193 | if let Ok(event) = event { 194 | match event { 195 | ServiceEvent::ServiceFound(service_type, full_name) => { 196 | tracing::debug!("New service found: {full_name}"); 197 | if service_type == query { 198 | services.lock().push(full_name.clone()); 199 | instances.lock().insert( 200 | full_name.clone(), 201 | ListWidget::default().name(full_name.clone()), 202 | ); 203 | let receiver = mdns.lock().browse(&full_name)?; 204 | let mut receivers = receivers.borrow_mut(); 205 | receivers.push(receiver); 206 | } 207 | } 208 | ServiceEvent::ServiceResolved(info) => { 209 | tracing::debug!("Service resolved: {info:#?}"); 210 | if let Some(resolved) = 211 | instances.lock().get_mut(info.get_type()) 212 | { 213 | resolved.push(Info { info }); 214 | } 215 | } 216 | ServiceEvent::ServiceRemoved(service_type, full_name) => { 217 | tracing::debug!("Service removed: {full_name}"); 218 | if service_type == query { 219 | services.lock().remove(&full_name); 220 | instances.lock().remove(&full_name); 221 | } else if let Some(resolved) = 222 | instances.lock().get_mut(&service_type) 223 | { 224 | resolved.remove(&full_name); 225 | } 226 | } 227 | ServiceEvent::SearchStarted(service) => { 228 | tracing::trace!("Search Started for {service}"); 229 | } 230 | ServiceEvent::SearchStopped(service) => { 231 | tracing::trace!("Search Stopped for {service}"); 232 | } 233 | } 234 | } 235 | 236 | Ok(()) 237 | } 238 | }; 239 | 240 | let mut stop = false; 241 | while !stop { 242 | let receivers = receivers.borrow().clone(); 243 | let mut selector = Selector::new(); 244 | for receiver in receivers.iter() { 245 | selector = selector.recv(receiver, &event_handler); 246 | } 247 | selector = selector.recv(&stop_rx, |_| { 248 | stop = true; 249 | Ok(()) 250 | }); 251 | selector.wait()?; 252 | } 253 | 254 | mdns.lock().shutdown()?; 255 | 256 | tracing::info!("Stopped the mDNS browsing"); 257 | 258 | Ok(()) 259 | }) 260 | }; 261 | 262 | Ok(Self { 263 | services, 264 | instances, 265 | stop: stop_tx, 266 | current_tab: Tab::Services, 267 | worker_handle: Some(worker), 268 | }) 269 | } 270 | 271 | fn handle_event(&mut self, event: Event) -> anyhow::Result { 272 | if let Event::Key(key) = event { 273 | if key.kind == KeyEventKind::Press { 274 | match key.code { 275 | KeyCode::Char('q') if key.modifiers.contains(KeyModifiers::CONTROL) => { 276 | return Ok(State::Exit) 277 | } 278 | KeyCode::Left => self.current_tab = Tab::Services, 279 | KeyCode::Right => self.current_tab = Tab::Instances, 280 | _ => { 281 | let mut services = self.services.lock(); 282 | let mut instances = self.instances.lock(); 283 | 284 | match self.current_tab { 285 | Tab::Services => { 286 | services.process_key_event(&key); 287 | } 288 | Tab::Instances => { 289 | if let Some(selected) = services 290 | .selected() 291 | .and_then(|service| instances.get_mut(service)) 292 | { 293 | selected.process_key_event(&key); 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | } 301 | 302 | Ok(State::Running) 303 | } 304 | 305 | fn run(&mut self, mut terminal: Terminal) -> anyhow::Result<()> { 306 | loop { 307 | terminal.draw(|frame| { 308 | frame.render_widget(self as &mut App, frame.size()); 309 | })?; 310 | 311 | if poll(Duration::from_millis( 312 | (K_REFRESH_RATE as f64 / 1000.) as u64, 313 | ))? { 314 | match self.handle_event(event::read()?)? { 315 | State::Exit => { 316 | return Ok(()); 317 | } 318 | _ => {} 319 | } 320 | } 321 | } 322 | } 323 | 324 | fn shutdown(&mut self) -> anyhow::Result<()> { 325 | self.stop.send(())?; 326 | if let Some(handle) = self.worker_handle.take() { 327 | handle 328 | .join() 329 | .expect("The worker being joined has panicked")?; 330 | } 331 | Ok(()) 332 | } 333 | } 334 | 335 | impl Widget for &mut App { 336 | fn render(self, area: Rect, buf: &mut Buffer) { 337 | let vertical = Layout::vertical([ 338 | Constraint::Length(2), 339 | Constraint::Min(0), 340 | Constraint::Length(12), 341 | Constraint::Length(2), 342 | ]); 343 | let [header_area, list_area, info_area, footer_area] = vertical.areas(area); 344 | 345 | Paragraph::new(format!( 346 | "{}, v{}", 347 | env!("CARGO_PKG_DESCRIPTION"), 348 | env!("CARGO_PKG_VERSION") 349 | )) 350 | .bold() 351 | .centered() 352 | .render(header_area, buf); 353 | 354 | let list_layout = 355 | Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); 356 | let [service_area, instances_area] = list_layout.areas(list_area); 357 | 358 | let services = self.services.lock(); 359 | services.render(service_area, buf, matches!(self.current_tab, Tab::Services)); 360 | if let Some(selected) = services.selected() { 361 | let instances = self.instances.lock(); 362 | if let Some(resolved_instances) = instances.get(selected) { 363 | resolved_instances.render( 364 | instances_area, 365 | buf, 366 | matches!(self.current_tab, Tab::Instances), 367 | ); 368 | if let Some(info) = resolved_instances.selected() { 369 | info.render(info_area, buf, false); 370 | } 371 | } 372 | } 373 | 374 | Paragraph::new(vec![ 375 | Line::from(services.controls()), 376 | Line::from("←→ to switch panes, C-q to exit."), 377 | ]) 378 | .centered() 379 | .render(footer_area, buf); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use crate::colors::{HEADER_BG, NORMAL_ROW_COLOR, SEARCH_STYLE_BORDER, TEXT_COLOR}; 2 | use crate::widget::DiscoveryWidget; 3 | use crossterm::event::{KeyCode, KeyEvent}; 4 | use ratatui::buffer::Buffer; 5 | use ratatui::layout::Rect; 6 | use ratatui::prelude::{Alignment, Constraint, Layout, Line, Stylize, Widget}; 7 | use ratatui::style::{Color, Style}; 8 | use ratatui::text::Span; 9 | use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; 10 | use regex::Regex; 11 | 12 | #[derive(Debug, Default)] 13 | pub struct Search { 14 | search: Option, 15 | } 16 | 17 | impl Search { 18 | pub fn compile_regex(&self) -> anyhow::Result> { 19 | if let Some(search) = self.search.as_ref() { 20 | let regex = Regex::new(search)?; 21 | Ok(Some(regex)) 22 | } else { 23 | Ok(None) 24 | } 25 | } 26 | } 27 | 28 | impl DiscoveryWidget for Search { 29 | fn title(&self) -> String { 30 | "Search".to_string() 31 | } 32 | 33 | fn controls(&self) -> String { 34 | "Use ↵ to apply. Esc to exit".to_string() 35 | } 36 | 37 | fn process_key_event(&mut self, key_event: &KeyEvent) { 38 | match (self.search.as_mut(), key_event.code) { 39 | (Some(regex), KeyCode::Char(c)) => { 40 | regex.push(c); 41 | } 42 | (Some(regex), KeyCode::Backspace) => { 43 | regex.pop(); 44 | } 45 | (None, KeyCode::Char(c)) => { 46 | self.search = Some(c.to_string()); 47 | } 48 | _ => {} 49 | } 50 | 51 | if self 52 | .search 53 | .as_ref() 54 | .filter(|search| !search.is_empty()) 55 | .is_none() 56 | { 57 | self.search = None; 58 | } 59 | } 60 | 61 | fn render(&self, area: Rect, buf: &mut Buffer, _selected: bool) 62 | where 63 | Self: Sized, 64 | { 65 | let block = Block::new() 66 | .borders(Borders::ALL) 67 | .border_style(Style::new().fg(SEARCH_STYLE_BORDER).bold()) 68 | .title_alignment(Alignment::Center) 69 | .title(self.title()) 70 | .title_style(Style::new().bold()) 71 | .fg(TEXT_COLOR) 72 | .bg(HEADER_BG); 73 | let inner_area = block.inner(area); 74 | block.render(area, buf); 75 | 76 | let [search_area, footer_area] = 77 | Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]) 78 | .areas(inner_area); 79 | let block = Block::new() 80 | .borders(Borders::NONE) 81 | .fg(TEXT_COLOR) 82 | .bg(NORMAL_ROW_COLOR); 83 | let input = Paragraph::new(Line::from(vec![ 84 | Span::styled(" /", Style::default().fg(Color::DarkGray)), 85 | Span::from(self.search.as_deref().unwrap_or("")), 86 | ])) 87 | .block(block); 88 | 89 | Widget::render(input, search_area, buf); 90 | 91 | Paragraph::new(self.controls()) 92 | .centered() 93 | .wrap(Wrap::default()) 94 | .render(footer_area, buf); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use ratatui::prelude::{Constraint, Direction, Layout, Rect}; 2 | 3 | pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 4 | let popup_layout = Layout::default() 5 | .direction(Direction::Vertical) 6 | .constraints( 7 | [ 8 | Constraint::Percentage((100 - percent_y) / 2), 9 | Constraint::Percentage(percent_y), 10 | Constraint::Percentage((100 - percent_y) / 2), 11 | ] 12 | .as_ref(), 13 | ) 14 | .split(r); 15 | 16 | Layout::default() 17 | .direction(Direction::Horizontal) 18 | .constraints( 19 | [ 20 | Constraint::Percentage((100 - percent_x) / 2), 21 | Constraint::Percentage(percent_x), 22 | Constraint::Percentage((100 - percent_x) / 2), 23 | ] 24 | .as_ref(), 25 | ) 26 | .split(popup_layout[1])[1] 27 | } 28 | -------------------------------------------------------------------------------- /src/widget.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::KeyEvent; 2 | use ratatui::prelude::{Buffer, Rect}; 3 | use tracing::instrument; 4 | 5 | pub trait DiscoveryWidget: Sized + std::fmt::Debug { 6 | fn title(&self) -> String; 7 | fn controls(&self) -> String; 8 | #[instrument] 9 | fn process_key_event(&mut self, key_event: &KeyEvent); 10 | #[instrument] 11 | fn render(&self, area: Rect, buf: &mut Buffer, selected: bool); 12 | } 13 | -------------------------------------------------------------------------------- /static/mdns.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustPretender/discovery-rs/bb75f1b2d7c06afa8f20f953d9758e4118889d25/static/mdns.gif --------------------------------------------------------------------------------