fix(cli): map kitty CSI-u sequences with the num_lock modifier bit - #88673
fix(cli): map kitty CSI-u sequences with the num_lock modifier bit#88673iamlukethedev wants to merge 1 commit into
Conversation
Kitty's keyboard protocol encodes the modifier field as a bitmask + 1
(shift=1, alt=2, ctrl=4, super=8, ..., num_lock=128) and ORs the
num_lock bit into EVERY key event while NumLock is on. Ctrl+C therefore
arrives as ESC[99;133u instead of ESC[99;5u, and plain Backspace as
ESC[127;129u. Those spellings are absent from ANSI_SEQUENCES, so
prompt_toolkit's VT100 parser leaks them into the input box as literal
text.
Every fixed-modifier registration in pt_input_extras now installs its
num_lock twin as well: install_shift_enter_alias (2), install_ctrl_enter_alias
(5), install_cmd_backspace_alias (9/10), _install_paired (2/3/4/5/6/7/8)
and the CSI-u Esc-key loop. Modifier 1 ("no modifiers") is registered for
Tab/Enter/Esc/Space/Backspace so unmodified keys under NumLock resolve too.
Semantics are unchanged: sequences are still only installed when absent
and the changed counter stays accurate.
Fixes NousResearch#88221
|
Verified this fixes the NumLock case from #88221 — applied the diff locally and One gap while testing: caps_lock (64) and caps+num (192) still leak, and the spec treats both locks identically — "if a modifier is active when the key event occurs, i.e. if the key is pressed or the lock (for caps lock/num lock) is enabled, the key event must have the bit set" (kitty keyboard protocol, Modifiers section). With this PR applied and CapsLock on: Suggestion: generalize |
|
New data point on #88221 (another user's report): with the lock bit on, arrow keys leak too — Also worth covering caps_lock (64) and caps+num (192) in the same pass, per my comment above — one variant generator over |
|
The lock-bit bug is fixed on main via #90561, which salvages #89676 (CSI-u lock twins, cherry-picked with authorship preserved) and #90291 (legacy nav-key slice), plus follow-up coverage for the alias installers, all-modifier navigation twins, and kitty PUA functional keys under lock bits. Your PR independently diagnosed the same root cause — lock state ORed into the CSI-u modifier parameter — and the merged coverage is a superset of the sequences yours mapped (including the Esc-mod family and the mod-1 plain keys yours handled). Closing in favor of the composite; thanks for the careful diagnosis and the tests, they helped cross-validate the coverage matrix. |
Summary
With NumLock on in kitty, keys stopped working in the CLI — Backspace typed
[127;129u, Ctrl+C typed[99;133u, and the raw escape codes landed in the input box as literal text. They now behave normally.Root cause: kitty's keyboard protocol encodes the modifier field as a bitmask + 1 (
shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps_lock=64, num_lock=128) and ORs thenum_lockbit into every key event while the lock is on.hermes_cli/pt_input_extras.pyregisters CSI-u / modifyOtherKeys sequences as exact-string keys in prompt_toolkit'sANSI_SEQUENCES, and every registration site hardcoded a bare modifier number —_install_paired(5, …)for Ctrl,\x1b[13;2ufor Shift+Enter,\x1b[127;9ufor Ctrl+Backspace, and theESC[27;<m>uloop for Esc. Kitty therefore emitsESC[99;133u(5 + 128) where the table only holdsESC[99;5u, no entry matches, and the VT100 parser falls through to literal text. Plain letters keep working because they arrive as UTF-8 rather than CSI-u.Same failure class as zellij-org/zellij#4178.
Changes
hermes_cli/pt_input_extras.py— one_NUM_LOCK = 128constant and a_num_lock_variants(mod) -> (mod, mod + 128)helper, applied at every fixed-modifier registration site so the whole bug class is covered rather than just the reported Ctrl+C:install_shift_enter_alias— modifier 2, now also 130install_ctrl_enter_alias— modifier 5, now also 133install_cmd_backspace_alias— modifiers 9/10, now also 137/138_install_paired()— one outer loop over the twins, so all its callers (modifiers 2–8) are covered at onceESC[27;<m>uEscape loop — modifiers 2–16, now also 130–144_install_paired(1, {Tab, Enter, Esc, Space, Backspace}). Modifier 1 means "no modifiers"; with NumLock on that becomes 129, which is exactly the reported plain-Backspace caseESC[127;129u.Routing every site through one helper means a future lock-bit report (e.g.
caps_lock) is a one-line widening rather than another sweep of the file.tests/cli/test_modify_other_keys_aliases.py— regression tests asserting the invariant that each NumLock-on sequence parses identically to its NumLock-off twin, across every affected installer, plus idempotency (a second install returns 0 and overwrites nothing).Deliberately not changed:
caps_lock(bit 64)The same mechanism applies —
ESC[99;69uwould leak identically. Left out on purpose: the issue reports NumLock only, NumLock is commonly left on permanently by keypad users while CapsLock is a transient state, and covering both would double the registrations again for a case with no reported user._num_lock_variantsis the single seam to widen if a report lands.How to test
TERM=xterm-kitty), turn NumLock onhermes[127;129u/[99;133uDeterministic without kitty by feeding the sequences directly:
Validation
pytest tests/cli/test_modify_other_keys_aliases.pyscripts/check-windows-footguns.pyANSI_SEQUENCESgrowthmainBehavior preserved: every site keeps its "only install when absent / when the value differs" guard, so earlier aliases are never overwritten, and the
changedcounter still reflects only newly-installed sequences.Tested on Linux only (Ubuntu, Python 3.13). The change is pure table registration with no platform-specific code paths.
Closes #88221