forked from genewildish/Mainline
- Detect REPL effect in pipeline and enable interactive mode - Enable raw terminal mode for REPL input capture - Add keyboard input loop for REPL commands (return, up/down arrows, backspace) - Process commands and handle pipeline mutations from REPL - Fix lint issues in graph and REPL modules (type annotations, imports)
159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
"""Adapter to convert Graph to Pipeline stages.
|
|
|
|
This module bridges the new graph-based abstraction with the existing
|
|
Stage-based pipeline system for backward compatibility.
|
|
"""
|
|
|
|
from typing import Any, Optional
|
|
|
|
from engine.camera import Camera
|
|
from engine.data_sources.sources import EmptyDataSource, HeadlinesDataSource
|
|
from engine.display import DisplayRegistry
|
|
from engine.effects import get_registry
|
|
from engine.pipeline.adapters import (
|
|
CameraStage,
|
|
DataSourceStage,
|
|
DisplayStage,
|
|
EffectPluginStage,
|
|
FontStage,
|
|
MessageOverlayStage,
|
|
PositionStage,
|
|
)
|
|
from engine.pipeline.adapters.positioning import PositioningMode
|
|
from engine.pipeline.controller import Pipeline, PipelineConfig
|
|
from engine.pipeline.core import PipelineContext
|
|
from engine.pipeline.graph import Graph, NodeType
|
|
from engine.pipeline.params import PipelineParams
|
|
|
|
|
|
class GraphAdapter:
|
|
"""Converts Graph to Pipeline with existing Stage classes."""
|
|
|
|
def __init__(self, graph: Graph):
|
|
self.graph = graph
|
|
self.pipeline: Pipeline | None = None
|
|
self.context: PipelineContext | None = None
|
|
|
|
def build_pipeline(
|
|
self, viewport_width: int = 80, viewport_height: int = 24
|
|
) -> Pipeline:
|
|
"""Build a Pipeline from the Graph."""
|
|
# Create pipeline context
|
|
self.context = PipelineContext()
|
|
self.context.terminal_width = viewport_width
|
|
self.context.terminal_height = viewport_height
|
|
|
|
# Create params
|
|
params = PipelineParams(
|
|
viewport_width=viewport_width,
|
|
viewport_height=viewport_height,
|
|
)
|
|
self.context.params = params
|
|
|
|
# Create pipeline config
|
|
config = PipelineConfig()
|
|
|
|
# Create pipeline
|
|
self.pipeline = Pipeline(config=config, context=self.context)
|
|
|
|
# Map graph nodes to pipeline stages
|
|
self._map_nodes_to_stages()
|
|
|
|
# Build pipeline
|
|
self.pipeline.build()
|
|
|
|
return self.pipeline
|
|
|
|
def _map_nodes_to_stages(self) -> None:
|
|
"""Map graph nodes to pipeline stages."""
|
|
for name, node in self.graph.nodes.items():
|
|
if not node.enabled:
|
|
continue
|
|
|
|
stage = self._create_stage_from_node(name, node)
|
|
if stage:
|
|
self.pipeline.add_stage(name, stage)
|
|
|
|
def _create_stage_from_node(self, name: str, node) -> Optional:
|
|
"""Create a pipeline stage from a graph node."""
|
|
stage = None
|
|
|
|
if node.type == NodeType.SOURCE:
|
|
source_type = node.config.get("source", "headlines")
|
|
if source_type == "headlines":
|
|
source = HeadlinesDataSource()
|
|
elif source_type == "empty":
|
|
source = EmptyDataSource(
|
|
width=self.context.terminal_width,
|
|
height=self.context.terminal_height,
|
|
)
|
|
else:
|
|
source = EmptyDataSource(
|
|
width=self.context.terminal_width,
|
|
height=self.context.terminal_height,
|
|
)
|
|
stage = DataSourceStage(source, name=name)
|
|
|
|
elif node.type == NodeType.CAMERA:
|
|
mode = node.config.get("mode", "scroll")
|
|
speed = node.config.get("speed", 1.0)
|
|
# Map mode string to Camera factory method
|
|
mode_lower = mode.lower()
|
|
if hasattr(Camera, mode_lower):
|
|
camera_factory = getattr(Camera, mode_lower)
|
|
camera = camera_factory(speed=speed)
|
|
else:
|
|
# Fallback to scroll mode
|
|
camera = Camera.scroll(speed=speed)
|
|
stage = CameraStage(camera, name=name)
|
|
|
|
elif node.type == NodeType.DISPLAY:
|
|
backend = node.config.get("backend", "terminal")
|
|
positioning = node.config.get("positioning", "mixed")
|
|
display = DisplayRegistry.create(backend)
|
|
if display:
|
|
stage = DisplayStage(display, name=name, positioning=positioning)
|
|
|
|
elif node.type == NodeType.EFFECT:
|
|
effect_name = node.config.get("effect", "")
|
|
intensity = node.config.get("intensity", 1.0)
|
|
effect = get_registry().get(effect_name)
|
|
if effect:
|
|
# Set effect intensity (modifies global effect state)
|
|
effect.config.intensity = intensity
|
|
# Effects typically depend on rendered output
|
|
dependencies = {"render.output"}
|
|
stage = EffectPluginStage(effect, name=name, dependencies=dependencies)
|
|
|
|
elif node.type == NodeType.RENDER:
|
|
stage = FontStage(name=name)
|
|
|
|
elif node.type == NodeType.OVERLAY:
|
|
stage = MessageOverlayStage(name=name)
|
|
|
|
elif node.type == NodeType.POSITION:
|
|
mode_str = node.config.get("mode", "mixed")
|
|
try:
|
|
mode = PositioningMode(mode_str)
|
|
except ValueError:
|
|
mode = PositioningMode.MIXED
|
|
stage = PositionStage(mode=mode, name=name)
|
|
|
|
return stage
|
|
|
|
|
|
def graph_to_pipeline(
|
|
graph: Graph, viewport_width: int = 80, viewport_height: int = 24
|
|
) -> Pipeline:
|
|
"""Convert a Graph to a Pipeline."""
|
|
adapter = GraphAdapter(graph)
|
|
return adapter.build_pipeline(viewport_width, viewport_height)
|
|
|
|
|
|
def dict_to_pipeline(
|
|
data: dict[str, Any], viewport_width: int = 80, viewport_height: int = 24
|
|
) -> Pipeline:
|
|
"""Convert a dictionary to a Pipeline."""
|
|
graph = Graph().from_dict(data)
|
|
return graph_to_pipeline(graph, viewport_width, viewport_height)
|