ADVANCED.md
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.
man fzfThis document will guide you through some examples that will familiarize you with the advanced features of fzf.
--heightfzf 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 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"
--tmux(Requires tmux 3.3 or later)
If you're using tmux, you can open fzf in a tmux popup using --tmux option.
# Open fzf in a tmux popup at the center of the screen with 70% width and height
fzf --tmux 70%
--tmux option is silently ignored if you're not on tmux. So if you're trying
to avoid opening fzf in fullscreen, try specifying both --height and --tmux.
# --tmux is specified later so it takes precedence over --height when on tmux.
# If you're not on tmux, --tmux is ignored and --height is used instead.
fzf --height 70% --tmux 70%
You can also specify the position, width, and height of the popup window in the following format:
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]]# 100% width and 60% height
fzf --tmux 100%,60% --border horizontal
# On the right (50% width)
fzf --tmux right
# On the left (40% width and 70% height)
fzf --tmux left,40%,70%
[!TIP] You might also want to check out my tmux plugins which support this popup window layout.
fzf can dynamically update the candidate list using an arbitrary program with
reload bindings (The design document for reload can be found
here).
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
(date; ps -ef). It prints the current date and
time, and the list of the processes.--header option, you can show any message as the fixed header.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.echo {} preview option, so we can see the entire line on the
preview window below even if it's too longYou'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)'
The above example uses two different key bindings to toggle between two modes, but can we just use a single key binding?
To make a key binding behave differently each time it is pressed, we need:
The following example shows how to 1. store the current mode in the prompt
string, 2. and use this information ($FZF_PROMPT) to determine which
actions to perform using the transform action.
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
--preview '[[ $FZF_PROMPT =~ Files ]] && bat --color=always {} || tree -C {}'
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.
man/man1/fzf.1:54:.BI "--algo=" TYPE
man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2)
man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)"
src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo"
: is the file path, and the second token is
the line number of the matching line. They respectively correspond to {1}
and {2} in the preview command.
--preview 'bat --color=always {1} --highlight-line {2}'rg with --color=always option, we should tell fzf to parse
ANSI color codes in the input by setting --ansi.--color option.
-1 tells fzf to keep the original color from the input. See man fzf for
available color options.--preview-window option consists of 5 components delimited
by ,
up -- Position of the preview window60% -- Size of the preview windowborder-bottom -- Preview window border only on the bottom side+{2}+3/3 -- Scroll offset of the preview contents~3 -- Fixed header+{2} -- The base offset is extracted from the second token+3 -- We add 3 lines to the base offset to compensate for the header
part of bat output
───────┬──────────────────────────────────────────────────────────
│ File: CHANGELOG.md
───────┼──────────────────────────────────────────────────────────
1 │ CHANGELOG
2 │ =========
3 │
4 │ 0.26.0
5 │ ------
/3 adjusts the offset so that the matching line is shown at a third
position in the window~3 makes the top three lines fixed header so that they are always
visible regardless of the scroll offsetbecome(...) action which was added in fzf 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}).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 --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q} || true" \
--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})'
rg ... | fzf form, we make it start
the initial Ripgrep process immediately via start:reload binding for the
consistency of the code.--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.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 --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
--prompt option to show that fzf is initially running in "Ripgrep
launcher mode".alt-enter binding that
change event, so Ripgrep is no longer restarted on key press2. fzf>alt-enter itself as this is a one-off event--color option for customizing how the matching chunks are
displayed in the second phasefzf 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 --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
/tmp/rg-fzf-{r,f} files and restore the query using
transform-query action which was added in fzf 0.36.0.ctrl-r binding on start event which is
triggered once when fzf starts.In contrast to the previous version, we use just one hotkey to toggle between
ripgrep and fzf mode. This is achieved by using the $FZF_PROMPT as a state
within the transform action, a feature introduced in fzf 0.45.0. A
more detailed explanation of this feature can be found in a previous section -
Toggling with a single keybinding.
When using the transform action, the placeholder (\{q}) should be escaped to
prevent immediate evaluation.
#!/usr/bin/env bash
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
rm -f /tmp/rg-fzf-{r,f}
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
fzf --ansi --disabled --query "$INITIAL_QUERY" \
--bind "start:reload:$RG_PREFIX {q}" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
echo "rebind(change)+change-prompt(1. ripgrep> )+disable-search+transform-query:echo \{q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r" ||
echo "unbind(change)+change-prompt(2. fzf> )+enable-search+transform-query:echo \{q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f"' \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--prompt '1. ripgrep> ' \
--delimiter : \
--header 'CTRL-T: Switch between ripgrep/fzf' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
--bind 'enter:become(vim {1} +{2})'
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.
#!/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})'
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() {
command='kubectl get pods --all-namespaces' fzf \
--info=inline --layout=reverse --header-lines=1 \
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
--bind 'start,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})' \
--preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
}
--tail=10000.execute bindings allow you to run any command without leaving fzf
kubectl exec into it80%,border-bottomhidden| translates to the default options from --preview-windowOftentimes, 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.
git status<kbd>CTRL-G</kbd><kbd>CTRL-F</kbd>
<kbd>CTRL-G</kbd><kbd>CTRL-B</kbd>
<kbd>CTRL-G</kbd><kbd>CTRL-H</kbd>
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'
fzf Theme Playground created by Vitor Mello is a webpage where you can interactively create fzf themes.
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[^']*")))