forked from genewildish/Mainline
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.
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
"""Adapter wrapping EffectPlugin as a Stage."""
|
|
|
|
from typing import Any
|
|
|
|
from sideline.pipeline.core import PipelineContext, Stage
|
|
|
|
|
|
class EffectPluginStage(Stage):
|
|
"""Adapter wrapping EffectPlugin as a Stage.
|
|
|
|
Supports capability-based dependencies through the dependencies parameter.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
effect_plugin,
|
|
name: str = "effect",
|
|
dependencies: set[str] | None = None,
|
|
):
|
|
self._effect = effect_plugin
|
|
self.name = name
|
|
self.category = "effect"
|
|
self.optional = False
|
|
self._dependencies = dependencies or set()
|
|
|
|
@property
|
|
def stage_type(self) -> str:
|
|
"""Return stage_type based on effect name.
|
|
|
|
Overlay effects have stage_type "overlay".
|
|
"""
|
|
if self.is_overlay:
|
|
return "overlay"
|
|
return self.category
|
|
|
|
@property
|
|
def render_order(self) -> int:
|
|
"""Return render_order based on effect type.
|
|
|
|
Overlay effects have high render_order to appear on top.
|
|
"""
|
|
if self.is_overlay:
|
|
return 100 # High order for overlays
|
|
return 0
|
|
|
|
@property
|
|
def is_overlay(self) -> bool:
|
|
"""Return True for overlay effects.
|
|
|
|
Overlay effects compose on top of the buffer
|
|
rather than transforming it for the next stage.
|
|
"""
|
|
# Check if the effect has an is_overlay attribute that is explicitly True
|
|
# (not just any truthy value from a mock object)
|
|
if hasattr(self._effect, "is_overlay"):
|
|
effect_overlay = self._effect.is_overlay
|
|
# Only return True if it's explicitly set to True
|
|
if effect_overlay is True:
|
|
return True
|
|
return self.name == "hud"
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {f"effect.{self.name}"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
return self._dependencies
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
from sideline.pipeline.core import DataType
|
|
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
from sideline.pipeline.core import DataType
|
|
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Process data through the effect."""
|
|
if data is None:
|
|
return None
|
|
from sideline.effects.types import EffectContext, apply_param_bindings
|
|
|
|
w = ctx.params.viewport_width if ctx.params else 80
|
|
h = ctx.params.viewport_height if ctx.params else 24
|
|
frame = ctx.params.frame_number if ctx.params else 0
|
|
|
|
effect_ctx = EffectContext(
|
|
terminal_width=w,
|
|
terminal_height=h,
|
|
scroll_cam=0,
|
|
ticker_height=h,
|
|
camera_x=0,
|
|
mic_excess=0.0,
|
|
grad_offset=(frame * 0.01) % 1.0,
|
|
frame_number=frame,
|
|
has_message=False,
|
|
items=ctx.get("items", []),
|
|
)
|
|
|
|
# Copy sensor state from PipelineContext to EffectContext
|
|
for key, value in ctx.state.items():
|
|
if key.startswith("sensor."):
|
|
effect_ctx.set_state(key, value)
|
|
|
|
# Copy metrics from PipelineContext to EffectContext
|
|
if "metrics" in ctx.state:
|
|
effect_ctx.set_state("metrics", ctx.state["metrics"])
|
|
|
|
# Copy pipeline_order from PipelineContext services to EffectContext state
|
|
pipeline_order = ctx.get("pipeline_order")
|
|
if pipeline_order:
|
|
effect_ctx.set_state("pipeline_order", pipeline_order)
|
|
|
|
# Apply sensor param bindings if effect has them
|
|
if hasattr(self._effect, "param_bindings") and self._effect.param_bindings:
|
|
bound_config = apply_param_bindings(self._effect, effect_ctx)
|
|
self._effect.configure(bound_config)
|
|
|
|
return self._effect.process(data, effect_ctx)
|