Advanced fzf examples
- Last update: 2023/02/15
- Requires fzf 0.38.0 or above
Introduction
fzf is an interactive Unix filter program that is designed to be
used with other Unix tools. It reads a list of items from the standard input,
allows you to select a subset of the items, and prints the selected ones to
the standard output. You can think of it as an interactive version of grep,
and it's already useful even if you don't know any of its options.
# 1. ps: Feed the list of processes to fzf
# 2. fzf: Interactively select a process using fuzzy matching algorithm
# 3. awk: Take the PID from the selected line
# 3. kill: Kill the process with the PID
ps -ef | fzf | awk '{print $2}' | xargs kill -9
While the above example succinctly summarizes the fundamental concept of fzf,
you can build much more sophisticated interactive workflows using fzf once you
learn its wide variety of features.
- To see the full list of options and features, see
man fzf
- To see the latest additions, see CHANGELOG.md
This document will guide you through some examples that will familiarize you
with the advanced features of fzf.
Screen Layout
--height
fzf by default opens in fullscreen mode, but it's not always desirable.
Oftentimes, you want to see the current context of the terminal while using
fzf. --height
is an option for opening fzf below the cursor in
non-fullscreen mode so you can still see the previous commands and their
results above it.
fzf --height=40%
You might also want to experiment with other layout options such as
--layout=reverse
, --info=inline
, --border
, --margin
, etc.
fzf --height=40% --layout=reverse
fzf --height=40% --layout=reverse --info=inline
fzf --height=40% --layout=reverse --info=inline --border
fzf --height=40% --layout=reverse --info=inline --border --margin=1
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)
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
could write a wrapper script or shell alias, but there is an easier option.
Define $FZF_DEFAULT_OPTS
like so:
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
fzf-tmux
Before fzf had --height
option, we would open fzf in a tmux split pane not
to take up the whole screen. This is done using fzf-tmux
script.
# Open fzf on a tmux split pane below the current pane.
# Takes the same set of options.
fzf-tmux --layout=reverse
The limitation of fzf-tmux
is that it only works when you're on tmux unlike
--height
option. But the advantage of it is that it's more flexible.
(See man fzf-tmux
for available options.)
# On the right (50%)
fzf-tmux -r
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
# Open tmux in a tmux popup window (default size: 50% of the screen)
fzf-tmux -p
# 80% width, 60% height
fzf-tmux -p 80%,60%
You might also want to check out my tmux plugins which support this popup
window layout.
Dynamic reloading of the list
fzf can dynamically update the candidate list using an arbitrary program with
reload
bindings (The design document for reload
can be found
here).
Updating the list of processes by pressing CTRL-R
This example shows how you can set up a binding for dynamically updating the
list without restarting fzf.
(date; ps -ef) |
fzf --bind='ctrl-r:reload(date; ps -ef)' \
--header=$'Press CTRL-R to reload\n\n' --header-lines=2 \
--preview='echo {}' --preview-window=down,3,wrap \
--layout=reverse --height=80% | awk '{print $2}' | xargs kill -9
- The initial command is
(date; ps -ef)
. It prints the current date and
time, and the list of the processes.
- With
--header
option, you can show any message as the fixed header.
- To disallow selecting the first two lines (
date
and ps
header), we use
--header-lines=2
option.
--bind='ctrl-r:reload(date; ps -ef)'
binds CTRL-R to reload
action that
runs date; ps -ef
, so we can update the list of the processes by pressing
CTRL-R.
- We use simple
echo {}
preview option, so we can see the entire line on the
preview window below even if it's too long
Toggling between data sources
You're not limited to just one reload binding. Set up multiple bindings so
you can switch between data sources.
find * | fzf --prompt 'All> ' \
--header 'CTRL-D: Directories / CTRL-F: Files' \
--bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \
--bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)'
Ripgrep integration
Using fzf as the secondary filter
fzf is pretty fast for filtering a list that you will rarely have to think
about its performance. But it is not the right tool for searching for text
inside many large files, and in that case you should definitely use something
like Ripgrep.
In the next example, Ripgrep is the primary filter that searches for the given
text in files, and fzf is used as the secondary fuzzy filter that adds
interactivity to the workflow. And we use bat to show the matching line in
the preview window.
This is a bash script and it will not run as expected on other non-compliant
shells. To avoid the compatibility issue, let's save this snippet as a script
file called rfv
.
#!/usr/bin/env bash
# 1. Search for text in files using Ripgrep
# 2. Interactively narrow down the list using fzf
# 3. Open the file in Vim
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
And run it with an initial query string.
# Make the script executable
chmod +x rfv
# Run it with the initial query "algo"
./rfv algo
Ripgrep will perform the initial search and list all the lines that contain
algo
. Then we further narrow down the list on fzf.
I know it's a lot to digest, let's try to break down the code.
Using fzf as interactive Ripgrep launcher
We have learned that we can bind reload
action to a key (e.g.
--bind=ctrl-r:execute(ps -ef)
). In the next example, we are going to bind
reload
action to change
event so that whenever the user changes the
query string on fzf, reload
action is triggered.
Here is a variation of the above rfv
script. fzf will restart Ripgrep every
time the user updates the query string on fzf. Searching and filtering is
completely done by Ripgrep, and fzf merely provides the interactive interface.
So we lose the "fuzziness", but the performance will be better on larger
projects, and it will free up memory as you narrow down the results.
#!/usr/bin/env bash
# 1. Search for text in files using Ripgrep
# 2. Interactively restart Ripgrep with reload action
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
- Instead of starting fzf in
rg ... | fzf
form, we start fzf without an
explicit input, but with a custom FZF_DEFAULT_COMMAND
variable. This way
fzf can kill the initial Ripgrep process it starts with the initial query.
Otherwise, the initial Ripgrep process will keep consuming system resources
even after reload
is triggered.
- Filtering is no longer a responsibility of fzf; hence
--disabled
{q}
in the reload command evaluates to the query string on fzf prompt.
sleep 0.1
in the reload command is for "debouncing". This small delay will
reduce the number of intermediate Ripgrep processes while we're typing in
a query.
Switching to fzf-only search mode
In the previous example, we lost fuzzy matching capability as we completely
delegated search functionality to Ripgrep. But we can dynamically switch to
fzf-only search mode by "unbinding" reload
action from change
event.
#!/usr/bin/env bash
# Two-phase filtering with Ripgrep and fzf
#
# 1. Search for text in files using Ripgrep
# 2. Interactively restart Ripgrep with reload action
# * Press alt-enter to switch to fzf-only filtering
# 3. Open the file in Vim
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--prompt '1. ripgrep> ' \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
- Phase 1. Filtering with Ripgrep
- Phase 2. Filtering with fzf
- We added
--prompt
option to show that fzf is initially running in "Ripgrep
launcher mode".
- We added
alt-enter
binding that
- unbinds
change
event, so Ripgrep is no longer restarted on key press
- changes the prompt to
2. fzf>
- enables search functionality of fzf
- clears the current query string that was used to start Ripgrep process
- and unbinds
alt-enter
itself as this is a one-off event
- We reverted
--color
option for customizing how the matching chunks are
displayed in the second phase
Switching between Ripgrep mode and fzf mode
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
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
CTRL-F.
#!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
--bind "start:unbind(ctrl-r)" \
--prompt '1. ripgrep> ' \
--delimiter : \
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
- To restore the query string when switching between modes, we store the
current query in /tmp/rg-fzf-{r,f}
files and restore the query using
transform-query
action which was added in fzf 0.36.0.
- Also note that we unbind
ctrl-r
binding on start
event which is
triggered once when fzf starts.
Log tailing
fzf can run long-running preview commands and render partial results before
completion. And when you specify follow
flag in --preview-window
option,
fzf will "tail -f
" the result, automatically scrolling to the bottom.
# With "follow", preview window will automatically scroll to the bottom.
# "\033[2J" is an ANSI escape sequence for clearing the screen.
# When fzf reads this code it clears the previous preview contents.
fzf --preview-window follow --preview 'for i in $(seq 100000); do
echo "$i"
sleep 0.01
(( i % 300 == 0 )) && printf "\033[2J"
done'
Admittedly, that was a silly example. Here's a practical one for browsing
Kubernetes pods.
pods() {
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
fzf --info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
--preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
}
- The preview window will "log tail" the pod
- Holding on to a large amount of log will consume a lot of memory. So we
limited the initial log amount with --tail=10000
.
execute
bindings allow you to run any command without leaving fzf
- Press enter key on a pod to
kubectl exec
into it
- Press CTRL-O to open the log in your editor
- Press CTRL-R to reload the pod list
- Press CTRL-/ repeatedly to to rotate through a different sets of preview
window options
80%,border-bottom
hidden
- Empty string after
|
translates to the default options from --preview-window
Key bindings for git objects
Oftentimes, you want to put the identifiers of various Git object to the
command-line. For example, it is common to write commands like these:
git checkout [SOME_COMMIT_HASH or BRANCH or TAG]
git diff [SOME_COMMIT_HASH or BRANCH or TAG] [SOME_COMMIT_HASH or BRANCH or TAG]
fzf-git.sh project defines a set of
fzf-based key bindings for Git objects. I strongly recommend that you check
them out because they are seriously useful.
Files listed in git status
CTRL-GCTRL-F
Branches
CTRL-GCTRL-B
Commit hashes
CTRL-GCTRL-H
Color themes
You can customize how fzf colors the text elements with --color
option. Here
are a few color themes. Note that you need a terminal emulator that can
display 24-bit colors.
# junegunn/seoul256.vim (dark)
export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99'
# junegunn/seoul256.vim (light)
export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899'
# morhetz/gruvbox
export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934'
# arcticicestudio/nord-vim
export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1'
# tomasr/molokai
export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672'
Generating fzf color theme from Vim color schemes
The Vim plugin of fzf can generate --color
option from the current color
scheme according to g:fzf_colors
variable. You can find the detailed
explanation here.
Here is an example. Add this to your Vim configuration file.
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'preview-bg': ['bg', 'NormalFloat'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
Then you can see how the --color
option is generated by printing the result
of fzf#wrap()
.
:echo fzf#wrap()
Use this command to append export FZF_DEFAULT_OPTS="..."
line to the end of
the current file.
:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*")))