""" Display backend system with registry pattern. Allows swapping output backends via the Display protocol. Supports auto-discovery of display backends. """ from enum import Enum, auto from typing import Protocol # Optional backend - requires moderngl package try: from engine.display.backends.moderngl import ModernGLDisplay _MODERNGL_AVAILABLE = True except ImportError: ModernGLDisplay = None _MODERNGL_AVAILABLE = False from engine.display.backends.multi import MultiDisplay from engine.display.backends.null import NullDisplay from engine.display.backends.pygame import PygameDisplay from engine.display.backends.replay import ReplayDisplay from engine.display.backends.terminal import TerminalDisplay from engine.display.backends.websocket import WebSocketDisplay class BorderMode(Enum): """Border rendering modes for displays.""" OFF = auto() # No border SIMPLE = auto() # Traditional border with FPS/frame time UI = auto() # Right-side UI panel with interactive controls class Display(Protocol): """Protocol for display backends. Required attributes: - width: int - height: int Required methods (duck typing - actual signatures may vary): - init(width, height, reuse=False) - show(buffer, border=False) - clear() - cleanup() - get_dimensions() -> (width, height) Optional attributes (for UI mode): - ui_panel: UIPanel instance (set by app when border=UI) Optional methods: - is_quit_requested() -> bool - clear_quit_request() -> None """ width: int height: int class DisplayRegistry: """Registry for display backends with auto-discovery.""" _backends: dict[str, type[Display]] = {} _initialized = False @classmethod def register(cls, name: str, backend_class: type[Display]) -> None: cls._backends[name.lower()] = backend_class @classmethod def get(cls, name: str) -> type[Display] | None: return cls._backends.get(name.lower()) @classmethod def list_backends(cls) -> list[str]: return list(cls._backends.keys()) @classmethod def create(cls, name: str, **kwargs) -> Display | None: cls.initialize() backend_class = cls.get(name) if backend_class: return backend_class(**kwargs) return None @classmethod def initialize(cls) -> None: if cls._initialized: return cls.register("terminal", TerminalDisplay) cls.register("null", NullDisplay) cls.register("replay", ReplayDisplay) cls.register("websocket", WebSocketDisplay) cls.register("pygame", PygameDisplay) if _MODERNGL_AVAILABLE: cls.register("moderngl", ModernGLDisplay) # type: ignore[arg-type] cls._initialized = True @classmethod def create_multi(cls, names: list[str]) -> MultiDisplay | None: displays = [] for name in names: backend = cls.create(name) if backend: displays.append(backend) else: return None if not displays: return None return MultiDisplay(displays) def get_monitor(): """Get the performance monitor.""" try: from engine.effects.performance import get_monitor as _get_monitor return _get_monitor() except Exception: return None def _strip_ansi(s: str) -> str: """Strip ANSI escape sequences from string for length calculation.""" import re return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", s) def _render_simple_border( buf: list[str], width: int, height: int, fps: float = 0.0, frame_time: float = 0.0 ) -> list[str]: """Render a traditional border around the buffer.""" if not buf or width < 3 or height < 3: return buf inner_w = width - 2 inner_h = height - 2 cropped = [] for i in range(min(inner_h, len(buf))): line = buf[i] visible_len = len(_strip_ansi(line)) if visible_len > inner_w: cropped.append(line[:inner_w]) else: cropped.append(line + " " * (inner_w - visible_len)) while len(cropped) < inner_h: cropped.append(" " * inner_w) 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 + "┐" 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 + "┘" result = [top_border] for line in cropped: if len(line) < inner_w: line = line + " " * (inner_w - len(line)) elif len(line) > inner_w: line = line[:inner_w] result.append("│" + line + "│") result.append(bottom_border) return result def render_ui_panel( buf: list[str], width: int, height: int, ui_panel, fps: float = 0.0, frame_time: float = 0.0, ) -> list[str]: """Render buffer with a right-side UI panel.""" from engine.pipeline.ui import UIPanel if not isinstance(ui_panel, UIPanel): return _render_simple_border(buf, width, height, fps, frame_time) panel_width = min(ui_panel.config.panel_width, width - 4) main_width = width - panel_width - 1 panel_lines = ui_panel.render(panel_width, height) main_buf = buf[: height - 2] main_result = _render_simple_border( main_buf, main_width + 2, height, fps, frame_time ) combined = [] for i in range(height): if i < len(main_result): main_line = main_result[i] if len(main_line) >= 2: main_content = ( main_line[1:-1] if main_line[-1] in "│┌┐└┘" else main_line[1:] ) main_content = main_content.ljust(main_width)[:main_width] else: main_content = " " * main_width else: main_content = " " * main_width panel_idx = i panel_line = ( panel_lines[panel_idx][:panel_width].ljust(panel_width) if panel_idx < len(panel_lines) else " " * panel_width ) separator = "│" if 0 < i < height - 1 else "┼" if i == 0 else "┴" combined.append(main_content + separator + panel_line) return combined def render_border( buf: list[str], width: int, height: int, fps: float = 0.0, frame_time: float = 0.0, border_mode: BorderMode | bool = BorderMode.SIMPLE, ) -> list[str]: """Render a border or UI panel around the buffer. Args: buf: Input buffer width: Display width height: Display height fps: FPS for top border frame_time: Frame time for bottom border border_mode: Border rendering mode Returns: Buffer with border/panel applied """ # Normalize border_mode to BorderMode enum if isinstance(border_mode, bool): border_mode = BorderMode.SIMPLE if border_mode else BorderMode.OFF if border_mode == BorderMode.UI: # UI panel requires a UIPanel instance (injected separately) # For now, this will be called by displays that have a ui_panel attribute # This function signature doesn't include ui_panel, so we'll handle it in render_ui_panel # Fall back to simple border if no panel available return _render_simple_border(buf, width, height, fps, frame_time) elif border_mode == BorderMode.SIMPLE: return _render_simple_border(buf, width, height, fps, frame_time) else: return buf __all__ = [ "Display", "DisplayRegistry", "get_monitor", "render_border", "render_ui_panel", "BorderMode", "TerminalDisplay", "NullDisplay", "ReplayDisplay", "WebSocketDisplay", "MultiDisplay", "PygameDisplay", ] if _MODERNGL_AVAILABLE: __all__.append("ModernGLDisplay")