Skip to content

Instantly share code, notes, and snippets.

@stavarengo
Created June 2, 2026 22:07
Show Gist options
  • Select an option

  • Save stavarengo/23d4116fd6a5cc3dfe8d41c5933f7d8b to your computer and use it in GitHub Desktop.

Select an option

Save stavarengo/23d4116fd6a5cc3dfe8d41c5933f7d8b to your computer and use it in GitHub Desktop.

rgfzf monster wrapper

Install:

mkdir -p ~/.local/bin
cp rgfzf.sh ~/.local/bin/rgfzf
chmod +x ~/.local/bin/rgfzf

Recommended dependencies:

# macOS
brew install fzf ripgrep bat

# Ubuntu/Debian
sudo apt install fzf ripgrep bat

Examples:

rgfzf customer
rgfzf -t php -g '!vendor/**' Onboarding
rgfzf -F 'literal [text]'
rgfzf -H -P '(?<=foo)bar'
rgfzf --print 'TODO|FIXME'

Key idea: fzf is just the interactive UI; ripgrep owns matching through --disabled + change:reload. bat owns preview. The script owns editor/copy/platform behavior.

#!/usr/bin/env bash
# rgfzf.sh - an aggressively useful ripgrep + fzf + bat UX wrapper.
# Portable target: Linux + macOS, Bash 3.2+ compatible enough for macOS system Bash.
set -u
SELF=${0}
usage() {
cat <<'USAGE'
rgfzf - interactive ripgrep/fzf code search
Usage:
rgfzf [options] [initial query] [-- paths...]
Options:
-h, --help Show help
-H, --hidden Include hidden files/directories
-u, --unrestricted Search hidden + ignored files (-uu)
-F, --fixed Treat query as a literal string
-w, --word Match whole words only
-P, --pcre2 Use PCRE2 regex engine
-L, --follow Follow symbolic links
-t, --type TYPE Include rg file type, repeatable (ex: -t php -t java)
-T, --type-not TYPE Exclude rg file type, repeatable
-g, --glob GLOB Include/exclude glob, repeatable (ex: -g '!vendor/**')
--editor CMD Editor command. Defaults to $RGFZF_EDITOR, $VISUAL, $EDITOR, then auto-detect.
--no-bat Use sed/awk preview instead of bat
--print Print selected file:line:column instead of opening
Environment:
RGFZF_EDITOR Preferred editor command
RGFZF_HEIGHT fzf height, default 95%
RGFZF_LAYOUT fzf layout, default reverse
RGFZF_BAT_STYLE bat style, default full
RGFZF_BAT_THEME optional bat theme
RGFZF_PREVIEW_CONTEXT preview context lines, default 180
Key bindings:
Enter open selection and exit
Ctrl-o open selection and stay in fzf
Ctrl-y copy file:line:column
Ctrl-p print file:line:column and exit
Ctrl-/ toggle preview
Ctrl-w toggle preview wrap
Ctrl-r reload current query
Alt-a select all matches
Alt-d deselect all
Alt-up/down scroll preview
Alt-left/right resize preview
Notes:
- Multi-select with Tab/Shift-Tab. Enter opens quickfix-style when supported.
- For live search, fzf filtering is disabled and rg owns the query.
- Search output is rg vimgrep-ish: file:line:column:text.
USAGE
}
have() { command -v "$1" >/dev/null 2>&1; }
bat_cmd() {
if [ "${RGFZF_NO_BAT:-0}" = "1" ]; then return 1; fi
if have bat; then printf '%s\n' bat; return 0; fi
if have batcat; then printf '%s\n' batcat; return 0; fi
return 1
}
editor_cmd() {
if [ -n "${RGFZF_EDITOR:-}" ]; then printf '%s\n' "$RGFZF_EDITOR"; return; fi
if [ -n "${VISUAL:-}" ]; then printf '%s\n' "$VISUAL"; return; fi
if [ -n "${EDITOR:-}" ]; then printf '%s\n' "$EDITOR"; return; fi
for e in nvim vim code cursor zed hx vi; do
if have "$e"; then printf '%s\n' "$e"; return; fi
done
printf '%s\n' vi
}
copy_cmd() {
if have pbcopy; then printf '%s\n' pbcopy; return 0; fi
if have wl-copy; then printf '%s\n' wl-copy; return 0; fi
if have xclip; then printf '%s\n' 'xclip -selection clipboard'; return 0; fi
if have xsel; then printf '%s\n' 'xsel --clipboard --input'; return 0; fi
return 1
}
# Internal search. Arguments after -- are rg args; query is in RGFZF_QUERY.
internal_search() {
query=${RGFZF_QUERY:-}
shift # --search
# Avoid rg showing usage/error noise for empty initial query. Empty regex means "all lines".
rg --vimgrep --column --line-number --no-heading --color=always --smart-case "$@" -- "$query" 2>/dev/null || true
}
internal_preview() {
# Receives one full selected line. Parse file:line:column:rest.
shift # --preview
item=${1:-}
file=${item%%:*}
rest=${item#*:}
line=${rest%%:*}
rest=${rest#*:}
col=${rest%%:*}
if [ -z "$file" ] || [ ! -f "$file" ]; then
printf 'No file selected yet.\n'
return 0
fi
case "$line" in ''|*[!0-9]*) line=1 ;; esac
context=${RGFZF_PREVIEW_CONTEXT:-180}
start=$((line - context / 2))
[ "$start" -lt 1 ] && start=1
end=$((line + context / 2))
if b=$(bat_cmd); then
style=${RGFZF_BAT_STYLE:-full}
theme_arg=
if [ -n "${RGFZF_BAT_THEME:-}" ]; then theme_arg="--theme=$RGFZF_BAT_THEME"; fi
# shellcheck disable=SC2086
"$b" --color=always --paging=never --style="$style" $theme_arg \
--highlight-line "$line" --line-range "${start}:${end}" -- "$file" 2>/dev/null || true
else
awk -v s="$start" -v e="$end" -v h="$line" '
NR>=s && NR<=e {
prefix = (NR == h ? ">" : " ");
printf "%s %6d │ %s\n", prefix, NR, $0
}
' "$file" 2>/dev/null || true
fi
}
quote_for_sh() {
# POSIX-ish single quote escaping.
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\''/g")"
}
open_one() {
file=$1 line=${2:-1} col=${3:-1}
ed=$(editor_cmd)
base=${ed%% *}
case "$base" in
code|cursor)
$ed --goto "$file:$line:$col" >/dev/null 2>&1 &
;;
zed)
$ed "$file:$line:$col" >/dev/null 2>&1 &
;;
hx|helix)
$ed "$file:$line:$col"
;;
vim|nvim|vi|mvim)
$ed "+call cursor($line,$col)" -- "$file"
;;
*)
$ed "$file"
;;
esac
}
internal_open() {
mode=${1:-}
shift || true
tmp=${1:-}
if [ -n "$tmp" ] && [ -f "$tmp" ]; then
count=$(wc -l < "$tmp" | tr -d ' ')
ed=$(editor_cmd)
base=${ed%% *}
if [ "$count" -gt 1 ]; then
case "$base" in
vim|nvim|vi|mvim)
$ed +cw -q "$tmp"
return $?
;;
esac
fi
first=$(sed -n '1p' "$tmp")
else
first=${tmp:-}
fi
file=${first%%:*}
rest=${first#*:}
line=${rest%%:*}
rest=${rest#*:}
col=${rest%%:*}
[ -n "$file" ] && open_one "$file" "$line" "$col"
}
internal_copy() {
shift # --copy
item=${1:-}
target=$(printf '%s' "$item" | awk -F: '{print $1":"$2":"$3}')
if c=$(copy_cmd); then
# shellcheck disable=SC2086
printf '%s' "$target" | $c
else
printf '%s\n' "$target"
fi
}
internal_print() {
shift # --print-item
item=${1:-}
printf '%s\n' "$item" | awk -F: '{print $1":"$2":"$3}'
}
main() {
if [ "${1:-}" = "--search" ]; then internal_search "$@"; exit 0; fi
if [ "${1:-}" = "--preview" ]; then internal_preview "$@"; exit 0; fi
if [ "${1:-}" = "--open" ]; then internal_open "$@"; exit 0; fi
if [ "${1:-}" = "--copy" ]; then internal_copy "$@"; exit 0; fi
if [ "${1:-}" = "--print-item" ]; then internal_print "$@"; exit 0; fi
have rg || { printf 'rgfzf: missing dependency: rg\n' >&2; exit 127; }
have fzf || { printf 'rgfzf: missing dependency: fzf\n' >&2; exit 127; }
rg_args=''
paths=''
query_parts=''
print_only=0
while [ "$#" -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-H|--hidden) rg_args="$rg_args --hidden"; shift ;;
-u|--unrestricted) rg_args="$rg_args -uu"; shift ;;
-F|--fixed) rg_args="$rg_args --fixed-strings"; shift ;;
-w|--word) rg_args="$rg_args --word-regexp"; shift ;;
-P|--pcre2) rg_args="$rg_args --pcre2"; shift ;;
-L|--follow) rg_args="$rg_args --follow"; shift ;;
-t|--type) shift; [ "$#" -gt 0 ] || { printf 'rgfzf: --type needs value\n' >&2; exit 2; }; rg_args="$rg_args --type $(quote_for_sh "$1")"; shift ;;
-T|--type-not) shift; [ "$#" -gt 0 ] || { printf 'rgfzf: --type-not needs value\n' >&2; exit 2; }; rg_args="$rg_args --type-not $(quote_for_sh "$1")"; shift ;;
-g|--glob) shift; [ "$#" -gt 0 ] || { printf 'rgfzf: --glob needs value\n' >&2; exit 2; }; rg_args="$rg_args --glob $(quote_for_sh "$1")"; shift ;;
--editor) shift; [ "$#" -gt 0 ] || { printf 'rgfzf: --editor needs value\n' >&2; exit 2; }; export RGFZF_EDITOR=$1; shift ;;
--no-bat) export RGFZF_NO_BAT=1; shift ;;
--print) print_only=1; shift ;;
--) shift; while [ "$#" -gt 0 ]; do paths="$paths $(quote_for_sh "$1")"; shift; done ;;
*) query_parts="${query_parts}${query_parts:+ }$1"; shift ;;
esac
done
# Search command executed by fzf. We intentionally go through this script so
# all quoting, flags, and future behavior stay centralized.
search_cmd="RGFZF_QUERY={q} $(quote_for_sh "$SELF") --search $rg_args $paths"
preview_cmd="$(quote_for_sh "$SELF") --preview {}"
copy_cmd_bind="$(quote_for_sh "$SELF") --copy {}"
print_cmd_bind="$(quote_for_sh "$SELF") --print-item {}"
# fzf {+f} writes selected lines to a temp file; ideal for Vim quickfix.
opener="$(quote_for_sh "$SELF") --open enter {+f}"
executor="$(quote_for_sh "$SELF") --open execute {+f}"
common_binds="start:reload:$search_cmd || true,change:reload:sleep 0.08; $search_cmd || true,ctrl-r:reload:$search_cmd || true,ctrl-o:execute:$executor,ctrl-y:execute-silent:$copy_cmd_bind,ctrl-p:become:$print_cmd_bind,ctrl-/:toggle-preview,ctrl-w:toggle-preview-wrap,alt-a:select-all,alt-d:deselect-all,alt-up:preview-up,alt-down:preview-down,alt-pgup:preview-page-up,alt-pgdn:preview-page-down,alt-left:change-preview-window(down,70%,border-top|right,60%,border-left|hidden),alt-right:change-preview-window(right,70%,border-left|down,70%,border-top|hidden)"
if [ "$print_only" = "1" ]; then
enter_bind="enter:become:$print_cmd_bind"
else
enter_bind="enter:become:$opener"
fi
height=${RGFZF_HEIGHT:-95%}
layout=${RGFZF_LAYOUT:-reverse}
fzf --ansi --disabled --multi --cycle --layout="$layout" --height="$height" \
--border --padding=1 \
--prompt='rg › ' \
--pointer='▶' --marker='✓' \
--header='type to rg live • Enter open • Ctrl-o open/stay • Ctrl-y copy loc • Ctrl-p print • Ctrl-/ preview • Alt-a/d all/none' \
--delimiter=':' \
--with-nth='1,2,3,4..' \
--preview="$preview_cmd" \
--preview-label=' preview ' \
--preview-window='right,60%,border-left,~4,+{2}+4/3,<100(up,60%,border-bottom)' \
--bind="$common_binds" \
--bind="$enter_bind" \
--query="$query_parts"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment