matrix_rain/lib.rs
1//! Classic "Matrix digital rain" effect for terminals, packaged as both a
2//! [ratatui](https://ratatui.rs/) [`StatefulWidget`](ratatui::widgets::StatefulWidget)
3//! library and a standalone `matrix` binary.
4//!
5//! The crate is published as `matrix-rain` on crates.io because the bare `matrix`
6//! name is taken; the installed binary is still called `matrix`.
7//!
8//! # Quick start
9//!
10//! Drop the widget into any ratatui layout. `MatrixRainState` carries the
11//! per-frame animation state (column streams, RNG, timing, cached color tier)
12//! across renders.
13//!
14//! ```
15//! use matrix_rain::{MatrixConfig, MatrixRain, MatrixRainState};
16//! use ratatui::buffer::Buffer;
17//! use ratatui::layout::Rect;
18//! use ratatui::widgets::StatefulWidget;
19//!
20//! let cfg = MatrixConfig::builder().fps(30).density(0.5).build().unwrap();
21//! let mut state = MatrixRainState::with_seed(42);
22//! let area = Rect::new(0, 0, 80, 24);
23//! let mut buf = Buffer::empty(area);
24//!
25//! MatrixRain::new(&cfg).render(area, &mut buf, &mut state);
26//! assert_eq!(state.streams_len(), 80);
27//! ```
28//!
29//! For a full embed inside a [`ratatui::Terminal`] event loop, see
30//! `examples/embedded.rs` in the source repo. For the standalone full-screen
31//! demo, see `examples/standalone.rs`.
32//!
33//! # Driving frames
34//!
35//! There are two ways to advance the animation:
36//!
37//! - **Wall-clock (default).** Each call to
38//! [`MatrixRain::render`](ratatui::widgets::StatefulWidget::render) reads
39//! `Instant::now()` internally and applies as many ticks as the elapsed time
40//! buys (capped at `MAX_CATCHUP_TICKS=4` so a process resumed from suspend
41//! doesn't render hundreds of frames at once). This is what `terminal.draw(…)`
42//! does naturally and what the bundled binary uses.
43//! - **Manual via [`MatrixRainState::tick`].** Each call advances exactly one
44//! frame regardless of wall-clock time. Useful for deterministic snapshot
45//! tests and external tick-loop apps.
46//!
47//! Mixing both modes in the same session produces visible drift; the snapshot
48//! suite suppresses wall-clock advance by setting `fps=1` together with a tiny
49//! `speed` (e.g. `0.001`) so the elapsed-time conversion floors to zero ticks
50//! per render. See [`MatrixRainState::set_color_count`] if you also need to
51//! lock the rendering tier for reproducibility.
52//!
53//! # Backends
54//!
55//! The library is backend-agnostic — `MatrixRain` renders into a ratatui
56//! [`Buffer`](ratatui::buffer::Buffer), so any ratatui backend works. Pick
57//! one via a feature flag:
58//!
59//! - `crossterm` (default; enabled by the `binary` feature)
60//! - `termion`
61//! - `termwiz`
62//!
63//! Each feature simply forwards to ratatui's same-named feature, so a single
64//! line in `Cargo.toml` covers both crates:
65//!
66//! ```toml
67//! matrix-rain = { version = "0.3", default-features = false, features = ["termion"] }
68//! ```
69//!
70//! The default `binary` feature pulls in `crossterm` (plus `clap`, `anyhow`,
71//! and `signal-hook`) for the standalone `matrix` binary. Library-only users
72//! who don't need the binary should opt out with `default-features = false`
73//! and pick exactly one backend feature.
74//!
75//! # `no_std` / embedded
76//!
77//! The library is `no_std`-capable (`alloc` is required for `Vec`-backed
78//! per-column streams; the `binary` feature, the wall-clock animation path,
79//! and env-var-based terminal capability detection all require `std`).
80//!
81//! To use the widget on a target without `std` (e.g. ESP32 with `esp-hal` +
82//! the `mousefood` embedded ratatui backend), opt out of every feature:
83//!
84//! ```toml
85//! matrix-rain = { version = "0.3", default-features = false }
86//! ratatui = { version = "0.30", default-features = false, features = ["underline-color"] }
87//! ```
88//!
89//! Embedded usage differs from desktop in three places:
90//!
91//! 1. **Construction.** [`MatrixRainState::new`] (which seeds from system
92//! entropy via `getrandom`) is unavailable; use
93//! [`MatrixRainState::with_seed`] with a seed of your choosing (e.g. an
94//! on-chip RNG, an HW counter, or just a constant).
95//! 2. **Driving frames.** Without `std`, [`MatrixRain::render`] handles
96//! resize and paints the current state but does **not** advance the
97//! animation — there is no `Instant::now()`. Drive frames manually with
98//! [`MatrixRainState::tick`] at whatever cadence your main loop dictates
99//! (e.g. once per `target_period - render_time`).
100//! 3. **Color tier.** Auto-detection (via `COLORTERM` / `TERM`) is disabled;
101//! call [`MatrixRainState::set_color_count`] once after construction to
102//! pick a tier. `16` is the safest default for an embedded display.
103//!
104//! # Color tiers
105//!
106//! Color depth is detected once per state on the first render via an env-var
107//! sniff: `COLORTERM=truecolor|24bit` (de-facto standard for advertising
108//! 24-bit support) wins; otherwise `TERM` is checked for `*256color*`.
109//! The result is cached on the state and drives one of three rendering paths:
110//!
111//! - **Truecolor**: linear RGB interpolation between the 5 stops in
112//! [`ColorRamp`]. Smoothest gradient.
113//! - **256-color**: nearest-of-5-stops; the terminal handles any further
114//! RGB→256 quantization.
115//! - **16-color**: 3-zone collapse (head, then `bright`/`mid`/`fade` zones)
116//! with each stop mapped to the nearest of the 16 named [`Color`] variants
117//! by euclidean RGB distance. Detection failure or any value the widget
118//! doesn't recognize also falls back to this path rather than panicking.
119//!
120//! Force a specific tier with [`MatrixRainState::set_color_count`]: pass `16`
121//! for accessibility, `256` for the quantized middle tier, or `u16::MAX` for
122//! the smooth-interpolation path.
123//!
124//! [`Color`]: ratatui::style::Color
125//!
126//! # Caveats
127//!
128//! - **Full-width and combining characters in [`CharSet::Custom`] are not
129//! detected.** Each glyph must occupy exactly one terminal cell or the
130//! column layout misaligns. CJK ideographs, emoji with variation selectors,
131//! and zero-width combiners are all single `char`s in Rust but multi-cell
132//! in the terminal. Display width cannot reliably be detected across
133//! terminals; verifying single-cell-ness is the caller's responsibility.
134//! - **Mixing [`MatrixRainState::tick`] with wall-clock rendering** produces
135//! visible drift over time. Tick driving is exact (each call advances
136//! exactly one frame); wall-clock driving advances based on elapsed
137//! [`Instant`](std::time::Instant)s. Pick one mode per session.
138//! - **16-color fallback is a 3-zone collapse**, not the original 5-stop
139//! gradient. If your theme has stops that map to the same named color
140//! (common with monochrome themes on 16-color), zones will visually merge.
141//! Use [`MatrixRainState::set_color_count`] to force a higher tier if your
142//! terminal actually supports it, or supply a [`Theme::Custom`] ramp whose
143//! stops are already in the named-color palette.
144//! - **Non-TTY refusal (binary only).** The standalone `matrix` binary exits
145//! with code 2 when stdout isn't a terminal so it doesn't garble logs when
146//! accidentally run under a pipe, in a CI runner, or as a systemd service.
147//! `--help` and `--version` still work in non-TTY contexts (they run before
148//! the check).
149
150#![cfg_attr(not(feature = "std"), no_std)]
151#![warn(missing_docs)]
152
153extern crate alloc;
154
155mod charset;
156mod config;
157mod error;
158mod state;
159mod stream;
160mod theme;
161mod widget;
162
163pub use charset::CharSet;
164pub use config::{MatrixConfig, MatrixConfigBuilder, MAX_TRAIL_LIMIT};
165pub use error::MatrixError;
166pub use state::MatrixRainState;
167pub use theme::{ColorRamp, Theme};
168pub use widget::MatrixRain;