""" Kitty graphics display backend - renders using kitty's native graphics protocol. """ import time from engine.display.renderer import get_default_font_path, parse_ansi def _encode_kitty_graphic(image_data: bytes, width: int, height: int) -> bytes: """Encode image data using kitty's graphics protocol.""" import base64 encoded = base64.b64encode(image_data).decode("ascii") chunks = [] for i in range(0, len(encoded), 4096): chunk = encoded[i : i + 4096] if i == 0: chunks.append(f"\x1b_Gf=100,t=d,s={width},v={height},c=1,r=1;{chunk}\x1b\\") else: chunks.append(f"\x1b_Gm={height};{chunk}\x1b\\") return "".join(chunks).encode("utf-8") class KittyDisplay: """Kitty graphics display backend using kitty's native protocol.""" width: int = 80 height: int = 24 def __init__(self, cell_width: int = 9, cell_height: int = 16): self.width = 80 self.height = 24 self.cell_width = cell_width self.cell_height = cell_height self._initialized = False self._font_path = 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 def _get_font_path(self) -> str | None: """Get font path from env or detect common locations.""" import os if self._font_path: return self._font_path env_font = os.environ.get("MAINLINE_KITTY_FONT") if env_font and os.path.exists(env_font): self._font_path = env_font return env_font font_path = get_default_font_path() if font_path: self._font_path = font_path return self._font_path def show(self, buffer: list[str], border: bool = False) -> None: import sys t0 = time.perf_counter() # Get metrics for border display fps = 0.0 frame_time = 0.0 from engine.display import get_monitor 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 if border: from engine.display import render_border buffer = render_border(buffer, self.width, self.height, fps, frame_time) img_width = self.width * self.cell_width img_height = self.height * self.cell_height try: from PIL import Image, ImageDraw, ImageFont except ImportError: return img = Image.new("RGBA", (img_width, img_height), (0, 0, 0, 255)) draw = ImageDraw.Draw(img) font_path = self._get_font_path() font = None if font_path: try: font = ImageFont.truetype(font_path, self.cell_height - 2) except Exception: font = None if font is None: try: font = ImageFont.load_default() except Exception: font = None for row_idx, line in enumerate(buffer[: self.height]): if row_idx >= self.height: break tokens = parse_ansi(line) x_pos = 0 y_pos = row_idx * self.cell_height for text, fg, bg, bold in tokens: if not text: continue if bg != (0, 0, 0): bbox = draw.textbbox((x_pos, y_pos), text, font=font) draw.rectangle(bbox, fill=(*bg, 255)) if bold and font: draw.text((x_pos - 1, y_pos - 1), text, fill=(*fg, 255), font=font) draw.text((x_pos, y_pos), text, fill=(*fg, 255), font=font) if font: x_pos += draw.textlength(text, font=font) from io import BytesIO output = BytesIO() img.save(output, format="PNG") png_data = output.getvalue() graphic = _encode_kitty_graphic(png_data, img_width, img_height) sys.stdout.buffer.write(graphic) sys.stdout.flush() 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("kitty_display", elapsed_ms, chars_in, chars_in) def clear(self) -> None: import sys sys.stdout.buffer.write(b"\x1b_Ga=d\x1b\\") sys.stdout.flush() def cleanup(self) -> None: self.clear() def get_dimensions(self) -> tuple[int, int]: """Get current dimensions. Returns: (width, height) in character cells """ return (self.width, self.height)