├── .github └── workflows │ ├── build.yml │ └── gh-pages.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile.toml ├── README.md ├── _config.yml ├── img ├── demo.gif └── demo.tape ├── rustfmt.toml └── src ├── cargo.rs ├── config.rs ├── event.rs ├── main.rs ├── matcher.rs ├── tui.rs └── util.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | prepare: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | msrv: ${{ steps.get-msrv.outputs.msrv }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Get MSRV 22 | id: get-msrv 23 | run: | 24 | MSRV=$(grep '^rust-version' Cargo.toml | cut -d '"' -f 2) 25 | echo "msrv=$MSRV" >> $GITHUB_OUTPUT 26 | build: 27 | needs: prepare 28 | strategy: 29 | matrix: 30 | versions: 31 | - stable 32 | - ${{ needs.prepare.outputs.msrv }} 33 | os: 34 | - ubuntu-24.04 35 | - macos-14 36 | - windows-2022 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: dtolnay/rust-toolchain@master 41 | with: 42 | toolchain: ${{ matrix.versions }} 43 | components: rustfmt, clippy 44 | - uses: Swatinem/rust-cache@v2 45 | - name: Version 46 | run: | 47 | rustc --version 48 | cargo --version 49 | cargo fmt -- --version 50 | cargo clippy -- --version 51 | - name: Format 52 | run: cargo fmt --all -- --check 53 | - name: Build 54 | run: cargo build --verbose 55 | - name: Lint 56 | run: cargo clippy --all-targets --all-features -- -D warnings 57 | - name: Run tests 58 | run: cargo test --verbose 59 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - README.md 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: false 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Setup Pages 27 | uses: actions/configure-pages@v4 28 | - name: Build with Jekyll 29 | uses: actions/jekyll-build-pages@v1 30 | with: 31 | source: ./ 32 | destination: ./_site 33 | - name: Upload artifact 34 | uses: actions/upload-pages-artifact@v3 35 | 36 | deploy: 37 | environment: 38 | name: github-pages 39 | url: ${{ steps.deployment.outputs.page_url }} 40 | runs-on: ubuntu-latest 41 | needs: build 42 | steps: 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v4 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "allocator-api2" 16 | version = "0.2.20" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys 0.59.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys 0.59.0", 67 | ] 68 | 69 | [[package]] 70 | name = "autocfg" 71 | version = "1.4.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 74 | 75 | [[package]] 76 | name = "bitflags" 77 | version = "2.6.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 80 | dependencies = [ 81 | "serde", 82 | ] 83 | 84 | [[package]] 85 | name = "camino" 86 | version = "1.1.9" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 89 | dependencies = [ 90 | "serde", 91 | ] 92 | 93 | [[package]] 94 | name = "cargo-platform" 95 | version = "0.1.8" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" 98 | dependencies = [ 99 | "serde", 100 | ] 101 | 102 | [[package]] 103 | name = "cargo-selector" 104 | version = "0.9.0" 105 | dependencies = [ 106 | "cargo_metadata", 107 | "clap", 108 | "console", 109 | "fuzzy-matcher", 110 | "laurier", 111 | "ratatui", 112 | "rstest", 113 | "serde", 114 | "toml", 115 | "tui-input", 116 | "umbra", 117 | ] 118 | 119 | [[package]] 120 | name = "cargo_metadata" 121 | version = "0.19.2" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" 124 | dependencies = [ 125 | "camino", 126 | "cargo-platform", 127 | "semver", 128 | "serde", 129 | "serde_json", 130 | "thiserror", 131 | ] 132 | 133 | [[package]] 134 | name = "cassowary" 135 | version = "0.3.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 138 | 139 | [[package]] 140 | name = "castaway" 141 | version = "0.2.3" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 144 | dependencies = [ 145 | "rustversion", 146 | ] 147 | 148 | [[package]] 149 | name = "cfg-if" 150 | version = "1.0.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 153 | 154 | [[package]] 155 | name = "clap" 156 | version = "4.5.32" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" 159 | dependencies = [ 160 | "clap_builder", 161 | "clap_derive", 162 | ] 163 | 164 | [[package]] 165 | name = "clap_builder" 166 | version = "4.5.32" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" 169 | dependencies = [ 170 | "anstream", 171 | "anstyle", 172 | "clap_lex", 173 | "strsim", 174 | ] 175 | 176 | [[package]] 177 | name = "clap_derive" 178 | version = "4.5.32" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 181 | dependencies = [ 182 | "heck", 183 | "proc-macro2", 184 | "quote", 185 | "syn", 186 | ] 187 | 188 | [[package]] 189 | name = "clap_lex" 190 | version = "0.7.4" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 193 | 194 | [[package]] 195 | name = "colorchoice" 196 | version = "1.0.3" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 199 | 200 | [[package]] 201 | name = "compact_str" 202 | version = "0.8.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 205 | dependencies = [ 206 | "castaway", 207 | "cfg-if", 208 | "itoa", 209 | "rustversion", 210 | "ryu", 211 | "serde", 212 | "static_assertions", 213 | ] 214 | 215 | [[package]] 216 | name = "console" 217 | version = "0.15.11" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 220 | dependencies = [ 221 | "encode_unicode", 222 | "libc", 223 | "once_cell", 224 | "unicode-width 0.2.0", 225 | "windows-sys 0.59.0", 226 | ] 227 | 228 | [[package]] 229 | name = "crossterm" 230 | version = "0.28.1" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 233 | dependencies = [ 234 | "bitflags", 235 | "crossterm_winapi", 236 | "mio", 237 | "parking_lot", 238 | "rustix", 239 | "signal-hook", 240 | "signal-hook-mio", 241 | "winapi", 242 | ] 243 | 244 | [[package]] 245 | name = "crossterm_winapi" 246 | version = "0.9.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 249 | dependencies = [ 250 | "winapi", 251 | ] 252 | 253 | [[package]] 254 | name = "darling" 255 | version = "0.20.10" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 258 | dependencies = [ 259 | "darling_core", 260 | "darling_macro", 261 | ] 262 | 263 | [[package]] 264 | name = "darling_core" 265 | version = "0.20.10" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 268 | dependencies = [ 269 | "fnv", 270 | "ident_case", 271 | "proc-macro2", 272 | "quote", 273 | "strsim", 274 | "syn", 275 | ] 276 | 277 | [[package]] 278 | name = "darling_macro" 279 | version = "0.20.10" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 282 | dependencies = [ 283 | "darling_core", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "diff" 290 | version = "0.1.13" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 293 | 294 | [[package]] 295 | name = "either" 296 | version = "1.13.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 299 | 300 | [[package]] 301 | name = "encode_unicode" 302 | version = "1.0.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 305 | 306 | [[package]] 307 | name = "equivalent" 308 | version = "1.0.1" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 311 | 312 | [[package]] 313 | name = "errno" 314 | version = "0.3.9" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 317 | dependencies = [ 318 | "libc", 319 | "windows-sys 0.52.0", 320 | ] 321 | 322 | [[package]] 323 | name = "fnv" 324 | version = "1.0.7" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 327 | 328 | [[package]] 329 | name = "foldhash" 330 | version = "0.1.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" 333 | 334 | [[package]] 335 | name = "futures-core" 336 | version = "0.3.31" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 339 | 340 | [[package]] 341 | name = "futures-macro" 342 | version = "0.3.31" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 345 | dependencies = [ 346 | "proc-macro2", 347 | "quote", 348 | "syn", 349 | ] 350 | 351 | [[package]] 352 | name = "futures-task" 353 | version = "0.3.31" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 356 | 357 | [[package]] 358 | name = "futures-timer" 359 | version = "3.0.3" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 362 | 363 | [[package]] 364 | name = "futures-util" 365 | version = "0.3.31" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 368 | dependencies = [ 369 | "futures-core", 370 | "futures-macro", 371 | "futures-task", 372 | "pin-project-lite", 373 | "pin-utils", 374 | "slab", 375 | ] 376 | 377 | [[package]] 378 | name = "fuzzy-matcher" 379 | version = "0.3.7" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 382 | dependencies = [ 383 | "thread_local", 384 | ] 385 | 386 | [[package]] 387 | name = "glob" 388 | version = "0.3.2" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 391 | 392 | [[package]] 393 | name = "hashbrown" 394 | version = "0.15.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" 397 | dependencies = [ 398 | "allocator-api2", 399 | "equivalent", 400 | "foldhash", 401 | ] 402 | 403 | [[package]] 404 | name = "heck" 405 | version = "0.5.0" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 408 | 409 | [[package]] 410 | name = "hermit-abi" 411 | version = "0.3.9" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 414 | 415 | [[package]] 416 | name = "ident_case" 417 | version = "1.0.1" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 420 | 421 | [[package]] 422 | name = "indexmap" 423 | version = "2.7.1" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 426 | dependencies = [ 427 | "equivalent", 428 | "hashbrown", 429 | ] 430 | 431 | [[package]] 432 | name = "indoc" 433 | version = "2.0.5" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 436 | 437 | [[package]] 438 | name = "instability" 439 | version = "0.3.3" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" 442 | dependencies = [ 443 | "darling", 444 | "indoc", 445 | "pretty_assertions", 446 | "proc-macro2", 447 | "quote", 448 | "syn", 449 | ] 450 | 451 | [[package]] 452 | name = "is_terminal_polyfill" 453 | version = "1.70.1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 456 | 457 | [[package]] 458 | name = "itertools" 459 | version = "0.13.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 462 | dependencies = [ 463 | "either", 464 | ] 465 | 466 | [[package]] 467 | name = "itoa" 468 | version = "1.0.13" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" 471 | 472 | [[package]] 473 | name = "laurier" 474 | version = "0.1.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "eab6c93df4d64a8b50f6fd7b9320b36a0bb434b9fdb85b6bf9473f8686f0081a" 477 | dependencies = [ 478 | "ratatui", 479 | ] 480 | 481 | [[package]] 482 | name = "libc" 483 | version = "0.2.164" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" 486 | 487 | [[package]] 488 | name = "linux-raw-sys" 489 | version = "0.4.14" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 492 | 493 | [[package]] 494 | name = "lock_api" 495 | version = "0.4.12" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 498 | dependencies = [ 499 | "autocfg", 500 | "scopeguard", 501 | ] 502 | 503 | [[package]] 504 | name = "log" 505 | version = "0.4.22" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 508 | 509 | [[package]] 510 | name = "lru" 511 | version = "0.12.5" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 514 | dependencies = [ 515 | "hashbrown", 516 | ] 517 | 518 | [[package]] 519 | name = "memchr" 520 | version = "2.7.4" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 523 | 524 | [[package]] 525 | name = "mio" 526 | version = "1.0.2" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 529 | dependencies = [ 530 | "hermit-abi", 531 | "libc", 532 | "log", 533 | "wasi", 534 | "windows-sys 0.52.0", 535 | ] 536 | 537 | [[package]] 538 | name = "once_cell" 539 | version = "1.20.2" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 542 | 543 | [[package]] 544 | name = "parking_lot" 545 | version = "0.12.3" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 548 | dependencies = [ 549 | "lock_api", 550 | "parking_lot_core", 551 | ] 552 | 553 | [[package]] 554 | name = "parking_lot_core" 555 | version = "0.9.10" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 558 | dependencies = [ 559 | "cfg-if", 560 | "libc", 561 | "redox_syscall", 562 | "smallvec", 563 | "windows-targets", 564 | ] 565 | 566 | [[package]] 567 | name = "paste" 568 | version = "1.0.15" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 571 | 572 | [[package]] 573 | name = "pin-project-lite" 574 | version = "0.2.16" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 577 | 578 | [[package]] 579 | name = "pin-utils" 580 | version = "0.1.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 583 | 584 | [[package]] 585 | name = "pretty_assertions" 586 | version = "1.4.1" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 589 | dependencies = [ 590 | "diff", 591 | "yansi", 592 | ] 593 | 594 | [[package]] 595 | name = "proc-macro-crate" 596 | version = "3.3.0" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 599 | dependencies = [ 600 | "toml_edit", 601 | ] 602 | 603 | [[package]] 604 | name = "proc-macro2" 605 | version = "1.0.91" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "307e3004becf10f5a6e0d59d20f3cd28231b0e0827a96cd3e0ce6d14bc1e4bb3" 608 | dependencies = [ 609 | "unicode-ident", 610 | ] 611 | 612 | [[package]] 613 | name = "quote" 614 | version = "1.0.37" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 617 | dependencies = [ 618 | "proc-macro2", 619 | ] 620 | 621 | [[package]] 622 | name = "ratatui" 623 | version = "0.29.0" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 626 | dependencies = [ 627 | "bitflags", 628 | "cassowary", 629 | "compact_str", 630 | "crossterm", 631 | "indoc", 632 | "instability", 633 | "itertools", 634 | "lru", 635 | "paste", 636 | "serde", 637 | "strum", 638 | "unicode-segmentation", 639 | "unicode-truncate", 640 | "unicode-width 0.2.0", 641 | ] 642 | 643 | [[package]] 644 | name = "redox_syscall" 645 | version = "0.5.7" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 648 | dependencies = [ 649 | "bitflags", 650 | ] 651 | 652 | [[package]] 653 | name = "regex" 654 | version = "1.11.1" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 657 | dependencies = [ 658 | "aho-corasick", 659 | "memchr", 660 | "regex-automata", 661 | "regex-syntax", 662 | ] 663 | 664 | [[package]] 665 | name = "regex-automata" 666 | version = "0.4.9" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 669 | dependencies = [ 670 | "aho-corasick", 671 | "memchr", 672 | "regex-syntax", 673 | ] 674 | 675 | [[package]] 676 | name = "regex-syntax" 677 | version = "0.8.5" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 680 | 681 | [[package]] 682 | name = "relative-path" 683 | version = "1.9.3" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 686 | 687 | [[package]] 688 | name = "rstest" 689 | version = "0.25.0" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" 692 | dependencies = [ 693 | "futures-timer", 694 | "futures-util", 695 | "rstest_macros", 696 | "rustc_version", 697 | ] 698 | 699 | [[package]] 700 | name = "rstest_macros" 701 | version = "0.25.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" 704 | dependencies = [ 705 | "cfg-if", 706 | "glob", 707 | "proc-macro-crate", 708 | "proc-macro2", 709 | "quote", 710 | "regex", 711 | "relative-path", 712 | "rustc_version", 713 | "syn", 714 | "unicode-ident", 715 | ] 716 | 717 | [[package]] 718 | name = "rustc_version" 719 | version = "0.4.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 722 | dependencies = [ 723 | "semver", 724 | ] 725 | 726 | [[package]] 727 | name = "rustix" 728 | version = "0.38.41" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" 731 | dependencies = [ 732 | "bitflags", 733 | "errno", 734 | "libc", 735 | "linux-raw-sys", 736 | "windows-sys 0.52.0", 737 | ] 738 | 739 | [[package]] 740 | name = "rustversion" 741 | version = "1.0.18" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 744 | 745 | [[package]] 746 | name = "ryu" 747 | version = "1.0.18" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 750 | 751 | [[package]] 752 | name = "scopeguard" 753 | version = "1.2.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 756 | 757 | [[package]] 758 | name = "semver" 759 | version = "1.0.23" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 762 | dependencies = [ 763 | "serde", 764 | ] 765 | 766 | [[package]] 767 | name = "serde" 768 | version = "1.0.219" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 771 | dependencies = [ 772 | "serde_derive", 773 | ] 774 | 775 | [[package]] 776 | name = "serde_derive" 777 | version = "1.0.219" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "syn", 784 | ] 785 | 786 | [[package]] 787 | name = "serde_json" 788 | version = "1.0.133" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 791 | dependencies = [ 792 | "itoa", 793 | "memchr", 794 | "ryu", 795 | "serde", 796 | ] 797 | 798 | [[package]] 799 | name = "serde_spanned" 800 | version = "0.6.8" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 803 | dependencies = [ 804 | "serde", 805 | ] 806 | 807 | [[package]] 808 | name = "signal-hook" 809 | version = "0.3.17" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 812 | dependencies = [ 813 | "libc", 814 | "signal-hook-registry", 815 | ] 816 | 817 | [[package]] 818 | name = "signal-hook-mio" 819 | version = "0.2.4" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 822 | dependencies = [ 823 | "libc", 824 | "mio", 825 | "signal-hook", 826 | ] 827 | 828 | [[package]] 829 | name = "signal-hook-registry" 830 | version = "1.4.2" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 833 | dependencies = [ 834 | "libc", 835 | ] 836 | 837 | [[package]] 838 | name = "slab" 839 | version = "0.4.9" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 842 | dependencies = [ 843 | "autocfg", 844 | ] 845 | 846 | [[package]] 847 | name = "smallvec" 848 | version = "1.13.2" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 851 | 852 | [[package]] 853 | name = "static_assertions" 854 | version = "1.1.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 857 | 858 | [[package]] 859 | name = "strsim" 860 | version = "0.11.1" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 863 | 864 | [[package]] 865 | name = "strum" 866 | version = "0.26.3" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 869 | dependencies = [ 870 | "strum_macros", 871 | ] 872 | 873 | [[package]] 874 | name = "strum_macros" 875 | version = "0.26.4" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 878 | dependencies = [ 879 | "heck", 880 | "proc-macro2", 881 | "quote", 882 | "rustversion", 883 | "syn", 884 | ] 885 | 886 | [[package]] 887 | name = "syn" 888 | version = "2.0.89" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" 891 | dependencies = [ 892 | "proc-macro2", 893 | "quote", 894 | "unicode-ident", 895 | ] 896 | 897 | [[package]] 898 | name = "thiserror" 899 | version = "2.0.5" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "643caef17e3128658ff44d85923ef2d28af81bb71e0d67bbfe1d76f19a73e053" 902 | dependencies = [ 903 | "thiserror-impl", 904 | ] 905 | 906 | [[package]] 907 | name = "thiserror-impl" 908 | version = "2.0.5" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "995d0bbc9995d1f19d28b7215a9352b0fc3cd3a2d2ec95c2cadc485cdedbcdde" 911 | dependencies = [ 912 | "proc-macro2", 913 | "quote", 914 | "syn", 915 | ] 916 | 917 | [[package]] 918 | name = "thread_local" 919 | version = "1.1.8" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 922 | dependencies = [ 923 | "cfg-if", 924 | "once_cell", 925 | ] 926 | 927 | [[package]] 928 | name = "toml" 929 | version = "0.8.20" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 932 | dependencies = [ 933 | "serde", 934 | "serde_spanned", 935 | "toml_datetime", 936 | "toml_edit", 937 | ] 938 | 939 | [[package]] 940 | name = "toml_datetime" 941 | version = "0.6.8" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 944 | dependencies = [ 945 | "serde", 946 | ] 947 | 948 | [[package]] 949 | name = "toml_edit" 950 | version = "0.22.24" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 953 | dependencies = [ 954 | "indexmap", 955 | "serde", 956 | "serde_spanned", 957 | "toml_datetime", 958 | "winnow", 959 | ] 960 | 961 | [[package]] 962 | name = "tui-input" 963 | version = "0.11.1" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "e5d1733c47f1a217b7deff18730ff7ca4ecafc5771368f715ab072d679a36114" 966 | dependencies = [ 967 | "ratatui", 968 | "unicode-width 0.2.0", 969 | ] 970 | 971 | [[package]] 972 | name = "umbra" 973 | version = "0.4.0" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "12553399f5a4558e1a4d549bc18e9ce33b67d33759c322289adcece94ab2f5e4" 976 | dependencies = [ 977 | "proc-macro2", 978 | "quote", 979 | "syn", 980 | ] 981 | 982 | [[package]] 983 | name = "unicode-ident" 984 | version = "1.0.14" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 987 | 988 | [[package]] 989 | name = "unicode-segmentation" 990 | version = "1.12.0" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 993 | 994 | [[package]] 995 | name = "unicode-truncate" 996 | version = "1.1.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 999 | dependencies = [ 1000 | "itertools", 1001 | "unicode-segmentation", 1002 | "unicode-width 0.1.14", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "unicode-width" 1007 | version = "0.1.14" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1010 | 1011 | [[package]] 1012 | name = "unicode-width" 1013 | version = "0.2.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1016 | 1017 | [[package]] 1018 | name = "utf8parse" 1019 | version = "0.2.2" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1022 | 1023 | [[package]] 1024 | name = "wasi" 1025 | version = "0.11.0+wasi-snapshot-preview1" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1028 | 1029 | [[package]] 1030 | name = "winapi" 1031 | version = "0.3.9" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1034 | dependencies = [ 1035 | "winapi-i686-pc-windows-gnu", 1036 | "winapi-x86_64-pc-windows-gnu", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "winapi-i686-pc-windows-gnu" 1041 | version = "0.4.0" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1044 | 1045 | [[package]] 1046 | name = "winapi-x86_64-pc-windows-gnu" 1047 | version = "0.4.0" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1050 | 1051 | [[package]] 1052 | name = "windows-sys" 1053 | version = "0.52.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1056 | dependencies = [ 1057 | "windows-targets", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "windows-sys" 1062 | version = "0.59.0" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1065 | dependencies = [ 1066 | "windows-targets", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "windows-targets" 1071 | version = "0.52.6" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1074 | dependencies = [ 1075 | "windows_aarch64_gnullvm", 1076 | "windows_aarch64_msvc", 1077 | "windows_i686_gnu", 1078 | "windows_i686_gnullvm", 1079 | "windows_i686_msvc", 1080 | "windows_x86_64_gnu", 1081 | "windows_x86_64_gnullvm", 1082 | "windows_x86_64_msvc", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "windows_aarch64_gnullvm" 1087 | version = "0.52.6" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1090 | 1091 | [[package]] 1092 | name = "windows_aarch64_msvc" 1093 | version = "0.52.6" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1096 | 1097 | [[package]] 1098 | name = "windows_i686_gnu" 1099 | version = "0.52.6" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1102 | 1103 | [[package]] 1104 | name = "windows_i686_gnullvm" 1105 | version = "0.52.6" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1108 | 1109 | [[package]] 1110 | name = "windows_i686_msvc" 1111 | version = "0.52.6" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1114 | 1115 | [[package]] 1116 | name = "windows_x86_64_gnu" 1117 | version = "0.52.6" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1120 | 1121 | [[package]] 1122 | name = "windows_x86_64_gnullvm" 1123 | version = "0.52.6" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1126 | 1127 | [[package]] 1128 | name = "windows_x86_64_msvc" 1129 | version = "0.52.6" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1132 | 1133 | [[package]] 1134 | name = "winnow" 1135 | version = "0.7.3" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" 1138 | dependencies = [ 1139 | "memchr", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "yansi" 1144 | version = "1.0.1" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1147 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-selector" 3 | version = "0.9.0" 4 | edition = "2021" 5 | description = "Cargo subcommand to select and execute binary/example targets" 6 | authors = ["Kyosuke Fujimoto "] 7 | homepage = "https://github.com/lusingander/cargo-selector" 8 | repository = "https://github.com/lusingander/cargo-selector" 9 | readme = "README.md" 10 | license = "MIT" 11 | keywords = ["cargo", "subcommand", "plugin", "tui"] 12 | categories = ["development-tools::cargo-plugins", "command-line-utilities"] 13 | exclude = ["/.github", "/img", "_config.yml"] 14 | rust-version = "1.79.0" 15 | 16 | [dependencies] 17 | cargo_metadata = "0.19.2" 18 | clap = { version = "4.5.32", features = ["derive"] } 19 | console = "0.15.11" 20 | fuzzy-matcher = "0.3.7" 21 | laurier = "0.1.0" 22 | ratatui = { version = "0.29.0", features = ["serde"] } 23 | serde = { version = "1.0.219", features = ["derive"] } 24 | toml = "0.8.20" 25 | tui-input = "0.11.1" 26 | umbra = "0.4.0" 27 | 28 | [dev-dependencies] 29 | rstest = "0.25.0" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kyosuke Fujimoto 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 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.fmt-check] 2 | command = "cargo" 3 | args = ["fmt", "--all", "--", "--check"] 4 | 5 | [tasks.clippy] 6 | command = "cargo" 7 | args = ["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"] 8 | 9 | [tasks.test] 10 | command = "cargo" 11 | args = ["test"] 12 | 13 | [tasks.lint] 14 | dependencies = ["fmt-check", "clippy"] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-selector 2 | 3 | [![Crate Status](https://img.shields.io/crates/v/cargo-selector.svg)](https://crates.io/crates/cargo-selector) 4 | 5 | Cargo subcommand to select and execute binary/example targets 6 | 7 | 8 | 9 | (This demo uses [Ratatui](https://github.com/ratatui-org/ratatui) as an example!) 10 | 11 | ## Installation 12 | 13 | ``` 14 | $ cargo install cargo-selector 15 | ``` 16 | 17 | ### AUR 18 | 19 | ``` 20 | $ paru -S cargo-selector 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Basic 26 | 27 | Run the command in the cargo project directory: 28 | 29 | ``` 30 | $ cargo selector 31 | ``` 32 | 33 | Then, target list will be displayed, and you can execute the following command by selecting it. 34 | 35 | ```sh 36 | # if the target is bin 37 | $ cargo run --bin xyz [--features "foo bar"] 38 | 39 | # if the target is example 40 | $ cargo run --example xyz [--features "foo bar"] 41 | ``` 42 | 43 | By switching the action, you can also run only the build. 44 | 45 | ### Options 46 | 47 | ``` 48 | Usage: cargo selector [OPTIONS] 49 | 50 | Options: 51 | -i, --inline Display list inline 52 | -n, --inline-list-size List size [default: 10] 53 | -k, --kind Target kind [possible values: bin, example] 54 | -t, --match-type Match type [possible values: substring, fuzzy] 55 | -a, --additional-args Additional arguments 56 | -h, --help Print help 57 | -V, --version Print version 58 | ``` 59 | 60 | #### -a, --additional-args 61 | 62 | If you run the command: 63 | 64 | ``` 65 | $ cargo selector -a "-- --foo 1" 66 | ``` 67 | 68 | Then select the command and it will run: 69 | 70 | ```sh 71 | $ cargo run --bin xyz -- --foo 1 72 | ``` 73 | 74 | ### Keybindings 75 | 76 | | Key | Description | 77 | | --------------------------------- | -------------------------------------------- | 78 | | Down Ctrl+n | cursor down | 79 | | Up Ctrl+p | cursor up | 80 | | Enter | execute `cargo run --bin/example ` | 81 | | Tab | switch actions | 82 | | Esc Ctrl+c | quit | 83 | 84 | ### Config 85 | 86 | If the `CARGO_SELECTOR_CONFIG` environment variable specifies the path to the config file, the config will be loaded. 87 | 88 | The config file uses the following format: 89 | 90 | ```toml 91 | # Sets the default match type. If argument `-t` or `--match-type` is specified, it will be overridden. 92 | # type: enum ("substring" | "fuzzy") 93 | match_type = "substring" 94 | 95 | # Sets the display colors. 96 | # Colors can be set in one of the following formats: 97 | # - ANSI color name 98 | # - "red", "bright-blue" 99 | # - 8-bit color (256-color) index values 100 | # - "34", "128", "255" 101 | # - 24-bit true color hex codes 102 | # - "#abcdef" 103 | # type: string 104 | [color] 105 | bg = "reset" 106 | action_run_bg = "green" 107 | action_run_fg = "black" 108 | action_build_bg = "blue" 109 | action_build_fg = "black" 110 | input_fg = "reset" 111 | numbers_fg = "darkgrey" 112 | kind_fg = "blue" 113 | name_fg = "white" 114 | name_match_fg = "red" 115 | path_fg = "darkgrey" 116 | features_fg = "darkgrey" 117 | selected_bg = "yellow" 118 | ``` 119 | 120 | ## License 121 | 122 | MIT 123 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pages-themes/minimal@v0.2.0 2 | plugins: 3 | - jekyll-remote-theme 4 | - jekyll-sitemap 5 | title: cargo-selector 6 | description: Cargo subcommand to select and execute binary/example targets 7 | show_downloads: false -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lusingander/cargo-selector/6c3096c90c64a989f8d83327a097edec4d6788d3/img/demo.gif -------------------------------------------------------------------------------- /img/demo.tape: -------------------------------------------------------------------------------- 1 | Set Shell zsh 2 | 3 | Output demo.gif 4 | 5 | Set FontSize 15 6 | Set Width 1000 7 | Set Height 500 8 | Set Padding 15 9 | Set WindowBar Colorful 10 | Set BorderRadius 20 11 | 12 | Set Theme "iceberg-dark" 13 | 14 | Sleep 500ms 15 | 16 | Type@100ms "cargo selector" 17 | Sleep 500ms 18 | Enter 19 | Sleep 1s 20 | 21 | Ctrl+N 22 | Sleep 500ms 23 | Ctrl+N 24 | Sleep 500ms 25 | Ctrl+N 26 | Sleep 1s 27 | 28 | Type@700ms "col" 29 | Sleep 1s 30 | Ctrl+W 31 | Sleep 500ms 32 | 33 | Type@700ms "dm" 34 | Sleep 500ms 35 | Ctrl+N 36 | Sleep 500ms 37 | Ctrl+N 38 | Sleep 500ms 39 | Enter 40 | 41 | Sleep 3s 42 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | use_small_heuristics = "Default" 6 | fn_call_width = 60 7 | attr_fn_like_width = 70 8 | struct_lit_width = 18 9 | struct_variant_width = 35 10 | array_width = 60 11 | chain_width = 60 12 | single_line_if_else_max_width = 50 13 | reorder_imports = true 14 | reorder_modules = true 15 | remove_nested_parens = true 16 | short_array_element_width_threshold = 10 17 | match_arm_leading_pipes = "Never" 18 | fn_params_layout = "Tall" 19 | match_block_trailing_comma = false 20 | edition = "2021" 21 | merge_derives = true 22 | use_try_shorthand = false 23 | use_field_init_shorthand = false 24 | force_explicit_abi = true 25 | disable_all_formatting = false 26 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | path::{Path, PathBuf}, 4 | process::{Command, ExitStatus}, 5 | }; 6 | 7 | use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand, Target as CargoTarget}; 8 | 9 | use crate::{Action, Target, TargetKind}; 10 | 11 | fn convert(metadata: CargoMetadata, current_dir: &Path) -> Vec { 12 | let mut targets = Vec::new(); 13 | for p in &metadata.packages { 14 | for t in &p.targets { 15 | if is_select_target(t) { 16 | targets.push(build_target(t, current_dir)); 17 | } 18 | } 19 | } 20 | targets 21 | } 22 | 23 | fn is_select_target(t: &CargoTarget) -> bool { 24 | t.is_bin() || t.is_example() 25 | } 26 | 27 | fn build_target(t: &CargoTarget, current_dir: &Path) -> Target { 28 | let name = t.name.to_owned(); 29 | let kind = if t.is_bin() { 30 | TargetKind::Bin 31 | } else { 32 | TargetKind::Example 33 | }; 34 | let path = t 35 | .src_path 36 | .strip_prefix(current_dir) 37 | .map(|p| p.to_string()) 38 | .unwrap_or("-".to_string()); 39 | let required_features = t.required_features.clone(); 40 | 41 | Target { 42 | name, 43 | kind, 44 | path, 45 | required_features, 46 | } 47 | } 48 | 49 | fn get_current_dir() -> PathBuf { 50 | env::current_dir().expect("failed to get current directory") 51 | } 52 | 53 | pub fn get_all_targets() -> Vec { 54 | let metadata = MetadataCommand::new() 55 | .no_deps() 56 | .exec() 57 | .expect("failed to exec metadata command"); 58 | let current_dir = get_current_dir(); 59 | convert(metadata, ¤t_dir) 60 | } 61 | 62 | pub fn exec_cargo_run( 63 | target: &Target, 64 | action: &Action, 65 | additional_args: Option, 66 | ) -> ExitStatus { 67 | let action = match action { 68 | Action::Run => "run", 69 | Action::Build => "build", 70 | }; 71 | let kind = match target.kind { 72 | TargetKind::Bin => "--bin", 73 | TargetKind::Example => "--example", 74 | }; 75 | let name = &target.name; 76 | 77 | let mut cmd = Command::new("cargo"); 78 | cmd.arg(action).arg(kind).arg(name); 79 | 80 | let require_features = !target.required_features.is_empty(); 81 | 82 | if require_features { 83 | let features = target.required_features.join(" "); 84 | cmd.arg("--features").arg(&features); 85 | }; 86 | 87 | if let Some(args) = additional_args { 88 | // todo: handle quoted arguments properly 89 | args.split_whitespace().for_each(|a| { 90 | cmd.arg(a); 91 | }); 92 | } 93 | 94 | eprintln!("{}", cmd_str(&cmd)); 95 | 96 | cmd.spawn() 97 | .unwrap_or_else(|_| panic!("failed to spawn cargo {} command", action)) 98 | .wait() 99 | .unwrap() 100 | } 101 | 102 | fn cmd_str(cmd: &Command) -> String { 103 | let program = cmd.get_program().to_string_lossy(); 104 | let args = cmd 105 | .get_args() 106 | .map(|a| { 107 | let a = a.to_string_lossy(); 108 | if a.contains(char::is_whitespace) { 109 | format!("\"{}\"", a).into() 110 | } else { 111 | a 112 | } 113 | }) 114 | .collect::>(); 115 | format!("{} {}", program, args.join(" ")) 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use rstest::rstest; 121 | 122 | use super::*; 123 | 124 | #[rstest] 125 | #[case( 126 | vec!["cargo", "run", "--bin", "xyz"], 127 | "cargo run --bin xyz", 128 | )] 129 | #[case( 130 | vec!["cargo", "build", "--example", "xyz", "--features", "feature1 feature2", "--release"], 131 | "cargo build --example xyz --features \"feature1 feature2\" --release", 132 | )] 133 | #[case( 134 | vec!["cargo", "run", "--bin", "xyz", "--", "-p", "Hello World", "-n", "1"], 135 | "cargo run --bin xyz -- -p \"Hello World\" -n 1", 136 | )] 137 | fn test_cmd_str(#[case] args: Vec<&str>, #[case] expected: &str) { 138 | let mut cmd = Command::new(args[0]); 139 | args.iter().skip(1).for_each(|a| { 140 | cmd.arg(a); 141 | }); 142 | assert_eq!(cmd_str(&cmd), expected); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use ratatui::style::Color; 4 | use serde::Deserialize; 5 | use umbra::optional; 6 | 7 | use crate::MatchType; 8 | 9 | const CONFIG_PATH_ENV_VAR: &str = "CARGO_SELECTOR_CONFIG"; 10 | 11 | #[optional(derives = [Deserialize])] 12 | #[derive(Debug, Default, PartialEq, Eq)] 13 | pub struct Config { 14 | pub match_type: Option, 15 | #[nested] 16 | pub color: ColorTheme, 17 | } 18 | 19 | impl Config { 20 | pub fn load() -> Config { 21 | if let Ok(path) = env::var(CONFIG_PATH_ENV_VAR) { 22 | let content = std::fs::read_to_string(path).unwrap(); 23 | let config: OptionalConfig = toml::from_str(&content).unwrap(); 24 | config.into() 25 | } else { 26 | Config::default() 27 | } 28 | } 29 | } 30 | 31 | #[optional(derives = [Deserialize])] 32 | #[derive(Debug, PartialEq, Eq)] 33 | pub struct ColorTheme { 34 | pub bg: Color, 35 | 36 | pub action_run_bg: Color, 37 | pub action_run_fg: Color, 38 | pub action_build_bg: Color, 39 | pub action_build_fg: Color, 40 | 41 | pub input_fg: Color, 42 | pub numbers_fg: Color, 43 | 44 | pub kind_fg: Color, 45 | pub name_fg: Color, 46 | pub name_match_fg: Color, 47 | pub path_fg: Color, 48 | pub features_fg: Color, 49 | 50 | pub selected_bg: Color, 51 | } 52 | 53 | impl Default for ColorTheme { 54 | fn default() -> Self { 55 | Self { 56 | bg: Color::Reset, 57 | 58 | action_run_bg: Color::Green, 59 | action_run_fg: Color::Black, 60 | action_build_bg: Color::Blue, 61 | action_build_fg: Color::Black, 62 | 63 | input_fg: Color::Reset, 64 | numbers_fg: Color::DarkGray, 65 | 66 | kind_fg: Color::Blue, 67 | name_fg: Color::White, 68 | name_match_fg: Color::Red, 69 | path_fg: Color::DarkGray, 70 | features_fg: Color::DarkGray, 71 | 72 | selected_bg: Color::Yellow, 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | pub enum UserEvent { 7 | Quit, 8 | Down, 9 | Up, 10 | ToggleAction, 11 | Execute, 12 | } 13 | 14 | #[derive(Debug, Default)] 15 | pub struct UserEventMapper { 16 | map: HashMap, 17 | } 18 | 19 | impl UserEventMapper { 20 | #[rustfmt::skip] 21 | pub fn new() -> UserEventMapper { 22 | let mut map = HashMap::new(); 23 | map.insert(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), UserEvent::Quit); 24 | map.insert(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), UserEvent::Quit); 25 | map.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), UserEvent::Down); 26 | map.insert(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL), UserEvent::Down); 27 | map.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE), UserEvent::Up); 28 | map.insert(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL), UserEvent::Up); 29 | map.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), UserEvent::ToggleAction); 30 | map.insert(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), UserEvent::Execute); 31 | UserEventMapper { map } 32 | } 33 | 34 | pub fn find_event(&self, key: KeyEvent) -> Option { 35 | self.map.get(&key).copied() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cargo; 2 | mod config; 3 | mod event; 4 | mod matcher; 5 | mod tui; 6 | mod util; 7 | 8 | use std::{ 9 | io::{stderr, BufWriter, Stderr}, 10 | panic, 11 | process::{ExitCode, ExitStatus}, 12 | }; 13 | 14 | use clap::{Args, Parser, ValueEnum}; 15 | use ratatui::{ 16 | backend::CrosstermBackend, 17 | crossterm::{ 18 | execute, 19 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 20 | }, 21 | Terminal, TerminalOptions, Viewport, 22 | }; 23 | use serde::Deserialize; 24 | 25 | use crate::{ 26 | config::Config, 27 | matcher::Matcher, 28 | tui::{Ret, Tui}, 29 | }; 30 | 31 | #[cfg(unix)] 32 | use std::os::unix::process::ExitStatusExt as _; 33 | 34 | #[derive(Debug, Parser)] 35 | #[command(name = "cargo", bin_name = "cargo")] 36 | enum Cli { 37 | Selector(SelectorArgs), 38 | } 39 | 40 | #[derive(Debug, Args)] 41 | #[command(version, about, long_about = None)] 42 | struct SelectorArgs { 43 | /// Display list inline 44 | #[arg(short, long)] 45 | inline: bool, 46 | 47 | /// List size 48 | #[arg(short = 'n', long, default_value = "10", value_name = "SIZE")] 49 | inline_list_size: u16, 50 | 51 | /// Target kind 52 | #[arg(short, long, value_name = "NAME")] 53 | kind: Option, 54 | 55 | /// Match type 56 | #[arg(short = 't', long, value_name = "TYPE")] 57 | match_type: Option, 58 | 59 | /// Additional arguments 60 | #[arg(short, long, value_name = "ARGS", allow_hyphen_values = true)] 61 | additional_args: Option, 62 | } 63 | 64 | #[derive(Debug, Clone)] 65 | pub struct Target { 66 | name: String, 67 | kind: TargetKind, 68 | path: String, 69 | required_features: Vec, 70 | } 71 | 72 | #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] 73 | pub enum TargetKind { 74 | Bin, 75 | Example, 76 | } 77 | 78 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, ValueEnum, Deserialize)] 79 | #[serde(rename_all = "snake_case")] 80 | pub enum MatchType { 81 | #[default] 82 | Substring, 83 | Fuzzy, 84 | } 85 | 86 | impl MatchType { 87 | fn matcher(self) -> Matcher { 88 | match self { 89 | MatchType::Substring => Matcher::substring(), 90 | MatchType::Fuzzy => Matcher::fuzzy(), 91 | } 92 | } 93 | } 94 | 95 | #[derive(Default, Clone, Copy)] 96 | pub enum Action { 97 | #[default] 98 | Run, 99 | Build, 100 | } 101 | 102 | fn setup( 103 | inline: bool, 104 | inline_list_size: u16, 105 | ) -> std::io::Result>>> { 106 | enable_raw_mode()?; 107 | if !inline { 108 | execute!(stderr(), EnterAlternateScreen)?; 109 | } 110 | 111 | let backend = CrosstermBackend::new(BufWriter::new(stderr())); 112 | let viewport = if inline { 113 | Viewport::Inline(inline_list_size + 1) 114 | } else { 115 | Viewport::Fullscreen 116 | }; 117 | Terminal::with_options(backend, TerminalOptions { viewport }) 118 | } 119 | 120 | fn shutdown(inline: bool) -> std::io::Result<()> { 121 | if !inline { 122 | execute!(stderr(), LeaveAlternateScreen)?; 123 | } 124 | disable_raw_mode()?; 125 | Ok(()) 126 | } 127 | 128 | fn initialize_panic_handler(inline: bool) { 129 | let original_hook = panic::take_hook(); 130 | panic::set_hook(Box::new(move |panic_info| { 131 | shutdown(inline).unwrap(); 132 | original_hook(panic_info); 133 | })); 134 | } 135 | 136 | #[cfg(unix)] 137 | fn to_exit_code(status: ExitStatus) -> ExitCode { 138 | if let Some(code) = status.code() { 139 | ExitCode::from(code as u8) 140 | } else if let Some(signal) = status.signal() { 141 | ExitCode::from(signal as u8 + 128) 142 | } else { 143 | ExitCode::FAILURE 144 | } 145 | } 146 | 147 | #[cfg(windows)] 148 | fn to_exit_code(status: ExitStatus) -> ExitCode { 149 | if let Some(code) = status.code() { 150 | ExitCode::from(code as u8) 151 | } else { 152 | ExitCode::FAILURE 153 | } 154 | } 155 | 156 | fn main() -> std::io::Result { 157 | let Cli::Selector(args) = Cli::parse(); 158 | let SelectorArgs { 159 | inline, 160 | inline_list_size, 161 | kind, 162 | match_type, 163 | additional_args, 164 | } = args; 165 | 166 | let config = Config::load(); 167 | let match_type = match_type.or(config.match_type).unwrap_or_default(); 168 | let theme = config.color; 169 | 170 | let mut targets = cargo::get_all_targets(); 171 | if let Some(kind) = kind { 172 | targets.retain(|t| t.kind == kind); 173 | } 174 | 175 | initialize_panic_handler(inline); 176 | let mut terminal = setup(inline, inline_list_size)?; 177 | let term_size = terminal.get_frame().area(); 178 | let matcher = match_type.matcher(); 179 | let ret = Tui::new(targets, term_size, matcher, theme).run(&mut terminal); 180 | shutdown(inline)?; 181 | 182 | if inline { 183 | terminal.clear()?; 184 | } 185 | 186 | ret.map(|t| match t { 187 | Ret::Quit => ExitCode::SUCCESS, 188 | Ret::Selected(t, a) => { 189 | let status = cargo::exec_cargo_run(&t, &a, additional_args); 190 | to_exit_code(status) 191 | } 192 | Ret::NotSelected => { 193 | eprintln!("no command selected"); 194 | ExitCode::SUCCESS 195 | } 196 | }) 197 | } 198 | -------------------------------------------------------------------------------- /src/matcher.rs: -------------------------------------------------------------------------------- 1 | use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; 2 | 3 | #[derive(Default)] 4 | pub enum Matcher { 5 | #[default] 6 | Substring, 7 | Fuzzy(Box), 8 | } 9 | 10 | impl Matcher { 11 | pub fn substring() -> Self { 12 | Matcher::Substring 13 | } 14 | 15 | pub fn fuzzy() -> Self { 16 | Matcher::Fuzzy(Box::default()) 17 | } 18 | 19 | pub fn match_indices(&self, text: &str, pattern: &str) -> Option> { 20 | match self { 21 | Matcher::Substring => text 22 | .find(pattern) 23 | .map(|pos| (pos..pos + pattern.len()).collect()), 24 | Matcher::Fuzzy(matcher) => matcher 25 | .fuzzy_indices(text, pattern) 26 | .map(|(_, indices)| indices), 27 | } 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn test_matcher_substring() { 37 | let matcher = Matcher::substring(); 38 | assert_eq!(matcher.match_indices("hello", "he"), Some(vec![0, 1])); 39 | assert_eq!(matcher.match_indices("hello", "lo"), Some(vec![3, 4])); 40 | assert_eq!(matcher.match_indices("hello", "ho"), None); 41 | assert_eq!(matcher.match_indices("hello", "wr"), None); 42 | } 43 | 44 | #[test] 45 | fn test_matcher_fuzzy() { 46 | let matcher = Matcher::fuzzy(); 47 | assert_eq!(matcher.match_indices("hello", "he"), Some(vec![0, 1])); 48 | assert_eq!(matcher.match_indices("hello", "lo"), Some(vec![3, 4])); 49 | assert_eq!(matcher.match_indices("hello", "ho"), Some(vec![0, 4])); 50 | assert_eq!(matcher.match_indices("hello", "wr"), None); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use console::truncate_str; 2 | use laurier::highlight::highlight_matched_text; 3 | use ratatui::{ 4 | backend::Backend, 5 | crossterm::event::{self, Event}, 6 | layout::{Constraint, Layout, Rect}, 7 | style::{Style, Stylize}, 8 | text::{Line, Text}, 9 | widgets::{Block, List, ListItem, Paragraph}, 10 | Frame, Terminal, 11 | }; 12 | use tui_input::{backend::crossterm::EventHandler, Input}; 13 | 14 | use crate::{ 15 | config::ColorTheme, 16 | event::{UserEvent, UserEventMapper}, 17 | matcher::Matcher, 18 | util::digits, 19 | Action, Target, TargetKind, 20 | }; 21 | 22 | const ELLIPSIS: &str = ".."; 23 | 24 | #[derive(Default)] 25 | pub struct Tui { 26 | targets: Vec, 27 | filtered: Vec, 28 | cursor: usize, 29 | input: Input, 30 | action: Action, 31 | 32 | list_height: usize, 33 | list_offset: usize, 34 | 35 | matcher: Matcher, 36 | mapper: UserEventMapper, 37 | theme: ColorTheme, 38 | } 39 | 40 | struct FilteredTarget { 41 | index: usize, 42 | match_indices: Vec, 43 | } 44 | 45 | pub enum Ret { 46 | Quit, 47 | Selected(Target, Action), 48 | NotSelected, 49 | } 50 | 51 | impl Tui { 52 | pub fn new(targets: Vec, term_size: Rect, matcher: Matcher, theme: ColorTheme) -> Tui { 53 | let mut tui = Tui { 54 | targets, 55 | list_height: Tui::calc_list_height(term_size.height), 56 | matcher, 57 | mapper: UserEventMapper::new(), 58 | theme, 59 | ..Default::default() 60 | }; 61 | tui.update_filter(); 62 | tui 63 | } 64 | 65 | pub fn run(&mut self, terminal: &mut Terminal) -> std::io::Result { 66 | loop { 67 | terminal.draw(|f| self.render(f))?; 68 | 69 | match event::read()? { 70 | Event::Key(key) => match self.mapper.find_event(key) { 71 | Some(UserEvent::Quit) => { 72 | return Ok(Ret::Quit); 73 | } 74 | Some(UserEvent::Down) => { 75 | self.select_next(); 76 | } 77 | Some(UserEvent::Up) => { 78 | self.select_prev(); 79 | } 80 | Some(UserEvent::ToggleAction) => { 81 | self.toggle_action(); 82 | } 83 | Some(UserEvent::Execute) => { 84 | let ret = match self.get_current_target() { 85 | Some(target) => Ret::Selected(target, self.action), 86 | None => Ret::NotSelected, 87 | }; 88 | return Ok(ret); 89 | } 90 | _ => { 91 | self.input.handle_event(&Event::Key(key)); 92 | self.update_filter(); 93 | } 94 | }, 95 | Event::Resize(_, h) => { 96 | self.list_height = Tui::calc_list_height(h); 97 | } 98 | _ => {} 99 | } 100 | } 101 | } 102 | 103 | fn calc_list_height(h: u16) -> usize { 104 | (h - 1) as usize 105 | } 106 | 107 | fn select_next(&mut self) { 108 | if self.cursor < self.filtered.len() - 1 { 109 | if self.cursor - self.list_offset == self.list_height - 1 { 110 | self.list_offset += 1; 111 | } 112 | self.cursor += 1; 113 | } 114 | } 115 | 116 | fn select_prev(&mut self) { 117 | if self.cursor > 0 { 118 | if self.cursor - self.list_offset == 0 { 119 | self.list_offset -= 1; 120 | } 121 | self.cursor -= 1; 122 | } 123 | } 124 | 125 | fn toggle_action(&mut self) { 126 | self.action = match self.action { 127 | Action::Run => Action::Build, 128 | Action::Build => Action::Run, 129 | }; 130 | } 131 | 132 | fn get_current_target(&self) -> Option { 133 | self.filtered 134 | .get(self.cursor) 135 | .and_then(|t| self.targets.get(t.index)) 136 | .cloned() 137 | } 138 | 139 | fn update_filter(&mut self) { 140 | let s = self.input.value(); 141 | self.filtered = self 142 | .targets 143 | .iter() 144 | .enumerate() 145 | .filter_map(|(i, t)| { 146 | self.matcher 147 | .match_indices(&t.name, s) 148 | .map(|indices| FilteredTarget { 149 | index: i, 150 | match_indices: indices, 151 | }) 152 | }) 153 | .collect(); 154 | self.cursor = 0; 155 | self.list_offset = 0; 156 | } 157 | 158 | fn render(&self, f: &mut Frame) { 159 | let block = Block::default().bg(self.theme.bg); 160 | f.render_widget(block, f.area()); 161 | 162 | let chunks = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).split(f.area()); 163 | self.render_input(f, chunks[0]); 164 | self.render_list(f, chunks[1]); 165 | } 166 | 167 | fn render_input(&self, f: &mut Frame, area: Rect) { 168 | let targets_num_digits = digits(self.targets.len()); 169 | let max_w = area.width as usize; 170 | let label_w = 7; 171 | let num_w = targets_num_digits * 2 + 5; 172 | let input_w = max_w - (label_w + num_w + 3); 173 | 174 | let (label, label_bg, label_fg) = match self.action { 175 | Action::Run => ( 176 | " run ", 177 | self.theme.action_run_bg, 178 | self.theme.action_run_fg, 179 | ), 180 | Action::Build => ( 181 | " build ", 182 | self.theme.action_build_bg, 183 | self.theme.action_build_fg, 184 | ), 185 | }; 186 | let input = format!("{:input_w$}", self.input.value()); 187 | let nums = if self.filtered.is_empty() { 188 | "".to_string() 189 | } else { 190 | format!( 191 | "({:targets_num_digits$} / {:targets_num_digits$})", 192 | self.cursor + 1, 193 | self.filtered.len() 194 | ) 195 | }; 196 | let spans = vec![ 197 | label.bg(label_bg).fg(label_fg), 198 | " ".into(), 199 | input.fg(self.theme.input_fg), 200 | " ".into(), 201 | nums.fg(self.theme.numbers_fg), 202 | " ".into(), 203 | ]; 204 | let line = Paragraph::new(Line::from(spans)); 205 | f.render_widget(line, area); 206 | 207 | let x = area.x + 8 + (self.input.visual_cursor() as u16); 208 | let y = area.y; 209 | f.set_cursor_position((x, y)); 210 | } 211 | 212 | fn render_list(&self, f: &mut Frame, area: Rect) { 213 | let max_w = area.width as usize; 214 | let items: Vec = self 215 | .filtered 216 | .iter() 217 | .enumerate() 218 | .skip(self.list_offset) 219 | .take(self.list_height) 220 | .flat_map(|(i, ft)| { 221 | let selected = i == self.cursor; 222 | self.targets 223 | .get(ft.index) 224 | .map(|t| self.build_list_item(t, selected, max_w, &ft.match_indices)) 225 | }) 226 | .collect(); 227 | let list = List::new(items); 228 | f.render_widget(list, area); 229 | } 230 | 231 | fn build_list_item( 232 | &self, 233 | target: &Target, 234 | selected: bool, 235 | max_w: usize, 236 | matched_indices: &[usize], 237 | ) -> ListItem { 238 | let kind_w: usize = 7; 239 | let name_w: usize = 25; 240 | let path_w: usize = 30; 241 | let features_w: usize = max_w - (kind_w + name_w + path_w + 5); 242 | 243 | let kind = match target.kind { 244 | TargetKind::Bin => "bin", 245 | TargetKind::Example => "example", 246 | }; 247 | let name = truncate_str(&target.name, name_w, ELLIPSIS); 248 | let path = truncate_str(&target.path, path_w, ELLIPSIS); 249 | let features = if target.required_features.is_empty() { 250 | "".to_string() 251 | } else { 252 | let s = format!("--features {:?}", target.required_features); 253 | truncate_str(&s, features_w, ELLIPSIS).into() 254 | }; 255 | 256 | let mut name_mt = highlight_matched_text(name.to_string()) 257 | .matched_indices(matched_indices.to_vec()) 258 | .not_matched_style(Style::default().fg(self.theme.name_fg)) 259 | .matched_style(Style::default().fg(self.theme.name_match_fg)); 260 | if name.ends_with(ELLIPSIS) { 261 | name_mt = name_mt.ellipsis(ELLIPSIS); 262 | } 263 | let mut name_spans = name_mt.into_spans(); 264 | if name.len() < name_w { 265 | name_spans.push(" ".repeat(name_w - name.len()).into()); 266 | } 267 | 268 | let mut spans = Vec::new(); 269 | spans.push(" ".into()); 270 | spans.push(format!("{:kind_w$}", kind).fg(self.theme.kind_fg)); 271 | spans.push(" ".into()); 272 | spans.extend(name_spans); 273 | spans.push(" ".into()); 274 | spans.push(format!("{:path_w$}", path).fg(self.theme.path_fg)); 275 | spans.push(" ".into()); 276 | spans.push(format!("{:features_w$}", features).fg(self.theme.features_fg)); 277 | spans.push(" ".into()); 278 | 279 | let line = Text::from(Line::from(spans)); 280 | let style = if selected { 281 | Style::default().bg(self.theme.selected_bg) 282 | } else { 283 | Style::default() 284 | }; 285 | ListItem::new(line).style(style) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | pub fn digits(n: usize) -> usize { 2 | if n == 0 { 3 | return 1; 4 | } 5 | let mut n = n; 6 | let mut c = 0; 7 | while n > 0 { 8 | n /= 10; 9 | c += 1; 10 | } 11 | c 12 | } 13 | --------------------------------------------------------------------------------