diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-09-01 23:46:16 +0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 21:46:16 +0200 |
| commit | 99c23cd1ea7468bd3661f6483c7105396503b417 (patch) | |
| tree | eb1582557a5a62f4679c706b8f53e29529d1568a /src | |
| parent | 98307c509e9688575d006b47d26d0db561cda0a4 (diff) | |
fix(windows): preserve native pane key identity (#3503)
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/mod.rs | 1 | ||||
| -rw-r--r-- | src/input/model.rs | 5 | ||||
| -rw-r--r-- | src/platform/windows.rs | 16 | ||||
| -rw-r--r-- | src/protocol/wire.rs | 68 | ||||
| -rw-r--r-- | src/server/clients.rs | 15 | ||||
| -rw-r--r-- | src/server/headless/tests/mod.rs | 7 |
6 files changed, 85 insertions, 27 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs index 0cd6c4bd..13b8ffa3 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -23,7 +23,6 @@ pub(crate) use lease::{InputLeaseKey, InputLeaseTable, RepeatPlan}; pub use model::ime_compatible_keyboard_enhancement_flags; #[cfg(any(unix, test))] pub use model::MouseProtocolMode; -#[cfg(any(windows, test))] pub use model::WindowsKeyRecord; pub use model::{ host_modify_other_keys_mode, KeyIdentity, KeyboardProtocol, MouseProtocolEncoding, TerminalKey, diff --git a/src/input/model.rs b/src/input/model.rs index e45ef378..304c4f20 100644 --- a/src/input/model.rs +++ b/src/input/model.rs @@ -3,7 +3,6 @@ use crossterm::event::KeyboardEnhancementFlags; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::{Deserialize, Serialize}; -#[cfg(any(windows, test))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct WindowsKeyRecord { pub key_down: bool, @@ -167,12 +166,14 @@ impl TerminalKey { } } - #[cfg(any(windows, test))] pub(crate) fn windows_record(&self) -> Option<WindowsKeyRecord> { + #[cfg(any(windows, test))] match self.source { KeySource::WindowsConsole { record, .. } => Some(record), KeySource::Synthesized | KeySource::Vt { .. } => None, } + #[cfg(not(any(windows, test)))] + None } pub(crate) fn identity(&self) -> KeyIdentity { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 03b11125..2fdba9c8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2586,27 +2586,27 @@ mod tests { #[test] fn windows_conpty_native_encoder_uses_canonical_phase_and_repeat_count() { let key = crate::input::TerminalKey::new( - crossterm::event::KeyCode::Esc, - crossterm::event::KeyModifiers::empty(), + crossterm::event::KeyCode::Char('7'), + crossterm::event::KeyModifiers::CONTROL, ) .with_windows_record(crate::input::WindowsKeyRecord { key_down: true, repeat_count: 3, - virtual_key_code: 27, - virtual_scan_code: 1, - unicode: 27, - control_key_state: 0, + virtual_key_code: 0x37, + virtual_scan_code: 0x08, + unicode: 0, + control_key_state: 0x0008, }); assert_eq!( super::encode_windows_conpty_fallback(&key), - Some(b"\x1b[27;1;27;1;0;3_".to_vec()) + Some(b"\x1b[55;8;0;1;8;3_".to_vec()) ); let mut release = key.with_kind(crossterm::event::KeyEventKind::Release); release.repeat_count = 3; assert_eq!( super::encode_windows_conpty_fallback(&release), - Some(b"\x1b[27;1;27;0;0;1_".to_vec()) + Some(b"\x1b[55;8;0;0;8;1_".to_vec()) ); } diff --git a/src/protocol/wire.rs b/src/protocol/wire.rs index b9b2eef8..be084aa6 100644 --- a/src/protocol/wire.rs +++ b/src/protocol/wire.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- /// Current protocol version. Bumped when wire format changes incompatibly. -pub const PROTOCOL_VERSION: u32 = 21; +pub const PROTOCOL_VERSION: u32 = 22; /// Maximum allowed frame payload size (2 MB). Frames larger than this are /// rejected to prevent denial-of-service via oversized length prefixes. @@ -160,6 +160,7 @@ pub enum ClientPaneInputEvent { generated_text: Option<String>, tracks_release: bool, physical_key_id: Option<u32>, + windows_record: Option<crate::input::WindowsKeyRecord>, }, TextCommit(String), Mouse { @@ -305,6 +306,7 @@ impl ClientPaneInputEvent { pub(crate) fn from_terminal_key(key: crate::input::TerminalKey) -> Option<Self> { let tracks_release = key.generated_text.is_none() || key.has_physical_identity(); let physical_key_id = key.physical_key_id(); + let windows_record = key.windows_record(); Some(Self::Key { code: ClientKeyCode::from_crossterm(key.code)?, modifiers: key.modifiers.bits(), @@ -314,6 +316,7 @@ impl ClientPaneInputEvent { generated_text: key.generated_text, tracks_release, physical_key_id, + windows_record, }) } @@ -327,6 +330,7 @@ impl ClientPaneInputEvent { shifted_codepoint, generated_text, tracks_release, + windows_record, .. } => { let mut key = crate::input::TerminalKey::new( @@ -340,6 +344,12 @@ impl ClientPaneInputEvent { if let Some(shifted_codepoint) = shifted_codepoint { key = key.with_shifted_codepoint(*shifted_codepoint); } + #[cfg(any(windows, test))] + if let Some(record) = windows_record { + key = key.with_windows_record(*record); + } + #[cfg(not(any(windows, test)))] + let _ = windows_record; crate::raw_input::RawInputEvent::Key(key) } Self::TextCommit(text) => { @@ -1819,19 +1829,41 @@ mod tests { } #[test] - fn client_shell_pane_input_roundtrips_semantic_keys() { + fn client_shell_pane_input_roundtrips_semantic_and_windows_keys() { + let windows_record = crate::input::WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x37, + virtual_scan_code: 0x08, + unicode: 0, + control_key_state: 0x0008, + }; let message = ClientMessage::ClientShellPaneInput { pane_id: "w1:p2".into(), - events: vec![ClientPaneInputEvent::Key { - code: ClientKeyCode::Char('l'), - modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), - kind: ClientKeyKind::Release, - repeat_count: 1, - shifted_codepoint: Some('L' as u32), - generated_text: None, - tracks_release: true, - physical_key_id: None, - }], + events: vec![ + ClientPaneInputEvent::Key { + code: ClientKeyCode::Char('l'), + modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), + kind: ClientKeyKind::Release, + repeat_count: 1, + shifted_codepoint: Some('L' as u32), + generated_text: None, + tracks_release: true, + physical_key_id: None, + windows_record: None, + }, + ClientPaneInputEvent::Key { + code: ClientKeyCode::Char('7'), + modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), + kind: ClientKeyKind::Press, + repeat_count: 1, + shifted_codepoint: None, + generated_text: None, + tracks_release: true, + physical_key_id: Some(0x08), + windows_record: Some(windows_record), + }, + ], }; let encoded = bincode::serde::encode_to_vec(&message, bincode::config::standard()) .expect("encode targeted semantic input"); @@ -1842,11 +1874,17 @@ mod tests { let ClientMessage::ClientShellPaneInput { events, .. } = decoded else { panic!("expected targeted semantic input"); }; - let crate::raw_input::RawInputEvent::Key(key) = events[0].to_raw_input_event() else { + let crate::raw_input::RawInputEvent::Key(semantic) = events[0].to_raw_input_event() else { panic!("expected semantic key"); }; - assert_eq!(key.shifted_codepoint, Some('L' as u32)); - assert_eq!(key.kind, crossterm::event::KeyEventKind::Release); + assert_eq!(semantic.shifted_codepoint, Some('L' as u32)); + assert_eq!(semantic.kind, crossterm::event::KeyEventKind::Release); + let crate::raw_input::RawInputEvent::Key(key) = events[1].to_raw_input_event() else { + panic!("expected Windows key"); + }; + assert_eq!(key.code, crossterm::event::KeyCode::Char('7')); + assert_eq!(key.modifiers, crossterm::event::KeyModifiers::CONTROL); + assert_eq!(key.windows_record(), Some(windows_record)); } #[test] diff --git a/src/server/clients.rs b/src/server/clients.rs index d380e2c3..3860f826 100644 --- a/src/server/clients.rs +++ b/src/server/clients.rs @@ -188,6 +188,7 @@ impl ClientConnection { shifted_codepoint, tracks_release: true, physical_key_id, + windows_record, .. } => { self.shell_held_inputs.insert( @@ -203,6 +204,7 @@ impl ClientConnection { generated_text: None, tracks_release: true, physical_key_id: *physical_key_id, + windows_record: *windows_record, }, }, ); @@ -458,6 +460,7 @@ mod tests { generated_text: Some("x".into()), tracks_release: false, physical_key_id: None, + windows_record: None, }], ); @@ -467,6 +470,14 @@ mod tests { #[test] fn physical_keys_with_the_same_semantic_code_keep_distinct_release_leases() { let mut client = shell_client(); + let windows_record = crate::input::WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x6c, + virtual_scan_code: 0x1c, + unicode: 13, + control_key_state: 0, + }; let key = |kind, physical_key_id| ClientPaneInputEvent::Key { code: crate::protocol::ClientKeyCode::Enter, modifiers: 0, @@ -476,6 +487,7 @@ mod tests { generated_text: None, tracks_release: true, physical_key_id: Some(physical_key_id), + windows_record: (physical_key_id == 108).then_some(windows_record), }; client.track_shell_input( ClientShellInputTarget::Pane("w1:p1".into()), @@ -494,8 +506,9 @@ mod tests { code: crate::protocol::ClientKeyCode::Enter, kind: ClientKeyKind::Release, physical_key_id: Some(108), + windows_record: Some(record), .. - } + } if *record == windows_record )); } } diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs index 102f2fc6..f5c33f6a 100644 --- a/src/server/headless/tests/mod.rs +++ b/src/server/headless/tests/mod.rs @@ -1262,6 +1262,7 @@ async fn client_shell_input_targets_runtime_without_server_shell_classification( generated_text: None, tracks_release: true, physical_key_id: None, + windows_record: None, }, crate::protocol::ClientPaneInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('c'), @@ -1272,6 +1273,7 @@ async fn client_shell_input_targets_runtime_without_server_shell_classification( generated_text: None, tracks_release: true, physical_key_id: None, + windows_record: None, }, crate::protocol::ClientPaneInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('x'), @@ -1282,6 +1284,7 @@ async fn client_shell_input_targets_runtime_without_server_shell_classification( generated_text: None, tracks_release: true, physical_key_id: None, + windows_record: None, }, crate::protocol::ClientPaneInputEvent::Mouse { kind: crate::protocol::ClientMouseKind::Down( @@ -1386,6 +1389,7 @@ async fn client_shell_hidden_pane_rejects_presses_but_accepts_releases() { generated_text: None, tracks_release: true, physical_key_id: Some(0x2d), + windows_record: None, }; assert!( @@ -1592,6 +1596,7 @@ async fn client_shell_release_under_popup_renders_when_it_resets_scrollback() { generated_text: None, tracks_release: true, physical_key_id: Some(0x2d), + windows_record: None, }], }); @@ -3000,6 +3005,7 @@ fn client_page_key( generated_text: None, tracks_release: true, physical_key_id: None, + windows_record: None, } } @@ -3558,6 +3564,7 @@ async fn client_shell_release_cleanup_does_not_promote_and_survives_disconnect() generated_text: (kind == crate::protocol::ClientKeyKind::Press).then(|| "x".to_owned()), tracks_release: true, physical_key_id: Some(0x2d), + windows_record: None, }; assert!( |
