Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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");
}
}
19 changes: 0 additions & 19 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
}
16 changes: 8 additions & 8 deletions src/framebuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -52,7 +52,7 @@ pub struct Framebuffer {
pub width: usize,
/// The height of the framebuffer.
pub height: usize,
buffer: Vec<Cell>,
buffer: Box<[Cell]>,
}

impl Framebuffer {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 8 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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::*;
16 changes: 0 additions & 16 deletions src/macros.rs

This file was deleted.

14 changes: 6 additions & 8 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<thread::JoinHandle<()>>,
/// The stop signal sender for the render thread.
Expand All @@ -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<Mutex<Framebuffer>>,
back_fb: Arc<Mutex<Framebuffer>>,
rendering_rate: time::Duration,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<f64, mpsc::TryRecvError> {
pub(crate) fn try_recv_fps(&self) -> Result<f64, mpsc::TryRecvError> {
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<dyn std::error::Error>> {
pub(crate) fn stop(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(tx) = self.stop_signal.take() {
tx.send(())?; // Send stop signal
}
Expand Down
67 changes: 31 additions & 36 deletions src/term.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -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 }
}
Expand All @@ -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();
Expand All @@ -64,20 +61,16 @@ 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

raw.control_flags
.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);
Comment on lines +72 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ECHONL flag is no longer explicitly removed from raw.local_flags. While ICANON is disabled, which usually makes ECHONL irrelevant, it's generally best practice in raw mode to explicitly disable all echoing to prevent any unexpected behavior or ensure consistent terminal state. Consider explicitly removing ECHONL if it's not intended to be active.

Suggested change
raw.local_flags
.remove(LocalFlags::ICANON | LocalFlags::ECHO | LocalFlags::ISIG | LocalFlags::IEXTEN);
raw.local_flags
.remove(LocalFlags::ICANON | LocalFlags::ECHONL | 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

Expand All @@ -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)?;
Expand All @@ -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());
}
Expand All @@ -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,
Expand Down
Loading