patternpythonplaywrightModerate
Screenshot curses TUI apps with Playwright via simulated rendering
Viewed 0 times
curses screenshotTUI captureplaywright terminalcurses to HTMLsimulated renderingPTY sandboxterminal screenshot
Problem
You need to take a screenshot of a Python curses-based TUI app using Playwright, but curses requires a real TTY (which sandboxed/CI environments lack), PTY forking via os.fork()/pty.openpty() hangs in sandboxed environments like Claude Code, and Playwright can't access file:// URLs. Three separate blockers that all need solving together.
Solution
Skip the live PTY capture entirely. Instead, simulate the curses rendering by recreating the screen layout programmatically — build the same visual output as a 2D grid of (text, color) segments, then convert to styled HTML with a terminal-like appearance (monospace font, dark background, colored spans). Serve the HTML via a Python http.server on localhost (not file://) and use Playwright MCP to navigate and screenshot. Full pipeline: (1) Write a render function that reproduces what curses.addstr() would draw, mapping color pairs to CSS colors. (2) Generate an HTML file with <span> elements styled with inline CSS for each character/segment. (3) Serve via http.server on a free port (8787+). (4) Navigate Playwright to http://localhost:PORT/file.html. (5) Screenshot the terminal div element. This avoids all three blockers: no TTY needed, no fork needed, no file:// needed.
Why
Three architectural constraints collide: (1) curses uses setupterm() which requires a kernel PTY device — pipes and redirects won't work. (2) os.fork() in sandboxed environments (containers, Claude Code CLI) may hang because the child process inherits the sandbox restrictions. (3) Playwright/Chromium blocks file:// for security — only http(s):// origins work. The simulated rendering approach sidesteps all three by never actually running curses, instead reproducing its visual output as HTML.
Gotchas
- PTY forking (os.fork + pty.openpty) hangs silently in sandboxed environments — no error, just blocks forever
- http.server serves from CWD by default — if CWD differs from the HTML file location (e.g. case-sensitive path mismatch on macOS), you get 404
- An old http.server process may hold the port — always check with lsof -i :PORT before starting
- The simulated approach only works if you know what the TUI renders — for dynamic/interactive apps, use pyte + script command outside sandboxed environments
- Catppuccin or similar terminal color schemes make the HTML output look authentic
Code Snippets
Simulated curses-to-HTML renderer core pattern
# Build screen as list of (text, color) segments per line
lines = []
lines.append([(' Title Bar ', 'title')])
lines.append([(' Status: OK', 'green'), (' Errors: 0', 'red')])
# Render to HTML spans
for line_segments in lines:
for text, color in line_segments:
fg = color_map[color]
html += f'<span style="color:{fg}">{text}</span>'
# Serve via HTTP (not file://!)
import http.server, threading
server = http.server.HTTPServer(('localhost', 8787), http.server.SimpleHTTPRequestHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
# Then Playwright navigates to http://localhost:8787/output.htmlRevisions (0)
No revisions yet.