Skip to content

Commit c42451e

Browse files
committed
Fix Meta keys not reaching terminal in TTY Emacs
TTY Emacs delivers M-<key> as two events (ESC then <key>) via esc-map. When the binding fires, last-command-event is just <key> with no meta modifier — the ESC was consumed by the prefix lookup. GUI Emacs delivers the composite single event with meta intact, which is why the earlier fix only worked there. Detect the esc-prefix path in ghostel--send-event via this-command-keys-vector and re-inject meta. Also bind M-DEL so TTY Alt-Backspace ([27 127]) routes through send-event instead of falling through to global backward-kill-word. Follow-up to 43220db. Fix #48.
1 parent 266e3e9 commit c42451e

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

lisp/ghostel.el

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,11 @@ is non-nil.")
10371037
(let ((key-str (format "M-%c" c)))
10381038
(unless (member key-str ghostel-keymap-exceptions)
10391039
(define-key map (kbd key-str) #'ghostel--send-event))))
1040+
;; M-DEL: TTY Emacs delivers Alt-Backspace as ESC + 0x7f, which
1041+
;; resolves to ?\M-\d. The `M-<backspace>' form above only covers
1042+
;; the `[M-backspace]' symbol path; without this binding, TTY
1043+
;; Alt-Backspace falls through to global `backward-kill-word'.
1044+
(define-key map (kbd "M-DEL") #'ghostel--send-event)
10401045
;; C-@ (NUL, same as C-SPC) — used by programs like Emacs-in-terminal
10411046
(define-key map (kbd "C-@")
10421047
(lambda () (interactive) (ghostel--send-string "\x00")))
@@ -1282,12 +1287,21 @@ paste, yank, drop."
12821287
"Send the current key event to the terminal via the key encoder.
12831288
Extracts the base key name and modifiers from `last-command-event'
12841289
and routes through the ghostty key encoder, which respects terminal
1285-
modes (application cursor keys, Kitty keyboard protocol, etc.)."
1290+
modes (application cursor keys, Kitty keyboard protocol, etc.).
1291+
1292+
In TTY Emacs, `M-<key>' arrives as two events (ESC then <key>) via
1293+
`esc-map'; `last-command-event' is just <key> and has no meta bit.
1294+
Detect that case via `this-command-keys-vector' and re-inject meta."
12861295
(interactive)
12871296
(ghostel--snap-to-input)
12881297
(let* ((event last-command-event)
1298+
(keys (this-command-keys-vector))
1299+
(via-esc (and (> (length keys) 1) (eq (aref keys 0) 27)))
12891300
(base (event-basic-type event))
12901301
(mods (event-modifiers event))
1302+
(mods (if (and via-esc (not (memq 'meta mods)))
1303+
(cons 'meta mods)
1304+
mods))
12911305
(key-name (cond
12921306
;; backtab is Emacs's name for S-TAB
12931307
((eq base 'backtab) "tab")

test/ghostel-test.el

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5955,7 +5955,36 @@ rather than the selected window's buffer."
59555955
(should-not (eq binding #'ghostel--send-event))
59565956
(should (eq binding #'ghostel--send-event))))))
59575957
;; M-y should be bound to ghostel-yank-pop, not send-event
5958-
(should (eq (lookup-key ghostel-mode-map (kbd "M-y")) #'ghostel-yank-pop)))
5958+
(should (eq (lookup-key ghostel-mode-map (kbd "M-y")) #'ghostel-yank-pop))
5959+
;; M-DEL must be bound so TTY Alt-Backspace ([27 127]) routes through
5960+
;; ghostel--send-event instead of global backward-kill-word.
5961+
(should (eq (lookup-key ghostel-mode-map (kbd "M-DEL")) #'ghostel--send-event)))
5962+
5963+
(ert-deftest ghostel-test-send-event-tty-esc-prefix ()
5964+
"Re-inject meta when the key arrives via ESC prefix (TTY Emacs).
5965+
In TTY Emacs, M-<key> is delivered as two events ([27 KEY]) via
5966+
`esc-map'. `last-command-event' is just KEY with no meta modifier,
5967+
but `this-command-keys-vector' retains the ESC prefix."
5968+
(let (captured-key captured-mods)
5969+
(cl-letf (((symbol-function 'ghostel--send-encoded)
5970+
(lambda (key mods &optional _utf8)
5971+
(setq captured-key key captured-mods mods))))
5972+
(cl-flet ((sim-tty (keys-vec event expected-key expected-mods)
5973+
(setq captured-key nil captured-mods nil)
5974+
(cl-letf (((symbol-function 'this-command-keys-vector)
5975+
(lambda () keys-vec)))
5976+
(let ((last-command-event event))
5977+
(ghostel--send-event)))
5978+
(should (equal expected-key captured-key))
5979+
(should (equal expected-mods captured-mods))))
5980+
;; M-b in TTY: ESC then b → re-inject meta
5981+
(sim-tty (vector 27 ?b) ?b "b" "meta")
5982+
(sim-tty (vector 27 ?f) ?f "f" "meta")
5983+
(sim-tty (vector 27 ?d) ?d "d" "meta")
5984+
;; M-DEL in TTY: ESC then 127 → backspace + meta
5985+
(sim-tty (vector 27 127) 127 "backspace" "meta")
5986+
;; Already-meta event (shouldn't double-add meta)
5987+
(sim-tty (vector 27 ?b) (aref (kbd "M-b") 0) "b" "meta")))))
59595988

59605989
;; -----------------------------------------------------------------------
59615990
;; Test: ghostel-yank-pop DWIM
@@ -7004,6 +7033,7 @@ while :; do sleep 0.1; done'\n")
70047033
ghostel-test-c-g-exits-copy-mode
70057034
ghostel-test-inhibit-quit
70067035
ghostel-test-meta-key-bindings
7036+
ghostel-test-send-event-tty-esc-prefix
70077037
ghostel-test-yank-pop-after-yank
70087038
ghostel-test-yank-pop-no-preceding-yank
70097039
ghostel-test-copy-mode-recenter

0 commit comments

Comments
 (0)