Zshのこだわりプロンプトが実用的だから見てほしい

# ことの発端
macOS Catalinaにアップデートしたタイミングでデフォルトのシェルがbashからzshに変わりました。
それまでは特に気にすることなくデフォルトのターミナルをそのまま使っていたのですが、zshになってプロンプトの最後の文字が`$`から`%`に変わってるのを見て、なんとなく違和感を感じました。
で、もしやカスタマイズできるのでは？と思い、調べてみたらかなり自由度があるじゃないですか！
そこで紆余曲折ありながらも現在のスタイルに落ち着いたので、こだわりのポイントなどをじっくりと共有させていただければと思った次第です。

# 追記
right_promptの配色条件を変更しました。

従来: ステージングされていないファイルがあるときに背景色、全てステージングされている時にフォント色で表していた
↓
現在: ステージングされたファイルがある時に背景色、全くステージングされていない時にフォント色表す

# 現在のスタイル紹介
まずは現在のスタイルをどうぞ。
![スクリーンショット 2020-12-04 20.07.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/0b8dbd59-0767-5142-242f-e5c05f168c83.png)

ﾌﾂｸｼｲ｡｡｡

# こだわりポイント
エンジニア好みなカッコE系のプロンプトではなく、機能性重視で派手すぎないプロンプトです。
実はこの画像内には下記のこだわりが存在します。

1. <font color="silver">(`cd`コマンドで自動`ls -a` ただし`~`を除く) <- プロンプトじゃないから今回は説明なし</font>
- <font color="silver">(半透明ウィンドウ、非アクティブ時にはさらに薄い半透明に) <- プロンプトじゃないから今回は説明なし</font>
- left_prompt
   1. カレントが短いパスの時
   - カレントが長いパスの時
   - sshしてる(されてる)時
- right_prompt
   1. 通常時
   - gitディレクトリ時
      1. クリーン時
         1. 全てpush済みの時
         - 未pushありの時
      - 内容変更あり&ステージングしてない時
         1. 既存ファイルの更新のみ
         - 新規、削除、リネームファイルがある時
      - 内容変更あり&ステージングファイルありの時
         1. 既存ファイルの更新のみ
         - 新規、削除、リネームファイルがある時
      - コンフリクト時

# left_prompt
`left_prompt`とは、世間一般的に言われている(?)プロンプト部分、コマンドより左側に自動的に表示される部分です。つまりここのことです。
![スクリーンショット 2020-12-04 20.07.49 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/501bf9d3-f561-ceda-be8c-12ce36544c23.png)

macの場合、デフォルトでユーザ名、ホスト名、カレントディレクトリが表示されています。
![スクリーンショット 2020-12-04 20.30.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/1cfe8835-e31b-6700-eb8f-7025fce4e2b3.png)

しかしこれには問題があります。

1. 大量の出力があったときに、どのコマンドに対する出力なのかを追うのが大変
2. カレントだけではどんなディレクトリ構成かわからない
3. ユーザ名、ホスト名が邪魔
4. 味気ない

## 大量の出力があったときに、どのコマンドに対する出力なのかを追うのが大変
多くの出力文字は白、自分が入力したコマンドも白。視覚的変化がないのでぱっと見ではどれが自分のコマンドなのかを見つけにくいです。
でもコマンドに色はつけたくない(宗教上の理由です)
となるとプロンプトに色をつければいい！！

でも見やすさと派手すぎないこともまた重要です。
簡単につけられる色は次の8色です。
どれがいいか・・・？

(※プロンプト上の色とブラウザ上の色は異なる場合があります)

|色の名前|プロンプト設定上の数字|
|:--|:--|
|<font color="black">black</font>|0|
|<font color="red">red</font>|1|
|<font color="green">green</font>|2|
|<font color="yellow">yellow</font>|3|
|<font color="blue">blue</font>|4|
|<font color="magenta">magenta</font>|5|
|<font color="cyan">cyan</font>|6|
|<font color="white">white</font>(white)|7|

プロンプトの色の付け方は、`~/.zshrc`ファイルに次のような記述をすることで適用できます。

```sh
PROMPT="%F{[色の名前]}[プロンプト部分]%{$reset_color}"

PROMPT="%F{green}%~%{$reset_color}" # <- プロンプトを緑で表示する例

# %{$reset_color}を最後につけることで、標準入力をデフォルトの色に戻す
```

`~/.zshrc`を変更後は次のコマンドで反映させます。

```sh
source ~/.zshrc
```

全部試した結果、黒ウィンドウには緑が適切と感じられたため緑色で表示しています。
プロンプトに色がつくと、ザーッとスクロールして眺めてる時に自分がコマンドを打った場所がぱっと見でわかるようになります。これは便利！！

そんな感じでプロンプトの色が緑です。
ちなみに同じ理由で通常時のright_promptにも色がついています。（後述）

## カレントだけではどんなディレクトリ構成かわからない
フルパス表示すりゃええやん、ということでそうしてみました。
![スクリーンショット 2020-12-04 20.54.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/23d2fc21-9546-4853-d234-dab80b3b63da.png)

#### UUUUUUUGLY!!!
プロンプトが2行になるなんて論外です。
そりゃウィンドウを小さくして無理矢理異常感を出してはいますが、実際なが〜いパスの時だってあります。

ではどうするか？

ここは実用性よりも見た目重視にしたいです（宗教上の理由です）
もう一度完成形のプロンプトを見てみましょう。
![スクリーンショット 2020-12-04 20.07.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/0b8dbd59-0767-5142-242f-e5c05f168c83.png)

そう、長すぎるパスの時は`...`でパスを省略しています。
仕組みは単純で、表示文字数がウィンドウサイズの50%を超える場合は50%になるよう省略しています。
ご覧ください、この素晴らしき省略っぷりを！！

全表示できる時
![スクリーンショット 2020-12-04 21.02.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/0d821eb3-b50d-86b7-978f-6c2b0f4dbeec.png)
一部省略時
![スクリーンショット 2020-12-04 21.02.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/6131dc8a-5c8b-bd2a-25e3-2cf1c845a31c.png)
もうちょっと省略時　right_promptに被るときは自動的に消えるので入力スペースが生まれます。
![スクリーンショット 2020-12-04 21.03.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/bb680bfc-33b3-2bd3-512c-265c596a45c6.png)
最小表示時　どんなに小さい時でもカレントディレクトリは表示させます
![スクリーンショット 2020-12-04 21.03.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/2266610c-a49c-5b5c-a402-c04086bb5a61.png)

プロンプトの省略方法は次の通りです。

```sh
PROMPT="%[省略するプロンプトの最大表示文字数]>[プロンプト省略後に表示するプロンプト]>[省略表示するプロンプト]%<<"

PROMPT="%10>.../>%~%<<" # <- カレントディレクトリの初め10文字を表示し、「...」で終わる例

# たとえ省略しなくても[プロンプト省略後に表示するプロンプト]は表示されます。
```

これで見た目性能と実用性とを両立できましたね！

## ユーザ名、ホスト名が邪魔
割とそのまんまなので消せばOKです。

・・・いやいや、表示して欲しい時もあるんですよ。

複数ユーザを跨いで使いたい時やホストを跨いで使うときは、今どれを制御しているのかを一目で確認したいです。
身近なところで言うなら、sshするときは表示して欲しいですよね。
なので私はssh接続時には`ユーザ名@ホスト名:パス`と言う表示にしています。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3336303435342f30623864626435392d303736372d353134322d323432662d6535633035663136386338332e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/1008cd5e-bdc8-d2bd-68e9-be51d2b46bf7.png)

もちろん長いパスの時には省略させますし、ユーザ名やホスト名も考慮してウィンドウサイズの50%までの表示です。

ちなみにですが、ユーザ名とホスト名はマゼンタで表示しています。
これはいつもとは違うところを制御していると言うことがすぐにわかるようにするためです。
あれこれやった後に「あーーーこれローカルじゃん。。。リモート側を操作したかったのに。。。」なんてことがないようにしています。

さらにこの表記は、scpの時のパスに対応しています。

```
user@host:path
```
別のホストから今SSHしてるカレントにファイルをコピーしたいと言う時に、left_promptのコピペで対応できます！
なんて便利なんでしょう！

ssh接続かどうかの分岐は次のようにできます。

```sh
if [ -n "$SSH_CONNECTION" ]; then
  # ssh接続中の処理
else
  # sshではない時の処理
fi
```

#### 味気ない
ここまでやれば味気なさはもうないですよ(^^)

# right_prompt
さあ続いてright_promptです。
right_promptは画面右端に表示されているもので、コマンドやleft_promptと重なる時には自動的に全てのright_promptが非表示になります。

今度はright_promptに注目して完成形のプロンプトを見てみましょう。(3度目)
![スクリーンショット 2020-12-04 20.07.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360454/0b8dbd59-0767-5142-242f-e5c05f168c83.png)

時刻の時と、何やらカラフルな色でmasterが表示されていますね。
そう、git管理ディレクトリではgitのブランチ名、それ以外のディレクトリでは時刻を表示しています。

時刻の表示は、コマンド実行時に再描画させているため、例えばプロンプト表示時に`[12/24 12:30:11]`だったとしても、コマンドを実行すると`[12/24 12:30:33]`という風に変化します。
要は、プロンプト表示時ではなく、コマンドの実行日時を表しています。
複数のウィンドウで作業しているときに、どの順番でコマンドを実行したのか追えるようになります。
最初はgitディレクトリ以外で右側が空くので、そのスペースの有効活用として導入したのですが、意外とあるといいものです。

## カラフルなgitブランチ名
gitディレクトリの時は、状況に応じて色が異なるようにしています。
優先度は高いものとしています。

|状況|表示内容|
|:--|:--|
|rebase時にコンフリクトが起きてbranchが取得できない時|赤色フォント下線付きで<font color="red"><u>CONFLICT</u></font>と表示|
|コンフリクトが起きている時、または自動マージできない時にステージングされているファイルがある時|背景が<font color="red">赤<font>|
|コンフリクトが起きている時、または自動マージできない時にステージングされているファイルがない時|フォントが<font color="red">赤<font>|
|新規、削除またはリネームされたファイルがステージングされている時|背景が<font color="magenta">マゼンタ</font>|
|更新されたファイルがステージングされている時|背景が<font color="yellow">黄色</font>|
|削除またはトラックされていないファイルがある時|フォントが<font color="magenta">マゼンタ</font>|
|更新されたファイルがある時|フォントが<font color="yellow">黄色</font>|
|クリーンでpushしていないコミットがある時|背景が<font color="green">緑</font>|
|クリーンでpushしていないコミットがない時|フォントが<font color="green">緑</font>|
|いずれにも当てはまらない時|フォントが<font color="white">白</font>(白)|


## 何が嬉しいのか？
まずブランチ名がわかることで、作業ブランチが正しいかどうかを瞬時に判断できます。
また、`pull`や`push`するときにブランチ名を指定する必要があるとき、コピペできます。

色がついていることのメリットは現在の状況を、プロンプトの邪魔をせずに可視化できることです。（前述の通り、実行した履歴を辿るのにも役立ちます）

例えば、色分けしていない時にブランチを変更したい時

```sh
% git branch
branchA <- current branch
branchB
branchC
% git checkout branchB
error: Your local changes to the following files would be overwritten by checkout...
% git checkout . # git stash
% git checkout branchB
```

カレントでちょっとした変更点があるせいで、一旦変更を取り消して(またはstashして)からもう一度ブランチを変更しています。
これが色分けされている時は、今変更があると言うのがすぐにわかるので、怒られる前に手を打てます。

また、作業中にステージングされているファイルがあるか、そのファイルは影響が大きい(新規・削除・リネームされている)かどうか、プッシュしてあるかも色で瞬時に判断できます。

### つまり、ちょっとした工数の削減やミス防止に貢献します。

# プロンプトのスクリプト
プロンプトを実装しているスクリプトを公開します。
無理やり実装してメンテしてないのでひどい書き方があります。
もしかしたらバグもあるかもしれませんが、参考までにどうぞ。

下記内容を`~/.zshrc`に追加して、`source ~/.zshrc`すると適用されるはずです。

```sh
function left_prompt() {
    fullpath=`pwd`
	echo "$fullpath" | grep $(echo "$HOME") > /dev/null
	if [ $? ] ; then
		fullpath_length=`expr ${(m)#fullpath} - ${(m)#HOME} + 1`
	else
		fullpath_length=`expr ${(m)#fullpath}`
	fi
	host_length=0
   	if [ -n "$SSH_CONNECTION" ]; then
		host_length=`expr ${(m)#USER} + ${(m)#HOST} - 4` # ホスト名には.localがつくため
	fi
	total_length=`expr $fullpath_length + $host_length`
    limit_length=`echo "($COLUMNS) / 2" | bc`
	base_color=`is_m1_X86_64 && echo yellow || echo green`
	# 文字数が多すぎるとき
    if [ $total_length -gt $limit_length ]; then
	    current=`basename "\`pwd\`"`
	    current_length=${(m)#current}
	    final_length=`expr $limit_length - $current_length - $host_length`
		if [ $final_length -lt 6 ]; then
			final_length=4
		fi
   		if [ -n "$SSH_CONNECTION" ]; then
       		PROMPT="%{$fg[magenta]%}%n@%m%{$reset_color%}:%{$fg_bold[$base_color]%}%$final_length>.../>%~%<<%c%{$reset_color%} %# "
		else
	    	PROMPT="%{$fg_bold[$base_color]%}%$final_length>.../>%~%<<%c%{$reset_color%} %# "
		fi
	# 全て表示できる時
    else
   		if [ -n "$SSH_CONNECTION" ]; then
       		PROMPT="%{$fg[magenta]%}%n@%m%{$reset_color%}:%{$fg_bold[$base_color]%}%~%{$reset_color%} %# "
		else
	    	PROMPT="%{$fg_bold[$base_color]%}%~%{$reset_color%} %# "
		fi
    fi
}

function right_prompt {
  local branch_name st branch_status

  if git rev-parse 2>/dev/null; then
    branch_name=`git rev-parse --abbrev-ref HEAD 2> /dev/null`
    st=`git status 2> /dev/null`
    # コンフリクトが起こった状態
    if [[ -n `echo "$st" | grep "^rebase in progress"` ]]; then
      branch_status="%U%F{red}"
      branch_name="CONFLICT"
    # 自動マージできないファイルがある状態
    elif [[ -n `echo "$st" | grep "^Unmerged paths"` ]]; then
      if [[ -n `echo "$st" | grep "Changes to be committed:"` ]]; then
        branch_status="%K{red}"
      else
        branch_status="%F{red}"
      fi
    # ステージングファイルがある場合
    elif [[ -n `echo "$st" | grep "Changes to be committed:"` ]]; then
      # 新規ファイル、リネーム、削除ファイルがある場合はマゼンタ塗りつぶし
      # ステージング部分の文字列のみ抽出し、そこに特定の文字が含まれているかどうか
      if [[ -n `echo "$st" | awk '/Changes to be committed:/,/(Changes not staged for commit:|Untracked files:)/' | grep -e "new file:" -e "deleted:" -e "renamed:"` ]]; then
        branch_status="%K{magenta}"
      else
        branch_status="%K{yellow}"
      fi
    # 変更はあるがステージングされていないファイルがある場合
    # ステージングされているファイルがないので、文字列の抽出は必要ない
    # elif [[ -n `echo "$st" | grep "Changes not staged for commit:"` ]]; then
    # 新規ファイル、リネーム、削除ファイルがある場合はマゼンタ
    elif [[ -n `echo "$st" | grep -e "Untracked files:" -e "deleted:"` ]]; then
      branch_status="%F{magenta}"
    elif [[ -n `echo "$st" | grep "modified"` ]]; then
      branch_status="%F{yellow}"
    # 全てcommitされてクリーンな状態
    elif [[ -n `echo "$st" | grep "^nothing to"` ]]; then
      # pushされていなければ塗りつぶし
      if [[ -n `git log origin/"$branch_name".."$branch_name"` ]]; then
        branch_status="%K{green}"
      else
        branch_status="%F{green}"
      fi
    # 上記以外の状態の場合
    else
      branch_status=""
    fi
    # ブランチ名を色付きで表示する
    RPROMPT="${branch_status}[$branch_name]%{$reset_color%}"
  else
    # git 管理されていないディレクトリは何も返さない
	RPROMPT="%{$fg[green]%}[%D{%m/%d} %*]%{$reset_color%}"
  fi
}

autoload -Uz add-zsh-hook
PERIOD=1 # gitディレクトリでのright_promptは描画にやや負荷がかかるため1秒以内はキャッシュしたものを使う
add-zsh-hook periodic right_prompt
add-zsh-hook precmd left_prompt

# コマンド実行時プロンプト再描画
re-prompt() {
	zle .accept-line
	zle .reset-prompt
}
zle -N accept-line re-prompt
```

こうしたほうが良くない？
とか
俺の方が便利だぜ！
みたいなのがあればぜひ教えてください！

# 最後に
プロンプトは個性が出るところです。
かっこいいのもいいですし、機能的なのもいいです。
ぜひ、自分好みのプロンプトを作り上げてください！