forked from genewildish/Mainline
Major changes: - Pipeline architecture with capability-based dependency resolution - Effects plugin system with performance monitoring - Display abstraction with multiple backends (terminal, null, websocket) - Camera system for viewport scrolling - Sensor framework for real-time input - Command-and-control system via ntfy - WebSocket display backend for browser clients - Comprehensive test suite and documentation Issue #48: ADR for preset scripting language included This commit consolidates 110 individual commits into a single feature integration that can be reviewed and tested before further refinement.
164 lines
4.6 KiB
Markdown
164 lines
4.6 KiB
Markdown
---
|
|
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.
|