fix(cli): restore Kitty keyboard protocol push and complete the extended-key alias table
Commit 4c34eeb416 fixed dead Ctrl+C by removing the Kitty protocol push (CSI >1u) from _EXTENDED_ENTER_KEYS_SEQ, keeping only modifyOtherKeys level 2. That regressed kitty-the-terminal completely: kitty removed xterm modifyOtherKeys support (kovidgoyal/kitty#4075) and only speaks its own protocol, so after the removal kitty users lost Shift+Enter and every other extended key — the CSI >4;2m we still pushed is a no-op there (kitty even logs a PARSE ERROR for it). The original reason for removing the push is obsolete: #87511 mapped CSI-u control sequences, so Ctrl+C as ESC[99;5u now parses to Keys.ControlC and fires the existing c-c binding. (The kernel-INTR concern in that commit was moot — prompt_toolkit's raw mode clears ISIG, so Ctrl+C is always handled by the binding, never the kernel.) Restore the dual push (CSI >1u + CSI >4;2m), exactly mirroring the Ink TUI, and complete the alias table for what the kitty disambiguate flag actually emits — #87511 left real gaps, some of which its PR body wrongly claimed were covered: - Esc key: ESC[27u (+ modifiers) — previously leaked '[27u' as text - Ctrl+Backspace -> backward-kill-word (#78285 was closed on the wrong claim that codepoint-127 mapping existed; it did not) - Shift+Space -> space (#86866's second symptom; the Ctrl+Space mapping never covered modifier 2) - Alt+Enter -> newline tuple; Shift+Tab -> BackTab; Ctrl+Tab -> Tab; Alt/Shift+Backspace - Multi-modifier letters (Shift+Alt 4, Ctrl+Shift 6, Ctrl+Alt 7, Ctrl+Alt+Shift 8) normalized onto their Ctrl/Escape-prefix targets, both unshifted (kitty) and shifted (mok emitters) codepoints - Kitty PUA functional keys: keypad -> non-keypad equivalents, F13-F24, and Ignore for lock/media/modifier-event keys so they are consumed instead of leaking (kitty emits these even in legacy mode) Also: clear the VT100 parser's prefix cache after installing (stale answers could misparse), and re-push extended keys after _recover_terminal_input_modes' reset — the recovery previously popped both modes mid-session and never re-enabled them, silently killing Shift+Enter until restart. Refs #87511, #87074, #56684, #56645, #78285, #86866, #87390.
This commit is contained in:
50
cli.py
50
cli.py
@@ -3887,7 +3887,7 @@ _TERMINAL_INPUT_MODE_RESET_SEQ = (
|
||||
"\x1b[0m" # reset text attributes
|
||||
"\x1b[?25h" # ensure cursor visible
|
||||
)
|
||||
_EXTENDED_ENTER_KEYS_SEQ = "\x1b[>4;2m"
|
||||
_EXTENDED_ENTER_KEYS_SEQ = "\x1b[>1u\x1b[>4;2m"
|
||||
|
||||
|
||||
_BACKSLASH_LINE_CONTINUATION_RE = re.compile(r"\\[ \t]*$")
|
||||
@@ -3920,25 +3920,29 @@ def _terminal_supports_extended_enter_keys(env: Optional[Mapping[str, str]] = No
|
||||
|
||||
|
||||
def _enable_extended_enter_keys(output=None, env: Optional[Mapping[str, str]] = None) -> bool:
|
||||
"""Ask allowlisted terminals to report Shift+Enter distinctly.
|
||||
"""Ask allowlisted terminals to report modified keys distinctly.
|
||||
|
||||
Writes xterm modifyOtherKeys level 2 (CSI >4;2m), mirroring the Ink TUI.
|
||||
We do NOT push the Kitty keyboard protocol (CSI >1u) here because
|
||||
prompt_toolkit 3.x cannot parse Kitty CSI-u sequences for control
|
||||
characters — Ctrl+C arrives as ``\\x1b[99;5u`` instead of ``\\x03``,
|
||||
which neither prompt_toolkit's key bindings nor the kernel's INTR
|
||||
mechanism can match, leaving Ctrl+C completely dead (#56684).
|
||||
Writes the Kitty keyboard protocol push (CSI >1u, disambiguate mode) AND
|
||||
xterm modifyOtherKeys level 2 (CSI >4;2m), mirroring the Ink TUI —
|
||||
terminals honor whichever protocol they implement. Both are needed:
|
||||
kitty-the-terminal removed modifyOtherKeys support entirely (it only
|
||||
speaks its own protocol), while tmux/VS Code only accept modifyOtherKeys.
|
||||
|
||||
modifyOtherKeys=2 re-encodes ALL Ctrl+key combos as
|
||||
``ESC[27;5;<codepoint>~`` instead of raw control bytes.
|
||||
Under either protocol the terminal re-encodes modified keys as escape
|
||||
sequences — Kitty disambiguate mode as ``ESC[<codepoint>;<mod>u`` (plus
|
||||
the Esc key as ``ESC[27u``), modifyOtherKeys=2 as
|
||||
``ESC[27;<mod>;<codepoint>~``. Stock prompt_toolkit 3.x maps almost
|
||||
none of these, which is why the CSI >1u push was temporarily removed in
|
||||
#87074 (Ctrl+C arrived as ``ESC[99;5u`` and died, #56684).
|
||||
``install_modify_other_keys_aliases()`` (called at CLI startup from
|
||||
``hermes_cli.pt_input_extras``) populates prompt_toolkit's
|
||||
``ANSI_SEQUENCES`` with the full Ctrl+letter / Ctrl+digit / Ctrl+symbol
|
||||
and Alt+letter mappings under both the modifyOtherKeys and CSI-u formats,
|
||||
so every existing key binding continues to fire (#87711).
|
||||
``hermes_cli.pt_input_extras``) now populates ``ANSI_SEQUENCES`` with the
|
||||
full Ctrl/Alt/Shift/multi-modifier and functional-key tables under BOTH
|
||||
formats, so every existing key binding continues to fire — including
|
||||
Ctrl+C, which is handled by prompt_toolkit's ``c-c`` binding (raw mode
|
||||
clears ISIG, so the kernel INTR path was never in play for the CLI).
|
||||
|
||||
The exit reset sequence already pops/resets both modes, so this is
|
||||
safe across normal exits, Ctrl+C, and SIGTERM cleanup.
|
||||
The exit reset sequence pops/resets both modes, so this is safe across
|
||||
normal exits, Ctrl+C, and SIGTERM cleanup.
|
||||
"""
|
||||
if not _terminal_supports_extended_enter_keys(env):
|
||||
return False
|
||||
@@ -8531,6 +8535,16 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# The reset sequence above pops kitty keyboard mode and resets
|
||||
# modifyOtherKeys too — re-request extended keys so Shift+Enter /
|
||||
# modified-key reporting isn't silently dead for the rest of the
|
||||
# session after a recovery (sibling of the startup push).
|
||||
try:
|
||||
if _cli_multiline_shortcuts_enabled(self.config or CLI_CONFIG):
|
||||
_enable_extended_enter_keys(output)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.warning("Recovered terminal input modes after leak: %s", reason)
|
||||
if not self._input_mode_recovery_notice_shown:
|
||||
self._input_mode_recovery_notice_shown = True
|
||||
@@ -19280,8 +19294,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
# The app enables focus reporting + mouse tracking; record that
|
||||
# so _run_cleanup resets them on exit (#36823). When multiline
|
||||
# shortcuts are on, also ask supported terminals (e.g. iTerm2)
|
||||
# to distinguish Shift+Enter from Enter; the same cleanup reset
|
||||
# pops kitty keyboard mode and resets modifyOtherKeys.
|
||||
# to report modified keys distinctly (kitty protocol +
|
||||
# modifyOtherKeys); the cleanup reset pops both modes.
|
||||
_mark_tui_input_modes_active()
|
||||
if _multiline_shortcuts_enabled:
|
||||
_enable_extended_enter_keys(app.output)
|
||||
|
||||
@@ -12,6 +12,24 @@ can be unit-tested without importing the whole CLI runtime.
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def _clear_vt100_prefix_cache() -> None:
|
||||
"""Drop prompt_toolkit's memoized "is this a prefix of a longer match?"
|
||||
answers after mutating ``ANSI_SEQUENCES``.
|
||||
|
||||
The cache is module-global and populated lazily per distinct prefix, so
|
||||
parsers created before an install (or primed by earlier tests) would
|
||||
otherwise keep stale ``False`` answers and misparse newly registered
|
||||
sequences. Call after any install that changed the table.
|
||||
"""
|
||||
try:
|
||||
from prompt_toolkit.input.vt100_parser import (
|
||||
_IS_PREFIX_OF_LONGER_MATCH_CACHE,
|
||||
)
|
||||
_IS_PREFIX_OF_LONGER_MATCH_CACHE.clear()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def install_shift_enter_alias() -> int:
|
||||
"""Map Shift+Enter byte sequences to the (Escape, ControlM) key tuple
|
||||
that Alt+Enter produces, so the existing Alt+Enter newline handler
|
||||
@@ -48,6 +66,8 @@ def install_shift_enter_alias() -> int:
|
||||
if ANSI_SEQUENCES.get(seq) != alt_enter:
|
||||
ANSI_SEQUENCES[seq] = alt_enter
|
||||
changed += 1
|
||||
if changed:
|
||||
_clear_vt100_prefix_cache()
|
||||
return changed
|
||||
|
||||
|
||||
@@ -80,6 +100,8 @@ def install_ctrl_enter_alias() -> int:
|
||||
if ANSI_SEQUENCES.get(seq) != alt_enter:
|
||||
ANSI_SEQUENCES[seq] = alt_enter
|
||||
changed += 1
|
||||
if changed:
|
||||
_clear_vt100_prefix_cache()
|
||||
return changed
|
||||
|
||||
|
||||
@@ -123,6 +145,8 @@ def install_cmd_backspace_alias() -> int:
|
||||
if ANSI_SEQUENCES.get(seq) != key:
|
||||
ANSI_SEQUENCES[seq] = key
|
||||
changed += 1
|
||||
if changed:
|
||||
_clear_vt100_prefix_cache()
|
||||
return changed
|
||||
|
||||
|
||||
@@ -140,7 +164,7 @@ def install_modify_other_keys_aliases() -> int:
|
||||
Stock prompt_toolkit 3.x only maps ``ESC[27;5;13~`` (Ctrl+Enter = Ctrl+M);
|
||||
all other Ctrl+letter combos are unmapped and leak as literal text or get
|
||||
swallowed — breaking Ctrl+A, Ctrl+C, Ctrl+D, Ctrl+E, Ctrl+K, Ctrl+R,
|
||||
Ctrl+U, Ctrl+W, Ctrl+Z, etc. (#56684, #87711).
|
||||
Ctrl+U, Ctrl+W, Ctrl+Z, etc. (#56684, #86866, #87390).
|
||||
|
||||
This function populates ``ANSI_SEQUENCES`` for the full set:
|
||||
|
||||
@@ -152,6 +176,25 @@ def install_modify_other_keys_aliases() -> int:
|
||||
* **Alt+letter** (a–z, A–Z): ``ESC[27;3;<codepoint>~`` and
|
||||
``ESC[<codepoint>;3u`` → ``(Keys.Escape, <letter>)`` — matching how
|
||||
prompt_toolkit handles a bare ``ESC`` followed by a character.
|
||||
* **Shift+letter** (a–z): → the uppercase character.
|
||||
* **Multi-modifier letters** (Shift+Alt=4, Ctrl+Shift=6, Ctrl+Alt=7,
|
||||
Ctrl+Alt+Shift=8): normalized onto the same targets — Ctrl-bearing
|
||||
combos behave as the Ctrl key (Alt adds an ``Escape`` prefix),
|
||||
matching how dte/kakoune normalize these protocols.
|
||||
* **Esc key**: ``ESC[27u`` / ``ESC[27;<mod>u`` (Kitty disambiguate mode
|
||||
reports Esc this way, #56684) → ``Keys.Escape``.
|
||||
* **Modified Enter/Tab/Backspace/Space**: Alt+Enter → the Alt+Enter
|
||||
newline tuple; Shift+Tab → ``BackTab``; Ctrl+Tab → plain Tab;
|
||||
Ctrl/Alt+Backspace → ``(Escape, ControlH)`` (backward-kill-word,
|
||||
matching the Ink TUI and Desktop, #78285); Shift+Backspace → plain
|
||||
backspace; Shift+Space → a plain space (#86866); Alt+Space →
|
||||
``(Escape, " ")``.
|
||||
* **Kitty functional keys** (Private Use Area codepoints): keypad keys
|
||||
→ their non-keypad equivalents (KP_ENTER → Enter, KP_4 → '4',
|
||||
KP_LEFT → Left, …); F13–F24 → ``Keys.F13``..``F24``; lock/media/
|
||||
modifier-event keys → ``Keys.Ignore`` so they are consumed instead of
|
||||
leaking as literal text. kitty emits these CSI-u forms even in legacy
|
||||
mode for keys that have no legacy encoding.
|
||||
|
||||
Existing mappings (including those installed by
|
||||
``install_shift_enter_alias`` / ``install_ctrl_enter_alias``) are never
|
||||
@@ -248,6 +291,101 @@ def install_modify_other_keys_aliases() -> int:
|
||||
shift_map[ch - 32] = upper_char
|
||||
_install_paired(2, shift_map)
|
||||
|
||||
# -- Multi-modifier letters: Shift+Alt (4), Ctrl+Shift (6),
|
||||
# Ctrl+Alt (7), Ctrl+Alt+Shift (8) ----
|
||||
# The Kitty protocol always reports the UNSHIFTED codepoint; some
|
||||
# modifyOtherKeys emitters send the shifted one — map both cases.
|
||||
# Ctrl-bearing combos normalize onto the Ctrl key (Alt adds an Escape
|
||||
# prefix), Shift+Alt onto (Escape, UPPER) — the same normalization
|
||||
# dte/kakoune apply to these protocols. Without these, Ctrl+Shift+R
|
||||
# etc. leak as literal text under either protocol.
|
||||
shift_alt_map: dict[int, tuple] = {}
|
||||
ctrl_shift_map: dict[int, object] = {}
|
||||
ctrl_alt_map: dict[int, tuple] = {}
|
||||
for ch in range(ord('a'), ord('z') + 1):
|
||||
upper_char = chr(ch - 32)
|
||||
ctrl_key = ctrl_key_map.get(ch)
|
||||
for cp in (ch, ch - 32):
|
||||
shift_alt_map[cp] = (Keys.Escape, upper_char)
|
||||
if ctrl_key is not None:
|
||||
ctrl_shift_map[cp] = ctrl_key
|
||||
ctrl_alt_map[cp] = (Keys.Escape, ctrl_key)
|
||||
_install_paired(4, shift_alt_map)
|
||||
_install_paired(6, ctrl_shift_map)
|
||||
_install_paired(7, ctrl_alt_map)
|
||||
_install_paired(8, ctrl_alt_map) # Ctrl+Alt+Shift — same normalization
|
||||
|
||||
# -- The Esc KEY under Kitty disambiguate mode: ESC[27u (+ modifiers) --
|
||||
# Disambiguate mode reports the Esc key as CSI-u so it is
|
||||
# distinguishable from the ESC byte that starts escape sequences
|
||||
# (#56684 — previously leaked "[27u" as literal text into the prompt).
|
||||
# Modifiers run to 16 because kitty reports Cmd as the super bit
|
||||
# (mod 9+) — same reason install_cmd_backspace_alias maps 9/10.
|
||||
for seq in ["\x1b[27u"] + [f"\x1b[27;{m}u" for m in range(2, 17)]:
|
||||
if seq not in ANSI_SEQUENCES:
|
||||
ANSI_SEQUENCES[seq] = Keys.Escape
|
||||
changed += 1
|
||||
|
||||
# -- Modified Enter / Tab / Backspace / Space ----
|
||||
# Shift+Enter / Ctrl+Enter are installed by install_shift_enter_alias /
|
||||
# install_ctrl_enter_alias (which run first and win via setdefault).
|
||||
_install_paired(2, {
|
||||
9: Keys.BackTab, # Shift+Tab — same as the legacy ESC[Z
|
||||
127: Keys.ControlH, # Shift+Backspace — plain backspace
|
||||
32: " ", # Shift+Space — still a space (#86866)
|
||||
})
|
||||
_install_paired(3, {
|
||||
13: (Keys.Escape, Keys.ControlM), # Alt+Enter — newline tuple
|
||||
127: (Keys.Escape, Keys.ControlH), # Alt+Backspace — backward-kill-word
|
||||
32: (Keys.Escape, " "), # Alt+Space
|
||||
})
|
||||
_install_paired(5, {
|
||||
9: Keys.ControlI, # Ctrl+Tab — degrade to Tab
|
||||
127: (Keys.Escape, Keys.ControlH), # Ctrl+Backspace — backward-kill-word,
|
||||
# matching Ink TUI + Desktop (#78285)
|
||||
})
|
||||
|
||||
# -- Kitty functional keys (Private Use Area codepoints) ----
|
||||
# kitty emits these CSI-u encodings even in LEGACY mode for keys that
|
||||
# have no legacy encoding, so unmapped they leak as literal text in any
|
||||
# kitty session regardless of which modes were pushed.
|
||||
functional_map: dict[int, object] = {}
|
||||
for d in range(10): # KP_0..KP_9 → digits
|
||||
functional_map[57399 + d] = str(d)
|
||||
functional_map.update({ # KP operators / punctuation
|
||||
57409: ".", 57410: "/", 57411: "*", 57412: "-",
|
||||
57413: "+", 57414: Keys.ControlM, 57415: "=", 57416: ",",
|
||||
})
|
||||
functional_map.update({ # KP navigation → non-keypad keys
|
||||
57417: Keys.Left, 57418: Keys.Right, 57419: Keys.Up,
|
||||
57420: Keys.Down, 57421: Keys.PageUp, 57422: Keys.PageDown,
|
||||
57423: Keys.Home, 57424: Keys.End, 57425: Keys.Insert,
|
||||
57426: Keys.Delete,
|
||||
})
|
||||
for n in range(13, 25): # F13..F24
|
||||
functional_map[57376 + (n - 13)] = getattr(Keys, f"F{n}")
|
||||
# No prompt_toolkit equivalent (lock keys, PrintScreen, Menu, F25-F35,
|
||||
# KP_BEGIN, media keys, bare modifier events): consume as Ignore
|
||||
# instead of leaking literal text.
|
||||
for code in (
|
||||
list(range(57358, 57364)) # locks, PrintScreen, Pause, Menu
|
||||
+ list(range(57388, 57399)) # F25..F35
|
||||
+ [57427] # KP_BEGIN
|
||||
+ list(range(57428, 57455)) # media keys + modifier key events
|
||||
):
|
||||
functional_map.setdefault(code, Keys.Ignore)
|
||||
for code, key_val in functional_map.items():
|
||||
seq = f"\x1b[{code}u"
|
||||
if seq not in ANSI_SEQUENCES:
|
||||
ANSI_SEQUENCES[seq] = key_val
|
||||
changed += 1
|
||||
|
||||
# New longer sequences can flip "is this a prefix of a longer match?"
|
||||
# answers the VT100 parser already cached — drop the cache so parsers
|
||||
# created before this install (or in earlier tests) can't misparse.
|
||||
if changed:
|
||||
_clear_vt100_prefix_cache()
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
@@ -285,4 +423,6 @@ def install_ignored_terminal_sequences() -> int:
|
||||
if seq not in ANSI_SEQUENCES:
|
||||
ANSI_SEQUENCES[seq] = Keys.Ignore
|
||||
changed += 1
|
||||
if changed:
|
||||
_clear_vt100_prefix_cache()
|
||||
return changed
|
||||
|
||||
@@ -27,13 +27,18 @@ from hermes_cli.pt_input_extras import install_modify_other_keys_aliases
|
||||
@pytest.fixture(autouse=True)
|
||||
def _ensure_alias_installed():
|
||||
"""Install the alias for each test, then restore ANSI_SEQUENCES to its
|
||||
prior state so 294 mappings don't leak into sibling test files."""
|
||||
prior state so the hundreds of installed mappings don't leak into
|
||||
sibling test files."""
|
||||
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES as _seq
|
||||
saved = dict(_seq)
|
||||
install_modify_other_keys_aliases()
|
||||
yield
|
||||
_seq.clear()
|
||||
_seq.update(saved)
|
||||
# Drop the parser's prefix cache too — it was computed against the
|
||||
# augmented table and would go stale after the restore above.
|
||||
from prompt_toolkit.input.vt100_parser import _IS_PREFIX_OF_LONGER_MATCH_CACHE
|
||||
_IS_PREFIX_OF_LONGER_MATCH_CACHE.clear()
|
||||
|
||||
|
||||
def _parse(byte_seq: str):
|
||||
@@ -301,3 +306,130 @@ def test_plain_enter_remains_distinct():
|
||||
assert enter != alt_enter
|
||||
assert len(enter) == 1
|
||||
assert len(alt_enter) == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kitty keyboard protocol: the Esc KEY (disambiguate mode, #56684)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_kitty_plain_escape_key():
|
||||
"""The Esc key under kitty disambiguate mode (ESC[27u) must parse as
|
||||
Keys.Escape, not leak '[27u' as literal text."""
|
||||
assert _parse("\x1b[27u") == [Keys.Escape]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("modifier", range(2, 9))
|
||||
def test_kitty_modified_escape_key(modifier):
|
||||
"""Modified Esc (Shift+Esc etc.) still behaves as Escape."""
|
||||
assert _parse(f"\x1b[27;{modifier}u") == [Keys.Escape]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modified Enter / Tab / Backspace / Space
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_alt_enter_produces_newline_tuple():
|
||||
"""Alt+Enter under either protocol must equal bare ESC+CR (newline)."""
|
||||
alt_enter = _parse("\x1b\r")
|
||||
assert _parse("\x1b[13;3u") == alt_enter
|
||||
assert _parse("\x1b[27;3;13~") == alt_enter
|
||||
|
||||
|
||||
def test_shift_tab_produces_backtab():
|
||||
"""Shift+Tab must behave like the legacy ESC[Z (BackTab)."""
|
||||
assert _parse("\x1b[9;2u") == [Keys.BackTab]
|
||||
assert _parse("\x1b[27;2;9~") == [Keys.BackTab]
|
||||
|
||||
|
||||
def test_ctrl_backspace_produces_backward_kill_word():
|
||||
"""Ctrl+Backspace must produce the (Escape, ControlH) tuple that fires
|
||||
prompt_toolkit's backward-kill-word — parity with the Ink TUI and
|
||||
Desktop (#78285)."""
|
||||
expected = [Keys.Escape, Keys.ControlH]
|
||||
assert _parse("\x1b[127;5u") == expected
|
||||
assert _parse("\x1b[27;5;127~") == expected
|
||||
|
||||
|
||||
def test_alt_backspace_produces_backward_kill_word():
|
||||
"""Alt+Backspace = ESC+DEL = backward-kill-word."""
|
||||
assert _parse("\x1b[127;3u") == [Keys.Escape, Keys.ControlH]
|
||||
assert _parse("\x1b[27;3;127~") == [Keys.Escape, Keys.ControlH]
|
||||
|
||||
|
||||
def test_shift_backspace_is_plain_backspace():
|
||||
assert _parse("\x1b[127;2u") == _parse("\x7f")
|
||||
|
||||
|
||||
def test_shift_space_inserts_space():
|
||||
"""Shift+Space must insert a space, not leak escape text (#86866)."""
|
||||
assert _parse("\x1b[32;2u") == [" "]
|
||||
assert _parse("\x1b[27;2;32~") == [" "]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-modifier combos (Ctrl+Shift / Ctrl+Alt / Shift+Alt / Ctrl+Alt+Shift)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("letter", ["c", "r", "z"])
|
||||
def test_ctrl_shift_letter_behaves_as_ctrl(letter):
|
||||
"""Ctrl+Shift+<letter> (modifier 6) must fire the same key as
|
||||
Ctrl+<letter> — both the unshifted and shifted codepoint variants."""
|
||||
expected = _parse(chr(ord(letter) & 0x1F))
|
||||
for cp in (ord(letter), ord(letter) - 32):
|
||||
assert _parse(f"\x1b[{cp};6u") == expected
|
||||
assert _parse(f"\x1b[27;6;{cp}~") == expected
|
||||
|
||||
|
||||
def test_ctrl_alt_letter_behaves_as_escape_ctrl():
|
||||
"""Ctrl+Alt+a (modifier 7) = Escape prefix + ControlA."""
|
||||
assert _parse("\x1b[97;7u") == [Keys.Escape, Keys.ControlA]
|
||||
|
||||
|
||||
def test_ctrl_alt_shift_letter_behaves_as_escape_ctrl():
|
||||
"""Ctrl+Alt+Shift+a (modifier 8) = Escape prefix + ControlA."""
|
||||
assert _parse("\x1b[97;8u") == [Keys.Escape, Keys.ControlA]
|
||||
|
||||
|
||||
def test_shift_alt_letter_behaves_as_escape_upper():
|
||||
"""Shift+Alt+f (modifier 4) = Escape prefix + 'F'."""
|
||||
assert _parse("\x1b[102;4u") == [Keys.Escape, "F"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kitty functional keys (Private Use Area codepoints)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_keypad_enter_behaves_as_enter():
|
||||
assert _parse("\x1b[57414u") == _parse("\r")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("digit", range(10))
|
||||
def test_keypad_digits_type_digits(digit):
|
||||
assert _parse(f"\x1b[{57399 + digit}u") == [str(digit)]
|
||||
|
||||
|
||||
def test_keypad_navigation_maps_to_arrows():
|
||||
assert _parse("\x1b[57417u") == [Keys.Left]
|
||||
assert _parse("\x1b[57418u") == [Keys.Right]
|
||||
assert _parse("\x1b[57419u") == [Keys.Up]
|
||||
assert _parse("\x1b[57420u") == [Keys.Down]
|
||||
|
||||
|
||||
def test_f13_maps_to_f13():
|
||||
assert _parse("\x1b[57376u") == [Keys.F13]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [57358, 57428, 57441, 57448])
|
||||
def test_lock_media_modifier_events_are_consumed(code):
|
||||
"""Caps Lock, media keys, and bare modifier press events must be
|
||||
consumed (Keys.Ignore), never leak as literal text."""
|
||||
result = _parse(f"\x1b[{code}u")
|
||||
assert result == [Keys.Ignore], f"CSI {code}u leaked: {result!r}"
|
||||
|
||||
|
||||
def test_cmd_backspace_alias_not_clobbered():
|
||||
"""install_cmd_backspace_alias's super-modifier mappings must survive."""
|
||||
from hermes_cli.pt_input_extras import install_cmd_backspace_alias
|
||||
install_cmd_backspace_alias()
|
||||
install_modify_other_keys_aliases()
|
||||
assert _parse("\x1b[127;9u") == [Keys.ControlU]
|
||||
|
||||
Reference in New Issue
Block a user