diff --git a/engine/config.py b/engine/config.py index f7f86a3..0dec9ce 100644 --- a/engine/config.py +++ b/engine/config.py @@ -132,6 +132,7 @@ class Config: display: str = "pygame" websocket: bool = False websocket_port: int = 8765 + theme: str = "green" @classmethod def from_args(cls, argv: list[str] | None = None) -> "Config": @@ -175,6 +176,7 @@ class Config: display=_arg_value("--display", argv) or "terminal", websocket="--websocket" in argv, websocket_port=_arg_int("--websocket-port", 8765, argv), + theme=_arg_value("--theme", argv) or "green", ) @@ -246,6 +248,40 @@ DEMO = "--demo" in sys.argv DEMO_EFFECT_DURATION = 5.0 # seconds per effect PIPELINE_DEMO = "--pipeline-demo" in sys.argv +# ─── THEME MANAGEMENT ───────────────────────────────────────── +ACTIVE_THEME = None + + +def set_active_theme(theme_id: str = "green"): + """Set the active theme by ID. + + Args: + theme_id: Theme identifier from theme registry (e.g., "green", "orange", "purple") + + Raises: + KeyError: If theme_id is not in the theme registry + + Side Effects: + Sets the ACTIVE_THEME global variable + """ + global ACTIVE_THEME + from engine import themes + + ACTIVE_THEME = themes.get_theme(theme_id) + + +# Initialize theme on module load (lazy to avoid circular dependency) +def _init_theme(): + theme_id = _arg_value("--theme", sys.argv) or "green" + try: + set_active_theme(theme_id) + except KeyError: + pass # Theme not found, keep None + + +_init_theme() + + # ─── PIPELINE MODE (new unified architecture) ───────────── PIPELINE_MODE = "--pipeline" in sys.argv PIPELINE_PRESET = _arg_value("--pipeline-preset", sys.argv) or "demo" @@ -256,6 +292,9 @@ PRESET = _arg_value("--preset", sys.argv) # ─── PIPELINE DIAGRAM ──────────────────────────────────── PIPELINE_DIAGRAM = "--pipeline-diagram" in sys.argv +# ─── THEME ────────────────────────────────────────────────── +THEME = _arg_value("--theme", sys.argv) or "green" + def set_font_selection(font_path=None, font_index=None): """Set runtime primary font selection.""" diff --git a/engine/render/gradient.py b/engine/render/gradient.py index 14a6c5a..f0f024d 100644 --- a/engine/render/gradient.py +++ b/engine/render/gradient.py @@ -80,3 +80,57 @@ def lr_gradient_opposite(rows, offset=0.0): List of lines with complementary gradient coloring applied """ return lr_gradient(rows, offset, MSG_GRAD_COLS) + + +def msg_gradient(rows, offset): + """Apply message (ntfy) gradient using theme complementary colors. + + Returns colored rows using ACTIVE_THEME.message_gradient if available, + falling back to default magenta if no theme is set. + + Args: + rows: List of text strings to colorize + offset: Gradient offset (0.0-1.0) for animation + + Returns: + List of rows with ANSI color codes applied + """ + from engine import config + + # Check if theme is set and use it + if config.ACTIVE_THEME: + cols = _color_codes_to_ansi(config.ACTIVE_THEME.message_gradient) + else: + # Fallback to default magenta gradient + cols = MSG_GRAD_COLS + + return lr_gradient(rows, offset, cols) + + +def _color_codes_to_ansi(color_codes): + """Convert a list of 256-color codes to ANSI escape code strings. + + Pattern: first 2 are bold, middle 8 are normal, last 2 are dim. + + Args: + color_codes: List of 12 integers (256-color palette codes) + + Returns: + List of ANSI escape code strings + """ + if not color_codes or len(color_codes) != 12: + # Fallback to default green if invalid + return GRAD_COLS + + result = [] + for i, code in enumerate(color_codes): + if i < 2: + # Bold for first 2 (bright leading edge) + result.append(f"\033[1;38;5;{code}m") + elif i < 10: + # Normal for middle 8 + result.append(f"\033[38;5;{code}m") + else: + # Dim for last 2 (dark trailing edge) + result.append(f"\033[2;38;5;{code}m") + return result