fix: remove unreliable Shift+Enter shortcut

maintainer edit: keep Alt+Enter as the supported multiline input path and remove the terminal-dependent Shift+Enter ANSI sequence patch.
This commit is contained in:
chengyongru
2026-07-07 15:41:08 +08:00
committed by Xubin Ren
parent 77a6003255
commit 01a0f5aaf3
2 changed files with 8 additions and 70 deletions
+1 -33
View File
@@ -312,34 +312,7 @@ def _build_cli_key_bindings() -> KeyBindings:
* Alt+Enter -> insert a newline. Universally supported across
terminal emulators, so this is the reliable way to
compose multi-line input.
* Shift+Enter -> insert a newline *if* the terminal sends a dedicated
sequence for it (kitty / iTerm2 with the CSI-u /
fixterms keyboard protocol). Plain terminals collapse
Shift+Enter into Enter and cannot be distinguished, in
which case Alt+Enter is the fallback.
"""
# prompt_toolkit has no symbolic Keys.ShiftEnter and @kb.add() rejects raw
# escape strings, so register the CSI-u Shift+Enter sequences against
# Keys.ControlF3 -- an enum member prompt_toolkit declares but never wires
# to a default ANSI sequence or key binding, unlike e.g. Keys.ControlJ,
# which is also the literal LF byte ("\x0a") that some terminals (WSL is
# the case prompt_toolkit itself calls out) send for a plain Enter
# keypress. Aliasing to ControlJ made Enter stop submitting there, since
# our handler shadowed prompt_toolkit's own "treat \n as \r" fallback.
#
# "\x1b[27;2;13~" (the older xterm modifyOtherKeys / rxvt encoding of
# Shift+Enter) is already registered by prompt_toolkit by default -- as
# Keys.ControlM, i.e. plain Enter/submit. A plain setdefault() would be a
# no-op against that existing entry, silently leaving this Shift+Enter
# variant behaving like a submit instead of inserting a newline. Assign
# directly to override it, since inserting a newline is the whole point
# of a Shift+Enter binding here.
with suppress(Exception):
from prompt_toolkit.input import ansi_escape_sequences as _aes
for _seq in ("\x1b[13;2u", "\x1b[27;2;13~"):
_aes.ANSI_SEQUENCES[_seq] = Keys.ControlF3
kb = KeyBindings()
@kb.add("enter")
@@ -359,10 +332,6 @@ 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
@@ -385,8 +354,7 @@ def _init_prompt_session() -> None:
history=SafeFileHistory(str(history_file)),
enable_open_in_editor=False,
# Multiline-capable buffer; Enter still submits via the custom key
# bindings, while Shift+Enter (supported terminals) or Alt+Enter adds
# a newline.
# bindings, while Alt+Enter adds a newline.
multiline=True,
key_bindings=_build_cli_key_bindings(),
)
+7 -37
View File
@@ -46,8 +46,8 @@ def test_init_prompt_session_creates_session():
# Ensure global is None before test
commands._PROMPT_SESSION = None
with patch("nanobot.cli.commands.PromptSession") as MockSession, \
patch("nanobot.cli.commands.FileHistory") as MockHistory, \
with patch("nanobot.cli.commands.PromptSession") as mock_session_cls, \
patch("nanobot.cli.commands.FileHistory"), \
patch("pathlib.Path.home") as mock_home:
mock_home.return_value = MagicMock()
@@ -55,17 +55,17 @@ def test_init_prompt_session_creates_session():
commands._init_prompt_session()
assert commands._PROMPT_SESSION is not None
MockSession.assert_called_once()
_, kwargs = MockSession.call_args
# Buffer is multiline-capable so Shift+Enter / Alt+Enter can insert
# newlines; Enter-to-submit is restored via custom key bindings.
mock_session_cls.assert_called_once()
_, kwargs = mock_session_cls.call_args
# Buffer is multiline-capable so Alt+Enter can insert newlines;
# Enter-to-submit is restored via custom key bindings.
assert kwargs["multiline"] is True
assert kwargs["enable_open_in_editor"] is False
assert kwargs.get("key_bindings") is not None
def test_cli_key_bindings_enter_submits_and_alt_enter_newlines():
"""Enter submits the buffer; Alt+Enter and Shift+Enter insert a newline."""
"""Enter submits the buffer; Alt+Enter inserts a newline."""
from prompt_toolkit.keys import Keys
kb = commands._build_cli_key_bindings()
@@ -88,14 +88,6 @@ def test_cli_key_bindings_enter_submits_and_alt_enter_newlines():
alt_enter.call(MagicMock(current_buffer=buf))
buf.insert_text.assert_called_once_with("\n")
# ControlF3 (synthetic carrier for the Shift+Enter CSI-u sequence) -> newline.
# Not ControlJ: that's also the literal LF byte some terminals (e.g. WSL)
# send for a plain Enter keypress, so binding it would break submit there.
shift_enter = bound[(Keys.ControlF3.value,)]
buf = MagicMock()
shift_enter.call(MagicMock(current_buffer=buf))
buf.insert_text.assert_called_once_with("\n")
@pytest.mark.asyncio
async def test_raw_lf_enter_still_submits_like_wsl_terminals():
@@ -118,28 +110,6 @@ async def test_raw_lf_enter_still_submits_like_wsl_terminals():
assert result == "hello"
@pytest.mark.asyncio
async def test_xterm_modifyotherkeys_shift_enter_inserts_newline():
""""\\x1b[27;2;13~" (the older xterm modifyOtherKeys/rxvt encoding of
Shift+Enter) is registered by prompt_toolkit *by default* as plain
Enter/submit, so a `setdefault()`-based override would silently no-op
against it. This must actually insert a newline like the kitty CSI-u
encoding does, which only a real PromptSession/parser can confirm.
"""
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[27;2;13~bar\r")
result = await session.prompt_async("> ")
assert result == "foo\nbar"
@pytest.mark.asyncio
async def test_alt_enter_inserts_newline_on_lf_terminals():
"""On LF-as-Enter terminals (WSL), Alt+Enter arrives as ESC + LF