junegunn/fzf
Patterns and conventions
This page documents the recurring code patterns that show up across the Go core. Read this before adding a new feature so the new code fits the existing seams.
Event coordination via EventBox
fzf's primary message bus is util.EventBox (src/util/eventbox.go). It's a sync.Cond-backed map of EventType → any. Producers call Set(evt, value). Consumers call Wait(callback) and process the event map under the lock, then Clear() the events they handled.
Three event boxes run concurrently:
| Owner | Consumer | Events |
|---|---|---|
Reader (src/reader.go) |
Run coordinator (src/core.go) |
EvtReadNew, EvtReadFin |
Terminal (src/terminal.go) |
Run coordinator |
EvtSearchNew, EvtQuit |
Matcher (src/matcher.go) |
Run coordinator |
EvtSearchProgress, EvtSearchFin |
When you add a feature that needs to coordinate between Reader, Matcher, and Terminal, route it through these events instead of adding direct method calls. That keeps the goroutines decoupled.
Bumping the input revision
src/core.go defines a revision struct (major, minor). Whenever input changes (a reload action restarts the reader, or change-nth/change-with-nth rewrites items in place), bumpMajor or bumpMinor is called. The matcher's cached results are keyed on the revision, so stale results are discarded automatically. New code that mutates inputs must bump the revision (search for inputRevision.bumpMinor() for examples).
The action engine
Every key press, mouse event, named event (change, focus, result, load, start, jump, one), and HTTP --listen command flows through the same machinery in src/terminal.go:
- Parsed
--binddefinitions live in theOptions.Keymapmap. - The terminal loop reads the next event, looks it up, and walks the chained list of
actionstructs. - Each
actionhas anactionType(200+ entries insrc/actiontype_string.go) and an optional argument string. executeCommand/executeBecome/transformValuehandle the actions that shell out.
When adding a new action:
- Add a constant to the
actionTypeenum insrc/options.go. - Run
make generate(orgo generate ./...) to regeneratesrc/actiontype_string.go. - Wire parsing in
parseAction(alsosrc/options.go). - Implement the behavior in
src/terminal.go's big action switch. - Update the man page (
man/man1/fzf.1) and theUsageconstant.
Placeholders
src/terminal.go defines a placeholder regex covering {}, {q}, {n}, {1..3}, {+f}, {fzf:query}, {fzf:action}, {fzf:prompt}, etc. Substitution happens through replacePlaceholder and parsePlaceholder. Any new placeholder must be added to the regex and to those two functions, with a corresponding test in src/terminal_test.go and test/test_preview.rb / test/test_core.rb.
Error handling
- Library code returns errors.
src/core.gotranslates errors into exit codes via theExitOk/ExitNoMatch/ExitError/ExitInterrupt/ExitBecomeconstants insrc/constants.go. - The
mainfunction inmain.goonly ever callsos.Exitthrough the smallexit(code, err)helper. Don't sprinkleos.Exitcalls elsewhere —defer util.RunAtExitFuncs()(insrc/core.go) needs to fire so registered cleanup hooks (likeos.Remove(temp)) actually run. - Renderer cleanup is handled by the
Renderer.Close()defer inTerminal.Loop, which restores the terminal to a sane state on panic too. Don't bypass it.
--no-X flags and "flag inversion"
Most boolean flags have a --no-X pair (e.g. --bold / --no-bold, --mouse / --no-mouse, --ansi / --no-ansi). Both forms parse to the same field via the optionalNumeric and validateBoolean helpers in src/options.go. When adding a boolean, follow the same pattern so users can override FZF_DEFAULT_OPTS from the command line.
Lazy / transform-* values
A subset of options can be either a literal value or a shell command whose output is the value:
--prompt/transform-prompt--header/transform-header--query/transform-query--nth/change-nth--with-nth/change-with-nth
The transform path runs the shell command, captures stdout, and treats the output as the new value. Implementations parse a leading transform: prefix or distinguish via the action name. New "transformable" options should follow the same convention, and must mark the change with the appropriate revision bump as described above.
Concurrency rules
- The
ChunkListis the only shared mutable state during a search. It uses an internalsync.Mutex. Always go throughPush/Snapshot/ForEachItem; never poke atchunksdirectly. - The
Matcherpartitions a snapshot acrossruntime.NumCPU()goroutines and uses pre-allocatedSlabs insrc/util/slab.go. Avoid per-scan heap allocation — that is the single biggest reason fzf is fast on millions of items. Terminalowns the screen. The Reader and Matcher must never write to the renderer.- Cancellation:
Matcher.CancelScanandTerminal.PauseRendering/ResumeRenderingexist precisely so an action can pause the matcher and renderer while it mutates items. Use them when adding any operation that touches items in place.
Build tags
tcell— usessrc/tui/tcell.goinstead ofsrc/tui/light.go. Keep both renderers feature-equivalent. New rendering features need to land in both files (or be guarded explicitly).pprof— flips the implementation of profiling flags betweensrc/options_pprof.goandsrc/options_no_pprof.go. Don't reference profiling APIs outside those files.- The Windows path uses
_windows.gosuffixed files (e.g.src/util/util_windows.go,src/tui/light_windows.go,src/winpty_windows.go). Match the existing split when adding OS-specific code.
Test conventions
- Go tests live next to their subject and use the standard
testingpackage, no third-party assertion library. - Ruby integration tests use Minitest; the
Tmuxhelper intest/lib/is the only reasonable way to assert on TUI behavior. - When adding a flag, add at least one Ruby integration test exercising the user-visible behavior. Unit tests on
src/options_test.gocover parsing, but rendering needs the tmux harness.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.