mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-17 21:43:50 -07:00
Compare commits
184 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d3311d9f43 | ||
|
3e1735b06e | ||
|
de7ef7eace | ||
|
7e89458a3b | ||
|
f212bafe46 | ||
|
86fe40708b | ||
|
d718747c5b | ||
|
46ee9ac41c | ||
|
f1d306feab | ||
|
2d0db98e83 | ||
|
3df06a1c68 | ||
|
a8f9432a3a | ||
|
561e0b04a8 | ||
|
404b6a864b | ||
|
4feaf31225 | ||
|
391aa14845 | ||
|
a0d61b4c37 | ||
|
2952737755 | ||
|
f103aa4753 | ||
|
884856023a | ||
|
d8188fce7b | ||
|
0f15f1ab73 | ||
|
488a236b7a | ||
|
e833823e15 | ||
|
ee4ba104e7 | ||
|
4fdc08295b | ||
|
a3ff49aaf1 | ||
|
76364ea767 | ||
|
8eec50d764 | ||
|
32b659b346 | ||
|
00809909ae | ||
|
9f7684f6fe | ||
|
2bed7d370e | ||
|
d2b852f7cb | ||
|
901939bd96 | ||
|
edfdcc8cee | ||
|
3982c9a552 | ||
|
4490b2d209 | ||
|
eb4bbf3294 | ||
|
dc97d48491 | ||
|
0f50dc848e | ||
|
c5e4b83de3 | ||
|
a08ab46713 | ||
|
f50a7058d6 | ||
|
2c74f0a040 | ||
|
58835e40f3 | ||
|
8befa5918a | ||
|
df80f7ff2a | ||
|
5f66786ef1 | ||
|
3a965856a5 | ||
|
03df609d77 | ||
|
178581b560 | ||
|
ffd2314120 | ||
|
815b595d2f | ||
|
11e56403dd | ||
|
4baadecda5 | ||
|
cf552b5f3b | ||
|
1894304d33 | ||
|
9d5392fb02 | ||
|
c280645671 | ||
|
45f92e6b38 | ||
|
6509f09961 | ||
|
3c279a6f0e | ||
|
9ec3f03871 | ||
|
84a9c2c112 | ||
|
af368119cb | ||
|
89b9189efa | ||
|
dd59b8c7b9 | ||
|
f83491274f | ||
|
c0435fdff4 | ||
|
3c09c77269 | ||
|
547e101f1d | ||
|
0130f64934 | ||
|
361e0543ee | ||
|
63aa5d3b4e | ||
|
01302d097c | ||
|
e6095cb7e8 | ||
|
b876b8af11 | ||
|
a7c41f3fcd | ||
|
4772bd8d4c | ||
|
d471067e3f | ||
|
d0b7780239 | ||
|
e627ca6bd7 | ||
|
c97172bdd4 | ||
|
ce8a745fb4 | ||
|
3e9efd1401 | ||
|
20340190b5 | ||
|
265040a78c | ||
|
448d7e0c5a | ||
|
6eb1874c5a | ||
|
4c70745cc1 | ||
|
7795748a3f | ||
|
098ef4d7cf | ||
|
e3f91bfe1b | ||
|
7374fe73a3 | ||
|
d2bde205f0 | ||
|
5620f70f9a | ||
|
37f258b1bf | ||
|
e2dd2a133e | ||
|
7514644e07 | ||
|
16b0aeda7d | ||
|
86e4f4a841 | ||
|
607eacf8c7 | ||
|
7a049644a8 | ||
|
17a13f00f8 | ||
|
43436e48e0 | ||
|
5a39102405 | ||
|
94999101e3 | ||
|
e619b7c4f4 | ||
|
b7c2e8cb67 | ||
|
fb76893e18 | ||
|
88d812fe82 | ||
|
77f9f4664a | ||
|
5c2f85c39e | ||
|
ac4d22cd12 | ||
|
cf95e44cb4 | ||
|
65dd2bb429 | ||
|
6be855be6a | ||
|
b6e3f4423b | ||
|
0c61d81713 | ||
|
7c6f5dba63 | ||
|
44cfc7e62a | ||
|
96670d5f16 | ||
|
36b971ee4e | ||
|
f1a9629652 | ||
|
20230402d0 | ||
|
5c2c3a6c88 | ||
|
fb019d43bf | ||
|
025aa33773 | ||
|
302e21fd58 | ||
|
211512ae64 | ||
|
8ec917b1c3 | ||
|
1c7534f009 | ||
|
ae745d9397 | ||
|
60f37aae2f | ||
|
d7daf5f724 | ||
|
e5103d9429 | ||
|
8fecb29848 | ||
|
290ea6179d | ||
|
9695a40fc9 | ||
|
1913b95227 | ||
|
a874aea692 | ||
|
69c52099e7 | ||
|
cfc0747d5d | ||
|
fcd7e8768d | ||
|
3c34dd8275 | ||
|
1116e481be | ||
|
63cf9d04de | ||
|
3364d4d147 | ||
|
57ad21e4bd | ||
|
414f87981f | ||
|
b1459c79cf | ||
|
352ea07226 | ||
|
27018787af | ||
|
4e305eca26 | ||
|
9e9c0ceaf4 | ||
|
b3bf18b1c0 | ||
|
b1619f675f | ||
|
96c3de12eb | ||
|
719dbb8bae | ||
|
f38a7f7f8f | ||
|
6ea38b4438 | ||
|
f7447aece1 | ||
|
aa2b9ec476 | ||
|
3ee00f8bc2 | ||
|
fccab60a5c | ||
|
0f4af38457 | ||
|
aef39f1160 | ||
|
2023012408 | ||
|
95a7661bb1 | ||
|
618d317803 | ||
|
ae897c8cdb | ||
|
d0a0f3c052 | ||
|
91b9591b10 | ||
|
aa7361337d | ||
|
284d77fe2e | ||
|
826178f1e2 | ||
|
acccf8a9b8 | ||
|
57c066f0be | ||
|
e44f64ae92 | ||
|
d51980a3f5 | ||
|
c3d73e7ecb | ||
|
b077f6821d | ||
|
a79de11af7 |
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
|
24
.github/workflows/sponsors.yml
vendored
Normal file
24
.github/workflows/sponsors.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
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: '.'
|
10
.github/workflows/typos.yml
vendored
Normal file
10
.github/workflows/typos.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
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.16.4
|
15
.github/workflows/winget.yml
vendored
Normal file
15
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
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 }}
|
@@ -74,6 +74,7 @@ builds:
|
||||
- arm64
|
||||
- loong64
|
||||
- ppc64le
|
||||
- s390x
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
|
@@ -1 +1 @@
|
||||
golang 1.19
|
||||
golang 1.20.4
|
||||
|
171
ADVANCED.md
171
ADVANCED.md
@@ -1,30 +1,33 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
*(Last update: 2022/08/25)*
|
||||
* *Last update: 2023/05/26*
|
||||
* *Requires fzf 0.41.0 or above*
|
||||
|
||||
---
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Screen Layout](#screen-layout)
|
||||
* [`--height`](#--height)
|
||||
* [`fzf-tmux`](#fzf-tmux)
|
||||
* [Popup window support](#popup-window-support)
|
||||
* [`--height`](#--height)
|
||||
* [`fzf-tmux`](#fzf-tmux)
|
||||
* [Popup window support](#popup-window-support)
|
||||
* [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)
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
* [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)
|
||||
* [Ripgrep integration](#ripgrep-integration)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [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)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [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)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
* [Branches](#branches)
|
||||
* [Commit hashes](#commit-hashes)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
* [Branches](#branches)
|
||||
* [Commit hashes](#commit-hashes)
|
||||
* [Color themes](#color-themes)
|
||||
* [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 -->
|
||||
|
||||
@@ -236,15 +239,13 @@ file called `rfv`.
|
||||
# 1. Search for text in files using Ripgrep
|
||||
# 2. Interactively narrow down the list using fzf
|
||||
# 3. Open the file in Vim
|
||||
IFS=: read -ra selected < <(
|
||||
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--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]}"
|
||||
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
And run it with an initial query string.
|
||||
@@ -307,8 +308,12 @@ I know it's a lot to digest, let's try to break down the code.
|
||||
position in the window
|
||||
- `~3` makes the top three lines fixed header so that they are always
|
||||
visible regardless of the scroll offset
|
||||
- Once we selected a line, we open the file with `vim` (`vim
|
||||
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
|
||||
- Instead of using shell script to process the final output of fzf, we use
|
||||
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
|
||||
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 interactive Ripgrep launcher
|
||||
|
||||
@@ -331,25 +336,22 @@ projects, and it will free up memory as you narrow down the results.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
IFS=: read -ra selected < <(
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--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]}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||

|
||||
|
||||
- Instead of starting fzf in `rg ... | fzf` form, we start fzf without an
|
||||
explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way
|
||||
fzf can kill the initial Ripgrep process it starts with the initial query.
|
||||
Otherwise, the initial Ripgrep process will keep consuming system resources
|
||||
even after `reload` is triggered.
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
|
||||
an empty input (`: | fzf`), then we make it start the initial Ripgrep
|
||||
process immediately via `start:reload` binding. This way, fzf owns the
|
||||
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
|
||||
the process will keep running in the background.
|
||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||
- `{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
|
||||
@@ -358,8 +360,6 @@ IFS=: read -ra selected < <(
|
||||
|
||||
### 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
|
||||
delegated search functionality to Ripgrep. But we can dynamically switch to
|
||||
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
@@ -375,19 +375,16 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
IFS=: read -ra selected < <(
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--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]}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
* Phase 1. Filtering with Ripgrep
|
||||
@@ -408,10 +405,8 @@ IFS=: read -ra selected < <(
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode
|
||||
|
||||
*(Requires fzf 0.30.0 or above)*
|
||||
|
||||
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
|
||||
previously "unbound" via `unbind`.
|
||||
[fzf 0.30.0][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
|
||||
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
||||
@@ -421,25 +416,32 @@ CTRL-F.
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 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 "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
IFS=: read -ra selected < <(
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
|
||||
--prompt '1. Ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (Ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--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]}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--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)" \
|
||||
--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)" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
- To restore the query string when switching between modes, we store the
|
||||
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
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
@@ -465,16 +467,17 @@ Kubernetes pods.
|
||||
|
||||
```bash
|
||||
pods() {
|
||||
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
|
||||
fzf --info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
: | command='kubectl get pods --all-namespaces' fzf \
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start:reload:$command' \
|
||||
--bind 'ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
}
|
||||
```
|
||||
|
||||
|
226
CHANGELOG.md
226
CHANGELOG.md
@@ -1,6 +1,228 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.43.0
|
||||
------
|
||||
- (Experimental) Added support for Kitty image protocol in the preview window
|
||||
```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 --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
|
||||
@@ -91,7 +313,7 @@ CHANGELOG
|
||||
- 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 `1`.
|
||||
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
|
||||
@@ -200,7 +422,7 @@ CHANGELOG
|
||||
(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.
|
||||
- ~~`--no-clear` is deprecated. Use `reload` action instead.~~
|
||||
|
||||
0.33.0
|
||||
------
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM archlinux
|
||||
FROM --platform=linux/amd64 archlinux
|
||||
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 echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
|
13
Makefile
13
Makefile
@@ -20,7 +20,7 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
ifdef FZF_REVISION
|
||||
REVISION := $(FZF_REVISION)
|
||||
else
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null)
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
|
||||
endif
|
||||
ifeq ($(REVISION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_REVISION)
|
||||
@@ -29,6 +29,7 @@ BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision
|
||||
|
||||
BINARY32 := fzf-$(GOOS)_386
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYS390 := fzf-$(GOOS)_s390x
|
||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
@@ -43,6 +44,8 @@ ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),amd64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),s390x)
|
||||
BINARY := $(BINARYS390)
|
||||
else ifeq ($(UNAME_M),i686)
|
||||
BINARY := $(BINARY32)
|
||||
else ifeq ($(UNAME_M),i386)
|
||||
@@ -54,7 +57,9 @@ else ifeq ($(UNAME_M),armv6l)
|
||||
else ifeq ($(UNAME_M),armv7l)
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),armv8l)
|
||||
BINARY := $(BINARYARM8)
|
||||
# armv8l is always 32-bit and should implement the armv7 ISA, so
|
||||
# just use the same filename as for armv7.
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),arm64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),aarch64)
|
||||
@@ -85,7 +90,7 @@ bench:
|
||||
install: bin/fzf
|
||||
|
||||
build:
|
||||
goreleaser --rm-dist --snapshot
|
||||
goreleaser build --rm-dist --snapshot --skip-post-hooks
|
||||
|
||||
release:
|
||||
ifndef GITHUB_TOKEN
|
||||
@@ -132,6 +137,8 @@ target/$(BINARY32): $(SOURCES)
|
||||
target/$(BINARY64): $(SOURCES)
|
||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYS390): $(SOURCES)
|
||||
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
|
||||
# https://github.com/golang/go/wiki/GoArm
|
||||
target/$(BINARYARM5): $(SOURCES)
|
||||
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
@@ -15,7 +15,7 @@ set rtp+=/usr/local/opt/fzf
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
```
|
||||
|
||||
@@ -26,7 +26,10 @@ written as:
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
Plug '/opt/homebrew/opt/fzf'
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
```
|
||||
|
||||
@@ -118,7 +121,7 @@ let g:fzf_action = {
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
|
179
README.md
179
README.md
@@ -19,6 +19,15 @@ Pros
|
||||
- Batteries included
|
||||
- Vim/Neovim plugin, key bindings, and fuzzy auto-completion
|
||||
|
||||
Sponsors ❤️
|
||||
-----------
|
||||
|
||||
I would like to thank all the sponsors of this project who make it possible for me to continue to improve fzf.
|
||||
|
||||
If you'd like to sponsor this project, please visit https://github.com/sponsors/junegunn.
|
||||
|
||||
<!-- sponsors --><a href="https://github.com/miyanokomiya"><img src="https://github.com/miyanokomiya.png" width="60px" alt="miyanokomiya" /></a><a href="https://github.com/jonhoo"><img src="https://github.com/jonhoo.png" width="60px" alt="Jon Gjengset" /></a><a href="https://github.com/AceofSpades5757"><img src="https://github.com/AceofSpades5757.png" width="60px" alt="Kyle L. Davis" /></a><a href="https://github.com/Frederick888"><img src="https://github.com/Frederick888.png" width="60px" alt="Frederick Zhang" /></a><a href="https://github.com/moritzdietz"><img src="https://github.com/moritzdietz.png" width="60px" alt="Moritz Dietz" /></a><a href="https://github.com/mikker"><img src="https://github.com/mikker.png" width="60px" alt="Mikkel Malmberg" /></a><a href="https://github.com/pldubouilh"><img src="https://github.com/pldubouilh.png" width="60px" alt="Pierre Dubouilh" /></a><a href="https://github.com/rcorre"><img src="https://github.com/rcorre.png" width="60px" alt="Ryan Roden-Corrent" /></a><a href="https://github.com/blissdev"><img src="https://github.com/blissdev.png" width="60px" alt="Jordan Arentsen" /></a><a href="https://github.com/mislav"><img src="https://github.com/mislav.png" width="60px" alt="Mislav Marohnić" /></a><a href="https://github.com/aexvir"><img src="https://github.com/aexvir.png" width="60px" alt="Alex Viscreanu" /></a><a href="https://github.com/dbalatero"><img src="https://github.com/dbalatero.png" width="60px" alt="David Balatero" /></a><a href="https://github.com/comatory"><img src="https://github.com/comatory.png" width="60px" alt="Ondrej Synacek" /></a><a href="https://github.com/moobar"><img src="https://github.com/moobar.png" width="60px" alt="" /></a><a href="https://github.com/majjoha"><img src="https://github.com/majjoha.png" width="60px" alt="Mathias Jean Johansen" /></a><a href="https://github.com/benelan"><img src="https://github.com/benelan.png" width="60px" alt="Ben Elan" /></a><a href="https://github.com/jryom"><img src="https://github.com/jryom.png" width="60px" alt="Jesper" /></a><a href="https://github.com/nmrnv"><img src="https://github.com/nmrnv.png" width="60px" alt="Nikolay Marinov" /></a><a href="https://github.com/pawelduda"><img src="https://github.com/pawelduda.png" width="60px" alt="Paweł Duda" /></a><a href="https://github.com/slezica"><img src="https://github.com/slezica.png" width="60px" alt="Santiago Lezica" /></a><a href="https://github.com/pbwn"><img src="https://github.com/pbwn.png" width="60px" alt="" /></a><a href="https://github.com/timgluz"><img src="https://github.com/timgluz.png" width="60px" alt="Timo Sulg" /></a><a href="https://github.com/seanmorton"><img src="https://github.com/seanmorton.png" width="60px" alt="Sean Morton" /></a><a href="https://github.com/pyrho"><img src="https://github.com/pyrho.png" width="60px" alt="Damien Rajon" /></a><a href="https://github.com/ArtBIT"><img src="https://github.com/ArtBIT.png" width="60px" alt="ArtBIT" /></a><a href="https://github.com/da-moon"><img src="https://github.com/da-moon.png" width="60px" alt="" /></a><a href="https://github.com/hovissimo"><img src="https://github.com/hovissimo.png" width="60px" alt="Hovis" /></a><a href="https://github.com/dariusjonda"><img src="https://github.com/dariusjonda.png" width="60px" alt="Darius Jonda" /></a><a href="https://github.com/cristiand391"><img src="https://github.com/cristiand391.png" width="60px" alt="Cristian Dominguez" /></a><a href="https://github.com/eliangcs"><img src="https://github.com/eliangcs.png" width="60px" alt="Chang-Hung Liang" /></a><a href="https://github.com/raveensrk"><img src="https://github.com/raveensrk.png" width="60px" alt="Raveen Kumar" /></a><a href="https://github.com/asphaltbuffet"><img src="https://github.com/asphaltbuffet.png" width="60px" alt="Ben Lechlitner" /></a><a href="https://github.com/yash1th"><img src="https://github.com/yash1th.png" width="60px" alt="yash" /></a><a href="https://github.com/kg8m"><img src="https://github.com/kg8m.png" width="60px" alt="Takumi KAGIYAMA" /></a><a href="https://github.com/polm"><img src="https://github.com/polm.png" width="60px" alt="Paul O'Leary McCann" /></a><a href="https://github.com/rbeeger"><img src="https://github.com/rbeeger.png" width="60px" alt="Robert Beeger" /></a><a href="https://github.com/veebch"><img src="https://github.com/veebch.png" width="60px" alt="VEEB Projects" /></a><!-- sponsors -->
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
@@ -33,35 +42,37 @@ Table of Contents
|
||||
* [Upgrading fzf](#upgrading-fzf)
|
||||
* [Building fzf](#building-fzf)
|
||||
* [Usage](#usage)
|
||||
* [Using the finder](#using-the-finder)
|
||||
* [Layout](#layout)
|
||||
* [Search syntax](#search-syntax)
|
||||
* [Environment variables](#environment-variables)
|
||||
* [Options](#options)
|
||||
* [Demo](#demo)
|
||||
* [Using the finder](#using-the-finder)
|
||||
* [Layout](#layout)
|
||||
* [Search syntax](#search-syntax)
|
||||
* [Environment variables](#environment-variables)
|
||||
* [Options](#options)
|
||||
* [Demo](#demo)
|
||||
* [Examples](#examples)
|
||||
* [`fzf-tmux` script](#fzf-tmux-script)
|
||||
* [Key bindings for command-line](#key-bindings-for-command-line)
|
||||
* [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh)
|
||||
* [Files and directories](#files-and-directories)
|
||||
* [Process IDs](#process-ids)
|
||||
* [Host names](#host-names)
|
||||
* [Environment variables / Aliases](#environment-variables--aliases)
|
||||
* [Settings](#settings)
|
||||
* [Supported commands](#supported-commands)
|
||||
* [Custom fuzzy completion](#custom-fuzzy-completion)
|
||||
* [Files and directories](#files-and-directories)
|
||||
* [Process IDs](#process-ids)
|
||||
* [Host names](#host-names)
|
||||
* [Environment variables / Aliases](#environment-variables--aliases)
|
||||
* [Settings](#settings)
|
||||
* [Supported commands](#supported-commands)
|
||||
* [Custom fuzzy completion](#custom-fuzzy-completion)
|
||||
* [Vim plugin](#vim-plugin)
|
||||
* [Advanced topics](#advanced-topics)
|
||||
* [Performance](#performance)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Turning into a different process](#turning-into-a-different-process)
|
||||
* [Reloading the candidate list](#reloading-the-candidate-list)
|
||||
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
||||
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
||||
* [Preview window](#preview-window)
|
||||
* [Previewing an image](#previewing-an-image)
|
||||
* [Tips](#tips)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Related projects](#related-projects)
|
||||
* [License](#license)
|
||||
|
||||
@@ -123,6 +134,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
| pkg | FreeBSD | `pkg install fzf` |
|
||||
| pkgin | NetBSD | `pkgin install fzf` |
|
||||
| pkg_add | OpenBSD | `pkg_add fzf` |
|
||||
| Portage | Gentoo | `emerge --ask app-shells/fzf` |
|
||||
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
||||
| Zypper | openSUSE | `sudo zypper install fzf` |
|
||||
|
||||
@@ -136,15 +148,17 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
### Windows
|
||||
|
||||
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
|
||||
available via [Chocolatey][choco] and [Scoop][scoop]:
|
||||
available via [Chocolatey][choco], [Scoop][scoop], and [Winget][winget]:
|
||||
|
||||
| Package manager | Command |
|
||||
| --- | --- |
|
||||
| Chocolatey | `choco install fzf` |
|
||||
| Scoop | `scoop install fzf` |
|
||||
| Package manager | Command |
|
||||
| --- | --- |
|
||||
| Chocolatey | `choco install fzf` |
|
||||
| Scoop | `scoop install fzf` |
|
||||
| Winget | `winget install fzf` |
|
||||
|
||||
[choco]: https://chocolatey.org/packages/fzf
|
||||
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
|
||||
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/j/junegunn/fzf
|
||||
|
||||
Known issues and limitations on Windows can be found on [the wiki
|
||||
page][windows-wiki].
|
||||
@@ -202,7 +216,23 @@ files excluding hidden ones. (You can override the default command with
|
||||
vim $(fzf)
|
||||
```
|
||||
|
||||
#### Using the finder
|
||||
> *:bulb: A more robust solution would be to use `xargs` but we've presented
|
||||
> the above as it's easier to grasp*
|
||||
> ```sh
|
||||
> fzf --print0 | xargs -0 -o vim
|
||||
> ```
|
||||
|
||||
>
|
||||
> *:bulb: fzf also has the ability to turn itself into a different process.*
|
||||
>
|
||||
> ```sh
|
||||
> fzf --bind 'enter:become(vim {})'
|
||||
> ```
|
||||
>
|
||||
> *See [Turning into a different process](#turning-into-a-different-process)
|
||||
> for more information.*
|
||||
|
||||
### Using the finder
|
||||
|
||||
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
||||
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
|
||||
@@ -211,7 +241,7 @@ vim $(fzf)
|
||||
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
|
||||
multi-select mode
|
||||
|
||||
#### Layout
|
||||
### Layout
|
||||
|
||||
fzf by default starts in fullscreen mode, but you can make it start below the
|
||||
cursor with `--height` option.
|
||||
@@ -234,7 +264,7 @@ default. For example,
|
||||
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
|
||||
```
|
||||
|
||||
#### Search syntax
|
||||
### Search syntax
|
||||
|
||||
Unless otherwise specified, fzf starts in "extended-search mode" where you can
|
||||
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
|
||||
@@ -262,7 +292,7 @@ or `py`.
|
||||
^core go$ | rb$ | py$
|
||||
```
|
||||
|
||||
#### Environment variables
|
||||
### Environment variables
|
||||
|
||||
- `FZF_DEFAULT_COMMAND`
|
||||
- Default command to use when input is tty
|
||||
@@ -278,11 +308,11 @@ or `py`.
|
||||
- Default options
|
||||
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
|
||||
|
||||
#### Options
|
||||
### Options
|
||||
|
||||
See the man page (`man fzf`) for the full list of options.
|
||||
|
||||
#### Demo
|
||||
### Demo
|
||||
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
|
||||
|
||||
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
|
||||
@@ -335,7 +365,7 @@ fish.
|
||||
- Set `FZF_CTRL_T_COMMAND` to override the default command
|
||||
- Set `FZF_CTRL_T_OPTS` to pass additional options to fzf
|
||||
```sh
|
||||
# Preview file content using bat (https://github.com/sharkdp/fd)
|
||||
# Preview file content using bat (https://github.com/sharkdp/bat)
|
||||
export FZF_CTRL_T_OPTS="
|
||||
--preview 'bat -n --color=always {}'
|
||||
--bind 'ctrl-/:change-preview-window(down|hidden|)'"
|
||||
@@ -363,7 +393,7 @@ fish.
|
||||
```
|
||||
|
||||
If you're on a tmux session, you can start fzf in a tmux split-pane or in
|
||||
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
|
||||
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `export FZF_TMUX_OPTS='-p80%,60%'`).
|
||||
See `fzf-tmux --help` for available options.
|
||||
|
||||
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
|
||||
@@ -371,7 +401,7 @@ More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/C
|
||||
Fuzzy completion for bash and zsh
|
||||
---------------------------------
|
||||
|
||||
#### Files and directories
|
||||
### Files and directories
|
||||
|
||||
Fuzzy completion for files and directories can be triggered if the word before
|
||||
the cursor ends with the trigger sequence, which is by default `**`.
|
||||
@@ -400,7 +430,7 @@ cd **<TAB>
|
||||
cd ~/github/fzf**<TAB>
|
||||
```
|
||||
|
||||
#### Process IDs
|
||||
### Process IDs
|
||||
|
||||
Fuzzy completion for PIDs is provided for kill command.
|
||||
|
||||
@@ -409,7 +439,7 @@ Fuzzy completion for PIDs is provided for kill command.
|
||||
kill -9 **<TAB>
|
||||
```
|
||||
|
||||
#### Host names
|
||||
### Host names
|
||||
|
||||
For ssh and telnet commands, fuzzy completion for hostnames is provided. The
|
||||
names are extracted from /etc/hosts and ~/.ssh/config.
|
||||
@@ -419,7 +449,7 @@ ssh **<TAB>
|
||||
telnet **<TAB>
|
||||
```
|
||||
|
||||
#### Environment variables / Aliases
|
||||
### Environment variables / Aliases
|
||||
|
||||
```sh
|
||||
unset **<TAB>
|
||||
@@ -427,7 +457,7 @@ export **<TAB>
|
||||
unalias **<TAB>
|
||||
```
|
||||
|
||||
#### Settings
|
||||
### Settings
|
||||
|
||||
```sh
|
||||
# Use ~~ as the trigger sequence instead of the default **
|
||||
@@ -465,7 +495,7 @@ _fzf_comprun() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Supported commands
|
||||
### Supported commands
|
||||
|
||||
On bash, fuzzy completion is enabled only for a predefined set of commands
|
||||
(`complete | grep _fzf` to see the list). But you can enable it for other
|
||||
@@ -477,7 +507,7 @@ _fzf_setup_completion path ag git kubectl
|
||||
_fzf_setup_completion dir tree
|
||||
```
|
||||
|
||||
#### Custom fuzzy completion
|
||||
### Custom fuzzy completion
|
||||
|
||||
_**(Custom completion API is experimental and subject to change)**_
|
||||
|
||||
@@ -560,6 +590,47 @@ fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort
|
||||
|
||||
See *KEY BINDINGS* section of the man page for details.
|
||||
|
||||
### Turning into a different process
|
||||
|
||||
`become(...)` is similar to `execute(...)`/`execute-silent(...)` described
|
||||
above, but instead of executing the command and coming back to fzf on
|
||||
complete, it turns fzf into a new process for the command.
|
||||
|
||||
```sh
|
||||
fzf --bind 'enter:become(vim {})'
|
||||
```
|
||||
|
||||
Compared to the seemingly equivalent command substitution `vim "$(fzf)"`, this
|
||||
approach has several advantages:
|
||||
|
||||
* Vim will not open an empty file when you terminate fzf with
|
||||
<kbd>CTRL-C</kbd>
|
||||
* Vim will not open an empty file when you press <kbd>ENTER</kbd> on an empty
|
||||
result
|
||||
* Can handle multiple selections even when they have whitespaces
|
||||
```sh
|
||||
fzf --multi --bind 'enter:become(vim {+})'
|
||||
```
|
||||
|
||||
To be fair, running `fzf --print0 | xargs -0 -o vim` instead of `vim "$(fzf)"`
|
||||
resolves all of the issues mentioned. Nonetheless, `become(...)` still offers
|
||||
additional benefits in different scenarios.
|
||||
|
||||
* You can set up multiple bindings to handle the result in different ways
|
||||
without any wrapping script
|
||||
```sh
|
||||
fzf --bind 'enter:become(vim {}),ctrl-e:become(emacs {})'
|
||||
```
|
||||
* Previously, you would have to use `--expect=ctrl-e` and check the first
|
||||
line of the output of fzf
|
||||
* You can easily build the subsequent command using the field index
|
||||
expressions of fzf
|
||||
```sh
|
||||
# Open the file in Vim and go to the line
|
||||
git grep --line-number . |
|
||||
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
### Reloading the candidate list
|
||||
|
||||
By binding `reload` action to a key or an event, you can make fzf dynamically
|
||||
@@ -569,8 +640,8 @@ more details.
|
||||
#### 1. Update the list of processes by pressing CTRL-R
|
||||
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='ps -ef' \
|
||||
fzf --bind 'ctrl-r:reload(eval "$FZF_DEFAULT_COMMAND")' \
|
||||
ps -ef |
|
||||
fzf --bind 'ctrl-r:reload(ps -ef)' \
|
||||
--header 'Press CTRL-R to reload' --header-lines=1 \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
@@ -592,12 +663,12 @@ expression `{q}`. Also, note that we used `--disabled` option so that fzf
|
||||
doesn't perform any secondary filtering.
|
||||
|
||||
```sh
|
||||
INITIAL_QUERY=""
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
|
||||
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
|
||||
--ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--height=50% --layout=reverse
|
||||
: | rg_prefix='rg --column --line-number --no-heading --color=always --smart-case' \
|
||||
fzf --bind 'start:reload:$rg_prefix ""' \
|
||||
--bind 'change:reload:$rg_prefix {q} || true' \
|
||||
--bind 'enter:become(vim {1} +{2})' \
|
||||
--ansi --disabled \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
|
||||
If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
|
||||
@@ -605,7 +676,7 @@ and fzf will warn you about it. To suppress the warning message, we added
|
||||
`|| true` to the command, so that it always exits with 0.
|
||||
|
||||
See ["Using fzf as interactive Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interactive-ripgrep-launcher)
|
||||
for a fuller example with preview window options.
|
||||
for more sophisticated examples.
|
||||
|
||||
### Preview window
|
||||
|
||||
@@ -660,10 +731,26 @@ seq 100 | fzf
|
||||
history | fzf
|
||||
```
|
||||
|
||||
### Previewing an image
|
||||
|
||||
Since 0.43.0, fzf has experimental support for [Kitty graphics
|
||||
protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/), so if you use
|
||||
Kitty, you can make fzf display an image in the preview window.
|
||||
|
||||
```sh
|
||||
fzf --preview='
|
||||
if file --mime-type {} | grep -qF image/; then
|
||||
kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \$d
|
||||
else
|
||||
bat --color=always {}
|
||||
fi
|
||||
'
|
||||
```
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
#### Respecting `.gitignore`
|
||||
### Respecting `.gitignore`
|
||||
|
||||
You can use [fd](https://github.com/sharkdp/fd),
|
||||
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
|
||||
@@ -692,7 +779,7 @@ hidden files, use the following command:
|
||||
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'
|
||||
```
|
||||
|
||||
#### Fish shell
|
||||
### Fish shell
|
||||
|
||||
`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
|
||||
token on the command-line as the root directory for the recursive search. For
|
||||
|
26
bin/fzf-tmux
26
bin/fzf-tmux
@@ -151,12 +151,18 @@ argsf="${TMPDIR:-/tmp}/fzf-args-$id"
|
||||
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||
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() {
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
# Restore tmux window options
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
|
||||
eval "tmux ${tmux_win_opts[*]}"
|
||||
fi
|
||||
|
||||
@@ -179,15 +185,21 @@ trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
tmux_version=$(tmux -V)
|
||||
if [[ ! $tmux_version =~ 3\.2 ]]; then
|
||||
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_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"
|
||||
|
||||
# Build arguments to fzf
|
||||
@@ -221,9 +233,9 @@ else
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
tmux \
|
||||
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
$tmux_off_opts \
|
||||
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
||||
cat $fifo2
|
||||
exit "$(cat $fifo3)"
|
||||
|
48
doc/fzf.txt
48
doc/fzf.txt
@@ -1,4 +1,4 @@
|
||||
fzf.txt fzf Last change: May 19 2021
|
||||
fzf.txt fzf Last change: September 17 2023
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
@@ -32,7 +32,10 @@ depending on the package manager.
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
<
|
||||
If you use {vim-plug}{1}, the same can be written as:
|
||||
@@ -40,7 +43,10 @@ If you use {vim-plug}{1}, the same can be written as:
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
Plug '/opt/homebrew/opt/fzf'
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
<
|
||||
But if you want the latest Vim plugin file from GitHub rather than the one
|
||||
@@ -68,16 +74,16 @@ SUMMARY *fzf-summary*
|
||||
The Vim plugin of fzf provides two core functions, and `:FZF` command which is
|
||||
the basic file selector command built on top of them.
|
||||
|
||||
1. `fzf#run([spec dict])`
|
||||
1. `fzf#run([spec dict])`
|
||||
- Starts fzf inside Vim with the given spec
|
||||
- `:call fzf#run({'source': 'ls'})`
|
||||
2. `fzf#wrap([spec dict]) -> (dict)`
|
||||
- `:call fzf#run({'source': 'ls'})`
|
||||
2. `fzf#wrap([spec dict]) -> (dict)`
|
||||
- Takes a spec for `fzf#run` and returns an extended version of it with
|
||||
additional options for addressing global preferences (`g:fzf_xxx`)
|
||||
- `:echo fzf#wrap({'source': 'ls'})`
|
||||
- `:echo fzf#wrap({'source': 'ls'})`
|
||||
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
|
||||
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
|
||||
3. `:FZF [fzf_options string] [path string]`
|
||||
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
|
||||
3. `:FZF [fzf_options string] [path string]`
|
||||
- Basic fuzzy file selector
|
||||
- A reference implementation for those who don't want to write VimScript to
|
||||
implement custom commands
|
||||
@@ -143,7 +149,7 @@ Examples~
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
@@ -222,12 +228,12 @@ list:
|
||||
`spinner` | Streaming input indicator
|
||||
`query` | Query string
|
||||
`disabled` | Query string when search is disabled
|
||||
`prompt` | Prompt before query ( `> ` )
|
||||
`prompt` | Prompt before query ( `> ` )
|
||||
`pointer` | Pointer to the current line ( `>` )
|
||||
----------------------------+------------------------------------------------------
|
||||
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
||||
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
|
||||
|
||||
For example, consider the following specification:
|
||||
@@ -268,7 +274,7 @@ as the sink.
|
||||
<
|
||||
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
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
>
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
<
|
||||
@@ -296,7 +302,7 @@ The following table summarizes the available options.
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
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
|
||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||
`sink` | funcref | Reference to function to process each selected item
|
||||
@@ -305,8 +311,8 @@ The following table summarizes the available options.
|
||||
`dir` | string | Working directory
|
||||
`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%` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`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
|
||||
@@ -343,7 +349,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
|
||||
`fzf#run`.
|
||||
|
||||
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
|
||||
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
|
||||
- All arguments are optional. Usually we only need to pass a spec
|
||||
dictionary.
|
||||
- `name` is for managing history files. It is ignored if `g:fzf_history_dir`
|
||||
@@ -377,7 +383,7 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
|
||||
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
|
||||
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
|
||||
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||
@@ -396,10 +402,10 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
|
||||
|
||||
- `g:fzf_layout`
|
||||
- `g:fzf_action`
|
||||
- Works only when no custom `sink` (or `sink*`) is provided
|
||||
- Works only when no custom `sink` (or `sinklist`) is provided
|
||||
- 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
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
- `g:fzf_colors`
|
||||
- `g:fzf_history_dir`
|
||||
|
||||
@@ -488,7 +494,7 @@ or above) by putting fzf-tmux options in `tmux` key.
|
||||
*fzf-hide-statusline*
|
||||
|
||||
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 `FileType fzf` autocmd to customize the settings of
|
||||
the window.
|
||||
|
||||
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||
|
6
go.mod
6
go.mod
@@ -5,10 +5,10 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.2
|
||||
github.com/rivo/uniseg v0.4.4
|
||||
github.com/saracen/walker v0.1.3
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
10
go.sum
10
go.sum
@@ -11,8 +11,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
|
||||
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -31,11 +31,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
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/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
9
install
9
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.36.0
|
||||
version=0.43.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -178,6 +178,7 @@ case "$archi" in
|
||||
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
@@ -195,12 +196,12 @@ if [ -n "$binary_error" ]; then
|
||||
echo " - $binary_error !!!"
|
||||
fi
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... "
|
||||
echo -n "Building binary (go install github.com/junegunn/fzf) ... "
|
||||
if [ -z "${GOPATH-}" ]; then
|
||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||
mkdir -p "$GOPATH"
|
||||
fi
|
||||
if go get -ldflags "-s -w -X main.version=$version -X main.revision=go-get" github.com/junegunn/fzf; then
|
||||
if go install -ldflags "-s -w -X main.version=$version -X main.revision=go-install" github.com/junegunn/fzf; then
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
@@ -244,7 +245,7 @@ for shell in $shells; do
|
||||
src=${prefix_expand}.${shell}
|
||||
echo -n "Generate $src ... "
|
||||
|
||||
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
|
||||
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
||||
if [ $auto_completion -eq 0 ]; then
|
||||
fzf_completion="# $fzf_completion"
|
||||
fi
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.36.0"
|
||||
$version="0.43.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.36"
|
||||
var version string = "0.43"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
@@ -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
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Jan 2023" "fzf 0.36.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Oct 2023" "fzf 0.43.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
360
man/man1/fzf.1
360
man/man1/fzf.1
@@ -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
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jan 2023" "fzf 0.36.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Oct 2023" "fzf 0.43.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -57,9 +57,9 @@ 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 for paths (additional bonus point only after path separator)
|
||||
.BR path " Scoring scheme well suited for file paths
|
||||
.br
|
||||
.BR history " Scoring scheme for command history (no additional bonus points).
|
||||
.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
|
||||
@@ -92,6 +92,19 @@ interface rather than a "fuzzy finder". You can later enable the search using
|
||||
.B "+s, --no-sort"
|
||||
Do not sort the result
|
||||
.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"
|
||||
Reverse the order of the input
|
||||
|
||||
@@ -215,6 +228,10 @@ Draw border around the finder
|
||||
.br
|
||||
.BR double " Border with double lines"
|
||||
.br
|
||||
.BR block " Border using block elements; suitable when using different background colors"
|
||||
.br
|
||||
.BR thinblock " Border using legacy computing symbols; may not be displayed on some terminals"
|
||||
.br
|
||||
.BR horizontal " Horizontal lines above and below the finder"
|
||||
.br
|
||||
.BR vertical " Vertical lines on each side of the finder"
|
||||
@@ -231,8 +248,9 @@ Draw border around the finder
|
||||
.br
|
||||
|
||||
If you use a terminal emulator where each box-drawing character takes
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
|
||||
still not properly rendered, set \fB--no-unicode\fR.
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
|
||||
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
|
||||
\fB--no-unicode\fR.
|
||||
|
||||
.TP
|
||||
.BI "--border-label" [=LABEL]
|
||||
@@ -337,11 +355,17 @@ e.g.
|
||||
Determines the display style of finder info (match counters).
|
||||
|
||||
.br
|
||||
.BR default " Display on the next line to the prompt"
|
||||
.BR default " Display on the next line to the prompt"
|
||||
.br
|
||||
.BR inline " Display on the same line"
|
||||
.BR right " Display on the right end of the next line to the prompt"
|
||||
.br
|
||||
.BR hidden " Do not display finder info"
|
||||
.BR inline " Display on the same line with the default separator ' < '"
|
||||
.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
|
||||
|
||||
.TP
|
||||
@@ -361,9 +385,10 @@ Do not display horizontal separator on the info line. A synonym for
|
||||
\fB--separator=''\fB
|
||||
|
||||
.TP
|
||||
.BI "--scrollbar=" "CHAR"
|
||||
.BI "--scrollbar=" "CHAR1[CHAR2]"
|
||||
Use the given character to render scrollbar. (default: '│' or ':' depending on
|
||||
\fB--no-unicode\fR).
|
||||
\fB--no-unicode\fR). The optional \fBCHAR2\fR is used to render scrollbar of
|
||||
the preview window.
|
||||
|
||||
.TP
|
||||
.B "--no-scrollbar"
|
||||
@@ -417,28 +442,30 @@ color mappings.
|
||||
\fBbw \fRNo colors (equivalent to \fB--no-color\fR)
|
||||
|
||||
.B COLOR NAMES:
|
||||
\fBfg \fRText
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBquery \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBseparator \fRHorizontal separator on info line
|
||||
\fBscrollbar \fRScrollbar
|
||||
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
|
||||
\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
|
||||
\fBfg \fRText
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBquery \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBscrollbar \fRScrollbar
|
||||
\fBpreview-border \fRBorder around the preview window (\fB--preview\fR)
|
||||
\fBpreview-scrollbar \fRScrollbar
|
||||
\fBseparator \fRHorizontal separator on info line
|
||||
\fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR)
|
||||
\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:
|
||||
\fB-1 \fRDefault terminal foreground/background color
|
||||
@@ -564,6 +591,18 @@ e.g.
|
||||
echo "$i"
|
||||
sleep 0.01
|
||||
done'\fR
|
||||
|
||||
Since 0.43.0, fzf has experimental support for Kitty graphics protocol,
|
||||
so if you use Kitty, you can make fzf display an image in the preview window.
|
||||
|
||||
e.g.
|
||||
\fBfzf --preview='
|
||||
if file --mime-type {} | grep -qF "image/"; then
|
||||
kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \\$d
|
||||
else
|
||||
bat --color=always {}
|
||||
fi
|
||||
'\fR
|
||||
.RE
|
||||
|
||||
.TP
|
||||
@@ -580,6 +619,10 @@ Should be used with one of the following \fB--preview-window\fR options.
|
||||
.br
|
||||
.B * border-double
|
||||
.br
|
||||
.B * border-block
|
||||
.br
|
||||
.B * border-thinblock
|
||||
.br
|
||||
.B * border-horizontal
|
||||
.br
|
||||
.B * border-top
|
||||
@@ -727,6 +770,21 @@ Read input delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--print0"
|
||||
Print output delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--no-clear"
|
||||
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
|
||||
\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
|
||||
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
|
||||
.B "--sync"
|
||||
Synchronous search for multi-staged filtering. If specified, fzf will launch
|
||||
@@ -736,16 +794,31 @@ ncurses finder only after the input stream is complete.
|
||||
e.g. \fBfzf --multi | fzf --sync\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--listen=HTTP_PORT"
|
||||
.B "--listen[=HTTP_PORT]"
|
||||
Start HTTP server on the given port. It allows external processes to send
|
||||
actions to perform via POST method.
|
||||
actions to perform via POST method. If the port number is omitted or given as
|
||||
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
|
||||
environment variable to the child processes started via \fBexecute\fR and
|
||||
\fBexecute-silent\fR actions. 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.
|
||||
|
||||
e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
fzf --listen 6266
|
||||
|
||||
# Get program state in JSON format (experimental)
|
||||
curl localhost:6266
|
||||
|
||||
# Send action to the server
|
||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||
|
||||
# Start HTTP server on port 6266 and send an authenticated action
|
||||
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
|
||||
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"
|
||||
@@ -763,6 +836,11 @@ this case make sure that the command is POSIX-compliant.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
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
|
||||
.BR 0 " Normal exit"
|
||||
@@ -848,6 +926,8 @@ e.g.
|
||||
.br
|
||||
\fIctrl-space\fR
|
||||
.br
|
||||
\fIctrl-delete\fR
|
||||
.br
|
||||
\fIctrl-\\\fR
|
||||
.br
|
||||
\fIctrl-]\fR
|
||||
@@ -916,6 +996,8 @@ e.g.
|
||||
.br
|
||||
\fIshift-right\fR
|
||||
.br
|
||||
\fIshift-delete\fR
|
||||
.br
|
||||
\fIalt-shift-up\fR
|
||||
.br
|
||||
\fIalt-shift-down\fR
|
||||
@@ -930,6 +1012,22 @@ e.g.
|
||||
.br
|
||||
\fIdouble-click\fR
|
||||
.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
|
||||
|
||||
.SS AVAILABLE EVENTS:
|
||||
@@ -959,6 +1057,44 @@ e.g.
|
||||
\fB# Move cursor to the first entry whenever the query is changed
|
||||
fzf --bind change:first\fR
|
||||
.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
|
||||
.RS
|
||||
@@ -972,89 +1108,103 @@ e.g.
|
||||
.SS AVAILABLE ACTIONS:
|
||||
A key or an event can be bound to one or more of the following actions.
|
||||
|
||||
\fBACTION: DEFAULT BINDINGS (NOTES):
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBbackward-char\fR \fIctrl-b left\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-kill-word\fR \fIalt-bs\fR
|
||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
\fBchange-prompt(...)\fR (change prompt to the given string)
|
||||
\fBchange-query(...)\fR (change query string to the given string)
|
||||
\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)
|
||||
\fBACTION: DEFAULT BINDINGS (NOTES):
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBbackward-char\fR \fIctrl-b left\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-kill-word\fR \fIalt-bs\fR
|
||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
|
||||
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
|
||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
|
||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
\fBchange-prompt(...)\fR (change prompt to the given string)
|
||||
\fBchange-query(...)\fR (change query string to the given string)
|
||||
\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-all\fR (deselect all matches)
|
||||
\fBdisable-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBenable-search\fR (enable search functionality)
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\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)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBdeselect-all\fR (deselect all matches)
|
||||
\fBdisable-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBenable-search\fR (enable search functionality)
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\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)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBignore\fR
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBjump-accept\fR (jump and accept)
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBjump-accept\fR (jump and accept)
|
||||
\fBkill-line\fR
|
||||
\fBkill-word\fR \fIalt-d\fR
|
||||
\fBlast\fR (move to the last match; same as \fBpos(-1)\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-up\fR \fIpgup\fR
|
||||
\fBkill-word\fR \fIalt-d\fR
|
||||
\fBlast\fR (move to the last match; same as \fBpos(-1)\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-up\fR \fIpgup\fR
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\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
|
||||
\fBhide-preview\fR
|
||||
\fBoffset-down\fR (similar to CTRL-E of Vim)
|
||||
\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-up\fR
|
||||
\fBpreview-half-page-down\fR
|
||||
\fBpreview-half-page-up\fR
|
||||
\fBpreview-bottom\fR
|
||||
\fBpreview-top\fR
|
||||
\fBprint-query\fR (print query and exit)
|
||||
\fBput\fR (put the character to the prompt)
|
||||
\fBput(...)\fR (put the given string to the prompt)
|
||||
\fBprint-query\fR (print query and exit)
|
||||
\fBput\fR (put the character to the prompt)
|
||||
\fBput(...)\fR (put the given string to the prompt)
|
||||
\fBrefresh-preview\fR
|
||||
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
|
||||
\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)
|
||||
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
|
||||
\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)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\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)
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBshow-preview\fR
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\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-wrap\fR
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\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
|
||||
\fBtoggle-track\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-header(...)\fR (transform header using an external command)
|
||||
\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
|
||||
|
||||
@@ -1119,6 +1269,14 @@ 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
|
||||
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
|
||||
|
||||
\fBreload(...)\fR action is used to dynamically update the input list
|
||||
|
@@ -164,7 +164,7 @@ function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
@@ -456,6 +456,30 @@ function! s:writefile(...)
|
||||
endif
|
||||
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
|
||||
try
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||
@@ -511,10 +535,8 @@ try
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
endif
|
||||
" Respect --border option given in 'options'
|
||||
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0
|
||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||
endif
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
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
|
||||
if len(source_command)
|
||||
let $FZF_DEFAULT_COMMAND = source_command
|
||||
@@ -741,7 +763,7 @@ function! s:calc_size(max, val, dict)
|
||||
return size
|
||||
endif
|
||||
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
|
||||
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
|
||||
let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
|
||||
if stridx(opts, '--header') > stridx(opts, '--no-header')
|
||||
let margin += len(split(opts, "\n"))
|
||||
endif
|
||||
@@ -923,7 +945,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
let term_opts.curwin = 1
|
||||
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')
|
||||
doautocmd <nomodeline> TerminalWinOpen
|
||||
endif
|
||||
|
@@ -9,23 +9,24 @@
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
[[ $- =~ i ]] || return 0
|
||||
|
||||
|
||||
# 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() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
if ! declare -F _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -68,59 +69,185 @@ _fzf_opts_completion() {
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="
|
||||
-h --help
|
||||
-x --extended
|
||||
-e --exact
|
||||
--extended-exact
|
||||
+x --no-extended
|
||||
+e --no-exact
|
||||
-q --query
|
||||
-f --filter
|
||||
--literal
|
||||
--no-literal
|
||||
--algo
|
||||
-i +i
|
||||
--scheme
|
||||
--expect
|
||||
--no-expect
|
||||
--enabled --no-phony
|
||||
--disabled --phony
|
||||
--tiebreak
|
||||
--bind
|
||||
--color
|
||||
--toggle-sort
|
||||
-d --delimiter
|
||||
-n --nth
|
||||
--with-nth
|
||||
-d --delimiter
|
||||
-s --sort
|
||||
+s --no-sort
|
||||
--track
|
||||
--no-track
|
||||
--tac
|
||||
--tiebreak
|
||||
--no-tac
|
||||
-i
|
||||
+i
|
||||
-m --multi
|
||||
+m --no-multi
|
||||
--ansi
|
||||
--no-ansi
|
||||
--no-mouse
|
||||
--bind
|
||||
--cycle
|
||||
--no-hscroll
|
||||
--jump-labels
|
||||
--height
|
||||
--literal
|
||||
+c --no-color
|
||||
+2 --no-256
|
||||
--black
|
||||
--no-black
|
||||
--bold
|
||||
--no-bold
|
||||
--layout
|
||||
--reverse
|
||||
--margin
|
||||
--no-reverse
|
||||
--cycle
|
||||
--no-cycle
|
||||
--keep-right
|
||||
--no-keep-right
|
||||
--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
|
||||
-1 --select-1
|
||||
+1 --no-select-1
|
||||
-0 --exit-0
|
||||
+0 --no-exit-0
|
||||
--read0
|
||||
--no-read0
|
||||
--print0
|
||||
--no-print0
|
||||
--print-query
|
||||
--no-print-query
|
||||
--prompt
|
||||
--pointer
|
||||
--marker
|
||||
--header
|
||||
--header-lines
|
||||
--ansi
|
||||
--tabstop
|
||||
--color
|
||||
--no-bold
|
||||
--sync
|
||||
--no-sync
|
||||
--async
|
||||
--no-history
|
||||
--history
|
||||
--history-size
|
||||
--no-header
|
||||
--no-header-lines
|
||||
--header
|
||||
--header-lines
|
||||
--header-first
|
||||
--no-header-first
|
||||
--ellipsis
|
||||
--preview
|
||||
--no-preview
|
||||
--preview-window
|
||||
-q --query
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
-f --filter
|
||||
--print-query
|
||||
--expect
|
||||
--sync"
|
||||
--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
|
||||
--listen
|
||||
--no-listen
|
||||
--clear
|
||||
--no-clear
|
||||
--version
|
||||
--"
|
||||
|
||||
case "${prev}" in
|
||||
--algo)
|
||||
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--scheme)
|
||||
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
|
||||
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--history)
|
||||
COMPREPLY=()
|
||||
--layout)
|
||||
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
|
||||
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
|
||||
;;
|
||||
esac
|
||||
@@ -170,9 +297,9 @@ __fzf_generic_path_completion() {
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base"
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
|
||||
dir=
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
@@ -182,7 +309,7 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[[ -z "$dir" ]] && dir='.'
|
||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||
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
|
||||
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
|
||||
printf "%q " "${item%$3}$3"
|
||||
done)
|
||||
matches=${matches% }
|
||||
@@ -195,7 +322,7 @@ __fzf_generic_path_completion() {
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
dir=$(command dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
@@ -229,16 +356,16 @@ _fzf_complete() {
|
||||
fi
|
||||
|
||||
local cur selected trigger cmd post
|
||||
post="$(caller 0 | awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
||||
post="$(caller 0 | command awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
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 | 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 | command tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [[ -n "$selected" ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
@@ -270,33 +397,68 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_proc_completion_post() {
|
||||
awk '{print $2}'
|
||||
command 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-completions’s _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 awk '{if (length($2) > 0) {print $2}}' | command sort -u
|
||||
}
|
||||
fi
|
||||
|
||||
_fzf_host_completion() {
|
||||
_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 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||
}
|
||||
|
||||
# Values for $1 $2 $3 are described here
|
||||
# https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
|
||||
# > 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_complete -m -- "$@" < <(
|
||||
declare -xp | sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
|
||||
declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_alias_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
alias | sed -En 's|^alias ([^=]+).*|\1|p'
|
||||
alias | command sed -En 's|^alias ([^=]+).*|\1|p'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -309,8 +471,8 @@ complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
||||
awk bat cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||
@@ -350,6 +512,9 @@ for cmd in $d_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# ssh
|
||||
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
|
||||
|
||||
unset cmd d_cmds a_cmds
|
||||
|
||||
_fzf_setup_completion() {
|
||||
@@ -373,9 +538,7 @@ _fzf_setup_completion() {
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts / Process
|
||||
_fzf_setup_completion 'var' export unset
|
||||
_fzf_setup_completion 'var' export unset printenv
|
||||
_fzf_setup_completion 'alias' unalias
|
||||
_fzf_setup_completion 'host' ssh telnet
|
||||
_fzf_setup_completion 'host' telnet
|
||||
_fzf_setup_completion 'proc' kill
|
||||
|
||||
fi
|
||||
|
@@ -9,6 +9,9 @@
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
|
||||
# Both branches of the following `if` do the same thing -- define
|
||||
# __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
|
||||
@@ -67,15 +70,12 @@ fi
|
||||
# control. There are several others that could wreck havoc if they are set
|
||||
# to values we don't expect. With the following `emulate` command we
|
||||
# sidestep this issue entirely.
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||
|
||||
# This brace is the start of try-always block. The `always` part is like
|
||||
# `finally` in lesser languages. We use it to *always* restore user options.
|
||||
{
|
||||
|
||||
# Bail out if not interactive shell.
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
@@ -137,7 +137,10 @@ __fzf_generic_path_completion() {
|
||||
tail=$6
|
||||
|
||||
setopt localoptions nonomatch
|
||||
eval "base=$base"
|
||||
if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while [ 1 ]; do
|
||||
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||
@@ -145,7 +148,7 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
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
|
||||
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
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n "${(q)item} "
|
||||
done)
|
||||
@@ -215,21 +218,37 @@ _fzf_complete() {
|
||||
command rm -f "$fifo"
|
||||
}
|
||||
|
||||
_fzf_complete_telnet() {
|
||||
_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
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
# 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 | 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
|
||||
)
|
||||
<(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') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
}
|
||||
fi
|
||||
|
||||
_fzf_complete_telnet() {
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||
}
|
||||
|
||||
# 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() {
|
||||
local tokens=(${(z)1})
|
||||
case ${tokens[-1]} in
|
||||
-i|-F|-E)
|
||||
_fzf_path_completion "$prefix" "$1"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_fzf_complete_export() {
|
||||
@@ -251,8 +270,9 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
)
|
||||
}
|
||||
|
||||
@@ -292,6 +312,9 @@ fzf-completion() {
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||
|
||||
[ -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]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||
|
@@ -11,15 +11,18 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
[[ $- =~ i ]] || return 0
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
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 d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
|
||||
-o -type l -print 2> /dev/null | command cut -b3-"}"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
|
||||
eval "$cmd" |
|
||||
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
|
||||
while read -r item; do
|
||||
@@ -27,8 +30,6 @@ __fzf_select__() {
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
__fzfcmd() {
|
||||
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
@@ -42,28 +43,60 @@ fzf-file-widget() {
|
||||
|
||||
__fzf_cd__() {
|
||||
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 \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
|
||||
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$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 \
|
||||
-o -type d -print 2> /dev/null | command cut -b3-"}"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
|
||||
dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
|
||||
}
|
||||
|
||||
__fzf_history__() {
|
||||
local output opts script
|
||||
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{$_}++'
|
||||
output=$(
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" |
|
||||
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
|
||||
}
|
||||
if command -v perl > /dev/null; then
|
||||
__fzf_history__() {
|
||||
local output opts script
|
||||
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{$_}++'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
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
|
||||
}
|
||||
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
|
||||
bind -m emacs-standard '"\er": redraw-current-line'
|
||||
@@ -79,7 +112,7 @@ if (( BASH_VERSINFO[0] < 4 )); then
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__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-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
else
|
||||
@@ -98,5 +131,3 @@ 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 vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
|
||||
fi
|
||||
|
@@ -11,6 +11,9 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
status is-interactive; or exit 0
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
function fzf_key_bindings
|
||||
@@ -22,17 +25,17 @@ function fzf_key_bindings
|
||||
set -l fzf_query $commandline[2]
|
||||
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.
|
||||
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 d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --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
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
@@ -79,11 +82,11 @@ function fzf_key_bindings
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
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@^\./@@'"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
|
@@ -11,6 +11,9 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
@@ -32,21 +35,19 @@ else
|
||||
}
|
||||
fi
|
||||
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||
|
||||
{
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__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 d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
local item
|
||||
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
|
||||
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
|
||||
echo -n "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
@@ -72,10 +73,10 @@ bindkey -M viins '^T' fzf-file-widget
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
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-"}"
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
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)"
|
||||
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)"
|
||||
if [[ -z "$dir" ]]; then
|
||||
zle redisplay
|
||||
return 0
|
||||
|
@@ -221,9 +221,9 @@ func charClassOfAscii(char rune) charClass {
|
||||
return charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
return charNumber
|
||||
} else if strings.IndexRune(whiteChars, char) >= 0 {
|
||||
} else if strings.ContainsRune(whiteChars, char) {
|
||||
return charWhite
|
||||
} else if strings.IndexRune(delimiterChars, char) >= 0 {
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
@@ -240,7 +240,7 @@ func charClassOfNonAscii(char rune) charClass {
|
||||
return charLetter
|
||||
} else if unicode.IsSpace(char) {
|
||||
return charWhite
|
||||
} else if strings.IndexRune(delimiterChars, char) >= 0 {
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
|
@@ -381,10 +381,19 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
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
|
||||
state.attr = state.attr &^ tui.Italic
|
||||
case 24: // tput rmul
|
||||
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:
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
|
@@ -58,9 +58,9 @@ var defaultCommand string
|
||||
|
||||
func init() {
|
||||
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" {
|
||||
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-"`
|
||||
}
|
||||
}
|
||||
|
||||
|
43
src/core.go
43
src/core.go
@@ -138,7 +138,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
inputRevision := 0
|
||||
snapshotRevision := 0
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
if opts.Filter != nil {
|
||||
@@ -209,8 +211,6 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
// Event coordination
|
||||
reading := true
|
||||
clearCache := util.Once(false)
|
||||
clearSelection := util.Once(false)
|
||||
ticks := 0
|
||||
var nextCommand *string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
@@ -219,6 +219,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
determine := func(final bool) {
|
||||
if heightUnknown {
|
||||
if total >= maxFit || final {
|
||||
deferred = false
|
||||
heightUnknown = false
|
||||
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
||||
}
|
||||
@@ -230,26 +231,20 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
useSnapshot := false
|
||||
var snapshot []*Chunk
|
||||
var prevSnapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command string) {
|
||||
reading = true
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
// We should not update snapshot if reload is triggered again while
|
||||
// the previous reload is in progress
|
||||
if useSnapshot && prevSnapshot != nil {
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
}
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision++
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
}
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
input := func(reloaded bool) []rune {
|
||||
input := func() []rune {
|
||||
reloaded := snapshotRevision != inputRevision
|
||||
paused, input := terminal.Input()
|
||||
if reloaded && paused {
|
||||
query = []rune{}
|
||||
@@ -279,29 +274,30 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
useSnapshot = false
|
||||
prevSnapshot = nil
|
||||
}
|
||||
if !useSnapshot {
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
opts.Sync = false
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
|
||||
}
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
var changed bool
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
changed = val.changed
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
@@ -313,13 +309,20 @@ func Run(opts *Options, version string, revision string) {
|
||||
} else {
|
||||
restart(*command)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
if !useSnapshot {
|
||||
snapshot, _ = chunkList.Snapshot()
|
||||
newSnapshot, _ := chunkList.Snapshot()
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// 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
|
||||
}
|
||||
}
|
||||
reset := !useSnapshot && clearCache()
|
||||
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||
delay = false
|
||||
|
||||
case EvtSearchProgress:
|
||||
@@ -359,7 +362,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
determine(val.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, clearSelection())
|
||||
terminal.UpdateList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -26,12 +25,12 @@ func NewHistory(path string, maxSize int) (*History, error) {
|
||||
}
|
||||
|
||||
// Read history file
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
// If it doesn't exist, check if we can create a file with the name
|
||||
if os.IsNotExist(err) {
|
||||
data = []byte{}
|
||||
if err := ioutil.WriteFile(path, data, 0600); err != nil {
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return nil, fmtError(err)
|
||||
}
|
||||
} else {
|
||||
@@ -62,11 +61,11 @@ func (h *History) append(line string) error {
|
||||
lines = lines[len(lines)-h.maxSize:]
|
||||
}
|
||||
h.lines = append(lines, "")
|
||||
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
|
||||
return os.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
|
||||
}
|
||||
|
||||
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 {
|
||||
h.lines[h.cursor] = str
|
||||
} else if h.cursor < len(h.lines)-1 {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -25,7 +24,7 @@ func TestHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f, _ := os.CreateTemp("", "fzf-history")
|
||||
f.Close()
|
||||
|
||||
{ // Append lines
|
||||
|
@@ -12,11 +12,11 @@ import (
|
||||
|
||||
// MatchRequest represents a search request
|
||||
type MatchRequest struct {
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
clearCache bool
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
revision int
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
@@ -29,6 +29,7 @@ type Matcher struct {
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
revision int
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -38,7 +39,7 @@ const (
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
return &Matcher{
|
||||
patternBuilder: patternBuilder,
|
||||
@@ -48,7 +49,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger)}
|
||||
mergerCache: make(map[string]*Merger),
|
||||
revision: revision}
|
||||
}
|
||||
|
||||
// Loop puts Matcher in action
|
||||
@@ -70,8 +72,9 @@ func (m *Matcher) Loop() {
|
||||
events.Clear()
|
||||
})
|
||||
|
||||
if request.sort != m.sort || request.clearCache {
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.revision = request.revision
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
clearChunkCache()
|
||||
}
|
||||
@@ -140,11 +143,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger, false
|
||||
return EmptyMerger(request.revision), false
|
||||
}
|
||||
pattern := request.pattern
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac), false
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
@@ -218,11 +221,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac), false
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
@@ -231,5 +234,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
||||
} else {
|
||||
event = reqRetry
|
||||
}
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
|
||||
}
|
||||
|
@@ -3,30 +3,36 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
func EmptyMerger(revision int) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
revision int
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -35,17 +41,18 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0}
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -53,11 +60,42 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
|
||||
return &mg
|
||||
}
|
||||
|
||||
// Revision returns revision number
|
||||
func (mg *Merger) Revision() int {
|
||||
return mg.revision
|
||||
}
|
||||
|
||||
// Length returns the number of items
|
||||
func (mg *Merger) Length() int {
|
||||
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
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
|
@@ -23,10 +23,10 @@ func randResult() Result {
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, EmptyMerger.Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger.count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
||||
assert(t, EmptyMerger(0).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(0).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
@@ -57,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false)
|
||||
mg := NewMerger(nil, lists, false, false, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -69,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false)
|
||||
mg := NewMerger(nil, lists, true, false, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -79,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false)
|
||||
mg2 := NewMerger(nil, lists, true, false, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
194
src/options.go
194
src/options.go
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/mattn/go-shellwords"
|
||||
@@ -32,6 +33,7 @@ const usage = `usage: fzf [options]
|
||||
field index expressions
|
||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||
+s, --no-sort Do not sort the result
|
||||
--track Track the current selection when the result is updated
|
||||
--tac Reverse the order of the input
|
||||
--disabled Do not perform search
|
||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||
@@ -61,7 +63,7 @@ const usage = `usage: fzf [options]
|
||||
(default: 10)
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
--border[=STYLE] Draw border around the finder
|
||||
[rounded|sharp|horizontal|vertical|
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
top|bottom|left|right|none] (default: rounded)
|
||||
--border-label=LABEL Label to print on the border
|
||||
--border-label-pos=COL Position of the border label
|
||||
@@ -70,10 +72,11 @@ const usage = `usage: fzf [options]
|
||||
(default: 0 or center)
|
||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--info=STYLE Finder info style [default|inline|hidden]
|
||||
--info=STYLE Finder info style
|
||||
[default|right|hidden|inline[:SEPARATOR]|inline-right]
|
||||
--separator=STR String to form horizontal separator on info line
|
||||
--no-separator Hide info line separator
|
||||
--scrollbar[=CHAR] Scrollbar character
|
||||
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
||||
--no-scrollbar Hide scrollbar
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
--pointer=STR Pointer to the current line (default: '>')
|
||||
@@ -115,16 +118,19 @@ const usage = `usage: fzf [options]
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
|
||||
--listen[=HTTP_PORT] Start HTTP server to receive actions (POST /)
|
||||
--version Display version information and exit
|
||||
|
||||
Environment variables
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
FZF_DEFAULT_OPTS Default options
|
||||
(e.g. '--layout=reverse --inline-info')
|
||||
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
||||
|
||||
`
|
||||
|
||||
const defaultInfoSep = " < "
|
||||
|
||||
// Case denotes case-sensitivity of search
|
||||
type Case int
|
||||
|
||||
@@ -161,6 +167,14 @@ func defaultMargin() [4]sizeSpec {
|
||||
return [4]sizeSpec{}
|
||||
}
|
||||
|
||||
type trackOption int
|
||||
|
||||
const (
|
||||
trackDisabled trackOption = iota
|
||||
trackEnabled
|
||||
trackCurrent
|
||||
)
|
||||
|
||||
type windowPosition int
|
||||
|
||||
const (
|
||||
@@ -182,10 +196,16 @@ type infoStyle int
|
||||
|
||||
const (
|
||||
infoDefault infoStyle = iota
|
||||
infoRight
|
||||
infoInline
|
||||
infoInlineRight
|
||||
infoHidden
|
||||
)
|
||||
|
||||
func (s infoStyle) noExtraLine() bool {
|
||||
return s == infoInline || s == infoInlineRight || s == infoHidden
|
||||
}
|
||||
|
||||
type labelOpts struct {
|
||||
label string
|
||||
column int
|
||||
@@ -246,6 +266,10 @@ func (a previewOpts) sameContentLayout(b previewOpts) bool {
|
||||
return a.wrap == b.wrap && a.headerLines == b.headerLines
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
return strings.SplitN(s, "\n", 2)[0]
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
type Options struct {
|
||||
Fuzzy bool
|
||||
@@ -259,6 +283,7 @@ type Options struct {
|
||||
WithNth []Range
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Track trackOption
|
||||
Tac bool
|
||||
Criteria []criterion
|
||||
Multi int
|
||||
@@ -277,6 +302,7 @@ type Options struct {
|
||||
ScrollOff int
|
||||
FileWord bool
|
||||
InfoStyle infoStyle
|
||||
InfoSep string
|
||||
Separator *string
|
||||
JumpLabels string
|
||||
Prompt string
|
||||
@@ -308,7 +334,7 @@ type Options struct {
|
||||
PreviewLabel labelOpts
|
||||
Unicode bool
|
||||
Tabstop int
|
||||
ListenPort int
|
||||
ListenPort *int
|
||||
ClearOnExit bool
|
||||
Version bool
|
||||
}
|
||||
@@ -330,6 +356,7 @@ func defaultOptions() *Options {
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Track: trackDisabled,
|
||||
Tac: false,
|
||||
Criteria: []criterion{byScore, byLength},
|
||||
Multi: 0,
|
||||
@@ -525,6 +552,10 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
return tui.BorderSharp
|
||||
case "bold":
|
||||
return tui.BorderBold
|
||||
case "block":
|
||||
return tui.BorderBlock
|
||||
case "thinblock":
|
||||
return tui.BorderThinBlock
|
||||
case "double":
|
||||
return tui.BorderDouble
|
||||
case "horizontal":
|
||||
@@ -545,7 +576,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
if optional && str == "" {
|
||||
return tui.DefaultBorderShape
|
||||
}
|
||||
errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||
errorExit("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
|
||||
}
|
||||
return tui.BorderNone
|
||||
}
|
||||
@@ -593,6 +624,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.BSpace)
|
||||
case "ctrl-space":
|
||||
add(tui.CtrlSpace)
|
||||
case "ctrl-delete":
|
||||
add(tui.CtrlDelete)
|
||||
case "ctrl-^", "ctrl-6":
|
||||
add(tui.CtrlCaret)
|
||||
case "ctrl-/", "ctrl-_":
|
||||
@@ -609,6 +642,12 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.Start)
|
||||
case "load":
|
||||
add(tui.Load)
|
||||
case "focus":
|
||||
add(tui.Focus)
|
||||
case "one":
|
||||
add(tui.One)
|
||||
case "zero":
|
||||
add(tui.Zero)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
@@ -657,12 +696,30 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.SLeft)
|
||||
case "shift-right":
|
||||
add(tui.SRight)
|
||||
case "shift-delete":
|
||||
add(tui.SDelete)
|
||||
case "left-click":
|
||||
add(tui.LeftClick)
|
||||
case "right-click":
|
||||
add(tui.RightClick)
|
||||
case "shift-left-click":
|
||||
add(tui.SLeftClick)
|
||||
case "shift-right-click":
|
||||
add(tui.SRightClick)
|
||||
case "double-click":
|
||||
add(tui.DoubleClick)
|
||||
case "scroll-up":
|
||||
add(tui.ScrollUp)
|
||||
case "scroll-down":
|
||||
add(tui.ScrollDown)
|
||||
case "shift-scroll-up":
|
||||
add(tui.SScrollUp)
|
||||
case "shift-scroll-down":
|
||||
add(tui.SScrollDown)
|
||||
case "preview-scroll-up":
|
||||
add(tui.PreviewScrollUp)
|
||||
case "preview-scroll-down":
|
||||
add(tui.PreviewScrollDown)
|
||||
case "f10":
|
||||
add(tui.F10)
|
||||
case "f11":
|
||||
@@ -863,10 +920,14 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
||||
mergeAttr(&theme.CurrentMatch)
|
||||
case "border":
|
||||
mergeAttr(&theme.Border)
|
||||
case "preview-border":
|
||||
mergeAttr(&theme.PreviewBorder)
|
||||
case "separator":
|
||||
mergeAttr(&theme.Separator)
|
||||
case "scrollbar":
|
||||
mergeAttr(&theme.Scrollbar)
|
||||
case "preview-scrollbar":
|
||||
mergeAttr(&theme.PreviewScrollbar)
|
||||
case "label":
|
||||
mergeAttr(&theme.BorderLabel)
|
||||
case "preview-label":
|
||||
@@ -912,7 +973,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -954,7 +1015,7 @@ Loop:
|
||||
ce = regexp.QuoteMeta(ce)
|
||||
|
||||
// @$ or @+
|
||||
loc = regexp.MustCompile(fmt.Sprintf(`^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
|
||||
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
|
||||
if loc == nil {
|
||||
masked += action
|
||||
break
|
||||
@@ -1068,6 +1129,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "toggle-track":
|
||||
appendAction(actToggleTrack)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "track":
|
||||
appendAction(actTrack)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
@@ -1102,12 +1169,20 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actPrevSelected)
|
||||
case "next-selected":
|
||||
appendAction(actNextSelected)
|
||||
case "show-preview":
|
||||
appendAction(actShowPreview)
|
||||
case "hide-preview":
|
||||
appendAction(actHidePreview)
|
||||
case "toggle-preview":
|
||||
appendAction(actTogglePreview)
|
||||
case "toggle-preview-wrap":
|
||||
appendAction(actTogglePreviewWrap)
|
||||
case "toggle-sort":
|
||||
appendAction(actToggleSort)
|
||||
case "offset-up":
|
||||
appendAction(actOffsetUp)
|
||||
case "offset-down":
|
||||
appendAction(actOffsetDown)
|
||||
case "preview-top":
|
||||
appendAction(actPreviewTop)
|
||||
case "preview-bottom":
|
||||
@@ -1158,6 +1233,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
}
|
||||
switch t {
|
||||
case actBecome:
|
||||
if util.IsWindows() {
|
||||
exit("become action is not supported on Windows")
|
||||
}
|
||||
case actUnbind, actRebind:
|
||||
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
||||
case actChangePreviewWindow:
|
||||
@@ -1210,6 +1289,8 @@ func isExecuteAction(str string) actionType {
|
||||
|
||||
prefix := actionNameRegexp.FindString(str)
|
||||
switch prefix {
|
||||
case "become":
|
||||
return actBecome
|
||||
case "reload":
|
||||
return actReload
|
||||
case "reload-sync":
|
||||
@@ -1220,6 +1301,12 @@ func isExecuteAction(str string) actionType {
|
||||
return actRebind
|
||||
case "preview":
|
||||
return actPreview
|
||||
case "change-border-label":
|
||||
return actChangeBorderLabel
|
||||
case "change-header":
|
||||
return actChangeHeader
|
||||
case "change-preview-label":
|
||||
return actChangePreviewLabel
|
||||
case "change-preview-window":
|
||||
return actChangePreviewWindow
|
||||
case "change-preview":
|
||||
@@ -1238,6 +1325,12 @@ func isExecuteAction(str string) actionType {
|
||||
return actExecuteMulti
|
||||
case "put":
|
||||
return actPut
|
||||
case "transform-border-label":
|
||||
return actTransformBorderLabel
|
||||
case "transform-preview-label":
|
||||
return actTransformPreviewLabel
|
||||
case "transform-header":
|
||||
return actTransformHeader
|
||||
case "transform-prompt":
|
||||
return actTransformPrompt
|
||||
case "transform-query":
|
||||
@@ -1309,18 +1402,26 @@ func parseLayout(str string) layoutType {
|
||||
return layoutDefault
|
||||
}
|
||||
|
||||
func parseInfoStyle(str string) infoStyle {
|
||||
func parseInfoStyle(str string) (infoStyle, string) {
|
||||
switch str {
|
||||
case "default":
|
||||
return infoDefault
|
||||
return infoDefault, ""
|
||||
case "right":
|
||||
return infoRight, ""
|
||||
case "inline":
|
||||
return infoInline
|
||||
return infoInline, defaultInfoSep
|
||||
case "inline-right":
|
||||
return infoInlineRight, ""
|
||||
case "hidden":
|
||||
return infoHidden
|
||||
return infoHidden, ""
|
||||
default:
|
||||
errorExit("invalid info style (expected: default|inline|hidden)")
|
||||
prefix := "inline:"
|
||||
if strings.HasPrefix(str, prefix) {
|
||||
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
|
||||
}
|
||||
errorExit("invalid info style (expected: default|right|hidden|inline[:SEPARATOR]|inline-right)")
|
||||
}
|
||||
return infoDefault
|
||||
return infoDefault, ""
|
||||
}
|
||||
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
@@ -1371,6 +1472,10 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
|
||||
opts.border = tui.BorderSharp
|
||||
case "border-bold":
|
||||
opts.border = tui.BorderBold
|
||||
case "border-block":
|
||||
opts.border = tui.BorderBlock
|
||||
case "border-thinblock":
|
||||
opts.border = tui.BorderThinBlock
|
||||
case "border-double":
|
||||
opts.border = tui.BorderDouble
|
||||
case "noborder", "border-none":
|
||||
@@ -1530,6 +1635,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Sort = optionalNumeric(allArgs, &i, 1)
|
||||
case "+s", "--no-sort":
|
||||
opts.Sort = 0
|
||||
case "--track":
|
||||
opts.Track = trackEnabled
|
||||
case "--no-track":
|
||||
opts.Track = trackDisabled
|
||||
case "--tac":
|
||||
opts.Tac = true
|
||||
case "--no-tac":
|
||||
@@ -1588,12 +1697,13 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--no-filepath-word":
|
||||
opts.FileWord = false
|
||||
case "--info":
|
||||
opts.InfoStyle = parseInfoStyle(
|
||||
opts.InfoStyle, opts.InfoSep = parseInfoStyle(
|
||||
nextString(allArgs, &i, "info style required"))
|
||||
case "--no-info":
|
||||
opts.InfoStyle = infoHidden
|
||||
case "--inline-info":
|
||||
opts.InfoStyle = infoInline
|
||||
opts.InfoSep = defaultInfoSep
|
||||
case "--no-inline-info":
|
||||
opts.InfoStyle = infoDefault
|
||||
case "--separator":
|
||||
@@ -1640,10 +1750,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--prompt":
|
||||
opts.Prompt = nextString(allArgs, &i, "prompt string required")
|
||||
case "--pointer":
|
||||
opts.Pointer = nextString(allArgs, &i, "pointer sign string required")
|
||||
opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
|
||||
validatePointer = true
|
||||
case "--marker":
|
||||
opts.Marker = nextString(allArgs, &i, "selected sign string required")
|
||||
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
|
||||
validateMarker = true
|
||||
case "--sync":
|
||||
opts.Sync = true
|
||||
@@ -1723,9 +1833,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--tabstop":
|
||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
||||
case "--listen":
|
||||
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
|
||||
port := optionalNumeric(allArgs, &i, 0)
|
||||
opts.ListenPort = &port
|
||||
case "--no-listen":
|
||||
opts.ListenPort = 0
|
||||
opts.ListenPort = nil
|
||||
case "--clear":
|
||||
opts.ClearOnExit = true
|
||||
case "--no-clear":
|
||||
@@ -1758,10 +1869,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--prompt="); match {
|
||||
opts.Prompt = value
|
||||
} else if match, value := optString(arg, "--pointer="); match {
|
||||
opts.Pointer = value
|
||||
opts.Pointer = firstLine(value)
|
||||
validatePointer = true
|
||||
} else if match, value := optString(arg, "--marker="); match {
|
||||
opts.Marker = value
|
||||
opts.Marker = firstLine(value)
|
||||
validateMarker = true
|
||||
} else if match, value := optString(arg, "-n", "--nth="); match {
|
||||
opts.Nth = splitNth(value)
|
||||
@@ -1778,7 +1889,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--layout="); match {
|
||||
opts.Layout = parseLayout(value)
|
||||
} else if match, value := optString(arg, "--info="); match {
|
||||
opts.InfoStyle = parseInfoStyle(value)
|
||||
opts.InfoStyle, opts.InfoSep = parseInfoStyle(value)
|
||||
} else if match, value := optString(arg, "--separator="); match {
|
||||
opts.Separator = &value
|
||||
} else if match, value := optString(arg, "--scrollbar="); match {
|
||||
@@ -1816,7 +1927,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--tabstop="); match {
|
||||
opts.Tabstop = atoi(value)
|
||||
} else if match, value := optString(arg, "--listen="); match {
|
||||
opts.ListenPort = atoi(value)
|
||||
port := atoi(value)
|
||||
opts.ListenPort = &port
|
||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||
opts.HscrollOff = atoi(value)
|
||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||
@@ -1846,7 +1958,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
errorExit("tab stop must be a positive integer")
|
||||
}
|
||||
|
||||
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
|
||||
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
|
||||
errorExit("invalid listen port")
|
||||
}
|
||||
|
||||
@@ -1890,8 +2002,16 @@ func postProcessOptions(opts *Options) {
|
||||
errorExit("--height option is currently not supported on this platform")
|
||||
}
|
||||
|
||||
if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 {
|
||||
errorExit("scrollbar display width should be 1")
|
||||
if opts.Scrollbar != nil {
|
||||
runes := []rune(*opts.Scrollbar)
|
||||
if len(runes) > 2 {
|
||||
errorExit("--scrollbar should be given one or two characters")
|
||||
}
|
||||
for _, r := range runes {
|
||||
if runewidth.RuneWidth(r) != 1 {
|
||||
errorExit("scrollbar display width should be 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||
@@ -1907,25 +2027,25 @@ func postProcessOptions(opts *Options) {
|
||||
// Extend the default key map
|
||||
keymap := defaultKeymap()
|
||||
for key, actions := range opts.Keymap {
|
||||
var lastChangePreviewWindow *action
|
||||
reordered := []*action{}
|
||||
for _, act := range actions {
|
||||
switch act.t {
|
||||
case actToggleSort:
|
||||
// To display "+S"/"-S" on info line
|
||||
opts.ToggleSort = true
|
||||
case actChangePreviewWindow:
|
||||
lastChangePreviewWindow = act
|
||||
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
|
||||
reordered = append(reordered, act)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-organize actions so that we only keep the last change-preview-window
|
||||
// and it comes first in the list.
|
||||
// Re-organize actions so that we put actions that change the preview window first in the list.
|
||||
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
|
||||
// -> change-preview-window(up,+20)+preview(sleep 3; cat {})
|
||||
if lastChangePreviewWindow != nil {
|
||||
reordered := []*action{lastChangePreviewWindow}
|
||||
// -> change-preview-window(up,+10)+change-preview-window(up,+20)+preview(sleep 3; cat {})
|
||||
if len(reordered) > 0 {
|
||||
for _, act := range actions {
|
||||
if act.t != actChangePreviewWindow {
|
||||
switch act.t {
|
||||
case actTogglePreview, actShowPreview, actHidePreview, actChangePreviewWindow:
|
||||
default:
|
||||
reordered = append(reordered, act)
|
||||
}
|
||||
}
|
||||
@@ -1968,9 +2088,7 @@ func postProcessOptions(opts *Options) {
|
||||
theme := opts.Theme
|
||||
boldify := func(c tui.ColorAttr) tui.ColorAttr {
|
||||
dup := c
|
||||
if !theme.Colored {
|
||||
dup.Attr |= tui.Bold
|
||||
} else if (c.Attr & tui.AttrRegular) == 0 {
|
||||
if (c.Attr & tui.AttrRegular) == 0 {
|
||||
dup.Attr |= tui.Bold
|
||||
}
|
||||
return dup
|
||||
|
@@ -2,7 +2,7 @@ package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
@@ -268,7 +268,7 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
parseKeymap(keymap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",f1:+first,f1:+top"+
|
||||
@@ -357,7 +357,7 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f, _ := os.CreateTemp("", "fzf-history")
|
||||
f.Close()
|
||||
hist := "--history=" + f.Name()
|
||||
check([]string{hist}, tui.CtrlN, actNextHistory)
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -98,8 +99,17 @@ func (r *Reader) ReadSource() {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
// The default command for *nix requires bash
|
||||
// The default command for *nix requires a shell that supports "pipefail"
|
||||
// https://unix.stackexchange.com/a/654932/62171
|
||||
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")
|
||||
if len(cmd) == 0 {
|
||||
if defaultCommand != "" {
|
||||
|
117
src/server.go
117
src/server.go
@@ -3,30 +3,69 @@ 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
|
||||
httpReadTimeout = 10 * time.Second
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
|
||||
func startHttpServer(port int, channel chan []*action) error {
|
||||
if port == 0 {
|
||||
return nil
|
||||
type httpServer struct {
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
responseChannel chan string
|
||||
}
|
||||
|
||||
func startHttpServer(port int, actionChannel chan []*action, responseChannel chan string) (error, int) {
|
||||
if port < 0 {
|
||||
return nil, port
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("port not available: %d", port)
|
||||
return fmt.Errorf("port not available: %d", port), port
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.SplitN(addr, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("cannot extract port: %s", addr), port
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return err, port
|
||||
}
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(os.Getenv("FZF_API_KEY")),
|
||||
actionChannel: actionChannel,
|
||||
responseChannel: responseChannel,
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -39,13 +78,13 @@ func startHttpServer(port int, channel chan []*action) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
conn.Write([]byte(handleHttpRequest(conn, channel)))
|
||||
conn.Write([]byte(server.handleHttpRequest(conn)))
|
||||
conn.Close()
|
||||
}
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
return nil, port
|
||||
}
|
||||
|
||||
// Here we are writing a simplistic HTTP server without using net/http
|
||||
@@ -54,12 +93,22 @@ func startHttpServer(port int, channel chan []*action) error {
|
||||
// * No --listen: 2.8MB
|
||||
// * --listen with net/http: 5.7MB
|
||||
// * --listen w/o net/http: 3.3MB
|
||||
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
contentLength := 0
|
||||
apiKey := ""
|
||||
body := ""
|
||||
bad := func(message string) string {
|
||||
answer := func(code string, message string) string {
|
||||
message += "\n"
|
||||
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||
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+"Content-Type: application/json"+crlf, message)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
@@ -80,7 +129,12 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
text := scanner.Text()
|
||||
switch section {
|
||||
case 0:
|
||||
if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
getMatch := getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) > 0 {
|
||||
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
|
||||
response := <-server.responseChannel
|
||||
return good(response)
|
||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
section++
|
||||
@@ -93,18 +147,27 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
continue
|
||||
}
|
||||
pair := strings.SplitN(text, ":", 2)
|
||||
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" {
|
||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||
if err != nil || length <= 0 || length > maxContentLength {
|
||||
return bad("invalid content length")
|
||||
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])
|
||||
}
|
||||
contentLength = length
|
||||
}
|
||||
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")
|
||||
}
|
||||
@@ -121,6 +184,28 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
|
||||
return bad("no action specified")
|
||||
}
|
||||
|
||||
channel <- actions
|
||||
server.actionChannel <- actions
|
||||
return httpOk
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
740
src/terminal.go
740
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ func (r *FullscreenRenderer) Init() {}
|
||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
func (r *FullscreenRenderer) Pause(bool) {}
|
||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
func (r *FullscreenRenderer) PassThrough(string) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
|
185
src/tui/light.go
185
src/tui/light.go
@@ -31,21 +31,32 @@ const consoleDevice string = "/dev/tty"
|
||||
var offsetRegexp *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) stderr(str string) {
|
||||
r.stderrInternal(str, true)
|
||||
func (r *LightRenderer) PassThrough(str string) {
|
||||
r.queued.WriteString(str)
|
||||
r.flush()
|
||||
}
|
||||
|
||||
// FIXME: Need better handling of non-displayable characters
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true, "")
|
||||
}
|
||||
|
||||
const CR string = "\x1b[2m␍"
|
||||
const LF string = "\x1b[2m␊"
|
||||
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
bytes := []byte(str)
|
||||
runes := []rune{}
|
||||
for len(bytes) > 0 {
|
||||
r, sz := utf8.DecodeRune(bytes)
|
||||
nlcr := r == '\n' || r == '\r'
|
||||
if r >= 32 || r == '\x1b' || nlcr {
|
||||
if r == utf8.RuneError || nlcr && !allowNLCR {
|
||||
runes = append(runes, ' ')
|
||||
} else {
|
||||
if nlcr && !allowNLCR {
|
||||
if r == '\r' {
|
||||
runes = append(runes, []rune(CR+resetCode)...)
|
||||
} else {
|
||||
runes = append(runes, []rune(LF+resetCode)...)
|
||||
}
|
||||
} else if r != utf8.RuneError {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
}
|
||||
@@ -54,8 +65,10 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
||||
r.queued.WriteString(string(runes))
|
||||
}
|
||||
|
||||
func (r *LightRenderer) csi(code string) {
|
||||
r.stderr("\x1b[" + code)
|
||||
func (r *LightRenderer) csi(code string) string {
|
||||
fullcode := "\x1b[" + code
|
||||
r.stderr(fullcode)
|
||||
return fullcode
|
||||
}
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
@@ -174,11 +187,7 @@ func (r *LightRenderer) Init() {
|
||||
}
|
||||
}
|
||||
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
r.csi("?1002h")
|
||||
r.csi("?1006h")
|
||||
}
|
||||
r.enableMouse()
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
@@ -426,7 +435,19 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case '3':
|
||||
return Event{Del, 0, nil}
|
||||
if r.buffer[3] == '~' {
|
||||
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':
|
||||
return Event{End, 0, nil}
|
||||
case '5':
|
||||
@@ -609,6 +630,7 @@ func (r *LightRenderer) rmcup() {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause(clear bool) {
|
||||
r.disableMouse()
|
||||
r.restoreTerminal()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
@@ -621,6 +643,22 @@ 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) {
|
||||
r.setupTerminal()
|
||||
if clear {
|
||||
@@ -629,14 +667,13 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
} else {
|
||||
r.rmcup()
|
||||
}
|
||||
r.enableMouse()
|
||||
r.flush()
|
||||
} else if sigcont && !r.fullscreen && r.mouse {
|
||||
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
||||
// It's highly likely that the offset we obtained at the beginning is
|
||||
// no longer correct, so we simply disable mouse input.
|
||||
r.csi("?1000l")
|
||||
r.csi("?1002l")
|
||||
r.csi("?1006l")
|
||||
r.disableMouse()
|
||||
r.mouse = false
|
||||
}
|
||||
}
|
||||
@@ -678,11 +715,7 @@ func (r *LightRenderer) Close() {
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
}
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
r.csi("?1002l")
|
||||
r.csi("?1006l")
|
||||
}
|
||||
r.disableMouse()
|
||||
r.flush()
|
||||
r.closePlatform()
|
||||
r.restoreTerminal()
|
||||
@@ -719,25 +752,38 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
|
||||
w.fg = r.theme.Fg.Color
|
||||
w.bg = r.theme.Bg.Color
|
||||
}
|
||||
w.drawBorder()
|
||||
w.drawBorder(false)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder() {
|
||||
func (w *LightWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||
switch w.border.shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
|
||||
w.drawBorderAround()
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||
w.drawBorderAround(onlyHorizontal)
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal(true, true)
|
||||
case BorderVertical:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(true, true)
|
||||
case BorderTop:
|
||||
w.drawBorderHorizontal(true, false)
|
||||
case BorderBottom:
|
||||
w.drawBorderHorizontal(false, true)
|
||||
case BorderLeft:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(true, false)
|
||||
case BorderRight:
|
||||
if onlyHorizontal {
|
||||
return
|
||||
}
|
||||
w.drawBorderVertical(false, true)
|
||||
}
|
||||
}
|
||||
@@ -747,14 +793,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.horizontal)
|
||||
hw := runewidth.RuneWidth(w.border.top)
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
|
||||
w.CPrint(color, repeat(w.border.top, w.width/hw))
|
||||
}
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width/hw))
|
||||
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,44 +816,46 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
for y := 0; y < w.height; y++ {
|
||||
w.Move(y, 0)
|
||||
if left {
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.left))
|
||||
}
|
||||
w.CPrint(color, repeat(' ', width))
|
||||
if right {
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.right))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderAround() {
|
||||
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
w.Move(0, 0)
|
||||
color := ColBorder
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.horizontal)
|
||||
vw := runewidth.RuneWidth(w.border.vertical)
|
||||
hw := runewidth.RuneWidth(w.border.top)
|
||||
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
|
||||
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
|
||||
rem := (w.width - tcw) % hw
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, repeat(' ', w.width-vw*2))
|
||||
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))
|
||||
if !onlyHorizontal {
|
||||
vw := runewidth.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)
|
||||
rem = (w.width - bcw) % hw
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+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) {
|
||||
w.renderer.csi(code)
|
||||
func (w *LightWindow) csi(code string) string {
|
||||
return w.renderer.csi(code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
|
||||
w.renderer.stderrInternal(str, allowNLCR)
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
w.renderer.stderrInternal(str, allowNLCR, resetCode)
|
||||
}
|
||||
|
||||
func (w *LightWindow) Top() int {
|
||||
@@ -913,10 +961,10 @@ func colorCodes(fg Color, bg Color) []string {
|
||||
return codes
|
||||
}
|
||||
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
|
||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||
w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0
|
||||
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0, code
|
||||
}
|
||||
|
||||
func (w *LightWindow) Print(text string) {
|
||||
@@ -928,16 +976,17 @@ func cleanse(str string) string {
|
||||
}
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
w.csi("m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
hasColors, code := w.csiColor(fg, bg, attr)
|
||||
if hasColors {
|
||||
defer w.csi("m")
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
}
|
||||
|
||||
type wrappedLine struct {
|
||||
@@ -957,6 +1006,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
if len(rs) == 1 && rs[0] == '\t' {
|
||||
w = tabstop - (prefixLength+width)%tabstop
|
||||
str = repeat(' ', w)
|
||||
} else if rs[0] == '\r' {
|
||||
w++
|
||||
} else {
|
||||
w = runewidth.StringWidth(str)
|
||||
}
|
||||
@@ -975,12 +1026,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
return lines
|
||||
}
|
||||
|
||||
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.stderrInternal(wl.text, false, resetCode)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
// Wrap line
|
||||
@@ -990,7 +1041,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
}
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
w.renderer.stderr(resetCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -999,22 +1050,26 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
return FillSuspend
|
||||
}
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
w.renderer.stderr(resetCode)
|
||||
return FillNextLine
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *LightWindow) setBg() {
|
||||
func (w *LightWindow) setBg() string {
|
||||
if w.bg != colDefault {
|
||||
w.csiColor(colDefault, w.bg, AttrRegular)
|
||||
_, code := 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 {
|
||||
w.Move(w.posy, w.posx)
|
||||
w.setBg()
|
||||
return w.fill(text, w.setBg)
|
||||
code := w.setBg()
|
||||
return w.fill(text, code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||
@@ -1025,11 +1080,11 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
||||
if bg == colDefault {
|
||||
bg = w.bg
|
||||
}
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
||||
defer w.csi("m")
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
return w.fill(text, resetCode)
|
||||
}
|
||||
return w.fill(text, w.setBg)
|
||||
return w.fill(text, w.setBg())
|
||||
}
|
||||
|
||||
func (w *LightWindow) FinishFill() {
|
||||
@@ -1040,7 +1095,7 @@ func (w *LightWindow) FinishFill() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Erase() {
|
||||
w.drawBorder()
|
||||
w.drawBorder(false)
|
||||
// We don't erase the window here to avoid flickering during scroll
|
||||
w.Move(0, 0)
|
||||
}
|
||||
|
100
src/tui/tcell.go
100
src/tui/tcell.go
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/encoding"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
@@ -97,6 +98,11 @@ const (
|
||||
AttrClear = Attr(1 << 8)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||
// No-op
|
||||
// https://github.com/gdamore/tcell/issues/363#issuecomment-680665073
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
|
||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
||||
@@ -412,6 +418,12 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyHome:
|
||||
return Event{Home, 0, nil}
|
||||
case tcell.KeyDelete:
|
||||
if ctrl {
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SDelete, 0, nil}
|
||||
}
|
||||
return Event{Del, 0, nil}
|
||||
case tcell.KeyEnd:
|
||||
return Event{End, 0, nil}
|
||||
@@ -512,7 +524,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
w.drawBorder()
|
||||
w.drawBorder(false)
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -572,26 +584,27 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
for gr.Next() {
|
||||
st := style
|
||||
rs := gr.Runes()
|
||||
|
||||
if len(rs) == 1 {
|
||||
r := rs[0]
|
||||
if r < rune(' ') { // ignore control characters
|
||||
continue
|
||||
if r == '\r' {
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
} else if r == '\n' {
|
||||
w.lastY++
|
||||
lx = 0
|
||||
continue
|
||||
} else if r == '\u000D' { // skip carriage return
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␊'
|
||||
} else if r < rune(' ') { // ignore control characters
|
||||
continue
|
||||
}
|
||||
}
|
||||
var xPos = w.left + w.lastX + lx
|
||||
var yPos = w.top + w.lastY
|
||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
}
|
||||
lx += runewidth.StringWidth(string(rs))
|
||||
lx += util.StringWidth(string(rs))
|
||||
}
|
||||
w.lastX += lx
|
||||
}
|
||||
@@ -620,13 +633,22 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
Loop:
|
||||
for gr.Next() {
|
||||
st := style
|
||||
rs := gr.Runes()
|
||||
if len(rs) == 1 && rs[0] == '\n' {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
continue
|
||||
if len(rs) == 1 {
|
||||
r := rs[0]
|
||||
switch r {
|
||||
case '\r':
|
||||
st = style.Dim(true)
|
||||
rs[0] = '␍'
|
||||
case '\n':
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
|
||||
// word wrap:
|
||||
@@ -643,8 +665,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
return FillSuspend
|
||||
}
|
||||
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
||||
lx += runewidth.StringWidth(string(rs))
|
||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||
lx += util.StringWidth(string(rs))
|
||||
}
|
||||
w.lastX += lx
|
||||
if w.lastX == w.width {
|
||||
@@ -670,7 +692,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
return w.fillString(str, NewColorPair(fg, bg, a))
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder() {
|
||||
func (w *TcellWindow) DrawHBorder() {
|
||||
w.drawBorder(true)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
shape := w.borderStyle.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
@@ -692,9 +718,9 @@ func (w *TcellWindow) drawBorder() {
|
||||
style = w.normal.style()
|
||||
}
|
||||
|
||||
hw := runewidth.RuneWidth(w.borderStyle.horizontal)
|
||||
hw := runewidth.RuneWidth(w.borderStyle.top)
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderTop {
|
||||
max = right - hw
|
||||
@@ -705,34 +731,36 @@ func (w *TcellWindow) drawBorder() {
|
||||
// ==================
|
||||
// ( HH ) => TR is ignored
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
|
||||
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
|
||||
max := right - 2*hw
|
||||
if shape == BorderHorizontal || shape == BorderBottom {
|
||||
max = right - hw
|
||||
}
|
||||
for x := left; x <= max; x += hw {
|
||||
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
|
||||
_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.RuneWidth(w.borderStyle.right)
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight:
|
||||
vw := runewidth.RuneWidth(w.borderStyle.vertical)
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderDouble:
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||
|
@@ -3,7 +3,6 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
@@ -17,13 +16,17 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
for _, prefix := range devPrefixes {
|
||||
files, err := ioutil.ReadDir(prefix)
|
||||
files, err := os.ReadDir(prefix)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
return prefix + file.Name()
|
||||
}
|
||||
}
|
||||
|
396
src/tui/tui.go
396
src/tui/tui.go
@@ -41,6 +41,7 @@ const (
|
||||
CtrlZ
|
||||
ESC
|
||||
CtrlSpace
|
||||
CtrlDelete
|
||||
|
||||
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||
CtrlBackSlash
|
||||
@@ -54,6 +55,14 @@ const (
|
||||
DoubleClick
|
||||
LeftClick
|
||||
RightClick
|
||||
SLeftClick
|
||||
SRightClick
|
||||
ScrollUp
|
||||
ScrollDown
|
||||
SScrollUp
|
||||
SScrollDown
|
||||
PreviewScrollUp
|
||||
PreviewScrollDown
|
||||
|
||||
BTab
|
||||
BSpace
|
||||
@@ -74,6 +83,7 @@ const (
|
||||
SDown
|
||||
SLeft
|
||||
SRight
|
||||
SDelete
|
||||
|
||||
F1
|
||||
F2
|
||||
@@ -92,6 +102,9 @@ const (
|
||||
BackwardEOF
|
||||
Start
|
||||
Load
|
||||
Focus
|
||||
One
|
||||
Zero
|
||||
|
||||
AltBS
|
||||
|
||||
@@ -250,29 +263,31 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
|
||||
}
|
||||
|
||||
type ColorTheme struct {
|
||||
Colored bool
|
||||
Input ColorAttr
|
||||
Disabled ColorAttr
|
||||
Fg ColorAttr
|
||||
Bg ColorAttr
|
||||
PreviewFg ColorAttr
|
||||
PreviewBg ColorAttr
|
||||
DarkBg ColorAttr
|
||||
Gutter ColorAttr
|
||||
Prompt ColorAttr
|
||||
Match ColorAttr
|
||||
Current ColorAttr
|
||||
CurrentMatch ColorAttr
|
||||
Spinner ColorAttr
|
||||
Info ColorAttr
|
||||
Cursor ColorAttr
|
||||
Selected ColorAttr
|
||||
Header ColorAttr
|
||||
Separator ColorAttr
|
||||
Scrollbar ColorAttr
|
||||
Border ColorAttr
|
||||
BorderLabel ColorAttr
|
||||
PreviewLabel ColorAttr
|
||||
Colored bool
|
||||
Input ColorAttr
|
||||
Disabled ColorAttr
|
||||
Fg ColorAttr
|
||||
Bg ColorAttr
|
||||
PreviewFg ColorAttr
|
||||
PreviewBg ColorAttr
|
||||
DarkBg ColorAttr
|
||||
Gutter ColorAttr
|
||||
Prompt ColorAttr
|
||||
Match ColorAttr
|
||||
Current ColorAttr
|
||||
CurrentMatch ColorAttr
|
||||
Spinner ColorAttr
|
||||
Info ColorAttr
|
||||
Cursor ColorAttr
|
||||
Selected ColorAttr
|
||||
Header ColorAttr
|
||||
Separator ColorAttr
|
||||
Scrollbar ColorAttr
|
||||
Border ColorAttr
|
||||
PreviewBorder ColorAttr
|
||||
PreviewScrollbar ColorAttr
|
||||
BorderLabel ColorAttr
|
||||
PreviewLabel ColorAttr
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
@@ -281,6 +296,15 @@ type Event struct {
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
func (e Event) Is(types ...EventType) bool {
|
||||
for _, t := range types {
|
||||
if e.Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
@@ -298,6 +322,8 @@ const (
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderBold
|
||||
BorderBlock
|
||||
BorderThinBlock
|
||||
BorderDouble
|
||||
BorderHorizontal
|
||||
BorderVertical
|
||||
@@ -325,8 +351,10 @@ func (s BorderShape) HasTop() bool {
|
||||
|
||||
type BorderStyle struct {
|
||||
shape BorderShape
|
||||
horizontal rune
|
||||
vertical rune
|
||||
top rune
|
||||
bottom rune
|
||||
left rune
|
||||
right rune
|
||||
topLeft rune
|
||||
topRight rune
|
||||
bottomLeft rune
|
||||
@@ -339,8 +367,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
if !unicode {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '-',
|
||||
vertical: '|',
|
||||
top: '-',
|
||||
bottom: '-',
|
||||
left: '|',
|
||||
right: '|',
|
||||
topLeft: '+',
|
||||
topRight: '+',
|
||||
bottomLeft: '+',
|
||||
@@ -351,8 +381,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
case BorderSharp:
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
top: '─',
|
||||
bottom: '─',
|
||||
left: '│',
|
||||
right: '│',
|
||||
topLeft: '┌',
|
||||
topRight: '┐',
|
||||
bottomLeft: '└',
|
||||
@@ -361,18 +393,54 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
case BorderBold:
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '━',
|
||||
vertical: '┃',
|
||||
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,
|
||||
horizontal: '═',
|
||||
vertical: '║',
|
||||
top: '═',
|
||||
bottom: '═',
|
||||
left: '║',
|
||||
right: '║',
|
||||
topLeft: '╔',
|
||||
topRight: '╗',
|
||||
bottomLeft: '╚',
|
||||
@@ -381,8 +449,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
}
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
top: '─',
|
||||
bottom: '─',
|
||||
left: '│',
|
||||
right: '│',
|
||||
topLeft: '╭',
|
||||
topRight: '╮',
|
||||
bottomLeft: '╰',
|
||||
@@ -393,8 +463,10 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
func MakeTransparentBorder() BorderStyle {
|
||||
return BorderStyle{
|
||||
shape: BorderRounded,
|
||||
horizontal: ' ',
|
||||
vertical: ' ',
|
||||
top: ' ',
|
||||
bottom: ' ',
|
||||
left: ' ',
|
||||
right: ' ',
|
||||
topLeft: ' ',
|
||||
topRight: ' ',
|
||||
bottomLeft: ' ',
|
||||
@@ -410,6 +482,7 @@ type Renderer interface {
|
||||
RefreshWindows(windows []Window)
|
||||
Refresh()
|
||||
Close()
|
||||
PassThrough(string)
|
||||
NeedScrollbarRedraw() bool
|
||||
|
||||
GetChar() Event
|
||||
@@ -426,6 +499,7 @@ type Window interface {
|
||||
Width() int
|
||||
Height() int
|
||||
|
||||
DrawHBorder()
|
||||
Refresh()
|
||||
FinishFill()
|
||||
Close()
|
||||
@@ -490,61 +564,67 @@ var (
|
||||
ColPreviewBorder ColorPair
|
||||
ColBorderLabel ColorPair
|
||||
ColPreviewLabel ColorPair
|
||||
ColPreviewScrollbar ColorPair
|
||||
ColPreviewSpinner ColorPair
|
||||
)
|
||||
|
||||
func EmptyTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: 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 {
|
||||
return &ColorTheme{
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrRegular},
|
||||
Fg: ColorAttr{colDefault, AttrRegular},
|
||||
Bg: ColorAttr{colDefault, AttrRegular},
|
||||
DarkBg: ColorAttr{colDefault, AttrRegular},
|
||||
Prompt: ColorAttr{colDefault, AttrRegular},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrRegular},
|
||||
Info: ColorAttr{colDefault, AttrRegular},
|
||||
Cursor: ColorAttr{colDefault, AttrRegular},
|
||||
Selected: ColorAttr{colDefault, AttrRegular},
|
||||
Header: ColorAttr{colDefault, AttrRegular},
|
||||
Border: ColorAttr{colDefault, AttrRegular},
|
||||
BorderLabel: ColorAttr{colDefault, AttrRegular},
|
||||
Disabled: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewFg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewBg: ColorAttr{colDefault, AttrRegular},
|
||||
Gutter: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrRegular},
|
||||
Separator: ColorAttr{colDefault, AttrRegular},
|
||||
Scrollbar: ColorAttr{colDefault, AttrRegular},
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||
Info: ColorAttr{colDefault, AttrUndefined},
|
||||
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||
Selected: ColorAttr{colDefault, AttrUndefined},
|
||||
Header: ColorAttr{colDefault, AttrUndefined},
|
||||
Border: ColorAttr{colDefault, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,79 +635,85 @@ func errorExit(message string) {
|
||||
|
||||
func init() {
|
||||
Default16 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||
Info: ColorAttr{colWhite, AttrUndefined},
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||
Info: ColorAttr{colWhite, AttrUndefined},
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: 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},
|
||||
}
|
||||
Dark256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
Current: ColorAttr{254, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{151, AttrUndefined},
|
||||
Spinner: ColorAttr{148, AttrUndefined},
|
||||
Info: ColorAttr{144, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined},
|
||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
Current: ColorAttr{254, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{151, AttrUndefined},
|
||||
Spinner: ColorAttr{148, AttrUndefined},
|
||||
Info: ColorAttr{144, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined},
|
||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: 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},
|
||||
}
|
||||
Light256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
Current: ColorAttr{237, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{23, AttrUndefined},
|
||||
Spinner: ColorAttr{65, AttrUndefined},
|
||||
Info: ColorAttr{101, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined},
|
||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
Current: ColorAttr{237, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{23, AttrUndefined},
|
||||
Spinner: ColorAttr{65, AttrUndefined},
|
||||
Info: ColorAttr{101, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined},
|
||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: 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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,8 +754,10 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
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)
|
||||
}
|
||||
@@ -707,5 +795,7 @@ func initPalette(theme *ColorTheme) {
|
||||
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
|
||||
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
|
||||
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
|
||||
ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
|
||||
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
|
||||
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
|
||||
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
|
||||
}
|
||||
|
@@ -11,6 +11,11 @@ import (
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// StringWidth returns string width where each CR/LF character takes 1 column
|
||||
func StringWidth(s string) int {
|
||||
return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
|
||||
}
|
||||
|
||||
// RunesWidth returns runes width
|
||||
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||
width := 0
|
||||
@@ -22,8 +27,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
|
||||
if len(rs) == 1 && rs[0] == '\t' {
|
||||
w = tabstop - (prefixWidth+width)%tabstop
|
||||
} else {
|
||||
s := string(rs)
|
||||
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
|
||||
w = StringWidth(string(rs))
|
||||
}
|
||||
width += w
|
||||
if width > limit {
|
||||
@@ -41,7 +45,7 @@ func Truncate(input string, limit int) ([]rune, int) {
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
w := runewidth.StringWidth(string(rs))
|
||||
w := StringWidth(string(rs))
|
||||
if width+w > limit {
|
||||
return runes, width
|
||||
}
|
||||
|
@@ -70,7 +70,7 @@ func TestMin32(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrain(t *testing.T) {
|
||||
func TestConstrain(t *testing.T) {
|
||||
if Constrain(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func TestContrain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrain32(t *testing.T) {
|
||||
func TestConstrain32(t *testing.T) {
|
||||
if Constrain32(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func TestAsUint16(t *testing.T) {
|
||||
if AsUint16(math.MinInt16) != 0 {
|
||||
t.Error("Expected", 0)
|
||||
}
|
||||
if AsUint16(math.MaxUint32) != math.MaxUint16 {
|
||||
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
|
||||
t.Error("Expected", math.MaxUint16)
|
||||
}
|
||||
}
|
||||
@@ -184,3 +184,10 @@ func TestRepeatToFill(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ExecCommand executes the given command with $SHELL
|
||||
@@ -45,3 +47,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(int(fd), b)
|
||||
}
|
||||
|
||||
func SetStdin(file *os.File) {
|
||||
unix.Dup2(int(file.Fd()), 0)
|
||||
}
|
||||
|
@@ -81,3 +81,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(syscall.Handle(fd), b)
|
||||
}
|
||||
|
||||
func SetStdin(file *os.File) {
|
||||
// No-op
|
||||
}
|
||||
|
626
test/test_go.rb
626
test/test_go.rb
@@ -8,6 +8,7 @@ require 'shellwords'
|
||||
require 'erb'
|
||||
require 'tempfile'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
TEMPLATE = DATA.read
|
||||
UNSETS = %w[
|
||||
@@ -16,6 +17,7 @@ UNSETS = %w[
|
||||
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||
FZF_ALT_C_COMMAND
|
||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
FZF_API_KEY
|
||||
fish_history
|
||||
].freeze
|
||||
DEFAULT_TIMEOUT = 10
|
||||
@@ -180,7 +182,7 @@ class TestBase < Minitest::Test
|
||||
end
|
||||
|
||||
def writelines(path, lines)
|
||||
File.unlink(path) while File.exist?(path)
|
||||
FileUtils.rm_f(path) while File.exist?(path)
|
||||
File.open(path, 'w') { |f| f.puts lines }
|
||||
end
|
||||
|
||||
@@ -188,7 +190,7 @@ class TestBase < Minitest::Test
|
||||
wait { assert_path_exists tempname }
|
||||
File.read(tempname)
|
||||
ensure
|
||||
File.unlink(tempname) while File.exist?(tempname)
|
||||
FileUtils.rm_f(tempname) while File.exist?(tempname)
|
||||
@temp_suffix += 1
|
||||
tmux.prepare
|
||||
end
|
||||
@@ -905,11 +907,7 @@ class TestGoFZF < TestBase
|
||||
history_file = '/tmp/fzf-test-history'
|
||||
|
||||
# History with limited number of entries
|
||||
begin
|
||||
File.unlink(history_file)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(history_file)
|
||||
opts = "--history=#{history_file} --history-size=4"
|
||||
input = %w[00 11 22 33 44]
|
||||
input.each do |keys|
|
||||
@@ -955,7 +953,7 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '> 33', lines[-1] }
|
||||
tmux.send_keys :Enter
|
||||
ensure
|
||||
File.unlink(history_file)
|
||||
FileUtils.rm_f(history_file)
|
||||
end
|
||||
|
||||
def test_execute
|
||||
@@ -984,11 +982,7 @@ class TestGoFZF < TestBase
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_multi
|
||||
@@ -1013,20 +1007,12 @@ class TestGoFZF < TestBase
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + '.tmp'
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
writelines(tempname, ['foo bar', '123 456'])
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
@@ -1059,21 +1045,13 @@ class TestGoFZF < TestBase
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
rescue StandardError
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
writelines(tempname,
|
||||
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
||||
system("chmod +x #{tempname}")
|
||||
@@ -1087,11 +1065,7 @@ class TestGoFZF < TestBase
|
||||
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
begin
|
||||
File.unlink(output)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_cycle
|
||||
@@ -1240,6 +1214,39 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_toggle_header
|
||||
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border", :Enter
|
||||
before = <<~OUTPUT
|
||||
╭───────
|
||||
│
|
||||
│ 4
|
||||
│ > 3
|
||||
│ 2/2
|
||||
│ >
|
||||
│ 2
|
||||
│ 1
|
||||
│ foo
|
||||
╰───────
|
||||
OUTPUT
|
||||
tmux.until { assert_block(before, _1) }
|
||||
tmux.send_keys :Space
|
||||
after = <<~OUTPUT
|
||||
╭───────
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│ 4
|
||||
│ > 3
|
||||
│ 2/2
|
||||
│ >
|
||||
╰───────
|
||||
OUTPUT
|
||||
tmux.until { assert_block(after, _1) }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { assert_block(before, _1) }
|
||||
end
|
||||
|
||||
def test_cancel
|
||||
tmux.send_keys "seq 10 | #{fzf('--bind 2:cancel')}", :Enter
|
||||
tmux.until { |lines| assert_equal ' 10/10', lines[-2] }
|
||||
@@ -1485,6 +1492,83 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
|
||||
end
|
||||
|
||||
def test_toggle_preview_without_default_preview_command
|
||||
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
refute_includes lines[1], '║ [1]'
|
||||
end
|
||||
|
||||
# toggle-preview should do nothing
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[1], '║ [1]'
|
||||
refute_includes lines[1], '║ [2]'
|
||||
end
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 3'
|
||||
refute_includes lines[1], '║ [3]'
|
||||
end
|
||||
|
||||
# One-off preview action
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
|
||||
|
||||
# toggle-preview to hide it
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
|
||||
|
||||
# toggle-preview again does nothing
|
||||
tmux.send_keys :Enter, :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 4'
|
||||
refute_includes lines[1], '║ [4]'
|
||||
end
|
||||
end
|
||||
|
||||
def test_show_and_hide_preview
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
|
||||
|
||||
# Hidden by default
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
refute_includes lines[1], '┃ [1]'
|
||||
end
|
||||
|
||||
# Show
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
|
||||
|
||||
# Already shown
|
||||
tmux.send_keys :a
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
|
||||
|
||||
# Hide
|
||||
tmux.send_keys :b
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 3'
|
||||
refute_includes lines[1], '┃ [3]'
|
||||
end
|
||||
|
||||
# Already hidden
|
||||
tmux.send_keys :b
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 4'
|
||||
refute_includes lines[1], '┃ [4]'
|
||||
end
|
||||
|
||||
# Show it again
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
|
||||
end
|
||||
|
||||
def test_preview_hidden
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
@@ -1497,11 +1581,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview_size_0
|
||||
begin
|
||||
File.unlink(tempname)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(tempname)
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
@@ -1526,6 +1606,32 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_size_0_hidden
|
||||
FileUtils.rm_f(tempname)
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 3' }
|
||||
wait { refute_path_exists tempname }
|
||||
tmux.send_keys :Space
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[3], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Down
|
||||
wait do
|
||||
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 5' }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 6' }
|
||||
tmux.send_keys :Space
|
||||
wait do
|
||||
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_flags
|
||||
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
|
||||
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
|
||||
@@ -1587,6 +1693,11 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '> 1', lines[-2] }
|
||||
end
|
||||
|
||||
def test_info_inline_separator
|
||||
tmux.send_keys 'seq 10 | fzf --info=inline:___ --no-separator', :Enter
|
||||
tmux.until { |lines| assert_equal '> ___10/10', lines[-1] }
|
||||
end
|
||||
|
||||
def test_change_first_last
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
@@ -1789,6 +1900,67 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '>', lines.last }
|
||||
end
|
||||
|
||||
def test_change_and_transform_header
|
||||
[
|
||||
'space:change-header:$(seq 4)',
|
||||
'space:transform-header:seq 4'
|
||||
].each_with_index do |binding, i|
|
||||
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :Space
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
next unless i.zero?
|
||||
|
||||
teardown
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_header
|
||||
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :Space
|
||||
expected = <<~OUTPUT
|
||||
> 3
|
||||
2
|
||||
1
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_change_query
|
||||
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||
@@ -1854,7 +2026,7 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_keep_right
|
||||
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('9999 10000') }
|
||||
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||
end
|
||||
|
||||
def test_backward_eof
|
||||
@@ -2082,11 +2254,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_accept
|
||||
@@ -2104,11 +2272,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_abort
|
||||
@@ -2129,11 +2293,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_accept
|
||||
@@ -2153,11 +2313,7 @@ class TestGoFZF < TestBase
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
FileUtils.rm_f(script)
|
||||
end
|
||||
|
||||
def test_preview_header
|
||||
@@ -2372,6 +2528,39 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate_hidden
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
|
||||
"a:change-preview-window(nohidden||down,1|)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate_hidden_down
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
end
|
||||
|
||||
def test_ellipsis
|
||||
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
@@ -2473,12 +2662,35 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_focus_event
|
||||
tmux.send_keys 'seq 100 | fzf --bind "focus:transform-prompt(echo [[{}]]),?:unbind(focus)"', :Enter
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
|
||||
tmux.send_keys :X
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
|
||||
tmux.send_keys '?'
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes(lines[-1], '[[1]]') }
|
||||
end
|
||||
|
||||
def test_labels_center
|
||||
tmux.send_keys ': | fzf --border --border-label foobar --preview : --preview-label barfoo', :Enter
|
||||
tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─foobar─')
|
||||
assert_includes(_1[1], '─barfoo─')
|
||||
end
|
||||
tmux.send_keys :space
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─foobarfoo─')
|
||||
assert_includes(_1[1], '─barfoobar─')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─fooxfoo─')
|
||||
assert_includes(_1[1], '─barxbar─')
|
||||
end
|
||||
end
|
||||
|
||||
def test_labels_left
|
||||
@@ -2530,6 +2742,16 @@ class TestGoFZF < TestBase
|
||||
tmux.until { assert(_1[-2] == ' 1/100') }
|
||||
end
|
||||
|
||||
def test_info_right
|
||||
tmux.send_keys "#{FZF} --info=right --separator x --bind 'start:reload:seq 100; sleep 10'", :Enter
|
||||
tmux.until { assert_match(%r{xxx [⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-2]) }
|
||||
end
|
||||
|
||||
def test_info_inline_right
|
||||
tmux.send_keys "#{FZF} --info=inline-right --bind 'start:reload:seq 100; sleep 10'", :Enter
|
||||
tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) }
|
||||
end
|
||||
|
||||
def test_prev_next_selected
|
||||
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
@@ -2550,11 +2772,49 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_listen
|
||||
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
|
||||
{ '--listen 6266' => -> { URI('http://localhost:6266') },
|
||||
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
|
||||
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
||||
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||
assert_equal 10, state[:totalCount]
|
||||
assert_equal 10, state[:matchCount]
|
||||
assert_empty state[:query]
|
||||
assert_equal({ index: 0, text: '1' }, state[:current])
|
||||
|
||||
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||
assert_equal 100, state[:totalCount]
|
||||
assert_equal 0, state[:matchCount]
|
||||
assert_equal 'yo', state[:query]
|
||||
assert_nil state[:current]
|
||||
|
||||
teardown
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_listen_with_api_key
|
||||
post_uri = URI('http://localhost:6266')
|
||||
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
# Incorrect API Key
|
||||
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
|
||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||
assert_equal '401', res.code
|
||||
assert_equal 'Unauthorized', res.message
|
||||
assert_equal "invalid api key\n", res.body
|
||||
end
|
||||
# Valid API Key
|
||||
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
|
||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||
assert_equal '200', res.code
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_toggle_alternative_preview_window
|
||||
@@ -2564,6 +2824,202 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||
end
|
||||
|
||||
def test_become
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 99, lines.item_count }
|
||||
end
|
||||
|
||||
def test_no_extra_newline_issue_3209
|
||||
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
|
||||
expected = <<~OUTPUT
|
||||
╭──────────
|
||||
│ ─────────
|
||||
│ something
|
||||
│
|
||||
╰──────────
|
||||
3
|
||||
2
|
||||
> 1
|
||||
100/100 ─
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_track
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
index = tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end.index('> 555')
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_equal '> 555', lines[index]
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_equal '> 555', lines[index]
|
||||
end
|
||||
tmux.send_keys '555'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys 't'
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys 't'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 1000, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_action
|
||||
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 555'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
end
|
||||
|
||||
# Automatically disabled when the tracking item is no longer visible
|
||||
tmux.send_keys '4'
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 271, lines.match_count
|
||||
assert_includes lines, '> 5'
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
end
|
||||
end
|
||||
|
||||
def test_one_and_zero
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
||||
tmux.send_keys '1'
|
||||
tmux.until do |lines|
|
||||
assert_equal 2, lines.match_count
|
||||
refute(lines.any? { _1.include?('only match') })
|
||||
refute(lines.any? { _1.include?('no match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert(lines.any? { _1.include?('only match') })
|
||||
end
|
||||
tmux.send_keys '0'
|
||||
tmux.until do |lines|
|
||||
assert_equal 0, lines.match_count
|
||||
assert(lines.any? { _1.include?('no match') })
|
||||
end
|
||||
end
|
||||
|
||||
def test_height_range_with_exit_0
|
||||
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
tmux.send_keys :c
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_and_change
|
||||
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_and_change_cache
|
||||
tmux.send_keys "echo bar | #{FZF} --bind 'zero:change-header(foo)+reload(echo foo)+clear-query'", :Enter
|
||||
expected = <<~OUTPUT
|
||||
> bar
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
tmux.send_keys :z
|
||||
expected = <<~OUTPUT
|
||||
> foo
|
||||
foo
|
||||
1/1
|
||||
>
|
||||
OUTPUT
|
||||
tmux.until { assert_block(expected, _1) }
|
||||
end
|
||||
|
||||
def test_delete_with_modifiers
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-delete:up+up,shift-delete:down,focus:transform-prompt:echo [{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 'C-Delete'
|
||||
tmux.until { |lines| assert_equal '[3]', lines[-1] }
|
||||
tmux.send_keys 'S-Delete'
|
||||
tmux.until { |lines| assert_equal '[2]', lines[-1] }
|
||||
end
|
||||
|
||||
def test_become_tty
|
||||
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '/dev/tty' }
|
||||
end
|
||||
|
||||
def test_disabled_preview_update
|
||||
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
|
||||
tmux.send_keys :x
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -2692,9 +3148,9 @@ module TestShell
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
tmux.send_keys 'foo bar'
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?('bar"') }
|
||||
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') }
|
||||
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
end
|
||||
@@ -2909,6 +3365,34 @@ module CompletionTest
|
||||
tmux.prepare
|
||||
tmux.send_keys 'unset -f _fzf_comprun', :Enter
|
||||
end
|
||||
|
||||
def test_ssh_completion
|
||||
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
|
||||
|
||||
tmux.send_keys 'ssh jg@localhost**', :Tab
|
||||
tmux.until do |lines|
|
||||
assert lines.match_count >= 1
|
||||
end
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
|
||||
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
|
||||
tmux.until do |lines|
|
||||
assert lines.match_count >= 5
|
||||
assert_equal 0, lines.select_count
|
||||
end
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.select_count
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
|
||||
|
||||
tmux.send_keys 'localhost**', :Tab
|
||||
tmux.until do |lines|
|
||||
assert lines.match_count >= 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestBash < TestBase
|
||||
|
9
typos.toml
Normal file
9
typos.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
# 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"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["README.md"]
|
Reference in New Issue
Block a user