😸

NeovimのTerminalモードをもうちょっと使いやすくする

2022/01/21に公開

背景編

temrminal/Neovimで暮らしている

Terminalやtmuxで画面分割して、その中でNeovimを使ってるという人がほとんどだと思いますが、
個人的に

  • セッションマネージャはほぼ使わない
  • 下のレイヤーの画面分割と、Neovimの画面分割が混在するのは嫌
  • (tmuxとNeovimで)Statusbarが重複するのも嫌
  • キーマップが衝突するリスクを考えるのも嫌
  • クリップボード周りで衝突を起こすのも嫌
  • Mac/Linux間でターミナルの際に振り回されるのも嫌

と、諸々ターミナルやtmuxの画面分割を使う気にならないという理由から、

Neovim内で生活するスタイルを続けています。

  • iTerm2 を起動したら即座にNeovimが起動する
  • Gnomeではターミナルを起動しないでNeovimを起動する(結果的にターミナル内でNeovimが起動するのだけど)
  • シェル操作をしたい時は、Neovim内の terminal を利用する

困りどころ

概ね慣れた Neovim 内の terminal の使い勝手ですが、以下の数点がどうにも使いにくく、
うまいこと回避できないだろうかと模索していました。

  1. 違うWindowにサッと移動できない
  2. 違うWindowから戻ると、モードがノーマルモードになってたりしてターミナルモードに入る操作がめんどくさい
  3. :terminal が既存のWindowを塗り替える
  • 回避するには :split してから :terminal とするなど、二度手間
  1. :terminal 内でのシェル操作を終えて close としても、画面が残る
  • 画面が残る割に、キー入力で閉じてしまうので、ひと手間増えるばかりであんまり意味がない

もっとサッとターミナルを立ち上げて、サッと閉じたいのです。

既存の解決策

この手の困りごとには多々プラグインが用意されるものですが、

  • やたら色々とフルパッケージになっていて邪魔くさい
  • シンプルなものはなぜかFloating Windowで開きたがる
    • 個人的には、Neovimで:terminalを開く時は他のファイルになにか動機となる情報があることが多いので、Floating Windowは使いたくない。
    • 個人的には、何かしらのライフサイクルが発生する作業にFloatingは向いてないと考えている

ということでどれもイマイチフィットしませんでした。

解決編

デカイプラグインではなく細かい設定で

一つ一つの困りどころは、それぞれに解決策となる小さな設定をぶつけるのが最良でしょう。
そこで、設定内に以下のようなものを用意してみました。

for 1: ターミナルモードにウインドウ操作系をマップしてしまう

いちいちターミナルモードからノーマルモードに戻り、ウィンドウを切り替えるのは億劫です。
だったらターミナルモードにマップしてしまえば良いのです。

" <C-w>で使えるウィンドウの管理系をターミナルモードにマップする
tnoremap <C-W>n       <cmd>new<cr>
tnoremap <C-W><C-N>   <cmd>new<cr>
tnoremap <C-W>q       <cmd>quit<cr>
tnoremap <C-W><C-Q>   <cmd>quit<cr>
tnoremap <C-W>c       <cmd>close<cr>
tnoremap <C-W>o       <cmd>only<cr>
tnoremap <C-W><C-O>   <cmd>only<cr>
tnoremap <C-W><Down>  <cmd>wincmd j<cr>
tnoremap <C-W><C-J>   <cmd>wincmd j<cr>
tnoremap <C-W>j       <cmd>wincmd j<cr>
tnoremap <C-W><Up>    <cmd>wincmd k<cr>
tnoremap <C-W><C-K>   <cmd>wincmd k<cr>
tnoremap <C-W>k       <cmd>wincmd k<cr>
tnoremap <C-W><Left>  <cmd>wincmd h<cr>
tnoremap <C-W><C-H>   <cmd>wincmd h<cr>
tnoremap <C-W><BS>    <cmd>wincmd h<cr>
tnoremap <C-W>h       <cmd>wincmd h<cr>
tnoremap <C-W><Right> <cmd>wincmd l<cr>
tnoremap <C-W><C-L>   <cmd>wincmd l<cr>
tnoremap <C-W>l       <cmd>wincmd l<cr>
tnoremap <C-W>w       <cmd>wincmd w<cr>
tnoremap <C-W><C-W>   <cmd>wincmd w<cr>
tnoremap <C-W>W       <cmd>wincmd W<cr>
tnoremap <C-W>t       <cmd>wincmd t<cr>
tnoremap <C-W><C-T>   <cmd>wincmd t<cr>
tnoremap <C-W>b       <cmd>wincmd b<cr>
tnoremap <C-W><C-B>   <cmd>wincmd b<cr>
tnoremap <C-W>p       <cmd>wincmd p<cr>
tnoremap <C-W><C-P>   <cmd>wincmd p<cr>
tnoremap <C-W>P       <cmd>wincmd P<cr>
tnoremap <C-W>r       <cmd>wincmd r<cr>
tnoremap <C-W><C-R>   <cmd>wincmd r<cr>
tnoremap <C-W>R       <cmd>wincmd R<cr>
tnoremap <C-W>x       <cmd>wincmd x<cr>
tnoremap <C-W><C-X>   <cmd>wincmd x<cr>
tnoremap <C-W>K       <cmd>wincmd K<cr>
tnoremap <C-W>J       <cmd>wincmd J<cr>
tnoremap <C-W>H       <cmd>wincmd H<cr>
tnoremap <C-W>L       <cmd>wincmd L<cr>
tnoremap <C-W>T       <cmd>wincmd T<cr>
tnoremap <C-W>=       <cmd>wincmd =<cr>
tnoremap <C-W>-       <cmd>wincmd -<cr>
tnoremap <C-W>+       <cmd>wincmd +<cr>
tnoremap <C-W>z       <cmd>pclose<cr>
tnoremap <C-W><C-Z>   <cmd>pclose<cr>

<C-W> はNeovimのデフォルトの操作と同じ操作感にしたくて潰しましたが、
shell側で単語単位のDeleteとして使ってる人もいると思うので、
そういう場合は違うPrefixを使ってもいいでしょう。

for 2: ターミナルのモードを逐次記憶してWindowに戻った際にリストアする

これはさほど難しくないです。
モード切り替えた際にbufferにモードを焼き付けておいて、BufEnterでリストアしているだけ。

https://github.com/kyoh86/dotfiles/tree/776e42a9636ce73ba27042aba17a884344111024/nvim/local/etc/restore_terminal_mode.vim

function s:save_terminal_mode()
  let b:term_mode = mode()
endfunction

function s:restore_terminal_mode()
  if get(b:, 'term_mode', '') ==# 't'
    startinsert
  endif
endfunction

augroup restore_terminal_mode
  autocmd!
  autocmd TermEnter term://* call s:save_terminal_mode()
  autocmd TermLeave term://* call s:save_terminal_mode()
  autocmd BufEnter term://* call s:restore_terminal_mode()
augroup END

拙作の vim-editerm とか、ありすえさんの edita.vim とは相性がとてもいいです。

for 3 & 4: 画面分割してサッと閉じられるターミナルを開くCommand群

横や縦でサイズ指定の画面分割を行い、ターミナルを立ち上げて、シェルを起動。
シェルの終了(exit)を以て画面を閉じます。

https://github.com/kyoh86/dotfiles/blob/776e42a9636ce73ba27042aba17a884344111024/nvim/local/etc/volatile_terminal.vim

function! s:open_volatile_terminal(opts) abort
  let l:bufnr = bufnr()
  let l:opts = extend(deepcopy(a:opts), {'on_exit': function('<SID>close_volatile_terminal', [l:bufnr])}, 'force')
  " 終了時にバッファを消すterminalを開く
  call termopen(&shell, l:opts)
endfunction

function! s:close_volatile_terminal(bufnr, job_id, code, event) dict
  " if a:code is 0
    call execute('silent! bdelete! ' .. a:bufnr)
  " endif
endfunction

function! s:nosplit_volatile_terminal(opts) abort
  if s:is_initial_buffer()
    enew
  endif
  call s:open_volatile_terminal(a:opts)
endfunction

function! s:split_volatile_terminal(size, mods, opts) abort
  " 指定方向に画面分割
  execute a:mods .. ' ' .. 'new'
  call s:open_volatile_terminal(a:opts)
  " 指定方向にresize
  let l:size = v:count
  if l:size == 0
    let l:size = a:size
  end
  if l:size != 0
    execute a:mods .. ' resize ' . l:size
  end
endfunction

" OpenVolatileTerminal: terminalを開く
"   - 新しいWindowで:   :NewVolatileTerminal
"   - 指定のWindowSize: :30NewVolatileTerminal
"   - 指定の位置:       :vertical NewVolatileTerminal  /  :botright 15NewVolatileTerminal
command! OpenVolatileTerminal :call s:nosplit_volatile_terminal({})
command! -count NewVolatileTerminal :call s:split_volatile_terminal(<count>, <q-mods>, {})
command! OpenVolatileTerminalFromCurrentBuffer :call s:nosplit_volatile_terminal({'cwd': expand('%:p:h')})
command! -count NewVolatileTerminalFromCurrentBuffer :call s:split_volatile_terminal(<count>, <q-mods>, {'cwd': expand('%:p:h')})

Neovim自体を:cd して生活しているため、基本はNeovimのcwdでshellを起動していますが、
「現在開いているファイルのdirectory」をwdとしてshellを起動するコマンド(xxxFromCurrentBuffer)も用意しました。

個人的にはサイズは割とどうでも良いので、等分で呼び出すキーマップも設定しています。

https://github.com/kyoh86/dotfiles/tree/776e42a9636ce73ba27042aba17a884344111024/nvim/local/etc/keymap_terminal.vim#L1-L8

" ターミナルをさっと開く
"   サイズ指定付き: 80tx 15tv
nnoremap <silent> tt <cmd>OpenVolatileTerminal<CR>
nnoremap <silent> tx <cmd>NewVolatileTerminal<CR>
nnoremap <silent> tv <cmd>vertical NewVolatileTerminal<CR>
nnoremap <silent> tct <cmd>OpenVolatileTerminalFromCurrentBuffer<CR>
nnoremap <silent> tcx <cmd>NewVolatileTerminalFromCurrentBuffer<CR>
nnoremap <silent> tcv <cmd>vertical NewVolatileTerminalFromCurrentBuffer<CR>

まとめ

terminal/Neovim 生活、癖が強いのであんまり人におすすめしにくいのですが、
興味がある人にこういう工夫で結構快適になるよ、というのを知っておいてもらえると、
少しこの流派が増えるかも、なんて期待したりしています。

Discussion