Files
Mainline/docs/superpowers/specs/2026-03-19-figment-mode-design.md
Gene Johnson 064eaaee3d docs: add figment mode design spec
Hybrid plugin + overlay architecture for periodic SVG glyph display
with theme-aware coloring and extensible input abstraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 00:32:52 -07:00

9.1 KiB

Figment Mode Design Spec

Periodic full-screen SVG glyph overlay with flickery animation, theme-aware coloring, and extensible physical device control.

Overview

Figment mode displays a randomly selected SVG from the figments/ directory as a flickery, glitchy half-block terminal overlay on top of the running ticker. It appears once per minute (configurable), holds for ~4.5 seconds with a three-phase animation (progressive reveal, strobing hold, dissolve), then fades back to the ticker. Colors are randomly chosen from the existing theme gradients.

The feature is designed for extensibility: a generic input protocol allows MQTT, ntfy, serial, or any other control surface to trigger figments and adjust parameters in real time.

Architecture: Hybrid Plugin + Overlay

The figment is an EffectPlugin for lifecycle, discovery, and configuration, but delegates rendering to a layers-style overlay helper. This avoids stretching the EffectPlugin.process() contract (which transforms line buffers) while still benefiting from the plugin system for C&C, auto-discovery, and config management.

Component Diagram

                     +-------------------+
                     |  FigmentTrigger   |  (Protocol)
                     |  - NtfyTrigger    |  (v1)
                     |  - MqttTrigger    |  (future)
                     |  - SerialTrigger  |  (future)
                     +--------+----------+
                              |
                              | FigmentCommand
                              v
+------------------+    +-----------------+    +----------------------+
| figment_render   |<---| FigmentPlugin   |--->| render_figment_      |
| .py              |    | (EffectPlugin)  |    | overlay() in         |
|                  |    |                 |    | layers.py            |
| SVG -> PIL ->    |    | Timer, state    |    |                      |
| half-block cache |    | machine, SVG    |    | ANSI cursor-position |
|                  |    | selection       |    | commands for overlay |
+------------------+    +-----------------+    +----------------------+
                              |
                              | get_figment_state()
                              v
                     +-------------------+
                     | scroll.py         |
                     | (5-line block)    |
                     +-------------------+

Section 1: SVG Rasterization

File: engine/figment_render.py

Reuses the same PIL-based half-block encoding that engine/render.py uses for OTF fonts.

Pipeline

  1. Load: cairosvg.svg2png() converts SVG to PNG bytes in memory (no temp files)
  2. Resize: PIL scales to fit terminal — width = tw(), height = th() * 2 pixels (each terminal row encodes 2 pixel rows via half-blocks)
  3. Threshold: Convert to greyscale ("L" mode), apply binary threshold to get visible/not-visible
  4. Half-block encode: Walk pixel pairs top-to-bottom. For each 2-row pair, emit (both lit), (top only), (bottom only), or space (neither)
  5. Cache: Results cached per (svg_path, terminal_width, terminal_height) — invalidated on terminal resize

Dependency

cairosvg added as an optional dependency in pyproject.toml (like sounddevice). Figment mode gracefully degrades if unavailable.

Key Function

def rasterize_svg(svg_path: str, width: int, height: int) -> list[str]:
    """Convert SVG file to list of half-block terminal rows (uncolored)."""

Section 2: Figment Overlay Rendering

Integration point: engine/layers.py

New function following the render_message_overlay() pattern.

Function Signature

def render_figment_overlay(figment_state: FigmentState, w: int, h: int) -> list[str]:
    """Return ANSI cursor-positioning commands for the current figment frame."""

Animation Phases (~4.5 seconds total)

Phase Duration Behavior
Reveal ~1.5s Progressive scanline fill. Each frame, a percentage of the figment's non-empty cells become visible in random block order.
Hold ~1.5s Full figment visible. Strobes between full brightness and dimmed/partial visibility every few frames.
Dissolve ~1.5s Inverse of reveal. Cells randomly drop out, replaced by spaces.

Color

A random theme gradient is selected from THEME_REGISTRY at trigger time. Applied via lr_gradient() — the same function that colors headlines and messages.

Positioning

Figment is centered in the viewport. Each visible row is an ANSI \033[row;colH command appended to the buffer, identical to how the message overlay works.

Section 3: FigmentPlugin (Effect Plugin)

File: effects_plugins/figment.py

An EffectPlugin(ABC) subclass that owns the figment lifecycle.

Responsibilities

  • Timer: Tracks elapsed frames. At the configured interval (default 60s), triggers a new figment.
  • SVG selection: Randomly picks from figments/*.svg. Avoids repeating the last shown.
  • State machine: idle -> reveal -> hold -> dissolve -> idle. Tracks phase progress (0.0 to 1.0).
  • Color selection: Picks a random theme key ("green", "orange", "purple") at trigger time.
  • Rasterization: Calls rasterize_svg() on trigger, caches result for the display duration.

State Machine

idle ──(timer fires or trigger received)──> reveal
reveal ──(progress >= 1.0)──> hold
hold ──(progress >= 1.0)──> dissolve
dissolve ──(progress >= 1.0)──> idle

Interface

The process() method is a no-op (returns buffer unchanged) since rendering is handled by the overlay. The plugin exposes state via:

def get_figment_state(self, frame_number: int) -> FigmentState | None:
    """Tick the state machine and return current state, or None if idle."""

This mirrors the ntfy_poller.get_active_message() pattern.

EffectConfig

  • enabled: bool (default False — opt-in)
  • intensity: float — scales strobe frequency and reveal/dissolve speed
  • params:
    • interval_secs: 60 (time between figments)
    • display_secs: 4.5 (total animation duration)
    • figment_dir: "figments" (SVG source directory)

Controllable via C&C: /effects figment on, /effects figment intensity 0.7.

Section 4: Input Abstraction (FigmentTrigger)

File: engine/figment_trigger.py

Protocol

class FigmentTrigger(Protocol):
    def poll(self) -> FigmentCommand | None: ...

FigmentCommand

@dataclass
class FigmentCommand:
    action: str   # "trigger" | "set_intensity" | "set_interval" | "set_color" | "stop"
    value: float | str | None = None

Adapters

Adapter Transport Dependency Status
NtfyTrigger Existing C&C ntfy topic None (reuses ntfy) v1
MqttTrigger MQTT broker paho-mqtt (optional) Future
SerialTrigger USB serial pyserial (optional) Future

Integration

The FigmentPlugin accepts a list of triggers. Each frame, it polls all triggers and acts on commands. Triggers are optional — if none are configured, the plugin runs on its internal timer alone.

EventBus Bridge

Any trigger can publish a FIGMENT_TRIGGER event to the EventBus, allowing other components to react (logging, multi-display sync).

Section 5: Scroll Loop Integration

Minimal change to engine/scroll.py — 5 lines following the ntfy overlay pattern:

# In stream() setup:
figment_plugin = registry.get("figment")

# In frame loop, after effects processing, before display.show():
if figment_plugin and figment_plugin.config.enabled:
    figment_state = figment_plugin.get_figment_state(frame_number)
    if figment_state is not None:
        figment_overlay = render_figment_overlay(figment_state, w, h)
        buf.extend(figment_overlay)

Overlay Priority

Figment overlay appends after effects processing but before the ntfy message overlay. This means:

  • Ntfy messages always appear on top of figments (higher priority)
  • Existing glitch/noise effects run over the ticker underneath the figment

Section 6: File Summary

New Files

File Purpose
effects_plugins/figment.py FigmentPlugin — lifecycle, timer, state machine, SVG selection
engine/figment_render.py SVG to half-block rasterization pipeline
engine/figment_trigger.py FigmentTrigger protocol, FigmentCommand, NtfyTrigger adapter
tests/test_figment.py Plugin lifecycle, state machine, timer, overlay rendering
tests/fixtures/test.svg Minimal SVG for testing rasterization

Modified Files

File Change
engine/scroll.py 5-line figment overlay integration
engine/layers.py Add render_figment_overlay() function
pyproject.toml Add cairosvg as optional dependency

Testing Strategy

  • Unit: State machine transitions, timer accuracy, SVG rasterization output, FigmentCommand parsing
  • Integration: Plugin discovery, overlay rendering with mock terminal dimensions, C&C command handling
  • Fixture: Minimal test.svg (simple shape) for deterministic rasterization tests