fix(cli): skip Kitty keyboard protocol push for Ghostty, use modifyOtherKeys only
Ghostty's Kitty disambiguate-mode implementation strips the Alt modifier from the Backspace key — Option+Backspace arrives as bare \x7f instead of the expected \x1b[27;3;127~, breaking backward-kill-word. This was a regression introduced when PR #87630 re-added the CSI >1u Kitty protocol push for all allowlisted terminals including Ghostty. Under modifyOtherKeys mode (CSI >4;2m), Ghostty correctly sends \x1b[27;3;127~ for Option+Backspace, which the alias table in pt_input_extras already maps to (Escape, ControlH) = backward-kill-word. Fix: for Ghostty only, push just modifyOtherKeys and skip the Kitty protocol push. All other terminals (iTerm2, WezTerm, kitty, tmux, VS Code) still get the full dual-protocol push. Ghostty upstream tracking: discussion #9560, issue #9895 (cmd+backspace variant of the same root cause).
This commit is contained in:
@@ -4095,6 +4095,13 @@ _TERMINAL_INPUT_MODE_RESET_SEQ = (
|
||||
"\x1b[?25h" # ensure cursor visible
|
||||
)
|
||||
_EXTENDED_ENTER_KEYS_SEQ = "\x1b[>1u\x1b[>4;2m"
|
||||
# Ghostty: push ONLY modifyOtherKeys, not the Kitty keyboard protocol.
|
||||
# Ghostty's Kitty disambiguate-mode implementation strips the Alt modifier
|
||||
# from the Backspace key — Option+Backspace arrives as bare \x7f instead of
|
||||
# the expected \x1b[27;3;127~, breaking backward-kill-word (#87630
|
||||
# regression). modifyOtherKeys mode works correctly on Ghostty and covers
|
||||
# every modified key combo the alias table handles.
|
||||
_GHOSTTY_EXTENDED_ENTER_KEYS_SEQ = "\x1b[>4;2m"
|
||||
|
||||
|
||||
_BACKSLASH_LINE_CONTINUATION_RE = re.compile(r"\\[ \t]*$")
|
||||
@@ -4148,20 +4155,37 @@ def _enable_extended_enter_keys(output=None, env: Optional[Mapping[str, str]] =
|
||||
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).
|
||||
|
||||
Ghostty exception: Ghostty's Kitty disambiguate mode strips the Alt
|
||||
modifier from the Backspace key, so Option+Backspace arrives as bare
|
||||
\x7f instead of \x1b[27;3;127~, breaking backward-kill-word. Ghostty
|
||||
implements modifyOtherKeys correctly, so for Ghostty we push only
|
||||
modifyOtherKeys and skip the Kitty protocol push.
|
||||
|
||||
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
|
||||
try:
|
||||
# Ghostty: skip Kitty protocol, use only modifyOtherKeys.
|
||||
if env is None:
|
||||
env = os.environ
|
||||
term_program = (env.get("TERM_PROGRAM") or "").strip()
|
||||
term = (env.get("TERM") or "").strip().lower()
|
||||
is_ghostty = (
|
||||
term_program == "ghostty"
|
||||
or term == "xterm-ghostty"
|
||||
)
|
||||
seq = _GHOSTTY_EXTENDED_ENTER_KEYS_SEQ if is_ghostty else _EXTENDED_ENTER_KEYS_SEQ
|
||||
|
||||
target = output
|
||||
if target is not None and hasattr(target, "write_raw"):
|
||||
target.write_raw(_EXTENDED_ENTER_KEYS_SEQ)
|
||||
target.write_raw(seq)
|
||||
target.flush()
|
||||
return True
|
||||
stream = sys.stdout
|
||||
if stream is not None and stream.isatty():
|
||||
stream.write(_EXTENDED_ENTER_KEYS_SEQ)
|
||||
stream.write(seq)
|
||||
stream.flush()
|
||||
return True
|
||||
except Exception:
|
||||
|
||||
@@ -153,6 +153,66 @@ def test_unknown_terminal_does_not_enable_extended_enter_keys():
|
||||
assert cli_mod._terminal_supports_extended_enter_keys({"TERM_PROGRAM": "unknown"}) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ghostty: must push ONLY modifyOtherKeys, not the Kitty keyboard protocol.
|
||||
# Ghostty's Kitty disambiguate mode strips the Alt modifier from Backspace,
|
||||
# so Option+Backspace arrives as bare \x7f instead of \x1b[27;3;127~,
|
||||
# breaking backward-kill-word. modifyOtherKeys mode works correctly.
|
||||
# ---------------------------------------------------------------------------
|
||||
class _FakeOutput:
|
||||
"""Minimal output object with write_raw + flush for _enable_extended_enter_keys."""
|
||||
def __init__(self):
|
||||
self.written = b""
|
||||
def write_raw(self, data):
|
||||
self.written += data.encode() if isinstance(data, str) else data
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
|
||||
def test_ghostty_uses_modify_other_keys_only():
|
||||
"""Ghostty must NOT push the Kitty keyboard protocol (CSI >1u)."""
|
||||
import cli as cli_mod
|
||||
|
||||
out = _FakeOutput()
|
||||
result = cli_mod._enable_extended_enter_keys(
|
||||
output=out,
|
||||
env={"TERM_PROGRAM": "ghostty", "TERM": "xterm-ghostty"},
|
||||
)
|
||||
assert result is True
|
||||
# Must contain modifyOtherKeys push ...
|
||||
assert b"\x1b[>4;2m" in out.written
|
||||
# ... but NOT the Kitty protocol push.
|
||||
assert b"\x1b[>1u" not in out.written
|
||||
|
||||
|
||||
def test_ghostty_via_term_var_uses_modify_other_keys_only():
|
||||
"""xterm-ghostty TERM (without TERM_PROGRAM) also skips Kitty protocol."""
|
||||
import cli as cli_mod
|
||||
|
||||
out = _FakeOutput()
|
||||
result = cli_mod._enable_extended_enter_keys(
|
||||
output=out,
|
||||
env={"TERM": "xterm-ghostty"},
|
||||
)
|
||||
assert result is True
|
||||
assert b"\x1b[>4;2m" in out.written
|
||||
assert b"\x1b[>1u" not in out.written
|
||||
|
||||
|
||||
def test_non_ghostty_terminals_still_push_kitty_protocol():
|
||||
"""iTerm2 and others still get the full dual-protocol push."""
|
||||
import cli as cli_mod
|
||||
|
||||
out = _FakeOutput()
|
||||
result = cli_mod._enable_extended_enter_keys(
|
||||
output=out,
|
||||
env={"TERM_PROGRAM": "iTerm.app", "TERM": "xterm-256color"},
|
||||
)
|
||||
assert result is True
|
||||
assert b"\x1b[>1u" in out.written
|
||||
assert b"\x1b[>4;2m" in out.written
|
||||
|
||||
|
||||
@pytest.mark.linux_only
|
||||
def test_proc_version_microsoft_marker_preserves_newline():
|
||||
"""WSL detection via /proc when env vars are scrubbed (sudo etc.).
|
||||
|
||||
Reference in New Issue
Block a user