@code-yeongyu/senpi-tui 2026.10.2 → 2026.10.4
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/stdin-buffer.d.ts +20 -0
- package/dist/stdin-buffer.js +116 -0
- package/dist/terminal.d.ts +6 -0
- package/dist/terminal.js +24 -1
- package/dist/tui.d.ts +8 -0
- package/dist/tui.js +37 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export { parseTerminalColorSchemeReport, type RgbColor, type TerminalColorScheme
|
|
|
34
34
|
export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
|
|
35
35
|
export { allocateImageId, buildKittyPlaceholderRow, type CellDimensions, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, type ImageDimensions, type ImageProtocol, type ImageRenderOptions, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, type TerminalCapabilities, type TmuxPassthroughState, wrapTmuxPassthrough, } from "./terminal-image.ts";
|
|
36
36
|
export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.ts";
|
|
37
|
-
export { type Component, CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, type Focusable, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, type OverlayAnchor, type OverlayBounds, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, renderAtFrameRow, resetMainScreenHistoryLines, type SizeValue, TUI, type TuiInputListener, type TuiInputListenerResult, type TuiMode, type TuiMouseButton, type TuiMouseDispatchResult, type TuiMouseEvent, type TuiMouseEventResult, type TuiMouseEventType, type TuiStopOptions, type ViewportTUI, } from "./tui.ts";
|
|
37
|
+
export { type Component, CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, type Focusable, type FrameLineBytesTotals, frameLineBytesTotals, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, type OverlayAnchor, type OverlayBounds, type OverlayHandle, type OverlayMargin, type OverlayOptions, type OverlayUnfocusOptions, renderAtFrameRow, resetMainScreenHistoryLines, type SizeValue, TUI, type TuiInputListener, type TuiInputListenerResult, type TuiMode, type TuiMouseButton, type TuiMouseDispatchResult, type TuiMouseEvent, type TuiMouseEventResult, type TuiMouseEventType, type TuiStopOptions, type ViewportTUI, } from "./tui.ts";
|
|
38
38
|
export { TuiAltScreen, type TuiAltScreenOptions } from "./tui-alt-screen.ts";
|
|
39
39
|
export { TuiMainScreen, type TuiMainScreenRenderState } from "./tui-main-screen.ts";
|
|
40
40
|
export { getGraphemeSegmenter, getOsc8LinkAtColumn, getWordSegmenter, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "./utils.ts";
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ export function calculateImageRows(imageDimensions, targetWidthCells, cellDimens
|
|
|
49
49
|
}
|
|
50
50
|
export { allocateImageId, buildKittyPlaceholderRow, deleteAllKittyImages, deleteKittyImage, detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, getImageDimensions, getJpegDimensions, getPngDimensions, getTerminalColorMode, getWebpDimensions, hyperlink, imageFallback, KITTY_PLACEHOLDER_MAX, outerKittyGraphicsMode, renderImage, resetCapabilitiesCache, setCapabilities, setCapabilityOverrides, setCellDimensions, wrapTmuxPassthrough, } from "./terminal-image.js";
|
|
51
51
|
export { sanitizeTerminalLabel, shortenImagePath } from "./terminal-text.js";
|
|
52
|
-
export { CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, renderAtFrameRow, resetMainScreenHistoryLines, TUI, } from "./tui.js";
|
|
52
|
+
export { CompositeRevision, Container, CURSOR_MARKER, claimFrameRow, compositeTuiLine, currentRenderRevision, dispatchMouseEvent, frameLineBytesTotals, frameMode, frameScrollbackRows, isFocusable, isViewportTUI, joinLineArrays, mainScreenHistoryLines, nextRenderRevision, renderAtFrameRow, resetMainScreenHistoryLines, TUI, } from "./tui.js";
|
|
53
53
|
export { TuiAltScreen } from "./tui-alt-screen.js";
|
|
54
54
|
export { TuiMainScreen } from "./tui-main-screen.js";
|
|
55
55
|
// Utilities
|
package/dist/stdin-buffer.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ export type StdinBufferOptions = {
|
|
|
28
28
|
* (default: 10ms). Increase for high-latency Alt+key input (SSH).
|
|
29
29
|
*/
|
|
30
30
|
escapeTimeout?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Window after the previous input inside which a trailing newline is held
|
|
33
|
+
* as a possible paste fragment instead of being emitted at once
|
|
34
|
+
* (default: 20ms). Keystroke-paced input is unaffected.
|
|
35
|
+
*/
|
|
36
|
+
burstWindowMs?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Clock used for burst pacing (default: Date.now). Tests inject a manual clock.
|
|
39
|
+
*/
|
|
40
|
+
now?: () => number;
|
|
31
41
|
};
|
|
32
42
|
export type StdinBufferEventMap = {
|
|
33
43
|
data: [string];
|
|
@@ -48,8 +58,18 @@ export declare class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
48
58
|
private pasteBuffer;
|
|
49
59
|
private pendingKittyPrintableCodepoint;
|
|
50
60
|
private decoder;
|
|
61
|
+
private readonly burstWindowMs;
|
|
62
|
+
private readonly clock;
|
|
63
|
+
private lastInputAt;
|
|
64
|
+
private heldNewline;
|
|
65
|
+
private heldNewlineEndsPaste;
|
|
66
|
+
private lastBurstPasteAt;
|
|
67
|
+
private burstTimer;
|
|
51
68
|
constructor(options?: StdinBufferOptions);
|
|
52
69
|
process(data: string | Buffer): void;
|
|
70
|
+
private consumeUnbracketedBurst;
|
|
71
|
+
private holdTrailingNewline;
|
|
72
|
+
private releaseHeldNewline;
|
|
53
73
|
private emitDataSequence;
|
|
54
74
|
flush(): string[];
|
|
55
75
|
clear(): void;
|
package/dist/stdin-buffer.js
CHANGED
|
@@ -21,8 +21,16 @@ import { EventEmitter } from "events";
|
|
|
21
21
|
const ESC = "\x1b";
|
|
22
22
|
const DEFAULT_SEQUENCE_TIMEOUT_MS = 50;
|
|
23
23
|
const DEFAULT_ESCAPE_TIMEOUT_MS = 10;
|
|
24
|
+
const DEFAULT_BURST_WINDOW_MS = 20;
|
|
24
25
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
25
26
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
27
|
+
const LINE_BREAK = /\r\n|\r|\n/g;
|
|
28
|
+
const TRAILING_LINE_BREAK_RUN = /(\r\n|\r|\n)+$/;
|
|
29
|
+
const LINE_BREAK_BEFORE_TEXT = /[\r\n][^\r\n]/;
|
|
30
|
+
function countLineBreaks(text) {
|
|
31
|
+
const matches = text.match(LINE_BREAK);
|
|
32
|
+
return matches === null ? 0 : matches.length;
|
|
33
|
+
}
|
|
26
34
|
/**
|
|
27
35
|
* Check if a string is a complete escape sequence or needs more data
|
|
28
36
|
*/
|
|
@@ -236,8 +244,13 @@ export class StdinBuffer extends EventEmitter {
|
|
|
236
244
|
this.pasteMode = false;
|
|
237
245
|
this.pasteBuffer = "";
|
|
238
246
|
this.decoder = new StringDecoder("utf8");
|
|
247
|
+
this.heldNewline = "";
|
|
248
|
+
this.heldNewlineEndsPaste = false;
|
|
249
|
+
this.burstTimer = null;
|
|
239
250
|
this.timeoutMs = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT_MS;
|
|
240
251
|
this.escapeTimeoutMs = options.escapeTimeout ?? DEFAULT_ESCAPE_TIMEOUT_MS;
|
|
252
|
+
this.burstWindowMs = options.burstWindowMs ?? DEFAULT_BURST_WINDOW_MS;
|
|
253
|
+
this.clock = options.now ?? Date.now;
|
|
241
254
|
}
|
|
242
255
|
process(data) {
|
|
243
256
|
// Clear any pending timeout
|
|
@@ -280,7 +293,30 @@ export class StdinBuffer extends EventEmitter {
|
|
|
280
293
|
}
|
|
281
294
|
this.discardingMouseFragment = false;
|
|
282
295
|
}
|
|
296
|
+
// Only a read that adds input takes over a held line break. A read that leaves above (an empty
|
|
297
|
+
// decode of half a multibyte character, a dropped mouse fragment) must keep its release timer running.
|
|
298
|
+
if (this.burstTimer) {
|
|
299
|
+
clearTimeout(this.burstTimer);
|
|
300
|
+
this.burstTimer = null;
|
|
301
|
+
}
|
|
283
302
|
this.buffer += str;
|
|
303
|
+
const chunkAt = str.length > 0 ? this.clock() : undefined;
|
|
304
|
+
const burstGap = chunkAt !== undefined && this.lastInputAt !== undefined ? chunkAt - this.lastInputAt : undefined;
|
|
305
|
+
if (chunkAt !== undefined) {
|
|
306
|
+
this.lastInputAt = chunkAt;
|
|
307
|
+
}
|
|
308
|
+
if (this.heldNewline.length > 0) {
|
|
309
|
+
// The clock decides, not timer delivery: a read outside the window means the held line
|
|
310
|
+
// break was an Enter, even when its release timer has not fired yet (a stalled loop).
|
|
311
|
+
if (burstGap !== undefined && burstGap >= this.burstWindowMs) {
|
|
312
|
+
this.releaseHeldNewline();
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.buffer = this.heldNewline + this.buffer;
|
|
316
|
+
this.heldNewline = "";
|
|
317
|
+
this.heldNewlineEndsPaste = false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
284
320
|
if (this.pasteMode) {
|
|
285
321
|
this.pasteBuffer += this.buffer;
|
|
286
322
|
this.buffer = "";
|
|
@@ -326,6 +362,9 @@ export class StdinBuffer extends EventEmitter {
|
|
|
326
362
|
}
|
|
327
363
|
return;
|
|
328
364
|
}
|
|
365
|
+
if (this.consumeUnbracketedBurst(burstGap)) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
329
368
|
const result = extractCompleteSequences(this.buffer);
|
|
330
369
|
this.buffer = result.remainder;
|
|
331
370
|
if (this.buffer.startsWith("\x1b[<")) {
|
|
@@ -351,6 +390,71 @@ export class StdinBuffer extends EventEmitter {
|
|
|
351
390
|
}, timeoutMs);
|
|
352
391
|
}
|
|
353
392
|
}
|
|
393
|
+
consumeUnbracketedBurst(burstGap) {
|
|
394
|
+
const text = this.buffer;
|
|
395
|
+
if (text.length === 0 || text.includes(ESC) || text.includes(BRACKETED_PASTE_START)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const breaks = countLineBreaks(text);
|
|
399
|
+
if (breaks === 0) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
// Typing delivers one key per read, so a single read that holds two line breaks, or text after a
|
|
403
|
+
// line break, is pasted. Both carry text: a run of bare Enters stays keystrokes.
|
|
404
|
+
if ((breaks >= 2 || LINE_BREAK_BEFORE_TEXT.test(text)) && /[^\r\n]/.test(text)) {
|
|
405
|
+
this.buffer = "";
|
|
406
|
+
this.lastBurstPasteAt = this.lastInputAt;
|
|
407
|
+
this.emit("paste", text);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
const trailing = text.match(TRAILING_LINE_BREAK_RUN)?.[0];
|
|
411
|
+
if (trailing === undefined || burstGap === undefined || burstGap >= this.burstWindowMs) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const head = text.slice(0, text.length - trailing.length);
|
|
415
|
+
// A newline that lands inside the window right after a burst paste ends that paste: releasing it
|
|
416
|
+
// as Enter would submit the block, which the same text in one read never does.
|
|
417
|
+
const endsPaste = this.lastBurstPasteAt !== undefined &&
|
|
418
|
+
this.lastInputAt !== undefined &&
|
|
419
|
+
this.lastInputAt - this.lastBurstPasteAt < this.burstWindowMs;
|
|
420
|
+
// A read of bare line breaks is a keystroke unless it closes a paste, so it is forwarded at once.
|
|
421
|
+
if (head.length === 0 && !endsPaste) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
for (const chunk of head) {
|
|
425
|
+
this.emitDataSequence(chunk);
|
|
426
|
+
}
|
|
427
|
+
this.buffer = "";
|
|
428
|
+
this.holdTrailingNewline(trailing, endsPaste);
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
holdTrailingNewline(run, endsPaste) {
|
|
432
|
+
this.heldNewline = run;
|
|
433
|
+
this.heldNewlineEndsPaste = endsPaste;
|
|
434
|
+
if (this.burstTimer) {
|
|
435
|
+
clearTimeout(this.burstTimer);
|
|
436
|
+
}
|
|
437
|
+
this.burstTimer = setTimeout(() => {
|
|
438
|
+
this.burstTimer = null;
|
|
439
|
+
this.releaseHeldNewline();
|
|
440
|
+
}, this.burstWindowMs);
|
|
441
|
+
}
|
|
442
|
+
releaseHeldNewline() {
|
|
443
|
+
if (this.heldNewline.length === 0) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const held = this.heldNewline;
|
|
447
|
+
const endsPaste = this.heldNewlineEndsPaste;
|
|
448
|
+
this.heldNewline = "";
|
|
449
|
+
this.heldNewlineEndsPaste = false;
|
|
450
|
+
if (endsPaste) {
|
|
451
|
+
this.emit("paste", held);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const chunk of held) {
|
|
455
|
+
this.emitDataSequence(chunk);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
354
458
|
emitDataSequence(sequence) {
|
|
355
459
|
const rawCodepoint = sequence.length === 1 ? sequence.codePointAt(0) : undefined;
|
|
356
460
|
if (rawCodepoint !== undefined && rawCodepoint === this.pendingKittyPrintableCodepoint) {
|
|
@@ -365,6 +469,11 @@ export class StdinBuffer extends EventEmitter {
|
|
|
365
469
|
clearTimeout(this.timeout);
|
|
366
470
|
this.timeout = null;
|
|
367
471
|
}
|
|
472
|
+
if (this.burstTimer) {
|
|
473
|
+
clearTimeout(this.burstTimer);
|
|
474
|
+
this.burstTimer = null;
|
|
475
|
+
}
|
|
476
|
+
this.releaseHeldNewline();
|
|
368
477
|
if (this.buffer.length === 0) {
|
|
369
478
|
return [];
|
|
370
479
|
}
|
|
@@ -390,6 +499,13 @@ export class StdinBuffer extends EventEmitter {
|
|
|
390
499
|
clearTimeout(this.timeout);
|
|
391
500
|
this.timeout = null;
|
|
392
501
|
}
|
|
502
|
+
if (this.burstTimer) {
|
|
503
|
+
clearTimeout(this.burstTimer);
|
|
504
|
+
this.burstTimer = null;
|
|
505
|
+
}
|
|
506
|
+
this.heldNewline = "";
|
|
507
|
+
this.heldNewlineEndsPaste = false;
|
|
508
|
+
this.lastBurstPasteAt = undefined;
|
|
393
509
|
this.buffer = "";
|
|
394
510
|
this.pasteMode = false;
|
|
395
511
|
this.pasteBuffer = "";
|
package/dist/terminal.d.ts
CHANGED
|
@@ -77,6 +77,12 @@ export interface ProcessTerminalOptions {
|
|
|
77
77
|
* another byte, so high-latency transports need a longer reassembly window.
|
|
78
78
|
*/
|
|
79
79
|
export declare function resolveEscapeTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve how long a line break that ends a read with text is held as a possible paste fragment
|
|
82
|
+
* when the terminal sends no bracketed-paste markers. Paste chunks over SSH arrive further apart,
|
|
83
|
+
* so the default window is longer there. `PI_TUI_BURST_WINDOW_MS=0` never holds a line break.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveBurstWindowMs(env?: NodeJS.ProcessEnv): number;
|
|
80
86
|
/**
|
|
81
87
|
* Real terminal using process.stdin/stdout
|
|
82
88
|
*/
|
package/dist/terminal.js
CHANGED
|
@@ -163,6 +163,8 @@ function unsubscribeFromStdinErrors(subscriber) {
|
|
|
163
163
|
}
|
|
164
164
|
const DEFAULT_ESCAPE_TIMEOUT_MS = 10;
|
|
165
165
|
const DEFAULT_SSH_ESCAPE_TIMEOUT_MS = 100;
|
|
166
|
+
const DEFAULT_BURST_WINDOW_MS = 20;
|
|
167
|
+
const DEFAULT_SSH_BURST_WINDOW_MS = 100;
|
|
166
168
|
/**
|
|
167
169
|
* Resolve how long to wait for the rest of an escape sequence before
|
|
168
170
|
* dispatching a lone ESC as the Escape key. Legacy Alt+key input is ESC plus
|
|
@@ -178,6 +180,24 @@ export function resolveEscapeTimeoutMs(env = process.env) {
|
|
|
178
180
|
}
|
|
179
181
|
return DEFAULT_ESCAPE_TIMEOUT_MS;
|
|
180
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve how long a line break that ends a read with text is held as a possible paste fragment
|
|
185
|
+
* when the terminal sends no bracketed-paste markers. Paste chunks over SSH arrive further apart,
|
|
186
|
+
* so the default window is longer there. `PI_TUI_BURST_WINDOW_MS=0` never holds a line break.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveBurstWindowMs(env = process.env) {
|
|
189
|
+
const raw = env.PI_TUI_BURST_WINDOW_MS?.trim();
|
|
190
|
+
if (raw !== undefined && raw.length > 0) {
|
|
191
|
+
const configured = Number(raw);
|
|
192
|
+
if (Number.isFinite(configured) && configured >= 0) {
|
|
193
|
+
return configured;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (env.SSH_CONNECTION || env.SSH_TTY) {
|
|
197
|
+
return DEFAULT_SSH_BURST_WINDOW_MS;
|
|
198
|
+
}
|
|
199
|
+
return DEFAULT_BURST_WINDOW_MS;
|
|
200
|
+
}
|
|
181
201
|
/**
|
|
182
202
|
* Real terminal using process.stdin/stdout
|
|
183
203
|
*/
|
|
@@ -392,7 +412,10 @@ export class ProcessTerminal {
|
|
|
392
412
|
* to handle the case where the response arrives split across multiple events.
|
|
393
413
|
*/
|
|
394
414
|
setupStdinBuffer() {
|
|
395
|
-
this.stdinBuffer = new StdinBuffer({
|
|
415
|
+
this.stdinBuffer = new StdinBuffer({
|
|
416
|
+
escapeTimeout: resolveEscapeTimeoutMs(),
|
|
417
|
+
burstWindowMs: resolveBurstWindowMs(),
|
|
418
|
+
});
|
|
396
419
|
// Forward individual sequences to the input handler
|
|
397
420
|
this.stdinBuffer.on("data", (sequence) => {
|
|
398
421
|
if (this.discardingPrivateResponse) {
|
package/dist/tui.d.ts
CHANGED
|
@@ -257,6 +257,11 @@ export declare function renderAtFrameRow(child: Component, width: number, row: n
|
|
|
257
257
|
export declare function nextRenderRevision(): number;
|
|
258
258
|
/** Current value of the render revision clock (see {@link nextRenderRevision}). */
|
|
259
259
|
export declare function currentRenderRevision(): number;
|
|
260
|
+
export interface FrameLineBytesTotals {
|
|
261
|
+
readonly previousLinesBytes: number;
|
|
262
|
+
}
|
|
263
|
+
/** Sum of every live TUI's current frame-line bytes; `0` before any frame renders. */
|
|
264
|
+
export declare function frameLineBytesTotals(): FrameLineBytesTotals;
|
|
260
265
|
/**
|
|
261
266
|
* Render revision of a component whose output is a pure function of its own state and its children's
|
|
262
267
|
* output. `bump()` records an own-state change; `read(children)` returns a revision that also changes
|
|
@@ -504,7 +509,10 @@ export declare abstract class TuiBase extends Container {
|
|
|
504
509
|
*/
|
|
505
510
|
private static readonly FRAME_BEGIN;
|
|
506
511
|
private static readonly FRAME_END;
|
|
512
|
+
private frameLineBytes;
|
|
507
513
|
private setPreviousLines;
|
|
514
|
+
/** Releases this TUI's frame from the process total (stop/dispose resets the frame). */
|
|
515
|
+
private dropPreviousLines;
|
|
508
516
|
/** Image presence of the committed frame, measured once per frame array instead of once per check. */
|
|
509
517
|
protected previousLinesHaveImage(): boolean;
|
|
510
518
|
/** Record image presence for a produced array; `undefined` leaves it to a scan when it is committed. */
|
package/dist/tui.js
CHANGED
|
@@ -385,6 +385,27 @@ export function nextRenderRevision() {
|
|
|
385
385
|
export function currentRenderRevision() {
|
|
386
386
|
return renderRevisionClock;
|
|
387
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Byte cost of the frame the TUI last held, summed over every live TUI in the process (senpi#1960):
|
|
390
|
+
* `previousLines` is the whole frame a terminal keeps for the differential pass, so a long session's
|
|
391
|
+
* transcript cost lives here. The memory report reads the figure structurally through a process-global
|
|
392
|
+
* key; a process with no TUI reports no figure. Estimator: 2 bytes per UTF-16 code unit plus an 8-byte
|
|
393
|
+
* array slot per line - the same estimate the tool-card render cache uses, so the two figures compare.
|
|
394
|
+
*/
|
|
395
|
+
const FRAME_LINE_BYTES_KEY = Symbol.for("senpi.tui.frame-line-bytes");
|
|
396
|
+
function frameLineBytesState() {
|
|
397
|
+
const existing = Reflect.get(globalThis, FRAME_LINE_BYTES_KEY);
|
|
398
|
+
if (typeof existing === "object" && existing !== null && typeof Reflect.get(existing, "bytes") === "number") {
|
|
399
|
+
return existing;
|
|
400
|
+
}
|
|
401
|
+
const created = { bytes: 0 };
|
|
402
|
+
Reflect.set(globalThis, FRAME_LINE_BYTES_KEY, created);
|
|
403
|
+
return created;
|
|
404
|
+
}
|
|
405
|
+
/** Sum of every live TUI's current frame-line bytes; `0` before any frame renders. */
|
|
406
|
+
export function frameLineBytesTotals() {
|
|
407
|
+
return { previousLinesBytes: frameLineBytesState().bytes };
|
|
408
|
+
}
|
|
388
409
|
/**
|
|
389
410
|
* Render revision of a component whose output is a pure function of its own state and its children's
|
|
390
411
|
* output. `bump()` records an own-state change; `read(children)` returns a revision that also changes
|
|
@@ -654,6 +675,7 @@ export class TuiBase extends Container {
|
|
|
654
675
|
this.mouseCommittedLineCount = 0;
|
|
655
676
|
this.mouseAnchorPending = false;
|
|
656
677
|
this.mouseExternalWritePending = false;
|
|
678
|
+
this.frameLineBytes = 0;
|
|
657
679
|
this.terminal = terminal;
|
|
658
680
|
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
659
681
|
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
@@ -1272,8 +1294,7 @@ export class TuiBase extends Container {
|
|
|
1272
1294
|
this.afterTerminalStop(options);
|
|
1273
1295
|
this.resetRenderState();
|
|
1274
1296
|
this.#lastCursorVisibility = undefined;
|
|
1275
|
-
this.
|
|
1276
|
-
this.previousRawLines = [];
|
|
1297
|
+
this.dropPreviousLines();
|
|
1277
1298
|
this.previousKittyImageIds.clear();
|
|
1278
1299
|
this.previousWidth = 0;
|
|
1279
1300
|
this.previousHeight = 0;
|
|
@@ -1346,8 +1367,7 @@ export class TuiBase extends Container {
|
|
|
1346
1367
|
/** Drop every cached frame so the next render repaints from a clean slate. */
|
|
1347
1368
|
resetForcedRenderState() {
|
|
1348
1369
|
this.resetRenderState();
|
|
1349
|
-
this.
|
|
1350
|
-
this.previousRawLines = [];
|
|
1370
|
+
this.dropPreviousLines();
|
|
1351
1371
|
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
|
|
1352
1372
|
this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
|
|
1353
1373
|
this.cursorRow = 0;
|
|
@@ -1755,9 +1775,22 @@ export class TuiBase extends Container {
|
|
|
1755
1775
|
static { this.FRAME_BEGIN = "\x1b[?2026h\x1b[?7l"; }
|
|
1756
1776
|
static { this.FRAME_END = "\x1b[?7h\x1b[?2026l"; }
|
|
1757
1777
|
setPreviousLines(lines, rawLines) {
|
|
1778
|
+
const state = frameLineBytesState();
|
|
1779
|
+
state.bytes -= this.frameLineBytes;
|
|
1780
|
+
let bytes = lines.length * 8;
|
|
1781
|
+
for (const line of lines)
|
|
1782
|
+
bytes += line.length * 2;
|
|
1783
|
+
this.frameLineBytes = bytes;
|
|
1784
|
+
state.bytes += bytes;
|
|
1758
1785
|
this.previousLines = lines;
|
|
1759
1786
|
this.previousRawLines = rawLines;
|
|
1760
1787
|
}
|
|
1788
|
+
/** Releases this TUI's frame from the process total (stop/dispose resets the frame). */
|
|
1789
|
+
dropPreviousLines() {
|
|
1790
|
+
if (this.previousLines.length === 0 && this.previousRawLines.length === 0 && this.frameLineBytes === 0)
|
|
1791
|
+
return;
|
|
1792
|
+
this.setPreviousLines([], []);
|
|
1793
|
+
}
|
|
1761
1794
|
/** Image presence of the committed frame, measured once per frame array instead of once per check. */
|
|
1762
1795
|
previousLinesHaveImage() {
|
|
1763
1796
|
const lines = this.previousLines;
|
package/package.json
CHANGED