astroshot 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.
Files changed (99) hide show
  1. package/node_modules/@archastro/astroshot/README.md +4 -0
  2. package/node_modules/@archastro/astroshot/bin/astroshot.mjs +56 -1
  3. package/node_modules/@archastro/astroshot/bin/demo.mjs +72 -16
  4. package/node_modules/@archastro/astroshot/package.json +5 -4
  5. package/node_modules/@archastro/astroshot-review/LICENSE +21 -0
  6. package/node_modules/@archastro/astroshot-review/README.md +19 -0
  7. package/node_modules/@archastro/astroshot-review/bin/astroshot-review.mjs +17 -0
  8. package/node_modules/@archastro/astroshot-review/dist/cli.d.ts +12 -0
  9. package/node_modules/@archastro/astroshot-review/dist/cli.js +240 -0
  10. package/node_modules/@archastro/astroshot-review/dist/data/friction.d.ts +22 -0
  11. package/node_modules/@archastro/astroshot-review/dist/data/friction.js +278 -0
  12. package/node_modules/@archastro/astroshot-review/dist/data/hash-cache.d.ts +17 -0
  13. package/node_modules/@archastro/astroshot-review/dist/data/hash-cache.js +51 -0
  14. package/node_modules/@archastro/astroshot-review/dist/data/index-cache.d.ts +22 -0
  15. package/node_modules/@archastro/astroshot-review/dist/data/index-cache.js +64 -0
  16. package/node_modules/@archastro/astroshot-review/dist/data/manifest.d.ts +41 -0
  17. package/node_modules/@archastro/astroshot-review/dist/data/manifest.js +105 -0
  18. package/node_modules/@archastro/astroshot-review/dist/data/model.d.ts +96 -0
  19. package/node_modules/@archastro/astroshot-review/dist/data/model.js +1 -0
  20. package/node_modules/@archastro/astroshot-review/dist/data/paths.d.ts +27 -0
  21. package/node_modules/@archastro/astroshot-review/dist/data/paths.js +98 -0
  22. package/node_modules/@archastro/astroshot-review/dist/data/review-store.d.ts +67 -0
  23. package/node_modules/@archastro/astroshot-review/dist/data/review-store.js +237 -0
  24. package/node_modules/@archastro/astroshot-review/dist/data/scan.d.ts +32 -0
  25. package/node_modules/@archastro/astroshot-review/dist/data/scan.js +227 -0
  26. package/node_modules/@archastro/astroshot-review/dist/data/store.d.ts +92 -0
  27. package/node_modules/@archastro/astroshot-review/dist/data/store.js +408 -0
  28. package/node_modules/@archastro/astroshot-review/dist/data/watcher.d.ts +37 -0
  29. package/node_modules/@archastro/astroshot-review/dist/data/watcher.js +126 -0
  30. package/node_modules/@archastro/astroshot-review/dist/images/halfblocks.d.ts +10 -0
  31. package/node_modules/@archastro/astroshot-review/dist/images/halfblocks.js +21 -0
  32. package/node_modules/@archastro/astroshot-review/dist/images/png.d.ts +14 -0
  33. package/node_modules/@archastro/astroshot-review/dist/images/png.js +45 -0
  34. package/node_modules/@archastro/astroshot-review/dist/images/scale.d.ts +31 -0
  35. package/node_modules/@archastro/astroshot-review/dist/images/scale.js +102 -0
  36. package/node_modules/@archastro/astroshot-review/dist/images/service.d.ts +54 -0
  37. package/node_modules/@archastro/astroshot-review/dist/images/service.js +163 -0
  38. package/node_modules/@archastro/astroshot-review/dist/images/worker.d.ts +21 -0
  39. package/node_modules/@archastro/astroshot-review/dist/images/worker.js +30 -0
  40. package/node_modules/@archastro/astroshot-review/dist/index.d.ts +12 -0
  41. package/node_modules/@archastro/astroshot-review/dist/index.js +9 -0
  42. package/node_modules/@archastro/astroshot-review/dist/terminal/graphics-stdout.d.ts +9 -0
  43. package/node_modules/@archastro/astroshot-review/dist/terminal/graphics-stdout.js +36 -0
  44. package/node_modules/@archastro/astroshot-review/dist/terminal/herdr.d.ts +61 -0
  45. package/node_modules/@archastro/astroshot-review/dist/terminal/herdr.js +327 -0
  46. package/node_modules/@archastro/astroshot-review/dist/terminal/image-layer.d.ts +122 -0
  47. package/node_modules/@archastro/astroshot-review/dist/terminal/image-layer.js +471 -0
  48. package/node_modules/@archastro/astroshot-review/dist/terminal/kitty.d.ts +74 -0
  49. package/node_modules/@archastro/astroshot-review/dist/terminal/kitty.js +112 -0
  50. package/node_modules/@archastro/astroshot-review/dist/terminal/probe.d.ts +49 -0
  51. package/node_modules/@archastro/astroshot-review/dist/terminal/probe.js +206 -0
  52. package/node_modules/@archastro/astroshot-review/dist/ui/app.d.ts +6 -0
  53. package/node_modules/@archastro/astroshot-review/dist/ui/app.js +695 -0
  54. package/node_modules/@archastro/astroshot-review/dist/ui/chrome.d.ts +53 -0
  55. package/node_modules/@archastro/astroshot-review/dist/ui/chrome.js +69 -0
  56. package/node_modules/@archastro/astroshot-review/dist/ui/context.d.ts +16 -0
  57. package/node_modules/@archastro/astroshot-review/dist/ui/context.js +8 -0
  58. package/node_modules/@archastro/astroshot-review/dist/ui/detail.d.ts +33 -0
  59. package/node_modules/@archastro/astroshot-review/dist/ui/detail.js +39 -0
  60. package/node_modules/@archastro/astroshot-review/dist/ui/friction.d.ts +42 -0
  61. package/node_modules/@archastro/astroshot-review/dist/ui/friction.js +84 -0
  62. package/node_modules/@archastro/astroshot-review/dist/ui/help.d.ts +4 -0
  63. package/node_modules/@archastro/astroshot-review/dist/ui/help.js +61 -0
  64. package/node_modules/@archastro/astroshot-review/dist/ui/hooks.d.ts +9 -0
  65. package/node_modules/@archastro/astroshot-review/dist/ui/hooks.js +32 -0
  66. package/node_modules/@archastro/astroshot-review/dist/ui/movie-player.d.ts +29 -0
  67. package/node_modules/@archastro/astroshot-review/dist/ui/movie-player.js +119 -0
  68. package/node_modules/@archastro/astroshot-review/dist/ui/picture.d.ts +19 -0
  69. package/node_modules/@archastro/astroshot-review/dist/ui/picture.js +100 -0
  70. package/node_modules/@archastro/astroshot-review/dist/ui/selectors.d.ts +26 -0
  71. package/node_modules/@archastro/astroshot-review/dist/ui/selectors.js +69 -0
  72. package/node_modules/@archastro/astroshot-review/dist/ui/settings.d.ts +5 -0
  73. package/node_modules/@archastro/astroshot-review/dist/ui/settings.js +17 -0
  74. package/node_modules/@archastro/astroshot-review/dist/ui/stream.d.ts +38 -0
  75. package/node_modules/@archastro/astroshot-review/dist/ui/stream.js +118 -0
  76. package/node_modules/@archastro/astroshot-review/dist/ui/system.d.ts +4 -0
  77. package/node_modules/@archastro/astroshot-review/dist/ui/system.js +34 -0
  78. package/node_modules/@archastro/astroshot-review/dist/ui/takeover.d.ts +26 -0
  79. package/node_modules/@archastro/astroshot-review/dist/ui/takeover.js +29 -0
  80. package/node_modules/@archastro/astroshot-review/dist/ui/text-input.d.ts +8 -0
  81. package/node_modules/@archastro/astroshot-review/dist/ui/text-input.js +74 -0
  82. package/node_modules/@archastro/astroshot-review/dist/ui/theme.d.ts +21 -0
  83. package/node_modules/@archastro/astroshot-review/dist/ui/theme.js +63 -0
  84. package/node_modules/@archastro/astroshot-review/dist/video/ffmpeg.d.ts +54 -0
  85. package/node_modules/@archastro/astroshot-review/dist/video/ffmpeg.js +206 -0
  86. package/node_modules/@archastro/astroshot-review/package.json +71 -0
  87. package/node_modules/@archastro/movie-harness/package.json +1 -1
  88. package/node_modules/@archastro/react-shot/package.json +4 -4
  89. package/node_modules/@archastro/tui-shot/README.md +9 -0
  90. package/node_modules/@archastro/tui-shot/dist/index.d.ts +2 -0
  91. package/node_modules/@archastro/tui-shot/dist/index.js +1 -0
  92. package/node_modules/@archastro/tui-shot/dist/kitty-graphics.d.ts +45 -0
  93. package/node_modules/@archastro/tui-shot/dist/kitty-graphics.js +279 -0
  94. package/node_modules/@archastro/tui-shot/dist/pty-shot.js +25 -3
  95. package/node_modules/@archastro/tui-shot/dist/shot.d.ts +2 -0
  96. package/node_modules/@archastro/tui-shot/dist/shot.js +3 -1
  97. package/node_modules/@archastro/tui-shot/dist/types.d.ts +6 -0
  98. package/node_modules/@archastro/tui-shot/package.json +1 -1
  99. package/package.json +10 -5
@@ -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
+ }
@@ -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,
@@ -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;
@@ -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({
@@ -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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archastro/tui-shot",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Deterministic PNG screenshots of Ink fixtures and arbitrary PTY programs",
5
5
  "keywords": [
6
6
  "ink",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astroshot",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "One CLI for deterministic React, terminal, and movie UI captures — installable with a plain npx astroshot",
5
5
  "keywords": [
6
6
  "react",
@@ -45,13 +45,17 @@
45
45
  "registry": "https://registry.npmjs.org/"
46
46
  },
47
47
  "dependencies": {
48
- "@archastro/astroshot": "0.2.1",
49
- "@archastro/movie-harness": "0.2.1",
50
- "@archastro/react-shot": "0.2.1",
51
- "@archastro/tui-shot": "0.2.1",
48
+ "@archastro/astroshot": "0.2.2",
49
+ "@archastro/astroshot-review": "0.2.2",
50
+ "@archastro/movie-harness": "0.2.2",
51
+ "@archastro/react-shot": "0.2.2",
52
+ "@archastro/tui-shot": "0.2.2",
52
53
  "@vitejs/plugin-react": "^5.2.0",
53
54
  "@xterm/headless": "^5.5.0",
55
+ "ink": "^7.1.0",
54
56
  "playwright": "1.61.1",
57
+ "pngjs": "^7.0.0",
58
+ "react": "^19.0.0",
55
59
  "tsx": "^4.20.0",
56
60
  "vite": "^6.2.0",
57
61
  "yaml": "^2.8.0"
@@ -61,6 +65,7 @@
61
65
  },
62
66
  "bundleDependencies": [
63
67
  "@archastro/astroshot",
68
+ "@archastro/astroshot-review",
64
69
  "@archastro/movie-harness",
65
70
  "@archastro/react-shot",
66
71
  "@archastro/tui-shot"