vterm-modeを使う

GUIEmacsとターミナルを切替えるのが面倒な事があるので、Emacsでもターミナルを使えるようにした。あまり癖のなさそうな、だだのターミナルエミュレーターとして動いてくれる vterm-mode (Emacs-libvterm) を使ってみる事にした。npm installとかの画面書き替えが激しく発生するような時も表示が崩れず優秀。トグルでさっと表示できるので便利。

GitHub - akermu/emacs-libvterm: Emacs libvterm integration

f:id:naokton:20201208150555p:plain
Emacsのvterm-modeでVimを起動したところ

vtermについては先日のEmacs Conf 2020で発表があった。いい感じなのが分かると思う。

EmacsConf - 2020 - talks - A tour of vterm

vterm の設定

vterm本体

(leaf vterm
  ;; requirements: brew install cmake libvterm libtool
  :ensure t
  :custom
  (vterm-max-scrollback . 10000)
  (vterm-buffer-name-string . "vterm: %s")
  ;; delete "C-h", add <f1> and <f2>
  (vterm-keymap-exceptions
   . '("<f1>" "<f2>" "C-c" "C-x" "C-u" "C-g" "C-l" "M-x" "M-o" "C-v" "M-v" "C-y" "M-y"))
  :config
  ;; Workaround of not working counsel-yank-pop
  ;; https://github.com/akermu/emacs-libvterm#counsel-yank-pop-doesnt-work
  (defun my/vterm-counsel-yank-pop-action (orig-fun &rest args)
    (if (equal major-mode 'vterm-mode)
        (let ((inhibit-read-only t)
              (yank-undo-function (lambda (_start _end) (vterm-undo))))
          (cl-letf (((symbol-function 'insert-for-yank)
                     (lambda (str) (vterm-send-string str t))))
            (apply orig-fun args)))
      (apply orig-fun args)))
  (advice-add 'counsel-yank-pop-action :around #'my/vterm-counsel-yank-pop-action))

vterm-keymap-exceptions は、Emacs側でPrefixキーバインドとして使いたいキーバインドを指定する。F1help-command として、F2 は後ろで出てくるtoggleで使うので、追加。C-h は元々prefixとして使っておらず delete として使いたいので、削除。このリストにあるキーバインドはシェルに渡らない事になるが、たとえば C-c, C-g, C-uC-c C-[cgu] で入力できるので困らない。他のキーバインドの事は知らない。

vterm-buffer-name-string は、vtermバッファーのバッファー名。%s はシェルの方で発行したウィンドウタイトル用の文字が入る。後記するシェルの設定をすると、ホスト名:パス となる。シェル内でディレクトリ移動すると追従する。

あと counselのyankがうまく動かないのでそのワークアラウンド

vterm-toggle の設定

vtermをトグル表示したりproject対応したりする拡張。

GitHub - jixiuf/vterm-toggle: toggles between the vterm buffer and whatever buffer you are editing.

(leaf vterm-toggle
  :ensure t
  :custom
  (vterm-toggle-scope . 'project)
  :config
  ;; Show vterm buffer in the window located at bottom
  (add-to-list 'display-buffer-alist
               '((lambda(bufname _) (with-current-buffer bufname (equal major-mode 'vterm-mode)))
                 (display-buffer-reuse-window display-buffer-in-direction)
                 (direction . bottom)
                 (reusable-frames . visible)
                 (window-height . 0.4)))
  ;; Above display config affects all vterm command, not only vterm-toggle
  (defun my/vterm-new-buffer-in-current-window()
    (interactive)
    (let ((display-buffer-alist nil))
            (vterm)))
  )

vterm-toggle-scope で、プロジェクト単位で1つのバッファーを作るように指定。デフォルトだとどのファイルを開いていても同じバッファが表示される。

display-buffer-alist の設定は、トグルしたウィンドウを常に画面下からポップアップするようにしている。vterm-toggle せずに直接 vterm した場合にも効いてしまうので、「このウィンドウに新しく vtermバッファーを作って表示したい」という事ができなくなる。そのため、display-buffer-alist を無効にしてvterm実行する関数を定義している。

projectile の設定

vtermを横串で移動したい要望との兼ね合いで、projectile-next-buffer/prev-buffer の対象から vtermバッファーを外す。

(leaf projectile
  :ensure t counsel-projectile
  :require t
  :config
  (projectile-mode +1)
  :defer-config
  (customize-set-variable 'projectile-globally-ignored-modes
                          (let ((newlist projectile-globally-ignored-modes))
                            (add-to-list 'newlist "vterm-mode"))))

キーバインド

関連しそうなprojectileの設定も合わせて記載。

(leaf vterm
  :bind
  ("<f2>" . vterm-toggle)
  (vterm-mode-map
   ("C-<f2>" . my/vterm-new-buffer-in-current-window)
   ("C-<return>" . vterm-toggle-insert-cd)
   ([remap projectile-previous-project-buffer] . vterm-toggle-forward)
   ([remap projectile-next-project-buffer] . vterm-toggle-backward)))

(leaf projectile
  :bind
  (projectile-mode-map
   ("C-." . projectile-next-project-buffer)
   ("C-," . projectile-previous-project-buffer)))

vtermバッファー上で C-. or C-, すると、vtermバッファーを順次切替えて表示。その他のバッファー上では、project内のファイル(vtermを除く)を順次切替え表示。

シェルの設定 (zsh)

vterm-mode用のシェルの設定

if [[ "$INSIDE_EMACS" = 'vterm' ]] \
    && [[ -n ${EMACS_VTERM_PATH} ]] \
    && [[ -f ${EMACS_VTERM_PATH}/etc/emacs-vterm-zsh.sh ]]; then
    source ${EMACS_VTERM_PATH}/etc/emacs-vterm-zsh.sh
    # Initialize TITLE
    print -Pn "\e]2;%m:%2~\a"
fi

elispのインストールディレクトリに、vtermとシェルの連携のための設定が置いてあるので、読み込んでいる。ただし一点、vtermバッファーのタイトルが、一度どこかにcdしないと表示されなかったので、title用のエスケープシーケンスを個別に出力(print)している。

最低限の操作

<f2> (vterm-toggle) でvterm起動/バッファのトグル(ぱかぱか開いたり閉じたりするので楽しい)。違うprojectのファイルを表示してトグル操作すると、そのprojectのディレクトリでvtermバッファーが作られる。同じプロジェクトの別ファイルからは、既存のバッファーが開かれる。

プロジェクト内で複数のターミナルが欲しいときは、vtermバッファーで C-<f2> で好きなだけ作る。

複数vtermバッファーがある時、vtermバッファーで C-, or C-. するとvtermバッファーを切替えられる。これはプロジェクト関係なく全vtermバッファーを横串。

普通のバッファーのようにカーソル移動やスクロールしたり、region選択してyankしたりしたいときは、C-c C-tvterm-copy-mode に入る。戻るときも C-c C-t

EmacsのPrefixを優先させたキーバインドのうち、ターミナル側でよく使うものは以下対応になっている。C-c prefixつけるだけなので大丈夫(?)。

  • C-c => C-c C-c
  • C-g => C-c C-g
  • C-u => C-c C-u

だいたいこれぐらいで、不自由なく操作できる。さすがにずっと使っていると反応遅いなーと感じる事が度々あるけれど、それぐらい。無視できないような機能的問題はあまりなさそう。

ターミナルでEmacs

ターミナルでEmacsを使う事も考えたんだけど、tmuxとのキーバインドバッティングをどうしたらよいか考えるのが面倒であきらめた。いい設定があれば、そっちも試してみたい。特にGUI Emacsを使いたい理由はない。