fzf Plus rg

As stand-alone tools fzf and rg are very useful. And there is an example in the fzf docs showing how to toggle between fzf and rg, with the initial mode being rg. It works by feeding files (or standard input) to rg, and then making the list available to fzf for fuzzy match. When you switch back to rg you rerun it with the original arguments.

I needed a variation on this theme that operates under different assumptions. First, it will start with fzf, because it’s in a pipeline and fuzzy match works well for the initial selection. Second, I want to successively refine the search, even going back and forth between the two modes.

Here’s a slightly simplified version of what I came up with.

fzf_key=ctrl-f
rg_key=ctrl-r
delimiter=:

# #_# review [fzf-args]
#_#   Search in inputs with fzf and rg
#_#
do_review() {
  rg_prompt="rg> "
  fzf_prompt="fzf> "
  rg_prefix="rg --no-heading --smart-case --with-filename "
  rg_fzf_dir="$(mktemp -d "${XDG_RUNTIME_DIR:-/tmp}/rg-fzf-XXXXXX")"
  rg_fzf_fzf="${rg_fzf_dir}/fzf"
  rg_fzf_rg="${rg_fzf_dir}/rg"
  rg_fzf_list="${rg_fzf_dir}/list"
  touch "${rg_fzf_fzf}" "${rg_fzf_rg}" "${rg_fzf_list}"
  fzf --multi \
    --bind "start:unbind(change,${fzf_key})+transform-prompt(echo \${FZF_PROMPT} '${fzf_prompt}')" \
    --bind "change:reload:sleep 0.1; ${rg_prefix} {q} \$(cat ${rg_fzf_list}) || true" \
    --bind "${fzf_key}:unbind(change,${fzf_key})+enable-search+rebind(${rg_key})+transform-query(echo {q} > ${rg_fzf_rg}; cat ${rg_fzf_fzf})" \
    --bind "${fzf_key}:+transform-prompt(echo \${FZF_PROMPT% *> } '${fzf_prompt}')" \
    --bind "${rg_key}:select-all+disable-search+execute(printf '%s\n' {+} | cut -d${delimiter} -f1 | sort -u > ${rg_fzf_list})+reload(${rg_prefix} {q} \$(cat ${rg_fzf_list}) || true)" \
    --bind "${rg_key}:+unbind(${rg_key})+rebind(change,${fzf_key})+transform-query(echo {q} > ${rg_fzf_fzf}; cat ${rg_fzf_rg})" \
    --bind "${rg_key}:+transform-prompt(echo \${FZF_PROMPT% *> } '${rg_prompt}')" \
    --delimiter="${delimiter}" \
    "$@"
  rm "${rg_fzf_fzf}" "${rg_fzf_rg}" "${rg_fzf_list}" && rmdir "${rg_fzf_dir}"
}

do_review "$@"

This assumes each record begins with a file or directory path. This may be followed by a delimiter and more text. The entire record is available for fuzzy match. We preserve the queries in use for each mode as in the original example, but also preserve the full list of files, so that we can feed them back to rg. The successive refinement is not exact. With the given definition of rg_prefix a match in any file expands to cover the entire file again after switching back to rg. This is not too difficult to work with. And it’s not too difficult to capture the initial inputs and reinitialize the selection list on demand.

I’ve already gotten a lot of mileage out of this function!

One weakness of this code lies in the handling of the list. In order to capture the list when mode switching, a select-all is required. This means that the code will not work without --multi. There is no way to save the current setting, set --multi, capture, and revert the setting. A better solution would be to have a placeholder for the full list. I propose {:} as the full-list analogue to the selected-line placeholder {+}.