fix(cli): make Ctrl+Enter insert newline on WSL/SSH/Windows Terminal (#22777)

Native Windows, WSL, SSH sessions, and Windows Terminal all send
Ctrl+Enter as bare LF (c-j). Hermes was binding c-j as submit on
every POSIX platform, so Ctrl+Enter submitted instead of inserting
a newline on those terminals. Reported in #22379.

Add _preserve_ctrl_enter_newline() predicate that detects the
environments where Ctrl+Enter must produce a newline (sys.platform
== 'win32', SSH_CONNECTION/SSH_CLIENT/SSH_TTY env, WT_SESSION,
WSL_DISTRO_NAME, /proc/version 'microsoft' marker). Gate the
c-j-as-submit binding off in those environments and gate the
c-j-as-newline handler on. Local POSIX TTYs without those markers
(docker exec, plain ssh from a Mac) keep c-j as submit so plain
Enter still works on thin PTYs.

Add install_ctrl_enter_alias() in hermes_cli/pt_input_extras.py
mapping the three CSI-u / modifyOtherKeys variants of Ctrl+Enter
('\x1b[13;5u', '\x1b[27;5;13~', '\x1b[27;5;13u') to the
(Escape, ControlM) tuple Alt+Enter produces. This lets Kitty /
mintty / xterm-with-modifyOtherKeys users over SSH get a Ctrl+Enter
newline through the existing Alt+Enter handler.

9 new tests + extended existing test_lf_enter_binds_to_submit_handler_posix
to cover bare-local vs SSH branches.

Closes #22379.
This commit is contained in:
Teknium 2026-05-09 12:48:14 -07:00 committed by GitHub
parent 2124ad72a2
commit 70bc52e408
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 210 additions and 24 deletions

69
cli.py
View file

@ -72,9 +72,10 @@ except (ImportError, AttributeError):
_STEADY_CURSOR = None
try:
from hermes_cli.pt_input_extras import install_shift_enter_alias
from hermes_cli.pt_input_extras import install_shift_enter_alias, install_ctrl_enter_alias
install_shift_enter_alias()
del install_shift_enter_alias
install_ctrl_enter_alias()
del install_shift_enter_alias, install_ctrl_enter_alias
except Exception:
pass
import threading
@ -1862,6 +1863,37 @@ _TERMINAL_INPUT_MODE_RESET_SEQ = (
)
def _preserve_ctrl_enter_newline() -> bool:
"""Detect environments where Ctrl+Enter must produce a newline, not submit.
Native Windows, WSL, SSH sessions, and Windows Terminal all send Ctrl+Enter
as bare LF (c-j). On those terminals c-j must NOT be bound to submit;
binding it to submit makes Ctrl+Enter (intended as 'newline like Alt+Enter')
submit instead. Local POSIX TTYs that deliver Enter as LF (docker exec,
some thin PTYs without SSH) still need c-j bound to submit, so we keep
that binding for those.
See issue #22379.
"""
if sys.platform == "win32":
return True
if any(os.environ.get(v) for v in ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY")):
return True
if os.environ.get("WT_SESSION"):
return True
if "microsoft" in os.environ.get("WSL_DISTRO_NAME", "").lower():
return True
# WSL detection — env vars can be scrubbed under sudo, also peek /proc.
for p in ("/proc/version", "/proc/sys/kernel/osrelease"):
try:
with open(p, "r", encoding="utf-8", errors="ignore") as f:
if "microsoft" in f.read().lower():
return True
except OSError:
continue
return False
def _bind_prompt_submit_keys(kb, handler) -> None:
"""Bind terminal Enter forms to the submit handler.
@ -1869,13 +1901,15 @@ def _bind_prompt_submit_keys(kb, handler) -> None:
some thin PTYs (docker exec, certain SSH flavors) deliver Enter as LF
instead of CR without this, Enter appears dead on those terminals.
On Windows, Windows Terminal delivers Ctrl+Enter as a distinct c-j key
while plain Enter is c-m, so we leave c-j unbound here it becomes the
multi-line newline keystroke, giving Windows users an Enter-involving
newline without any terminal settings changes.
Exception: on Windows, WSL, SSH sessions, and Windows Terminal,
c-j is the wire encoding of Ctrl+Enter (a distinct keystroke from
plain Enter / c-m). We leave c-j unbound there so the c-j newline
handler registered separately can fire giving the user an
Enter-involving newline keystroke without terminal settings changes.
See _preserve_ctrl_enter_newline() and issue #22379.
"""
kb.add("enter")(handler)
if sys.platform != "win32":
if sys.platform != "win32" and not _preserve_ctrl_enter_newline():
kb.add("c-j")(handler)
@ -10855,18 +10889,19 @@ class HermesCLI:
"""
event.current_buffer.insert_text('\n')
if sys.platform == "win32":
if _preserve_ctrl_enter_newline():
@kb.add('c-j')
def handle_ctrl_enter_newline_windows(event):
"""Ctrl+Enter inserts a newline on Windows.
def handle_ctrl_enter_newline(event):
"""Ctrl+Enter inserts a newline on Windows, WSL, SSH, and WT.
Windows Terminal delivers Ctrl+Enter as LF (c-j), distinct
from plain Enter (c-m). This binding makes Ctrl+Enter the
Windows equivalent of Alt+Enter, giving an Enter-involving
newline keystroke without requiring terminal settings changes.
Ctrl+J (the raw LF keystroke) also triggers this by virtue
of being the same key code a harmless side effect since
Ctrl+J has no conflicting Hermes binding.
Windows Terminal (incl. WSL/SSH sessions through it) delivers
Ctrl+Enter as LF (c-j), distinct from plain Enter (c-m). This
binding makes Ctrl+Enter the equivalent of Alt+Enter on those
terminals, giving an Enter-involving newline keystroke
without requiring terminal settings changes. Ctrl+J (the raw
LF keystroke) also triggers this by virtue of being the same
key code a harmless side effect since Ctrl+J has no
conflicting Hermes binding. See issue #22379.
"""
event.current_buffer.insert_text('\n')