@sayknow-cli/tui 0.3.4 → 0.3.6
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/CHANGELOG.md +8 -0
- package/package.json +8 -9
- package/src/components/settings-list.ts +12 -0
- package/src/terminal.ts +42 -2
- package/src/tui.ts +38 -4
- package/dist/types/autocomplete.d.ts +0 -82
- 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 -112
- 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 -14
- package/dist/types/components/markdown.d.ts +0 -64
- 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 -26
- 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 -75
- package/dist/types/terminal.d.ts +0 -80
- package/dist/types/ttyid.d.ts +0 -9
- package/dist/types/tui.d.ts +0 -182
- package/dist/types/utils.d.ts +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.8] - 2026-06-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- The render loop now isolates a component whose `render()` throws: the failure is logged once and replaced with a `[render error: <Name>]` fallback line instead of escaping the frame and tripping the process-level fail-fast `uncaughtException` exit. Previously any unguarded renderer fault (e.g. a tool renderer fed an undefined field) crashed the whole app on whatever triggered the next frame — a keystroke, resize, or command such as `/background` (#1291).
|
|
10
|
+
|
|
11
|
+
- Resolved terminal dimensions from the live TTY window size before stream defaults so wide Windows Terminal/PowerShell sessions render against the actual viewport width (#1239).
|
|
12
|
+
|
|
5
13
|
## [0.7.4] - 2026-06-27
|
|
6
14
|
|
|
7
15
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/tui",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/jaybeyond/Sayknow_CLI",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"cli"
|
|
28
28
|
],
|
|
29
29
|
"main": "./src/index.ts",
|
|
30
|
-
"types": "./
|
|
30
|
+
"types": "./src/index.ts",
|
|
31
31
|
"scripts": {
|
|
32
32
|
"check": "biome check . && bun run check:types",
|
|
33
33
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"fmt": "biome format --write ."
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@sayknow-cli/natives": "0.3.
|
|
42
|
-
"@sayknow-cli/utils": "0.3.
|
|
41
|
+
"@sayknow-cli/natives": "0.3.6",
|
|
42
|
+
"@sayknow-cli/utils": "0.3.6",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
|
@@ -53,20 +53,19 @@
|
|
|
53
53
|
"files": [
|
|
54
54
|
"src",
|
|
55
55
|
"README.md",
|
|
56
|
-
"CHANGELOG.md"
|
|
57
|
-
"dist/types"
|
|
56
|
+
"CHANGELOG.md"
|
|
58
57
|
],
|
|
59
58
|
"exports": {
|
|
60
59
|
".": {
|
|
61
|
-
"types": "./
|
|
60
|
+
"types": "./src/index.ts",
|
|
62
61
|
"import": "./src/index.ts"
|
|
63
62
|
},
|
|
64
63
|
"./*": {
|
|
65
|
-
"types": "./
|
|
64
|
+
"types": "./src/*.ts",
|
|
66
65
|
"import": "./src/*.ts"
|
|
67
66
|
},
|
|
68
67
|
"./components/*": {
|
|
69
|
-
"types": "./
|
|
68
|
+
"types": "./src/components/*.ts",
|
|
70
69
|
"import": "./src/components/*.ts"
|
|
71
70
|
},
|
|
72
71
|
"./*.js": "./src/*.ts"
|
|
@@ -32,6 +32,7 @@ export class SettingsList implements Component {
|
|
|
32
32
|
#maxVisible: number;
|
|
33
33
|
#onChange: (id: string, newValue: string) => void;
|
|
34
34
|
#onCancel: () => void;
|
|
35
|
+
#onSelectionChange?: (item: SettingItem | undefined) => void;
|
|
35
36
|
|
|
36
37
|
// Submenu state
|
|
37
38
|
#submenuComponent: Component | null = null;
|
|
@@ -43,12 +44,15 @@ export class SettingsList implements Component {
|
|
|
43
44
|
theme: SettingsListTheme,
|
|
44
45
|
onChange: (id: string, newValue: string) => void,
|
|
45
46
|
onCancel: () => void,
|
|
47
|
+
onSelectionChange?: (item: SettingItem | undefined) => void,
|
|
46
48
|
) {
|
|
47
49
|
this.#items = items;
|
|
48
50
|
this.#maxVisible = maxVisible;
|
|
49
51
|
this.#theme = theme;
|
|
50
52
|
this.#onChange = onChange;
|
|
51
53
|
this.#onCancel = onCancel;
|
|
54
|
+
this.#onSelectionChange = onSelectionChange;
|
|
55
|
+
this.#notifySelectionChange();
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** Update an item's currentValue */
|
|
@@ -73,12 +77,17 @@ export class SettingsList implements Component {
|
|
|
73
77
|
} else if (this.#selectedIndex >= this.#items.length) {
|
|
74
78
|
this.#selectedIndex = this.#items.length - 1;
|
|
75
79
|
}
|
|
80
|
+
this.#notifySelectionChange();
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
invalidate(): void {
|
|
79
84
|
this.#submenuComponent?.invalidate?.();
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
#notifySelectionChange(): void {
|
|
88
|
+
this.#onSelectionChange?.(this.#items[this.#selectedIndex]);
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
render(width: number): string[] {
|
|
83
92
|
// If submenu is active, render it instead
|
|
84
93
|
if (this.#submenuComponent) {
|
|
@@ -167,8 +176,10 @@ export class SettingsList implements Component {
|
|
|
167
176
|
const kb = getKeybindings();
|
|
168
177
|
if (kb.matches(data, "tui.select.up")) {
|
|
169
178
|
this.#selectedIndex = this.#selectedIndex === 0 ? this.#items.length - 1 : this.#selectedIndex - 1;
|
|
179
|
+
this.#notifySelectionChange();
|
|
170
180
|
} else if (kb.matches(data, "tui.select.down")) {
|
|
171
181
|
this.#selectedIndex = this.#selectedIndex === this.#items.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
182
|
+
this.#notifySelectionChange();
|
|
172
183
|
} else if (kb.matches(data, "tui.select.confirm") || data === " " || data === "\n") {
|
|
173
184
|
this.#activateItem();
|
|
174
185
|
} else if (kb.matches(data, "tui.select.cancel")) {
|
|
@@ -206,6 +217,7 @@ export class SettingsList implements Component {
|
|
|
206
217
|
if (this.#submenuItemIndex !== null) {
|
|
207
218
|
this.#selectedIndex = this.#submenuItemIndex;
|
|
208
219
|
this.#submenuItemIndex = null;
|
|
220
|
+
this.#notifySelectionChange();
|
|
209
221
|
}
|
|
210
222
|
}
|
|
211
223
|
}
|
package/src/terminal.ts
CHANGED
|
@@ -164,6 +164,46 @@ export interface Terminal {
|
|
|
164
164
|
get appearance(): TerminalAppearance | undefined;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
interface TerminalSizeStream {
|
|
168
|
+
columns?: number;
|
|
169
|
+
rows?: number;
|
|
170
|
+
getWindowSize?: () => [number, number] | number[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function positiveDimension(value: unknown): number | undefined {
|
|
174
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
175
|
+
const dimension = Math.trunc(value);
|
|
176
|
+
return dimension > 0 ? dimension : undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function resolveTerminalColumns(
|
|
180
|
+
stream: TerminalSizeStream = process.stdout,
|
|
181
|
+
envColumns: string | undefined = Bun.env.COLUMNS,
|
|
182
|
+
): number {
|
|
183
|
+
try {
|
|
184
|
+
const windowSize = stream.getWindowSize?.();
|
|
185
|
+
const liveColumns = positiveDimension(windowSize?.[0]);
|
|
186
|
+
if (liveColumns !== undefined) return liveColumns;
|
|
187
|
+
} catch {
|
|
188
|
+
// Fall back below when the stream cannot report a live TTY size.
|
|
189
|
+
}
|
|
190
|
+
return positiveDimension(stream.columns) ?? positiveDimension(Number(envColumns)) ?? 80;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function resolveTerminalRows(
|
|
194
|
+
stream: TerminalSizeStream = process.stdout,
|
|
195
|
+
envRows: string | undefined = Bun.env.LINES,
|
|
196
|
+
): number {
|
|
197
|
+
try {
|
|
198
|
+
const windowSize = stream.getWindowSize?.();
|
|
199
|
+
const liveRows = positiveDimension(windowSize?.[1]);
|
|
200
|
+
if (liveRows !== undefined) return liveRows;
|
|
201
|
+
} catch {
|
|
202
|
+
// Fall back below when the stream cannot report a live TTY size.
|
|
203
|
+
}
|
|
204
|
+
return positiveDimension(stream.rows) ?? positiveDimension(Number(envRows)) ?? 24;
|
|
205
|
+
}
|
|
206
|
+
|
|
167
207
|
function isWindowsSubsystemForLinux(): boolean {
|
|
168
208
|
return process.platform === "linux" && (!!$env.WSL_DISTRO_NAME || !!$env.WSL_INTEROP);
|
|
169
209
|
}
|
|
@@ -789,11 +829,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
789
829
|
}
|
|
790
830
|
|
|
791
831
|
get columns(): number {
|
|
792
|
-
return
|
|
832
|
+
return resolveTerminalColumns();
|
|
793
833
|
}
|
|
794
834
|
|
|
795
835
|
get rows(): number {
|
|
796
|
-
return
|
|
836
|
+
return resolveTerminalRows();
|
|
797
837
|
}
|
|
798
838
|
|
|
799
839
|
moveBy(lines: number): void {
|
package/src/tui.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { performance } from "node:perf_hooks";
|
|
7
|
-
import { $flag, getDebugLogPath } from "@sayknow-cli/utils";
|
|
7
|
+
import { $flag, getDebugLogPath, logger } from "@sayknow-cli/utils";
|
|
8
8
|
import { getKeybindings } from "./keybindings";
|
|
9
9
|
import { isKeyRelease } from "./keys";
|
|
10
10
|
import { renderMetrics } from "./metrics";
|
|
@@ -257,7 +257,7 @@ export class Container implements Component {
|
|
|
257
257
|
width = Math.max(1, width);
|
|
258
258
|
const lines: string[] = [];
|
|
259
259
|
for (const child of this.children) {
|
|
260
|
-
const childLines = child
|
|
260
|
+
const childLines = safeRenderComponent(child, width, "container-child");
|
|
261
261
|
for (let i = 0; i < childLines.length; i++) {
|
|
262
262
|
lines.push(childLines[i]);
|
|
263
263
|
}
|
|
@@ -266,6 +266,40 @@ export class Container implements Component {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
const MAX_REPORTED_RENDER_ERRORS = 200;
|
|
270
|
+
const reportedRenderErrors = new Set<string>();
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Render a component's lines without letting a thrown error escape the frame.
|
|
274
|
+
*
|
|
275
|
+
* The TUI render loop ({@link TUI.#doRender}) runs inside a `nextTick`/`setTimeout`
|
|
276
|
+
* with no try/catch, and the process installs a fail-fast `uncaughtException`
|
|
277
|
+
* handler that exits. So a single component whose `render()` throws (e.g. a tool
|
|
278
|
+
* renderer fed an optional/undefined field) used to take down the whole app —
|
|
279
|
+
* fatal on whatever happened to trigger the frame (a keystroke, resize, or a
|
|
280
|
+
* command such as `/background`). Isolate the failure: log it once, emit a
|
|
281
|
+
* visible fallback line, and keep rendering the rest of the tree.
|
|
282
|
+
*/
|
|
283
|
+
function safeRenderComponent(component: Component, width: number, where: string): string[] {
|
|
284
|
+
try {
|
|
285
|
+
return component.render(width);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const name = component?.constructor?.name ?? "Component";
|
|
288
|
+
const key = `${where}:${name}:${err instanceof Error ? err.message : String(err)}`;
|
|
289
|
+
if (!reportedRenderErrors.has(key)) {
|
|
290
|
+
if (reportedRenderErrors.size >= MAX_REPORTED_RENDER_ERRORS) reportedRenderErrors.clear();
|
|
291
|
+
reportedRenderErrors.add(key);
|
|
292
|
+
logger.error("Component render failed; emitting fallback line", {
|
|
293
|
+
where,
|
|
294
|
+
component: name,
|
|
295
|
+
error: err instanceof Error ? err.message : String(err),
|
|
296
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return [`[render error: ${name}]`];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
269
303
|
type LineNormalizationCacheEntry = {
|
|
270
304
|
normalized: string;
|
|
271
305
|
terminated: string;
|
|
@@ -1071,7 +1105,7 @@ export class TUI extends Container {
|
|
|
1071
1105
|
const { width, maxHeight } = this.#resolveOverlayLayout(options, 0, termWidth, termHeight);
|
|
1072
1106
|
|
|
1073
1107
|
// Render component at calculated width
|
|
1074
|
-
let overlayLines = component
|
|
1108
|
+
let overlayLines = safeRenderComponent(component, width, "overlay");
|
|
1075
1109
|
|
|
1076
1110
|
// Apply maxHeight if specified
|
|
1077
1111
|
if (maxHeight !== undefined && overlayLines.length > maxHeight) {
|
|
@@ -1302,7 +1336,7 @@ export class TUI extends Container {
|
|
|
1302
1336
|
|
|
1303
1337
|
let pinnedLineCount = 0;
|
|
1304
1338
|
for (let i = pinnedStart; i < this.children.length; i++) {
|
|
1305
|
-
pinnedLineCount += this.children[i]
|
|
1339
|
+
pinnedLineCount += safeRenderComponent(this.children[i], this.terminal.columns, "pinned").length;
|
|
1306
1340
|
}
|
|
1307
1341
|
|
|
1308
1342
|
const blankRows = height - lines.length;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
export declare function getSlashCommandMatchRank(query: string, commandName: string): number;
|
|
2
|
-
export interface AutocompleteItem {
|
|
3
|
-
value: string;
|
|
4
|
-
label: string;
|
|
5
|
-
description?: string;
|
|
6
|
-
/** Dim hint text shown inline after cursor when this item is selected */
|
|
7
|
-
hint?: string;
|
|
8
|
-
}
|
|
9
|
-
type Awaitable<T> = T | Promise<T>;
|
|
10
|
-
export interface SlashCommand {
|
|
11
|
-
name: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
argumentHint?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Higher values surface first in autocomplete, ahead of fuzzy-score ordering.
|
|
16
|
-
* Use this to pin first-class commands (e.g. bundled SKC skills) to the top.
|
|
17
|
-
*/
|
|
18
|
-
priority?: number;
|
|
19
|
-
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
|
20
|
-
/** Return inline hint text for the current argument state (shown as dim ghost text after cursor) */
|
|
21
|
-
getInlineHint?(argumentText: string): string | null;
|
|
22
|
-
}
|
|
23
|
-
export interface AutocompleteProvider {
|
|
24
|
-
/** Get autocomplete suggestions for current text/cursor position */
|
|
25
|
-
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
26
|
-
items: AutocompleteItem[];
|
|
27
|
-
prefix: string;
|
|
28
|
-
} | null>;
|
|
29
|
-
/** Apply the selected item and return new text + cursor position */
|
|
30
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
31
|
-
lines: string[];
|
|
32
|
-
cursorLine: number;
|
|
33
|
-
cursorCol: number;
|
|
34
|
-
onApplied?: () => void;
|
|
35
|
-
};
|
|
36
|
-
/** Get inline hint text to show as dim ghost text after the cursor */
|
|
37
|
-
getInlineHint?(lines: string[], cursorLine: number, cursorCol: number): string | null;
|
|
38
|
-
/** Synchronously try to complete a slash command at the start of a line (no async I/O). */
|
|
39
|
-
/** Returns matched items and the full prefix, or null if not applicable. */
|
|
40
|
-
trySyncSlashCompletion?(textBeforeCursor: string): {
|
|
41
|
-
items: AutocompleteItem[];
|
|
42
|
-
prefix: string;
|
|
43
|
-
} | null;
|
|
44
|
-
/**
|
|
45
|
-
* Synchronously try to expand text immediately before the cursor (no async I/O).
|
|
46
|
-
* Called after every single-character insert. Implementations MUST cheaply
|
|
47
|
-
* early-return when the trailing context cannot trigger them.
|
|
48
|
-
* Returns the number of characters to delete immediately before the cursor
|
|
49
|
-
* and the literal string to insert in their place, or null to leave the
|
|
50
|
-
* buffer untouched.
|
|
51
|
-
*/
|
|
52
|
-
trySyncInlineReplace?(textBeforeCursor: string): {
|
|
53
|
-
replaceLen: number;
|
|
54
|
-
insert: string;
|
|
55
|
-
} | null;
|
|
56
|
-
}
|
|
57
|
-
export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
58
|
-
#private;
|
|
59
|
-
constructor(commands?: (SlashCommand | AutocompleteItem)[], basePath?: string);
|
|
60
|
-
getSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
61
|
-
items: AutocompleteItem[];
|
|
62
|
-
prefix: string;
|
|
63
|
-
} | null>;
|
|
64
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
65
|
-
lines: string[];
|
|
66
|
-
cursorLine: number;
|
|
67
|
-
cursorCol: number;
|
|
68
|
-
};
|
|
69
|
-
invalidateDirCache(dir?: string): void;
|
|
70
|
-
getForceFileSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
71
|
-
items: AutocompleteItem[];
|
|
72
|
-
prefix: string;
|
|
73
|
-
} | null>;
|
|
74
|
-
shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
|
|
75
|
-
/** Get inline hint text for slash commands with subcommand hints */
|
|
76
|
-
getInlineHint(lines: string[], cursorLine: number, cursorCol: number): string | null;
|
|
77
|
-
trySyncSlashCompletion(textBeforeCursor: string): {
|
|
78
|
-
items: AutocompleteItem[];
|
|
79
|
-
prefix: string;
|
|
80
|
-
} | null;
|
|
81
|
-
}
|
|
82
|
-
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,112 +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
|
-
export declare class Editor implements Component, Focusable {
|
|
28
|
-
#private;
|
|
29
|
-
/** Focusable interface - set by TUI when focus changes */
|
|
30
|
-
focused: boolean;
|
|
31
|
-
/** When set, replaces the normal cursor glyph at end-of-text with this ANSI-styled string. */
|
|
32
|
-
cursorOverride: string | undefined;
|
|
33
|
-
/** Display width of the cursorOverride glyph (needed because override may contain ANSI escapes). */
|
|
34
|
-
cursorOverrideWidth: number | undefined;
|
|
35
|
-
borderColor: (str: string) => string;
|
|
36
|
-
onAutocompleteUpdate?: () => void;
|
|
37
|
-
onSubmit?: (text: string) => void;
|
|
38
|
-
onAltEnter?: (text: string) => void;
|
|
39
|
-
onChange?: (text: string) => void;
|
|
40
|
-
onAutocompleteCancel?: () => void;
|
|
41
|
-
onTabDeclined?: (text: string) => void;
|
|
42
|
-
disableSubmit: boolean;
|
|
43
|
-
constructor(theme: EditorTheme);
|
|
44
|
-
setAutocompleteProvider(provider: AutocompleteProvider): void;
|
|
45
|
-
getAutocompleteProvider(): AutocompleteProvider | undefined;
|
|
46
|
-
/** Whether the autocomplete dropdown is currently open. */
|
|
47
|
-
isAutocompleteOpen(): boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Set custom content for the top border (e.g., status line).
|
|
50
|
-
* Pass undefined to use the default plain border.
|
|
51
|
-
*/
|
|
52
|
-
setTopBorder(content: EditorTopBorder | undefined): void;
|
|
53
|
-
/**
|
|
54
|
-
* Show or hide the editor border chrome.
|
|
55
|
-
*/
|
|
56
|
-
setBorderVisible(borderVisible: boolean): void;
|
|
57
|
-
setBorderStyle(borderStyle: EditorBorderStyle): void;
|
|
58
|
-
setClosedBorderBox(closedBorderBox: boolean): void;
|
|
59
|
-
setPromptGutter(promptGutter: string | undefined): void;
|
|
60
|
-
setInputPrefix(inputPrefix: string | undefined): void;
|
|
61
|
-
setPlaceholder(placeholder: string | undefined): void;
|
|
62
|
-
/**
|
|
63
|
-
* Get the available width for top border content given a total terminal width.
|
|
64
|
-
* Accounts for the border characters and horizontal padding when visible.
|
|
65
|
-
*/
|
|
66
|
-
getTopBorderAvailableWidth(terminalWidth: number): number;
|
|
67
|
-
/**
|
|
68
|
-
* Use the real terminal cursor instead of rendering a cursor glyph.
|
|
69
|
-
*/
|
|
70
|
-
setUseTerminalCursor(useTerminalCursor: boolean): void;
|
|
71
|
-
getUseTerminalCursor(): boolean;
|
|
72
|
-
setMaxHeight(maxHeight: number | undefined): void;
|
|
73
|
-
setPaddingX(paddingX: number): void;
|
|
74
|
-
getAutocompleteMaxVisible(): number;
|
|
75
|
-
setAutocompleteMaxVisible(maxVisible: number): void;
|
|
76
|
-
setHistoryStorage(storage: HistoryStorage): void;
|
|
77
|
-
/**
|
|
78
|
-
* Add a prompt to history for up/down arrow navigation.
|
|
79
|
-
* Called after successful submission.
|
|
80
|
-
*/
|
|
81
|
-
addToHistory(text: string): void;
|
|
82
|
-
invalidate(): void;
|
|
83
|
-
render(width: number): string[];
|
|
84
|
-
handleInput(data: string): void;
|
|
85
|
-
/** Test-only seam: current wrap-cache entry count (memory-bound assertions). */
|
|
86
|
-
get wrappedLineCacheSize(): number;
|
|
87
|
-
getText(): string;
|
|
88
|
-
/**
|
|
89
|
-
* Get text with paste markers expanded to their actual content.
|
|
90
|
-
* Use this when you need the full content (e.g., for external editor).
|
|
91
|
-
*/
|
|
92
|
-
getExpandedText(): string;
|
|
93
|
-
getLines(): string[];
|
|
94
|
-
getCursor(): {
|
|
95
|
-
line: number;
|
|
96
|
-
col: number;
|
|
97
|
-
};
|
|
98
|
-
moveToLineStart(): void;
|
|
99
|
-
moveToLineEnd(): void;
|
|
100
|
-
moveToMessageStart(): void;
|
|
101
|
-
moveToMessageEnd(): void;
|
|
102
|
-
/**
|
|
103
|
-
* Undo the last meaningful edit while ignoring transient text that is still present at the cursor.
|
|
104
|
-
* Used for command-like autocomplete actions whose typed trigger should not count as the edit being undone.
|
|
105
|
-
*/
|
|
106
|
-
undoPastTransientText(transientText: string): void;
|
|
107
|
-
setText(text: string): void;
|
|
108
|
-
/** Insert text at the current cursor position */
|
|
109
|
-
insertText(text: string): void;
|
|
110
|
-
isShowingAutocomplete(): boolean;
|
|
111
|
-
}
|
|
112
|
-
export {};
|
|
@@ -1,16 +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
|
-
}
|
|
11
|
-
export declare class Image implements Component {
|
|
12
|
-
#private;
|
|
13
|
-
constructor(base64Data: string, mimeType: string, theme: ImageTheme, options?: ImageOptions, dimensions?: ImageDimensions);
|
|
14
|
-
invalidate(): void;
|
|
15
|
-
render(width: number): string[];
|
|
16
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { type Component, type Focusable } from "../tui";
|
|
2
|
-
/**
|
|
3
|
-
* Input component - single-line text input with horizontal scrolling
|
|
4
|
-
*/
|
|
5
|
-
export declare class Input implements Component, Focusable {
|
|
6
|
-
#private;
|
|
7
|
-
onSubmit?: (value: string) => void;
|
|
8
|
-
onEscape?: () => void;
|
|
9
|
-
/** Focusable interface - set by TUI when focus changes */
|
|
10
|
-
focused: boolean;
|
|
11
|
-
getValue(): string;
|
|
12
|
-
setValue(value: string): void;
|
|
13
|
-
handleInput(data: string): void;
|
|
14
|
-
invalidate(): void;
|
|
15
|
-
render(width: number): string[];
|
|
16
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { TUI } from "../tui";
|
|
2
|
-
import { Text } from "./text";
|
|
3
|
-
export declare class Loader extends Text {
|
|
4
|
-
#private;
|
|
5
|
-
private spinnerColorFn;
|
|
6
|
-
private messageColorFn;
|
|
7
|
-
private message;
|
|
8
|
-
constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, spinnerFrames?: string[]);
|
|
9
|
-
render(width: number): string[];
|
|
10
|
-
start(): void;
|
|
11
|
-
stop(): void;
|
|
12
|
-
dispose(): void;
|
|
13
|
-
setMessage(message: string): void;
|
|
14
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { SymbolTheme } from "../symbols";
|
|
2
|
-
import type { Component } from "../tui";
|
|
3
|
-
/** Test/diagnostic seam: number of synchronous highlight invocations since the last reset. */
|
|
4
|
-
export declare function getMarkdownHighlightCallCount(): number;
|
|
5
|
-
export declare function resetMarkdownHighlightCallCount(): void;
|
|
6
|
-
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
7
|
-
export declare function clearRenderCache(): void;
|
|
8
|
-
/**
|
|
9
|
-
* Default text styling for markdown content.
|
|
10
|
-
* Applied to all text unless overridden by markdown formatting.
|
|
11
|
-
*/
|
|
12
|
-
export interface DefaultTextStyle {
|
|
13
|
-
/** Foreground color function */
|
|
14
|
-
color?: (text: string) => string;
|
|
15
|
-
/** Background color function */
|
|
16
|
-
bgColor?: (text: string) => string;
|
|
17
|
-
/** Bold text */
|
|
18
|
-
bold?: boolean;
|
|
19
|
-
/** Italic text */
|
|
20
|
-
italic?: boolean;
|
|
21
|
-
/** Strikethrough text */
|
|
22
|
-
strikethrough?: boolean;
|
|
23
|
-
/** Underline text */
|
|
24
|
-
underline?: boolean;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Theme functions for markdown elements.
|
|
28
|
-
* Each function takes text and returns styled text with ANSI codes.
|
|
29
|
-
*/
|
|
30
|
-
export interface MarkdownTheme {
|
|
31
|
-
heading: (text: string) => string;
|
|
32
|
-
link: (text: string) => string;
|
|
33
|
-
linkUrl: (text: string) => string;
|
|
34
|
-
code: (text: string) => string;
|
|
35
|
-
codeBlock: (text: string) => string;
|
|
36
|
-
codeBlockBorder: (text: string) => string;
|
|
37
|
-
quote: (text: string) => string;
|
|
38
|
-
quoteBorder: (text: string) => string;
|
|
39
|
-
hr: (text: string) => string;
|
|
40
|
-
listBullet: (text: string) => string;
|
|
41
|
-
bold: (text: string) => string;
|
|
42
|
-
italic: (text: string) => string;
|
|
43
|
-
strikethrough: (text: string) => string;
|
|
44
|
-
underline: (text: string) => string;
|
|
45
|
-
highlightCode?: (code: string, lang?: string) => string[];
|
|
46
|
-
/**
|
|
47
|
-
* Resolve a mermaid ASCII rendering by fenced block source text.
|
|
48
|
-
* Return null to fall back to fenced code rendering.
|
|
49
|
-
*/
|
|
50
|
-
resolveMermaidAscii?: (source: string) => string | null;
|
|
51
|
-
symbols: SymbolTheme;
|
|
52
|
-
}
|
|
53
|
-
export declare class Markdown implements Component {
|
|
54
|
-
#private;
|
|
55
|
-
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
|
|
56
|
-
setText(text: string): void;
|
|
57
|
-
invalidate(): void;
|
|
58
|
-
render(width: number): string[];
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Render inline markdown (bold, italic, code, links, strikethrough) to a styled string.
|
|
62
|
-
* Unlike the full Markdown component, this produces a single line with no block-level elements.
|
|
63
|
-
*/
|
|
64
|
-
export declare function renderInlineMarkdown(text: string, mdTheme: MarkdownTheme, baseColor?: (t: string) => string): string;
|