@sayknow-cli/tui 0.6.7 → 0.6.8

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.8] - 2026-09-29
6
+
7
+ ### Changed
8
+
9
+ - `renderPetHalfBlocks(..., { scale: "compact" })` draws the pet's own sprite downscaled to 12×5 (eyes kept by feature-first pixel merging), replacing the redrawn `"half"` octopus. Every pose has the same height.
10
+
5
11
  ## [0.6.7] - 2026-09-28
6
12
 
7
13
  ### Added
@@ -129,10 +129,10 @@ export declare function buildSayknowPixelFrames(options: {
129
129
  * Draw a pet frame as text with half blocks, one terminal row per two pixel rows,
130
130
  * so the same octopus shows up where no pixel protocol is available (the launch
131
131
  * card). Fully transparent pixel rows at the top and bottom are cropped; columns
132
- * are kept so every frame of the animation keeps the same width. `scale: "half"`
133
- * draws the small octopus (8 columns, 4 rows); frames it has no pose for use its base.
132
+ * are kept so every frame of the animation keeps the same width. `scale: "compact"`
133
+ * draws the same sprite at three quarters size (12 columns, 5 rows).
134
134
  */
135
135
  export declare function renderPetHalfBlocks(frame: SayknowPixelFrameName, skin?: PetSkinId, colorMode?: "truecolor" | "256color", options?: {
136
- scale?: "full" | "half";
136
+ scale?: "full" | "compact";
137
137
  }): string[];
138
138
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/tui",
4
- "version": "0.6.7",
4
+ "version": "0.6.8",
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.7",
42
- "@sayknow-cli/utils": "0.6.7",
41
+ "@sayknow-cli/natives": "0.6.8",
42
+ "@sayknow-cli/utils": "0.6.8",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -434,49 +434,76 @@ export function buildSayknowPixelFrames(options: {
434
434
  return { frames, protocol: options.protocol, widthPx, heightPx, columns, rows, rasterRows };
435
435
  }
436
436
 
437
- // 8x8 small octopus for compact surfaces (the launch card): the same palette and
438
- // features — rounded mantle with a highlight, white eyes over dark pupils, a row of
439
- // tentacles — drawn for the size rather than downsampled, which turns the dome blocky.
440
- // biome-ignore format: pixel grid stays one row per line
441
- const SMALL_BASE = [
442
- "..KKKK..",
443
- ".KRrrRK.",
444
- "KRRRRRRK",
445
- "KRWRRWRK",
446
- "KRVRRVRK",
447
- "KRRRRRRK",
448
- "KRKRRKRK",
449
- "K.K..K.K",
450
- ];
451
- const SMALL_GRIDS: Partial<Record<SayknowPixelFrameName, string[]>> = {
452
- base: SMALL_BASE,
453
- danceL: mix(SMALL_BASE, { 6: "RKRRKRK.", 7: ".K..K.K." }),
454
- danceR: mix(SMALL_BASE, { 6: ".KRKRRKR", 7: ".K.K..K." }),
455
- gazeL: mix(SMALL_BASE, { 3: "KWRRWRRK", 4: "KVRRVRRK" }),
456
- gazeR: mix(SMALL_BASE, { 3: "KRRWRRWK", 4: "KRRVRRVK" }),
457
- flicker: mix(SMALL_BASE, { 3: "KRRRRRRK", 4: "KRKRRKRK" }),
458
- };
437
+ /**
438
+ * Pixel priority when several pixels merge into one on downscale: features first, so
439
+ * the eyes survive (a block holding a pupil stays a pupil), then outline over body.
440
+ */
441
+ const DOWNSCALE_PRIORITY = "VWGwKrRbHhA";
442
+
443
+ /**
444
+ * The pet's own sprite, scaled to `size` columns (rows in proportion; nearest block, feature-first),
445
+ * so compact surfaces show the same octopus rather than a redraw. A block becomes
446
+ * transparent only when most of it is.
447
+ */
448
+ function downscaleGrid(grid: readonly string[], size: number, palette: Palette, rowCount?: number): string[] {
449
+ const sourceRows = grid.length;
450
+ const source = grid[0]?.length ?? 0;
451
+ // Rows in proportion to columns unless a height is asked for.
452
+ const rows = rowCount ?? Math.round((sourceRows * size) / source);
453
+ const out: string[] = [];
454
+ for (let row = 0; row < rows; row++) {
455
+ const rowStart = Math.floor((row * sourceRows) / rows);
456
+ const rowEnd = Math.floor(((row + 1) * sourceRows) / rows);
457
+ let line = "";
458
+ for (let col = 0; col < size; col++) {
459
+ const colStart = Math.floor((col * source) / size);
460
+ const colEnd = Math.floor(((col + 1) * source) / size);
461
+ const cells: string[] = [];
462
+ for (let y = rowStart; y < rowEnd; y++) for (let x = colStart; x < colEnd; x++) cells.push(grid[y]![x] ?? ".");
463
+ const opaque = cells.filter(ch => palette[ch] != null);
464
+ line +=
465
+ opaque.length * 2 < cells.length
466
+ ? "."
467
+ : ([...DOWNSCALE_PRIORITY].find(ch => opaque.includes(ch)) ?? opaque[0]!);
468
+ }
469
+ out.push(line);
470
+ }
471
+ return out;
472
+ }
473
+
474
+ /** Pixel size of the compact pet: three quarters of the 16×16 sprite (12 columns, 5 rows). */
475
+ const COMPACT_PET_PIXELS = 12;
476
+ /** Terminal rows of the compact pet (two pixel rows each). */
477
+ const COMPACT_PET_ROWS = 5;
459
478
 
460
479
  /**
461
480
  * Draw a pet frame as text with half blocks, one terminal row per two pixel rows,
462
481
  * so the same octopus shows up where no pixel protocol is available (the launch
463
482
  * card). Fully transparent pixel rows at the top and bottom are cropped; columns
464
- * are kept so every frame of the animation keeps the same width. `scale: "half"`
465
- * draws the small octopus (8 columns, 4 rows); frames it has no pose for use its base.
483
+ * are kept so every frame of the animation keeps the same width. `scale: "compact"`
484
+ * draws the same sprite at three quarters size (12 columns, 5 rows).
466
485
  */
467
486
  export function renderPetHalfBlocks(
468
487
  frame: SayknowPixelFrameName,
469
488
  skin: PetSkinId = "red",
470
489
  colorMode: "truecolor" | "256color" = "truecolor",
471
- options: { scale?: "full" | "half" } = {},
490
+ options: { scale?: "full" | "compact" } = {},
472
491
  ): string[] {
473
492
  const palette = PET_SKINS[skin].palette;
474
- const grid = options.scale === "half" ? (SMALL_GRIDS[frame] ?? SMALL_BASE) : PIXEL_GRIDS[frame];
493
+ const compact = options.scale === "compact";
494
+ // Compact: the sprite's own pixel rows without its empty top and bottom margins (the
495
+ // same for every pose), scaled to 12×10 pixels = 12 columns × 5 rows. Cropping per pose
496
+ // would change the height while the tentacles move and make the card jump.
497
+ const grid = compact
498
+ ? downscaleGrid(PIXEL_GRIDS[frame].slice(1, 15), COMPACT_PET_PIXELS, palette, COMPACT_PET_ROWS * 2)
499
+ : PIXEL_GRIDS[frame];
475
500
  const opaque = (row: string): boolean => [...row].some(ch => palette[ch] != null);
476
501
  let top = 0;
477
- while (top < grid.length && !opaque(grid[top]!)) top++;
478
502
  let bottom = grid.length - 1;
479
- while (bottom > top && !opaque(grid[bottom]!)) bottom--;
503
+ if (!compact) {
504
+ while (top < grid.length && !opaque(grid[top]!)) top++;
505
+ while (bottom > top && !opaque(grid[bottom]!)) bottom--;
506
+ }
480
507
  const format = colorMode === "truecolor" ? "ansi-16m" : "ansi-256";
481
508
  const fg = (rgb: Rgb): string => Bun.color(`rgb(${rgb[0]},${rgb[1]},${rgb[2]})`, format) ?? "";
482
509
  const bg = (rgb: Rgb): string => fg(rgb).replace("\x1b[38;", "\x1b[48;");