ターミナルの256色について、改めて確認しましょう

訳あって(後述)、シェル上でターミナルの256色を24bitのRGBコードへ変換する必要が出てきました。
ここではその手法をメインに、ターミナルの256色について調べたことを書いてみました。

## 定義区分について
これら256色は全体で統一された規則によって定義されているわけではなく、3つの異なる定義方法で分けられる領域があるようです。

- システム用パレット(0 ~ 15)
- 有彩色(16 ~ 231)
- 無彩色(232 ~ 255)

基本の16色パレットは今回置いておくとして[^1]、問題なのは16から231までの「有彩色ゾーン」、232から255までの「無彩色ゾーン」。
こいつらの変換方法を確認しておきましょう。

## 有彩色ゾーンについて
まず**以下の連立方程式を満たす、色インデックス(&thinsp;$i$&thinsp;)に対応する三原色の色レベルを算出します。**

```math
\begin{eqnarray}
  \left\{
    \begin{array}{l}
      {6^2}r + 6g + b = i - 16 \\
      0 \leqq r \lt 6 \\
      0 \leqq g \lt 6 \\
      0 \leqq b \lt 6
    \end{array}
  \right.
\end{eqnarray}
```

これにより、0以上6未満の3値 $r, g, b$ が求められます。~~3値同値なら有彩色ゾーンなのに無彩色が入ってしまうのはここだけの秘密。~~

次に**各レベルを色の強さ(以降 明度値)に変換します。**
レベル0のときは(当然ながら)値は0。
そこから比例的に加算されると思いきや、レベル1で一気に**95(0x5F)**が加算され、以降レベルが1段階上昇するに従い**40(0x28)**が追加されます。
レベル上限は5であることを踏まえて表にするとこのようになります。

| レベル | コード | 明度(%) |
|---|---|---|
| 0 | 0x00 |   0.0 |
| 1 | 0x5F |  37.3 |
| 2 | 0x87 |  52.9 |
| 3 | 0xAF |  68.6 |
| 4 | 0xD7 |  84.3 |
| 5 | 0xFF | 100.0 |

あとは各明度値をくっつけておなじみ24bitのカラーコードにするだけです。

以上の流れをfishスクリプトにするとこんな感じ。

```fish:Fish
function index2levels4chroma -d "色インデックスを空白区切りにした3つのレベル値に変換"
    set -l index $argv[1] # ローカル変数の設定

    # 赤レベルの算出
    set -l red_level (math "$index / (6 ^ 2)")
    test $red_level -ge 6 # 6以上のとき
        and set red_level 5
    
    # 緑レベルの算出
    set -l green_level (math "($index - (6 ^ 2) * $red_level) / 6")
    test $grenn_level -ge 6
        and set green_level 5

    # 青レベルの算出
    set -l blue_level (math "$index - (6 ^ 2) * $red_level - 6 * $green_level")

    printf '%d %d %d' $red_level $green_level $blue_level
end

function level2hex4chroma -d "レベル値を16進数の明度値のコードに変換"
    switch $argv[1]
        case 0
            printf 00
        case 1 2 3 4 5
            printf '%02x' (math "95 + 40 * ($argv[1] - 1)")
    end
end

function index2code4chroma -d "色インデックスを24bitコードへ変換(有彩色用)"
    set -l levels (index2levels4chroma $argv[1])
    set -l list (string split ' ' "$levels")
    printf '#%s%s%s' \
        (level2hex4chroma $list[1]) \
        (level2hex4chroma $list[2]) \
        (level2hex4chroma $list[3])
end
```

## 無彩色ゾーンについて
無彩色ゾーンの求め方も前述のものに似ています。

1. インデックスから232を減算し、明度レベル( $l$ )を得る
2. $l_{dec} = 10 l + 8$ を満たす10進数の明度値( $l_{dec}$ )を求める
3. 16進数に変換、3つ並べて24bitコードにする

```fish:Fish
function index2code4monochroma -d "色インデックスを24bitコードへ変換(無彩色用)"
    set -l level (math "$argv[1] - 232")
    set -l dec   (math "10 * $level + 8")
    printf '#%02x%02x%02x' $dec $dec $dec
end
```

ちなみに有彩色ゾーンに出現する無彩色と組み合わせた、256色ターミナルで表現できる無彩色の一覧がこちら。

| インデックス | レベル(有彩) | レベル(無彩) | カラーコード | 明度(%) |
|---|---|---|---|---|
|  16 | 0 |    | #000000 |   0.0 |
| 232 |   |  0 | #080808 |   3.1 |
| 233 |   |  1 | #121212 |   7.1 |
| 234 |   |  2 | #1C1C1C |  11.0 |
| 235 |   |  3 | #262626 |  14.9 |
| 236 |   |  4 | #303030 |  18.8 |
| 237 |   |  5 | #3A3A3A |  22.7 |
| 238 |   |  6 | #444444 |  26.7 |
| 239 |   |  7 | #4E4E4E |  30.6 |
| 240 |   |  8 | #585858 |  34.5 |
|  43 | 1 |    | #5F5F5F |  37.3 |
| 241 |   |  9 | #626262 |  38.4 |
| 242 |   | 10 | #6C6C6C |  42.4 |
| 243 |   | 11 | #767676 |  46.3 |
| 244 |   | 12 | #808080 |  50.2 |
|  86 | 2 |    | #878787 |  52.9 |
| 245 |   | 13 | #8A8A8A |  54.1 |
| 246 |   | 14 | #949494 |  58.0 |
| 247 |   | 15 | #9E9E9E |  62.0 |
| 248 |   | 16 | #A8A8A8 |  65.9 |
| 129 | 3 |    | #AFAFAF |  68.6 |
| 249 |   | 17 | #B2B2B2 |  69.8 |
| 250 |   | 18 | #BCBCBC |  73.7 |
| 251 |   | 19 | #C6C6C6 |  77.6 |
| 252 |   | 20 | #D0D0D0 |  81.6 |
| 172 | 4 |    | #D7D7D7 |  84.3 |
| 253 |   | 21 | #DADADA |  85.5 |
| 254 |   | 22 | #E4E4E4 |  89.4 |
| 255 |   | 23 | #EEEEEE |  93.3 |
| 231 | 5 |    | #FFFFFF | 100.0 |

インデックス43, 86, 129, 172の中途半端感が凄まじい。

## なぜこれが必要だったのか
さて、ところで皆さん、**ターミナルのカラーパレットをprintfで上書きできる**って知ってました？

皆さんご存知の色変更シーケンスはこのような感じだと思います。

```bash:Bash,&nbsp;Zsh,&nbsp;Fish
# 文字色変更 基本の256色インデックス使用版
printf '\033[38;5;%dm%s\033[0m' $index_256 $string

# 文字色変更 RGB使用版らしい
printf '\033[38;2;%02x%02x%02xm%s\033[0m' $r $g $b $string
```

対してあまり語られないですが、その対話環境に限り永続的に色変更できるシーケンスも存在します。
[base16-shell](https://github.com/chriskempson/base16-shell)では次のようなコマンドを基本形として任意の24bitカラーでパレットを上書きしています[^2]。


```bash:Bash,&nbsp;Zsh,&nbsp;Fish
# $index: パレットのインデックス(例: 暗めの緑 → 3)
printf '\033]4;%d;rgb:%02x/%02x/%02x\033\\' $index $r $g $b
```

見れば分かる通りこのコマンド、**24bitのカラーコードが用意されていることが前提**となっていますね。
現在自作しているfish用の配色設定用プラグイン([onedark-fish](https://github.com/rkbk60/onedark-fish))ではこれをほぼそのまま用いているのですが、ここである問題が発生しました。

この拙作、設定可能な配色をonedarkに限定しながらもコマンドライン用配色とターミナルパレットをまとめて1コマンドでいい感じに統一させるというもので、開発初期はオリジナルonedarkで定義されている24bitカラーのみ用いていました。基にしているbase16-shellのコードを用いればLinuxのtty端末ですら24bitカラーが使えるという有能ぶりを見て、当初は256カラーなんて必要ないと思っていたんです。
しかし様々な端末で試験を重ねているうちに、**ターミナル上で開かれているVimから内蔵ターミナルを呼び出したとき**、コーディング画面との間で配色が若干崩れているのを発見したのです。

理由は簡単。ターミナルVimは配色指定に256色のインデックスを使っているから。

この問題を解決してVimでも統一性ある色味を実現する方法としては、

1. インデックスがそのまま使えるエスケープシーケンスを利用
2. インデックスを24bitRGBコードに変換して利用する

の2案が挙がりましたが、前述の通りターミナルパレット上書きの処理は24bitカラーのみ利用可能という制限があります。したがって案1は使えません。
結果、この問題の解決策は案2の256色インデックスを24bitカラーコードに変換すること。ただ1つしかなかったのです。

今回の調査のおかげで何とか上記問題は解決できました。

## おわりに
当初は上記自作プラグインの宣伝記事を書こうと思っていたのですが、気付いたら頭に変な解説がくっついてしまいました。それだけ省略されがちだったり知ってて当たり前なこと(?)だったり、なにより必要のない情報なのだと思います。
とりあえず(少なくとも個人で調べた限りでは)数少ない情報の備忘録として、ストックでもしてくれると嬉しいです。

ついでに[この子](https://github.com/rkbk60/onedark-fish)も「カラースキーム定義」というfish界ではなかなかないジャンルのプラグインなので、fish(fisherman)使いでonedarkジャンキーの方はぜひお試しくださいませ(ノルマ達成)。

## 参考(にしたもの、なったもの)
- ANSIエスケープシーケンス チートシート  
https://qiita.com/PruneMazui/items/8a023347772620025ad6
- base16-shell  
https://github.com/chriskempson/base16-shell
- 256 Colors - Cheat Sheet - Xterm, HEX, RGB, HSL  
http://jonasjacek.github.io/colors/
- 256color.pl  
https://gist.github.com/hSATAC/1095100
- ansi-256-colors  
https://github.com/jbnicolai/ansi-256-colors
- ANSI escpe code - Wikipedia  
https://en.wikipedia.org/wiki/ANSI_escape_code#Colors

[^1]: 端末によってデフォルトの色定義が異なる、という理由はあるがそもそも今回の目的がこの領域の上書きである。
[^2]: Emacs内蔵のtermやkonsoleでは使えなかった。TmuxやScreenは要加工ながらも利用可能。tty端末では`\e]P%d%02x`と違う形になっているが同様のことは可能。