diff --git a/.cargo/audit.toml b/.cargo/audit.toml deleted file mode 100644 index 69f3b0f..0000000 --- a/.cargo/audit.toml +++ /dev/null @@ -1,2 +0,0 @@ -[advisories] -ignore = ["RUSTSEC-2020-0095"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c0972b..cb15ac9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,19 @@ name: ci on: push: - branches: - - main - - next + branches: [main] pull_request: workflow_dispatch: - env: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} CARGO_INCREMENTAL: 0 CARGO_TERM_COLOR: always - jobs: ci: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: - # FIXME: Enable macos-latest when this is merged: https://nixpk.gs/pr-tracker.html?pr=163924 os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000..b73a46c --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,22 @@ +name: no-response +on: + issue_comment: + types: [created] + schedule: + - cron: "0 0 * * *" # daily at 00:00 +permissions: + issues: write +jobs: + no-response: + if: github.repository == 'ajeetdsouza/zoxide' + runs-on: ubuntu-latest + steps: + - uses: lee-dohm/no-response@v0.5.0 + with: + token: ${{ github.token }} + daysUntilClose: 30 + responseRequiredLabel: waiting-for-response + closeComment: > + This issue has been automatically closed due to inactivity. If you + feel this is still relevant, please comment here or create a fresh + issue. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3180683..8d0accf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,11 @@ name: release on: push: - branches: - - main - - next + branches: [main] pull_request: workflow_dispatch: - env: CARGO_INCREMENTAL: 0 - jobs: release: name: ${{ matrix.target }} @@ -48,7 +44,7 @@ jobs: - name: Get version id: get_version - uses: SebRollen/toml-action@v1.0.0 + uses: SebRollen/toml-action@v1.0.2 with: file: Cargo.toml field: package.version diff --git a/CHANGELOG.md b/CHANGELOG.md index a8645a6..b8a2052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Fish/Zsh: Aliases on `__zoxide_z` will now use completions. + +## [0.9.0] - 2023-01-08 + +### Added + +- `edit` subcommand to adjust the scores of entries. + ### Fixed - Zsh: completions clashing with `zsh-autocomplete`. - Fzf: 'invalid option' on macOS. - PowerShell: handle UTF-8 encoding correctly. +- Zsh: don't hide output from `chpwd` hooks. +- Nushell: upgrade minimum supported version to v0.73.0. +- Zsh: fix extra space in interactive completions when no match is found. +- Fzf: various improvements. +- Nushell: Accidental redefinition of hooks when initialized twice. + +### Removed + +- `remove -i` subcommand: use `edit` instead. ## [0.8.3] - 2022-09-02 @@ -411,6 +430,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GitHub Actions pipeline to build and upload releases. - Support for zsh. +[0.9.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.3...v0.9.0 [0.8.3]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.2...v0.8.3 [0.8.2]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.0...v0.8.1 diff --git a/Cargo.lock b/Cargo.lock index 18ae546..eecacd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,19 +3,31 @@ version = 3 [[package]] -name = "aho-corasick" -version = "0.7.18" +name = "Inflector" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] -name = "anyhow" -version = "1.0.61" +name = "aliasable" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "508b352bb5c066aac251f6daf6b36eccd03e8a88e8081cd44959ea277a3af9a8" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "askama" @@ -62,9 +74,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.0.4" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" +checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3" dependencies = [ "bstr", "doc-comment", @@ -74,23 +86,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - [[package]] name = "bincode" version = "1.3.3" @@ -108,15 +103,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "0.2.17" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" dependencies = [ - "lazy_static", "memchr", + "once_cell", "regex-automata", + "serde", ] +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + [[package]] name = "cfg-if" version = "1.0.0" @@ -125,35 +127,33 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.16" +version = "4.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex", - "indexmap", + "is-terminal", "once_cell", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_complete" -version = "3.2.3" +version = "4.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ead064480dfc4880a10764488415a97fdd36a4cf1bb022d372f02e8faf8386e1" +checksum = "10861370d2ba66b0f5989f83ebf35db6421713fd92351790e7fdd6c36774c56b" dependencies = [ "clap", ] [[package]] name = "clap_complete_fig" -version = "3.2.4" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed37b4c0c1214673eba6ad8ea31666626bf72be98ffb323067d973c48b4964b9" +checksum = "46b30e010e669cd021e5004f3be26cff6b7c08d2a8a0d65b48d43a8cc0efd6c3" dependencies = [ "clap", "clap_complete", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.15" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", @@ -174,23 +174,13 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "difflib" version = "0.4.0" @@ -225,15 +215,36 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dunce" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" +checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] [[package]] name = "fastrand" @@ -252,9 +263,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", @@ -263,15 +274,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick", "bstr", @@ -280,12 +291,6 @@ dependencies = [ "regex", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "heck" version = "0.4.0" @@ -294,20 +299,19 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3" dependencies = [ - "crossbeam-utils", "globset", "lazy_static", "log", @@ -319,16 +323,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] - [[package]] name = "instant" version = "0.1.12" @@ -339,10 +333,32 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.10.3" +name = "io-lifetimes" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -355,9 +371,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.131" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "log" @@ -398,20 +420,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" -version = "0.24.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" dependencies = [ "bitflags", "cfg-if", "libc", + "static_assertions", ] [[package]] name = "nom" -version = "7.1.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", @@ -419,21 +442,44 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "os_str_bytes" -version = "6.2.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "ouroboros" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +dependencies = [ + "aliasable", + "ouroboros_macro", +] + +[[package]] +name = "ouroboros_macro" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "predicates" -version = "2.1.1" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", "itertools", @@ -442,15 +488,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" [[package]] name = "predicates-tree" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" dependencies = [ "predicates-core", "termtree", @@ -482,18 +528,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -520,9 +566,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -537,9 +583,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -552,9 +598,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930" +checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf" dependencies = [ "rstest_macros", "rustc_version", @@ -562,15 +608,16 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66" +checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7" dependencies = [ "cfg-if", "proc-macro2", "quote", "rustc_version", "syn", + "unicode-ident", ] [[package]] @@ -593,6 +640,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.36.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "same-file" version = "1.0.6" @@ -604,24 +665,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.143" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.143" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -634,6 +695,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -642,9 +709,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -676,30 +743,24 @@ dependencies = [ [[package]] name = "termtree" -version = "0.2.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -726,9 +787,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "version_check" @@ -764,13 +825,13 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "which" -version = "4.2.5" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] @@ -804,6 +865,63 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "xtask" version = "0.1.0" @@ -816,7 +934,7 @@ dependencies = [ [[package]] name = "zoxide" -version = "0.8.3" +version = "0.9.0" dependencies = [ "anyhow", "askama", @@ -830,6 +948,7 @@ dependencies = [ "fastrand", "glob", "nix", + "ouroboros", "rstest", "rstest_reuse", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8c9c349..814f455 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ license = "MIT" name = "zoxide" readme = "README.md" repository = "https://github.com/ajeetdsouza/zoxide" -rust-version = "1.62" -version = "0.8.3" +rust-version = "1.65" +version = "0.9.0" [badges] maintenance = { status = "actively-developed" } @@ -18,44 +18,64 @@ maintenance = { status = "actively-developed" } [workspace] members = ["xtask/"] -[dependencies] +[workspace.dependencies] anyhow = "1.0.32" askama = { version = "0.11.0", default-features = false } +assert_cmd = "2.0.0" bincode = "1.3.1" -clap = { version = "3.1.0", features = ["derive"] } +clap = { version = "4.0.0", features = ["derive"] } +clap_complete = "4.0.0" +clap_complete_fig = "4.0.0" dirs = "4.0.0" dunce = "1.0.1" fastrand = "1.7.0" glob = "0.3.0" -serde = { version = "1.0.116", features = ["derive"] } - -[target.'cfg(unix)'.dependencies] -nix = { version = "0.24.1", default-features = false, features = [ +ignore = "0.4.18" +nix = { version = "0.26.1", default-features = false, features = [ "fs", "user", ] } - -[target.'cfg(windows)'.dependencies] +ouroboros = "0.15.5" +rstest = { version = "0.16.0", default-features = false } +rstest_reuse = "0.4.0" +serde = { version = "1.0.116", features = ["derive"] } +shell-words = "1.0.0" +tempfile = "3.1.0" which = "4.2.5" +[dependencies] +anyhow.workspace = true +askama.workspace = true +bincode.workspace = true +clap.workspace = true +dirs.workspace = true +dunce.workspace = true +fastrand.workspace = true +glob.workspace = true +ouroboros.workspace = true +serde.workspace = true + +[target.'cfg(unix)'.dependencies] +nix.workspace = true + +[target.'cfg(windows)'.dependencies] +which.workspace = true + [build-dependencies] -clap = { version = "3.1.0", features = ["derive"] } -clap_complete = "3.1.0" -clap_complete_fig = "3.1.0" +clap.workspace = true +clap_complete.workspace = true +clap_complete_fig.workspace = true [dev-dependencies] -assert_cmd = "2.0.0" -rstest = { version = "0.15.0", default-features = false } -rstest_reuse = "0.4.0" -tempfile = "3.1.0" +assert_cmd.workspace = true +rstest.workspace = true +rstest_reuse.workspace = true +tempfile.workspace = true [features] default = [] nix-dev = [] -[profile.dev] -debug = 0 - [profile.release] codegen-units = 1 debug = 0 diff --git a/README.md b/README.md index 3a49a5c..2d65d42 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ [![crates.io][crates.io-badge]][crates.io] [![Downloads][downloads-badge]][releases] -[![License][license-badge]][license] [![Built with Nix][builtwithnix-badge]][builtwithnix] zoxide is a **smarter cd command**, inspired by z and autojump. @@ -81,10 +80,11 @@ Or, you can use a package manager: | Fedora 32+ | [Fedora Packages] | `dnf install zoxide` | | Gentoo | [GURU Overlay] | `eselect repository enable guru`
`emerge --sync guru`
`emerge app-shells/zoxide` | | Manjaro | | `pacman -S zoxide` | -| NixOS | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` | +| NixOS 21.05+ | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` | | openSUSE Tumbleweed | [openSUSE Factory] | `zypper install zoxide` | | Parrot OS | | `apt install zoxide` | | Raspbian 11+ | [Raspbian Packages] | `apt install zoxide` | +| Slackware 15.0+ | [SlackBuilds] | [Instructions][slackbuilds-howto] | | Ubuntu 21.04+ | [Ubuntu Packages] | `apt install zoxide` | | Void Linux | [Void Linux Packages] | `xbps-install -S zoxide` | @@ -204,7 +204,7 @@ zoxide init fish | source Add this to your env file (find it by running `$nu.env-path` in Nushell): ```sh -zoxide init nushell | save ~/.zoxide.nu +zoxide init nushell | save -f ~/.zoxide.nu ``` Now, add this to the end of your config file (find it by running @@ -215,7 +215,7 @@ source ~/.zoxide.nu ``` > **Note** -> zoxide only supports Nushell v0.63.0 and above. +> zoxide only supports Nushell v0.73.0 and above. @@ -398,7 +398,7 @@ They must be set before `zoxide init` is called. | [nnn] | File manager | [nnn-autojump] | | [ranger] | File manager | [ranger-zoxide] | | [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] | -| [vim] | Text editor | [zoxide.vim] | +| [vim] / [neovim] | Text editor | [zoxide.vim] | | [xplr] | File manager | [zoxide.xplr] | | [xxh] | Transports shell configuration over SSH | [xxh-plugin-prerun-zoxide] | | [zabb] | Finds the shortest possible query for a path | Natively supported | @@ -408,18 +408,18 @@ They must be set before `zoxide init` is called. [algorithm-matching]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#matching [alpine linux packages]: https://pkgs.alpinelinux.org/packages?name=zoxide [arch linux community]: https://archlinux.org/packages/community/x86_64/zoxide/ -[builtwithnix-badge]: https://img.shields.io/badge/builtwith-nix-7d81f7?style=flat-square +[builtwithnix-badge]: https://img.shields.io/badge/builtwith-nix-7d81f7?logo=nixos&logoColor=white&style=flat-square [builtwithnix]: https://builtwithnix.org/ [chocolatey]: https://community.chocolatey.org/packages/zoxide [clink-zoxide]: https://github.com/shunsambongi/clink-zoxide [clink]: https://github.com/mridgers/clink [conda-forge]: https://anaconda.org/conda-forge/zoxide [copr]: https://copr.fedorainfracloud.org/coprs/atim/zoxide/ -[crates.io-badge]: https://img.shields.io/crates/v/zoxide?style=flat-square +[crates.io-badge]: https://img.shields.io/crates/v/zoxide?logo=rust&logoColor=white&style=flat-square [crates.io]: https://crates.io/crates/zoxide [debian packages]: https://packages.debian.org/stable/admin/zoxide [devuan packages]: https://pkginfo.devuan.org/cgi-bin/package-query.html?c=package&q=zoxide -[downloads-badge]: https://img.shields.io/endpoint?color=bright-green&label=downloads&style=flat-square&cacheSeconds=3600&url=https%3A%2F%2Fzoxide-dl-rlvir7rbe5ac.runkit.sh%2F +[downloads-badge]: https://img.shields.io/github/downloads/ajeetdsouza/zoxide/total?logo=github&logoColor=white&style=flat-square [dports]: https://github.com/DragonFlyBSD/DPorts/tree/master/sysutils/zoxide [emacs]: https://www.gnu.org/software/emacs/ [fedora packages]: https://src.fedoraproject.org/rpms/rust-zoxide @@ -435,8 +435,6 @@ They must be set before `zoxide init` is called. [joshuto]: https://github.com/kamiyaa/joshuto [lf]: https://github.com/gokcehan/lf [lf-wiki]: https://github.com/gokcehan/lf/wiki/Integrations#zoxide -[license-badge]: https://img.shields.io/github/license/ajeetdsouza/zoxide?color=lightgray&style=flat-square -[license]: https://github.com/ajeetdsouza/zoxide/blob/main/LICENSE [linuxbrew]: https://formulae.brew.sh/formula-linux/zoxide [macports]: https://ports.macports.org/port/zoxide/summary [neovim]: https://github.com/neovim/neovim @@ -450,6 +448,7 @@ They must be set before `zoxide init` is called. [raspbian packages]: https://archive.raspbian.org/raspbian/pool/main/r/rust-zoxide/ [releases]: https://github.com/ajeetdsouza/zoxide/releases [scoop]: https://github.com/ScoopInstaller/Main/tree/master/bucket/zoxide.json +[slackbuilds-howto]: https://slackbuilds.org/howto/ [telescope-zoxide]: https://github.com/jvgrootveld/telescope-zoxide [telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim [termux]: https://github.com/termux/termux-packages/tree/master/packages/zoxide diff --git a/build.rs b/build.rs index aa561b2..f024a34 100644 --- a/build.rs +++ b/build.rs @@ -9,8 +9,8 @@ fn main() { }; println!("cargo:rustc-env=ZOXIDE_VERSION={version}"); - // Since we are generating completions in the package directory, we need to set this so that - // Cargo doesn't rebuild every time. + // Since we are generating completions in the package directory, we need to set + // this so that Cargo doesn't rebuild every time. println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=src/"); println!("cargo:rerun-if-changed=templates/"); @@ -22,7 +22,7 @@ fn main() { fn git_version() -> Option { let dir = env!("CARGO_MANIFEST_DIR"); let mut git = Command::new("git"); - git.args(&["-C", dir, "describe", "--tags", "--match=v*.*.*", "--always", "--broken"]); + git.args(["-C", dir, "describe", "--tags", "--match=v*.*.*", "--always", "--broken"]); let output = git.output().ok()?; if !output.status.success() || output.stdout.is_empty() || !output.stderr.is_empty() { diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 312adb6..9a0df2a 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -37,6 +37,61 @@ _arguments "${_arguments_options[@]}" \ '*::paths:_files -/' \ && ret=0 ;; +(edit) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'-V[Print version information]' \ +'--version[Print version information]' \ +":: :_zoxide__edit_commands" \ +"*::: :->edit" \ +&& ret=0 + + case $state in + (edit) + words=($line[1] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:zoxide-edit-command-$line[1]:" + case $line[1] in + (decrement) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'-V[Print version information]' \ +'--version[Print version information]' \ +':path:' \ +&& ret=0 +;; +(delete) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'-V[Print version information]' \ +'--version[Print version information]' \ +':path:' \ +&& ret=0 +;; +(increment) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'-V[Print version information]' \ +'--version[Print version information]' \ +':path:' \ +&& ret=0 +;; +(reload) +_arguments "${_arguments_options[@]}" \ +'-h[Print help information]' \ +'--help[Print help information]' \ +'-V[Print version information]' \ +'--version[Print version information]' \ +&& ret=0 +;; + esac + ;; +esac +;; (import) _arguments "${_arguments_options[@]}" \ '--from=[Application to import from]:FROM:(autojump z)' \ @@ -62,7 +117,7 @@ _arguments "${_arguments_options[@]}" \ ;; (query) _arguments "${_arguments_options[@]}" \ -'--exclude=[Exclude a path from results]:path:_files -/' \ +'--exclude=[Exclude the current directory]:path:_files -/' \ '--all[Show deleted directories]' \ '(-l --list)-i[Use interactive selection]' \ '(-l --list)--interactive[Use interactive selection]' \ @@ -79,8 +134,6 @@ _arguments "${_arguments_options[@]}" \ ;; (remove) _arguments "${_arguments_options[@]}" \ -'-i[Use interactive selection]' \ -'--interactive[Use interactive selection]' \ '-h[Print help information]' \ '--help[Print help information]' \ '-V[Print version information]' \ @@ -97,6 +150,7 @@ esac _zoxide_commands() { local commands; commands=( 'add:Add a new directory or increment its rank' \ +'edit:Edit the database' \ 'import:Import entries from another application' \ 'init:Generate shell configuration' \ 'query:Search for a directory in the database' \ @@ -109,11 +163,36 @@ _zoxide__add_commands() { local commands; commands=() _describe -t commands 'zoxide add commands' commands "$@" } +(( $+functions[_zoxide__edit__decrement_commands] )) || +_zoxide__edit__decrement_commands() { + local commands; commands=() + _describe -t commands 'zoxide edit decrement commands' commands "$@" +} +(( $+functions[_zoxide__edit__delete_commands] )) || +_zoxide__edit__delete_commands() { + local commands; commands=() + _describe -t commands 'zoxide edit delete commands' commands "$@" +} +(( $+functions[_zoxide__edit_commands] )) || +_zoxide__edit_commands() { + local commands; commands=( +'decrement:' \ +'delete:' \ +'increment:' \ +'reload:' \ + ) + _describe -t commands 'zoxide edit commands' commands "$@" +} (( $+functions[_zoxide__import_commands] )) || _zoxide__import_commands() { local commands; commands=() _describe -t commands 'zoxide import commands' commands "$@" } +(( $+functions[_zoxide__edit__increment_commands] )) || +_zoxide__edit__increment_commands() { + local commands; commands=() + _describe -t commands 'zoxide edit increment commands' commands "$@" +} (( $+functions[_zoxide__init_commands] )) || _zoxide__init_commands() { local commands; commands=() @@ -124,6 +203,11 @@ _zoxide__query_commands() { local commands; commands=() _describe -t commands 'zoxide query commands' commands "$@" } +(( $+functions[_zoxide__edit__reload_commands] )) || +_zoxide__edit__reload_commands() { + local commands; commands=() + _describe -t commands 'zoxide edit reload commands' commands "$@" +} (( $+functions[_zoxide__remove_commands] )) || _zoxide__remove_commands() { local commands; commands=() diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index f1e0571..2978e19 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -26,6 +26,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') + [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database') [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') @@ -39,6 +40,45 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') break } + 'zoxide;edit' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('decrement', 'decrement', [CompletionResultType]::ParameterValue, 'decrement') + [CompletionResult]::new('delete', 'delete', [CompletionResultType]::ParameterValue, 'delete') + [CompletionResult]::new('increment', 'increment', [CompletionResultType]::ParameterValue, 'increment') + [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'reload') + break + } + 'zoxide;edit;decrement' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + break + } + 'zoxide;edit;delete' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + break + } + 'zoxide;edit;increment' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + break + } + 'zoxide;edit;reload' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + break + } 'zoxide;import' { [CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from') [CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database') @@ -59,7 +99,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { break } 'zoxide;query' { - [CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude a path from results') + [CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude the current directory') [CompletionResult]::new('--all', 'all', [CompletionResultType]::ParameterName, 'Show deleted directories') [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') @@ -74,8 +114,6 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { break } 'zoxide;remove' { - [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection') - [CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 03ad7a4..6583641 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -8,24 +8,39 @@ _zoxide() { for i in ${COMP_WORDS[@]} do - case "${i}" in - "$1") + case "${cmd},${i}" in + ",$1") cmd="zoxide" ;; - add) - cmd+="__add" + zoxide,add) + cmd="zoxide__add" ;; - import) - cmd+="__import" + zoxide,edit) + cmd="zoxide__edit" ;; - init) - cmd+="__init" + zoxide,import) + cmd="zoxide__import" ;; - query) - cmd+="__query" + zoxide,init) + cmd="zoxide__init" ;; - remove) - cmd+="__remove" + zoxide,query) + cmd="zoxide__query" + ;; + zoxide,remove) + cmd="zoxide__remove" + ;; + zoxide__edit,decrement) + cmd="zoxide__edit__decrement" + ;; + zoxide__edit,delete) + cmd="zoxide__edit__delete" + ;; + zoxide__edit,increment) + cmd="zoxide__edit__increment" + ;; + zoxide__edit,reload) + cmd="zoxide__edit__reload" ;; *) ;; @@ -34,7 +49,7 @@ _zoxide() { case "${cmd}" in zoxide) - opts="-h -V --help --version add import init query remove" + opts="-h -V --help --version add edit import init query remove" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -61,6 +76,76 @@ _zoxide() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + zoxide__edit) + opts="-h -V --help --version decrement delete increment reload" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__edit__decrement) + opts="-h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__edit__delete) + opts="-h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__edit__increment) + opts="-h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + zoxide__edit__reload) + opts="-h -V --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; zoxide__import) opts="-h -V --from --merge --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -102,7 +187,7 @@ _zoxide() { return 0 ;; zoxide__query) - opts="-i -l -s -h -V --all --interactive --list --score --exclude --help --version ..." + opts="-i -l -s -h -V --all --interactive --list --score --exclude --help --version [KEYWORDS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -120,7 +205,7 @@ _zoxide() { return 0 ;; zoxide__remove) - opts="-i -h -V --interactive --help --version ..." + opts="-h -V --help --version [PATHS]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 2e98e78..abc01d7 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -23,6 +23,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version information' cand --version 'Print version information' cand add 'Add a new directory or increment its rank' + cand edit 'Edit the database' cand import 'Import entries from another application' cand init 'Generate shell configuration' cand query 'Search for a directory in the database' @@ -34,6 +35,40 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version information' cand --version 'Print version information' } + &'zoxide;edit'= { + cand -h 'Print help information' + cand --help 'Print help information' + cand -V 'Print version information' + cand --version 'Print version information' + cand decrement 'decrement' + cand delete 'delete' + cand increment 'increment' + cand reload 'reload' + } + &'zoxide;edit;decrement'= { + cand -h 'Print help information' + cand --help 'Print help information' + cand -V 'Print version information' + cand --version 'Print version information' + } + &'zoxide;edit;delete'= { + cand -h 'Print help information' + cand --help 'Print help information' + cand -V 'Print version information' + cand --version 'Print version information' + } + &'zoxide;edit;increment'= { + cand -h 'Print help information' + cand --help 'Print help information' + cand -V 'Print version information' + cand --version 'Print version information' + } + &'zoxide;edit;reload'= { + cand -h 'Print help information' + cand --help 'Print help information' + cand -V 'Print version information' + cand --version 'Print version information' + } &'zoxide;import'= { cand --from 'Application to import from' cand --merge 'Merge into existing database' @@ -52,7 +87,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --version 'Print version information' } &'zoxide;query'= { - cand --exclude 'Exclude a path from results' + cand --exclude 'Exclude the current directory' cand --all 'Show deleted directories' cand -i 'Use interactive selection' cand --interactive 'Use interactive selection' @@ -66,8 +101,6 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --version 'Print version information' } &'zoxide;remove'= { - cand -i 'Use interactive selection' - cand --interactive 'Use interactive selection' cand -h 'Print help information' cand --help 'Print help information' cand -V 'Print version information' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 16bf84a..1dbe1c3 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -1,12 +1,27 @@ complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank' +complete -c zoxide -n "__fish_use_subcommand" -f -a "edit" -d 'Edit the database' complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application' complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database' complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -s h -l help -d 'Print help information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -s V -l version -d 'Print version information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "decrement" +complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "delete" +complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "increment" +complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "reload" +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s h -l help -d 'Print help information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s V -l version -d 'Print version information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s h -l help -d 'Print help information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s V -l version -d 'Print version information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s h -l help -d 'Print help information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s V -l version -d 'Print version information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help information' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }" complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database' complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help information' @@ -16,13 +31,12 @@ complete -c zoxide -n "__fish_seen_subcommand_from init" -l hook -d 'Changes how complete -c zoxide -n "__fish_seen_subcommand_from init" -l no-cmd -d 'Prevents zoxide from defining the `z` and `zi` commands' complete -c zoxide -n "__fish_seen_subcommand_from init" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from init" -s V -l version -d 'Print version information' -complete -c zoxide -n "__fish_seen_subcommand_from query" -l exclude -d 'Exclude a path from results' -r -f -a "(__fish_complete_directories)" +complete -c zoxide -n "__fish_seen_subcommand_from query" -l exclude -d 'Exclude the current directory' -r -f -a "(__fish_complete_directories)" complete -c zoxide -n "__fish_seen_subcommand_from query" -l all -d 'Show deleted directories' complete -c zoxide -n "__fish_seen_subcommand_from query" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_seen_subcommand_from query" -s l -l list -d 'List all matching directories' complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results' complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version information' -complete -c zoxide -n "__fish_seen_subcommand_from remove" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version information' diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index fe986b7..5f1d16f 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -21,6 +21,87 @@ const completion: Fig.Spec = { template: "folders", }, }, + { + name: "edit", + description: "Edit the database", + subcommands: [ + { + name: "decrement", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + args: { + name: "path", + }, + }, + { + name: "delete", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + args: { + name: "path", + }, + }, + { + name: "increment", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + args: { + name: "path", + }, + }, + { + name: "reload", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, + ], + options: [ + { + name: ["-h", "--help"], + description: "Print help information", + }, + { + name: ["-V", "--version"], + description: "Print version information", + }, + ], + }, { name: "import", description: "Import entries from another application", @@ -28,6 +109,7 @@ const completion: Fig.Spec = { { name: "--from", description: "Application to import from", + isRepeatable: true, args: { name: "from", suggestions: [ @@ -61,6 +143,7 @@ const completion: Fig.Spec = { { name: "--cmd", description: "Changes the prefix of the `z` and `zi` commands", + isRepeatable: true, args: { name: "cmd", isOptional: true, @@ -69,6 +152,7 @@ const completion: Fig.Spec = { { name: "--hook", description: "Changes how often zoxide increments a directory's score", + isRepeatable: true, args: { name: "hook", isOptional: true, @@ -112,7 +196,8 @@ const completion: Fig.Spec = { options: [ { name: "--exclude", - description: "Exclude a path from results", + description: "Exclude the current directory", + isRepeatable: true, args: { name: "exclude", isOptional: true, @@ -154,6 +239,7 @@ const completion: Fig.Spec = { ], args: { name: "keywords", + isVariadic: true, isOptional: true, }, }, @@ -161,10 +247,6 @@ const completion: Fig.Spec = { name: "remove", description: "Remove a directory from the database", options: [ - { - name: ["-i", "--interactive"], - description: "Use interactive selection", - }, { name: ["-h", "--help"], description: "Print help information", @@ -176,6 +258,7 @@ const completion: Fig.Spec = { ], args: { name: "paths", + isVariadic: true, isOptional: true, template: "folders", }, diff --git a/install.sh b/install.sh index f971b57..9f0784c 100755 --- a/install.sh +++ b/install.sh @@ -91,10 +91,12 @@ download_zoxide() { wget) _releases="$(wget -qO- "$_releases_url")" || err "wget: failed to download $_releases_url" ;; esac + (echo "$_releases" | grep -q 'API rate limit exceeded') && + err "you have exceeded GitHub's API rate limit. Please try again later, or use a different installation method: https://github.com/ajeetdsouza/zoxide/#installation" local _package_url _package_url="$(echo "$_releases" | grep "browser_download_url" | cut -d '"' -f 4 | grep "$_arch")" || - err "zoxide has not yet been packaged for your architecture ($_arch), please file an issue at https://github.com/ajeetdsouza/zoxide/issues" + err "zoxide has not yet been packaged for your architecture ($_arch), please file an issue: https://github.com/ajeetdsouza/zoxide/issues" local _ext case "$_package_url" in diff --git a/man/man1/zoxide-init.1 b/man/man1/zoxide-init.1 index 996e72c..e3331fb 100644 --- a/man/man1/zoxide-init.1 +++ b/man/man1/zoxide-init.1 @@ -35,7 +35,7 @@ Note: zoxide only supports fish v3.4.0 and above. Add this to your env file (find it by running \fB$nu.env-path\fR in Nushell): .sp .nf - \fBzoxide init nushell --hook prompt | save ~/.zoxide.nu\fR + \fBzoxide init nushell | save -f ~/.zoxide.nu\fR .fi .sp Now, add this to the end of your config file (find it by running @@ -45,7 +45,7 @@ Now, add this to the end of your config file (find it by running \fBsource ~/.zoxide.nu\fR .fi .sp -Note: zoxide only supports Nushell v0.63.0 and above. +Note: zoxide only supports Nushell v0.73.0 and above. .TP .B powershell Add this to your configuration (find it by running \fBecho $profile\fR in diff --git a/man/man1/zoxide-remove.1 b/man/man1/zoxide-remove.1 index 9f0d5da..e174a6f 100644 --- a/man/man1/zoxide-remove.1 +++ b/man/man1/zoxide-remove.1 @@ -10,9 +10,6 @@ If you'd like to permanently exclude a directory from the database, see the .TP .B -h, --help Print help information. -.TP -.B -i, --interactive [KEYWORDS] -Use interactive selection. This option requires \fBfzf\fR(1). .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp diff --git a/rustfmt.toml b/rustfmt.toml index 5106da6..ebaf638 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ -comment_width = 100 group_imports = "StdExternalCrate" imports_granularity = "Module" max_width = 120 diff --git a/shell.nix b/shell.nix index 3c5e68c..689a4b1 100644 --- a/shell.nix +++ b/shell.nix @@ -1,8 +1,8 @@ let rust = import (builtins.fetchTarball - "https://github.com/oxalica/rust-overlay/archive/60c2cfaa8b90ed8cebd18b214fac8682dcf222dd.tar.gz"); + "https://github.com/oxalica/rust-overlay/archive/9096306d4a1c3adcc8d20f2c9dcaee3dee30d1ad.tar.gz"); pkgs = import (builtins.fetchTarball - "https://github.com/NixOS/nixpkgs/archive/0323e1f8bac882f19905174639a89397db1930f1.tar.gz") { + "https://github.com/NixOS/nixpkgs/archive/5f902ae769594aaeaf326e8623a48482eeacfe89.tar.gz") { overlays = [ rust ]; }; in pkgs.mkShell { @@ -21,7 +21,6 @@ in pkgs.mkShell { pkgs.zsh # Tools - pkgs.cargo-audit pkgs.cargo-nextest pkgs.mandoc pkgs.nixfmt diff --git a/src/cmd/add.rs b/src/cmd/add.rs index 34ba0c0..21136cc 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -3,42 +3,39 @@ use std::path::Path; use anyhow::{bail, Result}; use crate::cmd::{Add, Run}; -use crate::db::DatabaseFile; +use crate::db::Database; use crate::{config, util}; impl Run for Add { fn run(&self) -> Result<()> { - // These characters can't be printed cleanly to a single line, so they can cause confusion - // when writing to fzf / stdout. + // These characters can't be printed cleanly to a single line, so they can cause + // confusion when writing to stdout. const EXCLUDE_CHARS: &[char] = &['\n', '\r']; - let data_dir = config::data_dir()?; let exclude_dirs = config::exclude_dirs()?; let max_age = config::maxage()?; let now = util::current_time()?; - let mut db = DatabaseFile::new(data_dir); - let mut db = db.open()?; + let mut db = Database::open()?; for path in &self.paths { let path = if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }(path)?; let path = util::path_to_str(&path)?; - // Ignore path if it contains unsupported characters, or if it's in the exclude list. + // Ignore path if it contains unsupported characters, or if it's in the exclude + // list. if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) { continue; } if !Path::new(path).is_dir() { bail!("not a directory: {path}"); } - db.add(path, now); + db.add_update(path, 1.0, now); } - if db.modified { + if db.dirty() { db.age(max_age); - db.save()?; } - - Ok(()) + db.save() } } diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 59775c6..2ae88de 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -2,15 +2,15 @@ use std::path::PathBuf; -use clap::{ArgEnum, Parser, ValueHint}; +use clap::{Parser, Subcommand, ValueEnum, ValueHint}; -const ENV_HELP: &str = "ENVIRONMENT VARIABLES: - _ZO_DATA_DIR Path for zoxide data files - _ZO_ECHO Print the matched directory before navigating to it when set to 1 - _ZO_EXCLUDE_DIRS List of directory globs to be excluded - _ZO_FZF_OPTS Custom flags to pass to fzf - _ZO_MAXAGE Maximum total age after which entries start getting deleted - _ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths"; +const ENV_HELP: &str = "Environment variables: + _ZO_DATA_DIR Path for zoxide data files + _ZO_ECHO Print the matched directory before navigating to it when set to 1 + _ZO_EXCLUDE_DIRS List of directory globs to be excluded + _ZO_FZF_OPTS Custom flags to pass to fzf + _ZO_MAXAGE Maximum total age after which entries start getting deleted + _ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths"; #[derive(Debug, Parser)] #[clap( @@ -24,6 +24,7 @@ const ENV_HELP: &str = "ENVIRONMENT VARIABLES: )] pub enum Cmd { Add(Add), + Edit(Edit), Import(Import), Init(Init), Query(Query), @@ -33,10 +34,29 @@ pub enum Cmd { /// Add a new directory or increment its rank #[derive(Debug, Parser)] pub struct Add { - #[clap(min_values = 1, required = true, value_hint = ValueHint::DirPath)] + #[clap(num_args = 1.., required = true, value_hint = ValueHint::DirPath)] pub paths: Vec, } +/// Edit the database +#[derive(Debug, Parser)] +pub struct Edit { + #[clap(subcommand)] + pub cmd: Option, +} + +#[derive(Clone, Debug, Subcommand)] +pub enum EditCommand { + #[clap(hide = true)] + Decrement { path: String }, + #[clap(hide = true)] + Delete { path: String }, + #[clap(hide = true)] + Increment { path: String }, + #[clap(hide = true)] + Reload, +} + /// Import entries from another application #[derive(Debug, Parser)] pub struct Import { @@ -44,7 +64,7 @@ pub struct Import { pub path: PathBuf, /// Application to import from - #[clap(arg_enum, long)] + #[clap(value_enum, long)] pub from: ImportFrom, /// Merge into existing database @@ -52,7 +72,7 @@ pub struct Import { pub merge: bool, } -#[derive(ArgEnum, Clone, Debug)] +#[derive(ValueEnum, Clone, Debug)] pub enum ImportFrom { Autojump, Z, @@ -61,7 +81,7 @@ pub enum ImportFrom { /// Generate shell configuration #[derive(Debug, Parser)] pub struct Init { - #[clap(arg_enum)] + #[clap(value_enum)] pub shell: InitShell, /// Prevents zoxide from defining the `z` and `zi` commands @@ -73,18 +93,18 @@ pub struct Init { pub cmd: String, /// Changes how often zoxide increments a directory's score - #[clap(arg_enum, long, default_value = "pwd")] + #[clap(value_enum, long, default_value = "pwd")] pub hook: InitHook, } -#[derive(ArgEnum, Clone, Copy, Debug, Eq, PartialEq)] +#[derive(ValueEnum, Clone, Copy, Debug, Eq, PartialEq)] pub enum InitHook { None, Prompt, Pwd, } -#[derive(ArgEnum, Clone, Debug)] +#[derive(ValueEnum, Clone, Debug)] pub enum InitShell { Bash, Elvish, @@ -117,7 +137,7 @@ pub struct Query { #[clap(long, short)] pub score: bool, - /// Exclude a path from results + /// Exclude the current directory #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] pub exclude: Option, } @@ -125,9 +145,6 @@ pub struct Query { /// Remove a directory from the database #[derive(Debug, Parser)] pub struct Remove { - /// Use interactive selection - #[clap(long, short)] - pub interactive: bool, #[clap(value_hint = ValueHint::DirPath)] pub paths: Vec, } diff --git a/src/cmd/edit.rs b/src/cmd/edit.rs new file mode 100644 index 0000000..2bbaedc --- /dev/null +++ b/src/cmd/edit.rs @@ -0,0 +1,83 @@ +use std::io::{self, Write}; + +use anyhow::Result; + +use crate::cmd::{Edit, EditCommand, Run}; +use crate::db::Database; +use crate::error::BrokenPipeHandler; +use crate::util::{self, Fzf, FzfChild}; + +impl Run for Edit { + fn run(&self) -> Result<()> { + let now = util::current_time()?; + let db = &mut Database::open()?; + + match &self.cmd { + Some(cmd) => { + match cmd { + EditCommand::Decrement { path } => db.add(path, -1.0, now), + EditCommand::Delete { path } => { + db.remove(path); + } + EditCommand::Increment { path } => db.add(path, 1.0, now), + EditCommand::Reload => {} + } + db.save()?; + + let stdout = &mut io::stdout().lock(); + for dir in db.dirs().iter().rev() { + write!(stdout, "{}\0", dir.display().with_score(now).with_separator('\t')).pipe_exit("fzf")?; + } + Ok(()) + } + None => { + db.sort_by_score(now); + db.save()?; + Self::get_fzf()?.wait()?; + Ok(()) + } + } + } +} + +impl Edit { + fn get_fzf() -> Result { + Fzf::new()? + .args([ + // Search mode + "--scheme=path", + // Search result + "--tiebreak=end,chunk,index", + // Interface + "--bind=\ +btab:up,\ +ctrl-r:reload(zoxide edit reload),\ +ctrl-d:reload(zoxide edit delete {2..}),\ +ctrl-w:reload(zoxide edit increment {2..}),\ +ctrl-s:reload(zoxide edit decrement {2..}),\ +ctrl-z:ignore,\ +double-click:ignore,\ +enter:abort,\ +start:reload(zoxide edit reload),\ +tab:down", + "--cycle", + "--keep-right", + // Layout + "--border=sharp", + "--border-label= zoxide-edit ", + "--header=\ +ctrl-r:reload \tctrl-d:delete +ctrl-w:increment\tctrl-s:decrement + + SCORE\tPATH", + "--info=inline", + "--layout=reverse", + "--padding=1,0,0,0", + // Display + "--color=label:bold", + "--tabstop=1", + ]) + .enable_preview() + .spawn() + } +} diff --git a/src/cmd/import.rs b/src/cmd/import.rs index b5a2fd2..5d84937 100644 --- a/src/cmd/import.rs +++ b/src/cmd/import.rs @@ -3,24 +3,21 @@ use std::fs; use anyhow::{bail, Context, Result}; use crate::cmd::{Import, ImportFrom, Run}; -use crate::config; -use crate::db::{Database, DatabaseFile, Dir}; +use crate::db::Database; impl Run for Import { fn run(&self) -> Result<()> { let buffer = fs::read_to_string(&self.path) .with_context(|| format!("could not open database for importing: {}", &self.path.display()))?; - let data_dir = config::data_dir()?; - let mut db = DatabaseFile::new(data_dir); - let db = &mut db.open()?; - if !self.merge && !db.dirs.is_empty() { + let mut db = Database::open()?; + if !self.merge && !db.dirs().is_empty() { bail!("current database is not empty, specify --merge to continue anyway"); } match self.from { - ImportFrom::Autojump => from_autojump(db, &buffer), - ImportFrom::Z => from_z(db, &buffer), + ImportFrom::Autojump => import_autojump(&mut db, &buffer), + ImportFrom::Z => import_z(&mut db, &buffer), } .context("import error")?; @@ -28,7 +25,7 @@ impl Run for Import { } } -fn from_autojump<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { +fn import_autojump(db: &mut Database, buffer: &str) -> Result<()> { for line in buffer.lines() { if line.is_empty() { continue; @@ -37,24 +34,23 @@ fn from_autojump<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { let rank = split.next().with_context(|| format!("invalid entry: {line}"))?; let mut rank = rank.parse::().with_context(|| format!("invalid rank: {rank}"))?; - // Normalize the rank using a sigmoid function. Don't import actual ranks from autojump, - // since its scoring algorithm is very different and might take a while to get normalized. + // Normalize the rank using a sigmoid function. Don't import actual ranks from + // autojump, since its scoring algorithm is very different and might + // take a while to get normalized. rank = sigmoid(rank); let path = split.next().with_context(|| format!("invalid entry: {line}"))?; - db.dirs.push(Dir { path: path.into(), rank, last_accessed: 0 }); - db.modified = true; + db.add_unchecked(path, rank, 0); } - if db.modified { + if db.dirty() { db.dedup(); } - Ok(()) } -fn from_z<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { +fn import_z(db: &mut Database, buffer: &str) -> Result<()> { for line in buffer.lines() { if line.is_empty() { continue; @@ -69,14 +65,12 @@ fn from_z<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { let path = split.next().with_context(|| format!("invalid entry: {line}"))?; - db.dirs.push(Dir { path: path.into(), rank, last_accessed }); - db.modified = true; + db.add_unchecked(path, rank, last_accessed); } - if db.modified { + if db.dirty() { db.dedup(); } - Ok(()) } @@ -86,33 +80,33 @@ fn sigmoid(x: f64) -> f64 { #[cfg(test)] mod tests { - use super::sigmoid; - use crate::db::{Database, Dir}; + use super::*; + use crate::db::Dir; #[test] fn from_autojump() { - let buffer = r#" + let data_dir = tempfile::tempdir().unwrap(); + let mut db = Database::open_dir(data_dir.path()).unwrap(); + for (path, rank, last_accessed) in [ + ("/quux/quuz", 1.0, 100), + ("/corge/grault/garply", 6.0, 600), + ("/waldo/fred/plugh", 3.0, 300), + ("/xyzzy/thud", 8.0, 800), + ("/foo/bar", 9.0, 900), + ] { + db.add_unchecked(path, rank, last_accessed); + } + + let buffer = "\ 7.0 /baz 2.0 /foo/bar -5.0 /quux/quuz -"#; +5.0 /quux/quuz"; + import_autojump(&mut db, buffer).unwrap(); - let dirs = vec![ - Dir { path: "/quux/quuz".into(), rank: 1.0, last_accessed: 100 }, - Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, - Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, - Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, - Dir { path: "/foo/bar".into(), rank: 9.0, last_accessed: 900 }, - ]; - let data_dir = tempfile::tempdir().unwrap(); - let data_dir = &data_dir.path().to_path_buf(); - let mut db = Database { dirs: dirs.into(), modified: false, data_dir }; + db.sort_by_path(); + println!("got: {:?}", &db.dirs()); - super::from_autojump(&mut db, buffer).unwrap(); - db.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path)); - println!("got: {:?}", &db.dirs.as_slice()); - - let exp = &[ + let exp = [ Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 }, Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 }, @@ -122,7 +116,7 @@ mod tests { ]; println!("exp: {exp:?}"); - for (dir1, dir2) in db.dirs.iter().zip(exp) { + for (dir1, dir2) in db.dirs().iter().zip(exp) { assert_eq!(dir1.path, dir2.path); assert!((dir1.rank - dir2.rank).abs() < 0.01); assert_eq!(dir1.last_accessed, dir2.last_accessed); @@ -131,29 +125,29 @@ mod tests { #[test] fn from_z() { - let buffer = r#" + let data_dir = tempfile::tempdir().unwrap(); + let mut db = Database::open_dir(data_dir.path()).unwrap(); + for (path, rank, last_accessed) in [ + ("/quux/quuz", 1.0, 100), + ("/corge/grault/garply", 6.0, 600), + ("/waldo/fred/plugh", 3.0, 300), + ("/xyzzy/thud", 8.0, 800), + ("/foo/bar", 9.0, 900), + ] { + db.add_unchecked(path, rank, last_accessed); + } + + let buffer = "\ /baz|7|700 /quux/quuz|4|400 /foo/bar|2|200 -/quux/quuz|5|500 -"#; +/quux/quuz|5|500"; + import_z(&mut db, buffer).unwrap(); - let dirs = vec![ - Dir { path: "/quux/quuz".into(), rank: 1.0, last_accessed: 100 }, - Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, - Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, - Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, - Dir { path: "/foo/bar".into(), rank: 9.0, last_accessed: 900 }, - ]; - let data_dir = tempfile::tempdir().unwrap(); - let data_dir = &data_dir.path().to_path_buf(); - let mut db = Database { dirs: dirs.into(), modified: false, data_dir }; + db.sort_by_path(); + println!("got: {:?}", &db.dirs()); - super::from_z(&mut db, buffer).unwrap(); - db.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path)); - println!("got: {:?}", &db.dirs.as_slice()); - - let exp = &[ + let exp = [ Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 }, Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 }, @@ -163,7 +157,7 @@ mod tests { ]; println!("exp: {exp:?}"); - for (dir1, dir2) in db.dirs.iter().zip(exp) { + for (dir1, dir2) in db.dirs().iter().zip(exp) { assert_eq!(dir1.path, dir2.path); assert!((dir1.rank - dir2.rank).abs() < 0.01); assert_eq!(dir1.last_accessed, dir2.last_accessed); diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 32f6896..2c7609d 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -11,10 +11,8 @@ use crate::shell::{self, Opts}; impl Run for Init { fn run(&self) -> Result<()> { let cmd = if self.no_cmd { None } else { Some(self.cmd.as_str()) }; - let echo = config::echo(); let resolve_symlinks = config::resolve_symlinks(); - let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks }; let source = match self.shell { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 876a6aa..5c17474 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,5 +1,6 @@ mod add; mod cmd; +mod edit; mod import; mod init; mod query; @@ -17,6 +18,7 @@ impl Run for Cmd { fn run(&self) -> Result<()> { match self { Cmd::Add(cmd) => cmd.run(), + Cmd::Edit(cmd) => cmd.run(), Cmd::Import(cmd) => cmd.run(), Cmd::Init(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(), diff --git a/src/cmd/query.rs b/src/cmd/query.rs index fd7b690..58a56cb 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -1,20 +1,16 @@ -use anyhow::{Context, Result}; -use std::env; use std::io::{self, Write}; -use std::path::PathBuf; -use std::str::FromStr; + +use anyhow::{bail, Context, Result}; use crate::cmd::{Query, Run}; use crate::config; -use crate::db::{Database, DatabaseFile}; +use crate::db::{Database, Epoch, Stream}; use crate::error::BrokenPipeHandler; -use crate::util::{self, Fzf}; +use crate::util::{self, Fzf, FzfChild}; impl Run for Query { fn run(&self) -> Result<()> { - let data_dir = config::data_dir()?; - let mut db = DatabaseFile::new(data_dir); - let mut db = db.open()?; + let mut db = crate::db::Database::open()?; self.query(&mut db).and(db.save()) } } @@ -22,7 +18,49 @@ impl Run for Query { impl Query { fn query(&self, db: &mut Database) -> Result<()> { let now = util::current_time()?; + let mut stream = self.get_stream(db, now); + if self.interactive { + let mut fzf = Self::get_fzf()?; + let selection = loop { + match stream.next() { + Some(dir) => { + if let Some(selection) = fzf.write(dir, now)? { + break selection; + } + } + None => break fzf.wait()?, + } + }; + + if self.score { + print!("{selection}"); + } else { + let path = selection.get(7..).context("could not read selection from fzf")?; + print!("{path}"); + } + } else if self.list { + let handle = &mut io::stdout().lock(); + while let Some(dir) = stream.next() { + let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; + writeln!(handle, "{dir}").pipe_exit("stdout")?; + } + } else { + let handle = &mut io::stdout(); + let Some(dir) = stream.next() else { + if stream.did_exclude() { + bail!("you are already in the only match"); + } + bail!("no match found"); + }; + let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; + writeln!(handle, "{dir}").pipe_exit("stdout")?; + } + + Ok(()) + } + + fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Stream<'a> { let mut stream = db.stream(now).with_keywords(&self.keywords); if !self.all { let resolve_symlinks = config::resolve_symlinks(); @@ -31,64 +69,36 @@ impl Query { if let Some(path) = &self.exclude { stream = stream.with_exclude(path); } + stream + } - if self.interactive { - let mut fzf = Fzf::new(false)?; - let stdin = fzf.stdin(); - - let selection = loop { - let dir = match stream.next() { - Some(dir) => dir, - None => break fzf.select()?, - }; - - match writeln!(stdin, "{}", dir.display_score(now)) { - Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?, - result => result.context("could not write to fzf")?, - } - }; - - if self.score { - print!("{selection}"); - } else { - let path = selection.get(5..).context("could not read selection from fzf")?; - print!("{path}"); - } - } else if self.list { - let handle = &mut io::stdout().lock(); - while let Some(dir) = stream.next() { - if self.score { - writeln!(handle, "{}", dir.display_score(now)) - } else { - writeln!(handle, "{}", dir.display()) - } - .pipe_exit("stdout")?; - } - handle.flush().pipe_exit("stdout")?; + fn get_fzf() -> Result { + let mut fzf = Fzf::new()?; + if let Some(fzf_opts) = config::fzf_opts() { + fzf.env("FZF_DEFAULT_OPTS", fzf_opts) } else { - let excluded_dir = self.exclude.as_ref().map_or(PathBuf::new(), |path| PathBuf::from_str(path).unwrap()); - - let try_dir = stream.next(); - let dir = match try_dir { - Some(dir) => dir, - None => { - // Current Directory is passed as excluded in __zoxide_z - if excluded_dir == env::current_dir()? { - try_dir.context("already in the matched directory")? - } else { - try_dir.context("no match found")? - } - } - }; - - if self.score { - writeln!(io::stdout(), "{}", dir.display_score(now)) - } else { - writeln!(io::stdout(), "{}", dir.display()) - } - .pipe_exit("stdout")?; + fzf.args([ + // Search mode + "--scheme=path", + // Search result + "--tiebreak=end,chunk,index", + // Interface + "--bind=ctrl-z:ignore,btab:up,tab:down", + "--cycle", + "--keep-right", + // Layout + "--border=sharp", // rounded edges don't display correctly on some terminals + "--height=45%", + "--info=inline", + "--layout=reverse", + // Display + "--tabstop=1", + // Scripting + "--exit-0", + "--select-1", + ]) + .enable_preview() } - - Ok(()) + .spawn() } } diff --git a/src/cmd/remove.rs b/src/cmd/remove.rs index 45b1c9b..55c6989 100644 --- a/src/cmd/remove.rs +++ b/src/cmd/remove.rs @@ -1,54 +1,19 @@ -use std::io::{self, Write}; - -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use crate::cmd::{Remove, Run}; -use crate::config; -use crate::db::DatabaseFile; -use crate::util::{self, Fzf}; +use crate::db::Database; +use crate::util; impl Run for Remove { fn run(&self) -> Result<()> { - let data_dir = config::data_dir()?; - let mut db = DatabaseFile::new(data_dir); - let mut db = db.open()?; + let mut db = Database::open()?; - if self.interactive { - let keywords = &self.paths; - let now = util::current_time()?; - let mut stream = db.stream(now).with_keywords(keywords); - - let mut fzf = Fzf::new(true)?; - let stdin = fzf.stdin(); - - let selection = loop { - let dir = match stream.next() { - Some(dir) => dir, - None => break fzf.select()?, - }; - - match writeln!(stdin, "{}", dir.display_score(now)) { - Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?, - result => result.context("could not write to fzf")?, - } - }; - - let paths = selection.lines().filter_map(|line| line.get(5..)); - for path in paths { - if !db.remove(path) { - db.modified = false; - bail!("path not found in database: {path}"); - } - } - } else { - for path in &self.paths { - if !db.remove(path) { - let path_abs = util::resolve_path(path)?; - let path_abs = util::path_to_str(&path_abs)?; - if path_abs == path || !db.remove(path_abs) { - db.modified = false; - bail!("path not found in database: {path}") - } + for path in &self.paths { + if !db.remove(path) { + let path_abs = util::resolve_path(path)?; + let path_abs = util::path_to_str(&path_abs)?; + if path_abs == path || !db.remove(path_abs) { + bail!("path not found in database: {path}") } } } diff --git a/src/db/dir.rs b/src/db/dir.rs index c59a441..5d6d62c 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -1,83 +1,9 @@ use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; -use std::ops::{Deref, DerefMut}; -use anyhow::{bail, Context, Result}; -use bincode::Options as _; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize)] -pub struct DirList<'a>(#[serde(borrow)] pub Vec>); - -impl DirList<'_> { - const VERSION: u32 = 3; - - pub fn new() -> DirList<'static> { - DirList(Vec::new()) - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - // Assume a maximum size for the database. This prevents bincode from throwing strange - // errors when it encounters invalid data. - const MAX_SIZE: u64 = 32 << 20; // 32 MiB - let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE); - - // Split bytes into sections. - let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _; - if bytes.len() < version_size { - bail!("could not deserialize database: corrupted data"); - } - let (bytes_version, bytes_dirs) = bytes.split_at(version_size); - - // Deserialize sections. - (|| { - let version = deserializer.deserialize(bytes_version)?; - match version { - Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?), - version => { - bail!("unsupported version (got {version}, supports {})", Self::VERSION) - } - } - })() - .context("could not deserialize database") - } - - pub fn to_bytes(&self) -> Result> { - (|| -> bincode::Result<_> { - // Preallocate buffer with combined size of sections. - let version_size = bincode::serialized_size(&Self::VERSION)?; - let dirs_size = bincode::serialized_size(&self)?; - let buffer_size = version_size + dirs_size; - let mut buffer = Vec::with_capacity(buffer_size as _); - - // Serialize sections into buffer. - bincode::serialize_into(&mut buffer, &Self::VERSION)?; - bincode::serialize_into(&mut buffer, &self)?; - Ok(buffer) - })() - .context("could not serialize database") - } -} - -impl<'a> Deref for DirList<'a> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a> DerefMut for DirList<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl<'a> From>> for DirList<'a> { - fn from(dirs: Vec>) -> Self { - DirList(dirs) - } -} +use crate::util::{DAY, HOUR, WEEK}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Dir<'a> { @@ -88,11 +14,11 @@ pub struct Dir<'a> { } impl Dir<'_> { - pub fn score(&self, now: Epoch) -> Rank { - const HOUR: Epoch = 60 * 60; - const DAY: Epoch = 24 * HOUR; - const WEEK: Epoch = 7 * DAY; + pub fn display(&self) -> DirDisplay<'_> { + DirDisplay::new(self) + } + pub fn score(&self, now: Epoch) -> Rank { // The older the entry, the lesser its importance. let duration = now.saturating_sub(self.last_accessed); if duration < HOUR { @@ -105,56 +31,39 @@ impl Dir<'_> { self.rank * 0.25 } } - - pub fn display(&self) -> DirDisplay { - DirDisplay { dir: self } - } - - pub fn display_score(&self, now: Epoch) -> DirDisplayScore { - DirDisplayScore { dir: self, now } - } } pub struct DirDisplay<'a> { dir: &'a Dir<'a>, + now: Option, + separator: char, +} + +impl<'a> DirDisplay<'a> { + fn new(dir: &'a Dir) -> Self { + Self { dir, separator: ' ', now: None } + } + + pub fn with_score(mut self, now: Epoch) -> Self { + self.now = Some(now); + self + } + + pub fn with_separator(mut self, separator: char) -> Self { + self.separator = separator; + self + } } impl Display for DirDisplay<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Some(now) = self.now { + let score = self.dir.score(now).clamp(0.0, 9999.0); + write!(f, "{score:>6.1}{}", self.separator)?; + } write!(f, "{}", self.dir.path) } } -pub struct DirDisplayScore<'a> { - dir: &'a Dir<'a>, - now: Epoch, -} - -impl Display for DirDisplayScore<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let score = self.dir.score(self.now).clamp(0.0, 9999.0) as u32; - write!(f, "{:>4} {}", score, self.dir.path) - } -} - pub type Rank = f64; pub type Epoch = u64; - -#[cfg(test)] -mod tests { - use std::borrow::Cow; - - use super::*; - - #[test] - fn zero_copy() { - let dirs = DirList(vec![Dir { path: "/".into(), rank: 0.0, last_accessed: 0 }]); - - let bytes = dirs.to_bytes().unwrap(); - let dirs = DirList::from_bytes(&bytes).unwrap(); - - for dir in dirs.iter() { - assert!(matches!(dir.path, Cow::Borrowed(_))) - } - } -} diff --git a/src/db/mod.rs b/src/db/mod.rs index cf9b54a..2cc7469 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -4,145 +4,220 @@ mod stream; use std::path::{Path, PathBuf}; use std::{fs, io}; -use anyhow::{Context, Result}; -pub use dir::{Dir, DirList, Epoch, Rank}; -pub use stream::Stream; +use anyhow::{bail, Context, Result}; +use bincode::Options; +use ouroboros::self_referencing; -use crate::util; +pub use crate::db::dir::{Dir, Epoch, Rank}; +pub use crate::db::stream::Stream; +use crate::{config, util}; -#[derive(Debug)] -pub struct Database<'file> { - pub dirs: DirList<'file>, - pub modified: bool, - pub data_dir: &'file Path, +#[self_referencing] +pub struct Database { + path: PathBuf, + bytes: Vec, + #[borrows(bytes)] + #[covariant] + pub dirs: Vec>, + dirty: bool, } -impl<'file> Database<'file> { - pub fn save(&mut self) -> Result<()> { - if !self.modified { - return Ok(()); - } +impl Database { + const VERSION: u32 = 3; - let buffer = self.dirs.to_bytes()?; - let path = db_path(&self.data_dir); - util::write(&path, &buffer).context("could not write to database")?; - self.modified = false; - Ok(()) + pub fn open() -> Result { + let data_dir = config::data_dir()?; + Self::open_dir(data_dir) } - /// Adds a new directory or increments its rank. Also updates its last accessed time. - pub fn add>(&mut self, path: S, now: Epoch) { - let path = path.as_ref(); + pub fn open_dir(data_dir: impl AsRef) -> Result { + let data_dir = data_dir.as_ref(); + let path = data_dir.join("db.zo"); - match self.dirs.iter_mut().find(|dir| dir.path == path) { - None => { - self.dirs.push(Dir { path: path.to_string().into(), last_accessed: now, rank: 1.0 }); - } - Some(dir) => { - dir.last_accessed = now; - dir.rank += 1.0; - } - }; - - self.modified = true; - } - - pub fn dedup(&mut self) { - // Sort by path, so that equal paths are next to each other. - self.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path)); - - for idx in (1..self.dirs.len()).rev() { - // Check if curr_dir and next_dir have equal paths. - let curr_dir = &self.dirs[idx]; - let next_dir = &self.dirs[idx - 1]; - if next_dir.path != curr_dir.path { - continue; - } - - // Merge curr_dir's rank and last_accessed into next_dir. - let rank = curr_dir.rank; - let last_accessed = curr_dir.last_accessed; - let next_dir = &mut self.dirs[idx - 1]; - next_dir.last_accessed = next_dir.last_accessed.max(last_accessed); - next_dir.rank += rank; - - // Delete curr_dir. - self.dirs.swap_remove(idx); - self.modified = true; - } - } - - // Streaming iterator for directories. - pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> { - Stream::new(self, now) - } - - /// Removes the directory with `path` from the store. This does not preserve ordering, but is - /// O(1). - pub fn remove>(&mut self, path: S) -> bool { - let path = path.as_ref(); - - if let Some(idx) = self.dirs.iter().position(|dir| dir.path == path) { - self.dirs.swap_remove(idx); - self.modified = true; - return true; - } - - false - } - - pub fn age(&mut self, max_age: Rank) { - let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::(); - if sum_age > max_age { - let factor = 0.9 * max_age / sum_age; - for idx in (0..self.dirs.len()).rev() { - let dir = &mut self.dirs[idx]; - dir.rank *= factor; - if dir.rank < 1.0 { - self.dirs.swap_remove(idx); - } - } - self.modified = true; - } - } -} - -pub struct DatabaseFile { - buffer: Vec, - data_dir: PathBuf, -} - -impl DatabaseFile { - pub fn new>(data_dir: P) -> Self { - DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() } - } - - pub fn open(&mut self) -> Result { - // Read the entire database to memory. For smaller files, this is faster than - // mmap / streaming, and allows for zero-copy deserialization. - let path = db_path(&self.data_dir); match fs::read(&path) { - Ok(buffer) => { - self.buffer = buffer; - let dirs = DirList::from_bytes(&self.buffer) - .with_context(|| format!("could not deserialize database: {}", path.display()))?; - Ok(Database { dirs, modified: false, data_dir: &self.data_dir }) - } + Ok(bytes) => Self::try_new(path, bytes, |bytes| Self::deserialize(bytes), false), Err(e) if e.kind() == io::ErrorKind::NotFound => { - // Create data directory, but don't create any file yet. The file will be created - // later by [`Database::save`] if any data is modified. - fs::create_dir_all(&self.data_dir) - .with_context(|| format!("unable to create data directory: {}", self.data_dir.display()))?; - Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir }) + // Create data directory, but don't create any file yet. The file will be + // created later by [`Database::save`] if any data is modified. + fs::create_dir_all(data_dir) + .with_context(|| format!("unable to create data directory: {}", data_dir.display()))?; + Ok(Self::new(path, Vec::new(), |_| Vec::new(), false)) } Err(e) => Err(e).with_context(|| format!("could not read from database: {}", path.display())), } } -} -fn db_path>(data_dir: P) -> PathBuf { - const DB_FILENAME: &str = "db.zo"; - data_dir.as_ref().join(DB_FILENAME) + pub fn save(&mut self) -> Result<()> { + // Only write to disk if the database is modified. + if !self.dirty() { + return Ok(()); + } + + let bytes = Self::serialize(self.dirs())?; + util::write(self.borrow_path(), bytes).context("could not write to database")?; + self.with_dirty_mut(|dirty| *dirty = false); + + Ok(()) + } + + /// Increments the rank of a directory, or creates it if it does not exist. + pub fn add(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { + self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { + Some(dir) => dir.rank = (dir.rank + by).max(0.0), + None => dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }), + }); + self.with_dirty_mut(|dirty| *dirty = true); + } + + /// Creates a new directory. This will create a duplicate entry if this + /// directory is always in the database, it is expected that the user either + /// does a check before calling this, or calls `dedup()` afterward. + pub fn add_unchecked(&mut self, path: impl AsRef + Into, rank: Rank, now: Epoch) { + self.with_dirs_mut(|dirs| dirs.push(Dir { path: path.into().into(), rank, last_accessed: now })); + self.with_dirty_mut(|dirty| *dirty = true); + } + + /// Increments the rank and updates the last_accessed of a directory, or + /// creates it if it does not exist. + pub fn add_update(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { + self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { + Some(dir) => { + dir.rank = (dir.rank + by).max(0.0); + dir.last_accessed = now; + } + None => dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }), + }); + self.with_dirty_mut(|dirty| *dirty = true); + } + + /// Removes the directory with `path` from the store. This does not preserve + /// ordering, but is O(1). + pub fn remove(&mut self, path: impl AsRef) -> bool { + match self.dirs().iter().position(|dir| dir.path == path.as_ref()) { + Some(idx) => { + self.swap_remove(idx); + true + } + None => false, + } + } + + pub fn swap_remove(&mut self, idx: usize) { + self.with_dirs_mut(|dirs| dirs.swap_remove(idx)); + self.with_dirty_mut(|dirty| *dirty = true); + } + + pub fn age(&mut self, max_age: Rank) { + let mut dirty = false; + self.with_dirs_mut(|dirs| { + let total_age = dirs.iter().map(|dir| dir.rank).sum::(); + if total_age > max_age { + let factor = 0.9 * max_age / total_age; + for idx in (0..dirs.len()).rev() { + let dir = &mut dirs[idx]; + dir.rank *= factor; + if dir.rank < 1.0 { + dirs.swap_remove(idx); + } + } + dirty = true; + } + }); + self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty); + } + + pub fn stream(&mut self, now: Epoch) -> Stream { + Stream::new(self, now) + } + + pub fn dedup(&mut self) { + // Sort by path, so that equal paths are next to each other. + self.sort_by_path(); + + let mut dirty = false; + self.with_dirs_mut(|dirs| { + for idx in (1..dirs.len()).rev() { + // Check if curr_dir and next_dir have equal paths. + let curr_dir = &dirs[idx]; + let next_dir = &dirs[idx - 1]; + if next_dir.path != curr_dir.path { + continue; + } + + // Merge curr_dir's rank and last_accessed into next_dir. + let rank = curr_dir.rank; + let last_accessed = curr_dir.last_accessed; + let next_dir = &mut dirs[idx - 1]; + next_dir.last_accessed = next_dir.last_accessed.max(last_accessed); + next_dir.rank += rank; + + // Delete curr_dir. + dirs.swap_remove(idx); + dirty = true; + } + }); + self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty); + } + + pub fn sort_by_path(&mut self) { + self.with_dirs_mut(|dirs| dirs.sort_unstable_by(|dir1, dir2| dir1.path.cmp(&dir2.path))); + self.with_dirty_mut(|dirty| *dirty = true); + } + + pub fn sort_by_score(&mut self, now: Epoch) { + self.with_dirs_mut(|dirs| { + dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| dir1.score(now).total_cmp(&dir2.score(now))) + }); + self.with_dirty_mut(|dirty| *dirty = true); + } + + pub fn dirty(&self) -> bool { + *self.borrow_dirty() + } + + pub fn dirs(&self) -> &[Dir] { + self.borrow_dirs() + } + + fn serialize(dirs: &[Dir<'_>]) -> Result> { + (|| -> bincode::Result<_> { + // Preallocate buffer with combined size of sections. + let buffer_size = bincode::serialized_size(&Self::VERSION)? + bincode::serialized_size(&dirs)?; + let mut buffer = Vec::with_capacity(buffer_size as usize); + + // Serialize sections into buffer. + bincode::serialize_into(&mut buffer, &Self::VERSION)?; + bincode::serialize_into(&mut buffer, &dirs)?; + + Ok(buffer) + })() + .context("could not serialize database") + } + + fn deserialize(bytes: &[u8]) -> Result> { + // Assume a maximum size for the database. This prevents bincode from throwing + // strange errors when it encounters invalid data. + const MAX_SIZE: u64 = 32 << 20; // 32 MiB + let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE); + + // Split bytes into sections. + let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _; + if bytes.len() < version_size { + bail!("could not deserialize database: corrupted data"); + } + let (bytes_version, bytes_dirs) = bytes.split_at(version_size); + + // Deserialize sections. + let version = deserializer.deserialize(bytes_version)?; + let dirs = match version { + Self::VERSION => deserializer.deserialize(bytes_dirs).context("could not deserialize database")?, + version => { + bail!("unsupported version (got {version}, supports {})", Self::VERSION) + } + }; + + Ok(dirs) + } } #[cfg(test)] @@ -151,50 +226,49 @@ mod tests { #[test] fn add() { + let data_dir = tempfile::tempdir().unwrap(); let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; let now = 946684800; - let data_dir = tempfile::tempdir().unwrap(); { - let mut db = DatabaseFile::new(data_dir.path()); - let mut db = db.open().unwrap(); - db.add(path, now); - db.add(path, now); + let mut db = Database::open_dir(data_dir.path()).unwrap(); + db.add(path, 1.0, now); + db.add(path, 1.0, now); db.save().unwrap(); } - { - let mut db = DatabaseFile::new(data_dir.path()); - let db = db.open().unwrap(); - assert_eq!(db.dirs.len(), 1); - let dir = &db.dirs[0]; + { + let db = Database::open_dir(data_dir.path()).unwrap(); + assert_eq!(db.dirs().len(), 1); + + let dir = &db.dirs()[0]; assert_eq!(dir.path, path); + assert!((dir.rank - 2.0).abs() < 0.01); assert_eq!(dir.last_accessed, now); } } #[test] fn remove() { + let data_dir = tempfile::tempdir().unwrap(); let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; let now = 946684800; - let data_dir = tempfile::tempdir().unwrap(); { - let mut db = DatabaseFile::new(data_dir.path()); - let mut db = db.open().unwrap(); - db.add(path, now); + let mut db = Database::open_dir(data_dir.path()).unwrap(); + db.add(path, 1.0, now); db.save().unwrap(); } + { - let mut db = DatabaseFile::new(data_dir.path()); - let mut db = db.open().unwrap(); + let mut db = Database::open_dir(data_dir.path()).unwrap(); assert!(db.remove(path)); db.save().unwrap(); } + { - let mut db = DatabaseFile::new(data_dir.path()); - let mut db = db.open().unwrap(); - assert!(db.dirs.is_empty()); + let mut db = Database::open_dir(data_dir.path()).unwrap(); + assert!(db.dirs().is_empty()); assert!(!db.remove(path)); db.save().unwrap(); } diff --git a/src/db/stream.rs b/src/db/stream.rs index 275d52e..44b59d9 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -3,33 +3,35 @@ use std::ops::Range; use std::{fs, path}; use crate::db::{Database, Dir, Epoch}; -use crate::util; +use crate::util::{self, MONTH}; -pub struct Stream<'db, 'file> { - db: &'db mut Database<'file>, +pub struct Stream<'a> { + // State + db: &'a mut Database, idxs: Rev>, + did_exclude: bool, + // Configuration keywords: Vec, - check_exists: bool, expire_below: Epoch, resolve_symlinks: bool, - exclude_path: Option, } -impl<'db, 'file> Stream<'db, 'file> { - pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self { - // Iterate in descending order of score. - db.dirs.sort_unstable_by(|dir1, dir2| dir1.score(now).total_cmp(&dir2.score(now))); - let idxs = (0..db.dirs.len()).rev(); +impl<'a> Stream<'a> { + pub fn new(db: &'a mut Database, now: Epoch) -> Self { + db.sort_by_score(now); + let idxs = (0..db.dirs().len()).rev(); - // If a directory is deleted and hasn't been used for 90 days, delete it from the database. - let expire_below = now.saturating_sub(90 * 24 * 60 * 60); + // If a directory is deleted and hasn't been used for 3 months, delete + // it from the database. + let expire_below = now.saturating_sub(3 * MONTH); Stream { db, idxs, + did_exclude: false, keywords: Vec::new(), check_exists: false, expire_below, @@ -38,7 +40,7 @@ impl<'db, 'file> Stream<'db, 'file> { } } - pub fn with_exclude>(mut self, path: S) -> Self { + pub fn with_exclude(mut self, path: impl Into) -> Self { self.exclude_path = Some(path.into()); self } @@ -49,14 +51,14 @@ impl<'db, 'file> Stream<'db, 'file> { self } - pub fn with_keywords>(mut self, keywords: &[S]) -> Self { + pub fn with_keywords(mut self, keywords: &[impl AsRef]) -> Self { self.keywords = keywords.iter().map(util::to_lowercase).collect(); self } - pub fn next(&mut self) -> Option<&Dir<'file>> { + pub fn next(&mut self) -> Option<&Dir> { while let Some(idx) = self.idxs.next() { - let dir = &self.db.dirs[idx]; + let dir = &self.db.dirs()[idx]; if !self.matches_keywords(&dir.path) { continue; @@ -64,32 +66,36 @@ impl<'db, 'file> Stream<'db, 'file> { if !self.matches_exists(&dir.path) { if dir.last_accessed < self.expire_below { - self.db.dirs.swap_remove(idx); - self.db.modified = true; + self.db.swap_remove(idx); } continue; } if Some(dir.path.as_ref()) == self.exclude_path.as_deref() { + self.did_exclude = true; continue; } - let dir = &self.db.dirs[idx]; + let dir = &self.db.dirs()[idx]; return Some(dir); } None } - fn matches_exists>(&self, path: S) -> bool { + pub fn did_exclude(&self) -> bool { + self.did_exclude + } + + fn matches_exists(&self, path: &str) -> bool { if !self.check_exists { return true; } let resolver = if self.resolve_symlinks { fs::symlink_metadata } else { fs::metadata }; - resolver(path.as_ref()).map(|m| m.is_dir()).unwrap_or_default() + resolver(path).map(|m| m.is_dir()).unwrap_or_default() } - fn matches_keywords>(&self, path: S) -> bool { + fn matches_keywords(&self, path: &str) -> bool { let (keywords_last, keywords) = match self.keywords.split_last() { Some(split) => split, None => return true, @@ -147,8 +153,8 @@ mod tests { #[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { - let mut db = Database { dirs: Vec::new().into(), modified: false, data_dir: &PathBuf::new() }; - let stream = db.stream(0).with_keywords(keywords); + let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let stream = Stream::new(db, 0).with_keywords(keywords); assert_eq!(is_match, stream.matches_keywords(path)); } } diff --git a/src/shell.rs b/src/shell.rs index 5662aa0..a3654c7 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -57,7 +57,7 @@ mod tests { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Bash(&opts).render().unwrap(); Command::new("bash") - .args(&["--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) + .args(["--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) .assert() .success() .stdout("") @@ -70,7 +70,7 @@ mod tests { let source = Bash(&opts).render().unwrap(); Command::new("shellcheck") - .args(&["--enable", "all", "--shell", "bash", "-"]) + .args(["--enable", "all", "--shell", "bash", "-"]) .write_stdin(source) .assert() .success() @@ -85,7 +85,7 @@ mod tests { source.push('\n'); Command::new("shfmt") - .args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"]) + .args(["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"]) .write_stdin(source) .assert() .success() @@ -98,14 +98,14 @@ mod tests { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let mut source = String::new(); - // Filter out lines using edit:*, since those functions are only available in the - // interactive editor. + // Filter out lines using edit:*, since those functions are only available in + // the interactive editor. for line in Elvish(&opts).render().unwrap().split('\n').filter(|line| !line.contains("edit:")) { source.push_str(line); source.push('\n'); } - Command::new("elvish").args(&["-c", &source, "-norc"]).assert().success().stdout("").stderr(""); + Command::new("elvish").args(["-c", &source, "-norc"]).assert().success().stdout("").stderr(""); } #[apply(opts)] @@ -118,7 +118,7 @@ mod tests { Command::new("fish") .env("HOME", tempdir) - .args(&["--command", &source, "--private"]) + .args(["--command", &source, "--no-config", "--private"]) .assert() .success() .stdout("") @@ -152,7 +152,7 @@ mod tests { let tempdir = tempdir.path(); let assert = - Command::new("nu").env("HOME", tempdir).args(&["--commands", &source]).assert().success().stderr(""); + Command::new("nu").env("HOME", tempdir).args(["--commands", &source]).assert().success().stderr(""); if opts.hook != InitHook::Pwd { assert.stdout(""); @@ -165,7 +165,7 @@ mod tests { let source = Posix(&opts).render().unwrap(); let assert = Command::new("bash") - .args(&["--posix", "--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) + .args(["--posix", "--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) .assert() .success() .stderr(""); @@ -179,7 +179,7 @@ mod tests { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Posix(&opts).render().unwrap(); - let assert = Command::new("dash").args(&["-e", "-u", "-c", &source]).assert().success().stderr(""); + let assert = Command::new("dash").args(["-e", "-u", "-c", &source]).assert().success().stderr(""); if opts.hook != InitHook::Pwd { assert.stdout(""); } @@ -191,7 +191,7 @@ mod tests { let source = Posix(&opts).render().unwrap(); Command::new("shellcheck") - .args(&["--enable", "all", "--shell", "sh", "-"]) + .args(["--enable", "all", "--shell", "sh", "-"]) .write_stdin(source) .assert() .success() @@ -206,7 +206,7 @@ mod tests { source.push('\n'); Command::new("shfmt") - .args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) + .args(["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) .write_stdin(source) .assert() .success() @@ -221,7 +221,7 @@ mod tests { Powershell(&opts).render_into(&mut source).unwrap(); Command::new("pwsh") - .args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source]) + .args(["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source]) .assert() .success() .stdout("") @@ -234,7 +234,7 @@ mod tests { let mut source = Xonsh(&opts).render().unwrap(); source.push('\n'); - Command::new("black").args(&["--check", "--diff", "-"]).write_stdin(source).assert().success().stdout(""); + Command::new("black").args(["--check", "--diff", "-"]).write_stdin(source).assert().success().stdout(""); } #[apply(opts)] @@ -242,7 +242,7 @@ mod tests { let opts = Opts { cmd, hook, echo, resolve_symlinks }; let source = Xonsh(&opts).render().unwrap(); - Command::new("mypy").args(&["--command", &source, "--strict"]).assert().success().stderr(""); + Command::new("mypy").args(["--command", &source, "--strict"]).assert().success().stderr(""); } #[apply(opts)] @@ -252,7 +252,7 @@ mod tests { source.push('\n'); Command::new("pylint") - .args(&["--from-stdin", "--persistent=n", "zoxide"]) + .args(["--from-stdin", "--persistent=n", "zoxide"]) .write_stdin(source) .assert() .success() @@ -268,7 +268,7 @@ mod tests { let tempdir = tempdir.path().to_str().unwrap(); Command::new("xonsh") - .args(&["-c", &source, "--no-rc"]) + .args(["-c", &source, "--no-rc"]) .env("HOME", tempdir) .assert() .success() @@ -283,7 +283,7 @@ mod tests { // ShellCheck doesn't support zsh yet: https://github.com/koalaman/shellcheck/issues/809 Command::new("shellcheck") - .args(&["--enable", "all", "--shell", "bash", "-"]) + .args(["--enable", "all", "--shell", "bash", "-"]) .write_stdin(source) .assert() .success() @@ -297,7 +297,7 @@ mod tests { let source = Zsh(&opts).render().unwrap(); Command::new("zsh") - .args(&["-e", "-u", "-o", "pipefail", "--no-globalrcs", "--no-rcs", "-c", &source]) + .args(["-e", "-u", "-o", "pipefail", "--no-globalrcs", "--no-rcs", "-c", &source]) .assert() .success() .stdout("") diff --git a/src/util.rs b/src/util.rs index d73e2cd..6827f7a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,8 @@ +use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Write}; use std::path::{Component, Path, PathBuf}; -use std::process::{Child, ChildStdin, Command, Stdio}; +use std::process::{Child, Command, Stdio}; use std::time::SystemTime; use std::{env, mem}; @@ -9,81 +10,133 @@ use std::{env, mem}; use anyhow::anyhow; use anyhow::{bail, Context, Result}; -use crate::config; -use crate::db::Epoch; +use crate::db::{Dir, Epoch}; use crate::error::SilentExit; -pub struct Fzf { - child: Child, -} +pub const SECOND: Epoch = 1; +pub const MINUTE: Epoch = 60 * SECOND; +pub const HOUR: Epoch = 60 * MINUTE; +pub const DAY: Epoch = 24 * HOUR; +pub const WEEK: Epoch = 7 * DAY; +pub const MONTH: Epoch = 30 * DAY; + +pub struct Fzf(Command); impl Fzf { - pub fn new(multiple: bool) -> Result { - const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?"; + const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?"; + pub fn new() -> Result { // On Windows, CreateProcess implicitly searches the current working // directory for the executable, which is a potential security issue. // Instead, we resolve the path to the executable and then pass it to // CreateProcess. #[cfg(windows)] - let mut command = Command::new(which::which("fzf.exe").map_err(|_| anyhow!(ERR_FZF_NOT_FOUND))?); + let program = which::which("fzf.exe").map_err(|_| anyhow!(Self::ERR_FZF_NOT_FOUND))?; #[cfg(not(windows))] - let mut command = Command::new("fzf"); - if multiple { - command.arg("-m"); - } - command.arg("--nth=2..").stdin(Stdio::piped()).stdout(Stdio::piped()); - if let Some(fzf_opts) = config::fzf_opts() { - command.env("FZF_DEFAULT_OPTS", fzf_opts); - } else { - command.args(&[ - // Search result - "--no-sort", - // Interface - "--keep-right", - // Layout - "--height=50%", - "--info=inline", - "--layout=reverse", - // Scripting - "--exit-0", - "--select-1", - // Key/Event bindings - "--bind=ctrl-z:ignore", - ]); - if cfg!(unix) { - // Non-POSIX args are only available on certain operating systems. - const PREVIEW_CMD: &str = if cfg!(target_os = "linux") { - r"\command -p ls -Cp --color=always --group-directories-first {2..}" - } else { - r"\command -p ls -Cp {2..}" - }; - command.args(&["--preview", PREVIEW_CMD, "--preview-window=down,30%"]).env("SHELL", "sh"); - } - } + let program = "fzf"; - let child = match command.spawn() { - Ok(child) => child, - Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(ERR_FZF_NOT_FOUND), - Err(e) => Err(e).context("could not launch fzf")?, - }; + let mut cmd = Command::new(program); + cmd.args([ + // Search mode + "--delimiter=\t", + "--nth=2", + // Scripting + "--read0", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); - Ok(Fzf { child }) + Ok(Fzf(cmd)) } - pub fn stdin(&mut self) -> &mut ChildStdin { - self.child.stdin.as_mut().unwrap() + pub fn enable_preview(&mut self) -> &mut Self { + // Previews are only supported on UNIX. + if !cfg!(unix) { + return self; + } + + self.args([ + // Non-POSIX args are only available on certain operating systems. + if cfg!(target_os = "linux") { + r"--preview=\command -p ls -Cp --color=always --group-directories-first {2..}" + } else { + r"--preview=\command -p ls -Cp {2..}" + }, + // Rounded edges don't display correctly on some terminals. + "--preview-window=down,30%,sharp", + ]) + .envs([ + // Enables colorized `ls` output on macOS / FreeBSD. + ("CLICOLOR", "1"), + // Forces colorized `ls` output when the output is not a + // TTY (like in fzf's preview window) on macOS / + // FreeBSD. + ("CLICOLOR_FORCE", "1"), + // Ensures that the preview command is run in a + // POSIX-compliant shell, regardless of what shell the + // user has selected. + ("SHELL", "sh"), + ]) } - pub fn select(mut self) -> Result { + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.0.args(args); + self + } + + pub fn env(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef, + V: AsRef, + { + self.0.env(key, val); + self + } + + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.0.envs(vars); + self + } + + pub fn spawn(&mut self) -> Result { + match self.0.spawn() { + Ok(child) => Ok(FzfChild(child)), + Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_FZF_NOT_FOUND), + Err(e) => Err(e).context("could not launch fzf"), + } + } +} + +pub struct FzfChild(Child); + +impl FzfChild { + pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result> { + let handle = self.0.stdin.as_mut().unwrap(); + match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) { + Ok(()) => Ok(None), + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => self.wait().map(Some), + Err(e) => Err(e).context("could not write to fzf"), + } + } + + pub fn wait(&mut self) -> Result { // Drop stdin to prevent deadlock. - mem::drop(self.child.stdin.take()); + mem::drop(self.0.stdin.take()); - let mut stdout = self.child.stdout.take().unwrap(); + let mut stdout = self.0.stdout.take().unwrap(); let mut output = String::new(); stdout.read_to_string(&mut output).context("failed to read from fzf")?; - let status = self.child.wait().context("wait failed on fzf")?; + let status = self.0.wait().context("wait failed on fzf")?; match status.code() { Some(0) => Ok(output), Some(1) => bail!("no match found"), @@ -96,7 +149,7 @@ impl Fzf { } /// Similar to [`fs::write`], but atomic (best effort on Windows). -pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { +pub fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { let path = path.as_ref(); let contents = contents.as_ref(); let dir = path.parent().unwrap(); @@ -133,7 +186,7 @@ pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> } /// Atomically create a tmpfile in the given directory. -fn tmpfile>(dir: P) -> Result<(File, PathBuf)> { +fn tmpfile(dir: impl AsRef) -> Result<(File, PathBuf)> { const MAX_ATTEMPTS: usize = 5; const TMP_NAME_LEN: usize = 16; let dir = dir.as_ref(); @@ -153,35 +206,34 @@ fn tmpfile>(dir: P) -> Result<(File, PathBuf)> { // Atomically create the tmpfile. match OpenOptions::new().write(true).create_new(true).open(&path) { Ok(file) => break Ok((file, path)), - Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => (), - Err(e) => break Err(e).with_context(|| format!("could not create file: {}", path.display())), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => {} + Err(e) => { + break Err(e).with_context(|| format!("could not create file: {}", path.display())); + } } } } -/// Similar to [`fs::rename`], but retries on Windows. -fn rename, Q: AsRef>(from: P, to: Q) -> Result<()> { - const MAX_ATTEMPTS: usize = 5; +/// Similar to [`fs::rename`], but with retries on Windows. +fn rename(from: impl AsRef, to: impl AsRef) -> Result<()> { let from = from.as_ref(); let to = to.as_ref(); - if cfg!(windows) { - let mut attempts = 0; - loop { - attempts += 1; - match fs::rename(from, to) { - Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => (), - result => break result, + const MAX_ATTEMPTS: usize = if cfg!(windows) { 5 } else { 1 }; + let mut attempts = 0; + + loop { + match fs::rename(from, to) { + Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => attempts += 1, + result => { + break result.with_context(|| format!("could not rename file: {} -> {}", from.display(), to.display())); } } - } else { - fs::rename(from, to) } - .with_context(|| format!("could not rename file: {} -> {}", from.display(), to.display())) } -pub fn canonicalize>(path: &P) -> Result { - dunce::canonicalize(path).with_context(|| format!("could not resolve path: {}", path.as_ref().display())) +pub fn canonicalize(path: impl AsRef) -> Result { + dunce::canonicalize(&path).with_context(|| format!("could not resolve path: {}", path.as_ref().display())) } pub fn current_dir() -> Result { @@ -195,14 +247,14 @@ pub fn current_time() -> Result { Ok(current_time) } -pub fn path_to_str>(path: &P) -> Result<&str> { +pub fn path_to_str(path: &impl AsRef) -> Result<&str> { let path = path.as_ref(); path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display())) } -/// Returns the absolute version of a path. Like [`std::path::Path::canonicalize`], but doesn't -/// resolve symlinks. -pub fn resolve_path>(path: &P) -> Result { +/// Returns the absolute version of a path. Like +/// [`std::path::Path::canonicalize`], but doesn't resolve symlinks. +pub fn resolve_path(path: impl AsRef) -> Result { let path = path.as_ref(); let base_path; @@ -213,7 +265,7 @@ pub fn resolve_path>(path: &P) -> Result { if cfg!(windows) { use std::path::Prefix; - fn get_drive_letter>(path: P) -> Option { + fn get_drive_letter(path: impl AsRef) -> Option { let path = path.as_ref(); let mut components = path.components(); @@ -293,7 +345,7 @@ pub fn resolve_path>(path: &P) -> Result { for component in components { match component { Component::Normal(_) => stack.push(component), - Component::CurDir => (), + Component::CurDir => {} Component::ParentDir => { if stack.last() != Some(&Component::RootDir) { stack.pop(); @@ -307,7 +359,7 @@ pub fn resolve_path>(path: &P) -> Result { } /// Convert a string to lowercase, with a fast path for ASCII strings. -pub fn to_lowercase>(s: S) -> String { +pub fn to_lowercase(s: impl AsRef) -> String { let s = s.as_ref(); if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() } } diff --git a/templates/fish.txt b/templates/fish.txt index 4a8a5bf..65de458 100644 --- a/templates/fish.txt +++ b/templates/fish.txt @@ -83,7 +83,7 @@ function __zoxide_z end end -# Completions for `z`. +# Completions. function __zoxide_z_complete set -l tokens (commandline --current-process --tokenize) set -l curr_tokens (commandline --cut-at-cursor --current-process --tokenize) @@ -99,6 +99,7 @@ function __zoxide_z_complete commandline --function repaint end end +complete --command __zoxide_z --no-files --arguments '(__zoxide_z_complete)' # Jump to a directory using interactive search. function __zoxide_zi @@ -114,17 +115,10 @@ end {%- when Some with (cmd) %} abbr --erase {{cmd}} &>/dev/null -complete --command {{cmd}} --erase -function {{cmd}} - __zoxide_z $argv -end -complete --command {{cmd}} --no-files --arguments '(__zoxide_z_complete)' +alias {{cmd}}=__zoxide_z abbr --erase {{cmd}}i &>/dev/null -complete --command {{cmd}}i --erase -function {{cmd}}i - __zoxide_zi $argv -end +alias {{cmd}}i=__zoxide_zi {%- when None %} diff --git a/templates/nushell.txt b/templates/nushell.txt index 7576e6c..27310c0 100644 --- a/templates/nushell.txt +++ b/templates/nushell.txt @@ -12,22 +12,25 @@ {%- else -%} # Initialize hook to add new entries to the database. +if (not ($env | default false __zoxide_hooked | get __zoxide_hooked)) { + let-env __zoxide_hooked = true {%- if hook == InitHook::Prompt %} -let-env config = ($env | default {} config).config -let-env config = ($env.config | default {} hooks) -let-env config = ($env.config | update hooks ($env.config.hooks | default [] pre_prompt)) -let-env config = ($env.config | update hooks.pre_prompt ($env.config.hooks.pre_prompt | append { - zoxide add -- $env.PWD -})) + let-env config = ($env | default {} config).config + let-env config = ($env.config | default {} hooks) + let-env config = ($env.config | update hooks ($env.config.hooks | default [] pre_prompt)) + let-env config = ($env.config | update hooks.pre_prompt ($env.config.hooks.pre_prompt | append { + zoxide add -- $env.PWD + })) {%- else if hook == InitHook::Pwd %} -let-env config = ($env | default {} config).config -let-env config = ($env.config | default {} hooks) -let-env config = ($env.config | update hooks ($env.config.hooks | default {} env_change)) -let-env config = ($env.config | update hooks.env_change ($env.config.hooks.env_change | default [] PWD)) -let-env config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.env_change.PWD | append {|_, dir| - zoxide add -- $dir -})) + let-env config = ($env | default {} config).config + let-env config = ($env.config | default {} hooks) + let-env config = ($env.config | update hooks ($env.config.hooks | default {} env_change)) + let-env config = ($env.config | update hooks.env_change ($env.config.hooks.env_change | default [] PWD)) + let-env config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.env_change.PWD | append {|_, dir| + zoxide add -- $dir + })) {%- endif %} +} {%- endif %} @@ -39,7 +42,7 @@ let-env config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.e def-env __zoxide_z [...rest:string] { # `z -` does not work yet, see https://github.com/nushell/nushell/issues/4769 let arg0 = ($rest | append '~').0 - let path = if (($rest | length) <= 1) && ($arg0 == '-' || ($arg0 | path expand | path type) == dir) { + let path = if (($rest | length) <= 1) and ($arg0 == '-' or ($arg0 | path expand | path type) == dir) { $arg0 } else { (zoxide query --exclude $env.PWD -- $rest | str trim -r -c "\n") @@ -77,11 +80,11 @@ alias {{cmd}}i = __zoxide_zi {{ section }} # Add this to your env file (find it by running `$nu.env-path` in Nushell): # -# zoxide init nushell --hook prompt | save ~/.zoxide.nu +# zoxide init nushell | save -f ~/.zoxide.nu # # Now, add this to the end of your config file (find it by running # `$nu.config-path` in Nushell): # # source ~/.zoxide.nu # -# Note: zoxide only supports Nushell v0.63.0 and above. +# Note: zoxide only supports Nushell v0.73.0 and above. diff --git a/templates/zsh.txt b/templates/zsh.txt index c850847..d261a31 100644 --- a/templates/zsh.txt +++ b/templates/zsh.txt @@ -19,7 +19,7 @@ function __zoxide_pwd() { # cd + custom logic based on the value of _ZO_ECHO. function __zoxide_cd() { # shellcheck disable=SC2164 - \builtin cd -- "$@" >/dev/null {%- if echo %} && __zoxide_pwd {%- endif %} + \builtin cd -- "$@" {%- if echo %} && __zoxide_pwd {%- endif %} } {{ section }} @@ -79,28 +79,12 @@ function __zoxide_zi() { result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${result}" } -{{ section }} -# Commands for zoxide. Disable these using --no-cmd. -# - -{%- match cmd %} -{%- when Some with (cmd) %} - -\builtin unalias {{cmd}} &>/dev/null || \builtin true -function {{cmd}}() { - __zoxide_z "$@" -} - -\builtin unalias {{cmd}}i &>/dev/null || \builtin true -function {{cmd}}i() { - __zoxide_zi "$@" -} - +# Completions. if [[ -o zle ]]; then function __zoxide_z_complete() { # Only show completions when the cursor is at the end of the line. # shellcheck disable=SC2154 - [[ "{{ "${#words[@]}" }}" -eq "${CURRENT}" ]] || return + [[ "{{ "${#words[@]}" }}" -eq "${CURRENT}" ]] || return 0 if [[ "{{ "${#words[@]}" }}" -eq 2 ]]; then _files -/ @@ -111,25 +95,26 @@ if [[ -o zle ]]; then result="${__zoxide_z_prefix}${result}" # shellcheck disable=SC2296 compadd -Q "${(q-)result}" - else -{#- - zsh-autocomplete calls the completion function multiple times if no match is - returned. -#} - compadd "" fi \builtin printf '\e[5n' fi + return 0 } - \builtin bindkey "\e[0n" 'reset-prompt' - if [[ "${+functions[compdef]}" -ne 0 ]]; then - \compdef -d {{cmd}} - \compdef -d {{cmd}}i - \compdef __zoxide_z_complete {{cmd}} - fi + \builtin bindkey '\e[0n' 'reset-prompt' + [[ "${+functions[compdef]}" -ne 0 ]] && \compdef __zoxide_z_complete __zoxide_z fi +{{ section }} +# Commands for zoxide. Disable these using --no-cmd. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +\builtin alias {{cmd}}=__zoxide_z +\builtin alias {{cmd}}i=__zoxide_zi + {%- when None %} {{ not_configured }} diff --git a/tests/completions.rs b/tests/completions.rs index 881242c..7fd71e3 100644 --- a/tests/completions.rs +++ b/tests/completions.rs @@ -6,12 +6,12 @@ use assert_cmd::Command; #[test] fn completions_bash() { let source = include_str!("../contrib/completions/zoxide.bash"); - Command::new("bash").args(&["--noprofile", "--norc", "-c", source]).assert().success().stdout("").stderr(""); + Command::new("bash").args(["--noprofile", "--norc", "-c", source]).assert().success().stdout("").stderr(""); } -// Elvish: the completions file uses editor commands to add completions to the shell. However, -// Elvish does not support running editor commands from a script, so we can't create a test for -// this. See: https://github.com/elves/elvish/issues/1299 +// Elvish: the completions file uses editor commands to add completions to the +// shell. However, Elvish does not support running editor commands from a +// script, so we can't create a test for this. See: https://github.com/elves/elvish/issues/1299 #[test] fn completions_fish() { @@ -21,7 +21,7 @@ fn completions_fish() { Command::new("fish") .env("HOME", tempdir) - .args(&["--command", source, "--private"]) + .args(["--command", source, "--private"]) .assert() .success() .stdout("") @@ -32,7 +32,7 @@ fn completions_fish() { fn completions_powershell() { let source = include_str!("../contrib/completions/_zoxide.ps1"); Command::new("pwsh") - .args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source]) + .args(["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source]) .assert() .success() .stdout("") @@ -50,5 +50,5 @@ fn completions_zsh() { compinit -u "#; - Command::new("zsh").args(&["-c", source, "--no-rcs"]).assert().success().stdout("").stderr(""); + Command::new("zsh").args(["-c", source, "--no-rcs"]).assert().success().stdout("").stderr(""); } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index b77af24..263575e 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -anyhow = "1.0.32" -clap = { version = "3.1.0", features = ["derive"] } -ignore = "0.4.18" -shell-words = "1.0.0" +anyhow.workspace = true +clap.workspace = true +ignore.workspace = true +shell-words.workspace = true diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 424f363..f3a9186 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -55,7 +55,7 @@ impl CommandExt for &mut Command { fn run_ci(nix_enabled: bool) -> Result<()> { // Run cargo-clippy. - Command::new("cargo").args(&["clippy", "--all-features", "--all-targets"]).args(&["--", "-Dwarnings"]).run()?; + Command::new("cargo").args(["clippy", "--all-features", "--all-targets"]).args(["--", "-Dwarnings"]).run()?; run_fmt(nix_enabled, true)?; run_lint(nix_enabled)?; run_tests(nix_enabled, "") @@ -63,8 +63,9 @@ fn run_ci(nix_enabled: bool) -> Result<()> { fn run_fmt(nix_enabled: bool, check: bool) -> Result<()> { // Run cargo-fmt. - // let check_args: &[&str] = if check { &["--check", "--files-with-diff"] } else { &[] }; - // Command::new("cargo").args(&["fmt", "--all", "--"]).args(check_args).run()?; + // let check_args: &[&str] = if check {&["--check", "--files-with-diff"] } else + // { &[] }; Command::new("cargo").args(&["fmt", "--all", + // "--"]).args(check_args).run()?; // Run nixfmt. if nix_enabled { @@ -83,9 +84,6 @@ fn run_fmt(nix_enabled: bool, check: bool) -> Result<()> { fn run_lint(nix_enabled: bool) -> Result<()> { if nix_enabled { - // Run cargo-audit. - Command::new("cargo").args(&["audit", "--deny=warnings"]).run()?; - // Run markdownlint. for result in Walk::new("./") { let entry = result.unwrap(); @@ -100,7 +98,7 @@ fn run_lint(nix_enabled: bool) -> Result<()> { let entry = result.unwrap(); let path = entry.path(); if path.is_file() && path.extension() == Some(OsStr::new("1")) { - Command::new("mandoc").args(&["-man", "-Wall", "-Tlint", "--"]).arg(path).run()?; + Command::new("mandoc").args(["-man", "-Wall", "-Tlint", "--"]).arg(path).run()?; } } } @@ -110,7 +108,7 @@ fn run_lint(nix_enabled: bool) -> Result<()> { fn run_tests(nix_enabled: bool, name: &str) -> Result<()> { let args: &[&str] = if nix_enabled { &["nextest", "run", "--all-features"] } else { &["test"] }; - Command::new("cargo").args(args).args(&["--no-fail-fast", "--workspace", "--", name]).run() + Command::new("cargo").args(args).args(["--no-fail-fast", "--workspace", "--", name]).run() } fn enable_nix() -> bool { @@ -131,6 +129,6 @@ fn enable_nix() -> bool { let args = env::args(); let cmd = shell_words::join(args); - let status = Command::new("nix-shell").args(&["--pure", "--run", &cmd, "--", "shell.nix"]).status().unwrap(); + let status = Command::new("nix-shell").args(["--pure", "--run", &cmd, "--", "shell.nix"]).status().unwrap(); process::exit(status.code().unwrap_or(1)); }