Skip to content

Encode modified character keys through the kitty keyboard protocol - #583

Merged
dakra merged 1 commit into
mainfrom
fix/kitty-modified-char-keys
Jul 29, 2026
Merged

dakra merged 1 commit into
mainfrom
fix/kitty-modified-char-keys

Conversation

@dakra

@dakra dakra commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #579

Problem

When a TUI enables the kitty keyboard protocol, ghostel still encoded modified character keys with legacy sequences: M-<letter> went out as ESC <letter> (rejected as ambiguous by kitty-mode programs) and C-M-<letter> dropped meta entirely, sending the bare control byte. Modified functional keys (backspace, return, arrows) already encoded correctly.

Root cause

The native encoder built libghostty key events with only key and mods. libghostty resolves character keys through unshifted_codepoint and the generated text (utf8), so every modified character key produced no output at all — in both kitty and legacy modes — and fell through to the kitty-unaware elisp fallback ghostel--raw-key-sequence, whose ctrl branch also drops meta. Additionally, macos_option_as_alt defaulted to false on macOS builds, suppressing alt handling even with the codepoint present.

Fix

  • input.keyEvent builder: single printable-ASCII keys carry their unshifted codepoint and the character as generated text; uppercase names fold to the lowercase codepoint with shift as a consumed modifier. Named/functional keys are untouched.
  • macos_option_as_alt = .true: Emacs resolves option-vs-meta before the event reaches the module, so meta always means alt.
  • C-<letter>, C-@, C-SPC, C-\ and ghostel-send-C-g now route through the encoder instead of raw-byte closures — legacy bytes are provably identical (libghostty's ctrlSeq yields the same C0 codes), kitty mode gets CSI-u.
  • Raw C0 events 13/9/27 are remapped to return/tab/escape in ghostel--send-event (with the artifact ctrl modifier stripped): a TTY delivers RET as C-m, which the encoder would otherwise fixterms-encode as CSI-u and break Enter/Tab.
  • Uppercase chords (delivered as lowercase base + shift) restore their case: M-T sends ESC T in legacy mode and reports the shifted alternate (\e[116:84;4u) under kitty flags=5.
  • ghostel-send-C-c/-C-z/-C-d deliberately stay raw bytes so SIGINT/SIGTSTP/EOF reach the tty line discipline even while a hung program holds kitty mode.
  • The elisp fallback's ctrl branch now ESC-prefixes when meta is set, so it no longer silently drops the modifier either.

API note: ghostel-send-key with "m"/"i"/"[" plus ctrl now yields the fixterms CSI-u sequence (matching upstream ghostty) instead of a C0 byte; live keystrokes are unaffected thanks to the C0 remap.

Testing

  • 8 zig unit tests against the vendored encoder (kitty CSI-u, plain-text passthrough, legacy sequences, fixterms cases); src/input.zig is now emacs-free and part of zig build test.
  • 6 native PTY byte-level ERT tests (kitty and legacy, meta/ctrl/ctrl-meta/shift chords) plus fallback, sim, and rescue-byte elisp tests.
  • Live-verified in six sandboxed Emacs sessions with real keystrokes (TTY and GUI-shaped events): the issue's exact repro (printf '\e[>1u'; cat -v), TTY Enter/Tab/arrows, ESC-prefixed meta chords, readline editing, less/vi, and the rescue hatches interrupting a program that holds kitty mode.
  • make -j8 all green.

@dakra
dakra force-pushed the fix/kitty-modified-char-keys branch from 418635c to 88f08f3 Compare July 29, 2026 14:25
The native encoder built libghostty key events with only key and mods.
libghostty resolves character keys through the unshifted codepoint and
generated text, so every modified character key produced no output in
both kitty and legacy modes and fell through to the kitty-unaware
elisp fallback: M-<letter> went out as legacy ESC <letter> even when
the program had pushed kitty flags, and C-M-<letter> lost meta
entirely, sending the bare control byte.

- Build key events in input.keyEvent: single printable-ASCII keys
  carry their unshifted codepoint and the character as generated text;
  uppercase names fold to the lowercase codepoint with shift as a
  consumed modifier.  Named/functional keys are unchanged.
- Set macos_option_as_alt: Emacs resolves option-vs-meta before the
  event reaches the module, so meta always means alt.
- Route C-<letter>, C-@, C-SPC, C-\ and ghostel-send-C-g through the
  encoder instead of raw-byte closures; legacy bytes are identical,
  kitty mode now gets CSI-u.
- Remap raw C0 events 13/9/27 in ghostel--send-event to
  return/tab/escape and drop the artifact ctrl modifier: a TTY
  delivers RET as C-m, which the encoder would otherwise
  fixterms-encode as CSI-u and break Enter/Tab.
- Restore case for uppercase chords (lowercase base + shift): M-T
  sends ESC T in legacy mode and reports the shifted alternate under
  kitty flags.
- Keep ghostel-send-C-c/-C-z/-C-d as raw bytes so SIGINT/SIGTSTP/EOF
  reach the line discipline even while a hung program holds kitty
  mode.
- ESC-prefix the elisp fallback's C0 byte when meta is set so that
  path no longer drops the modifier either.

API note: ghostel-send-key with "m"/"i"/"[" plus ctrl now yields the
fixterms CSI-u sequence (matching ghostty) instead of a C0 byte; live
keystrokes are unaffected thanks to the C0 remap above.

Closes #579
@dakra
dakra force-pushed the fix/kitty-modified-char-keys branch from 88f08f3 to fbadad2 Compare July 29, 2026 15:18
@dakra
dakra merged commit fbadad2 into main Jul 29, 2026
23 checks passed
@dakra
dakra deleted the fix/kitty-modified-char-keys branch July 29, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Modified character keys bypass the kitty keyboard protocol: M-<letter> sends legacy ESC, C-M-<letter> drops meta

1 participant