Compare commits

...

16 Commits

Author SHA1 Message Date
Junegunn Choi
352ea07226 0.38.0 2023-02-15 23:24:42 +09:00
Junegunn Choi
27018787af Describe become(...) action and use it to simplify examples 2023-02-15 23:24:42 +09:00
Junegunn Choi
4e305eca26 become: Set stdin to /dev/tty 2023-02-15 23:24:42 +09:00
sitiom
9e9c0ceaf4 Add Winget Releaser workflow (#3164) 2023-02-15 16:47:12 +09:00
Junegunn Choi
b3bf18b1c0 [fzf-tmux] Fix version check
The output of `tmux -V` starts with "tmux ".
2023-02-13 15:25:39 +09:00
Junegunn Choi
b1619f675f [fzf-tmux] Do not set --margin 0,1 on tmux 3.3 or above
Close #3162
2023-02-13 14:49:02 +09:00
Junegunn Choi
96c3de12eb Run 'become' only when the command template is properly evaluated 2023-02-12 22:06:21 +09:00
Junegunn Choi
719dbb8bae Update ADVANCED.md: transform-query to restore the query string
Close #2961
2023-02-12 17:23:17 +09:00
Junegunn Choi
f38a7f7f8f [bash] Enable environment variable completion for printenv
Close #3145
2023-02-12 16:58:36 +09:00
Junegunn Choi
6ea38b4438 Add become(...) action that replaces current fzf process
Close #3159
2023-02-11 20:26:31 +09:00
Junegunn Choi
f7447aece1 Code cleanup 2023-02-01 18:16:58 +09:00
Junegunn Choi
aa2b9ec476 Add 'show-preview' and 'hide-preview'
For cases where 'toggle-preview' is not enough
2023-01-31 17:34:11 +09:00
Junegunn Choi
3ee00f8bc2 toggle-preview should not show empty preview window 2023-01-30 22:13:29 +09:00
Junegunn Choi
fccab60a5c --preview-window 0,hidden should not execute the preview command
Until `toggle-preview` action is triggered

Fix #3149
2023-01-30 21:39:18 +09:00
Junegunn Choi
0f4af38457 [vim] Simplify --border injection
Prepend the border options so that the user can override them in
'options' entry of the spec.
2023-01-27 14:00:22 +09:00
Junegunn Choi
aef39f1160 [vim] Fix missing --border when --border-label is present 2023-01-27 11:00:59 +09:00
17 changed files with 411 additions and 185 deletions

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

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

View File

@@ -1,30 +1,33 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
*(Last update: 2022/08/25)* * *Last update: 2023/02/15*
* *Requires fzf 0.38.0 or above*
---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Introduction](#introduction) * [Introduction](#introduction)
* [Screen Layout](#screen-layout) * [Screen Layout](#screen-layout)
* [`--height`](#--height) * [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux) * [`fzf-tmux`](#fzf-tmux)
* [Popup window support](#popup-window-support) * [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list) * [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r) * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources) * [Toggling between data sources](#toggling-between-data-sources)
* [Ripgrep integration](#ripgrep-integration) * [Ripgrep integration](#ripgrep-integration)
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter) * [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher) * [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode) * [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Log tailing](#log-tailing) * [Log tailing](#log-tailing)
* [Key bindings for git objects](#key-bindings-for-git-objects) * [Key bindings for git objects](#key-bindings-for-git-objects)
* [Files listed in `git status`](#files-listed-in-git-status) * [Files listed in `git status`](#files-listed-in-git-status)
* [Branches](#branches) * [Branches](#branches)
* [Commit hashes](#commit-hashes) * [Commit hashes](#commit-hashes)
* [Color themes](#color-themes) * [Color themes](#color-themes)
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes) * [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
<!-- vim-markdown-toc --> <!-- vim-markdown-toc -->
@@ -236,15 +239,13 @@ file called `rfv`.
# 1. Search for text in files using Ripgrep # 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf # 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim # 3. Open the file in Vim
IFS=: read -ra selected < <( rg --color=always --line-number --no-heading --smart-case "${*:-}" |
rg --color=always --line-number --no-heading --smart-case "${*:-}" | fzf --ansi \
fzf --ansi \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --delimiter : \
--delimiter : \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --bind 'enter:become(vim {1} +{2})'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
And run it with an initial query string. And run it with an initial query string.
@@ -307,8 +308,12 @@ I know it's a lot to digest, let's try to break down the code.
position in the window position in the window
- `~3` makes the top three lines fixed header so that they are always - `~3` makes the top three lines fixed header so that they are always
visible regardless of the scroll offset visible regardless of the scroll offset
- Once we selected a line, we open the file with `vim` (`vim - Instead of using shell script to process the final output of fzf, we use
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`). `become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
into a new process that opens the file with `vim` (`vim {1}`) and move the
cursor to the line (`+{2}`).
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
### Using fzf as interactive Ripgrep launcher ### Using fzf as interactive Ripgrep launcher
@@ -331,16 +336,14 @@ projects, and it will free up memory as you narrow down the results.
# 3. Open the file in Vim # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <( FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ fzf --ansi \
fzf --ansi \ --disabled --query "$INITIAL_QUERY" \
--disabled --query "$INITIAL_QUERY" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --delimiter : \
--delimiter : \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --bind 'enter:become(vim {1} +{2})'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png) ![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png)
@@ -358,8 +361,6 @@ IFS=: read -ra selected < <(
### Switching to fzf-only search mode ### Switching to fzf-only search mode
*(Requires fzf 0.27.1 or above)*
In the previous example, we lost fuzzy matching capability as we completely In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by *"unbinding"* `reload` action from `change` event. fzf-only search mode by *"unbinding"* `reload` action from `change` event.
@@ -375,19 +376,17 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
# 3. Open the file in Vim # 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <( FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ fzf --ansi \
fzf --ansi \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --disabled --query "$INITIAL_QUERY" \
--disabled --query "$INITIAL_QUERY" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \ --prompt '1. ripgrep> ' \
--prompt '1. ripgrep> ' \ --delimiter : \
--delimiter : \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --bind 'enter:become(vim {1} +{2})'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
* Phase 1. Filtering with Ripgrep * Phase 1. Filtering with Ripgrep
@@ -408,10 +407,8 @@ IFS=: read -ra selected < <(
### Switching between Ripgrep mode and fzf mode ### Switching between Ripgrep mode and fzf mode
*(Requires fzf 0.30.0 or above)* [fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
that were previously "unbound" via `unbind`.
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
@@ -421,25 +418,34 @@ CTRL-F.
#!/usr/bin/env bash #!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F) # Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}" INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <( FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ fzf --ansi \
fzf --ansi \ --color "hl:-1:underline,hl+:-1:underline:reverse" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \ --disabled --query "$INITIAL_QUERY" \
--disabled --query "$INITIAL_QUERY" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \ --bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \ --bind "start:unbind(ctrl-r)" \
--prompt '1. Ripgrep> ' \ --prompt '1. ripgrep> ' \
--delimiter : \ --delimiter : \
--header ' CTRL-R (Ripgrep mode) CTRL-F (fzf mode) ' \ --header ' CTRL-R (ripgrep mode) CTRL-F (fzf mode) ' \
--preview 'bat --color=always {1} --highlight-line {2}' \ --preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
) --bind 'enter:become(vim {1} +{2})'
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
``` ```
- To restore the query string when switching between modes, we store the
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
- Also note that we unbind `ctrl-r` binding on `start` event which is
triggered once when fzf starts.
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
Log tailing Log tailing
----------- -----------

View File

@@ -1,6 +1,28 @@
CHANGELOG CHANGELOG
========= =========
0.38.0
------
- New actions
- `become(...)` - Replace the current fzf process with the specified
command using `execve(2)` system call.
See https://github.com/junegunn/fzf#turning-into-a-different-process for
more information.
```sh
# Open selected files in Vim
fzf --multi --bind 'enter:become(vim {+})'
# Open the file in Vim and go to the line
git grep --line-number . |
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
```
- This action is not supported on Windows
- `show-preview`
- `hide-preview`
- Bug fixes
- `--preview-window 0,hidden` should not execute the preview command until
`toggle-preview` action is triggered
0.37.0 0.37.0
------ ------
- Added a way to customize the separator of inline info - Added a way to customize the separator of inline info

View File

@@ -54,6 +54,7 @@ Table of Contents
* [Advanced topics](#advanced-topics) * [Advanced topics](#advanced-topics)
* [Performance](#performance) * [Performance](#performance)
* [Executing external programs](#executing-external-programs) * [Executing external programs](#executing-external-programs)
* [Turning into a different process](#turning-into-a-different-process)
* [Reloading the candidate list](#reloading-the-candidate-list) * [Reloading the candidate list](#reloading-the-candidate-list)
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r) * [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f) * [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
@@ -136,15 +137,17 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
### Windows ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available via [Chocolatey][choco] and [Scoop][scoop]: available via [Chocolatey][choco], [Scoop][scoop], and [Winget][winget]:
| Package manager | Command | | Package manager | Command |
| --- | --- | | --- | --- |
| Chocolatey | `choco install fzf` | | Chocolatey | `choco install fzf` |
| Scoop | `scoop install fzf` | | Scoop | `scoop install fzf` |
| Winget | `winget install fzf` |
[choco]: https://chocolatey.org/packages/fzf [choco]: https://chocolatey.org/packages/fzf
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json [scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/j/junegunn/fzf
Known issues and limitations on Windows can be found on [the wiki Known issues and limitations on Windows can be found on [the wiki
page][windows-wiki]. page][windows-wiki].
@@ -202,6 +205,22 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf) vim $(fzf)
``` ```
> *:bulb: A more robust solution would be to use `xargs` but we've presented
> the above as it's easier to grasp*
> ```sh
> fzf --print0 | xargs -0 -o vim
> ```
>
> *:bulb: fzf also has the ability to turn itself into a different process.*
>
> ```sh
> fzf --bind 'enter:become(vim {})'
> ```
>
> *See [Turning into a different process](#turning-into-a-different-process)
> for more information.*
### Using the finder ### Using the finder
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down - `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
@@ -560,6 +579,47 @@ fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort
See *KEY BINDINGS* section of the man page for details. See *KEY BINDINGS* section of the man page for details.
### Turning into a different process
`become(...)` is similar to `execute(...)`/`execute-silent(...)` described
above, but instead of executing the command and coming back to fzf on
complete, it turns fzf into a new process for the command.
```sh
fzf --bind 'enter:become(vim {})'
```
Compared to the seemingly equivalent command substitution `vim "$(fzf)"`, this
approach has several advantages:
* Vim will not open an empty file when you terminate fzf with
<kbd>CTRL-C</kbd>
* Vim will not open an empty file when you press <kbd>ENTER</kbd> on an empty
result
* Can handle multiple selections even when they have whitespaces
```sh
fzf --multi --bind 'enter:become(vim {+})'
```
To be fair, running `fzf --print0 | xargs -0 -o vim` instead of `vim "$(fzf)"`
resolves all of the issues mentioned. Nonetheless, `become(...)` still offers
additional benefits in different scenarios.
* You can set up multiple bindings to handle the result in different ways
without any wrapping script
```sh
fzf --bind 'enter:become(vim {}),ctrl-e:become(emacs {})'
```
* Previously, you would have to use `--expect=ctrl-e` and check the first
line of the output of fzf
* You can easily build the subsequent command using the field index
expressions of fzf
```sh
# Open the file in Vim and go to the line
git grep --line-number . |
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
```
### Reloading the candidate list ### Reloading the candidate list
By binding `reload` action to a key or an event, you can make fzf dynamically By binding `reload` action to a key or an event, you can make fzf dynamically

View File

@@ -179,9 +179,10 @@ trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then if [[ "$opt" =~ "-E" ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
tmux_version=$(tmux -V) tmux_version=$(tmux -V)
if [[ ! $tmux_version =~ 3\.2 ]]; then if [[ $tmux_version =~ ^tmux\ 3\.2[a-z]?$ ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
else
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS" FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt" opt="-B $opt"
fi fi

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.37.0 version=0.38.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jan 2023" "fzf 0.37.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Feb 2023" "fzf 0.38.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jan 2023" "fzf 0.37.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Feb 2023" "fzf 0.38.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -999,6 +999,7 @@ A key or an event can be bound to one or more of the following actions.
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \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-border-label(...)\fR (change \fB--border-label\fR to the given string)
@@ -1036,6 +1037,7 @@ A key or an event can be bound to one or more of the following actions.
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR \fBhalf-page-down\fR
\fBhalf-page-up\fR \fBhalf-page-up\fR
\fBhide-preview\fR
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end) \fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprev-selected\fR (move to the previous selected item) \fBprev-selected\fR (move to the previous selected item)
@@ -1058,6 +1060,7 @@ A key or an event can be bound to one or more of the following actions.
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR
\fBselect-all\fR (select all matches) \fBselect-all\fR (select all matches)
\fBshow-preview\fR
\fBtoggle\fR (\fIright-click\fR) \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches) \fBtoggle-all\fR (toggle all matches)
\fBtoggle+down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
@@ -1141,6 +1144,14 @@ On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
POSIX-compliant. POSIX-compliant.
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
current fzf process with the specifed command using \fBexecve(2)\fR system
call.
\fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list

View File

@@ -512,9 +512,7 @@ try
let optstr .= ' --height='.height let optstr .= ' --height='.height
endif endif
" Respect --border option given in 'options' " Respect --border option given in 'options'
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0 let optstr = join([s:border_opt(get(dict, 'window', 0)), optstr])
let optstr .= s:border_opt(get(dict, 'window', 0))
endif
let prev_default_command = $FZF_DEFAULT_COMMAND let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command) if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command let $FZF_DEFAULT_COMMAND = source_command
@@ -741,7 +739,7 @@ function! s:calc_size(max, val, dict)
return size return size
endif endif
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2 let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0 let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
if stridx(opts, '--header') > stridx(opts, '--no-header') if stridx(opts, '--header') > stridx(opts, '--no-header')
let margin += len(split(opts, "\n")) let margin += len(split(opts, "\n"))
endif endif

View File

@@ -373,7 +373,7 @@ _fzf_setup_completion() {
} }
# Environment variables / Aliases / Hosts / Process # Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset _fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias _fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' ssh telnet _fzf_setup_completion 'host' ssh telnet
_fzf_setup_completion 'proc' kill _fzf_setup_completion 'proc' kill

View File

@@ -10,6 +10,7 @@ import (
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
@@ -921,7 +922,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1111,6 +1112,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actPrevSelected) appendAction(actPrevSelected)
case "next-selected": case "next-selected":
appendAction(actNextSelected) appendAction(actNextSelected)
case "show-preview":
appendAction(actShowPreview)
case "hide-preview":
appendAction(actHidePreview)
case "toggle-preview": case "toggle-preview":
appendAction(actTogglePreview) appendAction(actTogglePreview)
case "toggle-preview-wrap": case "toggle-preview-wrap":
@@ -1167,6 +1172,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
actions = append(actions, &action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} }
switch t { switch t {
case actBecome:
if util.IsWindows() {
exit("become action is not supported on Windows")
}
case actUnbind, actRebind: case actUnbind, actRebind:
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
case actChangePreviewWindow: case actChangePreviewWindow:
@@ -1219,6 +1228,8 @@ func isExecuteAction(str string) actionType {
prefix := actionNameRegexp.FindString(str) prefix := actionNameRegexp.FindString(str)
switch prefix { switch prefix {
case "become":
return actBecome
case "reload": case "reload":
return actReload return actReload
case "reload-sync": case "reload-sync":

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"math" "math"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
@@ -104,7 +105,6 @@ type previewer struct {
version int64 version int64
lines []string lines []string
offset int offset int
enabled bool
scrollable bool scrollable bool
final bool final bool
following resumableState following resumableState
@@ -350,6 +350,8 @@ const (
actRefreshPreview actRefreshPreview
actReplaceQuery actReplaceQuery
actToggleSort actToggleSort
actShowPreview
actHidePreview
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actTransformBorderLabel actTransformBorderLabel
@@ -386,6 +388,7 @@ const (
actDeselect actDeselect
actUnbind actUnbind
actRebind actRebind
actBecome
) )
type placeholderFlags struct { type placeholderFlags struct {
@@ -643,7 +646,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
initialPreviewOpts: opts.Preview, initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview, previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, len(opts.Preview.command) > 0, false, true, disabledState, "", []bool{}}, previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
previewed: previewed{0, 0, 0, false}, previewed: previewed{0, 0, 0, false},
previewBox: previewBox, previewBox: previewBox,
eventBox: eventBox, eventBox: eventBox,
@@ -1032,7 +1035,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
if t.noInfoLine() { if t.noInfoLine() {
minAreaHeight -= 1 minAreaHeight -= 1
} }
if t.mayNeedPreviewWindow() { if t.needPreviewWindow() {
minPreviewHeight := 1 + borderLines(t.previewOpts.border) minPreviewHeight := 1 + borderLines(t.previewOpts.border)
minPreviewWidth := 5 minPreviewWidth := 5
switch t.previewOpts.position { switch t.previewOpts.position {
@@ -1115,7 +1118,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
// Set up preview window // Set up preview window
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if t.mayNeedPreviewWindow() { if forcePreview || t.needPreviewWindow() {
var resizePreviewWindows func(previewOpts *previewOpts) var resizePreviewWindows func(previewOpts *previewOpts)
resizePreviewWindows = func(previewOpts *previewOpts) { resizePreviewWindows = func(previewOpts *previewOpts) {
t.activePreviewOpts = previewOpts t.activePreviewOpts = previewOpts
@@ -1244,6 +1247,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
} }
} }
resizePreviewWindows(&t.previewOpts) resizePreviewWindows(&t.previewOpts)
} else {
t.activePreviewOpts = &t.previewOpts
} }
// Without preview window // Without preview window
@@ -2234,7 +2239,7 @@ func (t *Terminal) redraw() {
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string {
line := "" line := ""
valid, list := t.buildPlusList(template, forcePlus, false) valid, list := t.buildPlusList(template, forcePlus)
// captureFirstLine is used for transform-{prompt,query} and we don't want to // captureFirstLine is used for transform-{prompt,query} and we don't want to
// return an empty string in those cases // return an empty string in those cases
if !valid && !captureFirstLine { if !valid && !captureFirstLine {
@@ -2273,13 +2278,13 @@ func (t *Terminal) hasPreviewer() bool {
return t.previewBox != nil return t.previewBox != nil
} }
func (t *Terminal) mayNeedPreviewWindow() bool { func (t *Terminal) needPreviewWindow() bool {
return t.hasPreviewer() && t.previewer.enabled && t.previewOpts.Visible() return t.hasPreviewer() && len(t.previewOpts.command) > 0 && t.previewOpts.Visible()
} }
// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer) // Check if previewer is currently in action (invisible previewer with size 0 or visible previewer)
func (t *Terminal) isPreviewEnabled() bool { func (t *Terminal) canPreview() bool {
return t.hasPreviewer() && t.previewer.enabled && (!t.previewOpts.Visible() || t.pwindow != nil) return t.hasPreviewer() && (!t.previewOpts.Visible() && !t.previewOpts.hidden || t.hasPreviewWindow())
} }
func (t *Terminal) hasPreviewWindow() bool { func (t *Terminal) hasPreviewWindow() bool {
@@ -2294,10 +2299,10 @@ func (t *Terminal) currentItem() *Item {
return nil return nil
} }
func (t *Terminal) buildPlusList(template string, forcePlus bool, forceEvaluation bool) (bool, []*Item) { func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
current := t.currentItem() current := t.currentItem()
slot, plus, query := hasPreviewFlags(template) slot, plus, query := hasPreviewFlags(template)
if !forceEvaluation && !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) { if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
return current != nil, []*Item{current, current} return current != nil, []*Item{current, current}
} }
@@ -2387,7 +2392,7 @@ func (t *Terminal) Loop() {
pad := fitpad.pad pad := fitpad.pad
t.tui.Resize(func(termHeight int) int { t.tui.Resize(func(termHeight int) int {
contentHeight := fit + t.extraLines() contentHeight := fit + t.extraLines()
if t.mayNeedPreviewWindow() { if t.needPreviewWindow() {
if t.previewOpts.aboveOrBelow() { if t.previewOpts.aboveOrBelow() {
if t.previewOpts.size.percent { if t.previewOpts.size.percent {
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size)) newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size))
@@ -2621,8 +2626,8 @@ func (t *Terminal) Loop() {
} }
refreshPreview := func(command string) { refreshPreview := func(command string) {
if len(command) > 0 && t.isPreviewEnabled() { if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
} }
@@ -2696,7 +2701,7 @@ func (t *Terminal) Loop() {
case reqFullRedraw: case reqFullRedraw:
wasHidden := t.pwindow == nil wasHidden := t.pwindow == nil
t.redraw() t.redraw()
if wasHidden && t.pwindow != nil { if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command) refreshPreview(t.previewOpts.command)
} }
case reqClose: case reqClose:
@@ -2857,6 +2862,24 @@ func (t *Terminal) Loop() {
doAction = func(a *action) bool { doAction = func(a *action) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actBecome:
valid, list := t.buildPlusList(a.a, false)
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
shellPath, err := exec.LookPath(shell)
if err == nil {
t.tui.Close()
if t.history != nil {
t.history.append(string(t.input))
}
util.SetStdin(tui.TtyIn())
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
}
}
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false) t.executeCommand(a.a, false, a.t == actExecuteSilent, false)
case actExecuteMulti: case actExecuteMulti:
@@ -2864,16 +2887,21 @@ func (t *Terminal) Loop() {
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
case actTogglePreview: case actTogglePreview, actShowPreview, actHidePreview:
if t.hasPreviewer() { var act bool
if t.activePreviewOpts != nil { switch a.t {
t.activePreviewOpts.Toggle() case actShowPreview:
} else if !t.previewOpts.Visible() { act = !t.hasPreviewWindow() && len(t.previewOpts.command) > 0
t.previewer.enabled = !t.previewer.enabled case actHidePreview:
} act = t.hasPreviewWindow()
case actTogglePreview:
act = t.hasPreviewWindow() || len(t.previewOpts.command) > 0
}
if act {
t.activePreviewOpts.Toggle()
updatePreviewWindow(false) updatePreviewWindow(false)
if t.isPreviewEnabled() { if t.canPreview() {
valid, list := t.buildPlusList(t.previewOpts.command, false, false) valid, list := t.buildPlusList(t.previewOpts.command, false)
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
@@ -2968,7 +2996,6 @@ func (t *Terminal) Loop() {
t.prompt, t.promptLen = t.parsePrompt(a.a) t.prompt, t.promptLen = t.parsePrompt(a.a)
req(reqPrompt) req(reqPrompt)
case actPreview: case actPreview:
t.previewer.enabled = true
updatePreviewWindow(true) updatePreviewWindow(true)
refreshPreview(a.a) refreshPreview(a.a)
case actRefreshPreview: case actRefreshPreview:
@@ -3353,7 +3380,7 @@ func (t *Terminal) Loop() {
case actReload, actReloadSync: case actReload, actReloadSync:
t.failed = nil t.failed = nil
valid, list := t.buildPlusList(a.a, false, false) valid, list := t.buildPlusList(a.a, false)
if !valid { if !valid {
// We run the command even when there's no match // We run the command even when there's no match
// 1. If the template doesn't have any slots // 1. If the template doesn't have any slots
@@ -3381,9 +3408,8 @@ func (t *Terminal) Loop() {
} }
case actChangePreview: case actChangePreview:
if t.previewOpts.command != a.a { if t.previewOpts.command != a.a {
t.previewer.enabled = len(a.a) > 0
updatePreviewWindow(false)
t.previewOpts.command = a.a t.previewOpts.command = a.a
updatePreviewWindow(false)
refreshPreview(t.previewOpts.command) refreshPreview(t.previewOpts.command)
} }
case actChangePreviewWindow: case actChangePreviewWindow:
@@ -3403,7 +3429,7 @@ func (t *Terminal) Loop() {
if !currentPreviewOpts.sameLayout(t.previewOpts) { if !currentPreviewOpts.sameLayout(t.previewOpts) {
wasHidden := t.pwindow == nil wasHidden := t.pwindow == nil
updatePreviewWindow(false) updatePreviewWindow(false)
if wasHidden && t.pwindow != nil { if wasHidden && t.hasPreviewWindow() {
refreshPreview(t.previewOpts.command) refreshPreview(t.previewOpts.command)
} else { } else {
req(reqPreviewRefresh) req(reqPreviewRefresh)
@@ -3480,12 +3506,10 @@ func (t *Terminal) Loop() {
req(reqList) req(reqList)
} }
if queryChanged { if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
if t.isPreviewEnabled() { _, _, q := hasPreviewFlags(t.previewOpts.command)
_, _, q := hasPreviewFlags(t.previewOpts.command) if q {
if q { t.version++
t.version++
}
} }
} }

View File

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

View File

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

View File

@@ -180,7 +180,7 @@ class TestBase < Minitest::Test
end end
def writelines(path, lines) def writelines(path, lines)
File.unlink(path) while File.exist?(path) FileUtils.rm_f(path) while File.exist?(path)
File.open(path, 'w') { |f| f.puts lines } File.open(path, 'w') { |f| f.puts lines }
end end
@@ -188,7 +188,7 @@ class TestBase < Minitest::Test
wait { assert_path_exists tempname } wait { assert_path_exists tempname }
File.read(tempname) File.read(tempname)
ensure ensure
File.unlink(tempname) while File.exist?(tempname) FileUtils.rm_f(tempname) while File.exist?(tempname)
@temp_suffix += 1 @temp_suffix += 1
tmux.prepare tmux.prepare
end end
@@ -905,11 +905,7 @@ class TestGoFZF < TestBase
history_file = '/tmp/fzf-test-history' history_file = '/tmp/fzf-test-history'
# History with limited number of entries # History with limited number of entries
begin FileUtils.rm_f(history_file)
File.unlink(history_file)
rescue StandardError
nil
end
opts = "--history=#{history_file} --history-size=4" opts = "--history=#{history_file} --history-size=4"
input = %w[00 11 22 33 44] input = %w[00 11 22 33 44]
input.each do |keys| input.each do |keys|
@@ -955,7 +951,7 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> 33', lines[-1] } tmux.until { |lines| assert_equal '> 33', lines[-1] }
tmux.send_keys :Enter tmux.send_keys :Enter
ensure ensure
File.unlink(history_file) FileUtils.rm_f(history_file)
end end
def test_execute def test_execute
@@ -984,11 +980,7 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_multi def test_execute_multi
@@ -1013,20 +1005,12 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_plus_flag def test_execute_plus_flag
output = tempname + '.tmp' output = tempname + '.tmp'
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
writelines(tempname, ['foo bar', '123 456']) writelines(tempname, ['foo bar', '123 456'])
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
@@ -1059,21 +1043,13 @@ class TestGoFZF < TestBase
], File.readlines(output, chomp: true) ], File.readlines(output, chomp: true)
end end
rescue StandardError rescue StandardError
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_execute_shell def test_execute_shell
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
writelines(tempname, writelines(tempname,
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"]) ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
system("chmod +x #{tempname}") system("chmod +x #{tempname}")
@@ -1087,11 +1063,7 @@ class TestGoFZF < TestBase
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true) assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
end end
ensure ensure
begin FileUtils.rm_f(output)
File.unlink(output)
rescue StandardError
nil
end
end end
def test_cycle def test_cycle
@@ -1485,6 +1457,83 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' } tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
end 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 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.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.until { |lines| assert_equal '>', lines[-1] }
@@ -1497,11 +1546,7 @@ class TestGoFZF < TestBase
end end
def test_preview_size_0 def test_preview_size_0
begin FileUtils.rm_f(tempname)
File.unlink(tempname)
rescue StandardError
nil
end
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal 100, lines.item_count assert_equal 100, lines.item_count
@@ -1526,6 +1571,32 @@ class TestGoFZF < TestBase
end end
end end
def test_preview_size_0_hidden
FileUtils.rm_f(tempname)
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_includes lines, '> 3' }
wait { refute_path_exists tempname }
tmux.send_keys :Space
wait do
assert_path_exists tempname
assert_equal %w[3], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Down
wait do
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
end
tmux.send_keys :Space, :Down
tmux.until { |lines| assert_includes lines, '> 5' }
tmux.send_keys :Down
tmux.until { |lines| assert_includes lines, '> 6' }
tmux.send_keys :Space
wait do
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
end
end
def test_preview_flags def test_preview_flags
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' | tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter #{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
@@ -2087,11 +2158,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_default_command_on_accept def test_kill_default_command_on_accept
@@ -2109,11 +2176,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_reload_command_on_abort def test_kill_reload_command_on_abort
@@ -2134,11 +2197,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_kill_reload_command_on_accept def test_kill_reload_command_on_accept
@@ -2158,11 +2217,7 @@ class TestGoFZF < TestBase
wait { refute system("pgrep -f #{script}") } wait { refute system("pgrep -f #{script}") }
ensure ensure
system("pkill -9 -f #{script}") system("pkill -9 -f #{script}")
begin FileUtils.rm_f(script)
File.unlink(script)
rescue StandardError
nil
end
end end
def test_preview_header def test_preview_header
@@ -2588,6 +2643,19 @@ class TestGoFZF < TestBase
tmux.send_keys :Space tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '/1/1/' } tmux.until { |lines| assert_includes lines, '/1/1/' }
end end
def test_become
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 99, lines.item_count }
end
end end
module TestShell module TestShell