@cleocode/animations
v2026.5.61
Published
CLEO terminal animations — braille spinners, progress bars, and sparks for the cleo CLI and CleoOS. Forked from gunnargray-dev/unicode-animations (MIT).
Maintainers
Readme
@cleocode/animations
Unicode terminal animations for the cleo CLI and CleoOS — woven on the LOOM, gated by LAFS.
Provides four primitive surfaces, each gated by a single AnimateContext so the
LAFS protocol invariant ("JSON output is the default; human rendering requires
explicit opt-in") holds uniformly:
- 18 generic braille spinners — frame-cycled loaders ported from
unicode-animations(MIT, Gunnar Gray) - 9 canon spinner aliases — workshop vocabulary (
looming,weaving,heartbeat, …) on the same frame data - 3 progress bar styles —
tapestry,cascade,refinery(canon-themed segmented gauges) - 4 sparks — one-shot accents (
awaken,sweep,cascade,weave)
Install
pnpm add @cleocode/animations
# or
npm install @cleocode/animationsESM-only. Requires Node ≥ 22.
Quick start
Spinner during async work — createSpinnerHandle
createSpinnerHandle is the canonical owner of \r writes for this package.
It manages the timer, hides/restores the cursor, installs an exit handler so
Ctrl-C doesn't leave a hidden cursor, and routes everything through the
LAFS render gate. Calling process.stdout.write of a string starting with
\r outside this package is a contract violation — always go through the
handle.
import { resolveOutputFormat } from '@cleocode/lafs';
import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';
const ctx = createAnimateContext({
flagResolution: resolveOutputFormat({ humanFlag: true }),
});
const spinner = createSpinnerHandle(ctx, 'looming', 'Weaving tasks…');
spinner.start();
try {
await doWork();
spinner.stop('✔ Tapestry complete.');
} catch (err) {
spinner.stop();
throw err;
}Under --json / --quiet / non-TTY / NO_COLOR the handle is a frozen no-op
and emits zero output — call sites stay branch-free.
Progress bar with a known ratio
import { renderProgressBar } from '@cleocode/animations';
function tick(done: number, total: number) {
const ratio = done / total;
const bar = renderProgressBar('refinery', ratio, 36);
process.stdout.write(`\r\x1B[2K ${bar} ${done}/${total}`);
}One-shot spark on success
import { sparks } from '@cleocode/animations';
async function playSpark(name: 'awaken' | 'sweep' | 'cascade' | 'weave') {
const { frames, interval } = sparks[name];
for (const f of frames) {
process.stdout.write(`\r\x1B[2K ${f}`);
await new Promise(r => setTimeout(r, interval));
}
process.stdout.write('\n');
}
await shipRelease();
await playSpark('cascade');LAFS-aware rendering — AnimateContext
Every primitive routes through an AnimateContext so the package obeys the
LAFS protocol uniformly. The context is pure data — no I/O, no timers —
derived from the LAFS FlagResolution plus environment signals.
import { resolveOutputFormat } from '@cleocode/lafs';
import { createAnimateContext, createSpinnerHandle } from '@cleocode/animations';
const flags = resolveOutputFormat({ humanFlag: true });
const ctx = createAnimateContext({ flagResolution: flags });
// Hand `ctx` to any primitive — they all become no-ops when `ctx.enabled === false`.
const spinner = createSpinnerHandle(ctx, 'looming', 'Loading…');
if (!ctx.enabled) {
// ctx.reason ∈ 'format-json' | 'quiet' | 'no-tty' | 'no-color' | 'enabled'
console.log(`silent: ${ctx.reason}`);
}| Signal | Source | Effect | reason |
|---|---|---|---|
| format !== 'human' | LAFS flags | Disable (machine output) | format-json |
| quiet === true | LAFS flags | Disable (script-friendly) | quiet |
| !isTTY | process.stdout.isTTY | Disable (piped/redirected) | no-tty |
| NO_COLOR set | process.env.NO_COLOR | Disable (no-color.org) | no-color |
SILENT_CONTEXT is exported as a frozen always-disabled context for tests and
libraries that want to opt out without constructing a full LAFS resolution.
Spinner registry
Generic — 18 braille loaders
| Name | Frames | Interval | Name | Frames | Interval |
|---|---|---|---|---|---|
| braille | 10 | 80ms | cascade | 14 | 60ms |
| braillewave | 8 | 100ms | columns | 26 | 60ms |
| dna | 12 | 80ms | orbit | 8 | 100ms |
| scan | 10 | 70ms | breathe | 17 | 100ms |
| rain | 12 | 100ms | waverows | 16 | 90ms |
| scanline | 6 | 120ms | checkerboard | 4 | 250ms |
| pulse | 5 | 180ms | helix | 16 | 80ms |
| snake | 16 | 80ms | fillsweep | 11 | 100ms |
| sparkle | 6 | 150ms | diagswipe | 16 | 60ms |
Canon — 9 workshop aliases
| Canon name | → Generic | Cleo lore role |
|---|---|---|
| looming | helix | Twin strands weaving — task on the LOOM |
| weaving | braillewave | Pattern threading across columns |
| heartbeat | breathe | Organic in-out pulse — Hearth presence |
| awakening | pulse | Radial bloom — first dream / cleo init |
| sweeping | scan | Left→right beam — BRAIN integrity Sweep |
| watching | orbit | Circular sentinel — sentient daemon tick |
| cascade | cascade | Diagonal fall — command-success accent |
| tapestry | waverows | Multi-row sinusoidal — wave of tasks shipping |
| refinery | columns | Filling stages — memory promotion pipeline |
Canon entries are aliases, not copies — they reference the same Spinner
objects as the generic registry. Renaming a generic spinner automatically
updates the canon view. The mapping is exposed as CANON_TO_GENERIC and
resolveSpinner(name) accepts either form.
Progress bars
renderProgressBar(style, ratio, width) returns a fixed-width string. Three
canon styles:
| Style | Characters | Feel |
|---|---|---|
| tapestry | ░ ▒ ▓ █ | Coarse blocks — woven cloth filling cell-by-cell |
| cascade | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █ | 1/8 gradient steps — smooth waterfall edge |
| refinery | ⠀ ⡀ ⡄ ⡆ ⡇ ⣇ ⣧ ⣷ ⣿ | Braille block stages — BRAIN memory promotion pipeline |
Inputs outside [0, 1] are clamped. width ≤ 0 returns ''.
Sparks — one-shot accents
import { sparks, sparkDurationMs } from '@cleocode/animations';
sparkDurationMs('cascade'); // → ~980ms (frames * interval)| Spark | Frames | Duration | Played on |
|---|---|---|---|
| awaken | 13 × 90ms | ~1.17s | cleo init · first dream · sentient wake |
| sweep | 7 × 80ms | ~560ms | BRAIN integrity sweep complete |
| cascade | 14 × 70ms | ~980ms | Release shipped · task complete |
| weave | 18 × 70ms | ~1.26s | Playbook stage transition · CANT directive accepted |
Browser demo
A self-contained vitrine page ships at scripts/demo.html. Open it in any
browser to preview every spinner, canon alias, progress style, and spark
animating live, with API reference tables and code samples.
# From an npm install
open node_modules/@cleocode/animations/scripts/demo.html # macOS
xdg-open node_modules/@cleocode/animations/scripts/demo.html # Linux
# From the cleo monorepo checkout
open packages/animations/scripts/demo.html # macOS
xdg-open packages/animations/scripts/demo.html # LinuxThe page is fully self-contained (no build step, no fetch, no node_modules
runtime requirement) so it can be emailed, dropped into a slide deck, or
hosted as a static asset for design reviews.
Terminal demo
npx cleocode-animations # cycle through generic + canon spinners
npx cleocode-animations looming # preview one spinner (generic OR canon)
npx cleocode-animations spark cascade # play one spark and exit
npx cleocode-animations progress # loop through all 3 progress styles
npx cleocode-animations --list # full listing — spinners + sparks + progress
npx cleocode-animations --list-canon # canon aliases only
npx cleocode-animations --list-sparks # sparks only
npx cleocode-animations --list-progress # progress styles onlyAPI surface
Spinners
| Export | Type |
|---|---|
| spinners | Record<BrailleSpinnerName, Spinner> |
| canonSpinners | Record<CanonSpinnerName, Spinner> |
| CANON_TO_GENERIC | Record<CanonSpinnerName, BrailleSpinnerName> |
| resolveSpinner(name) | (string) => Spinner \| undefined |
| gridToBraille(grid) | (boolean[][]) => string |
| makeGrid(rows, cols) | (number, number) => boolean[][] |
| Spinner | { frames: readonly string[]; interval: number } |
| BrailleSpinnerName · CanonSpinnerName | TS string-literal unions |
SpinnerHandle (canonical \r owner)
| Export | Type |
|---|---|
| createSpinnerHandle(ctx, name, label, options?) | (AnimateContext, name, string, SpinnerHandleOptions?) => SpinnerHandle |
| SpinnerHandle | { start(); stop(finalLine?); update(label); enabled: boolean } |
| SpinnerHandleOptions | { delayMs?: number } — defaults to 150 |
AnimateContext
| Export | Type |
|---|---|
| createAnimateContext(input) | (AnimateContextInput) => AnimateContext |
| SILENT_CONTEXT | Frozen AnimateContext — always disabled |
| AnimateContext | { enabled, reason, inputs } |
| AnimateContextInput | { flagResolution, isTTY?, noColor? } |
| FlagResolutionLike | { format: 'json' \| 'human'; quiet: boolean } |
Progress + Sparks
| Export | Type |
|---|---|
| progressBars | Record<ProgressBarStyle, ProgressBarRenderer> |
| renderProgressBar(style, ratio, width) | (style, number, number) => string |
| ProgressBarStyle | 'tapestry' \| 'cascade' \| 'refinery' |
| sparks | Record<SparkName, Spark> |
| sparkDurationMs(name) | (SparkName) => number |
| SparkName | 'awaken' \| 'sweep' \| 'cascade' \| 'weave' |
Custom spinners
Every animation here is built from two primitives — compose your own:
import { gridToBraille, makeGrid } from '@cleocode/animations';
const grid = makeGrid(4, 4);
grid[0][0] = true;
grid[1][1] = true;
grid[2][2] = true;
grid[3][3] = true;
console.log(gridToBraille(grid)); // diagonal braille patternmakeGrid(rows, cols) returns a boolean[][]. Set cells to true to raise
braille dots. gridToBraille(grid) packs them into a braille string (2 dot
columns per character, U+2800 base).
License
MIT — dual copyright:
- © 2024 Gunnar Gray (original
unicode-animationsproject) - © 2026 CLEO Code (
@cleocode/animationsfork)
See LICENSE.
