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 { 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 { 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 { 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 { 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 { 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 { 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 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!() } } } }