@sayknow-cli/tui 0.3.16 → 0.4.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.md +18 -1
- package/package.json +6 -7
- package/src/autocomplete.ts +31 -1
- package/src/bracketed-paste.ts +106 -29
- package/src/components/editor.ts +81 -37
- package/src/components/input.ts +5 -1
- package/src/components/secret-input.ts +489 -0
- package/src/components/select-list.ts +138 -33
- package/src/index.ts +1 -0
- package/src/keybindings.ts +11 -3
- package/src/stdin-buffer.ts +153 -20
- package/src/terminal.ts +38 -2
- package/src/tui.ts +184 -18
- 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 -18
- 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 -87
- package/dist/types/components/sayknow-pet.d.ts +0 -128
- 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 -22
- 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 -28
- 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 -187
- package/dist/types/terminal.d.ts +0 -90
- package/dist/types/ttyid.d.ts +0 -9
- package/dist/types/tui.d.ts +0 -269
- package/dist/types/utils.d.ts +0 -110
package/src/tui.ts
CHANGED
|
@@ -45,6 +45,44 @@ type InputListener = (data: string) => InputListenerResult;
|
|
|
45
45
|
/**
|
|
46
46
|
* Component interface - all components must implement this
|
|
47
47
|
*/
|
|
48
|
+
export type MouseEvent = {
|
|
49
|
+
kind: "wheel" | "click";
|
|
50
|
+
direction?: -1 | 1;
|
|
51
|
+
button?: 0;
|
|
52
|
+
/** Terminal cell coordinates, one-based. */
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
/** Focused-overlay cell coordinates, one-based when dispatched to an overlay. */
|
|
56
|
+
localX?: number;
|
|
57
|
+
localY?: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type OverlayMouseBounds = {
|
|
61
|
+
row: number;
|
|
62
|
+
col: number;
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
termWidth: number;
|
|
66
|
+
termHeight: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Parse xterm SGR mouse reports. Drag and button-release reports are ignored. */
|
|
70
|
+
export function parseSgrMouseEvent(data: string): MouseEvent | undefined {
|
|
71
|
+
const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
|
|
72
|
+
if (!match) return undefined;
|
|
73
|
+
const button = Number(match[1]);
|
|
74
|
+
const x = Number(match[2]);
|
|
75
|
+
const y = Number(match[3]);
|
|
76
|
+
const terminator = match[4];
|
|
77
|
+
if (![button, x, y].every(Number.isSafeInteger) || x < 1 || y < 1) return undefined;
|
|
78
|
+
|
|
79
|
+
if (button & 32 || terminator === "m") return undefined;
|
|
80
|
+
if (button === 64) return { kind: "wheel", direction: -1, x, y };
|
|
81
|
+
if (button === 65) return { kind: "wheel", direction: 1, x, y };
|
|
82
|
+
if (button === 0) return { kind: "click", button, x, y };
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
export interface Component {
|
|
49
87
|
/**
|
|
50
88
|
* Render the component to lines for the given viewport width
|
|
@@ -58,6 +96,9 @@ export interface Component {
|
|
|
58
96
|
*/
|
|
59
97
|
handleInput?(data: string): void;
|
|
60
98
|
|
|
99
|
+
/** Optional handler for terminal mouse events when component has focus. */
|
|
100
|
+
handleMouse?(event: MouseEvent): void;
|
|
101
|
+
|
|
61
102
|
/**
|
|
62
103
|
* If true, component receives key release events (Kitty protocol).
|
|
63
104
|
* Default is false - release events are filtered out.
|
|
@@ -279,31 +320,41 @@ function isViewportSensitiveHost(
|
|
|
279
320
|
env: Record<string, string | undefined>,
|
|
280
321
|
platform: NodeJS.Platform,
|
|
281
322
|
includeNativeWindows: boolean,
|
|
323
|
+
includeProcessTerminal: boolean,
|
|
282
324
|
): boolean {
|
|
283
|
-
return
|
|
325
|
+
return (
|
|
326
|
+
isMultiplexerSession(env) ||
|
|
327
|
+
isWindowsTerminalSession(env) ||
|
|
328
|
+
includeProcessTerminal ||
|
|
329
|
+
(includeNativeWindows && platform === "win32")
|
|
330
|
+
);
|
|
284
331
|
}
|
|
285
332
|
/**
|
|
286
333
|
* True when repainting only the live viewport is safer than clearing/replaying
|
|
287
|
-
* the full transcript.
|
|
288
|
-
*
|
|
289
|
-
*
|
|
334
|
+
* the full transcript. Real process terminals are viewport-sensitive because
|
|
335
|
+
* their native scrollback position is not observable by the renderer. Native
|
|
336
|
+
* Windows console hosts are also recognized from platform identity when that
|
|
337
|
+
* process-terminal capability is unavailable.
|
|
290
338
|
*/
|
|
291
339
|
export function shouldUseViewportRepaintForHost(
|
|
292
340
|
env: Record<string, string | undefined> = Bun.env,
|
|
293
341
|
platform: NodeJS.Platform = process.platform,
|
|
294
|
-
options: { includeNativeWindows?: boolean } = {},
|
|
342
|
+
options: { includeNativeWindows?: boolean; includeProcessTerminal?: boolean } = {},
|
|
295
343
|
): boolean {
|
|
296
344
|
const multiplexed = isMultiplexerSession(env);
|
|
297
345
|
const includeNativeWindows = options.includeNativeWindows ?? true;
|
|
346
|
+
const includeProcessTerminal = options.includeProcessTerminal ?? false;
|
|
298
347
|
return (
|
|
299
|
-
isViewportSensitiveHost(env, platform, includeNativeWindows) &&
|
|
348
|
+
isViewportSensitiveHost(env, platform, includeNativeWindows, includeProcessTerminal) &&
|
|
300
349
|
!(multiplexed && useLegacyMultiplexerFullRender(env))
|
|
301
350
|
);
|
|
302
351
|
}
|
|
303
352
|
|
|
304
353
|
function useViewportRepaintPath(terminal: Terminal): boolean {
|
|
354
|
+
if (terminal.isProcessTerminal !== true) return false;
|
|
305
355
|
return shouldUseViewportRepaintForHost(Bun.env, process.platform, {
|
|
306
|
-
includeNativeWindows:
|
|
356
|
+
includeNativeWindows: true,
|
|
357
|
+
includeProcessTerminal: true,
|
|
307
358
|
});
|
|
308
359
|
}
|
|
309
360
|
|
|
@@ -319,7 +370,8 @@ function allowsHostNeutralOverflowRepaint(
|
|
|
319
370
|
}
|
|
320
371
|
|
|
321
372
|
function shouldPreserveScrollbackOnFullClear(terminal: Terminal): boolean {
|
|
322
|
-
|
|
373
|
+
if (terminal.isProcessTerminal !== true) return false;
|
|
374
|
+
return isViewportSensitiveHost(Bun.env, process.platform, true, true);
|
|
323
375
|
}
|
|
324
376
|
|
|
325
377
|
/**
|
|
@@ -618,6 +670,7 @@ export class TUI extends Container {
|
|
|
618
670
|
#stopped = false;
|
|
619
671
|
#terminalUnavailable = false;
|
|
620
672
|
#bottomPinnedComponent: Component | null = null;
|
|
673
|
+
#pendingTerminalCleanup: Array<{ payload: string; onDelivered?: () => void }> = [];
|
|
621
674
|
|
|
622
675
|
#unsubscribeTabWidthChange?: () => void;
|
|
623
676
|
static #renderCounters: TuiRenderCounterSnapshot = {
|
|
@@ -661,9 +714,14 @@ export class TUI extends Container {
|
|
|
661
714
|
options?: OverlayOptions;
|
|
662
715
|
preFocus: Component | null;
|
|
663
716
|
hidden: boolean;
|
|
717
|
+
mouseBounds?: OverlayMouseBounds;
|
|
664
718
|
}[] = [];
|
|
665
719
|
|
|
666
|
-
constructor(
|
|
720
|
+
constructor(
|
|
721
|
+
terminal: Terminal,
|
|
722
|
+
showHardwareCursor?: boolean,
|
|
723
|
+
private readonly options: { enableMouse?: boolean } = {},
|
|
724
|
+
) {
|
|
667
725
|
super();
|
|
668
726
|
this.terminal = terminal;
|
|
669
727
|
if (showHardwareCursor !== undefined) {
|
|
@@ -758,6 +816,50 @@ export class TUI extends Container {
|
|
|
758
816
|
if (this.#manualViewportAnchor !== null) this.#reconcileMissingViewportAnchor = true;
|
|
759
817
|
}
|
|
760
818
|
|
|
819
|
+
/** Reveal a semantic viewport anchor without changing the rendered content width. */
|
|
820
|
+
revealViewportAnchor(id: ViewportAnchorId, alignment: "top" | "center" | "bottom"): boolean {
|
|
821
|
+
const height = this.terminal.rows;
|
|
822
|
+
const width = this.terminal.columns;
|
|
823
|
+
const frame = this.#viewportAnchorFrame;
|
|
824
|
+
if (height <= 0 || width <= 0 || this.#previousLines.length === 0 || frame === null) return false;
|
|
825
|
+
|
|
826
|
+
const selectedRow = frame.anchors.findIndex(anchor => anchor?.id === id);
|
|
827
|
+
const selected = selectedRow < 0 ? null : frame.anchors[selectedRow];
|
|
828
|
+
if (selected === null) return false;
|
|
829
|
+
|
|
830
|
+
const desiredScreenRow = alignment === "top" ? 0 : alignment === "center" ? Math.floor(height / 2) : height - 1;
|
|
831
|
+
const targetViewportTop = Math.max(0, frame.startRow + selectedRow - desiredScreenRow);
|
|
832
|
+
this.#manualViewportAnchor = {
|
|
833
|
+
id: selected.id,
|
|
834
|
+
graphemeIndex: selected.graphemeStart,
|
|
835
|
+
cellOffset: selected.cellStart,
|
|
836
|
+
desiredScreenRow,
|
|
837
|
+
};
|
|
838
|
+
const firstCandidateRow = Math.max(0, targetViewportTop - frame.startRow);
|
|
839
|
+
const lastCandidateRow = Math.min(frame.anchors.length, targetViewportTop + height - frame.startRow);
|
|
840
|
+
const fallbacks: ManualViewportAnchor[] = [];
|
|
841
|
+
for (let row = firstCandidateRow; row < lastCandidateRow; row++) {
|
|
842
|
+
const anchor = frame.anchors[row];
|
|
843
|
+
if (anchor === null || row === selectedRow) continue;
|
|
844
|
+
fallbacks.push({
|
|
845
|
+
id: anchor.id,
|
|
846
|
+
graphemeIndex: anchor.graphemeStart,
|
|
847
|
+
cellOffset: anchor.cellStart,
|
|
848
|
+
desiredScreenRow: row + frame.startRow - targetViewportTop,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
fallbacks.sort(
|
|
852
|
+
(a, b) =>
|
|
853
|
+
Math.abs(a.desiredScreenRow - this.#manualViewportAnchor!.desiredScreenRow) -
|
|
854
|
+
Math.abs(b.desiredScreenRow - this.#manualViewportAnchor!.desiredScreenRow),
|
|
855
|
+
);
|
|
856
|
+
this.#manualViewportFallbackAnchors = fallbacks;
|
|
857
|
+
this.#manualViewportTop = this.#viewportTopRow;
|
|
858
|
+
this.#reconcileMissingViewportAnchor = false;
|
|
859
|
+
this.requestRender();
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
|
|
761
863
|
scrollViewportPages(direction: -1 | 1): boolean {
|
|
762
864
|
const height = this.terminal.rows;
|
|
763
865
|
const width = this.terminal.columns;
|
|
@@ -866,7 +968,8 @@ export class TUI extends Container {
|
|
|
866
968
|
* Returns a handle to control the overlay's visibility.
|
|
867
969
|
*/
|
|
868
970
|
showOverlay(component: Component, options?: OverlayOptions): OverlayHandle {
|
|
869
|
-
const entry = { component, options, preFocus: this.#focusedComponent, hidden: false };
|
|
971
|
+
const entry = { component, options, preFocus: this.#focusedComponent, hidden: false, mouseBounds: undefined };
|
|
972
|
+
|
|
870
973
|
this.overlayStack.push(entry);
|
|
871
974
|
// Only focus if overlay is actually visible
|
|
872
975
|
if (this.#isOverlayVisible(entry)) {
|
|
@@ -880,6 +983,8 @@ export class TUI extends Container {
|
|
|
880
983
|
hide: () => {
|
|
881
984
|
const index = this.overlayStack.indexOf(entry);
|
|
882
985
|
if (index !== -1) {
|
|
986
|
+
entry.mouseBounds = undefined;
|
|
987
|
+
|
|
883
988
|
this.overlayStack.splice(index, 1);
|
|
884
989
|
// Restore focus if this overlay had focus
|
|
885
990
|
if (this.#focusedComponent === component) {
|
|
@@ -893,6 +998,8 @@ export class TUI extends Container {
|
|
|
893
998
|
setHidden: (hidden: boolean) => {
|
|
894
999
|
if (entry.hidden === hidden) return;
|
|
895
1000
|
entry.hidden = hidden;
|
|
1001
|
+
entry.mouseBounds = undefined;
|
|
1002
|
+
|
|
896
1003
|
// Update focus when hiding/showing
|
|
897
1004
|
if (hidden) {
|
|
898
1005
|
// If this overlay had focus, move focus to next visible or preFocus
|
|
@@ -916,6 +1023,7 @@ export class TUI extends Container {
|
|
|
916
1023
|
hideOverlay(): void {
|
|
917
1024
|
const overlay = this.overlayStack.pop();
|
|
918
1025
|
if (!overlay) return;
|
|
1026
|
+
overlay.mouseBounds = undefined;
|
|
919
1027
|
// Find topmost visible overlay, or fall back to preFocus
|
|
920
1028
|
const topVisible = this.#getTopmostVisibleOverlay();
|
|
921
1029
|
this.setFocus(topVisible?.component ?? overlay.preFocus);
|
|
@@ -950,11 +1058,13 @@ export class TUI extends Container {
|
|
|
950
1058
|
override invalidate(): void {
|
|
951
1059
|
super.invalidate();
|
|
952
1060
|
for (const overlay of this.overlayStack) overlay.component.invalidate?.();
|
|
1061
|
+
for (const overlay of this.overlayStack) overlay.mouseBounds = undefined;
|
|
953
1062
|
}
|
|
954
1063
|
|
|
955
1064
|
start(): void {
|
|
956
1065
|
this.#stopped = false;
|
|
957
1066
|
this.#terminalUnavailable = false;
|
|
1067
|
+
this.terminal.setMouseEnabled?.(this.options.enableMouse === true);
|
|
958
1068
|
this.terminal.start(
|
|
959
1069
|
data => this.#handleInput(data),
|
|
960
1070
|
() => {
|
|
@@ -962,6 +1072,7 @@ export class TUI extends Container {
|
|
|
962
1072
|
this.requestResizeRender();
|
|
963
1073
|
},
|
|
964
1074
|
);
|
|
1075
|
+
this.flushTerminalCleanup();
|
|
965
1076
|
this.#hideCursor();
|
|
966
1077
|
this.#querySixelSupport();
|
|
967
1078
|
this.#queryCellSize();
|
|
@@ -1171,6 +1282,7 @@ export class TUI extends Container {
|
|
|
1171
1282
|
}
|
|
1172
1283
|
|
|
1173
1284
|
stop(): void {
|
|
1285
|
+
this.flushTerminalCleanup();
|
|
1174
1286
|
this.#clearSixelProbeState();
|
|
1175
1287
|
this.#stopped = true;
|
|
1176
1288
|
if (this.#renderTimer) {
|
|
@@ -1229,7 +1341,7 @@ export class TUI extends Container {
|
|
|
1229
1341
|
* `PI_TUI_LEGACY_MULTIPLEXER_FULL_RENDER=1` to restore the legacy tmux redraw.
|
|
1230
1342
|
*/
|
|
1231
1343
|
requestResizeRender(): void {
|
|
1232
|
-
this.requestRender(!useViewportRepaintPath(this.terminal)
|
|
1344
|
+
this.requestRender(!useViewportRepaintPath(this.terminal), "resize");
|
|
1233
1345
|
}
|
|
1234
1346
|
|
|
1235
1347
|
requestRender(force = false, source = "unknown"): void {
|
|
@@ -1357,6 +1469,44 @@ export class TUI extends Container {
|
|
|
1357
1469
|
data = current;
|
|
1358
1470
|
}
|
|
1359
1471
|
|
|
1472
|
+
const mouse = parseSgrMouseEvent(data);
|
|
1473
|
+
if (mouse) {
|
|
1474
|
+
// Coordinates outside the current terminal cannot name a visible cell.
|
|
1475
|
+
if (mouse.x > this.terminal.columns || mouse.y > this.terminal.rows) return;
|
|
1476
|
+
if (mouse.kind === "wheel") this.scrollViewportPages(mouse.direction!);
|
|
1477
|
+
else {
|
|
1478
|
+
const focusedOverlay = this.overlayStack.find(o => o.component === this.#focusedComponent);
|
|
1479
|
+
if (focusedOverlay) {
|
|
1480
|
+
if (!this.#isOverlayVisible(focusedOverlay)) {
|
|
1481
|
+
focusedOverlay.mouseBounds = undefined;
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const bounds = focusedOverlay.mouseBounds;
|
|
1485
|
+
if (bounds?.termWidth !== this.terminal.columns || bounds.termHeight !== this.terminal.rows) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (
|
|
1490
|
+
!bounds ||
|
|
1491
|
+
mouse.x < bounds.col + 1 ||
|
|
1492
|
+
mouse.x > bounds.col + bounds.width ||
|
|
1493
|
+
mouse.y < bounds.row + 1 ||
|
|
1494
|
+
mouse.y > bounds.row + bounds.height
|
|
1495
|
+
)
|
|
1496
|
+
return;
|
|
1497
|
+
this.#focusedComponent?.handleMouse?.({
|
|
1498
|
+
...mouse,
|
|
1499
|
+
localX: mouse.x - bounds.col,
|
|
1500
|
+
localY: mouse.y - bounds.row,
|
|
1501
|
+
});
|
|
1502
|
+
} else this.#focusedComponent?.handleMouse?.(mouse);
|
|
1503
|
+
}
|
|
1504
|
+
this.requestRender(false, "mouse");
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
// SGR-looking reports, including malformed reports, are terminal controls.
|
|
1508
|
+
if (data.startsWith("\x1b[<")) return;
|
|
1509
|
+
|
|
1360
1510
|
// Consume terminal cell size responses without blocking unrelated input.
|
|
1361
1511
|
if (this.#consumeCellSizeResponse(data)) {
|
|
1362
1512
|
return;
|
|
@@ -1556,6 +1706,7 @@ export class TUI extends Container {
|
|
|
1556
1706
|
#compositeOverlays(lines: string[], termWidth: number, termHeight: number): string[] {
|
|
1557
1707
|
if (this.overlayStack.length === 0) return lines;
|
|
1558
1708
|
const result = [...lines];
|
|
1709
|
+
for (const entry of this.overlayStack) entry.mouseBounds = undefined;
|
|
1559
1710
|
|
|
1560
1711
|
// Pre-render all visible overlays and calculate positions
|
|
1561
1712
|
const rendered: { overlayLines: string[]; row: number; col: number; w: number }[] = [];
|
|
@@ -1583,6 +1734,7 @@ export class TUI extends Container {
|
|
|
1583
1734
|
const { row, col } = this.#resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight);
|
|
1584
1735
|
|
|
1585
1736
|
rendered.push({ overlayLines, row, col, w: width });
|
|
1737
|
+
entry.mouseBounds = { row, col, width, height: overlayLines.length, termWidth, termHeight };
|
|
1586
1738
|
minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length);
|
|
1587
1739
|
}
|
|
1588
1740
|
|
|
@@ -2292,11 +2444,9 @@ export class TUI extends Container {
|
|
|
2292
2444
|
viewportRepaint(`terminal height changed (${this.#previousHeight} -> ${height})`);
|
|
2293
2445
|
return;
|
|
2294
2446
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
return;
|
|
2299
|
-
}
|
|
2447
|
+
logRedraw(`terminal height changed (${this.#previousHeight} -> ${height})`);
|
|
2448
|
+
fullRender(true, "terminal height changed");
|
|
2449
|
+
return;
|
|
2300
2450
|
}
|
|
2301
2451
|
|
|
2302
2452
|
// Content shrunk below the previous render and no overlays - re-render to clear empty rows
|
|
@@ -2351,8 +2501,8 @@ export class TUI extends Container {
|
|
|
2351
2501
|
}
|
|
2352
2502
|
|
|
2353
2503
|
const nextLiveViewportTop = Math.max(0, newLines.length - height);
|
|
2354
|
-
if (
|
|
2355
|
-
viewportRepaint(`
|
|
2504
|
+
if (newLines.length < this.#previousLines.length && nextLiveViewportTop !== prevViewportTop) {
|
|
2505
|
+
viewportRepaint(`content contraction changed viewport top (${prevViewportTop} -> ${nextLiveViewportTop})`);
|
|
2356
2506
|
return;
|
|
2357
2507
|
}
|
|
2358
2508
|
// All changes are in deleted lines (nothing to render, just clear)
|
|
@@ -2598,6 +2748,22 @@ export class TUI extends Container {
|
|
|
2598
2748
|
return { seq, toRow: targetRow };
|
|
2599
2749
|
}
|
|
2600
2750
|
|
|
2751
|
+
/** Retain terminal cleanup until a write succeeds, even after its component is disposed. */
|
|
2752
|
+
queueTerminalCleanup(payload: string, onDelivered?: () => void): void {
|
|
2753
|
+
this.#pendingTerminalCleanup.push({ payload, onDelivered });
|
|
2754
|
+
this.flushTerminalCleanup();
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/** Retry queued terminal cleanup after terminal recovery or before shutdown. */
|
|
2758
|
+
flushTerminalCleanup(): void {
|
|
2759
|
+
while (this.#pendingTerminalCleanup.length > 0) {
|
|
2760
|
+
const pending = this.#pendingTerminalCleanup[0];
|
|
2761
|
+
if (!this.#writeTerminal(pending.payload)) return;
|
|
2762
|
+
this.#pendingTerminalCleanup.shift();
|
|
2763
|
+
pending.onDelivered?.();
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2601
2767
|
/**
|
|
2602
2768
|
* Register an emitter whose escape payload is appended to every render
|
|
2603
2769
|
* write (inside its own synchronized-output block, cursor saved/restored).
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export type AnimationCadence = 16 | 80;
|
|
2
|
-
type AnimationCallback = (now: number) => void;
|
|
3
|
-
export interface AnimationRegistration {
|
|
4
|
-
unregister(): void;
|
|
5
|
-
}
|
|
6
|
-
export declare function registerAnimationCallback(callback: AnimationCallback, cadence?: AnimationCadence): AnimationRegistration;
|
|
7
|
-
export declare const __animationSchedulerTestHooks: {
|
|
8
|
-
getActiveTimerCount(cadence?: AnimationCadence): number;
|
|
9
|
-
getRegistrantCount(cadence?: AnimationCadence): number;
|
|
10
|
-
getStartedTimerCount(cadence?: AnimationCadence): number;
|
|
11
|
-
reset(): void;
|
|
12
|
-
};
|
|
13
|
-
export {};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
|
|
2
|
-
export declare function extractSlashCommandTokenPrefix(text: string): string | null;
|
|
3
|
-
export interface AutocompleteItem {
|
|
4
|
-
value: string;
|
|
5
|
-
label: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
/** Dim hint text shown inline after cursor when this item is selected */
|
|
8
|
-
hint?: string;
|
|
9
|
-
}
|
|
10
|
-
type Awaitable<T> = T | Promise<T>;
|
|
11
|
-
export interface SlashCommand {
|
|
12
|
-
name: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
argumentHint?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Higher values surface first in autocomplete, ahead of fuzzy-score ordering.
|
|
17
|
-
* Use this to pin first-class commands (e.g. bundled SKC skills) to the top.
|
|
18
|
-
*/
|
|
19
|
-
priority?: number;
|
|
20
|
-
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
|
21
|
-
/** Return inline hint text for the current argument state (shown as dim ghost text after cursor) */
|
|
22
|
-
getInlineHint?(argumentText: string): string | null;
|
|
23
|
-
}
|
|
24
|
-
export interface AutocompleteProvider {
|
|
25
|
-
/** Get autocomplete suggestions for current text/cursor position */
|
|
26
|
-
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
27
|
-
items: AutocompleteItem[];
|
|
28
|
-
prefix: string;
|
|
29
|
-
} | null>;
|
|
30
|
-
/** Apply the selected item and return new text + cursor position */
|
|
31
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
32
|
-
lines: string[];
|
|
33
|
-
cursorLine: number;
|
|
34
|
-
cursorCol: number;
|
|
35
|
-
onApplied?: () => void;
|
|
36
|
-
};
|
|
37
|
-
/** Get inline hint text to show as dim ghost text after the cursor */
|
|
38
|
-
getInlineHint?(lines: string[], cursorLine: number, cursorCol: number): string | null;
|
|
39
|
-
/** Synchronously try to complete a slash command at the start of a line (no async I/O). */
|
|
40
|
-
/** Returns matched items and the full prefix, or null if not applicable. */
|
|
41
|
-
trySyncSlashCompletion?(textBeforeCursor: string): {
|
|
42
|
-
items: AutocompleteItem[];
|
|
43
|
-
prefix: string;
|
|
44
|
-
} | null;
|
|
45
|
-
/**
|
|
46
|
-
* Synchronously try to expand text immediately before the cursor (no async I/O).
|
|
47
|
-
* Called after every single-character insert. Implementations MUST cheaply
|
|
48
|
-
* early-return when the trailing context cannot trigger them.
|
|
49
|
-
* Returns the number of characters to delete immediately before the cursor
|
|
50
|
-
* and the literal string to insert in their place, or null to leave the
|
|
51
|
-
* buffer untouched.
|
|
52
|
-
*/
|
|
53
|
-
trySyncInlineReplace?(textBeforeCursor: string): {
|
|
54
|
-
replaceLen: number;
|
|
55
|
-
insert: string;
|
|
56
|
-
} | null;
|
|
57
|
-
}
|
|
58
|
-
export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
59
|
-
#private;
|
|
60
|
-
constructor(commands?: (SlashCommand | AutocompleteItem)[], basePath?: string);
|
|
61
|
-
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
62
|
-
items: AutocompleteItem[];
|
|
63
|
-
prefix: string;
|
|
64
|
-
} | null>;
|
|
65
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
66
|
-
lines: string[];
|
|
67
|
-
cursorLine: number;
|
|
68
|
-
cursorCol: number;
|
|
69
|
-
};
|
|
70
|
-
invalidateDirCache(dir?: string): void;
|
|
71
|
-
getForceFileSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
72
|
-
items: AutocompleteItem[];
|
|
73
|
-
prefix: string;
|
|
74
|
-
} | null>;
|
|
75
|
-
shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
|
|
76
|
-
/** Get inline hint text for slash commands with subcommand hints */
|
|
77
|
-
getInlineHint(lines: string[], cursorLine: number, cursorCol: number): string | null;
|
|
78
|
-
trySyncSlashCompletion(textBeforeCursor: string): {
|
|
79
|
-
items: AutocompleteItem[];
|
|
80
|
-
prefix: string;
|
|
81
|
-
} | null;
|
|
82
|
-
}
|
|
83
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export type PasteResult = {
|
|
2
|
-
handled: false;
|
|
3
|
-
} | {
|
|
4
|
-
handled: true;
|
|
5
|
-
pasteContent?: string;
|
|
6
|
-
remaining: string;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Handles bracketed paste mode buffering for terminal input components.
|
|
10
|
-
*
|
|
11
|
-
* Bracketed paste mode wraps pasted content between start (\x1b[200~) and
|
|
12
|
-
* end (\x1b[201~) markers, which may arrive split across multiple chunks.
|
|
13
|
-
* This class buffers incoming data and assembles complete paste payloads.
|
|
14
|
-
*/
|
|
15
|
-
export declare class BracketedPasteHandler {
|
|
16
|
-
#private;
|
|
17
|
-
/**
|
|
18
|
-
* Process incoming terminal data for bracketed paste sequences.
|
|
19
|
-
*
|
|
20
|
-
* @returns `{ handled: false }` if the data contains no paste sequence and
|
|
21
|
-
* should be processed normally. `{ handled: true }` if the data was
|
|
22
|
-
* consumed by paste buffering — `pasteContent` is set when a complete
|
|
23
|
-
* paste has been assembled; omitted when still buffering.
|
|
24
|
-
*/
|
|
25
|
-
process(data: string): PasteResult;
|
|
26
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Component } from "../tui";
|
|
2
|
-
/**
|
|
3
|
-
* Box component - a container that applies padding and background to all children
|
|
4
|
-
*/
|
|
5
|
-
export declare class Box implements Component {
|
|
6
|
-
#private;
|
|
7
|
-
children: Component[];
|
|
8
|
-
constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
|
|
9
|
-
addChild(component: Component): void;
|
|
10
|
-
removeChild(component: Component): void;
|
|
11
|
-
/** Remove a child without disposing it (for detach-then-readd reuse). */
|
|
12
|
-
detachChild(component: Component): void;
|
|
13
|
-
clear(): void;
|
|
14
|
-
/** Remove all children without disposing them (for detach-then-readd reuse). */
|
|
15
|
-
detachAll(): void;
|
|
16
|
-
dispose(): void;
|
|
17
|
-
setBgFn(bgFn?: (text: string) => string): void;
|
|
18
|
-
invalidate(): void;
|
|
19
|
-
render(width: number): string[];
|
|
20
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Loader } from "./loader";
|
|
2
|
-
/**
|
|
3
|
-
* Loader that can be cancelled with Escape.
|
|
4
|
-
* Extends Loader with an AbortSignal for cancelling async operations.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* const loader = new CancellableLoader(tui, cyan, dim, "Working...");
|
|
8
|
-
* loader.onAbort = () => done(null);
|
|
9
|
-
* doWork(loader.signal).then(done);
|
|
10
|
-
*/
|
|
11
|
-
export declare class CancellableLoader extends Loader {
|
|
12
|
-
#private;
|
|
13
|
-
/** Called when user presses Escape */
|
|
14
|
-
onAbort?: () => void;
|
|
15
|
-
/** AbortSignal that is aborted when user presses Escape */
|
|
16
|
-
get signal(): AbortSignal;
|
|
17
|
-
/** Whether the loader was aborted */
|
|
18
|
-
get aborted(): boolean;
|
|
19
|
-
handleInput(data: string): void;
|
|
20
|
-
dispose(): void;
|
|
21
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { type AutocompleteProvider } from "../autocomplete";
|
|
2
|
-
import type { SymbolTheme } from "../symbols";
|
|
3
|
-
import { type Component, type Focusable } from "../tui";
|
|
4
|
-
import { type SelectListTheme } from "./select-list";
|
|
5
|
-
export interface EditorTheme {
|
|
6
|
-
borderColor: (str: string) => string;
|
|
7
|
-
selectList: SelectListTheme;
|
|
8
|
-
symbols: SymbolTheme;
|
|
9
|
-
editorPaddingX?: number;
|
|
10
|
-
/** Style function for inline hint/ghost text (dim text after cursor) */
|
|
11
|
-
hintStyle?: (text: string) => string;
|
|
12
|
-
}
|
|
13
|
-
export interface EditorTopBorder {
|
|
14
|
-
/** The status content (already styled) */
|
|
15
|
-
content: string;
|
|
16
|
-
/** Visible width of the content */
|
|
17
|
-
width: number;
|
|
18
|
-
}
|
|
19
|
-
export type EditorBorderStyle = "round" | "sharp";
|
|
20
|
-
interface HistoryEntry {
|
|
21
|
-
prompt: string;
|
|
22
|
-
}
|
|
23
|
-
interface HistoryStorage {
|
|
24
|
-
add(prompt: string, cwd?: string): Promise<void>;
|
|
25
|
-
getRecent(limit: number, cwd?: string): HistoryEntry[];
|
|
26
|
-
}
|
|
27
|
-
/** Test-only performance counters for advisory baseline tests. */
|
|
28
|
-
export declare const __editorPerfCounters: {
|
|
29
|
-
layoutTextInvocations: number;
|
|
30
|
-
layoutLogicalLinesProcessed: number;
|
|
31
|
-
visibleWidthMeasurements: number;
|
|
32
|
-
reset(): void;
|
|
33
|
-
};
|
|
34
|
-
export declare class Editor implements Component, Focusable {
|
|
35
|
-
#private;
|
|
36
|
-
/** Focusable interface - set by TUI when focus changes */
|
|
37
|
-
focused: boolean;
|
|
38
|
-
/** When set, replaces the normal cursor glyph at end-of-text with this ANSI-styled string. */
|
|
39
|
-
cursorOverride: string | undefined;
|
|
40
|
-
/** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
|
|
41
|
-
cursorOverrideWidth: number | undefined;
|
|
42
|
-
borderColor: (str: string) => string;
|
|
43
|
-
onAutocompleteUpdate?: () => void;
|
|
44
|
-
onSubmit?: (text: string) => void;
|
|
45
|
-
onAltEnter?: (text: string) => void;
|
|
46
|
-
onChange?: (text: string) => void;
|
|
47
|
-
onAutocompleteCancel?: () => void;
|
|
48
|
-
onTabDeclined?: (text: string) => void;
|
|
49
|
-
/**
|
|
50
|
-
* Called before Tab opens/applies autocomplete. Return true to consume Tab
|
|
51
|
-
* for app-level behavior (for example, queueing a draft while a turn runs).
|
|
52
|
-
*/
|
|
53
|
-
onTab?: (text: string) => boolean | undefined;
|
|
54
|
-
disableSubmit: boolean;
|
|
55
|
-
constructor(theme: EditorTheme);
|
|
56
|
-
dispose(): void;
|
|
57
|
-
setAutocompleteProvider(provider: AutocompleteProvider): void;
|
|
58
|
-
getAutocompleteProvider(): AutocompleteProvider | undefined;
|
|
59
|
-
/** Whether the autocomplete dropdown is currently open. */
|
|
60
|
-
isAutocompleteOpen(): boolean;
|
|
61
|
-
/**
|
|
62
|
-
* Set custom content for the top border (e.g., status line).
|
|
63
|
-
* Pass undefined to use the default plain border.
|
|
64
|
-
*/
|
|
65
|
-
setTopBorder(content: EditorTopBorder | undefined): void;
|
|
66
|
-
/**
|
|
67
|
-
* Show or hide the editor border chrome.
|
|
68
|
-
*/
|
|
69
|
-
setBorderVisible(borderVisible: boolean): void;
|
|
70
|
-
setBorderStyle(borderStyle: EditorBorderStyle): void;
|
|
71
|
-
setClosedBorderBox(closedBorderBox: boolean): void;
|
|
72
|
-
setPromptGutter(promptGutter: string | undefined): void;
|
|
73
|
-
setInputPrefix(inputPrefix: string | undefined): void;
|
|
74
|
-
setPlaceholder(placeholder: string | undefined): void;
|
|
75
|
-
/**
|
|
76
|
-
* Get the available width for top border content given a total terminal width.
|
|
77
|
-
* Accounts for right gutter, border characters, and horizontal padding when visible.
|
|
78
|
-
*/
|
|
79
|
-
getTopBorderAvailableWidth(terminalWidth: number): number;
|
|
80
|
-
/**
|
|
81
|
-
* Use the real terminal cursor instead of rendering a cursor glyph.
|
|
82
|
-
*/
|
|
83
|
-
setUseTerminalCursor(useTerminalCursor: boolean): void;
|
|
84
|
-
getUseTerminalCursor(): boolean;
|
|
85
|
-
setMaxHeight(maxHeight: number | undefined): void;
|
|
86
|
-
setPaddingX(paddingX: number): void;
|
|
87
|
-
setRightGutterWidth(width: number): void;
|
|
88
|
-
getAutocompleteMaxVisible(): number;
|
|
89
|
-
setAutocompleteMaxVisible(maxVisible: number): void;
|
|
90
|
-
setHistoryStorage(storage: HistoryStorage): void;
|
|
91
|
-
/**
|
|
92
|
-
* Add a prompt to history for up/down arrow navigation.
|
|
93
|
-
* Called after successful submission.
|
|
94
|
-
*/
|
|
95
|
-
addToHistory(text: string): void;
|
|
96
|
-
invalidate(): void;
|
|
97
|
-
render(width: number): string[];
|
|
98
|
-
handleInput(data: string): void;
|
|
99
|
-
/** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
|
|
100
|
-
get wrappedLineCacheSize(): number;
|
|
101
|
-
getText(): string;
|
|
102
|
-
/**
|
|
103
|
-
* Get text with paste markers expanded to their actual content.
|
|
104
|
-
* Use this when you need the full content (e.g., for external editor).
|
|
105
|
-
*/
|
|
106
|
-
getExpandedText(): string;
|
|
107
|
-
getLines(): string[];
|
|
108
|
-
getCursor(): {
|
|
109
|
-
line: number;
|
|
110
|
-
col: number;
|
|
111
|
-
};
|
|
112
|
-
moveToLineStart(): void;
|
|
113
|
-
moveToLineEnd(): void;
|
|
114
|
-
moveToMessageStart(): void;
|
|
115
|
-
moveToMessageEnd(): void;
|
|
116
|
-
/**
|
|
117
|
-
* Undo the last meaningful edit while ignoring transient text that is still present at the cursor.
|
|
118
|
-
* Used for command-like autocomplete actions whose typed trigger should not count as the edit being undone.
|
|
119
|
-
*/
|
|
120
|
-
undoPastTransientText(transientText: string): void;
|
|
121
|
-
setText(text: string): void;
|
|
122
|
-
/** Insert text at the current cursor position */
|
|
123
|
-
insertText(text: string): void;
|
|
124
|
-
isShowingAutocomplete(): boolean;
|
|
125
|
-
}
|
|
126
|
-
export {};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type ImageDimensions } from "../terminal-capabilities";
|
|
2
|
-
import type { Component } from "../tui";
|
|
3
|
-
export interface ImageTheme {
|
|
4
|
-
fallbackColor: (str: string) => string;
|
|
5
|
-
}
|
|
6
|
-
export interface ImageOptions {
|
|
7
|
-
maxWidthCells?: number;
|
|
8
|
-
maxHeightCells?: number;
|
|
9
|
-
filename?: string;
|
|
10
|
-
refetch?: () => string;
|
|
11
|
-
}
|
|
12
|
-
export declare class Image implements Component {
|
|
13
|
-
#private;
|
|
14
|
-
constructor(base64Data: string, mimeType: string, theme: ImageTheme, options?: ImageOptions, dimensions?: ImageDimensions);
|
|
15
|
-
invalidate(): void;
|
|
16
|
-
get retainedBase64DataForTest(): string | undefined;
|
|
17
|
-
render(width: number): string[];
|
|
18
|
-
}
|