Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1895,6 +1895,29 @@ def _preserve_ctrl_enter_newline() -> bool:
return False


def _submit_on_lf_opt_in() -> bool:
"""Return whether bare LF should force submit in the classic CLI."""
raw = os.environ.get("HERMES_CLI_SUBMIT_ON_LF", "")
return raw.strip().lower() in {"1", "true", "yes", "on"}


def _c_j_inserts_newline() -> bool:
"""Return whether bare LF/c-j should stay available for multiline input.

Windows/WSL/SSH/WT already need bare LF reserved for Ctrl+Enter newline.
Local macOS terminals can also collapse Shift+Enter into LF, so default to
newline there as well. Users on thin PTYs that genuinely deliver plain
Enter as LF can opt back into LF-submit with HERMES_CLI_SUBMIT_ON_LF=1.
"""
if sys.platform == "win32":
return True
if _preserve_ctrl_enter_newline():
return True
if sys.platform == "darwin" and not _submit_on_lf_opt_in():
return True
return False


def _bind_prompt_submit_keys(kb, handler) -> None:
"""Bind terminal Enter forms to the submit handler.

Expand All @@ -1904,13 +1927,13 @@ def _bind_prompt_submit_keys(kb, handler) -> None:

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.
plain Enter / c-m). Local macOS terminals can also collapse Shift+Enter
to bare LF. We leave c-j unbound in those cases so the newline handler
registered separately can fire, while thin-PTY LF-submit remains
available via HERMES_CLI_SUBMIT_ON_LF=1. See issue #22379 and #22908.
"""
kb.add("enter")(handler)
if sys.platform != "win32" and not _preserve_ctrl_enter_newline():
if not _c_j_inserts_newline():
kb.add("c-j")(handler)


Expand Down Expand Up @@ -10890,19 +10913,19 @@ def handle_alt_enter(event):
"""
event.current_buffer.insert_text('\n')

if _preserve_ctrl_enter_newline():
if _c_j_inserts_newline():
@kb.add('c-j')
def handle_ctrl_enter_newline(event):
"""Ctrl+Enter inserts a newline on Windows, WSL, SSH, and WT.
"""LF-backed Enter variants insert a newline when reserved.

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.
terminals, and local macOS terminals can collapse Shift+Enter
to the same LF path. 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 and #22908.
"""
event.current_buffer.insert_text('\n')

Expand Down
21 changes: 21 additions & 0 deletions tests/cli/test_cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,27 @@ def submit_handler(event):
assert bindings[("c-m",)] is submit_handler
assert ("c-j",) not in bindings

# Local macOS: keep c-j free so Shift+Enter paths that collapse to LF
# still insert a newline instead of submitting.
with _patch.object(_sys, "platform", "darwin"), \
_patch.dict(_os.environ, {}, clear=True), \
_patch("builtins.open", side_effect=OSError("no /proc")):
kb = KeyBindings()
_bind_prompt_submit_keys(kb, submit_handler)
bindings = {tuple(key.value for key in binding.keys): binding.handler for binding in kb.bindings}
assert bindings[("c-m",)] is submit_handler
assert ("c-j",) not in bindings

# Thin PTY opt-in: users who truly need Enter=LF can force c-j back to submit.
with _patch.object(_sys, "platform", "darwin"), \
_patch.dict(_os.environ, {"HERMES_CLI_SUBMIT_ON_LF": "1"}, clear=True), \
_patch("builtins.open", side_effect=OSError("no /proc")):
kb = KeyBindings()
_bind_prompt_submit_keys(kb, submit_handler)
bindings = {tuple(key.value for key in binding.keys): binding.handler for binding in kb.bindings}
assert bindings[("c-m",)] is submit_handler
assert bindings[("c-j",)] is submit_handler

def test_cpr_warning_callback_is_disabled(self):
from cli import _disable_prompt_toolkit_cpr_warning

Expand Down
16 changes: 16 additions & 0 deletions tests/cli/test_ctrl_enter_newline.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ def test_pure_local_linux_does_not_preserve():
assert cli_mod._preserve_ctrl_enter_newline() is False


def test_local_macos_uses_lf_for_newline_by_default():
import cli as cli_mod
with patch.object(sys, "platform", "darwin"):
with patch.dict(os.environ, {}, clear=True):
with patch("builtins.open", side_effect=OSError("no /proc")):
assert cli_mod._c_j_inserts_newline() is True


def test_local_macos_can_opt_into_lf_submit():
import cli as cli_mod
with patch.object(sys, "platform", "darwin"):
with patch.dict(os.environ, {"HERMES_CLI_SUBMIT_ON_LF": "1"}, clear=True):
with patch("builtins.open", side_effect=OSError("no /proc")):
assert cli_mod._c_j_inserts_newline() is False


def test_proc_version_microsoft_marker_preserves_newline():
"""WSL detection via /proc when env vars are scrubbed (sudo etc.)."""
import cli as cli_mod
Expand Down
Loading