cellophane 0.1.0

A terminal animation framework for Rust
Documentation

Cellophane

A terminal animation framework for Rust. Implement one trait, get a complete rendering pipeline with frame diffing, resize handling, and input forwarding.

cellophane

Features

  • One trait — implement Animation and you're done
  • Frame diffing — only changed cells are redrawn each frame
  • ANSI parsingFrameBuilder parses ansi-styled text (including 24-bit color) into a cell grid via VTE
  • Resize handling — terminal resize events are detected and forwarded automatically
  • Input forwarding — key and mouse events are passed to your animation for interactive use
  • Terminal lifecycle — alternate screen, raw mode, cursor visibility, and cleanup on drop
  • Unicode supportGrapheme type handles multi-codepoint characters with stack allocation via SmallVec

Quick start

Add cellophane to your project:

cargo add cellophane

Implement the Animation trait and let Animator handle the rest:

use std::time::Duration;
use cellophane::{Animation, Animator, Frame, Cell};
use cellophane::crossterm::style::Color;

struct Rainbow {
    tick: usize,
    rows: usize,
    cols: usize,
}

impl Animation for Rainbow {
    fn init(&mut self, initial: Frame) {
        let (rows, cols) = initial.dims().unwrap_or((0, 0));
        self.rows = rows;
        self.cols = cols;
    }

    fn update(&mut self, _dt: Duration) -> Frame {
        let mut frame = Frame::with_capacity(self.cols, self.rows);
        for row in 0..self.rows {
            for col in 0..self.cols {
                let hue = ((col + row + self.tick) % 256) as u8;
                if let Some(cell) = frame.get_cell_mut(row, col) {
                    *cell = Cell::default()
                        .with_bg(Color::Rgb { r: hue, g: 100, b: 255 - hue });
                }
            }
        }
        self.tick += 1;
        frame
    }

    fn is_done(&self) -> bool { false }
    fn resize(&mut self, w: usize, h: usize) {
        self.cols = w;
        self.rows = h;
    }
}

fn main() -> std::io::Result<()> {
    let anim = Box::new(Rainbow { tick: 0, rows: 0, cols: 0 });
    let mut animator = Animator::enter_with(anim)?;
    loop {
        match animator.tick() {
            Ok(true) => continue,
            Ok(false) => break,
            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => break,
            Err(e) => return Err(e),
        }
    }
    Ok(())
}

Core types

Type Description
Animation The trait you implement to define an animation
Animator Drives your animation with a frame-rate-limited render loop
Frame A grid of styled Cells representing one frame of output
FrameBuilder Parses ANSI-escaped text into a Frame
Cell A single terminal cell with character, colors, and attributes
CellFlags Bitflags for text attributes (bold, italic, underline, etc.)
Grapheme A unicode grapheme cluster backed by SmallVec<[char; 4]>

How it works

The Animator manages the full terminal lifecycle:

  1. Enters the alternate screen, enables raw mode, hides the cursor
  2. Each tick() polls for events, calls your update(), and renders the frame
  3. Only cells that differ from the previous frame are written to the terminal
  4. Resize events call your resize(), input events call your on_event()
  5. Ctrl+C returns Err(ErrorKind::Interrupted) for clean shutdown
  6. Terminal state is restored automatically on drop

Built with cellophane

  • whoa — a terminal screensaver featuring EarthBound battle backgrounds, procedural simulations, cellular automata, and more