@archastro/tui-shot 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/kitty-graphics.d.ts +45 -0
- package/dist/kitty-graphics.js +279 -0
- package/dist/pty-shot.js +25 -3
- package/dist/shot.d.ts +2 -0
- package/dist/shot.js +3 -1
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,3 +143,12 @@ await closeSharedBrowser();
|
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
Ink fixtures support Ink 7 and React 19 as peer dependencies.
|
|
146
|
+
|
|
147
|
+
## Graphics-aware PTY captures
|
|
148
|
+
|
|
149
|
+
Add `graphics: kitty` to a PTY fixture to emulate a terminal that supports the
|
|
150
|
+
Kitty graphics protocol. The harness answers the capability query and cell-size
|
|
151
|
+
reports, records transmitted images and their placements at the cursor, strips
|
|
152
|
+
the escape sequences from the text, and paints the pictures into the PNG in
|
|
153
|
+
cell units. `KittyGraphicsTracker` is exported for harnesses that drive a PTY
|
|
154
|
+
themselves.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { closeSharedBrowser, takeTuiShot } from "./shot.js";
|
|
2
2
|
export { takePtyShot } from "./pty-shot.js";
|
|
3
|
+
export { KittyGraphicsTracker, encodePng, overlaysToHtml } from "./kitty-graphics.js";
|
|
4
|
+
export type { GraphicsOverlay } from "./kitty-graphics.js";
|
|
3
5
|
export type { BatchEntry, BatchManifest, PtyAction, PtyKey, PtyShotFixture, PtyShotRequest, TuiShotFixture, TuiShotRequest, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { HeadlessTerminal } from "./terminal-html.js";
|
|
2
|
+
export interface GraphicsOverlay {
|
|
3
|
+
col: number;
|
|
4
|
+
row: number;
|
|
5
|
+
cols: number;
|
|
6
|
+
rows: number;
|
|
7
|
+
z: number;
|
|
8
|
+
/** PNG bytes as a data URL. */
|
|
9
|
+
dataUrl: string;
|
|
10
|
+
}
|
|
11
|
+
export interface KittyTrackerOptions {
|
|
12
|
+
terminal: HeadlessTerminal;
|
|
13
|
+
cols: number;
|
|
14
|
+
rows: number;
|
|
15
|
+
cellWidth: number;
|
|
16
|
+
cellHeight: number;
|
|
17
|
+
/** Where terminal replies (query answers, device attributes) are sent. */
|
|
18
|
+
reply: (data: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Encode raw RGB/RGBA pixels as a PNG (filter type 0 on every scanline). */
|
|
21
|
+
export declare function encodePng(pixels: Buffer, width: number, height: number, channels: 3 | 4): Buffer;
|
|
22
|
+
export declare class KittyGraphicsTracker {
|
|
23
|
+
private readonly options;
|
|
24
|
+
private readonly images;
|
|
25
|
+
private readonly placements;
|
|
26
|
+
private pending;
|
|
27
|
+
private carry;
|
|
28
|
+
private queryLog;
|
|
29
|
+
constructor(options: KittyTrackerOptions);
|
|
30
|
+
/** Queries the child sent, for assertions. */
|
|
31
|
+
get queries(): readonly string[];
|
|
32
|
+
get imageCount(): number;
|
|
33
|
+
/** Feed child output: text goes to the terminal, graphics are recorded. */
|
|
34
|
+
write(data: string): Promise<void>;
|
|
35
|
+
private writeText;
|
|
36
|
+
private handleCommand;
|
|
37
|
+
private storeImage;
|
|
38
|
+
private place;
|
|
39
|
+
private delete;
|
|
40
|
+
private pngFor;
|
|
41
|
+
/** Pictures currently visible, ordered for painting. */
|
|
42
|
+
overlays(): GraphicsOverlay[];
|
|
43
|
+
}
|
|
44
|
+
/** HTML for overlays, positioned in cell units so they track the font metrics. */
|
|
45
|
+
export declare function overlaysToHtml(overlays: GraphicsOverlay[], lineHeight: number): string;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kitty graphics protocol support for PTY captures.
|
|
3
|
+
*
|
|
4
|
+
* A headless xterm has no picture layer, so this tracker sits between the
|
|
5
|
+
* child's output and the terminal: it answers the capability queries a
|
|
6
|
+
* graphics-aware program sends, records every transmitted image and
|
|
7
|
+
* placement at the cursor position the terminal reports, strips the APC
|
|
8
|
+
* sequences from the text stream, and finally yields the pictures as
|
|
9
|
+
* absolutely positioned overlays for the HTML renderer.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import zlib from "node:zlib";
|
|
13
|
+
import { writeTerminal } from "./terminal-html.js";
|
|
14
|
+
const APC_START = "\x1b_G";
|
|
15
|
+
const ST = "\x1b\\";
|
|
16
|
+
function parseKeys(text) {
|
|
17
|
+
const keys = {};
|
|
18
|
+
for (const pair of text.split(",")) {
|
|
19
|
+
const equals = pair.indexOf("=");
|
|
20
|
+
if (equals > 0)
|
|
21
|
+
keys[pair.slice(0, equals)] = pair.slice(equals + 1);
|
|
22
|
+
}
|
|
23
|
+
return keys;
|
|
24
|
+
}
|
|
25
|
+
function crc32(buffer) {
|
|
26
|
+
let crc = ~0;
|
|
27
|
+
for (const byte of buffer) {
|
|
28
|
+
crc ^= byte;
|
|
29
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
30
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ~crc >>> 0;
|
|
34
|
+
}
|
|
35
|
+
function pngChunk(type, data) {
|
|
36
|
+
const length = Buffer.alloc(4);
|
|
37
|
+
length.writeUInt32BE(data.length);
|
|
38
|
+
const typed = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
39
|
+
const crc = Buffer.alloc(4);
|
|
40
|
+
crc.writeUInt32BE(crc32(typed));
|
|
41
|
+
return Buffer.concat([length, typed, crc]);
|
|
42
|
+
}
|
|
43
|
+
/** Encode raw RGB/RGBA pixels as a PNG (filter type 0 on every scanline). */
|
|
44
|
+
export function encodePng(pixels, width, height, channels) {
|
|
45
|
+
const stride = width * channels;
|
|
46
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
47
|
+
for (let y = 0; y < height; y += 1) {
|
|
48
|
+
raw[y * (stride + 1)] = 0;
|
|
49
|
+
pixels.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
|
|
50
|
+
}
|
|
51
|
+
const header = Buffer.alloc(13);
|
|
52
|
+
header.writeUInt32BE(width, 0);
|
|
53
|
+
header.writeUInt32BE(height, 4);
|
|
54
|
+
header[8] = 8;
|
|
55
|
+
header[9] = channels === 4 ? 6 : 2;
|
|
56
|
+
header[10] = 0;
|
|
57
|
+
header[11] = 0;
|
|
58
|
+
header[12] = 0;
|
|
59
|
+
return Buffer.concat([
|
|
60
|
+
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
61
|
+
pngChunk("IHDR", header),
|
|
62
|
+
pngChunk("IDAT", zlib.deflateSync(raw)),
|
|
63
|
+
pngChunk("IEND", Buffer.alloc(0)),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
export class KittyGraphicsTracker {
|
|
67
|
+
options;
|
|
68
|
+
images = new Map();
|
|
69
|
+
placements = new Map();
|
|
70
|
+
pending = null;
|
|
71
|
+
carry = "";
|
|
72
|
+
queryLog = [];
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.options = options;
|
|
75
|
+
}
|
|
76
|
+
/** Queries the child sent, for assertions. */
|
|
77
|
+
get queries() {
|
|
78
|
+
return this.queryLog;
|
|
79
|
+
}
|
|
80
|
+
get imageCount() {
|
|
81
|
+
return this.images.size;
|
|
82
|
+
}
|
|
83
|
+
/** Feed child output: text goes to the terminal, graphics are recorded. */
|
|
84
|
+
async write(data) {
|
|
85
|
+
let buffer = this.carry + data;
|
|
86
|
+
this.carry = "";
|
|
87
|
+
while (buffer.length > 0) {
|
|
88
|
+
const start = buffer.indexOf(APC_START);
|
|
89
|
+
if (start === -1) {
|
|
90
|
+
await this.writeText(buffer);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (start > 0)
|
|
94
|
+
await this.writeText(buffer.slice(0, start));
|
|
95
|
+
const end = buffer.indexOf(ST, start + APC_START.length);
|
|
96
|
+
if (end === -1) {
|
|
97
|
+
// Wait for the rest of the sequence.
|
|
98
|
+
this.carry = buffer.slice(start);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.handleCommand(buffer.slice(start + APC_START.length, end));
|
|
102
|
+
buffer = buffer.slice(end + ST.length);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async writeText(text) {
|
|
106
|
+
// Answer the size reports a graphics-aware program asks for. xterm itself
|
|
107
|
+
// replies to device attributes.
|
|
108
|
+
if (text.includes("\x1b[16t")) {
|
|
109
|
+
this.options.reply(`\x1b[6;${this.options.cellHeight};${this.options.cellWidth}t`);
|
|
110
|
+
}
|
|
111
|
+
if (text.includes("\x1b[14t")) {
|
|
112
|
+
this.options.reply(`\x1b[4;${this.options.cellHeight * this.options.rows};${this.options.cellWidth * this.options.cols}t`);
|
|
113
|
+
}
|
|
114
|
+
await writeTerminal(this.options.terminal, text);
|
|
115
|
+
}
|
|
116
|
+
handleCommand(body) {
|
|
117
|
+
const separator = body.indexOf(";");
|
|
118
|
+
const keys = parseKeys(separator === -1 ? body : body.slice(0, separator));
|
|
119
|
+
const payload = separator === -1 ? "" : body.slice(separator + 1);
|
|
120
|
+
const action = keys.a ?? (this.pending ? "t" : "t");
|
|
121
|
+
if (this.pending) {
|
|
122
|
+
this.pending.payload += payload;
|
|
123
|
+
if (keys.m !== "1") {
|
|
124
|
+
const complete = this.pending;
|
|
125
|
+
this.pending = null;
|
|
126
|
+
this.storeImage(complete.keys, complete.payload);
|
|
127
|
+
if (complete.keys.a === "T")
|
|
128
|
+
this.place(complete.keys);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
switch (action) {
|
|
133
|
+
case "q": {
|
|
134
|
+
this.queryLog.push(body);
|
|
135
|
+
const id = keys.i ?? "0";
|
|
136
|
+
const supported = keys.t === "d" || keys.t === "f";
|
|
137
|
+
this.options.reply(`\x1b_Gi=${id};${supported ? "OK" : "EINVAL:unsupported medium"}\x1b\\`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
case "t":
|
|
141
|
+
case "T": {
|
|
142
|
+
if (keys.m === "1") {
|
|
143
|
+
this.pending = { keys, payload };
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.storeImage(keys, payload);
|
|
147
|
+
if (action === "T")
|
|
148
|
+
this.place(keys);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case "p":
|
|
152
|
+
this.place(keys);
|
|
153
|
+
return;
|
|
154
|
+
case "d":
|
|
155
|
+
this.delete(keys);
|
|
156
|
+
return;
|
|
157
|
+
default:
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
storeImage(keys, payload) {
|
|
162
|
+
const id = Number(keys.i ?? 0);
|
|
163
|
+
this.images.set(id, {
|
|
164
|
+
id,
|
|
165
|
+
format: Number(keys.f ?? 32),
|
|
166
|
+
width: keys.s ? Number(keys.s) : undefined,
|
|
167
|
+
height: keys.v ? Number(keys.v) : undefined,
|
|
168
|
+
compressed: keys.o === "z",
|
|
169
|
+
payload,
|
|
170
|
+
medium: keys.t ?? "d",
|
|
171
|
+
});
|
|
172
|
+
if (keys.q !== "1" && keys.q !== "2") {
|
|
173
|
+
this.options.reply(`\x1b_Gi=${id};OK\x1b\\`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
place(keys) {
|
|
177
|
+
const imageId = Number(keys.i ?? 0);
|
|
178
|
+
if (!this.images.has(imageId))
|
|
179
|
+
return;
|
|
180
|
+
const buffer = this.options.terminal.buffer.active;
|
|
181
|
+
const placementId = Number(keys.p ?? 0);
|
|
182
|
+
const placement = {
|
|
183
|
+
imageId,
|
|
184
|
+
placementId,
|
|
185
|
+
col: buffer.cursorX,
|
|
186
|
+
row: buffer.cursorY,
|
|
187
|
+
cols: Number(keys.c ?? 0),
|
|
188
|
+
rows: Number(keys.r ?? 0),
|
|
189
|
+
z: Number(keys.z ?? 0),
|
|
190
|
+
};
|
|
191
|
+
this.placements.set(`${imageId}:${placementId}`, placement);
|
|
192
|
+
}
|
|
193
|
+
delete(keys) {
|
|
194
|
+
const mode = keys.d ?? "a";
|
|
195
|
+
const freeData = mode === mode.toUpperCase();
|
|
196
|
+
switch (mode.toLowerCase()) {
|
|
197
|
+
case "a":
|
|
198
|
+
this.placements.clear();
|
|
199
|
+
if (freeData)
|
|
200
|
+
this.images.clear();
|
|
201
|
+
return;
|
|
202
|
+
case "i": {
|
|
203
|
+
const imageId = Number(keys.i ?? 0);
|
|
204
|
+
const placementId = keys.p ? Number(keys.p) : null;
|
|
205
|
+
for (const [key, placement] of this.placements) {
|
|
206
|
+
if (placement.imageId !== imageId)
|
|
207
|
+
continue;
|
|
208
|
+
if (placementId !== null && placement.placementId !== placementId)
|
|
209
|
+
continue;
|
|
210
|
+
this.placements.delete(key);
|
|
211
|
+
}
|
|
212
|
+
if (freeData && placementId === null)
|
|
213
|
+
this.images.delete(imageId);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
default:
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
pngFor(image) {
|
|
221
|
+
if (image.png)
|
|
222
|
+
return image.png;
|
|
223
|
+
let bytes;
|
|
224
|
+
if (image.medium === "f") {
|
|
225
|
+
const filePath = Buffer.from(image.payload, "base64").toString("utf8");
|
|
226
|
+
try {
|
|
227
|
+
bytes = fs.readFileSync(filePath);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
bytes = Buffer.from(image.payload, "base64");
|
|
235
|
+
}
|
|
236
|
+
if (image.compressed)
|
|
237
|
+
bytes = zlib.inflateSync(bytes);
|
|
238
|
+
if (image.format === 100) {
|
|
239
|
+
image.png = bytes;
|
|
240
|
+
}
|
|
241
|
+
else if (image.width && image.height) {
|
|
242
|
+
image.png = encodePng(bytes, image.width, image.height, image.format === 24 ? 3 : 4);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return image.png;
|
|
248
|
+
}
|
|
249
|
+
/** Pictures currently visible, ordered for painting. */
|
|
250
|
+
overlays() {
|
|
251
|
+
const result = [];
|
|
252
|
+
for (const placement of this.placements.values()) {
|
|
253
|
+
const image = this.images.get(placement.imageId);
|
|
254
|
+
if (!image)
|
|
255
|
+
continue;
|
|
256
|
+
const png = this.pngFor(image);
|
|
257
|
+
if (!png)
|
|
258
|
+
continue;
|
|
259
|
+
if (placement.cols <= 0 || placement.rows <= 0)
|
|
260
|
+
continue;
|
|
261
|
+
result.push({
|
|
262
|
+
col: placement.col,
|
|
263
|
+
row: placement.row,
|
|
264
|
+
cols: placement.cols,
|
|
265
|
+
rows: placement.rows,
|
|
266
|
+
z: placement.z,
|
|
267
|
+
dataUrl: `data:image/png;base64,${png.toString("base64")}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return result.sort((a, b) => a.z - b.z);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** HTML for overlays, positioned in cell units so they track the font metrics. */
|
|
274
|
+
export function overlaysToHtml(overlays, lineHeight) {
|
|
275
|
+
return overlays
|
|
276
|
+
.map((overlay) => `<img class="tui-graphic" src="${overlay.dataUrl}" style="left:${overlay.col}ch;` +
|
|
277
|
+
`top:${overlay.row * lineHeight}em;width:${overlay.cols}ch;height:${overlay.rows * lineHeight}em" alt="" />`)
|
|
278
|
+
.join("");
|
|
279
|
+
}
|
package/dist/pty-shot.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import YAML from "yaml";
|
|
7
|
+
import { KittyGraphicsTracker, overlaysToHtml } from "./kitty-graphics.js";
|
|
7
8
|
import { captureTerminalHtml, queueTerminalShot, validPositive, } from "./shot.js";
|
|
8
9
|
import { createHeadlessTerminal, terminalPlainText, terminalToHtml, writeTerminal, } from "./terminal-html.js";
|
|
9
10
|
const KEYSTROKES = {
|
|
@@ -122,6 +123,9 @@ function loadPtyFixture(fixturePath) {
|
|
|
122
123
|
typeof value.allowNonZeroExit !== "boolean") {
|
|
123
124
|
throw fixtureError(absolute, "allowNonZeroExit must be a boolean");
|
|
124
125
|
}
|
|
126
|
+
if (value.graphics !== undefined && value.graphics !== "kitty") {
|
|
127
|
+
throw fixtureError(absolute, 'graphics must be "kitty" when set');
|
|
128
|
+
}
|
|
125
129
|
for (const field of ["background", "foreground", "fontFamily"]) {
|
|
126
130
|
if (value[field] !== undefined && typeof value[field] !== "string") {
|
|
127
131
|
throw fixtureError(absolute, `${field} must be a string`);
|
|
@@ -238,12 +242,18 @@ async function takeIsolatedPtyShot(request) {
|
|
|
238
242
|
throw new Error(`PTY fixture cwd is not a directory: ${cwd}`);
|
|
239
243
|
}
|
|
240
244
|
const terminal = createHeadlessTerminal(cols, rows);
|
|
245
|
+
const graphics = fixture.graphics === "kitty";
|
|
241
246
|
const childEnvironment = {
|
|
242
247
|
...process.env,
|
|
243
|
-
TERM: "xterm-256color",
|
|
248
|
+
TERM: graphics ? "xterm-kitty" : "xterm-256color",
|
|
244
249
|
COLORTERM: "truecolor",
|
|
245
250
|
...fixture.env,
|
|
246
251
|
};
|
|
252
|
+
// Cell metrics the emulated terminal reports; the HTML renderer positions
|
|
253
|
+
// overlays in ch/em units so the same ratios hold in the PNG.
|
|
254
|
+
const cellWidth = Math.max(1, Math.round(fontSize * 0.62));
|
|
255
|
+
const cellHeight = Math.max(1, Math.round(fontSize * lineHeight));
|
|
256
|
+
let tracker = null;
|
|
247
257
|
const command = resolvePtyCommand(fixture.command, cwd, childEnvironment);
|
|
248
258
|
const useExitWrapper = process.platform === "win32" ||
|
|
249
259
|
process.env.ASTROSHOT_TEST_FORCE_PTY_EXIT_WRAPPER === "1";
|
|
@@ -339,12 +349,23 @@ async function takeIsolatedPtyShot(request) {
|
|
|
339
349
|
throw new Error("PTY capture is unavailable because the optional node-pty native addon could not load on this platform. Reinstall @archastro/astroshot in a supported Node.js environment.", { cause: error });
|
|
340
350
|
}
|
|
341
351
|
child = spawnPty(spawnedCommand, spawnedArgs, {
|
|
342
|
-
name: "xterm-256color",
|
|
352
|
+
name: graphics ? "xterm-kitty" : "xterm-256color",
|
|
343
353
|
cols,
|
|
344
354
|
rows,
|
|
345
355
|
cwd,
|
|
346
356
|
env: childEnvironment,
|
|
347
357
|
});
|
|
358
|
+
if (graphics) {
|
|
359
|
+
const spawned = child;
|
|
360
|
+
const reply = (data) => {
|
|
361
|
+
if (!exited)
|
|
362
|
+
spawned.write(data);
|
|
363
|
+
};
|
|
364
|
+
tracker = new KittyGraphicsTracker({ terminal, cols, rows, cellWidth, cellHeight, reply });
|
|
365
|
+
// A real terminal answers device attributes and similar queries; forward
|
|
366
|
+
// xterm's replies so the program can finish its capability probe.
|
|
367
|
+
terminal.onData(reply);
|
|
368
|
+
}
|
|
348
369
|
child.onData((data) => {
|
|
349
370
|
if (useExitWrapper && wrappedExitCode === null) {
|
|
350
371
|
markerBuffer = (markerBuffer + data).slice(-4_096);
|
|
@@ -359,7 +380,7 @@ async function takeIsolatedPtyShot(request) {
|
|
|
359
380
|
}
|
|
360
381
|
}
|
|
361
382
|
}
|
|
362
|
-
writes = writes.then(() => writeTerminal(terminal, data));
|
|
383
|
+
writes = writes.then(() => tracker ? tracker.write(data) : writeTerminal(terminal, data));
|
|
363
384
|
});
|
|
364
385
|
child.onExit((event) => {
|
|
365
386
|
exited = event;
|
|
@@ -434,6 +455,7 @@ async function takeIsolatedPtyShot(request) {
|
|
|
434
455
|
});
|
|
435
456
|
return await captureTerminalHtml({
|
|
436
457
|
terminalRows,
|
|
458
|
+
overlays: tracker ? overlaysToHtml(tracker.overlays(), lineHeight) : "",
|
|
437
459
|
outPath: request.outPath,
|
|
438
460
|
headed: request.headed,
|
|
439
461
|
cols,
|
package/dist/shot.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare function validPositive(value: number, name: string, options: {
|
|
|
6
6
|
}): number;
|
|
7
7
|
export interface TerminalCaptureRequest {
|
|
8
8
|
terminalRows: string;
|
|
9
|
+
/** Absolutely positioned pictures painted over the rows (kitty graphics). */
|
|
10
|
+
overlays?: string;
|
|
9
11
|
outPath: string;
|
|
10
12
|
headed?: boolean;
|
|
11
13
|
cols: number;
|
package/dist/shot.js
CHANGED
|
@@ -133,15 +133,17 @@ export async function captureTerminalHtml(request) {
|
|
|
133
133
|
line-height: ${lineHeight};
|
|
134
134
|
text-rendering: geometricPrecision;
|
|
135
135
|
}
|
|
136
|
+
.tui-screen { position: relative; }
|
|
136
137
|
.tui-row {
|
|
137
138
|
height: ${lineHeight}em;
|
|
138
139
|
overflow: hidden;
|
|
139
140
|
white-space: pre;
|
|
140
141
|
}
|
|
142
|
+
.tui-graphic { position: absolute; display: block; object-fit: fill; }
|
|
141
143
|
</style>
|
|
142
144
|
</head>
|
|
143
145
|
<body>
|
|
144
|
-
<div data-tui-shot role="img" aria-label="Terminal screenshot">${terminalRows}</div>
|
|
146
|
+
<div data-tui-shot role="img" aria-label="Terminal screenshot"><div class="tui-screen">${terminalRows}${request.overlays ?? ""}</div></div>
|
|
145
147
|
</body>
|
|
146
148
|
</html>`, { waitUntil: "load" });
|
|
147
149
|
await page.locator("[data-tui-shot]").screenshot({
|
package/dist/types.d.ts
CHANGED
|
@@ -53,6 +53,12 @@ export interface PtyShotFixture {
|
|
|
53
53
|
settleMs?: number;
|
|
54
54
|
/** Permit a child that exits nonzero before capture. Defaults to false. */
|
|
55
55
|
allowNonZeroExit?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Emulate a graphics-capable terminal: answer the kitty graphics query and
|
|
58
|
+
* cell-size reports, record transmitted images, and paint them into the
|
|
59
|
+
* PNG at their placements.
|
|
60
|
+
*/
|
|
61
|
+
graphics?: "kitty";
|
|
56
62
|
actions?: PtyAction[];
|
|
57
63
|
expectText?: string[];
|
|
58
64
|
background?: string;
|