Edited at

Vimと端末背景色事情

More than 5 years have passed since last update.


はじめに

この記事は Vim Advent Calendar 2012 69日目の記事です。 昨日の記事は@scheakurさんの 「Just Press Dot Key」でした。

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


背景色の判定はemacsやMinEdでも行なわれているので、vimでもtermresponseフューチャーを有効化してコンパイルされたものは、これをおこなってよいだろうと考えています。

具体的には起動時のtermresponseフェーズにおいて、xtermのパッチレベル判定を行った後、端末に対してデフォルト前景色・背景色の報告を要求する特殊なクエリを投げ、応答を非同期で受け取りbackgroundオプション値自動設定の参考にするというものです。

背景色の判定が可能な端末は、xterm、rxvt-unicode、TeraTerm、RLogin、MinTTY、mltermなどです。gnome-terminal、rxvt、Poderosa、iTerm2などは、実はあと一歩というところまで実装されているので、対応を入れるのは楽そうだと考えています。


vimと端末エミュレーション


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

今回はもう少し具体的に書いてみます。


背景色取得のしくみ

近年の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 EmacsMinEdが挙げられます。

両者とも必ず出しているというわけではなく、端末特性応答などと絡めた複雑な条件でこの応答をつかうかどうかを決めています。

osc11

Xterm(ただしパッチレベル242以上279以下)上でのEmacs24.2や、mlterm(3.1.5以上)上でのMinEd2011.22、といった組み合わせでは、起動時にこの背景色の問い合わせ&応答のやりとりをばっちり観察することができます。

興味があればTrachetを利用して確認してみましょう。Trachetのチュートリアルはこちら


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

さて、Vimにおいてはこの素敵なフィーチャーをどのように活かしていけばよいか。まず思いついたのは、backgroundオプション値(light/dark)を動的に決定するための材料とする、という用途です。

これは@yoshikawさんの記事「Vimの背景色を環境変数で設定する」でも触れられているとおり、Vim本体側でも$TERMや$COLORFGBGを見るなどおおざっぱな方法ですでにやっています。

しかしおそらくこのあたりは、ユーザーが自由なアルゴリズムにもとづいてVim scriptでカスタマイズしたくなる部分ではないでしょうか。

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

想定する端末はxterm、rxvt-unicode、TeraTerm、RLogin、MinTTY、mlterm(3.1.5以上)あたりです。

screen/tmuxでも親端末がこの機能を実装していれば動作すると思います。

不具合など出たら教えてください。

まだまだ対応端末が少ないのがネックですね。なんとかしていきたいです。

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

"

" 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」を見ていただくとよいと思います。

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


おまけ

今回の件にはまったく無関係ですが、おなじく応答を使って起動時にオプション値ambiwidthを適切に設定するVim scriptができたので置いておきます。

同様のことを行う本体へのパッチも書いて送ろうとしてるんですが、その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 は、@supermomongaさんの、VimShellだいすき(エイリアス編)です。