mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-21 07:23:49 -07:00
Compare commits
166 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d226d841a1 | ||
|
c6d83047e5 | ||
|
46dabccdf1 | ||
|
cd9517b679 | ||
|
cd6677ba1d | ||
|
9c1a47acf7 | ||
|
0c280a3ce1 | ||
|
53e8b6e705 | ||
|
ad33165fa7 | ||
|
2055db61c8 | ||
|
d2c662e54f | ||
|
d24b58ef3f | ||
|
06ae9b0f3b | ||
|
2a9c1c06a4 | ||
|
90ad1b7f22 | ||
|
f22fbcd1af | ||
|
1d761684c5 | ||
|
e491770f1c | ||
|
a41be61506 | ||
|
1a8f633611 | ||
|
af8fe918d8 | ||
|
8ef9dfd9a2 | ||
|
66df24040f | ||
|
ed4442d9ea | ||
|
0edb5d5ebb | ||
|
9ffc2c7ca3 | ||
|
93cb3758b5 | ||
|
d22e75dcdd | ||
|
a1b2a6fe2c | ||
|
e15cba0c8c | ||
|
31fd207ba2 | ||
|
ba6d1b8772 | ||
|
0dce561ec9 | ||
|
376142eb0d | ||
|
664ee1f483 | ||
|
dac5b6fde1 | ||
|
998c57442b | ||
|
4a0ab6c926 | ||
|
f43e82f17f | ||
|
62238620a5 | ||
|
200745011a | ||
|
82fd88339b | ||
|
de0f2efbfb | ||
|
29cf28d845 | ||
|
7e4dbb5f3b | ||
|
923c3a814d | ||
|
779e3cc5b5 | ||
|
3f3d1ef8f5 | ||
|
f92f9f137a | ||
|
87f7f436e8 | ||
|
4298c0b1eb | ||
|
6c104d771e | ||
|
aefb9a5bc4 | ||
|
8868d7cbb8 | ||
|
10cbac20f9 | ||
|
26bcd0c90d | ||
|
fbece2bb67 | ||
|
0012183ede | ||
|
8916cbc6ab | ||
|
21ce70054f | ||
|
3ba82b6d87 | ||
|
e771c5d057 | ||
|
4e5e925e39 | ||
|
b7248d4115 | ||
|
639253840f | ||
|
710ebdf9c1 | ||
|
bb64d84ce4 | ||
|
cd1da27ff2 | ||
|
c1accc2e5b | ||
|
e4489dcbc1 | ||
|
c0d407f7ce | ||
|
461115afde | ||
|
bae1965231 | ||
|
b89c77ec9a | ||
|
1ca5f09d7b | ||
|
d79902ae59 | ||
|
77568e114f | ||
|
a24d274a3c | ||
|
dac81432d6 | ||
|
309b5081ef | ||
|
91bc4f2671 | ||
|
4c9d37d919 | ||
|
7e9566f66a | ||
|
3f7e8a475d | ||
|
1cf7c0f334 | ||
|
ff8ee9ee4e | ||
|
cbbd939a94 | ||
|
f232df2887 | ||
|
16bfb2c80c | ||
|
0ba066123e | ||
|
81c51c26cc | ||
|
6fa8295ac5 | ||
|
f975b40236 | ||
|
01d9d9c8c8 | ||
|
1eafc4e5d9 | ||
|
38e4020aa8 | ||
|
ac32fbb3b2 | ||
|
7d26eca5cc | ||
|
3347d61591 | ||
|
9abf2c8c9c | ||
|
84e2262ad6 | ||
|
378137d34a | ||
|
66ca16f836 | ||
|
282884ad83 | ||
|
7877ac42f0 | ||
|
19ef8891e3 | ||
|
bfea9e53a6 | ||
|
a2420026ab | ||
|
1be1991299 | ||
|
67dd7e1923 | ||
|
2b584586ed | ||
|
a1994ff0ab | ||
|
ca0e858871 | ||
|
06c6615507 | ||
|
818d0be436 | ||
|
fcd2baa945 | ||
|
62e0a2824a | ||
|
bbe1721a18 | ||
|
c1470a51b8 | ||
|
6ee31d5dc5 | ||
|
65d74387e7 | ||
|
7d0ea599c4 | ||
|
b7795a3dea | ||
|
323f6f6202 | ||
|
0c61223884 | ||
|
32234be7a2 | ||
|
178b49832e | ||
|
18cbb4a84d | ||
|
e84afe196a | ||
|
e1e171a3c4 | ||
|
d075c00015 | ||
|
6c0ca4a64a | ||
|
6b5d461411 | ||
|
7419e0dde1 | ||
|
cf2bb5e40e | ||
|
f466e94d65 | ||
|
eb0257d48f | ||
|
b83dd6c6b4 | ||
|
51c207448d | ||
|
a6a558da30 | ||
|
2bf5fa27be | ||
|
af7940746f | ||
|
a2aa1a156c | ||
|
2f8a72a42a | ||
|
8179ca5eaa | ||
|
4b74f882c7 | ||
|
7cf45af502 | ||
|
46c21158d8 | ||
|
80da0776f8 | ||
|
e91f10ab16 | ||
|
2c15cd7923 | ||
|
d6584543e9 | ||
|
c13228f346 | ||
|
7220d8233e | ||
|
0237bf09bf | ||
|
04017c25bb | ||
|
02199cd609 | ||
|
26b9f5831a | ||
|
243a76002c | ||
|
c71e4ddee4 | ||
|
32eb8c1be9 | ||
|
c587017830 | ||
|
fb885652cc | ||
|
afc2f05e5e | ||
|
06547d0cbe | ||
|
578108280e |
10
.github/workflows/linux.yml
vendored
10
.github/workflows/linux.yml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -30,19 +30,19 @@ jobs:
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.0
|
||||
ruby-version: 3.4.1
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
|
||||
run: bundle install
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
run: make lint
|
||||
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.28.4
|
||||
- uses: crate-ci/typos@v1.29.4
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,6 @@ bin/fzf.exe
|
||||
dist
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
@@ -12,3 +11,4 @@ gopath
|
||||
fzf
|
||||
tmp
|
||||
*.patch
|
||||
.idea
|
||||
|
10
.rubocop.yml
10
.rubocop.yml
@@ -1,9 +1,13 @@
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Metrics:
|
||||
Enabled: false
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
AllowedMethods:
|
||||
@@ -28,5 +32,11 @@ Style/WordArray:
|
||||
MinSize: 1
|
||||
Minitest/AssertEqual:
|
||||
Enabled: false
|
||||
Minitest/EmptyLineBeforeAssertionMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Enabled: false
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
|
@@ -1 +1,2 @@
|
||||
golang 1.20.13
|
||||
ruby 3.4.1
|
||||
|
48
ADVANCED.md
48
ADVANCED.md
@@ -1,8 +1,8 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2024/06/24*
|
||||
* *Requires fzf 0.54.0 or later*
|
||||
* *Last update: 2025/02/02*
|
||||
* *Requires fzf 0.59.0 or later*
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +22,7 @@ Advanced fzf examples
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
@@ -92,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
||||
|
||||

|
||||
|
||||
*(See `Layout` section of the man page to see the full list of options)*
|
||||
*(See man page to see the full list of options)*
|
||||
|
||||
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||
@@ -500,6 +501,44 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
### Controlling Ripgrep search and fzf search simultaneously
|
||||
|
||||
`search` and `transform-search` action allow you to trigger an fzf search with
|
||||
an arbitrary query string. This frees fzf from strictly following the prompt
|
||||
input, enabling custom search syntax.
|
||||
|
||||
In the example below, `transform` action is used to conditionally trigger
|
||||
`reload` for ripgrep, followed by `search` for fzf. The first word of the
|
||||
query initiates the Ripgrep process to generate the initial results, while the
|
||||
remainder of the query is passed to fzf for secondary filtering.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
INITIAL_QUERY="${*:-}"
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
@@ -529,8 +568,7 @@ pods() {
|
||||
--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 'start,ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
|
219
CHANGELOG.md
219
CHANGELOG.md
@@ -1,6 +1,223 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.62.0
|
||||
------
|
||||
- Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read
|
||||
```sh
|
||||
# seoul256-light
|
||||
fzf --style full --color='
|
||||
fg:#616161 fg+:#616161
|
||||
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
|
||||
hl:#719872 hl+:#719899
|
||||
pointer:#e12672 marker:#e17899
|
||||
header:#719872
|
||||
spinner:#719899 info:#727100
|
||||
prompt:#0099bd query:#616161
|
||||
border:#e1e1e1
|
||||
'
|
||||
```
|
||||
- Added `alt-bg` color to create striped lines to visually separate rows
|
||||
```sh
|
||||
fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
|
||||
|
||||
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --reverse --multi \
|
||||
--color bg:237,alt-bg:238,current-bg:236 --highlight-line
|
||||
```
|
||||
- [fish] Improvements in CTRL-R binding (@bitraid)
|
||||
- You can trigger CTRL-R in the middle of a command to insert the selected item
|
||||
- You can delete history items with SHIFT-DEL
|
||||
- Bug fixes and improvements
|
||||
- Fixed unnecessary 100ms delay after `reload` (#4364)
|
||||
- Fixed `selected-bg` not applied to colored items (#4372)
|
||||
|
||||
0.61.3
|
||||
------
|
||||
- Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
|
||||
- More environment variables for child processes (#4356)
|
||||
|
||||
0.61.2
|
||||
------
|
||||
- Fixed panic when using header border without pointer/marker (@phanen)
|
||||
- Fixed `--tmux` option when already inside a tmux popup (@peikk0)
|
||||
- Bug fixes and improvements in CTRL-T binding of fish (#4334) (@bitraid)
|
||||
- Added `--no-tty-default` option to make fzf search for the current TTY device instead of defaulting to `/dev/tty` (#4242)
|
||||
|
||||
0.61.1
|
||||
------
|
||||
- Disable bracketed-paste mode on exit. This fixes issue where pasting breaks after running fzf on old bash versions that don't support the mode.
|
||||
|
||||
0.61.0
|
||||
------
|
||||
- Added `--ghost=TEXT` to display a ghost text when the input is empty
|
||||
```sh
|
||||
# Display "Type to search" when the input is empty
|
||||
fzf --ghost "Type to search"
|
||||
```
|
||||
- Added `change-ghost` and `transform-ghost` actions for dynamically changing the ghost text
|
||||
- Added `change-pointer` and `transform-pointer` actions for dynamically changing the pointer sign
|
||||
- Added `r` flag for placeholder expression (raw mode) for unquoted output
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.60.3
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- [fish] Enable multiple history commands insertion (#4280) (@bitraid)
|
||||
- [walker] Append '/' to directory entries on MSYS2 (#4281)
|
||||
- Trim trailing whitespaces after processing ANSI sequences (#4282)
|
||||
- Remove temp files before `become` when using `--tmux` option (#4283)
|
||||
- Fix condition for using item numlines cache (#4285) (@alex-huff)
|
||||
- Make `--accept-nth` compatible with `--select-1` (#4287)
|
||||
- Increase the query length limit from 300 to 1000 (#4292)
|
||||
- [windows] Prevent fzf from consuming user input while paused (#4260)
|
||||
|
||||
0.60.2
|
||||
------
|
||||
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
|
||||
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
|
||||
- Thanks to @phanen for the fix
|
||||
- Fixed 'jump' action when the pointer is an empty string
|
||||
|
||||
0.60.1
|
||||
------
|
||||
- Bug fixes and minor improvements
|
||||
- Built-in walker now prints directory entries with a trailing slash
|
||||
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
|
||||
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
|
||||
|
||||
0.60.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
|
||||
|
||||
- Added `--accept-nth` for choosing output fields
|
||||
```sh
|
||||
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
|
||||
# Becomes
|
||||
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
|
||||
|
||||
git branch | fzf | cut -c3-
|
||||
# Can be rewritten as
|
||||
git branch | fzf --accept-nth -1
|
||||
```
|
||||
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
|
||||
```sh
|
||||
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
|
||||
# foo, baz, bar
|
||||
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
|
||||
# foo,baz,bar,foo,bar
|
||||
```
|
||||
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
|
||||
```sh
|
||||
seq 100 | fzf --bind 'ctrl-x:exclude'
|
||||
|
||||
# 'exclude-multi' will exclude the selected items or the current item
|
||||
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
|
||||
```
|
||||
- Preview window now prints wrap indicator when wrapping is enabled
|
||||
```sh
|
||||
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.59.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
|
||||
|
||||
- Prioritizing file name matches (#4192)
|
||||
- Added a new tiebreak option `pathname` for prioritizing file name matches
|
||||
- `--scheme=path` now sets `--tiebreak=pathname,length`
|
||||
- fzf will automatically choose `path` scheme
|
||||
* when the input is a TTY device, where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND` which is usually a command for listing files,
|
||||
* but not when `reload` or `transform` action is bound to `start` event, because in that case, fzf can't be sure of the input type.
|
||||
- Added `--header-lines-border` to display header from `--header-lines` with a separate border
|
||||
```sh
|
||||
# Use --header-lines-border to separate two headers
|
||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
|
||||
--header-lines-border bottom --no-list-border
|
||||
```
|
||||
- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
|
||||
```sh
|
||||
# Click on the header line to limit search scope
|
||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||
--header-lines-border bottom --no-list-border \
|
||||
--color fg:dim,nth:regular \
|
||||
--bind 'click-header:transform-nth(
|
||||
echo $FZF_CLICK_HEADER_NTH
|
||||
)+transform-prompt(
|
||||
echo "$FZF_CLICK_HEADER_WORD> "
|
||||
)'
|
||||
```
|
||||
- `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
|
||||
- `kill` completion for bash and zsh were updated to use this feature
|
||||
- Added `--no-input` option to completely disable and hide the input section
|
||||
```sh
|
||||
# Click header to trigger search
|
||||
fzf --header '[src] [test]' --no-input --layout reverse \
|
||||
--header-border bottom --input-border \
|
||||
--bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
|
||||
|
||||
# Vim-like mode switch
|
||||
fzf --layout reverse-list --no-input \
|
||||
--bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
|
||||
--bind 'enter,esc,ctrl-c:transform:
|
||||
if [[ $FZF_INPUT_STATE = enabled ]]; then
|
||||
echo "rebind(j,k,/)+hide-input"
|
||||
elif [[ $FZF_KEY = enter ]]; then
|
||||
echo accept
|
||||
else
|
||||
echo abort
|
||||
fi
|
||||
'
|
||||
```
|
||||
- You can later show the input section using `show-input` or `toggle-input` action, and hide it again using `hide-input`, or `toggle-input`.
|
||||
- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
|
||||
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
|
||||
```sh
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER"
|
||||
```
|
||||
- You can now bind actions to multiple keys and events at once by writing a comma-separated list of keys and events before the colon
|
||||
```sh
|
||||
# Load 'ps -ef' output on start and reload it on CTRL-R
|
||||
fzf --bind 'start,ctrl-r:reload:ps -ef'
|
||||
```
|
||||
- `--min-height` option now takes a number followed by `+`, which tells fzf to show at least that many items in the list section. The default value is now changed to `10+`.
|
||||
```sh
|
||||
# You will only see the input section which takes 3 lines
|
||||
fzf --style=full --height 1% --min-height 3
|
||||
|
||||
# You will see 3 items in the list section
|
||||
fzf --style full --height 1% --min-height 3+
|
||||
```
|
||||
- Shell integration scripts were updated to use `--min-height 20+` by default
|
||||
- `--header-lines` will be displayed at the top in `reverse-list` layout
|
||||
- Added `bell` action to ring the terminal bell
|
||||
```sh
|
||||
# Press CTRL-Y to copy the current line to the clipboard and ring the bell
|
||||
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
|
||||
```
|
||||
- Added `toggle-bind` action
|
||||
- Bug fixes and improvements
|
||||
- Fixed fish script to support fish 3.1.2 or later (@bitraid)
|
||||
|
||||
0.58.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
||||
@@ -269,7 +486,7 @@ _Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
|
||||
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
|
||||
```sh
|
||||
# Now this will work as expected. Previously, this would print an invalid header line.
|
||||
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||
# `load` event would fire and the header would be prematurely updated.
|
||||
fzf --header 'Loading ...' --header-lines 1 \
|
||||
--bind 'start:reload:sleep 1; ps -ef' \
|
||||
|
@@ -1,5 +1,5 @@
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
|
||||
FROM rubylang/ruby:3.4.1-noble
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
|
||||
RUN gem install --no-document -v 5.22.3 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
@@ -9,4 +9,4 @@ RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
ENV LANG=C.UTF-8
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
8
Gemfile
Normal file
8
Gemfile
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'minitest', '5.25.4'
|
||||
gem 'rubocop', '1.71.0'
|
||||
gem 'rubocop-minitest', '0.36.0'
|
||||
gem 'rubocop-performance', '1.23.1'
|
47
Gemfile.lock
Normal file
47
Gemfile.lock
Normal file
@@ -0,0 +1,47 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.9.1)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.25.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.71.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.36.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-performance (1.23.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-23
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (= 5.25.4)
|
||||
rubocop (= 1.71.0)
|
||||
rubocop-minitest (= 0.36.0)
|
||||
rubocop-performance (= 1.23.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
9
Makefile
9
Makefile
@@ -82,12 +82,15 @@ test: $(SOURCES)
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
itest:
|
||||
ruby test/runner.rb
|
||||
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
lint: $(SOURCES) test/test_go.rb
|
||||
lint: $(SOURCES) test/*.rb test/lib/*.rb
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
rubocop --require rubocop-minitest --require rubocop-performance
|
||||
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
@@ -186,4 +189,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all generate build release test bench lint install clean docker docker-test update
|
||||
.PHONY: all generate build release test itest bench lint install clean docker docker-test update
|
||||
|
@@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ 'query': ['fg', 'Normal'],
|
||||
\ 'hl': ['fg', 'Comment'],
|
||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||
|
@@ -57,15 +57,15 @@ elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stt
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. Use icat (from Kitty) if kitten is installed
|
||||
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
|
8
go.mod
8
go.mod
@@ -1,13 +1,13 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.9
|
||||
github.com/charlievieth/fastwalk v1.0.10
|
||||
github.com/gdamore/tcell/v2 v2.8.1
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/term v0.29.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
14
go.sum
14
go.sum
@@ -1,12 +1,12 @@
|
||||
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0=
|
||||
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/charlievieth/fastwalk v1.0.10 h1:0qUbvA2O+K+X+IrTfZTC0UH2DK5MOA+KjVfStAHUnGg=
|
||||
github.com/charlievieth/fastwalk v1.0.10/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -54,8 +54,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -64,8 +65,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
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=
|
||||
|
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.58.0
|
||||
version=0.62.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.58.0"
|
||||
$version="0.62.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.58"
|
||||
var version = "0.62"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
@@ -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 2025" "fzf 0.58.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "May 2025" "fzf 0.62.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
180
man/man1/fzf.1
180
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 2025" "fzf 0.58.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "May 2025" "fzf 0.62.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -56,7 +56,9 @@ Case-insensitive match (default: smart-case match)
|
||||
Case-sensitive match
|
||||
.TP
|
||||
.B "\-\-smart\-case"
|
||||
Smart-case match (default)
|
||||
Smart-case match (default). In this mode, the search is case-insensitive by
|
||||
default, but it becomes case-sensitive if the query contains any uppercase
|
||||
letters.
|
||||
.TP
|
||||
.B "\-\-literal"
|
||||
Do not normalize latin script letters for matching.
|
||||
@@ -76,7 +78,8 @@ Generic scoring scheme designed to work well with any type of input.
|
||||
.RS
|
||||
Additional bonus point is only given to the characters after path separator.
|
||||
You might want to choose this scheme over \fBdefault\fR if you have many files
|
||||
with spaces in their paths.
|
||||
with spaces in their paths. This also sets \fB\-\-tiebreak=pathname,length\fR,
|
||||
to prioritize matches occurring in the tail element of a file path.
|
||||
.RE
|
||||
.RE
|
||||
|
||||
@@ -90,6 +93,13 @@ more weight to the chronological ordering. This also sets
|
||||
.RE
|
||||
.RE
|
||||
|
||||
.RS
|
||||
fzf chooses \fBpath\fR scheme when the input is a TTY device, where fzf would
|
||||
start its built-in walker or run \fB$FZF_DEFAULT_COMMAND\fR, and there is no
|
||||
\fBreload\fR or \fBtransform\fR action bound to \fBstart\fR event. Otherwise,
|
||||
it chooses \fBdefault\fR scheme.
|
||||
.RE
|
||||
|
||||
.TP
|
||||
.BI "\-\-algo=" TYPE
|
||||
Fuzzy matching algorithm (default: v2)
|
||||
@@ -109,8 +119,38 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
|
||||
the original lines) because fzf doesn't allow searching against the hidden
|
||||
fields.
|
||||
.TP
|
||||
.BI "\-\-with\-nth=" "N[,..]"
|
||||
Transform the presentation of each line using field index expressions
|
||||
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
|
||||
Transform the presentation of each line using the field index expressions.
|
||||
For advanced transformation, you can provide a template containing field index
|
||||
expressions in curly braces. When you use a template, the trailing delimiter is
|
||||
stripped from each expression, giving you more control over the output.
|
||||
\fB{n}\fR in template evaluates to the zero-based ordinal index of the line.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
# Single expression: drop the first field
|
||||
echo foo bar baz | fzf --with-nth 2..
|
||||
|
||||
# Use template to rearrange fields
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
||||
Define which fields to print on accept. The last delimiter is stripped from the
|
||||
output. For advanced transformation, you can provide a template containing
|
||||
field index expressions in curly braces. When you use a template, the trailing
|
||||
delimiter is stripped from each expression, giving you more control over the
|
||||
output. \fB{n}\fR in template evaluates to the zero-based ordinal index of the
|
||||
line.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
# Single expression
|
||||
echo foo bar baz | fzf --accept-nth 2
|
||||
|
||||
# Template
|
||||
echo foo bar baz | fzf --accept-nth 'Index: {n}, 1st: {1}, 2nd: {2}, 3rd: {3}'
|
||||
.RE
|
||||
.TP
|
||||
.B "+s, \-\-no\-sort"
|
||||
Do not sort the result
|
||||
@@ -140,15 +180,17 @@ Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
.br
|
||||
|
||||
.br
|
||||
.BR length " Prefers line with shorter length"
|
||||
.BR length " Prefers line with shorter length"
|
||||
.br
|
||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||
.br
|
||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||
.BR pathname " Prefers line with matched substring in the file name of the path"
|
||||
.br
|
||||
.BR end " Prefers line with matched substring closer to the end"
|
||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||
.br
|
||||
.BR index " Prefers line that appeared earlier in the input stream"
|
||||
.BR end " Prefers line with matched substring closer to the end"
|
||||
.br
|
||||
.BR index " Prefers line that appeared earlier in the input stream"
|
||||
.br
|
||||
|
||||
.br
|
||||
@@ -186,6 +228,13 @@ e.g. \fB# Avoid rendering both fzf instances at the same time
|
||||
(sleep 1; seq 1000000; sleep 1) |
|
||||
fzf \-\-sync \-\-query 5 \-\-listen \-\-bind start:up,load:up,result:up,focus:change\-header:Ready\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-no\-tty\-default"
|
||||
Make fzf search for the current TTY device via standard error instead of
|
||||
defaulting to \fB/dev/tty\fR. This option avoids issues when launching
|
||||
emacsclient from within fzf. Alternatively, you can change the default TTY
|
||||
device by setting \fB--tty-default=DEVICE_NAME\fR.
|
||||
|
||||
.SS GLOBAL STYLE
|
||||
.TP
|
||||
.BI "\-\-style=" "PRESET"
|
||||
@@ -193,7 +242,7 @@ Apply a style preset [default|minimal|full[:BORDER_STYLE]]
|
||||
.TP
|
||||
.BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
|
||||
Color configuration. The name of the base color scheme is followed by custom
|
||||
color mappings.
|
||||
color mappings. Each entry is separated by a comma and/or whitespaces.
|
||||
|
||||
.RS
|
||||
.B BASE SCHEME:
|
||||
@@ -221,6 +270,7 @@ color mappings.
|
||||
\fBcurrent\-bg (bg+) \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
|
||||
\fBalt\-bg \fRAlternate background color to create striped lines
|
||||
\fBquery (input\-fg) \fRQuery string
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB\-\-disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
@@ -288,7 +338,19 @@ color mappings.
|
||||
# Seoul256 theme with 24-bit colors
|
||||
fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
|
||||
\-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
|
||||
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
|
||||
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'
|
||||
|
||||
# Seoul256 light theme with 24-bit colors, each entry separated by whitespaces
|
||||
fzf \-\-style full \-\-color='
|
||||
fg:#616161 fg+:#616161
|
||||
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
|
||||
hl:#719872 hl+:#719899
|
||||
pointer:#e12672 marker:#e17899
|
||||
header:#719872
|
||||
spinner:#719899 info:#727100
|
||||
prompt:#0099bd query:#616161
|
||||
border:#e1e1e1
|
||||
'\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-no\-color"
|
||||
@@ -326,9 +388,12 @@ Adaptive height has the following limitations:
|
||||
* It will not find the right size when there are multi-line items
|
||||
|
||||
.TP
|
||||
.BI "\-\-min\-height=" "HEIGHT"
|
||||
Minimum height when \fB\-\-height\fR is given in percent (default: 10).
|
||||
Ignored when \fB\-\-height\fR is not specified.
|
||||
.BI "\-\-min\-height=" "HEIGHT[+]"
|
||||
Minimum height when \fB\-\-height\fR is given as a percentage.
|
||||
Add \fB+\fR to automatically increase the value according to the other
|
||||
layout options so that the specified number of items are visible in the list
|
||||
section (default: \fB10+\fR).
|
||||
Ignored when \fB\-\-height\fR is not specified or set as an absolute value.
|
||||
.TP
|
||||
.BI "\-\-tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
|
||||
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
|
||||
@@ -607,6 +672,13 @@ Position of the list label
|
||||
|
||||
.SS INPUT SECTION
|
||||
|
||||
.TP
|
||||
.B "\-\-no\-input"
|
||||
Disable and hide the input section. You can no longer type in queries. To
|
||||
trigger a search, use \fBsearch\fR action. You can later show the input section
|
||||
using \fBshow\-input\fR or \fBtoggle\-input\fR action, and hide it again using
|
||||
\fBhide\-input\fR, or \fBtoggle\-input\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-prompt=" "STR"
|
||||
Input prompt (default: '> ')
|
||||
@@ -657,6 +729,10 @@ ANSI color codes are supported.
|
||||
Do not display horizontal separator on the info line. A synonym for
|
||||
\fB\-\-separator=''\fB
|
||||
|
||||
.TP
|
||||
.BI "\-\-ghost=" "TEXT"
|
||||
Ghost text to display when the input is empty
|
||||
|
||||
.TP
|
||||
.B "\-\-filepath\-word"
|
||||
Make word-wise movements and actions respect path separators. The following
|
||||
@@ -712,6 +788,12 @@ e.g.
|
||||
\fBfzf \-\-multi \-\-preview='head \-10 {+}'
|
||||
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
|
||||
|
||||
Each expression expands to a quoted string, so that it's safe to pass it as an
|
||||
argument to an external command. So you should not manually add quotes around
|
||||
the curly braces. But if you don't want this behavior, you can put
|
||||
\fBr\fR flag (raw) in the expression (e.g. \fB{r}\fR, \fB{r1}\fR, etc).
|
||||
Use it with caution as unquoted output can lead to broken commands.
|
||||
|
||||
When using a field index expression, leading and trailing whitespace is stripped
|
||||
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
|
||||
|
||||
@@ -730,6 +812,8 @@ Also,
|
||||
|
||||
* \fB{q}\fR is replaced to the current query string
|
||||
.br
|
||||
* \fB{q}\fR can contain field index expressions. e.g. \fB{q:1}\fR, \fB{q:2..}\fR, etc.
|
||||
.br
|
||||
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
|
||||
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
||||
.br
|
||||
@@ -929,6 +1013,12 @@ Label to print on the header border
|
||||
.BI "\-\-header\-label\-pos" [=N[:top|bottom]]
|
||||
Position of the header label
|
||||
|
||||
.TP
|
||||
.BI "\-\-header\-lines\-border" [=STYLE]
|
||||
Display header from \fB--header\-lines\fR with a separate border. Pass
|
||||
\fBnone\fR to still separate the header lines but without a border. To combine
|
||||
two headers, use \fB\-\-no\-header\-lines\-border\fR.
|
||||
|
||||
.SS SCRIPTING
|
||||
.TP
|
||||
.BI "\-q, \-\-query=" "STR"
|
||||
@@ -1122,9 +1212,9 @@ Show man page
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.TP
|
||||
.B FZF_DEFAULT_COMMAND
|
||||
Default command to use when input is tty. 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.
|
||||
Default command to use when input is a TTY device. 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.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
Default options.
|
||||
@@ -1196,14 +1286,26 @@ fzf exports the following environment variables to its child processes.
|
||||
.br
|
||||
.BR FZF_QUERY " Current query string"
|
||||
.br
|
||||
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
|
||||
.br
|
||||
.BR FZF_NTH " Current \-\-nth option"
|
||||
.br
|
||||
.BR FZF_PROMPT " Prompt string"
|
||||
.br
|
||||
.BR FZF_GHOST " Ghost string"
|
||||
.br
|
||||
.BR FZF_POINTER " Pointer string"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LABEL " Preview label string"
|
||||
.br
|
||||
.BR FZF_BORDER_LABEL " Border label string"
|
||||
.br
|
||||
.BR FZF_LIST_LABEL " List label string"
|
||||
.br
|
||||
.BR FZF_INPUT_LABEL " Input label string"
|
||||
.br
|
||||
.BR FZF_HEADER_LABEL " Header label string"
|
||||
.br
|
||||
.BR FZF_ACTION " The name of the last action performed"
|
||||
.br
|
||||
.BR FZF_KEY " The name of the last key pressed"
|
||||
@@ -1275,10 +1377,15 @@ more \fBactions\fR. You can use it to customize key bindings or implement
|
||||
dynamic behaviors.
|
||||
|
||||
\fB\-\-bind\fR takes a comma-separated list of binding expressions. Each binding
|
||||
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
|
||||
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. You can bind actions to
|
||||
multiple keys and events by writing comma-separated list of keys and events
|
||||
before the colon. e.g. \fBKEY1,KEY2,EVENT1,EVENT2:ACTION\fR.
|
||||
|
||||
e.g.
|
||||
\fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line\fR
|
||||
\fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line
|
||||
|
||||
# Load 'ps \-ef' output on start and reload it on CTRL\-R
|
||||
fzf \-\-bind 'start,ctrl\-r:reload:ps \-ef'\fR
|
||||
|
||||
.SS AVAILABLE KEYS: (SYNONYMS)
|
||||
\fIctrl\-[a\-z]\fR
|
||||
@@ -1503,11 +1610,21 @@ e.g.
|
||||
|
||||
\fIclick\-header\fR
|
||||
.RS
|
||||
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
|
||||
Triggered when a mouse click occurs within the header. Sets
|
||||
\fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment
|
||||
variables starting from 1. It optionally sets \fBFZF_CLICK_HEADER_WORD\fR and
|
||||
\fBFZF_CLICK_HEADER_NTH\fR if clicked on a word.
|
||||
|
||||
e.g.
|
||||
\fBprintf "head1\\nhead2" | fzf \-\-header\-lines=2 \-\-bind 'click\-header:transform\-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR
|
||||
|
||||
\fB# Click on the header line to limit search scope
|
||||
ps \-ef | fzf \-\-style full \-\-layout reverse \-\-header\-lines 1 \\
|
||||
\-\-header\-lines\-border bottom \-\-no\-list\-border \\
|
||||
\-\-color fg:dim,nth:regular \\
|
||||
\-\-bind 'click\-header:transform\-nth(
|
||||
echo $FZF_CLICK_HEADER_NTH
|
||||
)+transform\-prompt(
|
||||
echo "$FZF_CLICK_HEADER_WORD> "
|
||||
)'\fR
|
||||
.RE
|
||||
|
||||
.SS AVAILABLE ACTIONS:
|
||||
@@ -1525,8 +1642,10 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\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
|
||||
\fBbell\fR (ring the terminal bell)
|
||||
\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\-ghost(...)\fR (change ghost text to the given string)
|
||||
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
|
||||
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
|
||||
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
|
||||
@@ -1534,6 +1653,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBchange\-multi\fR (enable multi-select mode with no limit)
|
||||
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
|
||||
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
|
||||
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
|
||||
\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 '|')
|
||||
@@ -1551,6 +1671,8 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||
\fBenable\-search\fR (enable search functionality)
|
||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
||||
\fBexclude\fR (exclude the current item from the result)
|
||||
\fBexclude\-multi\fR (exclude the selected items or the current item from the result)
|
||||
\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)
|
||||
@@ -1568,6 +1690,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBhalf\-page\-down\fR
|
||||
\fBhalf\-page\-up\fR
|
||||
\fBhide\-header\fR
|
||||
\fBhide\-input\fR
|
||||
\fBhide\-preview\fR
|
||||
\fBoffset\-down\fR (similar to CTRL\-E of Vim)
|
||||
\fBoffset\-up\fR (similar to CTRL\-Y of Vim)
|
||||
@@ -1592,16 +1715,20 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\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)
|
||||
\fBsearch(...)\fR (trigger fzf search with the given string)
|
||||
\fBselect\fR
|
||||
\fBselect\-all\fR (select all matches)
|
||||
\fBshow\-header\fR
|
||||
\fBshow\-input\fR
|
||||
\fBshow\-preview\fR
|
||||
\fBtoggle\fR (\fIright\-click\fR)
|
||||
\fBtoggle\-all\fR (toggle all matches)
|
||||
\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\-bind\fR
|
||||
\fBtoggle\-header\fR
|
||||
\fBtoggle\-hscroll\fR
|
||||
\fBtoggle\-input\fR
|
||||
\fBtoggle\-multi\-line\fR
|
||||
\fBtoggle\-preview\fR
|
||||
\fBtoggle\-preview\-wrap\fR
|
||||
@@ -1615,13 +1742,17 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
|
||||
\fBtransform\-header(...)\fR (transform header using an external command)
|
||||
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
|
||||
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
|
||||
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
||||
\fBtransform\-nth(...)\fR (transform nth using an external command)
|
||||
\fBtransform\-pointer(...)\fR (transform pointer 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)
|
||||
\fBtransform\-search(...)\fR (trigger fzf search with the output of an external command)
|
||||
\fBunbind(...)\fR (unbind bindings)
|
||||
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
||||
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
||||
@@ -1637,6 +1768,9 @@ e.g.
|
||||
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
|
||||
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
|
||||
|
||||
Any action after a terminal action that exits fzf, such as \fBaccept\fR or
|
||||
\fBabort\fR, is ignored.
|
||||
|
||||
.SS ACTION ARGUMENT
|
||||
|
||||
An action denoted with \fB(...)\fR suffix takes an argument.
|
||||
|
@@ -358,7 +358,7 @@ endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
@@ -1081,7 +1081,7 @@ endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
|
@@ -37,7 +37,7 @@ bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
@@ -311,12 +311,12 @@ __fzf_generic_path_completion() {
|
||||
else
|
||||
if [[ $1 =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||
fi
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||
fi | while read -r item; do
|
||||
printf "%q " "${item%$3}$3"
|
||||
done
|
||||
@@ -409,7 +409,33 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
|
@@ -99,9 +99,9 @@ if [[ -o interactive ]]; then
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
echo -E "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_comprun() {
|
||||
@@ -290,7 +290,33 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
|
@@ -20,7 +20,7 @@ if [[ $- =~ i ]]; then
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
@@ -14,101 +14,33 @@
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
|
||||
# command substitution syntax $(cmd) should never be used, even behind a version
|
||||
# check, otherwise the source command will fail on fish versions older than 3.4.0.
|
||||
function fzf_key_bindings
|
||||
|
||||
# Check fish version
|
||||
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
|
||||
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
|
||||
echo "This script requires fish version 3.1b1 or newer." >&2
|
||||
return 1
|
||||
else if not type -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
function __fzf_defaults
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
|
||||
test -r "$FZF_DEFAULT_OPTS_FILE"; and string collect -N -- <$FZF_DEFAULT_OPTS_FILE
|
||||
echo $FZF_DEFAULT_OPTS $argv[2]
|
||||
end
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
set -l result
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_CTRL_T_OPTS")
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set result (eval (__fzfcmd) -m --query=$fzf_query)
|
||||
end
|
||||
if test -z "$result"
|
||||
commandline -f repaint
|
||||
return
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape -- $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
# merge history from other sessions before searching
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line +m $FZF_CTRL_R_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set -lx FZF_DEFAULT_COMMAND
|
||||
string match -q -r -- '/fish$' $SHELL; or set -lx SHELL (type -p fish)
|
||||
if type -q perl
|
||||
set -a FZF_DEFAULT_OPTS '--tac'
|
||||
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
||||
else
|
||||
set FZF_DEFAULT_COMMAND \
|
||||
'set -l h (builtin history -z --reverse | string split0);' \
|
||||
'for i in (seq (count $h) -1 1);' \
|
||||
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
||||
'end'
|
||||
end
|
||||
set -l result (eval "$FZF_DEFAULT_COMMAND | $(__fzfcmd) --read0 --print0 -q (commandline) --bind='enter:become:string replace -a -- \n\t \n {2..} | string collect'")
|
||||
and commandline -- $result
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_ALT_C_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
set -l result (eval (__fzfcmd) +m --query=$fzf_query)
|
||||
|
||||
if test -n "$result"
|
||||
cd -- $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
commandline -it -- $prefix
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
@@ -118,82 +50,181 @@ function fzf_key_bindings
|
||||
end
|
||||
end
|
||||
|
||||
bind \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind \ct fzf-file-widget
|
||||
end
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
bind -M insert \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l commandline (commandline -t)
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
# strip -option= from token if present
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
# Set variables containing the major and minor fish version numbers, using
|
||||
# a method compatible with all supported fish versions.
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
# Enable home directory expansion of leading ~/
|
||||
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
|
||||
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
# escape special characters, except for the $ sign of valid variable names,
|
||||
# so that after eval, the original string is returned, but with the
|
||||
# variable names replaced by their values.
|
||||
set commandline (string escape -n -- $commandline)
|
||||
set commandline (string replace -r -a -- '\x5c\$(?=[\w])' '\$' $commandline)
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
eval set commandline $commandline
|
||||
|
||||
# Combine multiple consecutive slashes into one
|
||||
set commandline (string replace -r -a -- '/+' '/' $commandline)
|
||||
|
||||
if test -z "$commandline"
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
# BUG: on combined expressions, if a left argument is a single `!`, the
|
||||
# builtin test command of fish will treat it as the ! operator. To
|
||||
# overcome this, have the variable parts on the right.
|
||||
if test "." = "$dir" -a "./" != (string sub -l 2 -- $commandline)
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
if test -n "$fzf_query"
|
||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
# fish v3.5.0 and newer
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r -- "^$dir/?" '' $commandline)
|
||||
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.4.1
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish v3.1b1 - v3.1.2
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
echo (string escape -- $dir)
|
||||
echo (string escape -- $fzf_query)
|
||||
echo $prefix
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# Strip trailing slash, unless $dir is root dir (/)
|
||||
set dir (string replace -r -- '(?<!^)/$' '' $dir)
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||
"$FZF_CTRL_T_OPTS --multi --print0")
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while test ! -d "$dir"
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname -- "$dir")
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
set -l -- command_line (commandline)
|
||||
set -l -- current_line (commandline -L)
|
||||
set -l -- total_lines (count $command_line)
|
||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
|
||||
'--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \
|
||||
"--bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \
|
||||
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND
|
||||
|
||||
if type -q perl
|
||||
set -a FZF_DEFAULT_OPTS '--tac'
|
||||
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
||||
else
|
||||
set FZF_DEFAULT_COMMAND \
|
||||
'set -l h (builtin history -z --reverse | string split0);' \
|
||||
'for i in (seq (count $h) -1 1);' \
|
||||
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
||||
'end'
|
||||
end
|
||||
|
||||
echo $dir
|
||||
# Merge history from other sessions before searching
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
|
||||
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
||||
if test "$total_lines" -eq 1
|
||||
commandline -- (string replace -a -- \n\t \n $result)
|
||||
else
|
||||
set -l a (math $current_line - 1)
|
||||
set -l b (math $current_line + 1)
|
||||
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
|
||||
commandline -a -- '' $command_line[$b..-1]
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=dir,follow,hidden --scheme=path" \
|
||||
"$FZF_ALT_C_OPTS --no-multi --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
|
||||
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
|
||||
cd -- $result
|
||||
commandline -rt -- $prefix
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
bind \cr fzf-history-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind \ct fzf-file-widget
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -41,9 +41,9 @@ if [[ -o interactive ]]; then
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
echo -E "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
|
@@ -12,129 +12,145 @@ func _() {
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actChar-4]
|
||||
_ = x[actMouse-5]
|
||||
_ = x[actBeginningOfLine-6]
|
||||
_ = x[actAbort-7]
|
||||
_ = x[actAccept-8]
|
||||
_ = x[actAcceptNonEmpty-9]
|
||||
_ = x[actAcceptOrPrintQuery-10]
|
||||
_ = x[actBackwardChar-11]
|
||||
_ = x[actBackwardDeleteChar-12]
|
||||
_ = x[actBackwardDeleteCharEof-13]
|
||||
_ = x[actBackwardWord-14]
|
||||
_ = x[actCancel-15]
|
||||
_ = x[actChangeBorderLabel-16]
|
||||
_ = x[actChangeListLabel-17]
|
||||
_ = x[actChangeInputLabel-18]
|
||||
_ = x[actChangeHeader-19]
|
||||
_ = x[actChangeHeaderLabel-20]
|
||||
_ = x[actChangeMulti-21]
|
||||
_ = x[actChangePreviewLabel-22]
|
||||
_ = x[actChangePrompt-23]
|
||||
_ = x[actChangeQuery-24]
|
||||
_ = x[actBracketedPasteBegin-4]
|
||||
_ = x[actBracketedPasteEnd-5]
|
||||
_ = x[actChar-6]
|
||||
_ = x[actMouse-7]
|
||||
_ = x[actBeginningOfLine-8]
|
||||
_ = x[actAbort-9]
|
||||
_ = x[actAccept-10]
|
||||
_ = x[actAcceptNonEmpty-11]
|
||||
_ = x[actAcceptOrPrintQuery-12]
|
||||
_ = x[actBackwardChar-13]
|
||||
_ = x[actBackwardDeleteChar-14]
|
||||
_ = x[actBackwardDeleteCharEof-15]
|
||||
_ = x[actBackwardWord-16]
|
||||
_ = x[actCancel-17]
|
||||
_ = x[actChangeBorderLabel-18]
|
||||
_ = x[actChangeGhost-19]
|
||||
_ = x[actChangeHeader-20]
|
||||
_ = x[actChangeHeaderLabel-21]
|
||||
_ = x[actChangeInputLabel-22]
|
||||
_ = x[actChangeListLabel-23]
|
||||
_ = x[actChangeMulti-24]
|
||||
_ = x[actChangeNth-25]
|
||||
_ = x[actClearScreen-26]
|
||||
_ = x[actClearQuery-27]
|
||||
_ = x[actClearSelection-28]
|
||||
_ = x[actClose-29]
|
||||
_ = x[actDeleteChar-30]
|
||||
_ = x[actDeleteCharEof-31]
|
||||
_ = x[actEndOfLine-32]
|
||||
_ = x[actFatal-33]
|
||||
_ = x[actForwardChar-34]
|
||||
_ = x[actForwardWord-35]
|
||||
_ = x[actKillLine-36]
|
||||
_ = x[actKillWord-37]
|
||||
_ = x[actUnixLineDiscard-38]
|
||||
_ = x[actUnixWordRubout-39]
|
||||
_ = x[actYank-40]
|
||||
_ = x[actBackwardKillWord-41]
|
||||
_ = x[actSelectAll-42]
|
||||
_ = x[actDeselectAll-43]
|
||||
_ = x[actToggle-44]
|
||||
_ = x[actToggleSearch-45]
|
||||
_ = x[actToggleAll-46]
|
||||
_ = x[actToggleDown-47]
|
||||
_ = x[actToggleUp-48]
|
||||
_ = x[actToggleIn-49]
|
||||
_ = x[actToggleOut-50]
|
||||
_ = x[actToggleTrack-51]
|
||||
_ = x[actToggleTrackCurrent-52]
|
||||
_ = x[actToggleHeader-53]
|
||||
_ = x[actToggleWrap-54]
|
||||
_ = x[actToggleMultiLine-55]
|
||||
_ = x[actToggleHscroll-56]
|
||||
_ = x[actTrackCurrent-57]
|
||||
_ = x[actUntrackCurrent-58]
|
||||
_ = x[actDown-59]
|
||||
_ = x[actUp-60]
|
||||
_ = x[actPageUp-61]
|
||||
_ = x[actPageDown-62]
|
||||
_ = x[actPosition-63]
|
||||
_ = x[actHalfPageUp-64]
|
||||
_ = x[actHalfPageDown-65]
|
||||
_ = x[actOffsetUp-66]
|
||||
_ = x[actOffsetDown-67]
|
||||
_ = x[actOffsetMiddle-68]
|
||||
_ = x[actJump-69]
|
||||
_ = x[actJumpAccept-70]
|
||||
_ = x[actPrintQuery-71]
|
||||
_ = x[actRefreshPreview-72]
|
||||
_ = x[actReplaceQuery-73]
|
||||
_ = x[actToggleSort-74]
|
||||
_ = x[actShowPreview-75]
|
||||
_ = x[actHidePreview-76]
|
||||
_ = x[actTogglePreview-77]
|
||||
_ = x[actTogglePreviewWrap-78]
|
||||
_ = x[actTransform-79]
|
||||
_ = x[actTransformBorderLabel-80]
|
||||
_ = x[actTransformListLabel-81]
|
||||
_ = x[actTransformInputLabel-82]
|
||||
_ = x[actTransformHeader-83]
|
||||
_ = x[actTransformHeaderLabel-84]
|
||||
_ = x[actTransformPreviewLabel-85]
|
||||
_ = x[actTransformPrompt-86]
|
||||
_ = x[actTransformQuery-87]
|
||||
_ = x[actPreview-88]
|
||||
_ = x[actChangePreview-89]
|
||||
_ = x[actChangePreviewWindow-90]
|
||||
_ = x[actPreviewTop-91]
|
||||
_ = x[actPreviewBottom-92]
|
||||
_ = x[actPreviewUp-93]
|
||||
_ = x[actPreviewDown-94]
|
||||
_ = x[actPreviewPageUp-95]
|
||||
_ = x[actPreviewPageDown-96]
|
||||
_ = x[actPreviewHalfPageUp-97]
|
||||
_ = x[actPreviewHalfPageDown-98]
|
||||
_ = x[actPrevHistory-99]
|
||||
_ = x[actPrevSelected-100]
|
||||
_ = x[actPrint-101]
|
||||
_ = x[actPut-102]
|
||||
_ = x[actNextHistory-103]
|
||||
_ = x[actNextSelected-104]
|
||||
_ = x[actExecute-105]
|
||||
_ = x[actExecuteSilent-106]
|
||||
_ = x[actExecuteMulti-107]
|
||||
_ = x[actSigStop-108]
|
||||
_ = x[actFirst-109]
|
||||
_ = x[actLast-110]
|
||||
_ = x[actReload-111]
|
||||
_ = x[actReloadSync-112]
|
||||
_ = x[actDisableSearch-113]
|
||||
_ = x[actEnableSearch-114]
|
||||
_ = x[actSelect-115]
|
||||
_ = x[actDeselect-116]
|
||||
_ = x[actUnbind-117]
|
||||
_ = x[actRebind-118]
|
||||
_ = x[actBecome-119]
|
||||
_ = x[actShowHeader-120]
|
||||
_ = x[actHideHeader-121]
|
||||
_ = x[actChangePointer-26]
|
||||
_ = x[actChangePreview-27]
|
||||
_ = x[actChangePreviewLabel-28]
|
||||
_ = x[actChangePreviewWindow-29]
|
||||
_ = x[actChangePrompt-30]
|
||||
_ = x[actChangeQuery-31]
|
||||
_ = x[actClearScreen-32]
|
||||
_ = x[actClearQuery-33]
|
||||
_ = x[actClearSelection-34]
|
||||
_ = x[actClose-35]
|
||||
_ = x[actDeleteChar-36]
|
||||
_ = x[actDeleteCharEof-37]
|
||||
_ = x[actEndOfLine-38]
|
||||
_ = x[actFatal-39]
|
||||
_ = x[actForwardChar-40]
|
||||
_ = x[actForwardWord-41]
|
||||
_ = x[actKillLine-42]
|
||||
_ = x[actKillWord-43]
|
||||
_ = x[actUnixLineDiscard-44]
|
||||
_ = x[actUnixWordRubout-45]
|
||||
_ = x[actYank-46]
|
||||
_ = x[actBackwardKillWord-47]
|
||||
_ = x[actSelectAll-48]
|
||||
_ = x[actDeselectAll-49]
|
||||
_ = x[actToggle-50]
|
||||
_ = x[actToggleSearch-51]
|
||||
_ = x[actToggleAll-52]
|
||||
_ = x[actToggleDown-53]
|
||||
_ = x[actToggleUp-54]
|
||||
_ = x[actToggleIn-55]
|
||||
_ = x[actToggleOut-56]
|
||||
_ = x[actToggleTrack-57]
|
||||
_ = x[actToggleTrackCurrent-58]
|
||||
_ = x[actToggleHeader-59]
|
||||
_ = x[actToggleWrap-60]
|
||||
_ = x[actToggleMultiLine-61]
|
||||
_ = x[actToggleHscroll-62]
|
||||
_ = x[actTrackCurrent-63]
|
||||
_ = x[actToggleInput-64]
|
||||
_ = x[actHideInput-65]
|
||||
_ = x[actShowInput-66]
|
||||
_ = x[actUntrackCurrent-67]
|
||||
_ = x[actDown-68]
|
||||
_ = x[actUp-69]
|
||||
_ = x[actPageUp-70]
|
||||
_ = x[actPageDown-71]
|
||||
_ = x[actPosition-72]
|
||||
_ = x[actHalfPageUp-73]
|
||||
_ = x[actHalfPageDown-74]
|
||||
_ = x[actOffsetUp-75]
|
||||
_ = x[actOffsetDown-76]
|
||||
_ = x[actOffsetMiddle-77]
|
||||
_ = x[actJump-78]
|
||||
_ = x[actJumpAccept-79]
|
||||
_ = x[actPrintQuery-80]
|
||||
_ = x[actRefreshPreview-81]
|
||||
_ = x[actReplaceQuery-82]
|
||||
_ = x[actToggleSort-83]
|
||||
_ = x[actShowPreview-84]
|
||||
_ = x[actHidePreview-85]
|
||||
_ = x[actTogglePreview-86]
|
||||
_ = x[actTogglePreviewWrap-87]
|
||||
_ = x[actTransform-88]
|
||||
_ = x[actTransformBorderLabel-89]
|
||||
_ = x[actTransformGhost-90]
|
||||
_ = x[actTransformHeader-91]
|
||||
_ = x[actTransformHeaderLabel-92]
|
||||
_ = x[actTransformInputLabel-93]
|
||||
_ = x[actTransformListLabel-94]
|
||||
_ = x[actTransformNth-95]
|
||||
_ = x[actTransformPointer-96]
|
||||
_ = x[actTransformPreviewLabel-97]
|
||||
_ = x[actTransformPrompt-98]
|
||||
_ = x[actTransformQuery-99]
|
||||
_ = x[actTransformSearch-100]
|
||||
_ = x[actSearch-101]
|
||||
_ = x[actPreview-102]
|
||||
_ = x[actPreviewTop-103]
|
||||
_ = x[actPreviewBottom-104]
|
||||
_ = x[actPreviewUp-105]
|
||||
_ = x[actPreviewDown-106]
|
||||
_ = x[actPreviewPageUp-107]
|
||||
_ = x[actPreviewPageDown-108]
|
||||
_ = x[actPreviewHalfPageUp-109]
|
||||
_ = x[actPreviewHalfPageDown-110]
|
||||
_ = x[actPrevHistory-111]
|
||||
_ = x[actPrevSelected-112]
|
||||
_ = x[actPrint-113]
|
||||
_ = x[actPut-114]
|
||||
_ = x[actNextHistory-115]
|
||||
_ = x[actNextSelected-116]
|
||||
_ = x[actExecute-117]
|
||||
_ = x[actExecuteSilent-118]
|
||||
_ = x[actExecuteMulti-119]
|
||||
_ = x[actSigStop-120]
|
||||
_ = x[actFirst-121]
|
||||
_ = x[actLast-122]
|
||||
_ = x[actReload-123]
|
||||
_ = x[actReloadSync-124]
|
||||
_ = x[actDisableSearch-125]
|
||||
_ = x[actEnableSearch-126]
|
||||
_ = x[actSelect-127]
|
||||
_ = x[actDeselect-128]
|
||||
_ = x[actUnbind-129]
|
||||
_ = x[actRebind-130]
|
||||
_ = x[actToggleBind-131]
|
||||
_ = x[actBecome-132]
|
||||
_ = x[actShowHeader-133]
|
||||
_ = x[actHideHeader-134]
|
||||
_ = x[actBell-135]
|
||||
_ = x[actExclude-136]
|
||||
_ = x[actExcludeMulti-137]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1220, 1238, 1255, 1265, 1281, 1303, 1316, 1332, 1344, 1358, 1374, 1392, 1412, 1434, 1448, 1463, 1471, 1477, 1491, 1506, 1516, 1532, 1547, 1557, 1565, 1572, 1581, 1594, 1610, 1625, 1634, 1645, 1654, 1663, 1672, 1685, 1698}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 318, 337, 355, 369, 381, 397, 413, 434, 456, 471, 485, 499, 512, 529, 537, 550, 566, 578, 586, 600, 614, 625, 636, 654, 671, 678, 697, 709, 723, 732, 747, 759, 772, 783, 794, 806, 820, 841, 856, 869, 887, 903, 918, 932, 944, 956, 973, 980, 985, 994, 1005, 1016, 1029, 1044, 1055, 1068, 1083, 1090, 1103, 1116, 1133, 1148, 1161, 1175, 1189, 1205, 1225, 1237, 1260, 1277, 1295, 1318, 1340, 1361, 1376, 1395, 1419, 1437, 1454, 1472, 1481, 1491, 1504, 1520, 1532, 1546, 1562, 1580, 1600, 1622, 1636, 1651, 1659, 1665, 1679, 1694, 1704, 1720, 1735, 1745, 1753, 1760, 1769, 1782, 1798, 1813, 1822, 1833, 1842, 1851, 1864, 1873, 1886, 1899, 1906, 1916, 1931}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
@@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
|
@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
|
||||
bytes[math.MaxUint16] = 'z'
|
||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
|
||||
func TestLongStringWithNormalize(t *testing.T) {
|
||||
bytes := make([]byte, 30000)
|
||||
for i := range bytes {
|
||||
bytes[i] = 'x'
|
||||
}
|
||||
unicodeString := string(bytes) + " Minímal example"
|
||||
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
|
||||
}
|
||||
|
15
src/ansi.go
15
src/ansi.go
@@ -358,12 +358,17 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
state.lbg = prevState.bg
|
||||
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
|
||||
state.url = nil
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
|
||||
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
|
||||
stLen := 2
|
||||
if strings.HasSuffix(ansiCode, "\a") {
|
||||
stLen = 1
|
||||
}
|
||||
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
|
||||
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
|
||||
state.url = nil
|
||||
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
params := ansiCode[4 : 4+paramsEnd]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
|
||||
state.url = &url{uri: uri, params: params}
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@ const (
|
||||
previewCancelWait = 500 * time.Millisecond
|
||||
previewChunkDelay = 100 * time.Millisecond
|
||||
previewDelayed = 500 * time.Millisecond
|
||||
maxPatternLength = 300
|
||||
maxPatternLength = 1000
|
||||
maxMulti = math.MaxInt32
|
||||
|
||||
// Matcher
|
||||
|
72
src/core.go
72
src/core.go
@@ -96,7 +96,7 @@ func Run(opts *Options) (int, error) {
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
if opts.WithNth == nil {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, byteString(data))
|
||||
@@ -109,6 +109,7 @@ func Run(opts *Options) (int, error) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
@@ -127,8 +128,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
transformed := nthTransformer(tokens, itemIndex)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
@@ -188,19 +188,37 @@ func Run(opts *Options) (int, error) {
|
||||
forward = false
|
||||
case byBegin:
|
||||
forward = true
|
||||
case byPathname:
|
||||
withPos = true
|
||||
forward = false
|
||||
}
|
||||
}
|
||||
|
||||
nth := opts.Nth
|
||||
nthRevision := 0
|
||||
patternCache := make(map[string]*Pattern)
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, nthRevision, runes)
|
||||
}
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
denyMutex.Lock()
|
||||
denylistCopy := make(map[int32]struct{})
|
||||
for k, v := range denylist {
|
||||
denylistCopy[k] = v
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
||||
}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
@@ -277,6 +295,7 @@ func Run(opts *Options) (int, error) {
|
||||
// Event coordination
|
||||
reading := true
|
||||
ticks := 0
|
||||
startTick := 0
|
||||
var nextCommand *commandSpec
|
||||
var nextEnviron []string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
@@ -299,7 +318,11 @@ func Run(opts *Options) (int, error) {
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
startTick = ticks
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
@@ -345,7 +368,8 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
@@ -376,10 +400,21 @@ func Run(opts *Options) (int, error) {
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
nthRevision++
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
@@ -444,8 +479,17 @@ func Run(opts *Options) (int, error) {
|
||||
if len(opts.Expect) > 0 {
|
||||
opts.Printer("")
|
||||
}
|
||||
transformer := func(item *Item) string {
|
||||
return item.AsString(opts.Ansi)
|
||||
}
|
||||
if opts.AcceptNth != nil {
|
||||
fn := opts.AcceptNth(opts.Delimiter)
|
||||
transformer = func(item *Item) string {
|
||||
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
||||
}
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
opts.Printer(transformer(val.Get(i).item))
|
||||
}
|
||||
if count == 0 {
|
||||
exitCode = ExitNoMatch
|
||||
@@ -467,7 +511,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
if delay && reading {
|
||||
dur := util.DurWithin(
|
||||
time.Duration(ticks)*coordinatorDelayStep,
|
||||
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||
0, coordinatorDelayMax)
|
||||
time.Sleep(dur)
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
type transformed struct {
|
||||
// Because nth can be changed dynamically by change-nth action, we need to
|
||||
// keep the revision number at the time of transformation.
|
||||
revision int
|
||||
revision revision
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
@@ -51,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
|
||||
}
|
||||
return item.text.ToString()
|
||||
}
|
||||
|
||||
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
transformed := transformer(tokens, item.Index())
|
||||
return StripLastDelimiter(transformed, delimiter)
|
||||
}
|
||||
|
368
src/options.go
368
src/options.go
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/junegunn/go-shellwords"
|
||||
"github.com/rivo/uniseg"
|
||||
@@ -40,14 +41,15 @@ Usage: fzf [options]
|
||||
integer or a range expression ([BEGIN]..[END]).
|
||||
--with-nth=N[,..] Transform the presentation of each line using
|
||||
field index expressions
|
||||
--accept-nth=N[,..] Define which fields to print on accept
|
||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||
+s, --no-sort Do not sort the result
|
||||
--literal Do not normalize latin script letters
|
||||
--tail=NUM Maximum number of items to keep in memory
|
||||
--disabled Do not perform search
|
||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||
when the scores are tied [length|chunk|begin|end|index]
|
||||
(default: length)
|
||||
when the scores are tied
|
||||
[length|chunk|pathname|begin|end|index] (default: length)
|
||||
|
||||
INPUT/OUTPUT
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
@@ -68,8 +70,9 @@ Usage: fzf [options]
|
||||
minus the given value.
|
||||
If prefixed with '~', fzf will determine the height
|
||||
according to the input size.
|
||||
--min-height=HEIGHT Minimum height for percent --height is given in percent
|
||||
(default: 10)
|
||||
--min-height=HEIGHT[+] Minimum height when --height is given as a percentage.
|
||||
Add '+' to automatically increase the value
|
||||
according to the other layout options (default: 10+).
|
||||
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
|
||||
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
[,border-native] (default: center,50%)
|
||||
@@ -125,6 +128,7 @@ Usage: fzf [options]
|
||||
(default: 0 or center)
|
||||
|
||||
INPUT SECTION
|
||||
--no-input Disable and hide the input section
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
--info=STYLE Finder info style
|
||||
[default|right|hidden|inline[-right][:PREFIX]]
|
||||
@@ -132,6 +136,7 @@ Usage: fzf [options]
|
||||
--separator=STR Draw horizontal separator on info line using the string
|
||||
(default: '─' or '-')
|
||||
--no-separator Hide info line separator
|
||||
--ghost=TEXT Ghost text to display when the input is empty
|
||||
--filepath-word Make word-wise movements respect path separators
|
||||
--input-border[=STYLE] Draw border around the input section
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
@@ -164,6 +169,9 @@ Usage: fzf [options]
|
||||
--header-border[=STYLE] Draw border around the header section
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
top|bottom|left|right|none] (default: rounded)
|
||||
--header-lines-border[=STYLE]
|
||||
Display header from --header-lines with a separate border.
|
||||
Pass 'none' to still separate it but without a border.
|
||||
--header-label=LABEL Label to print on the header border
|
||||
--header-label-pos=COL Position of the header label
|
||||
[POSITIVE_INTEGER: columns from left|
|
||||
@@ -238,6 +246,7 @@ const (
|
||||
byLength
|
||||
byBegin
|
||||
byEnd
|
||||
byPathname
|
||||
)
|
||||
|
||||
type heightSpec struct {
|
||||
@@ -532,10 +541,12 @@ type Options struct {
|
||||
Scheme string
|
||||
Extended bool
|
||||
Phony bool
|
||||
Inputless bool
|
||||
Case Case
|
||||
Normalize bool
|
||||
Nth []Range
|
||||
WithNth []Range
|
||||
WithNth func(Delimiter) func([]Token, int32) string
|
||||
AcceptNth func(Delimiter) func([]Token, int32) string
|
||||
Delimiter Delimiter
|
||||
Sort int
|
||||
Track trackOption
|
||||
@@ -564,6 +575,7 @@ type Options struct {
|
||||
InfoStyle infoStyle
|
||||
InfoPrefix string
|
||||
InfoCommand string
|
||||
Ghost string
|
||||
Separator *string
|
||||
JumpLabels string
|
||||
Prompt string
|
||||
@@ -597,6 +609,7 @@ type Options struct {
|
||||
ListBorderShape tui.BorderShape
|
||||
InputBorderShape tui.BorderShape
|
||||
HeaderBorderShape tui.BorderShape
|
||||
HeaderLinesShape tui.BorderShape
|
||||
InputLabel labelOpts
|
||||
HeaderLabel labelOpts
|
||||
BorderLabel labelOpts
|
||||
@@ -618,6 +631,7 @@ type Options struct {
|
||||
MEMProfile string
|
||||
BlockProfile string
|
||||
MutexProfile string
|
||||
TtyDefault string
|
||||
}
|
||||
|
||||
func filterNonEmpty(input []string) []string {
|
||||
@@ -649,25 +663,25 @@ func defaultOptions() *Options {
|
||||
Man: false,
|
||||
Fuzzy: true,
|
||||
FuzzyAlgo: algo.FuzzyMatchV2,
|
||||
Scheme: "default",
|
||||
Scheme: "", // Unknown
|
||||
Extended: true,
|
||||
Phony: false,
|
||||
Inputless: false,
|
||||
Case: CaseSmart,
|
||||
Normalize: true,
|
||||
Nth: make([]Range, 0),
|
||||
WithNth: make([]Range, 0),
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Track: trackDisabled,
|
||||
Tac: false,
|
||||
Criteria: []criterion{byScore, byLength},
|
||||
Criteria: []criterion{}, // Unknown
|
||||
Multi: 0,
|
||||
Ansi: false,
|
||||
Mouse: true,
|
||||
Theme: theme,
|
||||
Black: false,
|
||||
Bold: true,
|
||||
MinHeight: 10,
|
||||
MinHeight: -10,
|
||||
Layout: layoutDefault,
|
||||
Cycle: false,
|
||||
Wrap: false,
|
||||
@@ -678,6 +692,7 @@ func defaultOptions() *Options {
|
||||
ScrollOff: 3,
|
||||
FileWord: false,
|
||||
InfoStyle: infoDefault,
|
||||
Ghost: "",
|
||||
Separator: nil,
|
||||
JumpLabels: defaultJumpLabels,
|
||||
Prompt: "> ",
|
||||
@@ -716,6 +731,7 @@ func defaultOptions() *Options {
|
||||
WalkerOpts: walkerOpts{file: true, hidden: true, follow: true},
|
||||
WalkerRoot: []string{"."},
|
||||
WalkerSkip: []string{".git", "node_modules"},
|
||||
TtyDefault: tui.DefaultTtyDevice,
|
||||
Help: false,
|
||||
Version: false}
|
||||
}
|
||||
@@ -758,6 +774,70 @@ func splitNth(str string) ([]Range, error) {
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
func nthTransformer(str string) (func(Delimiter) func([]Token, int32) string, error) {
|
||||
// ^[0-9,-.]+$"
|
||||
if match, _ := regexp.MatchString("^[0-9,-.]+$", str); match {
|
||||
nth, err := splitNth(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(Delimiter) func([]Token, int32) string {
|
||||
return func(tokens []Token, index int32) string {
|
||||
return JoinTokens(Transform(tokens, nth))
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// {...} {...} ...
|
||||
placeholder := regexp.MustCompile("{[0-9,-.]+}|{n}")
|
||||
indexes := placeholder.FindAllStringIndex(str, -1)
|
||||
if indexes == nil {
|
||||
return nil, errors.New("template should include at least 1 placeholder: " + str)
|
||||
}
|
||||
|
||||
type NthParts struct {
|
||||
str string
|
||||
index bool
|
||||
nth []Range
|
||||
}
|
||||
|
||||
parts := make([]NthParts, len(indexes))
|
||||
idx := 0
|
||||
for _, index := range indexes {
|
||||
if idx < index[0] {
|
||||
parts = append(parts, NthParts{str: str[idx:index[0]]})
|
||||
}
|
||||
expr := str[index[0]+1 : index[1]-1]
|
||||
if expr == "n" {
|
||||
parts = append(parts, NthParts{index: true})
|
||||
} else if nth, err := splitNth(expr); err == nil {
|
||||
parts = append(parts, NthParts{nth: nth})
|
||||
}
|
||||
idx = index[1]
|
||||
}
|
||||
if idx < len(str) {
|
||||
parts = append(parts, NthParts{str: str[idx:]})
|
||||
}
|
||||
|
||||
return func(delimiter Delimiter) func([]Token, int32) string {
|
||||
return func(tokens []Token, index int32) string {
|
||||
str := ""
|
||||
for _, holder := range parts {
|
||||
if holder.nth != nil {
|
||||
str += StripLastDelimiter(JoinTokens(Transform(tokens, holder.nth)), delimiter)
|
||||
} else if holder.index {
|
||||
if index >= 0 {
|
||||
str += strconv.Itoa(int(index))
|
||||
}
|
||||
} else {
|
||||
str += holder.str
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func delimiterRegexp(str string) Delimiter {
|
||||
// Special handling of \t
|
||||
str = strings.ReplaceAll(str, "\\t", "\t")
|
||||
@@ -800,16 +880,6 @@ func parseAlgo(str string) (algo.Algo, error) {
|
||||
return nil, errors.New("invalid algorithm (expected: v1 or v2)")
|
||||
}
|
||||
|
||||
func processScheme(opts *Options) error {
|
||||
if !algo.Init(opts.Scheme) {
|
||||
return errors.New("invalid scoring scheme (expected: default|path|history)")
|
||||
}
|
||||
if opts.Scheme == "history" {
|
||||
opts.Criteria = []criterion{byScore}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) {
|
||||
switch str {
|
||||
case "line":
|
||||
@@ -885,7 +955,7 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
|
||||
case "right":
|
||||
add(tui.Right)
|
||||
case "enter", "return":
|
||||
add(tui.CtrlM)
|
||||
add(tui.Enter)
|
||||
case "space":
|
||||
chords[tui.Key(' ')] = key
|
||||
case "backspace", "bspace", "bs":
|
||||
@@ -1033,6 +1103,19 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
|
||||
return chords, nil
|
||||
}
|
||||
|
||||
func parseScheme(str string) (string, []criterion, error) {
|
||||
str = strings.ToLower(str)
|
||||
switch str {
|
||||
case "history":
|
||||
return str, []criterion{byScore}, nil
|
||||
case "path":
|
||||
return str, []criterion{byScore, byPathname, byLength}, nil
|
||||
case "default":
|
||||
return str, []criterion{byScore, byLength}, nil
|
||||
}
|
||||
return str, nil, errors.New("invalid scoring scheme: " + str + " (expected: default|path|history)")
|
||||
}
|
||||
|
||||
func parseTiebreak(str string) ([]criterion, error) {
|
||||
criteria := []criterion{byScore}
|
||||
hasIndex := false
|
||||
@@ -1040,6 +1123,7 @@ func parseTiebreak(str string) ([]criterion, error) {
|
||||
hasLength := false
|
||||
hasBegin := false
|
||||
hasEnd := false
|
||||
hasPathname := false
|
||||
check := func(notExpected *bool, name string) error {
|
||||
if *notExpected {
|
||||
return errors.New("duplicate sort criteria: " + name)
|
||||
@@ -1061,6 +1145,11 @@ func parseTiebreak(str string) ([]criterion, error) {
|
||||
return nil, err
|
||||
}
|
||||
criteria = append(criteria, byChunk)
|
||||
case "pathname":
|
||||
if err := check(&hasPathname, "pathname"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
criteria = append(criteria, byPathname)
|
||||
case "length":
|
||||
if err := check(&hasLength, "length"); err != nil {
|
||||
return nil, err
|
||||
@@ -1095,7 +1184,12 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
|
||||
var err error
|
||||
theme := dupeTheme(defaultTheme)
|
||||
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||
for _, str := range strings.Split(strings.ToLower(str), ",") {
|
||||
comma := regexp.MustCompile(`[\s,]+`)
|
||||
for _, str := range comma.Split(strings.ToLower(str), -1) {
|
||||
str = strings.TrimSpace(str)
|
||||
if len(str) == 0 {
|
||||
continue
|
||||
}
|
||||
switch str {
|
||||
case "dark":
|
||||
theme = dupeTheme(tui.Dark256)
|
||||
@@ -1206,6 +1300,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
|
||||
mergeAttr(&theme.Current)
|
||||
case "current-bg", "bg+":
|
||||
mergeAttr(&theme.DarkBg)
|
||||
case "alt-bg":
|
||||
mergeAttr(&theme.AltBg)
|
||||
case "selected-fg":
|
||||
mergeAttr(&theme.SelectedFg)
|
||||
case "selected-bg":
|
||||
@@ -1317,7 +1413,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header)|transform|change-(?:preview-window|preview|multi|nth)|(?:re|un)bind|pos|put|print)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth|pointer|ghost)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1372,6 +1468,8 @@ Loop:
|
||||
masked += strings.Repeat(" ", loc[1])
|
||||
action = action[loc[1]:]
|
||||
}
|
||||
masked = strings.ReplaceAll(masked, ",,,", string([]rune{',', escapedComma, ','}))
|
||||
masked = strings.ReplaceAll(masked, ",:,", string([]rune{',', escapedColon, ','}))
|
||||
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
|
||||
masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
|
||||
masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
|
||||
@@ -1479,6 +1577,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleTrack)
|
||||
case "toggle-track-current":
|
||||
appendAction(actToggleTrackCurrent)
|
||||
case "toggle-input":
|
||||
appendAction(actToggleInput)
|
||||
case "hide-input":
|
||||
appendAction(actHideInput)
|
||||
case "show-input":
|
||||
appendAction(actShowInput)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "toggle-wrap":
|
||||
@@ -1571,6 +1675,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
} else {
|
||||
return nil, errors.New("unable to put non-printable character")
|
||||
}
|
||||
case "bell":
|
||||
appendAction(actBell)
|
||||
case "exclude":
|
||||
appendAction(actExclude)
|
||||
case "exclude-multi":
|
||||
appendAction(actExcludeMulti)
|
||||
default:
|
||||
t := isExecuteAction(specLower)
|
||||
if t == actIgnore {
|
||||
@@ -1597,7 +1707,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
}
|
||||
switch t {
|
||||
case actUnbind, actRebind:
|
||||
case actUnbind, actRebind, actToggleBind:
|
||||
if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1621,33 +1731,44 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error {
|
||||
var err error
|
||||
masked := maskActionContents(str)
|
||||
idx := 0
|
||||
keys := []string{}
|
||||
for _, pairStr := range strings.Split(masked, ",") {
|
||||
origPairStr := str[idx : idx+len(pairStr)]
|
||||
idx += len(pairStr) + 1
|
||||
|
||||
pair := strings.SplitN(pairStr, ":", 2)
|
||||
if len(pair) < 2 {
|
||||
return errors.New("bind action not specified: " + origPairStr)
|
||||
if len(pair[0]) == 0 {
|
||||
return errors.New("key name required")
|
||||
}
|
||||
var key tui.Event
|
||||
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
|
||||
key = tui.Key(':')
|
||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
|
||||
key = tui.Key(',')
|
||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
|
||||
key = tui.Key('+')
|
||||
} else {
|
||||
keys, err := parseKeyChordsImpl(pair[0], "key name required")
|
||||
keys = append(keys, pair[0])
|
||||
if len(pair) < 2 {
|
||||
continue
|
||||
}
|
||||
for _, keyName := range keys {
|
||||
var key tui.Event
|
||||
if len(keyName) == 1 && keyName[0] == escapedColon {
|
||||
key = tui.Key(':')
|
||||
} else if len(keyName) == 1 && keyName[0] == escapedComma {
|
||||
key = tui.Key(',')
|
||||
} else if len(keyName) == 1 && keyName[0] == escapedPlus {
|
||||
key = tui.Key('+')
|
||||
} else {
|
||||
keys, err := parseKeyChordsImpl(keyName, "key name required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key = firstKey(keys)
|
||||
}
|
||||
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
|
||||
keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key = firstKey(keys)
|
||||
}
|
||||
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
|
||||
keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keys = keys[:0]
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
return errors.New("bind action not specified: " + strings.Join(keys, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1671,6 +1792,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actUnbind
|
||||
case "rebind":
|
||||
return actRebind
|
||||
case "toggle-bind":
|
||||
return actToggleBind
|
||||
case "preview":
|
||||
return actPreview
|
||||
case "change-header":
|
||||
@@ -1685,6 +1808,10 @@ func isExecuteAction(str string) actionType {
|
||||
return actChangeInputLabel
|
||||
case "change-header-label":
|
||||
return actChangeHeaderLabel
|
||||
case "change-ghost":
|
||||
return actChangeGhost
|
||||
case "change-pointer":
|
||||
return actChangePointer
|
||||
case "change-preview-window":
|
||||
return actChangePreviewWindow
|
||||
case "change-preview":
|
||||
@@ -1723,10 +1850,20 @@ func isExecuteAction(str string) actionType {
|
||||
return actTransformHeaderLabel
|
||||
case "transform-header":
|
||||
return actTransformHeader
|
||||
case "transform-ghost":
|
||||
return actTransformGhost
|
||||
case "transform-nth":
|
||||
return actTransformNth
|
||||
case "transform-pointer":
|
||||
return actTransformPointer
|
||||
case "transform-prompt":
|
||||
return actTransformPrompt
|
||||
case "transform-query":
|
||||
return actTransformQuery
|
||||
case "transform-search":
|
||||
return actTransformSearch
|
||||
case "search":
|
||||
return actSearch
|
||||
}
|
||||
return actIgnore
|
||||
}
|
||||
@@ -2208,6 +2345,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
}
|
||||
case "--no-tmux":
|
||||
opts.Tmux = nil
|
||||
case "--tty-default":
|
||||
if opts.TtyDefault, err = nextString("tty device name required"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-tty-default":
|
||||
opts.TtyDefault = ""
|
||||
case "--force-tty-in":
|
||||
// NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't
|
||||
// work on Neovim. Same as '-' option of fzf-tmux.
|
||||
@@ -2257,7 +2400,9 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Scheme = strings.ToLower(str)
|
||||
if opts.Scheme, opts.Criteria, err = parseScheme(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--expect":
|
||||
str, err := nextString("key names required")
|
||||
if err != nil {
|
||||
@@ -2276,6 +2421,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
opts.Phony = false
|
||||
case "--disabled", "--phony":
|
||||
opts.Phony = true
|
||||
case "--no-input":
|
||||
opts.Inputless = true
|
||||
case "--tiebreak":
|
||||
str, err := nextString("sort criterion required")
|
||||
if err != nil {
|
||||
@@ -2328,7 +2475,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.WithNth, err = splitNth(str); err != nil {
|
||||
if opts.WithNth, err = nthTransformer(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--accept-nth":
|
||||
str, err := nextString("nth expression required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.AcceptNth, err = nthTransformer(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "-s", "--sort":
|
||||
@@ -2468,6 +2623,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
case "--no-separator":
|
||||
nosep := ""
|
||||
opts.Separator = &nosep
|
||||
case "--ghost":
|
||||
if opts.Ghost, err = nextString("ghost text required"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--scrollbar":
|
||||
given, bar := optionalNextString()
|
||||
if given {
|
||||
@@ -2624,9 +2783,23 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
return err
|
||||
}
|
||||
case "--min-height":
|
||||
if opts.MinHeight, err = nextInt("height required: HEIGHT"); err != nil {
|
||||
expr, err := nextString("minimum height required: HEIGHT[+]")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auto := false
|
||||
if strings.HasSuffix(expr, "+") {
|
||||
expr = expr[:len(expr)-1]
|
||||
auto = true
|
||||
}
|
||||
num, err := atoi(expr)
|
||||
if err != nil || num < 0 {
|
||||
return errors.New("minimum height must be a non-negative integer")
|
||||
}
|
||||
if auto {
|
||||
num *= -1
|
||||
}
|
||||
opts.MinHeight = num
|
||||
case "--no-height":
|
||||
opts.Height = heightSpec{}
|
||||
case "--no-margin":
|
||||
@@ -2669,6 +2842,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-header-lines-border":
|
||||
opts.HeaderLinesShape = tui.BorderNone
|
||||
case "--header-lines-border":
|
||||
hasArg, arg := optionalNextString()
|
||||
if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg, false); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-header-label":
|
||||
opts.HeaderLabel.label = ""
|
||||
case "--header-label":
|
||||
@@ -2993,6 +3173,24 @@ func validateOptions(opts *Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func noSeparatorLine(style infoStyle, separator bool) bool {
|
||||
switch style {
|
||||
case infoInline:
|
||||
return true
|
||||
case infoHidden, infoInlineRight:
|
||||
return !separator
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (opts *Options) noSeparatorLine() bool {
|
||||
if opts.Inputless {
|
||||
return true
|
||||
}
|
||||
sep := opts.Separator == nil && !opts.InputBorderShape.Visible() || opts.Separator != nil && len(*opts.Separator) > 0
|
||||
return noSeparatorLine(opts.InfoStyle, sep)
|
||||
}
|
||||
|
||||
// This function can have side-effects and alter some global states.
|
||||
// So we run it on fzf.Run and not on ParseOptions.
|
||||
func postProcessOptions(opts *Options) error {
|
||||
@@ -3016,6 +3214,19 @@ func postProcessOptions(opts *Options) error {
|
||||
opts.HeaderBorderShape = tui.BorderNone
|
||||
}
|
||||
|
||||
if opts.HeaderLinesShape == tui.BorderNone {
|
||||
opts.HeaderLinesShape = tui.BorderPhantom
|
||||
} else if opts.HeaderLinesShape == tui.BorderUndefined {
|
||||
// In reverse-list layout, header lines should be at the top, while
|
||||
// ordinary header should be at the bottom. So let's use a separate
|
||||
// window for the header lines.
|
||||
if opts.Layout == layoutReverseList {
|
||||
opts.HeaderLinesShape = tui.BorderPhantom
|
||||
} else {
|
||||
opts.HeaderLinesShape = tui.BorderNone
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Pointer == nil {
|
||||
defaultPointer := "▌"
|
||||
if !opts.Unicode {
|
||||
@@ -3116,7 +3327,7 @@ func postProcessOptions(opts *Options) error {
|
||||
|
||||
// If 'double-click' is left unbound, bind it to the action bound to 'enter'
|
||||
if _, prs := opts.Keymap[tui.DoubleClick.AsEvent()]; !prs {
|
||||
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()]
|
||||
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.Enter.AsEvent()]
|
||||
}
|
||||
|
||||
// If we're not using extended search mode, --nth option becomes irrelevant
|
||||
@@ -3152,11 +3363,46 @@ func postProcessOptions(opts *Options) error {
|
||||
opts.Height = heightSpec{}
|
||||
}
|
||||
|
||||
// Sets --min-height automatically
|
||||
if opts.Height.size > 0 && opts.Height.percent && opts.MinHeight < 0 {
|
||||
opts.MinHeight = -opts.MinHeight + borderLines(opts.BorderShape) + borderLines(opts.ListBorderShape)
|
||||
if !opts.Inputless {
|
||||
opts.MinHeight += 1 + borderLines(opts.InputBorderShape)
|
||||
if !opts.noSeparatorLine() {
|
||||
opts.MinHeight++
|
||||
}
|
||||
}
|
||||
if len(opts.Header) > 0 {
|
||||
opts.MinHeight += borderLines(opts.HeaderBorderShape) + len(opts.Header)
|
||||
}
|
||||
if opts.HeaderLines > 0 {
|
||||
borderShape := opts.HeaderBorderShape
|
||||
if opts.HeaderLinesShape.Visible() {
|
||||
borderShape = opts.HeaderLinesShape
|
||||
}
|
||||
opts.MinHeight += borderLines(borderShape) + opts.HeaderLines
|
||||
}
|
||||
if len(opts.Preview.command) > 0 && (opts.Preview.position == posUp || opts.Preview.position == posDown) && opts.Preview.Visible() && opts.Preview.position == posUp {
|
||||
borderShape := opts.Preview.border
|
||||
if opts.Preview.border == tui.BorderLine {
|
||||
borderShape = tui.BorderTop
|
||||
}
|
||||
opts.MinHeight += borderLines(borderShape) + 10
|
||||
}
|
||||
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2], opts.Padding[0], opts.Padding[2]} {
|
||||
if !s.percent {
|
||||
opts.MinHeight += int(s.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := opts.initProfiling(); err != nil {
|
||||
return errors.New("failed to start pprof profiles: " + err.Error())
|
||||
}
|
||||
|
||||
return processScheme(opts)
|
||||
algo.Init(opts.Scheme)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseShellWords(str string) ([]string, error) {
|
||||
@@ -3206,7 +3452,26 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. Final validation of merged options
|
||||
// 4. Change default scheme when built-in walker is used
|
||||
if len(opts.Scheme) == 0 {
|
||||
opts.Scheme = "default"
|
||||
if len(opts.Criteria) == 0 {
|
||||
// NOTE: Let's assume $FZF_DEFAULT_COMMAND generates a list of file paths.
|
||||
// But it is possible that it is set to a command that doesn't generate
|
||||
// file paths.
|
||||
//
|
||||
// In that case, you can either
|
||||
// 1. explicitly set --scheme=default,
|
||||
// 2. or replace $FZF_DEFAULT_COMMAND with an equivalent 'start:reload'
|
||||
// binding, which is the new preferred way.
|
||||
if !opts.hasReloadOrTransformOnStart() && util.IsTty(os.Stdin) {
|
||||
opts.Scheme = "path"
|
||||
}
|
||||
_, opts.Criteria, _ = parseScheme(opts.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Final validation of merged options
|
||||
if err := validateOptions(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -3214,6 +3479,17 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (opts *Options) hasReloadOrTransformOnStart() bool {
|
||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||
for _, action := range actions {
|
||||
if action.t == actReload || action.t == actReloadSync || action.t == actTransform {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (opts *Options) extractReloadOnStart() string {
|
||||
cmd := ""
|
||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||
|
@@ -172,7 +172,7 @@ func TestParseKeys(t *testing.T) {
|
||||
if len(pairs) != 9 {
|
||||
t.Error(9)
|
||||
}
|
||||
check(tui.CtrlM, "Return")
|
||||
check(tui.Enter, "Return")
|
||||
checkEvent(tui.Key(' '), "space")
|
||||
check(tui.Tab, "tab")
|
||||
check(tui.ShiftTab, "btab")
|
||||
@@ -195,7 +195,7 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.ShiftLeft, "shift-left")
|
||||
check(tui.ShiftRight, "shift-right")
|
||||
check(tui.ShiftTab, "shift-tab")
|
||||
check(tui.CtrlM, "Enter")
|
||||
check(tui.Enter, "Enter")
|
||||
check(tui.Backspace, "bspace")
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||
}
|
||||
|
||||
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
|
||||
customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
||||
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
|
@@ -60,9 +60,10 @@ type Pattern struct {
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
revision int
|
||||
revision revision
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
}
|
||||
|
||||
var _splitRegex *regexp.Regexp
|
||||
@@ -73,7 +74,7 @@ func init() {
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision int, runes []rune) *Pattern {
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
@@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -296,14 +301,38 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
// Huge code duplication for minimizing unnecessary map lookups
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
@@ -403,6 +432,13 @@ func (p *Pattern) transformInput(item *Item) []Token {
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
// Strip the last delimiter to allow suffix match
|
||||
if len(ret) > 0 && !p.delimiter.IsAwk() {
|
||||
chars := ret[len(ret)-1].text
|
||||
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
|
||||
newChars := util.ToChars(stringBytes(stripped))
|
||||
ret[len(ret)-1].text = &newChars
|
||||
}
|
||||
item.transformed = &transformed{p.revision, ret}
|
||||
return ret
|
||||
}
|
||||
|
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||
withPos, cacheable, nth, delimiter, 0, runes)
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
|
15
src/proxy.go
15
src/proxy.go
@@ -59,12 +59,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
})
|
||||
}()
|
||||
|
||||
var command string
|
||||
var command, input string
|
||||
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
|
||||
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
|
||||
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
|
||||
} else {
|
||||
input, err := fifo("proxy-input")
|
||||
input, err = fifo("proxy-input")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
@@ -90,9 +90,9 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
}
|
||||
}
|
||||
|
||||
// To ensure that the options are processed by a POSIX-compliant shell,
|
||||
// we need to write the command to a temporary file and execute it with sh.
|
||||
var exports []string
|
||||
// * Write the command to a temporary file and run it with sh to ensure POSIX compliance.
|
||||
// * Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
|
||||
exports := []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
|
||||
needBash := false
|
||||
if withExports {
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
@@ -144,10 +144,13 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
env = elems[1:]
|
||||
}
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
ttyin, err := tui.TtyIn()
|
||||
ttyin, err := tui.TtyIn(opts.TtyDefault)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
os.Remove(temp)
|
||||
os.Remove(input)
|
||||
os.Remove(output)
|
||||
executor.Become(ttyin, env, command)
|
||||
}
|
||||
return code, err
|
||||
|
@@ -277,6 +277,9 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
ignoresFull := []string{}
|
||||
ignoresSuffix := []string{}
|
||||
sep := string(os.PathSeparator)
|
||||
if _, ok := os.LookupEnv("MSYSTEM"); ok {
|
||||
sep = "/"
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
if strings.ContainsRune(ignore, os.PathSeparator) {
|
||||
if strings.HasPrefix(ignore, sep) {
|
||||
@@ -320,6 +323,9 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if path != sep {
|
||||
path += sep
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
|
@@ -69,6 +69,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
}
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byPathname:
|
||||
if validOffsetFound {
|
||||
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
|
||||
lastDelim := -1
|
||||
s := item.text.ToString()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '/' || s[i] == '\\' {
|
||||
lastDelim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastDelim <= minBegin {
|
||||
val = util.AsUint16(minBegin - lastDelim)
|
||||
}
|
||||
}
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
@@ -104,7 +119,7 @@ func minRank() Result {
|
||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset {
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
// No ANSI codes
|
||||
@@ -167,18 +182,10 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current.Color
|
||||
} else {
|
||||
fg = theme.Fg.Color
|
||||
}
|
||||
fg = colBase.Fg()
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg.Color
|
||||
} else {
|
||||
bg = theme.Bg.Color
|
||||
}
|
||||
bg = colBase.Bg()
|
||||
}
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
|
@@ -131,7 +131,7 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, true)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||
@@ -158,7 +158,7 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, true)
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr)
|
||||
|
||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||
|
1022
src/terminal.go
1022
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {r}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {r..}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
@@ -484,7 +492,12 @@ func TestParsePlaceholder(t *testing.T) {
|
||||
// III. query type placeholder
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
`{q}`: `{qq}`,
|
||||
`{q}`: `{qq}`,
|
||||
`{q:1}`: `{qq:1}`,
|
||||
`{q:2..}`: `{qq:2..}`,
|
||||
`{q:..}`: `{qq:..}`,
|
||||
`{q:2..-1}`: `{qq:2..-1}`,
|
||||
`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
|
||||
|
||||
// IV. escaping placeholder
|
||||
`\{}`: `{}`,
|
||||
@@ -560,7 +573,7 @@ func (item *Item) String() string {
|
||||
}
|
||||
|
||||
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||
func templateToString(format string, data interface{}) string {
|
||||
func templateToString(format string, data any) string {
|
||||
bb := &bytes.Buffer{}
|
||||
|
||||
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
@@ -22,6 +23,18 @@ func (r Range) IsFull() bool {
|
||||
return r.begin == rangeEllipsis && r.end == rangeEllipsis
|
||||
}
|
||||
|
||||
func compareRanges(r1 []Range, r2 []Range) bool {
|
||||
if len(r1) != len(r2) {
|
||||
return false
|
||||
}
|
||||
for idx := range r1 {
|
||||
if r1[idx] != r2[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RangesToString(ranges []Range) string {
|
||||
strs := []string{}
|
||||
for _, r := range ranges {
|
||||
@@ -65,6 +78,11 @@ type Delimiter struct {
|
||||
str *string
|
||||
}
|
||||
|
||||
// IsAwk returns true if the delimiter is an AWK-style delimiter
|
||||
func (d Delimiter) IsAwk() bool {
|
||||
return d.regex == nil && d.str == nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Delimiter.
|
||||
func (d Delimiter) String() string {
|
||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||
@@ -199,7 +217,24 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) string {
|
||||
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
if lastLoc[1] == len(str) {
|
||||
str = str[:lastLoc[0]]
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
|
||||
// JoinTokens concatenates the tokens into a single string
|
||||
func JoinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
output.WriteString(token.text.ToString())
|
||||
@@ -217,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
||||
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
|
@@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != "abc: def: ghi: " {
|
||||
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
@@ -107,7 +107,7 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
|
@@ -44,6 +44,9 @@ func (r *FullscreenRenderer) PassThrough(string) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
||||
func (r *FullscreenRenderer) Bell() {}
|
||||
func (r *FullscreenRenderer) HideCursor() {}
|
||||
func (r *FullscreenRenderer) ShowCursor() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||
|
@@ -21,7 +21,7 @@ func _() {
|
||||
_ = x[CtrlJ-10]
|
||||
_ = x[CtrlK-11]
|
||||
_ = x[CtrlL-12]
|
||||
_ = x[CtrlM-13]
|
||||
_ = x[Enter-13]
|
||||
_ = x[CtrlN-14]
|
||||
_ = x[CtrlO-15]
|
||||
_ = x[CtrlP-16]
|
||||
@@ -84,35 +84,37 @@ func _() {
|
||||
_ = x[CtrlAlt-73]
|
||||
_ = x[Invalid-74]
|
||||
_ = x[Fatal-75]
|
||||
_ = x[Mouse-76]
|
||||
_ = x[DoubleClick-77]
|
||||
_ = x[LeftClick-78]
|
||||
_ = x[RightClick-79]
|
||||
_ = x[SLeftClick-80]
|
||||
_ = x[SRightClick-81]
|
||||
_ = x[ScrollUp-82]
|
||||
_ = x[ScrollDown-83]
|
||||
_ = x[SScrollUp-84]
|
||||
_ = x[SScrollDown-85]
|
||||
_ = x[PreviewScrollUp-86]
|
||||
_ = x[PreviewScrollDown-87]
|
||||
_ = x[Resize-88]
|
||||
_ = x[Change-89]
|
||||
_ = x[BackwardEOF-90]
|
||||
_ = x[Start-91]
|
||||
_ = x[Load-92]
|
||||
_ = x[Focus-93]
|
||||
_ = x[One-94]
|
||||
_ = x[Zero-95]
|
||||
_ = x[Result-96]
|
||||
_ = x[Jump-97]
|
||||
_ = x[JumpCancel-98]
|
||||
_ = x[ClickHeader-99]
|
||||
_ = x[BracketedPasteBegin-76]
|
||||
_ = x[BracketedPasteEnd-77]
|
||||
_ = x[Mouse-78]
|
||||
_ = x[DoubleClick-79]
|
||||
_ = x[LeftClick-80]
|
||||
_ = x[RightClick-81]
|
||||
_ = x[SLeftClick-82]
|
||||
_ = x[SRightClick-83]
|
||||
_ = x[ScrollUp-84]
|
||||
_ = x[ScrollDown-85]
|
||||
_ = x[SScrollUp-86]
|
||||
_ = x[SScrollDown-87]
|
||||
_ = x[PreviewScrollUp-88]
|
||||
_ = x[PreviewScrollDown-89]
|
||||
_ = x[Resize-90]
|
||||
_ = x[Change-91]
|
||||
_ = x[BackwardEOF-92]
|
||||
_ = x[Start-93]
|
||||
_ = x[Load-94]
|
||||
_ = x[Focus-95]
|
||||
_ = x[One-96]
|
||||
_ = x[Zero-97]
|
||||
_ = x[Result-98]
|
||||
_ = x[Jump-99]
|
||||
_ = x[JumpCancel-100]
|
||||
_ = x[ClickHeader-101]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
141
src/tui/light.go
141
src/tui/light.go
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -27,11 +28,15 @@ const (
|
||||
maxInputBuffer = 1024 * 1024
|
||||
)
|
||||
|
||||
const consoleDevice string = "/dev/tty"
|
||||
const DefaultTtyDevice string = "/dev/tty"
|
||||
|
||||
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
|
||||
var offsetRegexp = regexp.MustCompile("(.*?)\x00?\x1b\\[([0-9]+);([0-9]+)R")
|
||||
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||
|
||||
func (r *LightRenderer) Bell() {
|
||||
r.flushRaw("\a")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) PassThrough(str string) {
|
||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||
}
|
||||
@@ -40,8 +45,9 @@ func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true, "")
|
||||
}
|
||||
|
||||
const CR string = "\x1b[2m␍"
|
||||
const LF string = "\x1b[2m␊"
|
||||
const DIM string = "\x1b[2m"
|
||||
const CR string = DIM + "␍"
|
||||
const LF string = DIM + "␊"
|
||||
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
bytes := []byte(str)
|
||||
@@ -73,7 +79,13 @@ func (r *LightRenderer) csi(code string) string {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
r.flushRaw("\x1b[?7l\x1b[?25l" + r.queued.String() + "\x1b[?25h\x1b[?7h")
|
||||
raw := "\x1b[?7l\x1b[?25l" + r.queued.String()
|
||||
if r.showCursor {
|
||||
raw += "\x1b[?25h\x1b[?7h"
|
||||
} else {
|
||||
raw += "\x1b[?7h"
|
||||
}
|
||||
r.flushRaw(raw)
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
@@ -84,7 +96,6 @@ func (r *LightRenderer) flushRaw(sequence string) {
|
||||
|
||||
// Light renderer
|
||||
type LightRenderer struct {
|
||||
closed *util.AtomicBool
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
@@ -106,8 +117,10 @@ type LightRenderer struct {
|
||||
y int
|
||||
x int
|
||||
maxHeightFunc func(int) int
|
||||
showCursor bool
|
||||
|
||||
// Windows only
|
||||
mutex sync.Mutex
|
||||
ttyinChannel chan byte
|
||||
inHandle uintptr
|
||||
outHandle uintptr
|
||||
@@ -116,28 +129,29 @@ type LightRenderer struct {
|
||||
}
|
||||
|
||||
type LightWindow struct {
|
||||
renderer *LightRenderer
|
||||
colored bool
|
||||
windowType WindowType
|
||||
border BorderStyle
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
posx int
|
||||
posy int
|
||||
tabstop int
|
||||
fg Color
|
||||
bg Color
|
||||
renderer *LightRenderer
|
||||
colored bool
|
||||
windowType WindowType
|
||||
border BorderStyle
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
posx int
|
||||
posy int
|
||||
tabstop int
|
||||
fg Color
|
||||
bg Color
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
}
|
||||
|
||||
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
out, err := openTtyOut()
|
||||
func NewLightRenderer(ttyDefault string, ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
out, err := openTtyOut(ttyDefault)
|
||||
if err != nil {
|
||||
out = os.Stderr
|
||||
}
|
||||
r := LightRenderer{
|
||||
closed: util.NewAtomicBool(false),
|
||||
theme: theme,
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
@@ -148,7 +162,8 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse
|
||||
tabstop: tabstop,
|
||||
fullscreen: fullscreen,
|
||||
upOneLine: false,
|
||||
maxHeightFunc: maxHeightFunc}
|
||||
maxHeightFunc: maxHeightFunc,
|
||||
showCursor: true}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
@@ -198,7 +213,7 @@ func (r *LightRenderer) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
r.enableMouse()
|
||||
r.enableModes()
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
@@ -256,7 +271,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
|
||||
c, ok := r.getch(nonblock)
|
||||
if !nonblock && !ok {
|
||||
r.Close()
|
||||
return nil, errors.New("failed to read " + consoleDevice)
|
||||
return nil, errors.New("failed to read " + DefaultTtyDevice)
|
||||
}
|
||||
|
||||
retries := 0
|
||||
@@ -447,10 +462,11 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
// Bracketed paste mode: \e[200~ ... \e[201~
|
||||
if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
|
||||
// Immediately discard the sequence from the buffer and reread input
|
||||
r.buffer = r.buffer[6:]
|
||||
*sz = 0
|
||||
return r.GetChar()
|
||||
*sz = 6
|
||||
if r.buffer[4] == '0' {
|
||||
return Event{BracketedPasteBegin, 0, nil}
|
||||
}
|
||||
return Event{BracketedPasteEnd, 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case '3':
|
||||
@@ -622,15 +638,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
|
||||
// middle := t & 0b1
|
||||
left := t&0b11 == 0
|
||||
|
||||
// shift := t & 0b100
|
||||
// ctrl := t & 0b1000
|
||||
mod := t&0b1100 > 0
|
||||
|
||||
drag := t&0b100000 > 0
|
||||
ctrl := t&0b10000 > 0
|
||||
alt := t&0b01000 > 0
|
||||
shift := t&0b00100 > 0
|
||||
drag := t&0b100000 > 0 // 32
|
||||
|
||||
if scroll != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
double := false
|
||||
@@ -654,7 +668,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
}
|
||||
}
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) smcup() {
|
||||
@@ -668,7 +682,7 @@ func (r *LightRenderer) rmcup() {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause(clear bool) {
|
||||
r.disableMouse()
|
||||
r.disableModes()
|
||||
r.restoreTerminal()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
@@ -681,12 +695,13 @@ func (r *LightRenderer) Pause(clear bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) enableMouse() {
|
||||
func (r *LightRenderer) enableModes() {
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
r.csi("?1002h")
|
||||
r.csi("?1006h")
|
||||
}
|
||||
r.csi("?2004h") // Enable bracketed paste mode
|
||||
}
|
||||
|
||||
func (r *LightRenderer) disableMouse() {
|
||||
@@ -697,6 +712,11 @@ func (r *LightRenderer) disableMouse() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) disableModes() {
|
||||
r.disableMouse()
|
||||
r.csi("?2004l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
r.setupTerminal()
|
||||
if clear {
|
||||
@@ -705,7 +725,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
} else {
|
||||
r.rmcup()
|
||||
}
|
||||
r.enableMouse()
|
||||
r.enableModes()
|
||||
r.flush()
|
||||
} else if sigcont && !r.fullscreen && r.mouse {
|
||||
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
||||
@@ -757,11 +777,13 @@ func (r *LightRenderer) Close() {
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
}
|
||||
r.disableMouse()
|
||||
if !r.showCursor {
|
||||
r.csi("?25h")
|
||||
}
|
||||
r.disableModes()
|
||||
r.flush()
|
||||
r.closePlatform()
|
||||
r.restoreTerminal()
|
||||
r.closed.Set(true)
|
||||
r.closePlatform()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Top() int {
|
||||
@@ -1092,11 +1114,12 @@ type wrappedLine struct {
|
||||
displayWidth int
|
||||
}
|
||||
|
||||
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
||||
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
|
||||
lines := []wrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
max := initialMax
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
str := string(rs)
|
||||
@@ -1118,6 +1141,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = w
|
||||
max = initialMax - wrapSignWidth
|
||||
}
|
||||
}
|
||||
lines = append(lines, wrappedLine{string(line), width})
|
||||
@@ -1127,7 +1151,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
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)
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.text, false, resetCode)
|
||||
w.posx += wl.displayWidth
|
||||
@@ -1140,6 +1164,18 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
if len(lines) > 1 {
|
||||
sign := w.wrapSign
|
||||
width := w.wrapSignWidth
|
||||
if width > w.width {
|
||||
runes, truncatedWidth := util.Truncate(w.wrapSign, w.width)
|
||||
sign = string(runes)
|
||||
width = truncatedWidth
|
||||
}
|
||||
w.stderrInternal(DIM+sign, false, resetCode)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.Move(w.posy, width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1212,3 +1248,18 @@ func (w *LightWindow) Erase() {
|
||||
func (w *LightWindow) EraseMaybe() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *LightWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (r *LightRenderer) HideCursor() {
|
||||
r.showCursor = false
|
||||
r.csi("?25l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) ShowCursor() {
|
||||
r.showCursor = true
|
||||
r.csi("?25h")
|
||||
}
|
||||
|
@@ -42,26 +42,35 @@ func (r *LightRenderer) closePlatform() {
|
||||
r.ttyout.Close()
|
||||
}
|
||||
|
||||
func openTty(mode int) (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, mode, 0)
|
||||
if err != nil {
|
||||
func openTty(ttyDefault string, mode int) (*os.File, error) {
|
||||
var in *os.File
|
||||
var err error
|
||||
if len(ttyDefault) > 0 {
|
||||
in, err = os.OpenFile(ttyDefault, mode, 0)
|
||||
}
|
||||
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to open " + consoleDevice)
|
||||
if ttyDefault != DefaultTtyDevice {
|
||||
if in, err = os.OpenFile(DefaultTtyDevice, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to open " + DefaultTtyDevice)
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
return openTty(syscall.O_RDONLY)
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTty(ttyDefault, syscall.O_RDONLY)
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return openTty(syscall.O_WRONLY)
|
||||
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTty(ttyDefault, syscall.O_WRONLY)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
|
@@ -18,6 +18,7 @@ const (
|
||||
var (
|
||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
counter = uint64(0)
|
||||
)
|
||||
|
||||
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
|
||||
}
|
||||
r.inHandle = uintptr(inHandle)
|
||||
|
||||
r.setupTerminal()
|
||||
|
||||
// channel for non-blocking reads. Buffer to make sure
|
||||
// we get the ESC sets:
|
||||
r.ttyinChannel = make(chan byte, 1024)
|
||||
|
||||
// the following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for !r.closed.Get() {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
r.setupTerminal()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,27 +76,51 @@ func (r *LightRenderer) closePlatform() {
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
// not used
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||
return os.Stderr, nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
// The following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
current := counter
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
if _, err := util.Read(fd, b); err == nil {
|
||||
r.mutex.Lock()
|
||||
// This condition prevents the goroutine from running after the renderer
|
||||
// has been closed or paused.
|
||||
if current != counter {
|
||||
r.mutex.Unlock()
|
||||
break
|
||||
}
|
||||
r.ttyinChannel <- b[0]
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
func (r *LightRenderer) restoreTerminal() {
|
||||
r.mutex.Lock()
|
||||
counter++
|
||||
// We're setting ENABLE_VIRTUAL_TERMINAL_INPUT to allow escape sequences to be read during 'execute'.
|
||||
// e.g. fzf --bind 'enter:execute:less {}'
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput|windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
|
102
src/tui/tcell.go
102
src/tui/tcell.go
@@ -39,19 +39,22 @@ func (p ColorPair) style() tcell.Style {
|
||||
type Attr int32
|
||||
|
||||
type TcellWindow struct {
|
||||
color bool
|
||||
windowType WindowType
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
normal ColorPair
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
color bool
|
||||
windowType WindowType
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
normal ColorPair
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
showCursor bool
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -72,7 +75,9 @@ func (w *TcellWindow) Height() int {
|
||||
|
||||
func (w *TcellWindow) Refresh() {
|
||||
if w.moveCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
if w.showCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
}
|
||||
w.moveCursor = false
|
||||
}
|
||||
w.lastX = 0
|
||||
@@ -100,6 +105,18 @@ const (
|
||||
BoldForce = Attr(1 << 10)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) Bell() {
|
||||
_screen.Beep()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) HideCursor() {
|
||||
r.showCursor = false
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) ShowCursor() {
|
||||
r.showCursor = true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||
// No-op
|
||||
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
||||
@@ -164,6 +181,9 @@ func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if !r.showCursor {
|
||||
s.HideCursor()
|
||||
}
|
||||
_screen = s
|
||||
}
|
||||
return _screen, nil
|
||||
@@ -177,6 +197,7 @@ func (r *FullscreenRenderer) initScreen() error {
|
||||
if e = s.Init(); e != nil {
|
||||
return e
|
||||
}
|
||||
s.EnablePaste()
|
||||
if r.mouse {
|
||||
s.EnableMouse()
|
||||
} else {
|
||||
@@ -246,6 +267,11 @@ func (r *FullscreenRenderer) Size() TermSize {
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventPaste:
|
||||
if ev.Start() {
|
||||
return Event{BracketedPasteBegin, 0, nil}
|
||||
}
|
||||
return Event{BracketedPasteEnd, 0, nil}
|
||||
case *tcell.EventResize:
|
||||
// Ignore the first resize event
|
||||
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
|
||||
@@ -262,7 +288,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||
x, y := ev.Position()
|
||||
mod := ev.Modifiers() != 0
|
||||
|
||||
mod := ev.Modifiers()
|
||||
ctrl := (mod & tcell.ModCtrl) > 0
|
||||
alt := (mod & tcell.ModAlt) > 0
|
||||
shift := (mod & tcell.ModShift) > 0
|
||||
|
||||
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||
@@ -271,9 +301,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
switch {
|
||||
case button&tcell.WheelDown != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
|
||||
case button&tcell.WheelUp != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
|
||||
case button&tcell.Button1 != 0:
|
||||
double := false
|
||||
if !drag {
|
||||
@@ -296,9 +326,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
}
|
||||
// fire single or double click event
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
|
||||
case button&tcell.Button2 != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
|
||||
default:
|
||||
// double and single taps on Windows don't quite work due to
|
||||
// the console acting on the events and not allowing us
|
||||
@@ -307,7 +337,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
down := left || button&tcell.Button3 != 0
|
||||
double := false
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
// No need to report mouse movement events when no button is pressed
|
||||
if drag {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
// process keyboard:
|
||||
@@ -534,7 +568,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||
if clear {
|
||||
_screen.Fini()
|
||||
r.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +580,7 @@ func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
_screen.Fini()
|
||||
_screen = nil
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
@@ -578,7 +613,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
width: width,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
borderStyle: borderStyle,
|
||||
showCursor: r.showCursor}
|
||||
w.Erase()
|
||||
return w
|
||||
}
|
||||
@@ -601,6 +637,11 @@ func (w *TcellWindow) EraseMaybe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EncloseX(x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width)
|
||||
}
|
||||
@@ -729,11 +770,26 @@ Loop:
|
||||
|
||||
// word wrap:
|
||||
xPos := w.left + w.lastX + lx
|
||||
if xPos >= (w.left + w.width) {
|
||||
if xPos >= w.left+w.width {
|
||||
w.lastY++
|
||||
if w.lastY >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
sign := w.wrapSign
|
||||
if w.wrapSignWidth > w.width {
|
||||
runes, _ := util.Truncate(sign, w.width)
|
||||
sign = string(runes)
|
||||
}
|
||||
wgr := uniseg.NewGraphemes(sign)
|
||||
for wgr.Next() {
|
||||
rs := wgr.Runes()
|
||||
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||
lx += uniseg.StringWidth(string(rs))
|
||||
}
|
||||
xPos = w.left + lx
|
||||
}
|
||||
|
||||
yPos := w.top + w.lastY
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||
func assert(t *testing.T, context string, got any, want any) bool {
|
||||
if got == want {
|
||||
return true
|
||||
} else {
|
||||
@@ -82,9 +82,9 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
||||
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||
// KeyEnter is alias for KeyCR
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
// Ctrl+Alt keys
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
@@ -233,7 +233,7 @@ Quick reference
|
||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
|
||||
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||
|
@@ -44,11 +44,11 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to read user input
|
||||
func TtyIn() (*os.File, error) {
|
||||
return openTtyIn()
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTtyIn(ttyDefault)
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to write to
|
||||
func TtyOut() (*os.File, error) {
|
||||
return openTtyOut()
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTtyOut(ttyDefault)
|
||||
}
|
||||
|
@@ -11,11 +11,11 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() (*os.File, error) {
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return os.Stdin, nil
|
||||
}
|
||||
|
||||
// TtyOut on Windows returns nil
|
||||
func TtyOut() (*os.File, error) {
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ const (
|
||||
CtrlJ
|
||||
CtrlK
|
||||
CtrlL
|
||||
CtrlM
|
||||
Enter
|
||||
CtrlN
|
||||
CtrlO
|
||||
CtrlP
|
||||
@@ -103,6 +103,8 @@ const (
|
||||
|
||||
Invalid
|
||||
Fatal
|
||||
BracketedPasteBegin
|
||||
BracketedPasteEnd
|
||||
|
||||
Mouse
|
||||
DoubleClick
|
||||
@@ -150,12 +152,19 @@ func (e Event) Comparable() Event {
|
||||
}
|
||||
|
||||
func (e Event) KeyName() string {
|
||||
if me := e.MouseEvent; me != nil {
|
||||
return me.Name()
|
||||
}
|
||||
|
||||
if e.Type >= Invalid {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case Rune:
|
||||
if e.Char == ' ' {
|
||||
return "space"
|
||||
}
|
||||
return string(e.Char)
|
||||
case Alt:
|
||||
return "alt-" + string(e.Char)
|
||||
@@ -299,6 +308,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
||||
return dup
|
||||
}
|
||||
|
||||
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
|
||||
dup := p
|
||||
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
|
||||
return dup.Merge(bgPair)
|
||||
}
|
||||
|
||||
func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
|
||||
return p.WithAttr(other.attr)
|
||||
}
|
||||
@@ -319,6 +334,7 @@ type ColorTheme struct {
|
||||
Bg ColorAttr
|
||||
ListFg ColorAttr
|
||||
ListBg ColorAttr
|
||||
AltBg ColorAttr
|
||||
Nth ColorAttr
|
||||
SelectedFg ColorAttr
|
||||
SelectedBg ColorAttr
|
||||
@@ -367,7 +383,37 @@ type MouseEvent struct {
|
||||
Left bool
|
||||
Down bool
|
||||
Double bool
|
||||
Mod bool
|
||||
Ctrl bool
|
||||
Alt bool
|
||||
Shift bool
|
||||
}
|
||||
|
||||
func (e MouseEvent) Mod() bool {
|
||||
return e.Ctrl || e.Alt || e.Shift
|
||||
}
|
||||
|
||||
func (e MouseEvent) Name() string {
|
||||
name := ""
|
||||
if e.Down {
|
||||
return name
|
||||
}
|
||||
|
||||
if e.Ctrl {
|
||||
name += "ctrl-"
|
||||
}
|
||||
if e.Alt {
|
||||
name += "alt-"
|
||||
}
|
||||
if e.Shift {
|
||||
name += "shift-"
|
||||
}
|
||||
if e.Double {
|
||||
name += "double-"
|
||||
}
|
||||
if !e.Left {
|
||||
name += "right-"
|
||||
}
|
||||
return name + "click"
|
||||
}
|
||||
|
||||
type BorderShape int
|
||||
@@ -376,6 +422,7 @@ const (
|
||||
BorderUndefined BorderShape = iota
|
||||
BorderLine
|
||||
BorderNone
|
||||
BorderPhantom
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderBold
|
||||
@@ -392,7 +439,7 @@ const (
|
||||
|
||||
func (s BorderShape) HasLeft() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -400,7 +447,7 @@ func (s BorderShape) HasLeft() bool {
|
||||
|
||||
func (s BorderShape) HasRight() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -408,7 +455,7 @@ func (s BorderShape) HasRight() bool {
|
||||
|
||||
func (s BorderShape) HasTop() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -416,7 +463,7 @@ func (s BorderShape) HasTop() bool {
|
||||
|
||||
func (s BorderShape) HasBottom() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -441,7 +488,7 @@ type BorderStyle struct {
|
||||
type BorderCharacter int
|
||||
|
||||
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
if shape == BorderNone {
|
||||
if shape == BorderNone || shape == BorderPhantom {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
top: ' ',
|
||||
@@ -579,6 +626,9 @@ type Renderer interface {
|
||||
PassThrough(string)
|
||||
NeedScrollbarRedraw() bool
|
||||
ShouldEmitResizeEvent() bool
|
||||
Bell()
|
||||
HideCursor()
|
||||
ShowCursor()
|
||||
|
||||
GetChar() Event
|
||||
|
||||
@@ -618,6 +668,8 @@ type Window interface {
|
||||
LinkEnd()
|
||||
Erase()
|
||||
EraseMaybe() bool
|
||||
|
||||
SetWrapSign(string, int)
|
||||
}
|
||||
|
||||
type FullscreenRenderer struct {
|
||||
@@ -626,6 +678,7 @@ type FullscreenRenderer struct {
|
||||
forceBlack bool
|
||||
prevDownTime time.Time
|
||||
clicks [][2]int
|
||||
showCursor bool
|
||||
}
|
||||
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||
@@ -634,7 +687,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
||||
mouse: mouse,
|
||||
forceBlack: forceBlack,
|
||||
prevDownTime: time.Unix(0, 0),
|
||||
clicks: [][2]int{}}
|
||||
clicks: [][2]int{},
|
||||
showCursor: true}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -688,6 +742,7 @@ func EmptyTheme() *ColorTheme {
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
@@ -733,6 +788,7 @@ func NoColorTheme() *ColorTheme {
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListBg: ColorAttr{colDefault, AttrUndefined},
|
||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
@@ -778,6 +834,7 @@ func init() {
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
@@ -817,6 +874,7 @@ func init() {
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
@@ -856,6 +914,7 @@ func init() {
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
|
@@ -189,6 +189,27 @@ func (chars *Chars) TrimTrailingWhitespaces() {
|
||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||
lastIdx := len(chars.slice)
|
||||
firstIdx := lastIdx - len(runes)
|
||||
if firstIdx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := firstIdx; i < lastIdx; i++ {
|
||||
char := chars.Get(i)
|
||||
if char != runes[i-firstIdx] {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
chars.slice = chars.slice[0:firstIdx]
|
||||
}
|
||||
|
||||
func (chars *Chars) SliceRight(last int) {
|
||||
chars.slice = chars.slice[:last]
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return string(runes)
|
||||
@@ -273,9 +294,10 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
hasWrapSign := false
|
||||
for {
|
||||
cols := wrapCols
|
||||
if len(wrapped) > 0 {
|
||||
if hasWrapSign {
|
||||
cols -= wrapSignWidth
|
||||
}
|
||||
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
|
||||
@@ -288,9 +310,11 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
return wrapped, true
|
||||
}
|
||||
wrapped = append(wrapped, line[:overflowIdx])
|
||||
hasWrapSign = true
|
||||
line = line[overflowIdx:]
|
||||
continue
|
||||
}
|
||||
hasWrapSign = false
|
||||
|
||||
// Restore trailing '\n'
|
||||
if newline {
|
||||
|
@@ -76,7 +76,7 @@ func TestCharsLines(t *testing.T) {
|
||||
check(true, 100, 3, 1, 1, 8, false)
|
||||
|
||||
// With wrap sign (3 + 2)
|
||||
check(true, 100, 3, 2, 1, 12, false)
|
||||
check(true, 100, 3, 2, 1, 10, false)
|
||||
|
||||
// With wrap sign (3 + 2) and no multi-line
|
||||
check(false, 100, 3, 2, 1, 13, false)
|
||||
|
@@ -6,7 +6,7 @@ import "sync"
|
||||
type EventType int
|
||||
|
||||
// Events is a type that associates EventType to any data
|
||||
type Events map[EventType]interface{}
|
||||
type Events map[EventType]any
|
||||
|
||||
// EventBox is used for coordinating events
|
||||
type EventBox struct {
|
||||
@@ -36,7 +36,7 @@ func (b *EventBox) Wait(callback func(*Events)) {
|
||||
}
|
||||
|
||||
// Set turns on the event type on the box
|
||||
func (b *EventBox) Set(event EventType, value interface{}) {
|
||||
func (b *EventBox) Set(event EventType, value any) {
|
||||
b.cond.L.Lock()
|
||||
b.events[event] = value
|
||||
if _, found := b.ignore[event]; !found {
|
||||
|
240
test/lib/common.rb
Normal file
240
test/lib/common.rb
Normal file
@@ -0,0 +1,240 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'minitest/autorun'
|
||||
require 'fileutils'
|
||||
require 'English'
|
||||
require 'shellwords'
|
||||
require 'erb'
|
||||
require 'tempfile'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
||||
UNSETS = %w[
|
||||
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
||||
FZF_TMUX FZF_TMUX_OPTS
|
||||
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||
FZF_ALT_C_COMMAND
|
||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
FZF_API_KEY
|
||||
].freeze
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
FILE = File.expand_path(__FILE__)
|
||||
BASE = File.expand_path('../..', __dir__)
|
||||
Dir.chdir(BASE)
|
||||
FZF = "FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer \\> --marker \\>\" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf".freeze
|
||||
|
||||
def wait(timeout = DEFAULT_TIMEOUT)
|
||||
since = Time.now
|
||||
begin
|
||||
yield or raise Minitest::Assertion, 'Assertion failure'
|
||||
rescue Minitest::Assertion
|
||||
raise if Time.now - since > timeout
|
||||
|
||||
sleep(0.05)
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
class Shell
|
||||
class << self
|
||||
def bash
|
||||
@bash ||=
|
||||
begin
|
||||
bashrc = '/tmp/fzf.bash'
|
||||
File.open(bashrc, 'w') do |f|
|
||||
f.puts ERB.new(TEMPLATE).result(binding)
|
||||
end
|
||||
|
||||
"bash --rcfile #{bashrc}"
|
||||
end
|
||||
end
|
||||
|
||||
def zsh
|
||||
@zsh ||=
|
||||
begin
|
||||
zdotdir = '/tmp/fzf-zsh'
|
||||
FileUtils.rm_rf(zdotdir)
|
||||
FileUtils.mkdir_p(zdotdir)
|
||||
File.open("#{zdotdir}/.zshrc", 'w') do |f|
|
||||
f.puts ERB.new(TEMPLATE).result(binding)
|
||||
end
|
||||
"ZDOTDIR=#{zdotdir} zsh"
|
||||
end
|
||||
end
|
||||
|
||||
def fish
|
||||
"unset #{UNSETS.join(' ')}; rm -f ~/.local/share/fish/fzf_test_history; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history=fzf_test fish"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Tmux
|
||||
attr_reader :win
|
||||
|
||||
def initialize(shell = :bash)
|
||||
@win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
|
||||
go(%W[set-window-option -t #{@win} pane-base-index 0])
|
||||
return unless shell == :fish
|
||||
|
||||
send_keys 'function fish_prompt; end; clear', :Enter
|
||||
self.until(&:empty?)
|
||||
end
|
||||
|
||||
def kill
|
||||
go(%W[kill-window -t #{win}])
|
||||
end
|
||||
|
||||
def focus
|
||||
go(%W[select-window -t #{win}])
|
||||
end
|
||||
|
||||
def send_keys(*args)
|
||||
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
||||
end
|
||||
|
||||
def paste(str)
|
||||
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
||||
end
|
||||
|
||||
def capture
|
||||
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
||||
end
|
||||
|
||||
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
||||
lines = nil
|
||||
begin
|
||||
wait(timeout) do
|
||||
lines = capture
|
||||
class << lines
|
||||
def counts
|
||||
lazy
|
||||
.map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) }
|
||||
.reject(&:empty?)
|
||||
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
|
||||
end
|
||||
|
||||
def match_count
|
||||
counts[0]
|
||||
end
|
||||
|
||||
def item_count
|
||||
counts[1]
|
||||
end
|
||||
|
||||
def select_count
|
||||
counts[2]
|
||||
end
|
||||
|
||||
def any_include?(val)
|
||||
method = val.is_a?(Regexp) ? :match : :include?
|
||||
find { |line| line.send(method, val) }
|
||||
end
|
||||
end
|
||||
yield(lines).tap do |ok|
|
||||
send_keys 'C-l' if refresh && !ok
|
||||
end
|
||||
end
|
||||
rescue Minitest::Assertion
|
||||
puts $ERROR_INFO.backtrace
|
||||
puts '>' * 80
|
||||
puts lines
|
||||
puts '<' * 80
|
||||
raise
|
||||
end
|
||||
lines
|
||||
end
|
||||
|
||||
def prepare
|
||||
tries = 0
|
||||
begin
|
||||
self.until(true) do |lines|
|
||||
message = "Prepare[#{tries}]"
|
||||
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
||||
lines[-1] == message
|
||||
end
|
||||
rescue Minitest::Assertion
|
||||
(tries += 1) < 5 ? retry : raise
|
||||
end
|
||||
send_keys 'C-u', 'C-l'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def go(args)
|
||||
IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) }
|
||||
end
|
||||
end
|
||||
|
||||
class TestBase < Minitest::Test
|
||||
TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
|
||||
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
|
||||
|
||||
def writelines(lines)
|
||||
File.write(TEMPNAME, lines.join("\n"))
|
||||
end
|
||||
|
||||
def tempname
|
||||
TEMPNAME
|
||||
end
|
||||
|
||||
def fzf_output
|
||||
@thread.join.value.chomp.tap { @thread = nil }
|
||||
end
|
||||
|
||||
def fzf_output_lines
|
||||
fzf_output.lines(chomp: true)
|
||||
end
|
||||
|
||||
def setup
|
||||
File.mkfifo(FIFONAME)
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.rm_f([TEMPNAME, FIFONAME])
|
||||
end
|
||||
|
||||
alias assert_equal_org assert_equal
|
||||
def assert_equal(expected, actual)
|
||||
# Ignore info separator
|
||||
actual = actual&.sub(/\s*─+$/, '') if actual.is_a?(String) && actual&.match?(%r{\d+/\d+})
|
||||
assert_equal_org(expected, actual)
|
||||
end
|
||||
|
||||
# Run fzf with its output piped to a fifo
|
||||
def fzf(*opts)
|
||||
raise 'fzf_output not taken' if @thread
|
||||
|
||||
@thread = Thread.new { File.read(FIFONAME) }
|
||||
fzf!(*opts) + " > #{FIFONAME.shellescape}"
|
||||
end
|
||||
|
||||
def fzf!(*opts)
|
||||
opts = opts.filter_map do |o|
|
||||
case o
|
||||
when Symbol
|
||||
o = o.to_s
|
||||
o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}"
|
||||
when String, Numeric
|
||||
o.to_s
|
||||
end
|
||||
end
|
||||
"#{FZF} #{opts.join(' ')}"
|
||||
end
|
||||
end
|
||||
|
||||
class TestInteractive < TestBase
|
||||
attr_reader :tmux
|
||||
|
||||
def setup
|
||||
super
|
||||
@tmux = Tmux.new
|
||||
end
|
||||
|
||||
def teardown
|
||||
super
|
||||
@tmux.kill
|
||||
end
|
||||
end
|
59
test/lib/common.sh
Normal file
59
test/lib/common.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
set -u
|
||||
PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100
|
||||
unset <%= UNSETS.join(' ') %>
|
||||
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
|
||||
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
|
||||
|
||||
export FZF_DEFAULT_OPTS="--no-scrollbar --pointer '>' --marker '>'"
|
||||
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
|
||||
export PATH="${PATH:+${PATH}:}<%= BASE %>/bin"
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
source "<%= BASE %>/shell/key-bindings.<%= __method__ %>"
|
||||
|
||||
# Old API
|
||||
_fzf_complete_f() {
|
||||
_fzf_complete "+m --multi --prompt \"prompt-f> \"" "$@" < <(
|
||||
echo foo
|
||||
echo bar
|
||||
)
|
||||
}
|
||||
|
||||
# New API
|
||||
_fzf_complete_g() {
|
||||
_fzf_complete +m --multi --prompt "prompt-g> " -- "$@" < <(
|
||||
echo foo
|
||||
echo bar
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_f_post() {
|
||||
awk '{print "f" $0 $0}'
|
||||
}
|
||||
|
||||
_fzf_complete_g_post() {
|
||||
awk '{print "g" $0 $0}'
|
||||
}
|
||||
|
||||
[ -n "${BASH-}" ] && complete -F _fzf_complete_f -o default -o bashdefault f
|
||||
[ -n "${BASH-}" ] && complete -F _fzf_complete_g -o default -o bashdefault g
|
||||
|
||||
_comprun() {
|
||||
local command=$1
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
f) fzf "$@" --preview 'echo preview-f-{}' ;;
|
||||
g) fzf "$@" --preview 'echo preview-g-{}' ;;
|
||||
*) fzf "$@" ;;
|
||||
esac
|
||||
}
|
5
test/runner.rb
Normal file
5
test/runner.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Dir[File.join(__dir__, 'test_*.rb')].each { require it }
|
||||
|
||||
require 'minitest/autorun'
|
1942
test/test_core.rb
Normal file
1942
test/test_core.rb
Normal file
File diff suppressed because it is too large
Load Diff
417
test/test_exec.rb
Normal file
417
test/test_exec.rb
Normal file
@@ -0,0 +1,417 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Process execution: execute, become, reload
|
||||
class TestExec < TestInteractive
|
||||
def test_execute
|
||||
output = '/tmp/fzf-test-execute'
|
||||
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
|
||||
writelines(%w[foo'bar foo"bar foo$bar])
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
|
||||
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :C
|
||||
ready.call('C')
|
||||
|
||||
tmux.send_keys 'barfoo'
|
||||
tmux.until { |lines| assert_equal ' 0/3', lines[-2] }
|
||||
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal %w[
|
||||
/foo'bar/ /foo'barfoo'bar/
|
||||
/foo"bar/ /foo"barfoo"bar/
|
||||
/foo$barfoo$barfoo$bar/
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_multi
|
||||
output = '/tmp/fzf-test-execute-multi'
|
||||
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
|
||||
writelines(%w[foo'bar foo"bar foo$bar foobar])
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
|
||||
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||
|
||||
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :BTab, :BTab, :BTab
|
||||
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal [
|
||||
%(foo'bar/foo'bar),
|
||||
%(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar),
|
||||
%(foo'bar foo"bar foobar/foo'bar foo"bar foobar)
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + '.tmp'
|
||||
FileUtils.rm_f(output)
|
||||
writelines(['foo bar', '123 456'])
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
|
||||
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
|
||||
tmux.send_keys 'xy'
|
||||
tmux.until { |lines| assert_equal ' 0/2 (0)', lines[-2] }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Tab
|
||||
tmux.send_keys 'xy'
|
||||
tmux.until { |lines| assert_equal ' 0/2 (1)', lines[-2] }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal ' 2/2 (1)', lines[-2] }
|
||||
|
||||
tmux.send_keys :Tab
|
||||
tmux.send_keys 'xy'
|
||||
tmux.until { |lines| assert_equal ' 0/2 (2)', lines[-2] }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] }
|
||||
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal [
|
||||
%(foo bar/foo bar/bar/bar),
|
||||
%(123 456/foo bar/456/bar),
|
||||
%(123 456 foo bar/foo bar/456 bar/bar)
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
rescue StandardError
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
FileUtils.rm_f(output)
|
||||
writelines(['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
|
||||
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_interrupt_execute
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys 'C-l'
|
||||
tmux.until { |lines| assert lines.any_include?('executing 1') }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys 99
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_abort
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys FZF.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_accept
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', fzf_output
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_abort
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{FZF} --bind 'ctrl-r:reload(#{tempname})'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_accept
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{tempname})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', fzf_output
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_reload
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq $FZF_QUERY),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
|
||||
tmux.until { |lines| assert_equal 998, lines.match_count }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until do |lines|
|
||||
assert_equal 98, lines.item_count
|
||||
assert_equal 98, lines.match_count
|
||||
end
|
||||
tmux.send_keys 'b'
|
||||
tmux.until do |lines|
|
||||
assert_equal 198, lines.item_count
|
||||
assert_equal 198, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal ' 198/198 (1/2)', lines[-2] }
|
||||
tmux.send_keys '555'
|
||||
tmux.until { |lines| assert_equal ' 1/553 (0/2)', lines[-2] }
|
||||
end
|
||||
|
||||
def test_reload_even_when_theres_no_match
|
||||
tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
end
|
||||
|
||||
def test_reload_should_terminate_standard_input_stream
|
||||
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
end
|
||||
|
||||
def test_clear_list_when_header_lines_changed_due_to_reload
|
||||
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
|
||||
tmux.until { |lines| assert_includes lines, ' 9' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| refute_includes lines, ' 9' }
|
||||
end
|
||||
|
||||
def test_item_index_reset_on_reload
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[[9]]' }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_includes lines[1], '[[0]]'
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
|
||||
end
|
||||
|
||||
def test_reload_should_update_preview
|
||||
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], '1' }
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||
end
|
||||
|
||||
def test_reload_and_change_preview_should_update_preview
|
||||
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||
end
|
||||
|
||||
def test_reload_sync
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys '00'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
# After 1 second
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case1
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case2
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case3
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case4
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case5
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(echo xx; sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1001, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case6
|
||||
tmux.send_keys "seq 1000 | #{FZF} --disabled --bind 'change:reload:sleep 0.5; seq {q}'", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_equal 9, lines.match_count }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_equal 99, lines.match_count }
|
||||
|
||||
# TODO: How do we verify if an intermediate empty list is not shown?
|
||||
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_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
|
||||
|
||||
def test_start_on_reload
|
||||
tmux.send_keys %(echo foo | #{FZF} --header Loading --header-lines 1 --bind 'start:reload:sleep 2; echo bar' --bind 'load:change-header:Loaded' --bind space:change-header:), :Enter
|
||||
tmux.until(timeout: 1) { |lines| assert_includes lines[-3], 'Loading' }
|
||||
tmux.until(timeout: 1) { |lines| refute_includes lines[-4], 'foo' }
|
||||
tmux.until { |lines| assert_includes lines[-3], 'Loaded' }
|
||||
tmux.until { |lines| assert_includes lines[-4], 'bar' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-3], 'bar' }
|
||||
end
|
||||
|
||||
def test_become
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_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
|
||||
end
|
315
test/test_filter.rb
Normal file
315
test/test_filter.rb
Normal file
@@ -0,0 +1,315 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Non-interactive tests
|
||||
class TestFilter < TestBase
|
||||
def test_default_extended
|
||||
assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp
|
||||
assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp
|
||||
end
|
||||
|
||||
def test_exact
|
||||
assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length
|
||||
assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length
|
||||
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
|
||||
end
|
||||
|
||||
def test_or_operator
|
||||
assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines(chomp: true)
|
||||
assert_equal %w[1 10 2 3 4 5 6 7 8 9],
|
||||
`seq 10 | #{FZF} -f '1 | !1'`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_smart_case_for_each_term
|
||||
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
|
||||
end
|
||||
|
||||
def test_filter_exitstatus
|
||||
# filter / streaming filter
|
||||
['', '--no-sort'].each do |opts|
|
||||
assert_includes `echo foo | #{FZF} -f foo #{opts}`, 'foo'
|
||||
assert_equal 0, $CHILD_STATUS.exitstatus
|
||||
|
||||
assert_empty `echo foo | #{FZF} -f bar #{opts}`
|
||||
assert_equal 1, $CHILD_STATUS.exitstatus
|
||||
end
|
||||
end
|
||||
|
||||
def test_long_line
|
||||
data = '.' * 256 * 1024
|
||||
File.open(tempname, 'w') do |f|
|
||||
f << data
|
||||
end
|
||||
assert_equal data, `#{FZF} -f . < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_read0
|
||||
lines = `find .`.lines(chomp: true)
|
||||
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
|
||||
assert_equal \
|
||||
lines.last,
|
||||
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
|
||||
end
|
||||
|
||||
def test_nth_suffix_match
|
||||
assert_equal \
|
||||
'foo,bar,baz',
|
||||
`echo foo,bar,baz | #{FZF} -d, -f'bar$' -n2`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_basic
|
||||
writelines(['hello world ', 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_template
|
||||
writelines(['hello world ', 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_ansi
|
||||
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_no_ansi
|
||||
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
|
||||
writelines([src, 'byebye'])
|
||||
assert_equal \
|
||||
src,
|
||||
`#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_escaped_meta_characters
|
||||
input = [
|
||||
'foo^bar',
|
||||
'foo$bar',
|
||||
'foo!bar',
|
||||
"foo'bar",
|
||||
'foo bar',
|
||||
'bar foo'
|
||||
]
|
||||
writelines(input)
|
||||
|
||||
assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length
|
||||
assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length
|
||||
assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines(chomp: true)
|
||||
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines(chomp: true)
|
||||
assert_equal input.length - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.length
|
||||
end
|
||||
|
||||
def test_normalized_match
|
||||
echoes = '(echo a; echo á; echo A; echo Á;)'
|
||||
assert_equal %w[a á A Á], `#{echoes} | #{FZF} -f a`.lines.map(&:chomp)
|
||||
assert_equal %w[á Á], `#{echoes} | #{FZF} -f á`.lines.map(&:chomp)
|
||||
assert_equal %w[A Á], `#{echoes} | #{FZF} -f A`.lines.map(&:chomp)
|
||||
assert_equal %w[Á], `#{echoes} | #{FZF} -f Á`.lines.map(&:chomp)
|
||||
end
|
||||
|
||||
def test_unicode_case
|
||||
writelines(%w[строКА1 СТРОКА2 строка3 Строка4])
|
||||
assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true)
|
||||
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak
|
||||
input = %w[
|
||||
--foobar--------
|
||||
-----foobar---
|
||||
----foobar--
|
||||
-------foobar-
|
||||
]
|
||||
writelines(input)
|
||||
|
||||
assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true)
|
||||
|
||||
by_length = %w[
|
||||
----foobar--
|
||||
-----foobar---
|
||||
-------foobar-
|
||||
--foobar--------
|
||||
]
|
||||
assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.lines(chomp: true)
|
||||
assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.lines(chomp: true)
|
||||
|
||||
by_begin = %w[
|
||||
--foobar--------
|
||||
----foobar--
|
||||
-----foobar---
|
||||
-------foobar-
|
||||
]
|
||||
assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.lines(chomp: true)
|
||||
assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal %w[
|
||||
-------foobar-
|
||||
----foobar--
|
||||
-----foobar---
|
||||
--foobar--------
|
||||
], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_index_begin
|
||||
writelines([
|
||||
'xoxxxxxoxx',
|
||||
'xoxxxxxox',
|
||||
'xxoxxxoxx',
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
' xxoxoxxx'
|
||||
])
|
||||
|
||||
assert_equal [
|
||||
'xxxxoxox',
|
||||
' xxoxoxxx',
|
||||
'xxxoxoxxx',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxox',
|
||||
'xoxxxxxoxx'
|
||||
], `#{FZF} -foo < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
' xxoxoxxx',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxoxx',
|
||||
'xoxxxxxox'
|
||||
], `#{FZF} -foo --tiebreak=index < #{tempname}`.lines(chomp: true)
|
||||
|
||||
# Note that --tiebreak=begin is now based on the first occurrence of the
|
||||
# first character on the pattern
|
||||
assert_equal [
|
||||
' xxoxoxxx',
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxoxx',
|
||||
'xoxxxxxox'
|
||||
], `#{FZF} -foo --tiebreak=begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
' xxoxoxxx',
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxox',
|
||||
'xoxxxxxoxx'
|
||||
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_begin_algo_v2
|
||||
writelines(['baz foo bar',
|
||||
'foo bar baz'])
|
||||
assert_equal [
|
||||
'foo bar baz',
|
||||
'baz foo bar'
|
||||
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_end
|
||||
writelines(['xoxxxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xxxoxxxxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxxxoxxx',
|
||||
' xxxxoxxx'])
|
||||
|
||||
assert_equal [
|
||||
' xxxxoxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxxxoxxx',
|
||||
'xoxxxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xxxoxxxxxx'
|
||||
], `#{FZF} -fo < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'xxxxxoxxx',
|
||||
' xxxxoxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxoxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xoxxxxxxxx'
|
||||
], `#{FZF} -fo --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'xxxxxoxxx',
|
||||
' xxxxoxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxoxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xoxxxxxxxx'
|
||||
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
writelines(['/bar/baz', '/foo/bar/baz'])
|
||||
assert_equal [
|
||||
'/foo/bar/baz',
|
||||
'/bar/baz'
|
||||
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_length_with_nth
|
||||
input = %w[
|
||||
1:hell
|
||||
123:hello
|
||||
12345:he
|
||||
1234567:h
|
||||
]
|
||||
writelines(input)
|
||||
|
||||
output = %w[
|
||||
1:hell
|
||||
12345:he
|
||||
123:hello
|
||||
1234567:h
|
||||
]
|
||||
assert_equal output, `#{FZF} -fh < #{tempname}`.lines(chomp: true)
|
||||
|
||||
# Since 0.16.8, --nth doesn't affect --tiebreak
|
||||
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_chunk
|
||||
writelines(['1 foobarbaz ba',
|
||||
'2 foobar baz',
|
||||
'3 foo barbaz'])
|
||||
|
||||
assert_equal [
|
||||
'3 foo barbaz',
|
||||
'2 foobar baz',
|
||||
'1 foobarbaz ba'
|
||||
], `#{FZF} -fo --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'1 foobarbaz ba',
|
||||
'2 foobar baz',
|
||||
'3 foo barbaz'
|
||||
], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'3 foo barbaz'
|
||||
], `#{FZF} -f'!foobar' --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_boundary_match
|
||||
# Underscore boundaries should be ranked lower
|
||||
{
|
||||
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
|
||||
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
|
||||
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
|
||||
}.each do |scheme, expected|
|
||||
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
|
||||
assert_equal expected, result
|
||||
end
|
||||
end
|
||||
end
|
4323
test/test_go.rb
4323
test/test_go.rb
File diff suppressed because it is too large
Load Diff
1006
test/test_layout.rb
Normal file
1006
test/test_layout.rb
Normal file
File diff suppressed because it is too large
Load Diff
561
test/test_preview.rb
Normal file
561
test/test_preview.rb
Normal file
@@ -0,0 +1,561 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Test cases for preview
|
||||
class TestPreview < TestInteractive
|
||||
def test_preview
|
||||
tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], ' {-} ' }
|
||||
tmux.send_keys '555'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[1], ' {555-555} ' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert lines[-2]&.start_with?(' 28/1000 ') }
|
||||
tmux.send_keys 'foobar'
|
||||
tmux.until { |lines| refute_includes lines[1], ' {55-55} ' }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {-1} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {3-1 } ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {4-1 3} ' }
|
||||
tmux.send_keys :BTab
|
||||
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] }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_match(/ {1-1-1-[0-9]+}/, lines[-2]) }
|
||||
tmux.send_keys '555'
|
||||
tmux.until { |lines| assert_match(/ {555-555-1-[0-9]+}/, lines[-2]) }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_equal '> 555', lines[-1] }
|
||||
end
|
||||
|
||||
def test_preview_size_0
|
||||
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.match_count
|
||||
assert_equal ' 100/100', lines[1]
|
||||
assert_equal '> 1', lines[2]
|
||||
end
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down, :Down
|
||||
tmux.until { |lines| assert_equal '> 3', lines[4] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down
|
||||
tmux.until { |lines| assert_equal '> 4', lines[5] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1 3 4], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_size_0_hidden
|
||||
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.match_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
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 //0/0} ' }
|
||||
tmux.send_keys '123'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {////123//} ' }
|
||||
tmux.send_keys 'C-u', '1'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 /1/0/0} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1/1 /1/9/0} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1 10/1 10 /1/9/0 9} ' }
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /12//0 9} ' }
|
||||
tmux.send_keys '3'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
|
||||
end
|
||||
|
||||
def test_preview_file
|
||||
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar foobarfoo0101 ' }
|
||||
end
|
||||
|
||||
def test_preview_q_no_match
|
||||
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q} foo'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||
tmux.send_keys 'bar'
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo bar foo' }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||
end
|
||||
|
||||
def test_preview_q_no_match_with_initial_query
|
||||
tmux.send_keys %(: | #{FZF} --preview 'echo 1. /{q}/{q:1}/; echo 2. /{q:..}/{q:2}/{q:-1}/; echo 3. /{q:s-2}/{q:-2}/{q:x}/' --query 'foo bar'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], '1. /foo bar/foo/' }
|
||||
tmux.until { |lines| assert_includes lines[2], '2. /foo bar/bar/bar/' }
|
||||
tmux.until { |lines| assert_includes lines[3], '3. /foo /foo/{q:x}/' }
|
||||
end
|
||||
|
||||
def test_preview_update_on_select
|
||||
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
|
||||
:Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?(' 1 2 3 4 5 ') }) }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| lines.each { |line| refute_includes line, ' 1 2 3 4 5 ' } }
|
||||
end
|
||||
|
||||
def test_preview_correct_tab_width_after_ansi_reset_code
|
||||
writelines(["\x1b[31m+\x1b[m\t\x1b[32mgreen"])
|
||||
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' + green ' }
|
||||
end
|
||||
|
||||
def test_preview_bindings_with_default_preview
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [{}]' --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
|
||||
tmux.until { |lines| lines.match_count == 10 }
|
||||
tmux.until { |lines| assert_includes lines[1], '[1]' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||
tmux.send_keys 'c'
|
||||
tmux.until { |lines| assert_includes lines[1], '[1]' }
|
||||
tmux.send_keys 'b'
|
||||
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[2]' }
|
||||
end
|
||||
|
||||
def test_preview_bindings_without_default_preview
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
|
||||
tmux.until { |lines| lines.match_count == 10 }
|
||||
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||
tmux.send_keys 'c' # does nothing
|
||||
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||
tmux.send_keys 'b'
|
||||
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||
tmux.send_keys 9
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.until { |lines| refute_includes lines[1], '2' }
|
||||
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||
end
|
||||
|
||||
def test_preview_scroll_begin_constant
|
||||
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+123", :Enter
|
||||
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||
tmux.until { |lines| assert_match %r{123.*123/1000}, lines[1] }
|
||||
end
|
||||
|
||||
def test_preview_scroll_begin_expr
|
||||
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{3}", :Enter
|
||||
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||
tmux.until { |lines| assert_match %r{321.*321/1000}, lines[1] }
|
||||
end
|
||||
|
||||
def test_preview_scroll_begin_and_offset
|
||||
['echo foo 123 321', 'echo foo :123: 321'].each do |input|
|
||||
tmux.send_keys "#{input} | #{FZF} --preview 'seq 1000' --preview-window left:+{2}-2", :Enter
|
||||
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||
tmux.until { |lines| assert_match %r{121.*121/1000}, lines[1] }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_clear_screen
|
||||
tmux.send_keys %{seq 100 | #{FZF} --preview 'for i in $(seq 300); do (( i % 200 == 0 )) && printf "\\033[2J"; echo "[$i]"; sleep 0.001; done'}, :Enter
|
||||
tmux.until { |lines| lines.match_count == 100 }
|
||||
tmux.until { |lines| lines[1]&.include?('[200]') }
|
||||
end
|
||||
|
||||
def test_preview_window_follow
|
||||
file = Tempfile.new('fzf-follow')
|
||||
file.sync = true
|
||||
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo start; tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~4'), :Enter
|
||||
tmux.until { |lines| lines.match_count == 100 }
|
||||
|
||||
# Write to the temporary file, and check if the preview window is showing
|
||||
# the last line of the file
|
||||
tmux.until { |lines| assert_includes lines[1], 'start' }
|
||||
3.times { file.puts _1 } # header lines
|
||||
1000.times { file.puts _1 }
|
||||
tmux.until { |lines| assert_includes lines[1], '/1004' }
|
||||
tmux.until { |lines| assert_includes lines[-2], '999' }
|
||||
|
||||
# Scroll the preview window and fzf should stop following the file content
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[-2], '998' }
|
||||
file.puts 'foo', 'bar'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1006'
|
||||
assert_includes lines[-2], '998'
|
||||
end
|
||||
|
||||
# Scroll back to the bottom and fzf should start following the file again
|
||||
%w[999 foo bar].each do |item|
|
||||
wait do
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines[-2], item }
|
||||
end
|
||||
end
|
||||
file.puts 'baz'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1007'
|
||||
assert_includes lines[-2], 'baz'
|
||||
end
|
||||
|
||||
# Scroll upwards to stop following
|
||||
tmux.send_keys :Up
|
||||
wait { assert_includes lines[-2], 'bar' }
|
||||
file.puts 'aaa'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1008'
|
||||
assert_includes lines[-2], 'bar'
|
||||
end
|
||||
|
||||
# Manually enable following
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
|
||||
file.puts 'bbb'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1009'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
|
||||
# Disable following
|
||||
tmux.send_keys :Space
|
||||
file.puts 'ccc', 'ddd'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1011'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
rescue StandardError
|
||||
file.close
|
||||
file.unlink
|
||||
end
|
||||
|
||||
def test_toggle_preview_wrap
|
||||
tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter
|
||||
2.times do
|
||||
tmux.until { |lines| assert_includes lines[2], '2nd line' }
|
||||
tmux.send_keys 'C-w'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[2], 'wrapped'
|
||||
assert_includes lines[3], '2nd line'
|
||||
end
|
||||
tmux.send_keys 'C-w'
|
||||
end
|
||||
end
|
||||
|
||||
def test_close
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], 'foo' }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| refute_includes lines[1], 'foo' }
|
||||
tmux.send_keys '10'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
end
|
||||
|
||||
def test_preview_header
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '4/1000'
|
||||
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||
end
|
||||
tmux.send_keys '55'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-J'
|
||||
tmux.until do |lines|
|
||||
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 19, lines.match_count
|
||||
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-K'
|
||||
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||
end
|
||||
|
||||
def test_change_preview_window
|
||||
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --no-preview-border --bind '" \
|
||||
'a:change-preview(echo __{}__),' \
|
||||
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
|
||||
'c:change-preview(),d:change-preview-window(hidden),' \
|
||||
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
|
||||
|
||||
# change-preview action permanently changes the preview command set by --preview
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[0], '__1__' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[0], '__2__' }
|
||||
|
||||
# When multiple change-preview-window actions are bound to a single key,
|
||||
# the last one wins and the updated options are immediately applied to the new preview
|
||||
tmux.send_keys 'b'
|
||||
tmux.until { |lines| assert_equal '==2==', lines[0] }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal '==3==', lines[0] }
|
||||
|
||||
# change-preview with an empty preview command closes the preview window
|
||||
tmux.send_keys 'c'
|
||||
tmux.until { |lines| refute_includes lines[0], '==' }
|
||||
|
||||
# change-preview again to re-open the preview window
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_equal '__3__', lines[0] }
|
||||
|
||||
# Hide the preview window with hidden flag
|
||||
tmux.send_keys 'd'
|
||||
tmux.until { |lines| refute_includes lines[0], '__3__' }
|
||||
|
||||
# One-off preview
|
||||
tmux.send_keys 'e'
|
||||
tmux.until do |lines|
|
||||
assert_equal '::', lines[0]
|
||||
refute_includes lines[1], '3'
|
||||
end
|
||||
|
||||
# Wrapped
|
||||
tmux.send_keys 'f'
|
||||
tmux.until do |lines|
|
||||
assert_equal '::', lines[0]
|
||||
assert_equal '↳ 3', lines[1]
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_preview_window_should_not_reset_change_preview
|
||||
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
|
||||
tmux.until { |lines| assert_includes lines, 'hello' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_includes lines, '│ hello' }
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
|
||||
"a:change-preview-window(right|down|up|hidden|)'", :Enter
|
||||
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
|
||||
3.times do
|
||||
tmux.until { |lines| lines[0].start_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| lines[0].end_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| lines[-1].start_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_equal 'hello', lines[0] }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[0], 'hello' }
|
||||
tmux.send_keys 'a'
|
||||
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_toggle_alternative_preview_window
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines, '/1/1/' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||
end
|
||||
|
||||
def test_alternative_preview_window_opts
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.until do |lines|
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_width_exception
|
||||
tmux.send_keys "seq 10 | #{FZF} --scrollbar --preview-window border-left --border --preview 'seq 1000'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.end_with?(' 1/1000││')
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_hidden_on_focus
|
||||
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
end
|
||||
|
||||
def test_preview_query_should_not_be_affected_by_search
|
||||
tmux.send_keys "seq 1 | #{FZF} --bind 'change:transform-search(echo {q:1})' --preview 'echo [{q}/{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| assert lines.any_include?('[1/1]') }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert lines.any_include?('[1 /1]') }
|
||||
tmux.send_keys '2'
|
||||
tmux.until do |lines|
|
||||
assert lines.any_include?('[1 2/1]')
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
end
|
||||
end
|
52
test/test_server.rb
Normal file
52
test/test_server.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Test cases for API server
|
||||
class TestServer < TestInteractive
|
||||
def test_listen
|
||||
{ '--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.match_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.match_count }
|
||||
# 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
|
||||
end
|
517
test/test_shell_integration.rb
Normal file
517
test/test_shell_integration.rb
Normal file
@@ -0,0 +1,517 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Testing shell integration
|
||||
module TestShell
|
||||
attr_reader :tmux
|
||||
|
||||
def setup
|
||||
@tmux = Tmux.new(shell)
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def teardown
|
||||
@tmux.kill
|
||||
end
|
||||
|
||||
def set_var(name, val)
|
||||
tmux.prepare
|
||||
tmux.send_keys "export #{name}='#{val}'", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def unset_var(name)
|
||||
tmux.prepare
|
||||
tmux.send_keys "unset #{name}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_ctrl_t
|
||||
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| assert lines.any_include?(' (3)') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('1 2 3') }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
|
||||
def test_ctrl_t_unicode
|
||||
writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
|
||||
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo ', 'C-t'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.send_keys 'fzf-unicode'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
|
||||
end
|
||||
|
||||
def test_alt_c
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.end_with?(expected) }
|
||||
end
|
||||
|
||||
def test_alt_c_command
|
||||
set_var('FZF_ALT_C_COMMAND', 'echo /tmp')
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /', :Enter
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
tmux.until { |lines| assert_equal '/tmp', lines[-1] }
|
||||
end
|
||||
|
||||
def test_ctrl_r
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo 1st', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo 2nd', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo 3d', :Enter
|
||||
tmux.prepare
|
||||
3.times do
|
||||
tmux.send_keys 'echo 3rd', :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
tmux.send_keys 'echo 4th', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'e3d'
|
||||
# Duplicates removed: 3d (1) + 3rd (1) => 2 matches
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal '3rd', lines[-1] }
|
||||
end
|
||||
|
||||
def test_ctrl_r_multiline
|
||||
# NOTE: Current bash implementation shows an extra new line if there's
|
||||
# only entry in the history
|
||||
tmux.send_keys ':', :Enter
|
||||
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
tmux.send_keys 'foo bar'
|
||||
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
|
||||
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
end
|
||||
|
||||
def test_ctrl_r_abort
|
||||
skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4
|
||||
%w[foo ' "].each do |query|
|
||||
tmux.prepare
|
||||
tmux.send_keys :Enter, query
|
||||
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
|
||||
tmux.send_keys 'C-g'
|
||||
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module CompletionTest
|
||||
def test_file_completion
|
||||
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||
FileUtils.mkdir_p('/tmp/fzf test')
|
||||
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
|
||||
['no~such~user', '/tmp/fzf test/foobar'].each do |f|
|
||||
FileUtils.touch(File.expand_path(f))
|
||||
end
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys ' !d'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) do |lines|
|
||||
assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1]
|
||||
end
|
||||
|
||||
# ~USERNAME**<TAB>
|
||||
user = `whoami`.chomp
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~#{user}**", :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys "/#{user}"
|
||||
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) do |lines|
|
||||
assert_match %r{cat .*/#{user}}, lines[-1]
|
||||
end
|
||||
|
||||
# ~INVALID_USERNAME**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat ~such**', :Tab
|
||||
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
|
||||
|
||||
# /tmp/fzf\ test**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'foobar$'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert lines.any_include?('> /tmp/fzf test/foobar')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
|
||||
|
||||
# Should include hidden files
|
||||
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
|
||||
tmux.until(true) do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert lines.any_include?('/tmp/fzf-test/.hidden-')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
ensure
|
||||
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
|
||||
FileUtils.rm_rf(File.expand_path(f))
|
||||
end
|
||||
end
|
||||
|
||||
def test_file_completion_root
|
||||
tmux.send_keys 'ls /**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
|
||||
def test_dir_completion
|
||||
(1..100).each do |idx|
|
||||
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
|
||||
end
|
||||
FileUtils.touch('/tmp/fzf-test/d55/xxx')
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys :Tab, :Tab # Tab does not work here
|
||||
tmux.send_keys 55
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
assert_includes lines, '> /tmp/fzf-test/d55/'
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
||||
tmux.send_keys :xx
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
||||
|
||||
# Should not match regular files (bash-only)
|
||||
if instance_of?(TestBash)
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
||||
end
|
||||
|
||||
# Fail back to plusdirs
|
||||
tmux.send_keys :BSpace, :BSpace, :BSpace
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
||||
end
|
||||
|
||||
def test_process_completion
|
||||
tmux.send_keys 'sleep 12345 &', :Enter
|
||||
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
|
||||
pid = lines[-1]&.split&.last
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.send_keys 'kill **', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'sleep12345'
|
||||
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
|
||||
ensure
|
||||
if pid
|
||||
begin
|
||||
Process.kill('KILL', pid.to_i)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_completion
|
||||
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'ls /tmp/**', :Tab
|
||||
tmux.until { |lines| assert_equal 11, lines.match_count }
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] }
|
||||
end
|
||||
|
||||
def test_unset_completion
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
tmux.prepare
|
||||
|
||||
# Using tmux
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||
tmux.send_keys 'C-c'
|
||||
|
||||
# FZF_TMUX=1
|
||||
new_shell
|
||||
tmux.focus
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||
end
|
||||
|
||||
def test_completion_in_command_sequence
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
tmux.prepare
|
||||
|
||||
triggers = ['**', '~~', '++', 'ff', '/']
|
||||
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
|
||||
|
||||
triggers.each do |trigger|
|
||||
set_var('FZF_COMPLETION_TRIGGER', trigger)
|
||||
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
|
||||
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_file_completion_unicode
|
||||
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cat fzf-unicode**', :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
|
||||
end
|
||||
|
||||
def test_custom_completion_api
|
||||
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
|
||||
%w[f g].each do |command|
|
||||
tmux.prepare
|
||||
tmux.send_keys "#{command} b**", :Tab
|
||||
tmux.until do |lines|
|
||||
assert_equal 2, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
assert lines.any_include?("prompt-#{command}")
|
||||
assert lines.any_include?("preview-#{command}-bar")
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
|
||||
tmux.send_keys 'C-u'
|
||||
end
|
||||
ensure
|
||||
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_operator 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_operator 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_operator lines.match_count, :>=, 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestBash < TestBase
|
||||
include TestShell
|
||||
include CompletionTest
|
||||
|
||||
def shell
|
||||
:bash
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.prepare
|
||||
tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_dynamic_completion_loader
|
||||
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
|
||||
tmux.paste '_completion_loader() { complete -o default fake; }'
|
||||
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
|
||||
tmux.send_keys 'fake /tmp/foo**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'C-c'
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'fake /tmp/foo'
|
||||
tmux.send_keys :Tab, 'C-u'
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'fake /tmp/foo**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
end
|
||||
end
|
||||
|
||||
class TestZsh < TestBase
|
||||
include TestShell
|
||||
include CompletionTest
|
||||
|
||||
def shell
|
||||
:zsh
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_complete_quoted_command
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
['unset', '\unset', "'unset'"].each do |command|
|
||||
tmux.prepare
|
||||
tmux.send_keys "#{command} FZFFOOBR**", :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestFish < TestBase
|
||||
include TestShell
|
||||
|
||||
def shell
|
||||
:fish
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
|
||||
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
||||
tmux.until { |lines| assert_empty lines }
|
||||
end
|
||||
|
||||
def set_var(name, val)
|
||||
tmux.prepare
|
||||
tmux.send_keys "set -g #{name} '#{val}'", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_ctrl_r_multi
|
||||
tmux.send_keys ':', :Enter
|
||||
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-l', 'C-r'
|
||||
block = <<~BLOCK
|
||||
echo "foo
|
||||
bar"
|
||||
echo "bar
|
||||
foo"
|
||||
BLOCK
|
||||
tmux.until do |lines|
|
||||
block.lines.each_with_index do |line, idx|
|
||||
assert_includes lines[-6 + idx], line.chomp
|
||||
end
|
||||
end
|
||||
tmux.send_keys :BTab, :BTab
|
||||
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||
tmux.send_keys :Enter
|
||||
block = <<~BLOCK
|
||||
echo "bar
|
||||
foo"
|
||||
echo "foo
|
||||
bar"
|
||||
BLOCK
|
||||
tmux.until do |lines|
|
||||
assert_equal block.lines.map(&:chomp), lines
|
||||
end
|
||||
end
|
||||
end
|
@@ -13,7 +13,7 @@ Execute (fzf#run with dir option):
|
||||
|
||||
execute 'lcd' fnameescape(cwd)
|
||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||
AssertEqual ['fzf.vader'], result
|
||||
AssertEqual 1, haslocaldir()
|
||||
AssertEqual getcwd(), cwd
|
||||
|
||||
@@ -23,8 +23,8 @@ Execute (fzf#run with Funcref command):
|
||||
call add(g:ret, a:e)
|
||||
endfunction
|
||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
||||
AssertEqual ['fzf.vader'], result
|
||||
AssertEqual ['fzf.vader'], sort(g:ret)
|
||||
|
||||
Execute (fzf#run with string source):
|
||||
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
||||
@@ -78,18 +78,18 @@ Execute (fzf#wrap):
|
||||
|
||||
let opts = fzf#wrap('foobar')
|
||||
Log opts
|
||||
AssertEqual '~40%', opts.down
|
||||
AssertEqual 0.9, opts.window.width
|
||||
Assert opts.options =~ '--expect='
|
||||
Assert !has_key(opts, 'sink')
|
||||
Assert has_key(opts, 'sink*')
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 0)
|
||||
Log opts
|
||||
AssertEqual '~40%', opts.down
|
||||
AssertEqual 0.9, opts.window.width
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 1)
|
||||
Log opts
|
||||
Assert !has_key(opts, 'down')
|
||||
Assert !has_key(opts, 'window')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'down': '50%'})
|
||||
Log opts
|
||||
@@ -148,7 +148,7 @@ Execute (fzf#wrap):
|
||||
|
||||
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
||||
let opts = fzf#wrap({})
|
||||
Assert opts.options =~ '^--color=fg:'
|
||||
Assert opts.options =~ '--color=fg:'
|
||||
|
||||
Execute (fzf#shellescape with sh):
|
||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
Reference in New Issue
Block a user