@linxiraos/pi-tui 1.0.4 → 1.0.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 +18 -5
- package/dist/types/autocomplete.d.ts +116 -0
- package/dist/types/bracketed-paste.d.ts +51 -0
- package/dist/types/components/box.d.ts +31 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +166 -0
- package/dist/types/components/image.d.ts +165 -0
- package/dist/types/components/input.d.ts +25 -0
- package/dist/types/components/loader.d.ts +25 -0
- package/dist/types/components/markdown.d.ts +92 -0
- package/dist/types/components/scroll-view.d.ts +62 -0
- package/dist/types/components/select-list.d.ts +69 -0
- package/dist/types/components/settings-list.d.ts +123 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +89 -0
- package/dist/types/components/text.d.ts +28 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/deccara.d.ts +49 -0
- package/dist/types/desktop-notify.d.ts +52 -0
- package/dist/types/editor-component.d.ts +38 -0
- package/dist/types/fuzzy.d.ts +48 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/keybindings.d.ts +197 -0
- package/dist/types/keys.d.ts +210 -0
- package/dist/types/kill-ring.d.ts +20 -0
- package/dist/types/kitty-graphics.d.ts +76 -0
- package/dist/types/latex-block.d.ts +8 -0
- package/dist/types/latex-to-unicode.d.ts +50 -0
- package/dist/types/loop-watchdog.d.ts +49 -0
- package/dist/types/mouse.d.ts +67 -0
- package/dist/types/stdin-buffer.d.ts +60 -0
- package/dist/types/symbols.d.ts +25 -0
- package/dist/types/terminal-capabilities.d.ts +324 -0
- package/dist/types/terminal.d.ts +175 -0
- package/dist/types/tmux.d.ts +6 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +480 -0
- package/dist/types/utils.d.ts +112 -0
- package/package.json +70 -69
- package/src/keys.ts +1 -1
- package/src/loop-watchdog.ts +32 -6
- package/src/terminal.ts +15 -8
- package/src/tui.ts +70 -16
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split `data` into chunks whose encoded UTF-8 byte length is no greater than
|
|
3
|
+
* `maxChunkBytes`, preferring a line boundary (`\n`) as the cut point so
|
|
4
|
+
* escape sequences (which never contain `\n`) stay intact. The TUI's
|
|
5
|
+
* full-paint buffers are line-structured (`buffer += "\r\n"` between rows),
|
|
6
|
+
* so a newline almost always exists within the window. The fallback for a
|
|
7
|
+
* buffer with no newline in range is a hard cut at the last UTF-8 code-point
|
|
8
|
+
* boundary that still fits — the ConPTY viewport bug from a single oversized
|
|
9
|
+
* write is strictly worse than a one-frame escape-sequence glitch on a
|
|
10
|
+
* buffer the renderer effectively never produces.
|
|
11
|
+
*
|
|
12
|
+
* UTF-16 code units are walked manually rather than measuring with
|
|
13
|
+
* `Buffer.byteLength` per slice candidate: each code unit's UTF-8 width is
|
|
14
|
+
* known from its value (BMP `<0x80` → 1, `<0x800` → 2, surrogate pair → 4
|
|
15
|
+
* bytes across two units, other BMP → 3), and surrogate pairs are kept
|
|
16
|
+
* together so the chunker never splits a non-BMP character.
|
|
17
|
+
*
|
|
18
|
+
* Exported for unit testing of the chunking contract; `#safeWrite` is the
|
|
19
|
+
* sole production caller.
|
|
20
|
+
*/
|
|
21
|
+
export declare function chunkForConPTY(data: string, maxChunkBytes?: number): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Turns an unbounded, never-draining stdout writable buffer into a bounded
|
|
24
|
+
* disconnect signal.
|
|
25
|
+
*
|
|
26
|
+
* `process.stdout.write()` returns `false` once its buffer exceeds the stream
|
|
27
|
+
* high-water mark; the bytes stay queued and are only freed when the consumer
|
|
28
|
+
* drains (the `drain` event). While the consumer keeps up, writes are accepted
|
|
29
|
+
* and nothing accumulates. When it stalls, every subsequent write piles onto
|
|
30
|
+
* the buffer — a stalled-but-alive PTY reader never throws, so the write path
|
|
31
|
+
* has no other signal that output is going nowhere. This guard sums the bytes
|
|
32
|
+
* queued since backpressure began and reports when that backlog crosses the
|
|
33
|
+
* cap, at which point the caller treats the terminal as disconnected.
|
|
34
|
+
*
|
|
35
|
+
* Exported for unit testing; `ProcessTerminal` is the sole production user.
|
|
36
|
+
*/
|
|
37
|
+
export declare class OutputBacklogGuard {
|
|
38
|
+
#private;
|
|
39
|
+
private readonly capBytes;
|
|
40
|
+
constructor(capBytes?: number);
|
|
41
|
+
/** True once a refused write started a backlog that has not yet drained. */
|
|
42
|
+
get tracking(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Record one `stdout.write()`: `accepted` is that call's return value and
|
|
45
|
+
* `bytes` its encoded size. Returns true when the pending backlog now
|
|
46
|
+
* exceeds the cap and the terminal should be treated as disconnected.
|
|
47
|
+
*/
|
|
48
|
+
record(accepted: boolean, bytes: number): boolean;
|
|
49
|
+
/** Called on the stdout `drain` event: the buffer emptied, backlog cleared. */
|
|
50
|
+
reset(): void;
|
|
51
|
+
}
|
|
52
|
+
/** Record alternate-screen state (called by the TUI on `?1049h`/`?1049l` writes). */
|
|
53
|
+
export declare function setAltScreenActive(active: boolean): void;
|
|
54
|
+
/**
|
|
55
|
+
* Emergency terminal restore - call this from signal/crash handlers
|
|
56
|
+
* Resets terminal state without requiring access to the ProcessTerminal instance
|
|
57
|
+
*/
|
|
58
|
+
export declare function emergencyTerminalRestore(): void;
|
|
59
|
+
/** Terminal-reported appearance (dark/light mode). */
|
|
60
|
+
export type TerminalAppearance = "dark" | "light";
|
|
61
|
+
/** Identity of an accepted explicit terminal appearance refresh request. */
|
|
62
|
+
export type TerminalAppearanceRequestToken = number;
|
|
63
|
+
export interface Terminal {
|
|
64
|
+
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
65
|
+
stop(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Drain stdin before exiting to prevent Kitty key release events from
|
|
68
|
+
* leaking to the parent shell over slow SSH connections.
|
|
69
|
+
* @param maxMs - Maximum time to drain (default: 1000ms)
|
|
70
|
+
* @param idleMs - Exit early if no input arrives within this time (default: 50ms)
|
|
71
|
+
*/
|
|
72
|
+
drainInput(maxMs?: number, idleMs?: number): Promise<void>;
|
|
73
|
+
write(data: string): void;
|
|
74
|
+
get columns(): number;
|
|
75
|
+
get rows(): number;
|
|
76
|
+
get kittyProtocolActive(): boolean;
|
|
77
|
+
get kittyEnableSequence(): string | null;
|
|
78
|
+
readonly keyboardEnhancementEnterSequence?: string | null;
|
|
79
|
+
readonly keyboardEnhancementExitSequence?: string | null;
|
|
80
|
+
moveBy(lines: number): void;
|
|
81
|
+
hideCursor(force?: boolean): void;
|
|
82
|
+
showCursor(force?: boolean): void;
|
|
83
|
+
clearLine(): void;
|
|
84
|
+
clearFromCursor(): void;
|
|
85
|
+
clearScreen(): void;
|
|
86
|
+
setTitle(title: string): void;
|
|
87
|
+
setProgress(active: boolean): void;
|
|
88
|
+
/**
|
|
89
|
+
* Register a callback for terminal appearance (dark/light) changes.
|
|
90
|
+
* Detection uses OSC 11 background color query with Mode 2031 as a change trigger.
|
|
91
|
+
* Fires when the detected appearance changes, including the initial detection.
|
|
92
|
+
* Subscribers registered after detection are invoked immediately with the
|
|
93
|
+
* already-detected appearance so late subscribers never miss it.
|
|
94
|
+
*/
|
|
95
|
+
onAppearanceChange(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): void;
|
|
96
|
+
/**
|
|
97
|
+
* Register a callback fired for every valid OSC 11 appearance report,
|
|
98
|
+
* including reports whose classification matches the current appearance.
|
|
99
|
+
* Unlike onAppearanceChange, this does not replay an earlier report.
|
|
100
|
+
* Optional so custom Terminals built against older pi-tui versions keep working.
|
|
101
|
+
*/
|
|
102
|
+
onAppearanceReport?(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): (() => void) | void;
|
|
103
|
+
/**
|
|
104
|
+
* Start a bounded OSC 11 background-color refresh cycle, driving appearance
|
|
105
|
+
* callbacks through the same parse/dedup pipeline used at startup and on Mode
|
|
106
|
+
* 2031 notifications. Direct terminals need one query; tmux needs a
|
|
107
|
+
* passthrough query to update its cache followed by one delayed direct cache
|
|
108
|
+
* read. Invoked on the user's explicit display-reset gesture so terminals
|
|
109
|
+
* without end-to-end Mode 2031 notifications pick up a light/dark switch
|
|
110
|
+
* without a restart. No periodic probes are armed.
|
|
111
|
+
*
|
|
112
|
+
* A caller-provided token must be propagated unchanged to callbacks and
|
|
113
|
+
* returned when the request is accepted. This lets callers establish ownership
|
|
114
|
+
* before implementations synchronously dispatch a cached response. Optional so
|
|
115
|
+
* custom Terminals built against older pi-tui versions keep working.
|
|
116
|
+
*/
|
|
117
|
+
refreshAppearance?(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
|
|
118
|
+
/** The last detected terminal appearance, or undefined if not yet known. */
|
|
119
|
+
get appearance(): TerminalAppearance | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Register a callback fired once per DEC private mode when its DECRQM support
|
|
122
|
+
* status resolves. `confirmed` is false when the terminal answered the DA1
|
|
123
|
+
* sentinel without answering DECRQM, which proves only that querying support
|
|
124
|
+
* is unavailable — not that the private mode itself is unsupported.
|
|
125
|
+
*/
|
|
126
|
+
onPrivateModeReport?(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* True when stdout flows through a ConPTY pseudo-console (native win32, or
|
|
130
|
+
* Linux running under WSL where stdout still crosses into ConPTY at the
|
|
131
|
+
* `wslhost` boundary). ConPTY hosts share the per-WriteFile viewport-tracking
|
|
132
|
+
* quirks documented above and on {@link MAX_CONPTY_WRITE_CHUNK_BYTES}, so both
|
|
133
|
+
* `#safeWrite` and the renderer's post-big-paint settle gate hang off this
|
|
134
|
+
* single predicate.
|
|
135
|
+
*/
|
|
136
|
+
export declare function isConPTYHosted(): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Real terminal using process.stdin/stdout
|
|
139
|
+
*/
|
|
140
|
+
export declare class ProcessTerminal implements Terminal {
|
|
141
|
+
#private;
|
|
142
|
+
get kittyProtocolActive(): boolean;
|
|
143
|
+
get kittyEnableSequence(): string | null;
|
|
144
|
+
get keyboardEnhancementEnterSequence(): string | null;
|
|
145
|
+
get keyboardEnhancementExitSequence(): string | null;
|
|
146
|
+
get appearance(): TerminalAppearance | undefined;
|
|
147
|
+
onAppearanceChange(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): void;
|
|
148
|
+
onAppearanceReport(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): () => void;
|
|
149
|
+
/**
|
|
150
|
+
* Re-query the terminal background through the startup DA1-sentinel FIFO,
|
|
151
|
+
* pending/queued gating, parsing, dedup, and appearance callbacks. Inside
|
|
152
|
+
* tmux, only this explicit path first passes an OSC 11 query to the outer
|
|
153
|
+
* terminal, waits briefly for tmux to consume the response into its cache,
|
|
154
|
+
* then reads that cache with a direct query. The outer query deliberately has
|
|
155
|
+
* no DA1 sentinel: multiplexers can decode a fragmented DA1 response as a key
|
|
156
|
+
* sequence and leak the remaining bytes into the editor. Startup and Mode 2031
|
|
157
|
+
* probes remain direct. Suppressed while inactive, headless, or after teardown.
|
|
158
|
+
*/
|
|
159
|
+
refreshAppearance(requestToken?: TerminalAppearanceRequestToken): TerminalAppearanceRequestToken | void;
|
|
160
|
+
onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
|
|
161
|
+
start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void): void;
|
|
162
|
+
drainInput(maxMs?: number, idleMs?: number): Promise<void>;
|
|
163
|
+
stop(): void;
|
|
164
|
+
write(data: string): void;
|
|
165
|
+
get columns(): number;
|
|
166
|
+
get rows(): number;
|
|
167
|
+
moveBy(lines: number): void;
|
|
168
|
+
hideCursor(force?: boolean): void;
|
|
169
|
+
showCursor(force?: boolean): void;
|
|
170
|
+
clearLine(): void;
|
|
171
|
+
clearFromCursor(): void;
|
|
172
|
+
clearScreen(): void;
|
|
173
|
+
setTitle(title: string): void;
|
|
174
|
+
setProgress(active: boolean): void;
|
|
175
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether the process is running inside a tmux session. */
|
|
2
|
+
export declare function isInsideTmux(env?: NodeJS.ProcessEnv): boolean;
|
|
3
|
+
/** Wrap a control sequence in tmux's DCS passthrough envelope. */
|
|
4
|
+
export declare function wrapTmuxPassthrough(payload: string): string;
|
|
5
|
+
/** Pass a control sequence through tmux, leaving direct-terminal output unchanged. */
|
|
6
|
+
export declare function wrapTmuxPassthroughIfNeeded(payload: string, env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Resolve the TTY device path for stdin (fd 0) via POSIX `ttyname(3)`. */
|
|
2
|
+
export declare function getTtyPath(): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Get a stable identifier for the current terminal.
|
|
5
|
+
* Uses the TTY device path (e.g., /dev/pts/3), falling back to environment
|
|
6
|
+
* variables for terminal multiplexers or terminal emulators.
|
|
7
|
+
* Returns null if no terminal can be identified (e.g., piped input).
|
|
8
|
+
*/
|
|
9
|
+
export declare function getTerminalId(): string | null;
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { ImageBudget } from "./components/image.js";
|
|
2
|
+
import { type Terminal } from "./terminal.js";
|
|
3
|
+
import { visibleWidth } from "./utils.js";
|
|
4
|
+
type InputListenerResult = {
|
|
5
|
+
consume?: boolean;
|
|
6
|
+
data?: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
type InputListener = (data: string) => InputListenerResult;
|
|
9
|
+
type StartListener = () => void;
|
|
10
|
+
export interface RenderTimer {
|
|
11
|
+
cancel(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface RenderScheduler {
|
|
14
|
+
now(): number;
|
|
15
|
+
scheduleImmediate(callback: () => void): void;
|
|
16
|
+
scheduleRender(callback: () => void, delayMs: number): RenderTimer;
|
|
17
|
+
}
|
|
18
|
+
export interface TUIOptions {
|
|
19
|
+
renderScheduler?: RenderScheduler;
|
|
20
|
+
}
|
|
21
|
+
export interface TUIStartOptions {
|
|
22
|
+
/** Clear saved native scrollback before the first paint. */
|
|
23
|
+
clearScrollback?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Component interface - all components must implement this
|
|
27
|
+
*
|
|
28
|
+
* Render contract: the returned array (and its rows) belongs to the component.
|
|
29
|
+
* Callers MUST NOT mutate it — components are allowed to return a cached array
|
|
30
|
+
* and will return the exact same reference for as long as their rendered
|
|
31
|
+
* content is unchanged. Conversely, a component MUST return a fresh array
|
|
32
|
+
* reference whenever its content changed; reference equality across two
|
|
33
|
+
* render() calls is the engine's proof that the rows are byte-identical
|
|
34
|
+
* (containers memoize their concatenation on it, and the TUI derives the
|
|
35
|
+
* frame's stable prefix from it). A component that mutates a previously
|
|
36
|
+
* returned array in place must implement {@link RenderStablePrefix} to declare
|
|
37
|
+
* which leading rows survived.
|
|
38
|
+
*/
|
|
39
|
+
export interface Component {
|
|
40
|
+
/**
|
|
41
|
+
* Render the component to an array of physical rows at the given width.
|
|
42
|
+
* The result is component-owned and `readonly` to the caller; an unchanged
|
|
43
|
+
* component may (and should) return the same array reference it returned
|
|
44
|
+
* last time.
|
|
45
|
+
*/
|
|
46
|
+
render(width: number): readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Optional handler for keyboard input when component has focus
|
|
49
|
+
*/
|
|
50
|
+
handleInput?(data: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* If true, component receives key release events (Kitty protocol).
|
|
53
|
+
* Default is false - release events are filtered out.
|
|
54
|
+
*/
|
|
55
|
+
wantsKeyRelease?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Optional hook to invalidate any cached rendering state.
|
|
58
|
+
* Called when theme changes or when component needs to re-render from scratch.
|
|
59
|
+
*/
|
|
60
|
+
invalidate?(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Optional hook to set whether this component ignores tight layout mode.
|
|
63
|
+
*/
|
|
64
|
+
setIgnoreTight?(ignore: boolean): any;
|
|
65
|
+
/**
|
|
66
|
+
* Optional teardown. Called when the component is permanently removed from
|
|
67
|
+
* the live tree (e.g. a transcript reset). Release timers, intervals, and
|
|
68
|
+
* subscriptions here. Must be idempotent. Containers propagate dispose to
|
|
69
|
+
* their children; leaf components without resources may omit it.
|
|
70
|
+
*/
|
|
71
|
+
dispose?(): void;
|
|
72
|
+
}
|
|
73
|
+
/** Lets an overlay root delegate keyboard focus to components it owns. */
|
|
74
|
+
export interface OverlayFocusOwner {
|
|
75
|
+
/** Returns true when `component` is a focus target inside this overlay. */
|
|
76
|
+
ownsOverlayFocusTarget(component: Component): boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Component seam for append-only native-scrollback commits. A component whose
|
|
80
|
+
* rendered rows can still change reports, after each render, the local line
|
|
81
|
+
* index where that mutable suffix begins. Rows above the boundary are declared
|
|
82
|
+
* FINAL — byte-stable at the current width for the component's lifetime — and
|
|
83
|
+
* commit to native scrollback as exact, audited content. Rows at/after the
|
|
84
|
+
* boundary repaint in place inside the visible window; when they scroll above
|
|
85
|
+
* the window top they normally commit as frozen visual snapshots.
|
|
86
|
+
*
|
|
87
|
+
* A viewport-pinned region opts out of those mutable snapshot commits. Its
|
|
88
|
+
* offscreen mutable rows are virtually clipped until the boundary advances;
|
|
89
|
+
* use this for fixed-height dashboards whose frames replace each other rather
|
|
90
|
+
* than append. A root that reports no seam commits everything that scrolls as
|
|
91
|
+
* final (shell semantics).
|
|
92
|
+
*
|
|
93
|
+
* When several root children report a seam in the same frame, the topmost one
|
|
94
|
+
* defines the boundary and pinning policy: commits are prefix-only, so
|
|
95
|
+
* everything below the first seam is already excluded.
|
|
96
|
+
*/
|
|
97
|
+
export interface NativeScrollbackLiveRegion {
|
|
98
|
+
getNativeScrollbackLiveRegionStart(): number | undefined;
|
|
99
|
+
/** Keeps the mutable suffix viewport-local instead of recording frozen snapshots. */
|
|
100
|
+
isNativeScrollbackLiveRegionPinned?(): boolean;
|
|
101
|
+
}
|
|
102
|
+
export interface NativeScrollbackCommittedRows {
|
|
103
|
+
setNativeScrollbackCommittedRows(rows: number): void;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Width-independent source boundary for multiplexer resize epochs. Capture
|
|
107
|
+
* reads the last rendered source state; resolve maps that same logical boundary
|
|
108
|
+
* into the most recent render's physical rows at its new width. The current
|
|
109
|
+
* boundary identifies the source tail after updates queued during the resize.
|
|
110
|
+
*/
|
|
111
|
+
export interface NativeScrollbackWidthEpoch {
|
|
112
|
+
captureNativeScrollbackWidthEpoch(): unknown;
|
|
113
|
+
resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined;
|
|
114
|
+
getNativeScrollbackWidthEpochRows(): number | undefined;
|
|
115
|
+
/** False when updates can insert before captured trailing rows. */
|
|
116
|
+
isNativeScrollbackWidthEpochAppendOnly?(boundary: unknown): boolean;
|
|
117
|
+
/** Changes when child structure mutates independently of width reflow. */
|
|
118
|
+
getNativeScrollbackWidthEpochRevision?(): number;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A component that discards rows after they enter native scrollback implements
|
|
122
|
+
* this hook so a destructive full replay can rehydrate its complete frame.
|
|
123
|
+
*/
|
|
124
|
+
export interface NativeScrollbackReplay {
|
|
125
|
+
prepareNativeScrollbackReplay(): void;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Opt-in stability report for components that mutate their returned render
|
|
129
|
+
* array in place across frames (instead of returning a fresh array per
|
|
130
|
+
* change). The engine reads it right after the component's `render()` returns:
|
|
131
|
+
* the report counts the leading rows of the just-returned array that are
|
|
132
|
+
* byte-identical to the array state the reader last observed. The engine uses
|
|
133
|
+
* it to reuse the composed frame's prefix — skipping marker extraction, line
|
|
134
|
+
* preparation, and the committed-prefix audit for those rows.
|
|
135
|
+
*
|
|
136
|
+
* Contract:
|
|
137
|
+
* - Reading CONSUMES the report: it re-bases the baseline to the current
|
|
138
|
+
* array state. The accumulated count therefore covers every render since
|
|
139
|
+
* the previous read, so out-of-band `render()` calls between engine frames
|
|
140
|
+
* (an exporter walking the tree) can only lower the report, never inflate
|
|
141
|
+
* it past what the engine actually has.
|
|
142
|
+
* - An implementer that cannot prove stability for a frame must lower the
|
|
143
|
+
* accumulated count to 0 for that render.
|
|
144
|
+
* - Rows at or beyond the report may have been mutated in place; rows before
|
|
145
|
+
* it must be the identical string values at the identical indices.
|
|
146
|
+
*/
|
|
147
|
+
export interface RenderStablePrefix {
|
|
148
|
+
getRenderStablePrefixRows(): number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Opt-in fast path for composing only the visible tail of a tall component
|
|
152
|
+
* during a terminal resize. A drag emits a SIGWINCH burst, and the width
|
|
153
|
+
* changes on every event: a full compose re-lays-out (and, for markdown,
|
|
154
|
+
* re-lexes) the entire transcript per event — O(history) work that is
|
|
155
|
+
* discarded the instant the next event arrives. While the resize is in flight
|
|
156
|
+
* the engine paints only the viewport, so it asks each tall root child for at
|
|
157
|
+
* most `maxRows` rows from the bottom of its render at `width` and skips
|
|
158
|
+
* composing everything above the fold. The authoritative full paint replays
|
|
159
|
+
* once the drag settles (see {@link TUI} resize handling).
|
|
160
|
+
*
|
|
161
|
+
* Contract:
|
|
162
|
+
* - Returns the BOTTOM rows of the component's full render at `width`, in
|
|
163
|
+
* top-to-bottom order, capped at `maxRows` (fewer when the component is
|
|
164
|
+
* shorter). The rows MUST be byte-identical to the corresponding tail of
|
|
165
|
+
* what `render(width)` would have returned, modulo a one-row separator at
|
|
166
|
+
* the very top edge (a transient frame the settle paint overwrites).
|
|
167
|
+
* - MUST NOT mutate any persistent full-compose state: the next `render()`
|
|
168
|
+
* (the settle paint) has to reconcile exactly as if the tail render never
|
|
169
|
+
* happened. Warming pure per-width render caches is fine and desirable.
|
|
170
|
+
*/
|
|
171
|
+
export interface ViewportTailProvider {
|
|
172
|
+
renderViewportTail(width: number, maxRows: number): readonly string[];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Interface for components that can receive focus and display a cursor.
|
|
176
|
+
* When focused, the component should emit CURSOR_MARKER at the cursor position
|
|
177
|
+
* in its render output. TUI will find this marker and position the hardware
|
|
178
|
+
* cursor there for proper IME candidate window positioning.
|
|
179
|
+
*
|
|
180
|
+
* Components that can switch between terminal-cursor and software-cursor
|
|
181
|
+
* rendering expose `setUseTerminalCursor`; TUI keeps that mode in sync with
|
|
182
|
+
* its resolved hardware-cursor preference whenever focus or the preference
|
|
183
|
+
* changes.
|
|
184
|
+
*/
|
|
185
|
+
export interface Focusable {
|
|
186
|
+
/** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */
|
|
187
|
+
focused: boolean;
|
|
188
|
+
/** Set by TUI when hardware cursor rendering is enabled or disabled. */
|
|
189
|
+
setUseTerminalCursor?(useTerminalCursor: boolean): void;
|
|
190
|
+
}
|
|
191
|
+
/** Options for scheduling a TUI render. */
|
|
192
|
+
export interface RenderRequestOptions {
|
|
193
|
+
/** Clear terminal scrollback for intentional transcript replacement. */
|
|
194
|
+
clearScrollback?: boolean;
|
|
195
|
+
}
|
|
196
|
+
/** Type guard to check if a component implements Focusable */
|
|
197
|
+
export declare function isFocusable(component: Component | null): component is Component & Focusable;
|
|
198
|
+
/**
|
|
199
|
+
* Cursor position marker - APC (Application Program Command) sequence.
|
|
200
|
+
* This is a zero-width escape sequence that terminals ignore.
|
|
201
|
+
* Components emit this at the cursor position when focused.
|
|
202
|
+
* TUI finds and strips this marker, then positions the hardware cursor there.
|
|
203
|
+
*/
|
|
204
|
+
export declare const CURSOR_MARKER = "\u001B_pi:c\u0007";
|
|
205
|
+
export { visibleWidth };
|
|
206
|
+
/**
|
|
207
|
+
* Anchor position for overlays
|
|
208
|
+
*/
|
|
209
|
+
export type OverlayAnchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center" | "left-center" | "right-center";
|
|
210
|
+
/**
|
|
211
|
+
* Margin configuration for overlays
|
|
212
|
+
*/
|
|
213
|
+
export interface OverlayMargin {
|
|
214
|
+
top?: number;
|
|
215
|
+
right?: number;
|
|
216
|
+
bottom?: number;
|
|
217
|
+
left?: number;
|
|
218
|
+
}
|
|
219
|
+
/** Value that can be absolute (number) or percentage (string like "50%") */
|
|
220
|
+
export type SizeValue = number | `${number}%`;
|
|
221
|
+
/**
|
|
222
|
+
* Options for overlay positioning and sizing.
|
|
223
|
+
* Values can be absolute numbers or percentage strings (e.g., "50%").
|
|
224
|
+
*/
|
|
225
|
+
export interface OverlayOptions {
|
|
226
|
+
/** Width in columns, or percentage of terminal width (e.g., "50%") */
|
|
227
|
+
width?: SizeValue;
|
|
228
|
+
/** Minimum width in columns */
|
|
229
|
+
minWidth?: number;
|
|
230
|
+
/** Maximum height in rows, or percentage of terminal height (e.g., "50%") */
|
|
231
|
+
maxHeight?: SizeValue;
|
|
232
|
+
/** Anchor point for positioning (default: 'center') */
|
|
233
|
+
anchor?: OverlayAnchor;
|
|
234
|
+
/** Horizontal offset from anchor position (positive = right) */
|
|
235
|
+
offsetX?: number;
|
|
236
|
+
/** Vertical offset from anchor position (positive = down) */
|
|
237
|
+
offsetY?: number;
|
|
238
|
+
/** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */
|
|
239
|
+
row?: SizeValue;
|
|
240
|
+
/** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */
|
|
241
|
+
col?: SizeValue;
|
|
242
|
+
/** Margin from terminal edges. Number applies to all sides. */
|
|
243
|
+
margin?: OverlayMargin | number;
|
|
244
|
+
/**
|
|
245
|
+
* Control overlay visibility based on terminal dimensions.
|
|
246
|
+
* If provided, overlay is only rendered when this returns true.
|
|
247
|
+
* Called each render cycle with current terminal dimensions.
|
|
248
|
+
*/
|
|
249
|
+
visible?: (termWidth: number, termHeight: number) => boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Borrow the terminal's alternate screen buffer for this overlay's lifetime
|
|
252
|
+
* (vim/less idiom). While the topmost visible overlay sets this, the engine
|
|
253
|
+
* paints only the modal on the alt screen and emits no ED3 / scrollback
|
|
254
|
+
* bytes, so the transcript on the normal screen stays untouched and is not
|
|
255
|
+
* scrollable behind the modal. Defaults off — all other overlays are
|
|
256
|
+
* unchanged and still draw over the transcript on the normal screen.
|
|
257
|
+
*/
|
|
258
|
+
fullscreen?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Enable terminal mouse reporting while fullscreen. Defaults on; disable it
|
|
261
|
+
* when native terminal text selection takes precedence over pointer events.
|
|
262
|
+
*/
|
|
263
|
+
mouseTracking?: boolean;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Handle returned by showOverlay for controlling the overlay
|
|
267
|
+
*/
|
|
268
|
+
export interface OverlayHandle {
|
|
269
|
+
/** Permanently remove the overlay (cannot be shown again) */
|
|
270
|
+
hide(): void;
|
|
271
|
+
/** Temporarily hide or show the overlay */
|
|
272
|
+
setHidden(hidden: boolean): void;
|
|
273
|
+
/** Check if overlay is temporarily hidden */
|
|
274
|
+
isHidden(): boolean;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Container - a component that contains other components
|
|
278
|
+
*/
|
|
279
|
+
export declare class Container implements Component, NativeScrollbackCommittedRows, NativeScrollbackReplay, NativeScrollbackWidthEpoch {
|
|
280
|
+
#private;
|
|
281
|
+
children: Component[];
|
|
282
|
+
setIgnoreTight(ignore: boolean): this;
|
|
283
|
+
addChild(component: Component): void;
|
|
284
|
+
removeChild(component: Component): void;
|
|
285
|
+
clear(): void;
|
|
286
|
+
/** Dispose every child, then detach it from this container. */
|
|
287
|
+
disposeChildren(): void;
|
|
288
|
+
invalidate(): void;
|
|
289
|
+
/**
|
|
290
|
+
* Propagate teardown to children. Call when the container's children are
|
|
291
|
+
* being permanently discarded (not when they are detached for reuse — use
|
|
292
|
+
* {@link clear} for that). Idempotent per child via each child's own dispose.
|
|
293
|
+
*/
|
|
294
|
+
dispose(): void;
|
|
295
|
+
/**
|
|
296
|
+
* Split the committed prefix from the container's most recently rendered
|
|
297
|
+
* rows across its children. The memoized child arrays are the exact geometry
|
|
298
|
+
* that produced that frame; when the child list was invalidated or rebuilt,
|
|
299
|
+
* there is no safe old-to-new coordinate mapping, so propagation waits for
|
|
300
|
+
* the next render/post-emit publication.
|
|
301
|
+
*/
|
|
302
|
+
setNativeScrollbackCommittedRows(rows: number): void;
|
|
303
|
+
/** Recursively discard layout locks that are meaningful only to the old tape. */
|
|
304
|
+
prepareNativeScrollbackReplay(): void;
|
|
305
|
+
captureNativeScrollbackWidthEpoch(): unknown;
|
|
306
|
+
resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined;
|
|
307
|
+
getNativeScrollbackWidthEpochRows(): number | undefined;
|
|
308
|
+
isNativeScrollbackWidthEpochAppendOnly(boundary: unknown): boolean;
|
|
309
|
+
getNativeScrollbackWidthEpochRevision(): number;
|
|
310
|
+
render(width: number): readonly string[];
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Merge runs of byte-adjacent SGR sequences (`CSI [0-9;:]* m`) into one. Only
|
|
314
|
+
* CSI-SGR sequences are touched; text, cursor moves, OSC, hyperlinks and image
|
|
315
|
+
* payloads pass through verbatim. Returns the original reference when nothing
|
|
316
|
+
* merges, so SGR-light lines incur only a single `indexOf` scan.
|
|
317
|
+
*/
|
|
318
|
+
export declare function coalesceAdjacentSgr(line: string): string;
|
|
319
|
+
/**
|
|
320
|
+
* Decide whether `frame` still aligns with the committed prefix, and where to
|
|
321
|
+
* re-anchor the commit index when it does not. Returns the resync row index,
|
|
322
|
+
* or -1 when no resync is needed.
|
|
323
|
+
*
|
|
324
|
+
* Zones (verifiedTo ≤ finalTo ≤ prefix.length):
|
|
325
|
+
* [0, verifiedTo) VERIFIED exact rows — sampled with tolerance.
|
|
326
|
+
* [verifiedTo, finalTo) NEWLY-FINAL rows — frozen visual snapshots whose
|
|
327
|
+
* source just became declared-final (the block finalized / a barrier
|
|
328
|
+
* cleared). Hard-scanned in FULL with no tolerance: any content change
|
|
329
|
+
* (a pending header settling, a preview replaced by its result, a tail
|
|
330
|
+
* shifting up after a barrier removal) re-anchors so the engine can
|
|
331
|
+
* erase-and-replay history with the final content exactly once (or, on
|
|
332
|
+
* ED3-unsafe multiplexers, recommit it below the frozen snapshot —
|
|
333
|
+
* duplication, never loss) instead of committing it nowhere and
|
|
334
|
+
* painting it nowhere.
|
|
335
|
+
* [finalTo, prefix.length) FROZEN visual snapshots of still-live rows —
|
|
336
|
+
* exempt: their drift is expected (a collapsing preview, a ticking
|
|
337
|
+
* progress tree) and must never spray re-anchors mid-run.
|
|
338
|
+
*
|
|
339
|
+
* The verified zone's sampled check exploits the asymmetry between the two
|
|
340
|
+
* mutation classes: an in-place edit/restyle disturbs only the touched rows
|
|
341
|
+
* (alignment below stays intact; the stale copy in history is the accepted
|
|
342
|
+
* artifact), while an insertion/deletion shifts EVERY row below it. Up to 8
|
|
343
|
+
* non-blank rows within the last 24 verified rows are compared SGR-stripped
|
|
344
|
+
* (theme changes stay quiet), tolerating a SINGLE mismatch. The tolerance is
|
|
345
|
+
* load-bearing for roots that report NO seam: an animated row already in
|
|
346
|
+
* history would otherwise re-anchor on every glyph tick.
|
|
347
|
+
*
|
|
348
|
+
* Highly repetitive tails (identical filler rows) can mask a shift in the tail
|
|
349
|
+
* sample, in which case the skipped rows are content-identical to the committed
|
|
350
|
+
* ones — observationally harmless. Exported for the render-stress harness, whose
|
|
351
|
+
* shadow commit ledger must mirror the engine's law exactly.
|
|
352
|
+
*/
|
|
353
|
+
export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[], verifiedTo?: number, finalTo?: number): number;
|
|
354
|
+
/**
|
|
355
|
+
* TUI - Main class for managing terminal UI with differential rendering
|
|
356
|
+
*/
|
|
357
|
+
export declare class TUI extends Container {
|
|
358
|
+
#private;
|
|
359
|
+
terminal: Terminal;
|
|
360
|
+
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
361
|
+
onDebug?: () => void;
|
|
362
|
+
overlayStack: {
|
|
363
|
+
component: Component;
|
|
364
|
+
options?: OverlayOptions;
|
|
365
|
+
preFocus: Component | null;
|
|
366
|
+
hidden: boolean;
|
|
367
|
+
}[];
|
|
368
|
+
constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: TUIOptions);
|
|
369
|
+
captureNativeScrollbackWidthEpoch(): unknown;
|
|
370
|
+
resolveNativeScrollbackWidthEpoch(boundary: unknown): number | undefined;
|
|
371
|
+
getNativeScrollbackWidthEpochRows(): number | undefined;
|
|
372
|
+
render(width: number): readonly string[];
|
|
373
|
+
get fullRedraws(): number;
|
|
374
|
+
/**
|
|
375
|
+
* Transient viewport-only paints emitted by the non-multiplexer resize fast
|
|
376
|
+
* path. These never touch native scrollback or the commit ledger, so they
|
|
377
|
+
* are counted apart from {@link fullRedraws}.
|
|
378
|
+
*/
|
|
379
|
+
get resizeViewportPaints(): number;
|
|
380
|
+
/** Whether a non-multiplexer resize drag is currently in flight. */
|
|
381
|
+
get resizeViewportActive(): boolean;
|
|
382
|
+
/** Shared budget that caps how many inline images render as live graphics. */
|
|
383
|
+
get imageBudget(): ImageBudget;
|
|
384
|
+
/**
|
|
385
|
+
* Set how many inline images stay live graphics before older ones fall back
|
|
386
|
+
* to text (`0` disables the cap). Older images are hidden via a graphics purge
|
|
387
|
+
* plus a full redraw on the frame after a new image exceeds the cap.
|
|
388
|
+
*/
|
|
389
|
+
setMaxInlineImages(cap: number): void;
|
|
390
|
+
/** Delete every tracked Kitty image from the terminal graphics store. */
|
|
391
|
+
clearInlineImages(): void;
|
|
392
|
+
/**
|
|
393
|
+
* Get whether scrollback divergence rebuild is enabled.
|
|
394
|
+
*/
|
|
395
|
+
getScrollbackRebuild(): boolean;
|
|
396
|
+
/**
|
|
397
|
+
* Enable or disable scrollback divergence rebuild (default off).
|
|
398
|
+
* When enabled, the engine will erase and replay the terminal's
|
|
399
|
+
* scrollback (using ED3 / alt buffer / scrollback replay) to avoid
|
|
400
|
+
* duplicate blocks when a block's final form replaces its live preview.
|
|
401
|
+
*/
|
|
402
|
+
setScrollbackRebuild(enabled: boolean): void;
|
|
403
|
+
getShowHardwareCursor(): boolean;
|
|
404
|
+
setShowHardwareCursor(enabled: boolean): void;
|
|
405
|
+
/**
|
|
406
|
+
* Whether DEC 2026 synchronized-output wrappers are currently emitted around
|
|
407
|
+
* paints. Starts from conservative terminal/env detection and is reconciled at
|
|
408
|
+
* runtime against the terminal's DECRQM mode-2026 report — enabled on a
|
|
409
|
+
* positive report, disabled on a negative one.
|
|
410
|
+
*/
|
|
411
|
+
get synchronizedOutput(): boolean;
|
|
412
|
+
setFocus(component: Component | null): void;
|
|
413
|
+
/** Component currently receiving keyboard input, if any. */
|
|
414
|
+
getFocused(): Component | null;
|
|
415
|
+
/**
|
|
416
|
+
* Show an overlay component with configurable positioning and sizing.
|
|
417
|
+
* Returns a handle to control the overlay's visibility.
|
|
418
|
+
*/
|
|
419
|
+
showOverlay(component: Component, options?: OverlayOptions): OverlayHandle;
|
|
420
|
+
/** Hide the topmost overlay and restore previous focus. */
|
|
421
|
+
hideOverlay(): void;
|
|
422
|
+
/** Check if there are any visible overlays */
|
|
423
|
+
hasOverlay(): boolean;
|
|
424
|
+
invalidate(): void;
|
|
425
|
+
start(options?: TUIStartOptions): void;
|
|
426
|
+
addStartListener(listener: StartListener): () => void;
|
|
427
|
+
addInputListener(listener: InputListener): () => void;
|
|
428
|
+
removeInputListener(listener: InputListener): void;
|
|
429
|
+
stop(): void;
|
|
430
|
+
/**
|
|
431
|
+
* Force an immediate full replay of the current frame, including native
|
|
432
|
+
* scrollback. This is the keyboard-accessible equivalent of the resize reset:
|
|
433
|
+
* no queued diff frame or terminal scrollback probe can downgrade it to a
|
|
434
|
+
* viewport-only repaint.
|
|
435
|
+
*
|
|
436
|
+
* Invalidates every component first so the replay reflects current state. A
|
|
437
|
+
* geometry-driven reset thaws frozen scrollback snapshots implicitly (the new
|
|
438
|
+
* width misses every cached snapshot), but a same-width reset would otherwise
|
|
439
|
+
* replay stale snapshots — leaving host-frozen blocks (e.g. a transcript whose
|
|
440
|
+
* committed rows are immutable on ED3-risk terminals) showing pre-mutation
|
|
441
|
+
* content. Invalidation is the generic signal those containers use to retire
|
|
442
|
+
* their snapshots, which is exactly what a user-driven display reset wants.
|
|
443
|
+
*/
|
|
444
|
+
resetDisplay(): void;
|
|
445
|
+
requestRender(force?: boolean, options?: RenderRequestOptions): void;
|
|
446
|
+
/**
|
|
447
|
+
* Opt `component` into subtree-only renders when input leaves focus stable.
|
|
448
|
+
*
|
|
449
|
+
* The host must explicitly request renders for every sibling mutated by the
|
|
450
|
+
* component's input callbacks. Components without this opt-in retain the
|
|
451
|
+
* legacy full-root render after input.
|
|
452
|
+
*/
|
|
453
|
+
enableScopedInputRender(component: Component): void;
|
|
454
|
+
/**
|
|
455
|
+
* Schedule a render on behalf of `component` after a self-contained change
|
|
456
|
+
* (spinner frame, blink) that cannot have affected any other component.
|
|
457
|
+
*
|
|
458
|
+
* When every request since the last frame is component-scoped and the
|
|
459
|
+
* frame is otherwise quiet — no resize or geometry change, no overlays, no
|
|
460
|
+
* live inline images, no forced repaint, unchanged root child list — the
|
|
461
|
+
* next compose re-renders only the root subtrees containing the requesting
|
|
462
|
+
* components and reuses the previous frame's rows (and seam reports) for
|
|
463
|
+
* every other root child, skipping the full component-tree walk that makes
|
|
464
|
+
* long transcripts expensive to repaint at animation rate. Any concurrent
|
|
465
|
+
* full request or unsafe condition downgrades the frame to a normal full
|
|
466
|
+
* compose, so this is never less correct than `requestRender()` — only
|
|
467
|
+
* cheaper.
|
|
468
|
+
*/
|
|
469
|
+
requestComponentRender(component: Component): void;
|
|
470
|
+
/**
|
|
471
|
+
* Rewrite a quiet, visible component segment directly.
|
|
472
|
+
*
|
|
473
|
+
* Loader-style animation changes one already-positioned segment at a fixed
|
|
474
|
+
* size. When the current frame geometry is still valid, rewrite just those
|
|
475
|
+
* rows and update the diff baseline instead of scheduling a full render
|
|
476
|
+
* cycle. Unsafe states fall back to `requestComponentRender()`, preserving
|
|
477
|
+
* the ordinary renderer as the correctness path.
|
|
478
|
+
*/
|
|
479
|
+
requestDirectWrite(component: Component): void;
|
|
480
|
+
}
|