feat: New style option to fzf-tab group headers: active-group-style (#553)

* Feat: support different styling for the group labels

* Fix: improved portability of code to macOS

* Refactor: use robust template file for ftb_preview_init

* refactor: retrieve active-group-style as array, simplify normalization

Keeps the type prefix consistent with the project history. It covers both changes: switching from -s to -a (which also changes the user-facing syntax to space-separated, consistent with all other multi-value zstyles), and replacing the 60-line anonymous function with a clean four-step variable breakdown.

---

```zsh
test_normalize() {
  local -a active_group_style=("$@")
  local -a _active_style_lower=(${(L)active_group_style})
  local -a _active_style_valid=(${(M)_active_style_lower:#(bold|underline|none)})
  local -a _active_style_sorted=(${(ou)_active_style_valid})
  local normalized=${${(j:,:)_active_style_sorted}:-none}
  echo "input=(${(j: :)active_group_style})  →  \"$normalized\""
}

test_normalize bold
test_normalize underline
test_normalize bold underline
test_normalize underline bold       # order should not matter
test_normalize BOLD UNDERLINE       # uppercase
test_normalize bold bold            # duplicates
test_normalize none
test_normalize italic               # unknown → none fallback
test_normalize bold italic          # mixed valid+invalid
test_normalize                      # empty → none fallback
```

input=(bold)  →  "bold"
input=(underline)  →  "underline"
input=(bold underline)  →  "bold,underline"
input=(underline bold)  →  "bold,underline"
input=(BOLD UNDERLINE)  →  "bold,underline"
input=(bold bold)  →  "bold"
input=(none)  →  "none"
input=(italic)  →  "none"
input=(bold italic)  →  "bold"
input=()  →  "none"

* fix: avoid code duplication in `initial_command` array and `reload_command` with 0 appended as the offset

Note: (z) splits the string using shell word-splitting rules, turning the string back into an array of tokens, then 0 is appended as the offset for the initial render.

Co-authored-by: Aloxaf <aloxafx@gmail.com>

* fix: restoring the (z) flag on $fzf_command. The original code used ${(z)fzf_command} which splits the command string into words. The PR changed it to just $fzf_command.

Co-authored-by: Aloxaf <aloxafx@gmail.com>

* refactor: changed `"${initial_command[@]}"` (POSIX-style expansion) to `$initial_command` (zsh-idiomatic expansion of an array)

Co-authored-by: Aloxaf <aloxafx@gmail.com>

* refactor: avoided anonymous function

Co-authored-by: Aloxaf <aloxafx@gmail.com>

* refactor: replaced echo for `print -r --` and removed trailing spaces

Co-authored-by: Aloxaf <aloxafx@gmail.com>

---------

Co-authored-by: Aloxaf <aloxafx@gmail.com>
This commit is contained in:
Andrea Alberti 2026-02-26 10:05:24 +01:00 committed by GitHub
parent 747c15de85
commit 0fbd5753f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 117 additions and 42 deletions

View file

@ -7,29 +7,24 @@ zmathfunc
local tmp_dir=${TMPPREFIX:-/tmp/zsh}-fzf-tab-$USER
[[ -d $tmp_dir ]] || command mkdir $tmp_dir
local ftb_preview_init="
zmodload zsh/mapfile
local -a _ftb_compcap=(\"\${(@f)mapfile[$tmp_dir/compcap.$$]}\")
local -a _ftb_groups=(\"\${(@f)mapfile[$tmp_dir/groups.$$]}\")
local bs=\$'\2'
# get description
export desc=\${\${\"\$(<{f})\"%\$'\0'*}#*\$'\0'}
# get ctxt for current completion
local -A ctxt=(\"\${(@0)\${_ftb_compcap[(r)\${(b)desc}\$bs*]#*\$bs}}\")
# get group
if (( \$+ctxt[group] )); then
export group=\$_ftb_groups[\$ctxt[group]]
fi
# get original word
export word=\${(Q)ctxt[word]}
# get real path if it is file
if (( \$+ctxt[realdir] )); then
export realpath=\${ctxt[realdir]}\$word
fi
$(typeset -p words)
"
local ftb_preview_init
ftb_preview_init=$(<"$FZF_TAB_HOME/lib/-ftb-preview.tpl")
# Inject resolved paths (these MUST refer to the correct pid used to create the files)
local compcap_file="$tmp_dir/compcap.$$"
local groups_file="$tmp_dir/groups.$$"
ftb_preview_init=${ftb_preview_init//__FTB_COMPCAP__/${(q)compcap_file}}
ftb_preview_init=${ftb_preview_init//__FTB_GROUPS__/${(q)groups_file}}
# Inject the dump of 'words'
ftb_preview_init=${ftb_preview_init//__FTB_WORDS_DUMP__/$(typeset -p words)}
# command substitution strips all trailing newlines from the output
ftb_preview_init+=$'\n'
local default_binds=tab:down,btab:up,change:top,ctrl-space:toggle,bspace:backward-delete-char/eof,ctrl-h:backward-delete-char/eof
local fzf_command fzf_flags fzf_preview debug_command tmp switch_group fzf_pad fzf_min_height binds
local fzf_command fzf_flags fzf_preview debug_command tmp switch_group active_group_style fzf_pad fzf_min_height binds
local ret=0
-ftb-zstyle -s fzf-command fzf_command || fzf_command=fzf
@ -41,12 +36,18 @@ local ret=0
-ftb-zstyle -s fzf-pad fzf_pad || fzf_pad=2
-ftb-zstyle -s fzf-min-height fzf_min_height || fzf_min_height=0
-ftb-zstyle -b use-fzf-default-opts use_fzf_default_opts || use_fzf_default_opts="no"
-ftb-zstyle -a active-group-style active_group_style || active_group_style=(bold)
-ftb-zstyle -a debug-command debug_command && {
${(eX)debug_command} $fzf_flags
return
}
local -a _active_style_lower=(${(L)active_group_style})
local -a _active_style_valid=(${(M)_active_style_lower:#(bold|underline|none)})
local -a _active_style_sorted=(${(ou)_active_style_valid})
local normalized_active_group_style=${${(j:,:)_active_style_sorted}:-none}
print -rl -- $_ftb_compcap > $tmp_dir/compcap.$$
print -rl -- $_ftb_groups > $tmp_dir/groups.$$
print -r -- ${ftb_preview_init/{f}/\$1} > $tmp_dir/ftb_preview_init.$$
@ -55,7 +56,7 @@ binds=${binds//{_FTB_INIT_}/. $tmp_dir/ftb_preview_init.$$ {f} $'\n'}
local -i header_lines=$#_ftb_headers
local -i lines=$(( $#_ftb_compcap + fzf_pad + header_lines ))
local reload_command="$commands[zsh] -f $FZF_TAB_HOME/lib/ftb-switch-group $$ $header_lines $tmp_dir"
local reload_command="$commands[zsh] -f $FZF_TAB_HOME/lib/ftb-switch-group $$ $header_lines $normalized_active_group_style $tmp_dir"
# detect if we will use tmux popup
local use_tmux_popup=0
@ -87,6 +88,8 @@ if [[ "$use_fzf_default_opts" == "yes" ]]; then
fzf_default_opts=$FZF_DEFAULT_OPTS
fi
local -a initial_command=(${(z)reload_command} 0)
FZF_DEFAULT_OPTS=$fzf_default_opts SHELL=$ZSH_NAME ${(z)fzf_command} \
--ansi \
--bind=$binds \
@ -102,8 +105,8 @@ FZF_DEFAULT_OPTS=$fzf_default_opts SHELL=$ZSH_NAME ${(z)fzf_command} \
--print-query \
--query=$_ftb_query \
--tiebreak=begin \
${fzf_preview:+--preview=${ftb_preview_init/{f}/'{f}'}$fzf_preview} \
$fzf_flags < $tmp_dir/completions.$$ || ret=$?
${fzf_preview:+--preview=${ftb_preview_init}$fzf_preview} \
$fzf_flags < <($initial_command) || ret=$?
if (( ! use_tmux_popup )); then
echoti civis >/dev/tty 2>/dev/null

21
lib/-ftb-preview.tpl Executable file
View file

@ -0,0 +1,21 @@
zmodload zsh/mapfile
local -a _ftb_compcap=("${(@f)mapfile[__FTB_COMPCAP__]}")
local -a _ftb_groups=("${(@f)mapfile[__FTB_GROUPS__]}")
local bs=$'\2'
# get description
export desc=${${"$(<'{f}')"%$'\0'*}#*$'\0'}
# get ctxt for current completion
local -A ctxt=("${(@0)${_ftb_compcap[(r)${(b)desc}$bs*]#*$bs}}")
# get group
if (( $+ctxt[group] )); then
export group=$_ftb_groups[$ctxt[group]]
fi
# get original word
export word=${(Q)ctxt[word]}
# get real path if it is file
if (( $+ctxt[realdir] )); then
export realpath=${ctxt[realdir]}$word
fi
__FTB_WORDS_DUMP__

View file

@ -4,35 +4,86 @@ emulate -L zsh -o extended_glob
zmodload zsh/mapfile
# receive arguments
local pid=$1 header_lines=$2 tmp_dir=$3 offset=$@[-1]
local pid=$1 header_lines=$2 active_group_style=$3 tmp_dir=$4 offset=$5
# read completion list
local -a list=(${(f)mapfile[$tmp_dir/completions.$pid]})
# get total group count
local grep_cmd=''
if (( $#list > 10000 )); then
local -Ua total=(${(f)"$(print -l ${list:$header_lines} | grep -a -oP '^\x1b\[[0-9;]*m')"})
else
local -Ua total=(${(M)${list:$header_lines}#$'\x1b['[0-9;]#*m})
local v
# Prefer GNU grep: grep first (Linux), then ggrep (macOS/Homebrew)
# Use $commands[...] so we get the resolved executable path (not an alias/function).
if (( ${+commands[grep]} )); then
v=$("$commands[grep]" --version 2>/dev/null)
[[ $v == *'(GNU grep)'* ]] && grep_cmd=$commands[grep]
fi
if [[ -z $grep_cmd && ${+commands[ggrep]} -eq 1 ]]; then
v=$("$commands[ggrep]" --version 2>/dev/null)
[[ $v == *'(GNU grep)'* ]] && grep_cmd=$commands[ggrep]
fi
fi
# get current group index, start from 2
local current=2
if [[ -f $tmp_dir/current-group.$pid ]]; then
current=$(( $(<$tmp_dir/current-group.$pid) + offset ))
# --- group marker detection (fzf-tab groups) ---------------------------------
# When groups exist, fzf-tab prefixes each entry with a group SGR like $'\e[94m'.
# When no groups exist (often file lists), entries may start with $'\e[0m'
# (reset for LS_COLORS). Treat that as "no groups".
local -Ua group_sgr_prefixes
if [[ -n $grep_cmd ]]; then
group_sgr_prefixes=(
${(f)"$(
print -l -- ${list:$header_lines} | "$grep_cmd" -a -oP $'^\x1b\\[[0-9;]*m'
)"}
)
else
group_sgr_prefixes=(${(M)${list:$header_lines}#$'\x1b['[0-9;]#*m})
fi
(( current > $#total )) && current=1
(( current == 0 )) && current=$#total
echo $current > $tmp_dir/current-group.$pid
# if the only marker is reset, there are no real groups
if (( $#group_sgr_prefixes == 1 )) && [[ $group_sgr_prefixes[1] == $'\e[0m' ]]; then
group_sgr_prefixes=()
fi
# --- current group index (persist across reloads) ----------------------------
local current=1
if (( $#group_sgr_prefixes > 0 )) && [[ -f $tmp_dir/current-group.$pid ]]; then
current=$(( $(<$tmp_dir/current-group.$pid) + offset ))
(( current > $#group_sgr_prefixes )) && current=1
(( current <= 0 )) && current=$#group_sgr_prefixes
fi
print -r -- $current > $tmp_dir/current-group.$pid
# configure style of active header
local sgr_on='' sgr_off=''
case $active_group_style in
(bold) sgr_on=$'\x1b[1m'; sgr_off=$'\x1b[22m' ;;
(underline) sgr_on=$'\x1b[4m'; sgr_off=$'\x1b[24m' ;;
(bold,underline) sgr_on=$'\x1b[1;4m'; sgr_off=$'\x1b[22;24m' ;;
(none) sgr_on=''; sgr_off='' ;;
(*) sgr_on=$'\x1b[1m'; sgr_off=$'\x1b[22m' ;; # fallback
esac
# The ANSI SGR code that marks every line in the currently selected group.
local current_prefix=${group_sgr_prefixes[current]}
# print headers
if (( header_lines != 0 )); then
print -l ${list[1,header_lines]/${total[current]}/$'\x1b[1m'}
local -i i
local line
for i in {1..$header_lines}; do
line=$list[i]
if [[ $line == *$current_prefix* ]]; then
# Apply style only to the group label (up to the first ']'), then disable it
line=${line/$current_prefix(#b)([^]]##)\]/$current_prefix$sgr_on${match[1]}]$sgr_off}
fi
print -r $line
done
fi
# print current group
if (( $#list > 10000 )); then
print -l ${list:$header_lines} | grep -a -F "${total[current]}"
if [[ -n $grep_cmd ]]; then
print -rl -- ${list:$header_lines} | "$grep_cmd" -a -F -- "${group_sgr_prefixes[current]}"
else
print -l ${(M)${list:$header_lines}:#${total[current]}*}
print -rl -- ${(M)${list:$header_lines}:#${group_sgr_prefixes[current]}*}
fi