- Add EffectPlugin ABC with @abstractmethod decorators for interface enforcement - Add runtime interface checking in discover_plugins() with issubclass() - Add EffectContext factory with sensible defaults - Standardize Display __init__ (remove redundant init in TerminalDisplay) - Document effect behavior when ticker_height=0 - Evaluate legacy effects: document coexistence, no deprecation needed - Research plugin patterns (VST, Python entry points) - Fix pysixel dependency (removed broken dependency) Test coverage improvements: - Add DisplayRegistry tests - Add MultiDisplay tests - Add SixelDisplay tests - Add controller._get_display tests - Add effects controller command handling tests - Add benchmark regression tests (@pytest.mark.benchmark) - Add pytest marker for benchmark tests in pyproject.toml Documentation updates: - Update AGENTS.md with 56% coverage stats and effect plugin docs - Update README.md with Sixel display mode and benchmark commands - Add new modules to architecture section
122 lines
3.4 KiB
Python
122 lines
3.4 KiB
Python
"""
|
|
Visual effects type definitions and base classes.
|
|
|
|
EffectPlugin Architecture:
|
|
- Uses ABC (Abstract Base Class) for interface enforcement
|
|
- Runtime discovery via directory scanning (effects_plugins/)
|
|
- Configuration via EffectConfig dataclass
|
|
- Context passed through EffectContext dataclass
|
|
|
|
Plugin System Research (see AGENTS.md for references):
|
|
- VST: Standardized audio interfaces, chaining, presets (FXP/FXB)
|
|
- Python Entry Points: Namespace packages, importlib.metadata discovery
|
|
- Shadertoy: Shader-based with uniforms as context
|
|
|
|
Current gaps vs industry patterns:
|
|
- No preset save/load system
|
|
- No external plugin distribution via entry points
|
|
- No plugin metadata (version, author, description)
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
|
|
@dataclass
|
|
class EffectContext:
|
|
terminal_width: int
|
|
terminal_height: int
|
|
scroll_cam: int
|
|
ticker_height: int
|
|
mic_excess: float
|
|
grad_offset: float
|
|
frame_number: int
|
|
has_message: bool
|
|
items: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class EffectConfig:
|
|
enabled: bool = True
|
|
intensity: float = 1.0
|
|
params: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
class EffectPlugin(ABC):
|
|
"""Abstract base class for effect plugins.
|
|
|
|
Subclasses must define:
|
|
- name: str - unique identifier for the effect
|
|
- config: EffectConfig - current configuration
|
|
|
|
And implement:
|
|
- process(buf, ctx) -> list[str]
|
|
- configure(config) -> None
|
|
|
|
Effect Behavior with ticker_height=0:
|
|
- NoiseEffect: Returns buffer unchanged (no ticker to apply noise to)
|
|
- FadeEffect: Returns buffer unchanged (no ticker to fade)
|
|
- GlitchEffect: Processes normally (doesn't depend on ticker_height)
|
|
- FirehoseEffect: Returns buffer unchanged if no items in context
|
|
|
|
Effects should handle missing or zero context values gracefully by
|
|
returning the input buffer unchanged rather than raising errors.
|
|
"""
|
|
|
|
name: str
|
|
config: EffectConfig
|
|
|
|
@abstractmethod
|
|
def process(self, buf: list[str], ctx: EffectContext) -> list[str]:
|
|
"""Process the buffer with this effect applied.
|
|
|
|
Args:
|
|
buf: List of lines to process
|
|
ctx: Effect context with terminal state
|
|
|
|
Returns:
|
|
Processed buffer (may be same object or new list)
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
def configure(self, config: EffectConfig) -> None:
|
|
"""Configure the effect with new settings.
|
|
|
|
Args:
|
|
config: New configuration to apply
|
|
"""
|
|
...
|
|
|
|
|
|
def create_effect_context(
|
|
terminal_width: int = 80,
|
|
terminal_height: int = 24,
|
|
scroll_cam: int = 0,
|
|
ticker_height: int = 0,
|
|
mic_excess: float = 0.0,
|
|
grad_offset: float = 0.0,
|
|
frame_number: int = 0,
|
|
has_message: bool = False,
|
|
items: list | None = None,
|
|
) -> EffectContext:
|
|
"""Factory function to create EffectContext with sensible defaults."""
|
|
return EffectContext(
|
|
terminal_width=terminal_width,
|
|
terminal_height=terminal_height,
|
|
scroll_cam=scroll_cam,
|
|
ticker_height=ticker_height,
|
|
mic_excess=mic_excess,
|
|
grad_offset=grad_offset,
|
|
frame_number=frame_number,
|
|
has_message=has_message,
|
|
items=items or [],
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class PipelineConfig:
|
|
order: list[str] = field(default_factory=list)
|
|
effects: dict[str, EffectConfig] = field(default_factory=dict)
|