--- name: mainline-display description: Display backend implementation and the Display protocol compatibility: opencode metadata: audience: developers source_type: codebase --- ## What This Skill Covers This skill covers Mainline's display backend system - how to implement new display backends and how the Display protocol works. ## Key Concepts ### Display Protocol All backends implement a common Display protocol (in `engine/display/__init__.py`): ```python class Display(Protocol): width: int height: int def init(self, width: int, height: int, reuse: bool = False) -> None: """Initialize the display""" ... def show(self, buf: list[str], border: bool = False) -> None: """Display the buffer""" ... def clear(self) -> None: """Clear the display""" ... def cleanup(self) -> None: """Clean up resources""" ... def get_dimensions(self) -> tuple[int, int]: """Return (width, height)""" ... ``` ### DisplayRegistry Discovers and manages backends: ```python from engine.display import DisplayRegistry display = DisplayRegistry.create("terminal") # or "websocket", "null", "multi" ``` ### Available Backends | Backend | File | Description | |---------|------|-------------| | terminal | backends/terminal.py | ANSI terminal output | | websocket | backends/websocket.py | Web browser via WebSocket | | null | backends/null.py | Headless for testing | | multi | backends/multi.py | Forwards to multiple displays | | moderngl | backends/moderngl.py | GPU-accelerated OpenGL rendering (optional) | ### WebSocket Backend - WebSocket server: port 8765 - HTTP server: port 8766 (serves client/index.html) - Client has ANSI color parsing and fullscreen support ### Multi Backend Forwards to multiple displays simultaneously - useful for `terminal + websocket`. ## Adding a New Backend 1. Create `engine/display/backends/my_backend.py` 2. Implement the Display protocol methods 3. Register in `engine/display/__init__.py`'s `DisplayRegistry` Required methods: - `init(width: int, height: int, reuse: bool = False)` - Initialize display - `show(buf: list[str], border: bool = False)` - Display buffer - `clear()` - Clear screen - `cleanup()` - Clean up resources - `get_dimensions() -> tuple[int, int]` - Get terminal dimensions Optional methods: - `title(text: str)` - Set window title - `cursor(show: bool)` - Control cursor ## Usage ```bash python mainline.py --display terminal # default python mainline.py --display websocket python mainline.py --display moderngl # GPU-accelerated (requires moderngl) ``` ## Common Bugs and Patterns ### BorderMode.OFF Enum Bug **Problem**: `BorderMode.OFF` has enum value `1` (not `0`), and Python enums are always truthy. **Incorrect Code**: ```python if border: buffer = render_border(buffer, width, height, fps, frame_time) ``` **Correct Code**: ```python from engine.display import BorderMode if border and border != BorderMode.OFF: buffer = render_border(buffer, width, height, fps, frame_time) ``` **Why**: Checking `if border:` evaluates to `True` even when `border == BorderMode.OFF` because enum members are always truthy in Python. ### Context Type Mismatch **Problem**: `PipelineContext` and `EffectContext` have different APIs for storing data. - `PipelineContext`: Uses `set()`/`get()` for services - `EffectContext`: Uses `set_state()`/`get_state()` for state **Pattern for Passing Data**: ```python # In pipeline setup (uses PipelineContext) ctx.set("pipeline_order", pipeline.execution_order) # In EffectPluginStage (must copy to EffectContext) effect_ctx.set_state("pipeline_order", ctx.get("pipeline_order")) ``` ### Terminal Display ANSI Patterns **Screen Clearing**: ```python output = "\033[H\033[J" + "".join(buffer) ``` **Cursor Positioning** (used by HUD effect): - `\033[row;colH` - Move cursor to row, column - Example: `\033[1;1H` - Move to row 1, column 1 **Key Insight**: Terminal display joins buffer lines WITHOUT newlines, relying on ANSI cursor positioning codes to move the cursor to the correct location for each line. ### EffectPluginStage Context Copying **Problem**: When effects need access to pipeline services (like `pipeline_order`), they must be copied from `PipelineContext` to `EffectContext`. **Pattern**: ```python # In EffectPluginStage.process() # Copy pipeline_order from PipelineContext services to EffectContext state pipeline_order = ctx.get("pipeline_order") if pipeline_order: effect_ctx.set_state("pipeline_order", pipeline_order) ``` This ensures effects can access `ctx.get_state("pipeline_order")` in their process method.