""" Pygame display backend - renders to a native application window. """ import time class PygameDisplay: """Pygame display backend - renders to native window.""" width: int = 80 height: int = 24 window_width: int = 800 window_height: int = 600 def __init__( self, cell_width: int = 10, cell_height: int = 18, window_width: int = 800, window_height: int = 600, ): self.width = 80 self.height = 24 self.cell_width = cell_width self.cell_height = cell_height self.window_width = window_width self.window_height = window_height self._initialized = False self._pygame = None self._screen = None self._font = None def _get_font_path(self) -> str | None: """Get font path for rendering.""" import os import sys from pathlib import Path env_font = os.environ.get("MAINLINE_PYGAME_FONT") if env_font and os.path.exists(env_font): return env_font def search_dir(base_path: str) -> str | None: if not os.path.exists(base_path): return None if os.path.isfile(base_path): return base_path for font_file in Path(base_path).rglob("*"): if font_file.suffix.lower() in (".ttf", ".otf", ".ttc"): name = font_file.stem.lower() if "geist" in name and ("nerd" in name or "mono" in name): return str(font_file) return None search_dirs = [] if sys.platform == "darwin": search_dirs.append(os.path.expanduser("~/Library/Fonts/")) elif sys.platform == "win32": search_dirs.append( os.path.expanduser("~\\AppData\\Local\\Microsoft\\Windows\\Fonts\\") ) else: search_dirs.extend( [ os.path.expanduser("~/.local/share/fonts/"), os.path.expanduser("~/.fonts/"), "/usr/share/fonts/", ] ) for search_dir_path in search_dirs: found = search_dir(search_dir_path) if found: return found return None def init(self, width: int, height: int) -> None: self.width = width self.height = height try: import pygame except ImportError: return pygame.init() self._pygame = pygame self._screen = pygame.display.set_mode((self.window_width, self.window_height)) pygame.display.set_caption("Mainline") font_path = self._get_font_path() if font_path: try: self._font = pygame.font.Font(font_path, self.cell_height - 2) except Exception: self._font = pygame.font.SysFont("monospace", self.cell_height - 2) else: self._font = pygame.font.SysFont("monospace", self.cell_height - 2) self._initialized = True def show(self, buffer: list[str]) -> None: import sys if not self._initialized or not self._pygame: return t0 = time.perf_counter() for event in self._pygame.event.get(): if event.type == self._pygame.QUIT: sys.exit(0) self._screen.fill((0, 0, 0)) for row_idx, line in enumerate(buffer[: self.height]): if row_idx >= self.height: break tokens = self._parse_ansi(line) x_pos = 0 for text, fg, bg in tokens: if not text: continue if bg != (0, 0, 0): bg_surface = self._font.render(text, True, fg, bg) self._screen.blit(bg_surface, (x_pos, row_idx * self.cell_height)) else: text_surface = self._font.render(text, True, fg) self._screen.blit(text_surface, (x_pos, row_idx * self.cell_height)) x_pos += self._font.size(text)[0] self._pygame.display.flip() elapsed_ms = (time.perf_counter() - t0) * 1000 from engine.display import get_monitor monitor = get_monitor() if monitor: chars_in = sum(len(line) for line in buffer) monitor.record_effect("pygame_display", elapsed_ms, chars_in, chars_in) def _parse_ansi( self, text: str ) -> list[tuple[str, tuple[int, int, int], tuple[int, int, int]]]: """Parse ANSI text into tokens with fg/bg colors.""" tokens = [] current_text = "" fg = (204, 204, 204) bg = (0, 0, 0) i = 0 ANSI_COLORS = { 0: (0, 0, 0), 1: (205, 49, 49), 2: (13, 188, 121), 3: (229, 229, 16), 4: (36, 114, 200), 5: (188, 63, 188), 6: (17, 168, 205), 7: (229, 229, 229), 8: (102, 102, 102), 9: (241, 76, 76), 10: (35, 209, 139), 11: (245, 245, 67), 12: (59, 142, 234), 13: (214, 112, 214), 14: (41, 184, 219), 15: (255, 255, 255), } while i < len(text): char = text[i] if char == "\x1b" and i + 1 < len(text) and text[i + 1] == "[": if current_text: tokens.append((current_text, fg, bg)) current_text = "" i += 2 code = "" while i < len(text): c = text[i] if c.isalpha(): break code += c i += 1 if code: codes = code.split(";") for c in codes: if c == "0": fg = (204, 204, 204) bg = (0, 0, 0) elif c.isdigit(): color_idx = int(c) if color_idx in ANSI_COLORS: fg = ANSI_COLORS[color_idx] i += 1 else: current_text += char i += 1 if current_text: tokens.append((current_text, fg, bg)) return tokens def clear(self) -> None: if self._screen and self._pygame: self._screen.fill((0, 0, 0)) self._pygame.display.flip() def cleanup(self) -> None: if self._pygame: self._pygame.quit()