$ ~/projects Emacsのvtermでカレントディレクトリを
追跡してmode-lineに表示する
(directory tracking)

EmacsのvtermでCodexを使っていたら、カレントディレクトリがわかりにくいことがあります。
そこで、vtermのmode-lineにカレントディレクトリを表示できるよう設定をしました。

<svg class="eyecatch-svg" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
  <!-- 背景 -->
  <rect width="192" height="192" fill="white" rx="32" ry="32"/>
  
  <!-- ターミナルウィンドウ -->
  <g transform="translate(24, 36)">
    <!-- ウィンドウ枠 -->
    <rect width="144" height="120" fill="#263238" rx="6" ry="6"/>
    
    <!-- ウィンドウヘッダー -->
    <rect width="144" height="24" fill="#37474F" rx="6" ry="6"/>
    <rect y="24" width="144" height="6" fill="#37474F"/>
    
    <!-- ウィンドウコントロールボタン -->
    <circle cx="12" cy="12" r="4" fill="#F44336"/>
    <circle cx="24" cy="12" r="4" fill="#FF9800"/>
    <circle cx="36" cy="12" r="4" fill="#4CAF50"/>
    
    <!-- プロンプト記号 -->
    <text x="12" y="50" fill="#4CAF50" font-family="monospace" font-size="16" font-weight="bold">$</text>
    
    <!-- カレントディレクトリパス -->
    <text x="24" y="50" fill="#E0E0E0" font-family="monospace" font-size="12">~/projects</text>
    
    <!-- フォルダアイコン(中央下) -->
    <g transform="translate(48, 70)">
      <path d="M 0 8 L 0 40 L 48 40 L 48 8 L 20 8 L 16 4 L 0 4 Z" fill="#2196F3" stroke="#1976D2" stroke-width="2" stroke-linejoin="round"/>
      <rect x="0" y="8" width="48" height="4" fill="#1976D2"/>
    </g>
  </g>
  
  <!-- 追跡を示す矢印 -->
  <g transform="translate(140, 100)">
    <path d="M 0 0 L 20 0 L 20 -8 L 32 8 L 20 24 L 20 16 L 0 16 Z" fill="#FF9800" stroke="none"/>
  </g>
</svg>Emacsのvtermでカレントディレクトリを<br class="chiilabo-br is-on">追跡してmode-lineに表示する<br class="chiilabo-br is-on">(directory tracking)

関連記事

1. vtermでディレクトリ追跡が動かない理由

vtermを使っていて、ターミナル内でcdコマンドを実行しても、Emacsのdefault-directoryが更新されないことに気づきました。
別のバッファでC-x C-f(find-file)を実行しても、ターミナルで移動したディレクトリではなく、古いディレクトリが開いてしまいます。

ディレクトリ追跡が動かない理由 問題 cdコマンド実行 default-directory 更新されない 原因 シェル⇔Emacs 情報伝達の仕組み 未設定 解決方法 1 シェル側設定 2 Emacs側設定

どうも、vtermでディレクトリ追跡を行われていないようでした。
ディレクトリ追跡(directory tracking)とは、シェルでディレクトリを移動したときに、その情報をEmacsに伝える仕組みです。
この機能を有効にするには、Emacs側とシェル側の両方で設定が必要になります。

1.1. シェル側の設定:precmdフックを使う(zshの場合)

ディレクトリ追跡を実現するには、シェルがプロンプトを表示するたびに、現在のディレクトリ情報をvtermに送信する必要があります。
zshの場合、precmdというフックを使うと確実です1

シェル側の設定(zsh) precmdフックを使用 コマンド実行後に毎回呼ばれる特別な関数 vterm_printf 関数定義 vterm_prompt_end ディレクトリ情報送信 precmd設定 フック登録 送信データ \e]51;A$(whoami)@$(hostname):$(pwd)\e\\

precmdは、各コマンドの実行後に必ず呼ばれる特別な関数です。
ここに、vtermへディレクトリ情報を送る処理を追加します2

# .zshrc に追加
vterm_prompt_end() {
    printf "\e]51;A$(whoami)@$(hostname):$(pwd)\e\\"
}

precmd() {
    vterm_prompt_end
}Code language: PHP (php)

この設定の仕組みを説明します。
printfで出力している\e]51;Aは、vtermが認識する特殊なエスケープシーケンス(escape sequence)です。
エスケープシーケンスとは、画面に表示する文字ではなく、ターミナルに対する命令を表す特別な文字列です。

$(whoami)@$(hostname):$(pwd)の部分で、現在のユーザー名、ホスト名、作業ディレクトリを取得しています3
最後の\e\\は、エスケープシーケンスの終端を表します。
この出力は画面には表示されず、vtermが内部で解釈します。

precmdを使う利点は、PROMPTの設定と独立している点です。
後からプロンプトのテーマを変更したり、PROMPT=...で上書きしたりしても、ディレクトリ追跡は影響を受けません。

1.2. PROMPT_COMMAND(bashの場合)

bashの場合は、precmdの代わりに、PROMPT_COMMANDという変数を使います。

# .bashrc に追加
vterm_prompt_end() {
    printf "\e]51;A$(whoami)@$(hostname):$(pwd)\e\\"
}

PROMPT_COMMAND=vterm_prompt_end
Code language: PHP (php)

PROMPT_COMMANDは、bashでプロンプトを表示する直前に実行されるコマンドを指定する変数です。zshのprecmdと同じ役割を果たします。

既にPROMPT_COMMANDに何か設定している場合は、セミコロンで区切って追加します4

PROMPT_COMMAND="vterm_prompt_end; $PROMPT_COMMAND"
Code language: JavaScript (javascript)

1.3. Emacs側の設定:directory-tracking-enabledを有効にする

シェル側で情報を送信しても、Emacs側がそれを受け取る準備をしていなければ意味がありません。

Emacs側の設定 directory-tracking-enabled を有効にする vterm-mode-hook vtermバッファ起動時に実行 (add-hook ‘vterm-mode-hook (lambda () (setq-local … setq-local:バッファローカル変数に設定 vtermバッファのみで有効

vtermのフックでdirectory-tracking-enabledを有効にします5

(add-hook 'vterm-mode-hook
          (lambda ()
            (setq-local directory-tracking-enabled t)))

setq-localは、その変数をバッファローカル(buffer-local)にして値を設定する関数です。
vtermバッファでのみディレクトリ追跡を有効にし、他のバッファには影響しません。

2. mode-lineにカレントディレクトリを表示する

ディレクトリ追跡が動くようになったので、次はカレントディレクトリをmode-lineに表示してみます。

mode-lineに表示する mode-line = ウィンドウ下部の情報バー mode-line-buffer-identification を拡張 バッファ名表示部分に追加 取得する情報 default-directory ディレクトリ名のみ抽出 表示例 [projects]

mode-lineとは、Emacsのウィンドウ下部に表示される情報バーのことです6

mode-lineは、表示要素をリストで管理しています。
バッファ名を表示している部分はmode-line-buffer-identificationという変数で管理されているので、ここを拡張する方法が自然です。

(setq-default
 mode-line-buffer-identification
 (append mode-line-buffer-identification
         '((:eval
            (when (eq major-mode 'vterm-mode)
              (format " [%s]"
                      (propertize
                       (file-name-nondirectory
                        (directory-file-name default-directory))
                       'face 'bold)))))))Code language: PHP (php)

この設定では、vtermバッファでのみディレクトリ名を表示します。
file-name-nondirectorydirectory-file-nameを組み合わせることで、フルパスではなく、ディレクトリ名だけを取り出しています。

例えば、/home/user/projects/というパスからprojectsだけを抽出します。
directory-file-nameで末尾のスラッシュを削除し、file-name-nondirectoryで最後の要素だけを取得する流れです。

propertizeは、文字列に表示属性を付与する関数です。
ここでは'face 'boldを指定して、太字で表示しています。

2.1. 起動直後のゴミ表示を防ぐ

ところで、設定後にvtermを起動していたら、プロンプトの前に変な四角や余計な改行が表示される問題があることに気づきました。
ただし、バッファを切り替えて戻ると、この表示は追加されません。

起動時の表示問題を解決 問題 起動直後 変な四角や 余計な改行 原因 初期サイズ計算 のズレ 行番号表示の余白を 考慮していない 解決策 行番号表示を 無効化 display-line-numbers-mode -1

どうも、vtermの初期サイズ計算がずれていることが原因だったようです7
vtermは起動時に、ターミナルの行数と桁数を計算します。
このとき、行番号表示のような左側の余白が考慮されずに計算されると、描画がずれてゴミが残ります。
バッファを切り替えると、ウィンドウサイズが再通知されて再描画が走るため、ゴミがでなくなった、というわけです。

そこで、vtermバッファでは行番号表示を無効にしました。

(add-hook 'vterm-mode-hook
          (lambda ()
            (display-line-numbers-mode -1)))

display-line-numbers-mode-1を渡すと、その機能が無効になります。
vtermは純粋な端末の桁数を前提に描画するため、左側の余白幅が変動する機能とは相性が悪いのです。

3. 最終的な設定

以上の内容をまとめて、use-packageで記述すると次のようになります。

(use-package vterm
  :hook
  (vterm-mode . (lambda ()
                  (display-line-numbers-mode -1)
                  (setq-local cursor-type 'box
                              directory-tracking-enabled t)))
  :config
  (setq-default
   mode-line-buffer-identification
   (append mode-line-buffer-identification
           '((:eval
              (when (eq major-mode 'vterm-mode)
                (format " [%s]"
                        (propertize
                         (file-name-nondirectory
                          (directory-file-name default-directory))
                         'face 'bold))))))))Code language: PHP (php)

:hookキーワードで、vtermバッファが開かれたときに実行する処理を指定しています。
行番号表示を無効にし、カーソルの形状を箱型に設定し、ディレクトリ追跡を有効にする、という3つの設定をまとめています。

:configセクションでは、mode-lineの設定を行っています。
use-package:configは、パッケージのロード後に実行される部分です。

この設定により、vtermでcdコマンドを実行すると、mode-lineのバッファ名の右隣にカレントディレクトリ名が太字で表示されるようになります8
例えば、~/projects/myappに移動すると、mode-lineには[myapp]と表示されるようになりました。

  1. vterm_prompt_end関数を定義する前に、vterm_printf関数の定義も必要です。tmuxやscreen環境では特別なエスケープシーケンスのラップが必要になります。詳細は公式ドキュメントを参照してください。 – Directory tracking and Prompt tracking – emacs-libvterm
  2. zshでディレクトリ追跡を機能させるには、vterm_printf関数の定義に加えて、PROMPT変数への追加とsetopt PROMPT_SUBSTの設定も必要です。完全な設定例については公式リポジトリのサンプルファイルを参照してください。 – emacs-vterm-zsh.sh – emacs-libvterm
  3. リモートサーバーで使用する場合、$(hostname)が返す値が実際の接続ホスト名と異なることがあります。その場合は手動で正しいホスト名を指定する必要があります(例:HOSTNAME=correct-hostname)。 – Directory tracking and Prompt tracking – emacs-libvterm
  4. PROMPT_COMMANDで複数のコマンドを実行する場合、実行順序に注意が必要です。vterm_prompt_endは最後に実行されるべきなので、既存のコマンドの後ろに追加することを推奨します。 – PROMPT_COMMAND – Bash Reference Manual
  5. directory-tracking-enabledはvtermのドキュメントには明示的に記載されていない変数ですが、default-directoryの更新を制御するために使用されます。vtermの内部実装では、シェル側から送信された51;Aエスケープシーケンスを解釈してdefault-directoryを更新します。 – vterm.el – emacs-libvterm
  6. mode-lineのカスタマイズ方法は多様で、ここで紹介する方法以外にもモードライン全体を再構成する方法や、別のパッケージを使用する方法があります。基本的なカスタマイズについてはEmacsマニュアルを参照してください。 – Mode Line Format – GNU Emacs Manual
  7. この問題はvtermが端末のサイズを計算する際に、表示領域の幅を正確に把握できないことが原因です。他にもfringe-modeやwindow-marginsなど、ウィンドウの実効幅に影響する設定がある場合も同様の問題が発生する可能性があります。 – vterm initial display issues – GitHub Issues
  8. この設定例ではシェル側の設定が含まれていません。実際に動作させるには、.zshrcまたは.bashrcに前述のvterm_printf関数とvterm_prompt_end関数の定義、およびprecmdまたはPROMPT_COMMANDの設定が必要です。完全な設定例は公式リポジトリのetc/ディレクトリにあります。 – etc/emacs-vterm-zsh.sh – emacs-libvterm