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
-mor--multi[=MAX]to enable multi-select; parsed insrc/options.goaround line 96. - State Storage: Selected items are tracked in a
map[int32]selectedItemdefined at line 398 ofsrc/terminal.go. - Key Bindings: TAB triggers
actToggleDownand Shift-TAB triggersactToggleUp, bound at lines 28-30. - Toggle Logic: The
toggleItemmethod (lines 56-61) switches betweenselectItem(lines 22-34) anddeselectItem(lines 43-46), enforcing thet.multilimit. - Cursor Movement: After toggling,
vmove(-1)orvmove(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 theselectedmap.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →