Files
Mainline/engine/pipeline/presets.py
David Gwilliam 238bac1bb2 feat: Complete pipeline hot-rebuild implementation with acceptance tests
- Implements pipeline hot-rebuild with state preservation (issue #43)
- Adds auto-injection of MVP stages for missing capabilities
- Adds radial camera mode for polar coordinate scanning
- Adds afterimage and motionblur effects using framebuffer history
- Adds comprehensive acceptance tests for camera modes and pipeline rebuild
- Updates presets.toml with new effect configurations

Related to: #35 (Pipeline Mutation API epic)
Closes: #43, #44, #45
2026-03-19 03:34:06 -07:00

216 lines
5.9 KiB
Python

"""
Pipeline presets - Pre-configured pipeline configurations.
Provides PipelinePreset as a unified preset system.
Presets can be loaded from TOML files (presets.toml) or defined in code.
Loading order:
1. Built-in presets.toml in the package
2. User config ~/.config/mainline/presets.toml
3. Local ./presets.toml (overrides earlier)
"""
from dataclasses import dataclass, field
from typing import Any
from engine.display import BorderMode
from engine.pipeline.params import PipelineParams
def _load_toml_presets() -> dict[str, Any]:
"""Load presets from TOML file."""
try:
from engine.pipeline.preset_loader import load_presets
return load_presets()
except Exception:
return {}
_YAML_PRESETS = _load_toml_presets()
@dataclass
class PipelinePreset:
"""Pre-configured pipeline with stages and animation.
A PipelinePreset packages:
- Initial params: Starting configuration
- Stages: List of stage configurations to create
This is the new unified preset that works with the Pipeline class.
"""
name: str
description: str = ""
source: str = "headlines"
display: str = "terminal"
camera: str = "scroll"
effects: list[str] = field(default_factory=list)
border: bool | BorderMode = (
False # Border mode: False=off, True=simple, BorderMode.UI for panel
)
# Extended fields for fine-tuning
camera_speed: float = 1.0 # Camera movement speed
viewport_width: int = 80 # Viewport width in columns
viewport_height: int = 24 # Viewport height in rows
source_items: list[dict[str, Any]] | None = None # For ListDataSource
def to_params(self) -> PipelineParams:
"""Convert to PipelineParams."""
from engine.display import BorderMode
params = PipelineParams()
params.source = self.source
params.display = self.display
params.border = (
self.border
if isinstance(self.border, bool)
else BorderMode.UI
if self.border == BorderMode.UI
else False
)
params.camera_mode = self.camera
params.effect_order = self.effects.copy()
# Note: camera_speed, viewport_width/height are not stored in PipelineParams
# They are used directly from the preset object in pipeline_runner.py
return params
@classmethod
def from_yaml(cls, name: str, data: dict[str, Any]) -> "PipelinePreset":
"""Create a PipelinePreset from YAML data."""
return cls(
name=name,
description=data.get("description", ""),
source=data.get("source", "headlines"),
display=data.get("display", "terminal"),
camera=data.get("camera", "vertical"),
effects=data.get("effects", []),
border=data.get("border", False),
camera_speed=data.get("camera_speed", 1.0),
viewport_width=data.get("viewport_width", 80),
viewport_height=data.get("viewport_height", 24),
source_items=data.get("source_items"),
)
# Built-in presets
DEMO_PRESET = PipelinePreset(
name="demo",
description="Demo mode with effect cycling and camera modes",
source="headlines",
display="pygame",
camera="scroll",
effects=["noise", "fade", "glitch", "firehose"],
)
UI_PRESET = PipelinePreset(
name="ui",
description="Interactive UI mode with right-side control panel",
source="fixture",
display="pygame",
camera="scroll",
effects=["noise", "fade", "glitch"],
border=BorderMode.UI,
)
POETRY_PRESET = PipelinePreset(
name="poetry",
description="Poetry feed with subtle effects",
source="poetry",
display="pygame",
camera="scroll",
effects=["fade"],
)
PIPELINE_VIZ_PRESET = PipelinePreset(
name="pipeline",
description="Pipeline visualization mode",
source="pipeline",
display="terminal",
camera="trace",
effects=[],
)
WEBSOCKET_PRESET = PipelinePreset(
name="websocket",
description="WebSocket display mode",
source="headlines",
display="websocket",
camera="scroll",
effects=["noise", "fade", "glitch"],
)
FIREHOSE_PRESET = PipelinePreset(
name="firehose",
description="High-speed firehose mode",
source="headlines",
display="pygame",
camera="scroll",
effects=["noise", "fade", "glitch", "firehose"],
)
FIXTURE_PRESET = PipelinePreset(
name="fixture",
description="Use cached headline fixtures",
source="fixture",
display="pygame",
camera="scroll",
effects=["noise", "fade"],
border=False,
)
# Build presets from YAML data
def _build_presets() -> dict[str, PipelinePreset]:
"""Build preset dictionary from all sources."""
result = {}
# Add YAML presets
yaml_presets = _YAML_PRESETS.get("presets", {})
for name, data in yaml_presets.items():
result[name] = PipelinePreset.from_yaml(name, data)
# Add built-in presets as fallback (if not in YAML)
builtins = {
"demo": DEMO_PRESET,
"poetry": POETRY_PRESET,
"pipeline": PIPELINE_VIZ_PRESET,
"websocket": WEBSOCKET_PRESET,
"firehose": FIREHOSE_PRESET,
"ui": UI_PRESET,
"fixture": FIXTURE_PRESET,
}
for name, preset in builtins.items():
if name not in result:
result[name] = preset
return result
PRESETS: dict[str, PipelinePreset] = _build_presets()
def get_preset(name: str) -> PipelinePreset | None:
"""Get a preset by name."""
return PRESETS.get(name)
def list_presets() -> list[str]:
"""List all available preset names."""
return list(PRESETS.keys())
def create_preset_from_params(
params: PipelineParams, name: str = "custom"
) -> PipelinePreset:
"""Create a preset from PipelineParams."""
return PipelinePreset(
name=name,
source=params.source,
display=params.display,
camera=params.camera_mode,
effects=params.effect_order.copy() if hasattr(params, "effect_order") else [],
)