From 1a8fea3ce24dcec339c4532efd9c906b72bf6c8d Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:15:01 +0530 Subject: [PATCH] fix(cli): skip Kitty keyboard protocol push for Ghostty, use modifyOtherKeys only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- cli.py | 28 ++++++++++++- tests/cli/test_ctrl_enter_newline.py | 60 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index 97d5719fd4..b5cd92059b 100644 --- a/cli.py +++ b/cli.py @@ -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: diff --git a/tests/cli/test_ctrl_enter_newline.py b/tests/cli/test_ctrl_enter_newline.py index ba7cb2f564..917a2b393d 100644 --- a/tests/cli/test_ctrl_enter_newline.py +++ b/tests/cli/test_ctrl_enter_newline.py @@ -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.).