From 5a74f9308bba068e82b6d83ccc04d2f4376cdf37 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sat, 4 Mar 2023 14:24:15 -0600 Subject: [PATCH] move input handling into a separate input module --- src/input.rs | 161 +++++++++++++++++++++++++ src/main.rs | 334 ++++++++++++++++----------------------------------- 2 files changed, 264 insertions(+), 231 deletions(-) create mode 100644 src/input.rs diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..8cd6c51 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,161 @@ +use anyhow::{Context, Result}; +use crate::{error::Error, log::*}; +use windows::Win32::System::Console; +use windows::Win32::Foundation::HANDLE; + +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 { + // All buffered items have been processed, ask the OS for new event records + if self.buf_idx as u32 >= self.buf_len { + unsafe { + Error::check(Console::ReadConsoleInputW(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); + } +} diff --git a/src/main.rs b/src/main.rs index cc77b8c..c14ba15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod error; mod line; mod log; +mod input; mod prompt; use line::Line; @@ -14,88 +15,6 @@ use anyhow::{Context, Result}; use crate::{error::Error, log::*}; -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 log_output_mode(mode: Console::CONSOLE_MODE) { // Characters written by the WriteFile or WriteConsole function or echoed by the ReadFile or // ReadConsole function are parsed for ASCII control sequences, and the correct action is @@ -183,14 +102,6 @@ fn log_output_mode(mode: Console::CONSOLE_MODE) { } } -fn stdin_handle() -> Result { - unsafe { - let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE) - .context("unable to get stdin handle")?; - Ok(handle) - } -} - pub fn stdout_handle() -> Result { unsafe { let handle = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE) @@ -199,37 +110,6 @@ pub fn stdout_handle() -> Result { } } -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(()) -} - fn setup_stdout() -> Result<()> { unsafe { Console::SetConsoleOutputCP(65001); @@ -259,139 +139,131 @@ fn main() -> Result<()> { } } - setup_stdin()?; setup_stdout()?; - let stdin = stdin_handle()?; let stdout = stdout_handle()?; - let mut buf = [Console::INPUT_RECORD::default(); 100]; let mut line = Line::new(); let prompt = Prompt::new(); + let mut input = input::Reader::new()?; prompt.print()?; loop { - let mut n: u32 = 0; - unsafe { - Error::check(Console::ReadConsoleInputW(stdin, &mut buf, &mut n))?; - } - let n = n as usize; - for rec in &buf[0..n] { - debug!("Event Type: {}", rec.EventType); - match rec.EventType as u32 { - Console::FOCUS_EVENT => { - // The Event member contains a FOCUS_EVENT_RECORD structure. These events are - // used internally and should be ignored. - unsafe { - let event = rec.Event.FocusEvent; - debug!("Focus Event: {:?}", event); - } + let rec = input.next()?; + debug!("Event Type: {}", rec.EventType); + match rec.EventType as u32 { + Console::FOCUS_EVENT => { + // The Event member contains a FOCUS_EVENT_RECORD structure. These events are + // used internally and should be ignored. + unsafe { + let event = rec.Event.FocusEvent; + debug!("Focus Event: {:?}", event); } - Console::MENU_EVENT => { - // The Event member contains a MENU_EVENT_RECORD structure. These events are - // used internally and should be ignored. - unsafe { - let event = rec.Event.MenuEvent; - debug!("Menu Event: {:?}", event); - } + } + Console::MENU_EVENT => { + // The Event member contains a MENU_EVENT_RECORD structure. These events are + // used internally and should be ignored. + unsafe { + let event = rec.Event.MenuEvent; + debug!("Menu Event: {:?}", event); } - Console::KEY_EVENT => { - // The Event member contains a KEY_EVENT_RECORD structure with information - // about a keyboard event. - unsafe { - let event = rec.Event.KeyEvent; - let down = event.bKeyDown.as_bool(); - let repeats = event.wRepeatCount; - let key_code = event.wVirtualKeyCode; - let scan_code = event.wVirtualScanCode; - let c = event.uChar.UnicodeChar; - let modifiers = event.dwControlKeyState; - // handle key presses on key up, not key down. This is a little weird but - // it's sending both down and up events when you hit the key anyway. I - // dunno if this is right. But for some reason, the down events don't have - // the modifiers - if !down { - // Carriage Return (Enter key) - if c == 13 { - let mask = Console::LEFT_ALT_PRESSED - | Console::RIGHT_ALT_PRESSED - | Console::LEFT_CTRL_PRESSED - | Console::RIGHT_CTRL_PRESSED - | Console::SHIFT_PRESSED; - if modifiers & mask == 0 { - Error::check(Console::WriteConsoleA( - stdout, - "\r\n".as_bytes(), - None, - None, - ))?; - line.print()?; - Error::check(Console::WriteConsoleA( - stdout, - "\r\n".as_bytes(), - None, - None, - ))?; - prompt.print()?; - line.clear(); - } - } else if key_code == 68 - && (modifiers & Console::LEFT_CTRL_PRESSED - == Console::LEFT_CTRL_PRESSED) - { - // this is CTRL+D - CloseHandle(stdout); - return Ok(()); - } else if key_code == 74 - && (modifiers & Console::LEFT_CTRL_PRESSED - == Console::LEFT_CTRL_PRESSED) - { - // red bullet - let text = "\x1b[31m\u{2022}\x1b[0m"; + } + Console::KEY_EVENT => { + // The Event member contains a KEY_EVENT_RECORD structure with information + // about a keyboard event. + unsafe { + let event = rec.Event.KeyEvent; + let down = event.bKeyDown.as_bool(); + let repeats = event.wRepeatCount; + let key_code = event.wVirtualKeyCode; + let scan_code = event.wVirtualScanCode; + let c = event.uChar.UnicodeChar; + let modifiers = event.dwControlKeyState; + // handle key presses on key up, not key down. This is a little weird but + // it's sending both down and up events when you hit the key anyway. I + // dunno if this is right. But for some reason, the down events don't have + // the modifiers + if !down { + // Carriage Return (Enter key) + if c == 13 { + let mask = Console::LEFT_ALT_PRESSED + | Console::RIGHT_ALT_PRESSED + | Console::LEFT_CTRL_PRESSED + | Console::RIGHT_CTRL_PRESSED + | Console::SHIFT_PRESSED; + if modifiers & mask == 0 { + Error::check(Console::WriteConsoleA( + stdout, + "\r\n".as_bytes(), + None, + None, + ))?; + line.print()?; Error::check(Console::WriteConsoleA( stdout, - text.as_bytes(), + "\r\n".as_bytes(), None, None, ))?; - } else { - if let Some(c) = char::from_u32(event.uChar.UnicodeChar as u32) { - if !c.is_control() { - let mut buf = [0 as u8; 8]; - let s = c.encode_utf8(&mut buf); - s.chars().for_each(|c| line.append(c)); - Error::check(Console::WriteConsoleW( - stdout, - s.as_bytes(), - None, - None, - ))?; - } + prompt.print()?; + line.clear(); + } + } else if key_code == 68 + && (modifiers & Console::LEFT_CTRL_PRESSED + == Console::LEFT_CTRL_PRESSED) + { + // this is CTRL+D + CloseHandle(stdout); + return Ok(()); + } else if key_code == 74 + && (modifiers & Console::LEFT_CTRL_PRESSED + == Console::LEFT_CTRL_PRESSED) + { + // red bullet + let text = "\x1b[31m\u{2022}\x1b[0m"; + Error::check(Console::WriteConsoleA( + stdout, + text.as_bytes(), + None, + None, + ))?; + } else { + if let Some(c) = char::from_u32(event.uChar.UnicodeChar as u32) { + if !c.is_control() { + let mut buf = [0 as u8; 8]; + let s = c.encode_utf8(&mut buf); + s.chars().for_each(|c| line.append(c)); + Error::check(Console::WriteConsoleW( + stdout, + s.as_bytes(), + None, + None, + ))?; } } } - debug!( - "Key Event: down: {down:?} repeats: {repeats} key-code: {key_code} scan-code: {scan_code} char: {c} modifiers: {modifiers}" - ); } + debug!( + "Key Event: down: {down:?} repeats: {repeats} key-code: {key_code} scan-code: {scan_code} char: {c} modifiers: {modifiers}" + ); } - Console::MOUSE_EVENT => { - // The Event member contains a MOUSE_EVENT_RECORD structure with information - // about a mouse movement or button press event. - unsafe { - let event = rec.Event.MouseEvent; - debug!("Mouse Event: {:?}", event); - } + } + Console::MOUSE_EVENT => { + // The Event member contains a MOUSE_EVENT_RECORD structure with information + // about a mouse movement or button press event. + unsafe { + let event = rec.Event.MouseEvent; + debug!("Mouse Event: {:?}", event); } - Console::WINDOW_BUFFER_SIZE_EVENT => { - // The Event member contains a WINDOW_BUFFER_SIZE_RECORD structure with - // information about the new size of the console screen buffer. - unsafe { - let event = rec.Event.WindowBufferSizeEvent; - debug!("Window Buffer Event: {:?}", event); - } + } + Console::WINDOW_BUFFER_SIZE_EVENT => { + // The Event member contains a WINDOW_BUFFER_SIZE_RECORD structure with + // information about the new size of the console screen buffer. + unsafe { + let event = rec.Event.WindowBufferSizeEvent; + debug!("Window Buffer Event: {:?}", event); } - _ => {} } + _ => {} } } }