@sayknow-cli/tui 0.6.4 → 0.6.6
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,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.6.
|
|
5
|
+
## [0.6.6] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `renderPetHalfBlocks(frame, skin, colorMode)` draws a Sayknow pet frame as half-block text (two pixel rows per terminal row, truecolor or 256 colors), for surfaces with no sixel/kitty image support.
|
|
6
10
|
|
|
7
11
|
## [0.5.12] - 2026-09-15
|
|
8
12
|
### Fixed
|
|
@@ -70,6 +70,12 @@ export declare class Editor implements Component, Focusable {
|
|
|
70
70
|
setBorderStyle(borderStyle: EditorBorderStyle): void;
|
|
71
71
|
setClosedBorderBox(closedBorderBox: boolean): void;
|
|
72
72
|
setPromptGutter(promptGutter: string | undefined): void;
|
|
73
|
+
/**
|
|
74
|
+
* Borderless rail: draw `glyph` in the current `borderColor` at the start of
|
|
75
|
+
* every input row (not just the first), followed by one space. Takes
|
|
76
|
+
* precedence over `setPromptGutter` and is ignored while the border is visible.
|
|
77
|
+
*/
|
|
78
|
+
setRailGutter(glyph: string | undefined): void;
|
|
73
79
|
setInputPrefix(inputPrefix: string | undefined): void;
|
|
74
80
|
setPlaceholder(placeholder: string | undefined): void;
|
|
75
81
|
/**
|
|
@@ -125,4 +125,11 @@ export declare function buildSayknowPixelFrames(options: {
|
|
|
125
125
|
/** Color skin for the sprite palette (default "red"). */
|
|
126
126
|
skin?: PetSkinId;
|
|
127
127
|
}): SayknowPixelFrames;
|
|
128
|
+
/**
|
|
129
|
+
* Draw a pet frame as text with half blocks, one terminal row per two pixel rows,
|
|
130
|
+
* so the same octopus shows up where no pixel protocol is available (the launch
|
|
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.
|
|
133
|
+
*/
|
|
134
|
+
export declare function renderPetHalfBlocks(frame: SayknowPixelFrameName, skin?: PetSkinId, colorMode?: "truecolor" | "256color"): string[];
|
|
128
135
|
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.
|
|
4
|
+
"version": "0.6.6",
|
|
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.
|
|
42
|
-
"@sayknow-cli/utils": "0.6.
|
|
41
|
+
"@sayknow-cli/natives": "0.6.6",
|
|
42
|
+
"@sayknow-cli/utils": "0.6.6",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -427,6 +427,7 @@ export class Editor implements Component, Focusable {
|
|
|
427
427
|
/** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
|
|
428
428
|
cursorOverrideWidth: number | undefined;
|
|
429
429
|
#promptGutter: string | undefined;
|
|
430
|
+
#railGutter: string | undefined;
|
|
430
431
|
#inputPrefix: string | undefined;
|
|
431
432
|
#inputPrefixWidth = 0;
|
|
432
433
|
#placeholder: string | undefined;
|
|
@@ -567,6 +568,16 @@ export class Editor implements Component, Focusable {
|
|
|
567
568
|
this.#invalidateLayoutCache();
|
|
568
569
|
}
|
|
569
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Borderless rail: draw `glyph` in the current `borderColor` at the start of
|
|
573
|
+
* every input row (not just the first), followed by one space. Takes
|
|
574
|
+
* precedence over `setPromptGutter` and is ignored while the border is visible.
|
|
575
|
+
*/
|
|
576
|
+
setRailGutter(glyph: string | undefined): void {
|
|
577
|
+
this.#railGutter = glyph;
|
|
578
|
+
this.#invalidateLayoutCache();
|
|
579
|
+
}
|
|
580
|
+
|
|
570
581
|
setInputPrefix(inputPrefix: string | undefined): void {
|
|
571
582
|
this.#inputPrefix = inputPrefix;
|
|
572
583
|
this.#inputPrefixWidth = inputPrefix ? visibleWidth(inputPrefix) : 0;
|
|
@@ -732,21 +743,32 @@ export class Editor implements Component, Focusable {
|
|
|
732
743
|
}
|
|
733
744
|
|
|
734
745
|
#getPromptGutterWidth(width: number, paddingX: number): number {
|
|
735
|
-
if (this.#borderVisible
|
|
746
|
+
if (this.#borderVisible) return 0;
|
|
747
|
+
const gutterWidth = this.#railGutter
|
|
748
|
+
? visibleWidth(this.#railGutter) + 1
|
|
749
|
+
: this.#promptGutter
|
|
750
|
+
? visibleWidth(this.#promptGutter)
|
|
751
|
+
: 0;
|
|
752
|
+
if (gutterWidth === 0) return 0;
|
|
736
753
|
const chromeWidth = 2 * this.#getHorizontalChromeWidth(paddingX);
|
|
737
754
|
const availableWidth = Math.max(0, width - chromeWidth);
|
|
738
|
-
return Math.min(
|
|
755
|
+
return Math.min(gutterWidth, availableWidth);
|
|
739
756
|
}
|
|
740
757
|
|
|
741
758
|
#getPromptGutter(
|
|
742
759
|
width: number,
|
|
743
760
|
paddingX: number,
|
|
744
761
|
): { firstLine: string; continuation: string; width: number } | undefined {
|
|
745
|
-
if (this.#borderVisible || !this.#promptGutter) return undefined;
|
|
762
|
+
if (this.#borderVisible || !(this.#railGutter || this.#promptGutter)) return undefined;
|
|
746
763
|
const gutterWidth = this.#getPromptGutterWidth(width, paddingX);
|
|
747
764
|
if (gutterWidth === 0) return undefined;
|
|
765
|
+
if (this.#railGutter) {
|
|
766
|
+
// Colored at render time so mode/thinking border colors reach the rail.
|
|
767
|
+
const rail = this.borderColor(sliceByColumn(`${this.#railGutter} `, 0, gutterWidth, true));
|
|
768
|
+
return { firstLine: rail, continuation: rail, width: gutterWidth };
|
|
769
|
+
}
|
|
748
770
|
return {
|
|
749
|
-
firstLine: sliceByColumn(this.#promptGutter, 0, gutterWidth, true),
|
|
771
|
+
firstLine: sliceByColumn(this.#promptGutter ?? "", 0, gutterWidth, true),
|
|
750
772
|
continuation: padding(gutterWidth),
|
|
751
773
|
width: gutterWidth,
|
|
752
774
|
};
|
|
@@ -1559,6 +1581,7 @@ export class Editor implements Component, Focusable {
|
|
|
1559
1581
|
inputPrefixWidth: this.#inputPrefixWidth,
|
|
1560
1582
|
placeholder: this.#placeholder,
|
|
1561
1583
|
promptGutter: this.#promptGutter,
|
|
1584
|
+
railGutter: this.#railGutter,
|
|
1562
1585
|
useTerminalCursor: this.#useTerminalCursor,
|
|
1563
1586
|
cursorOverride: this.cursorOverride,
|
|
1564
1587
|
cursorOverrideWidth: this.cursorOverrideWidth,
|
|
@@ -433,3 +433,42 @@ export function buildSayknowPixelFrames(options: {
|
|
|
433
433
|
|
|
434
434
|
return { frames, protocol: options.protocol, widthPx, heightPx, columns, rows, rasterRows };
|
|
435
435
|
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Draw a pet frame as text with half blocks, one terminal row per two pixel rows,
|
|
439
|
+
* so the same octopus shows up where no pixel protocol is available (the launch
|
|
440
|
+
* card). Fully transparent pixel rows at the top and bottom are cropped; columns
|
|
441
|
+
* are kept so every frame of the animation keeps the same width.
|
|
442
|
+
*/
|
|
443
|
+
export function renderPetHalfBlocks(
|
|
444
|
+
frame: SayknowPixelFrameName,
|
|
445
|
+
skin: PetSkinId = "red",
|
|
446
|
+
colorMode: "truecolor" | "256color" = "truecolor",
|
|
447
|
+
): string[] {
|
|
448
|
+
const grid = PIXEL_GRIDS[frame];
|
|
449
|
+
const palette = PET_SKINS[skin].palette;
|
|
450
|
+
const opaque = (row: string): boolean => [...row].some(ch => palette[ch] != null);
|
|
451
|
+
let top = 0;
|
|
452
|
+
while (top < grid.length && !opaque(grid[top]!)) top++;
|
|
453
|
+
let bottom = grid.length - 1;
|
|
454
|
+
while (bottom > top && !opaque(grid[bottom]!)) bottom--;
|
|
455
|
+
const format = colorMode === "truecolor" ? "ansi-16m" : "ansi-256";
|
|
456
|
+
const fg = (rgb: Rgb): string => Bun.color(`rgb(${rgb[0]},${rgb[1]},${rgb[2]})`, format) ?? "";
|
|
457
|
+
const bg = (rgb: Rgb): string => fg(rgb).replace("\x1b[38;", "\x1b[48;");
|
|
458
|
+
const lines: string[] = [];
|
|
459
|
+
for (let row = top; row <= bottom; row += 2) {
|
|
460
|
+
const upper = grid[row]!;
|
|
461
|
+
const lower = row + 1 <= bottom ? grid[row + 1]! : "";
|
|
462
|
+
let line = "";
|
|
463
|
+
for (let col = 0; col < upper.length; col++) {
|
|
464
|
+
const up = palette[upper[col]!] ?? null;
|
|
465
|
+
const down = palette[lower[col] ?? "."] ?? null;
|
|
466
|
+
if (!up && !down) line += " ";
|
|
467
|
+
else if (up && down) line += `${fg(up)}${bg(down)}▀\x1b[0m`;
|
|
468
|
+
else if (up) line += `${fg(up)}▀\x1b[0m`;
|
|
469
|
+
else line += `${fg(down!)}▄\x1b[0m`;
|
|
470
|
+
}
|
|
471
|
+
lines.push(line);
|
|
472
|
+
}
|
|
473
|
+
return lines;
|
|
474
|
+
}
|