fzf で画像プレビュー

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

スクリプト

スクリプトは次の流れ

  1. ファイル名、保存ディレクトリ、ファイルの種類ごとにプレビューに用いるツールを分岐
  2. 画像ファイルなら
    1. 呼び出し元のウィンドウのプロセスを探す
    2. 見つかった呼び出し元の Window ID、ウィンドウ幅・高さを取得する
    3. 呼び出し元ウィンドウの右半分に画像プレビューアプリを貼り付ける

スクリプトの内容

#!/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
      など対応ターミナルは多いので、それほど困らないだろう
    もしくは
    X.org を使うように変えるか、次のように無理やり Wayland を無効にして再起動する必要がある
    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 }