🖼️

Vimで作るTUIアプリケーション

2021/12/05に公開

この記事はVim Advent Calendar 2021の5日目の記事です。

シェルスクリプトぐらいの手軽さでTUIアプリケーションを作りたい。かといってあまり依存物となるものを入れないでおきたい。そんな状況は無いでしょうか。いや、あんまり無い気はしますが…

そんなとき、Vimを使うと、依存物をVimのみにしつつ、ある程度簡単にTUIアプリケーションを作成することができます。あまり正攻法な感じでない、Vim本体の隠せない部分が残るといったことはあるものの、TUIアプリケーションを作るための機能自体は色々揃ってるので、意外となんとかはなります。

作ったもの

という訳で実際に作ってみました。それぞれ実際に使えるもの2つ、サンプルとして簡単なもの1つ。

ccg

〇×ゲームを実行するコマンドです。コマンドを実行することですぐ遊ぶことができます。

キー 操作
(ゲーム画面)
h, <Left> カーソルを一つ左に
j, <Down> カーソルを一つ下に
k, <Up> カーソルを一つ上に
l, <Right> カーソルを一つ右に
<CR>, <Space> マークを打点
(リザルト画面)
h, <Left> ゲームを続けるかの選択を切り替え
l, <Right> ゲームを続けるかの選択を切り替え
<CR>, <Space> 選択を実行
(共通)
q ゲームを終了
<C-c> ゲームを終了(Vimは終了しない)

https://github.com/nil-two/ccg

ファイラでディレクトリ移動するコマンドです。シェルの設定ファイルに設定が必要ですが、設定後はファイラで指定したディレクトリに移動ができるようになります。キーはvim-molderを参考にしています。

キー 操作
+ 隠しファイルの表示・非表示の切り替え(デフォルトでは非表示)
- ディレクトリを一つ上に移動
<CR> カーソル位置のディレクトリの中に移動
<C-g> ファイラを閉じて現在のディレクトリへ移動
q ファイラを閉じる

https://github.com/nil-two/ad

tcounter

数を数え上げるだけのコマンドです。これをサンプルとして作れるようにと作ってあります。

キー 操作
<CR> カウントを1増やす
q アプリを閉じる
<C-c> アプリを閉じる(Vimは終了しない)

https://github.com/nil-two/tcounter

作り方

作り方に特に決まりはないですが、基本的には次のコマンドで動くよう、Vim scriptのファイルを用意して、シェルスクリプト等でラップして実行できる形を作れば大丈夫です。

vim -u NONE -i NONE -N -n -S ここにVim scriptのファイル < /dev/tty > /dev/tty

ここで指定しているオプションは次の通りです。詳細は:h startingにあります。< /dev/tty > /dev/ttyは入出力をユーザーの端末にするために必要です。

オプション 概要
-u パス vimrcの場所を指定。NONEにすると何も読み込まない
-i パス viminfoの場所を指定。NONEにすると何も使用しない
-N set nocompatibleを有効に
-n set noswapfileを有効に
-S パス Vim起動後に実行するスクリプトのパスを指定

ラッパー

ラッパーはVimを起動するだけのものです。tcounterではラッパーにBashを使っています。Bashにはプロセス置換<(コマンド)という機能があって、これを使うことで一時ファイル等を作らずにファイルを用意することができます。

#!/bin/bash
set -eu

# ...

appscript() {
  cat <<'EOF'
ここにVim script
EOF
}

# ...

vim -u NONE -i NONE -NnS <(appscript) < /dev/tty > /dev/tty

ラッパーは必ずしもシェルスクリプトで書く必要はなく、Goのos/execを使ってもいいですし、その他コマンドを実行する手段を用いてもいいです。どうにかVim scriptを実行するところまで持っていったら、あとはVim script側で全部動かします。

Vim script

Vim scriptはアプリケーションを記述するものです。setで各種オプションの設定、highlight、syntaxで色付けの設定、mapでキーの設定等をしたら、後は操作に応じて画面が変わるようにするといい感じです。

作り方に決まりはないですが、こういう風にするとよいみたいなものはあって、それらについて説明します。

初期化処理

popupを使う場合は起動画面が被ってしまいます。それを避けるためには次の設定を追記して起動画面を無効化します(微妙に心苦しい)。

setlocal shortmess+=I

また、カーソルを隠したい場合は次の初期化時の設定をします。注意することとして、終了時に設定を戻さないとカーソルが消えたままになるので、次の終了時の設定を終了前にするようにします。

" 初期化時
setlocal t_ve=

" 終了時
setlocal t_ve&

同じく見た目の設定ですが、テキストが無い部分のチルダは次の設定で非表示にできます。

highlight EndOfBuffer ctermfg=0

デバッグ用の終了処理

Vimでスクリプトを編集・実行しながら作るときは、実行後終了しないでも止められるようにしておくと作りやすいです。tcounterでは<C-c>を入力すると次の設定を行うようにしています。

setlocal t_ve&     " カーソル非表示の無効化
call popup_clear() " ポップアップを全て削除
mapclear <buffer>  " バッファ内のキー設定を全て削除

popupを使う

少し新しい機能ではありますが、popupを使うと画面が作りやすいです。また、画面のどこに配置するかが設定できて、画面中央への配置もやってくれます。win_executeを使うとpopup内での各種設定等ができます。

キーの無効化

全部のキーを無効化するにはもうちょっと長い設定が必要ですが、基本的なキーを無効化するだけであれば次の設定でキーを無効化できます。こうすると、エンター、スペース、タブ、バックスペース、デリート、矢印キー、印字可能な文字から始まるキー、それらの頭にgが始まるキー、そしてCtrlキーとアルファベットの組み合わせのキーが無効化されます。コードはvim-duzzleを参考にしています。

let keys = []
call extend(keys, ['<CR>', '<Space>', '<Tab>', '<BS>', '<Del>', '<Up>', '<Down>', '<Left>', '<Right>'])
call extend(keys, map(range(33, 126),                    {_, nr -> escape(nr2char(nr), '|')}))
call extend(keys, map(range(33, 126),                    {_, nr -> printf('g%s', escape(nr2char(nr), '|'))}))
call extend(keys, map(range(char2nr('a'), char2nr('z')), {_, nr -> printf('<C-%s>', escape(nr2char(nr), '|'))}))
for key in keys
  execute printf('nnoremap <silent><buffer>%s <NOP>', key)
endfor

参考

作るときはVim scriptが分かったら大丈夫です。入門記事で雰囲気をつかんで、ドキュメントを都度確認し、UIを提供しているプラグインを参考にして作ると、いい感じだと思います。

入門記事

https://thinca.hatenablog.com/entry/20100201/1265009821
https://knowledge.sakura.ad.jp/23436/

ドキュメント

https://vim-jp.org/vimdoc-ja/

プラグイン

https://github.com/mattn/vim-molder
https://github.com/koron/nyancat-vim
https://github.com/rbtnn/vim-coloredit
https://github.com/deris/vim-duzzle

所感

  • Vimのキーバインドを潰さないで済む感じだときれいに作れるかも
  • Vimのキーバインドを全部潰すことはできない(頑張れば可能?)のでそこが悩みどころ
  • Vim scriptで書く必要がある。書いてみると意外と書きやすい気はするものの…
  • chalice.vimを作れるだけあって機能は揃っている。今は便利な機能も増えている
  • ちょっとしたものを作るなら案外悪くない

Discussion