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>
123 lines
6.1 KiB
C++
123 lines
6.1 KiB
C++
// 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;
|
||
}
|