diff --git a/cli.py b/cli.py index d474e405a16e..6bc880901487 100644 --- a/cli.py +++ b/cli.py @@ -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. @@ -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) @@ -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') diff --git a/tests/cli/test_cli_init.py b/tests/cli/test_cli_init.py index ee5ffb390d13..7d88b68030c6 100644 --- a/tests/cli/test_cli_init.py +++ b/tests/cli/test_cli_init.py @@ -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 diff --git a/tests/cli/test_ctrl_enter_newline.py b/tests/cli/test_ctrl_enter_newline.py index 57056ab0e189..5590c12cb09a 100644 --- a/tests/cli/test_ctrl_enter_newline.py +++ b/tests/cli/test_ctrl_enter_newline.py @@ -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