use std::io;
use winapi::um::{
consoleapi::ReadConsoleInputW,
processenv::GetStdHandle,
winbase::STD_INPUT_HANDLE,
wincon::{
PeekConsoleInputW, INPUT_RECORD, KEY_EVENT, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED,
RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED,
},
winuser::{
VK_BACK, VK_CONTROL, VK_DELETE, VK_ESCAPE, VK_F1, VK_F24, VK_MENU, VK_PRIOR, VK_SHIFT,
VK_TAB,
},
};
pub fn read_text_batch() -> io::Result<String> {
let mut records: [INPUT_RECORD; 1024] = unsafe { std::mem::zeroed() };
let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
let mut count = 0;
if unsafe {
PeekConsoleInputW(
handle,
records.as_mut_ptr(),
records.len() as u32,
&mut count,
)
} == 0
{
return Err(io::Error::last_os_error());
}
let (take, _) = text_prefix(&records[..count as usize]);
if take == 0 {
return Ok(String::new());
}
if unsafe { ReadConsoleInputW(handle, records.as_mut_ptr(), take as u32, &mut count) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(text_prefix(&records[..count as usize]).1)
}
fn text_prefix(records: &[INPUT_RECORD]) -> (usize, String) {
let mut units = Vec::new();
let mut take = 0;
let mut committed_units = 0;
let mut high_surrogate = false;
let mut previous_cr = false;
for (index, record) in records.iter().enumerate() {
if record.EventType != KEY_EVENT {
break;
}
let key = unsafe { record.Event.KeyEvent() };
let vk = key.wVirtualKeyCode as i32;
if matches!(vk, VK_BACK | VK_ESCAPE | VK_SHIFT | VK_CONTROL | VK_MENU)
|| (VK_PRIOR..=VK_DELETE).contains(&vk)
|| (VK_F1..=VK_F24).contains(&vk)
|| (vk == VK_TAB && key.dwControlKeyState & SHIFT_PRESSED != 0)
{
break;
}
if key.dwControlKeyState
& (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)
!= 0
{
break;
}
let unit = unsafe { *key.uChar.UnicodeChar() };
if unit < 32 && !matches!(unit, 9 | 10 | 13) || unit == 127 {
break;
}
if key.bKeyDown != 0 {
if (0xd800..=0xdbff).contains(&unit) {
if high_surrogate {
break;
}
high_surrogate = true;
} else if (0xdc00..=0xdfff).contains(&unit) {
if !high_surrogate {
break;
}
high_surrogate = false;
} else if high_surrogate {
break;
}
if unit != 10 || !previous_cr {
units.push(if unit == 13 { 10 } else { unit });
}
previous_cr = unit == 13;
}
if !high_surrogate && units.last() != Some(&10) {
take = index + 1;
committed_units = units.len();
}
}
units.truncate(committed_units);
(
take,
String::from_utf16(&units).expect("complete UTF-16 paste prefix"),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn key(unit: u16, down: bool, modifiers: u32) -> INPUT_RECORD {
let mut record: INPUT_RECORD = unsafe { std::mem::zeroed() };
record.EventType = KEY_EVENT;
unsafe {
let key = record.Event.KeyEvent_mut();
key.bKeyDown = i32::from(down);
key.wRepeatCount = 1;
key.dwControlKeyState = modifiers;
*key.uChar.UnicodeChar_mut() = unit;
}
record
}
fn records(text: &str) -> Vec<INPUT_RECORD> {
text.encode_utf16()
.flat_map(|unit| [key(unit, true, 0), key(unit, false, 0)])
.collect()
}
#[test]
fn large_unicode_paste_preserves_text_across_batches() {
let text = "中文🙂\t pasted text\n".repeat(6000);
let queue = records(&text);
let mut offset = 0;
let mut actual = String::new();
while offset < queue.len() {
let end = (offset + 1024).min(queue.len());
let (take, chunk) = text_prefix(&queue[offset..end]);
if take == 0 {
assert_eq!(text_prefix(&queue[offset..]).0, 0);
break;
}
offset += take;
actual.push_str(&chunk);
}
assert_eq!(actual, text.trim_end_matches('\n'));
assert_eq!(queue.len() - offset, 2);
let editor = crate::Editor::simple();
editor.handle_paste(&actual);
assert!(editor.get_text().starts_with("[paste #1"));
assert_eq!(editor.get_expanded_text(), actual.replace('\t', " "));
}
#[test]
fn shortcuts_and_mouse_events_are_left_for_normal_dispatch() {
let mut alt_code = key('中' as u16, false, 0);
unsafe {
alt_code.Event.KeyEvent_mut().wVirtualKeyCode = VK_MENU as u16;
}
let mut back_tab = key(9, true, SHIFT_PRESSED);
unsafe {
back_tab.Event.KeyEvent_mut().wVirtualKeyCode = VK_TAB as u16;
}
for event in [
key('c' as u16, true, LEFT_CTRL_PRESSED),
key('x' as u16, true, LEFT_ALT_PRESSED),
alt_code,
back_tab,
unsafe { std::mem::zeroed() },
] {
let mut queue = records("paste");
let expected = queue.len();
queue.push(event);
queue.extend(records("later"));
assert_eq!(text_prefix(&queue), (expected, "paste".into()));
}
}
#[test]
fn incomplete_surrogate_pair_remains_queued() {
let mut queue = records("中");
queue.push(key(0xd83d, true, 0));
queue.push(key(0xd83d, false, 0));
assert_eq!(text_prefix(&queue), (2, "中".into()));
queue.push(key(0xde42, true, 0));
queue.push(key(0xde42, false, 0));
assert_eq!(text_prefix(&queue), (6, "中🙂".into()));
}
#[test]
fn interior_newlines_and_tabs_are_text_but_final_enter_is_not_consumed() {
let queue = records("a\r\nb\tc\r");
assert_eq!(text_prefix(&queue), (queue.len() - 2, "a\nb\tc".into()));
assert_eq!(text_prefix(&records("\r")), (0, String::new()));
}
}