From fc8ae002898b0d9f554128d3622e95218d128ecc Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 6 Oct 2026 00:46:32 +0300 Subject: [PATCH] refactor: read live keyboard modes only from libghostty --- src/client/shell/tests/input_conformance.rs | 4 +- src/pane/kitty_keyboard.rs | 10 ++-- src/pane/terminal.rs | 62 +++++++++++---------- src/server/headless/tests/mod.rs | 5 +- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/client/shell/tests/input_conformance.rs b/src/client/shell/tests/input_conformance.rs index 0c02f9f92..92cab31bd 100644 --- a/src/client/shell/tests/input_conformance.rs +++ b/src/client/shell/tests/input_conformance.rs @@ -225,8 +225,10 @@ impl Oracle { } } +/// Panes that negotiated nothing. Like Ghostty, modifyOtherKeys level 1 counts +/// as nothing. fn is_legacy_pane(pane_mode: &[u8]) -> bool { - matches!(pane_mode, b"" | b"\x1b[?1h") + matches!(pane_mode, b"" | b"\x1b[?1h" | b"\x1b[>4;1m") } /// Agreed exception table: panes that negotiated no keyboard protocol keep the diff --git a/src/pane/kitty_keyboard.rs b/src/pane/kitty_keyboard.rs index bc8876e6e..53092bd5b 100644 --- a/src/pane/kitty_keyboard.rs +++ b/src/pane/kitty_keyboard.rs @@ -1,3 +1,7 @@ +/// Handoff-only record of the keyboard modes a pane's app requested, so an +/// update handoff can replay the full Kitty keyboard stack into the new server. +/// It is never consulted for live input: libghostty owns keyboard state and +/// encoding. Remove once handoff restores panes from libghostty snapshots. #[derive(Debug, Clone, Default)] pub(crate) struct KittyKeyboardTracker { pending: Vec, @@ -67,15 +71,11 @@ impl KittyKeyboardTracker { } } + #[cfg(test)] pub(crate) fn modify_other_keys_level(&self) -> u8 { self.modify_other_keys_level } - #[cfg(windows)] - pub(crate) fn modify_other_keys_enabled(&self) -> bool { - self.modify_other_keys_level > 0 - } - fn observe_modify_other_keys(&mut self, params: &[u8]) { let Some(params) = params.strip_prefix(b">") else { return; diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 41b956909..bd6afd741 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1855,10 +1855,16 @@ impl GhosttyPaneTerminal { .is_ok_and(|core| core.terminal.mouse_tracking_enabled().unwrap_or(false)) } + /// libghostty tracks only level 2; like Ghostty, level 1 behaves as if the + /// app negotiated nothing. pub fn modify_other_keys_level(&self) -> u8 { - self.core - .lock() - .map_or(0, |core| core.kitty_keyboard.modify_other_keys_level()) + self.core.lock().map_or(0, |core| { + if core.terminal.modify_other_keys_enabled().unwrap_or(false) { + 2 + } else { + 0 + } + }) } pub fn sgr_pixel_mouse_enabled(&self) -> bool { @@ -2024,20 +2030,15 @@ impl GhosttyPaneTerminal { pub fn encode_terminal_key( &self, key: crate::input::TerminalKey, - // Unused: libghostty reads the live modes. Removed with the shadow tracker. + // Unused: libghostty reads the live modes. _protocol: crate::input::KeyboardProtocol, ) -> Vec { #[cfg(windows)] - if self.core.lock().is_ok_and(|core| { - core.terminal - .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 self + .core + .lock() + .is_ok_and(|core| keyboard_negotiated_nothing(&core.terminal)) + { if let Some(bytes) = crate::platform::encode_windows_conpty_fallback(&key) { return bytes; } @@ -2062,15 +2063,7 @@ impl GhosttyPaneTerminal { let Ok(core) = self.core.lock() else { return Vec::new(); }; - let negotiated_nothing = 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); + let negotiated_nothing = keyboard_negotiated_nothing(&core.terminal); let key = if negotiated_nothing { legacy_shell_key(key) } else { @@ -2507,6 +2500,17 @@ impl GhosttyPaneTerminal { } } +/// True when the pane's app asked for no enhanced keyboard protocol (no Kitty +/// flags, no modifyOtherKeys 2), read from libghostty's live state. +fn keyboard_negotiated_nothing(terminal: &crate::ghostty::Terminal) -> bool { + terminal + .kitty_keyboard_flags() + .is_ok_and(|flags| flags == 0) + && terminal + .modify_other_keys_enabled() + .is_ok_and(|enabled| !enabled) +} + fn effective_cursor_state( core: &mut GhosttyPaneCore, current: Option, @@ -4830,8 +4834,8 @@ mod tests { 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); + // Like Ghostty, modifyOtherKeys level 1 behaves as if nothing was negotiated. 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~", @@ -4842,7 +4846,7 @@ mod tests { for (sequence, expected) in [ ("", legacy), - ("\x1b[>4;1m", mode_one), + ("\x1b[>4;1m", legacy), ("\x1b[>4;2m", mode_two), ("\x1b[>4n", legacy), ("\x1b[>4;2m", mode_two), @@ -4852,7 +4856,7 @@ mod tests { ("\x1b[>4;2m\x1b[>1u", kitty), ("\x1b[4;0m", legacy), - ("\x1b[>4;1m", mode_one), + ("\x1b[>4;1m", legacy), ("\x1b[>04n", legacy), ("\x1b[>4;2m", mode_two), ("\x1b[>4", mode_two), @@ -5317,17 +5321,17 @@ mod tests { } #[test] - fn ghostty_modify_other_keys_mode_one_preserves_shift_enter() { + fn ghostty_modify_other_keys_mode_one_is_treated_as_unnegotiated() { let (tx, _rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); let key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap(); pane.seed_history_ansi("\x1b[>4;1m"); - assert_eq!(pane.modify_other_keys_level(), 1); + assert_eq!(pane.modify_other_keys_level(), 0); let encoded = pane.encode_terminal_key(key.clone(), crate::input::KeyboardProtocol::Legacy); - assert_eq!(encoded, b"\x1b[27;2;13~"); + assert_eq!(encoded, b"\r"); } #[test] diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs index a9687e873..e65dd81bc 100644 --- a/src/server/headless/tests/mod.rs +++ b/src/server/headless/tests/mod.rs @@ -6222,11 +6222,12 @@ fn direct_terminal_streams_child_keyboard_and_mouse_modes() { read_server_message( client_control_rx .recv_timeout(Duration::from_millis(100)) - .expect("modifyOtherKeys mode-one keyboard message") + .expect("kitty flags change with modifyOtherKeys mode one") ), + // Like Ghostty, modifyOtherKeys level 1 is not a negotiated mode. ServerMessage::DirectTerminalKeyboardProtocol { flags: 3, - modify_other_keys_level: 1 + modify_other_keys_level: 0 } ));