@sayknow-cli/tui 0.3.11 → 0.3.13
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/package.json +8 -9
- package/src/components/image.ts +79 -39
- package/src/components/markdown.ts +282 -37
- package/src/components/sayknow-pet.ts +435 -0
- package/src/components/text.ts +60 -36
- package/src/index.ts +1 -0
- package/src/terminal-capabilities.ts +42 -0
- package/src/tui.ts +471 -55
- package/src/utils.ts +144 -8
- package/dist/types/animation-scheduler.d.ts +0 -13
- package/dist/types/autocomplete.d.ts +0 -83
- package/dist/types/bracketed-paste.d.ts +0 -26
- package/dist/types/components/box.d.ts +0 -20
- package/dist/types/components/cancellable-loader.d.ts +0 -21
- package/dist/types/components/editor.d.ts +0 -126
- package/dist/types/components/image.d.ts +0 -16
- package/dist/types/components/input.d.ts +0 -16
- package/dist/types/components/loader.d.ts +0 -23
- package/dist/types/components/markdown.d.ts +0 -77
- package/dist/types/components/select-list.d.ts +0 -46
- package/dist/types/components/settings-list.d.ts +0 -39
- package/dist/types/components/spacer.d.ts +0 -11
- package/dist/types/components/tab-bar.d.ts +0 -56
- package/dist/types/components/text.d.ts +0 -13
- package/dist/types/components/truncated-text.d.ts +0 -10
- package/dist/types/editor-component.d.ts +0 -36
- package/dist/types/fuzzy.d.ts +0 -15
- package/dist/types/index.d.ts +0 -27
- package/dist/types/keybindings.d.ts +0 -201
- package/dist/types/keys.d.ts +0 -208
- package/dist/types/kill-ring.d.ts +0 -27
- package/dist/types/metrics.d.ts +0 -85
- package/dist/types/stdin-buffer.d.ts +0 -50
- package/dist/types/symbols.d.ts +0 -23
- package/dist/types/terminal-capabilities.d.ts +0 -143
- package/dist/types/terminal.d.ts +0 -90
- package/dist/types/ttyid.d.ts +0 -9
- package/dist/types/tui.d.ts +0 -215
- package/dist/types/utils.d.ts +0 -87
package/src/tui.ts
CHANGED
|
@@ -98,6 +98,82 @@ export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
|
98
98
|
|
|
99
99
|
export { visibleWidth };
|
|
100
100
|
|
|
101
|
+
/** Durable source identifier for a semantically anchored viewport row. */
|
|
102
|
+
export type ViewportAnchorId = string;
|
|
103
|
+
|
|
104
|
+
export interface ViewportAnchorRow {
|
|
105
|
+
id: ViewportAnchorId;
|
|
106
|
+
graphemeStart: number;
|
|
107
|
+
graphemeEnd: number;
|
|
108
|
+
cellStart: number;
|
|
109
|
+
cellEnd: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ViewportAnchorRender {
|
|
113
|
+
lines: string[];
|
|
114
|
+
anchors: Array<ViewportAnchorRow | null>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ViewportAnchorProvider extends Component {
|
|
118
|
+
renderWithViewportAnchors(width: number): ViewportAnchorRender;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ViewportAnchorSource {
|
|
122
|
+
id: ViewportAnchorId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ViewportAnchorSourceRenderer extends Component {
|
|
126
|
+
renderWithViewportAnchorSource(width: number, source: ViewportAnchorSource): ViewportAnchorRender;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isViewportAnchorProvider(component: Component): component is ViewportAnchorProvider {
|
|
130
|
+
if (!("renderWithViewportAnchors" in component) || typeof component.renderWithViewportAnchors !== "function") {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return !(
|
|
134
|
+
component instanceof Container &&
|
|
135
|
+
component.renderWithViewportAnchors === Container.prototype.renderWithViewportAnchors &&
|
|
136
|
+
component.render !== Container.prototype.render
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isViewportAnchorSourceRenderer(component: Component): component is ViewportAnchorSourceRenderer {
|
|
141
|
+
return (
|
|
142
|
+
"renderWithViewportAnchorSource" in component && typeof component.renderWithViewportAnchorSource === "function"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function renderComponentWithViewportAnchors(component: Component, width: number): ViewportAnchorRender {
|
|
147
|
+
if (isViewportAnchorProvider(component)) {
|
|
148
|
+
const rendered = component.renderWithViewportAnchors(width);
|
|
149
|
+
if (rendered.anchors.length !== rendered.lines.length) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Viewport anchor provider returned ${rendered.anchors.length} anchors for ${rendered.lines.length} lines`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return rendered;
|
|
155
|
+
}
|
|
156
|
+
const lines = component.render(width);
|
|
157
|
+
return { lines, anchors: lines.map(() => null) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function renderComponentWithViewportAnchorSource(
|
|
161
|
+
component: Component,
|
|
162
|
+
width: number,
|
|
163
|
+
source: ViewportAnchorSource,
|
|
164
|
+
): ViewportAnchorRender {
|
|
165
|
+
if (!isViewportAnchorSourceRenderer(component)) {
|
|
166
|
+
throw new TypeError("Viewport anchor sources require renderer-owned row metadata");
|
|
167
|
+
}
|
|
168
|
+
const rendered = component.renderWithViewportAnchorSource(width, source);
|
|
169
|
+
if (rendered.anchors.length !== rendered.lines.length) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Viewport anchor source renderer returned ${rendered.anchors.length} anchors for ${rendered.lines.length} lines`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return rendered;
|
|
175
|
+
}
|
|
176
|
+
|
|
101
177
|
/**
|
|
102
178
|
* Anchor position for overlays
|
|
103
179
|
*/
|
|
@@ -212,6 +288,17 @@ function useViewportRepaintPath(terminal: Terminal): boolean {
|
|
|
212
288
|
});
|
|
213
289
|
}
|
|
214
290
|
|
|
291
|
+
function allowsHostNeutralOverflowRepaint(
|
|
292
|
+
terminal: Terminal,
|
|
293
|
+
env: Record<string, string | undefined> = Bun.env,
|
|
294
|
+
): boolean {
|
|
295
|
+
return (
|
|
296
|
+
terminal.isProcessTerminal === true &&
|
|
297
|
+
!isTermuxSession(env) &&
|
|
298
|
+
!(isMultiplexerSession(env) && useLegacyMultiplexerFullRender(env))
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
215
302
|
function shouldPreserveScrollbackOnFullClear(terminal: Terminal): boolean {
|
|
216
303
|
return isViewportSensitiveHost(Bun.env, process.platform, terminal.isProcessTerminal === true);
|
|
217
304
|
}
|
|
@@ -271,9 +358,10 @@ export interface OverlayHandle {
|
|
|
271
358
|
/**
|
|
272
359
|
* Container - a component that contains other components
|
|
273
360
|
*/
|
|
274
|
-
export class Container implements
|
|
361
|
+
export class Container implements ViewportAnchorProvider {
|
|
275
362
|
children: Component[] = [];
|
|
276
363
|
#disposed = false;
|
|
364
|
+
#viewportAnchorSources = new Map<Component, ViewportAnchorSource>();
|
|
277
365
|
|
|
278
366
|
addChild(component: Component): void {
|
|
279
367
|
this.children.push(component);
|
|
@@ -283,6 +371,7 @@ export class Container implements Component {
|
|
|
283
371
|
const index = this.children.indexOf(component);
|
|
284
372
|
if (index !== -1) {
|
|
285
373
|
this.children.splice(index, 1);
|
|
374
|
+
this.#viewportAnchorSources.delete(component);
|
|
286
375
|
component.dispose?.();
|
|
287
376
|
}
|
|
288
377
|
}
|
|
@@ -292,45 +381,62 @@ export class Container implements Component {
|
|
|
292
381
|
const index = this.children.indexOf(component);
|
|
293
382
|
if (index !== -1) {
|
|
294
383
|
this.children.splice(index, 1);
|
|
384
|
+
this.#viewportAnchorSources.delete(component);
|
|
295
385
|
}
|
|
296
386
|
}
|
|
297
387
|
|
|
298
388
|
clear(): void {
|
|
299
|
-
for (const child of this.children)
|
|
300
|
-
child.dispose?.();
|
|
301
|
-
}
|
|
389
|
+
for (const child of this.children) child.dispose?.();
|
|
302
390
|
this.children = [];
|
|
391
|
+
this.#viewportAnchorSources.clear();
|
|
303
392
|
}
|
|
304
393
|
|
|
305
394
|
/** Remove all children without disposing them (for detach-then-readd reuse). */
|
|
306
395
|
detachAll(): void {
|
|
307
396
|
this.children = [];
|
|
397
|
+
this.#viewportAnchorSources.clear();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Registers a direct child as eligible for semantic viewport anchoring. */
|
|
401
|
+
setViewportAnchorSource(component: Component, source: ViewportAnchorSource | null): void {
|
|
402
|
+
if (source !== null && !isViewportAnchorSourceRenderer(component)) {
|
|
403
|
+
throw new TypeError("Viewport anchor sources require renderer-owned row metadata");
|
|
404
|
+
}
|
|
405
|
+
if (source === null) this.#viewportAnchorSources.delete(component);
|
|
406
|
+
else this.#viewportAnchorSources.set(component, source);
|
|
308
407
|
}
|
|
309
408
|
|
|
310
409
|
dispose(): void {
|
|
311
410
|
if (this.#disposed) return;
|
|
312
411
|
this.#disposed = true;
|
|
313
|
-
for (const child of this.children)
|
|
314
|
-
|
|
315
|
-
}
|
|
412
|
+
for (const child of this.children) child.dispose?.();
|
|
413
|
+
this.#viewportAnchorSources.clear();
|
|
316
414
|
}
|
|
317
415
|
|
|
318
416
|
invalidate(): void {
|
|
319
|
-
for (const child of this.children)
|
|
320
|
-
child.invalidate?.();
|
|
321
|
-
}
|
|
417
|
+
for (const child of this.children) child.invalidate?.();
|
|
322
418
|
}
|
|
323
419
|
|
|
324
420
|
render(width: number): string[] {
|
|
421
|
+
return this.renderWithViewportAnchors(width).lines;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
renderWithViewportAnchors(width: number): ViewportAnchorRender {
|
|
325
425
|
width = Math.max(1, width);
|
|
326
426
|
const lines: string[] = [];
|
|
427
|
+
const anchors: Array<ViewportAnchorRow | null> = [];
|
|
327
428
|
for (const child of this.children) {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
429
|
+
const source = this.#viewportAnchorSources.get(child);
|
|
430
|
+
const rendered =
|
|
431
|
+
source === undefined
|
|
432
|
+
? safeRenderComponentWithViewportAnchors(child, width, "container-child")
|
|
433
|
+
: safeRenderComponentWithViewportAnchorSource(child, width, source, "container-anchor-child");
|
|
434
|
+
for (let index = 0; index < rendered.lines.length; index++) {
|
|
435
|
+
lines.push(rendered.lines[index]);
|
|
436
|
+
anchors.push(rendered.anchors[index] ?? null);
|
|
331
437
|
}
|
|
332
438
|
}
|
|
333
|
-
return lines;
|
|
439
|
+
return { lines, anchors };
|
|
334
440
|
}
|
|
335
441
|
}
|
|
336
442
|
|
|
@@ -348,23 +454,58 @@ const reportedRenderErrors = new Set<string>();
|
|
|
348
454
|
* command such as `/background`). Isolate the failure: log it once, emit a
|
|
349
455
|
* visible fallback line, and keep rendering the rest of the tree.
|
|
350
456
|
*/
|
|
457
|
+
function renderFailure(component: Component, where: string, err: unknown): string[] {
|
|
458
|
+
const name = component?.constructor?.name ?? "Component";
|
|
459
|
+
const key = `${where}:${name}:${err instanceof Error ? err.message : String(err)}`;
|
|
460
|
+
if (!reportedRenderErrors.has(key)) {
|
|
461
|
+
if (reportedRenderErrors.size >= MAX_REPORTED_RENDER_ERRORS) reportedRenderErrors.clear();
|
|
462
|
+
reportedRenderErrors.add(key);
|
|
463
|
+
logger.error("Component render failed; emitting fallback line", {
|
|
464
|
+
where,
|
|
465
|
+
component: name,
|
|
466
|
+
error: err instanceof Error ? err.message : String(err),
|
|
467
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return [`[render error: ${name}]`];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let viewportAnchorRenderFailureCount = 0;
|
|
474
|
+
|
|
351
475
|
function safeRenderComponent(component: Component, width: number, where: string): string[] {
|
|
352
476
|
try {
|
|
353
477
|
return component.render(width);
|
|
354
478
|
} catch (err) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
479
|
+
return renderFailure(component, where, err);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function safeRenderComponentWithViewportAnchors(
|
|
484
|
+
component: Component,
|
|
485
|
+
width: number,
|
|
486
|
+
where: string,
|
|
487
|
+
): ViewportAnchorRender {
|
|
488
|
+
try {
|
|
489
|
+
return renderComponentWithViewportAnchors(component, width);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
viewportAnchorRenderFailureCount += 1;
|
|
492
|
+
const lines = renderFailure(component, where, err);
|
|
493
|
+
return { lines, anchors: lines.map(() => null) };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function safeRenderComponentWithViewportAnchorSource(
|
|
498
|
+
component: Component,
|
|
499
|
+
width: number,
|
|
500
|
+
source: ViewportAnchorSource,
|
|
501
|
+
where: string,
|
|
502
|
+
): ViewportAnchorRender {
|
|
503
|
+
try {
|
|
504
|
+
return renderComponentWithViewportAnchorSource(component, width, source);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
viewportAnchorRenderFailureCount += 1;
|
|
507
|
+
const lines = renderFailure(component, where, err);
|
|
508
|
+
return { lines, anchors: lines.map(() => null) };
|
|
368
509
|
}
|
|
369
510
|
}
|
|
370
511
|
|
|
@@ -374,6 +515,18 @@ type LineNormalizationCacheEntry = {
|
|
|
374
515
|
width: number | undefined;
|
|
375
516
|
};
|
|
376
517
|
|
|
518
|
+
type ViewportAnchorFrame = {
|
|
519
|
+
startRow: number;
|
|
520
|
+
anchors: Array<ViewportAnchorRow | null>;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
type ManualViewportAnchor = {
|
|
524
|
+
id: ViewportAnchorId;
|
|
525
|
+
graphemeIndex: number;
|
|
526
|
+
cellOffset: number;
|
|
527
|
+
desiredScreenRow: number;
|
|
528
|
+
};
|
|
529
|
+
|
|
377
530
|
type TuiRenderCounterSnapshot = {
|
|
378
531
|
debugRedrawEnvReads: number;
|
|
379
532
|
debugRedrawAppendWrites: number;
|
|
@@ -386,6 +539,7 @@ type TuiRenderCounterSnapshot = {
|
|
|
386
539
|
export class TUI extends Container {
|
|
387
540
|
terminal: Terminal;
|
|
388
541
|
#previousLines: string[] = [];
|
|
542
|
+
#latestRenderedLines: string[] = [];
|
|
389
543
|
/**
|
|
390
544
|
* Raw (pre-normalization) lines from the previous frame, kept only when the
|
|
391
545
|
* virtual-viewport flag is on. Used to detect whether the off-screen prefix is
|
|
@@ -418,6 +572,11 @@ export class TUI extends Container {
|
|
|
418
572
|
#hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
|
419
573
|
#viewportTopRow = 0; // Content row currently mapped to screen row 0
|
|
420
574
|
#manualViewportTop: number | undefined;
|
|
575
|
+
#viewportAnchorComponent: Component | null = null;
|
|
576
|
+
#viewportAnchorFrame: ViewportAnchorFrame | null = null;
|
|
577
|
+
#manualViewportAnchor: ManualViewportAnchor | null = null;
|
|
578
|
+
#manualViewportFallbackAnchors: ManualViewportAnchor[] = [];
|
|
579
|
+
#reconcileMissingViewportAnchor = false;
|
|
421
580
|
#lastCursorPosition: { row: number; col: number } | null = null;
|
|
422
581
|
#sixelProbePendingDa = false;
|
|
423
582
|
#sixelProbePendingGraphics = false;
|
|
@@ -555,15 +714,100 @@ export class TUI extends Container {
|
|
|
555
714
|
this.#bottomPinnedComponent = component;
|
|
556
715
|
this.requestRender();
|
|
557
716
|
}
|
|
717
|
+
|
|
718
|
+
/** Register the direct child whose rows are eligible for semantic viewport anchoring. */
|
|
719
|
+
setViewportAnchorComponent(component: Component | null): void {
|
|
720
|
+
if (component !== null && !isViewportAnchorProvider(component)) {
|
|
721
|
+
throw new TypeError("Viewport anchor components must provide renderer-owned row metadata");
|
|
722
|
+
}
|
|
723
|
+
if (this.#viewportAnchorComponent === component) return;
|
|
724
|
+
this.#viewportAnchorComponent = component;
|
|
725
|
+
this.#viewportAnchorFrame = null;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Clear manual viewport ownership before replacing the transcript identity namespace. */
|
|
729
|
+
resetViewportAnchorIntent(): void {
|
|
730
|
+
this.#manualViewportTop = undefined;
|
|
731
|
+
this.#manualViewportAnchor = null;
|
|
732
|
+
this.#manualViewportFallbackAnchors = [];
|
|
733
|
+
this.#reconcileMissingViewportAnchor = false;
|
|
734
|
+
this.#viewportAnchorFrame = null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Allow one semantic-neighbor reconciliation after a definitive same-transcript rebuild. */
|
|
738
|
+
prepareViewportAnchorForTranscriptRebuild(): void {
|
|
739
|
+
if (this.#manualViewportAnchor !== null) this.#reconcileMissingViewportAnchor = true;
|
|
740
|
+
}
|
|
741
|
+
|
|
558
742
|
scrollViewportPages(direction: -1 | 1): boolean {
|
|
559
743
|
const height = this.terminal.rows;
|
|
560
744
|
const width = this.terminal.columns;
|
|
561
745
|
if (height <= 0 || width <= 0 || this.#previousLines.length === 0) return false;
|
|
562
746
|
const maxViewportTop = Math.max(0, this.#previousLines.length - height);
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
747
|
+
let currentViewportTop = Math.max(0, Math.min(maxViewportTop, this.#manualViewportTop ?? this.#viewportTopRow));
|
|
748
|
+
const frame = this.#viewportAnchorFrame;
|
|
749
|
+
if (this.#manualViewportAnchor !== null) {
|
|
750
|
+
if (frame === null) return false;
|
|
751
|
+
const resolvedViewportTop = this.#resolveManualAnchor(frame);
|
|
752
|
+
if (resolvedViewportTop === null) return false;
|
|
753
|
+
currentViewportTop = Math.max(0, Math.min(maxViewportTop, resolvedViewportTop));
|
|
754
|
+
}
|
|
755
|
+
const targetViewportTop = Math.max(
|
|
756
|
+
0,
|
|
757
|
+
Math.min(maxViewportTop, currentViewportTop + direction * Math.max(1, height - 1)),
|
|
758
|
+
);
|
|
759
|
+
if (frame !== null) {
|
|
760
|
+
const desiredScreenRow = this.#manualViewportAnchor?.desiredScreenRow ?? (direction < 0 ? 0 : height - 1);
|
|
761
|
+
const targetRow = targetViewportTop + desiredScreenRow - frame.startRow;
|
|
762
|
+
let selected: { row: number; anchor: ViewportAnchorRow } | undefined;
|
|
763
|
+
const firstCandidateRow = Math.max(0, targetViewportTop - frame.startRow);
|
|
764
|
+
const lastCandidateRow = Math.min(frame.anchors.length, targetViewportTop + height - frame.startRow);
|
|
765
|
+
for (let row = firstCandidateRow; row < lastCandidateRow; row++) {
|
|
766
|
+
const anchor = frame.anchors[row];
|
|
767
|
+
if (anchor === null) continue;
|
|
768
|
+
if (
|
|
769
|
+
selected === undefined ||
|
|
770
|
+
Math.abs(row - targetRow) < Math.abs(selected.row - targetRow) ||
|
|
771
|
+
(Math.abs(row - targetRow) === Math.abs(selected.row - targetRow) &&
|
|
772
|
+
(direction < 0 ? row < selected.row : row > selected.row))
|
|
773
|
+
)
|
|
774
|
+
selected = { row, anchor };
|
|
775
|
+
}
|
|
776
|
+
if (selected === undefined) {
|
|
777
|
+
if (this.#manualViewportAnchor !== null) return false;
|
|
778
|
+
} else {
|
|
779
|
+
this.#manualViewportAnchor = {
|
|
780
|
+
id: selected.anchor.id,
|
|
781
|
+
graphemeIndex:
|
|
782
|
+
direction < 0
|
|
783
|
+
? selected.anchor.graphemeStart
|
|
784
|
+
: Math.max(selected.anchor.graphemeStart, selected.anchor.graphemeEnd - 1),
|
|
785
|
+
cellOffset:
|
|
786
|
+
direction < 0
|
|
787
|
+
? selected.anchor.cellStart
|
|
788
|
+
: Math.max(selected.anchor.cellStart, selected.anchor.cellEnd - 1),
|
|
789
|
+
desiredScreenRow: selected.row + frame.startRow - targetViewportTop,
|
|
790
|
+
};
|
|
791
|
+
const fallbacks: ManualViewportAnchor[] = [];
|
|
792
|
+
for (let row = firstCandidateRow; row < lastCandidateRow; row++) {
|
|
793
|
+
const anchor = frame.anchors[row];
|
|
794
|
+
if (anchor === null || row === selected.row) continue;
|
|
795
|
+
fallbacks.push({
|
|
796
|
+
id: anchor.id,
|
|
797
|
+
graphemeIndex:
|
|
798
|
+
direction < 0 ? anchor.graphemeStart : Math.max(anchor.graphemeStart, anchor.graphemeEnd - 1),
|
|
799
|
+
cellOffset: direction < 0 ? anchor.cellStart : Math.max(anchor.cellStart, anchor.cellEnd - 1),
|
|
800
|
+
desiredScreenRow: row + frame.startRow - targetViewportTop,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
fallbacks.sort(
|
|
804
|
+
(a, b) =>
|
|
805
|
+
Math.abs(a.desiredScreenRow - this.#manualViewportAnchor!.desiredScreenRow) -
|
|
806
|
+
Math.abs(b.desiredScreenRow - this.#manualViewportAnchor!.desiredScreenRow),
|
|
807
|
+
);
|
|
808
|
+
this.#manualViewportFallbackAnchors = fallbacks;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
567
811
|
this.#manualViewportTop = targetViewportTop;
|
|
568
812
|
return this.#repaintViewportFromLines(
|
|
569
813
|
this.#previousLines,
|
|
@@ -572,6 +816,7 @@ export class TUI extends Container {
|
|
|
572
816
|
targetViewportTop,
|
|
573
817
|
null,
|
|
574
818
|
"manual viewport scroll",
|
|
819
|
+
this.#manualViewportAnchor !== null,
|
|
575
820
|
);
|
|
576
821
|
}
|
|
577
822
|
|
|
@@ -579,16 +824,22 @@ export class TUI extends Container {
|
|
|
579
824
|
if (this.#manualViewportTop === undefined) return false;
|
|
580
825
|
const height = this.terminal.rows;
|
|
581
826
|
const width = this.terminal.columns;
|
|
582
|
-
const
|
|
827
|
+
const liveLines = this.#latestRenderedLines;
|
|
828
|
+
const liveViewportTop = Math.max(0, liveLines.length - height);
|
|
583
829
|
this.#manualViewportTop = undefined;
|
|
584
|
-
|
|
585
|
-
|
|
830
|
+
this.#manualViewportAnchor = null;
|
|
831
|
+
this.#manualViewportFallbackAnchors = [];
|
|
832
|
+
this.#reconcileMissingViewportAnchor = false;
|
|
833
|
+
const repainted = this.#repaintViewportFromLines(
|
|
834
|
+
liveLines,
|
|
586
835
|
width,
|
|
587
836
|
height,
|
|
588
837
|
liveViewportTop,
|
|
589
838
|
this.#lastCursorPosition,
|
|
590
839
|
"manual viewport follow live",
|
|
591
840
|
);
|
|
841
|
+
if (repainted) this.#previousLines = liveLines;
|
|
842
|
+
return repainted;
|
|
592
843
|
}
|
|
593
844
|
|
|
594
845
|
/**
|
|
@@ -926,6 +1177,7 @@ export class TUI extends Container {
|
|
|
926
1177
|
// focus/listener state is intentionally preserved so input routing survives
|
|
927
1178
|
// a resume.
|
|
928
1179
|
this.#previousLines = [];
|
|
1180
|
+
this.#latestRenderedLines = [];
|
|
929
1181
|
this.#previousRaw = [];
|
|
930
1182
|
this.#lineNormalizationCache.clear();
|
|
931
1183
|
this.#lineTruncationCache.clear();
|
|
@@ -962,6 +1214,7 @@ export class TUI extends Container {
|
|
|
962
1214
|
// A forced full redraw supersedes any queued input-priority render.
|
|
963
1215
|
this.#inputRenderPending = false;
|
|
964
1216
|
this.#previousLines = [];
|
|
1217
|
+
this.#latestRenderedLines = [];
|
|
965
1218
|
this.#previousRaw = [];
|
|
966
1219
|
this.#lineNormalizationCache.clear();
|
|
967
1220
|
this.#lineTruncationCache.clear();
|
|
@@ -1547,7 +1800,11 @@ export class TUI extends Container {
|
|
|
1547
1800
|
return lines;
|
|
1548
1801
|
}
|
|
1549
1802
|
|
|
1550
|
-
#padBeforeBottomPinnedComponent(
|
|
1803
|
+
#padBeforeBottomPinnedComponent(
|
|
1804
|
+
lines: string[],
|
|
1805
|
+
height: number,
|
|
1806
|
+
renderedChildren: Map<Component, string[]>,
|
|
1807
|
+
): string[] {
|
|
1551
1808
|
const component = this.#bottomPinnedComponent;
|
|
1552
1809
|
if (component === null || lines.length >= height) return lines;
|
|
1553
1810
|
|
|
@@ -1562,7 +1819,7 @@ export class TUI extends Container {
|
|
|
1562
1819
|
|
|
1563
1820
|
let pinnedLineCount = 0;
|
|
1564
1821
|
for (let i = pinnedStart; i < this.children.length; i++) {
|
|
1565
|
-
pinnedLineCount +=
|
|
1822
|
+
pinnedLineCount += (renderedChildren.get(this.children[i]) ?? []).length;
|
|
1566
1823
|
}
|
|
1567
1824
|
|
|
1568
1825
|
const blankRows = height - lines.length;
|
|
@@ -1571,6 +1828,55 @@ export class TUI extends Container {
|
|
|
1571
1828
|
padded.splice(insertAt, 0, ...Array.from({ length: blankRows }, () => ""));
|
|
1572
1829
|
return padded;
|
|
1573
1830
|
}
|
|
1831
|
+
#resolveManualAnchor(frame: ViewportAnchorFrame): number | null {
|
|
1832
|
+
const anchor = this.#manualViewportAnchor;
|
|
1833
|
+
if (anchor === null) return null;
|
|
1834
|
+
const row = frame.anchors.findIndex(
|
|
1835
|
+
candidate =>
|
|
1836
|
+
candidate !== null &&
|
|
1837
|
+
candidate.id === anchor.id &&
|
|
1838
|
+
candidate.graphemeStart <= anchor.graphemeIndex &&
|
|
1839
|
+
anchor.graphemeIndex < candidate.graphemeEnd &&
|
|
1840
|
+
candidate.cellStart <= anchor.cellOffset &&
|
|
1841
|
+
anchor.cellOffset < candidate.cellEnd,
|
|
1842
|
+
);
|
|
1843
|
+
return row < 0 ? null : Math.max(0, frame.startRow + row - anchor.desiredScreenRow);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
#resolvePreparedManualAnchor(frame: ViewportAnchorFrame): number | null {
|
|
1847
|
+
const previous = this.#manualViewportAnchor;
|
|
1848
|
+
for (const fallback of this.#manualViewportFallbackAnchors) {
|
|
1849
|
+
this.#manualViewportAnchor = fallback;
|
|
1850
|
+
const resolved = this.#resolveManualAnchor(frame);
|
|
1851
|
+
if (resolved !== null) return resolved;
|
|
1852
|
+
}
|
|
1853
|
+
const targetRow = Math.max(
|
|
1854
|
+
0,
|
|
1855
|
+
Math.min(
|
|
1856
|
+
frame.anchors.length - 1,
|
|
1857
|
+
(this.#manualViewportTop ?? 0) + (previous?.desiredScreenRow ?? 0) - frame.startRow,
|
|
1858
|
+
),
|
|
1859
|
+
);
|
|
1860
|
+
let selectedRow = -1;
|
|
1861
|
+
for (let distance = 0; distance < frame.anchors.length; distance++) {
|
|
1862
|
+
for (const row of [targetRow - distance, targetRow + distance]) {
|
|
1863
|
+
if (row < 0 || row >= frame.anchors.length || frame.anchors[row] === null) continue;
|
|
1864
|
+
selectedRow = row;
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
if (selectedRow >= 0) break;
|
|
1868
|
+
}
|
|
1869
|
+
const selected = selectedRow >= 0 ? frame.anchors[selectedRow] : null;
|
|
1870
|
+
if (selected === null) return null;
|
|
1871
|
+
this.#manualViewportAnchor = {
|
|
1872
|
+
id: selected.id,
|
|
1873
|
+
graphemeIndex: selected.graphemeStart,
|
|
1874
|
+
cellOffset: selected.cellStart,
|
|
1875
|
+
desiredScreenRow: previous?.desiredScreenRow ?? 0,
|
|
1876
|
+
};
|
|
1877
|
+
return this.#resolveManualAnchor(frame);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1574
1880
|
#repaintViewportFromLines(
|
|
1575
1881
|
lines: string[],
|
|
1576
1882
|
width: number,
|
|
@@ -1578,9 +1884,10 @@ export class TUI extends Container {
|
|
|
1578
1884
|
viewportTop: number,
|
|
1579
1885
|
cursorPos: { row: number; col: number } | null,
|
|
1580
1886
|
reason: string,
|
|
1887
|
+
allowPastLiveBottom = false,
|
|
1581
1888
|
): boolean {
|
|
1582
1889
|
if (height <= 0 || width <= 0) return false;
|
|
1583
|
-
const maxViewportTop = Math.max(0, lines.length - height);
|
|
1890
|
+
const maxViewportTop = Math.max(0, lines.length - (allowPastLiveBottom ? 1 : height));
|
|
1584
1891
|
const nextViewportTop = Math.max(0, Math.min(maxViewportTop, viewportTop));
|
|
1585
1892
|
const currentScreenRow = Math.max(0, Math.min(height - 1, this.#hardwareCursorRow - this.#viewportTopRow));
|
|
1586
1893
|
let buffer = "\x1b[?2026h";
|
|
@@ -1616,7 +1923,7 @@ export class TUI extends Container {
|
|
|
1616
1923
|
this.#hardwareCursorRow = cursorToRow;
|
|
1617
1924
|
buffer += cursorSeq;
|
|
1618
1925
|
buffer += "\x1b[?2026l";
|
|
1619
|
-
if (!this.#
|
|
1926
|
+
if (!this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, lines.length)) return false;
|
|
1620
1927
|
|
|
1621
1928
|
if (this.#debugRedraw) {
|
|
1622
1929
|
const msg = `[${new Date().toISOString()}] viewportRepaint: ${reason} (lines=${lines.length}, height=${height}, viewportTop=${nextViewportTop})\n`;
|
|
@@ -1642,13 +1949,27 @@ export class TUI extends Container {
|
|
|
1642
1949
|
return targetScreenRow - currentScreenRow;
|
|
1643
1950
|
};
|
|
1644
1951
|
|
|
1645
|
-
// Render
|
|
1952
|
+
// Render direct children once so the registered transcript component retains row ownership.
|
|
1646
1953
|
const renderTreeStart = renderMetrics.now();
|
|
1647
|
-
|
|
1954
|
+
const renderedLines: string[] = [];
|
|
1955
|
+
const renderedChildren = new Map<Component, string[]>();
|
|
1956
|
+
let anchorFrame: ViewportAnchorFrame | null = null;
|
|
1957
|
+
const anchorRenderFailureCountBefore = viewportAnchorRenderFailureCount;
|
|
1958
|
+
for (const child of this.children) {
|
|
1959
|
+
const rendered = safeRenderComponentWithViewportAnchors(child, width, "tui-child");
|
|
1960
|
+
renderedChildren.set(child, rendered.lines);
|
|
1961
|
+
if (child === this.#viewportAnchorComponent && rendered.anchors.some(anchor => anchor !== null)) {
|
|
1962
|
+
anchorFrame = { startRow: renderedLines.length, anchors: rendered.anchors };
|
|
1963
|
+
}
|
|
1964
|
+
for (const line of rendered.lines) renderedLines.push(line);
|
|
1965
|
+
}
|
|
1966
|
+
const anchorRenderFailed = viewportAnchorRenderFailureCount !== anchorRenderFailureCountBefore;
|
|
1967
|
+
let newLines = renderedLines;
|
|
1968
|
+
this.#viewportAnchorFrame = anchorFrame;
|
|
1648
1969
|
if (renderMetrics.enabled) renderMetrics.recordHelper("renderTree", renderMetrics.now() - renderTreeStart);
|
|
1649
1970
|
|
|
1650
1971
|
if (this.#bottomPinnedComponent !== null && height > 0) {
|
|
1651
|
-
newLines = this.#padBeforeBottomPinnedComponent(newLines, height);
|
|
1972
|
+
newLines = this.#padBeforeBottomPinnedComponent(newLines, height, renderedChildren);
|
|
1652
1973
|
}
|
|
1653
1974
|
|
|
1654
1975
|
// Composite overlays into the rendered lines (before differential compare)
|
|
@@ -1719,20 +2040,78 @@ export class TUI extends Container {
|
|
|
1719
2040
|
renderMetrics.recordLineCount("measured", total - diffStart);
|
|
1720
2041
|
if (usedWindowNormalize) renderMetrics.recordLineCount("offscreenScan", diffStart);
|
|
1721
2042
|
}
|
|
2043
|
+
this.#latestRenderedLines = newLines;
|
|
1722
2044
|
|
|
1723
2045
|
if (this.#manualViewportTop !== undefined) {
|
|
1724
|
-
|
|
1725
|
-
|
|
2046
|
+
let resolvedAnchorTop = anchorFrame === null ? null : this.#resolveManualAnchor(anchorFrame);
|
|
2047
|
+
if (
|
|
2048
|
+
this.#manualViewportAnchor !== null &&
|
|
2049
|
+
resolvedAnchorTop === null &&
|
|
2050
|
+
this.#reconcileMissingViewportAnchor
|
|
2051
|
+
) {
|
|
2052
|
+
resolvedAnchorTop = anchorFrame === null ? null : this.#resolvePreparedManualAnchor(anchorFrame);
|
|
2053
|
+
this.#reconcileMissingViewportAnchor = false;
|
|
2054
|
+
if (resolvedAnchorTop === null) {
|
|
2055
|
+
this.#manualViewportAnchor = null;
|
|
2056
|
+
this.#manualViewportFallbackAnchors = [];
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (this.#manualViewportAnchor !== null && resolvedAnchorTop === null) {
|
|
2060
|
+
if (anchorRenderFailed) {
|
|
2061
|
+
// Keep semantic intent armed for recovery, but render the diagnostic frame
|
|
2062
|
+
// instead of masking a provider failure behind stale transcript content.
|
|
2063
|
+
this.#repaintViewportFromLines(
|
|
2064
|
+
newLines,
|
|
2065
|
+
width,
|
|
2066
|
+
height,
|
|
2067
|
+
this.#manualViewportTop,
|
|
2068
|
+
null,
|
|
2069
|
+
"failed semantic viewport render",
|
|
2070
|
+
true,
|
|
2071
|
+
);
|
|
2072
|
+
this.#previousLines = newLines;
|
|
2073
|
+
this.#previousWidth = width;
|
|
2074
|
+
this.#previousHeight = height;
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
// A formerly valid semantic target is temporarily absent (provider removal,
|
|
2078
|
+
// replacement, eviction, or object deletion). Keep the last resolved frame
|
|
2079
|
+
// instead of silently reinterpreting manual intent as a numeric viewport.
|
|
2080
|
+
const retainedLines = this.#previousLines.length > 0 ? this.#previousLines : newLines;
|
|
2081
|
+
this.#repaintViewportFromLines(
|
|
2082
|
+
retainedLines,
|
|
2083
|
+
width,
|
|
2084
|
+
height,
|
|
2085
|
+
this.#manualViewportTop,
|
|
2086
|
+
null,
|
|
2087
|
+
"unresolved semantic viewport render",
|
|
2088
|
+
true,
|
|
2089
|
+
);
|
|
2090
|
+
this.#previousWidth = width;
|
|
2091
|
+
this.#previousHeight = height;
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const nextViewportTop = resolvedAnchorTop ?? this.#manualViewportTop;
|
|
2095
|
+
if (
|
|
2096
|
+
this.#previousWidth === width &&
|
|
2097
|
+
this.#previousHeight === height &&
|
|
2098
|
+
nextViewportTop === this.#manualViewportTop &&
|
|
2099
|
+
newLines.length === this.#previousLines.length &&
|
|
2100
|
+
newLines.every((line, index) => line === this.#previousLines[index])
|
|
2101
|
+
) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
1726
2104
|
this.#manualViewportTop = nextViewportTop;
|
|
1727
|
-
|
|
2105
|
+
this.#reconcileMissingViewportAnchor = false;
|
|
1728
2106
|
if (
|
|
1729
2107
|
this.#repaintViewportFromLines(
|
|
1730
2108
|
newLines,
|
|
1731
2109
|
width,
|
|
1732
2110
|
height,
|
|
1733
2111
|
nextViewportTop,
|
|
1734
|
-
|
|
2112
|
+
null,
|
|
1735
2113
|
"manual viewport render",
|
|
2114
|
+
this.#manualViewportAnchor !== null,
|
|
1736
2115
|
)
|
|
1737
2116
|
) {
|
|
1738
2117
|
this.#previousLines = newLines;
|
|
@@ -1761,7 +2140,7 @@ export class TUI extends Container {
|
|
|
1761
2140
|
this.#hardwareCursorRow = toRow;
|
|
1762
2141
|
buffer += seq;
|
|
1763
2142
|
buffer += "\x1b[?2026l"; // End synchronized output
|
|
1764
|
-
if (!this.#
|
|
2143
|
+
if (!this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, newLines.length)) return;
|
|
1765
2144
|
// Reset max lines when clearing, otherwise track growth
|
|
1766
2145
|
if (clear) {
|
|
1767
2146
|
this.#maxLinesRendered = newLines.length;
|
|
@@ -1811,7 +2190,7 @@ export class TUI extends Container {
|
|
|
1811
2190
|
this.#hardwareCursorRow = cursorToRow;
|
|
1812
2191
|
buffer += cursorSeq;
|
|
1813
2192
|
buffer += "\x1b[?2026l";
|
|
1814
|
-
if (!this.#
|
|
2193
|
+
if (!this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, newLines.length)) return;
|
|
1815
2194
|
|
|
1816
2195
|
if (this.#debugRedraw) {
|
|
1817
2196
|
const msg = `[${new Date().toISOString()}] viewportRepaint: ${reason} (prev=${this.#previousLines.length}, new=${newLines.length}, height=${height}, viewportTop=${nextViewportTop})\n`;
|
|
@@ -1878,7 +2257,11 @@ export class TUI extends Container {
|
|
|
1878
2257
|
// Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
|
|
1879
2258
|
if (this.#clearOnShrink && newLines.length < this.#previousLines.length && this.overlayStack.length === 0) {
|
|
1880
2259
|
logRedraw(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
|
|
1881
|
-
if (
|
|
2260
|
+
if (
|
|
2261
|
+
useViewportRepaintPath(this.terminal) ||
|
|
2262
|
+
((this.#previousLines.length > height || newLines.length > height) &&
|
|
2263
|
+
allowsHostNeutralOverflowRepaint(this.terminal))
|
|
2264
|
+
) {
|
|
1882
2265
|
viewportRepaint(`clearOnShrink (prev=${this.#previousLines.length}, new=${newLines.length})`);
|
|
1883
2266
|
} else {
|
|
1884
2267
|
fullRender(true, "clearOnShrink");
|
|
@@ -1963,7 +2346,7 @@ export class TUI extends Container {
|
|
|
1963
2346
|
this.#hardwareCursorRow = toRow;
|
|
1964
2347
|
buffer += seq;
|
|
1965
2348
|
buffer += "\x1b[?2026l";
|
|
1966
|
-
if (!this.#
|
|
2349
|
+
if (!this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, newLines.length)) return;
|
|
1967
2350
|
}
|
|
1968
2351
|
this.#previousLines = newLines;
|
|
1969
2352
|
this.#previousWidth = width;
|
|
@@ -1981,7 +2364,12 @@ export class TUI extends Container {
|
|
|
1981
2364
|
// back to live.
|
|
1982
2365
|
if (firstChanged < prevViewportTop) {
|
|
1983
2366
|
logRedraw(`firstChanged < viewportTop (${firstChanged} < ${prevViewportTop})`);
|
|
1984
|
-
if (
|
|
2367
|
+
if (
|
|
2368
|
+
useViewportRepaintPath(this.terminal) ||
|
|
2369
|
+
(newLines.length <= this.#previousLines.length &&
|
|
2370
|
+
(this.#previousLines.length > height || newLines.length > height) &&
|
|
2371
|
+
allowsHostNeutralOverflowRepaint(this.terminal))
|
|
2372
|
+
) {
|
|
1985
2373
|
viewportRepaint(`firstChanged < viewportTop (${firstChanged} < ${prevViewportTop})`);
|
|
1986
2374
|
return;
|
|
1987
2375
|
}
|
|
@@ -2108,7 +2496,7 @@ export class TUI extends Container {
|
|
|
2108
2496
|
}
|
|
2109
2497
|
|
|
2110
2498
|
// Write entire buffer at once
|
|
2111
|
-
if (!this.#
|
|
2499
|
+
if (!this.#writeRenderBufferAndReanchorImeCursor(buffer, cursorPos, newLines.length)) return;
|
|
2112
2500
|
|
|
2113
2501
|
// Track cursor position for next render.
|
|
2114
2502
|
// cursorRow tracks end of content (for viewport calculation).
|
|
@@ -2163,19 +2551,47 @@ export class TUI extends Container {
|
|
|
2163
2551
|
return { seq, toRow: targetRow };
|
|
2164
2552
|
}
|
|
2165
2553
|
|
|
2554
|
+
/**
|
|
2555
|
+
* Register an emitter whose escape payload is appended to every render
|
|
2556
|
+
* write (inside its own synchronized-output block, cursor saved/restored).
|
|
2557
|
+
* Used for absolute-positioned overlays such as pixel-image pets that live
|
|
2558
|
+
* outside the line-based component model. Return null to emit nothing.
|
|
2559
|
+
*/
|
|
2560
|
+
setPostRenderEmitter(emitter: (() => string | null) | undefined): void {
|
|
2561
|
+
this.#postRenderEmitter = emitter;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
#postRenderEmitter: (() => string | null) | undefined;
|
|
2565
|
+
|
|
2566
|
+
#writeRenderBufferAndReanchorImeCursor(
|
|
2567
|
+
buffer: string,
|
|
2568
|
+
cursorPos: { row: number; col: number } | null,
|
|
2569
|
+
totalLines: number,
|
|
2570
|
+
): boolean {
|
|
2571
|
+
const overlay = this.#postRenderEmitter?.();
|
|
2572
|
+
if (overlay) {
|
|
2573
|
+
// DECSC/DECRC keep the hardware cursor stable; the dedicated
|
|
2574
|
+
// synchronized block prevents visible tearing while the overlay
|
|
2575
|
+
// area is cleared and redrawn.
|
|
2576
|
+
buffer += `\x1b[?2026h\x1b7${overlay}\x1b8\x1b[?2026l`;
|
|
2577
|
+
}
|
|
2578
|
+
if (!this.#writeTerminal(buffer)) return false;
|
|
2579
|
+
if (!this.#imeCursorActive) return true;
|
|
2580
|
+
return this.#writeCursorPosition(cursorPos, totalLines);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2166
2583
|
/**
|
|
2167
2584
|
* Write the hardware cursor position to the terminal as a standalone
|
|
2168
2585
|
* synchronized output block. Use when there is no surrounding render buffer
|
|
2169
2586
|
* to embed the sequences into.
|
|
2170
2587
|
*/
|
|
2171
|
-
#writeCursorPosition(cursorPos: { row: number; col: number } | null, totalLines: number):
|
|
2588
|
+
#writeCursorPosition(cursorPos: { row: number; col: number } | null, totalLines: number): boolean {
|
|
2172
2589
|
if (!cursorPos || totalLines <= 0) {
|
|
2173
|
-
this.#hideCursor();
|
|
2174
|
-
return;
|
|
2590
|
+
return this.#hideCursor();
|
|
2175
2591
|
}
|
|
2176
2592
|
const { seq, toRow } = this.#cursorControlSequence(cursorPos, totalLines, this.#hardwareCursorRow);
|
|
2177
2593
|
this.#hardwareCursorRow = toRow;
|
|
2178
2594
|
// No \x1b[?2026h/l wrapper: synchronized output flushes terminal state and discards macOS IME composition.
|
|
2179
|
-
this.#writeTerminal(seq);
|
|
2595
|
+
return this.#writeTerminal(seq);
|
|
2180
2596
|
}
|
|
2181
2597
|
}
|