diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-25 16:32:00 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-25 16:32:00 +0300 |
| commit | c792f2fd53794a02b12c369903fd463c4896b7ef (patch) | |
| tree | a80080d9f42c00aab8d4814e49f5d8c196875699 /src | |
| parent | 58b9fecc21a71d253b71cf104b1df0da8e81aeba (diff) | |
fix: preserve alacritty input on windows
refs #792
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/input.rs | 43 | ||||
| -rw-r--r-- | src/client/input/windows_vti.rs | 316 | ||||
| -rw-r--r-- | src/client/mod.rs | 8 | ||||
| -rw-r--r-- | src/input/model.rs | 43 | ||||
| -rw-r--r-- | src/main.rs | 6 |
5 files changed, 389 insertions, 27 deletions
diff --git a/src/client/input.rs b/src/client/input.rs index 7cf69533..7099311b 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -212,7 +212,7 @@ fn windows_event_is_control_key(event: &crossterm::event::Event) -> bool { ) } -#[cfg(windows)] +#[cfg(any(windows, test))] fn windows_key_raw_bytes( event: &crossterm::event::Event, raw_sequence_pending: bool, @@ -228,6 +228,9 @@ fn windows_key_raw_bytes( match key.code { KeyCode::Esc if key.modifiers.is_empty() => Some(vec![0x1b]), + KeyCode::Char('[') if !raw_sequence_pending && key.modifiers == KeyModifiers::CONTROL => { + Some(vec![0x1b]) + } KeyCode::Char(ch) if !raw_sequence_pending && matches!(ch, 'i' | 'I') @@ -365,7 +368,7 @@ mod tests { } } -#[cfg(all(test, windows))] +#[cfg(test)] mod windows_tests { use super::*; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; @@ -396,6 +399,42 @@ mod windows_tests { } #[test] + fn windows_ctrl_bracket_starts_raw_escape_sequence() { + let ctrl_bracket = Event::Key(KeyEvent::new(KeyCode::Char('['), KeyModifiers::CONTROL)); + assert_eq!( + windows_key_raw_bytes(&ctrl_bracket, false).as_deref(), + Some(b"\x1b".as_slice()) + ); + + let mut framer = crate::raw_input::RawInputFramer::default(); + assert!(framer.push(b"\x1b").is_empty()); + let events = framer.push(b"[<35;48;26M"); + assert_eq!(events.len(), 1); + + let event = windows_client_input_event_from_raw(events.into_iter().next().unwrap()) + .expect("raw mouse converts"); + assert!(matches!( + event, + crate::protocol::ClientInputEvent::Mouse { + kind: crate::protocol::ClientMouseKind::Moved, + column: 47, + row: 25, + modifiers: _, + } + )); + } + + #[test] + fn windows_ctrl_shift_bracket_stays_semantic() { + let ctrl_shift_bracket = Event::Key(KeyEvent::new( + KeyCode::Char('['), + KeyModifiers::CONTROL | KeyModifiers::SHIFT, + )); + assert_eq!(windows_key_raw_bytes(&ctrl_shift_bracket, false), None); + } + + #[cfg(windows)] + #[test] fn windows_ctrl_d_semantic_event_encodes_to_eot() { let event = Event::Key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL)); assert_eq!(windows_key_raw_bytes(&event, false), None); diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 376efdb5..9f6d3788 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -134,6 +134,7 @@ fn windows_console_input_record_from_os( key_down: key.bKeyDown != 0, repeat_count: key.wRepeatCount, virtual_key_code: key.wVirtualKeyCode, + virtual_scan_code: key.wVirtualScanCode, unicode, control_key_state: key.dwControlKeyState, })) @@ -168,6 +169,7 @@ struct WindowsKeyRecord { key_down: bool, repeat_count: u16, virtual_key_code: u16, + virtual_scan_code: u16, unicode: u16, control_key_state: u32, } @@ -198,6 +200,10 @@ struct WindowsInputPump { enum PlatformInputItem { Bytes(Vec<u8>), Semantic(crate::protocol::ClientInputEvent), + PasteAwareBytes { + paste_bytes: Vec<u8>, + raw_bytes: Vec<u8>, + }, PasteAwareKey { bytes: Vec<u8>, events: Vec<crate::protocol::ClientInputEvent>, @@ -228,6 +234,16 @@ impl WindowsInputPump { events.push(event); events } + PlatformInputItem::PasteAwareBytes { + paste_bytes, + raw_bytes, + } => { + if self.framer.has_pending_bracketed_paste() { + Self::raw_events_to_client_events(self.framer.push(&paste_bytes)) + } else { + Self::raw_events_to_client_events(self.framer.push(&raw_bytes)) + } + } PlatformInputItem::PasteAwareKey { bytes, events } => { if self.framer.has_pending_bracketed_paste() { Self::raw_events_to_client_events(self.framer.push(&bytes)) @@ -334,6 +350,10 @@ impl WindowsInputMapper { return Vec::new(); } + if self.key_record_is_raw_escape(key) { + return self.translate_win32_input_mode_bytes(&[0x1b]); + } + let repeat_count = key.repeat_count.max(1); if key.virtual_key_code == 0 { let mut items = Vec::new(); @@ -369,6 +389,24 @@ impl WindowsInputMapper { self.with_pending_win32_flush(items) } + fn key_record_is_raw_escape(&self, key: WindowsKeyRecord) -> bool { + let modifiers = windows_key_modifiers(key.control_key_state); + if !key.key_down + || key.repeat_count.max(1) != 1 + || modifiers.contains(crossterm::event::KeyModifiers::ALT) + { + return false; + } + + let bare_escape = modifiers.is_empty() + && (key.virtual_key_code == 0x1b || (key.virtual_key_code == 0 && key.unicode == 0x1b)); + let ctrl_bracket = key.virtual_key_code == 0xdb + && key.unicode == 0x1b + && modifiers == crossterm::event::KeyModifiers::CONTROL; + + bare_escape || ctrl_bracket + } + fn translate_win32_input_mode_bytes(&mut self, bytes: &[u8]) -> Vec<PlatformInputItem> { let mut items = Vec::new(); for item in self.win32_input.push(bytes) { @@ -377,16 +415,42 @@ impl WindowsInputMapper { items.push(PlatformInputItem::Bytes(bytes)) } WindowsWin32InputModeItem::Key { bytes, record } => { - items.push(PlatformInputItem::PasteAwareKey { - bytes, - events: self.translate_semantic_key_events(record), - }) + if let Some(raw_bytes) = self.win32_input_mode_key_record_raw_bytes(record) { + items.push(PlatformInputItem::PasteAwareBytes { + paste_bytes: bytes, + raw_bytes, + }); + } else { + items.push(PlatformInputItem::PasteAwareKey { + bytes, + events: self.translate_semantic_key_events(record), + }) + } } } } items } + fn win32_input_mode_key_record_raw_bytes( + &mut self, + record: WindowsKeyRecord, + ) -> Option<Vec<u8>> { + if self.key_record_is_raw_escape(record) { + return Some(vec![0x1b]); + } + + if !record.key_down + || record.virtual_key_code != 0 + || windows_key_modifiers(record.control_key_state).bits() != 0 + { + return None; + } + + self.synthetic_utf16_unit_to_bytes(record.unicode) + .map(|bytes| bytes.repeat(record.repeat_count.max(1) as usize)) + } + fn with_pending_win32_flush( &mut self, mut items: Vec<PlatformInputItem>, @@ -483,6 +547,18 @@ impl WindowsInputMapper { kind: crate::protocol::ClientKeyKind, ) -> Option<crate::protocol::ClientInputEvent> { let modifiers = windows_key_modifiers(key.control_key_state); + if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) + && key.unicode == 0x000a + && (key.virtual_key_code == 0x4a || key.virtual_scan_code == 0x24) + { + self.pending_high_surrogate = None; + return Some(crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('j'), + modifiers: modifiers.bits(), + kind, + }); + } + let code = if let Some(code) = windows_virtual_key_to_key_code(key.virtual_key_code, modifiers) { @@ -703,7 +779,7 @@ impl WindowsWin32InputModeFramer { fn parse_win32_input_mode_key_record(body: &str) -> Option<WindowsKeyRecord> { let mut fields = body.split(';'); let virtual_key_code = fields.next()?.parse::<u16>().ok()?; - let _scan_code = fields.next()?.parse::<u16>().ok()?; + let virtual_scan_code = fields.next()?.parse::<u16>().ok()?; let unicode = fields.next()?.parse::<u16>().ok()?; let key_down = fields.next()?.parse::<u16>().ok()? != 0; let control_key_state = fields.next()?.parse::<u32>().ok()?; @@ -716,6 +792,7 @@ fn parse_win32_input_mode_key_record(body: &str) -> Option<WindowsKeyRecord> { key_down, repeat_count, virtual_key_code, + virtual_scan_code, unicode, control_key_state, }) @@ -841,6 +918,7 @@ mod tests { key_down: true, repeat_count: 1, virtual_key_code: 0, + virtual_scan_code: 0, unicode: ch as u16, control_key_state: 0, }) @@ -855,6 +933,7 @@ mod tests { key_down: true, repeat_count: 1, virtual_key_code: vk, + virtual_scan_code: 0, unicode: ch as u16, control_key_state, }) @@ -865,6 +944,7 @@ mod tests { key_down: true, repeat_count: 1, virtual_key_code: vk, + virtual_scan_code: 0, unicode: unit, control_key_state: 0, }) @@ -875,16 +955,34 @@ mod tests { key_down: true, repeat_count: 1, virtual_key_code: vk, + virtual_scan_code: 0, unicode: unit, control_key_state, }) } + fn key_vk_with_scan_unicode( + vk: u16, + scan: u16, + ch: char, + control_key_state: u32, + ) -> WindowsInputRecord { + WindowsInputRecord::Key(WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: vk, + virtual_scan_code: scan, + unicode: ch as u16, + control_key_state, + }) + } + fn key_vk_with_repeat(vk: u16, repeat_count: u16) -> WindowsInputRecord { WindowsInputRecord::Key(WindowsKeyRecord { key_down: true, repeat_count, virtual_key_code: vk, + virtual_scan_code: 0, unicode: 0, control_key_state: 0, }) @@ -900,6 +998,7 @@ mod tests { key_down: true, repeat_count, virtual_key_code: vk, + virtual_scan_code: 0, unicode: ch as u16, control_key_state, }) @@ -910,6 +1009,23 @@ mod tests { key_down: false, repeat_count: 1, virtual_key_code: vk, + virtual_scan_code: 0, + unicode: ch as u16, + control_key_state, + }) + } + + fn key_vk_up_with_scan_unicode( + vk: u16, + scan: u16, + ch: char, + control_key_state: u32, + ) -> WindowsInputRecord { + WindowsInputRecord::Key(WindowsKeyRecord { + key_down: false, + repeat_count: 1, + virtual_key_code: vk, + virtual_scan_code: scan, unicode: ch as u16, control_key_state, }) @@ -925,6 +1041,18 @@ mod tests { .collect() } + fn win32_input_mode_encoded_raw_bytes(bytes: &[u8]) -> Vec<WindowsInputRecord> { + bytes + .iter() + .flat_map(|byte| { + format!("\x1b[0;0;{byte};1;0;1_") + .chars() + .map(key_char) + .collect::<Vec<_>>() + }) + .collect() + } + #[test] fn vti_bracketed_paste_records_emit_single_paste() { let records = "\x1b[200~alpha\rbravo\rcharlie\x1b[201~" @@ -986,7 +1114,6 @@ mod tests { fn vti_real_non_letter_control_records_are_preserved() { let cases = [ (0x20, 0x00, ' '), - (0xdb, 0x1b, '['), (0xdc, 0x1c, '\\'), (0xdd, 0x1d, ']'), (0x36, 0x1e, '^'), @@ -1007,6 +1134,78 @@ mod tests { } #[test] + fn vti_ctrl_bracket_record_flushes_to_escape_after_idle() { + let mut translator = WindowsInputTranslator::default(); + assert!(translator + .translate(key_vk_with_utf16_mods(0xdb, 0x1b, 0x0008)) + .is_empty()); + assert_eq!( + translator.idle(), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + }] + ); + } + + #[test] + fn vti_escape_key_record_flushes_to_escape_after_idle() { + let mut translator = WindowsInputTranslator::default(); + assert!(translator.translate(key_vk(0x1b, 0)).is_empty()); + assert_eq!( + translator.idle(), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + }] + ); + } + + #[test] + fn vti_repeated_escape_record_stays_semantic() { + 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, + }, + ] + ); + } + + #[test] + fn vti_modified_escape_remains_semantic() { + assert_eq!( + translate([key_vk_with_utf16_mods(0x1b, 0, 0x0010)]), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + modifiers: crossterm::event::KeyModifiers::SHIFT.bits(), + kind: crate::protocol::ClientKeyKind::Press, + }] + ); + } + + #[test] + fn vti_win32_input_mode_key_release_does_not_emit_raw_text() { + let records = "\x1b[0;0;97;0;0;1_".chars().map(key_char); + assert!(translate(records).is_empty()); + } + + #[test] fn vti_enter_outside_paste_becomes_enter_key() { assert_eq!( translate([key_char('\r')]), @@ -1215,6 +1414,50 @@ mod tests { } #[test] + fn vti_alacritty_ctrl_j_return_lf_record_preserves_lf_control_key() { + assert_eq!( + translate([ + key_vk_with_scan_unicode(0x0d, 0x24, '\n', 0x0008), + key_vk_up_with_scan_unicode(0x0d, 0x24, '\n', 0x0008), + ]), + vec![ + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('j'), + modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), + kind: crate::protocol::ClientKeyKind::Press, + }, + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Char('j'), + modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), + kind: crate::protocol::ClientKeyKind::Release, + }, + ] + ); + } + + #[test] + fn vti_ctrl_enter_return_lf_record_preserves_ctrl_enter() { + assert_eq!( + translate([ + key_vk_with_scan_unicode(0x0d, 0x1c, '\n', 0x0008), + key_vk_up_with_scan_unicode(0x0d, 0x1c, '\n', 0x0008), + ]), + vec![ + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Enter, + modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), + kind: crate::protocol::ClientKeyKind::Press, + }, + crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Enter, + modifiers: crossterm::event::KeyModifiers::CONTROL.bits(), + kind: crate::protocol::ClientKeyKind::Release, + }, + ] + ); + } + + #[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() @@ -1252,6 +1495,20 @@ mod tests { } #[test] + fn vti_win32_input_mode_raw_sequence_inside_bracketed_paste_stays_payload() { + let records = "\x1b[200~alpha\x1b[0;0;97;1;0;1_bravo\x1b[201~" + .chars() + .map(key_char); + + assert_eq!( + translate(records), + vec![crate::protocol::ClientInputEvent::Paste { + text: "alpha\x1b[0;0;97;1;0;1_bravo".into(), + }] + ); + } + + #[test] fn vti_win32_input_mode_leaves_mouse_sequence_for_raw_parser() { let records = "\x1b[<0;3;4M".chars().map(key_char); @@ -1269,6 +1526,53 @@ mod tests { } #[test] + fn vti_ctrl_bracket_escape_starts_mouse_sequence() { + let records = [key_vk_with_utf16_mods(0xdb, 0x1b, 0x0008)] + .into_iter() + .chain("[<35;48;26M".chars().map(key_char)); + + assert_eq!( + translate(records), + vec![crate::protocol::ClientInputEvent::Mouse { + kind: crate::protocol::ClientMouseKind::Moved, + column: 47, + row: 25, + modifiers: 0, + }] + ); + } + + #[test] + fn vti_escape_key_record_without_unicode_starts_mouse_sequence() { + let records = [key_vk(0x1b, 0)] + .into_iter() + .chain("[<35;48;26M".chars().map(key_char)); + + assert_eq!( + translate(records), + vec![crate::protocol::ClientInputEvent::Mouse { + kind: crate::protocol::ClientMouseKind::Moved, + column: 47, + row: 25, + modifiers: 0, + }] + ); + } + + #[test] + fn vti_win32_input_mode_encoded_mouse_sequence() { + assert_eq!( + translate(win32_input_mode_encoded_raw_bytes(b"\x1b[<35;48;26M")), + vec![crate::protocol::ClientInputEvent::Mouse { + kind: crate::protocol::ClientMouseKind::Moved, + column: 47, + row: 25, + modifiers: 0, + }] + ); + } + + #[test] fn vti_escape_arrow_sequence_becomes_arrow_key() { let records = "\x1b[A".chars().map(key_char); assert_eq!( diff --git a/src/client/mod.rs b/src/client/mod.rs index 67726e68..d9d73b95 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -377,13 +377,7 @@ fn setup_terminal_with_capabilities( } let modify_other_keys_mode = enable_client_protocols - .then(|| { - crate::input::host_modify_other_keys_mode( - std::env::var("TMUX").is_ok(), - std::env::var("TERM_PROGRAM").ok().as_deref(), - std::env::var_os("WEZTERM_PANE").is_some(), - ) - }) + .then(crate::input::host_modify_other_keys_mode) .flatten(); if let Some(mode) = modify_other_keys_mode { io::stdout().write_all(mode.set_sequence())?; diff --git a/src/input/model.rs b/src/input/model.rs index 1ce107d4..a3e74056 100644 --- a/src/input/model.rs +++ b/src/input/model.rs @@ -65,16 +65,34 @@ impl ModifyOtherKeysMode { } } -pub fn host_modify_other_keys_mode( +pub fn host_modify_other_keys_mode() -> Option<ModifyOtherKeysMode> { + #[cfg(windows)] + let alacritty_window_id = std::env::var_os("ALACRITTY_WINDOW_ID").is_some(); + #[cfg(not(windows))] + let alacritty_window_id = false; + + host_modify_other_keys_mode_for_env( + std::env::var("TMUX").is_ok(), + std::env::var("TERM_PROGRAM").ok().as_deref(), + std::env::var_os("WEZTERM_PANE").is_some(), + alacritty_window_id, + ) +} + +fn host_modify_other_keys_mode_for_env( in_tmux: bool, term_program: Option<&str>, wezterm_pane: bool, + alacritty_window_id: bool, ) -> Option<ModifyOtherKeysMode> { if in_tmux { return Some(ModifyOtherKeysMode::Mode2); } - if wezterm_pane || term_program.is_some_and(|program| program.eq_ignore_ascii_case("wezterm")) { + if wezterm_pane + || alacritty_window_id + || term_program.is_some_and(|program| program.eq_ignore_ascii_case("wezterm")) + { return Some(ModifyOtherKeysMode::Mode1); } @@ -159,7 +177,7 @@ mod tests { #[test] fn modify_other_keys_mode_is_enabled_for_tmux() { assert_eq!( - host_modify_other_keys_mode(true, Some("WezTerm"), true), + host_modify_other_keys_mode_for_env(true, Some("WezTerm"), true, true), Some(ModifyOtherKeysMode::Mode2) ); } @@ -167,11 +185,19 @@ mod tests { #[test] fn modify_other_keys_mode_is_enabled_for_wezterm_hosts() { assert_eq!( - host_modify_other_keys_mode(false, Some("WezTerm"), false), + host_modify_other_keys_mode_for_env(false, Some("WezTerm"), false, false), + Some(ModifyOtherKeysMode::Mode1) + ); + assert_eq!( + host_modify_other_keys_mode_for_env(false, None, true, false), Some(ModifyOtherKeysMode::Mode1) ); + } + + #[test] + fn modify_other_keys_mode_is_enabled_for_alacritty_hosts() { assert_eq!( - host_modify_other_keys_mode(false, None, true), + host_modify_other_keys_mode_for_env(false, None, false, true), Some(ModifyOtherKeysMode::Mode1) ); } @@ -179,9 +205,12 @@ mod tests { #[test] fn modify_other_keys_mode_is_not_enabled_for_unknown_hosts() { assert_eq!( - host_modify_other_keys_mode(false, Some("ghostty"), false), + host_modify_other_keys_mode_for_env(false, Some("ghostty"), false, false), + None + ); + assert_eq!( + host_modify_other_keys_mode_for_env(false, None, false, false), None ); - assert_eq!(host_modify_other_keys_mode(false, None, false), None); } } diff --git a/src/main.rs b/src/main.rs index ed4d4545..597b332e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -673,11 +673,7 @@ fn main() -> io::Result<()> { Err(err) => return Err(err), }; - let modify_other_keys_mode = crate::input::host_modify_other_keys_mode( - std::env::var("TMUX").is_ok(), - std::env::var("TERM_PROGRAM").ok().as_deref(), - std::env::var_os("WEZTERM_PANE").is_some(), - ); + let modify_other_keys_mode = crate::input::host_modify_other_keys_mode(); let original_hook = std::panic::take_hook(); let panic_resets_modify_other_keys = modify_other_keys_mode.is_some(); |
