Comprehensive plan with 6 chunks, each containing bite-sized TDD tasks: - Chunk 1: Theme class and registry - Chunk 2: Config integration - Chunk 3: Render pipeline - Chunk 4: Message gradient integration - Chunk 5: Color picker UI - Chunk 6: Integration and validation Each step includes exact code, test commands, and expected output.
26 KiB
Color Scheme Switcher Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Implement interactive color theme picker at startup that lets users choose between green, orange, or purple gradients with complementary message queue colors.
Architecture: New themes.py data module defines Theme class and THEME_REGISTRY. Config adds ACTIVE_THEME global set by picker. Render functions read from active theme instead of hardcoded constants. App adds picker UI that mirrors font picker pattern.
Tech Stack: Python 3.10+, ANSI 256-color codes, existing terminal I/O utilities
File Structure
| File | Purpose | Change Type |
|---|---|---|
engine/themes.py |
Theme class, THEME_REGISTRY, color codes | Create |
engine/config.py |
ACTIVE_THEME global, set_active_theme() | Modify |
engine/render.py |
Replace GRAD_COLS/MSG_GRAD_COLS with config lookup | Modify |
engine/scroll.py |
Update message gradient call | Modify |
engine/app.py |
pick_color_theme(), call in main() | Modify |
tests/test_themes.py |
Theme class and registry unit tests | Create |
Chunk 1: Theme Data Module
Task 1: Create themes.py with Theme class and registry
Files:
-
Create:
engine/themes.py -
Test:
tests/test_themes.py -
Step 1: Write failing test for Theme class
Create tests/test_themes.py:
"""Test color themes and registry."""
from engine.themes import Theme, THEME_REGISTRY, get_theme
def test_theme_construction():
"""Theme stores name and gradient lists."""
main = ["\033[1;38;5;231m"] * 12
msg = ["\033[1;38;5;225m"] * 12
theme = Theme(name="Test Green", main_gradient=main, message_gradient=msg)
assert theme.name == "Test Green"
assert theme.main_gradient == main
assert theme.message_gradient == msg
def test_gradient_length():
"""Each gradient must have exactly 12 ANSI codes."""
for theme_id, theme in THEME_REGISTRY.items():
assert len(theme.main_gradient) == 12, f"{theme_id} main gradient wrong length"
assert len(theme.message_gradient) == 12, f"{theme_id} message gradient wrong length"
def test_theme_registry_has_three_themes():
"""Registry contains green, orange, purple."""
assert len(THEME_REGISTRY) == 3
assert "green" in THEME_REGISTRY
assert "orange" in THEME_REGISTRY
assert "purple" in THEME_REGISTRY
def test_get_theme_valid():
"""get_theme returns Theme object for valid ID."""
theme = get_theme("green")
assert isinstance(theme, Theme)
assert theme.name == "Verdant Green"
def test_get_theme_invalid():
"""get_theme raises KeyError for invalid ID."""
with pytest.raises(KeyError):
get_theme("invalid_theme")
def test_green_theme_unchanged():
"""Green theme uses original green → magenta colors."""
green_theme = get_theme("green")
# First color should be white (bold)
assert green_theme.main_gradient[0] == "\033[1;38;5;231m"
# Last deep green
assert green_theme.main_gradient[9] == "\033[38;5;22m"
# Message gradient is magenta
assert green_theme.message_gradient[9] == "\033[38;5;89m"
Run: pytest tests/test_themes.py -v
Expected: FAIL (module doesn't exist)
- Step 2: Create themes.py with Theme class and finalized gradients
Create engine/themes.py:
"""Color theme definitions and registry."""
from typing import Optional
class Theme:
"""Encapsulates a color scheme: name, main gradient, message gradient."""
def __init__(self, name: str, main_gradient: list[str], message_gradient: list[str]):
"""Initialize theme with display name and gradient lists.
Args:
name: Display name (e.g., "Verdant Green")
main_gradient: List of 12 ANSI 256-color codes (white → primary color)
message_gradient: List of 12 ANSI codes (white → complementary color)
"""
self.name = name
self.main_gradient = main_gradient
self.message_gradient = message_gradient
# ─── FINALIZED GRADIENTS ──────────────────────────────────────────────────
# Each gradient: white → primary/complementary, 12 steps total
# Format: "\033[<brightness>;<color>m" where color is 38;5;<colorcode>
_GREEN_MAIN = [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;195m", # pale white-tint
"\033[38;5;123m", # bright cyan
"\033[38;5;118m", # bright lime
"\033[38;5;82m", # lime
"\033[38;5;46m", # bright green
"\033[38;5;40m", # green
"\033[38;5;34m", # medium green
"\033[38;5;28m", # dark green
"\033[38;5;22m", # deep green
"\033[2;38;5;22m", # dim deep green
"\033[2;38;5;235m", # near black
]
_GREEN_MESSAGE = [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;225m", # pale pink-white
"\033[38;5;219m", # bright pink
"\033[38;5;213m", # hot pink
"\033[38;5;207m", # magenta
"\033[38;5;201m", # bright magenta
"\033[38;5;165m", # orchid-red
"\033[38;5;161m", # ruby-magenta
"\033[38;5;125m", # dark magenta
"\033[38;5;89m", # deep maroon-magenta
"\033[2;38;5;89m", # dim deep maroon-magenta
"\033[2;38;5;235m", # near black
]
_ORANGE_MAIN = [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;215m", # pale orange-white
"\033[38;5;209m", # bright orange
"\033[38;5;208m", # vibrant orange
"\033[38;5;202m", # orange
"\033[38;5;166m", # dark orange
"\033[38;5;130m", # burnt orange
"\033[38;5;94m", # rust
"\033[38;5;58m", # dark rust
"\033[38;5;94m", # rust (hold)
"\033[2;38;5;94m", # dim rust
"\033[2;38;5;235m", # near black
]
_ORANGE_MESSAGE = [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;195m", # pale cyan-white
"\033[38;5;33m", # bright blue
"\033[38;5;27m", # blue
"\033[38;5;21m", # deep blue
"\033[38;5;21m", # deep blue (hold)
"\033[38;5;21m", # deep blue (hold)
"\033[38;5;18m", # navy
"\033[38;5;18m", # navy (hold)
"\033[38;5;18m", # navy (hold)
"\033[2;38;5;18m", # dim navy
"\033[2;38;5;235m", # near black
]
_PURPLE_MAIN = [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;225m", # pale purple-white
"\033[38;5;177m", # bright purple
"\033[38;5;171m", # vibrant purple
"\033[38;5;165m", # purple
"\033[38;5;135m", # medium purple
"\033[38;5;129m", # purple
"\033[38;5;93m", # dark purple
"\033[38;5;57m", # deep purple
"\033[38;5;57m", # deep purple (hold)
"\033[2;38;5;57m", # dim deep purple
"\033[2;38;5;235m", # near black
]
_PURPLE_MESSAGE = [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;226m", # pale yellow-white
"\033[38;5;226m", # bright yellow
"\033[38;5;220m", # yellow
"\033[38;5;220m", # yellow (hold)
"\033[38;5;184m", # dark yellow
"\033[38;5;184m", # dark yellow (hold)
"\033[38;5;178m", # olive-yellow
"\033[38;5;178m", # olive-yellow (hold)
"\033[38;5;172m", # golden
"\033[2;38;5;172m", # dim golden
"\033[2;38;5;235m", # near black
]
# ─── THEME REGISTRY ───────────────────────────────────────────────────────
THEME_REGISTRY = {
"green": Theme(
name="Verdant Green",
main_gradient=_GREEN_MAIN,
message_gradient=_GREEN_MESSAGE,
),
"orange": Theme(
name="Molten Orange",
main_gradient=_ORANGE_MAIN,
message_gradient=_ORANGE_MESSAGE,
),
"purple": Theme(
name="Violet Purple",
main_gradient=_PURPLE_MAIN,
message_gradient=_PURPLE_MESSAGE,
),
}
def get_theme(theme_id: str) -> Theme:
"""Retrieve a theme by ID.
Args:
theme_id: One of "green", "orange", "purple"
Returns:
Theme object
Raises:
KeyError: If theme_id not found in registry
"""
if theme_id not in THEME_REGISTRY:
raise KeyError(f"Unknown theme: {theme_id}. Available: {list(THEME_REGISTRY.keys())}")
return THEME_REGISTRY[theme_id]
- Step 3: Run tests to verify they pass
Run: pytest tests/test_themes.py -v
Expected: PASS (all 6 tests)
- Step 4: Commit
git add engine/themes.py tests/test_themes.py
git commit -m "feat: create Theme class and registry with finalized color gradients
- Define Theme class to encapsulate name and main/message gradients
- Create THEME_REGISTRY with green, orange, purple themes
- Each gradient has 12 ANSI 256-color codes finalized
- Complementary color pairs: green/magenta, orange/blue, purple/yellow
- Add get_theme() lookup with error handling
- Add comprehensive unit tests"
Chunk 2: Config Integration
Task 2: Add ACTIVE_THEME global and set_active_theme() to config.py
Files:
-
Modify:
engine/config.py:1-30 -
Test:
tests/test_config.py(expand existing) -
Step 1: Write failing tests for config changes
Add to tests/test_config.py:
def test_active_theme_initially_none():
"""ACTIVE_THEME is None before initialization."""
# This test may fail if config is already initialized
# We'll set it to None first for testing
import engine.config
engine.config.ACTIVE_THEME = None
assert engine.config.ACTIVE_THEME is None
def test_set_active_theme_green():
"""set_active_theme('green') sets ACTIVE_THEME to green theme."""
from engine.config import set_active_theme
from engine.themes import get_theme
set_active_theme("green")
assert config.ACTIVE_THEME is not None
assert config.ACTIVE_THEME.name == "Verdant Green"
assert config.ACTIVE_THEME == get_theme("green")
def test_set_active_theme_default():
"""set_active_theme() with no args defaults to green."""
from engine.config import set_active_theme
set_active_theme()
assert config.ACTIVE_THEME.name == "Verdant Green"
def test_set_active_theme_invalid():
"""set_active_theme() with invalid ID raises KeyError."""
from engine.config import set_active_theme
with pytest.raises(KeyError):
set_active_theme("invalid")
Run: pytest tests/test_config.py -v
Expected: FAIL (functions don't exist yet)
- Step 2: Add ACTIVE_THEME global and set_active_theme() to config.py
Edit engine/config.py, add after line 30 (after _resolve_font_path function):
# ─── COLOR THEME ──────────────────────────────────────────────────────────
ACTIVE_THEME = None # set by set_active_theme() after picker
def set_active_theme(theme_id: str = "green"):
"""Set the active color theme. Defaults to 'green' if not specified.
Args:
theme_id: One of "green", "orange", "purple"
Raises:
KeyError: If theme_id is invalid
"""
global ACTIVE_THEME
from engine import themes
ACTIVE_THEME = themes.get_theme(theme_id)
- Step 3: Remove hardcoded GRAD_COLS and MSG_GRAD_COLS from render.py
Edit engine/render.py, find and delete lines 20-49 (the hardcoded gradient arrays):
# DELETED:
# GRAD_COLS = [...]
# MSG_GRAD_COLS = [...]
- Step 4: Run tests to verify they pass
Run: pytest tests/test_config.py::test_active_theme_initially_none -v
Run: pytest tests/test_config.py::test_set_active_theme_green -v
Run: pytest tests/test_config.py::test_set_active_theme_default -v
Run: pytest tests/test_config.py::test_set_active_theme_invalid -v
Expected: PASS (all 4 new tests)
- Step 5: Verify existing config tests still pass
Run: pytest tests/test_config.py -v
Expected: PASS (all existing + new tests)
- Step 6: Commit
git add engine/config.py tests/test_config.py
git commit -m "feat: add ACTIVE_THEME global and set_active_theme() to config
- Add ACTIVE_THEME global (initialized to None)
- Add set_active_theme(theme_id) function with green default
- Remove hardcoded GRAD_COLS and MSG_GRAD_COLS (move to themes.py)
- Add comprehensive tests for theme setting"
Chunk 3: Render Pipeline Integration
Task 3: Update render.py to use config.ACTIVE_THEME
Files:
-
Modify:
engine/render.py:15-220 -
Test:
tests/test_render.py(expand existing) -
Step 1: Write failing test for lr_gradient with theme
Add to tests/test_render.py:
def test_lr_gradient_uses_active_theme(monkeypatch):
"""lr_gradient uses config.ACTIVE_THEME when cols=None."""
from engine import config, render
from engine.themes import get_theme
# Set orange theme
config.set_active_theme("orange")
# Create simple rows
rows = ["test row"]
result = render.lr_gradient(rows, offset=0, cols=None)
# Result should start with first color from orange main gradient
assert result[0].startswith("\033[1;38;5;231m") # white (same for all)
def test_lr_gradient_fallback_when_no_theme(monkeypatch):
"""lr_gradient uses fallback when ACTIVE_THEME is None."""
from engine import config, render
# Clear active theme
config.ACTIVE_THEME = None
rows = ["test row"]
result = render.lr_gradient(rows, offset=0, cols=None)
# Should not crash and should return something
assert result is not None
assert len(result) > 0
def test_default_green_gradient_length():
"""_default_green_gradient returns 12 colors."""
from engine import render
colors = render._default_green_gradient()
assert len(colors) == 12
Run: pytest tests/test_render.py::test_lr_gradient_uses_active_theme -v
Expected: FAIL (function signature doesn't match)
- Step 2: Update lr_gradient() to use config.ACTIVE_THEME
Edit engine/render.py, find the lr_gradient() function (around line 194) and update it:
def lr_gradient(rows, offset, cols=None):
"""
Render rows through a left-to-right color sweep.
Args:
rows: List of text rows to colorize
offset: Gradient position offset (for animation)
cols: Optional list of color codes. If None, uses active theme.
Returns:
List of colorized rows
"""
if cols is None:
from engine import config
cols = (
config.ACTIVE_THEME.main_gradient
if config.ACTIVE_THEME
else _default_green_gradient()
)
# ... rest of function unchanged ...
- Step 3: Add _default_green_gradient() fallback function
Add to engine/render.py before lr_gradient():
def _default_green_gradient():
"""Fallback green gradient (original colors) for initialization."""
return [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;195m", # pale white-tint
"\033[38;5;123m", # bright cyan
"\033[38;5;118m", # bright lime
"\033[38;5;82m", # lime
"\033[38;5;46m", # bright green
"\033[38;5;40m", # green
"\033[38;5;34m", # medium green
"\033[38;5;28m", # dark green
"\033[38;5;22m", # deep green
"\033[2;38;5;22m", # dim deep green
"\033[2;38;5;235m", # near black
]
def _default_magenta_gradient():
"""Fallback magenta gradient (original message colors) for initialization."""
return [
"\033[1;38;5;231m", # white (bold)
"\033[1;38;5;225m", # pale pink-white
"\033[38;5;219m", # bright pink
"\033[38;5;213m", # hot pink
"\033[38;5;207m", # magenta
"\033[38;5;201m", # bright magenta
"\033[38;5;165m", # orchid-red
"\033[38;5;161m", # ruby-magenta
"\033[38;5;125m", # dark magenta
"\033[38;5;89m", # deep maroon-magenta
"\033[2;38;5;89m", # dim deep maroon-magenta
"\033[2;38;5;235m", # near black
]
- Step 4: Run tests to verify they pass
Run: pytest tests/test_render.py::test_lr_gradient_uses_active_theme -v
Run: pytest tests/test_render.py::test_lr_gradient_fallback_when_no_theme -v
Run: pytest tests/test_render.py::test_default_green_gradient_length -v
Expected: PASS (all 3 new tests)
- Step 5: Run full render test suite
Run: pytest tests/test_render.py -v
Expected: PASS (existing tests may need adjustment for mocking)
- Step 6: Commit
git add engine/render.py tests/test_render.py
git commit -m "feat: update lr_gradient to use config.ACTIVE_THEME
- Update lr_gradient(cols=None) to check config.ACTIVE_THEME
- Add _default_green_gradient() and _default_magenta_gradient() fallbacks
- Fallback used when ACTIVE_THEME is None (non-interactive init)
- Add tests for theme-aware and fallback gradient rendering"
Chunk 4: Message Gradient Integration
Task 4: Update scroll.py to use message gradient from config
Files:
-
Modify:
engine/scroll.py:85-95 -
Test: existing
tests/test_scroll.py -
Step 1: Locate message gradient calls in scroll.py
Run: grep -n "MSG_GRAD_COLS\|lr_gradient_opposite" /Users/genejohnson/Dev/mainline/engine/scroll.py
Expected: Should find line(s) where MSG_GRAD_COLS or similar is used
- Step 2: Update scroll.py to use theme message gradient
Edit engine/scroll.py, find the line that uses message gradients (around line 89 based on spec) and update:
Old code:
# Some variation of:
rows = lr_gradient(rows, offset, MSG_GRAD_COLS)
New code:
from engine import config
msg_cols = (
config.ACTIVE_THEME.message_gradient
if config.ACTIVE_THEME
else render._default_magenta_gradient()
)
rows = lr_gradient(rows, offset, msg_cols)
Or use the helper approach (create msg_gradient() in render.py):
def msg_gradient(rows, offset):
"""Apply message (ntfy) gradient using theme complementary colors."""
from engine import config
cols = (
config.ACTIVE_THEME.message_gradient
if config.ACTIVE_THEME
else _default_magenta_gradient()
)
return lr_gradient(rows, offset, cols)
Then in scroll.py:
rows = render.msg_gradient(rows, offset)
- Step 3: Run existing scroll tests
Run: pytest tests/test_scroll.py -v
Expected: PASS (existing functionality unchanged)
- Step 4: Commit
git add engine/scroll.py engine/render.py
git commit -m "feat: update scroll.py to use theme message gradient
- Replace MSG_GRAD_COLS reference with config.ACTIVE_THEME.message_gradient
- Use fallback magenta gradient when theme not initialized
- Ensure ntfy messages render in complementary color from selected theme"
Chunk 5: Color Picker UI
Task 5: Create pick_color_theme() function in app.py
Files:
-
Modify:
engine/app.py:1-300 -
Test: manual/integration (interactive)
-
Step 1: Write helper functions for color picker UI
Edit engine/app.py, add before pick_font_face() function:
def _draw_color_picker(themes_list, selected):
"""Draw the color theme picker menu."""
import sys
from engine.terminal import CLR, W_GHOST, G_HI, G_DIM, tw
print(CLR, end="")
print()
print(f" {G_HI}▼ COLOR THEME{W_GHOST} ─ ↑/↓ or j/k to move, Enter/q to select{G_DIM}")
print(f" {W_GHOST}{'─' * (tw() - 4)}\n")
for i, (theme_id, theme) in enumerate(themes_list):
prefix = " ▶ " if i == selected else " "
color = G_HI if i == selected else ""
reset = "" if i == selected else W_GHOST
print(f"{prefix}{color}{theme.name}{reset}")
print()
- Step 2: Create pick_color_theme() function
Edit engine/app.py, add after helper function:
def pick_color_theme():
"""Interactive color theme picker. Defaults to 'green' if not TTY."""
import sys
import termios
import tty
from engine import config, themes
# Non-interactive fallback: use green
if not sys.stdin.isatty():
config.set_active_theme("green")
return
themes_list = list(themes.THEME_REGISTRY.items())
selected = 0
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
while True:
_draw_color_picker(themes_list, selected)
key = _read_picker_key()
if key == "up":
selected = max(0, selected - 1)
elif key == "down":
selected = min(len(themes_list) - 1, selected + 1)
elif key == "enter":
break
elif key == "interrupt":
raise KeyboardInterrupt
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
selected_theme_id = themes_list[selected][0]
config.set_active_theme(selected_theme_id)
theme_name = themes_list[selected][1].name
print(f" {G_DIM}> using {theme_name}{RST}")
time.sleep(0.8)
print(CLR, end="")
print(CURSOR_OFF, end="")
print()
- Step 3: Update main() to call pick_color_theme() before pick_font_face()
Edit engine/app.py, find the main() function and locate where pick_font_face() is called (around line 265). Add before it:
def main():
# ... existing signal handler setup ...
pick_color_theme() # NEW LINE - before font picker
pick_font_face()
# ... rest of main unchanged ...
- Step 4: Manual test - run in interactive terminal
Run: python3 mainline.py
Expected:
-
See color theme picker menu before font picker
-
Can navigate with ↑/↓ or j/k
-
Can select with Enter or q
-
Selected theme applies to scrolling headlines
-
Can select different themes and see colors change
-
Step 5: Manual test - run in non-interactive environment
Run: echo "" | python3 mainline.py
Expected:
-
No color picker menu shown
-
Defaults to green theme
-
App runs without error
-
Step 6: Commit
git add engine/app.py
git commit -m "feat: add pick_color_theme() UI and integration
- Create _draw_color_picker() to render menu
- Create pick_color_theme() function mirroring font picker pattern
- Integrate into main() before font picker
- Fallback to green theme in non-interactive environments
- Support arrow keys and j/k navigation"
Chunk 6: Integration & Validation
Task 6: End-to-end testing and cleanup
Files:
-
Test: All modified files
-
Verify: App functionality
-
Step 1: Run full test suite
Run: pytest tests/ -v
Expected: PASS (all tests, including new ones)
- Step 2: Run linter
Run: ruff check engine/ mainline.py
Expected: No errors (fix any style issues)
- Step 3: Manual integration test - green theme
Run: python3 mainline.py
Then select "Verdant Green" from picker.
Expected:
-
Headlines render in green → deep green
-
ntfy messages render in magenta gradient
-
Both work correctly during streaming
-
Step 4: Manual integration test - orange theme
Run: python3 mainline.py
Then select "Molten Orange" from picker.
Expected:
-
Headlines render in orange → deep orange
-
ntfy messages render in blue gradient
-
Colors are visually distinct from green
-
Step 5: Manual integration test - purple theme
Run: python3 mainline.py
Then select "Violet Purple" from picker.
Expected:
-
Headlines render in purple → deep purple
-
ntfy messages render in yellow gradient
-
Colors are visually distinct from green and orange
-
Step 6: Test poetry mode with color picker
Run: python3 mainline.py --poetry
Then select "orange" from picker.
Expected:
-
Poetry mode works with color picker
-
Colors apply to poetry rendering
-
Step 7: Test code mode with color picker
Run: python3 mainline.py --code
Then select "purple" from picker.
Expected:
-
Code mode works with color picker
-
Colors apply to code rendering
-
Step 8: Verify acceptance criteria
✓ Color picker displays 3 theme options at startup ✓ Selection applies to all headline and message gradients ✓ Boot UI (title, status) uses hardcoded green (not theme) ✓ Scrolling headlines and ntfy messages use theme gradients ✓ No persistence between runs (each run picks fresh) ✓ Non-TTY environments default to green without error ✓ Architecture supports future random/animation modes ✓ All gradient color codes finalized with no TBD values
- Step 9: Final commit
git add -A
git commit -m "feat: color scheme switcher implementation complete
Closes color-pick feature with:
- Three selectable color themes (green, orange, purple)
- Interactive menu at startup (mirrors font picker UI)
- Complementary colors for ntfy message queue
- Fallback to green in non-interactive environments
- All tests passing, manual validation complete"
- Step 10: Create feature branch PR summary
## Color Scheme Switcher
Implements interactive color theme selection for Mainline news ticker.
### What's New
- 3 color themes: Verdant Green, Molten Orange, Violet Purple
- Interactive picker at startup (↑/↓ or j/k, Enter to select)
- Complementary gradients for ntfy messages (magenta, blue, yellow)
- Fresh theme selection each run (no persistence)
### Files Changed
- `engine/themes.py` (new)
- `engine/config.py` (ACTIVE_THEME, set_active_theme)
- `engine/render.py` (theme-aware gradients)
- `engine/scroll.py` (message gradient integration)
- `engine/app.py` (pick_color_theme UI)
- `tests/test_themes.py` (new theme tests)
- `README.md` (documentation)
### Acceptance Criteria
All met. App fully tested and ready for merge.
Testing Checklist
- Unit tests:
pytest tests/test_themes.py -v - Unit tests:
pytest tests/test_config.py -v - Unit tests:
pytest tests/test_render.py -v - Full suite:
pytest tests/ -v - Linting:
ruff check engine/ mainline.py - Manual: Green theme selection
- Manual: Orange theme selection
- Manual: Purple theme selection
- Manual: Poetry mode with colors
- Manual: Code mode with colors
- Manual: Non-TTY fallback
Notes
themes.pyis data-only; never import config or render to prevent cyclesACTIVE_THEMEinitialized to None; guaranteed non-None before stream() via pick_color_theme()- Font picker UI remains hardcoded green; title/subtitle use G_HI/G_DIM constants (not theme)
- Message gradients use complementary colors; lookup in scroll.py
- Each gradient has 12 colors; verify length in tests
- No persistence; fresh picker each run