junegunn/fzf
Process modes
Purpose
fzf can run as a plain in-process TUI, or it can re-execute itself inside a wrapper that owns the terminal. The wrappers exist so a single command-line invocation can pop fzf inside a tmux popup, a Zellij floating pane, or a Windows winpty session, without the calling shell having to know about it.
Files
| File | Purpose |
|---|---|
src/proxy.go |
The shared "spawn a wrapper, pipe stdin/stdout through fifos, handle ExitBecome" logic. |
src/proxy_unix.go |
POSIX-specific helpers (named-pipe creation). |
src/proxy_windows.go |
Windows equivalents. |
src/tmux.go |
runTmux: builds the tmux display-popup command line and calls runProxy. |
src/zellij.go |
runZellij: same idea for Zellij. |
src/winpty.go |
runWinpty: re-execs fzf under winpty on Windows when ConPTY is unavailable. |
src/winpty_windows.go |
Windows-specific implementation. |
bin/fzf-tmux |
Older shell-script wrapper that predates the in-binary --tmux. Still shipped for users on tmux < 3.3. |
Key abstractions
| Symbol | File | Description |
|---|---|---|
runProxy(commandPrefix, cmdBuilder, opts, withExports) |
src/proxy.go |
The shared entry point. Creates fifos, spawns the wrapper, copies bytes, propagates ExitBecome. |
popupArgStr(args, opts) |
src/proxy.go |
Builds the inner fzf command line by quoting os.Args, dropping options that don't make sense inside a popup, and adding --no-popup --no-height. |
fifo(name) |
src/proxy.go |
Allocates a named pipe under os.TempDir(). |
runTmux(args, opts) |
src/tmux.go |
Wraps runProxy with tmux display-popup arguments. |
runZellij(args, opts) |
src/zellij.go |
Wraps runProxy with Zellij's floating pane invocation. |
runWinpty(args, opts) |
src/winpty.go |
Wraps runProxy with winpty on Windows. |
becomeSuffix = ".become" |
src/proxy.go |
Filename suffix for the file the inner fzf writes when become(...) fires. |
How it works
graph TD
OUTER[Outer fzf process<br/>main.go] --> CHECK{popup or<br/>winpty?}
CHECK -- yes --> PROXY[runProxy<br/>src/proxy.go]
CHECK -- no --> RUN[fzf.Run<br/>direct TUI]
PROXY --> FIFO[Create input/output fifos]
PROXY --> WRAPPER[Spawn wrapper:<br/>tmux / zellij / winpty]
WRAPPER --> INNER[Inner fzf process]
INNER --> WRITES[Writes selection to output fifo]
PROXY --> READS[Outer reads output fifo,<br/>writes to user stdout]
WRITES --> READS
INNER -- ExitBecome --> BECOME[Outer reads .become file,<br/>execs target program]Step-by-step
- The outer fzf process parses options. If
--tmux(or--popupfor Zellij, orneedWinpty) is set, it callsrunTmux/runZellij/runWinptybefore doing anything else. popupArgStrbuilds the inner fzf command line: it shell-quotesos.Args[0], appends the user's options minus the popup-relevant ones, and adds--no-popup --no-height.runProxycreates two named pipes (fzf-proxy-input-<ns>andfzf-proxy-output-<ns>) under$TMPDIR. If the input is a TTY, only the output pipe is needed; otherwise the outer process spools its stdin into the input pipe.runProxywrites a small shell snippet to a temp file (exports + command) and asks the wrapper to run it. The exports preserve environment variables, including bash-exported functions (BASH_FUNC_*).- The wrapper (e.g.
tmux display-popup -E -d <dir> ... sh /tmp/...) launches the inner fzf inside the popup. That inner process is a normalfzf.Runcall; it doesn't know it's nested. - The inner fzf writes its selection to the output pipe. The outer process copies the pipe to its own stdout (or to
opts.Outputif set programmatically). becomeactions: if the inner fzf wants to fall through to a target command (fzf --bind 'enter:become(vim {})'), it writes the command + environment to<temp>.becomeand exits withExitBecome. The outer process detects this exit code, reads the file, and re-execs the target command usingutil.Executor.Become(which callssyscall.Execon POSIX, replacing the outer process). This is howbecomecan leave the user insidevimafter fzf disappears, even from inside a tmux popup.
Tmux popup specifics
src/tmux.go translates --tmux options into tmux display-popup flags:
- Position:
up→-xC -y0,down→-xC -y9999,left→-x0 -yC,right→-xR -yC,center→-xC -yC. - Width / height come from
opts.Tmux.width/.height. - Border:
-Bis added unlessopts.Tmux.borderis true.
This requires tmux 3.3+ for the -E flag. Users on older tmux can use bin/fzf-tmux (a shell script) which uses a regular split window.
Zellij popup
Zellij gained equivalent functionality more recently. src/zellij.go follows the same runProxy pattern, building zellij run --floating ... invocations.
Winpty
On Windows without ConPTY, terminals can't allocate a real PTY. runWinpty re-execs fzf under winpty.exe (which the user must have installed) so the inner process gets a working PTY. The detection logic is in needWinpty(opts) in src/winpty_windows.go.
Integration points
- From
core.go: the very first thingRundoes (whenopts.Filter == nil) is consultopts.useTmux(),opts.useZellij(), andneedWinpty(opts). If any returns true,Rundelegates to the corresponding wrapper and returns its exit code. - From the inner process: the inner fzf doesn't need to know it's nested. Special case:
--no-popupand--no-heightare added to the inner command line so the user's own popup/height options don't accidentally compose. util.Executor.Become: shared with the regular (non-popup)becomepath. The proxy reads the<temp>.becomefile and forwards env + command to the executor.
Entry points for modification
- New popup backend: add a
runXfunction and anopts.useX()predicate. ReuserunProxyand provide acmdBuilderthat returns the right*exec.Cmd. - Tmux options: if you add to
opts.Tmux, extendpopupArgStrto optionally honor it inside the popup, andrunTmuxto forward it todisplay-popup. - Become file format: the format is "command \x00 env\n env\n ..."; if you change it, both
runProxyand the inner-processbecomewriter must agree. - Windows path:
proxy_unix.goandproxy_windows.goare siblings; keep them in sync. mintty has a quirk with reading from named pipes that's worked around inrunProxy(it falls back tocat <pipe> | inner).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.