diff --git a/Cargo.lock b/Cargo.lock index b971ee4..185ae32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "neutuino" -version = "0.2.0" +version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index e42e76a..4856d50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "neutuino" -version = "0.2.0" +version = "0.3.0" edition = "2024" repository = "https://github.com/Xyverle/neutuino" description = "A minimal zero-dependancy pure-rust cross-platform TUI library" diff --git a/README.md b/README.md index 51159c9..5eda69c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Neutuino Zero dependancy cross-platform pure-rust simple TUI library -Supported OSes: Windows 10+, MacOS (untested), Linux +Supported OSes: Windows 10+, MacOS (untested), and Linux This project is still highly work in progress and it will be a decent while until feature completeness -## Todo +## Roadmap - [x] Output (Unix) - [x] Output (Windows) - [x] Input (Unix) (Appears to work, more testing needed) diff --git a/examples/test.rs b/examples/input.rs similarity index 73% rename from examples/test.rs rename to examples/input.rs index b20ae81..26500c7 100644 --- a/examples/test.rs +++ b/examples/input.rs @@ -1,11 +1,6 @@ #![warn(clippy::all, clippy::pedantic)] -use neutuino::ansi::{ - COLORS_BG, COLORS_FG, STYLE_BOLD, STYLE_ITALIC, STYLE_RESET, STYLE_UNDERLINE, - move_cursor_to_column, set_window_title, -}; -use neutuino::input::{Event, KeyEvent, poll_input}; -use neutuino::os::{disable_raw_mode, enable_ansi, enable_raw_mode, get_terminal_size}; +use neutuino::prelude::*; use std::{io, time::Duration}; fn print_line_style_reset(string: &str) { @@ -16,13 +11,13 @@ fn main() -> io::Result<()> { let all_styles = format!("{STYLE_BOLD}{STYLE_ITALIC}{STYLE_UNDERLINE}"); enable_ansi()?; - enable_raw_mode()?; + let _raw_terminal = RawModeHandler::new()?; println!("q to quit{}", move_cursor_to_column(0)); let next = |x: usize| (x + 1) % COLORS_FG.len(); let terminal_size = get_terminal_size()?; - let terminal_size_str = format!("{terminal_size:?}"); + let terminal_size_str = format!("{:?}", terminal_size); print!("{}", set_window_title(terminal_size_str).unwrap()); let mut counter = 0; @@ -45,6 +40,5 @@ fn main() -> io::Result<()> { } counter = next(counter); } - disable_raw_mode()?; Ok(()) } diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..f7a705b --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,34 @@ +#![warn(clippy::all, clippy::pedantic)] + +use neutuino::prelude::*; +use std::{ + io::{self, Write}, + thread, time, +}; + +fn main() -> io::Result<()> { + enable_ansi()?; + + // makes the terminal raw until this value is dropped + let _raw_terminal = RawModeHandler::new()?; + let _alt_screen = AltScreenHandler::new()?; + + // gets the size of the terminal + let terminal_size = get_terminal_size()?; + let middle = (terminal_size.0 / 2, terminal_size.1 / 2); + + let string = "Hello, World!"; + + let adjusted_middle = (middle.0 - ((string.len() / 2) as u16), middle.1); + + print!( + "{COLOR_RED_BG}{}{string}", + move_cursor_to_position(adjusted_middle.0, adjusted_middle.1) + ); + io::stdout().flush()?; // VERY IMPORTANT! + + thread::sleep(time::Duration::new(3, 0)); + + // no flush needed here as the program is about to end and it will be auto flushed + Ok(()) +} diff --git a/src/ansi.rs b/src/ansi.rs index a66a657..fa661d1 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -4,21 +4,21 @@ //! //! For these to work on Windows you need to run the `enable_ansi` function in the os module -/// Sets the terminal to an arbitrary 12-bit/truecolor color +use std::io::{self, Write}; + +/// 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 { format!("\x1b[38;2;{red};{green};{blue}m") } -/// Sets the terminal to an arbitrary 12-bit/truecolor color +/// Sets the terminal to an arbitrary 12-bit/truecolor color in the background when printed #[must_use] pub fn rgb_color_code_bg(red: u8, green: u8, blue: u8) -> String { format!("\x1b[48;2;{red};{green};{blue}m") } -/// Sets the title of the window -/// -/// The title must be only in ASCII characters or **weird** things will happen +/// Sets the title of the window when printed #[must_use] pub fn set_window_title>(title: T) -> Option { let title = title.into(); @@ -28,31 +28,31 @@ pub fn set_window_title>(title: T) -> Option { Some(format!("\x1b]0;{title}\x1b\x5c")) } -/// Moves the cursor up {num} characters +/// Moves the cursor up {num} characters when printed #[must_use] pub fn move_cursor_up(num: u16) -> String { format!("\x1b[{num}A") } -/// Moves the cursor down {num} characters +/// Moves the cursor down {num} characters when printed #[must_use] pub fn move_cursor_down(num: u16) -> String { format!("\x1b[{num}B") } -/// Moves the cursor right {num} characters +/// Moves the cursor right {num} characters when printed #[must_use] pub fn move_cursor_right(num: u16) -> String { format!("\x1b[{num}C") } -/// Moves the cursor left {num} characters +/// Moves the cursor left {num} characters when printed #[must_use] pub fn move_cursor_left(num: u16) -> String { format!("\x1b[{num}A") } -/// Moves the cursor to {row} +/// Moves the cursor to {row} when printed /// /// Origin is 0, 0 #[must_use] @@ -60,7 +60,7 @@ pub fn move_cursor_to_row(line: u16) -> String { format!("\x1b[{}d", line.saturating_add(1)) } -/// Moves the cursor to {column} +/// Moves the cursor to {column} when printed /// /// Origin is 0, 0 #[must_use] @@ -68,7 +68,7 @@ pub fn move_cursor_to_column(column: u16) -> String { format!("\x1b[{}G", column.saturating_add(1)) } -/// Moves the cursor to Position {x}, {y} +/// Moves the cursor to Position {x}, {y} when printed /// /// Origin is 0, 0 #[must_use] @@ -250,3 +250,75 @@ pub const COLORS: [(&str, &str); 9] = [ (COLOR_WHITE_FG, COLOR_WHITE_BG), (COLOR_DEFAULT_FG, COLOR_DEFAULT_BG), ]; + +/// Struct that prints `ALT_SCREEN_ENTER` on construction +/// and `ALT_SCREEN_EXIT` on destruction +/// +/// Prefered over function as it prints `ALT_SCREEN_EXIT` on panic +pub struct AltScreenHandler { + enabled: bool, +} + +impl AltScreenHandler { + /// Creates a new instance and sets the terminal into the alternate screen + /// + /// # Errors + /// + /// If it fails to print or flush the output + pub fn new() -> io::Result { + print!("{ALT_SCREEN_ENTER}"); + io::stdout().flush()?; + Ok(Self { enabled: true }) + } + /// Enables raw mode + /// + /// # Errors + /// + /// Never errors if the alt screen is already enabled + /// + /// If it fails to print or flush the output + pub fn enable(&mut self) -> io::Result<()> { + self.set(true) + } + /// Disables raw mode + /// + /// # Errors + /// + /// Never errors if the alt screen is already disabled + /// + /// If it fails to print or flush the output + pub fn disable(&mut self) -> io::Result<()> { + self.set(false) + } + /// Sets raw mode + /// + /// # Errors + /// + /// Never errors if the alt screen is in the same state as the boolean + /// + /// If it fails to print or flush the output + pub fn set(&mut self, alt: bool) -> io::Result<()> { + if self.enabled == alt { + return Ok(()); + } + if alt { + print!("{ALT_SCREEN_ENTER}"); + } else { + print!("{ALT_SCREEN_EXIT}"); + } + io::stdout().flush()?; + self.enabled = alt; + Ok(()) + } + /// Gets if the alt screen is enabled + #[must_use] + pub fn get(&self) -> bool { + self.enabled + } +} + +impl Drop for AltScreenHandler { + fn drop(&mut self) { + self.disable().expect("Failed to disable alternate screen"); + } +} diff --git a/src/input.rs b/src/input.rs index e69de29..eeda390 100644 --- a/src/input.rs +++ b/src/input.rs @@ -0,0 +1,69 @@ +//! Various input functions, structs, etc. +//! +//! Very incomplete currently + +/// Different events that can happen through the terminal +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Event { + /// An event that happens upon a key being pressed + Key(KeyEvent), + /// An event that happens upon focus to the terminal window being gained + FocusGained, + /// An event that happens upon focus to the terminal window being lost + FocusLost, +} + +/// An event that happens upon a key being pressed +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum KeyEvent { + /// The Backspace key + Backspace, + /// The Up arrow key + Up, + /// The Down arrow key + Down, + /// The Left arrow key + Left, + /// The Right arrow key + Right, + /// The Home key + Home, + /// The End key + End, + /// The PageUp key + PageUp, + /// The PageDown key + PageDown, + /// The Tab key + Tab, + /// Shift + Tab key + ShiftTab, + /// The delete key + Delete, + /// The insert key + Insert, + /// The f1-f12 keys + F(u8), + /// Any character inputted by the keyboard + Char(char), + /// Ctrl + Char + Ctrl(char), + /// The Escape key + Escape, + /// A null byte sent to the terminal + /// + /// Can mean several different things + Null, +} + +impl From for Event { + fn from(value: KeyEvent) -> Self { + Self::Key(value) + } +} + +#[cfg(unix)] +pub use crate::unix::input::*; + +#[cfg(windows)] +pub use crate::windows::input::*; diff --git a/src/lib.rs b/src/lib.rs index f66aa58..fbd4a2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,62 +1,35 @@ #![warn(clippy::all, clippy::pedantic)] - -pub mod ansi; +// 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) +//! - [ ] Events (Focus reporting, Bracketed-paste) (Unix) +//! - [ ] Events (Focus reporting, Bracketed-paste) (Windows) +//! - [ ] Mouse input (Unix) +//! - [ ] Mouse input (Windows) +//! - [ ] Feature completeness / API cleanup #[cfg(unix)] mod unix; -#[cfg(unix)] -pub use crate::unix::*; - #[cfg(windows)] mod windows; -#[cfg(windows)] -pub use crate::windows::*; +pub mod ansi; +pub mod input; +pub mod os; -pub mod input { - //! Various input functions, structs, etc. - //! - //! Very incomplete currently - - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] - pub enum Event { - Key(KeyEvent), - FocusGained, - FocusLost, - } - - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] - pub enum KeyEvent { - Backspace, - Up, - Down, - Left, - Right, - Home, - End, - PageUp, - PageDown, - Tab, - BackTab, - Delete, - Insert, - F(u8), - Char(char), - Ctrl(char), - Escape, - Null, - } - - impl From for Event { - fn from(value: KeyEvent) -> Self { - Self::Key(value) - } - } - - #[cfg(unix)] - pub use crate::unix::input::*; - - #[cfg(windows)] - pub use crate::windows::input::*; +pub mod prelude { + //! Covenience re-export of common members + pub use crate::ansi::*; + pub use crate::input::*; + pub use crate::os::*; } diff --git a/src/os.rs b/src/os.rs index 51a7c01..9c2f623 100644 --- a/src/os.rs +++ b/src/os.rs @@ -1,5 +1,89 @@ //! Collection of functions that help control the terminal //! -//! These are built to work at least on these platforms: -//! Windows, Linux, and Mac, but are likely to work on more +//! These are built to work on Windows, Linux, and MacOS +use std::io; + +#[cfg(unix)] +pub use crate::unix::os::*; + +#[cfg(windows)] +pub use crate::windows::os::*; + +/// Struct that calls `enable_raw_mode` on construction +/// and `disable_raw_mode` on destruction +/// +/// Prefered over function as it calls `disable_raw_mode` on panic +pub struct RawModeHandler { + enabled: bool, +} + +impl RawModeHandler { + /// Creates a new instance and sets the terminal to raw mode + /// + /// # Errors + /// + /// If there is no stdin, + /// stdin is not a tty, + /// or it fails to change terminal settings + pub fn new() -> io::Result { + enable_raw_mode()?; + Ok(Self { enabled: true }) + } + /// Enables raw mode + /// + /// # Errors + /// + /// Never errors if raw mode is already enabled + /// + /// If there is no stdin, + /// stdin is not a tty, + /// or it fails to change terminal settings + pub fn enable(&mut self) -> io::Result<()> { + self.set(true) + } + /// Disables raw mode + /// + /// # Errors + /// + /// Never errors if raw mode is already disabled + /// + /// If there is no stdin, + /// stdin is not a tty, + /// or it fails to change terminal settings + pub fn disable(&mut self) -> io::Result<()> { + self.set(false) + } + /// Sets raw mode + /// + /// # Errors + /// + /// Never errors if raw mode is in the same state as the boolean + /// + /// If there is no stdin, + /// stdin is not a tty, + /// or it fails to change terminal settings + pub fn set(&mut self, raw: bool) -> io::Result<()> { + if self.enabled == raw { + return Ok(()); + } + if raw { + enable_raw_mode()?; + } else { + disable_raw_mode()?; + } + self.enabled = raw; + Ok(()) + } + /// Gets if raw mode is enabled + #[must_use] + pub fn get(&self) -> bool { + self.enabled + } +} + +impl Drop for RawModeHandler { + fn drop(&mut self) { + self.disable().expect("Failed to disable terminal raw mode"); + } +} diff --git a/src/unix.rs b/src/unix.rs index 3c494aa..3d31ca5 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -9,11 +9,11 @@ unsafe extern "C" { const STDIN_FILENO: c_int = 0; const STDOUT_FILENO: c_int = 1; -pub const POLLIN: c_short = 1; +const POLLIN: c_short = 1; -#[cfg(target_os = "linux")] +#[cfg(not(target_os = "macos"))] const TIOCGWINSZ: c_ulong = 0x5413; -#[cfg(target_os = "linux")] +#[cfg(not(target_os = "macos"))] const NCCS: usize = 0x20; #[cfg(target_os = "macos")] @@ -276,7 +276,7 @@ pub mod input { Some(Ok(b'B')) => Some(Event::Key(KeyEvent::Down)), Some(Ok(b'H')) => Some(Event::Key(KeyEvent::Home)), Some(Ok(b'F')) => Some(Event::Key(KeyEvent::End)), - Some(Ok(b'Z')) => Some(Event::Key(KeyEvent::BackTab)), + Some(Ok(b'Z')) => Some(Event::Key(KeyEvent::ShiftTab)), _ => None, } } diff --git a/src/windows.rs b/src/windows.rs index caf077c..f274b88 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -241,7 +241,7 @@ pub mod input { 0x08 => KeyEvent::Backspace, // VK_BACK 0x09 => { if shift { - KeyEvent::BackTab + KeyEvent::ShiftTab } else { KeyEvent::Tab }