diff options
| author | Eric Yue <jiacheng.yue@foxmail.com> | 2026-09-15 03:02:55 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-14 21:02:55 +0200 |
| commit | 4c3b613c8470885df44f0844b8eaecca744c84fb (patch) | |
| tree | eaafdf24efa3203685165ec352af7cdf14b4db91 | |
| parent | 01a9733f59ef205d113359fa778ae5521ac9cc9f (diff) | |
fix: keep modified enter compatible with legacy panes (#4110)
* fix: keep modified enter compatible with legacy panes
refs #4103
* fix: synchronize modified key negotiation state
refs #4103
* test: cover windows legacy modified enter
refs #4103
---------
Co-authored-by: Jonathan Liebig <jonathan.liebig@gmail.com>
| -rw-r--r-- | src/pane/kitty_keyboard.rs | 29 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 169 |
2 files changed, 197 insertions, 1 deletions
diff --git a/src/pane/kitty_keyboard.rs b/src/pane/kitty_keyboard.rs index 74de2538..bc8876e6 100644 --- a/src/pane/kitty_keyboard.rs +++ b/src/pane/kitty_keyboard.rs @@ -53,7 +53,6 @@ impl KittyKeyboardTracker { match bytes[end] { b'u' => self.observe_csi_u(&bytes[index + 2..end]), b'm' => self.observe_modify_other_keys(&bytes[index + 2..end]), - #[cfg(windows)] b'n' if bytes[index + 2..end] .strip_prefix(b">") .is_some_and(|params| { @@ -210,4 +209,32 @@ mod tests { tracker.observe(b"\x1b[>4;0m"); assert_eq!(tracker.modify_other_keys_level(), 0); } + + #[test] + fn modify_other_keys_disable_clears_state_across_chunks() { + for level in [1, 2] { + for sequence in [b"\x1b[>4n".as_slice(), b"\x1b[>04n".as_slice()] { + for split in 0..=sequence.len() { + let mut tracker = KittyKeyboardTracker::default(); + tracker.observe(b"\x1b[>5u"); + tracker.observe(format!("\x1b[>4;{level}m").as_bytes()); + tracker.observe(b"\x1b[4n\x1b[>1n\x1b[>4;2n"); + assert_eq!(tracker.modify_other_keys_level(), level); + + tracker.observe(&sequence[..split]); + tracker.observe(&sequence[split..]); + + assert_eq!( + tracker.modify_other_keys_level(), + 0, + "mode {level}, sequence {sequence:?}, split {split}" + ); + assert_eq!(tracker.flags, 5); + assert_eq!(tracker.stack, vec![0]); + #[cfg(unix)] + assert_eq!(tracker.replay_ansi().as_deref(), Some("\x1b[>5u")); + } + } + } + } } diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 931d0485..4a6bc173 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1987,6 +1987,10 @@ impl GhosttyPaneTerminal { .kitty_keyboard_flags() .is_ok_and(|flags| flags == 0) && !core.kitty_keyboard.modify_other_keys_enabled() + && core + .terminal + .modify_other_keys_enabled() + .is_ok_and(|enabled| !enabled) }) { if let Some(bytes) = crate::platform::encode_windows_conpty_fallback(&key) { return bytes; @@ -2011,6 +2015,24 @@ impl GhosttyPaneTerminal { key: crate::input::TerminalKey, protocol: crate::input::KeyboardProtocol, ) -> Vec<u8> { + // Ghostty emits extended Enter sequences even without negotiation. + // Ordinary shells need legacy input unless the child enabled an extension. + if key.code == crossterm::event::KeyCode::Enter + && !key.modifiers.is_empty() + && self.core.lock().is_ok_and(|core| { + core.terminal + .kitty_keyboard_flags() + .is_ok_and(|flags| flags == 0) + && core.kitty_keyboard.modify_other_keys_level() == 0 + && core + .terminal + .modify_other_keys_enabled() + .is_ok_and(|enabled| !enabled) + }) + { + return crate::input::encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy); + } + if matches!(protocol, crate::input::KeyboardProtocol::Legacy) && key.code == crossterm::event::KeyCode::Tab && key.modifiers == crossterm::event::KeyModifiers::CONTROL @@ -4572,6 +4594,126 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn ghostty_legacy_modified_enter_is_shell_compatible() { + use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; + + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + let protocol = crate::input::KeyboardProtocol::Legacy; + + for modifiers in [ + KeyModifiers::empty(), + KeyModifiers::SHIFT, + KeyModifiers::CONTROL, + KeyModifiers::SUPER, + KeyModifiers::ALT, + KeyModifiers::CONTROL | KeyModifiers::SHIFT, + KeyModifiers::ALT | KeyModifiers::SHIFT, + KeyModifiers::ALT | KeyModifiers::CONTROL | KeyModifiers::SUPER, + ] { + let key = crate::input::TerminalKey::new(KeyCode::Enter, modifiers); + let expected = if modifiers.contains(KeyModifiers::ALT) { + b"\x1b\r".as_slice() + } else { + b"\r".as_slice() + }; + for kind in [KeyEventKind::Press, KeyEventKind::Repeat] { + assert_eq!( + pane.encode_terminal_key(key.clone().with_kind(kind), protocol), + expected, + "{modifiers:?} {kind:?}" + ); + } + assert_eq!( + pane.encode_terminal_key(key.clone().with_repeat_count(3), protocol), + expected.repeat(3), + "{modifiers:?} grouped repeat" + ); + assert!( + pane.encode_terminal_key(key.with_kind(KeyEventKind::Release), protocol) + .is_empty(), + "{modifiers:?} release" + ); + } + } + + #[cfg(unix)] + #[test] + fn ghostty_modified_enter_tracks_live_protocol_negotiation() { + use crossterm::event::{KeyCode, KeyModifiers}; + + 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); + let legacy = ["\r", "\r", "\r", "\x1b\r"]; + let mode_one = ["\x1b[27;2;13~", "\x1b[27;5;13~", "\x1b[27;9;13~", "\x1b\r"]; + let mode_two = [ + "\x1b[27;2;13~", + "\x1b[27;5;13~", + "\x1b[27;9;13~", + "\x1b[27;3;13~", + ]; + let kitty = ["\x1b[13;2u", "\x1b[13;5u", "\x1b[13;9u", "\x1b[13;3u"]; + + for (sequence, expected) in [ + ("", legacy), + ("\x1b[>4;1m", mode_one), + ("\x1b[>4;2m", mode_two), + ("\x1b[>4n", legacy), + ("\x1b[>4;2m", mode_two), + ("\x1b[>4;0m", legacy), + ("\x1b[>5u", kitty), + ("\x1b[<u", legacy), + ("\x1b[>4;2m\x1b[>1u", kitty), + ("\x1b[<u", mode_two), + ("\x1b[>4;0m", legacy), + ("\x1b[>4;1m", mode_one), + ("\x1b[>04n", legacy), + ("\x1b[>4;2m", mode_two), + ("\x1b[>4", mode_two), + ("n", legacy), + ] { + pane.process_pty_bytes(pane_id, 0, sequence.as_bytes(), &tx); + for (modifiers, expected) in [ + KeyModifiers::SHIFT, + KeyModifiers::CONTROL, + KeyModifiers::SUPER, + KeyModifiers::ALT, + ] + .into_iter() + .zip(expected) + { + let key = crate::input::TerminalKey::new(KeyCode::Enter, modifiers); + assert_eq!( + pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy), + expected.as_bytes(), + "{modifiers:?} after {sequence:?}" + ); + } + } + } + + #[test] + fn ghostty_modified_enter_respects_existing_terminal_mode() { + let (tx, _rx) = mpsc::channel(4); + let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + terminal.write(b"\x1b[>4;2m"); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + let key = crate::input::TerminalKey::new( + crossterm::event::KeyCode::Enter, + crossterm::event::KeyModifiers::SHIFT, + ); + + assert_eq!( + pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy), + b"\x1b[27;2;13~" + ); + } + #[test] fn ghostty_enter_backspace_release_in_legacy_pane_emits_nothing() { let (tx, _rx) = mpsc::channel(4); @@ -4921,6 +5063,33 @@ mod tests { ); } + #[cfg(windows)] + #[test] + fn windows_ghostty_default_pane_sends_legacy_modified_enter() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); + + for (modifiers, expected) in [ + // Shift+Enter keeps using the native ConPTY fallback so the child + // receives a real key record rather than a CSI 27 sequence. + ( + crossterm::event::KeyModifiers::SHIFT, + b"\x1b[13;28;13;1;16;1_".as_slice(), + ), + (crossterm::event::KeyModifiers::CONTROL, b"\r".as_slice()), + (crossterm::event::KeyModifiers::SUPER, b"\r".as_slice()), + (crossterm::event::KeyModifiers::ALT, b"\x1b\r".as_slice()), + ] { + let key = crate::input::TerminalKey::new(crossterm::event::KeyCode::Enter, modifiers); + assert_eq!( + pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy), + expected, + "{modifiers:?}" + ); + } + } + #[test] fn ghostty_modify_other_keys_mode_one_preserves_shift_enter() { let (tx, _rx) = mpsc::channel(4); |
