Vimと端末背景色事情

はじめに
-------
この記事は [Vim Advent Calendar 2012](http://atnd.org/events/33746) 69日目の記事です。 昨日の記事は@scheakurさんの 「[Just Press Dot Key](http://blog.scheakur.com/post/42361658996/just-press-dot-key)」でした。

以前端末背景色の判定について軽く触れましたが、あまり詳しく書けていなかったので補足します。

> *背景色の判定はemacsやMinEdでも行なわれているので、vimでもtermresponseフューチャーを有効化してコンパイルされたものは、これをおこなってよいだろうと考えています。
具体的には起動時のtermresponseフェーズにおいて、xtermのパッチレベル判定を行った後、端末に対してデフォルト前景色・背景色の報告を要求する特殊なクエリを投げ、応答を非同期で受け取りbackgroundオプション値自動設定の参考にするというものです。
背景色の判定が可能な端末は、xterm、rxvt-unicode、TeraTerm、RLogin、MinTTY、mltermなどです。gnome-terminal、rxvt、Poderosa、iTerm2などは、実はあと一歩というところまで実装されているので、対応を入れるのは楽そうだと考えています。*
[vimと端末エミュレーション](http://qiita.com/items/415a30930a80b9b42adb)

今になって書いた本人が読んでみても何を言っているのかさっぱりです。だいぶテンパってますね。
今回はもう少し具体的に書いてみます。

背景色取得のしくみ
---------------

近年のXTerm相当のエミュレーションをする互換端末においては以下のクエリ：

```
ESC ] 1 1 ; ? ESC \
```

に対して、下記形式の応答が返ることが期待されています。

```
ESC ] 1 1 ; r g b : rrrr / gggg / bbbb ESC \
```

ここでrrrr、gggg、bbbbの組は、16進ディジットで表現されたRGB値です。

確認方法
-------

自分の使っている端末がこれに対応しているかは、シェルから

```
$ printf '\033]11;?\033\\'
```

と打って反応をみるとよいです。
ちなみにmltermにおいては

```
$ printf "\033]5380;%d;bg_color\033\\" $(cat $HOME/.mlterm/challenge)
```

などとしても端末背景色を得ることができます。


Vim以外での使用例
---------------

この背景色取得シーケンスを実際に使用しているアプリケーションとしては、[GNU Emacs](http://www.gnu.org/software/emacs/)や[MinEd](http://towo.net/mined/)が挙げられます。
両者とも必ず出しているというわけではなく、端末特性応答などと絡めた複雑な条件でこの応答をつかうかどうかを決めています。

![osc11](http://zuse.jp/misc/osc11.png)

Xterm(ただしパッチレベル242以上279以下)上でのEmacs24.2や、mlterm(3.1.5以上)上でのMinEd2011.22、といった組み合わせでは、起動時にこの背景色の問い合わせ&応答のやりとりをばっちり観察することができます。
興味があれば[Trachet](http://saitoha.github.com/trachet/)を利用して確認してみましょう。Trachetのチュートリアルは[こちら](http://qiita.com/items/3fcadb4633863e25e12d)。


backgroundオプションを動的に設定する
--------------------------------

さて、Vimにおいてはこの素敵なフィーチャーをどのように活かしていけばよいか。まず思いついたのは、backgroundオプション値(light/dark)を動的に決定するための材料とする、という用途です。
これは@yoshikawさんの記事「[Vimの背景色を環境変数で設定する](http://yskwkzhr.blogspot.jp/2012/12/set-background-color-of-vim-with-environment-variable.html)」でも触れられているとおり、Vim本体側でも$TERMや$COLORFGBGを見るなどおおざっぱな方法ですでにやっています。
しかしおそらくこのあたりは、ユーザーが自由なアルゴリズムにもとづいてVim scriptでカスタマイズしたくなる部分ではないでしょうか。

そこで.vimrcに追記する形での設定例の叩き台を書いてみました。

想定する端末はxterm、rxvt-unicode、TeraTerm、RLogin、MinTTY、mlterm(3.1.5以上)あたりです。
screen/tmuxでも親端末がこの機能を実装していれば動作すると思います。
不具合など出たら教えてください。
まだまだ対応端末が少ないのがネックですね。なんとかしていきたいです。

コードをこまぎれにして参照しながら本文で説明すると見にくくなるので、なにやってるかはコメントでだいたいわかるようにしています。

```.vimrc
"
" This snippet is licensed under NYSL.
" See http://www.kmonos.net/nysl/NYSL.TXT
"
if !has('gui_running')
  " 以下の条件を満たさない場合vim本体がbackgroundを上書きするので自前での判定はやらない
  " http://yskwkzhr.blogspot.jp/2012/12/set-background-color-of-vim-with-environment-variable.html
  if $TERM !~ 'linux\|screen.linux\|cygwin\|putty' && $COLORFGBG == ''

    " 背景色を問い合わせるクエリ文字列を設定
    " screen/tmuxではパススルーシーケンスを使用して親端末に問い合わせる
    if $TMUX != ""
      " tmuxを貫通させる
      let s:background_teststr = "\eP\e\e]11;?\e\e\\\\\e\\"
    elseif $TERM == "screen"
      " GNU Screenを貫通させる
      let s:background_teststr = "\eP\e]11;?\x07\e\\"
    else
      let s:background_teststr = "\e]11;?\e\\"
    endif

    " t_tiは端末がraw modeに入った直後くらいのタイミングで出力されるので、
    " ここにクエリ文字列を追加
    let &t_ti .= s:background_teststr

    " 想定する応答の先頭9文字をmapして非同期に応答を待つ
    nnoremap <special> <expr> <Esc>]11;rgb: g:SetBackground()

    " MinTTYのバグ対策：OSC11ではなく、OSC0が返る
    nnoremap <special> <expr> <Esc>]0;rgb: g:SetBackground()

    " 端末からの応答が得られれば呼ばれる（得られない端末も多い）
    function! g:SetBackground()
      " mapを削除
      unmap <Esc>]11;rgb:
      unmap <Esc>]0;rgb:

      " 追加したクエリ文字列をt_tiから取る
      let &t_ti = substitute(&t_ti, s:background_teststr, '', '')

      " 応答をパースして輝度を得る
      let l:gamma = s:GetGammaFromRGBReport()

      if l:gamma >= 0
        " 境界値32768000と比較してdark/lightを判定
        let l:threshold = 32768000
        if l:gamma > l:threshold
          set background=light
        else
          set background=dark
        endif
      endif
      return ''
    endfunction

    " 応答の残り rrrr/gggg/bbbb ESC \ をパースして輝度を返す
    " 一部 rr/gg/bb ESC \ の形式を返す端末もあるので考慮している
    function! s:GetGammaFromRGBReport()
      " get RGB value
      let l:current = 0
      let l:rgb = []
      let l:count = 0
      let l:failed = 0
      while 1
        let l:c = getchar(0)
        if !l:c
          break
        elseif l:c == 0x7  " <Bel>
          " Esc \のかわりにBELを終端とすることもできる
          if l:count == 2
            call add(l:rgb, l:current * 256)
          elseif l:count == 4
            call add(l:rgb, l:current)
          else
            let l:failed = 1
          endif
          let l:count = 0
          break
        elseif c == 0x1b  " <Esc>
          if l:count == 2
            call add(l:rgb, l:current * 256)
          elseif l:count == 4
            call add(l:rgb, l:current)
          else
            let l:failed = 1
          endif
          let l:count = 0
          " discard 1 more char
          let l:c = getchar(0)
          break
        elseif c == 0x2f  " /
          if l:count == 2
            call add(l:rgb, l:current * 256)
          elseif l:count == 4
            call add(l:rgb, l:current)
          else
            let l:failed = 1
          endif
          let l:count = 0
          let l:current = 0
        elseif c >= 0x30 && c < 0x40  " 0-9
          let l:current = l:current * 16 + c - 0x30
          let l:count += 1
        elseif c >= 0x41 && c < 0x47  " A-F
          let l:current = l:current * 16 + c - 0x31
          let l:count += 1
        elseif c >= 0x61 && c < 0x67  " a-f
          let l:current = l:current * 16 + c - 0x51
          let l:count += 1
        endif
      endwhile

      if l:failed || len(l:rgb) != 3
        return -1
      endif

      " 輝度を計算して返す
      " ref: http://themergency.com/calculate-text-color-based-on-background-color-brightness/
      return l:rgb[0] * 299 + l:rgb[1] * 587 + l:rgb[2] * 114
    endfunction

  endif
endif

```

なお、MinTTYの場合、最近まで

```
ESC ] 0 ; r g b : rrrr / gggg / bbbb ESC \
```

という応答を返すバグがありました。上のスニペットではこれを考慮しています。
MinTTY側にはすでに修正が入っているので、次のリリースあたりでは直っているはずです。
特定のバージョンのtanasinnやTeraTermでは

```
ESC ] 1 1 ; r g b : rr / gg / bb ESC \
```

という桁数の少ない応答が返りますので、これにも対応しています。
この形式はEmacs等で誤動作を起こし得るため、tanasinnでは今では1成分4桁としています。
Xの形式なので誤りでは無いんですが、ではパーサ側でどこまで想定したらよいか、といったあたりの判断は難しいですね。

さらなる応用について
-----------------

今回想定した応答シーケンス

```
ESC ] 1 1 ; r g b : rrrr / gggg / bbbb ESC \
```

は、逆にアプリケーション->端末の方向に送ってやると、背景色の取得ではなく、変更ができるようになります。
また「11」の部分を「10」に変えると前景色をいじれます。
さらに「4;n」にしてやると端末内部のn番目のパレットをいじったりできます。
詳細については、「[XTerm Control Sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#The%20Alternate%20Screen%20Buffer)」を見ていただくとよいと思います。

これらを使用すると色彩理論にもとづいた動的な自動カラースキーマの設定や、背景色・前景色に合わせたパレット色自動調整アルゴリズムを表現できそうな気がします。ぜひ活用してみてください。

おまけ
-----

今回の件にはまったく無関係ですが、おなじく応答を使って起動時にオプション値ambiwidthを適切に設定するVim scriptができたので置いておきます。
同様のことを行う[本体へのパッチ](https://gist.github.com/saitoha/4692491)も書いて送ろうとしてるんですが、そのVim script版です。
カーソル位置報告（CPR）を使っているのですが、MinEdのテクニックにはまだ遠く及ばない状況なので、いつかそのあたりをもうちょっと掘り下げて書きたいです。
パッチの方もこっちの方も動作報告とかいただけたら嬉しいです。

```
"
" This snippet is licensed under NYSL.
" See http://www.kmonos.net/nysl/NYSL.TXT
"
if !has('gui_running')
  function! g:SetAmbigousWidth(width)
    unmap <Esc>[1;2R
    unmap <Esc>[1;3R
    if a:width == 1
      set ambiwidth=single
    elseif a:width == 2
      set ambiwidth=double
    endif
    let &t_ti = substitute(&t_ti, s:ambiguous_teststr, '', '')
    return ''
  endfunction

  if &term =~? 'xterm\|screen\|fbterm\|yaft\|rxvt\|jfbterm'
    let s:ambiguous_teststr = "\e[1;1H\u25bd\e[6n"
  endif
  if exists('s:ambiguous_teststr')
    nnoremap <special> <expr> <Esc>[1;2R g:SetAmbigousWidth(1)
    nnoremap <special> <expr> <Esc>[1;3R g:SetAmbigousWidth(2)
    " jfbtermやdvtmはゼロオリジンのCPR応答を返すバグがあるので
    " (-1, -1)だけずれた位置が返る
    nnoremap <special> <expr> <Esc>[0;1R g:SetAmbigousWidth(1)
    nnoremap <special> <expr> <Esc>[0;2R g:SetAmbigousWidth(2)
    let &t_ti .= s:ambiguous_teststr
  endif
endif
```

Vim Advent Calendar 2012 70日目
-------------------------------
70日目の[Vim Advent Calendar 2012](http://atnd.org/events/33746) は、@supermomongaさんの、[VimShellだいすき（エイリアス編）](http://blog.supermomonga.com/articles/vim/vimshell-daisuki-alias.html)です。