fzf: 0.59.0

0.59.0 #

(February 2025)

Highlights #

  • Enhanced path scheme to prioritize file name matches
  • click-header event now sets $FZF_CLICK_HEADER_WORD and $FZF_CLICK_HEADER_NTH, allowing you to implement a clickable header for changing the search scope
  • search and transform-search action to extend the search syntax of fzf
  • --header-lines-border to display header from --header-lines with a separate border
  • --no-input option to completely disable and hide the input section

Prioritizing file name matches #

Since fzf is a general-purpose text filter, its algorithm was designed to “generally” work well with any kind of input. However, admittedly, there is no true one-size-fits-all solution, and you may want to tweak the algorithm and some of the settings depending on the type of the input. To make this process easier, fzf provides a set of “scheme"s for some common input types. i.e. default, path, and history.

This release enhances path scheme to prioritize matches occurring in the file name of the path. Here is an example.

git ls-files | fzf -qsgit ls-files | fzf -qs --scheme=path
src/LICENSEsrc/server.go
src/ansi.gosrc/util/slab.go
src/core.go.github/workflows/sponsors.yml
src/item.gosrc/LICENSE
src/tmux.gosrc/ansi.go
src/cache.gosrc/core.go
src/proxy.gosrc/item.go

You can see with --scheme=path, the matches with the search term occuring in their names are prioritized over the rest.

Automatic scheme selection #

fzf will automatically choose path scheme

  • when the input is a TTY device, where fzf would start its built-in walker or run $FZF_DEFAULT_COMMAND which is usually a command for listing files,
  • but not when reload or transform action is bound to start event, because in that case, fzf can’t be sure of the input type.
# Built-in walker is used, and --scheme=path is automatically applied
fzf

# fzf runs $FZF_DEFAULT_COMMAND, and --scheme=path is automatically applied
export FZF_DEFAULT_COMMAND='fd --type f --hidden'
fzf

However, this automatic selection may not always be desirable. If you observe suboptimal behavior, explicitly specify the scheme you want.

Note: I’m not entirely sure if this automatic selection is really a good idea. I wanted more users to benefit from the enhancement without having to change their settings. But it might make fzf less predictable. Please let me know if you have any feedback.

Understanding the path scheme #

path scheme does two things:

  1. Adjust the scoring algorithm to give the highest bonus points to the matching characters after the path separators. Characters after whitespaces are given less bonus points.
    • On the contrary, in the default scheme, characters after whitespaces are given the highest bonus points.
  2. Apply --tiebreak=pathname,length option

The additional pathname tiebreak is what’s new in this release. It involves more computation, so the path scheme will be slightly slower than the default scheme. However, the difference in performance should be negligible in most cases.

click-header enhancements #

When you click on the header, click-header event is triggered and it exports the following environment variables.

VariableDescription
$FZF_CLICK_HEADER_LINEClicked line number starting from 1
$FZF_CLICK_HEADER_COLUMNClicked column number starting from 1
$FZF_CLICK_HEADER_WORDThe word that was clicked (new)
$FZF_CLICK_HEADER_NTHThe nth expression for the clicked word (new)
$FZF_KEYThe name of the last key or event. Now it reports click types as well. click, ctrl-click, alt-click, shift-click, double-click, etc.

You can use these variables, especially the last three, to implement a clickable header that changes the search scope.

# Click on the header line to limit search scope
ps -ef | fzf --style full --layout reverse --header-lines 1 \
             --header-border bottom --no-list-border \
             --color fg:dim,nth:regular \
             --bind 'click-header:transform-nth(
                       echo $FZF_CLICK_HEADER_NTH
                     )+transform-prompt(
                       echo "$FZF_CLICK_HEADER_WORD> "
                     )'

Now the kill completion implements a more sophisticated version that allows selecting/toggling multiple columns with {ctrl,alt,shift}-click.

search and transform-search action #

The new search and transform-search action allow you to trigger an fzf search with an arbitrary query string. This frees fzf from strictly following the prompt input, enabling custom search syntax.

In the example below, transform action is used to conditionally trigger reload for ripgrep, followed by search for fzf. The first word of the query initiates the Ripgrep process to generate the initial results, while the remainder of the query is passed to fzf for secondary filtering.

#!/usr/bin/env bash

# Controlling Ripgrep search and fzf search simultaneously

export TEMP=$(mktemp -u)
trap 'rm -f "$TEMP"' EXIT

INITIAL_QUERY="${*:-}"
TRANSFORMER='
  rg_pat={q:1}      # The first word is passed to ripgrep
  fzf_pat={q:2..}   # The rest are passed to fzf

  if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
    echo "$rg_pat" > "$TEMP"
    printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
  fi
  echo "+search:$fzf_pat"
'
fzf --ansi --disabled --query "$INITIAL_QUERY" \
    --with-shell 'bash -c' \
    --bind "start,change:transform:$TRANSFORMER" \
    --color "hl:-1:underline,hl+:-1:underline:reverse" \
    --delimiter : \
    --preview 'bat --color=always {1} --highlight-line {2}' \
    --preview-window 'up,60%,border-line,+{2}+3/3,~3' \
    --bind 'enter:become(vim {1} +{2})'
  • As you can see, the first term return is passed to ripgrep, the primary filter, and the second term abc is passed to fzf, making it perform secondary filtering over the results from ripgrep.

Info

Note that we bind the same tranform action to start and change at once.

--bind "start,change:transform:$TRANSFORMER"

This was not possible in the previous versions.

Secondary border for the header lines #

After experimenting with --header-border, I realized that there are cases where you want to treat the header from --header-lines differently from the header from --header. Here’s an example. This is a classic use case of --header-lines.

ps -ef | fzf --style full --layout reverse --header-lines 1

Let’s say you want to provide a binding for reloading the list, and tell the users about it using the header.

ps -ef | fzf --style full --layout reverse --header-lines 1 \
             --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload'

Okay, but I’d prefer to have some visual separation between the two headers because they serve different purposes. I used to add new line characters to --header, but now we have a better option, --header-lines-border.

ps -ef | fzf --style full --layout reverse --header-lines 1 \
             --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
             --header-lines-border bottom --no-list-border

Unlike the other border types, none option has a different meaning here. It is used to still separate the two headers, but without an additional border.

ps -ef | fzf --style full --layout reverse --header-lines 1 \
             --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
             --header-lines-border none

--header-first option will only affect the header from --header.

ps -ef | fzf --style full --layout reverse --header-lines 1 \
             --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
             --header-lines-border none --header-first

Hiding input section #

--no-input option completely disables and hides the input section. You can no longer type in queries. What good is it? You may want to use it to create a very simple menu with no input capability.

(echo Yes; echo No) | fzf --height=~100% --no-input --reverse --style full

Although we can’t type in search queries, we can use the new search and transform-search actions to trigger searches like in the example below.

# Click header to trigger search
fzf --header '[src] [test]' --no-input --layout reverse \
    --header-border bottom --input-border \
    --bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'

Here is another example that implements Vim-like mode switching.

fzf --layout reverse-list --no-input \
    --bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
    --bind 'enter,esc,ctrl-c:transform:
      if [[ $FZF_INPUT_STATE = enabled ]]; then
        echo "rebind(j,k,/)+hide-input"
      elif [[ $FZF_KEY = enter ]]; then
        echo accept
      else
        echo abort
      fi
    '

(FZF_INPUT_STATE is one of enabled, disabled, or hidden.)

Personally, I don’t see myself using this feature much, but if you have a creative use case, I’d love to hear about it.

bell action 🔔 #

Last, and, yes, least. We have a new action called bell that rings the terminal bell. You can use it to provide feedback to the user.

# Press CTRL-Y to copy the current line to the clipboard and ring the bell
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
Last modified: Jan 30, 2025
Copyright © 2024 Junegunn Choi