From 6fe50416f1fc6014cb95f3391ba0695cf004b746 Mon Sep 17 00:00:00 2001 From: Alton Johnson Date: Tue, 28 Apr 2026 18:06:49 -0700 Subject: [PATCH] Forward Opt+Enter as ESC+CR to alt-screen programs on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a TUI like Claude, vim, or fish runs in alt-screen mode, pressing Option+Enter on macOS without "Option as Meta" enabled was sending a bare `\r` to the PTY instead of the xterm-style `\x1b\r`. The legacy escape sequence path early-returned on Mac whenever `keystroke.meta` was unset, which is correct for letter keys (Opt+E = é) but wrong for Enter, which never has an Option-modified glyph. Allow alt-enter through the meta path and add `"enter" => "\r"` to the special-key map so the existing meta path also handles users who have Option-as-Meta enabled. --- .../src/model/escape_sequences.rs | 9 ++++++-- .../src/model/escape_sequences_test.rs | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/warp_terminal/src/model/escape_sequences.rs b/crates/warp_terminal/src/model/escape_sequences.rs index a92d2b07..6b503d8d 100644 --- a/crates/warp_terminal/src/model/escape_sequences.rs +++ b/crates/warp_terminal/src/model/escape_sequences.rs @@ -551,6 +551,7 @@ fn cursor_movement_keystroke_to_escape_sequence( fn map_special_key_to_bytes(key: &str) -> Option<&[u8]> { match key { "backspace" => Some("\x7f".as_bytes()), + "enter" => Some("\r".as_bytes()), "insert" => Some("\x1b[2~".as_bytes()), "delete" => Some("\x1b[3~".as_bytes()), "pageup" => Some("\x1b[5~".as_bytes()), @@ -567,9 +568,13 @@ fn meta_keystroke_to_escape_sequence( _mode_provider: &impl ModeProvider, ) -> Option> { // On mac, we have a setting that allows users to map the Option keys to - // meta. + // meta. When that setting is off, Option+letter produces special glyphs + // (Option+E = é, etc.), so we don't treat alt as meta by default. The + // exception is Enter, which never has an Option-modified glyph — TUIs like + // claude/vim/fish expect Option+Enter to arrive as ESC+CR (xterm + // convention), so we let alt-enter through here too. if OperatingSystem::get().is_mac() { - if !keystroke.meta { + if !keystroke.meta && !(keystroke.alt && keystroke.key == "enter") { return None; } } else { diff --git a/crates/warp_terminal/src/model/escape_sequences_test.rs b/crates/warp_terminal/src/model/escape_sequences_test.rs index 8e2ebbd7..1ae24007 100644 --- a/crates/warp_terminal/src/model/escape_sequences_test.rs +++ b/crates/warp_terminal/src/model/escape_sequences_test.rs @@ -463,12 +463,34 @@ fn test_meta_keystroke_to_escape_sequence() { Keystroke::parse(metaify("'")).unwrap(), vec![C0::ESC, b'\''], ), + ( + Keystroke::parse(metaify("enter")).unwrap(), + vec![C0::ESC, b'\r'], + ), ]; let terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } +/// On macOS, Option+Enter must encode as ESC+CR even when the user has not +/// enabled Option-as-Meta. Without this, the bare `\r` falls through to the +/// PTY and TUIs like claude/vim/fish treat it as plain Enter (submit) rather +/// than the Alt+Enter sequence they expect for inserting a newline. +#[test] +fn test_alt_enter_on_mac_yields_esc_cr_without_option_as_meta() { + if !OperatingSystem::get().is_mac() { + return; + } + let test_cases: &[(Keystroke, Vec)] = &[( + Keystroke::parse("alt-enter").unwrap(), + vec![C0::ESC, b'\r'], + )]; + + let terminal_model_mock = TerminalModelMock::new(); + validate_keystroke_test_cases(test_cases, &terminal_model_mock); +} + #[test] fn test_unmatched_keystroke_does_not_yield_escape_sequence() { let test_cases: &[Keystroke] = &[