Zshのコマンドプロンプトを七色に輝かせるまで🌈

## 概要

あなたのキーボードは光りますか？
もちろん光りますよね。その方がかっこいいからです。

ではあなたのコマンドプロンプトは光りますか？
もし光らないのであれば、この記事を読んで光らせてみませんか？

以下では、[chameleon](https://github.com/kitta65/chameleon)という七色に輝くコマンドプロンプトを開発するまでの道のりをまとめていきます。

[![asciicast](https://asciinema.org/a/589480.svg)](https://asciinema.org/a/589480)

## 対象読者

* コマンドプロンプトの自作に興味があって、手ごろな実装例を探している方
* コマンドプロンプトを輝かせたい方

## 開発までの道のり
### 最小構成

ボトムアップに作り始めましょう。コマンドプロンプトを設定するために必要なファイルは、prompt_chameleon_setupというファイルのみです（`chameleon`の部分は任意の名前だから適宜読み替えてください）。実際はchameleon.zshのようなファイルを用意してシンボリックリンクを張ることが多いようです[^sym]。

[^sym]: 例えば[pure](https://github.com/sindresorhus/pure)

```sh:chameleon.zsh
prompt_chameleon_setup() {
  typeset -g prompt_chameleon_color='#FF0000' # 赤！
  PROMPT='%F{$prompt_chameleon_color}❯ %f' # プロンプトの文字や色の指定
  prompt_opts=(percent subst) # これがないと上記のPROMPTが展開されず文字通り解釈される
}
prompt_chameleon_setup "$@"
```

シンボリックリンクも作成すると、この[commit](https://github.com/kitta65/chameleon/tree/2ebae5478081148ccc5e5b3dfc8418696520afb9)の状態になります。このディレクトリで以下を実行すると、chameleonが有効化され赤い`❯`のプロンプトが表示されます。ひとまず動きました。

```sh
fpath+=($(pwd))
autoload -U promptinit; promptinit
prompt chameleon
```

### 再描画

さて、色を徐々に変えるにはどうしたらよいでしょうか？コマンドを実行する度に色を変えるなら、precmdというhookを使うことができます。ただし今回はコマンド入力中も徐々に色を変えたいから不採用です。

https://qiita.com/mollifier/items/558712f1a93ee07e22e2

TMOUTを利用するハックなら、この条件をクリアできます。ただ、更新間隔を1秒より短くできないという問題があり微妙です[^tmout]。

[^tmout]: chameleonでは、この方法をフォールバックとして採用しています。

https://vorfee.hatenablog.jp/entry/2017/02/03/031251

最終的に今回の要件に合致したのは、[zsh-async](https://github.com/mafredri/zsh-async)というライブラリでした。.zshrcに以下を追記すると、まだの場合にインストールしてくれます。

```sh:.zshrc
if ! [[ -d ~/.zsh/zsh-async ]]; then
  git clone https://github.com/mafredri/zsh-async.git "$HOME/.zsh/zsh-async"
fi
source $HOME/.zsh/zsh-async/async.zsh
```

`async_init` `async_start_worker` `async_register_callback` `async_job`などが使えるようになるので、先ほどのスクリプトに追記をしましょう。これで0.5秒おきにプロンプトが再描画されるようになります。

```sh:chameleon.zsh
prompt_chameleon_gradation() {
  prompt_chameleon_color='#000000' # 黒！
}

prompt_chameleon_refresh() {
  prompt_chameleon_gradation
  zle reset-prompt # 再描画
  async_job chameleon sleep $prompt_chameleon_interval # 0.5秒待つだけのjobを再度積む
}

prompt_chameleon_setup() {
  typeset -g prompt_chameleon_color='#FF0000'
  PROMPT='%F{$prompt_chameleon_color}❯ %f'
  prompt_opts=(percent subst)

  typeset -g prompt_chameleon_interval=0.5
  async_init # 初期化
  async_start_worker chameleon # cameleonというworkerを用意
  async_register_callback chameleon prompt_chameleon_refresh # job実行の度にprompt_chameleon_refresh
  async_job chameleon sleep $prompt_chameleon_interval # 0.5秒待つだけのjobを積む
}
prompt_chameleon_setup "$@"
```

ここまで作業するとこの[commit](https://github.com/kitta65/chameleon/tree/456497eb8511f0fc356be144747cfdffa0ca21df)の状態になります。先ほどと同様の方法でcameleonを有効化すると一瞬赤いプロンプトが表示されてすぐに黒くなることが確認できると思います。

### グラデーション
先ほどは赤いプロンプトを黒く変えるだけでしたが、本来の目的は七色に輝かせることでした。そのためのprompt_chameleon_gradationは以下です。先ほどの実装を置き換えてください。

2023/06/06 @ko1nksm さんのコメントを参考に修正

```sh:chameleon.zsh
prompt_chameleon_gradation() {
  local temp=$prompt_chameleon_color

  local -A rgb
  rgb=(
    r 0x${temp:1:2}
    g 0x${temp:3:2}
    b 0x${temp:5:2}
  )

  local increment=30
  if (( rgb[r] == 255 )) && (( rgb[g] < 255 )) && (( rgb[b] == 0 )); then
    # red -> yellow
    rgb[g]=$((rgb[g] + increment))
    rgb[g]=$((rgb[g] < 255 ? rgb[g] : 255))
  elif (( rgb[r] > 0 )) && (( rgb[g] == 255 )) && (( rgb[b] == 0 )); then
    # yellow -> green
    rgb[r]=$((rgb[r] - increment))
    rgb[r]=$((rgb[r] < 0 ? 0 : rgb[r]))
  elif (( rgb[r] == 0 )) && (( rgb[g] == 255 )) && (( rgb[b] < 255 )); then
    # yellow -> aqua
    rgb[b]=$((rgb[b] + increment))
    rgb[b]=$((rgb[b] < 255 ? rgb[b] : 255))
  elif (( rgb[r] == 0 )) && (( rgb[g] > 0 )) && (( rgb[b] == 255 )); then
    # aqua -> blue
    rgb[g]=$((rgb[g] - increment))
    rgb[g]=$((rgb[g] < 0 ? 0 : rgb[g]))
  elif (( rgb[r] < 255 )) && (( rgb[g] == 0 )) && (( rgb[b] == 255 )); then
    # blue -> purple
    rgb[r]=$((rgb[r] + increment))
    rgb[r]=$((rgb[r] < 255 ? rgb[r] : 255))
  elif (( rgb[r] == 255 )) && (( rgb[g] == 0 )) && (( rgb[b] > 0 )); then
    # purple -> red
    rgb[b]=$((rgb[b] - increment))
    rgb[b]=$((rgb[b] < 0 ? 0 : rgb[b]))
  fi

  for key in ${(k)rgb}; do
    printf -v "rgb[$key]" "%02x" $rgb[$key]
  done
  prompt_chameleon_color='#'${rgb[r]}${rgb[g]}${rgb[b]}
}
```

この状態のcommitが[これ](https://github.com/kitta65/chameleon/tree/b1ca9d87ec06d3048b7e348f5e6caf7629553cdd)です。ようやく七色に輝きました。

### 仕上げ

あとは何かエラーが生じた場合にworkerを再起動する設定を足したり、zsh-asyncが未インストールの場合の対応を追記したら完成です。これについては[mainブランチ](https://github.com/kitta65/chameleon/tree/main)の実装を見てください。

## 最後に

ちゃんと実用的にするならカレントディレクトリやGitの情報も表示するべきですが、今回は自由研究程度のつもりだったのでそこまではやりませんでした。とはいえせっかく実装したので、興味があればREADMEを見てインストールして遊んでみてください。

https://github.com/kitta65/chameleon

いつか時間があるときに、最強の輝くコマンドプロンプトを作ってみたいものですね。