From c35dae7da2e44ffe926702a234970ef0e3f28679 Mon Sep 17 00:00:00 2001 From: matkaas Date: Mon, 28 Mar 2022 22:06:12 +0200 Subject: [PATCH 1/4] Recognise Tab by virtual key code on Windows This enables us to deliver more key combinations triggered with tab, namely: * Ctrl+Tab * Ctrl+Shift+Tab --- src/event/sys/windows/parse.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/event/sys/windows/parse.rs b/src/event/sys/windows/parse.rs index 93000b86d..8a77ff046 100644 --- a/src/event/sys/windows/parse.rs +++ b/src/event/sys/windows/parse.rs @@ -5,7 +5,8 @@ use winapi::um::{ }, winuser::{ VK_BACK, VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, - VK_INSERT, VK_LEFT, VK_MENU, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_UP, + VK_INSERT, VK_LEFT, VK_MENU, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_TAB, + VK_UP, }, }; @@ -75,6 +76,8 @@ fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { VK_END => Some(KeyCode::End), VK_DELETE => Some(KeyCode::Delete), VK_INSERT => Some(KeyCode::Insert), + VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab), + VK_TAB => Some(KeyCode::Tab), _ => { // Modifier Keys (Ctrl, Alt, Shift) Support let character_raw = key_event.u_char; @@ -109,13 +112,7 @@ fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { } } - if modifiers.contains(KeyModifiers::SHIFT) && character == '\t' { - Some(KeyCode::BackTab) - } else if character == '\t' { - Some(KeyCode::Tab) - } else { - Some(KeyCode::Char(character)) - } + Some(KeyCode::Char(character)) } else { std::char::from_u32(character_raw as u32).map(KeyCode::Char) } From 75235031d0a355a762c578c22f079fe7c20efbd9 Mon Sep 17 00:00:00 2001 From: matkaas Date: Mon, 28 Mar 2022 22:36:11 +0200 Subject: [PATCH 2/4] Handle unicode input from supplemental planes on Windows This entails proper decoding of UTF-16 surrogate pairs. With this change, we deliver KeyEvent::Char(...) events for code points outside of the BMP, such as many CJK code points as well as all emojis. This works with both pasting and IME input in Windows Terminal. This currently only works with IME input in Conhost terminal. Pasting doesn't work because Conhost synthesizes Alt codes for the higher unicode scalar values, rather than delivering a pair of surrogate code points. Some special handling will be required to interpret unicode scalar values from Alt codes. --- src/event/source/windows.rs | 7 +- src/event/sys/windows/parse.rs | 116 +++++++++++++++++++++------------ 2 files changed, 79 insertions(+), 44 deletions(-) diff --git a/src/event/source/windows.rs b/src/event/source/windows.rs index 935988395..f53a39250 100644 --- a/src/event/source/windows.rs +++ b/src/event/source/windows.rs @@ -16,6 +16,7 @@ use super::super::{ pub(crate) struct WindowsEventSource { console: Console, poll: WinApiPoll, + surrogate_buffer: Option, } impl WindowsEventSource { @@ -28,6 +29,8 @@ impl WindowsEventSource { poll: WinApiPoll::new(), #[cfg(feature = "event-stream")] poll: WinApiPoll::new()?, + + surrogate_buffer: None, }) } } @@ -41,7 +44,9 @@ impl EventSource for WindowsEventSource { let number = self.console.number_of_console_input_events()?; if event_ready && number != 0 { let event = match self.console.read_single_input_event()? { - InputRecord::KeyEvent(record) => handle_key_event(record), + InputRecord::KeyEvent(record) => { + handle_key_event(record, &mut self.surrogate_buffer) + } InputRecord::MouseEvent(record) => handle_mouse_event(record), InputRecord::WindowBufferSizeEvent(record) => { Some(Event::Resize(record.size.x as u16, record.size.y as u16)) diff --git a/src/event/sys/windows/parse.rs b/src/event/sys/windows/parse.rs index 8a77ff046..f94825114 100644 --- a/src/event/sys/windows/parse.rs +++ b/src/event/sys/windows/parse.rs @@ -23,18 +23,54 @@ pub(crate) fn handle_mouse_event(mouse_event: MouseEvent) -> Option { None } -pub(crate) fn handle_key_event(key_event: KeyEventRecord) -> Option { +enum WindowsKeyEvent { + KeyEvent(KeyEvent), + Surrogate(u16), +} + +pub(crate) fn handle_key_event( + key_event: KeyEventRecord, + surrogate_buffer: &mut Option, +) -> Option { if key_event.key_down { - if let Some(event) = parse_key_event_record(&key_event) { - return Some(Event::Key(event)); + let windows_key_event = parse_key_event_record(&key_event)?; + match windows_key_event { + WindowsKeyEvent::KeyEvent(key_event) => { + // Discard any buffered surrogate value if another valid key event comes before the + // next surrogate value. + *surrogate_buffer = None; + Some(Event::Key(key_event)) + } + WindowsKeyEvent::Surrogate(new_surrogate) => { + let ch = handle_surrogate(surrogate_buffer, new_surrogate)?; + let modifiers = KeyModifiers::from(&key_event.control_key_state); + let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers); + Some(Event::Key(key_event)) + } } + } else { + None } +} - None +fn handle_surrogate(surrogate_buffer: &mut Option, new_surrogate: u16) -> Option { + match *surrogate_buffer { + Some(buffered_surrogate) => { + *surrogate_buffer = None; + std::char::decode_utf16([buffered_surrogate, new_surrogate]) + .next() + .unwrap() + .ok() + } + None => { + *surrogate_buffer = Some(new_surrogate); + None + } + } } -impl From for KeyModifiers { - fn from(state: ControlKeyState) -> Self { +impl From<&ControlKeyState> for KeyModifiers { + fn from(state: &ControlKeyState) -> Self { let shift = state.has_state(SHIFT_PRESSED); let alt = state.has_state(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); let control = state.has_state(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); @@ -55,8 +91,8 @@ impl From for KeyModifiers { } } -fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { - let modifiers = KeyModifiers::from(key_event.control_key_state); +fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { + let modifiers = KeyModifiers::from(&key_event.control_key_state); let key_code = key_event.virtual_key_code as i32; @@ -79,48 +115,42 @@ fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab), VK_TAB => Some(KeyCode::Tab), _ => { - // Modifier Keys (Ctrl, Alt, Shift) Support - let character_raw = key_event.u_char; - - if character_raw < 255 { - // Invalid character - if character_raw == 0 { - return None; + let utf16 = key_event.u_char; + match utf16 { + 0 => { + // Unsupported key combination. + None } - - let mut character = character_raw as u8 as char; - - if modifiers.contains(KeyModifiers::CONTROL) - && !modifiers.contains(KeyModifiers::ALT) - { - // we need to do some parsing - // Control character will take the ASCII code produced by the key and bitwise AND - // it with 31, forcing bits 6 and bits 7 to zero. - // So we can make a bitwise OR back to see what's the raw control character. - let c = character_raw as u8; - if c <= b'\x1F' { - character = (c | b'\x40') as char; - // if we press something like ctrl-g, we will get `character` with value `G`. - // in this case, convert the `character` to lowercase `g`. - if character.is_ascii_uppercase() - && !modifiers.contains(KeyModifiers::SHIFT) - { - character.make_ascii_lowercase(); - } - } else { - return None; + control_code @ 0x01..=0x1f if modifiers.contains(KeyModifiers::CONTROL) => { + // Terminal emulators encode the key combinations CTRL+A through CTRL+Z, CTRL+[ + // CTRL+\, CTRL+], CTRL+^, and CTRL+_ as control codes 0x01 through 0x1f + // respectively. + // We map them back to character codes before we return the key event. Other + // keys that produce control codes (ESC, TAB, ENTER, BACKSPACE) are handled + // above by their virtual key codes and distinguished that way. + let mut ch = (control_code + 64) as u8 as char; + if !modifiers.contains(KeyModifiers::SHIFT) { + ch.make_ascii_lowercase(); } + Some(KeyCode::Char(ch)) + } + surrogate @ 0xD800..=0xDFFF => { + return Some(WindowsKeyEvent::Surrogate(surrogate)); + } + unicode_scalar_value => { + // Unwrap is safe: We tested for surrogate values above and those are the only + // u16 values that are invalid when directly interpreted as unicode scalar + // values. + let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap(); + Some(KeyCode::Char(ch)) } - - Some(KeyCode::Char(character)) - } else { - std::char::from_u32(character_raw as u32).map(KeyCode::Char) } } }; if let Some(key_code) = parse_result { - return Some(KeyEvent::new(key_code, modifiers)); + let key_event = KeyEvent::new(key_code, modifiers); + return Some(WindowsKeyEvent::KeyEvent(key_event)); } None @@ -134,7 +164,7 @@ pub fn parse_relative_y(y: i16) -> Result { } fn parse_mouse_event_record(event: &MouseEvent) -> Result> { - let modifiers = KeyModifiers::from(event.control_key_state); + let modifiers = KeyModifiers::from(&event.control_key_state); let xpos = event.mouse_position.x as u16; let ypos = parse_relative_y(event.mouse_position.y)? as u16; From 0fcce0dcb431dc1e903b04e3cefa7260c28668b4 Mon Sep 17 00:00:00 2001 From: matkaas Date: Thu, 31 Mar 2022 14:52:32 +0200 Subject: [PATCH 3/4] Handle Alt code input on Windows In addition to handling manual user input of Alt codes, this also handles pasting of unicode from the supplemental planes into a Conhost terminal, as the Conhost terminal encodes such input by synthesizing key sequences for an Alt code. --- src/event/sys/windows/parse.rs | 58 +++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/src/event/sys/windows/parse.rs b/src/event/sys/windows/parse.rs index f94825114..421091604 100644 --- a/src/event/sys/windows/parse.rs +++ b/src/event/sys/windows/parse.rs @@ -32,24 +32,20 @@ pub(crate) fn handle_key_event( key_event: KeyEventRecord, surrogate_buffer: &mut Option, ) -> Option { - if key_event.key_down { - let windows_key_event = parse_key_event_record(&key_event)?; - match windows_key_event { - WindowsKeyEvent::KeyEvent(key_event) => { - // Discard any buffered surrogate value if another valid key event comes before the - // next surrogate value. - *surrogate_buffer = None; - Some(Event::Key(key_event)) - } - WindowsKeyEvent::Surrogate(new_surrogate) => { - let ch = handle_surrogate(surrogate_buffer, new_surrogate)?; - let modifiers = KeyModifiers::from(&key_event.control_key_state); - let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers); - Some(Event::Key(key_event)) - } + let windows_key_event = parse_key_event_record(&key_event)?; + match windows_key_event { + WindowsKeyEvent::KeyEvent(key_event) => { + // Discard any buffered surrogate value if another valid key event comes before the + // next surrogate value. + *surrogate_buffer = None; + Some(Event::Key(key_event)) + } + WindowsKeyEvent::Surrogate(new_surrogate) => { + let ch = handle_surrogate(surrogate_buffer, new_surrogate)?; + let modifiers = KeyModifiers::from(&key_event.control_key_state); + let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers); + Some(Event::Key(key_event)) } - } else { - None } } @@ -93,10 +89,34 @@ impl From<&ControlKeyState> for KeyModifiers { fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { let modifiers = KeyModifiers::from(&key_event.control_key_state); + let virtual_key_code = key_event.virtual_key_code as i32; + + // We normally ignore all key release events, but we will make an exception for an Alt key + // release if it carries a u_char value, as this indicates an Alt code. + let is_alt_code = virtual_key_code == VK_MENU && !key_event.key_down && key_event.u_char != 0; + if is_alt_code { + let utf16 = key_event.u_char; + match utf16 { + surrogate @ 0xD800..=0xDFFF => { + return Some(WindowsKeyEvent::Surrogate(surrogate)); + } + unicode_scalar_value => { + // Unwrap is safe: We tested for surrogate values above and those are the only + // u16 values that are invalid when directly interpreted as unicode scalar + // values. + let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap(); + let key_code = KeyCode::Char(ch); + let key_event = KeyEvent::new(key_code, modifiers); + return Some(WindowsKeyEvent::KeyEvent(key_event)); + } + } + } - let key_code = key_event.virtual_key_code as i32; + if !key_event.key_down { + return None; + } - let parse_result = match key_code { + let parse_result = match virtual_key_code { VK_SHIFT | VK_CONTROL | VK_MENU => None, VK_BACK => Some(KeyCode::Backspace), VK_ESCAPE => Some(KeyCode::Esc), From f52347c10c85b4be3e4349a4a7c0e7e04da7f69e Mon Sep 17 00:00:00 2001 From: matkaas Date: Sun, 3 Apr 2022 15:38:59 +0200 Subject: [PATCH 4/4] Handle key events that don't provide a character on Windows Many key combinations produce key events which have u_char == 0, and these have been discarded until now. This for example includes all combinations involving the Ctrl+Alt modifiers, as well as many key combinations with just Ctrl. We can provide events for such key combinations by determining the character associated with the keys from consulting the keyboard layout. Almost all keys on a keyboard have characters associated with them -- it's just a question of whether we can determine what character corresponds to a key event. There are some caveats involved in doing that... In addition, the key events with u_char in the ASCII control code range was until now mapped into the ASCII range '@' to '_' which is inaccurate for many keys and for users with non-US keyboard layouts. The character for key combinations that produce control codes are now also handled by consulting the keyboard layout. The caveats revolve around determining the keyboard layout, which has two issues: 1. There is a race condition between the user typing in their terminal with one keyboard layout active, and the console application determining the keyboard layout while processing the key event later, as these two events happen asynchronously. If a user changes the active keyboard layout in between the two events, then the console application might misinterpret the character. 2. For console applications running in a Conhost terminal, it turns out to be very difficult to determine the active keyboard layout. There appears to be no available APIs that reliably provide the layout. --- src/event/sys/windows/parse.rs | 144 ++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 20 deletions(-) diff --git a/src/event/sys/windows/parse.rs b/src/event/sys/windows/parse.rs index 421091604..b5c8cf7bd 100644 --- a/src/event/sys/windows/parse.rs +++ b/src/event/sys/windows/parse.rs @@ -1,12 +1,14 @@ use crossterm_winapi::{ControlKeyState, EventFlags, KeyEventRecord, MouseEvent, ScreenBuffer}; use winapi::um::{ wincon::{ - LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED, + CAPSLOCK_ON, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, + SHIFT_PRESSED, }, winuser::{ - VK_BACK, VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, - VK_INSERT, VK_LEFT, VK_MENU, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_TAB, - VK_UP, + GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_BACK, + VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT, + VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, + VK_TAB, VK_UP, }, }; @@ -87,6 +89,109 @@ impl From<&ControlKeyState> for KeyModifiers { } } +enum CharCase { + LowerCase, + UpperCase, +} + +fn try_ensure_char_case(ch: char, desired_case: CharCase) -> char { + match desired_case { + CharCase::LowerCase if ch.is_uppercase() => { + let mut iter = ch.to_lowercase(); + // Unwrap is safe; iterator yields one or more chars. + let ch_lower = iter.next().unwrap(); + if iter.next() == None { + ch_lower + } else { + ch + } + } + CharCase::UpperCase if ch.is_lowercase() => { + let mut iter = ch.to_uppercase(); + // Unwrap is safe; iterator yields one or more chars. + let ch_upper = iter.next().unwrap(); + if iter.next() == None { + ch_upper + } else { + ch + } + } + _ => ch, + } +} + +// Attempts to return the character for a key event accounting for the user's keyboard layout. +// The returned character (if any) is capitalized (if applicable) based on shift and capslock state. +// Returns None if the key doesn't map to a character or if it is a dead key. +// We use the *currently* active keyboard layout (if it can be determined). This layout may not +// correspond to the keyboard layout that was active when the user typed their input, since console +// applications get their input asynchronously from the terminal. By the time a console application +// can process a key input, the user may have changed the active layout. In this case, the character +// returned might not correspond to what the user expects, but there is no way for a console +// application to know what the keyboard layout actually was for a key event, so this is our best +// effort. If a console application processes input in a timely fashion, then it is unlikely that a +// user has time to change their keyboard layout before a key event is processed. +fn get_char_for_key(key_event: &KeyEventRecord) -> Option { + let virtual_key_code = key_event.virtual_key_code as u32; + let virtual_scan_code = key_event.virtual_scan_code as u32; + let key_state = [0u8; 256]; + let mut utf16_buf = [0u16, 16]; + let dont_change_kernel_keyboard_state = 0x4; + + // Best-effort attempt at determining the currently active keyboard layout. + // At the time of writing, this works for a console application running in Windows Terminal, but + // doesn't work under a Conhost terminal. For Conhost, the window handle returned by + // GetForegroundWindow() does not appear to actually be the foreground window which has the + // keyboard layout associated with it (or perhaps it is, but also has special protection that + // doesn't allow us to query it). + // When this determination fails, the returned keyboard layout handle will be null, which is an + // acceptable input for ToUnicodeEx, as that argument is optional. In this case ToUnicodeEx + // appears to use the keyboard layout associated with the current thread, which will be the + // layout that was inherited when the console application started (or possibly when the current + // thread was spawned). This is then unfortunately not updated when the user changes their + // keyboard layout in the terminal, but it's what we get. + let active_keyboard_layout = unsafe { + let foreground_window = GetForegroundWindow(); + let foreground_thread = GetWindowThreadProcessId(foreground_window, std::ptr::null_mut()); + GetKeyboardLayout(foreground_thread) + }; + + let ret = unsafe { + ToUnicodeEx( + virtual_key_code, + virtual_scan_code, + key_state.as_ptr(), + utf16_buf.as_mut_ptr(), + utf16_buf.len() as i32, + dont_change_kernel_keyboard_state, + active_keyboard_layout, + ) + }; + + // -1 indicates a dead key. + // 0 indicates no character for this key. + if ret < 1 { + return None; + } + + let mut ch_iter = std::char::decode_utf16(utf16_buf.into_iter().take(ret as usize)); + let mut ch = ch_iter.next()?.ok()?; + if ch_iter.next() != None { + // Key doesn't map to a single char. + return None; + } + + let is_shift_pressed = key_event.control_key_state.has_state(SHIFT_PRESSED); + let is_capslock_on = key_event.control_key_state.has_state(CAPSLOCK_ON); + let desired_case = if is_shift_pressed ^ is_capslock_on { + CharCase::UpperCase + } else { + CharCase::LowerCase + }; + ch = try_ensure_char_case(ch, desired_case); + Some(ch) +} + fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { let modifiers = KeyModifiers::from(&key_event.control_key_state); let virtual_key_code = key_event.virtual_key_code as i32; @@ -112,6 +217,14 @@ fn parse_key_event_record(key_event: &KeyEventRecord) -> Option } } + // Don't generate events for numpad key presses when they're producing Alt codes. + let is_numpad_numeric_key = (VK_NUMPAD0..=VK_NUMPAD9).contains(&virtual_key_code); + let is_only_alt_modifier = modifiers.contains(KeyModifiers::ALT) + && !modifiers.contains(KeyModifiers::SHIFT | KeyModifiers::CONTROL); + if is_only_alt_modifier && is_numpad_numeric_key { + return None; + } + if !key_event.key_down { return None; } @@ -137,22 +250,13 @@ fn parse_key_event_record(key_event: &KeyEventRecord) -> Option _ => { let utf16 = key_event.u_char; match utf16 { - 0 => { - // Unsupported key combination. - None - } - control_code @ 0x01..=0x1f if modifiers.contains(KeyModifiers::CONTROL) => { - // Terminal emulators encode the key combinations CTRL+A through CTRL+Z, CTRL+[ - // CTRL+\, CTRL+], CTRL+^, and CTRL+_ as control codes 0x01 through 0x1f - // respectively. - // We map them back to character codes before we return the key event. Other - // keys that produce control codes (ESC, TAB, ENTER, BACKSPACE) are handled - // above by their virtual key codes and distinguished that way. - let mut ch = (control_code + 64) as u8 as char; - if !modifiers.contains(KeyModifiers::SHIFT) { - ch.make_ascii_lowercase(); - } - Some(KeyCode::Char(ch)) + 0x00..=0x1f => { + // Some key combinations generate either no u_char value or generate control + // codes. To deliver back a KeyCode::Char(...) event we want to know which + // character the key normally maps to on the user's keyboard layout. + // The keys that intentionally generate control codes (ESC, ENTER, TAB, etc.) + // are handled by their virtual key codes above. + get_char_for_key(key_event).map(KeyCode::Char) } surrogate @ 0xD800..=0xDFFF => { return Some(WindowsKeyEvent::Surrogate(surrogate));