Compare commits

..

No commits in common. "master" and "v0.10.0" have entirely different histories.

820 changed files with 10867 additions and 74037 deletions

View file

@ -1,5 +0,0 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore

View file

@ -1,5 +0,0 @@
behavior:
output: minimal
test:
runner: nextest

View file

@ -1,79 +0,0 @@
experimental = ["wrapper-scripts"]
# The end-to-end tests drive `sk` inside a Zellij session that each test creates
# and tears down itself (see tests/common/zellij.rs), so — unlike the previous
# tmux-based harness — no shared multiplexer session needs to be set up here.
# Valgrind wrapper for memory leak detection
[scripts.wrapper.valgrind]
command = [
"valgrind",
"--leak-check=full",
"--show-leak-kinds=all",
"--track-origins=yes",
"--error-exitcode=1",
"--suppressions=.config/valgrind.supp"
]
# The end-to-end tests each spin up a full Zellij session (server + client +
# PTY). Running many concurrently overwhelms the machine and makes the
# timing-sensitive tests flaky, so serialize everything in this group while the
# rest of the suite keeps running in parallel. (Under raw `cargo test`, pass
# `--test-threads=1` for the e2e test binaries instead.)
[test-groups]
e2e = { max-threads = 1 }
[profile.default]
fail-fast = false
retries = 2
[[profile.default.overrides]]
filter = 'binary(interactive) | binary(listen) | binary(popup) | binary(execute)'
test-group = 'e2e'
[profile.default.junit]
path = "junit.xml"
[profile.ci]
retries = 9
[[profile.ci.overrides]]
filter = 'binary(interactive) | binary(listen) | binary(popup) | binary(execute)'
test-group = 'e2e'
# Valgrind profile for memory leak detection
# Usage: cargo nextest run --profile valgrind
#
# Note: Valgrind can detect memory leaks but does NOT detect dangling threads.
# For thread leak detection, use ThreadSanitizer instead (see below).
[profile.valgrind]
fail-fast = false
retries = 2
test-threads = 1 # Run tests serially to avoid interleaved valgrind output
[[profile.valgrind.scripts]]
platform = "cfg(unix)"
run-wrapper = "valgrind"
# ThreadSanitizer profile for detecting data races and thread issues
# Usage:
# 1. First build with sanitizer (rebuilds stdlib and all deps):
# RUSTFLAGS="-Zsanitizer=thread" cargo +nightly build --tests -Zbuild-std --target x86_64-unknown-linux-gnu
# 2. Then run tests:
# TSAN_OPTIONS="detect_deadlocks=1" cargo +nightly nextest run --profile tsan --target x86_64-unknown-linux-gnu
#
# Note: ThreadSanitizer can detect:
# - Data races (concurrent unsynchronized access to memory)
# - Deadlocks (with TSAN_OPTIONS=detect_deadlocks=1)
# - Thread leaks (threads not joined before program exit)
#
# Requirements:
# - Rust nightly (for -Zsanitizer and -Zbuild-std flags)
# - The -Zbuild-std flag rebuilds the standard library with ThreadSanitizer
# instrumentation to avoid ABI mismatch errors
#
# Important: This takes a long time on first build as it recompiles everything
# including the standard library with ThreadSanitizer instrumentation.
#
# Environment variables:
# TSAN_OPTIONS="detect_deadlocks=1 second_deadlock_stack=1"
[profile.tsan]
fail-fast = false
retries = 3
test-threads = 1 # TSan requires running tests serially

View file

@ -1,53 +0,0 @@
# Valgrind suppressions for skim tests
# This file suppresses known false positives from Rust stdlib and system libraries
# Rust std allocations that are intentionally not freed at program exit
{
rust_std_exit_cleanup
Memcheck:Leak
...
fun:*std*
}
# Thread-local storage cleanup
{
thread_local_cleanup
Memcheck:Leak
...
fun:pthread_create*
}
# Tokio runtime allocations
{
tokio_runtime
Memcheck:Leak
...
fun:*tokio*runtime*
}
# Crossterm/terminal allocations
{
crossterm_terminal
Memcheck:Leak
...
fun:*crossterm*
}
# Libc thread initialization
{
libc_thread_init
Memcheck:Leak
match-leak-kinds: possible
...
fun:calloc
fun:allocate_dtv
}
# DL allocations
{
dl_init
Memcheck:Leak
match-leak-kinds: possible
...
fun:*dl_*
}

View file

@ -1,8 +0,0 @@
target/
.github/
bin/
plugin/
man/
shell/
*.md
install

4
.envrc
View file

@ -1,4 +0,0 @@
export _skim_flake_profile="default"
# .envrc.local lets you override the _skim_flake_profile if needed: `export _skim_flake_profile="full"`
source_env_if_exists .envrc.local
use flake .#"$_skim_flake_profile"

View file

@ -1,9 +0,0 @@
set -xeuo pipefail
cargo +nightly fmt --check --all
cargo clippy --all-targets -- -Dwarnings
cargo clippy --no-default-features -- -Dwarnings
cargo clippy --no-default-features --features cli -- -Dwarnings
cargo clippy --no-default-features --features image -- -Dwarnings
cargo clippy --no-default-features --features listen -- -Dwarnings
cargo clippy --no-default-features --features frizbee -- -Dwarnings

View file

@ -1,75 +0,0 @@
# Contributor Guide
## Development environment
A [Nix flake](../flake.nix) is provided with opt-in package groups. The default shell contains only the base build tools (`rustup`, `just`); richer environments are available as named shells:
| Shell | Extra packages |
|---|---|
| `nix develop` | `rustup`, `just` |
| `nix develop .#tests` | + nextest, cargo-insta, cargo-llvm-cov, tmux |
| `nix develop .#utils` | + hyperfine, cargo-edit, cargo-public-api, git-cliff |
| `nix develop .#gungraun` | + valgrind, libclang, binutils |
| `nix develop .#bench` | + uv, matplotlib, requests (for `bench.py`) |
| `nix develop .#vagrant` | + vagrant, rsync (for Windows testing) |
| `nix develop .#full` | everything above |
## Running tests
All tests can be run by using [cargo-nextest](https://nexte.st/), which can be installed using `cargo install cargo-nextest` of following the instructions on the website.
You will need `tmux` to run some integration tests.
You can then run `cargo nextest run --release`, which should automatically build a release binary, run the unit tests and the integration tests.
Most integration tests use [cargo insta](https://insta.rs). If you need to add some tests or re-review them, you will need to install it, and run tests with `cargo insta test --tests --review`, which will let you review snapshots.
Note: you can run the tests without `--release`, but expect more flaky tests since the timings will be looser. I would advise testing manually any debug test failure if you have doubts.
Note2: A dockerfile is available if you want to run the tests inside docker. There is little to no cache, so the test will need to rebuild most of the application after each change.
To use it, build the image with `docker build -f test.dockerfile . -t skim-test` then run it using `docker run --rm -it skim-test`.
## Windows testing
A [Vagrantfile](../Vagrantfile) is provided to spin up a headless Windows Server 2022 Core VM via KVM/libvirt, letting you test Windows compatibility without a GUI.
**Host prerequisites (NixOS):**
```nix
virtualisation.libvirtd.enable = true;
users.users.<you>.extraGroups = [ "libvirtd" ]; # log out/in after applying
```
**Usage:**
```sh
nix develop .#vagrant
vagrant up # first boot: ~15-20 min, downloads box + provisions
vagrant ssh # connect to the VM
vagrant halt # stop the VM
vagrant destroy # delete the VM
```
Inside the VM the project root is synced to `C:\vagrant`. Re-sync after local changes with `vagrant rsync`. To build:
```powershell
cd C:\vagrant
cargo build
cargo test
```
## Submitting code
To avoid using up CI minutes uselessly, make sure that :
- You run `cargo clippy` and `cargo fmt` before pushing any code to an open PR.
- Your PR's title respects [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
Not respecting these guidelines could end up consuming all our minutes and preventing us from testing and releasing any new code until the end of the month.
Note: a git pre-commit hook is available in .githooks/pre-commit which will make the clippy & fmt checks. To use it, run `git config core.hooksPath ".githooks"`.
## Vibe Coding guidelines
Any code generated partially or completely using LLMs will be treated the same way as if you wrote it yourself.
This means that you are expected to understand if fully and are responsible for it.

View file

@ -1,33 +0,0 @@
---
name: Bug report
about: Report a bug encountered using `skim`
title: "[BUG] xxx"
labels: bug
assignees: LoricAndre
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (please complete the following information):**
- OS (`uname -a`):
- `skim` version (`sk --version`):
- Shell and version:
- Variables (`env | grep '^SKIM'`):
**Additional context**
Add any other context about the problem here.

View file

@ -1,27 +0,0 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: weekly
commit-message:
prefix: chore(deps)
prefix-development: chore(dev-deps)
groups:
cargo-prod:
dependency-type: production
cargo-dev:
dependency-type: development
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
commit-message:
prefix: chore(deps)
prefix-development: chore(dev-deps)
exclude-paths:
- .github/workflows/release.yml
groups:
gha-prod:
patterns:
- "*"

View file

@ -1,35 +0,0 @@
{
"LABEL": {
"name": "invalid-title",
"color": "B60205"
},
"CHECKS": {
"prefixes": [
"feat: ",
"feature: ",
"fix: ",
"bugfix: ",
"perf: ",
"refactor: ",
"test: ",
"tests: ",
"build: ",
"ci: ",
"doc: ",
"docs: ",
"style: ",
"chore: ",
"other: "
],
"regexp": "^\\w+(\\([a-z_-]+\\))?: ",
"regexpFlags": "",
"ignoreLabels": [
"skip-title-check"
]
},
"MESSAGES": {
"success": "PR title is valid",
"failure": "PR title is invalid",
"notice": ""
}
}

View file

@ -1,11 +0,0 @@
## Checklist
- [ ] The title of my PR follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] I have updated the documentation (`README.md`, comments, `src/manpage.rs` and/or `src/options.rs` if applicable)
- [ ] I have added unit tests
- [ ] I have added [integration tests](https://github.com/skim-rs/skim/tree/master/tests)
- [ ] I have linked all related issues or PRs
## Description of the changes

View file

@ -1,75 +0,0 @@
name: Deploy APT repo
# Publishes the release's .deb packages as a (flat) APT repository under
# /apt on the gh-pages branch, alongside the coverage report at /coverage.
# keep_files preserves the coverage content (and vice versa).
#
# Requires APT_GPG_PRIVATE_KEY to be set :
#
# ```sh
# export GNUPGHOME=$(mktemp -d)
# cat > "$GNUPGHOME/skim-key" <<'EOF'
# %no-protection
# Key-Type: eddsa
# Key-Curve: ed25519
# Subkey-Type: ecdh
# Subkey-Curve: cv25519
# Name-Real: skim apt repo
# Name-Email: apt@skim-rs.github.io
# Expire-Date: 0
# %commit
# EOF
# gpg --batch --gen-key "$GNUPGHOME/skim-key" 2>&1 | tail -3
# KEYID=$(gpg --list-secret-keys --with-colons apt@skim-rs.github.io | awk -F: '/^sec:/{print $5; exit}')
# # Copy the output of the line below
# gpg --export-secret-keys --armor "$KEYID"
# ```
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
apt:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download release .deb packages
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ fromJson(inputs.plan).announcement_tag }}
run: |
mkdir -p apt
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.deb' --dir apt
ls -l apt
- name: Build APT index
run: |
sudo apt-get update
sudo apt-get install -y dpkg-dev apt-utils
cd apt
dpkg-scanpackages --multiversion . > Packages
gzip -k -f Packages
apt-ftparchive release . > Release
- name: Sign the repo
# Skipped when the secret is absent; the repo then stays unsigned
# ([trusted=yes]). Add APT_GPG_PRIVATE_KEY (an ASCII-armored private
# key) to enable signed-by verification.
env:
GPG_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --batch --import
KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}')
cd apt
gpg --batch --yes --default-key "$KEYID" --clearsign -o InRelease Release
gpg --batch --yes --default-key "$KEYID" -abs -o Release.gpg Release
gpg --export --armor "$KEYID" > skim-archive-keyring.asc
- name: Deploy to gh-pages under /apt
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: apt
destination_dir: apt
keep_files: true

134
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,134 @@
name: Build & Test
on:
workflow_dispatch:
pull_request:
push:
branches:
- master
jobs:
test:
name: test
runs-on: ${{matrix.os}}
strategy:
matrix:
build: [linux, macos]
include:
- build: linux
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-musl
- build: macos
os: macos-latest
rust: stable
target: x86_64-apple-darwin
steps:
- name: Install dependencies (for Linux)
run: |
sudo apt-get install zsh
python3 -V
tmux -V
locale
if: runner.os == 'Linux'
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- name: Install dependencies (for MacOS)
run: |
brew install tmux
brew install zsh
python3 -V
tmux -V
locale
if: runner.os == 'macOS'
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Install correct toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- name: Build
run: cargo build --release
- name: Run Tests
run: tmux new-session -d && python3 test/test_skim.py --verbose
env:
LC_ALL: en_US.UTF-8
TERM: xterm-256color
- name: Cache
uses: Swatinem/rust-cache@v1
clippy:
name: clippy
runs-on: ${{matrix.os}}
strategy:
matrix:
build: [linux, macos]
include:
- build: linux
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-musl
- build: macos
os: macos-latest
rust: stable
target: x86_64-apple-darwin
steps:
- name: Install dependencies (for Linux)
run: |
sudo apt-get install zsh
python3 -V
tmux -V
locale
if: runner.os == 'Linux'
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- name: Install dependencies (for MacOS)
run: |
brew install tmux
brew install zsh
python3 -V
tmux -V
locale
if: runner.os == 'macOS'
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Install correct toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
components: clippy
- name: Clippy
run: cargo clippy
- name: Cache
uses: Swatinem/rust-cache@v1
rustfmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
components: rustfmt
- name: Check formatting
run: |
cargo fmt --all -- --check

29
.github/workflows/crates.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Release to crates.io
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Install correct toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Run cargo check
uses: actions-rs/cargo@v1
with:
command: check
- name: Login crates.io
run: cargo login ${CRATES_IO_TOKEN}
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
- run: cargo publish

View file

@ -1,80 +0,0 @@
name: Build Linux packages
# Custom dist job (wired in through `global-artifacts-jobs` in dist-workspace.toml).
# Builds a `.deb` and a `.rpm` for amd64 and arm64, each containing the `sk`
# executable, the man page and the shell completions, then uploads them under an
# `artifacts-*` name so that dist's `host` job attaches them to the GitHub
# Release. Both architectures build natively (no cross-compilation) on their
# respective GitHub-hosted runners.
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
linux-packages:
name: Build .deb and .rpm (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
- arch: arm64
runner: ubuntu-22.04-arm
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Install rust toolchain
run: rustup toolchain install
- name: Check the crate version matches the release plan
# These packages are built from the checked-out source (the same tree
# dist releases from), so the crate version must match the version dist
# planned for this release. Consume the plan to guard against drift.
env:
ANNOUNCEMENT_TAG: ${{ fromJson(inputs.plan).announcement_tag }}
run: |
plan_version="${ANNOUNCEMENT_TAG#v}"
crate_version="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
echo "release plan: '$plan_version' | crate: '$crate_version'"
if [ -n "$plan_version" ] && [ "$plan_version" != "$crate_version" ]; then
echo "::error::Cargo.toml version ($crate_version) does not match the release plan ($plan_version)"
exit 1
fi
- name: Install cargo-generate-rpm
uses: taiki-e/install-action@v2.87.1
with:
tool: cargo-generate-rpm@0.21
- name: Setup cargo cache
uses: Swatinem/rust-cache@v2
# cargo-deb's prebuilt binary links GLIBC_2.39, which ubuntu-22.04
# (glibc 2.35) lacks. Build it from source so it links the runner's
# glibc. We stay on 22.04 on purpose: the sk binary it packages must
# keep a low glibc floor for the .deb to run on older distros.
- name: Install cargo-deb
run: cargo install 'cargo-deb@^3' --locked
- name: Build release binary
run: cargo build --release --bin sk
- name: Build .deb package
run: cargo deb --no-build
- name: Build .rpm package
run: cargo generate-rpm
- name: Collect packages
run: |
mkdir -p target/linux-packages
cp target/debian/*.deb target/linux-packages/
cp target/generate-rpm/*.rpm target/linux-packages/
ls -l target/linux-packages/
- name: Upload packages
uses: actions/upload-artifact@v7
with:
name: artifacts-linux-packages-${{ matrix.arch }}
path: target/linux-packages/*
if-no-files-found: error

View file

@ -1,13 +0,0 @@
name: Pull request update
on:
push:
branches: [master]
jobs:
autoupdate:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: allonsy-studio/actions-pr-auto-update@5fbf9661cbf5c1f092ec33e5a1cfbe70dc5bd81c

View file

@ -1,104 +0,0 @@
on:
pull_request:
types:
- opened
- synchronize
- reopened
- edited
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- name: Create git-cliff mock context
env:
CONTEXT: >
[
{
"commits": [{
"id": "foo",
"message": $msg,
"links": [],
"author": {
"name": "",
"timestamp": 1
},
"committer": {
"name": "",
"timestamp": 1
},
"merge_commit": false,
"github": {
"pr_labels": [],
"is_first_time": false
},
"gitlab": {
"pr_labels": [],
"is_first_time": false
},
"gitea": {
"pr_labels": [],
"is_first_time": false
},
"bitbucket": {
"pr_labels": [],
"is_first_time": false
},
"azure_devops": {
"pr_labels": [],
"is_first_time": false
},
}],
"github": { "contributors": [] },
"gitlab": { "contributors": [] },
"gitea": { "contributors": [] },
"bitbucket": { "contributors": [] },
"azure_devops": { "contributors": [] },
"submodule_commits": {}
}
]
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
jq -nc --arg msg "$PR_TITLE" "$CONTEXT" | tee git-cliff-context.json
- name: Generate mock changelog entry
id: git-cliff
uses: orhun/git-cliff-action@v4
with:
args: >
--from-context git-cliff-context.json -s all
- name: Check generated entry
run: |
echo "Generated:"
cat "${{ steps.git-cliff.outputs.changelog }}"
echo "Checking..."
cat "${{ steps.git-cliff.outputs.changelog }}" | grep -Ev '^(## \[unreleased\]|)$' | grep -q '^.\+$'
check-generated-files:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Cache
uses: Swatinem/rust-cache@v2
with:
key: ${{ runner.os }}
add-job-id-key: "false"
add-rust-environment-hash-key: "false"
env-vars: "____"
cache-on-failure: "true"
cache-all-crates: "true"
- name: Generate files
run: |
cargo run --locked -- --man > ./man/man1/sk.1
cargo run --locked -- --shell bash > ./shell/completion.bash
cargo run --locked -- --shell zsh > ./shell/completion.zsh
cargo run --locked -- --shell fish > ./shell/completion.fish
cargo run --locked -- --shell nushell > ./shell/completion.nu
- name: Check diff
run: |
if git diff --exit-code; then
echo "No changes to generated files found, PR is safe to merge"
exit 0
else
echo "Found changes to generated files, regenerate them locally and push them using 'just generate-files'"
exit 1
fi

140
.github/workflows/publish-github.yml vendored Normal file
View file

@ -0,0 +1,140 @@
name: Publish to Github
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
create-release:
name: Create Github Release
runs-on: ubuntu-latest
steps:
- name: Create artifacts directory
run: mkdir artifacts
- name: Get the release version from the tag
run: |
# Apparently, this is the right way to get a tag name. Really?
#
# See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
echo "SK_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Create Release
id: release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ env.SK_VERSION }}
body: ${{ env.SK_VERSION }}
draft: false
prerelease: false
- name: Save release upload URL to artifact
run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url
- name: Save version number to artifact
run: echo "${{ env.SK_VERSION }}" > artifacts/release-version
- name: Upload artifacts
uses: actions/upload-artifact@v1
with:
name: artifacts
path: artifacts
publish-to-github:
name: Publish to Github
needs: ['create-release']
runs-on: ${{matrix.os}}
strategy:
matrix:
build: [linux, arm, arm-v7, macos]
include:
- build: linux
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-musl
cross: false
- build: arm
os: ubuntu-latest
rust: stable
target: arm-unknown-linux-gnueabihf
cross: true
- build: arm-v7
os: ubuntu-latest
rust: stable
target: armv7-unknown-linux-gnueabihf
cross: true
- build: macos
os: macos-latest
rust: stable
target: x86_64-apple-darwin
cross: false
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Install correct toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
use-cross: ${{ matrix.cross }}
override: true
- name: Get release download URL
uses: actions/download-artifact@v1
with:
name: artifacts
path: artifacts
- name: Set release upload URL and release version
shell: bash
run: |
release_upload_url="$(cat artifacts/release-upload-url)"
echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
echo "release upload url: $RELEASE_UPLOAD_URL"
release_version="$(cat artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
echo "release version: $RELEASE_VERSION"
- name: build
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: build
args: --release --target ${{ matrix.target }}
- name: Package Artifacts
run: |
src=$(pwd)
stage=
case $RUNNER_OS in
Linux)
stage=$(mktemp -d)
;;
macOS)
stage=$(mktemp -d -t tmp)
;;
esac
echo "src is: $src"
echo "stage is: $stage"
cp target/${{ matrix.target }}/release/sk $stage/
cd $stage
ASSET_NAME="skim-${{ env.RELEASE_VERSION }}-${{ matrix.target }}.tar.gz"
ASSET_PATH="$src/$ASSET_NAME"
echo "ASSET_NAME=$ASSET_NAME" >> $GITHUB_ENV
echo "ASSET_PATH=$ASSET_PATH" >> $GITHUB_ENV
tar czf $ASSET_PATH *
cd $src
- name: Upload release archive
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: ${{ env.ASSET_PATH }}
asset_name: ${{ env.ASSET_NAME }}
asset_content_type: application/octet-stream

View file

@ -1,24 +0,0 @@
name: Publish cargo crate
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- run: rustup toolchain install
- name: Cache
uses: Swatinem/rust-cache@v2
- name: Login
run: cargo login ${CRATES_IO_TOKEN}
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
- name: Publish
run: cargo publish

View file

@ -1,175 +0,0 @@
name: Release PR
on:
push:
branches: [master]
pull_request:
branches: [master]
types: [closed]
permissions:
contents: read
concurrency:
group: release-pr-${{ github.event_name }}
cancel-in-progress: true
jobs:
prepare:
if: "${{ github.event_name == 'push' && !startsWith(github.event.head_commit.message, 'release: v') }}"
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
changelog: ${{ steps.changelog.outputs.content }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: taiki-e/install-action@742a3317eac7bd62f91cd888b4eead5e784ba833 # v2
with:
tool: git-cliff
- id: version
name: Compute next version
shell: bash
run: |
version="$(git cliff --bumped-version)"
version="${version#v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]
if git rev-parse --verify --quiet "refs/tags/v$version"; then
echo "No releasable changes since v$version"
exit 0
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Prepare release files
if: steps.version.outputs.version != ''
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
sed -i 's/^version = ".*"/version = "'"$VERSION"'"/' Cargo.toml
SKIM_DEFAULT_OPTIONS='' cargo run -- --man > man/man1/sk.1
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell bash > shell/completion.bash
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell zsh > shell/completion.zsh
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell fish > shell/completion.fish
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell nushell > shell/completion.nu
git cliff -p CHANGELOG.md -t "v$VERSION" -u
cargo generate-lockfile
echo "$VERSION" > shell/version.txt
git diff --binary -- CHANGELOG.md Cargo.lock Cargo.toml man shell > "$RUNNER_TEMP/release.patch"
test -s "$RUNNER_TEMP/release.patch"
- id: changelog
name: Render release changelog
if: steps.version.outputs.version != ''
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
delimiter="changelog-$(cat /proc/sys/kernel/random/uuid)"
{
echo "content<<$delimiter"
git cliff -u -t "v$VERSION" --strip all
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: steps.version.outputs.version != ''
with:
name: release-${{ github.sha }}
path: ${{ runner.temp }}/release.patch
if-no-files-found: error
update-pr:
needs: prepare
if: needs.prepare.outputs.version != ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-${{ github.sha }}
path: ${{ runner.temp }}/release
- name: Create the release commit
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
git fetch origin master
test "$(git rev-parse origin/master)" = "$GITHUB_SHA"
git apply "$RUNNER_TEMP/release/release.patch"
unexpected="$(git diff --name-only | grep -Ev '^(CHANGELOG.md|Cargo.lock|Cargo.toml|man/|shell/)' || true)"
test -z "$unexpected"
git switch -c release-pr
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git add CHANGELOG.md Cargo.lock Cargo.toml man shell
git commit -m "release: v$VERSION"
# Mint the write token only on this fresh runner, after repository code has finished executing.
- id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
client-id: ${{ vars.SKIM_RS_BOT_CLIENT_ID }}
private-key: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Push branch and create or update PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ needs.prepare.outputs.version }}
CHANGELOG: ${{ needs.prepare.outputs.changelog }}
shell: bash
run: |
auth="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w0)"
git -c http.extraheader="AUTHORIZATION: basic $auth" push --force origin HEAD:release-pr
cat > "$RUNNER_TEMP/pr-body.md" <<EOF
Automated release preparation for v$VERSION.
This branch is rebuilt from master as one commit on every push. Merge it with the repository's required squash merge; the resulting commit will be tagged v$VERSION and picked up by the release workflow.
EOF
printf '%s\n' "$CHANGELOG" >> "$RUNNER_TEMP/pr-body.md"
pr="$(gh pr list --base master --head release-pr --state open --json number --jq '.[0].number')"
if [[ -n "$pr" ]]; then
gh pr edit "$pr" --title "release: v$VERSION" --body-file "$RUNNER_TEMP/pr-body.md"
else
gh pr create --base master --head release-pr --title "release: v$VERSION" --body-file "$RUNNER_TEMP/pr-body.md"
fi
tag:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref == 'release-pr' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
persist-credentials: false
sparse-checkout: Cargo.toml
- id: release
name: Validate release merge
env:
PR_TITLE: ${{ github.event.pull_request.title }}
shell: bash
run: |
version="$(awk -F '"' '/^version = "/ { print $2; exit }' Cargo.toml)"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]
test "$PR_TITLE" = "release: v$version"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
# No checked-out code runs after this repository-scoped, contents-only token is minted.
- id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
client-id: ${{ vars.SKIM_RS_BOT_CLIENT_ID }}
private-key: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }}
permission-contents: write
- name: Tag the squash commit
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
SHA: ${{ github.event.pull_request.merge_commit_sha }}
TAG: ${{ steps.release.outputs.tag }}
run: gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA"

View file

@ -1,359 +0,0 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v7
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
custom-test:
uses: ./.github/workflows/test.yml
secrets: inherit
permissions:
"contents": "write"
"id-token": "write"
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
- custom-test
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v8
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v7
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v8
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v8
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v7
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
custom-package:
needs:
- plan
- build-local-artifacts
uses: ./.github/workflows/package.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
- custom-package
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-package.result == 'skipped' || needs.custom-package.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v8
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v8
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v7
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v8
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
custom-publish:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
"id-token": "write"
"packages": "write"
custom-winget:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/winget.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
"id-token": "write"
"packages": "write"
custom-apt:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/apt.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
"contents": "write"
"id-token": "write"
announce:
needs:
- plan
- host
- custom-publish
- custom-winget
- custom-apt
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish.result == 'skipped' || needs.custom-publish.result == 'success') && (needs.custom-winget.result == 'skipped' || needs.custom-winget.result == 'success') && (needs.custom-apt.result == 'skipped' || needs.custom-apt.result == 'success') }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive

View file

@ -1,304 +0,0 @@
name: Build & Test
on:
workflow_dispatch:
workflow_call:
inputs:
plan:
required: false
type: string
# pull_request: # No need to trigger on PR, cargo-dist already does
push:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
nextest:
runs-on: ${{matrix.runner}}
# Cap the wall-clock so a harness regression that makes the Zellij pane never
# render (each `wait_ready` then burning its full budget) fails the leg in
# minutes instead of letting a serialized e2e run drag on for hours.
timeout-minutes: 45
strategy:
# Report every OS independently: a failure on one (e.g. Windows) must not
# cancel the others, so we still get a clear signal from Linux and macOS.
fail-fast: false
matrix: &matrix
build: [linux, macos, windows]
include:
- build: linux
runner: ubuntu-latest
- build: macos
runner: macos-latest
- build: windows
runner: windows-latest
permissions:
contents: read
steps:
- &zellij-install
# The e2e tests drive `sk` inside a Zellij session (Zellij 0.44+ runs on
# Linux, macOS and Windows). taiki-e/install-action fetches a prebuilt
# binary on Linux and macOS; it has no Windows binary and `cargo install
# zellij` fails building openssl from source on the runner, so Windows
# installs via winget below.
name: Install zellij (Linux/macOS)
if: runner.os != 'Windows'
uses: taiki-e/install-action@v2.87.1
with:
tool: zellij@0.44.3
- name: Install zellij (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
winget install --exact --id Zellij.Zellij --version 0.44.3 --source winget --accept-source-agreements --accept-package-agreements --disable-interactivity
# The winget package is an MSI that installs zellij and adds its dir to
# the machine PATH in the registry. Neither the current process nor
# GITHUB_PATH sees that until reloaded, so refresh PATH from the
# registry (with a Program Files fallback), then expose zellij's dir to
# later steps.
$env:PATH = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') + ';' + $env:PATH
$exe = (Get-Command zellij.exe -ErrorAction SilentlyContinue).Source
if (-not $exe) { $exe = (Get-ChildItem 'C:\Program Files' -Recurse -Filter zellij.exe -ErrorAction SilentlyContinue | Select-Object -First 1).FullName }
if (-not $exe) { throw "zellij.exe not found after winget install" }
$dir = Split-Path -Parent $exe
Write-Host "zellij installed at: $dir"
Add-Content -Path $env:GITHUB_PATH -Value $dir
& $exe --version
- name: Show locale
run: locale
if: runner.os != 'Windows'
- &checkout
name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
- &toolchain
name: Install rust toolchain
run: rustup toolchain install
- &nextest-install
name: Install nextest
uses: taiki-e/install-action@v2.87.1
with:
tool: nextest@0.9
- &cache
name: Setup cargo cache
uses: Swatinem/rust-cache@v2
- name: Run doctests
run: cargo test --doc
- name: "Run tests"
# Do not use `--all-targets` to avoid running benches
run: cargo nextest run --release --locked --profile ci --bins --lib --examples --tests
env:
LC_ALL: en_US.UTF-8
TERM: xterm-256color
- name: Show snapshot diffs on failure
if: failure()
run: |
find tests/snapshots/ -name "*.snap.new" | while read -r new_snap; do
base="${new_snap%.new}"
echo "=== Snapshot diff: $base ==="
echo "new: $new_snap"
cat "$new_snap"
if [ -f "$base" ]; then
echo "old: $base"
cat "$base"
diff "$base" "$new_snap"
fi
done
shell: bash
coverage:
runs-on: ubuntu-latest
continue-on-error: true
permissions:
contents: write
steps:
- *zellij-install
- *checkout
- *toolchain
- *nextest-install
- uses: taiki-e/install-action@v2.87.1
with:
tool: cargo-llvm-cov@0.8
- *cache
- name: "Run tests with coverage"
# Do not use `--all-targets` to avoid running benches
run: |
cargo +nightly llvm-cov nextest --locked --release --profile ci --branch --bins --lib --examples --tests --no-report
cargo +nightly llvm-cov report --release --cobertura --output-path coverage.xml
cargo +nightly llvm-cov report --release --html
echo "COVERAGE_PERCENT=$(cargo +nightly llvm-cov report --release | tail -n1 | awk '{ print $13 }')" | tee --append $GITHUB_ENV
env:
LC_ALL: en_US.UTF-8
TERM: xterm-256color
- name: "Generate coverage badge"
if: &if-master github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
uses: emibcn/badge-action@v2.0.4
with:
label: 'Coverage'
status: ${{ env.COVERAGE_PERCENT }}
color: 'blue'
path: 'target/llvm-cov/html/coverage.svg'
- name: "Deploy coverage to gh-pages under /coverage"
if: *if-master
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: target/llvm-cov/html
destination_dir: coverage
keep_files: true
clippy:
runs-on: ${{matrix.runner}}
strategy:
matrix: *matrix
steps:
- *checkout
- *toolchain
- *cache
- name: Clippy
run: cargo clippy
rustfmt:
runs-on: ${{matrix.runner}}
strategy:
matrix: *matrix
steps:
- *checkout
- *toolchain
- name: Check formatting
run: |
cargo fmt --all -- --check
clippy-no-default-features:
runs-on: ${{matrix.runner}}
strategy:
matrix: *matrix
steps:
- *checkout
- *toolchain
- *cache
- name: Run clippy with specific features
run: |
cargo clippy --locked --no-default-features -- -Dwarnings
cargo clippy --locked --no-default-features --features cli -- -Dwarnings
cargo clippy --locked --no-default-features --features image -- -Dwarnings
cargo clippy --locked --no-default-features --features listen -- -Dwarnings
cargo clippy --locked --no-default-features --features frizbee -- -Dwarnings
msrv:
runs-on: ubuntu-latest
steps:
- *checkout
- *toolchain
- name: MSRV Verify
run: cargo +1.91.0 build --release --locked --all-targets
fuzz:
permissions:
contents: read
runs-on: ${{matrix.runner}}
strategy:
matrix: *matrix
steps:
- *checkout
- *toolchain
- *cache
- name: Set up MSVC dev environment
# Without a sanitizer, libFuzzer's coverage instrumentation fails to
# link on Windows: neither MSVC's link.exe nor LLD's COFF driver
# synthesize the __start/__stop section-boundary symbols it needs
# (an ELF/Mach-O-only linker feature). MSVC's AddressSanitizer
# runtime provides an equivalent shim for those symbols, so it's
# required for the link to succeed at all, not just extra bug
# detection. It needs its DLL directory on PATH at run time, which
# this action sets up (see
# https://rust-fuzz.github.io/book/cargo-fuzz/windows/setup.html).
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
- uses: taiki-e/install-action@v2.87.1
with:
tool: cargo-fuzz@0.13
- name: Run fuzz targets
shell: bash
# 5 targets * 60s = 5 minutes of fuzzing total per run.
# Force the actual host target: the sanitizer build can't link
# against a statically-linked libc (e.g. if CARGO_BUILD_TARGET
# defaults to a musl target elsewhere in the matrix).
run: |
host_target="$(rustc +nightly -vV | sed -n 's/^host: //p')"
if [ "$RUNNER_OS" = "Windows" ]; then
# Git Bash's own coreutils `link` (for hardlinks) sits ahead of
# the MSVC one on PATH within this shell, so cargo/rustc would
# otherwise invoke the wrong `link.exe`. Point at the real MSVC
# linker explicitly, using the dev env ilammy/msvc-dev-cmd set up.
export CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="${VCToolsInstallDir}bin\\HostX64\\x64\\link.exe"
fi
for target in ansi_strip field_extract fuzzy_match query_match keymap_parse; do
cargo +nightly fuzz run "$target" --target "$host_target" -- -max_total_time=60
done
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: fuzz-crashes-${{ runner.os }}
path: fuzz/artifacts/
if-no-files-found: ignore
# Single aggregate status check for branch protection / repository rulesets,
# so they don't need to enumerate every matrix leg by name individually.
public-api:
name: Check for public API breaking changes
permissions:
contents: read
continue-on-error: true
runs-on: ubuntu-latest
steps:
- *checkout
- *toolchain
- *cache
- uses: taiki-e/install-action@v2.87.1
with:
tool: cargo-public-api@0.52
- run: rustup toolchain install nightly-x86_64-unknown-linux-gnu
name: Install nightly toolchain
# Compare two repository revisions so cargo-public-api uses their committed
# lockfiles. A registry comparison resolves the released crate again and can
# fail when a new, broken transitive dependency is published.
- run: git fetch --force --tags
name: Fetch release tags
- run: |
latest_tag="$(git tag --list 'v*' --sort=-version:refname | head -n1)"
cargo public-api diff "${latest_tag}..HEAD" --deny removed --deny changed
name: Run cargo-public-api
ci-success:
name: CI Success
if: always()
needs:
- nextest
- coverage
- clippy
- rustfmt
- clippy-no-default-features
- msrv
- fuzz
runs-on: ubuntu-latest
steps:
- name: Check all required jobs succeeded
run: |
echo "${{ toJson(needs) }}"
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or were cancelled."
exit 1
fi
echo "All required jobs passed."

View file

@ -1,38 +0,0 @@
name: Publish to winget
# Custom dist publish job (wired in through `publish-jobs` in dist-workspace.toml).
# After dist creates the GitHub Release, this submits the new version to the
# Windows Package Manager Community Repository (microsoft/winget-pkgs) using the
# Windows .zip artifact that dist already builds and attaches to the release.
#
# Requires a `WINGET_TOKEN` repository secret: a *classic* GitHub PAT with the
# `public_repo` scope that owns a fork of microsoft/winget-pkgs under the
# `fork-user` account (see below). Fine-grained tokens are not supported -- they
# can commit but cannot open the cross-fork PR to winget-pkgs. Without it the job
# fails at submission time.
on:
workflow_call:
inputs:
plan:
required: true
type: string
secrets:
WINGET_TOKEN:
required: true
description: "GitHub PAT with public_repo scope for winget-pkgs PR"
jobs:
winget:
name: Publish winget package
# winget-pkgs only accepts stable versions; never submit prereleases.
if: ${{ !fromJson(inputs.plan).announcement_is_prerelease }}
runs-on: ubuntu-latest
steps:
- name: Submit skim to the Windows Package Manager Community Repository
uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: skim-rs.skim
installers-regex: '-pc-windows-msvc\.zip$'
release-tag: ${{ fromJson(inputs.plan).announcement_tag }}
fork-user: skim-rs
token: ${{ secrets.WINGET_TOKEN }}

27
.gitignore vendored
View file

@ -10,31 +10,6 @@
# Generated by Cargo
/target/
/bin/sk
/bin/*
.idea/
.ropeproject/
.direnv
# Coverage
lcov.info
codecov.json
*.profraw
# Profiling
profile.json.gz
*.data
*.data.old
flamegraph.svg
cachegrind.out.*
/scripts/data/
__pycache__/
# Vagrant
.vagrant/
.envrc.local
/public
.codex
/coverage.xml

View file

@ -1,2 +1,2 @@
error_on_line_overflow = false
max_width = 120
imports_granularity = "Module"

148
AGENTS.md
View file

@ -1,148 +0,0 @@
# Skim Agent Guidelines
## Build/Test/Lint Commands
- Build: `cargo build [--release]`
- Run: `cargo run [--release]`
- Test (all): `cargo nextest run`
- Test (single): `cargo nextest test_name`
- Integration/E2E tests: `cargo nextest run --tests` (drives `sk` through Zellij under the hood; needs `zellij` >= 0.44 and `bash` on `$PATH`)
- Memory leak detection: `cargo nextest run --profile valgrind`
- Thread leak/race detection:
1. Build: `RUSTFLAGS="-Zsanitizer=thread" cargo +nightly build --tests -Zbuild-std --target x86_64-unknown-linux-gnu`
2. Run: `TSAN_OPTIONS="detect_deadlocks=1" cargo +nightly nextest run --profile tsan --target x86_64-unknown-linux-gnu`
- Lint: `cargo clippy`
- Format: `cargo +nightly fmt` (check only: `cargo +nightly fmt --check`)
- Fuzz (requires nightly + `cargo install cargo-fuzz`): `cargo +nightly fuzz run <target>` — see `fuzz/README.md` for target list
## Code Style
- Format with 120 char line width (defined in .rustfmt.toml)
- Use standard Rust naming conventions (snake_case for functions/variables, CamelCase for types)
- Organize imports by standard library, external crates, then internal modules
- Prefer Option/Result types for error handling over panicking
- Use proper error propagation with `?` operator
- Document public API with rustdoc comments
- Use meaningful type annotations, especially for public functions
- Follow the existing structure for new modules (see src/engine/ or src/model/)
- Implement relevant traits (SkimItem, etc.) for new types when needed
## Architecture Documentation
- `ARCHITECTURE.md` documents the full architecture: data flow, operating modes, subsystems, threading model, and public API.
- **Update `ARCHITECTURE.md` whenever you make structural changes**, including:
- Adding, removing, or renaming modules, structs, or traits
- Changing the data flow between subsystems (reader → pool → matcher → TUI)
- Adding new operating modes or modifying existing ones
- Changing the threading model or synchronization primitives
- Adding or removing public API surface (`SkimItem`, `SkimOptions`, `SkimOutput`, etc.)
- Changing the event/action system or key binding infrastructure
## Testing
The end-to-end tests drive a real `sk` process through a terminal, using the
Zellij-backed harness in `tests/common/zellij.rs` (`ZellijController` + the
`sk_test!` DSL). It requires `zellij` (>= 0.44) and `bash` on `$PATH`. The
harness is cross-platform (Linux, macOS and Windows). The pure-harness tests in
`interactive.rs` run on all three platforms; `execute.rs`, `popup.rs` and
`listen.rs` stay `#![cfg(unix)]` for reasons unrelated to the multiplexer (they
install POSIX mock binaries / bind a unix socket), so they run on Linux and
macOS. A few harness details make the non-Linux runners work: the pane's shell
is resolved to an absolute `bash` path (the Zellij server's environment may lack
`bash` on `PATH`); `ZELLIJ_SOCKET_DIR` is forced to a short path so the session's
unix socket path stays under the OS cap (macOS's default `$TMPDIR` is too long);
the drain thread answers the client's cursor-position report (`ESC[6n`), which
the Windows ConPTY client blocks on to learn the terminal size; and `wait_ready`
nudges the client's terminal size until the server gives the pane a non-zero
geometry to render into. The harness drives Zellij with:
- `zellij attach --create <session>` (spawned on an in-process PTY via
`portable-pty`) to start a detached session; `SKIM_DEFAULT_OPTIONS` and friends
are cleared on the spawned process.
- `zellij --session <session> action write <bytes...>` to inject keystrokes.
- `zellij --session <session> action dump-screen [--ansi]` to capture the pane.
When exploring manually you can reproduce the same flow with those commands; the
config the harness writes disables startup tips, pane frames and the kitty
keyboard protocol (so injected legacy escape sequences reach `sk`).
## Insta Snapshot Tests
Most TUI behaviour is covered by insta snapshot tests in `tests/`. The
infrastructure lives in `tests/common/insta.rs` and is exposed through two
macros: `snap!` and `insta_test!`.
### `insta_test!` — writing tests
**Simple variant** (single snapshot, no interaction):
```rust
insta_test!(my_test, ["item1", "item2"], &["--opt1", "opt2"]);
insta_test!(my_test, @cmd "printf 'a\nb'", &["--ansi"]);
insta_test!(my_test, @interactive, &["-i", "--cmd", "echo {q}"]);
```
**DSL variant** (multiple snapshots with interaction between them):
```rust
insta_test!(my_test, ["a", "b", "c"], &["--multi"], {
@snap; // take a snapshot (cell text only)
@snap_color; // snapshot cell styling (fg/bg/modifier) instead
@key Up; // send a named key (Enter, Down, Tab, …)
@char 'f'; // send a single character
@type "foo"; // type a string
@ctrl 'w'; // Ctrl+key
@alt 'b'; // Alt+key
@shift Tab; // Shift+key
@action Last; // send an Action variant (no args)
@action Down(1); // send an Action variant (with args)
@snap; // take another snapshot
@assert(|h| condition); // boolean assertion (does not snapshot)
@exited 0; // assert the app exited with this code
});
```
### Snapshot file naming
| Macro form | File pattern | Example |
|---|---|---|
| Simple variant | `{file}__{test}.snap` | `options__opt_wrap.snap` |
| DSL variant — Nth `@snap` | `{file}__{test}@{NNN}.snap` | `options__opt_cycle@002.snap` |
| DSL variant — Nth `@snap_color` | `{file}__{test}@color{NNN}.snap` | `ansi__ansi_flag_enabled@color002.snap` |
DSL snapshots use a zero-padded three-digit suffix (`@001`, `@002`, …) so that
`cargo insta review` presents them in the order they were taken.
### Snapshot front-matter
Every snapshot includes a `description` field in its YAML front-matter. For a
DSL test the description shows the input, options, and the DSL commands that
ran **since the previous `@snap`**, making it easy to understand what state
each screenshot captures:
```
description: "input: items [\"a\", \"b\", \"c\"]\noptions: --multi\nafter:\n @key Up\n @shift Tab"
```
The `expression` field is intentionally omitted (`omit_expression = true`) to
keep the files free of internal implementation details.
### Snapshot workflow
Generate / update snapshots:
```sh
# Generate all missing snapshots and accept them immediately:
INSTA_UPDATE=always cargo nextest run
# Generate missing snapshots as .snap.new files for manual review:
INSTA_UPDATE=new cargo nextest run
cargo insta review # accept / reject interactively
```
When **adding new tests** that produce snapshots:
1. Write the test with `@snap` markers.
2. Run `INSTA_UPDATE=always cargo nextest run --test <file> <test_name>` to
generate the initial snapshot files.
3. Inspect the generated `.snap` files to verify the rendered output is correct.
4. Commit both the test and its snapshot files.
When **changing rendering logic** that affects many existing snapshots:
1. Delete the affected `.snap` files:
`find tests/snapshots -name "prefix__*.snap" -delete`
2. Regenerate: `INSTA_UPDATE=always cargo nextest run`
3. Review the diff with `git diff tests/snapshots/` before committing.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
AGENTS.md

3633
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
[package]
name = "skim"
version = "5.7.0"
authors = ["Loric ANDRE", "Zhang Jinzhou <lotabout@gmail.com>"]
version = "0.9.4"
authors = ["Zhang Jinzhou <lotabout@gmail.com>"]
description = "Fuzzy Finder in rust!"
documentation = "https://docs.rs/skim"
repository = "https://github.com/skim-rs/skim"
homepage = "https://github.com/lotabout/skim"
repository = "https://github.com/lotabout/skim"
readme = "README.md"
keywords = ["fuzzy", "menu", "util"]
license = "MIT"
edition = "2024"
rust-version = "1.91"
default-run = "sk"
edition = "2018"
[lib]
name = "skim"
@ -18,162 +18,33 @@ path = "src/lib.rs"
[[bin]]
name = "sk"
path = "src/bin/main.rs"
required-features = ["cli"]
[dependencies]
nix = "0.25.0"
atty = "0.2.14"
regex = "1.6.0"
lazy_static = "1.4.0"
shlex = "1.1.0"
unicode-width = "0.1.9"
log = "0.4.17"
env_logger = "0.9.0"
time = "0.3.13"
clap = "3.2.22"
tuikit = "0.5.0"
vte = "0.11.0"
fuzzy-matcher = "0.3.7"
rayon = "1.5.3"
derive_builder = "0.11.2"
bitflags = "1.3.2"
timer = "0.2.0"
chrono = "0.4.22"
crossbeam = "0.8.2"
beef = "0.5.2" # compact cow
defer-drop = "1.2.0"
[features]
# Default is destined to the CLI, not to library usage.
default = ["cli", "frizbee", "image", "listen"]
# Everything needed to use skim as a cli (argument parsing, shell integrations...). This should not be needed for most libraries.
cli = ["dep:clap", "dep:clap_complete", "dep:clap_complete_nushell", "dep:shlex", "dep:env_logger", "dep:clap_mangen"]
# Include frizbee as a matching algorithm
frizbee = ["dep:frizbee"]
# Enable image previews (renders the preview argument as an image)
image = ["dep:image", "dep:ratatui-image"]
# Enable the IPC socket (--listen / --remote), driving skim from other processes
listen = ["dep:interprocess", "dep:ron", "dep:serde"]
# Enable gungraun (Valgrind-based) benchmarks
gungraun = ["dep:gungraun"]
default = []
[profile.release]
lto = true
codegen-units = 1
opt-level = 2
strip = true
# Kept for compatibility
[profile.dist]
inherits = "release"
[profile.release-debug]
inherits = "release"
debug = true
strip = false
[profile.dev]
debug = "line-tables-only"
split-debuginfo = "unpacked"
[profile.dev.package."*"]
debug = false
[lints.rust]
missing_docs = "warn"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage, coverage_nightly)'] }
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
incompatible_msrv = "warn"
collapsible_match = "allow"
default_trait_access = "allow"
[dependencies]
ansi-to-tui = "8.0.1"
assert_enum_variants = "0.1.2"
clap = { version = "4.6.1" , optional = true, features = ["cargo", "derive", "unstable-markdown"] }
clap_complete = { version = "4.6.5", optional = true }
clap_complete_nushell = { version = "4.6.0", optional = true }
clap_mangen = { version = "0.3.0", optional = true }
eyre = "0.6.12"
# Crossterm's version is selected by ratatui
crossterm = { version = ">=0.0.0", features = ["event-stream", "use-dev-tty", "libc"] }
derive_builder = "0.20.2"
derive_more = { version = "2.1.1", features = ["debug", "eq"] }
env_logger = { version = "0.11.10", optional = true, features = ["humantime"] }
frizbee = { version = "=0.13.0", optional = true }
futures = "0.3.32"
gungraun = { version = "0.19.1", optional = true }
image = { version = "0.25.10", optional = true, default-features = false, features = ["png", "jpeg", "gif", "webp"] }
indexmap = "2.13.1"
interprocess = { version = "2.4.2", features = ["tokio"], optional = true }
kanal = "0.1.1"
log = "0.4.31"
memchr = "2.8.1"
mimalloc = { version = "0.1.48", features = ["v3"] }
nix = { version = "0.31.3", features = ["fs", "poll", "signal"] }
portable-pty = "0.9.0"
ratatui = "0.30.0"
ratatui-image = { version = "11.0.4", features = ["crossterm"], default-features = false, optional = true }
regex = "1.12.3"
roff = "1.1.1"
ron = { version = "0.12.1", optional = true }
serde = { version = "1.0.228", features = ["derive"], optional = true }
shell-quote = "0.8.0"
shlex = { version = "2.0.1", optional = true }
tempfile = "3.27.0"
thiserror = "2.0.18"
thread_local = "1.1.9"
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "sync", "time", "tokio-macros"] }
tokio-util = "0.7.18"
tui-term = "0.3.4"
unicode-display-width = "0.3.0"
unicode-normalization = "0.1.25"
which = "8.0.2"
[dev-dependencies]
criterion = { version = "0.8.2", features = ["async_tokio"] }
gnuplot = "0.0.46"
insta = "1.47"
rand = "0.10.0"
serde_json = { version = "=1.0.151" }
serial_test = "=3.5.0"
[[bench]]
name = "read_and_match"
harness = false
[[bench]]
name = "filter"
harness = false
[[bench]]
name = "partial"
harness = false
[[bench]]
name = "matcher_micro"
harness = false
[[bench]]
name = "gungraun"
harness = false
required-features = ["gungraun"]
[[bench]]
name = "cli"
harness = false
bench = false
[package.metadata.wix]
upgrade-guid = "6DDAED06-EBE2-41C4-94F5-CB03F2A4B92E"
path-guid = "05D1A327-19E0-4C82-B5EC-6B28E40C691B"
license = false
eula = false
# Debian package (.deb) built via `cargo deb`.
# Ships the `sk` executable, the man pages and the shell completions.
[package.metadata.deb]
maintainer = "Loric ANDRE, Zhang Jinzhou <lotabout@gmail.com>"
copyright = "The skim developers"
section = "utils"
priority = "optional"
extended-description = "skim is a general purpose fuzzy finder written in Rust, usable as a command line tool and as a library."
assets = [
["target/release/sk", "usr/bin/", "755"],
["man/man1/sk.1", "usr/share/man/man1/", "644"],
["shell/completion.bash", "usr/share/bash-completion/completions/sk", "644"],
["shell/completion.zsh", "usr/share/zsh/vendor-completions/_sk", "644"],
["shell/completion.fish", "usr/share/fish/vendor_completions.d/sk.fish", "644"],
["README.md", "usr/share/doc/skim/README.md", "644"],
]
# RPM package (.rpm) built via `cargo generate-rpm`.
# Ships the `sk` executable, the man pages and the shell completions.
[package.metadata.generate-rpm]
assets = [
{ source = "target/release/sk", dest = "/usr/bin/sk", mode = "755" },
{ source = "man/man1/sk.1", dest = "/usr/share/man/man1/sk.1", mode = "644", doc = true },
{ source = "shell/completion.bash", dest = "/usr/share/bash-completion/completions/sk", mode = "644" },
{ source = "shell/completion.zsh", dest = "/usr/share/zsh/site-functions/_sk", mode = "644" },
{ source = "shell/completion.fish", dest = "/usr/share/fish/vendor_completions.d/sk.fish", mode = "644" },
{ source = "README.md", dest = "/usr/share/doc/skim/README.md", mode = "644", doc = true },
{ source = "LICENSE", dest = "/usr/share/doc/skim/LICENSE", mode = "644", doc = true },
]

604
README.md
View file

@ -1,206 +1,109 @@
<p align="center">
<a href="https://crates.io/crates/skim">
<img src="https://img.shields.io/crates/v/skim.svg" alt="Crates.io" />
</a>
<a href="https://github.com/skim-rs/skim/actions?query=workflow%3A%22Build+%26+Test%22+event%3Apush">
<img src="https://github.com/skim-rs/skim/actions/workflows/test.yml/badge.svg?event=push" alt="Build & Test" />
</a>
<a href="https://skim-rs.github.io/skim/coverage" >
<img src="https://skim-rs.github.io/skim/coverage/coverage.svg" alt="coverage badge" />
</a>
<a href="https://repology.org/project/skim-fuzzy-finder/versions">
<img src="https://repology.org/badge/tiny-repos/skim-fuzzy-finder.svg" alt="Packaging status" />
</a>
<a href="https://discord.gg/23PuxttufP">
<img alt="Skim Discord" src="https://img.shields.io/discord/1031830957432504361?label=&color=7389d8&labelColor=6a7ec2&logoColor=ffffff&logo=discord" />
</a>
<a href="https://matrix.to/#/#skim:matrix.org">
<img alt="Skim Matrix room" src="https://img.shields.io/badge/matrix-%23000000?style=flat&logo=matrix&logoColor=white" />
</a>
<a href="https://ratatui.rs">
<img alt="Built with Ratatui" src="https://ratatui.rs/built-with-ratatui/badge.svg" />
</a>
</p>
[![Crates.io](https://img.shields.io/crates/v/skim.svg)](https://crates.io/crates/skim)
[![Build & Test](https://github.com/lotabout/skim/workflows/Build%20&%20Test/badge.svg)](https://github.com/lotabout/skim/actions?query=workflow%3A%22Build+%26+Test%22)
[![Packaging status](https://repology.org/badge/tiny-repos/skim.svg)](https://repology.org/project/skim/versions)
> Life is short, skim!
We spend so much of our time navigating through files, lines, and commands. That's where Skim comes in!
It's a powerful fuzzy finder designed to make your workflow faster and more efficient.
Half of our life is spent on navigation: files, lines, commands… You need skim!
It is a general fuzzy finder that saves you time.
[![skim demo](https://asciinema.org/a/pIfwazaM0mTHA8F7qRbjrqOnm.svg)](https://asciinema.org/a/pIfwazaM0mTHA8F7qRbjrqOnm)
Skim provides a single executable called `sk`. Think of it as a smarter alternative to tools like
`grep` - once you try it, you'll wonder how you ever lived without it!
skim provides a single executable: `sk`. Basically anywhere you would want to use
`grep`, try `sk` instead.
# Table of contents
- [Installation](#installation)
* [Package Managers](#package-managers)
* [Manually](#manually)
- [Usage](#usage)
* [As Vim plugin](#as-vim-plugin)
* [As filter](#as-filter)
* [As Interactive Interface](#as-interactive-interface)
* [Shell Bindings](#shell-bindings)
* [Key Bindings](#key-bindings)
* [Search Syntax](#search-syntax)
* [exit code](#exit-code)
- [Tools compatible with `skim`](#tools-compatible-with-skim)
* [fzf-lua neovim plugin](#fzf-lua-neovim-plugin)
* [nu_plugin_skim](#nu_plugin_skim)
- [As Filter](#as-filter)
- [As Interactive Interface](#as-interactive-interface)
- [Key Bindings](#key-bindings)
- [Search Syntax](#search-syntax)
- [Exit code](#exit-code)
- [Customization](#customization)
* [Keymap](#keymap)
* [Sort Criteria](#sort-criteria)
* [Color Scheme](#color-scheme)
* [Misc](#misc)
- [Advanced Topics](#advanced-topics)
* [Interactive mode](#interactive-mode)
+ [How does it work?](#how-does-it-work)
* [Executing external programs](#executing-external-programs)
* [Algorithms](#algorithms)
* [Preview Window](#preview-window)
+ [How does it work?](#how-does-it-work-1)
* [Fields support](#fields-support)
* [Use as a library](#use-as-a-library)
* [Benchmarks](#benchmarks)
- [Keymap to redefine](#keymap)
- [Sort Criteria](#sort-criteria)
- [Color Scheme](#color-scheme)
- [Misc](#misc)
- [Advance Topics](#advance-topics)
- [Interactive Mode](#interactive-mode)
- [Executing external programs](#executing-external-programs)
- [Preview Window](#preview-window)
- [Fields Support](#fields-support)
- [Use as a Library](#use-as-a-library)
- [FAQ](#faq)
* [How to ignore files?](#how-to-ignore-files)
* [Some files are not shown in Vim plugin](#some-files-are-not-shown-in-vim-plugin)
- [Differences from fzf](#differences-from-fzf)
- [How to ignore files?](#how-to-ignore-files)
- [Some files are not shown in vim plugin](#some-files-are-not-shown-in-vim-plugin)
- [Differences to fzf](#differences-to-fzf)
- [How to contribute](#how-to-contribute)
* [Windows compatibility testing](#windows-compatibility-testing)
- [Troubleshooting](#troubleshooting)
* [No line feed issues with nix, FreeBSD, termux](#no-line-feed-issues-with-nix-freebsd-termux)
# Installation
The skim project contains several components:
1. `sk` executable - the core program
2. Vim/Nvim plugin - to call `sk` inside Vim/Nvim. Check [skim.vim](https://github.com/skim-rs/skim/blob/master/plugin/skim.vim) for Vim support.
1. `sk` executable -- the core.
2. `sk-tmux` -- script for launching `sk` in a tmux pane.
3. Vim/Nvim plugin -- to call `sk` inside Vim/Nvim. check [skim.vim](https://github.com/lotabout/skim.vim) for more Vim support.
## Package Managers
| OS | Package Manager | Command |
| -------------- | --------------- | ---------------------------- |
| macOS | Homebrew | `brew install sk` |
| macOS | MacPorts | `sudo port install skim` |
| Alpine | apk | `apk add skim` |
| Arch | pacman | `pacman -S skim` |
| Fedora | COPR | see below |
| Gentoo | Portage | `emerge --ask app-misc/skim` |
| Guix | guix | `guix install skim` |
| Void | XBPS | `xbps-install -S skim` |
| Windows | winget | `winget install skim` |
| Windows | Scoop | `scoop install skim` |
| Debian/Ubuntu | apt | see below |
| Fedora/RHEL | dnf | see below |
| Distribution | Package Manager | Command |
| -------------- | ----------------- | ------------------------- |
| macOS | Homebrew | `brew install sk` |
| macOS | MacPorts | `sudo port install skim` |
| Fedora | dnf | `dnf install skim` |
| Alpine | apk | `apk add skim` |
| Arch | pacman | `pacman -S skim` |
<a href="https://repology.org/project/skim-fuzzy-finder/versions">
<img src="https://repology.org/badge/vertical-allrepos/skim-fuzzy-finder.svg?columns=4" alt="Packaging status">
</a>
### Debian/Ubuntu
A custom APT repository is available and updated automatically during each release:
1. Import the signing key
With wget:
```sh
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/skim.asc https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc
```
Or with cURL:
```sh
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc | sudo tee /etc/apt/keyrings/skim.asc > /dev/null
```
2. Add the repository
```sh
echo 'deb [signed-by=/etc/apt/keyrings/skim.asc] https://skim-rs.github.io/skim/apt ./' | sudo tee /etc/apt/sources.list.d/skim.list
sudo apt-get update
```
3. Install
```sh
sudo apt-get install skim
```
Alternatively, `.deb` packages are attached directly to every [release](https://github.com/skim-rs/skim/releases/latest).
Download the one matching your architecture and run `sudo dpkg -i skim_*_amd64.deb`
See [repology](https://repology.org/project/skim/versions) for a comprehensive overview of package availability.
### Fedora/RHEL
Up-to-date Fedora/RHEL packages are provided via an unofficial community-maintained COPR repository.
```bash
sudo dnf copr enable sisyphus1813/skim
sudo dnf install skim
```
Alternatively, `.rpm` packages are attached directly to every [release](https://github.com/skim-rs/skim/releases/latest).
Download it and run `sudo rpm -i skim-*.x86_64.rpm`
## Manually
Any of the following applies:
- Using the install script:
```sh
# Always check the content of the script before running it !
$ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/skim-rs/skim/releases/latest/download/skim-installer.sh | sh
```
- Using Binary: Simply [download the sk executable](https://github.com/skim-rs/skim/releases) directly.
- Install from [crates.io](https://crates.io/): `cargo install skim`
- Build Manually:
```sh
$ git clone --depth 1 git@github.com:skim-rs/skim.git ~/.skim
$ cd ~/.skim
$ cargo build --release
$ # Add the resulting `target/release/sk` executable to your PATH
```
You will then have access to:
- The man page, which you can either write to the correct path or run `man --local-file <(sk --man)`
- The shell completions (and optional keybinds), using `source <(sk --shell \<shell> \[--shell-bindings])`, see below for details
# Usage
Skim can be used either as a general filter (similar to `grep`) or as an interactive
interface for running commands.
## As Vim plugin (on neovim, checkout [fzf-lua](https://github.com/ibhagwan/fzf-lua) with the skim profile)
## Install as Vim plugin
Via vim-plug (recommended):
Install skim, then :
```vim
Plug 'skim-rs/skim'
Plug 'lotabout/skim', { 'dir': '~/.skim', 'do': './install' }
```
## Hard Core
Any of the following applies:
- Using Git
```sh
$ git clone --depth 1 git@github.com:lotabout/skim.git ~/.skim
$ ~/.skim/install
```
- Using Binary: directly [download the sk executable](https://github.com/lotabout/skim/releases).
- Install from [crates.io](https://crates.io/): `cargo install skim`
- Build Manually
```sh
$ git clone --depth 1 git@github.com:lotabout/skim.git ~/.skim
$ cd ~/.skim
$ cargo install
$ cargo build --release
$ # put the resulting `target/release/sk` executable on your PATH.
```
# Usage
skim can be used as a general filter (like `grep`) or as an interactive
interface for invoking commands.
## As filter
Here are some examples to get you started:
Try the following
```bash
# directly invoke skim
sk
# Or pipe some input to it (press TAB key to select multiple items when -m is enabled)
# or pipe some input to it: (press TAB key select multiple items with -m enabled)
vim $(find . -name "*.rs" | sk -m)
```
This last command lets you select files with the ".rs" extension and opens
your selections in Vim - a great time-saver for developers!
The above command will allow you to select files with ".rs" extension and open
the ones you selected in Vim.
## As Interactive Interface
@ -213,63 +116,17 @@ project directory:
```sh
# works with grep
sk --ansi -i -c 'grep -rI --color=always --line-number {q} .'
sk --ansi -i -c 'grep -rI --color=always --line-number "{}" .'
# works with ack
sk --ansi -i -c 'ack --color {q}'
sk --ansi -i -c 'ack --color "{}"'
# works with ag
sk --ansi -i -c 'ag --color {q}'
sk --ansi -i -c 'ag --color "{}"'
# works with rg
sk --ansi -i -c 'rg --color=always --line-number {q}'
sk --ansi -i -c 'rg --color=always --line-number "{}"'
```
> **Note**: In these examples, `{q}` will be literally expanded to the current input query (wrapped in single quotes).
> This means these examples will search for the exact query string, not fuzzily.
> For fuzzy searching, pipe the command output into `sk` without using interactive mode.
![interactive mode demo](https://cloud.githubusercontent.com/assets/1527040/21603930/655d859a-d1db-11e6-9fec-c25099d30a12.gif)
## Shell Bindings
Bindings for Fish, Bash and Zsh are available in the `shell` directory:
- `completion.{shell}` contains the completion scripts for `sk` cli usage
- `key-bindings.{shell}` contains key-binds and shell integrations:
- `ctrl-t` to select a file through `sk`
- `ctrl-r` to select an history entry through `sk`
- `alt-c` to `cd` into a directory selected through `sk`
- (not available in `fish`) `**` to complete file paths, for example `ls **<tab>` will show a `sk` widget to select a folder
To enable these features, source the `key-bindings.{shell}` file and set up completions according to your shell's documentation or see below.
### Shell Completions
You can generate shell completions for your preferred shell using the `--shell` flag with one of the supported shells: `bash`, `zsh`, `fish`, `powershell`, or `elvish`:
#### Option 1: Source directly in your current shell session
```sh
# For bash
source <(sk --shell bash)
# For zsh
source <(sk --shell zsh)
# For fish
sk --shell fish | source
```
#### Option 2: Save to a file to be loaded automatically on shell startup
```sh
# For bash, add to ~/.bashrc
echo 'source <(sk --shell bash)' >> ~/.bashrc # Or save to ~/.bash_completion
# For zsh, add to ~/.zshrc
sk --shell zsh > ~/.zfunc/_sk # Create ~/.zfunc directory and add to fpath in ~/.zshrc
# For fish, add to ~/.config/fish/completions/
sk --shell fish > ~/.config/fish/completions/sk.fish
```
## Key Bindings
Some commonly used key bindings:
@ -283,12 +140,12 @@ Some commonly used key bindings:
| TAB | Toggle selection and move down (with `-m`) |
| Shift-TAB | Toggle selection and move up (with `-m`) |
For a complete list of key bindings, refer to the [man
page](https://github.com/skim-rs/skim/blob/master/man/man1/sk.1) (`man sk`).
For full list of key bindings, check out the [man
page](https://github.com/lotabout/skim/blob/master/man/man1/sk.1) (`man sk`).
## Search Syntax
`skim` borrows `fzf`'s syntax for matching items:
`skim` borrowed `fzf`'s syntax for matching items:
| Token | Match type | Description |
|----------|----------------------------|-----------------------------------|
@ -306,14 +163,10 @@ page](https://github.com/skim-rs/skim/blob/master/man/man1/sk.1) (`man sk`).
- ` | ` means `OR` (note the spaces around `|`). With the term `.md$ |
.markdown$`, `skim` will search for items ends with either `.md` or
`.markdown`.
- `OR` has higher precedence. For example, `readme .md$ | .markdown$` is interpreted as
- `OR` has higher precedence. So `readme .md$ | .markdown$` is grouped into
`readme AND (.md$ OR .markdown$)`.
- When using the `--split-match` option, each part around spaces or `|` will be matched in a split way:
- If the option's value (defaulting to `:`) is absent from the query, do a normal match
- If it is present, match everything before to everything before it in the items, and everything after it (including potential other occurrences of the delimiter) to the part after it in the items. This is particularly useful when piping in input from `rg` to match on both file name and content.
If you prefer using regular expressions, `skim` offers a `regex` mode:
In case that you want to use regular expressions, `skim` provides `regex` mode:
```sh
sk --regex
@ -323,44 +176,11 @@ You can switch to `regex` mode dynamically by pressing `Ctrl-R` (Rotate Mode).
## exit code
| Exit Code | Meaning |
|-----------|-------------------------------------|
| 0 | Exited normally |
| 1 | No Match found |
| 130 | Aborted by Ctrl-C/Ctrl-G/ESC/etc... |
# Tools compatible with `skim`
These tools are or aim to be compatible with `skim`:
## [fzf-lua neovim plugin](https://github.com/ibhagwan/fzf-lua)
A [neovim](https://neovim.io) plugin allowing fzf and skim to be used in a to navigate your code.
Install it with your package manager, following the README. For instance, with `lazy.nvim`:
```lua
{
"ibhagwan/fzf-lua",
-- enable `sk` support instead of the default `fzf`
opts = {'skim'}
}
```
## [nu_plugin_skim](https://github.com/idanarye/nu_plugin_skim)
A [nushell](https://www.nushell.sh/) plugin to allow for better interaction between skim and nushell.
Following the instruction in the plugin's README, you can install it with cargo:
```nu
cargo install nu_plugin_skim
plugin add ~/.cargo/bin/nu_plugin_skim
```
## [sqlite extension](https://github.com/tzachar/sqlite_skim)
An `sqlite` loadable module which enables a `skim_score` function in SQL
queries.
| Exit Code | Meaning |
|-----------|-----------------------------------|
| 0 | Exit normally |
| 1 | No Match found |
| 130 | Abort by Ctrl-C/Ctrl-G/ESC/etc... |
# Customization
@ -369,7 +189,7 @@ list of options.
## Keymap
Specify the bindings with comma separated pairs (no space allowed). For example:
Specify the bindings with comma separated pairs (no space allowed), example:
```sh
sk --bind 'alt-a:select-all,alt-d:deselect-all'
@ -381,84 +201,29 @@ See the _KEY BINDINGS_ section of the man page for details.
## Sort Criteria
There are five sort keys for results: `score, index, begin, end, length`. You can
There are five sort keys for results: `score, index, begin, end, length`, you can
specify how the records are sorted by `sk --tiebreak score,index,-begin` or any
other order you want.
## Color Scheme
You probably have your own aesthetic preferences! Fortunately, you aren't
limited to the default appearance - Skim supports comprehensive customization of its color scheme.
It is a high chance that you are a better artist than me. Luckily you won't
be stuck with the default colors, `skim` supports customization of the color scheme.
```sh
--color=[BASE_SCHEME][,COLOR:ANSI]
```
Skim also respects the `NO_COLOR` environment variable. Set it to anything and `sk` (and many other terminal apps) will disable all colored output. See [no-color.org](https://no-color.org/) for more details.
The configuration of colors starts with the name of the base color scheme,
followed by custom color mappings. For example:
### Available Base Color Schemes
Skim comes with several built-in color schemes that you can use as a starting point:
```sh
sk --color=dark # Default dark theme (256 colors)
sk --color=light # Light theme (256 colors)
sk --color=16 # Simple 16-color theme
sk --color=bw # Minimal black & white theme (no colors, just styles)
sk --color=none # Minimal black & white theme (no colors, no styles)
sk --color=molokai # Molokai-inspired theme (256 colors)
```
### Customizing Colors
You can customize individual UI elements by specifying color values after the base scheme:
```sh
sk --color=current_bg:24
sk --color=light,fg:232,bg:255,current_bg:116,info:27
```
Colors can be specified in several ways:
- ANSI colors (0-255): `sk --color=fg:232,bg:255`
- RGB hex values: `sk --color=fg:#FF0000` (red text)
### Available Color Customization Options
The following UI elements can be customized:
| Element | Description | Example |
|--------------------|---------------------------------------------|--------------------------------|
| `fg` | Normal text foreground color | `--color=fg:232` |
| `bg` | Normal text background color | `--color=bg:255` |
| `matched` | Matched text in search results | `--color=matched:108` |
| `matched_bg` | Background of matched text | `--color=matched_bg:0` |
| `current` | Current line foreground color | `--color=current:254` |
| `current_bg` | Current line background color | `--color=current_bg:236` |
| `current_match` | Matched text in current line | `--color=current_match:151` |
| `current_match_bg` | Background of matched text in current line | `--color=current_match_bg:236` |
| `spinner` | Progress indicator color | `--color=spinner:148` |
| `info` | Information line color | `--color=info:144` |
| `prompt` | Prompt color | `--color=prompt:110` |
| `cursor` | Cursor color | `--color=cursor:161` |
| `selected` | Selected item marker color | `--color=selected:168` |
| `header` | Header text color | `--color=header:109` |
| `border` | Border color for preview/layout | `--color=border:59` |
| `scrollbar` | Item list scrollbar thumb color | `--color=scrollbar:59` |
### Examples
```sh
# Use light theme but change the current line background
sk --color=light,current_bg:24
# Custom theme with multiple colors
sk --color=dark,matched:#00FF00,current:#FFFFFF,current_bg:#000080
# High contrast theme
sk --color=fg:232,bg:255,matched:160,current:255,current_bg:20
```
For more details, check the man page (`man sk`).
See `--color` option in the man page for details.
## Misc
@ -469,28 +234,28 @@ For more details, check the man page (`man sk`).
## Interactive mode
In **interactive mode**, you can invoke a command dynamically. Try it out:
With "interactive mode", you could invoke command dynamically. Try out:
```sh
sk --ansi -i -c 'rg --color=always --line-number {q}'
sk --ansi -i -c 'rg --color=always --line-number "{}"'
```
### How does it work?
How it works?
![How Skim's interactive mode works](https://user-images.githubusercontent.com/1527040/53381293-461ce380-39ab-11e9-8e86-7c3bbfd557bc.png)
![skim's interactive mode](https://user-images.githubusercontent.com/1527040/53381293-461ce380-39ab-11e9-8e86-7c3bbfd557bc.png)
- Skim accepts two kinds of sources: Command output or piped input
- Skim could accept two kinds of source: command output or piped input
- Skim has two kinds of prompts: A query prompt to specify the query pattern and a
command prompt to specify the "arguments" of the command
- `-c` is used to specify the command to execute and defaults to `SKIM_DEFAULT_COMMAND`
- `-i` tells skim to open command prompt on startup, which will show `c>` by default.
- `-c` is used to specify the command to execute while defaults to `SKIM_DEFAULT_COMMAND`
- `-i` is to tell skim open command prompt on startup, which will show `c>` by default.
To further narrow down the results returned by the command, press
If you want to further narrow down the results returned by the command, press
`Ctrl-Q` to toggle interactive mode.
## Executing external programs
You can configure key bindings to start external processes without leaving Skim (`execute`, `execute-silent`).
You can set up key bindings for starting external processes without leaving skim (`execute`, `execute-silent`).
```sh
# Press F1 to open the file with less without leaving skim
@ -498,29 +263,20 @@ You can configure key bindings to start external processes without leaving Skim
sk --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort'
```
## Algorithms
Skim offers multiple algorithms, check the help or manpage for an exhaustive list. Among them are:
- `skim_v2`, the default algorithm, loosely based on `fzf`'s algorithm
- `frizbee`, uses [frizbee](https://crates.io/frizbee), the typo-resistant algorithm from the [blink.cmp](https://github.com/saghen/blink.cmp) neovim plugin
- `fzy`, based on [fzy](https://github.com/jhawthorn/fzy/)'s algorithm expanded for basic typo-resistance
- `arinae`, skim's newest algorithm, designed in-house with typo-resistance in mind, expanding on all the above to make typo-resistant matching feel more natural while keeping the per-item performance up to the best standards
## Preview Window
This is a great feature of fzf that skim borrows. For example, we use 'ag' to
find the matched lines, and once we narrow down to the target lines, we want to
find the matched lines, once we narrow down to the target lines, we want to
finally decide which lines to pick by checking the context around the line.
`grep` and `ag` have the option `--context`, and skim can make use of `--context` for
a better preview window. For example:
`grep` and `ag` has an option `--context`, skim can do better with preview
window. For example:
```sh
sk --ansi -i -c 'ag --color {q}' --preview "preview.sh {}"
sk --ansi -i -c 'ag --color "{}"' --preview "preview.sh {}"
```
(Note that [preview.sh](https://github.com/junegunn/fzf.vim/blob/master/bin/preview.sh) is a script to print the context given filename:lines:columns)
You get things like this:
(Note the [preview.sh](https://github.com/junegunn/fzf.vim/blob/master/bin/preview.sh) is a script to print the context given filename:lines:columns)
You got things like this:
![preview demo](https://user-images.githubusercontent.com/1527040/30677573-0cee622e-9ebf-11e7-8316-c741324ecb3a.png)
@ -532,12 +288,13 @@ command to get the output, and print the output on the preview window.
Sometimes you don't need the whole line for invoking the command. In this case
you can use `{}`, `{1..}`, `{..3}` or `{1..5}` to select the fields. The
syntax is explained in the section [Fields Support](#filds-support).
syntax is explained in the section "Fields Support".
Lastly, you might want to configure the position of preview window with `--preview-window`:
Last, you might want to configure the position of preview windows, use
`--preview-window`.
- `--preview-window up:30%` to put the window in the up position with height
30% of the total height of skim.
- `--preview-window left:10:wrap` to specify the `wrap` allows the preview
- `--preview-window left:10:wrap`, to specify the `wrap` allows the preview
window to wrap the output of the preview command.
- `--preview-window wrap:hidden` to hide the preview window at startup, later
it can be shown by the action `toggle-preview`.
@ -558,12 +315,12 @@ but not matching line number or column number.
You can use `sk --delimiter ':' --nth 1` to achieve this.
You can also use `--with-nth` to re-arrange the order of fields.
Also you can use `--with-nth` to re-arrange the order of fields.
**Range Syntax**
- `<num>` -- to specify the `num`-th fields, starting with 1.
- `start..` -- starting from the `start`-th fields and the rest.
- `start..` -- starting from the `start`-th fields, and the rest.
- `..end` -- starting from the `0`-th field, all the way to `end`-th field,
including `end`.
- `start..end` -- starting from `start`-th field, all the way to `end`-th
@ -577,14 +334,9 @@ First, add skim into your `Cargo.toml`:
```toml
[dependencies]
skim = { version = "<version>", default-features = false, features = [..] }
skim = "*"
```
_Note on features_:
- the `cli` feature is required to use skim as a cli, it *should* not be needed when using it as a library.
### Basic usage
Then try to run this simple example:
```rust
@ -594,7 +346,7 @@ use std::io::Cursor;
pub fn main() {
let options = SkimOptionsBuilder::default()
.height("50%")
.height(Some("50%"))
.multi(true)
.build()
.unwrap();
@ -612,18 +364,11 @@ pub fn main() {
.unwrap_or_else(|| Vec::new());
for item in selected_items.iter() {
println!("{}", item.output());
print!("{}{}", item.output(), "\n");
}
}
```
### Fine-grained usage
You can also gain fine-grained usage of skim as a library using `tokio` and async code, allowing you to dynamically interact with
### Internal workings
Given an `Option<SkimItemReceiver>`, skim will read items accordingly, do its
job and bring us back the user selection including the selected items, the
query, etc. Note that:
@ -636,25 +381,17 @@ Trait `SkimItem` is provided to customize how a line could be displayed,
compared and previewed. It is implemented by default for `AsRef<str>`
Plus, `SkimItemReader` is a helper to convert a `BufRead` into
`SkimItemReceiver` (we can easily turn a `File` or `String` into `BufRead`),
so that you could deal with strings or files easily.
`SkimItemReceiver` (we can easily turn a `File` for `String` into `BufRead`).
So that you could deal with strings or files easily.
Check out more examples under the [examples/](https://github.com/skim-rs/skim/tree/master/skim/examples) directory.
## Benchmarks
This benchmarks runs the interactive interface in a tmux session, and waits for the UI to stabilize.
![benchmark graphs](./bench.png)
You can generate the graphs by using `just bench-plot` or running the recipe manually in GNU bash.
Check more examples under [examples/](https://github.com/lotabout/skim/tree/master/examples) directory.
# FAQ
## How to ignore files?
Skim invokes `find .` to fetch a list of files for filtering. You can override
this by setting the environment variable `SKIM_DEFAULT_COMMAND`. For example:
that by setting the environment variable `SKIM_DEFAULT_COMMAND`. For example:
```sh
$ SKIM_DEFAULT_COMMAND="fd --type f || git ls-tree -r --name-only HEAD || rg --files || find ."
@ -665,119 +402,38 @@ You could put it in your `.bashrc` or `.zshrc` if you like it to be default.
## Some files are not shown in Vim plugin
If you use the Vim plugin and execute the `:SK` command, you may find some
If you use the Vim plugin and execute the `:SK` command, you might find some
of your files not shown.
As described in [#3](https://github.com/skim-rs/skim/issues/3), in the Vim
As described in [#3](https://github.com/lotabout/skim/issues/3), in the Vim
plugin, `SKIM_DEFAULT_COMMAND` is set to the command by default:
```vim
let $SKIM_DEFAULT_COMMAND = "git ls-tree -r --name-only HEAD || rg --files || ag -l -g \"\" || find ."
```
This means files not recognized by git won't be shown. You can either override the
default with `let $SKIM_DEFAULT_COMMAND = ''` or locate the missing files by
That means, the files not recognized by git will not shown. Either override the
default with `let $SKIM_DEFAULT_COMMAND = ''` or find the missing file by
yourself.
# Differences from fzf
# Differences to fzf
[fzf](https://github.com/junegunn/fzf) is a command-line fuzzy finder written
in Go and [skim](https://github.com/skim-rs/skim) tries to implement a new one
in Go and [skim](https://github.com/lotabout/skim) tries to implement a new one
in Rust!
This project is written from scratch. Some decisions of implementation are
different from fzf. For example:
1. `skim` has an interactive mode.
2. `skim` supports pre-selection.
3. The fuzzy search algorithm is different.
More generally, `skim`'s maintainers allow themselves some freedom of implementation.
The goal is to keep `skim` as feature-full as `fzf` is, but the command flags might differ.
1. `skim` is a binary as well as a library while fzf is only a binary.
2. `skim` has an interactive mode.
3. `skim` supports pre-selection
4. The fuzzy search algorithm is different.
5. ~~UI of showing matched items. `fzf` will show only the range matched while
`skim` will show each character matched.~~ (fzf has this now)
6. ~~`skim`'s range syntax is Git style~~: now it is the same with fzf.
# How to contribute
[Create new issues](https://github.com/skim-rs/skim/issues/new) if you encounter any bugs
[Create new issues](https://github.com/lotabout/skim/issues/new) if you meet any bugs
or have any ideas. Pull requests are warmly welcomed.
## Windows compatibility testing
A `Vagrantfile` is included to spin up a headless Windows Server 2022 Core VM for testing
Windows compatibility without needing a GUI. It requires [VirtualBox](https://www.virtualbox.org/)
and [Vagrant](https://www.vagrantup.com/) on your host (`vagrant` is included in the Nix dev
shell via `flake.nix`).
```sh
vagrant up # First boot: downloads the box and provisions (~1520 min)
ssh -p 2222 vagrant@localhost # Password: vagrant
```
Inside the VM, the project root is mounted at `C:\vagrant`:
```powershell
cd C:\vagrant
cargo build
cargo test
```
Subsequent boots are fast — provisioning only runs once:
```sh
vagrant halt # Stop the VM
vagrant up # Resume
vagrant destroy # Delete the VM entirely
```
# Troubleshooting
To troubleshoot what's happening, you can set the environment variable `SKIM_LOG` or the flag `--log-level` to either `debug` or even `trace`, and set the environment variable `SKIM_LOG_FILE` or the flag `--log-file` to a path. You can then read those logs during or after the execution to better understand what's happening. Don't hesitate to add those logs to an issue if you need help.
## No line feed issues with nix, FreeBSD, termux
If you encounter display issues like:
```bash
$ for n in {1..10}; do echo "$n"; done | sk
0/10 0/0.> 10/10 10 9 8 7 6 5 4 3 2> 1
```
For example
- https://github.com/skim-rs/skim/issues/412
- https://github.com/skim-rs/skim/issues/455
You need to set TERMINFO or TERMINFO_DIRS to the path of a correct terminfo database path
For example, with termux, you can add this in your bashrc:
```
export TERMINFO=/data/data/com.termux/files/usr/share/terminfo
```
# Benchmarks
## Interactive benchmark (`cli`)
The `cli` bench benchmarks skim (or any compatible binary) against other versions or fzf by running the interactive interface inside a tmux session and polling the status line until the matched count stabilises. This is by no means a precise or foolproof measurement, but it has the added benefit of benchmarking against `fzf` and of providing resource metrics (peak RSS and CPU).
```sh
cargo bench --bench cli -- run # defaults: sk, 1 M items, query "test"
cargo bench --bench cli -- run sk -n 500000 -q foo # bare name resolved via $PATH
cargo bench --bench cli -- run ./old/sk ./new/sk -r 5 # compare two binaries, 5 runs each
cargo bench --bench cli -- run sk -f input.txt -q search # use an existing file
cargo bench --bench cli -- generate -f testdata.txt -n 2000000 # generate input file and exit
cargo bench --bench cli -- run sk --perf # record perf data (auto-named file)
cargo bench --bench cli -- run sk --strace # record strace data (auto-named file)
cargo bench --bench cli -- run sk -p perf.data # record perf data to perf.data
cargo bench --bench cli -- run sk -j # JSON output
cargo bench --bench cli -- run sk -r 3 -- --tiebreak=index # pass extra flags to sk
```
Binary names are resolved to absolute paths via `which` before use, so bare names like `sk` or `fzf` work as long as they are on `$PATH`.
### Criterion benchmarks
Criterion benchmarks are available to measure skim's performance more precisely.
To run them, you need to generate input data using `cargo bench --bench cli -- -g benches/fixtures/10M.txt -n 10000000 && cargo bench --bench cli -- -g benches/fixtures/1M.txt -n 1000000`, then run `cargo bench -j 1`.
These will run for several minutes.

121
Vagrantfile vendored
View file

@ -1,121 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# Windows development VM for testing skim on Windows without a GUI.
#
# Prerequisites (host, NixOS):
# virtualisation.libvirtd.enable = true;
# users.users.<you>.extraGroups = [ "libvirtd" ]; # then log out/in
#
# Usage:
# vagrant up # First boot: downloads box, provisions (~15-20 min)
# vagrant up --provision # Re-run provisioning on existing VM
# vagrant ssh # SSH in via vagrant
# vagrant ssh-config # Show IP/key if you prefer a raw ssh command
# vagrant halt # Stop the VM
# vagrant destroy # Delete the VM
#
# Inside the VM:
# cd C:\vagrant # Project root (synced from host, see note below)
# cargo build # Build skim
# cargo test # Run tests
#
# Note: The first `vagrant up` requires internet access on the VM to install
# packages via Chocolatey.
Vagrant.configure("2") do |config|
# Windows Server 2022 Core — minimal footprint, no desktop GUI.
# Box source: https://app.vagrantup.com/gusztavvargadr/boxes/windows-server-2022-standard-core
config.vm.box = "gusztavvargadr/windows-server-2022-standard-core"
# Vagrant manages the VM via WinRM (the Windows default).
config.vm.communicator = "winrm"
config.winrm.username = "vagrant"
config.winrm.password = "vagrant"
config.winrm.timeout = 600 # provisioning can take a while on first boot
config.vm.provider "libvirt" do |lv|
lv.driver = "kvm"
lv.memory = 2048
lv.cpus = 2
end
# rsync is used for the synced folder because libvirt has no native
# shared-folder support for Windows guests. rsync must be present on the
# guest, so the folder is disabled on boot and synced via a post-provision
# trigger (after Chocolatey installs rsync below).
# Re-sync manually at any time with: vagrant rsync
# rsync is used because libvirt has no native shared-folder support for
# Windows guests. On a brand-new VM the very first `vagrant up` will fail
# the rsync step (rsync not yet installed on the guest); run
# `vagrant provision && vagrant rsync` to recover, or just
# `vagrant destroy && vagrant up` after the box is cached locally.
# cwRsync (the Windows rsync from Chocolatey) uses Cygwin paths, so the
# guest path must use /cygdrive/c/... rather than a bare /vagrant.
config.vm.synced_folder ".", "/cygdrive/c/vagrant", type: "rsync",
rsync__exclude: [".git/", "target/", ".jj/"],
rsync__args: ["--verbose", "--archive", "--delete", "--copy-links", "--no-owner", "--no-group"]
# ---------------------------------------------------------------------------
# Provisioning: configure OpenSSH + install Rust toolchain via Chocolatey.
# The box ships with Win32-OpenSSH already present, so we only configure it.
# Runs once on `vagrant up`; re-run with `vagrant provision`.
# ---------------------------------------------------------------------------
config.vm.provision "shell", privileged: true, inline: <<-'POWERSHELL'
$ErrorActionPreference = "Stop"
# --- Chocolatey -------------------------------------------------------------
Write-Host "==> Installing Chocolatey..."
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol =
[System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression (
(New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')
)
}
# --- OpenSSH Server ---------------------------------------------------------
# The box ships with Win32-OpenSSH binaries at C:\Program Files\OpenSSH-Win64.
# Re-run install-sshd.ps1 to register the service (idempotent; safe to
# re-run if the service is already present).
Write-Host "==> Registering and starting sshd..."
& "C:\Program Files\OpenSSH-Win64\install-sshd.ps1"
Set-Service -Name sshd -StartupType Automatic
Start-Service -Name sshd
# Use PowerShell as the default shell for SSH sessions.
$regPath = "HKLM:\SOFTWARE\OpenSSH"
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
Set-ItemProperty -Path $regPath -Name DefaultShell `
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
# Allow inbound SSH through the Windows firewall.
$rule = Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue
if (-not $rule) {
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP" `
-DisplayName "OpenSSH Server (sshd)" `
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
}
# --- Rust, Git, rsync -------------------------------------------------------
Write-Host "==> Installing Rust, Git, rsync, and MinGW..."
choco install -y rust git rsync mingw
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") +
";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
# Add MinGW bin to the persistent system PATH so dlltool.exe is found in
# SSH sessions (which don't run the Chocolatey shim refresh).
$mingwBin = "C:\ProgramData\mingw64\mingw64\bin"
$machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
if ($machinePath -notlike "*$mingwBin*") {
[System.Environment]::SetEnvironmentVariable("Path", "$machinePath;$mingwBin", "Machine")
}
Write-Host ""
Write-Host "==> Provisioning complete."
Write-Host " SSH into the VM: vagrant ssh-config (then ssh to the reported IP)"
Write-Host " Build skim: cd C:\vagrant && cargo build"
POWERSHELL
end

BIN
bench.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,285 +0,0 @@
#![allow(missing_docs, clippy::pedantic)]
use std::fs;
use criterion::{Criterion, criterion_group, criterion_main};
use skim::Typos;
use skim::helper::item::DefaultSkimItem;
use skim::prelude::*;
const CHUNK_SIZE: usize = 1024;
fn load_lines(file: &str) -> Vec<String> {
let data = fs::read_to_string(format!("benches/fixtures/{file}")).expect("{file} missing");
data.lines().map(|l| l.to_string()).collect()
}
fn prepare(file: &str, opt_builder: &mut SkimOptionsBuilder) -> (SkimOptions, SkimItemReceiver) {
let lines = load_lines(file);
let opts = opt_builder.build().unwrap();
let (tx, rx) = unbounded();
let mut chunk_size = 0;
let mut chunk = Vec::new();
for line in lines {
if chunk_size >= CHUNK_SIZE {
tx.send(chunk).unwrap();
chunk_size = 0;
chunk = Vec::new();
}
chunk.push(Arc::new(DefaultSkimItem::from(line)) as Arc<dyn SkimItem>);
}
tx.send(chunk).unwrap();
(opts, rx)
}
fn criterion_benchmark_10m(c: &mut Criterion) {
c.bench_function("filter_10M_regex", |b| {
b.iter_batched(
|| prepare("10M.txt", SkimOptionsBuilder::default().filter("test").regex(true)),
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
#[cfg(feature = "frizbee")]
c.bench_function("filter_10M_frizbee", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Frizbee)
.typos(Typos::Disabled),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
#[cfg(feature = "frizbee")]
c.bench_function("filter_10M_frizbee_typos", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Frizbee)
.typos(Typos::Smart),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_10M_clangd", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Clangd),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_10M_fzy", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Fzy)
.typos(Typos::Disabled),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_10M_fzy_typos", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Fzy)
.typos(Typos::Smart),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_10M_arinae", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Arinae)
.typos(Typos::Disabled),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_10M_arinae_typos", |b| {
b.iter_batched(
|| {
prepare(
"10M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Arinae)
.typos(Typos::Smart),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
}
fn criterion_benchmark_1m(c: &mut Criterion) {
c.bench_function("filter_1M_regex", |b| {
b.iter_batched(
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("test").regex(true)),
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
#[cfg(feature = "frizbee")]
c.bench_function("filter_1M_frizbee", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Frizbee)
.typos(Typos::Disabled),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
#[cfg(feature = "frizbee")]
c.bench_function("filter_1M_frizbee_typos", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Frizbee)
.typos(Typos::Smart),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_1M_clangd", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Clangd),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_1M_fzy", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Fzy)
.typos(Typos::Disabled),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_1M_fzy_typos", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Fzy)
.typos(Typos::Smart),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_1M_arinae", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Arinae)
.typos(Typos::Disabled),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_1M_arinae_typos", |b| {
b.iter_batched(
|| {
prepare(
"1M.txt",
SkimOptionsBuilder::default()
.filter("test")
.algorithm(FuzzyAlgorithm::Arinae)
.typos(Typos::Smart),
)
},
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
c.bench_function("filter_1M_andor", |b| {
b.iter_batched(
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("boot foo | mnt foo")),
|(opts, rx)| Skim::run_with(opts, Some(rx)),
criterion::BatchSize::SmallInput,
);
});
}
criterion_group!(
name = benches_10m;
config = Criterion::default().sample_size(10);
targets = criterion_benchmark_10m
);
criterion_group!(
name = benches_1m;
config = Criterion::default().sample_size(100);
targets = criterion_benchmark_1m
);
criterion_main!(benches_1m, benches_10m);

View file

@ -1,2 +0,0 @@
!.gitignore
*

View file

@ -1,67 +0,0 @@
#![allow(missing_docs, clippy::pedantic)]
use gungraun::{library_benchmark, library_benchmark_group, main};
use std::fs;
use std::hint::black_box;
use skim::CaseMatching;
use skim::fuzzy_matcher::FuzzyMatcher;
use skim::fuzzy_matcher::arinae::ArinaeMatcher;
use skim::fuzzy_matcher::frizbee::FrizbeeMatcher;
use skim::prelude::SkimMatcherV2;
fn load_lines() -> Vec<String> {
let data = fs::read_to_string("benches/fixtures/1M.txt").expect("1M.txt missing");
data.lines().map(|l| l.to_string()).collect()
}
#[inline(always)]
fn bench_matcher(m: impl FuzzyMatcher, lines: Vec<String>) -> u64 {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_indices(line, "test").is_some() {
count += 1;
}
}
count
}
#[library_benchmark]
fn skim_v2() -> u64 {
bench_matcher(SkimMatcherV2::default().smart_case(), black_box(load_lines()))
}
#[library_benchmark]
fn frizbee() -> u64 {
bench_matcher(
FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(0)),
black_box(load_lines()),
)
}
#[library_benchmark]
fn frizbee_typos() -> u64 {
bench_matcher(
FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(1)),
black_box(load_lines()),
)
}
#[library_benchmark]
fn arinae() -> u64 {
bench_matcher(
ArinaeMatcher::new(CaseMatching::Smart, false, false),
black_box(load_lines()),
)
}
#[library_benchmark]
fn arinae_typos() -> u64 {
bench_matcher(
ArinaeMatcher::new(CaseMatching::Smart, true, false),
black_box(load_lines()),
)
}
library_benchmark_group!(
name = benches,
benchmarks = [skim_v2, frizbee, frizbee_typos, arinae, arinae_typos]
);
main!(library_benchmark_groups = benches);

View file

@ -1,151 +0,0 @@
#![allow(missing_docs, clippy::pedantic)]
//! Microbenchmark that isolates the fuzzy matcher DP from all other overhead
//! (I/O, threading, sorting).
use std::fs;
use criterion::{Criterion, criterion_group, criterion_main};
use skim::CaseMatching;
use skim::fuzzy_matcher::FuzzyMatcher;
use skim::fuzzy_matcher::arinae::ArinaeMatcher;
#[cfg(feature = "frizbee")]
use skim::fuzzy_matcher::frizbee::FrizbeeMatcher;
use skim::prelude::SkimMatcherV2;
fn load_lines() -> Vec<String> {
let data = fs::read_to_string("benches/fixtures/100K.txt").expect("100K.txt missing");
data.lines().map(|l| l.to_string()).collect()
}
fn bench_matcher(c: &mut Criterion) {
let lines = load_lines();
c.bench_function("micro_skim_v2", |b| {
let m = SkimMatcherV2::default().smart_case();
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_indices(line, "test").is_some() {
count += 1;
}
}
count
});
});
#[cfg(feature = "frizbee")]
c.bench_function("micro_frizbee_score", |b| {
let m = FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(0));
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_match(line, "test").is_some() {
count += 1;
}
}
count
});
});
#[cfg(feature = "frizbee")]
c.bench_function("micro_frizbee", |b| {
let m = FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(0));
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_indices(line, "test").is_some() {
count += 1;
}
}
count
});
});
#[cfg(feature = "frizbee")]
c.bench_function("micro_typos_frizbee", |b| {
let m = FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(1));
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_indices(line, "test").is_some() {
count += 1;
}
}
count
});
});
c.bench_function("micro_arinae", |b| {
let m = ArinaeMatcher::new(CaseMatching::Smart, false, false);
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_indices(line, "test").is_some() {
count += 1;
}
}
count
});
});
c.bench_function("micro_arinae_range", |b| {
let m = ArinaeMatcher::new(CaseMatching::Smart, false, false);
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_match_range(line, "test").is_some() {
count += 1;
}
}
count
});
});
c.bench_function("micro_arinae_score", |b| {
let m = ArinaeMatcher::new(CaseMatching::Smart, false, false);
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_match(line, "test").is_some() {
count += 1;
}
}
count
});
});
c.bench_function("micro_typos_arinae", |b| {
let m = ArinaeMatcher::new(CaseMatching::Smart, true, false);
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_indices(line, "test").is_some() {
count += 1;
}
}
count
});
});
c.bench_function("micro_typos_arinae_range", |b| {
let m = ArinaeMatcher::new(CaseMatching::Smart, true, false);
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_match_range(line, "test").is_some() {
count += 1;
}
}
count
});
});
c.bench_function("micro_typos_arinae_score", |b| {
let m = ArinaeMatcher::new(CaseMatching::Smart, true, false);
b.iter(|| {
let mut count = 0u64;
for line in &lines {
if m.fuzzy_match(line, "test").is_some() {
count += 1;
}
}
count
});
});
}
criterion_group!(benches, bench_matcher);
criterion_main!(benches);

View file

@ -1,291 +0,0 @@
#![allow(missing_docs, clippy::pedantic)]
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Cursor, Stderr};
use std::time::Duration;
use clap::Parser as _;
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
use ratatui::backend::TestBackend;
use ratatui::prelude::CrosstermBackend;
use skim::prelude::*;
/// Small inline fixture — fast to load, good for latency benchmarks.
const SMALL_ITEMS: &[&str] = &[
"src/main.rs",
"src/lib.rs",
"src/options.rs",
"src/skim.rs",
"benches/partial.rs",
"tests/common/insta.rs",
"Cargo.toml",
"README.md",
];
/// Path to the medium fixture shipped with the repo (≈664 lines).
const FIXTURE_DEFAULT: &str = "benches/fixtures/default.txt";
/// Path to the large fixture (100 000 lines). Skip in low-time CI runs if absent.
const FIXTURE_100K: &str = "benches/fixtures/100K.txt";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Read a fixture file into memory and return each non-empty line as a `String`.
fn load_fixture(path: &str) -> Vec<String> {
let f = File::open(path).unwrap_or_else(|e| panic!("cannot open fixture {path}: {e}"));
BufReader::new(f)
.lines()
.map_while(Result::ok)
.filter(|l| !l.is_empty())
.collect()
}
/// Build a newline-separated `Vec<u8>` from a slice of items.
fn items_to_bytes(items: &[impl AsRef<str>]) -> Vec<u8> {
let mut buf = Vec::new();
for item in items {
buf.extend_from_slice(item.as_ref().as_bytes());
buf.push(b'\n');
}
buf
}
/// Create a `SkimItemReceiver` that will produce `items` via the full
/// `SkimItemReader` pipeline (the same path used when skim reads stdin).
fn make_receiver(items: &[impl AsRef<str>], options: &SkimOptions) -> SkimItemReceiver {
let reader_opts = SkimItemReaderOption::from_options(options);
let item_reader = SkimItemReader::new(reader_opts);
item_reader.of_bufread(Cursor::new(items_to_bytes(items)))
}
/// Spin-wait until the reader is done and the matcher has stopped (mirrors the
/// logic in `tests/common/insta.rs`). Panics after `timeout`.
fn wait_until_done(skim: &mut Skim<TestBackend>, timeout: Duration) {
let start = std::time::Instant::now();
while !skim.reader_done() {
assert!(start.elapsed() < timeout, "timeout waiting for reader");
skim.check_reader();
std::thread::sleep(Duration::from_millis(1));
}
skim.check_reader();
while !skim.app().matcher_control.stopped() {
assert!(start.elapsed() < timeout, "timeout waiting for matcher");
std::thread::sleep(Duration::from_millis(1));
}
}
// ---------------------------------------------------------------------------
// Benchmark group
// ---------------------------------------------------------------------------
fn criterion_benchmark(c: &mut Criterion) {
// -----------------------------------------------------------------------
// Phase 0 — Options
// -----------------------------------------------------------------------
c.bench_function("parse_options", |b| {
b.iter(|| SkimOptions::parse_from(Vec::<&str>::new()));
});
// `from_env` merges SKIM_DEFAULT_OPTIONS, SKIM_OPTIONS_FILE, and argv.
// In a clean test environment it should behave identically to parse_from,
// but the code path is different and may regress independently.
c.bench_function("options_from_env", |b| {
b.iter(SkimOptions::from_env);
});
// `build()` post-processes raw options: expands keymaps, normalises
// tiebreak / layout, reads history files, etc.
c.bench_function("options_build", |b| {
b.iter_batched(
|| SkimOptions::parse_from(Vec::<&str>::new()),
|opts| opts.build(),
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 1 — SkimItemReader pipeline construction
// -----------------------------------------------------------------------
// Building the reader option struct from a SkimOptions (called in sk_main
// before Skim::init).
c.bench_function("item_reader_option_from_options", |b| {
let opts = SkimOptions::default().build();
b.iter(|| SkimItemReaderOption::from_options(&opts));
});
// Constructing a new SkimItemReader (spawns a thread-pool).
c.bench_function("item_reader_new", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
SkimItemReaderOption::from_options(&opts)
},
SkimItemReader::new,
BatchSize::SmallInput,
);
});
// `of_bufread` starts the I/O dispatcher and pool threads but does NOT
// wait for all items to be processed; it just returns the receiver.
// Benchmark with the default fixture to get a realistic buffer.
c.bench_function("of_bufread_setup", |b| {
let opts = SkimOptions::default().build();
let data = items_to_bytes(&load_fixture(FIXTURE_DEFAULT));
b.iter_batched(
|| {
let reader_opts = SkimItemReaderOption::from_options(&opts);
(SkimItemReader::new(reader_opts), data.clone())
},
|(reader, bytes)| {
// Calling of_bufread starts the background threads; we
// deliberately drop the receiver immediately so they clean up.
let _rx = reader.of_bufread(Cursor::new(bytes));
},
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 2 — Skim::init (unchanged from before, kept for comparison)
// -----------------------------------------------------------------------
c.bench_function("init", |b| {
b.iter_batched(
|| SkimOptions::default().build(),
|options: SkimOptions| Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(options, None),
BatchSize::SmallInput,
);
});
c.bench_function("init_with_source", |b| {
b.iter_batched(
|| {
let (_tx, rx) = bounded(8);
(SkimOptions::default().build(), rx)
},
|input: (SkimOptions, SkimItemReceiver)| {
Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(input.0, Some(input.1))
},
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 2+3 — Skim::init + start
// -----------------------------------------------------------------------
c.bench_function("start", |b| {
b.iter_batched(
|| Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(SkimOptions::default().build(), None).unwrap(),
|mut skim: Skim| skim.start(),
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 3+4 — ingest + match with realistic data
// -----------------------------------------------------------------------
// These benchmarks measure end-to-end reader+matcher throughput: time from
// start() until all items are ingested and the matcher has finished its
// first pass. They are the closest in-process analog to the CLI startup
// time measured by `cargo bench --bench cli`.
// Small inline fixture (8 items) — baseline latency floor.
c.bench_function("ingest_and_match_small", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
let rx = make_receiver(SMALL_ITEMS, &opts);
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
},
|mut skim: Skim<TestBackend>| {
skim.start();
wait_until_done(&mut skim, Duration::from_secs(5));
},
BatchSize::SmallInput,
);
});
// Medium fixture (≈664 lines from FIXTURE_DEFAULT).
{
let items = load_fixture(FIXTURE_DEFAULT);
let n = items.len() as u64;
let mut group = c.benchmark_group("ingest_and_match_default_fixture");
group.throughput(Throughput::Elements(n));
group.bench_function("ingest_and_match", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
let rx = make_receiver(&items, &opts);
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
},
|mut skim: Skim<TestBackend>| {
skim.start();
wait_until_done(&mut skim, Duration::from_secs(10));
},
BatchSize::SmallInput,
);
});
group.finish();
}
// Large fixture (100 000 lines). Only run when the file exists so the
// bench suite does not fail in environments without the fixture.
if std::path::Path::new(FIXTURE_100K).exists() {
let items = load_fixture(FIXTURE_100K);
let n = items.len() as u64;
let mut group = c.benchmark_group("ingest_and_match_100k_fixture");
group.throughput(Throughput::Elements(n));
// Fewer samples: each iteration loads 100 k items.
group.sample_size(10);
group.bench_function("ingest_and_match", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
let rx = make_receiver(&items, &opts);
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
},
|mut skim: Skim<TestBackend>| {
skim.start();
wait_until_done(&mut skim, Duration::from_secs(30));
},
BatchSize::SmallInput,
);
});
group.finish();
}
// -----------------------------------------------------------------------
// Phase 25 — full_setup (unchanged from before, kept for comparison)
// -----------------------------------------------------------------------
c.bench_function("full_setup", |b| {
b.iter(|| {
let mut options = SkimOptions::default().build();
if let Some(ref filter_query) = options.filter
&& options.query.is_none()
{
options.query = Some(filter_query.clone());
}
let mut skim = Skim::init(options, None).unwrap();
skim.start();
if skim.should_enter() {
skim.init_tui().unwrap();
}
});
});
}
criterion_group!(
name = benches;
config = Criterion::default().sample_size(100);
targets = criterion_benchmark
);
criterion_main!(benches);

View file

@ -1,131 +0,0 @@
#![allow(missing_docs, clippy::pedantic)]
use criterion::{Criterion, criterion_group, criterion_main};
use eyre::{Ok, Result};
use skim::prelude::*;
async fn wait_until_done(mut opts: SkimOptions) -> Result<SkimOutput> {
opts.cmd = Some(String::from("cat benches/fixtures/10M.txt"));
let mut skim = Skim::init(opts, None)?;
skim.start();
skim.init_tui()?;
skim.enter().await?;
while !skim.tick().await? {
if skim.reader_done() && skim.matcher_stopped() {
skim.event_sender().send(Event::Action(Action::Accept(None))).await?;
}
}
Ok(skim.output())
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("default", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt)
.iter(async || wait_until_done(SkimOptions::default()).await);
});
c.bench_function("query", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt)
.iter(async || wait_until_done(SkimOptionsBuilder::default().query("test").build().unwrap()).await);
});
#[cfg(feature = "frizbee")]
c.bench_function("query_frizbee", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt).iter(async || {
wait_until_done(
SkimOptionsBuilder::default()
.query("test")
.algorithm(FuzzyAlgorithm::Frizbee)
.no_typos(true)
.build()
.unwrap(),
)
.await
});
});
c.bench_function("query_ari", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt).iter(async || {
wait_until_done(
SkimOptionsBuilder::default()
.query("test")
.algorithm(FuzzyAlgorithm::Arinae)
.no_typos(true)
.build()
.unwrap(),
)
.await
});
});
#[cfg(feature = "frizbee")]
c.bench_function("query_frizbee_typos", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt).iter(async || {
wait_until_done(
SkimOptionsBuilder::default()
.query("test")
.algorithm(FuzzyAlgorithm::Frizbee)
.build()
.unwrap(),
)
.await
});
});
c.bench_function("query_ari_typos", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt).iter(async || {
wait_until_done(
SkimOptionsBuilder::default()
.query("test")
.algorithm(FuzzyAlgorithm::Arinae)
.build()
.unwrap(),
)
.await
});
});
c.bench_function("typing", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(rt).iter(async || {
let mut skim = Skim::init(SkimOptionsBuilder::default().cmd("cat bench_data.txt").build()?, None)?;
skim.start();
skim.init_tui()?;
skim.enter().await?;
let s = skim.event_sender();
let mut sent = false;
let mut done_since = 0;
let mut done = false;
while !skim.tick().await? {
if skim.reader_done() && skim.matcher_stopped() {
if done {
done_since += 1;
} else {
done_since = 1;
}
if sent && done_since > 50 {
s.send(Event::Action(Action::Accept(None))).await?;
} else if !sent {
s.send(Event::Action(Action::AddChar('t'))).await?;
s.send(Event::Action(Action::AddChar('e'))).await?;
s.send(Event::Action(Action::AddChar('s'))).await?;
s.send(Event::Action(Action::AddChar('t'))).await?;
sent = true;
}
done = true;
} else {
done = false;
}
}
Ok(skim.output())
});
});
}
criterion_group!(
name = benches;
config = Criterion::default().sample_size(10);
targets = criterion_benchmark
);
criterion_main!(benches);

View file

@ -28,8 +28,6 @@
# sk-tmux: starts sk in a tmux pane
# usage: sk-tmux [LAYOUT OPTIONS] [--] [SK OPTIONS]
echo "[WRN] This script is deprecated in favor of \`sk --tmux\` and will be removed in a later release" >&2
fail() {
>&2 echo "$1"
exit 2
@ -86,7 +84,7 @@ while [[ $# -gt 0 ]]; do
;;
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
if [[ "$arg" =~ ^-[pwhxy] ]]; then
[[ "$opt" =~ "-E" ]] || opt="-E"
[[ "$opt" =~ "-K -E" ]] || opt="-K -E"
elif [[ "$arg" =~ ^.[lr] ]]; then
opt="-h"
if [[ "$arg" =~ ^.l ]]; then
@ -167,7 +165,7 @@ fi
args=("${args[@]}" "--no-height")
# Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
if [[ ! "$opt" =~ "-K -E" ]] && tmux list-panes -F '#F' | grep -q Z; then
zoomed_without_popup=1
original_window=$(tmux display-message -p "#{window_id}")
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c sk-tmux is running\\r\"; done; done'")
@ -209,7 +207,7 @@ trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT
envs="export TERM=$TERM "
[[ "$opt" =~ "-E" ]] && SKIM_DEFAULT_OPTIONS="--margin 0,1 $SKIM_DEFAULT_OPTIONS"
[[ "$opt" =~ "-K -E" ]] && SKIM_DEFAULT_OPTIONS="--margin 0,1 $SKIM_DEFAULT_OPTIONS"
[[ -n "$SKIM_DEFAULT_OPTIONS" ]] && envs="$envs SKIM_DEFAULT_OPTIONS=$(printf %q "$SKIM_DEFAULT_OPTIONS")"
[[ -n "$SKIM_DEFAULT_COMMAND" ]] && envs="$envs SKIM_DEFAULT_COMMAND=$(printf %q "$SKIM_DEFAULT_COMMAND")"
echo "$envs;" > "$argsf"
@ -223,7 +221,7 @@ close="; trap - EXIT SIGINT SIGTERM $close"
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
mkfifo -m o+w $fifo2
if [[ "$opt" =~ "-E" ]]; then
if [[ "$opt" =~ "-K -E" ]]; then
cat $fifo2 &
if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$sk\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
@ -232,7 +230,7 @@ if [[ "$opt" =~ "-E" ]]; then
cat <<< "\"$sk\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
cat <&0 > $fifo1 &
fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
exit $?
fi

33
ci/before_deploy.sh Normal file
View file

@ -0,0 +1,33 @@
# This script takes care of building your crate and packaging it for release
set -ex
main() {
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
test -f Cargo.lock || cargo generate-lockfile
# TODO Update this to build the artifacts that matter to you
cross build --release --target $TARGET
# TODO Update this to package the right artifacts
cp target/$TARGET/release/sk $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main

66
ci/install.sh Normal file
View file

@ -0,0 +1,66 @@
set -ex
main() {
local target=
if [ $TRAVIS_OS_NAME = linux ]; then
target=x86_64-unknown-linux-musl
sort=sort
else
target=x86_64-apple-darwin
sort=gsort # for `sort --sort-version`, from brew's coreutils.
fi
# Builds for iOS are done on OSX, but require the specific target to be
# installed.
case $TARGET in
aarch64-apple-ios)
rustup target install aarch64-apple-ios
;;
armv7-apple-ios)
rustup target install armv7-apple-ios
;;
armv7s-apple-ios)
rustup target install armv7s-apple-ios
;;
i386-apple-ios)
rustup target install i386-apple-ios
;;
x86_64-apple-ios)
rustup target install x86_64-apple-ios
;;
esac
# This fetches latest stable release
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
| cut -d/ -f3 \
| grep -E '^v[0.1.0-9.]+$' \
| $sort --version-sort \
| tail -n1)
curl -LSfs https://japaric.github.io/trust/install.sh | \
sh -s -- \
--force \
--git japaric/cross \
--tag $tag \
--target $target
# For test
case $TARGET in
x86_64-unknown-linux-gnu|i686-unknown-linux-gnu)
python3 -V
tmux -V
sudo apt-get install -y zsh
stty cols 80
;;
x86_64-apple-darwin|i686-apple-darwin)
HOMEBREW_NO_AUTO_UPDATE=1 brew install tmux
HOMEBREW_NO_AUTO_UPDATE=1 brew install zsh
python -V
python3 -V
tmux -V
stty cols 80
;;
esac
}
main

30
ci/script.sh Normal file
View file

@ -0,0 +1,30 @@
# This script takes care of testing
set -ex
main() {
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --release --target $TARGET
cross build --release --target $TARGET
mkdir -p target/release
cp target/$TARGET/release/sk target/release
case $TARGET in
x86_64-unknown-linux-gnu|i686-unknown-linux-gnu|x86_64-unknown-linux-musl)
# run the integration test
tmux new "python3 test/test_skim.py &> out && touch ok" && cat out && [ -e ok ]
;;
x86_64-apple-darwin|i686-apple-darwin)
# run the integration test
tmux new "python3 test/test_skim.py &> out && touch ok" && cat out && [ -e ok ]
;;
*)
;;
esac
}
if [ -z $TRAVIS_TAG ]; then
main
fi

View file

@ -1,121 +0,0 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# A Tera template to be rendered for each release in the changelog.
# See https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
### New Contributors
{%- endif -%}
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor %}\n
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}{% raw %}\n{% endraw -%}{% endif %}
"""
header = """
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
"""
# Remove leading and trailing whitespaces from the changelog's body.
footer = "<!-- generated by git-cliff -->"
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = true
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = true
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Allow commits named `type!(scope): desc` and treat them like `type(scope)!: desc`
{ pattern = '(.*)!\((.*)\):(.*)', replace = "$1($2)!:$3" },
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^ci", group = "<!-- 7 -->🤖 CI" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "<!-- 8 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 9 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 10 -->◀️ Revert" },
{ message = "^release", skip = true },
{ message = ".*", group = "<!-- 11 -->💼 Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# Fail on a commit that is not matched by any commit parser.
fail_on_unmatched_commit = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order commits topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "oldest"
# Process submodules commits
recurse_submodules = false

View file

@ -1,35 +0,0 @@
[workspace]
members = ["cargo:."]
# Config for 'dist'
[dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.32.0"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell", "powershell", "msi"]
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
# Path that installers should place binaries in
install-path = "CARGO_HOME"
# Whether to install an updater program
install-updater = false
# Extra static files to include in each App (path relative to this Cargo.toml's dir)
include = ["./man/", "./shell/"]
# Plan jobs to run in CI
plan-jobs = ["./test"]
# Global artifacts jobs to run in CI
global-artifacts-jobs = ["./package"]
# Publish jobs to run in CI
publish-jobs = ["./publish", "./winget", "./apt"]
# Whether to publish prereleases to package managers
publish-prereleases = true
[dist.github-custom-job-permissions.test]
contents = "write"
id-token = "write"
[dist.github-custom-job-permissions.apt]
contents = "write"
id-token = "write"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

View file

@ -1,33 +0,0 @@
//! Demonstrates selecting ANSI-colored command output.
extern crate skim;
use skim::prelude::*;
use skim::reader::CommandCollector;
fn main() {
env_logger::init();
let glogm = "git log --oneline --color=always | head -n10";
let options = SkimOptionsBuilder::default()
.height("50%")
.cmd(glogm)
.preview("echo {}")
.multi(true)
.reverse(true)
.cmd_collector(Rc::new(RefCell::new(SkimItemReader::new(
SkimItemReaderOption::default().ansi(true),
))) as Rc<RefCell<dyn CommandCollector>>)
.build()
.unwrap();
log::debug!("Options: ansi {}", options.ansi);
let selected_items = Skim::run_with(options, None)
.map(|out| out.selected_items)
.unwrap_or_default();
for item in &selected_items {
println!("selected: {}", item.output());
}
}

View file

@ -1,10 +0,0 @@
//! Runs skim from an async Tokio entry point.
use skim::Skim;
use skim::prelude::SkimOptionsBuilder;
#[tokio::main]
async fn main() {
let options = SkimOptionsBuilder::default().build().unwrap();
Skim::run_with(options, None).unwrap();
}

View file

@ -1,19 +0,0 @@
//! Demonstrates basic item selection with inline status information.
use skim::prelude::*;
use skim::tui::statusline::InfoDisplay;
fn main() -> eyre::Result<()> {
let opts = SkimOptionsBuilder::default()
.multi(true)
.reverse(true)
.info(InfoDisplay::Inline)
.build()?;
let res = Skim::run_items(opts, ["hello", "world"])?;
for item in res.selected_items {
println!("Selected {} (id {})", item.output(), item.rank.index);
}
Ok(())
}

View file

@ -1,54 +0,0 @@
//! Demonstrates collecting items from a command.
extern crate skim;
use reader::CommandCollector;
use skim::prelude::*;
struct BasicSkimItem {
value: String,
}
impl SkimItem for BasicSkimItem {
fn text(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.value)
}
}
struct BasicCmdCollector {
pub items: Vec<String>,
}
impl CommandCollector for BasicCmdCollector {
fn invoke(&mut self, _cmd: &str, _components_to_stop: Arc<AtomicUsize>) -> (SkimItemReceiver, Sender<i32>) {
let (tx, rx) = unbounded();
let (tx_interrupt, _rx_interrupt) = unbounded();
let mut batch = Vec::new();
while let Some(value) = self.items.pop() {
let item = BasicSkimItem { value };
batch.push(Arc::from(item) as Arc<dyn SkimItem>);
}
if !batch.is_empty() {
tx.send(batch).unwrap();
}
(rx, tx_interrupt)
}
}
fn main() {
let cmd_collector = BasicCmdCollector {
items: vec![String::from("foo"), String::from("bar"), String::from("baz")],
};
let options = SkimOptionsBuilder::default()
.cmd_collector(Rc::from(RefCell::from(cmd_collector)))
.build()
.unwrap();
let selected_items = Skim::run_with(options, None)
.map(|out| out.selected_items)
.unwrap_or_default();
for item in &selected_items {
println!("{}", item.output());
}
}

View file

@ -1,96 +0,0 @@
//! Demonstrates binding custom action callbacks to keyboard shortcuts.
extern crate skim;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use skim::prelude::*;
use skim::tui::event::{Action, ActionCallback, Event};
use std::io::Cursor;
/// Runs the custom action keybinding example.
///
/// It shows how to:
/// 1. Create custom action callbacks (both sync and async)
/// 2. Bind them to specific key combinations
/// 3. Use them interactively in skim
fn main() {
// Create a synchronous callback that adds a prefix to the query.
// Use `new_sync` for plain closures that do not need to await anything.
let add_prefix_callback = ActionCallback::new_sync(|app: &mut skim::tui::App| {
// Get current query and add prefix
let current_query = app.input.value.clone();
// Clear the line first, then add new content
let mut events = vec![Event::Action(Action::UnixLineDiscard)];
let prefix = "TODO: ";
for ch in prefix.chars() {
events.push(Event::Action(Action::AddChar(ch)));
}
// Add back the original query
for ch in current_query.chars() {
events.push(Event::Action(Action::AddChar(ch)));
}
Ok(events)
});
// Create an async callback that selects all and exits.
// Use `new` for async closures or blocks that may await futures.
let select_all_callback = ActionCallback::new(|app: &mut skim::tui::App| {
let count = app.item_pool.len();
async move {
// Async work could go here (e.g. HTTP requests, file I/O, …).
Ok(vec![
Event::Action(Action::SelectAll),
Event::Action(Action::Accept(Some(format!("Selected {count} items")))),
])
}
});
// Build basic options
let mut options = SkimOptionsBuilder::default()
.multi(true)
.prompt("Select> ")
.header("<C-p>: add prefix to prompt\t<C-a>: select all and exit with count")
.build()
.unwrap();
// Now manually add custom keybindings to the keymap
// We can access the keymap directly since it's public
// Bind Ctrl-P to add prefix
options.keymap.insert(
KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL),
vec![Action::Custom(add_prefix_callback)],
);
// Bind Ctrl-A to select all with message
options.keymap.insert(
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
vec![Action::Custom(select_all_callback)],
);
// Create sample items
let items = [
"Write documentation",
"Fix bug #123",
"Implement feature X",
"Review pull request",
"Update dependencies",
"Refactor module Y",
"Add unit tests",
"Optimize performance",
];
let item_reader = SkimItemReader::default();
let input = items.join("\n");
let item_source = item_reader.of_bufread(Cursor::new(input));
// Run skim with our custom keybindings
if let Ok(output) = Skim::run_with(options, Some(item_source)) {
println!("output: {output:?}");
} else {
println!("\nAborted!");
}
}

View file

@ -1,5 +1,3 @@
//! Demonstrates using a custom item type with skim.
extern crate skim;
use skim::prelude::*;
@ -8,7 +6,7 @@ struct MyItem {
}
impl SkimItem for MyItem {
fn text(&self) -> Cow<'_, str> {
fn text(&self) -> Cow<str> {
Cow::Borrowed(&self.inner)
}
@ -21,35 +19,31 @@ impl SkimItem for MyItem {
}
}
fn main() {
pub fn main() {
let options = SkimOptionsBuilder::default()
.height("50%")
.height(Some("50%"))
.multi(true)
.preview("") // preview should be specified to enable preview window
.preview(Some("")) // preview should be specified to enable preview window
.build()
.unwrap();
env_logger::init();
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
let _ = tx_item.send(vec![
Arc::new(MyItem {
inner: "color aaaa".to_string(),
}) as Arc<dyn SkimItem>,
Arc::new(MyItem {
inner: "bbbb".to_string(),
}) as Arc<dyn SkimItem>,
Arc::new(MyItem {
inner: "ccc".to_string(),
}) as Arc<dyn SkimItem>,
]);
let _ = tx_item.send(Arc::new(MyItem {
inner: "color aaaa".to_string(),
}));
let _ = tx_item.send(Arc::new(MyItem {
inner: "bbbb".to_string(),
}));
let _ = tx_item.send(Arc::new(MyItem {
inner: "ccc".to_string(),
}));
drop(tx_item); // so that skim could know when to stop waiting for more items.
let selected_items = Skim::run_with(options, Some(rx_item))
let selected_items = Skim::run_with(&options, Some(rx_item))
.map(|out| out.selected_items)
.unwrap_or_default();
.unwrap_or_else(Vec::new);
for item in &selected_items {
for item in selected_items.iter() {
println!("{}", item.output());
}
}

View file

@ -1,39 +1,32 @@
//! Demonstrates binding custom key combinations to custom actions.
extern crate skim;
use crossterm::event::{KeyCode, KeyModifiers};
use skim::prelude::*;
// No action is actually performed on your filesystem!
// This example only produce friendly print statements!
fn fake_delete_item(item: &str) {
println!("Deleting item `{item}`...");
println!("Deleting item `{}`...", item);
}
fn fake_create_item(item: &str) {
println!("Creating a new item `{item}`...");
println!("Creating a new item `{}`...", item);
}
fn main() {
pub fn main() {
// Note: `accept` is a keyword used define custom actions.
// For full list of accepted keywords see `parse_event` in `src/event.rs`.
// `delete` and `create` are arbitrary keywords used for this example.
let options = SkimOptionsBuilder::default()
.multi(true)
.bind(vec!["bs:abort".into(), "enter:accept".into()])
.bind(vec!["bs:abort", "Enter:accept"])
.build()
.unwrap();
if let Ok(out) = Skim::run_with(options, None) {
match (out.final_key.code, out.final_key.modifiers) {
// Delete each selected item
(KeyCode::Backspace, KeyModifiers::NONE) => {
out.selected_items.iter().for_each(|i| fake_delete_item(&i.text()));
}
// Create a new item based on the query
(KeyCode::Enter, KeyModifiers::NONE) => fake_create_item(out.query.as_ref()),
_ => (),
}
}
Skim::run_with(&options, None).map(|out| match out.final_key {
// Delete each selected item
Key::Backspace => out.selected_items.iter().for_each(|i| fake_delete_item(&i.text())),
// Create a new item based on the query
Key::Enter => fake_create_item(out.query.as_ref()),
_ => (),
});
}

View file

@ -1,50 +1,48 @@
//! Illustrates downcasting custom structs that implement `SkimItem`.
extern crate skim;
use skim::prelude::*;
/// This example illustrates downcasting custom structs that implement
/// `SkimItem` after calling `Skim::run_with`.
#[derive(Debug, Clone)]
struct Item {
text: String,
}
impl SkimItem for Item {
fn text(&self) -> Cow<'_, str> {
fn text(&self) -> Cow<str> {
Cow::Borrowed(&self.text)
}
fn preview(&self, _context: PreviewContext) -> ItemPreview {
ItemPreview::Text(self.text.clone())
ItemPreview::Text(self.text.to_owned())
}
}
fn main() {
pub fn main() {
let options = SkimOptionsBuilder::default()
.height("50%")
.height(Some("50%"))
.multi(true)
.preview("")
.preview(Some(""))
.build()
.unwrap();
let (tx, rx): (SkimItemSender, SkimItemReceiver) = unbounded();
tx.send(vec![
Arc::new(Item { text: "a".into() }) as Arc<dyn SkimItem>,
Arc::new(Item { text: "b".into() }) as Arc<dyn SkimItem>,
Arc::new(Item { text: "c".into() }) as Arc<dyn SkimItem>,
])
.unwrap();
tx.send(Arc::new(Item { text: "a".to_string() })).unwrap();
tx.send(Arc::new(Item { text: "b".to_string() })).unwrap();
tx.send(Arc::new(Item { text: "c".to_string() })).unwrap();
drop(tx);
let selected_items = Skim::run_with(options, Some(rx))
let selected_items = Skim::run_with(&options, Some(rx))
.map(|out| out.selected_items)
.unwrap_or_default()
.unwrap_or_else(Vec::new)
.iter()
.map(|selected_item| selected_item.downcast_item::<Item>().unwrap().to_owned())
.map(|selected_item| (**selected_item).as_any().downcast_ref::<Item>().unwrap().to_owned())
.collect::<Vec<Item>>();
for item in selected_items {
println!("{item:?}");
println!("{:?}", item);
}
}

View file

@ -1,36 +0,0 @@
//! Demonstrates fine-grained control over skim lifecycle events.
extern crate skim;
use eyre::Result;
use skim::prelude::*;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let options = SkimOptionsBuilder::default().height("50%").multi(true).build()?;
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
let mut skim = Skim::init(options, Some(rx_item))?;
skim.start();
skim.init_tui()?;
let event_tx = skim.event_sender();
skim.enter().await?;
let output = skim
.run_until(async move {
for i in 1..=10 {
let _ = event_tx.try_send(Event::ClearItems);
let _ = tx_item.send(vec![Arc::new(format!("item {i}")) as Arc<dyn SkimItem>]);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
.await?;
for item in &output.selected_items {
println!("{}", item.output());
}
Ok(())
}

View file

@ -1,65 +0,0 @@
//! Demonstrates fuzzy matching lines with selectable matching algorithms.
use skim::fuzzy_matcher::FuzzyMatcher;
use skim::fuzzy_matcher::clangd::ClangdMatcher;
use skim::fuzzy_matcher::skim::SkimMatcherV2;
use std::env;
use std::io::{self, BufRead};
use std::process::exit;
type IndexType = usize;
fn main() {
let args: Vec<String> = env::args().collect();
// arg parsing (manually)
let mut arg_iter = args.iter().skip(1);
let mut pattern = String::new();
let mut algorithm = Some("skim");
while let Some(arg) = arg_iter.next() {
if arg == "--algo" {
algorithm = arg_iter.next().map(String::as_ref);
} else {
pattern.clone_from(arg);
}
}
if pattern.is_empty() {
eprintln!("Usage: echo <piped_input> | fz --algo [skim|clangd] <pattern>");
exit(1);
}
let matcher: Box<dyn FuzzyMatcher> = match algorithm {
Some("skim" | "skim_v2") => Box::new(SkimMatcherV2::default()),
Some("clangd") => Box::new(ClangdMatcher::default()),
_ => panic!("Algorithm not supported: {algorithm:?}"),
};
let stdin = io::stdin();
for line in stdin.lock().lines() {
if let Ok(line) = line
&& let Some((score, indices)) = matcher.fuzzy_indices(&line, &pattern)
{
println!("{:8}: {}", score, wrap_matches(&line, &indices));
}
}
}
fn wrap_matches(line: &str, indices: &[IndexType]) -> String {
let mut ret = String::new();
let mut peekable = indices.iter().peekable();
let ansi_invert: &str = str::from_utf8(&[27, b'[', b'7', b'm']).unwrap();
let ansi_reset: &str = str::from_utf8(&[27, b'[', b'0', b'm']).unwrap();
for (idx, ch) in line.chars().enumerate() {
let next_id = **peekable.peek().unwrap_or(&&(line.len() as IndexType));
if next_id == (idx as IndexType) {
ret.push_str(format!("{ansi_invert}{ch}{ansi_reset}").as_str());
peekable.next();
} else {
ret.push(ch);
}
}
ret
}

View file

@ -1,25 +0,0 @@
//! Demonstrates image previews using a literal image path.
//!
//! Run with:
//! `cargo run --example image`
use skim::options::ImageProtocol;
use skim::prelude::*;
fn main() -> eyre::Result<()> {
env_logger::init();
let options = SkimOptionsBuilder::default()
.preview("{}")
.preview_window("right:60%")
.image(ImageProtocol::Halfblocks)
.build()?;
let output = Skim::run_items(options, ["examples/Lenna.png"])?;
for item in &output.selected_items {
println!("{}", item.output());
}
Ok(())
}

View file

@ -1,26 +0,0 @@
//! Runs skim repeatedly to check that runs clean up their worker threads.
use skim::Skim;
use skim::prelude::SkimOptionsBuilder;
// Hint: use `ps -T -p $(pgrep -f target/debug/examples/multiple_runs)` to watch threads while the
// different invocations run, and make sure none is leaking through
fn main() {
for i in 0..3 {
let opts = SkimOptionsBuilder::default()
.header(format!("run {i}"))
.cmd("cat benches/fixtures/10M.txt")
.build()
.unwrap();
let res = Skim::run_with(opts, None).unwrap();
#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe {
nix::libc::malloc_trim(0);
}
println!(
"run {i}: {:?}, sleeping for 5 secs",
res.selected_items.first().map(|x| x.output())
);
std::thread::sleep(std::time::Duration::from_secs(5));
}
}

View file

@ -1,25 +1,22 @@
//! Demonstrates matching against selected fields with the `nth` option.
extern crate skim;
use skim::prelude::*;
use std::io::Cursor;
/// Runs the `nth` example.
///
/// `nth` option is supported by `SkimItemReader`.
/// `nth` option is supported by SkimItemReader.
/// In the example below, with `nth=2` set, only `123` could be matched.
fn main() {
pub fn main() {
let input = "foo 123";
let options = SkimOptionsBuilder::default().query("f").build().unwrap();
let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth(vec!["2"].into_iter()).build());
let options = SkimOptionsBuilder::default().query(Some("f")).build().unwrap();
let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth("2").build());
let items = item_reader.of_bufread(Cursor::new(input));
let selected_items = Skim::run_with(options, Some(items))
let selected_items = Skim::run_with(&options, Some(items))
.map(|out| out.selected_items)
.unwrap_or_default();
.unwrap_or_else(Vec::new);
for item in &selected_items {
for item in selected_items.iter() {
println!("{}", item.output());
}
}

View file

@ -1,35 +1,36 @@
//! Demonstrates configuring skim with `SkimOptionsBuilder`.
extern crate skim;
use skim::prelude::*;
use std::io::Cursor;
fn main() {
pub fn main() {
let options = SkimOptionsBuilder::default()
.height(Some("50%"))
.multi(true)
.build()
.unwrap();
let item_reader = SkimItemReader::default();
//==================================================
// first run
let options = SkimOptionsBuilder::default().height("50%").multi(true).build().unwrap();
let input = "aaaaa\nbbbb\nccc";
let items = item_reader.of_bufread(Cursor::new(input));
let selected_items = Skim::run_with(options, Some(items))
let selected_items = Skim::run_with(&options, Some(items))
.map(|out| out.selected_items)
.unwrap_or_default();
.unwrap_or_else(Vec::new);
for item in &selected_items {
for item in selected_items.iter() {
println!("{}", item.output());
}
//==================================================
// second run
let options = SkimOptionsBuilder::default().height("50%").multi(true).build().unwrap();
let input = "11111\n22222\n333333333";
let items = item_reader.of_bufread(Cursor::new(input));
let selected_items = Skim::run_with(options, Some(items))
let selected_items = Skim::run_with(&options, Some(items))
.map(|out| out.selected_items)
.unwrap_or_default();
.unwrap_or_else(Vec::new);
for item in &selected_items {
for item in selected_items.iter() {
println!("{}", item.output());
}
}

View file

@ -1,27 +0,0 @@
//! Demonstrates generating preview content with a callback.
use std::io::Cursor;
use skim::prelude::*;
fn main() {
env_logger::init();
let options = SkimOptionsBuilder::default()
.multi(true)
.preview_fn(PreviewCallback::from(|items: Vec<Arc<dyn SkimItem>>| {
items.iter().map(|s| s.text().to_ascii_uppercase()).collect::<Vec<_>>()
}))
.build()
.unwrap();
let item_reader = SkimItemReader::default();
let input = "aaaaa\nbbbb\nccc";
let items = item_reader.of_bufread(Cursor::new(input));
let selected_items = Skim::run_with(options, Some(items))
.map(|out| out.selected_items)
.unwrap_or_default();
for item in &selected_items {
println!("{}", item.output());
}
}

View file

@ -1,20 +0,0 @@
//! Sends multiple batches of items through a skim item receiver.
use std::sync::Arc;
use skim::prelude::*;
fn main() {
let (sender, receiver): (SkimItemSender, SkimItemReceiver) = unbounded();
let mut batch = Vec::new();
for num in 1..=8 {
batch.push(Arc::new(format!("Option {num}")) as Arc<dyn SkimItem>);
}
sender.send(batch).unwrap();
drop(sender); // bug replicates even without this
let _ = Skim::run_with(
SkimOptionsBuilder::default().multi(true).build().unwrap(),
Some(receiver),
);
}

View file

@ -1,16 +1,14 @@
//! Minimal example that runs skim and prints selected items.
extern crate skim;
use skim::prelude::*;
fn main() {
pub fn main() {
let options = SkimOptions::default();
let selected_items = Skim::run_with(options, None)
let selected_items = Skim::run_with(&options, None)
.map(|out| out.selected_items)
.unwrap_or_default();
.unwrap_or_else(Vec::new);
for item in &selected_items {
for item in selected_items.iter() {
println!("{}", item.output());
}
}

View file

@ -1,34 +0,0 @@
//! Demonstrates a custom selector implementation.
extern crate skim;
use skim::prelude::*;
struct BasicSelector {
pub pat: String,
}
impl Selector for BasicSelector {
fn should_select(&self, _index: usize, item: &dyn SkimItem) -> bool {
item.text().contains(&self.pat)
}
}
fn main() {
let selector = BasicSelector {
pat: String::from("examples"),
};
let options = SkimOptionsBuilder::default()
.multi(true)
.selector(Rc::from(selector))
.query("skim/")
.build()
.unwrap();
let selected_items = Skim::run_with(options, None)
.map(|out| out.selected_items)
.unwrap_or_default();
for item in &selected_items {
println!("{}", item.output());
}
}

View file

@ -1,23 +0,0 @@
//! Demonstrates manually driving skim ticks.
use skim::prelude::*;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let opts = SkimOptionsBuilder::default().cmd("cat bench_data.txt").build()?;
println!("START");
let mut skim = Skim::init(opts, None)?;
skim.start();
skim.init_tui()?;
skim.enter().await?;
while !skim.tick().await? {
if skim.reader_done() && skim.matcher_stopped() {
skim.event_sender()
.send(Event::Action(Action::Accept(Some(String::from("Done")))))
.await?;
}
}
println!("DONE: {:?}", skim.output());
eyre::Ok(())
}

View file

@ -1,24 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1782545840,
"narHash": "sha256-CAi8oAZaE6pTkcYQBnnOlvmfMgG/p1AO0FohKKN3J7I=",
"rev": "3d46470bb3030020f7e1361f33514854f5bfa86d",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1023445.3d46470bb303/nixexprs.tar.xz?lastModified=1782545840&rev=3d46470bb3030020f7e1361f33514854f5bfa86d"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View file

@ -1,85 +0,0 @@
{
description = "Nix flake for skim development";
inputs.nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
outputs =
inputs:
let
inherit (inputs.nixpkgs) lib;
systems = lib.systems.flakeExposed;
eachSystem = lib.genAttrs systems;
pkgsFor =
system:
import inputs.nixpkgs {
inherit system;
config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "vagrant" ];
};
in
{
devShells = eachSystem (
system:
let
pkgs = pkgsFor system;
# --- package groups -------------------------------------------------------
base = with pkgs; [
rustup
just
];
tests = with pkgs; [
cargo-nextest
cargo-insta
cargo-llvm-cov
cargo-fuzz
tmux
];
utils = with pkgs; [
hyperfine
cargo-edit
cargo-public-api
cargo-msrv
git-cliff
cargo-dist
cargo-cross
cargo-xwin
gnuplot
llvm
cargo-bloat
cargo-public-api
];
gungraun = with pkgs; [
valgrind
libclang
binutils
];
vagrantDeps = with pkgs; [
vagrant
rsync
];
# --- shell hooks (only groups that need env vars) -------------------------
gungraunHook = ''
export LIBCLANG_PATH="${pkgs.libclang.lib}/lib"
export LD_LIBRARY_PATH="${pkgs.valgrind.out}/lib:$LD_LIBRARY_PATH"
'';
vagrantHook = ''
export VAGRANT_LIBVIRT_OVMF_CODE="${pkgs.OVMF.fd}/FV/OVMF_CODE.fd"
'';
mkShell = packages: shellHook: pkgs.mkShellNoCC { inherit packages shellHook; };
in
{
default = mkShell base "";
tests = mkShell (base ++ tests) "";
utils = mkShell (base ++ utils) "";
dev = mkShell (base ++ tests ++ utils) "";
gungraun = mkShell (base ++ gungraun) gungraunHook;
vagrant = mkShell (base ++ vagrantDeps) vagrantHook;
full = mkShell (base ++ tests ++ utils ++ gungraun ++ vagrantDeps) (gungraunHook + vagrantHook);
}
);
formatter = eachSystem (system: (pkgsFor system).nixfmt);
};
}

4
fuzz/.gitignore vendored
View file

@ -1,4 +0,0 @@
target
corpus
artifacts
coverage

2267
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,52 +0,0 @@
[package]
name = "skim-fuzz"
version = "0.0.0"
publish = false
edition = "2024"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { version = "1", features = ["derive"] }
libfuzzer-sys = "0.4"
regex = "1"
[dependencies.skim]
path = ".."
default-features = false
[[bin]]
name = "ansi_strip"
path = "fuzz_targets/ansi_strip.rs"
test = false
doc = false
bench = false
[[bin]]
name = "field_extract"
path = "fuzz_targets/field_extract.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzzy_match"
path = "fuzz_targets/fuzzy_match.rs"
test = false
doc = false
bench = false
[[bin]]
name = "query_match"
path = "fuzz_targets/query_match.rs"
test = false
doc = false
bench = false
[[bin]]
name = "keymap_parse"
path = "fuzz_targets/keymap_parse.rs"
test = false
doc = false
bench = false

View file

@ -1,57 +0,0 @@
# Fuzzing
This directory contains [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)
(libFuzzer) targets for skim's hand-written, untrusted-input-facing parsers:
text that flows in from stdin, `--ansi` sequences, `--nth`/`--with-nth` field
specs, the search query syntax, and `--bind` key maps. These are exactly the
places where skim does manual byte/char-index bookkeeping on attacker- or
data-controlled strings, which is the most panic-prone code in the project.
## Targets
| Target | Exercises |
|-------------------|----------------------------------------------------------------------------|
| `ansi_strip` | `helper::item::strip_ansi` — ANSI escape stripping & byte/char index map |
| `field_extract` | `field::{FieldRange, get_string_by_field, parse_matching_fields, parse_transform_fields}``--nth`/`--with-nth` |
| `fuzzy_match` | `fuzzy_matcher::{skim, fzy, clangd}` — the fuzzy matching algorithms |
| `query_match` | `Matcher::create_engine_factory` + `DefaultSkimItem` — the full query → engine → match pipeline (exact/regex/AND-OR/fuzzy, with ANSI) |
| `keymap_parse` | `binds::KeyMap` — the `--bind` key-map parser |
Each target asserts more than "doesn't panic" where a cheap invariant is
available (e.g. reported match indices must be valid char indices into the
matched text, index mappings must stay monotonic and land on char
boundaries).
## Running
Install `cargo-fuzz` (requires a nightly toolchain):
```sh
cargo install cargo-fuzz
```
Run a target:
```sh
cargo +nightly fuzz run ansi_strip
```
Run for a bounded time (useful in CI or for a quick check):
```sh
cargo +nightly fuzz run query_match -- -max_total_time=60
```
## Reproducing a crash
`cargo fuzz run` writes failing inputs to `fuzz/artifacts/<target>/`. Replay one with:
```sh
cargo +nightly fuzz run <target> fuzz/artifacts/<target>/crash-<hash>
```
## Adding a target
Add a new `fuzz_targets/<name>.rs`, register it in `fuzz/Cargo.toml`'s
`[[bin]]` list, and prefer asserting a real invariant of the function under
test (bounds, monotonicity, round-tripping) rather than only catching panics.

View file

@ -1,37 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use skim::helper::item::strip_ansi;
// `strip_ansi` hand-parses ESC sequences while tracking a byte/char index
// mapping back to the original string. It is run on every line read from
// stdin when `--ansi` is set, so it must never panic on adversarial input
// and the mapping it returns must stay internally consistent.
fuzz_target!(|input: &str| {
let (stripped, mapping) = strip_ansi(input);
assert_eq!(
mapping.len(),
stripped.chars().count(),
"mapping length must match the number of chars in the stripped string"
);
let mut prev_byte_pos = None;
for &(byte_pos, char_idx) in &mapping {
assert!(
input.is_char_boundary(byte_pos),
"byte_pos {byte_pos} is not a char boundary in the original string"
);
// char_idx must be exactly the char position of byte_pos in the
// original string; this is stronger than (and implies) monotonicity.
let expected_char_idx = input[..byte_pos].chars().count();
assert_eq!(
char_idx, expected_char_idx,
"char_idx must equal the char position of byte_pos in the original string"
);
if let Some(prev) = prev_byte_pos {
assert!(prev < byte_pos, "byte positions in mapping must be strictly increasing");
}
prev_byte_pos = Some(byte_pos);
}
});

View file

@ -1,47 +0,0 @@
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use regex::Regex;
use skim::field::{FieldRange, get_string_by_field, parse_matching_fields, parse_transform_fields};
// Fuzzes the --nth/--with-nth field range parser and extractor, which slices
// arbitrary user-supplied text on an arbitrary user-supplied delimiter regex.
#[derive(Arbitrary, Debug)]
struct FieldFuzzInput<'a> {
delimiter_pattern: &'a str,
text: &'a str,
range_specs: Vec<&'a str>,
}
fuzz_target!(|input: FieldFuzzInput| {
// Bound the delimiter pattern length so we spend fuzzing time on the
// field logic rather than on the regex engine's own parser.
if input.delimiter_pattern.len() > 32 {
return;
}
let Ok(delimiter) = Regex::new(input.delimiter_pattern) else {
return;
};
let fields: Vec<FieldRange> = input
.range_specs
.iter()
.filter_map(|s| FieldRange::from_str(s))
.collect();
// `fields` can repeat/overlap ranges, so the transformed text is not
// bounded by the input length; just check it doesn't panic.
let _ = parse_transform_fields(&delimiter, input.text, &fields);
for (begin, end) in parse_matching_fields(&delimiter, input.text, &fields) {
assert!(begin <= end, "field range must not be inverted");
assert!(end <= input.text.len(), "field range must stay within the text");
// Slicing must not panic: begin/end must land on char boundaries.
let _ = &input.text[begin..end];
}
for field in &fields {
let _ = get_string_by_field(&delimiter, input.text, field);
}
});

View file

@ -1,39 +0,0 @@
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use skim::fuzzy_matcher::FuzzyMatcher;
use skim::fuzzy_matcher::clangd::ClangdMatcher;
use skim::fuzzy_matcher::fzy::FzyMatcher;
use skim::fuzzy_matcher::skim::SkimMatcherV2;
// Fuzzes the fuzzy matching algorithms directly on arbitrary unicode
// (choice, pattern) pairs. These run a lot of hand-written index/DP-matrix
// arithmetic over `char` boundaries, so they're prone to panics (overflow,
// out-of-bounds) on adversarial unicode input, and the returned match
// indices must always be valid character indices into `choice`.
#[derive(Arbitrary, Debug)]
struct MatchInput<'a> {
choice: &'a str,
pattern: &'a str,
}
fuzz_target!(|input: MatchInput| {
let skim_matcher = SkimMatcherV2::default();
let fzy_matcher = FzyMatcher::default();
let clangd_matcher = ClangdMatcher::default();
let matchers: [&dyn FuzzyMatcher; 3] = [&skim_matcher, &fzy_matcher, &clangd_matcher];
let num_chars = input.choice.chars().count();
for matcher in matchers {
if let Some((_score, indices)) = matcher.fuzzy_indices(input.choice, input.pattern) {
for &idx in &indices {
assert!(
idx < num_chars,
"match index {idx} out of bounds for choice with {num_chars} chars"
);
}
}
}
});

View file

@ -1,10 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use skim::binds::KeyMap;
// Fuzzes the `--bind` key-map parser, which splits an arbitrary user-supplied
// string on commas and colons to build key/action bindings.
fuzz_target!(|input: &str| {
let _ = KeyMap::from(input);
});

View file

@ -1,58 +0,0 @@
#![no_main]
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use regex::Regex;
use skim::helper::item::DefaultSkimItem;
use skim::matcher::Matcher;
use skim::{SkimItem, SkimOptions};
// End-to-end fuzz of the query -> engine -> match pipeline: builds a real
// `DefaultSkimItem` (exercising ANSI stripping / field transforms) and
// matches it with an engine built the same way skim builds it from CLI
// options (exact/regex/andor/fuzzy-algorithm wrapping), using an arbitrary
// query string. Checks that matching never panics and that any reported
// match range stays within the bounds of the text that was actually matched.
#[derive(Arbitrary, Debug)]
struct QueryInput<'a> {
query: &'a str,
text: &'a str,
exact: bool,
regex: bool,
ansi: bool,
case: u8,
}
fuzz_target!(|input: QueryInput| {
// The regex engine path takes the query as a user-supplied pattern; keep
// it short so fuzzing time goes into skim's logic, not regex parsing.
if input.regex && input.query.len() > 32 {
return;
}
let mut options = SkimOptions::default();
options.exact = input.exact;
options.regex = input.regex;
options.case = match input.case % 3 {
0 => skim::CaseMatching::Respect,
1 => skim::CaseMatching::Ignore,
_ => skim::CaseMatching::Smart,
};
let factory = Matcher::create_engine_factory(&options);
let engine = factory.create_engine_with_case(input.query, options.case);
let delimiter = Regex::new(" ").unwrap();
let item = DefaultSkimItem::new(input.text, input.ansi, &[], &[], &delimiter);
if let Some(result) = engine.match_item(&item) {
let matched_text = item.text();
let num_chars = matched_text.chars().count();
for idx in result.range_char_indices(&matched_text) {
assert!(
idx <= num_chars,
"matched char index {idx} out of bounds ({num_chars} chars)"
);
}
}
});

68
install Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
# This script trys to download the correct version of binary from github.
# You can download it manually and put it(i.e. `sk`) under `bin/`.
#
# If you know rust or have rust installed, you can build it with
# `cargo build --release`
set -u
version="0.9.4"
cd "$(dirname "${BASH_SOURCE[0]}")"
skim_base="$(pwd)"
check_binary() {
echo -n " - Checking skim executable ... "
local output
output=$("$skim_base"/bin/sk --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
elif [ "$version" != "$output" ]; then
echo "$output != $version"
else
echo "$output"
return 0
fi
rm -f "$skim_base"/bin/sk
return 1
}
# download version
download() {
echo "Downloading bin/sk ..."
mkdir -p "$skim_base"/bin && cd "$skim_base"/bin
if [ $? -ne 0 ]; then
binary_error="Failed to create bin directory"
return
fi
check_binary
local url=https://github.com/lotabout/skim/releases/download/v$version/${1}.tar.gz
echo "Downloading: $url"
if command -v curl > /dev/null; then
curl -fL $url | tar xz
elif command -v wget > /dev/null; then
wget -O - $url | tar xz
else
binary_error="curl or wget not found"
return
fi
if [ ! -f $1 ]; then
binary_error="Failed to download ${1}"
return
fi
}
archi=$(uname -sm)
case "$archi" in
Darwin\ x86_64) download skim-v$version-${binary_arch:-x86_64}-apple-darwin;;
Linux\ x86_64) download skim-v$version-${binary_arch:-x86_64}-unknown-linux-musl;;
*) ;;
esac
echo "Done :)"

View file

@ -1,99 +0,0 @@
alias pr := pr-review
bump-version version:
sed -i 's/^version = ".*"/version = "{{ version }}"/' ./Cargo.toml
generate-files:
SKIM_DEFAULT_OPTIONS= cargo run -- --man > ./man/man1/sk.1
SKIM_DEFAULT_OPTIONS= cargo run -- --shell bash > ./shell/completion.bash
SKIM_DEFAULT_OPTIONS= cargo run -- --shell zsh > ./shell/completion.zsh
SKIM_DEFAULT_OPTIONS= cargo run -- --shell fish > ./shell/completion.fish
SKIM_DEFAULT_OPTIONS= cargo run -- --shell nushell > ./shell/completion.nu
changelog version:
git cliff -p CHANGELOG.md -t 'v{{ version }}' -u
release version: (bump-version version) generate-files (changelog version) test
cargo generate-lockfile
echo '{{ version }}' > shell/version.txt
git add CHANGELOG.md Cargo.lock Cargo.toml man/ shell/
git commit -m 'release: v{{ version }}'
git tag 'v{{ version }}'
read -p "Press any key to confirm pushing tag v{{ version }}"
git push
git push --tags
auto-release:
just release $(git cliff --bumped-version | sed 's/v\(.*\)/\1/')
test target="":
cargo test --doc
cargo nextest run {{ target }}
# Each e2e test creates and tears down its own Zellij session, so there is
# no shared multiplexer session to clean up here.
bench-plot bins="./target/release/sk sk fzf":
#!/usr/bin/env bash
set -euo pipefail
declare -A inputs=(
["1"]=1
["10"]=10
["100"]=100
["1K"]=1000
["10K"]=10000
["100K"]=100000
["1M"]=1000000
["10M"]=10000000
["100M"]=100000000
)
echo "" > /tmp/bench.json
for f in "${!inputs[@]}"; do
p="benches/fixtures/$f.txt"
n="${inputs[$f]}"
if [ ! -f "$p" ]; then
cargo bench --bench cli -- generate -n "$n" -f "$p"
fi
# The formula for `--stable-secs` might need adjusting if the number of results varies between runs with the same input
cargo bench --bench cli -- run {{ bins }} -f "$p" --runs 10 --stable-secs "$(( 3 * $n / 10000000 )).5" --json >> /tmp/bench.json
done
cargo bench --bench cli -- plot -i /tmp/bench.json
pr-review id="":
#!/usr/bin/env bash
set -euo pipefail
PR_ID="{{ id }}"
if [[ -z "$PR_ID" ]]; then
PR_ID="$(gh pr list | sk | cut -d' ' -f1)"
fi
echo "Checking out PR $PR_ID"
gh pr checkout "$PR_ID"
# Check the PR title
PR_TITLE="$(gh pr view | head -n1 | sed 's@title:\s\+@@;s@skim-rs/skim#.*@@')"
echo "Checking $PR_TITLE by simulating a git cliff generation"
git cliff --from-context <(echo '[{"commits":[{"id": "foo", "message": "'$PR_TITLE'", "links": [], "author": {"name": "","timestamp":1}, "committer": {"name": "", "timestamp":0}, "merge_commit": false,"github":{"pr_labels":[], "is_first_time": false},"gitlab":{"pr_labels":[],"is_first_time":false},"gitea":{"pr_labels":[],"is_first_time":false},"bitbucket":{"pr_labels":[],"is_first_time":false},"azure_devops":{"pr_labels":[],"is_first_time":false}}],"submodule_commits":{},"github":{"contributors":[]},"gitlab":{"contributors":[]},"gitea":{"contributors":[]},"bitbucket":{"contributors":[]},"azure_devops":{"contributors":[]}}]') -s all | grep -Ev '^(## \[unreleased\]|)$' | grep -q '^.\+$'
echo "Check done, PR title is valid"
git diff master
tty -s && read -p "Confirm code review ?"
just generate-files
(git add man/ shell/ && git commit -m 'chore: generate-files' && git push) || echo "Nothing to do"
coverage *args="":
cargo +nightly llvm-cov nextest --ignore-run-fail --branch --lib --bins --examples --tests {{ args }}
cargo +nightly llvm-cov report --html
clippy *args="":
cargo clippy --all-targets {{ args }}
fmt *args="":
cargo +nightly fmt {{ args }}

View file

@ -22,7 +22,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH sk-tmux 1 "Oct 2018" "sk 0.10.4" "sk-tmux - open sk in tmux split pane"
.TH sk-tmux 1 "Oct 2018" "sk 0.17.5" "sk-tmux - open sk in tmux split pane"
.SH NAME
sk-tmux - open sk in tmux split pane

File diff suppressed because it is too large Load diff

View file

@ -136,12 +136,38 @@ function! s:default_layout()
\ : { 'down': '~40%' }
endfunction
function! skim#install()
if s:is_win && !has('win32unix')
let script = s:base_dir.'/install.ps1'
if !filereadable(script)
throw script.' not found'
endif
let script = 'powershell -ExecutionPolicy Bypass -file ' . script
else
let script = s:base_dir.'/install'
if !executable(script)
throw script.' not found'
endif
let script .= ' --bin'
endif
call s:warn('Running skim installer ...')
call system(script)
if v:shell_error
throw 'Failed to download skim: '.script
endif
endfunction
function! skim#exec()
if !exists('s:exec')
if executable(s:skim_rs)
let s:exec = s:skim_rs
elseif executable('sk')
let s:exec = 'sk'
elseif input('skim executable not found. Download binary? (y/n) ') =~? '^y'
redraw
call skim#install()
return skim#exec()
else
redraw
throw 'skim executable not found'
@ -316,12 +342,6 @@ function! skim#wrap(...)
endif
endif
" Interactive commands should use --cmd-history for query history
let history_option = '--history'
if index(opts['options'], '-i') != -1
let history_option = '--cmd-history'
endif
" Colors: g:skim_colors
let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
@ -332,13 +352,13 @@ function! skim#wrap(...)
call mkdir(dir, 'p')
endif
let history = skim#shellescape(dir.'/'.name)
let opts.options = join([history_option, history, opts.options])
let opts.options = join(['--history', history, opts.options])
endif
" Action: g:skim_action
if !s:has_any(opts, ['sink', 'sink*'])
let opts._action = get(g:, 'skim_action', s:default_action)
let opts.options .= ' '.skim#shellescape('--bind='.join(map(keys(opts._action), 'v:val.":accept(".v:val.")"'), ','))
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
@ -492,17 +512,21 @@ function! s:dopopd()
return
endif
" Note: We temporarily change the working directory to 'dir' entry
" FIXME: We temporarily change the working directory to 'dir' entry
" of options dictionary (set to the current working directory if not given)
" before running skim.
"
" e.g. call skim#run({'dir': '/tmp', 'source': 'ls', 'sink': 'e'})
"
" After processing the sink function, we restore the current working
" directory using a heuristic: we only change directory if the current
" working directory matches 'dir' entry. This handles most cases correctly,
" though it may not restore the directory if the sink function explicitly
" changed to the 'dir' path.
" After processing the sink function, we have to restore the current working
" directory. But doing so may not be desirable if the function changed the
" working directory on purpose.
"
" So how can we tell if we should do it or not? A simple heuristic we use
" here is that we change directory only if the current working directory
" matches 'dir' entry. However, it is possible that the sink function did
" change the directory to 'dir'. In that case, the user will have an
" unexpected result.
if s:skim_getcwd() ==# w:skim_pushd.dir && (!&autochdir || w:skim_pushd.bufname ==# bufname(''))
execute w:skim_pushd.command s:escape(w:skim_pushd.origin)
endif
@ -907,7 +931,6 @@ function! s:popup(opts) abort
endfunction
let s:default_action = {
\ 'enter': 'edit',
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }

View file

@ -1,4 +0,0 @@
[toolchain]
channel = "stable"
profile = "default"
components = ["rust-analyzer"]

View file

@ -1,438 +1,373 @@
_sk() {
local i cur prev opts cmd
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
cur="$2"
else
cur="${COMP_WORDS[COMP_CWORD]}"
fi
prev="$3"
cmd=""
opts=""
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ completion.bash
#
# - $SKIM_TMUX (default: 0)
# - $SKIM_TMUX_OPTS (default: empty)
# - $SKIM_COMPLETION_TRIGGER (default: '**')
# - $SKIM_COMPLETION_OPTS (default: empty)
# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/completion.bash
for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
do
case "${cmd},${i}" in
",$1")
cmd="sk"
;;
*)
;;
esac
done
if [[ $- =~ i ]]; then
case "${cmd}" in
sk)
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --min-query-length --no-sort --tiebreak --nth --with-nth --hide-nth --delimiter --exact --regex --algo --case --typos --no-typos --normalize --split-match --last-match --scheme --bind --multi --no-multi --no-mouse --cmd --interactive --color --highlight-line --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --disable-pattern --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --ellipsis --info --no-info --inline-info --header --header-lines --border --border-no-collapse --no-border --wrap --multiline --scrollbar --no-scrollbar --history --history-size --cmd-history --cmd-history-size --preview --preview-window --image --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --print-current --output-format --no-strip-ansi --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --shell-bindings --man --listen --remote --popup --log-level --log-file --flags --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --tail --style --no-color --padding --border-label --border-label-pos --wrap-sign --no-multi-line --raw --track --gap --gap-line --freeze-left --freeze-right --scroll-off --gutter --gutter-raw --marker-multi-line --list-border --list-label --list-label-pos --no-input --info-command --separator --no-separator --ghost --input-border --input-label --input-label-pos --preview-label --preview-label-pos --header-first --header-border --header-lines-border --footer --footer-border --footer-label --footer-label-pos --with-shell --expect --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--min-query-length)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--tiebreak)
COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index pathname -pathname" -- "${cur}"))
return 0
;;
-t)
COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index pathname -pathname" -- "${cur}"))
return 0
;;
--nth)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-n)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--with-nth)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--hide-nth)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--delimiter)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--algo)
COMPREPLY=($(compgen -W "arinae clangd fzy frizbee skim_v2" -- "${cur}"))
return 0
;;
--case)
COMPREPLY=($(compgen -W "respect ignore smart" -- "${cur}"))
return 0
;;
--typos)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--split-match)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--scheme)
COMPREPLY=($(compgen -W "default path history" -- "${cur}"))
return 0
;;
--bind)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-b)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--cmd)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-c)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-I)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--skip-to-pattern)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--disable-pattern)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--layout)
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "${cur}"))
return 0
;;
--height)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--min-height)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--margin)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--prompt)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-p)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--cmd-prompt)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--selector)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--multi-selector)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--tabstop)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ellipsis)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--info)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--header)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--header-lines)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--border)
COMPREPLY=($(compgen -W "force-off none plain rounded double thick light-double-dashed heavy-double-dashed light-triple-dashed heavy-triple-dashed light-quadruple-dashed heavy-quadruple-dashed quadrant-inside quadrant-outside" -- "${cur}"))
return 0
;;
--multiline)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--scrollbar)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--history)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--history-size)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--cmd-history)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--cmd-history-size)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--preview)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--preview-window)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--image)
COMPREPLY=($(compgen -W "detect halfblocks" -- "${cur}"))
return 0
;;
--query)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-q)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--cmd-query)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--output-format)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--pre-select-n)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--pre-select-pat)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--pre-select-items)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--pre-select-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell)
COMPREPLY=($(compgen -W "bash elvish fish nushell power-shell zsh" -- "${cur}"))
return 0
;;
--listen)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--remote)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--popup)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--log-level)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--flags)
COMPREPLY=($(compgen -W "no-preview-pty show-score show-index single-reader single-matcher" -- "${cur}"))
return 0
;;
--hscroll-off)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--jump-labels)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--tail)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--style)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--padding)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--border-label)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--border-label-pos)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--wrap-sign)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--gap)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--gap-line)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--freeze-left)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--freeze-right)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--scroll-off)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--gutter)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--gutter-raw)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--marker-multi-line)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--list-border)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--list-label)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--list-label-pos)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--info-command)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--separator)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ghost)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--input-border)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--input-label)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--input-label-pos)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--preview-label)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--preview-label-pos)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--header-border)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--header-lines-border)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--footer)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--footer-border)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--footer-label)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--footer-label-pos)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--with-shell)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--expect)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
# To use custom commands instead of find, override _skim_compgen_{path,dir}
if ! declare -f _skim_compgen_path > /dev/null; then
_skim_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
if ! declare -f _skim_compgen_dir > /dev/null; then
_skim_compgen_dir() {
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
###########################################################
# To redraw line after skim closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
__skim_comprun() {
if [ "$(type -t _skim_comprun 2>&1)" = function ]; then
_skim_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
shift
sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- "$@"
else
shift
sk "$@"
fi
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _sk -o nosort -o bashdefault -o default sk
else
complete -F _sk -o bashdefault -o default sk
__skim_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _skim_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__skim_nospace_commands" = *" \3 "* ]] \&\& __skim_nospace_commands="$__skim_nospace_commands \3 ";/' |
awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1'
}
_skim_opts_completion() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-x --extended
-e --exact
--algo
-i +i
-n --nth
--with-nth
-d --delimiter
+s --no-sort
--tac
--tiebreak
-m --multi
--no-mouse
--bind
--cycle
--no-hscroll
--jump-labels
--height
--literal
--reverse
--margin
--inline-info
--prompt
--pointer
--marker
--header
--header-lines
--ansi
--tabstop
--color
--no-bold
--history
--history-size
--preview
--preview-window
-q --query
-1 --select-1
-0 --exit-0
-f --filter
--print-query
--expect
--sync"
case "${prev}" in
--tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
return 0
;;
--color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
return 0
;;
--history)
COMPREPLY=()
return 0
;;
esac
if [[ "$cur" =~ ^-|\+ ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
return 0
fi
return 0
}
_skim_handle_dynamic_completion() {
local cmd orig_var orig ret orig_cmd orig_complete
cmd="$1"
shift
orig_cmd="$1"
orig_var="_skim_orig_completion_$cmd"
orig="${!orig_var##*#}"
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
$orig "$@"
elif [ -n "$_skim_completion_loader" ]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@"
ret=$?
# _completion_loader may not have updated completion for the command
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
eval "$(complete | command grep " -F.* $orig_cmd$" | __skim_orig_completion_filter)"
if [[ "$__skim_nospace_commands" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }"
else
eval "$orig_complete"
fi
fi
return $ret
fi
}
__skim_generic_path_completion() {
local cur base dir leftover matches trigger cmd
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=()
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}}
eval "base=$base"
[[ $base = *"/"* ]] && dir="$base"
while true; do
if [ -z "$dir" ] || [ -d "$dir" ]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $2" __skim_comprun "$4" -q "$leftover" | while read -r item; do
printf "%q$3 " "$item"
done)
matches=${matches% }
[[ -z "$3" ]] && [[ "$__skim_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [ -n "$matches" ]; then
COMPREPLY=( "$matches" )
else
COMPREPLY=( "$cur" )
fi
printf '\e[5n'
return 0
fi
dir=$(dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/
done
else
shift
shift
shift
_skim_handle_dynamic_completion "$cmd" "$@"
fi
}
_skim_complete() {
# Split arguments around --
local args rest str_arg i sep
args=("$@")
sep=
for i in "${!args[@]}"; do
if [[ "${args[$i]}" = -- ]]; then
sep=$i
break
fi
done
if [[ -n "$sep" ]]; then
str_arg=
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
args=("${args[@]:0:$sep}")
else
str_arg=$1
args=()
shift
rest=("$@")
fi
local cur selected trigger cmd post
post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
selected=$(SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [ -n "$selected" ]; then
COMPREPLY=("$selected")
else
COMPREPLY=("$cur")
fi
printf '\e[5n'
return 0
else
_skim_handle_dynamic_completion "$cmd" "${rest[@]}"
fi
}
_skim_path_completion() {
__skim_generic_path_completion _skim_compgen_path "-m" "" "$@"
}
# Deprecated. No file only completion.
_skim_file_completion() {
_skim_path_completion "$@"
}
_skim_dir_completion() {
__skim_generic_path_completion _skim_compgen_dir "" "/" "$@"
}
_skim_complete_kill() {
local trigger=${SKIM_COMPLETION_TRIGGER-'**'}
local cur="${COMP_WORDS[COMP_CWORD]}"
if [[ -z "$cur" ]]; then
COMP_WORDS[$COMP_CWORD]=$trigger
elif [[ "$cur" != *"$trigger" ]]; then
return 1
fi
_skim_proc_completion "$@"
}
_skim_proc_completion() {
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d
)
}
_skim_proc_completion_post() {
awk '{print $2}'
}
_skim_host_completion() {
_skim_complete --no-multi -- "$@" < <(
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_skim_var_completion() {
_skim_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_skim_alias_completion() {
_skim_complete -m -- "$@" < <(
alias | sed 's/=.*//' | sed 's/.* //'
)
}
# skim options
complete -o default -F _skim_opts_completion sk
d_cmds="${SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
awk cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg java
javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open
basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp
svn tar unzip zip"
# Preserve existing completion
eval "$(complete |
sed -E '/-F/!d; / _skim/d; '"/ ($(echo $d_cmds $a_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
__skim_orig_completion_filter)"
if type _completion_loader > /dev/null 2>&1; then
_skim_completion_loader=1
fi
__skim_defc() {
local cmd func opts orig_var orig def
cmd="$1"
func="$2"
opts="$3"
orig_var="_skim_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}"
if [ -n "$orig" ]; then
printf -v def "$orig" "$func"
eval "$def"
else
complete -F "$func" $opts "$cmd"
fi
}
# Anything
for cmd in $a_cmds; do
__skim_defc "$cmd" _skim_path_completion "-o default -o bashdefault"
done
# Directory
for cmd in $d_cmds; do
__skim_defc "$cmd" _skim_dir_completion "-o nospace -o dirnames"
done
# Kill completion (supports empty completion trigger)
complete -F _skim_complete_kill -o default -o bashdefault kill
unset cmd d_cmds a_cmds
_skim_setup_completion() {
local kind fn cmd
kind=$1
fn=_skim_${1}_completion
if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then
echo "usage: ${FUNCNAME[0]} path|dir|var|alias|host|proc COMMANDS..."
return 1
fi
shift
eval "$(complete -p "$@" 2> /dev/null | grep -v "$fn" | __skim_orig_completion_filter)"
for cmd in "$@"; do
case "$kind" in
dir) __skim_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
var) __skim_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
alias) __skim_defc "$cmd" "$fn" "-a" ;;
*) __skim_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
esac
done
}
# Environment variables / Aliases / Hosts
_skim_setup_completion 'var' export unset
_skim_setup_completion 'alias' unalias
_skim_setup_completion 'host' ssh telnet
fi

View file

@ -1,190 +0,0 @@
complete -c sk -l min-query-length -d 'Minimum query length to start showing results' -r
complete -c sk -s t -l tiebreak -d 'Comma-separated list of sort criteria to apply when the scores are tied.' -r -f -a "score\t''
-score\t''
begin\t''
-begin\t''
end\t''
-end\t''
length\t''
-length\t''
index\t''
-index\t''
pathname\t''
-pathname\t''"
complete -c sk -s n -l nth -d 'Fields to be matched' -r
complete -c sk -l with-nth -d 'Fields to be transformed' -r
complete -c sk -l hide-nth -d 'Fields to hide from display while keeping them searchable' -r
complete -c sk -s d -l delimiter -d 'Delimiter between fields' -r
complete -c sk -l algo -d 'Fuzzy matching algorithm' -r -f -a "arinae\t'Arinae: typo-resistant & natural algorithm, default'
clangd\t'Clangd fuzzy matching algorithm'
fzy\t'Fzy matching algorithm (https://github.com/jhawthorn/fzy)'
frizbee\t'Frizbee matching algorithm, typo resistant'
skim_v2\t'Previous skim fuzzy matching algorithm (v2)'"
complete -c sk -l case -d 'Case sensitivity' -r -f -a "respect\t'Case-sensitive matching'
ignore\t'Case-insensitive matching'
smart\t'Smart case: case-insensitive unless query contains uppercase'"
complete -c sk -l typos -d 'Enable typo-tolerant matching' -r
complete -c sk -l split-match -d 'Enable split matching and set delimiter' -r
complete -c sk -l scheme -r -f -a "default\t'Default scheme, no modifications to the options'
path\t'Path scheme: will find the furthest match in the item and set pathname as the main tiebreak'
history\t'History scheme: will force index as the first tiebreak'"
complete -c sk -s b -l bind -d 'Comma-separated key, event, and action bindings' -r
complete -c sk -s c -l cmd -d 'Command to invoke dynamically in interactive mode' -r
complete -c sk -s I -d 'Replace replstr with the selected item in commands' -r
complete -c sk -l color -d 'Set color theme' -r
complete -c sk -l skip-to-pattern -d 'Show the matched pattern at the line start' -r
complete -c sk -l disable-pattern -d 'Disable items based on this regex pattern' -r
complete -c sk -l layout -d 'Set layout' -r -f -a "default\t'Display from the bottom of the screen'
reverse\t'Display from the top of the screen'
reverse-list\t'Display from the top of the screen, prompt at the bottom'"
complete -c sk -l height -d 'Height of skim\'s window' -r
complete -c sk -l min-height -d 'Minimum height of skim\'s window as a non-negative row count' -r
complete -c sk -l margin -d 'Screen margin' -r
complete -c sk -s p -l prompt -d 'Set prompt' -r
complete -c sk -l cmd-prompt -d 'Set prompt in command mode' -r
complete -c sk -l selector -d 'Set selected item icon' -r
complete -c sk -l multi-selector -d 'Set multi-selected item icon' -r
complete -c sk -l tabstop -d 'Number of spaces that make up a tab' -r
complete -c sk -l ellipsis -d 'The characters used to display truncated lines' -r
complete -c sk -l info -d 'Set matching result count display position' -r
complete -c sk -l header -d 'Set header, displayed next to the info' -r
complete -c sk -l header-lines -d 'Number of lines of the input treated as header' -r
complete -c sk -l border -d 'Draw borders around the UI components' -r -f -a "force-off\t'ForceOff disables borders around popups too set with no_border'
none\t''
plain\t''
rounded\t''
double\t''
thick\t''
light-double-dashed\t''
heavy-double-dashed\t''
light-triple-dashed\t''
heavy-triple-dashed\t''
light-quadruple-dashed\t''
heavy-quadruple-dashed\t''
quadrant-inside\t''
quadrant-outside\t''"
complete -c sk -l multiline -d 'Split item text into multiple display lines at the given separator character defaults to \\n if read0 is set, and \\\\n if not (matching literal \\n in text)' -r
complete -c sk -l scrollbar -d 'Set scrollbar style for the item list' -r
complete -c sk -l history -d 'History file' -r
complete -c sk -l history-size -d 'Maximum number of query history entries to keep' -r
complete -c sk -l cmd-history -d 'Command history file' -r
complete -c sk -l cmd-history-size -d 'Maximum number of query history entries to keep' -r
complete -c sk -l preview -d 'Preview command' -r
complete -c sk -l preview-window -d 'Preview window layout' -r
complete -c sk -l image -d 'Enable image preview' -r -f -a "detect\t'Default: automatically detect the available backend at startup'
halfblocks\t'Force halfblocks if you want blurry previews but a faster startup or if the detection fails'"
complete -c sk -s q -l query -d 'Initial query' -r
complete -c sk -l cmd-query -d 'Initial query in interactive mode' -r
complete -c sk -l output-format -d 'Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands' -r
complete -c sk -l pre-select-n -d 'Pre-select the first n items in multi-selection mode' -r
complete -c sk -l pre-select-pat -d 'Pre-select the matched items in multi-selection mode' -r
complete -c sk -l pre-select-items -d 'Pre-select the items separated by newline character' -r
complete -c sk -l pre-select-file -d 'Pre-select the items read from this file' -r
complete -c sk -s f -l filter -d 'Query for filter mode' -r
complete -c sk -l shell -d 'Generate shell completion script' -r -f -a "bash\t'Bourne Again SHell'
elvish\t'Elvish shell'
fish\t'Friendly Interactive SHell'
nushell\t'Nushell (nu)'
power-shell\t'PowerShell'
zsh\t'Zsh'"
complete -c sk -l listen -d 'Run an IPC socket with optional name (defaults to sk)' -r
complete -c sk -l remote -d 'Send commands to an IPC socket with optional name (defaults to sk)' -r
complete -c sk -l popup -d 'Run in a tmux or zellij popup' -r
complete -c sk -l log-level -d 'Set the log level' -r
complete -c sk -l log-file -d 'Pipe log output to a file' -r
complete -c sk -l flags -d 'Feature flags' -r -f -a "no-preview-pty\t'Disable preview PTY on Linux'
show-score\t'Display the item\'s match score before its value in the item list (for matcher debugging)'
show-index\t'Display the item\'s index before its value in the item list'
single-reader\t'Limit the reader thread pool to a single thread'
single-matcher\t'Limit the matcher thread pool to a single thread'"
complete -c sk -l hscroll-off -r
complete -c sk -l jump-labels -r
complete -c sk -l tail -r
complete -c sk -l style -r
complete -c sk -l padding -r
complete -c sk -l border-label -r
complete -c sk -l border-label-pos -r
complete -c sk -l wrap-sign -r
complete -c sk -l gap -r
complete -c sk -l gap-line -r
complete -c sk -l freeze-left -r
complete -c sk -l freeze-right -r
complete -c sk -l scroll-off -r
complete -c sk -l gutter -r
complete -c sk -l gutter-raw -r
complete -c sk -l marker-multi-line -r
complete -c sk -l list-border -r
complete -c sk -l list-label -r
complete -c sk -l list-label-pos -r
complete -c sk -l info-command -r
complete -c sk -l separator -r
complete -c sk -l ghost -r
complete -c sk -l input-border -r
complete -c sk -l input-label -r
complete -c sk -l input-label-pos -r
complete -c sk -l preview-label -r
complete -c sk -l preview-label-pos -r
complete -c sk -l header-border -r
complete -c sk -l header-lines-border -r
complete -c sk -l footer -r
complete -c sk -l footer-border -r
complete -c sk -l footer-label -r
complete -c sk -l footer-label-pos -r
complete -c sk -l with-shell -r
complete -c sk -l expect -d 'Deprecated, kept for compatibility purposes. See accept() bind instead' -r
complete -c sk -l tac -d 'Show results in reverse order'
complete -c sk -l no-sort -d 'Do not sort the results'
complete -c sk -s e -l exact -d 'Run in exact mode'
complete -c sk -l regex -d 'Start in regex mode instead of fuzzy-match'
complete -c sk -l no-typos -d 'Disable typo-tolerant matching'
complete -c sk -l normalize -d 'Normalize unicode characters'
complete -c sk -l last-match -d 'Highlight the last match found, not the first one This makes tiebreak more pertinent on path items where we want to prioritize a match on the last parts'
complete -c sk -s m -l multi -d 'Enable multiple selection'
complete -c sk -l no-multi -d 'Disable multiple selection'
complete -c sk -l no-mouse -d 'Disable mouse'
complete -c sk -s i -l interactive -d 'Start skim in interactive mode'
complete -c sk -l highlight-line -d 'Highlight the entire current line, not just the text'
complete -c sk -l no-hscroll -d 'Disable horizontal scroll'
complete -c sk -l keep-right -d 'Keep the right end of the line visible on overflow'
complete -c sk -l no-clear-if-empty -d 'Do not clear previous line if the command returns an empty result'
complete -c sk -l no-clear-start -d 'Do not clear items on start'
complete -c sk -l no-clear -d 'Do not clear screen on exit'
complete -c sk -l show-cmd-error -d 'Show error message if command fails'
complete -c sk -l cycle -d 'Cycle the results by wrapping around when scrolling'
complete -c sk -l disabled -d 'Disable matching entirely'
complete -c sk -l reverse -d 'Shorthand for reverse layout'
complete -c sk -l no-height -d 'Disable height (force full screen)'
complete -c sk -l ansi -d 'Parse ANSI color codes in input strings'
complete -c sk -l no-info -d 'Alias for --info=hidden'
complete -c sk -l inline-info -d 'Alias for --info=inline'
complete -c sk -l border-no-collapse -d 'Do not collapse adjacent borders into a shared row or column'
complete -c sk -l no-border -d 'Disables all borders, including in tmux/zellij popups'
complete -c sk -l wrap -d 'Wrap items in the item list'
complete -c sk -l no-scrollbar -d 'Disable the scrollbar in the item list'
complete -c sk -l read0 -d 'Read input delimited by ASCII NUL(\\0) characters'
complete -c sk -l print0 -d 'Print output delimited by ASCII NUL(\\0) characters'
complete -c sk -l print-query -d 'Print the query as the first line'
complete -c sk -l print-cmd -d 'Print the command as the first line (after print-query)'
complete -c sk -l print-score -d 'Print the score after each item'
complete -c sk -l print-header -d 'Print the header as the first line (after print-score)'
complete -c sk -l print-current -d 'Print the current (highlighted) item as the first line (after print-header)'
complete -c sk -l no-strip-ansi -d 'Print the ANSI codes, making the output exactly match the input even when --ansi is on'
complete -c sk -s 1 -l select-1 -d 'Do not enter the TUI if the query passed in -q matches only one item and return it'
complete -c sk -s 0 -l exit-0 -d 'Do not enter the TUI if the query passed in -q does not match any item'
complete -c sk -l sync -d 'Synchronous search for multi-staged filtering'
complete -c sk -l shell-bindings -d 'Generate shell key bindings - only for bash, zsh and fish'
complete -c sk -l man -d 'Generate man page and output it to stdout'
complete -c sk -s x -l extended
complete -c sk -l literal
complete -c sk -l filepath-word
complete -c sk -l no-bold
complete -c sk -l phony
complete -c sk -l no-color
complete -c sk -l no-multi-line
complete -c sk -l raw
complete -c sk -l track
complete -c sk -l no-input
complete -c sk -l no-separator
complete -c sk -l header-first
complete -c sk -s h -l help -d 'Print help (see more with \'--help\')'
complete -c sk -s V -l version -d 'Print version'

View file

@ -1,191 +0,0 @@
module completions {
def "nu-complete sk tiebreak" [] {
[ "score" "-score" "begin" "-begin" "end" "-end" "length" "-length" "index" "-index" "pathname" "-pathname" ]
}
def "nu-complete sk algorithm" [] {
[ "arinae" "clangd" "fzy" "frizbee" "skim_v2" ]
}
def "nu-complete sk case" [] {
[ "respect" "ignore" "smart" ]
}
def "nu-complete sk scheme" [] {
[ "default" "path" "history" ]
}
def "nu-complete sk layout" [] {
[ "default" "reverse" "reverse-list" ]
}
def "nu-complete sk border" [] {
[ "force-off" "none" "plain" "rounded" "double" "thick" "light-double-dashed" "heavy-double-dashed" "light-triple-dashed" "heavy-triple-dashed" "light-quadruple-dashed" "heavy-quadruple-dashed" "quadrant-inside" "quadrant-outside" ]
}
def "nu-complete sk image" [] {
[ "detect" "halfblocks" ]
}
def "nu-complete sk shell" [] {
[ "bash" "elvish" "fish" "nushell" "power-shell" "zsh" ]
}
def "nu-complete sk flags" [] {
[ "no-preview-pty" "show-score" "show-index" "single-reader" "single-matcher" ]
}
# Fuzzy Finder in rust!
export extern sk [
--tac # Show results in reverse order
--min-query-length: string # Minimum query length to start showing results
--no-sort # Do not sort the results
--tiebreak(-t): string@"nu-complete sk tiebreak" # Comma-separated list of sort criteria to apply when the scores are tied.
--nth(-n): string # Fields to be matched
--with-nth: string # Fields to be transformed
--hide-nth: string # Fields to hide from display while keeping them searchable
--delimiter(-d): string # Delimiter between fields
--exact(-e) # Run in exact mode
--regex # Start in regex mode instead of fuzzy-match
--algo: string@"nu-complete sk algorithm" # Fuzzy matching algorithm
--case: string@"nu-complete sk case" # Case sensitivity
--typos: string # Enable typo-tolerant matching
--no-typos # Disable typo-tolerant matching
--normalize # Normalize unicode characters
--split-match: string # Enable split matching and set delimiter
--last-match # Highlight the last match found, not the first one This makes tiebreak more pertinent on path items where we want to prioritize a match on the last parts
--scheme: string@"nu-complete sk scheme"
--bind(-b): string # Comma-separated key, event, and action bindings
--multi(-m) # Enable multiple selection
--no-multi # Disable multiple selection
--no-mouse # Disable mouse
--cmd(-c): string # Command to invoke dynamically in interactive mode
--interactive(-i) # Start skim in interactive mode
-I: string # Replace replstr with the selected item in commands
--color: string # Set color theme
--highlight-line # Highlight the entire current line, not just the text
--no-hscroll # Disable horizontal scroll
--keep-right # Keep the right end of the line visible on overflow
--skip-to-pattern: string # Show the matched pattern at the line start
--no-clear-if-empty # Do not clear previous line if the command returns an empty result
--no-clear-start # Do not clear items on start
--no-clear # Do not clear screen on exit
--show-cmd-error # Show error message if command fails
--cycle # Cycle the results by wrapping around when scrolling
--disabled # Disable matching entirely
--disable-pattern: string # Disable items based on this regex pattern
--layout: string@"nu-complete sk layout" # Set layout
--reverse # Shorthand for reverse layout
--height: string # Height of skim's window
--no-height # Disable height (force full screen)
--min-height: string # Minimum height of skim's window as a non-negative row count
--margin: string # Screen margin
--prompt(-p): string # Set prompt
--cmd-prompt: string # Set prompt in command mode
--selector: string # Set selected item icon
--multi-selector: string # Set multi-selected item icon
--ansi # Parse ANSI color codes in input strings
--tabstop: string # Number of spaces that make up a tab
--ellipsis: string # The characters used to display truncated lines
--info: string # Set matching result count display position
--no-info # Alias for --info=hidden
--inline-info # Alias for --info=inline
--header: string # Set header, displayed next to the info
--header-lines: string # Number of lines of the input treated as header
--border: string@"nu-complete sk border" # Draw borders around the UI components
--border-no-collapse # Do not collapse adjacent borders into a shared row or column
--no-border # Disables all borders, including in tmux/zellij popups
--wrap # Wrap items in the item list
--multiline: string # Split item text into multiple display lines at the given separator character defaults to \n if read0 is set, and \\n if not (matching literal \n in text)
--scrollbar: string # Set scrollbar style for the item list
--no-scrollbar # Disable the scrollbar in the item list
--history: string # History file
--history-size: string # Maximum number of query history entries to keep
--cmd-history: string # Command history file
--cmd-history-size: string # Maximum number of query history entries to keep
--preview: string # Preview command
--preview-window: string # Preview window layout
--image: string@"nu-complete sk image" # Enable image preview
--query(-q): string # Initial query
--cmd-query: string # Initial query in interactive mode
--read0 # Read input delimited by ASCII NUL(\0) characters
--print0 # Print output delimited by ASCII NUL(\0) characters
--print-query # Print the query as the first line
--print-cmd # Print the command as the first line (after print-query)
--print-score # Print the score after each item
--print-header # Print the header as the first line (after print-score)
--print-current # Print the current (highlighted) item as the first line (after print-header)
--output-format: string # Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands
--no-strip-ansi # Print the ANSI codes, making the output exactly match the input even when --ansi is on
--select-1(-1) # Do not enter the TUI if the query passed in -q matches only one item and return it
--exit-0(-0) # Do not enter the TUI if the query passed in -q does not match any item
--sync # Synchronous search for multi-staged filtering
--pre-select-n: string # Pre-select the first n items in multi-selection mode
--pre-select-pat: string # Pre-select the matched items in multi-selection mode
--pre-select-items: string # Pre-select the items separated by newline character
--pre-select-file: string # Pre-select the items read from this file
--filter(-f): string # Query for filter mode
--shell: string@"nu-complete sk shell" # Generate shell completion script
--shell-bindings # Generate shell key bindings - only for bash, zsh and fish
--man # Generate man page and output it to stdout
--listen: string # Run an IPC socket with optional name (defaults to sk)
--remote: string # Send commands to an IPC socket with optional name (defaults to sk)
--popup: string # Run in a tmux or zellij popup
--log-level: string # Set the log level
--log-file: string # Pipe log output to a file
--flags: string@"nu-complete sk flags" # Feature flags
--extended(-x)
--literal
--hscroll-off: string
--filepath-word
--jump-labels: string
--no-bold
--phony
--tail: string
--style: string
--no-color
--padding: string
--border-label: string
--border-label-pos: string
--wrap-sign: string
--no-multi-line
--raw
--track
--gap: string
--gap-line: string
--freeze-left: string
--freeze-right: string
--scroll-off: string
--gutter: string
--gutter-raw: string
--marker-multi-line: string
--list-border: string
--list-label: string
--list-label-pos: string
--no-input
--info-command: string
--separator: string
--no-separator
--ghost: string
--input-border: string
--input-label: string
--input-label-pos: string
--preview-label: string
--preview-label-pos: string
--header-first
--header-border: string
--header-lines-border: string
--footer: string
--footer-border: string
--footer-label: string
--footer-label-pos: string
--with-shell: string
--expect: string # Deprecated, kept for compatibility purposes. See accept() bind instead
--help(-h) # Print help (see more with '--help')
--version(-V) # Print version
]
}
export use completions *

View file

@ -1,226 +1,329 @@
#compdef sk
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ completion.zsh
#
# - $SKIM_TMUX (default: 0)
# - $SKIM_TMUX_OPTS (default: '-d 40%')
# - $SKIM_COMPLETION_TRIGGER (default: '**')
# - $SKIM_COMPLETION_OPTS (default: empty)
autoload -U is-at-least
_sk() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" : \
'--min-query-length=[Minimum query length to start showing results]:MIN_QUERY_LENGTH:_default' \
'*-t+[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length index -index pathname -pathname)' \
'*--tiebreak=[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length index -index pathname -pathname)' \
'*-n+[Fields to be matched]:NTH:_default' \
'*--nth=[Fields to be matched]:NTH:_default' \
'*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \
'*--hide-nth=[Fields to hide from display while keeping them searchable]:HIDE_NTH:_default' \
'-d+[Delimiter between fields]:DELIMITER:_default' \
'--delimiter=[Delimiter between fields]:DELIMITER:_default' \
'--algo=[Fuzzy matching algorithm]:ALGORITHM:((arinae\:"Arinae\: typo-resistant & natural algorithm, default"
clangd\:"Clangd fuzzy matching algorithm"
fzy\:"Fzy matching algorithm (https\://github.com/jhawthorn/fzy)"
frizbee\:"Frizbee matching algorithm, typo resistant"
skim_v2\:"Previous skim fuzzy matching algorithm (v2)"))' \
'--case=[Case sensitivity]:CASE:((respect\:"Case-sensitive matching"
ignore\:"Case-insensitive matching"
smart\:"Smart case\: case-insensitive unless query contains uppercase"))' \
'--typos=[Enable typo-tolerant matching]::TYPOS:_default' \
'--split-match=[Enable split matching and set delimiter]::SPLIT_MATCH:_default' \
'--scheme=[]:SCHEME:((default\:"Default scheme, no modifications to the options"
path\:"Path scheme\: will find the furthest match in the item and set pathname as the main tiebreak"
history\:"History scheme\: will force index as the first tiebreak"))' \
'*-b+[Comma-separated key, event, and action bindings]::BIND:_default' \
'*--bind=[Comma-separated key, event, and action bindings]::BIND:_default' \
'-c+[Command to invoke dynamically in interactive mode]:CMD:_default' \
'--cmd=[Command to invoke dynamically in interactive mode]:CMD:_default' \
'-I+[Replace replstr with the selected item in commands]:REPLSTR:_default' \
'--color=[Set color theme]:COLOR:_default' \
'--skip-to-pattern=[Show the matched pattern at the line start]:SKIP_TO_PATTERN:_default' \
'--disable-pattern=[Disable items based on this regex pattern]:DISABLE_PATTERN:_default' \
'--layout=[Set layout]:LAYOUT:((default\:"Display from the bottom of the screen"
reverse\:"Display from the top of the screen"
reverse-list\:"Display from the top of the screen, prompt at the bottom"))' \
'--height=[Height of skim'\''s window]:HEIGHT:_default' \
'--min-height=[Minimum height of skim'\''s window as a non-negative row count]:MIN_HEIGHT:_default' \
'--margin=[Screen margin]:MARGIN:_default' \
'-p+[Set prompt]:PROMPT:_default' \
'--prompt=[Set prompt]:PROMPT:_default' \
'--cmd-prompt=[Set prompt in command mode]:CMD_PROMPT:_default' \
'--selector=[Set selected item icon]:SELECTOR_ICON:_default' \
'--multi-selector=[Set multi-selected item icon]:MULTI_SELECT_ICON:_default' \
'--tabstop=[Number of spaces that make up a tab]:TABSTOP:_default' \
'--ellipsis=[The characters used to display truncated lines]:ELLIPSIS:_default' \
'--info=[Set matching result count display position]:INFO:_default' \
'--header=[Set header, displayed next to the info]:HEADER:_default' \
'--header-lines=[Number of lines of the input treated as header]:HEADER_LINES:_default' \
'--border=[Draw borders around the UI components]::BORDER:((force-off\:"ForceOff disables borders around popups too set with no_border"
none\:""
plain\:""
rounded\:""
double\:""
thick\:""
light-double-dashed\:""
heavy-double-dashed\:""
light-triple-dashed\:""
heavy-triple-dashed\:""
light-quadruple-dashed\:""
heavy-quadruple-dashed\:""
quadrant-inside\:""
quadrant-outside\:""))' \
'--multiline=[Split item text into multiple display lines at the given separator character defaults to \\n if read0 is set, and \\\\n if not (matching literal \\n in text)]::MULTILINE:_default' \
'--scrollbar=[Set scrollbar style for the item list]:THUMB:_default' \
'--history=[History file]:HISTORY_FILE:_default' \
'--history-size=[Maximum number of query history entries to keep]:HISTORY_SIZE:_default' \
'--cmd-history=[Command history file]:CMD_HISTORY_FILE:_default' \
'--cmd-history-size=[Maximum number of query history entries to keep]:CMD_HISTORY_SIZE:_default' \
'--preview=[Preview command]:PREVIEW:_default' \
'--preview-window=[Preview window layout]:PREVIEW_WINDOW:_default' \
'--image=[Enable image preview]::IMAGE:((detect\:"Default\: automatically detect the available backend at startup"
halfblocks\:"Force halfblocks if you want blurry previews but a faster startup or if the detection fails"))' \
'-q+[Initial query]:QUERY:_default' \
'--query=[Initial query]:QUERY:_default' \
'--cmd-query=[Initial query in interactive mode]:CMD_QUERY:_default' \
'--output-format=[Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands]:OUTPUT_FORMAT:_default' \
'--pre-select-n=[Pre-select the first n items in multi-selection mode]:PRE_SELECT_N:_default' \
'--pre-select-pat=[Pre-select the matched items in multi-selection mode]:PRE_SELECT_PAT:_default' \
'--pre-select-items=[Pre-select the items separated by newline character]:PRE_SELECT_ITEMS:_default' \
'--pre-select-file=[Pre-select the items read from this file]:PRE_SELECT_FILE:_default' \
'-f+[Query for filter mode]:FILTER:_default' \
'--filter=[Query for filter mode]:FILTER:_default' \
'--shell=[Generate shell completion script]:SHELL:((bash\:"Bourne Again SHell"
elvish\:"Elvish shell"
fish\:"Friendly Interactive SHell"
nushell\:"Nushell (nu)"
power-shell\:"PowerShell"
zsh\:"Zsh"))' \
'--listen=[Run an IPC socket with optional name (defaults to sk)]::LISTEN:_default' \
'--remote=[Send commands to an IPC socket with optional name (defaults to sk)]::REMOTE:_default' \
'--popup=[Run in a tmux or zellij popup]::POPUP:_default' \
'--log-level=[Set the log level]:LOG_LEVEL:_default' \
'--log-file=[Pipe log output to a file]:LOG_FILE:_default' \
'*--flags=[Feature flags]:FLAGS:((no-preview-pty\:"Disable preview PTY on Linux"
show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)"
show-index\:"Display the item'\''s index before its value in the item list"
single-reader\:"Limit the reader thread pool to a single thread"
single-matcher\:"Limit the matcher thread pool to a single thread"))' \
'--hscroll-off=[]:HSCROLL_OFF:_default' \
'--jump-labels=[]:JUMP_LABELS:_default' \
'--tail=[]:TAIL:_default' \
'--style=[]:STYLE:_default' \
'--padding=[]:PADDING:_default' \
'--border-label=[]:BORDER_LABEL:_default' \
'--border-label-pos=[]:BORDER_LABEL_POS:_default' \
'--wrap-sign=[]:WRAP_SIGN:_default' \
'--gap=[]:GAP:_default' \
'--gap-line=[]:GAP_LINE:_default' \
'--freeze-left=[]:FREEZE_LEFT:_default' \
'--freeze-right=[]:FREEZE_RIGHT:_default' \
'--scroll-off=[]:SCROLL_OFF:_default' \
'--gutter=[]:GUTTER:_default' \
'--gutter-raw=[]:GUTTER_RAW:_default' \
'--marker-multi-line=[]:MARKER_MULTI_LINE:_default' \
'--list-border=[]:LIST_BORDER:_default' \
'--list-label=[]:LIST_LABEL:_default' \
'--list-label-pos=[]:LIST_LABEL_POS:_default' \
'--info-command=[]:INFO_COMMAND:_default' \
'--separator=[]:SEPARATOR:_default' \
'--ghost=[]:GHOST:_default' \
'--input-border=[]:INPUT_BORDER:_default' \
'--input-label=[]:INPUT_LABEL:_default' \
'--input-label-pos=[]:INPUT_LABEL_POS:_default' \
'--preview-label=[]:PREVIEW_LABEL:_default' \
'--preview-label-pos=[]:PREVIEW_LABEL_POS:_default' \
'--header-border=[]:HEADER_BORDER:_default' \
'--header-lines-border=[]:HEADER_LINES_BORDER:_default' \
'--footer=[]:FOOTER:_default' \
'--footer-border=[]:FOOTER_BORDER:_default' \
'--footer-label=[]:FOOTER_LABEL:_default' \
'--footer-label-pos=[]:FOOTER_LABEL_POS:_default' \
'--with-shell=[]:WITH_SHELL:_default' \
'--expect=[Deprecated, kept for compatibility purposes. See accept() bind instead]:EXPECT:_default' \
'--tac[Show results in reverse order]' \
'--no-sort[Do not sort the results]' \
'-e[Run in exact mode]' \
'--exact[Run in exact mode]' \
'--regex[Start in regex mode instead of fuzzy-match]' \
'--no-typos[Disable typo-tolerant matching]' \
'--normalize[Normalize unicode characters]' \
'--last-match[Highlight the last match found, not the first one This makes tiebreak more pertinent on path items where we want to prioritize a match on the last parts]' \
'-m[Enable multiple selection]' \
'--multi[Enable multiple selection]' \
'--no-multi[Disable multiple selection]' \
'--no-mouse[Disable mouse]' \
'-i[Start skim in interactive mode]' \
'--interactive[Start skim in interactive mode]' \
'--highlight-line[Highlight the entire current line, not just the text]' \
'--no-hscroll[Disable horizontal scroll]' \
'--keep-right[Keep the right end of the line visible on overflow]' \
'--no-clear-if-empty[Do not clear previous line if the command returns an empty result]' \
'--no-clear-start[Do not clear items on start]' \
'--no-clear[Do not clear screen on exit]' \
'--show-cmd-error[Show error message if command fails]' \
'--cycle[Cycle the results by wrapping around when scrolling]' \
'--disabled[Disable matching entirely]' \
'--reverse[Shorthand for reverse layout]' \
'--no-height[Disable height (force full screen)]' \
'--ansi[Parse ANSI color codes in input strings]' \
'--no-info[Alias for --info=hidden]' \
'--inline-info[Alias for --info=inline]' \
'--border-no-collapse[Do not collapse adjacent borders into a shared row or column]' \
'--no-border[Disables all borders, including in tmux/zellij popups]' \
'--wrap[Wrap items in the item list]' \
'--no-scrollbar[Disable the scrollbar in the item list]' \
'--read0[Read input delimited by ASCII NUL(\\0) characters]' \
'--print0[Print output delimited by ASCII NUL(\\0) characters]' \
'--print-query[Print the query as the first line]' \
'--print-cmd[Print the command as the first line (after print-query)]' \
'--print-score[Print the score after each item]' \
'--print-header[Print the header as the first line (after print-score)]' \
'--print-current[Print the current (highlighted) item as the first line (after print-header)]' \
'--no-strip-ansi[Print the ANSI codes, making the output exactly match the input even when --ansi is on]' \
'-1[Do not enter the TUI if the query passed in -q matches only one item and return it]' \
'--select-1[Do not enter the TUI if the query passed in -q matches only one item and return it]' \
'-0[Do not enter the TUI if the query passed in -q does not match any item]' \
'--exit-0[Do not enter the TUI if the query passed in -q does not match any item]' \
'--sync[Synchronous search for multi-staged filtering]' \
'--shell-bindings[Generate shell key bindings - only for bash, zsh and fish]' \
'--man[Generate man page and output it to stdout]' \
'-x[]' \
'--extended[]' \
'--literal[]' \
'--filepath-word[]' \
'--no-bold[]' \
'--phony[]' \
'--no-color[]' \
'--no-multi-line[]' \
'--raw[]' \
'--track[]' \
'--no-input[]' \
'--no-separator[]' \
'--header-first[]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0
}
(( $+functions[_sk_commands] )) ||
_sk_commands() {
local commands; commands=()
_describe -t commands 'sk commands' commands "$@"
}
if [ "$funcstack[1]" = "_sk" ]; then
_sk "$@"
# Both branches of the following `if` do the same thing -- define
# __skim_completion_options such that `eval $__skim_completion_options` sets
# all options to the same values they currently have. We'll do just that at
# the bottom of the file after changing options to what we prefer.
#
# IMPORTANT: Until we get to the `emulate` line, all words that *can* be quoted
# *must* be quoted in order to prevent alias expansion. In addition, code must
# be written in a way works with any set of zsh options. This is very tricky, so
# careful when you change it.
#
# Start by loading the builtin zsh/parameter module. It provides `options`
# associative array that stores current shell options.
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
# This is the fast branch and it gets taken on virtually all Zsh installations.
#
# ${(kv)options[@]} expands to array of keys (option names) and values ("on"
# or "off"). The subsequent expansion# with (j: :) flag joins all elements
# together separated by spaces. __skim_completion_options ends up with a value
# like this: "options=(shwordsplit off aliases on ...)".
__skim_completion_options="options=(${(j: :)${(kv)options[@]}})"
else
compdef _sk sk
# This branch is much slower because it forks to get the names of all
# zsh options. It's possible to eliminate this fork but it's not worth the
# trouble because this branch gets taken only on very ancient or broken
# zsh installations.
() {
# That `()` above defines an anonymous function. This is essentially a scope
# for local parameters. We use it to avoid polluting global scope.
'local' '__skim_opt'
__skim_completion_options="setopt"
# `set -o` prints one line for every zsh option. Each line contains option
# name, some spaces, and then either "on" or "off". We just want option names.
# Expansion with (@f) flag splits a string into lines. The outer expansion
# removes spaces and everything that follow them on every line. __skim_opt
# ends up iterating over option names: shwordsplit, aliases, etc.
for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do
if [[ -o "$__skim_opt" ]]; then
# Option $__skim_opt is currently on, so remember to set it back on.
__skim_completion_options+=" -o $__skim_opt"
else
# Option $__skim_opt is currently off, so remember to set it back off.
__skim_completion_options+=" +o $__skim_opt"
fi
done
# The value of __skim_completion_options here looks like this:
# "setopt +o shwordsplit -o aliases ..."
}
fi
# Enable the default zsh options (those marked with <Z> in `man zshoptions`)
# but without `aliases`. Aliases in functions are expanded when functions are
# defined, so if we disable aliases here, we'll be sure to have no pesky
# aliases in any of our functions. This way we won't need prefix every
# command with `command` or to quote every word to defend against global
# aliases. Note that `aliases` is not the only option that's important to
# control. There are several others that could wreck havoc if they are set
# to values we don't expect. With the following `emulate` command we
# sidestep this issue entirely.
'emulate' 'zsh' '-o' 'no_aliases'
# This brace is the start of try-always block. The `always` part is like
# `finally` in lesser languages. We use it to *always* restore user options.
{
# Bail out if not interactive shell.
[[ -o interactive ]] || return 0
# To use custom commands instead of find, override _skim_compgen_{path,dir}
if ! declare -f _skim_compgen_path > /dev/null; then
_skim_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
if ! declare -f _skim_compgen_dir > /dev/null; then
_skim_compgen_dir() {
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
###########################################################
__skim_comprun() {
if [[ "$(type _skim_comprun 2>&1)" =~ function ]]; then
_skim_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
shift
if [ -n "$SKIM_TMUX_OPTS" ]; then
sk-tmux ${(Q)${(Z+n+)SKIM_TMUX_OPTS}} -- "$@"
else
sk-tmux -d ${SKIM_TMUX_HEIGHT:-40%} -- "$@"
fi
else
shift
sk "$@"
fi
}
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
__skim_extract_command() {
local token tokens
tokens=(${(z)1})
for token in $tokens; do
token=${(Q)token}
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
echo "$token"
return
fi
done
echo "${tokens[1]}"
}
__skim_generic_path_completion() {
local base lbuf cmd compgen skim_opts suffix tail dir leftover matches
base=$1
lbuf=$2
cmd=$(__skim_extract_command "$lbuf")
compgen=$3
skim_opts=$4
suffix=$5
tail=$6
setopt localoptions nonomatch
eval "base=$base"
[[ $base = *"/"* ]] && dir="$base"
while [ 1 ]; do
if [[ -z "$dir" || -d ${dir} ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$compgen $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS" __skim_comprun "$cmd" ${(Q)${(Z+n+)skim_opts}} -q "$leftover" | while read item; do
echo -n "${(q)item}$suffix "
done)
matches=${matches% }
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail"
fi
zle reset-prompt
break
fi
dir=$(dirname "$dir")
dir=${dir%/}/
done
}
_skim_path_completion() {
__skim_generic_path_completion "$1" "$2" _skim_compgen_path \
"-m" "" " "
}
_skim_dir_completion() {
__skim_generic_path_completion "$1" "$2" _skim_compgen_dir \
"" "/" ""
}
_skim_feed_fifo() (
command rm -f "$1"
mkfifo "$1"
cat <&0 > "$1" &
)
_skim_complete() {
setopt localoptions ksh_arrays
# Split arguments around --
local args rest str_arg i sep
args=("$@")
sep=
for i in {0..${#args[@]}}; do
if [[ "${args[$i]}" = -- ]]; then
sep=$i
break
fi
done
if [[ -n "$sep" ]]; then
str_arg=
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
args=("${args[@]:0:$sep}")
else
str_arg=$1
args=()
shift
rest=("$@")
fi
local fifo lbuf cmd matches post
fifo="${TMPDIR:-/tmp}/skim-complete-fifo-$$"
lbuf=${rest[0]}
cmd=$(__skim_extract_command "$lbuf")
post="${funcstack[1]}_post"
type $post > /dev/null 2>&1 || post=cat
_skim_feed_fifo "$fifo"
matches=$(SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches"
fi
zle reset-prompt
command rm -f "$fifo"
}
_skim_complete_telnet() {
_skim_complete --no-multi -- "$@" < <(
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_skim_complete_ssh() {
_skim_complete --no-multi -- "$@" < <(
setopt localoptions nonomatch
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_skim_complete_export() {
_skim_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_skim_complete_unset() {
_skim_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_skim_complete_unalias() {
_skim_complete --no-multi -- "$@" < <(
alias | sed 's/=.*//'
)
}
_skim_complete_kill() {
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d
)
}
_skim_complete_kill_post() {
awk '{print $2}'
}
skim-completion() {
local tokens cmd prefix trigger tail matches lbuf d_cmds
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
tokens=(${(z)LBUFFER})
if [ ${#tokens} -lt 1 ]; then
zle ${skim_default_completion:-expand-or-complete}
return
fi
cmd=$(__skim_extract_command "$LBUFFER")
# Explicitly allow for empty trigger.
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
tokens[-2]="${tokens[-2]}${tokens[-1]}"
tokens=(${tokens[0,-2]})
fi
lbuf=$LBUFFER
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
tail=$trigger
tokens+=$trigger
lbuf="$lbuf$trigger"
fi
# Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _skim_complete_${cmd} > /dev/null"; then
prefix="$prefix" eval _skim_complete_${cmd} ${(q)lbuf}
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_skim_dir_completion "$prefix" "$lbuf"
else
_skim_path_completion "$prefix" "$lbuf"
fi
# Fall back to default completion
else
zle ${skim_default_completion:-expand-or-complete}
fi
}
[ -z "$skim_default_completion" ] && {
binding=$(bindkey '^I')
[[ $binding =~ 'undefined-key' ]] || skim_default_completion=$binding[(s: :w)2]
unset binding
}
zle -N skim-completion
bindkey '^I' skim-completion
} always {
# Restore the original options.
eval $__skim_completion_options
'unset' '__skim_completion_options'
}

View file

@ -1,400 +1,97 @@
# skim key bindings for bash
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ key-bindings.bash
#
# - $SKIM_TMUX_OPTS
# - $SKIM_CTRL_T_COMMAND
# - $SKIM_CTRL_T_OPTS
# - $SKIM_CTRL_R_OPTS
# - $SKIM_CTRL_R_IDX_COLOR (default: \033[2m, set NO_COLOR to disable)
# - $SKIM_ALT_C_COMMAND
# - $SKIM_ALT_C_OPTS
# - $SKIM_COMPLETION_TRIGGER (default: '**')
# - $SKIM_COMPLETION_OPTS (default: empty)
# Key bindings
# ------------
# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/key-bindings.bash
__skim_select__() {
local cmd="${SKIM_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
local cmd="${SKIM_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}"
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read -r item; do
printf '%q ' "$item"
done
echo
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read -r item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
__skimcmd() {
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
echo "sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}}" || echo "sk"
}
__skimcmd() {
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
echo "sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- " || echo "sk"
}
skim-file-widget() {
local selected="$(__skim_select__)"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$((READLINE_POINT + ${#selected}))
}
skim-file-widget() {
local selected="$(__skim_select__)"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
}
__skim_cd__() {
local cmd dir
cmd="${SKIM_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -printf '%P\\n' 2>/dev/null"}"
dir=$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)
if [ -n "$dir" ]; then
printf 'cd %q' "$dir"
fi
}
__skim_cd__() {
local cmd dir
cmd="${SKIM_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | cut -b3-"}"
dir=$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi) && printf 'cd %q' "$dir"
}
__skim_history__() {
local output
local c_idx='' c_reset='' ansi_opt=''
if [[ ! -v NO_COLOR ]]; then
c_idx="${SKIM_CTRL_R_IDX_COLOR:-\033[2m}"
c_reset='\033[0m'
ansi_opt='--ansi'
fi
output=$(
builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) awk -v last_hist="$last_hist" -v c_idx="$c_idx" -v c_reset="$c_reset" '
BEGIN { HISTCMD = last_hist + 1; cmd = ""; idx = 0 }
/^\t/ {
if (cmd != "" && !seen[cmd]++) printf "%s%d%s\t%s%c", c_idx, HISTCMD - idx, c_reset, cmd, 0
idx++; cmd = substr($0, 2); sub(/^[ *]/, "", cmd); next
}
{ cmd = cmd "\n" $0 }
END { if (cmd != "" && !seen[cmd]++) printf "%s%d%s\t%s%c", c_idx, HISTCMD - idx, c_reset, cmd, 0 }
' |
SKIM_DEFAULT_OPTIONS="$SKIM_DEFAULT_OPTIONS -n2..,.. --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi --read0 --multiline $ansi_opt" $(__skimcmd) --query "$READLINE_LINE"
) || return
echo -e "\033[0m"
READLINE_LINE=${output#*$'\t'}
if [ -z "$READLINE_POINT" ]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
__skim_history__() {
local output
output=$(
builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' |
SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} $SKIM_DEFAULT_OPTIONS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi --read0" $(__skimcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [ -z "$READLINE_POINT" ]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
# Required to refresh the prompt after skim
bind -m emacs-standard '"\er": redraw-current-line'
# Required to refresh the prompt after skim
bind -m emacs-standard '"\er": redraw-current-line'
bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode'
bind -m emacs-standard '"\C-z": vi-editing-mode'
bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode'
bind -m emacs-standard '"\C-z": vi-editing-mode'
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
# CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__skim_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
# CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__skim_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
# CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__skim_history__)"\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else
# CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard -x '"\C-t": skim-file-widget'
bind -m vi-command -x '"\C-t": skim-file-widget'
bind -m vi-insert -x '"\C-t": skim-file-widget'
# CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__skim_history__)"\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else
# CTRL-T - Paste the selected file path into the command line
bind -m emacs-standard -x '"\C-t": skim-file-widget'
bind -m vi-command -x '"\C-t": skim-file-widget'
bind -m vi-insert -x '"\C-t": skim-file-widget'
# CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard -x '"\C-r": __skim_history__'
bind -m vi-command -x '"\C-r": __skim_history__'
bind -m vi-insert -x '"\C-r": __skim_history__'
fi
# CTRL-R - Paste the selected command from history into the command line
bind -m emacs-standard -x '"\C-r": __skim_history__'
bind -m vi-command -x '"\C-r": __skim_history__'
bind -m vi-insert -x '"\C-r": __skim_history__'
fi
# ALT-C - cd into the selected directory
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__skim_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
# Completion
if ! declare -f _skim_compgen_path >/dev/null; then
_skim_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2>/dev/null | sed 's@^\./@@'
}
fi
if ! declare -f _skim_compgen_dir >/dev/null; then
_skim_compgen_dir() {
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2>/dev/null | sed 's@^\./@@'
}
fi
###########################################################
__skim_comprun() {
if [ "$(type -t _skim_comprun 2>&1)" = function ]; then
_skim_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
shift
sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}} "$@"
else
shift
sk "$@"
fi
}
__skim_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _skim_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__skim_nospace_commands" = *" \3 "* ]] \&\& __skim_nospace_commands="$__skim_nospace_commands \3 ";/' |
awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1'
}
_skim_handle_dynamic_completion() {
local cmd orig_var orig ret orig_cmd orig_complete
cmd="$1"
shift
orig_cmd="$1"
orig_var="_skim_orig_completion_$cmd"
orig="${!orig_var##*#}"
if [ -n "$orig" ] && type "$orig" >/dev/null 2>&1; then
$orig "$@"
elif [ -n "$_skim_completion_loader" ]; then
orig_complete=$(complete -p "$orig_cmd" 2>/dev/null)
_completion_loader "$@"
ret=$?
# _completion_loader may not have updated completion for the command
if [ "$(complete -p "$orig_cmd" 2>/dev/null)" != "$orig_complete" ]; then
eval "$(complete | command grep " -F.* $orig_cmd$" | __skim_orig_completion_filter)"
if [[ "$__skim_nospace_commands" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }"
else
eval "$orig_complete"
fi
fi
return $ret
fi
}
__skim_generic_path_completion() {
local cur base dir leftover matches trigger cmd
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=()
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}}
eval "base=$base"
[[ $base = *"/"* ]] && dir="$base"
while true; do
if [ -z "$dir" ] || [ -d "$dir" ]; then
leftover=${base/#"$dir"/}
leftover=${leftover/#\//}
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $2" __skim_comprun "$4" -q "$leftover" | while read -r item; do
printf "%q$3 " "$item"
done)
matches=${matches% }
[[ -z "$3" ]] && [[ "$__skim_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [ -n "$matches" ]; then
COMPREPLY=("$matches")
else
COMPREPLY=("$cur")
fi
# To redraw line after skim closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
printf '\e[5n'
return 0
fi
dir=$(dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/
done
else
shift
shift
shift
_skim_handle_dynamic_completion "$cmd" "$@"
fi
}
_skim_complete() {
# Split arguments around --
local args rest str_arg i sep
args=("$@")
sep=
for i in "${!args[@]}"; do
if [[ "${args[$i]}" = -- ]]; then
sep=$i
break
fi
done
if [[ -n "$sep" ]]; then
str_arg=
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
args=("${args[@]:0:$sep}")
else
str_arg=$1
args=()
shift
rest=("$@")
fi
local cur selected trigger cmd post
post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" >/dev/null 2>&1 || post=cat
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
selected=$(SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [ -n "$selected" ]; then
COMPREPLY=("$selected")
else
COMPREPLY=("$cur")
fi
# To redraw line after skim closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
printf '\e[5n'
echo -e "\033[0m"
return 0
else
_skim_handle_dynamic_completion "$cmd" "${rest[@]}"
fi
}
_skim_path_completion() {
__skim_generic_path_completion _skim_compgen_path "-m" "" "$@"
}
# Deprecated. No file only completion.
_skim_file_completion() {
_skim_path_completion "$@"
}
_skim_dir_completion() {
__skim_generic_path_completion _skim_compgen_dir "" "/" "$@"
}
_skim_complete_kill() {
local trigger=${SKIM_COMPLETION_TRIGGER-'**'}
local cur="${COMP_WORDS[COMP_CWORD]}"
if [[ -z "$cur" ]]; then
COMP_WORDS[$COMP_CWORD]=$trigger
elif [[ "$cur" != *"$trigger" ]]; then
return 1
fi
_skim_proc_completion "$@"
}
_skim_proc_completion() {
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d
)
}
_skim_proc_completion_post() {
awk '{print $2}'
}
_skim_host_completion() {
_skim_complete --no-multi -- "$@" < <(
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2>/dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -soE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_skim_var_completion() {
_skim_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_skim_alias_completion() {
_skim_complete -m -- "$@" < <(
alias | sed 's/=.*//' | sed 's/.* //'
)
}
d_cmds="${SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
awk cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg java
javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open
basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp
svn tar unzip zip"
# Preserve existing completion
eval "$(complete |
sed -E '/-F/!d; / _skim/d; '"/ ($(echo $d_cmds $a_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
__skim_orig_completion_filter)"
if type _completion_loader >/dev/null 2>&1; then
_skim_completion_loader=1
fi
__skim_defc() {
local cmd func opts orig_var orig def
cmd="$1"
func="$2"
opts="$3"
orig_var="_skim_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}"
if [ -n "$orig" ]; then
printf -v def "$orig" "$func"
eval "$def"
else
complete -F "$func" $opts "$cmd"
fi
}
# Anything
for cmd in $a_cmds; do
__skim_defc "$cmd" _skim_path_completion "-o default -o bashdefault"
done
# Directory
for cmd in $d_cmds; do
__skim_defc "$cmd" _skim_dir_completion "-o nospace -o dirnames"
done
# Kill completion (supports empty completion trigger)
complete -F _skim_complete_kill -o default -o bashdefault kill
unset cmd d_cmds a_cmds
_skim_setup_completion() {
local kind fn cmd
kind=$1
fn=_skim_${1}_completion
if [[ $# -lt 2 ]] || ! type -t "$fn" >/dev/null; then
echo "usage: ${FUNCNAME[0]} path|dir|var|alias|host|proc COMMANDS..."
return 1
fi
shift
eval "$(complete -p "$@" 2>/dev/null | grep -v "$fn" | __skim_orig_completion_filter)"
for cmd in "$@"; do
case "$kind" in
dir) __skim_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
var) __skim_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
alias) __skim_defc "$cmd" "$fn" "-a" ;;
*) __skim_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
esac
done
}
# Environment variables / Aliases / Hosts
_skim_setup_completion 'var' export unset
_skim_setup_completion 'alias' unalias
_skim_setup_completion 'host' ssh telnet
# ALT-C - cd into the selected directory
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__skim_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi

View file

@ -1,5 +1,11 @@
#!/bin/fish
# skim key bindings for fish
# completion.fish
# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/key-bindings.fish
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ key-bindings.fish
#
# - $SKIM_TMUX_OPTS
# - $SKIM_CTRL_T_COMMAND
@ -7,8 +13,6 @@
# - $SKIM_CTRL_R_OPTS
# - $SKIM_ALT_C_COMMAND
# - $SKIM_ALT_C_OPTS
# - $SKIM_COMPLETION_TRIGGER (default: '**')
# - $SKIM_COMPLETION_OPTS (default: empty)
# Key bindings
# ------------
@ -28,8 +32,9 @@ function skim_key_bindings
-o -type d -print \
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
begin
set -lx SKIM_DEFAULT_OPTIONS "--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS"
set -lx SKIM_DEFAULT_OPTIONS "--height $SKIM_TMUX_HEIGHT --reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS"
eval "$SKIM_CTRL_T_COMMAND | "(__skimcmd)' -m --query "'$skim_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
@ -47,8 +52,9 @@ function skim_key_bindings
end
function skim-history-widget -d "Show command history"
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
begin
set -lx SKIM_DEFAULT_OPTIONS "$SKIM_DEFAULT_OPTIONS --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi"
set -lx SKIM_DEFAULT_OPTIONS "--height $SKIM_TMUX_HEIGHT $SKIM_DEFAULT_OPTIONS --tiebreak=index --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi"
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.)
@ -57,7 +63,7 @@ function skim_key_bindings
# history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__skimcmd) --read0 --multiline --print0 -q '(commandline)' | read -lz result
history -z | eval (__skimcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
else
history | eval (__skimcmd) -q '(commandline)' | read -l result
@ -75,8 +81,9 @@ function skim_key_bindings
test -n "$SKIM_ALT_C_COMMAND"; or set -l SKIM_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
begin
set -lx SKIM_DEFAULT_OPTIONS "--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS"
set -lx SKIM_DEFAULT_OPTIONS "--height $SKIM_TMUX_HEIGHT --reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS"
eval "$SKIM_ALT_C_COMMAND | "(__skimcmd)' --no-multi --query "'$skim_query'"' | read -l result
if [ -n "$result" ]
@ -94,9 +101,9 @@ function skim_key_bindings
test -n "$SKIM_TMUX"; or set SKIM_TMUX 0
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
if [ -n "$SKIM_TMUX_OPTS" ]
echo "sk --tmux=$SKIM_TMUX_OPTS "
echo "sk-tmux $SKIM_TMUX_OPTS -- "
else if [ $SKIM_TMUX -eq 1 ]
echo "sk --tmux=center,$SKIM_TMUX_HEIGHT"
echo "sk-tmux -d$SKIM_TMUX_HEIGHT -- "
else
echo "sk"
end
@ -153,4 +160,5 @@ function skim_key_bindings
echo $dir
end
end

View file

@ -1,22 +1,23 @@
# skim key bindings for zsh
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/ key-bindings.zsh
#
# - $SKIM_TMUX_OPTS
# - $SKIM_CTRL_T_COMMAND
# - $SKIM_CTRL_T_OPTS
# - $SKIM_CTRL_R_OPTS
# - $SKIM_CTRL_R_IDX_COLOR (default: \033[2m, set NO_COLOR to disable)
# - $SKIM_CTRL_R_DATE_COLOR (default: \033[32m, set NO_COLOR to disable)
# - $SKIM_ALT_C_COMMAND
# - $SKIM_ALT_C_OPTS
# - $SKIM_COMPLETION_TRIGGER (default: '**')
# - $SKIM_COMPLETION_OPTS (default: empty)
# Key bindings
# ------------
# The code at the top and the bottom of this file is the same as in completion.zsh.
# Refer to that file for explanation.
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
__skim_key_bindings_options="options=(${(j: :)${(kv)options[@]}})"
__skim_completion_options="options=(${(j: :)${(kv)options[@]}})"
else
() {
__skim_key_bindings_options="setopt"
@ -29,17 +30,6 @@ else
fi
done
}
() {
'local' '__skim_opt'
__skim_completion_options="setopt"
for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do
if [[ -o "$__skim_opt" ]]; then
__skim_completion_options+=" -o $__skim_opt"
else
__skim_completion_options+=" +o $__skim_opt"
fi
done
}
fi
'emulate' 'zsh' '-o' 'no_aliases'
@ -57,7 +47,7 @@ __fsel() {
setopt localoptions pipefail no_aliases 2> /dev/null
REPORTTIME_=$REPORTTIME
unset REPORTTIME
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read item; do
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read item; do
echo -n "${(q)item} "
done
local ret=$?
@ -71,7 +61,7 @@ __fsel() {
__skimcmd() {
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
echo "sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}}" || echo "sk"
echo "sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- " || echo "sk"
}
skim-file-widget() {
@ -100,7 +90,7 @@ skim-cd-widget() {
setopt localoptions pipefail no_aliases 2> /dev/null
REPORTTIME_=$REPORTTIME
unset REPORTTIME
local dir="$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)"
local dir="$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)"
if ! [ -z $REPORTTIME_ ]; then
REPORTTIME=$REPORTTIME_
fi
@ -119,7 +109,6 @@ skim-cd-widget() {
local ret=$?
unset dir # ensure this doesn't end up appearing in prompt expansion
zle skim-redraw-prompt
tput cnorm
return $ret
}
zle -N skim-cd-widget
@ -129,35 +118,8 @@ bindkey '\ec' skim-cd-widget
skim-history-widget() {
local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
local c_idx='' c_date='' c_reset='' ansi_opt=''
if [[ ! -v NO_COLOR ]]; then
c_idx="${SKIM_CTRL_R_IDX_COLOR:-\033[2m}"
c_date="${SKIM_CTRL_R_DATE_COLOR:-\033[32m}"
c_reset='\033[0m'
ansi_opt='--ansi'
fi
local awk_filter='{ $1=$1; cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) { idx=$1; sub(idx, c_idx idx c_reset); print } }'
local n=2 fc_opts=''
if [[ -o extended_history ]]; then
local today=$(date +%Y-%m-%d)
# For today's commands, replace date ($2) with "today", otherwise remove time ($3).
# And filter out duplicates.
awk_filter='{
$1=$1;
cmd = $0; sub(/^[ \t]*[0-9]+\**[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+/, "", cmd)
if (!seen[cmd]++) {
time = $3; date = $2; idx = $1
if (date == today) sub(date " " time " ", c_date "today@" time c_reset "\t")
else sub(date " " time " ", c_date date c_reset "\t")
sub(idx, c_idx idx c_reset)
print
}
}'
fc_opts='-i'
n=3
fi
selected=( $(fc -rl $fc_opts 1 | awk -v c_idx="$c_idx" -v c_date="$c_date" -v c_reset="$c_reset" -v today="$today" "$awk_filter" | sed 's/\\n/\\n\t/g' |
SKIM_DEFAULT_OPTIONS="$SKIM_DEFAULT_OPTIONS -n$n..,.. --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --query=${(qqq)LBUFFER} --no-multi $ansi_opt --tabstop=20 --multiline" $(__skimcmd)) )
selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' |
SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} $SKIM_DEFAULT_OPTIONS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --query=${(qqq)LBUFFER} --no-multi" $(__skimcmd)) )
local ret=$?
if [ -n "$selected" ]; then
num=$selected[1]
@ -166,254 +128,12 @@ skim-history-widget() {
fi
fi
zle reset-prompt
tput cnorm
return $ret
}
zle -N skim-history-widget
bindkey '^R' skim-history-widget
# Completion
# To use custom commands instead of find, override _skim_compgen_{path,dir}
if ! declare -f _skim_compgen_path > /dev/null; then
_skim_compgen_path() {
echo "$1"
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
if ! declare -f _skim_compgen_dir > /dev/null; then
_skim_compgen_dir() {
command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
}
fi
###########################################################
__skim_comprun() {
if [[ "$(type _skim_comprun 2>&1)" =~ function ]]; then
_skim_comprun "$@"
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
shift
if [ -n "$SKIM_TMUX_OPTS" ]; then
sk --tmux=${(Q)${(Z+n+)SKIM_TMUX_OPTS}} "$@"
else
sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}} "$@"
fi
else
shift
sk "$@"
fi
}
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
__skim_extract_command() {
local token tokens
tokens=(${(z)1})
for token in $tokens; do
token=${(Q)token}
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
echo "$token"
return
fi
done
echo "${tokens[1]}"
}
__skim_generic_path_completion() {
local base lbuf cmd compgen skim_opts suffix tail dir leftover matches
base=$1
lbuf=$2
cmd=$(__skim_extract_command "$lbuf")
compgen=$3
skim_opts=$4
suffix=$5
tail=$6
setopt localoptions nonomatch
eval "base=$base"
[[ $base = *"/"* ]] && dir="$base"
while [ 1 ]; do
if [[ -z "$dir" || -d ${dir} ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$compgen $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS" __skim_comprun "$cmd" ${(Q)${(Z+n+)skim_opts}} -q "$leftover" | while read item; do
echo -n "${(q)item}$suffix "
done)
matches=${matches% }
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail"
fi
zle reset-prompt
break
fi
dir=$(dirname "$dir")
dir=${dir%/}/
done
}
_skim_path_completion() {
__skim_generic_path_completion "$1" "$2" _skim_compgen_path \
"-m" "" " "
}
_skim_dir_completion() {
__skim_generic_path_completion "$1" "$2" _skim_compgen_dir \
"" "/" ""
}
_skim_complete() {
setopt localoptions ksh_arrays
# Split arguments around --
local args rest str_arg i sep
args=("$@")
sep=
for i in {0..${#args[@]}}; do
if [[ "${args[$i]}" = -- ]]; then
sep=$i
break
fi
done
if [[ -n "$sep" ]]; then
str_arg=
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
args=("${args[@]:0:$sep}")
else
str_arg=$1
args=()
shift
rest=("$@")
fi
local lbuf cmd matches post
lbuf=${rest[0]}
cmd=$(__skim_extract_command "$lbuf")
post="${funcstack[1]}_post"
type $post > /dev/null 2>&1 || post=cat
matches=$(SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches"
fi
zle reset-prompt
}
_skim_complete_telnet() {
_skim_complete --no-multi -- "$@" < <(
command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_skim_complete_ssh() {
_skim_complete --no-multi -- "$@" < <(
setopt localoptions nonomatch
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
<(command grep -osE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_skim_complete_export() {
_skim_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_skim_complete_unset() {
_skim_complete -m -- "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_skim_complete_unalias() {
_skim_complete --no-multi -- "$@" < <(
alias | sed 's/=.*//'
)
}
_skim_complete_kill() {
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -ef | sed 1d
)
}
_skim_complete_kill_post() {
awk '{print $2}'
}
skim-completion() {
local tokens cmd prefix trigger tail matches lbuf d_cmds
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
tokens=(${(z)LBUFFER})
if [ ${#tokens} -lt 1 ]; then
zle ${skim_default_completion:-expand-or-complete}
return
fi
cmd=$(__skim_extract_command "$LBUFFER")
# Explicitly allow for empty trigger.
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
tokens[-2]="${tokens[-2]}${tokens[-1]}"
tokens=(${tokens[0,-2]})
fi
lbuf=$LBUFFER
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
tail=$trigger
tokens+=$trigger
lbuf="$lbuf$trigger"
fi
# Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _skim_complete_${cmd} > /dev/null"; then
prefix="$prefix" eval _skim_complete_${cmd} ${(q)lbuf}
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_skim_dir_completion "$prefix" "$lbuf"
else
_skim_path_completion "$prefix" "$lbuf"
fi
tput cnorm
# Fall back to default completion
else
zle ${skim_default_completion:-expand-or-complete}
fi
}
[ -z "$skim_default_completion" ] && {
binding=$(bindkey '^I')
[[ $binding =~ 'undefined-key' ]] || skim_default_completion=$binding[(s: :w)2]
unset binding
}
zle -N skim-completion
bindkey '^I' skim-completion
} always {
eval $__skim_key_bindings_options
'unset' '__skim_key_bindings_options'
eval $__skim_completion_options
'unset' '__skim_completion_options'
}

View file

@ -1 +0,0 @@
5.7.0

609
src/ansi.rs Normal file
View file

@ -0,0 +1,609 @@
// Parse ANSI attr code
use std::default::Default;
use beef::lean::Cow;
use std::cmp::max;
use tuikit::prelude::*;
use vte::{Params, Perform};
/// An ANSI Parser, will parse one line at a time.
///
/// It will cache the latest attribute used, that means if an attribute affect multiple
/// lines, the parser will recognize it.
#[derive(Debug, Default)]
pub struct ANSIParser {
partial_str: String,
last_attr: Attr,
stripped: String,
stripped_char_count: usize,
fragments: Vec<(Attr, (u32, u32))>, // [char_index_start, char_index_end)
}
impl Perform for ANSIParser {
fn print(&mut self, ch: char) {
self.partial_str.push(ch);
}
fn execute(&mut self, byte: u8) {
match byte {
// \b to delete character back
0x08 => {
self.partial_str.pop();
}
// put back \0 \r \n \t
0x00 | 0x0d | 0x0A | 0x09 => self.partial_str.push(byte as char),
// ignore all others
_ => trace!("AnsiParser:execute ignored {:?}", byte),
}
}
fn hook(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {
trace!("AnsiParser:hook ignored {:?}", params);
}
fn put(&mut self, byte: u8) {
trace!("AnsiParser:put ignored {:?}", byte);
}
fn unhook(&mut self) {
trace!("AnsiParser:unhook ignored");
}
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
trace!("AnsiParser:osc ignored {:?}", params);
}
fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, action: char) {
// https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
// Only care about graphic modes, ignore all others
if action != 'm' {
trace!("ignore: params: {:?}, action : {:?}", params, action);
return;
}
// \[[m => means reset
let mut attr = if params.is_empty() {
Attr::default()
} else {
self.last_attr
};
let mut iter = params.iter();
while let Some(code) = iter.next() {
match code[0] {
0 => attr = Attr::default(),
1 => attr.effect |= Effect::BOLD,
2 => attr.effect |= !Effect::BOLD,
4 => attr.effect |= Effect::UNDERLINE,
5 => attr.effect |= Effect::BLINK,
7 => attr.effect |= Effect::REVERSE,
num @ 30..=37 => attr.fg = Color::AnsiValue((num - 30) as u8),
38 => match iter.next() {
Some(&[2]) => {
// ESC[ 38;2;<r>;<g>;<b> m Select RGB foreground color
let (r, g, b) = match (iter.next(), iter.next(), iter.next()) {
(Some(r), Some(g), Some(b)) => (r[0] as u8, g[0] as u8, b[0] as u8),
_ => {
trace!("ignore CSI {:?} m", params);
continue;
}
};
attr.fg = Color::Rgb(r, g, b);
}
Some(&[5]) => {
// ESC[ 38;5;<n> m Select foreground color
let color = match iter.next() {
Some(color) => color[0] as u8,
None => {
trace!("ignore CSI {:?} m", params);
continue;
}
};
attr.fg = Color::AnsiValue(color);
}
_ => {
trace!("error on parsing CSI {:?} m", params);
}
},
39 => attr.fg = Color::Default,
num @ 40..=47 => attr.bg = Color::AnsiValue((num - 40) as u8),
48 => match iter.next() {
Some(&[2]) => {
// ESC[ 48;2;<r>;<g>;<b> m Select RGB background color
let (r, g, b) = match (iter.next(), iter.next(), iter.next()) {
(Some(r), Some(g), Some(b)) => (r[0] as u8, g[0] as u8, b[0] as u8),
_ => {
trace!("ignore CSI {:?} m", params);
continue;
}
};
attr.bg = Color::Rgb(r, g, b);
}
Some(&[5]) => {
// ESC[ 48;5;<n> m Select background color
let color = match iter.next() {
Some(color) => color[0] as u8,
None => {
trace!("ignore CSI {:?} m", params);
continue;
}
};
attr.bg = Color::AnsiValue(color);
}
_ => {
trace!("ignore CSI {:?} m", params);
}
},
49 => attr.bg = Color::Default,
num @ 90..=97 => attr.fg = Color::AnsiValue((num - 82) as u8),
num @ 100..=107 => attr.bg = Color::AnsiValue((num - 92) as u8),
_ => {
trace!("ignore CSI {:?} m", params);
}
}
}
self.attr_change(attr);
}
fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {
// ESC characters are replaced with \[
self.partial_str.push('"');
self.partial_str.push('[');
}
}
impl ANSIParser {
/// save the partial_str into fragments with current attr
fn save_str(&mut self) {
if self.partial_str.is_empty() {
return;
}
let string = std::mem::take(&mut self.partial_str);
let string_char_count = string.chars().count();
self.fragments.push((
self.last_attr,
(
self.stripped_char_count as u32,
(self.stripped_char_count + string_char_count) as u32,
),
));
self.stripped_char_count += string_char_count;
self.stripped.push_str(&string);
}
// accept a new attr
fn attr_change(&mut self, new_attr: Attr) {
if new_attr == self.last_attr {
return;
}
self.save_str();
self.last_attr = new_attr;
}
pub fn parse_ansi(&mut self, text: &str) -> AnsiString<'static> {
let mut statemachine = vte::Parser::new();
for byte in text.as_bytes() {
statemachine.advance(self, *byte);
}
self.save_str();
let stripped = std::mem::take(&mut self.stripped);
self.stripped_char_count = 0;
let fragments = std::mem::take(&mut self.fragments);
AnsiString::new_string(stripped, fragments)
}
}
/// A String that contains ANSI state (e.g. colors)
///
/// It is internally represented as Vec<(attr, string)>
#[derive(Clone, Debug)]
pub struct AnsiString<'a> {
stripped: Cow<'a, str>,
// attr: start, end
fragments: Option<Vec<(Attr, (u32, u32))>>,
}
impl<'a> AnsiString<'a> {
pub fn new_empty() -> Self {
Self {
stripped: Cow::borrowed(""),
fragments: None,
}
}
fn new_raw_string(string: String) -> Self {
Self {
stripped: Cow::owned(string),
fragments: None,
}
}
fn new_raw_str(str_ref: &'a str) -> Self {
Self {
stripped: Cow::borrowed(str_ref),
fragments: None,
}
}
/// assume the fragments are ordered by (start, end) while end is exclusive
pub fn new_str(stripped: &'a str, fragments: Vec<(Attr, (u32, u32))>) -> Self {
let fragments_empty = fragments.is_empty() || (fragments.len() == 1 && fragments[0].0 == Attr::default());
Self {
stripped: Cow::borrowed(stripped),
fragments: if fragments_empty { None } else { Some(fragments) },
}
}
/// assume the fragments are ordered by (start, end) while end is exclusive
pub fn new_string(stripped: String, fragments: Vec<(Attr, (u32, u32))>) -> Self {
let fragments_empty = fragments.is_empty() || (fragments.len() == 1 && fragments[0].0 == Attr::default());
Self {
stripped: Cow::owned(stripped),
fragments: if fragments_empty { None } else { Some(fragments) },
}
}
pub fn parse(raw: &'a str) -> AnsiString<'static> {
ANSIParser::default().parse_ansi(raw)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.stripped.is_empty()
}
#[inline]
pub fn into_inner(self) -> std::borrow::Cow<'a, str> {
std::borrow::Cow::Owned(self.stripped.into_owned())
}
pub fn iter(&'a self) -> Box<dyn Iterator<Item = (char, Attr)> + 'a> {
if self.fragments.is_none() {
return Box::new(self.stripped.chars().map(|c| (c, Attr::default())));
}
Box::new(AnsiStringIterator::new(
&self.stripped,
self.fragments.as_ref().unwrap(),
))
}
pub fn has_attrs(&self) -> bool {
self.fragments.is_some()
}
#[inline]
pub fn stripped(&self) -> &str {
&self.stripped
}
pub fn override_attrs(&mut self, attrs: Vec<(Attr, (u32, u32))>) {
if attrs.is_empty() {
// pass
} else if self.fragments.is_none() {
self.fragments = Some(attrs);
} else {
let current_fragments = self.fragments.take().expect("unreachable");
let new_fragments = merge_fragments(&current_fragments, &attrs);
self.fragments.replace(new_fragments);
}
}
}
impl<'a> From<&'a str> for AnsiString<'a> {
fn from(s: &'a str) -> AnsiString<'a> {
AnsiString::new_raw_str(s)
}
}
impl From<String> for AnsiString<'static> {
fn from(s: String) -> Self {
AnsiString::new_raw_string(s)
}
}
// (text, indices, highlight attribute) -> AnsiString
impl<'a> From<(&'a str, &'a [usize], Attr)> for AnsiString<'a> {
fn from((text, indices, attr): (&'a str, &'a [usize], Attr)) -> Self {
let fragments = indices
.iter()
.map(|&idx| (attr, (idx as u32, 1 + idx as u32)))
.collect();
AnsiString::new_str(text, fragments)
}
}
/// An iterator over all the (char, attr) characters.
pub struct AnsiStringIterator<'a> {
fragments: &'a [(Attr, (u32, u32))],
fragment_idx: usize,
chars_iter: std::iter::Enumerate<std::str::Chars<'a>>,
}
impl<'a> AnsiStringIterator<'a> {
pub fn new(stripped: &'a str, fragments: &'a [(Attr, (u32, u32))]) -> Self {
Self {
fragments,
fragment_idx: 0,
chars_iter: stripped.chars().enumerate(),
}
}
}
impl<'a> Iterator for AnsiStringIterator<'a> {
type Item = (char, Attr);
fn next(&mut self) -> Option<Self::Item> {
match self.chars_iter.next() {
Some((char_idx, char)) => {
// update fragment_idx
loop {
if self.fragment_idx >= self.fragments.len() {
break;
}
let (_attr, (_start, end)) = self.fragments[self.fragment_idx];
if char_idx < (end as usize) {
break;
} else {
self.fragment_idx += 1;
}
}
let (attr, (start, end)) = if self.fragment_idx >= self.fragments.len() {
(Attr::default(), (char_idx as u32, 1 + char_idx as u32))
} else {
self.fragments[self.fragment_idx]
};
if (start as usize) <= char_idx && char_idx < (end as usize) {
Some((char, attr))
} else {
Some((char, Attr::default()))
}
}
None => None,
}
}
}
fn merge_fragments(old: &[(Attr, (u32, u32))], new: &[(Attr, (u32, u32))]) -> Vec<(Attr, (u32, u32))> {
let mut ret = vec![];
let mut i = 0;
let mut j = 0;
let mut os = 0;
while i < old.len() && j < new.len() {
let (oa, (o_start, oe)) = old[i];
let (na, (ns, ne)) = new[j];
os = max(os, o_start);
if ns <= os && ne >= oe {
// [--old--] | [--old--] | [--old--] | [--old--]
// [----new----] | [---new---] | [---new---] | [--new--]
i += 1; // skip old
} else if ns <= os {
// [--old--] | [--old--] | [--old--] | [---old---]
// [--new--] | [--new--] | [--new--] | [--new--]
ret.push((na, (ns, ne)));
os = ne;
j += 1;
} else if ns >= oe {
// [--old--] | [--old--]
// [--new--] | [--new--]
ret.push((oa, (os, oe)));
i += 1;
} else {
// [---old---] | [---old---] | [--old--]
// [--new--] | [--new--] | [--new--]
ret.push((oa, (os, ns)));
os = ns;
}
}
if i < old.len() {
for &(oa, (s, e)) in old[i..].iter() {
ret.push((oa, (max(os, s), e)))
}
}
if j < new.len() {
ret.extend_from_slice(&new[j..]);
}
ret
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ansi_iterator() {
let input = "\x1B[48;2;5;10;15m\x1B[38;2;70;130;180mhi\x1B[0m";
let ansistring = ANSIParser::default().parse_ansi(input);
let mut it = ansistring.iter();
let attr = Attr {
fg: Color::Rgb(70, 130, 180),
bg: Color::Rgb(5, 10, 15),
..Attr::default()
};
assert_eq!(Some(('h', attr)), it.next());
assert_eq!(Some(('i', attr)), it.next());
assert_eq!(None, it.next());
assert_eq!(ansistring.stripped(), "hi");
}
#[test]
fn test_highlight_indices() {
let text = "abc";
let indices: Vec<usize> = vec![1];
let attr = Attr {
fg: Color::Rgb(70, 130, 180),
bg: Color::Rgb(5, 10, 15),
..Attr::default()
};
let ansistring = AnsiString::from((text, &indices as &[usize], attr));
let mut it = ansistring.iter();
assert_eq!(Some(('a', Attr::default())), it.next());
assert_eq!(Some(('b', attr)), it.next());
assert_eq!(Some(('c', Attr::default())), it.next());
assert_eq!(None, it.next());
}
#[test]
fn test_normal_string() {
let input = "ab";
let ansistring = ANSIParser::default().parse_ansi(input);
assert_eq!(false, ansistring.has_attrs());
let mut it = ansistring.iter();
assert_eq!(Some(('a', Attr::default())), it.next());
assert_eq!(Some(('b', Attr::default())), it.next());
assert_eq!(None, it.next());
assert_eq!(ansistring.stripped(), "ab");
}
#[test]
fn test_multiple_attributes() {
let input = "\x1B[1;31mhi";
let ansistring = ANSIParser::default().parse_ansi(input);
let mut it = ansistring.iter();
let attr = Attr {
fg: Color::AnsiValue(1),
effect: Effect::BOLD,
..Attr::default()
};
assert_eq!(Some(('h', attr)), it.next());
assert_eq!(Some(('i', attr)), it.next());
assert_eq!(None, it.next());
assert_eq!(ansistring.stripped(), "hi");
}
#[test]
fn test_reset() {
let input = "\x1B[35mA\x1B[mB";
let ansistring = ANSIParser::default().parse_ansi(input);
assert_eq!(ansistring.fragments.as_ref().map(|x| x.len()).unwrap(), 2);
assert_eq!(ansistring.stripped(), "AB");
}
#[test]
fn test_multi_bytes() {
let input = "中`\x1B[0m\x1B[1m\x1B[31mXYZ\x1B[0ms`";
let ansistring = ANSIParser::default().parse_ansi(input);
let mut it = ansistring.iter();
let default_attr = Attr::default();
let annotated = Attr {
fg: Color::AnsiValue(1),
effect: Effect::BOLD,
..default_attr
};
assert_eq!(Some(('中', default_attr)), it.next());
assert_eq!(Some(('`', default_attr)), it.next());
assert_eq!(Some(('X', annotated)), it.next());
assert_eq!(Some(('Y', annotated)), it.next());
assert_eq!(Some(('Z', annotated)), it.next());
assert_eq!(Some(('s', default_attr)), it.next());
assert_eq!(Some(('`', default_attr)), it.next());
assert_eq!(None, it.next());
}
#[test]
fn test_merge_fragments() {
let ao = Attr::default();
let an = Attr::default().bg(Color::BLUE);
assert_eq!(
merge_fragments(&[(ao, (0, 1)), (ao, (1, 2))], &[]),
vec![(ao, (0, 1)), (ao, (1, 2))]
);
assert_eq!(
merge_fragments(&[], &[(an, (0, 1)), (an, (1, 2))]),
vec![(an, (0, 1)), (an, (1, 2))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 6)), (ao, (9, 10))], &[(an, (0, 1))]),
vec![(an, (0, 1)), (ao, (1, 3)), (ao, (5, 6)), (ao, (9, 10))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (0, 2))]),
vec![(an, (0, 2)), (ao, (2, 3)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (0, 3))]),
vec![(an, (0, 3)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(
&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))],
&[(an, (0, 6)), (an, (6, 7))]
),
vec![(an, (0, 6)), (an, (6, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (1, 2))]),
vec![(an, (1, 2)), (ao, (2, 3)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (1, 3))]),
vec![(an, (1, 3)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (1, 4))]),
vec![(an, (1, 4)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (2, 3))]),
vec![(ao, (1, 2)), (an, (2, 3)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (2, 4))]),
vec![(ao, (1, 2)), (an, (2, 4)), (ao, (5, 7)), (ao, (9, 11))]
);
assert_eq!(
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (2, 6))]),
vec![(ao, (1, 2)), (an, (2, 6)), (ao, (6, 7)), (ao, (9, 11))]
);
}
#[test]
fn test_multi_byte_359() {
// https://github.com/lotabout/skim/issues/359
let highlight = Attr::default().effect(Effect::BOLD);
let ansistring = AnsiString::new_str("ああa", vec![(highlight, (2, 3))]);
let mut it = ansistring.iter();
assert_eq!(Some(('あ', Attr::default())), it.next());
assert_eq!(Some(('あ', Attr::default())), it.next());
assert_eq!(Some(('a', highlight)), it.next());
assert_eq!(None, it.next());
}
}

View file

@ -1,205 +1,474 @@
//! Command-line interface for skim fuzzy finder.
//!
//! This binary provides the `sk` command-line tool for fuzzy finding and filtering.
#![cfg_attr(coverage, allow(unused_features), feature(coverage_attribute))]
extern crate clap;
extern crate env_logger;
#[macro_use]
extern crate log;
extern crate atty;
extern crate shlex;
extern crate skim;
extern crate time;
use eyre::{Result, eyre};
#[cfg(feature = "listen")]
use interprocess::bound_util::RefWrite;
#[cfg(feature = "listen")]
use interprocess::local_socket::ToNsName as _;
#[cfg(feature = "listen")]
use interprocess::local_socket::traits::Stream as _;
use log::trace;
#[cfg(feature = "listen")]
use skim::binds::parse_action_chain;
use skim::reader::CommandCollector;
use derive_builder::Builder;
use std::env;
use std::fs::File;
use std::io;
use std::io::{BufReader, BufWriter, IsTerminal, Write};
use std::io::{BufRead, BufReader, BufWriter, Write};
use clap::{App, Arg, ArgMatches};
use skim::prelude::*;
fn init_logger(opts: &SkimOptions) {
let target = if let Some(ref log_file) = opts.log_file.as_ref().or(std::env::var("SKIM_LOG_FILE").ok().as_ref()) {
env_logger::Target::Pipe(Box::new(File::create(log_file).expect("Failed to create log file")))
} else {
env_logger::Target::Stdout
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
let env_var = "SKIM_LOG";
const USAGE: &str = "
Usage: sk [options]
let format = |buf: &mut env_logger::fmt::Formatter, record: &log::Record<'_>| {
writeln!(
buf,
"[{} {} {} ({}:{})] [{}/{:?}] {}",
buf.timestamp_nanos(),
record.level().as_str(),
record.module_path().unwrap_or("sk"),
record.file().unwrap_or_default(),
record.line().unwrap_or_default(),
std::thread::current().name().unwrap_or("?"),
std::thread::current().id(),
record.args()
)
};
Options
-h, --help print this help menu
--version print out the current version of skim
if let Some(level) = opts.log_level {
env_logger::builder()
.filter_level(level)
.parse_env(env_var)
.target(target)
.format(format)
.init();
} else {
env_logger::builder()
.parse_env(env_var)
.target(target)
.format(format)
.init();
}
}
Search
--tac reverse the order of search result
--no-sort Do not sort the result
-t, --tiebreak [score,begin,end,-score,length...]
comma seperated criteria
-n, --nth 1,2..5 specify the fields to be matched
--with-nth 1,2..5 specify the fields to be transformed
-d, --delimiter \\t specify the delimiter(in REGEX) for fields
-e, --exact start skim in exact mode
--regex use regex instead of fuzzy match
--algo=TYPE Fuzzy matching algorithm:
[skim_v1|skim_v2|clangd] (default: skim_v2)
--case [respect,ignore,smart] (default: smart)
case sensitive or not
Interface
-b, --bind KEYBINDS comma seperated keybindings, in KEY:ACTION
such as 'ctrl-j:accept,ctrl-k:kill-line'
-m, --multi Enable Multiple Selection
--no-multi Disable Multiple Selection
--no-mouse Disable mouse events
-c, --cmd ag command to invoke dynamically
-i, --interactive Start skim in interactive(command) mode
--color [BASE][,COLOR:ANSI]
change color theme
--no-hscroll Disable horizontal scroll
--keep-right Keep the right end of the line visible on overflow
--skip-to-pattern Line starts with the start of matched pattern
--no-clear-if-empty Do not clear previous items if command returns empty result
--no-clear-start Do not clear on start
--show-cmd-error Send command error message if command fails
Layout
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
--height=HEIGHT Height of skim's window (--height 40%)
--no-height Disable height feature
--min-height=HEIGHT Minimum height when --height is given by percent
(default: 10)
--margin=MARGIN Screen Margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
e.g. (sk --margin 1,10%)
-p, --prompt '> ' prompt string for query mode
--cmd-prompt '> ' prompt string for command mode
Display
--ansi parse ANSI color codes for input strings
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--inline-info Display info next to query
--header=STR Display STR next to info
--header-lines=N The first N lines of the input are treated as header
History
--history=FILE History file
--history-size=N Maximum number of query history entries (default: 1000)
--cmd-history=FILE command History file
--cmd-history-size=N Maximum number of command history entries (default: 1000)
Preview
--preview=COMMAND command to preview current highlighted line ({})
We can specify the fields. e.g. ({1}, {..3}, {0..})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:hidden][:+SCROLL[-OFFSET]]
Scripting
-q, --query \"\" specify the initial query
--cmd-query \"\" specify the initial query for interactive mode
--expect KEYS comma seperated keys that can be used to complete skim
--read0 Read input delimited by ASCII NUL(\\0) characters
--print0 Print output delimited by ASCII NUL(\\0) characters
--no-clear-start Do not clear screen on start
--no-clear Do not clear screen on exit
--print-query Print query as the first line
--print-cmd Print command query as the first line (after --print-query)
--print-score Print matching score in filter output (with --filter)
-1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match
--sync Synchronous search for multi-staged filtering
--pre-select-n=NUM Pre-select the first n items in multi-selection mode
--pre-select-pat=REGEX
Pre-select the matched items in multi-selection mode
--pre-select-items=$'item1\\nitem2'
Pre-select the items separated by newline character
--pre-select-file=FILENAME
Pre-select the items read from file
Environment variables
SKIM_DEFAULT_COMMAND Default command to use when input is tty
SKIM_DEFAULT_OPTIONS Default options (e.g. '--ansi --regex')
You should not include other environment variables
(e.g. '-c \"$HOME/bin/ag\"')
Removed
-I replstr replace `replstr` with the selected item
Reserved (not used for now)
--extended
--literal
--cycle
--hscroll-off=COL
--filepath-word
--jump-labels=CHARS
--border
--no-bold
--info
--pointer
--marker
--phony
";
const DEFAULT_HISTORY_SIZE: usize = 1000;
//------------------------------------------------------------------------------
fn main() -> Result<()> {
let mut opts = SkimOptions::from_env().unwrap_or_else(|e| {
e.exit();
});
init_logger(&opts);
fn main() {
env_logger::builder().format_timestamp_nanos().init();
// Build the options after setting the log target
opts = opts.build();
trace!("Command line: {:?}", std::env::args());
// Shell completion scripts
if let Some(shell) = opts.shell {
// Generate completion script directly to stdout
skim::shell::generate_completions(&shell, &mut std::io::stdout());
if opts.shell_bindings {
skim::shell::generate_key_bindings(&shell, &mut std::io::stdout())?;
}
return Ok(());
}
// Man page
if opts.man {
crate::manpage::generate(&mut std::io::stdout())?;
return Ok(());
}
#[cfg(feature = "listen")]
if let Some(remote) = opts.remote {
let ns_name = remote
.to_ns_name::<interprocess::local_socket::GenericNamespaced>()
.unwrap();
let stream = interprocess::local_socket::Stream::connect(ns_name)?;
let mut action_chain = String::new();
loop {
action_chain.clear();
let len = std::io::stdin().read_line(&mut action_chain)?;
log::debug!("Got line {} from stdin", action_chain.trim());
if len == 0 {
break;
}
let actions = parse_action_chain(action_chain.trim())?;
for act in actions {
stream
.as_write()
.write_all(format!("{}\n", ron::ser::to_string(&act)?).as_bytes())?;
log::debug!("Sent action {act:?} to listener");
}
}
return Ok(());
}
match sk_main(opts) {
match real_main() {
Ok(exit_code) => std::process::exit(exit_code),
Err(err) => match err.downcast_ref::<clap::error::Error>() {
Some(e) => e.exit(),
None => Err(eyre!(err)),
},
Err(err) => {
// if downstream pipe is closed, exit silently, see PR#279
if err.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0)
}
std::process::exit(2)
}
}
}
/// Returns `None` if the popup should not open, otherwise run the popup and return the result
#[cfg(unix)]
#[allow(clippy::option_option)]
fn check_and_run_popup(opts: &SkimOptions) -> Option<Option<SkimOutput>> {
if opts.popup.is_some() && popup::check_env() {
Some(crate::popup::run_with(opts))
} else {
None
#[rustfmt::skip]
fn real_main() -> Result<i32, std::io::Error> {
let mut stdout = std::io::stdout();
let mut args = Vec::new();
args.push(env::args().next().expect("there should be at least one arg: the application name"));
args.extend(env::var("SKIM_DEFAULT_OPTIONS")
.ok()
.and_then(|val| shlex::split(&val))
.unwrap_or_default());
for arg in env::args().skip(1) {
args.push(arg);
}
}
#[cfg(not(unix))]
#[allow(clippy::option_option)]
fn check_and_run_popup(_opts: &SkimOptions) -> Option<Option<SkimOutput>> {
None
}
fn sk_main(mut opts: SkimOptions) -> Result<i32> {
let reader_opts = SkimItemReaderOption::from_options(&opts);
let cmd_collector = Rc::new(RefCell::new(SkimItemReader::new(reader_opts)));
opts.cmd_collector = cmd_collector.clone() as Rc<RefCell<dyn CommandCollector>>;
let cmd_history = opts.cmd_history.clone();
let cmd_history_size = opts.cmd_history_size;
let cmd_history_file = opts.cmd_history_file.clone();
let query_history = opts.query_history.clone();
let history_size = opts.history_size;
let history_file = opts.history_file.clone();
//------------------------------------------------------------------------------
let bin_options = BinOptions::from_opts(&opts);
// parse options
let opts = App::new("sk")
.author("Jinzhou Zhang<lotabout@gmail.com>")
.arg(Arg::with_name("help").long("help").short('h'))
.arg(Arg::with_name("version").long("version").short('v'))
.arg(Arg::with_name("bind").long("bind").short('b').multiple(true).takes_value(true))
.arg(Arg::with_name("multi").long("multi").short('m').multiple(true))
.arg(Arg::with_name("no-multi").long("no-multi").multiple(true))
.arg(Arg::with_name("prompt").long("prompt").short('p').multiple(true).takes_value(true).default_value("> "))
.arg(Arg::with_name("cmd-prompt").long("cmd-prompt").multiple(true).takes_value(true).default_value("c> "))
.arg(Arg::with_name("expect").long("expect").multiple(true).takes_value(true))
.arg(Arg::with_name("tac").long("tac").multiple(true))
.arg(Arg::with_name("tiebreak").long("tiebreak").short('t').multiple(true).takes_value(true))
.arg(Arg::with_name("ansi").long("ansi").multiple(true))
.arg(Arg::with_name("exact").long("exact").short('e').multiple(true))
.arg(Arg::with_name("cmd").long("cmd").short('c').multiple(true).takes_value(true))
.arg(Arg::with_name("interactive").long("interactive").short('i').multiple(true))
.arg(Arg::with_name("query").long("query").short('q').multiple(true).takes_value(true))
.arg(Arg::with_name("cmd-query").long("cmd-query").multiple(true).takes_value(true))
.arg(Arg::with_name("regex").long("regex").multiple(true))
.arg(Arg::with_name("delimiter").long("delimiter").short('d').multiple(true).takes_value(true))
.arg(Arg::with_name("nth").long("nth").short('n').multiple(true).takes_value(true))
.arg(Arg::with_name("with-nth").long("with-nth").multiple(true).takes_value(true))
.arg(Arg::with_name("replstr").short('I').multiple(true).takes_value(true))
.arg(Arg::with_name("color").long("color").multiple(true).takes_value(true))
.arg(Arg::with_name("margin").long("margin").multiple(true).takes_value(true).default_value("0,0,0,0"))
.arg(Arg::with_name("min-height").long("min-height").multiple(true).takes_value(true).default_value("10"))
.arg(Arg::with_name("height").long("height").multiple(true).takes_value(true).default_value("100%"))
.arg(Arg::with_name("no-height").long("no-height").multiple(true))
.arg(Arg::with_name("no-clear").long("no-clear").multiple(true))
.arg(Arg::with_name("no-clear-start").long("no-clear-start").multiple(true))
.arg(Arg::with_name("no-mouse").long("no-mouse").multiple(true))
.arg(Arg::with_name("preview").long("preview").multiple(true).takes_value(true))
.arg(Arg::with_name("preview-window").long("preview-window").multiple(true).takes_value(true).default_value("right:50%"))
.arg(Arg::with_name("reverse").long("reverse").multiple(true))
.arg(Arg::with_name("algorithm").long("algo").multiple(true).takes_value(true).default_value("skim_v2"))
.arg(Arg::with_name("case").long("case").multiple(true).takes_value(true).default_value("smart"))
.arg(Arg::with_name("literal").long("literal").multiple(true))
.arg(Arg::with_name("cycle").long("cycle").multiple(true))
.arg(Arg::with_name("no-hscroll").long("no-hscroll").multiple(true))
.arg(Arg::with_name("hscroll-off").long("hscroll-off").multiple(true).takes_value(true).default_value("10"))
.arg(Arg::with_name("filepath-word").long("filepath-word").multiple(true))
.arg(Arg::with_name("jump-labels").long("jump-labels").multiple(true).takes_value(true).default_value("abcdefghijklmnopqrstuvwxyz"))
.arg(Arg::with_name("border").long("border").multiple(true))
.arg(Arg::with_name("inline-info").long("inline-info").multiple(true))
.arg(Arg::with_name("header").long("header").multiple(true).takes_value(true).default_value(""))
.arg(Arg::with_name("header-lines").long("header-lines").multiple(true).takes_value(true).default_value("0"))
.arg(Arg::with_name("tabstop").long("tabstop").multiple(true).takes_value(true).default_value("8"))
.arg(Arg::with_name("no-bold").long("no-bold").multiple(true))
.arg(Arg::with_name("history").long("history").multiple(true).takes_value(true))
.arg(Arg::with_name("cmd-history").long("cmd-history").multiple(true).takes_value(true))
.arg(Arg::with_name("history-size").long("history-size").multiple(true).takes_value(true).default_value("1000"))
.arg(Arg::with_name("cmd-history-size").long("cmd-history-size").multiple(true).takes_value(true).default_value("1000"))
.arg(Arg::with_name("print-query").long("print-query").multiple(true))
.arg(Arg::with_name("print-cmd").long("print-cmd").multiple(true))
.arg(Arg::with_name("print-score").long("print-score").multiple(true))
.arg(Arg::with_name("read0").long("read0").multiple(true))
.arg(Arg::with_name("print0").long("print0").multiple(true))
.arg(Arg::with_name("sync").long("sync").multiple(true))
.arg(Arg::with_name("extended").long("extended").short('x').multiple(true))
.arg(Arg::with_name("no-sort").long("no-sort").multiple(true))
.arg(Arg::with_name("select-1").long("select-1").short('1').multiple(true))
.arg(Arg::with_name("exit-0").long("exit-0").short('0').multiple(true))
.arg(Arg::with_name("filter").long("filter").short('f').takes_value(true).multiple(true))
.arg(Arg::with_name("layout").long("layout").multiple(true).takes_value(true).default_value("default"))
.arg(Arg::with_name("keep-right").long("keep-right").multiple(true))
.arg(Arg::with_name("skip-to-pattern").long("skip-to-pattern").multiple(true).takes_value(true).default_value(""))
.arg(Arg::with_name("pre-select-n").long("pre-select-n").multiple(true).takes_value(true).default_value("0"))
.arg(Arg::with_name("pre-select-pat").long("pre-select-pat").multiple(true).takes_value(true).default_value(""))
.arg(Arg::with_name("pre-select-items").long("pre-select-items").multiple(true).takes_value(true))
.arg(Arg::with_name("pre-select-file").long("pre-select-file").multiple(true).takes_value(true).default_value(""))
.arg(Arg::with_name("no-clear-if-empty").long("no-clear-if-empty").multiple(true))
.arg(Arg::with_name("show-cmd-error").long("show-cmd-error").multiple(true))
.get_matches_from(args);
if opts.is_present("help") {
write!(stdout, "{}", USAGE)?;
return Ok(0);
}
if opts.is_present("version") {
writeln!(stdout, "{}", VERSION)?;
return Ok(0);
}
//------------------------------------------------------------------------------
let mut options = parse_options(&opts);
let preview_window_joined = opts.values_of("preview-window").map(|x| x.collect::<Vec<_>>().join(":"));
options.preview_window = preview_window_joined.as_deref();
//------------------------------------------------------------------------------
// initialize collector
let item_reader_option = SkimItemReaderOption::default()
.ansi(opts.is_present("ansi"))
.delimiter(opts.values_of("delimiter").and_then(|vals| vals.last()).unwrap_or(""))
.with_nth(opts.values_of("with-nth").and_then(|vals| vals.last()).unwrap_or(""))
.nth(opts.values_of("nth").and_then(|vals| vals.last()).unwrap_or(""))
.read0(opts.is_present("read0"))
.show_error(opts.is_present("show-cmd-error"))
.build();
let cmd_collector = Rc::new(RefCell::new(SkimItemReader::new(item_reader_option)));
options.cmd_collector = cmd_collector.clone();
//------------------------------------------------------------------------------
// read in the history file
let fz_query_histories = opts.values_of("history").and_then(|vals| vals.last());
let cmd_query_histories = opts.values_of("cmd-history").and_then(|vals| vals.last());
let query_history = fz_query_histories.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_default();
let cmd_history = cmd_query_histories.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_default();
if fz_query_histories.is_some() || cmd_query_histories.is_some() {
options.query_history = &query_history;
options.cmd_history = &cmd_history;
// bind ctrl-n and ctrl-p to handle history
options.bind.insert(0, "ctrl-p:previous-history,ctrl-n:next-history");
}
//------------------------------------------------------------------------------
// handle pre-selection options
let pre_select_n: Option<usize> = opts.values_of("pre-select-n").and_then(|vals| vals.last()).and_then(|s| s.parse().ok());
let pre_select_pat = opts.values_of("pre-select-pat").and_then(|vals| vals.last());
let pre_select_items: Option<Vec<String>> = opts.values_of("pre-select-items").map(|vals| vals.flat_map(|m|m.split('\n')).map(|s|s.to_string()).collect());
let pre_select_file = opts.values_of("pre-select-file").and_then(|vals| vals.last());
if pre_select_n.is_some() || pre_select_pat.is_some() || pre_select_items.is_some() || pre_select_file.is_some() {
let first_n = pre_select_n.unwrap_or(0);
let pattern = pre_select_pat.unwrap_or("");
let preset_items = pre_select_items.unwrap_or_default();
let preset_file = pre_select_file.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_default();
let selector = DefaultSkimSelector::default()
.first_n(first_n)
.regex(pattern)
.preset(preset_items)
.preset(preset_file);
options.selector = Some(Rc::new(selector));
}
let options = options;
//------------------------------------------------------------------------------
let bin_options = BinOptionsBuilder::default()
.filter(opts.values_of("filter").and_then(|vals| vals.last()))
.print_query(opts.is_present("print-query"))
.print_cmd(opts.is_present("print-cmd"))
.output_ending(if opts.is_present("print0") { "\0" } else { "\n" })
.build()
.expect("");
//------------------------------------------------------------------------------
// read from pipe or command
let rx_item = if atty::isnt(atty::Stream::Stdin) {
let rx_item = cmd_collector.borrow().of_bufread(BufReader::new(std::io::stdin()));
Some(rx_item)
} else {
None
};
//------------------------------------------------------------------------------
// filter mode
if opts.is_present("filter") {
return filter(&bin_options, &options, rx_item);
}
//------------------------------------------------------------------------------
let output = Skim::run_with(&options, rx_item);
if output.is_none() { // error
return Ok(135);
}
//------------------------------------------------------------------------------
// output
let Some(result) = check_and_run_popup(&opts).unwrap_or_else(|| {
// read from pipe or command
let rx_item = if io::stdin().is_terminal() || (opts.interactive && opts.cmd.is_some()) {
None
} else {
let rx_item = cmd_collector.borrow().of_bufread(BufReader::new(std::io::stdin()));
Some(rx_item)
};
Skim::run_with(opts, rx_item).ok()
}) else {
return Ok(135);
};
log::debug!("result: {result:?}");
if result.is_abort {
let output = output.unwrap();
if output.is_abort {
return Ok(130);
}
// Output — use a large BufWriter to batch all writes into a few syscalls
// instead of one syscall per item (Rust's default LineWriter flushes on \n).
{
let stdout = io::stdout();
let mut out = BufWriter::with_capacity(1 << 20, stdout.lock());
result.write_output(&mut out, &bin_options)?;
out.flush()?;
// output query
if bin_options.print_query {
write!(stdout, "{}{}", output.query, bin_options.output_ending)?;
}
if bin_options.print_cmd {
write!(stdout, "{}{}", output.cmd, bin_options.output_ending)?;
}
if opts.is_present("expect") {
match output.final_event {
Event::EvActAccept(Some(accept_key)) => {
write!(stdout, "{}{}", accept_key, bin_options.output_ending)?;
}
Event::EvActAccept(None) => {
write!(stdout, "{}", bin_options.output_ending)?;
}
_ => {}
}
}
for item in output.selected_items.iter() {
write!(stdout, "{}{}", item.output(), bin_options.output_ending)?;
}
//------------------------------------------------------------------------------
// write the history with latest item
if let Some(file) = history_file {
let limit = history_size;
write_history_to_file(&query_history, &result.query, limit, &file)?;
if let Some(file) = fz_query_histories {
let limit = opts.values_of("history-size").and_then(|vals| vals.last())
.and_then(|size| size.parse::<usize>().ok())
.unwrap_or(DEFAULT_HISTORY_SIZE);
write_history_to_file(&query_history, &output.query, limit, file)?;
}
if let Some(file) = cmd_history_file {
let limit = cmd_history_size;
write_history_to_file(&cmd_history, &result.cmd, limit, &file)?;
if let Some(file) = cmd_query_histories {
let limit = opts.values_of("cmd-history-size").and_then(|vals| vals.last())
.and_then(|size| size.parse::<usize>().ok())
.unwrap_or(DEFAULT_HISTORY_SIZE);
write_history_to_file(&cmd_history, &output.cmd, limit, file)?;
}
Ok(i32::from(result.selected_items.is_empty()))
Ok(if output.selected_items.is_empty() { 1 } else { 0 })
}
fn parse_options(options: &ArgMatches) -> SkimOptions<'_> {
SkimOptionsBuilder::default()
.color(options.values_of("color").and_then(|vals| vals.last()))
.min_height(options.values_of("min-height").and_then(|vals| vals.last()))
.no_height(options.is_present("no-height"))
.height(options.values_of("height").and_then(|vals| vals.last()))
.margin(options.values_of("margin").and_then(|vals| vals.last()))
.preview(options.values_of("preview").and_then(|vals| vals.last()))
.cmd(options.values_of("cmd").and_then(|vals| vals.last()))
.query(options.values_of("query").and_then(|vals| vals.last()))
.cmd_query(options.values_of("cmd-query").and_then(|vals| vals.last()))
.interactive(options.is_present("interactive"))
.prompt(options.values_of("prompt").and_then(|vals| vals.last()))
.cmd_prompt(options.values_of("cmd-prompt").and_then(|vals| vals.last()))
.bind(
options
.values_of("bind")
.map(|x| x.collect::<Vec<_>>())
.unwrap_or_default(),
)
.expect(options.values_of("expect").map(|x| x.collect::<Vec<_>>().join(",")))
.multi(if options.is_present("no-multi") {
false
} else {
options.is_present("multi")
})
.layout(options.values_of("layout").and_then(|vals| vals.last()).unwrap_or(""))
.reverse(options.is_present("reverse"))
.no_hscroll(options.is_present("no-hscroll"))
.no_mouse(options.is_present("no-mouse"))
.no_clear(options.is_present("no-clear"))
.no_clear_start(options.is_present("no-clear-start"))
.tabstop(options.values_of("tabstop").and_then(|vals| vals.last()))
.tiebreak(options.values_of("tiebreak").map(|x| x.collect::<Vec<_>>().join(",")))
.tac(options.is_present("tac"))
.nosort(options.is_present("no-sort"))
.exact(options.is_present("exact"))
.regex(options.is_present("regex"))
.delimiter(options.values_of("delimiter").and_then(|vals| vals.last()))
.inline_info(options.is_present("inline-info"))
.header(options.values_of("header").and_then(|vals| vals.last()))
.header_lines(
options
.values_of("header-lines")
.and_then(|vals| vals.last())
.map(|s| s.parse::<usize>().unwrap_or(0))
.unwrap_or(0),
)
.layout(options.values_of("layout").and_then(|vals| vals.last()).unwrap_or(""))
.algorithm(FuzzyAlgorithm::of(
options.values_of("algorithm").and_then(|vals| vals.last()).unwrap(),
))
.case(match options.value_of("case") {
Some("smart") => CaseMatching::Smart,
Some("ignore") => CaseMatching::Ignore,
_ => CaseMatching::Respect,
})
.keep_right(options.is_present("keep-right"))
.skip_to_pattern(
options
.values_of("skip-to-pattern")
.and_then(|vals| vals.last())
.unwrap_or(""),
)
.select1(options.is_present("select-1"))
.exit0(options.is_present("exit-0"))
.sync(options.is_present("sync"))
.no_clear_if_empty(options.is_present("no-clear-if-empty"))
.build()
.unwrap()
}
fn read_file_lines(filename: &str) -> Result<Vec<String>, std::io::Error> {
let file = File::open(filename)?;
let ret = BufReader::new(file).lines().collect();
debug!("file content: {:?}", ret);
ret
}
fn write_history_to_file(
@ -208,11 +477,11 @@ fn write_history_to_file(
limit: usize,
filename: &str,
) -> Result<(), std::io::Error> {
if orig_history.last().map(String::as_str) == Some(latest) {
if orig_history.last().map(|l| l.as_str()) == Some(latest) {
// no point of having at the end of the history 5x the same command...
return Ok(());
}
let additional_lines = usize::from(!latest.trim().is_empty());
let additional_lines = if latest.trim().is_empty() { 0 } else { 1 };
let start_index = if orig_history.len() + additional_lines > limit {
orig_history.len() + additional_lines - limit
} else {
@ -228,51 +497,69 @@ fn write_history_to_file(
Ok(())
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
fn read(path: &std::path::Path) -> String {
std::fs::read_to_string(path).unwrap_or_default()
}
#[test]
fn write_history_appends_latest_entry() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hist");
let file_str = file.to_str().unwrap();
write_history_to_file(&["a".to_string(), "b".to_string()], "c", 10, file_str).unwrap();
assert_eq!(read(&file), "a\nb\nc");
}
#[test]
fn write_history_skips_duplicate_of_last() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hist");
let file_str = file.to_str().unwrap();
// The latest equals the last entry → nothing is written, no file created.
write_history_to_file(&["a".to_string(), "b".to_string()], "b", 10, file_str).unwrap();
assert!(!file.exists());
}
#[test]
fn write_history_truncates_to_limit() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hist");
let file_str = file.to_str().unwrap();
// limit 2 with 3 existing + 1 new keeps only the newest entries.
write_history_to_file(&["a".to_string(), "b".to_string(), "c".to_string()], "d", 2, file_str).unwrap();
assert_eq!(read(&file), "c\nd");
}
#[test]
fn write_history_empty_latest_does_not_count_towards_limit() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hist");
let file_str = file.to_str().unwrap();
// An empty latest adds 0 to the length, so no truncation occurs at limit 3.
write_history_to_file(&["a".to_string(), "b".to_string(), "c".to_string()], "", 3, file_str).unwrap();
assert_eq!(read(&file), "a\nb\nc\n");
}
#[derive(Builder)]
pub struct BinOptions<'a> {
filter: Option<&'a str>,
output_ending: &'a str,
print_query: bool,
print_cmd: bool,
}
pub fn filter(
bin_option: &BinOptions,
options: &SkimOptions,
source: Option<SkimItemReceiver>,
) -> Result<i32, std::io::Error> {
let mut stdout = std::io::stdout();
let default_command = match env::var("SKIM_DEFAULT_COMMAND").as_ref().map(String::as_ref) {
Ok("") | Err(_) => "find .".to_owned(),
Ok(val) => val.to_owned(),
};
let query = bin_option.filter.unwrap_or("");
let cmd = options.cmd.unwrap_or(&default_command);
// output query
if bin_option.print_query {
write!(stdout, "{}{}", query, bin_option.output_ending)?;
}
if bin_option.print_cmd {
write!(stdout, "{}{}", cmd, bin_option.output_ending)?;
}
//------------------------------------------------------------------------------
// matcher
let engine_factory: Box<dyn MatchEngineFactory> = if options.regex {
Box::new(RegexEngineFactory::builder())
} else {
let fuzzy_engine_factory = ExactOrFuzzyEngineFactory::builder()
.fuzzy_algorithm(options.algorithm)
.exact_mode(options.exact)
.build();
Box::new(AndOrEngineFactory::new(fuzzy_engine_factory))
};
let engine = engine_factory.create_engine_with_case(query, options.case);
//------------------------------------------------------------------------------
// start
let components_to_stop = Arc::new(AtomicUsize::new(0));
let stream_of_item = source.unwrap_or_else(|| {
let cmd_collector = options.cmd_collector.clone();
let (ret, _control) = cmd_collector.borrow_mut().invoke(cmd, components_to_stop);
ret
});
let mut num_matched = 0;
stream_of_item
.into_iter()
.filter_map(|item| engine.match_item(item.clone()).map(|result| (item, result)))
.try_for_each(|(item, _match_result)| {
num_matched += 1;
write!(stdout, "{}{}", item.output(), bin_option.output_ending)
})?;
Ok(if num_matched == 0 { 1 } else { 0 })
}

View file

@ -1,423 +0,0 @@
//! Key binding configuration and parsing.
//!
//! This module provides utilities for parsing and managing keyboard shortcuts
//! and their associated actions in skim.
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use eyre::{Result, eyre};
use crate::tui::actions::{self, Action};
/// Synthetic events that skim fires internally and that can be bound to actions
/// via the keymap, exactly like a real key press.
///
/// The keymap is keyed by crossterm's [`KeyEvent`], which cannot express
/// "the query changed" or "reading finished" directly. Each variant is
/// therefore represented *transparently* as a reserved function-key code in the
/// high-`F` range (`F(248)``F(255)`) that no real terminal ever emits.
/// Giving these reserved codes named variants keeps them in one place instead
/// of scattering magic function-key literals across the codebase, and lets
/// [`parse_key`] accept every friendly event name.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum SkimEvent {
/// Fired once, when skim has started up and entered its event loop.
Start,
/// Fired when the reader finishes producing items (once per read; a
/// `reload` starts a new read and fires it again).
Load,
/// Fired whenever the query changes.
Change,
/// Fired when filtering for the current query completes and the result
/// list is ready.
Result,
/// Fired when the focused item changes (cursor movement or a result update).
Focus,
/// Fired when a completed search yields no matches.
Zero,
/// Fired when a completed search yields exactly one match.
One,
/// Fired after two left mouse-button presses no more than 500 ms apart.
DoubleClick,
}
impl SkimEvent {
/// The reserved [`KeyCode`] used to route this event through the keymap.
#[must_use]
pub const fn key_code(self) -> KeyCode {
match self {
SkimEvent::Change => KeyCode::F(255),
SkimEvent::Start => KeyCode::F(254),
SkimEvent::Load => KeyCode::F(253),
SkimEvent::Result => KeyCode::F(252),
SkimEvent::Focus => KeyCode::F(251),
SkimEvent::Zero => KeyCode::F(250),
SkimEvent::One => KeyCode::F(249),
SkimEvent::DoubleClick => KeyCode::F(248),
}
}
/// The reserved [`KeyEvent`] used to route this event through the keymap.
#[must_use]
pub const fn key_event(self) -> KeyEvent {
KeyEvent::new(self.key_code(), KeyModifiers::NONE)
}
/// Parses an event name (`start`, `load`, `change`) into a [`SkimEvent`].
///
/// Returns `None` if the name is not a recognised event.
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"start" => Some(SkimEvent::Start),
"load" => Some(SkimEvent::Load),
"change" => Some(SkimEvent::Change),
"result" => Some(SkimEvent::Result),
"focus" => Some(SkimEvent::Focus),
"zero" => Some(SkimEvent::Zero),
"one" => Some(SkimEvent::One),
"double-click" => Some(SkimEvent::DoubleClick),
_ => None,
}
}
}
impl From<SkimEvent> for KeyEvent {
fn from(event: SkimEvent) -> Self {
event.key_event()
}
}
/// A map of key events to their associated actions
#[derive(Clone, Debug)]
pub struct KeyMap(pub HashMap<KeyEvent, Vec<Action>>);
impl Deref for KeyMap {
type Target = HashMap<KeyEvent, Vec<Action>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for KeyMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<&str> for KeyMap {
fn from(value: &str) -> Self {
parse_keymaps(split_top_level(value, ',').into_iter())
}
}
impl Default for KeyMap {
fn default() -> Self {
get_default_key_map()
}
}
impl KeyMap {
/// Adds keymaps from a comma-separated string.
pub(crate) fn add_keymaps_str(&mut self, source: &str) {
self.add_keymaps(split_top_level(source, ',').into_iter());
}
/// Adds keymaps from the source, parsing them using `parse_keymap`
pub fn add_keymaps<'a, T>(&mut self, source: T)
where
T: Iterator<Item = &'a str>,
{
for map in source {
if let Ok((key, action_chain)) = parse_keymap(map) {
self.bind(key, action_chain)
.unwrap_or_else(|err| debug!("Failed to bind key {map}: {err}"));
} else {
debug!("Failed to parse key: {map}");
}
}
}
fn bind(&mut self, key: &str, action_chain: Vec<Action>) -> Result<()> {
let key = parse_key(key)?;
// remove the key for existing keymap;
let _ = self.remove(&key);
self.entry(key).or_insert(action_chain);
Ok(())
}
}
/// Returns the default key bindings for skim
#[rustfmt::skip]
#[must_use]
pub fn get_default_key_map() -> KeyMap {
let mut ret = HashMap::new();
ret.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), vec![Action::Down(1)]);
ret.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE), vec![Action::Up(1)]);
ret.insert(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE), vec![Action::PageUp(1)]);
ret.insert(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE), vec![Action::PageDown(1)]);
ret.insert(KeyEvent::new(KeyCode::End, KeyModifiers::NONE), vec![Action::EndOfLine]);
ret.insert(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE), vec![Action::BeginningOfLine]);
ret.insert(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE), vec![Action::DeleteChar]);
ret.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), vec![Action::Toggle, Action::Down(1)]);
ret.insert(KeyEvent::new(KeyCode::BackTab, KeyModifiers::all()), vec![Action::Toggle, Action::Up(1)]);
ret.insert(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), vec![Action::Abort]);
ret.insert(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), vec![Action::Accept(None)]);
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), vec![Action::BackwardChar]);
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), vec![Action::ForwardChar]);
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), vec![Action::BackwardDeleteChar]);
ret.insert(SkimEvent::DoubleClick.key_event(), vec![Action::Accept(None)]);
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT), vec![Action::BackwardWord]);
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT), vec![Action::ForwardWord]);
ret.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT), vec![Action::PreviewUp(1)]);
ret.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT), vec![Action::PreviewDown(1)]);
ret.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT), vec![Action::Toggle, Action::Up(1)]);
ret.insert(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT), vec![Action::Toggle, Action::Up(1)]);
ret.insert(KeyEvent::new(KeyCode::Home, KeyModifiers::SHIFT), vec![Action::BeginningOfLine]);
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL), vec![Action::BackwardWord]);
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL), vec![Action::ForwardWord]);
ret.insert(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), vec![Action::BeginningOfLine]);
ret.insert(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL), vec![Action::BackwardChar]);
ret.insert(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), vec![Action::Abort]);
ret.insert(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), vec![Action::Abort]);
ret.insert(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL), vec![Action::EndOfLine]);
ret.insert(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL), vec![Action::ForwardChar]);
ret.insert(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL), vec![Action::Abort]);
ret.insert(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL), vec![Action::BackwardDeleteChar]);
ret.insert(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL), vec![Action::Down(1)]);
ret.insert(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL), vec![Action::Up(1)]);
ret.insert(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL), vec![Action::ClearScreen]);
ret.insert(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL), vec![Action::Down(1)]);
ret.insert(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL), vec![Action::Up(1)]);
ret.insert(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL), vec![Action::ToggleInteractive]);
ret.insert(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL), vec![Action::RotateMode]);
ret.insert(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL), vec![Action::UnixLineDiscard]);
ret.insert(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), vec![Action::UnixWordRubout]);
ret.insert(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL), vec![Action::Yank]);
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT), vec![Action::BackwardKillWord]);
ret.insert(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT), vec![Action::BackwardWord]);
ret.insert(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::ALT), vec![Action::KillWord]);
ret.insert(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT), vec![Action::ForwardWord]);
ret.insert(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT), vec![Action::ScrollLeft(1)]);
ret.insert(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::ALT), vec![Action::ScrollRight(1)]);
KeyMap(ret)
}
/// Parses a key str into a crossterm `KeyEvent`.
///
/// In addition to keyboard names, accepts all names recognized by
/// [`SkimEvent::from_name`], including `change`, `start`, and `double-click`.
///
/// # Errors
/// Returns an error if the key string is empty, contains an unknown modifier,
/// or does not correspond to a recognised key name.
pub fn parse_key(key: &str) -> Result<KeyEvent> {
if key.is_empty() {
return Err(eyre!("Cannot parse empty key"));
}
if let Some(event) = SkimEvent::from_name(key) {
return Ok(event.key_event());
}
let parts = key.split('-').collect::<Vec<&str>>();
let mut mods = KeyModifiers::NONE;
if parts.len() > 1 {
let mod_strs = &parts[..parts.len() - 1];
for mod_str in mod_strs {
mods |= match *mod_str {
"ctrl" => KeyModifiers::CONTROL,
"alt" => KeyModifiers::ALT,
"shift" => KeyModifiers::SHIFT,
s => return Err(eyre!("Failed to parse {} as key modifier", s)),
}
}
}
let key = parts.last().unwrap_or(&"").to_string();
let keycode: KeyCode;
if key.len() == 1 {
let char = key.chars().next().unwrap_or_default();
if char.is_uppercase() {
mods |= KeyModifiers::SHIFT;
keycode = KeyCode::Char(char.to_ascii_lowercase());
} else {
keycode = KeyCode::Char(char);
}
} else if let Some(f) = key.strip_prefix('f')
&& let Ok(f_index) = f.parse::<u8>()
{
// A function key like `f10`. If the suffix isn't numeric (e.g. `focus`,
// `first`), fall through to the named-key / event matching below.
keycode = KeyCode::F(f_index);
} else {
keycode = match key.as_str() {
"space" => KeyCode::Char(' '),
"enter" => KeyCode::Enter,
"bspace" | "bs" => KeyCode::Backspace,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"tab" => KeyCode::Tab,
"btab" => KeyCode::BackTab,
"esc" => KeyCode::Esc,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pgup" => KeyCode::PageUp,
"pgdown" => KeyCode::PageDown,
s => match SkimEvent::from_name(s) {
Some(event) => event.key_code(),
None => return Err(eyre!("Unknown key {}", s)),
},
}
}
debug!("parsed key {keycode:?} and mods {mods:?}");
Ok(KeyEvent::new(keycode, mods))
}
/// Parse an iterator of keymaps into a `KeyMap`
pub fn parse_keymaps<'a, T>(maps: T) -> KeyMap
where
T: Iterator<Item = &'a str>,
{
let mut res = KeyMap::default();
res.add_keymaps(maps);
res
}
pub(crate) fn split_top_level(value: &str, separator: char) -> Vec<&str> {
let mut depth = 0_u32;
let mut start = 0;
let mut parts = Vec::new();
for (index, ch) in value.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
_ if ch == separator && depth == 0 => {
parts.push(&value[start..index]);
start = index + ch.len_utf8();
}
_ => {}
}
}
parts.push(&value[start..]);
parts
}
/// Parses follow-up action bindings from raw `--bind` specs.
///
/// Any action can be bound as if it were an event: when the "key" of a bind is
/// not a real key but is a known action name, the bound chain becomes a
/// *follow-up* that runs right after that action. For example `reload:first`
/// queues `first` immediately after a `reload`. The returned map is keyed by the
/// action's canonical name (see [`Action::name`](crate::tui::actions::Action::name)),
/// so it can be looked up directly from the action that just ran.
///
/// Keys take precedence: if the "key" resolves to a real key it is left to the
/// key map, so a name shared by a key and an action (e.g. `up`) always binds the
/// key. To target the action in that case, prefix it with `act-` (`act-up`).
#[must_use]
pub fn parse_action_binds<'a, T>(maps: T) -> HashMap<String, Vec<Action>>
where
T: Iterator<Item = &'a str>,
{
let mut res = HashMap::new();
for map in maps {
let Some((key, chain)) = map.split_once(':') else {
continue;
};
// Keys win: anything that parses as a real key is not an action trigger.
if parse_key(key).is_ok() {
continue;
}
let Some(name) = action_trigger_name(key) else {
debug!("Ignoring bind `{map}`: `{key}` is neither a key nor an action");
continue;
};
match parse_action_chain(chain) {
Ok(actions) => {
res.insert(name.to_string(), actions);
}
Err(err) => debug!("Ignoring bind `{map}`: invalid action chain `{chain}`: {err}"),
}
}
res
}
/// Resolves a bind trigger to the canonical name of the action it targets
/// (see [`Action::name`](crate::tui::actions::Action::name)). `act-<name>`
/// explicitly targets the action `<name>`, even when `<name>` is also a key;
/// without the prefix, a bare action name works too. Returns `None` if the
/// name is not a known action.
///
/// This performs pure name resolution: callers that want "keys win" semantics
/// (e.g. [`parse_action_binds`]) must check [`parse_key`] first.
pub(crate) fn action_trigger_name(trigger: &str) -> Option<&'static str> {
let action_name = trigger.strip_prefix("act-").unwrap_or(trigger);
// Some actions require an argument when executed, but their canonical
// name is still valid as a trigger. `()` supplies the parser's empty
// placeholder solely for name validation.
let action = actions::parse_action(action_name).or_else(|| actions::parse_action(&format!("{action_name}()")))?;
Some(action.name())
}
/// Parses an action chain, separated by '+'s into the corresponding actions
///
/// # Errors
/// Returns an error if the action chain is empty or contains only unknown actions.
pub fn parse_action_chain(action_chain: &str) -> Result<Vec<Action>> {
let mut actions: Vec<Action> = vec![];
let mut split = split_top_level(action_chain, '+').into_iter();
while let Some(mut s) = split.next().map(String::from) {
if (s.starts_with("if-") || s.ends_with('{'))
&& let Some(otherwise) = split.next()
{
s += &(String::from("+") + otherwise);
}
if let Some(act) = actions::parse_action(&s) {
actions.push(act);
}
}
if actions.is_empty() {
Err(eyre!("Empty action chain or unknown action `{}`", action_chain))
} else {
Ok(actions)
}
}
/// Parse a single keymap and return the key and action(s)
///
/// # Errors
/// Returns an error if the string is empty, missing a colon separator, or the
/// action chain cannot be parsed.
pub fn parse_keymap(key_action: &str) -> Result<(&str, Vec<Action>)> {
if key_action.is_empty() {
return Err(eyre!("Got an empty keybind, skipping"));
}
debug!("got key_action: {key_action:?}");
let (key, action_chain) = key_action
.split_once(':')
.ok_or(eyre!("Failed to parse {} as key and action", key_action))?;
debug!("parsed key_action: {key:?}: {action_chain:?}");
Ok((key, parse_action_chain(action_chain)?))
}
#[cfg(test)]
#[path = "binds_tests.rs"]
mod tests;

View file

@ -1,309 +0,0 @@
use super::*;
use actions::Action::*;
#[test]
fn test_parse_action_chain() {
let parsed = parse_action_chain(
"execute-silent:1 {}+execute-silent:2 {+}+execute-silent:3 {+n}+reload+if-query-empty:reload+up",
);
assert!(parsed.is_ok());
let res = parsed.unwrap();
assert_eq!(
res,
vec![
ExecuteSilent("1 {}".into()),
ExecuteSilent("2 {+}".into()),
ExecuteSilent("3 {+n}".into()),
Reload(None),
IfQueryEmpty("reload".into(), Some("up".into())),
]
);
}
#[test]
fn test_parse_key() {
assert_eq!(
parse_key("a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
);
assert_eq!(
parse_key("A").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT)
);
assert_eq!(
parse_key("alt-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)
);
assert_eq!(
parse_key("alt-A").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT | KeyModifiers::SHIFT)
);
assert_eq!(
parse_key("alt-shift-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT | KeyModifiers::SHIFT)
);
assert_eq!(
parse_key("ctrl-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
);
assert_eq!(
parse_key("ctrl-A").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::SHIFT)
);
assert_eq!(
parse_key("ctrl-shift-a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::SHIFT)
);
assert_eq!(
parse_key("f10").unwrap(),
KeyEvent::new(KeyCode::F(10), KeyModifiers::empty())
);
assert_eq!(
parse_key("space").unwrap(),
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty())
);
assert_eq!(
parse_key("enter").unwrap(),
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
);
assert_eq!(
parse_key("bspace").unwrap(),
KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty())
);
assert_eq!(
parse_key("bs").unwrap(),
KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty())
);
assert_eq!(
parse_key("up").unwrap(),
KeyEvent::new(KeyCode::Up, KeyModifiers::empty())
);
assert_eq!(
parse_key("down").unwrap(),
KeyEvent::new(KeyCode::Down, KeyModifiers::empty())
);
assert_eq!(
parse_key("left").unwrap(),
KeyEvent::new(KeyCode::Left, KeyModifiers::empty())
);
assert_eq!(
parse_key("right").unwrap(),
KeyEvent::new(KeyCode::Right, KeyModifiers::empty())
);
assert_eq!(
parse_key("tab").unwrap(),
KeyEvent::new(KeyCode::Tab, KeyModifiers::empty())
);
assert_eq!(
parse_key("btab").unwrap(),
KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty())
);
assert_eq!(
parse_key("esc").unwrap(),
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
);
assert_eq!(
parse_key("home").unwrap(),
KeyEvent::new(KeyCode::Home, KeyModifiers::empty())
);
assert_eq!(
parse_key("end").unwrap(),
KeyEvent::new(KeyCode::End, KeyModifiers::empty())
);
assert_eq!(
parse_key("pgup").unwrap(),
KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty())
);
assert_eq!(
parse_key("pgdown").unwrap(),
KeyEvent::new(KeyCode::PageDown, KeyModifiers::empty())
);
assert_eq!(
parse_key("change").unwrap(),
KeyEvent::new(KeyCode::F(255), KeyModifiers::empty())
);
}
#[test]
fn skim_event_name_roundtrip() {
// Named events resolve to distinct reserved key events and back.
for (name, event) in [
("start", SkimEvent::Start),
("load", SkimEvent::Load),
("change", SkimEvent::Change),
("result", SkimEvent::Result),
("focus", SkimEvent::Focus),
("zero", SkimEvent::Zero),
("one", SkimEvent::One),
("double-click", SkimEvent::DoubleClick),
] {
assert_eq!(SkimEvent::from_name(name), Some(event));
assert_eq!(parse_key(name).unwrap(), KeyEvent::from(event));
}
// Unknown names are not events.
assert_eq!(SkimEvent::from_name("nope"), None);
// A binding referencing an event name resolves to an action chain.
let keymap = KeyMap::from("start:first,load:last,change:first,double-click:accept");
for event in [
SkimEvent::Start,
SkimEvent::Load,
SkimEvent::Change,
SkimEvent::DoubleClick,
] {
assert!(keymap.get(&event.key_event()).is_some());
}
}
#[test]
fn double_click_accepts_by_default() {
assert_eq!(
get_default_key_map().get(&SkimEvent::DoubleClick.key_event()),
Some(&vec![Accept(None)])
);
}
#[test]
fn parse_key_error_cases() {
// Empty input.
assert!(parse_key("").is_err());
// Unknown modifier.
assert!(parse_key("hyper-a").is_err());
// Unknown key name.
assert!(parse_key("notakey").is_err());
// Invalid function-key index.
assert!(parse_key("fXY").is_err());
}
#[test]
fn keymap_from_str_parses_bindings() {
let keymap = KeyMap::from("ctrl-a:abort,enter:accept");
// Both keys resolve to action chains.
assert!(keymap.get(&parse_key("ctrl-a").unwrap()).is_some());
assert!(keymap.get(&parse_key("enter").unwrap()).is_some());
}
#[test]
fn keymap_from_str_preserves_nested_bind_separators() {
let keymap = KeyMap::from("ctrl-z:bind(ctrl-x:abort+up),ctrl-w:unbind(ctrl-x,ctrl-y)");
assert_eq!(
keymap.get(&parse_key("ctrl-z").unwrap()),
Some(&vec![Bind("ctrl-x:abort+up".into())])
);
assert_eq!(
keymap.get(&parse_key("ctrl-w").unwrap()),
Some(&vec![Unbind("ctrl-x,ctrl-y".into())])
);
}
#[test]
fn parse_action_chain_preserves_nested_bind_chain() {
assert_eq!(
parse_action_chain("bind(ctrl-x:abort+up)").unwrap(),
vec![Bind("ctrl-x:abort+up".into())]
);
}
#[test]
fn parse_keymaps_collects_iterator() {
let keymap = parse_keymaps(["ctrl-x:abort", "up:up"].into_iter());
assert!(keymap.get(&parse_key("ctrl-x").unwrap()).is_some());
}
#[test]
fn action_binds_key_wins_over_action() {
// A bare action name that is not a key binds the action as a follow-up.
let binds = parse_action_binds(["first:last"].into_iter());
assert_eq!(binds.get("first"), Some(&vec![Last]));
// A name that is also a real key (`up`) is left to the key map, so it is
// NOT registered as an action trigger.
let binds = parse_action_binds(["up:down"].into_iter());
assert!(!binds.contains_key("up"));
// `act-` forces the action interpretation even for a key-shaped name.
let binds = parse_action_binds(["act-up:down"].into_iter());
assert_eq!(binds.get("up"), Some(&vec![Down(1)]));
// Actions that require arguments when executed are still valid triggers.
let binds = parse_action_binds(
[
"act-add-char:last",
"act-execute:last",
"act-execute-silent:last",
"act-set-preview-cmd:last",
"act-set-query:last",
]
.into_iter(),
);
for name in ["add-char", "execute", "execute-silent", "set-preview-cmd", "set-query"] {
assert_eq!(binds.get(name), Some(&vec![Last]), "missing trigger `{name}`");
}
}
#[test]
fn action_binds_parse_suppress_chain() {
// `suppress` is parsed like any other action and kept in the chain.
let binds = parse_action_binds(["act-up:suppress+down"].into_iter());
assert_eq!(binds.get("up"), Some(&vec![Suppress, Down(1)]));
}
#[test]
fn action_trigger_name_resolves_actions() {
// `act-` targets the action explicitly; a bare action name works too.
assert_eq!(action_trigger_name("act-up"), Some("up"));
assert_eq!(action_trigger_name("first"), Some("first"));
// Argument-taking actions resolve by name alone.
assert_eq!(action_trigger_name("act-execute"), Some("execute"));
// Unknown names are not triggers.
assert_eq!(action_trigger_name("nope"), None);
assert_eq!(action_trigger_name("act-nope"), None);
}
#[test]
fn action_binds_invalid_specs_are_skipped() {
// An unknown trigger and an invalid chain are both dropped (with a debug
// log) without affecting valid binds in the same list.
let binds = parse_action_binds(["nokey:last", "act-up:not-an-action", "first:last"].into_iter());
assert_eq!(binds.len(), 1);
assert_eq!(binds.get("first"), Some(&vec![Last]));
}
#[test]
fn action_binds_split_top_level_preserves_commas_in_args() {
// A single `--bind` spec containing a comma inside `(...)` must not be split
// there: `options.rs` uses `split_top_level(part, ',')` so the comma stays
// part of the action argument instead of garbling the follow-up binding.
let spec = "act-up:execute(echo a,b),first:last";
let binds = parse_action_binds(split_top_level(spec, ',').into_iter());
assert_eq!(binds.get("up"), Some(&vec![Execute(String::from("echo a,b"))]));
assert_eq!(binds.get("first"), Some(&vec![Last]));
}
#[test]
fn parse_action_chain_unknown_is_error() {
assert!(parse_action_chain("not-a-real-action").is_err());
}
#[test]
fn parse_action_chain_accept_execute_reload_with_args() {
// `accept:hello`, `execute(...)` and `reload(...)` parse to the expected actions.
assert_eq!(
parse_action_chain("accept:hello").unwrap(),
vec![Accept(Some("hello".into()))]
);
assert_eq!(
parse_action_chain("execute(echo foo)").unwrap(),
vec![Execute("echo foo".into())]
);
assert_eq!(
parse_action_chain("reload(echo hello)").unwrap(),
vec![Reload(Some("echo hello".into()))]
);
assert_eq!(parse_action_chain("reload").unwrap(), vec![Reload(None)]);
}

View file

@ -28,10 +28,10 @@ impl MatchAllEngine {
}
impl MatchEngine for MatchAllEngine {
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
let item_text = item.text();
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
let item_len = item.text().len();
Some(MatchResult {
rank: self.rank_builder.build_rank(0, 0, 0, &item_text),
rank: self.rank_builder.build_rank(0, 0, 0, item_len),
matched_range: MatchRange::ByteRange(0, 0),
})
}
@ -42,30 +42,3 @@ impl Display for MatchAllEngine {
write!(f, "Noop")
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
#[test]
fn matches_every_item_with_empty_range() {
let engine = MatchAllEngine::builder().build();
let result = engine.match_item(&"anything".to_string()).unwrap();
assert_eq!(result.matched_range, MatchRange::ByteRange(0, 0));
}
#[test]
fn rank_builder_override_is_used() {
let engine = MatchAllEngine::builder()
.rank_builder(Arc::new(RankBuilder::default()))
.build();
assert!(engine.match_item(&"x".to_string()).is_some());
}
#[test]
fn display_is_noop() {
let engine = MatchAllEngine::builder().build();
assert_eq!(format!("{engine}"), "Noop");
}
}

View file

@ -1,6 +1,6 @@
use std::fmt::{Display, Error, Formatter};
use std::sync::Arc;
use crate::fuzzy_matcher::MatchIndices;
use crate::{MatchEngine, MatchRange, MatchResult, SkimItem};
//------------------------------------------------------------------------------
@ -25,14 +25,15 @@ impl OrEngine {
}
impl MatchEngine for OrEngine {
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
let result = self
.engines
.iter()
.map(|e| e.match_item(item))
.max_by_key(|res| res.as_ref().map(|matched| matched.rank.score));
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
for engine in &self.engines {
let result = engine.match_item(Arc::clone(&item));
if result.is_some() {
return result;
}
}
result?
None
}
}
@ -43,7 +44,7 @@ impl Display for OrEngine {
"(Or: {})",
self.engines
.iter()
.map(|e| format!("{e}"))
.map(|e| format!("{}", e))
.collect::<Vec<_>>()
.join(", ")
)
@ -70,32 +71,21 @@ impl AndEngine {
self
}
fn merge_matched_items(items: Vec<MatchResult>, text: &str) -> MatchResult {
let mut ranges = MatchIndices::new();
let mut rank = crate::Rank {
score: 0,
begin: i32::MAX,
end: i32::MIN,
..items[0].rank
};
fn merge_matched_items(&self, items: Vec<MatchResult>, text: &str) -> MatchResult {
let rank = items[0].rank;
let mut ranges = vec![];
for item in items {
match item.matched_range {
MatchRange::ByteRange(..) => {
ranges.extend(item.range_char_indices(text));
}
MatchRange::CharRange(start, end) => {
ranges.extend(start..end);
}
MatchRange::Chars(vec) => {
ranges.extend(vec.iter().copied());
ranges.extend(vec.iter());
}
}
rank.score = rank.score.saturating_add(item.rank.score);
rank.begin = rank.begin.min(item.rank.begin);
rank.end = rank.end.max(item.rank.end);
}
ranges.sort_unstable();
ranges.sort();
ranges.dedup();
MatchResult {
rank,
@ -105,22 +95,18 @@ impl AndEngine {
}
impl MatchEngine for AndEngine {
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
// Fast path: single sub-engine — skip merge entirely.
if self.engines.len() == 1 {
return self.engines[0].match_item(item);
}
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
// mock
let mut results = vec![];
for engine in &self.engines {
let result = engine.match_item(item)?;
let result = engine.match_item(Arc::clone(&item))?;
results.push(result);
}
if results.is_empty() {
None
} else {
Some(Self::merge_matched_items(results, &item.text()))
Some(self.merge_matched_items(results, &item.text()))
}
}
}
@ -132,13 +118,9 @@ impl Display for AndEngine {
"(And: {})",
self.engines
.iter()
.map(|e| format!("{e}"))
.map(|e| format!("{}", e))
.collect::<Vec<_>>()
.join(", ")
)
}
}
#[cfg(test)]
#[path = "andor_tests.rs"]
mod tests;

View file

@ -1,101 +0,0 @@
use super::*;
use crate::engine::exact::{ExactEngine, ExactMatchingParam};
fn exact(query: &str) -> Box<dyn MatchEngine> {
Box::new(ExactEngine::builder(query, ExactMatchingParam::default()).build())
}
#[test]
fn or_engine_matches_if_any_subengine_matches() {
let engine = OrEngine::builder().engines(vec![exact("foo"), exact("zzz")]).build();
assert!(engine.match_item(&"a foo bar".to_string()).is_some());
}
#[test]
fn or_engine_returns_none_when_no_subengine_matches() {
let engine = OrEngine::builder().engines(vec![exact("xxx"), exact("zzz")]).build();
assert!(engine.match_item(&"a foo bar".to_string()).is_none());
}
#[test]
fn or_engine_empty_returns_none() {
let engine = OrEngine::builder().build();
assert!(engine.match_item(&"anything".to_string()).is_none());
}
#[test]
fn and_engine_single_engine_fast_path() {
let engine = AndEngine::builder().engines(vec![exact("foo")]).build();
assert!(engine.match_item(&"foobar".to_string()).is_some());
assert!(engine.match_item(&"nope".to_string()).is_none());
}
#[test]
fn and_engine_requires_all_subengines_to_match() {
let engine = AndEngine::builder().engines(vec![exact("foo"), exact("bar")]).build();
// Both substrings present -> matched, ranges merged.
let result = engine.match_item(&"foo and bar".to_string());
assert!(result.is_some());
let result = result.unwrap();
assert!(matches!(result.matched_range, MatchRange::Chars(_)));
// Missing one substring -> no match.
assert!(engine.match_item(&"foo only".to_string()).is_none());
}
#[test]
fn and_engine_empty_returns_none() {
// With no sub-engines the multi-engine path collects nothing and bails.
let engine = AndEngine::builder().build();
assert!(engine.match_item(&"anything".to_string()).is_none());
}
#[test]
fn display_formats_combinators() {
let or = OrEngine::builder().engines(vec![exact("a")]).build();
assert!(format!("{or}").starts_with("(Or:"));
let and = AndEngine::builder().engines(vec![exact("a")]).build();
assert!(format!("{and}").starts_with("(And:"));
}
fn result(range: MatchRange, score: i32, begin: i32, end: i32) -> MatchResult {
MatchResult {
rank: crate::Rank {
score,
begin,
end,
..Default::default()
},
matched_range: range,
}
}
#[test]
fn merge_handles_char_range_and_chars_variants() {
// CharRange expands to its index span; Chars copies indices verbatim.
// Scores are summed and begin/end take the widest span.
let merged = AndEngine::merge_matched_items(
vec![
result(MatchRange::CharRange(0, 2), 5, 0, 2),
result(MatchRange::Chars(vec![4, 5]), 3, 4, 5),
],
"abcdef",
);
assert_eq!(merged.rank.score, 8);
assert_eq!(merged.rank.begin, 0);
assert_eq!(merged.rank.end, 5);
assert_eq!(merged.matched_range, MatchRange::Chars(vec![0, 1, 4, 5]));
}
#[test]
fn merge_dedups_and_sorts_overlapping_ranges() {
let merged = AndEngine::merge_matched_items(
vec![
result(MatchRange::Chars(vec![3, 1]), 1, 1, 3),
result(MatchRange::CharRange(1, 3), 1, 1, 3),
],
"abcdef",
);
// Sorted and de-duplicated union of {3,1} and {1,2}.
assert_eq!(merged.matched_range, MatchRange::Chars(vec![1, 2, 3]));
}

Some files were not shown because too many files have changed in this diff Show more