SkillAgentSearch skills...

Cellophane

A terminal animation framework for Rust

Install / Use

/learn @km-clay/Cellophane
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Cellophane

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

cellophane

<sub>Animation provided by whoa</sub>

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:

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

ratatui Integration

Enable with:

cargo add cellophane --features=ratatui

This feature flag exposes the AnimationWidget struct, which allows Frame to be usable in the ratatui rendering pipeline. AnimationWidget implements the Widget trait so it can be composed with other ratatui widgets as you would expect. Here's an example using the Block widget:

fn main() -> std::io::Result<()> {
	let mut anim = SomeAnimation::new();
	anim.init();

	ratatui::run(|terminal| {
			loop {
				terminal.draw(|f| {
					let chunks = Layout::default()
					.direction(Direction::Horizontal)
					.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
					.split(f.area());

					let block = Block::default().title("Animation").borders(ratatui::widgets::Borders::ALL);
					let block_inner = block.inner(chunks[0]);

					// resize the animation to match block_inner
					anim.resize(block_inner.width as usize, block_inner.height as usize);
					let anim_frame = anim.update(); // get the frame

					// render the block widget, and the animation frame inside of it
					f.render_widget(block, chunks[0]);
					f.render_widget(AnimationWidget::new(&anim_frame), block_inner);
				})?;

			if event::poll(Duration::from_millis(16))? {
				if event::read()?.is_key_press() {
					break Ok(());
				}
			}
		}
	})
}

Built with cellophane

  • whoa - a terminal screensaver featuring EarthBound battle backgrounds, procedural simulations, cellular automata, and more
View on GitHub
GitHub Stars48
CategoryDevelopment
Updated2h ago
Forks1

Languages

Rust

Security Score

90/100

Audited on Apr 8, 2026

No findings