ターミナルを Ghostty に乗り換えてから、Emacs で C-_(undo)を押すと
C-_ is undefined
と出るようになりました。Terminal.app では普通に効いていたので、エディタ側の問題ではなくターミナル側のキー送信が変わっているはず — その正体と直し方の話です。
何が起きていたか
Ghostty はデフォルトで kitty keyboard protocol(および modifyOtherKeys)を有効化してキー入力をエンコードします。これは「修飾キー込みでの全キー」をエスケープシーケンスで明示的に伝える仕組みで、TUI 側が対応していれば Ctrl+i と Tab を区別できる、Shift 単独の検知ができる、といったメリットがあります。
ところが古めの Emacs(あるいは extended protocol 非対応モードで起動した Emacs)は、そのエスケープシーケンスを解釈できません。Terminal.app が昔から送ってきた 生バイト 0x1F だけを C-_ → undo として認識する設定になっているので、新しいエンコードに切り替わった瞬間にバインドが行方不明になり、“C-_ is undefined” という結果になっていました。
直し方:Ctrl+Shift+Minus を 0x1F 直送に上書き
~/.config/ghostty/config に 1 行:
# Emacs の C-_ (undo) を生バイト 0x1F で送る。
# Ghostty デフォルトは modifyOtherKeys / kitty keyboard protocol で
# エスケープシーケンス化するので、emacs (拡張 protocol 未対応モード)
# が認識せず "C-_ is undefined" になる。Terminal.app と同じ古い
# encoding に揃える上書き。
keybind = ctrl+shift+minus=text:\x1f
text:\x1f は「リテラルのバイト 0x1F を送る」という意味の Ghostty 拡張アクション。 これで Ctrl+Shift+- (= C-_)を打つと、エンコードされた kitty/modifyOtherKeys のシーケンスではなく、Emacs が期待している裸の 0x1F がそのまま流れます。Emacs 側は何も変えていません。
なぜ 0x1F なのか
ASCII で _ は 0x5F。一般的な US 配列では _ は Shift+-。これに Ctrl を合わせた “control character” がちょうど 0x1F(最上位の方を 0 にして 0x40 を引いた値)になります。Terminal.app は Ctrl+Shift+- を踏むと そのまま 0x1F 1 バイトだけ を送るので、Emacs はそれを (undefined-key 0x1f) ではなく C-_ として受け取り、undo にディスパッチできる、というだけのことでした。
いつこの修正は不要か
- Emacs 側で kitty keyboard protocol を有効にしている場合(
kkp.elや Emacs 30 系の対応)は、エスケープシーケンスを Emacs 自身が解釈できるので不要 - Terminal.app をそのまま使う、Alacritty や WezTerm でも legacy encoding にしている、といった環境では発生しない
逆に、kitty / Ghostty / WezTerm modern モードに乗り換えて Emacs の C-/ や C-_ が突然効かなくなったら、まずこの「ターミナル側がエスケープ化してエディタが復号できていない」を疑うのが早いです。同じ理由で他の C- 系バインドが拾われない場合も、Ghostty の keybind = ... = text:\xNN で個別に逃がせます。