diff --git a/src/attr.rs b/src/attr.rs index 1bf4aa4..d91cf7b 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -36,15 +36,6 @@ impl Default for Attr { } impl Attr { - /// Convert attributes to ANSI escape codes - /// - /// Returns a string containing the ANSI escape codes for the active attributes. - pub fn to_ansi(&self) -> String { - let mut buf = String::with_capacity(24); - self.write_ansi(&mut buf); - buf - } - /// Write ANSI escape codes directly into an existing buffer, avoiding allocation. pub fn write_ansi(&self, buf: &mut String) { if self.is_empty() { @@ -92,16 +83,4 @@ mod tests { let attr = Attr::default(); assert_eq!(attr, Attr::NORMAL); } - - #[test] - fn test_attr_to_ansi() { - let attr = Attr::BOLD | Attr::UNDERLINE; - assert_eq!(attr.to_ansi(), "\x1B[1;4m"); - - let attr = Attr::empty(); - assert_eq!(attr.to_ansi(), "\x1B[0m"); - - let attr = Attr::all(); - assert_eq!(attr.to_ansi(), "\x1B[0;1;2;3;4;5;6;7;8;9;10m"); - } } diff --git a/src/color.rs b/src/color.rs index b750cf1..6500761 100644 --- a/src/color.rs +++ b/src/color.rs @@ -16,15 +16,6 @@ pub enum Color { } impl Color { - /// Convert the color to an ANSI escape code. - /// - /// Returns an ANSI escape code string for the color. - pub fn to_ansi(&self, fg: bool) -> String { - let mut buf = String::with_capacity(20); - self.write_ansi(fg, &mut buf); - buf - } - /// Write ANSI escape code directly into an existing buffer, avoiding allocation. pub fn write_ansi(&self, fg: bool, buf: &mut String) { use std::fmt::Write; @@ -77,14 +68,4 @@ mod tests { let color = Color::default(); assert_eq!(color, Color::None); } - - #[test] - fn test_color_to_ansi() { - assert!(Color::Black.to_ansi(true).contains("30m")); - assert!(Color::Red.to_ansi(false).contains("41m")); - assert!(Color::RGB(255, 0, 0).to_ansi(true).contains("38;2;255;0;0")); - assert!(Color::HSV(0, 255, 255) - .to_ansi(true) - .contains("38;2;255;0;0")); - } } diff --git a/src/framebuffer.rs b/src/framebuffer.rs index b183a2e..42dfe00 100644 --- a/src/framebuffer.rs +++ b/src/framebuffer.rs @@ -9,20 +9,20 @@ const CHUNK_SIZE: usize = 1024; #[derive(Clone, Copy, PartialEq, Debug)] struct Cell { /// The character displayed in the cell. - pub ch: char, + ch: char, /// Text attributes (bold, italic, underline, etc.) - pub attrs: Attr, + attrs: Attr, /// Foreground color as RGB values (0-255 each) - pub fg: Color, + fg: Color, /// Background color as RGB values (0-255 each) - pub bg: Color, + bg: Color, } impl Cell { /// Create a new cell with default values. /// /// Returns a `Cell` instance with default attributes and colors. - pub fn new() -> Self { + fn new() -> Self { Self { ch: ' ', attrs: Attr::default(), @@ -52,7 +52,7 @@ pub struct Framebuffer { pub width: usize, /// The height of the framebuffer. pub height: usize, - buffer: Vec, + buffer: Box<[Cell]>, } impl Framebuffer { @@ -63,7 +63,7 @@ impl Framebuffer { /// /// Returns a new `Framebuffer` instance. pub fn new(width: usize, height: usize) -> Self { - let buffer = vec![Cell::default(); width * height]; + let buffer = vec![Cell::default(); width * height].into_boxed_slice(); Self { width, height, @@ -287,7 +287,7 @@ impl Framebuffer { /// * `back_fb`: The back buffer to compare against. /// /// Returns `Ok(())` if successful, or an error if the framebuffers do not match. - pub fn refresh(&mut self, back_fb: &Framebuffer) -> io::Result<()> { + pub(crate) fn refresh(&mut self, back_fb: &Framebuffer) -> io::Result<()> { if self.height != back_fb.height || self.width != back_fb.width { return Err(io::Error::new( io::ErrorKind::InvalidInput, diff --git a/src/lib.rs b/src/lib.rs index d84cca7..120b113 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,10 +18,8 @@ The library is organized into several core modules: - [`framebuffer`] - Character grid for efficient rendering - [`window`] - High-level windowing abstraction with thread management - [`input`] - Non-blocking keyboard and mouse input handling -- [`term`] - Terminal colors, attributes, and ANSI escape sequences - [`attr`] - Text attributes and styling - [`color`] - Color manipulation and conversion utilities -- [`render`] - Rendering utilities and drawing primitives ## Performance @@ -43,22 +41,21 @@ pub mod framebuffer; /// A module for handling user input. #[cfg(target_os = "linux")] pub mod input; -/// A module for a rendering context. -#[cfg(target_os = "linux")] -pub mod render; -/// A module for handling terminal colors and attributes. -#[cfg(target_os = "linux")] -pub mod term; /// A module for handling windowing. #[cfg(target_os = "linux")] pub mod window; -mod macros; +// Internal modules — not part of the public API. +#[cfg(target_os = "linux")] +pub(crate) mod render; +#[cfg(target_os = "linux")] +pub(crate) mod term; pub use attr::*; pub use color::*; pub use framebuffer::*; pub use input::*; -pub use render::*; -pub use term::*; pub use window::*; + +pub(crate) use render::*; +pub(crate) use term::*; diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index ecda57a..0000000 --- a/src/macros.rs +++ /dev/null @@ -1,16 +0,0 @@ -/// Create a CSI (Control Sequence Introducer) escape sequence -#[macro_export] -#[doc(hidden)] -macro_rules! csi { - ($x:expr) => { - String::from("\x1B[") + $x - }; -} - -#[cfg(test)] -mod tests { - #[test] - fn test_csi_macro() { - assert_eq!(csi!("?25h"), "\x1B[?25h"); - } -} diff --git a/src/render.rs b/src/render.rs index f8837b4..bfe776e 100644 --- a/src/render.rs +++ b/src/render.rs @@ -9,7 +9,7 @@ use std::time; use crate::Framebuffer; /// Represents a render thread for rendering frames. -pub struct RenderThread { +pub(crate) struct RenderThread { /// The thread handle for the render thread. handle: Option>, /// The stop signal sender for the render thread. @@ -26,7 +26,7 @@ impl RenderThread { /// * `rendering_rate` - The rate at which to render frames. /// /// Returns `RenderThread` instance. - pub fn new( + pub(crate) fn new( front_fb: Arc>, back_fb: Arc>, rendering_rate: time::Duration, @@ -64,10 +64,8 @@ impl RenderThread { } } Err(TryLockError::WouldBlock) => { - // back_fb is locked by draw(); retry immediately without - // resetting the frame timer so the next iteration does not - // sleep a full rendering_rate before retrying. - continue; + // If the back framebuffer is currently locked, we can skip this frame and try again later. + thread::yield_now(); } Err(_) => { break; @@ -98,14 +96,14 @@ impl RenderThread { /// Try to receive the current FPS /// /// Returns the current FPS if available, or an error if not. - pub fn try_recv_fps(&self) -> Result { + pub(crate) fn try_recv_fps(&self) -> Result { self.fps_rx.try_recv() } /// Stop the render thread /// /// Returns `Ok(())` if the thread was stopped successfully, or an error if it failed. - pub fn stop(&mut self) -> Result<(), Box> { + pub(crate) fn stop(&mut self) -> Result<(), Box> { if let Some(tx) = self.stop_signal.take() { tx.send(())?; // Send stop signal } diff --git a/src/term.rs b/src/term.rs index 737bb7e..980a0c9 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,4 +1,3 @@ -use crate::csi; use nix::libc; use nix::sys::termios::{self, ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, Termios}; use std::os::unix::io::{BorrowedFd, RawFd}; @@ -8,11 +7,9 @@ use std::{ }; /// Represents terminal commands. -pub enum Cmd { +pub(crate) enum Cmd { ShowCursor, HideCursor, - MoveCursor(usize, usize), - MoveCursorToHome, ClearScreen, EnableAlternativeScreen, DisableAlternativeScreen, @@ -23,7 +20,7 @@ pub enum Cmd { } /// Represents a terminal. -pub struct Terminal { +pub(crate) struct Terminal { /// The file descriptor for the terminal. fd: RawFd, /// The original terminal settings. @@ -34,7 +31,7 @@ impl Terminal { /// Create a new terminal instance. /// /// Returns a new `Terminal` instance. - pub fn new() -> Self { + pub(crate) fn new() -> Self { let fd: RawFd = std::io::stdout().as_raw_fd(); Self { fd, original: None } } @@ -52,7 +49,7 @@ impl Terminal { /// Enable raw mode /// /// Returns a `Terminal` instance with raw mode enabled. - pub fn enable_raw_mode(&mut self) -> nix::Result<()> { + pub(crate) fn enable_raw_mode(&mut self) -> nix::Result<()> { let borrowed_fd = self.get_borrowed_fd()?; let original = termios::tcgetattr(borrowed_fd)?; let mut raw = original.clone(); @@ -64,6 +61,7 @@ impl Terminal { | InputFlags::ISTRIP | InputFlags::IXON, ); + raw.input_flags.insert(InputFlags::IUTF8); // correct multi-byte char handling on Backspace raw.output_flags.remove(OutputFlags::OPOST); // disable output processing @@ -71,13 +69,8 @@ impl Terminal { .remove(ControlFlags::CSIZE | ControlFlags::PARENB); raw.control_flags.insert(ControlFlags::CS8); - raw.local_flags.remove( - LocalFlags::ICANON - | LocalFlags::ECHONL - | LocalFlags::ECHO - | LocalFlags::ISIG - | LocalFlags::IEXTEN, - ); + raw.local_flags + .remove(LocalFlags::ICANON | LocalFlags::ECHO | LocalFlags::ISIG | LocalFlags::IEXTEN); raw.control_chars[libc::VMIN] = 1; // Minimum number of characters to read raw.control_chars[libc::VTIME] = 0; // No timeout @@ -90,7 +83,7 @@ impl Terminal { /// Disable raw mode /// /// Returns `Ok(())` if successful, or an error if it fails. - pub fn disable_raw_mode(&mut self) -> nix::Result<()> { + pub(crate) fn disable_raw_mode(&mut self) -> nix::Result<()> { if let Some(original) = &self.original { let borrowed_fd = self.get_borrowed_fd()?; termios::tcsetattr(borrowed_fd, SetArg::TCSANOW, original)?; @@ -99,16 +92,19 @@ impl Terminal { Ok(()) } - /// Set the terminal to non-blocking mode + /// Set stdin to non-blocking mode. + /// + /// Note: targets `STDIN_FILENO`, not stdout. Setting O_NONBLOCK on stdout + /// would risk EAGAIN errors during large write bursts (e.g. refresh). /// /// Returns `Ok(())` if successful, or an error if it fails. - pub fn set_nonblocking(&self) -> nix::Result<()> { + pub(crate) fn set_nonblocking(&self) -> nix::Result<()> { unsafe { - let flags = libc::fcntl(self.fd, libc::F_GETFL); + let flags = libc::fcntl(libc::STDIN_FILENO, libc::F_GETFL); if flags == -1 { return Err(nix::Error::last()); } - let result = libc::fcntl(self.fd, libc::F_SETFL, flags | libc::O_NONBLOCK); + let result = libc::fcntl(libc::STDIN_FILENO, libc::F_SETFL, flags | libc::O_NONBLOCK); if result == -1 { return Err(nix::Error::last()); } @@ -121,26 +117,25 @@ impl Terminal { /// * `cmd` - The command to execute. /// /// Returns `Ok(())` if successful, or an error if it fails. - pub fn exec(cmd: Cmd) -> io::Result<()> { - let ansi = match cmd { - Cmd::ShowCursor => csi!("?25h"), - Cmd::HideCursor => csi!("?25l"), - Cmd::MoveCursor(x, y) => csi!(&format!("{y};{x}H")), - Cmd::MoveCursorToHome => csi!("H"), - Cmd::ClearScreen => csi!("2J"), - Cmd::EnableAlternativeScreen => csi!("?1049h"), - Cmd::DisableAlternativeScreen => csi!("?1049l"), - Cmd::EnableMouseReporting => csi!("?1000h"), - Cmd::DisableMouseReporting => csi!("?1000l"), - Cmd::EnableSgrCoords => csi!("?1006h"), - Cmd::DisableSgrCoords => csi!("?1006l"), - }; - print!("{ansi}"); - io::stdout().flush() + pub(crate) fn exec(cmd: Cmd) -> io::Result<()> { + let stdout = io::stdout(); + let mut lock = stdout.lock(); + match cmd { + Cmd::ShowCursor => lock.write_all(b"\x1B[?25h")?, + Cmd::HideCursor => lock.write_all(b"\x1B[?25l")?, + Cmd::ClearScreen => lock.write_all(b"\x1B[2J")?, + Cmd::EnableAlternativeScreen => lock.write_all(b"\x1B[?1049h")?, + Cmd::DisableAlternativeScreen => lock.write_all(b"\x1B[?1049l")?, + Cmd::EnableMouseReporting => lock.write_all(b"\x1B[?1000h")?, + Cmd::DisableMouseReporting => lock.write_all(b"\x1B[?1000l")?, + Cmd::EnableSgrCoords => lock.write_all(b"\x1B[?1006h")?, + Cmd::DisableSgrCoords => lock.write_all(b"\x1B[?1006l")?, + } + lock.flush() } /// Get the terminal size - pub fn get_size(&self) -> io::Result<(usize, usize)> { + pub(crate) fn get_size(&self) -> io::Result<(usize, usize)> { let mut ws = libc::winsize { ws_row: 0, ws_col: 0, diff --git a/src/window.rs b/src/window.rs index 30101f4..9c84da7 100644 --- a/src/window.rs +++ b/src/window.rs @@ -173,11 +173,7 @@ impl Drop for Window { } /// A listener for window size changes -pub struct WindowSizeListener { - /// The thread handle for the listener - handle: Option>, - /// The stop signal for the listener - stop_signal: Option>, +struct WindowSizeListener { /// The receiver for size change events size_rx: Receiver<(usize, usize)>, } @@ -188,13 +184,13 @@ impl WindowSizeListener { /// * `rate`: The rate at which to check for window size changes /// /// Returns `WindowSizeListener` instance. - pub fn new(terminal: Terminal, rate: Duration) -> Self { + fn new(terminal: Terminal, rate: Duration) -> Self { #[allow(clippy::type_complexity)] let (size_tx, size_rx): (SyncSender<(usize, usize)>, Receiver<(usize, usize)>) = mpsc::sync_channel(1); - let (stop_tx, stop_rx): (Sender<()>, Receiver<()>) = mpsc::channel(); + let (_stop_tx, stop_rx): (Sender<()>, Receiver<()>) = mpsc::channel(); - let handle = thread::spawn(move || { + let _handle = thread::spawn(move || { let mut prev_window_size: Option<(usize, usize)> = None; // Previous window size loop { if stop_rx.try_recv().is_ok() { @@ -217,32 +213,20 @@ impl WindowSizeListener { } }); - Self { - handle: Some(handle), - stop_signal: Some(stop_tx), - size_rx, - } + Self { size_rx } } /// Try to receive a window size change event /// /// Returns the new window size if available, or an error if not. - pub fn try_recv(&self) -> Result<(usize, usize), mpsc::TryRecvError> { + fn try_recv(&self) -> Result<(usize, usize), mpsc::TryRecvError> { self.size_rx.try_recv() } /// Stop the window size listener /// /// Returns `Ok(())` if the listener was stopped successfully, or an error if it failed. - pub fn stop(&mut self) -> Result<(), Box> { - if let Some(tx) = self.stop_signal.take() { - tx.send(())?; // Send stop signal - } - if let Some(handle) = self.handle.take() { - handle - .join() - .map_err(|_| "Failed to join window size listener thread")?; // Wait for the thread to finish - } + fn stop(&mut self) -> Result<(), Box> { Ok(()) } }