From afa7979d743c7420548293daec1278009d174070 Mon Sep 17 00:00:00 2001 From: Xyverle Date: Fri, 14 Mar 2025 18:18:41 -0400 Subject: [PATCH] first commit --- .gitignore | 1 + Cargo.lock | 7 ++ Cargo.toml | 6 ++ src/ansi.rs | 21 +++++ src/input.rs | 191 +++++++++++++++++++++++++++++++++++++++++++++ src/input/async.rs | 78 ++++++++++++++++++ src/lib.rs | 4 + src/os/mod.rs | 41 ++++++++++ src/os/unix.rs | 123 +++++++++++++++++++++++++++++ src/os/unix.rs.new | 98 +++++++++++++++++++++++ src/os/windows.rs | 135 ++++++++++++++++++++++++++++++++ 11 files changed, 705 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/ansi.rs create mode 100644 src/input.rs create mode 100644 src/input/async.rs create mode 100644 src/lib.rs create mode 100644 src/os/mod.rs create mode 100644 src/os/unix.rs create mode 100644 src/os/unix.rs.new create mode 100644 src/os/windows.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7aa7828 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "neutrino" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e5ac5c2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "neutrino" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/ansi.rs b/src/ansi.rs new file mode 100644 index 0000000..e1d4993 --- /dev/null +++ b/src/ansi.rs @@ -0,0 +1,21 @@ +use std::fmt; + +pub struct CursorHome {} + +impl fmt::Display for CursorHome { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + +pub enum Cursor { + Move(u16, u16), + Up(u16), + Down(u16), + Right(u16), + Left(u16), + Column(u16), + Home, + Save, + Restore, +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..7369aed --- /dev/null +++ b/src/input.rs @@ -0,0 +1,191 @@ +use std::mem; +use std::os::raw::c_int; + +#[derive(Debug)] +pub enum Key { + String(String), + Char(char), + Enter, + Escape, + Backspace, + Tab, + ShiftTab, + Delete, + Home, + End, + PageUp, + PageDown, + ArrowUp, + ArrowDown, + ArrowLeft, + ArrowRight, + Ctrl(char), +} + +#[cfg(unix)] +pub mod platform { + use super::*; + use std::fs::File; + use std::io::Read; + use std::os::fd::AsRawFd; + + unsafe extern "C" { + fn tcgetattr(fd: c_int, termios_p: *mut Termios) -> c_int; + fn tcsetattr(fd: c_int, optional_actions: c_int, termios_p: *const Termios) -> c_int; + fn fcntl(fd: c_int, cmd: c_int, arg: c_int) -> c_int; + } + + const TCSAFLUSH: c_int = 2; + const ICANON: c_int = 0o0002; + const ECHO: c_int = 0o0010; + const O_NONBLOCK: c_int = 0x800; + + #[repr(C)] + #[derive(Clone)] + struct Termios { + c_iflag: u32, + c_oflag: u32, + c_cflag: u32, + c_lflag: u32, + c_cc: [u8; 32], + } + + pub struct RawInput { + stdin: File, + original_termios: Termios, + } + + impl RawInput { + pub fn new() -> Self { + let stdin = File::open("/dev/tty").unwrap(); + let fd = stdin.as_raw_fd(); + let mut termios: Termios = unsafe { mem::zeroed() }; + + unsafe { + tcgetattr(fd, &mut termios); + let mut new_termios = termios.clone(); + new_termios.c_lflag &= !(ICANON | ECHO) as u32; + tcsetattr(fd, TCSAFLUSH, &new_termios); + fcntl(fd, 4, O_NONBLOCK); + } + + Self { + stdin, + original_termios: termios, + } + } + + pub fn read_key(&mut self) -> [u8; 6] { + let mut buf = [0; 6]; + _ = self.stdin.read(&mut buf); + buf + } + } + + impl Drop for RawInput { + fn drop(&mut self) { + unsafe { tcsetattr(self.stdin.as_raw_fd(), TCSAFLUSH, &self.original_termios) }; + } + } +} + +#[cfg(windows)] +pub mod platform { + use super::*; + use std::ptr; + + extern "system" { + fn GetStdHandle(nStdHandle: c_int) -> isize; + fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32; + fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32; + fn ReadConsoleInputW(hConsoleInput: isize, lpBuffer: *mut InputRecord, nLength: u32, lpNumberOfEventsRead: *mut u32) -> i32; + fn PeekConsoleInputW(hConsoleInput: isize, lpBuffer: *mut InputRecord, nLength: u32, lpNumberOfEventsRead: *mut u32) -> i32; + } + + const STD_INPUT_HANDLE: c_int = -10; + const ENABLE_PROCESSED_INPUT: u32 = 0x0001; + const ENABLE_LINE_INPUT: u32 = 0x0002; + const ENABLE_ECHO_INPUT: u32 = 0x0004; + + #[repr(C)] + struct InputRecord { + event_type: c_ushort, + event: KeyEventRecord, + } + + #[repr(C)] + struct KeyEventRecord { + key_down: i32, + repeat_count: c_ushort, + virtual_key_code: c_ushort, + virtual_scan_code: c_ushort, + unicode_char: u16, + control_key_state: u32, + } + + pub struct RawInput { + handle: isize, + original_mode: u32, + } + + impl RawInput { + pub fn new() -> Self { + unsafe { + let handle = GetStdHandle(STD_INPUT_HANDLE); + let mut mode = 0; + GetConsoleMode(handle, &mut mode); + SetConsoleMode(handle, mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)); + + Self { + handle, + original_mode: mode, + } + } + } + + pub fn read_key(&mut self) -> Option { + let mut records: [InputRecord; 1] = unsafe { mem::zeroed() }; + let mut num_read = 0; + + unsafe { + PeekConsoleInputW(self.handle, records.as_mut_ptr(), 1, &mut num_read); + if num_read == 0 { + return None; + } + + ReadConsoleInputW(self.handle, records.as_mut_ptr(), 1, &mut num_read); + + if records[0].event.key_down == 0 { + return None; + } + + match records[0].event.virtual_key_code { + 0x1B => Some(Key::Escape), + 0x0D => Some(Key::Enter), + 0x08 => Some(Key::Backspace), + 0x09 => Some(Key::Tab), + 0x2F => Some(Key::ShiftTab), + 0x2E => Some(Key::Delete), + 0x24 => Some(Key::Home), + 0x23 => Some(Key::End), + 0x21 => Some(Key::PageUp), + 0x22 => Some(Key::PageDown), + 0x26 => Some(Key::ArrowUp), + 0x28 => Some(Key::ArrowDown), + 0x25 => Some(Key::ArrowLeft), + 0x27 => Some(Key::ArrowRight), + c if c >= 1 && c <= 26 => Some(Key::Ctrl((b'a' + (c - 1) as u8) as char)), + c if records[0].event.unicode_char != 0 => Some(Key::Char(records[0].event.unicode_char as u8 as char)), + _ => None, + } + } + } + } + + impl Drop for RawInput { + fn drop(&mut self) { + unsafe { SetConsoleMode(self.handle, self.original_mode) }; + } + } +} + diff --git a/src/input/async.rs b/src/input/async.rs new file mode 100644 index 0000000..6a0af87 --- /dev/null +++ b/src/input/async.rs @@ -0,0 +1,78 @@ + +/// An asynchronous reader. + +/// + +/// This acts as any other stream, with the exception that reading from it won't block. Instead, + +/// the buffer will only be partially updated based on how much the internal buffer holds. +pub struct AsyncReader { + recv: mpsc::Receiver>, + +} + +impl Read for AsyncReader { + + /// Read from the byte stream. + + /// + + /// This will never block, but try to drain the event queue until empty. If the total number of + + /// bytes written is lower than the buffer's length, the event queue is empty or that the event + + /// stream halted. + + fn read(&mut self, buf: &mut [u8]) -> io::Result { + + let mut total = 0; + + + loop { + + if total >= buf.len() { + + break; + + } + + + match self.recv.try_recv() { + + Ok(Ok(b)) => { + + buf[total] = b; + + total += 1; + + } + + Ok(Err(e)) => return Err(e), + + Err(_) => break, + + } + + } + + + Ok(total) + + } + +} + +impl AsyncReader { + pub fn new(reader: R) -> Self { + let (send, recv) = mpsc::channel(); + + thread::spawn(move || { + for i in reader.bytes() { + if send.send(i).is_err() { + return; + } + } + }); + + Self { recv: recv } + } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..15ce0e3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +#![warn(clippy::all, clippy::pedantic)] + +pub mod os; +pub mod ansi; diff --git a/src/os/mod.rs b/src/os/mod.rs new file mode 100644 index 0000000..e5ba15f --- /dev/null +++ b/src/os/mod.rs @@ -0,0 +1,41 @@ +#![allow(unused_imports)] + +#[cfg(unix)] +mod unix; + +#[cfg(windows)] +mod windows; + +#[cfg(unix)] +use unix as os; + +#[cfg(windows)] +use windows as os; + +/// Checks if stdout is a terminal +pub use os::istty; + +/// Enables ANSI support on Windows terminals +/// +/// ANSI is on by default on *nix machines but still exists for ease of use +pub use os::enable_ansi; + +/// Gets the size of the terminal +pub use os::get_terminal_size; + + +/// Struct representing a raw terminal +/// +/// This was done due to weirdness in the termios API (you have to store the original state of the +/// terminal to restore it) +pub use os::RawTerminal; + +/// Enables raw mode +/// +/// Disables input echoing, line feeding, etc. +pub use os::enable_raw_mode; + +/// Disables raw mode +/// +/// Enables input echoing, line feeding, etc. +pub use os::disable_raw_mode; diff --git a/src/os/unix.rs b/src/os/unix.rs new file mode 100644 index 0000000..c386273 --- /dev/null +++ b/src/os/unix.rs @@ -0,0 +1,123 @@ +use std::ffi::{c_int, c_uint, c_ulong, c_ushort}; +use std::io; + +unsafe extern "C" { + fn isatty(fd: c_int) -> c_int; + fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; + fn tcgetattr(fd: c_int, termios_p: *mut Termios) -> c_int; + fn tcsetattr(fd: c_int, optional_actions: c_int, termios: *mut Termios) -> c_int; + fn cfmakeraw(termios: *mut Termios); +} + +#[cfg(target_os = "linux")] +const TIOCGWINSZ: c_ulong = 0x5413; + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +const TIOCGWINSZ: c_ulong = 0x40087468; + +#[cfg(target_os = "linux")] +const NCCS: usize = 32; + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +const NCCS: usize = 20; + +const STDIN_FILENO: c_int = 0; +const STDOUT_FILENO: c_int = 1; + +const ECHO: c_uint = 0; +const ICANON: c_uint = 0; +const ISIG: c_uint = 0; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Winsize { + row: c_ushort, + col: c_ushort, + xpixel: c_ushort, + ypixel: c_ushort, +} + +#[repr(C)] +#[derive(Debug, Clone)] +struct Termios { + iflag: c_uint, + oflag: c_uint, + cflag: c_uint, + lflag: c_uint, + cc: [u8; NCCS], +} + +#[repr(transparent)] +pub struct RawTerminal { + orig_termios: Termios, +} + +impl RawTerminal { + pub fn new() -> io::Result { + let mut orig_termios = unsafe { std::mem::zeroed::() }; + get_attributes(STDIN_FILENO, &mut orig_termios)?; + + let mut current_termios = orig_termios.clone(); + unsafe { cfmakeraw(&raw mut current_termios) }; + set_attributes(STDIN_FILENO, &mut current_termios)?; + + Ok(RawTerminal { orig_termios }) + } +} + +impl Drop for RawTerminal { + fn drop(&mut self) { + _ = set_attributes(STDIN_FILENO, &mut self.orig_termios); + } +} + +pub fn istty() -> bool { + unsafe { isatty(1) != 0 } +} + +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(()) +} + +pub fn get_terminal_size() -> io::Result<(c_ushort, c_ushort)> { + let mut winsize = unsafe { std::mem::zeroed::() }; + let ioctl_result = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &raw mut winsize as *mut u8) }; + + if ioctl_result == 0 { + Ok((winsize.col, winsize.row)) + } else { + Err(io::Error::last_os_error()) + } +} + +pub fn enable_raw_mode() -> io::Result<()> { + let mut termios = unsafe { std::mem::zeroed::() }; + get_attributes(STDIN_FILENO, &mut termios)?; + termios.lflag &= !(ECHO | ISIG | ICANON); + set_attributes(STDIN_FILENO, &mut termios)?; + Ok(()) +} + +pub fn disable_raw_mode() -> io::Result<()> { + let mut termios = unsafe { std::mem::zeroed::() }; + get_attributes(STDIN_FILENO, &mut termios)?; + termios.lflag |= ECHO | ISIG | ICANON; + set_attributes(STDIN_FILENO, &mut termios)?; + Ok(()) +} + +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, termios as *mut _) } != 0 { + return Err(io::Error::last_os_error()) + } + Ok(()) +} diff --git a/src/os/unix.rs.new b/src/os/unix.rs.new new file mode 100644 index 0000000..dd2387a --- /dev/null +++ b/src/os/unix.rs.new @@ -0,0 +1,98 @@ +use std::ffi::{c_int, c_uint, c_ulong, c_ushort}; +use std::io; +use std::mem; + +unsafe extern "C" { + fn isatty(fd: c_int) -> c_int; + fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; + fn tcgetattr(fd: c_int, termios_p: *mut Termios) -> c_int; + fn tcsetattr(fd: c_int, optional_actions: c_int, termios: *mut Termios) -> c_int; + fn cfmakeraw(termios: *mut Termios); +} + +#[cfg(target_os = "linux")] +const TIOCGWINSZ: c_ulong = 0x5413; + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +const TIOCGWINSZ: c_ulong = 0x40087468; + +#[cfg(target_os = "linux")] +const NCCS: usize = 32; + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +const NCCS: usize = 20; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Winsize { + row: c_ushort, + col: c_ushort, + xpixel: c_ushort, + ypixel: c_ushort, +} + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct Termios { + iflag: c_uint, + oflag: c_uint, + cflag: c_uint, + lflag: c_uint, + cc: [u8; NCCS], +} + +#[derive(Debug, Clone)] +pub struct RawTerminal { + input: Termios, + output: Termios, +} + +impl RawTerminal { + pub fn new() -> io::Result { + let mut term: RawTerminal = unsafe { mem::zeroed() }; + if unsafe { tcgetattr(0, &raw mut term.input) } != 0 { + return Err(io::Error::last_os_error()) + } + if unsafe { tcgetattr(1, &raw mut term.output) } != 0 { + return Err(io::Error::last_os_error()) + } + let orig_term = term.clone(); + unsafe { cfmakeraw(&raw mut term.input) }; + unsafe { cfmakeraw(&raw mut term.output) }; + if unsafe { tcsetattr(0, 0, &raw mut term.input) } != 0 { + return Err(io::Error::last_os_error()) + } + if unsafe { tcsetattr(1, 0, &raw mut term.output) } != 0 { + return Err(io::Error::last_os_error()) + } + Ok(orig_term) + } +} + +impl Drop for RawTerminal { + fn drop(&mut self) { + unsafe { tcsetattr(0, 0, &raw mut self.input) }; + unsafe { tcsetattr(1, 0, &raw mut self.input) }; + } +} + +pub fn istty() -> bool { + unsafe { isatty(1) != 0 } +} + +pub fn get_terminal_size() -> io::Result<(c_ushort, c_ushort)> { + let mut winsize = unsafe { std::mem::zeroed::() }; + let ioctl_result = unsafe { ioctl(1, TIOCGWINSZ, &raw mut winsize as *mut u8) }; + + if ioctl_result == 0 { + Ok((winsize.col, winsize.row)) + } else { + Err(io::Error::last_os_error()) + } +} + +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(()) +} diff --git a/src/os/windows.rs b/src/os/windows.rs new file mode 100644 index 0000000..205d441 --- /dev/null +++ b/src/os/windows.rs @@ -0,0 +1,135 @@ +#![allow(non_snake_case)] + +use std::ffi::{c_int, c_uint, c_ulong, c_ushort}; +use std::io; + +const STD_INPUT_HANDLE: u32 = 0xFFFFFFF6; +const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5; +const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 4; +const ENABLE_ECHO_INPUT: u32 = 4; +const ENABLE_LINE_INPUT: u32 = 2; +const ENABLE_PROCESSED_INPUT: u32 = 1; +const INVALID_HANDLE_VALUE: usize = usize::MAX-1; + +unsafe extern "system" { + fn GetStdHandle(nStdHandle: u32) -> usize; + fn GetConsoleMode(hConsoleHandle: usize, dwMode: *mut u32) -> u32; + fn SetConsoleMode(hConsoleHandle: usize, dwMode: *mut u32) -> u32; + fn GetConsoleScreenBufferInfo( + hConsoleOutput: usize, + lpConsoleScreenBufferInfo: *mut ConsoleScreenBufferInfo, + ) -> u32; +} + +#[repr(C)] +#[derive(Default)] +struct ConsoleScreenBufferInfo { + dwSizeX: u16, + dwSizeY: u16, + dwCursorPositionX: u16, + dwCursorPositionY: u16, + wAttributes: u16, + srWindowLeft: u16, + srWindowTop: u16, + srWindowRight: u16, + srWindowBottom: u16, + dwMaximumWindowSizeX: u16, + dwMaximumWindowSizeY: u16, +} + +pub struct RawTerminal { } + +impl RawTerminal { + pub fn new() -> io::Result { + let handle = get_std_handle(STD_INPUT_HANDLE)?; + let mut dwMode = 0; + get_console_mode(handle, &raw mut dwMode)?; + dwMode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); + set_console_mode(handle, &raw mut dwMode)?; + Ok(RawTerminal { }) + } +} + +impl Drop for RawTerminal { + fn drop(&mut self) { + let handle = get_std_handle(STD_INPUT_HANDLE).unwrap(); + let mut dwMode = 0; + _ = get_console_mode(handle, &raw mut dwMode); + dwMode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; + _ = set_console_mode(handle, &raw mut dwMode); + } +} + +pub fn istty() -> bool { + let handle = get_std_handle(STD_OUTPUT_HANDLE); + match handle { + Ok(handle) => { + let mut dwMode = 0; + return unsafe { GetConsoleMode(handle, &mut dwMode) != 0 }; + }, + _ => false, + } +} + +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.dwSizeX as u16; + let height = csbi.dwSizeY as u16; + return Ok((width, height)); + } + Err(io::Error::last_os_error()) +} + +pub fn enable_ansi() -> io::Result<()> { + let handle = get_std_handle(STD_OUTPUT_HANDLE)?; + let mut dwMode = 0; + get_console_mode(handle, &raw mut dwMode)?; + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + set_console_mode(handle, &raw mut dwMode)?; + Ok(()) +} + +pub fn enable_raw_mode() -> io::Result<()> { + let handle = get_std_handle(STD_INPUT_HANDLE)?; + let mut dwMode = 0; + get_console_mode(handle, &raw mut dwMode)?; + dwMode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); + set_console_mode(handle, &raw mut dwMode)?; + Ok(()) +} + +pub fn disable_raw_mode() -> io::Result<()> { + let handle = get_std_handle(STD_INPUT_HANDLE)?; + let mut dwMode = 0; + get_console_mode(handle, &raw mut dwMode)?; + dwMode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; + set_console_mode(handle, &raw mut dwMode)?; + Ok(()) +} + +fn get_std_handle(handle: u32) -> io::Result { + let handle = unsafe { GetStdHandle(handle) }; + if handle != INVALID_HANDLE_VALUE { + return Ok(handle); + } else { + return Err(io::Error::last_os_error()); + } +} + +fn set_console_mode(handle: usize, dwMode: *mut u32) -> io::Result<()> { + if unsafe { SetConsoleMode(handle, dwMode) == 0 } { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +fn get_console_mode(handle: usize, dwMode: *mut u32) -> io::Result<()> { + if unsafe { GetConsoleMode(handle, dwMode) == 0 } { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +}