junegunn/fzf
Terminal UI
Purpose
The terminal layer owns everything visible: the prompt, list, header, footer, preview, borders, info line, the cursor, key/mouse parsing, and the action engine that turns input events into state changes. It is the single largest part of fzf — src/terminal.go is over 8,000 lines.
The renderer abstraction in src/tui/tui.go keeps the screen-drawing primitives separate from the layout logic, so two backends (light and tcell) can coexist.
Directory layout
src/
├── terminal.go # Terminal: layout, action engine, key/mouse parsing
├── terminal_test.go
├── terminal_unix.go # POSIX-specific signal handling
├── terminal_windows.go # Windows console signal handling
└── tui/
├── tui.go # Renderer interface, Window type, Attr/Color, key codes
├── light.go # Default ANSI-escape renderer (uses x/term)
├── light_unix.go
├── light_windows.go
├── light_test.go
├── tcell.go # tcell-based renderer (build tag: tcell)
├── tcell_test.go
├── eventtype_string.go # Generated by stringer
├── ttyname_unix.go
├── ttyname_windows.go
├── tui_test.go
└── dummy.go # No-op renderer used in testsKey abstractions
| Symbol | File | Description |
|---|---|---|
Terminal |
src/terminal.go |
The TUI controller. Owns layout, state, key/mouse loop, action dispatch. |
NewTerminal(opts, eventBox, executor) |
src/terminal.go |
Constructs a terminal; chooses renderer based on options. |
Terminal.Loop() |
src/terminal.go |
The main UI goroutine; reads events, runs actions, redraws. |
Terminal.UpdateList(MatchResult) / UpdateCount / UpdateHeader / UpdateProgress |
src/terminal.go |
Update entry points called by the coordinator. |
Terminal.replacePlaceholder(...) |
src/terminal.go |
Substitutes {}, {q}, {n}, {1..3}, {+f}, {fzf:query}, etc. |
placeholder regex |
src/terminal.go |
The single source of truth for placeholder syntax. |
Renderer |
src/tui/tui.go |
The interface implemented by both backends. Methods include Init, Pause, Resume, Close, NewWindow, RefreshWindows. |
Window |
src/tui/tui.go |
A sub-region of the screen (list / preview / header / footer / border). |
LightRenderer |
src/tui/light.go |
Default renderer; writes ANSI escape codes via golang.org/x/term. |
TcellRenderer |
src/tui/tcell.go |
Built only with -tags tcell; uses github.com/gdamore/tcell/v2. |
Attr, Color, ColorPair, BorderShape |
src/tui/tui.go |
Rendering primitives. |
EventType |
src/tui/tui.go |
Key / mouse event identifiers. |
How it works
Renderer abstraction
graph TD
TERM[Terminal<br/>src/terminal.go] -->|interface calls| RENDERER[Renderer<br/>src/tui/tui.go]
RENDERER -.implemented by.-> LIGHT[LightRenderer<br/>src/tui/light.go]
RENDERER -.implemented by.-> TCELL[TcellRenderer<br/>src/tui/tcell.go]
LIGHT -->|raw ANSI| TTY[(TTY)]
TCELL -->|tcell screen| TTYBoth backends provide the same Renderer API. LightRenderer writes escape sequences directly with minimal abstraction — it knows about tput-style capabilities only insofar as it asks x/term for size and reads the user's terminal type. TcellRenderer delegates rendering to tcell's buffered screen.
The light renderer is the default because it produces a smaller binary and starts faster; the tcell renderer exists for terminals that the light path can't handle correctly (notably some legacy Windows console scenarios).
Layout
Terminal divides the screen into a tree of Windows. Each window is created via Renderer.NewWindow(top, left, width, height, preview, borderStyle) and rendered with RefreshWindows. The relevant windows are:
- List window — scrollable list of items.
- Header / footer windows — optional, can be inline, line-bordered, or block-bordered.
- Input window — prompt, query, info line.
- Preview window — rendered output of the
--previewcommand, with optional border, label, scrollbar. - Border / margin windows — drawn around the above to implement the
--border,--style,--margin, and--paddingoptions.
Terminal.printList, printHeader, printFooter, printPreview, printInfo, printPrompt each handle one of these.
Event loop
Terminal.Loop runs as a goroutine. The body:
- Read a
tui.Eventfrom the renderer (Renderer.GetChar()for keys,Renderer.GetMouse()for mouse). - Look up the event in the
Keymap(parsed from--bind). - For each chained action, run the corresponding case in the giant switch in
Terminal.Loop(or inexecuteAction). - If the user changed the query, send
EvtSearchNewto the matcher via the event box. - Redraw.
The action set is enumerated as actionType constants in src/options.go, with string representations generated into src/actiontype_string.go. There are 200+ action types.
Key parsing
Key bindings can be specified by name (ctrl-a, alt-up, f12, tab), by a literal character (x), or by a named event (change, result, focus, start, load, one, zero, jump). The parser in src/options.go (parseKeyChords, parseKeymap) builds a map[tui.Event]actionList.
Mouse events are parsed by both renderers; the renderer normalizes them to tui.Event values before passing to the terminal loop.
Placeholder substitution
The placeholder regex at the top of src/terminal.go:
\\?(?:{[+*sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{[+*]?f?nf?})replacePlaceholder walks every match, looks up the corresponding state (current item, multi-selected items, query, prompt, action name, file token slice), and substitutes. Modifiers:
+— operate on multi-selected items.*— operate on all items.s— separator-aware (used with{q:s5}etc.).f— output to a temp file and substitute the path (avoids shell length limits).r— pass raw, no shell escaping.n—(file)index, not contents.
Execute / become / transform actions
Three families of actions run external commands:
execute,execute-silent— run synchronously, optionally without leaving fzf's UI. Implemented inexecuteCommand.become— exit fzf withExitBecome, then have the wrapper (runProxyormain) re-exec the shell command. Used to "fall through" to the picked program (e.g.fzf --bind 'enter:become(vim {})').transform-*andreload— run the command, capture stdout, treat it as the new value of the corresponding option (prompt,header,query,nth,with-nth) or as new input. The stdout is parsed; the renderer is paused while items are mutated forchange-with-nth.
Bracketed paste and image protocol passthrough
passThroughBeginRegex and passThroughEndTmuxRegex in src/terminal.go recognize tmux passthrough wrappers, kitty graphics escape sequences, sixel, and iTerm2 image protocol. The terminal forwards those byte ranges verbatim so previews can include images.
Integration points
- From
core.go:NewTerminalis constructed alongside the matcher.core.gocallsterminal.Loop()to start the UI goroutine and the variousUpdate*setters as events arrive. - From
server.go: the HTTP server pushes parsed action lists intoactionChannel, which the terminal loop drains. - To
core.go:EvtSearchNew(new query / nth / with-nth / reload),EvtQuit(user accepted or aborted) are set on the event box. - To
executor(src/util/util.go): all subprocess invocations go throughutil.Executorwhich respects--with-shelland the OS-specific shell defaults.
Entry points for modification
- Adding a new action: see Patterns and conventions. The actual behavior lands in
src/terminal.go, usually in the action switch inTerminal.Loopor in a helper function it calls. - Adding a new placeholder: update the
placeholderregex,parsePlaceholder, andreplacePlaceholder. Add Ruby integration tests intest/test_preview.rbortest/test_core.rb. - Adding a new border style: add a value to the
BorderShapeenum insrc/tui/tui.go, implement drawing in bothLightRenderer.drawBorderandTcellRenderer.drawBorder. UpdateparseBorderinsrc/options.go. - Adding a new window: model it after
printPrevieworprintHeader. Make sure layout calculations inTerminal.layoutWindows(or its equivalent) account for it; otherwise sizes will silently overflow. - Renderer-only features: if a feature affects only one renderer, isolate it behind the
Rendererinterface. Don't add tcell-specific fields toTerminal.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.