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/.cargo/config.toml b/.cargo/config.toml index 35049cb..4b40e31 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,7 @@ [alias] xtask = "run --package xtask --" + +# On Windows MSVC, statically link the C runtime so that the resulting EXE does +# not depend on the vcruntime DLL. +[target.'cfg(all(windows, target_env = "msvc"))'] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.deepsource.toml b/.deepsource.toml index cf6e317..2895393 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -2,4 +2,9 @@ version = 1 [[analyzers]] name = "rust" -enabled = true + + [analyzers.meta] + msrv = "stable" + +[[analyzers]] +name = "shell" \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index a2a6a67..609eb60 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8500f7c..78290d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,31 +4,51 @@ on: branches: [main] pull_request: workflow_dispatch: +env: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always +permissions: + contents: read jobs: ci: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions-rs/toolchain@v1 if: ${{ matrix.os == 'windows-latest' }} with: - toolchain: stable - components: rustfmt, clippy + components: clippy profile: minimal - override: true - - uses: cachix/install-nix-action@v15 + toolchain: stable + - uses: actions-rs/toolchain@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + components: rustfmt + profile: minimal + toolchain: nightly + - uses: cachix/install-nix-action@v23 if: ${{ matrix.os != 'windows-latest' }} with: - nix_path: nixpkgs=channel:nixpkgs-unstable - - - run: cargo xtask ci - if: ${{ matrix.os == 'windows-latest' }} - - run: nix-shell --cores 0 --pure --run 'rm -rf ~/.cargo/bin; cargo xtask ci' - if: ${{ matrix.os != 'windows-latest' }} + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v12 + if: ${{ matrix.os != 'windows-latest' && env.CACHIX_AUTH_TOKEN != '' }} + with: + authToken: ${{ env.CACHIX_AUTH_TOKEN }} + name: zoxide + - name: Setup cache + uses: Swatinem/rust-cache@v2.7.0 + with: + key: ${{ matrix.os }} + - name: Install just + uses: taiki-e/install-action@v2 + with: + tool: just + - name: Run lints + tests + run: just lint test diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000..dd40342 --- /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 +jobs: + no-response: + if: github.repository == 'ajeetdsouza/zoxide' + permissions: + issues: write + 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 2947bfc..73b9ea1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,11 @@ name: release on: push: - branches: [main] - tags: ["v[0-9]+.[0-9]+.[0-9]+"] - pull_request: workflow_dispatch: - +env: + CARGO_INCREMENTAL: 0 +permissions: + contents: write jobs: release: name: ${{ matrix.target }} @@ -16,38 +16,33 @@ jobs: include: - os: ubuntu-latest target: x86_64-unknown-linux-musl + deb: true - os: ubuntu-latest target: arm-unknown-linux-musleabihf - os: ubuntu-latest target: armv7-unknown-linux-musleabihf - os: ubuntu-latest target: aarch64-unknown-linux-musl - + deb: true - os: macos-11 target: x86_64-apple-darwin - os: macos-11 target: aarch64-apple-darwin - - os: windows-latest target: x86_64-pc-windows-msvc - os: windows-latest target: aarch64-pc-windows-msvc steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Set artifact name - shell: bash - run: | - version="$(git describe --tags --match='v*.*.*' --always)" - name="zoxide-$version-${{ matrix.target }}" - echo "ARTIFACT_NAME=$name" >> $GITHUB_ENV - - echo "version: $version" - echo "artifact: $name" - + - name: Get version + id: get_version + uses: SebRollen/toml-action@v1.0.2 + with: + file: Cargo.toml + field: package.version - name: Install Rust uses: actions-rs/toolchain@v1 with: @@ -55,45 +50,58 @@ jobs: profile: minimal override: true target: ${{ matrix.target }} - + - name: Setup cache + uses: Swatinem/rust-cache@v2.7.0 + with: + key: ${{ matrix.target }} - name: Build binary uses: actions-rs/cargo@v1 with: command: build args: --release --locked --target=${{ matrix.target }} --color=always --verbose use-cross: ${{ runner.os == 'Linux' }} - + - name: Install cargo-deb + if: ${{ matrix.deb == true }} + uses: actions-rs/install@v0.1 + with: + crate: cargo-deb + - name: Build deb + if: ${{ matrix.deb == true }} + uses: actions-rs/cargo@v1 + with: + command: deb + args: --no-build --no-strip --output=. --target=${{ matrix.target }} - name: Package (*nix) if: runner.os != 'Windows' - run: > - tar -cv - CHANGELOG.md LICENSE README.md - man/ - -C contrib/ completions/ -C ../ - -C target/${{ matrix.target }}/release/ zoxide - | gzip --best > '${{ env.ARTIFACT_NAME }}.tar.gz' + run: | + tar -cv CHANGELOG.md LICENSE README.md man/ \ + -C contrib/ completions/ -C ../ \ + -C target/${{ matrix.target }}/release/ zoxide | + gzip --best > \ + zoxide-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.tar.gz - name: Package (Windows) if: runner.os == 'Windows' - run: > - 7z a ${{ env.ARTIFACT_NAME }}.zip - CHANGELOG.md LICENSE README.md - ./man/ - ./contrib/completions/ - ./target/${{ matrix.target }}/release/zoxide.exe - + run: | + 7z a zoxide-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.zip ` + CHANGELOG.md LICENSE README.md ./man/ ./contrib/completions/ ` + ./target/${{ matrix.target }}/release/zoxide.exe - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.target }} path: | - *.zip + *.deb *.tar.gz - + *.zip - name: Create release - if: startsWith(github.ref, 'refs/tags/v') + if: | + github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'chore(release)') uses: softprops/action-gh-release@v1 with: draft: true files: | - *.zip + *.deb *.tar.gz + *.zip + name: ${{ steps.get_version.outputs.value }} + tag_name: "" diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 0000000..307ebe2 --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,13 @@ +name: winget +on: + release: + types: [released] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: ajeetdsouza.zoxide + installers-regex: '-pc-windows-msvc\.zip$' + token: ${{ secrets.WINGET_TOKEN }} diff --git a/.gitignore b/.gitignore index 89b1201..887d2ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Compiled files and executables debug/ target/ +target_nix/ # Backup files generated by rustfmt **/*.rs.bk diff --git a/CHANGELOG.md b/CHANGELOG.md index 097eb03..93cf442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,22 +7,101 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [0.9.2] - 2023-08-04 + +### Added + +- Short option `-a` for `zoxide query --all`. + +### Fixed + +- PowerShell: use `global` scope for variables / functions. + +## [0.9.1] - 2023-05-07 + +### Added + +- Fish/Zsh: aliases on `__zoxide_z` will now use completions. +- Nushell: add support for v0.78.0. +- Fish: plugin now works on older versions. +- PowerShell: warn when PowerShell version is too old for `z -` and `z +`. +- PowerShell: support for PWD hooks on all versions. + +### Fixed + +- Fish: not providing `cd` completions when there is a space in the path. +- Bash/Fish/Zsh: providing `z` completions when the last argument starts with `z!`. +- Bash/Fish/Zsh: attempting to `cd` when the last argument is `z!`. + +## [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, upgrade minimum supported version to v0.33.0. +- Nushell: accidental redefinition of hooks when initialized twice. + +### Removed + +- `remove -i` subcommand: use `edit` instead. + +## [0.8.3] - 2022-09-02 + +### Added + +- Nushell: support for `z -`. +- Nushell: support for PWD hooks. + +### Changed + +- Fish: change fuzzy completion prefix to `z!`. +- Zsh: allow `z` to navigate dirstack via `+n` and `-n`. +- Fzf: improved preview window. + +### Fixed + +- Bash: double forward slash in completions. + +## [0.8.2] - 2022-06-26 + +### Changed + +- Fzf: show preview window below results. + +### Fixed + +- Bash/Fish/POSIX/Zsh: paths on Cygwin. +- Fish: completions not working on certain systems. +- Bash: completions not escaping spaces correctly. + +## [0.8.1] - 2021-04-23 ### Changed - Manpages: moved to `man/man1/*.1`. +- Replace `--no-aliases` with `--no-cmd`. +- Elvish: upgrade minimum supported version to v0.18.0. +- Nushell: upgrade minimum supported version to v0.61.0. ### Fixed - Bash/Zsh: rename `_z` completion function to avoid conflicts with other shell plugins. -- Elvish: upgrade to new lambda syntax. -- Fzf: added `--keep-right` option by default, upgraded minimum version to - v0.21.0. +- Fzf: added `--keep-right` option by default, upgrade minimum supported version + to v0.21.0. - Bash: only enable completions on 4.4+. - Fzf: bypass `ls` alias in preview window. - Retain ownership of database file. +- `zoxide query --interactive` should not conflict with `--score`. ## [0.8.0] - 2021-12-25 @@ -73,7 +152,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- PowerShell: Hook not initializing correctly. +- PowerShell: hook not initializing correctly. ## [0.7.6] - 2021-10-13 @@ -371,6 +450,12 @@ 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.2]: https://github.com/ajeetdsouza/zoxide/compare/v0.9.1...v0.9.2 +[0.9.1]: https://github.com/ajeetdsouza/zoxide/compare/v0.9.0...v0.9.1 +[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 [0.8.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.9...v0.8.0 [0.7.9]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.8...v0.7.9 [0.7.8]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.7...v0.7.8 diff --git a/Cargo.lock b/Cargo.lock index b125936..975dba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,39 +4,96 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] [[package]] -name = "anyhow" -version = "1.0.56" +name = "aliasable" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "askama" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139" +checksum = "47cbc3cf73fa8d9833727bbee4835ba5c421a0d65b72daf9a7b5d0e0f9cfb57e" dependencies = [ "askama_derive", "askama_escape", - "askama_shared", ] [[package]] name = "askama_derive" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71" +checksum = "c22fbe0413545c098358e56966ff22cdd039e10215ae213cfbd65032b119fc94" dependencies = [ - "askama_shared", + "mime", + "mime_guess", + "nom", "proc-macro2", - "syn", + "quote", + "syn 2.0.28", ] [[package]] @@ -45,27 +102,13 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" -[[package]] -name = "askama_shared" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0" -dependencies = [ - "askama_escape", - "mime", - "mime_guess", - "nom", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "assert_cmd" -version = "2.0.4" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" +checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" dependencies = [ + "anstyle", "bstr", "doc-comment", "predicates", @@ -74,23 +117,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" @@ -107,21 +133,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "bstr" -version = "0.2.17" +name = "bitflags" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ - "lazy_static", "memchr", "regex-automata", + "serde", ] [[package]] name = "cc" -version = "1.0.73" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -131,35 +166,41 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.1.6" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ - "atty", - "bitflags", + "clap_builder", "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", "strsim", - "termcolor", - "textwrap", ] [[package]] name = "clap_complete" -version = "3.1.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df6f3613c0a3cddfd78b41b10203eb322cb29b600cbdf808a7d3db95691b8e25" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" dependencies = [ "clap", ] [[package]] name = "clap_complete_fig" -version = "3.1.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c11f6f44afea4aee21bb57a5297879c88ac8dc97224fbbbe796edd60a098f0e" +checksum = "99fee1d30a51305a6c2ed3fc5709be3c8af626c9c958e04dd9ae94e27bcbce9f" dependencies = [ "clap", "clap_complete", @@ -167,27 +208,49 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.4" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] -name = "crossbeam-utils" -version = "0.8.7" +name = "clap_lex" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "color-print" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a5e6504ed8648554968650feecea00557a3476bc040d0ffc33080e66b646d0" dependencies = [ - "cfg-if", - "lazy_static", + "color-print-proc-macro", ] +[[package]] +name = "color-print-proc-macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51beaa537d73d2d1ff34ee70bc095f170420ab2ec5d687ecd3ec2b0d092514b" +dependencies = [ + "nom", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "difflib" version = "0.4.0" @@ -196,22 +259,23 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirs" -version = "4.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.6" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys", ] [[package]] @@ -222,36 +286,48 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dunce" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" -version = "1.6.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] -name = "fastrand" -version = "1.7.0" +name = "errno" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ - "instant", + "errno-dragonfly", + "libc", + "windows-sys", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "errno-dragonfly" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "getrandom" -version = "0.2.5" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -260,131 +336,65 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "globset" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] -name = "ignore" -version = "0.4.18" +name = "is-terminal" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "crossbeam-utils", - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", + "hermit-abi", + "rustix", + "windows-sys", ] [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.119" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] -name = "log" -version = "0.4.14" +name = "linux-raw-sys" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -404,67 +414,75 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" -version = "0.23.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", - "cc", + "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "static_assertions", ] [[package]] name = "nom" -version = "7.1.0" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", - "version_check", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", ] [[package]] name = "once_cell" -version = "1.10.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] -name = "ordered-float" -version = "2.10.0" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" dependencies = [ - "num-traits", + "aliasable", + "ouroboros_macro", + "static_assertions", ] [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "ouroboros_macro" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" dependencies = [ - "memchr", + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" -version = "2.1.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" dependencies = [ + "anstyle", "difflib", "itertools", "predicates-core", @@ -472,15 +490,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", @@ -495,7 +513,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -512,95 +530,153 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] [[package]] -name = "redox_syscall" -version = "0.2.11" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "bitflags", + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", + "thiserror", ] [[package]] name = "regex" -version = "1.5.5" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "relative-path" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698" [[package]] name = "rstest" -version = "0.12.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" +checksum = "2b96577ca10cb3eade7b337eb46520108a67ca2818a24d0b63f41fd62bc9651c" +dependencies = [ + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e674cf31712b8bb15fdbca3ec0c1b9d825c5a24407ff2b7e005fb6a29ba03" dependencies = [ "cfg-if", + "glob", "proc-macro2", "quote", + "regex", + "relative-path", "rustc_version", - "syn", + "syn 2.0.28", + "unicode-ident", ] [[package]] name = "rstest_reuse" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b29d3117bce27ea307d1fb7ce12c64ba11b3fd04311a42d32bc5f0072e6e3d4d" +checksum = "88530b681abe67924d42cca181d070e3ac20e0740569441a9e35a7cedd2b34a4" dependencies = [ "quote", + "rand", "rustc_version", - "syn", + "syn 2.0.28", ] [[package]] @@ -613,45 +689,49 @@ dependencies = [ ] [[package]] -name = "same-file" -version = "1.0.6" +name = "rustix" +version = "0.38.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" dependencies = [ - "winapi-util", + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] name = "semver" -version = "1.0.6" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.28", ] [[package]] -name = "shell-words" +name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" @@ -661,57 +741,63 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", + "redox_syscall 0.3.5", + "rustix", + "windows-sys", ] [[package]] name = "termtree" -version = "0.2.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] -name = "textwrap" -version = "0.15.0" +name = "thiserror" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - -[[package]] -name = "thread_local" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ - "once_cell", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", ] [[package]] @@ -724,10 +810,16 @@ dependencies = [ ] [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "version_check" @@ -744,67 +836,92 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "winapi" -version = "0.3.9" +name = "which" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "either", + "libc", + "once_cell", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "winapi", + "windows-targets", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-targets" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +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 = "xtask" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "ignore", - "shell-words", -] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "zoxide" -version = "0.8.0" +version = "0.9.2" dependencies = [ "anyhow", "askama", @@ -813,14 +930,16 @@ dependencies = [ "clap", "clap_complete", "clap_complete_fig", + "color-print", "dirs", "dunce", "fastrand", "glob", "nix", - "ordered-float", + "ouroboros", "rstest", "rstest_reuse", "serde", "tempfile", + "which", ] diff --git a/Cargo.toml b/Cargo.toml index 27d729f..b65ac87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,43 +3,50 @@ authors = ["Ajeet D'Souza <98ajeet@gmail.com>"] categories = ["command-line-utilities", "filesystem"] description = "A smarter cd command for your terminal" edition = "2021" -keywords = ["cli"] +homepage = "https://github.com/ajeetdsouza/zoxide" +keywords = ["cli", "filesystem", "shell", "tool", "utility"] license = "MIT" name = "zoxide" +readme = "README.md" repository = "https://github.com/ajeetdsouza/zoxide" -rust-version = "1.59" -version = "0.8.0" +rust-version = "1.65" +version = "0.9.2" [badges] maintenance = { status = "actively-developed" } -[workspace] -members = ["xtask/"] - [dependencies] anyhow = "1.0.32" -askama = { version = "0.11.0", default-features = false } +askama = { version = "0.12.0", default-features = false } bincode = "1.3.1" -clap = { version = "3.1.0", features = ["derive"] } -dirs = "4.0.0" +clap = { version = "4.3.0", features = ["derive"] } +color-print = "0.3.4" +dirs = "5.0.0" dunce = "1.0.1" -fastrand = "1.7.0" +fastrand = "2.0.0" glob = "0.3.0" -ordered-float = "2.0.0" +ouroboros = "0.17.2" serde = { version = "1.0.116", features = ["derive"] } [target.'cfg(unix)'.dependencies] -nix = "0.23.1" +nix = { version = "0.26.1", default-features = false, features = [ + "fs", + "user", +] } + +[target.'cfg(windows)'.dependencies] +which = "4.2.5" [build-dependencies] -clap = { version = "3.1.0", features = ["derive"] } -clap_complete = "3.1.0" -clap_complete_fig = "3.1.0" +clap = { version = "4.3.0", features = ["derive"] } +clap_complete = "4.3.0" +clap_complete_fig = "4.3.0" +color-print = "0.3.4" [dev-dependencies] assert_cmd = "2.0.0" -rstest = "0.12.0" -rstest_reuse = "0.3.0" +rstest = { version = "0.18.0", default-features = false } +rstest_reuse = "0.6.0" tempfile = "3.1.0" [features] @@ -48,5 +55,56 @@ nix-dev = [] [profile.release] codegen-units = 1 +debug = 0 lto = true strip = true + +[package.metadata.deb] +assets = [ + [ + "target/release/zoxide", + "usr/bin/", + "755", + ], + [ + "contrib/completions/zoxide.bash", + "usr/share/bash-completion/completions/zoxide", + "644", + ], + [ + "contrib/completions/zoxide.fish", + "usr/share/fish/vendor_completions.d/", + "664", + ], + [ + "contrib/completions/_zoxide", + "usr/share/zsh/vendor-completions/", + "644", + ], + [ + "man/man1/*", + "usr/share/man/man1/", + "644", + ], + [ + "README.md", + "usr/share/doc/zoxide/", + "644", + ], + [ + "CHANGELOG.md", + "usr/share/doc/zoxide/", + "644", + ], + [ + "LICENSE", + "usr/share/doc/zoxide/", + "644", + ], +] +extended-description = """\ +zoxide is a smarter cd command, inspired by z and autojump. It remembers which \ +directories you use most frequently, so you can "jump" to them in just a few \ +keystrokes.""" +priority = "optional" +section = "utils" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..4e38b0f --- /dev/null +++ b/Cross.toml @@ -0,0 +1,2 @@ +[build.env] +passthrough = ["CARGO_INCREMENTAL"] diff --git a/README.md b/README.md index 94e67e4..8600ef1 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,20 @@
+Special thanks to: + + +
+ Warp +
+ Warp is a modern, Rust-based terminal with AI built in so you and your team can build great software, faster. +
+ Visit warp.dev to learn more. +
+
+ +
+ # zoxide [![crates.io][crates.io-badge]][crates.io] @@ -51,240 +65,300 @@ Read more about the matching algorithm [here][algorithm-matching]. ## Installation -### *Step 1: Install zoxide* - -zoxide runs on most major platforms. If your platform isn't listed below, -please [open an issue][issues]. - -
-Linux - -To install zoxide, run this command in your terminal: - -```sh -curl -sS https://webinstall.dev/zoxide | bash -``` - -Alternatively, you can use a package manager: - -| Distribution | Repository | Instructions | -| ------------------ | ----------------------- | ---------------------------------------------------------------------------------------------- | -| ***Any*** | **[crates.io]** | `cargo install zoxide --locked` | -| *Any* | [conda-forge] | `conda install -c conda-forge zoxide` | -| *Any* | [Linuxbrew] | `brew install zoxide` | -| Alpine Linux 3.13+ | [Alpine Linux Packages] | `apk add zoxide` | -| Arch Linux | [Arch Linux Community] | `pacman -S zoxide` | -| CentOS 7+ | [Copr] | `dnf copr enable atim/zoxide`
`dnf install zoxide` | -| Debian 11+ | [Debian Packages] | `apt install zoxide` | -| Devuan 4.0+ | [Devuan Packages] | `apt install zoxide` | -| 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` | -| Parrot OS | | `apt install zoxide` | -| Raspbian 11+ | [Raspbian Packages] | `apt install zoxide` | -| Ubuntu 21.04+ | [Ubuntu Packages] | `apt install zoxide` | -| Void Linux | [Void Linux Packages] | `xbps-install -S zoxide` | - -
- -
-macOS - -To install zoxide, use a package manager: - -| Repository | Instructions | -| --------------- | ------------------------------------- | -| **[crates.io]** | `cargo install zoxide --locked` | -| [conda-forge] | `conda install -c conda-forge zoxide` | -| [Homebrew] | `brew install zoxide` | -| [MacPorts] | `port install zoxide` | - -
- -
-Windows - -To install zoxide, run this command in your command prompt: - -```sh -curl.exe -A "MS" https://webinstall.dev/zoxide | powershell -``` - -Alternatively, you can use a package manager: - -| Repository | Instructions | -| --------------- | ------------------------------------- | -| **[crates.io]** | `cargo install zoxide --locked` | -| [Chocolatey] | `choco install zoxide` | -| [conda-forge] | `conda install -c conda-forge zoxide` | -| [Scoop] | `scoop install zoxide` | - -
- -
-BSD - -To install zoxide, use a package manager: - -| Distribution | Repository | Instructions | -| ------------- | --------------- | ------------------------------- | -| ***Any*** | **[crates.io]** | `cargo install zoxide --locked` | -| DragonFly BSD | [DPorts] | `pkg install zoxide` | -| FreeBSD | [FreshPorts] | `pkg install zoxide` | -| NetBSD | [pkgsrc] | `pkgin install zoxide` | - -
- -
-Android - -To install zoxide, use a package manager: - -| Repository | Instructions | -| ---------- | -------------------- | -| [Termux] | `pkg install zoxide` | - -
- -### *Step 2: Add zoxide to your shell* - -To start using zoxide, add it to your shell. - -
-Bash - -Add this to your configuration (usually `~/.bashrc`): - -```sh -eval "$(zoxide init bash)" -``` - -
- -
-Elvish - -Add this to your configuration (usually `~/.elvish/rc.elv`): - -```sh -eval (zoxide init elvish | slurp) -``` - -Note: zoxide only supports elvish v0.16.0 and above. - -
- -
-Fish - -Add this to your configuration (usually `~/.config/fish/config.fish`): - -```fish -zoxide init fish | source -``` - -
- -
-Nushell - -Add this to your configuration (find it by running `config path` in Nushell): - -```toml -startup = ["zoxide init nushell --hook prompt | save ~/.zoxide.nu", "source ~/.zoxide.nu"] -``` - -Note: zoxide only supports Nushell v0.37.0 and above. - -
- -
-PowerShell - -Add this to your configuration (find it by running `echo $profile` in -PowerShell): - -```powershell -# For zoxide v0.8.0+ -Invoke-Expression (& { - $hook = if ($PSVersionTable.PSVersion.Major -lt 6) { 'prompt' } else { 'pwd' } - (zoxide init --hook $hook powershell | Out-String) -}) - -# For older versions of zoxide -Invoke-Expression (& { - $hook = if ($PSVersionTable.PSVersion.Major -lt 6) { 'prompt' } else { 'pwd' } - (zoxide init --hook $hook powershell) -join "`n" -}) -``` - -
- -
-Xonsh - -Add this to your configuration (usually `~/.xonshrc`): - -```python -execx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide') -``` - -
- -
-Zsh - -Add this to your configuration (usually `~/.zshrc`): - -```sh -eval "$(zoxide init zsh)" -``` - -For completions to work, the above line must be added _after_ `compinit` is -called. You may have to rebuild your cache by running `rm ~/.zcompdump*; compinit`. - -
- -
-Any POSIX shell - -Add this to your configuration: - -```sh -eval "$(zoxide init posix --hook prompt)" -``` - -
- -### *Step 3: Install fzf (optional)* - -[fzf] is a command-line fuzzy finder, used by zoxide for interactive -selection. It can be installed from [here][fzf-installation]. zoxide supports -fzf v0.21.0+. - -### *Step 4: Import your data (optional)* - -If you currently use any of the following utilities, you may want to import -your data into zoxide: - -
-autojump - -```sh -zoxide import --from autojump path/to/db -``` - -
- -
-z, z.lua, or zsh-z - -```sh -zoxide import --from z path/to/db -``` - -
+zoxide can be installed in 4 easy steps: + +1. **Install binary** + + zoxide runs on most major platforms. If your platform isn't listed below, + please [open an issue][issues]. + +
+ Linux + + > The recommended way to install zoxide is via the install script: + > + > ```sh + > curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash + > ``` + > + > Or, you can use a package manager: + > + > | Distribution | Repository | Instructions | + > | ------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- | + > | **_Any_** | **[crates.io]** | `cargo install zoxide --locked` | + > | _Any_ | [asdf] | `asdf plugin add zoxide https://github.com/nyrst/asdf-zoxide.git`
`asdf install zoxide latest` | + > | _Any_ | [conda-forge] | `conda install -c conda-forge zoxide` | + > | _Any_ | [Linuxbrew] | `brew install zoxide` | + > | _Any_ | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` | + > | Alpine Linux 3.13+ | [Alpine Linux Packages] | `apk add zoxide` | + > | Arch Linux | [Arch Linux Extra] | `pacman -S zoxide` | + > | CentOS 7+ | [Copr] | `dnf copr enable atim/zoxide`
`dnf install zoxide` | + > | Debian 11+[^1] | [Debian Packages] | `apt install zoxide` | + > | Devuan 4.0+[^1] | [Devuan Packages] | `apt install zoxide` | + > | 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` | + > | openSUSE Tumbleweed | [openSUSE Factory] | `zypper install zoxide` | + > | Parrot OS[^1] | | `apt install zoxide` | + > | Raspbian 11+[^1] | [Raspbian Packages] | `apt install zoxide` | + > | Rhino Linux | [Pacstall Packages] | `pacstall -I zoxide-deb` | + > | Slackware 15.0+ | [SlackBuilds] | [Instructions][slackbuilds-howto] | + > | Ubuntu 21.04+[^1] | [Ubuntu Packages] | `apt install zoxide` | + > | Void Linux | [Void Linux Packages] | `xbps-install -S zoxide` | + +
+ +
+ macOS + + > To install zoxide, use a package manager: + > + > | Repository | Instructions | + > | --------------- | ----------------------------------------------------------------------------------------------------- | + > | **[crates.io]** | `cargo install zoxide --locked` | + > | **[Homebrew]** | `brew install zoxide` | + > | [asdf] | `asdf plugin add zoxide https://github.com/nyrst/asdf-zoxide.git`
`asdf install zoxide latest` | + > | [conda-forge] | `conda install -c conda-forge zoxide` | + > | [MacPorts] | `port install zoxide` | + > | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` | + > + > Or, run this command in your terminal: + > + > ```sh + > curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash + > ``` + +
+ +
+ Windows + + > The recommended way to install zoxide is via `winget`: + > + > ```sh + > winget install ajeetdsouza.zoxide + > ``` + > + > Or, you can use an alternative package manager: + > + > | Repository | Instructions | + > | --------------- | ------------------------------------- | + > | **[crates.io]** | `cargo install zoxide --locked` | + > | [Chocolatey] | `choco install zoxide` | + > | [conda-forge] | `conda install -c conda-forge zoxide` | + > | [Scoop] | `scoop install zoxide` | + > + > If you're using Cygwin, Git Bash, or MSYS2, use the install script instead: + > + > ```sh + > curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash + > ``` + +
+ +
+ BSD + + > To install zoxide, use a package manager: + > + > | Distribution | Repository | Instructions | + > | ------------- | --------------- | ------------------------------- | + > | **_Any_** | **[crates.io]** | `cargo install zoxide --locked` | + > | DragonFly BSD | [DPorts] | `pkg install zoxide` | + > | FreeBSD | [FreshPorts] | `pkg install zoxide` | + > | NetBSD | [pkgsrc] | `pkgin install zoxide` | + +
+ +
+ Android + + > To install zoxide, use a package manager: + > + > | Repository | Instructions | + > | ---------- | -------------------- | + > | [Termux] | `pkg install zoxide` | + +
+ +2. **Setup zoxide on your shell** + + To start using zoxide, add it to your shell. + +
+ Bash + + > Add this to the **end** of your config file (usually `~/.bashrc`): + > + > ```sh + > eval "$(zoxide init bash)" + > ``` + +
+ +
+ Elvish + + > Add this to the **end** of your config file (usually `~/.elvish/rc.elv`): + > + > ```sh + > eval (zoxide init elvish | slurp) + > ``` + > + > **Note** + > zoxide only supports elvish v0.18.0 and above. + +
+ +
+ Fish + + > Add this to the **end** of your config file (usually + > `~/.config/fish/config.fish`): + > + > ```fish + > zoxide init fish | source + > ``` + +
+ +
+ Nushell + + > Add this to the **end** of your env file (find it by running `$nu.env-path` + > in Nushell): + > + > ```sh + > 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): + > + > ```sh + > source ~/.zoxide.nu + > ``` + > + > **Note** + > zoxide only supports Nushell v0.73.0 and above. + +
+ +
+ PowerShell + + > Add this to the **end** of your config file (find it by running + > `echo $profile` in PowerShell): + > + > ```powershell + > Invoke-Expression (& { (zoxide init powershell | Out-String) }) + > ``` + +
+ +
+ Xonsh + + > Add this to the **end** of your config file (usually `~/.xonshrc`): + > + > ```python + > execx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide') + > ``` + +
+ +
+ Zsh + + > Add this to the **end** of your config file (usually `~/.zshrc`): + > + > ```sh + > eval "$(zoxide init zsh)" + > ``` + > + > For completions to work, the above line must be added _after_ `compinit` is + > called. You may have to rebuild your completions cache by running + > `rm ~/.zcompdump*; compinit`. + +
+ +
+ Any POSIX shell + + > Add this to the **end** of your config file: + > + > ```sh + > eval "$(zoxide init posix --hook prompt)" + > ``` + +
+ +3. **Install fzf** (optional) + + [fzf] is a command-line fuzzy finder, used by zoxide for completions / + interactive selection. It can be installed from [here][fzf-installation]. + + > **Note** + > zoxide only supports fzf v0.33.0 and above. + +4. **Import your data** (optional) + + If you currently use any of these plugins, you may want to import your data + into zoxide: + +
+ autojump + + > Run this command in your terminal: + > + > ```sh + > zoxide import --from=autojump "/path/to/autojump/db" + > ``` + > + > The path usually varies according to your system: + > + > | OS | Path | Example | + > | ------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------ | + > | Linux | `$XDG_DATA_HOME/autojump/autojump.txt` or `$HOME/.local/share/autojump/autojump.txt` | `/home/alice/.local/share/autojump/autojump.txt` | + > | macOS | `$HOME/Library/autojump/autojump.txt` | `/Users/Alice/Library/autojump/autojump.txt` | + > | Windows | `%APPDATA%\autojump\autojump.txt` | `C:\Users\Alice\AppData\Roaming\autojump\autojump.txt` | + +
+ +
+ fasd, z, z.lua, zsh-z + + > Run this command in your terminal: + > + > ```sh + > zoxide import --from=z "path/to/z/db" + > ``` + > + > The path usually varies according to your system: + > + > | Plugin | Path | + > | ---------------- | ----------------------------------------------------------------------------------- | + > | fasd | `$_FASD_DATA` or `$HOME/.fasd` | + > | z (bash/zsh) | `$_Z_DATA` or `$HOME/.z` | + > | z (fish) | `$Z_DATA` or `$XDG_DATA_HOME/z/data` or `$HOME/.local/share/z/data` | + > | z.lua (bash/zsh) | `$_ZL_DATA` or `$HOME/.zlua` | + > | z.lua (fish) | `$XDG_DATA_HOME/zlua/zlua.txt` or `$HOME/.local/share/zlua/zlua.txt` or `$_ZL_DATA` | + > | zsh-z | `$ZSHZ_DATA` or `$_Z_DATA` or `$HOME/.z` | + +
+ +
+ ZLocation + + > Run this command in PowerShell: + > + > ```powershell + > $db = New-TemporaryFile + > (Get-ZLocation).GetEnumerator() | ForEach-Object { Write-Output ($_.Name+'|'+$_.Value+'|0') } | Out-File $db + > zoxide import --from=z $db + > ``` + +
## Configuration @@ -293,8 +367,8 @@ zoxide import --from z path/to/db When calling `zoxide init`, the following flags are available: - `--cmd` - - Changes the prefix of predefined aliases (`z`, `zi`). - - `--cmd j` would change the aliases to (`j`, `ji`). + - Changes the prefix of the `z` and `zi` commands. + - `--cmd j` would change the commands to (`j`, `ji`). - `--cmd cd` would replace the `cd` command (doesn't work on Nushell / POSIX shells). - `--hook ` - Changes how often zoxide increments a directory's score: @@ -303,15 +377,15 @@ When calling `zoxide init`, the following flags are available: | `none` | Never | | `prompt` | At every shell prompt | | `pwd` | Whenever the directory is changed | -- `--no-aliases` - - Don't define aliases (`z`, `zi`). +- `--no-cmd` + - Prevents zoxide from defining the `z` and `zi` commands. - These functions will still be available in your shell as `__zoxide_z` and `__zoxide_zi`, should you choose to redefine them. ### Environment variables -Environment variables[?][wiki-env] can be used for configuration. -They must be set before `zoxide init` is called. +Environment variables[^2] can be used for configuration. They must be set before +`zoxide init` is called. - `_ZO_DATA_DIR` - Specifies the directory in which the database is stored. @@ -346,38 +420,55 @@ They must be set before `zoxide init` is called. ## Third-party integrations -| Application | Description | Plugin | -| ------------------ | -------------------------------------------- | -------------------------- | -| [clink] | Improved cmd.exe for Windows | [clink-zoxide] | -| [emacs] | Text editor | [zoxide.el] | -| [nnn] | File manager | [nnn-autojump] | -| [ranger] | File manager | [ranger-zoxide] | -| [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] | -| [vim] | 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 | -| [zsh-autocomplete] | Realtime completions for zsh | Natively supported | +| Application | Description | Plugin | +| --------------------- | -------------------------------------------- | -------------------------- | +| [aerc] | Email client | Natively supported | +| [clink] | Improved cmd.exe for Windows | [clink-zoxide] | +| [emacs] | Text editor | [zoxide.el] | +| [felix] | File manager | Natively supported | +| [joshuto] | File manager | Natively supported | +| [lf] | File manager | See the [wiki][lf-wiki] | +| [nnn] | File manager | [nnn-autojump] | +| [ranger] | File manager | [ranger-zoxide] | +| [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] | +| [t] | `tmux` session manager | Natively supported | +| [tmux-session-wizard] | `tmux` session manager | Natively supported | +| [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 | +| [zsh-autocomplete] | Realtime completions for zsh | Natively supported | +[^1]: + Debian / Ubuntu derivatives update their packages very slowly. If you're + using one of these distributions, consider using the install script instead. + +[^2]: + If you're not sure how to set an environment variable on your shell, check + out the [wiki][wiki-env]. + +[aerc]: https://github.com/rjarry/aerc [algorithm-aging]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#aging [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 +[arch linux extra]: https://archlinux.org/packages/extra/x86_64/zoxide/ +[asdf]: https://github.com/asdf-vm/asdf +[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/github/downloads/ajeetdsouza/zoxide/total?style=flat-square +[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 +[felix]: https://github.com/kyoheiu/felix [freshports]: https://www.freshports.org/sysutils/zoxide/ [fzf-installation]: https://github.com/junegunn/fzf#installation [fzf-man]: https://manpages.ubuntu.com/manpages/en/man1/fzf.1.html @@ -386,23 +477,31 @@ They must be set before `zoxide init` is called. [guru overlay]: https://github.com/gentoo-mirror/guru [homebrew]: https://formulae.brew.sh/formula/zoxide [issues]: https://github.com/ajeetdsouza/zoxide/issues/new +[joshuto]: https://github.com/kamiyaa/joshuto +[lf]: https://github.com/gokcehan/lf +[lf-wiki]: https://github.com/gokcehan/lf/wiki/Integrations#zoxide [linuxbrew]: https://formulae.brew.sh/formula-linux/zoxide [macports]: https://ports.macports.org/port/zoxide/summary [neovim]: https://github.com/neovim/neovim [nixpkgs]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/zoxide/default.nix [nnn-autojump]: https://github.com/jarun/nnn/blob/master/plugins/autojump [nnn]: https://github.com/jarun/nnn +[opensuse factory]: https://build.opensuse.org/package/show/openSUSE:Factory/zoxide +[pacstall packages]: https://pacstall.dev/packages/zoxide-deb [pkgsrc]: https://pkgsrc.se/sysutils/zoxide [ranger-zoxide]: https://github.com/jchook/ranger-zoxide [ranger]: https://github.com/ranger/ranger [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/ +[t]: https://github.com/joshmedeski/t-smart-tmux-session-manager [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 +[tmux-session-wizard]: https://github.com/27medkamal/tmux-session-wizard [tutorial]: contrib/tutorial.webp -[ubuntu packages]: https://packages.ubuntu.com/hirsute/zoxide +[ubuntu packages]: https://packages.ubuntu.com/jammy/zoxide [vim]: https://github.com/vim/vim [void linux packages]: https://github.com/void-linux/void-packages/tree/master/srcpkgs/zoxide [wiki-env]: https://github.com/ajeetdsouza/zoxide/wiki/HOWTO:-set-environment-variables "HOWTO: set environment variables" diff --git a/build.rs b/build.rs index 2c1df03..5831b2d 100644 --- a/build.rs +++ b/build.rs @@ -1,56 +1,34 @@ -use std::process::Command; +#[path = "src/cmd/cmd.rs"] +mod cmd; + use std::{env, io}; -fn main() { - let pkg_version = env!("CARGO_PKG_VERSION"); - let version = match env::var_os("PROFILE") { - Some(profile) if profile == "release" => format!("v{}", pkg_version), - _ => git_version().unwrap_or_else(|| format!("v{}-unknown", pkg_version)), - }; - println!("cargo:rustc-env=ZOXIDE_VERSION={}", version); +use clap::CommandFactory; +use clap_complete::shells::{Bash, Elvish, Fish, PowerShell, Zsh}; +use clap_complete_fig::Fig; +use cmd::Cmd; - // Since we are generating completions in the package directory, we need to set this so that - // Cargo doesn't rebuild every time. +fn main() -> io::Result<()> { + // 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"); - println!("cargo:rerun-if-changed=tests"); - - generate_completions().unwrap(); -} - -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"]); - - let output = git.output().ok()?; - if !output.status.success() || output.stdout.is_empty() || !output.stderr.is_empty() { - return None; - } - String::from_utf8(output.stdout).ok() + println!("cargo:rerun-if-changed=src/"); + println!("cargo:rerun-if-changed=templates/"); + println!("cargo:rerun-if-changed=tests/"); + generate_completions() } fn generate_completions() -> io::Result<()> { - #[path = "src/cmd/_cmd.rs"] - mod cmd; - - use clap::CommandFactory; - use clap_complete::generate_to; - use clap_complete::shells::{Bash, Elvish, Fish, PowerShell, Zsh}; - use clap_complete_fig::Fig; - use cmd::Cmd; - + const BIN_NAME: &str = env!("CARGO_PKG_NAME"); + const OUT_DIR: &str = "contrib/completions"; let cmd = &mut Cmd::command(); - let bin_name = env!("CARGO_PKG_NAME"); - let out_dir = "contrib/completions"; - generate_to(Bash, cmd, bin_name, out_dir)?; - generate_to(Elvish, cmd, bin_name, out_dir)?; - generate_to(Fig, cmd, bin_name, out_dir)?; - generate_to(Fish, cmd, bin_name, out_dir)?; - generate_to(PowerShell, cmd, bin_name, out_dir)?; - generate_to(Zsh, cmd, bin_name, out_dir)?; + clap_complete::generate_to(Bash, cmd, BIN_NAME, OUT_DIR)?; + clap_complete::generate_to(Elvish, cmd, BIN_NAME, OUT_DIR)?; + clap_complete::generate_to(Fig, cmd, BIN_NAME, OUT_DIR)?; + clap_complete::generate_to(Fish, cmd, BIN_NAME, OUT_DIR)?; + clap_complete::generate_to(PowerShell, cmd, BIN_NAME, OUT_DIR)?; + clap_complete::generate_to(Zsh, cmd, BIN_NAME, OUT_DIR)?; Ok(()) } diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 9fb15d6..fd898e6 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -15,10 +15,10 @@ _zoxide() { local context curcontext="$curcontext" state line _arguments "${_arguments_options[@]}" \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ ":: :_zoxide_commands" \ "*::: :->zoxide" \ && ret=0 @@ -30,61 +30,115 @@ _zoxide() { case $line[1] in (add) _arguments "${_arguments_options[@]}" \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ '*::paths:_files -/' \ && ret=0 ;; +(edit) +_arguments "${_arguments_options[@]}" \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +":: :_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]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +':path:' \ +&& ret=0 +;; +(delete) +_arguments "${_arguments_options[@]}" \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +':path:' \ +&& ret=0 +;; +(increment) +_arguments "${_arguments_options[@]}" \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +':path:' \ +&& ret=0 +;; +(reload) +_arguments "${_arguments_options[@]}" \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; + esac + ;; +esac +;; (import) _arguments "${_arguments_options[@]}" \ '--from=[Application to import from]:FROM:(autojump z)' \ '--merge[Merge into existing database]' \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ ':path:_files' \ && ret=0 ;; (init) _arguments "${_arguments_options[@]}" \ -'--cmd=[Renames the '\''z'\'' command and corresponding aliases]:CMD: ' \ -'--hook=[Chooses event upon which an entry is added to the database]:HOOK:(none prompt pwd)' \ -'--no-aliases[Prevents zoxide from defining any commands]' \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'--cmd=[Changes the prefix of the \`z\` and \`zi\` commands]:CMD: ' \ +'--hook=[Changes how often zoxide increments a directory'\''s score]:HOOK:(none prompt pwd)' \ +'--no-cmd[Prevents zoxide from defining the \`z\` and \`zi\` commands]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ ':shell:(bash elvish fish nushell posix powershell xonsh zsh)' \ && ret=0 ;; (query) _arguments "${_arguments_options[@]}" \ -'--exclude=[Exclude a path from results]:path:_files -/' \ -'--all[Show deleted directories]' \ +'--exclude=[Exclude the current directory]:path:_files -/' \ +'-a[Show unavailable directories]' \ +'--all[Show unavailable directories]' \ '(-l --list)-i[Use interactive selection]' \ '(-l --list)--interactive[Use interactive selection]' \ '(-i --interactive)-l[List all matching directories]' \ '(-i --interactive)--list[List all matching directories]' \ -'(-i --interactive)-s[Print score with results]' \ -'(-i --interactive)--score[Print score with results]' \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-s[Print score with results]' \ +'--score[Print score with results]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ '*::keywords:' \ && ret=0 ;; (remove) _arguments "${_arguments_options[@]}" \ -'-i[Use interactive selection]' \ -'--interactive[Use interactive selection]' \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ '*::paths:_files -/' \ && ret=0 ;; @@ -97,6 +151,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 +164,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,10 +204,19 @@ _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=() _describe -t commands 'zoxide remove commands' commands "$@" } -_zoxide "$@" +if [ "$funcstack[1]" = "_zoxide" ]; then + _zoxide "$@" +else + compdef _zoxide zoxide +fi diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index ebe83de..ff0e6b0 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -21,11 +21,12 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { $completions = @(switch ($command) { 'zoxide' { - [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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') [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') @@ -33,53 +34,91 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { break } 'zoxide;add' { - [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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;edit' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') + [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') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;edit;delete' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;edit;increment' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') + break + } + 'zoxide;edit;reload' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') break } 'zoxide;import' { [CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from') [CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database') - [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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') break } 'zoxide;init' { - [CompletionResult]::new('--cmd', 'cmd', [CompletionResultType]::ParameterName, 'Renames the ''z'' command and corresponding aliases') - [CompletionResult]::new('--hook', 'hook', [CompletionResultType]::ParameterName, 'Chooses event upon which an entry is added to the database') - [CompletionResult]::new('--no-aliases', 'no-aliases', [CompletionResultType]::ParameterName, 'Prevents zoxide from defining any commands') - [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('--cmd', 'cmd', [CompletionResultType]::ParameterName, 'Changes the prefix of the `z` and `zi` commands') + [CompletionResult]::new('--hook', 'hook', [CompletionResultType]::ParameterName, 'Changes how often zoxide increments a directory''s score') + [CompletionResult]::new('--no-cmd', 'no-cmd', [CompletionResultType]::ParameterName, 'Prevents zoxide from defining the `z` and `zi` commands') + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') break } 'zoxide;query' { - [CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude a path from results') - [CompletionResult]::new('--all', 'all', [CompletionResultType]::ParameterName, 'Show deleted directories') + [CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude the current directory') + [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Show unavailable directories') + [CompletionResult]::new('--all', 'all', [CompletionResultType]::ParameterName, 'Show unavailable directories') [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print score with results') [CompletionResult]::new('--score', 'score', [CompletionResultType]::ParameterName, 'Print score with results') - [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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') 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') - [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') break } }) diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index e4ca62d..93c19d9 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -1,5 +1,5 @@ _zoxide() { - local i cur prev opts cmds + local i cur prev opts cmd COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -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 @@ -69,7 +154,7 @@ _zoxide() { fi case "${prev}" in --from) - COMPREPLY=($(compgen -W "" -- "${cur}")) + COMPREPLY=($(compgen -W "autojump z" -- "${cur}")) return 0 ;; *) @@ -80,7 +165,7 @@ _zoxide() { return 0 ;; zoxide__init) - opts="-h -V --no-aliases --cmd --hook --help --version bash elvish fish nushell posix powershell xonsh zsh" + opts="-h -V --no-cmd --cmd --hook --help --version bash elvish fish nushell posix powershell xonsh zsh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -91,7 +176,7 @@ _zoxide() { return 0 ;; --hook) - COMPREPLY=($(compgen -W "" -- "${cur}")) + COMPREPLY=($(compgen -W "none prompt pwd" -- "${cur}")) return 0 ;; *) @@ -102,7 +187,7 @@ _zoxide() { return 0 ;; zoxide__query) - opts="-i -l -s -h -V --all --interactive --list --score --exclude --help --version ..." + opts="-a -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 607c208..6183d37 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -18,60 +18,94 @@ set edit:completion:arg-completer[zoxide] = {|@words| } var completions = [ &'zoxide'= { - cand -h 'Print help information' - cand --help 'Print help information' - cand -V 'Print version information' - cand --version 'Print version information' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' 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' cand remove 'Remove a directory from the database' } &'zoxide;add'= { - cand -h 'Print help information' - cand --help 'Print help information' - cand -V 'Print version information' - cand --version 'Print version information' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;edit'= { + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + cand decrement 'decrement' + cand delete 'delete' + cand increment 'increment' + cand reload 'reload' + } + &'zoxide;edit;decrement'= { + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;edit;delete'= { + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;edit;increment'= { + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } + &'zoxide;edit;reload'= { + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' } &'zoxide;import'= { cand --from 'Application to import from' cand --merge 'Merge into existing database' - cand -h 'Print help information' - cand --help 'Print help information' - cand -V 'Print version information' - cand --version 'Print version information' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' } &'zoxide;init'= { - cand --cmd 'Renames the ''z'' command and corresponding aliases' - cand --hook 'Chooses event upon which an entry is added to the database' - cand --no-aliases 'Prevents zoxide from defining any commands' - cand -h 'Print help information' - cand --help 'Print help information' - cand -V 'Print version information' - cand --version 'Print version information' + cand --cmd 'Changes the prefix of the `z` and `zi` commands' + cand --hook 'Changes how often zoxide increments a directory''s score' + cand --no-cmd 'Prevents zoxide from defining the `z` and `zi` commands' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' } &'zoxide;query'= { - cand --exclude 'Exclude a path from results' - cand --all 'Show deleted directories' + cand --exclude 'Exclude the current directory' + cand -a 'Show unavailable directories' + cand --all 'Show unavailable directories' cand -i 'Use interactive selection' cand --interactive 'Use interactive selection' cand -l 'List all matching directories' cand --list 'List all matching directories' cand -s 'Print score with results' cand --score 'Print score with results' - cand -h 'Print help information' - cand --help 'Print help information' - cand -V 'Print version information' - cand --version 'Print version information' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' } &'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' - cand --version 'Print version information' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' } ] $completions[$command] diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 9945b3a..276cbaf 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -1,28 +1,42 @@ -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" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version' 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 add" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version' +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' +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' +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' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version' 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' -complete -c zoxide -n "__fish_seen_subcommand_from import" -s V -l version -d 'Print version information' -complete -c zoxide -n "__fish_seen_subcommand_from init" -l cmd -d 'Renames the \'z\' command and corresponding aliases' -r -complete -c zoxide -n "__fish_seen_subcommand_from init" -l hook -d 'Chooses event upon which an entry is added to the database' -r -f -a "{none ,prompt ,pwd }" -complete -c zoxide -n "__fish_seen_subcommand_from init" -l no-aliases -d 'Prevents zoxide from defining any 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 all -d 'Show deleted directories' +complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from import" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_seen_subcommand_from init" -l cmd -d 'Changes the prefix of the `z` and `zi` commands' -r +complete -c zoxide -n "__fish_seen_subcommand_from init" -l hook -d 'Changes how often zoxide increments a directory\'s score' -r -f -a "{none ,prompt ,pwd }" +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' +complete -c zoxide -n "__fish_seen_subcommand_from init" -s V -l version -d 'Print version' +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" -s a -l all -d 'Show unavailable 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' +complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version' +complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help' +complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version' diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index c3c6fb7..0200591 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -8,11 +8,11 @@ const completion: Fig.Spec = { options: [ { name: ["-h", "--help"], - description: "Print help information", + description: "Print help", }, { name: ["-V", "--version"], - description: "Print version information", + description: "Print version", }, ], args: { @@ -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", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "path", + }, + }, + { + name: "delete", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "path", + }, + }, + { + name: "increment", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + args: { + name: "path", + }, + }, + { + name: "reload", + hidden: true, + options: [ + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, + ], + options: [ + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, { name: "import", description: "Import entries from another application", @@ -28,16 +109,13 @@ const completion: Fig.Spec = { { name: "--from", description: "Application to import from", + isRepeatable: true, args: { name: "from", suggestions: [ - { - name: "autojump", - }, - { - name: "z", - }, - ] + "autojump", + "z", + ], }, }, { @@ -46,11 +124,11 @@ const completion: Fig.Spec = { }, { name: ["-h", "--help"], - description: "Print help information", + description: "Print help", }, { name: ["-V", "--version"], - description: "Print version information", + description: "Print version", }, ], args: { @@ -64,7 +142,8 @@ const completion: Fig.Spec = { options: [ { name: "--cmd", - description: "Renames the 'z' command and corresponding aliases", + description: "Changes the prefix of the `z` and `zi` commands", + isRepeatable: true, args: { name: "cmd", isOptional: true, @@ -72,64 +151,43 @@ const completion: Fig.Spec = { }, { name: "--hook", - description: "Chooses event upon which an entry is added to the database", + description: "Changes how often zoxide increments a directory's score", + isRepeatable: true, args: { name: "hook", isOptional: true, suggestions: [ - { - name: "none", - }, - { - name: "prompt", - }, - { - name: "pwd", - }, - ] + "none", + "prompt", + "pwd", + ], }, }, { - name: "--no-aliases", - description: "Prevents zoxide from defining any commands", + name: "--no-cmd", + description: "Prevents zoxide from defining the `z` and `zi` commands", }, { name: ["-h", "--help"], - description: "Print help information", + description: "Print help", }, { name: ["-V", "--version"], - description: "Print version information", + description: "Print version", }, ], args: { name: "shell", suggestions: [ - { - name: "bash", - }, - { - name: "elvish", - }, - { - name: "fish", - }, - { - name: "nushell", - }, - { - name: "posix", - }, - { - name: "powershell", - }, - { - name: "xonsh", - }, - { - name: "zsh", - }, - ] + "bash", + "elvish", + "fish", + "nushell", + "posix", + "powershell", + "xonsh", + "zsh", + ], }, }, { @@ -138,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, @@ -146,16 +205,24 @@ const completion: Fig.Spec = { }, }, { - name: "--all", - description: "Show deleted directories", + name: ["-a", "--all"], + description: "Show unavailable directories", }, { name: ["-i", "--interactive"], description: "Use interactive selection", + exclusiveOn: [ + "-l", + "--list", + ], }, { name: ["-l", "--list"], description: "List all matching directories", + exclusiveOn: [ + "-i", + "--interactive", + ], }, { name: ["-s", "--score"], @@ -163,15 +230,16 @@ const completion: Fig.Spec = { }, { name: ["-h", "--help"], - description: "Print help information", + description: "Print help", }, { name: ["-V", "--version"], - description: "Print version information", + description: "Print version", }, ], args: { name: "keywords", + isVariadic: true, isOptional: true, }, }, @@ -179,21 +247,18 @@ 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", + description: "Print help", }, { name: ["-V", "--version"], - description: "Print version information", + description: "Print version", }, ], args: { name: "paths", + isVariadic: true, isOptional: true, template: "folders", }, @@ -202,11 +267,11 @@ const completion: Fig.Spec = { options: [ { name: ["-h", "--help"], - description: "Print help information", + description: "Print help", }, { name: ["-V", "--version"], - description: "Print version information", + description: "Print version", }, ], }; diff --git a/contrib/warp.png b/contrib/warp.png new file mode 100644 index 0000000..28b35f1 Binary files /dev/null and b/contrib/warp.png differ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..796c4d9 --- /dev/null +++ b/install.sh @@ -0,0 +1,391 @@ +#!/bin/sh +# shellcheck shell=dash + +# The official zoxide installer. +# +# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local` +# extension. Note: Most shells limit `local` to 1 var per line, contra bash. + +main() { + if [ "${KSH_VERSION-}" = 'Version JM 93t+ 2010-03-05' ]; then + # The version of ksh93 that ships with many illumos systems does not + # support the "local" extension. Print a message rather than fail in + # subtle ways later on: + err 'the installer does not work with this ksh93 version; please try bash' + fi + + set -u + + # Detect and print host target triple. + ensure get_architecture + local _arch="${RETVAL}" + assert_nz "${_arch}" "arch" + echo "Detected architecture: ${_arch}" + + # Create and enter a temporary directory. + local _tmp_dir + _tmp_dir="$(mktemp -d)" || err "mktemp: could not create temporary directory" + cd "${_tmp_dir}" || err "cd: failed to enter directory: ${_tmp_dir}" + + # Download and extract zoxide. + ensure download_zoxide "${_arch}" + local _package="${RETVAL}" + assert_nz "${_package}" "package" + echo "Downloaded package: ${_package}" + case "${_package}" in + *.tar.gz) + need_cmd tar + ensure tar -xf "${_package}" + ;; + *.zip) + need_cmd unzip + ensure unzip -oq "${_package}" + ;; + *) + err "unsupported package format: ${_package}" + ;; + esac + + # Install binary. + local _bin_dir="${HOME}/.local/bin" + local _bin_name + case "${_arch}" in + *windows*) _bin_name="zoxide.exe" ;; + *) _bin_name="zoxide" ;; + esac + ensure mkdir -p "${_bin_dir}" + ensure cp "${_bin_name}" "${_bin_dir}" + ensure chmod +x "${_bin_dir}/${_bin_name}" + echo "Installed zoxide to ${_bin_dir}" + + # Install manpages. + local _man_dir="${HOME}/.local/share/man" + ensure mkdir -p "${_man_dir}/man1" + ensure cp "man/man1/"* "${_man_dir}/man1/" + echo "Installed manpages to ${_man_dir}" + + # Print success message and check $PATH. + echo "" + echo "zoxide is installed!" + if ! echo ":${PATH}:" | grep -Fq ":${_bin_dir}:"; then + echo "NOTE: ${_bin_dir} is not on your \$PATH. zoxide will not work unless it is added to \$PATH." + fi +} + +download_zoxide() { + local _arch="$1" + + if check_cmd curl; then + _dld=curl + elif check_cmd wget; then + _dld=wget + else + need_cmd 'curl or wget' + fi + need_cmd grep + + local _releases_url="https://api.github.com/repos/ajeetdsouza/zoxide/releases/latest" + local _releases + case "${_dld}" in + curl) _releases="$(curl -sL "${_releases_url}")" || + err "curl: failed to download ${_releases_url}" ;; + wget) _releases="$(wget -qO- "${_releases_url}")" || + err "wget: failed to download ${_releases_url}" ;; + *) err "unsupported downloader: ${_dld}" ;; + 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: https://github.com/ajeetdsouza/zoxide/issues" + + local _ext + case "${_package_url}" in + *.tar.gz) _ext="tar.gz" ;; + *.zip) _ext="zip" ;; + *) err "unsupported package format: ${_package_url}" ;; + esac + + local _package="zoxide.${_ext}" + case "${_dld}" in + curl) _releases="$(curl -sLo "${_package}" "${_package_url}")" || err "curl: failed to download ${_package_url}" ;; + wget) _releases="$(wget -qO "${_package}" "${_package_url}")" || err "wget: failed to download ${_package_url}" ;; + *) err "unsupported downloader: ${_dld}" ;; + esac + + RETVAL="${_package}" +} + +# The below functions have been extracted with minor modifications from the +# Rustup install script: +# +# https://github.com/rust-lang/rustup/blob/4c1289b2c3f3702783900934a38d7c5f912af787/rustup-init.sh + +get_architecture() { + local _ostype _cputype _bitness _arch _clibtype + _ostype="$(uname -s)" + _cputype="$(uname -m)" + _clibtype="musl" + + if [ "${_ostype}" = Linux ]; then + if [ "$(uname -o || true)" = Android ]; then + _ostype=Android + fi + fi + + if [ "${_ostype}" = Darwin ] && [ "${_cputype}" = i386 ]; then + # Darwin `uname -m` lies + if sysctl hw.optional.x86_64 | grep -q ': 1'; then + _cputype=x86_64 + fi + fi + + if [ "${_ostype}" = SunOS ]; then + # Both Solaris and illumos presently announce as "SunOS" in "uname -s" + # so use "uname -o" to disambiguate. We use the full path to the + # system uname in case the user has coreutils uname first in PATH, + # which has historically sometimes printed the wrong value here. + if [ "$(/usr/bin/uname -o || true)" = illumos ]; then + _ostype=illumos + fi + + # illumos systems have multi-arch userlands, and "uname -m" reports the + # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86 + # systems. Check for the native (widest) instruction set on the + # running kernel: + if [ "${_cputype}" = i86pc ]; then + _cputype="$(isainfo -n)" + fi + fi + + case "${_ostype}" in + Android) + _ostype=linux-android + ;; + Linux) + check_proc + _ostype=unknown-linux-${_clibtype} + _bitness=$(get_bitness) + ;; + FreeBSD) + _ostype=unknown-freebsd + ;; + NetBSD) + _ostype=unknown-netbsd + ;; + DragonFly) + _ostype=unknown-dragonfly + ;; + Darwin) + _ostype=apple-darwin + ;; + illumos) + _ostype=unknown-illumos + ;; + MINGW* | MSYS* | CYGWIN* | Windows_NT) + _ostype=pc-windows-msvc + ;; + *) + err "unrecognized OS type: ${_ostype}" + ;; + esac + + case "${_cputype}" in + i386 | i486 | i686 | i786 | x86) + _cputype=i686 + ;; + xscale | arm) + _cputype=arm + if [ "${_ostype}" = "linux-android" ]; then + _ostype=linux-androideabi + fi + ;; + armv6l) + _cputype=arm + if [ "${_ostype}" = "linux-android" ]; then + _ostype=linux-androideabi + else + _ostype="${_ostype}eabihf" + fi + ;; + armv7l | armv8l) + _cputype=armv7 + if [ "${_ostype}" = "linux-android" ]; then + _ostype=linux-androideabi + else + _ostype="${_ostype}eabihf" + fi + ;; + aarch64 | arm64) + _cputype=aarch64 + ;; + x86_64 | x86-64 | x64 | amd64) + _cputype=x86_64 + ;; + mips) + _cputype=$(get_endianness mips '' el) + ;; + mips64) + if [ "${_bitness}" -eq 64 ]; then + # only n64 ABI is supported for now + _ostype="${_ostype}abi64" + _cputype=$(get_endianness mips64 '' el) + fi + ;; + ppc) + _cputype=powerpc + ;; + ppc64) + _cputype=powerpc64 + ;; + ppc64le) + _cputype=powerpc64le + ;; + s390x) + _cputype=s390x + ;; + riscv64) + _cputype=riscv64gc + ;; + *) + err "unknown CPU type: ${_cputype}" + ;; + esac + + # Detect 64-bit linux with 32-bit userland + if [ "${_ostype}" = unknown-linux-musl ] && [ "${_bitness}" -eq 32 ]; then + case ${_cputype} in + x86_64) + # 32-bit executable for amd64 = x32 + if is_host_amd64_elf; then { + echo "x32 userland is unsupported" 1>&2 + exit 1 + }; else + _cputype=i686 + fi + ;; + mips64) + _cputype=$(get_endianness mips '' el) + ;; + powerpc64) + _cputype=powerpc + ;; + aarch64) + _cputype=armv7 + if [ "${_ostype}" = "linux-android" ]; then + _ostype=linux-androideabi + else + _ostype="${_ostype}eabihf" + fi + ;; + riscv64gc) + err "riscv64 with 32-bit userland unsupported" + ;; + *) ;; + esac + fi + + # Detect armv7 but without the CPU features Rust needs in that build, + # and fall back to arm. + # See https://github.com/rust-lang/rustup.rs/issues/587. + if [ "${_ostype}" = "unknown-linux-musleabihf" ] && [ "${_cputype}" = armv7 ]; then + if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then + # At least one processor does not have NEON. + _cputype=arm + fi + fi + + _arch="${_cputype}-${_ostype}" + RETVAL="${_arch}" +} + +get_bitness() { + need_cmd head + # Architecture detection without dependencies beyond coreutils. + # ELF files start out "\x7fELF", and the following byte is + # 0x01 for 32-bit and + # 0x02 for 64-bit. + # The printf builtin on some shells like dash only supports octal + # escape sequences, so we use those. + local _current_exe_head + _current_exe_head=$(head -c 5 /proc/self/exe) + if [ "${_current_exe_head}" = "$(printf '\177ELF\001')" ]; then + echo 32 + elif [ "${_current_exe_head}" = "$(printf '\177ELF\002')" ]; then + echo 64 + else + err "unknown platform bitness" + fi +} + +get_endianness() { + local cputype=$1 + local suffix_eb=$2 + local suffix_el=$3 + + # detect endianness without od/hexdump, like get_bitness() does. + need_cmd head + need_cmd tail + + local _current_exe_endianness + _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" + if [ "${_current_exe_endianness}" = "$(printf '\001')" ]; then + echo "${cputype}${suffix_el}" + elif [ "${_current_exe_endianness}" = "$(printf '\002')" ]; then + echo "${cputype}${suffix_eb}" + else + err "unknown platform endianness" + fi +} + +is_host_amd64_elf() { + need_cmd head + need_cmd tail + # ELF e_machine detection without dependencies beyond coreutils. + # Two-byte field at offset 0x12 indicates the CPU, + # but we're interested in it being 0x3E to indicate amd64, or not that. + local _current_exe_machine + _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1) + [ "${_current_exe_machine}" = "$(printf '\076')" ] +} + +check_proc() { + # Check for /proc by looking for the /proc/self/exe link. + # This is only run on Linux. + if ! test -L /proc/self/exe; then + err "unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc." + fi +} + +need_cmd() { + if ! check_cmd "$1"; then + err "need '$1' (command not found)" + fi +} + +check_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +# Run a command that should never fail. If the command fails execution +# will immediately terminate with an error showing the failing +# command. +ensure() { + if ! "$@"; then err "command failed: $*"; fi +} + +assert_nz() { + if [ -z "$1" ]; then err "found empty string: $2"; fi +} + +err() { + echo "Error: $1" >&2 + exit 1 +} + +# This is put in braces to ensure that the script does not run until it is +# downloaded completely. +{ + main "$@" || exit 1 +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..70f01eb --- /dev/null +++ b/justfile @@ -0,0 +1,39 @@ +default: + @just --list + +[unix] +fmt: + nix-shell --cores 0 --pure --run 'cargo-fmt --all' + nix-shell --cores 0 --pure --run 'nixfmt -- *.nix' + nix-shell --cores 0 --pure --run 'shfmt --indent=4 --language-dialect=posix --simplify --write *.sh' + nix-shell --cores 0 --pure --run 'yamlfmt -- .github/workflows/*.yml' + +[windows] +fmt: + cargo +nightly fmt --all + +[unix] +lint: + nix-shell --cores 0 --pure --run 'cargo-fmt --all --check' + nix-shell --cores 0 --pure --run 'cargo clippy --all-features --all-targets -- -Dwarnings' + nix-shell --cores 0 --pure --run 'cargo msrv verify' + nix-shell --cores 0 --pure --run 'cargo udeps --all-features --all-targets --workspace' + nix-shell --cores 0 --pure --run 'mandoc -man -Wall -Tlint -- man/man1/*.1' + nix-shell --cores 0 --pure --run 'markdownlint *.md' + nix-shell --cores 0 --pure --run 'nixfmt --check -- *.nix' + nix-shell --cores 0 --pure --run 'shellcheck --enable all *.sh' + nix-shell --cores 0 --pure --run 'shfmt --diff --indent=4 --language-dialect=posix --simplify *.sh' + nix-shell --cores 0 --pure --run 'yamlfmt -lint -- .github/workflows/*.yml' + +[windows] +lint: + cargo +nightly fmt --all --check + cargo +stable clippy --all-features --all-targets -- -Dwarnings + +[unix] +test *args: + nix-shell --cores 0 --pure --run 'cargo nextest run --all-features --no-fail-fast --workspace {{args}}' + +[windows] +test *args: + cargo +stable test --no-fail-fast --workspace {{args}} diff --git a/man/man1/zoxide-add.1 b/man/man1/zoxide-add.1 index 6e136bc..1ee9af3 100644 --- a/man/man1/zoxide-add.1 +++ b/man/man1/zoxide-add.1 @@ -19,6 +19,6 @@ Print help information. .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp -\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. +\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR .SH AUTHOR -Ajeet D'Souza <\fB98ajeet@gmail.com\fR> +Ajeet D'Souza \fB<98ajeet@gmail.com>\fR diff --git a/man/man1/zoxide-import.1 b/man/man1/zoxide-import.1 index 3d3b262..d008692 100644 --- a/man/man1/zoxide-import.1 +++ b/man/man1/zoxide-import.1 @@ -11,7 +11,7 @@ The format of the database being imported: tab(|); l l. \fBautojump\fR - \fBz\fR|(for \fBz\fR, \fBz.lua\fR, or \fBzsh-z\fR) + \fBz\fR|(for \fBfasd\fR, \fBz\fR, \fBz.lua\fR, or \fBzsh-z\fR) .TE .sp Note: zoxide only imports paths from autojump, since its matching @@ -26,6 +26,6 @@ option merges imported data into the existing database. .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp -\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. +\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR .SH AUTHOR -Ajeet D'Souza <\fB98ajeet@gmail.com\fR> +Ajeet D'Souza \fB<98ajeet@gmail.com>\fR diff --git a/man/man1/zoxide-init.1 b/man/man1/zoxide-init.1 index 69a3a9e..ee77216 100644 --- a/man/man1/zoxide-init.1 +++ b/man/man1/zoxide-init.1 @@ -7,58 +7,63 @@ To initialize zoxide on your shell: .TP .B bash -Add this to your configuration (usually \fB~/.bashrc\fR): +Add this to the \fBend\fR of your config file (usually \fB~/.bashrc\fR): .sp .nf \fBeval "$(zoxide init bash)"\fR .fi .TP .B elvish -Add this to your configuration (usually \fB~/.elvish/rc.elv\fR): +Add this to the \fBend\fR of your config file (usually \fB~/.elvish/rc.elv\fR): .sp .nf \fBeval $(zoxide init elvish | slurp)\fR .fi .sp -Note: zoxide only supports elvish v0.16.0 and above. +Note: zoxide only supports elvish v0.18.0 and above. .TP .B fish -Add this to your configuration (usually \fB~/.config/fish/config.fish\fR): +Add this to the \fBend\fR of your config file (usually +\fB~/.config/fish/config.fish\fR): .sp .nf \fBzoxide init fish | source\fR .fi .TP .B nushell -Add this to your configuration (find it by running \fBconfig path\fR in -Nushell): +Add this to the \fBend\fR of your env file (find it by running +\fB$nu.env-path\fR in Nushell): .sp .nf - \fBstartup = ["zoxide init nushell --hook prompt | save ~/.zoxide.nu", "source ~/.zoxide.nu"]\fR + \fBzoxide init nushell | save -f ~/.zoxide.nu\fR .fi .sp -Note: zoxide only supports Nushell v0.37.0 and above. -.TP -.B powershell -Add this to your configuration (find it by running \fBecho $profile\fR in -PowerShell): +Now, add this to the \fBend\fR of your config file (find it by running +\fB$nu.config-path\fR in Nushell): .sp .nf - \fBInvoke-Expression (& { - $hook = if ($PSVersionTable.PSVersion.Major -lt 6) { 'prompt' } else { 'pwd' } - (zoxide init --hook $hook powershell | Out-String) - })\fR + \fBsource ~/.zoxide.nu\fR +.fi +.sp +Note: zoxide only supports Nushell v0.73.0 and above. +.TP +.B powershell +Add this to the \fBend\fR of your config file (find it by running \fBecho +$profile\fR in PowerShell): +.sp +.nf + \fBInvoke-Expression (& { (zoxide init powershell | Out-String) })\fR .fi .TP .B xonsh -Add this to your configuration (usually \fB~/.xonshrc\fR): +Add this to the \fBend\fR of your config file (usually \fB~/.xonshrc\fR): .sp .nf \fBexecx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide')\fR .fi .TP .B zsh -Add this to your configuration (usually \fB~/.zshrc\fR): +Add this to the \fBend\fR of your config file (usually \fB~/.zshrc\fR): .sp .nf \fBeval "$(zoxide init zsh)"\fR @@ -66,7 +71,7 @@ Add this to your configuration (usually \fB~/.zshrc\fR): .TP .B any POSIX shell .sp -Add this to your configuration: +Add this to the \fBend\fR of your config file: .sp .nf \fBeval "$(zoxide init posix --hook prompt)"\fR @@ -74,9 +79,9 @@ Add this to your configuration: .SH OPTIONS .TP .B --cmd -Changes the prefix of predefined aliases (\fBz\fR, \fBzi\fR). +Changes the prefix of the \fBz\fR and \fBzi\fR commands. .br -\fB--cmd j\fR would change the aliases to (\fBj\fR, \fBji\fR). +\fB--cmd j\fR would change the commands to (\fBj\fR, \fBji\fR). .br \fB--cmd cd\fR would replace the \fBcd\fR command (doesn't work on Nushell / POSIX shells). @@ -94,13 +99,13 @@ l l. \fBpwd\fR|Whenever the directory is changed .TE .TP -.B --no-aliases -Don't define extra aliases (\fBz\fR, \fBzi\fR). These functions will still be -available in your shell as \fB__zoxide_z\fR and \fB__zoxide_zi\fR, should you -choose to redefine them. +.B --no-cmd +Prevents zoxide from defining the \fBz\fR and \fBzi\fR commands. These functions +will still be available in your shell as \fB__zoxide_z\fR and \fB__zoxide_zi\fR, +should you choose to redefine them. .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp -\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. +\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR .SH AUTHOR -Ajeet D'Souza <\fB98ajeet@gmail.com\fR> +Ajeet D'Souza \fB<98ajeet@gmail.com>\fR diff --git a/man/man1/zoxide-query.1 b/man/man1/zoxide-query.1 index bf19a43..e1ccd1d 100644 --- a/man/man1/zoxide-query.1 +++ b/man/man1/zoxide-query.1 @@ -28,6 +28,6 @@ Print the calculated score as well as the matched path. .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp -\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. +\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR .SH AUTHOR -Ajeet D'Souza <\fB98ajeet@gmail.com\fR> +Ajeet D'Souza \fB<98ajeet@gmail.com>\fR diff --git a/man/man1/zoxide-remove.1 b/man/man1/zoxide-remove.1 index 35611b9..e174a6f 100644 --- a/man/man1/zoxide-remove.1 +++ b/man/man1/zoxide-remove.1 @@ -10,12 +10,9 @@ 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 -\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. +\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR .SH AUTHOR -Ajeet D'Souza <\fB98ajeet@gmail.com\fR> +Ajeet D'Souza \fB<98ajeet@gmail.com>\fR diff --git a/man/man1/zoxide.1 b/man/man1/zoxide.1 index 9ebf751..ef1792b 100644 --- a/man/man1/zoxide.1 +++ b/man/man1/zoxide.1 @@ -129,6 +129,6 @@ l l. .SH REPORTING BUGS For any issues, feature requests, or questions, please visit: .sp -\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. +\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR .SH AUTHOR -Ajeet D'Souza <\fB98ajeet@gmail.com\fR> +Ajeet D'Souza \fB<98ajeet@gmail.com>\fR diff --git a/rustfmt.toml b/rustfmt.toml index 450ab8a..024f400 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,12 +1,8 @@ -# comment_width = 100 -# error_on_line_overflow = true -# error_on_unformatted = true -# group_imports = "StdExternalCrate" -# imports_granularity = "Module" -max_width = 120 +group_imports = "StdExternalCrate" +imports_granularity = "Module" newline_style = "Native" use_field_init_shorthand = true use_small_heuristics = "Max" use_try_shorthand = true -# wrap_comments = true -# version = "Two" +wrap_comments = true +version = "Two" diff --git a/shell.nix b/shell.nix index 1a4bd42..2059f4f 100644 --- a/shell.nix +++ b/shell.nix @@ -1,13 +1,22 @@ let - rust = import (builtins.fetchTarball - "https://github.com/oxalica/rust-overlay/archive/46d8d20fce510c6a25fa66f36e31f207f6ea49e4.tar.gz"); pkgs = import (builtins.fetchTarball - "https://github.com/NixOS/nixpkgs/archive/fae46e66a5df220327b45e0d7c27c6961cf922ce.tar.gz") { + "https://github.com/NixOS/nixpkgs/archive/22a6958f46fd8e14830d02856ff63b1d0e5cc3e4.tar.gz") { overlays = [ rust ]; }; + rust = import (builtins.fetchTarball + "https://github.com/oxalica/rust-overlay/archive/a61fcd9910229d097ffef92b5a2440065e3b64d5.tar.gz"); + + rust-nightly = + pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal); + cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" '' + export RUSTC="${rust-nightly}/bin/rustc"; + export CARGO="${rust-nightly}/bin/cargo"; + exec "${pkgs.cargo-udeps}/bin/cargo-udeps" "$@" + ''; in pkgs.mkShell { buildInputs = [ # Rust + (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.rustfmt)) pkgs.rust-bin.stable.latest.default # Shells @@ -21,7 +30,11 @@ in pkgs.mkShell { pkgs.zsh # Tools - pkgs.cargo-audit + cargo-udeps + pkgs.cargo-msrv + pkgs.cargo-nextest + pkgs.cargo-udeps + pkgs.just pkgs.mandoc pkgs.nixfmt pkgs.nodePackages.markdownlint-cli @@ -30,6 +43,7 @@ in pkgs.mkShell { pkgs.python3Packages.pylint pkgs.shellcheck pkgs.shfmt + pkgs.yamlfmt # Dependencies pkgs.cacert @@ -38,5 +52,5 @@ in pkgs.mkShell { pkgs.libiconv ]; - RUST_BACKTRACE = 1; + CARGO_TARGET_DIR = "target_nix"; } diff --git a/src/cmd/_cmd.rs b/src/cmd/_cmd.rs deleted file mode 100644 index d75a86f..0000000 --- a/src/cmd/_cmd.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::path::PathBuf; - -use clap::{ArgEnum, Parser, 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"; - -#[derive(Debug, Parser)] -#[clap( - bin_name = env!("CARGO_PKG_NAME"), - about, - author, - after_help = ENV_HELP, - disable_help_subcommand = true, - propagate_version = true, - version = option_env!("ZOXIDE_VERSION").unwrap_or_default() -)] -pub enum Cmd { - Add(Add), - Import(Import), - Init(Init), - Query(Query), - Remove(Remove), -} - -/// Add a new directory or increment its rank -#[derive(Debug, Parser)] -pub struct Add { - #[clap(min_values = 1, required = true, value_hint = ValueHint::DirPath)] - pub paths: Vec, -} - -/// Import entries from another application -#[derive(Debug, Parser)] -pub struct Import { - #[clap(value_hint = ValueHint::FilePath)] - pub path: PathBuf, - - /// Application to import from - #[clap(arg_enum, long)] - pub from: ImportFrom, - - /// Merge into existing database - #[clap(long)] - pub merge: bool, -} - -#[derive(ArgEnum, Clone, Debug)] -pub enum ImportFrom { - Autojump, - Z, -} - -/// Generate shell configuration -#[derive(Debug, Parser)] -pub struct Init { - #[clap(arg_enum)] - pub shell: InitShell, - - /// Prevents zoxide from defining any commands - #[clap(long)] - pub no_aliases: bool, - - /// Renames the 'z' command and corresponding aliases - #[clap(long, default_value = "z")] - pub cmd: String, - - /// Chooses event upon which an entry is added to the database - #[clap(arg_enum, long, default_value = "pwd")] - pub hook: InitHook, -} - -#[derive(ArgEnum, Clone, Copy, Debug, Eq, PartialEq)] -pub enum InitHook { - None, - Prompt, - Pwd, -} - -#[derive(ArgEnum, Clone, Debug)] -pub enum InitShell { - Bash, - Elvish, - Fish, - Nushell, - Posix, - Powershell, - Xonsh, - Zsh, -} - -/// Search for a directory in the database -#[derive(Debug, Parser)] -pub struct Query { - pub keywords: Vec, - - /// Show deleted directories - #[clap(long)] - pub all: bool, - - /// Use interactive selection - #[clap(long, short, conflicts_with = "list")] - pub interactive: bool, - - /// List all matching directories - #[clap(long, short, conflicts_with = "interactive")] - pub list: bool, - - /// Print score with results - #[clap(long, short, conflicts_with = "interactive")] - pub score: bool, - - /// Exclude a path from results - #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] - pub exclude: Option, -} - -/// 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/add.rs b/src/cmd/add.rs index 1bbe697..945bbe5 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -3,42 +3,42 @@ 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 = + 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); + 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 new file mode 100644 index 0000000..0e5f6c4 --- /dev/null +++ b/src/cmd/cmd.rs @@ -0,0 +1,186 @@ +#![allow(clippy::module_inception)] + +use std::path::PathBuf; + +use clap::{Parser, Subcommand, ValueEnum, ValueHint}; + +const HELP_TEMPLATE: &str = color_print::cstr!("\ +{before-help}{name} {version} +{author} +https://github.com/ajeetdsouza/zoxide + +{about} + +{usage-heading} +{tab}{usage} + +{all-args}{after-help} + +Environment variables: +{tab}_ZO_DATA_DIR {tab}Path for zoxide data files +{tab}_ZO_ECHO {tab}Print the matched directory before navigating to it when set to 1 +{tab}_ZO_EXCLUDE_DIRS {tab}List of directory globs to be excluded +{tab}_ZO_FZF_OPTS {tab}Custom flags to pass to fzf +{tab}_ZO_MAXAGE {tab}Maximum total age after which entries start getting deleted +{tab}_ZO_RESOLVE_SYMLINKS{tab}Resolve symlinks when storing paths"); + +#[derive(Debug, Parser)] +#[clap( + about, + author, + help_template = HELP_TEMPLATE, + disable_help_subcommand = true, + propagate_version = true, + version, +)] +pub enum Cmd { + Add(Add), + Edit(Edit), + Import(Import), + Init(Init), + Query(Query), + Remove(Remove), +} + +/// Add a new directory or increment its rank +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HELP_TEMPLATE, +)] +pub struct Add { + #[clap(num_args = 1.., required = true, value_hint = ValueHint::DirPath)] + pub paths: Vec, +} + +/// Edit the database +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HELP_TEMPLATE, +)] +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)] +#[clap( + author, + help_template = HELP_TEMPLATE, +)] +pub struct Import { + #[clap(value_hint = ValueHint::FilePath)] + pub path: PathBuf, + + /// Application to import from + #[clap(value_enum, long)] + pub from: ImportFrom, + + /// Merge into existing database + #[clap(long)] + pub merge: bool, +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum ImportFrom { + Autojump, + #[clap(alias = "fasd")] + Z, +} + +/// Generate shell configuration +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HELP_TEMPLATE, +)] +pub struct Init { + #[clap(value_enum)] + pub shell: InitShell, + + /// Prevents zoxide from defining the `z` and `zi` commands + #[clap(long, alias = "no-aliases")] + pub no_cmd: bool, + + /// Changes the prefix of the `z` and `zi` commands + #[clap(long, default_value = "z")] + pub cmd: String, + + /// Changes how often zoxide increments a directory's score + #[clap(value_enum, long, default_value = "pwd")] + pub hook: InitHook, +} + +#[derive(ValueEnum, Clone, Copy, Debug, Eq, PartialEq)] +pub enum InitHook { + None, + Prompt, + Pwd, +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum InitShell { + Bash, + Elvish, + Fish, + Nushell, + Posix, + Powershell, + Xonsh, + Zsh, +} + +/// Search for a directory in the database +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HELP_TEMPLATE, +)] +pub struct Query { + pub keywords: Vec, + + /// Show unavailable directories + #[clap(long, short)] + pub all: bool, + + /// Use interactive selection + #[clap(long, short, conflicts_with = "list")] + pub interactive: bool, + + /// List all matching directories + #[clap(long, short, conflicts_with = "interactive")] + pub list: bool, + + /// Print score with results + #[clap(long, short)] + pub score: bool, + + /// Exclude the current directory + #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] + pub exclude: Option, +} + +/// Remove a directory from the database +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HELP_TEMPLATE, +)] +pub struct Remove { + #[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..0f37165 --- /dev/null +++ b/src/cmd/edit.rs @@ -0,0 +1,84 @@ +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 + "--exact", + // Search result + "--no-sort", + // 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 83c939a..182a25f 100644 --- a/src/cmd/import.rs +++ b/src/cmd/import.rs @@ -3,24 +3,22 @@ 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 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,55 +26,51 @@ 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; } - let mut split = line.splitn(2, '\t'); + let (rank, path) = + line.split_once('\t').with_context(|| format!("invalid entry: {line}"))?; - 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. + 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. 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; } let mut split = line.rsplitn(3, '|'); - let last_accessed = split.next().with_context(|| format!("invalid entry: {}", line))?; - let last_accessed = last_accessed.parse().with_context(|| format!("invalid epoch: {}", last_accessed))?; + let last_accessed = split.next().with_context(|| format!("invalid entry: {line}"))?; + let last_accessed = + last_accessed.parse().with_context(|| format!("invalid epoch: {last_accessed}"))?; - let rank = split.next().with_context(|| format!("invalid entry: {}", line))?; - let rank = rank.parse().with_context(|| format!("invalid rank: {}", rank))?; + let rank = split.next().with_context(|| format!("invalid entry: {line}"))?; + let rank = rank.parse().with_context(|| format!("invalid rank: {rank}"))?; - let path = split.next().with_context(|| format!("invalid entry: {}", line))?; + 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 }, @@ -120,9 +114,9 @@ mod tests { Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, ]; - println!("exp: {:?}", &exp); + 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 }, @@ -161,9 +155,9 @@ mod tests { Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, ]; - println!("exp: {:?}", &exp); + 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 639ec0f..60bad63 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -6,28 +6,26 @@ use askama::Template; use crate::cmd::{Init, InitShell, Run}; use crate::config; use crate::error::BrokenPipeHandler; -use crate::shell::{self, Opts}; +use crate::shell::{Bash, Elvish, Fish, Nushell, Opts, Posix, Powershell, Xonsh, Zsh}; impl Run for Init { fn run(&self) -> Result<()> { - let cmd = if self.no_aliases { None } else { Some(self.cmd.as_str()) }; - + 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 { - InitShell::Bash => shell::Bash(opts).render(), - InitShell::Elvish => shell::Elvish(opts).render(), - InitShell::Fish => shell::Fish(opts).render(), - InitShell::Nushell => shell::Nushell(opts).render(), - InitShell::Posix => shell::Posix(opts).render(), - InitShell::Powershell => shell::Powershell(opts).render(), - InitShell::Xonsh => shell::Xonsh(opts).render(), - InitShell::Zsh => shell::Zsh(opts).render(), + InitShell::Bash => Bash(opts).render(), + InitShell::Elvish => Elvish(opts).render(), + InitShell::Fish => Fish(opts).render(), + InitShell::Nushell => Nushell(opts).render(), + InitShell::Posix => Posix(opts).render(), + InitShell::Powershell => Powershell(opts).render(), + InitShell::Xonsh => Xonsh(opts).render(), + InitShell::Zsh => Zsh(opts).render(), } .context("could not render template")?; - writeln!(io::stdout(), "{}", source).pipe_exit("stdout") + writeln!(io::stdout(), "{source}").pipe_exit("stdout") } } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index c9cab3c..5c17474 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,5 +1,6 @@ -mod _cmd; mod add; +mod cmd; +mod edit; mod import; mod init; mod query; @@ -7,7 +8,7 @@ mod remove; use anyhow::Result; -pub use crate::cmd::_cmd::*; +pub use crate::cmd::cmd::*; pub trait Run { fn run(&self) -> Result<()>; @@ -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 f095501..21ae197 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -1,18 +1,16 @@ use std::io::{self, Write}; -use anyhow::{Context, Result}; +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()) } } @@ -20,7 +18,50 @@ 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 { + bail!(if stream.did_exclude() { + "you are already in the only match" + } else { + "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(); @@ -29,51 +70,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 stdout = io::stdout(); - let handle = &mut 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 dir = stream.next().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 + "--exact", + // Search result + "--no-sort", + // 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 7805517..55c6989 100644 --- a/src/cmd/remove.rs +++ b/src/cmd/remove.rs @@ -1,52 +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) { - 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) { - bail!("path not found in database: {} ({})", path, path_abs) - } + 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/config.rs b/src/config.rs index 4ec7903..4a1b6b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,7 @@ use std::env; use std::ffi::OsString; use std::path::PathBuf; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use glob::Pattern; use crate::db::Rank; @@ -10,45 +10,35 @@ use crate::db::Rank; pub fn data_dir() -> Result { let path = match env::var_os("_ZO_DATA_DIR") { Some(path) => PathBuf::from(path), - None => match dirs::data_local_dir() { - Some(mut path) => { - path.push("zoxide"); - path - } - None => bail!("could not find data directory, please set _ZO_DATA_DIR manually"), - }, + None => dirs::data_local_dir() + .context("could not find data directory, please set _ZO_DATA_DIR manually")? + .join("zoxide"), }; - Ok(path) } pub fn echo() -> bool { - match env::var_os("_ZO_ECHO") { - Some(var) => var == "1", - None => false, - } + env::var_os("_ZO_ECHO").map_or(false, |var| var == "1") } pub fn exclude_dirs() -> Result> { - env::var_os("_ZO_EXCLUDE_DIRS").map_or_else( - || { + match env::var_os("_ZO_EXCLUDE_DIRS") { + Some(paths) => env::split_paths(&paths) + .map(|path| { + let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?; + Pattern::new(pattern) + .with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {pattern}")) + }) + .collect(), + None => { let pattern = (|| { let home = dirs::home_dir()?; - let home = home.to_str()?; - let home = Pattern::escape(home); + let home = Pattern::escape(home.to_str()?); Pattern::new(&home).ok() })(); Ok(pattern.into_iter().collect()) - }, - |paths| { - env::split_paths(&paths) - .map(|path| { - let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?; - Pattern::new(pattern).with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {}", pattern)) - }) - .collect() - }, - ) + } + } } pub fn fzf_opts() -> Option { @@ -56,20 +46,15 @@ pub fn fzf_opts() -> Option { } pub fn maxage() -> Result { - match env::var_os("_ZO_MAXAGE") { - Some(maxage) => { - let maxage = maxage.to_str().context("invalid unicode in _ZO_MAXAGE")?; - let maxage = - maxage.parse::().with_context(|| format!("unable to parse _ZO_MAXAGE as integer: {}", maxage))?; - Ok(maxage as Rank) - } - None => Ok(10000.0), - } + env::var_os("_ZO_MAXAGE").map_or(Ok(10_000.0), |maxage| { + let maxage = maxage.to_str().context("invalid unicode in _ZO_MAXAGE")?; + let maxage = maxage + .parse::() + .with_context(|| format!("unable to parse _ZO_MAXAGE as integer: {maxage}"))?; + Ok(maxage as Rank) + }) } pub fn resolve_symlinks() -> bool { - match env::var_os("_ZO_RESOLVE_SYMLINKS") { - Some(var) => var == "1", - None => false, - } + env::var_os("_ZO_RESOLVE_SYMLINKS").map_or(false, |var| var == "1") } diff --git a/src/db/dir.rs b/src/db/dir.rs index 1661a1f..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 {}, supports {})", version, 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,63 +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); - let score = if score > 9999.0 { - 9999 - } else if score > 0.0 { - score as u32 - } else { - 0 - }; - 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::{Dir, DirList}; - - #[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 f0e99d1..8eb9fc6 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,152 +1,237 @@ mod dir; mod stream; -use std::fs; -use std::io; 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> { +impl Database { + const VERSION: u32 = 3; + + pub fn open() -> Result { + let data_dir = config::data_dir()?; + Self::open_dir(data_dir) + } + + pub fn open_dir(data_dir: impl AsRef) -> Result { + let data_dir = data_dir.as_ref(); + let path = data_dir.join("db.zo"); + + match fs::read(&path) { + 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(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())) + } + } + } + pub fn save(&mut self) -> Result<()> { - if !self.modified { + // Only write to disk if the database is modified. + if !self.dirty() { return Ok(()); } - 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; + 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(()) } - /// 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(); - - match self.dirs.iter_mut().find(|dir| dir.path == path) { + /// 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 => { - self.dirs.push(Dir { path: path.to_string().into(), last_accessed: now, rank: 1.0 }); + dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) } - Some(dir) => { - dir.last_accessed = now; - dir.rank += 1.0; - } - }; + }); + self.with_dirty_mut(|dirty| *dirty = true); + } - self.modified = 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.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path)); + self.sort_by_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); + 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; } - } - self.modified = true; + // 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); -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 }) + // Deserialize sections. + let version = deserializer.deserialize(bytes_version)?; + let dirs = match version { + Self::VERSION => { + deserializer.deserialize(bytes_dirs).context("could not deserialize database")? } - 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 }) + version => { + bail!("unsupported version (got {version}, supports {})", Self::VERSION) } - 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) + Ok(dirs) + } } #[cfg(test)] @@ -155,50 +240,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 e5d3eb9..44b59d9 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -2,36 +2,36 @@ use std::iter::Rev; use std::ops::Range; use std::{fs, path}; -use ordered_float::OrderedFloat; +use crate::db::{Database, Dir, Epoch}; +use crate::util::{self, MONTH}; -use super::{Database, Dir, Epoch}; -use crate::util; - -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_key(|dir| OrderedFloat(dir.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, @@ -40,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 } @@ -51,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; @@ -66,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, @@ -126,7 +130,7 @@ mod tests { use rstest::rstest; - use super::Database; + use super::*; #[rstest] // Case normalization @@ -149,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/error.rs b/src/error.rs index 4a482b8..d2baa7f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,7 +6,7 @@ use anyhow::{bail, Context, Result}; /// Custom error type for early exit. #[derive(Debug)] pub struct SilentExit { - pub code: i32, + pub code: u8, } impl Display for SilentExit { @@ -23,7 +23,7 @@ impl BrokenPipeHandler for io::Result<()> { fn pipe_exit(self, device: &str) -> Result<()> { match self { Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), - result => result.with_context(|| format!("could not write to {}", device)), + result => result.with_context(|| format!("could not write to {device}")), } } } diff --git a/src/main.rs b/src/main.rs index b22b216..18ac3a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_component_path_imports)] // rstest_reuse must be imported at the top of the crate. -#[cfg(test)] +#[cfg(all(test, feature = "nix-dev"))] use rstest_reuse; mod cmd; @@ -11,26 +11,28 @@ mod error; mod shell; mod util; +use std::env; use std::io::{self, Write}; -use std::{env, process}; +use std::process::ExitCode; use clap::Parser; use crate::cmd::{Cmd, Run}; use crate::error::SilentExit; -pub fn main() { +pub fn main() -> ExitCode { // Forcibly disable backtraces. env::remove_var("RUST_LIB_BACKTRACE"); env::remove_var("RUST_BACKTRACE"); - if let Err(e) = Cmd::parse().run() { - match e.downcast::() { - Ok(SilentExit { code }) => process::exit(code), + match Cmd::parse().run() { + Ok(()) => ExitCode::SUCCESS, + Err(e) => match e.downcast::() { + Ok(SilentExit { code }) => code.into(), Err(e) => { - let _ = writeln!(io::stderr(), "zoxide: {:?}", e); - process::exit(1); + _ = writeln!(io::stderr(), "zoxide: {e:?}"); + ExitCode::FAILURE } - } + }, } } diff --git a/src/shell.rs b/src/shell.rs index 79554f4..a50c184 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(["--diff", "--indent=4", "--language-dialect=bash", "--simplify", "-"]) .write_stdin(source) .assert() .success() @@ -96,16 +96,21 @@ mod tests { #[apply(opts)] fn elvish_elvish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; - let mut source = String::new(); + let mut source = String::default(); - // 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:")) { + // Filter out lines using edit:*, since those functions are only available in + // the interactive editor. + for line in Elvish(&opts).render().unwrap().lines().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 +123,7 @@ mod tests { Command::new("fish") .env("HOME", tempdir) - .args(&["--command", &source, "--private"]) + .args(["--command", &source, "--no-config", "--private"]) .assert() .success() .stdout("") @@ -149,10 +154,14 @@ mod tests { let source = Nushell(&opts).render().unwrap(); let tempdir = tempfile::tempdir().unwrap(); - let tempdir = tempdir.path().to_str().unwrap(); + let tempdir = tempdir.path(); - let assert = - Command::new("nu").env("HOME", tempdir).args(&["--commands", &source]).assert().success().stderr(""); + let assert = Command::new("nu") + .env("HOME", tempdir) + .args(["--commands", &source]) + .assert() + .success() + .stderr(""); if opts.hook != InitHook::Pwd { assert.stdout(""); @@ -165,7 +174,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 +188,8 @@ 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 +201,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 +216,7 @@ mod tests { source.push('\n'); Command::new("shfmt") - .args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) + .args(["--diff", "--indent=4", "--language-dialect=posix", "--simplify", "-"]) .write_stdin(source) .assert() .success() @@ -221,7 +231,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 +244,12 @@ 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 +257,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 +267,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 +283,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 +298,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 +312,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 2ad0b16..47e7e93 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,83 +1,149 @@ -use std::env; +use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Write}; -use std::mem; use std::path::{Component, Path, PathBuf}; -use std::process::Command; -use std::process::{Child, ChildStdin, Stdio}; +use std::process::{Child, Command, Stdio}; use std::time::SystemTime; +use std::{env, mem}; -use anyhow::{anyhow, bail, Context, Result}; +#[cfg(windows)] +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 { - const ERR_NOT_FOUND: &'static str = "could not find fzf, is it installed?"; + const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?"; - pub fn new(multiple: bool) -> Result { - let bin = if cfg!(windows) { "fzf.exe" } else { "fzf" }; - let mut command = get_command(bin).map_err(|_| anyhow!(Self::ERR_NOT_FOUND))?; - if multiple { - command.arg("-m"); - } - command.arg("-n2..").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=40%", - "--info=inline", - "--layout=reverse", - // Scripting - "--exit-0", - "--select-1", - // Key/Event bindings - "--bind=ctrl-z:ignore", - ]); - if cfg!(unix) { - command.env("SHELL", "sh"); - command.arg(r"--preview=\command -p ls -p {2..}"); - } - } + 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 program = which::which("fzf.exe").map_err(|_| anyhow!(Self::ERR_FZF_NOT_FOUND))?; + #[cfg(not(windows))] + let program = "fzf"; - let child = match command.spawn() { - Ok(child) => child, - Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_NOT_FOUND), - Err(e) => Err(e).context("could not launch fzf")?, - }; + // TODO: check version of fzf here. - Ok(Fzf { child }) + let mut cmd = Command::new(program); + cmd.args([ + // Search mode + "--delimiter=\t", + "--nth=2", + // Scripting + "--read0", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); + + 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 output = String::new(); + let mut stdout = self.0.stdout.take().unwrap(); + let mut output = String::default(); 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"), Some(2) => bail!("fzf returned an error"), - Some(code @ 130) => bail!(SilentExit { code }), + Some(130) => bail!(SilentExit { code: 130 }), Some(128..=254) | None => bail!("fzf was terminated"), _ => bail!("fzf returned an unknown error"), } @@ -85,7 +151,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(); @@ -94,19 +160,22 @@ pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> let (mut tmp_file, tmp_path) = tmpfile(dir)?; let result = (|| { // Write to the tmpfile. - let _ = tmp_file.set_len(contents.len() as u64); - tmp_file.write_all(contents).with_context(|| format!("could not write to file: {}", tmp_path.display()))?; + _ = tmp_file.set_len(contents.len() as u64); + tmp_file + .write_all(contents) + .with_context(|| format!("could not write to file: {}", tmp_path.display()))?; // Set the owner of the tmpfile (UNIX only). #[cfg(unix)] if let Ok(metadata) = path.metadata() { - use nix::unistd::{self, Gid, Uid}; use std::os::unix::fs::MetadataExt; use std::os::unix::io::AsRawFd; + use nix::unistd::{self, Gid, Uid}; + let uid = Uid::from_raw(metadata.uid()); let gid = Gid::from_raw(metadata.gid()); - let _ = unistd::fchown(tmp_file.as_raw_fd(), Some(uid), Some(gid)); + _ = unistd::fchown(tmp_file.as_raw_fd(), Some(uid), Some(gid)); } // Close and rename the tmpfile. @@ -115,13 +184,13 @@ pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> })(); // In case of an error, delete the tmpfile. if result.is_err() { - let _ = fs::remove_file(&tmp_path); + _ = fs::remove_file(&tmp_path); } 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(); @@ -141,35 +210,39 @@ 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 { @@ -177,49 +250,22 @@ pub fn current_dir() -> Result { } pub fn current_time() -> Result { - let current_time = - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).context("system clock set to invalid time")?.as_secs(); + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .context("system clock set to invalid time")? + .as_secs(); Ok(current_time) } -/// Constructs a new [`Command`] for launching the program with the name -/// `program`. -/// -/// 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. -/// -/// On other platforms, this is a no-op. -/// -pub fn get_command>(program: P) -> Result { - let program = program.as_ref(); - if !cfg!(windows) { - return Ok(Command::new(program)); - } - - let paths = env::var_os("PATH").context("PATH environment variable not set")?; - for path in env::split_paths(&paths) { - if path.as_os_str().is_empty() { - continue; - } - let path = path.join(program); - if path.metadata().map_or(false, |m| !m.is_dir()) { - return Ok(Command::new(path)); - } - } - - bail!("executable not found in PATH: {}", program.display()); -} - -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; @@ -230,13 +276,15 @@ 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(); match components.next() { Some(Component::Prefix(prefix)) => match prefix.kind() { - Prefix::Disk(drive_letter) | Prefix::VerbatimDisk(drive_letter) => Some(drive_letter), + Prefix::Disk(drive_letter) | Prefix::VerbatimDisk(drive_letter) => { + Some(drive_letter) + } _ => None, }, _ => None, @@ -289,8 +337,9 @@ pub fn resolve_path>(path: &P) -> Result { components.next(); let current_dir = env::current_dir()?; - let drive_letter = get_drive_letter(¤t_dir) - .with_context(|| format!("could not get drive letter: {}", current_dir.display()))?; + let drive_letter = get_drive_letter(¤t_dir).with_context(|| { + format!("could not get drive letter: {}", current_dir.display()) + })?; base_path = get_drive_path(drive_letter); stack.extend(base_path.components()); } @@ -310,7 +359,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(); @@ -324,11 +373,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() - } + if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() } } diff --git a/templates/bash.txt b/templates/bash.txt index 0cd3baf..aeec524 100644 --- a/templates/bash.txt +++ b/templates/bash.txt @@ -7,7 +7,9 @@ # pwd based on the value of _ZO_RESOLVE_SYMLINKS. function __zoxide_pwd() { -{%- if resolve_symlinks %} +{%- if cfg!(windows) %} + \command cygpath -w "$(\builtin pwd -P)" +{%- else if resolve_symlinks %} \builtin pwd -P {%- else %} \builtin pwd -L @@ -32,7 +34,8 @@ function __zoxide_cd() { {%- if hook == InitHook::Prompt %} function __zoxide_hook() { \builtin local -r retval="$?" - \command zoxide add -- "$(__zoxide_pwd || \builtin true)" + # shellcheck disable=SC2312 + \command zoxide add -- "$(__zoxide_pwd)" return "${retval}" } {%- else if hook == InitHook::Pwd %} @@ -58,8 +61,7 @@ fi {% endif -%} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # __zoxide_z_prefix='z#' @@ -73,13 +75,14 @@ function __zoxide_z() { __zoxide_cd "${OLDPWD}" elif [[ $# -eq 1 && -d $1 ]]; then __zoxide_cd "$1" - elif [[ ${@: -1} == "${__zoxide_z_prefix}"* ]]; then + elif [[ ${@: -1} == "${__zoxide_z_prefix}"?* ]]; then # shellcheck disable=SC2124 \builtin local result="${@: -1}" __zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}" else \builtin local result - result="$(\command zoxide query --exclude "$(__zoxide_pwd || \builtin true)" -- "$@")" && + # shellcheck disable=SC2312 + result="$(\command zoxide query --exclude "$(__zoxide_pwd)" -- "$@")" && __zoxide_cd "${result}" fi } @@ -87,29 +90,22 @@ function __zoxide_z() { # Jump to a directory using interactive search. function __zoxide_zi() { \builtin local result - result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${result}" + result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${result}" } {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} {%- when Some with (cmd) %} -# Remove definitions. -function __zoxide_unset() { - \builtin unset -f "$@" &>/dev/null - \builtin unset -v "$@" &>/dev/null - \builtin unalias "$@" &>/dev/null || \builtin : -} - -__zoxide_unset {{cmd}} +\builtin unalias {{cmd}} &>/dev/null || \builtin true function {{cmd}}() { __zoxide_z "$@" } -__zoxide_unset {{cmd}}i +\builtin unalias {{cmd}}i &>/dev/null || \builtin true function {{cmd}}i() { __zoxide_zi "$@" } @@ -130,18 +126,21 @@ if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VER # If there is only one argument, use `cd` completions. if [[ {{ "${#COMP_WORDS[@]}" }} -eq 2 ]]; then - \builtin mapfile -t COMPREPLY < \ - <(\builtin compgen -A directory -S / -- "${COMP_WORDS[-1]}" || \builtin true) + \builtin mapfile -t COMPREPLY < <( + \builtin compgen -A directory -- "${COMP_WORDS[-1]}" || \builtin true + ) # If there is a space after the last word, use interactive selection. - elif [[ -z ${COMP_WORDS[-1]} ]]; then + elif [[ -z ${COMP_WORDS[-1]} ]] && [[ ${COMP_WORDS[-2]} != "${__zoxide_z_prefix}"?* ]]; then \builtin local result - result="$(\command zoxide query -i -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" && - COMPREPLY=("${__zoxide_z_prefix}${result@Q}") + # shellcheck disable=SC2312 + result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" && + COMPREPLY=("${__zoxide_z_prefix}${result}/") \builtin printf '\e[5n' fi } - \builtin complete -F __zoxide_z_complete -o nospace -- {{cmd}} + \builtin complete -F __zoxide_z_complete -o filenames -- {{cmd}} + \builtin complete -r {{cmd}}i &>/dev/null || \builtin true fi {%- when None %} diff --git a/templates/elvish.txt b/templates/elvish.txt index d507710..1f48a91 100644 --- a/templates/elvish.txt +++ b/templates/elvish.txt @@ -41,8 +41,7 @@ if (builtin:not (builtin:eq $E:__zoxide_shlvl $E:SHLVL)) { {%- endif %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # # Jump to a directory using only keywords. @@ -57,7 +56,7 @@ fn __zoxide_z {|@rest| var path try { set path = (zoxide query --exclude $pwd -- $@rest) - } except { + } catch { } else { __zoxide_cd $path } @@ -69,8 +68,8 @@ edit:add-var __zoxide_z~ $__zoxide_z~ fn __zoxide_zi {|@rest| var path try { - set path = (zoxide query -i -- $@rest) - } except { + set path = (zoxide query --interactive -- $@rest) + } catch { } else { __zoxide_cd $path } @@ -78,7 +77,7 @@ fn __zoxide_zi {|@rest| edit:add-var __zoxide_zi~ $__zoxide_zi~ {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} @@ -88,8 +87,10 @@ edit:add-var {{cmd}}~ $__zoxide_z~ edit:add-var {{cmd}}i~ $__zoxide_zi~ # Load completions. -{# zoxide-based completions are currently not possible, because Elvish only - # prints a completion if the current token is a prefix of it. -#} +{#- + zoxide-based completions are currently not possible, because Elvish only prints + a completion if the current token is a prefix of it. +#} fn __zoxide_z_complete {|@rest| if (!= (builtin:count $rest) 2) { builtin:return @@ -116,4 +117,4 @@ set edit:completion:arg-completer[{{cmd}}] = $__zoxide_z_complete~ # # eval (zoxide init elvish | slurp) # -# Note: zoxide only supports elvish v0.17.0 and above. +# Note: zoxide only supports elvish v0.18.0 and above. diff --git a/templates/fish.txt b/templates/fish.txt index 19cb234..1db92b0 100644 --- a/templates/fish.txt +++ b/templates/fish.txt @@ -7,7 +7,9 @@ # pwd based on the value of _ZO_RESOLVE_SYMLINKS. function __zoxide_pwd -{%- if resolve_symlinks %} +{%- if cfg!(windows) %} + command cygpath -w (builtin pwd -P) +{%- else if resolve_symlinks %} builtin pwd -P {%- else %} builtin pwd -L @@ -16,9 +18,9 @@ end # A copy of fish's internal cd function. This makes it possible to use # `alias cd=z` without causing an infinite loop. -if ! builtin functions -q __zoxide_cd_internal - if builtin functions -q cd - builtin functions -c cd __zoxide_cd_internal +if ! builtin functions --query __zoxide_cd_internal + if builtin functions --query cd + builtin functions --copy cd __zoxide_cd_internal else alias __zoxide_cd_internal='builtin cd' end @@ -26,7 +28,11 @@ end # cd + custom logic based on the value of _ZO_ECHO. function __zoxide_cd +{%- if cfg!(windows) %} + __zoxide_cd_internal (cygpath -u $argv) +{%- else %} __zoxide_cd_internal $argv +{%- endif %} {%- if echo %} and __zoxide_pwd {%- endif %} @@ -53,68 +59,67 @@ end {%- endif %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # +if test -z $__zoxide_z_prefix + set __zoxide_z_prefix 'z!' +end +set __zoxide_z_prefix_regex ^(string escape --style=regex $__zoxide_z_prefix) + # Jump to a directory using only keywords. function __zoxide_z - set argc (count $argv) + set -l argc (count $argv) if test $argc -eq 0 __zoxide_cd $HOME else if test "$argv" = - __zoxide_cd - else if test $argc -eq 1 -a -d $argv[1] __zoxide_cd $argv[1] + else if set -l result (string replace --regex $__zoxide_z_prefix_regex '' $argv[-1]); and test -n $result + __zoxide_cd $result else set -l result (command zoxide query --exclude (__zoxide_pwd) -- $argv) and __zoxide_cd $result 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) if test (count $tokens) -le 2 -a (count $curr_tokens) -eq 1 # If there are < 2 arguments, use `cd` completions. - __fish_complete_directories "$tokens[2]" '' - else if test (count $tokens) -eq (count $curr_tokens) - # If the last argument is empty, use interactive selection. + complete --do-complete "'' "(commandline --cut-at-cursor --current-token) | string match --regex '.*/$' + else if test (count $tokens) -eq (count $curr_tokens); and ! string match --quiet --regex $__zoxide_z_prefix_regex. $tokens[-1] + # If the last argument is empty and the one before doesn't start with + # $__zoxide_z_prefix, use interactive selection. set -l query $tokens[2..-1] - set -l result (zoxide query -i -- $query) - commandline --current-process "$tokens[1] "(string escape $result) + set -l result (zoxide query --exclude (__zoxide_pwd) --interactive -- $query) + and echo $__zoxide_z_prefix$result 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 - set -l result (command zoxide query -i -- $argv) + set -l result (command zoxide query --interactive -- $argv) and __zoxide_cd $result end {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} {%- when Some with (cmd) %} -# Remove definitions. -function __zoxide_unset - set --erase $argv >/dev/null 2>&1 - abbr --erase $argv >/dev/null 2>&1 - builtin functions --erase $argv >/dev/null 2>&1 -end - -__zoxide_unset {{cmd}} +abbr --erase {{cmd}} &>/dev/null alias {{cmd}}=__zoxide_z -complete -c {{cmd}} -e -complete -c {{cmd}} -f -a '(__zoxide_z_complete)' -__zoxide_unset {{cmd}}i +abbr --erase {{cmd}}i &>/dev/null alias {{cmd}}i=__zoxide_zi {%- when None %} @@ -127,4 +132,4 @@ alias {{cmd}}i=__zoxide_zi # To initialize zoxide, add this to your configuration (usually # ~/.config/fish/config.fish): # -# zoxide init fish | source +# zoxide init fish | source diff --git a/templates/nushell.txt b/templates/nushell.txt index 9874a31..c4894da 100644 --- a/templates/nushell.txt +++ b/templates/nushell.txt @@ -3,90 +3,65 @@ # Code generated by zoxide. DO NOT EDIT. -{{ section }} -# Utility functions for zoxide. -# - -# Default prompt for Nushell. -def __zoxide_prompt [] { - let git = $'(do -i {git rev-parse --abbrev-ref HEAD} | str trim -rc (char newline))' - let git = (if ($git | str length) == 0 { '' } { - build-string (char lparen) (ansi cb) $git (ansi reset) (char rparen) - }) - build-string (ansi gb) (pwd) (ansi reset) $git '> ' -} - {{ section }} # Hook configuration for zoxide. # -# Hook to add new entries to the database. -{%- match hook %} -{%- when InitHook::None %} +{% if hook == InitHook::None -%} {{ not_configured }} -{%- when InitHook::Prompt %} -def __zoxide_hook [] { - shells | where active == $true && name == filesystem | get path | each { - zoxide add -- $it - } +{%- else -%} +# Initialize hook to add new entries to the database. +if (not ($env | default false __zoxide_hooked | get __zoxide_hooked)) { + $env.__zoxide_hooked = true +{%- if hook == InitHook::Prompt %} + $env.config = ($env | default {} config).config + $env.config = ($env.config | default {} hooks) + $env.config = ($env.config | update hooks ($env.config.hooks | default [] pre_prompt)) + $env.config = ($env.config | update hooks.pre_prompt ($env.config.hooks.pre_prompt | append { || + zoxide add -- $env.PWD + })) +{%- else if hook == InitHook::Pwd %} + $env.config = ($env | default {} config).config + $env.config = ($env.config | default {} hooks) + $env.config = ($env.config | update hooks ($env.config.hooks | default {} env_change)) + $env.config = ($env.config | update hooks.env_change ($env.config.hooks.env_change | default [] PWD)) + $env.config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.env_change.PWD | append {|_, dir| + zoxide add -- $dir + })) +{%- endif %} } -# Initialize hook. -let-env PROMPT_COMMAND = ( - let prompt = (if ($nu.env | select PROMPT_COMMAND | empty?) { - if ($nu.config | select prompt | empty?) { '__zoxide_prompt' } { $nu.config.prompt } - } { $nu.env.PROMPT_COMMAND }); - if ($prompt | str contains '__zoxide_hook') { $prompt } { $'__zoxide_hook;($prompt)' } -) - -{%- when InitHook::Pwd %} -$'zoxide: PWD hooks are not supported on Nushell.(char nl)Use (char sq)zoxide init nushell --hook prompt(char sq) instead.(char nl)' -{%- endmatch %} +{%- endif %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # # Jump to a directory using only keywords. -def __zoxide_z [...rest:string] { - if (shells | where active == $true | get name) != filesystem { - if ($rest | length) > 1 { - $'zoxide: can only jump directories on filesystem(char nl)' - } { - cd $rest +def-env __zoxide_z [...rest:string] { + let arg0 = ($rest | append '~').0 + 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") + } + cd $path {%- if echo %} - pwd + echo $env.PWD {%- endif %} - } - } { - let arg0 = ($rest | append '~' | first 1); - if ($rest | length) <= 1 && ($arg0 == '-' || ($arg0 | path expand | path exists)) { - cd $arg0 - } { - cd $'(zoxide query --exclude (pwd) -- $rest | str trim -rc (char newline))' - } -{%- if echo %} - pwd -{%- endif %} - } } # Jump to a directory using interactive search. -def __zoxide_zi [...rest:string] { - if (shells | where active == $true | get name) != filesystem { - $'zoxide: can only jump directories on filesystem(char nl)' - } { - cd $'(zoxide query -i -- $rest | str trim -rc (char newline))' +def-env __zoxide_zi [...rest:string] { + cd $'(zoxide query --interactive -- $rest | str trim -r -c "\n")' {%- if echo %} - pwd + echo $env.PWD {%- endif %} - } } {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} @@ -102,9 +77,13 @@ alias {{cmd}}i = __zoxide_zi {%- endmatch %} {{ section }} -# To initialize zoxide, add this to your configuration (find it by running -# `config path` in Nushell): +# Add this to your env file (find it by running `$nu.env-path` in Nushell): # -# startup = ['zoxide init nushell --hook prompt | save ~/.zoxide.nu', 'source ~/.zoxide.nu'] +# zoxide init nushell | save -f ~/.zoxide.nu # -# Note: zoxide only supports Nushell v0.37.0 and above. +# 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.73.0 and above. diff --git a/templates/posix.txt b/templates/posix.txt index 3ac9878..ca4b53f 100644 --- a/templates/posix.txt +++ b/templates/posix.txt @@ -7,7 +7,9 @@ # pwd based on the value of _ZO_RESOLVE_SYMLINKS. __zoxide_pwd() { -{%- if resolve_symlinks %} +{%- if cfg!(windows) %} + \command cygpath -w "$(\builtin pwd -P)" +{%- else if resolve_symlinks %} \command pwd -P {%- else %} \command pwd -L @@ -47,8 +49,7 @@ fi {%- endmatch %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # # Jump to a directory using only keywords. @@ -73,30 +74,22 @@ __zoxide_z() { # Jump to a directory using interactive search. __zoxide_zi() { - __zoxide_result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${__zoxide_result}" + __zoxide_result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${__zoxide_result}" } {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} {%- when Some with (cmd) %} -# Remove definitions. -__zoxide_unset() { - \command unset -f "$@" >/dev/null 2>&1 - \command unset -v "$@" >/dev/null 2>&1 - # shellcheck disable=SC1001 - \command unalias "$@" >/dev/null 2>&1 || \: -} - -__zoxide_unset '{{cmd}}' +\command unalias {{cmd}} >/dev/null 2>&1 || \true {{cmd}}() { __zoxide_z "$@" } -__zoxide_unset '{{cmd}}i' +\command unalias {{cmd}}i >/dev/null 2>&1 || \true {{cmd}}i() { __zoxide_zi "$@" } diff --git a/templates/powershell.txt b/templates/powershell.txt index 4b7e51a..4a23246 100644 --- a/templates/powershell.txt +++ b/templates/powershell.txt @@ -5,8 +5,20 @@ # Utility functions for zoxide. # +# Call zoxide binary, returning the output as UTF-8. +function global:__zoxide_bin { + $encoding = [Console]::OutputEncoding + try { + [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new() + $result = zoxide @args + return $result + } finally { + [Console]::OutputEncoding = $encoding + } +} + # pwd based on zoxide's format. -function __zoxide_pwd { +function global:__zoxide_pwd { $cwd = Get-Location if ($cwd.Provider.Name -eq "FileSystem") { $cwd.ProviderPath @@ -14,11 +26,19 @@ function __zoxide_pwd { } # cd + custom logic based on the value of _ZO_ECHO. -function __zoxide_cd($dir, $literal) { +function global:__zoxide_cd($dir, $literal) { $dir = if ($literal) { Set-Location -LiteralPath $dir -Passthru -ErrorAction Stop } else { - Set-Location -Path $dir -Passthru -ErrorAction Stop + if ($dir -eq '-' -and ($PSVersionTable.PSVersion -lt 6.1)) { + Write-Error "cd - is not supported below PowerShell 6.1. Please upgrade your version of PowerShell." + } + elseif ($dir -eq '+' -and ($PSVersionTable.PSVersion -lt 6.2)) { + Write-Error "cd + is not supported below PowerShell 6.2. Please upgrade your version of PowerShell." + } + else { + Set-Location -Path $dir -Passthru -ErrorAction Stop + } } {%- if echo %} Write-Output $dir.Path @@ -29,66 +49,73 @@ function __zoxide_cd($dir, $literal) { # Hook configuration for zoxide. # +{% if hook == InitHook::None -%} +{{ not_configured }} + +{%- else -%} +{#- + Initialize $__zoxide_hooked if it does not exist. Removing this will cause an + unset variable error in StrictMode. +-#} +{%- if hook == InitHook::Prompt -%} # Hook to add new entries to the database. -function __zoxide_hook { +function global:__zoxide_hook { $result = __zoxide_pwd if ($null -ne $result) { zoxide add -- $result } } +{%- else if hook == InitHook::Pwd -%} +# Hook to add new entries to the database. +$global:__zoxide_oldpwd = __zoxide_pwd +function global:__zoxide_hook { + $result = __zoxide_pwd + if ($result -ne $global:__zoxide_oldpwd) { + if ($null -ne $result) { + zoxide add -- $result + } + $global:__zoxide_oldpwd = $result + } +} +{%- endif %} # Initialize hook. -{# Initialize $__zoxide_hooked if it does not exist. Removing this will cause - # an unset variable error in StrictMode. #} -$__zoxide_hooked = (Get-Variable __zoxide_hooked -ValueOnly -ErrorAction SilentlyContinue) -if ($__zoxide_hooked -ne 1) { - $__zoxide_hooked = 1 -{%- match hook %} -{%- when InitHook::None %} - {{ not_configured }} -{%- when InitHook::Prompt %} - $prompt_old = $function:prompt - function prompt { - $null = __zoxide_hook - & $prompt_old - } -{%- when InitHook::Pwd %} - if ($PSVersionTable.PSVersion.Major -ge 6) { - $ExecutionContext.InvokeCommand.LocationChangedAction = { - $null = __zoxide_hook +$global:__zoxide_hooked = (Get-Variable __zoxide_hooked -ErrorAction SilentlyContinue -ValueOnly) +if ($global:__zoxide_hooked -ne 1) { + $global:__zoxide_hooked = 1 + $global:__zoxide_prompt_old = $function:prompt + + function global:prompt { + if ($null -ne $__zoxide_prompt_old) { + & $__zoxide_prompt_old } + $null = __zoxide_hook } - else { - Write-Error ("`n" + - "zoxide: PWD hooks are not supported below powershell 6.`n" + - " Use 'zoxide init powershell --hook prompt' instead.") - } -{%- endmatch %} } +{%- endif %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # # Jump to a directory using only keywords. -function __zoxide_z { +function global:__zoxide_z { if ($args.Length -eq 0) { __zoxide_cd ~ $true } - elseif ( - $args.Length -eq 1 -and - (($args[0] -eq '-' -or $args[0] -eq '+') -or (Test-Path $args[0] -PathType Container)) - ) { + elseif ($args.Length -eq 1 -and ($args[0] -eq '-' -or $args[0] -eq '+')) { __zoxide_cd $args[0] $false } + elseif ($args.Length -eq 1 -and (Test-Path $args[0] -PathType Container)) { + __zoxide_cd $args[0] $true + } else { $result = __zoxide_pwd if ($null -ne $result) { - $result = zoxide query --exclude $result -- @args + $result = __zoxide_bin query --exclude $result -- @args } else { - $result = zoxide query -- @args + $result = __zoxide_bin query -- @args } if ($LASTEXITCODE -eq 0) { __zoxide_cd $result $true @@ -97,15 +124,15 @@ function __zoxide_z { } # Jump to a directory using interactive search. -function __zoxide_zi { - $result = zoxide query -i -- @args +function global:__zoxide_zi { + $result = __zoxide_bin query -i -- @args if ($LASTEXITCODE -eq 0) { __zoxide_cd $result $true } } {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} @@ -124,4 +151,4 @@ Set-Alias -Name {{cmd}}i -Value __zoxide_zi -Option AllScope -Scope Global -Forc # To initialize zoxide, add this to your configuration (find it by running # `echo $profile` in PowerShell): # -# Invoke-Expression (& { $hook = if ($PSVersionTable.PSVersion.Major -ge 6) { 'pwd' } else { 'prompt' } (zoxide init powershell --hook $hook | Out-String) }) +# Invoke-Expression (& { (zoxide init powershell | Out-String) }) diff --git a/templates/xonsh.txt b/templates/xonsh.txt index 74a241a..07baee5 100644 --- a/templates/xonsh.txt +++ b/templates/xonsh.txt @@ -108,8 +108,7 @@ if "__zoxide_hook" not in globals(): {% endif %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # @@ -157,7 +156,7 @@ def __zoxide_zi(args: typing.List[str]) -> None: {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} diff --git a/templates/zsh.txt b/templates/zsh.txt index bc585f4..20ddd5c 100644 --- a/templates/zsh.txt +++ b/templates/zsh.txt @@ -7,7 +7,9 @@ # pwd based on the value of _ZO_RESOLVE_SYMLINKS. function __zoxide_pwd() { -{%- if resolve_symlinks %} +{%- if cfg!(windows) %} + \command cygpath -w "$(\builtin pwd -P)" +{%- else if resolve_symlinks %} \builtin pwd -P {%- else %} \builtin pwd -L @@ -30,7 +32,8 @@ function __zoxide_cd() { {% else -%} # Hook to add new entries to the database. function __zoxide_hook() { - \command zoxide add -- "$(__zoxide_pwd || \builtin true)" + # shellcheck disable=SC2312 + \command zoxide add -- "$(__zoxide_pwd)" } # Initialize hook. @@ -46,8 +49,7 @@ fi {%- endif %} {{ section }} -# When using zoxide with --no-aliases, alias these internal functions as -# desired. +# When using zoxide with --no-cmd, alias these internal functions as desired. # __zoxide_z_prefix='z#' @@ -57,23 +59,16 @@ function __zoxide_z() { # shellcheck disable=SC2199 if [[ "$#" -eq 0 ]]; then __zoxide_cd ~ - elif [[ "$#" -eq 1 ]] && [[ "$1" = '-' ]]; then - if [[ -n "${OLDPWD}" ]]; then - __zoxide_cd "${OLDPWD}" - else - # shellcheck disable=SC2016 - \builtin printf 'zoxide: $OLDPWD is not set' - return 1 - fi - elif [[ "$#" -eq 1 ]] && [[ -d "$1" ]]; then + elif [[ "$#" -eq 1 ]] && { [[ -d "$1" ]] || [[ "$1" = '-' ]] || [[ "$1" =~ ^[-+][0-9]$ ]]; }; then __zoxide_cd "$1" - elif [[ "$@[-1]" == "${__zoxide_z_prefix}"* ]]; then + elif [[ "$@[-1]" == "${__zoxide_z_prefix}"?* ]]; then # shellcheck disable=SC2124 \builtin local result="${@[-1]}" __zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}" else \builtin local result - result="$(\command zoxide query --exclude "$(__zoxide_pwd || \builtin true)" -- "$@")" && + # shellcheck disable=SC2312 + result="$(\command zoxide query --exclude "$(__zoxide_pwd)" -- "$@")" && __zoxide_cd "${result}" fi } @@ -81,69 +76,44 @@ function __zoxide_z() { # Jump to a directory using interactive search. function __zoxide_zi() { \builtin local result - result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${result}" + result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${result}" } +# 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 0 + + if [[ "{{ "${#words[@]}" }}" -eq 2 ]]; then + _files -/ + elif [[ "${words[-1]}" == '' ]] && [[ "${words[-2]}" != "${__zoxide_z_prefix}"?* ]]; then + \builtin local result + # shellcheck disable=SC2086,SC2312 + if result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- ${words[2,-1]})"; then + result="${__zoxide_z_prefix}${result}" + # shellcheck disable=SC2296 + compadd -Q "${(q-)result}" + fi + \builtin printf '\e[5n' + fi + return 0 + } + + \builtin bindkey '\e[0n' 'reset-prompt' + [[ "${+functions[compdef]}" -ne 0 ]] && \compdef __zoxide_z_complete __zoxide_z +fi + {{ section }} -# Convenient aliases for zoxide. Disable these using --no-aliases. +# Commands for zoxide. Disable these using --no-cmd. # {%- match cmd %} {%- when Some with (cmd) %} -# Remove definitions. -function __zoxide_unset() { - \builtin unalias "$@" &>/dev/null || \builtin true - \builtin unfunction "$@" &>/dev/null || \builtin true - \builtin unset "$@" &>/dev/null -} - -__zoxide_unset {{cmd}} -function {{cmd}}() { - __zoxide_z "$@" -} - -__zoxide_unset {{cmd}}i -function {{cmd}}i() { - __zoxide_zi "$@" -} - -if [[ -o zle ]]; then - __zoxide_unset __zoxide_z_complete - function __zoxide_z_complete() { - # Only show completions when the cursor is at the end of the line. - # shellcheck disable=SC2154 - [[ "{{ "${#words[@]}" }}" -eq "${CURRENT}" ]] || return - - if [[ "{{ "${#words[@]}" }}" -eq 2 ]]; then - _files -/ - elif [[ "${words[-1]}" == '' ]]; then - \builtin local result - # shellcheck disable=SC2086 - if result="$(\command zoxide query -i -- ${words[2,-1]})"; then - __zoxide_result="${result}" - else - __zoxide_result='' - fi - \builtin printf '\e[5n' - fi - } - - __zoxide_unset __zoxide_z_complete_helper - function __zoxide_z_complete_helper() { - \builtin local result="${__zoxide_z_prefix}${__zoxide_result}" - # shellcheck disable=SC2296 - [[ -n "${__zoxide_result}" ]] && LBUFFER="${LBUFFER}${(q-)result}" - \builtin zle reset-prompt - } - - \builtin zle -N __zoxide_z_complete_helper - \builtin bindkey "\e[0n" __zoxide_z_complete_helper - if [[ "${+functions[compdef]}" -ne 0 ]]; then - \compdef -d {{cmd}} - \compdef __zoxide_z_complete {{cmd}} - fi -fi +\builtin alias {{cmd}}=__zoxide_z +\builtin alias {{cmd}}i=__zoxide_zi {%- when None %} diff --git a/tests/completions.rs b/tests/completions.rs index 881242c..50d807c 100644 --- a/tests/completions.rs +++ b/tests/completions.rs @@ -6,12 +6,17 @@ 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 +26,7 @@ fn completions_fish() { Command::new("fish") .env("HOME", tempdir) - .args(&["--command", source, "--private"]) + .args(["--command", source, "--private"]) .assert() .success() .stdout("") @@ -32,7 +37,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 +55,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 deleted file mode 100644 index b77af24..0000000 --- a/xtask/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "xtask" -version = "0.1.0" -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" diff --git a/xtask/src/main.rs b/xtask/src/main.rs deleted file mode 100644 index 63df538..0000000 --- a/xtask/src/main.rs +++ /dev/null @@ -1,154 +0,0 @@ -use anyhow::{bail, Context, Result}; -use clap::Parser; -use ignore::Walk; - -use std::env; -use std::ffi::OsStr; -use std::path::PathBuf; -use std::process::{self, Command}; - -fn main() -> Result<()> { - let nix_enabled = enable_nix(); - - let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let dir = dir.parent().with_context(|| format!("could not find workspace root: {}", dir.display()))?; - env::set_current_dir(dir).with_context(|| format!("could not set current directory: {}", dir.display()))?; - - let app = App::parse(); - match app { - App::CI => run_ci(nix_enabled)?, - App::Fmt { check } => run_fmt(nix_enabled, check)?, - App::Lint => run_lint(nix_enabled)?, - App::Test { name } => run_tests(nix_enabled, &name)?, - } - - Ok(()) -} - -#[derive(Parser)] -enum App { - CI, - Fmt { - #[clap(long)] - check: bool, - }, - Lint, - Test { - #[clap(default_value = "")] - name: String, - }, -} - -trait CommandExt { - fn _run(self) -> Result<()>; -} - -impl CommandExt for &mut Command { - fn _run(self) -> Result<()> { - println!(">>> {:?}", self); - let status = self.status().with_context(|| format!("command failed to start: {:?}", self))?; - if !status.success() { - bail!("command failed: {:?} with status: {:?}", self, status); - } - Ok(()) - } -} - -fn run_ci(nix_enabled: bool) -> Result<()> { - let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] }; - Command::new("cargo").args(&["check", "--all-features"]).args(color)._run()?; - - run_fmt(nix_enabled, true)?; - run_lint(nix_enabled)?; - run_tests(nix_enabled, "") -} - -fn run_fmt(nix_enabled: bool, check: bool) -> Result<()> { - // Run cargo-fmt. - let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] }; - let check_args: &[&str] = if check { &["--check", "--files-with-diff"] } else { &[] }; - Command::new("cargo").args(&["fmt", "--all", "--"]).args(color).args(check_args)._run()?; - - // Run nixfmt. - if nix_enabled { - for result in Walk::new("./") { - let entry = result.unwrap(); - let path = entry.path(); - if path.is_file() && path.extension() == Some(OsStr::new("nix")) { - let check_args: &[&str] = if check { &["--check"] } else { &[] }; - Command::new("nixfmt").args(check_args).arg("--").arg(path)._run()?; - } - } - } - - Ok(()) -} - -fn run_lint(nix_enabled: bool) -> Result<()> { - // Run cargo-clippy. - let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] }; - Command::new("cargo") - .args(&["clippy", "--all-features", "--all-targets"]) - .args(color) - .args(&["--", "-Dwarnings"]) - ._run()?; - - if nix_enabled { - // Run cargo-audit. - let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] }; - Command::new("cargo").args(&["audit", "--deny=warnings"]).args(color)._run()?; - - // Run markdownlint. - for result in Walk::new("./") { - let entry = result.unwrap(); - let path = entry.path(); - if path.is_file() && path.extension() == Some(OsStr::new("md")) { - Command::new("markdownlint").arg(path)._run()?; - } - } - - // Run mandoc with linting enabled. - for result in Walk::new("./man/") { - 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()?; - } - } - } - - Ok(()) -} - -fn run_tests(nix_enabled: bool, name: &str) -> Result<()> { - let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] }; - let features: &[&str] = if nix_enabled { &["--all-features"] } else { &[] }; - Command::new("cargo").args(&["test", "--no-fail-fast", "--workspace"]).args(color).args(features).arg(name)._run() -} - -fn is_ci() -> bool { - env::var_os("CI").is_some() -} - -fn enable_nix() -> bool { - let nix_supported = cfg!(any(target_os = "linux", target_os = "macos")); - if !nix_supported { - return false; - } - let nix_enabled = env::var_os("IN_NIX_SHELL").unwrap_or_default() == "pure"; - if nix_enabled { - return true; - } - let nix_detected = Command::new("nix-shell").arg("--version").status().map(|s| s.success()).unwrap_or(false); - if !nix_detected { - return false; - } - - println!("Detected Nix in environment, re-running in Nix."); - let args = env::args(); - let cmd = shell_words::join(args); - let mut nix_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - nix_path.push("../shell.nix"); - let status = Command::new("nix-shell").args(&["--pure", "--run", &cmd, "--"]).arg(nix_path).status().unwrap(); - process::exit(status.code().unwrap_or(1)); -}