percolを使ってターミナル操作を早く、便利に。

先日、shell勉強会で「zawを使ってシェル操作を快適に」というお話を聴いて、自分ももう少しそのあたりの環境を整えよう、と思い立った。


自分が使う選択をしたのは zaw ではなく percol
軽く調べてみたかんじでは zawってのは設定してある(もしくは自作する)決められたsourceを使って決められた操作を行うもので、zshに密接に結び付いているツールで。percolはそういうのではなく純粋に「入力をフィルタリングする」だけのツールなので、パイプなどを使って各コマンドと組み合わせることで色々な使い方ができそう。

percolの導入

Python製のツールなので、sudo pip install percolとかで入る。
READMEに書いてある通り、~/.percol.d/rc.pyに設定ファイルを用意することでプロンプトやキーマップをカスタマイズできる。
自分はできるだけEmacshelm と同じような操作感にするため こんなかんじにして使ってる。

percol.view.PROMPT  = ur"<green>Input:</green> %q"
percol.view.RPROMPT = ur"[%i/%I]"

percol.import_keymap({
    "C-f" : lambda percol: percol.command.forward_char(),
    "C-b" : lambda percol: percol.command.backward_char(),
    "C-p" : lambda percol: percol.command.select_previous(),
    "C-n" : lambda percol: percol.command.select_next(),
    "C-h" : lambda percol: percol.command.delete_backward_char(),
    "C-d" : lambda percol: percol.command.delete_forward_char(),
    "C-k" : lambda percol: percol.command.kill_end_of_line(),
    "C-a" : lambda percol: percol.command.beginning_of_line(),
    "C-e" : lambda percol: percol.command.end_of_line(),
    "C-v" : lambda percol: percol.command.select_next_page(),
    "M-v" : lambda percol: percol.command.select_previous_page(),
    "C-j" : lambda percol: percol.finish(),
    "C-g" : lambda percol: percol.cancel(),
})


ちなみに、現在これと同様なツールとしてgoで書かれたpecoというものが作られているので、もしかしたら将来的にこちらに移行するかもしれない。

percolでコマンド履歴の検索・絞り込み

READMEに書いてあるけれど。
シェルのhistoryをpercolに流し込むことで より素早く簡単にコマンド履歴を遡れる。
zshの場合はfc -l -n 1で全履歴を表示できる、のかな。

function percol_select_history() {
    local tac
    if which tac > /dev/null; then
        tac="tac"
    else
        tac="tail -r"
    fi
    BUFFER=$(fc -l -n 1 | eval $tac | percol --query "$LBUFFER")
    CURSOR=$#BUFFER             # move cursor
    zle -R -c                   # refresh
}
zle -N percol_select_history
bindkey '^R' percol_select_history

と設定を書いておけば、Ctrl+Rでの履歴検索をanything likeに快適に行うことができる。
あと、重複したコマンド履歴は必要ないので

setopt hist_ignore_all_dups

も指定しておいた。

percolでディレクトリ移動

の記事の通り、「一度でもcdしたことのあるディレクトリに効率よくcdする」ために、 autojumpz を使わずともpercolでディレクトリ訪問履歴を絞り込んで選択して移動、ということができる。
自分はしばらくzを使ってきてた けど、percol使ったほうが全然便利だわ!


上記参照記事ではchpwd_functionsに履歴の記録を仕込んでいるけれど、z.shを使っていれば履歴データが~/.zに残っているので 折角なのでそれを使うことにした。

function percol_select_directory() {
    local tac
    if which tac > /dev/null; then
        tac="tac"
    else
        tac="tail -r"
    fi
    local dest=$(_z -r 2>&1 | eval $tac | percol --query "$LBUFFER" | awk '{ print $2 }')
    if [ -n "${dest}" ]; then
        cd ${dest}
    fi
    zle reset-prompt
}
zle -N percol_select_directory
bindkey "^X^J" percol_select_directory

_z -rでランキング順?にsortされて最近訪問したディレクトリ一覧が出力されるので、それをpipeでpercolに渡して選択。

percolでtmuxのwindow選択・切り替え

これもREADMEに書いてあるけれど。

bind b split-window "tmux lsw | percol --initial-index $(tmux lsw | awk '/active.$/ {print NR-1}') | cut -d':' -f 1 | xargs tmux select-window -t"

のようにtmux.confに書いておくと、tmuxの現在のsessionで開いているwindowをpercolで絞り込み、選択して切り替えができる。
デフォルトで"w"キーにbindされているtmux choose-windowでも似たようなことが出来るのだけど、window数が多いときにpercolならインクリメンタルに絞り込みができるのと、tmux split-windowを使うことで現在のwindowが隠れることなく選択操作ができるようになるのが利点だと思う。


ちなみに、tmux list-windowsで表示される情報はwindow nameやpane情報だったりするのだけど、percolで絞り込むときなどは特に「そのwindowはどのディレクトで作業しているものか」などもあった方が嬉しいので、

tmux list-windows -F '#{window_index}: #{window_name}#{window_flags} (#{window_panes} panes) #{pane_current_path} #{?window_active, (active),}

のように#{pane_current_path}を含むformatを指定して表示させるようにした。最終的にtmux.confには

bind-key C-t split-window -c '#{pane_current_path}' "tmux list-windows -F '#{window_index}: #{window_name}#{window_flags} (#{window_panes} panes) #{pane_current_path} #{?window_active, (active),}' | percol --initial-index $(tmux lsw | awk '/active.$/ {print NR-1}') | cut -d':' -f 1 | xargs tmux select-window -t"

と(長いw)。


あと、window nameには通常は実行中のコマンド名が表示されているのだけれど、sshで他hostにログインしている場合はただ"ssh"とだけ出るのではなく そのhost情報などを表示するようにした方が分かりやすいし誤操作を防げそう。ということで

など参考にしつつ、自分ではsshコマンドをラップするシェルスクリプトを用意した。

#!/bin/sh
if [ -n "$TMUX" ]; then
    local_command='tmux rename-window $(echo "%r@%n(%h:%p)")'
fi

command -p ssh -o PermitLocalCommand=yes -o LocalCommand="${local_command}" "$@"

if [ -n "$TMUX" ]; then
    tmux set-window-option -u automatic-rename
fi

tmux上でsshを叩いたときだけ、接続先のhost名などの情報を含むwindow nameに更新し、終了したら元通りに。tmux rename-windowで変更した場合はautomatic-renameオプションがoffになるようなので、それを戻してあげるのが正しい復元方法だと思う。

percolでweechatのbuffer選択

IRC環境として weechatを使っている けれど、これのchannel選択なんかもpercolでできると嬉しい。

の記事を真似てみようとした。
WeeChat はデフォルトでFifo pluginが有効になっていて、起動中のweechatに対して任意のコマンドを送ることができる。

しかし内部の情報を取得するようなことはできなそうで、やろうとするとそういった機能を持つpluginを作るなり導入するなりする必要がありそうだった。
苦肉の策として、「コマンド送信によって現在のchannel(buffer)一覧情報をファイル保存させ、そのファイルを読み込む」という方法で一覧を取得することにした。

for fifo in $HOME/.weechat/weechat_fifo_*; do
    echo '*/mute layout store' > $fifo
    echo '*/mute save weechat' > $fifo
done

のようにFIFO pipeに対しlayout storesaveを送ることで、現在のbuffer情報が~/.weechat以下に保存される(毎回その結果がweechatのbufferに出力されるのを防ぐためにmuteコマンド経由で)。
あとはそれで保存された設定ファイルを読み込んでpercolに渡し、選択結果を使ったbuffer切り替えコマンドを再度FIFO pipeに流せばよい。

BUFFER=$(grep 'default.buffer' $HOME/.weechat/weechat.conf | cut -d'"' -f2 | percol | cut -d';' -f3)
if [ -n "$BUFFER" ]; then
    for fifo in $HOME/.weechat/weechat_fifo_*; do
        echo '*/mute buffer' $BUFFER > $fifo
    done
fi

これでだいぶ捗る。