Custom emoji
This is a how-to for plugin authors: add your own emoji so it renders inline in any message as :yourplugin.slug:. You’ll register an 8×8 sprite, see it on the sign, then add a hi-res version for scaled (big) signs.

What a sprite looks like: PixelData
Section titled “What a sprite looks like: PixelData”A low-res emoji is a PixelData — a list of (x, y, r, g, b) tuples, one per lit pixel on an 8×8 grid (x, y are 0–7; r, g, b are 0–255). Pixels you leave out stay transparent.
The easy way to build one is to “draw” it as text, then expand it:
# An 8×8 heart. "X" = a lit pixel, "." = transparent._HEART_ART = [ ".XX..XX.", "XXXXXXXX", "XXXXXXXX", "XXXXXXXX", ".XXXXXX.", "..XXXX..", "...XX...", "........",]_RED = (220, 40, 60)HEART = [ (x, y, *_RED) for y, row in enumerate(_HEART_ART) for x, cell in enumerate(row) if cell == "X"]# HEART == [(1, 0, 220, 40, 60), (2, 0, 220, 40, 60), ...] — 40 tuples.Register it and show it
Section titled “Register it and show it”-
Register the sprite inside your plugin’s
register(api)under a slug:def register(api):api.emoji("heart", HEART)The slug is namespaced automatically — in the
example_emojiplugin it becomesexample_emoji.heart. -
Use it inline in any message by wrapping the namespaced slug in colons. Add a section like this to the
config/config.tomlyou’ll render:[[playlist.section.widget]]type = "message"text = "we :example_emoji.heart: led-ticker" -
Install your plugin so led-ticker can find it.
make render-demoonly loads installed plugins (via their entry point), not a local plugin directory — so package-install yours first (see Package & install):Terminal window pip install -e . # run from your plugin's directory -
Preview it — no hardware needed. This renders the config from step 2:
Terminal window make render-demo CONFIG=config/config.toml OUT=preview.gifopen preview.gif # macOS; xdg-open on Linux
Low-res vs hi-res
Section titled “Low-res vs hi-res”led-ticker keeps two emoji registries:
- Low-res — the 8×8
PixelDataabove. Used by inline:slug:text and by small / unscaled signs. This is all most plugins need. - Hi-res — a larger sprite in physical pixel coordinates. Used on scaled (big) signs (
default_scale > 1) and by direct draws (draw_emoji_at).
Rule of thumb: small sign → register only the low-res sprite. Big / scaled sign → also register a hi-res version (keep the low-res one — inline :slug: always resolves through it; a hi-res sprite with no low-res counterpart logs a warning at load).
Adding a hi-res sprite
Section titled “Adding a hi-res sprite”HiResEmoji(pixels=…, physical_size=…) takes pixels in physical coordinates (0 … physical_size − 1). The quickest way is to scale your 8×8 sprite up — here, 2× into a 16×16:
from led_ticker.plugin import HiResEmoji
HEART_HIRES = tuple( (x * 2 + dx, y * 2 + dy, r, g, b) for (x, y, r, g, b) in HEART for dx in (0, 1) for dy in (0, 1))
def register(api): api.emoji("heart", HEART) api.hires_emoji("heart", HiResEmoji(pixels=HEART_HIRES, physical_size=16))From a PNG
Section titled “From a PNG”Have an image instead? This one-time helper converts an 8×8 PNG into a PixelData list you can paste in. It’s a script you run yourself — not part of the plugin.
# pip install pillowfrom PIL import Image
img = Image.open("heart.png").convert("RGBA") # an 8×8 imagepixels = []for y in range(img.height): for x in range(img.width): r, g, b, a = img.getpixel((x, y)) if a > 0: # skip fully transparent pixels pixels.append((x, y, r, g, b))
print(pixels) # paste this as your PixelDataComplete listing
Section titled “Complete listing”The full plugin — examples/plugins/example_emoji/__init__.py:
"""Example led-ticker plugin: a custom inline emoji (the 'Custom emoji' how-to).
Drop `example_emoji/` into your `config/plugins/` (local use), or package it withan `[project.entry-points."led_ticker.plugins"] example_emoji = "example_emoji:register"`entry, then use it inline in any message as `:example_emoji.heart:`.
Imports only `led_ticker.plugin` (the public surface) plus stdlib."""
from led_ticker.plugin import HiResEmoji
# An 8x8 heart. "X" = a lit pixel, "." = transparent._HEART_ART = [ ".XX..XX.", "XXXXXXXX", "XXXXXXXX", "XXXXXXXX", ".XXXXXX.", "..XXXX..", "...XX...", "........",]_RED = (220, 40, 60)
# Low-res sprite: a PixelData = list of (x, y, r, g, b), one tuple per lit pixel.HEART = [ (x, y, *_RED) for y, row in enumerate(_HEART_ART) for x, cell in enumerate(row) if cell == "X"]
# Hi-res sprite: scale the 8x8 up 2x into a 16x16, in physical coordinates.HEART_HIRES = tuple( (x * 2 + dx, y * 2 + dy, r, g, b) for (x, y, r, g, b) in HEART for dx in (0, 1) for dy in (0, 1))
def register(api): # Low-res: used by inline `:example_emoji.heart:` and small / unscaled signs. api.emoji("heart", HEART) # Hi-res: used on scaled (big) signs; keep the low-res one for inline use. api.hires_emoji("heart", HiResEmoji(pixels=HEART_HIRES, physical_size=16))If it doesn’t work
Section titled “If it doesn’t work”- The emoji doesn’t appear / shows as literal text — check the slug is namespaced (
:example_emoji.heart:, not:heart:) and that the plugin is installed/loaded (see Installing a plugin). - It shows on a small sign but not a big one — register a hi-res sprite (above); inline use still needs the low-res one.
- A “hi-res emoji has no low-res counterpart” warning at load — register a matching
api.emoji(slug, …)alongsideapi.hires_emoji(slug, …).