How fzf Implements Multi-Select with the -m Option and TAB Key

fzf enables multi-selection via the -m or --multi flag, storing selected items in a map[int32]selectedItem and binding the TAB key to actToggleDown (and Shift-TAB to actToggleUp) to toggle selection state while navigating the list.

The junegunn/fzf command-line fuzzy finder supports multi-select mode, allowing users to select multiple items using the TAB key when invoked with the -m option. This feature is implemented through a combination of command-line flag parsing in src/options.go and state management within the Terminal struct in src/terminal.go. Understanding how fzf handles multi-select reveals the elegant Go patterns used to manage selection state, key bindings, and visual feedback.

Enabling Multi-Select Mode

Multi-selection is activated by passing the -m or --multi[=MAX] flag. The default maximum number of selections is math.MaxInt32, effectively unlimited for practical purposes.

In src/options.go around line 96, the flag parser sets opts.Multi to the specified maximum:

// src/options.go ~L96
// Parsing: -m, --multi[=MAX]
opts.Multi = max // int value, default math.MaxInt32

When opts.Multi > 0, the Terminal initialization creates a map to track selections at line 398 in src/terminal.go:

// src/terminal.go ~L398
selected map[int32]selectedItem   // key: item index, value: selection metadata

Each entry stores the item's index and a timestamp recording when it was selected.

Key Bindings for TAB and Shift-TAB

The default keymap binds the physical Tab key to the action actToggleDown and Shift-Tab to actToggleUp. These bindings are established in src/terminal.go at lines 28-30:

// src/terminal.go ~L28-L30
add(tui.Tab, actToggleDown)      // Tab key
add(tui.ShiftTab, actToggleUp)    // Shift-Tab key

Both actions are no-ops unless multi-select is active (t.multi > 0), ensuring the keys perform standard completion when -m is not used.

The Toggle Logic

When a user presses Tab, the doAction method in Terminal receives actToggleDown and enters the switch case at lines 31-40 in src/terminal.go:

// src/terminal.go ~L31-L40
case actToggleDown:
    if t.multi > 0 && t.merger.Length() > 0 && toggle() {
        t.vmove(-1, true)   // move cursor up after toggling
        req(reqList)
    }
case actToggleUp:
    if t.multi > 0 && t.merger.Length() > 0 && toggle() {
        t.vmove(1, true)    // move cursor down after toggling
        req(reqList)
    }

The toggle closure (defined locally around lines 32-38) retrieves the current item and invokes t.toggleItem:

// src/terminal.go ~L32-L38 (within handleEvent)
toggle := func() bool {
    current := t.currentItem()
    if current != nil && t.toggleItem(current) {
        req(reqInfo)
        return true
    }
    return false
}

Selecting and Deselecting Items

The toggleItem method at lines 56-61 in src/terminal.go implements the state transition:

// src/terminal.go ~L56-L61
func (t *Terminal) toggleItem(item *Item) bool {
    if _, found := t.selected[item.Index()]; !found {
        return t.selectItem(item)   // not selected → select
    }
    t.deselectItem(item)          // already selected → deselect
    return true
}

selectItem (lines 22-34) enforces the maximum limit (t.multi) and adds the item to the map:

// src/terminal.go ~L22-L34
func (t *Terminal) selectItem(item *Item) bool {
    if len(t.selected) >= t.multi {
        return false // max reached
    }
    t.selected[item.Index()] = selectedItem{
        index: item.Index(),
        time:  time.Now(),
    }
    t.version++
    return true
}

deselectItem (lines 43-46) removes the entry:

// src/terminal.go ~L43-L46
func (t *Terminal) deselectItem(item *Item) {
    delete(t.selected, item.Index())
    t.version++
}

Both functions increment t.version to trigger a UI redraw.

Visual Feedback

When items are selected, fzf provides immediate visual feedback through markers and status indicators.

Selection Markers

The selection marker (default > or ┃) is configured via --marker and --marker-multi-line. During rendering at line 3457 in src/terminal.go, the selected map is consulted to apply the marker and highlight color to active lines.

Status Counter

The status line displays the current selection count in the format (+3/5), indicating three items selected out of a maximum of five. This is assembled at line 2936:

// src/terminal.go ~L2936
fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)

Summary

  • Activation: Pass -m or --multi[=MAX] to enable multi-select; parsed in src/options.go around line 96.
  • State Storage: Selected items are tracked in a map[int32]selectedItem defined at line 398 of src/terminal.go.
  • Key Bindings: TAB triggers actToggleDown and Shift-TAB triggers actToggleUp, bound at lines 28-30.
  • Toggle Logic: The toggleItem method (lines 56-61) switches between selectItem (lines 22-34) and deselectItem (lines 43-46), enforcing the t.multi limit.
  • Cursor Movement: After toggling, vmove(-1) or vmove(1) shifts the cursor to allow rapid selection of adjacent items.
  • UI Feedback: Selection markers and the (+count/max) status indicator are rendered by checking the selected map.

Frequently Asked Questions

How do I enable multi-select mode in fzf?

Pass the -m or --multi flag when invoking fzf. You can optionally specify a maximum number of selections with --multi=MAX (e.g., fzf -m5 limits you to five items). This flag is parsed in src/options.go and initializes the selection map in the Terminal struct.

What is the difference between TAB and Shift-TAB in fzf multi-select?

TAB (actToggleDown) selects the current item and moves the cursor up one line, while Shift-TAB (actToggleUp) selects the current item and moves the cursor down one line. This "select and continue" behavior allows rapid selection of multiple adjacent items without changing direction. Both keys call the toggleItem method to update the selected map.

How does fzf enforce the maximum selection limit?

When selectItem is invoked (around line 22 in src/terminal.go), it checks if len(t.selected) >= t.multi. If the limit is reached, the function returns false and the new item is not added to the selected map. The default maximum is math.MaxInt32, effectively unlimited for most use cases.

Where does fzf store the selected items during a session?

Selected items are stored in a map[int32]selectedItem named selected, defined at line 398 in src/terminal.go. The map uses the item's index as the key and stores a struct containing the index and selection timestamp. This map is checked during rendering to display selection markers and the status counter showing (+count/max).

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 →