From 637cbc55150204afb6375aa99e416dd423f328d1 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Mar 2026 19:53:58 -0700 Subject: [PATCH] fix: pass border parameter to display and handle special sources properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG FIXES: 1. Border parameter not being passed to display.show() - Display backends support border parameter but app.py wasn't passing it - Now app.py passes params.border to display.show(border=params.border) - Enables border-test preset to actually render borders 2. WebSocket and Multi displays didn't support border parameter - Updated WebSocket Protocol to include border parameter - Updated MultiDisplay.show() to accept and forward border parameter - Updated test to expect border parameter in mock calls 3. app.py didn't properly handle special sources (empty, pipeline-inspect) - Border-test preset with source='empty' was still fetching headlines - Pipeline-inspect source was never using the introspection data source - Now app.py detects special sources and uses appropriate data source stages: * 'empty' source → EmptyDataSource stage * 'pipeline-inspect' → PipelineIntrospectionSource stage * Other sources → traditional items-based approach - Uses SourceItemsToBufferStage for special sources instead of RenderStage - Sets pipeline on introspection source after build to avoid circular dependency TESTING: - All 463 tests pass - Linting passes - Manual test: `uv run mainline.py --preset border-test` now correctly shows empty source - border-test preset now properly initializes without fetching unnecessary content The issue was that the enhanced app.py code from the original diff didn't make it into the refactor commit. This fix restores that functionality. --- engine/app.py | 88 ++++++++++++++++++++-------- engine/display/backends/multi.py | 4 +- engine/display/backends/websocket.py | 2 +- tests/test_display.py | 6 +- 4 files changed, 71 insertions(+), 29 deletions(-) diff --git a/engine/app.py b/engine/app.py index 96866aa..75681cd 100644 --- a/engine/app.py +++ b/engine/app.py @@ -51,6 +51,7 @@ def run_pipeline_mode(preset_name: str = "demo"): from engine.fetch import fetch_all, fetch_poetry, load_cache from engine.pipeline.adapters import ( RenderStage, + SourceItemsToBufferStage, create_items_stage, create_stage_from_display, create_stage_from_effect, @@ -85,19 +86,29 @@ def run_pipeline_mode(preset_name: str = "demo"): ) print(" \033[38;5;245mFetching content...\033[0m") - cached = load_cache() - if cached: - items = cached - elif preset.source == "poetry": - items, _, _ = fetch_poetry() + + # Handle special sources that don't need traditional fetching + introspection_source = None + if preset.source == "pipeline-inspect": + items = [] + print(" \033[38;5;245mUsing pipeline introspection source\033[0m") + elif preset.source == "empty": + items = [] + print(" \033[38;5;245mUsing empty source (no content)\033[0m") else: - items, _, _ = fetch_all() + cached = load_cache() + if cached: + items = cached + elif preset.source == "poetry": + items, _, _ = fetch_poetry() + else: + items, _, _ = fetch_all() - if not items: - print(" \033[38;5;196mNo content available\033[0m") - sys.exit(1) + if not items: + print(" \033[38;5;196mNo content available\033[0m") + sys.exit(1) - print(f" \033[38;5;82mLoaded {len(items)} items\033[0m") + print(f" \033[38;5;82mLoaded {len(items)} items\033[0m") display = DisplayRegistry.create(preset.display) if not display: @@ -108,18 +119,45 @@ def run_pipeline_mode(preset_name: str = "demo"): effect_registry = get_registry() - pipeline.add_stage("source", create_items_stage(items, preset.source)) - pipeline.add_stage( - "render", - RenderStage( - items, - width=80, - height=24, - camera_speed=params.camera_speed, - camera_mode=preset.camera, - firehose_enabled=params.firehose_enabled, - ), - ) + # Create source stage based on preset source type + if preset.source == "pipeline-inspect": + from engine.data_sources.pipeline_introspection import ( + PipelineIntrospectionSource, + ) + from engine.pipeline.adapters import DataSourceStage + + introspection_source = PipelineIntrospectionSource( + pipeline=None, # Will be set after pipeline.build() + viewport_width=80, + viewport_height=24, + ) + pipeline.add_stage( + "source", DataSourceStage(introspection_source, name="pipeline-inspect") + ) + elif preset.source == "empty": + from engine.data_sources.sources import EmptyDataSource + from engine.pipeline.adapters import DataSourceStage + + empty_source = EmptyDataSource(width=80, height=24) + pipeline.add_stage("source", DataSourceStage(empty_source, name="empty")) + else: + pipeline.add_stage("source", create_items_stage(items, preset.source)) + + # Add appropriate render stage + if preset.source in ("pipeline-inspect", "empty"): + pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer")) + else: + pipeline.add_stage( + "render", + RenderStage( + items, + width=80, + height=24, + camera_speed=params.camera_speed, + camera_mode=preset.camera, + firehose_enabled=params.firehose_enabled, + ), + ) for effect_name in preset.effects: effect = effect_registry.get(effect_name) @@ -132,6 +170,10 @@ def run_pipeline_mode(preset_name: str = "demo"): pipeline.build() + # For pipeline-inspect, set the pipeline after build to avoid circular dependency + if introspection_source is not None: + introspection_source.set_pipeline(pipeline) + if not pipeline.initialize(): print(" \033[38;5;196mFailed to initialize pipeline\033[0m") sys.exit(1) @@ -162,7 +204,7 @@ def run_pipeline_mode(preset_name: str = "demo"): result = pipeline.execute(items) if result.success: - display.show(result.data) + display.show(result.data, border=params.border) if hasattr(display, "is_quit_requested") and display.is_quit_requested(): if hasattr(display, "clear_quit_request"): diff --git a/engine/display/backends/multi.py b/engine/display/backends/multi.py index 496eda9..131972a 100644 --- a/engine/display/backends/multi.py +++ b/engine/display/backends/multi.py @@ -30,9 +30,9 @@ class MultiDisplay: for d in self.displays: d.init(width, height, reuse=reuse) - def show(self, buffer: list[str]) -> None: + def show(self, buffer: list[str], border: bool = False) -> None: for d in self.displays: - d.show(buffer) + d.show(buffer, border=border) def clear(self) -> None: for d in self.displays: diff --git a/engine/display/backends/websocket.py b/engine/display/backends/websocket.py index 7ac31ce..5c0c141 100644 --- a/engine/display/backends/websocket.py +++ b/engine/display/backends/websocket.py @@ -24,7 +24,7 @@ class Display(Protocol): """Initialize display with dimensions.""" ... - def show(self, buffer: list[str]) -> None: + def show(self, buffer: list[str], border: bool = False) -> None: """Show buffer on display.""" ... diff --git a/tests/test_display.py b/tests/test_display.py index 46632aa..1491b83 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -165,10 +165,10 @@ class TestMultiDisplay: multi = MultiDisplay([mock_display1, mock_display2]) buffer = ["line1", "line2"] - multi.show(buffer) + multi.show(buffer, border=False) - mock_display1.show.assert_called_once_with(buffer) - mock_display2.show.assert_called_once_with(buffer) + mock_display1.show.assert_called_once_with(buffer, border=False) + mock_display2.show.assert_called_once_with(buffer, border=False) def test_clear_forwards_to_all_displays(self): """clear forwards to all displays."""