Open-Source Wikis

/

fzf

/

Systems

/

Terminal UI

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 tests

Key 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| TTY

Both 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 --preview command, with optional border, label, scrollbar.
  • Border / margin windows — drawn around the above to implement the --border, --style, --margin, and --padding options.

Terminal.printList, printHeader, printFooter, printPreview, printInfo, printPrompt each handle one of these.

Event loop

Terminal.Loop runs as a goroutine. The body:

  1. Read a tui.Event from the renderer (Renderer.GetChar() for keys, Renderer.GetMouse() for mouse).
  2. Look up the event in the Keymap (parsed from --bind).
  3. For each chained action, run the corresponding case in the giant switch in Terminal.Loop (or in executeAction).
  4. If the user changed the query, send EvtSearchNew to the matcher via the event box.
  5. 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:

  1. execute, execute-silent — run synchronously, optionally without leaving fzf's UI. Implemented in executeCommand.
  2. become — exit fzf with ExitBecome, then have the wrapper (runProxy or main) re-exec the shell command. Used to "fall through" to the picked program (e.g. fzf --bind 'enter:become(vim {})').
  3. transform-* and reload — 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 for change-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: NewTerminal is constructed alongside the matcher. core.go calls terminal.Loop() to start the UI goroutine and the various Update* setters as events arrive.
  • From server.go: the HTTP server pushes parsed action lists into actionChannel, 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 through util.Executor which respects --with-shell and 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 in Terminal.Loop or in a helper function it calls.
  • Adding a new placeholder: update the placeholder regex, parsePlaceholder, and replacePlaceholder. Add Ruby integration tests in test/test_preview.rb or test/test_core.rb.
  • Adding a new border style: add a value to the BorderShape enum in src/tui/tui.go, implement drawing in both LightRenderer.drawBorder and TcellRenderer.drawBorder. Update parseBorder in src/options.go.
  • Adding a new window: model it after printPreview or printHeader. Make sure layout calculations in Terminal.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 Renderer interface. Don't add tcell-specific fields to Terminal.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Terminal UI – fzf wiki | Factory