├── .config └── .gitkeep ├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── install.sh ├── justfile ├── rustfmt.toml └── src ├── action.rs ├── app.rs ├── components ├── home.rs ├── logger.rs └── mod.rs ├── event.rs ├── lib.rs ├── main.rs ├── systemd.rs ├── terminal.rs └── utils.rs /.config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgwood/systemctl-tui/feb0c475815f53c04baaa29f1a9686e0d447bead/.config/.gitkeep -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | upload-assets: 7 | strategy: 8 | matrix: 9 | include: 10 | - target: aarch64-unknown-linux-musl 11 | os: ubuntu-24.04 12 | - target: x86_64-unknown-linux-musl 13 | os: ubuntu-24.04 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: taiki-e/upload-rust-binary-action@v1 18 | with: 19 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 20 | # Note that glob pattern is not supported yet. 21 | bin: systemctl-tui 22 | target: ${{ matrix.target }} 23 | # (required) GitHub token for uploading assets to GitHub Releases. 24 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/rust 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=rust 3 | 4 | ### Rust ### 5 | # Generated by Cargo 6 | # will have compiled files and executables 7 | debug/ 8 | target/ 9 | /target 10 | .data 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # MSVC Windows builds of rustc generate these, which store debugging information 16 | *.pdb 17 | 18 | # End of https://www.toptal.com/developers/gitignore/api/rust 19 | 20 | # Samply 21 | profile.json 22 | .aider* 23 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 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.7" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" 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.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.16" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 46 | 47 | [[package]] 48 | name = "android-tzdata" 49 | version = "0.1.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 52 | 53 | [[package]] 54 | name = "android_system_properties" 55 | version = "0.1.5" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 58 | dependencies = [ 59 | "libc", 60 | ] 61 | 62 | [[package]] 63 | name = "anstream" 64 | version = "0.6.11" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 67 | dependencies = [ 68 | "anstyle", 69 | "anstyle-parse", 70 | "anstyle-query", 71 | "anstyle-wincon", 72 | "colorchoice", 73 | "utf8parse", 74 | ] 75 | 76 | [[package]] 77 | name = "anstyle" 78 | version = "1.0.4" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 81 | 82 | [[package]] 83 | name = "anstyle-parse" 84 | version = "0.2.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 87 | dependencies = [ 88 | "utf8parse", 89 | ] 90 | 91 | [[package]] 92 | name = "anstyle-query" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 96 | dependencies = [ 97 | "windows-sys 0.48.0", 98 | ] 99 | 100 | [[package]] 101 | name = "anstyle-wincon" 102 | version = "3.0.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 105 | dependencies = [ 106 | "anstyle", 107 | "windows-sys 0.48.0", 108 | ] 109 | 110 | [[package]] 111 | name = "anyhow" 112 | version = "1.0.75" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 115 | 116 | [[package]] 117 | name = "arboard" 118 | version = "3.2.1" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" 121 | dependencies = [ 122 | "clipboard-win", 123 | "log", 124 | "objc", 125 | "objc-foundation", 126 | "objc_id", 127 | "parking_lot", 128 | "thiserror", 129 | "winapi", 130 | "x11rb", 131 | ] 132 | 133 | [[package]] 134 | name = "async-broadcast" 135 | version = "0.7.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" 138 | dependencies = [ 139 | "event-listener 5.3.0", 140 | "event-listener-strategy 0.5.1", 141 | "futures-core", 142 | "pin-project-lite", 143 | ] 144 | 145 | [[package]] 146 | name = "async-channel" 147 | version = "1.9.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 150 | dependencies = [ 151 | "concurrent-queue", 152 | "event-listener 2.5.3", 153 | "futures-core", 154 | ] 155 | 156 | [[package]] 157 | name = "async-channel" 158 | version = "2.2.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" 161 | dependencies = [ 162 | "concurrent-queue", 163 | "event-listener 5.3.0", 164 | "event-listener-strategy 0.5.1", 165 | "futures-core", 166 | "pin-project-lite", 167 | ] 168 | 169 | [[package]] 170 | name = "async-io" 171 | version = "2.3.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" 174 | dependencies = [ 175 | "async-lock 3.3.0", 176 | "cfg-if", 177 | "concurrent-queue", 178 | "futures-io", 179 | "futures-lite 2.0.0", 180 | "parking", 181 | "polling", 182 | "rustix", 183 | "slab", 184 | "tracing", 185 | "windows-sys 0.52.0", 186 | ] 187 | 188 | [[package]] 189 | name = "async-lock" 190 | version = "2.8.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" 193 | dependencies = [ 194 | "event-listener 2.5.3", 195 | ] 196 | 197 | [[package]] 198 | name = "async-lock" 199 | version = "3.3.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" 202 | dependencies = [ 203 | "event-listener 4.0.3", 204 | "event-listener-strategy 0.4.0", 205 | "pin-project-lite", 206 | ] 207 | 208 | [[package]] 209 | name = "async-process" 210 | version = "2.2.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" 213 | dependencies = [ 214 | "async-channel 2.2.1", 215 | "async-io", 216 | "async-lock 3.3.0", 217 | "async-signal", 218 | "async-task", 219 | "blocking", 220 | "cfg-if", 221 | "event-listener 5.3.0", 222 | "futures-lite 2.0.0", 223 | "rustix", 224 | "tracing", 225 | "windows-sys 0.52.0", 226 | ] 227 | 228 | [[package]] 229 | name = "async-recursion" 230 | version = "1.0.5" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" 233 | dependencies = [ 234 | "proc-macro2", 235 | "quote", 236 | "syn 2.0.60", 237 | ] 238 | 239 | [[package]] 240 | name = "async-signal" 241 | version = "0.2.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" 244 | dependencies = [ 245 | "async-io", 246 | "async-lock 3.3.0", 247 | "atomic-waker", 248 | "cfg-if", 249 | "futures-core", 250 | "futures-io", 251 | "rustix", 252 | "signal-hook-registry", 253 | "slab", 254 | "windows-sys 0.52.0", 255 | ] 256 | 257 | [[package]] 258 | name = "async-task" 259 | version = "4.7.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" 262 | 263 | [[package]] 264 | name = "async-trait" 265 | version = "0.1.80" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" 268 | dependencies = [ 269 | "proc-macro2", 270 | "quote", 271 | "syn 2.0.60", 272 | ] 273 | 274 | [[package]] 275 | name = "atomic-waker" 276 | version = "1.1.2" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 279 | 280 | [[package]] 281 | name = "autocfg" 282 | version = "1.1.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 285 | 286 | [[package]] 287 | name = "backtrace" 288 | version = "0.3.69" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 291 | dependencies = [ 292 | "addr2line", 293 | "cc", 294 | "cfg-if", 295 | "libc", 296 | "miniz_oxide", 297 | "object", 298 | "rustc-demangle", 299 | ] 300 | 301 | [[package]] 302 | name = "base64" 303 | version = "0.13.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 306 | 307 | [[package]] 308 | name = "better-panic" 309 | version = "0.3.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "6fa9e1d11a268684cbd90ed36370d7577afb6c62d912ddff5c15fc34343e5036" 312 | dependencies = [ 313 | "backtrace", 314 | "console", 315 | ] 316 | 317 | [[package]] 318 | name = "bitflags" 319 | version = "1.3.2" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 322 | 323 | [[package]] 324 | name = "bitflags" 325 | version = "2.4.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 328 | 329 | [[package]] 330 | name = "block" 331 | version = "0.1.6" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 334 | 335 | [[package]] 336 | name = "block-buffer" 337 | version = "0.10.4" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 340 | dependencies = [ 341 | "generic-array", 342 | ] 343 | 344 | [[package]] 345 | name = "blocking" 346 | version = "1.4.0" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1" 349 | dependencies = [ 350 | "async-channel 1.9.0", 351 | "async-lock 2.8.0", 352 | "async-task", 353 | "fastrand", 354 | "futures-io", 355 | "futures-lite 1.13.0", 356 | "piper", 357 | "tracing", 358 | ] 359 | 360 | [[package]] 361 | name = "bumpalo" 362 | version = "3.14.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 365 | 366 | [[package]] 367 | name = "byteorder" 368 | version = "1.4.3" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 371 | 372 | [[package]] 373 | name = "bytes" 374 | version = "1.5.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 377 | 378 | [[package]] 379 | name = "cassowary" 380 | version = "0.3.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 383 | 384 | [[package]] 385 | name = "castaway" 386 | version = "0.2.3" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 389 | dependencies = [ 390 | "rustversion", 391 | ] 392 | 393 | [[package]] 394 | name = "cc" 395 | version = "1.0.83" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 398 | dependencies = [ 399 | "libc", 400 | ] 401 | 402 | [[package]] 403 | name = "cfg-if" 404 | version = "1.0.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 407 | 408 | [[package]] 409 | name = "cfg_aliases" 410 | version = "0.1.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 413 | 414 | [[package]] 415 | name = "chrono" 416 | version = "0.4.31" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 419 | dependencies = [ 420 | "android-tzdata", 421 | "iana-time-zone", 422 | "num-traits", 423 | "windows-targets 0.48.5", 424 | ] 425 | 426 | [[package]] 427 | name = "clap" 428 | version = "4.4.6" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" 431 | dependencies = [ 432 | "clap_builder", 433 | "clap_derive", 434 | ] 435 | 436 | [[package]] 437 | name = "clap_builder" 438 | version = "4.4.6" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" 441 | dependencies = [ 442 | "anstream", 443 | "anstyle", 444 | "clap_lex", 445 | "strsim", 446 | "terminal_size", 447 | "unicase", 448 | "unicode-width", 449 | ] 450 | 451 | [[package]] 452 | name = "clap_complete" 453 | version = "4.4.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "e3ae8ba90b9d8b007efe66e55e48fb936272f5ca00349b5b0e89877520d35ea7" 456 | dependencies = [ 457 | "clap", 458 | ] 459 | 460 | [[package]] 461 | name = "clap_derive" 462 | version = "4.4.2" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 465 | dependencies = [ 466 | "heck 0.4.1", 467 | "proc-macro2", 468 | "quote", 469 | "syn 2.0.60", 470 | ] 471 | 472 | [[package]] 473 | name = "clap_lex" 474 | version = "0.5.1" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 477 | 478 | [[package]] 479 | name = "clipboard-anywhere" 480 | version = "0.2.2" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "a320b5cd489194fe5975e9e6cd303db0eebbf776213fba75e6955c2842832674" 483 | dependencies = [ 484 | "anyhow", 485 | "arboard", 486 | "base64", 487 | "duct", 488 | "is-wsl", 489 | ] 490 | 491 | [[package]] 492 | name = "clipboard-win" 493 | version = "4.5.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" 496 | dependencies = [ 497 | "error-code", 498 | "str-buf", 499 | "winapi", 500 | ] 501 | 502 | [[package]] 503 | name = "colorchoice" 504 | version = "1.0.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 507 | 508 | [[package]] 509 | name = "colored" 510 | version = "2.0.4" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" 513 | dependencies = [ 514 | "is-terminal", 515 | "lazy_static", 516 | "windows-sys 0.48.0", 517 | ] 518 | 519 | [[package]] 520 | name = "compact_str" 521 | version = "0.8.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 524 | dependencies = [ 525 | "castaway", 526 | "cfg-if", 527 | "itoa", 528 | "rustversion", 529 | "ryu", 530 | "static_assertions", 531 | ] 532 | 533 | [[package]] 534 | name = "concurrent-queue" 535 | version = "2.5.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 538 | dependencies = [ 539 | "crossbeam-utils", 540 | ] 541 | 542 | [[package]] 543 | name = "console" 544 | version = "0.15.7" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 547 | dependencies = [ 548 | "encode_unicode", 549 | "lazy_static", 550 | "libc", 551 | "windows-sys 0.45.0", 552 | ] 553 | 554 | [[package]] 555 | name = "core-foundation-sys" 556 | version = "0.8.4" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 559 | 560 | [[package]] 561 | name = "cpufeatures" 562 | version = "0.2.9" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 565 | dependencies = [ 566 | "libc", 567 | ] 568 | 569 | [[package]] 570 | name = "crossbeam-channel" 571 | version = "0.5.13" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 574 | dependencies = [ 575 | "crossbeam-utils", 576 | ] 577 | 578 | [[package]] 579 | name = "crossbeam-utils" 580 | version = "0.8.20" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 583 | 584 | [[package]] 585 | name = "crossterm" 586 | version = "0.28.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 589 | dependencies = [ 590 | "bitflags 2.4.0", 591 | "crossterm_winapi", 592 | "futures-core", 593 | "mio 1.0.2", 594 | "parking_lot", 595 | "rustix", 596 | "signal-hook", 597 | "signal-hook-mio", 598 | "winapi", 599 | ] 600 | 601 | [[package]] 602 | name = "crossterm_winapi" 603 | version = "0.9.1" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 606 | dependencies = [ 607 | "winapi", 608 | ] 609 | 610 | [[package]] 611 | name = "crypto-common" 612 | version = "0.1.6" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 615 | dependencies = [ 616 | "generic-array", 617 | "typenum", 618 | ] 619 | 620 | [[package]] 621 | name = "deranged" 622 | version = "0.3.11" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 625 | dependencies = [ 626 | "powerfmt", 627 | ] 628 | 629 | [[package]] 630 | name = "derivative" 631 | version = "2.2.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 634 | dependencies = [ 635 | "proc-macro2", 636 | "quote", 637 | "syn 1.0.109", 638 | ] 639 | 640 | [[package]] 641 | name = "digest" 642 | version = "0.10.7" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 645 | dependencies = [ 646 | "block-buffer", 647 | "crypto-common", 648 | ] 649 | 650 | [[package]] 651 | name = "directories" 652 | version = "5.0.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 655 | dependencies = [ 656 | "dirs-sys", 657 | ] 658 | 659 | [[package]] 660 | name = "dirs-sys" 661 | version = "0.4.1" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 664 | dependencies = [ 665 | "libc", 666 | "option-ext", 667 | "redox_users", 668 | "windows-sys 0.48.0", 669 | ] 670 | 671 | [[package]] 672 | name = "duct" 673 | version = "0.13.6" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "37ae3fc31835f74c2a7ceda3aeede378b0ae2e74c8f1c36559fcc9ae2a4e7d3e" 676 | dependencies = [ 677 | "libc", 678 | "once_cell", 679 | "os_pipe", 680 | "shared_child", 681 | ] 682 | 683 | [[package]] 684 | name = "either" 685 | version = "1.9.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 688 | 689 | [[package]] 690 | name = "encode_unicode" 691 | version = "0.3.6" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 694 | 695 | [[package]] 696 | name = "endi" 697 | version = "1.1.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" 700 | 701 | [[package]] 702 | name = "enumflags2" 703 | version = "0.7.9" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" 706 | dependencies = [ 707 | "enumflags2_derive", 708 | "serde", 709 | ] 710 | 711 | [[package]] 712 | name = "enumflags2_derive" 713 | version = "0.7.9" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" 716 | dependencies = [ 717 | "proc-macro2", 718 | "quote", 719 | "syn 2.0.60", 720 | ] 721 | 722 | [[package]] 723 | name = "env_filter" 724 | version = "0.1.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 727 | dependencies = [ 728 | "log", 729 | "regex", 730 | ] 731 | 732 | [[package]] 733 | name = "env_logger" 734 | version = "0.11.1" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "05e7cf40684ae96ade6232ed84582f40ce0a66efcd43a5117aef610534f8e0b8" 737 | dependencies = [ 738 | "anstream", 739 | "anstyle", 740 | "env_filter", 741 | "humantime", 742 | "log", 743 | ] 744 | 745 | [[package]] 746 | name = "equivalent" 747 | version = "1.0.1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 750 | 751 | [[package]] 752 | name = "errno" 753 | version = "0.3.8" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 756 | dependencies = [ 757 | "libc", 758 | "windows-sys 0.52.0", 759 | ] 760 | 761 | [[package]] 762 | name = "error-code" 763 | version = "2.3.1" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 766 | dependencies = [ 767 | "libc", 768 | "str-buf", 769 | ] 770 | 771 | [[package]] 772 | name = "event-listener" 773 | version = "2.5.3" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 776 | 777 | [[package]] 778 | name = "event-listener" 779 | version = "4.0.3" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" 782 | dependencies = [ 783 | "concurrent-queue", 784 | "parking", 785 | "pin-project-lite", 786 | ] 787 | 788 | [[package]] 789 | name = "event-listener" 790 | version = "5.3.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" 793 | dependencies = [ 794 | "concurrent-queue", 795 | "parking", 796 | "pin-project-lite", 797 | ] 798 | 799 | [[package]] 800 | name = "event-listener-strategy" 801 | version = "0.4.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" 804 | dependencies = [ 805 | "event-listener 4.0.3", 806 | "pin-project-lite", 807 | ] 808 | 809 | [[package]] 810 | name = "event-listener-strategy" 811 | version = "0.5.1" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" 814 | dependencies = [ 815 | "event-listener 5.3.0", 816 | "pin-project-lite", 817 | ] 818 | 819 | [[package]] 820 | name = "fastrand" 821 | version = "2.0.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 824 | 825 | [[package]] 826 | name = "futures" 827 | version = "0.3.28" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 830 | dependencies = [ 831 | "futures-channel", 832 | "futures-core", 833 | "futures-executor", 834 | "futures-io", 835 | "futures-sink", 836 | "futures-task", 837 | "futures-util", 838 | ] 839 | 840 | [[package]] 841 | name = "futures-channel" 842 | version = "0.3.30" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 845 | dependencies = [ 846 | "futures-core", 847 | "futures-sink", 848 | ] 849 | 850 | [[package]] 851 | name = "futures-core" 852 | version = "0.3.30" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 855 | 856 | [[package]] 857 | name = "futures-executor" 858 | version = "0.3.28" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 861 | dependencies = [ 862 | "futures-core", 863 | "futures-task", 864 | "futures-util", 865 | ] 866 | 867 | [[package]] 868 | name = "futures-io" 869 | version = "0.3.30" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 872 | 873 | [[package]] 874 | name = "futures-lite" 875 | version = "1.13.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" 878 | dependencies = [ 879 | "futures-core", 880 | "pin-project-lite", 881 | ] 882 | 883 | [[package]] 884 | name = "futures-lite" 885 | version = "2.0.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "9c1155db57329dca6d018b61e76b1488ce9a2e5e44028cac420a5898f4fcef63" 888 | dependencies = [ 889 | "fastrand", 890 | "futures-core", 891 | "futures-io", 892 | "memchr", 893 | "parking", 894 | "pin-project-lite", 895 | "waker-fn", 896 | ] 897 | 898 | [[package]] 899 | name = "futures-macro" 900 | version = "0.3.30" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 903 | dependencies = [ 904 | "proc-macro2", 905 | "quote", 906 | "syn 2.0.60", 907 | ] 908 | 909 | [[package]] 910 | name = "futures-sink" 911 | version = "0.3.30" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 914 | 915 | [[package]] 916 | name = "futures-task" 917 | version = "0.3.30" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 920 | 921 | [[package]] 922 | name = "futures-util" 923 | version = "0.3.30" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 926 | dependencies = [ 927 | "futures-channel", 928 | "futures-core", 929 | "futures-io", 930 | "futures-macro", 931 | "futures-sink", 932 | "futures-task", 933 | "memchr", 934 | "pin-project-lite", 935 | "pin-utils", 936 | "slab", 937 | ] 938 | 939 | [[package]] 940 | name = "fxhash" 941 | version = "0.2.1" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 944 | dependencies = [ 945 | "byteorder", 946 | ] 947 | 948 | [[package]] 949 | name = "generic-array" 950 | version = "0.14.7" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 953 | dependencies = [ 954 | "typenum", 955 | "version_check", 956 | ] 957 | 958 | [[package]] 959 | name = "gethostname" 960 | version = "0.2.3" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" 963 | dependencies = [ 964 | "libc", 965 | "winapi", 966 | ] 967 | 968 | [[package]] 969 | name = "getrandom" 970 | version = "0.2.10" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 973 | dependencies = [ 974 | "cfg-if", 975 | "libc", 976 | "wasi", 977 | ] 978 | 979 | [[package]] 980 | name = "gimli" 981 | version = "0.28.0" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 984 | 985 | [[package]] 986 | name = "hashbrown" 987 | version = "0.14.1" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" 990 | dependencies = [ 991 | "ahash", 992 | "allocator-api2", 993 | ] 994 | 995 | [[package]] 996 | name = "heck" 997 | version = "0.4.1" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 1000 | 1001 | [[package]] 1002 | name = "heck" 1003 | version = "0.5.0" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1006 | 1007 | [[package]] 1008 | name = "hermit-abi" 1009 | version = "0.3.9" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 1012 | 1013 | [[package]] 1014 | name = "hex" 1015 | version = "0.4.3" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1018 | 1019 | [[package]] 1020 | name = "humantime" 1021 | version = "2.1.0" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 1024 | 1025 | [[package]] 1026 | name = "iana-time-zone" 1027 | version = "0.1.57" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" 1030 | dependencies = [ 1031 | "android_system_properties", 1032 | "core-foundation-sys", 1033 | "iana-time-zone-haiku", 1034 | "js-sys", 1035 | "wasm-bindgen", 1036 | "windows", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "iana-time-zone-haiku" 1041 | version = "0.1.2" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1044 | dependencies = [ 1045 | "cc", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "indexmap" 1050 | version = "2.0.2" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" 1053 | dependencies = [ 1054 | "equivalent", 1055 | "hashbrown", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "instability" 1060 | version = "0.3.2" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 1063 | dependencies = [ 1064 | "quote", 1065 | "syn 2.0.60", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "is-docker" 1070 | version = "0.2.0" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" 1073 | dependencies = [ 1074 | "once_cell", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "is-terminal" 1079 | version = "0.4.9" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 1082 | dependencies = [ 1083 | "hermit-abi", 1084 | "rustix", 1085 | "windows-sys 0.48.0", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "is-wsl" 1090 | version = "0.4.0" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" 1093 | dependencies = [ 1094 | "is-docker", 1095 | "once_cell", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "itertools" 1100 | version = "0.12.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" 1103 | dependencies = [ 1104 | "either", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "itertools" 1109 | version = "0.13.0" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 1112 | dependencies = [ 1113 | "either", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "itoa" 1118 | version = "1.0.11" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 1121 | 1122 | [[package]] 1123 | name = "js-sys" 1124 | version = "0.3.64" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 1127 | dependencies = [ 1128 | "wasm-bindgen", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "lazy_static" 1133 | version = "1.4.0" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 1136 | 1137 | [[package]] 1138 | name = "libc" 1139 | version = "0.2.153" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 1142 | 1143 | [[package]] 1144 | name = "linux-raw-sys" 1145 | version = "0.4.13" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 1148 | 1149 | [[package]] 1150 | name = "lock_api" 1151 | version = "0.4.10" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 1154 | dependencies = [ 1155 | "autocfg", 1156 | "scopeguard", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "log" 1161 | version = "0.4.20" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 1164 | 1165 | [[package]] 1166 | name = "lru" 1167 | version = "0.12.1" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7" 1170 | dependencies = [ 1171 | "hashbrown", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "malloc_buf" 1176 | version = "0.0.6" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 1179 | dependencies = [ 1180 | "libc", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "matchers" 1185 | version = "0.1.0" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1188 | dependencies = [ 1189 | "regex-automata 0.1.10", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "memchr" 1194 | version = "2.6.3" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 1197 | 1198 | [[package]] 1199 | name = "memoffset" 1200 | version = "0.6.5" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 1203 | dependencies = [ 1204 | "autocfg", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "memoffset" 1209 | version = "0.9.1" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 1212 | dependencies = [ 1213 | "autocfg", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "miniz_oxide" 1218 | version = "0.7.1" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 1221 | dependencies = [ 1222 | "adler", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "mio" 1227 | version = "0.8.8" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 1230 | dependencies = [ 1231 | "libc", 1232 | "wasi", 1233 | "windows-sys 0.48.0", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "mio" 1238 | version = "1.0.2" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 1241 | dependencies = [ 1242 | "hermit-abi", 1243 | "libc", 1244 | "log", 1245 | "wasi", 1246 | "windows-sys 0.52.0", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "nix" 1251 | version = "0.24.3" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 1254 | dependencies = [ 1255 | "bitflags 1.3.2", 1256 | "cfg-if", 1257 | "libc", 1258 | "memoffset 0.6.5", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "nix" 1263 | version = "0.28.0" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 1266 | dependencies = [ 1267 | "bitflags 2.4.0", 1268 | "cfg-if", 1269 | "cfg_aliases", 1270 | "libc", 1271 | "memoffset 0.9.1", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "nu-ansi-term" 1276 | version = "0.46.0" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1279 | dependencies = [ 1280 | "overload", 1281 | "winapi", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "num-conv" 1286 | version = "0.1.0" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1289 | 1290 | [[package]] 1291 | name = "num-traits" 1292 | version = "0.2.16" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" 1295 | dependencies = [ 1296 | "autocfg", 1297 | ] 1298 | 1299 | [[package]] 1300 | name = "num_cpus" 1301 | version = "1.16.0" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 1304 | dependencies = [ 1305 | "hermit-abi", 1306 | "libc", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "objc" 1311 | version = "0.2.7" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 1314 | dependencies = [ 1315 | "malloc_buf", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "objc-foundation" 1320 | version = "0.1.1" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 1323 | dependencies = [ 1324 | "block", 1325 | "objc", 1326 | "objc_id", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "objc_id" 1331 | version = "0.1.1" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 1334 | dependencies = [ 1335 | "objc", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "object" 1340 | version = "0.32.1" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 1343 | dependencies = [ 1344 | "memchr", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "once_cell" 1349 | version = "1.18.0" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 1352 | 1353 | [[package]] 1354 | name = "option-ext" 1355 | version = "0.2.0" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1358 | 1359 | [[package]] 1360 | name = "ordered-stream" 1361 | version = "0.2.0" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" 1364 | dependencies = [ 1365 | "futures-core", 1366 | "pin-project-lite", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "os_pipe" 1371 | version = "1.1.4" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" 1374 | dependencies = [ 1375 | "libc", 1376 | "windows-sys 0.48.0", 1377 | ] 1378 | 1379 | [[package]] 1380 | name = "overload" 1381 | version = "0.1.1" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1384 | 1385 | [[package]] 1386 | name = "parking" 1387 | version = "2.1.1" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" 1390 | 1391 | [[package]] 1392 | name = "parking_lot" 1393 | version = "0.12.1" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1396 | dependencies = [ 1397 | "lock_api", 1398 | "parking_lot_core", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "parking_lot_core" 1403 | version = "0.9.8" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 1406 | dependencies = [ 1407 | "cfg-if", 1408 | "libc", 1409 | "redox_syscall 0.3.5", 1410 | "smallvec", 1411 | "windows-targets 0.48.5", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "paste" 1416 | version = "1.0.14" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 1419 | 1420 | [[package]] 1421 | name = "pin-project-lite" 1422 | version = "0.2.13" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 1425 | 1426 | [[package]] 1427 | name = "pin-utils" 1428 | version = "0.1.0" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1431 | 1432 | [[package]] 1433 | name = "piper" 1434 | version = "0.2.1" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" 1437 | dependencies = [ 1438 | "atomic-waker", 1439 | "fastrand", 1440 | "futures-io", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "polling" 1445 | version = "3.7.0" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" 1448 | dependencies = [ 1449 | "cfg-if", 1450 | "concurrent-queue", 1451 | "hermit-abi", 1452 | "pin-project-lite", 1453 | "rustix", 1454 | "tracing", 1455 | "windows-sys 0.52.0", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "powerfmt" 1460 | version = "0.2.0" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1463 | 1464 | [[package]] 1465 | name = "ppv-lite86" 1466 | version = "0.2.17" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1469 | 1470 | [[package]] 1471 | name = "proc-macro-crate" 1472 | version = "3.1.0" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" 1475 | dependencies = [ 1476 | "toml_edit", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "proc-macro2" 1481 | version = "1.0.81" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 1484 | dependencies = [ 1485 | "unicode-ident", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "quote" 1490 | version = "1.0.36" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 1493 | dependencies = [ 1494 | "proc-macro2", 1495 | ] 1496 | 1497 | [[package]] 1498 | name = "rand" 1499 | version = "0.8.5" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1502 | dependencies = [ 1503 | "libc", 1504 | "rand_chacha", 1505 | "rand_core", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "rand_chacha" 1510 | version = "0.3.1" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1513 | dependencies = [ 1514 | "ppv-lite86", 1515 | "rand_core", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "rand_core" 1520 | version = "0.6.4" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1523 | dependencies = [ 1524 | "getrandom", 1525 | ] 1526 | 1527 | [[package]] 1528 | name = "ratatui" 1529 | version = "0.28.0" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" 1532 | dependencies = [ 1533 | "bitflags 2.4.0", 1534 | "cassowary", 1535 | "compact_str", 1536 | "crossterm", 1537 | "instability", 1538 | "itertools 0.13.0", 1539 | "lru", 1540 | "paste", 1541 | "strum", 1542 | "strum_macros", 1543 | "unicode-segmentation", 1544 | "unicode-truncate", 1545 | "unicode-width", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "redox_syscall" 1550 | version = "0.2.16" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1553 | dependencies = [ 1554 | "bitflags 1.3.2", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "redox_syscall" 1559 | version = "0.3.5" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 1562 | dependencies = [ 1563 | "bitflags 1.3.2", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "redox_users" 1568 | version = "0.4.3" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 1571 | dependencies = [ 1572 | "getrandom", 1573 | "redox_syscall 0.2.16", 1574 | "thiserror", 1575 | ] 1576 | 1577 | [[package]] 1578 | name = "regex" 1579 | version = "1.10.4" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 1582 | dependencies = [ 1583 | "aho-corasick", 1584 | "memchr", 1585 | "regex-automata 0.4.6", 1586 | "regex-syntax 0.8.3", 1587 | ] 1588 | 1589 | [[package]] 1590 | name = "regex-automata" 1591 | version = "0.1.10" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1594 | dependencies = [ 1595 | "regex-syntax 0.6.29", 1596 | ] 1597 | 1598 | [[package]] 1599 | name = "regex-automata" 1600 | version = "0.4.6" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 1603 | dependencies = [ 1604 | "aho-corasick", 1605 | "memchr", 1606 | "regex-syntax 0.8.3", 1607 | ] 1608 | 1609 | [[package]] 1610 | name = "regex-syntax" 1611 | version = "0.6.29" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1614 | 1615 | [[package]] 1616 | name = "regex-syntax" 1617 | version = "0.8.3" 1618 | source = "registry+https://github.com/rust-lang/crates.io-index" 1619 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 1620 | 1621 | [[package]] 1622 | name = "rustc-demangle" 1623 | version = "0.1.23" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 1626 | 1627 | [[package]] 1628 | name = "rustix" 1629 | version = "0.38.34" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 1632 | dependencies = [ 1633 | "bitflags 2.4.0", 1634 | "errno", 1635 | "libc", 1636 | "linux-raw-sys", 1637 | "windows-sys 0.52.0", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "rustversion" 1642 | version = "1.0.14" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 1645 | 1646 | [[package]] 1647 | name = "ryu" 1648 | version = "1.0.17" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 1651 | 1652 | [[package]] 1653 | name = "scopeguard" 1654 | version = "1.2.0" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1657 | 1658 | [[package]] 1659 | name = "serde" 1660 | version = "1.0.188" 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" 1662 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 1663 | dependencies = [ 1664 | "serde_derive", 1665 | ] 1666 | 1667 | [[package]] 1668 | name = "serde_derive" 1669 | version = "1.0.188" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 1672 | dependencies = [ 1673 | "proc-macro2", 1674 | "quote", 1675 | "syn 2.0.60", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "serde_repr" 1680 | version = "0.1.19" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 1683 | dependencies = [ 1684 | "proc-macro2", 1685 | "quote", 1686 | "syn 2.0.60", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "sha1" 1691 | version = "0.10.6" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1694 | dependencies = [ 1695 | "cfg-if", 1696 | "cpufeatures", 1697 | "digest", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "sharded-slab" 1702 | version = "0.1.6" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b" 1705 | dependencies = [ 1706 | "lazy_static", 1707 | ] 1708 | 1709 | [[package]] 1710 | name = "shared_child" 1711 | version = "1.0.0" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" 1714 | dependencies = [ 1715 | "libc", 1716 | "winapi", 1717 | ] 1718 | 1719 | [[package]] 1720 | name = "signal-hook" 1721 | version = "0.3.17" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1724 | dependencies = [ 1725 | "libc", 1726 | "signal-hook-registry", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "signal-hook-mio" 1731 | version = "0.2.4" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1734 | dependencies = [ 1735 | "libc", 1736 | "mio 1.0.2", 1737 | "signal-hook", 1738 | ] 1739 | 1740 | [[package]] 1741 | name = "signal-hook-registry" 1742 | version = "1.4.1" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1745 | dependencies = [ 1746 | "libc", 1747 | ] 1748 | 1749 | [[package]] 1750 | name = "slab" 1751 | version = "0.4.9" 1752 | source = "registry+https://github.com/rust-lang/crates.io-index" 1753 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1754 | dependencies = [ 1755 | "autocfg", 1756 | ] 1757 | 1758 | [[package]] 1759 | name = "smallvec" 1760 | version = "1.11.1" 1761 | source = "registry+https://github.com/rust-lang/crates.io-index" 1762 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 1763 | 1764 | [[package]] 1765 | name = "socket2" 1766 | version = "0.5.4" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" 1769 | dependencies = [ 1770 | "libc", 1771 | "windows-sys 0.48.0", 1772 | ] 1773 | 1774 | [[package]] 1775 | name = "static_assertions" 1776 | version = "1.1.0" 1777 | source = "registry+https://github.com/rust-lang/crates.io-index" 1778 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1779 | 1780 | [[package]] 1781 | name = "str-buf" 1782 | version = "1.0.6" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" 1785 | 1786 | [[package]] 1787 | name = "strsim" 1788 | version = "0.10.0" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1791 | 1792 | [[package]] 1793 | name = "strum" 1794 | version = "0.26.2" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 1797 | dependencies = [ 1798 | "strum_macros", 1799 | ] 1800 | 1801 | [[package]] 1802 | name = "strum_macros" 1803 | version = "0.26.4" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1806 | dependencies = [ 1807 | "heck 0.5.0", 1808 | "proc-macro2", 1809 | "quote", 1810 | "rustversion", 1811 | "syn 2.0.60", 1812 | ] 1813 | 1814 | [[package]] 1815 | name = "syn" 1816 | version = "1.0.109" 1817 | source = "registry+https://github.com/rust-lang/crates.io-index" 1818 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1819 | dependencies = [ 1820 | "proc-macro2", 1821 | "quote", 1822 | "unicode-ident", 1823 | ] 1824 | 1825 | [[package]] 1826 | name = "syn" 1827 | version = "2.0.60" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 1830 | dependencies = [ 1831 | "proc-macro2", 1832 | "quote", 1833 | "unicode-ident", 1834 | ] 1835 | 1836 | [[package]] 1837 | name = "systemctl-tui" 1838 | version = "0.4.0" 1839 | dependencies = [ 1840 | "anyhow", 1841 | "better-panic", 1842 | "chrono", 1843 | "clap", 1844 | "clap_complete", 1845 | "clipboard-anywhere", 1846 | "colored", 1847 | "crossterm", 1848 | "directories", 1849 | "env_logger", 1850 | "futures", 1851 | "indexmap", 1852 | "is-wsl", 1853 | "itertools 0.12.0", 1854 | "lazy_static", 1855 | "libc", 1856 | "log", 1857 | "nix 0.28.0", 1858 | "ratatui", 1859 | "signal-hook", 1860 | "tokio", 1861 | "tokio-stream", 1862 | "tokio-util", 1863 | "tracing", 1864 | "tracing-appender", 1865 | "tracing-macros", 1866 | "tracing-subscriber", 1867 | "tui-input", 1868 | "tui-logger", 1869 | "unicode-segmentation", 1870 | "zbus", 1871 | ] 1872 | 1873 | [[package]] 1874 | name = "tempfile" 1875 | version = "3.8.0" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" 1878 | dependencies = [ 1879 | "cfg-if", 1880 | "fastrand", 1881 | "redox_syscall 0.3.5", 1882 | "rustix", 1883 | "windows-sys 0.48.0", 1884 | ] 1885 | 1886 | [[package]] 1887 | name = "terminal_size" 1888 | version = "0.3.0" 1889 | source = "registry+https://github.com/rust-lang/crates.io-index" 1890 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 1891 | dependencies = [ 1892 | "rustix", 1893 | "windows-sys 0.48.0", 1894 | ] 1895 | 1896 | [[package]] 1897 | name = "thiserror" 1898 | version = "1.0.49" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" 1901 | dependencies = [ 1902 | "thiserror-impl", 1903 | ] 1904 | 1905 | [[package]] 1906 | name = "thiserror-impl" 1907 | version = "1.0.49" 1908 | source = "registry+https://github.com/rust-lang/crates.io-index" 1909 | checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" 1910 | dependencies = [ 1911 | "proc-macro2", 1912 | "quote", 1913 | "syn 2.0.60", 1914 | ] 1915 | 1916 | [[package]] 1917 | name = "thread_local" 1918 | version = "1.1.7" 1919 | source = "registry+https://github.com/rust-lang/crates.io-index" 1920 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 1921 | dependencies = [ 1922 | "cfg-if", 1923 | "once_cell", 1924 | ] 1925 | 1926 | [[package]] 1927 | name = "time" 1928 | version = "0.3.36" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1931 | dependencies = [ 1932 | "deranged", 1933 | "itoa", 1934 | "num-conv", 1935 | "powerfmt", 1936 | "serde", 1937 | "time-core", 1938 | "time-macros", 1939 | ] 1940 | 1941 | [[package]] 1942 | name = "time-core" 1943 | version = "0.1.2" 1944 | source = "registry+https://github.com/rust-lang/crates.io-index" 1945 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1946 | 1947 | [[package]] 1948 | name = "time-macros" 1949 | version = "0.2.18" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1952 | dependencies = [ 1953 | "num-conv", 1954 | "time-core", 1955 | ] 1956 | 1957 | [[package]] 1958 | name = "tokio" 1959 | version = "1.32.0" 1960 | source = "registry+https://github.com/rust-lang/crates.io-index" 1961 | checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" 1962 | dependencies = [ 1963 | "backtrace", 1964 | "bytes", 1965 | "libc", 1966 | "mio 0.8.8", 1967 | "num_cpus", 1968 | "parking_lot", 1969 | "pin-project-lite", 1970 | "signal-hook-registry", 1971 | "socket2", 1972 | "tokio-macros", 1973 | "tracing", 1974 | "windows-sys 0.48.0", 1975 | ] 1976 | 1977 | [[package]] 1978 | name = "tokio-macros" 1979 | version = "2.1.0" 1980 | source = "registry+https://github.com/rust-lang/crates.io-index" 1981 | checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" 1982 | dependencies = [ 1983 | "proc-macro2", 1984 | "quote", 1985 | "syn 2.0.60", 1986 | ] 1987 | 1988 | [[package]] 1989 | name = "tokio-stream" 1990 | version = "0.1.14" 1991 | source = "registry+https://github.com/rust-lang/crates.io-index" 1992 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 1993 | dependencies = [ 1994 | "futures-core", 1995 | "pin-project-lite", 1996 | "tokio", 1997 | ] 1998 | 1999 | [[package]] 2000 | name = "tokio-util" 2001 | version = "0.7.9" 2002 | source = "registry+https://github.com/rust-lang/crates.io-index" 2003 | checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" 2004 | dependencies = [ 2005 | "bytes", 2006 | "futures-core", 2007 | "futures-sink", 2008 | "pin-project-lite", 2009 | "tokio", 2010 | ] 2011 | 2012 | [[package]] 2013 | name = "toml_datetime" 2014 | version = "0.6.5" 2015 | source = "registry+https://github.com/rust-lang/crates.io-index" 2016 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 2017 | 2018 | [[package]] 2019 | name = "toml_edit" 2020 | version = "0.21.1" 2021 | source = "registry+https://github.com/rust-lang/crates.io-index" 2022 | checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" 2023 | dependencies = [ 2024 | "indexmap", 2025 | "toml_datetime", 2026 | "winnow", 2027 | ] 2028 | 2029 | [[package]] 2030 | name = "tracing" 2031 | version = "0.1.40" 2032 | source = "registry+https://github.com/rust-lang/crates.io-index" 2033 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 2034 | dependencies = [ 2035 | "pin-project-lite", 2036 | "tracing-attributes", 2037 | "tracing-core", 2038 | ] 2039 | 2040 | [[package]] 2041 | name = "tracing-appender" 2042 | version = "0.2.3" 2043 | source = "registry+https://github.com/rust-lang/crates.io-index" 2044 | checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" 2045 | dependencies = [ 2046 | "crossbeam-channel", 2047 | "thiserror", 2048 | "time", 2049 | "tracing-subscriber", 2050 | ] 2051 | 2052 | [[package]] 2053 | name = "tracing-attributes" 2054 | version = "0.1.27" 2055 | source = "registry+https://github.com/rust-lang/crates.io-index" 2056 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 2057 | dependencies = [ 2058 | "proc-macro2", 2059 | "quote", 2060 | "syn 2.0.60", 2061 | ] 2062 | 2063 | [[package]] 2064 | name = "tracing-core" 2065 | version = "0.1.32" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 2068 | dependencies = [ 2069 | "once_cell", 2070 | "valuable", 2071 | ] 2072 | 2073 | [[package]] 2074 | name = "tracing-log" 2075 | version = "0.2.0" 2076 | source = "registry+https://github.com/rust-lang/crates.io-index" 2077 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2078 | dependencies = [ 2079 | "log", 2080 | "once_cell", 2081 | "tracing-core", 2082 | ] 2083 | 2084 | [[package]] 2085 | name = "tracing-macros" 2086 | version = "0.0.0" 2087 | source = "registry+https://github.com/rust-lang/crates.io-index" 2088 | checksum = "379c276d5004d8ccded2485ba7ff97d8165ebe12f04b9a0a2dbd837a7d3034a1" 2089 | 2090 | [[package]] 2091 | name = "tracing-subscriber" 2092 | version = "0.3.18" 2093 | source = "registry+https://github.com/rust-lang/crates.io-index" 2094 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 2095 | dependencies = [ 2096 | "matchers", 2097 | "nu-ansi-term", 2098 | "once_cell", 2099 | "regex", 2100 | "sharded-slab", 2101 | "smallvec", 2102 | "thread_local", 2103 | "tracing", 2104 | "tracing-core", 2105 | "tracing-log", 2106 | ] 2107 | 2108 | [[package]] 2109 | name = "tui-input" 2110 | version = "0.10.0" 2111 | source = "registry+https://github.com/rust-lang/crates.io-index" 2112 | checksum = "68699e8bb4ca025ab41fcc602d3d53a5714a56b0cf2d6e93c98aaf4231e3d937" 2113 | dependencies = [ 2114 | "ratatui", 2115 | "unicode-width", 2116 | ] 2117 | 2118 | [[package]] 2119 | name = "tui-logger" 2120 | version = "0.12.0" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "c4f163885b0550ad9821f52700b6da2e0e249641722be4e2b615ee6c5010ec3e" 2123 | dependencies = [ 2124 | "chrono", 2125 | "fxhash", 2126 | "lazy_static", 2127 | "log", 2128 | "parking_lot", 2129 | "ratatui", 2130 | "tracing", 2131 | "tracing-subscriber", 2132 | ] 2133 | 2134 | [[package]] 2135 | name = "typenum" 2136 | version = "1.17.0" 2137 | source = "registry+https://github.com/rust-lang/crates.io-index" 2138 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2139 | 2140 | [[package]] 2141 | name = "uds_windows" 2142 | version = "1.1.0" 2143 | source = "registry+https://github.com/rust-lang/crates.io-index" 2144 | checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" 2145 | dependencies = [ 2146 | "memoffset 0.9.1", 2147 | "tempfile", 2148 | "winapi", 2149 | ] 2150 | 2151 | [[package]] 2152 | name = "unicase" 2153 | version = "2.7.0" 2154 | source = "registry+https://github.com/rust-lang/crates.io-index" 2155 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 2156 | dependencies = [ 2157 | "version_check", 2158 | ] 2159 | 2160 | [[package]] 2161 | name = "unicode-ident" 2162 | version = "1.0.12" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 2165 | 2166 | [[package]] 2167 | name = "unicode-segmentation" 2168 | version = "1.10.1" 2169 | source = "registry+https://github.com/rust-lang/crates.io-index" 2170 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 2171 | 2172 | [[package]] 2173 | name = "unicode-truncate" 2174 | version = "1.1.0" 2175 | source = "registry+https://github.com/rust-lang/crates.io-index" 2176 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 2177 | dependencies = [ 2178 | "itertools 0.13.0", 2179 | "unicode-segmentation", 2180 | "unicode-width", 2181 | ] 2182 | 2183 | [[package]] 2184 | name = "unicode-width" 2185 | version = "0.1.13" 2186 | source = "registry+https://github.com/rust-lang/crates.io-index" 2187 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 2188 | 2189 | [[package]] 2190 | name = "utf8parse" 2191 | version = "0.2.1" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 2194 | 2195 | [[package]] 2196 | name = "valuable" 2197 | version = "0.1.0" 2198 | source = "registry+https://github.com/rust-lang/crates.io-index" 2199 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2200 | 2201 | [[package]] 2202 | name = "version_check" 2203 | version = "0.9.4" 2204 | source = "registry+https://github.com/rust-lang/crates.io-index" 2205 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 2206 | 2207 | [[package]] 2208 | name = "waker-fn" 2209 | version = "1.1.1" 2210 | source = "registry+https://github.com/rust-lang/crates.io-index" 2211 | checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" 2212 | 2213 | [[package]] 2214 | name = "wasi" 2215 | version = "0.11.0+wasi-snapshot-preview1" 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" 2217 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2218 | 2219 | [[package]] 2220 | name = "wasm-bindgen" 2221 | version = "0.2.87" 2222 | source = "registry+https://github.com/rust-lang/crates.io-index" 2223 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 2224 | dependencies = [ 2225 | "cfg-if", 2226 | "wasm-bindgen-macro", 2227 | ] 2228 | 2229 | [[package]] 2230 | name = "wasm-bindgen-backend" 2231 | version = "0.2.87" 2232 | source = "registry+https://github.com/rust-lang/crates.io-index" 2233 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 2234 | dependencies = [ 2235 | "bumpalo", 2236 | "log", 2237 | "once_cell", 2238 | "proc-macro2", 2239 | "quote", 2240 | "syn 2.0.60", 2241 | "wasm-bindgen-shared", 2242 | ] 2243 | 2244 | [[package]] 2245 | name = "wasm-bindgen-macro" 2246 | version = "0.2.87" 2247 | source = "registry+https://github.com/rust-lang/crates.io-index" 2248 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 2249 | dependencies = [ 2250 | "quote", 2251 | "wasm-bindgen-macro-support", 2252 | ] 2253 | 2254 | [[package]] 2255 | name = "wasm-bindgen-macro-support" 2256 | version = "0.2.87" 2257 | source = "registry+https://github.com/rust-lang/crates.io-index" 2258 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 2259 | dependencies = [ 2260 | "proc-macro2", 2261 | "quote", 2262 | "syn 2.0.60", 2263 | "wasm-bindgen-backend", 2264 | "wasm-bindgen-shared", 2265 | ] 2266 | 2267 | [[package]] 2268 | name = "wasm-bindgen-shared" 2269 | version = "0.2.87" 2270 | source = "registry+https://github.com/rust-lang/crates.io-index" 2271 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 2272 | 2273 | [[package]] 2274 | name = "winapi" 2275 | version = "0.3.9" 2276 | source = "registry+https://github.com/rust-lang/crates.io-index" 2277 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2278 | dependencies = [ 2279 | "winapi-i686-pc-windows-gnu", 2280 | "winapi-x86_64-pc-windows-gnu", 2281 | ] 2282 | 2283 | [[package]] 2284 | name = "winapi-i686-pc-windows-gnu" 2285 | version = "0.4.0" 2286 | source = "registry+https://github.com/rust-lang/crates.io-index" 2287 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2288 | 2289 | [[package]] 2290 | name = "winapi-wsapoll" 2291 | version = "0.1.1" 2292 | source = "registry+https://github.com/rust-lang/crates.io-index" 2293 | checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" 2294 | dependencies = [ 2295 | "winapi", 2296 | ] 2297 | 2298 | [[package]] 2299 | name = "winapi-x86_64-pc-windows-gnu" 2300 | version = "0.4.0" 2301 | source = "registry+https://github.com/rust-lang/crates.io-index" 2302 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2303 | 2304 | [[package]] 2305 | name = "windows" 2306 | version = "0.48.0" 2307 | source = "registry+https://github.com/rust-lang/crates.io-index" 2308 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 2309 | dependencies = [ 2310 | "windows-targets 0.48.5", 2311 | ] 2312 | 2313 | [[package]] 2314 | name = "windows-sys" 2315 | version = "0.45.0" 2316 | source = "registry+https://github.com/rust-lang/crates.io-index" 2317 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 2318 | dependencies = [ 2319 | "windows-targets 0.42.2", 2320 | ] 2321 | 2322 | [[package]] 2323 | name = "windows-sys" 2324 | version = "0.48.0" 2325 | source = "registry+https://github.com/rust-lang/crates.io-index" 2326 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2327 | dependencies = [ 2328 | "windows-targets 0.48.5", 2329 | ] 2330 | 2331 | [[package]] 2332 | name = "windows-sys" 2333 | version = "0.52.0" 2334 | source = "registry+https://github.com/rust-lang/crates.io-index" 2335 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2336 | dependencies = [ 2337 | "windows-targets 0.52.5", 2338 | ] 2339 | 2340 | [[package]] 2341 | name = "windows-targets" 2342 | version = "0.42.2" 2343 | source = "registry+https://github.com/rust-lang/crates.io-index" 2344 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 2345 | dependencies = [ 2346 | "windows_aarch64_gnullvm 0.42.2", 2347 | "windows_aarch64_msvc 0.42.2", 2348 | "windows_i686_gnu 0.42.2", 2349 | "windows_i686_msvc 0.42.2", 2350 | "windows_x86_64_gnu 0.42.2", 2351 | "windows_x86_64_gnullvm 0.42.2", 2352 | "windows_x86_64_msvc 0.42.2", 2353 | ] 2354 | 2355 | [[package]] 2356 | name = "windows-targets" 2357 | version = "0.48.5" 2358 | source = "registry+https://github.com/rust-lang/crates.io-index" 2359 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2360 | dependencies = [ 2361 | "windows_aarch64_gnullvm 0.48.5", 2362 | "windows_aarch64_msvc 0.48.5", 2363 | "windows_i686_gnu 0.48.5", 2364 | "windows_i686_msvc 0.48.5", 2365 | "windows_x86_64_gnu 0.48.5", 2366 | "windows_x86_64_gnullvm 0.48.5", 2367 | "windows_x86_64_msvc 0.48.5", 2368 | ] 2369 | 2370 | [[package]] 2371 | name = "windows-targets" 2372 | version = "0.52.5" 2373 | source = "registry+https://github.com/rust-lang/crates.io-index" 2374 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 2375 | dependencies = [ 2376 | "windows_aarch64_gnullvm 0.52.5", 2377 | "windows_aarch64_msvc 0.52.5", 2378 | "windows_i686_gnu 0.52.5", 2379 | "windows_i686_gnullvm", 2380 | "windows_i686_msvc 0.52.5", 2381 | "windows_x86_64_gnu 0.52.5", 2382 | "windows_x86_64_gnullvm 0.52.5", 2383 | "windows_x86_64_msvc 0.52.5", 2384 | ] 2385 | 2386 | [[package]] 2387 | name = "windows_aarch64_gnullvm" 2388 | version = "0.42.2" 2389 | source = "registry+https://github.com/rust-lang/crates.io-index" 2390 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 2391 | 2392 | [[package]] 2393 | name = "windows_aarch64_gnullvm" 2394 | version = "0.48.5" 2395 | source = "registry+https://github.com/rust-lang/crates.io-index" 2396 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2397 | 2398 | [[package]] 2399 | name = "windows_aarch64_gnullvm" 2400 | version = "0.52.5" 2401 | source = "registry+https://github.com/rust-lang/crates.io-index" 2402 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 2403 | 2404 | [[package]] 2405 | name = "windows_aarch64_msvc" 2406 | version = "0.42.2" 2407 | source = "registry+https://github.com/rust-lang/crates.io-index" 2408 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 2409 | 2410 | [[package]] 2411 | name = "windows_aarch64_msvc" 2412 | version = "0.48.5" 2413 | source = "registry+https://github.com/rust-lang/crates.io-index" 2414 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2415 | 2416 | [[package]] 2417 | name = "windows_aarch64_msvc" 2418 | version = "0.52.5" 2419 | source = "registry+https://github.com/rust-lang/crates.io-index" 2420 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 2421 | 2422 | [[package]] 2423 | name = "windows_i686_gnu" 2424 | version = "0.42.2" 2425 | source = "registry+https://github.com/rust-lang/crates.io-index" 2426 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 2427 | 2428 | [[package]] 2429 | name = "windows_i686_gnu" 2430 | version = "0.48.5" 2431 | source = "registry+https://github.com/rust-lang/crates.io-index" 2432 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2433 | 2434 | [[package]] 2435 | name = "windows_i686_gnu" 2436 | version = "0.52.5" 2437 | source = "registry+https://github.com/rust-lang/crates.io-index" 2438 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 2439 | 2440 | [[package]] 2441 | name = "windows_i686_gnullvm" 2442 | version = "0.52.5" 2443 | source = "registry+https://github.com/rust-lang/crates.io-index" 2444 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 2445 | 2446 | [[package]] 2447 | name = "windows_i686_msvc" 2448 | version = "0.42.2" 2449 | source = "registry+https://github.com/rust-lang/crates.io-index" 2450 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 2451 | 2452 | [[package]] 2453 | name = "windows_i686_msvc" 2454 | version = "0.48.5" 2455 | source = "registry+https://github.com/rust-lang/crates.io-index" 2456 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2457 | 2458 | [[package]] 2459 | name = "windows_i686_msvc" 2460 | version = "0.52.5" 2461 | source = "registry+https://github.com/rust-lang/crates.io-index" 2462 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 2463 | 2464 | [[package]] 2465 | name = "windows_x86_64_gnu" 2466 | version = "0.42.2" 2467 | source = "registry+https://github.com/rust-lang/crates.io-index" 2468 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 2469 | 2470 | [[package]] 2471 | name = "windows_x86_64_gnu" 2472 | version = "0.48.5" 2473 | source = "registry+https://github.com/rust-lang/crates.io-index" 2474 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2475 | 2476 | [[package]] 2477 | name = "windows_x86_64_gnu" 2478 | version = "0.52.5" 2479 | source = "registry+https://github.com/rust-lang/crates.io-index" 2480 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 2481 | 2482 | [[package]] 2483 | name = "windows_x86_64_gnullvm" 2484 | version = "0.42.2" 2485 | source = "registry+https://github.com/rust-lang/crates.io-index" 2486 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 2487 | 2488 | [[package]] 2489 | name = "windows_x86_64_gnullvm" 2490 | version = "0.48.5" 2491 | source = "registry+https://github.com/rust-lang/crates.io-index" 2492 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2493 | 2494 | [[package]] 2495 | name = "windows_x86_64_gnullvm" 2496 | version = "0.52.5" 2497 | source = "registry+https://github.com/rust-lang/crates.io-index" 2498 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 2499 | 2500 | [[package]] 2501 | name = "windows_x86_64_msvc" 2502 | version = "0.42.2" 2503 | source = "registry+https://github.com/rust-lang/crates.io-index" 2504 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 2505 | 2506 | [[package]] 2507 | name = "windows_x86_64_msvc" 2508 | version = "0.48.5" 2509 | source = "registry+https://github.com/rust-lang/crates.io-index" 2510 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2511 | 2512 | [[package]] 2513 | name = "windows_x86_64_msvc" 2514 | version = "0.52.5" 2515 | source = "registry+https://github.com/rust-lang/crates.io-index" 2516 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 2517 | 2518 | [[package]] 2519 | name = "winnow" 2520 | version = "0.5.15" 2521 | source = "registry+https://github.com/rust-lang/crates.io-index" 2522 | checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" 2523 | dependencies = [ 2524 | "memchr", 2525 | ] 2526 | 2527 | [[package]] 2528 | name = "x11rb" 2529 | version = "0.10.1" 2530 | source = "registry+https://github.com/rust-lang/crates.io-index" 2531 | checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" 2532 | dependencies = [ 2533 | "gethostname", 2534 | "nix 0.24.3", 2535 | "winapi", 2536 | "winapi-wsapoll", 2537 | "x11rb-protocol", 2538 | ] 2539 | 2540 | [[package]] 2541 | name = "x11rb-protocol" 2542 | version = "0.10.0" 2543 | source = "registry+https://github.com/rust-lang/crates.io-index" 2544 | checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" 2545 | dependencies = [ 2546 | "nix 0.24.3", 2547 | ] 2548 | 2549 | [[package]] 2550 | name = "xdg-home" 2551 | version = "1.1.0" 2552 | source = "registry+https://github.com/rust-lang/crates.io-index" 2553 | checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" 2554 | dependencies = [ 2555 | "libc", 2556 | "winapi", 2557 | ] 2558 | 2559 | [[package]] 2560 | name = "zbus" 2561 | version = "4.1.2" 2562 | source = "registry+https://github.com/rust-lang/crates.io-index" 2563 | checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" 2564 | dependencies = [ 2565 | "async-broadcast", 2566 | "async-process", 2567 | "async-recursion", 2568 | "async-trait", 2569 | "derivative", 2570 | "enumflags2", 2571 | "event-listener 5.3.0", 2572 | "futures-core", 2573 | "futures-sink", 2574 | "futures-util", 2575 | "hex", 2576 | "nix 0.28.0", 2577 | "ordered-stream", 2578 | "rand", 2579 | "serde", 2580 | "serde_repr", 2581 | "sha1", 2582 | "static_assertions", 2583 | "tokio", 2584 | "tracing", 2585 | "uds_windows", 2586 | "windows-sys 0.52.0", 2587 | "xdg-home", 2588 | "zbus_macros", 2589 | "zbus_names", 2590 | "zvariant", 2591 | ] 2592 | 2593 | [[package]] 2594 | name = "zbus_macros" 2595 | version = "4.1.2" 2596 | source = "registry+https://github.com/rust-lang/crates.io-index" 2597 | checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0" 2598 | dependencies = [ 2599 | "proc-macro-crate", 2600 | "proc-macro2", 2601 | "quote", 2602 | "regex", 2603 | "syn 1.0.109", 2604 | "zvariant_utils", 2605 | ] 2606 | 2607 | [[package]] 2608 | name = "zbus_names" 2609 | version = "3.0.0" 2610 | source = "registry+https://github.com/rust-lang/crates.io-index" 2611 | checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" 2612 | dependencies = [ 2613 | "serde", 2614 | "static_assertions", 2615 | "zvariant", 2616 | ] 2617 | 2618 | [[package]] 2619 | name = "zerocopy" 2620 | version = "0.7.32" 2621 | source = "registry+https://github.com/rust-lang/crates.io-index" 2622 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 2623 | dependencies = [ 2624 | "zerocopy-derive", 2625 | ] 2626 | 2627 | [[package]] 2628 | name = "zerocopy-derive" 2629 | version = "0.7.32" 2630 | source = "registry+https://github.com/rust-lang/crates.io-index" 2631 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 2632 | dependencies = [ 2633 | "proc-macro2", 2634 | "quote", 2635 | "syn 2.0.60", 2636 | ] 2637 | 2638 | [[package]] 2639 | name = "zvariant" 2640 | version = "4.0.2" 2641 | source = "registry+https://github.com/rust-lang/crates.io-index" 2642 | checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" 2643 | dependencies = [ 2644 | "endi", 2645 | "enumflags2", 2646 | "serde", 2647 | "static_assertions", 2648 | "zvariant_derive", 2649 | ] 2650 | 2651 | [[package]] 2652 | name = "zvariant_derive" 2653 | version = "4.0.2" 2654 | source = "registry+https://github.com/rust-lang/crates.io-index" 2655 | checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" 2656 | dependencies = [ 2657 | "proc-macro-crate", 2658 | "proc-macro2", 2659 | "quote", 2660 | "syn 1.0.109", 2661 | "zvariant_utils", 2662 | ] 2663 | 2664 | [[package]] 2665 | name = "zvariant_utils" 2666 | version = "1.1.0" 2667 | source = "registry+https://github.com/rust-lang/crates.io-index" 2668 | checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" 2669 | dependencies = [ 2670 | "proc-macro2", 2671 | "quote", 2672 | "syn 1.0.109", 2673 | ] 2674 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "systemctl-tui" 3 | description = "A simple TUI for interacting with systemd services and their logs" 4 | homepage = "https://github.com/rgwood/systemctl-tui" 5 | repository = "https://github.com/rgwood/systemctl-tui" 6 | version = "0.4.0" 7 | edition = "2021" 8 | authors = ["Reilly Wood"] 9 | license = "MIT" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | ratatui = {version = "0.28.0"} 15 | crossterm = { version = "0.28.1", default-features = false, features = [ 16 | "event-stream", 17 | ] } 18 | tokio = { version = "1.28.2", features = ["full"] } 19 | tokio-stream = "0.1.14" 20 | unicode-segmentation = "1.10.1" 21 | anyhow = "1.0.71" 22 | better-panic = "0.3.0" 23 | clap = { version = "4.3.4", default-features = false, features = [ 24 | "std", 25 | "color", 26 | "help", 27 | "usage", 28 | "error-context", 29 | "suggestions", 30 | "derive", 31 | "cargo", 32 | "wrap_help", 33 | "unicode", 34 | "string", 35 | "unstable-styles", 36 | ] } 37 | clap_complete = "4.3.1" 38 | futures = "0.3.28" 39 | tracing-macros = "0.0.0" 40 | tracing = "0.1.37" 41 | tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 42 | env_logger = "0.11.1" 43 | directories = "5.0.1" 44 | tui-logger = { version = "0.12.0", default-features = false, features = [ 45 | "tracing-support", 46 | ] } 47 | colored = "2.0.0" 48 | log = "0.4.19" 49 | libc = "0.2.146" 50 | tui-input = "0.10.0" 51 | signal-hook = "0.3.15" 52 | tokio-util = "0.7.8" 53 | zbus = { version = "4.1.2", default-features = false, features = ["tokio"] } 54 | itertools = "0.12.0" 55 | indexmap = "2.0.0" 56 | clipboard-anywhere = "0.2.2" 57 | chrono = { version = "0.4.31", default-features = false } 58 | lazy_static = "1.4.0" 59 | nix = { version = "0.28.0", features = ["user"] } 60 | is-wsl = "0.4.0" 61 | tracing-appender = "0.2.3" 62 | 63 | # build with `cargo build --profile profiling` 64 | # to analyze performance with tooling like perf / samply / superluminal 65 | [profile.profiling] 66 | inherits = "release" 67 | strip = false 68 | debug = true 69 | 70 | [profile.release] 71 | lto = true # Enable Link Time Optimization (slow but makes a huge size difference) 72 | opt-level = 'z' # Optimize for size. 73 | panic = 'abort' # Abort on panic 74 | # strip = true # Strip symbols from binary. Big gains but idk if it's worth bad stack traces 75 | 76 | [[bin]] 77 | name = "systemctl-tui" 78 | path = "src/main.rs" 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Reilly Wood 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # systemctl-tui 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/systemctl-tui.svg)](https://crates.io/crates/systemctl-tui) 4 | 5 | A fast, simple TUI for interacting with [systemd](https://en.wikipedia.org/wiki/Systemd) services and their logs. 6 | ![Screenshot from 2025-01-23 21-44-31](https://github.com/user-attachments/assets/caac6034-d4e3-4c54-8163-24a8a6d39cb4) 7 | 8 | `systemctl-tui` can quickly browse service status and logs, start/stop/restart/reload services, and view/edit unit files. It aims to do a small number of things well. 9 | 10 | ## Install 11 | 12 | Note: this project only works on Linux (WSL works _if_ you [have systemd enabled](https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/)). Binaries are published for x64 and ARM64 in the [GitHub releases](https://github.com/rgwood/systemctl-tui/releases), and [distro packages](#distro-packages) are available. 13 | 14 | ### Binary Release 15 | 16 | Automated install/update (don't forget to always verify what you're piping into bash): 17 | 18 | ```sh 19 | curl https://raw.githubusercontent.com/rgwood/systemctl-tui/master/install.sh | bash 20 | ``` 21 | The script installs downloaded binary to `$HOME/.local/bin` directory by default, but it can be changed by setting `DIR` environment variable. 22 | 23 | ### Rust 24 | 25 | If you'd rather build from scratch you will need [Rust installed](https://rustup.rs/). Then either: 26 | 27 | 1. Run `cargo install systemctl-tui --locked` 28 | 2. Clone the repo and run `cargo build --release` to get a release binary at `target/release/systemctl-tui` 29 | 30 | ### Distro Packages 31 | 32 |
33 | Packaging status 34 | 35 | [![Packaging status](https://repology.org/badge/vertical-allrepos/systemctl-tui.svg)](https://repology.org/project/systemctl-tui/versions) 36 | 37 |
38 | 39 | #### Arch Linux 40 | 41 | `systemctl-tui` can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/systemctl-tui/): 42 | 43 | ```sh 44 | pacman -S systemctl-tui 45 | ``` 46 | 47 | #### Nix 48 | 49 | [A Nix package](https://search.nixos.org/packages?query=systemctl-tui) is available and can be installed as follows: 50 | 51 | ```sh 52 | nix-shell -p systemctl-tui 53 | ``` 54 | 55 | #### Optional: 56 | 57 | 1. Alias `systemctl-tui` to `st` for quick access 58 | 2. Create a symlink so `systemctl-tui` can be used with sudo: 59 | 60 | ```sh 61 | sudo ln -s ~/.cargo/bin/systemctl-tui /usr/bin/systemctl-tui 62 | ``` 63 | 64 | ## Help 65 | ![image](https://github.com/rgwood/systemctl-tui/assets/26268125/b1b49850-61c4-4667-9110-20a34f917055) 66 | 67 | ## Credits 68 | 69 | - Inspired by the truly wonderful [Lazygit](https://github.com/jesseduffield/lazygit) 70 | - [`sysz`](https://github.com/joehillen/sysz) is so cool 71 | - Used [`ratatui-template`](https://github.com/kdheepak/ratatui-template/) to get started 72 | - systemd code partially taken from [`servicer`](https://github.com/servicer-labs/servicer) 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | - [ ] show PID of services 4 | - [ ] show memory and maybe CPU use of services 5 | - [ ] unit files 6 | - [ ] command to open unit file in text editor 7 | - [x] figure out path to unit file 8 | - [x] command to copy unit file path to clipboard 9 | - [x] Overhaul rendering for lower CPU usage 10 | - [x] Figure out inconsistent dev compile times. Sometimes 1s, sometimes 17s 11 | - [x] Finish the work of updating services. Add new ones, delete no longer present ones 12 | - [x] Use indexmap to speed up updating services 13 | - [x] Fix jank where service refresh changes scroll position in services list 14 | - [x] show substate in parens like `Active (Running)` 15 | - [x] use journalctl -f to follow logs for instant refresh 16 | - [x] display error (like when start/stop fails) 17 | - [x] display spinner while starting up service 18 | - [x] generalize spinner logic to all actions 19 | - [x] refresh logs on a timer 20 | - [x] refresh services on a timer 21 | - [x] put on crates.io 22 | - [x] Implement scrolling with pgup/pgdown 23 | - [x] try adding a modal help menu/command picker like x/? in lazygit 24 | - [x] when searching, auto-select the first result 25 | - [x] select first item by default 26 | - [x] add color for stopped/running status 27 | - [x] add some color (for dates maybe?) 28 | - [x] ctrl-f for find 29 | - [x] move logs to their own pane 30 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # allow specifying different destination directory 4 | DIR="${DIR:-"$HOME/.local/bin"}" 5 | 6 | # map different architecture variations to the available binaries 7 | ARCH=$(uname -m) 8 | case $ARCH in 9 | i386|i686) ARCH=x86 ;; 10 | aarch64*) ARCH=arm64 ;; 11 | esac 12 | 13 | # prepare the download URL 14 | GITHUB_LATEST_VERSION=$(curl -L -s -H 'Accept: application/json' https://github.com/rgwood/systemctl-tui/releases/latest | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') 15 | GITHUB_FILE="systemctl-tui-${ARCH}-unknown-linux-musl.tar.gz" 16 | GITHUB_URL="https://github.com/rgwood/systemctl-tui/releases/download/${GITHUB_LATEST_VERSION}/${GITHUB_FILE}" 17 | 18 | # install/update the local binary 19 | curl -L -o systemctl-tui.tar.gz $GITHUB_URL 20 | tar xzvf systemctl-tui.tar.gz systemctl-tui 21 | install -Dm 755 systemctl-tui -t "$DIR" 22 | rm systemctl-tui systemctl-tui.tar.gz 23 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set shell := ["nu", "-c"] 2 | 3 | default: 4 | @just --list 5 | 6 | watch: 7 | watchexec --exts=rs --on-busy-update=restart -- cargo run 8 | 9 | run: 10 | cargo run 11 | 12 | test: 13 | cargo test 14 | 15 | watch-tests: 16 | watchexec --exts=rs -- cargo test 17 | 18 | expected_filename := "systemctl-tui" 19 | 20 | build-release: 21 | cargo build --release 22 | @$"Build size: (ls target/release/{{expected_filename}} | get size)" 23 | 24 | publish-to-local-bin: build-release 25 | cp target/release/{{expected_filename}} ~/bin/ 26 | 27 | build-linux-x64: 28 | cross build --target x86_64-unknown-linux-musl --release 29 | 30 | build-linux-arm64: 31 | cross build --target aarch64-unknown-linux-musl --release 32 | 33 | build-windows-on-linux: 34 | cross build --target x86_64-pc-windows-gnu --release 35 | 36 | publish-potato-pi: build-linux-x64 37 | rsync target/x86_64-unknown-linux-musl/release/systemctl-tui potato-pi:~/bin/ 38 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | use_small_heuristics = "Max" 3 | match_block_trailing_comma = true 4 | tab_spaces = 2 5 | use_field_init_shorthand = true 6 | use_try_shorthand = true 7 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | components::home::Mode, 3 | systemd::{UnitId, UnitWithStatus}, 4 | }; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum Action { 8 | Quit, 9 | Resume, 10 | Suspend, 11 | Render, 12 | DebouncedRender, 13 | SpinnerTick, 14 | Resize(u16, u16), 15 | ToggleShowLogger, 16 | RefreshServices, 17 | SetServices(Vec), 18 | EnterMode(Mode), 19 | EnterError(String), 20 | CancelTask, 21 | ToggleHelp, 22 | SetUnitFilePath { unit: UnitId, path: Result }, 23 | CopyUnitFilePath, 24 | SetLogs { unit: UnitId, logs: Vec }, 25 | AppendLogLine { unit: UnitId, line: String }, 26 | StartService(UnitId), 27 | StopService(UnitId), 28 | RestartService(UnitId), 29 | ReloadService(UnitId), 30 | EnableService(UnitId), 31 | DisableService(UnitId), 32 | ScrollUp(u16), 33 | ScrollDown(u16), 34 | ScrollToTop, 35 | ScrollToBottom, 36 | EditUnitFile { unit: UnitId, path: String }, 37 | Noop, 38 | } 39 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{process::Command, sync::Arc}; 2 | 3 | use anyhow::{Context, Result}; 4 | use log::error; 5 | use tokio::sync::{mpsc, Mutex}; 6 | use tracing::debug; 7 | 8 | use crate::{ 9 | action::Action, 10 | components::{ 11 | home::{Home, Mode}, 12 | Component, 13 | }, 14 | event::EventHandler, 15 | systemd::{get_all_services, Scope}, 16 | terminal::TerminalHandler, 17 | }; 18 | 19 | pub struct App { 20 | pub scope: Scope, 21 | pub home: Arc>, 22 | pub limit_units: Vec, 23 | pub should_quit: bool, 24 | pub should_suspend: bool, 25 | } 26 | 27 | impl App { 28 | pub fn new(scope: Scope, limit_units: Vec) -> Result { 29 | let home = Home::new(scope, &limit_units); 30 | let home = Arc::new(Mutex::new(home)); 31 | Ok(Self { scope, home, limit_units, should_quit: false, should_suspend: false }) 32 | } 33 | 34 | pub async fn run(&mut self) -> Result<()> { 35 | let (action_tx, mut action_rx) = mpsc::unbounded_channel(); 36 | 37 | let (debounce_tx, mut debounce_rx) = mpsc::unbounded_channel(); 38 | 39 | let cloned_action_tx = action_tx.clone(); 40 | tokio::spawn(async move { 41 | let debounce_duration = std::time::Duration::from_millis(0); 42 | let debouncing = Arc::new(Mutex::new(false)); 43 | 44 | loop { 45 | let _ = debounce_rx.recv().await; 46 | 47 | if *debouncing.lock().await { 48 | continue; 49 | } 50 | 51 | *debouncing.lock().await = true; 52 | 53 | let action_tx = cloned_action_tx.clone(); 54 | let debouncing = debouncing.clone(); 55 | tokio::spawn(async move { 56 | tokio::time::sleep(debounce_duration).await; 57 | let _ = action_tx.send(Action::Render); 58 | *debouncing.lock().await = false; 59 | }); 60 | } 61 | }); 62 | 63 | self.home.lock().await.init(action_tx.clone())?; 64 | 65 | let units = get_all_services(self.scope, &self.limit_units) 66 | .await 67 | .context("Unable to get services. Check that systemd is running and try running this tool with sudo.")?; 68 | self.home.lock().await.set_units(units); 69 | 70 | let mut terminal = TerminalHandler::new(self.home.clone()); 71 | let mut event = EventHandler::new(self.home.clone(), action_tx.clone()); 72 | 73 | terminal.render().await; 74 | 75 | loop { 76 | if let Some(action) = action_rx.recv().await { 77 | match &action { 78 | // these are too big to log in full 79 | Action::SetLogs { .. } => debug!("action: SetLogs"), 80 | Action::SetServices { .. } => debug!("action: SetServices"), 81 | _ => debug!("action: {:?}", action), 82 | } 83 | 84 | match action { 85 | Action::Render => { 86 | let start = std::time::Instant::now(); 87 | terminal.render().await; 88 | let duration = start.elapsed(); 89 | crate::utils::log_perf_event("render", duration); 90 | }, 91 | Action::DebouncedRender => debounce_tx.send(Action::Render).unwrap(), 92 | Action::Noop => {}, 93 | Action::Quit => self.should_quit = true, 94 | Action::Suspend => self.should_suspend = true, 95 | Action::Resume => self.should_suspend = false, 96 | Action::Resize(_, _) => terminal.render().await, 97 | // This would normally be in home.rs, but it needs to do some terminal and event handling stuff that's easier here 98 | Action::EditUnitFile { unit, path } => { 99 | event.stop(); 100 | let mut tui = terminal.tui.lock().await; 101 | tui.exit()?; 102 | 103 | let read_unit_file_contents = || match std::fs::read_to_string(&path) { 104 | Ok(contents) => contents, 105 | Err(e) => { 106 | error!("Failed to read unit file `{}`: {}", path, e); 107 | "".to_string() 108 | }, 109 | }; 110 | 111 | let unit_file_contents = read_unit_file_contents(); 112 | let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); 113 | match Command::new(&editor).arg(&path).status() { 114 | Ok(_) => { 115 | tui.enter()?; 116 | tui.clear()?; 117 | event = EventHandler::new(self.home.clone(), action_tx.clone()); 118 | 119 | let new_unit_file_contents = read_unit_file_contents(); 120 | if unit_file_contents != new_unit_file_contents { 121 | action_tx.send(Action::ReloadService(unit))?; 122 | } 123 | 124 | action_tx.send(Action::EnterMode(Mode::ServiceList))?; 125 | }, 126 | Err(e) => { 127 | tui.enter()?; 128 | tui.clear()?; 129 | event = EventHandler::new(self.home.clone(), action_tx.clone()); 130 | action_tx.send(Action::EnterError(format!("Failed to open editor `{}`: {}", editor, e)))?; 131 | }, 132 | } 133 | }, 134 | _ => { 135 | if let Some(_action) = self.home.lock().await.dispatch(action) { 136 | action_tx.send(_action)? 137 | }; 138 | }, 139 | } 140 | } 141 | if self.should_suspend { 142 | terminal.suspend()?; 143 | event.stop(); 144 | terminal.task.await?; 145 | event.task.await?; 146 | terminal = TerminalHandler::new(self.home.clone()); 147 | event = EventHandler::new(self.home.clone(), action_tx.clone()); 148 | action_tx.send(Action::Resume)?; 149 | action_tx.send(Action::Render)?; 150 | } else if self.should_quit { 151 | terminal.stop()?; 152 | event.stop(); 153 | terminal.task.await?; 154 | event.task.await?; 155 | break; 156 | } 157 | } 158 | Ok(()) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/components/home.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 2 | use futures::Future; 3 | use indexmap::IndexMap; 4 | use itertools::Itertools; 5 | use ratatui::{ 6 | layout::{Constraint, Direction, Layout, Rect}, 7 | style::{Color, Modifier, Style}, 8 | text::{Line, Span}, 9 | widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, 10 | }; 11 | use tokio::{ 12 | io::AsyncBufReadExt, 13 | sync::mpsc::{self, UnboundedSender}, 14 | task::JoinHandle, 15 | }; 16 | use tokio_util::sync::CancellationToken; 17 | use tracing::{error, info, warn}; 18 | use tui_input::{backend::crossterm::EventHandler, Input}; 19 | 20 | use std::{ 21 | process::{Command, Stdio}, 22 | time::Duration, 23 | }; 24 | 25 | use super::{logger::Logger, Component, Frame}; 26 | use crate::{ 27 | action::Action, 28 | systemd::{self, Scope, UnitId, UnitScope, UnitWithStatus}, 29 | }; 30 | 31 | #[derive(Debug, Default, Copy, Clone, PartialEq)] 32 | pub enum Mode { 33 | #[default] 34 | Search, 35 | ServiceList, 36 | Help, 37 | ActionMenu, 38 | Processing, 39 | Error, 40 | } 41 | 42 | #[derive(Default)] 43 | pub struct Home { 44 | pub scope: Scope, 45 | pub limit_units: Vec, 46 | pub logger: Logger, 47 | pub show_logger: bool, 48 | pub all_units: IndexMap, 49 | pub filtered_units: StatefulList, 50 | pub logs: Vec, 51 | pub logs_scroll_offset: u16, 52 | pub mode: Mode, 53 | pub previous_mode: Option, 54 | pub input: Input, 55 | pub menu_items: StatefulList, 56 | pub cancel_token: Option, 57 | pub spinner_tick: u8, 58 | pub error_message: String, 59 | pub action_tx: Option>, 60 | pub journalctl_tx: Option>, 61 | } 62 | 63 | pub struct MenuItem { 64 | pub name: String, 65 | pub action: Action, 66 | pub key: Option, 67 | } 68 | 69 | impl MenuItem { 70 | pub fn new(name: &str, action: Action, key: Option) -> Self { 71 | Self { name: name.to_owned(), action, key } 72 | } 73 | 74 | pub fn key_string(&self) -> String { 75 | if let Some(key) = self.key { 76 | format!("{}", key) 77 | } else { 78 | String::new() 79 | } 80 | } 81 | } 82 | 83 | pub struct StatefulList { 84 | state: ListState, 85 | items: Vec, 86 | } 87 | 88 | impl Default for StatefulList { 89 | fn default() -> Self { 90 | Self::with_items(vec![]) 91 | } 92 | } 93 | 94 | impl StatefulList { 95 | pub fn with_items(items: Vec) -> StatefulList { 96 | StatefulList { state: ListState::default(), items } 97 | } 98 | 99 | #[allow(dead_code)] 100 | fn selected_mut(&mut self) -> Option<&mut T> { 101 | if self.items.is_empty() { 102 | return None; 103 | } 104 | match self.state.selected() { 105 | Some(i) => Some(&mut self.items[i]), 106 | None => None, 107 | } 108 | } 109 | 110 | fn selected(&self) -> Option<&T> { 111 | if self.items.is_empty() { 112 | return None; 113 | } 114 | match self.state.selected() { 115 | Some(i) => Some(&self.items[i]), 116 | None => None, 117 | } 118 | } 119 | 120 | fn next(&mut self) { 121 | let i = match self.state.selected() { 122 | Some(i) => { 123 | if i >= self.items.len().saturating_sub(1) { 124 | 0 125 | } else { 126 | i + 1 127 | } 128 | }, 129 | None => 0, 130 | }; 131 | self.state.select(Some(i)); 132 | } 133 | 134 | fn previous(&mut self) { 135 | let i = match self.state.selected() { 136 | Some(i) => { 137 | if i == 0 { 138 | self.items.len() - 1 139 | } else { 140 | i - 1 141 | } 142 | }, 143 | None => 0, 144 | }; 145 | self.state.select(Some(i)); 146 | } 147 | 148 | fn select(&mut self, index: Option) { 149 | self.state.select(index); 150 | } 151 | 152 | fn unselect(&mut self) { 153 | self.state.select(None); 154 | } 155 | } 156 | 157 | impl Home { 158 | pub fn new(scope: Scope, limit_units: &[String]) -> Self { 159 | let limit_units = limit_units.to_vec(); 160 | Self { scope, limit_units, ..Default::default() } 161 | } 162 | 163 | pub fn set_units(&mut self, units: Vec) { 164 | self.all_units.clear(); 165 | for unit_status in units.into_iter() { 166 | self.all_units.insert(unit_status.id(), unit_status); 167 | } 168 | self.refresh_filtered_units(); 169 | } 170 | 171 | // Update units in-place, then filter the list 172 | // This is inefficient but it's fast enough 173 | // (on gen 13 i7: ~100 microseconds to update, ~100 microseconds to filter) 174 | // revisit if needed 175 | pub fn update_units(&mut self, units: Vec) { 176 | let now = std::time::Instant::now(); 177 | 178 | for unit in units { 179 | if let Some(existing) = self.all_units.get_mut(&unit.id()) { 180 | existing.update(unit); 181 | } else { 182 | self.all_units.insert(unit.id(), unit); 183 | } 184 | } 185 | info!("Updated units in {:?}", now.elapsed()); 186 | 187 | let now = std::time::Instant::now(); 188 | self.refresh_filtered_units(); 189 | info!("Filtered units in {:?}", now.elapsed()); 190 | } 191 | 192 | pub fn next(&mut self) { 193 | self.logs = vec![]; 194 | self.filtered_units.next(); 195 | self.get_logs(); 196 | self.logs_scroll_offset = 0; 197 | } 198 | 199 | pub fn previous(&mut self) { 200 | self.logs = vec![]; 201 | self.filtered_units.previous(); 202 | self.get_logs(); 203 | self.logs_scroll_offset = 0; 204 | } 205 | 206 | pub fn select(&mut self, index: Option, refresh_logs: bool) { 207 | if refresh_logs { 208 | self.logs = vec![]; 209 | } 210 | self.filtered_units.select(index); 211 | if refresh_logs { 212 | self.get_logs(); 213 | self.logs_scroll_offset = 0; 214 | } 215 | } 216 | 217 | pub fn unselect(&mut self) { 218 | self.logs = vec![]; 219 | self.filtered_units.unselect(); 220 | } 221 | 222 | pub fn selected_service(&self) -> Option { 223 | self.filtered_units.selected().map(|u| u.id()) 224 | } 225 | 226 | pub fn get_logs(&mut self) { 227 | if let Some(selected) = self.filtered_units.selected() { 228 | let unit_id = selected.id(); 229 | if let Err(e) = self.journalctl_tx.as_ref().unwrap().send(unit_id) { 230 | warn!("Error sending unit name to journalctl thread: {}", e); 231 | } 232 | } else { 233 | self.logs = vec![]; 234 | } 235 | } 236 | 237 | fn refresh_filtered_units(&mut self) { 238 | let previously_selected = self.selected_service(); 239 | let search_value_lower = self.input.value().to_lowercase(); 240 | // TODO: use fuzzy find 241 | let matching = self 242 | .all_units 243 | .values() 244 | .filter(|u| u.short_name().to_lowercase().contains(&search_value_lower)) 245 | .cloned() 246 | .collect_vec(); 247 | self.filtered_units.items = matching; 248 | 249 | // try to select the same item we had selected before 250 | // TODO: this is horrible, clean it up 251 | if let Some(previously_selected) = previously_selected { 252 | if let Some(index) = self 253 | .filtered_units 254 | .items 255 | .iter() 256 | .position(|u| u.name == previously_selected.name && u.scope == previously_selected.scope) 257 | { 258 | self.select(Some(index), false); 259 | } else { 260 | self.select(Some(0), true); 261 | } 262 | } else { 263 | // if we can't, select the first item in the list 264 | if !self.filtered_units.items.is_empty() { 265 | self.select(Some(0), true); 266 | } else { 267 | self.unselect(); 268 | } 269 | } 270 | } 271 | 272 | fn start_service(&mut self, service: UnitId) { 273 | let cancel_token = CancellationToken::new(); 274 | let future = systemd::start_service(service.clone(), cancel_token.clone()); 275 | self.service_action(service, "Start".into(), cancel_token, future); 276 | } 277 | 278 | fn stop_service(&mut self, service: UnitId) { 279 | let cancel_token = CancellationToken::new(); 280 | let future = systemd::stop_service(service.clone(), cancel_token.clone()); 281 | self.service_action(service, "Stop".into(), cancel_token, future); 282 | } 283 | 284 | fn reload_service(&mut self, service: UnitId) { 285 | let cancel_token = CancellationToken::new(); 286 | let future = systemd::reload(service.scope, cancel_token.clone()); 287 | self.service_action(service, "Reload".into(), cancel_token, future); 288 | } 289 | 290 | fn restart_service(&mut self, service: UnitId) { 291 | let cancel_token = CancellationToken::new(); 292 | let future = systemd::restart_service(service.clone(), cancel_token.clone()); 293 | self.service_action(service, "Restart".into(), cancel_token, future); 294 | } 295 | 296 | fn service_action(&mut self, service: UnitId, action_name: String, cancel_token: CancellationToken, action: Fut) 297 | where 298 | Fut: Future> + Send + 'static, 299 | { 300 | let tx = self.action_tx.clone().unwrap(); 301 | 302 | self.cancel_token = Some(cancel_token.clone()); 303 | 304 | let tx_clone = tx.clone(); 305 | let spinner_task = tokio::spawn(async move { 306 | let mut interval = tokio::time::interval(Duration::from_millis(200)); 307 | loop { 308 | interval.tick().await; 309 | tx_clone.send(Action::SpinnerTick).unwrap(); 310 | } 311 | }); 312 | 313 | tokio::spawn(async move { 314 | tx.send(Action::EnterMode(Mode::Processing)).unwrap(); 315 | match action.await { 316 | Ok(_) => { 317 | info!("{} of {:?} service {} succeeded", action_name, service.scope, service.name); 318 | tx.send(Action::EnterMode(Mode::ServiceList)).unwrap(); 319 | }, 320 | // would be nicer to check the error type here, but this is easier 321 | Err(_) if cancel_token.is_cancelled() => { 322 | warn!("{} of {:?} service {} was cancelled", action_name, service.scope, service.name) 323 | }, 324 | Err(e) => { 325 | error!("{} of {:?} service {} failed: {}", action_name, service.scope, service.name, e); 326 | let mut error_string = e.to_string(); 327 | 328 | if error_string.contains("AccessDenied") { 329 | error_string.push('\n'); 330 | error_string.push('\n'); 331 | error_string.push_str("Try running this tool with sudo."); 332 | } 333 | 334 | tx.send(Action::EnterError(error_string)).unwrap(); 335 | }, 336 | } 337 | spinner_task.abort(); 338 | tx.send(Action::RefreshServices).unwrap(); 339 | 340 | // Refresh a bit more frequently after a service action 341 | for _ in 0..3 { 342 | tokio::time::sleep(Duration::from_secs(1)).await; 343 | tx.send(Action::RefreshServices).unwrap(); 344 | } 345 | }); 346 | } 347 | } 348 | 349 | impl Component for Home { 350 | fn init(&mut self, tx: UnboundedSender) -> anyhow::Result<()> { 351 | self.action_tx = Some(tx.clone()); 352 | // TODO find a better name for these. They're used to run any async data loading that needs to happen after the selection is changed, 353 | // not just journalctl stuff 354 | let (journalctl_tx, journalctl_rx) = std::sync::mpsc::channel::(); 355 | self.journalctl_tx = Some(journalctl_tx); 356 | 357 | // TODO: move into function 358 | tokio::task::spawn_blocking(move || { 359 | let mut last_follow_handle: Option> = None; 360 | 361 | loop { 362 | let mut unit: UnitId = match journalctl_rx.recv() { 363 | Ok(unit) => unit, 364 | Err(_) => return, 365 | }; 366 | 367 | // drain the channel, use the last value 368 | while let Ok(service) = journalctl_rx.try_recv() { 369 | info!("Skipping logs for {}...", unit.name); 370 | unit = service; 371 | } 372 | 373 | if let Some(handle) = last_follow_handle.take() { 374 | info!("Cancelling previous journalctl task"); 375 | handle.abort(); 376 | } 377 | 378 | // lazy debounce to avoid spamming journalctl on slow connections/systems 379 | std::thread::sleep(Duration::from_millis(100)); 380 | 381 | // get the unit file path 382 | match systemd::get_unit_file_location(&unit) { 383 | Ok(path) => { 384 | let _ = tx.send(Action::SetUnitFilePath { unit: unit.clone(), path: Ok(path) }); 385 | let _ = tx.send(Action::Render); 386 | }, 387 | Err(e) => { 388 | // Fix this!!! Set the path to an error enum variant instead of a string 389 | let _ = 390 | tx.send(Action::SetUnitFilePath { unit: unit.clone(), path: Err("could not be determined".into()) }); 391 | let _ = tx.send(Action::Render); 392 | error!("Error getting unit file path for {}: {}", unit.name, e); 393 | }, 394 | } 395 | 396 | // First, get the N lines in a batch 397 | info!("Getting logs for {}", unit.name); 398 | let start = std::time::Instant::now(); 399 | 400 | let mut args = vec!["--quiet", "--output=short-iso", "--lines=500", "-u"]; 401 | 402 | args.push(&unit.name); 403 | 404 | if unit.scope == UnitScope::User { 405 | args.push("--user"); 406 | } 407 | 408 | match Command::new("journalctl").args(&args).output() { 409 | Ok(output) => { 410 | if output.status.success() { 411 | info!("Got logs for {} in {:?}", unit.name, start.elapsed()); 412 | if let Ok(stdout) = std::str::from_utf8(&output.stdout) { 413 | let mut logs = stdout.trim().split('\n').map(String::from).collect_vec(); 414 | 415 | if logs.is_empty() || logs[0].is_empty() { 416 | logs.push(String::from("No logs found/available. Maybe try relaunching with `sudo systemctl-tui`")); 417 | } 418 | let _ = tx.send(Action::SetLogs { unit: unit.clone(), logs }); 419 | let _ = tx.send(Action::Render); 420 | } else { 421 | warn!("Error parsing stdout for {}", unit.name); 422 | } 423 | } else { 424 | warn!("Error getting logs for {}: {}", unit.name, String::from_utf8_lossy(&output.stderr)); 425 | } 426 | }, 427 | Err(e) => warn!("Error getting logs for {}: {}", unit.name, e), 428 | } 429 | 430 | // Then follow the logs 431 | // Splitting this into two commands is a bit of a hack that makes it easier to get the initial batch of logs 432 | // This does mean that we'll miss any logs that are written between the two commands, low enough risk for now 433 | let tx = tx.clone(); 434 | last_follow_handle = Some(tokio::spawn(async move { 435 | let mut command = tokio::process::Command::new("journalctl"); 436 | command.arg("-u"); 437 | command.arg(unit.name.clone()); 438 | command.arg("--output=short-iso"); 439 | command.arg("--follow"); 440 | command.arg("--lines=0"); 441 | command.arg("--quiet"); 442 | command.stdout(Stdio::piped()); 443 | command.stderr(Stdio::piped()); 444 | 445 | if unit.scope == UnitScope::User { 446 | command.arg("--user"); 447 | } 448 | 449 | let mut child = command.spawn().expect("failed to execute process"); 450 | 451 | let stdout = child.stdout.take().unwrap(); 452 | 453 | let reader = tokio::io::BufReader::new(stdout); 454 | let mut lines = reader.lines(); 455 | while let Some(line) = lines.next_line().await.unwrap() { 456 | let _ = tx.send(Action::AppendLogLine { unit: unit.clone(), line }); 457 | let _ = tx.send(Action::Render); 458 | } 459 | })); 460 | } 461 | }); 462 | Ok(()) 463 | } 464 | 465 | fn handle_key_events(&mut self, key: KeyEvent) -> Vec { 466 | if key.modifiers.contains(KeyModifiers::CONTROL) { 467 | match key.code { 468 | KeyCode::Char('c') => return vec![Action::Quit], 469 | KeyCode::Char('q') => return vec![Action::Quit], 470 | KeyCode::Char('z') => return vec![Action::Suspend], 471 | KeyCode::Char('f') => return vec![Action::EnterMode(Mode::Search)], 472 | KeyCode::Char('l') => return vec![Action::ToggleShowLogger], 473 | // vim keybindings, apparently 474 | KeyCode::Char('d') => return vec![Action::ScrollDown(1), Action::Render], 475 | KeyCode::Char('u') => return vec![Action::ScrollUp(1), Action::Render], 476 | _ => (), 477 | } 478 | } 479 | 480 | if matches!(key.code, KeyCode::Char('?')) || matches!(key.code, KeyCode::F(1)) { 481 | return vec![Action::ToggleHelp, Action::Render]; 482 | } 483 | 484 | // TODO: seems like terminals can't recognize shift or ctrl at the same time as page up/down 485 | // Is there another way we could scroll in large increments? 486 | match key.code { 487 | KeyCode::PageDown => return vec![Action::ScrollDown(1), Action::Render], 488 | KeyCode::PageUp => return vec![Action::ScrollUp(1), Action::Render], 489 | KeyCode::Home => return vec![Action::ScrollToTop, Action::Render], 490 | KeyCode::End => return vec![Action::ScrollToBottom, Action::Render], 491 | _ => (), 492 | } 493 | 494 | match self.mode { 495 | Mode::ServiceList => { 496 | match key.code { 497 | KeyCode::Char('q') => vec![Action::Quit], 498 | KeyCode::Up | KeyCode::Char('k') => { 499 | // if we're filtering the list, and we're at the top, and there's text in the search box, go to search mode 500 | if self.filtered_units.state.selected() == Some(0) { 501 | return vec![Action::EnterMode(Mode::Search)]; 502 | } 503 | 504 | self.previous(); 505 | vec![Action::Render] 506 | }, 507 | KeyCode::Down | KeyCode::Char('j') => { 508 | self.next(); 509 | vec![Action::Render] 510 | }, 511 | KeyCode::Char('/') => vec![Action::EnterMode(Mode::Search)], 512 | KeyCode::Char('e') => { 513 | if let Some(selected) = self.filtered_units.selected() { 514 | if let Some(Ok(file_path)) = &selected.file_path { 515 | return vec![Action::EditUnitFile { unit: selected.id(), path: file_path.clone() }]; 516 | } 517 | } 518 | vec![] 519 | }, 520 | KeyCode::Enter | KeyCode::Char(' ') => vec![Action::EnterMode(Mode::ActionMenu)], 521 | _ => vec![], 522 | } 523 | }, 524 | Mode::Help => match key.code { 525 | KeyCode::Esc | KeyCode::Enter => vec![Action::ToggleHelp], 526 | _ => vec![], 527 | }, 528 | Mode::Error => match key.code { 529 | KeyCode::Esc | KeyCode::Enter => vec![Action::EnterMode(Mode::ServiceList)], 530 | _ => vec![], 531 | }, 532 | Mode::Search => match key.code { 533 | KeyCode::Esc => vec![Action::EnterMode(Mode::ServiceList)], 534 | KeyCode::Enter => vec![Action::EnterMode(Mode::ActionMenu)], 535 | KeyCode::Down | KeyCode::Tab => { 536 | self.next(); 537 | vec![Action::EnterMode(Mode::ServiceList)] 538 | }, 539 | KeyCode::Up => { 540 | self.previous(); 541 | vec![Action::EnterMode(Mode::ServiceList)] 542 | }, 543 | _ => { 544 | let prev_search_value = self.input.value().to_owned(); 545 | self.input.handle_event(&crossterm::event::Event::Key(key)); 546 | 547 | // if the search value changed, filter the list 548 | if prev_search_value != self.input.value() { 549 | self.refresh_filtered_units(); 550 | } 551 | vec![Action::Render] 552 | }, 553 | }, 554 | Mode::ActionMenu => match key.code { 555 | KeyCode::Esc => vec![Action::EnterMode(Mode::ServiceList)], 556 | KeyCode::Down | KeyCode::Char('j') => { 557 | self.menu_items.next(); 558 | vec![Action::Render] 559 | }, 560 | KeyCode::Up | KeyCode::Char('k') => { 561 | self.menu_items.previous(); 562 | vec![Action::Render] 563 | }, 564 | KeyCode::Enter | KeyCode::Char(' ') => match self.menu_items.selected() { 565 | Some(i) => vec![i.action.clone()], 566 | None => vec![Action::EnterMode(Mode::ServiceList)], 567 | }, 568 | _ => { 569 | for item in self.menu_items.items.iter() { 570 | if let Some(key_code) = item.key { 571 | if key_code == key.code { 572 | return vec![item.action.clone()]; 573 | } 574 | } 575 | } 576 | vec![] 577 | }, 578 | }, 579 | Mode::Processing => match key.code { 580 | KeyCode::Esc => vec![Action::CancelTask], 581 | _ => vec![], 582 | }, 583 | } 584 | } 585 | 586 | fn dispatch(&mut self, action: Action) -> Option { 587 | match action { 588 | Action::ToggleShowLogger => { 589 | self.show_logger = !self.show_logger; 590 | return Some(Action::Render); 591 | }, 592 | Action::EnterMode(mode) => { 593 | if mode == Mode::ActionMenu { 594 | if let Some(selected) = self.filtered_units.selected() { 595 | let mut menu_items = vec![ 596 | MenuItem::new("Start", Action::StartService(selected.id()), Some(KeyCode::Char('s'))), 597 | MenuItem::new("Stop", Action::StopService(selected.id()), Some(KeyCode::Char('t'))), 598 | MenuItem::new("Restart", Action::RestartService(selected.id()), Some(KeyCode::Char('r'))), 599 | MenuItem::new("Reload", Action::ReloadService(selected.id()), Some(KeyCode::Char('l'))), 600 | // TODO add these 601 | // MenuItem::new("Enable", Action::EnableService(selected.clone())), 602 | // MenuItem::new("Disable", Action::DisableService(selected.clone())), 603 | ]; 604 | 605 | if let Some(Ok(file_path)) = &selected.file_path { 606 | menu_items.push(MenuItem::new("Copy unit file path", Action::CopyUnitFilePath, Some(KeyCode::Char('c')))); 607 | menu_items.push(MenuItem::new( 608 | "Edit unit file", 609 | Action::EditUnitFile { unit: selected.id(), path: file_path.clone() }, 610 | Some(KeyCode::Char('e')), 611 | )); 612 | } 613 | 614 | self.menu_items = StatefulList::with_items(menu_items); 615 | self.menu_items.state.select(Some(0)); 616 | } else { 617 | return None; 618 | } 619 | } 620 | 621 | self.mode = mode; 622 | return Some(Action::Render); 623 | }, 624 | Action::EnterError(err) => { 625 | tracing::error!(err); 626 | self.error_message = err; 627 | return Some(Action::EnterMode(Mode::Error)); 628 | }, 629 | Action::ToggleHelp => { 630 | if self.mode != Mode::Help { 631 | self.previous_mode = Some(self.mode); 632 | self.mode = Mode::Help; 633 | } else { 634 | self.mode = self.previous_mode.unwrap_or(Mode::Search); 635 | } 636 | return Some(Action::Render); 637 | }, 638 | Action::CopyUnitFilePath => { 639 | if let Some(selected) = self.filtered_units.selected() { 640 | if let Some(Ok(file_path)) = &selected.file_path { 641 | match clipboard_anywhere::set_clipboard(file_path) { 642 | Ok(_) => return Some(Action::EnterMode(Mode::ServiceList)), 643 | Err(e) => return Some(Action::EnterError(format!("Error copying to clipboard: {}", e))), 644 | } 645 | } else { 646 | return Some(Action::EnterError("No unit file path available".into())); 647 | } 648 | } 649 | }, 650 | Action::SetUnitFilePath { unit, path } => { 651 | if let Some(unit) = self.all_units.get_mut(&unit) { 652 | unit.file_path = Some(path.clone()); 653 | } 654 | self.refresh_filtered_units(); // copy the updated unit file path to the filtered list 655 | }, 656 | Action::SetLogs { unit, logs } => { 657 | if let Some(selected) = self.filtered_units.selected() { 658 | if selected.id() == unit { 659 | self.logs = logs; 660 | } 661 | } 662 | }, 663 | Action::AppendLogLine { unit, line } => { 664 | if let Some(selected) = self.filtered_units.selected() { 665 | if selected.id() == unit { 666 | self.logs.push(line); 667 | } 668 | } 669 | }, 670 | Action::ScrollUp(offset) => { 671 | self.logs_scroll_offset = self.logs_scroll_offset.saturating_sub(offset); 672 | info!("scroll offset: {}", self.logs_scroll_offset); 673 | }, 674 | Action::ScrollDown(offset) => { 675 | self.logs_scroll_offset = self.logs_scroll_offset.saturating_add(offset); 676 | info!("scroll offset: {}", self.logs_scroll_offset); 677 | }, 678 | Action::ScrollToTop => { 679 | self.logs_scroll_offset = 0; 680 | }, 681 | Action::ScrollToBottom => { 682 | // TODO: this is partially broken, figure out a better way to scroll to end 683 | // problem: we don't actually know the height of the paragraph before it's rendered 684 | // because it's wrapped based on the width of the widget 685 | // A proper fix might need to wait until ratatui improves scrolling: https://github.com/ratatui-org/ratatui/issues/174 686 | self.logs_scroll_offset = self.logs.len() as u16; 687 | }, 688 | 689 | Action::StartService(service_name) => self.start_service(service_name), 690 | Action::StopService(service_name) => self.stop_service(service_name), 691 | Action::ReloadService(service_name) => self.reload_service(service_name), 692 | Action::RestartService(service_name) => self.restart_service(service_name), 693 | Action::RefreshServices => { 694 | let tx = self.action_tx.clone().unwrap(); 695 | let scope = self.scope; 696 | let limit_units = self.limit_units.to_vec(); 697 | tokio::spawn(async move { 698 | let units = systemd::get_all_services(scope, &limit_units) 699 | .await 700 | .expect("Failed to get services. Check that systemd is running and try running this tool with sudo."); 701 | tx.send(Action::SetServices(units)).unwrap(); 702 | }); 703 | }, 704 | Action::SetServices(units) => { 705 | self.update_units(units); 706 | return Some(Action::Render); 707 | }, 708 | Action::SpinnerTick => { 709 | self.spinner_tick = self.spinner_tick.wrapping_add(1); 710 | return Some(Action::Render); 711 | }, 712 | Action::CancelTask => { 713 | if let Some(cancel_token) = self.cancel_token.take() { 714 | cancel_token.cancel(); 715 | } 716 | self.mode = Mode::ServiceList; 717 | return Some(Action::Render); 718 | }, 719 | _ => (), 720 | } 721 | None 722 | } 723 | 724 | fn render(&mut self, f: &mut Frame<'_>, rect: Rect) { 725 | fn primary(s: &str) -> Span { 726 | Span::styled(s, Style::default().fg(Color::Cyan)) 727 | } 728 | 729 | fn span(s: &str, color: Color) -> Span { 730 | Span::styled(s, Style::default().fg(color)) 731 | } 732 | 733 | fn colored_line(value: &str, color: Color) -> Line { 734 | Line::from(vec![Span::styled(value, Style::default().fg(color))]) 735 | } 736 | 737 | let rect = if self.show_logger { 738 | let chunks = Layout::new(Direction::Vertical, Constraint::from_percentages([50, 50])).split(rect); 739 | 740 | self.logger.render(f, chunks[1]); 741 | chunks[0] 742 | } else { 743 | rect 744 | }; 745 | 746 | let rects = 747 | Layout::new(Direction::Vertical, [Constraint::Min(3), Constraint::Percentage(100), Constraint::Length(1)]) 748 | .split(rect); 749 | let search_panel = rects[0]; 750 | let main_panel = rects[1]; 751 | let help_line_rect = rects[2]; 752 | 753 | // Helper for colouring based on the same logic as sysz 754 | // https://github.com/joehillen/sysz/blob/8da8e0dcbfde8d68fbdb22382671e395bd370d69/sysz#L69C1-L72C24 755 | // Some units are colored based on state: 756 | // green active 757 | // red failed 758 | // yellow not-found 759 | fn unit_color(unit: &UnitWithStatus) -> Color { 760 | if unit.is_active() { 761 | Color::Green 762 | } else if unit.is_failed() { 763 | Color::Red 764 | } else if unit.is_not_found() { 765 | Color::Yellow 766 | } else { 767 | Color::Reset 768 | } 769 | } 770 | 771 | let items: Vec = self 772 | .filtered_units 773 | .items 774 | .iter() 775 | .map(|i| { 776 | let color = unit_color(i); 777 | let line = colored_line(i.short_name(), color); 778 | ListItem::new(line) 779 | }) 780 | .collect(); 781 | 782 | // Create a List from all list items and highlight the currently selected one 783 | let items = List::new(items) 784 | .block( 785 | Block::default() 786 | .borders(Borders::ALL) 787 | .border_type(BorderType::Rounded) 788 | .border_style(if self.mode == Mode::ServiceList { 789 | Style::default().fg(Color::LightGreen) 790 | } else { 791 | Style::default() 792 | }) 793 | .title("─Services"), 794 | ) 795 | .highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD)); 796 | 797 | let chunks = 798 | Layout::new(Direction::Horizontal, [Constraint::Min(30), Constraint::Percentage(100)]).split(main_panel); 799 | let right_panel = chunks[1]; 800 | 801 | f.render_stateful_widget(items, chunks[0], &mut self.filtered_units.state); 802 | 803 | let selected_item = self.filtered_units.selected(); 804 | 805 | let right_panel = 806 | Layout::new(Direction::Vertical, [Constraint::Min(7), Constraint::Percentage(100)]).split(right_panel); 807 | let details_panel = right_panel[0]; 808 | let logs_panel = right_panel[1]; 809 | 810 | let details_block = Block::default().title("─Details").borders(Borders::ALL).border_type(BorderType::Rounded); 811 | let details_panel_panes = Layout::new(Direction::Horizontal, [Constraint::Min(14), Constraint::Percentage(100)]) 812 | .split(details_block.inner(details_panel)); 813 | let props_pane = details_panel_panes[0]; 814 | let values_pane = details_panel_panes[1]; 815 | 816 | let props_lines = vec![ 817 | Line::from("Description: "), 818 | Line::from("Scope: "), 819 | Line::from("Loaded: "), 820 | Line::from("Active: "), 821 | Line::from("Unit file: "), 822 | ]; 823 | 824 | let details_text = if let Some(i) = selected_item { 825 | fn line_color_string<'a>(value: String, color: Color) -> Line<'a> { 826 | Line::from(vec![Span::styled(value, Style::default().fg(color))]) 827 | } 828 | 829 | let load_color = match i.load_state.as_str() { 830 | "loaded" => Color::Green, 831 | "not-found" => Color::Yellow, 832 | "error" => Color::Red, 833 | _ => Color::Reset, 834 | }; 835 | 836 | let active_color = match i.activation_state.as_str() { 837 | "active" => Color::Green, 838 | "inactive" => Color::Gray, 839 | "failed" => Color::Red, 840 | _ => Color::Reset, 841 | }; 842 | 843 | let active_state_value = format!("{} ({})", i.activation_state, i.sub_state); 844 | 845 | let scope = match i.scope { 846 | UnitScope::Global => "Global", 847 | UnitScope::User => "User", 848 | }; 849 | 850 | let lines = vec![ 851 | colored_line(&i.description, Color::Reset), 852 | colored_line(scope, Color::Reset), 853 | colored_line(&i.load_state, load_color), 854 | line_color_string(active_state_value, active_color), 855 | match &i.file_path { 856 | Some(Ok(file_path)) => Line::from(file_path.as_str()), 857 | Some(Err(e)) => colored_line(e, Color::Red), 858 | None => Line::from(""), 859 | }, 860 | ]; 861 | 862 | lines 863 | } else { 864 | vec![] 865 | }; 866 | 867 | let paragraph = Paragraph::new(details_text).style(Style::default()); 868 | 869 | let props_widget = Paragraph::new(props_lines).alignment(ratatui::layout::Alignment::Right); 870 | f.render_widget(props_widget, props_pane); 871 | 872 | f.render_widget(paragraph, values_pane); 873 | f.render_widget(details_block, details_panel); 874 | 875 | let log_lines = self 876 | .logs 877 | .iter() 878 | .rev() 879 | .map(|l| { 880 | if let Some((date, rest)) = l.splitn(2, ' ').collect_tuple() { 881 | // This is not a good way to identify dates; the length can vary by system. 882 | // TODO: find a better way to identify dates 883 | if date.len() != 25 { 884 | return Line::from(l.as_str()); 885 | } 886 | Line::from(vec![Span::styled(date, Style::default().fg(Color::DarkGray)), Span::raw(" "), Span::raw(rest)]) 887 | } else { 888 | Line::from(l.as_str()) 889 | } 890 | }) 891 | .collect_vec(); 892 | 893 | let paragraph = Paragraph::new(log_lines) 894 | .block(Block::default().title("─Service Logs").borders(Borders::ALL).border_type(BorderType::Rounded)) 895 | .style(Style::default()) 896 | .wrap(Wrap { trim: true }) 897 | .scroll((self.logs_scroll_offset, 0)); 898 | f.render_widget(paragraph, logs_panel); 899 | 900 | let width = search_panel.width.max(3) - 3; // keep 2 for borders and 1 for cursor 901 | let scroll = self.input.visual_scroll(width as usize); 902 | let input = Paragraph::new(self.input.value()) 903 | .style(match self.mode { 904 | Mode::Search => Style::default().fg(Color::LightGreen), 905 | _ => Style::default(), 906 | }) 907 | .scroll((0, scroll as u16)) 908 | .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded).title(Line::from(vec![ 909 | Span::raw("─Search "), 910 | Span::styled("(", Style::default().fg(Color::DarkGray)), 911 | Span::styled("ctrl+f", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), 912 | Span::styled(" or ", Style::default().fg(Color::DarkGray)), 913 | Span::styled("/", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), 914 | Span::styled(")", Style::default().fg(Color::DarkGray)), 915 | ]))); 916 | f.render_widget(input, search_panel); 917 | // clear top right of search panel so we can put help instructions there 918 | let help_width = 24; 919 | let help_area = Rect::new(search_panel.x + search_panel.width - help_width - 2, search_panel.y, help_width, 1); 920 | f.render_widget(Clear, help_area); 921 | let help_text = Paragraph::new(Line::from(vec![ 922 | Span::raw(" Press "), 923 | Span::styled("?", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), 924 | Span::raw(" or "), 925 | Span::styled("F1", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), 926 | Span::raw(" for help "), 927 | ])) 928 | .style(Style::default().fg(Color::DarkGray)); 929 | f.render_widget(help_text, help_area); 930 | 931 | if self.mode == Mode::Search { 932 | f.set_cursor_position(( 933 | (search_panel.x + 1 + self.input.cursor() as u16).min(search_panel.x + search_panel.width - 2), 934 | search_panel.y + 1, 935 | )); 936 | } 937 | 938 | if self.mode == Mode::Help { 939 | let popup = centered_rect_abs(50, 18, f.area()); 940 | 941 | let help_lines = vec![ 942 | Line::from(""), 943 | Line::from(Span::styled("Shortcuts", Style::default().add_modifier(Modifier::UNDERLINED))), 944 | Line::from(""), 945 | Line::from(vec![primary("ctrl+C"), Span::raw(" or "), primary("ctrl+Q"), Span::raw(" to quit")]), 946 | Line::from(vec![primary("ctrl+L"), Span::raw(" toggles the logger pane")]), 947 | Line::from(vec![primary("PageUp"), Span::raw(" / "), primary("PageDown"), Span::raw(" scroll the logs")]), 948 | Line::from(vec![primary("Home"), Span::raw(" / "), primary("End"), Span::raw(" scroll to top/bottom")]), 949 | Line::from(vec![primary("Enter"), Span::raw(" or "), primary("Space"), Span::raw(" open the action menu")]), 950 | Line::from(vec![primary("?"), Span::raw(" / "), primary("F1"), Span::raw(" open this help pane")]), 951 | Line::from(""), 952 | Line::from(Span::styled("Vim Style Shortcuts", Style::default().add_modifier(Modifier::UNDERLINED))), 953 | Line::from(""), 954 | Line::from(vec![primary("j"), Span::raw(" navigate down")]), 955 | Line::from(vec![primary("k"), Span::raw(" navigate up")]), 956 | Line::from(vec![primary("ctrl+U"), Span::raw(" / "), primary("ctrl+D"), Span::raw(" scroll the logs")]), 957 | ]; 958 | 959 | let name = env!("CARGO_PKG_NAME"); 960 | let version = env!("CARGO_PKG_VERSION"); 961 | let title = format!("─Help for {} v{}", name, version); 962 | 963 | let paragraph = Paragraph::new(help_lines) 964 | .block(Block::default().title(title).borders(Borders::ALL).border_type(BorderType::Rounded)) 965 | .style(Style::default()) 966 | .wrap(Wrap { trim: true }); 967 | 968 | f.render_widget(Clear, popup); 969 | f.render_widget(paragraph, popup); 970 | } 971 | 972 | if self.mode == Mode::Error { 973 | let popup = centered_rect_abs(50, 12, f.area()); 974 | let error_lines = self.error_message.split('\n').map(Line::from).collect_vec(); 975 | let paragraph = Paragraph::new(error_lines) 976 | .block( 977 | Block::default() 978 | .title("─Error") 979 | .borders(Borders::ALL) 980 | .border_type(BorderType::Rounded) 981 | .border_style(Style::default().fg(Color::Red)), 982 | ) 983 | .wrap(Wrap { trim: true }); 984 | 985 | f.render_widget(Clear, popup); 986 | f.render_widget(paragraph, popup); 987 | } 988 | 989 | let selected_item = match self.filtered_units.selected() { 990 | Some(s) => s, 991 | None => return, 992 | }; 993 | 994 | // Help line at the bottom 995 | 996 | let version = format!("v{}", env!("CARGO_PKG_VERSION")); 997 | 998 | let help_line_rects = 999 | Layout::new(Direction::Horizontal, [Constraint::Fill(1), Constraint::Length(version.len() as u16)]) 1000 | .split(help_line_rect); 1001 | let help_rect = help_line_rects[0]; 1002 | let version_rect = help_line_rects[1]; 1003 | 1004 | let help_line = match self.mode { 1005 | Mode::Search => Line::from(span("Show actions: ", Color::Blue)), 1006 | Mode::ServiceList => Line::from(span("Show actions: | Open unit file: e | Quit: q", Color::Blue)), 1007 | Mode::Help => Line::from(span("Close menu: ", Color::Blue)), 1008 | Mode::ActionMenu => Line::from(span("Execute action: | Close menu: ", Color::Blue)), 1009 | Mode::Processing => Line::from(span("Cancel task: ", Color::Blue)), 1010 | Mode::Error => Line::from(span("Close menu: ", Color::Blue)), 1011 | }; 1012 | 1013 | f.render_widget(help_line, help_rect); 1014 | f.render_widget(Line::from(version), version_rect); 1015 | 1016 | let title = format!("Actions for {}", selected_item.name); 1017 | let mut min_width = title.len() as u16 + 2; // title plus corners 1018 | min_width = min_width.max(24); // hack: the width of the longest action name + 2 1019 | 1020 | let popup_width = min_width.min(f.area().width); 1021 | 1022 | if self.mode == Mode::ActionMenu { 1023 | let height = self.menu_items.items.len() as u16 + 2; 1024 | let popup = centered_rect_abs(popup_width, height, f.area()); 1025 | 1026 | let items: Vec = self 1027 | .menu_items 1028 | .items 1029 | .iter() 1030 | .map(|i| { 1031 | let key_string = Span::styled(format!(" {:1} ", i.key_string()), Style::default().fg(Color::Blue)); 1032 | let line = Line::from(vec![key_string, Span::raw(&i.name)]); 1033 | ListItem::new(line) 1034 | }) 1035 | .collect(); 1036 | let items = List::new(items) 1037 | .block( 1038 | Block::default() 1039 | .borders(Borders::ALL) 1040 | .border_type(BorderType::Rounded) 1041 | .border_style(Style::default().fg(Color::LightGreen)) 1042 | .title(title), 1043 | ) 1044 | .highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD)); 1045 | 1046 | f.render_widget(Clear, popup); 1047 | f.render_stateful_widget(items, popup, &mut self.menu_items.state); 1048 | } 1049 | 1050 | if self.mode == Mode::Processing { 1051 | let height = self.menu_items.items.len() as u16 + 2; 1052 | let popup = centered_rect_abs(popup_width, height, f.area()); 1053 | 1054 | static SPINNER_CHARS: &[char] = &['⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽', '⣾']; 1055 | 1056 | let spinner_char = SPINNER_CHARS[self.spinner_tick as usize % SPINNER_CHARS.len()]; 1057 | // TODO: make this a spinner 1058 | let paragraph = Paragraph::new(vec![Line::from(format!("{}", spinner_char))]) 1059 | .block( 1060 | Block::default() 1061 | .title("Processing") 1062 | .border_type(BorderType::Rounded) 1063 | .borders(Borders::ALL) 1064 | .border_style(Style::default().fg(Color::LightGreen)), 1065 | ) 1066 | .style(Style::default()) 1067 | .wrap(Wrap { trim: true }); 1068 | 1069 | f.render_widget(Clear, popup); 1070 | f.render_widget(paragraph, popup); 1071 | } 1072 | } 1073 | } 1074 | 1075 | /// helper function to create a centered rect using up certain percentage of the available rect `r` 1076 | fn _centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 1077 | let popup_layout = Layout::new( 1078 | Direction::Vertical, 1079 | [ 1080 | Constraint::Percentage((100 - percent_y) / 2), 1081 | Constraint::Percentage(percent_y), 1082 | Constraint::Percentage((100 - percent_y) / 2), 1083 | ], 1084 | ) 1085 | .split(r); 1086 | 1087 | Layout::new( 1088 | Direction::Horizontal, 1089 | [ 1090 | Constraint::Percentage((100 - percent_x) / 2), 1091 | Constraint::Percentage(percent_x), 1092 | Constraint::Percentage((100 - percent_x) / 2), 1093 | ], 1094 | ) 1095 | .split(popup_layout[1])[1] 1096 | } 1097 | 1098 | fn centered_rect_abs(width: u16, height: u16, r: Rect) -> Rect { 1099 | let offset_x = (r.width.saturating_sub(width)) / 2; 1100 | let offset_y = (r.height.saturating_sub(height)) / 2; 1101 | let width = width.min(r.width); 1102 | let height = height.min(r.height); 1103 | 1104 | Rect::new(offset_x, offset_y, width, height) 1105 | } 1106 | -------------------------------------------------------------------------------- /src/components/logger.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::LevelFilter; 3 | use ratatui::{ 4 | layout::Rect, 5 | style::{Color, Style}, 6 | widgets::{Block, BorderType, Borders}, 7 | }; 8 | use tokio::sync::mpsc::UnboundedSender; 9 | use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; 10 | 11 | use super::{Component, Frame}; 12 | use crate::action::Action; 13 | 14 | #[derive(Default)] 15 | pub struct Logger { 16 | state: TuiWidgetState, 17 | } 18 | 19 | impl Component for Logger { 20 | fn init(&mut self, _: UnboundedSender) -> Result<()> { 21 | self.state = TuiWidgetState::new().set_default_display_level(LevelFilter::Debug); 22 | Ok(()) 23 | } 24 | 25 | fn render(&mut self, f: &mut Frame<'_>, rect: Rect) { 26 | let w = TuiLoggerWidget::default() 27 | .block(Block::default().title("─systemctl-tui logs").borders(Borders::ALL).border_type(BorderType::Rounded)) 28 | .style_error(Style::default().fg(Color::Red)) 29 | .style_debug(Style::default().fg(Color::Green)) 30 | .style_warn(Style::default().fg(Color::Yellow)) 31 | .style_trace(Style::default().fg(Color::Magenta)) 32 | .style_info(Style::default().fg(Color::Cyan)) 33 | .output_separator(':') 34 | .output_timestamp(Some("%H:%M:%S".to_string())) 35 | .output_level(Some(TuiLoggerLevelOutput::Long)) 36 | .output_target(false) 37 | .output_file(true) 38 | .output_line(true) 39 | .state(&self.state); 40 | f.render_widget(w, rect); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::{KeyEvent, MouseEvent}; 3 | use ratatui::{layout::Rect, Frame}; 4 | use tokio::sync::mpsc::UnboundedSender; 5 | 6 | use crate::{action::Action, event::Event}; 7 | 8 | pub mod home; 9 | pub mod logger; 10 | 11 | pub trait Component { 12 | #[allow(unused_variables)] 13 | fn init(&mut self, tx: UnboundedSender) -> Result<()> { 14 | Ok(()) 15 | } 16 | fn handle_events(&mut self, event: Option) -> Vec { 17 | match event { 18 | Some(Event::Quit) => vec![Action::Quit], 19 | Some(Event::RenderTick) => vec![Action::Render], 20 | Some(Event::Key(key_event)) => self.handle_key_events(key_event), 21 | Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event), 22 | Some(Event::Resize(x, y)) => vec![Action::Resize(x, y)], 23 | Some(Event::RefreshTick) => vec![Action::RefreshServices], 24 | Some(_) => vec![], 25 | None => vec![], 26 | } 27 | } 28 | #[allow(unused_variables)] 29 | fn handle_key_events(&mut self, key: KeyEvent) -> Vec { 30 | vec![] 31 | } 32 | #[allow(unused_variables)] 33 | fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Vec { 34 | vec![] 35 | } 36 | #[allow(unused_variables)] 37 | fn dispatch(&mut self, action: Action) -> Option { 38 | None 39 | } 40 | fn render(&mut self, f: &mut Frame<'_>, rect: Rect); 41 | } 42 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | 3 | use crossterm::event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent}; 4 | use futures::{FutureExt, StreamExt}; 5 | use tokio::{ 6 | sync::{mpsc, Mutex}, 7 | task::JoinHandle, 8 | }; 9 | use tokio_util::sync::CancellationToken; 10 | 11 | use crate::{ 12 | action::Action, 13 | components::{home::Home, Component}, 14 | }; 15 | 16 | #[derive(Clone, Copy, Debug)] 17 | pub enum Event { 18 | Quit, 19 | Error, 20 | Closed, 21 | RenderTick, 22 | RefreshTick, 23 | Key(KeyEvent), 24 | Mouse(MouseEvent), 25 | Resize(u16, u16), 26 | } 27 | 28 | pub struct EventHandler { 29 | pub task: JoinHandle<()>, 30 | cancellation_token: CancellationToken, 31 | } 32 | 33 | const SERVICE_REFRESH_INTERVAL_MS: u64 = 5000; 34 | 35 | impl EventHandler { 36 | pub fn new(home: Arc>, action_tx: mpsc::UnboundedSender) -> Self { 37 | let (event_tx, mut event_rx) = mpsc::unbounded_channel(); 38 | let cancellation_token = CancellationToken::new(); 39 | let _cancellation_token = cancellation_token.clone(); 40 | let task = tokio::spawn(async move { 41 | let mut reader = crossterm::event::EventStream::new(); 42 | let mut refresh_services_interval = tokio::time::interval(Duration::from_millis(SERVICE_REFRESH_INTERVAL_MS)); 43 | refresh_services_interval.tick().await; 44 | loop { 45 | let refresh_delay = refresh_services_interval.tick(); 46 | let crossterm_event = reader.next().fuse(); 47 | tokio::select! { 48 | _ = _cancellation_token.cancelled() => { 49 | break; 50 | } 51 | maybe_event = crossterm_event => { 52 | match maybe_event { 53 | Some(Ok(evt)) => { 54 | match evt { 55 | CrosstermEvent::Key(key) => { 56 | if key.kind == KeyEventKind::Press { 57 | event_tx.send(Event::Key(key)).unwrap(); 58 | } 59 | }, 60 | // interestingly, we never get these if running in dev mode with watchexec 61 | CrosstermEvent::Resize(x, y) => { 62 | event_tx.send(Event::Resize(x, y)).unwrap(); 63 | }, 64 | _ => {}, 65 | } 66 | } 67 | Some(Err(_)) => { 68 | event_tx.send(Event::Error).unwrap(); 69 | } 70 | None => {}, 71 | } 72 | }, 73 | _ = refresh_delay => { 74 | event_tx.send(Event::RefreshTick).unwrap(); 75 | }, 76 | event = event_rx.recv() => { 77 | let actions = home.lock().await.handle_events(event); 78 | for action in actions { 79 | action_tx.send(action).unwrap(); 80 | } 81 | } 82 | } 83 | } 84 | }); 85 | Self { task, cancellation_token } 86 | } 87 | 88 | pub fn stop(&mut self) { 89 | self.cancellation_token.cancel(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | 3 | pub mod action; 4 | 5 | pub mod components; 6 | 7 | pub mod event; 8 | 9 | pub mod terminal; 10 | 11 | pub mod utils; 12 | 13 | pub mod systemd; 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{Parser, ValueEnum}; 3 | use systemctl_tui::{ 4 | app::App, 5 | systemd, 6 | utils::{initialize_logging, initialize_panic_handler, version}, 7 | }; 8 | 9 | // Define the command line arguments structure 10 | #[derive(Parser, Debug)] 11 | #[command(version = version(), about = "A simple TUI for systemd services")] 12 | struct Args { 13 | /// The scope of the services to display. Defaults to "all" normally and "global" on WSL 14 | #[clap(short, long)] 15 | scope: Option, 16 | /// Enable performance tracing (in Chromium Event JSON format) 17 | #[clap(short, long)] 18 | trace: bool, 19 | /// Limit view to only these unit files 20 | #[clap(short, long, default_value="*.service", num_args=1..)] 21 | limit_units: Vec, 22 | } 23 | 24 | #[derive(Parser, Debug, ValueEnum, Clone)] 25 | pub enum Scope { 26 | Global, 27 | User, 28 | All, 29 | } 30 | 31 | #[tokio::main] 32 | async fn main() -> Result<()> { 33 | // Help users help me with bug reports by making sure they have stack traces 34 | if std::env::var("RUST_BACKTRACE").is_err() { 35 | std::env::set_var("RUST_BACKTRACE", "1"); 36 | } 37 | 38 | let args = Args::parse(); 39 | let _guard = initialize_logging(args.trace)?; 40 | initialize_panic_handler(); 41 | 42 | // There's probably a nicer way to do this than defining the scope enum twice, but this is fine for now 43 | let scope = match args.scope { 44 | Some(Scope::Global) => systemd::Scope::Global, 45 | Some(Scope::User) => systemd::Scope::User, 46 | Some(Scope::All) => systemd::Scope::All, 47 | // So, WSL doesn't *really* support user services yet: https://github.com/microsoft/WSL/issues/8842 48 | // Revisit this if that changes 49 | None => { 50 | if is_wsl::is_wsl() { 51 | systemd::Scope::Global 52 | } else { 53 | systemd::Scope::All 54 | } 55 | }, 56 | }; 57 | 58 | let mut app = App::new(scope, args.limit_units)?; 59 | app.run().await?; 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /src/systemd.rs: -------------------------------------------------------------------------------- 1 | // File initially taken from https://github.com/servicer-labs/servicer/blob/master/src/utils/systemd.rs, since modified 2 | 3 | use core::str; 4 | use std::process::Command; 5 | 6 | use anyhow::{bail, Context, Result}; 7 | use log::error; 8 | use tokio_util::sync::CancellationToken; 9 | use tracing::info; 10 | use zbus::{proxy, zvariant, Connection}; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct UnitWithStatus { 14 | pub name: String, // The primary unit name as string 15 | pub scope: UnitScope, // System or user? 16 | pub description: String, // The human readable description string 17 | pub file_path: Option>, // The unit file path - populated later on demand 18 | 19 | pub load_state: String, // The load state (i.e. whether the unit file has been loaded successfully) 20 | 21 | // Some comments re: state from this helpful comment: https://www.reddit.com/r/linuxquestions/comments/r58dvz/comment/hmlemfk/ 22 | /// One state, called the "activation state", essentially describes what the unit is doing now. The two most common values for this state are active and inactive, though there are a few other possibilities. (Each unit type has its own set of "substates" that map to these activation states. For instance, service units can be running or stopped. Again, there's a variety of other substates, and the list differs for each unit type.) 23 | pub activation_state: String, 24 | /// The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not) 25 | pub sub_state: String, 26 | 27 | /// The other state all units have is called the "enablement state". It describes how the unit might be automatically started in the future. A unit is enabled if it has been added to the requirements list of any other unit though symlinks in the filesystem. The set of symlinks to be created when enabling a unit is described by the unit's [Install] section. A unit is disabled if no symlinks are present. Again there's a variety of other values other than these two (e.g. not all units even have [Install] sections). 28 | /// Only populated when needed b/c this is much slower to get 29 | pub enablement_state: Option, 30 | // We don't use any of these right now, might as well skip'em so there's less data to clone 31 | // pub followed: String, // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string. 32 | // pub path: String, // The unit object path 33 | // pub job_id: u32, // If there is a job queued for the job unit the numeric job id, 0 otherwise 34 | // pub job_type: String, // The job type as string 35 | // pub job_path: String, // The job object path 36 | } 37 | 38 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 39 | pub enum UnitScope { 40 | Global, 41 | User, 42 | } 43 | 44 | /// Just enough info to fully identify a unit 45 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 46 | pub struct UnitId { 47 | pub name: String, 48 | pub scope: UnitScope, 49 | } 50 | 51 | impl UnitWithStatus { 52 | pub fn is_active(&self) -> bool { 53 | self.activation_state == "active" 54 | } 55 | 56 | pub fn is_failed(&self) -> bool { 57 | self.activation_state == "failed" 58 | } 59 | 60 | pub fn is_not_found(&self) -> bool { 61 | self.load_state == "not-found" 62 | } 63 | 64 | pub fn is_enabled(&self) -> bool { 65 | self.load_state == "loaded" && self.activation_state == "active" 66 | } 67 | 68 | pub fn short_name(&self) -> &str { 69 | if self.name.ends_with(".service") { 70 | &self.name[..self.name.len() - 8] 71 | } else { 72 | &self.name 73 | } 74 | } 75 | 76 | // TODO: should we have a non-allocating version of this? 77 | pub fn id(&self) -> UnitId { 78 | UnitId { name: self.name.clone(), scope: self.scope } 79 | } 80 | 81 | // useful for updating without wiping out the file path 82 | pub fn update(&mut self, other: UnitWithStatus) { 83 | self.description = other.description; 84 | self.load_state = other.load_state; 85 | self.activation_state = other.activation_state; 86 | self.sub_state = other.sub_state; 87 | } 88 | } 89 | 90 | type RawUnit = 91 | (String, String, String, String, String, String, zvariant::OwnedObjectPath, u32, String, zvariant::OwnedObjectPath); 92 | 93 | fn to_unit_status(raw_unit: RawUnit, scope: UnitScope) -> UnitWithStatus { 94 | let (name, description, load_state, active_state, sub_state, _followed, _path, _job_id, _job_type, _job_path) = 95 | raw_unit; 96 | 97 | UnitWithStatus { 98 | name, 99 | scope, 100 | description, 101 | file_path: None, 102 | enablement_state: None, 103 | load_state, 104 | activation_state: active_state, 105 | sub_state, 106 | } 107 | } 108 | 109 | // Different from UnitScope in that this is not for 1 specific unit (i.e. it can include multiple scopes) 110 | #[derive(Clone, Copy, Default, Debug)] 111 | pub enum Scope { 112 | Global, 113 | User, 114 | #[default] 115 | All, 116 | } 117 | 118 | // this takes like 5-10 ms on 13th gen Intel i7 (scope=all) 119 | pub async fn get_all_services(scope: Scope, services: &[String]) -> Result> { 120 | let start = std::time::Instant::now(); 121 | 122 | let mut units = vec![]; 123 | 124 | let is_root = nix::unistd::geteuid().is_root(); 125 | 126 | match scope { 127 | Scope::Global => { 128 | let system_units = get_services(UnitScope::Global, services).await?; 129 | units.extend(system_units); 130 | }, 131 | Scope::User => { 132 | let user_units = get_services(UnitScope::User, services).await?; 133 | units.extend(user_units); 134 | }, 135 | Scope::All => { 136 | let (system_units, user_units) = 137 | tokio::join!(get_services(UnitScope::Global, services), get_services(UnitScope::User, services)); 138 | units.extend(system_units?); 139 | 140 | // Should always be able to get user units, but it may fail when running as root 141 | if let Ok(user_units) = user_units { 142 | units.extend(user_units); 143 | } else if is_root { 144 | error!("Failed to get user units, ignoring because we're running as root") 145 | } else { 146 | user_units?; 147 | } 148 | }, 149 | } 150 | 151 | // sort by name case-insensitive 152 | units.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); 153 | 154 | info!("Loaded systemd services in {:?}", start.elapsed()); 155 | 156 | Ok(units) 157 | } 158 | 159 | async fn get_services(scope: UnitScope, services: &[String]) -> Result, anyhow::Error> { 160 | let connection = get_connection(scope).await?; 161 | let manager_proxy = ManagerProxy::new(&connection).await?; 162 | let units = manager_proxy.list_units_by_patterns(vec![], services.to_vec()).await?; 163 | let units: Vec<_> = units.into_iter().map(|u| to_unit_status(u, scope)).collect(); 164 | Ok(units) 165 | } 166 | 167 | pub fn get_unit_file_location(service: &UnitId) -> Result { 168 | // show -P FragmentPath reitunes.service 169 | let mut args = vec!["--quiet", "show", "-P", "FragmentPath"]; 170 | args.push(&service.name); 171 | 172 | if service.scope == UnitScope::User { 173 | args.insert(0, "--user"); 174 | } 175 | 176 | let output = Command::new("systemctl").args(&args).output()?; 177 | 178 | if output.status.success() { 179 | let path = str::from_utf8(&output.stdout)?.trim(); 180 | if path.is_empty() { 181 | bail!("No unit file found for {}", service.name); 182 | } 183 | Ok(path.trim().to_string()) 184 | } else { 185 | let stderr = String::from_utf8(output.stderr)?; 186 | bail!(stderr); 187 | } 188 | } 189 | 190 | pub async fn start_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> { 191 | async fn start_service(service: UnitId) -> Result<()> { 192 | let connection = get_connection(service.scope).await?; 193 | let manager_proxy = ManagerProxy::new(&connection).await?; 194 | manager_proxy.start_unit(service.name.clone(), "replace".into()).await?; 195 | Ok(()) 196 | } 197 | 198 | // god these select macros are ugly, is there really no better way to select? 199 | tokio::select! { 200 | _ = cancel_token.cancelled() => { 201 | anyhow::bail!("cancelled"); 202 | } 203 | result = start_service(service) => { 204 | result 205 | } 206 | } 207 | } 208 | 209 | pub async fn stop_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> { 210 | async fn stop_service(service: UnitId) -> Result<()> { 211 | let connection = get_connection(service.scope).await?; 212 | let manager_proxy = ManagerProxy::new(&connection).await?; 213 | manager_proxy.stop_unit(service.name, "replace".into()).await?; 214 | Ok(()) 215 | } 216 | 217 | // god these select macros are ugly, is there really no better way to select? 218 | tokio::select! { 219 | _ = cancel_token.cancelled() => { 220 | anyhow::bail!("cancelled"); 221 | } 222 | result = stop_service(service) => { 223 | result 224 | } 225 | } 226 | } 227 | 228 | pub async fn reload(scope: UnitScope, cancel_token: CancellationToken) -> Result<()> { 229 | async fn reload_(scope: UnitScope) -> Result<()> { 230 | let connection = get_connection(scope).await?; 231 | let manager_proxy: ManagerProxy<'_> = ManagerProxy::new(&connection).await?; 232 | let error_message = match scope { 233 | UnitScope::Global => "Failed to reload units, probably because superuser permissions are needed. Try running `sudo systemctl daemon-reload`", 234 | UnitScope::User => "Failed to reload units. Try running `systemctl --user daemon-reload`", 235 | }; 236 | manager_proxy.reload().await.context(error_message)?; 237 | Ok(()) 238 | } 239 | 240 | // god these select macros are ugly, is there really no better way to select? 241 | tokio::select! { 242 | _ = cancel_token.cancelled() => { 243 | anyhow::bail!("cancelled"); 244 | } 245 | result = reload_(scope) => { 246 | result 247 | } 248 | } 249 | } 250 | 251 | async fn get_connection(scope: UnitScope) -> Result { 252 | match scope { 253 | UnitScope::Global => Ok(Connection::system().await?), 254 | UnitScope::User => Ok(Connection::session().await?), 255 | } 256 | } 257 | 258 | pub async fn restart_service(service: UnitId, cancel_token: CancellationToken) -> Result<()> { 259 | async fn restart(service: UnitId) -> Result<()> { 260 | let connection = get_connection(service.scope).await?; 261 | let manager_proxy = ManagerProxy::new(&connection).await?; 262 | manager_proxy.restart_unit(service.name, "replace".into()).await?; 263 | Ok(()) 264 | } 265 | 266 | // god these select macros are ugly, is there really no better way to select? 267 | tokio::select! { 268 | _ = cancel_token.cancelled() => { 269 | // The token was cancelled 270 | anyhow::bail!("cancelled"); 271 | } 272 | result = restart(service) => { 273 | result 274 | } 275 | } 276 | } 277 | 278 | // useless function only added to test that cancellation works 279 | pub async fn sleep_test(_service: String, cancel_token: CancellationToken) -> Result<()> { 280 | // god these select macros are ugly, is there really no better way to select? 281 | tokio::select! { 282 | _ = cancel_token.cancelled() => { 283 | // The token was cancelled 284 | anyhow::bail!("cancelled"); 285 | } 286 | _ = tokio::time::sleep(std::time::Duration::from_secs(2)) => { 287 | Ok(()) 288 | } 289 | } 290 | } 291 | 292 | /// Proxy object for `org.freedesktop.systemd1.Manager`. 293 | /// Partially taken from https://github.com/lucab/zbus_systemd/blob/main/src/systemd1/generated.rs 294 | #[proxy( 295 | interface = "org.freedesktop.systemd1.Manager", 296 | default_service = "org.freedesktop.systemd1", 297 | default_path = "/org/freedesktop/systemd1", 298 | gen_blocking = false 299 | )] 300 | pub trait Manager { 301 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#StartUnit()) Call interface method `StartUnit`. 302 | #[dbus_proxy(name = "StartUnit")] 303 | fn start_unit(&self, name: String, mode: String) -> zbus::Result; 304 | 305 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#StopUnit()) Call interface method `StopUnit`. 306 | #[dbus_proxy(name = "StopUnit")] 307 | fn stop_unit(&self, name: String, mode: String) -> zbus::Result; 308 | 309 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#ReloadUnit()) Call interface method `ReloadUnit`. 310 | #[dbus_proxy(name = "ReloadUnit")] 311 | fn reload_unit(&self, name: String, mode: String) -> zbus::Result; 312 | 313 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#RestartUnit()) Call interface method `RestartUnit`. 314 | #[dbus_proxy(name = "RestartUnit")] 315 | fn restart_unit(&self, name: String, mode: String) -> zbus::Result; 316 | 317 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#EnableUnitFiles()) Call interface method `EnableUnitFiles`. 318 | #[dbus_proxy(name = "EnableUnitFiles")] 319 | fn enable_unit_files( 320 | &self, 321 | files: Vec, 322 | runtime: bool, 323 | force: bool, 324 | ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; 325 | 326 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#DisableUnitFiles()) Call interface method `DisableUnitFiles`. 327 | #[dbus_proxy(name = "DisableUnitFiles")] 328 | fn disable_unit_files(&self, files: Vec, runtime: bool) -> zbus::Result>; 329 | 330 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#ListUnits()) Call interface method `ListUnits`. 331 | #[dbus_proxy(name = "ListUnits")] 332 | fn list_units( 333 | &self, 334 | ) -> zbus::Result< 335 | Vec<( 336 | String, 337 | String, 338 | String, 339 | String, 340 | String, 341 | String, 342 | zvariant::OwnedObjectPath, 343 | u32, 344 | String, 345 | zvariant::OwnedObjectPath, 346 | )>, 347 | >; 348 | 349 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#ListUnitsByPatterns()) Call interface method `ListUnitsByPatterns`. 350 | #[dbus_proxy(name = "ListUnitsByPatterns")] 351 | fn list_units_by_patterns( 352 | &self, 353 | states: Vec, 354 | patterns: Vec, 355 | ) -> zbus::Result< 356 | Vec<( 357 | String, 358 | String, 359 | String, 360 | String, 361 | String, 362 | String, 363 | zvariant::OwnedObjectPath, 364 | u32, 365 | String, 366 | zvariant::OwnedObjectPath, 367 | )>, 368 | >; 369 | 370 | /// [📖](https://www.freedesktop.org/software/systemd/man/systemd.directives.html#Reload()) Call interface method `Reload`. 371 | #[dbus_proxy(name = "Reload")] 372 | fn reload(&self) -> zbus::Result<()>; 373 | } 374 | 375 | /// Proxy object for `org.freedesktop.systemd1.Unit`. 376 | /// Taken from https://github.com/lucab/zbus_systemd/blob/main/src/systemd1/generated.rs 377 | #[proxy( 378 | interface = "org.freedesktop.systemd1.Unit", 379 | default_service = "org.freedesktop.systemd1", 380 | assume_defaults = false, 381 | gen_blocking = false 382 | )] 383 | pub trait Unit { 384 | /// Get property `ActiveState`. 385 | #[dbus_proxy(property)] 386 | fn active_state(&self) -> zbus::Result; 387 | 388 | /// Get property `LoadState`. 389 | #[dbus_proxy(property)] 390 | fn load_state(&self) -> zbus::Result; 391 | 392 | /// Get property `UnitFileState`. 393 | #[dbus_proxy(property)] 394 | fn unit_file_state(&self) -> zbus::Result; 395 | } 396 | 397 | /// Proxy object for `org.freedesktop.systemd1.Service`. 398 | /// Taken from https://github.com/lucab/zbus_systemd/blob/main/src/systemd1/generated.rs 399 | #[proxy( 400 | interface = "org.freedesktop.systemd1.Service", 401 | default_service = "org.freedesktop.systemd1", 402 | assume_defaults = false, 403 | gen_blocking = false 404 | )] 405 | trait Service { 406 | /// Get property `MainPID`. 407 | #[dbus_proxy(property, name = "MainPID")] 408 | fn main_pid(&self) -> zbus::Result; 409 | } 410 | 411 | /// Returns the load state of a systemd unit 412 | /// 413 | /// Returns `invalid-unit-path` if the path is invalid 414 | /// 415 | /// # Arguments 416 | /// 417 | /// * `connection`: zbus connection 418 | /// * `full_service_name`: Full name of the service name with '.service' in the end 419 | /// 420 | pub async fn get_active_state(connection: &Connection, full_service_name: &str) -> String { 421 | let object_path = get_unit_path(full_service_name); 422 | 423 | match zvariant::ObjectPath::try_from(object_path) { 424 | Ok(path) => { 425 | let unit_proxy = UnitProxy::new(connection, path).await.unwrap(); 426 | unit_proxy.active_state().await.unwrap_or("invalid-unit-path".into()) 427 | }, 428 | Err(_) => "invalid-unit-path".to_string(), 429 | } 430 | } 431 | 432 | /// Returns the unit file state of a systemd unit. If the state is `enabled`, the unit loads on every boot 433 | /// 434 | /// Returns `invalid-unit-path` if the path is invalid 435 | /// 436 | /// # Arguments 437 | /// 438 | /// * `connection`: zbus connection 439 | /// * `full_service_name`: Full name of the service name with '.service' in the end 440 | /// 441 | pub async fn get_unit_file_state(connection: &Connection, full_service_name: &str) -> String { 442 | let object_path = get_unit_path(full_service_name); 443 | 444 | match zvariant::ObjectPath::try_from(object_path) { 445 | Ok(path) => { 446 | let unit_proxy = UnitProxy::new(connection, path).await.unwrap(); 447 | unit_proxy.unit_file_state().await.unwrap_or("invalid-unit-path".into()) 448 | }, 449 | Err(_) => "invalid-unit-path".to_string(), 450 | } 451 | } 452 | 453 | /// Returns the PID of a systemd service 454 | /// 455 | /// # Arguments 456 | /// 457 | /// * `connection`: zbus connection 458 | /// * `full_service_name`: Full name of the service name with '.service' in the end 459 | /// 460 | pub async fn get_main_pid(connection: &Connection, full_service_name: &str) -> Result { 461 | let object_path = get_unit_path(full_service_name); 462 | 463 | let validated_object_path = zvariant::ObjectPath::try_from(object_path).unwrap(); 464 | 465 | let service_proxy = ServiceProxy::new(connection, validated_object_path).await.unwrap(); 466 | service_proxy.main_pid().await 467 | } 468 | 469 | /// Encode into a valid dbus string 470 | /// 471 | /// # Arguments 472 | /// 473 | /// * `input_string` 474 | /// 475 | fn encode_as_dbus_object_path(input_string: &str) -> String { 476 | input_string 477 | .chars() 478 | .map(|c| if c.is_ascii_alphanumeric() || c == '/' || c == '_' { c.to_string() } else { format!("_{:x}", c as u32) }) 479 | .collect() 480 | } 481 | 482 | /// Unit file path for a service 483 | /// 484 | /// # Arguments 485 | /// 486 | /// * `full_service_name` 487 | /// 488 | pub fn get_unit_path(full_service_name: &str) -> String { 489 | format!("/org/freedesktop/systemd1/unit/{}", encode_as_dbus_object_path(full_service_name)) 490 | } 491 | -------------------------------------------------------------------------------- /src/terminal.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut}, 3 | sync::Arc, 4 | }; 5 | 6 | use anyhow::{anyhow, Context, Result}; 7 | use crossterm::{ 8 | cursor, 9 | event::{DisableMouseCapture, EnableMouseCapture}, 10 | terminal::{EnterAlternateScreen, LeaveAlternateScreen}, 11 | }; 12 | use ratatui::backend::CrosstermBackend as Backend; 13 | use signal_hook::{iterator::Signals, low_level}; 14 | use tokio::{ 15 | sync::{mpsc, Mutex}, 16 | task::JoinHandle, 17 | }; 18 | 19 | use crate::components::{home::Home, Component}; 20 | 21 | // A struct that mostly exists to be a catch-all for terminal operations that should be synchronized 22 | pub struct Tui { 23 | pub terminal: ratatui::Terminal>, 24 | } 25 | 26 | impl Tui { 27 | pub fn new() -> Result { 28 | let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?; 29 | 30 | // spin up a signal handler to catch SIGTERM and exit gracefully 31 | let _ = std::thread::spawn(move || { 32 | const SIGNALS: &[libc::c_int] = &[signal_hook::consts::signal::SIGTERM]; 33 | let mut sigs = Signals::new(SIGNALS).unwrap(); 34 | let signal = sigs.into_iter().next().unwrap(); 35 | let _ = exit(); 36 | low_level::emulate_default_handler(signal).unwrap(); 37 | }); 38 | 39 | Ok(Self { terminal }) 40 | } 41 | 42 | pub fn enter(&self) -> Result<()> { 43 | crossterm::terminal::enable_raw_mode()?; 44 | crossterm::execute!(std::io::stderr(), EnterAlternateScreen, EnableMouseCapture, cursor::Hide)?; 45 | Ok(()) 46 | } 47 | 48 | pub fn suspend(&self) -> Result<()> { 49 | self.exit()?; 50 | #[cfg(not(windows))] 51 | signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; 52 | Ok(()) 53 | } 54 | 55 | pub fn resume(&self) -> Result<()> { 56 | self.enter()?; 57 | Ok(()) 58 | } 59 | 60 | pub fn exit(&self) -> Result<()> { 61 | exit() 62 | } 63 | } 64 | 65 | // This one's public because we want to expose it to the panic handler 66 | pub fn exit() -> Result<()> { 67 | crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, DisableMouseCapture, cursor::Show)?; 68 | crossterm::terminal::disable_raw_mode()?; 69 | Ok(()) 70 | } 71 | 72 | impl Deref for Tui { 73 | type Target = ratatui::Terminal>; 74 | 75 | fn deref(&self) -> &Self::Target { 76 | &self.terminal 77 | } 78 | } 79 | 80 | impl DerefMut for Tui { 81 | fn deref_mut(&mut self) -> &mut Self::Target { 82 | &mut self.terminal 83 | } 84 | } 85 | 86 | impl Drop for Tui { 87 | fn drop(&mut self) { 88 | exit().unwrap(); 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 93 | enum Message { 94 | Render, 95 | Stop, 96 | Suspend, 97 | } 98 | 99 | pub struct TerminalHandler { 100 | pub task: JoinHandle<()>, 101 | tx: mpsc::UnboundedSender, 102 | home: Arc>, 103 | pub tui: Arc>, 104 | } 105 | 106 | impl TerminalHandler { 107 | pub fn new(home: Arc>) -> Self { 108 | let (tx, mut rx) = mpsc::unbounded_channel::(); 109 | let cloned_home = home.clone(); 110 | let tui = Tui::new().context(anyhow!("Unable to create terminal")).unwrap(); 111 | tui.enter().unwrap(); 112 | let tui = Arc::new(Mutex::new(tui)); 113 | let cloned_tui = tui.clone(); 114 | let task = tokio::spawn(async move { 115 | loop { 116 | match rx.recv().await { 117 | Some(Message::Stop) => { 118 | exit().unwrap_or_default(); 119 | break; 120 | }, 121 | Some(Message::Suspend) => { 122 | let t = tui.lock().await; 123 | t.suspend().unwrap_or_default(); 124 | break; 125 | }, 126 | Some(Message::Render) => { 127 | let mut t = tui.lock().await; 128 | let mut home = home.lock().await; 129 | render(&mut t, &mut home); 130 | }, 131 | None => {}, 132 | } 133 | } 134 | }); 135 | Self { task, tx, home: cloned_home, tui: cloned_tui } 136 | } 137 | 138 | pub fn suspend(&self) -> Result<()> { 139 | self.tx.send(Message::Suspend)?; 140 | Ok(()) 141 | } 142 | 143 | pub fn stop(&self) -> Result<()> { 144 | self.tx.send(Message::Stop)?; 145 | Ok(()) 146 | } 147 | 148 | pub async fn render(&self) { 149 | let mut home = self.home.lock().await; 150 | let mut tui = self.tui.lock().await; 151 | render(&mut tui, &mut home); 152 | } 153 | 154 | // little more performant in situations where we don't need to wait for the render to complete 155 | pub fn enqueue_render(&self) -> Result<()> { 156 | self.tx.send(Message::Render)?; 157 | Ok(()) 158 | } 159 | } 160 | 161 | fn render(tui: &mut Tui, home: &mut Home) { 162 | tui 163 | .draw(|f| { 164 | home.render(f, f.area()); 165 | }) 166 | .expect("Unable to draw"); 167 | } 168 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Write, path::PathBuf, sync::atomic::AtomicBool}; 2 | 3 | use anyhow::{anyhow, Context, Result}; 4 | use better_panic::Settings; 5 | use directories::ProjectDirs; 6 | use lazy_static::lazy_static; 7 | use tracing::{error, level_filters::LevelFilter}; 8 | use tracing_appender::{ 9 | non_blocking::WorkerGuard, 10 | rolling::{RollingFileAppender, Rotation}, 11 | }; 12 | use tracing_subscriber::{ 13 | self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, 14 | }; 15 | 16 | lazy_static! { 17 | static ref TRACE_FILE_NAME: PathBuf = { 18 | let directory = get_data_dir().expect("Unable to get data directory"); 19 | let timestamp_iso8601 = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); 20 | directory.join(format!("systemctl-tui-trace-{}.log", timestamp_iso8601)) 21 | }; 22 | } 23 | 24 | static TRACING_ENABLED: AtomicBool = AtomicBool::new(false); 25 | 26 | pub fn initialize_panic_handler() { 27 | std::panic::set_hook(Box::new(|panic_info| { 28 | if let Err(r) = crate::terminal::exit() { 29 | error!("Unable to exit Terminal: {r:?}"); 30 | } 31 | 32 | Settings::auto().most_recent_first(false).lineno_suffix(true).create_panic_handler()(panic_info); 33 | std::process::exit(libc::EXIT_FAILURE); 34 | })); 35 | } 36 | 37 | pub fn get_data_dir() -> Result { 38 | let directory = if let Ok(s) = std::env::var("SYSTEMCTL_TUI_DATA") { 39 | PathBuf::from(s) 40 | } else if let Some(proj_dirs) = ProjectDirs::from("com", "rgwood", "systemctl-tui") { 41 | proj_dirs.data_local_dir().to_path_buf() 42 | } else { 43 | return Err(anyhow!("Unable to find data directory for systemctl-tui")); 44 | }; 45 | Ok(directory) 46 | } 47 | 48 | pub fn get_config_dir() -> Result { 49 | let directory = if let Ok(s) = std::env::var("SYSTEMCTL_TUI_CONFIG") { 50 | PathBuf::from(s) 51 | } else if let Some(proj_dirs) = ProjectDirs::from("com", "rgwood", "systemctl-tui") { 52 | proj_dirs.config_local_dir().to_path_buf() 53 | } else { 54 | return Err(anyhow!("Unable to find config directory for systemctl-tui")); 55 | }; 56 | Ok(directory) 57 | } 58 | 59 | pub fn initialize_logging(enable_tracing: bool) -> Result { 60 | let directory = get_data_dir()?; 61 | std::fs::create_dir_all(directory.clone()).context(format!("{directory:?} could not be created"))?; 62 | // let log_path = directory.join("systemctl-tui.log"); 63 | 64 | // create a file appender that rolls daily 65 | let file_appender = RollingFileAppender::new(Rotation::DAILY, &directory, "systemctl-tui.log"); 66 | 67 | // create a non-blocking writer 68 | let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); 69 | 70 | // create a layer for the file logger 71 | let file_layer = tracing_subscriber::fmt::layer() 72 | .with_writer(non_blocking) 73 | .with_file(true) 74 | .with_line_number(true) 75 | .with_target(false) 76 | .with_ansi(false) 77 | .with_filter(EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy()); 78 | 79 | let tui_layer = tui_logger::tracing_subscriber_layer() 80 | .with_filter(EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy()); 81 | 82 | tracing_subscriber::registry().with(file_layer).with(tui_layer).init(); 83 | 84 | if enable_tracing { 85 | TRACING_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed); 86 | let mut trace_file = std::fs::File::create(&*TRACE_FILE_NAME).unwrap(); 87 | trace_file.write_all(b"[\n").unwrap(); // start of chrome://tracing file 88 | } 89 | 90 | let directory = directory.to_string_lossy().into_owned(); 91 | tracing::info!(directory, "Logging initialized"); 92 | 93 | Ok(guard) 94 | } 95 | 96 | // Write an event in chrome://tracing format 97 | // This is currently very basic+hacky, I'm mostly doing it to experiment with Perfetto 98 | // Reference: https://thume.ca/2023/12/02/tracing-methods/ 99 | pub fn log_perf_event(event: &str, duration: std::time::Duration) { 100 | if !TRACING_ENABLED.load(std::sync::atomic::Ordering::Relaxed) { 101 | return; 102 | } 103 | let log_path = &*TRACE_FILE_NAME; 104 | let system_time = std::time::SystemTime::now(); 105 | 106 | let event = format!( 107 | r#"{{ 108 | "name": "{}", 109 | "cat": "PERF", 110 | "ph": "X", 111 | "ts": {}, 112 | "dur": {} 113 | }}"#, 114 | event, 115 | system_time.duration_since(std::time::UNIX_EPOCH).unwrap().as_micros(), 116 | duration.as_micros() 117 | ); 118 | 119 | let mut file = std::fs::OpenOptions::new().append(true).create(true).open(log_path).unwrap(); 120 | file.write_all(event.as_bytes()).unwrap(); 121 | file.write_all(b",\n").unwrap(); 122 | } 123 | 124 | /// Similar to the `std::dbg!` macro, but generates `tracing` events rather 125 | /// than printing to stdout. 126 | /// 127 | /// By default, the verbosity level for the generated events is `DEBUG`, but 128 | /// this can be customized. 129 | #[macro_export] 130 | macro_rules! trace_dbg { 131 | (target: $target:expr, level: $level:expr, $ex:expr) => {{ 132 | match $ex { 133 | value => { 134 | tracing::event!(target: $target, $level, ?value, stringify!($ex)); 135 | value 136 | } 137 | } 138 | }}; 139 | (level: $level:expr, $ex:expr) => { 140 | trace_dbg!(target: module_path!(), level: $level, $ex) 141 | }; 142 | (target: $target:expr, $ex:expr) => { 143 | trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) 144 | }; 145 | ($ex:expr) => { 146 | trace_dbg!(level: tracing::Level::DEBUG, $ex) 147 | }; 148 | } 149 | 150 | pub fn version() -> String { 151 | let author = clap::crate_authors!(); 152 | 153 | let version = env!("CARGO_PKG_VERSION"); 154 | 155 | let config_dir_path = get_config_dir().unwrap().display().to_string(); 156 | let data_dir_path = get_data_dir().unwrap().display().to_string(); 157 | 158 | format!( 159 | "\ 160 | {version} 161 | 162 | Authors: {author} 163 | 164 | Config directory: {config_dir_path} 165 | Data directory: {data_dir_path}" 166 | ) 167 | } 168 | --------------------------------------------------------------------------------