How the fzf `--bind` Action System Works for Custom Key Bindings

The fzf --bind flag maps keys to actions through a three-stage pipeline that parses the command line, extracts key chords, and decodes action lists into executable handlers.

The junegunn/fzf repository implements a sophisticated event-driven architecture that transforms raw strings like ctrl-r:reload:git status into runtime behavior. Understanding this --bind action system reveals how fzf converts user input into concrete tui.Event mappings and executes action chains.

The Three-Stage Binding Pipeline

When you pass --bind "key:action", fzf processes the string through three distinct phases before the interactive session begins.

Stage 1: Option Parsing with parseKeymap

The entry point resides in src/options.go. When the command-line parser encounters --bind, it delegates the raw binding string to parseKeymap (lines 1972–1990). This function initializes the parsing process and prepares the string for structural analysis.

Stage 2: Key Extraction and Masking

Before splitting the string on delimiters, fzf must protect action arguments that contain commas, colons, or plus signs. The maskActionContents function (lines 1630–1660 in src/options.go) implements this protection by:

  • Scanning the bind string for action content (text following colons)
  • Replacing ,, :, and + inside actions with sentinel runes (escapedColon, escapedComma, escapedPlus)
  • Preserving these characters in the key name portion for accurate splitting

This masking ensures that a binding like ctrl-r:reload:git log,ctrl-y:accept splits correctly into two separate bindings rather than breaking on the comma inside git log.

Stage 3: Action Decoding

With the string masked, parseActionList (lines 1695–1780 in src/options.go) executes the final transformation:

  1. Key chord resolution: parseKeyChords (in src/keychord.go) converts symbolic names like ctrl-x, alt-Shift-A, or f5 into concrete tui.Event structures.
  2. Action tokenization: The right-hand side splits on + to support multi-action chains (e.g., reload+accept).
  3. Action type mapping: A comprehensive switch statement maps textual tokens (accept, reload, toggle-preview, unbind) to constants defined in src/actiontype_string.go (e.g., actAccept, actReload).
  4. Command-action handling: isExecuteAction (lines 1820–1890) identifies actions requiring arguments (reload:, execute:, preview:) and stores the post-colon suffix in the action's argument field.

Data Structures and Runtime Execution

The parsed bindings populate the Options struct defined in src/options.go:

type Options struct {
    // ... other fields ...
    Keymap map[tui.Event][]*action
}

This map associates each tui.Event (representing a physical key press or mouse event) with a slice of *action pointers. When fzf initializes, src/terminal.go establishes the defaultKeymap and merges user-defined bindings using last-one-wins semantics—later --bind arguments override earlier definitions for the same key.

During the interactive session, the terminal's event loop in src/terminal.go performs the following sequence:

  1. Captures raw input and translates it into a tui.Event
  2. Looks up the event in opts.Keymap
  3. If found, iterates over the []*action slice and invokes each action's handler (e.g., actAccept, actReload, actTogglePreview)
  4. Executes handlers immediately for synchronous actions or delegates to goroutines for asynchronous operations like reload or execute

Special Action Types and Meta-Actions

The --bind system supports several categories of actions that modify runtime behavior beyond simple selection:

Meta-Actions (Keymap Modification)

  • unbind: Removes a binding from the active keymap (implemented as actUnbind in src/terminal.go)
  • rebind: Restores a previously unbound key
  • toggle-bind: Switches a binding on or off dynamically

These actions mutate opts.Keymap during execution, allowing interactive reconfiguration without restarting fzf.

Source Regeneration

  • reload and reload-sync: Execute a shell command (stored in action.a) to regenerate the candidate list. reload runs asynchronously while reload-sync blocks the UI until completion. Handlers reside in src/terminal.go with command execution logic in src/reload.go.

Preview and Transformation

  • preview, change-preview, transform, and bg-transform: These actions trigger preview rendering or modify preview content. The presence of any preview-related action forces mayTriggerPreview to initialize a preview event box even when --preview was not explicitly provided on the command line. Implementation details span src/preview.go and src/terminal.go.

Practical Examples of fzf Custom Key Bindings

The following examples demonstrate real-world usage patterns, each processed by the parseKeymap pipeline described above:


# Bind Ctrl-P to accept the current selection (overrides default behavior)

fzf --bind "ctrl-p:accept"

# Reload git status on Enter, useful for dynamic lists

fzf --bind "enter:reload:git status"

# Combine preview toggling with list reloading

fzf \
  --preview 'bat --style=numbers {}' \
  --bind "space:toggle-preview" \
  --bind "ctrl-r:reload"

# Chain multiple actions: reload then accept first result

fzf --bind "f5:reload:git log|accept"

# Runtime keymap modification: toggle Ctrl-L binding for clearing query

fzf --bind "f2:toggle-bind+ctrl-l:clear-query"

Each binding string undergoes masking, splitting, and action resolution before the interactive session begins. Multiple --bind flags are concatenated and processed sequentially, with later bindings overriding earlier ones for the same key chord.

Summary

  • Three-stage parsing: parseKeymap initiates processing, maskActionContents protects action arguments from delimiter splitting, and parseActionList resolves keys to tui.Event objects and action tokens to actionType constants.
  • Core data structure: The Options.Keymap (defined in src/options.go) stores a map[tui.Event][]*action that links physical input events to executable action chains.
  • Runtime execution: The terminal event loop in src/terminal.go dispatches actions by looking up events in opts.Keymap and invoking handlers like actAccept, actReload, or actTogglePreview.
  • Meta-actions: Special actions including unbind, rebind, and toggle-bind mutate the keymap at runtime, while reload and preview actions integrate with external commands via src/reload.go and src/preview.go.

Frequently Asked Questions

How does fzf handle commas and colons inside action arguments?

fzf uses a masking mechanism in maskActionContents (located in src/options.go) to protect delimiters within action arguments. Before splitting the bind string, the parser replaces commas, colons, and plus signs inside action content (such as reload:git log --oneline) with sentinel runes (escapedComma, escapedColon, escapedPlus). This ensures that structural parsing occurs only on delimiters outside action arguments, preserving complex shell commands intact.

What is the difference between reload and reload-sync actions?

Both actions trigger source list regeneration, but they differ in execution concurrency. The reload action (mapped to actReload) executes the specified command asynchronously, allowing the UI to remain responsive while the command runs. In contrast, reload-sync (mapped to actReloadSync) blocks the terminal event loop until the command completes, preventing user interaction during execution. The implementation resides in src/terminal.go with command execution logic in src/reload.go.

Can I modify key bindings while fzf is running?

Yes, fzf supports runtime keymap modification through meta-actions. The unbind action removes a key binding from the active opts.Keymap, while rebind restores previously removed bindings. The toggle-bind action switches a binding on or off dynamically. These actions (defined as actUnbind, actRebind, and actToggleBind in src/actiontype_string.go) mutate the keymap during execution in src/terminal.go, allowing interactive reconfiguration without restarting the fzf process.

Why do later --bind arguments override earlier ones?

fzf implements a "last-one-wins" semantics for duplicate key bindings to allow incremental configuration. When parseKeymap processes the concatenated bind strings (from multiple --bind flags), it inserts entries directly into opts.Keymap. Because Go maps overwrite existing keys, subsequent bindings for the same tui.Event replace earlier definitions. This behavior enables users to define default bindings in shell aliases or environment variables while overriding specific keys via command-line arguments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →