Skip to content
Merged
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 {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Attr::to_ansi() was removed, which is a breaking change for any downstream code that used it (even though write_ansi remains). Consider keeping to_ansi as a thin wrapper around write_ansi and deprecating it, or ensure the crate versioning/docs clearly communicate the API removal.

Suggested change
impl Attr {
impl Attr {
/// Returns the ANSI escape sequence for this attribute as a `String`.
///
/// This method allocates a new `String`. Prefer [`write_ansi`] when you
/// want to write into an existing buffer to avoid allocations.
#[deprecated(
note = "use `Attr::write_ansi` instead to write into an existing buffer without allocating"
)]
pub fn to_ansi(&self) -> String {
let mut buf = String::new();
self.write_ansi(&mut buf);
buf
}

Copilot uses AI. Check for mistakes.
/// 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;
Comment on lines 18 to 21
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Color::to_ansi() was removed, which is a breaking change for downstream callers. If the goal is to avoid allocations, consider retaining to_ansi as a deprecated convenience wrapper around write_ansi so existing code keeps compiling while encouraging the more efficient API.

Copilot uses AI. Check for mistakes.
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"));
}
}
14 changes: 7 additions & 7 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
20 changes: 9 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,22 @@ 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;
/// A module for a rendering context.
#[cfg(target_os = "linux")]
pub(crate) mod render;
/// A module for handling terminal colors and attributes.
#[cfg(target_os = "linux")]
pub(crate) mod term;
Comment on lines +49 to +53
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

render and term were changed from public modules to pub(crate). This is a breaking change for downstream crates that import these modules/types (including the previously #[macro_export]ed csi! macro that lived under mod macros). If the intent is to make these internals, consider keeping a public compatibility layer (re-exports or deprecated items) or bumping the crate version appropriately and documenting the migration in the release notes/README.

Copilot uses AI. Check for mistakes.

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::*;
Comment on lines +61 to +62
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The change from pub use render::* / pub use term::* to pub(crate) use ... removes Cmd, Terminal, and render-related symbols from the public API surface. If external users are expected to control cursor/screen behavior or rendering directly, this will be a breaking API change; consider keeping the public re-exports (possibly with #[deprecated]) or introducing public wrapper APIs before making the internals crate-private.

Suggested change
pub(crate) use render::*;
pub(crate) use term::*;
pub use render::*;
pub use term::*;

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +62
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

render and term are #[cfg(target_os = "linux")], but the pub(crate) use render::*; / pub(crate) use term::*; re-exports are unconditional. This will fail to compile on non-Linux targets because those modules won't exist. Gate these re-exports with the same cfg (or a broader cfg that matches intended platform support).

Copilot uses AI. Check for mistakes.
16 changes: 0 additions & 16 deletions src/macros.rs

This file was deleted.

12 changes: 5 additions & 7 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,9 +64,7 @@ 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.
thread::yield_now(); // Yield to other threads if the back framebuffer is currently locked
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In the TryLockError::WouldBlock case, replacing the continue with only yield_now() changes the frame pacing behavior: the loop will proceed to the next iteration and may sleep for the remainder of rendering_rate even though the lock might become available sooner. This can reduce responsiveness under contention. Consider yielding and then retrying immediately (e.g., yield_now() + continue), or otherwise adjusting the frame-timer logic so lock contention doesn't introduce extra sleeps.

Suggested change
thread::yield_now(); // Yield to other threads if the back framebuffer is currently locked
thread::yield_now(); // Yield to other threads if the back framebuffer is currently locked
continue; // Retry immediately instead of proceeding with frame pacing logic

Copilot uses AI. Check for mistakes.
continue;
}
Comment on lines 66 to 69
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

On WouldBlock, the render loop now yield_now() and immediately continues without any sleep/backoff. Because Window::draw() holds the back_fb lock across an arbitrary user-provided closure, this can result in a hot retry loop and elevated CPU usage when drawing takes longer than a frame. Consider adding a small bounded backoff (e.g., short sleep/spin with exponential backoff) or restructuring so the render thread blocks more efficiently when the back buffer is contended.

Copilot uses AI. Check for mistakes.
Err(_) => {
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
46 changes: 21 additions & 25 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 @@ -11,8 +10,6 @@ use std::{
pub 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 Down Expand Up @@ -90,7 +87,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 @@ -102,7 +99,7 @@ impl Terminal {
/// Set the terminal to non-blocking mode
///
/// 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);
if flags == -1 {
Expand All @@ -121,26 +118,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
8 changes: 4 additions & 4 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ impl Drop for Window {
}

/// A listener for window size changes
pub struct WindowSizeListener {
struct WindowSizeListener {
/// The thread handle for the listener
handle: Option<thread::JoinHandle<()>>,
/// The stop signal for the listener
Expand All @@ -188,7 +188,7 @@ 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);
Expand Down Expand Up @@ -227,14 +227,14 @@ impl WindowSizeListener {
/// 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<dyn std::error::Error>> {
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
Loading