@sayknow-cli/tui 0.3.13 → 0.3.15
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/dist/types/animation-scheduler.d.ts +13 -0
- package/dist/types/autocomplete.d.ts +83 -0
- package/dist/types/bracketed-paste.d.ts +26 -0
- package/dist/types/components/box.d.ts +20 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +126 -0
- package/dist/types/components/image.d.ts +18 -0
- package/dist/types/components/input.d.ts +16 -0
- package/dist/types/components/loader.d.ts +23 -0
- package/dist/types/components/markdown.d.ts +87 -0
- package/dist/types/components/sayknow-pet.d.ts +128 -0
- package/dist/types/components/select-list.d.ts +46 -0
- package/dist/types/components/settings-list.d.ts +39 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +56 -0
- package/dist/types/components/text.d.ts +22 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/editor-component.d.ts +36 -0
- package/dist/types/fuzzy.d.ts +15 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/keybindings.d.ts +201 -0
- package/dist/types/keys.d.ts +208 -0
- package/dist/types/kill-ring.d.ts +27 -0
- package/dist/types/metrics.d.ts +85 -0
- package/dist/types/stdin-buffer.d.ts +50 -0
- package/dist/types/symbols.d.ts +23 -0
- package/dist/types/terminal-capabilities.d.ts +187 -0
- package/dist/types/terminal.d.ts +90 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +269 -0
- package/dist/types/utils.d.ts +110 -0
- package/package.json +9 -8
- package/src/terminal-capabilities.ts +77 -4
- package/src/tui.ts +50 -21
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Terminal } from "./terminal";
|
|
2
|
+
import { visibleWidth } from "./utils";
|
|
3
|
+
type InputListenerResult = {
|
|
4
|
+
consume?: boolean;
|
|
5
|
+
data?: string;
|
|
6
|
+
} | undefined;
|
|
7
|
+
type InputListener = (data: string) => InputListenerResult;
|
|
8
|
+
/**
|
|
9
|
+
* Component interface - all components must implement this
|
|
10
|
+
*/
|
|
11
|
+
export interface Component {
|
|
12
|
+
/**
|
|
13
|
+
* Render the component to lines for the given viewport width
|
|
14
|
+
* @param width - Current viewport width
|
|
15
|
+
* @returns Array of strings, each representing a line
|
|
16
|
+
*/
|
|
17
|
+
render(width: number): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Optional handler for keyboard input when component has focus
|
|
20
|
+
*/
|
|
21
|
+
handleInput?(data: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* If true, component receives key release events (Kitty protocol).
|
|
24
|
+
* Default is false - release events are filtered out.
|
|
25
|
+
*/
|
|
26
|
+
wantsKeyRelease?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Invalidate any cached rendering state.
|
|
29
|
+
* Called when theme changes or when component needs to re-render from scratch.
|
|
30
|
+
*/
|
|
31
|
+
invalidate(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Optional cleanup hook. Called once when the component is permanently
|
|
34
|
+
* removed from the tree via removeChild/clear/dispose. Implementations MUST
|
|
35
|
+
* be idempotent. Components meant to be re-added should be detached, not
|
|
36
|
+
* removed/cleared.
|
|
37
|
+
*/
|
|
38
|
+
dispose?(): void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Interface for components that can receive focus and display a hardware cursor.
|
|
42
|
+
* When focused, the component should emit CURSOR_MARKER at the cursor position
|
|
43
|
+
* in its render output. TUI will find this marker and position the hardware
|
|
44
|
+
* cursor there for proper IME candidate window positioning.
|
|
45
|
+
*/
|
|
46
|
+
export interface Focusable {
|
|
47
|
+
/** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */
|
|
48
|
+
focused: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Type guard to check if a component implements Focusable */
|
|
51
|
+
export declare function isFocusable(component: Component | null): component is Component & Focusable;
|
|
52
|
+
/**
|
|
53
|
+
* Cursor position marker - APC (Application Program Command) sequence.
|
|
54
|
+
* This is a zero-width escape sequence that terminals ignore.
|
|
55
|
+
* Components emit this at the cursor position when focused.
|
|
56
|
+
* TUI finds and strips this marker, then positions the hardware cursor there.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CURSOR_MARKER = "\u001B_pi:c\u0007";
|
|
59
|
+
export { visibleWidth };
|
|
60
|
+
/** Durable source identifier for a semantically anchored viewport row. */
|
|
61
|
+
export type ViewportAnchorId = string;
|
|
62
|
+
export interface ViewportAnchorRow {
|
|
63
|
+
id: ViewportAnchorId;
|
|
64
|
+
graphemeStart: number;
|
|
65
|
+
graphemeEnd: number;
|
|
66
|
+
cellStart: number;
|
|
67
|
+
cellEnd: number;
|
|
68
|
+
}
|
|
69
|
+
export interface ViewportAnchorRender {
|
|
70
|
+
lines: string[];
|
|
71
|
+
anchors: Array<ViewportAnchorRow | null>;
|
|
72
|
+
}
|
|
73
|
+
export interface ViewportAnchorProvider extends Component {
|
|
74
|
+
renderWithViewportAnchors(width: number): ViewportAnchorRender;
|
|
75
|
+
}
|
|
76
|
+
export interface ViewportAnchorSource {
|
|
77
|
+
id: ViewportAnchorId;
|
|
78
|
+
}
|
|
79
|
+
export interface ViewportAnchorSourceRenderer extends Component {
|
|
80
|
+
renderWithViewportAnchorSource(width: number, source: ViewportAnchorSource): ViewportAnchorRender;
|
|
81
|
+
}
|
|
82
|
+
export declare function isViewportAnchorProvider(component: Component): component is ViewportAnchorProvider;
|
|
83
|
+
export declare function isViewportAnchorSourceRenderer(component: Component): component is ViewportAnchorSourceRenderer;
|
|
84
|
+
export declare function renderComponentWithViewportAnchors(component: Component, width: number): ViewportAnchorRender;
|
|
85
|
+
export declare function renderComponentWithViewportAnchorSource(component: Component, width: number, source: ViewportAnchorSource): ViewportAnchorRender;
|
|
86
|
+
/**
|
|
87
|
+
* Anchor position for overlays
|
|
88
|
+
*/
|
|
89
|
+
export type OverlayAnchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center" | "left-center" | "right-center";
|
|
90
|
+
/**
|
|
91
|
+
* Margin configuration for overlays
|
|
92
|
+
*/
|
|
93
|
+
export interface OverlayMargin {
|
|
94
|
+
top?: number;
|
|
95
|
+
right?: number;
|
|
96
|
+
bottom?: number;
|
|
97
|
+
left?: number;
|
|
98
|
+
}
|
|
99
|
+
/** Value that can be absolute (number) or percentage (string like "50%") */
|
|
100
|
+
export type SizeValue = number | `${number}%`;
|
|
101
|
+
/**
|
|
102
|
+
* Startup sixel capability probe policy (pure; exported for tests):
|
|
103
|
+
* - Never probe when PI_FORCE_IMAGE_PROTOCOL is set — an explicit
|
|
104
|
+
* configuration (including "off") is authoritative.
|
|
105
|
+
* - Never probe inside a terminal multiplexer: tmux advertises DA1 ";4"
|
|
106
|
+
* whenever it was compiled with sixel support, regardless of whether the
|
|
107
|
+
* attached client terminal can render sixel, so a positive reply is not
|
|
108
|
+
* end-to-end evidence. Graphics under a multiplexer are strictly opt-in
|
|
109
|
+
* via PI_FORCE_IMAGE_PROTOCOL=sixel.
|
|
110
|
+
* - Probe Windows Terminal (>=1.22 renders sixel but exposes no env marker).
|
|
111
|
+
*/
|
|
112
|
+
export declare function shouldProbeSixelCapability(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* True when repainting only the live viewport is safer than clearing/replaying
|
|
115
|
+
* the full transcript. Native Windows console hosts are included even when
|
|
116
|
+
* WT_SESSION is absent because PowerShell/ConPTY launch chains can drop terminal
|
|
117
|
+
* identity variables while keeping the same scroll-jump behavior.
|
|
118
|
+
*/
|
|
119
|
+
export declare function shouldUseViewportRepaintForHost(env?: Record<string, string | undefined>, platform?: NodeJS.Platform, options?: {
|
|
120
|
+
includeNativeWindows?: boolean;
|
|
121
|
+
}): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Options for overlay positioning and sizing.
|
|
124
|
+
* Values can be absolute numbers or percentage strings (e.g., "50%").
|
|
125
|
+
*/
|
|
126
|
+
export interface OverlayOptions {
|
|
127
|
+
/** Width in columns, or percentage of terminal width (e.g., "50%") */
|
|
128
|
+
width?: SizeValue;
|
|
129
|
+
/** Minimum width in columns */
|
|
130
|
+
minWidth?: number;
|
|
131
|
+
/** Maximum height in rows, or percentage of terminal height (e.g., "50%") */
|
|
132
|
+
maxHeight?: SizeValue;
|
|
133
|
+
/** Anchor point for positioning (default: 'center') */
|
|
134
|
+
anchor?: OverlayAnchor;
|
|
135
|
+
/** Horizontal offset from anchor position (positive = right) */
|
|
136
|
+
offsetX?: number;
|
|
137
|
+
/** Vertical offset from anchor position (positive = down) */
|
|
138
|
+
offsetY?: number;
|
|
139
|
+
/** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */
|
|
140
|
+
row?: SizeValue;
|
|
141
|
+
/** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */
|
|
142
|
+
col?: SizeValue;
|
|
143
|
+
/** Margin from terminal edges. Number applies to all sides. */
|
|
144
|
+
margin?: OverlayMargin | number;
|
|
145
|
+
/**
|
|
146
|
+
* Control overlay visibility based on terminal dimensions.
|
|
147
|
+
* If provided, overlay is only rendered when this returns true.
|
|
148
|
+
* Called each render cycle with current terminal dimensions.
|
|
149
|
+
*/
|
|
150
|
+
visible?: (termWidth: number, termHeight: number) => boolean;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle returned by showOverlay for controlling the overlay
|
|
154
|
+
*/
|
|
155
|
+
export interface OverlayHandle {
|
|
156
|
+
/** Permanently remove the overlay (cannot be shown again) */
|
|
157
|
+
hide(): void;
|
|
158
|
+
/** Temporarily hide or show the overlay */
|
|
159
|
+
setHidden(hidden: boolean): void;
|
|
160
|
+
/** Check if overlay is temporarily hidden */
|
|
161
|
+
isHidden(): boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Container - a component that contains other components
|
|
165
|
+
*/
|
|
166
|
+
export declare class Container implements ViewportAnchorProvider {
|
|
167
|
+
#private;
|
|
168
|
+
children: Component[];
|
|
169
|
+
addChild(component: Component): void;
|
|
170
|
+
removeChild(component: Component): void;
|
|
171
|
+
/** Remove a child without disposing it (for detach-then-readd reuse). */
|
|
172
|
+
detachChild(component: Component): void;
|
|
173
|
+
clear(): void;
|
|
174
|
+
/** Remove all children without disposing them (for detach-then-readd reuse). */
|
|
175
|
+
detachAll(): void;
|
|
176
|
+
/** Registers a direct child as eligible for semantic viewport anchoring. */
|
|
177
|
+
setViewportAnchorSource(component: Component, source: ViewportAnchorSource | null): void;
|
|
178
|
+
dispose(): void;
|
|
179
|
+
invalidate(): void;
|
|
180
|
+
render(width: number): string[];
|
|
181
|
+
renderWithViewportAnchors(width: number): ViewportAnchorRender;
|
|
182
|
+
}
|
|
183
|
+
type TuiRenderCounterSnapshot = {
|
|
184
|
+
debugRedrawEnvReads: number;
|
|
185
|
+
debugRedrawAppendWrites: number;
|
|
186
|
+
differentialGuardVisibleWidthCalls: number;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* TUI - Main class for managing terminal UI with differential rendering
|
|
190
|
+
*/
|
|
191
|
+
export declare class TUI extends Container {
|
|
192
|
+
#private;
|
|
193
|
+
terminal: Terminal;
|
|
194
|
+
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
195
|
+
onDebug?: () => void;
|
|
196
|
+
static resetRenderCountersForTest(): void;
|
|
197
|
+
static getRenderCountersForTest(): TuiRenderCounterSnapshot;
|
|
198
|
+
overlayStack: {
|
|
199
|
+
component: Component;
|
|
200
|
+
options?: OverlayOptions;
|
|
201
|
+
preFocus: Component | null;
|
|
202
|
+
hidden: boolean;
|
|
203
|
+
}[];
|
|
204
|
+
constructor(terminal: Terminal, showHardwareCursor?: boolean);
|
|
205
|
+
dispose(): void;
|
|
206
|
+
get fullRedraws(): number;
|
|
207
|
+
getShowHardwareCursor(): boolean;
|
|
208
|
+
setShowHardwareCursor(enabled: boolean): void;
|
|
209
|
+
getClearOnShrink(): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Set whether to trigger full re-render when content shrinks.
|
|
212
|
+
* When true (default), empty rows are cleared when content shrinks.
|
|
213
|
+
* When false, empty rows remain (reduces redraws on slower terminals).
|
|
214
|
+
*/
|
|
215
|
+
setClearOnShrink(enabled: boolean): void;
|
|
216
|
+
setFocus(component: Component | null): void;
|
|
217
|
+
setBottomPinnedComponent(component: Component | null): void;
|
|
218
|
+
/** Register the direct child whose rows are eligible for semantic viewport anchoring. */
|
|
219
|
+
setViewportAnchorComponent(component: Component | null): void;
|
|
220
|
+
/** Clear manual viewport ownership before replacing the transcript identity namespace. */
|
|
221
|
+
resetViewportAnchorIntent(): void;
|
|
222
|
+
/** Allow one semantic-neighbor reconciliation after a definitive same-transcript rebuild. */
|
|
223
|
+
prepareViewportAnchorForTranscriptRebuild(): void;
|
|
224
|
+
scrollViewportPages(direction: -1 | 1): boolean;
|
|
225
|
+
followLiveViewport(): boolean;
|
|
226
|
+
/**
|
|
227
|
+
* Show an overlay component with configurable positioning and sizing.
|
|
228
|
+
* Returns a handle to control the overlay's visibility.
|
|
229
|
+
*/
|
|
230
|
+
showOverlay(component: Component, options?: OverlayOptions): OverlayHandle;
|
|
231
|
+
/** Hide the topmost overlay and restore previous focus. */
|
|
232
|
+
hideOverlay(): void;
|
|
233
|
+
/** Check if there are any visible overlays */
|
|
234
|
+
hasOverlay(): boolean;
|
|
235
|
+
invalidate(): void;
|
|
236
|
+
start(): void;
|
|
237
|
+
get terminalAvailable(): boolean;
|
|
238
|
+
addInputListener(listener: InputListener): () => void;
|
|
239
|
+
removeInputListener(listener: InputListener): void;
|
|
240
|
+
stop(): void;
|
|
241
|
+
/**
|
|
242
|
+
* Viewport-repaint-aware resize render request.
|
|
243
|
+
*
|
|
244
|
+
* A forced full redraw (`requestRender(true)`) resets `#previousWidth`/`#previousHeight`
|
|
245
|
+
* to -1, which makes `#doRender` treat the frame as a width change and fall into the
|
|
246
|
+
* `fullRender` path. In terminal multiplexers that path skips the scrollback-clearing
|
|
247
|
+
* `3J` escape (users navigate scrollback history), so replaying every transcript line
|
|
248
|
+
* piles it back on top of scrollback — the "top of screen scrolls down to the prompt at
|
|
249
|
+
* high speed" resize storm. Windows Terminal/ConPTY can also visibly jump to
|
|
250
|
+
* the transcript top during streaming redraws, so viewport-repaint sessions
|
|
251
|
+
* keep force off and let `#doRender` repaint only the live viewport. Set
|
|
252
|
+
* `PI_TUI_LEGACY_MULTIPLEXER_FULL_RENDER=1` to restore the legacy tmux redraw.
|
|
253
|
+
*/
|
|
254
|
+
requestResizeRender(): void;
|
|
255
|
+
requestRender(force?: boolean, source?: string): void;
|
|
256
|
+
getLineRenderCacheStats(): {
|
|
257
|
+
normalizationSize: number;
|
|
258
|
+
truncationSize: number;
|
|
259
|
+
normalizationLimit: number;
|
|
260
|
+
truncationLimit: number;
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Register an emitter whose escape payload is appended to every render
|
|
264
|
+
* write (inside its own synchronized-output block, cursor saved/restored).
|
|
265
|
+
* Used for absolute-positioned overlays such as pixel-image pets that live
|
|
266
|
+
* outside the line-based component model. Return null to emit nothing.
|
|
267
|
+
*/
|
|
268
|
+
setPostRenderEmitter(emitter: (() => string | null) | undefined): void;
|
|
269
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@sayknow-cli/natives";
|
|
2
|
+
export { Ellipsis } from "@sayknow-cli/natives";
|
|
3
|
+
export { getDefaultTabWidth, getIndentation } from "@sayknow-cli/utils";
|
|
4
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
5
|
+
export declare const __textHelperPerfCounters: {
|
|
6
|
+
truncateToWidthCalls: number;
|
|
7
|
+
wrapTextWithAnsiCalls: number;
|
|
8
|
+
truncateLinesToWidthCalls: number;
|
|
9
|
+
visibleWidthsCalls: number;
|
|
10
|
+
reset(): void;
|
|
11
|
+
};
|
|
12
|
+
export declare function invalidateTabWidthCache(): void;
|
|
13
|
+
export declare function isPrintableAscii(text: string): boolean;
|
|
14
|
+
export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
|
|
15
|
+
export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string;
|
|
16
|
+
export declare function truncateLinesToWidth(lines: readonly string[], maxWidth: number, ellipsisKind?: Ellipsis | null, pad?: boolean | null): string[];
|
|
17
|
+
export declare function wrapTextWithAnsi(text: string, width: number): string[];
|
|
18
|
+
export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter: boolean): ExtractSegmentsResult;
|
|
19
|
+
/**
|
|
20
|
+
* Tab width in columns for `file`, using `process.cwd()` as the project root for relative paths.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getIndentationNoescape(file?: string): number;
|
|
23
|
+
export declare function replaceTabs(text: string, file?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Returns a string of n spaces. Uses a pre-allocated buffer for efficiency.
|
|
26
|
+
*/
|
|
27
|
+
export declare function padding(n: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Get the shared grapheme segmenter instance.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getSegmenter(): Intl.Segmenter;
|
|
32
|
+
export interface ViewportAnchorSpan {
|
|
33
|
+
graphemeStart: number;
|
|
34
|
+
graphemeEnd: number;
|
|
35
|
+
cellStart: number;
|
|
36
|
+
cellEnd: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ViewportAnchorAnnotation {
|
|
39
|
+
text: string;
|
|
40
|
+
nextGrapheme: number;
|
|
41
|
+
nextCell: number;
|
|
42
|
+
token: string;
|
|
43
|
+
}
|
|
44
|
+
export declare const VIEWPORT_ANCHOR_PREFIX = "\u001B_ASKC_ANCHOR:";
|
|
45
|
+
/**
|
|
46
|
+
* Tag every visible grapheme with an APC marker that survives ANSI-aware
|
|
47
|
+
* wrapping. The marker contains source grapheme and monotonic cell offsets.
|
|
48
|
+
*/
|
|
49
|
+
export declare function annotateViewportAnchorGraphemes(text: string, startGrapheme?: number, startCell?: number, token?: `${string}-${string}-${string}-${string}-${string}`): ViewportAnchorAnnotation;
|
|
50
|
+
/** Remove viewport anchor markers and return the exact marked span for each row. */
|
|
51
|
+
export declare function extractViewportAnchorRows(lines: readonly string[], token: string): {
|
|
52
|
+
lines: string[];
|
|
53
|
+
spans: Array<ViewportAnchorSpan | null>;
|
|
54
|
+
};
|
|
55
|
+
export declare function visibleWidthRaw(str: string): number;
|
|
56
|
+
/**
|
|
57
|
+
* Calculate the visible width of a string in terminal columns.
|
|
58
|
+
*/
|
|
59
|
+
export declare function visibleWidth(str: string): number;
|
|
60
|
+
export declare function visibleWidthsNative(lines: readonly string[]): number[];
|
|
61
|
+
export declare function visibleWidths(lines: readonly string[]): number[];
|
|
62
|
+
/**
|
|
63
|
+
* Normalize text for terminal output without changing logical editor content.
|
|
64
|
+
* Some terminals render canonically decomposed Hangul jamo or precomposed
|
|
65
|
+
* Thai/Lao AM vowels inconsistently during differential repaint. Emit a stable
|
|
66
|
+
* terminal form while keeping the component/source strings unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeTerminalOutput(str: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a character is whitespace.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isWhitespaceChar(char: string): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Check if a character is punctuation.
|
|
75
|
+
*/
|
|
76
|
+
export declare function isPunctuationChar(char: string): boolean;
|
|
77
|
+
export type WordNavKind = "whitespace" | "delimiter" | "cjk" | "word" | "other";
|
|
78
|
+
/**
|
|
79
|
+
* Coarse Unicode-aware character classification for word navigation (Option/Alt + Left/Right).
|
|
80
|
+
* This intentionally avoids language-specific word segmentation for predictability across scripts.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getWordNavKind(grapheme: string): WordNavKind;
|
|
83
|
+
export declare function isWordNavJoiner(grapheme: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Move the cursor one "word" to the left using Unicode-aware coarse navigation.
|
|
86
|
+
*
|
|
87
|
+
* Returns a new cursor index in the range [0, text.length].
|
|
88
|
+
*/
|
|
89
|
+
export declare function moveWordLeft(text: string, cursor: number): number;
|
|
90
|
+
/**
|
|
91
|
+
* Move the cursor one "word" to the right using Unicode-aware coarse navigation.
|
|
92
|
+
*
|
|
93
|
+
* Returns a new cursor index in the range [0, text.length].
|
|
94
|
+
*/
|
|
95
|
+
export declare function moveWordRight(text: string, cursor: number): number;
|
|
96
|
+
/**
|
|
97
|
+
* Apply background color to a line, padding to full width.
|
|
98
|
+
*
|
|
99
|
+
* @param line - Line of text (may contain ANSI codes)
|
|
100
|
+
* @param width - Total width to pad to
|
|
101
|
+
* @param bgFn - Background color function
|
|
102
|
+
* @returns Line with background applied and padded to width
|
|
103
|
+
*/
|
|
104
|
+
export declare function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string;
|
|
105
|
+
/**
|
|
106
|
+
* Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
|
|
107
|
+
*
|
|
108
|
+
* @param strict - If true, exclude wide chars at boundary that would extend past the range
|
|
109
|
+
*/
|
|
110
|
+
export declare function sliceByColumn(line: string, startCol: number, length: number, strict?: boolean): string;
|
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.15",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"cli"
|
|
28
28
|
],
|
|
29
29
|
"main": "./src/index.ts",
|
|
30
|
-
"types": "./
|
|
30
|
+
"types": "./dist/types/index.d.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.15",
|
|
42
|
+
"@sayknow-cli/utils": "0.3.15",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
|
@@ -53,19 +53,20 @@
|
|
|
53
53
|
"files": [
|
|
54
54
|
"src",
|
|
55
55
|
"README.md",
|
|
56
|
-
"CHANGELOG.md"
|
|
56
|
+
"CHANGELOG.md",
|
|
57
|
+
"dist/types"
|
|
57
58
|
],
|
|
58
59
|
"exports": {
|
|
59
60
|
".": {
|
|
60
|
-
"types": "./
|
|
61
|
+
"types": "./dist/types/index.d.ts",
|
|
61
62
|
"import": "./src/index.ts"
|
|
62
63
|
},
|
|
63
64
|
"./*": {
|
|
64
|
-
"types": "./
|
|
65
|
+
"types": "./dist/types/*.d.ts",
|
|
65
66
|
"import": "./src/*.ts"
|
|
66
67
|
},
|
|
67
68
|
"./components/*": {
|
|
68
|
-
"types": "./
|
|
69
|
+
"types": "./dist/types/components/*.d.ts",
|
|
69
70
|
"import": "./src/components/*.ts"
|
|
70
71
|
},
|
|
71
72
|
"./*.js": "./src/*.ts"
|
|
@@ -53,6 +53,35 @@ export function isNotificationSuppressed(): boolean {
|
|
|
53
53
|
return value === "off" || value === "0" || value === "false";
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const MULTIPLEXER_DISABLED_ENV_VALUES = new Set(["0", "false", "off", "no"]);
|
|
57
|
+
|
|
58
|
+
function multiplexerEnvEnabled(value: string | undefined): boolean {
|
|
59
|
+
const normalized = value?.trim().toLowerCase();
|
|
60
|
+
return normalized !== undefined && normalized.length > 0 && !MULTIPLEXER_DISABLED_ENV_VALUES.has(normalized);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns whether the process runs under a terminal multiplexer (tmux, GNU
|
|
65
|
+
* screen, or zellij). Recognizes the same host markers as the renderer's
|
|
66
|
+
* multiplexer predicate in tui.ts so capability selection and viewport-repaint
|
|
67
|
+
* policy agree on what counts as a multiplexed host. Multiplexers intercept
|
|
68
|
+
* graphics escapes and OSC 8 hyperlinks instead of forwarding them to the
|
|
69
|
+
* outer terminal.
|
|
70
|
+
*/
|
|
71
|
+
export function isUnderTerminalMultiplexer(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
72
|
+
if (
|
|
73
|
+
multiplexerEnvEnabled(env.TMUX) ||
|
|
74
|
+
multiplexerEnvEnabled(env.TMUX_PANE) ||
|
|
75
|
+
multiplexerEnvEnabled(env.STY) ||
|
|
76
|
+
multiplexerEnvEnabled(env.ZELLIJ) ||
|
|
77
|
+
multiplexerEnvEnabled(env.SKC_TMUX_LAUNCHED)
|
|
78
|
+
) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const term = env.TERM?.trim().toLowerCase() ?? "";
|
|
82
|
+
return term.startsWith("tmux") || term.startsWith("screen");
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
let terminalGraphicsFallbackDepth = 0;
|
|
57
86
|
let cursorNeutralImageAllowedDepth = 0;
|
|
58
87
|
|
|
@@ -105,6 +134,15 @@ function getForcedImageProtocol(): ImageProtocol | null | undefined {
|
|
|
105
134
|
return null;
|
|
106
135
|
}
|
|
107
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Returns whether PI_FORCE_IMAGE_PROTOCOL explicitly configures the image
|
|
139
|
+
* protocol, including an explicit "off". An explicit configuration is
|
|
140
|
+
* authoritative: runtime capability probes must not override it.
|
|
141
|
+
*/
|
|
142
|
+
export function isImageProtocolForced(): boolean {
|
|
143
|
+
return getForcedImageProtocol() !== undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
function parseMajorMinorVersion(versionRaw?: string): { major: number; minor: number } | null {
|
|
109
147
|
if (!versionRaw) return null;
|
|
110
148
|
const match = /^(\d+)\.(\d+)/u.exec(versionRaw.trim());
|
|
@@ -137,7 +175,7 @@ function getFallbackImageProtocol(terminalId: TerminalId): ImageProtocol | null
|
|
|
137
175
|
if (!process.stdout.isTTY) return null;
|
|
138
176
|
if (terminalId === "vscode" || terminalId === "alacritty") return null;
|
|
139
177
|
const term = Bun.env.TERM?.toLowerCase() ?? "";
|
|
140
|
-
if (term.includes("
|
|
178
|
+
if (term.includes("ghostty")) {
|
|
141
179
|
return ImageProtocol.Kitty;
|
|
142
180
|
}
|
|
143
181
|
return null;
|
|
@@ -220,10 +258,10 @@ export const TERMINAL = (() => {
|
|
|
220
258
|
);
|
|
221
259
|
}
|
|
222
260
|
}
|
|
261
|
+
const underMultiplexer = isUnderTerminalMultiplexer();
|
|
223
262
|
// tmux and screen multiplexers do not reliably forward OSC 8 hyperlinks
|
|
224
263
|
// to the outer terminal, so force them off regardless of detected terminal.
|
|
225
|
-
|
|
226
|
-
if (resolved.hyperlinks && (Bun.env.TMUX || term.startsWith("tmux") || term.startsWith("screen"))) {
|
|
264
|
+
if (resolved.hyperlinks && underMultiplexer) {
|
|
227
265
|
resolved = new TerminalInfo(
|
|
228
266
|
resolved.id,
|
|
229
267
|
resolved.imageProtocol,
|
|
@@ -232,6 +270,17 @@ export const TERMINAL = (() => {
|
|
|
232
270
|
resolved.notifyProtocol,
|
|
233
271
|
);
|
|
234
272
|
}
|
|
273
|
+
// Multiplexers (tmux/screen/zellij) consume raw kitty/iTerm2 graphics
|
|
274
|
+
// escapes instead of forwarding them (no DCS passthrough wrapping is
|
|
275
|
+
// emitted), so a detected image protocol draws nothing while its
|
|
276
|
+
// out-of-band cursor writes corrupt the frame. Graphics are therefore
|
|
277
|
+
// unconditionally suppressed under a multiplexer; the runtime sixel probe
|
|
278
|
+
// never runs there (tmux advertises DA1 ";4" from compile-time support
|
|
279
|
+
// regardless of the attached client), and PI_FORCE_IMAGE_PROTOCOL=sixel
|
|
280
|
+
// is the only opt-in for chains that render sixel end-to-end.
|
|
281
|
+
if (resolved.imageProtocol && forcedImageProtocol === undefined && underMultiplexer) {
|
|
282
|
+
resolved = new TerminalInfo(resolved.id, null, resolved.trueColor, resolved.hyperlinks, resolved.notifyProtocol);
|
|
283
|
+
}
|
|
235
284
|
return resolved;
|
|
236
285
|
})();
|
|
237
286
|
|
|
@@ -239,11 +288,35 @@ type MutableTerminalInfo = {
|
|
|
239
288
|
imageProtocol: ImageProtocol | null;
|
|
240
289
|
};
|
|
241
290
|
|
|
291
|
+
type ImageProtocolChangeListener = (imageProtocol: ImageProtocol | null) => void;
|
|
292
|
+
const imageProtocolChangeListeners = new Set<ImageProtocolChangeListener>();
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Subscribe to runtime image-protocol changes (e.g. the asynchronous sixel
|
|
296
|
+
* capability probe enabling graphics after startup). Returns an unsubscribe
|
|
297
|
+
* function. Listeners fire only on actual changes.
|
|
298
|
+
*/
|
|
299
|
+
export function onImageProtocolChanged(listener: ImageProtocolChangeListener): () => void {
|
|
300
|
+
imageProtocolChangeListeners.add(listener);
|
|
301
|
+
return () => {
|
|
302
|
+
imageProtocolChangeListeners.delete(listener);
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
242
306
|
/**
|
|
243
307
|
* Override terminal image protocol at runtime after capability probes complete.
|
|
244
308
|
*/
|
|
245
309
|
export function setTerminalImageProtocol(imageProtocol: ImageProtocol | null): void {
|
|
246
|
-
|
|
310
|
+
const mutable = TERMINAL as unknown as MutableTerminalInfo;
|
|
311
|
+
if (mutable.imageProtocol === imageProtocol) return;
|
|
312
|
+
mutable.imageProtocol = imageProtocol;
|
|
313
|
+
for (const listener of imageProtocolChangeListeners) {
|
|
314
|
+
try {
|
|
315
|
+
listener(imageProtocol);
|
|
316
|
+
} catch {
|
|
317
|
+
// Listener failures must not break protocol switching.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
247
320
|
}
|
|
248
321
|
|
|
249
322
|
export function getTerminalInfo(terminalId: TerminalId): TerminalInfo {
|