aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEric Yue <jiacheng.yue@foxmail.com>2026-09-15 03:02:55 +0800
committerGitHub <noreply@github.com>2026-09-14 21:02:55 +0200
commit4c3b613c8470885df44f0844b8eaecca744c84fb (patch)
treeeaafdf24efa3203685165ec352af7cdf14b4db91 /src
parent01a9733f59ef205d113359fa778ae5521ac9cc9f (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>
Diffstat (limited to 'src')
-rw-r--r--src/pane/kitty_keyboard.rs29
-rw-r--r--src/pane/terminal.rs169
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);