fzf is one of those tools I’ve installed six times over the years and never internalized. The problem wasn’t fzf. The problem was my shell integration — out of the box, I’d get Ctrl-R for history search and Ctrl-T for file completion, and I’d use them for a week, and then forget. The defaults aren’t quite what I want, and “quite” compounds into “never.”

This year I sat down and configured it properly. Two weeks later I can’t live without it. Sharing the config in case it’s useful.

The baseline install

# mac
brew install fzf
$(brew --prefix)/opt/fzf/install

# or just the binary
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install

The install script sets up shell integration. The bit I cared about: enable keybindings, skip everything else.

The actual config

In my .zshrc (same works in bash with minor changes):

export FZF_DEFAULT_COMMAND='rg --files --hidden --follow --glob "!.git/*"'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_DEFAULT_OPTS="
  --height=60%
  --layout=reverse
  --border
  --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)'
  --bind 'ctrl-/:toggle-preview'
"
export FZF_CTRL_T_OPTS="
  --preview 'bat --color=always --style=plain --line-range :300 {}'
  --preview-window 'right:60%'
"
export FZF_CTRL_R_OPTS="
  --preview 'echo {}'
  --preview-window 'down:3:hidden:wrap'
  --bind '?:toggle-preview'
"
export FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'
export FZF_ALT_C_OPTS="
  --preview 'tree -C {} | head -200'
"

Translation, in order of importance to me:

  • rg --files instead of find. Faster, respects gitignore, sensible defaults. Huge difference on large repos.
  • bat for file preview. Syntax highlighted. --line-range :300 stops huge files from eating performance.
  • 60% height, reverse layout. Takes less of my screen, prompt at the top where my eyes go.
  • ctrl-y to copy selection. Often I just want the filename in the clipboard. pbcopy is Mac-specific; on Linux use xclip or wl-copy.
  • ctrl-/ to toggle preview. Sometimes the preview is noise; I want it gone.

The git integration that changed everything

Pure fzf is useful. fzf + git is transformative. A few functions I use constantly:

# interactive git branch switcher
fbr() {
    local branches branch
    branches=$(git for-each-ref --count=30 --sort=-committerdate refs/heads --format="%(refname:short)") &&
    branch=$(echo "$branches" | fzf --preview 'git log --oneline --graph --color=always {} | head -50') &&
    git switch "$branch"
}

# interactive git log with diff preview
fgl() {
    git log --graph --color=always --format="%C(auto)%h %s %C(blue)%an %C(cyan)%ar" "$@" |
    fzf --ansi --no-sort --tiebreak=index --preview \
        'f() { set -- $(grep -o "[a-f0-9]\{7,\}" <<< "$1" | head -1); [ "$1" ] && git show --color=always "$1"; }; f {}' \
        --bind 'enter:execute:(grep -o "[a-f0-9]\{7,\}" <<< {} | head -1 | xargs -I{} git show --color=always {}) | less -R'
}

# interactive stash viewer
fstash() {
    local out sha
    out=$(git stash list --pretty=format:'%gd: %gs' | fzf --preview 'git stash show -p $(echo {} | cut -d: -f1) --color=always') || return
    sha=$(echo "$out" | cut -d: -f1)
    git stash pop "$sha"
}

fbr for “switch to the branch I was just on,” fgl for “browse git log and show the diff of whatever I pick,” fstash for “bring back that stash I forgot about.”

The thing that made it stick

The thing I did that I hadn’t done before: I deleted my old aliases. I used to have gco for git checkout, gb for git branch, etc. Convenient — too convenient. Every time I reached for them I was skipping fzf. I replaced them with the fzf functions. Now gco is fbr, gb is fbr. The old way isn’t an option. Two weeks later I couldn’t imagine using git branch to look at my branches.

This pattern generalizes: if you want to build muscle memory for a new tool, remove the alternative. Don’t just add the new option.

What I still don’t use fzf for

  • File opening in my editor. nvim’s telescope fills that slot and integrates better with the editor.
  • Process picker. I tried, didn’t stick. ps aux | grep works fine.
  • ssh hostname picker. Always liked tab completion for this. Your mileage may vary.

Reflection

I’m a big believer in keyboard tools but I’m also lazy. A tool has to pay its cost in learning with big usability gains or I’ll drift back to whatever I was doing. fzf eventually paid off because I gave myself no alternative. And because I tuned the preview/layout to my aesthetic. The out-of-the-box experience is too plain to excite me; the tuned version feels like a spaceship.

If fzf hasn’t stuck for you, try the remove-the-alternative trick. Delete your gco alias. See what happens.

Related: git worktree as a daily driver.