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:
- a
target(tmux pane id%3orsession:window.pane) always means tmux; - no target and
TAI_PLEXER_SOCKETset: the native multiplexer. The agent passesports().plexer->native_read_paneas the reader; the last 200 lines of the raw pane log are replayed into aLINESxCOLUMNSscreen, default 50 x 160; - 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, steptermshot.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.diffandagent.e2ecompare against tai.py, so both callDetzTools::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/detzand 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
TuiVtScreengap). - 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.