Custom color provider
This is a how-to for plugin authors: write a custom color provider — the object that decides what color text is, frame by frame. You’ll build a pulse that makes a message breathe brighter and dimmer.
What you’ll need:
- A scaffolded plugin — see the authoring guide; you only import
led_ticker.plugin. - No hardware needed: you’ll preview with
make render-demo(run from the repo aftermake dev). - Your plugin installed (
pip install -e .from its directory) —make render-demoonly picks up installed plugins.

The color_for contract
Section titled “The color_for contract”A color provider is a class with two flags and one method:
class Pulse(ColorProviderBase): per_char = False frame_invariant = False
def color_for(self, frame, char_index, total_chars): ...color_for(frame, char_index, total_chars)returns aColorfor the currentframe.char_index/total_charsdescribe the character being drawn (for per-character effects).per_char—Falsereturns one color for the whole string;Trueis called per character, so you can color each letter differently (usingchar_index).frame_invariant— does your color depend onframe?Truemeans no (a constant or gradient — led-ticker paints it once and sleeps, a performance fast-path);Falsemeans yes (it animates — led-ticker re-renders every tick).
Build the pulse
Section titled “Build the pulse”The pulse keeps one color but scales its brightness up and down with a sine wave driven by frame:
def color_for(self, frame, char_index, total_chars): level = 0.65 + 0.35 * math.sin(frame * self.speed * 0.05) r, g, b = self.color return make_color(int(r * level), int(g * level), int(b * level))make_color(r, g, b) builds a Color (channels 0–255). The 0.65 is the midpoint brightness and 0.35 the swing, so brightness oscillates between about 30% and 100% — it never goes fully dark; the 0.05 just scales frame into the sine, so tune speed from TOML and leave the 0.05 alone. Because the output depends on frame, frame_invariant = False. Add a base color and a speed as constructor arguments and they’ll come from TOML.
Register and use it
Section titled “Register and use it”-
Register the class in your plugin’s
register(api):@api.color_provider("pulse")class Pulse(ColorProviderBase): ...It’s namespaced — in the
example_colorproviderplugin it becomesexample_colorprovider.pulse. -
Point a text widget’s
font_colorat it in yourconfig/config.toml(optionally with fields):[[playlist.section.widget]]type = "message"text = "breathing"font_color = { style = "example_colorprovider.pulse", color = [0, 200, 255], speed = 6 } -
Install your plugin so led-ticker can find it.
make render-demoloads only installed plugins (see Package & install), then preview — no hardware needed:Terminal window pip install -e . # run from your plugin's directorymake render-demo CONFIG=config/config.toml OUT=preview.gifopen preview.gif # macOS; xdg-open on Linux
Complete listing
Section titled “Complete listing”The full plugin — examples/plugins/example_colorprovider/__init__.py:
"""Example led-ticker plugin: a custom 'pulse' color provider (the 'Custom color provider' how-to).
Drop `example_colorprovider/` into your `config/plugins/` (local use), or package itwith an `[project.entry-points."led_ticker.plugins"] example_colorprovider = "example_colorprovider:register"`entry, then use it as `font_color = {style = "example_colorprovider.pulse"}`.
Imports only `led_ticker.plugin` (the public surface) plus stdlib."""
import math
from led_ticker.plugin import ColorProviderBase, make_color
def register(api): @api.color_provider("pulse") class Pulse(ColorProviderBase): # One color for the whole string (not per-character). per_char = False # `color_for` depends on `frame`, so the widget must re-render each tick. # Declaring this True would freeze the pulse — ColorProviderBase forces # you to set it explicitly. frame_invariant = False
# Config fields come from TOML, e.g. # font_color = {style = "example_colorprovider.pulse", color = [0, 200, 255], speed = 6} def __init__(self, color=(0, 200, 255), speed=6): self.color = color self.speed = speed
def color_for(self, frame, char_index, total_chars): # Brightness breathes between ~0.30 and ~1.00 as the frame advances. level = 0.65 + 0.35 * math.sin(frame * self.speed * 0.05) r, g, b = self.color return make_color(int(r * level), int(g * level), int(b * level))If it doesn’t work
Section titled “If it doesn’t work”- The color is frozen / doesn’t animate — you declared
frame_invariant = Truebutcolor_forusesframe. Setframe_invariant = False. TypeError: Pulse must define 'frame_invariant'— declare it as a class attribute (that’sColorProviderBasedoing its job).- “unknown font_color style” / not found — check the namespaced
style(example_colorprovider.pulse, notpulse) and that the plugin is installed/loaded (see Installing a plugin).