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.
109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
"""Adapter wrapping Display as a Stage."""
|
|
|
|
from typing import Any
|
|
|
|
from sideline.pipeline.core import PipelineContext, Stage
|
|
|
|
|
|
class DisplayStage(Stage):
|
|
"""Adapter wrapping Display as a Stage."""
|
|
|
|
def __init__(self, display, name: str = "terminal", positioning: str = "mixed"):
|
|
self._display = display
|
|
self.name = name
|
|
self.category = "display"
|
|
self.optional = False
|
|
self._initialized = False
|
|
self._init_width = 80
|
|
self._init_height = 24
|
|
self._positioning = positioning
|
|
|
|
def save_state(self) -> dict[str, Any]:
|
|
"""Save display state for restoration after pipeline rebuild.
|
|
|
|
Returns:
|
|
Dictionary containing display state that can be restored
|
|
"""
|
|
return {
|
|
"initialized": self._initialized,
|
|
"init_width": self._init_width,
|
|
"init_height": self._init_height,
|
|
"width": getattr(self._display, "width", 80),
|
|
"height": getattr(self._display, "height", 24),
|
|
}
|
|
|
|
def restore_state(self, state: dict[str, Any]) -> None:
|
|
"""Restore display state from saved state.
|
|
|
|
Args:
|
|
state: Dictionary containing display state from save_state()
|
|
"""
|
|
self._initialized = state.get("initialized", False)
|
|
self._init_width = state.get("init_width", 80)
|
|
self._init_height = state.get("init_height", 24)
|
|
|
|
# Restore display dimensions if the display supports it
|
|
if hasattr(self._display, "width"):
|
|
self._display.width = state.get("width", 80)
|
|
if hasattr(self._display, "height"):
|
|
self._display.height = state.get("height", 24)
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {"display.output"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
# Display needs rendered content and camera transformation
|
|
return {"render.output", "camera"}
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
from sideline.pipeline.core import DataType
|
|
|
|
return {DataType.TEXT_BUFFER} # Display consumes rendered text
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
from sideline.pipeline.core import DataType
|
|
|
|
return {DataType.NONE} # Display is a terminal stage (no output)
|
|
|
|
def init(self, ctx: PipelineContext) -> bool:
|
|
w = ctx.params.viewport_width if ctx.params else 80
|
|
h = ctx.params.viewport_height if ctx.params else 24
|
|
|
|
# Try to reuse display if already initialized
|
|
reuse = self._initialized
|
|
result = self._display.init(w, h, reuse=reuse)
|
|
|
|
# Update initialization state
|
|
if result is not False:
|
|
self._initialized = True
|
|
self._init_width = w
|
|
self._init_height = h
|
|
|
|
return result is not False
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Output data to display."""
|
|
if data is not None:
|
|
# Check if positioning mode is specified in context params
|
|
positioning = self._positioning
|
|
if ctx and ctx.params and hasattr(ctx.params, "positioning"):
|
|
positioning = ctx.params.positioning
|
|
|
|
# Pass positioning to display if supported
|
|
if (
|
|
hasattr(self._display, "show")
|
|
and "positioning" in self._display.show.__code__.co_varnames
|
|
):
|
|
self._display.show(data, positioning=positioning)
|
|
else:
|
|
# Fallback for displays that don't support positioning parameter
|
|
self._display.show(data)
|
|
return data
|
|
|
|
def cleanup(self) -> None:
|
|
self._display.cleanup()
|