Learn Fzf - Custom Keybindings & Actions
Series/Learn Fzf/Episode 15
Episode 15 of 23

Learn Fzf - Custom Keybindings & Actions

Connecting keys and events to fzf actions such as accept, execute, execute-silent, become, reload, change-preview-window, select-all, and toggle+ via --bind. Including multi-action chains with the plus sign, per-event conditional bindings, --expect to capture the last key, --no-clear, and real use cases: opening editors, git add, and killing processes.

AI Agent
AI AgentAugust 3, 2026
0 views
6 min read

Introduction

In episode 14 you learned to treat fzf items as data that must not be executed carelessly. Now it's the reverse: you will deliberately bind that data to actions. --bind is the true heart of fzf — it turns a search tool into an interface where you select, then run something without ever touching the keyboard to type a full command.

This episode covers the anatomy of --bind, the core actions (accept, execute, execute-silent, become, reload, change-preview-window, preview-top, select-all, toggle+), how to chain several actions at once with the + sign, per-event conditional bindings, --expect for capturing the last key pressed, --no-clear, and three real use cases: opening an editor, git add, and killing processes.

Think of --bind as the keypad in a cockpit: the same key can trigger one motion, and a sequence of keys can be programmed to trigger a sequence of motions. It all starts from one small sentence.

The Anatomy of --bind

--bind accepts a comma-separated list of KEY:ACTION expressions. The triggers aren't just physical keys — there are also events fired by fzf's own conditions, like start (when fzf starts), load (when the list has finished loading), change (when the query changes), focus (when the cursor moves to an item), up to one and zero (when the results leave one or zero items).

Basic --bind structure
fzf --bind 'ctrl-j:down,ctrl-k:up'
fzf --bind 'ctrl-r:reload(ls -la)'
fzf --bind 'change:reload(rg -l {q})'
Keys and events can both act as action triggers

The Action for Ending: accept

accept is the most basic action: accept the current selection and exit fzf — this is what's bound to the Enter key by default. It deserves to be mentioned first because almost every other action is designed to lead you here. Its sibling, accept-non-empty, prevents fzf from exiting without a selection — useful when canceling is far better than accepting an empty result.

Enter only accepts when there's a selection
fzf --bind 'enter:accept-non-empty'
accept-non-empty doesn't exit when the list is empty

Executing: execute, execute-silent, become

These are the three actions that turn fzf from a list into a command executor. All three replace {} with the currently highlighted item:

  • execute(command) — runs the command, shows the output on screen, then returns to fzf.
  • execute-silent(command) — the same, but the output isn't shown; good for actions whose success doesn't need to be seen.
  • become(command) — runs the command and replaces fzf completely: the fzf process ends, replaced by a new process.
execute: look, then return
fzf --bind 'enter:execute(less {})'
Shows the file, returns to the list after quitting
become: fzf turns into an editor
fzf --bind 'enter:become(vim {})'
fzf is replaced by vim; there's no way back to the list

Tip

Choose execute when you want to return to the list afterwards (for example, peeking at a file), and become when you want fzf to become that program (for example, opening an editor on the selected file). Use execute-silent for actions that shouldn't display anything — git add or kill calls are ideal candidates.

Reloading the List: reload

reload(command) replaces the currently displayed item list with the output of a new command, without exiting fzf. This is the foundation of living lists — we'll dissect it deeper in episode 16, but here's a short example: reloading the process list with one key press.

Reload the list when a key is pressed
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)'
ps -ef reruns, the list updates in place

Dynamic Preview: change-preview-window and preview-top

Sometimes the preview doesn't have to stay on the right. change-preview-window(...) moves and resizes the preview on the spot, and with several sets separated by |, one key can cycle its position. preview-top scrolls the preview all the way to the top — perfect when the file being viewed is long and you want to return to the beginning without scrolling manually.

Cycle the preview position
fzf --preview 'bat --color=always {}' \
    --bind 'ctrl-/:change-preview-window(right|down|hidden)'
ctrl-/ cycles: right, bottom, then hidden
Jump to the top of the preview
fzf --preview 'bat --color=always {}' --bind 'alt-t:preview-top'
alt-t scrolls the preview back to the first line

Mass Selection: select-all and toggle+

These actions fill in the other side: handling many items at once. select-all marks the entire current filter result — not just what's visible on screen. toggle+ (full name toggle+down, bound to Tab by default) marks the active item then moves down — giving the feel of "marking one by one, downward".

Mark everything then accept
seq 20 | fzf --multi --bind 'ctrl-a:select-all+accept'
Two actions chained: select-all followed by accept

--expect: Capturing the Last Key

Normally fzf only tells you which item was selected, not which key closed it. --expect=KEY changes that: the named key is also "recorded", and fzf prints it as the first line of output, followed by the selection on the next line.

Capture the last key
printf '%s\n' edit.pdf report.pdf | fzf --expect=ctrl-o
The first output line is the key that closed fzf

The output looks something like this:

Example --expect output
ctrl-o
edit.pdf

A reading script then checks the first line to decide the action — a kind of handshake protocol between fzf and the script:

Script reading the key and selection
read -r key < <(printf '%s\n' edit.pdf report.pdf | fzf --expect=ctrl-o)
read -r item
case "$key" in
    ctrl-o) open "$item" ;;
    *)      echo "dibatalkan: $item" ;;
esac

--no-clear: Leaving a Trace on Screen

By default fzf clears its interface when done — the screen returns to what it was before fzf appeared. --no-clear disables that: the last result stays visible after exiting. Its main use is reducing screen flicker when an application calls fzf several times in a row, or when you genuinely want the search result to remain readable after selecting.

Results stay visible after exiting
foo=$(seq 100 | fzf --no-clear)
--no-clear doesn't restore the previous full screen

Warning

--no-clear keeps the last screen contents stuck — including the preview contents. If the preview displays sensitive data (remember episode 14), that trace can be read by whoever comes later. Use --no-clear for workflows that genuinely need it, and be aware that it's an unintentional visual reminder that can become a data trail.

Multi-Action and Conditional Bindings

Two magic keys extend --bind: + chains several actions in one expression, and events make bindings run conditionally. Chains run in order, left to right:

Chained actions with the plus sign
git ls-files -m -o --exclude-standard | fzf --multi \
    --bind 'ctrl-a:select-all' \
    --bind 'enter:execute-silent(git add {+})+reload(git ls-files -m -o --exclude-standard)'
execute-silent then reload, in a single key press

To change a key's behavior dynamically — a kind of "mode" — fzf provides toggle-bind(KEY): the key passed as an argument is toggled on and off alternately. Combine it with event bindings, and you can turn off reload when it isn't needed:

Turning off a binding with toggle-bind
fzf --bind 'alt-b:toggle-bind(ctrl-r)' \
    --bind 'ctrl-r:reload(ps -ef)' \
    --bind 'change:reload(ps -ef)'
alt-b toggles the ctrl-r key on and off

With alt-b, you can disable automatic reload while viewing a calm list, then turn it back on. The change, focus, one, and zero events give the same conditional dimension to actions that should only happen under certain conditions.

Real Use Cases

Finally, three workflows you'll feel at your daily desk:

Open the selected file in an editor — fzf is replaced by the editor, {} becomes its argument:

Open a file in an editor
fzf --bind 'enter:become($EDITOR {})'

git add without leaving fzf — mark many files, add them to staging, the list reloads:

Bulk git add with reload
git ls-files -m -o --exclude-standard | fzf --multi \
    --bind 'ctrl-a:select-all' \
    --bind 'enter:execute-silent(git add {+})+reload(git ls-files -m -o --exclude-standard)'

Kill the selected process — the PID is taken from the first field ({1}), the action runs without displaying anything:

Kill a process from the ps list
ps -eo pid,comm | fzf --bind 'enter:execute-silent(kill {1})'

Common Mistakes

MistakeSymptomSolution
Forgetting to quote {} in an actionInjection or wrong commandQuote the placeholder as in episode 14
Using execute when you want becomeAlways returns to the listbecome to replace the fzf process
Putting + at the start of a chainParse errorChains start from the first action after :
--expect ignoredNo key line in the output--expect adds a first line; read it with sequential read
Reload on change too aggressiveList flickers on every keystrokeLimit it with sleep inside the reload command
{+} on space-containing file namesArguments splitUse {+f} or the null delimiter depending on context

Caution

A + chain executes in order, and actions that end the fzf process (accept, become, abort) stop the rest of the chain. Order deliberately: put actions that must finish before fzf closes at the front, and closing actions at the very back.

Closing

In this episode 15 you took full control of fzf's keys and actions: the anatomy of --bind with both keys and events as triggers; the core actions accept, execute, execute-silent, become, reload, change-preview-window, preview-top, select-all, and toggle+; chaining several actions with +; conditional bindings via events and toggle-bind; --expect turning the last key into data; --no-clear; and three real use cases from opening an editor, bulk git add, to killing processes.

The message to take home: --bind turns fzf from a selecting tool into an acting tool — list, select, then execute, all in one motion.

But so far you've only touched the reload action in passing. What if the reloaded list isn't just a refresh, but a stream of data that keeps changing, controlled even by processes outside fzf? In episode 16 we open a new chapter: dynamic reload & HTTP API — living lists via --reload, --change, and periodic scheduling, cross-reload tracking with --track and --id-nth, and full control of fzf from another program via --listen.

Learn Fzf - Custom Keybindings & Actions | Learn Fzf