""" ANSI terminal display backend. """ import os class TerminalDisplay: """ANSI terminal display backend. Renders buffer to stdout using ANSI escape codes. Supports reuse - when reuse=True, skips re-initializing terminal state. Auto-detects terminal dimensions on init. """ width: int = 80 height: int = 24 _initialized: bool = False def __init__(self, target_fps: float = 30.0): self.target_fps = target_fps self._frame_period = 1.0 / target_fps if target_fps > 0 else 0 self._last_frame_time = 0.0 self._cached_dimensions: tuple[int, int] | None = None def init(self, width: int, height: int, reuse: bool = False) -> None: """Initialize display with dimensions. If width/height are not provided (0/None), auto-detects terminal size. Otherwise uses provided dimensions or falls back to terminal size if the provided dimensions exceed terminal capacity. Args: width: Desired terminal width (0 = auto-detect) height: Desired terminal height (0 = auto-detect) reuse: If True, skip terminal re-initialization """ from engine.terminal import CURSOR_OFF # Auto-detect terminal size (handle case where no terminal) try: term_size = os.get_terminal_size() term_width = term_size.columns term_height = term_size.lines except OSError: # No terminal available (e.g., in tests) term_width = width if width > 0 else 80 term_height = height if height > 0 else 24 # Use provided dimensions if valid, otherwise use terminal size if width > 0 and height > 0: self.width = min(width, term_width) self.height = min(height, term_height) else: self.width = term_width self.height = term_height if not reuse or not self._initialized: print(CURSOR_OFF, end="", flush=True) self._initialized = True def get_dimensions(self) -> tuple[int, int]: """Get current terminal dimensions. Returns cached dimensions to avoid querying terminal every frame, which can cause inconsistent results. Dimensions are only refreshed when they actually change. Returns: (width, height) in character cells """ try: term_size = os.get_terminal_size() new_dims = (term_size.columns, term_size.lines) except OSError: new_dims = (self.width, self.height) # Only update cached dimensions if they actually changed if self._cached_dimensions is None or self._cached_dimensions != new_dims: self._cached_dimensions = new_dims self.width = new_dims[0] self.height = new_dims[1] return self._cached_dimensions def show(self, buffer: list[str], border: bool = False) -> None: import sys from engine.display import get_monitor, render_border # Note: Frame rate limiting is handled by the caller (e.g., FrameTimer). # This display renders every frame it receives. # Get metrics for border display fps = 0.0 frame_time = 0.0 monitor = get_monitor() if monitor: stats = monitor.get_stats() avg_ms = stats.get("pipeline", {}).get("avg_ms", 0) if stats else 0 frame_count = stats.get("frame_count", 0) if stats else 0 if avg_ms and frame_count > 0: fps = 1000.0 / avg_ms frame_time = avg_ms # Apply border if requested from engine.display import BorderMode if border and border != BorderMode.OFF: buffer = render_border(buffer, self.width, self.height, fps, frame_time) # Write buffer with cursor home + erase down to avoid flicker output = "\033[H\033[J" + "\n".join(buffer) sys.stdout.buffer.write(output.encode()) sys.stdout.flush() def clear(self) -> None: from engine.terminal import CLR print(CLR, end="", flush=True) def cleanup(self) -> None: from engine.terminal import CURSOR_ON print(CURSOR_ON, end="", flush=True) def is_quit_requested(self) -> bool: """Check if quit was requested (optional protocol method).""" return False def clear_quit_request(self) -> None: """Clear quit request (optional protocol method).""" pass