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.
88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
import time
|
|
|
|
from sideline.effects.performance import PerformanceMonitor, get_monitor
|
|
from sideline.effects.registry import EffectRegistry
|
|
from sideline.effects.types import EffectContext, PartialUpdate
|
|
|
|
|
|
class EffectChain:
|
|
def __init__(
|
|
self, registry: EffectRegistry, monitor: PerformanceMonitor | None = None
|
|
):
|
|
self._registry = registry
|
|
self._order: list[str] = []
|
|
self._monitor = monitor
|
|
|
|
def _get_monitor(self) -> PerformanceMonitor:
|
|
if self._monitor is not None:
|
|
return self._monitor
|
|
return get_monitor()
|
|
|
|
def set_order(self, names: list[str]) -> None:
|
|
self._order = list(names)
|
|
|
|
def get_order(self) -> list[str]:
|
|
return self._order.copy()
|
|
|
|
def add_effect(self, name: str, position: int | None = None) -> bool:
|
|
if name not in self._registry.list_all():
|
|
return False
|
|
if position is None:
|
|
self._order.append(name)
|
|
else:
|
|
self._order.insert(position, name)
|
|
return True
|
|
|
|
def remove_effect(self, name: str) -> bool:
|
|
if name in self._order:
|
|
self._order.remove(name)
|
|
return True
|
|
return False
|
|
|
|
def reorder(self, new_order: list[str]) -> bool:
|
|
all_plugins = set(self._registry.list_all().keys())
|
|
if not all(name in all_plugins for name in new_order):
|
|
return False
|
|
self._order = list(new_order)
|
|
return True
|
|
|
|
def process(self, buf: list[str], ctx: EffectContext) -> list[str]:
|
|
monitor = self._get_monitor()
|
|
frame_number = ctx.frame_number
|
|
monitor.start_frame(frame_number)
|
|
|
|
# Get dirty regions from canvas via context (set by CanvasStage)
|
|
dirty_rows = ctx.get_state("canvas.dirty_rows")
|
|
|
|
# Create PartialUpdate for effects that support it
|
|
full_buffer = dirty_rows is None or len(dirty_rows) == 0
|
|
partial = PartialUpdate(
|
|
rows=None,
|
|
cols=None,
|
|
dirty=dirty_rows,
|
|
full_buffer=full_buffer,
|
|
)
|
|
|
|
frame_start = time.perf_counter()
|
|
result = list(buf)
|
|
for name in self._order:
|
|
plugin = self._registry.get(name)
|
|
if plugin and plugin.config.enabled:
|
|
chars_in = sum(len(line) for line in result)
|
|
effect_start = time.perf_counter()
|
|
try:
|
|
# Use process_partial if supported, otherwise fall back to process
|
|
if getattr(plugin, "supports_partial_updates", False):
|
|
result = plugin.process_partial(result, ctx, partial)
|
|
else:
|
|
result = plugin.process(result, ctx)
|
|
except Exception:
|
|
plugin.config.enabled = False
|
|
elapsed = time.perf_counter() - effect_start
|
|
chars_out = sum(len(line) for line in result)
|
|
monitor.record_effect(name, elapsed * 1000, chars_in, chars_out)
|
|
|
|
total_elapsed = time.perf_counter() - frame_start
|
|
monitor.end_frame(frame_number, total_elapsed * 1000)
|
|
return result
|