Files
tai/taicpp/tests/termshot_test.cpp
T
retoorandClaude Opus 4.8 115e403e67 whatsapp: native C++ client + terminal-screenshot tool
Native WhatsApp Web multi-device client in C++ (src/whatsapp/, namespace
detz::wa), built on OpenSSL plus a vendored public-domain ed25519 ref10
(third_party/ed25519, zlib) for XEdDSA and the nayuki QR generator
(third_party/qrcodegen, MIT); no new runtime dependency.

- WaCrypto, WaBinary, WaProto, WaNoise, WaSignal, WaDevice, WaSocket,
  WaClient, WaStore, WaQr, WaWizard.
- Verified offline against golden vectors captured from the real Baileys
  client (tests/whatsapp_data, from jfelipesjc/wa-go, MIT): HKDF/PBKDF2 RFC
  vectors, XEdDSA roundtrips, codec battery 19/19, ClientPayload byte-exact,
  the Noise handshake, and a 4-exchange Signal session byte-exact. Unit test
  build/whatsapp_test (16/16), run_all step whatsapp.unit.
- Verified live against web.whatsapp.com: `detz --whatsapp-link` connects,
  completes the Noise handshake, receives pair-device, renders a scannable QR
  in the terminal, and persists the device to the vault (whatsapp/<profile>.db).
  Also --whatsapp-status / --whatsapp-unlink.
- Agent tools whatsapp_send and whatsapp_contacts (detz-only, in DetzTools).
- Pairing completion after the phone scans, and the message send path
  (usync + prekey fetch + Signal encrypt + send node), are ported faithfully
  from whatsmeow but unverified in-tree (no linked account in development).
  send_image and own-device fan-out are not wired yet. See docs/whatsapp.md.

Also in this snapshot: the terminal-screenshot tool (src/termshot/,
terminal_screenshot, docs/termshot.md, research/terminal-screenshot-rendering)
and a concurrent session's in-progress llm/muse failover changes carried along
in the shared files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-27 18:22:24 +01:00

123 lines
6.1 KiB
C++
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// termshot (src/termshot): font lookup, palette, ANSI fixtures to PNG, pixel colors, caps, file write.
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include <unistd.h>
#include <zlib.h>
#include "termshot_canvas.h"
#include "termshot_capture.h"
#include "termshot_consts.h"
#include "termshot_font.h"
#include "termshot_palette.h"
#include "termshot_png.h"
#include "termshot_renderer.h"
#include "termshot_shooter.h"
using namespace detz::termshot;
static int failures = 0;
#define CHECK(c) do { if (!(c)) { std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #c); ++failures; } } while (0)
// Decode the RGB rows of a PNG written by TermshotPng (single IDAT, filter 0).
static bool pixel_at(const std::string& png, int x, int y, std::uint32_t& rgb) {
int w = 0, h = 0;
if (!TermshotPng::dimensions(png, w, h)) return false;
size_t at = 8;
std::string idat;
while (at + 8 <= png.size()) {
uint32_t len = (uint32_t(uint8_t(png[at])) << 24) | (uint32_t(uint8_t(png[at + 1])) << 16) | (uint32_t(uint8_t(png[at + 2])) << 8) |
uint32_t(uint8_t(png[at + 3]));
std::string type = png.substr(at + 4, 4);
if (type == "IDAT") idat += png.substr(at + 8, len);
at += 12 + len;
}
uLongf raw_len = static_cast<uLongf>(h) * static_cast<uLongf>(w * 3 + 1);
std::string raw(raw_len, '\0');
if (uncompress(reinterpret_cast<Bytef*>(raw.data()), &raw_len, reinterpret_cast<const Bytef*>(idat.data()), idat.size()) != Z_OK) return false;
size_t p = static_cast<size_t>(y) * static_cast<size_t>(w * 3 + 1) + 1 + static_cast<size_t>(x) * 3;
if (raw[static_cast<size_t>(y) * static_cast<size_t>(w * 3 + 1)] != 0) return false;
rgb = (uint32_t(uint8_t(raw[p])) << 16) | (uint32_t(uint8_t(raw[p + 1])) << 8) | uint32_t(uint8_t(raw[p + 2]));
return true;
}
static TermshotFrame frame(const std::string& ansi, int rows, int cols) {
TermshotFrame f;
f.ansi = ansi;
f.rows = rows;
f.cols = cols;
f.source = "ansi";
return f;
}
int main() {
// Font: sorted table, the coverage a TUI needs, fallback for the rest.
CHECK(TermshotFont::size() == 1001);
CHECK(TermshotFont::find(U'A') != nullptr);
CHECK(TermshotFont::find(U'─') != nullptr); // box drawing
CHECK(TermshotFont::find(U'█') != nullptr); // full block
CHECK(TermshotFont::find(U'⠀') != nullptr); // braille
CHECK(TermshotFont::find(U'中') == nullptr); // CJK not covered
CHECK(TermshotFont::glyph(U'中').code == U'�' || TermshotFont::glyph(U'中').code == U'?');
const TermshotGlyph& block = TermshotFont::glyph(U'█');
CHECK(block.rows[0] == 0xFF && block.rows[15] == 0xFF);
// Palette: named, bright, bold-brightening, 256 cube, gray ramp, truecolor.
CHECK(TermshotPalette::fragment(std::string("31"), 0, false, false) == 0xCD0000);
CHECK(TermshotPalette::fragment(std::string("31"), 0, false, true) == 0xFF0000);
CHECK(TermshotPalette::fragment(std::string("94"), 0, false, false) == 0x5C5CFF);
CHECK(TermshotPalette::fragment(std::string("38;5;196"), 0, false, false) == 0xFF0000);
CHECK(TermshotPalette::fragment(std::string("38;5;232"), 0, false, false) == 0x080808);
CHECK(TermshotPalette::fragment(std::string("48;2;1;2;3"), 0, true, false) == 0x010203);
CHECK(TermshotPalette::fragment(std::nullopt, 0x123456, false, false) == 0x123456);
// Dimensions follow the grid plus padding.
std::string png = TermshotShooter::png(frame("hello", 3, 10));
int w = 0, h = 0;
CHECK(png.compare(0, 8, std::string("\x89PNG\r\n\x1a\n", 8)) == 0);
CHECK(TermshotPng::dimensions(png, w, h));
CHECK(w == 10 * kCellWidth + 2 * kPadding && h == 3 * kCellHeight + 2 * kPadding);
// Pixels: padding is the default background, a red-background cell is red,
// a full block in green fills its cell, reverse swaps fg and bg.
std::string colored = TermshotShooter::png(frame("\x1b[41m \x1b[0m\x1b[32m\xe2\x96\x88\x1b[0m\x1b[7m \x1b[0m", 1, 4));
std::uint32_t c = 0;
CHECK(pixel_at(colored, 0, 0, c) && c == kDefaultBg);
CHECK(pixel_at(colored, kPadding + 3, kPadding + 8, c) && c == 0xCD0000);
CHECK(pixel_at(colored, kPadding + kCellWidth + 3, kPadding + 8, c) && c == 0x00CD00);
CHECK(pixel_at(colored, kPadding + 2 * kCellWidth + 3, kPadding + 8, c) && c == kDefaultFg);
// Lines: \r\n moves down; text lands in row 2.
std::string two = TermshotShooter::png(frame("a\r\n\x1b[44m \x1b[0m", 2, 2));
CHECK(pixel_at(two, kPadding + 2, kPadding + kCellHeight + 8, c) && c == 0x0000EE);
// Cursor: drawn as a block on an empty cell.
TermshotFrame cur = frame("", 2, 2);
cur.cursor_x = 1; cur.cursor_y = 1;
std::string cpng = TermshotShooter::png(cur);
CHECK(pixel_at(cpng, kPadding + kCellWidth + 4, kPadding + kCellHeight + 8, c) && c == kCursorColor);
// Caps: absurd sizes are clamped.
int big_w = 0, big_h = 0;
std::string capped = TermshotShooter::png(frame("x", 100000, 100000));
CHECK(TermshotPng::dimensions(capped, big_w, big_h));
CHECK(big_w == TermshotRenderer::width_for(kMaxCols) && big_h == TermshotRenderer::height_for(kMaxRows));
TermshotFrame nat = TermshotCapture::native(std::string(kMaxCaptureBytes + 10, 'x'), "1");
CHECK(nat.ansi.size() == kMaxCaptureBytes && nat.rows == kNativeDefaultRows && nat.cols == kNativeDefaultCols);
CHECK(nat.source == "native pane 1");
// File: default path under <home>/shots, bytes on disk equal the encoder output.
auto home = std::filesystem::temp_directory_path() / ("termshot_test_" + std::to_string(::getpid()));
TermshotShooter shooter(home.string());
TermshotShot shot = shooter.shoot(frame("\x1b[1;33mdetz\x1b[0m", 2, 8));
CHECK(shot.path.rfind((home / kShotDir).string(), 0) == 0);
CHECK(std::filesystem::file_size(shot.path) == shot.bytes);
CHECK(shot.width == TermshotRenderer::width_for(8) && shot.rows == 2);
CHECK(shot.summary().find("image/png") != std::string::npos);
std::filesystem::remove_all(home);
if (failures == 0) std::printf("termshot_test: all checks passed\n");
return failures == 0 ? 0 : 1;
}