ターミナルの背景色を取得する

# はじめに
CUI/TUIアプリケーションを書いていると、実行中のターミナルの背景色を知りたいことがあります。
大抵の場合、自分が使っている黒背景を基準に色を決めてしまうのですが、広く使われるようになってくると
白背景のターミナルの人から「見づらいのでデフォルトの色を変えてほしい」といった要望が来ることもあります。

歴史あるターミナルアプリケーション（例えばvimやEmacs）は、実は様々な方法でターミナルの背景色を取得したり推定したりしています。この記事ではそれらの方法と2020年時点での各ターミナルの対応状況をまとめます。

最後に今回調査した結果を元にRustのライブラリを実装したのでそちらも紹介します。

調査する上で以下の記事が大変参考になりました。

* [Vimと端末背景色事情](https://qiita.com/kefir_/items/c2bd46728364bdc7470b)

また、最新のターミナル対応状況調査につきましてては

* [rust-jp Slack](https://rust-jp.slack.com/)
* [Rust User Forum](https://users.rust-lang.org)

の皆様にご協力頂きました。ありがとうございます。


# 背景色の取得方法

ここではターミナル背景色の取得方法を紹介します。
これらはターミナルが対応していれば確実に取得できますが、非対応な場合全く情報は得られません。

## XTerm Control Sequences

これは元々XTermというターミナルが実装していた方法で「特殊なシーケンスをターミナルに出力すると、背景色の情報がターミナルから返ってくる」というものです。

[XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)

詳細仕様は上記にありますが、簡単に言うと以下のようなシーケンスを`printf`すると背景色が得られます。

```console
$ printf "\x1b]11;?\x1b\\"
11;rgb:0000/0000/0000
```

また、tmuxやGNU Screenといったターミナルマルチプレクサを使っていると、上記のシーケンスはそのままターミナルに届かないので若干エスケープしてやる必要があります。

```cosole:tmux
$ printf "\x1bPtmux;\x1b\x1b]11;?\x07\x1b\\"
11;rgb:0000/0000/0000
```

```console:screen
$ printf "\x1bP\x1b]11;?\x07\x1b\\"
11;rgb:0000/0000/0000
```

当初XTermといくつかのターミナルだけが対応していた方式のようですが、2020年現在、かなりの数のターミナルが対応するようになってきています。

## WIN32API

これはWindowsのWin32 Console（いわゆる「コマンドプロンプト」）の背景色を取得する方法です。

[GetConsoleScreenBufferInfo](https://docs.microsoft.com/ja-jp/windows/console/getconsolescreenbufferinfo)

WIN32APIにあるGetConsoleScreenBufferInfoという関数で、ターミナルの情報を取得でき、そこから背景色を得ることができます。

# 背景色の推定方法

以下ではターミナル背景色の推定方法を紹介します。
取得方法に対応していない場合でも、比較的可能性の高い値を得るために使われます。

## `COLORFGBG`環境変数

これは元々rxvtというターミナルで前景・背景色を設定する方法でした。iTerm2などもこれを設定するようです。
この環境変数は `0;15` のように`;`区切りで前景と背景を0-15の16通りから指定します。この16通りはデフォルトでは以下のWikipediaに載っているような割り当てになっているので、それに基づいて背景色を得る方法になります。
（例えばvimでは0-6と8を黒背景と推定します）
しかし、実際にはこの割り当ては設定で変更可能なので、必ずしも正しい背景色を得られるわけではなさそうです。

https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit

## `TERM`環境変数

`TERM`環境変数には使っているターミナルの情報が入っているのでそれに基づいて背景色を推定する方法です。
（実際には必ずしも正しいターミナルの情報が入っているわけではないですが…）

例えばvimでは`TERM`が`putty`や`cygwin`だった場合に黒背景と推定します。
これらのターミナルでも背景色を設定することは可能であり、単に「デフォルトが黒だから黒の可能性が高いだろう」という程度の根拠と思われます。

## OS情報

OSの情報から背景色を推定することもあります。
例えばvimではMS-DOSやOS/2の場合黒背景と推定します。
（これらのOSで背景色が変更可能なのかどうかは知らないので、もしかしたら確定なのかもしれません）

# 最新のターミナル対応状況

取得方法のところで説明した通り、現状確実に取れそうなのはXTerm Control SequencesとWIN32APIだけです。
このうちWIN32APIは当然Win32 Consoleしか対応していないので実質的にはXTerm Control Sequences一択となります。
このXTerm Control Sequencesについて各ターミナルの対応状況を調べてみました。

| ターミナル | 対応/非対応 |
|:-- |:--|
| Alacritty  | o  |
| GNOME Terminal  | o  |
| GNU Screen  | o  |
| iTerm2  | o  |
| LilyTerm  | x  |
| macOS terminal  | o  |
|  MATE Terminal | o  |
|  mintty | o  |
| Poderosa  | x  |
| PuTTY  | x  |
| QTerminal  | x  |
| RLogin  | o  |
|  rxvt-unicode | o  |
| sakura  | o  |
|  Tera Term | o  |
| Terminator  | o  |
| tmux  | o  |
| Windows Terminal  | x  |
|  xfce4-terminal | o  |
| xterm  | o  |

このように全てというわけではないものの、多くのターミナルが対応するようになってきています。
最近ユーザが増えていると思われるWindows Terminalですが、こちらは[Issue](https://github.com/microsoft/terminal/issues/3718)が立っており、対応が期待されます。

# Rustライブラリ

ここで紹介した背景色の取得方法を実装したRustライブラリです。

https://github.com/dalance/termbg

以下のように`rgb`関数でRGB値を、`theme`関数でLight/Darkのどちらかを取得できます。

```rust
fn main() {
    let timeout = std::time::Duration::from_millis(100);

    let rgb = termbg::rgb(timeout);
    let theme = termbg::theme(timeout);

    match rgb {
        Ok(rgb) => {
            println!("  Color: R={:x}, G={:x}, B={:x}", rgb.r, rgb.g, rgb.b);
        }
        Err(e) => {
            println!("  Color: detection failed {:?}", e);
        }
    }

    match theme {
        Ok(theme) => {
            println!("  Theme: {:?}", theme);
        }
        Err(e) => {
            println!("  Theme: detection failed {:?}", e);
        }
    }
}
```

Rust製のCUI/TUIであればそのままライブラリ呼び出しで使えばいいですし、
これを使ったコマンドラインツールを作ってシェルスクリプトから呼び出すような使い方も可能です。