Fix release-mode segfault on *nix + add *nix alt support

This commit is contained in:
2025-08-06 07:40:18 -04:00
parent 0c0d5f15ef
commit df149ddfc8
6 changed files with 93 additions and 146 deletions

View File

@@ -1,5 +1,5 @@
# Neutuino # 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 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 (Unix)
- [x] Output (Windows) - [x] Output (Windows)
- [x] Input (Unix) (Appears to work, more testing needed) - [x] Input (Unix) (Appears to work, more testing needed)
- [x] Input (Windows) (Appears to work, more testing needed) - [ ] Input (Windows) (WIP)
- [ ] Input (Kitty) - [ ] Advanced Input (Kitty-like)
- [ ] Advanced Input (Windows)
- [ ] Events (Focus reporting, Bracketed-paste) (Unix) - [ ] Events (Focus reporting, Bracketed-paste) (Unix)
- [ ] Events (Focus reporting, Bracketed-paste) (Windows) - [ ] Events (Focus reporting, Bracketed-paste) (Windows)
- [ ] Mouse input (Unix) - [ ] Mouse input (Unix)
- [ ] Mouse input (Windows) - [ ] Mouse input (Windows)
- [ ] Feature completeness / API cleanup - [ ] 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

View File

@@ -14,7 +14,7 @@ fn main() -> io::Result<()> {
let all_styles = format!("{STYLE_BOLD}{STYLE_ITALIC}{STYLE_UNDERLINE}"); let all_styles = format!("{STYLE_BOLD}{STYLE_ITALIC}{STYLE_UNDERLINE}");
enable_ansi()?; enable_ansi()?;
let _raw_terminal = RawModeHandler::new()?; enable_raw_mode()?;
println!("q to quit{}", move_cursor_to_column(0)); println!("q to quit{}", move_cursor_to_column(0));
let next = |x: usize| (x + 1) % COLORS_FG.len(); let next = |x: usize| (x + 1) % COLORS_FG.len();
@@ -43,5 +43,6 @@ fn main() -> io::Result<()> {
} }
counter = next(counter); counter = next(counter);
} }
disable_raw_mode()?;
Ok(()) Ok(())
} }

View File

@@ -4,29 +4,12 @@
//! //!
//! For these to always work on Windows you need to run the `enable_ansi` function inside this module //! 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)] #[cfg(unix)]
pub use crate::unix::enable_ansi; pub use crate::unix::enable_ansi;
#[cfg(windows)] #[cfg(windows)]
pub use crate::windows::enable_ansi; 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> {
crate::Handler::new(&alt_screen)
}
/// 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

@@ -2,24 +2,8 @@
//! //!
//! These are built to work on Windows, Linux, and MacOS //! These are built to work on Windows, Linux, and MacOS
use std::io;
#[cfg(unix)] #[cfg(unix)]
pub use crate::unix::{disable_raw_mode, enable_raw_mode, get_terminal_size}; pub use crate::unix::{disable_raw_mode, enable_raw_mode, get_terminal_size};
#[cfg(windows)] #[cfg(windows)]
pub use crate::windows::{disable_raw_mode, enable_raw_mode, get_terminal_size}; 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> {
crate::Handler::new(&raw_mode)
}

View File

@@ -1,47 +1,7 @@
#![warn(clippy::all, clippy::pedantic)] #![warn(clippy::all, clippy::pedantic)]
// this lint has way too many false positives // this lint has way too many false positives
#![allow(clippy::doc_markdown)] #![allow(clippy::doc_markdown)]
//! This crate is a simple and minimal TUI library that supports the following OSes: #![doc = include_str!("../README.md")]
//! - 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;
#[cfg(unix)] #[cfg(unix)]
mod unix; mod unix;
@@ -58,64 +18,3 @@ pub mod prelude {
pub use crate::control::*; pub use crate::control::*;
pub use crate::input::*; 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<Self> {
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");
}
}

View File

@@ -10,7 +10,7 @@ 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;
fn cfmakeraw(termios: *mut Termios); fn cfmakeraw(termios: *mut Termios);
fn tcgetattr(fd: c_int, termios: *mut Termios) -> c_int; 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; const STDIN_FILENO: c_int = 0;
@@ -27,6 +27,8 @@ const TIOCGWINSZ: c_ulong = 0x4008_7468;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const NCCS: usize = 0x14; const NCCS: usize = 0x14;
const ERROR_MAGIC: i32 = 68905;
#[repr(C)] #[repr(C)]
#[derive(Default, Debug, Clone, Copy)] #[derive(Default, Debug, Clone, Copy)]
struct Winsize { struct Winsize {
@@ -44,6 +46,10 @@ struct Termios {
cflag: c_uint, cflag: c_uint,
lflag: c_uint, lflag: c_uint,
cc: [u8; NCCS], 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<()> { 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(()) Ok(())
} }
static TERMIOS: LazyLock<Option<Termios>> = LazyLock::new(|| { static TERMIOS: LazyLock<Result<Termios, i32>> = LazyLock::new(|| {
let mut orig_termios = Termios::default(); let mut orig_termios = unsafe { std::mem::zeroed() };
get_attributes(STDIN_FILENO, &mut orig_termios).ok()?; let attributes = get_attributes(STDIN_FILENO, &mut orig_termios);
Some(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 /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization
@@ -74,7 +83,12 @@ static TERMIOS: LazyLock<Option<Termios>> = LazyLock::new(|| {
/// 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 = (*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 { unsafe {
cfmakeraw(&mut termios); cfmakeraw(&mut termios);
} }
@@ -90,7 +104,12 @@ pub fn enable_raw_mode() -> io::Result<()> {
/// 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 = (*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)?; set_attributes(STDIN_FILENO, &mut termios)?;
Ok(()) Ok(())
} }
@@ -257,6 +276,29 @@ where
_ => Err(error), _ => Err(error),
}, },
Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(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), _ => Err(error),
} }
} }