docs: add color scheme switcher design spec

This commit is contained in:
2026-03-16 02:40:32 -07:00
parent d0e18518a2
commit 03cdd70ad0

View File

@@ -0,0 +1,204 @@
# 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