tinky 1.5.0 → 1.7.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 +91 -0
- package/lib/core/cell-buffer.js +346 -0
- package/lib/core/cell-log-update-run.d.ts +40 -0
- package/lib/core/cell-log-update-run.js +526 -0
- package/lib/core/cell-output.d.ts +20 -0
- package/lib/core/cell-output.js +231 -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 +21 -6
- package/lib/core/output.js +73 -8
- package/lib/core/render-border.d.ts +2 -12
- package/lib/core/render-border.js +78 -69
- package/lib/core/render-node-to-output.d.ts +2 -2
- package/lib/core/render.d.ts +17 -4
- package/lib/core/renderer.js +9 -6
- package/lib/core/tinky.d.ts +27 -3
- package/lib/core/tinky.js +125 -5
- package/lib/index.d.ts +1 -0
- package/lib/types/ansi.d.ts +5 -0
- package/lib/types/ansi.js +1 -0
- package/lib/utils/ansi-escapes.d.ts +2 -0
- package/lib/utils/ansi-escapes.js +2 -0
- package/lib/utils/colorize.d.ts +5 -0
- package/lib/utils/colorize.js +42 -20
- package/lib/utils/render-background.d.ts +2 -2
- 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
|
@@ -1,82 +1,91 @@
|
|
|
1
1
|
import { boxStyles } from "./box-styles.js";
|
|
2
2
|
import ansis from "ansis";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { getStyle } from "../utils/colorize.js";
|
|
5
|
+
// Helper to resolve styles once
|
|
6
|
+
const resolveBorderStyles = (color, dim) => {
|
|
7
|
+
const styles = [];
|
|
8
|
+
if (dim) {
|
|
9
|
+
styles.push({
|
|
10
|
+
type: "ansi",
|
|
11
|
+
code: ansis.dim.open,
|
|
12
|
+
endCode: ansis.dim.close,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const colorStyle = getStyle(color, "foreground");
|
|
16
|
+
if (colorStyle) {
|
|
17
|
+
styles.push(colorStyle);
|
|
18
|
+
}
|
|
19
|
+
return styles;
|
|
20
|
+
};
|
|
14
21
|
export const renderBorder = (x, y, node, output) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const contentWidth = width - (showLeftBorder ? 1 : 0) - (showRightBorder ? 1 : 0);
|
|
35
|
-
let topBorder = showTopBorder
|
|
36
|
-
? colorize((showLeftBorder ? box.topLeft : "") +
|
|
37
|
-
box.top.repeat(contentWidth) +
|
|
38
|
-
(showRightBorder ? box.topRight : ""), topBorderColor, "foreground")
|
|
39
|
-
: undefined;
|
|
40
|
-
if (showTopBorder && dimTopBorderColor) {
|
|
41
|
-
topBorder = ansis.dim(topBorder);
|
|
42
|
-
}
|
|
43
|
-
let verticalBorderHeight = height;
|
|
44
|
-
if (showTopBorder) {
|
|
45
|
-
verticalBorderHeight -= 1;
|
|
46
|
-
}
|
|
47
|
-
if (showBottomBorder) {
|
|
48
|
-
verticalBorderHeight -= 1;
|
|
49
|
-
}
|
|
50
|
-
let leftBorder = (colorize(box.left, leftBorderColor, "foreground") + "\n").repeat(verticalBorderHeight);
|
|
51
|
-
if (dimLeftBorderColor) {
|
|
52
|
-
leftBorder = ansis.dim(leftBorder);
|
|
22
|
+
const { borderStyle } = node.style;
|
|
23
|
+
if (!borderStyle) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const layout = node.taffyNode?.tree.getLayout(node.taffyNode.id);
|
|
27
|
+
const width = layout?.width ?? 0;
|
|
28
|
+
const height = layout?.height ?? 0;
|
|
29
|
+
const box = typeof borderStyle === "string" ? boxStyles[borderStyle] : borderStyle;
|
|
30
|
+
const { borderColor, borderTopColor, borderBottomColor, borderLeftColor, borderRightColor, borderDimColor, borderTopDimColor, borderBottomDimColor, borderLeftDimColor, borderRightDimColor, } = node.style;
|
|
31
|
+
const showTopBorder = node.style.borderTop !== false;
|
|
32
|
+
const showBottomBorder = node.style.borderBottom !== false;
|
|
33
|
+
const showLeftBorder = node.style.borderLeft !== false;
|
|
34
|
+
const showRightBorder = node.style.borderRight !== false;
|
|
35
|
+
const contentWidth = width - (showLeftBorder ? 1 : 0) - (showRightBorder ? 1 : 0);
|
|
36
|
+
// Top Border
|
|
37
|
+
if (showTopBorder) {
|
|
38
|
+
const styles = resolveBorderStyles(borderTopColor ?? borderColor, borderTopDimColor ?? borderDimColor);
|
|
39
|
+
if (showLeftBorder) {
|
|
40
|
+
output.fill(x, y, 1, box.topLeft, stringWidth(box.topLeft), styles);
|
|
53
41
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rightBorder = ansis.dim(rightBorder);
|
|
42
|
+
if (contentWidth > 0) {
|
|
43
|
+
output.fill(x + (showLeftBorder ? 1 : 0), y, contentWidth, box.top, stringWidth(box.top), styles);
|
|
57
44
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
box.bottom.repeat(contentWidth) +
|
|
61
|
-
(showRightBorder ? box.bottomRight : ""), bottomBorderColor, "foreground")
|
|
62
|
-
: undefined;
|
|
63
|
-
if (showBottomBorder && dimBottomBorderColor) {
|
|
64
|
-
bottomBorder = ansis.dim(bottomBorder);
|
|
45
|
+
if (showRightBorder) {
|
|
46
|
+
output.fill(x + width - 1, y, 1, box.topRight, stringWidth(box.topRight), styles);
|
|
65
47
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
}
|
|
49
|
+
// Vertical Borders
|
|
50
|
+
let verticalBorderHeight = height;
|
|
51
|
+
if (showTopBorder)
|
|
52
|
+
verticalBorderHeight -= 1;
|
|
53
|
+
if (showBottomBorder)
|
|
54
|
+
verticalBorderHeight -= 1;
|
|
55
|
+
if (verticalBorderHeight > 0) {
|
|
56
|
+
const offsetY = y + (showTopBorder ? 1 : 0);
|
|
57
|
+
// Optimization: Resolve styles once
|
|
58
|
+
const leftStyles = showLeftBorder
|
|
59
|
+
? resolveBorderStyles(borderLeftColor ?? borderColor, borderLeftDimColor ?? borderDimColor)
|
|
60
|
+
: [];
|
|
61
|
+
const rightStyles = showRightBorder
|
|
62
|
+
? resolveBorderStyles(borderRightColor ?? borderColor, borderRightDimColor ?? borderDimColor)
|
|
63
|
+
: [];
|
|
64
|
+
// Optimization: Calculate widths once
|
|
65
|
+
const leftWidth = showLeftBorder ? stringWidth(box.left) : 0;
|
|
66
|
+
const rightWidth = showRightBorder ? stringWidth(box.right) : 0;
|
|
67
|
+
for (let i = 0; i < verticalBorderHeight; i++) {
|
|
68
|
+
const row = offsetY + i;
|
|
69
|
+
if (showLeftBorder) {
|
|
70
|
+
output.fill(x, row, 1, box.left, leftWidth, leftStyles);
|
|
71
|
+
}
|
|
72
|
+
if (showRightBorder) {
|
|
73
|
+
output.fill(x + width - 1, row, 1, box.right, rightWidth, rightStyles);
|
|
74
|
+
}
|
|
69
75
|
}
|
|
76
|
+
}
|
|
77
|
+
// Bottom Border
|
|
78
|
+
if (showBottomBorder) {
|
|
79
|
+
const styles = resolveBorderStyles(borderBottomColor ?? borderColor, borderBottomDimColor ?? borderDimColor);
|
|
80
|
+
const bottomY = y + height - 1;
|
|
70
81
|
if (showLeftBorder) {
|
|
71
|
-
output.
|
|
82
|
+
output.fill(x, bottomY, 1, box.bottomLeft, stringWidth(box.bottomLeft), styles);
|
|
72
83
|
}
|
|
73
|
-
if (
|
|
74
|
-
output.
|
|
75
|
-
transformers: [],
|
|
76
|
-
});
|
|
84
|
+
if (contentWidth > 0) {
|
|
85
|
+
output.fill(x + (showLeftBorder ? 1 : 0), bottomY, contentWidth, box.bottom, stringWidth(box.bottom), styles);
|
|
77
86
|
}
|
|
78
|
-
if (
|
|
79
|
-
output.
|
|
87
|
+
if (showRightBorder) {
|
|
88
|
+
output.fill(x + width - 1, bottomY, 1, box.bottomRight, stringWidth(box.bottomRight), styles);
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
};
|
|
@@ -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,24 @@ 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 `run` strategy, terminal writes are skipped entirely when frames are unchanged.
|
|
80
|
+
* - In `debug` mode, Tinky always writes full frames.
|
|
81
|
+
* - In screen-reader mode, Tinky uses the screen-reader output path.
|
|
82
|
+
* - In CI mode, Tinky avoids cursor-diff updates.
|
|
70
83
|
*
|
|
71
84
|
* @defaultValue false
|
|
72
85
|
*/
|
|
73
|
-
incrementalRendering?:
|
|
86
|
+
incrementalRendering?: IncrementalRenderingOption;
|
|
74
87
|
/**
|
|
75
88
|
* Environment variables.
|
|
76
89
|
*
|
package/lib/core/renderer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderNodeToOutput, renderNodeToScreenReaderOutput, } from "./render-node-to-output.js";
|
|
2
|
-
import {
|
|
2
|
+
import { CellBuffer } from "./cell-buffer.js";
|
|
3
|
+
import { CellOutput } from "./cell-output.js";
|
|
3
4
|
/**
|
|
4
5
|
* Renders the DOM tree to a string output.
|
|
5
6
|
*
|
|
@@ -27,31 +28,33 @@ export const renderer = (node, isScreenReaderEnabled) => {
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
const layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
30
|
-
const
|
|
31
|
+
const buffer = new CellBuffer({
|
|
31
32
|
width: layout.width,
|
|
32
33
|
height: layout.height,
|
|
33
34
|
});
|
|
35
|
+
const output = new CellOutput(buffer);
|
|
34
36
|
renderNodeToOutput(node, output, {
|
|
35
37
|
skipStaticElements: true,
|
|
36
38
|
});
|
|
37
39
|
let staticOutput;
|
|
38
40
|
if (node.staticNode?.taffyNode) {
|
|
39
41
|
const staticLayout = node.staticNode.taffyNode.tree.getLayout(node.staticNode.taffyNode.id);
|
|
40
|
-
|
|
42
|
+
const staticBuffer = new CellBuffer({
|
|
41
43
|
width: staticLayout.width,
|
|
42
44
|
height: staticLayout.height,
|
|
43
45
|
});
|
|
46
|
+
staticOutput = new CellOutput(staticBuffer);
|
|
44
47
|
renderNodeToOutput(node.staticNode, staticOutput, {
|
|
45
48
|
skipStaticElements: false,
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
|
-
const
|
|
51
|
+
const generatedOutput = buffer.toString();
|
|
49
52
|
return {
|
|
50
53
|
output: generatedOutput,
|
|
51
|
-
outputHeight,
|
|
54
|
+
outputHeight: buffer.height,
|
|
52
55
|
// Newline at the end is needed, because static output doesn't have one,
|
|
53
56
|
// so interactive output will override last line of static output
|
|
54
|
-
staticOutput: staticOutput ? `${staticOutput.
|
|
57
|
+
staticOutput: staticOutput ? `${staticOutput.buffer.toString()}\n` : "",
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
return {
|
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,14 @@ export declare class Tinky {
|
|
|
137
153
|
* Handles string generation, static output, screen reader logic, and writing.
|
|
138
154
|
*/
|
|
139
155
|
onRender: () => void;
|
|
156
|
+
/**
|
|
157
|
+
* Swaps front and back buffers. After swap, the caller's newly-rendered
|
|
158
|
+
* frame becomes the front buffer. The old front becomes the back buffer
|
|
159
|
+
* and will be cleared by cellRenderer (resize + clear) at the start of
|
|
160
|
+
* the next render cycle.
|
|
161
|
+
*/
|
|
162
|
+
private swapRunBuffers;
|
|
163
|
+
private redrawRunBuffer;
|
|
140
164
|
/**
|
|
141
165
|
* Renders the given React node.
|
|
142
166
|
*
|
package/lib/core/tinky.js
CHANGED
|
@@ -5,15 +5,20 @@ import { isCI } from "../utils/check-ci.js";
|
|
|
5
5
|
import autoBind from "auto-bind";
|
|
6
6
|
import { onExit } from "../utils/signal-exit.js";
|
|
7
7
|
import { patchConsole } from "../utils/patch-console.js";
|
|
8
|
+
import { advanceSquashTextEpoch } from "../utils/squash-text-nodes.js";
|
|
8
9
|
import { LegacyRoot } from "react-reconciler/constants.js";
|
|
9
10
|
import wrapAnsi from "wrap-ansi";
|
|
10
11
|
import { reconciler } from "./reconciler.js";
|
|
11
12
|
import { renderer } from "./renderer.js";
|
|
13
|
+
import { cellRenderer } from "./cell-renderer.js";
|
|
12
14
|
import * as dom from "./dom.js";
|
|
13
15
|
import { logUpdate } from "./log-update.js";
|
|
16
|
+
import { cellLogUpdateRun, } from "./cell-log-update-run.js";
|
|
14
17
|
import { instances } from "./instances.js";
|
|
15
18
|
import { App } from "../components/App.js";
|
|
16
19
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
20
|
+
import { CellBuffer, StyleRegistry } from "./cell-buffer.js";
|
|
21
|
+
import { normalizeIncrementalRendering, } from "./incremental-rendering.js";
|
|
17
22
|
const noop = () => {
|
|
18
23
|
// no-op
|
|
19
24
|
};
|
|
@@ -52,6 +57,16 @@ export class Tinky {
|
|
|
52
57
|
unsubscribeResize;
|
|
53
58
|
/** Whether we are running in a CI environment. */
|
|
54
59
|
isCI;
|
|
60
|
+
/** Whether run-diff incremental rendering is active for this instance. */
|
|
61
|
+
usesRunIncrementalRendering;
|
|
62
|
+
/** Run-diff log updater. */
|
|
63
|
+
cellLog;
|
|
64
|
+
/** Shared style registry for run buffers. */
|
|
65
|
+
styleRegistry;
|
|
66
|
+
/** Previous rendered frame for run-diff strategy. */
|
|
67
|
+
frontBuffer;
|
|
68
|
+
/** Current frame render target for run-diff strategy. */
|
|
69
|
+
backBuffer;
|
|
55
70
|
/**
|
|
56
71
|
* Creates an instance of Tinky.
|
|
57
72
|
*
|
|
@@ -65,6 +80,16 @@ export class Tinky {
|
|
|
65
80
|
this.isScreenReaderEnabled =
|
|
66
81
|
options.isScreenReaderEnabled ??
|
|
67
82
|
options.env?.["TINKY_SCREEN_READER"] === "true";
|
|
83
|
+
this.isCI = isCI(options.env);
|
|
84
|
+
const incrementalRenderStrategy = normalizeIncrementalRendering(options.incrementalRendering);
|
|
85
|
+
// Run-diff uses cursor addressing and interactive frame tracking, so we keep
|
|
86
|
+
// existing render paths in debug/screen-reader/CI modes.
|
|
87
|
+
this.usesRunIncrementalRendering =
|
|
88
|
+
incrementalRenderStrategy === "run" &&
|
|
89
|
+
!options.debug &&
|
|
90
|
+
!this.isScreenReaderEnabled &&
|
|
91
|
+
!this.isCI;
|
|
92
|
+
const useLineIncremental = incrementalRenderStrategy === "line";
|
|
68
93
|
const unthrottled = options.debug || this.isScreenReaderEnabled;
|
|
69
94
|
const maxFps = options.maxFps ?? 30;
|
|
70
95
|
const renderThrottleMs = maxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;
|
|
@@ -76,7 +101,7 @@ export class Tinky {
|
|
|
76
101
|
});
|
|
77
102
|
this.rootNode.onImmediateRender = this.onRender;
|
|
78
103
|
this.log = logUpdate.create(options.stdout, {
|
|
79
|
-
incremental:
|
|
104
|
+
incremental: useLineIncremental,
|
|
80
105
|
});
|
|
81
106
|
this.throttledLog = unthrottled
|
|
82
107
|
? this.log
|
|
@@ -84,6 +109,17 @@ export class Tinky {
|
|
|
84
109
|
leading: true,
|
|
85
110
|
trailing: true,
|
|
86
111
|
});
|
|
112
|
+
if (this.usesRunIncrementalRendering) {
|
|
113
|
+
this.styleRegistry = new StyleRegistry();
|
|
114
|
+
this.frontBuffer = new CellBuffer({ width: 0, height: 0 }, this.styleRegistry);
|
|
115
|
+
this.backBuffer = new CellBuffer({ width: 0, height: 0 }, this.styleRegistry);
|
|
116
|
+
this.cellLog = cellLogUpdateRun.create(options.stdout, {
|
|
117
|
+
incremental: true,
|
|
118
|
+
mergeStrategy: "cost",
|
|
119
|
+
maxSegmentsBeforeFullRow: 12,
|
|
120
|
+
overwriteGapPenaltyBytes: 0,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
87
123
|
// Ignore last render after unmounting a tree to prevent empty output before
|
|
88
124
|
// exit
|
|
89
125
|
this.isUnmounted = false;
|
|
@@ -108,7 +144,6 @@ export class Tinky {
|
|
|
108
144
|
rendererPackageName: "tinky",
|
|
109
145
|
});
|
|
110
146
|
}
|
|
111
|
-
this.isCI = isCI(options.env);
|
|
112
147
|
if (this.options.patchConsole) {
|
|
113
148
|
this.patchConsole();
|
|
114
149
|
}
|
|
@@ -138,8 +173,16 @@ export class Tinky {
|
|
|
138
173
|
if (currentWidth < this.lastTerminalWidth) {
|
|
139
174
|
// We clear the screen when decreasing terminal width to prevent duplicate
|
|
140
175
|
// overlapping re-renders.
|
|
141
|
-
this.
|
|
176
|
+
if (this.usesRunIncrementalRendering) {
|
|
177
|
+
this.cellLog?.clear();
|
|
178
|
+
this.frontBuffer?.resize(0, 0);
|
|
179
|
+
this.frontBuffer?.clear();
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.log.clear();
|
|
183
|
+
}
|
|
142
184
|
this.lastOutput = "";
|
|
185
|
+
this.lastOutputHeight = 0;
|
|
143
186
|
}
|
|
144
187
|
this.calculateLayout();
|
|
145
188
|
this.onRender();
|
|
@@ -160,6 +203,8 @@ export class Tinky {
|
|
|
160
203
|
if (this.rootNode.taffyNode === undefined) {
|
|
161
204
|
return;
|
|
162
205
|
}
|
|
206
|
+
// Keep text-squash cache scoped to one layout/render cycle.
|
|
207
|
+
advanceSquashTextEpoch();
|
|
163
208
|
const terminalWidth = this.getTerminalWidth();
|
|
164
209
|
const { tree } = this.rootNode.taffyNode;
|
|
165
210
|
const rootStyle = tree.getStyle(this.rootNode.taffyNode.id);
|
|
@@ -186,6 +231,40 @@ export class Tinky {
|
|
|
186
231
|
if (this.isUnmounted) {
|
|
187
232
|
return;
|
|
188
233
|
}
|
|
234
|
+
if (this.usesRunIncrementalRendering && this.backBuffer) {
|
|
235
|
+
const startTime = performance.now();
|
|
236
|
+
const { buffer, outputHeight, staticOutput } = cellRenderer(this.rootNode, this.backBuffer, false);
|
|
237
|
+
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
238
|
+
const hasStaticOutput = staticOutput && staticOutput !== "\n";
|
|
239
|
+
if (hasStaticOutput) {
|
|
240
|
+
this.fullStaticOutput += staticOutput;
|
|
241
|
+
}
|
|
242
|
+
if (this.options.stdout.rows &&
|
|
243
|
+
this.lastOutputHeight >= this.options.stdout.rows) {
|
|
244
|
+
const output = buffer.toString();
|
|
245
|
+
this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
|
|
246
|
+
this.lastOutput = output;
|
|
247
|
+
this.lastOutputHeight = outputHeight;
|
|
248
|
+
this.cellLog?.sync(buffer);
|
|
249
|
+
this.swapRunBuffers();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (hasStaticOutput) {
|
|
253
|
+
// Static output is appended once, then the interactive frame is redrawn.
|
|
254
|
+
this.cellLog?.clear();
|
|
255
|
+
this.options.stdout.write(staticOutput);
|
|
256
|
+
if (this.frontBuffer && this.cellLog) {
|
|
257
|
+
this.cellLog(this.frontBuffer, buffer, { forceFull: true });
|
|
258
|
+
this.swapRunBuffers();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (this.frontBuffer && this.cellLog) {
|
|
262
|
+
this.cellLog(this.frontBuffer, buffer);
|
|
263
|
+
this.swapRunBuffers();
|
|
264
|
+
}
|
|
265
|
+
this.lastOutputHeight = outputHeight;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
189
268
|
const startTime = performance.now();
|
|
190
269
|
const { output, outputHeight, staticOutput } = renderer(this.rootNode, this.isScreenReaderEnabled);
|
|
191
270
|
this.options.onRender?.({ renderTime: performance.now() - startTime });
|
|
@@ -264,6 +343,25 @@ export class Tinky {
|
|
|
264
343
|
this.lastOutput = output;
|
|
265
344
|
this.lastOutputHeight = outputHeight;
|
|
266
345
|
};
|
|
346
|
+
/**
|
|
347
|
+
* Swaps front and back buffers. After swap, the caller's newly-rendered
|
|
348
|
+
* frame becomes the front buffer. The old front becomes the back buffer
|
|
349
|
+
* and will be cleared by cellRenderer (resize + clear) at the start of
|
|
350
|
+
* the next render cycle.
|
|
351
|
+
*/
|
|
352
|
+
swapRunBuffers() {
|
|
353
|
+
const previous = this.frontBuffer;
|
|
354
|
+
this.frontBuffer = this.backBuffer;
|
|
355
|
+
this.backBuffer = previous;
|
|
356
|
+
}
|
|
357
|
+
redrawRunBuffer() {
|
|
358
|
+
if (!this.frontBuffer || !this.cellLog) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.cellLog(this.frontBuffer, this.frontBuffer, { forceFull: true });
|
|
362
|
+
this.lastOutput = this.frontBuffer.toString();
|
|
363
|
+
this.lastOutputHeight = this.frontBuffer.height;
|
|
364
|
+
}
|
|
267
365
|
/**
|
|
268
366
|
* Renders the given React node.
|
|
269
367
|
*
|
|
@@ -291,6 +389,12 @@ export class Tinky {
|
|
|
291
389
|
this.options.stdout.write(data);
|
|
292
390
|
return;
|
|
293
391
|
}
|
|
392
|
+
if (this.usesRunIncrementalRendering) {
|
|
393
|
+
this.cellLog?.clear();
|
|
394
|
+
this.options.stdout.write(data);
|
|
395
|
+
this.redrawRunBuffer();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
294
398
|
this.log.clear();
|
|
295
399
|
this.options.stdout.write(data);
|
|
296
400
|
this.log(this.lastOutput);
|
|
@@ -313,6 +417,12 @@ export class Tinky {
|
|
|
313
417
|
this.options.stderr.write(data);
|
|
314
418
|
return;
|
|
315
419
|
}
|
|
420
|
+
if (this.usesRunIncrementalRendering) {
|
|
421
|
+
this.cellLog?.clear();
|
|
422
|
+
this.options.stderr.write(data);
|
|
423
|
+
this.redrawRunBuffer();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
316
426
|
this.log.clear();
|
|
317
427
|
this.options.stderr.write(data);
|
|
318
428
|
this.log(this.lastOutput);
|
|
@@ -345,7 +455,12 @@ export class Tinky {
|
|
|
345
455
|
this.options.stdout.write(this.lastOutput + "\n");
|
|
346
456
|
}
|
|
347
457
|
else if (!this.options.debug) {
|
|
348
|
-
this.
|
|
458
|
+
if (this.usesRunIncrementalRendering) {
|
|
459
|
+
this.cellLog?.done();
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
this.log.done();
|
|
463
|
+
}
|
|
349
464
|
}
|
|
350
465
|
this.isUnmounted = true;
|
|
351
466
|
reconciler.updateContainerSync(null, this.container, null, noop);
|
|
@@ -387,7 +502,12 @@ export class Tinky {
|
|
|
387
502
|
clear() {
|
|
388
503
|
const isCiEnv = isCI(this.options.env);
|
|
389
504
|
if (!isCiEnv) {
|
|
390
|
-
this.
|
|
505
|
+
if (this.usesRunIncrementalRendering) {
|
|
506
|
+
this.cellLog?.clear();
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
this.log.clear();
|
|
510
|
+
}
|
|
391
511
|
}
|
|
392
512
|
}
|
|
393
513
|
/**
|
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";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,6 +2,7 @@ export declare const cursorTo: (x: number, y?: number) => string;
|
|
|
2
2
|
export declare const cursorUp: (count?: number) => string;
|
|
3
3
|
export declare const cursorNextLine = "\u001B[E";
|
|
4
4
|
export declare const cursorLeft = "\u001B[G";
|
|
5
|
+
export declare const cursorForward: (count?: number) => string;
|
|
5
6
|
export declare const eraseEndLine = "\u001B[K";
|
|
6
7
|
export declare const eraseLine = "\u001B[2K";
|
|
7
8
|
export declare const eraseLines: (count: number) => string;
|
|
@@ -11,6 +12,7 @@ declare const ansiEscapes: {
|
|
|
11
12
|
cursorUp: (count?: number) => string;
|
|
12
13
|
cursorNextLine: string;
|
|
13
14
|
cursorLeft: string;
|
|
15
|
+
cursorForward: (count?: number) => string;
|
|
14
16
|
eraseEndLine: string;
|
|
15
17
|
eraseLine: string;
|
|
16
18
|
eraseLines: (count: number) => string;
|
|
@@ -11,6 +11,7 @@ export const cursorTo = (x, y) => {
|
|
|
11
11
|
export const cursorUp = (count = 1) => `${ESC}${count}A`;
|
|
12
12
|
export const cursorNextLine = `${ESC}E`;
|
|
13
13
|
export const cursorLeft = `${ESC}G`;
|
|
14
|
+
export const cursorForward = (count = 1) => `${ESC}${count}C`;
|
|
14
15
|
export const eraseEndLine = `${ESC}K`;
|
|
15
16
|
export const eraseLine = `${ESC}2K`;
|
|
16
17
|
export const eraseLines = (count) => {
|
|
@@ -29,6 +30,7 @@ const ansiEscapes = {
|
|
|
29
30
|
cursorUp,
|
|
30
31
|
cursorNextLine,
|
|
31
32
|
cursorLeft,
|
|
33
|
+
cursorForward,
|
|
32
34
|
eraseEndLine,
|
|
33
35
|
eraseLine,
|
|
34
36
|
eraseLines,
|
package/lib/utils/colorize.d.ts
CHANGED
|
@@ -20,5 +20,10 @@ type ColorType = "foreground" | "background";
|
|
|
20
20
|
* @param type - Whether to apply the color to the foreground or background.
|
|
21
21
|
* @returns The colorized string.
|
|
22
22
|
*/
|
|
23
|
+
import { type AnsiCode } from "../types/ansi.js";
|
|
24
|
+
/**
|
|
25
|
+
* Gets the ANSI style for a color.
|
|
26
|
+
*/
|
|
27
|
+
export declare const getStyle: (color: string | undefined, type: ColorType) => AnsiCode | undefined;
|
|
23
28
|
export declare const colorize: (str: string, color: string | undefined, type: ColorType) => string;
|
|
24
29
|
export {};
|