Dissecting fzf's basic model from stdin to filter to stdout, how subsequence-based fuzzy matching and relevance scores work, the exit code language, and the anatomy of its main interface components.

After episode 1 gave us fzf's history — from the 2013 Vim plugin by Junegunn Choi to the de-facto standard in Go — now it is time to dissect how fzf works from the inside. This episode is the bridge between "why fzf exists" and "how to use it properly", so follow the flow closely.
Many fzf users treat it like a magic box: give it a list, get a choice. Yet behind that lies a simple, predictable working model. Understanding this model — like opening the hood before driving — will make you far more confident when results don't match your expectations, and better prepared to make use of the options we cover in the following episodes.
If we had to reduce fzf to one sentence: fzf reads items from stdin, displays them interactively, then prints the selected item to stdout.
stdin -> fzf (interactive UI) -> stdoutThere are three stages in this model, and each has a clear role:
ls, find, rg, git branch, or even cat.Enter, fzf prints the selected item to standard output, ready to be passed to the next command in the pipeline.This pattern is what makes fzf so flexible: because it does not care where the list comes from, it can be used to pick files, processes, branches, or history in exactly the same way.
printf "apple\nbanana\ncherry\n" | fzfRun the command above, type ba, press Enter — fzf will print banana to the terminal. Now add a follow-up command to prove that the output really keeps flowing into the pipeline:
printf "apple\nbanana\ncherry\n" | fzf | wc -lIf you select banana, the command above prints 1 — fzf produces one line, and wc -l counts it. This is the essence of fzf's architecture: it is a filter, not an end goal. Its value is felt most when its output is consumed by another command.
The fuzzy matching behind fzf rests on a single concept: subsequence. A set of characters is said to match an item if those characters appear in the item in the same order — even if they are not adjacent.
Take the query fzf and the item learn-fzf. All the letters f, z, f appear in order inside the item, so it matches. Now the query lrf against the same item: l matches, r matches, f matches — still in order, still a match, just with gaps in between. That is the power of fuzzy matching: you don't need to type the item in full.
printf "learn-fzf\nfzf\ntmux\nzellij\n" | fzfTry typing lz in the example above — you will see an interesting result: learn-fzf can match (the letters l and z are present and in order), and so can zellij. This is where scoring comes in.
fzf does not just answer "match or not" — it computes a relevance score for each item, then sorts by score. The better the quality of the match — for example adjacent characters, characters at the start of a word, or characters forming a word prefix — the higher the score. Items with the highest score take the top positions, so the most relevant results are always easy to reach. This is what sets fzf apart from simple naive matching.
Every time fzf finishes, it "speaks" through its exit code — the number returned to the shell. This is fzf's primary signaling system for scripting, and the rules are simple:
| Exit Code | Meaning |
|---|---|
0 | An item was selected and printed to stdout |
1 | No item matched, or canceled without a selection |
130 | Interrupted with Ctrl+C (SIGINT) |
printf "apple\nbanana\n" | fzf
echo $?Press Esc to cancel, then look at $? — its value is 1. This is not an error in a bad sense; it is information: nothing was selected, continue with your logic. In the following episodes, the fzf || exit 1 pattern will be used often inside scripts — the exit code language is the key to using fzf as a reliable component.
When fzf runs, your screen is divided into several parts, each with its own role:
belajar <- prompt: the area where you type your query
learn-fzf.md <- list + pointer on the active item
2/10 <- info: pointer position and total items> symbol.Each of these components is controlled by a different option: --prompt sets the prompt text, --pointer sets the pointer symbol, --info sets the info display, and --preview sets the preview window. You will touch all of them starting in episode 4.
Beyond the interface, fzf also has controlling options — options that control behavior, not appearance. The most useful examples: --exit-0, which makes fzf immediately print the only item without waiting for interaction, and --filter, which runs fzf without any interface at all — perfect for non-interactive scripting. Both will become faithful companions in the advanced episodes.
Before closing this episode, get to know the five search modes you will use every day:
| Mode | Example | Meaning |
|---|---|---|
| Fuzzy (default) | fzf | Characters in order, gaps allowed |
| Exact | 'fzf | Characters exactly in order, no gaps |
| Prefix | ^learn | Item must start with learn |
| Suffix | .md$ | Item must end with .md |
| Extended | combination of the above | All patterns above active at once |
In modern fzf, extended search is on by default — which is why patterns like ^learn or 'build work right away without any configuration. The details of each mode, including how to combine them and control case sensitivity, will be covered thoroughly in episode 3.
In this episode 2, you dissected fzf's main architecture: the stdin -> filter -> stdout model that is its soul, how subsequence-based fuzzy matching and relevance scores work, the exit code language (0, 1, 130), and the anatomy of the interface components and controlling options.
The key takeaways:
0 selected, 1 nothing selected, 130 interrupted.--exit-0 and --filter.In the next episode, episode 3, we will master fuzzy matching and search modes in depth — search patterns, case control, and the v1 and v2 scoring algorithms that determine result order. See you in episode 3!