Vim で :terminal の使い勝手をよくした

重い腰を上げてやっと Vim:terminal を使ってみたんですが、いろいろと使い勝手が悪かってので魔改造しました。
いろいろと便利な設定を書こうと思ったら完全に煽り記事みたいになってしまい大変申し訳無くry。
:terminal を真面目に触ってからまだ半日ぐらいしか経ってないので間違っている部分があればゆるしててへぺろ
ちなみに Vim 8.0.1295 時点での話になります。

<C-w> prefix を変更する

さて、:terminal はなぜか Vim の思想に反して insert mode 主体で操作することになります。
なので :terminal を実行すると insert から開始されてまず面食らいます。
それはいいんですが、何故か insert から抜ける際に <Esc> ではなくて <C-w><S-n> を使用するわけのわからない仕様となっています。
と、いうのも :terminal 内ではなぜか insert 時の操作として <C-w> が prefix をして使用されており、これを基準として操作するからです。
insert で <C-w> ってめっちゃ使うのになんでこのキーを prefix にするのか小一時間(ry。
なのでまず <C-w> をどうにかします。
幸いにもこの prefix は 'termkey' で設定することができるので好きなキーに割り当てておきましょう。

" とりあえず Alt + w にでも
set termkey=<A-w>

これで <A-w><S-n> で insert から抜けることができます。
が、流石にこれも Vim らしくないので <Esc> で insert を抜けたいです。
幸いにも :tmap という :terminal の insert 時専用のキーマッピングを行う機能があるのでそれを使用します(なんで imap じゃないんだろうか???

" <Esc> で :terminal の insert を抜ける
" <A-w> の部分は 'termkey' によって変わる
tnoremap <Esc> <A-w><S-n>

これで :terminal を使用する上でのストレスはだいぶ軽減されたと思います。

filetype を設定する

次は 'filetype' まわりの設定を行います。
Vim といえば autocmd FileType 等でその filetype 固有の設定を行うのが当たり前ですよね。
しかし、なぜか :terminal では 'filetype' が設定されていないので自前で 'filetype' を設定することにします。
:terminal を実装した人って本当に Vim を作ったことがあるのか本気で気になる

function! s:bufnew()
   " 幸いにも 'buftype' は設定されているのでそれを基準とする
    if &buftype == "terminal" && &filetype == ""
        set filetype=terminal
    endif
endfunction


function! s:filetype()
   " ここに :terminal のバッファ固有の設定を記述する
endfunction


augroup my-terminal
    autocmd!
   " BufNew の時点では 'buftype' が設定されていないので timer イベントでごまかすなど…
    autocmd BufNew * call timer_start(0, { -> s:bufnew() })
    autocmd FileType terminal call s:filetype()
augroup END

ポイントとしては autocmd BufNew の時点では 'buftype' が設定されていないようなので timer_start() を使用して関数の呼び出しを遅延しています。
割と無理やり実装したのでこの辺りのタイミングはもっといい手段があるかもしれません…。
とにかくこれで filetype=terminal を設定できたのであとは autocmd FileType terminal 等でやりたい放題になります。

:terminal の起動直後に insert から抜ける

先程も書きましたが :terminal は insert から開始されます。
ですが、Vim を使っているのであればやはり normal 主体で使いたいですよね。
と、いうことで『:terminal を起動したあとに insert から抜ける』という設定を行います。

" 先ほど作った関数に追記する
function! s:filetype()
   " これも <A-w> の部分は自分の環境に合わせて直してね
    call timer_start(0, { -> feedkeys("\<A-w>\<S-n>") })
endfunction

これも filetype=terminal したタイミングだと inser から抜ける事ができなかったので timer_start() でごまかしています。

起動した :terminal を使い回す

最後に起動している :terminal を使い回すようにします。
どういうことかというと :terminal:terminal コマンドを実行する度に新しいバッファが生成されます。
この :terminal のバッファは :q! をしない限りは生き続けるので :terminal をする度にどんどん :terminal のバッファが増え続けます。
なので、すでに起動している :terminal があればその :terminal のバッファを開くようにしてみます。

function! s:open(args) abort
    if empty(term_list())
        execute "terminal" a:args
    else
        let bufnr = term_list()[0]
        execute term_getsize(bufnr)[0] . "new"
        execute "buffer + " bufnr
    endif
endfunction


" すでに :terminal が存在していればその :terminal を使用する
command! -nargs=*
\   Terminal call s:open(<q-args>)

:terminal の起動には :Terminal コマンドを使用します。 term_list() で『起動している :terminal のバッファリスト』を取得できるので、それを使用して既存の :terminal を開きます。 これは完全に簡易実装なのですが、バッファの開き方などを指定出来るようにはしたいですね…。

所感

と、いうことでざっと :terminal を使ってみたんですが、それ自体がつらいというか全体的に Vim のセオリーから外れている点が一番つらいですね…。 未だに <C-w> を prefix に割り当ててる理由がわかりません…。 よくよく考えたら『:terminal を使ってみた』ではなくて『:terminal を起動させてみた』ですねこれ。 まだ :terminal 自体全然使ってないな…これからがんばろう…。 あと * Vim 上で端末を使える利点ってなんですか? っていう質問をされた時の答えを教えてください。

補足

一応 <Esc> を使わないのは以下のような理由があるみたい。

設定まとめ

" vital-palette-keymapping
" <A-w> みたいなのを \<A-w> にして feedkeys() で呼び出せるようにするためのエスケープ関数
function! s:escape_special_key(key)
   " Workaround : <C-?> https://github.com/osyo-manga/vital-palette/issues/5
    if a:key ==# "<^?>"
        return "\<C-?>"
    endif
    execute 'let result = "' . substitute(escape(a:key, '\"'), '\(<.\{-}>\)', '\\\1', 'g') . '"'
    return result
endfunction


" キーマッピングの設定
set termkey=<A-w>
if exists(":tmap")
    tnoremap <Esc> <A-w><S-n>
endif


function! s:bufnew()
    if &buftype == "terminal" && &filetype == ""
        set filetype=terminal
    endif
endfunction


function! s:filetype()
   " set filetype=terminal のタイミングでは動作しなかったので
   " timer_start() で遅延して設定する
    call timer_start(0, { -> feedkeys(s:escape_special_key(&termkey) . "\<S-n>") })
endfunction


augroup my-terminal
    autocmd!
    autocmd BufNew * call timer_start(0, { -> s:bufnew() })
    autocmd FileType terminal call s:filetype()
augroup END


function! s:open(args) abort
    if empty(term_list())
        execute "terminal" a:args
    else
        let bufnr = term_list()[0]
        execute term_getsize(bufnr)[0] . "new"
        execute "buffer + " bufnr
    endif
endfunction


" すでに :terminal が存在していればその :terminal を使用する
command! -nargs=*
\   Terminal call s:open(<q-args>)