├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── screenshot-animated.svg └── screenshot.svg ├── config ├── debug.toml ├── default.toml └── everything.toml └── src ├── bin └── ix │ ├── config.rs │ ├── main.rs │ ├── searcher.rs │ ├── tui.rs │ └── tui │ ├── backend.rs │ ├── draw.rs │ ├── handlers.rs │ ├── table.rs │ └── text_box.rs ├── database.rs ├── database ├── builder.rs ├── indexer.rs ├── search.rs ├── search │ ├── filters.rs │ └── filters │ │ ├── basename.rs │ │ ├── component_wise_path.rs │ │ ├── full_path.rs │ │ ├── passthrough.rs │ │ └── regex_path.rs └── util.rs ├── error.rs ├── lib.rs ├── mode.rs ├── mode ├── unix.rs └── windows.rs ├── query.rs └── query └── regex_helper.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-20.04] 12 | rust: [stable, beta, nightly] 13 | include: 14 | - os: macos-10.15 15 | rust: stable 16 | - os: windows-2019 17 | rust: stable 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: hecrj/setup-rust-action@v1 21 | with: 22 | rust-version: ${{ matrix.rust }} 23 | components: rustfmt, clippy 24 | - run: cargo build --verbose 25 | - run: cargo test --verbose 26 | - run: cargo fmt --all -- --check 27 | - run: cargo clippy --all-targets -- -D warnings 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | database.db 3 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.56" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.1.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 45 | 46 | [[package]] 47 | name = "bincode" 48 | version = "1.3.3" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 51 | dependencies = [ 52 | "serde", 53 | ] 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "1.3.2" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 60 | 61 | [[package]] 62 | name = "byteorder" 63 | version = "1.4.3" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 66 | 67 | [[package]] 68 | name = "camino" 69 | version = "1.0.7" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "6f3132262930b0522068049f5870a856ab8affc80c70d08b6ecb785771a6fc23" 72 | dependencies = [ 73 | "serde", 74 | ] 75 | 76 | [[package]] 77 | name = "cassowary" 78 | version = "0.3.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 81 | 82 | [[package]] 83 | name = "cfg-if" 84 | version = "1.0.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 87 | 88 | [[package]] 89 | name = "chrono" 90 | version = "0.4.19" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 93 | dependencies = [ 94 | "libc", 95 | "num-integer", 96 | "num-traits", 97 | "time", 98 | "winapi", 99 | ] 100 | 101 | [[package]] 102 | name = "clap" 103 | version = "2.34.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 106 | dependencies = [ 107 | "ansi_term", 108 | "atty", 109 | "bitflags", 110 | "strsim", 111 | "textwrap", 112 | "unicode-width", 113 | "vec_map", 114 | ] 115 | 116 | [[package]] 117 | name = "console" 118 | version = "0.15.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" 121 | dependencies = [ 122 | "encode_unicode", 123 | "libc", 124 | "once_cell", 125 | "regex", 126 | "terminal_size", 127 | "unicode-width", 128 | "winapi", 129 | ] 130 | 131 | [[package]] 132 | name = "crossbeam-channel" 133 | version = "0.5.2" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" 136 | dependencies = [ 137 | "cfg-if", 138 | "crossbeam-utils", 139 | ] 140 | 141 | [[package]] 142 | name = "crossbeam-deque" 143 | version = "0.8.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" 146 | dependencies = [ 147 | "cfg-if", 148 | "crossbeam-epoch", 149 | "crossbeam-utils", 150 | ] 151 | 152 | [[package]] 153 | name = "crossbeam-epoch" 154 | version = "0.9.7" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" 157 | dependencies = [ 158 | "cfg-if", 159 | "crossbeam-utils", 160 | "lazy_static", 161 | "memoffset", 162 | "scopeguard", 163 | ] 164 | 165 | [[package]] 166 | name = "crossbeam-utils" 167 | version = "0.8.7" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" 170 | dependencies = [ 171 | "cfg-if", 172 | "lazy_static", 173 | ] 174 | 175 | [[package]] 176 | name = "crossterm" 177 | version = "0.22.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" 180 | dependencies = [ 181 | "bitflags", 182 | "crossterm_winapi", 183 | "libc", 184 | "mio", 185 | "parking_lot 0.11.2", 186 | "signal-hook", 187 | "signal-hook-mio", 188 | "winapi", 189 | ] 190 | 191 | [[package]] 192 | name = "crossterm_winapi" 193 | version = "0.9.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 196 | dependencies = [ 197 | "winapi", 198 | ] 199 | 200 | [[package]] 201 | name = "dialoguer" 202 | version = "0.10.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "349d6b4fabcd9e97e1df1ae15395ac7e49fb144946a0d453959dc2696273b9da" 205 | dependencies = [ 206 | "console", 207 | "tempfile", 208 | "zeroize", 209 | ] 210 | 211 | [[package]] 212 | name = "dirs" 213 | version = "4.0.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 216 | dependencies = [ 217 | "dirs-sys", 218 | ] 219 | 220 | [[package]] 221 | name = "dirs-sys" 222 | version = "0.3.6" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" 225 | dependencies = [ 226 | "libc", 227 | "redox_users", 228 | "winapi", 229 | ] 230 | 231 | [[package]] 232 | name = "dunce" 233 | version = "1.0.2" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" 236 | 237 | [[package]] 238 | name = "either" 239 | version = "1.6.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 242 | 243 | [[package]] 244 | name = "encode_unicode" 245 | version = "0.3.6" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 248 | 249 | [[package]] 250 | name = "enum-map" 251 | version = "2.0.3" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "82605a2a3d13a9661b07ba27f39d00496aa347c9c236b1a3b8201c1b6d761408" 254 | dependencies = [ 255 | "enum-map-derive", 256 | "serde", 257 | ] 258 | 259 | [[package]] 260 | name = "enum-map-derive" 261 | version = "0.8.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "a63b7a0ddec6f38dcec5e36257750b7a8fcaf4227e12ceb306e341d63634da05" 264 | dependencies = [ 265 | "proc-macro2", 266 | "quote", 267 | "syn", 268 | ] 269 | 270 | [[package]] 271 | name = "fastrand" 272 | version = "1.7.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 275 | dependencies = [ 276 | "instant", 277 | ] 278 | 279 | [[package]] 280 | name = "fxhash" 281 | version = "0.2.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 284 | dependencies = [ 285 | "byteorder", 286 | ] 287 | 288 | [[package]] 289 | name = "getrandom" 290 | version = "0.2.5" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" 293 | dependencies = [ 294 | "cfg-if", 295 | "libc", 296 | "wasi", 297 | ] 298 | 299 | [[package]] 300 | name = "hashbrown" 301 | version = "0.12.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "8c21d40587b92fa6a6c6e3c1bdbf87d75511db5672f9c93175574b3a00df1758" 304 | 305 | [[package]] 306 | name = "heck" 307 | version = "0.3.3" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 310 | dependencies = [ 311 | "unicode-segmentation", 312 | ] 313 | 314 | [[package]] 315 | name = "heck" 316 | version = "0.4.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 319 | 320 | [[package]] 321 | name = "hermit-abi" 322 | version = "0.1.19" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 325 | dependencies = [ 326 | "libc", 327 | ] 328 | 329 | [[package]] 330 | name = "indexa" 331 | version = "0.1.0" 332 | dependencies = [ 333 | "anyhow", 334 | "bincode", 335 | "camino", 336 | "cassowary", 337 | "chrono", 338 | "crossbeam-channel", 339 | "crossterm", 340 | "dialoguer", 341 | "dirs", 342 | "dunce", 343 | "enum-map", 344 | "fxhash", 345 | "hashbrown", 346 | "itertools", 347 | "num_cpus", 348 | "parking_lot 0.12.0", 349 | "rayon", 350 | "regex", 351 | "regex-syntax", 352 | "serde", 353 | "size", 354 | "structopt", 355 | "strum", 356 | "strum_macros", 357 | "tempfile", 358 | "thiserror", 359 | "thread_local", 360 | "toml", 361 | "tui", 362 | "unicode-segmentation", 363 | "unicode-width", 364 | ] 365 | 366 | [[package]] 367 | name = "instant" 368 | version = "0.1.12" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 371 | dependencies = [ 372 | "cfg-if", 373 | ] 374 | 375 | [[package]] 376 | name = "itertools" 377 | version = "0.10.3" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 380 | dependencies = [ 381 | "either", 382 | ] 383 | 384 | [[package]] 385 | name = "lazy_static" 386 | version = "1.4.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 389 | 390 | [[package]] 391 | name = "libc" 392 | version = "0.2.119" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" 395 | 396 | [[package]] 397 | name = "lock_api" 398 | version = "0.4.6" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" 401 | dependencies = [ 402 | "scopeguard", 403 | ] 404 | 405 | [[package]] 406 | name = "log" 407 | version = "0.4.14" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 410 | dependencies = [ 411 | "cfg-if", 412 | ] 413 | 414 | [[package]] 415 | name = "memchr" 416 | version = "2.4.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 419 | 420 | [[package]] 421 | name = "memoffset" 422 | version = "0.6.5" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 425 | dependencies = [ 426 | "autocfg", 427 | ] 428 | 429 | [[package]] 430 | name = "mio" 431 | version = "0.7.14" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" 434 | dependencies = [ 435 | "libc", 436 | "log", 437 | "miow", 438 | "ntapi", 439 | "winapi", 440 | ] 441 | 442 | [[package]] 443 | name = "miow" 444 | version = "0.3.7" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 447 | dependencies = [ 448 | "winapi", 449 | ] 450 | 451 | [[package]] 452 | name = "ntapi" 453 | version = "0.3.7" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 456 | dependencies = [ 457 | "winapi", 458 | ] 459 | 460 | [[package]] 461 | name = "num-integer" 462 | version = "0.1.44" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 465 | dependencies = [ 466 | "autocfg", 467 | "num-traits", 468 | ] 469 | 470 | [[package]] 471 | name = "num-traits" 472 | version = "0.2.14" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 475 | dependencies = [ 476 | "autocfg", 477 | ] 478 | 479 | [[package]] 480 | name = "num_cpus" 481 | version = "1.13.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 484 | dependencies = [ 485 | "hermit-abi", 486 | "libc", 487 | ] 488 | 489 | [[package]] 490 | name = "once_cell" 491 | version = "1.10.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" 494 | 495 | [[package]] 496 | name = "parking_lot" 497 | version = "0.11.2" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 500 | dependencies = [ 501 | "instant", 502 | "lock_api", 503 | "parking_lot_core 0.8.5", 504 | ] 505 | 506 | [[package]] 507 | name = "parking_lot" 508 | version = "0.12.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" 511 | dependencies = [ 512 | "lock_api", 513 | "parking_lot_core 0.9.1", 514 | ] 515 | 516 | [[package]] 517 | name = "parking_lot_core" 518 | version = "0.8.5" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 521 | dependencies = [ 522 | "cfg-if", 523 | "instant", 524 | "libc", 525 | "redox_syscall", 526 | "smallvec", 527 | "winapi", 528 | ] 529 | 530 | [[package]] 531 | name = "parking_lot_core" 532 | version = "0.9.1" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" 535 | dependencies = [ 536 | "cfg-if", 537 | "libc", 538 | "redox_syscall", 539 | "smallvec", 540 | "windows-sys", 541 | ] 542 | 543 | [[package]] 544 | name = "proc-macro-error" 545 | version = "1.0.4" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 548 | dependencies = [ 549 | "proc-macro-error-attr", 550 | "proc-macro2", 551 | "quote", 552 | "syn", 553 | "version_check", 554 | ] 555 | 556 | [[package]] 557 | name = "proc-macro-error-attr" 558 | version = "1.0.4" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 561 | dependencies = [ 562 | "proc-macro2", 563 | "quote", 564 | "version_check", 565 | ] 566 | 567 | [[package]] 568 | name = "proc-macro2" 569 | version = "1.0.36" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 572 | dependencies = [ 573 | "unicode-xid", 574 | ] 575 | 576 | [[package]] 577 | name = "quote" 578 | version = "1.0.15" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 581 | dependencies = [ 582 | "proc-macro2", 583 | ] 584 | 585 | [[package]] 586 | name = "rayon" 587 | version = "1.5.1" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" 590 | dependencies = [ 591 | "autocfg", 592 | "crossbeam-deque", 593 | "either", 594 | "rayon-core", 595 | ] 596 | 597 | [[package]] 598 | name = "rayon-core" 599 | version = "1.9.1" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" 602 | dependencies = [ 603 | "crossbeam-channel", 604 | "crossbeam-deque", 605 | "crossbeam-utils", 606 | "lazy_static", 607 | "num_cpus", 608 | ] 609 | 610 | [[package]] 611 | name = "redox_syscall" 612 | version = "0.2.11" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" 615 | dependencies = [ 616 | "bitflags", 617 | ] 618 | 619 | [[package]] 620 | name = "redox_users" 621 | version = "0.4.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 624 | dependencies = [ 625 | "getrandom", 626 | "redox_syscall", 627 | ] 628 | 629 | [[package]] 630 | name = "regex" 631 | version = "1.5.5" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 634 | dependencies = [ 635 | "aho-corasick", 636 | "memchr", 637 | "regex-syntax", 638 | ] 639 | 640 | [[package]] 641 | name = "regex-syntax" 642 | version = "0.6.25" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 645 | 646 | [[package]] 647 | name = "remove_dir_all" 648 | version = "0.5.3" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 651 | dependencies = [ 652 | "winapi", 653 | ] 654 | 655 | [[package]] 656 | name = "rustversion" 657 | version = "1.0.6" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" 660 | 661 | [[package]] 662 | name = "scopeguard" 663 | version = "1.1.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 666 | 667 | [[package]] 668 | name = "serde" 669 | version = "1.0.136" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 672 | dependencies = [ 673 | "serde_derive", 674 | ] 675 | 676 | [[package]] 677 | name = "serde_derive" 678 | version = "1.0.136" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" 681 | dependencies = [ 682 | "proc-macro2", 683 | "quote", 684 | "syn", 685 | ] 686 | 687 | [[package]] 688 | name = "signal-hook" 689 | version = "0.3.13" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" 692 | dependencies = [ 693 | "libc", 694 | "signal-hook-registry", 695 | ] 696 | 697 | [[package]] 698 | name = "signal-hook-mio" 699 | version = "0.2.1" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" 702 | dependencies = [ 703 | "libc", 704 | "mio", 705 | "signal-hook", 706 | ] 707 | 708 | [[package]] 709 | name = "signal-hook-registry" 710 | version = "1.4.0" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 713 | dependencies = [ 714 | "libc", 715 | ] 716 | 717 | [[package]] 718 | name = "size" 719 | version = "0.1.2" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "3e5021178e8e70579d009fb545932e274ec2dde4c917791c6063d1002bee2a56" 722 | dependencies = [ 723 | "num-traits", 724 | ] 725 | 726 | [[package]] 727 | name = "smallvec" 728 | version = "1.8.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 731 | 732 | [[package]] 733 | name = "strsim" 734 | version = "0.8.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 737 | 738 | [[package]] 739 | name = "structopt" 740 | version = "0.3.26" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 743 | dependencies = [ 744 | "clap", 745 | "lazy_static", 746 | "structopt-derive", 747 | ] 748 | 749 | [[package]] 750 | name = "structopt-derive" 751 | version = "0.4.18" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 754 | dependencies = [ 755 | "heck 0.3.3", 756 | "proc-macro-error", 757 | "proc-macro2", 758 | "quote", 759 | "syn", 760 | ] 761 | 762 | [[package]] 763 | name = "strum" 764 | version = "0.24.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "e96acfc1b70604b8b2f1ffa4c57e59176c7dbb05d556c71ecd2f5498a1dee7f8" 767 | 768 | [[package]] 769 | name = "strum_macros" 770 | version = "0.24.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef" 773 | dependencies = [ 774 | "heck 0.4.0", 775 | "proc-macro2", 776 | "quote", 777 | "rustversion", 778 | "syn", 779 | ] 780 | 781 | [[package]] 782 | name = "syn" 783 | version = "1.0.86" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" 786 | dependencies = [ 787 | "proc-macro2", 788 | "quote", 789 | "unicode-xid", 790 | ] 791 | 792 | [[package]] 793 | name = "tempfile" 794 | version = "3.3.0" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 797 | dependencies = [ 798 | "cfg-if", 799 | "fastrand", 800 | "libc", 801 | "redox_syscall", 802 | "remove_dir_all", 803 | "winapi", 804 | ] 805 | 806 | [[package]] 807 | name = "terminal_size" 808 | version = "0.1.17" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 811 | dependencies = [ 812 | "libc", 813 | "winapi", 814 | ] 815 | 816 | [[package]] 817 | name = "textwrap" 818 | version = "0.11.0" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 821 | dependencies = [ 822 | "unicode-width", 823 | ] 824 | 825 | [[package]] 826 | name = "thiserror" 827 | version = "1.0.30" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 830 | dependencies = [ 831 | "thiserror-impl", 832 | ] 833 | 834 | [[package]] 835 | name = "thiserror-impl" 836 | version = "1.0.30" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 839 | dependencies = [ 840 | "proc-macro2", 841 | "quote", 842 | "syn", 843 | ] 844 | 845 | [[package]] 846 | name = "thread_local" 847 | version = "1.1.4" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 850 | dependencies = [ 851 | "once_cell", 852 | ] 853 | 854 | [[package]] 855 | name = "time" 856 | version = "0.1.44" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 859 | dependencies = [ 860 | "libc", 861 | "wasi", 862 | "winapi", 863 | ] 864 | 865 | [[package]] 866 | name = "toml" 867 | version = "0.5.8" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 870 | dependencies = [ 871 | "serde", 872 | ] 873 | 874 | [[package]] 875 | name = "tui" 876 | version = "0.17.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "23ed0a32c88b039b73f1b6c5acbd0554bfa5b6be94467375fd947c4de3a02271" 879 | dependencies = [ 880 | "bitflags", 881 | "cassowary", 882 | "crossterm", 883 | "unicode-segmentation", 884 | "unicode-width", 885 | ] 886 | 887 | [[package]] 888 | name = "unicode-segmentation" 889 | version = "1.9.0" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 892 | 893 | [[package]] 894 | name = "unicode-width" 895 | version = "0.1.9" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 898 | 899 | [[package]] 900 | name = "unicode-xid" 901 | version = "0.2.2" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 904 | 905 | [[package]] 906 | name = "vec_map" 907 | version = "0.8.2" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 910 | 911 | [[package]] 912 | name = "version_check" 913 | version = "0.9.4" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 916 | 917 | [[package]] 918 | name = "wasi" 919 | version = "0.10.0+wasi-snapshot-preview1" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 922 | 923 | [[package]] 924 | name = "winapi" 925 | version = "0.3.9" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 928 | dependencies = [ 929 | "winapi-i686-pc-windows-gnu", 930 | "winapi-x86_64-pc-windows-gnu", 931 | ] 932 | 933 | [[package]] 934 | name = "winapi-i686-pc-windows-gnu" 935 | version = "0.4.0" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 938 | 939 | [[package]] 940 | name = "winapi-x86_64-pc-windows-gnu" 941 | version = "0.4.0" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 944 | 945 | [[package]] 946 | name = "windows-sys" 947 | version = "0.32.0" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" 950 | dependencies = [ 951 | "windows_aarch64_msvc", 952 | "windows_i686_gnu", 953 | "windows_i686_msvc", 954 | "windows_x86_64_gnu", 955 | "windows_x86_64_msvc", 956 | ] 957 | 958 | [[package]] 959 | name = "windows_aarch64_msvc" 960 | version = "0.32.0" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" 963 | 964 | [[package]] 965 | name = "windows_i686_gnu" 966 | version = "0.32.0" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" 969 | 970 | [[package]] 971 | name = "windows_i686_msvc" 972 | version = "0.32.0" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" 975 | 976 | [[package]] 977 | name = "windows_x86_64_gnu" 978 | version = "0.32.0" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" 981 | 982 | [[package]] 983 | name = "windows_x86_64_msvc" 984 | version = "0.32.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" 987 | 988 | [[package]] 989 | name = "zeroize" 990 | version = "1.5.3" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "50344758e2f40e3a1fcfc8f6f91aa57b5f8ebd8d27919fe6451f15aaaf9ee608" 993 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "indexa" 3 | version = "0.1.0" 4 | authors = ["mosm "] 5 | edition = "2018" 6 | 7 | description = "A locate alternative with incremental search" 8 | repository = "https://github.com/mosmeh/indexa" 9 | documentation = "https://github.com/mosmeh/indexa" 10 | homepage = "https://github.com/mosmeh/indexa" 11 | 12 | readme = "README.md" 13 | license-file = "LICENSE" 14 | categories = ["command-line-utilities"] 15 | keywords = ["search", "files", "command-line"] 16 | exclude = [ 17 | "/assets", 18 | "/config" 19 | ] 20 | 21 | [lib] 22 | name = "indexa" 23 | path = "src/lib.rs" 24 | 25 | [[bin]] 26 | name = "ix" 27 | path = "src/bin/ix/main.rs" 28 | doc = false 29 | required-features = ["app"] 30 | 31 | [features] 32 | default = ["app"] 33 | app = [ 34 | "anyhow", 35 | "bincode", 36 | "cassowary", 37 | "chrono", 38 | "crossbeam-channel", 39 | "crossterm", 40 | "dialoguer", 41 | "dirs", 42 | "num_cpus", 43 | "size", 44 | "structopt", 45 | "toml", 46 | "tui", 47 | "unicode-segmentation", 48 | "unicode-width" 49 | ] 50 | 51 | [dependencies] 52 | anyhow = { version = "1.0.56", optional = true } 53 | bincode = { version = "1.3.3", optional = true } 54 | camino = { version = "1.0.7", features = ["serde1"] } 55 | cassowary = { version = "0.3.0", optional = true } 56 | chrono = { version = "0.4.19", optional = true } 57 | crossbeam-channel = { version = "0.5.2", optional = true } 58 | crossterm = { version = "0.22.1", optional = true } 59 | dialoguer = { version = "0.10.0", optional = true } 60 | dirs = { version = "4.0.0", optional = true } 61 | dunce = "1.0.2" 62 | enum-map = { version = "2.0.3", features = ["serde"] } 63 | fxhash = "0.2.1" 64 | hashbrown = { version = "0.12.0", features = ["inline-more"], default-features = false } 65 | itertools = "0.10.3" 66 | num_cpus = { version = "1.13.1", optional = true } 67 | parking_lot = "0.12.0" 68 | rayon = "1.5.1" 69 | regex = "1.5.5" 70 | regex-syntax = "0.6.25" 71 | serde = { version = "1.0.136", features = ["derive"] } 72 | size = { version = "0.1.2", optional = true } 73 | structopt = { version = "0.3.26", optional = true } 74 | strum = "0.24.0" 75 | strum_macros = "0.24.0" 76 | thiserror = "1.0.30" 77 | thread_local = "1.1.4" 78 | toml = { version = "0.5.8", optional = true } 79 | tui = { version = "0.17.0", optional = true } 80 | unicode-segmentation = { version = "1.9.0", optional = true } 81 | unicode-width = { version = "0.1.9", optional = true } 82 | 83 | [dev-dependencies] 84 | tempfile = "3.3.0" 85 | 86 | [profile.release] 87 | lto = true 88 | codegen-units = 1 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mosm 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 | # indexa 2 | 3 | [![build](https://github.com/mosmeh/indexa/workflows/build/badge.svg)](https://github.com/mosmeh/indexa/actions) 4 | 5 | A locate alternative with incremental search 6 | 7 | ![](assets/screenshot.svg) 8 | 9 | ## Installation 10 | 11 | ```sh 12 | cargo install --git https://github.com/mosmeh/indexa 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```sh 18 | # view and search files & directories 19 | ix 20 | 21 | # choose which file to open in vi 22 | vi $(ix) 23 | 24 | # use regex 25 | ix -r 26 | 27 | # match full path 28 | ix -p 29 | ``` 30 | 31 | On the first launch, indexa will ask you if you want to create a database with a default configuration. 32 | 33 | To update the database, run: 34 | 35 | ```sh 36 | ix -u 37 | ``` 38 | 39 | ## Configuration 40 | 41 | indexa's behavior and appearance can be customized by editing a config file. 42 | 43 | The config file is located at `~/.config/indexa/config.toml` on Unix and `%APPDATA%\indexa\config.toml` on Windows. 44 | 45 | ## Key bindings 46 | 47 | - Enter to select current line and quit 48 | - ESC / Ctrl+C / Ctrl+G to abort 49 | - Up / Ctrl+P, Down / Ctrl+N, Page Up, and Page Down to move cursor up/down 50 | - Ctrl+Home / Shift+Home and Ctrl+End / Shift+End to scroll to top/bottom of the list 51 | - Ctrl+A / Home and Ctrl+E / End to move cursor to beginning/end of query 52 | - Ctrl+U to clear the query 53 | 54 | ## Command-line options 55 | 56 | ``` 57 | USAGE: 58 | ix [FLAGS] [OPTIONS] 59 | 60 | FLAGS: 61 | -s, --case-sensitive Search case-sensitively 62 | -i, --ignore-case Search case-insensitively 63 | -r, --regex Enable regex 64 | -u, --update Update database and exit 65 | -h, --help Prints help information 66 | -V, --version Prints version information 67 | 68 | OPTIONS: 69 | -q, --query Initial query 70 | -p, --match-path Match path 71 | -t, --threads Number of threads to use 72 | -C, --config Location of a config file 73 | ``` 74 | -------------------------------------------------------------------------------- /assets/screenshot.svg: -------------------------------------------------------------------------------- 1 | Basename▲SizeModifiedPathapt-patterns.7.gz2.61KiB2020-05-1305:02/usr/share/man/man7>argument-patterns.rs415B2020-06-2715:05/home/mosm/Documentassociated-const-match-p1.19KiB2020-06-2715:05/home/mosm/Documentassociated-const-range-m779B2020-06-2715:05/home/mosm/Documentbindings-after-at-or-pat4.49KiB2020-06-2715:05/home/mosm/Documentbindings-after-at-or-pat7.85KiB2020-06-2715:05/home/mosm/Documentborrowck-closures-slice-2.10KiB2020-06-2715:05/home/mosm/Documentborrowck-closures-slice-1.66KiB2020-06-2715:05/home/mosm/Documentborrowck-closures-slice-4.19KiB2020-06-2715:05/home/mosm/Documentbox-patterns.html72.3KiB2020-06-2312:55/home/mosm/.rustup/box-patterns.html63.9KiB2020-06-1013:41/home/mosm/.rustup/box-patterns.md643B2020-06-2715:05/home/mosm/Documentbox-patterns.rs792B2020-06-2715:05/home/mosm/Documentbox-patterns.rs818B2020-06-2715:05/home/mosm/Documentbraille-patterns.cti25.8KiB2020-01-2003:21/usr/share/liblouisch17-03-oo-design-patter26.3KiB2020-06-2312:55/home/mosm/.rustup/ch17-03-oo-design-patter73.6KiB2020-06-2312:55/home/mosm/.rustup/ch17-03-oo-design-patter26.0KiB2020-06-2312:55/home/mosm/.rustup/ch17-03-oo-design-patter25.1KiB2020-06-1013:41/home/mosm/.rustup/Ready199/824950/home/mosm/Documents/rust/src/test/ui/async-await/argument-patterns.rs>patterns -------------------------------------------------------------------------------- /config/debug.toml: -------------------------------------------------------------------------------- 1 | [flags] 2 | match_path = "auto" 3 | 4 | [database] 5 | location = "database.db" 6 | index = [ 7 | "size", 8 | "mode", 9 | "created", 10 | "modified", 11 | "accessed", 12 | ] 13 | fast_sort = [ 14 | "path", 15 | "extension", 16 | "size", 17 | "mode", 18 | "created", 19 | "modified", 20 | "accessed", 21 | ] 22 | dirs = [ 23 | "." 24 | ] 25 | 26 | [ui] 27 | sort_by = "modified" 28 | sort_order = "descending" 29 | sort_dirs_before_files = true 30 | 31 | [[ui.columns]] 32 | status = "basename" 33 | 34 | [[ui.columns]] 35 | status = "extension" 36 | width = 10 37 | 38 | [[ui.columns]] 39 | status = "size" 40 | width = 10 41 | 42 | [[ui.columns]] 43 | status = "mode" 44 | width = 10 45 | 46 | [[ui.columns]] 47 | status = "modified" 48 | width = 16 49 | 50 | [[ui.columns]] 51 | status = "path" 52 | -------------------------------------------------------------------------------- /config/default.toml: -------------------------------------------------------------------------------- 1 | # Default indexa configuration file. 2 | 3 | [flags] 4 | # Default values for command-line flags. 5 | # Flags provided to CLI will override this group of options. 6 | 7 | # Initial query. 8 | # query = "blah blah" 9 | 10 | # Whether to search case-sensitively. 11 | # Defaults to smart case. 12 | # case_sensitive = false 13 | 14 | # Whether to search case-insensitively. 15 | # Defaults to smart case. 16 | # ignore_case = false 17 | 18 | # Whether to match path. 19 | # Defaults to "never". 20 | # match_path = "always" 21 | # match_path = "never" 22 | # match_path = "auto" # match path only when query contains path separators 23 | 24 | # Whether to enable regex. 25 | # regex = true 26 | 27 | # Number of threads to use. 28 | # Defaults to the number of available CPUs - 1. 29 | # threads = 4 30 | 31 | [database] 32 | # Location of a database file. Defaults to {user's data directory}/indexa/database.db 33 | # location = "/path/to/database/database.db" 34 | 35 | # File/directory statuses to index. 36 | # basename, path and extension are implicitly specified. 37 | index = [ 38 | # "size", 39 | # "mode", 40 | # "created", 41 | # "modified", 42 | # "accessed", 43 | ] 44 | 45 | # File/directory statuses to enable fast sorting for. 46 | # basename is implicitly specified. 47 | fast_sort = [ 48 | # "path", 49 | # "extension", 50 | # "size", 51 | # "mode", 52 | # "created", 53 | # "modified", 54 | # "accessed", 55 | ] 56 | 57 | # Root directories to index. Defaults to / on Unix and %HOMEDRIVE%\ (usually C:\) on Windows. 58 | # dirs = [ 59 | # "/", 60 | # ] 61 | 62 | # Whether to ignore hidden files/directories. 63 | ignore_hidden = false 64 | 65 | [ui] 66 | # File/directory status to sort by. 67 | sort_by = "basename" 68 | # sort_by = "path" 69 | # sort_by = "extension" 70 | # sort_by = "size" 71 | # sort_by = "mode" 72 | # sort_by = "created" 73 | # sort_by = "modified" 74 | # sort_by = "accessed" 75 | 76 | # Sort order. 77 | sort_order = "ascending" 78 | # sort_order = "descending" 79 | 80 | # Whether to sort directories before files. 81 | sort_dirs_before_files = false 82 | 83 | # Whether to show size in human readable format. 84 | human_readable_size = true 85 | 86 | # Datetime format for Created, Modified, and Accessed columns. 87 | datetime_format = "%Y-%m-%d %R" 88 | 89 | # Margin between columns. 90 | column_spacing = 2 91 | 92 | # Columns from left to right. 93 | # Columns with width specified will have fixed widths. 94 | # Remaining screen width is evenly distributed among other columns. 95 | [[ui.columns]] 96 | status = "basename" 97 | 98 | [[ui.columns]] 99 | status = "size" 100 | width = 10 101 | 102 | [[ui.columns]] 103 | status = "modified" 104 | width = 16 105 | 106 | [[ui.columns]] 107 | status = "path" 108 | 109 | [ui.unix] 110 | # Format of mode column. 111 | mode_format = "symbolic" 112 | # mode_format = "octal" 113 | 114 | [ui.windows] 115 | # Format of mode column. 116 | mode_format = "traditional" 117 | # mode_format = "powershell" 118 | 119 | [ui.colors] 120 | # Colors in one of following formats: 121 | # - Color name 122 | # reset, black, white, red, green, yellow, blue, magenta, cyan, gray, darkgray, 123 | # lightred, lightgreen, lightyellow, lightblue, lightmagenta, lightcyan 124 | # - RGB e.g. "66, 135, 245" 125 | # - Hex e.g. "#e43", "#fcba03" 126 | 127 | # Text (selected line) 128 | selected_fg = "lightblue" 129 | # Background (selected line) 130 | selected_bg = "reset" 131 | 132 | # Text (matched substring) 133 | matched_fg = "black" 134 | # Background (matched substring) 135 | matched_bg = "lightblue" 136 | 137 | # Text (error message) 138 | error_fg = "red" 139 | # Background (error message) 140 | error_bg = "reset" 141 | 142 | # Prompt 143 | prompt = "lightblue" 144 | -------------------------------------------------------------------------------- /config/everything.toml: -------------------------------------------------------------------------------- 1 | # A config imitating the default options of Everything (https://www.voidtools.com/) 2 | 3 | [flags] 4 | ignore_case = true 5 | match_path = "auto" 6 | 7 | [database] 8 | index = [ 9 | "size", 10 | "modified", 11 | ] 12 | fast_sort = [ 13 | "path", 14 | "size", 15 | "modified", 16 | ] 17 | 18 | [ui] 19 | sort_dirs_before_files = true 20 | 21 | [[ui.columns]] 22 | status = "basename" 23 | width = 50 24 | 25 | [[ui.columns]] 26 | status = "path" 27 | 28 | [[ui.columns]] 29 | status = "size" 30 | width = 15 31 | 32 | [[ui.columns]] 33 | status = "modified" 34 | width = 16 35 | -------------------------------------------------------------------------------- /src/bin/ix/config.rs: -------------------------------------------------------------------------------- 1 | use crate::Opt; 2 | 3 | use indexa::{ 4 | database::StatusKind, 5 | query::{CaseSensitivity, MatchPathMode, SortOrder}, 6 | }; 7 | 8 | use anyhow::{anyhow, Context, Result}; 9 | use itertools::Itertools; 10 | use serde::{Deserialize, Deserializer}; 11 | use std::{ 12 | borrow::Cow, 13 | fs::{self, File}, 14 | io::{BufWriter, Write}, 15 | path::{Path, PathBuf}, 16 | }; 17 | use tui::style::Color; 18 | 19 | #[derive(Debug, Default, PartialEq, Deserialize)] 20 | #[serde(default, deny_unknown_fields)] 21 | pub struct Config { 22 | pub flags: FlagConfig, 23 | pub database: DatabaseConfig, 24 | pub ui: UIConfig, 25 | } 26 | 27 | #[derive(Debug, PartialEq, Deserialize)] 28 | #[serde(default, deny_unknown_fields)] 29 | pub struct FlagConfig { 30 | pub query: Option, 31 | pub case_sensitive: bool, 32 | pub ignore_case: bool, 33 | pub match_path: MatchPathMode, 34 | pub regex: bool, 35 | pub threads: usize, 36 | } 37 | 38 | impl Default for FlagConfig { 39 | fn default() -> Self { 40 | Self { 41 | query: None, 42 | case_sensitive: false, 43 | ignore_case: false, 44 | match_path: MatchPathMode::Never, 45 | regex: false, 46 | threads: (num_cpus::get() - 1).max(1), 47 | } 48 | } 49 | } 50 | 51 | impl FlagConfig { 52 | pub fn merge_opt(&mut self, opt: &Opt) { 53 | if let Some(query) = &opt.query { 54 | self.query = Some(query.clone()); 55 | } 56 | 57 | // HACK: case_sensitive takes precedence over ignore_case in config file 58 | // TODO: make them mutually exclusive as in CLI flags 59 | if self.case_sensitive && self.ignore_case { 60 | self.case_sensitive = true; 61 | self.ignore_case = false; 62 | } 63 | 64 | if opt.case_sensitive || opt.ignore_case { 65 | self.case_sensitive = opt.case_sensitive; 66 | self.ignore_case = opt.ignore_case; 67 | } 68 | 69 | if let Some(m) = opt.match_path { 70 | self.match_path = m.map(|x| x.0).unwrap_or(MatchPathMode::Always); 71 | } 72 | 73 | self.regex |= opt.regex; 74 | 75 | if let Some(threads) = opt.threads { 76 | self.threads = threads.min(num_cpus::get() - 1).max(1); 77 | } 78 | } 79 | 80 | pub fn case_sensitivity(&self) -> CaseSensitivity { 81 | if self.case_sensitive { 82 | CaseSensitivity::Sensitive 83 | } else if self.ignore_case { 84 | CaseSensitivity::Insensitive 85 | } else { 86 | CaseSensitivity::Smart 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, PartialEq, Deserialize)] 92 | #[serde(default, deny_unknown_fields)] 93 | pub struct DatabaseConfig { 94 | pub location: Option, 95 | pub index: Vec, 96 | pub fast_sort: Vec, 97 | pub dirs: Vec, 98 | pub ignore_hidden: bool, 99 | } 100 | 101 | impl Default for DatabaseConfig { 102 | fn default() -> Self { 103 | let location = dirs::data_dir().map(|data_dir| { 104 | let mut path = data_dir; 105 | path.push(env!("CARGO_PKG_NAME")); 106 | path.push("database.db"); 107 | path 108 | }); 109 | 110 | let dirs = if let Some(root_dir) = get_default_root_dir() { 111 | vec![root_dir] 112 | } else { 113 | Vec::new() 114 | }; 115 | 116 | Self { 117 | location, 118 | index: Vec::new(), 119 | fast_sort: Vec::new(), 120 | dirs, 121 | ignore_hidden: false, 122 | } 123 | } 124 | } 125 | 126 | #[derive(Debug, PartialEq, Deserialize)] 127 | #[serde(default, deny_unknown_fields)] 128 | pub struct UIConfig { 129 | pub sort_by: StatusKind, 130 | pub sort_order: SortOrder, 131 | pub sort_dirs_before_files: bool, 132 | pub human_readable_size: bool, 133 | pub datetime_format: String, 134 | pub column_spacing: u16, 135 | pub columns: Vec, 136 | pub unix: UIConfigUnix, 137 | pub windows: UIConfigWindows, 138 | pub colors: ColorConfig, 139 | } 140 | 141 | impl Default for UIConfig { 142 | fn default() -> Self { 143 | Self { 144 | sort_by: StatusKind::Basename, 145 | sort_order: SortOrder::Ascending, 146 | sort_dirs_before_files: false, 147 | human_readable_size: true, 148 | datetime_format: "%Y-%m-%d %R".to_string(), 149 | column_spacing: 2, 150 | columns: vec![ 151 | Column { 152 | status: StatusKind::Basename, 153 | width: None, 154 | }, 155 | Column { 156 | status: StatusKind::Size, 157 | width: Some(10), 158 | }, 159 | Column { 160 | status: StatusKind::Modified, 161 | width: Some(16), 162 | }, 163 | Column { 164 | status: StatusKind::Path, 165 | width: None, 166 | }, 167 | ], 168 | unix: Default::default(), 169 | windows: Default::default(), 170 | colors: Default::default(), 171 | } 172 | } 173 | } 174 | 175 | #[derive(Debug, PartialEq, Deserialize)] 176 | #[serde(default, deny_unknown_fields)] 177 | pub struct UIConfigUnix { 178 | pub mode_format: ModeFormatUnix, 179 | } 180 | 181 | impl Default for UIConfigUnix { 182 | fn default() -> Self { 183 | Self { 184 | mode_format: ModeFormatUnix::Symbolic, 185 | } 186 | } 187 | } 188 | 189 | #[derive(Debug, PartialEq, Deserialize)] 190 | #[serde(default, deny_unknown_fields)] 191 | pub struct UIConfigWindows { 192 | pub mode_format: ModeFormatWindows, 193 | } 194 | 195 | impl Default for UIConfigWindows { 196 | fn default() -> Self { 197 | Self { 198 | mode_format: ModeFormatWindows::Traditional, 199 | } 200 | } 201 | } 202 | 203 | #[derive(Debug, PartialEq, Deserialize)] 204 | #[serde(default, deny_unknown_fields)] 205 | pub struct ColorConfig { 206 | #[serde(deserialize_with = "deserialize_color")] 207 | pub selected_fg: Color, 208 | #[serde(deserialize_with = "deserialize_color")] 209 | pub selected_bg: Color, 210 | #[serde(deserialize_with = "deserialize_color")] 211 | pub matched_fg: Color, 212 | #[serde(deserialize_with = "deserialize_color")] 213 | pub matched_bg: Color, 214 | #[serde(deserialize_with = "deserialize_color")] 215 | pub error_fg: Color, 216 | #[serde(deserialize_with = "deserialize_color")] 217 | pub error_bg: Color, 218 | #[serde(deserialize_with = "deserialize_color")] 219 | pub prompt: Color, 220 | } 221 | 222 | impl Default for ColorConfig { 223 | fn default() -> Self { 224 | Self { 225 | selected_fg: Color::LightBlue, 226 | selected_bg: Color::Reset, 227 | matched_fg: Color::Black, 228 | matched_bg: Color::LightBlue, 229 | error_fg: Color::Red, 230 | error_bg: Color::Reset, 231 | prompt: Color::LightBlue, 232 | } 233 | } 234 | } 235 | 236 | #[derive(Debug, PartialEq, Deserialize)] 237 | pub struct Column { 238 | pub status: StatusKind, 239 | pub width: Option, 240 | } 241 | 242 | #[derive(Debug, PartialEq, Deserialize)] 243 | #[serde(rename_all = "lowercase")] 244 | pub enum ModeFormatUnix { 245 | Octal, 246 | Symbolic, 247 | } 248 | 249 | #[derive(Debug, PartialEq, Deserialize)] 250 | #[serde(rename_all = "lowercase")] 251 | pub enum ModeFormatWindows { 252 | Traditional, 253 | PowerShell, 254 | } 255 | 256 | const DEFAULT_CONFIG_STRING: &str = 257 | include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/config/default.toml")); 258 | 259 | pub fn read_or_create_config

(config_path: Option

) -> Result 260 | where 261 | P: AsRef, 262 | { 263 | const CONFIG_LOCATION_ERROR_MSG: &str = "Could not determine the location of config file. \ 264 | Please provide a location of config file with -C/--config option."; 265 | 266 | let path = if let Some(path) = config_path.as_ref() { 267 | Cow::Borrowed(path.as_ref()) 268 | } else if cfg!(windows) { 269 | let config_dir = dirs::config_dir().ok_or_else(|| anyhow!(CONFIG_LOCATION_ERROR_MSG))?; 270 | let mut path = config_dir; 271 | path.push(env!("CARGO_PKG_NAME")); 272 | path.push("config.toml"); 273 | Cow::Owned(path) 274 | } else { 275 | let home_dir = dirs::home_dir().ok_or_else(|| anyhow!(CONFIG_LOCATION_ERROR_MSG))?; 276 | let mut path = home_dir; 277 | path.push(".config"); 278 | path.push(env!("CARGO_PKG_NAME")); 279 | path.push("config.toml"); 280 | Cow::Owned(path) 281 | }; 282 | 283 | if let Ok(config_string) = fs::read_to_string(&path) { 284 | Ok(toml::from_str(config_string.as_str()) 285 | .context("Invalid config file. Please edit the config file and try again.")?) 286 | } else { 287 | if let Some(parent) = path.parent() { 288 | fs::create_dir_all(parent)?; 289 | } 290 | 291 | let mut writer = BufWriter::new(File::create(&path)?); 292 | writer.write_all(DEFAULT_CONFIG_STRING.as_bytes())?; 293 | writer.flush()?; 294 | 295 | eprintln!("Created a default configuration file at {}", path.display()); 296 | 297 | Ok(Default::default()) 298 | } 299 | } 300 | 301 | fn deserialize_color<'de, D>(deserializer: D) -> Result 302 | where 303 | D: Deserializer<'de>, 304 | { 305 | let string = String::deserialize(deserializer)?; 306 | 307 | match string.trim().to_lowercase().as_str() { 308 | "reset" => Ok(Color::Reset), 309 | "black" => Ok(Color::Black), 310 | "red" => Ok(Color::Red), 311 | "green" => Ok(Color::Green), 312 | "yellow" => Ok(Color::Yellow), 313 | "blue" => Ok(Color::Blue), 314 | "magenta" => Ok(Color::Magenta), 315 | "cyan" => Ok(Color::Cyan), 316 | "gray" => Ok(Color::Gray), 317 | "darkgray" => Ok(Color::DarkGray), 318 | "lightred" => Ok(Color::LightRed), 319 | "lightgreen" => Ok(Color::LightGreen), 320 | "lightyellow" => Ok(Color::LightYellow), 321 | "lightblue" => Ok(Color::LightBlue), 322 | "lightmagenta" => Ok(Color::LightMagenta), 323 | "lightcyan" => Ok(Color::LightCyan), 324 | "white" => Ok(Color::White), 325 | string => { 326 | let components: Result, _> = match string { 327 | hex if hex.starts_with('#') && hex.len() == 4 => hex 328 | .chars() 329 | .skip(1) 330 | .map(|x| u8::from_str_radix(&format!("{}{}", x, x), 16)) 331 | .collect(), 332 | hex if hex.starts_with('#') && hex.len() == 7 => hex 333 | .chars() 334 | .skip(1) 335 | .tuples() 336 | .map(|(a, b)| u8::from_str_radix(&format!("{}{}", a, b), 16)) 337 | .collect(), 338 | rgb => rgb.split(',').map(|c| c.trim().parse::()).collect(), 339 | }; 340 | if let Ok(components) = components { 341 | if let [r, g, b] = *components { 342 | return Ok(Color::Rgb(r, g, b)); 343 | } 344 | } 345 | Err(serde::de::Error::invalid_value( 346 | serde::de::Unexpected::Str(string), 347 | &"Color name, RGB, or hex value", 348 | )) 349 | } 350 | } 351 | } 352 | 353 | #[cfg(windows)] 354 | fn get_default_root_dir() -> Option { 355 | if let Ok(homedrive) = std::env::var("HOMEDRIVE") { 356 | let path = PathBuf::from(homedrive + "\\"); 357 | if path.exists() { 358 | return Some(path); 359 | } 360 | } 361 | 362 | let path = PathBuf::from(r"C:\"); 363 | path.exists().then(|| path) 364 | } 365 | 366 | #[cfg(not(windows))] 367 | fn get_default_root_dir() -> Option { 368 | let path = PathBuf::from("/"); 369 | path.exists().then(|| path) 370 | } 371 | 372 | #[cfg(test)] 373 | mod tests { 374 | use super::*; 375 | use tempfile::NamedTempFile; 376 | 377 | #[test] 378 | fn create_and_read_config() { 379 | let tmpdir = tempfile::tempdir().unwrap(); 380 | let nonexistent_file = tmpdir.path().join("config.toml"); 381 | let created_config = read_or_create_config(Some(&nonexistent_file)).unwrap(); 382 | 383 | let created_file = nonexistent_file; 384 | let read_config = read_or_create_config(Some(created_file)).unwrap(); 385 | 386 | assert_eq!(created_config, read_config); 387 | } 388 | 389 | #[test] 390 | fn default_config_is_consistent() { 391 | let from_str: Config = toml::from_str(DEFAULT_CONFIG_STRING).unwrap(); 392 | let from_default_trait = Config::default(); 393 | 394 | assert_eq!(from_str, from_default_trait); 395 | 396 | let tmpdir = tempfile::tempdir().unwrap(); 397 | let nonexistent_file = tmpdir.path().join("config.toml"); 398 | let created = read_or_create_config(Some(nonexistent_file)).unwrap(); 399 | 400 | assert_eq!(from_str, created); 401 | 402 | let empty_file = NamedTempFile::new().unwrap(); 403 | let written = read_or_create_config(Some(empty_file.path())).unwrap(); 404 | 405 | assert_eq!(from_str, written); 406 | } 407 | 408 | #[test] 409 | #[should_panic(expected = "Invalid config file")] 410 | fn invalid_config() { 411 | let mut file = NamedTempFile::new().unwrap(); 412 | writeln!(file, "xxx").unwrap(); 413 | 414 | read_or_create_config(Some(file.path())).unwrap(); 415 | } 416 | 417 | #[test] 418 | fn color() { 419 | use serde::de::IntoDeserializer; 420 | use tui::style::Color; 421 | 422 | type Deserializer<'a> = serde::de::value::StrDeserializer<'a, serde::de::value::Error>; 423 | 424 | let s: Deserializer = "blue".into_deserializer(); 425 | assert_eq!(deserialize_color(s), Ok(Color::Blue)); 426 | 427 | let s: Deserializer = "\t Red \r\n".into_deserializer(); 428 | assert_eq!(deserialize_color(s), Ok(Color::Red)); 429 | 430 | let s: Deserializer = "66, 135, 245".into_deserializer(); 431 | assert_eq!(deserialize_color(s), Ok(Color::Rgb(66, 135, 245))); 432 | 433 | let s: Deserializer = "#E43".into_deserializer(); 434 | assert_eq!(deserialize_color(s), Ok(Color::Rgb(238, 68, 51))); 435 | 436 | let s: Deserializer = "#fcba03".into_deserializer(); 437 | assert_eq!(deserialize_color(s), Ok(Color::Rgb(252, 186, 3))); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /src/bin/ix/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod searcher; 3 | mod tui; 4 | 5 | use crate::config::DatabaseConfig; 6 | use indexa::{database::DatabaseBuilder, query::MatchPathMode}; 7 | 8 | use anyhow::{anyhow, Error, Result}; 9 | use dialoguer::Confirm; 10 | use rayon::ThreadPoolBuilder; 11 | use std::{ 12 | fs::File, 13 | io::{BufWriter, Write}, 14 | path::PathBuf, 15 | str::FromStr, 16 | }; 17 | use structopt::{clap::AppSettings, StructOpt}; 18 | 19 | #[derive(Debug, Clone, Copy)] 20 | struct MatchPathOpt(MatchPathMode); 21 | 22 | impl FromStr for MatchPathOpt { 23 | type Err = Error; 24 | 25 | fn from_str(s: &str) -> Result { 26 | let m = match s.to_lowercase().as_str() { 27 | "always" | "yes" => MatchPathMode::Always, 28 | "never" | "no" => MatchPathMode::Never, 29 | "auto" => MatchPathMode::Auto, 30 | _ => { 31 | return Err(anyhow!(format!( 32 | "Invalid value '{}'. Valid values are 'always', 'never', or 'auto'.", 33 | s 34 | ))) 35 | } 36 | }; 37 | Ok(Self(m)) 38 | } 39 | } 40 | 41 | #[derive(Debug, StructOpt)] 42 | #[structopt( 43 | name = "indexa", 44 | author = env!("CARGO_PKG_AUTHORS"), 45 | rename_all = "kebab-case", 46 | setting(AppSettings::ColoredHelp), 47 | setting(AppSettings::DeriveDisplayOrder), 48 | setting(AppSettings::AllArgsOverrideSelf) 49 | )] 50 | pub struct Opt { 51 | /// Initial query. 52 | #[structopt(short = "q", long)] 53 | query: Option, 54 | 55 | /// Search case-sensitively. 56 | /// 57 | /// Defaults to smart case. 58 | #[structopt(short = "s", long, overrides_with_all = &["ignore-case", "case-sensitive"])] 59 | case_sensitive: bool, 60 | 61 | /// Search case-insensitively. 62 | /// 63 | /// Defaults to smart case. 64 | #[structopt(short = "i", long, overrides_with_all = &["case-sensitive", "ignore-case"])] 65 | ignore_case: bool, 66 | 67 | /// Match path. 68 | /// 69 | /// can be 'always' (default if omitted), 'auto', or 'never'. 70 | /// With 'auto', it matches path only when query contains path separators. 71 | /// 72 | /// Defaults to 'never'. 73 | #[structopt(short = "p", long, name = "when")] 74 | match_path: Option>, 75 | 76 | /// Enable regex. 77 | #[structopt(short, long)] 78 | regex: bool, 79 | 80 | /// Update database and exit. 81 | #[structopt(short, long)] 82 | update: bool, 83 | 84 | /// Number of threads to use. 85 | /// 86 | /// Defaults to the number of available CPUs minus 1. 87 | #[structopt(short, long)] 88 | threads: Option, 89 | 90 | /// Location of a config file. 91 | #[structopt(short = "C", long)] 92 | config: Option, 93 | } 94 | 95 | fn main() -> Result<()> { 96 | let opt = Opt::from_args(); 97 | let mut config = config::read_or_create_config(opt.config.as_ref())?; 98 | config.flags.merge_opt(&opt); 99 | 100 | let db_location = if let Some(location) = &config.database.location { 101 | location 102 | } else { 103 | return Err(anyhow!( 104 | "Could not determine the location of the database file. Please edit the config file." 105 | )); 106 | }; 107 | 108 | ThreadPoolBuilder::new() 109 | .num_threads(config.flags.threads) 110 | .build_global()?; 111 | 112 | if opt.update { 113 | create_database(&config.database)?; 114 | return Ok(()); 115 | } 116 | 117 | if !db_location.exists() { 118 | let yes = Confirm::new() 119 | .with_prompt("Database is not created yet. Create it now?") 120 | .default(true) 121 | .interact()?; 122 | if yes { 123 | create_database(&config.database)?; 124 | } else { 125 | return Ok(()); 126 | } 127 | } 128 | 129 | tui::run(&config)?; 130 | 131 | Ok(()) 132 | } 133 | 134 | fn create_database(db_config: &DatabaseConfig) -> Result<()> { 135 | let mut builder = DatabaseBuilder::new(); 136 | builder.ignore_hidden(db_config.ignore_hidden); 137 | for dir in &db_config.dirs { 138 | builder.add_dir(&dir); 139 | } 140 | for kind in &db_config.index { 141 | builder.index(*kind); 142 | } 143 | for kind in &db_config.fast_sort { 144 | builder.fast_sort(*kind); 145 | } 146 | 147 | eprintln!("Indexing"); 148 | let database = builder.build()?; 149 | eprintln!("Indexed {} files/directories", database.num_entries()); 150 | 151 | eprintln!("Writing"); 152 | 153 | let location = db_config.location.as_ref().unwrap(); 154 | let create = !location.exists(); 155 | 156 | if let Some(parent) = location.parent() { 157 | std::fs::create_dir_all(parent)?; 158 | } 159 | 160 | let mut writer = BufWriter::new(File::create(&location)?); 161 | bincode::serialize_into(&mut writer, &database)?; 162 | writer.flush()?; 163 | 164 | if create { 165 | eprintln!("Created a database at {}", location.display()); 166 | } else { 167 | eprintln!("Updated the database"); 168 | } 169 | 170 | Ok(()) 171 | } 172 | -------------------------------------------------------------------------------- /src/bin/ix/searcher.rs: -------------------------------------------------------------------------------- 1 | use indexa::{ 2 | database::{Database, EntryId}, 3 | query::Query, 4 | Error, 5 | }; 6 | 7 | use crossbeam_channel::Sender; 8 | use std::{ 9 | sync::{ 10 | atomic::{AtomicBool, Ordering}, 11 | Arc, 12 | }, 13 | thread, 14 | }; 15 | 16 | pub struct Searcher { 17 | database: Arc, 18 | tx: Sender>, 19 | search: Option, 20 | } 21 | 22 | impl Searcher { 23 | pub fn new(database: Arc, tx: Sender>) -> Self { 24 | Self { 25 | database, 26 | tx, 27 | search: None, 28 | } 29 | } 30 | 31 | pub fn search(&mut self, query: Query) { 32 | if let Some(search) = &self.search { 33 | search.abort(); 34 | } 35 | 36 | let abort_signal = Arc::new(AtomicBool::new(false)); 37 | 38 | { 39 | let database = self.database.clone(); 40 | let tx = self.tx.clone(); 41 | let abort_signal = abort_signal.clone(); 42 | 43 | thread::spawn(move || { 44 | let hits = database.abortable_search(&query, &abort_signal); 45 | match hits { 46 | Ok(hits) => { 47 | if !abort_signal.load(Ordering::Relaxed) { 48 | let _ = tx.send(hits); 49 | } 50 | } 51 | Err(Error::SearchAbort) => (), 52 | Err(e) => panic!("{}", e), 53 | } 54 | }); 55 | } 56 | 57 | self.search = Some(Search { abort_signal }); 58 | } 59 | } 60 | 61 | struct Search { 62 | abort_signal: Arc, 63 | } 64 | 65 | impl Drop for Search { 66 | fn drop(&mut self) { 67 | self.abort(); 68 | } 69 | } 70 | 71 | impl Search { 72 | fn abort(&self) { 73 | self.abort_signal.store(true, Ordering::Relaxed); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/bin/ix/tui.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | mod draw; 3 | mod handlers; 4 | mod table; 5 | mod text_box; 6 | 7 | use backend::CustomBackend; 8 | use table::TableState; 9 | use text_box::TextBoxState; 10 | 11 | use crate::{config::Config, searcher::Searcher}; 12 | 13 | use indexa::{ 14 | database::{Database, EntryId}, 15 | query::Query, 16 | }; 17 | 18 | use anyhow::{Context, Result}; 19 | use bincode::Options; 20 | use crossterm::{ 21 | event::{self, DisableMouseCapture, EnableMouseCapture}, 22 | terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, 23 | }; 24 | use std::{io, path::Path, sync::Arc, thread}; 25 | use tui::Terminal; 26 | 27 | pub fn run(config: &Config) -> Result<()> { 28 | TuiApp::new(config)?.run() 29 | } 30 | 31 | type Backend = CustomBackend; 32 | 33 | enum State { 34 | Loading, 35 | Ready, 36 | Searching, 37 | InvalidQuery(String), 38 | Aborted, 39 | Accepted, 40 | } 41 | 42 | struct TuiApp<'a> { 43 | config: &'a Config, 44 | status: State, 45 | database: Option>, 46 | searcher: Option, 47 | query: Option, 48 | hits: Vec, 49 | text_box_state: TextBoxState, 50 | table_state: TableState, 51 | page_scroll_amount: u16, 52 | } 53 | 54 | impl<'a> TuiApp<'a> { 55 | fn new(config: &'a Config) -> Result { 56 | let app = Self { 57 | config, 58 | status: State::Loading, 59 | database: None, 60 | searcher: None, 61 | query: None, 62 | hits: Vec::new(), 63 | text_box_state: TextBoxState::with_text( 64 | config.flags.query.clone().unwrap_or_else(|| "".to_string()), 65 | ), 66 | table_state: Default::default(), 67 | page_scroll_amount: 0, 68 | }; 69 | 70 | Ok(app) 71 | } 72 | 73 | fn run(&mut self) -> Result<()> { 74 | let (load_tx, load_rx) = crossbeam_channel::bounded(1); 75 | let db_path = self.config.database.location.as_ref().unwrap().clone(); 76 | 77 | thread::spawn(move || { 78 | load_tx.send(load_database(db_path)).unwrap(); 79 | }); 80 | 81 | let mut terminal = setup_terminal()?; 82 | 83 | let (input_tx, input_rx) = crossbeam_channel::unbounded(); 84 | thread::spawn(move || loop { 85 | if let Ok(event) = event::read() { 86 | let _ = input_tx.send(event); 87 | } 88 | }); 89 | 90 | let database = loop { 91 | let terminal_width = terminal.size()?.width; 92 | terminal.draw(|f| self.draw(f, terminal_width))?; 93 | 94 | crossbeam_channel::select! { 95 | recv(load_rx) -> database => { 96 | self.status = State::Ready; 97 | break Some(database??); 98 | }, 99 | recv(input_rx) -> event => self.handle_input(event?)?, 100 | } 101 | 102 | match self.status { 103 | State::Aborted | State::Accepted => { 104 | cleanup_terminal(&mut terminal)?; 105 | break None; 106 | } 107 | _ => (), 108 | } 109 | }; 110 | 111 | if let Some(database) = database { 112 | let database = Arc::new(database); 113 | self.database = Some(Arc::clone(&database)); 114 | 115 | let (result_tx, result_rx) = crossbeam_channel::bounded(1); 116 | self.searcher = Some(Searcher::new(database, result_tx)); 117 | 118 | self.handle_query_change()?; 119 | 120 | loop { 121 | let terminal_width = terminal.size()?.width; 122 | terminal.draw(|f| self.draw(f, terminal_width))?; 123 | 124 | crossbeam_channel::select! { 125 | recv(result_rx) -> hits => self.handle_search_result(hits?)?, 126 | recv(input_rx) -> event => self.handle_input(event?)?, 127 | } 128 | 129 | match self.status { 130 | State::Aborted => { 131 | cleanup_terminal(&mut terminal)?; 132 | break; 133 | } 134 | State::Accepted => { 135 | cleanup_terminal(&mut terminal)?; 136 | self.handle_accept()?; 137 | break; 138 | } 139 | _ => (), 140 | } 141 | } 142 | } 143 | 144 | Ok(()) 145 | } 146 | } 147 | 148 | fn setup_terminal() -> Result> { 149 | terminal::enable_raw_mode()?; 150 | let mut stderr = io::stderr(); 151 | crossterm::execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; 152 | let backend = CustomBackend::new(stderr); 153 | let mut terminal = Terminal::new(backend)?; 154 | terminal.hide_cursor()?; 155 | terminal.clear()?; 156 | 157 | Ok(terminal) 158 | } 159 | 160 | fn cleanup_terminal(terminal: &mut Terminal) -> Result<()> { 161 | terminal.show_cursor()?; 162 | terminal::disable_raw_mode()?; 163 | crossterm::execute!( 164 | terminal.backend_mut(), 165 | LeaveAlternateScreen, 166 | DisableMouseCapture 167 | )?; 168 | Ok(()) 169 | } 170 | 171 | fn load_database

(path: P) -> Result 172 | where 173 | P: AsRef, 174 | { 175 | bincode::DefaultOptions::new() 176 | .with_fixint_encoding() 177 | .reject_trailing_bytes() 178 | .deserialize(&std::fs::read(path)?) 179 | .context("Failed to load database. Try updating the database") 180 | } 181 | -------------------------------------------------------------------------------- /src/bin/ix/tui/backend.rs: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2016 Florian Dehau 4 | // Copyright (c) 2021-present mosm 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | // originally from https://github.com/fdehau/tui-rs/blob/e0b2572eba7512d336db9e749d0a29cc00c39ed3/src/backend/crossterm.rs 25 | 26 | use crossterm::{ 27 | cursor::MoveTo, 28 | queue, 29 | style::{ 30 | Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, 31 | SetForegroundColor, 32 | }, 33 | }; 34 | use std::io::{self, Write}; 35 | use tui::{ 36 | backend::{Backend, CrosstermBackend}, 37 | buffer::Cell, 38 | layout::Rect, 39 | style::{Color, Modifier}, 40 | }; 41 | 42 | pub struct CustomBackend(CrosstermBackend); 43 | 44 | impl CustomBackend 45 | where 46 | W: Write, 47 | { 48 | pub fn new(buffer: W) -> CustomBackend { 49 | Self(CrosstermBackend::new(buffer)) 50 | } 51 | } 52 | 53 | impl Write for CustomBackend 54 | where 55 | W: Write, 56 | { 57 | fn write(&mut self, buf: &[u8]) -> io::Result { 58 | self.0.write(buf) 59 | } 60 | 61 | fn flush(&mut self) -> io::Result<()> { 62 | Write::flush(&mut self.0) 63 | } 64 | } 65 | 66 | impl Backend for CustomBackend 67 | where 68 | W: Write, 69 | { 70 | fn draw<'a, I>(&mut self, content: I) -> io::Result<()> 71 | where 72 | I: Iterator, 73 | { 74 | let mut buffer = Vec::with_capacity(content.size_hint().0 * 3); 75 | let mut fg = Color::Reset; 76 | let mut bg = Color::Reset; 77 | let mut modifier = Modifier::empty(); 78 | let mut last_pos: Option<(u16, u16)> = None; 79 | for (x, y, cell) in content { 80 | // Move the cursor if the previous location was not (x - 1, y) 81 | if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) { 82 | map_error(queue!(buffer, MoveTo(x, y)))?; 83 | } 84 | last_pos = Some((x, y)); 85 | if cell.modifier != modifier { 86 | let diff = ModifierDiff { 87 | from: modifier, 88 | to: cell.modifier, 89 | }; 90 | diff.queue(&mut buffer)?; 91 | modifier = cell.modifier; 92 | } 93 | if cell.fg != fg { 94 | let color = CColorWrapper::from(cell.fg).0; 95 | map_error(queue!(buffer, SetForegroundColor(color)))?; 96 | fg = cell.fg; 97 | } 98 | if cell.bg != bg { 99 | let color = CColorWrapper::from(cell.bg).0; 100 | map_error(queue!(buffer, SetBackgroundColor(color)))?; 101 | bg = cell.bg; 102 | } 103 | 104 | map_error(queue!(buffer, Print(&cell.symbol)))?; 105 | } 106 | 107 | let string = std::str::from_utf8(&buffer).unwrap(); 108 | map_error(queue!( 109 | self.0, 110 | Print(string), 111 | SetForegroundColor(CColor::Reset), 112 | SetBackgroundColor(CColor::Reset), 113 | SetAttribute(CAttribute::Reset) 114 | )) 115 | } 116 | 117 | fn hide_cursor(&mut self) -> io::Result<()> { 118 | self.0.hide_cursor() 119 | } 120 | 121 | fn show_cursor(&mut self) -> io::Result<()> { 122 | self.0.show_cursor() 123 | } 124 | 125 | fn get_cursor(&mut self) -> io::Result<(u16, u16)> { 126 | self.0.get_cursor() 127 | } 128 | 129 | fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { 130 | self.0.set_cursor(x, y) 131 | } 132 | 133 | fn clear(&mut self) -> io::Result<()> { 134 | self.0.clear() 135 | } 136 | 137 | fn size(&self) -> io::Result { 138 | self.0.size() 139 | } 140 | 141 | fn flush(&mut self) -> io::Result<()> { 142 | Backend::flush(&mut self.0) 143 | } 144 | } 145 | 146 | fn map_error(error: crossterm::Result<()>) -> io::Result<()> { 147 | error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) 148 | } 149 | 150 | struct CColorWrapper(CColor); 151 | 152 | impl From for CColorWrapper { 153 | fn from(color: Color) -> Self { 154 | let c = match color { 155 | Color::Reset => CColor::Reset, 156 | Color::Black => CColor::Black, 157 | Color::Red => CColor::DarkRed, 158 | Color::Green => CColor::DarkGreen, 159 | Color::Yellow => CColor::DarkYellow, 160 | Color::Blue => CColor::DarkBlue, 161 | Color::Magenta => CColor::DarkMagenta, 162 | Color::Cyan => CColor::DarkCyan, 163 | Color::Gray => CColor::Grey, 164 | Color::DarkGray => CColor::DarkGrey, 165 | Color::LightRed => CColor::Red, 166 | Color::LightGreen => CColor::Green, 167 | Color::LightBlue => CColor::Blue, 168 | Color::LightYellow => CColor::Yellow, 169 | Color::LightMagenta => CColor::Magenta, 170 | Color::LightCyan => CColor::Cyan, 171 | Color::White => CColor::White, 172 | Color::Indexed(i) => CColor::AnsiValue(i), 173 | Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, 174 | }; 175 | Self(c) 176 | } 177 | } 178 | 179 | #[derive(Debug)] 180 | struct ModifierDiff { 181 | pub from: Modifier, 182 | pub to: Modifier, 183 | } 184 | 185 | impl ModifierDiff { 186 | fn queue(&self, mut w: W) -> io::Result<()> 187 | where 188 | W: io::Write, 189 | { 190 | //use crossterm::Attribute; 191 | let removed = self.from - self.to; 192 | if removed.contains(Modifier::REVERSED) { 193 | map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?; 194 | } 195 | if removed.contains(Modifier::BOLD) { 196 | map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; 197 | if self.to.contains(Modifier::DIM) { 198 | map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; 199 | } 200 | } 201 | if removed.contains(Modifier::ITALIC) { 202 | map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; 203 | } 204 | if removed.contains(Modifier::UNDERLINED) { 205 | map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?; 206 | } 207 | if removed.contains(Modifier::DIM) { 208 | map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; 209 | } 210 | if removed.contains(Modifier::CROSSED_OUT) { 211 | map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?; 212 | } 213 | if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { 214 | map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; 215 | } 216 | 217 | let added = self.to - self.from; 218 | if added.contains(Modifier::REVERSED) { 219 | map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; 220 | } 221 | if added.contains(Modifier::BOLD) { 222 | map_error(queue!(w, SetAttribute(CAttribute::Bold)))?; 223 | } 224 | if added.contains(Modifier::ITALIC) { 225 | map_error(queue!(w, SetAttribute(CAttribute::Italic)))?; 226 | } 227 | if added.contains(Modifier::UNDERLINED) { 228 | map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; 229 | } 230 | if added.contains(Modifier::DIM) { 231 | map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; 232 | } 233 | if added.contains(Modifier::CROSSED_OUT) { 234 | map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?; 235 | } 236 | if added.contains(Modifier::SLOW_BLINK) { 237 | map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?; 238 | } 239 | if added.contains(Modifier::RAPID_BLINK) { 240 | map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?; 241 | } 242 | 243 | Ok(()) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/bin/ix/tui/draw.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | table::{HighlightableText, Row, Table}, 3 | text_box::TextBox, 4 | Backend, State, TuiApp, 5 | }; 6 | 7 | use indexa::{ 8 | database::{Entry, EntryId, StatusKind}, 9 | mode::Mode, 10 | query::{Query, SortOrder}, 11 | }; 12 | 13 | use chrono::{offset::Local, DateTime}; 14 | use std::{ops::Range, time::SystemTime}; 15 | use tui::{ 16 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 17 | style::{Color, Modifier, Style}, 18 | text::Span, 19 | widgets::Paragraph, 20 | Frame, 21 | }; 22 | 23 | impl<'a> TuiApp<'a> { 24 | pub fn draw(&mut self, f: &mut Frame, terminal_width: u16) { 25 | let chunks = Layout::default() 26 | .constraints([ 27 | Constraint::Min(1), 28 | Constraint::Length(1), 29 | Constraint::Length(1), 30 | Constraint::Length(1), 31 | ]) 32 | .split(f.size()); 33 | 34 | // hits table 35 | self.draw_table(f, chunks[0], terminal_width); 36 | 37 | // status bar 38 | self.draw_status_bar(f, chunks[1]); 39 | 40 | // path of selected row 41 | let text = Span::raw( 42 | self.hits 43 | .get(self.table_state.selected()) 44 | .map(|id| { 45 | self.database 46 | .as_ref() 47 | .unwrap() 48 | .entry(*id) 49 | .path() 50 | .as_str() 51 | .to_owned() 52 | }) 53 | .unwrap_or_default(), 54 | ); 55 | let paragraph = Paragraph::new(text); 56 | f.render_widget(paragraph, chunks[2]); 57 | 58 | // input box 59 | let text_box = TextBox::new() 60 | .highlight_style(Style::default().fg(Color::Black).bg(Color::White)) 61 | .prompt(Span::styled( 62 | "> ", 63 | Style::default() 64 | .fg(self.config.ui.colors.prompt) 65 | .add_modifier(Modifier::BOLD), 66 | )); 67 | f.render_stateful_widget(text_box, chunks[3], &mut self.text_box_state); 68 | } 69 | 70 | fn draw_table(&mut self, f: &mut Frame, area: Rect, terminal_width: u16) { 71 | let columns = &self.config.ui.columns; 72 | 73 | let header = columns.iter().map(|column| { 74 | if column.status == self.config.ui.sort_by { 75 | match self.config.ui.sort_order { 76 | SortOrder::Ascending => format!("{}▲", column.status), 77 | SortOrder::Descending => format!("{}▼", column.status), 78 | } 79 | } else { 80 | column.status.to_string() 81 | } 82 | }); 83 | 84 | #[allow(clippy::needless_collect)] // false positive 85 | let display_func = |id: &EntryId| { 86 | let entry = self.database.as_ref().unwrap().entry(*id); 87 | let contents = columns 88 | .iter() 89 | .map(|column| { 90 | self.format_column_content(&column.status, &entry, self.query.as_ref().unwrap()) 91 | }) 92 | .collect::>(); 93 | Row::new(contents.into_iter()) 94 | }; 95 | 96 | let (num_fixed, sum_widths) = 97 | columns 98 | .iter() 99 | .fold((0, 0), |(num_fixed, sum_widths), column| { 100 | if let Some(width) = column.width { 101 | (num_fixed + 1, sum_widths + width) 102 | } else { 103 | (num_fixed, sum_widths) 104 | } 105 | }); 106 | let remaining_width = terminal_width - sum_widths; 107 | let num_flexible = columns.len() as u16 - num_fixed; 108 | let flexible_width = remaining_width.checked_div(num_flexible); 109 | let widths = columns 110 | .iter() 111 | .map(|column| { 112 | if let Some(width) = column.width { 113 | Constraint::Length(width) 114 | } else { 115 | Constraint::Min(flexible_width.unwrap()) 116 | } 117 | }) 118 | .collect::>(); 119 | 120 | let alignments = columns 121 | .iter() 122 | .map(|column| match column.status { 123 | StatusKind::Size => Alignment::Right, 124 | _ => Alignment::Left, 125 | }) 126 | .collect::>(); 127 | 128 | let table = Table::new(header, self.hits.iter(), display_func) 129 | .widths(&widths) 130 | .alignments(&alignments) 131 | .selected_style( 132 | Style::default() 133 | .fg(self.config.ui.colors.selected_fg) 134 | .bg(self.config.ui.colors.selected_bg), 135 | ) 136 | .highlight_style( 137 | Style::default() 138 | .fg(self.config.ui.colors.matched_fg) 139 | .bg(self.config.ui.colors.matched_bg), 140 | ) 141 | .selected_highlight_style( 142 | Style::default() 143 | .fg(self.config.ui.colors.matched_fg) 144 | .bg(self.config.ui.colors.matched_bg), 145 | ) 146 | .selected_symbol("> ") 147 | .header_gap(1) 148 | .column_spacing(self.config.ui.column_spacing); 149 | 150 | let mut table_state = self.table_state.clone(); 151 | f.render_stateful_widget(table, area, &mut table_state); 152 | self.table_state = table_state; 153 | 154 | self.page_scroll_amount = area 155 | .height 156 | .saturating_sub( 157 | // header 158 | 1 + 159 | // header_gap 160 | 1 + 161 | // one less than page height 162 | 1, 163 | ) 164 | .max(1); 165 | } 166 | 167 | fn draw_status_bar(&self, f: &mut Frame, area: Rect) { 168 | let message = match &self.status { 169 | State::Loading => Span::raw("Loading database"), 170 | State::Searching => Span::raw("Searching"), 171 | State::Ready | State::Aborted | State::Accepted => Span::raw("Ready"), 172 | State::InvalidQuery(msg) => Span::styled( 173 | msg, 174 | Style::default().fg(self.config.ui.colors.error_fg).bg(self 175 | .config 176 | .ui 177 | .colors 178 | .error_bg), 179 | ), 180 | }; 181 | 182 | let counter = self 183 | .database 184 | .as_ref() 185 | .map(|db| format!("{} / {}", self.hits.len(), db.num_entries())) 186 | .unwrap_or_else(|| "".to_string()); 187 | 188 | let chunks = Layout::default() 189 | .constraints([ 190 | Constraint::Min(1), 191 | Constraint::Length(counter.len() as u16 + 1), 192 | ]) 193 | .direction(Direction::Horizontal) 194 | .split(area); 195 | 196 | let message = Paragraph::new(message); 197 | f.render_widget(message, chunks[0]); 198 | 199 | let counter = Span::raw(counter); 200 | let counter = Paragraph::new(counter).alignment(Alignment::Right); 201 | f.render_widget(counter, chunks[1]); 202 | } 203 | 204 | fn format_column_content( 205 | &self, 206 | kind: &StatusKind, 207 | entry: &Entry, 208 | query: &Query, 209 | ) -> HighlightableText>> { 210 | match kind { 211 | StatusKind::Basename => HighlightableText::Highlighted( 212 | entry.basename().to_owned(), 213 | query.basename_matches(entry).into_iter(), 214 | ), 215 | StatusKind::Path => HighlightableText::Highlighted( 216 | entry.path().as_str().to_owned(), 217 | query.path_matches(entry).into_iter(), 218 | ), 219 | StatusKind::Extension => entry 220 | .extension() 221 | .map(|s| s.to_string().into()) 222 | .unwrap_or_default(), 223 | StatusKind::Size => entry 224 | .size() 225 | .map(|size| self.format_size(size, entry.is_dir()).into()) 226 | .unwrap_or_default(), 227 | StatusKind::Mode => entry 228 | .mode() 229 | .map(|mode| self.format_mode(mode).into()) 230 | .unwrap_or_default(), 231 | StatusKind::Created => entry 232 | .created() 233 | .map(|created| self.format_datetime(created).into()) 234 | .unwrap_or_default(), 235 | StatusKind::Modified => entry 236 | .modified() 237 | .map(|modified| self.format_datetime(modified).into()) 238 | .unwrap_or_default(), 239 | StatusKind::Accessed => entry 240 | .accessed() 241 | .map(|accessed| self.format_datetime(accessed).into()) 242 | .unwrap_or_default(), 243 | } 244 | } 245 | 246 | fn format_size(&self, size: u64, is_dir: bool) -> String { 247 | if is_dir { 248 | if size == 1 { 249 | format!("{} item", size) 250 | } else { 251 | format!("{} items", size) 252 | } 253 | } else if self.config.ui.human_readable_size { 254 | size::Size::Bytes(size).to_string(size::Base::Base2, size::Style::Abbreviated) 255 | } else { 256 | size.to_string() 257 | } 258 | } 259 | 260 | fn format_mode(&self, mode: Mode) -> String { 261 | #[cfg(unix)] 262 | { 263 | use crate::config::ModeFormatUnix; 264 | 265 | match self.config.ui.unix.mode_format { 266 | ModeFormatUnix::Octal => mode.display_octal().to_string(), 267 | ModeFormatUnix::Symbolic => mode.display_symbolic().to_string(), 268 | } 269 | } 270 | 271 | #[cfg(windows)] 272 | { 273 | use crate::config::ModeFormatWindows; 274 | 275 | match self.config.ui.windows.mode_format { 276 | ModeFormatWindows::Traditional => mode.display_traditional().to_string(), 277 | ModeFormatWindows::PowerShell => mode.display_powershell().to_string(), 278 | } 279 | } 280 | } 281 | 282 | fn format_datetime(&self, time: SystemTime) -> String { 283 | let datetime = DateTime::::from(time); 284 | datetime.format(&self.config.ui.datetime_format).to_string() 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/bin/ix/tui/handlers.rs: -------------------------------------------------------------------------------- 1 | use super::{State, TuiApp}; 2 | 3 | use indexa::{database::EntryId, query::QueryBuilder}; 4 | 5 | use anyhow::Result; 6 | use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; 7 | 8 | impl<'a> TuiApp<'a> { 9 | pub fn handle_input(&mut self, event: Event) -> Result<()> { 10 | match event { 11 | Event::Key(key) => self.handle_key(key)?, 12 | Event::Mouse(mouse) => self.handle_mouse(mouse)?, 13 | Event::Resize(_, _) => (), 14 | }; 15 | 16 | Ok(()) 17 | } 18 | 19 | fn handle_key(&mut self, key: KeyEvent) -> Result<()> { 20 | match (key.modifiers, key.code) { 21 | (_, KeyCode::Esc) 22 | | (KeyModifiers::CONTROL, KeyCode::Char('c')) 23 | | (KeyModifiers::CONTROL, KeyCode::Char('g')) => self.status = State::Aborted, 24 | (_, KeyCode::Enter) => self.status = State::Accepted, 25 | (_, KeyCode::Up) | (KeyModifiers::CONTROL, KeyCode::Char('p')) => self.on_up()?, 26 | (_, KeyCode::Down) | (KeyModifiers::CONTROL, KeyCode::Char('n')) => self.on_down()?, 27 | (_, KeyCode::PageUp) => self.on_pageup()?, 28 | (_, KeyCode::PageDown) => self.on_pagedown()?, 29 | (KeyModifiers::CONTROL, KeyCode::Home) | (KeyModifiers::SHIFT, KeyCode::Home) => { 30 | self.on_scroll_to_top()?; 31 | } 32 | (KeyModifiers::CONTROL, KeyCode::End) | (KeyModifiers::SHIFT, KeyCode::End) => { 33 | self.on_scroll_to_bottom()?; 34 | } 35 | (_, KeyCode::Backspace) | (KeyModifiers::CONTROL, KeyCode::Char('h')) => { 36 | if self.text_box_state.on_backspace() { 37 | self.handle_query_change()?; 38 | } 39 | } 40 | (_, KeyCode::Delete) | (KeyModifiers::CONTROL, KeyCode::Char('d')) => { 41 | if self.text_box_state.on_delete() { 42 | self.handle_query_change()?; 43 | } 44 | } 45 | (_, KeyCode::Left) | (KeyModifiers::CONTROL, KeyCode::Char('b')) => { 46 | self.text_box_state.on_left(); 47 | } 48 | (_, KeyCode::Right) | (KeyModifiers::CONTROL, KeyCode::Char('f')) => { 49 | self.text_box_state.on_right(); 50 | } 51 | (_, KeyCode::Home) | (KeyModifiers::CONTROL, KeyCode::Char('a')) => { 52 | self.text_box_state.on_home(); 53 | } 54 | (_, KeyCode::End) | (KeyModifiers::CONTROL, KeyCode::Char('e')) => { 55 | self.text_box_state.on_end(); 56 | } 57 | (KeyModifiers::CONTROL, KeyCode::Char('u')) => { 58 | self.text_box_state.clear(); 59 | self.handle_query_change()?; 60 | } 61 | (_, KeyCode::Char(c)) => { 62 | self.text_box_state.on_char(c); 63 | self.handle_query_change()?; 64 | } 65 | _ => (), 66 | }; 67 | 68 | Ok(()) 69 | } 70 | 71 | fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<()> { 72 | match mouse.kind { 73 | MouseEventKind::ScrollUp => self.on_up()?, 74 | MouseEventKind::ScrollDown => self.on_down()?, 75 | _ => (), 76 | }; 77 | 78 | Ok(()) 79 | } 80 | 81 | fn on_up(&mut self) -> Result<()> { 82 | if !self.hits.is_empty() { 83 | self.table_state 84 | .select(self.table_state.selected().saturating_sub(1)); 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | fn on_down(&mut self) -> Result<()> { 91 | if !self.hits.is_empty() { 92 | self.table_state 93 | .select((self.table_state.selected() + 1).min(self.hits.len() - 1)); 94 | } 95 | 96 | Ok(()) 97 | } 98 | 99 | fn on_pageup(&mut self) -> Result<()> { 100 | if !self.hits.is_empty() { 101 | self.table_state.select( 102 | self.table_state 103 | .selected() 104 | .saturating_sub(self.page_scroll_amount as usize), 105 | ); 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | fn on_pagedown(&mut self) -> Result<()> { 112 | if !self.hits.is_empty() { 113 | self.table_state.select( 114 | (self.table_state.selected() + self.page_scroll_amount as usize) 115 | .min(self.hits.len() - 1), 116 | ); 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | fn on_scroll_to_top(&mut self) -> Result<()> { 123 | if !self.hits.is_empty() { 124 | self.table_state.select(0); 125 | } 126 | 127 | Ok(()) 128 | } 129 | 130 | fn on_scroll_to_bottom(&mut self) -> Result<()> { 131 | if !self.hits.is_empty() { 132 | self.table_state.select(self.hits.len() - 1); 133 | } 134 | 135 | Ok(()) 136 | } 137 | 138 | pub fn handle_search_result(&mut self, hits: Vec) -> Result<()> { 139 | self.hits = hits; 140 | self.status = State::Ready; 141 | 142 | if !self.hits.is_empty() { 143 | self.table_state 144 | .select(self.table_state.selected().min(self.hits.len() - 1)); 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | pub fn handle_accept(&self) -> Result<()> { 151 | if let Some(id) = self.hits.get(self.table_state.selected()) { 152 | println!("{}", self.database.as_ref().unwrap().entry(*id).path()); 153 | } 154 | Ok(()) 155 | } 156 | 157 | pub fn handle_query_change(&mut self) -> Result<()> { 158 | if self.database.is_none() { 159 | return Ok(()); 160 | } 161 | 162 | let query = self.text_box_state.text(); 163 | let query = QueryBuilder::new(query) 164 | .match_path_mode(self.config.flags.match_path) 165 | .case_sensitivity(self.config.flags.case_sensitivity()) 166 | .regex(self.config.flags.regex) 167 | .sort_by(self.config.ui.sort_by) 168 | .sort_order(self.config.ui.sort_order) 169 | .sort_dirs_before_files(self.config.ui.sort_dirs_before_files) 170 | .build(); 171 | 172 | match query { 173 | Ok(query) => { 174 | self.query = Some(query.clone()); 175 | self.status = State::Searching; 176 | self.searcher.as_mut().unwrap().search(query); 177 | } 178 | Err(err) => { 179 | let err_str = err.to_string(); 180 | let err_str = err_str.trim(); 181 | 182 | // HACK: extract last line to fit in status bar 183 | let err_str = err_str 184 | .rsplit_once('\n') 185 | .map(|(_, s)| s.trim()) 186 | .unwrap_or(err_str); 187 | 188 | // capitalize first letter 189 | let mut chars = err_str.chars(); 190 | let err_str = chars 191 | .next() 192 | .map(|c| c.to_uppercase().collect::() + chars.as_str()) 193 | .unwrap_or_else(|| err_str.to_owned()); 194 | 195 | self.status = State::InvalidQuery(err_str); 196 | } 197 | } 198 | 199 | Ok(()) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/bin/ix/tui/table.rs: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2016 Florian Dehau 4 | // Copyright (c) 2020-present mosm 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | // originally from https://github.com/fdehau/tui-rs/blob/72511867624c9bc416e64a1b856026ced5c4e1eb/src/widgets/table.rs 25 | 26 | use cassowary::{ 27 | strength::{MEDIUM, REQUIRED, WEAK}, 28 | Expression, Solver, 29 | WeightedRelation::*, 30 | }; 31 | use itertools::izip; 32 | use std::{ 33 | collections::HashMap, 34 | fmt::Display, 35 | iter::{self, Iterator}, 36 | ops::Range, 37 | }; 38 | use tui::{ 39 | buffer::Buffer, 40 | layout::{Alignment, Constraint, Rect}, 41 | style::Style, 42 | text::{Span, Spans}, 43 | widgets::{Block, Paragraph, StatefulWidget, Widget}, 44 | }; 45 | use unicode_width::UnicodeWidthStr; 46 | 47 | #[derive(Default, Debug, Clone)] 48 | pub struct TableState { 49 | offset: usize, 50 | selected: usize, 51 | } 52 | 53 | impl TableState { 54 | pub fn selected(&self) -> usize { 55 | self.selected 56 | } 57 | 58 | pub fn select(&mut self, index: usize) { 59 | self.selected = index; 60 | } 61 | } 62 | 63 | #[derive(Debug, Clone)] 64 | pub enum HighlightableText 65 | where 66 | M: Iterator>, 67 | { 68 | Raw(String), 69 | Highlighted(String, M), 70 | } 71 | 72 | impl Default for HighlightableText 73 | where 74 | M: Iterator>, 75 | { 76 | fn default() -> Self { 77 | Self::Raw(String::new()) 78 | } 79 | } 80 | 81 | impl From for HighlightableText 82 | where 83 | M: Iterator>, 84 | { 85 | fn from(s: String) -> Self { 86 | Self::Raw(s) 87 | } 88 | } 89 | 90 | #[derive(Debug, Clone)] 91 | pub struct Row 92 | where 93 | M: Iterator>, 94 | D: Iterator>, 95 | { 96 | data: D, 97 | } 98 | 99 | impl Row 100 | where 101 | M: Iterator>, 102 | D: Iterator>, 103 | { 104 | pub fn new(data: D) -> Self { 105 | Self { data } 106 | } 107 | } 108 | 109 | #[derive(Debug, Clone)] 110 | pub struct Table<'a, H, R, F> { 111 | block: Option>, 112 | style: Style, 113 | header: H, 114 | header_style: Style, 115 | widths: &'a [Constraint], 116 | alignments: Option<&'a [Alignment]>, 117 | column_spacing: u16, 118 | header_gap: u16, 119 | selected_style: Style, 120 | highlight_style: Style, 121 | selected_highlight_style: Style, 122 | selected_symbol: Option<&'a str>, 123 | rows: R, 124 | display_func: F, 125 | } 126 | 127 | impl<'a, H, R, M, D, F, T> Table<'a, H, R, F> 128 | where 129 | H: Iterator, 130 | H::Item: Display, 131 | M: Iterator>, 132 | D: Iterator>, 133 | R: ExactSizeIterator, 134 | F: Fn(T) -> Row, 135 | { 136 | pub fn new(header: H, rows: R, display_func: F) -> Table<'a, H, R, F> { 137 | Table { 138 | block: None, 139 | style: Style::default(), 140 | header, 141 | header_style: Style::default(), 142 | widths: &[], 143 | alignments: None, 144 | column_spacing: 1, 145 | header_gap: 1, 146 | selected_style: Style::default(), 147 | highlight_style: Style::default(), 148 | selected_highlight_style: Style::default(), 149 | selected_symbol: None, 150 | rows, 151 | display_func, 152 | } 153 | } 154 | 155 | #[allow(dead_code)] 156 | pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R, F> { 157 | self.block = Some(block); 158 | self 159 | } 160 | 161 | #[allow(dead_code)] 162 | pub fn header(mut self, header: II) -> Table<'a, H, R, F> 163 | where 164 | II: IntoIterator, 165 | { 166 | self.header = header.into_iter(); 167 | self 168 | } 169 | 170 | #[allow(dead_code)] 171 | pub fn header_style(mut self, style: Style) -> Table<'a, H, R, F> { 172 | self.header_style = style; 173 | self 174 | } 175 | 176 | pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R, F> { 177 | let between_0_and_100 = |&w| match w { 178 | Constraint::Percentage(p) => p <= 100, 179 | _ => true, 180 | }; 181 | assert!( 182 | widths.iter().all(between_0_and_100), 183 | "Percentages should be between 0 and 100 inclusively." 184 | ); 185 | self.widths = widths; 186 | self 187 | } 188 | 189 | pub fn alignments(mut self, alignments: &'a [Alignment]) -> Table<'a, H, R, F> { 190 | self.alignments = Some(alignments); 191 | self 192 | } 193 | 194 | #[allow(dead_code)] 195 | pub fn rows(mut self, rows: II) -> Table<'a, H, R, F> 196 | where 197 | II: IntoIterator, 198 | { 199 | self.rows = rows.into_iter(); 200 | self 201 | } 202 | 203 | #[allow(dead_code)] 204 | pub fn style(mut self, style: Style) -> Table<'a, H, R, F> { 205 | self.style = style; 206 | self 207 | } 208 | 209 | pub fn selected_symbol(mut self, selected_symbol: &'a str) -> Table<'a, H, R, F> { 210 | self.selected_symbol = Some(selected_symbol); 211 | self 212 | } 213 | 214 | pub fn selected_style(mut self, selected_style: Style) -> Table<'a, H, R, F> { 215 | self.selected_style = selected_style; 216 | self 217 | } 218 | 219 | pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R, F> { 220 | self.highlight_style = highlight_style; 221 | self 222 | } 223 | 224 | pub fn selected_highlight_style( 225 | mut self, 226 | selected_highlight_style: Style, 227 | ) -> Table<'a, H, R, F> { 228 | self.selected_highlight_style = selected_highlight_style; 229 | self 230 | } 231 | 232 | pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R, F> { 233 | self.column_spacing = spacing; 234 | self 235 | } 236 | 237 | pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R, F> { 238 | self.header_gap = gap; 239 | self 240 | } 241 | } 242 | 243 | impl<'a, H, R, M, D, F, T> StatefulWidget for Table<'a, H, R, F> 244 | where 245 | H: Iterator, 246 | H::Item: Display, 247 | M: Iterator>, 248 | D: Iterator>, 249 | R: ExactSizeIterator, 250 | F: Fn(T) -> Row, 251 | { 252 | type State = TableState; 253 | 254 | fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 255 | buf.set_style(area, self.style); 256 | let table_area = match self.block.take() { 257 | Some(b) => { 258 | let inner_area = b.inner(area); 259 | b.render(area, buf); 260 | inner_area 261 | } 262 | None => area, 263 | }; 264 | 265 | let mut solver = Solver::new(); 266 | let mut var_indices = HashMap::new(); 267 | let mut ccs = Vec::new(); 268 | let mut variables = Vec::new(); 269 | for i in 0..self.widths.len() { 270 | let var = cassowary::Variable::new(); 271 | variables.push(var); 272 | var_indices.insert(var, i); 273 | } 274 | for (i, constraint) in self.widths.iter().enumerate() { 275 | ccs.push(variables[i] | GE(WEAK) | 0.); 276 | ccs.push(match *constraint { 277 | Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v), 278 | Constraint::Percentage(v) => { 279 | variables[i] | EQ(WEAK) | (f64::from(v * area.width) / 100.0) 280 | } 281 | Constraint::Ratio(n, d) => { 282 | variables[i] | EQ(WEAK) | (f64::from(area.width) * f64::from(n) / f64::from(d)) 283 | } 284 | Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v), 285 | Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v), 286 | }) 287 | } 288 | solver 289 | .add_constraint( 290 | variables 291 | .iter() 292 | .fold(Expression::from_constant(0.), |acc, v| acc + *v) 293 | | LE(REQUIRED) 294 | | f64::from( 295 | area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)), 296 | ), 297 | ) 298 | .unwrap(); 299 | solver.add_constraints(&ccs).unwrap(); 300 | let mut solved_widths = vec![0; variables.len()]; 301 | for &(var, value) in solver.fetch_changes() { 302 | let index = var_indices[&var]; 303 | let value = if value.is_sign_negative() { 304 | 0 305 | } else { 306 | value as u16 307 | }; 308 | solved_widths[index] = value 309 | } 310 | 311 | let alignments: Vec<_> = if let Some(alignments) = self.alignments { 312 | alignments.iter().collect() 313 | } else { 314 | iter::repeat(&Alignment::Left) 315 | .take(self.widths.len()) 316 | .collect() 317 | }; 318 | 319 | let mut y = table_area.top(); 320 | let mut x = table_area.left(); 321 | 322 | // Draw header 323 | if y < table_area.bottom() { 324 | for (w, &&alignment, t) in izip!( 325 | solved_widths.iter(), 326 | alignments.iter(), 327 | self.header.by_ref(), 328 | ) { 329 | let area = Rect { 330 | x, 331 | y, 332 | width: *w, 333 | height: 1, 334 | }; 335 | let text = Span::styled(t.to_string(), self.header_style); 336 | Paragraph::new(text).alignment(alignment).render(area, buf); 337 | 338 | x += *w + self.column_spacing; 339 | } 340 | } 341 | y += 1 + self.header_gap; 342 | 343 | let selected_symbol = self.selected_symbol.unwrap_or(""); 344 | let blank_symbol = " ".repeat(selected_symbol.width()); 345 | 346 | // Draw rows 347 | let default_style = Style::default(); 348 | if y < table_area.bottom() { 349 | let remaining = (table_area.bottom() - y) as usize; 350 | 351 | state.offset = state.offset.min(self.rows.len().saturating_sub(remaining)); 352 | state.offset = if state.selected >= remaining + state.offset - 1 { 353 | state.selected + 1 - remaining 354 | } else if state.selected < state.offset { 355 | state.selected 356 | } else { 357 | state.offset 358 | }; 359 | 360 | for (i, row) in self 361 | .rows 362 | .skip(state.offset) 363 | .take(remaining) 364 | .map(self.display_func) 365 | .enumerate() 366 | { 367 | let (style, highlight_style, symbol) = { 368 | if i == state.selected - state.offset { 369 | ( 370 | self.selected_style, 371 | self.selected_highlight_style, 372 | selected_symbol, 373 | ) 374 | } else { 375 | (default_style, self.highlight_style, blank_symbol.as_ref()) 376 | } 377 | }; 378 | 379 | x = table_area.left(); 380 | 381 | buf.set_stringn(x, y + i as u16, &symbol, symbol.width(), style); 382 | x += symbol.width() as u16; 383 | 384 | for (c, (w, &&alignment, elt)) in 385 | izip!(solved_widths.iter(), alignments.iter(), row.data).enumerate() 386 | { 387 | let width = if c == 0 { 388 | *w - symbol.width() as u16 389 | } else { 390 | *w 391 | }; 392 | let area = Rect { 393 | x, 394 | y: y + i as u16, 395 | width, 396 | height: 1, 397 | }; 398 | 399 | match elt { 400 | HighlightableText::Raw(text) => { 401 | let text = Span::styled(&text, style); 402 | Paragraph::new(text).alignment(alignment).render(area, buf); 403 | } 404 | HighlightableText::Highlighted(text, ranges) => { 405 | let text = build_spans(&text, ranges, &style, &highlight_style); 406 | Paragraph::new(text).alignment(alignment).render(area, buf); 407 | } 408 | } 409 | 410 | x += width + self.column_spacing; 411 | } 412 | } 413 | } 414 | } 415 | } 416 | 417 | impl<'a, H, R, M, D, F, T> Widget for Table<'a, H, R, F> 418 | where 419 | H: Iterator, 420 | H::Item: Display, 421 | M: Iterator>, 422 | D: Iterator>, 423 | R: ExactSizeIterator, 424 | F: Fn(T) -> Row, 425 | { 426 | fn render(self, area: Rect, buf: &mut Buffer) { 427 | let mut state = TableState::default(); 428 | StatefulWidget::render(self, area, buf, &mut state); 429 | } 430 | } 431 | 432 | fn build_spans<'t, M>( 433 | text: &'t str, 434 | matches: M, 435 | style: &Style, 436 | highlight_style: &Style, 437 | ) -> Spans<'t> 438 | where 439 | M: Iterator>, 440 | { 441 | let mut prev_end = 0; 442 | let mut texts = Vec::new(); 443 | for m in matches { 444 | if m.start > prev_end { 445 | texts.push(Span::styled(&text[prev_end..m.start], *style)); 446 | } 447 | if m.end > m.start { 448 | texts.push(Span::styled(&text[m.start..m.end], *highlight_style)); 449 | } 450 | prev_end = m.end; 451 | } 452 | if prev_end < text.len() { 453 | texts.push(Span::styled(&text[prev_end..], *style)); 454 | } 455 | Spans::from(texts) 456 | } 457 | -------------------------------------------------------------------------------- /src/bin/ix/tui/text_box.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | buffer::Buffer, 3 | layout::Rect, 4 | style::Style, 5 | text::{Span, Spans}, 6 | widgets::{Paragraph, StatefulWidget, Widget}, 7 | }; 8 | use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; 9 | 10 | pub struct TextBox<'b> { 11 | style: Style, 12 | highlight_style: Style, 13 | prompt: Span<'b>, 14 | } 15 | 16 | impl<'b> TextBox<'b> { 17 | pub fn new() -> Self { 18 | Self { 19 | style: Default::default(), 20 | highlight_style: Default::default(), 21 | prompt: Span::raw(""), 22 | } 23 | } 24 | 25 | #[allow(dead_code)] 26 | pub fn style(mut self, style: Style) -> Self { 27 | self.style = style; 28 | self 29 | } 30 | 31 | pub fn highlight_style(mut self, style: Style) -> Self { 32 | self.highlight_style = style; 33 | self 34 | } 35 | 36 | pub fn prompt(mut self, prompt: Span<'b>) -> Self { 37 | self.prompt = prompt; 38 | self 39 | } 40 | } 41 | 42 | impl StatefulWidget for TextBox<'_> { 43 | type State = TextBoxState; 44 | 45 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 46 | let grapheme_indices = UnicodeSegmentation::grapheme_indices(state.text.as_str(), true); 47 | let cursor = state.grapheme_cursor.cur_cursor(); 48 | 49 | let mut text = vec![self.prompt.clone()]; 50 | text.extend(grapheme_indices.map(|(i, grapheme)| { 51 | if i == cursor { 52 | Span::styled(grapheme, self.highlight_style) 53 | } else { 54 | Span::styled(grapheme, self.style) 55 | } 56 | })); 57 | if cursor >= state.text.len() { 58 | text.push(Span::styled(" ", self.highlight_style)); 59 | } 60 | 61 | let paragraph = Paragraph::new(Spans::from(text)); 62 | paragraph.render(area, buf); 63 | } 64 | } 65 | 66 | pub struct TextBoxState { 67 | text: String, 68 | grapheme_cursor: GraphemeCursor, 69 | } 70 | 71 | impl TextBoxState { 72 | #[allow(dead_code)] 73 | pub fn new() -> Self { 74 | Default::default() 75 | } 76 | 77 | pub fn with_text(text: String) -> Self { 78 | let len = text.len(); 79 | Self { 80 | text, 81 | grapheme_cursor: GraphemeCursor::new(len, len, true), 82 | } 83 | } 84 | 85 | pub fn text(&self) -> &str { 86 | &self.text 87 | } 88 | 89 | pub fn clear(&mut self) { 90 | self.text.clear(); 91 | self.grapheme_cursor = GraphemeCursor::new(0, 0, true); 92 | } 93 | 94 | pub fn on_char(&mut self, ch: char) { 95 | let cursor = self.grapheme_cursor.cur_cursor(); 96 | self.text.insert(cursor, ch); 97 | self.grapheme_cursor = GraphemeCursor::new(cursor, self.text.len(), true); 98 | self.grapheme_cursor 99 | .next_boundary(&self.text[cursor..], cursor) 100 | .unwrap(); 101 | } 102 | 103 | pub fn on_backspace(&mut self) -> bool { 104 | let prev_cursor = self.grapheme_cursor.cur_cursor(); 105 | if prev_cursor > 0 { 106 | self.grapheme_cursor 107 | .prev_boundary(&self.text[..prev_cursor], 0) 108 | .unwrap(); 109 | 110 | let new_cursor = self.grapheme_cursor.cur_cursor(); 111 | self.text.remove(new_cursor); 112 | self.grapheme_cursor = GraphemeCursor::new(new_cursor, self.text.len(), true); 113 | 114 | true 115 | } else { 116 | false 117 | } 118 | } 119 | 120 | pub fn on_delete(&mut self) -> bool { 121 | let cursor = self.grapheme_cursor.cur_cursor(); 122 | if cursor < self.text.len() { 123 | self.text.remove(cursor); 124 | self.grapheme_cursor = GraphemeCursor::new(cursor, self.text.len(), true); 125 | true 126 | } else { 127 | false 128 | } 129 | } 130 | 131 | pub fn on_left(&mut self) -> bool { 132 | let prev_cursor = self.grapheme_cursor.cur_cursor(); 133 | self.grapheme_cursor 134 | .prev_boundary(&self.text[..prev_cursor], 0) 135 | .unwrap(); 136 | self.grapheme_cursor.cur_cursor() < prev_cursor 137 | } 138 | 139 | pub fn on_right(&mut self) -> bool { 140 | let prev_cursor = self.grapheme_cursor.cur_cursor(); 141 | self.grapheme_cursor 142 | .next_boundary(&self.text[prev_cursor..], prev_cursor) 143 | .unwrap(); 144 | self.grapheme_cursor.cur_cursor() > prev_cursor 145 | } 146 | 147 | pub fn on_home(&mut self) -> bool { 148 | if self.grapheme_cursor.cur_cursor() > 0 { 149 | self.grapheme_cursor = GraphemeCursor::new(0, self.text.len(), true); 150 | true 151 | } else { 152 | false 153 | } 154 | } 155 | 156 | pub fn on_end(&mut self) -> bool { 157 | if self.grapheme_cursor.cur_cursor() < self.text.len() { 158 | self.grapheme_cursor = GraphemeCursor::new(self.text.len(), self.text.len(), true); 159 | true 160 | } else { 161 | false 162 | } 163 | } 164 | } 165 | 166 | impl Default for TextBoxState { 167 | fn default() -> Self { 168 | Self { 169 | text: "".to_string(), 170 | grapheme_cursor: GraphemeCursor::new(0, 0, true), 171 | } 172 | } 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | use super::*; 178 | 179 | #[test] 180 | fn edit() { 181 | let mut state = TextBoxState::new(); 182 | assert_eq!("", state.text()); 183 | state.on_char('a'); 184 | assert_eq!("a", state.text()); 185 | state.on_left(); 186 | state.on_char('x'); 187 | assert_eq!("xa", state.text()); 188 | state.on_char('あ'); 189 | assert_eq!("xあa", state.text()); 190 | state.on_backspace(); 191 | assert_eq!("xa", state.text()); 192 | state.on_end(); 193 | state.on_char('亜'); 194 | assert_eq!("xa亜", state.text()); 195 | state.on_left(); 196 | state.on_delete(); 197 | assert_eq!("xa", state.text()); 198 | state.on_home(); 199 | state.on_char('𠮷'); 200 | assert_eq!("𠮷xa", state.text()); 201 | state.on_right(); 202 | state.on_char('b'); 203 | assert_eq!("𠮷xba", state.text()); 204 | 205 | let mut state2 = TextBoxState::with_text("𠮷x".to_string()); 206 | state2.on_char('b'); 207 | state2.on_char('a'); 208 | assert_eq!(state.text(), state2.text()); 209 | 210 | state.clear(); 211 | assert_eq!("", state.text()); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod indexer; 3 | mod search; 4 | mod util; 5 | 6 | pub use builder::DatabaseBuilder; 7 | 8 | use crate::{mode::Mode, Result}; 9 | 10 | use camino::Utf8PathBuf; 11 | use enum_map::{Enum, EnumMap}; 12 | use fxhash::FxHashMap; 13 | use serde::{Deserialize, Serialize}; 14 | use std::{cmp::Ordering, time::SystemTime}; 15 | use strum_macros::{Display, EnumIter}; 16 | 17 | // Database can have multiple "root" entries, which correspond to directories 18 | // specified in "dirs" in config. 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | pub struct Database { 22 | /// names of all entries concatenated 23 | name_arena: String, 24 | nodes: Vec, 25 | root_paths: FxHashMap, 26 | size: Option>, 27 | mode: Option>, 28 | created: Option>, 29 | modified: Option>, 30 | accessed: Option>, 31 | sorted_ids: EnumMap>>, 32 | } 33 | 34 | impl Database { 35 | #[inline] 36 | pub fn num_entries(&self) -> usize { 37 | self.nodes.len() 38 | } 39 | 40 | #[inline] 41 | pub fn entries(&self) -> impl ExactSizeIterator> { 42 | (0..self.nodes.len() as u32).map(move |id| self.entry(EntryId(id))) 43 | } 44 | 45 | #[inline] 46 | pub fn root_entries(&self) -> impl ExactSizeIterator> { 47 | self.root_paths 48 | .keys() 49 | .map(move |id| self.entry(EntryId(*id))) 50 | } 51 | 52 | #[inline] 53 | pub fn is_indexed(&self, kind: StatusKind) -> bool { 54 | match kind { 55 | StatusKind::Basename | StatusKind::Path | StatusKind::Extension => true, 56 | StatusKind::Size => self.size.is_some(), 57 | StatusKind::Mode => self.mode.is_some(), 58 | StatusKind::Created => self.created.is_some(), 59 | StatusKind::Modified => self.modified.is_some(), 60 | StatusKind::Accessed => self.accessed.is_some(), 61 | } 62 | } 63 | 64 | #[inline] 65 | pub fn is_fast_sortable(&self, kind: StatusKind) -> bool { 66 | self.sorted_ids[kind].is_some() 67 | } 68 | 69 | #[inline] 70 | pub fn entry(&self, id: EntryId) -> Entry<'_> { 71 | Entry { database: self, id } 72 | } 73 | 74 | #[inline] 75 | fn basename_from_node(&self, node: &EntryNode) -> &str { 76 | &self.name_arena[node.name_start..node.name_start + node.name_len as usize] 77 | } 78 | 79 | #[inline] 80 | fn path_from_id(&self, id: u32) -> Utf8PathBuf { 81 | let node = &self.nodes[id as usize]; 82 | if node.parent == id { 83 | // root node 84 | self.root_paths[&id].clone() 85 | } else { 86 | let mut buf = self.path_from_id(node.parent); 87 | buf.push(&self.basename_from_node(node)); 88 | buf 89 | } 90 | } 91 | 92 | fn cmp_by_path(&self, id_a: u32, id_b: u32) -> Ordering { 93 | // -- Fast path -- 94 | 95 | if id_a == id_b { 96 | return Ordering::Equal; 97 | } 98 | 99 | let node_a = &self.nodes[id_a as usize]; 100 | let node_b = &self.nodes[id_b as usize]; 101 | 102 | let a_is_root = node_a.parent == id_a; 103 | let b_is_root = node_b.parent == id_b; 104 | 105 | if a_is_root && b_is_root { 106 | // e.g. C:\ vs. D:\ 107 | return Ord::cmp(&self.root_paths[&id_a], &self.root_paths[&id_b]); 108 | } 109 | 110 | if !a_is_root && !b_is_root && node_a.parent == node_b.parent { 111 | // e.g. /foo/bar vs. /foo/baz 112 | return Ord::cmp( 113 | self.basename_from_node(node_a), 114 | self.basename_from_node(node_b), 115 | ); 116 | } 117 | 118 | if !b_is_root && id_a == node_b.parent { 119 | // e.g. /foo vs. /foo/bar 120 | return Ordering::Less; 121 | } 122 | if !a_is_root && id_b == node_a.parent { 123 | // e.g. /foo/bar vs. /foo 124 | return Ordering::Greater; 125 | } 126 | 127 | // -- Slow path -- 128 | 129 | // "path" in the sense of graph 130 | fn path_from_root(db: &Database, mut id: u32) -> impl Iterator { 131 | let mut path = Vec::new(); 132 | loop { 133 | let node = &db.nodes[id as usize]; 134 | path.push(id); 135 | if node.parent == id { 136 | // root node 137 | return path.into_iter().rev(); 138 | } else { 139 | id = node.parent; 140 | } 141 | } 142 | } 143 | 144 | let mut path_a = path_from_root(self, id_a); 145 | let mut path_b = path_from_root(self, id_b); 146 | loop { 147 | match (path_a.next(), path_b.next()) { 148 | (Some(a), Some(b)) if a == b => continue, 149 | (None, None) => return Ordering::Equal, 150 | (None, Some(_)) => return Ordering::Less, // /foo vs. /foo/bar 151 | (Some(_), None) => return Ordering::Greater, // /foo/bar vs. /foo 152 | (Some(a), Some(b)) => { 153 | // /foo/bar vs. /foo/baz 154 | return Ord::cmp( 155 | self.basename_from_node(&self.nodes[a as usize]), 156 | self.basename_from_node(&self.nodes[b as usize]), 157 | ); 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Enum, Display, EnumIter)] 165 | #[serde(rename_all = "lowercase")] 166 | pub enum StatusKind { 167 | #[serde(alias = "name")] 168 | Basename, 169 | Path, 170 | #[serde(alias = "ext")] 171 | Extension, 172 | Size, 173 | #[serde( 174 | alias = "attribute", 175 | alias = "attributes", 176 | alias = "attr", 177 | alias = "attrs" 178 | )] 179 | Mode, 180 | #[serde(alias = "ctime")] 181 | Created, 182 | #[serde(alias = "mtime")] 183 | Modified, 184 | #[serde(alias = "atime")] 185 | Accessed, 186 | } 187 | 188 | type StatusFlags = EnumMap; 189 | 190 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 191 | pub struct EntryId(u32); 192 | 193 | /// A convenience struct which acts as if it holds data of the entry. 194 | /// 195 | /// If a requested status is indexed, Entry grabs it from database. 196 | /// Otherwise the status is fetched from file systems. 197 | #[derive(Debug, Clone)] 198 | pub struct Entry<'a> { 199 | database: &'a Database, 200 | id: EntryId, 201 | } 202 | 203 | impl<'a> Entry<'a> { 204 | #[inline] 205 | pub fn is_dir(&self) -> bool { 206 | self.node().is_dir 207 | } 208 | 209 | #[inline] 210 | pub fn children(&self) -> impl ExactSizeIterator> { 211 | let node = &self.node(); 212 | (node.child_start..node.child_end).map(move |id| self.database.entry(EntryId(id))) 213 | } 214 | 215 | #[inline] 216 | pub fn basename(&self) -> &str { 217 | self.database.basename_from_node(self.node()) 218 | } 219 | 220 | #[inline] 221 | pub fn path(&self) -> Utf8PathBuf { 222 | self.database.path_from_id(self.id.0) 223 | } 224 | 225 | #[inline] 226 | pub fn extension(&self) -> Option<&str> { 227 | let node = self.node(); 228 | if node.is_dir { 229 | return None; 230 | } 231 | 232 | self.database 233 | .basename_from_node(node) 234 | .rsplit_once('.') 235 | .map(|(_, ext)| ext) 236 | } 237 | 238 | #[inline] 239 | pub fn size(&self) -> Result { 240 | if let Some(size) = &self.database.size { 241 | return Ok(size[self.id.0 as usize]); 242 | } 243 | 244 | let size = if self.is_dir() { 245 | self.path().read_dir().map(|rd| rd.count() as u64)? 246 | } else { 247 | self.path() 248 | .symlink_metadata() 249 | .map(|metadata| metadata.len())? 250 | }; 251 | 252 | Ok(size) 253 | } 254 | 255 | #[inline] 256 | pub fn mode(&self) -> Result { 257 | if let Some(mode) = &self.database.mode { 258 | return Ok(mode[self.id.0 as usize]); 259 | } 260 | 261 | self.path() 262 | .symlink_metadata() 263 | .map(|metadata| Mode::from(&metadata)) 264 | .map_err(Into::into) 265 | } 266 | 267 | #[inline] 268 | pub fn created(&self) -> Result { 269 | if let Some(created) = &self.database.created { 270 | return Ok(created[self.id.0 as usize]); 271 | } 272 | 273 | self.path() 274 | .symlink_metadata() 275 | .and_then(|metadata| metadata.created()) 276 | .map(|created| util::sanitize_system_time(&created)) 277 | .map_err(Into::into) 278 | } 279 | 280 | #[inline] 281 | pub fn modified(&self) -> Result { 282 | if let Some(modified) = &self.database.modified { 283 | return Ok(modified[self.id.0 as usize]); 284 | } 285 | 286 | self.path() 287 | .symlink_metadata() 288 | .and_then(|metadata| metadata.modified()) 289 | .map(|modified| util::sanitize_system_time(&modified)) 290 | .map_err(Into::into) 291 | } 292 | 293 | #[inline] 294 | pub fn accessed(&self) -> Result { 295 | if let Some(accessed) = &self.database.accessed { 296 | return Ok(accessed[self.id.0 as usize]); 297 | } 298 | 299 | self.path() 300 | .symlink_metadata() 301 | .and_then(|metadata| metadata.accessed()) 302 | .map(|accessed| util::sanitize_system_time(&accessed)) 303 | .map_err(Into::into) 304 | } 305 | 306 | #[inline] 307 | fn node(&self) -> &EntryNode { 308 | &self.database.nodes[self.id.0 as usize] 309 | } 310 | 311 | #[inline] 312 | fn cmp_by_path(&self, other: &Self) -> Ordering { 313 | self.database.cmp_by_path(self.id.0, other.id.0) 314 | } 315 | 316 | #[inline] 317 | fn cmp_by_extension(&self, other: &Self) -> Ordering { 318 | if self.node().is_dir && other.node().is_dir { 319 | return Ordering::Equal; 320 | } 321 | self.extension().cmp(&other.extension()) 322 | } 323 | } 324 | 325 | #[derive(Debug, Serialize, Deserialize)] 326 | struct EntryNode { 327 | name_start: usize, 328 | parent: u32, 329 | child_start: u32, 330 | child_end: u32, 331 | name_len: u16, 332 | is_dir: bool, 333 | } 334 | 335 | impl EntryNode { 336 | #[inline] 337 | fn has_any_child(&self) -> bool { 338 | self.child_start < self.child_end 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/database/builder.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | indexer::{IndexOptions, Indexer}, 3 | util, Database, EntryId, StatusFlags, StatusKind, 4 | }; 5 | use crate::{Error, Result}; 6 | 7 | use enum_map::{enum_map, EnumMap}; 8 | use rayon::prelude::*; 9 | use std::path::PathBuf; 10 | 11 | #[derive(Default)] 12 | pub struct DatabaseBuilder { 13 | dirs: Vec, 14 | index_options: IndexOptions, 15 | fast_sort_flags: StatusFlags, 16 | } 17 | 18 | impl DatabaseBuilder { 19 | pub fn new() -> Self { 20 | Self { 21 | dirs: Vec::new(), 22 | index_options: Default::default(), 23 | fast_sort_flags: enum_map! { 24 | StatusKind::Basename => true, 25 | StatusKind::Path => false, 26 | StatusKind::Extension => false, 27 | StatusKind::Size => false, 28 | StatusKind::Mode => false, 29 | StatusKind::Created => false, 30 | StatusKind::Modified => false, 31 | StatusKind::Accessed => false, 32 | }, 33 | } 34 | } 35 | 36 | pub fn add_dir>(&mut self, path: P) -> &mut Self { 37 | self.dirs.push(path.into()); 38 | self 39 | } 40 | 41 | pub fn index(&mut self, kind: StatusKind) -> &mut Self { 42 | self.index_options.index_flags[kind] = true; 43 | self 44 | } 45 | 46 | pub fn fast_sort(&mut self, kind: StatusKind) -> &mut Self { 47 | self.fast_sort_flags[kind] = true; 48 | self 49 | } 50 | 51 | pub fn ignore_hidden(&mut self, yes: bool) -> &mut Self { 52 | self.index_options.ignore_hidden = yes; 53 | self 54 | } 55 | 56 | pub fn build(&self) -> Result { 57 | for (kind, enabled) in self.fast_sort_flags { 58 | if enabled && !self.index_options.index_flags[kind] { 59 | return Err(Error::InvalidOption( 60 | "Fast sorting cannot be enabled for a non-indexed status.".to_string(), 61 | )); 62 | } 63 | } 64 | 65 | let dirs = util::canonicalize_dirs(&self.dirs)?; 66 | let mut indexer = Indexer::new(&self.index_options); 67 | 68 | for path in dirs { 69 | indexer = indexer.index(path)?; 70 | } 71 | 72 | let mut database = indexer.finish(); 73 | 74 | let mut sorted_ids = EnumMap::default(); 75 | for (kind, ids) in sorted_ids.iter_mut() { 76 | if self.fast_sort_flags[kind] { 77 | *ids = Some(sort_ids(&database, kind)); 78 | } 79 | } 80 | database.sorted_ids = sorted_ids; 81 | 82 | Ok(database) 83 | } 84 | } 85 | 86 | fn sort_ids(database: &Database, sort_by: StatusKind) -> Vec { 87 | let compare_func = util::get_compare_func(sort_by); 88 | 89 | let mut ids = (0..database.nodes.len() as u32).collect::>(); 90 | ids.as_parallel_slice_mut().par_sort_unstable_by(|a, b| { 91 | compare_func(&database.entry(EntryId(*a)), &database.entry(EntryId(*b))) 92 | }); 93 | 94 | ids 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use crate::database::*; 100 | use itertools::Itertools; 101 | use std::{fs, path::Path}; 102 | use strum::IntoEnumIterator; 103 | use tempfile::TempDir; 104 | 105 | fn tmpdir() -> TempDir { 106 | tempfile::tempdir().unwrap() 107 | } 108 | 109 | fn create_dir_structure

(dirs: &[P]) -> TempDir 110 | where 111 | P: AsRef, 112 | { 113 | let tmpdir = tmpdir(); 114 | let path = tmpdir.path(); 115 | 116 | for dir in dirs { 117 | fs::create_dir_all(path.join(dir)).unwrap(); 118 | } 119 | 120 | tmpdir 121 | } 122 | 123 | fn collect_paths<'a>(entries: impl Iterator>) -> Vec { 124 | let mut paths = Vec::new(); 125 | for entry in entries { 126 | assert_eq!(entry.path().file_name().unwrap(), entry.basename()); 127 | paths.push(entry.path()); 128 | paths.append(&mut collect_paths(entry.children())); 129 | } 130 | paths 131 | } 132 | 133 | #[test] 134 | fn build() { 135 | let tmpdir = 136 | create_dir_structure(&[Path::new("a/b"), Path::new("e/a/b"), Path::new("b/c/d")]); 137 | let tmpdir2 = 138 | create_dir_structure(&[Path::new("a/b"), Path::new("f/b"), Path::new("𠮷/😥")]); 139 | let path = tmpdir.path(); 140 | let path2 = tmpdir2.path(); 141 | 142 | let mut builder = DatabaseBuilder::new(); 143 | 144 | let database1 = builder.add_dir(path).add_dir(path2).build().unwrap(); 145 | let mut paths1 = collect_paths(database1.root_entries()); 146 | paths1.sort_unstable(); 147 | 148 | for kind in StatusKind::iter() { 149 | builder.index(kind); 150 | builder.fast_sort(kind); 151 | } 152 | 153 | let database2 = builder.add_dir(path).add_dir(path2).build().unwrap(); 154 | let mut paths2 = collect_paths(database2.root_entries()); 155 | paths2.sort_unstable(); 156 | 157 | assert_eq!(paths1, paths2); 158 | assert_eq!( 159 | paths1, 160 | vec![ 161 | path.to_path_buf(), 162 | path.join("a"), 163 | path.join("a/b"), 164 | path.join("b"), 165 | path.join("b/c"), 166 | path.join("b/c/d"), 167 | path.join("e"), 168 | path.join("e/a"), 169 | path.join("e/a/b"), 170 | path2.to_path_buf(), 171 | path2.join("a"), 172 | path2.join("a/b"), 173 | path2.join("f"), 174 | path2.join("f/b"), 175 | path2.join("𠮷"), 176 | path2.join("𠮷/😥") 177 | ] 178 | .iter() 179 | .map(|p| dunce::canonicalize(p).unwrap()) 180 | .collect::>() 181 | .iter() 182 | .sorted() 183 | .cloned() 184 | .collect::>() 185 | ); 186 | } 187 | 188 | #[test] 189 | fn empty_database() { 190 | let database = DatabaseBuilder::new().build().unwrap(); 191 | assert_eq!(database.num_entries(), 0); 192 | } 193 | 194 | #[test] 195 | #[should_panic] 196 | fn nonexistent_root_dir() { 197 | let tmpdir = tempfile::tempdir().unwrap(); 198 | let dir = tmpdir.path().join("xxxx"); 199 | DatabaseBuilder::new().add_dir(dir).build().unwrap(); 200 | } 201 | 202 | #[test] 203 | #[should_panic(expected = "Fast sorting cannot be enabled for a non-indexed status")] 204 | fn fast_sort_for_non_indexed_status() { 205 | let tmpdir = tmpdir(); 206 | DatabaseBuilder::new() 207 | .fast_sort(StatusKind::Size) 208 | .add_dir(tmpdir.path()) 209 | .build() 210 | .unwrap(); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/database/indexer.rs: -------------------------------------------------------------------------------- 1 | use super::{util, Database, EntryNode, StatusFlags, StatusKind}; 2 | use crate::{mode::Mode, Error, Result}; 3 | 4 | use camino::{Utf8Path, Utf8PathBuf}; 5 | use enum_map::{enum_map, EnumMap}; 6 | use fxhash::FxHashMap; 7 | use hashbrown::{hash_map::RawEntryMut, HashMap}; 8 | use parking_lot::Mutex; 9 | use rayon::prelude::*; 10 | use std::{ 11 | path::{Path, PathBuf}, 12 | time::SystemTime, 13 | }; 14 | 15 | pub struct IndexOptions { 16 | pub index_flags: StatusFlags, 17 | pub ignore_hidden: bool, 18 | } 19 | 20 | impl Default for IndexOptions { 21 | fn default() -> Self { 22 | Self { 23 | index_flags: enum_map! { 24 | StatusKind::Basename => true, 25 | StatusKind::Path => true, 26 | StatusKind::Extension => true, 27 | StatusKind::Size => false, 28 | StatusKind::Mode => false, 29 | StatusKind::Created => false, 30 | StatusKind::Modified => false, 31 | StatusKind::Accessed => false, 32 | }, 33 | ignore_hidden: false, 34 | } 35 | } 36 | } 37 | 38 | impl IndexOptions { 39 | #[inline] 40 | fn needs_metadata(&self, is_dir: bool) -> bool { 41 | let flags = &self.index_flags; 42 | (!is_dir && flags[StatusKind::Size]) // "size" of a directory is overwritten with a number of its children 43 | || flags[StatusKind::Mode] 44 | || flags[StatusKind::Created] 45 | || flags[StatusKind::Modified] 46 | || flags[StatusKind::Accessed] 47 | } 48 | } 49 | 50 | pub struct Indexer<'a> { 51 | options: &'a IndexOptions, 52 | ctx: WalkContext, 53 | } 54 | 55 | impl<'a> Indexer<'a> { 56 | pub fn new(options: &'a IndexOptions) -> Indexer<'a> { 57 | let database = Database { 58 | name_arena: String::new(), 59 | nodes: Vec::new(), 60 | root_paths: FxHashMap::default(), 61 | size: options.index_flags[StatusKind::Size].then(Vec::new), 62 | mode: options.index_flags[StatusKind::Mode].then(Vec::new), 63 | created: options.index_flags[StatusKind::Created].then(Vec::new), 64 | modified: options.index_flags[StatusKind::Modified].then(Vec::new), 65 | accessed: options.index_flags[StatusKind::Accessed].then(Vec::new), 66 | sorted_ids: EnumMap::default(), 67 | }; 68 | 69 | Self { 70 | options, 71 | ctx: WalkContext::new(database), 72 | } 73 | } 74 | 75 | pub fn index>(mut self, path: P) -> Result { 76 | let path = Utf8PathBuf::from_path_buf(path.into()).map_err(|_| Error::NonUtf8Path)?; 77 | 78 | let root_entry = LeafOrInternalEntry::from_path(&path, self.options)?; 79 | let root_node_id = self.ctx.database.nodes.len() as u32; 80 | self.ctx.database.root_paths.insert(root_node_id, path); 81 | 82 | match root_entry { 83 | LeafOrInternalEntry::Leaf(entry) => { 84 | self.ctx.push_leaf_entry(&entry, root_node_id); 85 | } 86 | LeafOrInternalEntry::Internal(entry) => { 87 | self.ctx.push_internal_entry(&entry, root_node_id); 88 | let ctx = Mutex::new(self.ctx); 89 | walk_file_system( 90 | &ctx, 91 | self.options, 92 | root_node_id, 93 | entry.child_dir_entries.into(), 94 | ); 95 | self.ctx = ctx.into_inner(); 96 | } 97 | } 98 | 99 | Ok(self) 100 | } 101 | 102 | pub fn finish(self) -> Database { 103 | self.ctx.into_inner() 104 | } 105 | } 106 | 107 | /// Span in name_arena 108 | struct NameSpan { 109 | start: usize, 110 | len: u16, 111 | } 112 | 113 | struct WalkContext { 114 | database: Database, 115 | 116 | // Set of spans which represent interned strings. 117 | // HashMap (instead of HashSet) is used here to make use of raw_entry_mut(). 118 | // Also, () is specified as HashBuilder since we don't use the default hasher. 119 | // Each hash value is caluculated from a string NameSpan represents. 120 | name_spans: HashMap, 121 | } 122 | 123 | impl WalkContext { 124 | fn new(database: Database) -> WalkContext { 125 | Self { 126 | database, 127 | name_spans: HashMap::with_hasher(()), 128 | } 129 | } 130 | 131 | fn into_inner(self) -> Database { 132 | self.database 133 | } 134 | 135 | fn push_leaf_entry(&mut self, entry: &LeafEntry, parent_id: u32) { 136 | self.push_entry(&entry.name, &entry.metadata, entry.is_dir, parent_id); 137 | } 138 | 139 | fn push_internal_entry(&mut self, entry: &InternalEntry, parent_id: u32) { 140 | self.push_entry(&entry.name, &entry.metadata, true, parent_id); 141 | } 142 | 143 | fn push_entry(&mut self, name: &str, metadata: &Metadata, is_dir: bool, parent_id: u32) { 144 | let hash = fxhash::hash64(name); 145 | let hash_entry = { 146 | let name_arena = &self.database.name_arena; 147 | self.name_spans.raw_entry_mut().from_hash(hash, |span| { 148 | &name_arena[span.start..][..span.len as usize] == name 149 | }) 150 | }; 151 | 152 | let name_len = name.len() as u16; 153 | let name_start = match hash_entry { 154 | RawEntryMut::Occupied(entry) => { 155 | let NameSpan { start, len } = *entry.key(); 156 | debug_assert_eq!(len, name_len); 157 | start 158 | } 159 | RawEntryMut::Vacant(entry) => { 160 | let name_arena = &mut self.database.name_arena; 161 | let start = name_arena.len(); 162 | name_arena.push_str(name); 163 | entry.insert_with_hasher( 164 | hash, 165 | NameSpan { 166 | start, 167 | len: name_len, 168 | }, 169 | (), 170 | |span| fxhash::hash64(&name_arena[span.start..][..span.len as usize]), 171 | ); 172 | start 173 | } 174 | }; 175 | debug_assert_eq!(&self.database.name_arena[name_start..][..name.len()], name); 176 | 177 | self.database.nodes.push(EntryNode { 178 | name_start, 179 | name_len, 180 | parent: parent_id, 181 | child_start: u32::MAX, 182 | child_end: u32::MAX, 183 | is_dir, 184 | }); 185 | 186 | if let Some(size) = &mut self.database.size { 187 | size.push(metadata.size); 188 | } 189 | if let Some(mode) = &mut self.database.mode { 190 | mode.push(metadata.mode); 191 | } 192 | if let Some(created) = &mut self.database.created { 193 | created.push(metadata.created); 194 | } 195 | if let Some(modified) = &mut self.database.modified { 196 | modified.push(metadata.modified); 197 | } 198 | if let Some(accessed) = &mut self.database.accessed { 199 | accessed.push(metadata.accessed); 200 | } 201 | } 202 | } 203 | 204 | fn walk_file_system( 205 | ctx: &Mutex, 206 | options: &IndexOptions, 207 | parent_id: u32, 208 | dir_entries: Vec, 209 | ) { 210 | let mut child_leaf_entries = Vec::new(); 211 | let mut child_internal_entries = Vec::new(); 212 | for dent in dir_entries { 213 | match LeafOrInternalEntry::from_dir_entry(dent, options) { 214 | LeafOrInternalEntry::Leaf(entry) => { 215 | child_leaf_entries.push(entry); 216 | } 217 | LeafOrInternalEntry::Internal(entry) => { 218 | child_internal_entries.push(entry); 219 | } 220 | } 221 | } 222 | 223 | let (internal_start, internal_end) = { 224 | let mut ctx = ctx.lock(); 225 | 226 | let child_start = ctx.database.nodes.len() as u32; 227 | let internal_end = child_start + child_internal_entries.len() as u32; 228 | let child_end = internal_end + child_leaf_entries.len() as u32; 229 | 230 | let mut parent_node = &mut ctx.database.nodes[parent_id as usize]; 231 | parent_node.child_start = child_start; 232 | parent_node.child_end = child_end; 233 | 234 | for entry in &child_internal_entries { 235 | ctx.push_internal_entry(entry, parent_id); 236 | } 237 | for entry in child_leaf_entries { 238 | ctx.push_leaf_entry(&entry, parent_id); 239 | } 240 | 241 | (child_start, internal_end) 242 | }; 243 | 244 | (internal_start..internal_end) 245 | .into_par_iter() 246 | .zip(child_internal_entries.into_par_iter()) 247 | .for_each(|(id, entry)| walk_file_system(ctx, options, id, entry.child_dir_entries.into())); 248 | } 249 | 250 | fn list_dir>(path: P, options: &IndexOptions) -> Result<(Vec, u64)> { 251 | let rd = path.as_ref().read_dir()?; 252 | 253 | let mut dir_entries = Vec::new(); 254 | let mut num_children = 0; 255 | 256 | for dent in rd { 257 | num_children += 1; 258 | 259 | if let Ok(dent) = dent { 260 | if options.ignore_hidden && util::is_hidden(&dent) { 261 | continue; 262 | } 263 | if let Ok(dir_entry) = DirEntry::from_std_dir_entry(dent, options) { 264 | dir_entries.push(dir_entry); 265 | } 266 | } 267 | } 268 | 269 | Ok((dir_entries, num_children)) 270 | } 271 | 272 | /// Our version of DirEntry. 273 | // std::fs::DirEntry keeps a file descriptor open, which leads to 274 | // "too many open files" error when we are holding lots of std::fs::DirEntry. 275 | // We avoid the problem by extracting information into our DirEntry and 276 | // discarding std::fs::DirEntry. 277 | struct DirEntry { 278 | name: Box, 279 | path: Box, 280 | is_dir: bool, 281 | metadata: Metadata, 282 | } 283 | 284 | impl DirEntry { 285 | fn from_std_dir_entry(dent: std::fs::DirEntry, options: &IndexOptions) -> Result { 286 | let is_dir = dent.file_type()?.is_dir(); 287 | Ok(Self { 288 | name: dent.file_name().to_str().ok_or(Error::NonUtf8Path)?.into(), 289 | path: dent.path().into(), 290 | is_dir, 291 | metadata: if options.needs_metadata(is_dir) { 292 | Metadata::from_std_metadata(&dent.metadata()?, options)? 293 | } else { 294 | Metadata::default() 295 | }, 296 | }) 297 | } 298 | } 299 | 300 | /// Our version of Metadata. 301 | /// 302 | /// Fields corresponding to non-indexed statuses are never referenced, so they 303 | /// are filled with dummy values. 304 | struct Metadata { 305 | size: u64, 306 | mode: Mode, 307 | created: SystemTime, 308 | modified: SystemTime, 309 | accessed: SystemTime, 310 | } 311 | 312 | impl Default for Metadata { 313 | fn default() -> Self { 314 | Self { 315 | size: 0, 316 | mode: Mode::default(), 317 | created: SystemTime::UNIX_EPOCH, 318 | modified: SystemTime::UNIX_EPOCH, 319 | accessed: SystemTime::UNIX_EPOCH, 320 | } 321 | } 322 | } 323 | 324 | impl Metadata { 325 | fn from_std_metadata(metadata: &std::fs::Metadata, options: &IndexOptions) -> Result { 326 | Ok(Self { 327 | size: if options.index_flags[StatusKind::Size] { 328 | metadata.len() 329 | } else { 330 | 0 331 | }, 332 | mode: if options.index_flags[StatusKind::Mode] { 333 | metadata.into() 334 | } else { 335 | Mode::default() 336 | }, 337 | created: if options.index_flags[StatusKind::Created] { 338 | util::sanitize_system_time(&metadata.created()?) 339 | } else { 340 | SystemTime::UNIX_EPOCH 341 | }, 342 | modified: if options.index_flags[StatusKind::Modified] { 343 | util::sanitize_system_time(&metadata.modified()?) 344 | } else { 345 | SystemTime::UNIX_EPOCH 346 | }, 347 | accessed: if options.index_flags[StatusKind::Accessed] { 348 | util::sanitize_system_time(&metadata.accessed()?) 349 | } else { 350 | SystemTime::UNIX_EPOCH 351 | }, 352 | }) 353 | } 354 | } 355 | 356 | /// An entry that has no children. 357 | /// 358 | /// This can be a file or a directory. 359 | struct LeafEntry { 360 | name: Box, 361 | is_dir: bool, 362 | metadata: Metadata, 363 | } 364 | 365 | /// An entry that has at least one child. 366 | /// 367 | /// All internal entries are, by definition, directories. 368 | struct InternalEntry { 369 | name: Box, 370 | metadata: Metadata, 371 | child_dir_entries: Box<[DirEntry]>, 372 | } 373 | 374 | enum LeafOrInternalEntry { 375 | Leaf(LeafEntry), 376 | Internal(InternalEntry), 377 | } 378 | 379 | impl LeafOrInternalEntry { 380 | fn from_dir_entry(dent: DirEntry, options: &IndexOptions) -> Self { 381 | if !dent.is_dir { 382 | return Self::Leaf(LeafEntry { 383 | name: dent.name, 384 | is_dir: false, 385 | metadata: dent.metadata, 386 | }); 387 | } 388 | 389 | let (dir_entries, num_children) = list_dir(&dent.path, options).unwrap_or_default(); 390 | let metadata = Metadata { 391 | size: num_children, 392 | ..dent.metadata 393 | }; 394 | if dir_entries.is_empty() { 395 | Self::Leaf(LeafEntry { 396 | name: dent.name, 397 | is_dir: true, 398 | metadata, 399 | }) 400 | } else { 401 | Self::Internal(InternalEntry { 402 | name: dent.name, 403 | metadata, 404 | child_dir_entries: dir_entries.into(), 405 | }) 406 | } 407 | } 408 | 409 | fn from_path>(path: P, options: &IndexOptions) -> Result { 410 | let path = path.as_ref(); 411 | let metadata = path.symlink_metadata()?; 412 | let is_dir = metadata.is_dir(); 413 | 414 | let dent = DirEntry { 415 | name: util::get_basename(path).into(), 416 | path: path.into(), 417 | is_dir, 418 | metadata: options 419 | .needs_metadata(is_dir) 420 | .then(|| Metadata::from_std_metadata(&metadata, options)) 421 | .transpose()? 422 | .unwrap_or_default(), 423 | }; 424 | 425 | Ok(Self::from_dir_entry(dent, options)) 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/database/search.rs: -------------------------------------------------------------------------------- 1 | mod filters; 2 | 3 | use super::{util, Database, EntryId}; 4 | use crate::{ 5 | query::{Query, SortOrder}, 6 | Error, Result, 7 | }; 8 | use filters::{Filter, FilterContext}; 9 | 10 | use rayon::prelude::*; 11 | use std::sync::{ 12 | atomic::{AtomicBool, Ordering}, 13 | Arc, 14 | }; 15 | 16 | impl Database { 17 | pub fn search(&self, query: &Query) -> Result> { 18 | let abort_signal = Arc::new(AtomicBool::new(false)); 19 | self.abortable_search(query, &abort_signal) 20 | } 21 | 22 | pub fn abortable_search( 23 | &self, 24 | query: &Query, 25 | abort_signal: &Arc, 26 | ) -> Result> { 27 | if query.is_empty() { 28 | return self.filter_and_sort::(query, abort_signal); 29 | } 30 | if !query.match_path() { 31 | return self.filter_and_sort::(query, abort_signal); 32 | } 33 | if !query.is_literal() { 34 | return self.filter_and_sort::(query, abort_signal); 35 | } 36 | if !query.has_path_separator() { 37 | return self.filter_and_sort::(query, abort_signal); 38 | } 39 | self.filter_and_sort::(query, abort_signal) 40 | } 41 | 42 | fn filter_and_sort( 43 | &self, 44 | query: &Query, 45 | abort_signal: &Arc, 46 | ) -> Result> { 47 | let ctx = FilterContext::new(self, abort_signal, query.regex()); 48 | 49 | let mut hits = if let Some(ids) = self.sorted_ids[query.sort_by()].as_ref() { 50 | match query.sort_order() { 51 | SortOrder::Ascending => F::ordered(&ctx, ids.into_par_iter().copied())?, 52 | SortOrder::Descending => F::ordered(&ctx, ids.into_par_iter().rev().copied())?, 53 | } 54 | } else { 55 | let mut hits = F::unordered(&ctx)?; 56 | 57 | if abort_signal.load(Ordering::Relaxed) { 58 | return Err(Error::SearchAbort); 59 | } 60 | 61 | let compare_func = util::get_compare_func(query.sort_by()); 62 | let slice = hits.as_parallel_slice_mut(); 63 | match query.sort_order() { 64 | SortOrder::Ascending => slice.par_sort_unstable_by(|a, b| { 65 | compare_func(&self.entry(EntryId(*a)), &self.entry(EntryId(*b))) 66 | }), 67 | SortOrder::Descending => slice.par_sort_unstable_by(|a, b| { 68 | compare_func(&self.entry(EntryId(*b)), &self.entry(EntryId(*a))) 69 | }), 70 | }; 71 | 72 | hits 73 | }; 74 | 75 | if query.sort_dirs_before_files() { 76 | if abort_signal.load(Ordering::Relaxed) { 77 | return Err(Error::SearchAbort); 78 | } 79 | 80 | let slice = hits.as_parallel_slice_mut(); 81 | match query.sort_order() { 82 | SortOrder::Ascending => slice.par_sort_by(|a, b| { 83 | Ord::cmp( 84 | &self.nodes[*b as usize].is_dir, 85 | &self.nodes[*a as usize].is_dir, 86 | ) 87 | }), 88 | SortOrder::Descending => slice.par_sort_by(|a, b| { 89 | Ord::cmp( 90 | &self.nodes[*a as usize].is_dir, 91 | &self.nodes[*b as usize].is_dir, 92 | ) 93 | }), 94 | } 95 | } 96 | 97 | Ok(hits.into_iter().map(EntryId).collect()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/database/search/filters.rs: -------------------------------------------------------------------------------- 1 | mod basename; 2 | mod component_wise_path; 3 | mod full_path; 4 | mod passthrough; 5 | mod regex_path; 6 | 7 | pub use basename::BasenameFilter; 8 | pub use component_wise_path::ComponentWisePathFilter; 9 | pub use full_path::FullPathFilter; 10 | pub use passthrough::PassthroughFilter; 11 | pub use regex_path::RegexPathFilter; 12 | 13 | use crate::{ 14 | database::{Database, EntryNode}, 15 | Error, Result, 16 | }; 17 | 18 | use rayon::prelude::*; 19 | use regex::Regex; 20 | use std::sync::{ 21 | atomic::{AtomicBool, Ordering}, 22 | Arc, 23 | }; 24 | use thread_local::ThreadLocal; 25 | 26 | pub(crate) struct FilterContext<'d, 'a, 'r> { 27 | database: &'d Database, 28 | abort_signal: &'a Arc, 29 | regex: &'r Regex, 30 | 31 | // Since rust-lang/regex@e040c1b, regex library stopped using thread_local, 32 | // which had a performance impact on indexa. 33 | // We mitigate it by putting Regex in thread local storage. 34 | regex_tls: ThreadLocal, 35 | } 36 | 37 | impl<'d, 'a, 'r> FilterContext<'d, 'a, 'r> { 38 | pub fn new( 39 | database: &'d Database, 40 | abort_signal: &'a Arc, 41 | regex: &'r Regex, 42 | ) -> Self { 43 | Self { 44 | database, 45 | abort_signal, 46 | regex, 47 | regex_tls: ThreadLocal::with_capacity(rayon::current_num_threads() + 1), 48 | } 49 | } 50 | 51 | fn thread_local_regex(&self) -> &Regex { 52 | self.regex_tls.get_or(|| self.regex.clone()) 53 | } 54 | } 55 | 56 | // Filters can choose to directly implement `Filter` or 57 | // implement `MatchEntries` instead. 58 | 59 | pub(crate) trait Filter { 60 | /// Returns filtered ids without changing an order. 61 | fn ordered(ctx: &FilterContext, ids: impl ParallelIterator) -> Result>; 62 | 63 | /// Returns filtered ids in an arbitrary order. 64 | fn unordered(ctx: &FilterContext) -> Result>; 65 | } 66 | 67 | pub(crate) trait MatchEntries: Filter { 68 | fn match_entries(ctx: &FilterContext, matched: &mut [AtomicBool]) -> Result<()>; 69 | } 70 | 71 | impl Filter for T { 72 | fn ordered(ctx: &FilterContext, ids: impl ParallelIterator) -> Result> { 73 | let nodes = &ctx.database.nodes; 74 | let mut matched: Vec<_> = (0..nodes.len()).map(|_| AtomicBool::new(false)).collect(); 75 | 76 | Self::match_entries(ctx, &mut matched)?; 77 | 78 | let matched: Vec<_> = matched.into_iter().map(AtomicBool::into_inner).collect(); 79 | let hits = ids.filter(|id| matched[*id as usize]).collect(); 80 | Ok(hits) 81 | } 82 | 83 | fn unordered(ctx: &FilterContext) -> Result> { 84 | let nodes = &ctx.database.nodes; 85 | let mut matched: Vec<_> = (0..nodes.len()).map(|_| AtomicBool::new(false)).collect(); 86 | 87 | Self::match_entries(ctx, &mut matched)?; 88 | 89 | let hits = (0..ctx.database.num_entries() as u32) 90 | .into_iter() 91 | .zip(matched.into_iter()) 92 | .filter_map(|(id, m)| m.into_inner().then(|| id)) 93 | .collect(); 94 | Ok(hits) 95 | } 96 | } 97 | 98 | fn match_all_descendants( 99 | ctx: &FilterContext, 100 | matched: &[AtomicBool], 101 | node: &EntryNode, 102 | ) -> Result<()> { 103 | let children_range = node.child_start as usize..node.child_end as usize; 104 | ( 105 | &ctx.database.nodes[children_range.clone()], 106 | &matched[children_range], 107 | ) 108 | .into_par_iter() 109 | .try_for_each(|(node, m)| { 110 | if ctx.abort_signal.load(Ordering::Relaxed) { 111 | return Err(Error::SearchAbort); 112 | } 113 | 114 | m.store(true, Ordering::Relaxed); 115 | if node.has_any_child() { 116 | match_all_descendants(ctx, matched, node)?; 117 | } 118 | 119 | Ok(()) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /src/database/search/filters/basename.rs: -------------------------------------------------------------------------------- 1 | use super::{Filter, FilterContext}; 2 | use crate::{Error, Result}; 3 | 4 | use rayon::prelude::*; 5 | use std::sync::atomic::Ordering; 6 | 7 | pub enum BasenameFilter {} 8 | 9 | impl Filter for BasenameFilter { 10 | fn ordered(ctx: &FilterContext, ids: impl ParallelIterator) -> Result> { 11 | ids.filter_map(|id| { 12 | if ctx.abort_signal.load(Ordering::Relaxed) { 13 | return Some(Err(Error::SearchAbort)); 14 | } 15 | 16 | let node = &ctx.database.nodes[id as usize]; 17 | ctx.thread_local_regex() 18 | .is_match(ctx.database.basename_from_node(node)) 19 | .then(|| Ok(id)) 20 | }) 21 | .collect() 22 | } 23 | 24 | fn unordered(ctx: &FilterContext) -> Result> { 25 | let nodes = &ctx.database.nodes; 26 | (0..nodes.len() as u32) 27 | .into_par_iter() 28 | .zip(nodes.par_iter()) 29 | .filter_map(|(id, node)| { 30 | if ctx.abort_signal.load(Ordering::Relaxed) { 31 | return Some(Err(Error::SearchAbort)); 32 | } 33 | 34 | ctx.thread_local_regex() 35 | .is_match(ctx.database.basename_from_node(node)) 36 | .then(|| Ok(id)) 37 | }) 38 | .collect() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/database/search/filters/component_wise_path.rs: -------------------------------------------------------------------------------- 1 | use super::{FilterContext, MatchEntries}; 2 | use crate::{database::EntryNode, Error, Result}; 3 | 4 | use rayon::prelude::*; 5 | use std::sync::atomic::{AtomicBool, Ordering}; 6 | 7 | pub enum ComponentWisePathFilter {} 8 | 9 | impl MatchEntries for ComponentWisePathFilter { 10 | fn match_entries(ctx: &FilterContext, matched: &mut [AtomicBool]) -> Result<()> { 11 | let nodes = &ctx.database.nodes; 12 | let root_paths = &ctx.database.root_paths; 13 | 14 | for ((root_id, root_path), next_root_id) in root_paths.iter().zip( 15 | root_paths 16 | .keys() 17 | .skip(1) 18 | .copied() 19 | .chain(std::iter::once(nodes.len() as u32)), 20 | ) { 21 | if ctx.regex.is_match(root_path.as_str()) { 22 | for m in &mut matched[*root_id as usize..next_root_id as usize] { 23 | *m.get_mut() = true; 24 | } 25 | } else { 26 | let root_node = &nodes[*root_id as usize]; 27 | traverse_tree(ctx, matched, root_node)?; 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | } 34 | 35 | fn traverse_tree(ctx: &FilterContext, matched: &[AtomicBool], node: &EntryNode) -> Result<()> { 36 | let regex = ctx.thread_local_regex(); 37 | 38 | let children_range = node.child_start as usize..node.child_end as usize; 39 | ( 40 | &ctx.database.nodes[children_range.clone()], 41 | &matched[children_range], 42 | ) 43 | .into_par_iter() 44 | .try_for_each(|(node, m)| { 45 | if ctx.abort_signal.load(Ordering::Relaxed) { 46 | return Err(Error::SearchAbort); 47 | } 48 | 49 | if regex.is_match(ctx.database.basename_from_node(node)) { 50 | m.store(true, Ordering::Relaxed); 51 | if node.has_any_child() { 52 | super::match_all_descendants(ctx, matched, node)?; 53 | } 54 | return Ok(()); 55 | } 56 | 57 | if node.has_any_child() { 58 | traverse_tree(ctx, matched, node)?; 59 | return Ok(()); 60 | } 61 | 62 | Ok(()) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /src/database/search/filters/full_path.rs: -------------------------------------------------------------------------------- 1 | use super::{FilterContext, MatchEntries}; 2 | use crate::{database::EntryNode, Error, Result}; 3 | 4 | use camino::Utf8Path; 5 | use rayon::prelude::*; 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | 8 | pub enum FullPathFilter {} 9 | 10 | impl MatchEntries for FullPathFilter { 11 | fn match_entries(ctx: &FilterContext, matched: &mut [AtomicBool]) -> Result<()> { 12 | let nodes = &ctx.database.nodes; 13 | let root_paths = &ctx.database.root_paths; 14 | 15 | for ((root_id, root_path), next_root_id) in root_paths.iter().zip( 16 | root_paths 17 | .keys() 18 | .skip(1) 19 | .copied() 20 | .chain(std::iter::once(nodes.len() as u32)), 21 | ) { 22 | if ctx.regex.is_match(root_path.as_str()) { 23 | for m in &mut matched[*root_id as usize..next_root_id as usize] { 24 | *m.get_mut() = true; 25 | } 26 | } else { 27 | let root_node = &nodes[*root_id as usize]; 28 | traverse_tree(ctx, matched, root_node, root_path)?; 29 | } 30 | } 31 | 32 | Ok(()) 33 | } 34 | } 35 | 36 | fn traverse_tree( 37 | ctx: &FilterContext, 38 | matched: &[AtomicBool], 39 | node: &EntryNode, 40 | path: &Utf8Path, 41 | ) -> Result<()> { 42 | let regex = ctx.thread_local_regex(); 43 | 44 | let children_range = node.child_start as usize..node.child_end as usize; 45 | ( 46 | &ctx.database.nodes[children_range.clone()], 47 | &matched[children_range], 48 | ) 49 | .into_par_iter() 50 | .try_for_each(|(node, m)| { 51 | if ctx.abort_signal.load(Ordering::Relaxed) { 52 | return Err(Error::SearchAbort); 53 | } 54 | 55 | let child_path = path.join(&ctx.database.basename_from_node(node)); 56 | 57 | if regex.is_match(child_path.as_str()) { 58 | m.store(true, Ordering::Relaxed); 59 | if node.has_any_child() { 60 | super::match_all_descendants(ctx, matched, node)?; 61 | } 62 | return Ok(()); 63 | } 64 | 65 | if node.has_any_child() { 66 | traverse_tree(ctx, matched, node, &child_path)?; 67 | return Ok(()); 68 | } 69 | 70 | Ok(()) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/database/search/filters/passthrough.rs: -------------------------------------------------------------------------------- 1 | use super::{Filter, FilterContext}; 2 | use crate::Result; 3 | 4 | use rayon::prelude::*; 5 | 6 | pub enum PassthroughFilter {} 7 | 8 | impl Filter for PassthroughFilter { 9 | fn ordered(_: &FilterContext, ids: impl ParallelIterator) -> Result> { 10 | Ok(ids.collect()) 11 | } 12 | 13 | fn unordered(ctx: &FilterContext) -> Result> { 14 | let hits = (0..ctx.database.num_entries() as u32).collect(); 15 | Ok(hits) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/database/search/filters/regex_path.rs: -------------------------------------------------------------------------------- 1 | use super::{FilterContext, MatchEntries}; 2 | use crate::{database::EntryNode, Error, Result}; 3 | 4 | use camino::Utf8Path; 5 | use rayon::prelude::*; 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | 8 | pub enum RegexPathFilter {} 9 | 10 | impl MatchEntries for RegexPathFilter { 11 | fn match_entries(ctx: &FilterContext, matched: &mut [AtomicBool]) -> Result<()> { 12 | let nodes = &ctx.database.nodes; 13 | 14 | for (root_id, root_path) in &ctx.database.root_paths { 15 | if ctx.regex.is_match(root_path.as_str()) { 16 | *matched[*root_id as usize].get_mut() = true; 17 | } 18 | 19 | let root_node = &nodes[*root_id as usize]; 20 | traverse_tree(ctx, matched, root_node, root_path)?; 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | fn traverse_tree( 28 | ctx: &FilterContext, 29 | matched: &[AtomicBool], 30 | node: &EntryNode, 31 | path: &Utf8Path, 32 | ) -> Result<()> { 33 | let regex = ctx.thread_local_regex(); 34 | 35 | let children_range = node.child_start as usize..node.child_end as usize; 36 | ( 37 | &ctx.database.nodes[children_range.clone()], 38 | &matched[children_range], 39 | ) 40 | .into_par_iter() 41 | .try_for_each(|(node, m)| { 42 | if ctx.abort_signal.load(Ordering::Relaxed) { 43 | return Err(Error::SearchAbort); 44 | } 45 | 46 | let child_path = path.join(&ctx.database.basename_from_node(node)); 47 | 48 | if regex.is_match(child_path.as_str()) { 49 | m.store(true, Ordering::Relaxed); 50 | } 51 | if node.has_any_child() { 52 | traverse_tree(ctx, matched, node, &child_path)?; 53 | } 54 | 55 | Ok(()) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/database/util.rs: -------------------------------------------------------------------------------- 1 | use super::{Entry, StatusKind}; 2 | use crate::{Error, Result}; 3 | 4 | use camino::Utf8Path; 5 | use std::{ 6 | cmp::Ordering, 7 | path::{Path, PathBuf}, 8 | time::SystemTime, 9 | }; 10 | 11 | /// Canonicalize all paths and remove all redundant subdirectories 12 | pub fn canonicalize_dirs

(dirs: &[P]) -> Result> 13 | where 14 | P: AsRef, 15 | { 16 | let mut dirs = dirs 17 | .iter() 18 | .map(|path| { 19 | let canonicalized = dunce::canonicalize(path)?; 20 | let path_str = canonicalized 21 | .to_str() 22 | .ok_or(Error::NonUtf8Path)? 23 | .to_string(); 24 | Ok((canonicalized, path_str)) 25 | }) 26 | .collect::>>()?; 27 | 28 | // we use str::starts_with, because Path::starts_with doesn't work well for Windows paths 29 | dirs.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)); 30 | dirs.dedup_by(|(_, a), (_, b)| a.starts_with(b.as_str())); 31 | 32 | Ok(dirs.into_iter().map(|(path, _)| path).collect()) 33 | } 34 | 35 | pub fn get_basename(path: &Utf8Path) -> &str { 36 | path.file_name().unwrap_or_else(|| path.as_str()) 37 | } 38 | 39 | pub fn get_compare_func(kind: StatusKind) -> fn(&Entry, &Entry) -> Ordering { 40 | #[inline] 41 | fn cmp_by_basename(a: &Entry, b: &Entry) -> Ordering { 42 | Ord::cmp(a.basename(), b.basename()).then_with(|| Entry::cmp_by_path(a, b)) 43 | } 44 | fn cmp_by_path(a: &Entry, b: &Entry) -> Ordering { 45 | Entry::cmp_by_path(a, b) 46 | } 47 | fn cmp_by_extension(a: &Entry, b: &Entry) -> Ordering { 48 | Entry::cmp_by_extension(a, b).then_with(|| cmp_by_basename(a, b)) 49 | } 50 | fn cmp_by_size(a: &Entry, b: &Entry) -> Ordering { 51 | Ord::cmp(&a.size().ok(), &b.size().ok()).then_with(|| cmp_by_basename(a, b)) 52 | } 53 | fn cmp_by_mode(a: &Entry, b: &Entry) -> Ordering { 54 | Ord::cmp(&a.mode().ok(), &b.mode().ok()).then_with(|| cmp_by_basename(a, b)) 55 | } 56 | fn cmp_by_created(a: &Entry, b: &Entry) -> Ordering { 57 | Ord::cmp(&a.created().ok(), &b.created().ok()).then_with(|| cmp_by_basename(a, b)) 58 | } 59 | fn cmp_by_modified(a: &Entry, b: &Entry) -> Ordering { 60 | Ord::cmp(&a.modified().ok(), &b.modified().ok()).then_with(|| cmp_by_basename(a, b)) 61 | } 62 | fn cmp_by_accessed(a: &Entry, b: &Entry) -> Ordering { 63 | Ord::cmp(&a.accessed().ok(), &b.accessed().ok()).then_with(|| cmp_by_basename(a, b)) 64 | } 65 | 66 | match kind { 67 | StatusKind::Basename => cmp_by_basename, 68 | StatusKind::Path => cmp_by_path, 69 | StatusKind::Extension => cmp_by_extension, 70 | StatusKind::Size => cmp_by_size, 71 | StatusKind::Mode => cmp_by_mode, 72 | StatusKind::Created => cmp_by_created, 73 | StatusKind::Modified => cmp_by_modified, 74 | StatusKind::Accessed => cmp_by_accessed, 75 | } 76 | } 77 | 78 | /// check for invalid SystemTime (e.g. older than unix epoch) and fix them 79 | pub fn sanitize_system_time(time: &SystemTime) -> SystemTime { 80 | if let Ok(duration) = time.duration_since(SystemTime::UNIX_EPOCH) { 81 | SystemTime::UNIX_EPOCH + duration 82 | } else { 83 | // defaults to unix epoch 84 | SystemTime::UNIX_EPOCH 85 | } 86 | } 87 | 88 | #[cfg(unix)] 89 | #[inline] 90 | pub fn is_hidden(dent: &std::fs::DirEntry) -> bool { 91 | use std::os::unix::ffi::OsStrExt; 92 | 93 | dent.path() 94 | .file_name() 95 | .map(|filename| filename.as_bytes().get(0) == Some(&b'.')) 96 | .unwrap_or(false) 97 | } 98 | 99 | #[cfg(windows)] 100 | #[inline] 101 | pub fn is_hidden(dent: &std::fs::DirEntry) -> bool { 102 | use crate::mode::Mode; 103 | 104 | if let Ok(metadata) = dent.metadata() { 105 | if Mode::from(&metadata).is_hidden() { 106 | return true; 107 | } 108 | } 109 | 110 | dent.path() 111 | .file_name() 112 | .and_then(|filename| filename.to_str()) 113 | .map(|s| s.starts_with('.')) 114 | .unwrap_or(false) 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | use std::path::{Path, PathBuf}; 121 | 122 | #[test] 123 | fn test_canonicalize_dirs() { 124 | let tmpdir = tempfile::tempdir().unwrap(); 125 | let path = tmpdir.path(); 126 | 127 | let dirs = vec![ 128 | path.join("a"), 129 | path.join("a/b/.."), 130 | path.join("e"), 131 | path.join("b/c"), 132 | path.join("a/b"), 133 | path.join("b/c/d"), 134 | path.join("e/a/b"), 135 | path.join("e/."), 136 | ]; 137 | for dir in &dirs { 138 | std::fs::create_dir_all(dir).unwrap(); 139 | } 140 | 141 | assert_eq!( 142 | canonicalize_dirs(&dirs).unwrap(), 143 | vec![path.join("a"), path.join("b/c"), path.join("e")] 144 | .iter() 145 | .map(|p| dunce::canonicalize(p).unwrap()) 146 | .collect::>() 147 | ); 148 | 149 | assert!(canonicalize_dirs::(&[]).unwrap().is_empty()); 150 | 151 | let tmpdir = tempfile::tempdir().unwrap(); 152 | let path = tmpdir.path(); 153 | std::env::set_current_dir(path).unwrap(); 154 | assert_eq!( 155 | canonicalize_dirs(&[Path::new(".")]).unwrap(), 156 | vec![dunce::canonicalize(path).unwrap()] 157 | ); 158 | } 159 | 160 | #[test] 161 | #[should_panic] 162 | fn canonicalize_non_existent_dir() { 163 | let tmpdir = tempfile::tempdir().unwrap(); 164 | let dir = tmpdir.path().join("xxxx"); 165 | canonicalize_dirs(&[dir]).unwrap(); 166 | } 167 | 168 | #[cfg(unix)] 169 | #[test] 170 | fn test_get_basename() { 171 | assert_eq!("/", get_basename(Utf8Path::new("/"))); 172 | assert_eq!("foo", get_basename(Utf8Path::new("/foo"))); 173 | assert_eq!("bar", get_basename(Utf8Path::new("/foo/bar"))); 174 | } 175 | 176 | #[cfg(windows)] 177 | #[test] 178 | fn test_get_basename() { 179 | assert_eq!(r"C:\", get_basename(Utf8Path::new(r"C:\"))); 180 | assert_eq!("foo", get_basename(Utf8Path::new(r"C:\foo"))); 181 | assert_eq!("bar", get_basename(Utf8Path::new(r"C:\foo\bar"))); 182 | assert_eq!( 183 | r"\\server\share\", 184 | get_basename(Utf8Path::new(r"\\server\share\")) 185 | ); 186 | assert_eq!("foo", get_basename(Utf8Path::new(r"\\server\share\foo"))); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error(transparent)] 7 | Io(#[from] io::Error), 8 | #[error(transparent)] 9 | Regex(#[from] regex::Error), 10 | #[error(transparent)] 11 | RegexSyntax(#[from] regex_syntax::Error), 12 | #[error("{0}")] 13 | InvalidOption(String), 14 | #[error("Encountered non-UTF-8 path")] 15 | NonUtf8Path, 16 | #[error("Search aborted")] 17 | SearchAbort, 18 | } 19 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use camino; 2 | pub use enum_map; 3 | pub use strum; 4 | 5 | pub mod database; 6 | mod error; 7 | pub mod mode; 8 | pub mod query; 9 | 10 | pub use error::Error; 11 | pub type Result = std::result::Result; 12 | -------------------------------------------------------------------------------- /src/mode.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | pub mod unix; 3 | 4 | #[cfg(windows)] 5 | pub mod windows; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] 10 | pub struct Mode(u32); 11 | 12 | impl From for Mode { 13 | fn from(value: u32) -> Self { 14 | Self(value) 15 | } 16 | } 17 | 18 | trait HasFlag: Copy { 19 | fn has_flag(&self, other: Self) -> bool; 20 | } 21 | 22 | impl HasFlag for u32 { 23 | fn has_flag(&self, flag: Self) -> bool { 24 | self & flag == flag 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | #[test] 33 | fn flag() { 34 | assert!(0b11.has_flag(1)); 35 | assert!(0b11.has_flag(0)); 36 | assert!(0b11.has_flag(0b10)); 37 | assert!(!0b10.has_flag(1)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mode/unix.rs: -------------------------------------------------------------------------------- 1 | use super::{HasFlag, Mode}; 2 | use std::{ 3 | fmt::{self, Write}, 4 | fs::Metadata, 5 | os::unix::fs::MetadataExt, 6 | }; 7 | 8 | const S_IFMT: u32 = 0xf000; 9 | const S_IFIFO: u32 = 0x1000; 10 | const S_IFCHR: u32 = 0x2000; 11 | const S_IFDIR: u32 = 0x4000; 12 | const S_IFBLK: u32 = 0x6000; 13 | const S_IFREG: u32 = 0x8000; 14 | const S_IFLNK: u32 = 0xa000; 15 | const S_IFSOCK: u32 = 0xc000; 16 | 17 | const S_ISUID: u32 = 0o4000; 18 | const S_ISGID: u32 = 0o2000; 19 | const S_ISVTX: u32 = 0o1000; 20 | 21 | const S_IRUSR: u32 = 0o0400; 22 | const S_IWUSR: u32 = 0o0200; 23 | const S_IXUSR: u32 = 0o0100; 24 | 25 | const S_IRGRP: u32 = 0o0040; 26 | const S_IWGRP: u32 = 0o0020; 27 | const S_IXGRP: u32 = 0o0010; 28 | 29 | const S_IROTH: u32 = 0o0004; 30 | const S_IWOTH: u32 = 0o0002; 31 | const S_IXOTH: u32 = 0o0001; 32 | 33 | impl From<&Metadata> for Mode { 34 | fn from(metadata: &Metadata) -> Self { 35 | Self(metadata.mode()) 36 | } 37 | } 38 | 39 | impl Mode { 40 | pub fn display_octal(&self) -> DisplayOctal { 41 | DisplayOctal(self.0) 42 | } 43 | 44 | pub fn display_symbolic(&self) -> DisplaySymbolic { 45 | DisplaySymbolic(self.0) 46 | } 47 | } 48 | 49 | pub struct DisplayOctal(u32); 50 | 51 | impl fmt::Display for DisplayOctal { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | write!(f, "{:04o}", self.0 & 0o7777) 54 | } 55 | } 56 | 57 | pub struct DisplaySymbolic(u32); 58 | 59 | impl fmt::Display for DisplaySymbolic { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | match self.0 & S_IFMT { 62 | S_IFIFO => f.write_char('p')?, 63 | S_IFCHR => f.write_char('c')?, 64 | S_IFDIR => f.write_char('d')?, 65 | S_IFBLK => f.write_char('b')?, 66 | S_IFREG => f.write_char('-')?, 67 | S_IFLNK => f.write_char('l')?, 68 | S_IFSOCK => f.write_char('s')?, 69 | _ => f.write_char('-')?, 70 | }; 71 | 72 | f.write_char(if self.0.has_flag(S_IRUSR) { 'r' } else { '-' })?; 73 | f.write_char(if self.0.has_flag(S_IWUSR) { 'w' } else { '-' })?; 74 | f.write_char(match (self.0.has_flag(S_IXUSR), self.0.has_flag(S_ISUID)) { 75 | (false, false) => '-', 76 | (true, false) => 'x', 77 | (false, true) => 'S', 78 | (true, true) => 's', 79 | })?; 80 | 81 | f.write_char(if self.0.has_flag(S_IRGRP) { 'r' } else { '-' })?; 82 | f.write_char(if self.0.has_flag(S_IWGRP) { 'w' } else { '-' })?; 83 | f.write_char(match (self.0.has_flag(S_IXGRP), self.0.has_flag(S_ISGID)) { 84 | (false, false) => '-', 85 | (true, false) => 'x', 86 | (false, true) => 'S', 87 | (true, true) => 's', 88 | })?; 89 | 90 | f.write_char(if self.0.has_flag(S_IROTH) { 'r' } else { '-' })?; 91 | f.write_char(if self.0.has_flag(S_IWOTH) { 'w' } else { '-' })?; 92 | f.write_char(match (self.0.has_flag(S_IXOTH), self.0.has_flag(S_ISVTX)) { 93 | (false, false) => '-', 94 | (true, false) => 'x', 95 | (false, true) => 'T', 96 | (true, true) => 't', 97 | })?; 98 | 99 | Ok(()) 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use super::*; 106 | 107 | fn check(mode: u32, octal: &str, symbolic: &str) { 108 | let mode = Mode::from(mode); 109 | assert_eq!(format!("{}", mode.display_octal()), octal); 110 | assert_eq!(format!("{}", mode.display_symbolic()), symbolic); 111 | } 112 | 113 | #[test] 114 | fn check_both() { 115 | check(0o0555, "0555", "-r-xr-xr-x"); 116 | check(0o0600, "0600", "-rw-------"); 117 | check(0o0644, "0644", "-rw-r--r--"); 118 | check(0o0664, "0664", "-rw-rw-r--"); 119 | check(0o0755, "0755", "-rwxr-xr-x"); 120 | check(0o1600, "1600", "-rw------T"); 121 | check(0o1777, "1777", "-rwxrwxrwt"); 122 | check(0o2745, "2745", "-rwxr-Sr-x"); 123 | check(0o2755, "2755", "-rwxr-sr-x"); 124 | check(0o4455, "4455", "-r-Sr-xr-x"); 125 | check(0o4555, "4555", "-r-sr-xr-x"); 126 | check(0o020444, "0444", "cr--r--r--"); 127 | check(0o040700, "0700", "drwx------"); 128 | check(0o060640, "0640", "brw-r-----"); 129 | check(0o100555, "0555", "-r-xr-xr-x"); 130 | check(0o100600, "0600", "-rw-------"); 131 | check(0o100664, "0664", "-rw-rw-r--"); 132 | check(0o120755, "0755", "lrwxr-xr-x"); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/mode/windows.rs: -------------------------------------------------------------------------------- 1 | use super::{HasFlag, Mode}; 2 | use std::{ 3 | fmt::{self, Write}, 4 | fs::Metadata, 5 | os::windows::fs::MetadataExt, 6 | }; 7 | 8 | const FILE_ATTRIBUTE_READONLY: u32 = 0x00000001; 9 | const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002; 10 | const FILE_ATTRIBUTE_SYSTEM: u32 = 0x00000004; 11 | const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x00000010; 12 | const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x00000020; 13 | const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x00000400; 14 | 15 | const ATTRIBUTE_CHARS: [char; 21] = [ 16 | 'R', 'H', 'S', 'V', 'D', 'A', 'X', 'N', 'T', 'P', 'L', 'C', 'O', 'I', 'E', 'V', '\0', 'X', 17 | '\0', 'P', 'U', 18 | ]; 19 | 20 | impl From<&Metadata> for Mode { 21 | fn from(metadata: &Metadata) -> Self { 22 | Self(metadata.file_attributes()) 23 | } 24 | } 25 | 26 | impl Mode { 27 | pub fn is_hidden(&self) -> bool { 28 | self.0.has_flag(FILE_ATTRIBUTE_HIDDEN) 29 | } 30 | 31 | pub fn display_traditional(&self) -> DisplayTraditional { 32 | DisplayTraditional(self.0) 33 | } 34 | 35 | pub fn display_powershell(&self) -> DisplayPowerShell { 36 | DisplayPowerShell(self.0) 37 | } 38 | } 39 | 40 | pub struct DisplayTraditional(u32); 41 | 42 | impl fmt::Display for DisplayTraditional { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | for (i, &c) in ATTRIBUTE_CHARS.iter().enumerate() { 45 | if c != '\0' && self.0.has_flag(1 << i) { 46 | f.write_char(c)?; 47 | } 48 | } 49 | Ok(()) 50 | } 51 | } 52 | 53 | pub struct DisplayPowerShell(u32); 54 | 55 | impl fmt::Display for DisplayPowerShell { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | f.write_char(if self.0.has_flag(FILE_ATTRIBUTE_REPARSE_POINT) { 58 | 'l' 59 | } else if self.0.has_flag(FILE_ATTRIBUTE_DIRECTORY) { 60 | 'd' 61 | } else { 62 | '-' 63 | })?; 64 | f.write_char(if self.0.has_flag(FILE_ATTRIBUTE_ARCHIVE) { 65 | 'a' 66 | } else { 67 | '-' 68 | })?; 69 | f.write_char(if self.0.has_flag(FILE_ATTRIBUTE_READONLY) { 70 | 'r' 71 | } else { 72 | '-' 73 | })?; 74 | f.write_char(if self.0.has_flag(FILE_ATTRIBUTE_HIDDEN) { 75 | 'h' 76 | } else { 77 | '-' 78 | })?; 79 | f.write_char(if self.0.has_flag(FILE_ATTRIBUTE_SYSTEM) { 80 | 's' 81 | } else { 82 | '-' 83 | })?; 84 | Ok(()) 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | fn check(mode: u32, powershell: &str, traditional: &str) { 93 | let mode = Mode::from(mode); 94 | assert_eq!(format!("{}", mode.display_powershell()), powershell); 95 | assert_eq!(format!("{}", mode.display_traditional()), traditional); 96 | } 97 | 98 | #[test] 99 | fn check_both() { 100 | check(0x0000, "-----", ""); 101 | check(0x0010, "d----", "D"); 102 | check(0x0020, "-a---", "A"); 103 | check(0x0021, "-ar--", "RA"); 104 | check(0x0023, "-arh-", "RHA"); 105 | check(0x0024, "-a--s", "SA"); 106 | check(0x0027, "-arhs", "RHSA"); 107 | check(0x0122, "-a-h-", "HAT"); 108 | check(0x0220, "-a---", "AP"); 109 | check(0x0410, "l----", "DL"); 110 | check(0x0420, "la---", "AL"); 111 | check(0x0820, "-a---", "AC"); 112 | check(0x1010, "d----", "DO"); 113 | check(0x1224, "-a--s", "SAPO"); 114 | check(0x1326, "-a-hs", "HSATPO"); 115 | check(0x2004, "----s", "SI"); 116 | check(0x2020, "-a---", "AI"); 117 | check(0x2024, "-a--s", "SAI"); 118 | check(0x2026, "-a-hs", "HSAI"); 119 | check(0x2920, "-a---", "ATCI"); 120 | check(0x200000 - 1, "larhs", "RHSVDAXNTPLCOIEVXPU"); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | mod regex_helper; 2 | 3 | use crate::{ 4 | database::{Entry, StatusKind}, 5 | Result, 6 | }; 7 | use regex::{Regex, RegexBuilder}; 8 | use serde::Deserialize; 9 | use std::{borrow::Cow, ops::Range}; 10 | 11 | #[derive(Clone)] 12 | pub struct Query { 13 | regex: Regex, 14 | match_path: bool, 15 | sort_by: StatusKind, 16 | sort_order: SortOrder, 17 | sort_dirs_before_files: bool, 18 | is_literal: bool, 19 | has_path_separator: bool, 20 | } 21 | 22 | impl Query { 23 | #[inline] 24 | pub fn regex(&self) -> &Regex { 25 | &self.regex 26 | } 27 | 28 | #[inline] 29 | pub fn match_path(&self) -> bool { 30 | self.match_path 31 | } 32 | 33 | #[inline] 34 | pub fn sort_by(&self) -> StatusKind { 35 | self.sort_by 36 | } 37 | 38 | #[inline] 39 | pub fn sort_order(&self) -> SortOrder { 40 | self.sort_order 41 | } 42 | 43 | #[inline] 44 | pub fn sort_dirs_before_files(&self) -> bool { 45 | self.sort_dirs_before_files 46 | } 47 | 48 | #[inline] 49 | pub fn is_empty(&self) -> bool { 50 | self.regex.as_str().is_empty() 51 | } 52 | 53 | #[inline] 54 | pub fn is_match(&self, entry: &Entry) -> bool { 55 | if self.match_path { 56 | self.regex.is_match(entry.path().as_str()) 57 | } else { 58 | self.regex.is_match(entry.basename()) 59 | } 60 | } 61 | 62 | pub fn basename_matches(&self, entry: &Entry) -> Vec> { 63 | if self.is_empty() { 64 | return Vec::new(); 65 | } 66 | 67 | let basename = entry.basename(); 68 | 69 | if self.match_path { 70 | let path = entry.path(); 71 | let path_str = path.as_str(); 72 | 73 | self.regex 74 | .find_iter(path_str) 75 | .filter(|m| path_str.len() - m.end() < basename.len()) 76 | .map(|m| Range { 77 | start: basename.len().saturating_sub(path_str.len() - m.start()), 78 | end: basename.len() - (path_str.len() - m.end()), 79 | }) 80 | .collect() 81 | } else { 82 | self.regex.find_iter(basename).map(|m| m.range()).collect() 83 | } 84 | } 85 | 86 | pub fn path_matches(&self, entry: &Entry) -> Vec> { 87 | if self.is_empty() { 88 | return Vec::new(); 89 | } 90 | 91 | let path = entry.path(); 92 | let path_str = path.as_str(); 93 | 94 | if self.match_path { 95 | self.regex.find_iter(path_str).map(|m| m.range()).collect() 96 | } else { 97 | let basename = entry.basename(); 98 | 99 | self.regex 100 | .find_iter(basename) 101 | .map(|m| Range { 102 | start: path_str.len() - basename.len() + m.start(), 103 | end: path_str.len() - basename.len() + m.end(), 104 | }) 105 | .collect() 106 | } 107 | } 108 | 109 | #[inline] 110 | pub(crate) fn is_literal(&self) -> bool { 111 | self.is_literal 112 | } 113 | 114 | #[inline] 115 | pub(crate) fn has_path_separator(&self) -> bool { 116 | self.has_path_separator 117 | } 118 | } 119 | 120 | #[derive(Copy, Clone, Debug, PartialEq, Deserialize)] 121 | #[serde(rename_all = "lowercase")] 122 | pub enum MatchPathMode { 123 | #[serde(alias = "yes")] 124 | Always, 125 | #[serde(alias = "no")] 126 | Never, 127 | Auto, 128 | } 129 | 130 | #[derive(Copy, Clone, Debug)] 131 | pub enum CaseSensitivity { 132 | Sensitive, 133 | Insensitive, 134 | Smart, 135 | } 136 | 137 | #[derive(Copy, Clone, Debug, PartialEq, Deserialize)] 138 | #[serde(rename_all = "lowercase")] 139 | pub enum SortOrder { 140 | #[serde(alias = "asc")] 141 | Ascending, 142 | #[serde(alias = "desc")] 143 | Descending, 144 | } 145 | 146 | pub struct QueryBuilder<'a> { 147 | pattern: Cow<'a, str>, 148 | match_path_mode: MatchPathMode, 149 | case_sensitivity: CaseSensitivity, 150 | is_regex_enabled: bool, 151 | sort_by: StatusKind, 152 | sort_order: SortOrder, 153 | sort_dirs_before_files: bool, 154 | } 155 | 156 | impl<'a> QueryBuilder<'a> { 157 | pub fn new

(pattern: P) -> Self 158 | where 159 | P: Into>, 160 | { 161 | Self { 162 | pattern: pattern.into(), 163 | match_path_mode: MatchPathMode::Never, 164 | case_sensitivity: CaseSensitivity::Smart, 165 | is_regex_enabled: false, 166 | sort_by: StatusKind::Basename, 167 | sort_order: SortOrder::Ascending, 168 | sort_dirs_before_files: false, 169 | } 170 | } 171 | 172 | pub fn match_path_mode(&mut self, match_path_mode: MatchPathMode) -> &mut Self { 173 | self.match_path_mode = match_path_mode; 174 | self 175 | } 176 | 177 | pub fn case_sensitivity(&mut self, case_sensitivity: CaseSensitivity) -> &mut Self { 178 | self.case_sensitivity = case_sensitivity; 179 | self 180 | } 181 | 182 | pub fn regex(&mut self, yes: bool) -> &mut Self { 183 | self.is_regex_enabled = yes; 184 | self 185 | } 186 | 187 | pub fn sort_by(&mut self, kind: StatusKind) -> &mut Self { 188 | self.sort_by = kind; 189 | self 190 | } 191 | 192 | pub fn sort_order(&mut self, order: SortOrder) -> &mut Self { 193 | self.sort_order = order; 194 | self 195 | } 196 | 197 | pub fn sort_dirs_before_files(&mut self, yes: bool) -> &mut Self { 198 | self.sort_dirs_before_files = yes; 199 | self 200 | } 201 | 202 | pub fn build(&self) -> Result { 203 | let escaped_pattern = if self.is_regex_enabled { 204 | self.pattern.clone() 205 | } else { 206 | regex::escape(&self.pattern).into() 207 | }; 208 | 209 | let mut parser = regex_syntax::ParserBuilder::new() 210 | .allow_invalid_utf8(true) 211 | .build(); 212 | let hir = parser.parse(&escaped_pattern)?; 213 | 214 | let has_uppercase_char = regex_helper::hir_has_uppercase_char(&hir); 215 | let case_sensitive = should_be_case_sensitive(self.case_sensitivity, has_uppercase_char); 216 | 217 | let regex = RegexBuilder::new(&escaped_pattern) 218 | .case_insensitive(!case_sensitive) 219 | .build()?; 220 | 221 | let has_path_separator = regex_helper::hir_has_path_separator(&hir); 222 | let match_path = should_match_path(self.match_path_mode, has_path_separator); 223 | 224 | Ok(Query { 225 | regex, 226 | match_path, 227 | sort_by: self.sort_by, 228 | sort_order: self.sort_order, 229 | sort_dirs_before_files: self.sort_dirs_before_files, 230 | is_literal: hir.is_literal(), 231 | has_path_separator, 232 | }) 233 | } 234 | } 235 | 236 | fn should_match_path(match_path_mode: MatchPathMode, has_path_separator: bool) -> bool { 237 | match match_path_mode { 238 | MatchPathMode::Always => true, 239 | MatchPathMode::Never => false, 240 | MatchPathMode::Auto => has_path_separator, 241 | } 242 | } 243 | 244 | fn should_be_case_sensitive(case_sensitivity: CaseSensitivity, has_uppercase_char: bool) -> bool { 245 | match case_sensitivity { 246 | CaseSensitivity::Sensitive => true, 247 | CaseSensitivity::Insensitive => false, 248 | CaseSensitivity::Smart => has_uppercase_char, 249 | } 250 | } 251 | 252 | #[cfg(test)] 253 | mod tests { 254 | use super::*; 255 | use crate::database::*; 256 | use regex_syntax::hir::Hir; 257 | use std::{fs, path::Path}; 258 | use tempfile::TempDir; 259 | 260 | fn parse_pattern(pattern: &str, is_regex_enabled: bool) -> Hir { 261 | let mut parser = regex_syntax::ParserBuilder::new() 262 | .allow_invalid_utf8(true) 263 | .build(); 264 | let escaped_pattern = if is_regex_enabled { 265 | pattern.to_owned() 266 | } else { 267 | regex::escape(pattern) 268 | }; 269 | parser.parse(&escaped_pattern).unwrap() 270 | } 271 | 272 | #[test] 273 | fn match_path() { 274 | use std::path::MAIN_SEPARATOR; 275 | 276 | fn match_path( 277 | match_path_mode: MatchPathMode, 278 | is_regex_enabled: bool, 279 | pattern: &str, 280 | ) -> bool { 281 | let hir = parse_pattern(pattern, is_regex_enabled); 282 | let has_path_separator = regex_helper::hir_has_path_separator(&hir); 283 | should_match_path(match_path_mode, has_path_separator) 284 | } 285 | 286 | assert!(match_path(MatchPathMode::Always, false, "foo")); 287 | assert!(match_path( 288 | MatchPathMode::Auto, 289 | false, 290 | &format!("foo{}bar", MAIN_SEPARATOR) 291 | )); 292 | assert!(!match_path(MatchPathMode::Auto, false, "foo")); 293 | 294 | assert!(match_path( 295 | MatchPathMode::Auto, 296 | false, 297 | &format!(r"foo{}w", MAIN_SEPARATOR) 298 | )); 299 | assert!(!match_path(MatchPathMode::Auto, true, r"foo\w")); 300 | 301 | if regex_syntax::is_meta_character(MAIN_SEPARATOR) { 302 | // typically Windows, where MAIN_SEPARATOR is \ 303 | 304 | assert!(match_path( 305 | MatchPathMode::Auto, 306 | true, 307 | ®ex::escape(&format!(r"foo{}", MAIN_SEPARATOR)) 308 | )); 309 | assert!(match_path( 310 | MatchPathMode::Auto, 311 | true, 312 | ®ex::escape(&format!(r"foo{}bar", MAIN_SEPARATOR)) 313 | )); 314 | assert!(match_path(MatchPathMode::Auto, true, r".")); 315 | assert!(!match_path( 316 | MatchPathMode::Auto, 317 | true, 318 | &format!(r"[^{}]", regex::escape(&MAIN_SEPARATOR.to_string())) 319 | )); 320 | } else { 321 | assert!(match_path( 322 | MatchPathMode::Auto, 323 | true, 324 | &format!("foo{}", MAIN_SEPARATOR) 325 | )); 326 | assert!(match_path( 327 | MatchPathMode::Auto, 328 | true, 329 | &format!("foo{}bar", MAIN_SEPARATOR) 330 | )); 331 | assert!(match_path(MatchPathMode::Auto, true, r".")); 332 | assert!(!match_path( 333 | MatchPathMode::Auto, 334 | true, 335 | &format!(r"[^{}]", MAIN_SEPARATOR) 336 | )); 337 | } 338 | } 339 | 340 | #[test] 341 | fn case_sensitive() { 342 | fn is_case_sensitive( 343 | case_sensitivity: CaseSensitivity, 344 | is_regex_enabled: bool, 345 | pattern: &str, 346 | ) -> bool { 347 | let hir = parse_pattern(pattern, is_regex_enabled); 348 | let has_uppercase_char = regex_helper::hir_has_uppercase_char(&hir); 349 | should_be_case_sensitive(case_sensitivity, has_uppercase_char) 350 | } 351 | 352 | assert!(is_case_sensitive(CaseSensitivity::Sensitive, false, "foo")); 353 | assert!(!is_case_sensitive( 354 | CaseSensitivity::Insensitive, 355 | false, 356 | "foo" 357 | )); 358 | assert!(is_case_sensitive(CaseSensitivity::Smart, false, "Foo")); 359 | assert!(!is_case_sensitive(CaseSensitivity::Smart, false, "foo")); 360 | assert!(is_case_sensitive(CaseSensitivity::Smart, true, "[A-Z]x")); 361 | assert!(!is_case_sensitive(CaseSensitivity::Smart, true, "[a-z]x")); 362 | } 363 | 364 | #[test] 365 | fn literal() { 366 | fn is_literal(is_regex_enabled: bool, pattern: &str) -> bool { 367 | parse_pattern(pattern, is_regex_enabled).is_literal() 368 | } 369 | 370 | assert!(is_literal(false, "a")); 371 | assert!(is_literal(false, "a.b")); 372 | assert!(is_literal(false, r#"a\.b"#)); 373 | assert!(is_literal(false, "a(b)")); 374 | assert!(is_literal(false, r#"a\"#)); 375 | assert!(is_literal(false, r#"a\\"#)); 376 | 377 | assert!(is_literal(true, "a")); 378 | assert!(!is_literal(true, "a.b")); 379 | assert!(is_literal(true, r#"a\.b"#)); 380 | assert!(!is_literal(true, "a(b)")); 381 | assert!(!is_literal(true, r#"a\w"#)); 382 | assert!(is_literal(true, r#"a\\"#)); 383 | } 384 | 385 | fn create_dir_structure

(dirs: &[P]) -> TempDir 386 | where 387 | P: AsRef, 388 | { 389 | let tmpdir = tempfile::tempdir().unwrap(); 390 | let path = tmpdir.path(); 391 | 392 | for dir in dirs { 393 | fs::create_dir_all(path.join(dir)).unwrap(); 394 | } 395 | 396 | tmpdir 397 | } 398 | 399 | #[test] 400 | fn match_ranges() { 401 | let tmpdir = create_dir_structure(&[ 402 | Path::new("aaa/foobarbaz/barbaz"), 403 | Path::new("0042bar/a/foo123bar"), 404 | ]); 405 | let path = dunce::canonicalize(tmpdir.path()).unwrap(); 406 | let prefix = path.to_str().unwrap(); 407 | let prefix_len = if prefix.ends_with(std::path::MAIN_SEPARATOR) { 408 | prefix.len() 409 | } else { 410 | prefix.len() + 1 411 | }; 412 | 413 | let database = DatabaseBuilder::new() 414 | .add_dir(tmpdir.path()) 415 | .build() 416 | .unwrap(); 417 | 418 | let search = |query| { 419 | database 420 | .search(query) 421 | .unwrap() 422 | .into_iter() 423 | .map(|id| database.entry(id)) 424 | .collect::>() 425 | }; 426 | 427 | let query = QueryBuilder::new("bar").build().unwrap(); 428 | let entries = search(&query); 429 | assert_eq!( 430 | entries 431 | .iter() 432 | .map(|entry| entry.basename()) 433 | .collect::>(), 434 | vec!["0042bar", "barbaz", "foo123bar", "foobarbaz"] 435 | ); 436 | let entry = &entries[1]; 437 | assert_eq!(query.basename_matches(entry), vec![0..3]); 438 | assert_eq!( 439 | query.path_matches(entry), 440 | vec![prefix_len + 14..prefix_len + 17] 441 | ); 442 | 443 | let query = QueryBuilder::new("bar") 444 | .match_path_mode(MatchPathMode::Always) 445 | .build() 446 | .unwrap(); 447 | let entry = search(&query) 448 | .into_iter() 449 | .find(|entry| entry.basename() == "barbaz") 450 | .unwrap(); 451 | assert_eq!(query.basename_matches(&entry), vec![0..3]); 452 | assert_eq!( 453 | query 454 | .path_matches(&entry) 455 | .into_iter() 456 | .filter(|range| { prefix_len <= range.start }) 457 | .collect::>(), 458 | vec![ 459 | prefix_len + 7..prefix_len + 10, 460 | prefix_len + 14..prefix_len + 17 461 | ] 462 | ); 463 | 464 | let query = QueryBuilder::new("[0-9]+") 465 | .match_path_mode(MatchPathMode::Always) 466 | .regex(true) 467 | .build() 468 | .unwrap(); 469 | let entry = search(&query) 470 | .into_iter() 471 | .find(|entry| entry.basename() == "foo123bar") 472 | .unwrap(); 473 | assert_eq!(query.basename_matches(&entry), vec![3..6]); 474 | assert_eq!( 475 | query 476 | .path_matches(&entry) 477 | .into_iter() 478 | .filter(|range| { prefix_len <= range.start }) 479 | .collect::>(), 480 | vec![prefix_len..prefix_len + 4, prefix_len + 13..prefix_len + 16] 481 | ); 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/query/regex_helper.rs: -------------------------------------------------------------------------------- 1 | // idea from https://github.com/sharkdp/fd/blob/6f2c8cdf914aca3ec19809d5b661f124d2935900/src/regex_helper.rs 2 | 3 | use regex_syntax::hir::{Class, Group, Hir, HirKind, Literal, Repetition}; 4 | 5 | pub fn hir_has_path_separator(hir: &Hir) -> bool { 6 | use std::path::MAIN_SEPARATOR; 7 | 8 | match hir.kind() { 9 | HirKind::Literal(Literal::Unicode(c)) => *c == MAIN_SEPARATOR, 10 | HirKind::Literal(Literal::Byte(b)) => char::from(*b) == MAIN_SEPARATOR, 11 | HirKind::Class(Class::Unicode(ranges)) => ranges 12 | .iter() 13 | .any(|r| r.start() <= MAIN_SEPARATOR && MAIN_SEPARATOR <= r.end()), 14 | HirKind::Class(Class::Bytes(ranges)) => ranges.iter().any(|r| { 15 | char::from(r.start()) <= MAIN_SEPARATOR && MAIN_SEPARATOR <= char::from(r.end()) 16 | }), 17 | HirKind::Group(Group { hir, .. }) | HirKind::Repetition(Repetition { hir, .. }) => { 18 | hir_has_path_separator(hir) 19 | } 20 | HirKind::Concat(hirs) | HirKind::Alternation(hirs) => { 21 | hirs.iter().any(hir_has_path_separator) 22 | } 23 | _ => false, 24 | } 25 | } 26 | 27 | pub fn hir_has_uppercase_char(hir: &Hir) -> bool { 28 | match hir.kind() { 29 | HirKind::Literal(Literal::Unicode(c)) => c.is_uppercase(), 30 | HirKind::Literal(Literal::Byte(b)) => char::from(*b).is_uppercase(), 31 | HirKind::Class(Class::Unicode(ranges)) => ranges 32 | .iter() 33 | .any(|r| r.start().is_uppercase() || r.end().is_uppercase()), 34 | HirKind::Class(Class::Bytes(ranges)) => ranges 35 | .iter() 36 | .any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()), 37 | HirKind::Group(Group { hir, .. }) | HirKind::Repetition(Repetition { hir, .. }) => { 38 | hir_has_uppercase_char(hir) 39 | } 40 | HirKind::Concat(hirs) | HirKind::Alternation(hirs) => { 41 | hirs.iter().any(hir_has_uppercase_char) 42 | } 43 | _ => false, 44 | } 45 | } 46 | --------------------------------------------------------------------------------