aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-01 23:46:16 +0400
committerGitHub <noreply@github.com>2026-09-01 21:46:16 +0200
commit99c23cd1ea7468bd3661f6483c7105396503b417 (patch)
treeeb1582557a5a62f4679c706b8f53e29529d1568a /src
parent98307c509e9688575d006b47d26d0db561cda0a4 (diff)
fix(windows): preserve native pane key identity (#3503)
Diffstat (limited to 'src')
-rw-r--r--src/input/mod.rs1
-rw-r--r--src/input/model.rs5
-rw-r--r--src/platform/windows.rs16
-rw-r--r--src/protocol/wire.rs68
-rw-r--r--src/server/clients.rs15
-rw-r--r--src/server/headless/tests/mod.rs7
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!(