Skip to content

fix(cli): map kitty CSI-u sequences with the num_lock modifier bit - #88673

Closed
iamlukethedev wants to merge 1 commit into
NousResearch:mainfrom
iamlukethedev:fix/88221-kitty-numlock-csi-u
Closed

fix(cli): map kitty CSI-u sequences with the num_lock modifier bit#88673
iamlukethedev wants to merge 1 commit into
NousResearch:mainfrom
iamlukethedev:fix/88221-kitty-numlock-csi-u

Conversation

@iamlukethedev

Copy link
Copy Markdown
Contributor

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 the num_lock bit into every key event while the lock is on. hermes_cli/pt_input_extras.py registers CSI-u / modifyOtherKeys sequences as exact-string keys in prompt_toolkit's ANSI_SEQUENCES, and every registration site hardcoded a bare modifier number — _install_paired(5, …) for Ctrl, \x1b[13;2u for Shift+Enter, \x1b[127;9u for Ctrl+Backspace, and the ESC[27;<m>u loop for Esc. Kitty therefore emits ESC[99;133u (5 + 128) where the table only holds ESC[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 = 128 constant 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 130
    • install_ctrl_enter_alias — modifier 5, now also 133
    • install_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 once
    • the ESC[27;<m>u Escape loop — modifiers 2–16, now also 130–144
    • a new _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 case ESC[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;69u would 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_variants is the single seam to widen if a report lands.

How to test

  1. Open kitty (TERM=xterm-kitty), turn NumLock on
  2. Run hermes
  3. Press Backspace and Ctrl+C in the input box — both act normally instead of inserting [127;129u / [99;133u

Deterministic without kitty by feeding the sequences directly:

from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
from hermes_cli.pt_input_extras import install_modify_other_keys_aliases
install_modify_other_keys_aliases()
assert ANSI_SEQUENCES["\x1b[99;133u"]   # Ctrl+C,   NumLock on
assert ANSI_SEQUENCES["\x1b[127;129u"]  # Backspace, NumLock on

Validation

Check Result
pytest tests/cli/test_modify_other_keys_aliases.py 220 passed
Mutation check (production revert, tests kept) 48 failed, 172 passed — every failure is a new num_lock test, no pre-existing test broke
Mutation check (restored) 220 passed
Reproduction before fix 5/6 NumLock sequences unmapped
Reproduction after fix 0/6 unmapped
scripts/check-windows-footguns.py clean
ANSI_SEQUENCES growth 242 → 1850 (+1608), ~one twin per existing fixed-modifier registration; installed once at startup
Rebased onto current main yes, tests re-run green after rebase

Behavior preserved: every site keeps its "only install when absent / when the value differs" guard, so earlier aliases are never overwritten, and the changed counter 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

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
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 17, 2026
@yowakkojay

Copy link
Copy Markdown

Verified this fixes the NumLock case from #88221 — applied the diff locally and \x1b[99;133u / \x1b[127;129u now parse correctly.

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:

\x1b[99;69u    (Ctrl+C + CapsLock)   -> leaks as literal text
\x1b[99;197u   (Ctrl+C + both locks) -> leaks
\x1b[127;65u   (Backspace + Caps)    -> leaks
\x1b[127;193u  (Backspace + both)    -> leaks

Suggestion: generalize _num_lock_variants to OR in each lock bit — (64, 128, 192) — rather than only 128. I prototyped a generic variant pass while debugging the original report and am happy to push it onto this branch if welcome.

@yowakkojay

Copy link
Copy Markdown

New data point on #88221 (another user's report): with the lock bit on, arrow keys leak tooCSI 1;129D/[1;129C/[1;129A/[1;129B — which is a family this PR doesn't cover: _num_lock_variants only threads through the u/modifyOtherKeys registrations, but arrows/Home are letter-terminated (\x1b[D\x1b[1;129D) and Delete/PageUp are tilde-terminated (\x1b[3~\x1b[3;129~). Same lock-bit cause; prompt_toolkit already maps their clean-modifier forms (\x1b[1;5D, \x1b[3;5~), so they need the same twinning.

Also worth covering caps_lock (64) and caps+num (192) in the same pass, per my comment above — one variant generator over (64, 128, 192) applied to every registered CSI entry (u, ~, and letter finals) closes all of it.

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: kitty + NumLock on: CLI inserts raw CSI-u codes ([127;129u) instead of acting on keys

4 participants