121 lines
5.7 KiB
Markdown
121 lines
5.7 KiB
Markdown
# 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/`](../../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):
|
|
|
|
```cpp
|
|
#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.
|