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 ttyCore 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 onWait()(condition variable) and are woken onSet(). Events can be silently ignored (viaUnwatch) to suppress noise during certain phases (e.g., suppress EvtReadNew while waiting for EvtReadFin). - Key types:
EventBox,Events(type alias formap[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[]bytelines toChunkListvia apushercallback. SignalsEvtReadNew(batch ready) andEvtReadFin(done) on theEventBox. 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
Itemobjects, partitioned into fixed-sizeChunkarrays (1024 items).Snapshot()returns a copy of chunk pointers (not data), enabling concurrent reads by the Matcher without locking the write path.Clear()resets forreloadactions. - 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[]Resultfor 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
reqBoxfor Reset/Quit signals. On eachLoop()iteration waits for aMatchRequest, spins upmin(NumCPU, numChunks)goroutines (using an atomic chunk-index counter), collects[]Resultpartial results, merges them into aMerger, and postsEvtSearchFinon the sharedEventBox. Supports cooperative cancellation viacancelScanatomic flag andCancelScan()/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 callsalgo.FuzzyMatchV2(or V1) per term, scoring and accumulatingResultobjects. - 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 allEvtSearchNew/EvtQuitsignalling back to the coordinator. The Terminal’sLoop()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
Rendererinterface with two implementations:LightRenderer(direct termios+ANSI escape codes, no deps, default) andTcellRenderer(backed bytcell/v2, enabled via-tags tcell). DefinesColor,Attr,Event, andCelltypes. - Key types:
Renderer(interface),LightRenderer,TcellRenderer
Server (httpServer)#
- Package:
github.com/junegunn/fzf/src - File:
src/server.go - Responsibility: Optional HTTP server started when
--listenis passed. AcceptsPOST /with a plain-text body of fzf actions (same action language as key bindings). Dispatches toactionChannelwhich the Terminal consumes. Also handlesGET /for querying current state. Supports API key auth and Unix domain sockets. - Key types:
httpServer,listenAddress - Dependencies:
net, Terminal’sactionChannel
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-acceleratedIndexByte2helpers (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.PrinterBypasses 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 loopNo 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):
- Command-line flags — parsed via a custom flag parser (not
flagstdlib, not cobra; fzf rolls its own to support the--opt=val,--no-opt,-1/+1style flags unique to fzf) FZF_DEFAULT_OPTS_FILE— path to a file of whitespace-separated optionsFZF_DEFAULT_OPTS— env var with additional options (lower precedence)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#
EventBox over channels for inter-goroutine signalling. The
EventBoxis amap[EventType]anyunder 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.Chunk-based parallel scan with atomic work-stealing. The Matcher divides the chunk snapshot among
NumCPUgoroutines using anatomic.Int32counter (next chunk index). Workers self-assign chunks; no pre-partitioning. This gives near-perfect load balancing when chunks have unequal match density.Dual TUI renderer via build tags.
LightRendererrequires onlygolang.org/x/sys(for termios) and implements ANSI escape sequences directly — no allocation-heavy abstraction.TcellRendereris opt-in for users who need tcell’s richer terminal compatibility. TheRendererinterface is defined intui.goand both backends satisfy it, making the switch entirely transparent to Terminal.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.Streaming filter mode for zero-overhead passthrough. When
--filteris 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.