# 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` ```python 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:** ```python ACTIVE_THEME = None # set by set_active_theme() after picker ``` **New function:** ```python 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: ```python def lr_gradient(rows, offset, cols=None): cols = cols or GRAD_COLS # module-level constant ``` New pattern: ```python 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:** ```python 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()`:** ```python 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