mirror of
https://github.com/Xyverle/neutuino.git
synced 2026-06-26 22:23:14 -04:00
Fix release-mode segfault on *nix + add *nix alt support
This commit is contained in:
44
README.md
44
README.md
@@ -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
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/ansi.rs
17
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
|
//! 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 {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
103
src/lib.rs
103
src/lib.rs
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
56
src/unix.rs
56
src/unix.rs
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user