@sayknow-cli/tui 0.6.5 → 0.6.7
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 +14 -1
- package/dist/types/components/editor.d.ts +8 -0
- package/dist/types/components/sayknow-pet.d.ts +10 -0
- package/dist/types/tui.d.ts +16 -0
- package/package.json +3 -3
- package/src/components/editor.ts +36 -0
- package/src/components/sayknow-pet.ts +64 -0
- package/src/tui.ts +59 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.6.
|
|
5
|
+
## [0.6.7] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Component.handleClick(line, column)`: top-level children that are not focused can take left clicks, hit-tested against the rows they rendered in the last frame; returning true consumes the click before selection and the focused component.
|
|
10
|
+
- `TUI.setMouseEnabled(enabled)`, `TUI.mouseEnabled` and `TUI.getFocusedComponent()`: switch terminal mouse reporting at runtime and ask which component has keyboard focus.
|
|
11
|
+
- `Editor.setInputBackground(sgr)` paints a background band under every borderless input row (held across colored spans and the cursor); `Editor.isBrowsingHistory()`.
|
|
12
|
+
- `renderPetHalfBlocks(..., { scale: "half" })` draws the small 8×4 octopus.
|
|
13
|
+
|
|
14
|
+
## [0.6.6] - 2026-09-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `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
19
|
|
|
7
20
|
## [0.5.12] - 2026-09-15
|
|
8
21
|
### Fixed
|
|
@@ -76,6 +76,14 @@ export declare class Editor implements Component, Focusable {
|
|
|
76
76
|
* precedence over `setPromptGutter` and is ignored while the border is visible.
|
|
77
77
|
*/
|
|
78
78
|
setRailGutter(glyph: string | undefined): void;
|
|
79
|
+
/**
|
|
80
|
+
* Paint a background band (an SGR sequence such as `\x1b[48;2;36;30;24m`) under
|
|
81
|
+
* every input row of a borderless editor, so the line being typed stands out from
|
|
82
|
+
* the transcript. Undefined removes it. Ignored while the border is visible.
|
|
83
|
+
*/
|
|
84
|
+
setInputBackground(background: string | undefined): void;
|
|
85
|
+
/** True while Up/Down are walking prompt history rather than editing. */
|
|
86
|
+
isBrowsingHistory(): boolean;
|
|
79
87
|
setInputPrefix(inputPrefix: string | undefined): void;
|
|
80
88
|
setPlaceholder(placeholder: string | undefined): void;
|
|
81
89
|
/**
|
|
@@ -125,4 +125,14 @@ 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. `scale: "half"`
|
|
133
|
+
* draws the small octopus (8 columns, 4 rows); frames it has no pose for use its base.
|
|
134
|
+
*/
|
|
135
|
+
export declare function renderPetHalfBlocks(frame: SayknowPixelFrameName, skin?: PetSkinId, colorMode?: "truecolor" | "256color", options?: {
|
|
136
|
+
scale?: "full" | "half";
|
|
137
|
+
}): string[];
|
|
128
138
|
export {};
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export interface Component {
|
|
|
44
44
|
handleInput?(data: string): void;
|
|
45
45
|
/** Optional handler for terminal mouse events when component has focus. */
|
|
46
46
|
handleMouse?(event: MouseEvent): void;
|
|
47
|
+
/**
|
|
48
|
+
* Optional click hook for a top-level child that is not focused: called with the
|
|
49
|
+
* row (within this component's last rendered lines) and column of a left click.
|
|
50
|
+
* Return true to consume the click. Needs mouse reporting (`enableMouse` or
|
|
51
|
+
* {@link TUI.setMouseEnabled}).
|
|
52
|
+
*/
|
|
53
|
+
handleClick?(line: number, column: number): boolean;
|
|
47
54
|
/**
|
|
48
55
|
* If true, component receives key release events (Kitty protocol).
|
|
49
56
|
* Default is false - release events are filtered out.
|
|
@@ -298,6 +305,15 @@ export declare class TUI extends Container {
|
|
|
298
305
|
/** Check if there are any visible overlays */
|
|
299
306
|
hasOverlay(): boolean;
|
|
300
307
|
invalidate(): void;
|
|
308
|
+
/**
|
|
309
|
+
* Turn terminal mouse reporting on or off while running. With it on, the terminal
|
|
310
|
+
* sends clicks and the wheel to the app instead of doing its own selection (most
|
|
311
|
+
* terminals still select with Shift held).
|
|
312
|
+
*/
|
|
313
|
+
setMouseEnabled(enabled: boolean): void;
|
|
314
|
+
get mouseEnabled(): boolean;
|
|
315
|
+
/** The component that currently receives keyboard input. */
|
|
316
|
+
getFocusedComponent(): Component | null;
|
|
301
317
|
start(): void;
|
|
302
318
|
/**
|
|
303
319
|
* Wait for a specific render request generation to be written successfully.
|
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.7",
|
|
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.7",
|
|
42
|
+
"@sayknow-cli/utils": "0.6.7",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -428,6 +428,8 @@ export class Editor implements Component, Focusable {
|
|
|
428
428
|
cursorOverrideWidth: number | undefined;
|
|
429
429
|
#promptGutter: string | undefined;
|
|
430
430
|
#railGutter: string | undefined;
|
|
431
|
+
/** SGR background painted under every borderless input row (the composer band). */
|
|
432
|
+
#inputBackground: string | undefined;
|
|
431
433
|
#inputPrefix: string | undefined;
|
|
432
434
|
#inputPrefixWidth = 0;
|
|
433
435
|
#placeholder: string | undefined;
|
|
@@ -578,6 +580,21 @@ export class Editor implements Component, Focusable {
|
|
|
578
580
|
this.#invalidateLayoutCache();
|
|
579
581
|
}
|
|
580
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Paint a background band (an SGR sequence such as `\x1b[48;2;36;30;24m`) under
|
|
585
|
+
* every input row of a borderless editor, so the line being typed stands out from
|
|
586
|
+
* the transcript. Undefined removes it. Ignored while the border is visible.
|
|
587
|
+
*/
|
|
588
|
+
setInputBackground(background: string | undefined): void {
|
|
589
|
+
this.#inputBackground = background || undefined;
|
|
590
|
+
this.#invalidateLayoutCache();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** True while Up/Down are walking prompt history rather than editing. */
|
|
594
|
+
isBrowsingHistory(): boolean {
|
|
595
|
+
return this.#historyIndex !== -1;
|
|
596
|
+
}
|
|
597
|
+
|
|
581
598
|
setInputPrefix(inputPrefix: string | undefined): void {
|
|
582
599
|
this.#inputPrefix = inputPrefix;
|
|
583
600
|
this.#inputPrefixWidth = inputPrefix ? visibleWidth(inputPrefix) : 0;
|
|
@@ -935,6 +952,7 @@ export class Editor implements Component, Focusable {
|
|
|
935
952
|
}
|
|
936
953
|
|
|
937
954
|
// Render each layout line
|
|
955
|
+
const inputRowsStart = result.length;
|
|
938
956
|
// Emit hardware cursor marker only when focused and not showing autocomplete
|
|
939
957
|
const emitCursorMarker = this.focused && !this.#autocompleteState;
|
|
940
958
|
const lineContentWidth = Math.max(0, contentAreaWidth - inputPrefixWidth);
|
|
@@ -1123,6 +1141,13 @@ export class Editor implements Component, Focusable {
|
|
|
1123
1141
|
}
|
|
1124
1142
|
}
|
|
1125
1143
|
|
|
1144
|
+
const background = this.#inputBackground;
|
|
1145
|
+
if (background && !borderVisible) {
|
|
1146
|
+
for (let row = inputRowsStart; row < result.length; row++) {
|
|
1147
|
+
result[row] = paintBackground(result[row]!, background, renderWidth);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1126
1151
|
if (borderVisible && this.#closedBorderBox) {
|
|
1127
1152
|
const bottomFillWidth = Math.max(0, renderWidth - borderWidth * 2);
|
|
1128
1153
|
const bottomLeftClosed = this.borderColor(`${box.bottomLeft}${box.horizontal.repeat(paddingX)}`);
|
|
@@ -1582,6 +1607,7 @@ export class Editor implements Component, Focusable {
|
|
|
1582
1607
|
placeholder: this.#placeholder,
|
|
1583
1608
|
promptGutter: this.#promptGutter,
|
|
1584
1609
|
railGutter: this.#railGutter,
|
|
1610
|
+
inputBackground: this.#inputBackground,
|
|
1585
1611
|
useTerminalCursor: this.#useTerminalCursor,
|
|
1586
1612
|
cursorOverride: this.cursorOverride,
|
|
1587
1613
|
cursorOverrideWidth: this.cursorOverrideWidth,
|
|
@@ -3105,3 +3131,13 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
3105
3131
|
return null;
|
|
3106
3132
|
}
|
|
3107
3133
|
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Hold a background under a whole row: re-assert it after every reset inside the row
|
|
3137
|
+
* (cursor, ghost text and colored spans all end with one) and pad to `width`.
|
|
3138
|
+
*/
|
|
3139
|
+
function paintBackground(line: string, background: string, width: number): string {
|
|
3140
|
+
const fill = Math.max(0, width - visibleWidth(line));
|
|
3141
|
+
const held = line.replace(/\x1b\[(?:0)?m|\x1b\[49m/g, match => `${match}${background}`);
|
|
3142
|
+
return `${background}${held}${padding(fill)}\x1b[0m`;
|
|
3143
|
+
}
|
|
@@ -433,3 +433,67 @@ export function buildSayknowPixelFrames(options: {
|
|
|
433
433
|
|
|
434
434
|
return { frames, protocol: options.protocol, widthPx, heightPx, columns, rows, rasterRows };
|
|
435
435
|
}
|
|
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
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Draw a pet frame as text with half blocks, one terminal row per two pixel rows,
|
|
462
|
+
* so the same octopus shows up where no pixel protocol is available (the launch
|
|
463
|
+
* 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.
|
|
466
|
+
*/
|
|
467
|
+
export function renderPetHalfBlocks(
|
|
468
|
+
frame: SayknowPixelFrameName,
|
|
469
|
+
skin: PetSkinId = "red",
|
|
470
|
+
colorMode: "truecolor" | "256color" = "truecolor",
|
|
471
|
+
options: { scale?: "full" | "half" } = {},
|
|
472
|
+
): string[] {
|
|
473
|
+
const palette = PET_SKINS[skin].palette;
|
|
474
|
+
const grid = options.scale === "half" ? (SMALL_GRIDS[frame] ?? SMALL_BASE) : PIXEL_GRIDS[frame];
|
|
475
|
+
const opaque = (row: string): boolean => [...row].some(ch => palette[ch] != null);
|
|
476
|
+
let top = 0;
|
|
477
|
+
while (top < grid.length && !opaque(grid[top]!)) top++;
|
|
478
|
+
let bottom = grid.length - 1;
|
|
479
|
+
while (bottom > top && !opaque(grid[bottom]!)) bottom--;
|
|
480
|
+
const format = colorMode === "truecolor" ? "ansi-16m" : "ansi-256";
|
|
481
|
+
const fg = (rgb: Rgb): string => Bun.color(`rgb(${rgb[0]},${rgb[1]},${rgb[2]})`, format) ?? "";
|
|
482
|
+
const bg = (rgb: Rgb): string => fg(rgb).replace("\x1b[38;", "\x1b[48;");
|
|
483
|
+
const lines: string[] = [];
|
|
484
|
+
for (let row = top; row <= bottom; row += 2) {
|
|
485
|
+
const upper = grid[row]!;
|
|
486
|
+
const lower = row + 1 <= bottom ? grid[row + 1]! : "";
|
|
487
|
+
let line = "";
|
|
488
|
+
for (let col = 0; col < upper.length; col++) {
|
|
489
|
+
const up = palette[upper[col]!] ?? null;
|
|
490
|
+
const down = palette[lower[col] ?? "."] ?? null;
|
|
491
|
+
if (!up && !down) line += " ";
|
|
492
|
+
else if (up && down) line += `${fg(up)}${bg(down)}▀\x1b[0m`;
|
|
493
|
+
else if (up) line += `${fg(up)}▀\x1b[0m`;
|
|
494
|
+
else line += `${fg(down!)}▄\x1b[0m`;
|
|
495
|
+
}
|
|
496
|
+
lines.push(line);
|
|
497
|
+
}
|
|
498
|
+
return lines;
|
|
499
|
+
}
|
package/src/tui.ts
CHANGED
|
@@ -132,6 +132,14 @@ export interface Component {
|
|
|
132
132
|
/** Optional handler for terminal mouse events when component has focus. */
|
|
133
133
|
handleMouse?(event: MouseEvent): void;
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Optional click hook for a top-level child that is not focused: called with the
|
|
137
|
+
* row (within this component's last rendered lines) and column of a left click.
|
|
138
|
+
* Return true to consume the click. Needs mouse reporting (`enableMouse` or
|
|
139
|
+
* {@link TUI.setMouseEnabled}).
|
|
140
|
+
*/
|
|
141
|
+
handleClick?(line: number, column: number): boolean;
|
|
142
|
+
|
|
135
143
|
/**
|
|
136
144
|
* If true, component receives key release events (Kitty protocol).
|
|
137
145
|
* Default is false - release events are filtered out.
|
|
@@ -760,6 +768,9 @@ export class TUI extends Container {
|
|
|
760
768
|
#mouseSelectionStart: MouseSelectionPoint | null = null;
|
|
761
769
|
#mouseSelectionEnd: MouseSelectionPoint | null = null;
|
|
762
770
|
#mouseSelectionDragged = false;
|
|
771
|
+
/** Rows each top-level child occupied in the last frame, for click hit-testing. */
|
|
772
|
+
#childRows = new Map<Component, { start: number; count: number }>();
|
|
773
|
+
#mouseEnabled = false;
|
|
763
774
|
#viewportOutputSource: ViewportOutputSource | null = null;
|
|
764
775
|
#manualOutputNotice = false;
|
|
765
776
|
#manualTranscriptLineCount = 0;
|
|
@@ -828,6 +839,7 @@ export class TUI extends Container {
|
|
|
828
839
|
} = {},
|
|
829
840
|
) {
|
|
830
841
|
super();
|
|
842
|
+
this.#mouseEnabled = options.enableMouse === true;
|
|
831
843
|
this.terminal = terminal;
|
|
832
844
|
if (showHardwareCursor !== undefined) {
|
|
833
845
|
this.#showHardwareCursor = showHardwareCursor;
|
|
@@ -1315,13 +1327,34 @@ export class TUI extends Container {
|
|
|
1315
1327
|
for (const overlay of this.overlayStack) overlay.mouseBounds = undefined;
|
|
1316
1328
|
}
|
|
1317
1329
|
|
|
1330
|
+
/**
|
|
1331
|
+
* Turn terminal mouse reporting on or off while running. With it on, the terminal
|
|
1332
|
+
* sends clicks and the wheel to the app instead of doing its own selection (most
|
|
1333
|
+
* terminals still select with Shift held).
|
|
1334
|
+
*/
|
|
1335
|
+
setMouseEnabled(enabled: boolean): void {
|
|
1336
|
+
if (this.#mouseEnabled === enabled) return;
|
|
1337
|
+
this.#mouseEnabled = enabled;
|
|
1338
|
+
if (!enabled) this.#clearMouseSelection();
|
|
1339
|
+
if (!this.#stopped) this.terminal.setMouseEnabled?.(enabled);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
get mouseEnabled(): boolean {
|
|
1343
|
+
return this.#mouseEnabled;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/** The component that currently receives keyboard input. */
|
|
1347
|
+
getFocusedComponent(): Component | null {
|
|
1348
|
+
return this.#focusedComponent;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1318
1351
|
start(): void {
|
|
1319
1352
|
this.#stopped = false;
|
|
1320
1353
|
this.#terminalUnavailable = false;
|
|
1321
1354
|
// Seed the observed width so a spurious post-start resize event (iTerm2 tab
|
|
1322
1355
|
// activation, the self-sent SIGWINCH after resume) is not read as a reflow.
|
|
1323
1356
|
this.#lastObservedWidth = this.terminal.columns;
|
|
1324
|
-
this.terminal.setMouseEnabled?.(this
|
|
1357
|
+
this.terminal.setMouseEnabled?.(this.#mouseEnabled);
|
|
1325
1358
|
this.terminal.start(
|
|
1326
1359
|
data => this.#handleInput(data),
|
|
1327
1360
|
() => {
|
|
@@ -2022,6 +2055,10 @@ export class TUI extends Container {
|
|
|
2022
2055
|
this.#clearMouseSelection();
|
|
2023
2056
|
this.scrollViewportBy(mouse.direction! * DEFAULT_WHEEL_LINES, { pin: "stable" });
|
|
2024
2057
|
} else if (mouse.kind === "click") {
|
|
2058
|
+
if (this.#dispatchChildClick(mouse)) {
|
|
2059
|
+
this.requestRender(false, "mouse");
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2025
2062
|
this.#beginMouseSelection(mouse);
|
|
2026
2063
|
const focusedOverlay = this.overlayStack.find(o => o.component === this.#focusedComponent);
|
|
2027
2064
|
if (focusedOverlay) {
|
|
@@ -2096,6 +2133,22 @@ export class TUI extends Container {
|
|
|
2096
2133
|
}
|
|
2097
2134
|
}
|
|
2098
2135
|
|
|
2136
|
+
/** Give a click to the top-level child under it, if that child takes clicks and no overlay is focused. */
|
|
2137
|
+
#dispatchChildClick(mouse: MouseEvent): boolean {
|
|
2138
|
+
if (this.#childRows.size === 0) return false;
|
|
2139
|
+
if (this.overlayStack.some(overlay => overlay.component === this.#focusedComponent)) return false;
|
|
2140
|
+
const point = this.#mouseSelectionPoint(mouse);
|
|
2141
|
+
if (point === null) return false;
|
|
2142
|
+
for (const [child, rows] of this.#childRows) {
|
|
2143
|
+
if (point.line < rows.start || point.line >= rows.start + rows.count) continue;
|
|
2144
|
+
if (child.handleClick?.(point.line - rows.start, point.column)) {
|
|
2145
|
+
this.#clearMouseSelection();
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2099
2152
|
#mouseSelectionPoint(mouse: MouseEvent): MouseSelectionPoint | null {
|
|
2100
2153
|
if (this.#manualViewportTop === undefined) {
|
|
2101
2154
|
return { line: this.#viewportTopRow + mouse.y - 1, column: mouse.x - 1 };
|
|
@@ -2919,9 +2972,14 @@ export class TUI extends Container {
|
|
|
2919
2972
|
const renderedChildren = new Map<Component, string[]>();
|
|
2920
2973
|
let anchorFrame: ViewportAnchorFrame | null = null;
|
|
2921
2974
|
const anchorRenderFailureCountBefore = viewportAnchorRenderFailureCount;
|
|
2975
|
+
this.#childRows.clear();
|
|
2922
2976
|
for (const child of this.children) {
|
|
2923
2977
|
const rendered = safeRenderComponentWithViewportAnchors(child, width, "tui-child");
|
|
2924
2978
|
renderedChildren.set(child, rendered.lines);
|
|
2979
|
+
// Children above the pinned suffix keep these rows: padding and suffix trimming
|
|
2980
|
+
// only touch the pinned tail.
|
|
2981
|
+
if (child.handleClick)
|
|
2982
|
+
this.#childRows.set(child, { start: renderedLines.length, count: rendered.lines.length });
|
|
2925
2983
|
if (child === this.#viewportAnchorComponent && rendered.anchors.some(anchor => anchor !== null)) {
|
|
2926
2984
|
anchorFrame = { startRow: renderedLines.length, anchors: rendered.anchors };
|
|
2927
2985
|
}
|