diff --git a/lib/-ftb-fzf b/lib/-ftb-fzf index 3741d22..0af2e7e 100755 --- a/lib/-ftb-fzf +++ b/lib/-ftb-fzf @@ -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 diff --git a/lib/-ftb-preview.tpl b/lib/-ftb-preview.tpl new file mode 100755 index 0000000..4824854 --- /dev/null +++ b/lib/-ftb-preview.tpl @@ -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__ + diff --git a/lib/ftb-switch-group b/lib/ftb-switch-group index 8d06956..9c4397e 100644 --- a/lib/ftb-switch-group +++ b/lib/ftb-switch-group @@ -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