tinky 1.4.4 → 1.6.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/README.ja-JP.md +40 -0
- package/README.md +39 -0
- package/README.zh-CN.md +38 -0
- package/lib/components/Text.js +10 -3
- package/lib/core/cell-buffer.d.ts +89 -0
- package/lib/core/cell-buffer.js +288 -0
- package/lib/core/cell-log-update-run.d.ts +39 -0
- package/lib/core/cell-log-update-run.js +518 -0
- package/lib/core/cell-output.d.ts +13 -0
- package/lib/core/cell-output.js +107 -0
- package/lib/core/cell-renderer.d.ts +25 -0
- package/lib/core/cell-renderer.js +60 -0
- package/lib/core/incremental-rendering.d.ts +43 -0
- package/lib/core/incremental-rendering.js +22 -0
- package/lib/core/output.d.ts +18 -6
- package/lib/core/output.js +61 -8
- package/lib/core/render-border.d.ts +2 -2
- package/lib/core/render-node-to-output.d.ts +2 -2
- package/lib/core/render.d.ts +16 -4
- package/lib/core/tinky.d.ts +28 -3
- package/lib/core/tinky.js +127 -5
- package/lib/index.d.ts +1 -0
- package/lib/utils/ansi-escapes.d.ts +2 -0
- package/lib/utils/ansi-escapes.js +2 -0
- package/lib/utils/render-background.d.ts +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { renderNodeToOutput, renderNodeToScreenReaderOutput, } from "./render-node-to-output.js";
|
|
2
|
+
import { Output } from "./output.js";
|
|
3
|
+
import { CellOutput } from "./cell-output.js";
|
|
4
|
+
/**
|
|
5
|
+
* Renderer that writes interactive output into a CellBuffer.
|
|
6
|
+
*
|
|
7
|
+
* Static output is still rendered as string output because it is emitted once
|
|
8
|
+
* and not updated incrementally.
|
|
9
|
+
*
|
|
10
|
+
* @param node - Root DOM node to render.
|
|
11
|
+
* @param buffer - Reusable destination buffer for the interactive frame.
|
|
12
|
+
* @param isScreenReaderEnabled - Enables screen-reader rendering path.
|
|
13
|
+
* @returns The rendered interactive frame and any static output emitted this pass.
|
|
14
|
+
*/
|
|
15
|
+
export const cellRenderer = (node, buffer, isScreenReaderEnabled) => {
|
|
16
|
+
if (!node.taffyNode) {
|
|
17
|
+
buffer.resize(0, 0);
|
|
18
|
+
buffer.clear();
|
|
19
|
+
return { buffer, outputHeight: 0, staticOutput: "" };
|
|
20
|
+
}
|
|
21
|
+
if (isScreenReaderEnabled) {
|
|
22
|
+
const output = renderNodeToScreenReaderOutput(node, {
|
|
23
|
+
skipStaticElements: true,
|
|
24
|
+
});
|
|
25
|
+
const outputHeight = output === "" ? 0 : output.split("\n").length;
|
|
26
|
+
let staticOutput = "";
|
|
27
|
+
if (node.staticNode) {
|
|
28
|
+
staticOutput = renderNodeToScreenReaderOutput(node.staticNode, {
|
|
29
|
+
skipStaticElements: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
buffer,
|
|
34
|
+
outputHeight,
|
|
35
|
+
staticOutput: staticOutput ? `${staticOutput}\n` : "",
|
|
36
|
+
screenReaderOutput: output,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
40
|
+
buffer.resize(layout.width, layout.height);
|
|
41
|
+
buffer.clear();
|
|
42
|
+
const output = new CellOutput(buffer);
|
|
43
|
+
renderNodeToOutput(node, output, { skipStaticElements: true });
|
|
44
|
+
let staticOutput;
|
|
45
|
+
if (node.staticNode?.taffyNode) {
|
|
46
|
+
const staticLayout = node.staticNode.taffyNode.tree.getLayout(node.staticNode.taffyNode.id);
|
|
47
|
+
staticOutput = new Output({
|
|
48
|
+
width: staticLayout.width,
|
|
49
|
+
height: staticLayout.height,
|
|
50
|
+
});
|
|
51
|
+
renderNodeToOutput(node.staticNode, staticOutput, {
|
|
52
|
+
skipStaticElements: false,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
buffer,
|
|
57
|
+
outputHeight: buffer.height,
|
|
58
|
+
staticOutput: staticOutput ? `${staticOutput.get().output}\n` : "",
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing configuration for incremental rendering.
|
|
3
|
+
*
|
|
4
|
+
* You can use this object form when you need to choose a strategy explicitly
|
|
5
|
+
* or disable incremental rendering without changing existing option shapes.
|
|
6
|
+
*/
|
|
7
|
+
export interface IncrementalRenderingConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Master on/off switch for object mode.
|
|
10
|
+
*
|
|
11
|
+
* - `false`: disables incremental rendering, regardless of `strategy`.
|
|
12
|
+
* - `true` or omitted: enables incremental rendering and uses `strategy`.
|
|
13
|
+
*/
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Diff strategy used for interactive output updates.
|
|
17
|
+
*
|
|
18
|
+
* - `"line"`: compares line strings and redraws changed lines.
|
|
19
|
+
* - `"run"`: compares terminal cells and rewrites minimal changed runs.
|
|
20
|
+
*
|
|
21
|
+
* @defaultValue "run"
|
|
22
|
+
*/
|
|
23
|
+
strategy?: "line" | "run";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Public option accepted by `render(..., { incrementalRendering })`.
|
|
27
|
+
*/
|
|
28
|
+
export type IncrementalRenderingOption = boolean | IncrementalRenderingConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Internal normalized strategy used inside the renderer.
|
|
31
|
+
*/
|
|
32
|
+
export type IncrementalRenderStrategy = "none" | "line" | "run";
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes user-facing incremental rendering options to an internal strategy.
|
|
35
|
+
*
|
|
36
|
+
* Resolution order:
|
|
37
|
+
* - `undefined` or `false` -> `"none"`
|
|
38
|
+
* - `true` -> `"run"`
|
|
39
|
+
* - `{ enabled: false }` -> `"none"`
|
|
40
|
+
* - `{ strategy: "line" }` -> `"line"`
|
|
41
|
+
* - `{ strategy: "run" }` or omitted strategy -> `"run"`
|
|
42
|
+
*/
|
|
43
|
+
export declare const normalizeIncrementalRendering: (value: IncrementalRenderingOption | undefined) => IncrementalRenderStrategy;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes user-facing incremental rendering options to an internal strategy.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* - `undefined` or `false` -> `"none"`
|
|
6
|
+
* - `true` -> `"run"`
|
|
7
|
+
* - `{ enabled: false }` -> `"none"`
|
|
8
|
+
* - `{ strategy: "line" }` -> `"line"`
|
|
9
|
+
* - `{ strategy: "run" }` or omitted strategy -> `"run"`
|
|
10
|
+
*/
|
|
11
|
+
export const normalizeIncrementalRendering = (value) => {
|
|
12
|
+
if (value === undefined || value === false) {
|
|
13
|
+
return "none";
|
|
14
|
+
}
|
|
15
|
+
if (value === true) {
|
|
16
|
+
return "run";
|
|
17
|
+
}
|
|
18
|
+
if (value.enabled === false) {
|
|
19
|
+
return "none";
|
|
20
|
+
}
|
|
21
|
+
return value.strategy ?? "run";
|
|
22
|
+
};
|
package/lib/core/output.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { type OutputTransformer } from "./render-node-to-output.js";
|
|
2
2
|
import { type Dimension } from "../utils/dimension.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for writing text to an output target.
|
|
5
|
+
*/
|
|
6
|
+
export interface OutputWriteOptions {
|
|
7
|
+
/** Array of transformers to apply to each rendered line. */
|
|
8
|
+
transformers: OutputTransformer[];
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* Represents a clipping rectangle.
|
|
5
12
|
*/
|
|
6
|
-
interface Clip {
|
|
13
|
+
export interface Clip {
|
|
7
14
|
/** Left boundary (undefined for no limit). */
|
|
8
15
|
x1: number | undefined;
|
|
9
16
|
/** Right boundary (undefined for no limit). */
|
|
@@ -13,6 +20,14 @@ interface Clip {
|
|
|
13
20
|
/** Bottom boundary (undefined for no limit). */
|
|
14
21
|
y2: number | undefined;
|
|
15
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared write/clip/unclip surface used by render traversal.
|
|
25
|
+
*/
|
|
26
|
+
export interface OutputLike {
|
|
27
|
+
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
28
|
+
clip(clip: Clip): void;
|
|
29
|
+
unclip(): void;
|
|
30
|
+
}
|
|
16
31
|
/**
|
|
17
32
|
* "Virtual" output class
|
|
18
33
|
*
|
|
@@ -21,7 +36,7 @@ interface Clip {
|
|
|
21
36
|
*
|
|
22
37
|
* Used to generate the final output of all nodes before writing to stdout.
|
|
23
38
|
*/
|
|
24
|
-
export declare class Output {
|
|
39
|
+
export declare class Output implements OutputLike {
|
|
25
40
|
width: number;
|
|
26
41
|
height: number;
|
|
27
42
|
private readonly operations;
|
|
@@ -39,9 +54,7 @@ export declare class Output {
|
|
|
39
54
|
* @param text - The text to write.
|
|
40
55
|
* @param options - Options containing transformers.
|
|
41
56
|
*/
|
|
42
|
-
write(x: number, y: number, text: string, options:
|
|
43
|
-
transformers: OutputTransformer[];
|
|
44
|
-
}): void;
|
|
57
|
+
write(x: number, y: number, text: string, options: OutputWriteOptions): void;
|
|
45
58
|
/**
|
|
46
59
|
* Sets a clipping area for subsequent operations.
|
|
47
60
|
*
|
|
@@ -62,4 +75,3 @@ export declare class Output {
|
|
|
62
75
|
height: number;
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
|
-
export {};
|
package/lib/core/output.js
CHANGED
|
@@ -33,7 +33,6 @@ export class Output {
|
|
|
33
33
|
* @param options - Options containing transformers.
|
|
34
34
|
*/
|
|
35
35
|
write(x, y, text, options) {
|
|
36
|
-
const { transformers } = options;
|
|
37
36
|
if (!text) {
|
|
38
37
|
return;
|
|
39
38
|
}
|
|
@@ -42,7 +41,7 @@ export class Output {
|
|
|
42
41
|
x,
|
|
43
42
|
y,
|
|
44
43
|
text,
|
|
45
|
-
|
|
44
|
+
options,
|
|
46
45
|
});
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
@@ -73,6 +72,10 @@ export class Output {
|
|
|
73
72
|
// Initialize output array with a specific set of rows, so that
|
|
74
73
|
// margin/padding at the bottom is preserved
|
|
75
74
|
const output = [];
|
|
75
|
+
// Tracks whether a cell was explicitly written this frame.
|
|
76
|
+
const touched = [];
|
|
77
|
+
// Preserves intentional trailing spaces produced by transformers.
|
|
78
|
+
const preserveTrailingSpace = [];
|
|
76
79
|
for (let y = 0; y < this.height; y++) {
|
|
77
80
|
const row = [];
|
|
78
81
|
for (let x = 0; x < this.width; x++) {
|
|
@@ -83,7 +86,11 @@ export class Output {
|
|
|
83
86
|
styles: [],
|
|
84
87
|
});
|
|
85
88
|
}
|
|
89
|
+
const rowTouched = new Array(this.width).fill(false);
|
|
90
|
+
const rowPreserve = new Array(this.width).fill(false);
|
|
86
91
|
output.push(row);
|
|
92
|
+
touched.push(rowTouched);
|
|
93
|
+
preserveTrailingSpace.push(rowPreserve);
|
|
87
94
|
}
|
|
88
95
|
const clips = [];
|
|
89
96
|
for (const operation of this.operations) {
|
|
@@ -94,9 +101,11 @@ export class Output {
|
|
|
94
101
|
clips.pop();
|
|
95
102
|
}
|
|
96
103
|
if (operation.type === "write") {
|
|
97
|
-
const { text
|
|
104
|
+
const { text } = operation;
|
|
105
|
+
const { transformers } = operation.options;
|
|
98
106
|
let { x, y } = operation;
|
|
99
107
|
let lines = text.split("\n");
|
|
108
|
+
const preserveLineTrailingSpaces = transformers.length > 0;
|
|
100
109
|
const clip = clips.at(-1);
|
|
101
110
|
if (clip) {
|
|
102
111
|
const clipHorizontally = typeof clip.x1 === "number" && typeof clip.x2 === "number";
|
|
@@ -142,10 +151,13 @@ export class Output {
|
|
|
142
151
|
}
|
|
143
152
|
let offsetY = 0;
|
|
144
153
|
for (const [index, line] of lines.entries()) {
|
|
145
|
-
const
|
|
154
|
+
const rowIndex = y + offsetY;
|
|
155
|
+
const currentLine = output[rowIndex];
|
|
156
|
+
const touchedRow = touched[rowIndex];
|
|
157
|
+
const preserveRow = preserveTrailingSpace[rowIndex];
|
|
146
158
|
// Line can be missing if `text` is taller than height of
|
|
147
159
|
// pre-initialized `this.output`
|
|
148
|
-
if (!currentLine) {
|
|
160
|
+
if (!currentLine || !touchedRow || !preserveRow) {
|
|
149
161
|
continue;
|
|
150
162
|
}
|
|
151
163
|
let transformedLine = line;
|
|
@@ -156,6 +168,8 @@ export class Output {
|
|
|
156
168
|
let offsetX = x;
|
|
157
169
|
for (const character of characters) {
|
|
158
170
|
currentLine[offsetX] = character;
|
|
171
|
+
touchedRow[offsetX] = true;
|
|
172
|
+
preserveRow[offsetX] = preserveLineTrailingSpaces;
|
|
159
173
|
// Determine printed width using string-width to align with measurement
|
|
160
174
|
const characterWidth = Math.max(1, stringWidth(character.value));
|
|
161
175
|
// For multi-column characters, clear following cells to avoid stray
|
|
@@ -168,6 +182,8 @@ export class Output {
|
|
|
168
182
|
fullWidth: false,
|
|
169
183
|
styles: character.styles,
|
|
170
184
|
};
|
|
185
|
+
touchedRow[offsetX + index] = true;
|
|
186
|
+
preserveRow[offsetX + index] = preserveLineTrailingSpaces;
|
|
171
187
|
}
|
|
172
188
|
}
|
|
173
189
|
offsetX += characterWidth;
|
|
@@ -177,9 +193,46 @@ export class Output {
|
|
|
177
193
|
}
|
|
178
194
|
}
|
|
179
195
|
const generatedOutput = output
|
|
180
|
-
.map((line) => {
|
|
181
|
-
const
|
|
182
|
-
|
|
196
|
+
.map((line, rowIndex) => {
|
|
197
|
+
const rowTouched = touched[rowIndex] ?? [];
|
|
198
|
+
const rowPreserve = preserveTrailingSpace[rowIndex] ?? [];
|
|
199
|
+
const compactLine = [];
|
|
200
|
+
const compactTouched = [];
|
|
201
|
+
const compactPreserve = [];
|
|
202
|
+
for (let index = 0; index < line.length; index++) {
|
|
203
|
+
const item = line[index];
|
|
204
|
+
if (item === undefined) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
compactLine.push(item);
|
|
208
|
+
compactTouched.push(rowTouched[index] === true);
|
|
209
|
+
compactPreserve.push(rowPreserve[index] === true);
|
|
210
|
+
}
|
|
211
|
+
let end = compactLine.length - 1;
|
|
212
|
+
while (end >= 0) {
|
|
213
|
+
const item = compactLine[end];
|
|
214
|
+
if (!item) {
|
|
215
|
+
end--;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!compactTouched[end]) {
|
|
219
|
+
end--;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const canTrimUnstyledSpace = item.type === "char" &&
|
|
223
|
+
item.value === " " &&
|
|
224
|
+
item.styles.length === 0 &&
|
|
225
|
+
!compactPreserve[end];
|
|
226
|
+
if (canTrimUnstyledSpace) {
|
|
227
|
+
end--;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (end < 0) {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
return styledCharsToString(compactLine.slice(0, end + 1));
|
|
183
236
|
})
|
|
184
237
|
.join("\n");
|
|
185
238
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DOMNode } from "./dom.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type OutputLike } from "./output.js";
|
|
3
3
|
/**
|
|
4
4
|
* Renders the border for a DOM node.
|
|
5
5
|
* Calculates border dimensions and draws border characters with specified
|
|
@@ -10,4 +10,4 @@ import { type Output } from "./output.js";
|
|
|
10
10
|
* @param node - The DOM node for which to render the border.
|
|
11
11
|
* @param output - The output instance to write the border to.
|
|
12
12
|
*/
|
|
13
|
-
export declare const renderBorder: (x: number, y: number, node: DOMNode, output:
|
|
13
|
+
export declare const renderBorder: (x: number, y: number, node: DOMNode, output: OutputLike) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DOMElement } from "./dom.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type OutputLike } from "./output.js";
|
|
3
3
|
/**
|
|
4
4
|
* Function type for transforming output text.
|
|
5
5
|
*/
|
|
@@ -35,7 +35,7 @@ export declare const renderNodeToScreenReaderOutput: (node: DOMElement, options?
|
|
|
35
35
|
* @param options.transformers - Array of text transformers.
|
|
36
36
|
* @param options.skipStaticElements - Whether to skip static elements.
|
|
37
37
|
*/
|
|
38
|
-
export declare const renderNodeToOutput: (node: DOMElement, output:
|
|
38
|
+
export declare const renderNodeToOutput: (node: DOMElement, output: OutputLike, options: {
|
|
39
39
|
offsetX?: number;
|
|
40
40
|
offsetY?: number;
|
|
41
41
|
transformers?: OutputTransformer[];
|
package/lib/core/render.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type ReadStream, type WriteStream } from "../types/io.js";
|
|
3
3
|
import { Tinky, type RenderMetrics } from "./tinky.js";
|
|
4
|
+
import { type IncrementalRenderingConfig, type IncrementalRenderingOption } from "./incremental-rendering.js";
|
|
5
|
+
export type { IncrementalRenderingConfig, IncrementalRenderingOption };
|
|
4
6
|
/**
|
|
5
7
|
* Options for the render function.
|
|
6
8
|
*/
|
|
@@ -64,13 +66,23 @@ export interface RenderOptions {
|
|
|
64
66
|
*/
|
|
65
67
|
maxFps?: number;
|
|
66
68
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
69
|
+
* Configure incremental rendering mode.
|
|
70
|
+
*
|
|
71
|
+
* - `true`: Enables run-diff incremental rendering.
|
|
72
|
+
* - `false` or omitted: Disables incremental rendering.
|
|
73
|
+
* - Object mode:
|
|
74
|
+
* - `{ enabled: false }` disables incremental rendering.
|
|
75
|
+
* - `{ strategy: "line" }` enables line-diff incremental rendering.
|
|
76
|
+
* - `{ strategy: "run" }` (or omitted strategy) enables run-diff rendering.
|
|
77
|
+
*
|
|
78
|
+
* Runtime notes:
|
|
79
|
+
* - In `debug` mode, Tinky always writes full frames.
|
|
80
|
+
* - In screen-reader mode, Tinky uses the screen-reader output path.
|
|
81
|
+
* - In CI mode, Tinky avoids cursor-diff updates.
|
|
70
82
|
*
|
|
71
83
|
* @defaultValue false
|
|
72
84
|
*/
|
|
73
|
-
incrementalRendering?:
|
|
85
|
+
incrementalRendering?: IncrementalRenderingOption;
|
|
74
86
|
/**
|
|
75
87
|
* Environment variables.
|
|
76
88
|
*
|
package/lib/core/tinky.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type ReadStream, type WriteStream } from "../types/io.js";
|
|
3
|
+
import { type IncrementalRenderingOption } from "./incremental-rendering.js";
|
|
3
4
|
/**
|
|
4
5
|
* Performance metrics for a render operation.
|
|
5
6
|
*/
|
|
@@ -59,10 +60,15 @@ export interface Options {
|
|
|
59
60
|
*/
|
|
60
61
|
maxFps?: number;
|
|
61
62
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* Configure incremental rendering mode.
|
|
64
|
+
*
|
|
65
|
+
* - `true`: Enables run-diff incremental rendering.
|
|
66
|
+
* - `false` or omitted: Disables incremental rendering.
|
|
67
|
+
* - `{ enabled: false }`: Disables incremental rendering.
|
|
68
|
+
* - `{ strategy: "line" }`: Enables line-diff incremental rendering.
|
|
69
|
+
* - `{ strategy: "run" }` (or omitted strategy): Enables run-diff rendering.
|
|
64
70
|
*/
|
|
65
|
-
incrementalRendering?:
|
|
71
|
+
incrementalRendering?: IncrementalRenderingOption;
|
|
66
72
|
/**
|
|
67
73
|
* Environment variables.
|
|
68
74
|
*/
|
|
@@ -103,6 +109,16 @@ export declare class Tinky {
|
|
|
103
109
|
private readonly unsubscribeResize?;
|
|
104
110
|
/** Whether we are running in a CI environment. */
|
|
105
111
|
private readonly isCI;
|
|
112
|
+
/** Whether run-diff incremental rendering is active for this instance. */
|
|
113
|
+
private readonly usesRunIncrementalRendering;
|
|
114
|
+
/** Run-diff log updater. */
|
|
115
|
+
private readonly cellLog?;
|
|
116
|
+
/** Shared style registry for run buffers. */
|
|
117
|
+
private readonly styleRegistry?;
|
|
118
|
+
/** Previous rendered frame for run-diff strategy. */
|
|
119
|
+
private frontBuffer?;
|
|
120
|
+
/** Current frame render target for run-diff strategy. */
|
|
121
|
+
private backBuffer?;
|
|
106
122
|
/**
|
|
107
123
|
* Creates an instance of Tinky.
|
|
108
124
|
*
|
|
@@ -137,6 +153,15 @@ export declare class Tinky {
|
|
|
137
153
|
* Handles string generation, static output, screen reader logic, and writing.
|
|
138
154
|
*/
|
|
139
155
|
onRender: () => void;
|
|
156
|
+
private isRunFrameEqual;
|
|
157
|
+
/**
|
|
158
|
+
* Swaps front and back buffers. After swap, the caller's newly-rendered
|
|
159
|
+
* frame becomes the front buffer. The old front becomes the back buffer
|
|
160
|
+
* and will be cleared by cellRenderer (resize + clear) at the start of
|
|
161
|
+
* the next render cycle.
|
|
162
|
+
*/
|
|
163
|
+
private swapRunBuffers;
|
|
164
|
+
private redrawRunBuffer;
|
|
140
165
|
/**
|
|
141
166
|
* Renders the given React node.
|
|
142
167
|
*
|
package/lib/core/tinky.js
CHANGED
|
@@ -9,11 +9,15 @@ import { LegacyRoot } from "react-reconciler/constants.js";
|
|
|
9
9
|
import wrapAnsi from "wrap-ansi";
|
|
10
10
|
import { reconciler } from "./reconciler.js";
|
|
11
11
|
import { renderer } from "./renderer.js";
|
|
12
|
+
import { cellRenderer } from "./cell-renderer.js";
|
|
12
13
|
import * as dom from "./dom.js";
|
|
13
14
|
import { logUpdate } from "./log-update.js";
|
|
15
|
+
import { cellLogUpdateRun, } from "./cell-log-update-run.js";
|
|
14
16
|
import { instances } from "./instances.js";
|
|
15
17
|
import { App } from "../components/App.js";
|
|
16
18
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
19
|
+
import { CellBuffer, StyleRegistry } from "./cell-buffer.js";
|
|
20
|
+
import { normalizeIncrementalRendering, } from "./incremental-rendering.js";
|
|
17
21
|
const noop = () => {
|
|
18
22
|
// no-op
|
|
19
23
|
};
|
|
@@ -52,6 +56,16 @@ export class Tinky {
|
|
|
52
56
|
unsubscribeResize;
|
|
53
57
|
/** Whether we are running in a CI environment. */
|
|
54
58
|
isCI;
|
|
59
|
+
/** Whether run-diff incremental rendering is active for this instance. */
|
|
60
|
+
usesRunIncrementalRendering;
|
|
61
|
+
/** Run-diff log updater. */
|
|
62
|
+
cellLog;
|
|
63
|
+
/** Shared style registry for run buffers. */
|
|
64
|
+
styleRegistry;
|
|
65
|
+
/** Previous rendered frame for run-diff strategy. */
|
|
66
|
+
frontBuffer;
|
|
67
|
+
/** Current frame render target for run-diff strategy. */
|
|
68
|
+
backBuffer;
|
|
55
69
|
/**
|
|
56
70
|
* Creates an instance of Tinky.
|
|
57
71
|
*
|
|
@@ -65,6 +79,16 @@ export class Tinky {
|
|
|
65
79
|
this.isScreenReaderEnabled =
|
|
66
80
|
options.isScreenReaderEnabled ??
|
|
67
81
|
options.env?.["TINKY_SCREEN_READER"] === "true";
|
|
82
|
+
this.isCI = isCI(options.env);
|
|
83
|
+
const incrementalRenderStrategy = normalizeIncrementalRendering(options.incrementalRendering);
|
|
84
|
+
// Run-diff uses cursor addressing and interactive frame tracking, so we keep
|
|
85
|
+
// existing render paths in debug/screen-reader/CI modes.
|
|
86
|
+
this.usesRunIncrementalRendering =
|
|
87
|
+
incrementalRenderStrategy === "run" &&
|
|
88
|
+
!options.debug &&
|
|
89
|
+
!this.isScreenReaderEnabled &&
|
|
90
|
+
!this.isCI;
|
|
91
|
+
const useLineIncremental = incrementalRenderStrategy === "line";
|
|
68
92
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
69
93
|
const maxFps = options.maxFps ?? 30;
|
|
70
94
|
const renderThrottleMs = maxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;
|
|
@@ -76,7 +100,7 @@ export class Tinky {
|
|
|
76
100
|
});
|
|
77
101
|
this.rootNode.onImmediateRender = this.onRender;
|
|
78
102
|
this.log = logUpdate.create(options.stdout, {
|
|
79
|
-
incremental:
|
|
103
|
+
incremental: useLineIncremental,
|
|
80
104
|
});
|
|
81
105
|
this.throttledLog = unthrottled
|
|
82
106
|
? this.log
|
|
@@ -84,6 +108,17 @@ export class Tinky {
|
|
|
84
108
|
leading: true,
|
|
85
109
|
trailing: true,
|
|
86
110
|
});
|
|
111
|
+
if (this.usesRunIncrementalRendering) {
|
|
112
|
+
this.styleRegistry = new StyleRegistry();
|
|
113
|
+
this.frontBuffer = new CellBuffer({ width: 0, height: 0 }, this.styleRegistry);
|
|
114
|
+
this.backBuffer = new CellBuffer({ width: 0, height: 0 }, this.styleRegistry);
|
|
115
|
+
this.cellLog = cellLogUpdateRun.create(options.stdout, {
|
|
116
|
+
incremental: true,
|
|
117
|
+
mergeStrategy: "cost",
|
|
118
|
+
maxSegmentsBeforeFullRow: 12,
|
|
119
|
+
overwriteGapPenaltyBytes: 0,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
87
122
|
// Ignore last render after unmounting a tree to prevent empty output before
|
|
88
123
|
// exit
|
|
89
124
|
this.isUnmounted = false;
|
|
@@ -108,7 +143,6 @@ export class Tinky {
|
|
|
108
143
|
rendererPackageName: "tinky",
|
|
109
144
|
});
|
|
110
145
|
}
|
|
111
|
-
this.isCI = isCI(options.env);
|
|
112
146
|
if (this.options.patchConsole) {
|
|
113
147
|
this.patchConsole();
|
|
114
148
|
}
|
|
@@ -138,8 +172,16 @@ export class Tinky {
|
|
|
138
172
|
if (currentWidth < this.lastTerminalWidth) {
|
|
139
173
|
// We clear the screen when decreasing terminal width to prevent duplicate
|
|
140
174
|
// overlapping re-renders.
|
|
141
|
-
this.
|
|
175
|
+
if (this.usesRunIncrementalRendering) {
|
|
176
|
+
this.cellLog?.clear();
|
|
177
|
+
this.frontBuffer?.resize(0, 0);
|
|
178
|
+
this.frontBuffer?.clear();
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.log.clear();
|
|
182
|
+
}
|
|
142
183
|
this.lastOutput = "";
|
|
184
|
+
this.lastOutputHeight = 0;
|
|
143
185
|
}
|
|
144
186
|
this.calculateLayout();
|
|
145
187
|
this.onRender();
|
|
@@ -186,6 +228,42 @@ export class Tinky {
|
|
|
186
228
|
if (this.isUnmounted) {
|
|
187
229
|
return;
|
|
188
230
|
}
|
|
231
|
+
if (this.usesRunIncrementalRendering && this.backBuffer) {
|
|
232
|
+
const startTime = performance.now();
|
|
233
|
+
const { buffer, outputHeight, staticOutput } = cellRenderer(this.rootNode, this.backBuffer, false);
|
|
234
|
+
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
235
|
+
const hasStaticOutput = staticOutput && staticOutput !== "\n";
|
|
236
|
+
if (hasStaticOutput) {
|
|
237
|
+
this.fullStaticOutput += staticOutput;
|
|
238
|
+
}
|
|
239
|
+
if (this.options.stdout.rows &&
|
|
240
|
+
this.lastOutputHeight >= this.options.stdout.rows) {
|
|
241
|
+
const output = buffer.toString();
|
|
242
|
+
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
243
|
+
this.lastOutput = output;
|
|
244
|
+
this.lastOutputHeight = outputHeight;
|
|
245
|
+
this.cellLog?.sync(buffer);
|
|
246
|
+
this.swapRunBuffers();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (hasStaticOutput) {
|
|
250
|
+
// Static output is appended once, then the interactive frame is redrawn.
|
|
251
|
+
this.cellLog?.clear();
|
|
252
|
+
this.options.stdout.write(staticOutput);
|
|
253
|
+
if (this.frontBuffer && this.cellLog) {
|
|
254
|
+
this.cellLog(this.frontBuffer, buffer, { forceFull: true });
|
|
255
|
+
this.swapRunBuffers();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (this.frontBuffer &&
|
|
259
|
+
this.cellLog &&
|
|
260
|
+
!this.isRunFrameEqual(buffer)) {
|
|
261
|
+
this.cellLog(this.frontBuffer, buffer);
|
|
262
|
+
this.swapRunBuffers();
|
|
263
|
+
}
|
|
264
|
+
this.lastOutputHeight = outputHeight;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
189
267
|
const startTime = performance.now();
|
|
190
268
|
const { output, outputHeight, staticOutput } = renderer(this.rootNode, this.isScreenReaderEnabled);
|
|
191
269
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
@@ -264,6 +342,28 @@ export class Tinky {
|
|
|
264
342
|
this.lastOutput = output;
|
|
265
343
|
this.lastOutputHeight = outputHeight;
|
|
266
344
|
};
|
|
345
|
+
isRunFrameEqual(nextBuffer) {
|
|
346
|
+
return this.frontBuffer?.isEqual(nextBuffer) ?? false;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Swaps front and back buffers. After swap, the caller's newly-rendered
|
|
350
|
+
* frame becomes the front buffer. The old front becomes the back buffer
|
|
351
|
+
* and will be cleared by cellRenderer (resize + clear) at the start of
|
|
352
|
+
* the next render cycle.
|
|
353
|
+
*/
|
|
354
|
+
swapRunBuffers() {
|
|
355
|
+
const previous = this.frontBuffer;
|
|
356
|
+
this.frontBuffer = this.backBuffer;
|
|
357
|
+
this.backBuffer = previous;
|
|
358
|
+
}
|
|
359
|
+
redrawRunBuffer() {
|
|
360
|
+
if (!this.frontBuffer || !this.cellLog) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.cellLog(this.frontBuffer, this.frontBuffer, { forceFull: true });
|
|
364
|
+
this.lastOutput = this.frontBuffer.toString();
|
|
365
|
+
this.lastOutputHeight = this.frontBuffer.height;
|
|
366
|
+
}
|
|
267
367
|
/**
|
|
268
368
|
* Renders the given React node.
|
|
269
369
|
*
|
|
@@ -291,6 +391,12 @@ export class Tinky {
|
|
|
291
391
|
this.options.stdout.write(data);
|
|
292
392
|
return;
|
|
293
393
|
}
|
|
394
|
+
if (this.usesRunIncrementalRendering) {
|
|
395
|
+
this.cellLog?.clear();
|
|
396
|
+
this.options.stdout.write(data);
|
|
397
|
+
this.redrawRunBuffer();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
294
400
|
this.log.clear();
|
|
295
401
|
this.options.stdout.write(data);
|
|
296
402
|
this.log(this.lastOutput);
|
|
@@ -313,6 +419,12 @@ export class Tinky {
|
|
|
313
419
|
this.options.stderr.write(data);
|
|
314
420
|
return;
|
|
315
421
|
}
|
|
422
|
+
if (this.usesRunIncrementalRendering) {
|
|
423
|
+
this.cellLog?.clear();
|
|
424
|
+
this.options.stderr.write(data);
|
|
425
|
+
this.redrawRunBuffer();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
316
428
|
this.log.clear();
|
|
317
429
|
this.options.stderr.write(data);
|
|
318
430
|
this.log(this.lastOutput);
|
|
@@ -345,7 +457,12 @@ export class Tinky {
|
|
|
345
457
|
this.options.stdout.write(this.lastOutput + "\n");
|
|
346
458
|
}
|
|
347
459
|
else if (!this.options.debug) {
|
|
348
|
-
this.
|
|
460
|
+
if (this.usesRunIncrementalRendering) {
|
|
461
|
+
this.cellLog?.done();
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this.log.done();
|
|
465
|
+
}
|
|
349
466
|
}
|
|
350
467
|
this.isUnmounted = true;
|
|
351
468
|
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
@@ -387,7 +504,12 @@ export class Tinky {
|
|
|
387
504
|
clear() {
|
|
388
505
|
const isCiEnv = isCI(this.options.env);
|
|
389
506
|
if (!isCiEnv) {
|
|
390
|
-
this.
|
|
507
|
+
if (this.usesRunIncrementalRendering) {
|
|
508
|
+
this.cellLog?.clear();
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
this.log.clear();
|
|
512
|
+
}
|
|
391
513
|
}
|
|
392
514
|
}
|
|
393
515
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type ReadStream, type WriteStream } from "./types/io.js";
|
|
2
2
|
export { render, type RenderOptions, type Instance } from "./core/render.js";
|
|
3
|
+
export { type IncrementalRenderingConfig, type IncrementalRenderingOption, } from "./core/render.js";
|
|
3
4
|
export { Box, type BoxProps } from "./components/Box.js";
|
|
4
5
|
export { Text, type TextProps } from "./components/Text.js";
|
|
5
6
|
export { AppContext, type AppProps } from "./contexts/AppContext.js";
|