├── .clangd ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates.nix ├── flake.lock ├── flake.nix ├── result ├── rustfmt.toml ├── src ├── key_handler.rs ├── logging.rs ├── main.rs ├── model.rs ├── tui.rs ├── update.rs ├── view.rs └── workers.rs └── worker ├── .clang-format ├── inspector.cc ├── inspector.hh ├── main.cc └── meson.build /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: [-std=c++20, U_FORTIFY_SOURCE, -Denable_cplusplus=ON] 3 | Compiler: gcc 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .direnv 3 | build 4 | .cache 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.1.2 4 | 5 | - Hotfixed a dependency issue with ansi-to-tui which breaks builds 6 | - Adjusted how paths are loaded by default to load cwd if /etc/nixos is unavailable 7 | 8 | ### 0.1.1 9 | 10 | - Fixed bug with exploring items with dots in their name (services.nginx.virtualHosts."example.com".root etc.) 11 | - Added support for moving forward and backward in search and path navigator mode (n = move forward, N = move backward) 12 | - Fixed navigating up in the list shifting the entire view up 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.18" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 46 | 47 | [[package]] 48 | name = "ansi-to-tui" 49 | version = "4.0.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "8438af3d7e7dccdb98eff55e5351587d9bec2294daff505fc9a061bd14d22db0" 52 | dependencies = [ 53 | "nom", 54 | "ratatui", 55 | "simdutf8", 56 | "smallvec", 57 | "thiserror", 58 | ] 59 | 60 | [[package]] 61 | name = "anstream" 62 | version = "0.6.13" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 65 | dependencies = [ 66 | "anstyle", 67 | "anstyle-parse", 68 | "anstyle-query", 69 | "anstyle-wincon", 70 | "colorchoice", 71 | "utf8parse", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle" 76 | version = "1.0.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 79 | 80 | [[package]] 81 | name = "anstyle-parse" 82 | version = "0.2.3" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 85 | dependencies = [ 86 | "utf8parse", 87 | ] 88 | 89 | [[package]] 90 | name = "anstyle-query" 91 | version = "1.0.2" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 94 | dependencies = [ 95 | "windows-sys 0.52.0", 96 | ] 97 | 98 | [[package]] 99 | name = "anstyle-wincon" 100 | version = "3.0.2" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 103 | dependencies = [ 104 | "anstyle", 105 | "windows-sys 0.52.0", 106 | ] 107 | 108 | [[package]] 109 | name = "anyhow" 110 | version = "1.0.82" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" 113 | 114 | [[package]] 115 | name = "autocfg" 116 | version = "1.2.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 119 | 120 | [[package]] 121 | name = "backtrace" 122 | version = "0.3.71" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 125 | dependencies = [ 126 | "addr2line", 127 | "cc", 128 | "cfg-if", 129 | "libc", 130 | "miniz_oxide", 131 | "object", 132 | "rustc-demangle", 133 | ] 134 | 135 | [[package]] 136 | name = "bitflags" 137 | version = "1.3.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 140 | 141 | [[package]] 142 | name = "bitflags" 143 | version = "2.5.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 146 | 147 | [[package]] 148 | name = "cassowary" 149 | version = "0.3.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 152 | 153 | [[package]] 154 | name = "castaway" 155 | version = "0.2.2" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 158 | dependencies = [ 159 | "rustversion", 160 | ] 161 | 162 | [[package]] 163 | name = "cc" 164 | version = "1.0.94" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" 167 | 168 | [[package]] 169 | name = "cfg-if" 170 | version = "1.0.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 173 | 174 | [[package]] 175 | name = "cfg_aliases" 176 | version = "0.1.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 179 | 180 | [[package]] 181 | name = "clap" 182 | version = "4.5.4" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 185 | dependencies = [ 186 | "clap_builder", 187 | "clap_derive", 188 | ] 189 | 190 | [[package]] 191 | name = "clap_builder" 192 | version = "4.5.2" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 195 | dependencies = [ 196 | "anstream", 197 | "anstyle", 198 | "clap_lex", 199 | "strsim", 200 | ] 201 | 202 | [[package]] 203 | name = "clap_derive" 204 | version = "4.5.4" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" 207 | dependencies = [ 208 | "heck 0.5.0", 209 | "proc-macro2", 210 | "quote", 211 | "syn", 212 | ] 213 | 214 | [[package]] 215 | name = "clap_lex" 216 | version = "0.7.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 219 | 220 | [[package]] 221 | name = "color-eyre" 222 | version = "0.6.3" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 225 | dependencies = [ 226 | "backtrace", 227 | "color-spantrace", 228 | "eyre", 229 | "indenter", 230 | "once_cell", 231 | "owo-colors", 232 | "tracing-error", 233 | ] 234 | 235 | [[package]] 236 | name = "color-spantrace" 237 | version = "0.2.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 240 | dependencies = [ 241 | "once_cell", 242 | "owo-colors", 243 | "tracing-core", 244 | "tracing-error", 245 | ] 246 | 247 | [[package]] 248 | name = "colorchoice" 249 | version = "1.0.0" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 252 | 253 | [[package]] 254 | name = "compact_str" 255 | version = "0.7.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 258 | dependencies = [ 259 | "castaway", 260 | "cfg-if", 261 | "itoa", 262 | "ryu", 263 | "static_assertions", 264 | ] 265 | 266 | [[package]] 267 | name = "crossterm" 268 | version = "0.27.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 271 | dependencies = [ 272 | "bitflags 2.5.0", 273 | "crossterm_winapi", 274 | "libc", 275 | "mio", 276 | "parking_lot", 277 | "signal-hook", 278 | "signal-hook-mio", 279 | "winapi", 280 | ] 281 | 282 | [[package]] 283 | name = "crossterm_winapi" 284 | version = "0.9.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 287 | dependencies = [ 288 | "winapi", 289 | ] 290 | 291 | [[package]] 292 | name = "deranged" 293 | version = "0.3.11" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 296 | dependencies = [ 297 | "powerfmt", 298 | ] 299 | 300 | [[package]] 301 | name = "directories" 302 | version = "5.0.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 305 | dependencies = [ 306 | "dirs-sys", 307 | ] 308 | 309 | [[package]] 310 | name = "dirs-sys" 311 | version = "0.4.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 314 | dependencies = [ 315 | "libc", 316 | "option-ext", 317 | "redox_users", 318 | "windows-sys 0.48.0", 319 | ] 320 | 321 | [[package]] 322 | name = "either" 323 | version = "1.11.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 326 | 327 | [[package]] 328 | name = "eyre" 329 | version = "0.6.12" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 332 | dependencies = [ 333 | "indenter", 334 | "once_cell", 335 | ] 336 | 337 | [[package]] 338 | name = "futures-core" 339 | version = "0.3.30" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 342 | 343 | [[package]] 344 | name = "getrandom" 345 | version = "0.2.14" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 348 | dependencies = [ 349 | "cfg-if", 350 | "libc", 351 | "wasi", 352 | ] 353 | 354 | [[package]] 355 | name = "gimli" 356 | version = "0.28.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 359 | 360 | [[package]] 361 | name = "hashbrown" 362 | version = "0.14.3" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 365 | dependencies = [ 366 | "ahash", 367 | "allocator-api2", 368 | ] 369 | 370 | [[package]] 371 | name = "heck" 372 | version = "0.4.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 375 | 376 | [[package]] 377 | name = "heck" 378 | version = "0.5.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 381 | 382 | [[package]] 383 | name = "indenter" 384 | version = "0.3.3" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 387 | 388 | [[package]] 389 | name = "indoc" 390 | version = "2.0.5" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 393 | 394 | [[package]] 395 | name = "itertools" 396 | version = "0.12.1" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 399 | dependencies = [ 400 | "either", 401 | ] 402 | 403 | [[package]] 404 | name = "itoa" 405 | version = "1.0.11" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 408 | 409 | [[package]] 410 | name = "kanal" 411 | version = "0.1.0-pre8" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "b05d55519627edaf7fd0f29981f6dc03fb52df3f5b257130eb8d0bf2801ea1d7" 414 | dependencies = [ 415 | "futures-core", 416 | "lock_api", 417 | ] 418 | 419 | [[package]] 420 | name = "lazy_static" 421 | version = "1.4.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 424 | 425 | [[package]] 426 | name = "libc" 427 | version = "0.2.153" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 430 | 431 | [[package]] 432 | name = "libredox" 433 | version = "0.1.3" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 436 | dependencies = [ 437 | "bitflags 2.5.0", 438 | "libc", 439 | ] 440 | 441 | [[package]] 442 | name = "lock_api" 443 | version = "0.4.11" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 446 | dependencies = [ 447 | "autocfg", 448 | "scopeguard", 449 | ] 450 | 451 | [[package]] 452 | name = "log" 453 | version = "0.4.21" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 456 | 457 | [[package]] 458 | name = "lru" 459 | version = "0.12.3" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" 462 | dependencies = [ 463 | "hashbrown", 464 | ] 465 | 466 | [[package]] 467 | name = "matchers" 468 | version = "0.1.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 471 | dependencies = [ 472 | "regex-automata 0.1.10", 473 | ] 474 | 475 | [[package]] 476 | name = "memchr" 477 | version = "2.7.2" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 480 | 481 | [[package]] 482 | name = "minimal-lexical" 483 | version = "0.2.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 486 | 487 | [[package]] 488 | name = "miniz_oxide" 489 | version = "0.7.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 492 | dependencies = [ 493 | "adler", 494 | ] 495 | 496 | [[package]] 497 | name = "mio" 498 | version = "0.8.11" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 501 | dependencies = [ 502 | "libc", 503 | "log", 504 | "wasi", 505 | "windows-sys 0.48.0", 506 | ] 507 | 508 | [[package]] 509 | name = "nix" 510 | version = "0.28.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 513 | dependencies = [ 514 | "bitflags 2.5.0", 515 | "cfg-if", 516 | "cfg_aliases", 517 | "libc", 518 | ] 519 | 520 | [[package]] 521 | name = "nix-inspect" 522 | version = "0.1.2" 523 | dependencies = [ 524 | "ansi-to-tui", 525 | "anyhow", 526 | "clap", 527 | "color-eyre", 528 | "crossterm", 529 | "directories", 530 | "kanal", 531 | "lazy_static", 532 | "nix", 533 | "parking_lot", 534 | "ratatui", 535 | "serde", 536 | "serde_json", 537 | "tracing", 538 | "tracing-error", 539 | "tracing-subscriber", 540 | ] 541 | 542 | [[package]] 543 | name = "nom" 544 | version = "7.1.3" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 547 | dependencies = [ 548 | "memchr", 549 | "minimal-lexical", 550 | ] 551 | 552 | [[package]] 553 | name = "nu-ansi-term" 554 | version = "0.46.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 557 | dependencies = [ 558 | "overload", 559 | "winapi", 560 | ] 561 | 562 | [[package]] 563 | name = "num-conv" 564 | version = "0.1.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 567 | 568 | [[package]] 569 | name = "num_threads" 570 | version = "0.1.7" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 573 | dependencies = [ 574 | "libc", 575 | ] 576 | 577 | [[package]] 578 | name = "object" 579 | version = "0.32.2" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 582 | dependencies = [ 583 | "memchr", 584 | ] 585 | 586 | [[package]] 587 | name = "once_cell" 588 | version = "1.19.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 591 | 592 | [[package]] 593 | name = "option-ext" 594 | version = "0.2.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 597 | 598 | [[package]] 599 | name = "overload" 600 | version = "0.1.1" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 603 | 604 | [[package]] 605 | name = "owo-colors" 606 | version = "3.5.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 609 | 610 | [[package]] 611 | name = "parking_lot" 612 | version = "0.12.1" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 615 | dependencies = [ 616 | "lock_api", 617 | "parking_lot_core", 618 | ] 619 | 620 | [[package]] 621 | name = "parking_lot_core" 622 | version = "0.9.9" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 625 | dependencies = [ 626 | "cfg-if", 627 | "libc", 628 | "redox_syscall", 629 | "smallvec", 630 | "windows-targets 0.48.5", 631 | ] 632 | 633 | [[package]] 634 | name = "paste" 635 | version = "1.0.14" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 638 | 639 | [[package]] 640 | name = "pin-project-lite" 641 | version = "0.2.14" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 644 | 645 | [[package]] 646 | name = "powerfmt" 647 | version = "0.2.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 650 | 651 | [[package]] 652 | name = "proc-macro2" 653 | version = "1.0.81" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 656 | dependencies = [ 657 | "unicode-ident", 658 | ] 659 | 660 | [[package]] 661 | name = "quote" 662 | version = "1.0.36" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 665 | dependencies = [ 666 | "proc-macro2", 667 | ] 668 | 669 | [[package]] 670 | name = "ratatui" 671 | version = "0.26.2" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" 674 | dependencies = [ 675 | "bitflags 2.5.0", 676 | "cassowary", 677 | "compact_str", 678 | "crossterm", 679 | "indoc", 680 | "itertools", 681 | "lru", 682 | "paste", 683 | "stability", 684 | "strum", 685 | "time", 686 | "unicode-segmentation", 687 | "unicode-width", 688 | ] 689 | 690 | [[package]] 691 | name = "redox_syscall" 692 | version = "0.4.1" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 695 | dependencies = [ 696 | "bitflags 1.3.2", 697 | ] 698 | 699 | [[package]] 700 | name = "redox_users" 701 | version = "0.4.5" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" 704 | dependencies = [ 705 | "getrandom", 706 | "libredox", 707 | "thiserror", 708 | ] 709 | 710 | [[package]] 711 | name = "regex" 712 | version = "1.10.4" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 715 | dependencies = [ 716 | "aho-corasick", 717 | "memchr", 718 | "regex-automata 0.4.6", 719 | "regex-syntax 0.8.3", 720 | ] 721 | 722 | [[package]] 723 | name = "regex-automata" 724 | version = "0.1.10" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 727 | dependencies = [ 728 | "regex-syntax 0.6.29", 729 | ] 730 | 731 | [[package]] 732 | name = "regex-automata" 733 | version = "0.4.6" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 736 | dependencies = [ 737 | "aho-corasick", 738 | "memchr", 739 | "regex-syntax 0.8.3", 740 | ] 741 | 742 | [[package]] 743 | name = "regex-syntax" 744 | version = "0.6.29" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 747 | 748 | [[package]] 749 | name = "regex-syntax" 750 | version = "0.8.3" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 753 | 754 | [[package]] 755 | name = "rustc-demangle" 756 | version = "0.1.23" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 759 | 760 | [[package]] 761 | name = "rustversion" 762 | version = "1.0.15" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" 765 | 766 | [[package]] 767 | name = "ryu" 768 | version = "1.0.17" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 771 | 772 | [[package]] 773 | name = "scopeguard" 774 | version = "1.2.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 777 | 778 | [[package]] 779 | name = "serde" 780 | version = "1.0.198" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" 783 | dependencies = [ 784 | "serde_derive", 785 | ] 786 | 787 | [[package]] 788 | name = "serde_derive" 789 | version = "1.0.198" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" 792 | dependencies = [ 793 | "proc-macro2", 794 | "quote", 795 | "syn", 796 | ] 797 | 798 | [[package]] 799 | name = "serde_json" 800 | version = "1.0.116" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" 803 | dependencies = [ 804 | "itoa", 805 | "ryu", 806 | "serde", 807 | ] 808 | 809 | [[package]] 810 | name = "sharded-slab" 811 | version = "0.1.7" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 814 | dependencies = [ 815 | "lazy_static", 816 | ] 817 | 818 | [[package]] 819 | name = "signal-hook" 820 | version = "0.3.17" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 823 | dependencies = [ 824 | "libc", 825 | "signal-hook-registry", 826 | ] 827 | 828 | [[package]] 829 | name = "signal-hook-mio" 830 | version = "0.2.3" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 833 | dependencies = [ 834 | "libc", 835 | "mio", 836 | "signal-hook", 837 | ] 838 | 839 | [[package]] 840 | name = "signal-hook-registry" 841 | version = "1.4.1" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 844 | dependencies = [ 845 | "libc", 846 | ] 847 | 848 | [[package]] 849 | name = "simdutf8" 850 | version = "0.1.4" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" 853 | 854 | [[package]] 855 | name = "smallvec" 856 | version = "1.13.2" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 859 | 860 | [[package]] 861 | name = "stability" 862 | version = "0.2.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" 865 | dependencies = [ 866 | "quote", 867 | "syn", 868 | ] 869 | 870 | [[package]] 871 | name = "static_assertions" 872 | version = "1.1.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 875 | 876 | [[package]] 877 | name = "strsim" 878 | version = "0.11.1" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 881 | 882 | [[package]] 883 | name = "strum" 884 | version = "0.26.2" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 887 | dependencies = [ 888 | "strum_macros", 889 | ] 890 | 891 | [[package]] 892 | name = "strum_macros" 893 | version = "0.26.2" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 896 | dependencies = [ 897 | "heck 0.4.1", 898 | "proc-macro2", 899 | "quote", 900 | "rustversion", 901 | "syn", 902 | ] 903 | 904 | [[package]] 905 | name = "syn" 906 | version = "2.0.59" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" 909 | dependencies = [ 910 | "proc-macro2", 911 | "quote", 912 | "unicode-ident", 913 | ] 914 | 915 | [[package]] 916 | name = "thiserror" 917 | version = "1.0.58" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 920 | dependencies = [ 921 | "thiserror-impl", 922 | ] 923 | 924 | [[package]] 925 | name = "thiserror-impl" 926 | version = "1.0.58" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 929 | dependencies = [ 930 | "proc-macro2", 931 | "quote", 932 | "syn", 933 | ] 934 | 935 | [[package]] 936 | name = "thread_local" 937 | version = "1.1.8" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 940 | dependencies = [ 941 | "cfg-if", 942 | "once_cell", 943 | ] 944 | 945 | [[package]] 946 | name = "time" 947 | version = "0.3.36" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 950 | dependencies = [ 951 | "deranged", 952 | "libc", 953 | "num-conv", 954 | "num_threads", 955 | "powerfmt", 956 | "serde", 957 | "time-core", 958 | ] 959 | 960 | [[package]] 961 | name = "time-core" 962 | version = "0.1.2" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 965 | 966 | [[package]] 967 | name = "tracing" 968 | version = "0.1.40" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 971 | dependencies = [ 972 | "pin-project-lite", 973 | "tracing-attributes", 974 | "tracing-core", 975 | ] 976 | 977 | [[package]] 978 | name = "tracing-attributes" 979 | version = "0.1.27" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 982 | dependencies = [ 983 | "proc-macro2", 984 | "quote", 985 | "syn", 986 | ] 987 | 988 | [[package]] 989 | name = "tracing-core" 990 | version = "0.1.32" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 993 | dependencies = [ 994 | "once_cell", 995 | "valuable", 996 | ] 997 | 998 | [[package]] 999 | name = "tracing-error" 1000 | version = "0.2.0" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 1003 | dependencies = [ 1004 | "tracing", 1005 | "tracing-subscriber", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "tracing-log" 1010 | version = "0.2.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1013 | dependencies = [ 1014 | "log", 1015 | "once_cell", 1016 | "tracing-core", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "tracing-subscriber" 1021 | version = "0.3.18" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 1024 | dependencies = [ 1025 | "matchers", 1026 | "nu-ansi-term", 1027 | "once_cell", 1028 | "regex", 1029 | "sharded-slab", 1030 | "smallvec", 1031 | "thread_local", 1032 | "tracing", 1033 | "tracing-core", 1034 | "tracing-log", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "unicode-ident" 1039 | version = "1.0.12" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1042 | 1043 | [[package]] 1044 | name = "unicode-segmentation" 1045 | version = "1.11.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1048 | 1049 | [[package]] 1050 | name = "unicode-width" 1051 | version = "0.1.11" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1054 | 1055 | [[package]] 1056 | name = "utf8parse" 1057 | version = "0.2.1" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1060 | 1061 | [[package]] 1062 | name = "valuable" 1063 | version = "0.1.0" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1066 | 1067 | [[package]] 1068 | name = "version_check" 1069 | version = "0.9.4" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1072 | 1073 | [[package]] 1074 | name = "wasi" 1075 | version = "0.11.0+wasi-snapshot-preview1" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1078 | 1079 | [[package]] 1080 | name = "winapi" 1081 | version = "0.3.9" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1084 | dependencies = [ 1085 | "winapi-i686-pc-windows-gnu", 1086 | "winapi-x86_64-pc-windows-gnu", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "winapi-i686-pc-windows-gnu" 1091 | version = "0.4.0" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1094 | 1095 | [[package]] 1096 | name = "winapi-x86_64-pc-windows-gnu" 1097 | version = "0.4.0" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1100 | 1101 | [[package]] 1102 | name = "windows-sys" 1103 | version = "0.48.0" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1106 | dependencies = [ 1107 | "windows-targets 0.48.5", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "windows-sys" 1112 | version = "0.52.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1115 | dependencies = [ 1116 | "windows-targets 0.52.5", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "windows-targets" 1121 | version = "0.48.5" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1124 | dependencies = [ 1125 | "windows_aarch64_gnullvm 0.48.5", 1126 | "windows_aarch64_msvc 0.48.5", 1127 | "windows_i686_gnu 0.48.5", 1128 | "windows_i686_msvc 0.48.5", 1129 | "windows_x86_64_gnu 0.48.5", 1130 | "windows_x86_64_gnullvm 0.48.5", 1131 | "windows_x86_64_msvc 0.48.5", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "windows-targets" 1136 | version = "0.52.5" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1139 | dependencies = [ 1140 | "windows_aarch64_gnullvm 0.52.5", 1141 | "windows_aarch64_msvc 0.52.5", 1142 | "windows_i686_gnu 0.52.5", 1143 | "windows_i686_gnullvm", 1144 | "windows_i686_msvc 0.52.5", 1145 | "windows_x86_64_gnu 0.52.5", 1146 | "windows_x86_64_gnullvm 0.52.5", 1147 | "windows_x86_64_msvc 0.52.5", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "windows_aarch64_gnullvm" 1152 | version = "0.48.5" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1155 | 1156 | [[package]] 1157 | name = "windows_aarch64_gnullvm" 1158 | version = "0.52.5" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1161 | 1162 | [[package]] 1163 | name = "windows_aarch64_msvc" 1164 | version = "0.48.5" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1167 | 1168 | [[package]] 1169 | name = "windows_aarch64_msvc" 1170 | version = "0.52.5" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1173 | 1174 | [[package]] 1175 | name = "windows_i686_gnu" 1176 | version = "0.48.5" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1179 | 1180 | [[package]] 1181 | name = "windows_i686_gnu" 1182 | version = "0.52.5" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1185 | 1186 | [[package]] 1187 | name = "windows_i686_gnullvm" 1188 | version = "0.52.5" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1191 | 1192 | [[package]] 1193 | name = "windows_i686_msvc" 1194 | version = "0.48.5" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1197 | 1198 | [[package]] 1199 | name = "windows_i686_msvc" 1200 | version = "0.52.5" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1203 | 1204 | [[package]] 1205 | name = "windows_x86_64_gnu" 1206 | version = "0.48.5" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1209 | 1210 | [[package]] 1211 | name = "windows_x86_64_gnu" 1212 | version = "0.52.5" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1215 | 1216 | [[package]] 1217 | name = "windows_x86_64_gnullvm" 1218 | version = "0.48.5" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1221 | 1222 | [[package]] 1223 | name = "windows_x86_64_gnullvm" 1224 | version = "0.52.5" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1227 | 1228 | [[package]] 1229 | name = "windows_x86_64_msvc" 1230 | version = "0.48.5" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1233 | 1234 | [[package]] 1235 | name = "windows_x86_64_msvc" 1236 | version = "0.52.5" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1239 | 1240 | [[package]] 1241 | name = "zerocopy" 1242 | version = "0.7.32" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 1245 | dependencies = [ 1246 | "zerocopy-derive", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "zerocopy-derive" 1251 | version = "0.7.32" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 1254 | dependencies = [ 1255 | "proc-macro2", 1256 | "quote", 1257 | "syn", 1258 | ] 1259 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nix-inspect" 3 | version = "0.1.2" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | anyhow = "1.0.79" 8 | crossterm = "0.27.0" 9 | ratatui = { version = "0.26.0", features = ["all-widgets"] } 10 | parking_lot = "0.12.1" 11 | kanal = "0.1.0-pre8" 12 | color-eyre = "0.6.2" 13 | tracing = "0.1.40" 14 | tracing-error = "0.2.0" 15 | directories = "5.0.1" 16 | lazy_static = "1.4.0" 17 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 18 | serde = { version = "1.0.197", features = ["derive"] } 19 | serde_json = "1.0.114" 20 | clap = { version = "4.5.4", features = ["derive"] } 21 | nix = { version = "0.28.0", features = ["hostname"] } 22 | ansi-to-tui = "4.0.0" 23 | 24 | [profile.release] 25 | opt-level = "z" 26 | lto = true 27 | codegen-units = 1 28 | strip = true 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mia Korennykh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nix-inspect 2 | 3 | A ranger-like TUI for inspecting your nixos config and other arbitrary nix expressions. 4 | 5 | ``` 6 | nix run github:bluskript/nix-inspect 7 | ``` 8 | 9 | 10 | https://github.com/bluskript/nix-inspect/assets/52386117/21cfc643-653c-43c8-abf1-d75c07f15b7f 11 | 12 | ### Motivation 13 | 14 | A lot of the time my workflow using nixos would involve running a series of commands to find the final merged result of my config: 15 | ``` 16 | ❯ : nix repl 17 | nix-repl> :lf /etc/nixos 18 | Added 18 variables. 19 | nix-repl> nixosConfigurations.felys.config.home-manager.users.blusk.stylix.fonts.monospace.name 20 | "Cozette" 21 | ``` 22 | 23 | `nix-inspect` aims to improve on this workflow by offering a interactive way to browse a config and offering quality of life features such as bookmarks and a path navigator mode to get where you need quickly. 24 | 25 | ### Features 26 | - 🪡 Path navigator to quickly type in or paste a path which live updates as you type (.) 27 | - Supports tab completion! 28 | - 🔍Fuzzy search in the current directory (Ctrl-F or /) 29 | - 🔖 Bookmarks to save important nix paths, automatically populated with your current system and user (s) 30 | - ⌨️ Vim keybindings (hjkl, ctl+u, ctrl+d) 31 | - (planned) 🕑 Recently visited paths tab 32 | 33 | ### Usage 34 | 35 | By default, `nix-inspect` will try to load your config where it is, by default this will be /etc/nixos if you are using flakes or the path in NIX_PATH if you are using legacy. If this behavior is not what you want, `nix-inspect` comes with some flags: 36 | 37 | - `--expr` / `-e` - load an arbitrary expression. Example: `nix-inspect -e { a = 1; }` 38 | - `--path` / `-p` - load a config at a specific path. Example: `nix-inspect -p /persist/etc/nixos` 39 | 40 | ### Key Bindings 41 | 42 | | Key | Behavior | 43 | | --------------- | ------------------- | 44 | | q | Exit | 45 | | h / left arrow | Navigate up a level | 46 | | j / down arrow | Select lower item | 47 | | k / up arrow | Select upper item | 48 | | l / right arrow | Enter selected item | 49 | | f / "/" | Search | 50 | | ctrl+d | Half-Page Down | 51 | | ctrl+u | Half-Page Up | 52 | | s | Save bookmark | 53 | | . | Path Navigator mode | 54 | | n | Next Search Occurence | 55 | | N | Previous Search Occurence | 56 | 57 | 58 | ### Installation 59 | This project has been added to nixpkgs, but there may have been changes not yet landed there. It is recommended to use nix-inspect as a flake like so: 60 | ```nix 61 | { 62 | inputs = { 63 | nix-inspect.url = "github:bluskript/nix-inspect"; 64 | }; 65 | } 66 | ``` 67 | and then reference it in your `environment.systemPackages`: 68 | ```nix 69 | {inputs, ...}: { 70 | environment.systemPackages = [ 71 | inputs.nix-inspect.packages.default 72 | ]; 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /crates.nix: -------------------------------------------------------------------------------- 1 | {inputs, ...}: { 2 | perSystem = { 3 | pkgs, 4 | config, 5 | ... 6 | }: let 7 | crateName = "nix-inspect"; 8 | workerPackage = pkgs.stdenv.mkDerivation { 9 | name = "worker"; 10 | src = ./worker; 11 | 12 | nativeBuildInputs = with pkgs; [ 13 | meson 14 | ninja 15 | pkg-config 16 | ]; 17 | 18 | buildInputs = with pkgs; [ 19 | boost 20 | nlohmann_json 21 | nixVersions.nix_2_24.dev 22 | ]; 23 | 24 | configurePhase = "meson setup build"; 25 | buildPhase = "ninja -C build"; 26 | 27 | installPhase = '' 28 | mkdir -p $out/bin 29 | cp build/nix-inspect $out/bin/ 30 | ''; 31 | }; 32 | in { 33 | # declare projects 34 | nci.projects."nix-inspect".path = ./.; 35 | # configure crates 36 | nci.crates.${crateName} = { 37 | drvConfig = { 38 | env.WORKER_BINARY_PATH = "${workerPackage}/bin/nix-inspect"; 39 | }; 40 | }; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1727316705, 7 | "narHash": "sha256-/mumx8AQ5xFuCJqxCIOFCHTVlxHkMT21idpbgbm/TIE=", 8 | "owner": "ipetkov", 9 | "repo": "crane", 10 | "rev": "5b03654ce046b5167e7b0bccbd8244cb56c16f0e", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "ipetkov", 15 | "ref": "v0.19.0", 16 | "repo": "crane", 17 | "type": "github" 18 | } 19 | }, 20 | "dream2nix": { 21 | "inputs": { 22 | "nixpkgs": [ 23 | "nci", 24 | "nixpkgs" 25 | ], 26 | "purescript-overlay": "purescript-overlay", 27 | "pyproject-nix": "pyproject-nix" 28 | }, 29 | "locked": { 30 | "lastModified": 1732214960, 31 | "narHash": "sha256-ViyEMSYwaza6y55XTDrsRi2K4YKCLsefMTorjWSE27s=", 32 | "owner": "nix-community", 33 | "repo": "dream2nix", 34 | "rev": "a8dac99db44307fdecead13a39c584b97812d0d4", 35 | "type": "github" 36 | }, 37 | "original": { 38 | "owner": "nix-community", 39 | "repo": "dream2nix", 40 | "type": "github" 41 | } 42 | }, 43 | "flake-compat": { 44 | "flake": false, 45 | "locked": { 46 | "lastModified": 1696426674, 47 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 48 | "owner": "edolstra", 49 | "repo": "flake-compat", 50 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 51 | "type": "github" 52 | }, 53 | "original": { 54 | "owner": "edolstra", 55 | "repo": "flake-compat", 56 | "type": "github" 57 | } 58 | }, 59 | "mk-naked-shell": { 60 | "flake": false, 61 | "locked": { 62 | "lastModified": 1681286841, 63 | "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", 64 | "owner": "yusdacra", 65 | "repo": "mk-naked-shell", 66 | "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "yusdacra", 71 | "repo": "mk-naked-shell", 72 | "type": "github" 73 | } 74 | }, 75 | "nci": { 76 | "inputs": { 77 | "crane": "crane", 78 | "dream2nix": "dream2nix", 79 | "mk-naked-shell": "mk-naked-shell", 80 | "nixpkgs": [ 81 | "nixpkgs" 82 | ], 83 | "parts": "parts", 84 | "rust-overlay": "rust-overlay", 85 | "treefmt": "treefmt" 86 | }, 87 | "locked": { 88 | "lastModified": 1732342495, 89 | "narHash": "sha256-7qfvmnJQByEtl5bS+rTydLCe3Saz9kMRaJxPCdqb1wQ=", 90 | "owner": "yusdacra", 91 | "repo": "nix-cargo-integration", 92 | "rev": "ae9de2d06519a3bb26b649e1c0d1cfa22c20dc0e", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "yusdacra", 97 | "repo": "nix-cargo-integration", 98 | "type": "github" 99 | } 100 | }, 101 | "nixpkgs": { 102 | "locked": { 103 | "lastModified": 1732014248, 104 | "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", 105 | "owner": "nixos", 106 | "repo": "nixpkgs", 107 | "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", 108 | "type": "github" 109 | }, 110 | "original": { 111 | "owner": "nixos", 112 | "ref": "nixos-unstable", 113 | "repo": "nixpkgs", 114 | "type": "github" 115 | } 116 | }, 117 | "parts": { 118 | "inputs": { 119 | "nixpkgs-lib": [ 120 | "nci", 121 | "nixpkgs" 122 | ] 123 | }, 124 | "locked": { 125 | "lastModified": 1730504689, 126 | "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", 127 | "owner": "hercules-ci", 128 | "repo": "flake-parts", 129 | "rev": "506278e768c2a08bec68eb62932193e341f55c90", 130 | "type": "github" 131 | }, 132 | "original": { 133 | "owner": "hercules-ci", 134 | "repo": "flake-parts", 135 | "type": "github" 136 | } 137 | }, 138 | "parts_2": { 139 | "inputs": { 140 | "nixpkgs-lib": [ 141 | "nixpkgs" 142 | ] 143 | }, 144 | "locked": { 145 | "lastModified": 1730504689, 146 | "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", 147 | "owner": "hercules-ci", 148 | "repo": "flake-parts", 149 | "rev": "506278e768c2a08bec68eb62932193e341f55c90", 150 | "type": "github" 151 | }, 152 | "original": { 153 | "owner": "hercules-ci", 154 | "repo": "flake-parts", 155 | "type": "github" 156 | } 157 | }, 158 | "purescript-overlay": { 159 | "inputs": { 160 | "flake-compat": "flake-compat", 161 | "nixpkgs": [ 162 | "nci", 163 | "dream2nix", 164 | "nixpkgs" 165 | ], 166 | "slimlock": "slimlock" 167 | }, 168 | "locked": { 169 | "lastModified": 1728546539, 170 | "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", 171 | "owner": "thomashoneyman", 172 | "repo": "purescript-overlay", 173 | "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", 174 | "type": "github" 175 | }, 176 | "original": { 177 | "owner": "thomashoneyman", 178 | "repo": "purescript-overlay", 179 | "type": "github" 180 | } 181 | }, 182 | "pyproject-nix": { 183 | "flake": false, 184 | "locked": { 185 | "lastModified": 1702448246, 186 | "narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=", 187 | "owner": "davhau", 188 | "repo": "pyproject.nix", 189 | "rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb", 190 | "type": "github" 191 | }, 192 | "original": { 193 | "owner": "davhau", 194 | "ref": "dream2nix", 195 | "repo": "pyproject.nix", 196 | "type": "github" 197 | } 198 | }, 199 | "root": { 200 | "inputs": { 201 | "nci": "nci", 202 | "nixpkgs": "nixpkgs", 203 | "parts": "parts_2" 204 | } 205 | }, 206 | "rust-overlay": { 207 | "inputs": { 208 | "nixpkgs": [ 209 | "nci", 210 | "nixpkgs" 211 | ] 212 | }, 213 | "locked": { 214 | "lastModified": 1732328983, 215 | "narHash": "sha256-RHt12f/slrzDpSL7SSkydh8wUE4Nr4r23HlpWywed9E=", 216 | "owner": "oxalica", 217 | "repo": "rust-overlay", 218 | "rev": "ed8aa5b64f7d36d9338eb1d0a3bb60cf52069a72", 219 | "type": "github" 220 | }, 221 | "original": { 222 | "owner": "oxalica", 223 | "repo": "rust-overlay", 224 | "type": "github" 225 | } 226 | }, 227 | "slimlock": { 228 | "inputs": { 229 | "nixpkgs": [ 230 | "nci", 231 | "dream2nix", 232 | "purescript-overlay", 233 | "nixpkgs" 234 | ] 235 | }, 236 | "locked": { 237 | "lastModified": 1688756706, 238 | "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", 239 | "owner": "thomashoneyman", 240 | "repo": "slimlock", 241 | "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", 242 | "type": "github" 243 | }, 244 | "original": { 245 | "owner": "thomashoneyman", 246 | "repo": "slimlock", 247 | "type": "github" 248 | } 249 | }, 250 | "treefmt": { 251 | "inputs": { 252 | "nixpkgs": [ 253 | "nci", 254 | "nixpkgs" 255 | ] 256 | }, 257 | "locked": { 258 | "lastModified": 1732292307, 259 | "narHash": "sha256-5WSng844vXt8uytT5djmqBCkopyle6ciFgteuA9bJpw=", 260 | "owner": "numtide", 261 | "repo": "treefmt-nix", 262 | "rev": "705df92694af7093dfbb27109ce16d828a79155f", 263 | "type": "github" 264 | }, 265 | "original": { 266 | "owner": "numtide", 267 | "repo": "treefmt-nix", 268 | "type": "github" 269 | } 270 | } 271 | }, 272 | "root": "root", 273 | "version": 7 274 | } 275 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 3 | inputs.nci.url = "github:yusdacra/nix-cargo-integration"; 4 | inputs.nci.inputs.nixpkgs.follows = "nixpkgs"; 5 | inputs.parts.url = "github:hercules-ci/flake-parts"; 6 | inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 7 | 8 | outputs = inputs @ { 9 | parts, 10 | nci, 11 | ... 12 | }: 13 | parts.lib.mkFlake {inherit inputs;} { 14 | systems = ["x86_64-linux" "aarch64-darwin" "aarch64-linux" "i686-linux" "x86_64-darwin"]; 15 | imports = [ 16 | nci.flakeModule 17 | ./crates.nix 18 | ]; 19 | perSystem = { 20 | pkgs, 21 | config, 22 | ... 23 | }: let 24 | crateOutputs = config.nci.outputs."nix-inspect"; 25 | in { 26 | devShells.default = crateOutputs.devShell.overrideAttrs (old: { 27 | WORKER_BINARY_PATH = "./worker/build/nix-inspect"; 28 | packages = 29 | (old.packages or []) 30 | ++ (with pkgs; [ 31 | rust-analyzer 32 | clang-tools 33 | pkg-config 34 | ninja 35 | boost 36 | meson 37 | nlohmann_json 38 | nixVersions.nix_2_24.dev 39 | ]); 40 | }); 41 | packages.default = crateOutputs.packages.release; 42 | }; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /result: -------------------------------------------------------------------------------- 1 | /nix/store/jwc49vkd2a119wj0kk3rqpd42gkh05yq-nix-inspect-0.1.2 -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /src/key_handler.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crossterm::event::{self, KeyCode, KeyModifiers}; 4 | 5 | use crate::model::{InputModel, InputState, Message, Model}; 6 | 7 | pub fn register_key_handler(tx: &kanal::Sender) { 8 | let tx = tx.clone(); 9 | std::thread::spawn(move || -> anyhow::Result<()> { 10 | loop { 11 | if let Ok(true) = event::poll(Duration::from_millis(100)) { 12 | let _ = tx.send(Message::TermEvent(event::read()?)); 13 | } 14 | } 15 | }); 16 | } 17 | 18 | pub fn handle_key(key: event::KeyEvent, model: &Model) -> Option { 19 | if let InputState::Active(state) = &model.search_input { 20 | handle_search_input(state, key) 21 | } else if let InputState::Active(state) = &model.path_navigator_input { 22 | handle_navigator_input(state, key) 23 | } else if let InputState::Active(state) = &model.new_bookmark_input { 24 | handle_bookmark_input(state, key) 25 | } else { 26 | handle_normal_input(key) 27 | } 28 | } 29 | 30 | fn handle_search_input(state: &InputModel, key: event::KeyEvent) -> Option { 31 | if !state.typing { 32 | match key.code { 33 | KeyCode::Char('n') => Some(Message::SearchNext), 34 | KeyCode::Char('N') => Some(Message::SearchPrev), 35 | KeyCode::Esc => Some(Message::SearchExit), 36 | _ => None, 37 | } 38 | } else { 39 | match key.code { 40 | KeyCode::Esc => Some(Message::SearchExit), 41 | _ => Some(Message::SearchInput(key)), 42 | } 43 | } 44 | } 45 | 46 | pub fn handle_bookmark_input(_: &InputModel, key: event::KeyEvent) -> Option { 47 | match key.code { 48 | KeyCode::Esc => Some(Message::BookmarkInputExit), 49 | KeyCode::Enter => Some(Message::CreateBookmark), 50 | _ => Some(Message::BookmarkInput(key)), 51 | } 52 | } 53 | 54 | pub fn handle_navigator_input(state: &InputModel, key: event::KeyEvent) -> Option { 55 | if !state.typing { 56 | match key.code { 57 | KeyCode::Char('n') => Some(Message::NavigatorNext), 58 | KeyCode::Char('N') => Some(Message::NavigatorPrev), 59 | KeyCode::Esc => Some(Message::NavigatorExit), 60 | _ => Some(Message::NavigatorInput(key)), 61 | } 62 | } else { 63 | match key.code { 64 | KeyCode::Esc => Some(Message::NavigatorExit), 65 | _ => Some(Message::NavigatorInput(key)), 66 | } 67 | } 68 | } 69 | 70 | pub fn handle_normal_input(key: event::KeyEvent) -> Option { 71 | match key.code { 72 | KeyCode::Char('q') => Some(Message::Quit), 73 | KeyCode::Char('h') | KeyCode::Left => Some(Message::Back), 74 | KeyCode::Char('j') | KeyCode::Down => Some(Message::ListDown), 75 | KeyCode::Char('k') | KeyCode::Up => Some(Message::ListUp), 76 | KeyCode::Char('l') | KeyCode::Right => Some(Message::EnterItem), 77 | KeyCode::Char('f') | KeyCode::Char('/') => Some(Message::SearchEnter), 78 | KeyCode::Char('s') => Some(Message::BookmarkInputEnter), 79 | KeyCode::Char('r') => Some(Message::Refresh), 80 | KeyCode::Char('d') => { 81 | if key.modifiers.contains(KeyModifiers::CONTROL) { 82 | Some(Message::PageDown) 83 | } else { 84 | Some(Message::DeleteBookmark) 85 | } 86 | } 87 | KeyCode::Char('u') => { 88 | if key.modifiers.contains(KeyModifiers::CONTROL) { 89 | return Some(Message::PageUp); 90 | } 91 | None 92 | } 93 | KeyCode::Char('.') => Some(Message::NavigatorEnter), 94 | _ => None, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use color_eyre::eyre::Result; 4 | use directories::ProjectDirs; 5 | use lazy_static::lazy_static; 6 | use tracing_error::ErrorLayer; 7 | use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer}; 8 | 9 | lazy_static! { 10 | pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); 11 | pub static ref DATA_FOLDER: Option = 12 | std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) 13 | .ok() 14 | .map(PathBuf::from); 15 | pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); 16 | pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); 17 | } 18 | 19 | pub fn project_directory() -> Option { 20 | ProjectDirs::from("dev", "blusk", env!("CARGO_PKG_NAME")) 21 | } 22 | 23 | pub fn get_data_dir() -> PathBuf { 24 | let directory = if let Some(s) = DATA_FOLDER.clone() { 25 | s 26 | } else if let Some(proj_dirs) = project_directory() { 27 | proj_dirs.data_local_dir().to_path_buf() 28 | } else { 29 | PathBuf::from(".").join(".data") 30 | }; 31 | directory 32 | } 33 | 34 | pub fn initialize_logging() -> Result<()> { 35 | let directory = get_data_dir(); 36 | std::fs::create_dir_all(directory.clone())?; 37 | let log_path = directory.join(LOG_FILE.clone()); 38 | let log_file = std::fs::File::create(log_path)?; 39 | std::env::set_var( 40 | "RUST_LOG", 41 | std::env::var("RUST_LOG") 42 | .or_else(|_| std::env::var(LOG_ENV.clone())) 43 | .unwrap_or_else(|_| format!("{}=trace", env!("CARGO_CRATE_NAME"))), 44 | ); 45 | let file_subscriber = tracing_subscriber::fmt::layer() 46 | .with_file(true) 47 | .with_line_number(true) 48 | .with_writer(log_file) 49 | .with_target(false) 50 | .with_ansi(false) 51 | .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); 52 | tracing_subscriber::registry() 53 | .with(file_subscriber) 54 | .with(ErrorLayer::default()) 55 | .init(); 56 | Ok(()) 57 | } 58 | 59 | /// Similar to the `std::dbg!` macro, but generates `tracing` events rather 60 | /// than printing to stdout. 61 | /// 62 | /// By default, the verbosity level for the generated events is `DEBUG`, but 63 | /// this can be customized. 64 | #[macro_export] 65 | macro_rules! trace_dbg { 66 | (target: $target:expr, level: $level:expr, $ex:expr) => {{ 67 | match $ex { 68 | value => { 69 | tracing::event!(target: $target, $level, ?value, stringify!($ex)); 70 | value 71 | } 72 | } 73 | }}; 74 | (level: $level:expr, $ex:expr) => { 75 | trace_dbg!(target: module_path!(), level: $level, $ex) 76 | }; 77 | (target: $target:expr, $ex:expr) => { 78 | trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) 79 | }; 80 | ($ex:expr) => { 81 | trace_dbg!(level: tracing::Level::DEBUG, $ex) 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | fs::{create_dir_all, File}, 4 | io::{stdout, Read}, 5 | path::{Path, PathBuf}, 6 | sync::Arc, 7 | }; 8 | 9 | use clap::Parser; 10 | use color_eyre::eyre::OptionExt; 11 | use crossterm::{ 12 | terminal::{ 13 | disable_raw_mode, enable_raw_mode, size, EnterAlternateScreen, LeaveAlternateScreen, 14 | SetSize, 15 | }, 16 | ExecutableCommand, 17 | }; 18 | use key_handler::register_key_handler; 19 | use logging::{initialize_logging, project_directory}; 20 | use model::{Bookmark, BrowserPath, BrowserStack, BrowserStackItem, Message, Model, RunningState}; 21 | use ratatui::{backend::CrosstermBackend, widgets::ListState, Terminal}; 22 | use serde::{Deserialize, Serialize}; 23 | use update::UpdateContext; 24 | use view::view; 25 | use workers::WorkerHost; 26 | 27 | use crate::view::ViewData; 28 | 29 | pub mod key_handler; 30 | pub mod logging; 31 | pub mod model; 32 | pub mod tui; 33 | pub mod update; 34 | pub mod view; 35 | pub mod workers; 36 | 37 | #[derive(Serialize, Deserialize, Default, Debug, Clone)] 38 | pub struct Config { 39 | bookmarks: Vec, 40 | } 41 | 42 | #[derive(Parser, Debug)] 43 | #[command(version, about, long_about = None)] 44 | struct Args { 45 | #[arg(short, long)] 46 | path: Option, 47 | #[arg(short, long)] 48 | expr: Option, 49 | } 50 | 51 | pub fn find_in_nix_path() -> color_eyre::Result { 52 | let x = env::var("NIX_PATH")?; 53 | Ok(x.split(":") 54 | .filter_map(|x| x.split_once("=")) 55 | .find(|(x, _)| x == &"nixos-config") 56 | .map(|(_, v)| v.to_string()) 57 | .unwrap_or_else(|| ".".to_string())) 58 | } 59 | 60 | fn load_config(args: &Args) -> color_eyre::Result { 61 | if let Some(expr) = &args.expr { 62 | Ok(expr.to_string()) 63 | } else if let Some(path) = &args.path { 64 | let path = Path::new(path).canonicalize()?; 65 | let is_file = path.is_file(); 66 | let is_flake = is_file && path.ends_with("flake.nix") || path.join("flake.nix").exists(); 67 | 68 | Ok(if is_flake { 69 | format!(r#"builtins.getFlake "{}""#, path.display()) 70 | } else { 71 | format!("(import ) {{ system = builtins.currentSystem; configuration = import {}; }}", path.display()) 72 | }) 73 | } else { 74 | let nixos_path = Path::new("/etc/nixos").canonicalize()?; 75 | let etc_nixos_flake = nixos_path.join("flake.nix"); 76 | if etc_nixos_flake.exists() { 77 | Ok(format!(r#"builtins.getFlake "{}""#, nixos_path.display())) 78 | } else { 79 | let path = find_in_nix_path()?; 80 | let path = Path::new(&path) 81 | .canonicalize() 82 | .unwrap_or(".".into()) 83 | .canonicalize()?; 84 | let flake_path = path.join("flake.nix"); 85 | if flake_path.exists() { 86 | Ok(format!(r#"builtins.getFlake "{}""#, path.display())) 87 | } else { 88 | Ok(format!("(import ) {{ system = builtins.currentSystem; configuration = import {}; }}", path.display())) 89 | } 90 | } 91 | } 92 | } 93 | 94 | pub fn read_config(p: PathBuf) -> anyhow::Result { 95 | let config = std::fs::read_to_string(p)?; 96 | let cfg: Config = serde_json::from_str(&config)?; 97 | Ok(cfg) 98 | } 99 | 100 | fn main() -> color_eyre::Result<()> { 101 | let args = Args::parse(); 102 | let (cols, rows) = size()?; 103 | enable_raw_mode()?; 104 | stdout().execute(EnterAlternateScreen)?; 105 | let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; 106 | initialize_logging()?; 107 | tui::install_panic_hook(); 108 | 109 | let project_dirs = &project_directory().unwrap(); 110 | let config_path = project_dirs.config_local_dir().join("config.json"); 111 | let config = if let Ok(c) = read_config(config_path.clone()) { 112 | c 113 | } else { 114 | // Same behavior as nixos-rebuild 115 | let hostname = nix::unistd::gethostname()?; 116 | let hostname = hostname.to_string_lossy(); 117 | let hostname_path = format!(".nixosConfigurations.{}", hostname); 118 | 119 | let user = env::var("USER")?; 120 | let user_path = format!("{hostname_path}.config.home-manager.users.{user}"); 121 | 122 | let config = Config { 123 | bookmarks: vec![ 124 | Bookmark { 125 | display: hostname.to_string(), 126 | path: BrowserPath::from(hostname_path), 127 | }, 128 | Bookmark { 129 | display: user.to_string(), 130 | path: BrowserPath::from(user_path.to_string()), 131 | }, 132 | ], 133 | }; 134 | create_dir_all(config_path.parent().unwrap())?; 135 | let x = serde_json::to_string_pretty(&config)?; 136 | std::fs::write(config_path.clone(), x)?; 137 | 138 | config 139 | }; 140 | 141 | let expr = load_config(&args)?; 142 | tracing::debug!("{}", expr); 143 | 144 | let worker_host = WorkerHost::new(expr); 145 | let mut model = Model { 146 | running_state: RunningState::Running, 147 | visit_stack: BrowserStack(vec![BrowserStackItem::Root]), 148 | root_view_state: ListState::default().with_selected(Some(0)), 149 | bookmark_view_state: ListState::default().with_selected(Some(0)), 150 | config, 151 | ..Default::default() 152 | }; 153 | 154 | let mut update_context = UpdateContext { 155 | req_tx: worker_host.tx.clone(), 156 | config_path, 157 | }; 158 | 159 | let (tx, rx) = kanal::unbounded::(); 160 | register_key_handler(&tx); 161 | 162 | { 163 | let worker_rx = worker_host.rx.clone(); 164 | std::thread::spawn(move || loop { 165 | match worker_rx.recv() { 166 | Ok((p, v)) => { 167 | let _ = tx.send(Message::Data(p, v)); 168 | } 169 | Err(_) => break, 170 | } 171 | }); 172 | } 173 | 174 | while model.running_state != RunningState::Stopped { 175 | // Render the current view 176 | let mut view_data: ViewData = ViewData::default(); 177 | terminal.draw(|f| { 178 | view_data = view(&mut model, f); 179 | })?; 180 | 181 | let mut current_msg = Some(rx.recv()?); 182 | 183 | // Process updates as long as they return a non-None message 184 | while let Some(msg) = current_msg { 185 | tracing::info!("{:?}", msg); 186 | if let Ok(msg) = update_context.update(&view_data, &mut model, msg) { 187 | current_msg = msg; 188 | } else { 189 | current_msg = None; 190 | } 191 | } 192 | } 193 | 194 | stdout().execute(LeaveAlternateScreen)?; 195 | disable_raw_mode()?; 196 | stdout().execute(SetSize(cols, rows))?; 197 | 198 | Ok(()) 199 | } 200 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt, 4 | ops::{Deref, DerefMut}, 5 | }; 6 | 7 | use crossterm::event::{KeyCode, KeyEvent}; 8 | use ratatui::{text::Text, widgets::ListState}; 9 | use serde::{Deserialize, Deserializer, Serialize}; 10 | 11 | use crate::{workers::NixValue, Config}; 12 | 13 | #[derive(Default, Debug)] 14 | pub struct Model { 15 | pub running_state: RunningState, 16 | 17 | pub path_data: PathDataMap, 18 | pub recents: Vec, 19 | 20 | pub config: Config, 21 | 22 | pub visit_stack: BrowserStack, 23 | 24 | pub search_input: InputState, 25 | pub path_navigator_input: InputState, 26 | pub new_bookmark_input: InputState, 27 | 28 | /// TODO: things that the architecture doesnt handle all that well 29 | pub prev_tab_completion: Option, 30 | 31 | pub root_view_state: ListState, 32 | pub bookmark_view_state: ListState, 33 | pub recents_view_state: ListState, 34 | } 35 | 36 | impl Model { 37 | pub fn selected_bookmark(&self) -> Option<&Bookmark> { 38 | self.bookmark_view_state 39 | .selected() 40 | .and_then(|i| self.config.bookmarks.get(i)) 41 | } 42 | 43 | pub fn selected_recent(&self) -> Option<&BrowserPath> { 44 | self.recents_view_state 45 | .selected() 46 | .and_then(|i| self.recents.get(i)) 47 | } 48 | 49 | /// Update the selection of the parent to match the current path 50 | pub fn update_parent_selection(&mut self, current_path: BrowserPath) { 51 | let mut new_stack = vec![]; 52 | let mut path = current_path; 53 | new_stack.push(BrowserStackItem::BrowserPath(path.clone())); 54 | while let Some(parent) = path.parent() { 55 | new_stack.push(BrowserStackItem::BrowserPath(parent.clone())); 56 | if let Some(PathData::List(list)) = self.path_data.get_mut(&parent) { 57 | if let Some(pos) = list.list.iter().position(|x| x == path.0.last().unwrap()) { 58 | list.state.select(Some(pos)); 59 | } 60 | } 61 | path = parent; 62 | } 63 | new_stack.push(BrowserStackItem::Root); 64 | self.root_view_state.select(Some(2)); 65 | new_stack.reverse(); 66 | *self.visit_stack = new_stack; 67 | } 68 | } 69 | 70 | #[derive(Default, Debug)] 71 | pub struct PathDataMap(HashMap); 72 | 73 | impl Deref for PathDataMap { 74 | type Target = HashMap; 75 | 76 | fn deref(&self) -> &Self::Target { 77 | &self.0 78 | } 79 | } 80 | impl DerefMut for PathDataMap { 81 | fn deref_mut(&mut self) -> &mut Self::Target { 82 | &mut self.0 83 | } 84 | } 85 | 86 | impl PathDataMap { 87 | pub fn current_list(&self, current_path: &BrowserPath) -> Option<&ListData> { 88 | self.get(current_path).and_then(|x| match x { 89 | PathData::List(data) => Some(data), 90 | _ => None, 91 | }) 92 | } 93 | pub fn current_list_mut(&mut self, current_path: &BrowserPath) -> Option<&mut ListData> { 94 | self.get_mut(¤t_path).and_then(|x| match x { 95 | PathData::List(data) => Some(data), 96 | _ => None, 97 | }) 98 | } 99 | } 100 | 101 | #[derive(Debug, Default)] 102 | pub struct BrowserStack(pub Vec); 103 | 104 | impl Deref for BrowserStack { 105 | type Target = Vec; 106 | 107 | fn deref(&self) -> &Self::Target { 108 | &self.0 109 | } 110 | } 111 | impl DerefMut for BrowserStack { 112 | fn deref_mut(&mut self) -> &mut Self::Target { 113 | &mut self.0 114 | } 115 | } 116 | 117 | impl BrowserStack { 118 | pub fn push_path(&mut self, path: BrowserPath) { 119 | self.0.push(BrowserStackItem::BrowserPath(path)) 120 | } 121 | pub fn prev_item(&self) -> Option<&BrowserStackItem> { 122 | self.0.len().checked_sub(2).and_then(|i| self.0.get(i)) 123 | } 124 | pub fn current(&self) -> Option<&BrowserPath> { 125 | match self.0.last() { 126 | Some(BrowserStackItem::BrowserPath(p)) => Some(p), 127 | _ => None, 128 | } 129 | } 130 | pub fn current_force(&self) -> &BrowserPath { 131 | match self.0.last() { 132 | Some(BrowserStackItem::BrowserPath(p)) => p, 133 | _ => panic!("current visit stack item is not a path"), 134 | } 135 | } 136 | } 137 | 138 | #[derive(Debug)] 139 | pub enum Message { 140 | TermEvent(crossterm::event::Event), 141 | Data(BrowserPath, PathData), 142 | CurrentPath(BrowserPath), 143 | Refresh, 144 | PageDown, 145 | PageUp, 146 | SearchEnter, 147 | SearchExit, 148 | SearchInput(KeyEvent), 149 | NavigatorEnter, 150 | NavigatorExit, 151 | NavigatorInput(KeyEvent), 152 | BookmarkInputEnter, 153 | BookmarkInputExit, 154 | BookmarkInput(KeyEvent), 155 | CreateBookmark, 156 | DeleteBookmark, 157 | Back, 158 | EnterItem, 159 | ListUp, 160 | ListDown, 161 | SearchNext, 162 | SearchPrev, 163 | NavigatorNext, 164 | NavigatorPrev, 165 | Quit, 166 | } 167 | 168 | #[derive(Debug, Clone)] 169 | pub enum BrowserStackItem { 170 | Root, 171 | Bookmarks, 172 | Recents, 173 | BrowserPath(BrowserPath), 174 | } 175 | 176 | #[derive(Debug, Default, Eq, Hash, PartialEq, Clone)] 177 | pub struct BrowserPath(pub Vec); 178 | 179 | impl Serialize for BrowserPath { 180 | fn serialize(&self, serializer: S) -> Result 181 | where 182 | S: serde::Serializer, 183 | { 184 | serializer.collect_str(&self.to_expr()) 185 | } 186 | } 187 | 188 | impl<'de> Deserialize<'de> for BrowserPath { 189 | fn deserialize(deserializer: D) -> Result 190 | where 191 | D: Deserializer<'de>, 192 | { 193 | let s = String::deserialize(deserializer)?; 194 | Ok(BrowserPath::from(s)) 195 | } 196 | } 197 | 198 | impl BrowserPath { 199 | pub fn parent(&self) -> Option { 200 | if self.0.len() > 1 { 201 | Some(BrowserPath(self.0[..self.0.len() - 1].to_vec())) 202 | } else { 203 | None 204 | } 205 | } 206 | pub fn child(&self, name: String) -> BrowserPath { 207 | let mut clone = self.0.clone(); 208 | clone.push(name); 209 | BrowserPath(clone) 210 | } 211 | pub fn extend(mut self, other: &BrowserPath) -> BrowserPath { 212 | self.0.extend_from_slice(&other.0); 213 | self 214 | } 215 | pub fn to_expr(&self) -> String { 216 | let mut result = String::new(); 217 | 218 | let mut items = self.0.iter().peekable(); 219 | 220 | if let Some(0) = items.peek().map(|x| x.len()) { 221 | items.next(); 222 | } 223 | 224 | for (i, element) in items.enumerate() { 225 | if i > 0 { 226 | result.push('.'); 227 | } 228 | 229 | if element.contains('.') { 230 | result.push('"'); 231 | result.push_str(element); 232 | result.push('"'); 233 | } else { 234 | result.push_str(element); 235 | } 236 | } 237 | 238 | result 239 | } 240 | } 241 | 242 | impl From for BrowserPath { 243 | fn from(value: String) -> Self { 244 | let mut res = Vec::new(); 245 | let mut cur = String::new(); 246 | let mut chars = value.chars().peekable(); 247 | 248 | while let Some(c) = chars.next() { 249 | match c { 250 | '.' => { 251 | res.push(cur); 252 | cur = String::new(); 253 | } 254 | '"' => { 255 | while let Some(inner_c) = chars.next() { 256 | if inner_c == '"' { 257 | break; 258 | } 259 | cur.push(inner_c); 260 | } 261 | } 262 | _ => cur.push(c), 263 | } 264 | } 265 | 266 | res.push(cur); 267 | 268 | BrowserPath(res) 269 | } 270 | } 271 | 272 | #[test] 273 | pub fn test_expr_conversion() { 274 | let path = BrowserPath::from(".".to_string()); 275 | assert_eq!(path.to_expr(), ""); 276 | let path = BrowserPath::from(".nixosConfigurations".to_string()); 277 | assert_eq!(path.to_expr(), "nixosConfigurations"); 278 | let path = BrowserPath::from(r#".nixosConfigurations."example.com""#.to_string()); 279 | assert_eq!(path.to_expr(), r#"nixosConfigurations."example.com""#); 280 | } 281 | 282 | #[derive(Default, Debug, PartialEq, Eq)] 283 | pub enum RunningState { 284 | #[default] 285 | Running, 286 | Stopped, 287 | } 288 | 289 | #[derive(Debug, Clone)] 290 | pub enum ListType { 291 | List, 292 | Attrset, 293 | } 294 | 295 | #[derive(Debug, Clone)] 296 | pub struct ListData { 297 | pub state: ListState, 298 | pub list_type: ListType, 299 | pub list: Vec, 300 | } 301 | 302 | impl ListData { 303 | pub fn selected(&self, current_path: &BrowserPath) -> Option { 304 | self.state 305 | .selected() 306 | .and_then(|i| self.list.get(i)) 307 | .map(|x| current_path.child(x.to_string())) 308 | } 309 | } 310 | 311 | #[derive(Debug, Clone)] 312 | pub enum PathData { 313 | List(ListData), 314 | Thunk, 315 | Int(i64), 316 | Float(f64), 317 | Bool(bool), 318 | String(String), 319 | Path(String), 320 | Null, 321 | Function, 322 | External, 323 | Loading, 324 | Error(String), 325 | } 326 | 327 | impl fmt::Display for PathData { 328 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 329 | match self { 330 | PathData::List(list_data) => write!(f, "{:?}", list_data), 331 | PathData::Thunk => write!(f, "Thunk"), 332 | PathData::Int(value) => write!(f, "{}", value), 333 | PathData::Float(value) => write!(f, "{}", value), 334 | PathData::Bool(value) => write!(f, "{}", value), 335 | PathData::String(value) => write!(f, "\"{}\"", value), 336 | PathData::Path(value) => write!(f, "Path(\"{}\")", value), 337 | PathData::Null => write!(f, "Null"), 338 | PathData::Function => write!(f, "Function"), 339 | PathData::External => write!(f, "External"), 340 | PathData::Loading => write!(f, "Loading"), 341 | PathData::Error(reason) => write!(f, "{}", reason), 342 | } 343 | } 344 | } 345 | 346 | impl From for PathData { 347 | fn from(value: NixValue) -> Self { 348 | match value { 349 | NixValue::Thunk => PathData::Thunk, 350 | NixValue::Int(i) => PathData::Int(i), 351 | NixValue::Float(f) => PathData::Float(f), 352 | NixValue::Bool(b) => PathData::Bool(b), 353 | NixValue::String(s) => PathData::String(s), 354 | NixValue::Path(p) => PathData::Path(p), 355 | NixValue::Null => PathData::Null, 356 | NixValue::Attrs(attrs) => PathData::List(ListData { 357 | list_type: ListType::Attrset, 358 | state: ListState::default().with_selected(Some(0)), 359 | list: attrs, 360 | }), 361 | NixValue::List(size) => PathData::List(ListData { 362 | list_type: ListType::List, 363 | state: ListState::default().with_selected(Some(0)), 364 | list: (0..size).map(|i| format!("{}", i)).collect(), 365 | }), 366 | NixValue::Function => PathData::Function, 367 | NixValue::External => PathData::External, 368 | NixValue::Error(e) => PathData::Error(e), 369 | } 370 | } 371 | } 372 | 373 | impl PathData { 374 | pub fn get_type(&self) -> String { 375 | match self { 376 | PathData::List(data) => match data.list_type { 377 | ListType::Attrset => "Attrset", 378 | ListType::List => "List", 379 | }, 380 | PathData::Thunk => "Thunk", 381 | PathData::Int(_) => "Int", 382 | PathData::Float(_) => "Float", 383 | PathData::Bool(_) => "Bool", 384 | PathData::String(_) => "String", 385 | PathData::Path(_) => "Path", 386 | PathData::Null => "Null", 387 | PathData::Function => "Function", 388 | PathData::External => "External", 389 | PathData::Loading => "Loading", 390 | PathData::Error(_) => "Error", 391 | } 392 | .to_string() 393 | } 394 | } 395 | 396 | #[derive(Debug, Clone, Deserialize, Serialize)] 397 | pub struct Bookmark { 398 | pub display: String, 399 | pub path: BrowserPath, 400 | } 401 | 402 | impl<'a> Into> for Bookmark { 403 | fn into(self) -> Text<'a> { 404 | Text::raw(self.display) 405 | } 406 | } 407 | 408 | #[derive(Debug, Default)] 409 | pub enum InputState { 410 | #[default] 411 | Normal, 412 | Active(InputModel), 413 | } 414 | 415 | #[derive(Debug)] 416 | pub struct InputModel { 417 | pub typing: bool, 418 | pub input: String, 419 | pub cursor_position: usize, 420 | } 421 | 422 | impl InputModel { 423 | pub fn handle_key_event(&mut self, key: KeyEvent) { 424 | match key.code { 425 | KeyCode::Char(c) => { 426 | self.insert(c); 427 | } 428 | KeyCode::Backspace => { 429 | self.backspace(); 430 | } 431 | KeyCode::Left => { 432 | self.move_cursor_left(); 433 | } 434 | KeyCode::Right => { 435 | self.move_cursor_right(); 436 | } 437 | _ => {} 438 | } 439 | } 440 | 441 | pub fn insert(&mut self, c: char) { 442 | self.input.insert(self.cursor_position, c); 443 | self.cursor_position += 1; 444 | } 445 | 446 | pub fn backspace(&mut self) { 447 | if self.cursor_position == 0 { 448 | return; 449 | } 450 | 451 | let current_index = self.cursor_position; 452 | let from_left_to_current_index = current_index - 1; 453 | let before_char_to_delete = self.input.chars().take(from_left_to_current_index); 454 | let after_char_to_delete = self.input.chars().skip(current_index); 455 | self.input = before_char_to_delete.chain(after_char_to_delete).collect(); 456 | self.move_cursor_left(); 457 | } 458 | 459 | pub fn move_cursor_left(&mut self) { 460 | self.cursor_position = self.clamp_cursor(self.cursor_position - 1); 461 | } 462 | 463 | pub fn move_cursor_right(&mut self) { 464 | self.cursor_position = self.clamp_cursor(self.cursor_position + 1); 465 | } 466 | 467 | fn clamp_cursor(&mut self, pos: usize) -> usize { 468 | pos.clamp(0, self.input.len()) 469 | } 470 | } 471 | 472 | pub fn next(i: usize, len: usize) -> usize { 473 | if i >= len - 1 { 474 | 0 475 | } else { 476 | i + 1 477 | } 478 | } 479 | 480 | pub fn select_next(list_state: &mut ListState, len: usize) { 481 | list_state.select(list_state.selected().map(|i| next(i, len)).or(Some(0))); 482 | } 483 | 484 | pub fn prev(i: usize, len: usize) -> usize { 485 | if i == 0 { 486 | len - 1 487 | } else { 488 | i - 1 489 | } 490 | } 491 | 492 | pub fn select_prev(list_state: &mut ListState, len: usize) { 493 | list_state.select(list_state.selected().map(|i| prev(i, len)).or(Some(0))); 494 | } 495 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use crossterm::{ 2 | terminal::{disable_raw_mode, LeaveAlternateScreen}, 3 | ExecutableCommand, 4 | }; 5 | use std::{io::stdout, panic}; 6 | 7 | pub fn install_panic_hook() { 8 | let original_hook = panic::take_hook(); 9 | panic::set_hook(Box::new(move |panic_info| { 10 | stdout().execute(LeaveAlternateScreen).unwrap(); 11 | disable_raw_mode().unwrap(); 12 | original_hook(panic_info); 13 | })); 14 | } 15 | -------------------------------------------------------------------------------- /src/update.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crossterm::event::{self, Event, KeyCode}; 4 | 5 | use crate::{ 6 | key_handler::handle_key, 7 | model::{ 8 | next, prev, select_next, select_prev, Bookmark, BrowserPath, BrowserStackItem, InputModel, 9 | InputState, Message, Model, PathData, RunningState, 10 | }, 11 | view::ViewData, 12 | Config, 13 | }; 14 | 15 | pub struct UpdateContext { 16 | pub req_tx: kanal::Sender, 17 | pub config_path: PathBuf, 18 | } 19 | 20 | pub fn save_config(path: PathBuf, config: Config) { 21 | std::thread::spawn(move || { 22 | let _ = std::fs::write(&path, &serde_json::to_string_pretty(&config).unwrap()); 23 | }); 24 | } 25 | 26 | impl UpdateContext { 27 | pub fn queue_reeval(&self, path: &BrowserPath) { 28 | let path = path.clone(); 29 | let req_tx = self.req_tx.clone(); 30 | std::thread::spawn(move || { 31 | let _ = req_tx.send(path.clone()); 32 | }); 33 | } 34 | 35 | pub fn maybe_reeval_path(&self, path: &BrowserPath, model: &Model) { 36 | if model.path_data.get(&path).is_none() { 37 | let path = path.clone(); 38 | self.queue_reeval(&path); 39 | } 40 | } 41 | 42 | pub fn maybe_reeval_parent(&self, model: &Model) { 43 | if let Some(BrowserStackItem::BrowserPath(path)) = model.visit_stack.prev_item() { 44 | self.maybe_reeval_path(path, model); 45 | } 46 | } 47 | 48 | pub fn maybe_reeval_selection_browser(&self, p: &BrowserPath, model: &Model) { 49 | let current_value = match model.path_data.get(&p) { 50 | Some(x) => x, 51 | None => return, 52 | }; 53 | let list = match current_value { 54 | PathData::List(x) => x, 55 | _ => return, 56 | }; 57 | let selected = match list.selected(&p) { 58 | Some(x) => x, 59 | None => return, 60 | }; 61 | if list.list.contains(selected.0.last().unwrap()) { 62 | self.maybe_reeval_path(&selected, model); 63 | } 64 | } 65 | 66 | pub fn maybe_reeval_current_selection( 67 | &self, 68 | current_location: &BrowserStackItem, 69 | model: &Model, 70 | ) { 71 | match current_location { 72 | BrowserStackItem::BrowserPath(p) => self.maybe_reeval_selection_browser(p, model), 73 | BrowserStackItem::Bookmarks => { 74 | if let Some(b) = model.selected_bookmark() { 75 | self.maybe_reeval_path(&b.path, model); 76 | } 77 | } 78 | BrowserStackItem::Recents => { 79 | if let Some(x) = model.selected_recent() { 80 | self.maybe_reeval_path(&x, model); 81 | } 82 | } 83 | BrowserStackItem::Root => {} 84 | } 85 | } 86 | 87 | pub fn maybe_reeval_selection(&self, model: &Model) { 88 | if let Some(x) = model.visit_stack.last() { 89 | self.maybe_reeval_current_selection(x, model); 90 | } 91 | } 92 | 93 | pub fn update( 94 | &mut self, 95 | view_data: &ViewData, 96 | model: &mut Model, 97 | msg: Message, 98 | ) -> color_eyre::Result> { 99 | match msg { 100 | Message::TermEvent(event) => match event { 101 | Event::Key(key) => { 102 | if key.kind == event::KeyEventKind::Press { 103 | if let Some(msg) = handle_key(key, &model) { 104 | return Ok(Some(msg)); 105 | } 106 | } 107 | } 108 | _ => {} 109 | }, 110 | Message::Data(p, d) => { 111 | let data = d.clone(); 112 | model 113 | .path_data 114 | .entry(p) 115 | .and_modify(|x| match (x, data) { 116 | (PathData::List(p), PathData::List(d)) => { 117 | let cursor = p.state.selected().unwrap_or(0); 118 | p.state.select(Some(cursor.min(d.list.len()).max(0))); 119 | p.list = d.list; 120 | } 121 | x @ _ => *x.0 = x.1, 122 | }) 123 | .or_insert(d.clone()); 124 | self.maybe_reeval_selection(model); 125 | } 126 | Message::CurrentPath(p) => { 127 | model.visit_stack.push(BrowserStackItem::BrowserPath(p)); 128 | self.maybe_reeval_selection(model); 129 | } 130 | Message::Refresh => { 131 | if let Some(path) = model.visit_stack.current() { 132 | self.queue_reeval(path); 133 | if let Some(data) = model.path_data.current_list(path) { 134 | if let Some(path) = data.selected(path) { 135 | self.queue_reeval(&path); 136 | } 137 | } 138 | } 139 | if let Some(BrowserStackItem::BrowserPath(path)) = model.visit_stack.prev_item() { 140 | self.queue_reeval(path); 141 | } 142 | if let Some(path) = model.visit_stack.current() { 143 | self.queue_reeval(path); 144 | } 145 | } 146 | Message::PageUp => { 147 | if let Some(x) = model.visit_stack.current() { 148 | if let Some(list) = model.path_data.current_list_mut(x) { 149 | let cursor = list.state.selected().unwrap_or(0); 150 | list.state.select(Some( 151 | cursor.saturating_sub(view_data.current_list_height.max(1) as usize / 2) 152 | as usize, 153 | )); 154 | } 155 | } 156 | } 157 | Message::PageDown => { 158 | if let Some(x) = model.visit_stack.current() { 159 | if let Some(list) = model.path_data.current_list_mut(x) { 160 | let cursor = list.state.selected().unwrap_or(0); 161 | *list.state.selected_mut() = Some( 162 | (cursor + (view_data.current_list_height.max(1) / 2) as usize) 163 | .max(0) 164 | .min(list.list.len() - 1), 165 | ); 166 | } 167 | } 168 | } 169 | Message::SearchNext | Message::SearchPrev => { 170 | if let InputState::Active(ref mut input_model) = model.search_input { 171 | let current_list = match model.visit_stack.current().cloned() { 172 | Some(x) => match model.path_data.current_list_mut(&x) { 173 | Some(x) => x, 174 | None => return Ok(None), 175 | }, 176 | None => return Ok(None), 177 | }; 178 | let cursor = current_list.state.selected().unwrap_or(0); 179 | if let Some((_, (i, _))) = match msg { 180 | Message::SearchNext => current_list 181 | .list 182 | .iter() 183 | .enumerate() 184 | .skip(cursor + 1) 185 | .closest_item(|(_, x)| x.contains(&input_model.input), 0), 186 | _ => current_list 187 | .list 188 | .iter() 189 | .enumerate() 190 | .take(cursor) 191 | .closest_item(|(_, x)| x.contains(&input_model.input), cursor), 192 | } { 193 | current_list.state.select(Some(i)); 194 | } 195 | self.maybe_reeval_selection(model); 196 | } 197 | } 198 | Message::SearchEnter => { 199 | model.search_input = InputState::Active(InputModel { 200 | typing: true, 201 | input: "".to_string(), 202 | cursor_position: 0, 203 | }); 204 | } 205 | Message::SearchExit => model.search_input = InputState::default(), 206 | Message::SearchInput(ev) => { 207 | if let InputState::Active(ref mut input_model) = model.search_input { 208 | input_model.handle_key_event(ev); 209 | match ev.code { 210 | KeyCode::Char(_) => { 211 | let current_list = match model.visit_stack.current().cloned() { 212 | Some(x) => match model.path_data.current_list_mut(&x) { 213 | Some(x) => x, 214 | None => return Ok(None), 215 | }, 216 | None => return Ok(None), 217 | }; 218 | if let Some((i, _)) = current_list.list.iter().closest_item( 219 | |x| x.contains(&input_model.input), 220 | current_list.state.selected().unwrap_or(0), 221 | ) { 222 | *current_list.state.selected_mut() = Some(i); 223 | } 224 | self.maybe_reeval_selection(model); 225 | } 226 | KeyCode::Esc => return Ok(Some(Message::SearchExit)), 227 | KeyCode::Enter => input_model.typing = false, 228 | _ => {} 229 | } 230 | } 231 | } 232 | Message::BookmarkInputEnter => { 233 | let path_str = model 234 | .visit_stack 235 | .current() 236 | .and_then(|x| model.path_data.current_list(x)) 237 | .and_then(|x| x.list.get(x.state.selected().unwrap_or(0)).cloned()) 238 | .unwrap_or("".to_string()); 239 | model.new_bookmark_input = InputState::Active(InputModel { 240 | typing: false, 241 | cursor_position: path_str.len(), 242 | input: path_str.to_string(), 243 | }) 244 | } 245 | Message::BookmarkInputExit => { 246 | model.new_bookmark_input = InputState::Normal; 247 | } 248 | Message::BookmarkInput(key) => { 249 | if let InputState::Active(ref mut x) = model.new_bookmark_input { 250 | x.handle_key_event(key); 251 | } 252 | } 253 | Message::NavigatorNext | Message::NavigatorPrev => { 254 | if let InputState::Active(ref mut input_model) = model.path_navigator_input { 255 | let path = BrowserPath::from(input_model.input.clone()); 256 | if let Some(parent) = path.parent() { 257 | if let Some(PathData::List(current_list)) = model.path_data.get_mut(&parent) 258 | { 259 | let cursor = current_list.state.selected().unwrap_or(0); 260 | let tab_prefix = path.0.last().unwrap(); 261 | if let Some((_, (i, _))) = match msg { 262 | Message::NavigatorNext => current_list 263 | .list 264 | .iter() 265 | .enumerate() 266 | .skip(cursor + 1) 267 | .closest_item(|(_, x)| x.starts_with(tab_prefix), 0), 268 | _ => current_list 269 | .list 270 | .iter() 271 | .enumerate() 272 | .take(cursor) 273 | .rev() 274 | .closest_item(|(_, x)| x.contains(tab_prefix), cursor), 275 | } { 276 | *current_list.state.selected_mut() = Some(i); 277 | } 278 | self.maybe_reeval_selection(model); 279 | } 280 | } 281 | } 282 | } 283 | Message::NavigatorEnter => { 284 | let current_path = model.visit_stack.current(); 285 | let path_str = ".".to_string() 286 | + ¤t_path 287 | .map(|x| x.to_expr() + if x.0.len() > 1 { "." } else { "" }) 288 | .unwrap_or("nixosConfigurations.".to_string()); 289 | model.path_navigator_input = InputState::Active(InputModel { 290 | typing: true, 291 | cursor_position: path_str.len(), 292 | input: path_str, 293 | }) 294 | } 295 | Message::NavigatorExit => model.path_navigator_input = InputState::default(), 296 | Message::NavigatorInput(ev) => { 297 | if let InputState::Active(ref mut x) = model.path_navigator_input { 298 | x.handle_key_event(ev); 299 | if ev.code != KeyCode::Tab && ev.code != KeyCode::BackTab { 300 | model.prev_tab_completion = None; 301 | } 302 | match ev.code { 303 | KeyCode::Char(_) | KeyCode::Backspace => { 304 | let path = BrowserPath::from(x.input.clone()); 305 | if let Some(new_path) = path.parent() { 306 | self.maybe_reeval_path(&new_path, model); 307 | model.update_parent_selection(new_path); 308 | self.maybe_reeval_parent(model); 309 | self.maybe_reeval_selection(model); 310 | } 311 | if let Some(parent) = path.parent() { 312 | if let Some(PathData::List(parent_list)) = 313 | model.path_data.get_mut(&parent) 314 | { 315 | let tab_prefix = path.0.last().unwrap(); 316 | let nearest_occurrence_index = parent_list 317 | .list 318 | .iter() 319 | .enumerate() 320 | .find(|(_, x)| x.starts_with(tab_prefix)) 321 | .map(|(i, _)| i); 322 | 323 | if let Some(nearest_occurrence_index) = nearest_occurrence_index 324 | { 325 | parent_list.state.select(Some(nearest_occurrence_index)); 326 | } 327 | } 328 | } 329 | } 330 | KeyCode::Tab | KeyCode::BackTab => { 331 | let path = BrowserPath::from(x.input.clone()); 332 | if let Some(parent) = path.parent() { 333 | if let Some(PathData::List(parent_list)) = 334 | model.path_data.get_mut(&parent) 335 | { 336 | let cursor = parent_list.state.selected().unwrap_or(0); 337 | let tab_prefix = model 338 | .prev_tab_completion 339 | .as_ref() 340 | .unwrap_or(path.0.last().unwrap()); 341 | let nearest_occurence_index = if ev.code == KeyCode::Tab { 342 | parent_list 343 | .list 344 | .iter() 345 | .enumerate() 346 | .skip(cursor + 1) 347 | .find(|(_, x)| x.starts_with(tab_prefix)) 348 | .map(|(i, _)| i) 349 | .or_else(|| { 350 | parent_list 351 | .list 352 | .iter() 353 | .enumerate() 354 | .find(|(_, x)| x.starts_with(tab_prefix)) 355 | .map(|(i, _)| i) 356 | }) 357 | } else { 358 | parent_list 359 | .list 360 | .iter() 361 | .enumerate() 362 | .take(parent_list.state.selected().unwrap_or(0)) 363 | .rev() 364 | .find(|(_, x)| x.starts_with(tab_prefix)) 365 | .map(|(i, _)| i) 366 | .or_else(|| { 367 | parent_list 368 | .list 369 | .iter() 370 | .enumerate() 371 | .skip(cursor + 1) 372 | .rev() 373 | .find(|(_, x)| x.starts_with(tab_prefix)) 374 | .map(|(i, _)| i) 375 | }) 376 | }; 377 | 378 | if let Some(nearest_occurrence_index) = nearest_occurence_index 379 | { 380 | if model.prev_tab_completion.is_none() { 381 | model.prev_tab_completion = Some(tab_prefix.clone()); 382 | } 383 | parent_list.state.select(Some(nearest_occurrence_index)); 384 | let nearest_occurrence = 385 | &parent_list.list[nearest_occurrence_index]; 386 | let new_path = 387 | parent.child(nearest_occurrence.to_string()).to_expr(); 388 | x.cursor_position = new_path.len() + 1; 389 | x.input = ".".to_string() + &new_path; 390 | } 391 | } 392 | } 393 | self.maybe_reeval_selection(model); 394 | } 395 | KeyCode::Esc => return Ok(Some(Message::NavigatorExit)), 396 | KeyCode::Enter => x.typing = false, 397 | _ => {} 398 | } 399 | } 400 | } 401 | Message::CreateBookmark => { 402 | if let Some(p) = model.visit_stack.current() { 403 | if let InputState::Active(state) = &model.new_bookmark_input { 404 | let name = &state.input; 405 | let target_path = model 406 | .visit_stack 407 | .current() 408 | .and_then(|x| model.path_data.current_list(x)) 409 | .and_then(|x| x.selected(p)); 410 | 411 | model.config.bookmarks.push(Bookmark { 412 | display: if name.len() > 0 { 413 | name.to_string() 414 | } else { 415 | p.0.last().unwrap_or(&"".to_string()).clone() 416 | }, 417 | path: target_path.unwrap_or(p.clone()), 418 | }); 419 | model.new_bookmark_input = InputState::Normal; 420 | save_config(self.config_path.clone(), model.config.clone()); 421 | } 422 | } 423 | } 424 | Message::DeleteBookmark => { 425 | if let Some(i) = model.bookmark_view_state.selected() { 426 | model.config.bookmarks.remove(i); 427 | let bookmarks_len = model.config.bookmarks.len(); 428 | let selected = model.bookmark_view_state.selected_mut(); 429 | let new = selected.map(|x| x.min(bookmarks_len - 1)); 430 | *selected = new; 431 | } 432 | save_config(self.config_path.clone(), model.config.clone()); 433 | } 434 | Message::Back => { 435 | if model.visit_stack.len() > 1 { 436 | model.visit_stack.pop(); 437 | self.maybe_reeval_selection(model); 438 | } 439 | } 440 | Message::EnterItem => match model.visit_stack.last().unwrap_or(&BrowserStackItem::Root) 441 | { 442 | BrowserStackItem::Root => { 443 | match model.root_view_state.selected() { 444 | Some(0) => { 445 | model.visit_stack.push(BrowserStackItem::Bookmarks); 446 | self.maybe_reeval_current_selection( 447 | &BrowserStackItem::Bookmarks, 448 | model, 449 | ); 450 | } 451 | Some(1) => { 452 | model.visit_stack.push(BrowserStackItem::Recents); 453 | self.maybe_reeval_current_selection(&BrowserStackItem::Recents, model); 454 | } 455 | Some(2) => { 456 | let x = BrowserPath::from("".to_string()); 457 | self.maybe_reeval_selection_browser(&x, model); 458 | model.visit_stack.push_path(x); 459 | } 460 | _ => unreachable!(), 461 | }; 462 | } 463 | BrowserStackItem::BrowserPath(p) => { 464 | if let Some(selected_item) = model 465 | .path_data 466 | .current_list(&p) 467 | .and_then(|list| list.state.selected().and_then(|i| list.list.get(i))) 468 | { 469 | let x = p.child(selected_item.clone()); 470 | self.maybe_reeval_selection_browser(&x, model); 471 | model.visit_stack.push_path(x); 472 | } 473 | } 474 | BrowserStackItem::Bookmarks => { 475 | if let Some(x) = model.selected_bookmark() { 476 | self.maybe_reeval_selection_browser(&x.path, model); 477 | model.visit_stack.push_path(x.path.clone()); 478 | } 479 | } 480 | BrowserStackItem::Recents => { 481 | if let Some(x) = model.selected_recent() { 482 | self.maybe_reeval_selection_browser(&x, model); 483 | model.visit_stack.push_path(x.clone()); 484 | } 485 | } 486 | }, 487 | Message::ListUp => { 488 | let x = model.visit_stack.last().unwrap_or(&BrowserStackItem::Root); 489 | match x { 490 | BrowserStackItem::Root => { 491 | select_prev(&mut model.root_view_state, 3); 492 | } 493 | BrowserStackItem::BrowserPath(p) => { 494 | if let Some(list) = model.path_data.current_list_mut(&p) { 495 | let cursor = list.state.selected().unwrap_or(0); 496 | list.state.select(Some(prev(cursor, list.list.len()))); 497 | } 498 | } 499 | BrowserStackItem::Bookmarks => { 500 | select_prev(&mut model.bookmark_view_state, model.config.bookmarks.len()); 501 | } 502 | BrowserStackItem::Recents => { 503 | select_prev(&mut model.recents_view_state, model.recents.len()); 504 | } 505 | } 506 | self.maybe_reeval_current_selection(&x, model); 507 | } 508 | Message::ListDown => { 509 | let x = model.visit_stack.last().unwrap_or(&BrowserStackItem::Root); 510 | match x { 511 | BrowserStackItem::Root => { 512 | select_next(&mut model.root_view_state, 3); 513 | if let Some(1) = model.root_view_state.selected() { 514 | let req_tx = self.req_tx.clone(); 515 | std::thread::spawn(move || { 516 | let _ = req_tx.send(BrowserPath::from("".to_string())); 517 | }); 518 | } 519 | } 520 | BrowserStackItem::BrowserPath(p) => { 521 | if let Some(list) = model.path_data.current_list_mut(&p) { 522 | let cursor = list.state.selected().unwrap_or(0); 523 | list.state.select(Some(next(cursor, list.list.len()))); 524 | let selected = list.selected(&p); 525 | if let Some(selected) = selected { 526 | if model.path_data.get(&selected).is_none() { 527 | let _ = self.req_tx.send(selected); 528 | } 529 | } 530 | } 531 | self.maybe_reeval_selection(model); 532 | } 533 | BrowserStackItem::Bookmarks => { 534 | select_next(&mut model.bookmark_view_state, model.config.bookmarks.len()); 535 | } 536 | BrowserStackItem::Recents => { 537 | select_next(&mut model.recents_view_state, model.recents.len()); 538 | } 539 | } 540 | self.maybe_reeval_current_selection(&x, model); 541 | } 542 | Message::Quit => model.running_state = RunningState::Stopped, 543 | }; 544 | Ok(None) 545 | } 546 | } 547 | 548 | /// Returns the closest item to a specific index 549 | /// Used for search in lists to make it not jump around as much 550 | trait ClosestItem { 551 | type Item; 552 | 553 | fn closest_item

(self, predicate: P, pos: usize) -> Option<(usize, Self::Item)> 554 | where 555 | Self: Sized, 556 | P: FnMut(&Self::Item) -> bool; 557 | } 558 | 559 | impl ClosestItem for I 560 | where 561 | I: Iterator, 562 | { 563 | type Item = I::Item; 564 | 565 | fn closest_item

(self, mut predicate: P, pos: usize) -> Option<(usize, Self::Item)> 566 | where 567 | P: FnMut(&Self::Item) -> bool, 568 | { 569 | self.enumerate() 570 | .filter(|(_, x)| predicate(x)) 571 | .min_by(|(i, _), (j, _)| { 572 | let cmp_i = pos as i32 - *i as i32; 573 | let cmp_j = pos as i32 - *j as i32; 574 | cmp_i.abs().cmp(&cmp_j.abs()) 575 | }) 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | use ansi_to_tui::IntoText; 2 | use lazy_static::lazy_static; 3 | use ratatui::layout::Flex; 4 | use ratatui::text::Text; 5 | use ratatui::widgets::{Clear, Widget, Wrap}; 6 | use ratatui::Frame; 7 | 8 | use ratatui::{ 9 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 10 | style::{Color, Style, Stylize}, 11 | symbols, 12 | text::{Line, Span}, 13 | widgets::{Block, Borders, List, ListItem, Paragraph}, 14 | }; 15 | 16 | use crate::model::{ 17 | BrowserPath, BrowserStackItem, InputState, ListData, Model, PathData, PathDataMap, 18 | }; 19 | 20 | /// View data that should be provided to the update handler (for page-up / page-down behavior) 21 | #[derive(Default)] 22 | pub struct ViewData { 23 | pub current_list_height: u16, 24 | } 25 | 26 | pub fn view(model: &mut Model, f: &mut Frame) -> ViewData { 27 | let path_rect = Layout::default() 28 | .direction(Direction::Vertical) 29 | .constraints(vec![Constraint::Length(1), Constraint::Fill(1)]) 30 | .split(f.size()); 31 | let miller_layout = Layout::default() 32 | .direction(Direction::Horizontal) 33 | .constraints(vec![ 34 | Constraint::Percentage(20), 35 | Constraint::Fill(1), 36 | Constraint::Percentage(30), 37 | ]) 38 | .split(path_rect[1]); 39 | 40 | let previous_list_block = 41 | Block::default().borders(Borders::TOP | Borders::BOTTOM | Borders::LEFT); 42 | let previous_inner = previous_list_block.inner(miller_layout[0]); 43 | f.render_widget(previous_list_block, miller_layout[0]); 44 | 45 | render_previous_stack(model, f, previous_inner); 46 | 47 | let mut view_data = ViewData::default(); 48 | 49 | let path = model 50 | .visit_stack 51 | .iter() 52 | .map(|x| match x { 53 | BrowserStackItem::BrowserPath(p) => p.0.last().unwrap_or(&"".to_string()).to_owned(), 54 | BrowserStackItem::Root => "Root".to_string(), 55 | BrowserStackItem::Recents => "Recents".to_string(), 56 | BrowserStackItem::Bookmarks => "Bookmarks".to_string(), 57 | }) 58 | .collect::>() 59 | .join(" > "); 60 | 61 | let path_rect = path_rect[0]; 62 | 63 | f.render_widget( 64 | Paragraph::new(&path[path.len().saturating_sub(path_rect.width as usize)..]) 65 | .alignment(Alignment::Left), 66 | Rect::new(path_rect.x + 1, path_rect.y, path_rect.width - 1, 1), 67 | ); 68 | 69 | match model 70 | .visit_stack 71 | .last() 72 | .cloned() 73 | .unwrap_or(BrowserStackItem::Root) 74 | { 75 | BrowserStackItem::BrowserPath(p) => match model.path_data.get_mut(&p) { 76 | Some(data) if !matches!(data, PathData::List(_)) => { 77 | let block = Block::new() 78 | .borders(Borders::ALL) 79 | .border_set(symbols::border::Set { 80 | top_left: symbols::line::NORMAL.horizontal_down, 81 | bottom_left: symbols::line::NORMAL.horizontal_up, 82 | ..symbols::border::PLAIN 83 | }) 84 | .title_style(Style::new().blue()) 85 | .title(data.get_type()); 86 | let outer = miller_layout[2].union(miller_layout[1]); 87 | let inner = block.inner(outer); 88 | view_data.current_list_height = inner.height; 89 | f.render_widget(block, outer); 90 | let _ = render_value_preview(f, data, inner); 91 | } 92 | x @ _ => { 93 | let current_list_block = current_frame(); 94 | let inner = current_list_block.inner(miller_layout[1]); 95 | view_data.current_list_height = inner.height; 96 | f.render_widget(current_list_block, miller_layout[1]); 97 | if let Some(PathData::List(current_path_data)) = x { 98 | let _ = render_list( 99 | f, 100 | current_path_data, 101 | inner, 102 | Some(&model.search_input), 103 | Some(&model.path_navigator_input), 104 | &model.prev_tab_completion, 105 | ); 106 | } 107 | let _ = render_preview(f, model, miller_layout[2], &p); 108 | } 109 | }, 110 | x @ _ => { 111 | let current_list_block = current_frame(); 112 | let current_inner = current_list_block.inner(miller_layout[1]); 113 | view_data.current_list_height = current_inner.height; 114 | f.render_widget(current_list_block, miller_layout[1]); 115 | 116 | let preview_frame = preview_frame(); 117 | let preview_inner = preview_frame.inner(miller_layout[2]); 118 | f.render_widget(preview_frame, miller_layout[2]); 119 | 120 | match x { 121 | BrowserStackItem::Root => { 122 | render_root(model, f, current_inner); 123 | 124 | match model.root_view_state.selected() { 125 | // Bookmarks 126 | Some(0) => { 127 | render_bookmarks(model, f, preview_inner); 128 | } 129 | Some(1) => { 130 | render_recents(model, f, preview_inner); 131 | } 132 | // Root 133 | Some(2) => { 134 | if let Some(PathData::List(current_list_data)) = 135 | model.path_data.get_mut(&BrowserPath::from("".to_string())) 136 | { 137 | render_list( 138 | f, 139 | current_list_data, 140 | preview_inner, 141 | Some(&model.search_input), 142 | Some(&model.path_navigator_input), 143 | &model.prev_tab_completion, 144 | ); 145 | } 146 | } 147 | _ => {} 148 | } 149 | } 150 | BrowserStackItem::Bookmarks => { 151 | render_bookmarks(model, f, current_inner); 152 | 153 | let selected_bookmark_index = model.bookmark_view_state.selected(); 154 | 155 | if let Some(bookmark) = 156 | selected_bookmark_index.and_then(|i| model.config.bookmarks.get(i)) 157 | { 158 | let path = bookmark.path.clone(); 159 | if let Some(data) = model.path_data.get_mut(&path) { 160 | render_value_preview(f, data, preview_inner); 161 | } 162 | } 163 | } 164 | BrowserStackItem::Recents => { 165 | render_recents(model, f, current_inner); 166 | let selected_recent_index = model.recents_view_state.selected(); 167 | if let Some(path) = selected_recent_index.and_then(|i| model.recents.get(i)) { 168 | let path = path.clone(); 169 | if let Some(data) = model.path_data.get_mut(&path) { 170 | render_value_preview(f, data, preview_inner); 171 | } 172 | } 173 | } 174 | BrowserStackItem::BrowserPath(_) => unreachable!(), 175 | } 176 | } 177 | } 178 | 179 | let rect = f.size(); 180 | render_bottom( 181 | f, 182 | model, 183 | Rect::new(rect.x + 1, rect.y + 1, rect.width - 1, rect.height - 1), 184 | ); 185 | 186 | view_data 187 | } 188 | 189 | pub fn render_previous_stack(model: &mut Model, f: &mut Frame, inner: Rect) { 190 | match model.visit_stack.prev_item() { 191 | Some(BrowserStackItem::BrowserPath(p)) => { 192 | render_previous_list(f, &mut model.path_data, inner, p) 193 | } 194 | Some(BrowserStackItem::Bookmarks) => render_bookmarks(model, f, inner), 195 | Some(BrowserStackItem::Root) => render_root(model, f, inner), 196 | Some(BrowserStackItem::Recents) => render_recents(model, f, inner), 197 | None => {} 198 | } 199 | } 200 | 201 | pub fn with_selected_style(x: List) -> List { 202 | x.highlight_symbol(">>").highlight_style(*SELECTED_STYLE) 203 | } 204 | 205 | pub fn render_root(model: &mut Model, f: &mut Frame, inner: Rect) { 206 | f.render_stateful_widget( 207 | with_selected_style(List::new(["Bookmarks", "Recents", "Root"])), 208 | inner, 209 | &mut model.root_view_state, 210 | ); 211 | } 212 | 213 | pub fn render_recents(model: &mut Model, f: &mut Frame, inner: Rect) { 214 | f.render_stateful_widget( 215 | with_selected_style(List::new(model.recents.iter().map(|x| x.to_expr()))), 216 | inner, 217 | &mut model.recents_view_state, 218 | ) 219 | } 220 | 221 | pub fn render_bookmarks(model: &mut Model, f: &mut Frame, inner: Rect) { 222 | f.render_stateful_widget( 223 | with_selected_style(List::new(model.config.bookmarks.clone())), 224 | inner, 225 | &mut model.bookmark_view_state, 226 | ) 227 | } 228 | 229 | pub fn current_frame<'a>() -> Block<'a> { 230 | Block::default() 231 | .borders(Borders::ALL) 232 | .border_set(symbols::border::Set { 233 | top_left: symbols::line::NORMAL.horizontal_down, 234 | top_right: symbols::line::NORMAL.horizontal_down, 235 | bottom_left: symbols::line::NORMAL.horizontal_up, 236 | bottom_right: symbols::line::NORMAL.horizontal_up, 237 | ..symbols::border::PLAIN 238 | }) 239 | } 240 | 241 | lazy_static! { 242 | pub static ref SELECTED_STYLE: ratatui::style::Style = 243 | Style::default().bg(Color::Yellow).fg(Color::Black); 244 | } 245 | 246 | pub fn render_list( 247 | f: &mut Frame, 248 | list: &mut ListData, 249 | inner: Rect, 250 | search_input: Option<&InputState>, 251 | path_navigator_input: Option<&InputState>, 252 | prev_tab_completion: &Option, 253 | ) { 254 | let selected_style = *SELECTED_STYLE; 255 | let render_list: Vec<_> = list 256 | .list 257 | .iter() 258 | .enumerate() 259 | .map(|(i, x)| { 260 | let highlight_style = if Some(i) == list.state.selected() { 261 | selected_style 262 | } else { 263 | Style::default() 264 | }; 265 | match (path_navigator_input, search_input) { 266 | (Some(_), Some(InputState::Active(search_model))) => { 267 | ListItem::new(highlight_on_match(x.as_str(), search_model.input.as_str())) 268 | .style(highlight_style) 269 | } 270 | (Some(InputState::Active(nav_model)), Some(_)) => { 271 | let search_str = prev_tab_completion 272 | .as_deref() 273 | .or_else(|| nav_model.input.split('.').last()) 274 | .filter(|x| !x.is_empty()); 275 | ListItem::new(x.as_str()).style(search_str.map_or( 276 | highlight_style, 277 | |search_str| { 278 | if x.starts_with(search_str) { 279 | Style::default().on_green().fg(Color::Black) 280 | } else { 281 | highlight_style 282 | } 283 | }, 284 | )) 285 | } 286 | _ => ListItem::new(x.clone()).style(highlight_style), 287 | } 288 | }) 289 | .collect(); 290 | 291 | f.render_stateful_widget(List::new(render_list), inner, &mut list.state); 292 | } 293 | 294 | /// TODO: unify with other list code 295 | pub fn render_previous_list( 296 | f: &mut Frame, 297 | path_data: &mut PathDataMap, 298 | inner: Rect, 299 | p: &BrowserPath, 300 | ) { 301 | let list = match path_data.get_mut(&p) { 302 | Some(PathData::List(list)) => list, 303 | _ => return, 304 | }; 305 | 306 | f.render_stateful_widget( 307 | with_selected_style(List::new(list.list.clone())), 308 | inner, 309 | &mut list.state, 310 | ); 311 | } 312 | 313 | pub fn render_keymap(model: &Model, f: &mut Frame, rect: Rect) { 314 | let typing = match (&model.path_navigator_input, &model.search_input) { 315 | (InputState::Active(m), _) => Some(m.typing), 316 | (_, InputState::Active(m)) => Some(m.typing), 317 | _ => None, 318 | }; 319 | let keymap: &[(&str, &str)] = match typing { 320 | Some(true) => &[("", "Confirm"), ("", "Exit Search")], 321 | Some(false) => &[ 322 | ("n", "Next Occurence"), 323 | ("N", "Previous Occurence"), 324 | ("", "Exit Search"), 325 | ], 326 | None => &[ 327 | (".", "Go To Path"), 328 | ("/", "Find"), 329 | ("r", "Refresh"), 330 | ("s", "Save Bookmark"), 331 | ("d", "Delete Bookmark"), 332 | ("q", "Quit"), 333 | ("", "Half-page down"), 334 | ("", "Half-page up"), 335 | ], 336 | }; 337 | let texts = keymap 338 | .iter() 339 | .map(|(key, text)| { 340 | [ 341 | key.black().on_gray(), 342 | Span::from(format!(" {text} ")).fg(Color::default()), 343 | ] 344 | }) 345 | .flatten() 346 | .collect::>(); 347 | let paragraph = Paragraph::new(Line::from(texts)).alignment(Alignment::Center); 348 | f.render_widget(paragraph, rect); 349 | } 350 | 351 | pub fn render_input<'a>(f: &mut Frame, text: impl Into>, rect: Rect) { 352 | Clear.render(rect, f.buffer_mut()); 353 | f.render_widget( 354 | Paragraph::new(text) 355 | .alignment(Alignment::Left) 356 | .fg(Color::Gray) 357 | .bg(Color::default()), 358 | rect, 359 | ); 360 | } 361 | 362 | pub fn render_bottom(f: &mut Frame, model: &Model, inner: Rect) { 363 | // Offset from the bottom, in case there are two parallel inputs being displayed 364 | let mut offset = 1; 365 | 366 | render_keymap( 367 | model, 368 | f, 369 | Rect::new(inner.left(), inner.bottom() - offset, inner.width, 1), 370 | ); 371 | 372 | offset += 1; 373 | 374 | // Render the search string in the bottom right corner of the container 375 | if let InputState::Active(search_model) = &model.search_input { 376 | let render_text = format!("Search: {}", search_model.input.clone()); 377 | // ratatui does not have a concept of a "right overflow" to my understanding, so clip the 378 | // text from the left manually if it starts overflowing 379 | let render_text = &render_text[render_text.len().saturating_sub(inner.width as usize)..]; 380 | 381 | render_input( 382 | f, 383 | render_text, 384 | Rect::new(inner.left(), inner.bottom() - offset, inner.width, 1), 385 | ); 386 | offset += 1; 387 | } 388 | if let InputState::Active(navigator_state) = &model.path_navigator_input { 389 | let render_text = format!("Goto: {}", navigator_state.input.clone()); 390 | let render_text = &render_text[render_text.len().saturating_sub(inner.width as usize)..]; 391 | render_input( 392 | f, 393 | render_text, 394 | Rect::new(inner.left(), inner.bottom() - offset, inner.width, 1), 395 | ); 396 | offset += 1; 397 | } 398 | 399 | if let InputState::Active(bookmark_input_state) = &model.new_bookmark_input { 400 | let render_text = format!("bookmark name: {}", bookmark_input_state.input.clone()); 401 | let render_text = &render_text[render_text.len().saturating_sub(inner.width as usize)..]; 402 | render_input( 403 | f, 404 | render_text, 405 | Rect::new(inner.left(), inner.bottom() - offset, inner.width, 1), 406 | ); 407 | offset += 1; 408 | } 409 | } 410 | 411 | pub fn render_value_preview(f: &mut Frame, path_data: &mut PathData, inner: Rect) { 412 | match path_data { 413 | // NixValue::Attrs(list) => { 414 | // let items = list.iter().map(|(k, _v)| { 415 | // model 416 | // .values 417 | // .get(&path.child(k.clone())) 418 | // .map(|x| { 419 | // let value_type = x.value.get_preview_symbol(); 420 | // let highlight_color = color_from_type(&x.value); 421 | // ListItem::new(format!("{: ^5} {} = {}", value_type, k, x.value)) 422 | // .fg(highlight_color) 423 | // }) 424 | // .unwrap_or(ListItem::new(format!("? {}", k))) 425 | // }); 426 | // f.render_widget(List::new(items), inner); 427 | // } 428 | // NixValue::List(ref list) => { 429 | // let items = list.iter().map(|x| format!("{:?}", x)).collect::>(); 430 | // f.render_widget( 431 | // List::new(items).style(Style::new().fg(color_from_type(&value))), 432 | // inner, 433 | // ); 434 | // } 435 | PathData::List(list) => { 436 | render_list(f, list, inner, None, None, &None); 437 | } 438 | _ => { 439 | let value = path_data.to_string(); 440 | let value = value.into_text().unwrap_or(value.to_string().into()); 441 | f.render_widget( 442 | Paragraph::new(value) 443 | .style(Style::new().fg(color_from_type(path_data))) 444 | .wrap(Wrap { trim: true }), 445 | inner, 446 | ); 447 | } 448 | } 449 | } 450 | 451 | pub fn preview_frame<'a>() -> Block<'a> { 452 | Block::new() 453 | .borders(Borders::RIGHT | Borders::TOP | Borders::BOTTOM) 454 | .title_style(Style::new().blue()) 455 | } 456 | 457 | pub fn render_preview(f: &mut Frame, model: &mut Model, outer: Rect, current_path: &BrowserPath) { 458 | let mut block = preview_frame(); 459 | 460 | let selected_path = model 461 | .path_data 462 | .current_list(¤t_path) 463 | .and_then(|list| list.selected(¤t_path)); 464 | 465 | if let Some(selected_path) = selected_path { 466 | if let Some(value) = model.path_data.get_mut(&selected_path) { 467 | block = block.title(value.get_type()); 468 | let inner = block.inner(outer); 469 | f.render_widget(block, outer); 470 | render_value_preview(f, value, inner); 471 | return; 472 | } 473 | } 474 | 475 | f.render_widget(block, outer); 476 | } 477 | 478 | fn color_from_type(value: &PathData) -> Color { 479 | match value { 480 | // PathData::Attrs(_) => Color::Yellow, 481 | PathData::List(_) => Color::Cyan, 482 | PathData::Int(_) | PathData::Float(_) => Color::LightBlue, 483 | PathData::String(_) => Color::LightRed, 484 | PathData::Path(_) => Color::Rgb(187, 159, 252), 485 | PathData::Bool(_) => Color::Green, 486 | PathData::Function => Color::Magenta, 487 | PathData::Thunk => Color::LightMagenta, 488 | PathData::Error(_) => Color::Red, 489 | _ => Color::default(), 490 | } 491 | } 492 | 493 | fn highlight_on_match<'a>(haystack: &'a str, needle: &'a str) -> Line<'a> { 494 | let mut spans = Vec::new(); 495 | let mut last_index = 0; 496 | 497 | for (index, _) in haystack.match_indices(needle) { 498 | if index > last_index { 499 | spans.push(Span::raw(&haystack[last_index..index])); 500 | } 501 | spans.push(Span::styled( 502 | needle, 503 | Style::new().fg(Color::Black).bg(Color::Blue), 504 | )); 505 | last_index = index + needle.len(); 506 | } 507 | 508 | if last_index < haystack.len() { 509 | spans.push(Span::raw(&haystack[last_index..])); 510 | } 511 | 512 | Line::from(spans) 513 | } 514 | -------------------------------------------------------------------------------- /src/workers.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::io::{BufRead, BufReader, Write}; 3 | use std::process::{Command, Stdio}; 4 | 5 | use crate::model::{BrowserPath, PathData}; 6 | 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "type", content = "data")] 9 | pub enum NixValue { 10 | #[serde(rename = "0")] 11 | Thunk, 12 | #[serde(rename = "1")] 13 | Int(i64), 14 | #[serde(rename = "2")] 15 | Float(f64), 16 | #[serde(rename = "3")] 17 | Bool(bool), 18 | #[serde(rename = "4")] 19 | String(String), 20 | #[serde(rename = "5")] 21 | Path(String), 22 | #[serde(rename = "6")] 23 | Null, 24 | #[serde(rename = "7")] 25 | Attrs(Vec), 26 | #[serde(rename = "8")] 27 | List(usize), 28 | #[serde(rename = "9")] 29 | Function, 30 | #[serde(rename = "10")] 31 | External, 32 | #[serde(rename = "11")] 33 | Error(String), 34 | } 35 | 36 | pub const WORKER_BINARY_PATH: &str = env!("WORKER_BINARY_PATH"); 37 | 38 | pub struct WorkerHost { 39 | pub tx: kanal::Sender, 40 | pub rx: kanal::Receiver<(BrowserPath, PathData)>, 41 | } 42 | 43 | impl WorkerHost { 44 | pub fn new(expr: String) -> WorkerHost { 45 | let (tx, rx) = kanal::unbounded::(); 46 | let (result_tx, result_rx) = kanal::unbounded(); 47 | 48 | let rx = rx.clone(); 49 | let result_tx = result_tx.clone(); 50 | std::thread::spawn(move || { 51 | let mut child = Command::new(WORKER_BINARY_PATH) 52 | .stdin(Stdio::piped()) 53 | .stdout(Stdio::piped()) 54 | // .stderr(Stdio::piped()) 55 | .spawn() 56 | .expect("Failed to spawn worker"); 57 | 58 | let mut stdin = child.stdin.take().expect("Failed to open stdin"); 59 | let stdout = child.stdout.take().expect("Failed to open stdout"); 60 | let mut reader = BufReader::new(stdout); 61 | 62 | let _ = writeln!(stdin, "{}", expr); 63 | 64 | loop { 65 | let received = rx.recv(); 66 | tracing::info!("{:?}", received); 67 | match received { 68 | Ok(path) => { 69 | result_tx 70 | .send((path.clone(), PathData::Loading)) 71 | .expect("Failed to send loading state"); 72 | if let Err(e) = writeln!(stdin, "{}", path.to_expr()) { 73 | tracing::error!("Failed to send path, {e}"); 74 | break; 75 | } 76 | 77 | let mut response = String::new(); 78 | if let Err(e) = reader.read_line(&mut response) { 79 | tracing::error!("Failed to read response: {e}"); 80 | let _ = result_tx.send(( 81 | path, 82 | PathData::Error(format!("Failed to read response: {e}")), 83 | )); 84 | continue; 85 | } 86 | 87 | let value: NixValue = match serde_json::from_str(&response) { 88 | Ok(v) => v, 89 | Err(e) => { 90 | tracing::error!("{response}"); 91 | tracing::error!("Failed to deserialize response: {e}"); 92 | let _ = result_tx.send(( 93 | path, 94 | PathData::Error(format!("Failed to deserialize response: {e}")), 95 | )); 96 | continue; 97 | } 98 | }; 99 | 100 | result_tx 101 | .send((path, value.into())) 102 | .expect("Failed to send result"); 103 | } 104 | Err(_) => { 105 | // Channel closed, exit the loop 106 | break; 107 | } 108 | } 109 | } 110 | 111 | child.kill().expect("killing child failed"); 112 | }); 113 | 114 | WorkerHost { tx, rx: result_rx } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /worker/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | AlignAfterOpenBracket: BlockIndent 3 | -------------------------------------------------------------------------------- /worker/inspector.cc: -------------------------------------------------------------------------------- 1 | #include "inspector.hh" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "logging.hh" 20 | 21 | const auto MAX_SIZE = 32768; 22 | 23 | NixInspector::NixInspector(std::string expr) 24 | : state(getEvalState().get_ptr().get()), 25 | vRoot(*state->allocValue()), 26 | autoArgs(*state->buildBindings(0).finish()) { 27 | // auto attrs = state->buildBindings(1); 28 | // Value* root = state->allocValue(); 29 | state->eval( 30 | state->parseExprFromString(expr, state->rootPath(CanonPath::root)), vRoot 31 | ); 32 | // attrs.insert(state->symbols.create("root"), root); 33 | // vRoot.mkAttrs(attrs.finish()); 34 | } 35 | 36 | // void NixInspector::initEnv() { 37 | // env = &state->allocEnv(MAX_SIZE); 38 | // env->up = &state->baseEnv; 39 | // displ = 0; 40 | // static_env->vars.clear(); 41 | // static_env->sort(); 42 | // } 43 | 44 | // void NixInspector::addAttrsToScope(Value &attrs) { 45 | // // state->forceAttrs( 46 | // // attrs, [&]() { return attrs.determinePos(noPos); }, 47 | // // "while evaluating an attribute set to be merged in the global 48 | // scope"); if (displ + attrs.attrs->size() >= MAX_SIZE) 49 | // throw Error("environment full; cannot add more variables"); 50 | // 51 | // for (auto i : *attrs.attrs) { 52 | // static_env->vars.emplace_back(i.name, displ); 53 | // env->values[displ++] = i.value; 54 | // } 55 | // static_env->sort(); 56 | // static_env->deduplicate(); 57 | // } 58 | // 59 | // Value NixInspector::_eval(std::string path) { 60 | // Value vRes; 61 | // auto expr = state->parseExprFromString( 62 | // path, state->rootPath(CanonPath::root), static_env 63 | // ); 64 | // 65 | // expr->eval(*state, *env, vRes); 66 | // 67 | // return vRes; 68 | // } 69 | 70 | std::shared_ptr NixInspector::inspect(std::string &attrPath) { 71 | // if (attrPath.length() == 0) { 72 | // attrPath = "root"; 73 | // } else { 74 | // attrPath = "root." + attrPath; 75 | // } 76 | Value &v( 77 | *findAlongAttrPath(*state, std::string(attrPath), autoArgs, vRoot).first 78 | ); 79 | state->forceValue(v, v.determinePos(noPos)); 80 | Value vRes; 81 | state->autoCallFunction(autoArgs, v, vRes); 82 | return std::make_shared(vRes); 83 | } 84 | 85 | int32_t NixInspector::v_int(const Value &value) { return value.integer(); } 86 | float_t NixInspector::v_float(const Value &value) { return value.fpoint(); } 87 | bool NixInspector::v_bool(const Value &value) { return value.boolean(); } 88 | std::string NixInspector::v_string(const Value &value) { 89 | return std::string(value.string_view()); 90 | } 91 | std::string NixInspector::v_path(const Value &value) { 92 | return value.path().path.c_str(); 93 | } 94 | nlohmann::json NixInspector::v_repr(const Value &value) { 95 | switch (value.type()) { 96 | case nix::nAttrs: { 97 | auto collected = std::vector(); 98 | for (auto x : *value.attrs()) { 99 | auto name = state->symbols[x.name]; 100 | collected.push_back(std::string(name)); 101 | } 102 | return collected; 103 | } 104 | case nix::nList: { 105 | return value.listSize(); 106 | } 107 | case nix::nString: 108 | return value.string_view(); 109 | case nix::nPath: 110 | return value.path().path.c_str(); 111 | case nix::nBool: 112 | return value.boolean(); 113 | case nix::nFloat: 114 | return value.fpoint(); 115 | case nix::nInt: 116 | return value.integer(); 117 | case nix::nNull: 118 | return nullptr; 119 | case nix::nExternal: 120 | case nix::nThunk: 121 | case nix::nFunction: 122 | return nullptr; 123 | } 124 | return nullptr; 125 | } 126 | 127 | // std::vector NixInspector::v_attrs(const Value &value) { 128 | // auto collected = std::vector(); 129 | // for (auto x : *value.attrs) { 130 | // auto name = state->symbols[x.name]; 131 | // auto value = std::make_shared(*x.value); 132 | // collected.push_back(NixAttr{.key = (std::string)name, .value = value}); 133 | // ; 134 | // } 135 | // return collected; 136 | // } 137 | std::unique_ptr> NixInspector::v_list(const Value &value) { 138 | auto collected = std::vector(); 139 | for (auto x : value.listItems()) { 140 | collected.emplace_back(*x); 141 | } 142 | return std::make_unique>(collected); 143 | } 144 | void init_nix_inspector() { 145 | nix::initNix(); 146 | nix::initGC(); 147 | nix::flake::initLib(nix::flakeSettings); 148 | logger = new CaptureLogger(); 149 | } 150 | ValueType NixInspector::v_type(const Value &value) { return value.type(); } 151 | 152 | // Gets a attribute at a specific name and if the passed value is a thunk it 153 | // evaluates it SAFETY: this function only safe to call if the value being 154 | // passed is an attrset or a thunk that results in an attrset 155 | std::shared_ptr NixInspector::v_child( 156 | const Value &value, std::string key 157 | ) { 158 | auto x = value.attrs()->get(state->symbols.create(std::string(key))); 159 | Value vRes; 160 | state->forceValue(*x->value, x->value->determinePos(noPos)); 161 | vRes = *x->value; 162 | return std::make_shared(vRes); 163 | } 164 | -------------------------------------------------------------------------------- /worker/inspector.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "command.hh" 9 | #include "eval.hh" 10 | #include "nixexpr.hh" 11 | #include "types.hh" 12 | #include "value.hh" 13 | 14 | using Value = nix::Value; 15 | struct NixInspector; 16 | 17 | using namespace nix; 18 | 19 | // using ValueType = nix::ValueType; 20 | 21 | // struct NixAttr { 22 | // rust::String key; 23 | // std::unique_ptr value; 24 | // }; 25 | 26 | // Capture all logs to an internal stream 27 | // TODO: expose this stream of data in the UI 28 | class CaptureLogger : public Logger { 29 | std::ostringstream oss; 30 | 31 | public: 32 | CaptureLogger() {} 33 | 34 | std::string get() const { return oss.str(); } 35 | 36 | void log(Verbosity lvl, std::string_view s) override { 37 | oss << s << std::endl; 38 | } 39 | 40 | void logEI(const ErrorInfo &ei) override { 41 | showErrorInfo(oss, ei, loggerSettings.showTrace.get()); 42 | } 43 | }; 44 | 45 | // nix is designed with command-line use in mind, and there's some setup stuff 46 | // that's tied to the EvalCommand class. 47 | struct NixInspector : virtual EvalCommand { 48 | public: 49 | EvalState *state; 50 | Value &vRoot; 51 | Bindings &autoArgs; 52 | 53 | NixInspector(std::string expr); 54 | void addAttrsToScope(Value &attrs); 55 | ref getEvalStore(); 56 | 57 | std::shared_ptr inspect(std::string &attrPaths); 58 | ValueType v_type(const Value &value); 59 | int32_t v_int(const Value &value); 60 | float_t v_float(const Value &value); 61 | bool v_bool(const Value &value); 62 | std::string v_string(const Value &value); 63 | std::string v_path(const Value &value); 64 | // std::vector v_attrs(const Value &value); 65 | std::unique_ptr> v_list(const Value &value); 66 | std::shared_ptr v_child(const Value &value, std::string key); 67 | nlohmann::json v_repr(const Value &value); 68 | 69 | void run(ref store) override { 70 | // so it doesn't complain about unused variables 71 | (void)store; 72 | } 73 | }; 74 | 75 | void init_nix_inspector(); 76 | -------------------------------------------------------------------------------- /worker/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "inspector.hh" 7 | 8 | int main() { 9 | init_nix_inspector(); 10 | std::string expr; 11 | getline(std::cin, expr); 12 | auto inspector = NixInspector(expr); 13 | std::string data; 14 | while (getline(std::cin, data)) { 15 | try { 16 | auto value = inspector.inspect(data); 17 | nlohmann::json out = { 18 | {"type", std::to_string(value->type())}, 19 | {"data", inspector.v_repr(*value)} 20 | }; 21 | std::cout << out << std::endl; 22 | } catch (const std::exception& ex) { 23 | nlohmann::json out = {{"type", "11"}, {"data", ex.what()}}; 24 | std::cout << out << std::endl; 25 | } catch (...) { 26 | std::cout << "error" << std::endl; 27 | } 28 | } 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /worker/meson.build: -------------------------------------------------------------------------------- 1 | project( 'nix-inspect' 2 | , ['c', 'cpp'] 3 | , default_options : ['cpp_std=gnu++20'] 4 | , version: 'nightly' 5 | ) 6 | 7 | config_h = configuration_data() 8 | 9 | configure_file( 10 | output: 'nix-inspect-config.h', 11 | configuration: config_h, 12 | ) 13 | 14 | cpp = meson.get_compiler('cpp') 15 | 16 | add_project_arguments([ 17 | '-I' + meson.project_build_root(), 18 | ], language: 'cpp') 19 | 20 | pkgconfig = import('pkgconfig') 21 | 22 | nix_all = [ dependency('nix-expr') 23 | , dependency('nix-cmd') 24 | , dependency('nix-store') 25 | , dependency('nix-main') 26 | , dependency('nix-flake') 27 | ] 28 | 29 | nix_inspect = executable( 30 | 'nix-inspect', 31 | ['main.cc', 'inspector.cc'], 32 | dependencies: [ 33 | nix_all, 34 | ] 35 | , install: true 36 | ) 37 | --------------------------------------------------------------------------------