#[derive(Debug)]
pub enum Key {
ArrowUp, ArrowDown, ArrowLeft, ArrowRight,
Home, End, Insert, Delete, PageUp, PageDown,
Enter, Backspace, Tab, ShiftTab,
CtrlU, CtrlK, CtrlD,
Char(char), }
#[cfg(windows)]
pub mod platform {
use super::Key;
use std::io;
use winapi::shared::minwindef::DWORD;
use winapi::um::consoleapi::ReadConsoleInputW;
use winapi::um::processenv::GetStdHandle;
use winapi::um::winbase::STD_INPUT_HANDLE;
use winapi::um::wincon::{INPUT_RECORD, KEY_EVENT};
use winapi::um::wincontypes::KEY_EVENT_RECORD;
const LEFT_CTRL_PRESSED: u32 = 0x0008;
const RIGHT_CTRL_PRESSED: u32 = 0x0004;
const SHIFT_PRESSED: u32 = 0x0010;
pub fn read_key() -> io::Result<Key> {
unsafe {
let handle = GetStdHandle(STD_INPUT_HANDLE);
if handle.is_null() {
return Err(io::Error::new(io::ErrorKind::Other, "Invalid handle"));
}
let mut record: INPUT_RECORD = std::mem::zeroed();
let mut read: DWORD = 0;
loop {
if ReadConsoleInputW(handle, &mut record, 1, &mut read) == 0 {
return Err(io::Error::last_os_error());
}
if record.EventType == KEY_EVENT {
let key_event: KEY_EVENT_RECORD = *record.Event.KeyEvent();
if key_event.bKeyDown == 0 {
continue; }
let vkey = key_event.wVirtualKeyCode;
let c = *key_event.uChar.UnicodeChar() as u32;
let ctrl = (key_event.dwControlKeyState
& (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED))
!= 0;
let shift = (key_event.dwControlKeyState & SHIFT_PRESSED) != 0;
if ctrl {
match vkey {
0x55 => return Ok(Key::CtrlU), 0x4B => return Ok(Key::CtrlK), 0x44 => return Ok(Key::CtrlD), _ => {}
}
}
match vkey {
0x21 => return Ok(Key::PageUp),
0x22 => return Ok(Key::PageDown),
0x23 => return Ok(Key::End),
0x24 => return Ok(Key::Home),
0x25 => return Ok(Key::ArrowLeft),
0x26 => return Ok(Key::ArrowUp),
0x27 => return Ok(Key::ArrowRight),
0x28 => return Ok(Key::ArrowDown),
0x2E => return Ok(Key::Delete),
0x08 => return Ok(Key::Backspace),
0x09 => return Ok(if shift { Key::ShiftTab } else { Key::Tab }),
0x0D => return Ok(Key::Enter),
_ => {}
}
if c != 0 {
return Ok(Key::Char(std::char::from_u32(c).unwrap_or('\0')));
}
}
}
}
}
}
#[cfg(not(windows))]
pub mod platform {
use super::Key;
use std::io::{self, Read};
pub fn read_key() -> io::Result<Key> {
let stdin = io::stdin();
let mut bytes = stdin.lock().bytes();
while let Some(Ok(b)) = bytes.next() {
match b {
b'\x1B' => {
if let Some(Ok(b2)) = bytes.next()
&& b2 == b'['
&& let Some(Ok(b3)) = bytes.next()
{
return Ok(match b3 {
b'A' => Key::ArrowUp,
b'B' => Key::ArrowDown,
b'C' => Key::ArrowRight,
b'D' => Key::ArrowLeft,
b'H' => Key::Home,
b'F' => Key::End,
b'Z' => Key::ShiftTab,
b'1' | b'2' | b'3' | b'5' | b'6' => {
let _ = bytes.next();
match b3 {
b'1' => Key::Home,
b'2' => Key::Insert,
b'3' => Key::Delete,
b'5' => Key::PageUp,
b'6' => Key::PageDown,
_ => Key::Char('~'),
}
}
_ => Key::Char(b3 as char),
});
}
}
b'\x15' => return Ok(Key::CtrlU), b'\x0B' => return Ok(Key::CtrlK), b'\x04' => return Ok(Key::CtrlD),
b'\r' | b'\n' => return Ok(Key::Enter),
b'\t' => return Ok(Key::Tab),
b'\x7F' | b'\x08' => return Ok(Key::Backspace),
c => return Ok(Key::Char(c as char)),
}
}
Err(io::Error::new(io::ErrorKind::UnexpectedEof, "No input"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_enum_debug() {
let key = Key::ArrowUp;
assert_eq!(format!("{:?}", key), "ArrowUp");
let char_key = Key::Char('a');
assert_eq!(format!("{:?}", char_key), "Char('a')");
}
#[test]
fn test_key_variants_exist() {
let _keys = vec![
Key::ArrowUp,
Key::ArrowDown,
Key::ArrowLeft,
Key::ArrowRight,
Key::Home,
Key::End,
Key::Insert,
Key::Delete,
Key::PageUp,
Key::PageDown,
Key::Enter,
Key::Backspace,
Key::Tab,
Key::ShiftTab,
Key::CtrlU,
Key::CtrlK,
Key::CtrlD,
Key::Char('x'),
];
}
#[test]
fn test_char_key_with_various_characters() {
let alphanumeric = Key::Char('A');
let numeric = Key::Char('5');
let special = Key::Char('!');
let space = Key::Char(' ');
let unicode = Key::Char('ü');
match alphanumeric {
Key::Char('A') => (),
_ => panic!("Expected Char('A')"),
}
match numeric {
Key::Char('5') => (),
_ => panic!("Expected Char('5')"),
}
match special {
Key::Char('!') => (),
_ => panic!("Expected Char('!')"),
}
match space {
Key::Char(' ') => (),
_ => panic!("Expected Char(' ')"),
}
match unicode {
Key::Char('ü') => (),
_ => panic!("Expected Char('ü')"),
}
}
#[test]
fn test_key_matching() {
fn is_arrow_key(key: &Key) -> bool {
matches!(
key,
Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight
)
}
assert!(is_arrow_key(&Key::ArrowUp));
assert!(is_arrow_key(&Key::ArrowLeft));
assert!(!is_arrow_key(&Key::Enter));
assert!(!is_arrow_key(&Key::Char('a')));
}
#[test]
fn test_key_categorization() {
fn is_navigation_key(key: &Key) -> bool {
matches!(
key,
Key::ArrowUp
| Key::ArrowDown
| Key::ArrowLeft
| Key::ArrowRight
| Key::Home
| Key::End
| Key::PageUp
| Key::PageDown
)
}
fn is_editing_key(key: &Key) -> bool {
matches!(
key,
Key::Backspace | Key::Delete | Key::CtrlU | Key::CtrlK | Key::CtrlD
)
}
assert!(is_navigation_key(&Key::Home));
assert!(is_navigation_key(&Key::PageDown));
assert!(!is_navigation_key(&Key::Tab));
assert!(is_editing_key(&Key::Delete));
assert!(is_editing_key(&Key::CtrlU));
assert!(!is_editing_key(&Key::Enter));
}
#[test]
fn test_key_to_string_representation() {
fn key_name(key: &Key) -> &str {
match key {
Key::ArrowUp => "↑",
Key::ArrowDown => "↓",
Key::ArrowLeft => "←",
Key::ArrowRight => "→",
Key::Home => "Home",
Key::End => "End",
Key::Enter => "Enter",
Key::Backspace => "Backspace",
Key::Tab => "Tab",
Key::ShiftTab => "Shift+Tab",
Key::CtrlU => "Ctrl+U",
Key::CtrlK => "Ctrl+K",
Key::CtrlD => "Ctrl+D",
Key::Delete => "Delete",
Key::PageUp => "PgUp",
Key::PageDown => "PgDn",
Key::Insert => "Insert",
Key::Char(c) => return if c.is_whitespace() { "Space" } else { "Char" },
}
}
assert_eq!(key_name(&Key::ArrowUp), "↑");
assert_eq!(key_name(&Key::CtrlK), "Ctrl+K");
assert_eq!(key_name(&Key::Char(' ')), "Space");
}
}