@sayknow-cli/tui 0.3.7 → 0.3.9
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/types/animation-scheduler.d.ts +13 -0
- package/dist/types/autocomplete.d.ts +1 -0
- package/dist/types/components/editor.d.ts +11 -2
- package/dist/types/components/loader.d.ts +10 -1
- package/dist/types/components/markdown.d.ts +14 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/terminal-capabilities.d.ts +70 -2
- package/dist/types/terminal.d.ts +2 -0
- package/dist/types/tui.d.ts +22 -4
- package/dist/types/utils.d.ts +12 -0
- package/package.json +4 -4
- package/src/animation-scheduler.ts +99 -0
- package/src/autocomplete.ts +119 -96
- package/src/components/editor.ts +315 -175
- package/src/components/image.ts +43 -9
- package/src/components/loader.ts +36 -37
- package/src/components/markdown.ts +79 -2
- package/src/components/settings-list.ts +4 -2
- package/src/index.ts +1 -0
- package/src/stdin-buffer.ts +89 -11
- package/src/terminal-capabilities.ts +149 -7
- package/src/terminal.ts +7 -0
- package/src/tui.ts +274 -96
- package/src/utils.ts +77 -11
|
@@ -222,6 +222,40 @@ export interface ImageRenderOptions {
|
|
|
222
222
|
maxWidthCells?: number;
|
|
223
223
|
maxHeightCells?: number;
|
|
224
224
|
preserveAspectRatio?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Kitty-only: stable placement id (`p=`). Re-emitting the same image id +
|
|
227
|
+
* placement id *replaces* the existing placement instead of stacking a new
|
|
228
|
+
* copy, which makes diff-renderer repaints idempotent. Callers that render
|
|
229
|
+
* a persistent component should allocate one id per component instance.
|
|
230
|
+
*/
|
|
231
|
+
placementId?: number;
|
|
232
|
+
/**
|
|
233
|
+
* Kitty-only: stable image id (`i=`). Defaults to a content hash of the
|
|
234
|
+
* base64 payload ({@link kittyImageId}). Pass a precomputed id to avoid
|
|
235
|
+
* re-hashing large payloads on every render.
|
|
236
|
+
*/
|
|
237
|
+
imageId?: number;
|
|
238
|
+
/**
|
|
239
|
+
* Kitty-only: sink for the out-of-band data transmission (`a=t`) emitted
|
|
240
|
+
* the first time an image id is rendered. Defaults to the process-wide
|
|
241
|
+
* writer configured via {@link setKittyTransmitWriter} (stdout).
|
|
242
|
+
*/
|
|
243
|
+
onTransmit?: (sequence: string) => void;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Derive a stable 32-bit non-zero kitty image id (`i=`) from image content
|
|
248
|
+
* (FNV-1a over the base64 payload). Identical content maps to the same id, so
|
|
249
|
+
* retransmission replaces the stored image instead of accumulating copies.
|
|
250
|
+
*/
|
|
251
|
+
export function kittyImageId(base64Data: string): number {
|
|
252
|
+
let hash = 0x811c9dc5;
|
|
253
|
+
for (let i = 0; i < base64Data.length; i++) {
|
|
254
|
+
hash ^= base64Data.charCodeAt(i);
|
|
255
|
+
hash = Math.imul(hash, 0x01000193);
|
|
256
|
+
}
|
|
257
|
+
hash >>>= 0;
|
|
258
|
+
return hash === 0 ? 1 : hash;
|
|
225
259
|
}
|
|
226
260
|
|
|
227
261
|
// Default cell dimensions - updated by TUI when terminal responds to query
|
|
@@ -241,6 +275,7 @@ export function encodeKitty(
|
|
|
241
275
|
columns?: number;
|
|
242
276
|
rows?: number;
|
|
243
277
|
imageId?: number;
|
|
278
|
+
placementId?: number;
|
|
244
279
|
} = {},
|
|
245
280
|
): string {
|
|
246
281
|
const CHUNK_SIZE = 4096;
|
|
@@ -249,7 +284,77 @@ export function encodeKitty(
|
|
|
249
284
|
|
|
250
285
|
if (options.columns) params.push(`c=${options.columns}`);
|
|
251
286
|
if (options.rows) params.push(`r=${options.rows}`);
|
|
252
|
-
if (options.imageId)
|
|
287
|
+
if (options.imageId) {
|
|
288
|
+
params.push(`i=${options.imageId}`);
|
|
289
|
+
// A placement id is only meaningful together with an image id. Same
|
|
290
|
+
// i= + p= replaces the previous placement (kitty graphics spec), so
|
|
291
|
+
// re-emitting this sequence never duplicates the image on screen.
|
|
292
|
+
if (options.placementId) params.push(`p=${options.placementId}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (base64Data.length <= CHUNK_SIZE) {
|
|
296
|
+
return `\x1b_G${params.join(",")};${base64Data}\x1b\\`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const chunks: string[] = [];
|
|
300
|
+
let offset = 0;
|
|
301
|
+
let isFirst = true;
|
|
302
|
+
|
|
303
|
+
while (offset < base64Data.length) {
|
|
304
|
+
const chunk = base64Data.slice(offset, offset + CHUNK_SIZE);
|
|
305
|
+
const isLast = offset + CHUNK_SIZE >= base64Data.length;
|
|
306
|
+
|
|
307
|
+
if (isFirst) {
|
|
308
|
+
chunks.push(`\x1b_G${params.join(",")},m=1;${chunk}\x1b\\`);
|
|
309
|
+
isFirst = false;
|
|
310
|
+
} else if (isLast) {
|
|
311
|
+
chunks.push(`\x1b_Gm=0;${chunk}\x1b\\`);
|
|
312
|
+
} else {
|
|
313
|
+
chunks.push(`\x1b_Gm=1;${chunk}\x1b\\`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
offset += CHUNK_SIZE;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return chunks.join("");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Kitty image ids already uploaded to the terminal in this process. */
|
|
323
|
+
const transmittedKittyImageIds = new Set<number>();
|
|
324
|
+
|
|
325
|
+
/** Test hook: forget which kitty image ids were transmitted. */
|
|
326
|
+
export function resetKittyTransmissions(): void {
|
|
327
|
+
transmittedKittyImageIds.clear();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let kittyTransmitWriter: (sequence: string) => void = sequence => {
|
|
331
|
+
process.stdout.write(sequence);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Override where out-of-band kitty data transmissions (`a=t`) are written.
|
|
336
|
+
* The default writes directly to stdout: a transmit-only escape is
|
|
337
|
+
* cursor-neutral (it uploads pixel data without drawing anything), so the
|
|
338
|
+
* only ordering requirement is that it reaches the terminal before the
|
|
339
|
+
* placement escape that references it — which the synchronous write during
|
|
340
|
+
* render guarantees. Tests use this to capture transmissions.
|
|
341
|
+
*/
|
|
342
|
+
export function setKittyTransmitWriter(writer: (sequence: string) => void): void {
|
|
343
|
+
kittyTransmitWriter = writer;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Encode a kitty transmit-only (`a=t`) escape: uploads image data under a
|
|
348
|
+
* stable id without creating a placement. Chunked at 4096 bytes per spec.
|
|
349
|
+
*
|
|
350
|
+
* This is deliberately separate from placement: re-sending data (`a=t`/`a=T`)
|
|
351
|
+
* for an existing image id deletes the image and ALL of its placements, so
|
|
352
|
+
* data must be uploaded exactly once per id and repaints must go through
|
|
353
|
+
* {@link encodeKittyPlacement} only.
|
|
354
|
+
*/
|
|
355
|
+
export function encodeKittyTransmit(base64Data: string, imageId: number): string {
|
|
356
|
+
const CHUNK_SIZE = 4096;
|
|
357
|
+
const params = ["a=t", "f=100", "q=2", `i=${imageId}`];
|
|
253
358
|
|
|
254
359
|
if (base64Data.length <= CHUNK_SIZE) {
|
|
255
360
|
return `\x1b_G${params.join(",")};${base64Data}\x1b\\`;
|
|
@@ -278,6 +383,22 @@ export function encodeKitty(
|
|
|
278
383
|
return chunks.join("");
|
|
279
384
|
}
|
|
280
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Encode a kitty placement-only (`a=p`) escape referencing previously
|
|
388
|
+
* transmitted data. Re-emitting the same i=/p= pair replaces that one
|
|
389
|
+
* placement (never stacks, never touches sibling placements), and C=1
|
|
390
|
+
* keeps the cursor where it is so the escape can be emitted from the
|
|
391
|
+
* component's first row without cursor-up tricks.
|
|
392
|
+
*/
|
|
393
|
+
export function encodeKittyPlacement(options: {
|
|
394
|
+
imageId: number;
|
|
395
|
+
placementId: number;
|
|
396
|
+
columns: number;
|
|
397
|
+
rows: number;
|
|
398
|
+
}): string {
|
|
399
|
+
return `\x1b_Ga=p,i=${options.imageId},p=${options.placementId},c=${options.columns},r=${options.rows},C=1,q=2\x1b\\`;
|
|
400
|
+
}
|
|
401
|
+
|
|
281
402
|
export function encodeITerm2(
|
|
282
403
|
base64Data: string,
|
|
283
404
|
options: {
|
|
@@ -485,11 +606,23 @@ export function getImageDimensions(base64Data: string, mimeType: string): ImageD
|
|
|
485
606
|
return null;
|
|
486
607
|
}
|
|
487
608
|
|
|
609
|
+
export interface RenderedImage {
|
|
610
|
+
sequence: string;
|
|
611
|
+
rows: number;
|
|
612
|
+
/**
|
|
613
|
+
* True when the escape neither moves the cursor nor carries pixel data
|
|
614
|
+
* (kitty `a=p,C=1` placements). Cursor-neutral sequences can be emitted
|
|
615
|
+
* from the component's first row; cursor-advancing protocols
|
|
616
|
+
* (iTerm2/SIXEL) must draw from the last reserved row instead.
|
|
617
|
+
*/
|
|
618
|
+
cursorNeutral?: boolean;
|
|
619
|
+
}
|
|
620
|
+
|
|
488
621
|
export function renderImage(
|
|
489
622
|
base64Data: string,
|
|
490
623
|
imageDimensions: ImageDimensions,
|
|
491
624
|
options: ImageRenderOptions = {},
|
|
492
|
-
):
|
|
625
|
+
): RenderedImage | null {
|
|
493
626
|
if (!TERMINAL.imageProtocol) {
|
|
494
627
|
return null;
|
|
495
628
|
}
|
|
@@ -498,11 +631,20 @@ export function renderImage(
|
|
|
498
631
|
const fit = calculateImageFit(imageDimensions, options, cellDims);
|
|
499
632
|
|
|
500
633
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
634
|
+
const imageId = options.imageId ?? kittyImageId(base64Data);
|
|
635
|
+
const placementId = options.placementId ?? 1;
|
|
636
|
+
// Upload data once per image id (out-of-band; the transmit escape is
|
|
637
|
+
// cursor-neutral), then return only a tiny placement escape. Repaints
|
|
638
|
+
// re-emit just the placement, which replaces/moves that placement —
|
|
639
|
+
// re-sending data (a=T/a=t) for an existing id would delete the image
|
|
640
|
+
// and ALL of its placements (breaking sibling components showing the
|
|
641
|
+
// same content) and would re-send multi-MB payloads on every repaint.
|
|
642
|
+
if (!transmittedKittyImageIds.has(imageId)) {
|
|
643
|
+
transmittedKittyImageIds.add(imageId);
|
|
644
|
+
(options.onTransmit ?? kittyTransmitWriter)(encodeKittyTransmit(base64Data, imageId));
|
|
645
|
+
}
|
|
646
|
+
const sequence = encodeKittyPlacement({ imageId, placementId, columns: fit.columns, rows: fit.rows });
|
|
647
|
+
return { sequence, rows: fit.rows, cursorNeutral: true };
|
|
506
648
|
}
|
|
507
649
|
|
|
508
650
|
if (TERMINAL.imageProtocol === ImageProtocol.Sixel) {
|
package/src/terminal.ts
CHANGED
|
@@ -128,6 +128,9 @@ export interface Terminal {
|
|
|
128
128
|
// Whether terminal output is still writable
|
|
129
129
|
get available(): boolean;
|
|
130
130
|
|
|
131
|
+
// True for the real process stdin/stdout terminal (not virtual test terminals).
|
|
132
|
+
readonly isProcessTerminal?: boolean;
|
|
133
|
+
|
|
131
134
|
// Get terminal dimensions
|
|
132
135
|
get columns(): number;
|
|
133
136
|
get rows(): number;
|
|
@@ -238,6 +241,10 @@ export class ProcessTerminal implements Terminal {
|
|
|
238
241
|
#mode2031DebounceTimer?: Timer;
|
|
239
242
|
#progressTimer?: ReturnType<typeof setInterval>;
|
|
240
243
|
|
|
244
|
+
get isProcessTerminal(): boolean {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
241
248
|
get kittyProtocolActive(): boolean {
|
|
242
249
|
return this.#kittyProtocolActive;
|
|
243
250
|
}
|