diff --git a/src/app/mod.rs b/src/app/mod.rs index dc6de136..b2aa4ff2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -5441,8 +5441,8 @@ last_pane = "prefix+tab" let mut app = test_app(); let mut workspace = Workspace::test_new("test"); let focused = workspace.focused_pane_id().unwrap(); - let (runtime, mut rx) = - TerminalRuntime::test_with_channel_and_scrollback_bytes(80, 24, 0, b"\x1b[>4;1m", 4); + let (runtime, mut rx) = TerminalRuntime::test_with_channel(80, 24); + runtime.test_process_pty_bytes(b"\x1b[>4;1m"); workspace.tabs[0].runtimes.insert(focused, runtime); app.state.workspaces = vec![workspace]; app.state.active = Some(0); diff --git a/src/pane/kitty_keyboard.rs b/src/pane/kitty_keyboard.rs index 89824ee3..30ab62ba 100644 --- a/src/pane/kitty_keyboard.rs +++ b/src/pane/kitty_keyboard.rs @@ -3,6 +3,8 @@ pub(crate) struct KittyKeyboardTracker { pending: Vec, stack: Vec, flags: u16, + #[cfg(windows)] + modify_other_keys: bool, } impl KittyKeyboardTracker { @@ -30,6 +32,10 @@ impl KittyKeyboardTracker { self.store_pending(&bytes[index..]); break; } + #[cfg(windows)] + if bytes[index + 1] == b'c' { + self.modify_other_keys = false; + } if bytes[index + 1] != b'[' { index += 1; continue; @@ -44,13 +50,52 @@ impl KittyKeyboardTracker { break; } - if bytes[end] == b'u' { - self.observe_csi_u(&bytes[index + 2..end]); + match bytes[end] { + b'u' => self.observe_csi_u(&bytes[index + 2..end]), + #[cfg(windows)] + 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| { + !params.contains(&b';') && parse_kitty_keyboard_flags(params) == 4 + }) => + { + self.modify_other_keys = false; + } + _ => {} } index = end + 1; } } + #[cfg(windows)] + pub(crate) fn modify_other_keys_enabled(&self) -> bool { + self.modify_other_keys + } + + #[cfg(windows)] + fn observe_modify_other_keys(&mut self, params: &[u8]) { + let Some(params) = params.strip_prefix(b">") else { + return; + }; + if params.is_empty() { + self.modify_other_keys = false; + return; + } + + let mut parts = params.split(|byte| *byte == b';'); + let resource = parts.next().unwrap_or_default(); + let value = parts.next(); + if parts.next().is_some() { + return; + } + if parse_kitty_keyboard_flags(resource) == 4 { + self.modify_other_keys = + value.is_some_and(|value| parse_kitty_keyboard_flags(value) != 0); + } + } + fn store_pending(&mut self, bytes: &[u8]) { self.pending.clear(); if bytes.len() <= 64 { @@ -115,11 +160,19 @@ mod tests { fn buffers_split_csi_sequences() { let mut tracker = KittyKeyboardTracker::default(); - tracker.observe(b"\x1b[>1u\x1b[>5"); - tracker.observe(b"u\x1b[<"); + tracker.observe(b"\x1b[>1u\x1b[>4;"); + tracker.observe(b"01m\x1b[>5u\x1b[<"); tracker.observe(b"u"); assert_eq!(tracker.flags, 1); assert_eq!(tracker.stack, vec![0]); + #[cfg(windows)] + { + assert!(tracker.modify_other_keys_enabled()); + tracker.observe(b"\x1b[>1m"); + assert!(tracker.modify_other_keys_enabled()); + tracker.observe(b"\x1b[>04n"); + assert!(!tracker.modify_other_keys_enabled()); + } } } diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index d5070b9b..76ab20d3 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1286,6 +1286,8 @@ impl GhosttyPaneTerminal { let Ok(mut core) = self.core.lock() else { return; }; + #[cfg(windows)] + core.kitty_keyboard.observe(ansi.as_bytes()); core.terminal.write(ansi.as_bytes()); #[cfg(windows)] windows_recent_fallback::update(&mut core); @@ -1565,6 +1567,9 @@ impl GhosttyPaneTerminal { mouse_protocol_mode, mouse_protocol_encoding, mouse_alternate_scroll, + #[cfg(windows)] + modify_other_keys: core.kitty_keyboard.modify_other_keys_enabled(), + #[cfg(not(windows))] modify_other_keys: core .terminal .keyboard_state_ansi() @@ -1619,6 +1624,18 @@ impl GhosttyPaneTerminal { key: crate::input::TerminalKey, protocol: crate::input::KeyboardProtocol, ) -> Vec { + #[cfg(windows)] + if let Some(bytes) = crate::platform::encode_windows_conpty_shift_enter(key) { + 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() + }) { + return bytes; + } + } + if ghostty_prefers_herdr_text_encoding(key) { return crate::input::encode_terminal_key(key, protocol); } @@ -3967,11 +3984,16 @@ mod tests { fn ghostty_modify_other_keys_mode_one_preserves_shift_enter() { 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[>4;1m", &tx); - + let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); let key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap(); + + #[cfg(windows)] + assert_eq!( + pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy), + b"\x1b[13;28;13;1;16;1_" + ); + + pane.seed_history_ansi("\x1b[>4;1m"); let encoded = pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy); assert_eq!(encoded, b"\x1b[27;2;13~"); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index d77a30be..8a171716 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -56,6 +56,17 @@ use super::{ClipboardImage, ForegroundJob, Signal}; const STILL_ACTIVE: u32 = 259; const FOREGROUND_PROCESS_SNAPSHOT_CACHE_TTL: Duration = Duration::from_millis(250); +pub(crate) fn encode_windows_conpty_shift_enter(key: crate::input::TerminalKey) -> Option> { + use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; + + if key.code != KeyCode::Enter || key.modifiers != KeyModifiers::SHIFT { + return None; + } + + let key_down = !matches!(key.kind, KeyEventKind::Release); + Some(format!("\x1b[13;28;13;{};16;1_", u8::from(key_down)).into_bytes()) +} + #[derive(Debug)] struct CachedProcessSnapshot { built_at: Instant,