fzf — Architecture#

Architectural style#

Event-driven coordinator with parallel pipeline stages

fzf is structured as a single-binary application whose core is an explicit event coordination loop (Run() in src/core.go). Four concurrent components — Reader, ChunkList/Cache, Matcher, and Terminal — run in separate goroutines and communicate through a shared EventBox (a mutex+condition-variable-backed event bus defined in src/util/eventbox.go). There is no dependency injection framework, no service locator, and no plugin system; the architecture is direct and imperative. Each component is a self-contained struct with a Loop() goroutine and a private reqBox for intra-component requests.

The pattern resembles a staged pipeline (Reader → ChunkList → Matcher → Terminal) but the stages do not push data forward through channels; instead they post events into the shared EventBox and the coordinator reacts, taking a snapshot and dispatching to the next stage. This decoupling allows the coordinator to coalesce rapid events (e.g., multiple EvtReadNew before the first EvtReadFin) and add backpressure via a configurable delay (coordinatorDelayStep, coordinatorDelayMax).

Component diagram (textual)#

  main.go
  ────────
  protector.Protect()
  ParseOptions()   ──────►  fzf.Run(opts)
                            │
                            │  creates
                            ├──► EventBox  (shared signal bus)
                            ├──► ChunkList (item store, chunk-based)
                            ├──► Reader    (goroutine)
                            ├──► Matcher   (goroutine, NumCPU workers)
                            ├──► Terminal  (goroutine, event loop)
                            └──► Server    (goroutine, optional --listen)
                                    │
                            ┌───────▼──────────────────────────────────┐
                            │          Event coordination loop          │
                            │  (core.go Run(), single goroutine)        │
                            │                                           │
                            │  EvtReadNew/Fin  ──► chunkList.Snapshot  │
                            │                      ──► matcher.Reset   │
                            │                                           │
                            │  EvtSearchNew    ──► matcher.Reset       │
                            │  (from Terminal)     (may restart reader) │
                            │                                           │
                            │  EvtSearchFin    ──► terminal.UpdateList │
                            │  EvtSearchProgress ► terminal.UpdateProg │
                            │                                           │
                            │  EvtQuit         ──► break + return code │
                            └───────────────────────────────────────────┘

  stdin / cmd
  ──────────► Reader.ReadSource()
                  │  []byte  (per line)
                  ▼
             ChunkList.Push()  →  [Chunk₀][Chunk₁]...[ChunkN]
                  │                 (1024 items each)
                  │ Snapshot (copy of chunk pointers)
                  ▼
             Matcher.scan()
                  │  NumCPU goroutines, each assigned chunks via atomic counter
                  │  Each goroutine: Pattern.Match(chunk) → []Result
                  │  Results merged by Merger (radix-sorted if needed)
                  ▼
             Terminal.UpdateList()  →  renders to tty

Core components#

EventBox#

  • Package: github.com/junegunn/fzf/src/util
  • File: src/util/eventbox.go
  • Responsibility: Thread-safe event bus. Stores the latest value per event type in a map[EventType]any. Goroutines block on Wait() (condition variable) and are woken on Set(). Events can be silently ignored (via Unwatch) to suppress noise during certain phases (e.g., suppress EvtReadNew while waiting for EvtReadFin).
  • Key types: EventBox, Events (type alias for map[EventType]any)
  • Dependencies: sync.Cond, nothing else

Reader#

  • Package: github.com/junegunn/fzf/src
  • File: src/reader.go
  • Responsibility: Reads items from stdin, an external command, or the filesystem (via fastwalk). Pushes raw []byte lines to ChunkList via a pusher callback. Signals EvtReadNew (batch ready) and EvtReadFin (done) on the EventBox. Supports termination (terminate()), restart (restart(command, environ)), and zero-delimited input (--read0).
  • Key types: Reader
  • Dependencies: EventBox, util.Executor (for shell command execution), fastwalk (filesystem walk)

ChunkList#

  • Package: github.com/junegunn/fzf/src
  • File: src/chunklist.go
  • Responsibility: Append-only store of Item objects, partitioned into fixed-size Chunk arrays (1024 items). Snapshot() returns a copy of chunk pointers (not data), enabling concurrent reads by the Matcher without locking the write path. Clear() resets for reload actions.
  • Key types: ChunkList, Chunk, Item
  • Dependencies: ChunkCache

ChunkCache#

  • Package: github.com/junegunn/fzf/src
  • File: src/cache.go
  • Responsibility: Two-level cache keyed by (chunk, pattern_string). Stores pre-scored []Result for a chunk+pattern combination so repeated queries with identical prefixes skip re-scanning. Invalidated on revision bump (new items or reload).
  • Key types: ChunkCache

Matcher#

  • Package: github.com/junegunn/fzf/src
  • File: src/matcher.go
  • Responsibility: Runs fuzzy search over a snapshot of chunks. Maintains an internal reqBox for Reset/Quit signals. On each Loop() iteration waits for a MatchRequest, spins up min(NumCPU, numChunks) goroutines (using an atomic chunk-index counter), collects []Result partial results, merges them into a Merger, and posts EvtSearchFin on the shared EventBox. Supports cooperative cancellation via cancelScan atomic flag and CancelScan()/ResumeScan() for safe in-place item mutation.
  • Key types: Matcher, MatchRequest, MatchResult
  • Dependencies: ChunkCache, Pattern, Merger, util.EventBox, util.Slab

Pattern#

  • Package: github.com/junegunn/fzf/src
  • File: src/pattern.go
  • Responsibility: Represents a compiled search query. Supports fuzzy, exact, prefix, suffix, inverse, and extended multi-term queries. Match(chunk, slab) iterates items and calls algo.FuzzyMatchV2 (or V1) per term, scoring and accumulating Result objects.
  • Key types: Pattern, Token, termSet
  • Dependencies: src/algo

Merger#

  • Package: github.com/junegunn/fzf/src
  • File: src/merger.go
  • Responsibility: Merges sorted partial result slices (one per worker goroutine) into a single virtual sorted sequence using a lazy k-way merge. Get(i) performs the merge on demand. Also handles the pass-through merger (no query — returns all items in chunk order).
  • Key types: Merger

Terminal#

  • Package: github.com/junegunn/fzf/src
  • File: src/terminal.go (+ terminal_unix.go, terminal_windows.go)
  • Responsibility: Largest component (~4000 lines). Owns the interactive TUI: opens the tty, starts the tui.Renderer, processes keyboard/mouse events, manages key bindings and actions (functions.go, actiontype_string.go), handles preview (--preview), multi-select, header, prompt rendering, and all EvtSearchNew / EvtQuit signalling back to the coordinator. The Terminal’s Loop() is the primary event consumer for user input.
  • Key types: Terminal, action, actionType, fitpad
  • Dependencies: tui.Renderer, util.EventBox, util.Executor

TUI Layer (tui package)#

  • Package: github.com/junegunn/fzf/src/tui
  • Files: tui.go, light.go (+ unix/windows variants), tcell.go
  • Responsibility: Abstracts terminal I/O behind a Renderer interface with two implementations: LightRenderer (direct termios+ANSI escape codes, no deps, default) and TcellRenderer (backed by tcell/v2, enabled via -tags tcell). Defines Color, Attr, Event, and Cell types.
  • Key types: Renderer (interface), LightRenderer, TcellRenderer

Server (httpServer)#

  • Package: github.com/junegunn/fzf/src
  • File: src/server.go
  • Responsibility: Optional HTTP server started when --listen is passed. Accepts POST / with a plain-text body of fzf actions (same action language as key bindings). Dispatches to actionChannel which the Terminal consumes. Also handles GET / for querying current state. Supports API key auth and Unix domain sockets.
  • Key types: httpServer, listenAddress
  • Dependencies: net, Terminal’s actionChannel

Fuzzy Algorithm (algo package)#

  • Package: github.com/junegunn/fzf/src/algo
  • Responsibility: Smith-Waterman-inspired dynamic programming fuzzy match (FuzzyMatchV2) with bonus scoring for camelCase, path boundaries, and start-of-word. Also provides exact/prefix/suffix matchers. Has SIMD-accelerated IndexByte2 helpers (indexbyte2_amd64.s, indexbyte2_arm64.s).
  • Key types: Result (score, positions), scoring bonus tables

Data flow#

Interactive mode (typical user session)#

1. ParseOptions()  →  Options struct
2. Run() wires up EventBox, ChunkList, Matcher, Terminal, Reader
3. reader goroutine: ReadSource → pushes []byte lines → ChunkList.Push
   ↳ polls EvtReadNew every 10–50ms via startEventPoller
4. Coordinator receives EvtReadNew:
   ↳ chunkList.Snapshot() → []* Chunk (zero-copy)
   ↳ matcher.Reset(snapshot, query, ...) → posts reqRetry on matcher.reqBox
5. Matcher goroutine receives MatchRequest:
   ↳ spins N=NumCPU goroutines, each atomically claims chunks
   ↳ each goroutine: Pattern.Match(chunk, slab) → []Result
   ↳ radix-sort per worker (if sort enabled)
   ↳ NewMerger(partialResults) → posts EvtSearchFin on shared eventBox
6. Coordinator receives EvtSearchFin:
   ↳ terminal.UpdateList(MatchResult)
7. Terminal renders the updated list on the tty
8. User types → Terminal captures keypress:
   ↳ runs bound action (e.g., change query)
   ↳ posts EvtSearchNew{changed: true} on eventBox
9. Coordinator receives EvtSearchNew → goto 4 (with new query runes)
10. User presses Enter:
    ↳ Terminal posts EvtQuit{code: ExitOk}
    ↳ Coordinator breaks loop, calls opts.Printer for each selected item
    ↳ Returns exit code to main()

Filter mode (non-interactive: fzf --filter=query)#

stdin → Reader (streaming) → Pattern.MatchItem() per item → opts.Printer

Bypasses ChunkList, Matcher Loop, and Terminal entirely. Supports streaming (no sort/tac) or batch (reads all, then scans, then prints).

Initialization / Bootstrap#

main.go:
  1. protector.Protect()         — OpenBSD pledge(2); no-op elsewhere
  2. fzf.ParseOptions(true, args) — builds *Options from flags+env
  3. fzf.Run(opts)               — creates all components and enters loop

fzf.Run(opts):
  a. postProcessOptions(opts)    — validate, expand paths, resolve conflicts
  b. util.NewEventBox()          — shared event bus
  c. NewChunkCache() / NewChunkList() — item store
  d. util.NewExecutor()          — shell command runner
  e. NewTerminal(opts, eventBox, executor) — TUI; may open /dev/tty
  f. NewReader(pusher, eventBox, ...)
  g. go reader.ReadSource(...)   — starts reading; blocks until ready on readyChan
  h. NewMatcher(cache, patternBuilder, ...)
  i. go matcher.Loop()           — starts waiting for MatchRequests
  j. go terminal.Loop()          — starts TUI; sends to terminal.startChan when ready
  k. Enter coordinator event loop

No dependency injection framework. All wiring is manual in Run(). Components receive the eventBox and each other’s channels/callbacks as constructor arguments. This is a textbook example of manual DI without a container.

The tmux.go and zellij.go files handle a special case: when --tmux or --zellij is passed, Run() re-execs itself as a child process inside a new tmux/zellij popup pane, using os.Args and a UNIX socket for IPC, before entering the normal initialization path.

Configuration#

ParseOptions() in src/options.go parses all configuration. No Viper, no YAML config files. Configuration sources (in order of precedence):

  1. Command-line flags — parsed via a custom flag parser (not flag stdlib, not cobra; fzf rolls its own to support the --opt=val, --no-opt, -1/+1 style flags unique to fzf)
  2. FZF_DEFAULT_OPTS_FILE — path to a file of whitespace-separated options
  3. FZF_DEFAULT_OPTS — env var with additional options (lower precedence)
  4. FZF_DEFAULT_COMMAND — env var for the default input command

All parsed into a single *Options struct. Platform-sensitive defaults are resolved in postProcessOptions().

Key design decisions#

  1. EventBox over channels for inter-goroutine signalling. The EventBox is a map[EventType]any under a condition variable. This means posting the same event twice coalesces — only the latest value is delivered. This is exactly right for search progress: a burst of EvtReadNew during fast ingestion collapses into a single notification, preventing the matcher from restarting on every line.

  2. Chunk-based parallel scan with atomic work-stealing. The Matcher divides the chunk snapshot among NumCPU goroutines using an atomic.Int32 counter (next chunk index). Workers self-assign chunks; no pre-partitioning. This gives near-perfect load balancing when chunks have unequal match density.

  3. Dual TUI renderer via build tags. LightRenderer requires only golang.org/x/sys (for termios) and implements ANSI escape sequences directly — no allocation-heavy abstraction. TcellRenderer is opt-in for users who need tcell’s richer terminal compatibility. The Renderer interface is defined in tui.go and both backends satisfy it, making the switch entirely transparent to Terminal.

  4. HTTP server as a control plane. --listen [addr] exposes fzf’s action system over HTTP. External processes (shell scripts, editors, other tools) can POST action strings (change-query(foo)\nexecute(echo bar)) to manipulate a running fzf instance. This transforms fzf from a filter into a reusable interactive UI component — a significant architectural upgrade over the traditional Unix pipe model.

  5. Streaming filter mode for zero-overhead passthrough. When --filter is combined with no sorting and no --tac, fzf bypasses the entire chunk/matcher/terminal architecture and runs as a simple streaming transformer: read line → match → print. Memory usage stays constant regardless of input size, making it safe to embed in pipelines processing very large streams.