@sayknow-cli/tui 0.6.9 → 0.7.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.1] - 2026-09-29
6
+
7
+ ## [0.7.0] - 2026-09-29
8
+
9
+ ### Added
10
+
11
+ - `renderSayoPose` / `decodePng` (sayo-sprite): Sayo mascot poses and tints rendered from an embedded image; `prepareSayoPlaceholderImages` (sayo-placeholder): kitty Unicode-placeholder images for text lines.
12
+ - Pet skin `orange` (Sayo), now first in `PET_SKIN_IDS`; kitty pet frames draw the Sayo image and are sent with `o=z`.
13
+
5
14
  ## [0.6.9] - 2026-09-29
6
15
 
7
16
  ### Changed
@@ -1,53 +1,9 @@
1
- /**
2
- * ┌─ SAYKNOW PET SPRITE SPEC ────────────────────────────────────────────────┐
3
- * The pet is a 16×16 pixel octopus drawn beside the composer. Everything here
4
- * is data: no PNGs, no assets — each frame is 16 strings of 16 chars, encoded
5
- * to a sixel or kitty escape at runtime. Author a new frame by drawing a grid.
6
- *
7
- * GRID RULES
8
- * - Exactly 16 rows × 16 columns. Only PALETTE keys below are valid chars.
9
- * - `.` = transparent. Keep the outer columns transparent so the sprite sits
10
- * snug beside the input box (the widget reserves +1 column of slack).
11
- *
12
- * PALETTE (char → role) — see PALETTE for exact RGB:
13
- * .=transparent K=dark outline R=mantle body r=body highlight
14
- * W=eye white V=pupil G=eye sparkle b=underside
15
- * w=tear H h A=reserved (unused by the octopus art)
16
- *
17
- * FRAME CATALOG (SayknowPixelFrameName → PIXEL_GRIDS):
18
- * base idle rest; also the dance "drop/settle" beat
19
- * gazeL eyes glance left ┐ idle loop (see sayknow-pet-widget IDLE_LOOP)
20
- * gazeR eyes glance right │
21
- * flicker eyes blink ┘
22
- * flex sparkly "yay" eyes; dance accent + random idle flex burst
23
- * danceL tentacles sway left ┐ work loop (PARA_PARA_STEPS)
24
- * danceR tentacles sway right ┘
25
- * cry1..3 a tear trails from the outer eye corners (BlueOcto sob)
26
- *
27
- * RENDERING: buildSayknowPixelFrames({ protocol, cellWidthPx, cellHeightPx,
28
- * targetRows: 2 }) scales the art to 2 terminal rows and encodes each frame
29
- * once. Kitty uses a native `Y=` sub-cell drop (set by the widget) to sit on the
30
- * composer border; sixel uses transparent top padding.
31
- *
32
- * BEHAVIOR (timing, positioning, on/off) lives in
33
- * packages/coding-agent/src/modes/components/sayknow-pet-widget.ts.
34
- *
35
- * ADD A FRAME: draw the grid → add its name to SayknowPixelFrameName → register it in
36
- * PIXEL_GRIDS → reference it from an idle/work loop or a skin burst.
37
- *
38
- * ADD A PET (skin): append one entry to PET_SKINS below — { id, label, description,
39
- * palette, burst }. The id flows into PetSkinId/PetMode automatically, the settings
40
- * enum, `/pet` command and both selectors derive their options from PET_SKINS, and the
41
- * widget reads `burst` to animate — no other file needs editing. Recolor with a palette
42
- * spread (see BLUE_PALETTE); add frames only for poses the catalog lacks.
43
- * └────────────────────────────────────────────────────────────────────────┘
44
- */
45
1
  type Rgb = readonly [number, number, number];
46
2
  export type Palette = Record<string, Rgb | null>;
47
- export declare const PET_SKIN_IDS: readonly ["red", "blue"];
3
+ export declare const PET_SKIN_IDS: readonly ["orange", "red", "blue"];
48
4
  export type PetSkinId = (typeof PET_SKIN_IDS)[number];
49
5
  /** Every pet mode: "off" plus each skin id, in menu order. */
50
- export declare const PET_MODE_IDS: readonly ["off", "red", "blue"];
6
+ export declare const PET_MODE_IDS: readonly ["off", "orange", "red", "blue"];
51
7
  export type PetMode = (typeof PET_MODE_IDS)[number];
52
8
  /** Narrow an arbitrary string to a PetMode. */
53
9
  export declare function isPetMode(value: string): value is PetMode;
@@ -93,6 +49,12 @@ export declare const __sayknowPetTestHooks: {
93
49
  };
94
50
  /** Encode a grid as a transparent SIXEL image, optionally bottom-aligned by top padding. */
95
51
  export declare function encodeGridSixel(grid: string[], scale: number, topPaddingPx?: number, palette?: Palette): string;
52
+ /**
53
+ * Kitty escape for a straight-RGBA canvas already laid out to the placement box: delete
54
+ * the previous image with this id, then transmit and display the new one in `cols`×`rows`
55
+ * cells. `Y=` drops it by sub-cell pixels; `C=1` keeps the cursor where it was.
56
+ */
57
+ export declare function encodeRgbaKitty(rgba: Uint8Array, w: number, h: number, imageId: number, cols: number, rows: number, cellYOffsetPx?: number): string;
96
58
  /** Encode a bottom-aligned grid as kitty raw RGBA at `scale`. */
97
59
  export declare function encodeGridKitty(grid: string[], scale: number, imageId: number, cols: number, rows: number, topPaddingPx?: number, cellYOffsetPx?: number, leftPaddingPx?: number, rightPaddingPx?: number, palette?: Palette): string;
98
60
  export interface SayknowPixelFrames {
@@ -122,7 +84,7 @@ export declare function buildSayknowPixelFrames(options: {
122
84
  /** Native sub-cell `Y=` pixel offset that drops the kitty sprite within its first cell. */
123
85
  kittyCellYOffsetPx?: number;
124
86
  kittyImageId?: number;
125
- /** Color skin for the sprite palette (default "red"). */
87
+ /** Color skin (default "orange", Sayo). */
126
88
  skin?: PetSkinId;
127
89
  }): SayknowPixelFrames;
128
90
  /**
@@ -0,0 +1,23 @@
1
+ import { type SayoPose, type SayoTint } from "./sayo-sprite";
2
+ /** Largest image this module can address in cells. */
3
+ export declare const SAYO_PLACEHOLDER_MAX_CELLS: number;
4
+ export interface SayoPlaceholderImages {
5
+ columns: number;
6
+ rows: number;
7
+ /** Escape payload that uploads every pose and creates its virtual placement. */
8
+ upload: string;
9
+ /** Text row `row` of `pose`: placeholder cells colored with that pose's image id. */
10
+ line(pose: SayoPose, row: number): string;
11
+ }
12
+ /**
13
+ * Prepare `poses` of Sayo in `tint`, `rows` cells tall and square in pixels. Image ids
14
+ * are `idBase + index` and must fit in 24 bits (the id travels as an RGB color).
15
+ */
16
+ export declare function prepareSayoPlaceholderImages(options: {
17
+ tint: SayoTint;
18
+ rows: number;
19
+ cellWidthPx: number;
20
+ cellHeightPx: number;
21
+ poses: readonly SayoPose[];
22
+ idBase: number;
23
+ }): SayoPlaceholderImages;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sayo, the Sayknow octopus mascot (Sayknow Kit `public/sayo-logo.png`), as a 200×200
3
+ * RGBA PNG. Embedded as text so single-file builds carry it; decoded once at runtime by
4
+ * sayo-sprite.ts, which draws every pose and color from it.
5
+ */
6
+ export declare const SAYO_SOURCE_PNG_BASE64: string;
@@ -0,0 +1,13 @@
1
+ export type SayoPose = "base" | "gazeL" | "gazeR" | "flicker" | "flex" | "danceL" | "danceR" | "cry1" | "cry2" | "cry3";
2
+ export type SayoTint = "orange" | "red" | "blue";
3
+ /** Straight (non-premultiplied) RGBA pixels. */
4
+ export interface RgbaImage {
5
+ width: number;
6
+ height: number;
7
+ data: Uint8Array;
8
+ }
9
+ export declare function decodePng(bytes: Uint8Array): RgbaImage;
10
+ /** Render one pose, tinted, into a `size`×`size` straight-RGBA image. */
11
+ export declare function renderSayoPose(pose: SayoPose, tint: SayoTint, size: number): RgbaImage;
12
+ /** Every pose the pet state machine names. */
13
+ export declare const SAYO_POSES: SayoPose[];
@@ -8,6 +8,8 @@ export * from "./components/input";
8
8
  export * from "./components/loader";
9
9
  export * from "./components/markdown";
10
10
  export * from "./components/sayknow-pet";
11
+ export * from "./components/sayo-placeholder";
12
+ export * from "./components/sayo-sprite";
11
13
  export * from "./components/secret-input";
12
14
  export * from "./components/select-list";
13
15
  export * from "./components/settings-list";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/tui",
4
- "version": "0.6.9",
4
+ "version": "0.7.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@sayknow-cli/natives": "0.6.9",
42
- "@sayknow-cli/utils": "0.6.9",
41
+ "@sayknow-cli/natives": "0.7.1",
42
+ "@sayknow-cli/utils": "0.7.1",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * ┌─ SAYKNOW PET SPRITE SPEC ────────────────────────────────────────────────┐
3
- * The pet is a 16×16 pixel octopus drawn beside the composer. Everything here
4
- * is data: no PNGs, no assets — each frame is 16 strings of 16 chars, encoded
5
- * to a sixel or kitty escape at runtime. Author a new frame by drawing a grid.
3
+ * The pet is Sayo, the Sayknow octopus, beside the composer. Kitty-graphics terminals
4
+ * (Ghostty, kitty, WezTerm) draw the Sayo image itself, one transformed image per pose
5
+ * (sayo-sprite.ts). Sixel terminals and the half-block text fallback draw the 16×16
6
+ * pixel octopus below instead: each frame is 16 strings of 16 chars, encoded at runtime.
7
+ * A new pose needs both: a grid here and a transform in sayo-sprite.ts POSES.
6
8
  *
7
9
  * GRID RULES
8
10
  * - Exactly 16 rows × 16 columns. Only PALETTE keys below are valid chars.
@@ -42,10 +44,13 @@
42
44
  * spread (see BLUE_PALETTE); add frames only for poses the catalog lacks.
43
45
  * └────────────────────────────────────────────────────────────────────────┘
44
46
  */
47
+ import { deflateSync } from "node:zlib";
48
+ import { renderSayoPose } from "./sayo-sprite";
49
+
45
50
  type Rgb = readonly [number, number, number];
46
51
 
47
52
  export type Palette = Record<string, Rgb | null>;
48
- export const PET_SKIN_IDS = ["red", "blue"] as const;
53
+ export const PET_SKIN_IDS = ["orange", "red", "blue"] as const;
49
54
  export type PetSkinId = (typeof PET_SKIN_IDS)[number];
50
55
  /** Every pet mode: "off" plus each skin id, in menu order. */
51
56
  export const PET_MODE_IDS = ["off", ...PET_SKIN_IDS] as const;
@@ -69,6 +74,16 @@ const RED_PALETTE: Palette = {
69
74
  A: [196, 60, 30], // reserved
70
75
  w: [200, 230, 255], // tear (BlueOcto sob)
71
76
  };
77
+ // Sayo's orange for the dot fallback (sixel and text terminals); kitty terminals draw
78
+ // the Sayo image itself (sayo-sprite.ts).
79
+ const ORANGE_PALETTE: Palette = {
80
+ ...RED_PALETTE,
81
+ K: [120, 40, 10], // outline
82
+ R: [255, 122, 30], // mantle body
83
+ r: [255, 176, 100], // highlight
84
+ b: [232, 70, 40], // underside
85
+ A: [214, 80, 20], // reserved
86
+ };
72
87
  // BlueOcto recolors the octopus for the blue-octopus theme: ocean outline, a bright
73
88
  // mantle, azure highlight and foam tears. Reserved hat keys are shared but unused.
74
89
  const BLUE_PALETTE: Palette = {
@@ -194,6 +209,16 @@ export interface PetSkin {
194
209
 
195
210
  /** Skin registry — the single source for palettes, behavior and selector/command copy. */
196
211
  export const PET_SKINS: Record<PetSkinId, PetSkin> = {
212
+ orange: {
213
+ id: "orange",
214
+ label: "Sayo",
215
+ description: "Sayo, the orange Sayknow octopus.",
216
+ palette: ORANGE_PALETTE,
217
+ burst: {
218
+ intro: PARA_PARA_STEPS,
219
+ tail: { frames: ["flex", "base"], stepMs: 220, ms: 880 },
220
+ },
221
+ },
197
222
  red: {
198
223
  id: "red",
199
224
  label: "RedOctopus",
@@ -306,6 +331,35 @@ export function encodeGridSixel(
306
331
  return `${out}\x1b\\`;
307
332
  }
308
333
 
334
+ /**
335
+ * Kitty escape for a straight-RGBA canvas already laid out to the placement box: delete
336
+ * the previous image with this id, then transmit and display the new one in `cols`×`rows`
337
+ * cells. `Y=` drops it by sub-cell pixels; `C=1` keeps the cursor where it was.
338
+ */
339
+ export function encodeRgbaKitty(
340
+ rgba: Uint8Array,
341
+ w: number,
342
+ h: number,
343
+ imageId: number,
344
+ cols: number,
345
+ rows: number,
346
+ cellYOffsetPx = 0,
347
+ ): string {
348
+ // zlib-compressed (`o=z`): a mostly transparent sprite shrinks several-fold.
349
+ const data = deflateSync(rgba).toString("base64");
350
+ const CHUNK = 4000;
351
+ const yParam = cellYOffsetPx > 0 ? `,Y=${Math.round(cellYOffsetPx)}` : "";
352
+ let out = `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
353
+ for (let off = 0, first = true; off < data.length; off += CHUNK, first = false) {
354
+ const chunk = data.slice(off, off + CHUNK);
355
+ const more = off + CHUNK < data.length ? 1 : 0;
356
+ out += first
357
+ ? `\x1b_Ga=T,f=32,o=z,s=${w},v=${h},c=${cols},r=${rows},i=${imageId},q=2,C=1${yParam},m=${more};${chunk}\x1b\\`
358
+ : `\x1b_Gm=${more};${chunk}\x1b\\`;
359
+ }
360
+ return out;
361
+ }
362
+
309
363
  /** Encode a bottom-aligned grid as kitty raw RGBA at `scale`. */
310
364
  export function encodeGridKitty(
311
365
  grid: string[],
@@ -345,21 +399,36 @@ export function encodeGridKitty(
345
399
  rgba[o + 3] = 255;
346
400
  }
347
401
  }
348
- const data = Buffer.from(rgba).toString("base64");
349
- const CHUNK = 4000;
350
- // `Y=` offsets the sprite down by sub-cell pixels within the first cell — the
351
- // kitty analogue of the sixel top-padding drop. `C=1` keeps the placement
352
- // cursor-neutral so the overlay never nudges the composer's real cursor.
353
- const yParam = cellYOffsetPx > 0 ? `,Y=${Math.round(cellYOffsetPx)}` : "";
354
- let out = `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
355
- for (let off = 0, first = true; off < data.length; off += CHUNK, first = false) {
356
- const chunk = data.slice(off, off + CHUNK);
357
- const more = off + CHUNK < data.length ? 1 : 0;
358
- out += first
359
- ? `\x1b_Ga=T,f=32,s=${w},v=${h},c=${cols},r=${rows},i=${imageId},q=2,C=1${yParam},m=${more};${chunk}\x1b\\`
360
- : `\x1b_Gm=${more};${chunk}\x1b\\`;
402
+ return encodeRgbaKitty(rgba, w, h, imageId, cols, rows, cellYOffsetPx);
403
+ }
404
+
405
+ /**
406
+ * Sayo for kitty: the pose rendered at the sprite size and placed in the padded box the
407
+ * grid frames use, so placement, the sub-cell drop and cleanup are unchanged.
408
+ */
409
+ function encodeSayoKitty(
410
+ pose: SayknowPixelFrameName,
411
+ skin: PetSkinId,
412
+ spritePx: number,
413
+ imageId: number,
414
+ cols: number,
415
+ rows: number,
416
+ topPaddingPx: number,
417
+ cellYOffsetPx: number,
418
+ leftPaddingPx: number,
419
+ rightPaddingPx: number,
420
+ ): string {
421
+ const sprite = renderSayoPose(pose, skin, spritePx);
422
+ const w = spritePx + leftPaddingPx + rightPaddingPx;
423
+ const h = spritePx + topPaddingPx;
424
+ const rgba = new Uint8Array(w * h * 4);
425
+ for (let y = 0; y < spritePx; y++) {
426
+ rgba.set(
427
+ sprite.data.subarray(y * spritePx * 4, (y + 1) * spritePx * 4),
428
+ ((y + topPaddingPx) * w + leftPaddingPx) * 4,
429
+ );
361
430
  }
362
- return out;
431
+ return encodeRgbaKitty(rgba, w, h, imageId, cols, rows, cellYOffsetPx);
363
432
  }
364
433
 
365
434
  export interface SayknowPixelFrames {
@@ -390,7 +459,7 @@ export function buildSayknowPixelFrames(options: {
390
459
  /** Native sub-cell `Y=` pixel offset that drops the kitty sprite within its first cell. */
391
460
  kittyCellYOffsetPx?: number;
392
461
  kittyImageId?: number;
393
- /** Color skin for the sprite palette (default "red"). */
462
+ /** Color skin (default "orange", Sayo). */
394
463
  skin?: PetSkinId;
395
464
  }): SayknowPixelFrames {
396
465
  const targetRows = options.targetRows ?? 2;
@@ -411,15 +480,16 @@ export function buildSayknowPixelFrames(options: {
411
480
  const leftPaddingPx = Math.floor(horizontalPaddingPx / 2);
412
481
  const rightPaddingPx = horizontalPaddingPx - leftPaddingPx;
413
482
  const imageId = options.kittyImageId ?? 0xc0de;
414
- const palette = PET_SKINS[options.skin ?? "red"].palette;
483
+ const palette = PET_SKINS[options.skin ?? "orange"].palette;
415
484
  const frames = {} as Record<SayknowPixelFrameName, string>;
416
485
  for (const name of Object.keys(PIXEL_GRIDS) as SayknowPixelFrameName[]) {
417
486
  frames[name] =
418
487
  options.protocol === "sixel"
419
488
  ? encodeGridSixel(PIXEL_GRIDS[name], scale, topPaddingPx, palette)
420
- : encodeGridKitty(
421
- PIXEL_GRIDS[name],
422
- scale,
489
+ : encodeSayoKitty(
490
+ name,
491
+ options.skin ?? "orange",
492
+ widthPx,
423
493
  imageId,
424
494
  columns,
425
495
  rows,
@@ -427,7 +497,6 @@ export function buildSayknowPixelFrames(options: {
427
497
  options.kittyCellYOffsetPx ?? 0,
428
498
  leftPaddingPx,
429
499
  rightPaddingPx,
430
- palette,
431
500
  );
432
501
  }
433
502
 
@@ -485,7 +554,7 @@ const COMPACT_PET_ROWS = 5;
485
554
  */
486
555
  export function renderPetHalfBlocks(
487
556
  frame: SayknowPixelFrameName,
488
- skin: PetSkinId = "red",
557
+ skin: PetSkinId = "orange",
489
558
  colorMode: "truecolor" | "256color" = "truecolor",
490
559
  options: { scale?: "full" | "compact" } = {},
491
560
  ): string[] {
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Sayo in text lines, through kitty graphics Unicode placeholders (Ghostty, kitty).
3
+ *
4
+ * Each pose is uploaded once as an image with a virtual placement (`U=1`). A line then
5
+ * shows it by writing placeholder cells (U+10EEEE plus row/column diacritics) in a
6
+ * foreground color that encodes the image id. The image lives in ordinary text: it
7
+ * scrolls, redraws and clears with the transcript, and switching poses is a color change
8
+ * on those cells rather than a new image.
9
+ */
10
+ import { deflateSync } from "node:zlib";
11
+ import { renderSayoPose, type SayoPose, type SayoTint } from "./sayo-sprite";
12
+
13
+ const PLACEHOLDER = String.fromCodePoint(0x10eeee);
14
+
15
+ /** Kitty's row/column diacritics (rowcolumn-diacritics.txt), enough for small images. */
16
+ const DIACRITICS = [
17
+ 0x0305, 0x030d, 0x030e, 0x0310, 0x0312, 0x033d, 0x033e, 0x033f, 0x0346, 0x034a, 0x034b, 0x034c, 0x0350, 0x0351,
18
+ 0x0352, 0x0357, 0x035b, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d,
19
+ 0x036e, 0x036f,
20
+ ].map(code => String.fromCodePoint(code));
21
+
22
+ /** Largest image this module can address in cells. */
23
+ export const SAYO_PLACEHOLDER_MAX_CELLS = DIACRITICS.length;
24
+
25
+ export interface SayoPlaceholderImages {
26
+ columns: number;
27
+ rows: number;
28
+ /** Escape payload that uploads every pose and creates its virtual placement. */
29
+ upload: string;
30
+ /** Text row `row` of `pose`: placeholder cells colored with that pose's image id. */
31
+ line(pose: SayoPose, row: number): string;
32
+ }
33
+
34
+ function kittyChunks(control: string, base64: string): string {
35
+ const CHUNK = 4096;
36
+ let out = "";
37
+ for (let offset = 0, first = true; offset < base64.length || first; offset += CHUNK, first = false) {
38
+ const chunk = base64.slice(offset, offset + CHUNK);
39
+ const more = offset + CHUNK < base64.length ? 1 : 0;
40
+ out += first ? `\x1b_G${control},m=${more};${chunk}\x1b\\` : `\x1b_Gm=${more};${chunk}\x1b\\`;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /**
46
+ * Prepare `poses` of Sayo in `tint`, `rows` cells tall and square in pixels. Image ids
47
+ * are `idBase + index` and must fit in 24 bits (the id travels as an RGB color).
48
+ */
49
+ export function prepareSayoPlaceholderImages(options: {
50
+ tint: SayoTint;
51
+ rows: number;
52
+ cellWidthPx: number;
53
+ cellHeightPx: number;
54
+ poses: readonly SayoPose[];
55
+ idBase: number;
56
+ }): SayoPlaceholderImages {
57
+ const rows = Math.max(1, Math.min(SAYO_PLACEHOLDER_MAX_CELLS, Math.round(options.rows)));
58
+ const side = rows * options.cellHeightPx;
59
+ const columns = Math.max(1, Math.min(SAYO_PLACEHOLDER_MAX_CELLS, Math.round(side / options.cellWidthPx)));
60
+ const width = columns * options.cellWidthPx;
61
+ const height = rows * options.cellHeightPx;
62
+ const left = Math.max(0, Math.floor((width - side) / 2));
63
+ const ids = new Map<SayoPose, number>();
64
+ let upload = "";
65
+ for (const [index, pose] of options.poses.entries()) {
66
+ const id = (options.idBase + index) & 0xffffff;
67
+ ids.set(pose, id);
68
+ const sprite = renderSayoPose(pose, options.tint, Math.min(side, width));
69
+ const rgba = new Uint8Array(width * height * 4);
70
+ const top = height - sprite.height;
71
+ for (let y = 0; y < sprite.height; y++) {
72
+ rgba.set(
73
+ sprite.data.subarray(y * sprite.width * 4, (y + 1) * sprite.width * 4),
74
+ ((y + top) * width + left) * 4,
75
+ );
76
+ }
77
+ const data = deflateSync(rgba).toString("base64");
78
+ upload += `\x1b_Ga=d,d=I,i=${id},q=2\x1b\\`;
79
+ upload += kittyChunks(`a=t,f=32,o=z,s=${width},v=${height},i=${id},q=2`, data);
80
+ upload += `\x1b_Ga=p,U=1,i=${id},c=${columns},r=${rows},q=2\x1b\\`;
81
+ }
82
+ const rowCache = new Map<string, string>();
83
+ return {
84
+ columns,
85
+ rows,
86
+ upload,
87
+ line(pose, row) {
88
+ const id = ids.get(pose) ?? ids.values().next().value ?? options.idBase;
89
+ const key = `${id}:${row}`;
90
+ let text = rowCache.get(key);
91
+ if (text === undefined) {
92
+ const color = `\x1b[38;2;${(id >> 16) & 255};${(id >> 8) & 255};${id & 255}m`;
93
+ let cells = "";
94
+ for (let column = 0; column < columns; column++)
95
+ cells += `${PLACEHOLDER}${DIACRITICS[row]}${DIACRITICS[column]}`;
96
+ text = `${color}${cells}\x1b[39m`;
97
+ rowCache.set(key, text);
98
+ }
99
+ return text;
100
+ },
101
+ };
102
+ }