Looping and infinite effects
Some effects play a cycle that repeats: a color wave, a pulse, a rotation. They usually let the user choose how many
cycles to play, and treat 0 as "forever". This page shows how to count cycles, how to end a looping effect cleanly,
and what an infinite effect means for the people running it. Built-in effects with this option include ColorShift,
whose --cycles 0 loops until interrupted.
The Wave example
Wave gives every character a looping scene that cycles through a looping gradient. Each character starts at a
different point in the cycle, offset by its column, so a band of color sweeps across the text from right to left.
After the last cycle, every character settles to a final color. Save it as wave.py in your community effects
directory:
"""Wave: a band of color sweeps across the text in a loop, for a set number of cycles or forever.
An Effect Guide example for the Looping and infinite effects pattern.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import Color, ColorPair, Gradient
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.utils import argutils
WAVE = Gradient(Color("#2040ff"), Color("#20e0ff"), Color("#ffffff"), steps=10, loop=True)
TICKS_PER_COLOR = 2
FINAL_COLOR = ColorPair(fg=Color("#20e0ff"))
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "wave", Wave, WaveConfig
@dataclass
class WaveConfig(BaseConfig):
"""Configuration for the Wave effect."""
parser_spec = argutils.ParserSpec(
name="wave",
help="A band of color sweeps across the text in a loop.",
description="wave | A band of color sweeps across the text in a loop.",
epilog="Example: tte wave --cycles 0",
)
cycles: int = argutils.ArgSpec(
name="--cycles",
type=argutils.NonNegativeInt.type_parser,
default=2,
metavar=argutils.NonNegativeInt.METAVAR,
help="Number of times the wave crosses the text. 0 loops until interrupted.",
) # pyright: ignore[reportAssignmentType]
"int : Number of times the wave crosses the text. 0 loops until interrupted."
class WaveIterator(BaseEffectIterator[WaveConfig]):
"""Produce the frames of the Wave effect."""
def __init__(self, effect: Wave) -> None:
"""Give every character a looping scene, offset by its column so the colors travel."""
super().__init__(effect)
colors = WAVE.spectrum
self.cycle_ticks = len(colors) * TICKS_PER_COLOR
self.ticks = 0
for character in self.terminal.get_characters():
offset = character.input_coord.column % len(colors)
scene = character.animation.new_scene(is_looping=True)
for color in colors[offset:] + colors[:offset]:
scene.add_frame(character.input_symbol, TICKS_PER_COLOR, colors=ColorPair(fg=color))
character.animation.activate_scene(scene)
self.terminal.set_character_visibility(character, is_visible=True)
self.active_characters.add(character)
def __next__(self) -> str:
"""Advance the wave; after the last cycle, settle every character and stop."""
if not self.active_characters:
raise StopIteration
if self.config.cycles and self.ticks == self.config.cycles * self.cycle_ticks:
for character in self.active_characters:
character.animation.deactivate_scene()
character.animation.set_appearance(character.input_symbol, FINAL_COLOR)
self.active_characters.clear()
return self.frame
self.ticks += 1
self.update()
return self.frame
class Wave(BaseEffect[WaveConfig]):
"""A band of color sweeps across the text in a loop."""
@property
def _config_cls(self) -> type[WaveConfig]:
return WaveConfig
@property
def _iterator_cls(self) -> type[WaveIterator]:
return WaveIterator

With --cycles 0, the wave keeps sweeping until you press Ctrl+C:
Looping scenes and gradients
Two looping options work together here:
Gradient(..., loop=True)adds a transition from the last stop back to the first, so the spectrum ends where it starts, and cycling through it has no jump (see Gradients).new_scene(is_looping=True)starts the scene again from its first frame after the last one, forever (see More scene options).
Rotating each character's list of colors by its column, colors[offset:] + colors[:offset], is what turns the same
cycle into a moving wave: neighboring characters are one color apart at every tick.
Ending a loop
A looping scene never completes, so its character never becomes inactive, and an iterator that waits for
active_characters to empty would never stop. The iterator has to decide when to stop, and Wave counts ticks:
- One cycle lasts
len(colors) * TICKS_PER_COLORticks, the length of the scene. - After
cyclesfull cycles,__next__stops each character's scene withdeactivate_scene(), gives it its final appearance withset_appearance(), and clearsactive_characters. It returns that settled frame, and the next call raisesStopIteration.
set_appearance() changes what is drawn at once, without an update(), so the settled frame shows the final colors.
Ending on a deliberate final appearance, rather than wherever the loop happened to be, makes the effect look finished.
Other ways to end a loop:
- Replace the looping scene with a final, non-looping scene, and wait for it to complete as usual.
- For a looping path, call
motion.deactivate_path()or activate a final path.
Infinite effects
With --cycles 0, Wave never raises StopIteration. That is fine on the command line, where the user stops it with
Ctrl+C, but it changes how the effect can be used:
- Someone running the effect from Python must stop iterating themselves, as the Library Guide explains for the built-in effects.
- Tests and recordings need a finite setting, so make sure every option has a finite value, as Wave's default of 2 cycles is.
- Say in the option's help that
0loops until interrupted, as Wave does.
Where to go next
- Animation covers scenes, frame durations, and looping.
- Phases and staggered starts covers effects that end in a final stage.