ターミナルの中の Emacs

2023年12月2日 2023年12月13日

こんにちは。今年もこの季節がやってきました。 この記事は Emacs Advent Calendar 2023 の3日目の記事です。

Emacs にはGUI版とCUI版があり、GUI版はCUI版よりできることが多いため、私はローカル端末ではGUI版を使っています。

しかし、サーバや計算機マシンを利用する場合は、ターミナルから ssh で接続していますので、GUI版を使えません。Tramp を使えばGUI版からアクセスできますが、私は、以下の理由から Tramp を使っていません(使えていません…)。

  • language server と上手く接続できない。なぜ…
  • ローカル環境と同じ感覚で操作ができない。ghq の連携とか…
  • 突然ハングアップする。原因が特定できず…

私のEmacs力が足りないだけなのですが、Tramp を上手く使っている方は Advent Calendar 等で記事にしていただけるととても嬉しいです!

ということで、ssh越しにサーバや計算機マシンで Emacs を快適に(CUI版をGUI版と同様の操作感で)扱うために色々設定しているので、本記事では、それらの設定を紹介していきます。

ターミナルエミュレータ

選択

まず、どのターミナルエミュレータで Emacs を使うかです。私は ローカル端末の OS として、 macOS と Linux を利用するため、マルチプラットフォームであると設定の管理が楽になります。

マルチプラットフォームの選択肢には色々なターミナルエミュレータ(Kitty、Alacritty、WezTerm 等)がありますが、CUI版Emacs と上手く連携するために、私は細やかな設定ができる WezTerm を選択しました。もしかしたら、他のターミナルエミュレータでも同様の設定ができるかもしれません

キー入力

OS ごとに一部のキーの書き方が変わるので、この記事では、以下の通りに対応させます。

CUI版Emacsの割り当てWindowsmacOSLinux今回の書き方
ControlControlControlControlControl、またはCtrl
SuperWindowsCommandSuperCommand、またはCmd
MetaAltOptionAltOption、またはOpt

Metaキーの割り当て

私は、GUI版Emacs では、CommandキーをMetaキーとして送信しており、CUI版Emacs でも同様にしたいので、CUI版Emacs でMetaキーとして使いたい組み合わせを以下のように WezTerm に設定していきます。

~/.config/wezterm/wezerm.lua
local wezterm = require 'wezterm';

local keys = {
  {key="s",mods="CMD",action=wezterm.action.SendKey{key="s", mods="OPT"}},
  {key="x",mods="CMD",action=wezterm.action.SendKey{key="x", mods="OPT"}},
  {key="w",mods="CMD",action=wezterm.action.SendKey{key="w", mods="OPT"}},
  {key="y",mods="CMD",action=wezterm.action.SendKey{key="y", mods="OPT"}},
  {key="i",mods="CMD",action=wezterm.action.SendKey{key="i", mods="OPT"}},
  {key=",",mods="CMD",action=wezterm.action.SendKey{key=",", mods="OPT"}},
  {key=".",mods="CMD",action=wezterm.action.SendKey{key=".", mods="OPT"}},
  {key=";",mods="CMD",action=wezterm.action.SendKey{key=";", mods="OPT"}},
  {key="/",mods="CMD",action=wezterm.action.SendKey{key="/", mods="OPT"}},
  {key="<",mods="CMD|SHIFT",action=wezterm.action.SendKey{key="<", mods="OPT"}},
  {key=">",mods="CMD|SHIFT",action=wezterm.action.SendKey{key=">", mods="OPT"}},
  {key="?",mods="CMD|SHIFT",action=wezterm.action.SendKey{key="?", mods="OPT"}},
}

return {
  keys=keys
}

適宜自分が必要なMetaキーの組み合わせを追加します。M-x を使うために {key="x",mods="CMD",action=wezterm.action.SendKey{key="x", mods="OPT"}} の設定は必須ですね。

Ctrlキーの割り当て

Emacs では Ctrlキーと何かしらのキーの組み合わせ(C-a 等)を多用しますが、ターミナルエミュレータ上ではCtrlキーと組み合わせて送信できないキーがあります。例えば、Ctrl-/Ctrl-; 等です。 なぜCtrlキーと特定のキーの組み合わせをターミナルエミュレータ上で送信することができないかは、以下の記事が分かりやすく解説してくれていますので、ぜひご覧になってください。簡単に言うと Control character が定義されていない組み合わせだからです。

そのため、何かしらの関数をこれらにバインドしている場合、GUI版Emacs では利用可能ですが、CUI版Emacs では利用できません。

この問題を解決するために、WezTerm の SendString アクション(特定のキー押下時に、任意の文字列を送信することができるアクション)を利用します。具体的な流れは以下の通りです。

  1. SendStringControl character が定義されていない組み合わせ(Ctrl-; 等) が押下された際に Ctrl-x @ {Ctrlキーと同時に押下したい文字} を送信する。

    例えば、Ctrl-; が押下されたら、Ctrl-x @ ; を送信するイメージです。

  2. Emacs の設定で Ctrl-x @ {Ctrlキーと同時に押下したい文字}に対して関数をバインドする。 例えば、Ctrl-x @ ; に関数をバインドするイメージです。

先ほどの WezTerm の設定に以下を追記します。

~/.config/wezterm/wezerm.lua
local keys = {
  (前述と同様のため省略)
  -- 以下では、"\x18" が "Ctrl-x" を意味します。
  {key=";",mods="CTRL",action=wezterm.action.SendString "\x18@;"}, -- for emacs in terminal
  {key=".",mods="CTRL",action=wezterm.action.SendString "\x18@."}, -- for emacs in terminal
}

日本語入力

私は日本語入力には SKK を利用しており、以下のように使いわけています。

  • Emacs での日本語入力: ddskk(Emacsでのみ使える)
  • Emacs 以外での日本語入力: AquaSKK(macOS), fcitx-skk(Linux)

この時に困るパターンが CUI版Emacs です。 WezTerm上で Emacs を起動している(CUI版Emacs を起動している)場合は ddskk が使いたいですし、WezTerm上でEmacs 以外の操作(shell操作やvim操作等)をしている時は AquaSKK や fcitx-skk を使いたいです。

そこで、以下のように対応をします。

  • WezTerm 内
    • Emacs: Ctrl-j で ddskk の日本語入力開始
    • Emacs 以外: Ctrl-Shift-j でAquaSKK(macOS), fcitx-skk(Linux)の日本語入力開始
  • WezTerm 外
    • Emacs: Ctrl-j でddskkの日本語入力開始
    • Emacs 以外: Ctrl-j でAquaSKK(macOS), fcitx-skk(Linux)の日本語入力開始

つまり、WezTerm で Emacs を使っていないパターンのみ Ctrl-Shift-j で 日本語入力を開始します(それ以外のパターンはCtrl-jで開始)。 WezTerm では、Emacs を起動している時ぐらいしか日本語を入力しないので、たまに必要な時のみ Ctrl-Shift-j を押下すれば問題ありません。

上記を実現するために、以下の2種類の設定をしています。

  • Emacsの設定 Ctrl-j のみではなく Ctrl-x @ j でも ddskk の日本語入力を開始できるようにします。

    ~/.config/emacs/init.el
    (use-package ddskk
      :ensure t
      :demand t
      :bind* ("C-j" . skk-kakutei)
      :bind ("C-x @ j". skk-kakutei) ;; for ctrl-j from wezterm
      :custom
      (default-input-method "japanese-skk")
      (skk-byte-compile-init-file t)
      :init
      (setq viper-mode nil))
  • WezTerm の設定 先ほど WezTerm の設定でも説明した手順と同様に、Ctrl-j が押下された際に Ctrl-x @ j を送信するように、以下設定を追記します。この変更をしないと、Ctrl-j で AquaSKK や fcitx-skk が起動してしまいます。

    ~/.config/wezterm/wezerm.lua
    local keys = {
     (前述と同様のため省略)
     {key="j",mods="CTRL",action=wezterm.action.SendString "\x18@j"},
    }

上記設定で上手く OS の IunputMethod(AwuaSKK や fcitx-skk) と ddskk を共存させることができました。

余談ですが、GUI版Emacs で ddskk を利用するために、AquaSKK や fcitx-skk では GUI版Emacs がアクティブ時には日本語入力を起動しないように設定しています。

24bit color 対応

EmacsのGUI版とCUI版で同じカラーテーマを使っていても表示されている色合いが異なる場合があります。 その原因はGUI版とCUI版で表示できるカラーが異なっているからです。 そこで、CUI版でもGUI版と同じカラーを表示できるようにターミナル上で 24bit color で表示できるように設定します。

24bit colorへの対応に関しては、以下の記事がとても参考になりますので、ぜひやってみてください。

ちなみに、私の最近のカラーテーマのお勧めは、ef-themesef-maris-dark です。

corfu-terminal

私は、Emacs の補完UIとして corfu を利用していますが、 corfu は補完候補を表示するために child-frame を使用しており、CUI版Emacs では同様の見た目にはなりません。そのため、CUI版Emacs では、corfu-terminal を使います。

~/.config/emacs/init.el
;; CUI版の時のみ corfu-terminal をロード
(use-package corfu-terminal
  :ensure t
  :if (not (display-graphic-p))
  :config
  (corfu-terminal-mode +1))

(use-package corfu
  :ensure t
  :custom ((corfu-auto t)
           (corfu-auto-prefix 1)
           (corfu-auto-delay 0)
           (corfu-cycle t))
  :init
  (global-corfu-mode)
  (corfu-popupinfo-mode))

これで、 CUI版Emacs でも同じように見えるようになりました。後述する nerd-icons と組み合わせることで、さらに見やすくなります。

nerd-icons

Emacs でアイコンを使う場合は、all-the-icons パッケージが有名です。しかし、こちらは GUI版Emacs でしか利用できません。

そこで、CUI版Emacs でもアイコンを使うために、Nerd Fonts のアイコンフォント(以下、nerd icons と記載)を使います。 先ほどのフォント設定の際に、Nerd Fonts が含まれているフォントを設定しているので、後は CUI版Emacs から nerd icons を呼び出してあげるだけです。

そのためのパッケージとして、nerd-icons.el があります。 もちろん、GUI版Emacs でも使えますので、CUI版及びGUI版Emacs で統一的にアイコンを利用することができます。

~/.config/emacs/init.el
;; nerd icon を扱えるように
(use-package nerd-icons :ensure t)

;; dired で nerd icon を表示
(use-package nerd-icons-dired
  :ensure t
  :hook (dired-mode-hook . nerd-icons-dired-mode))

;; completion で nerd icon を表示
(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  :config
  (nerd-icons-completion-mode)
  :hook (marginalia-mode-hook . #'nerd-icons-completion-marginalia-setup))

;; corfu で nerd icon を表示
(use-package nerd-icons-corfu
  :ensure t
  :after corfu
  :config
  (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))

また、他にも treemacsdirvish でも nerd icons を表示することができますので、ぜひRelated Packagesを参照してみてください。

クリップボードの共有

Emacs でのクリップボードといえば、 kill-ring (キルリング) です。 GUI版Emacs を利用している場合、OS自体のクリップボードと kill-ring の中身を共有してくれます。

しかし、CUI版Emacs では、クリップボードと kill-ring を共有することはできません。 状況を整理すると以下のようになります(Tmux を利用しているとさらに挙動は異なります)。

状況(macOSの場合)クリップボードへの保存kill-ring への保存
マウスで範囲選択をして、Cmd-c を押下する成功失敗
マウスで範囲選択をして、 M-w を押下する失敗失敗
Emacs でリージョン選択をして、Cmd-c を押下する失敗失敗
Emacs でリージョン選択をして、M-w を押下する失敗成功

クリップボードと kill-ring の両方に保存が成功している状況は一つもありませんね。問題を整理すると以下の3つになります。

  • マウスの範囲選択はあくまでも WezTerm の画面の範囲選択なので、 CUI版Emacs のリージョン選択とは異なる。
  • Cmd-c に WezTerm のコピーアクションが割り当たっている。Emacs では Cmd-cM-c を割り当てたい。
  • クリップボード と kill-ring が共有されない。

では、1つずつ問題を簡単な順につぶしていきましょう。

「マウスの範囲選択はあくまでも WezTerm の画面の範囲選択なので、 CUI版Emacs のリージョン選択とは異なる。」問題

これは、Emacs でマウスを使わなければ発生しないので、それで対処完了です。簡単ですね。 もしマウスの範囲選択を共存させたいとなると、ターミナルと Emacs の密な連携が必要になりとても大変だと思うので、個人的にはマウスを使わない一択です。

Cmd-c に WezTerm のコピーアクションが割り当たっている。Emacs では Cmd-cM-c を割り当てたい。」問題

私の用途では、正確には左側の Cmd-cM-c を割り当てたいです。 そのため、左側の Cmd-cM-c に、右側の Cmd-c に WezTerm のコピーアクションを割り当てます。併せて、左側の Cmd-vM-v を、右側の Cmd-cに WezTerm のペーストアクションを割り当てます。

つまり、OS由来?のショートカットは右側のCommandキーのみで操作するようにします。

では、以下から設定していきます。

WezTerm では、 左右のComandキーを区別することができないため、他のキーカスタマイズアプリケーションを組みあわせて対応します。私は、キーカスタマイズアプリケーションとして、macOS であれば Karabiner-Elements、Linux であれば xremap を利用しています。

今回は macOS時の設定を記載していきます。

方針として、Karabiner-Elements で右のCommandキーをoptionキーに変更し、WezTerm で option + v にweztermのペーストアクション等を割り当てます。 以下のルールを ~/.config/karabiner/karabiner.jsonrules に追記することで、WezTerm がアクティブな時のみ右のCommanddキーがoptionキーになります。

~/.config/karabiner/karabiner.json
{
    "title": "WezTermで右側のCommandキーをOptionに変更",
    "rules": [
        {
            "description": "WezTermで右側のCommandキーをOptionに",
            "manipulators": [
                {
                    "type": "basic",
                    "conditions": [
                        {
                            "type": "frontmost_application_if",
                            "bundle_identifiers": ["^com\\.github\\.wez\\.wezterm$"]
                        }
                    ],
                    "from": {
                        "key_code": "right_command",
                        "modifiers": {
                            "optional": ["any"]
                        }
                    },
                    "to": [
                        {
                            "key_code": "right_option"
                        }
                    ]
                }
            ]
        }
    ]
}

次に、WezTerm の設定です。先ほどの WezTerm の設定ファイルに以下を追記します。

~/.config/wezterm/wezterm.lua
local keys = {
  (前述と同様のため省略)
  {key="c",mods="CMD",action=wezterm.action.SendKey{key="c", mods="OPT"}}, -- "Cmd-c"押下で"Opt-c"キーを送信
  {key="v",mods="CMD",action=wezterm.action.SendKey{key="v", mods="OPT"}}, -- "Cmd-v"押下で"Opt-v"キーを送信
  {key="v",mods="OPT",action=wezterm.action.PasteFrom 'Clipboard'},        -- "OPT-v"押下で WezTerm のペーストアクションを割り当て
}

これで、左側のCmd-cM-cを、左側のCmd-vM-vを、右側のCmd-vクリップボードからのコピーを実施できるようになりました。

「クリップボード と kill-ring が共有されない。」問題

先ほどの対応でクリップボードから CUI版Emacs にペーストできるようになったので、ここでは kill-ring に登録された文字列をクリップボードに保存できるようにします。

上記を実現するために、OSC 52 を使います。 OSC 52Operating System Command の一種で、ターミナルエミュレータとホストシステム間でクリップボードの内容を転送するために使用されます。ターミナルエミュレータはOSC 52エスケープシーケンスを受信すると、そのシーケンスに含まれるデータをローカルマシンのクリップボードに転送します。 すべてのターミナルエミュレータが OSC 52 をサポートしているわけではありませんが、 WezTerm ではサポートされています。

OSC 52の詳細については、以下のページが参考になると思います。

方針としては、以下の通りです。

  1. kill ring に直近保存されている文字列を base64 でエンコード
  2. エンコードした文字列をOSC52エスケープシーケンスと組み合せて、CUI版Emacs からターミナルエミュレータに送信

上記を実行する関数がHave Vim / Emacs / Tmux use System Clipboardで紹介されています。

私の環境(WezTerm + tmux + emacs / WezTerm + ssh + tmux + emacs)では以下のように修正した関数で問題ありませんでした。

~/.config/emacs/init.el
(defun yank-to-clipboard ()
  "Copy the most recently killed text to the system clipboard with OSC 52."
  (interactive)
  (let ((base64_text (base64-encode-string (encode-coding-string (substring-no-properties (nth 0 kill-ring)) 'utf-8) t)))
   (send-string-to-terminal (format "\033]52;c;%s\a" base64_text))))

kill ring に保存した後に、 M-x yank-to-clipboard を実行すると、クリップボードに kill ring の直近の文字列が保存されます。

もちろん、ssh越しでもローカル端末のクリップボードと共有できますので、便利です。sshを多用しているので、この恩恵がとても凄いです。

私は都度 M-x yank-to-clipboard を実行することが面倒だったため、以下の関数を作成して M-w に割り当てています。

  1. リージョン内の文字列をselected-text変数に保存
  2. kill ring への保存
    • GUI版Emacs の場合は、selected-text変数を kill ringに保存。
    • CUI版Emacs の場合は、selected-text変数を kill ringに保存するとともに base64 でエンコードしてOSC52エスケープシーケンスと組み合せてターミナルに送信
~/.config/emacs/init.el
(use-package emacs
  :bind ("M-w" . region-to-clipboard)
  :config
  (defun region-to-clipboard ()
    "Copy the selected region to both the kill-ring and clipboard with OSC 52."
    (interactive)
    (if (region-active-p)
        (let* ((selected-text (buffer-substring-no-properties (region-beginning) (region-end)))
               (base64_text (base64-encode-string (encode-coding-string selected-text 'utf-8) t)))
          (if (display-graphic-p)
              (clipboard-kill-ring-save (region-beginning) (region-end))
            (kill-new selected-text)
            (send-string-to-terminal (format "\033]52;c;%s\a" base64_text))))
      (message "No region selected."))))

OSC 52 の注意点

OSC 52 はとても便利ですが、クリップボードの内容を変更するため、セキュリティの観点から懸念が生じることがあります。例えば、不正なテキストがクリップボードに挿入されるリスクがあります。 そのため、セキュリティ上のリスクを理解し、信頼できる環境でのみ使用したほうが良いと思います。

また、ターミナルエミュレータやアプリケーション、ツール等によって、OSC 52 で送信できる最大長が制限される場合があるので、大量の文字列を扱う場合も注意が必要です。

終わりに

これらの設定でターミナルでも GUI版と同様に Emacs を快適に使えるようになりました。 今回は簡単のために、いくつか設定を省略しているので、設定しても同じような挙動にならない等あれば、ぜひコメントや X 等でご連絡ください。

また、今回の設定を含む私の設定が以下にありますので、よろしければ参考にしてください。

それでは、すべてのEmacsユーザに幸あれ!

謝辞

今回紹介させていただいたパッケージや参考にさせていただきましたサイトの作者、ありがとうございます。 不都合等ありましたら、コメント等でご連絡いただけますと幸いです。

Posted by mako
関連記事
コメント
...