|
|
|
use crate::{error::Error, key, log::*};
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
use windows::Win32::Foundation::HANDLE;
|
|
|
|
use windows::Win32::System::Console;
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn setup_stdin() -> 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 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; 100],
|
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Reader {
|
|
|
|
pub fn new() -> Result<Self> {
|
|
|
|
setup_stdin()?;
|
|
|
|
Ok(Self {
|
|
|
|
buf: [Console::INPUT_RECORD::default(); 100],
|
|
|
|
buf_len: 0,
|
|
|
|
buf_idx: 0,
|
|
|
|
input: stdin_handle()?,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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()?);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(rec.into())
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
))?;
|
|
|
|
}
|
|
|
|
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> {
|
|
|
|
self.take_bracket()?;
|
|
|
|
match self.next_escape_char()? {
|
|
|
|
'D' => Ok(Event::Left),
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
|
|
|
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: event.wVirtualKeyCode,
|
|
|
|
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!()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|