feat: update scroll.py to use theme message gradient
Add msg_gradient() helper function to render.py that applies message (ntfy) gradient using the active theme's message_gradient property. This replaces hardcoded magenta gradient in scroll.py with a theme-aware approach that uses complementary colors from the active theme. - Add msg_gradient(rows, offset) helper in render.py with fallback to default magenta gradient when no theme is active - Update scroll.py imports to use msg_gradient instead of lr_gradient_opposite - Replace lr_gradient_opposite() call with msg_gradient() in message overlay rendering - Add 6 comprehensive tests for msg_gradient covering theme usage, fallback behavior, and edge cases All tests pass (121 passed), no regressions detected. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,6 +254,29 @@ def lr_gradient_opposite(rows, offset=0.0):
|
|||||||
return lr_gradient(rows, offset, _default_magenta_gradient())
|
return lr_gradient(rows, offset, _default_magenta_gradient())
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
cols = (
|
||||||
|
_color_codes_to_ansi(config.ACTIVE_THEME.message_gradient)
|
||||||
|
if config.ACTIVE_THEME
|
||||||
|
else _default_magenta_gradient()
|
||||||
|
)
|
||||||
|
return lr_gradient(rows, offset, cols)
|
||||||
|
|
||||||
|
|
||||||
# ─── HEADLINE BLOCK ASSEMBLY ─────────────────────────────
|
# ─── HEADLINE BLOCK ASSEMBLY ─────────────────────────────
|
||||||
def make_block(title, src, ts, w):
|
def make_block(title, src, ts, w):
|
||||||
"""Render a headline into a content block with color."""
|
"""Render a headline into a content block with color."""
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from engine.effects import (
|
|||||||
noise,
|
noise,
|
||||||
vis_trunc,
|
vis_trunc,
|
||||||
)
|
)
|
||||||
from engine.render import big_wrap, lr_gradient, lr_gradient_opposite, make_block
|
from engine.render import big_wrap, lr_gradient, msg_gradient, make_block
|
||||||
from engine.terminal import CLR, RST, W_COOL, th, tw
|
from engine.terminal import CLR, RST, W_COOL, th, tw
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ def stream(items, ntfy_poller, mic_monitor):
|
|||||||
_msg_cache = (cache_key, msg_rows)
|
_msg_cache = (cache_key, msg_rows)
|
||||||
else:
|
else:
|
||||||
msg_rows = _msg_cache[1]
|
msg_rows = _msg_cache[1]
|
||||||
msg_rows = lr_gradient_opposite(
|
msg_rows = msg_gradient(
|
||||||
msg_rows, (time.monotonic() * config.GRAD_SPEED) % 1.0
|
msg_rows, (time.monotonic() * config.GRAD_SPEED) % 1.0
|
||||||
)
|
)
|
||||||
# Layout: rendered text + meta + border
|
# Layout: rendered text + meta + border
|
||||||
|
|||||||
@@ -172,3 +172,130 @@ class TestLrGradientBasicFunctionality:
|
|||||||
assert len(result) == 3
|
assert len(result) == 3
|
||||||
finally:
|
finally:
|
||||||
config.ACTIVE_THEME = original_theme
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|
||||||
|
|
||||||
|
class TestMsgGradient:
|
||||||
|
"""Tests for msg_gradient function (message/ntfy overlay coloring)."""
|
||||||
|
|
||||||
|
def test_msg_gradient_uses_active_theme(self):
|
||||||
|
"""msg_gradient uses ACTIVE_THEME.message_gradient when theme is set."""
|
||||||
|
# Save original state
|
||||||
|
original_theme = config.ACTIVE_THEME
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set a theme
|
||||||
|
config.set_active_theme("green")
|
||||||
|
|
||||||
|
# Create simple test data
|
||||||
|
rows = ["MESSAGE"]
|
||||||
|
|
||||||
|
# Call msg_gradient
|
||||||
|
result = render.msg_gradient(rows, offset=0.0)
|
||||||
|
|
||||||
|
# Should return colored output using theme's message_gradient
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 1
|
||||||
|
# Should have ANSI codes from the message gradient
|
||||||
|
assert result[0] != "MESSAGE"
|
||||||
|
assert "\033[" in result[0]
|
||||||
|
finally:
|
||||||
|
# Restore original state
|
||||||
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|
||||||
|
def test_msg_gradient_fallback_when_no_theme(self):
|
||||||
|
"""msg_gradient uses fallback magenta 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 = ["MESSAGE"]
|
||||||
|
|
||||||
|
# Call msg_gradient
|
||||||
|
result = render.msg_gradient(rows, offset=0.0)
|
||||||
|
|
||||||
|
# Should return colored output using default magenta
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 1
|
||||||
|
# Should have ANSI codes
|
||||||
|
assert result[0] != "MESSAGE"
|
||||||
|
assert "\033[" in result[0]
|
||||||
|
finally:
|
||||||
|
# Restore original state
|
||||||
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|
||||||
|
def test_msg_gradient_returns_colored_rows(self):
|
||||||
|
"""msg_gradient returns properly colored rows with animation offset."""
|
||||||
|
# Save original state
|
||||||
|
original_theme = config.ACTIVE_THEME
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set a theme
|
||||||
|
config.set_active_theme("orange")
|
||||||
|
|
||||||
|
rows = ["NTFY", "ALERT"]
|
||||||
|
|
||||||
|
# Call with offset
|
||||||
|
result = render.msg_gradient(rows, offset=0.5)
|
||||||
|
|
||||||
|
# Should return same number of rows
|
||||||
|
assert len(result) == 2
|
||||||
|
# Both should be colored
|
||||||
|
assert all("\033[" in r for r in result)
|
||||||
|
# Should not be the original text
|
||||||
|
assert result != rows
|
||||||
|
finally:
|
||||||
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|
||||||
|
def test_msg_gradient_different_themes_produce_different_results(self):
|
||||||
|
"""msg_gradient produces different colors for different themes."""
|
||||||
|
original_theme = config.ACTIVE_THEME
|
||||||
|
|
||||||
|
try:
|
||||||
|
rows = ["TEST"]
|
||||||
|
|
||||||
|
# Get result with green theme
|
||||||
|
config.set_active_theme("green")
|
||||||
|
result_green = render.msg_gradient(rows, offset=0.0)
|
||||||
|
|
||||||
|
# Get result with orange theme
|
||||||
|
config.set_active_theme("orange")
|
||||||
|
result_orange = render.msg_gradient(rows, offset=0.0)
|
||||||
|
|
||||||
|
# Results should be different (different message gradients)
|
||||||
|
assert result_green != result_orange
|
||||||
|
finally:
|
||||||
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|
||||||
|
def test_msg_gradient_preserves_spacing(self):
|
||||||
|
"""msg_gradient preserves spaces in rows."""
|
||||||
|
original_theme = config.ACTIVE_THEME
|
||||||
|
|
||||||
|
try:
|
||||||
|
config.set_active_theme("purple")
|
||||||
|
rows = ["M E S S A G E"]
|
||||||
|
|
||||||
|
result = render.msg_gradient(rows, offset=0.0)
|
||||||
|
|
||||||
|
# Spaces should be preserved
|
||||||
|
assert " " in result[0]
|
||||||
|
finally:
|
||||||
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|
||||||
|
def test_msg_gradient_empty_rows(self):
|
||||||
|
"""msg_gradient handles empty rows correctly."""
|
||||||
|
original_theme = config.ACTIVE_THEME
|
||||||
|
|
||||||
|
try:
|
||||||
|
config.set_active_theme("green")
|
||||||
|
rows = [""]
|
||||||
|
|
||||||
|
result = render.msg_gradient(rows, offset=0.0)
|
||||||
|
|
||||||
|
# Empty row should stay empty
|
||||||
|
assert result == [""]
|
||||||
|
finally:
|
||||||
|
config.ACTIVE_THEME = original_theme
|
||||||
|
|||||||
Reference in New Issue
Block a user