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.
- package/node_modules/@archastro/astroshot/README.md +4 -0
- package/node_modules/@archastro/astroshot/bin/astroshot.mjs +56 -1
- package/node_modules/@archastro/astroshot/bin/demo.mjs +72 -16
- package/node_modules/@archastro/astroshot/package.json +5 -4
- package/node_modules/@archastro/astroshot-review/LICENSE +21 -0
- package/node_modules/@archastro/astroshot-review/README.md +19 -0
- package/node_modules/@archastro/astroshot-review/bin/astroshot-review.mjs +17 -0
- package/node_modules/@archastro/astroshot-review/dist/cli.d.ts +12 -0
- package/node_modules/@archastro/astroshot-review/dist/cli.js +240 -0
- package/node_modules/@archastro/astroshot-review/dist/data/friction.d.ts +22 -0
- package/node_modules/@archastro/astroshot-review/dist/data/friction.js +278 -0
- package/node_modules/@archastro/astroshot-review/dist/data/hash-cache.d.ts +17 -0
- package/node_modules/@archastro/astroshot-review/dist/data/hash-cache.js +51 -0
- package/node_modules/@archastro/astroshot-review/dist/data/index-cache.d.ts +22 -0
- package/node_modules/@archastro/astroshot-review/dist/data/index-cache.js +64 -0
- package/node_modules/@archastro/astroshot-review/dist/data/manifest.d.ts +41 -0
- package/node_modules/@archastro/astroshot-review/dist/data/manifest.js +105 -0
- package/node_modules/@archastro/astroshot-review/dist/data/model.d.ts +96 -0
- package/node_modules/@archastro/astroshot-review/dist/data/model.js +1 -0
- package/node_modules/@archastro/astroshot-review/dist/data/paths.d.ts +27 -0
- package/node_modules/@archastro/astroshot-review/dist/data/paths.js +98 -0
- package/node_modules/@archastro/astroshot-review/dist/data/review-store.d.ts +67 -0
- package/node_modules/@archastro/astroshot-review/dist/data/review-store.js +237 -0
- package/node_modules/@archastro/astroshot-review/dist/data/scan.d.ts +32 -0
- package/node_modules/@archastro/astroshot-review/dist/data/scan.js +227 -0
- package/node_modules/@archastro/astroshot-review/dist/data/store.d.ts +92 -0
- package/node_modules/@archastro/astroshot-review/dist/data/store.js +408 -0
- package/node_modules/@archastro/astroshot-review/dist/data/watcher.d.ts +37 -0
- package/node_modules/@archastro/astroshot-review/dist/data/watcher.js +126 -0
- package/node_modules/@archastro/astroshot-review/dist/images/halfblocks.d.ts +10 -0
- package/node_modules/@archastro/astroshot-review/dist/images/halfblocks.js +21 -0
- package/node_modules/@archastro/astroshot-review/dist/images/png.d.ts +14 -0
- package/node_modules/@archastro/astroshot-review/dist/images/png.js +45 -0
- package/node_modules/@archastro/astroshot-review/dist/images/scale.d.ts +31 -0
- package/node_modules/@archastro/astroshot-review/dist/images/scale.js +102 -0
- package/node_modules/@archastro/astroshot-review/dist/images/service.d.ts +54 -0
- package/node_modules/@archastro/astroshot-review/dist/images/service.js +163 -0
- package/node_modules/@archastro/astroshot-review/dist/images/worker.d.ts +21 -0
- package/node_modules/@archastro/astroshot-review/dist/images/worker.js +30 -0
- package/node_modules/@archastro/astroshot-review/dist/index.d.ts +12 -0
- package/node_modules/@archastro/astroshot-review/dist/index.js +9 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/graphics-stdout.d.ts +9 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/graphics-stdout.js +36 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/herdr.d.ts +61 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/herdr.js +327 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/image-layer.d.ts +122 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/image-layer.js +471 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/kitty.d.ts +74 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/kitty.js +112 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/probe.d.ts +49 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/probe.js +206 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/app.d.ts +6 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/app.js +695 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/chrome.d.ts +53 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/chrome.js +69 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/context.d.ts +16 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/context.js +8 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/detail.d.ts +33 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/detail.js +39 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/friction.d.ts +42 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/friction.js +84 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/help.d.ts +4 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/help.js +61 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/hooks.d.ts +9 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/hooks.js +32 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/movie-player.d.ts +29 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/movie-player.js +119 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/picture.d.ts +19 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/picture.js +100 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/selectors.d.ts +26 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/selectors.js +69 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/settings.d.ts +5 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/settings.js +17 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/stream.d.ts +38 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/stream.js +118 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/system.d.ts +4 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/system.js +34 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/takeover.d.ts +26 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/takeover.js +29 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/text-input.d.ts +8 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/text-input.js +74 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/theme.d.ts +21 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/theme.js +63 -0
- package/node_modules/@archastro/astroshot-review/dist/video/ffmpeg.d.ts +54 -0
- package/node_modules/@archastro/astroshot-review/dist/video/ffmpeg.js +206 -0
- package/node_modules/@archastro/astroshot-review/package.json +71 -0
- package/node_modules/@archastro/movie-harness/package.json +1 -1
- package/node_modules/@archastro/react-shot/package.json +4 -4
- package/node_modules/@archastro/tui-shot/README.md +9 -0
- package/node_modules/@archastro/tui-shot/dist/index.d.ts +2 -0
- package/node_modules/@archastro/tui-shot/dist/index.js +1 -0
- package/node_modules/@archastro/tui-shot/dist/kitty-graphics.d.ts +45 -0
- package/node_modules/@archastro/tui-shot/dist/kitty-graphics.js +279 -0
- package/node_modules/@archastro/tui-shot/dist/pty-shot.js +25 -3
- package/node_modules/@archastro/tui-shot/dist/shot.d.ts +2 -0
- package/node_modules/@archastro/tui-shot/dist/shot.js +3 -1
- package/node_modules/@archastro/tui-shot/dist/types.d.ts +6 -0
- package/node_modules/@archastro/tui-shot/package.json +1 -1
- package/package.json +10 -5
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* herdr pane-graphics transport.
|
|
3
|
+
*
|
|
4
|
+
* herdr (an agent terminal multiplexer) emulates the terminal itself and does
|
|
5
|
+
* not forward a program's raw Kitty escapes. Instead it exposes a unix-socket
|
|
6
|
+
* JSON API — `pane.graphics.set` / `pane.graphics.clear` place and remove
|
|
7
|
+
* images on named layers composited over the pane's text. This module speaks
|
|
8
|
+
* that API so the review tray shows pixel-perfect images inside herdr.
|
|
9
|
+
*
|
|
10
|
+
* Enabling it needs `[experimental] kitty_graphics = true` in herdr's
|
|
11
|
+
* config.toml AND a client reattach: herdr 0.8.2 latches the client's graphics
|
|
12
|
+
* setting at startup, so a client started with it off reports the host cell
|
|
13
|
+
* size as unavailable until it is detached and reattached once.
|
|
14
|
+
*
|
|
15
|
+
* Wire format: newline-delimited JSON over the socket named by
|
|
16
|
+
* HERDR_SOCKET_PATH, targeting HERDR_PANE_ID.
|
|
17
|
+
*/
|
|
18
|
+
import { createConnection } from "node:net";
|
|
19
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
20
|
+
export function herdrAddress(env = process.env) {
|
|
21
|
+
if (env.HERDR_ENV !== "1")
|
|
22
|
+
return null;
|
|
23
|
+
if (!env.HERDR_SOCKET_PATH || !env.HERDR_PANE_ID)
|
|
24
|
+
return null;
|
|
25
|
+
return { socket: env.HERDR_SOCKET_PATH, pane: env.HERDR_PANE_ID };
|
|
26
|
+
}
|
|
27
|
+
/** A short-lived request/response over the socket. */
|
|
28
|
+
function request(socket, method, params) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const client = createConnection(socket);
|
|
31
|
+
let text = "";
|
|
32
|
+
const fail = (error) => {
|
|
33
|
+
client.removeAllListeners();
|
|
34
|
+
client.destroy();
|
|
35
|
+
reject(error);
|
|
36
|
+
};
|
|
37
|
+
client.on("connect", () => client.write(JSON.stringify({ id: "astroshot-review", method, params }) + "\n"));
|
|
38
|
+
client.on("data", (chunk) => {
|
|
39
|
+
text += chunk.toString();
|
|
40
|
+
const end = text.indexOf("\n");
|
|
41
|
+
if (end < 0) {
|
|
42
|
+
if (text.length > 1024 * 1024)
|
|
43
|
+
fail(new Error("herdr response too large"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const reply = JSON.parse(text.slice(0, end));
|
|
48
|
+
client.removeAllListeners();
|
|
49
|
+
client.destroy();
|
|
50
|
+
resolve(reply);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
fail(new Error("invalid herdr response"));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
client.on("error", fail);
|
|
57
|
+
client.setTimeout(2500, () => fail(new Error("herdr request timed out")));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// 1x1 transparent PNG, for probing whether pane.graphics.set is accepted.
|
|
61
|
+
const PROBE_PNG = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64");
|
|
62
|
+
/**
|
|
63
|
+
* Confirm herdr accepts a `pane.graphics.stream` layer. A missing ack is
|
|
64
|
+
* treated as success — the open reply may be silent — so only an explicit
|
|
65
|
+
* error reply rejects it. The probe stream is closed immediately, which makes
|
|
66
|
+
* herdr drop its layer.
|
|
67
|
+
*/
|
|
68
|
+
export async function probeHerdrSet(address) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const client = createConnection(address.socket);
|
|
71
|
+
let text = "";
|
|
72
|
+
let settled = false;
|
|
73
|
+
const finish = (result) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
client.destroy();
|
|
78
|
+
resolve(result);
|
|
79
|
+
};
|
|
80
|
+
client.on("connect", () => client.write(JSON.stringify({ id: "astroshot-review", method: "pane.graphics.stream", params: { pane_id: address.pane, layer_id: "astroshot-review-probe", z_index: -1 } }) + "\n"));
|
|
81
|
+
client.on("data", (chunk) => {
|
|
82
|
+
text += chunk.toString();
|
|
83
|
+
const end = text.indexOf("\n");
|
|
84
|
+
if (end < 0)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
const reply = JSON.parse(text.slice(0, end));
|
|
88
|
+
finish(reply.error ? { ok: false, reason: reasonFor(reply) } : { ok: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
finish({ ok: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
client.on("error", () => finish({ ok: true }));
|
|
95
|
+
setTimeout(() => finish({ ok: true }), 600);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function reasonFor(reply) {
|
|
99
|
+
if (reply.error?.code === "feature_disabled") {
|
|
100
|
+
return "herdr image rendering is off — set [experimental] kitty_graphics = true in ~/.config/herdr/config.toml, run `herdr server reload-config`, then reattach the herdr client";
|
|
101
|
+
}
|
|
102
|
+
if (reply.error?.code === "cell_size_unavailable") {
|
|
103
|
+
return "herdr hasn't reported pixel size — detach and reattach the herdr client once (its graphics setting latches at startup)";
|
|
104
|
+
}
|
|
105
|
+
if (reply.error)
|
|
106
|
+
return `herdr graphics unavailable (${reply.error.code})`;
|
|
107
|
+
return "herdr returned no pixel dimensions for this pane";
|
|
108
|
+
}
|
|
109
|
+
/** Poll `pane.graphics.info` until herdr reports the cell size, or give up. */
|
|
110
|
+
export async function discoverHerdr(address, options = {}) {
|
|
111
|
+
const deadline = Date.now() + (options.timeoutMs ?? 5000);
|
|
112
|
+
let announced = false;
|
|
113
|
+
for (;;) {
|
|
114
|
+
let reply;
|
|
115
|
+
try {
|
|
116
|
+
reply = await request(address.socket, "pane.graphics.info", { pane_id: address.pane });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
120
|
+
}
|
|
121
|
+
if (reply.error?.code === "cell_size_unavailable" && Date.now() < deadline) {
|
|
122
|
+
if (!announced) {
|
|
123
|
+
announced = true;
|
|
124
|
+
options.onWaiting?.();
|
|
125
|
+
}
|
|
126
|
+
await delay(Math.min(options.retryMs ?? 150, Math.max(1, deadline - Date.now())));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (reply.error || !reply.result?.cell_width_px || !reply.result?.cell_height_px) {
|
|
130
|
+
return { ok: false, reason: reasonFor(reply) };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
cellWidth: reply.result.cell_width_px,
|
|
135
|
+
cellHeight: reply.result.cell_height_px,
|
|
136
|
+
maxLayers: reply.result.max_layers_per_pane || 16,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Extract PNG pixel dimensions from the IHDR chunk. */
|
|
141
|
+
function pngSize(png) {
|
|
142
|
+
return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Places images on herdr via one `pane.graphics.stream` connection PER layer.
|
|
146
|
+
* herdr removes a stream's layer the moment its socket closes, so closing a
|
|
147
|
+
* layer's connection — or the whole process dying — cleans up automatically,
|
|
148
|
+
* with no persistent server-side state to leak (the trap of pane.graphics.set).
|
|
149
|
+
*/
|
|
150
|
+
export class HerdrSink {
|
|
151
|
+
address;
|
|
152
|
+
onError;
|
|
153
|
+
layers = new Map();
|
|
154
|
+
/** Every socket opened and not yet closed, whether or not its layer is still mapped. */
|
|
155
|
+
connections = new Set();
|
|
156
|
+
disposed = false;
|
|
157
|
+
generationCount = 0;
|
|
158
|
+
constructor(address, onError) {
|
|
159
|
+
this.address = address;
|
|
160
|
+
this.onError = onError;
|
|
161
|
+
}
|
|
162
|
+
/** Bumps when a layer stream drops unexpectedly, so callers re-send. */
|
|
163
|
+
get generation() {
|
|
164
|
+
return this.generationCount;
|
|
165
|
+
}
|
|
166
|
+
set(layerId, png, _imageWidth, _imageHeight, placement) {
|
|
167
|
+
if (this.disposed)
|
|
168
|
+
return;
|
|
169
|
+
let layer = this.layers.get(layerId);
|
|
170
|
+
if (!layer) {
|
|
171
|
+
layer = { socket: null, opening: false, ready: false, z: placement.z, pending: null, draining: false };
|
|
172
|
+
this.layers.set(layerId, layer);
|
|
173
|
+
}
|
|
174
|
+
layer.pending = { png, placement };
|
|
175
|
+
if (layer.ready && !layer.draining) {
|
|
176
|
+
this.flush(layerId, layer);
|
|
177
|
+
}
|
|
178
|
+
else if (!layer.opening && !layer.socket) {
|
|
179
|
+
this.open(layerId, layer);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
clear(layerId) {
|
|
183
|
+
const layer = this.layers.get(layerId);
|
|
184
|
+
if (!layer)
|
|
185
|
+
return;
|
|
186
|
+
this.layers.delete(layerId);
|
|
187
|
+
layer.pending = null;
|
|
188
|
+
layer.opening = false;
|
|
189
|
+
layer.ready = false;
|
|
190
|
+
const socket = layer.socket;
|
|
191
|
+
layer.socket = null;
|
|
192
|
+
// Closing the connection makes herdr drop this layer. This must happen
|
|
193
|
+
// even mid-handshake: a connection that outlives its layer keeps a ghost
|
|
194
|
+
// layer in herdr and keeps this process alive after the tray quits.
|
|
195
|
+
if (socket)
|
|
196
|
+
this.close(socket);
|
|
197
|
+
}
|
|
198
|
+
close(socket) {
|
|
199
|
+
this.connections.delete(socket);
|
|
200
|
+
try {
|
|
201
|
+
socket.destroy();
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// already gone
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
clearAll() {
|
|
208
|
+
for (const layerId of [...this.layers.keys()])
|
|
209
|
+
this.clear(layerId);
|
|
210
|
+
}
|
|
211
|
+
open(layerId, layer) {
|
|
212
|
+
const client = createConnection(this.address.socket);
|
|
213
|
+
// The layer owns its socket from the first moment, not from the open ack:
|
|
214
|
+
// clear() and dispose() close whatever is here, handshake or not.
|
|
215
|
+
layer.opening = true;
|
|
216
|
+
layer.ready = false;
|
|
217
|
+
layer.socket = client;
|
|
218
|
+
this.connections.add(client);
|
|
219
|
+
let text = "";
|
|
220
|
+
let opened = false;
|
|
221
|
+
// Still the mapped layer's current socket, on a live sink.
|
|
222
|
+
const current = () => !this.disposed && this.layers.get(layerId) === layer && layer.socket === client;
|
|
223
|
+
const markOpen = () => {
|
|
224
|
+
if (opened)
|
|
225
|
+
return;
|
|
226
|
+
opened = true;
|
|
227
|
+
if (!current()) {
|
|
228
|
+
this.close(client);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
layer.opening = false;
|
|
232
|
+
layer.ready = true;
|
|
233
|
+
this.flush(layerId, layer);
|
|
234
|
+
};
|
|
235
|
+
client.on("connect", () => {
|
|
236
|
+
client.write(JSON.stringify({ id: "astroshot-review", method: "pane.graphics.stream", params: { pane_id: this.address.pane, layer_id: layerId, z_index: layer.z } }) + "\n");
|
|
237
|
+
});
|
|
238
|
+
client.on("data", (chunk) => {
|
|
239
|
+
text += chunk.toString();
|
|
240
|
+
let end;
|
|
241
|
+
while ((end = text.indexOf("\n")) >= 0) {
|
|
242
|
+
const line = text.slice(0, end);
|
|
243
|
+
text = text.slice(end + 1);
|
|
244
|
+
let reply;
|
|
245
|
+
try {
|
|
246
|
+
reply = JSON.parse(line);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (reply.error) {
|
|
252
|
+
this.onError(new Error(`herdr stream ${layerId}: ${reply.error.code}`));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
markOpen();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const drop = (reason) => {
|
|
259
|
+
clearTimeout(grace);
|
|
260
|
+
this.connections.delete(client);
|
|
261
|
+
// A socket that clear() already detached says nothing about the layer.
|
|
262
|
+
if (layer.socket !== client)
|
|
263
|
+
return;
|
|
264
|
+
layer.socket = null;
|
|
265
|
+
layer.ready = false;
|
|
266
|
+
layer.opening = false;
|
|
267
|
+
// Unexpected drop (not from clear/dispose): let callers re-send everything.
|
|
268
|
+
if (this.layers.get(layerId) === layer && !this.disposed) {
|
|
269
|
+
this.generationCount += 1;
|
|
270
|
+
this.onError(new Error(`herdr stream ${layerId} ${reason}`));
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
client.on("error", () => drop("error"));
|
|
274
|
+
client.on("close", () => drop("closed"));
|
|
275
|
+
// If the open reply never comes, assume success after a short grace so a
|
|
276
|
+
// silent-ack build still renders.
|
|
277
|
+
const grace = setTimeout(markOpen, 400);
|
|
278
|
+
}
|
|
279
|
+
flush(layerId, layer) {
|
|
280
|
+
const socket = layer.socket;
|
|
281
|
+
const frame = layer.pending;
|
|
282
|
+
if (!socket || !frame)
|
|
283
|
+
return;
|
|
284
|
+
layer.pending = null;
|
|
285
|
+
const size = pngSize(frame.png);
|
|
286
|
+
const header = {
|
|
287
|
+
format: "png",
|
|
288
|
+
image_width: size.width,
|
|
289
|
+
image_height: size.height,
|
|
290
|
+
data_length: frame.png.length,
|
|
291
|
+
placement: {
|
|
292
|
+
viewport_col: frame.placement.col,
|
|
293
|
+
viewport_row: frame.placement.row,
|
|
294
|
+
grid_cols: frame.placement.cols,
|
|
295
|
+
grid_rows: frame.placement.rows,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
let ok = false;
|
|
299
|
+
try {
|
|
300
|
+
ok = socket.write(Buffer.concat([Buffer.from(JSON.stringify(header) + "\n"), frame.png]));
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
this.onError(error instanceof Error ? error : new Error(String(error)));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!ok) {
|
|
307
|
+
// Backpressure: wait for drain, then send only the newest pending frame.
|
|
308
|
+
layer.draining = true;
|
|
309
|
+
socket.once("drain", () => {
|
|
310
|
+
layer.draining = false;
|
|
311
|
+
if (layer.pending)
|
|
312
|
+
this.flush(layerId, layer);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else if (layer.pending) {
|
|
316
|
+
this.flush(layerId, layer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
dispose() {
|
|
320
|
+
if (this.disposed)
|
|
321
|
+
return;
|
|
322
|
+
this.disposed = true;
|
|
323
|
+
for (const socket of [...this.connections])
|
|
324
|
+
this.close(socket);
|
|
325
|
+
this.layers.clear();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges Ink's layout tree to kitty graphics placements.
|
|
3
|
+
*
|
|
4
|
+
* Components register a source path and the DOM element that reserves cells
|
|
5
|
+
* for the picture. After every Ink frame the layer measures each element,
|
|
6
|
+
* fits the picture into that box, transmits bytes the terminal has not seen
|
|
7
|
+
* yet, and (re)places every visible image. Placements share the frame's
|
|
8
|
+
* synchronized-update block so text and pictures land together.
|
|
9
|
+
*/
|
|
10
|
+
import { type DOMElement } from "ink";
|
|
11
|
+
import type { ImageService, PreparedImage } from "../images/service.js";
|
|
12
|
+
import type { HerdrSink } from "./herdr.js";
|
|
13
|
+
import type { TerminalCapabilities } from "./probe.js";
|
|
14
|
+
export interface ImageHandle {
|
|
15
|
+
readonly id: number;
|
|
16
|
+
setNode(node: DOMElement | null): void;
|
|
17
|
+
/** `version` (for example the file's mtime) forces a fresh decode when the bytes change. */
|
|
18
|
+
setSource(src: string | null, version?: number): void;
|
|
19
|
+
/** Update the view: zoom (1 = whole image), pan center in [0,1], and the fill cap. */
|
|
20
|
+
setView(view: {
|
|
21
|
+
zoom?: number;
|
|
22
|
+
panX?: number;
|
|
23
|
+
panY?: number;
|
|
24
|
+
maxUpscale?: number;
|
|
25
|
+
}): void;
|
|
26
|
+
unregister(): void;
|
|
27
|
+
}
|
|
28
|
+
/** A picture that changes every few milliseconds (movie playback). */
|
|
29
|
+
export interface FrameHandle {
|
|
30
|
+
readonly id: number;
|
|
31
|
+
setNode(node: DOMElement | null): void;
|
|
32
|
+
/** Transmit and show a new PNG frame immediately. */
|
|
33
|
+
pushFrame(frame: {
|
|
34
|
+
png: Buffer;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}): void;
|
|
38
|
+
/** Drop the current frame (the poster shows through again). */
|
|
39
|
+
clearFrame(): void;
|
|
40
|
+
unregister(): void;
|
|
41
|
+
}
|
|
42
|
+
export interface Placement {
|
|
43
|
+
placementId: number;
|
|
44
|
+
imageId: number;
|
|
45
|
+
col: number;
|
|
46
|
+
row: number;
|
|
47
|
+
cols: number;
|
|
48
|
+
rows: number;
|
|
49
|
+
z: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ImageLayerOptions {
|
|
52
|
+
capabilities: TerminalCapabilities;
|
|
53
|
+
service: ImageService;
|
|
54
|
+
/** Writes straight to the terminal, bypassing Ink (used for async readiness). */
|
|
55
|
+
write: (data: string) => void;
|
|
56
|
+
/** Called when an image finished preparing and the screen should refresh. */
|
|
57
|
+
onReady?: () => void;
|
|
58
|
+
onError?: (src: string, error: Error) => void;
|
|
59
|
+
/** Diagnostics sink (enabled by ASTROSHOT_REVIEW_DEBUG). */
|
|
60
|
+
onDebug?: (message: string) => void;
|
|
61
|
+
/** When present, images are placed through herdr's socket API instead of Kitty escapes. */
|
|
62
|
+
herdr?: HerdrSink;
|
|
63
|
+
maxTransmitted?: number;
|
|
64
|
+
}
|
|
65
|
+
export declare class ImageLayer {
|
|
66
|
+
private readonly entries;
|
|
67
|
+
private readonly transmitted;
|
|
68
|
+
private lastPlacements;
|
|
69
|
+
private nextEntryId;
|
|
70
|
+
private nextImageId;
|
|
71
|
+
private tick;
|
|
72
|
+
private resync;
|
|
73
|
+
private flushScheduled;
|
|
74
|
+
private readonly capabilities;
|
|
75
|
+
private readonly service;
|
|
76
|
+
private readonly write;
|
|
77
|
+
private readonly onReady?;
|
|
78
|
+
private readonly onError?;
|
|
79
|
+
private readonly onDebug?;
|
|
80
|
+
private readonly herdr?;
|
|
81
|
+
private readonly herdrLayers;
|
|
82
|
+
private herdrGeneration;
|
|
83
|
+
private readonly maxTransmitted;
|
|
84
|
+
private lastSummary;
|
|
85
|
+
/** Row offset of the live region's first line, 1-based screen row minus 1. */
|
|
86
|
+
originRow: number;
|
|
87
|
+
originCol: number;
|
|
88
|
+
constructor(options: ImageLayerOptions);
|
|
89
|
+
get enabled(): boolean;
|
|
90
|
+
private herdrLayerId;
|
|
91
|
+
register(options: {
|
|
92
|
+
src: string | null;
|
|
93
|
+
version?: number;
|
|
94
|
+
z?: number;
|
|
95
|
+
maxUpscale?: number;
|
|
96
|
+
zoom?: number;
|
|
97
|
+
}): ImageHandle;
|
|
98
|
+
registerFrames(options?: {
|
|
99
|
+
z?: number;
|
|
100
|
+
}): FrameHandle;
|
|
101
|
+
/** Where a frame entry's current picture lands, given the live layout. */
|
|
102
|
+
private placementFor;
|
|
103
|
+
private placeCommand;
|
|
104
|
+
/** Prepared image for an entry, if any (lets components show dimensions). */
|
|
105
|
+
prepared(id: number): PreparedImage | null;
|
|
106
|
+
failure(id: number): string | null;
|
|
107
|
+
/** After a resize or screen clear, every placement must be re-sent. */
|
|
108
|
+
invalidate(): void;
|
|
109
|
+
/**
|
|
110
|
+
* Reconcile the herdr layers with the current placement set: (re)place any
|
|
111
|
+
* image whose bytes or box changed, and clear layers no longer shown.
|
|
112
|
+
*/
|
|
113
|
+
private syncHerdr;
|
|
114
|
+
/** Escape sequences that bring the terminal in line with the current layout. */
|
|
115
|
+
render(): string;
|
|
116
|
+
private evictTransmitted;
|
|
117
|
+
/** Write placements now, outside an Ink frame. Safe for the cursor. */
|
|
118
|
+
flushNow(): void;
|
|
119
|
+
scheduleFlush(): void;
|
|
120
|
+
/** Remove every placement and free image data in the terminal. */
|
|
121
|
+
clear(): string;
|
|
122
|
+
}
|