Merge branch 'ajeetdsouza:main' into install

This commit is contained in:
eWɘyn 2023-11-04 16:30:51 +02:00 committed by GitHub
commit e7b753e365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 3380 additions and 2217 deletions

View File

@ -1,2 +0,0 @@
[advisories]
ignore = ["RUSTSEC-2020-0095"]

View File

@ -1,2 +1,7 @@
[alias] [alias]
xtask = "run --package xtask --" xtask = "run --package xtask --"
# On Windows MSVC, statically link the C runtime so that the resulting EXE does
# not depend on the vcruntime DLL.
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]

View File

@ -2,4 +2,9 @@ version = 1
[[analyzers]] [[analyzers]]
name = "rust" name = "rust"
enabled = true
[analyzers.meta]
msrv = "stable"
[[analyzers]]
name = "shell"

View File

@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
* Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
* Focusing on what is best not just for us as individuals, but for the - Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban ### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community **Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals. individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within **Consequence**: A permanent ban from any sort of public interaction within

6
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

View File

@ -4,31 +4,51 @@ on:
branches: [main] branches: [main]
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
env:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs: jobs:
ci: ci:
name: ${{ matrix.os }} name: ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
if: ${{ matrix.os == 'windows-latest' }} if: ${{ matrix.os == 'windows-latest' }}
with: with:
toolchain: stable components: clippy
components: rustfmt, clippy
profile: minimal profile: minimal
override: true toolchain: stable
- uses: cachix/install-nix-action@v15 - uses: actions-rs/toolchain@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
components: rustfmt
profile: minimal
toolchain: nightly
- uses: cachix/install-nix-action@v23
if: ${{ matrix.os != 'windows-latest' }} if: ${{ matrix.os != 'windows-latest' }}
with: with:
nix_path: nixpkgs=channel:nixpkgs-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v12
- run: cargo xtask ci if: ${{ matrix.os != 'windows-latest' && env.CACHIX_AUTH_TOKEN != '' }}
if: ${{ matrix.os == 'windows-latest' }} with:
- run: nix-shell --cores 0 --pure --run 'rm -rf ~/.cargo/bin; cargo xtask ci' authToken: ${{ env.CACHIX_AUTH_TOKEN }}
if: ${{ matrix.os != 'windows-latest' }} name: zoxide
- name: Setup cache
uses: Swatinem/rust-cache@v2.7.0
with:
key: ${{ matrix.os }}
- name: Install just
uses: taiki-e/install-action@v2
with:
tool: just
- name: Run lints + tests
run: just lint test

22
.github/workflows/no-response.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: no-response
on:
issue_comment:
types: [created]
schedule:
- cron: "0 0 * * *" # daily at 00:00
jobs:
no-response:
if: github.repository == 'ajeetdsouza/zoxide'
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
daysUntilClose: 30
responseRequiredLabel: waiting-for-response
closeComment: >
This issue has been automatically closed due to inactivity. If you feel
this is still relevant, please comment here or create a fresh issue.

View File

@ -1,11 +1,11 @@
name: release name: release
on: on:
push: push:
branches: [main]
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
pull_request:
workflow_dispatch: workflow_dispatch:
env:
CARGO_INCREMENTAL: 0
permissions:
contents: write
jobs: jobs:
release: release:
name: ${{ matrix.target }} name: ${{ matrix.target }}
@ -16,38 +16,33 @@ jobs:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-musl target: x86_64-unknown-linux-musl
deb: true
- os: ubuntu-latest - os: ubuntu-latest
target: arm-unknown-linux-musleabihf target: arm-unknown-linux-musleabihf
- os: ubuntu-latest - os: ubuntu-latest
target: armv7-unknown-linux-musleabihf target: armv7-unknown-linux-musleabihf
- os: ubuntu-latest - os: ubuntu-latest
target: aarch64-unknown-linux-musl target: aarch64-unknown-linux-musl
deb: true
- os: macos-11 - os: macos-11
target: x86_64-apple-darwin target: x86_64-apple-darwin
- os: macos-11 - os: macos-11
target: aarch64-apple-darwin target: aarch64-apple-darwin
- os: windows-latest - os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
- os: windows-latest - os: windows-latest
target: aarch64-pc-windows-msvc target: aarch64-pc-windows-msvc
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get version
- name: Set artifact name id: get_version
shell: bash uses: SebRollen/toml-action@v1.0.2
run: | with:
version="$(git describe --tags --match='v*.*.*' --always)" file: Cargo.toml
name="zoxide-$version-${{ matrix.target }}" field: package.version
echo "ARTIFACT_NAME=$name" >> $GITHUB_ENV
echo "version: $version"
echo "artifact: $name"
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -55,45 +50,58 @@ jobs:
profile: minimal profile: minimal
override: true override: true
target: ${{ matrix.target }} target: ${{ matrix.target }}
- name: Setup cache
uses: Swatinem/rust-cache@v2.7.0
with:
key: ${{ matrix.target }}
- name: Build binary - name: Build binary
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: build command: build
args: --release --locked --target=${{ matrix.target }} --color=always --verbose args: --release --locked --target=${{ matrix.target }} --color=always --verbose
use-cross: ${{ runner.os == 'Linux' }} use-cross: ${{ runner.os == 'Linux' }}
- name: Install cargo-deb
if: ${{ matrix.deb == true }}
uses: actions-rs/install@v0.1
with:
crate: cargo-deb
- name: Build deb
if: ${{ matrix.deb == true }}
uses: actions-rs/cargo@v1
with:
command: deb
args: --no-build --no-strip --output=. --target=${{ matrix.target }}
- name: Package (*nix) - name: Package (*nix)
if: runner.os != 'Windows' if: runner.os != 'Windows'
run: > run: |
tar -cv tar -cv CHANGELOG.md LICENSE README.md man/ \
CHANGELOG.md LICENSE README.md -C contrib/ completions/ -C ../ \
man/ -C target/${{ matrix.target }}/release/ zoxide |
-C contrib/ completions/ -C ../ gzip --best > \
-C target/${{ matrix.target }}/release/ zoxide zoxide-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.tar.gz
| gzip --best > '${{ env.ARTIFACT_NAME }}.tar.gz'
- name: Package (Windows) - name: Package (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: > run: |
7z a ${{ env.ARTIFACT_NAME }}.zip 7z a zoxide-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.zip `
CHANGELOG.md LICENSE README.md CHANGELOG.md LICENSE README.md ./man/ ./contrib/completions/ `
./man/ ./target/${{ matrix.target }}/release/zoxide.exe
./contrib/completions/
./target/${{ matrix.target }}/release/zoxide.exe
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.target }} name: ${{ matrix.target }}
path: | path: |
*.zip *.deb
*.tar.gz *.tar.gz
*.zip
- name: Create release - name: Create release
if: startsWith(github.ref, 'refs/tags/v') if: |
github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'chore(release)')
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
draft: true draft: true
files: | files: |
*.zip *.deb
*.tar.gz *.tar.gz
*.zip
name: ${{ steps.get_version.outputs.value }}
tag_name: ""

13
.github/workflows/winget.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: winget
on:
release:
types: [released]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: ajeetdsouza.zoxide
installers-regex: '-pc-windows-msvc\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
# Compiled files and executables # Compiled files and executables
debug/ debug/
target/ target/
target_nix/
# Backup files generated by rustfmt # Backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk

View File

@ -7,22 +7,101 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## [0.9.2] - 2023-08-04
### Added
- Short option `-a` for `zoxide query --all`.
### Fixed
- PowerShell: use `global` scope for variables / functions.
## [0.9.1] - 2023-05-07
### Added
- Fish/Zsh: aliases on `__zoxide_z` will now use completions.
- Nushell: add support for v0.78.0.
- Fish: plugin now works on older versions.
- PowerShell: warn when PowerShell version is too old for `z -` and `z +`.
- PowerShell: support for PWD hooks on all versions.
### Fixed
- Fish: not providing `cd` completions when there is a space in the path.
- Bash/Fish/Zsh: providing `z` completions when the last argument starts with `z!`.
- Bash/Fish/Zsh: attempting to `cd` when the last argument is `z!`.
## [0.9.0] - 2023-01-08
### Added
- `edit` subcommand to adjust the scores of entries.
### Fixed
- Zsh: completions clashing with `zsh-autocomplete`.
- Fzf: 'invalid option' on macOS.
- PowerShell: handle UTF-8 encoding correctly.
- Zsh: don't hide output from `chpwd` hooks.
- Nushell: upgrade minimum supported version to v0.73.0.
- Zsh: fix extra space in interactive completions when no match is found.
- Fzf: various improvements, upgrade minimum supported version to v0.33.0.
- Nushell: accidental redefinition of hooks when initialized twice.
### Removed
- `remove -i` subcommand: use `edit` instead.
## [0.8.3] - 2022-09-02
### Added
- Nushell: support for `z -`.
- Nushell: support for PWD hooks.
### Changed
- Fish: change fuzzy completion prefix to `z!`.
- Zsh: allow `z` to navigate dirstack via `+n` and `-n`.
- Fzf: improved preview window.
### Fixed
- Bash: double forward slash in completions.
## [0.8.2] - 2022-06-26
### Changed
- Fzf: show preview window below results.
### Fixed
- Bash/Fish/POSIX/Zsh: paths on Cygwin.
- Fish: completions not working on certain systems.
- Bash: completions not escaping spaces correctly.
## [0.8.1] - 2021-04-23
### Changed ### Changed
- Manpages: moved to `man/man1/*.1`. - Manpages: moved to `man/man1/*.1`.
- Replace `--no-aliases` with `--no-cmd`.
- Elvish: upgrade minimum supported version to v0.18.0.
- Nushell: upgrade minimum supported version to v0.61.0.
### Fixed ### Fixed
- Bash/Zsh: rename `_z` completion function to avoid conflicts with other shell - Bash/Zsh: rename `_z` completion function to avoid conflicts with other shell
plugins. plugins.
- Elvish: upgrade to new lambda syntax. - Fzf: added `--keep-right` option by default, upgrade minimum supported version
- Fzf: added `--keep-right` option by default, upgraded minimum version to to v0.21.0.
v0.21.0.
- Bash: only enable completions on 4.4+. - Bash: only enable completions on 4.4+.
- Fzf: bypass `ls` alias in preview window. - Fzf: bypass `ls` alias in preview window.
- Retain ownership of database file. - Retain ownership of database file.
- `zoxide query --interactive` should not conflict with `--score`.
## [0.8.0] - 2021-12-25 ## [0.8.0] - 2021-12-25
@ -73,7 +152,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- PowerShell: Hook not initializing correctly. - PowerShell: hook not initializing correctly.
## [0.7.6] - 2021-10-13 ## [0.7.6] - 2021-10-13
@ -371,6 +450,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- GitHub Actions pipeline to build and upload releases. - GitHub Actions pipeline to build and upload releases.
- Support for zsh. - Support for zsh.
[0.9.2]: https://github.com/ajeetdsouza/zoxide/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/ajeetdsouza/zoxide/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.3...v0.9.0
[0.8.3]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.2...v0.8.3
[0.8.2]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/ajeetdsouza/zoxide/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.9...v0.8.0 [0.8.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.9...v0.8.0
[0.7.9]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.8...v0.7.9 [0.7.9]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.8...v0.7.9
[0.7.8]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.7...v0.7.8 [0.7.8]: https://github.com/ajeetdsouza/zoxide/compare/v0.7.7...v0.7.8

791
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,43 +3,50 @@ authors = ["Ajeet D'Souza <98ajeet@gmail.com>"]
categories = ["command-line-utilities", "filesystem"] categories = ["command-line-utilities", "filesystem"]
description = "A smarter cd command for your terminal" description = "A smarter cd command for your terminal"
edition = "2021" edition = "2021"
keywords = ["cli"] homepage = "https://github.com/ajeetdsouza/zoxide"
keywords = ["cli", "filesystem", "shell", "tool", "utility"]
license = "MIT" license = "MIT"
name = "zoxide" name = "zoxide"
readme = "README.md"
repository = "https://github.com/ajeetdsouza/zoxide" repository = "https://github.com/ajeetdsouza/zoxide"
rust-version = "1.59" rust-version = "1.65"
version = "0.8.0" version = "0.9.2"
[badges] [badges]
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[workspace]
members = ["xtask/"]
[dependencies] [dependencies]
anyhow = "1.0.32" anyhow = "1.0.32"
askama = { version = "0.11.0", default-features = false } askama = { version = "0.12.0", default-features = false }
bincode = "1.3.1" bincode = "1.3.1"
clap = { version = "3.1.0", features = ["derive"] } clap = { version = "4.3.0", features = ["derive"] }
dirs = "4.0.0" color-print = "0.3.4"
dirs = "5.0.0"
dunce = "1.0.1" dunce = "1.0.1"
fastrand = "1.7.0" fastrand = "2.0.0"
glob = "0.3.0" glob = "0.3.0"
ordered-float = "2.0.0" ouroboros = "0.17.2"
serde = { version = "1.0.116", features = ["derive"] } serde = { version = "1.0.116", features = ["derive"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.23.1" nix = { version = "0.26.1", default-features = false, features = [
"fs",
"user",
] }
[target.'cfg(windows)'.dependencies]
which = "4.2.5"
[build-dependencies] [build-dependencies]
clap = { version = "3.1.0", features = ["derive"] } clap = { version = "4.3.0", features = ["derive"] }
clap_complete = "3.1.0" clap_complete = "4.3.0"
clap_complete_fig = "3.1.0" clap_complete_fig = "4.3.0"
color-print = "0.3.4"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.0" assert_cmd = "2.0.0"
rstest = "0.12.0" rstest = { version = "0.18.0", default-features = false }
rstest_reuse = "0.3.0" rstest_reuse = "0.6.0"
tempfile = "3.1.0" tempfile = "3.1.0"
[features] [features]
@ -48,5 +55,56 @@ nix-dev = []
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
debug = 0
lto = true lto = true
strip = true strip = true
[package.metadata.deb]
assets = [
[
"target/release/zoxide",
"usr/bin/",
"755",
],
[
"contrib/completions/zoxide.bash",
"usr/share/bash-completion/completions/zoxide",
"644",
],
[
"contrib/completions/zoxide.fish",
"usr/share/fish/vendor_completions.d/",
"664",
],
[
"contrib/completions/_zoxide",
"usr/share/zsh/vendor-completions/",
"644",
],
[
"man/man1/*",
"usr/share/man/man1/",
"644",
],
[
"README.md",
"usr/share/doc/zoxide/",
"644",
],
[
"CHANGELOG.md",
"usr/share/doc/zoxide/",
"644",
],
[
"LICENSE",
"usr/share/doc/zoxide/",
"644",
],
]
extended-description = """\
zoxide is a smarter cd command, inspired by z and autojump. It remembers which \
directories you use most frequently, so you can "jump" to them in just a few \
keystrokes."""
priority = "optional"
section = "utils"

2
Cross.toml Normal file
View File

@ -0,0 +1,2 @@
[build.env]
passthrough = ["CARGO_INCREMENTAL"]

613
README.md
View File

@ -9,6 +9,20 @@
<div align="center"> <div align="center">
<sup>Special thanks to:</sup>
<a href="https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=zoxide_20231001">
<div>
<img src="contrib/warp.png" width="230" alt="Warp" />
</div>
<b>Warp is a modern, Rust-based terminal with AI built in so you and your team can build great software, faster.</b>
<div>
<sup>Visit <u>warp.dev</u> to learn more.</sup>
</div>
</a>
<hr />
# zoxide # zoxide
[![crates.io][crates.io-badge]][crates.io] [![crates.io][crates.io-badge]][crates.io]
@ -51,240 +65,300 @@ Read more about the matching algorithm [here][algorithm-matching].
## Installation ## Installation
### *Step 1: Install zoxide* zoxide can be installed in 4 easy steps:
zoxide runs on most major platforms. If your platform isn't listed below, 1. **Install binary**
please [open an issue][issues].
zoxide runs on most major platforms. If your platform isn't listed below,
<details> please [open an issue][issues].
<summary>Linux</summary>
<details>
To install zoxide, run this command in your terminal: <summary>Linux</summary>
```sh > The recommended way to install zoxide is via the install script:
curl -sS https://webinstall.dev/zoxide | bash >
``` > ```sh
> curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash
Alternatively, you can use a package manager: > ```
>
| Distribution | Repository | Instructions | > Or, you can use a package manager:
| ------------------ | ----------------------- | ---------------------------------------------------------------------------------------------- | >
| ***Any*** | **[crates.io]** | `cargo install zoxide --locked` | > | Distribution | Repository | Instructions |
| *Any* | [conda-forge] | `conda install -c conda-forge zoxide` | > | ------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
| *Any* | [Linuxbrew] | `brew install zoxide` | > | **_Any_** | **[crates.io]** | `cargo install zoxide --locked` |
| Alpine Linux 3.13+ | [Alpine Linux Packages] | `apk add zoxide` | > | _Any_ | [asdf] | `asdf plugin add zoxide https://github.com/nyrst/asdf-zoxide.git` <br /> `asdf install zoxide latest` |
| Arch Linux | [Arch Linux Community] | `pacman -S zoxide` | > | _Any_ | [conda-forge] | `conda install -c conda-forge zoxide` |
| CentOS 7+ | [Copr] | `dnf copr enable atim/zoxide` <br /> `dnf install zoxide` | > | _Any_ | [Linuxbrew] | `brew install zoxide` |
| Debian 11+ | [Debian Packages] | `apt install zoxide` | > | _Any_ | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` |
| Devuan 4.0+ | [Devuan Packages] | `apt install zoxide` | > | Alpine Linux 3.13+ | [Alpine Linux Packages] | `apk add zoxide` |
| Fedora 32+ | [Fedora Packages] | `dnf install zoxide` | > | Arch Linux | [Arch Linux Extra] | `pacman -S zoxide` |
| Gentoo | [GURU Overlay] | `eselect repository enable guru` <br /> `emerge --sync guru` <br /> `emerge app-shells/zoxide` | > | CentOS 7+ | [Copr] | `dnf copr enable atim/zoxide` <br /> `dnf install zoxide` |
| Manjaro | | `pacman -S zoxide` | > | Debian 11+[^1] | [Debian Packages] | `apt install zoxide` |
| NixOS | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` | > | Devuan 4.0+[^1] | [Devuan Packages] | `apt install zoxide` |
| Parrot OS | | `apt install zoxide` | > | Fedora 32+ | [Fedora Packages] | `dnf install zoxide` |
| Raspbian 11+ | [Raspbian Packages] | `apt install zoxide` | > | Gentoo | [GURU Overlay] | `eselect repository enable guru` <br /> `emerge --sync guru` <br /> `emerge app-shells/zoxide` |
| Ubuntu 21.04+ | [Ubuntu Packages] | `apt install zoxide` | > | Manjaro | | `pacman -S zoxide` |
| Void Linux | [Void Linux Packages] | `xbps-install -S zoxide` | > | openSUSE Tumbleweed | [openSUSE Factory] | `zypper install zoxide` |
> | Parrot OS[^1] | | `apt install zoxide` |
</details> > | Raspbian 11+[^1] | [Raspbian Packages] | `apt install zoxide` |
> | Rhino Linux | [Pacstall Packages] | `pacstall -I zoxide-deb` |
<details> > | Slackware 15.0+ | [SlackBuilds] | [Instructions][slackbuilds-howto] |
<summary>macOS</summary> > | Ubuntu 21.04+[^1] | [Ubuntu Packages] | `apt install zoxide` |
> | Void Linux | [Void Linux Packages] | `xbps-install -S zoxide` |
To install zoxide, use a package manager:
</details>
| Repository | Instructions |
| --------------- | ------------------------------------- | <details>
| **[crates.io]** | `cargo install zoxide --locked` | <summary>macOS</summary>
| [conda-forge] | `conda install -c conda-forge zoxide` |
| [Homebrew] | `brew install zoxide` | > To install zoxide, use a package manager:
| [MacPorts] | `port install zoxide` | >
> | Repository | Instructions |
</details> > | --------------- | ----------------------------------------------------------------------------------------------------- |
> | **[crates.io]** | `cargo install zoxide --locked` |
<details> > | **[Homebrew]** | `brew install zoxide` |
<summary>Windows</summary> > | [asdf] | `asdf plugin add zoxide https://github.com/nyrst/asdf-zoxide.git` <br /> `asdf install zoxide latest` |
> | [conda-forge] | `conda install -c conda-forge zoxide` |
To install zoxide, run this command in your command prompt: > | [MacPorts] | `port install zoxide` |
> | [nixpkgs] | `nix-env -iA nixpkgs.zoxide` |
```sh >
curl.exe -A "MS" https://webinstall.dev/zoxide | powershell > Or, run this command in your terminal:
``` >
> ```sh
Alternatively, you can use a package manager: > curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash
> ```
| Repository | Instructions |
| --------------- | ------------------------------------- | </details>
| **[crates.io]** | `cargo install zoxide --locked` |
| [Chocolatey] | `choco install zoxide` | <details>
| [conda-forge] | `conda install -c conda-forge zoxide` | <summary>Windows</summary>
| [Scoop] | `scoop install zoxide` |
> The recommended way to install zoxide is via `winget`:
</details> >
> ```sh
<details> > winget install ajeetdsouza.zoxide
<summary>BSD</summary> > ```
>
To install zoxide, use a package manager: > Or, you can use an alternative package manager:
>
| Distribution | Repository | Instructions | > | Repository | Instructions |
| ------------- | --------------- | ------------------------------- | > | --------------- | ------------------------------------- |
| ***Any*** | **[crates.io]** | `cargo install zoxide --locked` | > | **[crates.io]** | `cargo install zoxide --locked` |
| DragonFly BSD | [DPorts] | `pkg install zoxide` | > | [Chocolatey] | `choco install zoxide` |
| FreeBSD | [FreshPorts] | `pkg install zoxide` | > | [conda-forge] | `conda install -c conda-forge zoxide` |
| NetBSD | [pkgsrc] | `pkgin install zoxide` | > | [Scoop] | `scoop install zoxide` |
>
</details> > If you're using Cygwin, Git Bash, or MSYS2, use the install script instead:
>
<details> > ```sh
<summary>Android</summary> > curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash
> ```
To install zoxide, use a package manager:
</details>
| Repository | Instructions |
| ---------- | -------------------- | <details>
| [Termux] | `pkg install zoxide` | <summary>BSD</summary>
</details> > To install zoxide, use a package manager:
>
### *Step 2: Add zoxide to your shell* > | Distribution | Repository | Instructions |
> | ------------- | --------------- | ------------------------------- |
To start using zoxide, add it to your shell. > | **_Any_** | **[crates.io]** | `cargo install zoxide --locked` |
> | DragonFly BSD | [DPorts] | `pkg install zoxide` |
<details> > | FreeBSD | [FreshPorts] | `pkg install zoxide` |
<summary>Bash</summary> > | NetBSD | [pkgsrc] | `pkgin install zoxide` |
Add this to your configuration (usually `~/.bashrc`): </details>
```sh <details>
eval "$(zoxide init bash)" <summary>Android</summary>
```
> To install zoxide, use a package manager:
</details> >
> | Repository | Instructions |
<details> > | ---------- | -------------------- |
<summary>Elvish</summary> > | [Termux] | `pkg install zoxide` |
Add this to your configuration (usually `~/.elvish/rc.elv`): </details>
```sh 2. **Setup zoxide on your shell**
eval (zoxide init elvish | slurp)
``` To start using zoxide, add it to your shell.
Note: zoxide only supports elvish v0.16.0 and above. <details>
<summary>Bash</summary>
</details>
> Add this to the **end** of your config file (usually `~/.bashrc`):
<details> >
<summary>Fish</summary> > ```sh
> eval "$(zoxide init bash)"
Add this to your configuration (usually `~/.config/fish/config.fish`): > ```
```fish </details>
zoxide init fish | source
``` <details>
<summary>Elvish</summary>
</details>
> Add this to the **end** of your config file (usually `~/.elvish/rc.elv`):
<details> >
<summary>Nushell</summary> > ```sh
> eval (zoxide init elvish | slurp)
Add this to your configuration (find it by running `config path` in Nushell): > ```
>
```toml > **Note**
startup = ["zoxide init nushell --hook prompt | save ~/.zoxide.nu", "source ~/.zoxide.nu"] > zoxide only supports elvish v0.18.0 and above.
```
</details>
Note: zoxide only supports Nushell v0.37.0 and above.
<details>
</details> <summary>Fish</summary>
<details> > Add this to the **end** of your config file (usually
<summary>PowerShell</summary> > `~/.config/fish/config.fish`):
>
Add this to your configuration (find it by running `echo $profile` in > ```fish
PowerShell): > zoxide init fish | source
> ```
```powershell
# For zoxide v0.8.0+ </details>
Invoke-Expression (& {
$hook = if ($PSVersionTable.PSVersion.Major -lt 6) { 'prompt' } else { 'pwd' } <details>
(zoxide init --hook $hook powershell | Out-String) <summary>Nushell</summary>
})
> Add this to the **end** of your env file (find it by running `$nu.env-path`
# For older versions of zoxide > in Nushell):
Invoke-Expression (& { >
$hook = if ($PSVersionTable.PSVersion.Major -lt 6) { 'prompt' } else { 'pwd' } > ```sh
(zoxide init --hook $hook powershell) -join "`n" > zoxide init nushell | save -f ~/.zoxide.nu
}) > ```
``` >
> Now, add this to the **end** of your config file (find it by running
</details> > `$nu.config-path` in Nushell):
>
<details> > ```sh
<summary>Xonsh</summary> > source ~/.zoxide.nu
> ```
Add this to your configuration (usually `~/.xonshrc`): >
> **Note**
```python > zoxide only supports Nushell v0.73.0 and above.
execx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide')
``` </details>
</details> <details>
<summary>PowerShell</summary>
<details>
<summary>Zsh</summary> > Add this to the **end** of your config file (find it by running
> `echo $profile` in PowerShell):
Add this to your configuration (usually `~/.zshrc`): >
> ```powershell
```sh > Invoke-Expression (& { (zoxide init powershell | Out-String) })
eval "$(zoxide init zsh)" > ```
```
</details>
For completions to work, the above line must be added _after_ `compinit` is
called. You may have to rebuild your cache by running `rm ~/.zcompdump*; compinit`. <details>
<summary>Xonsh</summary>
</details>
> Add this to the **end** of your config file (usually `~/.xonshrc`):
<details> >
<summary>Any POSIX shell</summary> > ```python
> execx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide')
Add this to your configuration: > ```
```sh </details>
eval "$(zoxide init posix --hook prompt)"
``` <details>
<summary>Zsh</summary>
</details>
> Add this to the **end** of your config file (usually `~/.zshrc`):
### *Step 3: Install fzf (optional)* >
> ```sh
[fzf] is a command-line fuzzy finder, used by zoxide for interactive > eval "$(zoxide init zsh)"
selection. It can be installed from [here][fzf-installation]. zoxide supports > ```
fzf v0.21.0+. >
> For completions to work, the above line must be added _after_ `compinit` is
### *Step 4: Import your data (optional)* > called. You may have to rebuild your completions cache by running
> `rm ~/.zcompdump*; compinit`.
If you currently use any of the following utilities, you may want to import
your data into zoxide: </details>
<details> <details>
<summary>autojump</summary> <summary>Any POSIX shell</summary>
```sh > Add this to the **end** of your config file:
zoxide import --from autojump path/to/db >
``` > ```sh
> eval "$(zoxide init posix --hook prompt)"
</details> > ```
<details> </details>
<summary>z, z.lua, or zsh-z</summary>
3. **Install fzf** <sup>(optional)</sup>
```sh
zoxide import --from z path/to/db [fzf] is a command-line fuzzy finder, used by zoxide for completions /
``` interactive selection. It can be installed from [here][fzf-installation].
</details> > **Note**
> zoxide only supports fzf v0.33.0 and above.
4. **Import your data** <sup>(optional)</sup>
If you currently use any of these plugins, you may want to import your data
into zoxide:
<details>
<summary>autojump</summary>
> Run this command in your terminal:
>
> ```sh
> zoxide import --from=autojump "/path/to/autojump/db"
> ```
>
> The path usually varies according to your system:
>
> | OS | Path | Example |
> | ------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
> | Linux | `$XDG_DATA_HOME/autojump/autojump.txt` or `$HOME/.local/share/autojump/autojump.txt` | `/home/alice/.local/share/autojump/autojump.txt` |
> | macOS | `$HOME/Library/autojump/autojump.txt` | `/Users/Alice/Library/autojump/autojump.txt` |
> | Windows | `%APPDATA%\autojump\autojump.txt` | `C:\Users\Alice\AppData\Roaming\autojump\autojump.txt` |
</details>
<details>
<summary>fasd, z, z.lua, zsh-z</summary>
> Run this command in your terminal:
>
> ```sh
> zoxide import --from=z "path/to/z/db"
> ```
>
> The path usually varies according to your system:
>
> | Plugin | Path |
> | ---------------- | ----------------------------------------------------------------------------------- |
> | fasd | `$_FASD_DATA` or `$HOME/.fasd` |
> | z (bash/zsh) | `$_Z_DATA` or `$HOME/.z` |
> | z (fish) | `$Z_DATA` or `$XDG_DATA_HOME/z/data` or `$HOME/.local/share/z/data` |
> | z.lua (bash/zsh) | `$_ZL_DATA` or `$HOME/.zlua` |
> | z.lua (fish) | `$XDG_DATA_HOME/zlua/zlua.txt` or `$HOME/.local/share/zlua/zlua.txt` or `$_ZL_DATA` |
> | zsh-z | `$ZSHZ_DATA` or `$_Z_DATA` or `$HOME/.z` |
</details>
<details>
<summary>ZLocation</summary>
> Run this command in PowerShell:
>
> ```powershell
> $db = New-TemporaryFile
> (Get-ZLocation).GetEnumerator() | ForEach-Object { Write-Output ($_.Name+'|'+$_.Value+'|0') } | Out-File $db
> zoxide import --from=z $db
> ```
</details>
## Configuration ## Configuration
@ -293,8 +367,8 @@ zoxide import --from z path/to/db
When calling `zoxide init`, the following flags are available: When calling `zoxide init`, the following flags are available:
- `--cmd` - `--cmd`
- Changes the prefix of predefined aliases (`z`, `zi`). - Changes the prefix of the `z` and `zi` commands.
- `--cmd j` would change the aliases to (`j`, `ji`). - `--cmd j` would change the commands to (`j`, `ji`).
- `--cmd cd` would replace the `cd` command (doesn't work on Nushell / POSIX shells). - `--cmd cd` would replace the `cd` command (doesn't work on Nushell / POSIX shells).
- `--hook <HOOK>` - `--hook <HOOK>`
- Changes how often zoxide increments a directory's score: - Changes how often zoxide increments a directory's score:
@ -303,15 +377,15 @@ When calling `zoxide init`, the following flags are available:
| `none` | Never | | `none` | Never |
| `prompt` | At every shell prompt | | `prompt` | At every shell prompt |
| `pwd` | Whenever the directory is changed | | `pwd` | Whenever the directory is changed |
- `--no-aliases` - `--no-cmd`
- Don't define aliases (`z`, `zi`). - Prevents zoxide from defining the `z` and `zi` commands.
- These functions will still be available in your shell as `__zoxide_z` and - These functions will still be available in your shell as `__zoxide_z` and
`__zoxide_zi`, should you choose to redefine them. `__zoxide_zi`, should you choose to redefine them.
### Environment variables ### Environment variables
Environment variables<sup>[?][wiki-env]</sup> can be used for configuration. Environment variables[^2] can be used for configuration. They must be set before
They must be set before `zoxide init` is called. `zoxide init` is called.
- `_ZO_DATA_DIR` - `_ZO_DATA_DIR`
- Specifies the directory in which the database is stored. - Specifies the directory in which the database is stored.
@ -346,38 +420,55 @@ They must be set before `zoxide init` is called.
## Third-party integrations ## Third-party integrations
| Application | Description | Plugin | | Application | Description | Plugin |
| ------------------ | -------------------------------------------- | -------------------------- | | --------------------- | -------------------------------------------- | -------------------------- |
| [clink] | Improved cmd.exe for Windows | [clink-zoxide] | | [aerc] | Email client | Natively supported |
| [emacs] | Text editor | [zoxide.el] | | [clink] | Improved cmd.exe for Windows | [clink-zoxide] |
| [nnn] | File manager | [nnn-autojump] | | [emacs] | Text editor | [zoxide.el] |
| [ranger] | File manager | [ranger-zoxide] | | [felix] | File manager | Natively supported |
| [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] | | [joshuto] | File manager | Natively supported |
| [vim] | Text editor | [zoxide.vim] | | [lf] | File manager | See the [wiki][lf-wiki] |
| [xplr] | File manager | [zoxide.xplr] | | [nnn] | File manager | [nnn-autojump] |
| [xxh] | Transports shell configuration over SSH | [xxh-plugin-prerun-zoxide] | | [ranger] | File manager | [ranger-zoxide] |
| [zabb] | Finds the shortest possible query for a path | Natively supported | | [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] |
| [zsh-autocomplete] | Realtime completions for zsh | Natively supported | | [t] | `tmux` session manager | Natively supported |
| [tmux-session-wizard] | `tmux` session manager | Natively supported |
| [vim] / [neovim] | Text editor | [zoxide.vim] |
| [xplr] | File manager | [zoxide.xplr] |
| [xxh] | Transports shell configuration over SSH | [xxh-plugin-prerun-zoxide] |
| [zabb] | Finds the shortest possible query for a path | Natively supported |
| [zsh-autocomplete] | Realtime completions for zsh | Natively supported |
[^1]:
Debian / Ubuntu derivatives update their packages very slowly. If you're
using one of these distributions, consider using the install script instead.
[^2]:
If you're not sure how to set an environment variable on your shell, check
out the [wiki][wiki-env].
[aerc]: https://github.com/rjarry/aerc
[algorithm-aging]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#aging [algorithm-aging]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#aging
[algorithm-matching]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#matching [algorithm-matching]: https://github.com/ajeetdsouza/zoxide/wiki/Algorithm#matching
[alpine linux packages]: https://pkgs.alpinelinux.org/packages?name=zoxide [alpine linux packages]: https://pkgs.alpinelinux.org/packages?name=zoxide
[arch linux community]: https://archlinux.org/packages/community/x86_64/zoxide/ [arch linux extra]: https://archlinux.org/packages/extra/x86_64/zoxide/
[builtwithnix-badge]: https://img.shields.io/badge/builtwith-nix-7d81f7?style=flat-square [asdf]: https://github.com/asdf-vm/asdf
[builtwithnix-badge]: https://img.shields.io/badge/builtwith-nix-7d81f7?logo=nixos&logoColor=white&style=flat-square
[builtwithnix]: https://builtwithnix.org/ [builtwithnix]: https://builtwithnix.org/
[chocolatey]: https://community.chocolatey.org/packages/zoxide [chocolatey]: https://community.chocolatey.org/packages/zoxide
[clink-zoxide]: https://github.com/shunsambongi/clink-zoxide [clink-zoxide]: https://github.com/shunsambongi/clink-zoxide
[clink]: https://github.com/mridgers/clink [clink]: https://github.com/mridgers/clink
[conda-forge]: https://anaconda.org/conda-forge/zoxide [conda-forge]: https://anaconda.org/conda-forge/zoxide
[copr]: https://copr.fedorainfracloud.org/coprs/atim/zoxide/ [copr]: https://copr.fedorainfracloud.org/coprs/atim/zoxide/
[crates.io-badge]: https://img.shields.io/crates/v/zoxide?style=flat-square [crates.io-badge]: https://img.shields.io/crates/v/zoxide?logo=rust&logoColor=white&style=flat-square
[crates.io]: https://crates.io/crates/zoxide [crates.io]: https://crates.io/crates/zoxide
[debian packages]: https://packages.debian.org/stable/admin/zoxide [debian packages]: https://packages.debian.org/stable/admin/zoxide
[devuan packages]: https://pkginfo.devuan.org/cgi-bin/package-query.html?c=package&q=zoxide [devuan packages]: https://pkginfo.devuan.org/cgi-bin/package-query.html?c=package&q=zoxide
[downloads-badge]: https://img.shields.io/github/downloads/ajeetdsouza/zoxide/total?style=flat-square [downloads-badge]: https://img.shields.io/github/downloads/ajeetdsouza/zoxide/total?logo=github&logoColor=white&style=flat-square
[dports]: https://github.com/DragonFlyBSD/DPorts/tree/master/sysutils/zoxide [dports]: https://github.com/DragonFlyBSD/DPorts/tree/master/sysutils/zoxide
[emacs]: https://www.gnu.org/software/emacs/ [emacs]: https://www.gnu.org/software/emacs/
[fedora packages]: https://src.fedoraproject.org/rpms/rust-zoxide [fedora packages]: https://src.fedoraproject.org/rpms/rust-zoxide
[felix]: https://github.com/kyoheiu/felix
[freshports]: https://www.freshports.org/sysutils/zoxide/ [freshports]: https://www.freshports.org/sysutils/zoxide/
[fzf-installation]: https://github.com/junegunn/fzf#installation [fzf-installation]: https://github.com/junegunn/fzf#installation
[fzf-man]: https://manpages.ubuntu.com/manpages/en/man1/fzf.1.html [fzf-man]: https://manpages.ubuntu.com/manpages/en/man1/fzf.1.html
@ -386,23 +477,31 @@ They must be set before `zoxide init` is called.
[guru overlay]: https://github.com/gentoo-mirror/guru [guru overlay]: https://github.com/gentoo-mirror/guru
[homebrew]: https://formulae.brew.sh/formula/zoxide [homebrew]: https://formulae.brew.sh/formula/zoxide
[issues]: https://github.com/ajeetdsouza/zoxide/issues/new [issues]: https://github.com/ajeetdsouza/zoxide/issues/new
[joshuto]: https://github.com/kamiyaa/joshuto
[lf]: https://github.com/gokcehan/lf
[lf-wiki]: https://github.com/gokcehan/lf/wiki/Integrations#zoxide
[linuxbrew]: https://formulae.brew.sh/formula-linux/zoxide [linuxbrew]: https://formulae.brew.sh/formula-linux/zoxide
[macports]: https://ports.macports.org/port/zoxide/summary [macports]: https://ports.macports.org/port/zoxide/summary
[neovim]: https://github.com/neovim/neovim [neovim]: https://github.com/neovim/neovim
[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/zoxide/default.nix [nixpkgs]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/zoxide/default.nix
[nnn-autojump]: https://github.com/jarun/nnn/blob/master/plugins/autojump [nnn-autojump]: https://github.com/jarun/nnn/blob/master/plugins/autojump
[nnn]: https://github.com/jarun/nnn [nnn]: https://github.com/jarun/nnn
[opensuse factory]: https://build.opensuse.org/package/show/openSUSE:Factory/zoxide
[pacstall packages]: https://pacstall.dev/packages/zoxide-deb
[pkgsrc]: https://pkgsrc.se/sysutils/zoxide [pkgsrc]: https://pkgsrc.se/sysutils/zoxide
[ranger-zoxide]: https://github.com/jchook/ranger-zoxide [ranger-zoxide]: https://github.com/jchook/ranger-zoxide
[ranger]: https://github.com/ranger/ranger [ranger]: https://github.com/ranger/ranger
[raspbian packages]: https://archive.raspbian.org/raspbian/pool/main/r/rust-zoxide/ [raspbian packages]: https://archive.raspbian.org/raspbian/pool/main/r/rust-zoxide/
[releases]: https://github.com/ajeetdsouza/zoxide/releases [releases]: https://github.com/ajeetdsouza/zoxide/releases
[scoop]: https://github.com/ScoopInstaller/Main/tree/master/bucket/zoxide.json [scoop]: https://github.com/ScoopInstaller/Main/tree/master/bucket/zoxide.json
[slackbuilds-howto]: https://slackbuilds.org/howto/
[t]: https://github.com/joshmedeski/t-smart-tmux-session-manager
[telescope-zoxide]: https://github.com/jvgrootveld/telescope-zoxide [telescope-zoxide]: https://github.com/jvgrootveld/telescope-zoxide
[telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim [telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim
[termux]: https://github.com/termux/termux-packages/tree/master/packages/zoxide [termux]: https://github.com/termux/termux-packages/tree/master/packages/zoxide
[tmux-session-wizard]: https://github.com/27medkamal/tmux-session-wizard
[tutorial]: contrib/tutorial.webp [tutorial]: contrib/tutorial.webp
[ubuntu packages]: https://packages.ubuntu.com/hirsute/zoxide [ubuntu packages]: https://packages.ubuntu.com/jammy/zoxide
[vim]: https://github.com/vim/vim [vim]: https://github.com/vim/vim
[void linux packages]: https://github.com/void-linux/void-packages/tree/master/srcpkgs/zoxide [void linux packages]: https://github.com/void-linux/void-packages/tree/master/srcpkgs/zoxide
[wiki-env]: https://github.com/ajeetdsouza/zoxide/wiki/HOWTO:-set-environment-variables "HOWTO: set environment variables" [wiki-env]: https://github.com/ajeetdsouza/zoxide/wiki/HOWTO:-set-environment-variables "HOWTO: set environment variables"

View File

@ -1,56 +1,34 @@
use std::process::Command; #[path = "src/cmd/cmd.rs"]
mod cmd;
use std::{env, io}; use std::{env, io};
fn main() { use clap::CommandFactory;
let pkg_version = env!("CARGO_PKG_VERSION"); use clap_complete::shells::{Bash, Elvish, Fish, PowerShell, Zsh};
let version = match env::var_os("PROFILE") { use clap_complete_fig::Fig;
Some(profile) if profile == "release" => format!("v{}", pkg_version), use cmd::Cmd;
_ => git_version().unwrap_or_else(|| format!("v{}-unknown", pkg_version)),
};
println!("cargo:rustc-env=ZOXIDE_VERSION={}", version);
// Since we are generating completions in the package directory, we need to set this so that fn main() -> io::Result<()> {
// Cargo doesn't rebuild every time. // Since we are generating completions in the package directory, we need to
// set this so that Cargo doesn't rebuild every time.
println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src"); println!("cargo:rerun-if-changed=src/");
println!("cargo:rerun-if-changed=templates"); println!("cargo:rerun-if-changed=templates/");
println!("cargo:rerun-if-changed=tests"); println!("cargo:rerun-if-changed=tests/");
generate_completions()
generate_completions().unwrap();
}
fn git_version() -> Option<String> {
let dir = env!("CARGO_MANIFEST_DIR");
let mut git = Command::new("git");
git.args(&["-C", dir, "describe", "--tags", "--match=v*.*.*", "--always", "--broken"]);
let output = git.output().ok()?;
if !output.status.success() || output.stdout.is_empty() || !output.stderr.is_empty() {
return None;
}
String::from_utf8(output.stdout).ok()
} }
fn generate_completions() -> io::Result<()> { fn generate_completions() -> io::Result<()> {
#[path = "src/cmd/_cmd.rs"] const BIN_NAME: &str = env!("CARGO_PKG_NAME");
mod cmd; const OUT_DIR: &str = "contrib/completions";
use clap::CommandFactory;
use clap_complete::generate_to;
use clap_complete::shells::{Bash, Elvish, Fish, PowerShell, Zsh};
use clap_complete_fig::Fig;
use cmd::Cmd;
let cmd = &mut Cmd::command(); let cmd = &mut Cmd::command();
let bin_name = env!("CARGO_PKG_NAME");
let out_dir = "contrib/completions";
generate_to(Bash, cmd, bin_name, out_dir)?; clap_complete::generate_to(Bash, cmd, BIN_NAME, OUT_DIR)?;
generate_to(Elvish, cmd, bin_name, out_dir)?; clap_complete::generate_to(Elvish, cmd, BIN_NAME, OUT_DIR)?;
generate_to(Fig, cmd, bin_name, out_dir)?; clap_complete::generate_to(Fig, cmd, BIN_NAME, OUT_DIR)?;
generate_to(Fish, cmd, bin_name, out_dir)?; clap_complete::generate_to(Fish, cmd, BIN_NAME, OUT_DIR)?;
generate_to(PowerShell, cmd, bin_name, out_dir)?; clap_complete::generate_to(PowerShell, cmd, BIN_NAME, OUT_DIR)?;
generate_to(Zsh, cmd, bin_name, out_dir)?; clap_complete::generate_to(Zsh, cmd, BIN_NAME, OUT_DIR)?;
Ok(()) Ok(())
} }

View File

@ -15,10 +15,10 @@ _zoxide() {
local context curcontext="$curcontext" state line local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'-h[Print help information]' \ '-h[Print help]' \
'--help[Print help information]' \ '--help[Print help]' \
'-V[Print version information]' \ '-V[Print version]' \
'--version[Print version information]' \ '--version[Print version]' \
":: :_zoxide_commands" \ ":: :_zoxide_commands" \
"*::: :->zoxide" \ "*::: :->zoxide" \
&& ret=0 && ret=0
@ -30,61 +30,115 @@ _zoxide() {
case $line[1] in case $line[1] in
(add) (add)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'-h[Print help information]' \ '-h[Print help]' \
'--help[Print help information]' \ '--help[Print help]' \
'-V[Print version information]' \ '-V[Print version]' \
'--version[Print version information]' \ '--version[Print version]' \
'*::paths:_files -/' \ '*::paths:_files -/' \
&& ret=0 && ret=0
;; ;;
(edit)
_arguments "${_arguments_options[@]}" \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
":: :_zoxide__edit_commands" \
"*::: :->edit" \
&& ret=0
case $state in
(edit)
words=($line[1] "${words[@]}")
(( CURRENT += 1 ))
curcontext="${curcontext%:*:*}:zoxide-edit-command-$line[1]:"
case $line[1] in
(decrement)
_arguments "${_arguments_options[@]}" \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
':path:' \
&& ret=0
;;
(delete)
_arguments "${_arguments_options[@]}" \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
':path:' \
&& ret=0
;;
(increment)
_arguments "${_arguments_options[@]}" \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
':path:' \
&& ret=0
;;
(reload)
_arguments "${_arguments_options[@]}" \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0
;;
esac
;;
esac
;;
(import) (import)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'--from=[Application to import from]:FROM:(autojump z)' \ '--from=[Application to import from]:FROM:(autojump z)' \
'--merge[Merge into existing database]' \ '--merge[Merge into existing database]' \
'-h[Print help information]' \ '-h[Print help]' \
'--help[Print help information]' \ '--help[Print help]' \
'-V[Print version information]' \ '-V[Print version]' \
'--version[Print version information]' \ '--version[Print version]' \
':path:_files' \ ':path:_files' \
&& ret=0 && ret=0
;; ;;
(init) (init)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'--cmd=[Renames the '\''z'\'' command and corresponding aliases]:CMD: ' \ '--cmd=[Changes the prefix of the \`z\` and \`zi\` commands]:CMD: ' \
'--hook=[Chooses event upon which an entry is added to the database]:HOOK:(none prompt pwd)' \ '--hook=[Changes how often zoxide increments a directory'\''s score]:HOOK:(none prompt pwd)' \
'--no-aliases[Prevents zoxide from defining any commands]' \ '--no-cmd[Prevents zoxide from defining the \`z\` and \`zi\` commands]' \
'-h[Print help information]' \ '-h[Print help]' \
'--help[Print help information]' \ '--help[Print help]' \
'-V[Print version information]' \ '-V[Print version]' \
'--version[Print version information]' \ '--version[Print version]' \
':shell:(bash elvish fish nushell posix powershell xonsh zsh)' \ ':shell:(bash elvish fish nushell posix powershell xonsh zsh)' \
&& ret=0 && ret=0
;; ;;
(query) (query)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'--exclude=[Exclude a path from results]:path:_files -/' \ '--exclude=[Exclude the current directory]:path:_files -/' \
'--all[Show deleted directories]' \ '-a[Show unavailable directories]' \
'--all[Show unavailable directories]' \
'(-l --list)-i[Use interactive selection]' \ '(-l --list)-i[Use interactive selection]' \
'(-l --list)--interactive[Use interactive selection]' \ '(-l --list)--interactive[Use interactive selection]' \
'(-i --interactive)-l[List all matching directories]' \ '(-i --interactive)-l[List all matching directories]' \
'(-i --interactive)--list[List all matching directories]' \ '(-i --interactive)--list[List all matching directories]' \
'(-i --interactive)-s[Print score with results]' \ '-s[Print score with results]' \
'(-i --interactive)--score[Print score with results]' \ '--score[Print score with results]' \
'-h[Print help information]' \ '-h[Print help]' \
'--help[Print help information]' \ '--help[Print help]' \
'-V[Print version information]' \ '-V[Print version]' \
'--version[Print version information]' \ '--version[Print version]' \
'*::keywords:' \ '*::keywords:' \
&& ret=0 && ret=0
;; ;;
(remove) (remove)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'-i[Use interactive selection]' \ '-h[Print help]' \
'--interactive[Use interactive selection]' \ '--help[Print help]' \
'-h[Print help information]' \ '-V[Print version]' \
'--help[Print help information]' \ '--version[Print version]' \
'-V[Print version information]' \
'--version[Print version information]' \
'*::paths:_files -/' \ '*::paths:_files -/' \
&& ret=0 && ret=0
;; ;;
@ -97,6 +151,7 @@ esac
_zoxide_commands() { _zoxide_commands() {
local commands; commands=( local commands; commands=(
'add:Add a new directory or increment its rank' \ 'add:Add a new directory or increment its rank' \
'edit:Edit the database' \
'import:Import entries from another application' \ 'import:Import entries from another application' \
'init:Generate shell configuration' \ 'init:Generate shell configuration' \
'query:Search for a directory in the database' \ 'query:Search for a directory in the database' \
@ -109,11 +164,36 @@ _zoxide__add_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide add commands' commands "$@" _describe -t commands 'zoxide add commands' commands "$@"
} }
(( $+functions[_zoxide__edit__decrement_commands] )) ||
_zoxide__edit__decrement_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit decrement commands' commands "$@"
}
(( $+functions[_zoxide__edit__delete_commands] )) ||
_zoxide__edit__delete_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit delete commands' commands "$@"
}
(( $+functions[_zoxide__edit_commands] )) ||
_zoxide__edit_commands() {
local commands; commands=(
'decrement:' \
'delete:' \
'increment:' \
'reload:' \
)
_describe -t commands 'zoxide edit commands' commands "$@"
}
(( $+functions[_zoxide__import_commands] )) || (( $+functions[_zoxide__import_commands] )) ||
_zoxide__import_commands() { _zoxide__import_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide import commands' commands "$@" _describe -t commands 'zoxide import commands' commands "$@"
} }
(( $+functions[_zoxide__edit__increment_commands] )) ||
_zoxide__edit__increment_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit increment commands' commands "$@"
}
(( $+functions[_zoxide__init_commands] )) || (( $+functions[_zoxide__init_commands] )) ||
_zoxide__init_commands() { _zoxide__init_commands() {
local commands; commands=() local commands; commands=()
@ -124,10 +204,19 @@ _zoxide__query_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide query commands' commands "$@" _describe -t commands 'zoxide query commands' commands "$@"
} }
(( $+functions[_zoxide__edit__reload_commands] )) ||
_zoxide__edit__reload_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit reload commands' commands "$@"
}
(( $+functions[_zoxide__remove_commands] )) || (( $+functions[_zoxide__remove_commands] )) ||
_zoxide__remove_commands() { _zoxide__remove_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide remove commands' commands "$@" _describe -t commands 'zoxide remove commands' commands "$@"
} }
_zoxide "$@" if [ "$funcstack[1]" = "_zoxide" ]; then
_zoxide "$@"
else
compdef _zoxide zoxide
fi

View File

@ -21,11 +21,12 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
$completions = @(switch ($command) { $completions = @(switch ($command) {
'zoxide' { 'zoxide' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank')
[CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database')
[CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application')
[CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration')
[CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database')
@ -33,53 +34,91 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
break break
} }
'zoxide;add' { 'zoxide;add' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
'zoxide;edit' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('decrement', 'decrement', [CompletionResultType]::ParameterValue, 'decrement')
[CompletionResult]::new('delete', 'delete', [CompletionResultType]::ParameterValue, 'delete')
[CompletionResult]::new('increment', 'increment', [CompletionResultType]::ParameterValue, 'increment')
[CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'reload')
break
}
'zoxide;edit;decrement' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
'zoxide;edit;delete' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
'zoxide;edit;increment' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
'zoxide;edit;reload' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break break
} }
'zoxide;import' { 'zoxide;import' {
[CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from') [CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from')
[CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database') [CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break break
} }
'zoxide;init' { 'zoxide;init' {
[CompletionResult]::new('--cmd', 'cmd', [CompletionResultType]::ParameterName, 'Renames the ''z'' command and corresponding aliases') [CompletionResult]::new('--cmd', 'cmd', [CompletionResultType]::ParameterName, 'Changes the prefix of the `z` and `zi` commands')
[CompletionResult]::new('--hook', 'hook', [CompletionResultType]::ParameterName, 'Chooses event upon which an entry is added to the database') [CompletionResult]::new('--hook', 'hook', [CompletionResultType]::ParameterName, 'Changes how often zoxide increments a directory''s score')
[CompletionResult]::new('--no-aliases', 'no-aliases', [CompletionResultType]::ParameterName, 'Prevents zoxide from defining any commands') [CompletionResult]::new('--no-cmd', 'no-cmd', [CompletionResultType]::ParameterName, 'Prevents zoxide from defining the `z` and `zi` commands')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break break
} }
'zoxide;query' { 'zoxide;query' {
[CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude a path from results') [CompletionResult]::new('--exclude', 'exclude', [CompletionResultType]::ParameterName, 'Exclude the current directory')
[CompletionResult]::new('--all', 'all', [CompletionResultType]::ParameterName, 'Show deleted directories') [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Show unavailable directories')
[CompletionResult]::new('--all', 'all', [CompletionResultType]::ParameterName, 'Show unavailable directories')
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection')
[CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection')
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List all matching directories')
[CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List all matching directories') [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List all matching directories')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print score with results') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print score with results')
[CompletionResult]::new('--score', 'score', [CompletionResultType]::ParameterName, 'Print score with results') [CompletionResult]::new('--score', 'score', [CompletionResultType]::ParameterName, 'Print score with results')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break break
} }
'zoxide;remove' { 'zoxide;remove' {
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break break
} }
}) })

View File

@ -1,5 +1,5 @@
_zoxide() { _zoxide() {
local i cur prev opts cmds local i cur prev opts cmd
COMPREPLY=() COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
@ -8,24 +8,39 @@ _zoxide() {
for i in ${COMP_WORDS[@]} for i in ${COMP_WORDS[@]}
do do
case "${i}" in case "${cmd},${i}" in
"$1") ",$1")
cmd="zoxide" cmd="zoxide"
;; ;;
add) zoxide,add)
cmd+="__add" cmd="zoxide__add"
;; ;;
import) zoxide,edit)
cmd+="__import" cmd="zoxide__edit"
;; ;;
init) zoxide,import)
cmd+="__init" cmd="zoxide__import"
;; ;;
query) zoxide,init)
cmd+="__query" cmd="zoxide__init"
;; ;;
remove) zoxide,query)
cmd+="__remove" cmd="zoxide__query"
;;
zoxide,remove)
cmd="zoxide__remove"
;;
zoxide__edit,decrement)
cmd="zoxide__edit__decrement"
;;
zoxide__edit,delete)
cmd="zoxide__edit__delete"
;;
zoxide__edit,increment)
cmd="zoxide__edit__increment"
;;
zoxide__edit,reload)
cmd="zoxide__edit__reload"
;; ;;
*) *)
;; ;;
@ -34,7 +49,7 @@ _zoxide() {
case "${cmd}" in case "${cmd}" in
zoxide) zoxide)
opts="-h -V --help --version add import init query remove" opts="-h -V --help --version add edit import init query remove"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -61,6 +76,76 @@ _zoxide() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
zoxide__edit)
opts="-h -V --help --version decrement delete increment reload"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__decrement)
opts="-h -V --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__delete)
opts="-h -V --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__increment)
opts="-h -V --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__reload)
opts="-h -V --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__import) zoxide__import)
opts="-h -V --from --merge --help --version <PATH>" opts="-h -V --from --merge --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
@ -69,7 +154,7 @@ _zoxide() {
fi fi
case "${prev}" in case "${prev}" in
--from) --from)
COMPREPLY=($(compgen -W "" -- "${cur}")) COMPREPLY=($(compgen -W "autojump z" -- "${cur}"))
return 0 return 0
;; ;;
*) *)
@ -80,7 +165,7 @@ _zoxide() {
return 0 return 0
;; ;;
zoxide__init) zoxide__init)
opts="-h -V --no-aliases --cmd --hook --help --version bash elvish fish nushell posix powershell xonsh zsh" opts="-h -V --no-cmd --cmd --hook --help --version bash elvish fish nushell posix powershell xonsh zsh"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -91,7 +176,7 @@ _zoxide() {
return 0 return 0
;; ;;
--hook) --hook)
COMPREPLY=($(compgen -W "" -- "${cur}")) COMPREPLY=($(compgen -W "none prompt pwd" -- "${cur}"))
return 0 return 0
;; ;;
*) *)
@ -102,7 +187,7 @@ _zoxide() {
return 0 return 0
;; ;;
zoxide__query) zoxide__query)
opts="-i -l -s -h -V --all --interactive --list --score --exclude --help --version <KEYWORDS>..." opts="-a -i -l -s -h -V --all --interactive --list --score --exclude --help --version [KEYWORDS]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -120,7 +205,7 @@ _zoxide() {
return 0 return 0
;; ;;
zoxide__remove) zoxide__remove)
opts="-i -h -V --interactive --help --version <PATHS>..." opts="-h -V --help --version [PATHS]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@ -18,60 +18,94 @@ set edit:completion:arg-completer[zoxide] = {|@words|
} }
var completions = [ var completions = [
&'zoxide'= { &'zoxide'= {
cand -h 'Print help information' cand -h 'Print help'
cand --help 'Print help information' cand --help 'Print help'
cand -V 'Print version information' cand -V 'Print version'
cand --version 'Print version information' cand --version 'Print version'
cand add 'Add a new directory or increment its rank' cand add 'Add a new directory or increment its rank'
cand edit 'Edit the database'
cand import 'Import entries from another application' cand import 'Import entries from another application'
cand init 'Generate shell configuration' cand init 'Generate shell configuration'
cand query 'Search for a directory in the database' cand query 'Search for a directory in the database'
cand remove 'Remove a directory from the database' cand remove 'Remove a directory from the database'
} }
&'zoxide;add'= { &'zoxide;add'= {
cand -h 'Print help information' cand -h 'Print help'
cand --help 'Print help information' cand --help 'Print help'
cand -V 'Print version information' cand -V 'Print version'
cand --version 'Print version information' cand --version 'Print version'
}
&'zoxide;edit'= {
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
cand decrement 'decrement'
cand delete 'delete'
cand increment 'increment'
cand reload 'reload'
}
&'zoxide;edit;decrement'= {
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
}
&'zoxide;edit;delete'= {
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
}
&'zoxide;edit;increment'= {
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
}
&'zoxide;edit;reload'= {
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
} }
&'zoxide;import'= { &'zoxide;import'= {
cand --from 'Application to import from' cand --from 'Application to import from'
cand --merge 'Merge into existing database' cand --merge 'Merge into existing database'
cand -h 'Print help information' cand -h 'Print help'
cand --help 'Print help information' cand --help 'Print help'
cand -V 'Print version information' cand -V 'Print version'
cand --version 'Print version information' cand --version 'Print version'
} }
&'zoxide;init'= { &'zoxide;init'= {
cand --cmd 'Renames the ''z'' command and corresponding aliases' cand --cmd 'Changes the prefix of the `z` and `zi` commands'
cand --hook 'Chooses event upon which an entry is added to the database' cand --hook 'Changes how often zoxide increments a directory''s score'
cand --no-aliases 'Prevents zoxide from defining any commands' cand --no-cmd 'Prevents zoxide from defining the `z` and `zi` commands'
cand -h 'Print help information' cand -h 'Print help'
cand --help 'Print help information' cand --help 'Print help'
cand -V 'Print version information' cand -V 'Print version'
cand --version 'Print version information' cand --version 'Print version'
} }
&'zoxide;query'= { &'zoxide;query'= {
cand --exclude 'Exclude a path from results' cand --exclude 'Exclude the current directory'
cand --all 'Show deleted directories' cand -a 'Show unavailable directories'
cand --all 'Show unavailable directories'
cand -i 'Use interactive selection' cand -i 'Use interactive selection'
cand --interactive 'Use interactive selection' cand --interactive 'Use interactive selection'
cand -l 'List all matching directories' cand -l 'List all matching directories'
cand --list 'List all matching directories' cand --list 'List all matching directories'
cand -s 'Print score with results' cand -s 'Print score with results'
cand --score 'Print score with results' cand --score 'Print score with results'
cand -h 'Print help information' cand -h 'Print help'
cand --help 'Print help information' cand --help 'Print help'
cand -V 'Print version information' cand -V 'Print version'
cand --version 'Print version information' cand --version 'Print version'
} }
&'zoxide;remove'= { &'zoxide;remove'= {
cand -i 'Use interactive selection' cand -h 'Print help'
cand --interactive 'Use interactive selection' cand --help 'Print help'
cand -h 'Print help information' cand -V 'Print version'
cand --help 'Print help information' cand --version 'Print version'
cand -V 'Print version information'
cand --version 'Print version information'
} }
] ]
$completions[$command] $completions[$command]

View File

@ -1,28 +1,42 @@
complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank' complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank'
complete -c zoxide -n "__fish_use_subcommand" -f -a "edit" -d 'Edit the database'
complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application' complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application'
complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration'
complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database'
complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database' complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database'
complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "decrement"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "delete"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "increment"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "reload"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }" complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }"
complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database' complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database'
complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from import" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from import" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from init" -l cmd -d 'Renames the \'z\' command and corresponding aliases' -r complete -c zoxide -n "__fish_seen_subcommand_from init" -l cmd -d 'Changes the prefix of the `z` and `zi` commands' -r
complete -c zoxide -n "__fish_seen_subcommand_from init" -l hook -d 'Chooses event upon which an entry is added to the database' -r -f -a "{none ,prompt ,pwd }" complete -c zoxide -n "__fish_seen_subcommand_from init" -l hook -d 'Changes how often zoxide increments a directory\'s score' -r -f -a "{none ,prompt ,pwd }"
complete -c zoxide -n "__fish_seen_subcommand_from init" -l no-aliases -d 'Prevents zoxide from defining any commands' complete -c zoxide -n "__fish_seen_subcommand_from init" -l no-cmd -d 'Prevents zoxide from defining the `z` and `zi` commands'
complete -c zoxide -n "__fish_seen_subcommand_from init" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from init" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from init" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from init" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from query" -l exclude -d 'Exclude a path from results' -r -f -a "(__fish_complete_directories)" complete -c zoxide -n "__fish_seen_subcommand_from query" -l exclude -d 'Exclude the current directory' -r -f -a "(__fish_complete_directories)"
complete -c zoxide -n "__fish_seen_subcommand_from query" -l all -d 'Show deleted directories' complete -c zoxide -n "__fish_seen_subcommand_from query" -s a -l all -d 'Show unavailable directories'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_seen_subcommand_from query" -s i -l interactive -d 'Use interactive selection'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s l -l list -d 'List all matching directories' complete -c zoxide -n "__fish_seen_subcommand_from query" -s l -l list -d 'List all matching directories'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results' complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s i -l interactive -d 'Use interactive selection' complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version information'

View File

@ -8,11 +8,11 @@ const completion: Fig.Spec = {
options: [ options: [
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help",
}, },
{ {
name: ["-V", "--version"], name: ["-V", "--version"],
description: "Print version information", description: "Print version",
}, },
], ],
args: { args: {
@ -21,6 +21,87 @@ const completion: Fig.Spec = {
template: "folders", template: "folders",
}, },
}, },
{
name: "edit",
description: "Edit the database",
subcommands: [
{
name: "decrement",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help",
},
{
name: ["-V", "--version"],
description: "Print version",
},
],
args: {
name: "path",
},
},
{
name: "delete",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help",
},
{
name: ["-V", "--version"],
description: "Print version",
},
],
args: {
name: "path",
},
},
{
name: "increment",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help",
},
{
name: ["-V", "--version"],
description: "Print version",
},
],
args: {
name: "path",
},
},
{
name: "reload",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help",
},
{
name: ["-V", "--version"],
description: "Print version",
},
],
},
],
options: [
{
name: ["-h", "--help"],
description: "Print help",
},
{
name: ["-V", "--version"],
description: "Print version",
},
],
},
{ {
name: "import", name: "import",
description: "Import entries from another application", description: "Import entries from another application",
@ -28,16 +109,13 @@ const completion: Fig.Spec = {
{ {
name: "--from", name: "--from",
description: "Application to import from", description: "Application to import from",
isRepeatable: true,
args: { args: {
name: "from", name: "from",
suggestions: [ suggestions: [
{ "autojump",
name: "autojump", "z",
}, ],
{
name: "z",
},
]
}, },
}, },
{ {
@ -46,11 +124,11 @@ const completion: Fig.Spec = {
}, },
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help",
}, },
{ {
name: ["-V", "--version"], name: ["-V", "--version"],
description: "Print version information", description: "Print version",
}, },
], ],
args: { args: {
@ -64,7 +142,8 @@ const completion: Fig.Spec = {
options: [ options: [
{ {
name: "--cmd", name: "--cmd",
description: "Renames the 'z' command and corresponding aliases", description: "Changes the prefix of the `z` and `zi` commands",
isRepeatable: true,
args: { args: {
name: "cmd", name: "cmd",
isOptional: true, isOptional: true,
@ -72,64 +151,43 @@ const completion: Fig.Spec = {
}, },
{ {
name: "--hook", name: "--hook",
description: "Chooses event upon which an entry is added to the database", description: "Changes how often zoxide increments a directory's score",
isRepeatable: true,
args: { args: {
name: "hook", name: "hook",
isOptional: true, isOptional: true,
suggestions: [ suggestions: [
{ "none",
name: "none", "prompt",
}, "pwd",
{ ],
name: "prompt",
},
{
name: "pwd",
},
]
}, },
}, },
{ {
name: "--no-aliases", name: "--no-cmd",
description: "Prevents zoxide from defining any commands", description: "Prevents zoxide from defining the `z` and `zi` commands",
}, },
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help",
}, },
{ {
name: ["-V", "--version"], name: ["-V", "--version"],
description: "Print version information", description: "Print version",
}, },
], ],
args: { args: {
name: "shell", name: "shell",
suggestions: [ suggestions: [
{ "bash",
name: "bash", "elvish",
}, "fish",
{ "nushell",
name: "elvish", "posix",
}, "powershell",
{ "xonsh",
name: "fish", "zsh",
}, ],
{
name: "nushell",
},
{
name: "posix",
},
{
name: "powershell",
},
{
name: "xonsh",
},
{
name: "zsh",
},
]
}, },
}, },
{ {
@ -138,7 +196,8 @@ const completion: Fig.Spec = {
options: [ options: [
{ {
name: "--exclude", name: "--exclude",
description: "Exclude a path from results", description: "Exclude the current directory",
isRepeatable: true,
args: { args: {
name: "exclude", name: "exclude",
isOptional: true, isOptional: true,
@ -146,16 +205,24 @@ const completion: Fig.Spec = {
}, },
}, },
{ {
name: "--all", name: ["-a", "--all"],
description: "Show deleted directories", description: "Show unavailable directories",
}, },
{ {
name: ["-i", "--interactive"], name: ["-i", "--interactive"],
description: "Use interactive selection", description: "Use interactive selection",
exclusiveOn: [
"-l",
"--list",
],
}, },
{ {
name: ["-l", "--list"], name: ["-l", "--list"],
description: "List all matching directories", description: "List all matching directories",
exclusiveOn: [
"-i",
"--interactive",
],
}, },
{ {
name: ["-s", "--score"], name: ["-s", "--score"],
@ -163,15 +230,16 @@ const completion: Fig.Spec = {
}, },
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help",
}, },
{ {
name: ["-V", "--version"], name: ["-V", "--version"],
description: "Print version information", description: "Print version",
}, },
], ],
args: { args: {
name: "keywords", name: "keywords",
isVariadic: true,
isOptional: true, isOptional: true,
}, },
}, },
@ -179,21 +247,18 @@ const completion: Fig.Spec = {
name: "remove", name: "remove",
description: "Remove a directory from the database", description: "Remove a directory from the database",
options: [ options: [
{
name: ["-i", "--interactive"],
description: "Use interactive selection",
},
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help",
}, },
{ {
name: ["-V", "--version"], name: ["-V", "--version"],
description: "Print version information", description: "Print version",
}, },
], ],
args: { args: {
name: "paths", name: "paths",
isVariadic: true,
isOptional: true, isOptional: true,
template: "folders", template: "folders",
}, },
@ -202,11 +267,11 @@ const completion: Fig.Spec = {
options: [ options: [
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help",
}, },
{ {
name: ["-V", "--version"], name: ["-V", "--version"],
description: "Print version information", description: "Print version",
}, },
], ],
}; };

BIN
contrib/warp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

391
install.sh Executable file
View File

@ -0,0 +1,391 @@
#!/bin/sh
# shellcheck shell=dash
# The official zoxide installer.
#
# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local`
# extension. Note: Most shells limit `local` to 1 var per line, contra bash.
main() {
if [ "${KSH_VERSION-}" = 'Version JM 93t+ 2010-03-05' ]; then
# The version of ksh93 that ships with many illumos systems does not
# support the "local" extension. Print a message rather than fail in
# subtle ways later on:
err 'the installer does not work with this ksh93 version; please try bash'
fi
set -u
# Detect and print host target triple.
ensure get_architecture
local _arch="${RETVAL}"
assert_nz "${_arch}" "arch"
echo "Detected architecture: ${_arch}"
# Create and enter a temporary directory.
local _tmp_dir
_tmp_dir="$(mktemp -d)" || err "mktemp: could not create temporary directory"
cd "${_tmp_dir}" || err "cd: failed to enter directory: ${_tmp_dir}"
# Download and extract zoxide.
ensure download_zoxide "${_arch}"
local _package="${RETVAL}"
assert_nz "${_package}" "package"
echo "Downloaded package: ${_package}"
case "${_package}" in
*.tar.gz)
need_cmd tar
ensure tar -xf "${_package}"
;;
*.zip)
need_cmd unzip
ensure unzip -oq "${_package}"
;;
*)
err "unsupported package format: ${_package}"
;;
esac
# Install binary.
local _bin_dir="${HOME}/.local/bin"
local _bin_name
case "${_arch}" in
*windows*) _bin_name="zoxide.exe" ;;
*) _bin_name="zoxide" ;;
esac
ensure mkdir -p "${_bin_dir}"
ensure cp "${_bin_name}" "${_bin_dir}"
ensure chmod +x "${_bin_dir}/${_bin_name}"
echo "Installed zoxide to ${_bin_dir}"
# Install manpages.
local _man_dir="${HOME}/.local/share/man"
ensure mkdir -p "${_man_dir}/man1"
ensure cp "man/man1/"* "${_man_dir}/man1/"
echo "Installed manpages to ${_man_dir}"
# Print success message and check $PATH.
echo ""
echo "zoxide is installed!"
if ! echo ":${PATH}:" | grep -Fq ":${_bin_dir}:"; then
echo "NOTE: ${_bin_dir} is not on your \$PATH. zoxide will not work unless it is added to \$PATH."
fi
}
download_zoxide() {
local _arch="$1"
if check_cmd curl; then
_dld=curl
elif check_cmd wget; then
_dld=wget
else
need_cmd 'curl or wget'
fi
need_cmd grep
local _releases_url="https://api.github.com/repos/ajeetdsouza/zoxide/releases/latest"
local _releases
case "${_dld}" in
curl) _releases="$(curl -sL "${_releases_url}")" ||
err "curl: failed to download ${_releases_url}" ;;
wget) _releases="$(wget -qO- "${_releases_url}")" ||
err "wget: failed to download ${_releases_url}" ;;
*) err "unsupported downloader: ${_dld}" ;;
esac
(echo "${_releases}" | grep -q 'API rate limit exceeded') &&
err "you have exceeded GitHub's API rate limit. Please try again later, or use a different installation method: https://github.com/ajeetdsouza/zoxide/#installation"
local _package_url
_package_url="$(echo "${_releases}" | grep "browser_download_url" | cut -d '"' -f 4 | grep "${_arch}")" ||
err "zoxide has not yet been packaged for your architecture (${_arch}), please file an issue: https://github.com/ajeetdsouza/zoxide/issues"
local _ext
case "${_package_url}" in
*.tar.gz) _ext="tar.gz" ;;
*.zip) _ext="zip" ;;
*) err "unsupported package format: ${_package_url}" ;;
esac
local _package="zoxide.${_ext}"
case "${_dld}" in
curl) _releases="$(curl -sLo "${_package}" "${_package_url}")" || err "curl: failed to download ${_package_url}" ;;
wget) _releases="$(wget -qO "${_package}" "${_package_url}")" || err "wget: failed to download ${_package_url}" ;;
*) err "unsupported downloader: ${_dld}" ;;
esac
RETVAL="${_package}"
}
# The below functions have been extracted with minor modifications from the
# Rustup install script:
#
# https://github.com/rust-lang/rustup/blob/4c1289b2c3f3702783900934a38d7c5f912af787/rustup-init.sh
get_architecture() {
local _ostype _cputype _bitness _arch _clibtype
_ostype="$(uname -s)"
_cputype="$(uname -m)"
_clibtype="musl"
if [ "${_ostype}" = Linux ]; then
if [ "$(uname -o || true)" = Android ]; then
_ostype=Android
fi
fi
if [ "${_ostype}" = Darwin ] && [ "${_cputype}" = i386 ]; then
# Darwin `uname -m` lies
if sysctl hw.optional.x86_64 | grep -q ': 1'; then
_cputype=x86_64
fi
fi
if [ "${_ostype}" = SunOS ]; then
# Both Solaris and illumos presently announce as "SunOS" in "uname -s"
# so use "uname -o" to disambiguate. We use the full path to the
# system uname in case the user has coreutils uname first in PATH,
# which has historically sometimes printed the wrong value here.
if [ "$(/usr/bin/uname -o || true)" = illumos ]; then
_ostype=illumos
fi
# illumos systems have multi-arch userlands, and "uname -m" reports the
# machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86
# systems. Check for the native (widest) instruction set on the
# running kernel:
if [ "${_cputype}" = i86pc ]; then
_cputype="$(isainfo -n)"
fi
fi
case "${_ostype}" in
Android)
_ostype=linux-android
;;
Linux)
check_proc
_ostype=unknown-linux-${_clibtype}
_bitness=$(get_bitness)
;;
FreeBSD)
_ostype=unknown-freebsd
;;
NetBSD)
_ostype=unknown-netbsd
;;
DragonFly)
_ostype=unknown-dragonfly
;;
Darwin)
_ostype=apple-darwin
;;
illumos)
_ostype=unknown-illumos
;;
MINGW* | MSYS* | CYGWIN* | Windows_NT)
_ostype=pc-windows-msvc
;;
*)
err "unrecognized OS type: ${_ostype}"
;;
esac
case "${_cputype}" in
i386 | i486 | i686 | i786 | x86)
_cputype=i686
;;
xscale | arm)
_cputype=arm
if [ "${_ostype}" = "linux-android" ]; then
_ostype=linux-androideabi
fi
;;
armv6l)
_cputype=arm
if [ "${_ostype}" = "linux-android" ]; then
_ostype=linux-androideabi
else
_ostype="${_ostype}eabihf"
fi
;;
armv7l | armv8l)
_cputype=armv7
if [ "${_ostype}" = "linux-android" ]; then
_ostype=linux-androideabi
else
_ostype="${_ostype}eabihf"
fi
;;
aarch64 | arm64)
_cputype=aarch64
;;
x86_64 | x86-64 | x64 | amd64)
_cputype=x86_64
;;
mips)
_cputype=$(get_endianness mips '' el)
;;
mips64)
if [ "${_bitness}" -eq 64 ]; then
# only n64 ABI is supported for now
_ostype="${_ostype}abi64"
_cputype=$(get_endianness mips64 '' el)
fi
;;
ppc)
_cputype=powerpc
;;
ppc64)
_cputype=powerpc64
;;
ppc64le)
_cputype=powerpc64le
;;
s390x)
_cputype=s390x
;;
riscv64)
_cputype=riscv64gc
;;
*)
err "unknown CPU type: ${_cputype}"
;;
esac
# Detect 64-bit linux with 32-bit userland
if [ "${_ostype}" = unknown-linux-musl ] && [ "${_bitness}" -eq 32 ]; then
case ${_cputype} in
x86_64)
# 32-bit executable for amd64 = x32
if is_host_amd64_elf; then {
echo "x32 userland is unsupported" 1>&2
exit 1
}; else
_cputype=i686
fi
;;
mips64)
_cputype=$(get_endianness mips '' el)
;;
powerpc64)
_cputype=powerpc
;;
aarch64)
_cputype=armv7
if [ "${_ostype}" = "linux-android" ]; then
_ostype=linux-androideabi
else
_ostype="${_ostype}eabihf"
fi
;;
riscv64gc)
err "riscv64 with 32-bit userland unsupported"
;;
*) ;;
esac
fi
# Detect armv7 but without the CPU features Rust needs in that build,
# and fall back to arm.
# See https://github.com/rust-lang/rustup.rs/issues/587.
if [ "${_ostype}" = "unknown-linux-musleabihf" ] && [ "${_cputype}" = armv7 ]; then
if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then
# At least one processor does not have NEON.
_cputype=arm
fi
fi
_arch="${_cputype}-${_ostype}"
RETVAL="${_arch}"
}
get_bitness() {
need_cmd head
# Architecture detection without dependencies beyond coreutils.
# ELF files start out "\x7fELF", and the following byte is
# 0x01 for 32-bit and
# 0x02 for 64-bit.
# The printf builtin on some shells like dash only supports octal
# escape sequences, so we use those.
local _current_exe_head
_current_exe_head=$(head -c 5 /proc/self/exe)
if [ "${_current_exe_head}" = "$(printf '\177ELF\001')" ]; then
echo 32
elif [ "${_current_exe_head}" = "$(printf '\177ELF\002')" ]; then
echo 64
else
err "unknown platform bitness"
fi
}
get_endianness() {
local cputype=$1
local suffix_eb=$2
local suffix_el=$3
# detect endianness without od/hexdump, like get_bitness() does.
need_cmd head
need_cmd tail
local _current_exe_endianness
_current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)"
if [ "${_current_exe_endianness}" = "$(printf '\001')" ]; then
echo "${cputype}${suffix_el}"
elif [ "${_current_exe_endianness}" = "$(printf '\002')" ]; then
echo "${cputype}${suffix_eb}"
else
err "unknown platform endianness"
fi
}
is_host_amd64_elf() {
need_cmd head
need_cmd tail
# ELF e_machine detection without dependencies beyond coreutils.
# Two-byte field at offset 0x12 indicates the CPU,
# but we're interested in it being 0x3E to indicate amd64, or not that.
local _current_exe_machine
_current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1)
[ "${_current_exe_machine}" = "$(printf '\076')" ]
}
check_proc() {
# Check for /proc by looking for the /proc/self/exe link.
# This is only run on Linux.
if ! test -L /proc/self/exe; then
err "unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc."
fi
}
need_cmd() {
if ! check_cmd "$1"; then
err "need '$1' (command not found)"
fi
}
check_cmd() {
command -v "$1" >/dev/null 2>&1
}
# Run a command that should never fail. If the command fails execution
# will immediately terminate with an error showing the failing
# command.
ensure() {
if ! "$@"; then err "command failed: $*"; fi
}
assert_nz() {
if [ -z "$1" ]; then err "found empty string: $2"; fi
}
err() {
echo "Error: $1" >&2
exit 1
}
# This is put in braces to ensure that the script does not run until it is
# downloaded completely.
{
main "$@" || exit 1
}

39
justfile Normal file
View File

@ -0,0 +1,39 @@
default:
@just --list
[unix]
fmt:
nix-shell --cores 0 --pure --run 'cargo-fmt --all'
nix-shell --cores 0 --pure --run 'nixfmt -- *.nix'
nix-shell --cores 0 --pure --run 'shfmt --indent=4 --language-dialect=posix --simplify --write *.sh'
nix-shell --cores 0 --pure --run 'yamlfmt -- .github/workflows/*.yml'
[windows]
fmt:
cargo +nightly fmt --all
[unix]
lint:
nix-shell --cores 0 --pure --run 'cargo-fmt --all --check'
nix-shell --cores 0 --pure --run 'cargo clippy --all-features --all-targets -- -Dwarnings'
nix-shell --cores 0 --pure --run 'cargo msrv verify'
nix-shell --cores 0 --pure --run 'cargo udeps --all-features --all-targets --workspace'
nix-shell --cores 0 --pure --run 'mandoc -man -Wall -Tlint -- man/man1/*.1'
nix-shell --cores 0 --pure --run 'markdownlint *.md'
nix-shell --cores 0 --pure --run 'nixfmt --check -- *.nix'
nix-shell --cores 0 --pure --run 'shellcheck --enable all *.sh'
nix-shell --cores 0 --pure --run 'shfmt --diff --indent=4 --language-dialect=posix --simplify *.sh'
nix-shell --cores 0 --pure --run 'yamlfmt -lint -- .github/workflows/*.yml'
[windows]
lint:
cargo +nightly fmt --all --check
cargo +stable clippy --all-features --all-targets -- -Dwarnings
[unix]
test *args:
nix-shell --cores 0 --pure --run 'cargo nextest run --all-features --no-fail-fast --workspace {{args}}'
[windows]
test *args:
cargo +stable test --no-fail-fast --workspace {{args}}

View File

@ -19,6 +19,6 @@ Print help information.
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp
\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR
.SH AUTHOR .SH AUTHOR
Ajeet D'Souza <\fB98ajeet@gmail.com\fR> Ajeet D'Souza \fB<98ajeet@gmail.com>\fR

View File

@ -11,7 +11,7 @@ The format of the database being imported:
tab(|); tab(|);
l l. l l.
\fBautojump\fR \fBautojump\fR
\fBz\fR|(for \fBz\fR, \fBz.lua\fR, or \fBzsh-z\fR) \fBz\fR|(for \fBfasd\fR, \fBz\fR, \fBz.lua\fR, or \fBzsh-z\fR)
.TE .TE
.sp .sp
Note: zoxide only imports paths from autojump, since its matching Note: zoxide only imports paths from autojump, since its matching
@ -26,6 +26,6 @@ option merges imported data into the existing database.
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp
\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR
.SH AUTHOR .SH AUTHOR
Ajeet D'Souza <\fB98ajeet@gmail.com\fR> Ajeet D'Souza \fB<98ajeet@gmail.com>\fR

View File

@ -7,58 +7,63 @@
To initialize zoxide on your shell: To initialize zoxide on your shell:
.TP .TP
.B bash .B bash
Add this to your configuration (usually \fB~/.bashrc\fR): Add this to the \fBend\fR of your config file (usually \fB~/.bashrc\fR):
.sp .sp
.nf .nf
\fBeval "$(zoxide init bash)"\fR \fBeval "$(zoxide init bash)"\fR
.fi .fi
.TP .TP
.B elvish .B elvish
Add this to your configuration (usually \fB~/.elvish/rc.elv\fR): Add this to the \fBend\fR of your config file (usually \fB~/.elvish/rc.elv\fR):
.sp .sp
.nf .nf
\fBeval $(zoxide init elvish | slurp)\fR \fBeval $(zoxide init elvish | slurp)\fR
.fi .fi
.sp .sp
Note: zoxide only supports elvish v0.16.0 and above. Note: zoxide only supports elvish v0.18.0 and above.
.TP .TP
.B fish .B fish
Add this to your configuration (usually \fB~/.config/fish/config.fish\fR): Add this to the \fBend\fR of your config file (usually
\fB~/.config/fish/config.fish\fR):
.sp .sp
.nf .nf
\fBzoxide init fish | source\fR \fBzoxide init fish | source\fR
.fi .fi
.TP .TP
.B nushell .B nushell
Add this to your configuration (find it by running \fBconfig path\fR in Add this to the \fBend\fR of your env file (find it by running
Nushell): \fB$nu.env-path\fR in Nushell):
.sp .sp
.nf .nf
\fBstartup = ["zoxide init nushell --hook prompt | save ~/.zoxide.nu", "source ~/.zoxide.nu"]\fR \fBzoxide init nushell | save -f ~/.zoxide.nu\fR
.fi .fi
.sp .sp
Note: zoxide only supports Nushell v0.37.0 and above. Now, add this to the \fBend\fR of your config file (find it by running
.TP \fB$nu.config-path\fR in Nushell):
.B powershell
Add this to your configuration (find it by running \fBecho $profile\fR in
PowerShell):
.sp .sp
.nf .nf
\fBInvoke-Expression (& { \fBsource ~/.zoxide.nu\fR
$hook = if ($PSVersionTable.PSVersion.Major -lt 6) { 'prompt' } else { 'pwd' } .fi
(zoxide init --hook $hook powershell | Out-String) .sp
})\fR Note: zoxide only supports Nushell v0.73.0 and above.
.TP
.B powershell
Add this to the \fBend\fR of your config file (find it by running \fBecho
$profile\fR in PowerShell):
.sp
.nf
\fBInvoke-Expression (& { (zoxide init powershell | Out-String) })\fR
.fi .fi
.TP .TP
.B xonsh .B xonsh
Add this to your configuration (usually \fB~/.xonshrc\fR): Add this to the \fBend\fR of your config file (usually \fB~/.xonshrc\fR):
.sp .sp
.nf .nf
\fBexecx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide')\fR \fBexecx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide')\fR
.fi .fi
.TP .TP
.B zsh .B zsh
Add this to your configuration (usually \fB~/.zshrc\fR): Add this to the \fBend\fR of your config file (usually \fB~/.zshrc\fR):
.sp .sp
.nf .nf
\fBeval "$(zoxide init zsh)"\fR \fBeval "$(zoxide init zsh)"\fR
@ -66,7 +71,7 @@ Add this to your configuration (usually \fB~/.zshrc\fR):
.TP .TP
.B any POSIX shell .B any POSIX shell
.sp .sp
Add this to your configuration: Add this to the \fBend\fR of your config file:
.sp .sp
.nf .nf
\fBeval "$(zoxide init posix --hook prompt)"\fR \fBeval "$(zoxide init posix --hook prompt)"\fR
@ -74,9 +79,9 @@ Add this to your configuration:
.SH OPTIONS .SH OPTIONS
.TP .TP
.B --cmd .B --cmd
Changes the prefix of predefined aliases (\fBz\fR, \fBzi\fR). Changes the prefix of the \fBz\fR and \fBzi\fR commands.
.br .br
\fB--cmd j\fR would change the aliases to (\fBj\fR, \fBji\fR). \fB--cmd j\fR would change the commands to (\fBj\fR, \fBji\fR).
.br .br
\fB--cmd cd\fR would replace the \fBcd\fR command (doesn't work on Nushell / \fB--cmd cd\fR would replace the \fBcd\fR command (doesn't work on Nushell /
POSIX shells). POSIX shells).
@ -94,13 +99,13 @@ l l.
\fBpwd\fR|Whenever the directory is changed \fBpwd\fR|Whenever the directory is changed
.TE .TE
.TP .TP
.B --no-aliases .B --no-cmd
Don't define extra aliases (\fBz\fR, \fBzi\fR). These functions will still be Prevents zoxide from defining the \fBz\fR and \fBzi\fR commands. These functions
available in your shell as \fB__zoxide_z\fR and \fB__zoxide_zi\fR, should you will still be available in your shell as \fB__zoxide_z\fR and \fB__zoxide_zi\fR,
choose to redefine them. should you choose to redefine them.
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp
\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR
.SH AUTHOR .SH AUTHOR
Ajeet D'Souza <\fB98ajeet@gmail.com\fR> Ajeet D'Souza \fB<98ajeet@gmail.com>\fR

View File

@ -28,6 +28,6 @@ Print the calculated score as well as the matched path.
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp
\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR
.SH AUTHOR .SH AUTHOR
Ajeet D'Souza <\fB98ajeet@gmail.com\fR> Ajeet D'Souza \fB<98ajeet@gmail.com>\fR

View File

@ -10,12 +10,9 @@ If you'd like to permanently exclude a directory from the database, see the
.TP .TP
.B -h, --help .B -h, --help
Print help information. Print help information.
.TP
.B -i, --interactive [KEYWORDS]
Use interactive selection. This option requires \fBfzf\fR(1).
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp
\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR
.SH AUTHOR .SH AUTHOR
Ajeet D'Souza <\fB98ajeet@gmail.com\fR> Ajeet D'Souza \fB<98ajeet@gmail.com>\fR

View File

@ -129,6 +129,6 @@ l l.
.SH REPORTING BUGS .SH REPORTING BUGS
For any issues, feature requests, or questions, please visit: For any issues, feature requests, or questions, please visit:
.sp .sp
\fBhttps://github.com/ajeetdsouza/zoxide/issues\fR. \fBhttps://github.com/ajeetdsouza/zoxide/issues\fR
.SH AUTHOR .SH AUTHOR
Ajeet D'Souza <\fB98ajeet@gmail.com\fR> Ajeet D'Souza \fB<98ajeet@gmail.com>\fR

View File

@ -1,12 +1,8 @@
# comment_width = 100 group_imports = "StdExternalCrate"
# error_on_line_overflow = true imports_granularity = "Module"
# error_on_unformatted = true
# group_imports = "StdExternalCrate"
# imports_granularity = "Module"
max_width = 120
newline_style = "Native" newline_style = "Native"
use_field_init_shorthand = true use_field_init_shorthand = true
use_small_heuristics = "Max" use_small_heuristics = "Max"
use_try_shorthand = true use_try_shorthand = true
# wrap_comments = true wrap_comments = true
# version = "Two" version = "Two"

View File

@ -1,13 +1,22 @@
let let
rust = import (builtins.fetchTarball
"https://github.com/oxalica/rust-overlay/archive/46d8d20fce510c6a25fa66f36e31f207f6ea49e4.tar.gz");
pkgs = import (builtins.fetchTarball pkgs = import (builtins.fetchTarball
"https://github.com/NixOS/nixpkgs/archive/fae46e66a5df220327b45e0d7c27c6961cf922ce.tar.gz") { "https://github.com/NixOS/nixpkgs/archive/22a6958f46fd8e14830d02856ff63b1d0e5cc3e4.tar.gz") {
overlays = [ rust ]; overlays = [ rust ];
}; };
rust = import (builtins.fetchTarball
"https://github.com/oxalica/rust-overlay/archive/a61fcd9910229d097ffef92b5a2440065e3b64d5.tar.gz");
rust-nightly =
pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.minimal);
cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" ''
export RUSTC="${rust-nightly}/bin/rustc";
export CARGO="${rust-nightly}/bin/cargo";
exec "${pkgs.cargo-udeps}/bin/cargo-udeps" "$@"
'';
in pkgs.mkShell { in pkgs.mkShell {
buildInputs = [ buildInputs = [
# Rust # Rust
(pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.rustfmt))
pkgs.rust-bin.stable.latest.default pkgs.rust-bin.stable.latest.default
# Shells # Shells
@ -21,7 +30,11 @@ in pkgs.mkShell {
pkgs.zsh pkgs.zsh
# Tools # Tools
pkgs.cargo-audit cargo-udeps
pkgs.cargo-msrv
pkgs.cargo-nextest
pkgs.cargo-udeps
pkgs.just
pkgs.mandoc pkgs.mandoc
pkgs.nixfmt pkgs.nixfmt
pkgs.nodePackages.markdownlint-cli pkgs.nodePackages.markdownlint-cli
@ -30,6 +43,7 @@ in pkgs.mkShell {
pkgs.python3Packages.pylint pkgs.python3Packages.pylint
pkgs.shellcheck pkgs.shellcheck
pkgs.shfmt pkgs.shfmt
pkgs.yamlfmt
# Dependencies # Dependencies
pkgs.cacert pkgs.cacert
@ -38,5 +52,5 @@ in pkgs.mkShell {
pkgs.libiconv pkgs.libiconv
]; ];
RUST_BACKTRACE = 1; CARGO_TARGET_DIR = "target_nix";
} }

View File

@ -1,131 +0,0 @@
use std::path::PathBuf;
use clap::{ArgEnum, Parser, ValueHint};
const ENV_HELP: &str = "ENVIRONMENT VARIABLES:
_ZO_DATA_DIR Path for zoxide data files
_ZO_ECHO Print the matched directory before navigating to it when set to 1
_ZO_EXCLUDE_DIRS List of directory globs to be excluded
_ZO_FZF_OPTS Custom flags to pass to fzf
_ZO_MAXAGE Maximum total age after which entries start getting deleted
_ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths";
#[derive(Debug, Parser)]
#[clap(
bin_name = env!("CARGO_PKG_NAME"),
about,
author,
after_help = ENV_HELP,
disable_help_subcommand = true,
propagate_version = true,
version = option_env!("ZOXIDE_VERSION").unwrap_or_default()
)]
pub enum Cmd {
Add(Add),
Import(Import),
Init(Init),
Query(Query),
Remove(Remove),
}
/// Add a new directory or increment its rank
#[derive(Debug, Parser)]
pub struct Add {
#[clap(min_values = 1, required = true, value_hint = ValueHint::DirPath)]
pub paths: Vec<PathBuf>,
}
/// Import entries from another application
#[derive(Debug, Parser)]
pub struct Import {
#[clap(value_hint = ValueHint::FilePath)]
pub path: PathBuf,
/// Application to import from
#[clap(arg_enum, long)]
pub from: ImportFrom,
/// Merge into existing database
#[clap(long)]
pub merge: bool,
}
#[derive(ArgEnum, Clone, Debug)]
pub enum ImportFrom {
Autojump,
Z,
}
/// Generate shell configuration
#[derive(Debug, Parser)]
pub struct Init {
#[clap(arg_enum)]
pub shell: InitShell,
/// Prevents zoxide from defining any commands
#[clap(long)]
pub no_aliases: bool,
/// Renames the 'z' command and corresponding aliases
#[clap(long, default_value = "z")]
pub cmd: String,
/// Chooses event upon which an entry is added to the database
#[clap(arg_enum, long, default_value = "pwd")]
pub hook: InitHook,
}
#[derive(ArgEnum, Clone, Copy, Debug, Eq, PartialEq)]
pub enum InitHook {
None,
Prompt,
Pwd,
}
#[derive(ArgEnum, Clone, Debug)]
pub enum InitShell {
Bash,
Elvish,
Fish,
Nushell,
Posix,
Powershell,
Xonsh,
Zsh,
}
/// Search for a directory in the database
#[derive(Debug, Parser)]
pub struct Query {
pub keywords: Vec<String>,
/// Show deleted directories
#[clap(long)]
pub all: bool,
/// Use interactive selection
#[clap(long, short, conflicts_with = "list")]
pub interactive: bool,
/// List all matching directories
#[clap(long, short, conflicts_with = "interactive")]
pub list: bool,
/// Print score with results
#[clap(long, short, conflicts_with = "interactive")]
pub score: bool,
/// Exclude a path from results
#[clap(long, value_hint = ValueHint::DirPath, value_name = "path")]
pub exclude: Option<String>,
}
/// Remove a directory from the database
#[derive(Debug, Parser)]
pub struct Remove {
/// Use interactive selection
#[clap(long, short)]
pub interactive: bool,
#[clap(value_hint = ValueHint::DirPath)]
pub paths: Vec<String>,
}

View File

@ -3,42 +3,42 @@ use std::path::Path;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use crate::cmd::{Add, Run}; use crate::cmd::{Add, Run};
use crate::db::DatabaseFile; use crate::db::Database;
use crate::{config, util}; use crate::{config, util};
impl Run for Add { impl Run for Add {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
// These characters can't be printed cleanly to a single line, so they can cause confusion // These characters can't be printed cleanly to a single line, so they can cause
// when writing to fzf / stdout. // confusion when writing to stdout.
const EXCLUDE_CHARS: &[char] = &['\n', '\r']; const EXCLUDE_CHARS: &[char] = &['\n', '\r'];
let data_dir = config::data_dir()?;
let exclude_dirs = config::exclude_dirs()?; let exclude_dirs = config::exclude_dirs()?;
let max_age = config::maxage()?; let max_age = config::maxage()?;
let now = util::current_time()?; let now = util::current_time()?;
let mut db = DatabaseFile::new(data_dir); let mut db = Database::open()?;
let mut db = db.open()?;
for path in &self.paths { for path in &self.paths {
let path = if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }(path)?; let path =
if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }(
path,
)?;
let path = util::path_to_str(&path)?; let path = util::path_to_str(&path)?;
// Ignore path if it contains unsupported characters, or if it's in the exclude list. // Ignore path if it contains unsupported characters, or if it's in the exclude
// list.
if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) { if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) {
continue; continue;
} }
if !Path::new(path).is_dir() { if !Path::new(path).is_dir() {
bail!("not a directory: {}", path); bail!("not a directory: {path}");
} }
db.add(path, now); db.add_update(path, 1.0, now);
} }
if db.modified { if db.dirty() {
db.age(max_age); db.age(max_age);
db.save()?;
} }
db.save()
Ok(())
} }
} }

186
src/cmd/cmd.rs Normal file
View File

@ -0,0 +1,186 @@
#![allow(clippy::module_inception)]
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum, ValueHint};
const HELP_TEMPLATE: &str = color_print::cstr!("\
{before-help}<bold><underline>{name} {version}</underline></bold>
{author}
https://github.com/ajeetdsouza/zoxide
{about}
{usage-heading}
{tab}{usage}
{all-args}{after-help}
<bold><underline>Environment variables:</underline></bold>
{tab}<bold>_ZO_DATA_DIR</bold> {tab}Path for zoxide data files
{tab}<bold>_ZO_ECHO</bold> {tab}Print the matched directory before navigating to it when set to 1
{tab}<bold>_ZO_EXCLUDE_DIRS</bold> {tab}List of directory globs to be excluded
{tab}<bold>_ZO_FZF_OPTS</bold> {tab}Custom flags to pass to fzf
{tab}<bold>_ZO_MAXAGE</bold> {tab}Maximum total age after which entries start getting deleted
{tab}<bold>_ZO_RESOLVE_SYMLINKS</bold>{tab}Resolve symlinks when storing paths");
#[derive(Debug, Parser)]
#[clap(
about,
author,
help_template = HELP_TEMPLATE,
disable_help_subcommand = true,
propagate_version = true,
version,
)]
pub enum Cmd {
Add(Add),
Edit(Edit),
Import(Import),
Init(Init),
Query(Query),
Remove(Remove),
}
/// Add a new directory or increment its rank
#[derive(Debug, Parser)]
#[clap(
author,
help_template = HELP_TEMPLATE,
)]
pub struct Add {
#[clap(num_args = 1.., required = true, value_hint = ValueHint::DirPath)]
pub paths: Vec<PathBuf>,
}
/// Edit the database
#[derive(Debug, Parser)]
#[clap(
author,
help_template = HELP_TEMPLATE,
)]
pub struct Edit {
#[clap(subcommand)]
pub cmd: Option<EditCommand>,
}
#[derive(Clone, Debug, Subcommand)]
pub enum EditCommand {
#[clap(hide = true)]
Decrement { path: String },
#[clap(hide = true)]
Delete { path: String },
#[clap(hide = true)]
Increment { path: String },
#[clap(hide = true)]
Reload,
}
/// Import entries from another application
#[derive(Debug, Parser)]
#[clap(
author,
help_template = HELP_TEMPLATE,
)]
pub struct Import {
#[clap(value_hint = ValueHint::FilePath)]
pub path: PathBuf,
/// Application to import from
#[clap(value_enum, long)]
pub from: ImportFrom,
/// Merge into existing database
#[clap(long)]
pub merge: bool,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum ImportFrom {
Autojump,
#[clap(alias = "fasd")]
Z,
}
/// Generate shell configuration
#[derive(Debug, Parser)]
#[clap(
author,
help_template = HELP_TEMPLATE,
)]
pub struct Init {
#[clap(value_enum)]
pub shell: InitShell,
/// Prevents zoxide from defining the `z` and `zi` commands
#[clap(long, alias = "no-aliases")]
pub no_cmd: bool,
/// Changes the prefix of the `z` and `zi` commands
#[clap(long, default_value = "z")]
pub cmd: String,
/// Changes how often zoxide increments a directory's score
#[clap(value_enum, long, default_value = "pwd")]
pub hook: InitHook,
}
#[derive(ValueEnum, Clone, Copy, Debug, Eq, PartialEq)]
pub enum InitHook {
None,
Prompt,
Pwd,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum InitShell {
Bash,
Elvish,
Fish,
Nushell,
Posix,
Powershell,
Xonsh,
Zsh,
}
/// Search for a directory in the database
#[derive(Debug, Parser)]
#[clap(
author,
help_template = HELP_TEMPLATE,
)]
pub struct Query {
pub keywords: Vec<String>,
/// Show unavailable directories
#[clap(long, short)]
pub all: bool,
/// Use interactive selection
#[clap(long, short, conflicts_with = "list")]
pub interactive: bool,
/// List all matching directories
#[clap(long, short, conflicts_with = "interactive")]
pub list: bool,
/// Print score with results
#[clap(long, short)]
pub score: bool,
/// Exclude the current directory
#[clap(long, value_hint = ValueHint::DirPath, value_name = "path")]
pub exclude: Option<String>,
}
/// Remove a directory from the database
#[derive(Debug, Parser)]
#[clap(
author,
help_template = HELP_TEMPLATE,
)]
pub struct Remove {
#[clap(value_hint = ValueHint::DirPath)]
pub paths: Vec<String>,
}

84
src/cmd/edit.rs Normal file
View File

@ -0,0 +1,84 @@
use std::io::{self, Write};
use anyhow::Result;
use crate::cmd::{Edit, EditCommand, Run};
use crate::db::Database;
use crate::error::BrokenPipeHandler;
use crate::util::{self, Fzf, FzfChild};
impl Run for Edit {
fn run(&self) -> Result<()> {
let now = util::current_time()?;
let db = &mut Database::open()?;
match &self.cmd {
Some(cmd) => {
match cmd {
EditCommand::Decrement { path } => db.add(path, -1.0, now),
EditCommand::Delete { path } => {
db.remove(path);
}
EditCommand::Increment { path } => db.add(path, 1.0, now),
EditCommand::Reload => {}
}
db.save()?;
let stdout = &mut io::stdout().lock();
for dir in db.dirs().iter().rev() {
write!(stdout, "{}\0", dir.display().with_score(now).with_separator('\t'))
.pipe_exit("fzf")?;
}
Ok(())
}
None => {
db.sort_by_score(now);
db.save()?;
Self::get_fzf()?.wait()?;
Ok(())
}
}
}
}
impl Edit {
fn get_fzf() -> Result<FzfChild> {
Fzf::new()?
.args([
// Search mode
"--exact",
// Search result
"--no-sort",
// Interface
"--bind=\
btab:up,\
ctrl-r:reload(zoxide edit reload),\
ctrl-d:reload(zoxide edit delete {2..}),\
ctrl-w:reload(zoxide edit increment {2..}),\
ctrl-s:reload(zoxide edit decrement {2..}),\
ctrl-z:ignore,\
double-click:ignore,\
enter:abort,\
start:reload(zoxide edit reload),\
tab:down",
"--cycle",
"--keep-right",
// Layout
"--border=sharp",
"--border-label= zoxide-edit ",
"--header=\
ctrl-r:reload \tctrl-d:delete
ctrl-w:increment\tctrl-s:decrement
SCORE\tPATH",
"--info=inline",
"--layout=reverse",
"--padding=1,0,0,0",
// Display
"--color=label:bold",
"--tabstop=1",
])
.enable_preview()
.spawn()
}
}

View File

@ -3,24 +3,22 @@ use std::fs;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use crate::cmd::{Import, ImportFrom, Run}; use crate::cmd::{Import, ImportFrom, Run};
use crate::config; use crate::db::Database;
use crate::db::{Database, DatabaseFile, Dir};
impl Run for Import { impl Run for Import {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let buffer = fs::read_to_string(&self.path) let buffer = fs::read_to_string(&self.path).with_context(|| {
.with_context(|| format!("could not open database for importing: {}", &self.path.display()))?; format!("could not open database for importing: {}", &self.path.display())
})?;
let data_dir = config::data_dir()?; let mut db = Database::open()?;
let mut db = DatabaseFile::new(data_dir); if !self.merge && !db.dirs().is_empty() {
let db = &mut db.open()?;
if !self.merge && !db.dirs.is_empty() {
bail!("current database is not empty, specify --merge to continue anyway"); bail!("current database is not empty, specify --merge to continue anyway");
} }
match self.from { match self.from {
ImportFrom::Autojump => from_autojump(db, &buffer), ImportFrom::Autojump => import_autojump(&mut db, &buffer),
ImportFrom::Z => from_z(db, &buffer), ImportFrom::Z => import_z(&mut db, &buffer),
} }
.context("import error")?; .context("import error")?;
@ -28,55 +26,51 @@ impl Run for Import {
} }
} }
fn from_autojump<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { fn import_autojump(db: &mut Database, buffer: &str) -> Result<()> {
for line in buffer.lines() { for line in buffer.lines() {
if line.is_empty() { if line.is_empty() {
continue; continue;
} }
let mut split = line.splitn(2, '\t'); let (rank, path) =
line.split_once('\t').with_context(|| format!("invalid entry: {line}"))?;
let rank = split.next().with_context(|| format!("invalid entry: {}", line))?; let mut rank = rank.parse::<f64>().with_context(|| format!("invalid rank: {rank}"))?;
let mut rank = rank.parse::<f64>().with_context(|| format!("invalid rank: {}", rank))?; // Normalize the rank using a sigmoid function. Don't import actual ranks from
// Normalize the rank using a sigmoid function. Don't import actual ranks from autojump, // autojump, since its scoring algorithm is very different and might
// since its scoring algorithm is very different and might take a while to get normalized. // take a while to get normalized.
rank = sigmoid(rank); rank = sigmoid(rank);
let path = split.next().with_context(|| format!("invalid entry: {}", line))?; db.add_unchecked(path, rank, 0);
db.dirs.push(Dir { path: path.into(), rank, last_accessed: 0 });
db.modified = true;
} }
if db.modified { if db.dirty() {
db.dedup(); db.dedup();
} }
Ok(()) Ok(())
} }
fn from_z<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { fn import_z(db: &mut Database, buffer: &str) -> Result<()> {
for line in buffer.lines() { for line in buffer.lines() {
if line.is_empty() { if line.is_empty() {
continue; continue;
} }
let mut split = line.rsplitn(3, '|'); let mut split = line.rsplitn(3, '|');
let last_accessed = split.next().with_context(|| format!("invalid entry: {}", line))?; let last_accessed = split.next().with_context(|| format!("invalid entry: {line}"))?;
let last_accessed = last_accessed.parse().with_context(|| format!("invalid epoch: {}", last_accessed))?; let last_accessed =
last_accessed.parse().with_context(|| format!("invalid epoch: {last_accessed}"))?;
let rank = split.next().with_context(|| format!("invalid entry: {}", line))?; let rank = split.next().with_context(|| format!("invalid entry: {line}"))?;
let rank = rank.parse().with_context(|| format!("invalid rank: {}", rank))?; let rank = rank.parse().with_context(|| format!("invalid rank: {rank}"))?;
let path = split.next().with_context(|| format!("invalid entry: {}", line))?; let path = split.next().with_context(|| format!("invalid entry: {line}"))?;
db.dirs.push(Dir { path: path.into(), rank, last_accessed }); db.add_unchecked(path, rank, last_accessed);
db.modified = true;
} }
if db.modified { if db.dirty() {
db.dedup(); db.dedup();
} }
Ok(()) Ok(())
} }
@ -86,33 +80,33 @@ fn sigmoid(x: f64) -> f64 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::sigmoid; use super::*;
use crate::db::{Database, Dir}; use crate::db::Dir;
#[test] #[test]
fn from_autojump() { fn from_autojump() {
let buffer = r#" let data_dir = tempfile::tempdir().unwrap();
let mut db = Database::open_dir(data_dir.path()).unwrap();
for (path, rank, last_accessed) in [
("/quux/quuz", 1.0, 100),
("/corge/grault/garply", 6.0, 600),
("/waldo/fred/plugh", 3.0, 300),
("/xyzzy/thud", 8.0, 800),
("/foo/bar", 9.0, 900),
] {
db.add_unchecked(path, rank, last_accessed);
}
let buffer = "\
7.0 /baz 7.0 /baz
2.0 /foo/bar 2.0 /foo/bar
5.0 /quux/quuz 5.0 /quux/quuz";
"#; import_autojump(&mut db, buffer).unwrap();
let dirs = vec![ db.sort_by_path();
Dir { path: "/quux/quuz".into(), rank: 1.0, last_accessed: 100 }, println!("got: {:?}", &db.dirs());
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 },
Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 },
Dir { path: "/foo/bar".into(), rank: 9.0, last_accessed: 900 },
];
let data_dir = tempfile::tempdir().unwrap();
let data_dir = &data_dir.path().to_path_buf();
let mut db = Database { dirs: dirs.into(), modified: false, data_dir };
super::from_autojump(&mut db, buffer).unwrap(); let exp = [
db.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path));
println!("got: {:?}", &db.dirs.as_slice());
let exp = &[
Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 }, Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 },
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 }, Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 },
@ -120,9 +114,9 @@ mod tests {
Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 },
Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 },
]; ];
println!("exp: {:?}", &exp); println!("exp: {exp:?}");
for (dir1, dir2) in db.dirs.iter().zip(exp) { for (dir1, dir2) in db.dirs().iter().zip(exp) {
assert_eq!(dir1.path, dir2.path); assert_eq!(dir1.path, dir2.path);
assert!((dir1.rank - dir2.rank).abs() < 0.01); assert!((dir1.rank - dir2.rank).abs() < 0.01);
assert_eq!(dir1.last_accessed, dir2.last_accessed); assert_eq!(dir1.last_accessed, dir2.last_accessed);
@ -131,29 +125,29 @@ mod tests {
#[test] #[test]
fn from_z() { fn from_z() {
let buffer = r#" let data_dir = tempfile::tempdir().unwrap();
let mut db = Database::open_dir(data_dir.path()).unwrap();
for (path, rank, last_accessed) in [
("/quux/quuz", 1.0, 100),
("/corge/grault/garply", 6.0, 600),
("/waldo/fred/plugh", 3.0, 300),
("/xyzzy/thud", 8.0, 800),
("/foo/bar", 9.0, 900),
] {
db.add_unchecked(path, rank, last_accessed);
}
let buffer = "\
/baz|7|700 /baz|7|700
/quux/quuz|4|400 /quux/quuz|4|400
/foo/bar|2|200 /foo/bar|2|200
/quux/quuz|5|500 /quux/quuz|5|500";
"#; import_z(&mut db, buffer).unwrap();
let dirs = vec![ db.sort_by_path();
Dir { path: "/quux/quuz".into(), rank: 1.0, last_accessed: 100 }, println!("got: {:?}", &db.dirs());
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 },
Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 },
Dir { path: "/foo/bar".into(), rank: 9.0, last_accessed: 900 },
];
let data_dir = tempfile::tempdir().unwrap();
let data_dir = &data_dir.path().to_path_buf();
let mut db = Database { dirs: dirs.into(), modified: false, data_dir };
super::from_z(&mut db, buffer).unwrap(); let exp = [
db.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path));
println!("got: {:?}", &db.dirs.as_slice());
let exp = &[
Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 }, Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 },
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 }, Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 },
@ -161,9 +155,9 @@ mod tests {
Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 }, Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 },
Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 }, Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 },
]; ];
println!("exp: {:?}", &exp); println!("exp: {exp:?}");
for (dir1, dir2) in db.dirs.iter().zip(exp) { for (dir1, dir2) in db.dirs().iter().zip(exp) {
assert_eq!(dir1.path, dir2.path); assert_eq!(dir1.path, dir2.path);
assert!((dir1.rank - dir2.rank).abs() < 0.01); assert!((dir1.rank - dir2.rank).abs() < 0.01);
assert_eq!(dir1.last_accessed, dir2.last_accessed); assert_eq!(dir1.last_accessed, dir2.last_accessed);

View File

@ -6,28 +6,26 @@ use askama::Template;
use crate::cmd::{Init, InitShell, Run}; use crate::cmd::{Init, InitShell, Run};
use crate::config; use crate::config;
use crate::error::BrokenPipeHandler; use crate::error::BrokenPipeHandler;
use crate::shell::{self, Opts}; use crate::shell::{Bash, Elvish, Fish, Nushell, Opts, Posix, Powershell, Xonsh, Zsh};
impl Run for Init { impl Run for Init {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let cmd = if self.no_aliases { None } else { Some(self.cmd.as_str()) }; let cmd = if self.no_cmd { None } else { Some(self.cmd.as_str()) };
let echo = config::echo(); let echo = config::echo();
let resolve_symlinks = config::resolve_symlinks(); let resolve_symlinks = config::resolve_symlinks();
let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks }; let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks };
let source = match self.shell { let source = match self.shell {
InitShell::Bash => shell::Bash(opts).render(), InitShell::Bash => Bash(opts).render(),
InitShell::Elvish => shell::Elvish(opts).render(), InitShell::Elvish => Elvish(opts).render(),
InitShell::Fish => shell::Fish(opts).render(), InitShell::Fish => Fish(opts).render(),
InitShell::Nushell => shell::Nushell(opts).render(), InitShell::Nushell => Nushell(opts).render(),
InitShell::Posix => shell::Posix(opts).render(), InitShell::Posix => Posix(opts).render(),
InitShell::Powershell => shell::Powershell(opts).render(), InitShell::Powershell => Powershell(opts).render(),
InitShell::Xonsh => shell::Xonsh(opts).render(), InitShell::Xonsh => Xonsh(opts).render(),
InitShell::Zsh => shell::Zsh(opts).render(), InitShell::Zsh => Zsh(opts).render(),
} }
.context("could not render template")?; .context("could not render template")?;
writeln!(io::stdout(), "{}", source).pipe_exit("stdout") writeln!(io::stdout(), "{source}").pipe_exit("stdout")
} }
} }

View File

@ -1,5 +1,6 @@
mod _cmd;
mod add; mod add;
mod cmd;
mod edit;
mod import; mod import;
mod init; mod init;
mod query; mod query;
@ -7,7 +8,7 @@ mod remove;
use anyhow::Result; use anyhow::Result;
pub use crate::cmd::_cmd::*; pub use crate::cmd::cmd::*;
pub trait Run { pub trait Run {
fn run(&self) -> Result<()>; fn run(&self) -> Result<()>;
@ -17,6 +18,7 @@ impl Run for Cmd {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
match self { match self {
Cmd::Add(cmd) => cmd.run(), Cmd::Add(cmd) => cmd.run(),
Cmd::Edit(cmd) => cmd.run(),
Cmd::Import(cmd) => cmd.run(), Cmd::Import(cmd) => cmd.run(),
Cmd::Init(cmd) => cmd.run(), Cmd::Init(cmd) => cmd.run(),
Cmd::Query(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(),

View File

@ -1,18 +1,16 @@
use std::io::{self, Write}; use std::io::{self, Write};
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use crate::cmd::{Query, Run}; use crate::cmd::{Query, Run};
use crate::config; use crate::config;
use crate::db::{Database, DatabaseFile}; use crate::db::{Database, Epoch, Stream};
use crate::error::BrokenPipeHandler; use crate::error::BrokenPipeHandler;
use crate::util::{self, Fzf}; use crate::util::{self, Fzf, FzfChild};
impl Run for Query { impl Run for Query {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let data_dir = config::data_dir()?; let mut db = crate::db::Database::open()?;
let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?;
self.query(&mut db).and(db.save()) self.query(&mut db).and(db.save())
} }
} }
@ -20,7 +18,50 @@ impl Run for Query {
impl Query { impl Query {
fn query(&self, db: &mut Database) -> Result<()> { fn query(&self, db: &mut Database) -> Result<()> {
let now = util::current_time()?; let now = util::current_time()?;
let mut stream = self.get_stream(db, now);
if self.interactive {
let mut fzf = Self::get_fzf()?;
let selection = loop {
match stream.next() {
Some(dir) => {
if let Some(selection) = fzf.write(dir, now)? {
break selection;
}
}
None => break fzf.wait()?,
}
};
if self.score {
print!("{selection}");
} else {
let path = selection.get(7..).context("could not read selection from fzf")?;
print!("{path}");
}
} else if self.list {
let handle = &mut io::stdout().lock();
while let Some(dir) = stream.next() {
let dir = if self.score { dir.display().with_score(now) } else { dir.display() };
writeln!(handle, "{dir}").pipe_exit("stdout")?;
}
} else {
let handle = &mut io::stdout();
let Some(dir) = stream.next() else {
bail!(if stream.did_exclude() {
"you are already in the only match"
} else {
"no match found"
});
};
let dir = if self.score { dir.display().with_score(now) } else { dir.display() };
writeln!(handle, "{dir}").pipe_exit("stdout")?;
}
Ok(())
}
fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Stream<'a> {
let mut stream = db.stream(now).with_keywords(&self.keywords); let mut stream = db.stream(now).with_keywords(&self.keywords);
if !self.all { if !self.all {
let resolve_symlinks = config::resolve_symlinks(); let resolve_symlinks = config::resolve_symlinks();
@ -29,51 +70,36 @@ impl Query {
if let Some(path) = &self.exclude { if let Some(path) = &self.exclude {
stream = stream.with_exclude(path); stream = stream.with_exclude(path);
} }
stream
}
if self.interactive { fn get_fzf() -> Result<FzfChild> {
let mut fzf = Fzf::new(false)?; let mut fzf = Fzf::new()?;
let stdin = fzf.stdin(); if let Some(fzf_opts) = config::fzf_opts() {
fzf.env("FZF_DEFAULT_OPTS", fzf_opts)
let selection = loop {
let dir = match stream.next() {
Some(dir) => dir,
None => break fzf.select()?,
};
match writeln!(stdin, "{}", dir.display_score(now)) {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?,
result => result.context("could not write to fzf")?,
}
};
if self.score {
print!("{}", selection);
} else {
let path = selection.get(5..).context("could not read selection from fzf")?;
print!("{}", path);
}
} else if self.list {
let stdout = io::stdout();
let handle = &mut stdout.lock();
while let Some(dir) = stream.next() {
if self.score {
writeln!(handle, "{}", dir.display_score(now))
} else {
writeln!(handle, "{}", dir.display())
}
.pipe_exit("stdout")?;
}
handle.flush().pipe_exit("stdout")?;
} else { } else {
let dir = stream.next().context("no match found")?; fzf.args([
if self.score { // Search mode
writeln!(io::stdout(), "{}", dir.display_score(now)) "--exact",
} else { // Search result
writeln!(io::stdout(), "{}", dir.display()) "--no-sort",
} // Interface
.pipe_exit("stdout")?; "--bind=ctrl-z:ignore,btab:up,tab:down",
"--cycle",
"--keep-right",
// Layout
"--border=sharp", // rounded edges don't display correctly on some terminals
"--height=45%",
"--info=inline",
"--layout=reverse",
// Display
"--tabstop=1",
// Scripting
"--exit-0",
"--select-1",
])
.enable_preview()
} }
.spawn()
Ok(())
} }
} }

View File

@ -1,52 +1,19 @@
use std::io::{self, Write}; use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use crate::cmd::{Remove, Run}; use crate::cmd::{Remove, Run};
use crate::config; use crate::db::Database;
use crate::db::DatabaseFile; use crate::util;
use crate::util::{self, Fzf};
impl Run for Remove { impl Run for Remove {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let data_dir = config::data_dir()?; let mut db = Database::open()?;
let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?;
if self.interactive { for path in &self.paths {
let keywords = &self.paths; if !db.remove(path) {
let now = util::current_time()?; let path_abs = util::resolve_path(path)?;
let mut stream = db.stream(now).with_keywords(keywords); let path_abs = util::path_to_str(&path_abs)?;
if path_abs == path || !db.remove(path_abs) {
let mut fzf = Fzf::new(true)?; bail!("path not found in database: {path}")
let stdin = fzf.stdin();
let selection = loop {
let dir = match stream.next() {
Some(dir) => dir,
None => break fzf.select()?,
};
match writeln!(stdin, "{}", dir.display_score(now)) {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?,
result => result.context("could not write to fzf")?,
}
};
let paths = selection.lines().filter_map(|line| line.get(5..));
for path in paths {
if !db.remove(path) {
bail!("path not found in database: {}", path);
}
}
} else {
for path in &self.paths {
if !db.remove(path) {
let path_abs = util::resolve_path(path)?;
let path_abs = util::path_to_str(&path_abs)?;
if path_abs != path && !db.remove(path_abs) {
bail!("path not found in database: {} ({})", path, path_abs)
}
} }
} }
} }

View File

@ -2,7 +2,7 @@ use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result};
use glob::Pattern; use glob::Pattern;
use crate::db::Rank; use crate::db::Rank;
@ -10,45 +10,35 @@ use crate::db::Rank;
pub fn data_dir() -> Result<PathBuf> { pub fn data_dir() -> Result<PathBuf> {
let path = match env::var_os("_ZO_DATA_DIR") { let path = match env::var_os("_ZO_DATA_DIR") {
Some(path) => PathBuf::from(path), Some(path) => PathBuf::from(path),
None => match dirs::data_local_dir() { None => dirs::data_local_dir()
Some(mut path) => { .context("could not find data directory, please set _ZO_DATA_DIR manually")?
path.push("zoxide"); .join("zoxide"),
path
}
None => bail!("could not find data directory, please set _ZO_DATA_DIR manually"),
},
}; };
Ok(path) Ok(path)
} }
pub fn echo() -> bool { pub fn echo() -> bool {
match env::var_os("_ZO_ECHO") { env::var_os("_ZO_ECHO").map_or(false, |var| var == "1")
Some(var) => var == "1",
None => false,
}
} }
pub fn exclude_dirs() -> Result<Vec<Pattern>> { pub fn exclude_dirs() -> Result<Vec<Pattern>> {
env::var_os("_ZO_EXCLUDE_DIRS").map_or_else( match env::var_os("_ZO_EXCLUDE_DIRS") {
|| { Some(paths) => env::split_paths(&paths)
.map(|path| {
let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
Pattern::new(pattern)
.with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {pattern}"))
})
.collect(),
None => {
let pattern = (|| { let pattern = (|| {
let home = dirs::home_dir()?; let home = dirs::home_dir()?;
let home = home.to_str()?; let home = Pattern::escape(home.to_str()?);
let home = Pattern::escape(home);
Pattern::new(&home).ok() Pattern::new(&home).ok()
})(); })();
Ok(pattern.into_iter().collect()) Ok(pattern.into_iter().collect())
}, }
|paths| { }
env::split_paths(&paths)
.map(|path| {
let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
Pattern::new(pattern).with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {}", pattern))
})
.collect()
},
)
} }
pub fn fzf_opts() -> Option<OsString> { pub fn fzf_opts() -> Option<OsString> {
@ -56,20 +46,15 @@ pub fn fzf_opts() -> Option<OsString> {
} }
pub fn maxage() -> Result<Rank> { pub fn maxage() -> Result<Rank> {
match env::var_os("_ZO_MAXAGE") { env::var_os("_ZO_MAXAGE").map_or(Ok(10_000.0), |maxage| {
Some(maxage) => { let maxage = maxage.to_str().context("invalid unicode in _ZO_MAXAGE")?;
let maxage = maxage.to_str().context("invalid unicode in _ZO_MAXAGE")?; let maxage = maxage
let maxage = .parse::<u32>()
maxage.parse::<u64>().with_context(|| format!("unable to parse _ZO_MAXAGE as integer: {}", maxage))?; .with_context(|| format!("unable to parse _ZO_MAXAGE as integer: {maxage}"))?;
Ok(maxage as Rank) Ok(maxage as Rank)
} })
None => Ok(10000.0),
}
} }
pub fn resolve_symlinks() -> bool { pub fn resolve_symlinks() -> bool {
match env::var_os("_ZO_RESOLVE_SYMLINKS") { env::var_os("_ZO_RESOLVE_SYMLINKS").map_or(false, |var| var == "1")
Some(var) => var == "1",
None => false,
}
} }

View File

@ -1,83 +1,9 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::ops::{Deref, DerefMut};
use anyhow::{bail, Context, Result};
use bincode::Options as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)] use crate::util::{DAY, HOUR, WEEK};
pub struct DirList<'a>(#[serde(borrow)] pub Vec<Dir<'a>>);
impl DirList<'_> {
const VERSION: u32 = 3;
pub fn new() -> DirList<'static> {
DirList(Vec::new())
}
pub fn from_bytes(bytes: &[u8]) -> Result<DirList> {
// Assume a maximum size for the database. This prevents bincode from throwing strange
// errors when it encounters invalid data.
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
// Split bytes into sections.
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
if bytes.len() < version_size {
bail!("could not deserialize database: corrupted data");
}
let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
// Deserialize sections.
(|| {
let version = deserializer.deserialize(bytes_version)?;
match version {
Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?),
version => {
bail!("unsupported version (got {}, supports {})", version, Self::VERSION,)
}
}
})()
.context("could not deserialize database")
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
(|| -> bincode::Result<_> {
// Preallocate buffer with combined size of sections.
let version_size = bincode::serialized_size(&Self::VERSION)?;
let dirs_size = bincode::serialized_size(&self)?;
let buffer_size = version_size + dirs_size;
let mut buffer = Vec::with_capacity(buffer_size as _);
// Serialize sections into buffer.
bincode::serialize_into(&mut buffer, &Self::VERSION)?;
bincode::serialize_into(&mut buffer, &self)?;
Ok(buffer)
})()
.context("could not serialize database")
}
}
impl<'a> Deref for DirList<'a> {
type Target = Vec<Dir<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DirList<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> From<Vec<Dir<'a>>> for DirList<'a> {
fn from(dirs: Vec<Dir<'a>>) -> Self {
DirList(dirs)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Dir<'a> { pub struct Dir<'a> {
@ -88,11 +14,11 @@ pub struct Dir<'a> {
} }
impl Dir<'_> { impl Dir<'_> {
pub fn score(&self, now: Epoch) -> Rank { pub fn display(&self) -> DirDisplay<'_> {
const HOUR: Epoch = 60 * 60; DirDisplay::new(self)
const DAY: Epoch = 24 * HOUR; }
const WEEK: Epoch = 7 * DAY;
pub fn score(&self, now: Epoch) -> Rank {
// The older the entry, the lesser its importance. // The older the entry, the lesser its importance.
let duration = now.saturating_sub(self.last_accessed); let duration = now.saturating_sub(self.last_accessed);
if duration < HOUR { if duration < HOUR {
@ -105,63 +31,39 @@ impl Dir<'_> {
self.rank * 0.25 self.rank * 0.25
} }
} }
pub fn display(&self) -> DirDisplay {
DirDisplay { dir: self }
}
pub fn display_score(&self, now: Epoch) -> DirDisplayScore {
DirDisplayScore { dir: self, now }
}
} }
pub struct DirDisplay<'a> { pub struct DirDisplay<'a> {
dir: &'a Dir<'a>, dir: &'a Dir<'a>,
now: Option<Epoch>,
separator: char,
}
impl<'a> DirDisplay<'a> {
fn new(dir: &'a Dir) -> Self {
Self { dir, separator: ' ', now: None }
}
pub fn with_score(mut self, now: Epoch) -> Self {
self.now = Some(now);
self
}
pub fn with_separator(mut self, separator: char) -> Self {
self.separator = separator;
self
}
} }
impl Display for DirDisplay<'_> { impl Display for DirDisplay<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(now) = self.now {
let score = self.dir.score(now).clamp(0.0, 9999.0);
write!(f, "{score:>6.1}{}", self.separator)?;
}
write!(f, "{}", self.dir.path) write!(f, "{}", self.dir.path)
} }
} }
pub struct DirDisplayScore<'a> {
dir: &'a Dir<'a>,
now: Epoch,
}
impl Display for DirDisplayScore<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let score = self.dir.score(self.now);
let score = if score > 9999.0 {
9999
} else if score > 0.0 {
score as u32
} else {
0
};
write!(f, "{:>4} {}", score, self.dir.path)
}
}
pub type Rank = f64; pub type Rank = f64;
pub type Epoch = u64; pub type Epoch = u64;
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::{Dir, DirList};
#[test]
fn zero_copy() {
let dirs = DirList(vec![Dir { path: "/".into(), rank: 0.0, last_accessed: 0 }]);
let bytes = dirs.to_bytes().unwrap();
let dirs = DirList::from_bytes(&bytes).unwrap();
for dir in dirs.iter() {
assert!(matches!(dir.path, Cow::Borrowed(_)))
}
}
}

View File

@ -1,152 +1,237 @@
mod dir; mod dir;
mod stream; mod stream;
use std::fs;
use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{fs, io};
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
pub use dir::{Dir, DirList, Epoch, Rank}; use bincode::Options;
pub use stream::Stream; use ouroboros::self_referencing;
use crate::util; pub use crate::db::dir::{Dir, Epoch, Rank};
pub use crate::db::stream::Stream;
use crate::{config, util};
#[derive(Debug)] #[self_referencing]
pub struct Database<'file> { pub struct Database {
pub dirs: DirList<'file>, path: PathBuf,
pub modified: bool, bytes: Vec<u8>,
pub data_dir: &'file Path, #[borrows(bytes)]
#[covariant]
pub dirs: Vec<Dir<'this>>,
dirty: bool,
} }
impl<'file> Database<'file> { impl Database {
const VERSION: u32 = 3;
pub fn open() -> Result<Self> {
let data_dir = config::data_dir()?;
Self::open_dir(data_dir)
}
pub fn open_dir(data_dir: impl AsRef<Path>) -> Result<Self> {
let data_dir = data_dir.as_ref();
let path = data_dir.join("db.zo");
match fs::read(&path) {
Ok(bytes) => Self::try_new(path, bytes, |bytes| Self::deserialize(bytes), false),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Create data directory, but don't create any file yet. The file will be
// created later by [`Database::save`] if any data is modified.
fs::create_dir_all(data_dir).with_context(|| {
format!("unable to create data directory: {}", data_dir.display())
})?;
Ok(Self::new(path, Vec::new(), |_| Vec::new(), false))
}
Err(e) => {
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
}
}
}
pub fn save(&mut self) -> Result<()> { pub fn save(&mut self) -> Result<()> {
if !self.modified { // Only write to disk if the database is modified.
if !self.dirty() {
return Ok(()); return Ok(());
} }
let buffer = self.dirs.to_bytes()?; let bytes = Self::serialize(self.dirs())?;
let path = db_path(&self.data_dir); util::write(self.borrow_path(), bytes).context("could not write to database")?;
util::write(&path, &buffer).context("could not write to database")?; self.with_dirty_mut(|dirty| *dirty = false);
self.modified = false;
Ok(()) Ok(())
} }
/// Adds a new directory or increments its rank. Also updates its last accessed time. /// Increments the rank of a directory, or creates it if it does not exist.
pub fn add<S: AsRef<str>>(&mut self, path: S, now: Epoch) { pub fn add(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
let path = path.as_ref(); self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) {
Some(dir) => dir.rank = (dir.rank + by).max(0.0),
match self.dirs.iter_mut().find(|dir| dir.path == path) {
None => { None => {
self.dirs.push(Dir { path: path.to_string().into(), last_accessed: now, rank: 1.0 }); dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now })
} }
Some(dir) => { });
dir.last_accessed = now; self.with_dirty_mut(|dirty| *dirty = true);
dir.rank += 1.0; }
}
};
self.modified = true; /// Creates a new directory. This will create a duplicate entry if this
/// directory is always in the database, it is expected that the user either
/// does a check before calling this, or calls `dedup()` afterward.
pub fn add_unchecked(&mut self, path: impl AsRef<str> + Into<String>, rank: Rank, now: Epoch) {
self.with_dirs_mut(|dirs| {
dirs.push(Dir { path: path.into().into(), rank, last_accessed: now })
});
self.with_dirty_mut(|dirty| *dirty = true);
}
/// Increments the rank and updates the last_accessed of a directory, or
/// creates it if it does not exist.
pub fn add_update(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) {
Some(dir) => {
dir.rank = (dir.rank + by).max(0.0);
dir.last_accessed = now;
}
None => {
dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now })
}
});
self.with_dirty_mut(|dirty| *dirty = true);
}
/// Removes the directory with `path` from the store. This does not preserve
/// ordering, but is O(1).
pub fn remove(&mut self, path: impl AsRef<str>) -> bool {
match self.dirs().iter().position(|dir| dir.path == path.as_ref()) {
Some(idx) => {
self.swap_remove(idx);
true
}
None => false,
}
}
pub fn swap_remove(&mut self, idx: usize) {
self.with_dirs_mut(|dirs| dirs.swap_remove(idx));
self.with_dirty_mut(|dirty| *dirty = true);
}
pub fn age(&mut self, max_age: Rank) {
let mut dirty = false;
self.with_dirs_mut(|dirs| {
let total_age = dirs.iter().map(|dir| dir.rank).sum::<Rank>();
if total_age > max_age {
let factor = 0.9 * max_age / total_age;
for idx in (0..dirs.len()).rev() {
let dir = &mut dirs[idx];
dir.rank *= factor;
if dir.rank < 1.0 {
dirs.swap_remove(idx);
}
}
dirty = true;
}
});
self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty);
}
pub fn stream(&mut self, now: Epoch) -> Stream {
Stream::new(self, now)
} }
pub fn dedup(&mut self) { pub fn dedup(&mut self) {
// Sort by path, so that equal paths are next to each other. // Sort by path, so that equal paths are next to each other.
self.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path)); self.sort_by_path();
for idx in (1..self.dirs.len()).rev() { let mut dirty = false;
// Check if curr_dir and next_dir have equal paths. self.with_dirs_mut(|dirs| {
let curr_dir = &self.dirs[idx]; for idx in (1..dirs.len()).rev() {
let next_dir = &self.dirs[idx - 1]; // Check if curr_dir and next_dir have equal paths.
if next_dir.path != curr_dir.path { let curr_dir = &dirs[idx];
continue; let next_dir = &dirs[idx - 1];
} if next_dir.path != curr_dir.path {
continue;
// Merge curr_dir's rank and last_accessed into next_dir.
let rank = curr_dir.rank;
let last_accessed = curr_dir.last_accessed;
let next_dir = &mut self.dirs[idx - 1];
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
next_dir.rank += rank;
// Delete curr_dir.
self.dirs.swap_remove(idx);
self.modified = true;
}
}
// Streaming iterator for directories.
pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> {
Stream::new(self, now)
}
/// Removes the directory with `path` from the store. This does not preserve ordering, but is
/// O(1).
pub fn remove<S: AsRef<str>>(&mut self, path: S) -> bool {
let path = path.as_ref();
if let Some(idx) = self.dirs.iter().position(|dir| dir.path == path) {
self.dirs.swap_remove(idx);
self.modified = true;
return true;
}
false
}
pub fn age(&mut self, max_age: Rank) {
let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::<Rank>();
if sum_age > max_age {
let factor = 0.9 * max_age / sum_age;
for idx in (0..self.dirs.len()).rev() {
let dir = &mut self.dirs[idx];
dir.rank *= factor;
if dir.rank < 1.0 {
self.dirs.swap_remove(idx);
} }
}
self.modified = true; // Merge curr_dir's rank and last_accessed into next_dir.
let rank = curr_dir.rank;
let last_accessed = curr_dir.last_accessed;
let next_dir = &mut dirs[idx - 1];
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
next_dir.rank += rank;
// Delete curr_dir.
dirs.swap_remove(idx);
dirty = true;
}
});
self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty);
}
pub fn sort_by_path(&mut self) {
self.with_dirs_mut(|dirs| dirs.sort_unstable_by(|dir1, dir2| dir1.path.cmp(&dir2.path)));
self.with_dirty_mut(|dirty| *dirty = true);
}
pub fn sort_by_score(&mut self, now: Epoch) {
self.with_dirs_mut(|dirs| {
dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| {
dir1.score(now).total_cmp(&dir2.score(now))
})
});
self.with_dirty_mut(|dirty| *dirty = true);
}
pub fn dirty(&self) -> bool {
*self.borrow_dirty()
}
pub fn dirs(&self) -> &[Dir] {
self.borrow_dirs()
}
fn serialize(dirs: &[Dir<'_>]) -> Result<Vec<u8>> {
(|| -> bincode::Result<_> {
// Preallocate buffer with combined size of sections.
let buffer_size =
bincode::serialized_size(&Self::VERSION)? + bincode::serialized_size(&dirs)?;
let mut buffer = Vec::with_capacity(buffer_size as usize);
// Serialize sections into buffer.
bincode::serialize_into(&mut buffer, &Self::VERSION)?;
bincode::serialize_into(&mut buffer, &dirs)?;
Ok(buffer)
})()
.context("could not serialize database")
}
fn deserialize(bytes: &[u8]) -> Result<Vec<Dir>> {
// Assume a maximum size for the database. This prevents bincode from throwing
// strange errors when it encounters invalid data.
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
// Split bytes into sections.
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
if bytes.len() < version_size {
bail!("could not deserialize database: corrupted data");
} }
} let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
}
pub struct DatabaseFile { // Deserialize sections.
buffer: Vec<u8>, let version = deserializer.deserialize(bytes_version)?;
data_dir: PathBuf, let dirs = match version {
} Self::VERSION => {
deserializer.deserialize(bytes_dirs).context("could not deserialize database")?
impl DatabaseFile {
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() }
}
pub fn open(&mut self) -> Result<Database> {
// Read the entire database to memory. For smaller files, this is faster than
// mmap / streaming, and allows for zero-copy deserialization.
let path = db_path(&self.data_dir);
match fs::read(&path) {
Ok(buffer) => {
self.buffer = buffer;
let dirs = DirList::from_bytes(&self.buffer)
.with_context(|| format!("could not deserialize database: {}", path.display()))?;
Ok(Database { dirs, modified: false, data_dir: &self.data_dir })
} }
Err(e) if e.kind() == io::ErrorKind::NotFound => { version => {
// Create data directory, but don't create any file yet. The file will be created bail!("unsupported version (got {version}, supports {})", Self::VERSION)
// later by [`Database::save`] if any data is modified.
fs::create_dir_all(&self.data_dir)
.with_context(|| format!("unable to create data directory: {}", self.data_dir.display()))?;
Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir })
} }
Err(e) => Err(e).with_context(|| format!("could not read from database: {}", path.display())), };
}
}
}
fn db_path<P: AsRef<Path>>(data_dir: P) -> PathBuf { Ok(dirs)
const DB_FILENAME: &str = "db.zo"; }
data_dir.as_ref().join(DB_FILENAME)
} }
#[cfg(test)] #[cfg(test)]
@ -155,50 +240,49 @@ mod tests {
#[test] #[test]
fn add() { fn add() {
let data_dir = tempfile::tempdir().unwrap();
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
let now = 946684800; let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap(); db.add(path, 1.0, now);
db.add(path, now); db.add(path, 1.0, now);
db.add(path, now);
db.save().unwrap(); db.save().unwrap();
} }
{
let mut db = DatabaseFile::new(data_dir.path());
let db = db.open().unwrap();
assert_eq!(db.dirs.len(), 1);
let dir = &db.dirs[0]; {
let db = Database::open_dir(data_dir.path()).unwrap();
assert_eq!(db.dirs().len(), 1);
let dir = &db.dirs()[0];
assert_eq!(dir.path, path); assert_eq!(dir.path, path);
assert!((dir.rank - 2.0).abs() < 0.01);
assert_eq!(dir.last_accessed, now); assert_eq!(dir.last_accessed, now);
} }
} }
#[test] #[test]
fn remove() { fn remove() {
let data_dir = tempfile::tempdir().unwrap();
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
let now = 946684800; let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap(); db.add(path, 1.0, now);
db.add(path, now);
db.save().unwrap(); db.save().unwrap();
} }
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap();
assert!(db.remove(path)); assert!(db.remove(path));
db.save().unwrap(); db.save().unwrap();
} }
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap(); assert!(db.dirs().is_empty());
assert!(db.dirs.is_empty());
assert!(!db.remove(path)); assert!(!db.remove(path));
db.save().unwrap(); db.save().unwrap();
} }

View File

@ -2,36 +2,36 @@ use std::iter::Rev;
use std::ops::Range; use std::ops::Range;
use std::{fs, path}; use std::{fs, path};
use ordered_float::OrderedFloat; use crate::db::{Database, Dir, Epoch};
use crate::util::{self, MONTH};
use super::{Database, Dir, Epoch}; pub struct Stream<'a> {
use crate::util; // State
db: &'a mut Database,
pub struct Stream<'db, 'file> {
db: &'db mut Database<'file>,
idxs: Rev<Range<usize>>, idxs: Rev<Range<usize>>,
did_exclude: bool,
// Configuration
keywords: Vec<String>, keywords: Vec<String>,
check_exists: bool, check_exists: bool,
expire_below: Epoch, expire_below: Epoch,
resolve_symlinks: bool, resolve_symlinks: bool,
exclude_path: Option<String>, exclude_path: Option<String>,
} }
impl<'db, 'file> Stream<'db, 'file> { impl<'a> Stream<'a> {
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self { pub fn new(db: &'a mut Database, now: Epoch) -> Self {
// Iterate in descending order of score. db.sort_by_score(now);
db.dirs.sort_unstable_by_key(|dir| OrderedFloat(dir.score(now))); let idxs = (0..db.dirs().len()).rev();
let idxs = (0..db.dirs.len()).rev();
// If a directory is deleted and hasn't been used for 90 days, delete it from the database. // If a directory is deleted and hasn't been used for 3 months, delete
let expire_below = now.saturating_sub(90 * 24 * 60 * 60); // it from the database.
let expire_below = now.saturating_sub(3 * MONTH);
Stream { Stream {
db, db,
idxs, idxs,
did_exclude: false,
keywords: Vec::new(), keywords: Vec::new(),
check_exists: false, check_exists: false,
expire_below, expire_below,
@ -40,7 +40,7 @@ impl<'db, 'file> Stream<'db, 'file> {
} }
} }
pub fn with_exclude<S: Into<String>>(mut self, path: S) -> Self { pub fn with_exclude(mut self, path: impl Into<String>) -> Self {
self.exclude_path = Some(path.into()); self.exclude_path = Some(path.into());
self self
} }
@ -51,14 +51,14 @@ impl<'db, 'file> Stream<'db, 'file> {
self self
} }
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Self { pub fn with_keywords(mut self, keywords: &[impl AsRef<str>]) -> Self {
self.keywords = keywords.iter().map(util::to_lowercase).collect(); self.keywords = keywords.iter().map(util::to_lowercase).collect();
self self
} }
pub fn next(&mut self) -> Option<&Dir<'file>> { pub fn next(&mut self) -> Option<&Dir> {
while let Some(idx) = self.idxs.next() { while let Some(idx) = self.idxs.next() {
let dir = &self.db.dirs[idx]; let dir = &self.db.dirs()[idx];
if !self.matches_keywords(&dir.path) { if !self.matches_keywords(&dir.path) {
continue; continue;
@ -66,32 +66,36 @@ impl<'db, 'file> Stream<'db, 'file> {
if !self.matches_exists(&dir.path) { if !self.matches_exists(&dir.path) {
if dir.last_accessed < self.expire_below { if dir.last_accessed < self.expire_below {
self.db.dirs.swap_remove(idx); self.db.swap_remove(idx);
self.db.modified = true;
} }
continue; continue;
} }
if Some(dir.path.as_ref()) == self.exclude_path.as_deref() { if Some(dir.path.as_ref()) == self.exclude_path.as_deref() {
self.did_exclude = true;
continue; continue;
} }
let dir = &self.db.dirs[idx]; let dir = &self.db.dirs()[idx];
return Some(dir); return Some(dir);
} }
None None
} }
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool { pub fn did_exclude(&self) -> bool {
self.did_exclude
}
fn matches_exists(&self, path: &str) -> bool {
if !self.check_exists { if !self.check_exists {
return true; return true;
} }
let resolver = if self.resolve_symlinks { fs::symlink_metadata } else { fs::metadata }; let resolver = if self.resolve_symlinks { fs::symlink_metadata } else { fs::metadata };
resolver(path.as_ref()).map(|m| m.is_dir()).unwrap_or_default() resolver(path).map(|m| m.is_dir()).unwrap_or_default()
} }
fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool { fn matches_keywords(&self, path: &str) -> bool {
let (keywords_last, keywords) = match self.keywords.split_last() { let (keywords_last, keywords) = match self.keywords.split_last() {
Some(split) => split, Some(split) => split,
None => return true, None => return true,
@ -126,7 +130,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use super::Database; use super::*;
#[rstest] #[rstest]
// Case normalization // Case normalization
@ -149,8 +153,8 @@ mod tests {
#[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/bar", false)]
#[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
let mut db = Database { dirs: Vec::new().into(), modified: false, data_dir: &PathBuf::new() }; let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false);
let stream = db.stream(0).with_keywords(keywords); let stream = Stream::new(db, 0).with_keywords(keywords);
assert_eq!(is_match, stream.matches_keywords(path)); assert_eq!(is_match, stream.matches_keywords(path));
} }
} }

View File

@ -6,7 +6,7 @@ use anyhow::{bail, Context, Result};
/// Custom error type for early exit. /// Custom error type for early exit.
#[derive(Debug)] #[derive(Debug)]
pub struct SilentExit { pub struct SilentExit {
pub code: i32, pub code: u8,
} }
impl Display for SilentExit { impl Display for SilentExit {
@ -23,7 +23,7 @@ impl BrokenPipeHandler for io::Result<()> {
fn pipe_exit(self, device: &str) -> Result<()> { fn pipe_exit(self, device: &str) -> Result<()> {
match self { match self {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }),
result => result.with_context(|| format!("could not write to {}", device)), result => result.with_context(|| format!("could not write to {device}")),
} }
} }
} }

View File

@ -1,7 +1,7 @@
#![allow(clippy::single_component_path_imports)] #![allow(clippy::single_component_path_imports)]
// rstest_reuse must be imported at the top of the crate. // rstest_reuse must be imported at the top of the crate.
#[cfg(test)] #[cfg(all(test, feature = "nix-dev"))]
use rstest_reuse; use rstest_reuse;
mod cmd; mod cmd;
@ -11,26 +11,28 @@ mod error;
mod shell; mod shell;
mod util; mod util;
use std::env;
use std::io::{self, Write}; use std::io::{self, Write};
use std::{env, process}; use std::process::ExitCode;
use clap::Parser; use clap::Parser;
use crate::cmd::{Cmd, Run}; use crate::cmd::{Cmd, Run};
use crate::error::SilentExit; use crate::error::SilentExit;
pub fn main() { pub fn main() -> ExitCode {
// Forcibly disable backtraces. // Forcibly disable backtraces.
env::remove_var("RUST_LIB_BACKTRACE"); env::remove_var("RUST_LIB_BACKTRACE");
env::remove_var("RUST_BACKTRACE"); env::remove_var("RUST_BACKTRACE");
if let Err(e) = Cmd::parse().run() { match Cmd::parse().run() {
match e.downcast::<SilentExit>() { Ok(()) => ExitCode::SUCCESS,
Ok(SilentExit { code }) => process::exit(code), Err(e) => match e.downcast::<SilentExit>() {
Ok(SilentExit { code }) => code.into(),
Err(e) => { Err(e) => {
let _ = writeln!(io::stderr(), "zoxide: {:?}", e); _ = writeln!(io::stderr(), "zoxide: {e:?}");
process::exit(1); ExitCode::FAILURE
} }
} },
} }
} }

View File

@ -57,7 +57,7 @@ mod tests {
let opts = Opts { cmd, hook, echo, resolve_symlinks }; let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap(); let source = Bash(&opts).render().unwrap();
Command::new("bash") Command::new("bash")
.args(&["--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) .args(["--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -70,7 +70,7 @@ mod tests {
let source = Bash(&opts).render().unwrap(); let source = Bash(&opts).render().unwrap();
Command::new("shellcheck") Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"]) .args(["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source) .write_stdin(source)
.assert() .assert()
.success() .success()
@ -85,7 +85,7 @@ mod tests {
source.push('\n'); source.push('\n');
Command::new("shfmt") Command::new("shfmt")
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"]) .args(["--diff", "--indent=4", "--language-dialect=bash", "--simplify", "-"])
.write_stdin(source) .write_stdin(source)
.assert() .assert()
.success() .success()
@ -96,16 +96,21 @@ mod tests {
#[apply(opts)] #[apply(opts)]
fn elvish_elvish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { fn elvish_elvish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks }; let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = String::new(); let mut source = String::default();
// Filter out lines using edit:*, since those functions are only available in the // Filter out lines using edit:*, since those functions are only available in
// interactive editor. // the interactive editor.
for line in Elvish(&opts).render().unwrap().split('\n').filter(|line| !line.contains("edit:")) { for line in Elvish(&opts).render().unwrap().lines().filter(|line| !line.contains("edit:")) {
source.push_str(line); source.push_str(line);
source.push('\n'); source.push('\n');
} }
Command::new("elvish").args(&["-c", &source, "-norc"]).assert().success().stdout("").stderr(""); Command::new("elvish")
.args(["-c", &source, "-norc"])
.assert()
.success()
.stdout("")
.stderr("");
} }
#[apply(opts)] #[apply(opts)]
@ -118,7 +123,7 @@ mod tests {
Command::new("fish") Command::new("fish")
.env("HOME", tempdir) .env("HOME", tempdir)
.args(&["--command", &source, "--private"]) .args(["--command", &source, "--no-config", "--private"])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -149,10 +154,14 @@ mod tests {
let source = Nushell(&opts).render().unwrap(); let source = Nushell(&opts).render().unwrap();
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap(); let tempdir = tempdir.path();
let assert = let assert = Command::new("nu")
Command::new("nu").env("HOME", tempdir).args(&["--commands", &source]).assert().success().stderr(""); .env("HOME", tempdir)
.args(["--commands", &source])
.assert()
.success()
.stderr("");
if opts.hook != InitHook::Pwd { if opts.hook != InitHook::Pwd {
assert.stdout(""); assert.stdout("");
@ -165,7 +174,7 @@ mod tests {
let source = Posix(&opts).render().unwrap(); let source = Posix(&opts).render().unwrap();
let assert = Command::new("bash") let assert = Command::new("bash")
.args(&["--posix", "--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source]) .args(["--posix", "--noprofile", "--norc", "-e", "-u", "-o", "pipefail", "-c", &source])
.assert() .assert()
.success() .success()
.stderr(""); .stderr("");
@ -179,7 +188,8 @@ mod tests {
let opts = Opts { cmd, hook, echo, resolve_symlinks }; let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap(); let source = Posix(&opts).render().unwrap();
let assert = Command::new("dash").args(&["-e", "-u", "-c", &source]).assert().success().stderr(""); let assert =
Command::new("dash").args(["-e", "-u", "-c", &source]).assert().success().stderr("");
if opts.hook != InitHook::Pwd { if opts.hook != InitHook::Pwd {
assert.stdout(""); assert.stdout("");
} }
@ -191,7 +201,7 @@ mod tests {
let source = Posix(&opts).render().unwrap(); let source = Posix(&opts).render().unwrap();
Command::new("shellcheck") Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "sh", "-"]) .args(["--enable", "all", "--shell", "sh", "-"])
.write_stdin(source) .write_stdin(source)
.assert() .assert()
.success() .success()
@ -206,7 +216,7 @@ mod tests {
source.push('\n'); source.push('\n');
Command::new("shfmt") Command::new("shfmt")
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) .args(["--diff", "--indent=4", "--language-dialect=posix", "--simplify", "-"])
.write_stdin(source) .write_stdin(source)
.assert() .assert()
.success() .success()
@ -221,7 +231,7 @@ mod tests {
Powershell(&opts).render_into(&mut source).unwrap(); Powershell(&opts).render_into(&mut source).unwrap();
Command::new("pwsh") Command::new("pwsh")
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source]) .args(["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -234,7 +244,12 @@ mod tests {
let mut source = Xonsh(&opts).render().unwrap(); let mut source = Xonsh(&opts).render().unwrap();
source.push('\n'); source.push('\n');
Command::new("black").args(&["--check", "--diff", "-"]).write_stdin(source).assert().success().stdout(""); Command::new("black")
.args(["--check", "--diff", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("");
} }
#[apply(opts)] #[apply(opts)]
@ -242,7 +257,7 @@ mod tests {
let opts = Opts { cmd, hook, echo, resolve_symlinks }; let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Xonsh(&opts).render().unwrap(); let source = Xonsh(&opts).render().unwrap();
Command::new("mypy").args(&["--command", &source, "--strict"]).assert().success().stderr(""); Command::new("mypy").args(["--command", &source, "--strict"]).assert().success().stderr("");
} }
#[apply(opts)] #[apply(opts)]
@ -252,7 +267,7 @@ mod tests {
source.push('\n'); source.push('\n');
Command::new("pylint") Command::new("pylint")
.args(&["--from-stdin", "--persistent=n", "zoxide"]) .args(["--from-stdin", "--persistent=n", "zoxide"])
.write_stdin(source) .write_stdin(source)
.assert() .assert()
.success() .success()
@ -268,7 +283,7 @@ mod tests {
let tempdir = tempdir.path().to_str().unwrap(); let tempdir = tempdir.path().to_str().unwrap();
Command::new("xonsh") Command::new("xonsh")
.args(&["-c", &source, "--no-rc"]) .args(["-c", &source, "--no-rc"])
.env("HOME", tempdir) .env("HOME", tempdir)
.assert() .assert()
.success() .success()
@ -283,7 +298,7 @@ mod tests {
// ShellCheck doesn't support zsh yet: https://github.com/koalaman/shellcheck/issues/809 // ShellCheck doesn't support zsh yet: https://github.com/koalaman/shellcheck/issues/809
Command::new("shellcheck") Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"]) .args(["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source) .write_stdin(source)
.assert() .assert()
.success() .success()
@ -297,7 +312,7 @@ mod tests {
let source = Zsh(&opts).render().unwrap(); let source = Zsh(&opts).render().unwrap();
Command::new("zsh") Command::new("zsh")
.args(&["-e", "-u", "-o", "pipefail", "--no-globalrcs", "--no-rcs", "-c", &source]) .args(["-e", "-u", "-o", "pipefail", "--no-globalrcs", "--no-rcs", "-c", &source])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")

View File

@ -1,83 +1,149 @@
use std::env; use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::mem;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::process::Command; use std::process::{Child, Command, Stdio};
use std::process::{Child, ChildStdin, Stdio};
use std::time::SystemTime; use std::time::SystemTime;
use std::{env, mem};
use anyhow::{anyhow, bail, Context, Result}; #[cfg(windows)]
use anyhow::anyhow;
use anyhow::{bail, Context, Result};
use crate::config; use crate::db::{Dir, Epoch};
use crate::db::Epoch;
use crate::error::SilentExit; use crate::error::SilentExit;
pub struct Fzf { pub const SECOND: Epoch = 1;
child: Child, pub const MINUTE: Epoch = 60 * SECOND;
} pub const HOUR: Epoch = 60 * MINUTE;
pub const DAY: Epoch = 24 * HOUR;
pub const WEEK: Epoch = 7 * DAY;
pub const MONTH: Epoch = 30 * DAY;
pub struct Fzf(Command);
impl Fzf { impl Fzf {
const ERR_NOT_FOUND: &'static str = "could not find fzf, is it installed?"; const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?";
pub fn new(multiple: bool) -> Result<Self> { pub fn new() -> Result<Self> {
let bin = if cfg!(windows) { "fzf.exe" } else { "fzf" }; // On Windows, CreateProcess implicitly searches the current working
let mut command = get_command(bin).map_err(|_| anyhow!(Self::ERR_NOT_FOUND))?; // directory for the executable, which is a potential security issue.
if multiple { // Instead, we resolve the path to the executable and then pass it to
command.arg("-m"); // CreateProcess.
} #[cfg(windows)]
command.arg("-n2..").stdin(Stdio::piped()).stdout(Stdio::piped()); let program = which::which("fzf.exe").map_err(|_| anyhow!(Self::ERR_FZF_NOT_FOUND))?;
if let Some(fzf_opts) = config::fzf_opts() { #[cfg(not(windows))]
command.env("FZF_DEFAULT_OPTS", fzf_opts); let program = "fzf";
} else {
command.args(&[
// Search result
"--no-sort",
// Interface
"--keep-right",
// Layout
"--height=40%",
"--info=inline",
"--layout=reverse",
// Scripting
"--exit-0",
"--select-1",
// Key/Event bindings
"--bind=ctrl-z:ignore",
]);
if cfg!(unix) {
command.env("SHELL", "sh");
command.arg(r"--preview=\command -p ls -p {2..}");
}
}
let child = match command.spawn() { // TODO: check version of fzf here.
Ok(child) => child,
Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_NOT_FOUND),
Err(e) => Err(e).context("could not launch fzf")?,
};
Ok(Fzf { child }) let mut cmd = Command::new(program);
cmd.args([
// Search mode
"--delimiter=\t",
"--nth=2",
// Scripting
"--read0",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped());
Ok(Fzf(cmd))
} }
pub fn stdin(&mut self) -> &mut ChildStdin { pub fn enable_preview(&mut self) -> &mut Self {
self.child.stdin.as_mut().unwrap() // Previews are only supported on UNIX.
if !cfg!(unix) {
return self;
}
self.args([
// Non-POSIX args are only available on certain operating systems.
if cfg!(target_os = "linux") {
r"--preview=\command -p ls -Cp --color=always --group-directories-first {2..}"
} else {
r"--preview=\command -p ls -Cp {2..}"
},
// Rounded edges don't display correctly on some terminals.
"--preview-window=down,30%,sharp",
])
.envs([
// Enables colorized `ls` output on macOS / FreeBSD.
("CLICOLOR", "1"),
// Forces colorized `ls` output when the output is not a
// TTY (like in fzf's preview window) on macOS /
// FreeBSD.
("CLICOLOR_FORCE", "1"),
// Ensures that the preview command is run in a
// POSIX-compliant shell, regardless of what shell the
// user has selected.
("SHELL", "sh"),
])
} }
pub fn select(mut self) -> Result<String> { pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.0.args(args);
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.env(key, val);
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(vars);
self
}
pub fn spawn(&mut self) -> Result<FzfChild> {
match self.0.spawn() {
Ok(child) => Ok(FzfChild(child)),
Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_FZF_NOT_FOUND),
Err(e) => Err(e).context("could not launch fzf"),
}
}
}
pub struct FzfChild(Child);
impl FzfChild {
pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result<Option<String>> {
let handle = self.0.stdin.as_mut().unwrap();
match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) {
Ok(()) => Ok(None),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => self.wait().map(Some),
Err(e) => Err(e).context("could not write to fzf"),
}
}
pub fn wait(&mut self) -> Result<String> {
// Drop stdin to prevent deadlock. // Drop stdin to prevent deadlock.
mem::drop(self.child.stdin.take()); mem::drop(self.0.stdin.take());
let mut stdout = self.child.stdout.take().unwrap(); let mut stdout = self.0.stdout.take().unwrap();
let mut output = String::new(); let mut output = String::default();
stdout.read_to_string(&mut output).context("failed to read from fzf")?; stdout.read_to_string(&mut output).context("failed to read from fzf")?;
let status = self.child.wait().context("wait failed on fzf")?; let status = self.0.wait().context("wait failed on fzf")?;
match status.code() { match status.code() {
Some(0) => Ok(output), Some(0) => Ok(output),
Some(1) => bail!("no match found"), Some(1) => bail!("no match found"),
Some(2) => bail!("fzf returned an error"), Some(2) => bail!("fzf returned an error"),
Some(code @ 130) => bail!(SilentExit { code }), Some(130) => bail!(SilentExit { code: 130 }),
Some(128..=254) | None => bail!("fzf was terminated"), Some(128..=254) | None => bail!("fzf was terminated"),
_ => bail!("fzf returned an unknown error"), _ => bail!("fzf returned an unknown error"),
} }
@ -85,7 +151,7 @@ impl Fzf {
} }
/// Similar to [`fs::write`], but atomic (best effort on Windows). /// Similar to [`fs::write`], but atomic (best effort on Windows).
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
let path = path.as_ref(); let path = path.as_ref();
let contents = contents.as_ref(); let contents = contents.as_ref();
let dir = path.parent().unwrap(); let dir = path.parent().unwrap();
@ -94,19 +160,22 @@ pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
let (mut tmp_file, tmp_path) = tmpfile(dir)?; let (mut tmp_file, tmp_path) = tmpfile(dir)?;
let result = (|| { let result = (|| {
// Write to the tmpfile. // Write to the tmpfile.
let _ = tmp_file.set_len(contents.len() as u64); _ = tmp_file.set_len(contents.len() as u64);
tmp_file.write_all(contents).with_context(|| format!("could not write to file: {}", tmp_path.display()))?; tmp_file
.write_all(contents)
.with_context(|| format!("could not write to file: {}", tmp_path.display()))?;
// Set the owner of the tmpfile (UNIX only). // Set the owner of the tmpfile (UNIX only).
#[cfg(unix)] #[cfg(unix)]
if let Ok(metadata) = path.metadata() { if let Ok(metadata) = path.metadata() {
use nix::unistd::{self, Gid, Uid};
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use nix::unistd::{self, Gid, Uid};
let uid = Uid::from_raw(metadata.uid()); let uid = Uid::from_raw(metadata.uid());
let gid = Gid::from_raw(metadata.gid()); let gid = Gid::from_raw(metadata.gid());
let _ = unistd::fchown(tmp_file.as_raw_fd(), Some(uid), Some(gid)); _ = unistd::fchown(tmp_file.as_raw_fd(), Some(uid), Some(gid));
} }
// Close and rename the tmpfile. // Close and rename the tmpfile.
@ -115,13 +184,13 @@ pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>
})(); })();
// In case of an error, delete the tmpfile. // In case of an error, delete the tmpfile.
if result.is_err() { if result.is_err() {
let _ = fs::remove_file(&tmp_path); _ = fs::remove_file(&tmp_path);
} }
result result
} }
/// Atomically create a tmpfile in the given directory. /// Atomically create a tmpfile in the given directory.
fn tmpfile<P: AsRef<Path>>(dir: P) -> Result<(File, PathBuf)> { fn tmpfile(dir: impl AsRef<Path>) -> Result<(File, PathBuf)> {
const MAX_ATTEMPTS: usize = 5; const MAX_ATTEMPTS: usize = 5;
const TMP_NAME_LEN: usize = 16; const TMP_NAME_LEN: usize = 16;
let dir = dir.as_ref(); let dir = dir.as_ref();
@ -141,35 +210,39 @@ fn tmpfile<P: AsRef<Path>>(dir: P) -> Result<(File, PathBuf)> {
// Atomically create the tmpfile. // Atomically create the tmpfile.
match OpenOptions::new().write(true).create_new(true).open(&path) { match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => break Ok((file, path)), Ok(file) => break Ok((file, path)),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => (), Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => {}
Err(e) => break Err(e).with_context(|| format!("could not create file: {}", path.display())), Err(e) => {
break Err(e).with_context(|| format!("could not create file: {}", path.display()));
}
} }
} }
} }
/// Similar to [`fs::rename`], but retries on Windows. /// Similar to [`fs::rename`], but with retries on Windows.
fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> { fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
const MAX_ATTEMPTS: usize = 5;
let from = from.as_ref(); let from = from.as_ref();
let to = to.as_ref(); let to = to.as_ref();
if cfg!(windows) { const MAX_ATTEMPTS: usize = if cfg!(windows) { 5 } else { 1 };
let mut attempts = 0; let mut attempts = 0;
loop {
attempts += 1; loop {
match fs::rename(from, to) { match fs::rename(from, to) {
Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => (), Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => {
result => break result, attempts += 1
}
result => {
break result.with_context(|| {
format!("could not rename file: {} -> {}", from.display(), to.display())
});
} }
} }
} else {
fs::rename(from, to)
} }
.with_context(|| format!("could not rename file: {} -> {}", from.display(), to.display()))
} }
pub fn canonicalize<P: AsRef<Path>>(path: &P) -> Result<PathBuf> { pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
dunce::canonicalize(path).with_context(|| format!("could not resolve path: {}", path.as_ref().display())) dunce::canonicalize(&path)
.with_context(|| format!("could not resolve path: {}", path.as_ref().display()))
} }
pub fn current_dir() -> Result<PathBuf> { pub fn current_dir() -> Result<PathBuf> {
@ -177,49 +250,22 @@ pub fn current_dir() -> Result<PathBuf> {
} }
pub fn current_time() -> Result<Epoch> { pub fn current_time() -> Result<Epoch> {
let current_time = let current_time = SystemTime::now()
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).context("system clock set to invalid time")?.as_secs(); .duration_since(SystemTime::UNIX_EPOCH)
.context("system clock set to invalid time")?
.as_secs();
Ok(current_time) Ok(current_time)
} }
/// Constructs a new [`Command`] for launching the program with the name pub fn path_to_str(path: &impl AsRef<Path>) -> Result<&str> {
/// `program`.
///
/// On Windows, CreateProcess implicitly searches the current working directory
/// for the executable, which is a potential security issue. Instead, we resolve
/// the path to the executable and then pass it to CreateProcess.
///
/// On other platforms, this is a no-op.
///
pub fn get_command<P: AsRef<Path>>(program: P) -> Result<Command> {
let program = program.as_ref();
if !cfg!(windows) {
return Ok(Command::new(program));
}
let paths = env::var_os("PATH").context("PATH environment variable not set")?;
for path in env::split_paths(&paths) {
if path.as_os_str().is_empty() {
continue;
}
let path = path.join(program);
if path.metadata().map_or(false, |m| !m.is_dir()) {
return Ok(Command::new(path));
}
}
bail!("executable not found in PATH: {}", program.display());
}
pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
let path = path.as_ref(); let path = path.as_ref();
path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display())) path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display()))
} }
/// Returns the absolute version of a path. Like [`std::path::Path::canonicalize`], but doesn't /// Returns the absolute version of a path. Like
/// resolve symlinks. /// [`std::path::Path::canonicalize`], but doesn't resolve symlinks.
pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> { pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
let path = path.as_ref(); let path = path.as_ref();
let base_path; let base_path;
@ -230,13 +276,15 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
if cfg!(windows) { if cfg!(windows) {
use std::path::Prefix; use std::path::Prefix;
fn get_drive_letter<P: AsRef<Path>>(path: P) -> Option<u8> { fn get_drive_letter(path: impl AsRef<Path>) -> Option<u8> {
let path = path.as_ref(); let path = path.as_ref();
let mut components = path.components(); let mut components = path.components();
match components.next() { match components.next() {
Some(Component::Prefix(prefix)) => match prefix.kind() { Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::Disk(drive_letter) | Prefix::VerbatimDisk(drive_letter) => Some(drive_letter), Prefix::Disk(drive_letter) | Prefix::VerbatimDisk(drive_letter) => {
Some(drive_letter)
}
_ => None, _ => None,
}, },
_ => None, _ => None,
@ -289,8 +337,9 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
components.next(); components.next();
let current_dir = env::current_dir()?; let current_dir = env::current_dir()?;
let drive_letter = get_drive_letter(&current_dir) let drive_letter = get_drive_letter(&current_dir).with_context(|| {
.with_context(|| format!("could not get drive letter: {}", current_dir.display()))?; format!("could not get drive letter: {}", current_dir.display())
})?;
base_path = get_drive_path(drive_letter); base_path = get_drive_path(drive_letter);
stack.extend(base_path.components()); stack.extend(base_path.components());
} }
@ -310,7 +359,7 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
for component in components { for component in components {
match component { match component {
Component::Normal(_) => stack.push(component), Component::Normal(_) => stack.push(component),
Component::CurDir => (), Component::CurDir => {}
Component::ParentDir => { Component::ParentDir => {
if stack.last() != Some(&Component::RootDir) { if stack.last() != Some(&Component::RootDir) {
stack.pop(); stack.pop();
@ -324,11 +373,7 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
} }
/// Convert a string to lowercase, with a fast path for ASCII strings. /// Convert a string to lowercase, with a fast path for ASCII strings.
pub fn to_lowercase<S: AsRef<str>>(s: S) -> String { pub fn to_lowercase(s: impl AsRef<str>) -> String {
let s = s.as_ref(); let s = s.as_ref();
if s.is_ascii() { if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() }
s.to_ascii_lowercase()
} else {
s.to_lowercase()
}
} }

View File

@ -7,7 +7,9 @@
# pwd based on the value of _ZO_RESOLVE_SYMLINKS. # pwd based on the value of _ZO_RESOLVE_SYMLINKS.
function __zoxide_pwd() { function __zoxide_pwd() {
{%- if resolve_symlinks %} {%- if cfg!(windows) %}
\command cygpath -w "$(\builtin pwd -P)"
{%- else if resolve_symlinks %}
\builtin pwd -P \builtin pwd -P
{%- else %} {%- else %}
\builtin pwd -L \builtin pwd -L
@ -32,7 +34,8 @@ function __zoxide_cd() {
{%- if hook == InitHook::Prompt %} {%- if hook == InitHook::Prompt %}
function __zoxide_hook() { function __zoxide_hook() {
\builtin local -r retval="$?" \builtin local -r retval="$?"
\command zoxide add -- "$(__zoxide_pwd || \builtin true)" # shellcheck disable=SC2312
\command zoxide add -- "$(__zoxide_pwd)"
return "${retval}" return "${retval}"
} }
{%- else if hook == InitHook::Pwd %} {%- else if hook == InitHook::Pwd %}
@ -58,8 +61,7 @@ fi
{% endif -%} {% endif -%}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
__zoxide_z_prefix='z#' __zoxide_z_prefix='z#'
@ -73,13 +75,14 @@ function __zoxide_z() {
__zoxide_cd "${OLDPWD}" __zoxide_cd "${OLDPWD}"
elif [[ $# -eq 1 && -d $1 ]]; then elif [[ $# -eq 1 && -d $1 ]]; then
__zoxide_cd "$1" __zoxide_cd "$1"
elif [[ ${@: -1} == "${__zoxide_z_prefix}"* ]]; then elif [[ ${@: -1} == "${__zoxide_z_prefix}"?* ]]; then
# shellcheck disable=SC2124 # shellcheck disable=SC2124
\builtin local result="${@: -1}" \builtin local result="${@: -1}"
__zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}" __zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}"
else else
\builtin local result \builtin local result
result="$(\command zoxide query --exclude "$(__zoxide_pwd || \builtin true)" -- "$@")" && # shellcheck disable=SC2312
result="$(\command zoxide query --exclude "$(__zoxide_pwd)" -- "$@")" &&
__zoxide_cd "${result}" __zoxide_cd "${result}"
fi fi
} }
@ -87,29 +90,22 @@ function __zoxide_z() {
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
function __zoxide_zi() { function __zoxide_zi() {
\builtin local result \builtin local result
result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${result}" result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${result}"
} }
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
{%- when Some with (cmd) %} {%- when Some with (cmd) %}
# Remove definitions. \builtin unalias {{cmd}} &>/dev/null || \builtin true
function __zoxide_unset() {
\builtin unset -f "$@" &>/dev/null
\builtin unset -v "$@" &>/dev/null
\builtin unalias "$@" &>/dev/null || \builtin :
}
__zoxide_unset {{cmd}}
function {{cmd}}() { function {{cmd}}() {
__zoxide_z "$@" __zoxide_z "$@"
} }
__zoxide_unset {{cmd}}i \builtin unalias {{cmd}}i &>/dev/null || \builtin true
function {{cmd}}i() { function {{cmd}}i() {
__zoxide_zi "$@" __zoxide_zi "$@"
} }
@ -130,18 +126,21 @@ if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VER
# If there is only one argument, use `cd` completions. # If there is only one argument, use `cd` completions.
if [[ {{ "${#COMP_WORDS[@]}" }} -eq 2 ]]; then if [[ {{ "${#COMP_WORDS[@]}" }} -eq 2 ]]; then
\builtin mapfile -t COMPREPLY < \ \builtin mapfile -t COMPREPLY < <(
<(\builtin compgen -A directory -S / -- "${COMP_WORDS[-1]}" || \builtin true) \builtin compgen -A directory -- "${COMP_WORDS[-1]}" || \builtin true
)
# If there is a space after the last word, use interactive selection. # If there is a space after the last word, use interactive selection.
elif [[ -z ${COMP_WORDS[-1]} ]]; then elif [[ -z ${COMP_WORDS[-1]} ]] && [[ ${COMP_WORDS[-2]} != "${__zoxide_z_prefix}"?* ]]; then
\builtin local result \builtin local result
result="$(\command zoxide query -i -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" && # shellcheck disable=SC2312
COMPREPLY=("${__zoxide_z_prefix}${result@Q}") result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" &&
COMPREPLY=("${__zoxide_z_prefix}${result}/")
\builtin printf '\e[5n' \builtin printf '\e[5n'
fi fi
} }
\builtin complete -F __zoxide_z_complete -o nospace -- {{cmd}} \builtin complete -F __zoxide_z_complete -o filenames -- {{cmd}}
\builtin complete -r {{cmd}}i &>/dev/null || \builtin true
fi fi
{%- when None %} {%- when None %}

View File

@ -41,8 +41,7 @@ if (builtin:not (builtin:eq $E:__zoxide_shlvl $E:SHLVL)) {
{%- endif %} {%- endif %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
# Jump to a directory using only keywords. # Jump to a directory using only keywords.
@ -57,7 +56,7 @@ fn __zoxide_z {|@rest|
var path var path
try { try {
set path = (zoxide query --exclude $pwd -- $@rest) set path = (zoxide query --exclude $pwd -- $@rest)
} except { } catch {
} else { } else {
__zoxide_cd $path __zoxide_cd $path
} }
@ -69,8 +68,8 @@ edit:add-var __zoxide_z~ $__zoxide_z~
fn __zoxide_zi {|@rest| fn __zoxide_zi {|@rest|
var path var path
try { try {
set path = (zoxide query -i -- $@rest) set path = (zoxide query --interactive -- $@rest)
} except { } catch {
} else { } else {
__zoxide_cd $path __zoxide_cd $path
} }
@ -78,7 +77,7 @@ fn __zoxide_zi {|@rest|
edit:add-var __zoxide_zi~ $__zoxide_zi~ edit:add-var __zoxide_zi~ $__zoxide_zi~
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
@ -88,8 +87,10 @@ edit:add-var {{cmd}}~ $__zoxide_z~
edit:add-var {{cmd}}i~ $__zoxide_zi~ edit:add-var {{cmd}}i~ $__zoxide_zi~
# Load completions. # Load completions.
{# zoxide-based completions are currently not possible, because Elvish only {#-
# prints a completion if the current token is a prefix of it. -#} zoxide-based completions are currently not possible, because Elvish only prints
a completion if the current token is a prefix of it.
#}
fn __zoxide_z_complete {|@rest| fn __zoxide_z_complete {|@rest|
if (!= (builtin:count $rest) 2) { if (!= (builtin:count $rest) 2) {
builtin:return builtin:return
@ -116,4 +117,4 @@ set edit:completion:arg-completer[{{cmd}}] = $__zoxide_z_complete~
# #
# eval (zoxide init elvish | slurp) # eval (zoxide init elvish | slurp)
# #
# Note: zoxide only supports elvish v0.17.0 and above. # Note: zoxide only supports elvish v0.18.0 and above.

View File

@ -7,7 +7,9 @@
# pwd based on the value of _ZO_RESOLVE_SYMLINKS. # pwd based on the value of _ZO_RESOLVE_SYMLINKS.
function __zoxide_pwd function __zoxide_pwd
{%- if resolve_symlinks %} {%- if cfg!(windows) %}
command cygpath -w (builtin pwd -P)
{%- else if resolve_symlinks %}
builtin pwd -P builtin pwd -P
{%- else %} {%- else %}
builtin pwd -L builtin pwd -L
@ -16,9 +18,9 @@ end
# A copy of fish's internal cd function. This makes it possible to use # A copy of fish's internal cd function. This makes it possible to use
# `alias cd=z` without causing an infinite loop. # `alias cd=z` without causing an infinite loop.
if ! builtin functions -q __zoxide_cd_internal if ! builtin functions --query __zoxide_cd_internal
if builtin functions -q cd if builtin functions --query cd
builtin functions -c cd __zoxide_cd_internal builtin functions --copy cd __zoxide_cd_internal
else else
alias __zoxide_cd_internal='builtin cd' alias __zoxide_cd_internal='builtin cd'
end end
@ -26,7 +28,11 @@ end
# cd + custom logic based on the value of _ZO_ECHO. # cd + custom logic based on the value of _ZO_ECHO.
function __zoxide_cd function __zoxide_cd
{%- if cfg!(windows) %}
__zoxide_cd_internal (cygpath -u $argv)
{%- else %}
__zoxide_cd_internal $argv __zoxide_cd_internal $argv
{%- endif %}
{%- if echo %} {%- if echo %}
and __zoxide_pwd and __zoxide_pwd
{%- endif %} {%- endif %}
@ -53,68 +59,67 @@ end
{%- endif %} {%- endif %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
if test -z $__zoxide_z_prefix
set __zoxide_z_prefix 'z!'
end
set __zoxide_z_prefix_regex ^(string escape --style=regex $__zoxide_z_prefix)
# Jump to a directory using only keywords. # Jump to a directory using only keywords.
function __zoxide_z function __zoxide_z
set argc (count $argv) set -l argc (count $argv)
if test $argc -eq 0 if test $argc -eq 0
__zoxide_cd $HOME __zoxide_cd $HOME
else if test "$argv" = - else if test "$argv" = -
__zoxide_cd - __zoxide_cd -
else if test $argc -eq 1 -a -d $argv[1] else if test $argc -eq 1 -a -d $argv[1]
__zoxide_cd $argv[1] __zoxide_cd $argv[1]
else if set -l result (string replace --regex $__zoxide_z_prefix_regex '' $argv[-1]); and test -n $result
__zoxide_cd $result
else else
set -l result (command zoxide query --exclude (__zoxide_pwd) -- $argv) set -l result (command zoxide query --exclude (__zoxide_pwd) -- $argv)
and __zoxide_cd $result and __zoxide_cd $result
end end
end end
# Completions for `z`. # Completions.
function __zoxide_z_complete function __zoxide_z_complete
set -l tokens (commandline --current-process --tokenize) set -l tokens (commandline --current-process --tokenize)
set -l curr_tokens (commandline --cut-at-cursor --current-process --tokenize) set -l curr_tokens (commandline --cut-at-cursor --current-process --tokenize)
if test (count $tokens) -le 2 -a (count $curr_tokens) -eq 1 if test (count $tokens) -le 2 -a (count $curr_tokens) -eq 1
# If there are < 2 arguments, use `cd` completions. # If there are < 2 arguments, use `cd` completions.
__fish_complete_directories "$tokens[2]" '' complete --do-complete "'' "(commandline --cut-at-cursor --current-token) | string match --regex '.*/$'
else if test (count $tokens) -eq (count $curr_tokens) else if test (count $tokens) -eq (count $curr_tokens); and ! string match --quiet --regex $__zoxide_z_prefix_regex. $tokens[-1]
# If the last argument is empty, use interactive selection. # If the last argument is empty and the one before doesn't start with
# $__zoxide_z_prefix, use interactive selection.
set -l query $tokens[2..-1] set -l query $tokens[2..-1]
set -l result (zoxide query -i -- $query) set -l result (zoxide query --exclude (__zoxide_pwd) --interactive -- $query)
commandline --current-process "$tokens[1] "(string escape $result) and echo $__zoxide_z_prefix$result
commandline --function repaint commandline --function repaint
end end
end end
complete --command __zoxide_z --no-files --arguments '(__zoxide_z_complete)'
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
function __zoxide_zi function __zoxide_zi
set -l result (command zoxide query -i -- $argv) set -l result (command zoxide query --interactive -- $argv)
and __zoxide_cd $result and __zoxide_cd $result
end end
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
{%- when Some with (cmd) %} {%- when Some with (cmd) %}
# Remove definitions. abbr --erase {{cmd}} &>/dev/null
function __zoxide_unset
set --erase $argv >/dev/null 2>&1
abbr --erase $argv >/dev/null 2>&1
builtin functions --erase $argv >/dev/null 2>&1
end
__zoxide_unset {{cmd}}
alias {{cmd}}=__zoxide_z alias {{cmd}}=__zoxide_z
complete -c {{cmd}} -e
complete -c {{cmd}} -f -a '(__zoxide_z_complete)'
__zoxide_unset {{cmd}}i abbr --erase {{cmd}}i &>/dev/null
alias {{cmd}}i=__zoxide_zi alias {{cmd}}i=__zoxide_zi
{%- when None %} {%- when None %}
@ -127,4 +132,4 @@ alias {{cmd}}i=__zoxide_zi
# To initialize zoxide, add this to your configuration (usually # To initialize zoxide, add this to your configuration (usually
# ~/.config/fish/config.fish): # ~/.config/fish/config.fish):
# #
# zoxide init fish | source # zoxide init fish | source

View File

@ -3,90 +3,65 @@
# Code generated by zoxide. DO NOT EDIT. # Code generated by zoxide. DO NOT EDIT.
{{ section }}
# Utility functions for zoxide.
#
# Default prompt for Nushell.
def __zoxide_prompt [] {
let git = $'(do -i {git rev-parse --abbrev-ref HEAD} | str trim -rc (char newline))'
let git = (if ($git | str length) == 0 { '' } {
build-string (char lparen) (ansi cb) $git (ansi reset) (char rparen)
})
build-string (ansi gb) (pwd) (ansi reset) $git '> '
}
{{ section }} {{ section }}
# Hook configuration for zoxide. # Hook configuration for zoxide.
# #
# Hook to add new entries to the database. {% if hook == InitHook::None -%}
{%- match hook %}
{%- when InitHook::None %}
{{ not_configured }} {{ not_configured }}
{%- when InitHook::Prompt %} {%- else -%}
def __zoxide_hook [] { # Initialize hook to add new entries to the database.
shells | where active == $true && name == filesystem | get path | each { if (not ($env | default false __zoxide_hooked | get __zoxide_hooked)) {
zoxide add -- $it $env.__zoxide_hooked = true
} {%- if hook == InitHook::Prompt %}
$env.config = ($env | default {} config).config
$env.config = ($env.config | default {} hooks)
$env.config = ($env.config | update hooks ($env.config.hooks | default [] pre_prompt))
$env.config = ($env.config | update hooks.pre_prompt ($env.config.hooks.pre_prompt | append { ||
zoxide add -- $env.PWD
}))
{%- else if hook == InitHook::Pwd %}
$env.config = ($env | default {} config).config
$env.config = ($env.config | default {} hooks)
$env.config = ($env.config | update hooks ($env.config.hooks | default {} env_change))
$env.config = ($env.config | update hooks.env_change ($env.config.hooks.env_change | default [] PWD))
$env.config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.env_change.PWD | append {|_, dir|
zoxide add -- $dir
}))
{%- endif %}
} }
# Initialize hook. {%- endif %}
let-env PROMPT_COMMAND = (
let prompt = (if ($nu.env | select PROMPT_COMMAND | empty?) {
if ($nu.config | select prompt | empty?) { '__zoxide_prompt' } { $nu.config.prompt }
} { $nu.env.PROMPT_COMMAND });
if ($prompt | str contains '__zoxide_hook') { $prompt } { $'__zoxide_hook;($prompt)' }
)
{%- when InitHook::Pwd %}
$'zoxide: PWD hooks are not supported on Nushell.(char nl)Use (char sq)zoxide init nushell --hook prompt(char sq) instead.(char nl)'
{%- endmatch %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
# Jump to a directory using only keywords. # Jump to a directory using only keywords.
def __zoxide_z [...rest:string] { def-env __zoxide_z [...rest:string] {
if (shells | where active == $true | get name) != filesystem { let arg0 = ($rest | append '~').0
if ($rest | length) > 1 { let path = if (($rest | length) <= 1) and ($arg0 == '-' or ($arg0 | path expand | path type) == dir) {
$'zoxide: can only jump directories on filesystem(char nl)' $arg0
} { } else {
cd $rest (zoxide query --exclude $env.PWD -- $rest | str trim -r -c "\n")
}
cd $path
{%- if echo %} {%- if echo %}
pwd echo $env.PWD
{%- endif %} {%- endif %}
}
} {
let arg0 = ($rest | append '~' | first 1);
if ($rest | length) <= 1 && ($arg0 == '-' || ($arg0 | path expand | path exists)) {
cd $arg0
} {
cd $'(zoxide query --exclude (pwd) -- $rest | str trim -rc (char newline))'
}
{%- if echo %}
pwd
{%- endif %}
}
} }
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
def __zoxide_zi [...rest:string] { def-env __zoxide_zi [...rest:string] {
if (shells | where active == $true | get name) != filesystem { cd $'(zoxide query --interactive -- $rest | str trim -r -c "\n")'
$'zoxide: can only jump directories on filesystem(char nl)'
} {
cd $'(zoxide query -i -- $rest | str trim -rc (char newline))'
{%- if echo %} {%- if echo %}
pwd echo $env.PWD
{%- endif %} {%- endif %}
}
} }
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
@ -102,9 +77,13 @@ alias {{cmd}}i = __zoxide_zi
{%- endmatch %} {%- endmatch %}
{{ section }} {{ section }}
# To initialize zoxide, add this to your configuration (find it by running # Add this to your env file (find it by running `$nu.env-path` in Nushell):
# `config path` in Nushell):
# #
# startup = ['zoxide init nushell --hook prompt | save ~/.zoxide.nu', 'source ~/.zoxide.nu'] # zoxide init nushell | save -f ~/.zoxide.nu
# #
# Note: zoxide only supports Nushell v0.37.0 and above. # Now, add this to the end of your config file (find it by running
# `$nu.config-path` in Nushell):
#
# source ~/.zoxide.nu
#
# Note: zoxide only supports Nushell v0.73.0 and above.

View File

@ -7,7 +7,9 @@
# pwd based on the value of _ZO_RESOLVE_SYMLINKS. # pwd based on the value of _ZO_RESOLVE_SYMLINKS.
__zoxide_pwd() { __zoxide_pwd() {
{%- if resolve_symlinks %} {%- if cfg!(windows) %}
\command cygpath -w "$(\builtin pwd -P)"
{%- else if resolve_symlinks %}
\command pwd -P \command pwd -P
{%- else %} {%- else %}
\command pwd -L \command pwd -L
@ -47,8 +49,7 @@ fi
{%- endmatch %} {%- endmatch %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
# Jump to a directory using only keywords. # Jump to a directory using only keywords.
@ -73,30 +74,22 @@ __zoxide_z() {
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
__zoxide_zi() { __zoxide_zi() {
__zoxide_result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${__zoxide_result}" __zoxide_result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${__zoxide_result}"
} }
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
{%- when Some with (cmd) %} {%- when Some with (cmd) %}
# Remove definitions. \command unalias {{cmd}} >/dev/null 2>&1 || \true
__zoxide_unset() {
\command unset -f "$@" >/dev/null 2>&1
\command unset -v "$@" >/dev/null 2>&1
# shellcheck disable=SC1001
\command unalias "$@" >/dev/null 2>&1 || \:
}
__zoxide_unset '{{cmd}}'
{{cmd}}() { {{cmd}}() {
__zoxide_z "$@" __zoxide_z "$@"
} }
__zoxide_unset '{{cmd}}i' \command unalias {{cmd}}i >/dev/null 2>&1 || \true
{{cmd}}i() { {{cmd}}i() {
__zoxide_zi "$@" __zoxide_zi "$@"
} }

View File

@ -5,8 +5,20 @@
# Utility functions for zoxide. # Utility functions for zoxide.
# #
# Call zoxide binary, returning the output as UTF-8.
function global:__zoxide_bin {
$encoding = [Console]::OutputEncoding
try {
[Console]::OutputEncoding = [System.Text.Utf8Encoding]::new()
$result = zoxide @args
return $result
} finally {
[Console]::OutputEncoding = $encoding
}
}
# pwd based on zoxide's format. # pwd based on zoxide's format.
function __zoxide_pwd { function global:__zoxide_pwd {
$cwd = Get-Location $cwd = Get-Location
if ($cwd.Provider.Name -eq "FileSystem") { if ($cwd.Provider.Name -eq "FileSystem") {
$cwd.ProviderPath $cwd.ProviderPath
@ -14,11 +26,19 @@ function __zoxide_pwd {
} }
# cd + custom logic based on the value of _ZO_ECHO. # cd + custom logic based on the value of _ZO_ECHO.
function __zoxide_cd($dir, $literal) { function global:__zoxide_cd($dir, $literal) {
$dir = if ($literal) { $dir = if ($literal) {
Set-Location -LiteralPath $dir -Passthru -ErrorAction Stop Set-Location -LiteralPath $dir -Passthru -ErrorAction Stop
} else { } else {
Set-Location -Path $dir -Passthru -ErrorAction Stop if ($dir -eq '-' -and ($PSVersionTable.PSVersion -lt 6.1)) {
Write-Error "cd - is not supported below PowerShell 6.1. Please upgrade your version of PowerShell."
}
elseif ($dir -eq '+' -and ($PSVersionTable.PSVersion -lt 6.2)) {
Write-Error "cd + is not supported below PowerShell 6.2. Please upgrade your version of PowerShell."
}
else {
Set-Location -Path $dir -Passthru -ErrorAction Stop
}
} }
{%- if echo %} {%- if echo %}
Write-Output $dir.Path Write-Output $dir.Path
@ -29,66 +49,73 @@ function __zoxide_cd($dir, $literal) {
# Hook configuration for zoxide. # Hook configuration for zoxide.
# #
{% if hook == InitHook::None -%}
{{ not_configured }}
{%- else -%}
{#-
Initialize $__zoxide_hooked if it does not exist. Removing this will cause an
unset variable error in StrictMode.
-#}
{%- if hook == InitHook::Prompt -%}
# Hook to add new entries to the database. # Hook to add new entries to the database.
function __zoxide_hook { function global:__zoxide_hook {
$result = __zoxide_pwd $result = __zoxide_pwd
if ($null -ne $result) { if ($null -ne $result) {
zoxide add -- $result zoxide add -- $result
} }
} }
{%- else if hook == InitHook::Pwd -%}
# Hook to add new entries to the database.
$global:__zoxide_oldpwd = __zoxide_pwd
function global:__zoxide_hook {
$result = __zoxide_pwd
if ($result -ne $global:__zoxide_oldpwd) {
if ($null -ne $result) {
zoxide add -- $result
}
$global:__zoxide_oldpwd = $result
}
}
{%- endif %}
# Initialize hook. # Initialize hook.
{# Initialize $__zoxide_hooked if it does not exist. Removing this will cause $global:__zoxide_hooked = (Get-Variable __zoxide_hooked -ErrorAction SilentlyContinue -ValueOnly)
# an unset variable error in StrictMode. #} if ($global:__zoxide_hooked -ne 1) {
$__zoxide_hooked = (Get-Variable __zoxide_hooked -ValueOnly -ErrorAction SilentlyContinue) $global:__zoxide_hooked = 1
if ($__zoxide_hooked -ne 1) { $global:__zoxide_prompt_old = $function:prompt
$__zoxide_hooked = 1
{%- match hook %} function global:prompt {
{%- when InitHook::None %} if ($null -ne $__zoxide_prompt_old) {
{{ not_configured }} & $__zoxide_prompt_old
{%- when InitHook::Prompt %}
$prompt_old = $function:prompt
function prompt {
$null = __zoxide_hook
& $prompt_old
}
{%- when InitHook::Pwd %}
if ($PSVersionTable.PSVersion.Major -ge 6) {
$ExecutionContext.InvokeCommand.LocationChangedAction = {
$null = __zoxide_hook
} }
$null = __zoxide_hook
} }
else {
Write-Error ("`n" +
"zoxide: PWD hooks are not supported below powershell 6.`n" +
" Use 'zoxide init powershell --hook prompt' instead.")
}
{%- endmatch %}
} }
{%- endif %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
# Jump to a directory using only keywords. # Jump to a directory using only keywords.
function __zoxide_z { function global:__zoxide_z {
if ($args.Length -eq 0) { if ($args.Length -eq 0) {
__zoxide_cd ~ $true __zoxide_cd ~ $true
} }
elseif ( elseif ($args.Length -eq 1 -and ($args[0] -eq '-' -or $args[0] -eq '+')) {
$args.Length -eq 1 -and
(($args[0] -eq '-' -or $args[0] -eq '+') -or (Test-Path $args[0] -PathType Container))
) {
__zoxide_cd $args[0] $false __zoxide_cd $args[0] $false
} }
elseif ($args.Length -eq 1 -and (Test-Path $args[0] -PathType Container)) {
__zoxide_cd $args[0] $true
}
else { else {
$result = __zoxide_pwd $result = __zoxide_pwd
if ($null -ne $result) { if ($null -ne $result) {
$result = zoxide query --exclude $result -- @args $result = __zoxide_bin query --exclude $result -- @args
} }
else { else {
$result = zoxide query -- @args $result = __zoxide_bin query -- @args
} }
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
__zoxide_cd $result $true __zoxide_cd $result $true
@ -97,15 +124,15 @@ function __zoxide_z {
} }
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
function __zoxide_zi { function global:__zoxide_zi {
$result = zoxide query -i -- @args $result = __zoxide_bin query -i -- @args
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
__zoxide_cd $result $true __zoxide_cd $result $true
} }
} }
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
@ -124,4 +151,4 @@ Set-Alias -Name {{cmd}}i -Value __zoxide_zi -Option AllScope -Scope Global -Forc
# To initialize zoxide, add this to your configuration (find it by running # To initialize zoxide, add this to your configuration (find it by running
# `echo $profile` in PowerShell): # `echo $profile` in PowerShell):
# #
# Invoke-Expression (& { $hook = if ($PSVersionTable.PSVersion.Major -ge 6) { 'pwd' } else { 'prompt' } (zoxide init powershell --hook $hook | Out-String) }) # Invoke-Expression (& { (zoxide init powershell | Out-String) })

View File

@ -108,8 +108,7 @@ if "__zoxide_hook" not in globals():
{% endif %} {% endif %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
@ -157,7 +156,7 @@ def __zoxide_zi(args: typing.List[str]) -> None:
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}

View File

@ -7,7 +7,9 @@
# pwd based on the value of _ZO_RESOLVE_SYMLINKS. # pwd based on the value of _ZO_RESOLVE_SYMLINKS.
function __zoxide_pwd() { function __zoxide_pwd() {
{%- if resolve_symlinks %} {%- if cfg!(windows) %}
\command cygpath -w "$(\builtin pwd -P)"
{%- else if resolve_symlinks %}
\builtin pwd -P \builtin pwd -P
{%- else %} {%- else %}
\builtin pwd -L \builtin pwd -L
@ -30,7 +32,8 @@ function __zoxide_cd() {
{% else -%} {% else -%}
# Hook to add new entries to the database. # Hook to add new entries to the database.
function __zoxide_hook() { function __zoxide_hook() {
\command zoxide add -- "$(__zoxide_pwd || \builtin true)" # shellcheck disable=SC2312
\command zoxide add -- "$(__zoxide_pwd)"
} }
# Initialize hook. # Initialize hook.
@ -46,8 +49,7 @@ fi
{%- endif %} {%- endif %}
{{ section }} {{ section }}
# When using zoxide with --no-aliases, alias these internal functions as # When using zoxide with --no-cmd, alias these internal functions as desired.
# desired.
# #
__zoxide_z_prefix='z#' __zoxide_z_prefix='z#'
@ -57,23 +59,16 @@ function __zoxide_z() {
# shellcheck disable=SC2199 # shellcheck disable=SC2199
if [[ "$#" -eq 0 ]]; then if [[ "$#" -eq 0 ]]; then
__zoxide_cd ~ __zoxide_cd ~
elif [[ "$#" -eq 1 ]] && [[ "$1" = '-' ]]; then elif [[ "$#" -eq 1 ]] && { [[ -d "$1" ]] || [[ "$1" = '-' ]] || [[ "$1" =~ ^[-+][0-9]$ ]]; }; then
if [[ -n "${OLDPWD}" ]]; then
__zoxide_cd "${OLDPWD}"
else
# shellcheck disable=SC2016
\builtin printf 'zoxide: $OLDPWD is not set'
return 1
fi
elif [[ "$#" -eq 1 ]] && [[ -d "$1" ]]; then
__zoxide_cd "$1" __zoxide_cd "$1"
elif [[ "$@[-1]" == "${__zoxide_z_prefix}"* ]]; then elif [[ "$@[-1]" == "${__zoxide_z_prefix}"?* ]]; then
# shellcheck disable=SC2124 # shellcheck disable=SC2124
\builtin local result="${@[-1]}" \builtin local result="${@[-1]}"
__zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}" __zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}"
else else
\builtin local result \builtin local result
result="$(\command zoxide query --exclude "$(__zoxide_pwd || \builtin true)" -- "$@")" && # shellcheck disable=SC2312
result="$(\command zoxide query --exclude "$(__zoxide_pwd)" -- "$@")" &&
__zoxide_cd "${result}" __zoxide_cd "${result}"
fi fi
} }
@ -81,69 +76,44 @@ function __zoxide_z() {
# Jump to a directory using interactive search. # Jump to a directory using interactive search.
function __zoxide_zi() { function __zoxide_zi() {
\builtin local result \builtin local result
result="$(\command zoxide query -i -- "$@")" && __zoxide_cd "${result}" result="$(\command zoxide query --interactive -- "$@")" && __zoxide_cd "${result}"
} }
# Completions.
if [[ -o zle ]]; then
function __zoxide_z_complete() {
# Only show completions when the cursor is at the end of the line.
# shellcheck disable=SC2154
[[ "{{ "${#words[@]}" }}" -eq "${CURRENT}" ]] || return 0
if [[ "{{ "${#words[@]}" }}" -eq 2 ]]; then
_files -/
elif [[ "${words[-1]}" == '' ]] && [[ "${words[-2]}" != "${__zoxide_z_prefix}"?* ]]; then
\builtin local result
# shellcheck disable=SC2086,SC2312
if result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- ${words[2,-1]})"; then
result="${__zoxide_z_prefix}${result}"
# shellcheck disable=SC2296
compadd -Q "${(q-)result}"
fi
\builtin printf '\e[5n'
fi
return 0
}
\builtin bindkey '\e[0n' 'reset-prompt'
[[ "${+functions[compdef]}" -ne 0 ]] && \compdef __zoxide_z_complete __zoxide_z
fi
{{ section }} {{ section }}
# Convenient aliases for zoxide. Disable these using --no-aliases. # Commands for zoxide. Disable these using --no-cmd.
# #
{%- match cmd %} {%- match cmd %}
{%- when Some with (cmd) %} {%- when Some with (cmd) %}
# Remove definitions. \builtin alias {{cmd}}=__zoxide_z
function __zoxide_unset() { \builtin alias {{cmd}}i=__zoxide_zi
\builtin unalias "$@" &>/dev/null || \builtin true
\builtin unfunction "$@" &>/dev/null || \builtin true
\builtin unset "$@" &>/dev/null
}
__zoxide_unset {{cmd}}
function {{cmd}}() {
__zoxide_z "$@"
}
__zoxide_unset {{cmd}}i
function {{cmd}}i() {
__zoxide_zi "$@"
}
if [[ -o zle ]]; then
__zoxide_unset __zoxide_z_complete
function __zoxide_z_complete() {
# Only show completions when the cursor is at the end of the line.
# shellcheck disable=SC2154
[[ "{{ "${#words[@]}" }}" -eq "${CURRENT}" ]] || return
if [[ "{{ "${#words[@]}" }}" -eq 2 ]]; then
_files -/
elif [[ "${words[-1]}" == '' ]]; then
\builtin local result
# shellcheck disable=SC2086
if result="$(\command zoxide query -i -- ${words[2,-1]})"; then
__zoxide_result="${result}"
else
__zoxide_result=''
fi
\builtin printf '\e[5n'
fi
}
__zoxide_unset __zoxide_z_complete_helper
function __zoxide_z_complete_helper() {
\builtin local result="${__zoxide_z_prefix}${__zoxide_result}"
# shellcheck disable=SC2296
[[ -n "${__zoxide_result}" ]] && LBUFFER="${LBUFFER}${(q-)result}"
\builtin zle reset-prompt
}
\builtin zle -N __zoxide_z_complete_helper
\builtin bindkey "\e[0n" __zoxide_z_complete_helper
if [[ "${+functions[compdef]}" -ne 0 ]]; then
\compdef -d {{cmd}}
\compdef __zoxide_z_complete {{cmd}}
fi
fi
{%- when None %} {%- when None %}

View File

@ -6,12 +6,17 @@ use assert_cmd::Command;
#[test] #[test]
fn completions_bash() { fn completions_bash() {
let source = include_str!("../contrib/completions/zoxide.bash"); let source = include_str!("../contrib/completions/zoxide.bash");
Command::new("bash").args(&["--noprofile", "--norc", "-c", source]).assert().success().stdout("").stderr(""); Command::new("bash")
.args(["--noprofile", "--norc", "-c", source])
.assert()
.success()
.stdout("")
.stderr("");
} }
// Elvish: the completions file uses editor commands to add completions to the shell. However, // Elvish: the completions file uses editor commands to add completions to the
// Elvish does not support running editor commands from a script, so we can't create a test for // shell. However, Elvish does not support running editor commands from a
// this. See: https://github.com/elves/elvish/issues/1299 // script, so we can't create a test for this. See: https://github.com/elves/elvish/issues/1299
#[test] #[test]
fn completions_fish() { fn completions_fish() {
@ -21,7 +26,7 @@ fn completions_fish() {
Command::new("fish") Command::new("fish")
.env("HOME", tempdir) .env("HOME", tempdir)
.args(&["--command", source, "--private"]) .args(["--command", source, "--private"])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -32,7 +37,7 @@ fn completions_fish() {
fn completions_powershell() { fn completions_powershell() {
let source = include_str!("../contrib/completions/_zoxide.ps1"); let source = include_str!("../contrib/completions/_zoxide.ps1");
Command::new("pwsh") Command::new("pwsh")
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source]) .args(["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -50,5 +55,5 @@ fn completions_zsh() {
compinit -u compinit -u
"#; "#;
Command::new("zsh").args(&["-c", source, "--no-rcs"]).assert().success().stdout("").stderr(""); Command::new("zsh").args(["-c", source, "--no-rcs"]).assert().success().stdout("").stderr("");
} }

View File

@ -1,11 +0,0 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
anyhow = "1.0.32"
clap = { version = "3.1.0", features = ["derive"] }
ignore = "0.4.18"
shell-words = "1.0.0"

View File

@ -1,154 +0,0 @@
use anyhow::{bail, Context, Result};
use clap::Parser;
use ignore::Walk;
use std::env;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::{self, Command};
fn main() -> Result<()> {
let nix_enabled = enable_nix();
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dir = dir.parent().with_context(|| format!("could not find workspace root: {}", dir.display()))?;
env::set_current_dir(dir).with_context(|| format!("could not set current directory: {}", dir.display()))?;
let app = App::parse();
match app {
App::CI => run_ci(nix_enabled)?,
App::Fmt { check } => run_fmt(nix_enabled, check)?,
App::Lint => run_lint(nix_enabled)?,
App::Test { name } => run_tests(nix_enabled, &name)?,
}
Ok(())
}
#[derive(Parser)]
enum App {
CI,
Fmt {
#[clap(long)]
check: bool,
},
Lint,
Test {
#[clap(default_value = "")]
name: String,
},
}
trait CommandExt {
fn _run(self) -> Result<()>;
}
impl CommandExt for &mut Command {
fn _run(self) -> Result<()> {
println!(">>> {:?}", self);
let status = self.status().with_context(|| format!("command failed to start: {:?}", self))?;
if !status.success() {
bail!("command failed: {:?} with status: {:?}", self, status);
}
Ok(())
}
}
fn run_ci(nix_enabled: bool) -> Result<()> {
let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] };
Command::new("cargo").args(&["check", "--all-features"]).args(color)._run()?;
run_fmt(nix_enabled, true)?;
run_lint(nix_enabled)?;
run_tests(nix_enabled, "")
}
fn run_fmt(nix_enabled: bool, check: bool) -> Result<()> {
// Run cargo-fmt.
let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] };
let check_args: &[&str] = if check { &["--check", "--files-with-diff"] } else { &[] };
Command::new("cargo").args(&["fmt", "--all", "--"]).args(color).args(check_args)._run()?;
// Run nixfmt.
if nix_enabled {
for result in Walk::new("./") {
let entry = result.unwrap();
let path = entry.path();
if path.is_file() && path.extension() == Some(OsStr::new("nix")) {
let check_args: &[&str] = if check { &["--check"] } else { &[] };
Command::new("nixfmt").args(check_args).arg("--").arg(path)._run()?;
}
}
}
Ok(())
}
fn run_lint(nix_enabled: bool) -> Result<()> {
// Run cargo-clippy.
let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] };
Command::new("cargo")
.args(&["clippy", "--all-features", "--all-targets"])
.args(color)
.args(&["--", "-Dwarnings"])
._run()?;
if nix_enabled {
// Run cargo-audit.
let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] };
Command::new("cargo").args(&["audit", "--deny=warnings"]).args(color)._run()?;
// Run markdownlint.
for result in Walk::new("./") {
let entry = result.unwrap();
let path = entry.path();
if path.is_file() && path.extension() == Some(OsStr::new("md")) {
Command::new("markdownlint").arg(path)._run()?;
}
}
// Run mandoc with linting enabled.
for result in Walk::new("./man/") {
let entry = result.unwrap();
let path = entry.path();
if path.is_file() && path.extension() == Some(OsStr::new("1")) {
Command::new("mandoc").args(&["-man", "-Wall", "-Tlint", "--"]).arg(path)._run()?;
}
}
}
Ok(())
}
fn run_tests(nix_enabled: bool, name: &str) -> Result<()> {
let color: &[&str] = if is_ci() { &["--color=always"] } else { &[] };
let features: &[&str] = if nix_enabled { &["--all-features"] } else { &[] };
Command::new("cargo").args(&["test", "--no-fail-fast", "--workspace"]).args(color).args(features).arg(name)._run()
}
fn is_ci() -> bool {
env::var_os("CI").is_some()
}
fn enable_nix() -> bool {
let nix_supported = cfg!(any(target_os = "linux", target_os = "macos"));
if !nix_supported {
return false;
}
let nix_enabled = env::var_os("IN_NIX_SHELL").unwrap_or_default() == "pure";
if nix_enabled {
return true;
}
let nix_detected = Command::new("nix-shell").arg("--version").status().map(|s| s.success()).unwrap_or(false);
if !nix_detected {
return false;
}
println!("Detected Nix in environment, re-running in Nix.");
let args = env::args();
let cmd = shell_words::join(args);
let mut nix_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
nix_path.push("../shell.nix");
let status = Command::new("nix-shell").args(&["--pure", "--run", &cmd, "--"]).arg(nix_path).status().unwrap();
process::exit(status.code().unwrap_or(1));
}