@fx-/chat-ink 0.0.0-stage → 0.1.0
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/dist/images.d-KUBzdbd9.d.ts +383 -0
- package/dist/index.d.ts +806 -0
- package/dist/index.js +3918 -0
- package/dist/kitty-jpeg.d.ts +26 -0
- package/dist/kitty-jpeg.js +84 -0
- package/dist/kitty.d.ts +86 -0
- package/dist/kitty.js +704 -0
- package/package.json +71 -4
- package/README.md +0 -3
package/dist/kitty.js
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
//#region src/kitty/diacritics.ts
|
|
4
|
+
/**
|
|
5
|
+
* Row/column combining diacritics for kitty Unicode placeholders, in order:
|
|
6
|
+
* the n-th code point encodes the number n. From the kitty graphics
|
|
7
|
+
* protocol's published list (`gen/rowcolumn-diacritics.txt`, 297 entries).
|
|
8
|
+
*/
|
|
9
|
+
const DIACRITICS = [
|
|
10
|
+
773,
|
|
11
|
+
781,
|
|
12
|
+
782,
|
|
13
|
+
784,
|
|
14
|
+
786,
|
|
15
|
+
829,
|
|
16
|
+
830,
|
|
17
|
+
831,
|
|
18
|
+
838,
|
|
19
|
+
842,
|
|
20
|
+
843,
|
|
21
|
+
844,
|
|
22
|
+
848,
|
|
23
|
+
849,
|
|
24
|
+
850,
|
|
25
|
+
855,
|
|
26
|
+
859,
|
|
27
|
+
867,
|
|
28
|
+
868,
|
|
29
|
+
869,
|
|
30
|
+
870,
|
|
31
|
+
871,
|
|
32
|
+
872,
|
|
33
|
+
873,
|
|
34
|
+
874,
|
|
35
|
+
875,
|
|
36
|
+
876,
|
|
37
|
+
877,
|
|
38
|
+
878,
|
|
39
|
+
879,
|
|
40
|
+
1155,
|
|
41
|
+
1156,
|
|
42
|
+
1157,
|
|
43
|
+
1158,
|
|
44
|
+
1159,
|
|
45
|
+
1426,
|
|
46
|
+
1427,
|
|
47
|
+
1428,
|
|
48
|
+
1429,
|
|
49
|
+
1431,
|
|
50
|
+
1432,
|
|
51
|
+
1433,
|
|
52
|
+
1436,
|
|
53
|
+
1437,
|
|
54
|
+
1438,
|
|
55
|
+
1439,
|
|
56
|
+
1440,
|
|
57
|
+
1441,
|
|
58
|
+
1448,
|
|
59
|
+
1449,
|
|
60
|
+
1451,
|
|
61
|
+
1452,
|
|
62
|
+
1455,
|
|
63
|
+
1476,
|
|
64
|
+
1552,
|
|
65
|
+
1553,
|
|
66
|
+
1554,
|
|
67
|
+
1555,
|
|
68
|
+
1556,
|
|
69
|
+
1557,
|
|
70
|
+
1558,
|
|
71
|
+
1559,
|
|
72
|
+
1623,
|
|
73
|
+
1624,
|
|
74
|
+
1625,
|
|
75
|
+
1626,
|
|
76
|
+
1627,
|
|
77
|
+
1629,
|
|
78
|
+
1630,
|
|
79
|
+
1750,
|
|
80
|
+
1751,
|
|
81
|
+
1752,
|
|
82
|
+
1753,
|
|
83
|
+
1754,
|
|
84
|
+
1755,
|
|
85
|
+
1756,
|
|
86
|
+
1759,
|
|
87
|
+
1760,
|
|
88
|
+
1761,
|
|
89
|
+
1762,
|
|
90
|
+
1764,
|
|
91
|
+
1767,
|
|
92
|
+
1768,
|
|
93
|
+
1771,
|
|
94
|
+
1772,
|
|
95
|
+
1840,
|
|
96
|
+
1842,
|
|
97
|
+
1843,
|
|
98
|
+
1845,
|
|
99
|
+
1846,
|
|
100
|
+
1850,
|
|
101
|
+
1853,
|
|
102
|
+
1855,
|
|
103
|
+
1856,
|
|
104
|
+
1857,
|
|
105
|
+
1859,
|
|
106
|
+
1861,
|
|
107
|
+
1863,
|
|
108
|
+
1865,
|
|
109
|
+
1866,
|
|
110
|
+
2027,
|
|
111
|
+
2028,
|
|
112
|
+
2029,
|
|
113
|
+
2030,
|
|
114
|
+
2031,
|
|
115
|
+
2032,
|
|
116
|
+
2033,
|
|
117
|
+
2035,
|
|
118
|
+
2070,
|
|
119
|
+
2071,
|
|
120
|
+
2072,
|
|
121
|
+
2073,
|
|
122
|
+
2075,
|
|
123
|
+
2076,
|
|
124
|
+
2077,
|
|
125
|
+
2078,
|
|
126
|
+
2079,
|
|
127
|
+
2080,
|
|
128
|
+
2081,
|
|
129
|
+
2082,
|
|
130
|
+
2083,
|
|
131
|
+
2085,
|
|
132
|
+
2086,
|
|
133
|
+
2087,
|
|
134
|
+
2089,
|
|
135
|
+
2090,
|
|
136
|
+
2091,
|
|
137
|
+
2092,
|
|
138
|
+
2093,
|
|
139
|
+
2385,
|
|
140
|
+
2387,
|
|
141
|
+
2388,
|
|
142
|
+
3970,
|
|
143
|
+
3971,
|
|
144
|
+
3974,
|
|
145
|
+
3975,
|
|
146
|
+
4957,
|
|
147
|
+
4958,
|
|
148
|
+
4959,
|
|
149
|
+
6109,
|
|
150
|
+
6458,
|
|
151
|
+
6679,
|
|
152
|
+
6773,
|
|
153
|
+
6774,
|
|
154
|
+
6775,
|
|
155
|
+
6776,
|
|
156
|
+
6777,
|
|
157
|
+
6778,
|
|
158
|
+
6779,
|
|
159
|
+
6780,
|
|
160
|
+
7019,
|
|
161
|
+
7021,
|
|
162
|
+
7022,
|
|
163
|
+
7023,
|
|
164
|
+
7024,
|
|
165
|
+
7025,
|
|
166
|
+
7026,
|
|
167
|
+
7027,
|
|
168
|
+
7376,
|
|
169
|
+
7377,
|
|
170
|
+
7378,
|
|
171
|
+
7386,
|
|
172
|
+
7387,
|
|
173
|
+
7392,
|
|
174
|
+
7616,
|
|
175
|
+
7617,
|
|
176
|
+
7619,
|
|
177
|
+
7620,
|
|
178
|
+
7621,
|
|
179
|
+
7622,
|
|
180
|
+
7623,
|
|
181
|
+
7624,
|
|
182
|
+
7625,
|
|
183
|
+
7627,
|
|
184
|
+
7628,
|
|
185
|
+
7633,
|
|
186
|
+
7634,
|
|
187
|
+
7635,
|
|
188
|
+
7636,
|
|
189
|
+
7637,
|
|
190
|
+
7638,
|
|
191
|
+
7639,
|
|
192
|
+
7640,
|
|
193
|
+
7641,
|
|
194
|
+
7642,
|
|
195
|
+
7643,
|
|
196
|
+
7644,
|
|
197
|
+
7645,
|
|
198
|
+
7646,
|
|
199
|
+
7647,
|
|
200
|
+
7648,
|
|
201
|
+
7649,
|
|
202
|
+
7650,
|
|
203
|
+
7651,
|
|
204
|
+
7652,
|
|
205
|
+
7653,
|
|
206
|
+
7654,
|
|
207
|
+
7678,
|
|
208
|
+
8400,
|
|
209
|
+
8401,
|
|
210
|
+
8404,
|
|
211
|
+
8405,
|
|
212
|
+
8406,
|
|
213
|
+
8407,
|
|
214
|
+
8411,
|
|
215
|
+
8412,
|
|
216
|
+
8417,
|
|
217
|
+
8423,
|
|
218
|
+
8425,
|
|
219
|
+
8432,
|
|
220
|
+
11503,
|
|
221
|
+
11504,
|
|
222
|
+
11505,
|
|
223
|
+
11744,
|
|
224
|
+
11745,
|
|
225
|
+
11746,
|
|
226
|
+
11747,
|
|
227
|
+
11748,
|
|
228
|
+
11749,
|
|
229
|
+
11750,
|
|
230
|
+
11751,
|
|
231
|
+
11752,
|
|
232
|
+
11753,
|
|
233
|
+
11754,
|
|
234
|
+
11755,
|
|
235
|
+
11756,
|
|
236
|
+
11757,
|
|
237
|
+
11758,
|
|
238
|
+
11759,
|
|
239
|
+
11760,
|
|
240
|
+
11761,
|
|
241
|
+
11762,
|
|
242
|
+
11763,
|
|
243
|
+
11764,
|
|
244
|
+
11765,
|
|
245
|
+
11766,
|
|
246
|
+
11767,
|
|
247
|
+
11768,
|
|
248
|
+
11769,
|
|
249
|
+
11770,
|
|
250
|
+
11771,
|
|
251
|
+
11772,
|
|
252
|
+
11773,
|
|
253
|
+
11774,
|
|
254
|
+
11775,
|
|
255
|
+
42607,
|
|
256
|
+
42620,
|
|
257
|
+
42621,
|
|
258
|
+
42736,
|
|
259
|
+
42737,
|
|
260
|
+
43232,
|
|
261
|
+
43233,
|
|
262
|
+
43234,
|
|
263
|
+
43235,
|
|
264
|
+
43236,
|
|
265
|
+
43237,
|
|
266
|
+
43238,
|
|
267
|
+
43239,
|
|
268
|
+
43240,
|
|
269
|
+
43241,
|
|
270
|
+
43242,
|
|
271
|
+
43243,
|
|
272
|
+
43244,
|
|
273
|
+
43245,
|
|
274
|
+
43246,
|
|
275
|
+
43247,
|
|
276
|
+
43248,
|
|
277
|
+
43249,
|
|
278
|
+
43696,
|
|
279
|
+
43698,
|
|
280
|
+
43699,
|
|
281
|
+
43703,
|
|
282
|
+
43704,
|
|
283
|
+
43710,
|
|
284
|
+
43711,
|
|
285
|
+
43713,
|
|
286
|
+
65056,
|
|
287
|
+
65057,
|
|
288
|
+
65058,
|
|
289
|
+
65059,
|
|
290
|
+
65060,
|
|
291
|
+
65061,
|
|
292
|
+
65062,
|
|
293
|
+
68111,
|
|
294
|
+
68152,
|
|
295
|
+
119173,
|
|
296
|
+
119174,
|
|
297
|
+
119175,
|
|
298
|
+
119176,
|
|
299
|
+
119177,
|
|
300
|
+
119210,
|
|
301
|
+
119211,
|
|
302
|
+
119212,
|
|
303
|
+
119213,
|
|
304
|
+
119362,
|
|
305
|
+
119363,
|
|
306
|
+
119364
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/kitty/provider.ts
|
|
311
|
+
const ESC = String.fromCharCode(27);
|
|
312
|
+
const PLACEHOLDER = String.fromCodePoint(1109742);
|
|
313
|
+
const PNG_SIGNATURE = [
|
|
314
|
+
137,
|
|
315
|
+
80,
|
|
316
|
+
78,
|
|
317
|
+
71,
|
|
318
|
+
13,
|
|
319
|
+
10,
|
|
320
|
+
26,
|
|
321
|
+
10
|
|
322
|
+
];
|
|
323
|
+
/** Ids available to images (the 256-colour foreground carries the id). */
|
|
324
|
+
const MAX_IMAGES = 255;
|
|
325
|
+
/** The largest width or height in pixels a terminal accepts (kitty's and ghostty's limit). */
|
|
326
|
+
const MAX_IMAGE_SIDE = 1e4;
|
|
327
|
+
/** The largest grid in cells: one combining diacritic per row and per column number. */
|
|
328
|
+
const MAX_CELLS = DIACRITICS.length;
|
|
329
|
+
/** The longest timer `setTimeout` keeps; longer ones fire at once. */
|
|
330
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
331
|
+
/** Whether the terminal speaks the kitty graphics protocol (and is not behind a multiplexer). */
|
|
332
|
+
function detectKitty(env = process.env) {
|
|
333
|
+
if (env.TMUX || env.STY || /^(screen|tmux)/.test(env.TERM ?? "")) return false;
|
|
334
|
+
return env.TERM_PROGRAM === "ghostty" || env.TERM_PROGRAM === "kitty" || Boolean(env.GHOSTTY_RESOURCES_DIR) || Boolean(env.KITTY_WINDOW_ID) || env.TERM === "xterm-ghostty" || env.TERM === "xterm-kitty";
|
|
335
|
+
}
|
|
336
|
+
function isPng(bytes) {
|
|
337
|
+
return PNG_SIGNATURE.every((b, i) => bytes[i] === b);
|
|
338
|
+
}
|
|
339
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
340
|
+
let c = n;
|
|
341
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
342
|
+
return c >>> 0;
|
|
343
|
+
});
|
|
344
|
+
function crc32(bytes) {
|
|
345
|
+
let c = 4294967295;
|
|
346
|
+
for (const b of bytes) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
|
|
347
|
+
return (c ^ 4294967295) >>> 0;
|
|
348
|
+
}
|
|
349
|
+
/** Legal PNG bit depths per colour type. */
|
|
350
|
+
const BIT_DEPTHS = {
|
|
351
|
+
0: [
|
|
352
|
+
1,
|
|
353
|
+
2,
|
|
354
|
+
4,
|
|
355
|
+
8,
|
|
356
|
+
16
|
|
357
|
+
],
|
|
358
|
+
2: [8, 16],
|
|
359
|
+
3: [
|
|
360
|
+
1,
|
|
361
|
+
2,
|
|
362
|
+
4,
|
|
363
|
+
8
|
|
364
|
+
],
|
|
365
|
+
4: [8, 16],
|
|
366
|
+
6: [8, 16]
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* A PNG's pixel size from its header: undefined unless the signature is
|
|
370
|
+
* followed by a complete IHDR chunk (length, type, data, CRC) whose CRC
|
|
371
|
+
* matches and whose fields are legal. The image data is not decoded here.
|
|
372
|
+
*/
|
|
373
|
+
function pngSize(bytes) {
|
|
374
|
+
if (!isPng(bytes) || bytes.length < 33) return void 0;
|
|
375
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
376
|
+
if (view.getUint32(8) !== 13 || view.getUint32(12) !== 1229472850) return void 0;
|
|
377
|
+
if (crc32(bytes.subarray(12, 29)) !== view.getUint32(29)) return void 0;
|
|
378
|
+
const width = view.getUint32(16);
|
|
379
|
+
const height = view.getUint32(20);
|
|
380
|
+
if (width === 0 || height === 0 || width > 2 ** 31 - 1 || height > 2 ** 31 - 1) return void 0;
|
|
381
|
+
if (!BIT_DEPTHS[bytes[25]]?.includes(bytes[24])) return void 0;
|
|
382
|
+
if (bytes[26] !== 0 || bytes[27] !== 0 || bytes[28] > 1) return void 0;
|
|
383
|
+
return {
|
|
384
|
+
width,
|
|
385
|
+
height
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The cells an image of `px` pixels takes in `box`, keeping its aspect and
|
|
390
|
+
* never upscaling. The box is first capped to `MAX_CELLS` each way, the most
|
|
391
|
+
* rows and columns placeholders can number.
|
|
392
|
+
*/
|
|
393
|
+
function fitCells(px, within, cell) {
|
|
394
|
+
const box = {
|
|
395
|
+
columns: Math.min(within.columns, MAX_CELLS),
|
|
396
|
+
rows: Math.min(within.rows, MAX_CELLS)
|
|
397
|
+
};
|
|
398
|
+
const scale = Math.min(box.columns * cell.width / px.width, box.rows * cell.height / px.height, 1);
|
|
399
|
+
return {
|
|
400
|
+
columns: Math.max(1, Math.min(box.columns, Math.ceil(px.width * scale / cell.width))),
|
|
401
|
+
rows: Math.max(1, Math.min(box.rows, Math.ceil(px.height * scale / cell.height)))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/** Placeholder text for an image: one string per cell row, at most `MAX_CELLS` each way. */
|
|
405
|
+
function placeholderRows(columns, rows) {
|
|
406
|
+
const mark = (n) => String.fromCodePoint(DIACRITICS[n]);
|
|
407
|
+
return Array.from({ length: Math.min(rows, MAX_CELLS) }, (_, r) => Array.from({ length: Math.min(columns, MAX_CELLS) }, (_$1, c) => `${PLACEHOLDER}${mark(r)}${mark(c)}`).join(""));
|
|
408
|
+
}
|
|
409
|
+
const apc = (control, payload = "") => `${ESC}_G${control}${payload ? `;${payload}` : ""}${ESC}\\`;
|
|
410
|
+
/** Transmit PNG bytes quietly under `id`, in protocol-sized chunks. */
|
|
411
|
+
function transmit(id, png) {
|
|
412
|
+
const data = Buffer.from(png).toString("base64");
|
|
413
|
+
const chunks = data.match(/.{1,4096}/g) ?? [""];
|
|
414
|
+
return chunks.map((chunk, i) => {
|
|
415
|
+
const more = i < chunks.length - 1 ? 1 : 0;
|
|
416
|
+
return i === 0 ? apc(`a=t,f=100,t=d,i=${id},q=2,m=${more}`, chunk) : apc(`m=${more}`, chunk);
|
|
417
|
+
}).join("");
|
|
418
|
+
}
|
|
419
|
+
/** One virtual placement for `id`, sized to a cell box (replacing the previous one). */
|
|
420
|
+
function place(id, box) {
|
|
421
|
+
return apc(`a=p,U=1,i=${id},p=1,c=${box.columns},r=${box.rows},q=2`);
|
|
422
|
+
}
|
|
423
|
+
/** Forget an image and its placements. */
|
|
424
|
+
function forget(id) {
|
|
425
|
+
return apc(`a=d,d=I,i=${id},q=2`);
|
|
426
|
+
}
|
|
427
|
+
/** Ask the terminal for its cell size in pixels. */
|
|
428
|
+
const CELL_SIZE_QUERY = `${ESC}[16t`;
|
|
429
|
+
const CELL_SIZE_REPLY = new RegExp(`^(?:${ESC})?\\[6;(\\d+);(\\d+)t`);
|
|
430
|
+
/** The byte a valid `%XX` escape at `i` stands for, else undefined. */
|
|
431
|
+
function escapeAt(body, i) {
|
|
432
|
+
if (body.charCodeAt(i) !== 37) return void 0;
|
|
433
|
+
const hex = body.slice(i + 1, i + 3);
|
|
434
|
+
return /^[0-9a-f]{2}$/i.test(hex) ? Number.parseInt(hex, 16) : void 0;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The exact bytes a percent-encoded body decodes to, counted without decoding
|
|
438
|
+
* it: a valid `%XX` is one byte, anything else its UTF-8 length (a stray `%`
|
|
439
|
+
* stays literal, a lone surrogate becomes U+FFFD).
|
|
440
|
+
*/
|
|
441
|
+
function percentLength(body) {
|
|
442
|
+
let n = 0;
|
|
443
|
+
for (let i = 0; i < body.length; i++) {
|
|
444
|
+
const code = body.charCodeAt(i);
|
|
445
|
+
if (escapeAt(body, i) !== void 0) {
|
|
446
|
+
n++;
|
|
447
|
+
i += 2;
|
|
448
|
+
} else if (code < 128) n++;
|
|
449
|
+
else if (code < 2048) n += 2;
|
|
450
|
+
else if (code >= 55296 && code < 56320 && (body.charCodeAt(i + 1) & 64512) === 56320) {
|
|
451
|
+
n += 4;
|
|
452
|
+
i++;
|
|
453
|
+
} else n += 3;
|
|
454
|
+
}
|
|
455
|
+
return n;
|
|
456
|
+
}
|
|
457
|
+
/** Percent-decode a body into the `length` raw bytes `percentLength` counted. */
|
|
458
|
+
function percentDecode(body, length) {
|
|
459
|
+
const out = new Uint8Array(length);
|
|
460
|
+
const encoder = new TextEncoder();
|
|
461
|
+
let at = 0;
|
|
462
|
+
let i = 0;
|
|
463
|
+
while (i < body.length) {
|
|
464
|
+
const escaped = escapeAt(body, i);
|
|
465
|
+
if (escaped !== void 0) {
|
|
466
|
+
out[at++] = escaped;
|
|
467
|
+
i += 3;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
let end = i + 1;
|
|
471
|
+
while (end < body.length && escapeAt(body, end) === void 0) end++;
|
|
472
|
+
at += encoder.encodeInto(body.slice(i, end), out.subarray(at)).written;
|
|
473
|
+
i = end;
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The most bytes a base64 body decodes to: exact for clean base64, more when
|
|
479
|
+
* it holds whitespace or other skipped characters, so it never undercounts.
|
|
480
|
+
*/
|
|
481
|
+
function base64Ceiling(body) {
|
|
482
|
+
const padding = body.endsWith("==") ? 2 : body.endsWith("=") ? 1 : 0;
|
|
483
|
+
return Math.floor(body.length * 3 / 4) - padding;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Decode a data URL, refusing one whose body decodes to more than `maxBytes`
|
|
487
|
+
* before allocating the decoded bytes.
|
|
488
|
+
*/
|
|
489
|
+
function decodeDataUrl(src, maxBytes) {
|
|
490
|
+
const comma = src.indexOf(",");
|
|
491
|
+
if (comma < 0) throw new Error("malformed data URL");
|
|
492
|
+
const header = src.slice(5, comma);
|
|
493
|
+
const body = src.slice(comma + 1);
|
|
494
|
+
const mediaType = header.split(";")[0] || "text/plain";
|
|
495
|
+
if (header.endsWith(";base64")) {
|
|
496
|
+
if (base64Ceiling(body) > maxBytes) throw new Error("too large");
|
|
497
|
+
return {
|
|
498
|
+
bytes: new Uint8Array(Buffer.from(body, "base64")),
|
|
499
|
+
mediaType
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const length = percentLength(body);
|
|
503
|
+
if (length > maxBytes) throw new Error("too large");
|
|
504
|
+
return {
|
|
505
|
+
bytes: percentDecode(body, length),
|
|
506
|
+
mediaType
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/** Read a response body in chunks, cancelling it once it passes `maxBytes`. */
|
|
510
|
+
async function readCapped(response, maxBytes) {
|
|
511
|
+
if (!response.body) return new Uint8Array();
|
|
512
|
+
const reader = response.body.getReader();
|
|
513
|
+
const chunks = [];
|
|
514
|
+
let total = 0;
|
|
515
|
+
for (;;) {
|
|
516
|
+
const { done, value } = await reader.read();
|
|
517
|
+
if (done) break;
|
|
518
|
+
total += value.byteLength;
|
|
519
|
+
if (total > maxBytes) {
|
|
520
|
+
await reader.cancel();
|
|
521
|
+
throw new Error("too large");
|
|
522
|
+
}
|
|
523
|
+
chunks.push(value);
|
|
524
|
+
}
|
|
525
|
+
const bytes = new Uint8Array(total);
|
|
526
|
+
let at = 0;
|
|
527
|
+
for (const chunk of chunks) {
|
|
528
|
+
bytes.set(chunk, at);
|
|
529
|
+
at += chunk.byteLength;
|
|
530
|
+
}
|
|
531
|
+
return bytes;
|
|
532
|
+
}
|
|
533
|
+
/** Refuse an option that is not a finite number in (0, `max`]. */
|
|
534
|
+
function positive(name, value, max = Number.MAX_VALUE) {
|
|
535
|
+
if (!(value > 0 && value <= max)) throw new RangeError(`${name} must be a positive number up to ${max}, got ${value}`);
|
|
536
|
+
}
|
|
537
|
+
function createKittyProvider(options) {
|
|
538
|
+
const { write, decode, autoFetch = (src) => src.startsWith("data:"), timeoutMs = 15e3, maxBytes = 20 * 1024 * 1024, maxMegapixels = 50 } = options;
|
|
539
|
+
positive("timeoutMs", timeoutMs, MAX_TIMEOUT_MS);
|
|
540
|
+
positive("maxBytes", maxBytes);
|
|
541
|
+
positive("maxMegapixels", maxMegapixels);
|
|
542
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
543
|
+
const entries = new Map();
|
|
544
|
+
const allowed = new Set();
|
|
545
|
+
const listeners = new Set();
|
|
546
|
+
const ids = [];
|
|
547
|
+
let version = 0;
|
|
548
|
+
let cell = {
|
|
549
|
+
width: 9,
|
|
550
|
+
height: 18
|
|
551
|
+
};
|
|
552
|
+
const changed = () => {
|
|
553
|
+
version++;
|
|
554
|
+
for (const listener of listeners) listener();
|
|
555
|
+
};
|
|
556
|
+
const fail = (entry, error) => {
|
|
557
|
+
entry.state = "failed";
|
|
558
|
+
entry.error = error;
|
|
559
|
+
};
|
|
560
|
+
async function bytesOf(src, clicked) {
|
|
561
|
+
if (src.startsWith("data:")) return decodeDataUrl(src, maxBytes);
|
|
562
|
+
const controller = new AbortController();
|
|
563
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
564
|
+
try {
|
|
565
|
+
const response = await fetcher(src, {
|
|
566
|
+
redirect: clicked ? "follow" : "error",
|
|
567
|
+
signal: controller.signal
|
|
568
|
+
});
|
|
569
|
+
const refuse = (message) => {
|
|
570
|
+
response.body?.cancel().catch(() => {});
|
|
571
|
+
return new Error(message);
|
|
572
|
+
};
|
|
573
|
+
if (!response.ok) throw refuse(`HTTP ${response.status}`);
|
|
574
|
+
if (Number(response.headers.get("content-length") ?? 0) > maxBytes) throw refuse("too large");
|
|
575
|
+
const bytes = await readCapped(response, maxBytes);
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
return {
|
|
578
|
+
bytes,
|
|
579
|
+
mediaType: response.headers.get("content-type") ?? ""
|
|
580
|
+
};
|
|
581
|
+
} catch (error) {
|
|
582
|
+
clearTimeout(timer);
|
|
583
|
+
throw controller.signal.aborted ? new Error("timed out") : error;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function load(src, entry, clicked) {
|
|
587
|
+
entry.state = "loading";
|
|
588
|
+
try {
|
|
589
|
+
const { bytes, mediaType } = await bytesOf(src, clicked);
|
|
590
|
+
const png = isPng(bytes) ? bytes : await decode?.(bytes, mediaType);
|
|
591
|
+
if (png && png.byteLength > maxBytes) throw new Error("too large");
|
|
592
|
+
if (!png) entry.state = "declined";
|
|
593
|
+
else {
|
|
594
|
+
const px = pngSize(png);
|
|
595
|
+
if (!px) throw new Error("malformed image");
|
|
596
|
+
if (px.width > MAX_IMAGE_SIDE || px.height > MAX_IMAGE_SIDE || px.width * px.height > maxMegapixels * 1e6) throw new Error("too large");
|
|
597
|
+
Object.assign(entry, {
|
|
598
|
+
state: "ready",
|
|
599
|
+
png,
|
|
600
|
+
px
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
} catch (error) {
|
|
604
|
+
fail(entry, error instanceof Error ? error.message : String(error));
|
|
605
|
+
}
|
|
606
|
+
changed();
|
|
607
|
+
}
|
|
608
|
+
const entryFor = (src) => {
|
|
609
|
+
let entry = entries.get(src);
|
|
610
|
+
if (!entry) {
|
|
611
|
+
entry = { state: "ask" };
|
|
612
|
+
entries.set(src, entry);
|
|
613
|
+
if (allowed.has(src) || autoFetch(src)) load(src, entry, allowed.has(src));
|
|
614
|
+
}
|
|
615
|
+
return entry;
|
|
616
|
+
};
|
|
617
|
+
const assignId = (src, entry) => {
|
|
618
|
+
if (entry.id !== void 0) return entry.id;
|
|
619
|
+
const used = new Set(ids.map((i) => i.id));
|
|
620
|
+
let id = 1;
|
|
621
|
+
while (used.has(id) && id <= MAX_IMAGES) id++;
|
|
622
|
+
if (id > MAX_IMAGES) {
|
|
623
|
+
const oldest = ids.shift();
|
|
624
|
+
write(forget(oldest.id));
|
|
625
|
+
entries.delete(oldest.src);
|
|
626
|
+
id = oldest.id;
|
|
627
|
+
queueMicrotask(changed);
|
|
628
|
+
}
|
|
629
|
+
ids.push({
|
|
630
|
+
id,
|
|
631
|
+
src
|
|
632
|
+
});
|
|
633
|
+
entry.id = id;
|
|
634
|
+
return id;
|
|
635
|
+
};
|
|
636
|
+
return {
|
|
637
|
+
get(src, box) {
|
|
638
|
+
const entry = entryFor(src);
|
|
639
|
+
switch (entry.state) {
|
|
640
|
+
case "ask": return { kind: "ask" };
|
|
641
|
+
case "loading": return { kind: "loading" };
|
|
642
|
+
case "failed": return {
|
|
643
|
+
kind: "failed",
|
|
644
|
+
error: entry.error
|
|
645
|
+
};
|
|
646
|
+
case "declined": return void 0;
|
|
647
|
+
}
|
|
648
|
+
const id = assignId(src, entry);
|
|
649
|
+
if (!entry.sent) {
|
|
650
|
+
write(transmit(id, entry.png));
|
|
651
|
+
entry.sent = true;
|
|
652
|
+
entry.png = void 0;
|
|
653
|
+
}
|
|
654
|
+
const cells = fitCells(entry.px, box, cell);
|
|
655
|
+
const key = `${cells.columns}x${cells.rows}`;
|
|
656
|
+
if (entry.placed !== key) {
|
|
657
|
+
write(place(id, cells));
|
|
658
|
+
entry.placed = key;
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
kind: "ready",
|
|
662
|
+
rows: placeholderRows(cells.columns, cells.rows),
|
|
663
|
+
color: `ansi256(${id})`
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
allow(src) {
|
|
667
|
+
allowed.add(src);
|
|
668
|
+
const entry = entries.get(src);
|
|
669
|
+
if (!entry) entryFor(src);
|
|
670
|
+
else if (entry.state === "ask") load(src, entry, true);
|
|
671
|
+
changed();
|
|
672
|
+
},
|
|
673
|
+
get version() {
|
|
674
|
+
return version;
|
|
675
|
+
},
|
|
676
|
+
subscribe(listener) {
|
|
677
|
+
listeners.add(listener);
|
|
678
|
+
return () => {
|
|
679
|
+
listeners.delete(listener);
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
query: CELL_SIZE_QUERY,
|
|
683
|
+
get cellSize() {
|
|
684
|
+
return cell;
|
|
685
|
+
},
|
|
686
|
+
takeReply(input) {
|
|
687
|
+
const match = CELL_SIZE_REPLY.exec(input);
|
|
688
|
+
if (!match) return false;
|
|
689
|
+
const next = {
|
|
690
|
+
width: Number(match[2]),
|
|
691
|
+
height: Number(match[1])
|
|
692
|
+
};
|
|
693
|
+
if (next.width > 0 && next.height > 0 && (next.width !== cell.width || next.height !== cell.height)) {
|
|
694
|
+
cell = next;
|
|
695
|
+
for (const entry of entries.values()) entry.placed = void 0;
|
|
696
|
+
changed();
|
|
697
|
+
}
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
//#endregion
|
|
704
|
+
export { CELL_SIZE_QUERY, DIACRITICS, MAX_CELLS, MAX_IMAGES, MAX_IMAGE_SIDE, createKittyProvider, detectKitty, fitCells, forget, isPng, place, placeholderRows, pngSize, transmit };
|