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:
kshitij
2026-08-17 18:15:01 +05:30
parent 67029492d4
commit 1a8fea3ce2
2 changed files with 86 additions and 2 deletions
+26 -2
View File
@@ -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:
+60
View File
@@ -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.).