Unyablog.

のにれんのブログ

zsh の色付き vcs_info を一行上の右端に出す

書いておかないとあとで分からなくなるのでメモ。

zsh 便利ですよね。vcs_info も便利ですよね。

vcs_info は名前の通り VCS の情報を出してくれる君です。

zshのターミナルにリポジトリの情報を表示してみる · けんごのお屋敷

本題

最近 zsh のプロンプトを以下のように変更しました。

f:id:nonylene:20170308024523p:plain

f:id:nonylene:20170308034716p:plain

ポイントは左にあった vcs_info が右に移動していることです。

RPROMPT 問題

vcs_info を右に移動するには vcs_info を単純に RPROMPT に移動させれば良いのでは?と思いますが、 RPROMPT はコマンド入力と同じ場所に表示されるため、パスやコマンドが長いと消えてしまいます。

f:id:nonylene:20170308025835p:plain

なら上に表示すれば良いのではと思って PROMPTRPOMPT を二行にしてみると、今度は表示されなくなってしまいます。

RPOMPTPROMPT が複数行に渡っても最終行と同じ行に表示され、改行ができません。

f:id:nonylene:20170308025857p:plain

一行上の右端に表示するには、 precmd 内でプロンプト復帰前に printf コマンドを用いて表示する必要がありました。

printf 問題

色がない場合、 printf で右端に表示するには以下のように行なえば良いです。

# プロンプト復帰前に呼ばれる
precmd() {
  # $prompt_right -> 右端に表示する文字列
  # $prompt_left -> 左端に表示する文字列
  printf "%s%*s" "$prompt_left" "$((${COLUMNS} - ${#prompt_left}" "$prompt_right"
}

%*s ( * で幅を指定する) で長さに "$((${COLUMNS}-${#prompt_left}" (全体の横の長さから $prompt_left の長さを引いたもの) を指定することによって、右寄せを実現しています。

しかし、これを色付きで行うと崩れてしまいます。

f:id:nonylene:20170308031636p:plain

なぜなら、ANSI エスケープシーケンスを含めた時、 $prompt_right 等にエスケープシーケンスが入って文字の長さが長くなり、 printf の幅空けや ${#prompt_left} による文字長の取得が狂うためです…!!!!😇

この時、ちゃんとシーケンスの長さを考慮してあげるとうまく表示できるようになります。

# 長さは適当に 10 にしている
printf "%s%*s" "$prompt_left" "$((${COLUMNS} - ${#prompt_left} + 10" "$prompt_right"

vcs_info の action 問題

vcs_info には actionformats という機能があり、コンフリクト解決やリベース時などにその action を表示することが出来ます。

自分は、acrion の中身は目立つように赤色にしています。なので action があると、シーケンスの文字数が増えて再び描画が崩れてしまいます。

f:id:nonylene:20170308032729p:plain

action があるかどうかは precmd 内部では分かりません。どうしようかなと調べていると、 vcs_info にはフック関数を設定できることが分かりました。

zsh.sourceforge.net

これを使って、フック関数の中で action があるかどうかを見れば、あとは変数にセットすることで precmd 内でも分かります。

最終的に以下の様になりました。

# tput でエスケープシーケンスを出力する
tput_normal=$(tput sgr0)
tput_yellow=$(tput setaf 3)
tput_red=$(tput setaf 1)
tput_cyan=$(tput setaf 6)

# vcs_info の設定
zstyle ':vcs_info:*' enable git hg
zstyle ':vcs_info:*:*' formats "[${tput_yellow}%r: %b${tput_normal}]"
zstyle ':vcs_info:*:*' actionformats "[${tput_yellow}%r: %b${tput_normal} (${tput_red}%a${tput_normal})]"

# hook 関数の設定
# メッセージが設定される直前
zstyle ':vcs_info:*+set-message:*' hooks vcs_info_hook
# VCS が存在しないとき
zstyle ':vcs_info:*+no-vcs:*' hooks no_vcs_hook

# VCS がないときのエスケープシーケンス長
novcs_color_len=$((${#tput_cyan} + ${#tput_normal}))
# VCS があるが action がないとき
noaction_color_len=$((${novcs_color_len} + ${#tput_yellow} + ${#tput_normal}))
# VCS があり、action もあるとき
action_color_len=$((${noaction_color_len} + ${#tput_red} + ${#tput_normal}))

# $prompt_color_len にシーケンス長を設定する
# VCS があるときの hook
function +vi-vcs_info_hook() {
  # action があるかどうかは hook_com[action] を見れば良い
  if [ -z ${hook_com[action]} ]; then
    prompt_color_len=$noaction_color_len
  else
    prompt_color_len=$action_color_len
  fi
}

# VCS がないときの hook
function +vi-no_vcs_hook() {
  prompt_color_len=$novcs_color_len
}

precmd() {
  vcs_info
  # 左寄せする文字列
  prompt_left="${tput_cyan}${USER}@${HOST}${tput_normal}"

  # 各情報を表示
  printf "\n%s%*s\n" "$prompt_left" \
    "$((${COLUMNS}-${#prompt_left}+${prompt_color_len}-1))" \
    "${vcs_info_msg_0_}"
}

# このプロンプトは printf 後に表示される (パスの部分)
PROMPT='%~ %(!. !root! #.>) '

途中で -1 しているのは、右寄せの最後に一文字空けるためです (wrap っぽく判別されるのを防ぐ)。

これで大体いい感じになりました。

f:id:nonylene:20170308034641p:plain

リサイズ問題

ウィンドウのサイズを変更した時、 RPROMPT では良い感じに右寄せ文字が移動してくれるのですが precmdprintf した文字列は再描画されないので残念ながらこのままではズレてしまいます。

f:id:nonylene:20170308135044p:plain

ここで、 TRAPWINCH を用います。

TRAPWINCH はサイズが変更された時に呼ばれる関数です。しかし、単純にここで printf すると、プロンプトの下に追加されてしまうので思った通りになりません。

f:id:nonylene:20170308135638p:plain

そこで、 printf していたのを PROMPT に変数として組み込み、TRAPWINCH 内で zle reset-prompt することによって PROMPT ごと再描画するようにします。

# $prompt_header を生成する
function generate_promopt_header() {
  prompt_header=$(printf "\n%s%*s" "$prompt_left" \
    "$((${COLUMNS}-${#prompt_left}+${prompt_color_len}-1))" \
    "${vcs_info_msg_0_}")
}

precmd() {
  vcs_info
  prompt_left="${tput_cyan}${USER}@${HOST}${tput_normal}"
  # printf をやめて $prompt_header を生成するのみとする
  generate_promopt_header
}

TRAPWINCH() {
  # 長さが変わったので $prompt_header を再生成   
  generate_promopt_header
  # プロンプト再描画
  zle reset-prompt
}

# $prompt_header を改行前に含める
PROMPT='${prompt_header}
%~ %(!. !root! #.>) '

こうして、幅が変わっても無事に再描画されるようになりました。

f:id:nonylene:20170308140603g:plain

残る問題

実はまだ問題が残っていて、ブランチ名等が日本語等のときにカラム幅が合わなくなり、はみ出してしまいます。

f:id:nonylene:20170308140749p:plain

wc -c 等を使ってもバイト数だから上手くいかないし、どうしたものか…。

まあ日本語で git ブランチや git 用のディレクトリ作ることはそうそう無さそうなので今回は放置。

全体的な zshrc はこちらです。(大したことやってない)

dotfiles/.zshrc at master · nonylene/dotfiles · GitHub