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 <noreply@anthropic.com>
This commit is contained in:
104
engine/render.py
104
engine/render.py
@@ -16,37 +16,69 @@ from engine.terminal import RST
|
|||||||
from engine.translate import detect_location_language, translate_headline
|
from engine.translate import detect_location_language, translate_headline
|
||||||
|
|
||||||
# ─── GRADIENT ─────────────────────────────────────────────
|
# ─── GRADIENT ─────────────────────────────────────────────
|
||||||
# Left → right: white-hot leading edge fades to near-black
|
def _color_codes_to_ansi(color_codes):
|
||||||
GRAD_COLS = [
|
"""Convert a list of 256-color codes to ANSI escape code strings.
|
||||||
"\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
|
|
||||||
]
|
|
||||||
|
|
||||||
# Complementary sweep for queue messages (opposite hue family from ticker greens)
|
Pattern: first 2 are bold, middle 8 are normal, last 2 are dim.
|
||||||
MSG_GRAD_COLS = [
|
|
||||||
"\033[1;38;5;231m", # white
|
Args:
|
||||||
"\033[1;38;5;225m", # pale pink-white
|
color_codes: List of 12 integers (256-color palette codes)
|
||||||
"\033[38;5;219m", # bright pink
|
|
||||||
"\033[38;5;213m", # hot pink
|
Returns:
|
||||||
"\033[38;5;207m", # magenta
|
List of ANSI escape code strings
|
||||||
"\033[38;5;201m", # bright magenta
|
"""
|
||||||
"\033[38;5;165m", # orchid-red
|
if not color_codes or len(color_codes) != 12:
|
||||||
"\033[38;5;161m", # ruby-magenta
|
# Fallback to default green if invalid
|
||||||
"\033[38;5;125m", # dark magenta
|
return _default_green_gradient()
|
||||||
"\033[38;5;89m", # deep maroon-magenta
|
|
||||||
"\033[2;38;5;89m", # dim deep maroon-magenta
|
result = []
|
||||||
"\033[2;38;5;235m", # near black
|
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 LOADING ─────────────────────────────────────────
|
||||||
_FONT_OBJ = None
|
_FONT_OBJ = None
|
||||||
@@ -189,9 +221,15 @@ def big_wrap(text, max_w, fnt=None):
|
|||||||
return out
|
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."""
|
"""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)
|
n = len(cols)
|
||||||
max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1)
|
max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1)
|
||||||
out = []
|
out = []
|
||||||
@@ -213,7 +251,7 @@ def lr_gradient(rows, offset=0.0, grad_cols=None):
|
|||||||
|
|
||||||
def lr_gradient_opposite(rows, offset=0.0):
|
def lr_gradient_opposite(rows, offset=0.0):
|
||||||
"""Complementary (opposite wheel) gradient used for queue message panels."""
|
"""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 ─────────────────────────────
|
# ─── HEADLINE BLOCK ASSEMBLY ─────────────────────────────
|
||||||
|
|||||||
174
tests/test_render.py
Normal file
174
tests/test_render.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user