#!/usr/bin/env python3
"""PTY-backed terminal E2E harness for mim.
Runs a tiny high-signal scenario through a real pseudo-terminal and writes
browser-viewable terminal artifacts. No third-party dependencies.
"""
from __future__ import annotations
import argparse
import html
import os
import pty
import select
import signal
import shutil
import sys
import tempfile
import time
from pathlib import Path
ESC = 0x1B
def visible_controls(data: bytes) -> str:
out: list[str] = []
text = data.decode("utf-8", errors="replace")
for ch in text:
code = ord(ch)
if ch == "\r":
out.append("␍")
elif ch == "\n":
out.append("␊\n")
elif ch == "\x1b":
out.append("␛")
elif ch == "\t":
out.append("⇥")
elif code < 0x20:
out.append("␀")
else:
out.append(ch)
return "".join(out)
class TerminalGrid:
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
self.rows = [[" " for _ in range(width)] for _ in range(height)]
self.row = 0
self.col = 0
def clear(self) -> None:
self.rows = [[" " for _ in range(self.width)] for _ in range(self.height)]
self.row = 0
self.col = 0
def newline(self) -> None:
self.row += 1
if self.row >= self.height:
self.rows.pop(0)
self.rows.append([" " for _ in range(self.width)])
self.row = self.height - 1
def put_char(self, ch: str) -> None:
if ch == "\r":
self.col = 0
return
if ch == "\n":
self.newline()
return
if self.col >= self.width:
self.col = 0
self.newline()
if 0 <= self.row < self.height and 0 <= self.col < self.width:
self.rows[self.row][self.col] = ch
self.col += 1
def feed(self, data: bytes) -> None:
i = 0
text = data.decode("utf-8", errors="replace")
while i < len(text):
ch = text[i]
if ch == "\x1b" and i + 1 < len(text) and text[i + 1] == "[":
j = i + 2
while j < len(text) and not ("@" <= text[j] <= "~"):
j += 1
if j >= len(text):
break
command = text[j]
params = text[i + 2 : j]
if command == "H":
self.row = 0
self.col = 0
elif command == "J" and params.endswith("2"):
self.clear()
# SGR and unknown CSI are intentionally ignored for semantic grid.
i = j + 1
continue
self.put_char(ch)
i += 1
def lines(self) -> list[str]:
return ["".join(row).rstrip() for row in self.rows]
def read_available(fd: int, timeout: float = 0.12) -> bytes:
chunks: list[bytes] = []
deadline = time.monotonic() + timeout
while True:
remaining = max(0.0, deadline - time.monotonic())
ready, _, _ = select.select([fd], [], [], remaining)
if not ready:
break
try:
chunk = os.read(fd, 65536)
except OSError:
break
if not chunk:
break
chunks.append(chunk)
deadline = time.monotonic() + 0.04
return b"".join(chunks)
def spawn_under_pty(argv: list[str], width: int, height: int) -> tuple[int, int]:
pid, fd = pty.fork()
if pid == 0:
os.environ["TERM"] = "xterm-256color"
try:
import fcntl
import struct
import termios
winsize = struct.pack("HHHH", height, width, 0, 0)
fcntl.ioctl(0, termios.TIOCSWINSZ, winsize)
except Exception:
pass
os.execv(argv[0], argv)
return pid, fd
def svg_for_lines(lines: list[str], width: int, height: int) -> str:
cell_w = 9
cell_h = 18
pad = 12
svg_w = pad * 2 + width * cell_w
svg_h = pad * 2 + height * cell_h
body = []
for idx, line in enumerate(lines[:height]):
body.append(
f'