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 CHANGED
@@ -308,6 +308,34 @@ unmount();
308
308
  clear();
309
309
  ```
310
310
 
311
+ ### incrementalRendering
312
+
313
+ `incrementalRendering` を使うと、Tinky がインタラクティブフレームを更新する
314
+ 方法を選べます。run モードはターミナルセル単位で diff し、変更区間だけを書
315
+ き込みます。line モードは行単位で diff し、変更行を再描画します。
316
+
317
+ ```tsx
318
+ import { render } from "tinky";
319
+
320
+ render(<App />, {
321
+ // 次と同じ: { strategy: "run" }
322
+ incrementalRendering: true,
323
+ });
324
+
325
+ render(<App />, {
326
+ incrementalRendering: { strategy: "line" },
327
+ });
328
+
329
+ render(<App />, {
330
+ incrementalRendering: { enabled: false },
331
+ });
332
+ ```
333
+
334
+ `debug`、スクリーンリーダー、CI 環境では、Tinky は自動的に非 run パスへ
335
+ フォールバックします。戦略の比較と挙動の詳細は
336
+ [増分レンダリングガイド](./docs/incremental-rendering.ja-JP.md) を参照して
337
+ ください。
338
+
311
339
  ### measureElement(ref)
312
340
 
313
341
  レンダリングされた要素のサイズを測定します。
@@ -337,6 +365,18 @@ Tinky は Bun を使用してテストを行います。テストスイートを
337
365
  bun test
338
366
  ```
339
367
 
368
+ 増分レンダリングのローカルベンチマークを実行するには、次を実行します。
369
+
370
+ ```bash
371
+ bun run perf:render
372
+ ```
373
+
374
+ CI と同じ性能ゲートを実行するには、次を実行します。
375
+
376
+ ```bash
377
+ bun run perf:gate
378
+ ```
379
+
340
380
  ## 📄 ライセンス
341
381
 
342
382
  MIT © [ByteLandTechnology](https://github.com/ByteLandTechnology)
package/README.md CHANGED
@@ -308,6 +308,33 @@ unmount();
308
308
  clear();
309
309
  ```
310
310
 
311
+ ### incrementalRendering
312
+
313
+ Use `incrementalRendering` to control how Tinky updates interactive frames.
314
+ Run mode diffs terminal cells and writes minimal changed runs. Line mode diffs
315
+ whole lines and rewrites changed lines.
316
+
317
+ ```tsx
318
+ import { render } from "tinky";
319
+
320
+ render(<App />, {
321
+ // Equivalent to: { strategy: "run" }
322
+ incrementalRendering: true,
323
+ });
324
+
325
+ render(<App />, {
326
+ incrementalRendering: { strategy: "line" },
327
+ });
328
+
329
+ render(<App />, {
330
+ incrementalRendering: { enabled: false },
331
+ });
332
+ ```
333
+
334
+ Tinky automatically falls back to non-run paths in `debug`, screen-reader, and
335
+ CI environments. For strategy trade-offs and behavior details, read
336
+ [Incremental rendering guide](./docs/incremental-rendering.md).
337
+
311
338
  ### measureElement(ref)
312
339
 
313
340
  Measure the dimensions of a rendered element.
@@ -337,6 +364,18 @@ Tinky uses Bun for testing. Run the test suite:
337
364
  bun test
338
365
  ```
339
366
 
367
+ To benchmark incremental rendering locally, run:
368
+
369
+ ```bash
370
+ bun run perf:render
371
+ ```
372
+
373
+ To enforce the performance threshold used by CI, run:
374
+
375
+ ```bash
376
+ bun run perf:gate
377
+ ```
378
+
340
379
  ## 📄 License
341
380
 
342
381
  MIT © [ByteLandTechnology](https://github.com/ByteLandTechnology)
package/README.zh-CN.md CHANGED
@@ -308,6 +308,32 @@ unmount();
308
308
  clear();
309
309
  ```
310
310
 
311
+ ### incrementalRendering
312
+
313
+ 你可以通过 `incrementalRendering` 控制 Tinky 更新交互帧的方式。run 模式会按
314
+ 终端单元格做 diff,只写入变化片段;line 模式按整行做 diff,并重写变化行。
315
+
316
+ ```tsx
317
+ import { render } from "tinky";
318
+
319
+ render(<App />, {
320
+ // 等价于: { strategy: "run" }
321
+ incrementalRendering: true,
322
+ });
323
+
324
+ render(<App />, {
325
+ incrementalRendering: { strategy: "line" },
326
+ });
327
+
328
+ render(<App />, {
329
+ incrementalRendering: { enabled: false },
330
+ });
331
+ ```
332
+
333
+ 在 `debug`、屏幕阅读器和 CI 环境中,Tinky 会自动回退到非 run 路径。关于
334
+ 策略取舍和行为细节,请参阅
335
+ [增量渲染指南](./docs/incremental-rendering.zh-CN.md)。
336
+
311
337
  ### measureElement(ref)
312
338
 
313
339
  测量已渲染元素的尺寸。
@@ -337,6 +363,18 @@ Tinky 使用 Bun 进行测试。运行测试套件:
337
363
  bun test
338
364
  ```
339
365
 
366
+ 如需在本地做增量渲染性能基准测试,请运行:
367
+
368
+ ```bash
369
+ bun run perf:render
370
+ ```
371
+
372
+ 如需执行与 CI 一致的性能门禁,请运行:
373
+
374
+ ```bash
375
+ bun run perf:gate
376
+ ```
377
+
340
378
  ## 📄 许可证
341
379
 
342
380
  MIT © [ByteLandTechnology](https://github.com/ByteLandTechnology)
@@ -29,12 +29,19 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
29
29
  const { isScreenReaderEnabled } = useContext(AccessibilityContext);
30
30
  const inheritedBackgroundColor = useContext(backgroundContext);
31
31
  const childrenOrAriaLabel = isScreenReaderEnabled && ariaLabel ? ariaLabel : children;
32
+ const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
33
+ const hasTextStyle = color !== undefined ||
34
+ effectiveBackgroundColor !== undefined ||
35
+ dimColor ||
36
+ bold ||
37
+ italic ||
38
+ underline ||
39
+ strikethrough ||
40
+ inverse;
32
41
  if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
33
42
  return null;
34
43
  }
35
44
  const transform = (children) => {
36
- // Use explicit backgroundColor if provided, otherwise use inherited from parent Box
37
- const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
38
45
  return applyTextStyles(children, {
39
46
  color,
40
47
  backgroundColor: effectiveBackgroundColor,
@@ -54,5 +61,5 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
54
61
  flexShrink: 1,
55
62
  flexDirection: "row",
56
63
  textWrap: wrap,
57
- }, internal_transform: transform, children: isScreenReaderEnabled && ariaLabel ? ariaLabel : children }));
64
+ }, internal_transform: hasTextStyle ? transform : undefined, children: isScreenReaderEnabled && ariaLabel ? ariaLabel : children }));
58
65
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Cell-buffer rendering primitives.
3
+ *
4
+ * This stores terminal cells in compact parallel arrays and keeps ANSI styles
5
+ * as small numeric IDs via StyleRegistry.
6
+ */
7
+ import { type AnsiCode } from "../types/ansi.js";
8
+ export interface CellBufferSize {
9
+ width: number;
10
+ height: number;
11
+ }
12
+ export declare class StyleRegistry {
13
+ private readonly keyToId;
14
+ private readonly idToStyles;
15
+ constructor();
16
+ static keyFor(styles: readonly AnsiCode[] | null | undefined): string;
17
+ getId(styles: readonly AnsiCode[] | null | undefined): number;
18
+ getStyles(id: number): readonly AnsiCode[];
19
+ }
20
+ export declare class CellBuffer {
21
+ width: number;
22
+ height: number;
23
+ readonly styleRegistry: StyleRegistry;
24
+ /**
25
+ * Per-cell visible glyph. Continuation cells store empty string.
26
+ */
27
+ chars: string[];
28
+ /**
29
+ * Per-cell display width: 1 or 2 on leading cells, 0 on continuation cells.
30
+ */
31
+ widths: Uint8Array;
32
+ /**
33
+ * Per-cell style id. Continuation cells keep the leading style id.
34
+ */
35
+ styleIds: Uint32Array;
36
+ /**
37
+ * Marks whether a cell was explicitly written in the current frame.
38
+ */
39
+ touched: Uint8Array;
40
+ /**
41
+ * Marks whether trailing spaces on a touched cell should be preserved.
42
+ */
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;
49
+ constructor(size?: CellBufferSize, styleRegistry?: StyleRegistry);
50
+ resize(width: number, height: number): void;
51
+ clear(styleId?: number): void;
52
+ /**
53
+ * Converts `(x, y)` coordinates to a linear index in parallel arrays.
54
+ */
55
+ index(x: number, y: number): number;
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;
58
+ isRowEqual(other: CellBuffer, row: number): boolean;
59
+ isEqual(other: CellBuffer): boolean;
60
+ /**
61
+ * Computes the visible right edge for a row.
62
+ *
63
+ * This keeps transformer-produced trailing spaces when `preserveTrailingSpace`
64
+ * is set, while trimming untouched or default trailing cells.
65
+ */
66
+ getRowRightEdge(row: number): number;
67
+ /**
68
+ * Closes active styles and opens next styles when style id changes.
69
+ */
70
+ appendStyleTransition(out: string[], activeStyleId: number, nextStyleId: number): number;
71
+ /**
72
+ * Closes any currently active style sequence.
73
+ */
74
+ appendCloseActiveStyle(out: string[], activeStyleId: number): number;
75
+ /**
76
+ * Serializes a contiguous range of cells from one row, preserving ANSI style
77
+ * transitions and wide-character boundaries.
78
+ */
79
+ appendStyledRange(row: number, start: number, end: number, out: string[], activeStyleId: number, cursorX: number): {
80
+ cursorX: number;
81
+ activeStyleId: number;
82
+ };
83
+ /**
84
+ * Serializes one row into ANSI text, trimming only non-visible tail cells.
85
+ */
86
+ serializeRow(row: number): string;
87
+ /**
88
+ * Serializes the entire buffer into a multi-line ANSI string.
89
+ */
90
+ toString(): string;
91
+ }
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Cell-buffer rendering primitives.
3
+ *
4
+ * This stores terminal cells in compact parallel arrays and keeps ANSI styles
5
+ * as small numeric IDs via StyleRegistry.
6
+ */
7
+ export class StyleRegistry {
8
+ keyToId = new Map();
9
+ idToStyles = [];
10
+ constructor() {
11
+ // id=0 is "no style"
12
+ this.keyToId.set("", 0);
13
+ this.idToStyles[0] = [];
14
+ }
15
+ static keyFor(styles) {
16
+ if (!styles || styles.length === 0) {
17
+ return "";
18
+ }
19
+ let key = "";
20
+ for (const style of styles) {
21
+ key += style.code;
22
+ key += "\u0000";
23
+ key += style.endCode;
24
+ key += "\u0001";
25
+ }
26
+ return key;
27
+ }
28
+ getId(styles) {
29
+ const key = StyleRegistry.keyFor(styles);
30
+ const cached = this.keyToId.get(key);
31
+ if (cached !== undefined) {
32
+ return cached;
33
+ }
34
+ const id = this.idToStyles.length;
35
+ const cloned = (styles ?? []).map((style) => ({
36
+ type: "ansi",
37
+ code: style.code,
38
+ endCode: style.endCode,
39
+ }));
40
+ this.keyToId.set(key, id);
41
+ this.idToStyles[id] = cloned;
42
+ return id;
43
+ }
44
+ getStyles(id) {
45
+ return this.idToStyles[id] ?? [];
46
+ }
47
+ }
48
+ export class CellBuffer {
49
+ width = 0;
50
+ height = 0;
51
+ styleRegistry;
52
+ /**
53
+ * Per-cell visible glyph. Continuation cells store empty string.
54
+ */
55
+ chars = [];
56
+ /**
57
+ * Per-cell display width: 1 or 2 on leading cells, 0 on continuation cells.
58
+ */
59
+ widths = new Uint8Array(0);
60
+ /**
61
+ * Per-cell style id. Continuation cells keep the leading style id.
62
+ */
63
+ styleIds = new Uint32Array(0);
64
+ /**
65
+ * Marks whether a cell was explicitly written in the current frame.
66
+ */
67
+ touched = new Uint8Array(0);
68
+ /**
69
+ * Marks whether trailing spaces on a touched cell should be preserved.
70
+ */
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);
77
+ constructor(size = { width: 0, height: 0 }, styleRegistry) {
78
+ this.styleRegistry = styleRegistry ?? new StyleRegistry();
79
+ this.resize(size.width, size.height);
80
+ this.clear();
81
+ }
82
+ resize(width, height) {
83
+ const nextWidth = Math.max(0, Math.floor(width));
84
+ const nextHeight = Math.max(0, Math.floor(height));
85
+ if (this.width === nextWidth && this.height === nextHeight) {
86
+ return;
87
+ }
88
+ this.width = nextWidth;
89
+ this.height = nextHeight;
90
+ const size = nextWidth * nextHeight;
91
+ this.chars = new Array(size);
92
+ this.widths = new Uint8Array(size);
93
+ this.styleIds = new Uint32Array(size);
94
+ this.touched = new Uint8Array(size);
95
+ this.preserveTrailingSpace = new Uint8Array(size);
96
+ this.rowTouched = new Uint8Array(nextHeight);
97
+ }
98
+ clear(styleId = 0) {
99
+ const size = this.width * this.height;
100
+ this.chars.fill(" ", 0, size);
101
+ this.widths.fill(1, 0, size);
102
+ this.styleIds.fill(styleId, 0, size);
103
+ this.touched.fill(0, 0, size);
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
+ }
112
+ }
113
+ /**
114
+ * Converts `(x, y)` coordinates to a linear index in parallel arrays.
115
+ */
116
+ index(x, y) {
117
+ return y * this.width + x;
118
+ }
119
+ setCell(x, y, ch, charWidth, styleId, preserveTrailing = false) {
120
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
121
+ return;
122
+ }
123
+ const width = Math.max(1, Math.floor(charWidth));
124
+ if (x + width > this.width) {
125
+ return;
126
+ }
127
+ const cellIndex = this.index(x, y);
128
+ this.chars[cellIndex] = ch;
129
+ this.widths[cellIndex] = width;
130
+ this.styleIds[cellIndex] = styleId;
131
+ this.touched[cellIndex] = 1;
132
+ this.preserveTrailingSpace[cellIndex] = preserveTrailing ? 1 : 0;
133
+ this.rowTouched[y] = 1;
134
+ if (width > 1) {
135
+ for (let i = 1; i < width; i++) {
136
+ const continuationIndex = cellIndex + i;
137
+ this.chars[continuationIndex] = "";
138
+ this.widths[continuationIndex] = 0;
139
+ this.styleIds[continuationIndex] = styleId;
140
+ this.touched[continuationIndex] = 1;
141
+ this.preserveTrailingSpace[continuationIndex] = preserveTrailing
142
+ ? 1
143
+ : 0;
144
+ }
145
+ }
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
+ }
174
+ isRowEqual(other, row) {
175
+ if (this.width !== other.width || this.height !== other.height) {
176
+ return false;
177
+ }
178
+ if (row < 0 || row >= this.height) {
179
+ return false;
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
+ }
193
+ const start = row * this.width;
194
+ const end = start + thisRightEdge;
195
+ for (let index = start; index < end; index++) {
196
+ if (this.styleIds[index] !== other.styleIds[index]) {
197
+ return false;
198
+ }
199
+ if (this.widths[index] !== other.widths[index]) {
200
+ return false;
201
+ }
202
+ if (this.chars[index] !== other.chars[index]) {
203
+ return false;
204
+ }
205
+ }
206
+ return true;
207
+ }
208
+ isEqual(other) {
209
+ if (this.width !== other.width || this.height !== other.height) {
210
+ return false;
211
+ }
212
+ for (let row = 0; row < this.height; row++) {
213
+ if (!this.isRowEqual(other, row)) {
214
+ return false;
215
+ }
216
+ }
217
+ return true;
218
+ }
219
+ /**
220
+ * Computes the visible right edge for a row.
221
+ *
222
+ * This keeps transformer-produced trailing spaces when `preserveTrailingSpace`
223
+ * is set, while trimming untouched or default trailing cells.
224
+ */
225
+ getRowRightEdge(row) {
226
+ const width = this.width;
227
+ if (row < 0 || row >= this.height) {
228
+ return 0;
229
+ }
230
+ if (!this.rowTouched[row]) {
231
+ return 0;
232
+ }
233
+ const offset = row * width;
234
+ for (let x = width - 1; x >= 0; x--) {
235
+ const index = offset + x;
236
+ if (this.widths[index] === 0) {
237
+ continue;
238
+ }
239
+ if (this.touched[index] === 0) {
240
+ continue;
241
+ }
242
+ const canTrimUnstyledSpace = this.chars[index] === " " &&
243
+ this.styleIds[index] === 0 &&
244
+ this.preserveTrailingSpace[index] === 0;
245
+ if (canTrimUnstyledSpace) {
246
+ continue;
247
+ }
248
+ const cellWidth = this.widths[index] || 1;
249
+ return Math.min(width, x + cellWidth);
250
+ }
251
+ return 0;
252
+ }
253
+ /**
254
+ * Closes active styles and opens next styles when style id changes.
255
+ */
256
+ appendStyleTransition(out, activeStyleId, nextStyleId) {
257
+ if (activeStyleId === nextStyleId) {
258
+ return activeStyleId;
259
+ }
260
+ const activeStyles = this.styleRegistry.getStyles(activeStyleId);
261
+ for (let i = activeStyles.length - 1; i >= 0; i--) {
262
+ out.push(activeStyles[i]?.endCode);
263
+ }
264
+ const nextStyles = this.styleRegistry.getStyles(nextStyleId);
265
+ for (const style of nextStyles) {
266
+ out.push(style.code);
267
+ }
268
+ return nextStyleId;
269
+ }
270
+ /**
271
+ * Closes any currently active style sequence.
272
+ */
273
+ appendCloseActiveStyle(out, activeStyleId) {
274
+ if (activeStyleId === 0) {
275
+ return 0;
276
+ }
277
+ const activeStyles = this.styleRegistry.getStyles(activeStyleId);
278
+ for (let i = activeStyles.length - 1; i >= 0; i--) {
279
+ out.push(activeStyles[i]?.endCode);
280
+ }
281
+ return 0;
282
+ }
283
+ /**
284
+ * Serializes a contiguous range of cells from one row, preserving ANSI style
285
+ * transitions and wide-character boundaries.
286
+ */
287
+ appendStyledRange(row, start, end, out, activeStyleId, cursorX) {
288
+ const width = this.width;
289
+ if (row < 0 || row >= this.height) {
290
+ return { cursorX, activeStyleId };
291
+ }
292
+ const safeStart = Math.max(0, Math.min(width, start));
293
+ const safeEnd = Math.max(0, Math.min(width, end));
294
+ if (safeEnd <= safeStart) {
295
+ return { cursorX, activeStyleId };
296
+ }
297
+ const rowOffset = row * width;
298
+ let x = safeStart;
299
+ while (x < safeEnd) {
300
+ const index = rowOffset + x;
301
+ const cellWidth = this.widths[index];
302
+ if (cellWidth === 0) {
303
+ x++;
304
+ continue;
305
+ }
306
+ const styleId = this.styleIds[index];
307
+ activeStyleId = this.appendStyleTransition(out, activeStyleId, styleId);
308
+ out.push(this.chars[index] ?? " ");
309
+ const advance = cellWidth || 1;
310
+ cursorX += advance;
311
+ x += advance;
312
+ }
313
+ return { cursorX, activeStyleId };
314
+ }
315
+ /**
316
+ * Serializes one row into ANSI text, trimming only non-visible tail cells.
317
+ */
318
+ serializeRow(row) {
319
+ const rightEdge = this.getRowRightEdge(row);
320
+ if (rightEdge === 0) {
321
+ return "";
322
+ }
323
+ const out = [];
324
+ const { activeStyleId } = this.appendStyledRange(row, 0, rightEdge, out, 0, 0);
325
+ this.appendCloseActiveStyle(out, activeStyleId);
326
+ return out.join("");
327
+ }
328
+ /**
329
+ * Serializes the entire buffer into a multi-line ANSI string.
330
+ */
331
+ toString() {
332
+ const out = [];
333
+ for (let row = 0; row < this.height; row++) {
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);
343
+ }
344
+ return out.join("");
345
+ }
346
+ }
@@ -0,0 +1,40 @@
1
+ import { type WriteStream } from "../types/io.js";
2
+ import { type CellBuffer } from "./cell-buffer.js";
3
+ /**
4
+ * Run-diff renderer for interactive terminal output.
5
+ *
6
+ * Compared with line-diff mode, this renderer diffs cell buffers and emits
7
+ * minimal cursor movement + text writes for changed runs. It entirely skips
8
+ * terminal writes when consecutive frames are identical.
9
+ */
10
+ export interface CellLogUpdateRunOptions {
11
+ /** Enables diff updates. Set `false` to force full-frame redraws. */
12
+ incremental?: boolean;
13
+ /** Keeps the terminal cursor visible during updates. */
14
+ showCursor?: boolean;
15
+ /** Gap merge threshold used by `mergeStrategy: "threshold"`. */
16
+ mergeThreshold?: number;
17
+ /** Segment merge policy. `cost` chooses cheapest bytes, `threshold` uses gap. */
18
+ mergeStrategy?: "cost" | "threshold";
19
+ /** Fallback to full-row writes when too many segments are present. */
20
+ maxSegmentsBeforeFullRow?: number;
21
+ /** Penalty when overwriting unchanged gaps in `cost` mode. */
22
+ overwriteGapPenaltyBytes?: number;
23
+ }
24
+ export interface RenderOptions {
25
+ /** Forces full redraw for the current frame. */
26
+ forceFull?: boolean;
27
+ }
28
+ export type CellLogUpdateRender = ((prevBuffer: CellBuffer, nextBuffer: CellBuffer, options?: RenderOptions) => void) & {
29
+ /** Erases currently rendered interactive frame. */
30
+ clear: () => void;
31
+ /** Resets internal frame state without writing to stream. */
32
+ reset: () => void;
33
+ /** Synchronizes internal state after an external full redraw. */
34
+ sync: (buffer: CellBuffer) => void;
35
+ /** Finalizes renderer and restores cursor visibility. */
36
+ done: () => void;
37
+ };
38
+ export declare const cellLogUpdateRun: {
39
+ create: (stream: WriteStream, options?: CellLogUpdateRunOptions) => CellLogUpdateRender;
40
+ };