├── .cargo └── config.toml ├── .deepsource.toml ├── .envrc ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── no-response.yml │ ├── release.yml │ └── winget.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── README.md ├── build.rs ├── contrib ├── completions │ ├── README.md │ ├── _zoxide │ ├── _zoxide.ps1 │ ├── zoxide.bash │ ├── zoxide.elv │ ├── zoxide.fish │ ├── zoxide.nu │ └── zoxide.ts ├── tutorial.webp ├── warp-packs-green.png └── warp.png ├── init.fish ├── install.sh ├── justfile ├── man └── man1 │ ├── zoxide-add.1 │ ├── zoxide-import.1 │ ├── zoxide-init.1 │ ├── zoxide-query.1 │ ├── zoxide-remove.1 │ └── zoxide.1 ├── rustfmt.toml ├── shell.nix ├── src ├── cmd │ ├── add.rs │ ├── cmd.rs │ ├── edit.rs │ ├── import.rs │ ├── init.rs │ ├── mod.rs │ ├── query.rs │ └── remove.rs ├── config.rs ├── db │ ├── dir.rs │ ├── mod.rs │ └── stream.rs ├── error.rs ├── main.rs ├── shell.rs └── util.rs ├── templates ├── bash.txt ├── elvish.txt ├── fish.txt ├── nushell.txt ├── posix.txt ├── powershell.txt ├── tcsh.txt ├── xonsh.txt └── zsh.txt ├── tests └── completions.rs └── zoxide.plugin.zsh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | 4 | # On Windows MSVC, statically link the C runtime so that the resulting EXE does 5 | # not depend on the vcruntime DLL. 6 | [target.'cfg(all(windows, target_env = "msvc"))'] 7 | rustflags = ["-C", "target-feature=+crt-static"] 8 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "rust" 5 | 6 | [analyzers.meta] 7 | msrv = "stable" 8 | 9 | [[analyzers]] 10 | name = "shell" -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /contrib/completions/* eol=lf linguist-generated=true text 2 | /contrib/completions/README.md -eol -linguist-generated -text 3 | /init.fish eol=lf text 4 | /templates/*.txt eol=lf text 5 | /zoxide.plugin.zsh eol=lf text 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | 98ajeet@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | workflow_dispatch: 7 | env: 8 | CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} 9 | CARGO_INCREMENTAL: 0 10 | CARGO_TERM_COLOR: always 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 13 | cancel-in-progress: true 14 | permissions: 15 | contents: read 16 | jobs: 17 | ci: 18 | name: ${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest] 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - uses: actions-rs/toolchain@v1 28 | if: ${{ matrix.os == 'windows-latest' }} 29 | with: 30 | components: clippy 31 | profile: minimal 32 | toolchain: stable 33 | - uses: actions-rs/toolchain@v1 34 | if: ${{ matrix.os == 'windows-latest' }} 35 | with: 36 | components: rustfmt 37 | profile: minimal 38 | toolchain: nightly 39 | - uses: cachix/install-nix-action@v31 40 | if: ${{ matrix.os != 'windows-latest' }} 41 | with: 42 | nix_path: nixpkgs=channel:nixos-unstable 43 | - uses: cachix/cachix-action@v16 44 | if: ${{ matrix.os != 'windows-latest' && env.CACHIX_AUTH_TOKEN != '' }} 45 | with: 46 | authToken: ${{ env.CACHIX_AUTH_TOKEN }} 47 | name: zoxide 48 | - name: Setup cache 49 | uses: Swatinem/rust-cache@v2.7.8 50 | with: 51 | key: ${{ matrix.os }} 52 | - name: Install just 53 | uses: taiki-e/install-action@v2 54 | with: 55 | tool: just 56 | - name: Run lints + tests 57 | run: just lint test 58 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | name: no-response 2 | on: 3 | issue_comment: 4 | types: [created] 5 | schedule: 6 | - cron: "0 0 * * *" # daily at 00:00 7 | jobs: 8 | no-response: 9 | if: github.repository == 'ajeetdsouza/zoxide' 10 | permissions: 11 | issues: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: lee-dohm/no-response@v0.5.0 15 | with: 16 | token: ${{ github.token }} 17 | daysUntilClose: 30 18 | responseRequiredLabel: waiting-for-response 19 | closeComment: > 20 | This issue has been automatically closed due to inactivity. If you feel this is still relevant, please comment here or create a fresh issue. 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | workflow_dispatch: 7 | env: 8 | CARGO_INCREMENTAL: 0 9 | permissions: 10 | contents: write 11 | jobs: 12 | release: 13 | name: ${{ matrix.target }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | target: x86_64-unknown-linux-musl 21 | deb: true 22 | - os: ubuntu-latest 23 | target: arm-unknown-linux-musleabihf 24 | - os: ubuntu-latest 25 | target: armv7-unknown-linux-musleabihf 26 | deb: true 27 | - os: ubuntu-latest 28 | target: aarch64-unknown-linux-musl 29 | deb: true 30 | - os: ubuntu-latest 31 | target: i686-unknown-linux-musl 32 | deb: true 33 | - os: ubuntu-latest 34 | target: aarch64-linux-android 35 | - os: macos-latest 36 | target: x86_64-apple-darwin 37 | - os: macos-latest 38 | target: aarch64-apple-darwin 39 | - os: windows-latest 40 | target: x86_64-pc-windows-msvc 41 | - os: windows-latest 42 | target: aarch64-pc-windows-msvc 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | - name: Get version 49 | id: get_version 50 | uses: SebRollen/toml-action@v1.2.0 51 | with: 52 | file: Cargo.toml 53 | field: package.version 54 | - name: Install Rust 55 | uses: actions-rs/toolchain@v1 56 | with: 57 | toolchain: stable 58 | profile: minimal 59 | override: true 60 | target: ${{ matrix.target }} 61 | - name: Setup cache 62 | uses: Swatinem/rust-cache@v2.7.8 63 | with: 64 | key: ${{ matrix.target }} 65 | - name: Install cross 66 | if: ${{ runner.os == 'Linux' }} 67 | uses: actions-rs/cargo@v1 68 | with: 69 | command: install 70 | args: --color=always --git=https://github.com/cross-rs/cross.git --locked --rev=02bf930e0cb0c6f1beffece0788f3932ecb2c7eb --verbose cross 71 | - name: Build binary 72 | uses: actions-rs/cargo@v1 73 | with: 74 | command: build 75 | args: --release --locked --target=${{ matrix.target }} --color=always --verbose 76 | use-cross: ${{ runner.os == 'Linux' }} 77 | - name: Install cargo-deb 78 | if: ${{ matrix.deb == true }} 79 | uses: actions-rs/install@v0.1 80 | with: 81 | crate: cargo-deb 82 | - name: Build deb 83 | if: ${{ matrix.deb == true }} 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: deb 87 | args: --no-build --no-strip --output=. --target=${{ matrix.target }} 88 | - name: Package (*nix) 89 | if: runner.os != 'Windows' 90 | run: | 91 | tar -cv CHANGELOG.md LICENSE README.md man/ \ 92 | -C contrib/ completions/ -C ../ \ 93 | -C target/${{ matrix.target }}/release/ zoxide | 94 | gzip --best > \ 95 | zoxide-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.tar.gz 96 | - name: Package (Windows) 97 | if: runner.os == 'Windows' 98 | run: | 99 | 7z a zoxide-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.zip ` 100 | CHANGELOG.md LICENSE README.md ./man/ ./contrib/completions/ ` 101 | ./target/${{ matrix.target }}/release/zoxide.exe 102 | - name: Upload artifact 103 | uses: actions/upload-artifact@v4 104 | with: 105 | name: ${{ matrix.target }} 106 | path: | 107 | *.deb 108 | *.tar.gz 109 | *.zip 110 | - name: Create release 111 | if: | 112 | github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'chore(release)') 113 | uses: softprops/action-gh-release@v2 114 | with: 115 | draft: true 116 | files: | 117 | *.deb 118 | *.tar.gz 119 | *.zip 120 | name: ${{ steps.get_version.outputs.value }} 121 | tag_name: "" 122 | -------------------------------------------------------------------------------- /.github/workflows/winget.yml: -------------------------------------------------------------------------------- 1 | name: winget 2 | on: 3 | release: 4 | types: [released] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: vedantmgoyal2009/winget-releaser@v2 10 | with: 11 | identifier: ajeetdsouza.zoxide 12 | installers-regex: '-pc-windows-msvc\.zip$' 13 | token: ${{ secrets.WINGET_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Custom ### 2 | .vscode/ 3 | 4 | ### Rust ### 5 | # Compiled files and executables 6 | debug/ 7 | target/ 8 | target_nix/ 9 | 10 | # Backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | ### Python ### 14 | .mypy_cache/ 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Ajeet D'Souza <98ajeet@gmail.com>"] 3 | categories = ["command-line-utilities", "filesystem"] 4 | description = "A smarter cd command for your terminal" 5 | edition = "2024" 6 | homepage = "https://github.com/ajeetdsouza/zoxide" 7 | keywords = ["cli", "filesystem", "shell", "tool", "utility"] 8 | license = "MIT" 9 | name = "zoxide" 10 | readme = "README.md" 11 | repository = "https://github.com/ajeetdsouza/zoxide" 12 | rust-version = "1.85.0" 13 | version = "0.9.8" 14 | 15 | [badges] 16 | maintenance = { status = "actively-developed" } 17 | 18 | [dependencies] 19 | anyhow = "1.0.32" 20 | askama = { version = "0.14.0", default-features = false, features = [ 21 | "derive", 22 | "std", 23 | ] } 24 | bincode = "1.3.1" 25 | clap = { version = "4.3.0", features = ["derive"] } 26 | color-print = "0.3.4" 27 | dirs = "6.0.0" 28 | dunce = "1.0.1" 29 | fastrand = "2.0.0" 30 | glob = "0.3.0" 31 | ouroboros = "0.18.3" 32 | serde = { version = "1.0.116", features = ["derive"] } 33 | 34 | [target.'cfg(unix)'.dependencies] 35 | nix = { version = "0.30.1", default-features = false, features = [ 36 | "fs", 37 | "user", 38 | ] } 39 | 40 | [target.'cfg(windows)'.dependencies] 41 | which = "7.0.3" 42 | 43 | [build-dependencies] 44 | clap = { version = "4.3.0", features = ["derive"] } 45 | clap_complete = "4.5.50" 46 | clap_complete_fig = "4.5.2" 47 | clap_complete_nushell = "4.5.5" 48 | color-print = "0.3.4" 49 | 50 | [dev-dependencies] 51 | assert_cmd = "2.0.0" 52 | rstest = { version = "0.25.0", default-features = false } 53 | rstest_reuse = "0.7.0" 54 | tempfile = "=3.15.0" 55 | 56 | [features] 57 | default = [] 58 | nix-dev = [] 59 | 60 | [profile.release] 61 | codegen-units = 1 62 | debug = 0 63 | lto = true 64 | strip = true 65 | 66 | [package.metadata.deb] 67 | assets = [ 68 | [ 69 | "target/release/zoxide", 70 | "usr/bin/", 71 | "755", 72 | ], 73 | [ 74 | "contrib/completions/zoxide.bash", 75 | "usr/share/bash-completion/completions/zoxide", 76 | "644", 77 | ], 78 | [ 79 | "contrib/completions/zoxide.fish", 80 | "usr/share/fish/vendor_completions.d/", 81 | "664", 82 | ], 83 | [ 84 | "contrib/completions/_zoxide", 85 | "usr/share/zsh/vendor-completions/", 86 | "644", 87 | ], 88 | [ 89 | "man/man1/*", 90 | "usr/share/man/man1/", 91 | "644", 92 | ], 93 | [ 94 | "README.md", 95 | "usr/share/doc/zoxide/", 96 | "644", 97 | ], 98 | [ 99 | "CHANGELOG.md", 100 | "usr/share/doc/zoxide/", 101 | "644", 102 | ], 103 | [ 104 | "LICENSE", 105 | "usr/share/doc/zoxide/", 106 | "644", 107 | ], 108 | ] 109 | extended-description = """\ 110 | zoxide is a smarter cd command, inspired by z and autojump. It remembers which \ 111 | directories you use most frequently, so you can "jump" to them in just a few \ 112 | keystrokes.""" 113 | priority = "optional" 114 | section = "utils" 115 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = ["CARGO_INCREMENTAL"] 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ajeet D'Souza 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #[path = "src/cmd/cmd.rs"] 2 | mod cmd; 3 | 4 | use std::{env, io}; 5 | 6 | use clap::CommandFactory as _; 7 | use clap_complete::shells::{Bash, Elvish, Fish, PowerShell, Zsh}; 8 | use clap_complete_fig::Fig; 9 | use clap_complete_nushell::Nushell; 10 | use cmd::Cmd; 11 | 12 | fn main() -> io::Result<()> { 13 | // Since we are generating completions in the package directory, we need to 14 | // set this so that Cargo doesn't rebuild every time. 15 | println!("cargo:rerun-if-changed=build.rs"); 16 | println!("cargo:rerun-if-changed=src/"); 17 | println!("cargo:rerun-if-changed=templates/"); 18 | println!("cargo:rerun-if-changed=tests/"); 19 | generate_completions() 20 | } 21 | 22 | fn generate_completions() -> io::Result<()> { 23 | const BIN_NAME: &str = env!("CARGO_PKG_NAME"); 24 | const OUT_DIR: &str = "contrib/completions"; 25 | let cmd = &mut Cmd::command(); 26 | 27 | clap_complete::generate_to(Bash, cmd, BIN_NAME, OUT_DIR)?; 28 | clap_complete::generate_to(Elvish, cmd, BIN_NAME, OUT_DIR)?; 29 | clap_complete::generate_to(Fig, cmd, BIN_NAME, OUT_DIR)?; 30 | clap_complete::generate_to(Fish, cmd, BIN_NAME, OUT_DIR)?; 31 | clap_complete::generate_to(Nushell, cmd, BIN_NAME, OUT_DIR)?; 32 | clap_complete::generate_to(PowerShell, cmd, BIN_NAME, OUT_DIR)?; 33 | clap_complete::generate_to(Zsh, cmd, BIN_NAME, OUT_DIR)?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /contrib/completions/README.md: -------------------------------------------------------------------------------- 1 | # completions 2 | 3 | Shell completions for zoxide, generated by [clap]. Since clap is in beta, these 4 | completions should not be treated as stable. 5 | 6 | [clap]: https://github.com/clap-rs/clap 7 | -------------------------------------------------------------------------------- /contrib/completions/_zoxide: -------------------------------------------------------------------------------- 1 | #compdef zoxide 2 | 3 | autoload -U is-at-least 4 | 5 | _zoxide() { 6 | typeset -A opt_args 7 | typeset -a _arguments_options 8 | local ret=1 9 | 10 | if is-at-least 5.2; then 11 | _arguments_options=(-s -S -C) 12 | else 13 | _arguments_options=(-s -C) 14 | fi 15 | 16 | local context curcontext="$curcontext" state line 17 | _arguments "${_arguments_options[@]}" : \ 18 | '-h[Print help]' \ 19 | '--help[Print help]' \ 20 | '-V[Print version]' \ 21 | '--version[Print version]' \ 22 | ":: :_zoxide_commands" \ 23 | "*::: :->zoxide" \ 24 | && ret=0 25 | case $state in 26 | (zoxide) 27 | words=($line[1] "${words[@]}") 28 | (( CURRENT += 1 )) 29 | curcontext="${curcontext%:*:*}:zoxide-command-$line[1]:" 30 | case $line[1] in 31 | (add) 32 | _arguments "${_arguments_options[@]}" : \ 33 | '-s+[The rank to increment the entry if it exists or initialize it with if it doesn'\''t]:SCORE:_default' \ 34 | '--score=[The rank to increment the entry if it exists or initialize it with if it doesn'\''t]:SCORE:_default' \ 35 | '-h[Print help]' \ 36 | '--help[Print help]' \ 37 | '-V[Print version]' \ 38 | '--version[Print version]' \ 39 | '*::paths:_files -/' \ 40 | && ret=0 41 | ;; 42 | (edit) 43 | _arguments "${_arguments_options[@]}" : \ 44 | '-h[Print help]' \ 45 | '--help[Print help]' \ 46 | '-V[Print version]' \ 47 | '--version[Print version]' \ 48 | ":: :_zoxide__edit_commands" \ 49 | "*::: :->edit" \ 50 | && ret=0 51 | 52 | case $state in 53 | (edit) 54 | words=($line[1] "${words[@]}") 55 | (( CURRENT += 1 )) 56 | curcontext="${curcontext%:*:*}:zoxide-edit-command-$line[1]:" 57 | case $line[1] in 58 | (decrement) 59 | _arguments "${_arguments_options[@]}" : \ 60 | '-h[Print help]' \ 61 | '--help[Print help]' \ 62 | '-V[Print version]' \ 63 | '--version[Print version]' \ 64 | ':path:_default' \ 65 | && ret=0 66 | ;; 67 | (delete) 68 | _arguments "${_arguments_options[@]}" : \ 69 | '-h[Print help]' \ 70 | '--help[Print help]' \ 71 | '-V[Print version]' \ 72 | '--version[Print version]' \ 73 | ':path:_default' \ 74 | && ret=0 75 | ;; 76 | (increment) 77 | _arguments "${_arguments_options[@]}" : \ 78 | '-h[Print help]' \ 79 | '--help[Print help]' \ 80 | '-V[Print version]' \ 81 | '--version[Print version]' \ 82 | ':path:_default' \ 83 | && ret=0 84 | ;; 85 | (reload) 86 | _arguments "${_arguments_options[@]}" : \ 87 | '-h[Print help]' \ 88 | '--help[Print help]' \ 89 | '-V[Print version]' \ 90 | '--version[Print version]' \ 91 | && ret=0 92 | ;; 93 | esac 94 | ;; 95 | esac 96 | ;; 97 | (import) 98 | _arguments "${_arguments_options[@]}" : \ 99 | '--from=[Application to import from]:FROM:(autojump z)' \ 100 | '--merge[Merge into existing database]' \ 101 | '-h[Print help]' \ 102 | '--help[Print help]' \ 103 | '-V[Print version]' \ 104 | '--version[Print version]' \ 105 | ':path:_files' \ 106 | && ret=0 107 | ;; 108 | (init) 109 | _arguments "${_arguments_options[@]}" : \ 110 | '--cmd=[Changes the prefix of the \`z\` and \`zi\` commands]:CMD:_default' \ 111 | '--hook=[Changes how often zoxide increments a directory'\''s score]:HOOK:(none prompt pwd)' \ 112 | '--no-cmd[Prevents zoxide from defining the \`z\` and \`zi\` commands]' \ 113 | '-h[Print help]' \ 114 | '--help[Print help]' \ 115 | '-V[Print version]' \ 116 | '--version[Print version]' \ 117 | ':shell:(bash elvish fish nushell posix powershell tcsh xonsh zsh)' \ 118 | && ret=0 119 | ;; 120 | (query) 121 | _arguments "${_arguments_options[@]}" : \ 122 | '--exclude=[Exclude the current directory]:path:_files -/' \ 123 | '-a[Show unavailable directories]' \ 124 | '--all[Show unavailable directories]' \ 125 | '(-l --list)-i[Use interactive selection]' \ 126 | '(-l --list)--interactive[Use interactive selection]' \ 127 | '(-i --interactive)-l[List all matching directories]' \ 128 | '(-i --interactive)--list[List all matching directories]' \ 129 | '-s[Print score with results]' \ 130 | '--score[Print score with results]' \ 131 | '-h[Print help]' \ 132 | '--help[Print help]' \ 133 | '-V[Print version]' \ 134 | '--version[Print version]' \ 135 | '*::keywords:_default' \ 136 | && ret=0 137 | ;; 138 | (remove) 139 | _arguments "${_arguments_options[@]}" : \ 140 | '-h[Print help]' \ 141 | '--help[Print help]' \ 142 | '-V[Print version]' \ 143 | '--version[Print version]' \ 144 | '*::paths:_files -/' \ 145 | && ret=0 146 | ;; 147 | esac 148 | ;; 149 | esac 150 | } 151 | 152 | (( $+functions[_zoxide_commands] )) || 153 | _zoxide_commands() { 154 | local commands; commands=( 155 | 'add:Add a new directory or increment its rank' \ 156 | 'edit:Edit the database' \ 157 | 'import:Import entries from another application' \ 158 | 'init:Generate shell configuration' \ 159 | 'query:Search for a directory in the database' \ 160 | 'remove:Remove a directory from the database' \ 161 | ) 162 | _describe -t commands 'zoxide commands' commands "$@" 163 | } 164 | (( $+functions[_zoxide__add_commands] )) || 165 | _zoxide__add_commands() { 166 | local commands; commands=() 167 | _describe -t commands 'zoxide add commands' commands "$@" 168 | } 169 | (( $+functions[_zoxide__edit_commands] )) || 170 | _zoxide__edit_commands() { 171 | local commands; commands=( 172 | 'decrement:' \ 173 | 'delete:' \ 174 | 'increment:' \ 175 | 'reload:' \ 176 | ) 177 | _describe -t commands 'zoxide edit commands' commands "$@" 178 | } 179 | (( $+functions[_zoxide__edit__decrement_commands] )) || 180 | _zoxide__edit__decrement_commands() { 181 | local commands; commands=() 182 | _describe -t commands 'zoxide edit decrement commands' commands "$@" 183 | } 184 | (( $+functions[_zoxide__edit__delete_commands] )) || 185 | _zoxide__edit__delete_commands() { 186 | local commands; commands=() 187 | _describe -t commands 'zoxide edit delete commands' commands "$@" 188 | } 189 | (( $+functions[_zoxide__edit__increment_commands] )) || 190 | _zoxide__edit__increment_commands() { 191 | local commands; commands=() 192 | _describe -t commands 'zoxide edit increment commands' commands "$@" 193 | } 194 | (( $+functions[_zoxide__edit__reload_commands] )) || 195 | _zoxide__edit__reload_commands() { 196 | local commands; commands=() 197 | _describe -t commands 'zoxide edit reload commands' commands "$@" 198 | } 199 | (( $+functions[_zoxide__import_commands] )) || 200 | _zoxide__import_commands() { 201 | local commands; commands=() 202 | _describe -t commands 'zoxide import commands' commands "$@" 203 | } 204 | (( $+functions[_zoxide__init_commands] )) || 205 | _zoxide__init_commands() { 206 | local commands; commands=() 207 | _describe -t commands 'zoxide init commands' commands "$@" 208 | } 209 | (( $+functions[_zoxide__query_commands] )) || 210 | _zoxide__query_commands() { 211 | local commands; commands=() 212 | _describe -t commands 'zoxide query commands' commands "$@" 213 | } 214 | (( $+functions[_zoxide__remove_commands] )) || 215 | _zoxide__remove_commands() { 216 | local commands; commands=() 217 | _describe -t commands 'zoxide remove commands' commands "$@" 218 | } 219 | 220 | if [ "$funcstack[1]" = "_zoxide" ]; then 221 | _zoxide "$@" 222 | else 223 | compdef _zoxide zoxide 224 | fi 225 | -------------------------------------------------------------------------------- /contrib/completions/_zoxide.ps1: -------------------------------------------------------------------------------- 1 | 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { 6 | param($wordToComplete, $commandAst, $cursorPosition) 7 | 8 | $commandElements = $commandAst.CommandElements 9 | $command = @( 10 | 'zoxide' 11 | for ($i = 1; $i -lt $commandElements.Count; $i++) { 12 | $element = $commandElements[$i] 13 | if ($element -isnot [StringConstantExpressionAst] -or 14 | $element.StringConstantType -ne [StringConstantType]::BareWord -or 15 | $element.Value.StartsWith('-') -or 16 | $element.Value -eq $wordToComplete) { 17 | break 18 | } 19 | $element.Value 20 | }) -join ';' 21 | 22 | $completions = @(switch ($command) { 23 | 'zoxide' { 24 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 25 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 26 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 27 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 28 | [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') 29 | [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database') 30 | [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') 31 | [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') 32 | [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') 33 | [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Remove a directory from the database') 34 | break 35 | } 36 | 'zoxide;add' { 37 | [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'The rank to increment the entry if it exists or initialize it with if it doesn''t') 38 | [CompletionResult]::new('--score', '--score', [CompletionResultType]::ParameterName, 'The rank to increment the entry if it exists or initialize it with if it doesn''t') 39 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 40 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 41 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 42 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 43 | break 44 | } 45 | 'zoxide;edit' { 46 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 47 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 48 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 49 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 50 | [CompletionResult]::new('decrement', 'decrement', [CompletionResultType]::ParameterValue, 'decrement') 51 | [CompletionResult]::new('delete', 'delete', [CompletionResultType]::ParameterValue, 'delete') 52 | [CompletionResult]::new('increment', 'increment', [CompletionResultType]::ParameterValue, 'increment') 53 | [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'reload') 54 | break 55 | } 56 | 'zoxide;edit;decrement' { 57 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 58 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 59 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 60 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 61 | break 62 | } 63 | 'zoxide;edit;delete' { 64 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 65 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 66 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 67 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 68 | break 69 | } 70 | 'zoxide;edit;increment' { 71 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 72 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 73 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 74 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 75 | break 76 | } 77 | 'zoxide;edit;reload' { 78 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 79 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 80 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 81 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 82 | break 83 | } 84 | 'zoxide;import' { 85 | [CompletionResult]::new('--from', '--from', [CompletionResultType]::ParameterName, 'Application to import from') 86 | [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') 87 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 88 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 89 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 90 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 91 | break 92 | } 93 | 'zoxide;init' { 94 | [CompletionResult]::new('--cmd', '--cmd', [CompletionResultType]::ParameterName, 'Changes the prefix of the `z` and `zi` commands') 95 | [CompletionResult]::new('--hook', '--hook', [CompletionResultType]::ParameterName, 'Changes how often zoxide increments a directory''s score') 96 | [CompletionResult]::new('--no-cmd', '--no-cmd', [CompletionResultType]::ParameterName, 'Prevents zoxide from defining the `z` and `zi` commands') 97 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 98 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 99 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 100 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 101 | break 102 | } 103 | 'zoxide;query' { 104 | [CompletionResult]::new('--exclude', '--exclude', [CompletionResultType]::ParameterName, 'Exclude the current directory') 105 | [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'Show unavailable directories') 106 | [CompletionResult]::new('--all', '--all', [CompletionResultType]::ParameterName, 'Show unavailable directories') 107 | [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Use interactive selection') 108 | [CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') 109 | [CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'List all matching directories') 110 | [CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'List all matching directories') 111 | [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Print score with results') 112 | [CompletionResult]::new('--score', '--score', [CompletionResultType]::ParameterName, 'Print score with results') 113 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 114 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 115 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 116 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 117 | break 118 | } 119 | 'zoxide;remove' { 120 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 121 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 122 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 123 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 124 | break 125 | } 126 | }) 127 | 128 | $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | 129 | Sort-Object -Property ListItemText 130 | } 131 | -------------------------------------------------------------------------------- /contrib/completions/zoxide.bash: -------------------------------------------------------------------------------- 1 | _zoxide() { 2 | local i cur prev opts cmd 3 | COMPREPLY=() 4 | if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then 5 | cur="$2" 6 | else 7 | cur="${COMP_WORDS[COMP_CWORD]}" 8 | fi 9 | prev="$3" 10 | cmd="" 11 | opts="" 12 | 13 | for i in "${COMP_WORDS[@]:0:COMP_CWORD}" 14 | do 15 | case "${cmd},${i}" in 16 | ",$1") 17 | cmd="zoxide" 18 | ;; 19 | zoxide,add) 20 | cmd="zoxide__add" 21 | ;; 22 | zoxide,edit) 23 | cmd="zoxide__edit" 24 | ;; 25 | zoxide,import) 26 | cmd="zoxide__import" 27 | ;; 28 | zoxide,init) 29 | cmd="zoxide__init" 30 | ;; 31 | zoxide,query) 32 | cmd="zoxide__query" 33 | ;; 34 | zoxide,remove) 35 | cmd="zoxide__remove" 36 | ;; 37 | zoxide__edit,decrement) 38 | cmd="zoxide__edit__decrement" 39 | ;; 40 | zoxide__edit,delete) 41 | cmd="zoxide__edit__delete" 42 | ;; 43 | zoxide__edit,increment) 44 | cmd="zoxide__edit__increment" 45 | ;; 46 | zoxide__edit,reload) 47 | cmd="zoxide__edit__reload" 48 | ;; 49 | *) 50 | ;; 51 | esac 52 | done 53 | 54 | case "${cmd}" in 55 | zoxide) 56 | opts="-h -V --help --version add edit import init query remove" 57 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 58 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 59 | return 0 60 | fi 61 | case "${prev}" in 62 | *) 63 | COMPREPLY=() 64 | ;; 65 | esac 66 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 67 | return 0 68 | ;; 69 | zoxide__add) 70 | opts="-s -h -V --score --help --version ..." 71 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 72 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 73 | return 0 74 | fi 75 | case "${prev}" in 76 | --score) 77 | COMPREPLY=($(compgen -f "${cur}")) 78 | return 0 79 | ;; 80 | -s) 81 | COMPREPLY=($(compgen -f "${cur}")) 82 | return 0 83 | ;; 84 | *) 85 | COMPREPLY=() 86 | ;; 87 | esac 88 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 89 | return 0 90 | ;; 91 | zoxide__edit) 92 | opts="-h -V --help --version decrement delete increment reload" 93 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 94 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 95 | return 0 96 | fi 97 | case "${prev}" in 98 | *) 99 | COMPREPLY=() 100 | ;; 101 | esac 102 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 103 | return 0 104 | ;; 105 | zoxide__edit__decrement) 106 | opts="-h -V --help --version " 107 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then 108 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 109 | return 0 110 | fi 111 | case "${prev}" in 112 | *) 113 | COMPREPLY=() 114 | ;; 115 | esac 116 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 117 | return 0 118 | ;; 119 | zoxide__edit__delete) 120 | opts="-h -V --help --version " 121 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then 122 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 123 | return 0 124 | fi 125 | case "${prev}" in 126 | *) 127 | COMPREPLY=() 128 | ;; 129 | esac 130 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 131 | return 0 132 | ;; 133 | zoxide__edit__increment) 134 | opts="-h -V --help --version " 135 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then 136 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 137 | return 0 138 | fi 139 | case "${prev}" in 140 | *) 141 | COMPREPLY=() 142 | ;; 143 | esac 144 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 145 | return 0 146 | ;; 147 | zoxide__edit__reload) 148 | opts="-h -V --help --version" 149 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then 150 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 151 | return 0 152 | fi 153 | case "${prev}" in 154 | *) 155 | COMPREPLY=() 156 | ;; 157 | esac 158 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 159 | return 0 160 | ;; 161 | zoxide__import) 162 | opts="-h -V --from --merge --help --version " 163 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 164 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 165 | return 0 166 | fi 167 | case "${prev}" in 168 | --from) 169 | COMPREPLY=($(compgen -W "autojump z" -- "${cur}")) 170 | return 0 171 | ;; 172 | *) 173 | COMPREPLY=() 174 | ;; 175 | esac 176 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 177 | return 0 178 | ;; 179 | zoxide__init) 180 | opts="-h -V --no-cmd --cmd --hook --help --version bash elvish fish nushell posix powershell tcsh xonsh zsh" 181 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 182 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 183 | return 0 184 | fi 185 | case "${prev}" in 186 | --cmd) 187 | COMPREPLY=($(compgen -f "${cur}")) 188 | return 0 189 | ;; 190 | --hook) 191 | COMPREPLY=($(compgen -W "none prompt pwd" -- "${cur}")) 192 | return 0 193 | ;; 194 | *) 195 | COMPREPLY=() 196 | ;; 197 | esac 198 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 199 | return 0 200 | ;; 201 | zoxide__query) 202 | opts="-a -i -l -s -h -V --all --interactive --list --score --exclude --help --version [KEYWORDS]..." 203 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 204 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 205 | return 0 206 | fi 207 | case "${prev}" in 208 | --exclude) 209 | COMPREPLY=() 210 | if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then 211 | compopt -o plusdirs 212 | fi 213 | return 0 214 | ;; 215 | *) 216 | COMPREPLY=() 217 | ;; 218 | esac 219 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 220 | return 0 221 | ;; 222 | zoxide__remove) 223 | opts="-h -V --help --version [PATHS]..." 224 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 225 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 226 | return 0 227 | fi 228 | case "${prev}" in 229 | *) 230 | COMPREPLY=() 231 | ;; 232 | esac 233 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 234 | return 0 235 | ;; 236 | esac 237 | } 238 | 239 | if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then 240 | complete -F _zoxide -o nosort -o bashdefault -o default zoxide 241 | else 242 | complete -F _zoxide -o bashdefault -o default zoxide 243 | fi 244 | -------------------------------------------------------------------------------- /contrib/completions/zoxide.elv: -------------------------------------------------------------------------------- 1 | 2 | use builtin; 3 | use str; 4 | 5 | set edit:completion:arg-completer[zoxide] = {|@words| 6 | fn spaces {|n| 7 | builtin:repeat $n ' ' | str:join '' 8 | } 9 | fn cand {|text desc| 10 | edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc 11 | } 12 | var command = 'zoxide' 13 | for word $words[1..-1] { 14 | if (str:has-prefix $word '-') { 15 | break 16 | } 17 | set command = $command';'$word 18 | } 19 | var completions = [ 20 | &'zoxide'= { 21 | cand -h 'Print help' 22 | cand --help 'Print help' 23 | cand -V 'Print version' 24 | cand --version 'Print version' 25 | cand add 'Add a new directory or increment its rank' 26 | cand edit 'Edit the database' 27 | cand import 'Import entries from another application' 28 | cand init 'Generate shell configuration' 29 | cand query 'Search for a directory in the database' 30 | cand remove 'Remove a directory from the database' 31 | } 32 | &'zoxide;add'= { 33 | cand -s 'The rank to increment the entry if it exists or initialize it with if it doesn''t' 34 | cand --score 'The rank to increment the entry if it exists or initialize it with if it doesn''t' 35 | cand -h 'Print help' 36 | cand --help 'Print help' 37 | cand -V 'Print version' 38 | cand --version 'Print version' 39 | } 40 | &'zoxide;edit'= { 41 | cand -h 'Print help' 42 | cand --help 'Print help' 43 | cand -V 'Print version' 44 | cand --version 'Print version' 45 | cand decrement 'decrement' 46 | cand delete 'delete' 47 | cand increment 'increment' 48 | cand reload 'reload' 49 | } 50 | &'zoxide;edit;decrement'= { 51 | cand -h 'Print help' 52 | cand --help 'Print help' 53 | cand -V 'Print version' 54 | cand --version 'Print version' 55 | } 56 | &'zoxide;edit;delete'= { 57 | cand -h 'Print help' 58 | cand --help 'Print help' 59 | cand -V 'Print version' 60 | cand --version 'Print version' 61 | } 62 | &'zoxide;edit;increment'= { 63 | cand -h 'Print help' 64 | cand --help 'Print help' 65 | cand -V 'Print version' 66 | cand --version 'Print version' 67 | } 68 | &'zoxide;edit;reload'= { 69 | cand -h 'Print help' 70 | cand --help 'Print help' 71 | cand -V 'Print version' 72 | cand --version 'Print version' 73 | } 74 | &'zoxide;import'= { 75 | cand --from 'Application to import from' 76 | cand --merge 'Merge into existing database' 77 | cand -h 'Print help' 78 | cand --help 'Print help' 79 | cand -V 'Print version' 80 | cand --version 'Print version' 81 | } 82 | &'zoxide;init'= { 83 | cand --cmd 'Changes the prefix of the `z` and `zi` commands' 84 | cand --hook 'Changes how often zoxide increments a directory''s score' 85 | cand --no-cmd 'Prevents zoxide from defining the `z` and `zi` commands' 86 | cand -h 'Print help' 87 | cand --help 'Print help' 88 | cand -V 'Print version' 89 | cand --version 'Print version' 90 | } 91 | &'zoxide;query'= { 92 | cand --exclude 'Exclude the current directory' 93 | cand -a 'Show unavailable directories' 94 | cand --all 'Show unavailable directories' 95 | cand -i 'Use interactive selection' 96 | cand --interactive 'Use interactive selection' 97 | cand -l 'List all matching directories' 98 | cand --list 'List all matching directories' 99 | cand -s 'Print score with results' 100 | cand --score 'Print score with results' 101 | cand -h 'Print help' 102 | cand --help 'Print help' 103 | cand -V 'Print version' 104 | cand --version 'Print version' 105 | } 106 | &'zoxide;remove'= { 107 | cand -h 'Print help' 108 | cand --help 'Print help' 109 | cand -V 'Print version' 110 | cand --version 'Print version' 111 | } 112 | ] 113 | $completions[$command] 114 | } 115 | -------------------------------------------------------------------------------- /contrib/completions/zoxide.fish: -------------------------------------------------------------------------------- 1 | # Print an optspec for argparse to handle cmd's options that are independent of any subcommand. 2 | function __fish_zoxide_global_optspecs 3 | string join \n h/help V/version 4 | end 5 | 6 | function __fish_zoxide_needs_command 7 | # Figure out if the current invocation already has a command. 8 | set -l cmd (commandline -opc) 9 | set -e cmd[1] 10 | argparse -s (__fish_zoxide_global_optspecs) -- $cmd 2>/dev/null 11 | or return 12 | if set -q argv[1] 13 | # Also print the command, so this can be used to figure out what it is. 14 | echo $argv[1] 15 | return 1 16 | end 17 | return 0 18 | end 19 | 20 | function __fish_zoxide_using_subcommand 21 | set -l cmd (__fish_zoxide_needs_command) 22 | test -z "$cmd" 23 | and return 1 24 | contains -- $cmd[1] $argv 25 | end 26 | 27 | complete -c zoxide -n "__fish_zoxide_needs_command" -s h -l help -d 'Print help' 28 | complete -c zoxide -n "__fish_zoxide_needs_command" -s V -l version -d 'Print version' 29 | complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "add" -d 'Add a new directory or increment its rank' 30 | complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "edit" -d 'Edit the database' 31 | complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "import" -d 'Import entries from another application' 32 | complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "init" -d 'Generate shell configuration' 33 | complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "query" -d 'Search for a directory in the database' 34 | complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "remove" -d 'Remove a directory from the database' 35 | complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s s -l score -d 'The rank to increment the entry if it exists or initialize it with if it doesn\'t' -r 36 | complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s h -l help -d 'Print help' 37 | complete -c zoxide -n "__fish_zoxide_using_subcommand add" -s V -l version -d 'Print version' 38 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s h -l help -d 'Print help' 39 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -s V -l version -d 'Print version' 40 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -f -a "decrement" 41 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -f -a "delete" 42 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -f -a "increment" 43 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and not __fish_seen_subcommand_from decrement delete increment reload" -f -a "reload" 44 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from decrement" -s h -l help -d 'Print help' 45 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from decrement" -s V -l version -d 'Print version' 46 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from delete" -s h -l help -d 'Print help' 47 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from delete" -s V -l version -d 'Print version' 48 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from increment" -s h -l help -d 'Print help' 49 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from increment" -s V -l version -d 'Print version' 50 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help' 51 | complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version' 52 | complete -c zoxide -n "__fish_zoxide_using_subcommand import" -l from -d 'Application to import from' -r -f -a "autojump\t'' 53 | z\t''" 54 | complete -c zoxide -n "__fish_zoxide_using_subcommand import" -l merge -d 'Merge into existing database' 55 | complete -c zoxide -n "__fish_zoxide_using_subcommand import" -s h -l help -d 'Print help' 56 | complete -c zoxide -n "__fish_zoxide_using_subcommand import" -s V -l version -d 'Print version' 57 | complete -c zoxide -n "__fish_zoxide_using_subcommand init" -l cmd -d 'Changes the prefix of the `z` and `zi` commands' -r 58 | complete -c zoxide -n "__fish_zoxide_using_subcommand init" -l hook -d 'Changes how often zoxide increments a directory\'s score' -r -f -a "none\t'' 59 | prompt\t'' 60 | pwd\t''" 61 | complete -c zoxide -n "__fish_zoxide_using_subcommand init" -l no-cmd -d 'Prevents zoxide from defining the `z` and `zi` commands' 62 | complete -c zoxide -n "__fish_zoxide_using_subcommand init" -s h -l help -d 'Print help' 63 | complete -c zoxide -n "__fish_zoxide_using_subcommand init" -s V -l version -d 'Print version' 64 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -l exclude -d 'Exclude the current directory' -r -f -a "(__fish_complete_directories)" 65 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s a -l all -d 'Show unavailable directories' 66 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s i -l interactive -d 'Use interactive selection' 67 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s l -l list -d 'List all matching directories' 68 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s s -l score -d 'Print score with results' 69 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s h -l help -d 'Print help' 70 | complete -c zoxide -n "__fish_zoxide_using_subcommand query" -s V -l version -d 'Print version' 71 | complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s h -l help -d 'Print help' 72 | complete -c zoxide -n "__fish_zoxide_using_subcommand remove" -s V -l version -d 'Print version' 73 | -------------------------------------------------------------------------------- /contrib/completions/zoxide.nu: -------------------------------------------------------------------------------- 1 | module completions { 2 | 3 | # A smarter cd command for your terminal 4 | export extern zoxide [ 5 | --help(-h) # Print help 6 | --version(-V) # Print version 7 | ] 8 | 9 | # Add a new directory or increment its rank 10 | export extern "zoxide add" [ 11 | ...paths: path 12 | --score(-s): string # The rank to increment the entry if it exists or initialize it with if it doesn't 13 | --help(-h) # Print help 14 | --version(-V) # Print version 15 | ] 16 | 17 | # Edit the database 18 | export extern "zoxide edit" [ 19 | --help(-h) # Print help 20 | --version(-V) # Print version 21 | ] 22 | 23 | export extern "zoxide edit decrement" [ 24 | path: string 25 | --help(-h) # Print help 26 | --version(-V) # Print version 27 | ] 28 | 29 | export extern "zoxide edit delete" [ 30 | path: string 31 | --help(-h) # Print help 32 | --version(-V) # Print version 33 | ] 34 | 35 | export extern "zoxide edit increment" [ 36 | path: string 37 | --help(-h) # Print help 38 | --version(-V) # Print version 39 | ] 40 | 41 | export extern "zoxide edit reload" [ 42 | --help(-h) # Print help 43 | --version(-V) # Print version 44 | ] 45 | 46 | def "nu-complete zoxide import from" [] { 47 | [ "autojump" "z" ] 48 | } 49 | 50 | # Import entries from another application 51 | export extern "zoxide import" [ 52 | path: path 53 | --from: string@"nu-complete zoxide import from" # Application to import from 54 | --merge # Merge into existing database 55 | --help(-h) # Print help 56 | --version(-V) # Print version 57 | ] 58 | 59 | def "nu-complete zoxide init shell" [] { 60 | [ "bash" "elvish" "fish" "nushell" "posix" "powershell" "tcsh" "xonsh" "zsh" ] 61 | } 62 | 63 | def "nu-complete zoxide init hook" [] { 64 | [ "none" "prompt" "pwd" ] 65 | } 66 | 67 | # Generate shell configuration 68 | export extern "zoxide init" [ 69 | shell: string@"nu-complete zoxide init shell" 70 | --no-cmd # Prevents zoxide from defining the `z` and `zi` commands 71 | --cmd: string # Changes the prefix of the `z` and `zi` commands 72 | --hook: string@"nu-complete zoxide init hook" # Changes how often zoxide increments a directory's score 73 | --help(-h) # Print help 74 | --version(-V) # Print version 75 | ] 76 | 77 | # Search for a directory in the database 78 | export extern "zoxide query" [ 79 | ...keywords: string 80 | --all(-a) # Show unavailable directories 81 | --interactive(-i) # Use interactive selection 82 | --list(-l) # List all matching directories 83 | --score(-s) # Print score with results 84 | --exclude: path # Exclude the current directory 85 | --help(-h) # Print help 86 | --version(-V) # Print version 87 | ] 88 | 89 | # Remove a directory from the database 90 | export extern "zoxide remove" [ 91 | ...paths: path 92 | --help(-h) # Print help 93 | --version(-V) # Print version 94 | ] 95 | 96 | } 97 | 98 | export use completions * 99 | -------------------------------------------------------------------------------- /contrib/completions/zoxide.ts: -------------------------------------------------------------------------------- 1 | const completion: Fig.Spec = { 2 | name: "zoxide", 3 | description: "A smarter cd command for your terminal", 4 | subcommands: [ 5 | { 6 | name: "add", 7 | description: "Add a new directory or increment its rank", 8 | options: [ 9 | { 10 | name: ["-s", "--score"], 11 | description: "The rank to increment the entry if it exists or initialize it with if it doesn't", 12 | isRepeatable: true, 13 | args: { 14 | name: "score", 15 | isOptional: true, 16 | }, 17 | }, 18 | { 19 | name: ["-h", "--help"], 20 | description: "Print help", 21 | }, 22 | { 23 | name: ["-V", "--version"], 24 | description: "Print version", 25 | }, 26 | ], 27 | args: { 28 | name: "paths", 29 | isVariadic: true, 30 | template: "folders", 31 | }, 32 | }, 33 | { 34 | name: "edit", 35 | description: "Edit the database", 36 | subcommands: [ 37 | { 38 | name: "decrement", 39 | hidden: true, 40 | options: [ 41 | { 42 | name: ["-h", "--help"], 43 | description: "Print help", 44 | }, 45 | { 46 | name: ["-V", "--version"], 47 | description: "Print version", 48 | }, 49 | ], 50 | args: { 51 | name: "path", 52 | }, 53 | }, 54 | { 55 | name: "delete", 56 | hidden: true, 57 | options: [ 58 | { 59 | name: ["-h", "--help"], 60 | description: "Print help", 61 | }, 62 | { 63 | name: ["-V", "--version"], 64 | description: "Print version", 65 | }, 66 | ], 67 | args: { 68 | name: "path", 69 | }, 70 | }, 71 | { 72 | name: "increment", 73 | hidden: true, 74 | options: [ 75 | { 76 | name: ["-h", "--help"], 77 | description: "Print help", 78 | }, 79 | { 80 | name: ["-V", "--version"], 81 | description: "Print version", 82 | }, 83 | ], 84 | args: { 85 | name: "path", 86 | }, 87 | }, 88 | { 89 | name: "reload", 90 | hidden: true, 91 | options: [ 92 | { 93 | name: ["-h", "--help"], 94 | description: "Print help", 95 | }, 96 | { 97 | name: ["-V", "--version"], 98 | description: "Print version", 99 | }, 100 | ], 101 | }, 102 | ], 103 | options: [ 104 | { 105 | name: ["-h", "--help"], 106 | description: "Print help", 107 | }, 108 | { 109 | name: ["-V", "--version"], 110 | description: "Print version", 111 | }, 112 | ], 113 | }, 114 | { 115 | name: "import", 116 | description: "Import entries from another application", 117 | options: [ 118 | { 119 | name: "--from", 120 | description: "Application to import from", 121 | isRepeatable: true, 122 | args: { 123 | name: "from", 124 | suggestions: [ 125 | "autojump", 126 | "z", 127 | ], 128 | }, 129 | }, 130 | { 131 | name: "--merge", 132 | description: "Merge into existing database", 133 | }, 134 | { 135 | name: ["-h", "--help"], 136 | description: "Print help", 137 | }, 138 | { 139 | name: ["-V", "--version"], 140 | description: "Print version", 141 | }, 142 | ], 143 | args: { 144 | name: "path", 145 | template: "filepaths", 146 | }, 147 | }, 148 | { 149 | name: "init", 150 | description: "Generate shell configuration", 151 | options: [ 152 | { 153 | name: "--cmd", 154 | description: "Changes the prefix of the `z` and `zi` commands", 155 | isRepeatable: true, 156 | args: { 157 | name: "cmd", 158 | isOptional: true, 159 | }, 160 | }, 161 | { 162 | name: "--hook", 163 | description: "Changes how often zoxide increments a directory's score", 164 | isRepeatable: true, 165 | args: { 166 | name: "hook", 167 | isOptional: true, 168 | suggestions: [ 169 | "none", 170 | "prompt", 171 | "pwd", 172 | ], 173 | }, 174 | }, 175 | { 176 | name: "--no-cmd", 177 | description: "Prevents zoxide from defining the `z` and `zi` commands", 178 | }, 179 | { 180 | name: ["-h", "--help"], 181 | description: "Print help", 182 | }, 183 | { 184 | name: ["-V", "--version"], 185 | description: "Print version", 186 | }, 187 | ], 188 | args: { 189 | name: "shell", 190 | suggestions: [ 191 | "bash", 192 | "elvish", 193 | "fish", 194 | "nushell", 195 | "posix", 196 | "powershell", 197 | "tcsh", 198 | "xonsh", 199 | "zsh", 200 | ], 201 | }, 202 | }, 203 | { 204 | name: "query", 205 | description: "Search for a directory in the database", 206 | options: [ 207 | { 208 | name: "--exclude", 209 | description: "Exclude the current directory", 210 | isRepeatable: true, 211 | args: { 212 | name: "exclude", 213 | isOptional: true, 214 | template: "folders", 215 | }, 216 | }, 217 | { 218 | name: ["-a", "--all"], 219 | description: "Show unavailable directories", 220 | }, 221 | { 222 | name: ["-i", "--interactive"], 223 | description: "Use interactive selection", 224 | exclusiveOn: [ 225 | "-l", 226 | "--list", 227 | ], 228 | }, 229 | { 230 | name: ["-l", "--list"], 231 | description: "List all matching directories", 232 | exclusiveOn: [ 233 | "-i", 234 | "--interactive", 235 | ], 236 | }, 237 | { 238 | name: ["-s", "--score"], 239 | description: "Print score with results", 240 | }, 241 | { 242 | name: ["-h", "--help"], 243 | description: "Print help", 244 | }, 245 | { 246 | name: ["-V", "--version"], 247 | description: "Print version", 248 | }, 249 | ], 250 | args: { 251 | name: "keywords", 252 | isVariadic: true, 253 | isOptional: true, 254 | }, 255 | }, 256 | { 257 | name: "remove", 258 | description: "Remove a directory from the database", 259 | options: [ 260 | { 261 | name: ["-h", "--help"], 262 | description: "Print help", 263 | }, 264 | { 265 | name: ["-V", "--version"], 266 | description: "Print version", 267 | }, 268 | ], 269 | args: { 270 | name: "paths", 271 | isVariadic: true, 272 | isOptional: true, 273 | template: "folders", 274 | }, 275 | }, 276 | ], 277 | options: [ 278 | { 279 | name: ["-h", "--help"], 280 | description: "Print help", 281 | }, 282 | { 283 | name: ["-V", "--version"], 284 | description: "Print version", 285 | }, 286 | ], 287 | }; 288 | 289 | export default completion; 290 | -------------------------------------------------------------------------------- /contrib/tutorial.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajeetdsouza/zoxide/db14bdc2ffd2691921be830b22692e45d493557d/contrib/tutorial.webp -------------------------------------------------------------------------------- /contrib/warp-packs-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajeetdsouza/zoxide/db14bdc2ffd2691921be830b22692e45d493557d/contrib/warp-packs-green.png -------------------------------------------------------------------------------- /contrib/warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajeetdsouza/zoxide/db14bdc2ffd2691921be830b22692e45d493557d/contrib/warp.png -------------------------------------------------------------------------------- /init.fish: -------------------------------------------------------------------------------- 1 | if command -sq zoxide 2 | zoxide init fish | source 3 | else 4 | echo 'zoxide: command not found, please install it from https://github.com/ajeetdsouza/zoxide' 5 | end 6 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck shell=dash 3 | # shellcheck disable=SC3043 # Assume `local` extension 4 | 5 | # The official zoxide installer. 6 | # 7 | # It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local` 8 | # extension. Note: Most shells limit `local` to 1 var per line, contra bash. 9 | 10 | main() { 11 | # The version of ksh93 that ships with many illumos systems does not support the "local" 12 | # extension. Print a message rather than fail in subtle ways later on: 13 | if [ "${KSH_VERSION-}" = 'Version JM 93t+ 2010-03-05' ]; then 14 | err 'the installer does not work with this ksh93 version; please try bash' 15 | fi 16 | 17 | set -u 18 | 19 | parse_args "$@" 20 | 21 | local _arch 22 | _arch="${ARCH:-$(ensure get_architecture)}" 23 | assert_nz "${_arch}" "arch" 24 | echo "Detected architecture: ${_arch}" 25 | 26 | local _bin_name 27 | case "${_arch}" in 28 | *windows*) _bin_name="zoxide.exe" ;; 29 | *) _bin_name="zoxide" ;; 30 | esac 31 | 32 | # Create and enter a temporary directory. 33 | local _tmp_dir 34 | _tmp_dir="$(mktemp -d)" || err "mktemp: could not create temporary directory" 35 | cd "${_tmp_dir}" || err "cd: failed to enter directory: ${_tmp_dir}" 36 | 37 | # Download and extract zoxide. 38 | local _package 39 | _package="$(ensure download_zoxide "${_arch}")" 40 | assert_nz "${_package}" "package" 41 | echo "Downloaded package: ${_package}" 42 | case "${_package}" in 43 | *.tar.gz) 44 | need_cmd tar 45 | ensure tar -xf "${_package}" 46 | ;; 47 | *.zip) 48 | need_cmd unzip 49 | ensure unzip -oq "${_package}" 50 | ;; 51 | *) 52 | err "unsupported package format: ${_package}" 53 | ;; 54 | esac 55 | 56 | # Install binary. 57 | ensure try_sudo mkdir -p -- "${BIN_DIR}" 58 | ensure try_sudo cp -- "${_bin_name}" "${BIN_DIR}/${_bin_name}" 59 | ensure try_sudo chmod +x "${BIN_DIR}/${_bin_name}" 60 | echo "Installed zoxide to ${BIN_DIR}" 61 | 62 | # Install manpages. 63 | ensure try_sudo mkdir -p -- "${MAN_DIR}/man1" 64 | ensure try_sudo cp -- "man/man1/"* "${MAN_DIR}/man1/" 65 | echo "Installed manpages to ${MAN_DIR}" 66 | 67 | # Print success message and check $PATH. 68 | echo "" 69 | echo "zoxide is installed!" 70 | if ! echo ":${PATH}:" | grep -Fq ":${BIN_DIR}:"; then 71 | echo "Note: ${BIN_DIR} is not on your \$PATH. zoxide will not work unless it is added to \$PATH." 72 | fi 73 | } 74 | 75 | # Parse the arguments passed and set variables accordingly. 76 | parse_args() { 77 | BIN_DIR_DEFAULT="${HOME}/.local/bin" 78 | MAN_DIR_DEFAULT="${HOME}/.local/share/man" 79 | SUDO_DEFAULT="sudo" 80 | 81 | BIN_DIR="${BIN_DIR_DEFAULT}" 82 | MAN_DIR="${MAN_DIR_DEFAULT}" 83 | SUDO="${SUDO_DEFAULT}" 84 | 85 | while [ "$#" -gt 0 ]; do 86 | case "$1" in 87 | --arch) ARCH="$2" && shift 2 ;; 88 | --arch=*) ARCH="${1#*=}" && shift 1 ;; 89 | --bin-dir) BIN_DIR="$2" && shift 2 ;; 90 | --bin-dir=*) BIN_DIR="${1#*=}" && shift 1 ;; 91 | --man-dir) MAN_DIR="$2" && shift 2 ;; 92 | --man-dir=*) MAN_DIR="${1#*=}" && shift 1 ;; 93 | --sudo) SUDO="$2" && shift 2 ;; 94 | --sudo=*) SUDO="${1#*=}" && shift 1 ;; 95 | -h | --help) usage && exit 0 ;; 96 | *) err "Unknown option: $1" ;; 97 | esac 98 | done 99 | } 100 | 101 | usage() { 102 | # heredocs are not defined in POSIX. 103 | local _text_heading _text_reset 104 | _text_heading="$(tput bold || true 2>/dev/null)$(tput smul || true 2>/dev/null)" 105 | _text_reset="$(tput sgr0 || true 2>/dev/null)" 106 | 107 | local _arch 108 | _arch="$(get_architecture || true)" 109 | 110 | echo "\ 111 | ${_text_heading}zoxide installer${_text_reset} 112 | Ajeet D'Souza <98ajeet@gmail.com> 113 | https://github.com/ajeetdsouza/zoxide 114 | 115 | Fetches and installs zoxide. If zoxide is already installed, it will be updated to the latest version. 116 | 117 | ${_text_heading}Usage:${_text_reset} 118 | install.sh [OPTIONS] 119 | 120 | ${_text_heading}Options:${_text_reset} 121 | --arch Override the architecture identified by the installer [current: ${_arch}] 122 | --bin-dir Override the installation directory [default: ${BIN_DIR_DEFAULT}] 123 | --man-dir Override the manpage installation directory [default: ${MAN_DIR_DEFAULT}] 124 | --sudo Override the command used to elevate to root privileges [default: ${SUDO_DEFAULT}] 125 | -h, --help Print help" 126 | } 127 | 128 | download_zoxide() { 129 | local _arch="$1" 130 | 131 | if check_cmd curl; then 132 | _dld=curl 133 | elif check_cmd wget; then 134 | _dld=wget 135 | else 136 | need_cmd 'curl or wget' 137 | fi 138 | need_cmd grep 139 | 140 | local _releases_url="https://api.github.com/repos/ajeetdsouza/zoxide/releases/latest" 141 | local _releases 142 | case "${_dld}" in 143 | curl) _releases="$(curl -sL "${_releases_url}")" || 144 | err "curl: failed to download ${_releases_url}" ;; 145 | wget) _releases="$(wget -qO- "${_releases_url}")" || 146 | err "wget: failed to download ${_releases_url}" ;; 147 | *) err "unsupported downloader: ${_dld}" ;; 148 | esac 149 | (echo "${_releases}" | grep -q 'API rate limit exceeded') && 150 | 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" 151 | 152 | local _package_url 153 | _package_url="$(echo "${_releases}" | grep "browser_download_url" | cut -d '"' -f 4 | grep -- "${_arch}")" || 154 | err "zoxide has not yet been packaged for your architecture (${_arch}), please file an issue: https://github.com/ajeetdsouza/zoxide/issues" 155 | 156 | local _ext 157 | case "${_package_url}" in 158 | *.tar.gz) _ext="tar.gz" ;; 159 | *.zip) _ext="zip" ;; 160 | *) err "unsupported package format: ${_package_url}" ;; 161 | esac 162 | 163 | local _package="zoxide.${_ext}" 164 | case "${_dld}" in 165 | curl) _releases="$(curl -sLo "${_package}" "${_package_url}")" || err "curl: failed to download ${_package_url}" ;; 166 | wget) _releases="$(wget -qO "${_package}" "${_package_url}")" || err "wget: failed to download ${_package_url}" ;; 167 | *) err "unsupported downloader: ${_dld}" ;; 168 | esac 169 | 170 | echo "${_package}" 171 | } 172 | 173 | try_sudo() { 174 | if "$@" >/dev/null 2>&1; then 175 | return 0 176 | fi 177 | 178 | need_sudo 179 | "${SUDO}" "$@" 180 | } 181 | 182 | need_sudo() { 183 | if ! check_cmd "${SUDO}"; then 184 | err "\ 185 | could not find the command \`${SUDO}\` needed to get permissions for install. 186 | 187 | If you are on Windows, please run your shell as an administrator, then rerun this script. 188 | Otherwise, please run this script as root, or install \`sudo\`." 189 | fi 190 | 191 | if ! "${SUDO}" -v; then 192 | err "sudo permissions not granted, aborting installation" 193 | fi 194 | } 195 | 196 | # The below functions have been extracted with minor modifications from the 197 | # Rustup install script: 198 | # 199 | # https://github.com/rust-lang/rustup/blob/4c1289b2c3f3702783900934a38d7c5f912af787/rustup-init.sh 200 | 201 | get_architecture() { 202 | local _ostype _cputype _bitness _arch _clibtype 203 | _ostype="$(uname -s)" 204 | _cputype="$(uname -m)" 205 | _clibtype="musl" 206 | 207 | if [ "${_ostype}" = Linux ]; then 208 | if [ "$(uname -o || true)" = Android ]; then 209 | _ostype=Android 210 | fi 211 | fi 212 | 213 | if [ "${_ostype}" = Darwin ] && [ "${_cputype}" = i386 ]; then 214 | # Darwin `uname -m` lies 215 | if sysctl hw.optional.x86_64 | grep -q ': 1'; then 216 | _cputype=x86_64 217 | fi 218 | fi 219 | 220 | if [ "${_ostype}" = SunOS ]; then 221 | # Both Solaris and illumos presently announce as "SunOS" in "uname -s" 222 | # so use "uname -o" to disambiguate. We use the full path to the 223 | # system uname in case the user has coreutils uname first in PATH, 224 | # which has historically sometimes printed the wrong value here. 225 | if [ "$(/usr/bin/uname -o || true)" = illumos ]; then 226 | _ostype=illumos 227 | fi 228 | 229 | # illumos systems have multi-arch userlands, and "uname -m" reports the 230 | # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86 231 | # systems. Check for the native (widest) instruction set on the 232 | # running kernel: 233 | if [ "${_cputype}" = i86pc ]; then 234 | _cputype="$(isainfo -n)" 235 | fi 236 | fi 237 | 238 | case "${_ostype}" in 239 | Android) 240 | _ostype=linux-android 241 | ;; 242 | Linux) 243 | check_proc 244 | _ostype=unknown-linux-${_clibtype} 245 | _bitness=$(get_bitness) 246 | ;; 247 | FreeBSD) 248 | _ostype=unknown-freebsd 249 | ;; 250 | NetBSD) 251 | _ostype=unknown-netbsd 252 | ;; 253 | DragonFly) 254 | _ostype=unknown-dragonfly 255 | ;; 256 | Darwin) 257 | _ostype=apple-darwin 258 | ;; 259 | illumos) 260 | _ostype=unknown-illumos 261 | ;; 262 | MINGW* | MSYS* | CYGWIN* | Windows_NT) 263 | _ostype=pc-windows-msvc 264 | ;; 265 | *) 266 | err "unrecognized OS type: ${_ostype}" 267 | ;; 268 | esac 269 | 270 | case "${_cputype}" in 271 | i386 | i486 | i686 | i786 | x86) 272 | _cputype=i686 273 | ;; 274 | xscale | arm) 275 | _cputype=arm 276 | if [ "${_ostype}" = "linux-android" ]; then 277 | _ostype=linux-androideabi 278 | fi 279 | ;; 280 | armv6l) 281 | _cputype=arm 282 | if [ "${_ostype}" = "linux-android" ]; then 283 | _ostype=linux-androideabi 284 | else 285 | _ostype="${_ostype}eabihf" 286 | fi 287 | ;; 288 | armv7l | armv8l) 289 | _cputype=armv7 290 | if [ "${_ostype}" = "linux-android" ]; then 291 | _ostype=linux-androideabi 292 | else 293 | _ostype="${_ostype}eabihf" 294 | fi 295 | ;; 296 | aarch64 | arm64) 297 | _cputype=aarch64 298 | ;; 299 | x86_64 | x86-64 | x64 | amd64) 300 | _cputype=x86_64 301 | ;; 302 | mips) 303 | _cputype=$(get_endianness mips '' el) 304 | ;; 305 | mips64) 306 | if [ "${_bitness}" -eq 64 ]; then 307 | # only n64 ABI is supported for now 308 | _ostype="${_ostype}abi64" 309 | _cputype=$(get_endianness mips64 '' el) 310 | fi 311 | ;; 312 | ppc) 313 | _cputype=powerpc 314 | ;; 315 | ppc64) 316 | _cputype=powerpc64 317 | ;; 318 | ppc64le) 319 | _cputype=powerpc64le 320 | ;; 321 | s390x) 322 | _cputype=s390x 323 | ;; 324 | riscv64) 325 | _cputype=riscv64gc 326 | ;; 327 | *) 328 | err "unknown CPU type: ${_cputype}" 329 | ;; 330 | esac 331 | 332 | # Detect 64-bit linux with 32-bit userland 333 | if [ "${_ostype}" = unknown-linux-musl ] && [ "${_bitness}" -eq 32 ]; then 334 | case ${_cputype} in 335 | x86_64) 336 | # 32-bit executable for amd64 = x32 337 | if is_host_amd64_elf; then { 338 | err "x32 userland is unsupported" 339 | }; else 340 | _cputype=i686 341 | fi 342 | ;; 343 | mips64) 344 | _cputype=$(get_endianness mips '' el) 345 | ;; 346 | powerpc64) 347 | _cputype=powerpc 348 | ;; 349 | aarch64) 350 | _cputype=armv7 351 | if [ "${_ostype}" = "linux-android" ]; then 352 | _ostype=linux-androideabi 353 | else 354 | _ostype="${_ostype}eabihf" 355 | fi 356 | ;; 357 | riscv64gc) 358 | err "riscv64 with 32-bit userland unsupported" 359 | ;; 360 | *) ;; 361 | esac 362 | fi 363 | 364 | # Detect armv7 but without the CPU features Rust needs in that build, 365 | # and fall back to arm. 366 | # See https://github.com/rust-lang/rustup.rs/issues/587. 367 | if [ "${_ostype}" = "unknown-linux-musleabihf" ] && [ "${_cputype}" = armv7 ]; then 368 | if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then 369 | # At least one processor does not have NEON. 370 | _cputype=arm 371 | fi 372 | fi 373 | 374 | _arch="${_cputype}-${_ostype}" 375 | echo "${_arch}" 376 | } 377 | 378 | get_bitness() { 379 | need_cmd head 380 | # Architecture detection without dependencies beyond coreutils. 381 | # ELF files start out "\x7fELF", and the following byte is 382 | # 0x01 for 32-bit and 383 | # 0x02 for 64-bit. 384 | # The printf builtin on some shells like dash only supports octal 385 | # escape sequences, so we use those. 386 | local _current_exe_head 387 | _current_exe_head=$(head -c 5 /proc/self/exe) 388 | if [ "${_current_exe_head}" = "$(printf '\177ELF\001')" ]; then 389 | echo 32 390 | elif [ "${_current_exe_head}" = "$(printf '\177ELF\002')" ]; then 391 | echo 64 392 | else 393 | err "unknown platform bitness" 394 | fi 395 | } 396 | 397 | get_endianness() { 398 | local cputype="$1" 399 | local suffix_eb="$2" 400 | local suffix_el="$3" 401 | 402 | # detect endianness without od/hexdump, like get_bitness() does. 403 | need_cmd head 404 | need_cmd tail 405 | 406 | local _current_exe_endianness 407 | _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" 408 | if [ "${_current_exe_endianness}" = "$(printf '\001')" ]; then 409 | echo "${cputype}${suffix_el}" 410 | elif [ "${_current_exe_endianness}" = "$(printf '\002')" ]; then 411 | echo "${cputype}${suffix_eb}" 412 | else 413 | err "unknown platform endianness" 414 | fi 415 | } 416 | 417 | is_host_amd64_elf() { 418 | need_cmd head 419 | need_cmd tail 420 | # ELF e_machine detection without dependencies beyond coreutils. 421 | # Two-byte field at offset 0x12 indicates the CPU, 422 | # but we're interested in it being 0x3E to indicate amd64, or not that. 423 | local _current_exe_machine 424 | _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1) 425 | [ "${_current_exe_machine}" = "$(printf '\076')" ] 426 | } 427 | 428 | check_proc() { 429 | # Check for /proc by looking for the /proc/self/exe link. 430 | # This is only run on Linux. 431 | if ! test -L /proc/self/exe; then 432 | err "unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc." 433 | fi 434 | } 435 | 436 | need_cmd() { 437 | if ! check_cmd "$1"; then 438 | err "need '$1' (command not found)" 439 | fi 440 | } 441 | 442 | check_cmd() { 443 | command -v -- "$1" >/dev/null 2>&1 444 | } 445 | 446 | # Run a command that should never fail. If the command fails execution 447 | # will immediately terminate with an error showing the failing 448 | # command. 449 | ensure() { 450 | if ! "$@"; then err "command failed: $*"; fi 451 | } 452 | 453 | assert_nz() { 454 | if [ -z "$1" ]; then err "found empty string: $2"; fi 455 | } 456 | 457 | err() { 458 | echo "Error: $1" >&2 459 | exit 1 460 | } 461 | 462 | # This is put in braces to ensure that the script does not run until it is 463 | # downloaded completely. 464 | { 465 | main "$@" || exit 1 466 | } 467 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | @just --list 3 | 4 | [unix] 5 | fmt: 6 | nix-shell --cores 0 --pure --run 'cargo-fmt --all' 7 | nix-shell --cores 0 --pure --run 'nixfmt -- *.nix' 8 | nix-shell --cores 0 --pure --run 'shfmt --indent=4 --language-dialect=posix --simplify --write *.sh' 9 | nix-shell --cores 0 --pure --run 'yamlfmt -- .github/workflows/*.yml' 10 | 11 | [windows] 12 | fmt: 13 | cargo +nightly fmt --all 14 | 15 | [unix] 16 | lint: 17 | nix-shell --cores 0 --pure --run 'cargo-fmt --all --check' 18 | nix-shell --cores 0 --pure --run 'cargo clippy --all-features --all-targets -- -Dwarnings' 19 | nix-shell --cores 0 --pure --run 'cargo msrv verify' 20 | nix-shell --cores 0 --pure --run 'cargo udeps --all-features --all-targets --workspace' 21 | nix-shell --cores 0 --pure --run 'mandoc -man -Wall -Tlint -- man/man1/*.1' 22 | nix-shell --cores 0 --pure --run 'markdownlint *.md' 23 | nix-shell --cores 0 --pure --run 'nixfmt --check -- *.nix' 24 | nix-shell --cores 0 --pure --run 'shellcheck --enable all *.sh' 25 | nix-shell --cores 0 --pure --run 'shfmt --diff --indent=4 --language-dialect=posix --simplify *.sh' 26 | nix-shell --cores 0 --pure --run 'yamlfmt -lint -- .github/workflows/*.yml' 27 | 28 | [windows] 29 | lint: 30 | cargo +nightly fmt --all --check 31 | cargo +stable clippy --all-features --all-targets -- -Dwarnings 32 | 33 | [unix] 34 | test *args: 35 | nix-shell --cores 0 --pure --run 'cargo nextest run --all-features --no-fail-fast --workspace {{args}}' 36 | 37 | [windows] 38 | test *args: 39 | cargo +stable test --no-fail-fast --workspace {{args}} 40 | -------------------------------------------------------------------------------- /man/man1/zoxide-add.1: -------------------------------------------------------------------------------- 1 | .TH "ZOXIDE" "1" "2021-04-12" "" "zoxide" 2 | .SH NAME 3 | \fBzoxide-add\fR - add a new directory or increment its rank 4 | .SH SYNOPSIS 5 | .B zoxide add [PATHS] 6 | .SH DESCRIPTION 7 | If the directory is not already in the database, this command creates a new 8 | entry for it with a default score of 1, otherwise, it increments the existing 9 | score by 1. It then sets the last updated field of the entry to the current 10 | time. After this, it runs the \fBAGING\fR algorithm on the database. See 11 | \fBzoxide\fR(1) for more about the algorithm. 12 | .sp 13 | If you'd like to prevent a directory from being added to the database, see the 14 | \fB_ZO_EXCLUDE_DIRS\fR environment variable in \fBzoxide\fR(1). 15 | .SH OPTIONS 16 | .TP 17 | .B -h, --help 18 | Print help information. 19 | .SH REPORTING BUGS 20 | For any issues, feature requests, or questions, please visit: 21 | .sp 22 | \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR 23 | .SH AUTHOR 24 | Ajeet D'Souza \fB<98ajeet@gmail.com>\fR 25 | -------------------------------------------------------------------------------- /man/man1/zoxide-import.1: -------------------------------------------------------------------------------- 1 | .TH "ZOXIDE" "1" "2021-04-12" "" "zoxide" 2 | .SH NAME 3 | \fBzoxide-import\fR - import data from other tools 4 | .SH SYNOPSIS 5 | .B zoxide import PATH --from FORMAT [OPTIONS] 6 | .SH OPTIONS 7 | .TP 8 | .B --from FORMAT 9 | The format of the database being imported: 10 | .TS 11 | tab(|); 12 | l l. 13 | \fBautojump\fR 14 | \fBz\fR|(for \fBfasd\fR, \fBz\fR, \fBz.lua\fR, or \fBzsh-z\fR) 15 | .TE 16 | .sp 17 | Note: zoxide only imports paths from autojump, since its matching 18 | algorithm is too different to import the scores. 19 | .TP 20 | .B -h, --help 21 | Print help information. 22 | .TP 23 | .B --merge 24 | By default, the import fails if the current database is not already empty. This 25 | option merges imported data into the existing database. 26 | .SH REPORTING BUGS 27 | For any issues, feature requests, or questions, please visit: 28 | .sp 29 | \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR 30 | .SH AUTHOR 31 | Ajeet D'Souza \fB<98ajeet@gmail.com>\fR 32 | -------------------------------------------------------------------------------- /man/man1/zoxide-init.1: -------------------------------------------------------------------------------- 1 | .TH "ZOXIDE" "1" "2021-04-12" "" "zoxide" 2 | .SH NAME 3 | \fBzoxide-init\fR - generate shell configuration for zoxide 4 | .SH SYNOPSIS 5 | .B zoxide init SHELL [OPTIONS] 6 | .SH DESCRIPTION 7 | To initialize zoxide on your shell: 8 | .TP 9 | .B bash 10 | Add this to the \fBend\fR of your config file (usually \fB~/.bashrc\fR): 11 | .sp 12 | .nf 13 | \fBeval "$(zoxide init bash)"\fR 14 | .fi 15 | .TP 16 | .B elvish 17 | Add this to the \fBend\fR of your config file (usually \fB~/.elvish/rc.elv\fR): 18 | .sp 19 | .nf 20 | \fBeval $(zoxide init elvish | slurp)\fR 21 | .fi 22 | .sp 23 | Note: zoxide only supports elvish v0.18.0 and above. 24 | .TP 25 | .B fish 26 | Add this to the \fBend\fR of your config file (usually 27 | \fB~/.config/fish/config.fish\fR): 28 | .sp 29 | .nf 30 | \fBzoxide init fish | source\fR 31 | .fi 32 | .TP 33 | .B nushell 34 | Add this to the \fBend\fR of your env file (find it by running 35 | \fB$nu.env-path\fR in Nushell): 36 | .sp 37 | .nf 38 | \fBzoxide init nushell | save -f ~/.zoxide.nu\fR 39 | .fi 40 | .sp 41 | Now, add this to the \fBend\fR of your config file (find it by running 42 | \fB$nu.config-path\fR in Nushell): 43 | .sp 44 | .nf 45 | \fBsource ~/.zoxide.nu\fR 46 | .fi 47 | .sp 48 | Note: zoxide only supports Nushell v0.89.0+. 49 | .TP 50 | .B powershell 51 | Add this to the \fBend\fR of your config file (find it by running \fBecho 52 | $profile\fR in PowerShell): 53 | .sp 54 | .nf 55 | \fBInvoke-Expression (& { (zoxide init powershell | Out-String) })\fR 56 | .fi 57 | .TP 58 | .B tcsh 59 | Add this to the \fBend\fR of your config file (usually \fB~/.tcshrc\fR): 60 | .sp 61 | .nf 62 | \fBzoxide init tcsh > ~/.zoxide.tcsh\fR 63 | \fBsource ~/.zoxide.tcsh\fR 64 | .fi 65 | .TP 66 | .B xonsh 67 | Add this to the \fBend\fR of your config file (usually \fB~/.xonshrc\fR): 68 | .sp 69 | .nf 70 | \fBexecx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide')\fR 71 | .fi 72 | .TP 73 | .B zsh 74 | Add this to the \fBend\fR of your config file (usually \fB~/.zshrc\fR): 75 | .sp 76 | .nf 77 | \fBeval "$(zoxide init zsh)"\fR 78 | .fi 79 | .TP 80 | .B any POSIX shell 81 | .sp 82 | Add this to the \fBend\fR of your config file: 83 | .sp 84 | .nf 85 | \fBeval "$(zoxide init posix --hook prompt)"\fR 86 | .fi 87 | .SH OPTIONS 88 | .TP 89 | .B --cmd 90 | Changes the prefix of the \fBz\fR and \fBzi\fR commands. 91 | .br 92 | \fB--cmd j\fR would change the commands to (\fBj\fR, \fBji\fR). 93 | .br 94 | \fB--cmd cd\fR would replace the \fBcd\fR command (doesn't work on Nushell / 95 | POSIX shells). 96 | .TP 97 | .B -h, --help 98 | Print help information. 99 | .TP 100 | .B --hook HOOK 101 | Changes how often zoxide increments a directory's score: 102 | .TS 103 | tab(|); 104 | l l. 105 | \fBnone\fR|Never 106 | \fBprompt\fR|At every shell prompt 107 | \fBpwd\fR|Whenever the directory is changed 108 | .TE 109 | .TP 110 | .B --no-cmd 111 | Prevents zoxide from defining the \fBz\fR and \fBzi\fR commands. These functions 112 | will still be available in your shell as \fB__zoxide_z\fR and \fB__zoxide_zi\fR, 113 | should you choose to redefine them. 114 | .SH REPORTING BUGS 115 | For any issues, feature requests, or questions, please visit: 116 | .sp 117 | \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR 118 | .SH AUTHOR 119 | Ajeet D'Souza \fB<98ajeet@gmail.com>\fR 120 | -------------------------------------------------------------------------------- /man/man1/zoxide-query.1: -------------------------------------------------------------------------------- 1 | .TH "ZOXIDE" "1" "2021-04-12" "" "zoxide" 2 | .SH NAME 3 | \fBzoxide-query\fR - search for a directory in the database 4 | .SH SYNOPSIS 5 | .B zoxide query [KEYWORDS] [OPTIONS] 6 | .SH DESCRIPTION 7 | Query the database for paths matching the keywords. The exact \fBMATCHING\fR 8 | algorithm is described in \fBzoxide\fR(1). 9 | .SH OPTIONS 10 | .TP 11 | .B --all 12 | Show deleted directories. 13 | .TP 14 | .B --exclude PATH 15 | Exclude a path from query results. 16 | .TP 17 | .B -h, --help 18 | Print help information. 19 | .TP 20 | .B -i, --interactive 21 | Use interactive selection. This option requires \fBfzf\fR(1). 22 | .TP 23 | .B -l, --list 24 | List all results, rather than just the one with the highest frecency. 25 | .TP 26 | .B -s, --score 27 | Print the calculated score as well as the matched path. 28 | .SH REPORTING BUGS 29 | For any issues, feature requests, or questions, please visit: 30 | .sp 31 | \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR 32 | .SH AUTHOR 33 | Ajeet D'Souza \fB<98ajeet@gmail.com>\fR 34 | -------------------------------------------------------------------------------- /man/man1/zoxide-remove.1: -------------------------------------------------------------------------------- 1 | .TH "ZOXIDE" "1" "2021-04-12" "" "zoxide" 2 | .SH NAME 3 | \fBzoxide-remove\fR - remove a directory from the database 4 | .SH SYNOPSIS 5 | .B zoxide remove [PATHS] [OPTIONS] 6 | .SH DESCRIPTION 7 | If you'd like to permanently exclude a directory from the database, see the 8 | \fB_ZO_EXCLUDE_DIRS\fR environment variable in \fBzoxide\fR(1). 9 | .SH OPTIONS 10 | .TP 11 | .B -h, --help 12 | Print help information. 13 | .SH REPORTING BUGS 14 | For any issues, feature requests, or questions, please visit: 15 | .sp 16 | \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR 17 | .SH AUTHOR 18 | Ajeet D'Souza \fB<98ajeet@gmail.com>\fR 19 | -------------------------------------------------------------------------------- /man/man1/zoxide.1: -------------------------------------------------------------------------------- 1 | .TH "ZOXIDE" "1" "2021-04-12" "" "zoxide" 2 | .SH NAME 3 | \fBzoxide\fR - a smarter cd command 4 | .SH SYNOPSIS 5 | .B zoxide SUBCOMMAND [OPTIONS] 6 | .SH DESCRIPTION 7 | zoxide is a smarter cd command for your terminal. It keeps track of the 8 | directories you use most frequently, and uses a ranking algorithm to navigate 9 | to the best match. 10 | .SH USAGE 11 | .nf 12 | z foo # cd into highest ranked directory matching foo 13 | z foo bar # cd into highest ranked directory matching foo and bar 14 | z foo / # cd into a subdirectory starting with foo 15 | .sp 16 | z ~/foo # z also works like a regular cd command 17 | z foo/ # cd into relative path 18 | z .. # cd one level up 19 | z - # cd into previous directory 20 | .sp 21 | zi foo # cd with interactive selection (using fzf) 22 | .sp 23 | z foo # show interactive completions (bash 4.4+/fish/zsh only) 24 | .fi 25 | .SH SUBCOMMANDS 26 | .TP 27 | \fBzoxide-add\fR(1) 28 | Add a new directory to the database, or increment its rank. 29 | .TP 30 | \fBzoxide-import\fR(1) 31 | Import entries from another application. 32 | .TP 33 | \fBzoxide-init\fR(1) 34 | Generate shell configuration. 35 | .TP 36 | \fBzoxide-query\fR(1) 37 | Search for a directory in the database. 38 | .TP 39 | \fBzoxide-remove\fR(1) 40 | Remove a directory from the database. 41 | .SH OPTIONS 42 | .TP 43 | .B -h, --help 44 | Print help information. 45 | .TP 46 | .B -V, --version 47 | Print version information. 48 | .SH ENVIRONMENT VARIABLES 49 | Environment variables can be used for configuration. They must be set before 50 | \fBzoxide-init\fR(1) is called. 51 | .TP 52 | .B _ZO_DATA_DIR 53 | Specifies the directory in which the database is stored. The default value 54 | varies across OSes: 55 | .TS 56 | tab(|); 57 | l l. 58 | \fBOS|Path\fR 59 | \fBLinux/BSD\fR|T{ 60 | \fB$XDG_DATA_HOME\fR or \fB$HOME/.local/share\fR, eg. 61 | \fB/home/alice/.local/share\fR 62 | T} 63 | \fBmacOS\fR|T{ 64 | \fB$HOME/Library/Application Support\fR, eg. 65 | \fB/Users/Alice/Library/Application Support\fR 66 | T} 67 | \fBWindows\fR|T{ 68 | \fB%LOCALAPPDATA%\fR, eg. \fBC:\\Users\\Alice\\AppData\\Local\fR 69 | T} 70 | .TE 71 | .TP 72 | .B _ZO_ECHO 73 | When set to 1, \fBz\fR will print the matched directory before navigating to it. 74 | .TP 75 | .B _ZO_EXCLUDE_DIRS 76 | Prevents the specified directories from being added to the database. This is 77 | provided as a list of globs, separated by OS-specific characters: 78 | .TS 79 | tab(|); 80 | l l. 81 | \fBOS|Separator\fR 82 | \fBLinux/macOS/BSD\fR|T{ 83 | \fB:\fR, eg. \fB$HOME:$HOME/private/*\fR 84 | T} 85 | \fBWindows\fR|\fB;\fR, eg. \fB$HOME;$HOME/private/*\fR 86 | .TE 87 | .sp 88 | By default, this is set to \fB$HOME\fR. After setting this up, you might need 89 | to use \fBzoxide-remove\fR(1) to remove any existing entries from the database. 90 | .TP 91 | .B _ZO_FZF_OPTS 92 | Custom options to pass to \fBfzf\fR(1) during interactive selection. See the 93 | manpage for the full list of options. 94 | .TP 95 | .B _ZO_MAXAGE 96 | Configures the aging algorithm, which limits the maximum number of entries in 97 | the database. By default, this is set to 10000. 98 | .TP 99 | .B _ZO_RESOLVE_SYMLINKS 100 | When set to 1, \fBz\fR will resolve symlinks before adding directories to 101 | the database. 102 | .SH ALGORITHM 103 | .TP 104 | .B AGING 105 | zoxide uses a parameter called \fB_ZO_MAXAGE\fR to limit the number of entries 106 | in the database based on usage patterns. If the total \fBFRECENCY\fR of the 107 | directories in the database exceeds this value, we divide each directory's 108 | score by a factor \fBk\fR - such that the new total becomes ~90% of 109 | \fB_ZO_MAXAGE\fR. Thereafter, if the new score of any directory falls below 110 | 1, it is removed from the database. 111 | .sp 112 | Theoretically, the maximum number of directories in the database is 113 | \fB4 * _ZO_MAXAGE\fR, although it is lower in practice. 114 | .TP 115 | .B FRECENCY 116 | Each directory in zoxide is given a score, starting with 1 the first time 117 | it is accessed. Every subsequent access increases the score by 1. When a 118 | query is made, we calculate frecency based on the last time the directory was 119 | accessed: 120 | .TS 121 | tab(|); 122 | l l. 123 | \fBLast access time\fR|\fBFrecency\fR 124 | Within the last hour|score * 4 125 | Within the last day|score * 2 126 | Within the last week|score / 2 127 | Otherwise|score / 4 128 | .TE 129 | .SH REPORTING BUGS 130 | For any issues, feature requests, or questions, please visit: 131 | .sp 132 | \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR 133 | .SH AUTHOR 134 | Ajeet D'Souza \fB<98ajeet@gmail.com>\fR 135 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Module" 3 | newline_style = "Native" 4 | use_field_init_shorthand = true 5 | use_small_heuristics = "Max" 6 | use_try_shorthand = true 7 | wrap_comments = true 8 | style_edition = "2024" 9 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import (builtins.fetchTarball 3 | "https://github.com/NixOS/nixpkgs/archive/ec9ef366451af88284d7dfd18ee017b7e86a0710.tar.gz") { 4 | overlays = [ rust ]; 5 | }; 6 | rust = import (builtins.fetchTarball 7 | "https://github.com/oxalica/rust-overlay/archive/026e8fedefd6b167d92ed04b195c658d95ffc7a5.tar.gz"); 8 | 9 | rust-nightly = 10 | pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal); 11 | cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" '' 12 | export RUSTC="${rust-nightly}/bin/rustc"; 13 | export CARGO="${rust-nightly}/bin/cargo"; 14 | exec "${pkgs.cargo-udeps}/bin/cargo-udeps" "$@" 15 | ''; 16 | in pkgs.mkShell { 17 | buildInputs = [ 18 | # Rust 19 | (pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.rustfmt)) 20 | pkgs.rust-bin.stable.latest.default 21 | 22 | # Shells 23 | pkgs.bash 24 | pkgs.dash 25 | pkgs.elvish 26 | pkgs.fish 27 | pkgs.ksh 28 | pkgs.nushell 29 | pkgs.powershell 30 | pkgs.tcsh 31 | pkgs.xonsh 32 | pkgs.zsh 33 | 34 | # Tools 35 | cargo-udeps 36 | pkgs.cargo-msrv 37 | pkgs.cargo-nextest 38 | pkgs.cargo-udeps 39 | pkgs.just 40 | pkgs.mandoc 41 | pkgs.nixfmt 42 | pkgs.nodePackages.markdownlint-cli 43 | pkgs.python3Packages.black 44 | pkgs.python3Packages.mypy 45 | pkgs.python3Packages.pylint 46 | pkgs.shellcheck 47 | pkgs.shfmt 48 | pkgs.yamlfmt 49 | 50 | # Dependencies 51 | pkgs.cacert 52 | pkgs.fzf 53 | pkgs.git 54 | pkgs.libiconv 55 | ]; 56 | 57 | CARGO_TARGET_DIR = "target_nix"; 58 | } 59 | -------------------------------------------------------------------------------- /src/cmd/add.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{Result, bail}; 4 | 5 | use crate::cmd::{Add, Run}; 6 | use crate::db::Database; 7 | use crate::{config, util}; 8 | 9 | impl Run for Add { 10 | fn run(&self) -> Result<()> { 11 | // These characters can't be printed cleanly to a single line, so they can cause 12 | // confusion when writing to stdout. 13 | const EXCLUDE_CHARS: &[char] = &['\n', '\r']; 14 | 15 | let exclude_dirs = config::exclude_dirs()?; 16 | let max_age = config::maxage()?; 17 | let now = util::current_time()?; 18 | 19 | let mut db = Database::open()?; 20 | 21 | for path in &self.paths { 22 | let path = 23 | if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }( 24 | path, 25 | )?; 26 | let path = util::path_to_str(&path)?; 27 | 28 | // Ignore path if it contains unsupported characters, or if it's in the exclude 29 | // list. 30 | if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) { 31 | continue; 32 | } 33 | if !Path::new(path).is_dir() { 34 | bail!("not a directory: {path}"); 35 | } 36 | 37 | let by = self.score.unwrap_or(1.0); 38 | db.add_update(path, by, now); 39 | } 40 | 41 | if db.dirty() { 42 | db.age(max_age); 43 | } 44 | db.save() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/cmd/cmd.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] 2 | 3 | use std::path::PathBuf; 4 | 5 | use clap::builder::{IntoResettable, Resettable, StyledStr}; 6 | use clap::{Parser, Subcommand, ValueEnum, ValueHint}; 7 | 8 | struct HelpTemplate; 9 | 10 | impl IntoResettable for HelpTemplate { 11 | fn into_resettable(self) -> Resettable { 12 | color_print::cstr!("\ 13 | {before-help}{name} {version} 14 | {author} 15 | https://github.com/ajeetdsouza/zoxide 16 | 17 | {about} 18 | 19 | {usage-heading} 20 | {tab}{usage} 21 | 22 | {all-args}{after-help} 23 | 24 | Environment variables: 25 | {tab}_ZO_DATA_DIR {tab}Path for zoxide data files 26 | {tab}_ZO_ECHO {tab}Print the matched directory before navigating to it when set to 1 27 | {tab}_ZO_EXCLUDE_DIRS {tab}List of directory globs to be excluded 28 | {tab}_ZO_FZF_OPTS {tab}Custom flags to pass to fzf 29 | {tab}_ZO_MAXAGE {tab}Maximum total age after which entries start getting deleted 30 | {tab}_ZO_RESOLVE_SYMLINKS{tab}Resolve symlinks when storing paths").into_resettable() 31 | } 32 | } 33 | 34 | #[derive(Debug, Parser)] 35 | #[clap( 36 | about, 37 | author, 38 | help_template = HelpTemplate, 39 | disable_help_subcommand = true, 40 | propagate_version = true, 41 | version, 42 | )] 43 | pub enum Cmd { 44 | Add(Add), 45 | Edit(Edit), 46 | Import(Import), 47 | Init(Init), 48 | Query(Query), 49 | Remove(Remove), 50 | } 51 | 52 | /// Add a new directory or increment its rank 53 | #[derive(Debug, Parser)] 54 | #[clap( 55 | author, 56 | help_template = HelpTemplate, 57 | )] 58 | pub struct Add { 59 | #[clap(num_args = 1.., required = true, value_hint = ValueHint::DirPath)] 60 | pub paths: Vec, 61 | 62 | /// The rank to increment the entry if it exists or initialize it with if it 63 | /// doesn't 64 | #[clap(short, long)] 65 | pub score: Option, 66 | } 67 | 68 | /// Edit the database 69 | #[derive(Debug, Parser)] 70 | #[clap( 71 | author, 72 | help_template = HelpTemplate, 73 | )] 74 | pub struct Edit { 75 | #[clap(subcommand)] 76 | pub cmd: Option, 77 | } 78 | 79 | #[derive(Clone, Debug, Subcommand)] 80 | pub enum EditCommand { 81 | #[clap(hide = true)] 82 | Decrement { path: String }, 83 | #[clap(hide = true)] 84 | Delete { path: String }, 85 | #[clap(hide = true)] 86 | Increment { path: String }, 87 | #[clap(hide = true)] 88 | Reload, 89 | } 90 | 91 | /// Import entries from another application 92 | #[derive(Debug, Parser)] 93 | #[clap( 94 | author, 95 | help_template = HelpTemplate, 96 | )] 97 | pub struct Import { 98 | #[clap(value_hint = ValueHint::FilePath)] 99 | pub path: PathBuf, 100 | 101 | /// Application to import from 102 | #[clap(value_enum, long)] 103 | pub from: ImportFrom, 104 | 105 | /// Merge into existing database 106 | #[clap(long)] 107 | pub merge: bool, 108 | } 109 | 110 | #[derive(ValueEnum, Clone, Debug)] 111 | pub enum ImportFrom { 112 | Autojump, 113 | #[clap(alias = "fasd")] 114 | Z, 115 | } 116 | 117 | /// Generate shell configuration 118 | #[derive(Debug, Parser)] 119 | #[clap( 120 | author, 121 | help_template = HelpTemplate, 122 | )] 123 | pub struct Init { 124 | #[clap(value_enum)] 125 | pub shell: InitShell, 126 | 127 | /// Prevents zoxide from defining the `z` and `zi` commands 128 | #[clap(long, alias = "no-aliases")] 129 | pub no_cmd: bool, 130 | 131 | /// Changes the prefix of the `z` and `zi` commands 132 | #[clap(long, default_value = "z")] 133 | pub cmd: String, 134 | 135 | /// Changes how often zoxide increments a directory's score 136 | #[clap(value_enum, long, default_value = "pwd")] 137 | pub hook: InitHook, 138 | } 139 | 140 | #[derive(ValueEnum, Clone, Copy, Debug, Eq, PartialEq)] 141 | pub enum InitHook { 142 | None, 143 | Prompt, 144 | Pwd, 145 | } 146 | 147 | #[derive(ValueEnum, Clone, Debug)] 148 | pub enum InitShell { 149 | Bash, 150 | Elvish, 151 | Fish, 152 | Nushell, 153 | #[clap(alias = "ksh")] 154 | Posix, 155 | Powershell, 156 | Tcsh, 157 | Xonsh, 158 | Zsh, 159 | } 160 | 161 | /// Search for a directory in the database 162 | #[derive(Debug, Parser)] 163 | #[clap( 164 | author, 165 | help_template = HelpTemplate, 166 | )] 167 | pub struct Query { 168 | pub keywords: Vec, 169 | 170 | /// Show unavailable directories 171 | #[clap(long, short)] 172 | pub all: bool, 173 | 174 | /// Use interactive selection 175 | #[clap(long, short, conflicts_with = "list")] 176 | pub interactive: bool, 177 | 178 | /// List all matching directories 179 | #[clap(long, short, conflicts_with = "interactive")] 180 | pub list: bool, 181 | 182 | /// Print score with results 183 | #[clap(long, short)] 184 | pub score: bool, 185 | 186 | /// Exclude the current directory 187 | #[clap(long, value_hint = ValueHint::DirPath, value_name = "path")] 188 | pub exclude: Option, 189 | } 190 | 191 | /// Remove a directory from the database 192 | #[derive(Debug, Parser)] 193 | #[clap( 194 | author, 195 | help_template = HelpTemplate, 196 | )] 197 | pub struct Remove { 198 | #[clap(value_hint = ValueHint::DirPath)] 199 | pub paths: Vec, 200 | } 201 | -------------------------------------------------------------------------------- /src/cmd/edit.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use anyhow::Result; 4 | 5 | use crate::cmd::{Edit, EditCommand, Run}; 6 | use crate::db::Database; 7 | use crate::error::BrokenPipeHandler; 8 | use crate::util::{self, Fzf, FzfChild}; 9 | 10 | impl Run for Edit { 11 | fn run(&self) -> Result<()> { 12 | let now = util::current_time()?; 13 | let db = &mut Database::open()?; 14 | 15 | match &self.cmd { 16 | Some(cmd) => { 17 | match cmd { 18 | EditCommand::Decrement { path } => db.add(path, -1.0, now), 19 | EditCommand::Delete { path } => { 20 | db.remove(path); 21 | } 22 | EditCommand::Increment { path } => db.add(path, 1.0, now), 23 | EditCommand::Reload => {} 24 | } 25 | db.save()?; 26 | 27 | let stdout = &mut io::stdout().lock(); 28 | for dir in db.dirs().iter().rev() { 29 | write!(stdout, "{}\0", dir.display().with_score(now).with_separator('\t')) 30 | .pipe_exit("fzf")?; 31 | } 32 | Ok(()) 33 | } 34 | None => { 35 | db.sort_by_score(now); 36 | db.save()?; 37 | Self::get_fzf()?.wait()?; 38 | Ok(()) 39 | } 40 | } 41 | } 42 | } 43 | 44 | impl Edit { 45 | fn get_fzf() -> Result { 46 | Fzf::new()? 47 | .args([ 48 | // Search mode 49 | "--exact", 50 | // Search result 51 | "--no-sort", 52 | // Interface 53 | "--bind=\ 54 | btab:up,\ 55 | ctrl-r:reload(zoxide edit reload),\ 56 | ctrl-d:reload(zoxide edit delete {2..}),\ 57 | ctrl-w:reload(zoxide edit increment {2..}),\ 58 | ctrl-s:reload(zoxide edit decrement {2..}),\ 59 | ctrl-z:ignore,\ 60 | double-click:ignore,\ 61 | enter:abort,\ 62 | start:reload(zoxide edit reload),\ 63 | tab:down", 64 | "--cycle", 65 | "--keep-right", 66 | // Layout 67 | "--border=sharp", 68 | "--border-label= zoxide-edit ", 69 | "--header=\ 70 | ctrl-r:reload \tctrl-d:delete 71 | ctrl-w:increment\tctrl-s:decrement 72 | 73 | SCORE\tPATH", 74 | "--info=inline", 75 | "--layout=reverse", 76 | "--padding=1,0,0,0", 77 | // Display 78 | "--color=label:bold", 79 | "--tabstop=1", 80 | ]) 81 | .enable_preview() 82 | .spawn() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/cmd/import.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::{Context, Result, bail}; 4 | 5 | use crate::cmd::{Import, ImportFrom, Run}; 6 | use crate::db::Database; 7 | 8 | impl Run for Import { 9 | fn run(&self) -> Result<()> { 10 | let buffer = fs::read_to_string(&self.path).with_context(|| { 11 | format!("could not open database for importing: {}", &self.path.display()) 12 | })?; 13 | 14 | let mut db = Database::open()?; 15 | if !self.merge && !db.dirs().is_empty() { 16 | bail!("current database is not empty, specify --merge to continue anyway"); 17 | } 18 | 19 | match self.from { 20 | ImportFrom::Autojump => import_autojump(&mut db, &buffer), 21 | ImportFrom::Z => import_z(&mut db, &buffer), 22 | } 23 | .context("import error")?; 24 | 25 | db.save() 26 | } 27 | } 28 | 29 | fn import_autojump(db: &mut Database, buffer: &str) -> Result<()> { 30 | for line in buffer.lines() { 31 | if line.is_empty() { 32 | continue; 33 | } 34 | let (rank, path) = 35 | line.split_once('\t').with_context(|| format!("invalid entry: {line}"))?; 36 | 37 | let mut rank = rank.parse::().with_context(|| format!("invalid rank: {rank}"))?; 38 | // Normalize the rank using a sigmoid function. Don't import actual ranks from 39 | // autojump, since its scoring algorithm is very different and might 40 | // take a while to get normalized. 41 | rank = sigmoid(rank); 42 | 43 | db.add_unchecked(path, rank, 0); 44 | } 45 | 46 | if db.dirty() { 47 | db.dedup(); 48 | } 49 | Ok(()) 50 | } 51 | 52 | fn import_z(db: &mut Database, buffer: &str) -> Result<()> { 53 | for line in buffer.lines() { 54 | if line.is_empty() { 55 | continue; 56 | } 57 | let mut split = line.rsplitn(3, '|'); 58 | 59 | let last_accessed = split.next().with_context(|| format!("invalid entry: {line}"))?; 60 | let last_accessed = 61 | last_accessed.parse().with_context(|| format!("invalid epoch: {last_accessed}"))?; 62 | 63 | let rank = split.next().with_context(|| format!("invalid entry: {line}"))?; 64 | let rank = rank.parse().with_context(|| format!("invalid rank: {rank}"))?; 65 | 66 | let path = split.next().with_context(|| format!("invalid entry: {line}"))?; 67 | 68 | db.add_unchecked(path, rank, last_accessed); 69 | } 70 | 71 | if db.dirty() { 72 | db.dedup(); 73 | } 74 | Ok(()) 75 | } 76 | 77 | fn sigmoid(x: f64) -> f64 { 78 | 1.0 / (1.0 + (-x).exp()) 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use crate::db::Dir; 85 | 86 | #[test] 87 | fn from_autojump() { 88 | let data_dir = tempfile::tempdir().unwrap(); 89 | let mut db = Database::open_dir(data_dir.path()).unwrap(); 90 | for (path, rank, last_accessed) in [ 91 | ("/quux/quuz", 1.0, 100), 92 | ("/corge/grault/garply", 6.0, 600), 93 | ("/waldo/fred/plugh", 3.0, 300), 94 | ("/xyzzy/thud", 8.0, 800), 95 | ("/foo/bar", 9.0, 900), 96 | ] { 97 | db.add_unchecked(path, rank, last_accessed); 98 | } 99 | 100 | let buffer = "\ 101 | 7.0 /baz 102 | 2.0 /foo/bar 103 | 5.0 /quux/quuz"; 104 | import_autojump(&mut db, buffer).unwrap(); 105 | 106 | db.sort_by_path(); 107 | println!("got: {:?}", &db.dirs()); 108 | 109 | let exp = [ 110 | Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 }, 111 | Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, 112 | Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 }, 113 | Dir { path: "/quux/quuz".into(), rank: 1.0 + sigmoid(5.0), last_accessed: 100 }, 114 | Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, 115 | Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, 116 | ]; 117 | println!("exp: {exp:?}"); 118 | 119 | for (dir1, dir2) in db.dirs().iter().zip(exp) { 120 | assert_eq!(dir1.path, dir2.path); 121 | assert!((dir1.rank - dir2.rank).abs() < 0.01); 122 | assert_eq!(dir1.last_accessed, dir2.last_accessed); 123 | } 124 | } 125 | 126 | #[test] 127 | fn from_z() { 128 | let data_dir = tempfile::tempdir().unwrap(); 129 | let mut db = Database::open_dir(data_dir.path()).unwrap(); 130 | for (path, rank, last_accessed) in [ 131 | ("/quux/quuz", 1.0, 100), 132 | ("/corge/grault/garply", 6.0, 600), 133 | ("/waldo/fred/plugh", 3.0, 300), 134 | ("/xyzzy/thud", 8.0, 800), 135 | ("/foo/bar", 9.0, 900), 136 | ] { 137 | db.add_unchecked(path, rank, last_accessed); 138 | } 139 | 140 | let buffer = "\ 141 | /baz|7|700 142 | /quux/quuz|4|400 143 | /foo/bar|2|200 144 | /quux/quuz|5|500"; 145 | import_z(&mut db, buffer).unwrap(); 146 | 147 | db.sort_by_path(); 148 | println!("got: {:?}", &db.dirs()); 149 | 150 | let exp = [ 151 | Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 }, 152 | Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, 153 | Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 }, 154 | Dir { path: "/quux/quuz".into(), rank: 10.0, last_accessed: 500 }, 155 | Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, 156 | Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, 157 | ]; 158 | println!("exp: {exp:?}"); 159 | 160 | for (dir1, dir2) in db.dirs().iter().zip(exp) { 161 | assert_eq!(dir1.path, dir2.path); 162 | assert!((dir1.rank - dir2.rank).abs() < 0.01); 163 | assert_eq!(dir1.last_accessed, dir2.last_accessed); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/cmd/init.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use anyhow::{Context, Result}; 4 | use askama::Template; 5 | 6 | use crate::cmd::{Init, InitShell, Run}; 7 | use crate::config; 8 | use crate::error::BrokenPipeHandler; 9 | use crate::shell::{Bash, Elvish, Fish, Nushell, Opts, Posix, Powershell, Tcsh, Xonsh, Zsh}; 10 | 11 | impl Run for Init { 12 | fn run(&self) -> Result<()> { 13 | let cmd = if self.no_cmd { None } else { Some(self.cmd.as_str()) }; 14 | let echo = config::echo(); 15 | let resolve_symlinks = config::resolve_symlinks(); 16 | let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks }; 17 | 18 | let source = match self.shell { 19 | InitShell::Bash => Bash(opts).render(), 20 | InitShell::Elvish => Elvish(opts).render(), 21 | InitShell::Fish => Fish(opts).render(), 22 | InitShell::Nushell => Nushell(opts).render(), 23 | InitShell::Posix => Posix(opts).render(), 24 | InitShell::Powershell => Powershell(opts).render(), 25 | InitShell::Tcsh => Tcsh(opts).render(), 26 | InitShell::Xonsh => Xonsh(opts).render(), 27 | InitShell::Zsh => Zsh(opts).render(), 28 | } 29 | .context("could not render template")?; 30 | writeln!(io::stdout(), "{source}").pipe_exit("stdout") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | mod add; 2 | mod cmd; 3 | mod edit; 4 | mod import; 5 | mod init; 6 | mod query; 7 | mod remove; 8 | 9 | use anyhow::Result; 10 | 11 | pub use crate::cmd::cmd::*; 12 | 13 | pub trait Run { 14 | fn run(&self) -> Result<()>; 15 | } 16 | 17 | impl Run for Cmd { 18 | fn run(&self) -> Result<()> { 19 | match self { 20 | Cmd::Add(cmd) => cmd.run(), 21 | Cmd::Edit(cmd) => cmd.run(), 22 | Cmd::Import(cmd) => cmd.run(), 23 | Cmd::Init(cmd) => cmd.run(), 24 | Cmd::Query(cmd) => cmd.run(), 25 | Cmd::Remove(cmd) => cmd.run(), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/cmd/query.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | use crate::cmd::{Query, Run}; 6 | use crate::config; 7 | use crate::db::{Database, Epoch, Stream, StreamOptions}; 8 | use crate::error::BrokenPipeHandler; 9 | use crate::util::{self, Fzf, FzfChild}; 10 | 11 | impl Run for Query { 12 | fn run(&self) -> Result<()> { 13 | let mut db = crate::db::Database::open()?; 14 | self.query(&mut db).and(db.save()) 15 | } 16 | } 17 | 18 | impl Query { 19 | fn query(&self, db: &mut Database) -> Result<()> { 20 | let now = util::current_time()?; 21 | let mut stream = self.get_stream(db, now)?; 22 | 23 | if self.interactive { 24 | self.query_interactive(&mut stream, now) 25 | } else if self.list { 26 | self.query_list(&mut stream, now) 27 | } else { 28 | self.query_first(&mut stream, now) 29 | } 30 | } 31 | 32 | fn query_interactive(&self, stream: &mut Stream, now: Epoch) -> Result<()> { 33 | let mut fzf = Self::get_fzf()?; 34 | let selection = loop { 35 | match stream.next() { 36 | Some(dir) if Some(dir.path.as_ref()) == self.exclude.as_deref() => continue, 37 | Some(dir) => { 38 | if let Some(selection) = fzf.write(dir, now)? { 39 | break selection; 40 | } 41 | } 42 | None => break fzf.wait()?, 43 | } 44 | }; 45 | 46 | if self.score { 47 | print!("{selection}"); 48 | } else { 49 | let path = selection.get(7..).context("could not read selection from fzf")?; 50 | print!("{path}"); 51 | } 52 | Ok(()) 53 | } 54 | 55 | fn query_list(&self, stream: &mut Stream, now: Epoch) -> Result<()> { 56 | let handle = &mut io::stdout().lock(); 57 | while let Some(dir) = stream.next() { 58 | if Some(dir.path.as_ref()) == self.exclude.as_deref() { 59 | continue; 60 | } 61 | let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; 62 | writeln!(handle, "{dir}").pipe_exit("stdout")?; 63 | } 64 | Ok(()) 65 | } 66 | 67 | fn query_first(&self, stream: &mut Stream, now: Epoch) -> Result<()> { 68 | let handle = &mut io::stdout(); 69 | 70 | let mut dir = stream.next().context("no match found")?; 71 | while Some(dir.path.as_ref()) == self.exclude.as_deref() { 72 | dir = stream.next().context("you are already in the only match")?; 73 | } 74 | 75 | let dir = if self.score { dir.display().with_score(now) } else { dir.display() }; 76 | writeln!(handle, "{dir}").pipe_exit("stdout") 77 | } 78 | 79 | fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Result> { 80 | let mut options = StreamOptions::new(now) 81 | .with_keywords(self.keywords.iter().map(|s| s.as_str())) 82 | .with_exclude(config::exclude_dirs()?); 83 | if !self.all { 84 | let resolve_symlinks = config::resolve_symlinks(); 85 | options = options.with_exists(true).with_resolve_symlinks(resolve_symlinks); 86 | } 87 | 88 | let stream = Stream::new(db, options); 89 | Ok(stream) 90 | } 91 | 92 | fn get_fzf() -> Result { 93 | let mut fzf = Fzf::new()?; 94 | if let Some(fzf_opts) = config::fzf_opts() { 95 | fzf.env("FZF_DEFAULT_OPTS", fzf_opts) 96 | } else { 97 | fzf.args([ 98 | // Search mode 99 | "--exact", 100 | // Search result 101 | "--no-sort", 102 | // Interface 103 | "--bind=ctrl-z:ignore,btab:up,tab:down", 104 | "--cycle", 105 | "--keep-right", 106 | // Layout 107 | "--border=sharp", // rounded edges don't display correctly on some terminals 108 | "--height=45%", 109 | "--info=inline", 110 | "--layout=reverse", 111 | // Display 112 | "--tabstop=1", 113 | // Scripting 114 | "--exit-0", 115 | ]) 116 | .enable_preview() 117 | } 118 | .spawn() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/cmd/remove.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | 3 | use crate::cmd::{Remove, Run}; 4 | use crate::db::Database; 5 | use crate::util; 6 | 7 | impl Run for Remove { 8 | fn run(&self) -> Result<()> { 9 | let mut db = Database::open()?; 10 | 11 | for path in &self.paths { 12 | if !db.remove(path) { 13 | let path_abs = util::resolve_path(path)?; 14 | let path_abs = util::path_to_str(&path_abs)?; 15 | if path_abs == path || !db.remove(path_abs) { 16 | bail!("path not found in database: {path}") 17 | } 18 | } 19 | } 20 | 21 | db.save() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | use std::path::PathBuf; 4 | 5 | use anyhow::{Context, Result, ensure}; 6 | use glob::Pattern; 7 | 8 | use crate::db::Rank; 9 | 10 | pub fn data_dir() -> Result { 11 | let dir = match env::var_os("_ZO_DATA_DIR") { 12 | Some(path) => PathBuf::from(path), 13 | None => dirs::data_local_dir() 14 | .context("could not find data directory, please set _ZO_DATA_DIR manually")? 15 | .join("zoxide"), 16 | }; 17 | 18 | ensure!(dir.is_absolute(), "_ZO_DATA_DIR must be an absolute path"); 19 | Ok(dir) 20 | } 21 | 22 | pub fn echo() -> bool { 23 | env::var_os("_ZO_ECHO").is_some_and(|var| var == "1") 24 | } 25 | 26 | pub fn exclude_dirs() -> Result> { 27 | match env::var_os("_ZO_EXCLUDE_DIRS") { 28 | Some(paths) => env::split_paths(&paths) 29 | .map(|path| { 30 | let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?; 31 | Pattern::new(pattern) 32 | .with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {pattern}")) 33 | }) 34 | .collect(), 35 | None => { 36 | let pattern = (|| { 37 | let home = dirs::home_dir()?; 38 | let home = Pattern::escape(home.to_str()?); 39 | Pattern::new(&home).ok() 40 | })(); 41 | Ok(pattern.into_iter().collect()) 42 | } 43 | } 44 | } 45 | 46 | pub fn fzf_opts() -> Option { 47 | env::var_os("_ZO_FZF_OPTS") 48 | } 49 | 50 | pub fn maxage() -> Result { 51 | env::var_os("_ZO_MAXAGE").map_or(Ok(10_000.0), |maxage| { 52 | let maxage = maxage.to_str().context("invalid unicode in _ZO_MAXAGE")?; 53 | let maxage = maxage 54 | .parse::() 55 | .with_context(|| format!("unable to parse _ZO_MAXAGE as integer: {maxage}"))?; 56 | Ok(maxage as Rank) 57 | }) 58 | } 59 | 60 | pub fn resolve_symlinks() -> bool { 61 | env::var_os("_ZO_RESOLVE_SYMLINKS").is_some_and(|var| var == "1") 62 | } 63 | -------------------------------------------------------------------------------- /src/db/dir.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::{self, Display, Formatter}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::util::{DAY, HOUR, WEEK}; 7 | 8 | #[derive(Clone, Debug, Deserialize, Serialize)] 9 | pub struct Dir<'a> { 10 | #[serde(borrow)] 11 | pub path: Cow<'a, str>, 12 | pub rank: Rank, 13 | pub last_accessed: Epoch, 14 | } 15 | 16 | impl Dir<'_> { 17 | pub fn display(&self) -> DirDisplay<'_> { 18 | DirDisplay::new(self) 19 | } 20 | 21 | pub fn score(&self, now: Epoch) -> Rank { 22 | // The older the entry, the lesser its importance. 23 | let duration = now.saturating_sub(self.last_accessed); 24 | if duration < HOUR { 25 | self.rank * 4.0 26 | } else if duration < DAY { 27 | self.rank * 2.0 28 | } else if duration < WEEK { 29 | self.rank * 0.5 30 | } else { 31 | self.rank * 0.25 32 | } 33 | } 34 | } 35 | 36 | pub struct DirDisplay<'a> { 37 | dir: &'a Dir<'a>, 38 | now: Option, 39 | separator: char, 40 | } 41 | 42 | impl<'a> DirDisplay<'a> { 43 | fn new(dir: &'a Dir) -> Self { 44 | Self { dir, separator: ' ', now: None } 45 | } 46 | 47 | pub fn with_score(mut self, now: Epoch) -> Self { 48 | self.now = Some(now); 49 | self 50 | } 51 | 52 | pub fn with_separator(mut self, separator: char) -> Self { 53 | self.separator = separator; 54 | self 55 | } 56 | } 57 | 58 | impl Display for DirDisplay<'_> { 59 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 60 | if let Some(now) = self.now { 61 | let score = self.dir.score(now).clamp(0.0, 9999.0); 62 | write!(f, "{score:>6.1}{}", self.separator)?; 63 | } 64 | write!(f, "{}", self.dir.path) 65 | } 66 | } 67 | 68 | pub type Rank = f64; 69 | pub type Epoch = u64; 70 | -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | mod dir; 2 | mod stream; 3 | 4 | use std::path::{Path, PathBuf}; 5 | use std::{fs, io}; 6 | 7 | use anyhow::{Context, Result, bail}; 8 | use bincode::Options; 9 | use ouroboros::self_referencing; 10 | 11 | pub use crate::db::dir::{Dir, Epoch, Rank}; 12 | pub use crate::db::stream::{Stream, StreamOptions}; 13 | use crate::{config, util}; 14 | 15 | #[self_referencing] 16 | pub struct Database { 17 | path: PathBuf, 18 | bytes: Vec, 19 | #[borrows(bytes)] 20 | #[covariant] 21 | pub dirs: Vec>, 22 | dirty: bool, 23 | } 24 | 25 | impl Database { 26 | const VERSION: u32 = 3; 27 | 28 | pub fn open() -> Result { 29 | let data_dir = config::data_dir()?; 30 | Self::open_dir(data_dir) 31 | } 32 | 33 | pub fn open_dir(data_dir: impl AsRef) -> Result { 34 | let data_dir = data_dir.as_ref(); 35 | let path = data_dir.join("db.zo"); 36 | let path = fs::canonicalize(&path).unwrap_or(path); 37 | 38 | match fs::read(&path) { 39 | Ok(bytes) => Self::try_new(path, bytes, |bytes| Self::deserialize(bytes), false), 40 | Err(e) if e.kind() == io::ErrorKind::NotFound => { 41 | // Create data directory, but don't create any file yet. The file will be 42 | // created later by [`Database::save`] if any data is modified. 43 | fs::create_dir_all(data_dir).with_context(|| { 44 | format!("unable to create data directory: {}", data_dir.display()) 45 | })?; 46 | Ok(Self::new(path, Vec::new(), |_| Vec::new(), false)) 47 | } 48 | Err(e) => { 49 | Err(e).with_context(|| format!("could not read from database: {}", path.display())) 50 | } 51 | } 52 | } 53 | 54 | pub fn save(&mut self) -> Result<()> { 55 | // Only write to disk if the database is modified. 56 | if !self.dirty() { 57 | return Ok(()); 58 | } 59 | 60 | let bytes = Self::serialize(self.dirs())?; 61 | util::write(self.borrow_path(), bytes).context("could not write to database")?; 62 | self.with_dirty_mut(|dirty| *dirty = false); 63 | 64 | Ok(()) 65 | } 66 | 67 | /// Increments the rank of a directory, or creates it if it does not exist. 68 | pub fn add(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { 69 | self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { 70 | Some(dir) => dir.rank = (dir.rank + by).max(0.0), 71 | None => { 72 | dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) 73 | } 74 | }); 75 | self.with_dirty_mut(|dirty| *dirty = true); 76 | } 77 | 78 | /// Creates a new directory. This will create a duplicate entry if this 79 | /// directory is always in the database, it is expected that the user either 80 | /// does a check before calling this, or calls `dedup()` afterward. 81 | pub fn add_unchecked(&mut self, path: impl AsRef + Into, rank: Rank, now: Epoch) { 82 | self.with_dirs_mut(|dirs| { 83 | dirs.push(Dir { path: path.into().into(), rank, last_accessed: now }) 84 | }); 85 | self.with_dirty_mut(|dirty| *dirty = true); 86 | } 87 | 88 | /// Increments the rank and updates the last_accessed of a directory, or 89 | /// creates it if it does not exist. 90 | pub fn add_update(&mut self, path: impl AsRef + Into, by: Rank, now: Epoch) { 91 | self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) { 92 | Some(dir) => { 93 | dir.rank = (dir.rank + by).max(0.0); 94 | dir.last_accessed = now; 95 | } 96 | None => { 97 | dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }) 98 | } 99 | }); 100 | self.with_dirty_mut(|dirty| *dirty = true); 101 | } 102 | 103 | /// Removes the directory with `path` from the store. This does not preserve 104 | /// ordering, but is O(1). 105 | pub fn remove(&mut self, path: impl AsRef) -> bool { 106 | match self.dirs().iter().position(|dir| dir.path == path.as_ref()) { 107 | Some(idx) => { 108 | self.swap_remove(idx); 109 | true 110 | } 111 | None => false, 112 | } 113 | } 114 | 115 | pub fn swap_remove(&mut self, idx: usize) { 116 | self.with_dirs_mut(|dirs| dirs.swap_remove(idx)); 117 | self.with_dirty_mut(|dirty| *dirty = true); 118 | } 119 | 120 | pub fn age(&mut self, max_age: Rank) { 121 | let mut dirty = false; 122 | self.with_dirs_mut(|dirs| { 123 | let total_age = dirs.iter().map(|dir| dir.rank).sum::(); 124 | if total_age > max_age { 125 | let factor = 0.9 * max_age / total_age; 126 | for idx in (0..dirs.len()).rev() { 127 | let dir = &mut dirs[idx]; 128 | dir.rank *= factor; 129 | if dir.rank < 1.0 { 130 | dirs.swap_remove(idx); 131 | } 132 | } 133 | dirty = true; 134 | } 135 | }); 136 | self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty); 137 | } 138 | 139 | pub fn dedup(&mut self) { 140 | // Sort by path, so that equal paths are next to each other. 141 | self.sort_by_path(); 142 | 143 | let mut dirty = false; 144 | self.with_dirs_mut(|dirs| { 145 | for idx in (1..dirs.len()).rev() { 146 | // Check if curr_dir and next_dir have equal paths. 147 | let curr_dir = &dirs[idx]; 148 | let next_dir = &dirs[idx - 1]; 149 | if next_dir.path != curr_dir.path { 150 | continue; 151 | } 152 | 153 | // Merge curr_dir's rank and last_accessed into next_dir. 154 | let rank = curr_dir.rank; 155 | let last_accessed = curr_dir.last_accessed; 156 | let next_dir = &mut dirs[idx - 1]; 157 | next_dir.last_accessed = next_dir.last_accessed.max(last_accessed); 158 | next_dir.rank += rank; 159 | 160 | // Delete curr_dir. 161 | dirs.swap_remove(idx); 162 | dirty = true; 163 | } 164 | }); 165 | self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty); 166 | } 167 | 168 | pub fn sort_by_path(&mut self) { 169 | self.with_dirs_mut(|dirs| dirs.sort_unstable_by(|dir1, dir2| dir1.path.cmp(&dir2.path))); 170 | self.with_dirty_mut(|dirty| *dirty = true); 171 | } 172 | 173 | pub fn sort_by_score(&mut self, now: Epoch) { 174 | self.with_dirs_mut(|dirs| { 175 | dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| { 176 | dir1.score(now).total_cmp(&dir2.score(now)) 177 | }) 178 | }); 179 | self.with_dirty_mut(|dirty| *dirty = true); 180 | } 181 | 182 | pub fn dirty(&self) -> bool { 183 | *self.borrow_dirty() 184 | } 185 | 186 | pub fn dirs(&self) -> &[Dir] { 187 | self.borrow_dirs() 188 | } 189 | 190 | fn serialize(dirs: &[Dir<'_>]) -> Result> { 191 | (|| -> bincode::Result<_> { 192 | // Preallocate buffer with combined size of sections. 193 | let buffer_size = 194 | bincode::serialized_size(&Self::VERSION)? + bincode::serialized_size(&dirs)?; 195 | let mut buffer = Vec::with_capacity(buffer_size as usize); 196 | 197 | // Serialize sections into buffer. 198 | bincode::serialize_into(&mut buffer, &Self::VERSION)?; 199 | bincode::serialize_into(&mut buffer, &dirs)?; 200 | 201 | Ok(buffer) 202 | })() 203 | .context("could not serialize database") 204 | } 205 | 206 | fn deserialize(bytes: &[u8]) -> Result> { 207 | // Assume a maximum size for the database. This prevents bincode from throwing 208 | // strange errors when it encounters invalid data. 209 | const MAX_SIZE: u64 = 32 << 20; // 32 MiB 210 | let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE); 211 | 212 | // Split bytes into sections. 213 | let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _; 214 | if bytes.len() < version_size { 215 | bail!("could not deserialize database: corrupted data"); 216 | } 217 | let (bytes_version, bytes_dirs) = bytes.split_at(version_size); 218 | 219 | // Deserialize sections. 220 | let version = deserializer.deserialize(bytes_version)?; 221 | let dirs = match version { 222 | Self::VERSION => { 223 | deserializer.deserialize(bytes_dirs).context("could not deserialize database")? 224 | } 225 | version => { 226 | bail!("unsupported version (got {version}, supports {})", Self::VERSION) 227 | } 228 | }; 229 | 230 | Ok(dirs) 231 | } 232 | } 233 | 234 | #[cfg(test)] 235 | mod tests { 236 | use super::*; 237 | 238 | #[test] 239 | fn add() { 240 | let data_dir = tempfile::tempdir().unwrap(); 241 | let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; 242 | let now = 946684800; 243 | 244 | { 245 | let mut db = Database::open_dir(data_dir.path()).unwrap(); 246 | db.add(path, 1.0, now); 247 | db.add(path, 1.0, now); 248 | db.save().unwrap(); 249 | } 250 | 251 | { 252 | let db = Database::open_dir(data_dir.path()).unwrap(); 253 | assert_eq!(db.dirs().len(), 1); 254 | 255 | let dir = &db.dirs()[0]; 256 | assert_eq!(dir.path, path); 257 | assert!((dir.rank - 2.0).abs() < 0.01); 258 | assert_eq!(dir.last_accessed, now); 259 | } 260 | } 261 | 262 | #[test] 263 | fn remove() { 264 | let data_dir = tempfile::tempdir().unwrap(); 265 | let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; 266 | let now = 946684800; 267 | 268 | { 269 | let mut db = Database::open_dir(data_dir.path()).unwrap(); 270 | db.add(path, 1.0, now); 271 | db.save().unwrap(); 272 | } 273 | 274 | { 275 | let mut db = Database::open_dir(data_dir.path()).unwrap(); 276 | assert!(db.remove(path)); 277 | db.save().unwrap(); 278 | } 279 | 280 | { 281 | let mut db = Database::open_dir(data_dir.path()).unwrap(); 282 | assert!(db.dirs().is_empty()); 283 | assert!(!db.remove(path)); 284 | db.save().unwrap(); 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/db/stream.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Rev; 2 | use std::ops::Range; 3 | use std::{fs, path}; 4 | 5 | use glob::Pattern; 6 | 7 | use crate::db::{Database, Dir, Epoch}; 8 | use crate::util::{self, MONTH}; 9 | 10 | pub struct Stream<'a> { 11 | db: &'a mut Database, 12 | idxs: Rev>, 13 | options: StreamOptions, 14 | } 15 | 16 | impl<'a> Stream<'a> { 17 | pub fn new(db: &'a mut Database, options: StreamOptions) -> Self { 18 | db.sort_by_score(options.now); 19 | let idxs = (0..db.dirs().len()).rev(); 20 | Stream { db, idxs, options } 21 | } 22 | 23 | pub fn next(&mut self) -> Option<&Dir> { 24 | while let Some(idx) = self.idxs.next() { 25 | let dir = &self.db.dirs()[idx]; 26 | 27 | if !self.filter_by_keywords(&dir.path) { 28 | continue; 29 | } 30 | 31 | if !self.filter_by_exclude(&dir.path) { 32 | self.db.swap_remove(idx); 33 | continue; 34 | } 35 | 36 | if !self.filter_by_exists(&dir.path) { 37 | if dir.last_accessed < self.options.ttl { 38 | self.db.swap_remove(idx); 39 | } 40 | continue; 41 | } 42 | 43 | let dir = &self.db.dirs()[idx]; 44 | return Some(dir); 45 | } 46 | 47 | None 48 | } 49 | 50 | fn filter_by_keywords(&self, path: &str) -> bool { 51 | let (keywords_last, keywords) = match self.options.keywords.split_last() { 52 | Some(split) => split, 53 | None => return true, 54 | }; 55 | 56 | let path = util::to_lowercase(path); 57 | let mut path = path.as_str(); 58 | match path.rfind(keywords_last) { 59 | Some(idx) => { 60 | if path[idx + keywords_last.len()..].contains(path::is_separator) { 61 | return false; 62 | } 63 | path = &path[..idx]; 64 | } 65 | None => return false, 66 | } 67 | 68 | for keyword in keywords.iter().rev() { 69 | match path.rfind(keyword) { 70 | Some(idx) => path = &path[..idx], 71 | None => return false, 72 | } 73 | } 74 | 75 | true 76 | } 77 | 78 | fn filter_by_exclude(&self, path: &str) -> bool { 79 | !self.options.exclude.iter().any(|pattern| pattern.matches(path)) 80 | } 81 | 82 | fn filter_by_exists(&self, path: &str) -> bool { 83 | if !self.options.exists { 84 | return true; 85 | } 86 | 87 | // The logic here is reversed - if we resolve symlinks when adding entries to 88 | // the database, we should not return symlinks when querying back from 89 | // the database. 90 | let resolver = 91 | if self.options.resolve_symlinks { fs::symlink_metadata } else { fs::metadata }; 92 | resolver(path).map(|metadata| metadata.is_dir()).unwrap_or_default() 93 | } 94 | } 95 | 96 | pub struct StreamOptions { 97 | /// The current time. 98 | now: Epoch, 99 | 100 | /// Only directories matching these keywords will be returned. 101 | keywords: Vec, 102 | 103 | /// Directories that match any of these globs will be lazily removed. 104 | exclude: Vec, 105 | 106 | /// Directories will only be returned if they exist on the filesystem. 107 | exists: bool, 108 | 109 | /// Whether to resolve symlinks when checking if a directory exists. 110 | resolve_symlinks: bool, 111 | 112 | /// Directories that do not exist and haven't been accessed since TTL will 113 | /// be lazily removed. 114 | ttl: Epoch, 115 | } 116 | 117 | impl StreamOptions { 118 | pub fn new(now: Epoch) -> Self { 119 | StreamOptions { 120 | now, 121 | keywords: Vec::new(), 122 | exclude: Vec::new(), 123 | exists: false, 124 | resolve_symlinks: false, 125 | ttl: now.saturating_sub(3 * MONTH), 126 | } 127 | } 128 | 129 | pub fn with_keywords(mut self, keywords: I) -> Self 130 | where 131 | I: IntoIterator, 132 | I::Item: AsRef, 133 | { 134 | self.keywords = keywords.into_iter().map(util::to_lowercase).collect(); 135 | self 136 | } 137 | 138 | pub fn with_exclude(mut self, exclude: Vec) -> Self { 139 | self.exclude = exclude; 140 | self 141 | } 142 | 143 | pub fn with_exists(mut self, exists: bool) -> Self { 144 | self.exists = exists; 145 | self 146 | } 147 | 148 | pub fn with_resolve_symlinks(mut self, resolve_symlinks: bool) -> Self { 149 | self.resolve_symlinks = resolve_symlinks; 150 | self 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use std::path::PathBuf; 157 | 158 | use rstest::rstest; 159 | 160 | use super::*; 161 | 162 | #[rstest] 163 | // Case normalization 164 | #[case(&["fOo", "bAr"], "/foo/bar", true)] 165 | // Last component 166 | #[case(&["ba"], "/foo/bar", true)] 167 | #[case(&["fo"], "/foo/bar", false)] 168 | // Slash as suffix 169 | #[case(&["foo/"], "/foo", false)] 170 | #[case(&["foo/"], "/foo/bar", true)] 171 | #[case(&["foo/"], "/foo/bar/baz", false)] 172 | #[case(&["foo", "/"], "/foo", false)] 173 | #[case(&["foo", "/"], "/foo/bar", true)] 174 | #[case(&["foo", "/"], "/foo/bar/baz", true)] 175 | // Split components 176 | #[case(&["/", "fo", "/", "ar"], "/foo/bar", true)] 177 | #[case(&["oo/ba"], "/foo/bar", true)] 178 | // Overlap 179 | #[case(&["foo", "o", "bar"], "/foo/bar", false)] 180 | #[case(&["/foo/", "/bar"], "/foo/bar", false)] 181 | #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] 182 | fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { 183 | let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); 184 | let options = StreamOptions::new(0).with_keywords(keywords.iter()); 185 | let stream = Stream::new(db, options); 186 | assert_eq!(is_match, stream.filter_by_keywords(path)); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | use std::io; 3 | 4 | use anyhow::{Context, Result, bail}; 5 | 6 | /// Custom error type for early exit. 7 | #[derive(Debug)] 8 | pub struct SilentExit { 9 | pub code: u8, 10 | } 11 | 12 | impl Display for SilentExit { 13 | fn fmt(&self, _: &mut Formatter<'_>) -> fmt::Result { 14 | Ok(()) 15 | } 16 | } 17 | 18 | pub trait BrokenPipeHandler { 19 | fn pipe_exit(self, device: &str) -> Result<()>; 20 | } 21 | 22 | impl BrokenPipeHandler for io::Result<()> { 23 | fn pipe_exit(self, device: &str) -> Result<()> { 24 | match self { 25 | Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), 26 | result => result.with_context(|| format!("could not write to {device}")), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::single_component_path_imports)] 2 | 3 | mod cmd; 4 | mod config; 5 | mod db; 6 | mod error; 7 | mod shell; 8 | mod util; 9 | 10 | use std::env; 11 | use std::io::{self, Write}; 12 | use std::process::ExitCode; 13 | 14 | use clap::Parser; 15 | 16 | use crate::cmd::{Cmd, Run}; 17 | use crate::error::SilentExit; 18 | 19 | pub fn main() -> ExitCode { 20 | // Forcibly disable backtraces. 21 | unsafe { env::remove_var("RUST_LIB_BACKTRACE") }; 22 | unsafe { env::remove_var("RUST_BACKTRACE") }; 23 | 24 | match Cmd::parse().run() { 25 | Ok(()) => ExitCode::SUCCESS, 26 | Err(e) => match e.downcast::() { 27 | Ok(SilentExit { code }) => code.into(), 28 | Err(e) => { 29 | _ = writeln!(io::stderr(), "zoxide: {e:?}"); 30 | ExitCode::FAILURE 31 | } 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::InitHook; 2 | 3 | #[derive(Debug, Eq, PartialEq)] 4 | pub struct Opts<'a> { 5 | pub cmd: Option<&'a str>, 6 | pub hook: InitHook, 7 | pub echo: bool, 8 | pub resolve_symlinks: bool, 9 | } 10 | 11 | macro_rules! make_template { 12 | ($name:ident, $path:expr) => { 13 | #[derive(::std::fmt::Debug, ::askama::Template)] 14 | #[template(path = $path)] 15 | pub struct $name<'a>(pub &'a self::Opts<'a>); 16 | 17 | impl<'a> ::std::ops::Deref for $name<'a> { 18 | type Target = self::Opts<'a>; 19 | fn deref(&self) -> &Self::Target { 20 | self.0 21 | } 22 | } 23 | }; 24 | } 25 | 26 | make_template!(Bash, "bash.txt"); 27 | make_template!(Elvish, "elvish.txt"); 28 | make_template!(Fish, "fish.txt"); 29 | make_template!(Nushell, "nushell.txt"); 30 | make_template!(Posix, "posix.txt"); 31 | make_template!(Powershell, "powershell.txt"); 32 | make_template!(Tcsh, "tcsh.txt"); 33 | make_template!(Xonsh, "xonsh.txt"); 34 | make_template!(Zsh, "zsh.txt"); 35 | 36 | #[cfg(feature = "nix-dev")] 37 | #[cfg(test)] 38 | mod tests { 39 | use askama::Template; 40 | use assert_cmd::Command; 41 | use rstest::rstest; 42 | use rstest_reuse::{apply, template}; 43 | 44 | use super::*; 45 | 46 | #[template] 47 | #[rstest] 48 | fn opts( 49 | #[values(None, Some("z"))] cmd: Option<&str>, 50 | #[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook, 51 | #[values(false, true)] echo: bool, 52 | #[values(false, true)] resolve_symlinks: bool, 53 | ) { 54 | } 55 | 56 | #[apply(opts)] 57 | fn bash_bash(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 58 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 59 | let source = Bash(&opts).render().unwrap(); 60 | Command::new("bash") 61 | .args(["--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) 62 | .assert() 63 | .success() 64 | .stdout("") 65 | .stderr(""); 66 | } 67 | 68 | #[apply(opts)] 69 | fn bash_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 70 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 71 | let source = Bash(&opts).render().unwrap(); 72 | 73 | Command::new("shellcheck") 74 | .args(["--enable=all", "-"]) 75 | .write_stdin(source) 76 | .assert() 77 | .success() 78 | .stdout("") 79 | .stderr(""); 80 | } 81 | 82 | #[apply(opts)] 83 | fn bash_shfmt(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 84 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 85 | let mut source = Bash(&opts).render().unwrap(); 86 | source.push('\n'); 87 | 88 | Command::new("shfmt") 89 | .args(["--diff", "--indent=4", "--language-dialect=bash", "--simplify", "-"]) 90 | .write_stdin(source) 91 | .assert() 92 | .success() 93 | .stdout("") 94 | .stderr(""); 95 | } 96 | 97 | #[apply(opts)] 98 | fn elvish_elvish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 99 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 100 | let mut source = String::default(); 101 | 102 | // Filter out lines using edit:*, since those functions are only available in 103 | // the interactive editor. 104 | for line in Elvish(&opts).render().unwrap().lines().filter(|line| !line.contains("edit:")) { 105 | source.push_str(line); 106 | source.push('\n'); 107 | } 108 | 109 | Command::new("elvish") 110 | .args(["-c", &source, "-norc"]) 111 | .assert() 112 | .success() 113 | .stdout("") 114 | .stderr(""); 115 | } 116 | 117 | #[apply(opts)] 118 | fn fish_no_builtin_abbr(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 119 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 120 | let source = Fish(&opts).render().unwrap(); 121 | assert!( 122 | !source.contains("builtin abbr"), 123 | "`builtin abbr` does not work on older versions of Fish" 124 | ); 125 | } 126 | 127 | #[apply(opts)] 128 | fn fish_fish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 129 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 130 | let source = Fish(&opts).render().unwrap(); 131 | 132 | let tempdir = tempfile::tempdir().unwrap(); 133 | let tempdir = tempdir.path().to_str().unwrap(); 134 | 135 | Command::new("fish") 136 | .env("HOME", tempdir) 137 | .args(["--command", &source, "--no-config", "--private"]) 138 | .assert() 139 | .success() 140 | .stdout("") 141 | .stderr(""); 142 | } 143 | 144 | #[apply(opts)] 145 | fn fish_fishindent(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 146 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 147 | let mut source = Fish(&opts).render().unwrap(); 148 | source.push('\n'); 149 | 150 | let tempdir = tempfile::tempdir().unwrap(); 151 | let tempdir = tempdir.path().to_str().unwrap(); 152 | 153 | Command::new("fish_indent") 154 | .env("HOME", tempdir) 155 | .write_stdin(source.to_string()) 156 | .assert() 157 | .success() 158 | .stdout(source) 159 | .stderr(""); 160 | } 161 | 162 | #[apply(opts)] 163 | fn nushell_nushell(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 164 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 165 | let source = Nushell(&opts).render().unwrap(); 166 | 167 | let tempdir = tempfile::tempdir().unwrap(); 168 | let tempdir = tempdir.path(); 169 | 170 | let assert = Command::new("nu") 171 | .env("HOME", tempdir) 172 | .args(["--commands", &source]) 173 | .assert() 174 | .success() 175 | .stderr(""); 176 | 177 | if opts.hook != InitHook::Pwd { 178 | assert.stdout(""); 179 | } 180 | } 181 | 182 | #[apply(opts)] 183 | fn posix_bash(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 184 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 185 | let source = Posix(&opts).render().unwrap(); 186 | 187 | let assert = Command::new("bash") 188 | .args(["--posix", "--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) 189 | .assert() 190 | .success() 191 | .stderr(""); 192 | if opts.hook != InitHook::Pwd { 193 | assert.stdout(""); 194 | } 195 | } 196 | 197 | #[apply(opts)] 198 | fn posix_dash(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 199 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 200 | let source = Posix(&opts).render().unwrap(); 201 | 202 | let assert = 203 | Command::new("dash").args(["-e", "-u", "-c", &source]).assert().success().stderr(""); 204 | if opts.hook != InitHook::Pwd { 205 | assert.stdout(""); 206 | } 207 | } 208 | 209 | #[apply(opts)] 210 | fn posix_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 211 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 212 | let source = Posix(&opts).render().unwrap(); 213 | 214 | Command::new("shellcheck") 215 | .args(["--enable=all", "-"]) 216 | .write_stdin(source) 217 | .assert() 218 | .success() 219 | .stdout("") 220 | .stderr(""); 221 | } 222 | 223 | #[apply(opts)] 224 | fn posix_shfmt(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 225 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 226 | let mut source = Posix(&opts).render().unwrap(); 227 | source.push('\n'); 228 | 229 | Command::new("shfmt") 230 | .args(["--diff", "--indent=4", "--language-dialect=posix", "--simplify", "-"]) 231 | .write_stdin(source) 232 | .assert() 233 | .success() 234 | .stdout("") 235 | .stderr(""); 236 | } 237 | 238 | #[apply(opts)] 239 | fn powershell_pwsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 240 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 241 | let mut source = "Set-StrictMode -Version latest\n".to_string(); 242 | Powershell(&opts).render_into(&mut source).unwrap(); 243 | 244 | Command::new("pwsh") 245 | .args(["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source]) 246 | .assert() 247 | .success() 248 | .stdout("") 249 | .stderr(""); 250 | } 251 | 252 | #[apply(opts)] 253 | fn tcsh_tcsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 254 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 255 | let source = Tcsh(&opts).render().unwrap(); 256 | 257 | Command::new("tcsh") 258 | .args(["-e", "-f", "-s"]) 259 | .write_stdin(source) 260 | .assert() 261 | .success() 262 | .stdout("") 263 | .stderr(""); 264 | } 265 | 266 | #[apply(opts)] 267 | fn xonsh_black(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 268 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 269 | let mut source = Xonsh(&opts).render().unwrap(); 270 | source.push('\n'); 271 | 272 | Command::new("black") 273 | .args(["--check", "--diff", "-"]) 274 | .write_stdin(source) 275 | .assert() 276 | .success() 277 | .stdout(""); 278 | } 279 | 280 | #[apply(opts)] 281 | fn xonsh_mypy(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 282 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 283 | let source = Xonsh(&opts).render().unwrap(); 284 | 285 | Command::new("mypy").args(["--command", &source, "--strict"]).assert().success().stderr(""); 286 | } 287 | 288 | #[apply(opts)] 289 | fn xonsh_pylint(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 290 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 291 | let mut source = Xonsh(&opts).render().unwrap(); 292 | source.push('\n'); 293 | 294 | Command::new("pylint") 295 | .args(["--from-stdin", "--persistent=n", "zoxide"]) 296 | .write_stdin(source) 297 | .assert() 298 | .success() 299 | .stderr(""); 300 | } 301 | 302 | #[apply(opts)] 303 | fn xonsh_xonsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 304 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 305 | let source = Xonsh(&opts).render().unwrap(); 306 | 307 | let tempdir = tempfile::tempdir().unwrap(); 308 | let tempdir = tempdir.path().to_str().unwrap(); 309 | 310 | Command::new("xonsh") 311 | .args(["-c", &source, "--no-rc"]) 312 | .env("HOME", tempdir) 313 | .assert() 314 | .success() 315 | .stdout("") 316 | .stderr(""); 317 | } 318 | 319 | #[apply(opts)] 320 | fn zsh_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 321 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 322 | let source = Zsh(&opts).render().unwrap(); 323 | 324 | // ShellCheck doesn't support zsh yet: https://github.com/koalaman/shellcheck/issues/809 325 | Command::new("shellcheck") 326 | .args(["--enable=all", "-"]) 327 | .write_stdin(source) 328 | .assert() 329 | .success() 330 | .stdout("") 331 | .stderr(""); 332 | } 333 | 334 | #[apply(opts)] 335 | fn zsh_zsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { 336 | let opts = Opts { cmd, hook, echo, resolve_symlinks }; 337 | let source = Zsh(&opts).render().unwrap(); 338 | 339 | Command::new("zsh") 340 | .args(["-e", "-u", "-o", "pipefail", "--no-globalrcs", "--no-rcs", "-c", &source]) 341 | .assert() 342 | .success() 343 | .stdout("") 344 | .stderr(""); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fs::{self, File, OpenOptions}; 3 | use std::io::{self, Read, Write}; 4 | use std::path::{Component, Path, PathBuf}; 5 | use std::process::{Child, Command, Stdio}; 6 | use std::time::SystemTime; 7 | use std::{env, mem}; 8 | 9 | #[cfg(windows)] 10 | use anyhow::anyhow; 11 | use anyhow::{Context, Result, bail}; 12 | 13 | use crate::db::{Dir, Epoch}; 14 | use crate::error::SilentExit; 15 | 16 | pub const SECOND: Epoch = 1; 17 | pub const MINUTE: Epoch = 60 * SECOND; 18 | pub const HOUR: Epoch = 60 * MINUTE; 19 | pub const DAY: Epoch = 24 * HOUR; 20 | pub const WEEK: Epoch = 7 * DAY; 21 | pub const MONTH: Epoch = 30 * DAY; 22 | 23 | pub struct Fzf(Command); 24 | 25 | impl Fzf { 26 | const ERR_FZF_NOT_FOUND: &'static str = "could not find fzf, is it installed?"; 27 | 28 | pub fn new() -> Result { 29 | // On Windows, CreateProcess implicitly searches the current working 30 | // directory for the executable, which is a potential security issue. 31 | // Instead, we resolve the path to the executable and then pass it to 32 | // CreateProcess. 33 | #[cfg(windows)] 34 | let program = which::which("fzf.exe").map_err(|_| anyhow!(Self::ERR_FZF_NOT_FOUND))?; 35 | #[cfg(not(windows))] 36 | let program = "fzf"; 37 | 38 | // TODO: check version of fzf here. 39 | 40 | let mut cmd = Command::new(program); 41 | cmd.args([ 42 | // Search mode 43 | "--delimiter=\t", 44 | "--nth=2", 45 | // Scripting 46 | "--read0", 47 | ]) 48 | .stdin(Stdio::piped()) 49 | .stdout(Stdio::piped()); 50 | 51 | Ok(Fzf(cmd)) 52 | } 53 | 54 | pub fn enable_preview(&mut self) -> &mut Self { 55 | // Previews are only supported on UNIX. 56 | if !cfg!(unix) { 57 | return self; 58 | } 59 | 60 | self.args([ 61 | // Non-POSIX args are only available on certain operating systems. 62 | if cfg!(target_os = "linux") { 63 | r"--preview=\command -p ls -Cp --color=always --group-directories-first {2..}" 64 | } else { 65 | r"--preview=\command -p ls -Cp {2..}" 66 | }, 67 | // Rounded edges don't display correctly on some terminals. 68 | "--preview-window=down,30%,sharp", 69 | ]) 70 | .envs([ 71 | // Enables colorized `ls` output on macOS / FreeBSD. 72 | ("CLICOLOR", "1"), 73 | // Forces colorized `ls` output when the output is not a 74 | // TTY (like in fzf's preview window) on macOS / 75 | // FreeBSD. 76 | ("CLICOLOR_FORCE", "1"), 77 | // Ensures that the preview command is run in a 78 | // POSIX-compliant shell, regardless of what shell the 79 | // user has selected. 80 | ("SHELL", "sh"), 81 | ]) 82 | } 83 | 84 | pub fn args(&mut self, args: I) -> &mut Self 85 | where 86 | I: IntoIterator, 87 | S: AsRef, 88 | { 89 | self.0.args(args); 90 | self 91 | } 92 | 93 | pub fn env(&mut self, key: K, val: V) -> &mut Self 94 | where 95 | K: AsRef, 96 | V: AsRef, 97 | { 98 | self.0.env(key, val); 99 | self 100 | } 101 | 102 | pub fn envs(&mut self, vars: I) -> &mut Self 103 | where 104 | I: IntoIterator, 105 | K: AsRef, 106 | V: AsRef, 107 | { 108 | self.0.envs(vars); 109 | self 110 | } 111 | 112 | pub fn spawn(&mut self) -> Result { 113 | match self.0.spawn() { 114 | Ok(child) => Ok(FzfChild(child)), 115 | Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_FZF_NOT_FOUND), 116 | Err(e) => Err(e).context("could not launch fzf"), 117 | } 118 | } 119 | } 120 | 121 | pub struct FzfChild(Child); 122 | 123 | impl FzfChild { 124 | pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result> { 125 | let handle = self.0.stdin.as_mut().unwrap(); 126 | match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) { 127 | Ok(()) => Ok(None), 128 | Err(e) if e.kind() == io::ErrorKind::BrokenPipe => self.wait().map(Some), 129 | Err(e) => Err(e).context("could not write to fzf"), 130 | } 131 | } 132 | 133 | pub fn wait(&mut self) -> Result { 134 | // Drop stdin to prevent deadlock. 135 | mem::drop(self.0.stdin.take()); 136 | 137 | let mut stdout = self.0.stdout.take().unwrap(); 138 | let mut output = String::default(); 139 | stdout.read_to_string(&mut output).context("failed to read from fzf")?; 140 | 141 | let status = self.0.wait().context("wait failed on fzf")?; 142 | match status.code() { 143 | Some(0) => Ok(output), 144 | Some(1) => bail!("no match found"), 145 | Some(2) => bail!("fzf returned an error"), 146 | Some(130) => bail!(SilentExit { code: 130 }), 147 | Some(128..=254) | None => bail!("fzf was terminated"), 148 | _ => bail!("fzf returned an unknown error"), 149 | } 150 | } 151 | } 152 | 153 | /// Similar to [`fs::write`], but atomic (best effort on Windows). 154 | pub fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { 155 | let path = path.as_ref(); 156 | let contents = contents.as_ref(); 157 | let dir = path.parent().unwrap(); 158 | 159 | // Create a tmpfile. 160 | let (mut tmp_file, tmp_path) = tmpfile(dir)?; 161 | let result = (|| { 162 | // Write to the tmpfile. 163 | _ = tmp_file.set_len(contents.len() as u64); 164 | tmp_file 165 | .write_all(contents) 166 | .with_context(|| format!("could not write to file: {}", tmp_path.display()))?; 167 | 168 | // Set the owner of the tmpfile (UNIX only). 169 | #[cfg(unix)] 170 | if let Ok(metadata) = path.metadata() { 171 | use std::os::unix::fs::MetadataExt; 172 | 173 | use nix::unistd::{self, Gid, Uid}; 174 | 175 | let uid = Uid::from_raw(metadata.uid()); 176 | let gid = Gid::from_raw(metadata.gid()); 177 | _ = unistd::fchown(&tmp_file, Some(uid), Some(gid)); 178 | } 179 | 180 | // Close and rename the tmpfile. 181 | // In some cases, errors from the last write() are reported only on close(). 182 | // Rust ignores errors from close(), since it occurs inside `Drop`. To 183 | // catch these errors, we manually call `File::sync_all()` first. 184 | tmp_file 185 | .sync_all() 186 | .with_context(|| format!("could not sync writes to file: {}", tmp_path.display()))?; 187 | mem::drop(tmp_file); 188 | rename(&tmp_path, path) 189 | })(); 190 | // In case of an error, delete the tmpfile. 191 | if result.is_err() { 192 | _ = fs::remove_file(&tmp_path); 193 | } 194 | result 195 | } 196 | 197 | /// Atomically create a tmpfile in the given directory. 198 | fn tmpfile(dir: impl AsRef) -> Result<(File, PathBuf)> { 199 | const MAX_ATTEMPTS: usize = 5; 200 | const TMP_NAME_LEN: usize = 16; 201 | let dir = dir.as_ref(); 202 | 203 | let mut attempts = 0; 204 | loop { 205 | attempts += 1; 206 | 207 | // Generate a random name for the tmpfile. 208 | let mut name = String::with_capacity(TMP_NAME_LEN); 209 | name.push_str("tmp_"); 210 | while name.len() < TMP_NAME_LEN { 211 | name.push(fastrand::alphanumeric()); 212 | } 213 | let path = dir.join(name); 214 | 215 | // Atomically create the tmpfile. 216 | match OpenOptions::new().write(true).create_new(true).open(&path) { 217 | Ok(file) => break Ok((file, path)), 218 | Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => {} 219 | Err(e) => { 220 | break Err(e).with_context(|| format!("could not create file: {}", path.display())); 221 | } 222 | } 223 | } 224 | } 225 | 226 | /// Similar to [`fs::rename`], but with retries on Windows. 227 | fn rename(from: impl AsRef, to: impl AsRef) -> Result<()> { 228 | let from = from.as_ref(); 229 | let to = to.as_ref(); 230 | 231 | const MAX_ATTEMPTS: usize = if cfg!(windows) { 5 } else { 1 }; 232 | let mut attempts = 0; 233 | 234 | loop { 235 | match fs::rename(from, to) { 236 | Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => { 237 | attempts += 1 238 | } 239 | result => { 240 | break result.with_context(|| { 241 | format!("could not rename file: {} -> {}", from.display(), to.display()) 242 | }); 243 | } 244 | } 245 | } 246 | } 247 | 248 | pub fn canonicalize(path: impl AsRef) -> Result { 249 | dunce::canonicalize(&path) 250 | .with_context(|| format!("could not resolve path: {}", path.as_ref().display())) 251 | } 252 | 253 | pub fn current_dir() -> Result { 254 | env::current_dir().context("could not get current directory") 255 | } 256 | 257 | pub fn current_time() -> Result { 258 | let current_time = SystemTime::now() 259 | .duration_since(SystemTime::UNIX_EPOCH) 260 | .context("system clock set to invalid time")? 261 | .as_secs(); 262 | 263 | Ok(current_time) 264 | } 265 | 266 | pub fn path_to_str(path: &impl AsRef) -> Result<&str> { 267 | let path = path.as_ref(); 268 | path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display())) 269 | } 270 | 271 | /// Returns the absolute version of a path. Like 272 | /// [`std::path::Path::canonicalize`], but doesn't resolve symlinks. 273 | pub fn resolve_path(path: impl AsRef) -> Result { 274 | let path = path.as_ref(); 275 | let base_path; 276 | 277 | let mut components = path.components().peekable(); 278 | let mut stack = Vec::new(); 279 | 280 | // initialize root 281 | if cfg!(windows) { 282 | use std::path::Prefix; 283 | 284 | fn get_drive_letter(path: impl AsRef) -> Option { 285 | let path = path.as_ref(); 286 | let mut components = path.components(); 287 | 288 | match components.next() { 289 | Some(Component::Prefix(prefix)) => match prefix.kind() { 290 | Prefix::Disk(drive_letter) | Prefix::VerbatimDisk(drive_letter) => { 291 | Some(drive_letter) 292 | } 293 | _ => None, 294 | }, 295 | _ => None, 296 | } 297 | } 298 | 299 | fn get_drive_path(drive_letter: u8) -> PathBuf { 300 | format!(r"{}:\", drive_letter as char).into() 301 | } 302 | 303 | fn get_drive_relative(drive_letter: u8) -> Result { 304 | let path = current_dir()?; 305 | if Some(drive_letter) == get_drive_letter(&path) { 306 | return Ok(path); 307 | } 308 | 309 | if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) { 310 | return Ok(path.into()); 311 | } 312 | 313 | let path = get_drive_path(drive_letter); 314 | Ok(path) 315 | } 316 | 317 | match components.peek() { 318 | Some(Component::Prefix(prefix)) => match prefix.kind() { 319 | Prefix::Disk(drive_letter) => { 320 | let disk = components.next().unwrap(); 321 | if components.peek() == Some(&Component::RootDir) { 322 | let root = components.next().unwrap(); 323 | stack.push(disk); 324 | stack.push(root); 325 | } else { 326 | base_path = get_drive_relative(drive_letter)?; 327 | stack.extend(base_path.components()); 328 | } 329 | } 330 | Prefix::VerbatimDisk(drive_letter) => { 331 | components.next(); 332 | if components.peek() == Some(&Component::RootDir) { 333 | components.next(); 334 | } 335 | 336 | base_path = get_drive_path(drive_letter); 337 | stack.extend(base_path.components()); 338 | } 339 | _ => bail!("invalid path: {}", path.display()), 340 | }, 341 | Some(Component::RootDir) => { 342 | components.next(); 343 | 344 | let current_dir = env::current_dir()?; 345 | let drive_letter = get_drive_letter(¤t_dir).with_context(|| { 346 | format!("could not get drive letter: {}", current_dir.display()) 347 | })?; 348 | base_path = get_drive_path(drive_letter); 349 | stack.extend(base_path.components()); 350 | } 351 | _ => { 352 | base_path = current_dir()?; 353 | stack.extend(base_path.components()); 354 | } 355 | } 356 | } else if components.peek() == Some(&Component::RootDir) { 357 | let root = components.next().unwrap(); 358 | stack.push(root); 359 | } else { 360 | base_path = current_dir()?; 361 | stack.extend(base_path.components()); 362 | } 363 | 364 | for component in components { 365 | match component { 366 | Component::Normal(_) => stack.push(component), 367 | Component::CurDir => {} 368 | Component::ParentDir => { 369 | if stack.last() != Some(&Component::RootDir) { 370 | stack.pop(); 371 | } 372 | } 373 | Component::Prefix(_) | Component::RootDir => unreachable!(), 374 | } 375 | } 376 | 377 | Ok(stack.iter().collect()) 378 | } 379 | 380 | /// Convert a string to lowercase, with a fast path for ASCII strings. 381 | pub fn to_lowercase(s: impl AsRef) -> String { 382 | let s = s.as_ref(); 383 | if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() } 384 | } 385 | -------------------------------------------------------------------------------- /templates/bash.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | # shellcheck shell=bash 5 | 6 | {{ section }} 7 | # Utility functions for zoxide. 8 | # 9 | 10 | # pwd based on the value of _ZO_RESOLVE_SYMLINKS. 11 | function __zoxide_pwd() { 12 | {%- if cfg!(windows) %} 13 | \command cygpath -w "$(\builtin pwd -P)" 14 | {%- else if resolve_symlinks %} 15 | \builtin pwd -P 16 | {%- else %} 17 | \builtin pwd -L 18 | {%- endif %} 19 | } 20 | 21 | # cd + custom logic based on the value of _ZO_ECHO. 22 | function __zoxide_cd() { 23 | # shellcheck disable=SC2164 24 | \builtin cd -- "$@" {%- if echo %} && __zoxide_pwd {%- endif %} 25 | } 26 | 27 | {{ section }} 28 | # Hook configuration for zoxide. 29 | # 30 | {%- if hook != InitHook::None %} 31 | 32 | # Hook to add new entries to the database. 33 | {%- if hook == InitHook::Prompt %} 34 | function __zoxide_hook() { 35 | \builtin local -r retval="$?" 36 | # shellcheck disable=SC2312 37 | \command zoxide add -- "$(__zoxide_pwd)" 38 | return "${retval}" 39 | } 40 | 41 | {%- else if hook == InitHook::Pwd %} 42 | __zoxide_oldpwd="$(__zoxide_pwd)" 43 | 44 | function __zoxide_hook() { 45 | \builtin local -r retval="$?" 46 | \builtin local pwd_tmp 47 | pwd_tmp="$(__zoxide_pwd)" 48 | if [[ ${__zoxide_oldpwd} != "${pwd_tmp}" ]]; then 49 | __zoxide_oldpwd="${pwd_tmp}" 50 | \command zoxide add -- "${__zoxide_oldpwd}" 51 | fi 52 | return "${retval}" 53 | } 54 | {%- endif %} 55 | 56 | # Initialize hook. 57 | if [[ ${PROMPT_COMMAND:=} != *'__zoxide_hook'* ]]; then 58 | PROMPT_COMMAND="__zoxide_hook;${PROMPT_COMMAND#;}" 59 | fi 60 | 61 | {%- endif %} 62 | 63 | # Report common issues. 64 | function __zoxide_doctor() { 65 | {%- if hook == InitHook::None %} 66 | return 0 67 | 68 | {%- else %} 69 | [[ ${_ZO_DOCTOR:-1} -eq 0 ]] && return 0 70 | # shellcheck disable=SC2199 71 | [[ ${PROMPT_COMMAND[@]:-} == *'__zoxide_hook'* ]] && return 0 72 | # shellcheck disable=SC2199 73 | [[ ${__vsc_original_prompt_command[@]:-} == *'__zoxide_hook'* ]] && return 0 74 | 75 | _ZO_DOCTOR=0 76 | \builtin printf '%s\n' \ 77 | 'zoxide: detected a possible configuration issue.' \ 78 | 'Please ensure that zoxide is initialized right at the end of your shell configuration file (usually ~/.bashrc).' \ 79 | '' \ 80 | 'If the issue persists, consider filing an issue at:' \ 81 | 'https://github.com/ajeetdsouza/zoxide/issues' \ 82 | '' \ 83 | 'Disable this message by setting _ZO_DOCTOR=0.' \ 84 | '' >&2 85 | {%- endif %} 86 | } 87 | 88 | {{ section }} 89 | # When using zoxide with --no-cmd, alias these internal functions as desired. 90 | # 91 | 92 | __zoxide_z_prefix='z#' 93 | 94 | # Jump to a directory using only keywords. 95 | function __zoxide_z() { 96 | __zoxide_doctor 97 | 98 | # shellcheck disable=SC2199 99 | if [[ $# -eq 0 ]]; then 100 | __zoxide_cd ~ 101 | elif [[ $# -eq 1 && $1 == '-' ]]; then 102 | __zoxide_cd "${OLDPWD}" 103 | elif [[ $# -eq 1 && -d $1 ]]; then 104 | __zoxide_cd "$1" 105 | elif [[ $# -eq 2 && $1 == '--' ]]; then 106 | __zoxide_cd "$2" 107 | elif [[ ${@: -1} == "${__zoxide_z_prefix}"?* ]]; then 108 | # shellcheck disable=SC2124 109 | \builtin local result="${@: -1}" 110 | __zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}" 111 | else 112 | \builtin local result 113 | # shellcheck disable=SC2312 114 | result="$(\command zoxide query --exclude "$(__zoxide_pwd)" -- "$@")" && 115 | __zoxide_cd "${result}" 116 | fi 117 | } 118 | 119 | # Jump to a directory using interactive search. 120 | function __zoxide_zi() { 121 | __zoxide_doctor 122 | \builtin local result 123 | result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${result}" 124 | } 125 | 126 | {{ section }} 127 | # Commands for zoxide. Disable these using --no-cmd. 128 | # 129 | 130 | {%- match cmd %} 131 | {%- when Some with (cmd) %} 132 | 133 | \builtin unalias {{cmd}} &>/dev/null || \builtin true 134 | function {{cmd}}() { 135 | __zoxide_z "$@" 136 | } 137 | 138 | \builtin unalias {{cmd}}i &>/dev/null || \builtin true 139 | function {{cmd}}i() { 140 | __zoxide_zi "$@" 141 | } 142 | 143 | # Load completions. 144 | # - Bash 4.4+ is required to use `@Q`. 145 | # - Completions require line editing. Since Bash supports only two modes of 146 | # line editing (`vim` and `emacs`), we check if either them is enabled. 147 | # - Completions don't work on `dumb` terminals. 148 | if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VERSINFO[0]:-0} -ge 5 ]] && 149 | [[ :"${SHELLOPTS}": =~ :(vi|emacs): && ${TERM} != 'dumb' ]]; then 150 | 151 | function __zoxide_z_complete_helper() { 152 | READLINE_LINE="{{ cmd }} ${__zoxide_result@Q}" 153 | READLINE_POINT={{ "${#READLINE_LINE}" }} 154 | bind '"\e[0n": accept-line' 155 | \builtin printf '\e[5n' >/dev/tty 156 | } 157 | 158 | function __zoxide_z_complete() { 159 | # Only show completions when the cursor is at the end of the line. 160 | [[ {{ "${#COMP_WORDS[@]}" }} -eq $((COMP_CWORD + 1)) ]] || return 161 | 162 | # If there is only one argument, use `cd` completions. 163 | if [[ {{ "${#COMP_WORDS[@]}" }} -eq 2 ]]; then 164 | \builtin mapfile -t COMPREPLY < <( 165 | \builtin compgen -A directory -- "${COMP_WORDS[-1]}" || \builtin true 166 | ) 167 | # If there is a space after the last word, use interactive selection. 168 | elif [[ -z ${COMP_WORDS[-1]} ]]; then 169 | # shellcheck disable=SC2312 170 | __zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" && { 171 | # In case the terminal does not respond to \e[5n or another 172 | # mechanism steals the response, it is still worth completing 173 | # the directory in the command line. 174 | COMPREPLY=("${__zoxide_z_prefix}${__zoxide_result}/") 175 | 176 | # Note: We here call "bind" without prefixing "\builtin" to be 177 | # compatible with frameworks like ble.sh, which emulates Bash's 178 | # builtin "bind". 179 | bind -x '"\e[0n": __zoxide_z_complete_helper' 180 | \builtin printf '\e[5n' >/dev/tty 181 | } 182 | fi 183 | } 184 | 185 | \builtin complete -F __zoxide_z_complete -o filenames -- {{cmd}} 186 | \builtin complete -r {{cmd}}i &>/dev/null || \builtin true 187 | fi 188 | 189 | {%- when None %} 190 | 191 | {{ not_configured }} 192 | 193 | {%- endmatch %} 194 | 195 | {{ section }} 196 | # To initialize zoxide, add this to your shell configuration file (usually ~/.bashrc): 197 | # 198 | # eval "$(zoxide init bash)" 199 | -------------------------------------------------------------------------------- /templates/elvish.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | use builtin 5 | use path 6 | 7 | {{ section }} 8 | # Utility functions for zoxide. 9 | # 10 | 11 | # cd + custom logic based on the value of _ZO_ECHO. 12 | fn __zoxide_cd {|path| 13 | builtin:cd $path 14 | {%- if echo %} 15 | builtin:echo $pwd 16 | {%- endif %} 17 | } 18 | 19 | {{ section }} 20 | # Hook configuration for zoxide. 21 | # 22 | 23 | # Initialize hook to track previous directory. 24 | var oldpwd = $builtin:pwd 25 | set builtin:before-chdir = [$@builtin:before-chdir {|_| set oldpwd = $builtin:pwd }] 26 | 27 | # Initialize hook to add directories to zoxide. 28 | {%- if hook == InitHook::None %} 29 | {{ not_configured }} 30 | 31 | {%- else %} 32 | if (builtin:not (builtin:eq $E:__zoxide_shlvl $E:SHLVL)) { 33 | set E:__zoxide_shlvl = $E:SHLVL 34 | {%- if hook == InitHook::Prompt %} 35 | set edit:before-readline = [$@edit:before-readline {|| zoxide add -- $pwd }] 36 | {%- else if hook == InitHook::Pwd %} 37 | set builtin:after-chdir = [$@builtin:after-chdir {|_| zoxide add -- $pwd }] 38 | {%- endif %} 39 | } 40 | 41 | {%- endif %} 42 | 43 | {{ section }} 44 | # When using zoxide with --no-cmd, alias these internal functions as desired. 45 | # 46 | 47 | # Jump to a directory using only keywords. 48 | fn __zoxide_z {|@rest| 49 | if (builtin:eq [] $rest) { 50 | __zoxide_cd ~ 51 | } elif (builtin:eq [-] $rest) { 52 | __zoxide_cd $oldpwd 53 | } elif (and ('builtin:==' (builtin:count $rest) 1) (path:is-dir &follow-symlink=$true $rest[0])) { 54 | __zoxide_cd $rest[0] 55 | } else { 56 | var path 57 | try { 58 | set path = (zoxide query --exclude $pwd -- $@rest) 59 | } catch { 60 | } else { 61 | __zoxide_cd $path 62 | } 63 | } 64 | } 65 | edit:add-var __zoxide_z~ $__zoxide_z~ 66 | 67 | # Jump to a directory using interactive search. 68 | fn __zoxide_zi {|@rest| 69 | var path 70 | try { 71 | set path = (zoxide query --interactive -- $@rest) 72 | } catch { 73 | } else { 74 | __zoxide_cd $path 75 | } 76 | } 77 | edit:add-var __zoxide_zi~ $__zoxide_zi~ 78 | 79 | {{ section }} 80 | # Commands for zoxide. Disable these using --no-cmd. 81 | # 82 | 83 | {%- match cmd %} 84 | {%- when Some with (cmd) %} 85 | 86 | edit:add-var {{cmd}}~ $__zoxide_z~ 87 | edit:add-var {{cmd}}i~ $__zoxide_zi~ 88 | 89 | # Load completions. 90 | {#- 91 | zoxide-based completions are currently not possible, because Elvish only prints 92 | a completion if the current token is a prefix of it. 93 | #} 94 | fn __zoxide_z_complete {|@rest| 95 | if (!= (builtin:count $rest) 2) { 96 | builtin:return 97 | } 98 | edit:complete-filename $rest[1] | 99 | builtin:each {|completion| 100 | var dir = $completion[stem] 101 | if (path:is-dir $dir) { 102 | builtin:put $dir 103 | } 104 | } 105 | } 106 | set edit:completion:arg-completer[{{cmd}}] = $__zoxide_z_complete~ 107 | 108 | {%- when None %} 109 | 110 | {{ not_configured }} 111 | 112 | {%- endmatch %} 113 | 114 | {{ section }} 115 | # To initialize zoxide, add this to your configuration (usually 116 | # ~/.elvish/rc.elv): 117 | # 118 | # eval (zoxide init elvish | slurp) 119 | # 120 | # Note: zoxide only supports elvish v0.18.0 and above. 121 | -------------------------------------------------------------------------------- /templates/fish.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | {{ section }} 5 | # Utility functions for zoxide. 6 | # 7 | 8 | # pwd based on the value of _ZO_RESOLVE_SYMLINKS. 9 | function __zoxide_pwd 10 | {%- if cfg!(windows) %} 11 | command cygpath -w (builtin pwd -P) 12 | {%- else if resolve_symlinks %} 13 | builtin pwd -P 14 | {%- else %} 15 | builtin pwd -L 16 | {%- endif %} 17 | end 18 | 19 | # A copy of fish's internal cd function. This makes it possible to use 20 | # `alias cd=z` without causing an infinite loop. 21 | if ! builtin functions --query __zoxide_cd_internal 22 | string replace --regex -- '^function cd\s' 'function __zoxide_cd_internal ' <$__fish_data_dir/functions/cd.fish | source 23 | end 24 | 25 | # cd + custom logic based on the value of _ZO_ECHO. 26 | function __zoxide_cd 27 | if set -q __zoxide_loop 28 | builtin echo "zoxide: infinite loop detected" 29 | builtin echo "Avoid aliasing `cd` to `z` directly, use `zoxide init --cmd=cd fish` instead" 30 | return 1 31 | end 32 | 33 | {%- if cfg!(windows) %} 34 | __zoxide_loop=1 __zoxide_cd_internal (cygpath -u $argv) 35 | {%- else %} 36 | __zoxide_loop=1 __zoxide_cd_internal $argv 37 | {%- endif %} 38 | {%- if echo %} 39 | and __zoxide_pwd 40 | {%- endif %} 41 | end 42 | 43 | {{ section }} 44 | # Hook configuration for zoxide. 45 | # 46 | 47 | {% if hook == InitHook::None -%} 48 | {{ not_configured }} 49 | 50 | {%- else -%} 51 | # Initialize hook to add new entries to the database. 52 | {%- if hook == InitHook::Prompt %} 53 | function __zoxide_hook --on-event fish_prompt 54 | {%- else if hook == InitHook::Pwd %} 55 | function __zoxide_hook --on-variable PWD 56 | {%- endif %} 57 | test -z "$fish_private_mode" 58 | and command zoxide add -- (__zoxide_pwd) 59 | end 60 | 61 | {%- endif %} 62 | 63 | {{ section }} 64 | # When using zoxide with --no-cmd, alias these internal functions as desired. 65 | # 66 | 67 | # Jump to a directory using only keywords. 68 | function __zoxide_z 69 | set -l argc (builtin count $argv) 70 | if test $argc -eq 0 71 | __zoxide_cd $HOME 72 | else if test "$argv" = - 73 | __zoxide_cd - 74 | else if test $argc -eq 1 -a -d $argv[1] 75 | __zoxide_cd $argv[1] 76 | else if test $argc -eq 2 -a $argv[1] = -- 77 | __zoxide_cd -- $argv[2] 78 | else 79 | set -l result (command zoxide query --exclude (__zoxide_pwd) -- $argv) 80 | and __zoxide_cd $result 81 | end 82 | end 83 | 84 | # Completions. 85 | function __zoxide_z_complete 86 | set -l tokens (builtin commandline --current-process --tokenize) 87 | set -l curr_tokens (builtin commandline --cut-at-cursor --current-process --tokenize) 88 | 89 | if test (builtin count $tokens) -le 2 -a (builtin count $curr_tokens) -eq 1 90 | # If there are < 2 arguments, use `cd` completions. 91 | complete --do-complete "'' "(builtin commandline --cut-at-cursor --current-token) | string match --regex -- '.*/$' 92 | else if test (builtin count $tokens) -eq (builtin count $curr_tokens) 93 | # If the last argument is empty, use interactive selection. 94 | set -l query $tokens[2..-1] 95 | set -l result (command zoxide query --exclude (__zoxide_pwd) --interactive -- $query) 96 | and __zoxide_cd $result 97 | and builtin commandline --function cancel-commandline repaint 98 | end 99 | end 100 | complete --command __zoxide_z --no-files --arguments '(__zoxide_z_complete)' 101 | 102 | # Jump to a directory using interactive search. 103 | function __zoxide_zi 104 | set -l result (command zoxide query --interactive -- $argv) 105 | and __zoxide_cd $result 106 | end 107 | 108 | {{ section }} 109 | # Commands for zoxide. Disable these using --no-cmd. 110 | # 111 | 112 | {%- match cmd %} 113 | {%- when Some with (cmd) %} 114 | 115 | abbr --erase {{cmd}} &>/dev/null 116 | alias {{cmd}}=__zoxide_z 117 | 118 | abbr --erase {{cmd}}i &>/dev/null 119 | alias {{cmd}}i=__zoxide_zi 120 | 121 | {%- when None %} 122 | 123 | {{ not_configured }} 124 | 125 | {%- endmatch %} 126 | 127 | {{ section }} 128 | # To initialize zoxide, add this to your configuration (usually 129 | # ~/.config/fish/config.fish): 130 | # 131 | # zoxide init fish | source 132 | -------------------------------------------------------------------------------- /templates/nushell.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | # Code generated by zoxide. DO NOT EDIT. 5 | 6 | {{ section }} 7 | # Hook configuration for zoxide. 8 | # 9 | 10 | {% if hook == InitHook::None -%} 11 | {{ not_configured }} 12 | 13 | {%- else -%} 14 | # Initialize hook to add new entries to the database. 15 | export-env { 16 | {%- if hook == InitHook::Prompt %} 17 | $env.config = ( 18 | $env.config? 19 | | default {} 20 | | upsert hooks { default {} } 21 | | upsert hooks.pre_prompt { default [] } 22 | ) 23 | let __zoxide_hooked = ( 24 | $env.config.hooks.pre_prompt | any { try { get __zoxide_hook } catch { false } } 25 | ) 26 | if not $__zoxide_hooked { 27 | $env.config.hooks.pre_prompt = ($env.config.hooks.pre_prompt | append { 28 | __zoxide_hook: true, 29 | code: {|| ^zoxide add -- $env.PWD} 30 | }) 31 | } 32 | {%- else if hook == InitHook::Pwd %} 33 | $env.config = ( 34 | $env.config? 35 | | default {} 36 | | upsert hooks { default {} } 37 | | upsert hooks.env_change { default {} } 38 | | upsert hooks.env_change.PWD { default [] } 39 | ) 40 | let __zoxide_hooked = ( 41 | $env.config.hooks.env_change.PWD | any { try { get __zoxide_hook } catch { false } } 42 | ) 43 | if not $__zoxide_hooked { 44 | $env.config.hooks.env_change.PWD = ($env.config.hooks.env_change.PWD | append { 45 | __zoxide_hook: true, 46 | code: {|_, dir| ^zoxide add -- $dir} 47 | }) 48 | } 49 | {%- endif %} 50 | } 51 | 52 | {%- endif %} 53 | 54 | {{ section }} 55 | # When using zoxide with --no-cmd, alias these internal functions as desired. 56 | # 57 | 58 | # Jump to a directory using only keywords. 59 | def --env --wrapped __zoxide_z [...rest: string] { 60 | let path = match $rest { 61 | [] => {'~'}, 62 | [ '-' ] => {'-'}, 63 | [ $arg ] if ($arg | path expand | path type) == 'dir' => {$arg} 64 | _ => { 65 | ^zoxide query --exclude $env.PWD -- ...$rest | str trim -r -c "\n" 66 | } 67 | } 68 | cd $path 69 | {%- if echo %} 70 | echo $env.PWD 71 | {%- endif %} 72 | } 73 | 74 | # Jump to a directory using interactive search. 75 | def --env --wrapped __zoxide_zi [...rest:string] { 76 | cd $'(^zoxide query --interactive -- ...$rest | str trim -r -c "\n")' 77 | {%- if echo %} 78 | echo $env.PWD 79 | {%- endif %} 80 | } 81 | 82 | {{ section }} 83 | # Commands for zoxide. Disable these using --no-cmd. 84 | # 85 | 86 | {%- match cmd %} 87 | {%- when Some with (cmd) %} 88 | 89 | alias {{cmd}} = __zoxide_z 90 | alias {{cmd}}i = __zoxide_zi 91 | 92 | {%- when None %} 93 | 94 | {{ not_configured }} 95 | 96 | {%- endmatch %} 97 | 98 | {{ section }} 99 | # Add this to your env file (find it by running `$nu.env-path` in Nushell): 100 | # 101 | # zoxide init nushell | save -f ~/.zoxide.nu 102 | # 103 | # Now, add this to the end of your config file (find it by running 104 | # `$nu.config-path` in Nushell): 105 | # 106 | # source ~/.zoxide.nu 107 | # 108 | # Note: zoxide only supports Nushell v0.89.0+. 109 | -------------------------------------------------------------------------------- /templates/posix.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | # shellcheck shell=sh 5 | 6 | {{ section }} 7 | # Utility functions for zoxide. 8 | # 9 | 10 | # pwd based on the value of _ZO_RESOLVE_SYMLINKS. 11 | __zoxide_pwd() { 12 | {%- if cfg!(windows) %} 13 | \command cygpath -w "$(\builtin pwd -P)" 14 | {%- else if resolve_symlinks %} 15 | \command pwd -P 16 | {%- else %} 17 | \command pwd -L 18 | {%- endif %} 19 | } 20 | 21 | # cd + custom logic based on the value of _ZO_ECHO. 22 | __zoxide_cd() { 23 | # shellcheck disable=SC2164 24 | \command cd "$@" {%- if echo %} && __zoxide_pwd {%- endif %} 25 | } 26 | 27 | {{ section }} 28 | # Hook configuration for zoxide. 29 | # 30 | 31 | {% match hook %} 32 | {%- when InitHook::None -%} 33 | {{ not_configured }} 34 | 35 | {%- when InitHook::Prompt -%} 36 | # Hook to add new entries to the database. 37 | __zoxide_hook() { 38 | \command zoxide add -- "$(__zoxide_pwd || \builtin true)" 39 | } 40 | 41 | # Initialize hook. 42 | if [ "${PS1:=}" = "${PS1#*\$(__zoxide_hook)}" ]; then 43 | PS1="${PS1}\$(__zoxide_hook)" 44 | fi 45 | 46 | # Report common issues. 47 | __zoxide_doctor() { 48 | {%- if hook != InitHook::Prompt %} 49 | return 0 50 | {%- else %} 51 | [ "${_ZO_DOCTOR:-1}" -eq 0 ] && return 0 52 | case "${PS1:-}" in 53 | *__zoxide_hook*) return 0 ;; 54 | *) ;; 55 | esac 56 | 57 | _ZO_DOCTOR=0 58 | \command printf '%s\n' \ 59 | 'zoxide: detected a possible configuration issue.' \ 60 | 'Please ensure that zoxide is initialized right at the end of your shell configuration file.' \ 61 | '' \ 62 | 'If the issue persists, consider filing an issue at:' \ 63 | 'https://github.com/ajeetdsouza/zoxide/issues' \ 64 | '' \ 65 | 'Disable this message by setting _ZO_DOCTOR=0.' \ 66 | '' >&2 67 | {%- endif %} 68 | } 69 | 70 | {%- when InitHook::Pwd -%} 71 | \command printf "%s\n%s\n" \ 72 | "zoxide: PWD hooks are not supported on POSIX shells." \ 73 | " Use 'zoxide init posix --hook prompt' instead." 74 | 75 | {%- endmatch %} 76 | 77 | {{ section }} 78 | # When using zoxide with --no-cmd, alias these internal functions as desired. 79 | # 80 | 81 | # Jump to a directory using only keywords. 82 | __zoxide_z() { 83 | __zoxide_doctor 84 | 85 | if [ "$#" -eq 0 ]; then 86 | __zoxide_cd ~ 87 | elif [ "$#" -eq 1 ] && [ "$1" = '-' ]; then 88 | if [ -n "${OLDPWD}" ]; then 89 | __zoxide_cd "${OLDPWD}" 90 | else 91 | # shellcheck disable=SC2016 92 | \command printf 'zoxide: $OLDPWD is not set' 93 | return 1 94 | fi 95 | elif [ "$#" -eq 1 ] && [ -d "$1" ]; then 96 | __zoxide_cd "$1" 97 | else 98 | __zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd || \builtin true)" -- "$@")" && 99 | __zoxide_cd "${__zoxide_result}" 100 | fi 101 | } 102 | 103 | # Jump to a directory using interactive search. 104 | __zoxide_zi() { 105 | __zoxide_doctor 106 | __zoxide_result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${__zoxide_result}" 107 | } 108 | 109 | {{ section }} 110 | # Commands for zoxide. Disable these using --no-cmd. 111 | # 112 | 113 | {%- match cmd %} 114 | {%- when Some with (cmd) %} 115 | 116 | \command unalias {{cmd}} >/dev/null 2>&1 || \true 117 | {{cmd}}() { 118 | __zoxide_z "$@" 119 | } 120 | 121 | \command unalias {{cmd}}i >/dev/null 2>&1 || \true 122 | {{cmd}}i() { 123 | __zoxide_zi "$@" 124 | } 125 | 126 | {%- when None %} 127 | 128 | {{ not_configured }} 129 | 130 | {%- endmatch %} 131 | 132 | {{ section }} 133 | # To initialize zoxide, add this to your configuration: 134 | # 135 | # eval "$(zoxide init posix --hook prompt)" 136 | -------------------------------------------------------------------------------- /templates/powershell.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | {{ section }} 5 | # Utility functions for zoxide. 6 | # 7 | 8 | # Call zoxide binary, returning the output as UTF-8. 9 | function global:__zoxide_bin { 10 | $encoding = [Console]::OutputEncoding 11 | try { 12 | [Console]::OutputEncoding = [System.Text.Utf8Encoding]::new() 13 | $result = zoxide @args 14 | return $result 15 | } finally { 16 | [Console]::OutputEncoding = $encoding 17 | } 18 | } 19 | 20 | # pwd based on zoxide's format. 21 | function global:__zoxide_pwd { 22 | $cwd = Get-Location 23 | if ($cwd.Provider.Name -eq "FileSystem") { 24 | $cwd.ProviderPath 25 | } 26 | } 27 | 28 | # cd + custom logic based on the value of _ZO_ECHO. 29 | function global:__zoxide_cd($dir, $literal) { 30 | $dir = if ($literal) { 31 | Set-Location -LiteralPath $dir -Passthru -ErrorAction Stop 32 | } else { 33 | if ($dir -eq '-' -and ($PSVersionTable.PSVersion -lt 6.1)) { 34 | Write-Error "cd - is not supported below PowerShell 6.1. Please upgrade your version of PowerShell." 35 | } 36 | elseif ($dir -eq '+' -and ($PSVersionTable.PSVersion -lt 6.2)) { 37 | Write-Error "cd + is not supported below PowerShell 6.2. Please upgrade your version of PowerShell." 38 | } 39 | else { 40 | Set-Location -Path $dir -Passthru -ErrorAction Stop 41 | } 42 | } 43 | {%- if echo %} 44 | Write-Output $dir.Path 45 | {%- endif %} 46 | } 47 | 48 | {{ section }} 49 | # Hook configuration for zoxide. 50 | # 51 | 52 | {% if hook == InitHook::None -%} 53 | {{ not_configured }} 54 | 55 | {%- else -%} 56 | {#- 57 | Initialize $__zoxide_hooked if it does not exist. Removing this will cause an 58 | unset variable error in StrictMode. 59 | -#} 60 | {%- if hook == InitHook::Prompt -%} 61 | # Hook to add new entries to the database. 62 | function global:__zoxide_hook { 63 | $result = __zoxide_pwd 64 | if ($null -ne $result) { 65 | zoxide add "--" $result 66 | } 67 | } 68 | {%- else if hook == InitHook::Pwd -%} 69 | # Hook to add new entries to the database. 70 | $global:__zoxide_oldpwd = __zoxide_pwd 71 | function global:__zoxide_hook { 72 | $result = __zoxide_pwd 73 | if ($result -ne $global:__zoxide_oldpwd) { 74 | if ($null -ne $result) { 75 | zoxide add "--" $result 76 | } 77 | $global:__zoxide_oldpwd = $result 78 | } 79 | } 80 | {%- endif %} 81 | 82 | # Initialize hook. 83 | $global:__zoxide_hooked = (Get-Variable __zoxide_hooked -ErrorAction Ignore -ValueOnly) 84 | if ($global:__zoxide_hooked -ne 1) { 85 | $global:__zoxide_hooked = 1 86 | $global:__zoxide_prompt_old = $function:prompt 87 | 88 | function global:prompt { 89 | if ($null -ne $__zoxide_prompt_old) { 90 | & $__zoxide_prompt_old 91 | } 92 | $null = __zoxide_hook 93 | } 94 | } 95 | {%- endif %} 96 | 97 | {{ section }} 98 | # When using zoxide with --no-cmd, alias these internal functions as desired. 99 | # 100 | 101 | # Jump to a directory using only keywords. 102 | function global:__zoxide_z { 103 | if ($args.Length -eq 0) { 104 | __zoxide_cd ~ $true 105 | } 106 | elseif ($args.Length -eq 1 -and ($args[0] -eq '-' -or $args[0] -eq '+')) { 107 | __zoxide_cd $args[0] $false 108 | } 109 | elseif ($args.Length -eq 1 -and (Test-Path -PathType Container -LiteralPath $args[0])) { 110 | __zoxide_cd $args[0] $true 111 | } 112 | elseif ($args.Length -eq 1 -and (Test-Path -PathType Container -Path $args[0] )) { 113 | __zoxide_cd $args[0] $false 114 | } 115 | else { 116 | $result = __zoxide_pwd 117 | if ($null -ne $result) { 118 | $result = __zoxide_bin query --exclude $result "--" @args 119 | } 120 | else { 121 | $result = __zoxide_bin query "--" @args 122 | } 123 | if ($LASTEXITCODE -eq 0) { 124 | __zoxide_cd $result $true 125 | } 126 | } 127 | } 128 | 129 | # Jump to a directory using interactive search. 130 | function global:__zoxide_zi { 131 | $result = __zoxide_bin query -i "--" @args 132 | if ($LASTEXITCODE -eq 0) { 133 | __zoxide_cd $result $true 134 | } 135 | } 136 | 137 | {{ section }} 138 | # Commands for zoxide. Disable these using --no-cmd. 139 | # 140 | 141 | {%- match cmd %} 142 | {%- when Some with (cmd) %} 143 | 144 | Set-Alias -Name {{cmd}} -Value __zoxide_z -Option AllScope -Scope Global -Force 145 | Set-Alias -Name {{cmd}}i -Value __zoxide_zi -Option AllScope -Scope Global -Force 146 | 147 | {%- when None %} 148 | 149 | {{ not_configured }} 150 | 151 | {%- endmatch %} 152 | 153 | {{ section }} 154 | # To initialize zoxide, add this to your configuration (find it by running 155 | # `echo $profile` in PowerShell): 156 | # 157 | # Invoke-Expression (& { (zoxide init powershell | Out-String) }) 158 | -------------------------------------------------------------------------------- /templates/tcsh.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | {%- let pwd_cmd -%} 5 | {%- if resolve_symlinks -%} 6 | {%- let pwd_cmd = "pwd -P" -%} 7 | {%- else -%} 8 | {%- let pwd_cmd = "pwd -L" -%} 9 | {%- endif -%} 10 | 11 | {{ section }} 12 | # Hook configuration for zoxide. 13 | # 14 | {%- if hook != InitHook::None %} 15 | 16 | # Hook to add new entries to the database. 17 | {%- if hook == InitHook::Prompt %} 18 | alias __zoxide_hook 'zoxide add -- "`{{ pwd_cmd }}`"' 19 | 20 | {%- else if hook == InitHook::Pwd %} 21 | set __zoxide_pwd_old = `{{ pwd_cmd }}` 22 | alias __zoxide_hook 'set __zoxide_pwd_tmp = "`{{ pwd_cmd }}`"; test "$__zoxide_pwd_tmp" != "$__zoxide_pwd_old" && zoxide add -- "$__zoxide_pwd_tmp"; set __zoxide_pwd_old = "$__zoxide_pwd_tmp"' 23 | {%- endif %} 24 | 25 | # Initialize hook. 26 | alias precmd ';__zoxide_hook' 27 | 28 | {%- endif %} 29 | 30 | {{ section }} 31 | # When using zoxide with --no-cmd, alias these internal functions as desired. 32 | # 33 | 34 | # Jump to a directory using only keywords. 35 | alias __zoxide_z 'set __zoxide_args = (\!*)\ 36 | if ("$#__zoxide_args" == 0) then\ 37 | cd ~\ 38 | else\ 39 | if ("$#__zoxide_args" == 1 && "$__zoxide_args[1]" == "-") then\ 40 | cd -\ 41 | else if ("$#__zoxide_args" == 1 && -d "$__zoxide_args[1]") then\ 42 | cd "$__zoxide_args[1]"\ 43 | else\ 44 | set __zoxide_pwd = `{{ pwd_cmd }}`\ 45 | set __zoxide_result = "`zoxide query --exclude '"'"'$__zoxide_pwd'"'"' -- $__zoxide_args`" && cd "$__zoxide_result"\ 46 | endif\ 47 | endif' 48 | 49 | # Jump to a directory using interactive search. 50 | alias __zoxide_zi 'set __zoxide_args = (\!*)\ 51 | set __zoxide_pwd = `{{ pwd_cmd }}`\ 52 | set __zoxide_result = "`zoxide query --exclude '"'"'$__zoxide_pwd'"'"' --interactive -- $__zoxide_args`" && cd "$__zoxide_result"' 53 | 54 | {{ section }} 55 | # Commands for zoxide. Disable these using --no-cmd. 56 | # 57 | 58 | {%- match cmd %} 59 | {%- when Some with (cmd) %} 60 | 61 | alias {{cmd}} __zoxide_z 62 | alias {{cmd}}i __zoxide_zi 63 | 64 | {%- when None %} 65 | 66 | {{ not_configured }} 67 | 68 | {%- endmatch %} 69 | 70 | {{ section }} 71 | # To initialize zoxide, add this to your shell configuration file (usually ~/.tcshrc): 72 | # 73 | # zoxide init tcsh > ~/.zoxide.tcsh 74 | # source ~/.zoxide.tcsh 75 | -------------------------------------------------------------------------------- /templates/xonsh.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | # pylint: disable=missing-module-docstring 5 | 6 | import builtins # pylint: disable=unused-import 7 | import os 8 | import os.path 9 | import subprocess 10 | import sys 11 | import typing 12 | 13 | import xonsh.dirstack # type: ignore # pylint: disable=import-error 14 | import xonsh.environ # type: ignore # pylint: disable=import-error 15 | 16 | {{ section }} 17 | # Utility functions for zoxide. 18 | # 19 | 20 | 21 | def __zoxide_bin() -> str: 22 | """Finds and returns the location of the zoxide binary.""" 23 | zoxide = typing.cast(str, xonsh.environ.locate_binary("zoxide")) 24 | if zoxide is None: 25 | zoxide = "zoxide" 26 | return zoxide 27 | 28 | 29 | def __zoxide_env() -> dict[str, str]: 30 | """Returns the current environment.""" 31 | return builtins.__xonsh__.env.detype() # type: ignore # pylint:disable=no-member 32 | 33 | 34 | def __zoxide_pwd() -> str: 35 | """pwd based on the value of _ZO_RESOLVE_SYMLINKS.""" 36 | {%- if resolve_symlinks %} 37 | pwd = os.getcwd() 38 | {%- else %} 39 | pwd = __zoxide_env().get("PWD") 40 | if pwd is None: 41 | raise RuntimeError("$PWD not found") 42 | {%- endif %} 43 | return pwd 44 | 45 | 46 | def __zoxide_cd(path: str | bytes | None = None) -> None: 47 | """cd + custom logic based on the value of _ZO_ECHO.""" 48 | if path is None: 49 | args = [] 50 | elif isinstance(path, bytes): 51 | args = [path.decode("utf-8")] 52 | else: 53 | args = [path] 54 | _, exc, _ = xonsh.dirstack.cd(args) 55 | if exc is not None: 56 | raise RuntimeError(exc) 57 | {%- if echo %} 58 | print(__zoxide_pwd()) 59 | {%- endif %} 60 | 61 | 62 | class ZoxideSilentException(Exception): 63 | """Exit without complaining.""" 64 | 65 | 66 | def __zoxide_errhandler( 67 | func: typing.Callable[[list[str]], None], 68 | ) -> typing.Callable[[list[str]], int]: 69 | """Print exception and exit with error code 1.""" 70 | 71 | def wrapper(args: list[str]) -> int: 72 | try: 73 | func(args) 74 | return 0 75 | except ZoxideSilentException: 76 | return 1 77 | except Exception as exc: # pylint: disable=broad-except 78 | print(f"zoxide: {exc}", file=sys.stderr) 79 | return 1 80 | 81 | return wrapper 82 | 83 | 84 | {{ section }} 85 | # Hook configuration for zoxide. 86 | # 87 | 88 | {% if hook == InitHook::None -%} 89 | {{ not_configured }} 90 | 91 | {%- else -%} 92 | # Initialize hook to add new entries to the database. 93 | if "__zoxide_hook" not in globals(): 94 | {% if hook == InitHook::Prompt %} 95 | @builtins.events.on_post_prompt # type: ignore # pylint:disable=no-member 96 | {%- else if hook == InitHook::Pwd %} 97 | @builtins.events.on_chdir # type: ignore # pylint:disable=no-member 98 | {%- endif %} 99 | def __zoxide_hook(**_kwargs: typing.Any) -> None: 100 | """Hook to add new entries to the database.""" 101 | pwd = __zoxide_pwd() 102 | zoxide = __zoxide_bin() 103 | subprocess.run( 104 | [zoxide, "add", "--", pwd], 105 | check=False, 106 | env=__zoxide_env(), 107 | ) 108 | {% endif %} 109 | 110 | {{ section }} 111 | # When using zoxide with --no-cmd, alias these internal functions as desired. 112 | # 113 | 114 | 115 | @__zoxide_errhandler 116 | def __zoxide_z(args: list[str]) -> None: 117 | """Jump to a directory using only keywords.""" 118 | if args == []: 119 | __zoxide_cd() 120 | elif args == ["-"]: 121 | __zoxide_cd("-") 122 | elif len(args) == 1 and os.path.isdir(args[0]): 123 | __zoxide_cd(args[0]) 124 | else: 125 | try: 126 | zoxide = __zoxide_bin() 127 | cmd = subprocess.run( 128 | [zoxide, "query", "--exclude", __zoxide_pwd(), "--"] + args, 129 | check=True, 130 | env=__zoxide_env(), 131 | stdout=subprocess.PIPE, 132 | ) 133 | except subprocess.CalledProcessError as exc: 134 | raise ZoxideSilentException() from exc 135 | 136 | result = cmd.stdout[:-1] 137 | __zoxide_cd(result) 138 | 139 | 140 | @__zoxide_errhandler 141 | def __zoxide_zi(args: list[str]) -> None: 142 | """Jump to a directory using interactive search.""" 143 | try: 144 | zoxide = __zoxide_bin() 145 | cmd = subprocess.run( 146 | [zoxide, "query", "-i", "--"] + args, 147 | check=True, 148 | env=__zoxide_env(), 149 | stdout=subprocess.PIPE, 150 | ) 151 | except subprocess.CalledProcessError as exc: 152 | raise ZoxideSilentException() from exc 153 | 154 | result = cmd.stdout[:-1] 155 | __zoxide_cd(result) 156 | 157 | 158 | {{ section }} 159 | # Commands for zoxide. Disable these using --no-cmd. 160 | # 161 | 162 | {%- match cmd %} 163 | {%- when Some with (cmd) %} 164 | 165 | builtins.aliases["{{cmd}}"] = __zoxide_z # type: ignore # pylint:disable=no-member 166 | builtins.aliases["{{cmd}}i"] = __zoxide_zi # type: ignore # pylint:disable=no-member 167 | 168 | {%- when None %} 169 | 170 | {{ not_configured }} 171 | 172 | {%- endmatch %} 173 | 174 | {{ section }} 175 | # To initialize zoxide, add this to your configuration (usually ~/.xonshrc): 176 | # 177 | # execx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide') 178 | -------------------------------------------------------------------------------- /templates/zsh.txt: -------------------------------------------------------------------------------- 1 | {%- let section = "# =============================================================================\n#" -%} 2 | {%- let not_configured = "# -- not configured --" -%} 3 | 4 | # shellcheck shell=bash 5 | 6 | {{ section }} 7 | # Utility functions for zoxide. 8 | # 9 | 10 | # pwd based on the value of _ZO_RESOLVE_SYMLINKS. 11 | function __zoxide_pwd() { 12 | {%- if cfg!(windows) %} 13 | \command cygpath -w "$(\builtin pwd -P)" 14 | {%- else if resolve_symlinks %} 15 | \builtin pwd -P 16 | {%- else %} 17 | \builtin pwd -L 18 | {%- endif %} 19 | } 20 | 21 | # cd + custom logic based on the value of _ZO_ECHO. 22 | function __zoxide_cd() { 23 | # shellcheck disable=SC2164 24 | \builtin cd -- "$@" {%- if echo %} && __zoxide_pwd {%- endif %} 25 | } 26 | 27 | {{ section }} 28 | # Hook configuration for zoxide. 29 | # 30 | 31 | # Hook to add new entries to the database. 32 | function __zoxide_hook() { 33 | # shellcheck disable=SC2312 34 | \command zoxide add -- "$(__zoxide_pwd)" 35 | } 36 | 37 | # Initialize hook. 38 | \builtin typeset -ga precmd_functions 39 | \builtin typeset -ga chpwd_functions 40 | # shellcheck disable=SC2034,SC2296 41 | precmd_functions=("${(@)precmd_functions:#__zoxide_hook}") 42 | # shellcheck disable=SC2034,SC2296 43 | chpwd_functions=("${(@)chpwd_functions:#__zoxide_hook}") 44 | 45 | {%- if hook == InitHook::Prompt %} 46 | precmd_functions+=(__zoxide_hook) 47 | {%- else if hook == InitHook::Pwd %} 48 | chpwd_functions+=(__zoxide_hook) 49 | {%- endif %} 50 | 51 | # Report common issues. 52 | function __zoxide_doctor() { 53 | {%- if hook == InitHook::None %} 54 | return 0 55 | 56 | {%- else %} 57 | [[ ${_ZO_DOCTOR:-1} -ne 0 ]] || return 0 58 | 59 | {%- if hook == InitHook::Prompt %} 60 | [[ ${precmd_functions[(Ie)__zoxide_hook]:-} -eq 0 ]] || return 0 61 | {%- else if hook == InitHook::Pwd %} 62 | [[ ${chpwd_functions[(Ie)__zoxide_hook]:-} -eq 0 ]] || return 0 63 | {%- endif %} 64 | 65 | _ZO_DOCTOR=0 66 | \builtin printf '%s\n' \ 67 | 'zoxide: detected a possible configuration issue.' \ 68 | 'Please ensure that zoxide is initialized right at the end of your shell configuration file (usually ~/.zshrc).' \ 69 | '' \ 70 | 'If the issue persists, consider filing an issue at:' \ 71 | 'https://github.com/ajeetdsouza/zoxide/issues' \ 72 | '' \ 73 | 'Disable this message by setting _ZO_DOCTOR=0.' \ 74 | '' >&2 75 | {%- endif %} 76 | } 77 | 78 | {{ section }} 79 | # When using zoxide with --no-cmd, alias these internal functions as desired. 80 | # 81 | 82 | # Jump to a directory using only keywords. 83 | function __zoxide_z() { 84 | __zoxide_doctor 85 | if [[ "$#" -eq 0 ]]; then 86 | __zoxide_cd ~ 87 | elif [[ "$#" -eq 1 ]] && { [[ -d "$1" ]] || [[ "$1" = '-' ]] || [[ "$1" =~ ^[-+][0-9]$ ]]; }; then 88 | __zoxide_cd "$1" 89 | elif [[ "$#" -eq 2 ]] && [[ "$1" = "--" ]]; then 90 | __zoxide_cd "$2" 91 | else 92 | \builtin local result 93 | # shellcheck disable=SC2312 94 | result="$(\command zoxide query --exclude "$(__zoxide_pwd)" -- "$@")" && __zoxide_cd "${result}" 95 | fi 96 | } 97 | 98 | # Jump to a directory using interactive search. 99 | function __zoxide_zi() { 100 | __zoxide_doctor 101 | \builtin local result 102 | result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${result}" 103 | } 104 | 105 | {{ section }} 106 | # Commands for zoxide. Disable these using --no-cmd. 107 | # 108 | 109 | {%- match cmd %} 110 | {%- when Some with (cmd) %} 111 | 112 | function {{ cmd }}() { 113 | __zoxide_z "$@" 114 | } 115 | 116 | function {{ cmd }}i() { 117 | __zoxide_zi "$@" 118 | } 119 | 120 | {%- when None %} 121 | 122 | {{ not_configured }} 123 | 124 | {%- endmatch %} 125 | 126 | # Completions. 127 | if [[ -o zle ]]; then 128 | __zoxide_result='' 129 | 130 | function __zoxide_z_complete() { 131 | # Only show completions when the cursor is at the end of the line. 132 | # shellcheck disable=SC2154 133 | [[ "{{ "${#words[@]}" }}" -eq "${CURRENT}" ]] || return 0 134 | 135 | if [[ "{{ "${#words[@]}" }}" -eq 2 ]]; then 136 | # Show completions for local directories. 137 | _cd -/ 138 | 139 | elif [[ "${words[-1]}" == '' ]]; then 140 | # Show completions for Space-Tab. 141 | # shellcheck disable=SC2086 142 | __zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd || \builtin true)" --interactive -- ${words[2,-1]})" || __zoxide_result='' 143 | 144 | # Set a result to ensure completion doesn't re-run 145 | compadd -Q "" 146 | 147 | # Bind '\e[0n' to helper function. 148 | \builtin bindkey '\e[0n' '__zoxide_z_complete_helper' 149 | # Sends query device status code, which results in a '\e[0n' being sent to console input. 150 | \builtin printf '\e[5n' 151 | 152 | # Report that the completion was successful, so that we don't fall back 153 | # to another completion function. 154 | return 0 155 | fi 156 | } 157 | 158 | function __zoxide_z_complete_helper() { 159 | if [[ -n "${__zoxide_result}" ]]; then 160 | # shellcheck disable=SC2034,SC2296 161 | BUFFER="{{ cmd.unwrap_or("cd") }} ${(q-)__zoxide_result}" 162 | __zoxide_result='' 163 | \builtin zle reset-prompt 164 | \builtin zle accept-line 165 | else 166 | \builtin zle reset-prompt 167 | fi 168 | } 169 | \builtin zle -N __zoxide_z_complete_helper 170 | {%- if let Some(cmd) = cmd %} 171 | 172 | [[ "${+functions[compdef]}" -ne 0 ]] && \compdef __zoxide_z_complete {{ cmd }} 173 | {%- endif %} 174 | fi 175 | 176 | {{ section }} 177 | # To initialize zoxide, add this to your shell configuration file (usually ~/.zshrc): 178 | # 179 | # eval "$(zoxide init zsh)" 180 | -------------------------------------------------------------------------------- /tests/completions.rs: -------------------------------------------------------------------------------- 1 | //! Test clap generated completions. 2 | #![cfg(feature = "nix-dev")] 3 | 4 | use assert_cmd::Command; 5 | 6 | #[test] 7 | fn completions_bash() { 8 | let source = include_str!("../contrib/completions/zoxide.bash"); 9 | Command::new("bash") 10 | .args(["--noprofile", "--norc", "-c", source]) 11 | .assert() 12 | .success() 13 | .stdout("") 14 | .stderr(""); 15 | } 16 | 17 | // Elvish: the completions file uses editor commands to add completions to the 18 | // shell. However, Elvish does not support running editor commands from a 19 | // script, so we can't create a test for this. See: https://github.com/elves/elvish/issues/1299 20 | 21 | #[test] 22 | fn completions_fish() { 23 | let source = include_str!("../contrib/completions/zoxide.fish"); 24 | let tempdir = tempfile::tempdir().unwrap(); 25 | let tempdir = tempdir.path().to_str().unwrap(); 26 | 27 | Command::new("fish") 28 | .env("HOME", tempdir) 29 | .args(["--command", source, "--private"]) 30 | .assert() 31 | .success() 32 | .stdout("") 33 | .stderr(""); 34 | } 35 | 36 | #[test] 37 | fn completions_powershell() { 38 | let source = include_str!("../contrib/completions/_zoxide.ps1"); 39 | Command::new("pwsh") 40 | .args(["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source]) 41 | .assert() 42 | .success() 43 | .stdout("") 44 | .stderr(""); 45 | } 46 | 47 | #[test] 48 | fn completions_zsh() { 49 | let source = r#" 50 | set -eu 51 | completions='./contrib/completions' 52 | test -d "$completions" 53 | fpath=("$completions" $fpath) 54 | autoload -Uz compinit 55 | compinit -u 56 | "#; 57 | 58 | Command::new("zsh").args(["-c", source, "--no-rcs"]).assert().success().stdout("").stderr(""); 59 | } 60 | -------------------------------------------------------------------------------- /zoxide.plugin.zsh: -------------------------------------------------------------------------------- 1 | if (( $+commands[zoxide] )); then 2 | eval "$(zoxide init zsh)" 3 | else 4 | echo 'zoxide: command not found, please install it from https://github.com/ajeetdsouza/zoxide' 5 | fi 6 | --------------------------------------------------------------------------------