From 31ae40b96a248306f1a4407d39b935bf5d273f7d Mon Sep 17 00:00:00 2001 From: Xyverle Date: Thu, 27 Mar 2025 21:52:22 -0400 Subject: [PATCH] tons of various things but no major changes + rename --- Cargo.toml | 2 +- src/ansi.rs | 19 ++++- src/input.rs | 153 +++++++++++++++++++++++++++-------------- src/input_sequences.md | 16 +++++ src/lib.rs | 2 +- src/unix.rs | 107 ++++++++++++++-------------- src/windows.rs | 81 +++++++++++----------- 7 files changed, 227 insertions(+), 153 deletions(-) create mode 100644 src/input_sequences.md diff --git a/Cargo.toml b/Cargo.toml index e5ac5c2..4a22304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "neutrino" +name = "neutuino" version = "0.1.0" edition = "2024" diff --git a/src/ansi.rs b/src/ansi.rs index 0bbe55c..76413ce 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -14,9 +14,12 @@ pub fn rgb_color_code(red: u8, green: u8, blue: u8) -> String { /// /// The title must be only in ASCII characters or **weird** things will happen #[must_use] -pub fn set_window_title(title: [u8; 255]) -> String { - let title = String::from_utf8_lossy(&title); - format!("\x1b]0;{title}\x1b\x5c") +pub fn set_window_title>(title: T) -> Option { + let title = title.into(); + if title.len() > 255 { + return None; + } + Some(format!("\x1b]0;{title}\x1b\x5c")) } /// Moves the cursor up {num} characters @@ -67,6 +70,16 @@ pub fn move_cursor_to_position(column: u16, line: u16) -> String { format!("\x1b[{};{}H", line.saturating_add(1), column.saturating_add(1)) } +/// Sends input when terminal is in focus +pub const FOCUS_REPORTING_ENABLE: &str = "\x1b[?1004h"; +/// Stops sending input when terminal is in focus +pub const FOCUS_REPORTING_DISABLE: &str = "\x1b[?1004l"; + +/// Makes all pasted text treated differently +pub const BRACKETED_PASTE_ENABLE: &str = "\x1b[?2004h"; +/// Disables bracketed paste +pub const BRACKETED_PASTE_DISABLE: &str = "\x1b[?2004l"; + /// Saves the current cursor position pub const CURSOR_POSITION_SAVE: &str = "\x1b7"; /// Restores the saved cursor position diff --git a/src/input.rs b/src/input.rs index 39b9ada..1e6cdb7 100644 --- a/src/input.rs +++ b/src/input.rs @@ -2,59 +2,106 @@ //! //! Very incomplete currently -use std::io::{self, Read}; -use std::sync::mpsc; -use std::thread; -/// 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. -/// -/// Taken from the Termion crate -pub struct AsyncReader { - recv: mpsc::Receiver>, -} +// pub(crate) fn parse_event( +// buffer: &[u8], +// input_available: bool, +// ) -> io::Result> { +// if buffer.is_empty() { +// return Ok(None); +// } -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 buffer[0] { +// b'\x1B' => { +// if buffer.len() == 1 { +// if input_available { +// // Possible Esc sequence +// Ok(None) +// } else { +// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))) +// } +// } else { +// match buffer[1] { +// b'O' => { +// if buffer.len() == 2 { +// Ok(None) +// } else { +// match buffer[2] { +// b'D' => { +// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into())))) +// } +// b'C' => Ok(Some(InternalEvent::Event(Event::Key( +// KeyCode::Right.into(), +// )))), +// b'A' => { +// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into())))) +// } +// b'B' => { +// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into())))) +// } +// b'H' => { +// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into())))) +// } +// b'F' => { +// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into())))) +// } +// // F1-F4 +// val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( +// KeyCode::F(1 + val - b'P').into(), +// )))), +// _ => Err(could_not_parse_event_error()), +// } +// } +// } +// b'[' => parse_csi(buffer), +// b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), +// _ => parse_event(&buffer[1..], input_available).map(|event_option| { +// event_option.map(|event| { +// if let InternalEvent::Event(Event::Key(key_event)) = event { +// let mut alt_key_event = key_event; +// alt_key_event.modifiers |= KeyModifiers::ALT; +// InternalEvent::Event(Event::Key(alt_key_event)) +// } else { +// event +// } +// }) +// }), +// } +// } +// } +// b'\r' => Ok(Some(InternalEvent::Event(Event::Key( +// KeyCode::Enter.into(), +// )))), +// // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get +// // newlines as input is because the terminal converts \r into \n for us. When we +// // enter raw mode, we disable that, so \n no longer has any meaning - it's better to +// // use Ctrl+J. Waiting to handle it here means it gets picked up later +// b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event( +// Event::Key(KeyCode::Enter.into()), +// ))), +// b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))), +// b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key( +// KeyCode::Backspace.into(), +// )))), +// c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( +// KeyCode::Char((c - 0x1 + b'a') as char), +// KeyModifiers::CONTROL, +// ))))), +// c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( +// KeyCode::Char((c - 0x1C + b'4') as char), +// KeyModifiers::CONTROL, +// ))))), +// b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( +// KeyCode::Char(' '), +// KeyModifiers::CONTROL, +// ))))), +// _ => parse_utf8_char(buffer).map(|maybe_char| { +// maybe_char +// .map(KeyCode::Char) +// .map(char_code_to_event) +// .map(Event::Key) +// .map(InternalEvent::Event) +// }), +// } +// } - 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 } - } -} diff --git a/src/input_sequences.md b/src/input_sequences.md new file mode 100644 index 0000000..963065f --- /dev/null +++ b/src/input_sequences.md @@ -0,0 +1,16 @@ +* 0x1b = Escape +* 0x1b1b = Escape +* 0x1b41 = Up Arrow +* 0x1b42 = Down Arrow +* 0x1b43 = Right Arrow +* 0x1b44 = Left Arrow +* 0x1b46 = End +* 0x1b48 = Home +* 0x1b5b41 = Up Arrow +* 0x1b5b42 = Down Arrow +* 0x1b5b43 = Right Arrow +* 0x1b5b44 = Left Arrow +* 0x1b5b46 = End +* 0x1b5b48 = Home +* 0x20-7e = Char(n) + diff --git a/src/lib.rs b/src/lib.rs index ba5c869..082d447 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![warn(clippy::all, clippy::pedantic)] pub mod ansi; -pub mod input; +// pub mod input; pub mod os; diff --git a/src/unix.rs b/src/unix.rs index 95f9293..70a7d1b 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -3,31 +3,26 @@ use std::io; unsafe extern "C" { fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; - fn tcgetattr(fd: c_int, termios_p: *mut Termios) -> c_int; + safe fn cfmakeraw(termios: *mut Termios); + fn tcgetattr(fd: c_int, termios: *mut Termios) -> c_int; fn tcsetattr(fd: c_int, optional_actions: c_int, termios: *mut Termios) -> c_int; } -#[cfg(any(target_os = "linux", target_os = "redox"))] +const STDIN_FILENO: c_int = 0x0; +const STDOUT_FILENO: c_int = 0x1; + +#[cfg(target_os = "linux")] const TIOCGWINSZ: c_ulong = 0x5413; +#[cfg(target_os = "linux")] +const NCCS: usize = 0x20; -#[cfg(any(target_os = "macos", target_os = "freebsd"))] -const TIOCGWINSZ: c_ulong = 0x40087468; - -#[cfg(any(target_os = "linux", target_os = "redox"))] -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 = 8; -const ICANON: c_uint = 2; -const ISIG: c_uint = 1; +#[cfg(target_os = "macos")] +const TIOCGWINSZ: c_ulong = 0x4008_7468; +#[cfg(target_os = "macos")] +const NCCS: usize = 0x14; #[repr(C)] -#[derive(Debug, Clone, Copy)] +#[derive(Default, Debug, Clone, Copy)] struct Winsize { row: c_ushort, col: c_ushort, @@ -36,7 +31,7 @@ struct Winsize { } #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] struct Termios { iflag: c_uint, oflag: c_uint, @@ -45,6 +40,42 @@ 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 +} + +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 mut orig_termios = Termios::default(); + get_attributes(STDIN_FILENO, &mut orig_termios)?; + let mut termios = orig_termios.clone(); + cfmakeraw(&raw mut termios); + set_attributes(STDIN_FILENO, &mut termios)?; + Ok(Self { orig_termios }) + } +} + +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"); + } +} + /// Enables ANSI support on Windows terminals /// /// ANSI is on by default on *nix machines but still exists on them for simpler usage @@ -72,50 +103,16 @@ pub fn enable_ansi() -> io::Result<()> { /// 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 = unsafe { std::mem::zeroed::() }; + let mut winsize = Winsize::default(); let ioctl_result = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, (&raw mut winsize).cast::()) }; if ioctl_result == 0 { - Ok((winsize.col as u16, winsize.row as u16)) + Ok((winsize.col, winsize.row)) } else { Err(io::Error::last_os_error()) } } -/// Enables raw mode -/// -/// Disables input echoing, line feeding, etc. -/// -/// # Errors -/// -/// If there is no stdout, -/// if stdout isn't a TTY, or -/// if it fails to get or set terminal settings -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(()) -} - -/// Disables raw mode -/// -/// Enables input echoing, line feeding, etc. -/// -/// # Errors -/// -/// If there is no stdout, -/// if stdout isn't a TTY, or -/// if it fails to get or set terminal settings -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()); diff --git a/src/windows.rs b/src/windows.rs index 5330f01..cf29b57 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -26,6 +26,43 @@ 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 @@ -40,9 +77,9 @@ struct ConsoleScreenBufferInfo { pub fn enable_ansi() -> io::Result<()> { let handle = get_std_handle(STD_OUTPUT_HANDLE)?; let mut mode = 0; - get_console_mode(handle, &raw mut mode)?; + get_console_mode(handle, &mut mode)?; mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; - set_console_mode(handle, &raw mut mode)?; + set_console_mode(handle, &mut mode)?; Ok(()) } @@ -66,42 +103,6 @@ pub fn get_terminal_size() -> io::Result<(u16, u16)> { Err(io::Error::last_os_error()) } -/// Enables raw mode -/// -/// Disables input echoing, line feeding, etc. -/// -/// # Errors -/// -/// If there is no stdout, -/// if stdout isn't a TTY, or -/// if it fails to get or set 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, &raw mut mode)?; - mode &= !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); - set_console_mode(handle, &raw mut mode)?; - Ok(()) -} - -/// Disables raw mode -/// -/// Enables input echoing, line feeding, etc. -/// -/// # Errors -/// -/// If there is no stdout, -/// if stdout isn't a TTY, or -/// if it fails to get or set 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, &raw mut mode)?; - mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; - set_console_mode(handle, &raw mut mode)?; - Ok(()) -} - fn get_std_handle(handle: u32) -> io::Result { let handle = unsafe { GetStdHandle(handle) }; if handle == INVALID_HANDLE_VALUE { @@ -111,7 +112,7 @@ fn get_std_handle(handle: u32) -> io::Result { } } -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 { @@ -119,7 +120,7 @@ fn set_console_mode(handle: usize, mode: *mut u32) -> io::Result<()> { } } -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 {