Combining fzf with fd for fast file search that respects .gitignore, and with ripgrep for content search, producing a precise, instant file and text search workflow.

Episode 10's preview window shows file contents; episode 11 makes sure the files shown deserve to be shown. We'll combine fzf with two tools born from the same ecosystem: fd (file search) and ripgrep (text search). Starting with a line that will become a habit: fd --type f.
Why two more tools? Because fzf is only a filter — it does its best with the input it's given, but the quality of the input determines the result. Built-in find is slow, noisy (it includes node_modules, .git), and doesn't understand .gitignore. fd and rg solve all three problems at once, and both write "fzf-friendly" output.
These three tools are not competitors, but a production chain with different jobs:
| Tool | Job | Analogy |
|---|---|---|
fd | Finds files by name | The receptionist who knows where things are stored |
rg | Finds lines by content | The researcher who looks up words in documents |
fzf | Presents results interactively | The cashier who hands you the choices |
Each tool does one thing extremely well, and their combination is faster and more precise than a sloppy find | grep | fzf.
.gitignorefd is a Rust-written alternative to find: its results respect .gitignore by default and it's fast because it runs in parallel. This makes it the ideal data source for FZF_DEFAULT_COMMAND (episode 8):
export FZF_DEFAULT_COMMAND="fd --type f"Now the fzf you run without a pipe, CTRL-T, and file fuzzy completion — all use a clean file list from fd: no node_modules, no .git, no build folders, unless told otherwise. This is a change you notice immediately in large repositories.
| Option | Function |
|---|---|
--type f / -t f | Files only |
--type d / -t d | Directories only |
--hidden / -H | Also include hidden files (dotfiles) |
--exclude / -E | Exclude specific patterns |
--no-ignore | Ignore .gitignore rules |
--extension / -e | Filter by extension |
A typical combination for finding all files including dotfiles while still respecting gitignore:
fd --type f --hidden --exclude node_modulesNote
The key difference from find: fd respects .gitignore by default, while find never does. For file lists that are familiar in a git repository, fd's default is almost always what you want. Use --no-ignore only when you deliberately want to see what git "throws away".
When the file list comes from fd, episode 10's preview window becomes a perfect companion: a clean list from fd, contents from bat. Store both in FZF_DEFAULT_OPTS so they stay consistent across every invocation:
export FZF_DEFAULT_COMMAND="fd --type f"
export FZF_DEFAULT_OPTS="--preview='bat --color=always --style=numbers --line-range=:200 {}'"Now pressing CTRL-T shows a clean file list, and every highlighted file immediately shows its contents. This is already a workflow worthy of daily use.
rg (ripgrep) is the modern grep: fast, respects .gitignore, and supports regex. It plays two roles in an fzf workflow — as a finder (producing the list of files containing a pattern) and as a previewer (showing the matching lines).
To find which files contain a certain word, use rg -l (files-with-matches):
rg -l 'TODOFIX' | fzfThe output of rg -l is a list of paths — the exact format fzf consumes. Each highlighted file can then be peeked at with a preview:
rg -l 'TODOFIX' | fzf --preview 'bat --color=always {}'rg is also perfect inside --preview — using the {q} placeholder from episode 10 for a content search that follows the query:
fzf --preview 'rg --color=always -C 3 {q} {}' --ansiAs you type, rg searches for {q} inside the highlighted file and shows 3 lines of context above/below each match. This creates a "search-in-search" preview: the list is files, the preview is the matching lines.
Tip
To keep the preview from draining performance on giant files, combine it with --line-range in bat or use --max-count 5 in rg so only the first few matches are processed.
All the pieces are ready — now let's assemble one complete setup. One key line: FZF_DEFAULT_OPTS can hold a --bind for reload, so the content search is done by rg (not fzf's fuzzy matching) whenever the query changes.
# ---------- Data source: fd ----------
export FZF_DEFAULT_COMMAND="fd --type f --hidden --exclude node_modules"
# ---------- Global options + bat preview ----------
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --border \
--preview='bat --color=always --style=numbers --line-range=:200 {}' \
--bind='ctrl-r:reload(rg -l {q} || true)'"
# ---------- Per keybinding options ----------
export FZF_CTRL_T_OPTS="--preview='bat --color=always {}'"
export FZF_CTRL_R_OPTS="--scheme=history --sort"
# ---------- Load shell integration ----------
eval "$(fzf --bash)"Let's dissect the new --bind part: ctrl-r:reload(rg -l {q} || true) instructs fzf — when CTRL-R is pressed inside fzf — to reload the item list using rg -l {q}. In other words: type a query, press CTRL-R, and the list turns into files whose contents match the query; || true prevents an error when there are no matches.
Important
This is the core of the integration: fd for guessing file names, rg for guessing contents, bat for displaying, and fzf as the controller. None is most important — each covers another's weaknesses. If the query is empty, rg -l '' will return all files; the || true above keeps the reload safe in that case.
Episode 11 united fzf with its search ecosystem: fd as a .gitignore-respecting file source (with --hidden/--exclude), rg as a content finder and previewer, and bat as the renderer — assembled in FZF_DEFAULT_COMMAND and FZF_DEFAULT_OPTS plus a reload bind.
The key takeaways:
FZF_DEFAULT_COMMAND="fd --type f" gives fzf a clean, fast file list.rg -l 'pattern' | fzf finds files by content.rg {q} {} preview and the reload(rg -l {q}) bind create an interactive content search.In episode 12, we will take fzf to a different layer: integration with tmux & zellij — running fzf inside a terminal multiplexer session, floating panes, and cross-pane keybindings. See you in episode 12!