tinky 1.6.0 → 1.8.0
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/lib/components/Text.js +12 -3
- package/lib/core/cell-buffer.d.ts +7 -5
- package/lib/core/cell-buffer.js +68 -10
- package/lib/core/cell-log-update-run.d.ts +2 -1
- package/lib/core/cell-log-update-run.js +44 -36
- package/lib/core/cell-output.d.ts +7 -0
- package/lib/core/cell-output.js +126 -2
- package/lib/core/dom.d.ts +3 -0
- package/lib/core/output.d.ts +3 -0
- package/lib/core/output.js +12 -0
- package/lib/core/reconciler.js +3 -0
- package/lib/core/render-border.d.ts +0 -10
- package/lib/core/render-border.js +78 -69
- package/lib/core/render.d.ts +1 -0
- package/lib/core/renderer.js +9 -6
- package/lib/core/size-observer.d.ts +60 -0
- package/lib/core/size-observer.js +133 -0
- package/lib/core/tinky.d.ts +0 -1
- package/lib/core/tinky.js +7 -6
- package/lib/hooks/use-size-observer.d.ts +44 -0
- package/lib/hooks/use-size-observer.js +70 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/types/ansi.d.ts +5 -0
- package/lib/types/ansi.js +1 -0
- package/lib/utils/colorize.d.ts +5 -0
- package/lib/utils/colorize.js +42 -20
- package/lib/utils/render-background.js +4 -3
- package/lib/utils/squash-text-nodes.d.ts +5 -0
- package/lib/utils/squash-text-nodes.js +18 -2
- package/package.json +3 -1
package/lib/components/Text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useContext } from "react";
|
|
2
|
+
import { useContext, useCallback } from "react";
|
|
3
3
|
import { applyTextStyles, } from "../utils/apply-text-styles.js";
|
|
4
4
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
5
5
|
import { backgroundContext } from "../contexts/BackgroundContext.js";
|
|
@@ -41,7 +41,7 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
|
|
|
41
41
|
if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
-
const transform = (children) => {
|
|
44
|
+
const transform = useCallback((children) => {
|
|
45
45
|
return applyTextStyles(children, {
|
|
46
46
|
color,
|
|
47
47
|
backgroundColor: effectiveBackgroundColor,
|
|
@@ -52,7 +52,16 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
|
|
|
52
52
|
strikethrough,
|
|
53
53
|
inverse,
|
|
54
54
|
});
|
|
55
|
-
}
|
|
55
|
+
}, [
|
|
56
|
+
color,
|
|
57
|
+
effectiveBackgroundColor,
|
|
58
|
+
dimColor,
|
|
59
|
+
bold,
|
|
60
|
+
italic,
|
|
61
|
+
underline,
|
|
62
|
+
strikethrough,
|
|
63
|
+
inverse,
|
|
64
|
+
]);
|
|
56
65
|
if (isScreenReaderEnabled && ariaHidden) {
|
|
57
66
|
return null;
|
|
58
67
|
}
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
* This stores terminal cells in compact parallel arrays and keeps ANSI styles
|
|
5
5
|
* as small numeric IDs via StyleRegistry.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
type: "ansi";
|
|
9
|
-
code: string;
|
|
10
|
-
endCode: string;
|
|
11
|
-
}
|
|
7
|
+
import { type AnsiCode } from "../types/ansi.js";
|
|
12
8
|
export interface CellBufferSize {
|
|
13
9
|
width: number;
|
|
14
10
|
height: number;
|
|
@@ -45,6 +41,11 @@ export declare class CellBuffer {
|
|
|
45
41
|
* Marks whether trailing spaces on a touched cell should be preserved.
|
|
46
42
|
*/
|
|
47
43
|
preserveTrailingSpace: Uint8Array;
|
|
44
|
+
/**
|
|
45
|
+
* Marks whether a row has been touched by any write operation.
|
|
46
|
+
* Used to optimize isRowEqual/diffing.
|
|
47
|
+
*/
|
|
48
|
+
rowTouched: Uint8Array;
|
|
48
49
|
constructor(size?: CellBufferSize, styleRegistry?: StyleRegistry);
|
|
49
50
|
resize(width: number, height: number): void;
|
|
50
51
|
clear(styleId?: number): void;
|
|
@@ -53,6 +54,7 @@ export declare class CellBuffer {
|
|
|
53
54
|
*/
|
|
54
55
|
index(x: number, y: number): number;
|
|
55
56
|
setCell(x: number, y: number, ch: string, charWidth: number, styleId: number, preserveTrailing?: boolean): void;
|
|
57
|
+
fill(x: number, y: number, length: number, char: string, charWidth: number, styleId: number): void;
|
|
56
58
|
isRowEqual(other: CellBuffer, row: number): boolean;
|
|
57
59
|
isEqual(other: CellBuffer): boolean;
|
|
58
60
|
/**
|
package/lib/core/cell-buffer.js
CHANGED
|
@@ -69,6 +69,11 @@ export class CellBuffer {
|
|
|
69
69
|
* Marks whether trailing spaces on a touched cell should be preserved.
|
|
70
70
|
*/
|
|
71
71
|
preserveTrailingSpace = new Uint8Array(0);
|
|
72
|
+
/**
|
|
73
|
+
* Marks whether a row has been touched by any write operation.
|
|
74
|
+
* Used to optimize isRowEqual/diffing.
|
|
75
|
+
*/
|
|
76
|
+
rowTouched = new Uint8Array(0);
|
|
72
77
|
constructor(size = { width: 0, height: 0 }, styleRegistry) {
|
|
73
78
|
this.styleRegistry = styleRegistry ?? new StyleRegistry();
|
|
74
79
|
this.resize(size.width, size.height);
|
|
@@ -88,6 +93,7 @@ export class CellBuffer {
|
|
|
88
93
|
this.styleIds = new Uint32Array(size);
|
|
89
94
|
this.touched = new Uint8Array(size);
|
|
90
95
|
this.preserveTrailingSpace = new Uint8Array(size);
|
|
96
|
+
this.rowTouched = new Uint8Array(nextHeight);
|
|
91
97
|
}
|
|
92
98
|
clear(styleId = 0) {
|
|
93
99
|
const size = this.width * this.height;
|
|
@@ -96,6 +102,13 @@ export class CellBuffer {
|
|
|
96
102
|
this.styleIds.fill(styleId, 0, size);
|
|
97
103
|
this.touched.fill(0, 0, size);
|
|
98
104
|
this.preserveTrailingSpace.fill(0, 0, size);
|
|
105
|
+
// Only rows with default style (0) are considered "untouched" for optimization
|
|
106
|
+
if (styleId === 0) {
|
|
107
|
+
this.rowTouched.fill(0);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.rowTouched.fill(1);
|
|
111
|
+
}
|
|
99
112
|
}
|
|
100
113
|
/**
|
|
101
114
|
* Converts `(x, y)` coordinates to a linear index in parallel arrays.
|
|
@@ -117,6 +130,7 @@ export class CellBuffer {
|
|
|
117
130
|
this.styleIds[cellIndex] = styleId;
|
|
118
131
|
this.touched[cellIndex] = 1;
|
|
119
132
|
this.preserveTrailingSpace[cellIndex] = preserveTrailing ? 1 : 0;
|
|
133
|
+
this.rowTouched[y] = 1;
|
|
120
134
|
if (width > 1) {
|
|
121
135
|
for (let i = 1; i < width; i++) {
|
|
122
136
|
const continuationIndex = cellIndex + i;
|
|
@@ -130,6 +144,33 @@ export class CellBuffer {
|
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
}
|
|
147
|
+
fill(x, y, length, char, charWidth, styleId) {
|
|
148
|
+
if (y < 0 || y >= this.height || length <= 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const startX = Math.max(0, x);
|
|
152
|
+
const endX = Math.min(this.width, x + length);
|
|
153
|
+
const fillLength = endX - startX;
|
|
154
|
+
if (fillLength <= 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const start = this.index(startX, y);
|
|
158
|
+
// Optimized path for single-width characters (common for background/borders)
|
|
159
|
+
if (charWidth === 1) {
|
|
160
|
+
const end = start + fillLength;
|
|
161
|
+
this.chars.fill(char, start, end);
|
|
162
|
+
this.widths.fill(1, start, end);
|
|
163
|
+
this.styleIds.fill(styleId, start, end);
|
|
164
|
+
this.touched.fill(1, start, end);
|
|
165
|
+
this.preserveTrailingSpace.fill(0, start, end);
|
|
166
|
+
this.rowTouched[y] = 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Fallback for multi-width characters
|
|
170
|
+
for (let i = 0; i < fillLength; i += charWidth) {
|
|
171
|
+
this.setCell(startX + i, y, char, charWidth, styleId, false);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
133
174
|
isRowEqual(other, row) {
|
|
134
175
|
if (this.width !== other.width || this.height !== other.height) {
|
|
135
176
|
return false;
|
|
@@ -137,8 +178,20 @@ export class CellBuffer {
|
|
|
137
178
|
if (row < 0 || row >= this.height) {
|
|
138
179
|
return false;
|
|
139
180
|
}
|
|
181
|
+
// Optimization: if both rows are untouched (empty/default), they are equal
|
|
182
|
+
if (!this.rowTouched[row] && !other.rowTouched[row]) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const thisRightEdge = this.getRowRightEdge(row);
|
|
186
|
+
const otherRightEdge = other.getRowRightEdge(row);
|
|
187
|
+
if (thisRightEdge !== otherRightEdge) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (thisRightEdge === 0) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
140
193
|
const start = row * this.width;
|
|
141
|
-
const end = start +
|
|
194
|
+
const end = start + thisRightEdge;
|
|
142
195
|
for (let index = start; index < end; index++) {
|
|
143
196
|
if (this.styleIds[index] !== other.styleIds[index]) {
|
|
144
197
|
return false;
|
|
@@ -149,12 +202,6 @@ export class CellBuffer {
|
|
|
149
202
|
if (this.chars[index] !== other.chars[index]) {
|
|
150
203
|
return false;
|
|
151
204
|
}
|
|
152
|
-
if (this.touched[index] !== other.touched[index]) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
if (this.preserveTrailingSpace[index] !== other.preserveTrailingSpace[index]) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
205
|
}
|
|
159
206
|
return true;
|
|
160
207
|
}
|
|
@@ -180,6 +227,9 @@ export class CellBuffer {
|
|
|
180
227
|
if (row < 0 || row >= this.height) {
|
|
181
228
|
return 0;
|
|
182
229
|
}
|
|
230
|
+
if (!this.rowTouched[row]) {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
183
233
|
const offset = row * width;
|
|
184
234
|
for (let x = width - 1; x >= 0; x--) {
|
|
185
235
|
const index = offset + x;
|
|
@@ -279,10 +329,18 @@ export class CellBuffer {
|
|
|
279
329
|
* Serializes the entire buffer into a multi-line ANSI string.
|
|
280
330
|
*/
|
|
281
331
|
toString() {
|
|
282
|
-
const
|
|
332
|
+
const out = [];
|
|
283
333
|
for (let row = 0; row < this.height; row++) {
|
|
284
|
-
|
|
334
|
+
if (row > 0) {
|
|
335
|
+
out.push("\n");
|
|
336
|
+
}
|
|
337
|
+
const rightEdge = this.getRowRightEdge(row);
|
|
338
|
+
if (rightEdge === 0) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const { activeStyleId } = this.appendStyledRange(row, 0, rightEdge, out, 0, 0);
|
|
342
|
+
this.appendCloseActiveStyle(out, activeStyleId);
|
|
285
343
|
}
|
|
286
|
-
return
|
|
344
|
+
return out.join("");
|
|
287
345
|
}
|
|
288
346
|
}
|
|
@@ -4,7 +4,8 @@ import { type CellBuffer } from "./cell-buffer.js";
|
|
|
4
4
|
* Run-diff renderer for interactive terminal output.
|
|
5
5
|
*
|
|
6
6
|
* Compared with line-diff mode, this renderer diffs cell buffers and emits
|
|
7
|
-
* minimal cursor movement + text writes for changed runs.
|
|
7
|
+
* minimal cursor movement + text writes for changed runs. It entirely skips
|
|
8
|
+
* terminal writes when consecutive frames are identical.
|
|
8
9
|
*/
|
|
9
10
|
export interface CellLogUpdateRunOptions {
|
|
10
11
|
/** Enables diff updates. Set `false` to force full-frame redraws. */
|
|
@@ -348,29 +348,24 @@ const create = (stream, options = {}) => {
|
|
|
348
348
|
const prevCount = previousHeight + 1;
|
|
349
349
|
const nextCount = nextHeight + 1;
|
|
350
350
|
const visibleCount = nextHeight;
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
chunks.push(ansiEscapes.eraseLines(prevCount - nextCount + 1));
|
|
354
|
-
chunks.push(ansiEscapes.cursorUp(visibleCount));
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
chunks.push(ansiEscapes.cursorUp(prevCount - 1));
|
|
358
|
-
}
|
|
351
|
+
const rowChunks = [];
|
|
352
|
+
let hasDiff = false;
|
|
359
353
|
const width = nextBuffer.width;
|
|
360
354
|
// Incremental path: process one row at a time and emit minimal writes.
|
|
361
355
|
for (let row = 0; row < visibleCount; row++) {
|
|
362
356
|
if (prevBuffer.isRowEqual(nextBuffer, row)) {
|
|
363
|
-
|
|
357
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
364
358
|
continue;
|
|
365
359
|
}
|
|
366
|
-
|
|
360
|
+
hasDiff = true;
|
|
361
|
+
rowChunks.push(cursorToColumn(0));
|
|
367
362
|
const nextEnd = nextBuffer.getRowRightEdge(row);
|
|
368
363
|
const prevEnd = prevBuffer.getRowRightEdge(row);
|
|
369
364
|
const tailClear = prevEnd > nextEnd;
|
|
370
365
|
const scanEnd = tailClear ? nextEnd : Math.max(nextEnd, prevEnd);
|
|
371
366
|
if (scanEnd === 0 && tailClear) {
|
|
372
|
-
|
|
373
|
-
|
|
367
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
368
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
374
369
|
continue;
|
|
375
370
|
}
|
|
376
371
|
let segments = computeSegments(prevBuffer, nextBuffer, row, scanEnd);
|
|
@@ -378,20 +373,20 @@ const create = (stream, options = {}) => {
|
|
|
378
373
|
segments = mergeByGapThreshold(segments, mergeThreshold);
|
|
379
374
|
}
|
|
380
375
|
if (segments.length > maxSegmentsBeforeFullRow) {
|
|
381
|
-
const full = nextBuffer.appendStyledRange(row, 0, nextEnd,
|
|
376
|
+
const full = nextBuffer.appendStyledRange(row, 0, nextEnd, rowChunks, 0, 0);
|
|
382
377
|
let activeStyleId = full.activeStyleId;
|
|
383
|
-
activeStyleId = nextBuffer.appendCloseActiveStyle(
|
|
378
|
+
activeStyleId = nextBuffer.appendCloseActiveStyle(rowChunks, activeStyleId);
|
|
384
379
|
if (tailClear) {
|
|
385
|
-
|
|
380
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
386
381
|
}
|
|
387
|
-
|
|
382
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
388
383
|
continue;
|
|
389
384
|
}
|
|
390
385
|
if (segments.length === 0) {
|
|
391
386
|
if (tailClear) {
|
|
392
|
-
|
|
387
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
393
388
|
}
|
|
394
|
-
|
|
389
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
395
390
|
continue;
|
|
396
391
|
}
|
|
397
392
|
let cursorX = 0;
|
|
@@ -399,14 +394,14 @@ const create = (stream, options = {}) => {
|
|
|
399
394
|
if (mergeStrategy === "cost") {
|
|
400
395
|
const plan = planRowOpsWithCostModel(nextBuffer, styleLenCache, row, segments, nextEnd, tailClear, overwriteGapPenaltyBytes);
|
|
401
396
|
if (plan.useFullRow) {
|
|
402
|
-
const full = nextBuffer.appendStyledRange(row, 0, nextEnd,
|
|
397
|
+
const full = nextBuffer.appendStyledRange(row, 0, nextEnd, rowChunks, activeStyleId, cursorX);
|
|
403
398
|
cursorX = full.cursorX;
|
|
404
399
|
activeStyleId = full.activeStyleId;
|
|
405
|
-
activeStyleId = nextBuffer.appendCloseActiveStyle(
|
|
400
|
+
activeStyleId = nextBuffer.appendCloseActiveStyle(rowChunks, activeStyleId);
|
|
406
401
|
if (tailClear) {
|
|
407
|
-
|
|
402
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
408
403
|
}
|
|
409
|
-
|
|
404
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
410
405
|
continue;
|
|
411
406
|
}
|
|
412
407
|
for (const op of plan.ops) {
|
|
@@ -418,15 +413,15 @@ const create = (stream, options = {}) => {
|
|
|
418
413
|
}
|
|
419
414
|
if (delta > 0) {
|
|
420
415
|
if (op.method === "abs") {
|
|
421
|
-
|
|
416
|
+
rowChunks.push(cursorToColumn(toX));
|
|
422
417
|
}
|
|
423
418
|
else {
|
|
424
|
-
|
|
419
|
+
rowChunks.push(cursorForward(delta));
|
|
425
420
|
}
|
|
426
421
|
cursorX = toX;
|
|
427
422
|
}
|
|
428
423
|
else {
|
|
429
|
-
|
|
424
|
+
rowChunks.push(cursorToColumn(toX));
|
|
430
425
|
cursorX = toX;
|
|
431
426
|
}
|
|
432
427
|
continue;
|
|
@@ -442,14 +437,14 @@ const create = (stream, options = {}) => {
|
|
|
442
437
|
if (start !== cursorX) {
|
|
443
438
|
const move = chooseHorizontalMove(cursorX, start);
|
|
444
439
|
if (move.method === "abs") {
|
|
445
|
-
|
|
440
|
+
rowChunks.push(cursorToColumn(start));
|
|
446
441
|
}
|
|
447
442
|
else {
|
|
448
|
-
|
|
443
|
+
rowChunks.push(cursorForward(start - cursorX));
|
|
449
444
|
}
|
|
450
445
|
cursorX = start;
|
|
451
446
|
}
|
|
452
|
-
const result = nextBuffer.appendStyledRange(row, start, end,
|
|
447
|
+
const result = nextBuffer.appendStyledRange(row, start, end, rowChunks, activeStyleId, cursorX);
|
|
453
448
|
cursorX = result.cursorX;
|
|
454
449
|
activeStyleId = result.activeStyleId;
|
|
455
450
|
}
|
|
@@ -466,33 +461,46 @@ const create = (stream, options = {}) => {
|
|
|
466
461
|
}
|
|
467
462
|
const delta = segStart - cursorX;
|
|
468
463
|
if (delta > 0) {
|
|
469
|
-
|
|
464
|
+
rowChunks.push(cursorForward(delta));
|
|
470
465
|
cursorX = segStart;
|
|
471
466
|
}
|
|
472
467
|
else if (delta < 0) {
|
|
473
|
-
|
|
468
|
+
rowChunks.push(cursorToColumn(segStart));
|
|
474
469
|
cursorX = segStart;
|
|
475
470
|
}
|
|
476
|
-
const result = nextBuffer.appendStyledRange(row, segStart, segEnd,
|
|
471
|
+
const result = nextBuffer.appendStyledRange(row, segStart, segEnd, rowChunks, activeStyleId, cursorX);
|
|
477
472
|
cursorX = result.cursorX;
|
|
478
473
|
activeStyleId = result.activeStyleId;
|
|
479
474
|
}
|
|
480
475
|
}
|
|
481
|
-
activeStyleId = nextBuffer.appendCloseActiveStyle(
|
|
476
|
+
activeStyleId = nextBuffer.appendCloseActiveStyle(rowChunks, activeStyleId);
|
|
482
477
|
if (tailClear) {
|
|
483
478
|
if (cursorX !== nextEnd) {
|
|
484
479
|
const move = chooseHorizontalMove(cursorX, nextEnd);
|
|
485
480
|
if (move.method === "abs") {
|
|
486
|
-
|
|
481
|
+
rowChunks.push(cursorToColumn(nextEnd));
|
|
487
482
|
}
|
|
488
483
|
else {
|
|
489
|
-
|
|
484
|
+
rowChunks.push(cursorForward(Math.max(0, nextEnd - cursorX)));
|
|
490
485
|
}
|
|
491
486
|
}
|
|
492
|
-
|
|
487
|
+
rowChunks.push(ansiEscapes.eraseEndLine);
|
|
493
488
|
}
|
|
494
|
-
|
|
489
|
+
rowChunks.push(ansiEscapes.cursorNextLine);
|
|
490
|
+
}
|
|
491
|
+
if (!hasDiff && nextCount === prevCount) {
|
|
492
|
+
previousHeight = nextHeight;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const chunks = [];
|
|
496
|
+
if (nextCount < prevCount) {
|
|
497
|
+
chunks.push(ansiEscapes.eraseLines(prevCount - nextCount + 1));
|
|
498
|
+
chunks.push(ansiEscapes.cursorUp(visibleCount));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
chunks.push(ansiEscapes.cursorUp(prevCount - 1));
|
|
495
502
|
}
|
|
503
|
+
chunks.push(...rowChunks);
|
|
496
504
|
stream.write(chunks.join(""));
|
|
497
505
|
previousHeight = nextHeight;
|
|
498
506
|
});
|
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type CellBuffer } from "./cell-buffer.js";
|
|
5
5
|
import { type Clip, type OutputLike, type OutputWriteOptions } from "./output.js";
|
|
6
|
+
import { type AnsiCode } from "../types/ansi.js";
|
|
6
7
|
export declare class CellOutput implements OutputLike {
|
|
7
8
|
private readonly clips;
|
|
9
|
+
private readonly styleIdByRef;
|
|
10
|
+
private lastStyleRef;
|
|
11
|
+
private lastStyleId;
|
|
8
12
|
readonly buffer: CellBuffer;
|
|
9
13
|
constructor(buffer: CellBuffer);
|
|
10
14
|
clip(clip: Clip): void;
|
|
11
15
|
unclip(): void;
|
|
16
|
+
fill(x: number, y: number, length: number, char: string, charWidth: number, styles: AnsiCode[]): void;
|
|
17
|
+
private resolveStyleId;
|
|
18
|
+
private writePlainLine;
|
|
12
19
|
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
13
20
|
}
|
package/lib/core/cell-output.js
CHANGED
|
@@ -5,8 +5,38 @@ import sliceAnsi from "slice-ansi";
|
|
|
5
5
|
import stringWidth from "string-width";
|
|
6
6
|
import widestLine from "widest-line";
|
|
7
7
|
import { styledCharsFromTokens, tokenize } from "@alcalzone/ansi-tokenize";
|
|
8
|
+
const ESC = "\u001b";
|
|
9
|
+
const isPrintableAsciiText = (value) => {
|
|
10
|
+
for (let index = 0; index < value.length; index++) {
|
|
11
|
+
const code = value.charCodeAt(index);
|
|
12
|
+
if (code === 10) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (code < 0x20 || code > 0x7e) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
const getCharWidth = (value, widthCache) => {
|
|
22
|
+
if (value.length === 1 &&
|
|
23
|
+
value.charCodeAt(0) >= 0x20 &&
|
|
24
|
+
value.charCodeAt(0) <= 0x7e) {
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const cached = widthCache.get(value);
|
|
28
|
+
if (cached !== undefined) {
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
const width = Math.max(1, stringWidth(value));
|
|
32
|
+
widthCache.set(value, width);
|
|
33
|
+
return width;
|
|
34
|
+
};
|
|
8
35
|
export class CellOutput {
|
|
9
36
|
clips = [];
|
|
37
|
+
styleIdByRef = new WeakMap();
|
|
38
|
+
lastStyleRef;
|
|
39
|
+
lastStyleId = 0;
|
|
10
40
|
buffer;
|
|
11
41
|
constructor(buffer) {
|
|
12
42
|
this.buffer = buffer;
|
|
@@ -17,6 +47,74 @@ export class CellOutput {
|
|
|
17
47
|
unclip() {
|
|
18
48
|
this.clips.pop();
|
|
19
49
|
}
|
|
50
|
+
fill(x, y, length, char, charWidth, styles) {
|
|
51
|
+
if (length <= 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const visualLength = length * charWidth;
|
|
55
|
+
let startX = x;
|
|
56
|
+
let endX = x + visualLength;
|
|
57
|
+
const row = y;
|
|
58
|
+
const clip = this.clips.at(-1);
|
|
59
|
+
if (clip) {
|
|
60
|
+
const clipY1 = clip.y1 ?? 0;
|
|
61
|
+
// Clipping logic: y2 is exclusive boundary
|
|
62
|
+
if (clip.y2 !== undefined && row >= clip.y2) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (row < clipY1) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const clipX1 = clip.x1 ?? 0;
|
|
69
|
+
startX = Math.max(startX, clipX1);
|
|
70
|
+
if (clip.x2 !== undefined) {
|
|
71
|
+
endX = Math.min(endX, clip.x2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const fillVisualLength = endX - startX;
|
|
75
|
+
if (fillVisualLength <= 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const styleId = this.buffer.styleRegistry.getId(styles);
|
|
79
|
+
this.buffer.fill(startX, row, fillVisualLength, char, charWidth, styleId);
|
|
80
|
+
}
|
|
81
|
+
resolveStyleId(styles) {
|
|
82
|
+
if (styles.length === 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (styles === this.lastStyleRef) {
|
|
86
|
+
return this.lastStyleId;
|
|
87
|
+
}
|
|
88
|
+
const byRef = this.styleIdByRef.get(styles);
|
|
89
|
+
if (byRef !== undefined) {
|
|
90
|
+
this.lastStyleRef = styles;
|
|
91
|
+
this.lastStyleId = byRef;
|
|
92
|
+
return byRef;
|
|
93
|
+
}
|
|
94
|
+
const styleId = this.buffer.styleRegistry.getId(styles);
|
|
95
|
+
this.styleIdByRef.set(styles, styleId);
|
|
96
|
+
this.lastStyleRef = styles;
|
|
97
|
+
this.lastStyleId = styleId;
|
|
98
|
+
return styleId;
|
|
99
|
+
}
|
|
100
|
+
writePlainLine(line, writeX, row, preserveLineTrailingSpaces, widthCache) {
|
|
101
|
+
let offsetX = writeX;
|
|
102
|
+
for (const value of line) {
|
|
103
|
+
const charWidth = getCharWidth(value, widthCache);
|
|
104
|
+
if (offsetX < 0) {
|
|
105
|
+
offsetX += charWidth;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (offsetX >= this.buffer.width) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (offsetX + charWidth > this.buffer.width) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
this.buffer.setCell(offsetX, row, value, charWidth, 0, preserveLineTrailingSpaces);
|
|
115
|
+
offsetX += charWidth;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
20
118
|
write(x, y, text, options) {
|
|
21
119
|
const { transformers } = options;
|
|
22
120
|
if (!text) {
|
|
@@ -28,7 +126,27 @@ export class CellOutput {
|
|
|
28
126
|
// Match Output.get() semantics: transformed lines may intentionally keep
|
|
29
127
|
// trailing spaces (for alignment or visual effects), so mark them.
|
|
30
128
|
const preserveLineTrailingSpaces = transformers.length > 0;
|
|
129
|
+
const widthCache = new Map();
|
|
31
130
|
const clip = this.clips.at(-1);
|
|
131
|
+
if (!clip &&
|
|
132
|
+
transformers.length === 0 &&
|
|
133
|
+
!text.includes(ESC) &&
|
|
134
|
+
isPrintableAsciiText(text)) {
|
|
135
|
+
let offsetY = 0;
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const row = writeY + offsetY;
|
|
138
|
+
if (row < 0) {
|
|
139
|
+
offsetY++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (row >= this.buffer.height) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
this.writePlainLine(line, writeX, row, preserveLineTrailingSpaces, widthCache);
|
|
146
|
+
offsetY++;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
32
150
|
if (clip) {
|
|
33
151
|
const clipHorizontally = typeof clip.x1 === "number" && typeof clip.x2 === "number";
|
|
34
152
|
const clipVertically = typeof clip.y1 === "number" && typeof clip.y2 === "number";
|
|
@@ -83,10 +201,16 @@ export class CellOutput {
|
|
|
83
201
|
for (const transformer of transformers) {
|
|
84
202
|
transformedLine = transformer(transformedLine, lineIndex);
|
|
85
203
|
}
|
|
204
|
+
if (!transformedLine.includes(ESC) &&
|
|
205
|
+
isPrintableAsciiText(transformedLine)) {
|
|
206
|
+
this.writePlainLine(transformedLine, writeX, row, preserveLineTrailingSpaces, widthCache);
|
|
207
|
+
offsetY++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
86
210
|
const styledChars = styledCharsFromTokens(tokenize(transformedLine));
|
|
87
211
|
let offsetX = writeX;
|
|
88
212
|
for (const char of styledChars) {
|
|
89
|
-
const charWidth =
|
|
213
|
+
const charWidth = getCharWidth(char.value, widthCache);
|
|
90
214
|
if (offsetX < 0) {
|
|
91
215
|
offsetX += charWidth;
|
|
92
216
|
continue;
|
|
@@ -97,7 +221,7 @@ export class CellOutput {
|
|
|
97
221
|
if (offsetX + charWidth > this.buffer.width) {
|
|
98
222
|
break;
|
|
99
223
|
}
|
|
100
|
-
const styleId = this.
|
|
224
|
+
const styleId = this.resolveStyleId(char.styles);
|
|
101
225
|
this.buffer.setCell(offsetX, row, char.value, charWidth, styleId, preserveLineTrailingSpaces);
|
|
102
226
|
offsetX += charWidth;
|
|
103
227
|
}
|
package/lib/core/dom.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Styles } from "./styles.js";
|
|
2
2
|
import { type OutputTransformer } from "./render-node-to-output.js";
|
|
3
3
|
import { TaffyNode } from "./taffy-node.js";
|
|
4
|
+
import { type SizeObserver } from "./size-observer.js";
|
|
4
5
|
/**
|
|
5
6
|
* Interface representing a node in the Tinky tree.
|
|
6
7
|
*/
|
|
@@ -74,6 +75,8 @@ export type DOMElement = {
|
|
|
74
75
|
onRender?: () => void;
|
|
75
76
|
/** Callback to trigger an immediate render. */
|
|
76
77
|
onImmediateRender?: () => void;
|
|
78
|
+
/** Set of resize observers attached to this element. */
|
|
79
|
+
resizeObservers?: Set<SizeObserver>;
|
|
77
80
|
} & TinkyNode;
|
|
78
81
|
/**
|
|
79
82
|
* Interface representing a text node in Tinky.
|
package/lib/core/output.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type OutputTransformer } from "./render-node-to-output.js";
|
|
2
2
|
import { type Dimension } from "../utils/dimension.js";
|
|
3
|
+
import { type AnsiCode } from "../types/ansi.js";
|
|
3
4
|
/**
|
|
4
5
|
* Options for writing text to an output target.
|
|
5
6
|
*/
|
|
@@ -27,6 +28,7 @@ export interface OutputLike {
|
|
|
27
28
|
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
28
29
|
clip(clip: Clip): void;
|
|
29
30
|
unclip(): void;
|
|
31
|
+
fill(x: number, y: number, length: number, char: string, charWidth: number, styles: AnsiCode[]): void;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* "Virtual" output class
|
|
@@ -65,6 +67,7 @@ export declare class Output implements OutputLike {
|
|
|
65
67
|
* Removes the last clipping area, restoring the previous one.
|
|
66
68
|
*/
|
|
67
69
|
unclip(): void;
|
|
70
|
+
fill(x: number, y: number, length: number, char: string, _charWidth: number, styles: AnsiCode[]): void;
|
|
68
71
|
/**
|
|
69
72
|
* Generates the final output string and its height.
|
|
70
73
|
*
|
package/lib/core/output.js
CHANGED
|
@@ -63,6 +63,18 @@ export class Output {
|
|
|
63
63
|
type: "unclip",
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
+
fill(x, y, length, char, _charWidth, styles) {
|
|
67
|
+
if (length <= 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const prefix = styles.map((style) => style.code).join("");
|
|
71
|
+
const suffix = [...styles]
|
|
72
|
+
.reverse()
|
|
73
|
+
.map((style) => style.endCode)
|
|
74
|
+
.join("");
|
|
75
|
+
const text = prefix + char.repeat(length) + suffix;
|
|
76
|
+
this.write(x, y, text, { transformers: [] });
|
|
77
|
+
}
|
|
66
78
|
/**
|
|
67
79
|
* Generates the final output string and its height.
|
|
68
80
|
*
|
package/lib/core/reconciler.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createContext } from "react";
|
|
|
5
5
|
import { createTextNode, appendChildNode, insertBeforeNode, removeChildNode, setStyle, setTextNodeValue, createNode, setAttribute, } from "./dom.js";
|
|
6
6
|
import { applyStyles } from "./styles.js";
|
|
7
7
|
import { process } from "../utils/node-adapter.js";
|
|
8
|
+
import { removeSizeObserversInSubtree } from "./size-observer.js";
|
|
8
9
|
// We need to conditionally perform devtools connection to avoid
|
|
9
10
|
// accidentally breaking other third-party code.
|
|
10
11
|
if (process?.env?.["DEV"] === "true") {
|
|
@@ -204,6 +205,7 @@ export const reconciler = createReconciler({
|
|
|
204
205
|
insertInContainerBefore: insertBeforeNode,
|
|
205
206
|
removeChildFromContainer(node, removeNode) {
|
|
206
207
|
removeChildNode(node, removeNode);
|
|
208
|
+
removeSizeObserversInSubtree(removeNode);
|
|
207
209
|
cleanupTaffyNode(removeNode.taffyNode);
|
|
208
210
|
},
|
|
209
211
|
commitUpdate(node, _type, oldProps, newProps) {
|
|
@@ -241,6 +243,7 @@ export const reconciler = createReconciler({
|
|
|
241
243
|
},
|
|
242
244
|
removeChild(node, removeNode) {
|
|
243
245
|
removeChildNode(node, removeNode);
|
|
246
|
+
removeSizeObserversInSubtree(removeNode);
|
|
244
247
|
cleanupTaffyNode(removeNode.taffyNode);
|
|
245
248
|
},
|
|
246
249
|
setCurrentUpdatePriority(newPriority) {
|
|
@@ -1,13 +1,3 @@
|
|
|
1
1
|
import { type DOMNode } from "./dom.js";
|
|
2
2
|
import { type OutputLike } from "./output.js";
|
|
3
|
-
/**
|
|
4
|
-
* Renders the border for a DOM node.
|
|
5
|
-
* Calculates border dimensions and draws border characters with specified
|
|
6
|
-
* styles.
|
|
7
|
-
*
|
|
8
|
-
* @param x - The x-coordinate (column) where the border starts.
|
|
9
|
-
* @param y - The y-coordinate (row) where the border starts.
|
|
10
|
-
* @param node - The DOM node for which to render the border.
|
|
11
|
-
* @param output - The output instance to write the border to.
|
|
12
|
-
*/
|
|
13
3
|
export declare const renderBorder: (x: number, y: number, node: DOMNode, output: OutputLike) => void;
|