Step 6: Configure
MySpray now looks like Spray, but every value is fixed in the code: the spray comes from the right edge, the speeds are between 0.6 and 1.4, and the gradient is always purple to white. In this step those values become command-line options, with the values used so far as their defaults.
Engine chapter used: Configuration and the CLI.
Update the module
Replace myspray.py with this version:
"""MySpray, tutorial step 6: add command-line options.
The spray position, release rate, movement, and final gradient become options with the defaults used so far.
"""
from __future__ import annotations
import random
import typing
from dataclasses import dataclass
from terminaltexteffects import Color, Coord, EffectCharacter, EventHandler, Gradient, easing
from terminaltexteffects.engine.base_config import (
BaseConfig,
FinalGradientDirectionArg,
FinalGradientStepsArg,
FinalGradientStopsArg,
)
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.utils import argutils
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "myspray", MySpray, MySprayConfig
@dataclass
class MySprayConfig(BaseConfig):
"""Configuration for the MySpray effect."""
parser_spec = argutils.ParserSpec(
name="myspray",
help="Sprays the characters from a single point.",
description="myspray | Sprays the characters from a single point.",
epilog="Example: tte myspray --spray-position center --final-gradient-direction radial",
)
spray_position: typing.Literal["n", "ne", "e", "se", "s", "sw", "w", "nw", "center"] = argutils.ArgSpec(
name="--spray-position",
choices=["n", "ne", "e", "se", "s", "sw", "w", "nw", "center"],
default="e",
help="Position for the spray origin.",
) # pyright: ignore[reportAssignmentType]
"typing.Literal['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center'] : Position for the spray origin."
spray_volume: float = argutils.ArgSpec(
name="--spray-volume",
type=argutils.PositiveRatio.type_parser,
default=0.005,
metavar=argutils.PositiveRatio.METAVAR,
help="Number of characters to spray per tick as a fraction of the total number of characters.",
) # pyright: ignore[reportAssignmentType]
"float : Number of characters to spray per tick as a fraction of the total number of characters."
movement_speed_range: tuple[float, float] = argutils.ArgSpec(
name="--movement-speed-range",
type=argutils.PositiveFloatRange.type_parser,
default=(0.6, 1.4),
metavar=argutils.PositiveFloatRange.METAVAR,
help="Movement speed range of the characters.",
) # pyright: ignore[reportAssignmentType]
"tuple[float, float] : Movement speed range of the characters."
movement_easing: easing.EasingFunction = argutils.ArgSpec(
name="--movement-easing",
type=argutils.Ease.type_parser,
default=easing.out_expo,
metavar=argutils.Ease.METAVAR,
help="Easing function to use for character movement.",
) # pyright: ignore[reportAssignmentType]
"easing.EasingFunction : Easing function to use for character movement."
final_gradient_stops: tuple[Color, ...] = FinalGradientStopsArg(
default=(Color("#8A008A"), Color("#00D1FF"), Color("#FFFFFF")),
) # pyright: ignore[reportAssignmentType]
"tuple[Color, ...] : Colors for the final gradient. With one color, the characters are shown in that color."
final_gradient_steps: tuple[int, ...] | int = FinalGradientStepsArg(
default=12,
) # pyright: ignore[reportAssignmentType]
"tuple[int, ...] | int : Number of steps between each pair of final gradient stops."
final_gradient_direction: Gradient.Direction = FinalGradientDirectionArg(
default=Gradient.Direction.VERTICAL,
) # pyright: ignore[reportAssignmentType]
"Gradient.Direction : Direction of the final gradient across the text."
class MySprayIterator(BaseEffectIterator[MySprayConfig]):
"""Produce the frames of the MySpray effect."""
def __init__(self, effect: MySpray) -> None:
"""Set up every character before the first frame."""
super().__init__(effect)
self.pending_chars: list[EffectCharacter] = []
self.build()
def build(self) -> None:
"""Give every character a path home and a droplet scene, and queue it for release."""
canvas = self.terminal.canvas
final_gradient = Gradient(*self.config.final_gradient_stops, steps=self.config.final_gradient_steps)
final_gradient_mapping = final_gradient.build_coordinate_color_mapping(
canvas.text_bottom,
canvas.text_top,
canvas.text_left,
canvas.text_right,
self.config.final_gradient_direction,
)
spray_origin_map = {
"center": canvas.center,
"n": Coord(canvas.center_column, canvas.top),
"nw": Coord(canvas.left, canvas.top),
"w": Coord(canvas.left, canvas.center_row),
"sw": Coord(canvas.left, canvas.bottom),
"s": Coord(canvas.center_column, canvas.bottom),
"se": Coord(canvas.right - 1, canvas.bottom),
"e": Coord(canvas.right - 1, canvas.center_row),
"ne": Coord(canvas.right - 1, canvas.top),
}
spray_origin = spray_origin_map[self.config.spray_position]
for character in self.terminal.get_characters():
character.motion.set_coordinate(spray_origin)
input_coord_path = character.motion.new_path(
speed=random.uniform(*self.config.movement_speed_range),
ease=self.config.movement_easing,
)
input_coord_path.new_waypoint(character.input_coord)
character.event_handler.register_event(
EventHandler.Event.PATH_ACTIVATED,
input_coord_path,
EventHandler.Action.SET_LAYER,
1,
)
character.event_handler.register_event(
EventHandler.Event.PATH_COMPLETE,
input_coord_path,
EventHandler.Action.SET_LAYER,
0,
)
droplet_scn = character.animation.new_scene()
spray_gradient = Gradient(
random.choice(final_gradient.spectrum),
final_gradient_mapping[character.input_coord],
steps=7,
)
droplet_scn.apply_gradient_to_symbols(character.input_symbol, 20, fg_gradient=spray_gradient)
character.animation.activate_scene(droplet_scn)
character.motion.activate_path(input_coord_path)
self.pending_chars.append(character)
random.shuffle(self.pending_chars)
# Release up to `spray_volume` of the characters per frame, and at least one.
self._volume = max(int(len(self.pending_chars) * self.config.spray_volume), 1)
def __next__(self) -> str:
"""Release a few characters, advance the animation, and return the frame."""
if self.pending_chars or self.active_characters:
for _ in range(random.randint(1, self._volume)):
if self.pending_chars:
next_character = self.pending_chars.pop()
self.terminal.set_character_visibility(next_character, is_visible=True)
self.active_characters.add(next_character)
self.update()
return self.frame
raise StopIteration
class MySpray(BaseEffect[MySprayConfig]):
"""Sprays the characters from a single point."""
@property
def _config_cls(self) -> type[MySprayConfig]:
return MySprayConfig
@property
def _iterator_cls(self) -> type[MySprayIterator]:
return MySprayIterator
What changed
The options
MySprayConfig now has seven options, the same as the built-in Spray:
| Option | Validator | Default |
|---|---|---|
--spray-position |
choices, the nine keys of spray_origin_map |
e |
--spray-volume |
PositiveRatio |
0.005 |
--movement-speed-range |
PositiveFloatRange |
0.6-1.4 |
--movement-easing |
Ease |
out_expo |
--final-gradient-stops |
FinalGradientStopsArg |
8A008A 00D1FF FFFFFF |
--final-gradient-steps |
FinalGradientStepsArg |
12 |
--final-gradient-direction |
FinalGradientDirectionArg |
vertical |
The first four are declared with ArgSpec. The three final gradient options use the shared declarations from
terminaltexteffects.engine.base_config, which already have their names, validators, and help, so MySpray only sets
their defaults. The epilog now shows an example that uses two of the options.
Using the options
build() reads each value from self.config where it used a fixed value before. The values are already converted:
movement_speed_range is a tuple of two floats, so random.uniform(*self.config.movement_speed_range) picks a speed
between them; movement_easing is the easing function itself; and final_gradient_stops is a tuple of Color
objects ready to pass to Gradient.
spray_position is one of the keys of spray_origin_map, the dictionary from step 3, so it selects the origin
directly.
Run it
With no options, MySpray runs as it did in step 5:

The options change it. This command, the one in the epilog, sprays from the center of the canvas and colors the text with a radial gradient:
tte --input-file tutorial_input.txt myspray --spray-position center --final-gradient-direction radial

tte myspray -h lists the options with their defaults:
usage: tte myspray [-h] [--spray-position {n,ne,e,se,s,sw,w,nw,center}]
[--spray-volume (0 < float(n) <= 1)]
[--movement-speed-range (hyphen separated float range e.g. '0.25-0.5')]
[--movement-easing (Easing Function)]
[--final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]]
[--final-gradient-steps (int > 0) [(int > 0) ...]]
[--final-gradient-direction (diagonal, horizontal, vertical, radial)]
myspray | Sprays the characters from a single point.
options:
-h, --help show this help message and exit
--spray-position {n,ne,e,se,s,sw,w,nw,center}
Position for the spray origin. (default: e)
--spray-volume (0 < float(n) <= 1)
Number of characters to spray per tick as a fraction of the total number
of characters. (default: 0.005)
--movement-speed-range (hyphen separated float range e.g. '0.25-0.5')
Movement speed range of the characters. (default: 0.6-1.4)
--movement-easing (Easing Function)
Easing function to use for character movement. (default: out_expo)
--final-gradient-stops (XTerm [0-255] OR RGB Hex [000000-ffffff]) [(XTerm [0-255] OR RGB Hex [000000-ffffff]) ...]
Space separated, unquoted, list of colors for the character gradient
(applied across the canvas). If only one color is provided, the characters
will be displayed in that color. (default: 8a008a 00d1ff ffffff)
--final-gradient-steps (int > 0) [(int > 0) ...]
Space separated, unquoted, list of the number of gradient steps to use.
More steps will create a smoother and longer gradient animation. (default:
12)
--final-gradient-direction (diagonal, horizontal, vertical, radial)
Direction of the final gradient across the text. (default: vertical)
Example: tte myspray --spray-position center --final-gradient-direction radial
Invalid values are rejected before the effect starts:
$ tte --input-file tutorial_input.txt myspray --spray-volume 2
tte myspray: error: argument --spray-volume: invalid value: '2' must be 0 < n <=1. Example: 0.5
argparse prints the usage lines shown above before the error.
Next step
Step 7 handles colors in the input, tests MySpray against unusual input, and compares the result with the built-in Spray.