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.
@@ -1,82 +1,91 @@
1
1
  import { boxStyles } from "./box-styles.js";
2
2
  import ansis from "ansis";
3
- import { colorize } from "../utils/colorize.js";
4
- /**
5
- * Renders the border for a DOM node.
6
- * Calculates border dimensions and draws border characters with specified
7
- * styles.
8
- *
9
- * @param x - The x-coordinate (column) where the border starts.
10
- * @param y - The y-coordinate (row) where the border starts.
11
- * @param node - The DOM node for which to render the border.
12
- * @param output - The output instance to write the border to.
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
- if (node.style.borderStyle) {
16
- const layout = node.taffyNode?.tree.getLayout(node.taffyNode.id);
17
- const width = layout?.width ?? 0;
18
- const height = layout?.height ?? 0;
19
- const box = typeof node.style.borderStyle === "string"
20
- ? boxStyles[node.style.borderStyle]
21
- : node.style.borderStyle;
22
- const topBorderColor = node.style.borderTopColor ?? node.style.borderColor;
23
- const bottomBorderColor = node.style.borderBottomColor ?? node.style.borderColor;
24
- const leftBorderColor = node.style.borderLeftColor ?? node.style.borderColor;
25
- const rightBorderColor = node.style.borderRightColor ?? node.style.borderColor;
26
- const dimTopBorderColor = node.style.borderTopDimColor ?? node.style.borderDimColor;
27
- const dimBottomBorderColor = node.style.borderBottomDimColor ?? node.style.borderDimColor;
28
- const dimLeftBorderColor = node.style.borderLeftDimColor ?? node.style.borderDimColor;
29
- const dimRightBorderColor = node.style.borderRightDimColor ?? node.style.borderDimColor;
30
- const showTopBorder = node.style.borderTop !== false;
31
- const showBottomBorder = node.style.borderBottom !== false;
32
- const showLeftBorder = node.style.borderLeft !== false;
33
- const showRightBorder = node.style.borderRight !== false;
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
- let rightBorder = (colorize(box.right, rightBorderColor, "foreground") + "\n").repeat(verticalBorderHeight);
55
- if (dimRightBorderColor) {
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
- let bottomBorder = showBottomBorder
59
- ? colorize((showLeftBorder ? box.bottomLeft : "") +
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
- const offsetY = showTopBorder ? 1 : 0;
67
- if (topBorder) {
68
- output.write(x, y, topBorder, { transformers: [] });
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.write(x, y + offsetY, leftBorder, { transformers: [] });
82
+ output.fill(x, bottomY, 1, box.bottomLeft, stringWidth(box.bottomLeft), styles);
72
83
  }
73
- if (showRightBorder) {
74
- output.write(x + width - 1, y + offsetY, rightBorder, {
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 (bottomBorder) {
79
- output.write(x, y + height - 1, bottomBorder, { transformers: [] });
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 Output } from "./output.js";
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: Output, options: {
38
+ export declare const renderNodeToOutput: (node: DOMElement, output: OutputLike, options: {
39
39
  offsetX?: number;
40
40
  offsetY?: number;
41
41
  transformers?: OutputTransformer[];
@@ -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
- * Enable incremental rendering mode which only updates changed lines instead
68
- * of redrawing the entire output. Reduces flickering and improves
69
- * performance.
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?: boolean;
86
+ incrementalRendering?: IncrementalRenderingOption;
74
87
  /**
75
88
  * Environment variables.
76
89
  *
@@ -1,5 +1,6 @@
1
1
  import { renderNodeToOutput, renderNodeToScreenReaderOutput, } from "./render-node-to-output.js";
2
- import { Output } from "./output.js";
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 output = new Output({
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
- staticOutput = new Output({
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 { output: generatedOutput, height: outputHeight } = output.get();
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.get().output}\n` : "",
57
+ staticOutput: staticOutput ? `${staticOutput.buffer.toString()}\n` : "",
55
58
  };
56
59
  }
57
60
  return {
@@ -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
- * Enable incremental rendering mode which only updates changed lines instead
63
- * of redrawing the entire output.
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?: boolean;
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: options.incrementalRendering,
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.log.clear();
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.log.done();
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.log.clear();
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,5 @@
1
+ export interface AnsiCode {
2
+ type: "ansi";
3
+ code: string;
4
+ endCode: string;
5
+ }
@@ -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,
@@ -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 {};