#!/usr/bin/env python3 """ANSI pane dump → HTML → PNG (chrome headless). Usage: tui_shot.py in.ansi out.png""" from __future__ import annotations import html import subprocess import sys from pathlib import Path SPECIAL = { 75: "#6ea8fe", 74: "#61b8c7", 216: "#faa06b", 233: "#0e0f12", 234: "#14161e", 235: "#1a1c24", 244: "#80848f", 250: "#bcbcbc", 252: "#d1d4d9", 255: "#ffffff", 242: "#6a6a6a", 114: "#66bf73", 167: "#d95252", 157: "#afff87", 22: "#005f00", 203: "#ff5f5f", 52: "#5f0000", } def ansi_to_html(text: str) -> str: cmap = { 30: "#3c3c46", 31: "#e65a5a", 32: "#5ad278", 33: "#faa06b", 34: "#6ea8fe", 35: "#d282d2", 36: "#5ad2e6", 37: "#bcbcbc", 90: "#80848f", 91: "#ff8282", 92: "#82ff96", 93: "#ffeb82", 94: "#96beff", 95: "#eba0eb", 96: "#82ebff", 97: "#ffffff", } out: list[str] = [] i = 0 bold = dim = False fg = "#d1d4d9" open_span = False def close() -> None: nonlocal open_span if open_span: out.append("") open_span = False def reopen() -> None: nonlocal open_span close() styles = [f"color:{fg}"] if bold: styles.append("font-weight:700") if dim: styles.append("opacity:.65") out.append(f'') open_span = True reopen() while i < len(text): if text[i] == "\x1b" and i + 1 < len(text) and text[i + 1] == "[": j = i + 2 while j < len(text) and text[j] != "m": j += 1 seq = text[i + 2 : j] i = j + 1 if j < len(text) else len(text) parts = [int(p) for p in seq.split(";") if p.isdigit()] if seq else [0] k = 0 while k < len(parts): c = parts[k] if c == 0: bold = dim = False fg = "#d1d4d9" elif c == 1: bold = True elif c == 2: dim = True elif c == 22: bold = dim = False elif c == 38 and k + 2 < len(parts) and parts[k + 1] == 5: fg = SPECIAL.get(parts[k + 2], fg) k += 2 elif c in cmap: fg = cmap[c] elif c == 39: fg = "#d1d4d9" k += 1 reopen() continue if text[i] == "\n": out.append("\n") i += 1 continue if text[i] == "\r": i += 1 continue out.append(html.escape(text[i])) i += 1 close() body = "".join(out) return f"""
{body}
""" def main() -> int: if len(sys.argv) < 3: print("usage: tui_shot.py in.ansi out.png", file=sys.stderr) return 2 ansi = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace") html_path = Path(sys.argv[2]).with_suffix(".html") html_path.write_text(ansi_to_html(ansi), encoding="utf-8") png = Path(sys.argv[2]) chrome = "google-chrome" subprocess.run( [ chrome, "--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars", "--window-size=1280,800", f"--screenshot={png}", html_path.resolve().as_uri(), ], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) print(png, png.stat().st_size if png.exists() else 0) return 0 if png.exists() else 1 if __name__ == "__main__": raise SystemExit(main())