forked from genewildish/Mainline
refactor: consolidate pipeline architecture with unified data source system
MAJOR REFACTORING: Consolidate duplicated pipeline code and standardize on capability-based dependency resolution. This is a significant but backwards-compatible restructuring that improves maintainability and extensibility. ## ARCHITECTURE CHANGES ### Data Sources Consolidation - Move engine/sources_v2.py → engine/data_sources/sources.py - Move engine/pipeline_sources/ → engine/data_sources/ - Create unified DataSource ABC with common interface: * fetch() - idempotent data retrieval * get_items() - cached access with automatic refresh * refresh() - force cache invalidation * is_dynamic - indicate streaming vs static sources - Support for SourceItem dataclass (content, source, timestamp, metadata) ### Display Backend Improvements - Update all 7 display backends to use new import paths - Terminal: Improve dimension detection and handling - WebSocket: Better error handling and client lifecycle - Sixel: Refactor graphics rendering - Pygame: Modernize event handling - Kitty: Add protocol support for inline images - Multi: Ensure proper forwarding to all backends - Null: Maintain testing backend functionality ### Pipeline Adapter Consolidation - Refactor adapter stages for clarity and flexibility - RenderStage now handles both item-based and buffer-based rendering - Add SourceItemsToBufferStage for converting data source items - Improve DataSourceStage to work with all source types - Add DisplayStage wrapper for display backends ### Camera & Viewport Refinements - Update Camera class for new architecture - Improve viewport dimension detection - Better handling of resize events across backends ### New Effect Plugins - border.py: Frame rendering effect with configurable style - crop.py: Viewport clipping effect for selective display - tint.py: Color filtering effect for atmosphere ### Tests & Quality - Add test_border_effect.py with comprehensive border tests - Add test_crop_effect.py with viewport clipping tests - Add test_tint_effect.py with color filtering tests - Update test_pipeline.py for new architecture - Update test_pipeline_introspection.py for new data source location - All 463 tests pass with 56% coverage - Linting: All checks pass with ruff ### Removals (Code Cleanup) - Delete engine/benchmark.py (deprecated performance testing) - Delete engine/pipeline_sources/__init__.py (moved to data_sources) - Delete engine/sources_v2.py (replaced by data_sources/sources.py) - Update AGENTS.md to reflect new structure ### Import Path Updates - Update engine/pipeline/controller.py::create_default_pipeline() * Old: from engine.sources_v2 import HeadlinesDataSource * New: from engine.data_sources.sources import HeadlinesDataSource - All display backends import from new locations - All tests import from new locations ## BACKWARDS COMPATIBILITY This refactoring is intended to be backwards compatible: - Pipeline execution unchanged (DAG-based with capability matching) - Effect plugins unchanged (EffectPlugin interface same) - Display protocol unchanged (Display duck-typing works as before) - Config system unchanged (presets.toml format same) ## TESTING - 463 tests pass (0 failures, 19 skipped) - Full linting check passes - Manual testing on demo, poetry, websocket modes - All new effect plugins tested ## FILES CHANGED - 24 files modified/added/deleted - 723 insertions, 1,461 deletions (net -738 LOC - cleanup!) - No breaking changes to public APIs - All transitive imports updated correctly
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
ANSI terminal display backend.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
@@ -10,40 +11,106 @@ class TerminalDisplay:
|
||||
|
||||
Renders buffer to stdout using ANSI escape codes.
|
||||
Supports reuse - when reuse=True, skips re-initializing terminal state.
|
||||
Auto-detects terminal dimensions on init.
|
||||
"""
|
||||
|
||||
width: int = 80
|
||||
height: int = 24
|
||||
_initialized: bool = False
|
||||
|
||||
def __init__(self, target_fps: float = 30.0):
|
||||
self.target_fps = target_fps
|
||||
self._frame_period = 1.0 / target_fps if target_fps > 0 else 0
|
||||
self._last_frame_time = 0.0
|
||||
|
||||
def init(self, width: int, height: int, reuse: bool = False) -> None:
|
||||
"""Initialize display with dimensions.
|
||||
|
||||
If width/height are not provided (0/None), auto-detects terminal size.
|
||||
Otherwise uses provided dimensions or falls back to terminal size
|
||||
if the provided dimensions exceed terminal capacity.
|
||||
|
||||
Args:
|
||||
width: Terminal width in characters
|
||||
height: Terminal height in rows
|
||||
width: Desired terminal width (0 = auto-detect)
|
||||
height: Desired terminal height (0 = auto-detect)
|
||||
reuse: If True, skip terminal re-initialization
|
||||
"""
|
||||
from engine.terminal import CURSOR_OFF
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
# Auto-detect terminal size (handle case where no terminal)
|
||||
try:
|
||||
term_size = os.get_terminal_size()
|
||||
term_width = term_size.columns
|
||||
term_height = term_size.lines
|
||||
except OSError:
|
||||
# No terminal available (e.g., in tests)
|
||||
term_width = width if width > 0 else 80
|
||||
term_height = height if height > 0 else 24
|
||||
|
||||
# Use provided dimensions if valid, otherwise use terminal size
|
||||
if width > 0 and height > 0:
|
||||
self.width = min(width, term_width)
|
||||
self.height = min(height, term_height)
|
||||
else:
|
||||
self.width = term_width
|
||||
self.height = term_height
|
||||
|
||||
if not reuse or not self._initialized:
|
||||
print(CURSOR_OFF, end="", flush=True)
|
||||
self._initialized = True
|
||||
|
||||
def show(self, buffer: list[str]) -> None:
|
||||
def get_dimensions(self) -> tuple[int, int]:
|
||||
"""Get current terminal dimensions.
|
||||
|
||||
Returns:
|
||||
(width, height) in character cells
|
||||
"""
|
||||
try:
|
||||
term_size = os.get_terminal_size()
|
||||
return (term_size.columns, term_size.lines)
|
||||
except OSError:
|
||||
return (self.width, self.height)
|
||||
|
||||
def show(self, buffer: list[str], border: bool = False) -> None:
|
||||
import sys
|
||||
|
||||
from engine.display import get_monitor, render_border
|
||||
|
||||
t0 = time.perf_counter()
|
||||
sys.stdout.buffer.write("".join(buffer).encode())
|
||||
|
||||
# FPS limiting - skip frame if we're going too fast
|
||||
if self._frame_period > 0:
|
||||
now = time.perf_counter()
|
||||
elapsed = now - self._last_frame_time
|
||||
if elapsed < self._frame_period:
|
||||
# Skip this frame - too soon
|
||||
return
|
||||
self._last_frame_time = now
|
||||
|
||||
# Get metrics for border display
|
||||
fps = 0.0
|
||||
frame_time = 0.0
|
||||
monitor = get_monitor()
|
||||
if monitor:
|
||||
stats = monitor.get_stats()
|
||||
avg_ms = stats.get("avg_ms", 0) if stats else 0
|
||||
frame_count = stats.get("frame_count", 0) if stats else 0
|
||||
if avg_ms and frame_count > 0:
|
||||
fps = 1000.0 / avg_ms
|
||||
frame_time = avg_ms
|
||||
|
||||
# Apply border if requested
|
||||
if border:
|
||||
buffer = render_border(buffer, self.width, self.height, fps, frame_time)
|
||||
|
||||
# Clear screen and home cursor before each frame
|
||||
from engine.terminal import CLR
|
||||
|
||||
output = CLR + "".join(buffer)
|
||||
sys.stdout.buffer.write(output.encode())
|
||||
sys.stdout.flush()
|
||||
elapsed_ms = (time.perf_counter() - t0) * 1000
|
||||
|
||||
from engine.display import get_monitor
|
||||
|
||||
monitor = get_monitor()
|
||||
if monitor:
|
||||
chars_in = sum(len(line) for line in buffer)
|
||||
monitor.record_effect("terminal_display", elapsed_ms, chars_in, chars_in)
|
||||
|
||||
Reference in New Issue
Block a user