import assert from "node:assert/strict"; import { test } from "node:test"; import { imageDimensions } from "../src/dimensions.ts"; import { parseCellSizeReport } from "../src/cell-size.ts"; import { deleteVirtual, fitToGrid, placeholderRows, transmitVirtual, } from "../src/placeholder.ts"; import { PLACEHOLDER, ROWCOLUMN_DIACRITICS } from "../src/diacritics.ts"; const CELL = { widthPx: 10, heightPx: 21 }; // Measured on Ghostty 1.3.1. test("placeholder rows are plain text, not a graphics escape", () => { // This is the whole anti-flicker contract: pi-tui's isImageLine() looks for // ESC_G and switches such lines onto the reserved-row path that can bail to // a full-screen redraw. Placeholder rows must never trip it. const rows = placeholderRows(42, { cols: 8, rows: 3 }); for (const row of rows) { assert.ok(!row.includes("\x1b_G"), "placeholder row must not contain a graphics escape"); assert.ok(!row.includes("\x1b]1337"), "placeholder row must not contain an iTerm escape"); } }); test("each placeholder row occupies exactly `cols` display cells", () => { // Diacritics are zero-width and combine with the placeholder, so a row of N // cells must measure N. If this drifts, pi-tui throws "line exceeds width". const grid = { cols: 12, rows: 4 }; const rows = placeholderRows(7, grid); assert.equal(rows.length, grid.rows); for (const row of rows) { const stripped = row.replace(/\x1b\[[0-9;]*m/g, ""); const cells = [...stripped].filter((ch) => ch === PLACEHOLDER).length; assert.equal(cells, grid.cols); } }); test("row and column are encoded with the right diacritics", () => { const rows = placeholderRows(1, { cols: 2, rows: 2 }); const stripped = rows[1]!.replace(/\x1b\[[0-9;]*m/g, ""); const codepoints = [...stripped].map((ch) => ch.codePointAt(0)!); // row 1, col 0 -> placeholder, diacritic[1], diacritic[0] assert.equal(codepoints[0], 0x10eeee); assert.equal(codepoints[1], ROWCOLUMN_DIACRITICS[1]); assert.equal(codepoints[2], ROWCOLUMN_DIACRITICS[0]); // row 1, col 1 assert.equal(codepoints[4], ROWCOLUMN_DIACRITICS[1]); assert.equal(codepoints[5], ROWCOLUMN_DIACRITICS[1]); }); test("image id above 24 bits gets the high-byte diacritic", () => { const id = 42 + (2 << 24); // 33554474, the spec's own example. const stripped = placeholderRows(id, { cols: 1, rows: 1 })[0]!.replace(/\x1b\[[0-9;]*m/g, ""); const codepoints = [...stripped].map((ch) => ch.codePointAt(0)!); assert.equal(codepoints.length, 4, "expected placeholder + row + col + high byte"); assert.equal(codepoints[3], ROWCOLUMN_DIACRITICS[2]); // Low 24 bits ride in the foreground colour. assert.ok(placeholderRows(id, { cols: 1, rows: 1 })[0]!.startsWith("\x1b[38;2;0;0;42m")); }); test("transmit uses a virtual placement and stays silent", () => { const escape = transmitVirtual("AAAA", 5, { cols: 2, rows: 2 }); assert.ok(escape.includes("U=1"), "must be a virtual placement or nothing renders"); assert.ok(escape.includes("q=2"), "a reply would be parsed as typed input"); assert.ok(escape.includes("i=5")); assert.ok(escape.includes("c=2,r=2")); }); test("large payloads are chunked with correct continuation flags", () => { const data = "x".repeat(10_000); // > 2 chunks of 4096. const escape = transmitVirtual(data, 1, { cols: 4, rows: 4 }); const chunks = escape.split("\x1b\\").filter(Boolean); assert.equal(chunks.length, 3); assert.ok(chunks[0]!.includes("m=1"), "first chunk must signal more data"); assert.ok(chunks[1]!.startsWith("\x1b_Gm=1;"), "middle chunk carries no control keys"); assert.ok(chunks[2]!.startsWith("\x1b_Gm=0;"), "last chunk must terminate the transmission"); // The payload must survive chunking byte for byte. const rejoined = chunks.map((c) => c.slice(c.indexOf(";") + 1)).join(""); assert.equal(rejoined, data); }); test("fitToGrid preserves aspect ratio within the cell budget", () => { // A 200x210px image in 10x21px cells is exactly 20x10 cells. const grid = fitToGrid(200, 210, CELL, 80, 40); assert.deepEqual(grid, { cols: 20, rows: 10 }); }); test("fitToGrid clamps to the column budget and keeps the shape", () => { const grid = fitToGrid(4000, 2000, CELL, 40, 30); assert.equal(grid.cols, 40); // 4000x2000 scaled to 400px wide -> 200px tall -> ~10 rows of 21px. assert.equal(grid.rows, 10); }); test("fitToGrid clamps to the row budget for tall images", () => { const grid = fitToGrid(500, 5000, CELL, 80, 12); assert.ok(grid.rows <= 12, `rows ${grid.rows} must respect the budget`); assert.ok(grid.cols >= 1); }); test("fitToGrid never returns a zero dimension", () => { for (const [w, h] of [ [1, 1], [1, 10_000], [10_000, 1], ]) { const grid = fitToGrid(w!, h!, CELL, 80, 30); assert.ok(grid.cols >= 1 && grid.rows >= 1, `bad grid for ${w}x${h}`); } }); test("grids beyond the diacritic table are rejected, not silently wrong", () => { assert.throws(() => placeholderRows(1, { cols: 400, rows: 1 }), RangeError); assert.throws(() => placeholderRows(1, { cols: 1, rows: 400 }), RangeError); }); test("delete frees the image data, not just the placement", () => { // Lowercase d=i would leave the payload in terminal memory for the session. assert.ok(deleteVirtual(9).includes("d=I")); assert.ok(deleteVirtual(9).includes("i=9")); }); test("cell size report parses the real Ghostty reply", () => { assert.deepEqual(parseCellSizeReport("\x1b[6;21;10t"), { widthPx: 10, heightPx: 21 }); }); test("cell size report rejects junk and empty replies", () => { // Herdr panes answer nothing; that must not become a 0px cell. assert.equal(parseCellSizeReport(""), null); assert.equal(parseCellSizeReport("\x1b[6;0;0t"), null); assert.equal(parseCellSizeReport("garbage"), null); }); test("PNG dimensions are read from the IHDR header", () => { // 8x8 red PNG, generated by zlib+crc32 the same way the probe did. const png = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4/5/h/1+GAAAI/AL+Sy9CAAAAAElFTkSuQmCC", "base64", ); assert.deepEqual(imageDimensions(png), { widthPx: 8, heightPx: 8 }); }); test("unparseable bytes fall back instead of throwing", () => { const dims = imageDimensions(Buffer.from("not an image")); assert.ok(dims.widthPx > 0 && dims.heightPx > 0); });