Fishの色設定をターミナルだけで完結する

Fishの魅力のひとつに「ブラウザからカラーテーマが変更できる」というものがあります。
私も使い始めた頃はこの恩恵に預かっていたものの、次第にこんな悩みが出てきました。

- Vimのターミナルで正常な色が出てこない
- コマンドラインと出力の色が一致しない
- 別環境での再現が面倒だし、dotfilesで管理もできない
- **そもそもブラウザが開けない環境では設定すらできない**

これらが結構致命的だったため過去に[こんなプラグイン](https://github.com/rkbk60/onedark-fish)を作ったものの、いろいろあって以下の方法にすることにしました。

## 要約

- `fish_config`での色変更 = ただの変数定義
- [base16-shell](https://github.com/chriskempson/base16-shell)を使おう
- Fish バージョン 3.1.2 での話

## 入力の色設定

`fish_config`コマンドをPythonの存在しない環境下で実行すると以下の表示が出ます。

> Cannot launch the web configuration tool: 
> fish_config requires Python. 
> Installing python2 or python3 will fix this, and also enable completions to be 
> automatically generated from man pages. 
> 
> To change your prompt, create a fish_prompt function. 
> There are examples in $\_\_fish_data_dir/tools/web_config/sample_prompts.
> 
> You can tweak your colors by setting the $fish_color_* variables. 

つまり`fish_color_`で始まる変数を設定することでブラウザレスに色設定ができるというわけですね。

### 設定項目

まずは対象となる変数名を確認しておきましょう。[公式](https://fishshell.com/docs/current/#variables-for-changing-highlighting-colors)に書いてありました。

| 変数名 (コマンドライン用)     | 対象                                                         | 備考                               |
| ----------------------------- | ------------------------------------------------------------ | ---------------------------------- |
| fish_color_**normal**         | デフォルト[^1]                                               |                                    |
| fish_color_**command**        | コマンド                                                     |                                    |
| fish_color_**quote**          | クォートを含む文字列全体                          |                                    |
| fish_color_**redirection**    | リダイレクト `>/dev/null` など                               |                                    |
| fish_color_**error**          | エラーになる箇所                                             |                                    |
| fish_color_**end**            | コマンド末端につけるもの<br />- コマンド終端 `;`<br />- バックグラウンド実行 `&` など |           |
| fish_color_**param**          | コマンドの引数                                               |                                    |
| fish_color_**comment**        | コメント                                                     |                                    |
| fish_color_**match**          | カーソル位置に対応する括弧                                   |                                    |
| fish_color_**selection**      | viモード時visualでの選択箇所                             | -bもしくは-rによる背景色変更のみ可能 |
| fish_color_**search_match**   | 履歴検索時の一致箇所<br />Fishでのデフォルト履歴検索は矢印キー上下 | -bもしくは-rによる背景色変更のみ可能 |
| fish_color_**operator**       | Fish内で使用する特殊記号<br />- $HOME `~`<br />- ワイルドカード `*` など |                                    |
| fish_color_**escape**         | エスケープ文字 `\n` `\x70`など                               |                                    |
| fish_color_**autosuggestion** | コマンドライン右側に出てくるコマンドや引数の入力候補         |                                    |
| fish_color_**cwd**            | カレントワーキングディレクトリ                               | デフォルトのプロンプトでのみ使用   |
| fish_color_**user**           | ユーザー名                                                   | デフォルトのプロンプトでのみ使用   |
| fish_color_**host**           | ホスト名                                                     | デフォルトのプロンプトでのみ使用   |
| fish_color_**host_remote**    | リモートホスト名                                             | デフォルトのプロンプトでのみ使用   |
| fish_color_**cancel**         | killシグナル `^C`                                            |                                    |

| 変数名 (ページャー[^2]用)            | 対象                                                         | 備考                                                        |
| -------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- |
| fish_pager_color_**progress**    | ページャー下部にある行数表示部分                             |                                                             |
| fish_pager_color_**background**  | ページャーの背景                                             | -bもしくは-rによる背景色変更のみ可能                          |
| fish_pager_color_**prefix**      | 一致部分                                                     |                                                             |
| fish_pager_color_**completion**  | 補完部分                                                     |                                                             |
| fish_pager_color_**description** | 説明文                                                       |                                                             |
| fish_pager_color_**secondary_X** | 非選択箇所の色を上書き。<br />以下の要素がXに入る。<br />- background<br />- prefix<br />- completion<br />- description | 未指定の場合はそれぞれの<br />fish_pager_color_X を継承する |
| fish_pager_color_**selected_X**  | 選択箇所の色を上書き。<br />以下の要素がXに入る。<br />- background<br />- prefix<br />- completion<br />- description | 未指定の場合はそれぞれの<br />fish_pager_color_X を継承する |

### 設定方法

つぎに設定方法ですね。[ソースコード](https://github.com/fish-shell/fish-shell/blob/385b069eb22995d448579d7dd3c6445d796186f9/src/color.cpp)を読むと以下のことがわかります。

- 色設定の方法は2通り
  - RGB形式による指定
  - 色名による指定
- その他 `set_color` のオプションも一部使用可能
  - `-b` , `--background`  
    背景色の指定。set_colorと違って **= でつなげる必要あり** 。  
    ただし[このオプションが反映されてない不具合](https://github.com/fish-shell/fish-shell/issues/6939)がいくつか確認されているので気休め程度に。
  - `-d` , `--dim`  
    色を暗くする。
  - `-i` , `--italics`  
    斜体にする。
  - `-o` , `--bold`  
    太字にする。
  - `-r` , `--reverse`  
    文字色を背景色にして抜き文字で表示する。

色設定について、前者は以下の形式ができるとのこと。

> // We support the following style of rgb formats (case insensitive):
> //  #FA3
> //  #F3A035
> //  FA3
> //  F3A035

対して後者はこんな感じ。接頭辞の`br`は**bright**を意味するものと思われます。

| 色名                    | ANSIカラー インデックス |
| ----------------------- | ----------------------- |
| black                   | 0                       |
| red                     | 1                       |
| green                   | 2                       |
| yellow<br />brown       | 3                       |
| blue                    | 4                       |
| magenta<br />purple     | 5                       |
| cyan                    | 6                       |
| white<br />grey         | 7                       |
| brblack<br />brgrey     | 8                       |
| brred                   | 9                       |
| brgreen                 | 10                      |
| bryellow<br />brbrown   | 11                      |
| brblue                  | 12                      |
| brmagenta<br />brpurple | 13                      |
| brcyan                  | 14                      |
| brwhite                 | 15                      |

問題はどちらを採用するかですが、私は後者の色名指定にしました。
GNOME-terminalやiTerm2しか使わない理想的環境下であれば前者でもいいのですが、それ以外の状況下では常に指定した色を確実に出力してくれる保証もなければ後述する理由で細かい色指定をする必要がないこともあり、いかなる環境下でも最低限「だいたいの色ルール」を維持しておきたいということでこうなりました。

### できたもの

というわけで私の`config.fish`には以下の記述が入っています。
`br`だらけなのは視認性向上のため。

```sh:config.fish
# 色配置は次のコマンドの出力に基づいています
# fish-vimcolor onedark

# commandline
set fish_color_normal         brwhite
set fish_color_autosuggestion brblack
set fish_color_cancel         brcyan
set fish_color_command        brpurple
set fish_color_comment        brblack
set fish_color_cwd            brred
set fish_color_end            brwhite
set fish_color_error          brred
set fish_color_escape         brcyan
set fish_color_host           brgreen
set fish_color_host_remote    bryellow
set fish_color_match          brcyan --underline
set fish_color_operator       brpurple
set fish_color_param          brred
set fish_color_quote          brgreen
set fish_color_redirection    brcyan
set fish_color_search_match   --background=brblack
set fish_color_selection      --background=brblack
set fish_color_user           brblue

# pager
set fish_pager_color_progress              brblack --italics
set fish_pager_color_secondary_background  # null
set fish_pager_color_secondary_completion  brblack
set fish_pager_color_secondary_description brblack
set fish_pager_color_secondary_prefix      brblack
set fish_pager_color_selected_background   --background=brblack
set fish_pager_color_selected_completion   bryellow
set fish_pager_color_selected_description  bryellow
set fish_pager_color_selected_prefix       bryellow

```



## 出力の色設定

つづいて出力の色設定です。 `ls -al --color=always`などの色をコマンドで変えられます。

本来はターミナルの設定画面を開くなりしないと変えられないアウトプットカラーですが、コマンドラインから変更する方法もないわけではないため**どの環境でも常に同じ色を瞬時に出力できる**のです。

加えて出力のカラーパレットを上書きすることは前述の**色名で指定したコマンドラインの色も連動的に変えられる**ことも意味しています。これによりブラウザからの設定では絶対に実現し得ない**100%のカラースキーム統一が実現できる**のです。

### 出力の色はどうやって変えるのか

ここでは以前紹介したことのある[base16-shell](https://github.com/chriskempson/base16-shell)を使ってみます。
私としてはこんな理由があって使っています。

- Fishにも対応している
- プリセットが豊富
- プリセットからの書き換えも楽
- 10ミリ秒弱という実行コストの低さ
- 1000超のスター数とIssuesの活発さによる安心感
- **そもそもbase16というプロジェクトが好き**

以前はまだFishに対応もしていなかったり理解が足りていなかったこともあり雑なプラグインを作ってしまいましたが、久々に本家を見てみたらかなり進化していました。お気に入りのOneDarkもあって使わない手はないな、と。

### 使い方

公式の説明をまとめて`config.fish`に書くならこんな感じです。

```sh:config.fish
# base16-onedarkを設定したい
if status --is-interactive
    set -l BASH16_DIR ~/.config/base16-shell
    if test ! -d $BASH16_DIR
        git clone https://github.com/chriskempson/base16-shell.git $BASE16_DIR
    end
    source $BASE16_DIR/profile_helper.fish
    base16-onedark
end
```

しかしこのままだと問題がいくつか。

まず起動がほんのり遅くなります。`time`コマンドで調べると私の環境では100 〜 200ミリ秒ほどかかっていました。anyenvほどじゃないけどやや重めといったところか。
調べてみると`profile_helper.fish`というもの、基本的にはテーマの数だけforで回してshスクリプトを呼び出すFish関数を作成するだけのものだったので、使いたいテーマが決まっていればそのshスクリプトを実行するだけで済むということがわかりました。

続いてわかったのが特定のターミナル環境だと上手くいかないということ。たとえばVim/NeovimのターミナルなどはFish側では何をやっても色はつかない……なので環境変数に`VIM`があるときはbase16-shellの実行を回避してVim側で色設定[^3]する必要があります。
こんな感じでうまくいかない場面はけっこうあるので、設定中に`begin; ...; end`を作っておいてパスすればshを実行、とするといいでしょう。

というわけで上記を踏まえて書き換えたのが以下の通り。

```sh:config.fish
# base16-onedarkを設定したい
if status --is-interactive
    set -l BASH16_DIR ~/.config/base16-shell
    if type -q git; and test ! -d $BASH16_DIR
        git clone https://github.com/chriskempson/base16-shell.git $BASE16_DIR
    end

    begin
    	test -d $BASH16_DIR
        and not set -qx VIM    # Vim/Neovim
        and not set -qx VSCODE # VSCode with terminal.integrated.env.linux
    end
    if test $status -eq 0
        # POSIX準拠ではないらしいのでBashを強制
        bash $BASE16_DIR/scripts/base16-onedark.sh
    end
end
```

`profile_helper`なしでコマンドひとつでカラースキームを変えたい場合はこんなものを付け加えてみてはいかがでしょうか。

```sh:config.fish
# base16_theme -t monokai でテーマ変更が可能
function base16_theme -V BASE16_DIR -d "set base16 theme"
    argparse -n=base16_theme 't/transparent' -- $argv; or return
    test -z "$argv"; and return

    set -l theme $argv[1]
    set -lq _flag_transparent
        and set -x BASE16_SHELL_SET_BACKGROUND false # clear
        or  set -x BASE16_SHELL_SET_BACKGROUND true  # non-clear

    sh $BASE16_DIR/scripts/base16-$theme.sh
end

function pick_themes -V BASE16_DIR -d "list themes to complete base16_theme argument" 
    find $BASE16_DIR/scripts/*.sh -type f -printf '%f\n' \
        | string replace -ar '^base16-(.*)\.sh$' '$1' \
        | string join ' '
end

complete -c base16_theme -s t -l transparent -d "make transparent background"
complete -c base16_theme -xa (pick_themes)
functions -e pick_themes
```

### カスタマイズ

以前紹介した通りこのshスクリプトはRGB形式で宣言した色コードを加工しprintfすることでターミナルのカラーパレットを上書きする手法を採っています。

printfによる上書きの原理こそややこしいですが、シェルスクリプトとしてはとても簡単なものになっているのでカスタマイズもそう難しくないと思います。



## まとめ

以前書いた色コードの記事がほぼすべて無駄になり、おかげで様々なしがらみからも解放され、bcコマンドの勉強もできました。

皆様もぜひFishを使いましょう。


[^1]: これが適用されるところは見たことがないが、念の為設定しておくといいはず。
[^2]: Tabを2回押すと出てくる補完候補を一覧してくれるやつのこと。
[^3]: それぞれ [Vimのターミナル色設定](https://vim-jp.org/vimdoc-ja/terminal.html#g:terminal_ansi_colors)と[Neovimのターミナル色設定](https://neovim.io/doc/user/nvim_terminal_emulator.html#terminal-configuration)。