From 1e0e0b26f71e47a33f21b28344f13ea1d657b990 Mon Sep 17 00:00:00 2001 From: Xyverle Date: Mon, 23 Jun 2025 04:02:45 -0400 Subject: [PATCH] reorganize some things --- src/ansi.rs | 8 +- src/{os.rs => control.rs} | 4 +- src/input.rs | 4 +- src/lib.rs | 4 +- src/unix.rs | 498 ++++++++++++++++++-------------------- src/windows.rs | 417 +++++++++++++++---------------- 6 files changed, 452 insertions(+), 483 deletions(-) rename src/{os.rs => control.rs} (93%) diff --git a/src/ansi.rs b/src/ansi.rs index fa661d1..5ad675e 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -2,10 +2,16 @@ //! //! These should work on *most* terminals (i.e. Xterm compatible terminals) //! -//! For these to work on Windows you need to run the `enable_ansi` function in the os module +//! For these to work on Windows you need to run the `enable_ansi` function inside this module use std::io::{self, Write}; +#[cfg(unix)] +pub use crate::unix::enable_ansi; + +#[cfg(windows)] +pub use crate::windows::enable_ansi; + /// Sets the terminal to an arbitrary 12-bit/truecolor color in the foreground when printed #[must_use] pub fn rgb_color_code_fg(red: u8, green: u8, blue: u8) -> String { diff --git a/src/os.rs b/src/control.rs similarity index 93% rename from src/os.rs rename to src/control.rs index 9c2f623..277fe8b 100644 --- a/src/os.rs +++ b/src/control.rs @@ -5,10 +5,10 @@ use std::io; #[cfg(unix)] -pub use crate::unix::os::*; +pub use crate::unix::{disable_raw_mode, enable_raw_mode, get_terminal_size}; #[cfg(windows)] -pub use crate::windows::os::*; +pub use crate::windows::{disable_raw_mode, enable_raw_mode, get_terminal_size}; /// Struct that calls `enable_raw_mode` on construction /// and `disable_raw_mode` on destruction diff --git a/src/input.rs b/src/input.rs index 226e5ac..a904c8b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -114,7 +114,7 @@ pub enum KeyType { } #[cfg(unix)] -pub use crate::unix::input::*; +pub use crate::unix::poll_input; #[cfg(windows)] -pub use crate::windows::input::*; +pub use crate::windows::poll_input; diff --git a/src/lib.rs b/src/lib.rs index e52626d..403085b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,12 +47,12 @@ mod unix; mod windows; pub mod ansi; +pub mod control; pub mod input; -pub mod os; pub mod prelude { //! Covenience re-export of common members pub use crate::ansi::*; + pub use crate::control::*; pub use crate::input::*; - pub use crate::os::*; } diff --git a/src/unix.rs b/src/unix.rs index 2e97d6f..8f8c080 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -1,5 +1,10 @@ -use std::ffi::{c_int, c_short, c_uint, c_ulong, c_ushort}; +use std::ffi::{c_int, c_uint, c_ulong, c_ushort}; use std::io; +use std::sync::LazyLock; + +use crate::input::{Event, Key, KeyModifiers, KeyType}; +use std::ffi::{c_short, c_void}; +use std::time::Duration; unsafe extern "C" { fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; @@ -55,297 +60,274 @@ fn set_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { Ok(()) } -fn make_raw(termios: &mut Termios) { +static TERMIOS: LazyLock> = LazyLock::new(|| { + let mut orig_termios = Termios::default(); + get_attributes(STDIN_FILENO, &mut orig_termios).ok()?; + Some(orig_termios) +}); + +/// Enables raw mode, which disables line buffering, input echoing, and output canonicalization +/// +/// # Errors +/// +/// If there is no stdin, +/// stdin is not a tty, +/// or it fails to change terminal settings +pub fn enable_raw_mode() -> io::Result<()> { + let mut termios = (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?; unsafe { - cfmakeraw(termios); + cfmakeraw(&mut termios); } - // termios.iflag |= !(ICRNL); + set_attributes(STDIN_FILENO, &mut termios)?; + Ok(()) } -pub mod os { - use super::{STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ}; - use super::{Termios, Winsize}; - use super::{get_attributes, ioctl, make_raw, set_attributes}; - use std::io; - use std::sync::LazyLock; +/// Disables raw mode, which enables line buffering, input echoing, and output canonicalization +/// +/// # Errors +/// +/// If there is no stdin, +/// stdin is not a tty, +/// or it fails to change terminal settings +pub fn disable_raw_mode() -> io::Result<()> { + let mut termios = (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?; + set_attributes(STDIN_FILENO, &mut termios)?; + Ok(()) +} - static TERMIOS: LazyLock> = LazyLock::new(|| { - let mut orig_termios = Termios::default(); - get_attributes(STDIN_FILENO, &mut orig_termios).ok()?; - Some(orig_termios) - }); +/// Enables ANSI support on Windows terminals +/// +/// ANSI is on by default on *nix machines but still exists on them for simpler usage +/// +/// # Errors +/// +/// Never on *nix +/// +/// If There is no stdout, +/// if stdout isn't a TTY, or +/// if it cannot change terminal properties on Windows +#[cfg(unix)] +pub fn enable_ansi() -> io::Result<()> { + // ANSI is on by default on unix platforms + // This is here for compatibility with the windows version of this API + Ok(()) +} - /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization - /// - /// # Errors - /// - /// If there is no stdin, - /// stdin is not a tty, - /// or it fails to change terminal settings - pub fn enable_raw_mode() -> io::Result<()> { - let mut termios = - (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?; - make_raw(&mut termios); - set_attributes(STDIN_FILENO, &mut termios)?; - Ok(()) +/// Gets the size of the terminal +/// +/// Returns in (width, height) format +/// +/// # Errors +/// +/// If there is no stdout, +/// if stdout isn't a TTY, or +/// if it fails to retrieve the terminal size +pub fn get_terminal_size() -> io::Result<(u16, u16)> { + let mut winsize = Winsize::default(); + let ioctl_result = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, (&raw mut winsize).cast::()) }; + + if ioctl_result == 0 { + Ok((winsize.col, winsize.row)) + } else { + Err(io::Error::last_os_error()) } +} - /// Disables raw mode, which enables line buffering, input echoing, and output canonicalization - /// - /// # Errors - /// - /// If there is no stdin, - /// stdin is not a tty, - /// or it fails to change terminal settings - pub fn disable_raw_mode() -> io::Result<()> { - let mut termios = - (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?; - set_attributes(STDIN_FILENO, &mut termios)?; - Ok(()) +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 } } +} - /// Enables ANSI support on Windows terminals - /// - /// ANSI is on by default on *nix machines but still exists on them for simpler usage - /// - /// # Errors - /// - /// Never on *nix - /// - /// If There is no stdout, - /// if stdout isn't a TTY, or - /// if it cannot change terminal properties on Windows - pub fn enable_ansi() -> io::Result<()> { - // ANSI is on by default on unix platforms - // This is here for compatibility with the windows version of this API - Ok(()) - } +impl Iterator for ReadIterator { + type Item = io::Result; - /// Gets the size of the terminal - /// - /// Returns in (width, height) format - /// - /// # Errors - /// - /// If there is no stdout, - /// if stdout isn't a TTY, or - /// if it fails to retrieve the terminal size - pub fn get_terminal_size() -> io::Result<(u16, u16)> { - let mut winsize = Winsize::default(); - let ioctl_result = - unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, (&raw mut winsize).cast::()) }; + fn next(&mut self) -> Option { + let bytes_read = unsafe { read(self.fd, (&raw mut self.buf).cast::(), 1) }; - if ioctl_result == 0 { - Ok((winsize.col, winsize.row)) - } else { - Err(io::Error::last_os_error()) + match bytes_read { + 1.. => Some(Ok(self.buf)), + 0 => None, + _ => Some(Err(io::Error::last_os_error())), } } } -pub mod input { - use super::{POLLIN, STDIN_FILENO}; - use crate::input::{Event, Key, KeyModifiers, KeyType}; - use std::ffi::{c_int, c_short, c_ulong, c_void}; - use std::io; - use std::time::Duration; +/// Attempts to fetch input from stdin +/// +/// # Errors +/// If the timeout has expired or +/// there was an error getting the data +pub fn poll_input(timeout: Duration) -> io::Result { + let mut fds = [PollFD { + fd: STDIN_FILENO, + events: POLLIN, + revents: 0, + }]; + let result = unsafe { + #[allow(clippy::cast_possible_truncation)] + poll( + fds.as_mut_ptr(), + fds.len() as c_ulong, + timeout.as_millis() as c_int, + ) + }; + let mut read_iter = ReadIterator::new(STDIN_FILENO); - 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; + let timed_out: io::Error = io::ErrorKind::TimedOut.into(); + + match result { + 1.. => { + let item = read_iter.next().ok_or(timed_out)??; + try_parse_event(item, &mut read_iter) + } + 0 => Err(timed_out), + _ => Err(io::Error::last_os_error()), } +} - #[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 } +fn try_parse_event(item: u8, iter: &mut I) -> io::Result +where + I: Iterator>, +{ + match item { + b'\x1b' => try_parse_ansi_sequence(iter), + b'\r' => Ok(Event::Key( + Key::Char('\n'), + KeyType::Press, + KeyModifiers::none(), + )), + b'\n' => Ok(Event::Key( + Key::Char('j'), + KeyType::Press, + KeyModifiers::none().ctrl(), + )), + b'\t' => Ok(Event::Key( + Key::Char('\t'), + KeyType::Press, + KeyModifiers::none(), + )), + b'\x7f' => Ok(Event::Key( + Key::Backspace, + KeyType::Press, + KeyModifiers::none(), + )), + b'\0' => Ok(Event::Key(Key::Null, KeyType::Press, KeyModifiers::none())), + c @ b'\x01'..=b'\x1a' => Ok(Event::Key( + Key::Char((c + 96) as char), + KeyType::Press, + KeyModifiers::none().ctrl(), + )), + c @ b'\x1c'..=b'\x1f' => Ok(Event::Key( + Key::Char((c + 24) as char), + KeyType::Press, + KeyModifiers::none().ctrl(), + )), + c => { + let character = parse_utf8_char(c, iter)?; + Ok(Event::Key( + Key::Char(parse_utf8_char(c, iter)?), + KeyType::Press, + KeyModifiers { + shift: character.is_uppercase(), + alt: false, + ctrl: false, + meta: false, + }, + )) } } +} - impl Iterator for ReadIterator { - type Item = io::Result; +fn parse_utf8_char(c: u8, iter: &mut I) -> io::Result +where + I: Iterator>, +{ + let error = || io::Error::new(io::ErrorKind::InvalidData, "Input char is not valid UTF-8"); + let mut bytes = vec![c]; - fn next(&mut self) -> Option { - let bytes_read = unsafe { read(self.fd, (&raw mut self.buf).cast::(), 1) }; - - match bytes_read { - 1.. => Some(Ok(self.buf)), - 0 => None, - _ => Some(Err(io::Error::last_os_error())), - } + for _ in 1..=4 { + if let Ok(string) = std::str::from_utf8(&bytes) { + return Ok(string.chars().next().unwrap()); } + bytes.push(iter.next().ok_or_else(error)??); } + Err(error()) +} - /// Attempts to fetch input from stdin - /// - /// # Errors - /// If the timeout has expired or - /// there was an error getting the data - pub fn poll_input(timeout: Duration) -> io::Result { - let mut fds = [PollFD { - fd: STDIN_FILENO, - events: POLLIN, - revents: 0, - }]; - let result = unsafe { - #[allow(clippy::cast_possible_truncation)] - poll( - fds.as_mut_ptr(), - fds.len() as c_ulong, - timeout.as_millis() as c_int, - ) - }; - let mut read_iter = ReadIterator::new(STDIN_FILENO); - - let timed_out: io::Error = io::ErrorKind::TimedOut.into(); - - match result { - 1.. => { - let item = read_iter.next().ok_or(timed_out)??; - try_parse_event(item, &mut read_iter) - } - 0 => Err(timed_out), - _ => 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'\r' => Ok(Event::Key( - Key::Char('\n'), +fn try_parse_ansi_sequence(iter: &mut I) -> io::Result +where + I: Iterator>, +{ + let error = io::Error::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( + Key::F(1 + val - b'P'), KeyType::Press, KeyModifiers::none(), )), - b'\n' => Ok(Event::Key( - Key::Char('j'), - KeyType::Press, - KeyModifiers::none().ctrl(), - )), - b'\t' => Ok(Event::Key( - Key::Char('\t'), - KeyType::Press, - KeyModifiers::none(), - )), - b'\x7f' => Ok(Event::Key( - Key::Backspace, - KeyType::Press, - KeyModifiers::none(), - )), - b'\0' => Ok(Event::Key(Key::Null, KeyType::Press, KeyModifiers::none())), - c @ b'\x01'..=b'\x1a' => Ok(Event::Key( - Key::Char((c + 96) as char), - KeyType::Press, - KeyModifiers::none().ctrl(), - )), - c @ b'\x1c'..=b'\x1f' => Ok(Event::Key( - Key::Char((c + 24) as char), - KeyType::Press, - KeyModifiers::none().ctrl(), - )), - c => { - let character = parse_utf8_char(c, iter)?; - Ok(Event::Key( - Key::Char(parse_utf8_char(c, iter)?), - KeyType::Press, - KeyModifiers { - shift: character.is_uppercase(), - alt: false, - ctrl: false, - meta: false, - }, - )) - } - } - } - - fn parse_utf8_char(c: u8, iter: &mut I) -> io::Result - where - I: Iterator>, - { - 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()); - } - bytes.push(iter.next().ok_or_else(error)??); - } - Err(error()) - } - - fn try_parse_ansi_sequence(iter: &mut I) -> io::Result - where - I: Iterator>, - { - let error = io::Error::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( - Key::F(1 + val - b'P'), - KeyType::Press, - KeyModifiers::none(), - )), - _ => Err(error), - }, - Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error), _ => Err(error), - } + }, + Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error), + _ => Err(error), } +} - fn try_parse_csi_sequence(iter: &mut I) -> Option - where - I: Iterator>, - { - match iter.next() { - Some(Ok(b'[')) => match iter.next() { - Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key( - Key::F(1 + val - b'A'), - KeyType::Press, - KeyModifiers::none(), - )), - _ => None, - }, - Some(Ok(b'D')) => Some(Event::Key(Key::Left, KeyType::Press, KeyModifiers::none())), - Some(Ok(b'C')) => Some(Event::Key(Key::Right, KeyType::Press, KeyModifiers::none())), - Some(Ok(b'A')) => Some(Event::Key(Key::Up, KeyType::Press, KeyModifiers::none())), - Some(Ok(b'B')) => Some(Event::Key(Key::Down, KeyType::Press, KeyModifiers::none())), - Some(Ok(b'H')) => Some(Event::Key(Key::Home, KeyType::Press, KeyModifiers::none())), - Some(Ok(b'F')) => Some(Event::Key(Key::End, KeyType::Press, KeyModifiers::none())), - Some(Ok(b'Z')) => Some(Event::Key( - Key::Tab, +fn try_parse_csi_sequence(iter: &mut I) -> Option +where + I: Iterator>, +{ + match iter.next() { + Some(Ok(b'[')) => match iter.next() { + Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key( + Key::F(1 + val - b'A'), KeyType::Press, - KeyModifiers::none().shift(), + KeyModifiers::none(), )), _ => None, - } - } - - #[test] - fn test_parse_utf8() { - let string = "abcéŷ¤£€ù%323"; - let ref mut bytes = string.bytes().map(|x| Ok(x)); - let chars = string.chars(); - for c in chars { - let b = bytes.next().unwrap().unwrap(); - let character = parse_utf8_char(b, bytes).unwrap(); - assert!(c == character); - } + }, + Some(Ok(b'D')) => Some(Event::Key(Key::Left, KeyType::Press, KeyModifiers::none())), + Some(Ok(b'C')) => Some(Event::Key(Key::Right, KeyType::Press, KeyModifiers::none())), + Some(Ok(b'A')) => Some(Event::Key(Key::Up, KeyType::Press, KeyModifiers::none())), + Some(Ok(b'B')) => Some(Event::Key(Key::Down, KeyType::Press, KeyModifiers::none())), + Some(Ok(b'H')) => Some(Event::Key(Key::Home, KeyType::Press, KeyModifiers::none())), + Some(Ok(b'F')) => Some(Event::Key(Key::End, KeyType::Press, KeyModifiers::none())), + Some(Ok(b'Z')) => Some(Event::Key( + Key::Tab, + KeyType::Press, + KeyModifiers::none().shift(), + )), + _ => None, + } +} + +#[test] +fn test_parse_utf8() { + let string = "abcéŷ¤£€ù%323"; + let ref mut bytes = string.bytes().map(|x| Ok(x)); + let chars = string.chars(); + for c in chars { + let b = bytes.next().unwrap().unwrap(); + let character = parse_utf8_char(b, bytes).unwrap(); + assert!(c == character); } } diff --git a/src/windows.rs b/src/windows.rs index a8418f8..9d91f1d 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,5 +1,6 @@ -use std::io; +use crate::input::{Event, Key, KeyModifiers, KeyType}; use std::os::windows::raw::HANDLE; +use std::{io, mem, time::Duration}; #[link(name = "kernel32")] unsafe extern "system" { @@ -62,234 +63,214 @@ fn get_console_mode(handle: HANDLE, mode: &mut u32) -> io::Result<()> { } } -pub mod os { - use super::{ - ConsoleScreenBufferInfo, GetConsoleScreenBufferInfo, get_console_mode, get_stdin_handle, - get_stdout_handle, set_console_mode, - }; - use super::{ - ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT, - ENABLE_VIRTUAL_TERMINAL_PROCESSING, - }; - use std::io; +/// Enables raw mode, which disables line buffering, input echoing, and output canonicalization +/// +/// # Errors +/// +/// If there is no stdin, +/// stdin is not a tty, +/// or it fails to change terminal settings +pub fn enable_raw_mode() -> io::Result<()> { + let handle = get_stdin_handle()?; + let mut mode = 0; + get_console_mode(handle, &mut mode)?; + mode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); + set_console_mode(handle, mode)?; + Ok(()) +} - /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization - /// - /// # Errors - /// - /// If there is no stdin, - /// stdin is not a tty, - /// or it fails to change terminal settings - pub fn enable_raw_mode() -> io::Result<()> { - let handle = get_stdin_handle()?; - let mut mode = 0; - get_console_mode(handle, &mut mode)?; - mode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); - set_console_mode(handle, mode)?; - Ok(()) +/// Disables raw mode, which enables line buffering, input echoing, and output canonicalization +/// +/// # Errors +/// +/// If there is no stdin, +/// stdin is not a tty, +/// or it fails to change terminal settings +pub fn disable_raw_mode() -> io::Result<()> { + let handle = get_stdin_handle()?; + let mut mode = 0; + get_console_mode(handle, &mut mode)?; + mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; + set_console_mode(handle, mode)?; + Ok(()) +} + +/// Enables ANSI support on Windows terminals +/// +/// ANSI is on by default on *nix machines but still exists on them for simpler usage +/// +/// # Errors +/// +/// Never on *nix +/// +/// On Windows, if There is no stdout, +/// if stdout isn't a TTY, or +/// if it cannot change terminal properties +pub fn enable_ansi() -> io::Result<()> { + let handle = get_stdout_handle()?; + let mut mode = 0; + get_console_mode(handle, &mut mode)?; + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + set_console_mode(handle, mode)?; + Ok(()) +} + +/// Gets the size of the terminal +/// +/// Returns in (width, height) format +/// +/// # Errors +/// +/// If there is no stdout, +/// if stdout isn't a TTY, or +/// if it fails to retrieve the terminal size +pub fn get_terminal_size() -> io::Result<(u16, u16)> { + let handle = get_stdout_handle()?; + let mut csbi = ConsoleScreenBufferInfo::default(); + if unsafe { GetConsoleScreenBufferInfo(handle, &mut csbi) != 0 } { + let width = csbi.x; + let height = csbi.y; + return Ok((width, height)); + } + Err(io::Error::last_os_error()) +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct InputRecord { + event_type: u16, + event: EventRecord, +} + +#[repr(C)] +#[derive(Copy, Clone)] +union EventRecord { + key: KeyEventRecord, + focus: FocusEventRecord, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct KeyEventRecord { + key_down: i32, + repeat_count: u16, + virtual_key_code: u16, + virtual_scan_code: u16, + u_char: CharUnion, + control_key_state: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct FocusEventRecord { + set_focus: i32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +union CharUnion { + unicode_char: u16, + ascii_char: u8, +} + +unsafe extern "system" { + fn ReadConsoleInputW( + console_input: HANDLE, + buffer: *mut InputRecord, + length: u32, + number_of_events_read: *mut u32, + ) -> i32; + fn WaitForSingleObject(handle: HANDLE, wait_time_ms: u32) -> u32; +} + +/// Attempts to fetch input from stdin +/// +/// # Errors +/// If the timeout has expired or +/// there was an error getting the data +pub fn poll_input(timeout: Duration) -> io::Result { + let handle = get_stdin_handle()?; + let mut record: InputRecord = unsafe { mem::zeroed() }; + let mut read = 0; + + // shut up clippy no reasonable person would expect to be able to have a poll longer than a + // month + #[allow(clippy::cast_possible_truncation)] + let wait_time_millis = timeout.as_millis() as u32; + let result = unsafe { WaitForSingleObject(handle, wait_time_millis) }; + + // The function timed out + if result != 0 { + return Err(io::ErrorKind::TimedOut.into()); } - /// Disables raw mode, which enables line buffering, input echoing, and output canonicalization - /// - /// # Errors - /// - /// If there is no stdin, - /// stdin is not a tty, - /// or it fails to change terminal settings - pub fn disable_raw_mode() -> io::Result<()> { - let handle = get_stdin_handle()?; - let mut mode = 0; - get_console_mode(handle, &mut mode)?; - mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; - set_console_mode(handle, mode)?; - Ok(()) - } + let result = unsafe { ReadConsoleInputW(handle, &mut record, 1, &mut read) }; - /// Enables ANSI support on Windows terminals - /// - /// ANSI is on by default on *nix machines but still exists on them for simpler usage - /// - /// # Errors - /// - /// Never on *nix - /// - /// On Windows, if There is no stdout, - /// if stdout isn't a TTY, or - /// if it cannot change terminal properties - pub fn enable_ansi() -> io::Result<()> { - let handle = get_stdout_handle()?; - let mut mode = 0; - get_console_mode(handle, &mut mode)?; - mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; - set_console_mode(handle, mode)?; - Ok(()) + if result == 0 { + Err(io::Error::last_os_error())?; } - - /// Gets the size of the terminal - /// - /// Returns in (width, height) format - /// - /// # Errors - /// - /// If there is no stdout, - /// if stdout isn't a TTY, or - /// if it fails to retrieve the terminal size - pub fn get_terminal_size() -> io::Result<(u16, u16)> { - let handle = get_stdout_handle()?; - let mut csbi = ConsoleScreenBufferInfo::default(); - if unsafe { GetConsoleScreenBufferInfo(handle, &mut csbi) != 0 } { - let width = csbi.x; - let height = csbi.y; - return Ok((width, height)); + match record.event_type { + 0x10 => { + // Focus Event + Err(io::ErrorKind::InvalidData.into()) + } + 0x1 => { + // Key Event + let key_event: KeyEventRecord = unsafe { record.event.key }; + if key_event.key_down == 0 { + // return Ok(Event::Key(KeyEvent::Null)); + // I don't quite know why but this seems to happen a lot, until I investigate + // more this will have to do + return Err(io::ErrorKind::Other.into()); + } + Ok(parse_key_event(&key_event)) + } + _ => { + //TODO Make this better + Err(io::ErrorKind::InvalidData.into()) } - Err(io::Error::last_os_error()) } } -pub mod input { - use super::get_stdin_handle; - use crate::input::{Event, Key, KeyModifiers, KeyType}; +fn parse_key_event(event: &KeyEventRecord) -> Event { + 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 - use std::os::windows::raw::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: KeyEventRecord, - focus: FocusEventRecord, - } - - #[repr(C)] - #[derive(Copy, Clone)] - struct KeyEventRecord { - key_down: i32, - repeat_count: u16, - virtual_key_code: u16, - virtual_scan_code: u16, - u_char: CharUnion, - control_key_state: u32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - struct FocusEventRecord { - set_focus: i32, - } - - #[repr(C)] - #[derive(Copy, Clone)] - union CharUnion { - unicode_char: u16, - ascii_char: u8, - } - - unsafe extern "system" { - fn ReadConsoleInputW( - console_input: HANDLE, - buffer: *mut InputRecord, - length: u32, - number_of_events_read: *mut u32, - ) -> i32; - fn WaitForSingleObject(handle: HANDLE, wait_time_ms: u32) -> u32; - } - - /// Attempts to fetch input from stdin - /// - /// # Errors - /// If the timeout has expired or - /// there was an error getting the data - pub fn poll_input(timeout: Duration) -> io::Result { - let handle = get_stdin_handle()?; - let mut record: InputRecord = unsafe { mem::zeroed() }; - let mut read = 0; - - // shut up clippy no reasonable person would expect to be able to have a poll longer than a - // month + match event.virtual_key_code { + 0x08 => Event::Key(Key::Backspace, KeyType::Press, KeyModifiers::none()), + 0x09 => { + if shift { + Event::Key(Key::Tab, KeyType::Press, KeyModifiers::none().shift()) + } else { + Event::Key(Key::Tab, KeyType::Press, KeyModifiers::none()) + } + } + 0x0D => Event::Key(Key::Char('\n'), KeyType::Press, KeyModifiers::none()), + 0x1B => Event::Key(Key::Escape, KeyType::Press, KeyModifiers::none()), + 0x21 => Event::Key(Key::PageUp, KeyType::Press, KeyModifiers::none()), + 0x22 => Event::Key(Key::PageDown, KeyType::Press, KeyModifiers::none()), + 0x23 => Event::Key(Key::End, KeyType::Press, KeyModifiers::none()), + 0x24 => Event::Key(Key::Home, KeyType::Press, KeyModifiers::none()), + 0x25 => Event::Key(Key::Left, KeyType::Press, KeyModifiers::none()), + 0x26 => Event::Key(Key::Up, KeyType::Press, KeyModifiers::none()), + 0x27 => Event::Key(Key::Right, KeyType::Press, KeyModifiers::none()), + 0x28 => Event::Key(Key::Down, KeyType::Press, KeyModifiers::none()), + 0x2D => Event::Key(Key::Insert, KeyType::Press, KeyModifiers::none()), + 0x2E => Event::Key(Key::Delete, KeyType::Press, KeyModifiers::none()), + // I don't think anybody is going to try to press F256 clippy #[allow(clippy::cast_possible_truncation)] - let wait_time_millis = timeout.as_millis() as u32; - let result = unsafe { WaitForSingleObject(handle, wait_time_millis) }; - - // The function timed out - if result != 0 { - return Err(io::ErrorKind::TimedOut.into()); - } - - let result = unsafe { ReadConsoleInputW(handle, &mut record, 1, &mut read) }; - - if result == 0 { - Err(io::Error::last_os_error())?; - } - match record.event_type { - 0x10 => { - // Focus Event - Err(io::ErrorKind::InvalidData.into()) - } - 0x1 => { - // Key Event - let key_event: KeyEventRecord = unsafe { record.event.key }; - if key_event.key_down == 0 { - // return Ok(Event::Key(KeyEvent::Null)); - // I don't quite know why but this seems to happen a lot, until I investigate - // more this will have to do - return Err(io::ErrorKind::Other.into()); - } - Ok(parse_key_event(&key_event)) - } - _ => { - //TODO Make this better - Err(io::ErrorKind::InvalidData.into()) - } - } - } - - fn parse_key_event(event: &KeyEventRecord) -> Event { - 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 => Event::Key(Key::Backspace, KeyType::Press, KeyModifiers::none()), - 0x09 => { - if shift { - Event::Key(Key::Tab, KeyType::Press, KeyModifiers::none().shift()) - } else { - Event::Key(Key::Tab, KeyType::Press, KeyModifiers::none()) - } - } - 0x0D => Event::Key(Key::Char('\n'), KeyType::Press, KeyModifiers::none()), - 0x1B => Event::Key(Key::Escape, KeyType::Press, KeyModifiers::none()), - 0x21 => Event::Key(Key::PageUp, KeyType::Press, KeyModifiers::none()), - 0x22 => Event::Key(Key::PageDown, KeyType::Press, KeyModifiers::none()), - 0x23 => Event::Key(Key::End, KeyType::Press, KeyModifiers::none()), - 0x24 => Event::Key(Key::Home, KeyType::Press, KeyModifiers::none()), - 0x25 => Event::Key(Key::Left, KeyType::Press, KeyModifiers::none()), - 0x26 => Event::Key(Key::Up, KeyType::Press, KeyModifiers::none()), - 0x27 => Event::Key(Key::Right, KeyType::Press, KeyModifiers::none()), - 0x28 => Event::Key(Key::Down, KeyType::Press, KeyModifiers::none()), - 0x2D => Event::Key(Key::Insert, KeyType::Press, KeyModifiers::none()), - 0x2E => Event::Key(Key::Delete, KeyType::Press, KeyModifiers::none()), - // I don't think anybody is going to try to press F256 clippy - #[allow(clippy::cast_possible_truncation)] - 0x70..=0x87 => Event::Key( - Key::F((event.virtual_key_code - 0x6F) as u8), - KeyType::Press, - KeyModifiers::none(), - ), // F1-F24 - _ => { - let c = - char::from_u32(u32::from(unsafe { event.u_char.unicode_char })).unwrap_or(' '); - if ctrl && c.is_ascii_alphabetic() { - Event::Key(Key::Char(c), KeyType::Press, KeyModifiers::none().ctrl()) - } else { - Event::Key(Key::Char(c), KeyType::Press, KeyModifiers::none()) - } + 0x70..=0x87 => Event::Key( + Key::F((event.virtual_key_code - 0x6F) as u8), + KeyType::Press, + KeyModifiers::none(), + ), // F1-F24 + _ => { + let num = u32::from(unsafe { event.u_char.unicode_char }); + let c = char::from_u32(num).unwrap_or(' '); + if ctrl && c.is_ascii_alphabetic() { + Event::Key(Key::Char(c), KeyType::Press, KeyModifiers::none().ctrl()) + } else { + Event::Key(Key::Char(c), KeyType::Press, KeyModifiers::none()) } } }