reorganize some things

This commit is contained in:
2025-06-23 04:02:45 -04:00
parent 160414819e
commit 1e0e0b26f7
6 changed files with 452 additions and 483 deletions

View File

@@ -2,10 +2,16 @@
//! //!
//! These should work on *most* terminals (i.e. Xterm compatible terminals) //! 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}; 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 /// Sets the terminal to an arbitrary 12-bit/truecolor color in the foreground when printed
#[must_use] #[must_use]
pub fn rgb_color_code_fg(red: u8, green: u8, blue: u8) -> String { pub fn rgb_color_code_fg(red: u8, green: u8, blue: u8) -> String {

View File

@@ -5,10 +5,10 @@
use std::io; use std::io;
#[cfg(unix)] #[cfg(unix)]
pub use crate::unix::os::*; pub use crate::unix::{disable_raw_mode, enable_raw_mode, get_terminal_size};
#[cfg(windows)] #[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 /// Struct that calls `enable_raw_mode` on construction
/// and `disable_raw_mode` on destruction /// and `disable_raw_mode` on destruction

View File

@@ -114,7 +114,7 @@ pub enum KeyType {
} }
#[cfg(unix)] #[cfg(unix)]
pub use crate::unix::input::*; pub use crate::unix::poll_input;
#[cfg(windows)] #[cfg(windows)]
pub use crate::windows::input::*; pub use crate::windows::poll_input;

View File

@@ -47,12 +47,12 @@ mod unix;
mod windows; mod windows;
pub mod ansi; pub mod ansi;
pub mod control;
pub mod input; pub mod input;
pub mod os;
pub mod prelude { pub mod prelude {
//! Covenience re-export of common members //! Covenience re-export of common members
pub use crate::ansi::*; pub use crate::ansi::*;
pub use crate::control::*;
pub use crate::input::*; pub use crate::input::*;
pub use crate::os::*;
} }

View File

@@ -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::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" { unsafe extern "C" {
fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int;
@@ -55,126 +60,104 @@ fn set_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> {
Ok(()) Ok(())
} }
fn make_raw(termios: &mut Termios) { static TERMIOS: LazyLock<Option<Termios>> = LazyLock::new(|| {
unsafe {
cfmakeraw(termios);
}
// termios.iflag |= !(ICRNL);
}
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;
static TERMIOS: LazyLock<Option<Termios>> = LazyLock::new(|| {
let mut orig_termios = Termios::default(); let mut orig_termios = Termios::default();
get_attributes(STDIN_FILENO, &mut orig_termios).ok()?; get_attributes(STDIN_FILENO, &mut orig_termios).ok()?;
Some(orig_termios) Some(orig_termios)
}); });
/// Enables raw mode, which disables line buffering, input echoing, and output canonicalization /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization
/// ///
/// # Errors /// # Errors
/// ///
/// If there is no stdin, /// If there is no stdin,
/// stdin is not a tty, /// stdin is not a tty,
/// or it fails to change terminal settings /// or it fails to change terminal settings
pub fn enable_raw_mode() -> io::Result<()> { pub fn enable_raw_mode() -> io::Result<()> {
let mut termios = let mut termios = (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?;
(*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?; unsafe {
make_raw(&mut termios); cfmakeraw(&mut termios);
}
set_attributes(STDIN_FILENO, &mut termios)?; set_attributes(STDIN_FILENO, &mut termios)?;
Ok(()) Ok(())
} }
/// Disables raw mode, which enables line buffering, input echoing, and output canonicalization /// Disables raw mode, which enables line buffering, input echoing, and output canonicalization
/// ///
/// # Errors /// # Errors
/// ///
/// If there is no stdin, /// If there is no stdin,
/// stdin is not a tty, /// stdin is not a tty,
/// or it fails to change terminal settings /// or it fails to change terminal settings
pub fn disable_raw_mode() -> io::Result<()> { pub fn disable_raw_mode() -> io::Result<()> {
let mut termios = let mut termios = (*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?;
(*TERMIOS).ok_or(io::Error::other("Failed to get terminal properties"))?;
set_attributes(STDIN_FILENO, &mut termios)?; set_attributes(STDIN_FILENO, &mut termios)?;
Ok(()) Ok(())
} }
/// Enables ANSI support on Windows terminals /// Enables ANSI support on Windows terminals
/// ///
/// ANSI is on by default on *nix machines but still exists on them for simpler usage /// ANSI is on by default on *nix machines but still exists on them for simpler usage
/// ///
/// # Errors /// # Errors
/// ///
/// Never on *nix /// Never on *nix
/// ///
/// If There is no stdout, /// If There is no stdout,
/// if stdout isn't a TTY, or /// if stdout isn't a TTY, or
/// if it cannot change terminal properties on Windows /// if it cannot change terminal properties on Windows
pub fn enable_ansi() -> io::Result<()> { #[cfg(unix)]
pub fn enable_ansi() -> io::Result<()> {
// ANSI is on by default on unix platforms // ANSI is on by default on unix platforms
// This is here for compatibility with the windows version of this API // This is here for compatibility with the windows version of this API
Ok(()) Ok(())
} }
/// Gets the size of the terminal /// Gets the size of the terminal
/// ///
/// Returns in (width, height) format /// Returns in (width, height) format
/// ///
/// # Errors /// # Errors
/// ///
/// If there is no stdout, /// If there is no stdout,
/// if stdout isn't a TTY, or /// if stdout isn't a TTY, or
/// if it fails to retrieve the terminal size /// if it fails to retrieve the terminal size
pub fn get_terminal_size() -> io::Result<(u16, u16)> { pub fn get_terminal_size() -> io::Result<(u16, u16)> {
let mut winsize = Winsize::default(); let mut winsize = Winsize::default();
let ioctl_result = let ioctl_result = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, (&raw mut winsize).cast::<u8>()) };
unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, (&raw mut winsize).cast::<u8>()) };
if ioctl_result == 0 { if ioctl_result == 0 {
Ok((winsize.col, winsize.row)) Ok((winsize.col, winsize.row))
} else { } else {
Err(io::Error::last_os_error()) Err(io::Error::last_os_error())
} }
}
} }
pub mod input { unsafe extern "C" {
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;
unsafe extern "C" {
fn poll(fds: *mut PollFD, nfds: c_ulong, timeout: c_int) -> c_int; 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; fn read(fd: c_int, buf: *mut c_void, count: c_ulong) -> c_short;
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct PollFD { struct PollFD {
fd: c_int, fd: c_int,
events: c_short, events: c_short,
revents: c_short, revents: c_short,
} }
struct ReadIterator { struct ReadIterator {
fd: c_int, fd: c_int,
buf: u8, buf: u8,
} }
impl ReadIterator { impl ReadIterator {
fn new(fd: c_int) -> Self { fn new(fd: c_int) -> Self {
Self { fd, buf: 0 } Self { fd, buf: 0 }
} }
} }
impl Iterator for ReadIterator { impl Iterator for ReadIterator {
type Item = io::Result<u8>; type Item = io::Result<u8>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
@@ -186,14 +169,14 @@ pub mod input {
_ => Some(Err(io::Error::last_os_error())), _ => Some(Err(io::Error::last_os_error())),
} }
} }
} }
/// Attempts to fetch input from stdin /// Attempts to fetch input from stdin
/// ///
/// # Errors /// # Errors
/// If the timeout has expired or /// If the timeout has expired or
/// there was an error getting the data /// there was an error getting the data
pub fn poll_input(timeout: Duration) -> io::Result<Event> { pub fn poll_input(timeout: Duration) -> io::Result<Event> {
let mut fds = [PollFD { let mut fds = [PollFD {
fd: STDIN_FILENO, fd: STDIN_FILENO,
events: POLLIN, events: POLLIN,
@@ -219,12 +202,12 @@ pub mod input {
0 => Err(timed_out), 0 => Err(timed_out),
_ => Err(io::Error::last_os_error()), _ => Err(io::Error::last_os_error()),
} }
} }
fn try_parse_event<I>(item: u8, iter: &mut I) -> io::Result<Event> fn try_parse_event<I>(item: u8, iter: &mut I) -> io::Result<Event>
where where
I: Iterator<Item = io::Result<u8>>, I: Iterator<Item = io::Result<u8>>,
{ {
match item { match item {
b'\x1b' => try_parse_ansi_sequence(iter), b'\x1b' => try_parse_ansi_sequence(iter),
b'\r' => Ok(Event::Key( b'\r' => Ok(Event::Key(
@@ -272,12 +255,12 @@ pub mod input {
)) ))
} }
} }
} }
fn parse_utf8_char<I>(c: u8, iter: &mut I) -> io::Result<char> fn parse_utf8_char<I>(c: u8, iter: &mut I) -> io::Result<char>
where where
I: Iterator<Item = io::Result<u8>>, I: Iterator<Item = io::Result<u8>>,
{ {
let error = || io::Error::new(io::ErrorKind::InvalidData, "Input char is not valid UTF-8"); let error = || io::Error::new(io::ErrorKind::InvalidData, "Input char is not valid UTF-8");
let mut bytes = vec![c]; let mut bytes = vec![c];
@@ -288,12 +271,12 @@ pub mod input {
bytes.push(iter.next().ok_or_else(error)??); bytes.push(iter.next().ok_or_else(error)??);
} }
Err(error()) Err(error())
} }
fn try_parse_ansi_sequence<I>(iter: &mut I) -> io::Result<Event> fn try_parse_ansi_sequence<I>(iter: &mut I) -> io::Result<Event>
where where
I: Iterator<Item = io::Result<u8>>, I: Iterator<Item = io::Result<u8>>,
{ {
let error = io::Error::other("Could not parse event"); let error = io::Error::other("Could not parse event");
match iter.next() { match iter.next() {
Some(Ok(b'O')) => match iter.next() { Some(Ok(b'O')) => match iter.next() {
@@ -307,12 +290,12 @@ pub mod input {
Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error), Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error),
_ => Err(error), _ => Err(error),
} }
} }
fn try_parse_csi_sequence<I>(iter: &mut I) -> Option<Event> fn try_parse_csi_sequence<I>(iter: &mut I) -> Option<Event>
where where
I: Iterator<Item = io::Result<u8>>, I: Iterator<Item = io::Result<u8>>,
{ {
match iter.next() { match iter.next() {
Some(Ok(b'[')) => match iter.next() { Some(Ok(b'[')) => match iter.next() {
Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key( Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key(
@@ -335,10 +318,10 @@ pub mod input {
)), )),
_ => None, _ => None,
} }
} }
#[test] #[test]
fn test_parse_utf8() { fn test_parse_utf8() {
let string = "abcéŷ¤£€ù%323"; let string = "abcéŷ¤£€ù%323";
let ref mut bytes = string.bytes().map(|x| Ok(x)); let ref mut bytes = string.bytes().map(|x| Ok(x));
let chars = string.chars(); let chars = string.chars();
@@ -347,5 +330,4 @@ pub mod input {
let character = parse_utf8_char(b, bytes).unwrap(); let character = parse_utf8_char(b, bytes).unwrap();
assert!(c == character); assert!(c == character);
} }
}
} }

View File

@@ -1,5 +1,6 @@
use std::io; use crate::input::{Event, Key, KeyModifiers, KeyType};
use std::os::windows::raw::HANDLE; use std::os::windows::raw::HANDLE;
use std::{io, mem, time::Duration};
#[link(name = "kernel32")] #[link(name = "kernel32")]
unsafe extern "system" { unsafe extern "system" {
@@ -62,79 +63,68 @@ fn get_console_mode(handle: HANDLE, mode: &mut u32) -> io::Result<()> {
} }
} }
pub mod os { /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization
use super::{ ///
ConsoleScreenBufferInfo, GetConsoleScreenBufferInfo, get_console_mode, get_stdin_handle, /// # Errors
get_stdout_handle, set_console_mode, ///
}; /// If there is no stdin,
use super::{ /// stdin is not a tty,
ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT, /// or it fails to change terminal settings
ENABLE_VIRTUAL_TERMINAL_PROCESSING, pub fn enable_raw_mode() -> io::Result<()> {
};
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 handle = get_stdin_handle()?;
let mut mode = 0; let mut mode = 0;
get_console_mode(handle, &mut mode)?; get_console_mode(handle, &mut mode)?;
mode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); mode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
set_console_mode(handle, mode)?; set_console_mode(handle, mode)?;
Ok(()) Ok(())
} }
/// Disables raw mode, which enables line buffering, input echoing, and output canonicalization /// Disables raw mode, which enables line buffering, input echoing, and output canonicalization
/// ///
/// # Errors /// # Errors
/// ///
/// If there is no stdin, /// If there is no stdin,
/// stdin is not a tty, /// stdin is not a tty,
/// or it fails to change terminal settings /// or it fails to change terminal settings
pub fn disable_raw_mode() -> io::Result<()> { pub fn disable_raw_mode() -> io::Result<()> {
let handle = get_stdin_handle()?; let handle = get_stdin_handle()?;
let mut mode = 0; let mut mode = 0;
get_console_mode(handle, &mut mode)?; get_console_mode(handle, &mut mode)?;
mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
set_console_mode(handle, mode)?; set_console_mode(handle, mode)?;
Ok(()) Ok(())
} }
/// Enables ANSI support on Windows terminals /// Enables ANSI support on Windows terminals
/// ///
/// ANSI is on by default on *nix machines but still exists on them for simpler usage /// ANSI is on by default on *nix machines but still exists on them for simpler usage
/// ///
/// # Errors /// # Errors
/// ///
/// Never on *nix /// Never on *nix
/// ///
/// On Windows, if There is no stdout, /// On Windows, if There is no stdout,
/// if stdout isn't a TTY, or /// if stdout isn't a TTY, or
/// if it cannot change terminal properties /// if it cannot change terminal properties
pub fn enable_ansi() -> io::Result<()> { pub fn enable_ansi() -> io::Result<()> {
let handle = get_stdout_handle()?; let handle = get_stdout_handle()?;
let mut mode = 0; let mut mode = 0;
get_console_mode(handle, &mut mode)?; get_console_mode(handle, &mut mode)?;
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
set_console_mode(handle, mode)?; set_console_mode(handle, mode)?;
Ok(()) Ok(())
} }
/// Gets the size of the terminal /// Gets the size of the terminal
/// ///
/// Returns in (width, height) format /// Returns in (width, height) format
/// ///
/// # Errors /// # Errors
/// ///
/// If there is no stdout, /// If there is no stdout,
/// if stdout isn't a TTY, or /// if stdout isn't a TTY, or
/// if it fails to retrieve the terminal size /// if it fails to retrieve the terminal size
pub fn get_terminal_size() -> io::Result<(u16, u16)> { pub fn get_terminal_size() -> io::Result<(u16, u16)> {
let handle = get_stdout_handle()?; let handle = get_stdout_handle()?;
let mut csbi = ConsoleScreenBufferInfo::default(); let mut csbi = ConsoleScreenBufferInfo::default();
if unsafe { GetConsoleScreenBufferInfo(handle, &mut csbi) != 0 } { if unsafe { GetConsoleScreenBufferInfo(handle, &mut csbi) != 0 } {
@@ -143,55 +133,47 @@ pub mod os {
return Ok((width, height)); return Ok((width, height));
} }
Err(io::Error::last_os_error()) Err(io::Error::last_os_error())
}
} }
pub mod input { #[repr(C)]
use super::get_stdin_handle; #[derive(Copy, Clone)]
use crate::input::{Event, Key, KeyModifiers, KeyType}; struct InputRecord {
use std::os::windows::raw::HANDLE;
use std::{io, mem, time::Duration};
#[repr(C)]
#[derive(Copy, Clone)]
struct InputRecord {
event_type: u16, event_type: u16,
event: EventRecord, event: EventRecord,
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
union EventRecord { union EventRecord {
key: KeyEventRecord, key: KeyEventRecord,
focus: FocusEventRecord, focus: FocusEventRecord,
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
struct KeyEventRecord { struct KeyEventRecord {
key_down: i32, key_down: i32,
repeat_count: u16, repeat_count: u16,
virtual_key_code: u16, virtual_key_code: u16,
virtual_scan_code: u16, virtual_scan_code: u16,
u_char: CharUnion, u_char: CharUnion,
control_key_state: u32, control_key_state: u32,
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
struct FocusEventRecord { struct FocusEventRecord {
set_focus: i32, set_focus: i32,
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
union CharUnion { union CharUnion {
unicode_char: u16, unicode_char: u16,
ascii_char: u8, ascii_char: u8,
} }
unsafe extern "system" { unsafe extern "system" {
fn ReadConsoleInputW( fn ReadConsoleInputW(
console_input: HANDLE, console_input: HANDLE,
buffer: *mut InputRecord, buffer: *mut InputRecord,
@@ -199,14 +181,14 @@ pub mod input {
number_of_events_read: *mut u32, number_of_events_read: *mut u32,
) -> i32; ) -> i32;
fn WaitForSingleObject(handle: HANDLE, wait_time_ms: u32) -> u32; fn WaitForSingleObject(handle: HANDLE, wait_time_ms: u32) -> u32;
} }
/// Attempts to fetch input from stdin /// Attempts to fetch input from stdin
/// ///
/// # Errors /// # Errors
/// If the timeout has expired or /// If the timeout has expired or
/// there was an error getting the data /// there was an error getting the data
pub fn poll_input(timeout: Duration) -> io::Result<Event> { pub fn poll_input(timeout: Duration) -> io::Result<Event> {
let handle = get_stdin_handle()?; let handle = get_stdin_handle()?;
let mut record: InputRecord = unsafe { mem::zeroed() }; let mut record: InputRecord = unsafe { mem::zeroed() };
let mut read = 0; let mut read = 0;
@@ -248,9 +230,9 @@ pub mod input {
Err(io::ErrorKind::InvalidData.into()) Err(io::ErrorKind::InvalidData.into())
} }
} }
} }
fn parse_key_event(event: &KeyEventRecord) -> Event { fn parse_key_event(event: &KeyEventRecord) -> Event {
let ctrl = event.control_key_state & (0x0008 | 0x0004) != 0; // LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED 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 let shift = event.control_key_state & 0x0010 != 0; // SHIFT_PRESSED
@@ -283,8 +265,8 @@ pub mod input {
KeyModifiers::none(), KeyModifiers::none(),
), // F1-F24 ), // F1-F24
_ => { _ => {
let c = let num = u32::from(unsafe { event.u_char.unicode_char });
char::from_u32(u32::from(unsafe { event.u_char.unicode_char })).unwrap_or(' '); let c = char::from_u32(num).unwrap_or(' ');
if ctrl && c.is_ascii_alphabetic() { if ctrl && c.is_ascii_alphabetic() {
Event::Key(Key::Char(c), KeyType::Press, KeyModifiers::none().ctrl()) Event::Key(Key::Char(c), KeyType::Press, KeyModifiers::none().ctrl())
} else { } else {
@@ -292,5 +274,4 @@ pub mod input {
} }
} }
} }
}
} }