fix(cli): handle CSI-u Shift+Enter instead of dumping raw escapes
Terminals speaking the CSI-u (kitty / fixterms) keyboard protocol -- kitty,
Ghostty, WezTerm, and terminal panes that default to it -- encode Shift+Enter
as the escape sequence "\x1b[13;2u". prompt_toolkit 3.0 has no support for
that protocol and no default mapping for the sequence, so its Vt100Parser
fails to recognise it and dumps the raw bytes ("^[[13;2u") straight into the
prompt buffer. The multiline-input work kept only Alt+Enter, so on these
terminals Shift+Enter now leaks visible escape garbage into the input.
Register "\x1b[13;2u" (absent from prompt_toolkit's default ANSI_SEQUENCES,
so setdefault() overrides nothing) and bind it to insert a newline, matching
the Alt+Enter behaviour. This is best-effort: prompt_toolkit cannot negotiate
the protocol, so we only react to a CSI-u sequence a terminal already emits;
terminals that collapse Shift+Enter into plain Enter fall back to Alt+Enter,
which stays the primary shortcut.
Add a real-PromptSession regression test asserting the sequence inserts a
newline rather than leaking raw escape bytes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Claude Opus 4.8
parent
87602a74ea
commit
99e04c2da2
+30
-1
@@ -310,8 +310,33 @@ def _build_cli_key_bindings() -> KeyBindings:
|
||||
* Enter -> submit the current input (keeps the familiar
|
||||
single-line Enter-to-send feel even though the buffer
|
||||
is multiline-capable).
|
||||
* Alt+Enter -> insert a newline for multi-line input.
|
||||
* Alt+Enter -> insert a newline for multi-line input. Universally
|
||||
supported, so this is the reliable multiline shortcut.
|
||||
* Shift+Enter -> insert a newline on terminals that emit the CSI-u
|
||||
(kitty / fixterms) keyboard-protocol encoding for it.
|
||||
"""
|
||||
# Terminals speaking the CSI-u (kitty / fixterms) keyboard protocol -- e.g.
|
||||
# kitty, Ghostty and WezTerm when it is enabled -- send Shift+Enter as the
|
||||
# escape sequence "\x1b[13;2u". prompt_toolkit 3.0 has no support for that
|
||||
# protocol (no way to negotiate it, no default mapping for the sequence), so
|
||||
# when such a terminal sends it the Vt100Parser fails to recognise the
|
||||
# sequence and dumps the raw bytes ("^[[13;2u") straight into the buffer.
|
||||
#
|
||||
# Register the sequence so it parses as one keypress bound to insert a
|
||||
# newline instead. "\x1b[13;2u" is absent from prompt_toolkit's default
|
||||
# ANSI_SEQUENCES table, so this is a pure addition -- setdefault() overrides
|
||||
# nothing -- carried on Keys.ControlF3, an enum member prompt_toolkit
|
||||
# declares but never wires to a default sequence or binding.
|
||||
#
|
||||
# This is best-effort: without protocol negotiation we can only *react* to a
|
||||
# CSI-u sequence a terminal already emits, not *request* one. Terminals that
|
||||
# collapse Shift+Enter into a plain Enter are indistinguishable and fall back
|
||||
# to Alt+Enter, which is why Alt+Enter stays the primary shortcut.
|
||||
with suppress(Exception):
|
||||
from prompt_toolkit.input import ansi_escape_sequences as _aes
|
||||
|
||||
_aes.ANSI_SEQUENCES.setdefault("\x1b[13;2u", Keys.ControlF3)
|
||||
|
||||
kb = KeyBindings()
|
||||
|
||||
@kb.add("enter")
|
||||
@@ -327,6 +352,10 @@ def _build_cli_key_bindings() -> KeyBindings:
|
||||
def _(event):
|
||||
event.current_buffer.insert_text("\n")
|
||||
|
||||
@kb.add(Keys.ControlF3) # Shift+Enter on CSI-u capable terminals
|
||||
def _(event):
|
||||
event.current_buffer.insert_text("\n")
|
||||
|
||||
return kb
|
||||
|
||||
|
||||
|
||||
@@ -127,6 +127,29 @@ async def test_alt_enter_inserts_newline_on_lf_terminals():
|
||||
assert result == "foo\nbar"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_csi_u_shift_enter_inserts_newline_not_raw_escape():
|
||||
"""Terminals speaking the CSI-u (kitty / fixterms) keyboard protocol send
|
||||
Shift+Enter as "\\x1b[13;2u". prompt_toolkit has no support for that protocol
|
||||
and would otherwise dump the raw escape bytes ("^[[13;2u") into the buffer;
|
||||
the binding must instead parse it as one keypress and insert a newline. Only
|
||||
a real PromptSession/parser exercises the ANSI_SEQUENCES registration.
|
||||
"""
|
||||
from prompt_toolkit.application import create_app_session
|
||||
from prompt_toolkit.input import create_pipe_input
|
||||
from prompt_toolkit.output import DummyOutput
|
||||
|
||||
with create_pipe_input() as pipe_input:
|
||||
with create_app_session(input=pipe_input, output=DummyOutput()):
|
||||
commands._init_prompt_session()
|
||||
session = commands._PROMPT_SESSION
|
||||
pipe_input.send_text("foo\x1b[13;2ubar\r")
|
||||
result = await session.prompt_async("> ")
|
||||
|
||||
# A newline is inserted and no raw escape bytes leak into the result.
|
||||
assert result == "foo\nbar"
|
||||
|
||||
|
||||
def test_thinking_spinner_pause_stops_and_restarts():
|
||||
"""Pause should stop the active spinner and restart it afterward."""
|
||||
spinner = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user