From 5924f9eb37426bfad7facf2d3e7102efa15dc157 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sun, 9 Aug 2026 04:59:54 +0300 Subject: [PATCH] fix(input): preserve alt-prefixed control keys refs #2514 --- docs/next/CHANGELOG.md | 1 + src/input/parse.rs | 26 ++++++++++++++++++++------ src/pane/terminal.rs | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 5d123a8ce7..72d3b0f52a 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -14,6 +14,7 @@ - Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay. ### Fixed +- Fish `Ctrl+Alt` keybindings now work in panes after legacy Alt-prefixed control bytes are decoded with both modifiers. (#2514) - `herdr config check` now reports unknown built-in theme names instead of silently accepting them. (#2452) - macOS `herdr --remote` clients now keep the accepted bridge socket blocking, preventing an immediate disconnect after the protocol handshake. (#2478, thanks @mathijshenquet) - Prefix keybindings now preserve Shift in WezTerm Kitty keyboard mode, so commands such as config reload no longer trigger their unshifted action. (#2435) diff --git a/src/input/parse.rs b/src/input/parse.rs index 085757c3d9..b2318339f8 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -87,12 +87,9 @@ fn parse_legacy_key_sequence(data: &str) -> Option { _ if data.starts_with('\x1b') => { let rest = data.strip_prefix('\x1b')?; if rest.chars().count() == 1 { - let ch = rest.chars().next()?; - let mut modifiers = KeyModifiers::ALT; - if ch.is_ascii_uppercase() { - modifiers |= KeyModifiers::SHIFT; - } - Some(TerminalKey::new(KeyCode::Char(ch), modifiers)) + let mut key = parse_legacy_key_sequence(rest)?; + key.modifiers |= KeyModifiers::ALT; + Some(key) } else { None } @@ -549,6 +546,23 @@ mod tests { assert_eq!(encode_terminal_key(key, KeyboardProtocol::Legacy), b"\x1bA"); } + #[test] + fn parse_legacy_alt_control_letter_composes_modifiers() { + let key = parse_terminal_key_sequence("\x1b\x06") + .expect("ctrl-alt-f legacy sequence should parse"); + assert_terminal_key_eq( + key.clone(), + KeyCode::Char('f'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + crossterm::event::KeyEventKind::Press, + None, + ); + assert_eq!( + encode_terminal_key(key, KeyboardProtocol::Legacy), + b"\x1b\x06" + ); + } + #[test] fn unknown_legacy_ss3_sequence_remains_unsupported() { assert!(parse_terminal_key_sequence("\x1bOz").is_none()); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 91fcb4fef7..a6e5c1da8d 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -4592,6 +4592,23 @@ mod tests { assert_eq!(encoded, b"\x1b[127;3u"); } + #[test] + fn ghostty_kitty_pane_preserves_legacy_ctrl_alt_letter() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.process_pty_bytes(pane_id, 0, b"\x1b[>5u", &tx); + + let mut events = crate::raw_input::parse_raw_input_bytes_sync(b"\x1b\x06"); + let crate::raw_input::RawInputEvent::Key(key) = events.remove(0) else { + panic!("expected key event"); + }; + let encoded = pane.encode_terminal_key(key, pane.keyboard_protocol().unwrap()); + + assert_eq!(encoded, b"\x1b[102;7u"); + } + #[test] fn ghostty_pane_characterizes_ctrl_backspace_encoding() { let (tx, _rx) = mpsc::channel(4);