# Aisling
Embeddable terminal text effects and loader animations for Rust TUI applications.
Aisling renders animated text effects, spinners, and progress loaders as plain Rust data. It does not own your terminal, start an event loop, sleep, or print unless you run the included examples. Your app drives the iterator or asks for the current loader frame and maps each styled `Frame` into Ratatui, Crossterm, ANSI output, tests, logs, or any custom renderer.
The effect catalog includes embeddable terminal text animations plus loader/status animations for TUI applications.
## Install
After publishing:
```bash
cargo add aisling
```
Or in `Cargo.toml`:
```toml
[dependencies]
aisling = "0.3.17"
```
For local development from this repository:
```toml
[dependencies]
aisling = { path = "../aisling" }
```
## Quick Start
```rust
use aisling::{Effect, EffectConfig, EffectKind};
let config = EffectConfig::default()
.with_duration(80)
.with_seed(42);
let effect = Effect::with_config(EffectKind::Matrix, "Aisling", config);
for frame in effect.iter() {
// Map frame.cells() into your TUI buffer.
let plain = frame.to_plain_string();
let ansi = frame.to_ansi_string();
}
```
Progress loaders are also frame-based:
```rust
use aisling::{Loader, LoaderConfig, LoaderKind, LoaderProgress};
let loader = Loader::with_config(
LoaderKind::Tqdm,
LoaderConfig::default()
.with_width(64)
.with_label("download")
.with_unit("B")
.with_fraction(true),
);
let downloaded = 42;
let total = 100;
let frame = loader.frame(7, LoaderProgress::from_counts(downloaded, total));
let line = loader.line(7, LoaderProgress::from_counts(downloaded, total));
```
## TUI Integration
`Effect::iter()` yields `Frame` values. A `Frame` is a fixed-size grid with row-major `Cell` data.
Important types:
| Type | Purpose |
| --- | --- |
| `Effect` | Owns the effect kind, input text, and config. |
| `EffectKind` | Selects one of the built-in effects. |
| `EffectConfig` | Controls duration, seed, gradient, canvas size, color handling, and more. |
| `Loader` | Owns a loader kind and config; render with the current tick and progress. |
| `LoaderKind` | Selects a built-in spinner/progress/download animation. |
| `LoaderConfig` | Controls loader width, height, label, units, colors, and duration. |
| `LoaderProgress` | Carries a progress fraction or current/total counters. |
| `Frame` | Rendered 2D grid for a single animation tick. |
| `Cell` | Character plus foreground/background colors and simple styles. |
| `Color`, `Gradient`, `Easing` | Color and timing helpers. |
Minimal renderer loop shape:
```rust
use aisling::{Effect, EffectKind};
let effect = Effect::new(EffectKind::Wipe, "ship it");
for frame in effect.iter() {
for y in 0..frame.height() {
for x in 0..frame.width() {
let cell = frame.cell(x, y).unwrap();
// Write cell.ch and cell.colors into your TUI buffer here.
}
}
}
```
## Loaders
Loaders are for status lines, downloads, installs, background tasks, and tqdm-style progress indicators. Unlike text effects, real app progress usually comes from your own IO loop, so call `Loader::frame(tick, progress)` whenever your UI redraws.
```rust
use aisling::{Loader, LoaderKind, LoaderProgress};
let loader = Loader::new(LoaderKind::Download);
let tick = 12;
let frame = loader.frame(tick, LoaderProgress::from_counts(512, 1024));
```
Built-in loaders:
```text
bar, tqdm, blocks, meter, battery, gauge, dots, pulse, line, bounce,
scanner, snake, pong, marquee, steps, download, upload, equalizer,
wave, matrix, spark, orbit, brackets, crawl, braided, burn, decrypt,
rain, thunderstorm, laseretch, vhstape, blackhole, fireworks, pour,
synthgrid, beams, smoke, binarypath, bouncyballs, bubbles, colorshift,
crumble, errorcorrect, expand, highlight, middleout, orbittingvolley,
overflow, print, randomsequence, rings, scattered, slice, slide,
spotlights, spray, swarm, sweep, unstable, waves, wipe, ember,
cipher, glitch, vortex, stormflicker, lasersweep, cascade, gridpulse
```
The effect-inspired loaders use the same `Frame`/`Cell` model but are designed for long-running tasks: `burn` smolders across the progress line, `decrypt` cascades cipher characters downward into the status, `laseretch` scans a hot beam, `rings` orbits around the transfer cursor, `swarm` throws particles around the active edge, `thunderstorm` layers rain with lightning, and the newer `ember`, `cipher`, `glitch`, `vortex`, `stormflicker`, `lasersweep`, `cascade`, and `gridpulse` variants provide denser full-panel status motion.
For quick plain-line output, use `Loader::line(tick, progress)`. For TUI rendering, use `Loader::frame(tick, progress)` and map its cells into your buffer.
## Effects
All built-in effects are available through `EffectKind` and `EffectKind::all()`.
| Command | Enum |
| --- | --- |
| `beams` | `EffectKind::Beams` |
| `binarypath` | `EffectKind::BinaryPath` |
| `blackhole` | `EffectKind::Blackhole` |
| `bouncyballs` | `EffectKind::BouncyBalls` |
| `bubbles` | `EffectKind::Bubbles` |
| `burn` | `EffectKind::Burn` |
| `colorshift` | `EffectKind::ColorShift` |
| `crumble` | `EffectKind::Crumble` |
| `decrypt` | `EffectKind::Decrypt` |
| `errorcorrect` | `EffectKind::ErrorCorrect` |
| `expand` | `EffectKind::Expand` |
| `fireworks` | `EffectKind::Fireworks` |
| `highlight` | `EffectKind::Highlight` |
| `laseretch` | `EffectKind::LaserEtch` |
| `matrix` | `EffectKind::Matrix` |
| `middleout` | `EffectKind::MiddleOut` |
| `orbittingvolley` | `EffectKind::OrbittingVolley` |
| `overflow` | `EffectKind::Overflow` |
| `pour` | `EffectKind::Pour` |
| `print` | `EffectKind::Print` |
| `rain` | `EffectKind::Rain` |
| `randomsequence` | `EffectKind::RandomSequence` |
| `rings` | `EffectKind::Rings` |
| `scattered` | `EffectKind::Scattered` |
| `slice` | `EffectKind::Slice` |
| `slide` | `EffectKind::Slide` |
| `smoke` | `EffectKind::Smoke` |
| `spotlights` | `EffectKind::Spotlights` |
| `spray` | `EffectKind::Spray` |
| `swarm` | `EffectKind::Swarm` |
| `sweep` | `EffectKind::Sweep` |
| `synthgrid` | `EffectKind::SynthGrid` |
| `thunderstorm` | `EffectKind::Thunderstorm` |
| `unstable` | `EffectKind::Unstable` |
| `vhstape` | `EffectKind::VhsTape` |
| `waves` | `EffectKind::Waves` |
| `wipe` | `EffectKind::Wipe` |
String parsing is supported:
```rust
use std::str::FromStr;
use aisling::EffectKind;
let kind = EffectKind::from_str("laseretch").unwrap();
assert_eq!(kind, EffectKind::LaserEtch);
```
## Configuration
```rust
use aisling::{Color, EffectConfig, Easing, Gradient, GradientDirection};
let config = EffectConfig::default()
.with_duration(120)
.with_seed(9001)
.with_canvas_size(48, 8)
.with_gradient(
Gradient::new(
vec![Color::rgb(255, 170, 60), Color::rgb(80, 110, 95)],
24,
),
GradientDirection::Horizontal,
);
```
Notable `EffectConfig` fields:
| Field | Meaning |
| --- | --- |
| `duration` | Number of animation frames. |
| `hold_final_frames` | Extra frames that hold the final text. |
| `seed` | Deterministic random seed for repeatable playback. |
| `gradient` | Final/effect color gradient. |
| `gradient_direction` | Horizontal, vertical, diagonal, or radial mapping. |
| `easing` | Global easing used by many effect renderers. |
| `existing_color_handling` | Preserve or ignore parsed ANSI input colors. |
| `no_color` | Disable color output. |
| `canvas_width`, `canvas_height` | Optional fixed frame dimensions. |
| `tab_width` | Spaces per tab when parsing input text. |
## ANSI Input
Aisling parses common ANSI SGR sequences in input text, including foreground/background 3-bit, bright 3-bit, 8-bit xterm, and 24-bit RGB colors. Parsed styling is kept in each final `Cell` when the color-handling mode allows it.
```rust
use aisling::{Effect, EffectKind};
let effect = Effect::new(EffectKind::Print, "\x1b[38;2;255;80;80mred text\x1b[0m");
let final_frame = effect.final_frame();
assert_eq!(final_frame.to_plain_string(), "red text");
```
## Examples
Run a single effect in the terminal:
```bash
cargo run --example demo -- wipe "Hello from Aisling"
cargo run --example demo -- matrix "opencode style"
cargo run --example loaders -- tqdm --label download
cargo run --example loaders -- all --seconds 20 --fps 6
cargo run --example loader_lab -- --seconds 120
cargo run --example new_loaders -- --seconds 24 --fps 10
cargo run --example story -- --seconds 90 --fps 12
```
Use the helper script while developing locally:
```bash
./run.sh wipe "Hello from Aisling"
./run.sh matrix "opencode style"
./run.sh list
./run.sh all "Aisling"
./run.sh loader tqdm --label download
./run.sh loaders --seconds 20 --fps 6
./run.sh new-loaders --seconds 24
./run.sh list-loaders
./loaders.sh
./loaders.sh --seconds 120 --focus-seconds 8
./story.sh
./story.sh --seconds 90 --fps 12
./text_effects.sh
```
Run the multi-pane all-effects showcase:
```bash
./run.sh showcase
./run.sh showcase --rainbow
./run.sh showcase --theme industrial
./run.sh showcase --seconds 60
./run.sh showcase --forever
```
The showcase defaults to a curated high-contrast palette and a masonry pane layout. Wider/taller effects such as `matrix`, `blackhole`, `rain`, `thunderstorm`, and `vhstape` receive larger panes when the terminal has room. Use `--rainbow` for the original full-spectrum theme or `--industrial` for a steel/amber palette.
`test.sh` launches the showcase with a longer embedded text sample:
```bash
./test.sh
./test.sh 90
./test.sh --rainbow
./test.sh --industrial
./test.sh --industrial 90
./test.sh -loaders
```
`./run.sh loaders` and `./test.sh -loaders` show the full loader catalog in a grid so the loader styles can be compared at once. The `test.sh -loaders` path defaults to slower playback.
`loaders.sh` runs the full loader command deck. It keeps every loader visible in a catalog strip, gives one loader a large story-style focus pane, keeps six surrounding live mock transfer lanes running at different speeds, and supports arrow-key focus changes in a real terminal.
`story.sh` runs a full TUI-style Knott Dynamics narrative demo. It uses the embedded text from `test.sh`, then composes matrix, burn, thunderstorm, laser etch, rings, swarm, fireworks, fake downloads, and exotic loaders into one animated dashboard.
`text_effects.sh` reuses the `demo` effect example and runs the baseline text effect catalog at full terminal size, using the Knott Dynamics story text by default.
## Package Verification
Before publishing:
```bash
cargo fmt
cargo test
cargo package --allow-dirty
```
Publish when ready:
```bash
cargo publish
```
## License
MIT