Files
sideline/sideline/render/gradient.py
David Gwilliam e4b143ff36 feat: Implement Sideline plugin system with consistent terminology
This commit implements the Sideline/Mainline split with a clean plugin architecture:

## Core Changes

### Sideline Framework (New Directory)
- Created  directory containing the reusable pipeline framework
- Moved pipeline core, controllers, adapters, and registry to
- Moved display system to
- Moved effects system to
- Created plugin system with security and compatibility management in
- Created preset pack system with ASCII art encoding in
- Added default font (Corptic) to
- Added terminal ANSI constants to

### Mainline Application (Updated)
- Created  for Mainline stage component registration
- Updated  to register Mainline stages at startup
- Updated  as a compatibility shim re-exporting from sideline

### Terminology Consistency
- : Base class for all pipeline components (sources, effects, displays, cameras)
- : Base class for distributable plugin packages (was )
- : Base class for visual effects (was )
- Backward compatibility aliases maintained for existing code

## Key Features
- Plugin discovery via entry points and explicit registration
- Security permissions system for plugins
- Compatibility management with semantic version constraints
- Preset pack system for distributable configurations
- Default font bundled with Sideline (Corptic.otf)

## Testing
- Updated tests to register Mainline stages before discovery
- All StageRegistry tests passing

Note: This is a major refactoring that separates the framework (Sideline) from the application (Mainline), enabling Sideline to be used by other applications.
2026-03-30 19:41:04 -07:00

137 lines
4.1 KiB
Python

"""Gradient coloring for rendered block characters.
Provides left-to-right and complementary gradient effects for terminal display.
"""
from sideline.terminal import RST
# 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
]
# 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
]
def lr_gradient(rows, offset=0.0, grad_cols=None):
"""Color each non-space block character with a shifting left-to-right gradient.
Args:
rows: List of text lines with block characters
offset: Gradient offset (0.0-1.0) for animation
grad_cols: List of ANSI color codes (default: GRAD_COLS)
Returns:
List of lines with gradient coloring applied
"""
cols = grad_cols or GRAD_COLS
n = len(cols)
max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1)
out = []
for row in rows:
if not row.strip():
out.append(row)
continue
buf = []
for x, ch in enumerate(row):
if ch == " ":
buf.append(" ")
else:
shifted = (x / max(max_x - 1, 1) + offset) % 1.0
idx = min(round(shifted * (n - 1)), n - 1)
buf.append(f"{cols[idx]}{ch}{RST}")
out.append("".join(buf))
return out
def lr_gradient_opposite(rows, offset=0.0):
"""Complementary (opposite wheel) gradient used for queue message panels.
Args:
rows: List of text lines with block characters
offset: Gradient offset (0.0-1.0) for animation
Returns:
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