From 1be8ae31123b029f2ed231fdbab9b100c93d153c Mon Sep 17 00:00:00 2001 From: Xyverle Date: Sun, 30 Mar 2025 17:20:44 -0400 Subject: [PATCH] prototype Windows input, needs testing --- src/ansi.rs | 10 --- src/input.rs | 4 +- src/unix.rs | 8 ++- src/unix_input.rs | 151 +++++++++++++++++++++++++++++++------------ src/windows.rs | 10 +-- src/windows_input.rs | 130 +++++++++++++++++++++++++++++++++++++ 6 files changed, 252 insertions(+), 61 deletions(-) diff --git a/src/ansi.rs b/src/ansi.rs index c3ff436..a8493d7 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -80,16 +80,6 @@ pub fn move_cursor_to_position(column: u16, line: u16) -> String { ) } -/// Sends input when terminal is in focus -pub const FOCUS_REPORTING_ENABLE: &str = "\x1b[?1004h"; -/// Stops sending input when terminal is in focus -pub const FOCUS_REPORTING_DISABLE: &str = "\x1b[?1004l"; - -/// Makes all pasted text treated differently -pub const BRACKETED_PASTE_ENABLE: &str = "\x1b[?2004h"; -/// Disables bracketed paste -pub const BRACKETED_PASTE_DISABLE: &str = "\x1b[?2004l"; - /// Saves the current cursor position pub const CURSOR_POSITION_SAVE: &str = "\x1b7"; /// Restores the saved cursor position diff --git a/src/input.rs b/src/input.rs index b6f1f03..0fdb6c6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -18,7 +18,9 @@ pub use windows_input::*; pub enum Event { Key(KeyEvent), - Mouse(MouseEvent) + Mouse(MouseEvent), + FocusGained, + FocusLost, } pub enum KeyEvent { diff --git a/src/unix.rs b/src/unix.rs index 231b6fe..fa799fd 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -1,4 +1,4 @@ -use std::ffi::{c_int, c_uint, c_ulong, c_ushort}; +use std::ffi::{c_int, c_short, c_uint, c_ulong, c_ushort}; use std::io; unsafe extern "C" { @@ -8,8 +8,9 @@ unsafe extern "C" { fn tcsetattr(fd: c_int, optional_actions: c_int, termios: *mut Termios) -> c_int; } -const STDIN_FILENO: c_int = 0x0; -const STDOUT_FILENO: c_int = 0x1; +pub(crate) const STDIN_FILENO: c_int = 0x0; +pub(crate) const STDOUT_FILENO: c_int = 0x1; +pub(crate) const POLLIN: c_short = 0x001; #[cfg(target_os = "linux")] const TIOCGWINSZ: c_ulong = 0x5413; @@ -21,6 +22,7 @@ const TIOCGWINSZ: c_ulong = 0x4008_7468; #[cfg(target_os = "macos")] const NCCS: usize = 0x14; +// #[repr(C)] #[derive(Default, Debug, Clone, Copy)] struct Winsize { diff --git a/src/unix_input.rs b/src/unix_input.rs index 0f61d56..ed6c3d9 100644 --- a/src/unix_input.rs +++ b/src/unix_input.rs @@ -1,14 +1,80 @@ -use crate::input::{Event, KeyEvent, MouseEvent, MouseButton}; +use crate::input::{Event, KeyEvent, MouseButton, MouseEvent}; +use crate::os::{POLLIN, STDIN_FILENO}; +use std::ffi::{c_int, c_short, c_ulong, c_void}; use std::io; +use std::time::Duration; -pub fn try_parse_event(item: u8, iter: &mut I) -> io::Result +unsafe extern "C" { + fn poll(fds: *mut PollFD, nfds: c_ulong, timeout: c_int) -> c_int; + fn read(fd: c_int, buf: *mut c_void, count: c_ulong) -> c_short; +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct PollFD { + fd: c_int, + events: c_short, + revents: c_short, +} + +struct ReadIterator { + fd: c_int, + buf: u8, +} + +impl ReadIterator { + fn new(fd: c_int) -> Self { + Self { fd, buf: 0 } + } +} + +impl Iterator for ReadIterator { + type Item = io::Result; + + fn next(&mut self) -> Option { + let bytes_read = unsafe { read(self.fd, &raw mut self.buf as *mut c_void, 1) }; + + if bytes_read > 0 { + Some(Ok(self.buf)) + } else if bytes_read == 0 { + None + } else { + Some(Err(io::Error::last_os_error())) + } + } +} + +pub fn poll_input(timeout: Duration) -> Option> { + let mut fds = [PollFD { + fd: STDIN_FILENO, + events: POLLIN, + revents: 0, + }]; + let result = unsafe { + poll( + fds.as_mut_ptr(), + fds.len() as c_ulong, + timeout.as_millis() as c_int, + ) + }; + let mut read_iter = ReadIterator::new(STDIN_FILENO); + + if result > 0 { + let item = read_iter.next()?.ok()?; + Some(try_parse_event(item, &mut read_iter)) + } else if result == 0 { + None + } else { + Some(Err(io::Error::last_os_error())) + } +} + +fn try_parse_event(item: u8, iter: &mut I) -> io::Result where I: Iterator>, { match item { - b'\x1b' => { - try_parse_ansi_sequence(iter) - } + b'\x1b' => try_parse_ansi_sequence(iter), b'\n' | b'\r' => Ok(Event::Key(KeyEvent::Char('\n'))), b'\t' => Ok(Event::Key(KeyEvent::Tab)), b'\x7f' => Ok(Event::Key(KeyEvent::Backspace)), @@ -25,10 +91,10 @@ where { let error = || io::Error::new(io::ErrorKind::InvalidData, "Input char is not valid UTF-8"); let mut bytes = vec![c]; - + for _ in 1..=4 { if let Ok(string) = std::str::from_utf8(&bytes) { - return Ok(string.chars().next().unwrap()) + return Ok(string.chars().next().unwrap()); } bytes.push(iter.next().ok_or_else(error)??); } @@ -41,15 +107,11 @@ where { let error = io::Error::new(io::ErrorKind::Other, "Could not parse event"); match iter.next() { - Some(Ok(b'O')) => { - match iter.next() { - Some(Ok(val @ b'P'..=b's')) => Ok(Event::Key(KeyEvent::F(1 + val - b'P'))), - _ => Err(error) - } - } - Some(Ok(b'[')) => { - try_parse_csi_sequence(iter).ok_or(error) - } + Some(Ok(b'O')) => match iter.next() { + Some(Ok(val @ b'P'..=b's')) => Ok(Event::Key(KeyEvent::F(1 + val - b'P'))), + _ => Err(error), + }, + Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error), _ => Err(error), } } @@ -59,13 +121,10 @@ where I: Iterator>, { match iter.next() { - Some(Ok(b'[')) => { - match iter.next() { - Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key(KeyEvent::F(1 + val - b'A'))), - _ => None - - } - } + Some(Ok(b'[')) => match iter.next() { + Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key(KeyEvent::F(1 + val - b'A'))), + _ => None, + }, Some(Ok(b'D')) => Some(Event::Key(KeyEvent::Left)), Some(Ok(b'C')) => Some(Event::Key(KeyEvent::Right)), Some(Ok(b'A')) => Some(Event::Key(KeyEvent::Up)), @@ -73,17 +132,10 @@ where Some(Ok(b'H')) => Some(Event::Key(KeyEvent::Home)), Some(Ok(b'F')) => Some(Event::Key(KeyEvent::End)), Some(Ok(b'Z')) => Some(Event::Key(KeyEvent::BackTab)), - Some(Ok(b'M')) => { - try_parse_x10_mouse(iter) - } - Some(Ok(b'<')) => { - try_parse_xterm_mouse(iter) - } - Some(Ok(c @ b'0'..=b'9')) => { - try_parse_rxvt_mouse(c, iter) - } - _ => None - + Some(Ok(b'M')) => try_parse_x10_mouse(iter), + Some(Ok(b'<')) => try_parse_xterm_mouse(iter), + Some(Ok(c @ b'0'..=b'9')) => try_parse_rxvt_mouse(c, iter), + _ => None, } } @@ -92,35 +144,50 @@ where I: Iterator>, { let cb = iter.next()?.ok()? - 32; - + let cx = u16::from(iter.next()?.ok()?.saturating_sub(33)); let cy = u16::from(iter.next()?.ok()?.saturating_sub(33)); match cb & 0x11 { 0 => { if cb & 0x40 != 0 { - Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, cx, cy))) + Some(Event::Mouse(MouseEvent::Press( + MouseButton::WheelUp, + cx, + cy, + ))) } else { Some(Event::Mouse(MouseEvent::Press(MouseButton::Left, cx, cy))) } } 1 => { if cb & 0x40 != 0 { - Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, cx, cy))) - + Some(Event::Mouse(MouseEvent::Press( + MouseButton::WheelDown, + cx, + cy, + ))) } else { Some(Event::Mouse(MouseEvent::Press(MouseButton::Middle, cx, cy))) } } 2 => { if cb & 0x40 != 0 { - Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelLeft, cx, cy))) + Some(Event::Mouse(MouseEvent::Press( + MouseButton::WheelLeft, + cx, + cy, + ))) } else { Some(Event::Mouse(MouseEvent::Press(MouseButton::Right, cx, cy))) } } 3 => { if cb & 0x40 != 0 { - Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelRight, cx, cy))) + Some(Event::Mouse(MouseEvent::Press( + MouseButton::WheelRight, + cx, + cy, + ))) } else { Some(Event::Mouse(MouseEvent::Release(cx, cy))) } @@ -135,7 +202,7 @@ where { let mut buf = Vec::new(); let mut character = iter.next()?.ok()?; - while !matches!(character, b'm' | b'M' ) { + while !matches!(character, b'm' | b'M') { buf.push(character); character = iter.next()?.ok()?; } @@ -164,7 +231,7 @@ where b'm' => MouseEvent::Release(cx, cy), _ => return None, } - }, + } 32 | 3 => MouseEvent::Hold(cx, cy), _ => return None, }; diff --git a/src/windows.rs b/src/windows.rs index 7eb9cb5..74b11fa 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -10,8 +10,8 @@ unsafe extern "system" { ) -> u32; } -const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; -const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5; +pub(crate) const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; +pub(crate) const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5; const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 4; const ENABLE_ECHO_INPUT: u32 = 4; const ENABLE_LINE_INPUT: u32 = 2; @@ -103,7 +103,7 @@ pub fn get_terminal_size() -> io::Result<(u16, u16)> { Err(io::Error::last_os_error()) } -fn get_std_handle(handle: u32) -> io::Result { +pub(crate) fn get_std_handle(handle: u32) -> io::Result { let handle = unsafe { GetStdHandle(handle) }; if handle == INVALID_HANDLE_VALUE { Err(io::Error::last_os_error()) @@ -112,7 +112,7 @@ fn get_std_handle(handle: u32) -> io::Result { } } -fn set_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { +pub(crate) fn set_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { if unsafe { SetConsoleMode(handle, mode) == 0 } { Err(io::Error::last_os_error()) } else { @@ -120,7 +120,7 @@ fn set_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { } } -fn get_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { +pub(crate) fn get_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { if unsafe { GetConsoleMode(handle, mode) == 0 } { Err(io::Error::last_os_error()) } else { diff --git a/src/windows_input.rs b/src/windows_input.rs index e69de29..7c26c0c 100644 --- a/src/windows_input.rs +++ b/src/windows_input.rs @@ -0,0 +1,130 @@ +use crate::input::{Event, KeyEvent}; +use crate::os::{STD_INPUT_HANDLE, get_std_handle}; + +use std::{io, mem, time::Duration}; + +#[repr(C)] +#[derive(Copy, Clone)] +struct InputRecord { + event_type: u16, + event: EventRecord, +} + +#[repr(C)] +#[derive(Copy, Clone)] +union EventRecord { + key_event: KeyEventRecord, + mouse_event: MouseEventRecord, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct KeyEventRecord { + key_down: i32, + virtual_key_code: u16, + u_char: CharUnion, + control_key_state: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +union CharUnion { + unicode_char: u16, + ascii_char: u8, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct MouseEventRecord { + mouse_position: Coord, + button_state: u32, + control_key_state: u32, + event_flags: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct Coord { + x: i16, + y: i16, +} + +unsafe extern "system" { + fn ReadConsoleInputW( + h_console_input: usize, + lp_buffer: *mut InputRecord, + n_length: u32, + lp_number_of_events_read: *mut u32, + ) -> i32; + fn WaitForMultipleObjects( + n_count: u32, + lp_handles: *mut usize, + b_wait_all: i32, + dw_wait_time: u32, + ) -> u32; +} + +pub fn poll_input(timeout: Duration) -> Option> { + let handle = get_std_handle(STD_INPUT_HANDLE).ok()?; + let mut record: InputRecord = unsafe { mem::zeroed() }; + let mut read = 0; + + let wait_time_millis = timeout.as_millis() as u32; + let mut handles = [handle]; + let result = unsafe { WaitForMultipleObjects(1, handles.as_mut_ptr(), 0, wait_time_millis) }; + + if result != 0 { + return None; + } + + let result = unsafe { ReadConsoleInputW(handle, &mut record, 1, &mut read) }; + + if result == 0 { + return Some(Err(io::Error::last_os_error())); + } + if record.event_type == 1 { + let key_event = unsafe { record.event.key_event }; + if key_event.key_down == 0 { + return Some(Ok(Event::Key(KeyEvent::Null))); + } + return Some(Ok(Event::Key(parse_key_event(&key_event)))); + } + None +} + +fn parse_key_event(event: &KeyEventRecord) -> KeyEvent { + let ctrl = event.control_key_state & (0x0008 | 0x0004) != 0; // LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED + let shift = event.control_key_state & 0x0010 != 0; // SHIFT_PRESSED + + match event.virtual_key_code { + 0x08 => KeyEvent::Backspace, // VK_BACK + 0x09 => { + if shift { + KeyEvent::BackTab + } else { + KeyEvent::Tab + } + } + 0x0D => KeyEvent::Char('\n'), + 0x1B => KeyEvent::Escape, + 0x21 => KeyEvent::PageUp, + 0x22 => KeyEvent::PageDown, + 0x23 => KeyEvent::End, + 0x24 => KeyEvent::Home, + 0x25 => KeyEvent::Left, + 0x26 => KeyEvent::Up, + 0x27 => KeyEvent::Right, + 0x28 => KeyEvent::Down, + 0x2D => KeyEvent::Insert, + 0x2E => KeyEvent::Delete, + 0x70..=0x87 => KeyEvent::F((event.virtual_key_code - 0x6F) as u8), // F1-F24 + _ => { + let c = unsafe { event.u_char.unicode_char } as u8 as char; + if ctrl && c.is_ascii_alphabetic() { + KeyEvent::Ctrl(c) + } else { + KeyEvent::Char(c) + } + } + } +}