first commit

This commit is contained in:
2025-03-14 18:18:41 -04:00
commit afa7979d74
11 changed files with 705 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

7
Cargo.lock generated Normal file
View File

@@ -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"

6
Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "neutrino"
version = "0.1.0"
edition = "2024"
[dependencies]

21
src/ansi.rs Normal file
View File

@@ -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,
}

191
src/input.rs Normal file
View File

@@ -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<Key> {
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) };
}
}
}

78
src/input/async.rs Normal file
View File

@@ -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<io::Result<u8>>,
}
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<usize> {
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<R: Read> 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 }
}

4
src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
#![warn(clippy::all, clippy::pedantic)]
pub mod os;
pub mod ansi;

41
src/os/mod.rs Normal file
View File

@@ -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;

123
src/os/unix.rs Normal file
View File

@@ -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<RawTerminal> {
let mut orig_termios = unsafe { std::mem::zeroed::<Termios>() };
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::<Winsize>() };
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::<Termios>() };
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::<Termios>() };
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(())
}

98
src/os/unix.rs.new Normal file
View File

@@ -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<RawTerminal> {
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::<Winsize>() };
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(())
}

135
src/os/windows.rs Normal file
View File

@@ -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<RawTerminal> {
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<usize> {
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(())
}
}