diff --git a/docs/docs/releases.md b/docs/docs/releases.md index e3f4ac5bcf..dc00a02f1b 100644 --- a/docs/docs/releases.md +++ b/docs/docs/releases.md @@ -5,7 +5,16 @@ language: 'en' # Releases -## 0.2.29 (unreleased) +## 0.2.30 (unreleased) + +- **Add Win32 input mode support for Windows**: Implements Win32 input mode protocol for enhanced keyboard handling + - Enables applications to receive complete Windows KEY_EVENT_RECORD data as escape sequences + - Supports special key combinations like Ctrl+Space, Shift+Enter, Ctrl+Alt+? that don't work in traditional terminal input + - Sends both key press and release events with full modifier state information + - Compatible with applications expecting Microsoft Terminal's Win32 input mode format + - Can be enabled/disabled via escape sequences: `CSI ? 9001 h` (enable) / `CSI ? 9001 l` (disable) + +## 0.2.29 - Fix blinking cursor issue [#1269](https://github.com/raphamorim/rio/issues/1269) - Fix Rio uses UNC (\?\) path as working directory, breaking Neovim subprocesses on Windows diff --git a/frontends/rioterm/src/bindings/kitty_keyboard.rs b/frontends/rioterm/src/bindings/kitty_keyboard.rs index b01c05571a..d23a9a83a9 100644 --- a/frontends/rioterm/src/bindings/kitty_keyboard.rs +++ b/frontends/rioterm/src/bindings/kitty_keyboard.rs @@ -10,8 +10,30 @@ use rio_window::keyboard::NamedKey; use rio_window::platform::modifier_supplement::KeyEventExtModifierSupplement; use std::borrow::Cow; +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mode_check() { + // Test that Win32 input mode flag is properly defined + let mode = Mode::WIN32_INPUT; + assert!(!mode.is_empty()); + + // Test that it doesn't conflict with other modes + let kitty_mode = Mode::KITTY_KEYBOARD_PROTOCOL; + assert!(!mode.intersects(kitty_mode)); + } +} + #[inline(never)] pub fn build_key_sequence(key: &KeyEvent, mods: ModifiersState, mode: Mode) -> Vec { + // Check for Win32 input mode on Windows + #[cfg(target_os = "windows")] + if mode.contains(Mode::WIN32_INPUT) { + return crate::bindings::win32_keyboard::build_win32_sequence(key, mods); + } + let mut modifiers = mods.into(); let kitty_seq = mode.intersects( diff --git a/frontends/rioterm/src/bindings/mod.rs b/frontends/rioterm/src/bindings/mod.rs index 482dfc2765..f70768eb6a 100644 --- a/frontends/rioterm/src/bindings/mod.rs +++ b/frontends/rioterm/src/bindings/mod.rs @@ -3,6 +3,8 @@ // which is licensed under Apache 2.0 license. pub mod kitty_keyboard; +#[cfg(target_os = "windows")] +pub mod win32_keyboard; use crate::crosswords::vi_mode::ViMotion; use crate::crosswords::Mode; diff --git a/frontends/rioterm/src/bindings/win32_keyboard.rs b/frontends/rioterm/src/bindings/win32_keyboard.rs new file mode 100644 index 0000000000..0bb6c79a75 --- /dev/null +++ b/frontends/rioterm/src/bindings/win32_keyboard.rs @@ -0,0 +1,169 @@ +// Win32 input mode support for Windows +// Implements the Win32 input mode protocol that sends Windows KEY_EVENT_RECORD data as escape sequences +// Format: ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ + +use rio_window::event::{ElementState, KeyEvent}; +use rio_window::keyboard::{Key, KeyCode, ModifiersState, NamedKey, PhysicalKey}; +use rio_window::platform::modifier_supplement::KeyEventExtModifierSupplement; +use rio_window::platform::scancode::PhysicalKeyExtScancode; + +/// Build Win32 input mode sequence for Windows. +/// Format: ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ +/// +/// Where: +/// - Vk: Virtual key code (wVirtualKeyCode) +/// - Sc: Virtual scan code (wVirtualScanCode) +/// - Uc: Unicode character value (uChar.UnicodeChar) +/// - Kd: Key down flag (1 for down, 0 for up) +/// - Cs: Control key state (dwControlKeyState) +/// - Rc: Repeat count (wRepeatCount) +pub fn build_win32_sequence(key: &KeyEvent, mods: ModifiersState) -> Vec { + // Get the scan code from the physical key + let sc = key.physical_key.to_scancode().unwrap_or(0) as u16; + + // Get virtual key code from the logical key or physical key + let vk = match &key.logical_key { + Key::Named(named) => named_key_to_vk(named), + Key::Character(s) if s.len() == 1 => { + // For single characters, use the character's uppercase value + s.chars().next().unwrap().to_ascii_uppercase() as u16 + } + _ => { + // Fall back to physical key mapping + if let PhysicalKey::Code(code) = key.physical_key { + keycode_to_vk(code) + } else { + 0 + } + } + }; + + // Get Unicode character value + let uc = key + .text_with_all_modifiers() + .and_then(|s| s.chars().next()) + .map(|c| c as u16) + .unwrap_or(0); + + // Key down flag (1 for pressed, 0 for released) + let kd = if key.state == ElementState::Pressed { + 1 + } else { + 0 + }; + + // Control key state - Windows format + let mut cs = 0u16; + if mods.shift_key() { + cs |= 0x0010; + } // SHIFT_PRESSED + if mods.control_key() { + cs |= 0x0008; + } // LEFT_CTRL_PRESSED + if mods.alt_key() { + cs |= 0x0001; + } // LEFT_ALT_PRESSED + if mods.super_key() { + cs |= 0x0008; + } // Windows key maps to CTRL + + // Repeat count + let rc = if key.repeat { 2 } else { 1 }; + + format!("\x1b[{};{};{};{};{};{}_", vk, sc, uc, kd, cs, rc).into_bytes() +} + +fn named_key_to_vk(key: &NamedKey) -> u16 { + use NamedKey::*; + match key { + Backspace => 0x08, + Tab => 0x09, + Enter => 0x0D, + Escape => 0x1B, + Space => 0x20, + Delete => 0x2E, + ArrowLeft => 0x25, + ArrowUp => 0x26, + ArrowRight => 0x27, + ArrowDown => 0x28, + Insert => 0x2D, + Home => 0x24, + End => 0x23, + PageUp => 0x21, + PageDown => 0x22, + F1 => 0x70, + F2 => 0x71, + F3 => 0x72, + F4 => 0x73, + F5 => 0x74, + F6 => 0x75, + F7 => 0x76, + F8 => 0x77, + F9 => 0x78, + F10 => 0x79, + F11 => 0x7A, + F12 => 0x7B, + _ => 0, + } +} + +fn keycode_to_vk(code: KeyCode) -> u16 { + use KeyCode::*; + match code { + KeyA => 0x41, + KeyB => 0x42, + KeyC => 0x43, + KeyD => 0x44, + KeyE => 0x45, + KeyF => 0x46, + KeyG => 0x47, + KeyH => 0x48, + KeyI => 0x49, + KeyJ => 0x4A, + KeyK => 0x4B, + KeyL => 0x4C, + KeyM => 0x4D, + KeyN => 0x4E, + KeyO => 0x4F, + KeyP => 0x50, + KeyQ => 0x51, + KeyR => 0x52, + KeyS => 0x53, + KeyT => 0x54, + KeyU => 0x55, + KeyV => 0x56, + KeyW => 0x57, + KeyX => 0x58, + KeyY => 0x59, + KeyZ => 0x5A, + Digit0 => 0x30, + Digit1 => 0x31, + Digit2 => 0x32, + Digit3 => 0x33, + Digit4 => 0x34, + Digit5 => 0x35, + Digit6 => 0x36, + Digit7 => 0x37, + Digit8 => 0x38, + Digit9 => 0x39, + Space => 0x20, + _ => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rio_backend::crosswords::Mode; + + #[test] + fn test_win32_input_mode_flag() { + // Test that Win32 input mode flag is properly defined + let mode = Mode::WIN32_INPUT; + assert!(!mode.is_empty()); + + // Test that it doesn't conflict with Kitty keyboard protocol + let kitty_mode = Mode::KITTY_KEYBOARD_PROTOCOL; + assert!(!mode.intersects(kitty_mode)); + } +} diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index e599c89701..6a2e334455 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -554,6 +554,13 @@ impl Screen<'_> { let mods = self.modifiers.state(); if key.state == ElementState::Released { + // Win32 input mode sends all key release events + if mode.contains(Mode::WIN32_INPUT) { + let bytes = build_key_sequence(key, mods, mode); + self.ctx_mut().current_mut().messenger.send_write(bytes); + return; + } + if !mode.contains(Mode::REPORT_EVENT_TYPES) || mode.contains(Mode::VI) || self.search_active() diff --git a/rio-backend/src/ansi/mode.rs b/rio-backend/src/ansi/mode.rs index e79f3f42e5..5a305037e3 100644 --- a/rio-backend/src/ansi/mode.rs +++ b/rio-backend/src/ansi/mode.rs @@ -69,6 +69,7 @@ impl PrivateMode { 1049 => Self::Named(NamedPrivateMode::SwapScreenAndSetRestoreCursor), 2004 => Self::Named(NamedPrivateMode::BracketedPaste), 2026 => Self::Named(NamedPrivateMode::SyncUpdate), + 9001 => Self::Named(NamedPrivateMode::Win32Input), _ => Self::Unknown(mode), } } @@ -120,6 +121,8 @@ pub enum NamedPrivateMode { BracketedPaste = 2004, /// The mode is handled automatically by [`Processor`]. SyncUpdate = 2026, + /// Win32 input mode - sends Windows KEY_EVENT_RECORD data as escape sequences. + Win32Input = 9001, } /// Mode for clearing line. diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs index 9380e76765..d41943c67e 100644 --- a/rio-backend/src/crosswords/mod.rs +++ b/rio-backend/src/crosswords/mod.rs @@ -97,6 +97,7 @@ bitflags! { const REPORT_ALTERNATE_KEYS = 1 << 20; const REPORT_ALL_KEYS_AS_ESC = 1 << 21; const REPORT_ASSOCIATED_TEXT = 1 << 22; + const WIN32_INPUT = 1 << 23; const MOUSE_MODE = Self::MOUSE_REPORT_CLICK.bits() | Self::MOUSE_MOTION.bits() | Self::MOUSE_DRAG.bits(); const KITTY_KEYBOARD_PROTOCOL = Self::DISAMBIGUATE_ESC_CODES.bits() | Self::REPORT_EVENT_TYPES.bits() @@ -1525,6 +1526,7 @@ impl Handler for Crosswords { .send_event(RioEvent::CursorBlinkingChange, self.window_id); } NamedPrivateMode::SyncUpdate => (), + NamedPrivateMode::Win32Input => self.mode.insert(Mode::WIN32_INPUT), } } @@ -1597,6 +1599,7 @@ impl Handler for Crosswords { .send_event(RioEvent::CursorBlinkingChange, self.window_id); } NamedPrivateMode::SyncUpdate => (), + NamedPrivateMode::Win32Input => self.mode.remove(Mode::WIN32_INPUT), } } @@ -1644,6 +1647,9 @@ impl Handler for Crosswords { } NamedPrivateMode::SyncUpdate => ModeState::Reset, NamedPrivateMode::ColumnMode => ModeState::NotSupported, + NamedPrivateMode::Win32Input => { + self.mode.contains(Mode::WIN32_INPUT).into() + } }, PrivateMode::Unknown(_) => ModeState::NotSupported, }; @@ -3067,6 +3073,7 @@ impl Dimensions for Crosswords { #[cfg(test)] mod tests { use super::*; + use crate::ansi::mode::{NamedPrivateMode, PrivateMode}; use crate::crosswords::pos::{Column, Line, Pos, Side}; use crate::crosswords::CrosswordsSize; use crate::event::VoidListener; @@ -3677,4 +3684,32 @@ mod tests { // Should damage line 5 assert_eq!(damage_result_2, Some(true), "Should damage line 5"); } + + #[test] + fn test_win32_input_mode() { + use crate::performer::handler::Handler; + + let size = CrosswordsSize::new(10, 10); + let window_id = crate::event::WindowId::from(0); + let route_id = 0; + let mut term = + Crosswords::new(size, CursorShape::Block, VoidListener, window_id, route_id); + + // Initially Win32 input mode should be off + assert!(!term.mode.contains(Mode::WIN32_INPUT)); + + // Enable Win32 input mode + Handler::set_private_mode( + &mut term, + PrivateMode::Named(NamedPrivateMode::Win32Input), + ); + assert!(term.mode.contains(Mode::WIN32_INPUT)); + + // Disable Win32 input mode + Handler::unset_private_mode( + &mut term, + PrivateMode::Named(NamedPrivateMode::Win32Input), + ); + assert!(!term.mode.contains(Mode::WIN32_INPUT)); + } }