14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Neovim のターミナルでコマンドが完了したら通知する

Last updated at Posted at 2025-02-16

この記事は Vim 駅伝 2025/2/17 の記事です。前回は静カニさんの「シュレディンガーのyasunori | 静カニのブログ」でした。

前回の記事を受けて

以前「WezTerm + fish でコマンドが完了したら通知する」という記事を書きました。そこでは、別のペインやタブで実行したコマンドが完了した時に通知するようにしたのでした。

これはこれで問題無く動いているのですが、Neovim でターミナルを立ち上げた際にも同種の通知ができないか考えました。

スクリーンショット 2025-02-16 午前11.55.30.png
  1. :terminal でターミナルを起動し、時間の掛かるコマンドを実行する。
  2. 待っている間に別のバッファーで作業する。
    • ターミナルは画面から消えていても、つまり、hidden 状態(:h hidden-buffer)でも良い。
  3. 完了したら通知が来る。

ただしこの通知は、まさにそのターミナルに居る間には来て欲しくありません。完了したかどうかなんて見てれば分かりますからね。

問題はこの、「そのターミナルに居る間」というのが、内部で動いているシェルからは分からない、という点にあります。WezTerm だとシェルに対しアクティブなペインを環境変数 $WEZTERM_PANE で通知してくれたのでこれを実現できたのでした。しかし Neovim 内のターミナルではどうしたら良いでしょう?

neovim-remote で Neovim に問い合わせる

Neovim でターミナルを起動した場合、ターミナルと Neovim はソケットで通信することができます(:h $NVIM)。この通信は MessagePack を使って行われます(:h msgpack-rpc)が、そのコードをイチから書くのは流石に骨が折れます。今回は簡単に済ませるために mhinz/neovim-remote を利用したいと思います。

neovim-remote のインストール

macOS なら Homebrew で一発です。

brew install neovim-remote

その他の OS の場合は pip でインストールしましょう。

pip3 install neovim-remote

nvr コマンドの使い方

neovim-remote パッケージをインストールすると nvr というコマンドが使えるようになります。Neovim の外から Neovim 自体を(文字通りリモートに)操作することもできるのですが、今回は内部のターミナルから以下のコマンドを打ってみましょう。

  1. Neovim で :terminal してターミナルを開く。
  2. nvr -cc split hoge.txt すると新しいウィンドウで hoge.txt が開く。

一番簡単な使い方としては上記のように、引数に与えたファイルを Neovim で開くことができます。

-cc split というオプションを除いて nvr hoge.txt とすると、ターミナルを開いているウィンドウ自体が hoge.txt で置き換えられます。これはおそらく期待する動作では無いでしょう。

nvr --remote-expr '1 + 1'
# 2 と表示するはず

他にも色んな機能があるのですが、今回注目したいのは上の例に挙げた、--remote-expr オプションです。これは引数に与えた文字列を Vim script として解釈し、その演算結果を標準出力に返してくれるものです。これを使ってターミナル内から Neovim の情報を取り出すことができます。

ターミナルを見ていない時にだけ通知したい

実現したい要件はこれ↑です。これはつまり、現在 Neovim 内でそのターミナルにフォーカスが当たっているかどうかを、シェルから知ることができれば解決です。これを以下のようにして実現することにします。

  1. nvr コマンドを使って、現在フォーカスの当たっているウィンドウで動くターミナルのプロセス番号を知る。
  2. それを現在のシェルのプロセス番号と比較し、一致していたら通知しない。

先に 2. の方から考えますと、これは簡単ですね。例えば Fish なら次の通り。

set -l pid (nvr --remote-expr '何か素晴らしい Vim script')
if test $fish_pid = $pid
    # この時は通知しない
end

Fish では $fish_pid で現在のプロセス番号を知ることができます。Bash / Zsh なら $$ を使えばいいでしょう。

現在フォーカスの当たっているターミナルのプロセス番号を知る

これが中々難題でしたが、以下のようにして求められることが分かりました。

  1. 現在アクティブなバッファーがターミナルであるか調べる(buftype オプション(:h 'buftype')を使います)。
  2. 1. が真なら、jobpid() 関数(:h jobpid())にそのバッファーの 'channel' オプション(:h 'channel')の値を渡すと、シェルのプロセス番号が得られる。

Lua で書くならこんな感じでしょう。

local function get_shell_pid()
  local is_terminal = vim.bo.buftype == "terminal"
  if is_terminal then
    return vim.fn.jobpid(vim.bo.channel)
  end
end

今回は nvr --remote-expr を使いますが、これは引数に渡した文字列を nvim_eval() 関数(:h nvim_eval())で評価するオプションです1。ヘルプを見ると分かるのですが、Vim script として評価できる形で引数を渡す必要があります。という訳で、以下のようなコマンドで目的を達せそうです。

# Neovim 内のターミナルで実行するとプロセス番号を表示します。
# 他のバッファーがアクティブな場合 0 を返します。
nvr --remote-expr '&buftype == "terminal" ? jobpid(&channel) : 0'

luaeval() 関数(:h luaeval())を使って Lua で書く方法もありますね。その場合は Vim script の時と同じようにきちんと 'buftype' オプションの値を判定するか、

# Neovim 内のターミナルで実行するとプロセス番号を表示します。
# 他のバッファーがアクティブな場合 False を返します。
nvr --remote-expr 'luaeval("vim.bo.buftype == [[terminal]] and vim.fn.jobpid(vim.bo.channel)")'

あるいは、vim.F.npcall() を使って簡潔に書くこともできるでしょう。

# Neovim 内のターミナルで実行するとプロセス番号を表示します。
# 他のバッファーがアクティブな場合 None を返します。
nvr --remote-expr 'luaeval("vim.F.npcall(vim.fn.jobpid, vim.bo.channel)")'

vim.F というモジュールには Neovim 内部のスクリプトで使われるユーティリティ関数が収められています。今回使った vim.F.npcall() は、引数に指定した関数の呼び出しに成功したらその返り値を、例外を起こして失敗した時には nil を返してくれる関数です。ヘルプには記載が無いのですが、他にも痒い所に手が届くものが多いのでオススメです。

Neovim の中から通知するなら vim.notify を使う

さて、後はこれを元に通知するだけです。前回の記事では osascript コマンドを使って macOS の機能を呼び出していました。

osascript -e 'display notification "'$msg'" with title "command completed"'

今回はこれを改造して、通常のターミナルなら osascript、Neovim 内なら vim.notify:h vim.notify())を使うようにしたいです。この場合は Neovim が返す値を受け取る必要は無く、シェルから送ったコマンドを実行してもらえれば十分なので、--remote-expr オプションではなく --remote-send オプションを使います。

# Neovim 内のターミナルで実行すると hoge と表示します
nvr --remote-send '<Cmd>lua vim.notify "hoge"<CR>'

このオプションで実行するコマンドはマッピングに書くように <Cmd><CR> で囲むようにしましょう。

--remote-send オプションは、引数に与えた文字列を Neovim でキーとして押したものと同じ働きをします。ターミナル上で nvr --remote-send 'abc' と実行してもターミナルに abc と書き込まれるだけで特に面白くないのですが、上記のような特殊文字(<Cmd> など)を利用すると様々なことができます(:h <Cmd>)。

if test -n "$NVIM"; and type -q nvr
    nvr --remote-send '<Cmd>lua vim.notify("'$msg'", vim.log.levels.DEBUG, { title = "command completed" })<CR>' > /dev/null
else
    osascript -e 'display notification "'$msg'" with title "command completed"'
end

実際には以上のようなコードにしました。ここでは Snacks.notifier などのプラグインを使うことを想定して、title オプションを設定しています。同様のプラグインがインストールされていない場合、nvim_echo() 関数(:h nvim_echo())にフォールバックされます。

仕上げ

という訳で、最初に戻りましょう。前回の記事で紹介したスクリプトを修正し、Neovim 内のターミナルにも対応させてみます。

このスクリプトは前回の環境(WezTerm + Fish)で動作しますが、WezTerm ではない別のアプリ(Terminal.app など)で Neovim を起動し、その中で動くターミナルに対しても正しく動きます。Fish 以外のシェルでも同様に実現できると思いますから、各自でやってみてください。

まとめ

この記事では neovim-remote を使って Neovim 内のターミナルから Neovim 本体の情報を得る方法を紹介しました。最近は Lua しか書いていませんでしたが、こういう時は Vim script の知識も必要になりますね。neovim-remote はもっと色々な使い方ができそうなので研究してみたいです。

  1. --remote-expr オプションの実装はこの辺にあります。この中の nvr.server.eval() というのが呼び出し部分で、実際に Neovim へリクエストしている実装は pynvim モジュールのここにあります。

14
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?