feat(display): add reuse flag to Display protocol

- Add reuse parameter to Display.init() for all backends
- PygameDisplay: reuse existing SDL window via class-level flag
- TerminalDisplay: skip re-init when reuse=True
- WebSocketDisplay: skip server start when reuse=True
- SixelDisplay, KittyDisplay, NullDisplay: ignore reuse (not applicable)
- MultiDisplay: pass reuse to child displays
- Update benchmark.py to reuse pygame display for effect benchmarks
- Add test_websocket_e2e.py with e2e marker
- Register e2e marker in pyproject.toml
This commit is contained in:
2026-03-16 00:30:52 -07:00
parent f9991c24af
commit f5de2c62e0
13 changed files with 309 additions and 34 deletions

View File

@@ -126,7 +126,14 @@ class KittyDisplay:
self._initialized = False
self._font_path = None
def init(self, width: int, height: int) -> None:
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: Ignored for KittyDisplay (protocol doesn't support reuse)
"""
self.width = width
self.height = height
self._initialized = True

View File

@@ -4,7 +4,10 @@ Multi display backend - forwards to multiple displays.
class MultiDisplay:
"""Display that forwards to multiple displays."""
"""Display that forwards to multiple displays.
Supports reuse - passes reuse flag to all child displays.
"""
width: int = 80
height: int = 24
@@ -14,11 +17,18 @@ class MultiDisplay:
self.width = 80
self.height = 24
def init(self, width: int, height: int) -> None:
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize all child displays with dimensions.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: If True, use reuse mode for child displays
"""
self.width = width
self.height = height
for d in self.displays:
d.init(width, height)
d.init(width, height, reuse=reuse)
def show(self, buffer: list[str]) -> None:
for d in self.displays:

View File

@@ -6,12 +6,23 @@ import time
class NullDisplay:
"""Headless/null display - discards all output."""
"""Headless/null display - discards all output.
This display does nothing - useful for headless benchmarking
or when no display output is needed.
"""
width: int = 80
height: int = 24
def init(self, width: int, height: int) -> None:
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: Ignored for NullDisplay (no resources to reuse)
"""
self.width = width
self.height = height

View File

@@ -6,12 +6,16 @@ import time
class PygameDisplay:
"""Pygame display backend - renders to native window."""
"""Pygame display backend - renders to native window.
Supports reuse mode - when reuse=True, skips SDL initialization
and reuses the existing pygame window from a previous instance.
"""
width: int = 80
height: int = 24
window_width: int = 800
window_height: int = 600
_pygame_initialized: bool = False
def __init__(
self,
@@ -76,20 +80,37 @@ class PygameDisplay:
return None
def init(self, width: int, height: int) -> None:
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: If True, attach to existing pygame window instead of creating new
"""
self.width = width
self.height = height
import os
os.environ["SDL_VIDEODRIVER"] = "x11"
try:
import pygame
except ImportError:
return
if reuse and PygameDisplay._pygame_initialized:
self._pygame = pygame
self._initialized = True
return
pygame.init()
self._pygame = pygame
pygame.display.set_caption("Mainline")
self._screen = pygame.display.set_mode((self.window_width, self.window_height))
pygame.display.set_caption("Mainline")
self._pygame = pygame
PygameDisplay._pygame_initialized = True
font_path = self._get_font_path()
if font_path:
@@ -218,6 +239,18 @@ class PygameDisplay:
self._screen.fill((0, 0, 0))
self._pygame.display.flip()
def cleanup(self) -> None:
if self._pygame:
def cleanup(self, quit_pygame: bool = True) -> None:
"""Cleanup display resources.
Args:
quit_pygame: If True, quit pygame entirely. Set to False when
reusing the display to avoid closing shared window.
"""
if quit_pygame and self._pygame:
self._pygame.quit()
PygameDisplay._pygame_initialized = False
@classmethod
def reset_state(cls) -> None:
"""Reset pygame state - useful for testing."""
cls._pygame_initialized = False

View File

@@ -271,7 +271,14 @@ class SixelDisplay:
return None
def init(self, width: int, height: int) -> None:
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: Ignored for SixelDisplay
"""
self.width = width
self.height = height
self._initialized = True

View File

@@ -6,17 +6,32 @@ import time
class TerminalDisplay:
"""ANSI terminal display backend."""
"""ANSI terminal display backend.
Renders buffer to stdout using ANSI escape codes.
Supports reuse - when reuse=True, skips re-initializing terminal state.
"""
width: int = 80
height: int = 24
_initialized: bool = False
def init(self, width: int, height: int) -> None:
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: If True, skip terminal re-initialization
"""
from engine.terminal import CURSOR_OFF
self.width = width
self.height = height
print(CURSOR_OFF, end="", flush=True)
if not reuse or not self._initialized:
print(CURSOR_OFF, end="", flush=True)
self._initialized = True
def show(self, buffer: list[str]) -> None:
import sys

View File

@@ -86,12 +86,20 @@ class WebSocketDisplay:
"""Check if WebSocket support is available."""
return self._available
def init(self, width: int, height: int) -> None:
"""Initialize display with dimensions and start server."""
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions and start server.
Args:
width: Terminal width in characters
height: Terminal height in rows
reuse: If True, skip starting servers (assume already running)
"""
self.width = width
self.height = height
self.start_server()
self.start_http_server()
if not reuse or not self._server_running:
self.start_server()
self.start_http_server()
def show(self, buffer: list[str]) -> None:
"""Broadcast buffer to all connected clients."""