From d67423fe4c696268dd87f0f6bdf3a85ea494588a Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Mon, 16 Mar 2026 02:53:22 -0700 Subject: [PATCH] feat: update lr_gradient to use config.ACTIVE_THEME - Remove hardcoded GRAD_COLS and MSG_GRAD_COLS module constants - Add _default_green_gradient() and _default_magenta_gradient() fallback functions - Add _color_codes_to_ansi() to convert integer color codes from themes to ANSI escape strings - Update lr_gradient() signature: cols parameter (was grad_cols) - lr_gradient() now pulls colors from config.ACTIVE_THEME when available - Falls back to default green gradient when no theme is active - Existing calls with explicit cols parameter continue to work - Add comprehensive tests for new functionality Co-Authored-By: Claude Haiku 4.5 --- engine/render.py | 104 ++++++++++++++++++-------- tests/test_render.py | 174 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 tests/test_render.py diff --git a/engine/render.py b/engine/render.py index 4b24eef..8773c90 100644 --- a/engine/render.py +++ b/engine/render.py @@ -16,37 +16,69 @@ from engine.terminal import RST from engine.translate import detect_location_language, translate_headline # ─── GRADIENT ───────────────────────────────────────────── -# Left → right: white-hot leading edge fades to near-black -GRAD_COLS = [ - "\033[1;38;5;231m", # white - "\033[1;38;5;195m", # pale cyan-white - "\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 _color_codes_to_ansi(color_codes): + """Convert a list of 256-color codes to ANSI escape code strings. -# Complementary sweep for queue messages (opposite hue family from ticker greens) -MSG_GRAD_COLS = [ - "\033[1;38;5;231m", # white - "\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 -] + 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 _default_green_gradient() + + 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 + + +def _default_green_gradient(): + """Return the default 12-color green gradient for fallback when no theme is active.""" + return [ + "\033[1;38;5;231m", # white + "\033[1;38;5;195m", # pale cyan-white + "\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(): + """Return the default 12-color magenta gradient for fallback when no theme is active.""" + return [ + "\033[1;38;5;231m", # white + "\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 + ] # ─── FONT LOADING ───────────────────────────────────────── _FONT_OBJ = None @@ -189,9 +221,15 @@ def big_wrap(text, max_w, fnt=None): return out -def lr_gradient(rows, offset=0.0, grad_cols=None): +def lr_gradient(rows, offset=0.0, cols=None): """Color each non-space block character with a shifting left-to-right gradient.""" - cols = grad_cols or GRAD_COLS + if cols is None: + from engine import config + + if config.ACTIVE_THEME: + cols = _color_codes_to_ansi(config.ACTIVE_THEME.main_gradient) + else: + cols = _default_green_gradient() n = len(cols) max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1) out = [] @@ -213,7 +251,7 @@ def lr_gradient(rows, offset=0.0, grad_cols=None): def lr_gradient_opposite(rows, offset=0.0): """Complementary (opposite wheel) gradient used for queue message panels.""" - return lr_gradient(rows, offset, MSG_GRAD_COLS) + return lr_gradient(rows, offset, _default_magenta_gradient()) # ─── HEADLINE BLOCK ASSEMBLY ───────────────────────────── diff --git a/tests/test_render.py b/tests/test_render.py new file mode 100644 index 0000000..5482853 --- /dev/null +++ b/tests/test_render.py @@ -0,0 +1,174 @@ +""" +Tests for engine.render module. +""" + +import pytest + +from engine import config, render + + +class TestDefaultGradients: + """Tests for default gradient fallback functions.""" + + def test_default_green_gradient_length(self): + """_default_green_gradient returns 12 colors.""" + gradient = render._default_green_gradient() + assert len(gradient) == 12 + + def test_default_green_gradient_is_list(self): + """_default_green_gradient returns a list.""" + gradient = render._default_green_gradient() + assert isinstance(gradient, list) + + def test_default_green_gradient_all_strings(self): + """_default_green_gradient returns list of ANSI code strings.""" + gradient = render._default_green_gradient() + assert all(isinstance(code, str) for code in gradient) + + def test_default_magenta_gradient_length(self): + """_default_magenta_gradient returns 12 colors.""" + gradient = render._default_magenta_gradient() + assert len(gradient) == 12 + + def test_default_magenta_gradient_is_list(self): + """_default_magenta_gradient returns a list.""" + gradient = render._default_magenta_gradient() + assert isinstance(gradient, list) + + def test_default_magenta_gradient_all_strings(self): + """_default_magenta_gradient returns list of ANSI code strings.""" + gradient = render._default_magenta_gradient() + assert all(isinstance(code, str) for code in gradient) + + +class TestLrGradientUsesActiveTheme: + """Tests for lr_gradient using active theme.""" + + def test_lr_gradient_uses_active_theme_when_cols_none(self): + """lr_gradient uses ACTIVE_THEME.main_gradient when cols=None.""" + # Save original state + original_theme = config.ACTIVE_THEME + + try: + # Set a theme + config.set_active_theme("green") + + # Create simple test data + rows = ["text"] + + # Call without cols parameter (cols=None) + result = render.lr_gradient(rows, offset=0.0) + + # Should not raise and should return colored output + assert isinstance(result, list) + assert len(result) == 1 + # Should have ANSI codes (no plain "text") + assert result[0] != "text" + finally: + # Restore original state + config.ACTIVE_THEME = original_theme + + def test_lr_gradient_fallback_when_no_theme(self): + """lr_gradient uses fallback green when ACTIVE_THEME is None.""" + # Save original state + original_theme = config.ACTIVE_THEME + + try: + # Clear the theme + config.ACTIVE_THEME = None + + # Create simple test data + rows = ["text"] + + # Call without cols parameter (should use fallback) + result = render.lr_gradient(rows, offset=0.0) + + # Should not raise and should return colored output + assert isinstance(result, list) + assert len(result) == 1 + # Should have ANSI codes (no plain "text") + assert result[0] != "text" + finally: + # Restore original state + config.ACTIVE_THEME = original_theme + + def test_lr_gradient_explicit_cols_parameter_still_works(self): + """lr_gradient with explicit cols parameter overrides theme.""" + # Custom gradient + custom_cols = ["\033[38;5;1m", "\033[38;5;2m"] * 6 + + rows = ["xy"] + result = render.lr_gradient(rows, offset=0.0, cols=custom_cols) + + # Should use the provided cols + assert isinstance(result, list) + assert len(result) == 1 + + def test_lr_gradient_respects_cols_parameter_name(self): + """lr_gradient accepts cols as keyword argument.""" + custom_cols = ["\033[38;5;1m", "\033[38;5;2m"] * 6 + + rows = ["xy"] + # Call with cols as keyword + result = render.lr_gradient(rows, offset=0.0, cols=custom_cols) + + assert isinstance(result, list) + + +class TestLrGradientBasicFunctionality: + """Tests to ensure lr_gradient basic functionality still works.""" + + def test_lr_gradient_colors_non_space_chars(self): + """lr_gradient colors non-space characters.""" + rows = ["hello"] + + # Set a theme for the test + original_theme = config.ACTIVE_THEME + try: + config.set_active_theme("green") + result = render.lr_gradient(rows, offset=0.0) + + # Result should have ANSI codes + assert any("\033[" in r for r in result), "Expected ANSI codes in result" + finally: + config.ACTIVE_THEME = original_theme + + def test_lr_gradient_preserves_spaces(self): + """lr_gradient preserves spaces in output.""" + rows = ["a b c"] + + original_theme = config.ACTIVE_THEME + try: + config.set_active_theme("green") + result = render.lr_gradient(rows, offset=0.0) + + # Spaces should be preserved (not colored) + assert " " in result[0] + finally: + config.ACTIVE_THEME = original_theme + + def test_lr_gradient_empty_rows(self): + """lr_gradient handles empty rows correctly.""" + rows = [""] + + original_theme = config.ACTIVE_THEME + try: + config.set_active_theme("green") + result = render.lr_gradient(rows, offset=0.0) + + assert result == [""] + finally: + config.ACTIVE_THEME = original_theme + + def test_lr_gradient_multiple_rows(self): + """lr_gradient handles multiple rows.""" + rows = ["row1", "row2", "row3"] + + original_theme = config.ACTIVE_THEME + try: + config.set_active_theme("green") + result = render.lr_gradient(rows, offset=0.0) + + assert len(result) == 3 + finally: + config.ACTIVE_THEME = original_theme