diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 49e77909..6404e586 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -497,6 +497,10 @@ impl WindowsInputMapper { return None; } + if record.virtual_key_code != 0 && (record.unicode < 0x20 || record.unicode == 0x7f) { + return None; + } + self.synthetic_utf16_unit_to_bytes(record.unicode) } @@ -1566,6 +1570,27 @@ mod tests { ); } + #[test] + fn vti_win32_input_mode_backspace_stays_backspace() { + let records = "\x1b[8;14;8;1;0;1_\x1b[8;14;8;0;0;1_".chars().map(key_char); + + assert_eq!( + translate(records), + vec![ + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Backspace, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + }, + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Backspace, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Release, + }, + ] + ); + } + #[test] fn vti_win32_input_mode_ctrl_j_preserves_lf_control_key() { let records = "\x1b[74;36;10;1;8;1_\x1b[74;36;10;0;8;1_" diff --git a/src/input/encode.rs b/src/input/encode.rs index 6323f1ee..a9eb19f0 100644 --- a/src/input/encode.rs +++ b/src/input/encode.rs @@ -4,6 +4,7 @@ use super::{KeyboardProtocol, MouseProtocolEncoding, TerminalKey}; const KITTY_FLAG_REPORT_EVENT_TYPES: u16 = 0b0000_0010; const KITTY_FLAG_REPORT_ALTERNATE_KEYS: u16 = 0b0000_0100; +const KITTY_FLAG_REPORT_ALL_KEYS: u16 = 0b0000_1000; /// Encode a key event for a PTY child using the pane's negotiated keyboard protocol. #[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers @@ -158,9 +159,17 @@ fn push_mouse_codepoint(bytes: &mut Vec, value: u32) -> Option<()> { fn try_encode_csi_u(key: &TerminalKey, flags: u16) -> Option> { let mods = key.modifiers; let event_suffix = kitty_event_suffix(key, flags); + let report_all_keys = flags & KITTY_FLAG_REPORT_ALL_KEYS != 0; + + if !report_all_keys + && key.modifiers.is_empty() + && matches!(key.code, KeyCode::Enter | KeyCode::Tab | KeyCode::Backspace) + { + return None; + } // Unmodified keys use legacy encoding (more compatible) - if mods.is_empty() && event_suffix.is_none() { + if mods.is_empty() && event_suffix.is_none() && !report_all_keys { return None; } @@ -180,7 +189,7 @@ fn try_encode_csi_u(key: &TerminalKey, flags: u16) -> Option> { | KeyCode::Insert | KeyCode::Delete | KeyCode::F(_) - if event_suffix.is_none() => + if event_suffix.is_none() && !report_all_keys => { return None; // let legacy handle these } @@ -707,6 +716,83 @@ mod tests { assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"a"); } + #[test] + fn kitty_report_event_types_keeps_basic_compatibility_keys_legacy() { + let cases = [ + (KeyCode::Enter, b"\r".as_slice()), + (KeyCode::Tab, b"\t".as_slice()), + (KeyCode::Backspace, b"\x7f".as_slice()), + ]; + + for (code, expected) in cases { + let press = KeyEvent::new_with_kind( + code, + KeyModifiers::empty(), + crossterm::event::KeyEventKind::Press, + ); + assert_eq!( + encode_key(press, KeyboardProtocol::Kitty { flags: 3 }), + expected, + "{code:?} press should stay legacy-compatible without REPORT_ALL_KEYS" + ); + + let repeat = KeyEvent::new_with_kind( + code, + KeyModifiers::empty(), + crossterm::event::KeyEventKind::Repeat, + ); + assert_eq!( + encode_key(repeat, KeyboardProtocol::Kitty { flags: 3 }), + expected, + "{code:?} repeat should stay legacy-compatible without REPORT_ALL_KEYS" + ); + + let release = KeyEvent::new_with_kind( + code, + KeyModifiers::empty(), + crossterm::event::KeyEventKind::Release, + ); + assert_eq!( + encode_key(release, KeyboardProtocol::Kitty { flags: 3 }), + b"", + "{code:?} release should not fall back to legacy bytes" + ); + } + } + + #[test] + fn kitty_report_all_keys_encodes_basic_compatibility_keys_with_events() { + let enter_press = KeyEvent::new_with_kind( + KeyCode::Enter, + KeyModifiers::empty(), + crossterm::event::KeyEventKind::Press, + ); + assert_eq!( + encode_key(enter_press, KeyboardProtocol::Kitty { flags: 9 }), + b"\x1b[13;1u" + ); + + let backspace_press = KeyEvent::new_with_kind( + KeyCode::Backspace, + KeyModifiers::empty(), + crossterm::event::KeyEventKind::Press, + ); + assert_eq!( + encode_key(backspace_press, KeyboardProtocol::Kitty { flags: 11 }), + b"\x1b[127;1:1u" + ); + + let backspace_release = KeyEvent::new_with_kind( + KeyCode::Backspace, + KeyModifiers::empty(), + crossterm::event::KeyEventKind::Release, + ); + assert_eq!( + encode_key(backspace_release, KeyboardProtocol::Kitty { flags: 11 }), + b"\x1b[127;1:3u" + ); + } + #[test] fn kitty_shift_tab() { let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 708a7d28..79b4a19e 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -2689,30 +2689,36 @@ mod tests { } #[test] - fn ghostty_release_still_encoded_for_report_event_pane() { + fn ghostty_report_event_pane_keeps_basic_compatibility_keys_legacy() { let (tx, _rx) = mpsc::channel(4); // Push kitty flags including REPORT_EVENT_TYPES (0b10) + DISAMBIGUATE (0b1). let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); terminal.write(b"\x1b[>3u"); let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap(); - let release = pane.encode_terminal_key( - crate::input::TerminalKey::new( - crossterm::event::KeyCode::Enter, - crossterm::event::KeyModifiers::empty(), - ) - .with_kind(crossterm::event::KeyEventKind::Release), - pane.keyboard_protocol().unwrap(), - ); - let parsed = - crate::input::parse_terminal_key_sequence(std::str::from_utf8(&release).unwrap()) - .unwrap(); - assert_eq!(parsed.code, crossterm::event::KeyCode::Enter); - assert_eq!( - parsed.kind, - crossterm::event::KeyEventKind::Release, - "report-event pane should encode a release, got {release:?}" - ); + for (code, expected) in [ + (crossterm::event::KeyCode::Enter, b"\r".as_slice()), + (crossterm::event::KeyCode::Backspace, b"\x7f".as_slice()), + ] { + let press = pane.encode_terminal_key( + crate::input::TerminalKey::new(code, crossterm::event::KeyModifiers::empty()), + pane.keyboard_protocol().unwrap(), + ); + assert_eq!( + press, expected, + "{code:?} press should stay legacy-compatible without REPORT_ALL_KEYS" + ); + + let release = pane.encode_terminal_key( + crate::input::TerminalKey::new(code, crossterm::event::KeyModifiers::empty()) + .with_kind(crossterm::event::KeyEventKind::Release), + pane.keyboard_protocol().unwrap(), + ); + assert!( + release.is_empty(), + "{code:?} release should not fall back to legacy bytes, got {release:?}" + ); + } } #[test] @@ -2938,6 +2944,47 @@ mod tests { assert_eq!(encoded, b"\x1b[127;3u"); } + #[test] + fn ghostty_pane_characterizes_ctrl_backspace_encoding() { + let (tx, _rx) = mpsc::channel(4); + let legacy = GhosttyPaneTerminal::new( + crate::ghostty::Terminal::new(80, 24, 0).unwrap(), + tx.clone(), + ) + .unwrap(); + + let ctrl_backspace = crate::input::TerminalKey::new( + crossterm::event::KeyCode::Backspace, + crossterm::event::KeyModifiers::CONTROL, + ); + assert_eq!( + legacy.encode_terminal_key(ctrl_backspace, crate::input::KeyboardProtocol::Legacy), + b"\x08" + ); + + let plain_backspace = crate::input::TerminalKey::new( + crossterm::event::KeyCode::Backspace, + crossterm::event::KeyModifiers::empty(), + ); + assert_eq!( + legacy.encode_terminal_key(plain_backspace, crate::input::KeyboardProtocol::Legacy), + b"\x7f" + ); + + let kitty = GhosttyPaneTerminal::new( + crate::ghostty::Terminal::new(80, 24, 0).unwrap(), + tx.clone(), + ) + .unwrap(); + let pane_id = PaneId::from_raw(1); + kitty.process_pty_bytes(pane_id, 0, b"\x1b[>1u", &tx); + + assert_eq!( + kitty.encode_terminal_key(ctrl_backspace, crate::input::KeyboardProtocol::Legacy), + b"\x1b[127;5u" + ); + } + #[test] fn ghostty_key_encoders_are_isolated_per_pane() { let (tx, _rx) = mpsc::channel(4);