junegunn/fzf
Listen mode
--listen [ADDR] starts an HTTP server inside fzf so external programs can drive its UI. This is the foundation for interactive ripgrep filters, kubectl browsers, and any tool that wants fzf's UI but its own data flow.
Quick start
# Terminal A: start fzf with a listen server
seq 100 | fzf --listen 8123
# Terminal B: change the prompt and the query
curl -X POST -d 'change-prompt(>> )+change-query(42)' localhost:8123
# Terminal B: get the current first 5 visible items
curl 'localhost:8123/?limit=5'Address format
parseListenAddress in src/server.go accepts:
<port>(e.g.8123) — bind onlocalhost.<host>:<port>— explicit host. Onlylocalhost/127.0.0.1is allowed unlessFZF_API_KEYis set.<path>.sock— Unix domain socket. The file is created with mode0600.- Empty (
--listen) — bind onlocalhostand a random port. The chosen port is printed when the server starts.
Authentication
For non-localhost TCP binds, FZF_API_KEY is required. Every request must include X-API-Key: <value>. The comparison uses subtle.ConstantTimeCompare. For Unix sockets and localhost, the API key is optional.
Verbs
POST / — execute actions
The body is a single fzf action list, using the same syntax as --bind arguments:
change-prompt(picked> )+reload(ls)+firstThe body is parsed by the same parseSingleActionList machinery as --bind, so anything you can do with a key binding you can do over HTTP.
A Content-Length header is required. Bodies above 1 MiB are rejected with 400 Bad Request. Successful posts return 200 OK with no body.
GET / — fetch state
GET /?limit=100&offset=0Returns the current visible match list as JSON (the count, the query, and the items, including any --with-nth transformations). Both limit (default 100) and offset (default 0) are accepted.
If the terminal cannot service the GET within 2 seconds, fzf returns 503 Service Unavailable — meaning the action queue is full or the renderer is paused.
Status codes
| Code | Reason |
|---|---|
200 OK |
Success. JSON body for GETs. |
400 Bad Request |
Bad method, malformed action, missing/invalid Content-Length. |
401 Unauthorized |
Missing or wrong API key. |
503 Service Unavailable |
Action queue full or GET handler timed out. |
Why no net/http?
The comment at the top of src/server.go records the binary-size impact:
* No --listen: 2.8MB
* --listen with net/http: 5.7MB
* --listen w/o net/http: 3.3MBHand-rolling the HTTP parser saves ~2.4 MB compared to the stdlib. The parser handles only what fzf needs: GET / POST methods, Content-Length, X-API-Key, and a JSON or plain-text body.
Programmatic clients
- The README documents a few examples in the "Listen mode" section.
- The companion
junegunn/fzf.vimplugin uses--listenfor some live-filter pickers. - Any HTTP client works. There's no streaming/long-poll protocol; clients poll
GET /if they need to track changes.
Where it lives
| Concern | File |
|---|---|
| Server | src/server.go |
| Address parsing | src/server.go (parseListenAddress) |
| Action parsing | src/options.go (parseSingleActionList) |
| Action injection into Terminal | src/terminal.go (actionChannel) |
| GET handler | src/terminal.go (getHandler) |
| API key env var | FZF_API_KEY |
| Tests | test/test_server.rb |
See systems/http-server for a deeper walkthrough.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.