diff --git a/Cargo.toml b/Cargo.toml index 91669e2..e42e76a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neutuino" -version = "0.1.0" +version = "0.2.0" edition = "2024" repository = "https://github.com/Xyverle/neutuino" description = "A minimal zero-dependancy pure-rust cross-platform TUI library" diff --git a/README.md b/README.md index ca4366b..51159c9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Zero dependancy cross-platform pure-rust simple TUI library Supported OSes: Windows 10+, MacOS (untested), Linux -This project is still highly work in progress and it will be a decent while until 1.0 +This project is still highly work in progress and it will be a decent while until feature completeness ## Todo - [x] Output (Unix) @@ -14,4 +14,4 @@ This project is still highly work in progress and it will be a decent while unti - [ ] Events (Focus reporting, Bracketed-paste) (Windows) - [ ] Mouse input (Unix) - [ ] Mouse input (Windows) -- [ ] 1.0 / API cleanup +- [ ] Feature completeness / API cleanup diff --git a/examples/test.rs b/examples/test.rs index 1f8b3da..b20ae81 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -1,10 +1,12 @@ #![warn(clippy::all, clippy::pedantic)] -use std::{time::Duration, io}; -use neutuino::ansi::{COLORS_BG, COLORS_FG, STYLE_BOLD, STYLE_ITALIC, STYLE_RESET, STYLE_UNDERLINE, move_cursor_to_column, set_window_title}; -use neutuino::os::{enable_ansi, get_terminal_size}; -use neutuino::input::{poll_input, Event, KeyEvent}; - +use neutuino::ansi::{ + COLORS_BG, COLORS_FG, STYLE_BOLD, STYLE_ITALIC, STYLE_RESET, STYLE_UNDERLINE, + move_cursor_to_column, set_window_title, +}; +use neutuino::input::{Event, KeyEvent, poll_input}; +use neutuino::os::{disable_raw_mode, enable_ansi, enable_raw_mode, get_terminal_size}; +use std::{io, time::Duration}; fn print_line_style_reset(string: &str) { println!("{}{}{}", string, STYLE_RESET, move_cursor_to_column(0)); @@ -12,8 +14,9 @@ fn print_line_style_reset(string: &str) { fn main() -> io::Result<()> { let all_styles = format!("{STYLE_BOLD}{STYLE_ITALIC}{STYLE_UNDERLINE}"); - let _raw_terminal = neutuino::os::RawTerminal::new()?; + enable_ansi()?; + enable_raw_mode()?; println!("q to quit{}", move_cursor_to_column(0)); let next = |x: usize| (x + 1) % COLORS_FG.len(); @@ -42,5 +45,6 @@ fn main() -> io::Result<()> { } counter = next(counter); } + disable_raw_mode()?; Ok(()) } diff --git a/src/ansi.rs b/src/ansi.rs index 6800479..a66a657 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -209,6 +209,8 @@ pub const COLOR_DEFAULT_FG: &str = "\x1b[39m"; /// Makes characters sent to the screen have a default background pub const COLOR_DEFAULT_BG: &str = "\x1b[49m"; +/// List of all foreground colors in the order: +/// Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default pub const COLORS_FG: [&str; 9] = [ COLOR_BLACK_FG, COLOR_RED_FG, @@ -221,6 +223,8 @@ pub const COLORS_FG: [&str; 9] = [ COLOR_DEFAULT_FG, ]; +/// List of all background colors in the order: +/// Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default pub const COLORS_BG: [&str; 9] = [ COLOR_BLACK_BG, COLOR_RED_BG, @@ -233,6 +237,8 @@ pub const COLORS_BG: [&str; 9] = [ COLOR_DEFAULT_BG, ]; +/// List of all foreground and background colors in the order: +/// Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default pub const COLORS: [(&str, &str); 9] = [ (COLOR_BLACK_FG, COLOR_BLACK_BG), (COLOR_RED_FG, COLOR_RED_BG), diff --git a/src/input.rs b/src/input.rs index a33c833..e69de29 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,52 +0,0 @@ -//! Various input functions, structs, etc. -//! -//! Very incomplete currently - -#[cfg(unix)] -#[path = "unix_input.rs"] -mod unix_input; - -#[cfg(unix)] -pub use unix_input::*; - -#[cfg(windows)] -#[path = "windows_input.rs"] -mod windows_input; - -#[cfg(windows)] -pub use windows_input::*; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum Event { - Key(KeyEvent), - FocusGained, - FocusLost, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum KeyEvent { - Backspace, - Up, - Down, - Left, - Right, - Home, - End, - PageUp, - PageDown, - Tab, - BackTab, - Delete, - Insert, - F(u8), - Char(char), - Ctrl(char), - Escape, - Null, -} - -impl From for Event { - fn from(value: KeyEvent) -> Self { - Self::Key(value) - } -} diff --git a/src/lib.rs b/src/lib.rs index ba5c869..f66aa58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,62 @@ #![warn(clippy::all, clippy::pedantic)] pub mod ansi; -pub mod input; -pub mod os; + +#[cfg(unix)] +mod unix; + +#[cfg(unix)] +pub use crate::unix::*; + +#[cfg(windows)] +mod windows; + +#[cfg(windows)] +pub use crate::windows::*; + +pub mod input { + //! Various input functions, structs, etc. + //! + //! Very incomplete currently + + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] + pub enum Event { + Key(KeyEvent), + FocusGained, + FocusLost, + } + + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] + pub enum KeyEvent { + Backspace, + Up, + Down, + Left, + Right, + Home, + End, + PageUp, + PageDown, + Tab, + BackTab, + Delete, + Insert, + F(u8), + Char(char), + Ctrl(char), + Escape, + Null, + } + + impl From for Event { + fn from(value: KeyEvent) -> Self { + Self::Key(value) + } + } + + #[cfg(unix)] + pub use crate::unix::input::*; + + #[cfg(windows)] + pub use crate::windows::input::*; +} diff --git a/src/os.rs b/src/os.rs index a447010..51a7c01 100644 --- a/src/os.rs +++ b/src/os.rs @@ -3,16 +3,3 @@ //! These are built to work at least on these platforms: //! Windows, Linux, and Mac, but are likely to work on more -#[cfg(unix)] -#[path = "unix.rs"] -mod unix; - -#[cfg(unix)] -pub use unix::*; - -#[cfg(windows)] -#[path = "windows.rs"] -mod windows; - -#[cfg(windows)] -pub use windows::*; diff --git a/src/unix.rs b/src/unix.rs index f8df7d1..3c494aa 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -1,5 +1,4 @@ use std::ffi::{c_int, c_short, c_uint, c_ulong, c_ushort}; -use std::io; unsafe extern "C" { fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; @@ -8,9 +7,9 @@ unsafe extern "C" { fn tcsetattr(fd: c_int, optional_actions: c_int, termios: *mut Termios) -> c_int; } -pub(crate) const STDIN_FILENO: c_int = 0x0; -pub(crate) const STDOUT_FILENO: c_int = 0x1; -pub(crate) const POLLIN: c_short = 0x001; +const STDIN_FILENO: c_int = 0; +const STDOUT_FILENO: c_int = 1; +pub const POLLIN: c_short = 1; #[cfg(target_os = "linux")] const TIOCGWINSZ: c_ulong = 0x5413; @@ -32,7 +31,7 @@ struct Winsize { } #[repr(C)] -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Copy)] struct Termios { iflag: c_uint, oflag: c_uint, @@ -41,89 +40,256 @@ struct Termios { cc: [u8; NCCS], } -/// This struct represents a raw terminal -/// -/// This struct will automatically enable raw mode when it is created -/// and disable raw mode when it is destructed -/// -/// This insures that you never exit with a terminal still in raw mode which is problematic for -/// users -pub struct RawTerminal { - orig_termios: Termios, -} +pub mod os { + use super::{STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ}; + use super::{Termios, Winsize}; + use super::{cfmakeraw, ioctl, tcgetattr, tcsetattr}; + use std::ffi::c_int; + use std::io; + use std::sync::LazyLock; -impl RawTerminal { - /// This constructs a terminal, automatically making it raw + 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, - /// if it fails to change terminal settings - pub fn new() -> io::Result { - let mut orig_termios = Termios::default(); - get_attributes(STDIN_FILENO, &mut orig_termios)?; - let mut termios = orig_termios.clone(); + /// 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"))?; cfmakeraw(&raw mut termios); set_attributes(STDIN_FILENO, &mut termios)?; - Ok(Self { orig_termios }) + 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 mut termios = + (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?; + set_attributes(STDIN_FILENO, &mut termios)?; + 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 + /// + /// 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(()) + } + + /// 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()) + } + } + + fn get_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { + if unsafe { tcgetattr(fd, &raw mut *termios) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + + fn set_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { + if unsafe { tcsetattr(fd, 0, std::ptr::from_mut(termios)) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) } } -impl Drop for RawTerminal { - fn drop(&mut self) { - let mut termios = self.orig_termios.clone(); - set_attributes(STDIN_FILENO, &mut termios).expect("Failed to disable terminal raw mode"); +pub mod input { + use super::{POLLIN, STDIN_FILENO}; + use crate::input::{Event, KeyEvent}; + use std::ffi::{c_int, c_short, c_ulong, c_void}; + use std::io; + use std::time::Duration; + + 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).cast::(), 1) }; + + match bytes_read { + 1.. => Some(Ok(self.buf)), + 0 => None, + _ => Some(Err(io::Error::last_os_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'\n' | b'\r' => Ok(Event::Key(KeyEvent::Char('\n'))), + b'\t' => Ok(Event::Key(KeyEvent::Tab)), + b'\x7f' => Ok(Event::Key(KeyEvent::Backspace)), + b'\0' => Ok(Event::Key(KeyEvent::Null)), + c @ b'\x01'..=b'\x1a' => Ok(Event::Key(KeyEvent::Ctrl((c + 96) as char))), + c @ b'\x1c'..=b'\x1f' => Ok(Event::Key(KeyEvent::Ctrl((c + 24) as char))), + c => Ok(Event::Key(KeyEvent::Char(parse_utf8_char(c, iter)?))), + } + } + + 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(KeyEvent::F(1 + val - b'P'))), + _ => 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(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)), + Some(Ok(b'B')) => Some(Event::Key(KeyEvent::Down)), + 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)), + _ => 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); + } } } - -/// 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(()) -} - -/// 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()) - } -} - -fn get_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { - if unsafe { tcgetattr(fd, &raw mut *termios) } != 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - -fn set_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { - if unsafe { tcsetattr(fd, 0, std::ptr::from_mut(termios)) } != 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} diff --git a/src/unix_input.rs b/src/unix_input.rs deleted file mode 100644 index fe7f182..0000000 --- a/src/unix_input.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::input::{Event, KeyEvent}; -use crate::os::{POLLIN, STDIN_FILENO}; -use std::ffi::{c_int, c_short, c_ulong, c_void}; -use std::io; -use std::time::Duration; - -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).cast::(), 1) }; - - match bytes_read { - 1.. => Some(Ok(self.buf)), - 0 => None, - _ => Some(Err(io::Error::last_os_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'\n' | b'\r' => Ok(Event::Key(KeyEvent::Char('\n'))), - b'\t' => Ok(Event::Key(KeyEvent::Tab)), - b'\x7f' => Ok(Event::Key(KeyEvent::Backspace)), - b'\0' => Ok(Event::Key(KeyEvent::Null)), - c @ b'\x01'..=b'\x1a' => Ok(Event::Key(KeyEvent::Ctrl((c + 96) as char))), - c @ b'\x1c'..=b'\x1f' => Ok(Event::Key(KeyEvent::Ctrl((c + 24) as char))), - c => Ok(Event::Key(KeyEvent::Char(parse_utf8_char(c, iter)?))), - } -} - -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::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), - _ => 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(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)), - Some(Ok(b'B')) => Some(Event::Key(KeyEvent::Down)), - 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)), - _ => 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 74b11fa..caf077c 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -10,8 +10,8 @@ unsafe extern "system" { ) -> u32; } -pub(crate) const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; -pub(crate) const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5; +const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; +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; @@ -26,84 +26,7 @@ struct ConsoleScreenBufferInfo { _unused: [u16; 9], } -/// This struct represents a raw terminal -/// -/// This struct will automatically enable raw mode when it is created -/// and disable raw mode when it is destructed -/// -/// This insures that you never exit with a terminal still in raw mode which is problematic for -/// users -pub struct RawTerminal; - -impl RawTerminal { - /// This constructs a terminal, automatically making it raw - /// - /// # Errors - /// - /// If there is no stdin, - /// stdin is not a tty, - /// if it fails to change terminal settings - pub fn new() -> io::Result { - let handle = get_std_handle(STD_INPUT_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, &mut mode)?; - Ok(Self) - } -} - -impl Drop for RawTerminal { - fn drop(&mut self) { - let handle = get_std_handle(STD_INPUT_HANDLE).expect("Failed to disable terminal raw mode"); - let mut mode = 0; - get_console_mode(handle, &mut mode).expect("Failed to disable terminal raw mode"); - mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; - set_console_mode(handle, &mut mode).expect("Failed to disable terminal raw mode"); - } -} - -/// 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_std_handle(STD_OUTPUT_HANDLE)?; - let mut mode = 0; - get_console_mode(handle, &mut mode)?; - mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; - set_console_mode(handle, &mut 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_std_handle(STD_OUTPUT_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()) -} - -pub(crate) fn get_std_handle(handle: u32) -> io::Result { +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 +35,7 @@ pub(crate) fn get_std_handle(handle: u32) -> io::Result { } } -pub(crate) fn set_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { +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,10 +43,233 @@ pub(crate) fn set_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> } } -pub(crate) fn get_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { +fn get_console_mode(handle: usize, mode: &mut u32) -> io::Result<()> { if unsafe { GetConsoleMode(handle, mode) == 0 } { Err(io::Error::last_os_error()) } else { Ok(()) } } + +pub mod os { + use super::{ConsoleScreenBufferInfo, get_console_mode, get_std_handle, set_console_mode}; + use super::{ + ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT, + ENABLE_VIRTUAL_TERMINAL_PROCESSING, + }; + use super::{GetConsoleScreenBufferInfo, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}; + 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_std_handle(STD_INPUT_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, &mut 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_std_handle(STD_INPUT_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, &mut 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_std_handle(STD_OUTPUT_HANDLE)?; + let mut mode = 0; + get_console_mode(handle, &mut mode)?; + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + set_console_mode(handle, &mut 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_std_handle(STD_OUTPUT_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()) + } +} + +pub mod input { + use super::{STD_INPUT_HANDLE, get_std_handle}; + use crate::input::{Event, KeyEvent}; + + 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: usize, + buffer: *mut InputRecord, + length: u32, + number_of_events_read: *mut u32, + ) -> i32; + fn WaitForSingleObject(handle: usize, 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_std_handle(STD_INPUT_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()); + } + + 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)); + } + Ok(Event::Key(parse_key_event(&key_event))) + } + _ => { + //TODO Make this better + Err(io::ErrorKind::InvalidData.into()) + } + } + } + + 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, + // I don't think anybody is going to try to press F256 clippy + #[allow(clippy::cast_possible_truncation)] + 0x70..=0x87 => KeyEvent::F((event.virtual_key_code - 0x6F) as u8), // F1-F24 + _ => { + let c = + char::from_u32(u32::from(unsafe { event.u_char.unicode_char })).unwrap_or(' '); + if ctrl && c.is_ascii_alphabetic() { + KeyEvent::Ctrl(c) + } else { + KeyEvent::Char(c) + } + } + } + } +} diff --git a/src/windows_input.rs b/src/windows_input.rs deleted file mode 100644 index b02ec4b..0000000 --- a/src/windows_input.rs +++ /dev/null @@ -1,127 +0,0 @@ -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: 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: usize, - buffer: *mut InputRecord, - length: u32, - number_of_events_read: *mut u32, - ) -> i32; - fn WaitForSingleObject( - handle: usize, - wait_time_ms: u32, - ) -> u32; -} - -pub fn poll_input(timeout: Duration) -> io::Result { - let handle = get_std_handle(STD_INPUT_HANDLE)?; - let mut record: InputRecord = unsafe { mem::zeroed() }; - let mut read = 0; - - 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 { - return 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)); - } - Ok(Event::Key(parse_key_event(&key_event))) - }, - _ => { //TODO Make this better - Err(io::ErrorKind::InvalidData.into()) - } - } -} - -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) - } - } - } -}