m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 22:33:47 -05:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Junegunn Choi
a0ef8987fb 0.31.0 2022-07-21 22:46:34 +09:00
75 changed files with 2076 additions and 7678 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
github: junegunn custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]

View File

@@ -27,18 +27,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@1ed1437484560351c5be56cf73a48a279d116b78
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@1ed1437484560351c5be56cf73a48a279d116b78
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@1ed1437484560351c5be56cf73a48a279d116b78

View File

@@ -1,14 +0,0 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View File

@@ -11,32 +11,29 @@ on:
permissions: permissions:
contents: read contents: read
env:
LANG: C.UTF-8
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with: with:
go-version: 1.19 go-version: 1.18
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@ebaea52cb20fea395b0904125276395e37183dac
with: with:
ruby-version: 3.1.0 ruby-version: 3.0.0
- name: Install packages - name: Install packages
run: sudo apt-get install --yes zsh fish tmux run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems - name: Install Ruby gems
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2 run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
- name: Rubocop - name: Rubocop
run: rubocop --require rubocop-minitest --require rubocop-performance run: rubocop --require rubocop-minitest --require rubocop-performance
@@ -45,7 +42,4 @@ jobs:
run: make test run: make test
- name: Integration test - name: Integration test
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
- name: Integration test (tcell)
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose

View File

@@ -15,17 +15,17 @@ jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with: with:
go-version: 1.18 go-version: 1.18
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@ebaea52cb20fea395b0904125276395e37183dac
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0

View File

@@ -1,24 +0,0 @@
---
name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 0
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_TOKEN }}
file: 'README.md'
- name: Deploy to GitHub Pages 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: master
folder: '.'

View File

@@ -1,10 +0,0 @@
name: "Spell Check"
on: [pull_request]
jobs:
typos:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: crate-ci/typos@v1.17.2

View File

@@ -1,15 +0,0 @@
name: Publish to Winget
on:
release:
types: [released]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: junegunn.fzf
version: ${{ github.event.release.tag_name }}
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -18,7 +18,7 @@ builds:
post: | post: |
sh -c ' sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"] source = ["./dist/fzf-macos_darwin_amd64/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "kr.junegunn.fzf"
apple_id { apple_id {
username = "junegunn.c@gmail.com" username = "junegunn.c@gmail.com"
@@ -72,9 +72,6 @@ builds:
- amd64 - amd64
- arm - arm
- arm64 - arm64
- loong64
- ppc64le
- s390x
goarm: goarm:
- 5 - 5
- 6 - 6
@@ -102,10 +99,6 @@ archives:
files: files:
- non-existent* - non-existent*
checksum:
extra_files:
- glob: ./dist/fzf-*darwin*.zip
release: release:
github: github:
owner: junegunn owner: junegunn

View File

@@ -26,7 +26,3 @@ Style/OptionalBooleanParameter:
Enabled: false Enabled: false
Style/WordArray: Style/WordArray:
MinSize: 1 MinSize: 1
Minitest/AssertEqual:
Enabled: false
Naming/VariableNumber:
Enabled: false

View File

@@ -1 +1 @@
golang 1.20.13 golang 1.18

View File

@@ -1,36 +1,30 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2024/01/20* *(Last update: 2021/05/22)*
* *Requires fzf 0.46.0 or above*
---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Introduction](#introduction) * [Introduction](#introduction)
* [Screen Layout](#screen-layout) * [Screen Layout](#screen-layout)
* [`--height`](#--height) * [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux) * [`fzf-tmux`](#fzf-tmux)
* [Popup window support](#popup-window-support) * [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list) * [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r) * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources) * [Toggling between data sources](#toggling-between-data-sources)
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
* [Ripgrep integration](#ripgrep-integration) * [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter) * [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher) * [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode) * [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
* [Log tailing](#log-tailing) * [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects) * [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status) * [Files listed in `git status`](#files-listed-in-git-status)
* [Branches](#branches) * [Branches](#branches)
* [Commit hashes](#commit-hashes) * [Commit hashes](#commit-hashes)
* [Color themes](#color-themes) * [Color themes](#color-themes)
* [fzf Theme Playground](#fzf-theme-playground) * [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
<!-- vim-markdown-toc --> <!-- vim-markdown-toc -->
@@ -211,30 +205,6 @@ find * | fzf --prompt 'All> ' \
![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png) ![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png)
### Toggling with a single key binding
The above example uses two different key bindings to toggle between two modes,
but can we just use a single key binding?
To make a key binding behave differently each time it is pressed, we need:
1. a way to store the current state. i.e. "which mode are we in?"
2. and a way to dynamically perform different actions depending on the state.
The following example shows how to 1. store the current mode in the prompt
string, 2. and use this information (`$FZF_PROMPT`) to determine which
actions to perform using the `transform` action.
```sh
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
--preview '[[ $FZF_PROMPT =~ Files ]] && bat --color=always {} || tree -C {}'
```
Ripgrep integration Ripgrep integration
------------------- -------------------
@@ -266,13 +236,15 @@ file called `rfv`.
# 1. Search for text in files using Ripgrep # 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf # 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim # 3. Open the file in Vim
rg --color=always --line-number --no-heading --smart-case "${*:-}" | IFS=: read -ra selected < <(
fzf --ansi \ rg --color=always --line-number --no-heading --smart-case "${*:-}" |
--color "hl:-1:underline,hl+:-1:underline:reverse" \ fzf --ansi \
--delimiter : \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--preview 'bat --color=always {1} --highlight-line {2}' \ --delimiter : \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--bind 'enter:become(vim {1} +{2})' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
And run it with an initial query string. And run it with an initial query string.
@@ -335,14 +307,10 @@ I know it's a lot to digest, let's try to break down the code.
position in the window position in the window
- `~3` makes the top three lines fixed header so that they are always - `~3` makes the top three lines fixed header so that they are always
visible regardless of the scroll offset visible regardless of the scroll offset
- Instead of using shell script to process the final output of fzf, we use - Once we selected a line, we open the file with `vim` (`vim
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf "${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
into a new process that opens the file with `vim` (`vim {1}`) and move the
cursor to the line (`+{2}`).
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380 ### Using fzf as interative Ripgrep launcher
### Using fzf as interactive Ripgrep launcher
We have learned that we can bind `reload` action to a key (e.g. We have learned that we can bind `reload` action to a key (e.g.
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind `--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
@@ -363,22 +331,25 @@ projects, and it will free up memory as you narrow down the results.
# 3. Open the file in Vim # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ IFS=: read -ra selected < <(
--bind "start:reload:$RG_PREFIX {q}" \ FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ fzf --ansi \
--delimiter : \ --disabled --query "$INITIAL_QUERY" \
--preview 'bat --color=always {1} --highlight-line {2}' \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \ --delimiter : \
--bind 'enter:become(vim {1} +{2})' --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png) ![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with - Instead of starting fzf in `rg ... | fzf` form, we start fzf without an
an empty input (`: | fzf`), then we make it start the initial Ripgrep explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way
process immediately via `start:reload` binding. This way, fzf owns the fzf can kill the initial Ripgrep process it starts with the initial query.
initial Ripgrep process so it can kill it on the next `reload`. Otherwise, Otherwise, the initial Ripgrep process will keep consuming system resources
the process will keep running in the background. even after `reload` is triggered.
- Filtering is no longer a responsibility of fzf; hence `--disabled` - Filtering is no longer a responsibility of fzf; hence `--disabled`
- `{q}` in the reload command evaluates to the query string on fzf prompt. - `{q}` in the reload command evaluates to the query string on fzf prompt.
- `sleep 0.1` in the reload command is for "debouncing". This small delay will - `sleep 0.1` in the reload command is for "debouncing". This small delay will
@@ -387,6 +358,8 @@ INITIAL_QUERY="${*:-}"
### Switching to fzf-only search mode ### Switching to fzf-only search mode
*(Requires fzf 0.27.1 or above)*
In the previous example, we lost fuzzy matching capability as we completely In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by *"unbinding"* `reload` action from `change` event. fzf-only search mode by *"unbinding"* `reload` action from `change` event.
@@ -402,16 +375,19 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
# 3. Open the file in Vim # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ IFS=: read -ra selected < <(
--bind "start:reload:$RG_PREFIX {q}" \ FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ fzf --ansi \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --disabled --query "$INITIAL_QUERY" \
--prompt '1. ripgrep> ' \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \ --bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--preview 'bat --color=always {1} --highlight-line {2}' \ --prompt '1. ripgrep> ' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \ --delimiter : \
--bind 'enter:become(vim {1} +{2})' --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
* Phase 1. Filtering with Ripgrep * Phase 1. Filtering with Ripgrep
@@ -432,8 +408,10 @@ INITIAL_QUERY="${*:-}"
### Switching between Ripgrep mode and fzf mode ### Switching between Ripgrep mode and fzf mode
[fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings *(Requires fzf 0.30.0 or above)*
that were previously "unbound" via `unbind`.
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
@@ -443,65 +421,23 @@ CTRL-F.
#!/usr/bin/env bash #!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F) # Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \ IFS=: read -ra selected < <(
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \ FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ fzf --ansi \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \ --disabled --query "$INITIAL_QUERY" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--prompt '1. ripgrep> ' \ --bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
--delimiter : \ --bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
--header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \ --prompt '1. Ripgrep> ' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --delimiter : \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \ --header ' CTRL-R (Ripgrep mode) CTRL-F (fzf mode) ' \
--bind 'enter:become(vim {1} +{2})' --preview 'bat --color=always {1} --highlight-line {2}' \
``` --preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
- To restore the query string when switching between modes, we store the [ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
- Also note that we unbind `ctrl-r` binding on `start` event which is
triggered once when fzf starts.
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
### Switching between Ripgrep mode and fzf mode using a single key binding
In contrast to the previous version, we use just one hotkey to toggle between
ripgrep and fzf mode. This is achieved by using the `$FZF_PROMPT` as a state
within the `transform` action, a feature introduced in [fzf 0.45.0][0.45.0]. A
more detailed explanation of this feature can be found in a previous section -
[Toggling with a single keybinding](#toggling-with-a-single-key-binding).
[0.45.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0450
When using the `transform` action, the placeholder (`\{q}`) should be escaped to
prevent immediate evaluation.
```sh
#!/usr/bin/env bash
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
echo "rebind(change)+change-prompt(1. ripgrep> )+disable-search+transform-query:echo \{q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r" ||
echo "unbind(change)+change-prompt(2. fzf> )+enable-search+transform-query:echo \{q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f"' \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--header 'CTRL-T: Switch between ripgrep/fzf' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
``` ```
Log tailing Log tailing
@@ -529,17 +465,16 @@ Kubernetes pods.
```bash ```bash
pods() { pods() {
: | command='kubectl get pods --all-namespaces' fzf \ FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
--info=inline --layout=reverse --header-lines=1 \ fzf --info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \ --prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \ --header $' Enter (kubectl exec) CTRL-O (open log in editor) CTRL-R (reload) \n\n' \
--bind 'start:reload:$command' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'ctrl-r:reload:$command' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \ --bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \ --preview-window up:follow \
--preview-window up:follow \ --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
} }
``` ```
@@ -561,17 +496,9 @@ pods() {
Key bindings for git objects Key bindings for git objects
---------------------------- ----------------------------
Oftentimes, you want to put the identifiers of various Git object to the I have [blogged](https://junegunn.kr/2016/07/fzf-git) about my fzf+git key
command-line. For example, it is common to write commands like these: bindings a few years ago. I'm going to show them here again, because they are
seriously useful.
```sh
git checkout [SOME_COMMIT_HASH or BRANCH or TAG]
git diff [SOME_COMMIT_HASH or BRANCH or TAG] [SOME_COMMIT_HASH or BRANCH or TAG]
```
[fzf-git.sh](https://github.com/junegunn/fzf-git.sh) project defines a set of
fzf-based key bindings for Git objects. I strongly recommend that you check
them out because they are seriously useful.
### Files listed in `git status` ### Files listed in `git status`
@@ -591,6 +518,9 @@ them out because they are seriously useful.
![image](https://user-images.githubusercontent.com/700826/113473765-91692080-94a6-11eb-8d38-ed4d41f27ac1.png) ![image](https://user-images.githubusercontent.com/700826/113473765-91692080-94a6-11eb-8d38-ed4d41f27ac1.png)
The full source code can be found [here](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236).
Color themes Color themes
------------ ------------
@@ -633,12 +563,6 @@ export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#
![molokai](https://user-images.githubusercontent.com/700826/113475085-8619f300-94ae-11eb-85e4-2766fc3246bf.png) ![molokai](https://user-images.githubusercontent.com/700826/113475085-8619f300-94ae-11eb-85e4-2766fc3246bf.png)
### fzf Theme Playground
[fzf Theme Playground](https://vitormv.github.io/fzf-themes/) created by
[Vitor Mello](https://github.com/vitormv) is a webpage where you can
interactively create fzf themes.
### Generating fzf color theme from Vim color schemes ### Generating fzf color theme from Vim color schemes
The Vim plugin of fzf can generate `--color` option from the current color The Vim plugin of fzf can generate `--color` option from the current color

View File

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites ### Prerequisites
- Go 1.18 or above - Go 1.17 or above
### Using Makefile ### Using Makefile
@@ -34,8 +34,8 @@ make release
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
- [rivo/uniseg](https://github.com/rivo/uniseg) - [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-isatty](https://github.com/mattn/go-isatty) - [mattn/go-isatty](https://github.com/mattn/go-isatty)

View File

@@ -1,632 +1,6 @@
CHANGELOG CHANGELOG
========= =========
0.46.1
------
- Bug fixes and improvements
- Fixed Windows binaries
- Downgraded Go version to 1.20 to support older versions of Windows
- https://tip.golang.org/doc/go1.21#windows
- Updated [rivo/uniseg](https://github.com/rivo/uniseg) dependency to v0.4.6
0.46.0
------
- Added two new events
- `result` - triggered when the filtering for the current query is complete and the result list is ready
- `resize` - triggered when the terminal size is changed
- fzf now exports the following environment variables to the child processes
| Variable | Description |
| --- | --- |
| `FZF_LINES` | Number of lines fzf takes up excluding padding and margin |
| `FZF_COLUMNS` | Number of columns fzf takes up excluding padding and margin |
| `FZF_TOTAL_COUNT` | Total number of items |
| `FZF_MATCH_COUNT` | Number of matched items |
| `FZF_SELECT_COUNT` | Number of selected items |
| `FZF_QUERY` | Current query string |
| `FZF_PROMPT` | Prompt string |
| `FZF_ACTION` | The name of the last action performed |
- This allows you to write sophisticated transformations like so
```sh
# Script to dynamically resize the preview window
transformer='
# 1 line for info, another for prompt, and 2 more lines for preview window border
lines=$(( FZF_LINES - FZF_MATCH_COUNT - 4 ))
if [[ $FZF_MATCH_COUNT -eq 0 ]]; then
echo "change-preview-window:hidden"
elif [[ $lines -gt 3 ]]; then
echo "change-preview-window:$lines"
elif [[ $FZF_PREVIEW_LINES -ne 3 ]]; then
echo "change-preview-window:3"
fi
'
seq 10000 | fzf --preview 'seq {} 10000' --preview-window up \
--bind "result:transform:$transformer" \
--bind "resize:transform:$transformer"
```
- And we're phasing out `{fzf:prompt}` and `{fzf:action}`
- Changed [mattn/go-runewidth](https://github.com/mattn/go-runewidth) dependency to [rivo/uniseg](https://github.com/rivo/uniseg) for accurate results
- Set `--ambidouble` if your terminal displays ambiguous width characters (e.g. box-drawing characters for borders) as 2 columns
- `RUNEWIDTH_EASTASIAN=1` is still respected for backward compatibility, but it's recommended that you use this new option instead
- Bug fixes
0.45.0
------
- Added `transform` action to conditionally perform a series of actions
```sh
# Disallow selecting an empty line
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'
# Move cursor past the empty line
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
--bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'
# A single key binding to toggle between modes
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"'
```
- Added placeholder expressions
- `{fzf:action}` - The name of the last action performed
- `{fzf:prompt}` - Prompt string (including ANSI color codes)
- `{fzf:query}` - Synonym for `{q}`
- Added support for negative height
```sh
# Terminal height minus 1, so you can still see the command line
fzf --height=-1
```
- This handles a terminal resize better than `--height=$(($(tput lines) - 1))`
- Added `accept-or-print-query` action that acts like `accept` but prints the
current query when there's no match for the query
```sh
# You can make CTRL-R paste the current query when there's no match
export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query'
```
- Note that there are alternative ways to implement the same strategy
```sh
# 'become' is apparently more versatile but it's not available on Windows.
export FZF_CTRL_R_OPTS='--bind "enter:become:if [ -z {} ]; then echo {q}; else echo {}; fi"'
# Using the new 'transform' action
export FZF_CTRL_R_OPTS='--bind "enter:transform:[ -z {} ] && echo print-query || echo accept"'
```
- Added `show-header` and `hide-header` actions
- Bug fixes
0.44.1
------
- Fixed crash when preview window is hidden on `focus` event
0.44.0
------
- (Experimental) Sixel image support in preview window (not available on Windows)
- [bin/fzf-preview.sh](bin/fzf-preview.sh) is added to demonstrate how to
display an image using Kitty image protocol or Sixel. You can use it
like so:
```sh
fzf --preview='fzf-preview.sh {}'
```
- (Experimental) iTerm2 inline image protocol support in preview window (not available on Windows)
```sh
# Using https://iterm2.com/utilities/imgcat
fzf --preview 'imgcat -W $FZF_PREVIEW_COLUMNS -H $FZF_PREVIEW_LINES {}'
```
- HTTP server can be configured to accept remote connections
```sh
# FZF_API_KEY is required for a non-localhost listen address
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 0.0.0.0:6266
```
- To allow remote process execution, use `--listen-unsafe` instead
(`execute*`, `reload*`, `become`, `preview`, `change-preview`, `transform-*`)
```sh
fzf --listen-unsafe 0.0.0.0:6266
```
- Bug fixes
0.43.0
------
- (Experimental) Added support for Kitty image protocol in the preview window
(not available on Windows)
```sh
fzf --preview='
if file --mime-type {} | grep -qF image/; then
# --transfer-mode=memory is the fastest option but if you want fzf to be able
# to redraw the image on terminal resize or on 'change-preview-window',
# you need to use --transfer-mode=stream.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \$d
else
bat --color=always {}
fi
'
```
- (Experimental) `--listen` server can report program state in JSON format (`GET /`)
```sh
# fzf server started in "headless" mode
fzf --listen 6266 2> /dev/null
# Get program state
curl localhost:6266 | jq .
# Increase the number of items returned (default: 100)
curl localhost:6266?limit=1000 | jq .
```
- `--listen` server can be secured by setting `$FZF_API_KEY` environment
variable.
```sh
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
# Server
fzf --listen 6266
# Client
curl localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
```
- Added `toggle-header` action
- Added mouse events for `--bind`
- `scroll-up` (bound to `up`)
- `scroll-down` (bound to `down`)
- `shift-scroll-up` (bound to `toggle+up`)
- `shift-scroll-down` (bound to `toggle+down`)
- `shift-left-click` (bound to `toggle`)
- `shift-right-click` (bound to `toggle`)
- `preview-scroll-up` (bound to `preview-up`)
- `preview-scroll-down` (bound to `preview-down`)
```sh
# Twice faster scrolling both in the main window and the preview window
fzf --bind 'scroll-up:up+up,scroll-down:down+down' \
--bind 'preview-scroll-up:preview-up+preview-up' \
--bind 'preview-scroll-down:preview-down+preview-down' \
--preview 'cat {}'
```
- Added `offset-up` and `offset-down` actions
```sh
# Scrolling will behave similarly to CTRL-E and CTRL-Y of vim
fzf --bind scroll-up:offset-up,scroll-down:offset-down \
--bind ctrl-y:offset-up,ctrl-e:offset-down \
--scroll-off=5
```
- Shell extensions
- Updated bash completion for fzf options
- bash key bindings no longer requires perl; it will use awk or mawk
instead if perl is not found
- Basic context-aware completion for ssh command
- Applied `--scheme=path` for better ordering of the result
- Bug fixes and improvements
0.42.0
------
- Added new info style: `--info=right`
- Added new info style: `--info=inline-right`
- Added new border style `thinblock` which uses symbols for legacy computing
[one eighth block elements](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing)
- Similarly to `block`, this style is suitable when using a different
background color because the window is completely contained within the border.
```sh
BAT_THEME=GitHub fzf --info=right --border=thinblock --preview-window=border-thinblock \
--margin=3 --scrollbar=▏▕ --preview='bat --color=always --style=numbers {}' \
--color=light,query:238,fg:238,bg:251,bg+:249,gutter:251,border:248,preview-bg:253
```
- This style may not render correctly depending on the font and the
terminal emulator.
0.41.1
------
- Fixed a bug where preview window is not updated when `--disabled` is set and
a reload is triggered by `change:reload` binding
0.41.0
------
- Added color name `preview-border` and `preview-scrollbar`
- Added new border style `block` which uses [block elements](https://en.wikipedia.org/wiki/Block_Elements)
- `--scrollbar` can take two characters, one for the main window, the other
for the preview window
- Putting it altogether:
```sh
fzf-tmux -p 80% --padding 1,2 --preview 'bat --style=plain --color=always {}' \
--color 'bg:237,bg+:235,gutter:237,border:238,scrollbar:236' \
--color 'preview-bg:235,preview-border:236,preview-scrollbar:234' \
--preview-window 'border-block' --border block --scrollbar '▌▐'
```
- Bug fixes and improvements
0.40.0
------
- Added `zero` event that is triggered when there's no match
```sh
# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
```
- New actions
- Added `track` action which makes fzf track the current item when the
search result is updated. If the user manually moves the cursor, or the
item is not in the updated search result, tracking is automatically
disabled. Tracking is useful when you want to see the surrounding items
by deleting the query string.
```sh
# Narrow down the list with a query, point to a command,
# and hit CTRL-T to see its surrounding commands.
export FZF_CTRL_R_OPTS="
--preview 'echo {}' --preview-window up:3:hidden:wrap
--bind 'ctrl-/:toggle-preview'
--bind 'ctrl-t:track+clear-query'
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--color header:italic
--header 'Press CTRL-Y to copy command into clipboard'"
```
- Added `change-header(...)`
- Added `transform-header(...)`
- Added `toggle-track` action
- Fixed `--track` behavior when used with `--tac`
- However, using `--track` with `--tac` is not recommended. The resulting
behavior can be very confusing.
- Bug fixes and improvements
0.39.0
------
- Added `one` event that is triggered when there's only one match
```sh
# Automatically select the only match
seq 10 | fzf --bind one:accept
```
- Added `--track` option that makes fzf track the current selection when the
result list is updated. This can be useful when browsing logs using fzf with
sorting disabled.
```sh
git log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list
```
- If you use `--listen` option without a port number fzf will automatically
allocate an available port and export it as `$FZF_PORT` environment
variable.
```sh
# Automatic port assignment
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
# Say hello
curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.'
```
- A carriage return and a line feed character will be rendered as dim ␍ and
␊ respectively.
```sh
printf "foo\rbar\nbaz" | fzf --read0 --preview 'echo {}'
```
- fzf will stop rendering a non-displayable characters as a space. This will
likely cause less glitches in the preview window.
```sh
fzf --preview 'head -1000 /dev/random'
```
- Bug fixes and improvements
0.38.0
------
- New actions
- `become(...)` - Replace the current fzf process with the specified
command using `execve(2)` system call.
See https://github.com/junegunn/fzf#turning-into-a-different-process for
more information.
```sh
# Open selected files in Vim
fzf --multi --bind 'enter:become(vim {+})'
# Open the file in Vim and go to the line
git grep --line-number . |
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
```
- This action is not supported on Windows
- `show-preview`
- `hide-preview`
- Bug fixes
- `--preview-window 0,hidden` should not execute the preview command until
`toggle-preview` action is triggered
0.37.0
------
- Added a way to customize the separator of inline info
```sh
fzf --info 'inline: ' --prompt ' ' --color prompt:bright-yellow
```
- New event
- `focus` - Triggered when the focus changes due to a vertical cursor
movement or a search result update
```sh
fzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle
```
- New actions
- `change-border-label`
- `change-preview-label`
- `transform-border-label`
- `transform-preview-label`
- Bug fixes and improvements
0.36.0
------
- Added `--listen=HTTP_PORT` option to start HTTP server. It allows external
processes to send actions to perform via POST method.
```sh
# Start HTTP server on port 6266
fzf --listen 6266
# Send actions to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
```
- Added draggable scrollbar to the main search window and the preview window
```sh
# Hide scrollbar
fzf --no-scrollbar
# Customize scrollbar
fzf --scrollbar ┆ --color scrollbar:blue
```
- New event
- Added `load` event that is triggered when the input stream is complete
and the initial processing of the list is complete.
```sh
# Change the prompt to "loaded" when the input stream is complete
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '
# You can use it instead of 'start' event without `--sync` if asynchronous
# trigger is not an issue.
(seq 10; sleep 1; seq 11 20) | fzf --bind 'load:last'
```
- New actions
- Added `pos(...)` action to move the cursor to the numeric position
- `first` and `last` are equivalent to `pos(1)` and `pos(-1)` respectively
```sh
# Put the cursor on the 10th item
seq 100 | fzf --sync --bind 'start:pos(10)'
# Put the cursor on the 10th to last item
seq 100 | fzf --sync --bind 'start:pos(-10)'
```
- Added `reload-sync(...)` action which replaces the current list only after
the reload process is complete. This is useful when the command takes
a while to produce the initial output and you don't want fzf to run against
an empty list while the command is running.
```sh
# You can still filter and select entries from the initial list for 3 seconds
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'
```
- Added `next-selected` and `prev-selected` actions to move between selected
items
```sh
# `next-selected` will move the pointer to the next selected item below the current line
# `prev-selected` will move the pointer to the previous selected item above the current line
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected
# Both actions respect --layout option
seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected --layout reverse
```
- Added `change-query(...)` action that simply changes the query string to the
given static string. This can be useful when used with `--listen`.
```sh
curl localhost:6266 -d "change-query:$(date)"
```
- Added `transform-prompt(...)` action for transforming the prompt string
using an external command
```sh
# Press space to change the prompt string using an external command
# (only the first line of the output is taken)
fzf --bind 'space:reload(ls),load:transform-prompt(printf "%s> " "$(date)")'
```
- Added `transform-query(...)` action for transforming the query string using
an external command
```sh
# Press space to convert the query to uppercase letters
fzf --bind 'space:transform-query(tr "[:lower:]" "[:upper:]" <<< {q})'
# Bind it to 'change' event for automatic conversion
fzf --bind 'change:transform-query(tr "[:lower:]" "[:upper:]" <<< {q})'
# Can only type numbers
fzf --bind 'change:transform-query(sed "s/[^0-9]//g" <<< {q})'
```
- `put` action can optionally take an argument string
```sh
# a will put 'alpha' on the prompt, ctrl-b will put 'bravo'
fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)'
```
- Added color name `preview-label` for `--preview-label` (defaults to `label`
for `--border-label`)
- Better support for (Windows) terminals where each box-drawing character
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
- Behavior changes
- fzf will always execute the preview command if the command template
contains `{q}` even when it's empty. If you prefer the old behavior,
you'll have to check if `{q}` is empty in your command.
```sh
# This will show // even when the query is empty
: | fzf --preview 'echo /{q}/'
# But if you don't want it,
: | fzf --preview '[ -n {q} ] || exit; echo /{q}/'
```
- `double-click` will behave the same as `enter` unless otherwise specified,
so you don't have to repeat the same action twice in `--bind` in most cases.
```sh
# No need to bind 'double-click' to the same action
fzf --bind 'enter:execute:less {}' # --bind 'double-click:execute:less {}'
```
- If the color for `separator` is not specified, it will default to the
color for `border`. Same holds true for `scrollbar`. This is to reduce
the number of configuration items required to achieve a consistent color
scheme.
- If `follow` flag is specified in `--preview-window` option, fzf will
automatically scroll to the bottom of the streaming preview output. But
when the user manually scrolls the window, the following stops. With
this version, fzf will resume following if the user scrolls the window
to the bottom.
- Default border style on Windows is changed to `sharp` because some
Windows terminals are not capable of displaying `rounded` border
characters correctly.
- Minor bug fixes and improvements
0.35.1
------
- Fixed a bug where fzf with `--tiebreak=chunk` crashes on inverse match query
- Fixed a bug where clicking above fzf would paste escape sequences
0.35.0
------
- Added `start` event that is triggered only once when fzf finder starts.
Since fzf consumes the input stream asynchronously, the input list is not
available unless you use `--sync`.
```sh
seq 100 | fzf --multi --sync --bind 'start:last+select-all+preview(echo welcome)'
```
- Added `--border-label` and `--border-label-pos` for putting label on the border
```sh
# ANSI color codes are supported
# (with https://github.com/busyloop/lolcat)
label=$(curl -s http://metaphorpsum.com/sentences/1 | lolcat -f)
# Border label at the center
fzf --height=10 --border --border-label="╢ $label ╟" --color=label:italic:black
# Left-aligned (positive integer)
fzf --height=10 --border --border-label="╢ $label ╟" --border-label-pos=3 --color=label:italic:black
# Right-aligned (negative integer) on the bottom line (:bottom)
fzf --height=10 --border --border-label="╢ $label ╟" --border-label-pos=-3:bottom --color=label:italic:black
```
- Also added `--preview-label` and `--preview-label-pos` for the border of the
preview window
```sh
fzf --preview 'cat {}' --border --preview-label=' Preview ' --preview-label-pos=2
```
- Info panel (match counter) will be followed by a horizontal separator by
default
- Use `--no-separator` or `--separator=''` to hide the separator
- You can specify an arbitrary string that is repeated to form the
horizontal separator. e.g. `--separator=╸`
- The color of the separator can be customized via `--color=separator:...`
- ANSI color codes are also supported
```sh
fzf --separator=╸ --color=separator:green
fzf --separator=$(lolcat -f -F 1.4 <<< ▁▁▂▃▄▅▆▆▅▄▃▂▁▁) --info=inline
```
- Added `--border=bold` and `--border=double` along with
`--preview-window=border-bold` and `--preview-window=border-double`
0.34.0
------
- Added support for adaptive `--height`. If the `--height` value is prefixed
with `~`, fzf will automatically determine the height in the range according
to the input size.
```sh
seq 1 | fzf --height ~70% --border --padding 1 --margin 1
seq 10 | fzf --height ~70% --border --padding 1 --margin 1
seq 100 | fzf --height ~70% --border --padding 1 --margin 1
```
- There are a few limitations
- Not compatible with percent top/bottom margin/padding
```sh
# This is not allowed (top/bottom margin in percent value)
fzf --height ~50% --border --margin 5%,10%
# This is allowed (top/bottom margin in fixed value)
fzf --height ~50% --border --margin 2,10%
```
- fzf will not start until it can determine the right height for the input
```sh
# fzf will open immediately
(sleep 2; seq 10) | fzf --height 50%
# fzf will open after 2 seconds
(sleep 2; seq 10) | fzf --height ~50%
(sleep 2; seq 1000) | fzf --height ~50%
```
- Fixed tcell renderer used to render full-screen fzf on Windows
- ~~`--no-clear` is deprecated. Use `reload` action instead.~~
0.33.0
------
- Added `--scheme=[default|path|history]` option to choose scoring scheme
- (Experimental)
- We updated the scoring algorithm in 0.32.0, however we have learned that
this new scheme (`default`) is not always giving the optimal result
- `path`: Additional bonus point is only given to the characters after
path separator. You might want to choose this scheme if you have many
files with spaces in their paths.
- `history`: No additional bonus points are given so that we give more
weight to the chronological ordering. This is equivalent to the scoring
scheme before 0.32.0. This also sets `--tiebreak=index`.
- ANSI color sequences with colon delimiters are now supported.
```sh
printf "\e[38;5;208mOption 1\e[m\nOption 2" | fzf --ansi
printf "\e[38:5:208mOption 1\e[m\nOption 2" | fzf --ansi
```
- Support `border-{up,down}` as the synonyms for `border-{top,bottom}` in
`--preview-window`
- Added support for ANSI `strikethrough`
```sh
printf "\e[9mdeleted" | fzf --ansi
fzf --color fg+:strikethrough
```
0.32.1
------
- Fixed incorrect ordering of `--tiebreak=chunk`
- fzf-tmux will show fzf border instead of tmux popup border (requires tmux 3.3)
```sh
fzf-tmux -p70%
fzf-tmux -p70% --color=border:bright-red
fzf-tmux -p100%,60% --color=border:bright-yellow --border=horizontal --padding 1,5 --margin 1,0
fzf-tmux -p70%,100% --color=border:bright-green --border=vertical
# Key bindings (CTRL-T, CTRL-R, ALT-C) will use these options
export FZF_TMUX_OPTS='-p100%,60% --color=border:green --border=horizontal --padding 1,5 --margin 1,0'
```
0.32.0
------
- Updated the scoring algorithm
- Different bonus points to different categories of word boundaries
(listed higher to lower bonus point)
- Word after whitespace characters or beginning of the string
- Word after common delimiter characters (`/,:;|`)
- Word after other non-word characters
```sh
# foo/bar.sh` is preferred over `foo-bar.sh` on `bar`
fzf --query=bar --height=4 << EOF
foo-bar.sh
foo/bar.sh
EOF
```
- Added a new tiebreak `chunk`
- Favors the line with shorter matched chunk. A chunk is a set of
consecutive non-whitespace characters.
- Unlike the default `length`, this scheme works well with tabular input
```sh
# length prefers item #1, because the whole line is shorter,
# chunk prefers item #2, because the matched chunk ("foo") is shorter
fzf --height=6 --header-lines=2 --tiebreak=chunk --reverse --query=fo << "EOF"
N | Field1 | Field2 | Field3
- | ------ | ------ | ------
1 | hello | foobar | baz
2 | world | foo | bazbaz
EOF
```
- If the input does not contain any spaces, `chunk` is equivalent to
`length`. But we're not going to set it as the default because it is
computationally more expensive.
- Bug fixes and improvements
0.31.0 0.31.0
------ ------
- Added support for an alternative preview window layout that is activated - Added support for an alternative preview window layout that is activated

View File

@@ -1,5 +1,5 @@
FROM --platform=linux/amd64 ubuntu:22.04 FROM archlinux
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
RUN gem install --no-document -v 5.14.2 minitest RUN gem install --no-document -v 5.14.2 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile RUN echo '. ~/.bashrc' >> ~/.bash_profile
@@ -8,5 +8,4 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
RUN rm -f /etc/bash.bashrc RUN rm -f /etc/bash.bashrc
COPY . /fzf COPY . /fzf
RUN cd /fzf && make install && ./install --all RUN cd /fzf && make install && ./install --all
ENV LANG C.UTF-8
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ] CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2021 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -20,7 +20,7 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
ifdef FZF_REVISION ifdef FZF_REVISION
REVISION := $(FZF_REVISION) REVISION := $(FZF_REVISION)
else else
REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null) REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null)
endif endif
ifeq ($(REVISION),) ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION) $(error Not on git repository; cannot determine $$FZF_REVISION)
@@ -29,7 +29,6 @@ BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYS390 := fzf-$(GOOS)_s390x
BINARYARM5 := fzf-$(GOOS)_arm5 BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
@@ -44,8 +43,6 @@ ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64) else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64) BINARY := $(BINARY64)
else ifeq ($(UNAME_M),s390x)
BINARY := $(BINARYS390)
else ifeq ($(UNAME_M),i686) else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32) BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386) else ifeq ($(UNAME_M),i386)
@@ -57,9 +54,7 @@ else ifeq ($(UNAME_M),armv6l)
else ifeq ($(UNAME_M),armv7l) else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7) BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),armv8l) else ifeq ($(UNAME_M),armv8l)
# armv8l is always 32-bit and should implement the armv7 ISA, so BINARY := $(BINARYARM8)
# just use the same filename as for armv7.
BINARY := $(BINARYARM7)
else ifeq ($(UNAME_M),arm64) else ifeq ($(UNAME_M),arm64)
BINARY := $(BINARYARM8) BINARY := $(BINARYARM8)
else ifeq ($(UNAME_M),aarch64) else ifeq ($(UNAME_M),aarch64)
@@ -89,17 +84,10 @@ bench:
install: bin/fzf install: bin/fzf
generate:
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
build: build:
goreleaser build --clean --snapshot --skip=post-hooks goreleaser --rm-dist --snapshot
release: release:
# Make sure that the tests pass and the build works
TAGS=tcell make test
make test build clean
ifndef GITHUB_TOKEN ifndef GITHUB_TOKEN
$(error GITHUB_TOKEN is not defined) $(error GITHUB_TOKEN is not defined)
endif endif
@@ -126,7 +114,7 @@ endif
git push origin temp --follow-tags --force git push origin temp --follow-tags --force
# Make a GitHub release # Make a GitHub release
goreleaser --clean --release-notes tmp/release-note goreleaser --rm-dist --release-notes tmp/release-note
# Push to master # Push to master
git checkout master git checkout master
@@ -144,8 +132,6 @@ target/$(BINARY32): $(SOURCES)
target/$(BINARY64): $(SOURCES) target/$(BINARY64): $(SOURCES)
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
target/$(BINARYS390): $(SOURCES)
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm # https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) target/$(BINARYARM5): $(SOURCES)
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
@@ -169,7 +155,6 @@ target/$(BINARYLOONG64): $(SOURCES)
GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@ GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin bin/fzf: target/$(BINARY) | bin
-rm -f bin/fzf
cp -f target/$(BINARY) bin/fzf cp -f target/$(BINARY) bin/fzf
docker: docker:
@@ -184,4 +169,4 @@ update:
$(GO) get -u $(GO) get -u
$(GO) mod tidy $(GO) mod tidy
.PHONY: all generate build release test bench install clean docker docker-test update .PHONY: all build release test bench install clean docker docker-test update

View File

@@ -12,10 +12,7 @@ differ depending on the package manager.
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using Homebrew on Apple Silicon " If installed using git
set rtp+=/opt/homebrew/opt/fzf
" If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
@@ -26,10 +23,7 @@ written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using Homebrew on Apple Silicon " If installed using git
Plug '/opt/homebrew/opt/fzf'
" If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
``` ```
@@ -121,7 +115,7 @@ let g:fzf_action = {
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines) function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }')) call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
copen copen
cc cc
endfunction endfunction
@@ -315,7 +309,7 @@ following options are allowed:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `relative` [boolean default v:false] - `relative` [boolean default v:false]
- `border` [string default `rounded` (`sharp` on Windows)]: Border style - `border` [string default `rounded`]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
`fzf#wrap` `fzf#wrap`
@@ -489,4 +483,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2021 Junegunn Choi

250
README.md

File diff suppressed because one or more lines are too long

View File

@@ -1,74 +0,0 @@
#!/usr/bin/env bash
#
# The purpose of this script is to demonstrate how to preview a file or an
# image in the preview window of fzf.
#
# Dependencies:
# - https://github.com/sharkdp/bat
# - https://github.com/hpjansson/chafa
# - https://iterm2.com/utilities/imgcat
if [[ $# -ne 1 ]]; then
>&2 echo "usage: $0 FILENAME"
exit 1
fi
file=${1/#\~\//$HOME/}
type=$(file --dereference --mime -- "$file")
if [[ ! $type =~ image/ ]]; then
if [[ $type =~ =binary ]]; then
file "$1"
exit
fi
# Sometimes bat is installed as batcat.
if command -v batcat > /dev/null; then
batname="batcat"
elif command -v bat > /dev/null; then
batname="bat"
else
cat "$1"
exit
fi
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
exit
fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
# * https://github.com/junegunn/fzf/issues/2544
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
fi
# 1. Use kitty icat on kitty terminal
if [[ $KITTY_WINDOW_ID ]]; then
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
# you have to use 'stream'.
#
# 2. The last line of the output is the ANSI reset code without newline.
# This confuses fzf and makes it render scroll offset indicator.
# So we remove the last line and append the reset code to its previous line.
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
# 2. Use chafa with Sixel output
elif command -v chafa > /dev/null; then
chafa -f sixel -s "$dim" "$file"
# Add a new line character so that fzf can display multiple images in the preview window
echo
# 3. If chafa is not found but imgcat is available, use it on iTerm2
elif command -v imgcat > /dev/null; then
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
# user is running iTerm2. But for the sake of simplicity, we just assume
# that's the case here.
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
# 4. Cannot find any suitable method to preview the image
else
file "$file"
fi

View File

@@ -10,6 +10,7 @@ fail() {
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found' [[ -x "$fzf" ]] || fail 'fzf executable not found'
tmux_args=()
args=() args=()
opt="" opt=""
skip="" skip=""
@@ -57,7 +58,7 @@ while [[ $# -gt 0 ]]; do
;; ;;
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*) -p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
if [[ "$arg" =~ ^-[pwhxy] ]]; then if [[ "$arg" =~ ^-[pwhxy] ]]; then
[[ "$opt" =~ "-E" ]] || opt="-E" [[ "$opt" =~ "-K -E" ]] || opt="-K -E"
elif [[ "$arg" =~ ^.[lr] ]]; then elif [[ "$arg" =~ ^.[lr] ]]; then
opt="-h" opt="-h"
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
@@ -118,6 +119,8 @@ while [[ $# -gt 0 ]]; do
# "--" can be used to separate fzf-tmux options from fzf options to # "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts # avoid conflicts
skip=1 skip=1
tmux_args=("${args[@]}")
args=()
continue continue
;; ;;
*) *)
@@ -136,7 +139,7 @@ fi
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore") args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
# Handle zoomed tmux pane without popup options by moving it to a temp window # 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 zoomed_without_popup=1
original_window=$(tmux display-message -p "#{window_id}") 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 fzf-tmux is running\\r\"; done; done'") tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
@@ -151,18 +154,12 @@ argsf="${TMPDIR:-/tmp}/fzf-args-$id"
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
else
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
fi
cleanup() { cleanup() {
\rm -f $argsf $fifo1 $fifo2 $fifo3 \rm -f $argsf $fifo1 $fifo2 $fifo3
# Restore tmux window options # Restore tmux window options
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then
eval "tmux ${tmux_win_opts[*]}" eval "tmux ${tmux_win_opts[*]}"
fi fi
@@ -184,22 +181,9 @@ trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then [[ "$opt" =~ "-K -E" ]] && FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt"
elif [[ $tmux_version = 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
else
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
exit 2
fi
fi
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf" echo "$envs;" > "$argsf"
# Build arguments to fzf # Build arguments to fzf
@@ -211,7 +195,7 @@ close="; trap - EXIT SIGINT SIGTERM $close"
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX") export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo2
if [[ "$opt" =~ "-E" ]]; then if [[ "$opt" =~ "-K -E" ]]; then
cat $fifo2 & cat $fifo2 &
if [[ -n "$term" ]] || [[ -t 0 ]]; then if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
@@ -221,7 +205,15 @@ if [[ "$opt" =~ "-E" ]]; then
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
tmux popup -d "$PWD" $opt "bash $argsf" > /dev/null 2>&1 # tmux dropped the support for `-K`, `-R` to popup command
# TODO: We can remove this once tmux 3.2 is released
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
opt="${opt/-K/}"
else
opt="${opt} -R"
fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
exit $? exit $?
fi fi
@@ -233,9 +225,9 @@ else
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
tmux \ tmux set-window-option synchronize-panes off \;\
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \ set-window-option remain-on-exit off \;\
$tmux_off_opts \ split-window -c "$PWD" $opt "${tmux_args[@]}" "bash -c 'exec -a fzf bash $argsf'" $swap \
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; } > /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
cat $fifo2 cat $fifo2
exit "$(cat $fifo3)" exit "$(cat $fifo3)"

View File

@@ -1,4 +1,4 @@
fzf.txt fzf Last change: January 1 2024 fzf.txt fzf Last change: May 19 2021
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -32,10 +32,7 @@ depending on the package manager.
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using Homebrew on Apple Silicon " If installed using git
set rtp+=/opt/homebrew/opt/fzf
" If you have cloned fzf on ~/.fzf directory
set rtp+=~/.fzf set rtp+=~/.fzf
< <
If you use {vim-plug}{1}, the same can be written as: If you use {vim-plug}{1}, the same can be written as:
@@ -43,10 +40,7 @@ If you use {vim-plug}{1}, the same can be written as:
" If installed using Homebrew " If installed using Homebrew
Plug '/usr/local/opt/fzf' Plug '/usr/local/opt/fzf'
" If installed using Homebrew on Apple Silicon " If installed using git
Plug '/opt/homebrew/opt/fzf'
" If you have cloned fzf on ~/.fzf directory
Plug '~/.fzf' Plug '~/.fzf'
< <
But if you want the latest Vim plugin file from GitHub rather than the one But if you want the latest Vim plugin file from GitHub rather than the one
@@ -74,16 +68,16 @@ SUMMARY *fzf-summary*
The Vim plugin of fzf provides two core functions, and `:FZF` command which is The Vim plugin of fzf provides two core functions, and `:FZF` command which is
the basic file selector command built on top of them. the basic file selector command built on top of them.
1. `fzf#run([spec dict])` 1. `fzf#run([specdict])`
- Starts fzf inside Vim with the given spec - Starts fzf inside Vim with the given spec
- `:call fzf#run({'source': 'ls'})` - `:callfzf#run({'source':'ls'})`
2. `fzf#wrap([spec dict]) -> (dict)` 2. `fzf#wrap([specdict])->(dict)`
- Takes a spec for `fzf#run` and returns an extended version of it with - Takes a spec for `fzf#run` and returns an extended version of it with
additional options for addressing global preferences (`g:fzf_xxx`) additional options for addressing global preferences (`g:fzf_xxx`)
- `:echo fzf#wrap({'source': 'ls'})` - `:echofzf#wrap({'source':'ls'})`
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run` - We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
- `:call fzf#run(fzf#wrap({'source': 'ls'}))` - `:callfzf#run(fzf#wrap({'source':'ls'}))`
3. `:FZF [fzf_options string] [path string]` 3. `:FZF[fzf_optionsstring][pathstring]`
- Basic fuzzy file selector - Basic fuzzy file selector
- A reference implementation for those who don't want to write VimScript to - A reference implementation for those who don't want to write VimScript to
implement custom commands implement custom commands
@@ -149,7 +143,7 @@ Examples~
" An action can be a reference to a function that processes selected lines " An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines) function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }')) call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
copen copen
cc cc
endfunction endfunction
@@ -228,12 +222,12 @@ list:
`spinner` | Streaming input indicator `spinner` | Streaming input indicator
`query` | Query string `query` | Query string
`disabled` | Query string when search is disabled `disabled` | Query string when search is disabled
`prompt` | Prompt before query ( `> ` ) `prompt` | Prompt before query ( `>` )
`pointer` | Pointer to the current line ( `>` ) `pointer` | Pointer to the current line ( `>` )
----------------------------+------------------------------------------------------ ----------------------------+------------------------------------------------------
- `component` specifies the component (`fg` / `bg`) from which to extract the - `component` specifies the component (`fg` / `bg`) from which to extract the
color when considering each of the following highlight groups color when considering each of the following highlight groups
- `group1 [, group2, ...]` is a list of highlight groups that are searched (in - `group1[,group2,...]` is a list of highlight groups that are searched (in
order) for a matching color definition order) for a matching color definition
For example, consider the following specification: For example, consider the following specification:
@@ -274,7 +268,7 @@ as the sink.
< <
Instead of using the default find command, you can use any shell command as Instead of using the default find command, you can use any shell command as
the source. The following example will list the files managed by git. It's the source. The following example will list the files managed by git. It's
equivalent to running `git ls-files | fzf` on shell. equivalent to running `gitls-files|fzf` on shell.
> >
call fzf#run({'source': 'git ls-files', 'sink': 'e'}) call fzf#run({'source': 'git ls-files', 'sink': 'e'})
< <
@@ -302,7 +296,7 @@ The following table summarizes the available options.
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
Option name | Type | Description ~ Option name | Type | Description ~
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find .` ) `source` | string | External command to generate input to fzf (e.g. `find.` )
`source` | list | Vim list as input to fzf `source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item `sink` | funcref | Reference to function to process each selected item
@@ -311,8 +305,8 @@ The following table summarizes the available options.
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `verticalaboveleft30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width':0.9,'height':0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
`options` entry can be either a string or a list. For simple cases, string `options` entry can be either a string or a list. For simple cases, string
@@ -331,7 +325,7 @@ following options are allowed:
- `yoffset` [float default 0.5 range [0 ~ 1]] - `yoffset` [float default 0.5 range [0 ~ 1]]
- `xoffset` [float default 0.5 range [0 ~ 1]] - `xoffset` [float default 0.5 range [0 ~ 1]]
- `relative` [boolean default v:false] - `relative` [boolean default v:false]
- `border` [string default `rounded` (`sharp` on Windows)]: Border style - `border` [string default `rounded`]: Border style
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
@@ -349,7 +343,7 @@ So how can we make our custom `fzf#run` calls also respect those variables?
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
`fzf#run`. `fzf#run`.
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)` - `fzf#wrap([namestring],[specdict],[fullscreenbool])->(dict)`
- All arguments are optional. Usually we only need to pass a spec - All arguments are optional. Usually we only need to pass a spec
dictionary. dictionary.
- `name` is for managing history files. It is ignored if `g:fzf_history_dir` - `name` is for managing history files. It is ignored if `g:fzf_history_dir`
@@ -383,7 +377,7 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0)) command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
< <
Our `:LS` command will be much more useful if we can pass a directory argument Our `:LS` command will be much more useful if we can pass a directory argument
to it, so that something like `:LS /tmp` is possible. to it, so that something like `:LS/tmp` is possible.
> >
command! -bang -complete=dir -nargs=? LS command! -bang -complete=dir -nargs=? LS
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0)) \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
@@ -402,10 +396,10 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
- `g:fzf_layout` - `g:fzf_layout`
- `g:fzf_action` - `g:fzf_action`
- Works only when no custom `sink` (or `sinklist`) is provided - Works only when no custom `sink` (or `sink*`) is provided
- Having custom sink usually means that each entry is not an ordinary - Having custom sink usually means that each entry is not an ordinary
file path (e.g. name of color scheme), so we can't blindly apply the file path (e.g. name of color scheme), so we can't blindly apply the
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense) same strategy (i.e. `tabeditsome-color-scheme` doesn't make sense)
- `g:fzf_colors` - `g:fzf_colors`
- `g:fzf_history_dir` - `g:fzf_history_dir`
@@ -494,7 +488,7 @@ or above) by putting fzf-tmux options in `tmux` key.
*fzf-hide-statusline* *fzf-hide-statusline*
When fzf starts in a terminal buffer, the file type of the buffer is set to When fzf starts in a terminal buffer, the file type of the buffer is set to
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of `fzf`. So you can set up `FileTypefzf` autocmd to customize the settings of
the window. the window.
For example, if you open fzf on the bottom on the screen (e.g. `{'down': For example, if you open fzf on the bottom on the screen (e.g. `{'down':
@@ -512,7 +506,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2021 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

18
go.mod
View File

@@ -1,21 +1,21 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell/v2 v2.7.0 github.com/gdamore/tcell v1.4.0
github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.6 github.com/rivo/uniseg v0.2.0
github.com/saracen/walker v0.1.3 github.com/saracen/walker v0.1.2
golang.org/x/sys v0.16.0 golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12
golang.org/x/term v0.16.0 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
) )
require ( require (
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sync v0.5.0 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.14.0 // indirect
) )
go 1.17 go 1.17

68
go.sum
View File

@@ -1,60 +1,32 @@
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.0 h1:I5LiGTQuwrysAt1KS9wg1yFfOI3arI3ucFrxtd/xqaA= github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
github.com/gdamore/tcell/v2 v2.7.0/go.mod h1:hl/KtAANGBecfIPxk+FzKvThTqI84oplgbPEmVX60b8= github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

41
install
View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.46.1 version=0.31.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -168,24 +168,21 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;; Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;; Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;; Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;; Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;; Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;; Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;; FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;; OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;; CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;; MINGW*\ *64) download fzf-$version-windows_amd64.zip ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;; MSYS*\ *64) download fzf-$version-windows_amd64.zip ;;
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;; Windows*\ *64) download fzf-$version-windows_amd64.zip ;;
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;; *) binary_available=0 binary_error=1 ;;
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;;
Windows*\ *64) download fzf-$version-windows_amd64.zip ;;
*) binary_available=0 binary_error=1 ;;
esac esac
cd "$fzf_base" cd "$fzf_base"
@@ -196,12 +193,12 @@ if [ -n "$binary_error" ]; then
echo " - $binary_error !!!" echo " - $binary_error !!!"
fi fi
if command -v go > /dev/null; then if command -v go > /dev/null; then
echo -n "Building binary (go install github.com/junegunn/fzf) ... " echo -n "Building binary (go get -u github.com/junegunn/fzf) ... "
if [ -z "${GOPATH-}" ]; then if [ -z "${GOPATH-}" ]; then
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
mkdir -p "$GOPATH" mkdir -p "$GOPATH"
fi fi
if go install -ldflags "-s -w -X main.version=$version -X main.revision=go-install" github.com/junegunn/fzf; then if go get -ldflags "-s -w -X main.version=$version -X main.revision=go-get" github.com/junegunn/fzf; then
echo "OK" echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/" cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else else
@@ -245,7 +242,7 @@ for shell in $shells; do
src=${prefix_expand}.${shell} src=${prefix_expand}.${shell}
echo -n "Generate $src ... " echo -n "Generate $src ... "
fzf_completion="source \"$fzf_base/shell/completion.${shell}\"" fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $auto_completion -eq 0 ]; then if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi

View File

@@ -1,4 +1,4 @@
$version="0.46.1" $version="0.31.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.46" var version string = "0.31"
var revision string = "devel" var revision string = "devel"
func main() { func main() {

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2021 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Feb 2024" "fzf 0.46.1" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jul 2022" "fzf 0.31.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2021 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Feb 2024" "fzf 0.46.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jul 2022" "fzf 0.31.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -51,18 +51,6 @@ Case-sensitive match
.B "--literal" .B "--literal"
Do not normalize latin script letters for matching. Do not normalize latin script letters for matching.
.TP .TP
.BI "--scheme=" SCHEME
Choose scoring scheme tailored for different types of input.
.br
.BR default " Generic scoring scheme designed to work well with any type of input"
.br
.BR path " Scoring scheme well suited for file paths
.br
.BR history " Scoring scheme well suited for command history or any input where chronological ordering is important
Sets \fB--tiebreak=index\fR as well.
.br
.TP
.BI "--algo=" TYPE .BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2) Fuzzy matching algorithm (default: v2)
@@ -92,19 +80,6 @@ interface rather than a "fuzzy finder". You can later enable the search using
.B "+s, --no-sort" .B "+s, --no-sort"
Do not sort the result Do not sort the result
.TP .TP
.B "--track"
Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled. It is
not recommended to use this option with \fB--tac\fR as the resulting behavior
can be confusing. Also, consider using \fBtrack\fR action instead of this
option.
.RS
e.g.
\fBgit log --oneline --graph --color=always | nl |
fzf --ansi --track --no-sort --layout=reverse-list\fR
.RE
.TP
.B "--tac" .B "--tac"
Reverse the order of the input Reverse the order of the input
@@ -120,8 +95,6 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.br .br
.BR length " Prefers line with shorter length" .BR length " Prefers line with shorter length"
.br .br
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
.br
.BR begin " Prefers line with matched substring closer to the beginning" .BR begin " Prefers line with matched substring closer to the beginning"
.br .br
.BR end " Prefers line with matched substring closer to the end" .BR end " Prefers line with matched substring closer to the end"
@@ -190,23 +163,9 @@ actions are affected:
Label characters for \fBjump\fR and \fBjump-accept\fR Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout .SS Layout
.TP .TP
.BI "--height=" "[~]HEIGHT[%]" .BI "--height=" "HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using Display fzf window below the cursor with the given height instead of using
the full screen. the full screen.
If a negative value is specified, the height is calculated as the terminal
height minus the given value.
fzf --height=-1
When prefixed with \fB~\fR, fzf will automatically determine the height in the
range according to the input size. Note that adaptive height is not compatible
with top/bottom margin and padding given in percent size. It is also not
compatible with a negative height value.
# Will not take up 100% of the screen
seq 5 | fzf --height=~100%
.TP .TP
.BI "--min-height=" "HEIGHT" .BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10). Minimum height when \fB--height\fR is given in percent (default: 10).
@@ -228,29 +187,21 @@ Choose the layout (default: default)
A synonym for \fB--layout=reverse\fB A synonym for \fB--layout=reverse\fB
.TP .TP
.BI "--border" [=BORDER_OPT] .BI "--border" [=STYLE]
Draw border around the finder Draw border around the finder
.br .br
.BR rounded " Border with rounded corners (default)" .BR rounded " Border with rounded corners (default)"
.br .br
.BR sharp " Border with sharp corners" .BR sharp " Border with sharp corners"
.br .br
.BR bold " Border with bold lines" .BR horizontal " Horizontal lines above and below the finder"
.br .br
.BR double " Border with double lines" .BR vertical " Vertical lines on each side of the finder"
.br .br
.BR block " Border using block elements; suitable when using different background colors" .BR top
.br .br
.BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals" .BR bottom
.br
.BR horizontal " Horizontal lines above and below the finder"
.br
.BR vertical " Vertical lines on each side of the finder"
.br
.BR top " (up)"
.br
.BR bottom " (down)"
.br .br
.BR left .BR left
.br .br
@@ -259,63 +210,9 @@ Draw border around the finder
.BR none .BR none
.br .br
If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fB--ambidouble\fR. If the border is still not properly
rendered, set \fB--no-unicode\fR.
.TP
.BI "--border-label" [=LABEL]
Label to print on the horizontal border line. Should be used with one of the
following \fB--border\fR options.
.br
.B * rounded
.br
.B * sharp
.br
.B * bold
.br
.B * double
.br
.B * horizontal
.br
.BR "* top" " (up)"
.br
.BR "* bottom" " (down)"
.br
.br
e.g.
\fB# ANSI color codes are supported
# (with https://github.com/busyloop/lolcat)
label=$(curl -s http://metaphorpsum.com/sentences/1 | lolcat -f)
# Border label at the center
fzf --height=10 --border --border-label="╢ $label ╟" --color=label:italic:black
# Left-aligned (positive integer)
fzf --height=10 --border --border-label="╢ $label ╟" --border-label-pos=3 --color=label:italic:black
# Right-aligned (negative integer) on the bottom line (:bottom)
fzf --height=10 --border --border-label="╢ $label ╟" --border-label-pos=-3:bottom --color=label:italic:black\fR
.TP
.BI "--border-label-pos" [=N[:top|bottom]]
Position of the border label on the border line. Specify a positive integer as
the column position from the left. Specify a negative integer to right-align
the label. Label is printed on the top border line by default, add
\fB:bottom\fR to put it on the border line on the bottom. The default value
\fB0 (or \fBcenter\fR) will put the label at the center of the border line.
.TP .TP
.B "--no-unicode" .B "--no-unicode"
Use ASCII characters instead of Unicode drawing characters to draw borders, Use ASCII characters instead of Unicode box drawing characters to draw border
the spinner and the horizontal separator.
.TP
.B "--ambidouble"
Set this option if your terminal displays ambiguous width characters (e.g.
box-drawing characters for borders) as 2 columns.
.TP .TP
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
@@ -368,48 +265,20 @@ e.g.
.TP .TP
.BI "--info=" "STYLE" .BI "--info=" "STYLE"
Determines the display style of finder info (match counters). Determines the display style of finder info.
.br .br
.BR default " Display on the next line to the prompt" .BR default " Display on the next line to the prompt"
.br .br
.BR right " Display on the right end of the next line to the prompt" .BR inline " Display on the same line"
.br .br
.BR inline " Display on the same line with the default separator ' < '" .BR hidden " Do not display finder info"
.br
.BR inline:SEPARATOR " Display on the same line with a non-default separator"
.br
.BR inline-right " Display on the right end of the same line
.br
.BR hidden " Do not display finder info"
.br .br
.TP .TP
.B "--no-info" .B "--no-info"
A synonym for \fB--info=hidden\fB A synonym for \fB--info=hidden\fB
.TP
.BI "--separator=" "STR"
The given string will be repeated to form the horizontal separator on the info
line (default: '─' or '-' depending on \fB--no-unicode\fR).
ANSI color codes are supported.
.TP
.B "--no-separator"
Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB
.TP
.BI "--scrollbar=" "CHAR1[CHAR2]"
Use the given character to render scrollbar. (default: '│' or ':' depending on
\fB--no-unicode\fR). The optional \fBCHAR2\fR is used to render scrollbar of
the preview window.
.TP
.B "--no-scrollbar"
Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
.TP .TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
@@ -458,30 +327,24 @@ color mappings.
\fBbw \fRNo colors (equivalent to \fB--no-color\fR) \fBbw \fRNo colors (equivalent to \fB--no-color\fR)
.B COLOR NAMES: .B COLOR NAMES:
\fBfg \fRText \fBfg \fRText
\fBpreview-fg \fRPreview window text \fBbg \fRBackground
\fBbg \fRBackground \fBpreview-fg \fRPreview window text
\fBpreview-bg \fRPreview window background \fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings \fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line) \fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line) \fBbg+ \fRBackground (current line)
\fBgutter \fRGutter on the left \fBgutter \fRGutter on the left (defaults to \fBbg+\fR)
\fBhl+ \fRHighlighted substrings (current line) \fBhl+ \fRHighlighted substrings (current line)
\fBquery \fRQuery string \fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR) \fBdisabled \fRQuery string when search is disabled
\fBinfo \fRInfo line (match counters) \fBinfo \fRInfo line (match counters)
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
\fBscrollbar \fRScrollbar \fBprompt \fRPrompt
\fBpreview-border \fRBorder around the preview window (\fB--preview\fR) \fBpointer \fRPointer to the current line
\fBpreview-scrollbar \fRScrollbar \fBmarker \fRMulti-select marker
\fBseparator \fRHorizontal separator on info line \fBspinner \fRStreaming input indicator
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR) \fBheader \fRHeader
\fBpreview-label \fRBorder label of the preview window (\fB--preview-label\fR)
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.B ANSI COLORS: .B ANSI COLORS:
\fB-1 \fRDefault terminal foreground/background color \fB-1 \fRDefault terminal foreground/background color
@@ -513,7 +376,6 @@ color mappings.
\fBreverse\fR \fBreverse\fR
\fBdim\fR \fBdim\fR
\fBitalic\fR \fBitalic\fR
\fBstrikethrough\fR
.B EXAMPLES: .B EXAMPLES:
@@ -539,7 +401,7 @@ Use black background
.BI "--history=" "HISTORY_FILE" .BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion. Load search history from the specified file and update the file on completion.
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
\fBnext-history\fR and \fBprev-history\fR. \fBnext-history\fR and \fBprevious-history\fR.
.TP .TP
.BI "--history-size=" "N" .BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
@@ -564,9 +426,6 @@ they represent the exact size of the preview window. (It also overrides
by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
prefix.) prefix.)
fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
the preview command can determine the position of the preview window.
A placeholder expression starting with \fB+\fR flag will be replaced to the A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection space-separated list of the selected lines (or the current line if no selection
was made) individually quoted. was made) individually quoted.
@@ -578,6 +437,10 @@ e.g.
When using a field index expression, leading and trailing whitespace is stripped When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag. from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
all index numbers when multiple lines are selected.
A placeholder expression with \fBf\fR flag is replaced to the path of A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may multi-select a large number of items and the length of the evaluated string may
@@ -589,19 +452,10 @@ e.g.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\ seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR --preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
Also,
* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string
.br
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
.br
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current Preview window will be updated even when there is no match for the current
query if any of the placeholder expressions evaluates to a non-empty string query if any of the placeholder expressions evaluates to a non-empty string.
or \fB{q}\fR is in the command template.
Since 0.24.0, fzf can render partial preview content before the preview command Since 0.24.0, fzf can render partial preview content before the preview command
completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is
@@ -614,50 +468,7 @@ e.g.
echo "$i" echo "$i"
sleep 0.01 sleep 0.01
done'\fR done'\fR
fzf has experimental support for Kitty graphics protocol and Sixel graphics.
The following example uses https://github.com/junegunn/fzf/blob/master/bin/fzf-preview.sh
script to render an image using either of the protocols inside the preview window.
e.g.
\fBfzf --preview='fzf-preview.sh {}'
.RE .RE
.TP
.BI "--preview-label" [=LABEL]
Label to print on the horizontal border line of the preview window.
Should be used with one of the following \fB--preview-window\fR options.
.br
.B * border-rounded (default on non-Windows platforms)
.br
.B * border-sharp (default on Windows)
.br
.B * border-bold
.br
.B * border-double
.br
.B * border-block
.br
.B * border-thinblock
.br
.B * border-horizontal
.br
.B * border-top
.br
.B * border-bottom
.br
.TP
.BI "--preview-label-pos" [=N[:top|bottom]]
Position of the border label on the border line of the preview window. Specify
a positive integer as the column position from the left. Specify a negative
integer to right-align the label. Label is printed on the top border line by
default, add \fB:bottom\fR to put it on the border line on the bottom. The
default value 0 (or \fBcenter\fR) will put the label at the center of the
border line.
.TP .TP
.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]" .BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
@@ -794,16 +605,7 @@ Print output delimited by ASCII NUL characters instead of newline characters
Do not clear finder interface on exit. If fzf was started in full screen mode, Do not clear finder interface on exit. If fzf was started in full screen mode,
it will not switch back to the original screen, so you'll have to manually run it will not switch back to the original screen, so you'll have to manually run
\fBtput rmcup\fR to return. This option can be used to avoid flickering of the \fBtput rmcup\fR to return. This option can be used to avoid flickering of the
screen when your application needs to start fzf multiple times in order. (Note screen when your application needs to start fzf multiple times in order.
that in most cases, it is preferable to use \fBreload\fR action instead.)
e.g.
\fBfoo=$(seq 100 | fzf --no-clear) || (
# Need to manually switch back to the main screen when cancelled
tput rmcup
exit 1
) && seq "$foo" 100 | fzf
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
@@ -813,44 +615,6 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method.
- If the port number is omitted or given as 0, fzf will automatically choose
a port and export it as \fBFZF_PORT\fR environment variable to the child processes
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
sending an API key with the same value in the \fBx-api-key\fR HTTP header
- \fBFZF_API_KEY\fR is required for a non-localhost listen address
- To allow remote process execution, use \fB--listen-unsafe\fR
e.g.
\fB# Start HTTP server on port 6266
fzf --listen 6266
# Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Get program state in JSON format (experimental)
# * Make sure NOT to access this endpoint from execute/transform actions
# as it will result in a timeout
curl localhost:6266
# Start HTTP server on port 6266 with remote connections allowed
# * Listening on non-localhost address requires using an API key
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
fzf --listen 0.0.0.0:6266
# Send an authenticated action
curl -XPOST localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
# Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR
.TP
.B "--version" .B "--version"
Display version information and exit Display version information and exit
@@ -866,11 +630,6 @@ this case make sure that the command is POSIX-compliant.
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
.TP
.B FZF_API_KEY
Can be used to require an API key when using \fB--listen\fR option. If not set,
no authentication will be required by the server. You can set this value if
you need to protect against DNS rebinding and privilege escalation attacks.
.SH EXIT STATUS .SH EXIT STATUS
.BR 0 " Normal exit" .BR 0 " Normal exit"
@@ -905,39 +664,6 @@ of field index expressions.
.BR .. " All the fields" .BR .. " All the fields"
.br .br
.SH ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES
fzf exports the following environment variables to its child processes.
.BR FZF_LINES " Number of lines fzf takes up excluding padding and margin"
.br
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
.br
.BR FZF_TOTAL_COUNT " Total number of items"
.br
.BR FZF_MATCH_COUNT " Number of matched items"
.br
.BR FZF_SELECT_COUNT " Number of selected items"
.br
.BR FZF_QUERY " Current query string"
.br
.BR FZF_PROMPT " Prompt string"
.br
.BR FZF_ACTION " The name of the last action performed"
.br
.BR FZF_PORT " Port number when --listen option is used"
.br
The following variables are additionally exported to the preview commands.
.BR FZF_PREVIEW_TOP " Top position of the preview window"
.br
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
.br
.BR FZF_PREVIEW_LINES " Number of lines in the preview window"
.br
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
.SH EXTENDED SEARCH MODE .SH EXTENDED SEARCH MODE
Unless specified otherwise, fzf will start in "extended-search mode". In this Unless specified otherwise, fzf will start in "extended-search mode". In this
@@ -989,8 +715,6 @@ e.g.
.br .br
\fIctrl-space\fR \fIctrl-space\fR
.br .br
\fIctrl-delete\fR
.br
\fIctrl-\\\fR \fIctrl-\\\fR
.br .br
\fIctrl-]\fR \fIctrl-]\fR
@@ -1059,8 +783,6 @@ e.g.
.br .br
\fIshift-right\fR \fIshift-right\fR
.br .br
\fIshift-delete\fR
.br
\fIalt-shift-up\fR \fIalt-shift-up\fR
.br .br
\fIalt-shift-down\fR \fIalt-shift-down\fR
@@ -1075,59 +797,9 @@ e.g.
.br .br
\fIdouble-click\fR \fIdouble-click\fR
.br .br
\fIscroll-up\fR
.br
\fIscroll-down\fR
.br
\fIpreview-scroll-up\fR
.br
\fIpreview-scroll-down\fR
.br
\fIshift-left-click\fR
.br
\fIshift-right-click\fR
.br
\fIshift-scroll-up\fR
.br
\fIshift-scroll-down\fR
.br
or any single character or any single character
.SS AVAILABLE EVENTS: .SS AVAILABLE EVENTS:
\fIstart\fR
.RS
Triggered only once when fzf finder starts. Since fzf consumes the input stream
asynchronously, the input list is not available unless you use \fI--sync\fR.
e.g.
\fB# Move cursor to the last item and select all items
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
.RE
\fIload\fR
.RS
Triggered when the input stream is complete and the initial processing of the
list is complete.
e.g.
\fB# Change the prompt to "loaded" when the input stream is complete
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
.RE
\fIresize\fR
.RS
Triggered when the terminal size is changed.
e.g.
\fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR
.RE
\fIresult\fR
.RS
Triggered when the filtering for the current query is complete and the result list is ready.
e.g.
\fB# Put the cursor on the second item when the query string is empty
# * Note that you can't use 'change' event in this case because the second position may not be available
fzf --sync --bind 'result:transform:[[ -z {fzf:query} ]] && echo "pos(2)"'\fR
.RE
\fIchange\fR \fIchange\fR
.RS .RS
Triggered whenever the query string is changed Triggered whenever the query string is changed
@@ -1136,44 +808,6 @@ e.g.
\fB# Move cursor to the first entry whenever the query is changed \fB# Move cursor to the first entry whenever the query is changed
fzf --bind change:first\fR fzf --bind change:first\fR
.RE .RE
\fIfocus\fR
.RS
Triggered when the focus changes due to a vertical cursor movement or a search
result update.
e.g.
\fBfzf --bind 'focus:transform-preview-label:echo [ {} ]' --preview 'cat {}'
# Any action bound to the event runs synchronously and thus can make the interface sluggish
# e.g. lolcat isn't one of the fastest programs, and every cursor movement in
# fzf will be noticeably affected by its execution time
fzf --bind 'focus:transform-preview-label:echo [ {} ] | lolcat -f' --preview 'cat {}'
# Beware not to introduce an infinite loop
seq 10 | fzf --bind 'focus:up' --cycle\fR
.RE
\fIone\fR
.RS
Triggered when there's only one match. \fBone:accept\fR binding is comparable
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
effective before the interactive finder starts but \fBone\fR event is triggered
by the interactive finder.
e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\fIzero\fR
.RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
effective before the interactive finder starts but \fBzero\fR event is
triggered by the interactive finder.
e.g.
\fB# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
.RE
\fIbackward-eof\fR \fIbackward-eof\fR
.RS .RS
@@ -1187,107 +821,81 @@ e.g.
.SS AVAILABLE ACTIONS: .SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions. A key or an event can be bound to one or more of the following actions.
\fBACTION: DEFAULT BINDINGS (NOTES): \fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBaccept-or-print-query\fR (same as \fBaccept\fR except that it prints the query when there's no match) \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR) \fBclear-screen\fR \fIctrl-l\fR
\fBchange-preview(...)\fR (change \fB--preview\fR option) \fBclear-selection\fR (clear multi-selection)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) \fBclose\fR (close preview window if open, abort fzf otherwise)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') \fBclear-query\fR (clear query string)
\fBchange-prompt(...)\fR (change prompt to the given string) \fBdelete-char\fR \fIdel\fR
\fBchange-query(...)\fR (change query string to the given string) \fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR \fBdeselect\fR
\fBdeselect-all\fR (deselect all matches) \fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality) \fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBenable-search\fR (enable search functionality) \fBenable-search\fR (enable search functionality)
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-silent(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR) \fBfirst\fR (move to the first match)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBjump-accept\fR (jump and accept) \fBjump-accept\fR (jump and accept)
\fBkill-line\fR \fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR \fBkill-word\fR \fIalt-d\fR
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR) \fBlast\fR (move to the last match)
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBnext-selected\fR (move to the next selected item) \fBpage-down\fR \fIpgdn\fR
\fBpage-down\fR \fIpgdn\fR \fBpage-up\fR \fIpgup\fR
\fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR \fBhalf-page-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBhide-header\fR \fBpreview(...)\fR (see below for the details)
\fBhide-preview\fR \fBpreview-down\fR \fIshift-down\fR
\fBoffset-down\fR (similar to CTRL-E of Vim) \fBpreview-up\fR \fIshift-up\fR
\fBoffset-up\fR (similar to CTRL-Y of Vim)
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprev-selected\fR (move to the previous selected item)
\fBpreview(...)\fR (see below for the details)
\fBpreview-down\fR \fIshift-down\fR
\fBpreview-up\fR \fIshift-up\fR
\fBpreview-page-down\fR \fBpreview-page-down\fR
\fBpreview-page-up\fR \fBpreview-page-up\fR
\fBpreview-half-page-down\fR \fBpreview-half-page-down\fR
\fBpreview-half-page-up\fR \fBpreview-half-page-up\fR
\fBpreview-bottom\fR \fBpreview-bottom\fR
\fBpreview-top\fR \fBpreview-top\fR
\fBprint-query\fR (print query and exit) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBput\fR (put the character to the prompt) \fBprint-query\fR (print query and exit)
\fBput(...)\fR (put the given string to the prompt) \fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR) \fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreload-sync(...)\fR (see below for the details) \fBreplace-query\fR (replace query string with the current selection)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBshow-header\fR \fBtoggle\fR (\fIright-click\fR)
\fBshow-preview\fR \fBtoggle-all\fR (toggle all matches)
\fBtoggle\fR (\fIright-click\fR) \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-all\fR (toggle all matches) \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-header\fR
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-preview-wrap\fR \fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality) \fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle-track\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBunbind(...)\fR (unbind bindings)
\fBtrack\fR (track the current item; automatically disabled if focus changes) \fBunix-line-discard\fR \fIctrl-u\fR
\fBtransform(...)\fR (transform states using the output of an external command) \fBunix-word-rubout\fR \fIctrl-w\fR
\fBtransform-border-label(...)\fR (transform border label using an external command) \fBup\fR \fIctrl-k ctrl-p up\fR
\fBtransform-header(...)\fR (transform header using an external command) \fByank\fR \fIctrl-y\fR
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command)
\fBunbind(...)\fR (unbind bindings)
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.SS ACTION COMPOSITION .SS ACTION COMPOSITION
@@ -1310,8 +918,6 @@ that case, you can use any of the following alternative notations to avoid
parse errors. parse errors.
\fBaction-name[...]\fR \fBaction-name[...]\fR
\fBaction-name{...}\fR
\fBaction-name<...>\fR
\fBaction-name~...~\fR \fBaction-name~...~\fR
\fBaction-name!...!\fR \fBaction-name!...!\fR
\fBaction-name@...@\fR \fBaction-name@...@\fR
@@ -1352,14 +958,6 @@ On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
POSIX-compliant. POSIX-compliant.
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
current fzf process with the specified command using \fBexecve(2)\fR system
call.
\fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list
@@ -1380,38 +978,6 @@ e.g.
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
--ansi --disabled --query "$INITIAL_QUERY"\fR --ansi --disabled --query "$INITIAL_QUERY"\fR
\fBreload-sync(...)\fR is a synchronous version of \fBreload\fR that replaces
the list only when the command is complete. This is useful when the command
takes a while to produce the initial output and you don't want fzf to run
against an empty list while the command is running.
e.g.
\fB# You can still filter and select entries from the initial list for 3 seconds
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
.SS TRANSFORM ACTIONS
Actions with \fBtransform-\fR prefix are used to transform the states of fzf
using the output of an external command. The output of these commands are
expected to be a single line of text.
e.g.
\fBfzf --bind 'focus:transform-header:file --brief {}'\fR
\fBtransform(...)\fR action runs an external command that should print a series
of actions to be performed. The output should be in the same format as the
payload of HTTP POST request to the \fB--listen\fR server.
e.g.
\fB# Disallow selecting an empty line
echo -e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \\
--bind 'enter:transform:[[ -n {} ]] &&
echo accept ||
echo "change-header:Invalid selection"'
\fR
.SS PREVIEW BINDING .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013-2024 Junegunn Choi " Copyright (c) 2017 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -143,7 +143,7 @@ function! fzf#install()
if !filereadable(script) if !filereadable(script)
throw script.' not found' throw script.' not found'
endif endif
let script = 'powershell -ExecutionPolicy Bypass -file ' . shellescape(script) let script = 'powershell -ExecutionPolicy Bypass -file ' . script
else else
let script = s:base_dir.'/install' let script = s:base_dir.'/install'
if !executable(script) if !executable(script)
@@ -164,7 +164,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin) if has_key(s:versions, a:bin)
return s:versions[a:bin] return s:versions[a:bin]
end end
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height' let command = fzf#shellescape(a:bin) . ' --version --no-height'
let output = systemlist(command) let output = systemlist(command)
if v:shell_error || empty(output) if v:shell_error || empty(output)
return '' return ''
@@ -342,8 +342,7 @@ function! s:common_sink(action, lines) abort
endfunction endfunction
function! s:get_color(attr, ...) function! s:get_color(attr, ...)
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152) let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
let fam = gui ? 'gui' : 'cterm' let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000 for group in a:000
@@ -456,30 +455,6 @@ function! s:writefile(...)
endif endif
endfunction endfunction
function! s:extract_option(opts, name)
let opt = ''
let expect = 0
" There are a few cases where this function doesn't work as expected.
" Let's just assume such cases are extremely unlikely in real world.
" e.g. --query --border
for word in split(a:opts)
if expect && word !~ '^"\=-'
let opt = opt . ' ' . word
let expect = 0
elseif word == '--no-'.a:name
let opt = ''
elseif word =~ '^--'.a:name.'='
let opt = word
elseif word =~ '^--'.a:name.'$'
let opt = word
let expect = 1
elseif expect
let expect = 0
endif
endfor
return opt
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -488,7 +463,7 @@ try
let temps = { 'result': s:fzf_tempname() } let temps = { 'result': s:fzf_tempname() }
let optstr = s:evaluate_opts(get(dict, 'options', '')) let optstr = s:evaluate_opts(get(dict, 'options', ''))
try try
let fzf_exec = shellescape(fzf#exec()) let fzf_exec = fzf#shellescape(fzf#exec())
catch catch
throw v:exception throw v:exception
endtry endtry
@@ -535,8 +510,7 @@ try
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --height='.height
endif endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options' let optstr .= s:border_opt(get(dict, 'window', 0))
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let prev_default_command = $FZF_DEFAULT_COMMAND let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command) if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command let $FZF_DEFAULT_COMMAND = source_command
@@ -763,7 +737,7 @@ function! s:calc_size(max, val, dict)
return size return size
endif endif
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2 let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0 let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
if stridx(opts, '--header') > stridx(opts, '--no-header') if stridx(opts, '--header') > stridx(opts, '--no-header')
let margin += len(split(opts, "\n")) let margin += len(split(opts, "\n"))
endif endif
@@ -780,9 +754,9 @@ function! s:border_opt(window)
endif endif
" Border style " Border style
let style = tolower(get(a:window, 'border', '')) let style = tolower(get(a:window, 'border', 'rounded'))
if !has_key(a:window, 'border') && has_key(a:window, 'rounded') if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1)
let style = a:window.rounded ? 'rounded' : 'sharp' let style = 'sharp'
endif endif
if style == 'none' || style == 'no' if style == 'none' || style == 'no'
return '' return ''
@@ -790,7 +764,7 @@ function! s:border_opt(window)
" For --border styles, we need fzf 0.24.0 or above " For --border styles, we need fzf 0.24.0 or above
call fzf#exec('0.24.0') call fzf#exec('0.24.0')
let opt = ' --border ' . style let opt = ' --border=' . style
if has_key(a:window, 'highlight') if has_key(a:window, 'highlight')
let color = s:get_color('fg', a:window.highlight) let color = s:get_color('fg', a:window.highlight)
if len(color) if len(color)
@@ -852,17 +826,6 @@ if exists(':tnoremap')
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n> tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
endif endif
let s:warned = 0
function! s:handle_ambidouble(dict)
if &ambiwidth == 'double'
let a:dict.env = { 'RUNEWIDTH_EASTASIAN': '1' }
elseif !s:warned && $RUNEWIDTH_EASTASIAN == '1' && &ambiwidth !=# 'double'
call s:warn("$RUNEWIDTH_EASTASIAN is '1' but &ambiwidth is not 'double'")
2sleep
let s:warned = 1
endif
endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd() let winrest = winrestcmd()
let pbuf = bufnr('') let pbuf = bufnr('')
@@ -932,7 +895,6 @@ function! s:execute_term(dict, command, temps) abort
endif endif
let command .= s:term_marker let command .= s:term_marker
if has('nvim') if has('nvim')
call s:handle_ambidouble(fzf)
call termopen(command, fzf) call termopen(command, fzf)
else else
let term_opts = {'exit_cb': function(fzf.on_exit)} let term_opts = {'exit_cb': function(fzf.on_exit)}
@@ -944,8 +906,7 @@ function! s:execute_term(dict, command, temps) abort
else else
let term_opts.curwin = 1 let term_opts.curwin = 1
endif endif
call s:handle_ambidouble(term_opts) let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
keepjumps let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
if is_popup && exists('#TerminalWinOpen') if is_popup && exists('#TerminalWinOpen')
doautocmd <nomodeline> TerminalWinOpen doautocmd <nomodeline> TerminalWinOpen
endif endif
@@ -1011,16 +972,16 @@ function! s:callback(dict, lines) abort
endfunction endfunction
if has('nvim') if has('nvim')
function s:create_popup(opts) abort function s:create_popup(hl, opts) abort
let buf = nvim_create_buf(v:false, v:true) let buf = nvim_create_buf(v:false, v:true)
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts) let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let win = nvim_open_win(buf, v:true, opts) let win = nvim_open_win(buf, v:true, opts)
silent! call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal') call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
call setwinvar(win, '&colorcolumn', '') call setwinvar(win, '&colorcolumn', '')
return buf return buf
endfunction endfunction
else else
function! s:create_popup(opts) abort function! s:create_popup(hl, opts) abort
let s:popup_create = {buf -> popup_create(buf, #{ let s:popup_create = {buf -> popup_create(buf, #{
\ line: a:opts.row, \ line: a:opts.row,
\ col: a:opts.col, \ col: a:opts.col,
@@ -1055,7 +1016,7 @@ function! s:popup(opts) abort
let row += !has('nvim') let row += !has('nvim')
let col += !has('nvim') let col += !has('nvim')
call s:create_popup({ call s:create_popup('Normal', {
\ 'row': row, 'col': col, 'width': width, 'height': height \ 'row': row, 'col': col, 'width': width, 'height': height
\ }) \ })
endfunction endfunction

View File

@@ -9,24 +9,23 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ $- =~ i ]] || return 0 if [[ $- =~ i ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -F _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
command find -L "$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 \) \ -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 | command sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
fi fi
if ! declare -F _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
command find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
fi fi
@@ -38,7 +37,7 @@ bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then elif [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; }; then
shift shift
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@" fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
else else
@@ -56,8 +55,8 @@ __fzf_orig_completion() {
cmd="${BASH_REMATCH[3]}" cmd="${BASH_REMATCH[3]}"
[[ "$f" = _fzf_* ]] && continue [[ "$f" = _fzf_* ]] && continue
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}" printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then if [[ "$l" = *" -o nospace "* ]] && [[ ! "$__fzf_nospace_commands" = *" $cmd "* ]]; then
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd " __fzf_nospace_commands="$__fzf_nospace_commands $cmd "
fi fi
fi fi
done done
@@ -69,185 +68,59 @@ _fzf_opts_completion() {
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-h --help
-x --extended -x --extended
-e --exact -e --exact
--extended-exact
+x --no-extended
+e --no-exact
-q --query
-f --filter
--literal
--no-literal
--algo --algo
--scheme -i +i
--expect
--no-expect
--enabled --no-phony
--disabled --phony
--tiebreak
--bind
--color
--toggle-sort
-d --delimiter
-n --nth -n --nth
--with-nth --with-nth
-s --sort -d --delimiter
+s --no-sort +s --no-sort
--track
--no-track
--tac --tac
--no-tac --tiebreak
-i
+i
-m --multi -m --multi
+m --no-multi
--ansi
--no-ansi
--no-mouse --no-mouse
+c --no-color --bind
+2 --no-256
--black
--no-black
--bold
--no-bold
--layout
--reverse
--no-reverse
--cycle --cycle
--no-cycle
--keep-right
--no-keep-right
--hscroll
--no-hscroll --no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--no-filepath-word
--info
--no-info
--inline-info
--no-inline-info
--separator
--no-separator
--scrollbar
--no-scrollbar
--jump-labels --jump-labels
-1 --select-1 --height
+1 --no-select-1 --literal
-0 --exit-0 --reverse
+0 --no-exit-0 --margin
--read0 --inline-info
--no-read0
--print0
--no-print0
--print-query
--no-print-query
--prompt --prompt
--pointer --pointer
--marker --marker
--sync
--no-sync
--async
--no-history
--history
--history-size
--no-header
--no-header-lines
--header --header
--header-lines --header-lines
--header-first --ansi
--no-header-first
--ellipsis
--preview
--no-preview
--preview-window
--height
--min-height
--no-height
--no-margin
--no-padding
--no-border
--border
--no-border-label
--border-label
--border-label-pos
--no-preview-label
--preview-label
--preview-label-pos
--no-unicode
--unicode
--margin
--padding
--tabstop --tabstop
--listen --color
--no-listen --no-bold
--clear --history
--no-clear --history-size
--version --preview
--" --preview-window
-q --query
-1 --select-1
-0 --exit-0
-f --filter
--print-query
--expect
--sync"
case "${prev}" in case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
;;
--tiebreak) --tiebreak)
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") ) COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
return 0 return 0
;; ;;
--color) --color)
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") ) COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
return 0 return 0
;; ;;
--layout) --history)
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") ) COMPREPLY=()
return 0
;;
--info)
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
return 0
;;
--preview-window)
COMPREPLY=( $(compgen -W "
default
hidden
nohidden
wrap
nowrap
cycle
nocycle
up top
down bottom
left
right
rounded border border-rounded
sharp border-sharp
border-bold
border-block
border-thinblock
border-double
noborder border-none
border-horizontal
border-vertical
border-up border-top
border-down border-bottom
border-left
border-right
follow
nofollow" -- "$cur") )
return 0
;;
--border)
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
return 0
;;
--border-label-pos|--preview-label-pos)
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
return 0 return 0
;; ;;
esac esac
@@ -266,18 +139,17 @@ _fzf_handle_dynamic_completion() {
shift shift
orig_cmd="$1" orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd" orig_var="_fzf_orig_completion_$cmd"
orig="${!orig_var-}" orig="${!orig_var##*#}"
orig="${orig##*#}"
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
$orig "$@" $orig "$@"
elif [[ -n "${_fzf_completion_loader-}" ]]; then elif [[ -n "$_fzf_completion_loader" ]]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
# _completion_loader may not have updated completion for the command # _completion_loader may not have updated completion for the command
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null) __fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }" eval "${orig_complete/ -F / -o nospace -F }"
else else
eval "$orig_complete" eval "$orig_complete"
@@ -289,19 +161,14 @@ _fzf_handle_dynamic_completion() {
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd local cur base dir leftover matches trigger cmd
cmd="${COMP_WORDS[0]}" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
if [[ $cmd == \\* ]]; then
cmd="${cmd:1}"
fi
cmd="${cmd//[^A-Za-z0-9_=]/_}"
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then if [[ "$cur" == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" 2> /dev/null || return eval "base=$base"
dir=
[[ $base = *"/"* ]] && dir="$base" [[ $base = *"/"* ]] && dir="$base"
while true; do while true; do
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
@@ -309,11 +176,11 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[[ -z "$dir" ]] && dir='.' [[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}" [[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
printf "%q " "${item%$3}$3" printf "%q$3 " "$item"
done) done)
matches=${matches% } matches=${matches% }
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " [[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [[ -n "$matches" ]]; then if [[ -n "$matches" ]]; then
COMPREPLY=( "$matches" ) COMPREPLY=( "$matches" )
else else
@@ -322,7 +189,7 @@ __fzf_generic_path_completion() {
printf '\e[5n' printf '\e[5n'
return 0 return 0
fi fi
dir=$(command dirname "$dir") dir=$(dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/ [[ "$dir" =~ /$ ]] || dir="$dir"/
done done
else else
@@ -356,16 +223,16 @@ _fzf_complete() {
fi fi
local cur selected trigger cmd post local cur selected trigger cmd post
post="$(caller 0 | command awk '{print $2}')_post" post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post='command cat' type -t "$post" > /dev/null 2>&1 || post=cat
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ') selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then if [[ -n "$selected" ]]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -397,68 +264,33 @@ _fzf_complete_kill() {
} }
_fzf_proc_completion() { _fzf_proc_completion() {
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null || command ps -ef | sed 1d
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
_fzf_proc_completion_post() { _fzf_proc_completion_post() {
command awk '{print $2}' awk '{print $2}'
} }
# To use custom hostname lists, override __fzf_list_hosts.
# The function is expected to print hostnames, one per line as well as in the
# desired sorting and with any duplicates removed, to standard output.
#
# e.g.
# # Use bash-completionss _known_hosts_real() for getting the list of hosts
# __fzf_list_hosts() {
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
# local COMPREPLY=()
# _known_hosts_real ''
# printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
# }
if ! declare -F __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() {
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
}
fi
_fzf_host_completion() { _fzf_host_completion() {
_fzf_complete +m -- "$@" < <(__fzf_list_hosts) _fzf_complete +m -- "$@" < <(
} 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 }') \
# Values for $1 $2 $3 are described here <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
# https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html awk '{if (length($2) > 0) {print $2}}' | sort -u
# > the first argument ($1) is the name of the command whose arguments are being completed, )
# > the second argument ($2) is the word being completed,
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
_fzf_complete_ssh() {
case $3 in
-i|-F|-E)
_fzf_path_completion "$@"
;;
*)
local user=
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}')
;;
esac
} }
_fzf_var_completion() { _fzf_var_completion() {
_fzf_complete -m -- "$@" < <( _fzf_complete -m -- "$@" < <(
declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p' declare -xp | sed 's/=.*//' | sed 's/.* //'
) )
} }
_fzf_alias_completion() { _fzf_alias_completion() {
_fzf_complete -m -- "$@" < <( _fzf_complete -m -- "$@" < <(
alias | command sed -En 's|^alias ([^=]+).*|\1|p' alias | sed 's/=.*//' | sed 's/.* //'
) )
} }
@@ -471,8 +303,8 @@ complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds=" a_cmds="
awk bat cat diff diff3 awk cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java emacs emacsclient ex file ftp g++ gcc gvim head hg java
javac ld less more mvim nvim patch perl python ruby javac ld less more mvim nvim patch perl python ruby
sed sftp sort source tail tee uniq vi view vim wc xdg-open sed sftp sort source tail tee uniq vi view vim wc xdg-open
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
@@ -481,7 +313,7 @@ a_cmds="
svn tar unzip zip" svn tar unzip zip"
# Preserve existing completion # Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null) __fzf_orig_completion < <(complete -p $d_cmds $a_cmds 2> /dev/null)
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
@@ -493,7 +325,7 @@ __fzf_defc() {
func="$2" func="$2"
opts="$3" opts="$3"
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var-}" orig="${!orig_var}"
if [[ -n "$orig" ]]; then if [[ -n "$orig" ]]; then
printf -v def "$orig" "$func" printf -v def "$orig" "$func"
eval "$def" eval "$def"
@@ -512,9 +344,6 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done done
# ssh
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
unset cmd d_cmds a_cmds unset cmd d_cmds a_cmds
_fzf_setup_completion() { _fzf_setup_completion() {
@@ -538,7 +367,9 @@ _fzf_setup_completion() {
} }
# Environment variables / Aliases / Hosts / Process # Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset printenv _fzf_setup_completion 'var' export unset
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet _fzf_setup_completion 'host' ssh telnet
_fzf_setup_completion 'proc' kill _fzf_setup_completion 'proc' kill
fi

View File

@@ -9,9 +9,6 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
[[ -o interactive ]] || return 0
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
# __fzf_completion_options such that `eval $__fzf_completion_options` sets # __fzf_completion_options such that `eval $__fzf_completion_options` sets
# all options to the same values they currently have. We'll do just that at # all options to the same values they currently have. We'll do just that at
@@ -70,12 +67,15 @@ fi
# control. There are several others that could wreck havoc if they are set # 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 # to values we don't expect. With the following `emulate` command we
# sidestep this issue entirely. # sidestep this issue entirely.
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases' 'emulate' 'zsh' '-o' 'no_aliases'
# This brace is the start of try-always block. The `always` part is like # 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. # `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 _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
@@ -99,9 +99,9 @@ fi
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
elif [ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; }; then elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then
shift shift
if [ -n "${FZF_TMUX_OPTS-}" ]; then if [ -n "$FZF_TMUX_OPTS" ]; then
fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@" fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@"
else else
fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@" fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@"
@@ -137,10 +137,7 @@ __fzf_generic_path_completion() {
tail=$6 tail=$6
setopt localoptions nonomatch setopt localoptions nonomatch
if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then eval "base=$base"
return
fi
eval "base=$base" 2> /dev/null || return
[[ $base = *"/"* ]] && dir="$base" [[ $base = *"/"* ]] && dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [[ -z "$dir" || -d ${dir} ]]; then if [[ -z "$dir" || -d ${dir} ]]; then
@@ -148,9 +145,8 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
item="${item%$suffix}$suffix" echo -n "${(q)item}$suffix "
echo -n "${(q)item} "
done) done)
matches=${matches% } matches=${matches% }
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
@@ -187,7 +183,7 @@ _fzf_complete() {
args=("$@") args=("$@")
sep= sep=
for i in {0..${#args[@]}}; do for i in {0..${#args[@]}}; do
if [[ "${args[$i]-}" = -- ]]; then if [[ "${args[$i]}" = -- ]]; then
sep=$i sep=$i
break break
fi fi
@@ -211,44 +207,28 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat type $post > /dev/null 2>&1 || post=cat
_fzf_feed_fifo "$fifo" _fzf_feed_fifo "$fifo"
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
command rm -f "$fifo" command rm -f "$fifo"
} }
# To use custom hostname lists, override __fzf_list_hosts.
# The function is expected to print hostnames, one per line as well as in the
# desired sorting and with any duplicates removed, to standard output.
if ! declare -f __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() {
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 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
}
fi
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete +m -- "$@" < <(__fzf_list_hosts) _fzf_complete +m -- "$@" < <(
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
} }
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
# The current word without the trigger is in the $prefix variable passed from the caller.
_fzf_complete_ssh() { _fzf_complete_ssh() {
local tokens=(${(z)1}) _fzf_complete +m -- "$@" < <(
case ${tokens[-1]} in setopt localoptions nonomatch
-i|-F|-E) 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 '[*?]') \
_fzf_path_completion "$prefix" "$1" <(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
local user= )
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
;;
esac
} }
_fzf_complete_export() { _fzf_complete_export() {
@@ -270,9 +250,8 @@ _fzf_complete_unalias() {
} }
_fzf_complete_kill() { _fzf_complete_kill() {
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( _fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
command ps -eo user,pid,ppid,start,time,command 2> /dev/null || command ps -ef | sed 1d
command ps -eo user,pid,ppid,time,args # For BusyBox
) )
} }
@@ -299,8 +278,8 @@ fzf-completion() {
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
# When the trigger starts with ';', it becomes a separate token # When the trigger starts with ';', it becomes a separate token
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
tokens[-2]="${tokens[-2]-}${tokens[-1]}" tokens[-2]="${tokens[-2]}${tokens[-1]}"
tokens=(${tokens[0,-2]}) tokens=(${tokens[0,-2]})
fi fi
@@ -312,9 +291,6 @@ fzf-completion() {
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
return
fi
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}} [ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
if eval "type _fzf_complete_${cmd} > /dev/null"; then if eval "type _fzf_complete_${cmd} > /dev/null"; then

View File

@@ -11,18 +11,15 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ $- =~ i ]] || return 0
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd opts local cmd opts
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ cmd="${FZF_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 f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | command cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m" opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS -m"
eval "$cmd" | eval "$cmd" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
while read -r item; do while read -r item; do
@@ -30,8 +27,10 @@ __fzf_select__() {
done done
} }
if [[ $- =~ i ]]; then
__fzfcmd() { __fzfcmd() {
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } && [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
} }
@@ -43,60 +42,28 @@ fzf-file-widget() {
__fzf_cd__() { __fzf_cd__() {
local cmd opts dir local cmd opts dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ cmd="${FZF_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 | command cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m" opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS +m"
dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
} }
if command -v perl > /dev/null; then __fzf_history__() {
__fzf_history__() { local output opts script
local output opts script opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0"
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0" script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++' output=$(
output=$( builtin fc -lnr -2147483648 |
set +o pipefail last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" |
builtin fc -lnr -2147483648 | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" | ) || return
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE" READLINE_LINE=${output#*$'\t'}
) || return if [[ -z "$READLINE_POINT" ]]; then
READLINE_LINE=${output#*$'\t'} echo "$READLINE_LINE"
if [[ -z "$READLINE_POINT" ]]; then else
echo "$READLINE_LINE" READLINE_POINT=0x7fffffff
else fi
READLINE_POINT=0x7fffffff }
fi
}
else # awk - fallback for POSIX systems
__fzf_history__() {
local output opts script n x y z d
if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
NR==1 { b = substr($0, 2); next }
/^\t/ { P(b); b = substr($0, 2); next }
{ b = b RS $0 }
END { if (NR) P(b) }'
output=$(
set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
) || return
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE"
else
READLINE_POINT=0x7fffffff
fi
}
fi
# Required to refresh the prompt after fzf # Required to refresh the prompt after fzf
bind -m emacs-standard '"\er": redraw-current-line' bind -m emacs-standard '"\er": redraw-current-line'
@@ -112,7 +79,7 @@ if (( BASH_VERSINFO[0] < 4 )); then
bind -m vi-insert '"\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 # 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`__fzf_history__`\e\C-e\er"' bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"' bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"' bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
else else
@@ -131,3 +98,5 @@ fi
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_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-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"' bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi

View File

@@ -11,9 +11,6 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
status is-interactive; or exit 0
# Key bindings # Key bindings
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
@@ -25,17 +22,17 @@ function fzf_key_bindings
set -l fzf_query $commandline[2] set -l fzf_query $commandline[2]
set -l prefix $commandline[3] set -l prefix $commandline[3]
# "-path \$dir'*/.*'" matches hidden files/folders inside $dir but not # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden. # $dir itself, even if hidden.
test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND " test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 's@^\./@@'" -o -type l -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
@@ -56,7 +53,7 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
set -l FISH_MAJOR (echo $version | cut -f1 -d.) set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
@@ -82,15 +79,15 @@ function fzf_key_bindings
set -l prefix $commandline[3] set -l prefix $commandline[3]
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND " test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ 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@^\./@@'" -o -type d -print 2> /dev/null | sed 's@^\./@@'"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd -- $result builtin cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""

View File

@@ -11,9 +11,6 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
[[ -o interactive ]] || return 0
# Key bindings # Key bindings
# ------------ # ------------
@@ -35,19 +32,21 @@ else
} }
fi fi
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases' 'emulate' 'zsh' '-o' 'no_aliases'
{ {
[[ -o interactive ]] || return 0
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_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 f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local item local item
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" | while read item; do eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@@ -56,7 +55,7 @@ __fsel() {
} }
__fzfcmd() { __fzfcmd() {
[ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; } && [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
} }
@@ -73,10 +72,10 @@ bindkey -M viins '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_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-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)" local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0
@@ -98,15 +97,13 @@ bindkey -M viins '\ec' fzf-cd-widget
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^\s*[0-9]+\**\s+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd))" FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected") num=$selected[1]
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then if [ -n "$num" ]; then
zle vi-fetch-history -n ${num%\*} zle vi-fetch-history -n $num
else # selected is a custom query, not from history
LBUFFER="$selected"
fi fi
fi fi
zle reset-prompt zle reset-prompt

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2021 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,129 +0,0 @@
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
package fzf
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[actIgnore-0]
_ = x[actStart-1]
_ = x[actClick-2]
_ = x[actInvalid-3]
_ = x[actChar-4]
_ = x[actMouse-5]
_ = x[actBeginningOfLine-6]
_ = x[actAbort-7]
_ = x[actAccept-8]
_ = x[actAcceptNonEmpty-9]
_ = x[actAcceptOrPrintQuery-10]
_ = x[actBackwardChar-11]
_ = x[actBackwardDeleteChar-12]
_ = x[actBackwardDeleteCharEof-13]
_ = x[actBackwardWord-14]
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18]
_ = x[actChangePrompt-19]
_ = x[actChangeQuery-20]
_ = x[actClearScreen-21]
_ = x[actClearQuery-22]
_ = x[actClearSelection-23]
_ = x[actClose-24]
_ = x[actDeleteChar-25]
_ = x[actDeleteCharEof-26]
_ = x[actEndOfLine-27]
_ = x[actForwardChar-28]
_ = x[actForwardWord-29]
_ = x[actKillLine-30]
_ = x[actKillWord-31]
_ = x[actUnixLineDiscard-32]
_ = x[actUnixWordRubout-33]
_ = x[actYank-34]
_ = x[actBackwardKillWord-35]
_ = x[actSelectAll-36]
_ = x[actDeselectAll-37]
_ = x[actToggle-38]
_ = x[actToggleSearch-39]
_ = x[actToggleAll-40]
_ = x[actToggleDown-41]
_ = x[actToggleUp-42]
_ = x[actToggleIn-43]
_ = x[actToggleOut-44]
_ = x[actToggleTrack-45]
_ = x[actToggleHeader-46]
_ = x[actTrack-47]
_ = x[actDown-48]
_ = x[actUp-49]
_ = x[actPageUp-50]
_ = x[actPageDown-51]
_ = x[actPosition-52]
_ = x[actHalfPageUp-53]
_ = x[actHalfPageDown-54]
_ = x[actOffsetUp-55]
_ = x[actOffsetDown-56]
_ = x[actJump-57]
_ = x[actJumpAccept-58]
_ = x[actPrintQuery-59]
_ = x[actRefreshPreview-60]
_ = x[actReplaceQuery-61]
_ = x[actToggleSort-62]
_ = x[actShowPreview-63]
_ = x[actHidePreview-64]
_ = x[actTogglePreview-65]
_ = x[actTogglePreviewWrap-66]
_ = x[actTransform-67]
_ = x[actTransformBorderLabel-68]
_ = x[actTransformHeader-69]
_ = x[actTransformPreviewLabel-70]
_ = x[actTransformPrompt-71]
_ = x[actTransformQuery-72]
_ = x[actPreview-73]
_ = x[actChangePreview-74]
_ = x[actChangePreviewWindow-75]
_ = x[actPreviewTop-76]
_ = x[actPreviewBottom-77]
_ = x[actPreviewUp-78]
_ = x[actPreviewDown-79]
_ = x[actPreviewPageUp-80]
_ = x[actPreviewPageDown-81]
_ = x[actPreviewHalfPageUp-82]
_ = x[actPreviewHalfPageDown-83]
_ = x[actPrevHistory-84]
_ = x[actPrevSelected-85]
_ = x[actPut-86]
_ = x[actNextHistory-87]
_ = x[actNextSelected-88]
_ = x[actExecute-89]
_ = x[actExecuteSilent-90]
_ = x[actExecuteMulti-91]
_ = x[actSigStop-92]
_ = x[actFirst-93]
_ = x[actLast-94]
_ = x[actReload-95]
_ = x[actReloadSync-96]
_ = x[actDisableSearch-97]
_ = x[actEnableSearch-98]
_ = x[actSelect-99]
_ = x[actDeselect-100]
_ = x[actUnbind-101]
_ = x[actRebind-102]
_ = x[actBecome-103]
_ = x[actResponse-104]
_ = x[actShowHeader-105]
_ = x[actHideHeader-106]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
}

View File

@@ -80,7 +80,6 @@ Scoring criteria
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@@ -90,10 +89,6 @@ import (
var DEBUG bool var DEBUG bool
var delimiterChars = "/,:;|"
const whiteChars = " \t\n\v\f\r\x85\xA0"
func indexAt(index int, max int, forward bool) int { func indexAt(index int, max int, forward bool) int {
if forward { if forward {
return index return index
@@ -145,51 +140,16 @@ const (
bonusFirstCharMultiplier = 2 bonusFirstCharMultiplier = 2
) )
var (
// Extra bonus for word boundary after whitespace character or beginning of the string
bonusBoundaryWhite int16 = bonusBoundary + 2
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite
)
type charClass int type charClass int
const ( const (
charWhite charClass = iota charNonWord charClass = iota
charNonWord
charDelimiter
charLower charLower
charUpper charUpper
charLetter charLetter
charNumber charNumber
) )
func Init(scheme string) bool {
switch scheme {
case "default":
bonusBoundaryWhite = bonusBoundary + 2
bonusBoundaryDelimiter = bonusBoundary + 1
case "path":
bonusBoundaryWhite = bonusBoundary
bonusBoundaryDelimiter = bonusBoundary + 1
if os.PathSeparator == '/' {
delimiterChars = "/"
} else {
delimiterChars = string([]rune{os.PathSeparator, '/'})
}
initialCharClass = charDelimiter
case "history":
bonusBoundaryWhite = bonusBoundary
bonusBoundaryDelimiter = bonusBoundary
default:
return false
}
return true
}
func posArray(withPos bool, len int) *[]int { func posArray(withPos bool, len int) *[]int {
if withPos { if withPos {
pos := make([]int, 0, len) pos := make([]int, 0, len)
@@ -221,10 +181,6 @@ func charClassOfAscii(char rune) charClass {
return charUpper return charUpper
} else if char >= '0' && char <= '9' { } else if char >= '0' && char <= '9' {
return charNumber return charNumber
} else if strings.ContainsRune(whiteChars, char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
} }
return charNonWord return charNonWord
} }
@@ -238,10 +194,6 @@ func charClassOfNonAscii(char rune) charClass {
return charNumber return charNumber
} else if unicode.IsLetter(char) { } else if unicode.IsLetter(char) {
return charLetter return charLetter
} else if unicode.IsSpace(char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
} }
return charNonWord return charNonWord
} }
@@ -254,33 +206,22 @@ func charClassOf(char rune) charClass {
} }
func bonusFor(prevClass charClass, class charClass) int16 { func bonusFor(prevClass charClass, class charClass) int16 {
if class > charNonWord { if prevClass == charNonWord && class != charNonWord {
if prevClass == charWhite { // Word boundary
// Word boundary after whitespace return bonusBoundary
return bonusBoundaryWhite } else if prevClass == charLower && class == charUpper ||
} else if prevClass == charDelimiter {
// Word boundary after a delimiter character
return bonusBoundaryDelimiter
} else if prevClass == charNonWord {
// Word boundary
return bonusBoundary
}
}
if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber { prevClass != charNumber && class == charNumber {
// camelCase letter123 // camelCase letter123
return bonusCamel123 return bonusCamel123
} else if class == charNonWord { } else if class == charNonWord {
return bonusNonWord return bonusNonWord
} else if class == charWhite {
return bonusBoundaryWhite
} }
return 0 return 0
} }
func bonusAt(input *util.Chars, idx int) int16 { func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 { if idx == 0 {
return bonusBoundaryWhite return bonusBoundary
} }
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
} }
@@ -436,7 +377,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Phase 2. Calculate bonus for each point // Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0 maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0 pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
Tsub := T[idx:] Tsub := T[idx:]
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)] H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub { for off, char := range Tsub {
@@ -476,7 +417,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
C0sub[off] = 1 C0sub[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off maxScore, maxScorePos = score, idx+off
if forward && bonus >= bonusBoundary { if forward && bonus == bonusBoundary {
break break
} }
} }
@@ -545,14 +486,11 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
s1 = Hdiag[off] + scoreMatch s1 = Hdiag[off] + scoreMatch
b := Bsub[off] b := Bsub[off]
consecutive = Cdiag[off] + 1 consecutive = Cdiag[off] + 1
if consecutive > 1 { // Break consecutive chunk
fb := B[col-int(consecutive)+1] if b == bonusBoundary {
// Break consecutive chunk consecutive = 1
if b >= bonusBoundary && b > fb { } else if consecutive > 1 {
consecutive = 1 b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
} else {
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
}
} }
if s1+b < s2 { if s1+b < s2 {
s1 += Bsub[off] s1 += Bsub[off]
@@ -617,7 +555,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern)) pos := posArray(withPos, len(pattern))
prevClass := initialCharClass prevClass := charNonWord
if sidx > 0 { if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1)) prevClass = charClassOf(text.Get(sidx - 1))
} }
@@ -645,7 +583,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
firstBonus = bonus firstBonus = bonus
} else { } else {
// Break consecutive chunk // Break consecutive chunk
if bonus >= bonusBoundary && bonus > firstBonus { if bonus == bonusBoundary {
firstBonus = bonus firstBonus = bonus
} }
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive) bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
@@ -803,7 +741,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
if bonus > bestBonus { if bonus > bestBonus {
bestPos, bestBonus = index, bonus bestPos, bestBonus = index, bonus
} }
if bonus >= bonusBoundary { if bonus == bonusBoundary {
break break
} }
index -= pidx - 1 index -= pidx - 1
@@ -939,8 +877,8 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
match = runesStr == string(pattern) match = runesStr == string(pattern)
} }
if match { if match {
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+int(bonusBoundaryWhite))*lenPattern + return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
(bonusFirstCharMultiplier-1)*int(bonusBoundaryWhite)}, nil (bonusFirstCharMultiplier-1)*bonusBoundary}, nil
} }
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
} }

View File

@@ -45,29 +45,29 @@ func TestFuzzyMatch(t *testing.T) {
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9, assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9, assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+ scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
int(bonusBoundaryWhite)*2+2*scoreGapStart+4*scoreGapExtension) bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13, assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2) scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10, assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3) scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13, assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(bonusBoundaryDelimiter)) scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10, assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension) scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10, assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension) scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9, assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+ scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
int(bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension) bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7, assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+ scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension) bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8, assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)+ scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
scoreGapStart*2+scoreGapExtension*3) scoreGapStart*2+scoreGapExtension*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4, assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*3) scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6, assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+ scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
bonusNonWord+bonusBoundary) bonusNonWord+bonusBoundary)
@@ -75,14 +75,14 @@ func TestFuzzyMatch(t *testing.T) {
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9, assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9, assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*2+ scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
scoreGapStart*2+scoreGapExtension*4) scoreGapStart*2+scoreGapExtension*4)
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7, assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+ scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
scoreGapStart*2+scoreGapExtension*2) scoreGapStart*2+scoreGapExtension*2)
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4, assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+ scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
util.Max(bonusCamel123, int(bonusBoundaryWhite))) util.Max(bonusCamel123, bonusBoundary))
// Consecutive bonus updated // Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6, assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
@@ -98,10 +98,10 @@ func TestFuzzyMatch(t *testing.T) {
func TestFuzzyMatchBackward(t *testing.T) { func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4, assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+ scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtension) scoreGapStart+scoreGapExtension)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9, assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)) scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
@@ -114,9 +114,9 @@ func TestExactMatchNaive(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2) scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+3)) scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+int(bonusBoundaryDelimiter)) scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
} }
} }
@@ -128,7 +128,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
score := scoreMatch*3 + int(bonusBoundaryWhite)*bonusFirstCharMultiplier + int(bonusBoundaryWhite)*2 score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
@@ -156,10 +156,9 @@ func TestSuffixMatch(t *testing.T) {
// Strip trailing white space from the string // Strip trailing white space from the string
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9, assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
scoreMatch*3+bonusConsecutive*2) scoreMatch*3+bonusConsecutive*2)
// Only when the pattern doesn't end with a space // Only when the pattern doesn't end with a space
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10, assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
scoreMatch*4+bonusConsecutive*2+int(bonusBoundaryWhite)) scoreMatch*4+bonusConsecutive*2+bonusNonWord)
} }
} }
@@ -183,9 +182,9 @@ func TestNormalize(t *testing.T) {
input, pattern, sidx, eidx, score) input, pattern, sidx, eidx, score)
} }
} }
test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive) test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2) test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch) test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
} }
func TestLongString(t *testing.T) { func TestLongString(t *testing.T) {

View File

@@ -55,9 +55,6 @@ func (s *ansiState) ToString() string {
if s.attr&tui.Reverse > 0 { if s.attr&tui.Reverse > 0 {
ret += "7;" ret += "7;"
} }
if s.attr&tui.StrikeThrough > 0 {
ret += "9;"
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40) ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m" return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
@@ -88,7 +85,7 @@ func isPrint(c uint8) bool {
} }
func matchOperatingSystemCommand(s string) int { func matchOperatingSystemCommand(s string) int {
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)` // `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
// ^ match starting here // ^ match starting here
// //
i := 5 // prefix matched in nextAnsiEscapeSequence() i := 5 // prefix matched in nextAnsiEscapeSequence()
@@ -106,37 +103,30 @@ func matchOperatingSystemCommand(s string) int {
} }
func matchControlSequence(s string) int { func matchControlSequence(s string) int {
// `\x1b[\\[()][0-9;:?]*[a-zA-Z@]` // `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
// ^ match starting here // ^ match starting here
// //
i := 2 // prefix matched in nextAnsiEscapeSequence() i := 2 // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s); i++ { for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';' || s[i] == '?'); i++ {
}
if i < len(s) {
c := s[i] c := s[i]
switch c { if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ';', ':', '?': return i + 1
// ok
default:
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
return i + 1
}
return -1
} }
} }
return -1 return -1
} }
func isCtrlSeqStart(c uint8) bool { func isCtrlSeqStart(c uint8) bool {
switch c { return c == '\\' || c == '[' || c == '(' || c == ')'
case '\\', '[', '(', ')':
return true
}
return false
} }
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to // nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used): // calling FindStringIndex() on the below regex (which was originally used):
// //
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)" // "(?:\x1b[\\[()][0-9;?]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
//
func nextAnsiEscapeSequence(s string) (int, int) { func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences // fast check for ANSI escape sequences
i := 0 i := 0
@@ -164,16 +154,16 @@ Loop:
return i - n, i + 1 return i - n, i + 1
} }
case '\x1b': case '\x1b':
// match: `\x1b[\\[()][0-9;:?]*[a-zA-Z@]` // match: `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) { if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
if j := matchControlSequence(s[i:]); j != -1 { if j := matchControlSequence(s[i:]); j != -1 {
return i, i + j return i, i + j
} }
} }
// match: `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)` // match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) && if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
(s[i+3] == ';' || s[i+3] == ':') && isPrint(s[i+4]) { s[i+3] == ';' && isPrint(s[i+4]) {
if j := matchOperatingSystemCommand(s[i:]); j != -1 { if j := matchOperatingSystemCommand(s[i:]); j != -1 {
return i, i + j return i, i + j
@@ -290,20 +280,9 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
return trimmed, nil, state return trimmed, nil, state
} }
func parseAnsiCode(s string, delimiter byte) (int, byte, string) { func parseAnsiCode(s string) (int, string) {
var remaining string var remaining string
i := -1 if i := strings.IndexByte(s, ';'); i >= 0 {
if delimiter == 0 {
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
if i < 0 {
i = strings.IndexByte(s, ':')
}
} else {
i = strings.IndexByte(s, delimiter)
}
if i >= 0 {
delimiter = s[i]
remaining = s[i+1:] remaining = s[i+1:]
s = s[:i] s = s[:i]
} }
@@ -315,14 +294,14 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
for _, ch := range []byte(s) { for _, ch := range []byte(s) {
ch -= '0' ch -= '0'
if ch > 9 { if ch > 9 {
return -1, delimiter, remaining return -1, remaining
} }
code = code*10 + int(ch) code = code*10 + int(ch)
} }
return code, delimiter, remaining return code, remaining
} }
return -1, delimiter, remaining return -1, remaining
} }
func interpretCode(ansiCode string, prevState *ansiState) ansiState { func interpretCode(ansiCode string, prevState *ansiState) ansiState {
@@ -350,12 +329,9 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0 state256 := 0
ptr := &state.fg ptr := &state.fg
var delimiter byte = 0
count := 0
for len(ansiCode) != 0 { for len(ansiCode) != 0 {
var num int var num int
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 { if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
count++
switch state256 { switch state256 {
case 0: case 0:
switch num { switch num {
@@ -381,21 +357,10 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state.attr = state.attr | tui.Blink state.attr = state.attr | tui.Blink
case 7: case 7:
state.attr = state.attr | tui.Reverse state.attr = state.attr | tui.Reverse
case 9:
state.attr = state.attr | tui.StrikeThrough
case 22:
state.attr = state.attr &^ tui.Bold
state.attr = state.attr &^ tui.Dim
case 23: // tput rmso case 23: // tput rmso
state.attr = state.attr &^ tui.Italic state.attr = state.attr &^ tui.Italic
case 24: // tput rmul case 24: // tput rmul
state.attr = state.attr &^ tui.Underline state.attr = state.attr &^ tui.Underline
case 25:
state.attr = state.attr &^ tui.Blink
case 27:
state.attr = state.attr &^ tui.Reverse
case 29:
state.attr = state.attr &^ tui.StrikeThrough
case 0: case 0:
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
@@ -437,13 +402,6 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
} }
} }
// Empty sequence: reset
if count == 0 {
state.fg = -1
state.bg = -1
state.attr = 0
}
if state256 > 0 { if state256 > 0 {
*ptr = -1 *ptr = -1
} }

View File

@@ -15,14 +15,14 @@ import (
// testing nextAnsiEscapeSequence(). // testing nextAnsiEscapeSequence().
// //
// References: // References:
// - https://github.com/gnachman/iTerm2 // - https://github.com/gnachman/iTerm2
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php // - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
// (archived from http://ascii-table.com/ansi-escape-sequences.php) // (archived from http://ascii-table.com/ansi-escape-sequences.php)
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php // - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php) // (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html // - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html // - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)") var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
func testParserReference(t testing.TB, str string) { func testParserReference(t testing.TB, str string) {
t.Helper() t.Helper()
@@ -348,9 +348,6 @@ func TestAnsiCodeStringConversion(t *testing.T) {
} }
assert("\x1b[m", nil, "") assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "") assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[31m", nil, "\x1b[31;49m") assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m") assert("\x1b[41m", nil, "\x1b[39;41m")
@@ -361,7 +358,6 @@ func TestAnsiCodeStringConversion(t *testing.T) {
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m") assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m") assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m") assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m") assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m") assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
assert("\x1b[48;5;100;38;2;10;20;30;7m", assert("\x1b[48;5;100;38;2;10;20;30;7m",
@@ -381,7 +377,7 @@ func TestParseAnsiCode(t *testing.T) {
{"-2", "", -1}, {"-2", "", -1},
} }
for _, x := range tests { for _, x := range tests {
n, _, s := parseAnsiCode(x.In, 0) n, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp { if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp) t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
} }
@@ -389,9 +385,9 @@ func TestParseAnsiCode(t *testing.T) {
} }
// kernel/bpf/preload/iterators/README // kernel/bpf/preload/iterators/README
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" + const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" + "\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
"\x1b[0m\x1b[38:5:149mMakefile\x1b[m\x1b[K\x1b[0m" "\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
func BenchmarkNextAnsiEscapeSequence(b *testing.B) { func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString))) b.SetBytes(int64(len(ansiBenchmarkString)))

View File

@@ -58,9 +58,9 @@ var defaultCommand string
func init() { func init() {
if !util.IsWindows() { if !util.IsWindows() {
defaultCommand = `set -o pipefail; 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 l -print 2> /dev/null | cut -b3-` defaultCommand = `set -o pipefail; 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 l -print 2> /dev/null | cut -b3-`
} else if os.Getenv("TERM") == "cygwin" { } else if os.Getenv("TERM") == "cygwin" {
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
} }
} }

View File

@@ -1,4 +1,28 @@
// Package fzf implements fzf, a command-line fuzzy finder. /*
Package fzf implements fzf, a command-line fuzzy finder.
The MIT License (MIT)
Copyright (c) 2013-2021 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package fzf package fzf
import ( import (
@@ -122,25 +146,21 @@ func Run(opts *Options, version string, revision string) {
// Matcher // Matcher
forward := true forward := true
withPos := false for _, cri := range opts.Criteria[1:] {
for idx := len(opts.Criteria) - 1; idx > 0; idx-- { if cri == byEnd {
switch opts.Criteria[idx] {
case byChunk:
withPos = true
case byEnd:
forward = false forward = false
case byBegin: break
forward = true }
if cri == byBegin {
break
} }
} }
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
inputRevision := 0 matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@@ -196,55 +216,33 @@ func Run(opts *Options, version string, revision string) {
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox) terminal := NewTerminal(opts, eventBox)
maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0
heightUnknown := opts.Height.auto
if heightUnknown {
maxFit, padHeight = terminal.MaxFitAndPad()
}
deferred := opts.Select1 || opts.Exit0 deferred := opts.Select1 || opts.Exit0
go terminal.Loop() go terminal.Loop()
if !deferred && !heightUnknown { if !deferred {
// Start right away terminal.startChan <- true
terminal.startChan <- fitpad{-1, -1}
} }
// Event coordination // Event coordination
reading := true reading := true
clearCache := util.Once(false)
clearSelection := util.Once(false)
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *string
eventBox.Watch(EvtReadNew)
total := 0
query := []rune{}
determine := func(final bool) {
if heightUnknown {
if total >= maxFit || final {
deferred = false
heightUnknown = false
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
}
} else if deferred {
deferred = false
terminal.startChan <- fitpad{-1, -1}
}
}
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command string) { restart := func(command string) {
reading = true reading = true
clearCache = util.Once(true)
clearSelection = util.Once(true)
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision++
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command) go reader.restart(command)
} }
eventBox.Watch(EvtReadNew)
query := []rune{}
for { for {
delay := true delay := true
ticks++ ticks++
input := func() []rune { input := func(reloaded bool) []rune {
reloaded := snapshotRevision != inputRevision
paused, input := terminal.Input() paused, input := terminal.Input()
if reloaded && paused { if reloaded && paused {
query = []rune{} query = []rune{}
@@ -272,35 +270,21 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
if useSnapshot && evt == EvtReadFin { snapshot, count := chunkList.Snapshot()
useSnapshot = false terminal.UpdateCount(count, !reading, value.(*string))
}
if !useSnapshot {
snapshot, count = chunkList.Snapshot()
snapshotRevision = inputRevision
}
total = count
terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync { if opts.Sync {
opts.Sync = false opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision)) terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
} }
if heightUnknown && !deferred { reset := clearCache()
determine(!reading) matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
}
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *string
var changed bool
switch val := value.(type) { switch val := value.(type) {
case searchRequest: case searchRequest:
sort = val.sort sort = val.sort
command = val.command command = val.command
changed = val.changed
if command != nil {
useSnapshot = val.sync
}
} }
if command != nil { if command != nil {
if reading { if reading {
@@ -309,20 +293,11 @@ func Run(opts *Options, version string, revision string) {
} else { } else {
restart(*command) restart(*command)
} }
}
if !changed {
break break
} }
if !useSnapshot { snapshot, _ := chunkList.Snapshot()
newSnapshot, _ := chunkList.Snapshot() reset := clearCache()
// We want to avoid showing empty list when reload is triggered matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
// and the query string is changed at the same time i.e. command != nil && changed
if command == nil || len(newSnapshot) > 0 {
snapshot = newSnapshot
snapshotRevision = inputRevision
}
}
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -342,7 +317,8 @@ func Run(opts *Options, version string, revision string) {
if deferred { if deferred {
count := val.Length() count := val.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
determine(val.final) deferred = false
terminal.startChan <- true
} else if val.final { } else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery { if opts.PrintQuery {
@@ -359,10 +335,11 @@ func Run(opts *Options, version string, revision string) {
} }
os.Exit(exitNoMatch) os.Exit(exitNoMatch)
} }
determine(val.final) deferred = false
terminal.startChan <- true
} }
} }
terminal.UpdateList(val) terminal.UpdateList(val, clearSelection())
} }
} }
} }

View File

@@ -2,6 +2,7 @@ package fzf
import ( import (
"errors" "errors"
"io/ioutil"
"os" "os"
"strings" "strings"
) )
@@ -25,12 +26,12 @@ func NewHistory(path string, maxSize int) (*History, error) {
} }
// Read history file // Read history file
data, err := os.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
// If it doesn't exist, check if we can create a file with the name // If it doesn't exist, check if we can create a file with the name
if os.IsNotExist(err) { if os.IsNotExist(err) {
data = []byte{} data = []byte{}
if err := os.WriteFile(path, data, 0600); err != nil { if err := ioutil.WriteFile(path, data, 0600); err != nil {
return nil, fmtError(err) return nil, fmtError(err)
} }
} else { } else {
@@ -61,11 +62,11 @@ func (h *History) append(line string) error {
lines = lines[len(lines)-h.maxSize:] lines = lines[len(lines)-h.maxSize:]
} }
h.lines = append(lines, "") h.lines = append(lines, "")
return os.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
} }
func (h *History) override(str string) { func (h *History) override(str string) {
// You can update the history, but they're not written to the file // You can update the history but they're not written to the file
if h.cursor == len(h.lines)-1 { if h.cursor == len(h.lines)-1 {
h.lines[h.cursor] = str h.lines[h.cursor] = str
} else if h.cursor < len(h.lines)-1 { } else if h.cursor < len(h.lines)-1 {

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os" "os"
"runtime" "runtime"
"testing" "testing"
@@ -24,7 +25,7 @@ func TestHistory(t *testing.T) {
} }
} }
f, _ := os.CreateTemp("", "fzf-history") f, _ := ioutil.TempFile("", "fzf-history")
f.Close() f.Close()
{ // Append lines { // Append lines

View File

@@ -12,11 +12,11 @@ import (
// MatchRequest represents a search request // MatchRequest represents a search request
type MatchRequest struct { type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
revision int clearCache bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -29,7 +29,6 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int
} }
const ( const (
@@ -39,7 +38,7 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
@@ -49,8 +48,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: partitions, partitions: partitions,
slab: make([]*util.Slab, partitions), slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger), mergerCache: make(map[string]*Merger)}
revision: revision}
} }
// Loop puts Matcher in action // Loop puts Matcher in action
@@ -72,9 +70,8 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort || request.revision != m.revision { if request.sort != m.sort || request.clearCache {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() clearChunkCache()
} }
@@ -143,11 +140,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
numChunks := len(request.chunks) numChunks := len(request.chunks)
if numChunks == 0 { if numChunks == 0 {
return EmptyMerger(request.revision), false return EmptyMerger, false
} }
pattern := request.pattern pattern := request.pattern
if pattern.IsEmpty() { if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac, request.revision), false return PassMerger(&request.chunks, m.tac), false
} }
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
@@ -221,11 +218,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false return NewMerger(pattern, partialResults, m.sort, m.tac), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -234,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
} }

View File

@@ -3,36 +3,30 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
func EmptyMerger(revision int) *Merger { var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
return NewMerger(nil, [][]Result{}, false, false, revision)
}
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
type Merger struct { type Merger struct {
pattern *Pattern pattern *Pattern
lists [][]Result lists [][]Result
merged []Result merged []Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
tac bool tac bool
final bool final bool
count int count int
pass bool
revision int
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger { func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
chunks: chunks, chunks: chunks,
tac: tac, tac: tac,
count: 0, count: 0}
pass: true,
revision: revision}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@@ -41,18 +35,17 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
merged: []Result{}, merged: []Result{},
chunks: nil, chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
tac: tac, tac: tac,
final: false, final: false,
count: 0, count: 0}
revision: revision}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)
@@ -60,42 +53,11 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
return &mg return &mg
} }
// Revision returns revision number
func (mg *Merger) Revision() int {
return mg.revision
}
// Length returns the number of items // Length returns the number of items
func (mg *Merger) Length() int { func (mg *Merger) Length() int {
return mg.count return mg.count
} }
func (mg *Merger) First() Result {
if mg.tac && !mg.sorted {
return mg.Get(mg.count - 1)
}
return mg.Get(0)
}
// FindIndex returns the index of the item with the given item index
func (mg *Merger) FindIndex(itemIndex int32) int {
index := -1
if mg.pass {
index = int(itemIndex)
if mg.tac {
index = mg.count - index - 1
}
} else {
for i := 0; i < mg.count; i++ {
if mg.Get(i).item.Index() == itemIndex {
index = i
break
}
}
}
return index
}
// Get returns the pointer to the Result object indexed by the given integer // Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) Result { func (mg *Merger) Get(idx int) Result {
if mg.chunks != nil { if mg.chunks != nil {

View File

@@ -23,10 +23,10 @@ func randResult() Result {
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
assert(t, EmptyMerger(0).Length() == 0, "Not empty") assert(t, EmptyMerger.Length() == 0, "Not empty")
assert(t, EmptyMerger(0).count == 0, "Invalid count") assert(t, EmptyMerger.count == 0, "Invalid count")
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists") assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]Result, []Result) { func buildLists(partiallySorted bool) ([][]Result, []Result) {
@@ -57,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(nil, lists, false, false, 0) mg := NewMerger(nil, lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -69,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(nil, lists, true, false, 0) mg := NewMerger(nil, lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@@ -79,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(nil, lists, true, false, 0) mg2 := NewMerger(nil, lists, true, false)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ package fzf
import ( import (
"fmt" "fmt"
"os" "io/ioutil"
"testing" "testing"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
@@ -262,17 +262,13 @@ func TestBind(t *testing.T) {
} }
} }
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA.AsEvent(), "", actKillLine) check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp) check(tui.Key('c'), "", actPageUp)
@@ -290,15 +286,12 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, "f1:abort", errorFn) parseKeymap(keymap, "f1:abort")
check(tui.F1.AsEvent(), "", actAbort) check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
@@ -357,14 +350,14 @@ func TestDefaultCtrlNP(t *testing.T) {
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
f, _ := os.CreateTemp("", "fzf-history") f, _ := ioutil.TempFile("", "fzf-history")
f.Close() f.Close()
hist := "--history=" + f.Name() hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory) check([]string{hist}, tui.CtrlN, actNextHistory)
check([]string{hist}, tui.CtrlP, actPrevHistory) check([]string{hist}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPrevHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
@@ -473,38 +466,3 @@ func TestValidateSign(t *testing.T) {
} }
} }
} }
func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {})
if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions))
}
if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
t.Errorf("Invalid action parsed: %v", actions[0])
}
if actions[1].t != actUp || actions[2].t != actUp {
t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
}
if actions[3].t != actReload || actions[3].a != "down+down" {
t.Errorf("Invalid action parsed: %v", actions[3])
}
}
func TestParseSingleActionListError(t *testing.T) {
err := ""
parseSingleActionList("change-query(foobar)baz", func(e string) {
err = e
})
if len(err) == 0 {
t.Errorf("Failed to detect error")
}
}
func TestMaskActionContents(t *testing.T) {
original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
expected := ":execute +change-query +up,x:reload "
masked := maskActionContents(original)
if masked != expected {
t.Errorf("Not masked: %s", masked)
}
}

View File

@@ -51,7 +51,6 @@ type Pattern struct {
caseSensitive bool caseSensitive bool
normalize bool normalize bool
forward bool forward bool
withPos bool
text []rune text []rune
termSets []termSet termSets []termSet
sortable bool sortable bool
@@ -86,7 +85,7 @@ func clearChunkCache() {
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
if extended { if extended {
@@ -146,7 +145,6 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
normalize: normalize, normalize: normalize,
forward: forward, forward: forward,
withPos: withPos,
text: []rune(asString), text: []rune(asString),
termSets: termSets, termSets: termSets,
sortable: sortable, sortable: sortable,
@@ -304,13 +302,13 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
} }
} else { } else {
for _, result := range space { for _, result := range space {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
} }

View File

@@ -67,7 +67,7 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc")) chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
@@ -83,7 +83,7 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str)) chars := util.ToChars([]byte(str))
@@ -106,17 +106,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -129,7 +129,7 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}}) trans := Transform(tokens, []Range{{1, 1}})
@@ -164,7 +164,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache() clearPatternCache()
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr)) pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
@@ -188,7 +188,7 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) { func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) { test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache() clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str)) pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }

View File

@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {
unix.PledgePromises("stdio rpath tty proc exec inet tmppath") unix.PledgePromises("stdio rpath tty proc exec")
} }

View File

@@ -6,7 +6,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -99,17 +98,8 @@ func (r *Reader) ReadSource() {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if util.IsTty() {
// The default command for *nix requires a shell that supports "pipefail" // The default command for *nix requires bash
// https://unix.stackexchange.com/a/654932/62171
shell := "bash" shell := "bash"
currentShell := os.Getenv("SHELL")
currentShellName := path.Base(currentShell)
for _, shellName := range []string{"bash", "zsh", "ksh", "ash", "hush", "mksh", "yash"} {
if currentShellName == shellName {
shell = currentShell
break
}
}
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
if defaultCommand != "" { if defaultCommand != "" {

View File

@@ -49,22 +49,6 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
case byScore: case byScore:
// Higher is better // Higher is better
val = math.MaxUint16 - util.AsUint16(score) val = math.MaxUint16 - util.AsUint16(score)
case byChunk:
if validOffsetFound {
b := minBegin
e := maxEnd
for ; b >= 1; b-- {
if unicode.IsSpace(item.text.Get(b - 1)) {
break
}
}
for ; e < numChars; e++ {
if unicode.IsSpace(item.text.Get(e)) {
break
}
}
val = util.AsUint16(e - b)
}
case byLength: case byLength:
val = item.TrimLength() val = item.TrimLength()
case byBegin, byEnd: case byBegin, byEnd:
@@ -80,7 +64,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
if criterion == byBegin { if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen) val = util.AsUint16(minEnd - whitePrefixLen)
} else { } else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1)) val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
} }
} }
} }
@@ -138,9 +122,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
for i := off[0]; i < off[1]; i++ { for i := off[0]; i < off[1]; i++ {
// Negative of 1-based index of itemColors // Negative of 1-based index of itemColors
// - The extra -1 means highlighted // - The extra -1 means highlighted
if cols[i] >= 0 { cols[i] = cols[i]*-1 - 1
cols[i] = cols[i]*-1 - 1
}
} }
} }

View File

@@ -54,9 +54,9 @@ func TestResultRank(t *testing.T) {
// FIXME global // FIXME global
sortCriteria = []criterion{byScore, byLength} sortCriteria = []criterion{byScore, byLength}
str := []rune("foo") strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult( item1 := buildResult(
withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2) withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
if item1.points[3] != math.MaxUint16-2 || // Bonus if item1.points[3] != math.MaxUint16-2 || // Bonus
item1.points[2] != 3 || // Length item1.points[2] != 3 || // Length
item1.points[1] != 0 || // Unused item1.points[1] != 0 || // Unused
@@ -65,7 +65,7 @@ func TestResultRank(t *testing.T) {
t.Error(item1) t.Error(item1)
} }
// Only differ in index // Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2) item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
items := []Result{item1, item2} items := []Result{item1, item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
@@ -98,29 +98,12 @@ func TestResultRank(t *testing.T) {
} }
} }
func TestChunkTiebreak(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byChunk}
score := 100
test := func(input string, offset Offset, chunk string) {
item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
t.Error(item.points)
}
}
test("hello foobar goodbye", Offset{8, 9}, "foobar")
test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
test("hello foobar goodbye", Offset{0, 1}, "hello")
test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
}
func TestColorOffset(t *testing.T) { func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ---- // ------------ 20 ---- -- ----
// ++++++++ ++++++++++ // ++++++++ ++++++++++
// --++++++++-- --++++++++++--- // --++++++++-- --++++++++++---
offsets := []Offset{{5, 15}, {10, 12}, {25, 35}} offsets := []Offset{{5, 15}, {25, 35}}
item := Result{ item := Result{
item: &Item{ item: &Item{
colors: &[]ansiOffset{ colors: &[]ansiOffset{

View File

@@ -1,254 +0,0 @@
package fzf
import (
"bufio"
"bytes"
"crypto/subtle"
"errors"
"fmt"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"
)
var getRegex *regexp.Regexp
func init() {
getRegex = regexp.MustCompile(`^GET /(?:\?([a-z0-9=&]+))? HTTP`)
}
type getParams struct {
limit int
offset int
}
const (
crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
httpReadTimeout = 10 * time.Second
jsonContentType = "Content-Type: application/json" + crlf
maxContentLength = 1024 * 1024
)
type httpServer struct {
apiKey []byte
actionChannel chan []*action
responseChannel chan string
}
type listenAddress struct {
host string
port int
}
func (addr listenAddress) IsLocal() bool {
return addr.host == "localhost" || addr.host == "127.0.0.1"
}
var defaultListenAddr = listenAddress{"localhost", 0}
func parseListenAddress(address string) (error, listenAddress) {
parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 {
parts = []string{"localhost", parts[0]}
}
if len(parts) != 2 {
return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr
}
portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 {
return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr
}
if len(parts[0]) == 0 {
parts[0] = "localhost"
}
return nil, listenAddress{parts[0], port}
}
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) {
host := address.host
port := address.port
apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 {
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
}
addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr)
if err != nil {
return fmt.Errorf("failed to listen on %s", addrStr), port
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil {
return err, port
}
}
server := httpServer{
apiKey: []byte(apiKey),
actionChannel: actionChannel,
responseChannel: responseChannel,
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
}
}
conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close()
}
listener.Close()
}()
return nil, port
}
// Here we are writing a simplistic HTTP server without using net/http
// package to reduce the size of the binary.
//
// * No --listen: 2.8MB
// * --listen with net/http: 5.7MB
// * --listen w/o net/http: 3.3MB
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
contentLength := 0
apiKey := ""
body := ""
answer := func(code string, message string) string {
message += "\n"
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
}
unauthorized := func(message string) string {
return answer(httpUnauthorized, message)
}
bad := func(message string) string {
return answer(httpBadRequest, message)
}
good := func(message string) string {
return answer(httpOk+jsonContentType, message)
}
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
scanner := bufio.NewScanner(conn)
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
found := bytes.Index(data, []byte(crlf))
if found >= 0 {
token := data[:found+len(crlf)]
return len(token), token, nil
}
if atEOF || len(body)+len(data) >= contentLength {
return 0, data, bufio.ErrFinalToken
}
return 0, nil, nil
})
section := 0
for scanner.Scan() {
text := scanner.Text()
switch section {
case 0:
getMatch := getRegex.FindStringSubmatch(text)
if len(getMatch) > 0 {
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
select {
case response := <-server.responseChannel:
return good(response)
case <-time.After(2 * time.Second):
go func() {
// Drain the channel
<-server.responseChannel
}()
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
}
} else if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
section++
case 1:
if text == crlf {
if contentLength == 0 {
return bad("content-length header missing")
}
section++
continue
}
pair := strings.SplitN(text, ":", 2)
if len(pair) == 2 {
switch strings.ToLower(pair[0]) {
case "content-length":
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
if err != nil || length <= 0 || length > maxContentLength {
return bad("invalid content length")
}
contentLength = length
case "x-api-key":
apiKey = strings.TrimSpace(pair[1])
}
}
case 2:
body += text
}
}
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
return unauthorized("invalid api key")
}
if len(body) < contentLength {
return bad("incomplete request")
}
body = body[:contentLength]
errorMessage := ""
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
}
if len(actions) == 0 {
return bad("no action specified")
}
server.actionChannel <- actions
return httpOk + crlf
}
func parseGetParams(query string) getParams {
params := getParams{limit: 100, offset: 0}
for _, pair := range strings.Split(query, "&") {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
switch parts[0] {
case "limit":
val, err := strconv.Atoi(parts[1])
if err == nil {
params.limit = val
}
case "offset":
val, err := strconv.Atoi(parts[1])
if err == nil {
params.offset = val
}
}
}
}
return params
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,20 +12,6 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
return replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: stripAnsi,
delimiter: delimiter,
printsep: printsep,
forcePlus: forcePlus,
query: query,
allItems: allItems,
lastAction: actBackwardDeleteCharEof,
prompt: "prompt",
})
}
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1} items1 := []*Item{item1, item1}
@@ -66,90 +52,90 @@ func TestReplacePlaceholder(t *testing.T) {
*/ */
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {q} // {q}
result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// forcePlus // forcePlus
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// Whitespace preserving flag with "'" delimiter // Whitespace preserving flag with "'" delimiter
result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.O}}") checkFormat("echo {{.O}} foo{{.O}}")
result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}}bar baz{{.O}}") checkFormat("echo {{.O}}bar baz{{.O}}")
result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// Whitespace preserving flag with regex delimiter // Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`) regex = regexp.MustCompile(`\w+`)
result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}}{{.I}}{{.O}}") checkFormat("echo {{.O}}{{.I}}{{.O}}")
result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
// No match // No match
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter // String delimiter
result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
// Regex delimiter // Regex delimiter
regex = regexp.MustCompile("[oa]+") regex = regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
/* /*
@@ -169,6 +155,7 @@ func TestReplacePlaceholder(t *testing.T) {
newItem("7a 7b 7c 7d 7e 7f"), newItem("7a 7b 7c 7d 7e 7f"),
} }
stripAnsi := false stripAnsi := false
printsep = "\n"
forcePlus := false forcePlus := false
query := "sample query" query := "sample query"
@@ -211,23 +198,18 @@ func TestReplacePlaceholder(t *testing.T) {
// query flag is not removed after parsing, so it gets doubled // query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes // while the double q is invalid, it is useful here for testing purposes
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
// IV. escaping placeholder // IV. escaping placeholder
templateToOutput[`\{}`] = `{}` templateToOutput[`\{}`] = `{}`
templateToOutput[`\{q}`] = `{q}`
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
templateToOutput[`\{++}`] = `{++}` templateToOutput[`\{++}`] = `{++}`
templateToOutput[`{++}`] = templateToOutput[`{+}`] templateToOutput[`{++}`] = templateToOutput[`{+}`]
for giveTemplate, wantOutput := range templateToOutput { for giveTemplate, wantOutput := range templateToOutput {
result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
checkFormat(wantOutput) checkFormat(wantOutput)
} }
for giveTemplate, wantOutput := range templateToFile { for giveTemplate, wantOutput := range templateToFile {
path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
data, err := readFile(path) data, err := readFile(path)
if err != nil { if err != nil {
@@ -439,10 +421,10 @@ func TestPowershellCommands(t *testing.T) {
} }
/* /*
Test typical valid placeholders and parsing of them. Test typical valid placeholders and parsing of them.
Also since the parser assumes the input is matched with `placeholder` regex, Also since the parser assumes the input is matched with `placeholder` regex,
the regex is tested here as well. the regex is tested here as well.
*/ */
func TestParsePlaceholder(t *testing.T) { func TestParsePlaceholder(t *testing.T) {
// give, want pairs // give, want pairs
@@ -581,7 +563,7 @@ func testCommands(t *testing.T, tests []testCase) {
// evaluate the test cases // evaluate the test cases
for idx, test := range tests { for idx, test := range tests {
gotOutput := replacePlaceholderTest( gotOutput := replacePlaceholder(
test.give.template, stripAnsi, delimiter, printsep, forcePlus, test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query, test.give.query,
test.give.allItems) test.give.allItems)
@@ -623,7 +605,7 @@ func (flags placeholderFlags) encodePlaceholder() string {
if flags.file { if flags.file {
encoded += "f" encoded += "f"
} }
if flags.forceUpdate { // FIXME if flags.query {
encoded += "q" encoded += "q"
} }
return encoded return encoded

View File

@@ -7,35 +7,14 @@ import (
"os/signal" "os/signal"
"strings" "strings"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
var escaper *strings.Replacer
func init() {
tokens := strings.Split(os.Getenv("SHELL"), "/")
if tokens[len(tokens)-1] == "fish" {
// https://fishshell.com/docs/current/language.html#quotes
// > The only meaningful escape sequences in single quotes are \', which
// > escapes a single quote and \\, which escapes the backslash symbol.
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
} else {
escaper = strings.NewReplacer("'", "'\\''")
}
}
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH) signal.Notify(resizeChan, syscall.SIGWINCH)
} }
func notifyStop(p *os.Process) { func notifyStop(p *os.Process) {
pid := p.Pid p.Signal(syscall.SIGSTOP)
pgid, err := unix.Getpgid(pid)
if err == nil {
pid = pgid * -1
}
unix.Kill(pid, syscall.SIGSTOP)
} }
func notifyOnCont(resizeChan chan<- os.Signal) { func notifyOnCont(resizeChan chan<- os.Signal) {
@@ -43,5 +22,5 @@ func notifyOnCont(resizeChan chan<- os.Signal) {
} }
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
return "'" + escaper.Replace(entry) + "'" return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }

View File

@@ -8,41 +8,32 @@ func HasFullscreenRenderer() bool {
return false return false
} }
var DefaultBorderShape BorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
} }
const ( const (
AttrUndefined = Attr(0) AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8) AttrRegular = Attr(1 << 7)
AttrClear = Attr(1 << 9) AttrClear = Attr(1 << 8)
Bold = Attr(1) Bold = Attr(1)
Dim = Attr(1 << 1) Dim = Attr(1 << 1)
Italic = Attr(1 << 2) Italic = Attr(1 << 2)
Underline = Attr(1 << 3) Underline = Attr(1 << 3)
Blink = Attr(1 << 4) Blink = Attr(1 << 4)
Blink2 = Attr(1 << 5) Blink2 = Attr(1 << 5)
Reverse = Attr(1 << 6) Reverse = Attr(1 << 6)
StrikeThrough = Attr(1 << 7)
) )
func (r *FullscreenRenderer) Init() {} func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) PassThrough(string) {} func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Clear() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) Top() int { return 0 }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 }

View File

@@ -10,6 +10,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
"golang.org/x/term" "golang.org/x/term"
@@ -30,31 +31,21 @@ const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8")
}
func (r *LightRenderer) stderr(str string) { func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true, "") r.stderrInternal(str, true)
} }
const CR string = "\x1b[2m␍" // FIXME: Need better handling of non-displayable characters
const LF string = "\x1b[2m␊" func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
bytes := []byte(str) bytes := []byte(str)
runes := []rune{} runes := []rune{}
for len(bytes) > 0 { for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes) r, sz := utf8.DecodeRune(bytes)
nlcr := r == '\n' || r == '\r' nlcr := r == '\n' || r == '\r'
if r >= 32 || r == '\x1b' || nlcr { if r >= 32 || r == '\x1b' || nlcr {
if nlcr && !allowNLCR { if r == utf8.RuneError || nlcr && !allowNLCR {
if r == '\r' { runes = append(runes, ' ')
runes = append(runes, []rune(CR+resetCode)...) } else {
} else {
runes = append(runes, []rune(LF+resetCode)...)
}
} else if r != utf8.RuneError {
runes = append(runes, r) runes = append(runes, r)
} }
} }
@@ -63,10 +54,8 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode str
r.queued.WriteString(string(runes)) r.queued.WriteString(string(runes))
} }
func (r *LightRenderer) csi(code string) string { func (r *LightRenderer) csi(code string) {
fullcode := "\x1b[" + code r.stderr("\x1b[" + code)
r.stderr(fullcode)
return fullcode
} }
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
@@ -83,7 +72,7 @@ type LightRenderer struct {
forceBlack bool forceBlack bool
clearOnExit bool clearOnExit bool
prevDownTime time.Time prevDownTime time.Time
clicks [][2]int clickY []int
ttyin *os.File ttyin *os.File
buffer []byte buffer []byte
origState *term.State origState *term.State
@@ -185,7 +174,10 @@ func (r *LightRenderer) Init() {
} }
} }
r.enableMouse() if r.mouse {
r.csi("?1000h")
r.csi("?1006h")
}
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K") r.csi("K")
@@ -197,10 +189,6 @@ func (r *LightRenderer) Init() {
} }
} }
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
r.maxHeightFunc = maxHeightFunc
}
func (r *LightRenderer) makeSpace() { func (r *LightRenderer) makeSpace() {
r.stderr("\n") r.stderr("\n")
r.csi("G") r.csi("G")
@@ -401,7 +389,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{F3, 0, nil} return Event{F3, 0, nil}
case 'S': case 'S':
return Event{F4, 0, nil} return Event{F4, 0, nil}
case '1', '2', '3', '4', '5', '6', '7', '8': case '1', '2', '3', '4', '5', '6':
if len(r.buffer) < 4 { if len(r.buffer) < 4 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -433,29 +421,13 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case '3': case '3':
if r.buffer[3] == '~' { return Event{Del, 0, nil}
return Event{Del, 0, nil}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '5':
return Event{CtrlDelete, 0, nil}
case '2':
return Event{SDelete, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
return Event{PgUp, 0, nil} return Event{PgUp, 0, nil}
case '6': case '6':
return Event{PgDn, 0, nil} return Event{PgDn, 0, nil}
case '7':
return Event{Home, 0, nil}
case '8':
return Event{End, 0, nil}
case '1': case '1':
switch r.buffer[3] { switch r.buffer[3] {
case '~': case '~':
@@ -568,8 +540,8 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
t := atoi(elems[0], -1) t := atoi(elems[0], -1)
x := atoi(elems[1], -1) - 1 x := atoi(elems[1], -1) - 1
y := atoi(elems[2], -1) - 1 - r.yoffset y := atoi(elems[2], -1) - 1
if t < 0 || x < 0 { if t < 0 || x < 0 || y < 0 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz += end + 1 *sz += end + 1
@@ -593,31 +565,25 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// ctrl := t & 0b1000 // ctrl := t & 0b1000
mod := t&0b1100 > 0 mod := t&0b1100 > 0
drag := t&0b100000 > 0
if scroll != 0 { if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
} }
double := false double := false
if down && !drag { if down {
now := time.Now() now := time.Now()
if !left { // Right double click is not allowed if !left { // Right double click is not allowed
r.clicks = [][2]int{} r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration { } else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clicks = append(r.clicks, [2]int{x, y}) r.clickY = append(r.clickY, y)
} else { } else {
r.clicks = [][2]int{{x, y}} r.clickY = []int{y}
} }
r.prevDownTime = now r.prevDownTime = now
} else { } else {
n := len(r.clicks) if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
if len(r.clicks) > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1] &&
time.Since(r.prevDownTime) < doubleClickDuration { time.Since(r.prevDownTime) < doubleClickDuration {
double = true double = true
if double {
r.clicks = [][2]int{}
}
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
@@ -632,7 +598,6 @@ func (r *LightRenderer) rmcup() {
} }
func (r *LightRenderer) Pause(clear bool) { func (r *LightRenderer) Pause(clear bool) {
r.disableMouse()
r.restoreTerminal() r.restoreTerminal()
if clear { if clear {
if r.fullscreen { if r.fullscreen {
@@ -645,22 +610,6 @@ func (r *LightRenderer) Pause(clear bool) {
} }
} }
func (r *LightRenderer) enableMouse() {
if r.mouse {
r.csi("?1000h")
r.csi("?1002h")
r.csi("?1006h")
}
}
func (r *LightRenderer) disableMouse() {
if r.mouse {
r.csi("?1000l")
r.csi("?1002l")
r.csi("?1006l")
}
}
func (r *LightRenderer) Resume(clear bool, sigcont bool) { func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal() r.setupTerminal()
if clear { if clear {
@@ -669,13 +618,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
} else { } else {
r.rmcup() r.rmcup()
} }
r.enableMouse()
r.flush() r.flush()
} else if sigcont && !r.fullscreen && r.mouse { } else if sigcont && !r.fullscreen && r.mouse {
// NOTE: SIGCONT (Coming back from CTRL-Z): // NOTE: SIGCONT (Coming back from CTRL-Z):
// It's highly likely that the offset we obtained at the beginning is // It's highly likely that the offset we obtained at the beginning is
// no longer correct, so we simply disable mouse input. // no longer correct, so we simply disable mouse input.
r.disableMouse() r.csi("?1000l")
r.csi("?1006l")
r.mouse = false r.mouse = false
} }
} }
@@ -690,14 +639,6 @@ func (r *LightRenderer) Clear() {
r.flush() r.flush()
} }
func (r *LightRenderer) NeedScrollbarRedraw() bool {
return false
}
func (r *LightRenderer) ShouldEmitResizeEvent() bool {
return false
}
func (r *LightRenderer) RefreshWindows(windows []Window) { func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush() r.flush()
} }
@@ -721,24 +662,20 @@ func (r *LightRenderer) Close() {
} else if !r.fullscreen { } else if !r.fullscreen {
r.csi("u") r.csi("u")
} }
r.disableMouse() if r.mouse {
r.csi("?1000l")
r.csi("?1006l")
}
r.flush() r.flush()
r.closePlatform() r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
} }
func (r *LightRenderer) Top() int {
return r.yoffset
}
func (r *LightRenderer) MaxX() int { func (r *LightRenderer) MaxX() int {
return r.width return r.width
} }
func (r *LightRenderer) MaxY() int { func (r *LightRenderer) MaxY() int {
if r.height == 0 {
r.updateTerminalSize()
}
return r.height return r.height
} }
@@ -762,42 +699,25 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
w.fg = r.theme.Fg.Color w.fg = r.theme.Fg.Color
w.bg = r.theme.Bg.Color w.bg = r.theme.Bg.Color
} }
w.drawBorder(false) w.drawBorder()
return w return w
} }
func (w *LightWindow) DrawBorder() { func (w *LightWindow) drawBorder() {
w.drawBorder(false)
}
func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
switch w.border.shape { switch w.border.shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble: case BorderRounded, BorderSharp:
w.drawBorderAround(onlyHorizontal) w.drawBorderAround()
case BorderHorizontal: case BorderHorizontal:
w.drawBorderHorizontal(true, true) w.drawBorderHorizontal(true, true)
case BorderVertical: case BorderVertical:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, true) w.drawBorderVertical(true, true)
case BorderTop: case BorderTop:
w.drawBorderHorizontal(true, false) w.drawBorderHorizontal(true, false)
case BorderBottom: case BorderBottom:
w.drawBorderHorizontal(false, true) w.drawBorderHorizontal(false, true)
case BorderLeft: case BorderLeft:
if onlyHorizontal {
return
}
w.drawBorderVertical(true, false) w.drawBorderVertical(true, false)
case BorderRight: case BorderRight:
if onlyHorizontal {
return
}
w.drawBorderVertical(false, true) w.drawBorderVertical(false, true)
} }
} }
@@ -807,14 +727,13 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runeWidth(w.border.top)
if top { if top {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw)) w.CPrint(color, repeat(w.border.horizontal, w.width))
} }
if bottom { if bottom {
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw)) w.CPrint(color, repeat(w.border.horizontal, w.width))
} }
} }
@@ -830,46 +749,38 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0) w.Move(y, 0)
if left { if left {
w.CPrint(color, string(w.border.left)) w.CPrint(color, string(w.border.vertical))
} }
w.CPrint(color, repeat(' ', width)) w.CPrint(color, repeat(' ', width))
if right { if right {
w.CPrint(color, string(w.border.right)) w.CPrint(color, string(w.border.vertical))
} }
} }
} }
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) { func (w *LightWindow) drawBorderAround() {
w.Move(0, 0) w.Move(0, 0)
color := ColBorder color := ColBorder
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runeWidth(w.border.top) w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight) for y := 1; y < w.height-1; y++ {
bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight) w.Move(y, 0)
rem := (w.width - tcw) % hw w.CPrint(color, string(w.border.vertical))
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight)) w.CPrint(color, repeat(' ', w.width-2))
if !onlyHorizontal { w.CPrint(color, string(w.border.vertical))
vw := runeWidth(w.border.left)
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, string(w.border.right))
}
} }
w.Move(w.height-1, 0) w.Move(w.height-1, 0)
rem = (w.width - bcw) % hw w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.bottom, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
} }
func (w *LightWindow) csi(code string) string { func (w *LightWindow) csi(code string) {
return w.renderer.csi(code) w.renderer.csi(code)
} }
func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
w.renderer.stderrInternal(str, allowNLCR, resetCode) w.renderer.stderrInternal(str, allowNLCR)
} }
func (w *LightWindow) Top() int { func (w *LightWindow) Top() int {
@@ -945,9 +856,6 @@ func attrCodes(attr Attr) []string {
if (attr & Reverse) > 0 { if (attr & Reverse) > 0 {
codes = append(codes, "7") codes = append(codes, "7")
} }
if (attr & StrikeThrough) > 0 {
codes = append(codes, "9")
}
return codes return codes
} }
@@ -975,10 +883,10 @@ func colorCodes(fg Color, bg Color) []string {
return codes return codes
} }
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) { func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
codes := append(attrCodes(attr), colorCodes(fg, bg)...) codes := append(attrCodes(attr), colorCodes(fg, bg)...)
code := w.csi(";" + strings.Join(codes, ";") + "m") w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0, code return len(codes) > 0
} }
func (w *LightWindow) Print(text string) { func (w *LightWindow) Print(text string) {
@@ -990,17 +898,16 @@ func cleanse(str string) string {
} }
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
w.stderrInternal(cleanse(text), false, code) w.stderrInternal(cleanse(text), false)
w.csi("m") w.csi("m")
} }
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
hasColors, code := w.csiColor(fg, bg, attr) if w.csiColor(fg, bg, attr) {
if hasColors {
defer w.csi("m") defer w.csi("m")
} }
w.stderrInternal(cleanse(text), false, code) w.stderrInternal(cleanse(text), false)
} }
type wrappedLine struct { type wrappedLine struct {
@@ -1020,10 +927,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
if len(rs) == 1 && rs[0] == '\t' { if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixLength+width)%tabstop w = tabstop - (prefixLength+width)%tabstop
str = repeat(' ', w) str = repeat(' ', w)
} else if rs[0] == '\r' {
w++
} else { } else {
w = uniseg.StringWidth(str) w = runewidth.StringWidth(str)
} }
width += w width += w
@@ -1040,12 +945,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
return lines return lines
} }
func (w *LightWindow) fill(str string, resetCode string) FillReturn { func (w *LightWindow) fill(str string, onMove func()) FillReturn {
allLines := strings.Split(str, "\n") allLines := strings.Split(str, "\n")
for i, line := range allLines { for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop) lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines { for j, wl := range lines {
w.stderrInternal(wl.text, false, resetCode) w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth w.posx += wl.displayWidth
// Wrap line // Wrap line
@@ -1055,7 +960,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
} }
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.stderr(resetCode) onMove()
} }
} }
} }
@@ -1064,26 +969,22 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
return FillSuspend return FillSuspend
} }
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.stderr(resetCode) onMove()
return FillNextLine return FillNextLine
} }
return FillContinue return FillContinue
} }
func (w *LightWindow) setBg() string { func (w *LightWindow) setBg() {
if w.bg != colDefault { if w.bg != colDefault {
_, code := w.csiColor(colDefault, w.bg, AttrRegular) w.csiColor(colDefault, w.bg, AttrRegular)
return code
} }
// Should clear dim attribute after ␍ in the preview window
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
return "\x1b[m"
} }
func (w *LightWindow) Fill(text string) FillReturn { func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx) w.Move(w.posy, w.posx)
code := w.setBg() w.setBg()
return w.fill(text, code) return w.fill(text, w.setBg)
} }
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
@@ -1094,29 +995,22 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
if bg == colDefault { if bg == colDefault {
bg = w.bg bg = w.bg
} }
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors { if w.csiColor(fg, bg, attr) {
defer w.csi("m") defer w.csi("m")
return w.fill(text, resetCode) return w.fill(text, func() { w.csiColor(fg, bg, attr) })
} }
return w.fill(text, w.setBg()) return w.fill(text, w.setBg)
} }
func (w *LightWindow) FinishFill() { func (w *LightWindow) FinishFill() {
if w.posy < w.height { w.MoveAndClear(w.posy, w.posx)
w.MoveAndClear(w.posy, w.posx)
}
for y := w.posy + 1; y < w.height; y++ { for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0) w.MoveAndClear(y, 0)
} }
} }
func (w *LightWindow) Erase() { func (w *LightWindow) Erase() {
w.DrawBorder() w.drawBorder()
w.Move(0, 0) // We don't erase the window here to avoid flickering during scroll
w.FinishFill()
w.Move(0, 0) w.Move(0, 0)
} }
func (w *LightWindow) EraseMaybe() bool {
return false
}

View File

@@ -10,7 +10,6 @@ import (
"syscall" "syscall"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/sys/unix"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -109,11 +108,3 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
} }
return int(b[0]), true return int(b[0]), true
} }
func (r *LightRenderer) Size() TermSize {
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
if err != nil {
return TermSize{}
}
return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
}

View File

@@ -110,24 +110,16 @@ func (r *LightRenderer) restoreTerminal() error {
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
} }
func (r *LightRenderer) Size() TermSize { func (r *LightRenderer) updateTerminalSize() {
var w, h int
var bufferInfo windows.ConsoleScreenBufferInfo var bufferInfo windows.ConsoleScreenBufferInfo
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
w = getEnv("COLUMNS", defaultWidth) r.width = getEnv("COLUMNS", defaultWidth)
h = r.maxHeightFunc(getEnv("LINES", defaultHeight)) r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
} else { } else {
w = int(bufferInfo.Window.Right - bufferInfo.Window.Left) r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
h = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top)) r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
} }
return TermSize{h, w, 0, 0}
}
func (r *LightRenderer) updateTerminalSize() {
size := r.Size()
r.width = size.Columns
r.height = size.Lines
} }
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {

View File

@@ -6,10 +6,12 @@ import (
"os" "os"
"time" "time"
"github.com/gdamore/tcell/v2" "runtime"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/gdamore/tcell"
"github.com/gdamore/tcell/encoding"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
@@ -17,26 +19,12 @@ func HasFullscreenRenderer() bool {
return true return true
} }
var DefaultBorderShape BorderShape = BorderSharp
func asTcellColor(color Color) tcell.Color {
if color == colDefault {
return tcell.ColorDefault
}
value := uint64(tcell.ColorValid) + uint64(color)
if color.is24() {
value = value | uint64(tcell.ColorIsRGB)
}
return tcell.Color(value)
}
func (p ColorPair) style() tcell.Style { func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault style := tcell.StyleDefault
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg())) return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
} }
type Attr int32 type Attr tcell.Style
type TcellWindow struct { type TcellWindow struct {
color bool color bool
@@ -75,6 +63,8 @@ func (w *TcellWindow) Refresh() {
} }
w.lastX = 0 w.lastX = 0
w.lastY = 0 w.lastY = 0
w.drawBorder()
} }
func (w *TcellWindow) FinishFill() { func (w *TcellWindow) FinishFill() {
@@ -82,13 +72,12 @@ func (w *TcellWindow) FinishFill() {
} }
const ( const (
Bold Attr = Attr(tcell.AttrBold) Bold Attr = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim) Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink) Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse) Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline) Underline = Attr(tcell.AttrUnderline)
StrikeThrough = Attr(tcell.AttrStrikeThrough) Italic = Attr(tcell.AttrItalic)
Italic = Attr(tcell.AttrItalic)
) )
const ( const (
@@ -97,13 +86,6 @@ const (
AttrClear = Attr(1 << 8) AttrClear = Attr(1 << 8)
) )
func (r *FullscreenRenderer) PassThrough(str string) {
// No-op
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
}
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme { func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 { if _screen.Colors() >= 256 {
return Dark256 return Dark256
@@ -143,7 +125,6 @@ func (a Attr) Merge(b Attr) Attr {
var ( var (
_screen tcell.Screen _screen tcell.Screen
_prevMouseButton tcell.ButtonMask _prevMouseButton tcell.ButtonMask
_initialResize bool = true
) )
func (r *FullscreenRenderer) initScreen() { func (r *FullscreenRenderer) initScreen() {
@@ -172,10 +153,6 @@ func (r *FullscreenRenderer) Init() {
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
} }
func (r *FullscreenRenderer) Top() int {
return 0
}
func (r *FullscreenRenderer) MaxX() int { func (r *FullscreenRenderer) MaxX() int {
ncols, _ := _screen.Size() ncols, _ := _screen.Size()
return int(ncols) return int(ncols)
@@ -199,34 +176,14 @@ func (r *FullscreenRenderer) Clear() {
_screen.Clear() _screen.Clear()
} }
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
return true
}
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool {
return true
}
func (r *FullscreenRenderer) Refresh() { func (r *FullscreenRenderer) Refresh() {
// noop // noop
} }
// TODO: Pixel width and height not implemented
func (r *FullscreenRenderer) Size() TermSize {
cols, lines := _screen.Size()
return TermSize{lines, cols, 0, 0}
}
func (r *FullscreenRenderer) GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
case *tcell.EventResize: case *tcell.EventResize:
// Ignore the first resize event
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
if _initialResize {
_initialResize = false
return Event{Invalid, 0, nil}
}
return Event{Resize, 0, nil} return Event{Resize, 0, nil}
// process mouse events: // process mouse events:
@@ -248,38 +205,54 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
case button&tcell.WheelUp != 0: case button&tcell.WheelUp != 0:
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
case button&tcell.Button1 != 0: case button&tcell.Button1 != 0 && !drag:
double := false // all potential double click events put their 'line' coordinate in the clickY array
if !drag { // double click event has two conditions, temporal and spatial, the first is checked here
// all potential double click events put their coordinates in the clicks array now := time.Now()
// double click event has two conditions, temporal and spatial, the first is checked here if now.Sub(r.prevDownTime) < doubleClickDuration {
now := time.Now() r.clickY = append(r.clickY, y)
if now.Sub(r.prevDownTime) < doubleClickDuration { } else {
r.clicks = append(r.clicks, [2]int{x, y}) r.clickY = []int{y}
} else {
r.clicks = [][2]int{{x, y}}
}
r.prevDownTime = now
// detect double clicks (also check for spatial condition)
n := len(r.clicks)
double = n > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1]
if double {
// make sure two consecutive double clicks require four clicks
r.clicks = [][2]int{}
}
} }
r.prevDownTime = now
// detect double clicks (also check for spatial condition)
n := len(r.clickY)
double := n > 1 && r.clickY[n-2] == r.clickY[n-1]
if double {
// make sure two consecutive double clicks require four clicks
r.clickY = []int{}
}
// fire single or double click event // fire single or double click event
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
case button&tcell.Button2 != 0: case button&tcell.Button2 != 0 && !drag:
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
default: case runtime.GOOS != "windows":
// double and single taps on Windows don't quite work due to // double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us // the console acting on the events and not allowing us
// to consume them. // to consume them.
left := button&tcell.Button1 != 0 left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0 down := left || button&tcell.Button3 != 0
double := false double := false
if down {
now := time.Now()
if !left {
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x)
} else {
r.clickY = []int{x}
r.prevDownTime = now
}
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
} }
@@ -438,12 +411,6 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyHome: case tcell.KeyHome:
return Event{Home, 0, nil} return Event{Home, 0, nil}
case tcell.KeyDelete: case tcell.KeyDelete:
if ctrl {
return Event{CtrlDelete, 0, nil}
}
if shift {
return Event{SDelete, 0, nil}
}
return Event{Del, 0, nil} return Event{Del, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
return Event{End, 0, nil} return Event{End, 0, nil}
@@ -535,7 +502,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
if preview { if preview {
normal = ColPreview normal = ColPreview
} }
w := &TcellWindow{ return &TcellWindow{
color: r.theme.Colored, color: r.theme.Colored,
preview: preview, preview: preview,
top: top, top: top,
@@ -544,8 +511,6 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
height: height, height: height,
normal: normal, normal: normal,
borderStyle: borderStyle} borderStyle: borderStyle}
w.drawBorder(false)
return w
} }
func (w *TcellWindow) Close() { func (w *TcellWindow) Close() {
@@ -561,13 +526,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
w.drawBorder(false) fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ')
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
}
func (w *TcellWindow) EraseMaybe() bool {
w.Erase()
return true
} }
func (w *TcellWindow) Enclose(y int, x int) bool { func (w *TcellWindow) Enclose(y int, x int) bool {
@@ -602,7 +561,6 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
style = style. style = style.
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0). Italic(a&Attr(tcell.AttrItalic) != 0).
Blink(a&Attr(tcell.AttrBlink) != 0). Blink(a&Attr(tcell.AttrBlink) != 0).
Dim(a&Attr(tcell.AttrDim) != 0) Dim(a&Attr(tcell.AttrDim) != 0)
@@ -610,27 +568,26 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
gr := uniseg.NewGraphemes(text) gr := uniseg.NewGraphemes(text)
for gr.Next() { for gr.Next() {
st := style
rs := gr.Runes() rs := gr.Runes()
if len(rs) == 1 { if len(rs) == 1 {
r := rs[0] r := rs[0]
if r == '\r' { if r < rune(' ') { // ignore control characters
st = style.Dim(true) continue
rs[0] = '␍'
} else if r == '\n' { } else if r == '\n' {
st = style.Dim(true) w.lastY++
rs[0] = '␊' lx = 0
} else if r < rune(' ') { // ignore control characters continue
} else if r == '\u000D' { // skip carriage return
continue continue
} }
} }
var xPos = w.left + w.lastX + lx var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) { if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st) _screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
} }
lx += util.StringWidth(string(rs)) lx += runewidth.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
} }
@@ -655,26 +612,16 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Dim(a&Attr(tcell.AttrDim) != 0). Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0) Italic(a&Attr(tcell.AttrItalic) != 0)
gr := uniseg.NewGraphemes(text) gr := uniseg.NewGraphemes(text)
Loop:
for gr.Next() { for gr.Next() {
st := style
rs := gr.Runes() rs := gr.Runes()
if len(rs) == 1 { if len(rs) == 1 && rs[0] == '\n' {
r := rs[0] w.lastY++
switch r { w.lastX = 0
case '\r': lx = 0
st = style.Dim(true) continue
rs[0] = '␍'
case '\n':
w.lastY++
w.lastX = 0
lx = 0
continue Loop
}
} }
// word wrap: // word wrap:
@@ -691,8 +638,8 @@ Loop:
return FillSuspend return FillSuspend
} }
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st) _screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
lx += util.StringWidth(string(rs)) lx += runewidth.StringWidth(string(rs))
} }
w.lastX += lx w.lastX += lx
if w.lastX == w.width { if w.lastX == w.width {
@@ -718,15 +665,7 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a)) return w.fillString(str, NewColorPair(fg, bg, a))
} }
func (w *TcellWindow) DrawBorder() { func (w *TcellWindow) drawBorder() {
w.drawBorder(false)
}
func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
shape := w.borderStyle.shape shape := w.borderStyle.shape
if shape == BorderNone { if shape == BorderNone {
return return
@@ -748,52 +687,35 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = w.normal.style() style = w.normal.style()
} }
hw := runeWidth(w.borderStyle.top)
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop: case BorderRounded, BorderSharp, BorderHorizontal, BorderTop:
max := right - 2*hw for x := left; x < right; x++ {
if shape == BorderHorizontal || shape == BorderTop { _screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
max = right - hw
}
// tcell has an issue displaying two overlapping wide runes
// e.g. SetContent( HH )
// SetContent( TR )
// ==================
// ( HH ) => TR is ignored
for x := left; x <= max; x += hw {
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom: case BorderRounded, BorderSharp, BorderHorizontal, BorderBottom:
max := right - 2*hw for x := left; x < right; x++ {
if shape == BorderHorizontal || shape == BorderBottom { _screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
max = right - hw
}
for x := left; x <= max; x += hw {
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
}
}
if !onlyHorizontal {
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
vw := runeWidth(w.borderStyle.right)
for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
}
} }
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble: case BorderRounded, BorderSharp, BorderVertical, BorderLeft:
for y := top; y < bot; y++ {
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp, BorderVertical, BorderRight:
for y := top; y < bot; y++ {
_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
}
}
switch shape {
case BorderRounded, BorderSharp:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
_screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style) _screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style)
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
_screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style) _screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style)
} }
} }

View File

@@ -5,7 +5,7 @@ package tui
import ( import (
"testing" "testing"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -182,7 +182,6 @@ func TestGetCharEventKey(t *testing.T) {
r.Init() r.Init()
// run and evaluate the tests // run and evaluate the tests
initialResizeAsInvalid := true
for _, test := range tests { for _, test := range tests {
// generate key event // generate key event
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods) giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
@@ -192,9 +191,8 @@ func TestGetCharEventKey(t *testing.T) {
// process the event in fzf and evaluate the test // process the event in fzf and evaluate the test
gotEvent := r.GetChar() gotEvent := r.GetChar()
// skip Resize events, those are sometimes put in the buffer outside of this test // skip Resize events, those are sometimes put in the buffer outside of this test
if initialResizeAsInvalid && gotEvent.Type == Invalid { for gotEvent.Type == Resize {
t.Logf("Resize as Invalid swallowed") t.Logf("Resize swallowed")
initialResizeAsInvalid = false
gotEvent = r.GetChar() gotEvent = r.GetChar()
} }
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char) t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)

View File

@@ -3,6 +3,7 @@
package tui package tui
import ( import (
"io/ioutil"
"os" "os"
"syscall" "syscall"
) )
@@ -16,17 +17,13 @@ func ttyname() string {
} }
for _, prefix := range devPrefixes { for _, prefix := range devPrefixes {
files, err := os.ReadDir(prefix) files, err := ioutil.ReadDir(prefix)
if err != nil { if err != nil {
continue continue
} }
for _, file := range files { for _, file := range files {
info, err := file.Info() if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
if err != nil {
continue
}
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name() return prefix + file.Name()
} }
} }

View File

@@ -5,8 +5,6 @@ import (
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/rivo/uniseg"
) )
// Types of user action // Types of user action
@@ -43,7 +41,6 @@ const (
CtrlZ CtrlZ
ESC ESC
CtrlSpace CtrlSpace
CtrlDelete
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash CtrlBackSlash
@@ -57,14 +54,6 @@ const (
DoubleClick DoubleClick
LeftClick LeftClick
RightClick RightClick
SLeftClick
SRightClick
ScrollUp
ScrollDown
SScrollUp
SScrollDown
PreviewScrollUp
PreviewScrollDown
BTab BTab
BSpace BSpace
@@ -85,7 +74,6 @@ const (
SDown SDown
SLeft SLeft
SRight SRight
SDelete
F1 F1
F2 F2
@@ -102,12 +90,6 @@ const (
Change Change
BackwardEOF BackwardEOF
Start
Load
Focus
One
Zero
Result
AltBS AltBS
@@ -266,31 +248,25 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
} }
type ColorTheme struct { type ColorTheme struct {
Colored bool Colored bool
Input ColorAttr Input ColorAttr
Disabled ColorAttr Disabled ColorAttr
Fg ColorAttr Fg ColorAttr
Bg ColorAttr Bg ColorAttr
PreviewFg ColorAttr PreviewFg ColorAttr
PreviewBg ColorAttr PreviewBg ColorAttr
DarkBg ColorAttr DarkBg ColorAttr
Gutter ColorAttr Gutter ColorAttr
Prompt ColorAttr Prompt ColorAttr
Match ColorAttr Match ColorAttr
Current ColorAttr Current ColorAttr
CurrentMatch ColorAttr CurrentMatch ColorAttr
Spinner ColorAttr Spinner ColorAttr
Info ColorAttr Info ColorAttr
Cursor ColorAttr Cursor ColorAttr
Selected ColorAttr Selected ColorAttr
Header ColorAttr Header ColorAttr
Separator ColorAttr Border ColorAttr
Scrollbar ColorAttr
Border ColorAttr
PreviewBorder ColorAttr
PreviewScrollbar ColorAttr
BorderLabel ColorAttr
PreviewLabel ColorAttr
} }
type Event struct { type Event struct {
@@ -299,15 +275,6 @@ type Event struct {
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct { type MouseEvent struct {
Y int Y int
X int X int
@@ -324,10 +291,6 @@ const (
BorderNone BorderShape = iota BorderNone BorderShape = iota
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold
BorderBlock
BorderThinBlock
BorderDouble
BorderHorizontal BorderHorizontal
BorderVertical BorderVertical
BorderTop BorderTop
@@ -336,28 +299,10 @@ const (
BorderRight BorderRight
) )
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false
}
return true
}
func (s BorderShape) HasTop() bool {
switch s {
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false
}
return true
}
type BorderStyle struct { type BorderStyle struct {
shape BorderShape shape BorderShape
top rune horizontal rune
bottom rune vertical rune
left rune
right rune
topLeft rune topLeft rune
topRight rune topRight rune
bottomLeft rune bottomLeft rune
@@ -367,143 +312,64 @@ type BorderStyle struct {
type BorderCharacter int type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if !unicode { if unicode {
return BorderStyle{ if shape == BorderRounded {
shape: shape, return BorderStyle{
top: '-', shape: shape,
bottom: '-', horizontal: '',
left: '|', vertical: '',
right: '|', topLeft: '',
topLeft: '+', topRight: '',
topRight: '+', bottomLeft: '',
bottomLeft: '+', bottomRight: '',
bottomRight: '+', }
} }
}
switch shape {
case BorderSharp:
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
top: '─', horizontal: '─',
bottom: '', vertical: '',
left: '│',
right: '│',
topLeft: '┌', topLeft: '┌',
topRight: '┐', topRight: '┐',
bottomLeft: '└', bottomLeft: '└',
bottomRight: '┘', bottomRight: '┘',
} }
case BorderBold:
return BorderStyle{
shape: shape,
top: '━',
bottom: '━',
left: '┃',
right: '┃',
topLeft: '┏',
topRight: '┓',
bottomLeft: '┗',
bottomRight: '┛',
}
case BorderBlock:
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
// ▌ ▐
// ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
return BorderStyle{
shape: shape,
top: '▀',
bottom: '▄',
left: '▌',
right: '▐',
topLeft: '▛',
topRight: '▜',
bottomLeft: '▙',
bottomRight: '▟',
}
case BorderThinBlock:
// 🭽▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔🭾
// ▏ ▕
// 🭼▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁🭿
return BorderStyle{
shape: shape,
top: '▔',
bottom: '▁',
left: '▏',
right: '▕',
topLeft: '🭽',
topRight: '🭾',
bottomLeft: '🭼',
bottomRight: '🭿',
}
case BorderDouble:
return BorderStyle{
shape: shape,
top: '═',
bottom: '═',
left: '║',
right: '║',
topLeft: '╔',
topRight: '╗',
bottomLeft: '╚',
bottomRight: '╝',
}
} }
return BorderStyle{ return BorderStyle{
shape: shape, shape: shape,
top: '', horizontal: '-',
bottom: '', vertical: '|',
left: '', topLeft: '+',
right: '', topRight: '+',
topLeft: '', bottomLeft: '+',
topRight: '', bottomRight: '+',
bottomLeft: '╰',
bottomRight: '╯',
} }
} }
func MakeTransparentBorder() BorderStyle { func MakeTransparentBorder() BorderStyle {
return BorderStyle{ return BorderStyle{
shape: BorderRounded, shape: BorderRounded,
top: ' ', horizontal: ' ',
bottom: ' ', vertical: ' ',
left: ' ',
right: ' ',
topLeft: ' ', topLeft: ' ',
topRight: ' ', topRight: ' ',
bottomLeft: ' ', bottomLeft: ' ',
bottomRight: ' '} bottomRight: ' '}
} }
type TermSize struct {
Lines int
Columns int
PxWidth int
PxHeight int
}
type Renderer interface { type Renderer interface {
Init() Init()
Resize(maxHeightFunc func(int) int)
Pause(clear bool) Pause(clear bool)
Resume(clear bool, sigcont bool) Resume(clear bool, sigcont bool)
Clear() Clear()
RefreshWindows(windows []Window) RefreshWindows(windows []Window)
Refresh() Refresh()
Close() Close()
PassThrough(string)
NeedScrollbarRedraw() bool
ShouldEmitResizeEvent() bool
GetChar() Event GetChar() Event
Top() int
MaxX() int MaxX() int
MaxY() int MaxY() int
Size() TermSize
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
} }
@@ -513,8 +379,6 @@ type Window interface {
Width() int Width() int
Height() int Height() int
DrawBorder()
DrawHBorder()
Refresh() Refresh()
FinishFill() FinishFill()
Close() Close()
@@ -530,7 +394,6 @@ type Window interface {
Fill(text string) FillReturn Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn CFill(fg Color, bg Color, attr Attr, text string) FillReturn
Erase() Erase()
EraseMaybe() bool
} }
type FullscreenRenderer struct { type FullscreenRenderer struct {
@@ -538,7 +401,7 @@ type FullscreenRenderer struct {
mouse bool mouse bool
forceBlack bool forceBlack bool
prevDownTime time.Time prevDownTime time.Time
clicks [][2]int clickY []int
} }
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
@@ -547,7 +410,7 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
mouse: mouse, mouse: mouse,
forceBlack: forceBlack, forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0), prevDownTime: time.Unix(0, 0),
clicks: [][2]int{}} clickY: []int{}}
return r return r
} }
@@ -573,75 +436,55 @@ var (
ColSpinner ColorPair ColSpinner ColorPair
ColInfo ColorPair ColInfo ColorPair
ColHeader ColorPair ColHeader ColorPair
ColSeparator ColorPair
ColScrollbar ColorPair
ColBorder ColorPair ColBorder ColorPair
ColPreview ColorPair ColPreview ColorPair
ColPreviewBorder ColorPair ColPreviewBorder ColorPair
ColBorderLabel ColorPair
ColPreviewLabel ColorPair
ColPreviewScrollbar ColorPair
ColPreviewSpinner ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined}, Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colUndefined, AttrUndefined}, DarkBg: ColorAttr{colUndefined, AttrUndefined},
CurrentMatch: ColorAttr{colUndefined, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined}, Match: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined}, Current: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined}, CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Spinner: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}, Info: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined}}
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
} }
func NoColorTheme() *ColorTheme { func NoColorTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
Colored: false, Colored: false,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrRegular},
Fg: ColorAttr{colDefault, AttrUndefined}, Disabled: ColorAttr{colDefault, AttrRegular},
Bg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrRegular},
DarkBg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrRegular},
Prompt: ColorAttr{colDefault, AttrUndefined}, PreviewFg: ColorAttr{colDefault, AttrRegular},
Match: ColorAttr{colDefault, Underline}, PreviewBg: ColorAttr{colDefault, AttrRegular},
Current: ColorAttr{colDefault, Reverse}, DarkBg: ColorAttr{colDefault, AttrRegular},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline}, Gutter: ColorAttr{colDefault, AttrRegular},
Spinner: ColorAttr{colDefault, AttrUndefined}, Prompt: ColorAttr{colDefault, AttrRegular},
Info: ColorAttr{colDefault, AttrUndefined}, Match: ColorAttr{colDefault, Underline},
Cursor: ColorAttr{colDefault, AttrUndefined}, Current: ColorAttr{colDefault, Reverse},
Selected: ColorAttr{colDefault, AttrUndefined}, CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
Header: ColorAttr{colDefault, AttrUndefined}, Spinner: ColorAttr{colDefault, AttrRegular},
Border: ColorAttr{colDefault, AttrUndefined}, Info: ColorAttr{colDefault, AttrRegular},
BorderLabel: ColorAttr{colDefault, AttrUndefined}, Cursor: ColorAttr{colDefault, AttrRegular},
Disabled: ColorAttr{colDefault, AttrUndefined}, Selected: ColorAttr{colDefault, AttrRegular},
PreviewFg: ColorAttr{colDefault, AttrUndefined}, Header: ColorAttr{colDefault, AttrRegular},
PreviewBg: ColorAttr{colDefault, AttrUndefined}, Border: ColorAttr{colDefault, AttrRegular}}
Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
}
} }
func errorExit(message string) { func errorExit(message string) {
@@ -651,86 +494,65 @@ func errorExit(message string) {
func init() { func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colYellow, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
CurrentMatch: ColorAttr{colGreen, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Current: ColorAttr{colYellow, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined}, CurrentMatch: ColorAttr{colGreen, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Info: ColorAttr{colWhite, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{colMagenta, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined}}
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{254, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
CurrentMatch: ColorAttr{151, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{148, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Current: ColorAttr{254, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, CurrentMatch: ColorAttr{151, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{59, AttrUndefined}}
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Disabled: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{237, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
CurrentMatch: ColorAttr{23, AttrUndefined}, Gutter: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{65, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Current: ColorAttr{237, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, CurrentMatch: ColorAttr{23, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{145, AttrUndefined}}
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
}
} }
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
@@ -749,9 +571,13 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
return c return c
} }
theme.Input = o(baseTheme.Input, theme.Input) theme.Input = o(baseTheme.Input, theme.Input)
theme.Disabled = o(theme.Input, o(baseTheme.Disabled, theme.Disabled))
theme.Fg = o(baseTheme.Fg, theme.Fg) theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg) theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
theme.Prompt = o(baseTheme.Prompt, theme.Prompt) theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match) theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current) theme.Current = o(baseTheme.Current, theme.Current)
@@ -762,18 +588,6 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border) theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
theme.Separator = o(theme.Border, theme.Separator)
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
initPalette(theme) initPalette(theme)
} }
@@ -805,17 +619,7 @@ func initPalette(theme *ColorTheme) {
ColSpinner = pair(theme.Spinner, theme.Bg) ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg) ColInfo = pair(theme.Info, theme.Bg)
ColHeader = pair(theme.Header, theme.Bg) ColHeader = pair(theme.Header, theme.Bg)
ColSeparator = pair(theme.Separator, theme.Bg)
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg) ColBorder = pair(theme.Border, theme.Bg)
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
ColPreview = pair(theme.PreviewFg, theme.PreviewBg) ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg) ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
}
func runeWidth(r rune) int {
return uniseg.StringWidth(string(r))
} }

View File

@@ -9,6 +9,7 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtClose
) )
func TestEventBox(t *testing.T) { func TestEventBox(t *testing.T) {

View File

@@ -7,14 +7,10 @@ import (
"time" "time"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
// StringWidth returns string width where each CR/LF character takes 1 column
func StringWidth(s string) int {
return uniseg.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
}
// RunesWidth returns runes width // RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) { func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
width := 0 width := 0
@@ -26,7 +22,8 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
if len(rs) == 1 && rs[0] == '\t' { if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixWidth+width)%tabstop w = tabstop - (prefixWidth+width)%tabstop
} else { } else {
w = StringWidth(string(rs)) s := string(rs)
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
} }
width += w width += w
if width > limit { if width > limit {
@@ -44,7 +41,7 @@ func Truncate(input string, limit int) ([]rune, int) {
gr := uniseg.NewGraphemes(input) gr := uniseg.NewGraphemes(input)
for gr.Next() { for gr.Next() {
rs := gr.Runes() rs := gr.Runes()
w := StringWidth(string(rs)) w := runewidth.StringWidth(string(rs))
if width+w > limit { if width+w > limit {
return runes, width return runes, width
} }
@@ -156,23 +153,3 @@ func Once(nextResponse bool) func() bool {
return prevState return prevState
} }
} }
// RepeatToFill repeats the given string to fill the given width
func RepeatToFill(str string, length int, limit int) string {
times := limit / length
rest := limit % length
output := strings.Repeat(str, times)
if rest > 0 {
for _, r := range str {
rest -= uniseg.StringWidth(string(r))
if rest < 0 {
break
}
output += string(r)
if rest == 0 {
break
}
}
}
return output
}

View File

@@ -1,76 +1,14 @@
package util package util
import ( import "testing"
"math"
"strings"
"testing"
"time"
)
func TestMax(t *testing.T) { func TestMax(t *testing.T) {
if Max(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max(-2, 5) != 5 { if Max(-2, 5) != 5 {
t.Error("Expected", 5) t.Error("Invalid result")
} }
} }
func TestMax16(t *testing.T) { func TestContrain(t *testing.T) {
if Max16(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max16(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
t.Error("Expected", math.MaxInt16)
}
if Max16(0, math.MinInt16) != 0 {
t.Error("Expected", 0)
}
}
func TestMax32(t *testing.T) {
if Max32(10, 1) != 10 {
t.Error("Expected", 10)
}
if Max32(-2, 5) != 5 {
t.Error("Expected", 5)
}
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
t.Error("Expected", math.MaxInt32)
}
if Max32(0, math.MinInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestMin(t *testing.T) {
if Min(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min(-2, 5) != -2 {
t.Error("Expected", -2)
}
}
func TestMin32(t *testing.T) {
if Min32(10, 1) != 1 {
t.Error("Expected", 1)
}
if Min32(-2, 5) != -2 {
t.Error("Expected", -2)
}
if Min32(math.MaxInt32, 0) != 0 {
t.Error("Expected", 0)
}
if Min32(0, math.MinInt32) != math.MinInt32 {
t.Error("Expected", math.MinInt32)
}
}
func TestConstrain(t *testing.T) {
if Constrain(-3, -1, 3) != -1 { if Constrain(-3, -1, 3) != -1 {
t.Error("Expected", -1) t.Error("Expected", -1)
} }
@@ -83,55 +21,6 @@ func TestConstrain(t *testing.T) {
} }
} }
func TestConstrain32(t *testing.T) {
if Constrain32(-3, -1, 3) != -1 {
t.Error("Expected", -1)
}
if Constrain32(2, -1, 3) != 2 {
t.Error("Expected", 2)
}
if Constrain32(5, -1, 3) != 3 {
t.Error("Expected", 3)
}
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
t.Error("Expected", 0)
}
}
func TestAsUint16(t *testing.T) {
if AsUint16(5) != 5 {
t.Error("Expected", 5)
}
if AsUint16(-10) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint16) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
if AsUint16(math.MinInt32) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MinInt16) != 0 {
t.Error("Expected", 0)
}
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
t.Error("Expected", math.MaxUint16)
}
}
func TestDurWithIn(t *testing.T) {
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
t.Error("Expected", time.Duration(0))
}
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
t.Error("Expected", time.Second)
}
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
t.Error("Expected", time.Second)
}
}
func TestOnce(t *testing.T) { func TestOnce(t *testing.T) {
o := Once(false) o := Once(false)
if o() { if o() {
@@ -164,18 +53,6 @@ func TestRunesWidth(t *testing.T) {
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx) t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
} }
} }
for _, input := range []struct {
s string
w int
}{
{"▶", 1},
{"▶️", 2},
} {
width, _ := RunesWidth([]rune(input.s), 0, 0, 100)
if width != input.w {
t.Errorf("Expected width of %s: %d, actual: %d", input.s, input.w, width)
}
}
} }
func TestTruncate(t *testing.T) { func TestTruncate(t *testing.T) {
@@ -187,19 +64,3 @@ func TestTruncate(t *testing.T) {
t.Errorf("Expected: 6, actual: %d", width) t.Errorf("Expected: 6, actual: %d", width)
} }
} }
func TestRepeatToFill(t *testing.T) {
if RepeatToFill("abcde", 10, 50) != strings.Repeat("abcde", 5) {
t.Error("Expected:", strings.Repeat("abcde", 5))
}
if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] {
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
}
}
func TestStringWidth(t *testing.T) {
w := StringWidth("─")
if w != 1 {
t.Errorf("Expected: %d, Actual: %d", 1, w)
}
}

View File

@@ -6,8 +6,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
@@ -47,7 +45,3 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(int(fd), b) return syscall.Read(int(fd), b)
} }
func SetStdin(file *os.File) {
unix.Dup2(int(file.Fd()), 0)
}

View File

@@ -81,7 +81,3 @@ func SetNonblock(file *os.File, nonblock bool) {
func Read(fd int, b []byte) (int, error) { func Read(fd int, b []byte) (int, error) {
return syscall.Read(syscall.Handle(fd), b) return syscall.Read(syscall.Handle(fd), b)
} }
func SetStdin(file *os.File) {
// No-op
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos
[default.extend-words]
ba = "ba"
fo = "fo"
enew = "enew"
tabe = "tabe"
Iterm = "Iterm"
[files]
extend-exclude = ["README.md"]