From df149ddfc89be7d6edd48abc18cc3c70fe2943a7 Mon Sep 17 00:00:00 2001 From: Xyverle Date: Wed, 6 Aug 2025 07:40:18 -0400 Subject: [PATCH] Fix release-mode segfault on *nix + add *nix alt support --- README.md | 44 ++++++++++++++++++-- examples/input.rs | 3 +- src/ansi.rs | 17 -------- src/control.rs | 16 ------- src/lib.rs | 103 +--------------------------------------------- src/unix.rs | 56 +++++++++++++++++++++---- 6 files changed, 93 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index 4b3847b..7112174 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Neutuino -Zero dependency cross-platform pure-rust simple TUI library +Zero dependency pure-rust simple TUI library Supported OSes: Windows 10+, MacOS (untested), and Linux @@ -9,10 +9,48 @@ This project is still highly work in progress and it will be a decent while unti - [x] Output (Unix) - [x] Output (Windows) - [x] Input (Unix) (Appears to work, more testing needed) -- [x] Input (Windows) (Appears to work, more testing needed) -- [ ] Input (Kitty) +- [ ] Input (Windows) (WIP) +- [ ] Advanced Input (Kitty-like) +- [ ] Advanced Input (Windows) - [ ] Events (Focus reporting, Bracketed-paste) (Unix) - [ ] Events (Focus reporting, Bracketed-paste) (Windows) - [ ] Mouse input (Unix) - [ ] Mouse input (Windows) - [ ] Feature completeness / API cleanup + +## Stability +This library is unstable, for now every release should be considered breaking + +## Support +This library generally attempts to have as much functionality as it can but sadly many terminal +emulators are heavily limited, there are a few protocols I have decided not to support +(i.e. [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/)) + +In general, this library will work best with terminals that support +[Kitty comprehensive keyboard handling](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) + +### Protocol Support +This is a list of terminal protocols and whether they will be supported, they still might not +work as this library is work in progress but eventually will be + +Just because a protocol is listed as not planned doesn't mean it definitely won't be added, but +it is most likely not without good reason +- Standard Windows terminals (Full support planned)\* +- WinPTY (Windows psuedo-terminals) (Full support planned) +- Standard \*nix terminals (Full support planned)\* +- OSC 52 system clipboard (Full support planned) +- Kitty comprehensive keyboard handling (Full support planned) +- Kitty colored and styled underlines (Full support planned) +- Other Kitty protocols (there are a lot of them) (Not planned) + +\* Do not have full support for advanced input + +## Structure +This library has multiple APIs + +Inside the `cli` module there are utilities for making something similar to Cargo or Git + +And inside the `tui` module there are utilities for making something similar to Neovim or Emacs + +And inside the rest of the library there are lower-level APIs in case you want to abstract over +this library yourself or if you need more precise control over the terminal diff --git a/examples/input.rs b/examples/input.rs index 0305e3f..ab43e3f 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -14,7 +14,7 @@ fn main() -> io::Result<()> { let all_styles = format!("{STYLE_BOLD}{STYLE_ITALIC}{STYLE_UNDERLINE}"); enable_ansi()?; - let _raw_terminal = RawModeHandler::new()?; + enable_raw_mode()?; println!("q to quit{}", move_cursor_to_column(0)); let next = |x: usize| (x + 1) % COLORS_FG.len(); @@ -43,5 +43,6 @@ fn main() -> io::Result<()> { } counter = next(counter); } + disable_raw_mode()?; Ok(()) } diff --git a/src/ansi.rs b/src/ansi.rs index 9473d6f..9c3cf0f 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -4,29 +4,12 @@ //! //! For these to always work on Windows you need to run the `enable_ansi` function inside this module -use std::io::{self, Write}; - #[cfg(unix)] pub use crate::unix::enable_ansi; #[cfg(windows)] pub use crate::windows::enable_ansi; -fn alt_screen(bool: bool) -> std::io::Result<()> { - if bool { - print!("{ALT_SCREEN_ENTER}"); - } else { - print!("{ALT_SCREEN_EXIT}"); - } - io::stdout().flush()?; - Ok(()) -} - -/// Creates a handler for the alt screen -pub fn alt_screen_handler() -> io::Result { - crate::Handler::new(&alt_screen) -} - /// Sets the terminal to an arbitrary 12-bit/truecolor color in the foreground when printed #[must_use] pub fn rgb_color_code_fg(red: u8, green: u8, blue: u8) -> String { diff --git a/src/control.rs b/src/control.rs index 333eb66..4a45d97 100644 --- a/src/control.rs +++ b/src/control.rs @@ -2,24 +2,8 @@ //! //! These are built to work on Windows, Linux, and MacOS -use std::io; - #[cfg(unix)] pub use crate::unix::{disable_raw_mode, enable_raw_mode, get_terminal_size}; #[cfg(windows)] pub use crate::windows::{disable_raw_mode, enable_raw_mode, get_terminal_size}; - -fn raw_mode(bool: bool) -> std::io::Result<()> { - if bool { - enable_raw_mode()?; - } else { - disable_raw_mode()?; - } - Ok(()) -} - -/// Creates a handler for raw mode -pub fn raw_mode_handler() -> io::Result { - crate::Handler::new(&raw_mode) -} diff --git a/src/lib.rs b/src/lib.rs index b32e9b7..5ba0a80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,47 +1,7 @@ #![warn(clippy::all, clippy::pedantic)] // this lint has way too many false positives #![allow(clippy::doc_markdown)] -//! This crate is a simple and minimal TUI library that supports the following OSes: -//! - Windows 10+ -//! - MacOS (currently untested) -//! - Linux -//! -//! ## Roadmap -//! - [x] Output (Unix) -//! - [x] Output (Windows) -//! - [x] Input (Unix) (Appears to work, more testing needed) -//! - [ ] Input (Windows) (WIP) -//! - [ ] Advanced Input (Kitty-like) -//! - [ ] Advanced Input (Windows) -//! - [ ] Events (Focus reporting, Bracketed-paste) (Unix) -//! - [ ] Events (Focus reporting, Bracketed-paste) (Windows) -//! - [ ] Mouse input (Unix) -//! - [ ] Mouse input (Windows) -//! - [ ] Feature completeness / API cleanup -//! -//! ## Support -//! This library generally attempts to have as much functionality as it can but sadly many terminal -//! emulators are heavily limited, there are a few protocols I have decided not to support but for -//! the most part this holds true -//! -//! ### Protocol Support -//! This is a list of terminal protocols and whether they will be supported, they still might not -//! work as this library is work in progress but eventually will be -//! -//! Just because a protocol is listed as not planned doesn't mean it definetly won't be added but -//! it is most likely not without good reason -//! - Standard Windows terminals (Full support planned)\* -//! - WinPTY (Windows psuedo-terminals) (Full support planned) -//! - Standard \*nix terminals (Full support planned)\* -//! - OSC 52 system clipboard (Full support planned) -//! - Kitty comprehensive keyboard handling (Full support planned) -//! - Kitty colored and styled underlines (Full support planned) -//! - Other Kitty protocols (there are a lot of them) (Not planned) -//! -//! \* Do not have full support for advanced input - -use std::io; - +#![doc = include_str!("../README.md")] #[cfg(unix)] mod unix; @@ -58,64 +18,3 @@ pub mod prelude { pub use crate::control::*; pub use crate::input::*; } - -/// Struct that calls `func(true)` on construction -/// and `func(false)` on destruction -pub struct Handler { - enabled: bool, - func: &'static dyn Fn(bool) -> io::Result<()>, -} - -impl Handler { - /// Creates a new instance and turns it on - /// - /// # Errors - /// - /// If the function errors - pub fn new(func: &'static dyn Fn(bool) -> io::Result<()>) -> io::Result { - let mut handler = Self { - enabled: true, - func, - }; - handler.set(true)?; - return Ok(handler); - } - /// Calls `func(true)` - /// - /// # Errors - /// - /// If the function errors - pub fn enable(&mut self) -> io::Result<()> { - self.set(true) - } - /// Calls `func(false)` - /// - /// # Errors - /// - /// If the function errors - pub fn disable(&mut self) -> io::Result<()> { - self.set(false) - } - /// Calls `func(set)` - /// - /// # Errors - /// - /// If the function errors - pub fn set(&mut self, set: bool) -> io::Result<()> { - if self.enabled != set { - (self.func)(set)?; - } - Ok(()) - } - /// Gets if it is enabled - #[must_use] - pub fn get(&self) -> bool { - self.enabled - } -} - -impl Drop for Handler { - fn drop(&mut self) { - self.disable().expect("Failed to disable terminal raw mode"); - } -} diff --git a/src/unix.rs b/src/unix.rs index 5b77e35..35892ad 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -10,7 +10,7 @@ unsafe extern "C" { fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; 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; + fn tcsetattr(fd: c_int, optional_actions: c_int, termios: *const Termios) -> c_int; } const STDIN_FILENO: c_int = 0; @@ -27,6 +27,8 @@ const TIOCGWINSZ: c_ulong = 0x4008_7468; #[cfg(target_os = "macos")] const NCCS: usize = 0x14; +const ERROR_MAGIC: i32 = 68905; + #[repr(C)] #[derive(Default, Debug, Clone, Copy)] struct Winsize { @@ -44,6 +46,10 @@ struct Termios { cflag: c_uint, lflag: c_uint, cc: [u8; NCCS], + #[cfg(target_os = "linux")] + ispeed: c_ulong, + #[cfg(target_os = "linux")] + ospeed: c_ulong, } fn get_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { @@ -60,10 +66,13 @@ fn set_attributes(fd: c_int, termios: &mut Termios) -> io::Result<()> { Ok(()) } -static TERMIOS: LazyLock> = LazyLock::new(|| { - let mut orig_termios = Termios::default(); - get_attributes(STDIN_FILENO, &mut orig_termios).ok()?; - Some(orig_termios) +static TERMIOS: LazyLock> = LazyLock::new(|| { + let mut orig_termios = unsafe { std::mem::zeroed() }; + let attributes = get_attributes(STDIN_FILENO, &mut orig_termios); + match attributes { + Ok(()) => Ok(orig_termios), + Err(e) => Err(e.raw_os_error().unwrap_or(ERROR_MAGIC)), + } }); /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization @@ -74,7 +83,12 @@ static TERMIOS: LazyLock> = LazyLock::new(|| { /// stdin is not a tty, /// 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"))?; + let mut termios = (*TERMIOS).map_err(|e| { + if e == ERROR_MAGIC { + return io::Error::other("Failed to get terminal properties"); + } + io::Error::from_raw_os_error(e) + })?; unsafe { cfmakeraw(&mut termios); } @@ -90,7 +104,12 @@ pub fn enable_raw_mode() -> io::Result<()> { /// 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"))?; + let mut termios = (*TERMIOS).map_err(|e| { + if e == ERROR_MAGIC { + return io::Error::other("Failed to get terminal properties"); + } + io::Error::from_raw_os_error(e) + })?; set_attributes(STDIN_FILENO, &mut termios)?; Ok(()) } @@ -257,6 +276,29 @@ where _ => Err(error), }, Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error), + Some(Ok(c)) => match c { + b'\r' => Ok(press_key(Key::Char('\r'), KeyMods::ALT)), + b'\n' => Ok(press_key(Key::Char('j'), KeyMods::CTRL.alt(true))), + b'\t' => Ok(press_key(Key::Char('\t'), KeyMods::ALT)), + b'\x7f' => Ok(press_key(Key::Backspace, KeyMods::ALT)), + b'\0' => Ok(press_key(Key::Char(' '), KeyMods::CTRL.alt(true))), + c @ b'\x01'..=b'\x1a' => Ok(press_key( + Key::Char((c + 96) as char), + KeyMods::CTRL.alt(true), + )), + c @ b'\x1c'..=b'\x1f' => Ok(press_key( + Key::Char((c + 24) as char), + KeyMods::CTRL.alt(true), + )), + c => { + let character = parse_utf8_char(c, iter)?; + Ok(Event::Key( + Key::Char(character), + KeyType::Press, + KeyMods::NONE.shift(character.is_uppercase()).alt(true), + )) + } + }, _ => Err(error), } }