From e7fc85bfdb51f89488430adbfe5bbced3be79c2f Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 23 Jul 2026 03:47:34 +0300 Subject: fix: preserve kitty printable key releases refs #1746 --- src/client/input.rs | 48 +++++++++++++++++++++++++++++++++++------ src/client/input/windows_vti.rs | 31 +++++++++++++++++++++----- src/client/mod.rs | 13 +++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) (limited to 'src/client') diff --git a/src/client/input.rs b/src/client/input.rs index 1d557543..c37a15c7 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -210,7 +210,7 @@ fn windows_crossterm_reader_loop( tracing::debug!(event = ?event, "windows control key forwarded as semantic input"); } - let Some(event) = crate::protocol::ClientInputEvent::from_crossterm(event) else { + let Some(event) = windows_crossterm_input_event(event) else { continue; }; if event_tx @@ -226,6 +226,21 @@ fn windows_crossterm_reader_loop( } } +#[cfg(any(windows, test))] +fn windows_crossterm_input_event( + event: crossterm::event::Event, +) -> Option { + let event = crate::protocol::ClientInputEvent::from_crossterm(event)?; + match event { + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char(codepoint), + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + } => Some(crate::protocol::ClientInputEvent::Text { codepoint }), + event => Some(event), + } +} + #[cfg(windows)] fn windows_event_is_control_key(event: &crossterm::event::Event) -> bool { use crossterm::event::{Event, KeyModifiers}; @@ -308,11 +323,22 @@ fn windows_client_input_event_from_raw( event: crate::raw_input::RawInputEvent, ) -> Option { match event { - crate::raw_input::RawInputEvent::Key(key) => Some(crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::from_crossterm(key.code)?, - modifiers: key.modifiers.bits(), - kind: crate::protocol::ClientKeyKind::from_crossterm(key.kind), - }), + crate::raw_input::RawInputEvent::Key(key) if key.is_text_commit => { + let crossterm::event::KeyCode::Char(codepoint) = key.code else { + return None; + }; + Some(crate::protocol::ClientInputEvent::Text { codepoint }) + } + crate::raw_input::RawInputEvent::Key(key) => { + let code = crate::protocol::ClientKeyCode::from_crossterm(key.code)?; + let modifiers = key.modifiers.bits(); + let kind = crate::protocol::ClientKeyKind::from_crossterm(key.kind); + Some(crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + }) + } crate::raw_input::RawInputEvent::Mouse(mouse) => { Some(crate::protocol::ClientInputEvent::Mouse { kind: crate::protocol::ClientMouseKind::from_crossterm(mouse.kind)?, @@ -473,6 +499,16 @@ mod windows_tests { ); } + #[test] + fn windows_crossterm_printable_press_is_text() { + let event = Event::Key(KeyEvent::new(KeyCode::Char('你'), KeyModifiers::empty())); + + assert_eq!( + windows_crossterm_input_event(event), + Some(crate::protocol::ClientInputEvent::Text { codepoint: '你' }) + ); + } + #[test] fn windows_ctrl_bracket_starts_raw_escape_sequence() { let ctrl_bracket = Event::Key(KeyEvent::new(KeyCode::Char('['), KeyModifiers::CONTROL)); diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 5742b825..8966112e 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -622,6 +622,12 @@ impl WindowsInputMapper { kind: crate::protocol::ClientKeyKind, ) -> Option { let modifiers = windows_key_modifiers(key.control_key_state); + if key.virtual_key_code == 0 { + let codepoint = self.utf16_unit_to_char(key.unicode)?; + if !codepoint.is_control() { + return Some(crate::protocol::ClientInputEvent::Text { codepoint }); + } + } if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) && key.unicode == 0x000a && (key.virtual_key_code == 0x4a || key.virtual_scan_code == 0x24) @@ -1219,6 +1225,25 @@ mod tests { ); } + #[test] + fn vti_win32_input_mode_marks_ime_commit_as_text() { + for control_key_state in [0, 0x0010, 0x0008] { + let records = win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0, + virtual_scan_code: 0, + unicode: '你' as u16, + control_key_state, + }); + + assert_eq!( + translate(records), + vec![crate::protocol::ClientInputEvent::Text { codepoint: '你' }] + ); + } + } + #[test] fn vti_win32_input_mode_decoded_paste_flag_clears_after_raw_completion() { let mut records = win32_input_mode_encoded_key_bytes(b"\x1b[200~"); @@ -1694,11 +1719,7 @@ mod tests { assert_eq!( translate(records), vec![ - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Char('a'), - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Press, - }, + crate::protocol::ClientInputEvent::Text { codepoint: 'a' }, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('a'), modifiers: 0, diff --git a/src/client/mod.rs b/src/client/mod.rs index 011de937..a22fd227 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -69,6 +69,8 @@ struct ClientState { blit_encoder: render_ansi::BlitEncoder, /// Whether host mouse capture is currently active. mouse_capture_active: bool, + /// Whether the host terminal currently reports all keys as Kitty sequences. + keyboard_report_all_active: bool, /// The terminal size we reported to the server in our last Hello/Resize. reported_size: (u16, u16), /// Client-local sound playback config, refreshed on server request. @@ -1290,6 +1292,7 @@ async fn run_client_loop( let mut state = ClientState { blit_encoder: render_ansi::BlitEncoder::new(), mouse_capture_active: config.mouse_capture_active, + keyboard_report_all_active: false, reported_size: (cols, rows), sound_config: config.sound_config, kitty_graphics_enabled: config.kitty_graphics_enabled, @@ -1585,6 +1588,16 @@ async fn run_client_loop( host_mouse_capture_active.store(desired, Ordering::Release); } } + ServerMessage::KittyKeyboardReportAll { enabled } => { + if enabled != state.keyboard_report_all_active { + crate::terminal_modes::set_host_kitty_keyboard_report_all( + &mut io::stdout(), + enabled, + ) + .map_err(ClientError::ConnectionFailed)?; + state.keyboard_report_all_active = enabled; + } + } ServerMessage::PrefixInputSource { active } => { if active { prefix_input_source.switch_to_ascii(); -- cgit v1.2.3