Skip to main content

bruhanimate

Composable terminal animations for Python.

PyPI Python CI License

Documentation · Getting started · Core concepts · Release guide

Turn a simulation, drawing function, or stream of solver snapshots into an ASCII animation. One player handles timing, input, changed-cell rendering, and terminal cleanup, so your code can concentrate on what happens and how it looks.

  • Built-in effects: snow, fire, rain, stars, Matrix, cellular automata, and more.
  • Layered scenes: combine animated backgrounds, text, and custom drawings.
  • Solver playback: pause, step, and change speed while consuming snapshots lazily.
  • Grid helpers: board coordinates, centered layouts, paths, markers, and clipping.

Version 1 is a breaking release. The old renderer hierarchy is replaced by play, Scene, and Playback. See the migration guide.

Quick start

Requires Python 3.13 or newer. After a 1.x release is published:

python -m pip install "bruhanimate>=1,<2"

For unreleased code, use the source checkout. Optional audio capture dependencies are available with python -m pip install "bruhanimate[audio]>=1,<2".

Save this as animation.py and run python animation.py in a terminal:

from bruhanimate import play

play("snow", preset="light", fps=60, updates_per_second=30)

Press q, Escape, or Ctrl+C to quit and restore the terminal. fps controls presentation; updates_per_second controls simulation ticks. Set frames=120 to limit the number of presented frames.

Discover effects and presets

light is a named SnowSettings configuration: intensity 0.005, wind 0.0, with collision and info display disabled. Discover the choices and their values:

from bruhanimate import effects

print(effects.names())             # All registered effects
print(effects.snow.presets())      # light, moderate, blizzard, windy + settings
print(effects.snow.preset("light")) # A fresh SnowSettings copy
print(effects.snow.defaults())     # Default settings, with typed fields

For editor completion of effects, effect-specific presets, and settings, use the typed catalog. It runs through the same player as play("snow", ...):

from bruhanimate import effects

effects.snow.play(preset="light", fps=60, updates_per_second=30, duration=10)

duration limits wall-clock seconds after startup, including paused/held time. The first duration, frame, completion, or quit condition wins. Zero starts no effect; play still opens/restores its Screen. Slow callbacks can overrun the deadline. Built-in settings validate types, ranges, and choices before use.

Explore without writing a script or opening an animation:

bruhanimate list
bruhanimate presets snow
bruhanimate info snow
bruhanimate play snow --preset light --duration 10 --set wind=0.4

python -m bruhanimate provides the same CLI. In a source checkout, prefix either command with uv run --locked. See the discovery guide for configuration, CLI options, typing, and custom registry entries.

Capture and record

Use a seed to reproduce an effect with the same settings, size, update sequence, and dependency versions. Each effect has independent Python/NumPy randomness. The typed catalog also exposes headless capture and recording:

from bruhanimate import Frame, effects

def inspect(frame: Frame) -> None:
    print(frame.index, frame.time, frame.text())

effects.snow.capture(on_frame=inspect, preset="light", seed=42,
                     width=40, height=12, frames=4)
bruhanimate record snow snow.cast --preset light --seed 42 --duration 5

Capture uses virtual time and requires a frame or duration bound. It streams immutable snapshots through the existing player without opening a terminal or sleeping. effects.snow.record("snow.cast", ...) writes asciicast v2 files; existing files are protected unless overwrite=True / --overwrite is set. Live audio still requires matching external input for repeatability. See the capture guide for timing, ownership, and format details.

Build a scene

Put a title over an animated background:

from bruhanimate import ImageEffect, Scene, effect_registry, play


def build(buffer):
    snow = effect_registry.create("snow", buffer, " ", preset="light")
    return Scene(snow, ImageEffect("Advent of Code"))


play(effect_factory=build, fps=60, updates_per_second=30)

The factory receives a screen-sized buffer. A scene paints its children from back to front, and ImageEffect centers text by default. Use Layer(animation, transparent=True) when plain spaces should reveal lower layers; None cells are always transparent.

Animate a solver

Yield snapshots from your solver and describe how to draw one:

from bruhanimate import Playback, draw_grid, play

board = ("S...", ".##.", "...E")


def states():
    yield from ((0, 0), (1, 0), (2, 0), (3, 0), (3, 1), (3, 2))


def draw(position, buffer):
    grid = draw_grid(buffer, board, spacing=(2, 1))
    grid.put(*position, "@")


animation = Playback(states(), draw, steps_per_second=5)
play(animation, fps=60, event_handler=animation.handle_event, on_complete="hold")

Space pauses, n/right arrow steps, +/- changes speed, and q quits. The final state stays visible; use on_complete="exit" to close automatically. Yield independent snapshots, and keep the work between yields short enough for responsive input. Playback keeps the current snapshot and closes its iterator on completion or early quit.

Choose the right building block

You want to… Start with
Run a built-in effect play("fire") or a named preset
Write a small custom animation CallbackEffect(draw, update=update)
Keep a persistent simulation raster Subclass BaseEffect and implement update(dt)
Combine backgrounds, labels, and overlays Scene, Layer, and ImageEffect
Animate a solver or recorded states Playback and draw_grid
Supply your own Screen or clock Player

Change state in update(dt) and read it in draw(buffer). The player owns the loop; animations own their state. The core-concepts guide walks through this division with complete examples.

Explore the demos

python -m bruhanimate.demos.snow_demo
python -m bruhanimate.demos.fire_demo
python -m bruhanimate.demos.shared_player_demo
python -m bruhanimate.demos.grid_demo

From a uv source checkout, prefix these commands with uv run --locked. The grid demo includes a complete BFS solver, animated background, status text, and playback controls. Demos expose run(frames=None, fps=...) for use from Python.

Documentation

Guide What it covers
Getting started Installation, first animation, and demos
Core concepts Player, state, buffers, composition, and solver snapshots
Player and composition Timing, lifecycle, ownership, and the API
Solver playback Pause, stepping, completion, and bounded solver work
Migration to 1.0 Replacing the 0.2.x renderer APIs
Release process Dev-to-main workflow, versions, tags, and publishing

Resize currently ends playback after cleanup; automatic reflow and image/video export remain planned work. Headless frame capture and .cast export are supported. The terminal demo is a pipe-based command console, not a PTY emulator.

Development

Use Python 3.13 and uv 0.13.4, pinned in pyproject.toml:

git clone https://github.com/ethanlchristensen/bruh.animate.git
cd bruh.animate
git switch dev
uv sync --locked --group docs
uv run --locked ruff check .
uv run --locked mypy
uv run --locked pytest
uv run --locked python scripts/check_examples.py
uv run --locked --group docs python scripts/build_docs.py

Install uv with python -m pip install uv==0.13.4, or update an existing standalone installation with uv self update 0.13.4. Sync installs this checkout in editable mode. Open docs/build/html/index.html to preview the documentation.

Read AGENTS.md for architecture, lifecycle rules, and contribution checks. CI tests Linux, Windows, and macOS and smoke-tests installed wheel/source distributions.

Releases

Work on dev → merge into main → wait for green CI → push a matching version tag.

A merge updates the documentation site. Pushing vMAJOR.MINOR.PATCH starts package validation and publishing to PyPI, followed by a GitHub release. The preparation helper previews by default and never creates a tag:

# Check the currently prepared version; no version or Git changes:
uv run --locked python publish.py --check

Follow the complete release checklist for version/lockfile agreement, authentication setup, tag commands, and recovery.

License

Apache 2.0. Created by Ethan Christensen, with inspiration from Asciimatics.

Metadata

Release files for bruhanimate 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bruhanimate 1.1.0
File Size Uploaded Provenance
bruhanimate-1.1.0.tar.gz 108.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for bruhanimate 1.1.0
File Interpreter ABI Platform
bruhanimate-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 263.6 kB

Release files / bruhanimate-1.1.0.tar.gz

Download URL bruhanimate-1.1.0.tar.gz
Size 108.1 kB
Tags Source
SHA-256 checksum
How to use checksums
74a12d932d9b62339243ecc3a64110cc0e1a6defdcbffb5ba8baa62c10a55694
BLAKE2b-256 checksum
How to use checksums
a200f60ec760f0555bfde3fb664d11c6665bc2bf314ade011fcab9c3581e6f21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / bruhanimate-1.1.0-py3-none-any.whl

Download URL bruhanimate-1.1.0-py3-none-any.whl
Size 155.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d94a3adb07b85c43cab5de7f77422e975c3d24fe8af82b7b28ad739dbd8c95fc
BLAKE2b-256 checksum
How to use checksums
b38cef08a695b6237f0469df942b5b21ab9bcad407f867428fc5b8e2e9362f24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for bruhanimate 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page