Emacs の vterm から ssh して、さらに tmux の中で作業する。
この構成はとても快適ですが、「Emacs 側のカレントディレクトリが全然追従しない」ことに気づきました。
最初は設定ミスだと思っていたのですが、実際には複数の要因が重なっていました。
1. vterm のディレクトリ追跡が動かない
症状は単純でした。
Emacs → vterm → ssh → Lubuntu → bash → tmux という構成で、
- ssh 先で
cdしても - tmux の中でディレクトリを移動しても
Emacs 側のvterm上のモードラインにカレントディレクトリを表示させたいのですが、 default-directory が変わりません。
ローカルの vterm では問題なく動くようにできたので、「ssh か tmux のどちらかが怪しい」と感じました。
2. vterm が何を見ているのか
調べていくと、vterm はシェルの状態をシェル側から送られる制御シーケンスをそのまま信じている、という仕組みだと分かりました。
ここで出てくるのが OSC(Operating System Command)です。
OSC 51;A という形式のシーケンスを送ると、「ユーザー名・ホスト名・カレントディレクトリ」を Emacs に教えられます。
つまり、
- ssh 先の bash が
- プロンプト表示のたびに
- このシーケンスを端末へ出力している
必要があります。
したがって、ローカルに設定するだけでなく、ssh 先の bash に書く必要があります。
3. bash と zsh の違いで一度つまずく
最初に見つけた設定例は zsh 用でした。
precmd というフックを使う形です。
ただし、bash には precmd がありません。
その代わりに PROMPT_COMMAND という仕組みがあります。
これは「プロンプトが表示される直前に実行されるコマンド」を指定する変数です。
vterm_prompt_end() {
printf '\e]51;A%s@%s:%s\e\\' "$(whoami)" "$(hostname)" "$(pwd)"
}
PROMPT_COMMAND=vterm_prompt_endCode language: JavaScript (javascript)
ssh だけなら、これで反映されました。
ただし tmux に入った瞬間、また動かなくなります。
4. tmux がエスケープシーケンスを止めている
ここからが少しややこしいところです。
tmux は「端末の中に端末を作る」ツールです。
その都合上、OSC のような制御シーケンスをそのまま外に流さず、いったん受け止めます。
結果として、
- bash は正しく出力している
- でも Emacs まで届かない
という状態になっていました。
5. tmux / screen 対応の vterm_printf
そこで使うのが、よく知られている vterm_printf という関数です。
tmux や GNU screen(screen という別の端末多重化ツール)を検出し、適切な形式で OSC を送ります。
if declare -p PROMPT_COMMAND >/dev/null 2>&1; then
if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" =~ "declare -a" ]]; then
PROMPT_COMMAND+=(vterm_prompt_end)
else
PROMPT_COMMAND=(${PROMPT_COMMAND:+$PROMPT_COMMAND} vterm_prompt_end)
fi
else
PROMPT_COMMAND=(vterm_prompt_end)
fiCode language: PHP (php)
これで「tmux を意識した出力」はできるようになりました。
しかし、まだ反映されませんでした。
6. tmux の allow-passthrough という壁
原因は tmux の設定でした。
tmux 3.4 では、外部へ制御シーケンスを流すために allow-passthrough を有効にする必要があります。
デフォルトでは off でした。
tmux show -gqv allow-passthrough
この結果が off だったため、どれだけ正しく包んでも外に出ていませんでした。
そこで ~/.tmux.conf に、
set -g allow-passthrough onCode language: JavaScript (javascript)
を書くことで解決しました。