@sayknow-cli/tui 0.3.7 → 0.3.9
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 +1 -0
- package/dist/types/components/editor.d.ts +11 -2
- package/dist/types/components/loader.d.ts +10 -1
- package/dist/types/components/markdown.d.ts +14 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/terminal-capabilities.d.ts +70 -2
- package/dist/types/terminal.d.ts +2 -0
- package/dist/types/tui.d.ts +22 -4
- package/dist/types/utils.d.ts +12 -0
- package/package.json +4 -4
- package/src/animation-scheduler.ts +99 -0
- package/src/autocomplete.ts +119 -96
- package/src/components/editor.ts +315 -175
- package/src/components/image.ts +43 -9
- package/src/components/loader.ts +36 -37
- package/src/components/markdown.ts +79 -2
- package/src/components/settings-list.ts +4 -2
- package/src/index.ts +1 -0
- package/src/stdin-buffer.ts +89 -11
- package/src/terminal-capabilities.ts +149 -7
- package/src/terminal.ts +7 -0
- package/src/tui.ts +274 -96
- package/src/utils.ts +77 -11
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AnimationCadence = 16 | 80;
|
|
2
|
+
type AnimationCallback = (now: number) => void;
|
|
3
|
+
export interface AnimationRegistration {
|
|
4
|
+
unregister(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare function registerAnimationCallback(callback: AnimationCallback, cadence?: AnimationCadence): AnimationRegistration;
|
|
7
|
+
export declare const __animationSchedulerTestHooks: {
|
|
8
|
+
getActiveTimerCount(cadence?: AnimationCadence): number;
|
|
9
|
+
getRegistrantCount(cadence?: AnimationCadence): number;
|
|
10
|
+
getStartedTimerCount(cadence?: AnimationCadence): number;
|
|
11
|
+
reset(): void;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AutocompleteProvider } from "../autocomplete";
|
|
2
2
|
import type { SymbolTheme } from "../symbols";
|
|
3
3
|
import { type Component, type Focusable } from "../tui";
|
|
4
4
|
import { type SelectListTheme } from "./select-list";
|
|
@@ -24,6 +24,13 @@ interface HistoryStorage {
|
|
|
24
24
|
add(prompt: string, cwd?: string): Promise<void>;
|
|
25
25
|
getRecent(limit: number, cwd?: string): HistoryEntry[];
|
|
26
26
|
}
|
|
27
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
28
|
+
export declare const __editorPerfCounters: {
|
|
29
|
+
layoutTextInvocations: number;
|
|
30
|
+
layoutLogicalLinesProcessed: number;
|
|
31
|
+
visibleWidthMeasurements: number;
|
|
32
|
+
reset(): void;
|
|
33
|
+
};
|
|
27
34
|
export declare class Editor implements Component, Focusable {
|
|
28
35
|
#private;
|
|
29
36
|
/** Focusable interface - set by TUI when focus changes */
|
|
@@ -46,6 +53,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
46
53
|
onTab?: (text: string) => boolean | undefined;
|
|
47
54
|
disableSubmit: boolean;
|
|
48
55
|
constructor(theme: EditorTheme);
|
|
56
|
+
dispose(): void;
|
|
49
57
|
setAutocompleteProvider(provider: AutocompleteProvider): void;
|
|
50
58
|
getAutocompleteProvider(): AutocompleteProvider | undefined;
|
|
51
59
|
/** Whether the autocomplete dropdown is currently open. */
|
|
@@ -66,7 +74,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
66
74
|
setPlaceholder(placeholder: string | undefined): void;
|
|
67
75
|
/**
|
|
68
76
|
* Get the available width for top border content given a total terminal width.
|
|
69
|
-
* Accounts for
|
|
77
|
+
* Accounts for right gutter, border characters, and horizontal padding when visible.
|
|
70
78
|
*/
|
|
71
79
|
getTopBorderAvailableWidth(terminalWidth: number): number;
|
|
72
80
|
/**
|
|
@@ -76,6 +84,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
76
84
|
getUseTerminalCursor(): boolean;
|
|
77
85
|
setMaxHeight(maxHeight: number | undefined): void;
|
|
78
86
|
setPaddingX(paddingX: number): void;
|
|
87
|
+
setRightGutterWidth(width: number): void;
|
|
79
88
|
getAutocompleteMaxVisible(): number;
|
|
80
89
|
setAutocompleteMaxVisible(maxVisible: number): void;
|
|
81
90
|
setHistoryStorage(storage: HistoryStorage): void;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { TUI } from "../tui";
|
|
2
2
|
import { Text } from "./text";
|
|
3
|
+
export interface LoaderOptions {
|
|
4
|
+
timeDependentColor?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
7
|
+
export declare const __loaderPerfCounters: {
|
|
8
|
+
liveIntervals: number;
|
|
9
|
+
startedIntervals: number;
|
|
10
|
+
reset(): void;
|
|
11
|
+
};
|
|
3
12
|
export declare class Loader extends Text {
|
|
4
13
|
#private;
|
|
5
14
|
private spinnerColorFn;
|
|
6
15
|
private messageColorFn;
|
|
7
16
|
private message;
|
|
8
|
-
constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, spinnerFrames?: string[]);
|
|
17
|
+
constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, spinnerFrames?: string[], options?: LoaderOptions);
|
|
9
18
|
render(width: number): string[];
|
|
10
19
|
start(): void;
|
|
11
20
|
stop(): void;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { SymbolTheme } from "../symbols";
|
|
2
2
|
import type { Component } from "../tui";
|
|
3
|
+
/** Test-only clock seam for streaming throttle tests. */
|
|
4
|
+
export declare function __setMarkdownNowForTest(now: (() => number) | undefined): void;
|
|
3
5
|
/** Test/diagnostic seam: number of synchronous highlight invocations since the last reset. */
|
|
4
6
|
export declare function getMarkdownHighlightCallCount(): number;
|
|
5
7
|
export declare function resetMarkdownHighlightCallCount(): void;
|
|
8
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
9
|
+
export declare const __markdownPerfCounters: {
|
|
10
|
+
lexerInvocations: number;
|
|
11
|
+
lexedBytes: number;
|
|
12
|
+
reset(): void;
|
|
13
|
+
};
|
|
6
14
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
7
15
|
export declare function clearRenderCache(): void;
|
|
8
16
|
/**
|
|
@@ -53,7 +61,12 @@ export interface MarkdownTheme {
|
|
|
53
61
|
export declare class Markdown implements Component {
|
|
54
62
|
#private;
|
|
55
63
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
|
|
56
|
-
|
|
64
|
+
setOnStaleThrottle(callback: (() => void) | undefined): void;
|
|
65
|
+
setText(text: string, options?: {
|
|
66
|
+
streaming?: boolean;
|
|
67
|
+
}): void;
|
|
68
|
+
setStreaming(streaming: boolean): void;
|
|
69
|
+
dispose(): void;
|
|
57
70
|
invalidate(): void;
|
|
58
71
|
render(width: number): string[];
|
|
59
72
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -47,13 +47,73 @@ export interface ImageRenderOptions {
|
|
|
47
47
|
maxWidthCells?: number;
|
|
48
48
|
maxHeightCells?: number;
|
|
49
49
|
preserveAspectRatio?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Kitty-only: stable placement id (`p=`). Re-emitting the same image id +
|
|
52
|
+
* placement id *replaces* the existing placement instead of stacking a new
|
|
53
|
+
* copy, which makes diff-renderer repaints idempotent. Callers that render
|
|
54
|
+
* a persistent component should allocate one id per component instance.
|
|
55
|
+
*/
|
|
56
|
+
placementId?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Kitty-only: stable image id (`i=`). Defaults to a content hash of the
|
|
59
|
+
* base64 payload ({@link kittyImageId}). Pass a precomputed id to avoid
|
|
60
|
+
* re-hashing large payloads on every render.
|
|
61
|
+
*/
|
|
62
|
+
imageId?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Kitty-only: sink for the out-of-band data transmission (`a=t`) emitted
|
|
65
|
+
* the first time an image id is rendered. Defaults to the process-wide
|
|
66
|
+
* writer configured via {@link setKittyTransmitWriter} (stdout).
|
|
67
|
+
*/
|
|
68
|
+
onTransmit?: (sequence: string) => void;
|
|
50
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Derive a stable 32-bit non-zero kitty image id (`i=`) from image content
|
|
72
|
+
* (FNV-1a over the base64 payload). Identical content maps to the same id, so
|
|
73
|
+
* retransmission replaces the stored image instead of accumulating copies.
|
|
74
|
+
*/
|
|
75
|
+
export declare function kittyImageId(base64Data: string): number;
|
|
51
76
|
export declare function getCellDimensions(): CellDimensions;
|
|
52
77
|
export declare function setCellDimensions(dims: CellDimensions): void;
|
|
53
78
|
export declare function encodeKitty(base64Data: string, options?: {
|
|
54
79
|
columns?: number;
|
|
55
80
|
rows?: number;
|
|
56
81
|
imageId?: number;
|
|
82
|
+
placementId?: number;
|
|
83
|
+
}): string;
|
|
84
|
+
/** Test hook: forget which kitty image ids were transmitted. */
|
|
85
|
+
export declare function resetKittyTransmissions(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Override where out-of-band kitty data transmissions (`a=t`) are written.
|
|
88
|
+
* The default writes directly to stdout: a transmit-only escape is
|
|
89
|
+
* cursor-neutral (it uploads pixel data without drawing anything), so the
|
|
90
|
+
* only ordering requirement is that it reaches the terminal before the
|
|
91
|
+
* placement escape that references it — which the synchronous write during
|
|
92
|
+
* render guarantees. Tests use this to capture transmissions.
|
|
93
|
+
*/
|
|
94
|
+
export declare function setKittyTransmitWriter(writer: (sequence: string) => void): void;
|
|
95
|
+
/**
|
|
96
|
+
* Encode a kitty transmit-only (`a=t`) escape: uploads image data under a
|
|
97
|
+
* stable id without creating a placement. Chunked at 4096 bytes per spec.
|
|
98
|
+
*
|
|
99
|
+
* This is deliberately separate from placement: re-sending data (`a=t`/`a=T`)
|
|
100
|
+
* for an existing image id deletes the image and ALL of its placements, so
|
|
101
|
+
* data must be uploaded exactly once per id and repaints must go through
|
|
102
|
+
* {@link encodeKittyPlacement} only.
|
|
103
|
+
*/
|
|
104
|
+
export declare function encodeKittyTransmit(base64Data: string, imageId: number): string;
|
|
105
|
+
/**
|
|
106
|
+
* Encode a kitty placement-only (`a=p`) escape referencing previously
|
|
107
|
+
* transmitted data. Re-emitting the same i=/p= pair replaces that one
|
|
108
|
+
* placement (never stacks, never touches sibling placements), and C=1
|
|
109
|
+
* keeps the cursor where it is so the escape can be emitted from the
|
|
110
|
+
* component's first row without cursor-up tricks.
|
|
111
|
+
*/
|
|
112
|
+
export declare function encodeKittyPlacement(options: {
|
|
113
|
+
imageId: number;
|
|
114
|
+
placementId: number;
|
|
115
|
+
columns: number;
|
|
116
|
+
rows: number;
|
|
57
117
|
}): string;
|
|
58
118
|
export declare function encodeITerm2(base64Data: string, options?: {
|
|
59
119
|
width?: number | string;
|
|
@@ -68,8 +128,16 @@ export declare function getJpegDimensions(base64Data: string): ImageDimensions |
|
|
|
68
128
|
export declare function getGifDimensions(base64Data: string): ImageDimensions | null;
|
|
69
129
|
export declare function getWebpDimensions(base64Data: string): ImageDimensions | null;
|
|
70
130
|
export declare function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null;
|
|
71
|
-
export
|
|
131
|
+
export interface RenderedImage {
|
|
72
132
|
sequence: string;
|
|
73
133
|
rows: number;
|
|
74
|
-
|
|
134
|
+
/**
|
|
135
|
+
* True when the escape neither moves the cursor nor carries pixel data
|
|
136
|
+
* (kitty `a=p,C=1` placements). Cursor-neutral sequences can be emitted
|
|
137
|
+
* from the component's first row; cursor-advancing protocols
|
|
138
|
+
* (iTerm2/SIXEL) must draw from the last reserved row instead.
|
|
139
|
+
*/
|
|
140
|
+
cursorNeutral?: boolean;
|
|
141
|
+
}
|
|
142
|
+
export declare function renderImage(base64Data: string, imageDimensions: ImageDimensions, options?: ImageRenderOptions): RenderedImage | null;
|
|
75
143
|
export declare function imageFallback(mimeType: string, dimensions?: ImageDimensions, filename?: string): string;
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface Terminal {
|
|
|
32
32
|
drainInput(maxMs?: number, idleMs?: number): Promise<void>;
|
|
33
33
|
write(data: string): void;
|
|
34
34
|
get available(): boolean;
|
|
35
|
+
readonly isProcessTerminal?: boolean;
|
|
35
36
|
get columns(): number;
|
|
36
37
|
get rows(): number;
|
|
37
38
|
get kittyProtocolActive(): boolean;
|
|
@@ -64,6 +65,7 @@ export declare function resolveTerminalRows(stream?: TerminalSizeStream, envRows
|
|
|
64
65
|
*/
|
|
65
66
|
export declare class ProcessTerminal implements Terminal {
|
|
66
67
|
#private;
|
|
68
|
+
get isProcessTerminal(): boolean;
|
|
67
69
|
get kittyProtocolActive(): boolean;
|
|
68
70
|
get appearance(): TerminalAppearance | undefined;
|
|
69
71
|
onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -72,6 +72,15 @@ export interface OverlayMargin {
|
|
|
72
72
|
}
|
|
73
73
|
/** Value that can be absolute (number) or percentage (string like "50%") */
|
|
74
74
|
export type SizeValue = number | `${number}%`;
|
|
75
|
+
/**
|
|
76
|
+
* True when repainting only the live viewport is safer than clearing/replaying
|
|
77
|
+
* the full transcript. Native Windows console hosts are included even when
|
|
78
|
+
* WT_SESSION is absent because PowerShell/ConPTY launch chains can drop terminal
|
|
79
|
+
* identity variables while keeping the same scroll-jump behavior.
|
|
80
|
+
*/
|
|
81
|
+
export declare function shouldUseViewportRepaintForHost(env?: Record<string, string | undefined>, platform?: NodeJS.Platform, options?: {
|
|
82
|
+
includeNativeWindows?: boolean;
|
|
83
|
+
}): boolean;
|
|
75
84
|
/**
|
|
76
85
|
* Options for overlay positioning and sizing.
|
|
77
86
|
* Values can be absolute numbers or percentage strings (e.g., "50%").
|
|
@@ -130,6 +139,11 @@ export declare class Container implements Component {
|
|
|
130
139
|
invalidate(): void;
|
|
131
140
|
render(width: number): string[];
|
|
132
141
|
}
|
|
142
|
+
type TuiRenderCounterSnapshot = {
|
|
143
|
+
debugRedrawEnvReads: number;
|
|
144
|
+
debugRedrawAppendWrites: number;
|
|
145
|
+
differentialGuardVisibleWidthCalls: number;
|
|
146
|
+
};
|
|
133
147
|
/**
|
|
134
148
|
* TUI - Main class for managing terminal UI with differential rendering
|
|
135
149
|
*/
|
|
@@ -138,6 +152,8 @@ export declare class TUI extends Container {
|
|
|
138
152
|
terminal: Terminal;
|
|
139
153
|
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
140
154
|
onDebug?: () => void;
|
|
155
|
+
static resetRenderCountersForTest(): void;
|
|
156
|
+
static getRenderCountersForTest(): TuiRenderCounterSnapshot;
|
|
141
157
|
overlayStack: {
|
|
142
158
|
component: Component;
|
|
143
159
|
options?: OverlayOptions;
|
|
@@ -145,6 +161,7 @@ export declare class TUI extends Container {
|
|
|
145
161
|
hidden: boolean;
|
|
146
162
|
}[];
|
|
147
163
|
constructor(terminal: Terminal, showHardwareCursor?: boolean);
|
|
164
|
+
dispose(): void;
|
|
148
165
|
get fullRedraws(): number;
|
|
149
166
|
getShowHardwareCursor(): boolean;
|
|
150
167
|
setShowHardwareCursor(enabled: boolean): void;
|
|
@@ -175,16 +192,17 @@ export declare class TUI extends Container {
|
|
|
175
192
|
removeInputListener(listener: InputListener): void;
|
|
176
193
|
stop(): void;
|
|
177
194
|
/**
|
|
178
|
-
*
|
|
195
|
+
* Viewport-repaint-aware resize render request.
|
|
179
196
|
*
|
|
180
197
|
* A forced full redraw (`requestRender(true)`) resets `#previousWidth`/`#previousHeight`
|
|
181
198
|
* to -1, which makes `#doRender` treat the frame as a width change and fall into the
|
|
182
199
|
* `fullRender` path. In terminal multiplexers that path skips the scrollback-clearing
|
|
183
200
|
* `3J` escape (users navigate scrollback history), so replaying every transcript line
|
|
184
201
|
* piles it back on top of scrollback — the "top of screen scrolls down to the prompt at
|
|
185
|
-
* high speed" resize storm.
|
|
186
|
-
*
|
|
187
|
-
*
|
|
202
|
+
* high speed" resize storm. Windows Terminal/ConPTY can also visibly jump to
|
|
203
|
+
* the transcript top during streaming redraws, so viewport-repaint sessions
|
|
204
|
+
* keep force off and let `#doRender` repaint only the live viewport. Set
|
|
205
|
+
* `PI_TUI_LEGACY_MULTIPLEXER_FULL_RENDER=1` to restore the legacy tmux redraw.
|
|
188
206
|
*/
|
|
189
207
|
requestResizeRender(): void;
|
|
190
208
|
requestRender(force?: boolean, source?: string): void;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@sayknow-cli/natives";
|
|
2
2
|
export { Ellipsis } from "@sayknow-cli/natives";
|
|
3
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;
|
|
4
13
|
export declare function isPrintableAscii(text: string): boolean;
|
|
5
14
|
export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
|
|
6
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[];
|
|
7
17
|
export declare function wrapTextWithAnsi(text: string, width: number): string[];
|
|
8
18
|
export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter: boolean): ExtractSegmentsResult;
|
|
9
19
|
/**
|
|
@@ -24,6 +34,8 @@ export declare function visibleWidthRaw(str: string): number;
|
|
|
24
34
|
* Calculate the visible width of a string in terminal columns.
|
|
25
35
|
*/
|
|
26
36
|
export declare function visibleWidth(str: string): number;
|
|
37
|
+
export declare function visibleWidthsNative(lines: readonly string[]): number[];
|
|
38
|
+
export declare function visibleWidths(lines: readonly string[]): number[];
|
|
27
39
|
/**
|
|
28
40
|
* Normalize text for terminal output without changing logical editor content.
|
|
29
41
|
* Some terminals render canonically decomposed Hangul jamo or precomposed
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/tui",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.9",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
8
8
|
"contributors": [
|
|
9
9
|
"Mario Zechner"
|
|
@@ -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.9",
|
|
42
|
+
"@sayknow-cli/utils": "0.3.9",
|
|
43
43
|
"lru-cache": "11.3.6",
|
|
44
44
|
"marked": "^18.0.3"
|
|
45
45
|
},
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type AnimationCadence = 16 | 80;
|
|
2
|
+
|
|
3
|
+
type TimerHandle = ReturnType<typeof setInterval>;
|
|
4
|
+
type AnimationCallback = (now: number) => void;
|
|
5
|
+
|
|
6
|
+
interface CadenceBucket {
|
|
7
|
+
callbacks: Set<AnimationCallback>;
|
|
8
|
+
timer?: TimerHandle;
|
|
9
|
+
startedTimers: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const buckets = new Map<AnimationCadence, CadenceBucket>();
|
|
13
|
+
|
|
14
|
+
function getBucket(cadence: AnimationCadence): CadenceBucket {
|
|
15
|
+
let bucket = buckets.get(cadence);
|
|
16
|
+
if (!bucket) {
|
|
17
|
+
bucket = { callbacks: new Set(), startedTimers: 0 };
|
|
18
|
+
buckets.set(cadence, bucket);
|
|
19
|
+
}
|
|
20
|
+
return bucket;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function startBucket(cadence: AnimationCadence, bucket: CadenceBucket): void {
|
|
24
|
+
if (bucket.timer) return;
|
|
25
|
+
bucket.timer = setInterval(() => {
|
|
26
|
+
const now = performance.now();
|
|
27
|
+
// Snapshot so re-entrant register/unregister during a tick is safe, and
|
|
28
|
+
// isolate each callback so one throwing registrant cannot starve siblings
|
|
29
|
+
// or surface as an uncaught exception that kills the shared timer.
|
|
30
|
+
for (const callback of [...bucket.callbacks]) {
|
|
31
|
+
try {
|
|
32
|
+
callback(now);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("[animation-scheduler] callback threw:", err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}, cadence);
|
|
38
|
+
bucket.startedTimers += 1;
|
|
39
|
+
bucket.timer?.unref?.();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stopBucket(bucket: CadenceBucket): void {
|
|
43
|
+
if (!bucket.timer) return;
|
|
44
|
+
clearInterval(bucket.timer);
|
|
45
|
+
bucket.timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AnimationRegistration {
|
|
49
|
+
unregister(): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function registerAnimationCallback(
|
|
53
|
+
callback: AnimationCallback,
|
|
54
|
+
cadence: AnimationCadence = 80,
|
|
55
|
+
): AnimationRegistration {
|
|
56
|
+
const bucket = getBucket(cadence);
|
|
57
|
+
bucket.callbacks.add(callback);
|
|
58
|
+
startBucket(cadence, bucket);
|
|
59
|
+
let registered = true;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
unregister(): void {
|
|
63
|
+
if (!registered) return;
|
|
64
|
+
registered = false;
|
|
65
|
+
bucket.callbacks.delete(callback);
|
|
66
|
+
if (bucket.callbacks.size === 0) stopBucket(bucket);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const __animationSchedulerTestHooks = {
|
|
72
|
+
getActiveTimerCount(cadence?: AnimationCadence): number {
|
|
73
|
+
if (cadence !== undefined) return getBucket(cadence).timer ? 1 : 0;
|
|
74
|
+
let count = 0;
|
|
75
|
+
for (const bucket of buckets.values()) {
|
|
76
|
+
if (bucket.timer) count += 1;
|
|
77
|
+
}
|
|
78
|
+
return count;
|
|
79
|
+
},
|
|
80
|
+
getRegistrantCount(cadence?: AnimationCadence): number {
|
|
81
|
+
if (cadence !== undefined) return getBucket(cadence).callbacks.size;
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const bucket of buckets.values()) count += bucket.callbacks.size;
|
|
84
|
+
return count;
|
|
85
|
+
},
|
|
86
|
+
getStartedTimerCount(cadence?: AnimationCadence): number {
|
|
87
|
+
if (cadence !== undefined) return getBucket(cadence).startedTimers;
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const bucket of buckets.values()) count += bucket.startedTimers;
|
|
90
|
+
return count;
|
|
91
|
+
},
|
|
92
|
+
reset(): void {
|
|
93
|
+
for (const bucket of buckets.values()) {
|
|
94
|
+
stopBucket(bucket);
|
|
95
|
+
bucket.callbacks.clear();
|
|
96
|
+
bucket.startedTimers = 0;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|