Files
tai/taicpp/docs/termshot.md
T

5.7 KiB

termshot (taicpp): PNG pictures of the current terminal

Namespace detz::termshot, sources in src/termshot/. detz-only: tai.py has no equivalent. Research and decisions: ../../research/terminal-screenshot-rendering/.

What it does

Makes a PNG of the terminal the user is looking at, on a headless server too. No desktop, X server, system font, browser or external binary is involved.

tmux capture-pane -e -p -N      (or the native multiplexer pane log tail)
        |
TuiVtScreen (src/tui)            replay the bytes into a rows x cols cell grid
        |
TermshotRenderer                 background, glyph, bold, dim, reverse, underline, strike, cursor
        |
TermshotPng                      8-bit RGB, filter 0, zlib deflate, zlib crc32
        |
<home>/shots/terminal-YYYYmmdd-HHMMSS-mmm.png

Classes

Class File Role
TermshotFont termshot_font.*, termshot_font_data.cpp embedded Spleen 8x16 glyphs, binary search, fallback U+FFFD then ?
TermshotPalette termshot_palette.* SGR fragment to 0xRRGGBB: 16 xterm colors, 256 cube and gray ramp, truecolor, bold brightens 0..7
TermshotCanvas termshot_canvas.* RGB buffer: fill, glyph (bold doubles lit pixels right), pixel
TermshotPng termshot_png.* encoder and IHDR size reader
TermshotFrame, TermshotError termshot_frame.h captured bytes plus size, cursor, source label; failure with a tool-ready reason
TermshotCapture termshot_capture.* tmux(target), native(raw, pane, rows, cols), current(target, reader)
TermshotRenderer termshot_renderer.* frame to canvas
TermshotShooter, TermshotShot termshot_shooter.* frame to PNG bytes to file, cap check, one-line summary

Capture rules

TermshotCapture::current(target, reader) resolves the pane exactly like get_current_terminal_content:

  1. a target (tmux pane id %3 or session:window.pane) always means tmux;
  2. no target and TAI_PLEXER_SOCKET set: the native multiplexer. The agent passes ports().plexer->native_read_pane as the reader; the last 200 lines of the raw pane log are replayed into a LINES x COLUMNS screen, default 50 x 160;
  3. otherwise tmux with TMUX_PANE, or tmux's current pane when that is unset.

tmux calls: display-message -p '#{pane_width} #{pane_height} #{cursor_x} #{cursor_y} #{cursor_flag} #{pane_id}', then capture-pane -e -p -N [-t target] (visible screen, escapes kept, trailing spaces kept so background colors reach the right edge). Line ends become \r\n before the replay.

Font

Spleen 8x16 2.2.0 by Frederic Cambus, BSD 2-Clause. The BDF and the license are vendored in src/termshot/gen/; python3 src/termshot/gen/gen_font.py regenerates termshot_font_data.cpp (1001 glyphs, 16 bytes each, license text in the header). Covers Basic Latin, Latin-1, Latin Extended-A, box drawing, block elements, braille and CP437. Bold is synthesized (each lit pixel doubled one to the right).

Caps

All in termshot_consts.h.

Constant Value Why
kMaxCols 400 wider panes are cut at the right
kMaxRows 200 taller panes are cut
kNativeDefaultCols x kNativeDefaultRows 160 x 50 the native log has no pane size
kMaxCaptureBytes 4 MiB bytes fed to the VT parser (native keeps the tail)
kMaxPngBytes 16 MiB refuse larger files, also the WhatsApp image cap
kCaptureTimeout 10 s per tmux call
kCellWidth x kCellHeight 8 x 16 px font cell
kPadding 12 px border around the grid
kDefaultFg / kDefaultBg / kCursorColor #D0D0D0 / #1C1C1C / #D0D0D0 look

Measured: a 192 x 35 tmux pane gives a 1560 x 584 PNG of 22 to 26 KB.

Tool

terminal_screenshot {target?: string, path?: string}, registered in src/agent/detz_tools.cpp (detz-only tools; data.cpp stays a byte-exact copy of tai.py). Trigger tags: terminal, screenshot, picture, screen, image, snapshot. Result: terminal screenshot <path> (<bytes> bytes, <w>x<h> px, <rows> rows x <cols> cols, <source>, image/png), or error: terminal screenshot failed: <reason>.

For a caller in C++ (for example a WhatsApp send):

#include "../termshot/termshot_capture.h"
#include "../termshot/termshot_shooter.h"
using namespace detz::termshot;
TermshotFrame frame = TermshotCapture::current(target, reader);   // reader may be empty
TermshotShot shot = TermshotShooter(home).shoot(frame);           // writes the file
std::string png = TermshotShooter::png(frame);                     // or bytes only

WhatsApp: send the bytes as an imageMessage with mimetype image/png; no JPEG conversion is done (see the research conclusion whatsapp-image-as-png).

Tests

  • build/termshot_test (tests/termshot_test.cpp, step termshot.unit): font coverage and fallback, palette values, PNG signature and size, decoded pixel colors for background, SGR background, full block foreground, reverse, line feed, cursor, caps, native tail cut, file write under <home>/shots.
  • agent.diff and agent.e2e compare against tai.py, so both call DetzTools::set_enabled(false); with detz tools on they stay on in every real run.
  • Live: panes rendered from tmux 3.6 and viewed; a real model (devplace) called the tool through build/detz and returned the written path.

Gaps

  • Glyphs Spleen lacks (CJK, Greek, Cyrillic beyond Latin Extended-A, symbols such as U+276F or U+23F5) draw as ?.
  • Wide characters take one column (the same TuiVtScreen gap).
  • The native multiplexer path replays a log tail into a guessed size; not verified live.
  • Only the visible screen; no scrollback option.
  • No JPEG thumbnail is produced for WhatsApp; the chat preview may stay empty until the image is downloaded.