プロンプトのgitブランチ表示を定期更新

Gitのブランチ名をzshのプロンプトに表示させているのですが、別端末でブランチを切り替えたりすると、普通は現状が反映されません。心配だったらenterキーを押せば良いのですが、端末がたくさん開いてるときに限って間違ったブランチで作業してしまいそうになるので、これを防ぐために1分に1度プロンプトを更新するようにしました。ついでにプロンプトの時刻も更新されます。


関係ありそうな .zshrc の部分を切り出すと、こんな感じ。補完候補(menu select)表示中に更新が起こると候補が消えてしまうという課題がありますが…

zmodload zsh/datetime # $EPOCHSECONDS, strftime等を利用可能に

#gitブランチ名表示
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats '%c%u%b'
zstyle ':vcs_info:git:*' actionformats '%c%u%b|%a'

#git情報更新
update_vcs_info() {
  psvar=()
  LANG=en_US.UTF-8 vcs_info
  [[ -n "$vcs_info_msg_0_" ]] && psvar[1]="$vcs_info_msg_0_"
}

#プロンプトを毎分更新
reset_tmout() { TMOUT=$[60-EPOCHSECONDS%60] }
precmd_functions=($precmd_functions update_vcs_info reset_tmout)
redraw_tmout() { zle reset-prompt; reset_tmout }
TRAPALRM() { update_vcs_info; redraw_tmout }

#プロンプト
unsetopt promptcr               # 改行のない出力をプロンプトで上書きするのを防ぐ
setopt PROMPT_SUBST
PROMPT="%F{green}[%m-%T]%f%# "
RPROMPT="%(?..%F{red}-%?-)%F{green}[%1(v|%F{yellow}%1v%F{green} |)%n:%~]%f"
[[ -n "$SSH_CLIENT" ]] && PROMPT="%F{green}[%F{cyan}%B%m%b%F{green}-%T]%f%# "
追記

menuselectが消えてしまう件は、以下のように$WIDGET (直前に入力したキーのアクション) と$_lastcomp[insert] (最後に表示した補完の情報)の値を調べて、menu selectが表示されていると思われる場合にはresetを止めることで回避できそうです。なお該当する $WIDGET の値はキーバインドによって変わる可能性があります。自分の環境で何にすべきかは bindkey "^I" でTabキーに割り当てられているアクションを調べれば分かります。
ただし、古いzsh(5.1未満)でTRAPALRMの中で$WIDGETを評価しようとするとSEGVで死んでしまうバグがあるようで、そのような環境では諦めるしかありません。。

autoload -U is-at-least
precmd_functions=($precmd_functions reset_tmout reset_lastcomp)
reset_lastcomp() { _lastcomp=() }
if is-at-least 5.1; then
    # avoid menuselect to be cleared by reset-prompt
    redraw_tmout() {
        [ "$WIDGET" = "expand-or-complete" ] && [[ "$_lastcomp[insert]" =~ "^automenu$|^menu:" ]] || zle reset-prompt
        reset_tmout
    }
else
    # evaluating $WIDGET in TMOUT may crash :(
    redraw_tmout() { zle reset-prompt; reset_tmout }
fi
TRAPALRM() { check_gitinfo_update; redraw_tmout }

...
追記ここまで


なお変更の有無を表示する check-for-changes は巨大レポジトリだとあからさまに遅くなるので入れてません。


しかし、環境によって vcs_info が遅いことがあるので、ディレクトリやgit HEADが移動された時以外は実行しないようにし、さらにそのチェックも別のプロセスを立ち上げたりすることなくzshのプロセス内で処理が完結するようにした版が以下。


やってることは、precmdでコマンドを記録しておいて、gitが実行されたりディレクトリ移動が起きたらvcs_info、それ以外でも、各コマンド実行後と1分ごとに.git/HEADの更新日時を見て、前回のチェック時刻から更新が起きていたらvcs_info、という感じです。更新日時のチェックはextended_globの更新日時を元にマッチさせる機能を使っているので、外部コマンドの呼び出したりはしない(つまりforkしない)ため高速なはず。

zmodload zsh/datetime # $EPOCHSECONDS, strftime等を利用可能に
autoload -U is-at-least

#gitブランチ名表示
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats '%c%u%b'
zstyle ':vcs_info:git:*' actionformats '%c%u%b|%a'

#プロンプトを毎分更新
reset_tmout() { TMOUT=$[60-EPOCHSECONDS%60] }
precmd_functions=($precmd_functions reset_tmout reset_lastcomp)
reset_lastcomp() { _lastcomp=() }
if is-at-least 5.1; then
    # avoid menuselect to be cleared by reset-prompt
    redraw_tmout() {
        [ "$WIDGET" = "expand-or-complete" ] && [[ "$_lastcomp[insert]" =~ "^automenu$|^menu:" ]] || zle reset-prompt
        reset_tmout
    }
else
    # evaluating $WIDGET in TMOUT may crash :(
    redraw_tmout() { zle reset-prompt; reset_tmout }
fi
TRAPALRM() { check_gitinfo_update; redraw_tmout }

#プロンプト
unsetopt promptcr               # 改行のない出力をプロンプトで上書きするのを防ぐ
setopt PROMPT_SUBST
PROMPT="%F{green}[%m-%T]%f%# "
RPROMPT="%(?..%F{red}-%?-)%F{green}[%1(v|%F{yellow}%1v%F{green} |)%n:%~]%f"
[[ -n "$SSH_CLIENT" ]] && PROMPT="%F{green}[%F{cyan}%B%m%b%F{green}-%T]%f%# "

#カレントディレクトリ/コマンド記録
local _cmd=''
local _lastdir=''
preexec_gitupdate() {
  _cmd="$1"
  _lastdir="$PWD"
}
preexec_functions=($preexec_functions preexec_gitupdate)
#git情報更新
update_vcs_info() {
  psvar=()
  LANG=en_US.UTF-8 vcs_info
  [[ -n "$vcs_info_msg_0_" ]] && psvar[1]="$vcs_info_msg_0_"
}
#同一dir内でシェル外でgitのHEADが更新されていたら情報更新
check_gitinfo_update() {
  if [ -n "$_git_info_dir" -a -n "$_git_info_check_date" ]; then
    if [ -f "$_git_info_dir"/HEAD(ms-$((EPOCHSECONDS-$_git_info_check_date))) ]; then
      _git_info_check_date=$EPOCHSECONDS
      update_vcs_info
    fi 2>/dev/null
  fi
}
#カレントディレクトリ変更時/git関連コマンド実行時に情報更新
precmd_gitupdate() {
  local _r=$?
  local _git_used=0
  case "${_cmd}" in
    git*|stg*) _git_used=1
  esac
  if [ $_git_used = 1 -o "${_lastdir}" != "$PWD" ]; then
    local cwd="./"
    _git_info_dir=
    _git_info_check_date=
    while [ "$(echo $cwd(:a))" != / ]; do
      if [ -f .git/HEAD ]; then
        _git_info_dir="$PWD/.git"
        _git_info_check_date=$EPOCHSECONDS
        break
      fi
      cwd="../$cwd"
    done
    update_vcs_info
  else
    check_gitinfo_update
  fi
  return $_r
}
precmd_functions=($precmd_functions precmd_gitupdate)