23
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-14

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

定義区分について

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

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

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

有彩色ゾーンについて

まず以下の連立方程式を満たす、色インデックス( $i$ )に対応する三原色の色レベルを算出します。

\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
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
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, Zsh, 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では次のようなコマンドを基本形として任意の24bitカラーでパレットを上書きしています2

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

見れば分かる通りこのコマンド、24bitのカラーコードが用意されていることが前提となっていますね。
現在自作しているfish用の配色設定用プラグイン(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つしかなかったのです。

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

おわりに

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

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

参考(にしたもの、なったもの)


  1. 端末によってデフォルトの色定義が異なる、という理由はあるがそもそも今回の目的がこの領域の上書きである。 

  2. Emacs内蔵のtermやkonsoleでは使えなかった。TmuxやScreenは要加工ながらも利用可能。tty端末では\e]P%d%02xと違う形になっているが同様のことは可能。 

23
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
16