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

> Master fzf --bind custom key bindings. Understand the three stage pipeline parsing key chords and decoding actions into executable handlers for powerful customization.

- Repository: [Junegunn Choi/fzf](https://github.com/junegunn/fzf)
- Tags: internals
- Published: 2026-03-01

---

**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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/options.go)) executes the final transformation:

1. **Key chord resolution**: `parseKeyChords` (in [`src/keychord.go`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/options.go):

```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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/terminal.go) with command execution logic in [`src/reload.go`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/preview.go) and [`src/terminal.go`](https://github.com/junegunn/fzf/blob/main/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:

```bash

# 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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/reload.go) and [`src/preview.go`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/terminal.go) with command execution logic in [`src/reload.go`](https://github.com/junegunn/fzf/blob/main/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`](https://github.com/junegunn/fzf/blob/main/src/actiontype_string.go)) mutate the keymap during execution in [`src/terminal.go`](https://github.com/junegunn/fzf/blob/main/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.