You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

421 lines
16 KiB
Rust

use crate::{error::Error, key, log::*};
use anyhow::{Context, Result};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::System::Console;
use macros::escapes;
#[allow(dead_code)]
fn log_input_mode(mode: Console::CONSOLE_MODE) {
// Characters read by the ReadFile or ReadConsole function are written to the active screen
// buffer as they are typed into the console. This mode can be used only if the
// ENABLE_LINE_INPUT mode is also enabled.
if (mode & Console::ENABLE_ECHO_INPUT).0 > 0 {
debug!("Echo Input: Enabled");
} else {
debug!("Echo Input: Disabled");
}
// When enabled, text entered in a console window will be inserted at the current cursor
// location and all text following that location will not be overwritten. When disabled, all
// following text will be overwritten.
if (mode & Console::ENABLE_INSERT_MODE).0 > 0 {
debug!("Insert Mode: Enabled");
} else {
debug!("Insert Mode: Disabled");
}
// The ReadFile or ReadConsole function returns only when a carriage return character is read.
// If this mode is disabled, the functions return when one or more characters are available.
if (mode & Console::ENABLE_LINE_INPUT).0 > 0 {
debug!("Line Input Mode: Enabled");
} else {
debug!("Line Input Mode: Disabled");
}
// If the mouse pointer is within the borders of the console window and the window has the
// keyboard focus, mouse events generated by mouse movement and button presses are placed in
// the input buffer. These events are discarded by ReadFile or ReadConsole, even when this mode
// is enabled. The ReadConsoleInput function can be used to read MOUSE_EVENT input records from
// the input buffer.
if (mode & Console::ENABLE_MOUSE_INPUT).0 > 0 {
debug!("Mouse Input: Enabled");
} else {
debug!("Mouse Input: Disabled");
}
// CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer
// is being read by ReadFile or ReadConsole, other control keys are processed by the system and
// are not returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is
// also enabled, backspace, carriage return, and line feed characters are handled by the
// system.
if (mode & Console::ENABLE_PROCESSED_INPUT).0 > 0 {
debug!("Processed Input: Enabled");
} else {
debug!("Processed Input: Disabled");
}
// This flag enables the user to use the mouse to select and edit text. To enable this mode,
// use ENABLE_QUICK_EDIT_MODE | ENABLE_EXTENDED_FLAGS. To disable this mode, use
// ENABLE_EXTENDED_FLAGS without this flag.
if (mode & Console::ENABLE_QUICK_EDIT_MODE).0 > 0 {
debug!("Quick Edit Mode: Enabled");
} else {
debug!("Quick Edit Mode: Disabled");
}
// User interactions that change the size of the console screen buffer are reported in the
// console's input buffer. Information about these events can be read from the input buffer by
// applications using the ReadConsoleInput function, but not by those using ReadFile or
// ReadConsole.
if (mode & Console::ENABLE_WINDOW_INPUT).0 > 0 {
debug!("Window Input: Enabled");
} else {
debug!("Window Input: Disabled");
}
// Setting this flag directs the Virtual Terminal processing engine to convert user input
// received by the console window into Console Virtual Terminal Sequences that can be retrieved
// by a supporting application through ReadFile or ReadConsole functions.
//
// The typical usage of this flag is intended in conjunction with
// ENABLE_VIRTUAL_TERMINAL_PROCESSING on the output handle to connect to an application that
// communicates exclusively via virtual terminal sequences.
if (mode & Console::ENABLE_VIRTUAL_TERMINAL_INPUT).0 > 0 {
debug!("Virtual Terminal Input: Enabled");
} else {
debug!("Virtual Terminal Input: Disabled");
}
}
fn stdin_handle() -> Result<HANDLE> {
unsafe {
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE)
.context("unable to get stdin handle")?;
Ok(handle)
}
}
pub struct Reader {
input: HANDLE,
// this area in memory is where the Windows API will write events read off of the keyboard.
buf: [Console::INPUT_RECORD; 32],
// the number of valid input record items in the buf since last read
buf_len: u32,
// the position of our current indexer into the input record buffer
buf_idx: usize,
ctrl: bool,
}
impl Reader {
pub fn new() -> Result<Self> {
let v = Self {
buf: [Console::INPUT_RECORD::default(); 32],
buf_len: 0,
buf_idx: 0,
input: stdin_handle()?,
ctrl: false,
};
v.reset()?;
Ok(v)
}
pub fn reset(&self) -> Result<()> {
let mut mode = Console::CONSOLE_MODE(0);
unsafe {
Console::SetConsoleCP(65001);
let handle = stdin_handle()?;
Error::check(Console::GetConsoleMode(handle, &mut mode))?;
// allow terminal input characters
mode |= Console::ENABLE_VIRTUAL_TERMINAL_INPUT;
// disable automatic processing of CTRL+C, we'll handle it ourselves
mode &= !Console::ENABLE_PROCESSED_INPUT;
// disable line mode to get every input as its pressed
mode &= !Console::ENABLE_LINE_INPUT;
// disable automatic echoing of inputs
mode &= !Console::ENABLE_ECHO_INPUT;
// enable mouse input
mode |= Console::ENABLE_MOUSE_INPUT;
Error::check(Console::SetConsoleMode(handle, mode))?;
Error::check(Console::GetConsoleMode(handle, &mut mode))?;
// debug!("Stdin details:");
// log_input_mode(mode);
}
Ok(())
}
pub fn next(&mut self) -> Result<Event> {
let rec = self.next_rec()?;
if rec.EventType as u32 == Console::KEY_EVENT {
unsafe {
let event = rec.Event.KeyEvent;
if event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 27 {
return Ok(self.next_escape_sequence()?);
}
if event.wVirtualKeyCode == 17 {
self.ctrl = event.bKeyDown.as_bool();
debug!("ctrl {}", event.bKeyDown.as_bool());
}
}
}
Ok(rec.into())
}
fn log_recs(&mut self) {
debug!(" +---------------------+");
for i in 0..self.buf_len {
let rec = self.buf[i as usize];
let e: Event = rec.into();
match e {
Event::Key(k) => debug!(" | {} |", k),
_ => debug!(" | {:<14?} |", &e),
}
}
debug!(" +---------------------+");
}
fn next_rec(&mut self) -> Result<Console::INPUT_RECORD> {
if self.buf_idx as u32 >= self.buf_len {
unsafe {
Error::check(Console::ReadConsoleInputA(
self.input,
&mut self.buf,
&mut self.buf_len,
))?;
}
debug!("• {}", self.buf_len);
self.log_recs();
self.buf_idx = 0;
}
let rec = self.buf[self.buf_idx];
self.buf_idx += 1;
return Ok(rec);
}
fn next_escape_sequence(&mut self) -> Result<Event> {
match self.next_escape_char()? {
'[' => match self.next_escape_char()? {
'A' => Ok(Event::Up),
'B' => Ok(Event::Down),
'C' => Ok(Event::Right),
'D' => Ok(Event::Left),
'H' => Ok(Event::Home),
'F' => Ok(Event::End),
'1' => match self.next_escape_char()? {
'3' => match self.next_escape_char()? {
';' => match self.next_escape_char()? {
'5' => match self.next_escape_char()? {
'u' => Ok(Event::Drop(String::from("13;5u"))),
e => Err(Error::input_error(format!("[13;5 unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[13; unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[13 unexpected escape char: {}", e)).into()),
}
'5' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[15~ - F5"))),
e => Err(Error::input_error(format!("[15 unexpected escape char: {}", e)).into()),
}
'7' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[17~ - F6"))),
e => Err(Error::input_error(format!("[17 unexpected escape char: {}", e)).into()),
}
'8' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[18~ - F7"))),
e => Err(Error::input_error(format!("[18 unexpected escape char: {}", e)).into()),
}
'9' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[19~ - F8"))),
e => Err(Error::input_error(format!("[19 unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[1 unexpected escape char: {}", e)).into()),
},
'2' => match self.next_escape_char()? {
'0' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[20~ - F9"))),
e => Err(Error::input_error(format!("[20 unexpected escape char: {}", e)).into()),
}
'1' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[21~ - F10"))),
e => Err(Error::input_error(format!("[20 unexpected escape char: {}", e)).into()),
}
'3' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[23~ - F11"))),
e => Err(Error::input_error(format!("[23 unexpected escape char: {}", e)).into()),
}
'4' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[24~ - F12"))),
e => Err(Error::input_error(format!("[24 unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[2 unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[ unexpected escape char: {}", e)).into()),
},
'O' => match self.next_escape_char()? {
'P' => Ok(Event::Drop(String::from("OP - F1"))),
'Q' => Ok(Event::Drop(String::from("OQ - F2"))),
'R' => Ok(Event::Drop(String::from("OR - F3"))),
'S' => Ok(Event::Drop(String::from("OS - F4"))),
e => Err(Error::input_error(format!("O unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("unexpected escape char: {}", e)).into()),
}
}
// fn take_bracket(&mut self) -> Result<()> {
// let rec = self.next_rec()?;
// if rec.EventType as u32 != Console::KEY_EVENT {
// Err(Error::input_error("failed to read escape sequence: not a key event").into())
// } else {
// unsafe {
// let event = rec.Event.KeyEvent;
// if event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 91 {
// Ok(())
// } else {
// Err(Error::input_error("failed to read escape sequence: not a [").into())
// }
// }
// }
// }
fn next_escape_char(&mut self) -> Result<char> {
let rec = self.next_rec()?;
if rec.EventType as u32 != Console::KEY_EVENT {
Err(
Error::input_error("failed to read char in escape sequence: not a key event")
.into(),
)
} else {
unsafe {
let n = rec.Event.KeyEvent.uChar.UnicodeChar as u32;
let c = char::from_u32(n);
c.ok_or_else(|| {
let msg = format!("escape key value is not a valid unicode character: {}", n);
Error::input_error(msg).into()
})
}
}
}
}
#[derive(Debug)]
pub enum Event {
Focus(bool),
Menu(u32),
Key(key::Event),
Mouse { x: i16, y: i16 },
Size,
Left,
Right,
Up,
Down,
Home,
End,
Drop(String),
}
const ALT_KEYS: u32 = 0x0002 | 0x0001;
const CTRL_KEYS: u32 = 0x0008 | 0x0004;
const SHIFT_PRESSED: u32 = 0x0010;
impl From<Console::INPUT_RECORD> for Event {
fn from(rec: Console::INPUT_RECORD) -> Self {
// This is documented here:
// https://learn.microsoft.com/en-us/windows/console/input-record-str
match rec.EventType as u32 {
Console::FOCUS_EVENT => unsafe {
let event = rec.Event.FocusEvent;
Event::Focus(event.bSetFocus.as_bool())
},
Console::MENU_EVENT => unsafe {
let event = rec.Event.MenuEvent;
Event::Menu(event.dwCommandId)
},
Console::KEY_EVENT => {
// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
unsafe {
let event = rec.Event.KeyEvent;
let mstate = event.dwControlKeyState;
let c = char::from_u32(event.uChar.UnicodeChar as u32).unwrap_or('💀');
Event::Key(key::Event {
down: event.bKeyDown.as_bool(),
repeats: event.wRepeatCount,
code: key::CODES[event.wVirtualKeyCode as usize],
alt: mstate & ALT_KEYS > 0,
ctrl: mstate & CTRL_KEYS > 0,
shift: mstate & SHIFT_PRESSED > 0,
char: c,
})
}
}
Console::MOUSE_EVENT => {
// OK I think it's safe to ignore these events since we're using the terminal
// escape sequences. I think we never see them because terminal.exe is going to
// turn mouse events into virtual terminal sequences anyway
unsafe {
let event = rec.Event.MouseEvent;
// pub dwMousePosition: COORD, i16 i16
// pub dwButtonState: u32,
// pub dwControlKeyState: u32,
// pub dwEventFlags: u32,
Event::Mouse {
x: event.dwMousePosition.X,
y: event.dwMousePosition.Y,
}
}
}
Console::WINDOW_BUFFER_SIZE_EVENT => Event::Size,
_ => {
unreachable!()
}
}
}
}
escapes! {
"[A" Up
"[B" Down
"[C" Right
"[D" Left
"[H" Home
"[F" End
"[3~" Delete
"[5~" PageUp
"[6~" PageDown
"[13;2u" Shift_Enter
"[13;5u" Ctrl_Enter
"[13;6u" Ctrl_Shift_Enter
"[1;2P" Shift_F1
"[1;2Q" Shift_F2
"[1;5P" Ctrl_F1
"[1;6P" Ctrl_Shift_F1
"[1;3P" Alt_F1
"[1;4P" Shift_Alt_F1
"OP" F1
"OQ" F2
"OR" F3
"OS" F4
"[15~" F5
"[15;2~" Shift_F5
"[17~" F6
"[18~" F7
"[19~" F8
"[24~" F12
"[53;5u" Ctrl_Shift_5
}
/*
*/