メインコンテンツへスキップ

ターミナルの中のEmacs

··4 分·
Terminal Emacs
Makoto Morinaga
著者
Makoto Morinaga
技術メモ、コーディング、環境構築のための個人ノート。
目次

こんにちは。今年もこの季節がやってきました。 この記事は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の割り当て Windows macOS Linux 今回の書き方
Control Control Control Control Control、またはCtrl
Super Windows Command Super Command、またはCmd
Meta Alt Option Alt Option、またはOpt

Metaキーの割り当て
#

私は、GUI版Emacsでは、CommandキーをMetaキーとして送信しており、CUI版Emacsでも同様にしたいので、CUI版EmacsでMetaキーとして使いたい組み合わせを ~/.config/wezterm/wezerm.lua に設定します。

~/.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 @ ; に関数をバインドするイメージです。

先ほどの ~/.config/wezterm/wezerm.lua に以下を追記します。

~/.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の ~/.config/emacs/init.el 設定 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の ~/.config/wezterm/wezerm.lua 設定 先ほど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-themesのef-maris-darkです。

corfu-terminal
#

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

emacs-lisp
;; 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で統一的にアイコンを利用することができます。

emacs-lisp
;; 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))

また、他にもtreemacsやdirvishでも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-v にWezTermのペーストアクションを割り当てます。

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

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

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

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

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

~/.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 52はOperating 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)では以下のように修正した関数で問題ありませんでした。

emacs-lisp
(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エスケープシーケンスと組み合せてターミナルに送信
emacs-lisp
(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ユーザに幸あれ!

謝辞
#

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

関連記事

EmacsでのlspをベースとしたPython開発環境
··4 分
Emacs Lsp Python
EmacsでのElpyをベースとしたPython開発環境
··3 分
Elpy Python Emacs
EmacsからRuffを使う
··2 分
Python Ruff Emacs