fzf とは?
fzf が何かしらない人へ簡単に説明すると fuzzy finder の一つ
公式のデモや下の動画を見ると分かりやすいと思う
上に有るとおり
- 主にターミナルで使い、入力内容で絞り込むことができる
- fuzzy の名の通り入力内容は前後するなど、ある程度曖昧な入力で OK
- ここではファイルの検索を絞り込んでいるが、コマンドの実行履歴などテキスト・データならあらゆるデータの絞り込みができる
- 例えば次のような使い方をすると、絞り込んだ内容でアプリの起動もできる
vim $( find . -name "*.*" | fzf )
プレビュー
必要なツールを用意し、fzf 起動時に適切な引数でそれらを指定すれば、上の様にこの絞り込み時に、ディレクトリのファイルリストや内容をプレビューできる。
しかしながら、画像のプレビューができない。これはそもそもターミナルで画像を表示方法が少ないのが主な原因
汎用性の有るターミナルでの画像表示方法に Sixel が有るが、対応ターミナルもまだ多くなく (Gnome 標準の Gnome Terminal も未対応)、対応したターミナルを用いても fzf が未対応でプレビューできない。
そこで画像プレビュー・アプリを使い、fzf の選択位置に合わせてプレビュー画像が切り替わるように
- fzf から呼び出すシェル・スクリプトを用意
- シェル・スクリプト内で使う次のツールを準備
- 呼び出し元の Window ID を調べる wmctrl
- 画像プレビューワ nsxiv
確認したアプリ
- Gnome Terminal
- mlterm
- Tilda
- Guake
- Vim/GVim
スクリプト
スクリプトは次の流れ
- ファイル名、保存ディレクトリ、ファイルの種類ごとにプレビューに用いるツールを分岐
- 画像ファイルなら
- 呼び出し元のウィンドウのプロセスを探す
- 見つかった呼び出し元の Window ID、ウィンドウ幅・高さを取得する
- 呼び出し元ウィンドウの右半分に画像プレビューアプリを貼り付ける
スクリプトの内容
#!/bin/sh
f=$( echo "$@"|
sed -e "s>^~>$HOME>" -e "s/'/\\\'/g" -e 's/"/\\"/g' |
xargs -I{} readlink -f "{}")
mime=$( file --dereference --brief --mime-type "$f" )
sxiv_preview(){
ppid=$PPID
ps_xo=$( ps xo pid,ppid,comm )
wmctrl_lGp=$( wmctrl -lGp |
grep -E '^0x[0-9a-f]+' |
sed -E 's/(0x[0-9a-f]+) +-?[0-9]+ ([0-9]+) +-?[0-9]+ +-?[0-9]+ +([0-9]+) +([0-9]+) .+/\1\t\2\t\3\t\4/' )
while true ; do
win_id=$( echo "$wmctrl_lGp" |
grep -Ei "^0x[0-9a-f]+[[:space:]]$ppid([[:space:]][0-9]+){2}$" )
if [ -n "$win_id" ]; then
width=$(( $( echo "$win_id" | cut -f3 ) / 2 ))
height=$( echo "$win_id" | cut -f4 )
win_id=$( echo "$win_id" | cut -f1 )
break
fi
if ! echo "$ps_xo" | grep -Eq "^ *$ppid" ; then
return 0
fi
ppid=$( echo "$ps_xo" | grep -E "^ *$ppid" )
if echo "$ppid" | grep -Eq ' tmux: server$' ; then
ppid=$( tmux list-clients -F '#{session_activity}:#{client_pid}' |
sort |
tail -n 1 |
cut -d: -f2 )
else
ppid=$( echo "$ppid" | sed -E 's/ *[0-9]+ +([0-9]+) +.+/\1/' )
fi
done
echo ''
nsxiv --embed "$win_id" -g "${width}x${height}+${width}+0" --no-bar --scale-mode f --private "$1" &
wmctrl -ia "$win_id"
return 1
}
echo "$f"
case "${f##*/}" in
vimrc|gvimrc )
source-highlight --failsafe -f esc --lang-def=vim.lang --style-file=esc.style -i "$f" ;;
.bashrc|.bash_history|bashrc|.cshrc|.profile|.xprofile|.zshrc|.kshrc )
source-highlight --failsafe -f esc --lang-def=sh.lang --style-file=esc.style -i "$f" ;;
.*rc|.gitconfig|.gitattributes|.gitignore )
source-highlight --failsafe -f esc --lang-def=conf.lang --style-file=esc.style -i "$f" ;;
*)
case "${f%/*}" in
*/.bash|*/.csh|*/.ksh|*/.zsh )
source-highlight --failsafe -f esc --lang-def=sh.lang --style-file=esc.style -i "$f" ;;
*)
case "$mime" in
inode/directory ) tree -L 1 -C "$f" ;;
application/json ) jq -C . "$f" ;;
application/zip ) unzip -l "$f" ;;
application/gzip ) gzip --list "$f" ;;
application/x-compressed-tar ) gzip --list "$f" ;;
application/x-xz ) xz --list "$f" ;;
application/pdf ) pdftotext "$f" - ;;
audio/* ) ffmpeg -hide_banner -i "$f" -f metadata - | tail -n +2 ;;
text/*|application/x-shellscript|application/javascript|application/rdf+xml|application/xml|application/x-perl|application/x-desktop )
/usr/share/source-highlight/src-hilite-lesspipe.sh "$f" ;;
application/vnd.openxmlformats-officedocument.wordprocessingml.document|application/vnd.oasis.opendocument.text )
soffice --convert-to "txt:Text (encoded):UTF8" --cat "$f" ;;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet|application/vnd.oasis.opendocument.spreadsheet )
csv=$HOME/.tmp/$(basename $f).csv
ssconvert --export-file-per-sheet "$f" "$csv" 1>/dev/null 2>&1
for i in "$csv".*
do
cat "$i"
rm "$i"
done
image/* )
if sxiv_preview "$f"; then
exiftool "$f"
fi
;;
video/* )
time=$( ffmpeg -hide_banner -i "$f" 2>&1 |
awk '/^\s*Duration:/{
split($2, t, /[:,]/)
printf("%d", ( t[1] * 3600 + t[2] * 60 + t[3] ) / 2)
}'
)
time=$(( time / 2 ))
ffmpeg -ss $time -i "$f" -frames:v 1 -y "$HOME/.tmp/tmp-move-sixv.jpg" 2> /dev/null
if sxiv_preview "$HOME/.tmp/tmp-move-sixv.jpg" ; then
ffmpeg -hide_banner -i "$f" -f metadata - | tail -n +2
fi
;;
*) echo "$mime" ;;
esac
;;
esac
;;
esac
exit 0
スクリプト内で使っているツール
標準でインストールされていないツールとしては次を使用
- wmctrl, nsxiv
- 先に紹介した画像プレビュー処理に用いているツール
- source-highlight
- テキスト・ファイルをカラー表示: less のカラー化にも使用
- tree
- ディレクトリ内をツリー表示
- jq
- json を読みやすく整形
- unzip
- zip ファイル内のファイルリスト
- pdftotext
- PDF 内のテキストデータ
- ffmpeg
- 動画・音声ファイルのメタ情報
- ssconvert
- Excel などのスプレッド・シート内のテキストデータ
他にも同様のツールが有るが、出力されるシートに制限がある
スクリプト内で使っているツールのインストール例
sudo apt install -y wmctrl nsxiv source-highlight tree jq p7zip-full poppler-utils gnumeric
上記スクリプトの欠点
- wmctrl は X.org では問題ないが、そのままでは Wayland では動作しない場合が有る
この場合 Ubuntu 24.10 にアップグレードする- Wayland でも wmctrl が Window ID を取得できるアプリが増えているので、問題が解消できる可能性が有る
- 私が最もよく使う GVim では問題なく動作する
- ただし、相変わらず標準のターミナルである Gnome Terminal は未対応なので、適当なターミナルが必要になる
- Guake
- Tilda
- XTerm
- mlterm
sudo sed -i 's/^ *# *WaylandEnable=false/WaylandEnable=false/' /etc/gdm3/custom.conf - Vim/GVim から使うときは関係ないが、Gnome Terminal などのターミナルから使っている場合、表示している画像をデフォルト画像アプリで開くとプレビュー画像がそのまま残ってしまう
- これに関してはファイル検索して、絞り込んだファイルをアプリを開きたいときは、入力が楽なように 1 文字コマンドにしていたので、次のような関数化をして対処した
- やっているのは、fzf 起動前に起動済みの nsxiv をリスト・アップしておき、関数終了時に起動後に残っている nsxiv を強制的に終了させるだけ
f(){
sxiv_ls="^($( cat <<-_EOF_|
DUMMY
$( pgrep -u "$USER" -f 'nsxiv --embed ' )
_EOF_
tr '\n' '|' |
sed -E 's/\|+$//'
)$)"
if command -v fdfind > /dev/null ; then
if [ $# -eq 0 ]; then
dir=""
elif [ -d "${@:$#:1}" ]; then
dir=" . $@"
else
dir="$@"
fi
eval "${FZF_DEFAULT_COMMAND% .} $dir" |
fzf --prompt 'Files> ' --preview "$HOME/bin/fzf-preview.sh {}" --bind='enter:become(xdg-open {})'
else
if [ $# -eq 0 ]; then
dir=$( pwd )
args=''
elif [ -d "${@:$#:1}" ]; then
dir="${@:$#:1}"
args="${@%%${@:$#:1}}"
else
dir=$( pwd )
args="$@"
fi
cmd="${FZF_DEFAULT_COMMAND/find -L ./find -L $dir} $args"
eval "${cmd/ -printf \"%P\\n\"/ -print0}" |
xargs --no-run-if-empty -n 1 -P 0 -0 -I{} realpath --no-symlinks --relative-base="$( pwd )" "{}" 2> /dev/null |
fzf --preview "$HOME/bin/fzf-preview.sh {}" --bind='enter:become(xdg-open {})'
fi
pgrep -u "$USER" -f 'nsxiv --embed ' |
grep -Ev "$sxiv_ls" |
xargs --no-run-if-empty -n 1 -P 0 kill
}