Jun 13,
2021

Making Fzf Stop Eating My Commands

Howdie folks!

Fzf or fuzzy finder is a neat command line utility that choreographically helps select an element from a list.

I use it to search command history via Ctrl-R (requires fzf shell integration for Zsh). The problem: if you type a query that doesn't match any history entry and exit fzf (with ESC), your precious typed text vanishes. Gone. You have to retype it manually in the shell. Annoying when you realize what you want isn't in history after you've already typed it.

I was hit by this multiple times. Searched the Interweb, found others with same problem, but no ready-made solutions. So I decided to fix it myself.

Maximum Effort

Programming Zsh requires a lot of patience on my part - too much cruft accumulated over the years, too many ways to do something mixed with arcane syntax.

Anyway, meet fc the builtin fix command. It's the shell's history interface - fc -l lists history, fc -e edits it. The history command you use daily? Just an alias to fc.

In this case, fc -rl 1 does:

  • -r: reverse order (newest first)
  • -l: list format with line numbers
  • 1: starting from entry 1 (entire history)

Output format: linenum command which is perfect for piping to fzf. The line number becomes the handle for vi-fetch-history to retrieve the actual command later.

The Solution

Here's the complete widget implementation:

# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
  local selected num ret
  setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
  selected=( $(fc -rl 1 |
                   FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS \
                   -n2..,.. --tiebreak=index --bind=ctrl-s:toggle-sort,ctrl-k:kill-line \
                   --expect=ctrl-e $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} \
                   +m --print-query" $(__fzfcmd)) )
  ret=$?

  if (( $selected[(I)ctrl-e] )); then
      if [[ $ret = 0 ]]; then
          [[ $selected[3] =~ ^[0-9]+$ ]] && shift selected
          zle vi-fetch-history -n $selected[2]
      elif [[ $ret = 1 ]]; then
          shift -p selected
          LBUFFER="${LBUFFER}${selected}"
      fi
  else
      if [[ $ret = 0 ]]; then
          zle vi-fetch-history -n $selected[2]
      elif [[ $ret = 1 ]]; then
          LBUFFER="${LBUFFER}${selected}"
      fi
      zle accept-line
  fi
  zle reset-prompt
  return $ret
}
zle     -N   fzf-history-widget
bindkey '^R' fzf-history-widget

How It Works

The magic happens in the fzf invocation. Key options:

  • --print-query: outputs the user's typed query as first line, regardless of match
  • --expect=ctrl-e: outputs "ctrl-e" if pressed, allowing edit-vs-execute choice
  • --query=${(qqq)LBUFFER}: pre-fills fzf with current command line buffer (LBUFFER is Zsh's line editor buffer left of cursor, triple-q escapes special chars)
  • +m: single selection mode

The selected array captures fzf's multiline output (Zsh arrays are 1-indexed):

  • selected[1]: User's query string (from --print-query)
  • selected[2]: Pressed key if --expect matched (e.g., "ctrl-e"), OR history line number if no key pressed
  • selected[3]: Selected history entry (if any)

Return codes: 0 = match selected, 1 = no match (ESC/empty selection), 130 = interrupt.

The Control Flow

The nested conditionals handle four scenarios. First check: $selected[(I)ctrl-e] uses Zsh subscript flag (I) to search the array for "ctrl-e", returning its index (0 if not found).

Ctrl-e pressed (edit mode): - Match found (ret=0): fetch history via vi-fetch-history -n (the -n flag uses the line number), don't execute - No match (ret=1): shift -p removes "ctrl-e" from array, leaving query to append to buffer, don't execute

Enter pressed (execute mode): - Match found (ret=0): fetch history and execute via accept-line - No match (ret=1): append user's query to LBUFFER and execute

The quirk: when scrolling without typing, array positions shift - selected[3] gets the history number instead of selected[2]. The regex check ^[0-9]+$ detects this and normalizes via shift.

Finally, zle reset-prompt refreshes the command line with the updated LBUFFER content.

Interrupt (ret=130): code doesn't explicitly handle it, just returns the code and lets the shell deal with it.

Putting It Together

With --print-query, user input survives in selected[1] even when ret=1, so no more lost commands when history doesn't match.

Add the function to your .zshrc after sourcing fzf shell integration. This replaces the default fzf-history-widget with the enhanced version. The bindkey '^R' line maps it to Ctrl-R.

Usage:

  • Type query, no match? Hit ESC → query appears at prompt
  • Want to edit before executing? Hit Ctrl-e instead of Enter

Update (January 2024)

Fzf version 0.45.0 added built-in support via accept-or-print-query:

export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query'

Single line versus 30+ lines of Zsh. Progress.

The custom widget above remains useful if you need the Ctrl-e edit mode or are stuck on older fzf versions.