Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions frontends/rioterm/src/bindings/kitty_keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
// 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(
Expand Down
2 changes: 2 additions & 0 deletions frontends/rioterm/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
169 changes: 169 additions & 0 deletions frontends/rioterm/src/bindings/win32_keyboard.rs
Original file line number Diff line number Diff line change
@@ -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<u8> {
// 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));
}
}
7 changes: 7 additions & 0 deletions frontends/rioterm/src/screen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions rio-backend/src/ansi/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions rio-backend/src/crosswords/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -1525,6 +1526,7 @@ impl<U: EventListener> Handler for Crosswords<U> {
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
}
NamedPrivateMode::SyncUpdate => (),
NamedPrivateMode::Win32Input => self.mode.insert(Mode::WIN32_INPUT),
}
}

Expand Down Expand Up @@ -1597,6 +1599,7 @@ impl<U: EventListener> Handler for Crosswords<U> {
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
}
NamedPrivateMode::SyncUpdate => (),
NamedPrivateMode::Win32Input => self.mode.remove(Mode::WIN32_INPUT),
}
}

Expand Down Expand Up @@ -1644,6 +1647,9 @@ impl<U: EventListener> Handler for Crosswords<U> {
}
NamedPrivateMode::SyncUpdate => ModeState::Reset,
NamedPrivateMode::ColumnMode => ModeState::NotSupported,
NamedPrivateMode::Win32Input => {
self.mode.contains(Mode::WIN32_INPUT).into()
}
},
PrivateMode::Unknown(_) => ModeState::NotSupported,
};
Expand Down Expand Up @@ -3067,6 +3073,7 @@ impl<T: EventListener> Dimensions for Crosswords<T> {
#[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;
Expand Down Expand Up @@ -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));
}
}
Loading