Files
Mainline/docs/superpowers/specs/2026-03-16-color-scheme-design.md

6.1 KiB

Color Scheme Switcher Design

Date: 2026-03-16 Status: Draft Scope: Interactive color theme selection for Mainline news ticker


Overview

Mainline currently renders news headlines with a fixed white-hot → deep green gradient. This feature adds an interactive theme picker at startup that lets users choose between three precise color schemes (green, orange, purple), each with complementary message queue colors.

The implementation uses a dedicated Theme class to encapsulate gradients and metadata, enabling future extensions like random rotation, animation, or additional themes without architectural changes.


Requirements

Functional:

  1. User selects a color theme from an interactive menu at startup (green, orange, or purple)
  2. Main headline gradient uses the selected primary color (white → color)
  3. Message queue (ntfy) gradient uses the precise complementary color (white → opposite)
  4. Selection is fresh each run (no persistence)
  5. Design supports future "random rotation" mode without refactoring

Complementary colors (precise opposites):

  • Green (38;5;22) → Magenta (38;5;89) (current, unchanged)
  • Orange (38;5;208) → Blue (38;5;21)
  • Purple (38;5;129) → Yellow (38;5;226)

Non-functional:

  • Reuse the existing font picker pattern for UI consistency
  • Zero runtime overhead during streaming (theme lookup happens once at startup)
  • Boot messages and title render in the selected theme

Architecture

New Module: engine/themes.py

class Theme:
    """Encapsulates a color scheme: name, main gradient, message gradient."""

    def __init__(self, name: str, main_gradient: list[str], message_gradient: list[str]):
        self.name = name
        self.main_gradient = main_gradient      # white → primary color
        self.message_gradient = message_gradient  # white → complementary

Theme Registry: Three instances registered by ID: "verdant", "molten", "violet".

Each gradient is a list of 12 ANSI 256-color codes matching the current format in render.py:

[
    "\033[1;38;5;231m",  # white (bold)
    "\033[1;38;5;195m",  # pale white-tint
    ...,
    "\033[2;38;5;235m",  # near black
]

Public API:

  • get_theme(theme_id: str) -> Theme — lookup by ID, raises if not found
  • THEME_REGISTRY — dict of all available themes (for picker)

Modified: engine/config.py

New globals:

ACTIVE_THEME = None  # set by set_active_theme() after picker

New function:

def set_active_theme(theme_id: str):
    """Set the active theme after user selection."""
    global ACTIVE_THEME
    from engine import themes
    ACTIVE_THEME = themes.get_theme(theme_id)

Removal:

  • Delete hardcoded GRAD_COLS and MSG_GRAD_COLS — these move to themes.py

Modified: engine/render.py

Updated gradient access:

Old pattern:

def lr_gradient(rows, offset, cols=None):
    cols = cols or GRAD_COLS  # module-level constant

New pattern:

def lr_gradient(rows, offset, cols=None):
    if cols is None:
        from engine import config
        cols = config.ACTIVE_THEME.main_gradient if config.ACTIVE_THEME else _DEFAULT_GRAD
    # ... rest of function unchanged

Fallback: Define _DEFAULT_GRAD (current green) for cases where theme isn't selected yet.

Message gradient lookup:

def msg_gradient_rows(rows, offset):
    from engine import config
    return lr_gradient(rows, offset, config.ACTIVE_THEME.message_gradient)

Modified: engine/app.py

New function: pick_color_theme()

Mirrors pick_font_face() pattern:

  1. Display menu: [1] Verdant Green [2] Molten Orange [3] Violet Purple
  2. Read arrow keys (↑/↓) or j/k to navigate
  3. Return selected theme ID
  4. Call config.set_active_theme(theme_id)

Placement in main():

def main():
    # ... signal handler setup ...
    pick_color_theme()  # NEW — before title/subtitle
    pick_font_face()
    # ... rest of boot sequence ...

This ensures boot messages (title, subtitle, status lines) render in the selected theme color.


Data Flow

User starts: mainline.py
    ↓
main() called
    ↓
pick_color_theme()
    → Display menu
    → Read user input
    → config.set_active_theme(theme_id)
    ↓
pick_font_face() — renders with active theme
    ↓
Boot messages (title, status, etc.) — all use active theme
    ↓
stream() — news + messages use active theme gradients
    ↓
On exit: no persistence

Implementation Notes

Color Selection

ANSI 256-color palette is used throughout. Each gradient has 12 steps from white to deep primary/complementary color, matching the current green gradient structure exactly.

Orange gradient: 231 → 195 → 214 → 208 → 202 → 166 → 130 → 94 → 58 → 94 → 94 (dim) → 235 Purple gradient: 231 → 225 → 183 → 177 → 171 → 165 → 129 → 93 → 57 → 57 → 57 (dim) → 235 (Exact codes TBD via manual testing for visual balance)

Future Extensions

The Theme class can be extended with:

  • animation_delay: for strobing/pulsing themes
  • metadata: tags like ["cyberpunk", "retro", "ethereal"]
  • random_pick(): returns a random theme (enables future --color=random flag)

The picker can offer a "Random" option without any code changes to render or config.

Testing

  • Unit test themes.py for Theme construction and registry lookup
  • Integration test that boot sequence respects active theme
  • No changes needed to existing gradient tests (they now pull from config.ACTIVE_THEME)

Files Changed

  • engine/themes.py (new)
  • engine/config.py (add ACTIVE_THEME, set_active_theme())
  • engine/render.py (update gradient access to use config.ACTIVE_THEME)
  • engine/app.py (add pick_color_theme(), call in main)
  • tests/test_themes.py (new unit tests)

Acceptance Criteria

  1. ✓ Color picker displays 3 theme options at startup
  2. ✓ Selection applies to all headline and message gradients
  3. ✓ Boot messages render in selected theme
  4. ✓ No persistence between runs
  5. ✓ Architecture supports future random/animation modes