🐼

:terminalのZSHからNeovimにコマンドの終了を通知すると便利

2024/11/18に公開

やりたいこと

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するには、precmdHookが利用できます。

https://zsh.sourceforge.io/Doc/Release/Functions.html#Hook-Functions

  • 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側での環境変数は引き継がれます。
サーバーアドレスも適当な環境変数に設定してやるとシェル側で受け取れます。

env.vim
" Neovim server name
let $NVIM_SERVER_NAME = v:servername

または

env.lua
-- Neovim server name
vim.env.NVIM_SERVER_NAME = vim.v.servername

autocmdを呼び出す

そのままシェルからremote-sendで複雑な処理を呼び出しても良いですが、

  • Hookにあまり重い処理を入れるとツラい
  • 複雑な処理のVim scriptないしluaで書きつつシェルのエスケープを考慮するのが面倒くさい

という問題があるので、remote-sendではautocmdの呼び出しを行うだけにしておきます。

notif.zsh
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で待ち受けて処理します。

zsh-result.vim
function s:zsh_precmd()
    echomsg 'Process done in terminal'
    " 他にもターミナルウインドウを自動でひらく、などの処理をしても良いかも
endfunction

autocmd User zsh-precmd call <SID>zsh_precmd()

または

zsh-result.lua
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のパターンに文字情報として含められます。

zsh-result.vim
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>)

または

zsh-result.lua
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は色々な自由が効くので、このあたりを色々凝ってみるのも楽しそうです。
最後に今の私の設定を最後に貼っておきます。

https://github.com/kyoh86/dotfiles/blob/5fdef4f60889e2a77998b3539911d0a9814c7db8/nvim/lua/kyoh86/conf/envar.lua#L23-L24
https://github.com/kyoh86/dotfiles/blob/5fdef4f60889e2a77998b3539911d0a9814c7db8/zsh/part/zsh_notify_done.zsh
https://github.com/kyoh86/dotfiles/blob/5fdef4f60889e2a77998b3539911d0a9814c7db8/nvim/lua/kyoh86/conf/zsh-result.lua
https://github.com/kyoh86/dotfiles/blob/5fdef4f60889e2a77998b3539911d0a9814c7db8/nvim/lua/kyoh86/lib/volatile_terminal.lua

Discussion