matrix-rain 0.3.0

Classic Matrix digital rain effect as a ratatui widget (no_std-capable) and standalone TUI binary.
Documentation

matrix-rain

The classic Matrix digital rain effect — glowing columns of characters falling from the top of the terminal — packaged as both a ratatui StatefulWidget and a standalone matrix binary.

  • Smooth animation at 30–60 FPS, configurable density / speed / trail length.
  • Five built-in themes (classic green, amber, cyan, red, rainbow) and five charsets (matrix, ascii, hex, binary, or a UTF-8 file you supply).
  • Three rendering tiers — smooth RGB interpolation on truecolor terminals, quantized 5-stop ramp on 256-color, nearest-named collapse on 16-color — automatically detected and cached per state.
  • Drop-in widget that respects area.x / area.y offsets and handles resize, suspend/resume, broken-pipe shutdown, and panic-during-raw-mode without stranding the terminal.

Install

cargo install matrix-rain

Or from a local checkout:

cargo install --path .

The package is matrix-rain; the installed binary is matrix.

Run

matrix                          # classic green, 30 fps
matrix --theme rainbow          # red → yellow → green → blue trails
matrix --charset binary         # 0s and 1s
matrix --fps 60 --density 0.8   # snappier and denser
matrix --seed 42                # deterministic output (handy for screenshots)
matrix --help                   # full reference

Quit with q, Esc, or Ctrl-C (or use --quit-on-any-key). Press p to pause / resume the animation.

CLI options

Flag Default Description
-f, --fps <FPS> 30 Frames per second (must be ≥ 1)
-s, --speed <FLOAT> 1.0 Global speed multiplier (> 0, finite)
-d, --density <0..1> 0.6 Fraction of columns kept active
--charset <NAME|PATH> matrix matrix, ascii, hex, binary, or a path to a UTF-8 charset file (≤ 1 MiB)
--theme <NAME> green green, amber, cyan, red, rainbow
--no-head-white off Disable the classic white head; use the theme's "bright" color instead
--no-bold off Disable bold rendering on the head cell
--seed <U64> random Deterministic RNG seed (system entropy by default)
-q, --quit-on-any-key off Exit on any keypress instead of just q/Esc/Ctrl-C

Library usage

Add to Cargo.toml, picking your backend:

# crossterm (most common):
matrix-rain = { version = "0.3", default-features = false, features = ["crossterm"] }
ratatui = "0.30"

# termion:
matrix-rain = { version = "0.3", default-features = false, features = ["termion"] }
ratatui = { version = "0.30", default-features = false, features = ["termion"] }

# termwiz:
matrix-rain = { version = "0.3", default-features = false, features = ["termwiz"] }
ratatui = { version = "0.30", default-features = false, features = ["termwiz"] }

Disabling default features drops clap, anyhow, signal-hook, and the matrix binary itself — they're only used by the standalone installer. The crossterm / termion / termwiz features each forward to ratatui's same-named feature so one line covers both crates.

The library itself is backend-agnostic — MatrixRain renders into a ratatui::buffer::Buffer. The bundled examples/ use crossterm for their terminal-lifecycle scaffolding; embedders on other backends should treat them as templates and adapt the lifecycle to their backend.

Embed the widget inside a larger ratatui layout:

use ratatui::prelude::*;
use ratatui::widgets::Block;
use matrix_rain::{MatrixRain, MatrixRainState, MatrixConfig};

let cfg = MatrixConfig::builder()
    .fps(30)
    .density(0.5)
    .build()?;
let mut state = MatrixRainState::new();

terminal.draw(|f| {
    let chunks = Layout::vertical([Constraint::Min(0), Constraint::Length(3)])
        .split(f.size());
    f.render_stateful_widget(MatrixRain::new(&cfg), chunks[0], &mut state);
    f.render_widget(Block::bordered().title("Demo"), chunks[1]);
})?;

Runnable examples in examples/:

  • cargo run --example standalone — full-screen demo
  • cargo run --example embedded — widget inside a larger layout
  • cargo run --example custom_charsetCharSet::Custom with hand-picked glyphs

The widget is intentionally StatefulWidget-only. Animation needs state that persists across frames (MatrixRainState holds per-column streams, RNG, timing, and a cached color count); a stateless wrapper would either reset every frame or hide global mutable state behind the API.

Built-in character sets

CharSet Contents
Matrix (default) Half-width katakana U+FF66..=U+FF9D + digits 0-9 (66 glyphs)
Ascii Printable ASCII 0x21..=0x7E (94 glyphs, space excluded)
Hex 0-9 a-f (lowercase)
Binary 0 and 1
Custom(Vec<char>) User-supplied glyphs. Builder rejects empty / control chars.

Built-in themes

Theme Look
ClassicGreen (default) White head over green trail (0xCCFFCC → 0x00FF00 → 0x009900 → 0x003300)
Amber Phosphor amber
Cyan Cyan glow
Red Hostile red
Rainbow White head → red → yellow → green → blue across the trail
Custom(ColorRamp) Hand-built 5-stop ramp

no_std / embedded (ESP32, etc.)

The library is no_std-capable (requires alloc). Drop the std-dependent pieces — wall-clock animation, env-var-based color detection, entropy-seeded constructor, and the standalone binary — by opting out of every feature:

matrix-rain = { version = "0.3", default-features = false }
ratatui    = { version = "0.30", default-features = false, features = ["underline-color"] }

Then pick a ratatui backend appropriate for your target (e.g. mousefood for embedded-graphics displays). Three differences from the desktop story:

  1. Construct with with_seed. MatrixRainState::new() is std-only (it pulls entropy via getrandom); embedded callers supply a seed from wherever they please.
  2. Drive frames manually. Without std there is no Instant::now(), so MatrixRain::render only handles resize and paints the current state. Call state.tick() once per frame at your loop's cadence — typically target_period - render_time measured with esp_hal::time::Instant or similar.
  3. Set the color tier explicitly. Env-var sniffing is gone; call state.set_color_count(16) (or 256, or u16::MAX for smooth RGB) once at startup.

Note on riscv32imc (ESP32-C3 / no-atomics): ratatui's layout solver needs atomic CAS, so on chips without it you must enable ratatui's portable-atomic feature and configure the polyfill per its docs. ESP32-C6 / -H2 (riscv32imac) have native atomics and work out of the box.

Pause / resume

state.pause();   // freezes wall-clock-driven advance; render still paints
state.resume();  // next render is treated as a first render (no catch-up stutter)
state.is_paused();

tick() bypasses pause — manual driving always advances. reset() clears the paused flag. The binary toggles pause on the p key.

Determinism

Pass --seed <U64> (or use MatrixRainState::with_seed) to get a reproducible animation. The library's snapshot test suite drives frames purely via state.tick() so output is identical across runs given the same seed, area, and config.

MatrixRainState::set_color_count(count) overrides the cached terminal color count, letting you force a specific rendering tier (e.g. 16 for accessibility, u16::MAX to force smooth interpolation, or any value in tests where TERM/COLORTERM shouldn't matter).

Caveats

  • Full-width and combining characters in custom charsets are not detected. Each glyph must occupy exactly one terminal cell or the column layout misaligns. CJK ideographs, emoji with variation selectors, and zero-width combiners are all single chars in Rust but multi-cell in the terminal. Display width can't reliably be detected across terminals; verifying single-cell-ness is the caller's responsibility.
  • Mixing MatrixRainState::tick() with wall-clock rendering produces visible drift over time. Tick driving is exact (each call advances exactly one frame); wall-clock driving advances based on elapsed Instants. Pick one mode per session.
  • Non-TTY refusal. The binary exits with code 2 when stdout isn't a terminal so it doesn't garble logs when accidentally run under a pipe, in a CI runner, or as a systemd service. --help and --version still work in non-TTY contexts (they run before the check).

License

MIT. See LICENSE (or Cargo.toml's license field) for details.