diff --git a/docs/next/api/herdr-api.schema.json b/docs/next/api/herdr-api.schema.json index 59a0c6a331..b4704cd15c 100644 --- a/docs/next/api/herdr-api.schema.json +++ b/docs/next/api/herdr-api.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "protocol": 18, + "protocol": 19, "schema_version": 1, "schemas": { "error_response": { diff --git a/src/app/input/clipboard.rs b/src/app/input/clipboard.rs index db3fa9f2b6..ea3640c3f0 100644 --- a/src/app/input/clipboard.rs +++ b/src/app/input/clipboard.rs @@ -46,7 +46,8 @@ impl App { return false; } - self.suppressed_repeat_keys.insert((source_id, key.code)); + self.suppressed_repeat_keys + .insert(super::super::pressed_key_identity(source_id, &key)); true } } @@ -132,7 +133,15 @@ mod tests { assert_visible_selection(&app); assert!(app.event_rx.try_recv().is_err()); - let ctrl_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL); + let ctrl_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL) + .with_windows_record(crate::input::WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x43, + virtual_scan_code: 0x2e, + unicode: 'c' as u16, + control_key_state: 0x0008, + }); let source_id = 41; app.route_client_events_from( source_id, @@ -156,11 +165,13 @@ mod tests { app.route_client_events_from( source_id, - vec![crate::raw_input::RawInputEvent::Key( - ctrl_c.with_kind(KeyEventKind::Repeat), - )], + vec![ + crate::raw_input::RawInputEvent::Key(ctrl_c), + crate::raw_input::RawInputEvent::Key(ctrl_c.with_kind(KeyEventKind::Repeat)), + ], false, ); + assert_eq!(app.suppressed_repeat_keys.len(), 1); assert!(app.event_rx.try_recv().is_err()); assert!(input_rx.try_recv().is_err()); @@ -171,14 +182,20 @@ mod tests { )], false, ); + assert!(app.suppressed_repeat_keys.is_empty()); app.route_client_events_from( source_id, vec![crate::raw_input::RawInputEvent::Key(ctrl_c)], false, ); + let expected = if cfg!(windows) { + b"\x1b[67;46;99;1;8;1_".as_slice() + } else { + b"\x03".as_slice() + }; assert_eq!( input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(), - b"\x03" + expected ); assert!(app.event_rx.try_recv().is_err()); } diff --git a/src/app/mod.rs b/src/app/mod.rs index 1733e23465..feb30ab2b5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -216,7 +216,18 @@ fn pressed_key_identity( source_id: InputSourceId, key: &crate::input::TerminalKey, ) -> (InputSourceId, crossterm::event::KeyCode) { - (source_id, key.code) + const ENHANCED_KEY: u32 = 0x0100; + let native_code = key.windows_record.and_then(|record| { + (record.virtual_scan_code != 0) + .then(|| { + 0xf0000 + + u32::from(record.virtual_scan_code) + + (u32::from(record.control_key_state & ENHANCED_KEY != 0) << 16) + }) + .and_then(char::from_u32) + .map(crossterm::event::KeyCode::Char) + }); + (source_id, native_code.unwrap_or(key.code)) } fn auto_updates_enabled(no_session: bool) -> bool { @@ -1616,6 +1627,15 @@ impl App { match event { crate::raw_input::RawInputEvent::Key(key) => { let pressed_key_id = pressed_key_identity(source_id, &key); + let key = if key.kind == crossterm::event::KeyEventKind::Press + && key.windows_record.is_some() + && (self.pressed_terminal_keys.contains_key(&pressed_key_id) + || self.suppressed_repeat_keys.contains(&pressed_key_id)) + { + key.with_kind(crossterm::event::KeyEventKind::Repeat) + } else { + key + }; match key.kind { crossterm::event::KeyEventKind::Press => { if self.state.popup_pane.is_some() || self.state.mode == Mode::Terminal @@ -1835,6 +1855,88 @@ mod tests { ) } + #[cfg(windows)] + #[tokio::test] + async fn native_repeats_and_releases_follow_the_pressed_pane() { + let record = crate::input::WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 27, + virtual_scan_code: 1, + unicode: 27, + control_key_state: 0, + }; + let key = crate::input::TerminalKey::new(KeyCode::Esc, KeyModifiers::empty()) + .with_windows_record(record); + let mut enhanced = key; + enhanced.windows_record.as_mut().unwrap().control_key_state = 0x0100; + assert_ne!( + pressed_key_identity(7, &key), + pressed_key_identity(7, &enhanced) + ); + + let mut app = test_app(); + let mut workspace = Workspace::test_new("test"); + let pressed_pane = workspace.focused_pane_id().unwrap(); + let other_pane = workspace.test_split(ratatui::layout::Direction::Horizontal); + workspace.tabs[0].layout.focus_pane(pressed_pane); + let (pressed_runtime, mut pressed_rx) = + TerminalRuntime::test_with_channel_capacity(80, 24, 6); + let (other_runtime, mut other_rx) = TerminalRuntime::test_with_channel_capacity(80, 24, 6); + workspace.tabs[0] + .runtimes + .insert(pressed_pane, pressed_runtime); + workspace.tabs[0].runtimes.insert(other_pane, other_runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + let release = crate::input::TerminalKey::new(KeyCode::Esc, KeyModifiers::empty()) + .with_windows_record(crate::input::WindowsKeyRecord { + key_down: false, + repeat_count: 1, + unicode: 0, + ..record + }) + .with_kind(KeyEventKind::Release); + app.route_client_events(vec![crate::raw_input::RawInputEvent::Key(key)], false); + assert!(app.state.focus_pane_in_workspace(0, other_pane)); + app.route_client_events( + vec![ + crate::raw_input::RawInputEvent::Key(key), + crate::raw_input::RawInputEvent::Key(key.with_kind(KeyEventKind::Repeat)), + crate::raw_input::RawInputEvent::Key(release), + crate::raw_input::RawInputEvent::Key(key), + crate::raw_input::RawInputEvent::OuterFocusLost, + ], + false, + ); + + for _ in 0..3 { + assert_eq!( + pressed_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[27;1;27;1;0;1_") + ); + } + assert_eq!( + pressed_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[27;1;0;0;0;1_") + ); + assert!(pressed_rx.try_recv().is_err()); + + assert_eq!( + other_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[27;1;27;1;0;1_") + ); + assert_eq!( + other_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"\x1b[27;1;27;0;0;1_") + ); + assert!(other_rx.try_recv().is_err()); + assert!(app.pressed_terminal_keys.is_empty()); + } + fn release_notes_state() -> state::ReleaseNotesState { state::ReleaseNotesState { version: "0.1.0".into(), diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 8966112e0d..88a7efccfe 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -9,6 +9,7 @@ use tokio::sync::mpsc; use super::windows_client_input_event_from_raw; #[cfg(windows)] use super::ClientLoopEvent; +use crate::input::WindowsKeyRecord; #[cfg(windows)] pub(super) fn raw_console_reader_loop( @@ -164,16 +165,6 @@ enum WindowsInputRecord { Focus(bool), } -#[derive(Clone, Copy, Debug)] -struct WindowsKeyRecord { - key_down: bool, - repeat_count: u16, - virtual_key_code: u16, - virtual_scan_code: u16, - unicode: u16, - control_key_state: u32, -} - #[derive(Clone, Copy, Debug)] struct WindowsMouseRecord { x: u16, @@ -502,6 +493,10 @@ impl WindowsInputMapper { return Some(vec![0x1b]); } + if record.virtual_scan_code != 0 { + return None; + } + if !record.key_down || record.repeat_count.max(1) != 1 || windows_key_modifiers(record.control_key_state).bits() != 0 @@ -574,6 +569,12 @@ impl WindowsInputMapper { } let is_alt_code = Self::is_alt_code(key); + let carries_native_record = key.repeat_count != 0 + && key.virtual_key_code != 0 + && key.virtual_key_code != 0x03 + && !matches!(key.virtual_key_code, 0xe5 | 0xe7) + && !is_alt_code + && !(0xd800..=0xdfff).contains(&key.unicode); (0..key.repeat_count.max(1)) .filter_map(|repeat_idx| { self.translate_semantic_key_event( @@ -581,6 +582,28 @@ impl WindowsInputMapper { Self::semantic_key_kind(key, is_alt_code, repeat_idx), ) }) + .map(|event| { + if carries_native_record { + match event { + crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + } => crate::protocol::ClientInputEvent::WindowsKey { + code, + modifiers, + kind, + record: WindowsKeyRecord { + repeat_count: 1, + ..key + }, + }, + event => event, + } + } else { + event + } + }) .collect() } @@ -1063,7 +1086,7 @@ mod tests { key_down: true, repeat_count, virtual_key_code: vk, - virtual_scan_code: 0, + virtual_scan_code: 1, unicode: 0, control_key_state: 0, }) @@ -1114,6 +1137,33 @@ mod tests { fn translate( records: impl IntoIterator, + ) -> Vec { + semantic_only(translate_with_provenance(records)) + } + + fn semantic_only( + events: impl IntoIterator, + ) -> Vec { + events + .into_iter() + .map(|event| match event { + crate::protocol::ClientInputEvent::WindowsKey { + code, + modifiers, + kind, + .. + } => crate::protocol::ClientInputEvent::Key { + code, + modifiers, + kind, + }, + event => event, + }) + .collect() + } + + fn translate_with_provenance( + records: impl IntoIterator, ) -> Vec { let mut translator = WindowsInputTranslator::default(); records @@ -1384,41 +1434,74 @@ mod tests { #[test] fn vti_physical_escape_key_record_is_immediately_semantic() { - let mut translator = WindowsInputTranslator::default(); + let record = WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x1b, + virtual_scan_code: 0x01, + unicode: 0x1b, + control_key_state: 0, + }; assert_eq!( - translator.translate(key_vk_with_scan_unicode(0x1b, 0x01, '\x1b', 0)), - vec![crate::protocol::ClientInputEvent::Key { + translate_with_provenance([WindowsInputRecord::Key(record)]), + vec![crate::protocol::ClientInputEvent::WindowsKey { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + record, }] ); } #[test] - fn vti_repeated_escape_record_stays_semantic() { + fn vti_grouped_escape_down_and_up_keep_native_ownership_record() { + use crate::protocol::ClientKeyKind::{Press, Release, Repeat}; + + let events = translate_with_provenance([ + key_vk_with_repeat(0x1b, 3), + key_vk_up_with_scan_unicode(0x1b, 1, '\0', 0), + ]); + assert_eq!( - translate([key_vk_with_repeat(0x1b, 3)]), - vec![ - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Esc, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Press, - }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Esc, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Repeat, - }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Esc, - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Repeat, - }, + events + .iter() + .map(|event| match event { + crate::protocol::ClientInputEvent::WindowsKey { + code: crate::protocol::ClientKeyCode::Esc, + kind, + record, + .. + } => (*kind, record.key_down, record.repeat_count), + event => panic!("unexpected grouped event: {event:?}"), + }) + .collect::>(), + [ + (Press, true, 1), + (Repeat, true, 1), + (Repeat, true, 1), + (Release, false, 1) ] ); } + #[test] + fn vti_ctrl_break_record_keeps_semantic_path() { + assert!(matches!( + translate_with_provenance([key_vk(0x03, 0x0008)]).as_slice(), + [crate::protocol::ClientInputEvent::Key { .. }] + )); + } + + #[test] + fn vti_ime_virtual_keys_keep_semantic_path() { + for vk in [0xe5, 0xe7] { + assert!(matches!( + translate_with_provenance([key_vk_with_unicode(vk, 'é', 0)]).as_slice(), + [crate::protocol::ClientInputEvent::Key { .. }] + )); + } + } + #[test] fn vti_modified_escape_remains_semantic() { assert_eq!( @@ -1711,22 +1794,74 @@ mod tests { } #[test] - fn vti_win32_input_mode_printable_key_becomes_char() { - let records = "\x1b[65;30;97;1;0;1_\x1b[65;30;97;0;0;1_" - .chars() - .map(key_char); - + fn vti_win32_input_mode_printable_keys_preserve_physical_records() { assert_eq!( - translate(records), - vec![ - crate::protocol::ClientInputEvent::Text { codepoint: 'a' }, - crate::protocol::ClientInputEvent::Key { - code: crate::protocol::ClientKeyCode::Char('a'), - modifiers: 0, - kind: crate::protocol::ClientKeyKind::Release, - }, - ] + translate(win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0, + virtual_scan_code: 0, + unicode: b'a'.into(), + control_key_state: 0, + })), + vec![crate::protocol::ClientInputEvent::Text { codepoint: 'a' }] ); + + use crate::protocol::ClientKeyKind::{Press, Release, Repeat}; + for (repeat_count, expected) in [ + (1, vec![(Press, true), (Release, false)]), + ( + 3, + vec![ + (Press, true), + (Repeat, true), + (Repeat, true), + (Release, false), + ], + ), + ] { + let mut records = win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count, + virtual_key_code: 0x41, + virtual_scan_code: 30, + unicode: b'a'.into(), + control_key_state: 0, + }); + records.extend(win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: false, + repeat_count: 1, + virtual_key_code: 0x41, + virtual_scan_code: 30, + unicode: b'a'.into(), + control_key_state: 0, + })); + + assert_eq!( + translate_with_provenance(records) + .iter() + .map(|event| match event { + crate::protocol::ClientInputEvent::WindowsKey { + code: crate::protocol::ClientKeyCode::Char('a'), + modifiers: 0, + kind, + record: + WindowsKeyRecord { + repeat_count: 1, + virtual_key_code: 0x41, + virtual_scan_code: 30, + unicode, + control_key_state: 0, + key_down, + }, + } if *unicode == u16::from(b'a') => (*kind, *key_down), + event => panic!("unexpected printable key event: {event:?}"), + }) + .collect::>(), + expected, + "repeat_count={repeat_count}" + ); + } } #[test] @@ -1813,24 +1948,33 @@ mod tests { let mut translator = WindowsInputTranslator::default(); assert_eq!( translator.translate(key_vk_with_scan_unicode(0x1b, 0x02, '\0', 0)), - vec![crate::protocol::ClientInputEvent::Key { + vec![crate::protocol::ClientInputEvent::WindowsKey { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + record: WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x1b, + virtual_scan_code: 0x02, + unicode: 0, + control_key_state: 0, + }, }] ); } #[test] fn vti_win32_input_mode_physical_escape_is_immediately_semantic() { - let records = win32_input_mode_encoded_record(WindowsKeyRecord { + let record = WindowsKeyRecord { key_down: true, repeat_count: 1, virtual_key_code: 0x1b, virtual_scan_code: 0x01, unicode: 0x1b, control_key_state: 0, - }); + }; + let records = win32_input_mode_encoded_record(record); let mut translator = WindowsInputTranslator::default(); assert_eq!( @@ -1838,10 +1982,11 @@ mod tests { .into_iter() .flat_map(|record| translator.translate(record)) .collect::>(), - vec![crate::protocol::ClientInputEvent::Key { + vec![crate::protocol::ClientInputEvent::WindowsKey { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, + record, }] ); } @@ -1912,7 +2057,7 @@ mod tests { let mut translator = WindowsInputTranslator::default(); assert!(translator.translate(key_char('\x1b')).is_empty()); assert_eq!( - translator.translate(key_vk(0x26, 0)), + semantic_only(translator.translate(key_vk(0x26, 0))), vec![ crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Esc, diff --git a/src/input/mod.rs b/src/input/mod.rs index 3882b03507..7dd0b115a2 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -10,6 +10,6 @@ pub use encode::{ pub use model::ime_compatible_keyboard_enhancement_flags; pub use model::{ host_modify_other_keys_mode, KeyboardProtocol, MouseProtocolEncoding, MouseProtocolMode, - TerminalKey, + TerminalKey, WindowsKeyRecord, }; pub use parse::parse_terminal_key_sequence; diff --git a/src/input/model.rs b/src/input/model.rs index f6072bc680..edbe6a8dd6 100644 --- a/src/input/model.rs +++ b/src/input/model.rs @@ -3,6 +3,16 @@ use crossterm::event::KeyboardEnhancementFlags; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct WindowsKeyRecord { + pub key_down: bool, + pub repeat_count: u16, + pub virtual_key_code: u16, + pub virtual_scan_code: u16, + pub unicode: u16, + pub control_key_state: u32, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TerminalKey { pub code: KeyCode, @@ -10,6 +20,7 @@ pub struct TerminalKey { pub kind: crossterm::event::KeyEventKind, pub shifted_codepoint: Option, pub is_text_commit: bool, + pub windows_record: Option, } impl TerminalKey { @@ -20,10 +31,14 @@ impl TerminalKey { kind: crossterm::event::KeyEventKind::Press, shifted_codepoint: None, is_text_commit: false, + windows_record: None, } } pub fn with_kind(mut self, kind: crossterm::event::KeyEventKind) -> Self { + if let Some(record) = self.windows_record.as_mut() { + record.key_down = kind != crossterm::event::KeyEventKind::Release; + } self.kind = kind; self } @@ -34,6 +49,11 @@ impl TerminalKey { self } + pub fn with_windows_record(mut self, record: WindowsKeyRecord) -> Self { + self.windows_record = Some(record); + self + } + pub fn as_text_commit(mut self) -> Self { let has_text_only_modifiers = match self.code { KeyCode::Char(ch) if ch.is_ascii_uppercase() => { diff --git a/src/input/parse.rs b/src/input/parse.rs index e4c554f83f..e840211d98 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -46,6 +46,7 @@ fn parse_kitty_key_sequence(data: &str) -> Option { kind, shifted_codepoint, is_text_commit: false, + windows_record: None, }) } diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 76ab20d398..3b765cca7f 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1625,7 +1625,7 @@ impl GhosttyPaneTerminal { protocol: crate::input::KeyboardProtocol, ) -> Vec { #[cfg(windows)] - if let Some(bytes) = crate::platform::encode_windows_conpty_shift_enter(key) { + if let Some(bytes) = crate::platform::encode_windows_conpty_fallback(key) { if self.core.lock().is_ok_and(|core| { core.terminal .kitty_keyboard_flags() diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 35ecfd827f..70272cc1a9 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -64,15 +64,39 @@ 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> { +/// Native Win32 input, with targeted fallbacks for semantic-only input. +pub(crate) fn encode_windows_conpty_fallback(key: crate::input::TerminalKey) -> Option> { use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; - if key.code != KeyCode::Enter || key.modifiers != KeyModifiers::SHIFT { - return None; - } + let record = match key.windows_record { + Some(record) if record.repeat_count == 1 => record, + Some(_) | None => { + if key.code != KeyCode::Enter || key.modifiers != KeyModifiers::SHIFT { + return None; + } + crate::input::WindowsKeyRecord { + key_down: !matches!(key.kind, KeyEventKind::Release), + repeat_count: 1, + virtual_key_code: 13, + virtual_scan_code: 28, + unicode: 13, + control_key_state: 16, + } + } + }; - let key_down = !matches!(key.kind, KeyEventKind::Release); - Some(format!("\x1b[13;28;13;{};16;1_", u8::from(key_down)).into_bytes()) + Some( + format!( + "\x1b[{};{};{};{};{};{}_", + record.virtual_key_code, + record.virtual_scan_code, + record.unicode, + u8::from(record.key_down), + record.control_key_state, + record.repeat_count, + ) + .into_bytes(), + ) } #[derive(Debug)] diff --git a/src/protocol/wire.rs b/src/protocol/wire.rs index 0d4b14bba7..0a96a4a157 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 = 18; +pub const PROTOCOL_VERSION: u32 = 19; /// Maximum allowed frame payload size (2 MB). Frames larger than this are /// rejected to prevent denial-of-service via oversized length prefixes. @@ -130,6 +130,12 @@ pub enum ClientInputEvent { }, FocusGained, FocusLost, + WindowsKey { + code: ClientKeyCode, + modifiers: u8, + kind: ClientKeyKind, + record: crate::input::WindowsKeyRecord, + }, } impl ClientKeyKind { @@ -309,6 +315,19 @@ impl ClientInputEvent { Self::Paste { text } => crate::raw_input::RawInputEvent::Paste(text.clone()), Self::FocusGained => crate::raw_input::RawInputEvent::OuterFocusGained, Self::FocusLost => crate::raw_input::RawInputEvent::OuterFocusLost, + Self::WindowsKey { + code, + modifiers, + kind, + record, + } => crate::raw_input::RawInputEvent::Key( + crate::input::TerminalKey::new( + code.to_crossterm(), + crossterm::event::KeyModifiers::from_bits_truncate(*modifiers), + ) + .with_windows_record(*record) + .with_kind(kind.to_crossterm()), + ), } } } @@ -1083,6 +1102,19 @@ mod tests { modifiers: 0, kind: ClientKeyKind::Press, }, + ClientInputEvent::WindowsKey { + code: ClientKeyCode::Esc, + modifiers: 0, + kind: ClientKeyKind::Release, + record: crate::input::WindowsKeyRecord { + key_down: false, + repeat_count: 1, + virtual_key_code: 27, + virtual_scan_code: 1, + unicode: 27, + control_key_state: 0, + }, + }, ClientInputEvent::Mouse { kind: ClientMouseKind::Down(ClientMouseButton::Left), column: 3, @@ -1099,10 +1131,19 @@ mod tests { #[test] fn client_input_events_convert_to_raw_keys() { - let shifted = ClientInputEvent::Key { + let record = crate::input::WindowsKeyRecord { + key_down: false, + repeat_count: 1, + virtual_key_code: 78, + virtual_scan_code: 49, + unicode: 78, + control_key_state: 16, + }; + let shifted = ClientInputEvent::WindowsKey { code: ClientKeyCode::Char('N'), modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), kind: ClientKeyKind::Press, + record, } .to_raw_input_event(); match shifted { @@ -1110,6 +1151,7 @@ mod tests { assert_eq!(key.code, crossterm::event::KeyCode::Char('N')); assert_eq!(key.modifiers, crossterm::event::KeyModifiers::SHIFT); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); + assert_eq!(key.windows_record.map(|record| record.key_down), Some(true)); } other => panic!("expected shifted key event, got {other:?}"), } diff --git a/tests/api_ping.rs b/tests/api_ping.rs index 344fe6ecd0..1033628a92 100644 --- a/tests/api_ping.rs +++ b/tests/api_ping.rs @@ -304,7 +304,7 @@ fn ping_over_socket_returns_version() { assert_eq!(value["result"]["version"], env!("CARGO_PKG_VERSION")); // Intentionally hardcoded so wire protocol bumps require updating this test. // Changing this value means old clients/servers are no longer compatible. - assert_eq!(value["result"]["protocol"], 18); + assert_eq!(value["result"]["protocol"], 19); cleanup_spawned_herdr(child, base); } diff --git a/tests/cli/sessions.rs b/tests/cli/sessions.rs index b7655542ec..8cb0f4b799 100644 --- a/tests/cli/sessions.rs +++ b/tests/cli/sessions.rs @@ -326,7 +326,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {full_stdout}" ); assert!( - full_stdout.contains(" protocol: 18"), + full_stdout.contains(" protocol: 19"), "stdout: {full_stdout}" ); assert!(full_stdout.contains("server:\n"), "stdout: {full_stdout}"); @@ -359,7 +359,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {server_stdout}" ); assert!( - server_stdout.contains("protocol: 18"), + server_stdout.contains("protocol: 19"), "stdout: {server_stdout}" ); @@ -371,7 +371,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {client_stdout}" ); assert!( - client_stdout.contains("protocol: 18"), + client_stdout.contains("protocol: 19"), "stdout: {client_stdout}" ); assert!( @@ -381,7 +381,7 @@ fn status_commands_report_client_and_server_versions() { let full_json = run_cli_json(&socket_path, &["status", "--json"]); assert_eq!(full_json["client"]["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(full_json["client"]["protocol"], 18); + assert_eq!(full_json["client"]["protocol"], 19); assert_eq!(full_json["server"]["status"], "running"); assert_eq!(full_json["server"]["running"], true); assert_eq!(full_json["server"]["compatible"], true); @@ -395,12 +395,12 @@ fn status_commands_report_client_and_server_versions() { let server_json = run_cli_json(&socket_path, &["status", "server", "--json"]); assert_eq!(server_json["status"], "running"); assert_eq!(server_json["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(server_json["protocol"], 18); + assert_eq!(server_json["protocol"], 19); assert_eq!(server_json["compatible"], true); let client_json = run_cli_json(&socket_path, &["status", "client", "--json"]); assert_eq!(client_json["version"], env!("CARGO_PKG_VERSION")); - assert_eq!(client_json["protocol"], 18); + assert_eq!(client_json["protocol"], 19); assert!(client_json["binary"] .as_str() .is_some_and(|path| !path.is_empty())); diff --git a/tests/support/mod.rs b/tests/support/mod.rs index c964512991..b10e2a93ae 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -15,7 +15,7 @@ static INIT: Once = Once::new(); static CLEANUP_GUARD: OnceLock = OnceLock::new(); const WATCHDOG_SCAN_INTERVAL: Duration = Duration::from_secs(1); const RUNTIME_OWNER_MARKER: &str = ".herdr-test-owner-pid"; -pub const CURRENT_PROTOCOL: u32 = 18; +pub const CURRENT_PROTOCOL: u32 = 19; pub fn register_spawned_herdr_pid(pid: Option) { let Some(pid) = pid else {