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](https://github.com/user-attachments/assets/541ccc80-a115-4893-80a1-daa7278210ed)

## Features

- **One trait** — implement `Animation` and you're done
- **Frame diffing** — only changed cells are redrawn each frame
- **ANSI parsing**`FrameBuilder` 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 support**`Grapheme` type handles multi-codepoint characters with stack allocation via `SmallVec`

## Quick start

Add cellophane to your project:

```sh
cargo add cellophane
```

Implement the `Animation` trait and let `Animator` handle the rest:

```rust
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 `Cell`s 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]https://github.com/km-clay/whoa — a terminal screensaver featuring EarthBound battle backgrounds, procedural simulations, cellular automata, and more