Terminal resize

Enable live reflow with on_resize="resize" on play, Player, or a typed effect’s .play() method. ResizePolicy is a Literal alias containing "raise" and "resize" for editor completion. The default "raise" retains the existing ScreenResizedError and cleanup behavior.

from bruhanimate import Column, Fixed, Label, Slot, StatusBar, play

layout = Column(
    Slot(Label("Resize this terminal", align="center"), size=Fixed(1)),
    Slot(Label("The same content stays alive", align="center", vertical_align="middle")),
    Slot(StatusBar("Ready", right="q quit"), size=Fixed(1)),
)
play(effect_factory=layout.build, on_resize="resize", fps=60, frames=180)

CLI playback accepts the same choices:

bruhanimate play snow --preset light --on-resize resize
python -m bruhanimate.demos.dashboard_demo

What survives

Player keeps the live animation and factory result, refreshes the Screen size, notifies content, and invalidates the previous frame so every cell is repainted. Frames and duration limits span the whole run; resizing does not restart them. Scene retains layer clocks, and TimedOverlay retains its visibility window. AnimationSequence keeps its active child/time and passes the latest viewport to future children when their factories run; see Animation sequences. Input continues through the existing event handler. Resize errors propagate after normal ownership cleanup; borrowed content is never closed by Player.

Row and Column recalculate their existing Fixed/Flex allocations. Previously started children keep updating even if hidden, and still count toward finite completion. Initially zero-area slots defer startup until visible. Each factory runs at most once. Layout trees remain single-use after close.

Playback keeps the acquired iterator, current snapshot, pause flag, speed, accumulated solver time and queued manual steps. Reflow itself neither consumes a snapshot nor advances time. Normal updates still run at the configured rate. In the Dashboard demo, pause, resize smaller and larger, and press n to confirm that the same solver continues one step at a time.

Panels and simulations

Panel(..., fit=True) follows the parent viewport, subtracting its border from the content size. Use it inside layout slot factories. Content retains at least one cell per axis; allocations too small for the border clip its footprint. The default fit=False keeps fixed dimensions. Explicit bounds are unchanged; move_to((x, y)) moves a panel without advancing its simulation.

Effect-specific adapters use the same typed resize((width, height)) hook; there is no extra flag to enable them. on_resize="resize" automatically uses the implementation provided by the effect:

  • Snow: the simulation fills the new width and height. Each contiguous pile connected to the old floor moves by the height difference, keeping its column and depth relative to the bottom. Other settled snow keeps absolute coordinates, including snow perched on configured collision surfaces. Falling flakes retain their coordinates, fall delay and drift when still visible and uncovered. Cropped pile cells/columns, cropped falling flakes and flakes covered by a relocated pile are discarded; later growth does not restore them. New columns start empty and immediately participate in spawning on the next update.

  • Plasma: wave anchors follow the new dimensions and the whole raster is repainted at its existing phase. Frequencies, palette and random streams stay unchanged, so resize does not produce a simulation step or a random reset.

  • Fire: both heat fields, heat spots and the last visible flame raster follow the bottom edge, retaining left-hand columns. Heat-spot lifetimes, wind, swell phase and random streams are preserved. Cropped heat/spots are discarded. New cells start cold; new columns use neutral fuel variation until the next normal random refresh. The last colored pixels are copied instead of rerolling flicker.

  • Rain: existing drops, image splashes and lightning cells keep their absolute positions. Visible bottom-row splashes move to the new floor. Bolt lifetimes, intensity, swell direction and wind are preserved. Cropped cells are discarded; new space starts empty and receives rain during normal updates. Explicit collision masks keep their dimensions and coordinates; cells outside a mask do not collide.

  • Matrix: existing characters, colored pixels, per-row update intervals and character/color ticks keep their coordinates. Cropped rows and columns are discarded. New cells start blank; the next simulation update chooses characters for them and update intervals for new rows. Repeated resizes before that tick retain only the pending cells still visible. Resize itself consumes no randomness.

  • Sand: retained grains and pixels follow the bottom; new cells are empty.

  • Water: displacement and previous displacement retain their top-left overlap, preserving local wave velocity; added cells are still. The field repaints.

  • Smoke/Ink: density and obstacle cells retain their top-left overlap. Added cells are empty; the source uses the new top/bottom edge on its next update.

  • Metaballs, Cloth: the current phase remaps to the new viewport. Blob orbits, cloth art and random streams stay intact; the raster repaints immediately.

  • Terrain: the same seeded height field and camera travel position are projected into the new dimensions without advancing or regenerating terrain.

  • Game of Life, Automaton, Diffusion: grids retain their top-left overlap, including cell ages, the active automaton row, or chemical concentrations. Added cells are dead/empty, or U=1/V=0 for diffusion. No random reseeding occurs.

  • Boids: positions wrap into new bounds; flock size and velocities survive.

  • Voronoi: sites clamp to new bounds without changing velocities; region geometry is rebuilt and repainted immediately.

  • Perlin: sampling coordinates rebuild at the existing phase, keeping seeded offsets; the complete field repaints immediately.

  • Twinkle: retained stars keep their brightness and direction. Added cells initialize on the next update; repeated resizes keep only visible pending cells.

  • Noise, Stars, Static, Offset, Julia, DrawLines: the raster retains top-left pixels, fills added cells with the background, and uses its new dimensions on the next update. Julia’s tick and line endpoints remain unchanged.

  • Fireworks: live particles keep positions, velocities and lifetimes, with updated bounds. Launch peaks clamp below the new height; future launches use the new area. Existing trails clip in place.

  • Audio: capture stays running. New raster cells start with the background; the next audio update repaints the selected mode. Waveform history retains its newest samples on the right. Automatic bar counts follow width, interpolating existing smoothed values; explicit bar counts remain fixed. Geometry caches invalidate without reading a new sample or advancing mode clocks.

  • TerminalEffect: retained output/input repaints using the new viewport. The subprocess and output queue are untouched. Previously wrapped history is retained as stored lines; this pipe-based console does not resize a child PTY.

  • Custom BaseEffect subclasses: the default still keeps the original private raster and clips its bottom/right edges. Growth reveals that raster again; cells beyond it reveal the owner background. Override resize to adapt state.

Cropped grid/raster state is discarded and is not restored on growth. Particle policies are effect-specific as described above. Resizing never consumes random numbers or advances simulation. Audio regression tests use synthetic samples; those tests do not establish hardware support.

The adapters prepare their private buffers during the resize notification, so preserved or relocated pixels appear before the next scheduled layer update. New random activity and deferred raster repainting begin during normal updates. Previously presented frames remain unchanged. Explicit collision geometry is never automatically moved or inferred from layout. An embedded terminal subprocess is not resized. The Dashboard demo exercises snow and plasma with the existing solver. Try the other adapters directly:

bruhanimate play fire --on-resize resize
bruhanimate play rain --preset thunderstorm --on-resize resize
bruhanimate play matrix --on-resize resize

Custom animations

ScrollView.resize changes only its viewport and clamps the scroll offset; its child continues drawing on the same full canvas. set_content_size is the explicit canvas resize operation. LogView.resize rewraps retained history, preserving the top logical line/character while browsing or staying at the bottom while following new messages. Neither operation consumes solver snapshots or appends log messages. See Scrollable views and activity logs for typed options and navigation bindings.

Resizable is an optional structural protocol with resize(size: tuple[int, int]) -> None; size is positive (width, height) in terminal cells. ResizableAnimation combines it with Animation, and is the return type of layout.build. Inheritance is optional. Implement the hook to recalculate cached geometry; do not advance simulation, consume input, or restart existing children. It may run before startup for a delayed overlay’s content. For a BaseEffect adapter, override the typed resize method, validate dimensions before changing state, resize every dependent simulation array, and rebuild the private raster without calling update or consuming randomness. Finish with super().resize(size) to update its drawing viewport. Replacing only buffer is unsafe for simulations with size-dependent particle arrays or collision maps. Drawing must stay read-only and must not retain a previously presented buffer.

Animations without the hook receive the newly sized drawing buffer directly; their draw implementation must handle arbitrary dimensions. Playback, Label, StatusBar, ImageEffect and buffer-aware drawing callbacks already do so. Manual users of a built layout call animation.resize((width, height)) before drawing into a differently sized buffer.

Backend behavior and limits

Windows refreshes console dimensions and resets cursor tracking. Unix marks a pending signal and refreshes curses from Player’s loop, outside the signal handler. Both acknowledge the resize without reopening the terminal. Player repaints the whole viewport even if a resize notification reports the same size.

For a temporarily empty/minimized viewport, Player waits in short intervals, polls quit input, and checks the original duration deadline. It does not draw or accumulate simulation catch-up while waiting. Custom Screen implementations supporting live resize must provide refresh_size() -> bool: update width and height, reset cursor state, acknowledge the event, and return True for a positive size; return False and leave the event pending for an empty size. Custom presenters can override FramePresenter.resize() to invalidate their own state. Headless capture/recording retain their configured dimensions and produce no terminal resize events.

API

Optional viewport notifications, independent of animation ownership.

class bruhanimate.resize.Resizable(*args, **kwargs)[source]

Optional capability for animations receiving a new parent viewport.

Dimensions are positive (width, height) in cells. A notification must not advance simulation, consume input, or restart/close existing children. Owners call it between frames, before drawing at the new size. Animations without this method must already draw into arbitrary buffer dimensions.

resize(size: tuple[int, int]) → None[source]

Reflow the view while retaining live animation and solver state.

__init__(*args, **kwargs)