@code-yeongyu/senpi-tui 2026.9.12 → 2026.9.13-2
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.md +61 -4
- package/dist/alt-screen-search.d.ts +21 -1
- package/dist/alt-screen-search.d.ts.map +1 -1
- package/dist/alt-screen-search.js +199 -42
- package/dist/alt-screen-search.js.map +1 -1
- package/dist/components/box.d.ts +3 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +28 -0
- package/dist/components/box.js.map +1 -1
- package/dist/components/editor.d.ts +6 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +98 -18
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts +12 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +40 -6
- package/dist/components/input.js.map +1 -1
- package/dist/components/latex.d.ts.map +1 -1
- package/dist/components/latex.js +7 -0
- package/dist/components/latex.js.map +1 -1
- package/dist/components/loader.d.ts +3 -0
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +20 -8
- package/dist/components/loader.js.map +1 -1
- package/dist/components/mouse-region.d.ts +12 -0
- package/dist/components/mouse-region.d.ts.map +1 -0
- package/dist/components/mouse-region.js +19 -0
- package/dist/components/mouse-region.js.map +1 -0
- package/dist/components/scroll-view.d.ts +6 -3
- package/dist/components/scroll-view.d.ts.map +1 -1
- package/dist/components/scroll-view.js +6 -1
- package/dist/components/scroll-view.js.map +1 -1
- package/dist/components/select-list.d.ts +4 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +48 -2
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +5 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +54 -5
- package/dist/components/settings-list.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +3 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +32 -9
- package/dist/layout.js.map +1 -1
- package/dist/mouse-input.d.ts +37 -0
- package/dist/mouse-input.d.ts.map +1 -0
- package/dist/mouse-input.js +97 -0
- package/dist/mouse-input.js.map +1 -0
- package/dist/native-modifiers.d.ts +2 -1
- package/dist/native-modifiers.d.ts.map +1 -1
- package/dist/native-modifiers.js +3 -44
- package/dist/native-modifiers.js.map +1 -1
- package/dist/native-platform.d.ts +18 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +42 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/stdin-buffer.d.ts +2 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +38 -0
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal.d.ts +25 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +112 -24
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +31 -1
- package/dist/tui-alt-screen.d.ts.map +1 -1
- package/dist/tui-alt-screen.js +327 -78
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui-main-screen.d.ts +12 -0
- package/dist/tui-main-screen.d.ts.map +1 -1
- package/dist/tui-main-screen.js +129 -3
- package/dist/tui-main-screen.js.map +1 -1
- package/dist/tui.d.ts +111 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +278 -4
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -1
- package/native/clipboard.h +170 -0
- package/native/darwin/README.md +8 -6
- package/native/darwin/build.sh +12 -5
- package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/prebuilds/darwin-x64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/src/darwin-platform.m +93 -0
- package/native/linux/README.md +23 -0
- package/native/linux/build.sh +65 -0
- package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/native/linux/src/clipboard-worker.h +70 -0
- package/native/linux/src/linux-platform-x11.c +274 -0
- package/native/napi.h +89 -0
- package/native/win32/README.md +18 -7
- package/native/win32/build.mjs +7 -4
- package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/native/win32/src/win32-platform.c +241 -0
- package/package.json +10 -3
- package/native/darwin/src/darwin-modifiers.c +0 -70
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/native/win32/src/win32-console-mode.c +0 -119
package/dist/tui.js
CHANGED
|
@@ -60,6 +60,41 @@ function extractKittyImageRows(line) {
|
|
|
60
60
|
function isTermuxSession() {
|
|
61
61
|
return Boolean(process.env.TERMUX_VERSION);
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Dispatch an event to a component and retain the exact target and coordinate
|
|
65
|
+
* transform. Containers use this when forwarding events to nested children.
|
|
66
|
+
*/
|
|
67
|
+
export function dispatchMouseEvent(component, event) {
|
|
68
|
+
const result = component.handleMouse?.(event);
|
|
69
|
+
if (!result)
|
|
70
|
+
return undefined;
|
|
71
|
+
if ("target" in result)
|
|
72
|
+
return result;
|
|
73
|
+
if (!result.handled && !result.capture && !result.focus)
|
|
74
|
+
return undefined;
|
|
75
|
+
return {
|
|
76
|
+
...result,
|
|
77
|
+
handled: true,
|
|
78
|
+
...(result.focus ? { focusTarget: component } : {}),
|
|
79
|
+
target: {
|
|
80
|
+
component,
|
|
81
|
+
originX: event.screenX - event.x,
|
|
82
|
+
originY: event.screenY - event.y,
|
|
83
|
+
width: event.width,
|
|
84
|
+
height: event.height,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Recreate local coordinates for a previously dispatched mouse target. */
|
|
89
|
+
export function retargetMouseEvent(event, target) {
|
|
90
|
+
return {
|
|
91
|
+
...event,
|
|
92
|
+
x: event.screenX - target.originX,
|
|
93
|
+
y: event.screenY - target.originY,
|
|
94
|
+
width: target.width,
|
|
95
|
+
height: target.height,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
63
98
|
/** Type guard to check if a component implements Focusable */
|
|
64
99
|
export function isFocusable(component) {
|
|
65
100
|
return component !== null && "focused" in component;
|
|
@@ -250,8 +285,31 @@ export class Container {
|
|
|
250
285
|
child.invalidate?.();
|
|
251
286
|
}
|
|
252
287
|
}
|
|
288
|
+
handleMouse(event) {
|
|
289
|
+
if (event.y < 0 || event.y >= event.height)
|
|
290
|
+
return undefined;
|
|
291
|
+
const mouseChildren = this.mouseLayout?.width === event.width
|
|
292
|
+
? this.mouseLayout.children
|
|
293
|
+
: this.children.map((component) => ({ component, height: component.render(event.width).length }));
|
|
294
|
+
let childY = 0;
|
|
295
|
+
for (const { component: child, height: childHeight } of mouseChildren) {
|
|
296
|
+
if (event.y >= childY && event.y < childY + childHeight) {
|
|
297
|
+
const result = dispatchMouseEvent(child, {
|
|
298
|
+
...event,
|
|
299
|
+
y: event.y - childY,
|
|
300
|
+
height: childHeight,
|
|
301
|
+
});
|
|
302
|
+
if (result?.focus && this.handleInput)
|
|
303
|
+
return { ...result, focusTarget: this };
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
childY += childHeight;
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
253
310
|
render(width) {
|
|
254
311
|
const lines = [];
|
|
312
|
+
const mouseChildren = [];
|
|
255
313
|
for (const child of this.children) {
|
|
256
314
|
let childLines;
|
|
257
315
|
try {
|
|
@@ -263,10 +321,12 @@ export class Container {
|
|
|
263
321
|
// Focus ownership stays unchanged; render containment must not steal or clear focus implicitly.
|
|
264
322
|
childLines = [`[render error: ${componentName}]`];
|
|
265
323
|
}
|
|
324
|
+
mouseChildren.push({ component: child, height: childLines.length });
|
|
266
325
|
for (const line of childLines) {
|
|
267
326
|
lines.push(line);
|
|
268
327
|
}
|
|
269
328
|
}
|
|
329
|
+
this.mouseLayout = { width, children: mouseChildren };
|
|
270
330
|
return lines;
|
|
271
331
|
}
|
|
272
332
|
}
|
|
@@ -352,7 +412,15 @@ export class TuiBase extends Container {
|
|
|
352
412
|
// Overlay stack for modal components rendered on top of base content
|
|
353
413
|
this.focusOrderCounter = 0;
|
|
354
414
|
this.overlayStack = [];
|
|
415
|
+
this.renderedOverlayLayouts = [];
|
|
355
416
|
this.overlayFocusRestore = { status: "inactive" };
|
|
417
|
+
this.mouseLeases = new Map();
|
|
418
|
+
this.mouseBlockers = new Set();
|
|
419
|
+
this.placementEpoch = 0;
|
|
420
|
+
this.anchor = { kind: "unknown", epoch: 0, rows: 0, columns: 0 };
|
|
421
|
+
this.mouseCommittedLineCount = 0;
|
|
422
|
+
this.mouseAnchorPending = false;
|
|
423
|
+
this.mouseExternalWritePending = false;
|
|
356
424
|
this.terminal = terminal;
|
|
357
425
|
// Preserve existing positional boolean callers while allowing explicit render-policy overrides.
|
|
358
426
|
const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
|
|
@@ -362,6 +430,152 @@ export class TuiBase extends Container {
|
|
|
362
430
|
this.showHardwareCursor = normalizedOptions.showHardwareCursor;
|
|
363
431
|
}
|
|
364
432
|
}
|
|
433
|
+
/** Host-owned intent. A replacement renderer starts with no leases. */
|
|
434
|
+
acquireMouseCapture(reason) {
|
|
435
|
+
const token = Symbol(reason);
|
|
436
|
+
const first = this.mouseLeases.size === 0;
|
|
437
|
+
this.mouseLeases.set(token, reason);
|
|
438
|
+
if (first) {
|
|
439
|
+
this.mouseWriteUnsubscribe ??= this.terminal.observeExternalWrites?.(() => {
|
|
440
|
+
this.placementEpoch++;
|
|
441
|
+
this.mouseExternalWritePending = true;
|
|
442
|
+
});
|
|
443
|
+
this.applyMouseTracking(this.mouseCaptureEnabled);
|
|
444
|
+
this.calibrateMouseAnchor();
|
|
445
|
+
}
|
|
446
|
+
return () => {
|
|
447
|
+
if (!this.mouseLeases.delete(token))
|
|
448
|
+
return;
|
|
449
|
+
if (this.mouseLeases.size === 0)
|
|
450
|
+
this.applyMouseTracking(false);
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
resetMouseCaptureState() {
|
|
454
|
+
this.mouseLeases.clear();
|
|
455
|
+
this.mouseBlockers.clear();
|
|
456
|
+
this.mouseWriteUnsubscribe?.();
|
|
457
|
+
this.mouseWriteUnsubscribe = undefined;
|
|
458
|
+
this.placementEpoch++;
|
|
459
|
+
}
|
|
460
|
+
get mouseCaptureEnabled() {
|
|
461
|
+
return this.mouseLeases.size > 0 && this.mouseBlockers.size === 0;
|
|
462
|
+
}
|
|
463
|
+
applyMouseTracking(_enabled) { }
|
|
464
|
+
getMouseLayoutRoots() {
|
|
465
|
+
return [...this.getMountedRoots(), ...this.renderedOverlayLayouts.map((layout) => layout.entry.component)];
|
|
466
|
+
}
|
|
467
|
+
setMouseBlocker(name, on) {
|
|
468
|
+
if (this.mouseBlockers.has(name) === on)
|
|
469
|
+
return;
|
|
470
|
+
if (on)
|
|
471
|
+
this.mouseBlockers.add(name);
|
|
472
|
+
else
|
|
473
|
+
this.mouseBlockers.delete(name);
|
|
474
|
+
this.placementEpoch++;
|
|
475
|
+
this.applyMouseTracking(this.mouseCaptureEnabled);
|
|
476
|
+
}
|
|
477
|
+
noteFullRender(clear) {
|
|
478
|
+
this.placementEpoch++;
|
|
479
|
+
this.anchor = {
|
|
480
|
+
kind: clear ? "cleared" : "unknown",
|
|
481
|
+
frameTopScreenRow: clear ? 0 : undefined,
|
|
482
|
+
epoch: this.placementEpoch,
|
|
483
|
+
rows: this.terminal.rows,
|
|
484
|
+
columns: this.terminal.columns,
|
|
485
|
+
};
|
|
486
|
+
this.mouseCommittedLineCount = this.previousLines.length;
|
|
487
|
+
this.noteCommittedMouseFrame();
|
|
488
|
+
this.calibrateMouseAnchor();
|
|
489
|
+
}
|
|
490
|
+
/** Called only after the renderer has published its geometry and bytes. */
|
|
491
|
+
noteCommittedMouseFrame() {
|
|
492
|
+
if (this.mouseCommittedLineCount !== this.previousLines.length)
|
|
493
|
+
this.placementEpoch++;
|
|
494
|
+
this.mouseCommittedLineCount = this.previousLines.length;
|
|
495
|
+
if (this.previousLines.some(isImageLine)) {
|
|
496
|
+
this.placementEpoch++;
|
|
497
|
+
this.anchor.kind = "unknown";
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (this.previousLines.length >= this.terminal.rows) {
|
|
501
|
+
this.anchor = {
|
|
502
|
+
kind: "viewport",
|
|
503
|
+
epoch: this.placementEpoch,
|
|
504
|
+
rows: this.terminal.rows,
|
|
505
|
+
columns: this.terminal.columns,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
else if (this.anchor.epoch !== this.placementEpoch ||
|
|
509
|
+
this.anchor.rows !== this.terminal.rows ||
|
|
510
|
+
this.anchor.columns !== this.terminal.columns) {
|
|
511
|
+
this.anchor = {
|
|
512
|
+
kind: "unknown",
|
|
513
|
+
epoch: this.placementEpoch,
|
|
514
|
+
rows: this.terminal.rows,
|
|
515
|
+
columns: this.terminal.columns,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Never block a frame on terminal protocol negotiation or a missing reply. */
|
|
520
|
+
calibrateMouseAnchor() {
|
|
521
|
+
if (!this.mouseCaptureEnabled ||
|
|
522
|
+
this.stopped ||
|
|
523
|
+
this.mouseAnchorPending ||
|
|
524
|
+
this.mouseExternalWritePending ||
|
|
525
|
+
!this.terminal.queryCursorPosition ||
|
|
526
|
+
this.previousLines.length === 0 ||
|
|
527
|
+
this.previousLines.some(isImageLine))
|
|
528
|
+
return;
|
|
529
|
+
if (this.anchor.kind !== "unknown" &&
|
|
530
|
+
this.anchor.epoch === this.placementEpoch &&
|
|
531
|
+
this.anchor.rows === this.terminal.rows &&
|
|
532
|
+
this.anchor.columns === this.terminal.columns)
|
|
533
|
+
return;
|
|
534
|
+
const epoch = this.placementEpoch;
|
|
535
|
+
const rows = this.terminal.rows;
|
|
536
|
+
const columns = this.terminal.columns;
|
|
537
|
+
const hardwareCursorRow = this.hardwareCursorRow;
|
|
538
|
+
const lineCount = this.previousLines.length;
|
|
539
|
+
this.mouseAnchorPending = true;
|
|
540
|
+
void this.terminal.queryCursorPosition().then((position) => {
|
|
541
|
+
this.mouseAnchorPending = false;
|
|
542
|
+
if (!position ||
|
|
543
|
+
this.stopped ||
|
|
544
|
+
!this.mouseCaptureEnabled ||
|
|
545
|
+
epoch !== this.placementEpoch ||
|
|
546
|
+
rows !== this.terminal.rows ||
|
|
547
|
+
columns !== this.terminal.columns ||
|
|
548
|
+
hardwareCursorRow !== this.hardwareCursorRow ||
|
|
549
|
+
lineCount !== this.previousLines.length)
|
|
550
|
+
return;
|
|
551
|
+
const top = position.row - 1 - hardwareCursorRow;
|
|
552
|
+
if (!Number.isSafeInteger(top) ||
|
|
553
|
+
top < 0 ||
|
|
554
|
+
top + lineCount > rows ||
|
|
555
|
+
!Number.isSafeInteger(position.column) ||
|
|
556
|
+
position.column < 1 ||
|
|
557
|
+
// Some emulators report the pending-wrap cell just past the right edge.
|
|
558
|
+
position.column > columns + 1 ||
|
|
559
|
+
(position.page !== undefined && position.page !== 1))
|
|
560
|
+
return;
|
|
561
|
+
this.anchor = { kind: "cpr", frameTopScreenRow: top, epoch, rows, columns };
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/** Input rows are one-based; the returned committed frame line is zero-based. */
|
|
565
|
+
resolveFrameLine(screenRow) {
|
|
566
|
+
const anchor = this.anchor;
|
|
567
|
+
if (anchor.kind === "unknown" ||
|
|
568
|
+
anchor.epoch !== this.placementEpoch ||
|
|
569
|
+
anchor.rows !== this.terminal.rows ||
|
|
570
|
+
anchor.columns !== this.terminal.columns ||
|
|
571
|
+
screenRow < 1 ||
|
|
572
|
+
screenRow > anchor.rows)
|
|
573
|
+
return undefined;
|
|
574
|
+
const line = anchor.kind === "viewport"
|
|
575
|
+
? this.previousViewportTop + screenRow - 1
|
|
576
|
+
: screenRow - 1 - (anchor.frameTopScreenRow ?? 0);
|
|
577
|
+
return line >= 0 && line < this.previousLines.length ? line : undefined;
|
|
578
|
+
}
|
|
365
579
|
resetRenderState() { }
|
|
366
580
|
beforeTerminalStart() { }
|
|
367
581
|
afterTerminalStart() { }
|
|
@@ -387,8 +601,8 @@ export class TuiBase extends Container {
|
|
|
387
601
|
}
|
|
388
602
|
/**
|
|
389
603
|
* Set whether to trigger full re-render when content shrinks.
|
|
390
|
-
* When true
|
|
391
|
-
* When false, empty rows remain (reduces redraws on slower terminals).
|
|
604
|
+
* When true, empty rows are cleared when content shrinks.
|
|
605
|
+
* When false (default), empty rows remain (reduces redraws on slower terminals).
|
|
392
606
|
*/
|
|
393
607
|
setClearOnShrink(enabled) {
|
|
394
608
|
this.clearOnShrink = enabled;
|
|
@@ -608,6 +822,11 @@ export class TuiBase extends Container {
|
|
|
608
822
|
this.requestRender();
|
|
609
823
|
},
|
|
610
824
|
isFocused: () => this.focusedComponent === component,
|
|
825
|
+
getBounds: () => {
|
|
826
|
+
if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry) || !entry.bounds)
|
|
827
|
+
return undefined;
|
|
828
|
+
return { ...entry.bounds };
|
|
829
|
+
},
|
|
611
830
|
};
|
|
612
831
|
}
|
|
613
832
|
/** Hide the topmost overlay and restore previous focus. */
|
|
@@ -635,6 +854,42 @@ export class TuiBase extends Container {
|
|
|
635
854
|
isOverlayFocused() {
|
|
636
855
|
return this.overlayStack.some((entry) => entry.component === this.focusedComponent && this.isOverlayVisible(entry));
|
|
637
856
|
}
|
|
857
|
+
/** Keep overlay containers as keyboard focus owners when a nested control is clicked. */
|
|
858
|
+
resolveMouseFocusTarget(component) {
|
|
859
|
+
for (let index = this.overlayStack.length - 1; index >= 0; index--) {
|
|
860
|
+
const overlay = this.overlayStack[index];
|
|
861
|
+
if (this.isOverlayVisible(overlay) && this.containsComponent(overlay.component, component)) {
|
|
862
|
+
return overlay.component;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return component;
|
|
866
|
+
}
|
|
867
|
+
/** Dispatch to the visually topmost overlay under the pointer. */
|
|
868
|
+
dispatchMouseToOverlay(event) {
|
|
869
|
+
for (let index = this.renderedOverlayLayouts.length - 1; index >= 0; index--) {
|
|
870
|
+
const layout = this.renderedOverlayLayouts[index];
|
|
871
|
+
if (event.screenX < layout.col ||
|
|
872
|
+
event.screenX >= layout.col + layout.width ||
|
|
873
|
+
event.screenY < layout.row ||
|
|
874
|
+
event.screenY >= layout.row + layout.height) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const result = dispatchMouseEvent(layout.entry.component, {
|
|
878
|
+
...event,
|
|
879
|
+
x: event.screenX - layout.col,
|
|
880
|
+
y: event.screenY - layout.row,
|
|
881
|
+
width: layout.width,
|
|
882
|
+
height: layout.height,
|
|
883
|
+
});
|
|
884
|
+
return result
|
|
885
|
+
? {
|
|
886
|
+
hit: true,
|
|
887
|
+
result: result.focus ? { ...result, focusTarget: layout.entry.component } : result,
|
|
888
|
+
}
|
|
889
|
+
: { hit: true };
|
|
890
|
+
}
|
|
891
|
+
return { hit: false };
|
|
892
|
+
}
|
|
638
893
|
/** Check if an overlay entry is currently visible */
|
|
639
894
|
isOverlayVisible(entry) {
|
|
640
895
|
if (entry.hidden)
|
|
@@ -721,6 +976,7 @@ export class TuiBase extends Container {
|
|
|
721
976
|
this.terminal.write("\x1b[?2031l");
|
|
722
977
|
}
|
|
723
978
|
this.beforeTerminalStop(options);
|
|
979
|
+
this.resetMouseCaptureState();
|
|
724
980
|
// Move cursor to the end of the content to prevent overwriting/artifacts on exit.
|
|
725
981
|
// Skipped when the screen is preserved for another renderer taking over this terminal.
|
|
726
982
|
if (!options.preserveScreen && this.previousLines.length > 0) {
|
|
@@ -1129,9 +1385,13 @@ export class TuiBase extends Container {
|
|
|
1129
1385
|
}
|
|
1130
1386
|
/** Composite all overlays into content lines (sorted by focusOrder, higher = on top). */
|
|
1131
1387
|
compositeOverlays(lines, termWidth, termHeight) {
|
|
1132
|
-
if (this.overlayStack.length === 0)
|
|
1388
|
+
if (this.overlayStack.length === 0) {
|
|
1389
|
+
this.renderedOverlayLayouts = [];
|
|
1133
1390
|
return lines;
|
|
1391
|
+
}
|
|
1134
1392
|
const result = [...lines];
|
|
1393
|
+
for (const entry of this.overlayStack)
|
|
1394
|
+
entry.bounds = undefined;
|
|
1135
1395
|
// Pre-render all visible overlays and calculate positions
|
|
1136
1396
|
const rendered = [];
|
|
1137
1397
|
let minLinesNeeded = result.length;
|
|
@@ -1150,9 +1410,17 @@ export class TuiBase extends Container {
|
|
|
1150
1410
|
}
|
|
1151
1411
|
// Get final row/col with actual overlay height
|
|
1152
1412
|
const { row, col } = this.resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight);
|
|
1153
|
-
|
|
1413
|
+
entry.bounds = { row, col, width, height: overlayLines.length };
|
|
1414
|
+
rendered.push({ entry, overlayLines, row, col, w: width });
|
|
1154
1415
|
minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length);
|
|
1155
1416
|
}
|
|
1417
|
+
this.renderedOverlayLayouts = rendered.map(({ entry, row, col, w, overlayLines }) => ({
|
|
1418
|
+
entry,
|
|
1419
|
+
row,
|
|
1420
|
+
col,
|
|
1421
|
+
width: w,
|
|
1422
|
+
height: overlayLines.length,
|
|
1423
|
+
}));
|
|
1156
1424
|
// Pad to at least terminal height so overlays have screen-relative positions.
|
|
1157
1425
|
// Excludes maxLinesRendered: the historical high-water mark caused self-reinforcing
|
|
1158
1426
|
// inflation that pushed content into scrollback on terminal widen.
|
|
@@ -1435,6 +1703,7 @@ export class TuiBase extends Container {
|
|
|
1435
1703
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1436
1704
|
this.previousWidth = width;
|
|
1437
1705
|
this.previousHeight = height;
|
|
1706
|
+
this.placementEpoch++;
|
|
1438
1707
|
}
|
|
1439
1708
|
renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
|
|
1440
1709
|
let buffer = TUI.FRAME_BEGIN;
|
|
@@ -1462,6 +1731,7 @@ export class TuiBase extends Container {
|
|
|
1462
1731
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1463
1732
|
this.previousWidth = width;
|
|
1464
1733
|
this.previousHeight = height;
|
|
1734
|
+
this.placementEpoch++;
|
|
1465
1735
|
}
|
|
1466
1736
|
renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
|
|
1467
1737
|
const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
|
|
@@ -1491,6 +1761,7 @@ export class TuiBase extends Container {
|
|
|
1491
1761
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1492
1762
|
this.previousWidth = width;
|
|
1493
1763
|
this.previousHeight = height;
|
|
1764
|
+
this.placementEpoch++;
|
|
1494
1765
|
return true;
|
|
1495
1766
|
}
|
|
1496
1767
|
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
|
@@ -1581,6 +1852,8 @@ export class TuiBase extends Container {
|
|
|
1581
1852
|
const height = this.terminal.rows;
|
|
1582
1853
|
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
|
|
1583
1854
|
const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height;
|
|
1855
|
+
if (widthChanged || heightChanged)
|
|
1856
|
+
this.placementEpoch++;
|
|
1584
1857
|
const previousBufferLength = this.previousHeight > 0 ? this.previousViewportTop + this.previousHeight : height;
|
|
1585
1858
|
let prevViewportTop = heightChanged ? Math.max(0, previousBufferLength - height) : this.previousViewportTop;
|
|
1586
1859
|
let viewportTop = prevViewportTop;
|
|
@@ -1651,6 +1924,7 @@ export class TuiBase extends Container {
|
|
|
1651
1924
|
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
|
1652
1925
|
this.previousWidth = width;
|
|
1653
1926
|
this.previousHeight = height;
|
|
1927
|
+
this.noteFullRender(clear);
|
|
1654
1928
|
};
|
|
1655
1929
|
const debugRedraw = process.env.PI_DEBUG_REDRAW === "1";
|
|
1656
1930
|
const logRedraw = (reason) => {
|