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:
105
effects_plugins/border.py
Normal file
105
effects_plugins/border.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from engine.effects.types import EffectConfig, EffectContext, EffectPlugin
|
||||
|
||||
|
||||
class BorderEffect(EffectPlugin):
|
||||
"""Simple border effect for terminal display.
|
||||
|
||||
Draws a border around the buffer and optionally displays
|
||||
performance metrics in the border corners.
|
||||
|
||||
Internally crops to display dimensions to ensure border fits.
|
||||
"""
|
||||
|
||||
name = "border"
|
||||
config = EffectConfig(enabled=True, intensity=1.0)
|
||||
|
||||
def process(self, buf: list[str], ctx: EffectContext) -> list[str]:
|
||||
if not buf:
|
||||
return buf
|
||||
|
||||
# Get actual display dimensions from context
|
||||
display_w = ctx.terminal_width
|
||||
display_h = ctx.terminal_height
|
||||
|
||||
# If dimensions are reasonable, crop first - use slightly smaller to ensure fit
|
||||
if display_w >= 10 and display_h >= 3:
|
||||
# Subtract 2 for border characters (left and right)
|
||||
crop_w = display_w - 2
|
||||
crop_h = display_h - 2
|
||||
buf = self._crop_to_size(buf, crop_w, crop_h)
|
||||
w = display_w
|
||||
h = display_h
|
||||
else:
|
||||
# Use buffer dimensions
|
||||
h = len(buf)
|
||||
w = max(len(line) for line in buf) if buf else 0
|
||||
|
||||
if w < 3 or h < 3:
|
||||
return buf
|
||||
|
||||
inner_w = w - 2
|
||||
|
||||
# Get metrics from context
|
||||
fps = 0.0
|
||||
frame_time = 0.0
|
||||
metrics = ctx.get_state("metrics")
|
||||
if metrics:
|
||||
avg_ms = metrics.get("avg_ms")
|
||||
frame_count = metrics.get("frame_count", 0)
|
||||
if avg_ms and frame_count > 0:
|
||||
fps = 1000.0 / avg_ms
|
||||
frame_time = avg_ms
|
||||
|
||||
# Build borders
|
||||
# Top border: ┌────────────────────┐ or with FPS
|
||||
if fps > 0:
|
||||
fps_str = f" FPS:{fps:.0f}"
|
||||
if len(fps_str) < inner_w:
|
||||
right_len = inner_w - len(fps_str)
|
||||
top_border = "┌" + "─" * right_len + fps_str + "┐"
|
||||
else:
|
||||
top_border = "┌" + "─" * inner_w + "┐"
|
||||
else:
|
||||
top_border = "┌" + "─" * inner_w + "┐"
|
||||
|
||||
# Bottom border: └────────────────────┘ or with frame time
|
||||
if frame_time > 0:
|
||||
ft_str = f" {frame_time:.1f}ms"
|
||||
if len(ft_str) < inner_w:
|
||||
right_len = inner_w - len(ft_str)
|
||||
bottom_border = "└" + "─" * right_len + ft_str + "┘"
|
||||
else:
|
||||
bottom_border = "└" + "─" * inner_w + "┘"
|
||||
else:
|
||||
bottom_border = "└" + "─" * inner_w + "┘"
|
||||
|
||||
# Build result with left/right borders
|
||||
result = [top_border]
|
||||
for line in buf[: h - 2]:
|
||||
if len(line) >= inner_w:
|
||||
result.append("│" + line[:inner_w] + "│")
|
||||
else:
|
||||
result.append("│" + line + " " * (inner_w - len(line)) + "│")
|
||||
|
||||
result.append(bottom_border)
|
||||
|
||||
return result
|
||||
|
||||
def _crop_to_size(self, buf: list[str], w: int, h: int) -> list[str]:
|
||||
"""Crop buffer to fit within w x h."""
|
||||
result = []
|
||||
for i in range(min(h, len(buf))):
|
||||
line = buf[i]
|
||||
if len(line) > w:
|
||||
result.append(line[:w])
|
||||
else:
|
||||
result.append(line + " " * (w - len(line)))
|
||||
|
||||
# Pad with empty lines if needed (for border)
|
||||
while len(result) < h:
|
||||
result.append(" " * w)
|
||||
|
||||
return result
|
||||
|
||||
def configure(self, config: EffectConfig) -> None:
|
||||
self.config = config
|
||||
Reference in New Issue
Block a user