:terminalのZSHからNeovimにコマンドの終了を通知すると便利
やりたいこと
Neovimで:temrinal
を使ってターミナルでの操作をしていると、ときおり長時間かかるタスクを待つ時間がもったいないですね。
ターミナルバッファとそれにひもづくセッション自体は継続するので、ウインドウを閉じてしまうこともあります。
しかしそうすると、今度は「いつタスクが終わったか知りたい」という欲求が発生します。
技術的背景
Neovim/Vimのリモートサーバー機能
Neovim/Vimにはリモートサーバーという機能があります。
これはNeovim/Vim自身がサーバーとして動作し、他のクライアントから受け取ったリクエストを実行できるものです。
ref: https://neovim.io/doc/user/remote.html#clientserver
*v:servername* *servername-variable*
v:servername
Primary listen-address of Nvim, the first item returned by
|serverlist()|. Usually this is the named pipe created by Nvim
at |startup| or given by |--listen| (or the deprecated
|$NVIM_LISTEN_ADDRESS| env var).
See also |serverstart()| |serverstop()|.
Read-only.
*--remote-send*
--remote-send {keys} Send {keys} to server and exit. The {keys}
are not mapped. Special key names are
recognized, e.g., "<CR>" results in a CR
character.
*--server*
--server {addr} Connect to the named pipe or socket at the
given address for executing remote commands.
See |--listen| for specifying an address when
starting a server.
あるNeovim/Vimインスタンスでv:servername
に保持されているサーバーアドレスを見て、
:=vim.v.servername
/run/user/1000//nvim.35590.0
他のNeovim/Vimインスタンスからコマンドを渡すことができます。
$ nvim --headless \
--server /run/user/1000//nvim.111110.0 \
--remote-send "<cmd>echomsg foo<cr>"
foo
ZSHのHook関数
ZSHには、何か操作したときに呼び出されるHook関数を登録できます。
1つのコマンドが実行を完了したとき…つまり、つぎのコマンドプロンプトが表示される直前をHookするには、precmd
Hookが利用できます。
- precmd
- Executed before each prompt. Note that precommand functions are not re-executed simply because the command line is redrawn, as happens, for example, when a notification about an exiting job is displayed.
実際にやってみる
設定としてコマンドの終了を通知してみます。
サーバーアドレスを環境変数で引き渡す
Neovim/Vimの:terminal
で立ち上がるシェルは子プロセスですから、Neovim/Vim側での環境変数は引き継がれます。
サーバーアドレスも適当な環境変数に設定してやるとシェル側で受け取れます。
" Neovim server name
let $NVIM_SERVER_NAME = v:servername
または
-- Neovim server name
vim.env.NVIM_SERVER_NAME = vim.v.servername
autocmdを呼び出す
そのままシェルからremote-sendで複雑な処理を呼び出しても良いですが、
- Hookにあまり重い処理を入れるとツラい
- 複雑な処理のVim scriptないしluaで書きつつシェルのエスケープを考慮するのが面倒くさい
という問題があるので、remote-sendではautocmdの呼び出しを行うだけにしておきます。
if [ -n "${NVIM_SERVER_NAME}" ] ; then
function _notify_precmd_to_nvim() {
nvim --server ${NVIM_SERVER_NAME} --headless --remote-send "<cmd>doautocmd User zsh-precmd<cr>"
}
add-zsh-hook precmd _notify_precmd_to_nvim
fi
autocmdで待ち受ける
実際の処理は、autocmdで待ち受けて処理します。
function s:zsh_precmd()
echomsg 'Process done in terminal'
" 他にもターミナルウインドウを自動でひらく、などの処理をしても良いかも
endfunction
autocmd User zsh-precmd call <SID>zsh_precmd()
または
vim.api.nvim_create_autocmd("User", {
pattern = "zsh-precmd",
callback = function()
vim.notify("Process done in terminal", vim.log.levels.INFO)
end,
})
追加:ZSH側の結果を受け取る
コマンドの結果(成功/失敗)ZSHから受け取りたい場合、autocmdのパターンに文字情報として含められます。
function s:zsh_precmd(match)
let l:words = split(a:match, ':')
if l:words[1] ==# '0'
echomsg 'Process done in terminal'
" 他にもターミナルウインドウを自動でひらく、などの処理をしても良いかも
else
echoerr 'Process failed(' .. l:words[1] .. ') in terminal'
endif
endfunction
autocmd User zsh-precmd:* call <SID>zsh_precmd(<amatch>)
または
vim.api.nvim_create_autocmd("User", {
pattern = "zsh-precmd:*",
callback = function(ev)
local terms = vim.split(ev.match, ":")
local ret = terms[2]
if ret == "0" then
vim.notify("Process done in terminal", vim.log.levels.INFO)
else
vim.notify("Process failed(" .. ret .. ") in terminal", vim.log.levels.ERROR)
end
end,
})
戻り値以外の情報も、色々な情報を文字列としてパターンに含めると、応用の幅はありそうです。
(ただし、autocmdのパターン長にも限界があるので、野放図には使えません)
おわりに
Neovimの:terminal
は色々な自由が効くので、このあたりを色々凝ってみるのも楽しそうです。
最後に今の私の設定を最後に貼っておきます。
Discussion