@silvery/examples 0.17.3 → 0.17.4
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/UPNG-Cy7ViL8f.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
- package/dist/_banner-DLPxCqVy.mjs +44 -0
- package/dist/ansi-CCE2pVS0.mjs +16397 -0
- package/dist/apng-HhhBjRGt.mjs +68 -0
- package/dist/apng-mwUQbTTF.mjs +3 -0
- package/dist/apps/aichat/index.mjs +1299 -0
- package/dist/apps/app-todo.mjs +139 -0
- package/dist/apps/async-data.mjs +204 -0
- package/dist/apps/cli-wizard.mjs +339 -0
- package/dist/apps/clipboard.mjs +198 -0
- package/dist/apps/components.mjs +864 -0
- package/dist/apps/data-explorer.mjs +483 -0
- package/dist/apps/dev-tools.mjs +397 -0
- package/dist/apps/explorer.mjs +698 -0
- package/dist/apps/gallery.mjs +766 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +280 -0
- package/dist/apps/layout-ref.mjs +187 -0
- package/dist/apps/outline.mjs +203 -0
- package/dist/apps/paste-demo.mjs +189 -0
- package/dist/apps/scroll.mjs +86 -0
- package/dist/apps/search-filter.mjs +287 -0
- package/dist/apps/selection.mjs +355 -0
- package/dist/apps/spatial-focus-demo.mjs +388 -0
- package/dist/apps/task-list.mjs +258 -0
- package/dist/apps/terminal-caps-demo.mjs +315 -0
- package/dist/apps/terminal.mjs +872 -0
- package/dist/apps/text-selection-demo.mjs +254 -0
- package/dist/apps/textarea.mjs +178 -0
- package/dist/apps/theme.mjs +661 -0
- package/dist/apps/transform.mjs +215 -0
- package/dist/apps/virtual-10k.mjs +422 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Bahh9mKN.mjs +1179 -0
- package/dist/backends-CCtCDQ94.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +48 -0
- package/dist/components/hello.mjs +31 -0
- package/dist/components/progress-bar.mjs +59 -0
- package/dist/components/select-list.mjs +85 -0
- package/dist/components/spinner.mjs +57 -0
- package/dist/components/text-input.mjs +62 -0
- package/dist/components/virtual-list.mjs +51 -0
- package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
- package/dist/gif-BZaqPPVX.mjs +3 -0
- package/dist/gif-BtnXuxLF.mjs +71 -0
- package/dist/gifenc-CLRW41dk.mjs +728 -0
- package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1204 -0
- package/dist/layout/live-resize.mjs +303 -0
- package/dist/layout/overflow.mjs +70 -0
- package/dist/layout/text-layout.mjs +335 -0
- package/dist/node-NuJ94BWl.mjs +1083 -0
- package/dist/plugins-D1KtkT4a.mjs +3057 -0
- package/dist/resvg-js-C_8Wps1F.mjs +201 -0
- package/dist/src-BTEVGpd9.mjs +23538 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-CzfRafCQ.mjs +814 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
- package/package.json +18 -13
- package/_banner.tsx +0 -60
- package/apps/aichat/components.tsx +0 -469
- package/apps/aichat/index.tsx +0 -220
- package/apps/aichat/script.ts +0 -460
- package/apps/aichat/state.ts +0 -325
- package/apps/aichat/types.ts +0 -19
- package/apps/app-todo.tsx +0 -201
- package/apps/async-data.tsx +0 -196
- package/apps/cli-wizard.tsx +0 -332
- package/apps/clipboard.tsx +0 -183
- package/apps/components.tsx +0 -658
- package/apps/data-explorer.tsx +0 -490
- package/apps/dev-tools.tsx +0 -395
- package/apps/explorer.tsx +0 -731
- package/apps/gallery.tsx +0 -653
- package/apps/inline-bench.tsx +0 -138
- package/apps/kanban.tsx +0 -265
- package/apps/layout-ref.tsx +0 -173
- package/apps/outline.tsx +0 -160
- package/apps/panes/index.tsx +0 -203
- package/apps/paste-demo.tsx +0 -185
- package/apps/scroll.tsx +0 -80
- package/apps/search-filter.tsx +0 -240
- package/apps/selection.tsx +0 -346
- package/apps/spatial-focus-demo.tsx +0 -372
- package/apps/task-list.tsx +0 -271
- package/apps/terminal-caps-demo.tsx +0 -317
- package/apps/terminal.tsx +0 -784
- package/apps/text-selection-demo.tsx +0 -193
- package/apps/textarea.tsx +0 -155
- package/apps/theme.tsx +0 -515
- package/apps/transform.tsx +0 -229
- package/apps/virtual-10k.tsx +0 -405
- package/apps/vterm-demo/index.tsx +0 -216
- package/components/counter.tsx +0 -49
- package/components/hello.tsx +0 -38
- package/components/progress-bar.tsx +0 -52
- package/components/select-list.tsx +0 -54
- package/components/spinner.tsx +0 -44
- package/components/text-input.tsx +0 -61
- package/components/virtual-list.tsx +0 -56
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs.map +0 -1
- package/layout/dashboard.tsx +0 -953
- package/layout/live-resize.tsx +0 -282
- package/layout/overflow.tsx +0 -51
- package/layout/text-layout.tsx +0 -283
|
@@ -0,0 +1,3057 @@
|
|
|
1
|
+
import { a as __toCommonJS, i as __require } from "./chunk-BSw8zbkd.mjs";
|
|
2
|
+
import { A as buffer_exports, C as detectTextSizingSupport, F as enableFocusReporting, I as isModifierOnlyEvent, L as keyToAnsi, N as init_buffer, O as bufferToStyledText, P as createTermProvider, R as keyToKittyAnsi, T as isTextSizingLikelySupported, a as IncrementalRenderMismatchError, i as outputPhase, k as bufferToText, n as createTerm, r as createOutputPhase, t as createOutputGuard, w as getCachedProbeResult, z as parseKey } from "./ansi-CCE2pVS0.mjs";
|
|
3
|
+
import { a as init_ThemeContext, m as createColorSchemeDetector, r as init_detect } from "./src-CUUOuRH6.mjs";
|
|
4
|
+
import { A as createFocusManager, B as isAnyDirty, C as enterAlternateScreen, D as getAncestorPath, E as createPipeline, F as reconciler, G as FocusManagerContext, H as isLayoutEngineInitialized, I as setOnNodeRemoved, J as StdoutContext, K as RuntimeContext, L as executeRender, M as createContainer, N as createFiberRoot, O as CursorProvider, P as getContainerRoot, R as createAg, S as enableMouse, T as resetCursorStyle, U as CacheBackendContext, V as ensureDefaultLayoutEngine, W as CapabilityRegistryContext, Y as TermContext, _ as detectKittyFromStdio, a as renderSelectionOverlay, b as disableMouse, c as terminalSelectionUpdate, d as hitTest, f as processMouseEvent, g as createWidthDetector, h as applyWidthConfig, i as createVirtualScrollback, j as findByTestID, k as createCursorStore, l as createMouseEventProcessor, m as updateKeyboardModifiers, n as renderSearchBar, o as createTerminalSelectionState, p as selectionHitTest, q as StderrContext, r as searchUpdate, s as extractText, t as createSearchState, u as findContainBoundary, v as KittyFlags, w as leaveAlternateScreen, x as enableKittyKeyboard, y as disableKittyKeyboard, z as signal } from "./src-BTEVGpd9.mjs";
|
|
5
|
+
import { t as _usingCtx } from "./usingCtx-CsEf0xO3.mjs";
|
|
6
|
+
import { t as require_jsx_runtime } from "./jsx-runtime-dMs_8fNu.mjs";
|
|
7
|
+
import React, { Component, createContext, useContext, useEffect, useRef } from "react";
|
|
8
|
+
import { createLogger } from "loggily";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import { writeSync } from "node:fs";
|
|
11
|
+
import "node:path";
|
|
12
|
+
import process$1 from "node:process";
|
|
13
|
+
//#region ../packages/ag-term/src/runtime/layout.ts
|
|
14
|
+
init_buffer();
|
|
15
|
+
/**
|
|
16
|
+
* Ensure layout engine is initialized.
|
|
17
|
+
* Must be called before layout() in async contexts.
|
|
18
|
+
*/
|
|
19
|
+
async function ensureLayoutEngine() {
|
|
20
|
+
if (!isLayoutEngineInitialized()) await ensureDefaultLayoutEngine();
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region ../packages/ag-term/src/runtime/diff.ts
|
|
24
|
+
/**
|
|
25
|
+
* Pure diff function for silvery-loop.
|
|
26
|
+
*
|
|
27
|
+
* Takes prev and next buffers, returns minimal ANSI patch.
|
|
28
|
+
* This is an internal function used by the runtime.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Compute the minimal ANSI diff between two buffers.
|
|
32
|
+
*
|
|
33
|
+
* @param prev Previous buffer (null on first render)
|
|
34
|
+
* @param next Current buffer
|
|
35
|
+
* @param mode Render mode (fullscreen or inline)
|
|
36
|
+
* @returns ANSI escape sequence string to transform prev into next
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { diff, layout } from '@silvery/ag-term/runtime'
|
|
41
|
+
*
|
|
42
|
+
* const prev = layout(<Text>Hello</Text>, dims)
|
|
43
|
+
* const next = layout(<Text>World</Text>, dims)
|
|
44
|
+
* const patch = diff(prev, next)
|
|
45
|
+
* process.stdout.write(patch)
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
function diff(prev, next, mode = "fullscreen", scrollbackOffset = 0, termRows) {
|
|
49
|
+
const prevBuffer = prev?._buffer ?? null;
|
|
50
|
+
const nextBuffer = next._buffer;
|
|
51
|
+
return outputPhase(prevBuffer, nextBuffer, mode, scrollbackOffset, termRows);
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ../packages/ag-term/src/runtime/create-buffer.ts
|
|
55
|
+
init_buffer();
|
|
56
|
+
function createBuffer(termBuffer, nodes) {
|
|
57
|
+
let _text;
|
|
58
|
+
let _ansi;
|
|
59
|
+
return {
|
|
60
|
+
get text() {
|
|
61
|
+
return _text ??= bufferToText(termBuffer);
|
|
62
|
+
},
|
|
63
|
+
get ansi() {
|
|
64
|
+
return _ansi ??= bufferToStyledText(termBuffer);
|
|
65
|
+
},
|
|
66
|
+
nodes,
|
|
67
|
+
_buffer: termBuffer
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region ../packages/create/src/streams/index.ts
|
|
72
|
+
/**
|
|
73
|
+
* AsyncIterable stream helpers for event-driven TUI architecture.
|
|
74
|
+
*
|
|
75
|
+
* These are pure functions over AsyncIterables - no EventEmitters, no callbacks.
|
|
76
|
+
* All helpers properly handle cleanup via return() on early break.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const keys = term.keys()
|
|
81
|
+
* const resizes = term.resizes()
|
|
82
|
+
*
|
|
83
|
+
* // Merge multiple sources
|
|
84
|
+
* const events = merge(
|
|
85
|
+
* map(keys, k => ({ type: 'key', ...k })),
|
|
86
|
+
* map(resizes, r => ({ type: 'resize', ...r }))
|
|
87
|
+
* )
|
|
88
|
+
*
|
|
89
|
+
* // Consume until ctrl+c
|
|
90
|
+
* for await (const event of events) {
|
|
91
|
+
* if (event.type === 'key' && event.key === 'ctrl+c') break
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* Merge multiple AsyncIterables into one.
|
|
97
|
+
*
|
|
98
|
+
* Values are emitted in arrival order (first-come). When all sources complete,
|
|
99
|
+
* the merged iterable completes. If any source throws, the error propagates
|
|
100
|
+
* and remaining sources are cleaned up.
|
|
101
|
+
*
|
|
102
|
+
* IMPORTANT: Each call to merge() creates a fresh iterable. Don't share
|
|
103
|
+
* the same merged iterable between multiple consumers.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const merged = merge(keys, resizes, ticks)
|
|
108
|
+
* for await (const event of merged) {
|
|
109
|
+
* // Process events from any source
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
async function* merge(...sources) {
|
|
114
|
+
if (sources.length === 0) return;
|
|
115
|
+
const iterators = sources.map((source) => source[Symbol.asyncIterator]());
|
|
116
|
+
const pending = /* @__PURE__ */ new Map();
|
|
117
|
+
async function nextWithIndex(idx) {
|
|
118
|
+
const iterator = iterators[idx];
|
|
119
|
+
if (!iterator) throw new Error(`No iterator at index ${idx}`);
|
|
120
|
+
return {
|
|
121
|
+
index: idx,
|
|
122
|
+
result: await iterator.next()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
for (let i = 0; i < iterators.length; i++) pending.set(i, nextWithIndex(i));
|
|
126
|
+
try {
|
|
127
|
+
while (pending.size > 0) {
|
|
128
|
+
const { index, result } = await Promise.race(pending.values());
|
|
129
|
+
if (result.done) pending.delete(index);
|
|
130
|
+
else {
|
|
131
|
+
yield result.value;
|
|
132
|
+
pending.set(index, nextWithIndex(index));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
await Promise.all(iterators.map((it) => it.return ? it.return() : Promise.resolve()));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Transform each value from an AsyncIterable.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const keyEvents = map(keys, k => ({ type: 'key' as const, key: k }))
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
async function* map(source, fn) {
|
|
148
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
149
|
+
try {
|
|
150
|
+
for await (const value of { [Symbol.asyncIterator]: () => iterator }) yield fn(value);
|
|
151
|
+
} finally {
|
|
152
|
+
if (iterator.return) await iterator.return();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Take values until an AbortSignal fires.
|
|
157
|
+
*
|
|
158
|
+
* When the signal aborts, the iterator completes gracefully (no error thrown).
|
|
159
|
+
* The source iterator is properly cleaned up.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const controller = new AbortController()
|
|
164
|
+
* const events = takeUntil(allEvents, controller.signal)
|
|
165
|
+
*
|
|
166
|
+
* // Later: controller.abort() will end the iteration
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
async function* takeUntil(source, signal) {
|
|
170
|
+
if (signal.aborted) return;
|
|
171
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
172
|
+
let abortResolve;
|
|
173
|
+
const abortPromise = new Promise((resolve) => {
|
|
174
|
+
abortResolve = resolve;
|
|
175
|
+
});
|
|
176
|
+
const onAbort = () => abortResolve();
|
|
177
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
178
|
+
try {
|
|
179
|
+
while (!signal.aborted) {
|
|
180
|
+
const result = await Promise.race([iterator.next(), abortPromise.then(() => ({
|
|
181
|
+
done: true,
|
|
182
|
+
value: void 0
|
|
183
|
+
}))]);
|
|
184
|
+
if (result.done) break;
|
|
185
|
+
yield result.value;
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
signal.removeEventListener("abort", onAbort);
|
|
189
|
+
if (iterator.return) await iterator.return();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region ../packages/ag-term/src/runtime/create-runtime.ts
|
|
194
|
+
/**
|
|
195
|
+
* Create the silvery-loop runtime kernel.
|
|
196
|
+
*
|
|
197
|
+
* The runtime owns the event loop, diffing, and output. Users interact via:
|
|
198
|
+
* - events() - AsyncIterable of all events (keys, resize, effects)
|
|
199
|
+
* - schedule() - Queue effects for async execution
|
|
200
|
+
* - render() - Output a buffer (diffing handled internally)
|
|
201
|
+
*
|
|
202
|
+
* NOTE: This runtime is designed for single-consumer use. Calling events()
|
|
203
|
+
* multiple times concurrently will cause events to be split between consumers.
|
|
204
|
+
* Each call returns a fresh AsyncIterable, but they share the underlying queue.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* using runtime = createRuntime({ target: termTarget })
|
|
209
|
+
*
|
|
210
|
+
* for await (const event of runtime.events()) {
|
|
211
|
+
* state = reducer(state, event)
|
|
212
|
+
* runtime.render(layout(view(state), runtime.getDims()))
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
/**
|
|
217
|
+
* Create an event channel that bridges callbacks to AsyncIterable.
|
|
218
|
+
*
|
|
219
|
+
* This is the single point where callbacks (resize, effect completion)
|
|
220
|
+
* are converted to the async iterable pattern. External sources like
|
|
221
|
+
* keyboard events are already AsyncIterable and merged at a higher level.
|
|
222
|
+
*/
|
|
223
|
+
function createEventChannel(signal) {
|
|
224
|
+
const queue = [];
|
|
225
|
+
let pendingResolve;
|
|
226
|
+
let disposed = false;
|
|
227
|
+
const onAbort = () => {
|
|
228
|
+
if (pendingResolve) {
|
|
229
|
+
pendingResolve(null);
|
|
230
|
+
pendingResolve = void 0;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
234
|
+
return {
|
|
235
|
+
push(event) {
|
|
236
|
+
if (disposed || signal.aborted) return;
|
|
237
|
+
if (pendingResolve) {
|
|
238
|
+
const r = pendingResolve;
|
|
239
|
+
pendingResolve = void 0;
|
|
240
|
+
r(event);
|
|
241
|
+
} else queue.push(event);
|
|
242
|
+
},
|
|
243
|
+
events() {
|
|
244
|
+
return { [Symbol.asyncIterator]() {
|
|
245
|
+
return { async next() {
|
|
246
|
+
if (disposed || signal.aborted) return {
|
|
247
|
+
done: true,
|
|
248
|
+
value: void 0
|
|
249
|
+
};
|
|
250
|
+
if (queue.length > 0) return {
|
|
251
|
+
done: false,
|
|
252
|
+
value: queue.shift()
|
|
253
|
+
};
|
|
254
|
+
const event = await new Promise((resolve) => {
|
|
255
|
+
pendingResolve = resolve;
|
|
256
|
+
});
|
|
257
|
+
if (event === null || disposed || signal.aborted) return {
|
|
258
|
+
done: true,
|
|
259
|
+
value: void 0
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
done: false,
|
|
263
|
+
value: event
|
|
264
|
+
};
|
|
265
|
+
} };
|
|
266
|
+
} };
|
|
267
|
+
},
|
|
268
|
+
dispose() {
|
|
269
|
+
disposed = true;
|
|
270
|
+
signal.removeEventListener("abort", onAbort);
|
|
271
|
+
if (pendingResolve) {
|
|
272
|
+
pendingResolve(null);
|
|
273
|
+
pendingResolve = void 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Create a runtime kernel.
|
|
280
|
+
*
|
|
281
|
+
* @param options Runtime configuration
|
|
282
|
+
* @returns Runtime instance implementing Symbol.dispose
|
|
283
|
+
*/
|
|
284
|
+
function createRuntime(options) {
|
|
285
|
+
const { target, signal: externalSignal, mode = "fullscreen" } = options;
|
|
286
|
+
const fallbackOutputPhase = mode === "inline" ? createOutputPhase({}) : void 0;
|
|
287
|
+
let outputPhaseFn = options.outputPhaseFn ?? fallbackOutputPhase;
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
const signal = controller.signal;
|
|
290
|
+
let externalAbortHandler;
|
|
291
|
+
if (externalSignal) if (externalSignal.aborted) controller.abort();
|
|
292
|
+
else {
|
|
293
|
+
externalAbortHandler = () => controller.abort();
|
|
294
|
+
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
295
|
+
}
|
|
296
|
+
let prevBuffer = null;
|
|
297
|
+
let scrollbackOffset = 0;
|
|
298
|
+
let disposed = false;
|
|
299
|
+
const eventChannel = createEventChannel(signal);
|
|
300
|
+
let unsubscribeResize;
|
|
301
|
+
if (target.onResize) unsubscribeResize = target.onResize((dims) => {
|
|
302
|
+
eventChannel.push({
|
|
303
|
+
type: "resize",
|
|
304
|
+
cols: dims.cols,
|
|
305
|
+
rows: dims.rows
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
let effectId = 0;
|
|
309
|
+
return {
|
|
310
|
+
events() {
|
|
311
|
+
return takeUntil(eventChannel.events(), signal);
|
|
312
|
+
},
|
|
313
|
+
schedule(effect, opts) {
|
|
314
|
+
if (disposed) return;
|
|
315
|
+
const id = `effect-${effectId++}`;
|
|
316
|
+
const effectSignal = opts?.signal;
|
|
317
|
+
if (effectSignal?.aborted) return;
|
|
318
|
+
const execute = async () => {
|
|
319
|
+
let abortHandler;
|
|
320
|
+
try {
|
|
321
|
+
if (effectSignal) {
|
|
322
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
323
|
+
abortHandler = () => reject(/* @__PURE__ */ new Error("Effect aborted"));
|
|
324
|
+
effectSignal.addEventListener("abort", abortHandler, { once: true });
|
|
325
|
+
});
|
|
326
|
+
const result = await Promise.race([effect(), aborted]);
|
|
327
|
+
if (abortHandler) effectSignal.removeEventListener("abort", abortHandler);
|
|
328
|
+
eventChannel.push({
|
|
329
|
+
type: "effect",
|
|
330
|
+
id,
|
|
331
|
+
result
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
const result = await effect();
|
|
335
|
+
eventChannel.push({
|
|
336
|
+
type: "effect",
|
|
337
|
+
id,
|
|
338
|
+
result
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (abortHandler && effectSignal) effectSignal.removeEventListener("abort", abortHandler);
|
|
343
|
+
if (error instanceof Error && (error.message === "Effect aborted" || error.name === "AbortError")) return;
|
|
344
|
+
eventChannel.push({
|
|
345
|
+
type: "error",
|
|
346
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
queueMicrotask(() => {
|
|
351
|
+
execute();
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
render(buffer) {
|
|
355
|
+
if (disposed) return;
|
|
356
|
+
const offset = scrollbackOffset;
|
|
357
|
+
scrollbackOffset = 0;
|
|
358
|
+
const termRows = target.getDims().rows;
|
|
359
|
+
let patch;
|
|
360
|
+
if (outputPhaseFn) {
|
|
361
|
+
const prevBuf = prevBuffer?._buffer ?? null;
|
|
362
|
+
const nextBuf = buffer._buffer;
|
|
363
|
+
patch = outputPhaseFn(prevBuf, nextBuf, mode, offset, termRows);
|
|
364
|
+
} else patch = diff(prevBuffer, buffer, mode, offset, termRows);
|
|
365
|
+
prevBuffer = buffer;
|
|
366
|
+
if (process.env.SILVERY_CAPTURE_RAW) try {
|
|
367
|
+
__require("fs").appendFileSync("/tmp/silvery-runtime-raw.ansi", patch);
|
|
368
|
+
} catch {}
|
|
369
|
+
target.write(patch);
|
|
370
|
+
},
|
|
371
|
+
addScrollbackLines(lines) {
|
|
372
|
+
if (mode !== "inline" || lines <= 0) return;
|
|
373
|
+
scrollbackOffset += lines;
|
|
374
|
+
},
|
|
375
|
+
invalidate() {
|
|
376
|
+
prevBuffer = null;
|
|
377
|
+
},
|
|
378
|
+
setOutputPhaseFn(fn) {
|
|
379
|
+
if (fn) outputPhaseFn = fn;
|
|
380
|
+
},
|
|
381
|
+
resetInlineCursor() {
|
|
382
|
+
outputPhaseFn?.resetInlineState?.();
|
|
383
|
+
},
|
|
384
|
+
getInlineCursorRow() {
|
|
385
|
+
return outputPhaseFn?.getInlineCursorRow?.() ?? -1;
|
|
386
|
+
},
|
|
387
|
+
promoteScrollback(content, lines) {
|
|
388
|
+
outputPhaseFn?.promoteScrollback?.(content, lines);
|
|
389
|
+
},
|
|
390
|
+
getDims() {
|
|
391
|
+
return target.getDims();
|
|
392
|
+
},
|
|
393
|
+
[Symbol.dispose]() {
|
|
394
|
+
if (disposed) return;
|
|
395
|
+
disposed = true;
|
|
396
|
+
controller.abort();
|
|
397
|
+
if (externalAbortHandler && externalSignal) externalSignal.removeEventListener("abort", externalAbortHandler);
|
|
398
|
+
if (unsubscribeResize) unsubscribeResize();
|
|
399
|
+
eventChannel.dispose();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region ../packages/create/src/signal-store.ts
|
|
405
|
+
/**
|
|
406
|
+
* Signal Store — Zustand StoreApi-compatible store backed by alien-signals.
|
|
407
|
+
*
|
|
408
|
+
* Drop-in replacement for Zustand's createStore(). Provides the same
|
|
409
|
+
* StoreApi<T> interface so useApp()/StoreContext keep working unchanged.
|
|
410
|
+
*
|
|
411
|
+
* Also re-exports StateCreator for backward compatibility with code
|
|
412
|
+
* that imported it from "zustand".
|
|
413
|
+
*/
|
|
414
|
+
function createStore(factory) {
|
|
415
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
416
|
+
const state$ = signal(void 0);
|
|
417
|
+
let initialState;
|
|
418
|
+
const setState = (partial, replace) => {
|
|
419
|
+
const prev = state$();
|
|
420
|
+
const raw = typeof partial === "function" ? partial(prev) : partial;
|
|
421
|
+
let next;
|
|
422
|
+
if (!replace && raw !== null && typeof raw === "object" && !Array.isArray(raw)) next = {
|
|
423
|
+
...prev,
|
|
424
|
+
...raw
|
|
425
|
+
};
|
|
426
|
+
else next = raw;
|
|
427
|
+
if (Object.is(prev, next)) return;
|
|
428
|
+
state$(next);
|
|
429
|
+
for (const listener of listeners) listener(next, prev);
|
|
430
|
+
};
|
|
431
|
+
const getState = () => state$();
|
|
432
|
+
const getInitialState = () => initialState;
|
|
433
|
+
const subscribe = (listener) => {
|
|
434
|
+
listeners.add(listener);
|
|
435
|
+
return () => {
|
|
436
|
+
listeners.delete(listener);
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
const api = {
|
|
440
|
+
setState,
|
|
441
|
+
getState,
|
|
442
|
+
getInitialState,
|
|
443
|
+
subscribe
|
|
444
|
+
};
|
|
445
|
+
const created = factory(setState, getState, api);
|
|
446
|
+
state$(created);
|
|
447
|
+
initialState = created;
|
|
448
|
+
return api;
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region ../packages/ag-react/src/error-boundary.tsx
|
|
452
|
+
/**
|
|
453
|
+
* ErrorBoundary — Built-in error boundary for silvery apps.
|
|
454
|
+
*
|
|
455
|
+
* Catches render errors in the component tree and displays a rich
|
|
456
|
+
* error message with source location, code excerpt, and stack trace
|
|
457
|
+
* using low-level silvery host elements (no dependency on Box/Text
|
|
458
|
+
* from @silvery/ag-react/ui). This is the default root wrapper for all
|
|
459
|
+
* silvery apps — createApp() and run() wrap the element tree with it
|
|
460
|
+
* automatically.
|
|
461
|
+
*
|
|
462
|
+
* Uses `silvery-box` and `silvery-text` host elements directly to
|
|
463
|
+
* avoid circular deps with higher-level component libraries.
|
|
464
|
+
*
|
|
465
|
+
* @packageDocumentation
|
|
466
|
+
*/
|
|
467
|
+
/**
|
|
468
|
+
* Parse a stack line to extract function name, file, line, column.
|
|
469
|
+
* Handles both `at Foo (file:line:col)` and `at file:line:col` formats.
|
|
470
|
+
*/
|
|
471
|
+
function parseStackLine(line) {
|
|
472
|
+
const trimmed = line.trim();
|
|
473
|
+
if (!trimmed.startsWith("at ")) return null;
|
|
474
|
+
const rest = trimmed.slice(3);
|
|
475
|
+
const match1 = rest.match(/^(.+?)\s+\((.+?):(\d+):(\d+)\)$/);
|
|
476
|
+
if (match1) return {
|
|
477
|
+
function: match1[1],
|
|
478
|
+
file: match1[2],
|
|
479
|
+
line: Number(match1[3]),
|
|
480
|
+
column: Number(match1[4])
|
|
481
|
+
};
|
|
482
|
+
const match2 = rest.match(/^(.+?):(\d+):(\d+)$/);
|
|
483
|
+
if (match2) return {
|
|
484
|
+
file: match2[1],
|
|
485
|
+
line: Number(match2[2]),
|
|
486
|
+
column: Number(match2[3])
|
|
487
|
+
};
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Clean up file path by removing cwd prefix and file:// protocol.
|
|
492
|
+
*/
|
|
493
|
+
function cleanupPath(filePath) {
|
|
494
|
+
if (!filePath) return filePath;
|
|
495
|
+
let p = filePath;
|
|
496
|
+
const cwdPath = process.cwd();
|
|
497
|
+
p = p.replace(/^file:\/\//, "");
|
|
498
|
+
for (const prefix of [cwdPath, `/private${cwdPath}`]) if (p.startsWith(`${prefix}/`)) {
|
|
499
|
+
p = p.slice(prefix.length + 1);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
return p;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get source code excerpt around a line number (±3 lines).
|
|
506
|
+
*/
|
|
507
|
+
function getCodeExcerpt(filePath, line) {
|
|
508
|
+
try {
|
|
509
|
+
if (!fs.existsSync(filePath)) return null;
|
|
510
|
+
const lines = fs.readFileSync(filePath, "utf8").split("\n");
|
|
511
|
+
const start = Math.max(0, line - 4);
|
|
512
|
+
const end = Math.min(lines.length, line + 3);
|
|
513
|
+
const result = [];
|
|
514
|
+
for (let i = start; i < end; i++) result.push({
|
|
515
|
+
line: i + 1,
|
|
516
|
+
value: (lines[i] ?? "").replace(/\t/g, " ")
|
|
517
|
+
});
|
|
518
|
+
return result;
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Rich error boundary for silvery's runtime layer.
|
|
525
|
+
*
|
|
526
|
+
* Must be a class component (React limitation for error boundaries).
|
|
527
|
+
* Renders error info using silvery-box/silvery-text host elements — no Box/Text dependency.
|
|
528
|
+
* Shows: ERROR label, error message, file location, source code excerpt, and stack trace.
|
|
529
|
+
*/
|
|
530
|
+
var SilveryErrorBoundary = class extends Component {
|
|
531
|
+
state = { error: null };
|
|
532
|
+
static getDerivedStateFromError(error) {
|
|
533
|
+
return { error };
|
|
534
|
+
}
|
|
535
|
+
componentDidCatch(error) {
|
|
536
|
+
this.props.onError?.(error);
|
|
537
|
+
}
|
|
538
|
+
render() {
|
|
539
|
+
if (this.state.error) {
|
|
540
|
+
const err = this.state.error;
|
|
541
|
+
const stack = err.stack ? err.stack.split("\n").slice(1) : [];
|
|
542
|
+
const origin = stack.length > 0 ? parseStackLine(stack[0]) : null;
|
|
543
|
+
const filePath = cleanupPath(origin?.file);
|
|
544
|
+
let excerpt = null;
|
|
545
|
+
let lineWidth = 0;
|
|
546
|
+
if (filePath && origin?.line) {
|
|
547
|
+
excerpt = getCodeExcerpt(filePath, origin.line);
|
|
548
|
+
if (excerpt) for (const { line } of excerpt) lineWidth = Math.max(lineWidth, String(line).length);
|
|
549
|
+
}
|
|
550
|
+
const children = [];
|
|
551
|
+
children.push(React.createElement("silvery-box", { key: "header" }, React.createElement("silvery-text", {
|
|
552
|
+
backgroundColor: "red",
|
|
553
|
+
color: "white"
|
|
554
|
+
}, " ERROR "), React.createElement("silvery-text", {}, ` ${err.message}`)));
|
|
555
|
+
if (filePath && origin) children.push(React.createElement("silvery-box", {
|
|
556
|
+
key: "location",
|
|
557
|
+
marginTop: 1
|
|
558
|
+
}, React.createElement("silvery-text", { dimColor: true }, `${filePath}:${origin.line}:${origin.column}`)));
|
|
559
|
+
if (excerpt && origin) {
|
|
560
|
+
const codeLines = excerpt.map(({ line, value }) => {
|
|
561
|
+
const lineNum = String(line).padStart(lineWidth, " ");
|
|
562
|
+
return React.createElement("silvery-box", { key: `code-${line}` }, React.createElement("silvery-text", {
|
|
563
|
+
dimColor: line !== origin.line,
|
|
564
|
+
backgroundColor: line === origin.line ? "red" : void 0,
|
|
565
|
+
color: line === origin.line ? "white" : void 0
|
|
566
|
+
}, `${lineNum}:`), React.createElement("silvery-text", {
|
|
567
|
+
backgroundColor: line === origin.line ? "red" : void 0,
|
|
568
|
+
color: line === origin.line ? "white" : void 0
|
|
569
|
+
}, ` ${value}`));
|
|
570
|
+
});
|
|
571
|
+
children.push(React.createElement("silvery-box", {
|
|
572
|
+
key: "code",
|
|
573
|
+
marginTop: 1,
|
|
574
|
+
flexDirection: "column"
|
|
575
|
+
}, ...codeLines));
|
|
576
|
+
}
|
|
577
|
+
if (stack.length > 0) {
|
|
578
|
+
const stackLines = stack.map((line, i) => {
|
|
579
|
+
const parsed = parseStackLine(line);
|
|
580
|
+
if (!parsed) return React.createElement("silvery-box", { key: `stack-${i}` }, React.createElement("silvery-text", { dimColor: true }, `- ${line.trim()}`));
|
|
581
|
+
const cleanFile = cleanupPath(parsed.file);
|
|
582
|
+
return React.createElement("silvery-box", { key: `stack-${i}` }, React.createElement("silvery-text", { dimColor: true }, "- "), React.createElement("silvery-text", {
|
|
583
|
+
dimColor: true,
|
|
584
|
+
bold: true
|
|
585
|
+
}, parsed.function ?? ""), React.createElement("silvery-text", {
|
|
586
|
+
dimColor: true,
|
|
587
|
+
color: "gray"
|
|
588
|
+
}, ` (${cleanFile ?? ""}:${parsed.line}:${parsed.column})`));
|
|
589
|
+
});
|
|
590
|
+
children.push(React.createElement("silvery-box", {
|
|
591
|
+
key: "stack",
|
|
592
|
+
marginTop: 1,
|
|
593
|
+
flexDirection: "column"
|
|
594
|
+
}, ...stackLines));
|
|
595
|
+
}
|
|
596
|
+
return React.createElement("silvery-box", {
|
|
597
|
+
flexDirection: "column",
|
|
598
|
+
padding: 1
|
|
599
|
+
}, ...children);
|
|
600
|
+
}
|
|
601
|
+
return this.props.children;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region ../packages/ag/src/focus-events.ts
|
|
606
|
+
/**
|
|
607
|
+
* Create a synthetic keyboard event.
|
|
608
|
+
*/
|
|
609
|
+
function createKeyEvent(input, key, target) {
|
|
610
|
+
let propagationStopped = false;
|
|
611
|
+
let defaultPrevented = false;
|
|
612
|
+
return {
|
|
613
|
+
key: input,
|
|
614
|
+
input,
|
|
615
|
+
ctrl: key.ctrl,
|
|
616
|
+
meta: key.meta,
|
|
617
|
+
shift: key.shift,
|
|
618
|
+
super: key.super,
|
|
619
|
+
hyper: key.hyper,
|
|
620
|
+
eventType: key.eventType,
|
|
621
|
+
target,
|
|
622
|
+
currentTarget: target,
|
|
623
|
+
nativeEvent: {
|
|
624
|
+
input,
|
|
625
|
+
key
|
|
626
|
+
},
|
|
627
|
+
get propagationStopped() {
|
|
628
|
+
return propagationStopped;
|
|
629
|
+
},
|
|
630
|
+
get defaultPrevented() {
|
|
631
|
+
return defaultPrevented;
|
|
632
|
+
},
|
|
633
|
+
stopPropagation() {
|
|
634
|
+
propagationStopped = true;
|
|
635
|
+
},
|
|
636
|
+
preventDefault() {
|
|
637
|
+
defaultPrevented = true;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Create a synthetic focus event.
|
|
643
|
+
*/
|
|
644
|
+
function createFocusEvent(type, target, relatedTarget) {
|
|
645
|
+
let propagationStopped = false;
|
|
646
|
+
return {
|
|
647
|
+
type,
|
|
648
|
+
target,
|
|
649
|
+
relatedTarget,
|
|
650
|
+
currentTarget: target,
|
|
651
|
+
get propagationStopped() {
|
|
652
|
+
return propagationStopped;
|
|
653
|
+
},
|
|
654
|
+
stopPropagation() {
|
|
655
|
+
propagationStopped = true;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Dispatch a keyboard event through the render tree with DOM-style
|
|
661
|
+
* capture/target/bubble phases.
|
|
662
|
+
*
|
|
663
|
+
* For press/repeat events:
|
|
664
|
+
* 1. Capture phase: root → target (onKeyDownCapture props)
|
|
665
|
+
* 2. Target phase: target's onKeyDown
|
|
666
|
+
* 3. Bubble phase: target parent → root (onKeyDown props)
|
|
667
|
+
*
|
|
668
|
+
* For release events:
|
|
669
|
+
* 1. Target phase: target's onKeyUp
|
|
670
|
+
* 2. Bubble phase: target parent → root (onKeyUp props)
|
|
671
|
+
* (No capture phase for keyUp — deliberate simplification; React DOM has onKeyUpCapture)
|
|
672
|
+
*
|
|
673
|
+
* stopPropagation() halts traversal at any phase.
|
|
674
|
+
*/
|
|
675
|
+
function dispatchKeyEvent(event, dispatch) {
|
|
676
|
+
const path = getAncestorPath(event.target);
|
|
677
|
+
const mutableEvent = event;
|
|
678
|
+
const isRelease = event.eventType === "release";
|
|
679
|
+
const handlerProp = isRelease ? "onKeyUp" : "onKeyDown";
|
|
680
|
+
if (!isRelease) for (let i = path.length - 1; i > 0; i--) {
|
|
681
|
+
if (event.propagationStopped) return;
|
|
682
|
+
const node = path[i];
|
|
683
|
+
const handler = node.props.onKeyDownCapture;
|
|
684
|
+
if (handler) {
|
|
685
|
+
mutableEvent.currentTarget = node;
|
|
686
|
+
handler(event);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (!event.propagationStopped) {
|
|
690
|
+
const target = path[0];
|
|
691
|
+
mutableEvent.currentTarget = target;
|
|
692
|
+
const handler = target.props[handlerProp];
|
|
693
|
+
if (handler) handler(event, dispatch);
|
|
694
|
+
}
|
|
695
|
+
for (let i = 1; i < path.length; i++) {
|
|
696
|
+
if (event.propagationStopped) return;
|
|
697
|
+
const node = path[i];
|
|
698
|
+
const handler = node.props[handlerProp];
|
|
699
|
+
if (handler) {
|
|
700
|
+
mutableEvent.currentTarget = node;
|
|
701
|
+
handler(event, dispatch);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Dispatch a focus event through the render tree.
|
|
707
|
+
*
|
|
708
|
+
* Fires onFocus/onBlur on the target, then bubbles to ancestors.
|
|
709
|
+
*/
|
|
710
|
+
function dispatchFocusEvent(event) {
|
|
711
|
+
const handlerProp = event.type === "focus" ? "onFocus" : "onBlur";
|
|
712
|
+
const path = getAncestorPath(event.target);
|
|
713
|
+
const mutableEvent = event;
|
|
714
|
+
for (const node of path) {
|
|
715
|
+
if (event.propagationStopped) break;
|
|
716
|
+
const handler = node.props[handlerProp];
|
|
717
|
+
if (handler) {
|
|
718
|
+
mutableEvent.currentTarget = node;
|
|
719
|
+
handler(event);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region ../packages/ag-term/src/scheduler.ts
|
|
725
|
+
init_buffer();
|
|
726
|
+
createLogger("silvery:scheduler");
|
|
727
|
+
process.env.SILVERY_SYNC_UPDATE === "1" || process.env.SILVERY_SYNC_UPDATE;
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region ../packages/ag-term/src/runtime/event-handlers.ts
|
|
730
|
+
/**
|
|
731
|
+
* Build the EventHandlerContext passed to user-defined event handlers.
|
|
732
|
+
* Shared by runEventHandler() and press().
|
|
733
|
+
*
|
|
734
|
+
* When the store was created with `tea()` middleware, `dispatch` is
|
|
735
|
+
* automatically wired from the store state.
|
|
736
|
+
*/
|
|
737
|
+
function createHandlerContext(store, focusManager, container) {
|
|
738
|
+
const state = store.getState();
|
|
739
|
+
const teaDispatch = typeof state.dispatch === "function" ? state.dispatch : void 0;
|
|
740
|
+
return {
|
|
741
|
+
set: store.setState,
|
|
742
|
+
get: store.getState,
|
|
743
|
+
focusManager,
|
|
744
|
+
focus(testID) {
|
|
745
|
+
const root = getContainerRoot(container);
|
|
746
|
+
focusManager.focusById(testID, root, "programmatic");
|
|
747
|
+
},
|
|
748
|
+
activateScope(scopeId) {
|
|
749
|
+
const root = getContainerRoot(container);
|
|
750
|
+
focusManager.activateScope(scopeId, root);
|
|
751
|
+
},
|
|
752
|
+
getFocusPath() {
|
|
753
|
+
const root = getContainerRoot(container);
|
|
754
|
+
return focusManager.getFocusPath(root);
|
|
755
|
+
},
|
|
756
|
+
dispatch: teaDispatch,
|
|
757
|
+
hitTest(x, y) {
|
|
758
|
+
return hitTest(getContainerRoot(container), x, y);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Dispatch a key event through the focus system and handle default
|
|
764
|
+
* focus navigation (Tab, Shift+Tab, Enter scope, Escape scope).
|
|
765
|
+
*
|
|
766
|
+
* Returns "consumed" if the focus system handled the event (caller should
|
|
767
|
+
* render and return), or "continue" if the event should proceed to app handlers.
|
|
768
|
+
*/
|
|
769
|
+
function handleFocusNavigation(input, parsedKey, focusManager, container) {
|
|
770
|
+
if (focusManager.activeElement) {
|
|
771
|
+
const keyEvent = createKeyEvent(input, parsedKey, focusManager.activeElement);
|
|
772
|
+
dispatchKeyEvent(keyEvent);
|
|
773
|
+
if (keyEvent.propagationStopped || keyEvent.defaultPrevented) return "consumed";
|
|
774
|
+
}
|
|
775
|
+
const root = getContainerRoot(container);
|
|
776
|
+
if (parsedKey.tab && !parsedKey.shift) {
|
|
777
|
+
focusManager.focusNext(root);
|
|
778
|
+
return "consumed";
|
|
779
|
+
}
|
|
780
|
+
if (parsedKey.tab && parsedKey.shift) {
|
|
781
|
+
focusManager.focusPrev(root);
|
|
782
|
+
return "consumed";
|
|
783
|
+
}
|
|
784
|
+
if (parsedKey.return && focusManager.activeElement) {
|
|
785
|
+
const activeEl = focusManager.activeElement;
|
|
786
|
+
const props = activeEl.props;
|
|
787
|
+
const testID = typeof props.testID === "string" ? props.testID : null;
|
|
788
|
+
if (props.focusScope && testID) {
|
|
789
|
+
focusManager.enterScope(testID);
|
|
790
|
+
focusManager.focusNext(root, activeEl);
|
|
791
|
+
return "consumed";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (parsedKey.escape) {
|
|
795
|
+
if (focusManager.scopeStack.length > 0) {
|
|
796
|
+
const scopeId = focusManager.scopeStack[focusManager.scopeStack.length - 1];
|
|
797
|
+
focusManager.exitScope();
|
|
798
|
+
const scopeNode = findByTestID(root, scopeId);
|
|
799
|
+
if (scopeNode) focusManager.focus(scopeNode, "keyboard");
|
|
800
|
+
return "consumed";
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return "continue";
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Dispatch a DOM-level mouse event to the node tree.
|
|
807
|
+
* Called from runEventHandler for mouse events.
|
|
808
|
+
*/
|
|
809
|
+
function dispatchMouseEventToTree(event, mouseEventState, root) {
|
|
810
|
+
if (event.event !== "mouse" || !event.data) return false;
|
|
811
|
+
const mouseData = event.data;
|
|
812
|
+
return processMouseEvent(mouseEventState, {
|
|
813
|
+
button: mouseData.button,
|
|
814
|
+
x: mouseData.x,
|
|
815
|
+
y: mouseData.y,
|
|
816
|
+
action: mouseData.action,
|
|
817
|
+
delta: mouseData.delta,
|
|
818
|
+
shift: mouseData.shift,
|
|
819
|
+
meta: mouseData.meta,
|
|
820
|
+
ctrl: mouseData.ctrl
|
|
821
|
+
}, root);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Invoke the namespaced handler for a single event (state mutation only, no render).
|
|
825
|
+
* Returns true to continue, false to exit, or "flush" for a render barrier.
|
|
826
|
+
*
|
|
827
|
+
* Also dispatches DOM-level mouse events when applicable.
|
|
828
|
+
*/
|
|
829
|
+
function invokeEventHandler(event, handlers, ctx, mouseEventState, container) {
|
|
830
|
+
if (dispatchMouseEventToTree(event, mouseEventState, getContainerRoot(container))) return true;
|
|
831
|
+
const namespacedHandler = handlers?.[event.type];
|
|
832
|
+
if (namespacedHandler && typeof namespacedHandler === "function") {
|
|
833
|
+
const result = namespacedHandler(event.data, ctx);
|
|
834
|
+
if (result === "exit") return false;
|
|
835
|
+
if (result === "flush") return "flush";
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Dispatch a term:key event to app handlers (namespaced + legacy).
|
|
841
|
+
* Returns "exit" if the handler signaled exit, undefined otherwise.
|
|
842
|
+
*/
|
|
843
|
+
function dispatchKeyToHandlers(input, parsedKey, handlers, ctx) {
|
|
844
|
+
const namespacedHandler = handlers?.["term:key"];
|
|
845
|
+
if (namespacedHandler && typeof namespacedHandler === "function") {
|
|
846
|
+
if (namespacedHandler({
|
|
847
|
+
input,
|
|
848
|
+
key: parsedKey
|
|
849
|
+
}, ctx) === "exit") return "exit";
|
|
850
|
+
}
|
|
851
|
+
if (handlers?.key) {
|
|
852
|
+
if (handlers.key(input, parsedKey, ctx) === "exit") return "exit";
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region ../packages/ag-term/src/runtime/terminal-lifecycle.ts
|
|
857
|
+
/**
|
|
858
|
+
* Terminal Lifecycle Events
|
|
859
|
+
*
|
|
860
|
+
* Handles suspend/resume (Ctrl+Z/SIGCONT) and interrupt (Ctrl+C) for TUI apps.
|
|
861
|
+
* When stdin is in raw mode, the terminal does not generate SIGTSTP/SIGINT for
|
|
862
|
+
* Ctrl+Z/Ctrl+C. This module intercepts the raw bytes and manages the full
|
|
863
|
+
* terminal state save/restore cycle.
|
|
864
|
+
*
|
|
865
|
+
* Inspired by ncurses (endwin/refresh), bubbletea, and Textual.
|
|
866
|
+
*
|
|
867
|
+
* Protocols managed:
|
|
868
|
+
* - Raw mode (stdin)
|
|
869
|
+
* - Alternate screen buffer (DEC private mode 1049)
|
|
870
|
+
* - Cursor visibility (DEC private mode 25)
|
|
871
|
+
* - Mouse tracking (modes 1000, 1002, 1006)
|
|
872
|
+
* - Kitty keyboard protocol (CSI > flags u / CSI < u)
|
|
873
|
+
* - Bracketed paste (DEC private mode 2004)
|
|
874
|
+
* - SGR attributes (reset via CSI 0 m)
|
|
875
|
+
*/
|
|
876
|
+
/**
|
|
877
|
+
* Capture the current terminal protocol state.
|
|
878
|
+
*
|
|
879
|
+
* This builds a TerminalState from the options passed to run()/createApp(),
|
|
880
|
+
* since terminal state is not directly queryable from the OS.
|
|
881
|
+
*/
|
|
882
|
+
function captureTerminalState(opts) {
|
|
883
|
+
return {
|
|
884
|
+
rawMode: opts.rawMode ?? true,
|
|
885
|
+
alternateScreen: opts.alternateScreen ?? false,
|
|
886
|
+
cursorHidden: opts.cursorHidden ?? true,
|
|
887
|
+
mouseEnabled: opts.mouse ?? false,
|
|
888
|
+
kittyEnabled: opts.kitty ?? false,
|
|
889
|
+
kittyFlags: opts.kittyFlags ?? 11,
|
|
890
|
+
bracketedPaste: opts.bracketedPaste ?? false,
|
|
891
|
+
focusReporting: opts.focusReporting ?? false
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Restore terminal to normal state before suspending or exiting.
|
|
896
|
+
*
|
|
897
|
+
* Uses writeSync for reliability during signal handling (async write
|
|
898
|
+
* may not complete before the process suspends).
|
|
899
|
+
*
|
|
900
|
+
* Order matters: disable protocols first, then show cursor, then exit
|
|
901
|
+
* alternate screen, then disable raw mode.
|
|
902
|
+
*/
|
|
903
|
+
function restoreTerminalState(stdout, stdin) {
|
|
904
|
+
try {
|
|
905
|
+
stdin.removeAllListeners("data");
|
|
906
|
+
stdin.pause();
|
|
907
|
+
} catch {}
|
|
908
|
+
const sequences = [
|
|
909
|
+
"\x1B[0m",
|
|
910
|
+
"\x1B[?1004l",
|
|
911
|
+
disableMouse(),
|
|
912
|
+
disableKittyKeyboard(),
|
|
913
|
+
"\x1B[?2004l",
|
|
914
|
+
resetCursorStyle(),
|
|
915
|
+
"\x1B[?25h",
|
|
916
|
+
"\x1B[?1049l"
|
|
917
|
+
].join("");
|
|
918
|
+
if (stdout === process.stdout) try {
|
|
919
|
+
writeSync(stdout.fd, sequences);
|
|
920
|
+
} catch {
|
|
921
|
+
try {
|
|
922
|
+
stdout.write(sequences);
|
|
923
|
+
} catch {}
|
|
924
|
+
}
|
|
925
|
+
else try {
|
|
926
|
+
stdout.write(sequences);
|
|
927
|
+
} catch {}
|
|
928
|
+
try {
|
|
929
|
+
stdin.resume();
|
|
930
|
+
while (stdin.read() !== null);
|
|
931
|
+
stdin.pause();
|
|
932
|
+
} catch {}
|
|
933
|
+
if (stdin.isTTY && stdin.isRaw) try {
|
|
934
|
+
stdin.setRawMode(false);
|
|
935
|
+
} catch {}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Re-enter TUI mode after resuming from suspend (SIGCONT).
|
|
939
|
+
*
|
|
940
|
+
* Restores all protocols that were active before suspend, in the correct
|
|
941
|
+
* order: raw mode first, then alternate screen, then protocols, then
|
|
942
|
+
* trigger a full redraw via synthetic resize.
|
|
943
|
+
*/
|
|
944
|
+
function resumeTerminalState(state, stdout, stdin) {
|
|
945
|
+
if (state.rawMode && stdin.isTTY) try {
|
|
946
|
+
stdin.setRawMode(true);
|
|
947
|
+
stdin.resume();
|
|
948
|
+
} catch {}
|
|
949
|
+
const sequences = [];
|
|
950
|
+
if (state.alternateScreen) sequences.push("\x1B[?1049h");
|
|
951
|
+
sequences.push("\x1B[2J\x1B[H");
|
|
952
|
+
if (state.cursorHidden) sequences.push("\x1B[?25l");
|
|
953
|
+
if (state.kittyEnabled) sequences.push(enableKittyKeyboard(state.kittyFlags));
|
|
954
|
+
if (state.mouseEnabled) sequences.push(enableMouse());
|
|
955
|
+
if (state.bracketedPaste) sequences.push("\x1B[?2004h");
|
|
956
|
+
if (state.focusReporting) sequences.push("\x1B[?1004h");
|
|
957
|
+
const joined = sequences.join("");
|
|
958
|
+
if (stdout === process.stdout) try {
|
|
959
|
+
writeSync(stdout.fd, joined);
|
|
960
|
+
} catch {
|
|
961
|
+
try {
|
|
962
|
+
stdout.write(joined);
|
|
963
|
+
} catch {}
|
|
964
|
+
}
|
|
965
|
+
else try {
|
|
966
|
+
stdout.write(joined);
|
|
967
|
+
} catch {}
|
|
968
|
+
stdout.emit("resize");
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Execute the full suspend flow: save state, restore terminal, SIGTSTP,
|
|
972
|
+
* and set up SIGCONT handler to resume.
|
|
973
|
+
*
|
|
974
|
+
* @param state - Terminal state snapshot to restore on resume
|
|
975
|
+
* @param stdout - Output stream
|
|
976
|
+
* @param stdin - Input stream
|
|
977
|
+
* @param onResume - Optional callback after resume
|
|
978
|
+
*/
|
|
979
|
+
function performSuspend(state, stdout, stdin, onResume) {
|
|
980
|
+
restoreTerminalState(stdout, stdin);
|
|
981
|
+
process.once("SIGCONT", () => {
|
|
982
|
+
resumeTerminalState(state, stdout, stdin);
|
|
983
|
+
onResume?.();
|
|
984
|
+
});
|
|
985
|
+
process.kill(process.pid, "SIGTSTP");
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region ../packages/ag-term/src/features/selection.ts
|
|
989
|
+
/**
|
|
990
|
+
* Create a bridge SelectionFeature that delegates to an external state owner.
|
|
991
|
+
*
|
|
992
|
+
* Used by create-app to expose its inline selection state to React hooks
|
|
993
|
+
* (useSelection) and copy-mode (which calls setRange/clear) without
|
|
994
|
+
* duplicating the state machine. Mouse handlers are no-ops — the external
|
|
995
|
+
* owner (create-app's event loop) handles mouse events directly.
|
|
996
|
+
*/
|
|
997
|
+
function createSelectionBridge(options) {
|
|
998
|
+
return {
|
|
999
|
+
get state() {
|
|
1000
|
+
return options.getState();
|
|
1001
|
+
},
|
|
1002
|
+
subscribe(listener) {
|
|
1003
|
+
return options.subscribe(listener);
|
|
1004
|
+
},
|
|
1005
|
+
handleMouseDown(_col, _row, _altKey) {},
|
|
1006
|
+
handleMouseMove(_col, _row) {},
|
|
1007
|
+
handleMouseUp(_col, _row) {},
|
|
1008
|
+
setRange(range) {
|
|
1009
|
+
options.setRange(range);
|
|
1010
|
+
},
|
|
1011
|
+
clear() {
|
|
1012
|
+
options.clear();
|
|
1013
|
+
},
|
|
1014
|
+
dispose() {}
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
//#endregion
|
|
1018
|
+
//#region ../packages/create/src/internal/capability-registry.ts
|
|
1019
|
+
/** Create a new capability registry (one per app instance). */
|
|
1020
|
+
function createCapabilityRegistry() {
|
|
1021
|
+
const capabilities = /* @__PURE__ */ new Map();
|
|
1022
|
+
return {
|
|
1023
|
+
register(key, capability) {
|
|
1024
|
+
capabilities.set(key, capability);
|
|
1025
|
+
},
|
|
1026
|
+
get(key) {
|
|
1027
|
+
return capabilities.get(key);
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region ../packages/create/src/internal/capabilities.ts
|
|
1033
|
+
/**
|
|
1034
|
+
* Capability symbols — well-known keys for the CapabilityRegistry.
|
|
1035
|
+
*
|
|
1036
|
+
* Features register themselves under these symbols so other parts
|
|
1037
|
+
* of the composition chain can discover and interact with them.
|
|
1038
|
+
*
|
|
1039
|
+
* @internal Not exported from the public barrel.
|
|
1040
|
+
*/
|
|
1041
|
+
/** Selection feature: text selection state + mouse handling. */
|
|
1042
|
+
const SELECTION_CAPABILITY = Symbol.for("silvery.selection");
|
|
1043
|
+
/** Clipboard feature: copy/paste via OSC 52 or other backends. */
|
|
1044
|
+
const CLIPBOARD_CAPABILITY = Symbol.for("silvery.clipboard");
|
|
1045
|
+
/** Input router: priority-based event dispatch for interaction features. */
|
|
1046
|
+
const INPUT_ROUTER = Symbol.for("silvery.input-router");
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region ../packages/ag-term/src/runtime/perf.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* Keypress performance instrumentation.
|
|
1051
|
+
*
|
|
1052
|
+
* Zero-overhead when TRACE is not set — all logging uses optional chaining.
|
|
1053
|
+
* When TRACE=silvery:perf is set, emits span timing for each keypress cycle
|
|
1054
|
+
* and a summary on exit.
|
|
1055
|
+
*
|
|
1056
|
+
* @example
|
|
1057
|
+
* ```bash
|
|
1058
|
+
* TRACE=silvery:perf bun km view ~/vault
|
|
1059
|
+
* # → SPAN silvery:perf:keypress (5ms) {key: "j"}
|
|
1060
|
+
* # → on exit: keypress summary: 42 presses, mean=4.2ms, p95=12.1ms, max=18.3ms, overruns=2
|
|
1061
|
+
* ```
|
|
1062
|
+
*/
|
|
1063
|
+
/** Exported for ?. chaining in hot paths: `perfLog.span?.("keypress", { key })` */
|
|
1064
|
+
const perfLog = createLogger("silvery:perf");
|
|
1065
|
+
let samples = null;
|
|
1066
|
+
let budgetOverruns = 0;
|
|
1067
|
+
/**
|
|
1068
|
+
* Record a completed keypress and check budget.
|
|
1069
|
+
* Only records when tracing is active (samples array initialized by startTracking).
|
|
1070
|
+
* Call after the keypress cycle completes (render done).
|
|
1071
|
+
*/
|
|
1072
|
+
function checkBudget(key, durationMs, budgetMs = 16) {
|
|
1073
|
+
if (samples) samples.push({
|
|
1074
|
+
key,
|
|
1075
|
+
durationMs
|
|
1076
|
+
});
|
|
1077
|
+
if (durationMs > budgetMs) {
|
|
1078
|
+
budgetOverruns++;
|
|
1079
|
+
perfLog.warn?.(`keypress over budget: ${key} took ${durationMs.toFixed(1)}ms (budget: ${budgetMs}ms)`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/** Call once when first span is created to start accumulating samples. */
|
|
1083
|
+
function startTracking() {
|
|
1084
|
+
if (!samples) samples = [];
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Log a summary of all recorded keypress spans.
|
|
1088
|
+
*
|
|
1089
|
+
* Call when the app unmounts/exits. Only produces output when TRACE is
|
|
1090
|
+
* enabled and at least one span was recorded.
|
|
1091
|
+
*/
|
|
1092
|
+
function logExitSummary() {
|
|
1093
|
+
if (!samples || samples.length === 0) return;
|
|
1094
|
+
const durations = samples.map((s) => s.durationMs).sort((a, b) => a - b);
|
|
1095
|
+
const total = samples.length;
|
|
1096
|
+
const mean = durations.reduce((sum, d) => sum + d, 0) / total;
|
|
1097
|
+
const p95 = durations[Math.min(Math.floor(total * .95), total - 1)];
|
|
1098
|
+
const max = durations[total - 1];
|
|
1099
|
+
perfLog.info?.(`keypress summary: ${total} presses, mean=${mean.toFixed(1)}ms, p95=${p95.toFixed(1)}ms, max=${max.toFixed(1)}ms, overruns=${budgetOverruns}`);
|
|
1100
|
+
samples = null;
|
|
1101
|
+
budgetOverruns = 0;
|
|
1102
|
+
}
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region ../packages/ag-term/src/runtime/create-app.tsx
|
|
1105
|
+
/**
|
|
1106
|
+
* createApp() - Layer 3 entry point for silvery-loop
|
|
1107
|
+
*
|
|
1108
|
+
* Provides signal-backed store integration with unified providers.
|
|
1109
|
+
* Providers are stores (getState/subscribe) + event sources (events()).
|
|
1110
|
+
*
|
|
1111
|
+
* @example
|
|
1112
|
+
* ```tsx
|
|
1113
|
+
* import { createApp, useApp } from '@silvery/create/create-app'
|
|
1114
|
+
* import { createTermProvider } from '@silvery/ag-term/runtime'
|
|
1115
|
+
*
|
|
1116
|
+
* const app = createApp(
|
|
1117
|
+
* // Store factory
|
|
1118
|
+
* ({ term }) => (set, get) => ({
|
|
1119
|
+
* count: 0,
|
|
1120
|
+
* increment: () => set(s => ({ count: s.count + 1 })),
|
|
1121
|
+
* }),
|
|
1122
|
+
* // Event handlers - namespaced as 'provider:event'
|
|
1123
|
+
* {
|
|
1124
|
+
* 'term:key': ({ input, key }, { set }) => {
|
|
1125
|
+
* if (input === 'j') set(s => ({ count: s.count + 1 }))
|
|
1126
|
+
* if (input === 'q') return 'exit'
|
|
1127
|
+
* },
|
|
1128
|
+
* 'term:resize': ({ cols, rows }, { set }) => {
|
|
1129
|
+
* // handle resize
|
|
1130
|
+
* },
|
|
1131
|
+
* }
|
|
1132
|
+
* )
|
|
1133
|
+
*
|
|
1134
|
+
* function Counter() {
|
|
1135
|
+
* const count = useApp(s => s.count)
|
|
1136
|
+
* return <Text>Count: {count}</Text>
|
|
1137
|
+
* }
|
|
1138
|
+
*
|
|
1139
|
+
* const term = createTermProvider(process.stdin, process.stdout)
|
|
1140
|
+
* await app.run(<Counter />, { term })
|
|
1141
|
+
*
|
|
1142
|
+
* // Frame iteration:
|
|
1143
|
+
* for await (const frame of app.run(<Counter />, { term })) {
|
|
1144
|
+
* expect(frame.text).toContain('Count:')
|
|
1145
|
+
* }
|
|
1146
|
+
* ```
|
|
1147
|
+
*/
|
|
1148
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
1149
|
+
const log = createLogger("silvery:app");
|
|
1150
|
+
const ENV = typeof process$1 !== "undefined" ? process$1.env : void 0;
|
|
1151
|
+
const NO_INCREMENTAL = ENV?.SILVERY_NO_INCREMENTAL === "1";
|
|
1152
|
+
const STRICT_MODE = (() => {
|
|
1153
|
+
const v = ENV?.SILVERY_STRICT;
|
|
1154
|
+
return !!v && v !== "0" && v !== "false";
|
|
1155
|
+
})();
|
|
1156
|
+
const CELL_DEBUG = (() => {
|
|
1157
|
+
const v = ENV?.SILVERY_CELL_DEBUG;
|
|
1158
|
+
if (!v || !v.includes(",")) return null;
|
|
1159
|
+
const [cx, cy] = v.split(",").map(Number);
|
|
1160
|
+
if (!Number.isFinite(cx) || !Number.isFinite(cy)) return null;
|
|
1161
|
+
return {
|
|
1162
|
+
x: cx,
|
|
1163
|
+
y: cy
|
|
1164
|
+
};
|
|
1165
|
+
})();
|
|
1166
|
+
const INSTRUMENTED = STRICT_MODE || CELL_DEBUG !== null;
|
|
1167
|
+
/**
|
|
1168
|
+
* Check if value is a Provider with events (full interface).
|
|
1169
|
+
*/
|
|
1170
|
+
function isFullProvider(value) {
|
|
1171
|
+
if (value === null || value === void 0) return false;
|
|
1172
|
+
if (typeof value !== "object" && typeof value !== "function") return false;
|
|
1173
|
+
return "getState" in value && "subscribe" in value && "events" in value && typeof value.getState === "function" && typeof value.subscribe === "function" && typeof value.events === "function";
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Check if value is a basic Provider (just getState/subscribe, Zustand-compatible).
|
|
1177
|
+
*/
|
|
1178
|
+
function isBasicProvider(value) {
|
|
1179
|
+
if (value === null || value === void 0) return false;
|
|
1180
|
+
if (typeof value !== "object" && typeof value !== "function") return false;
|
|
1181
|
+
return "getState" in value && "subscribe" in value && typeof value.getState === "function" && typeof value.subscribe === "function";
|
|
1182
|
+
}
|
|
1183
|
+
const StoreContext = createContext(null);
|
|
1184
|
+
/**
|
|
1185
|
+
* Hook for accessing app state with selectors.
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* ```tsx
|
|
1189
|
+
* const count = useApp(s => s.count)
|
|
1190
|
+
* const { count, increment } = useApp(s => ({ count: s.count, increment: s.increment }))
|
|
1191
|
+
* ```
|
|
1192
|
+
*/
|
|
1193
|
+
function useApp(selector) {
|
|
1194
|
+
const store = useContext(StoreContext);
|
|
1195
|
+
if (!store) throw new Error("useApp must be used within createApp().run()");
|
|
1196
|
+
const [state, setState] = React.useState(() => selector(store.getState()));
|
|
1197
|
+
const selectorRef = useRef(selector);
|
|
1198
|
+
selectorRef.current = selector;
|
|
1199
|
+
useEffect(() => {
|
|
1200
|
+
return store.subscribe((newState) => {
|
|
1201
|
+
const next = selectorRef.current(newState);
|
|
1202
|
+
setState((prev) => Object.is(prev, next) ? prev : next);
|
|
1203
|
+
});
|
|
1204
|
+
}, [store]);
|
|
1205
|
+
return state;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Create an app with Zustand store and provider integration.
|
|
1209
|
+
*
|
|
1210
|
+
* This is Layer 3 - it provides:
|
|
1211
|
+
* - Zustand store with fine-grained subscriptions
|
|
1212
|
+
* - Providers as unified stores + event sources
|
|
1213
|
+
* - Event handlers namespaced as 'provider:event'
|
|
1214
|
+
*
|
|
1215
|
+
* @param factory Store factory function that receives providers
|
|
1216
|
+
* @param handlers Optional event handlers (namespaced as 'provider:event')
|
|
1217
|
+
*/
|
|
1218
|
+
function createApp(factory, handlers) {
|
|
1219
|
+
return { run(element, options = {}) {
|
|
1220
|
+
let handlePromise = null;
|
|
1221
|
+
const init = () => {
|
|
1222
|
+
if (handlePromise) return handlePromise;
|
|
1223
|
+
handlePromise = initApp(factory, handlers, element, options);
|
|
1224
|
+
return handlePromise;
|
|
1225
|
+
};
|
|
1226
|
+
return {
|
|
1227
|
+
then(onfulfilled, onrejected) {
|
|
1228
|
+
return init().then(onfulfilled, onrejected);
|
|
1229
|
+
},
|
|
1230
|
+
[Symbol.asyncIterator]() {
|
|
1231
|
+
let handle = null;
|
|
1232
|
+
let iterator = null;
|
|
1233
|
+
let started = false;
|
|
1234
|
+
return {
|
|
1235
|
+
async next() {
|
|
1236
|
+
if (!started) {
|
|
1237
|
+
started = true;
|
|
1238
|
+
handle = await init();
|
|
1239
|
+
iterator = handle[Symbol.asyncIterator]();
|
|
1240
|
+
}
|
|
1241
|
+
return iterator.next();
|
|
1242
|
+
},
|
|
1243
|
+
async return() {
|
|
1244
|
+
if (handle) handle.unmount();
|
|
1245
|
+
return {
|
|
1246
|
+
done: true,
|
|
1247
|
+
value: void 0
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
} };
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Initialize the app — extracted from run() for clarity.
|
|
1257
|
+
*/
|
|
1258
|
+
async function initApp(factory, handlers, element, options) {
|
|
1259
|
+
const { cols: explicitCols, rows: explicitRows, stdout: explicitStdout, stdin = process$1.stdin, signal: externalSignal, alternateScreen = false, kittyMode: explicitKittyMode, kitty: kittyOption, mouse: mouseOption = false, virtualInline: virtualInlineOption = false, suspendOnCtrlZ: suspendOption = true, exitOnCtrlC: exitOnCtrlCOption = true, onSuspend: onSuspendHook, onResume: onResumeHook, onInterrupt: onInterruptHook, textSizing: textSizingOption, widthDetection: widthDetectionOption, focusReporting: focusReportingOption = false, selection: selectionOption, caps: capsOption, guardOutput: guardOutputOption, Root: RootComponent, capabilityRegistry: capabilityRegistryOption, writable: explicitWritable, onResize: explicitOnResize, ...injectValues } = options;
|
|
1260
|
+
const useKittyMode = explicitKittyMode ?? !!kittyOption;
|
|
1261
|
+
const headless = explicitCols != null && explicitRows != null && !explicitStdout || explicitWritable != null;
|
|
1262
|
+
const cols = explicitCols ?? process$1.stdout.columns ?? 80;
|
|
1263
|
+
const rows = explicitRows ?? process$1.stdout.rows ?? 24;
|
|
1264
|
+
const stdout = explicitStdout ?? process$1.stdout;
|
|
1265
|
+
const isRealStdout = stdout === process$1.stdout;
|
|
1266
|
+
const shouldGuardOutput = guardOutputOption ?? (alternateScreen && !headless && isRealStdout);
|
|
1267
|
+
let outputGuard = null;
|
|
1268
|
+
await ensureLayoutEngine();
|
|
1269
|
+
const controller = new AbortController();
|
|
1270
|
+
const signal = controller.signal;
|
|
1271
|
+
if (externalSignal) if (externalSignal.aborted) controller.abort();
|
|
1272
|
+
else externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
1273
|
+
const providers = {};
|
|
1274
|
+
const plainValues = {};
|
|
1275
|
+
const providerCleanups = [];
|
|
1276
|
+
let termProvider = null;
|
|
1277
|
+
if (!("term" in injectValues) || !isFullProvider(injectValues.term)) {
|
|
1278
|
+
const resizeListeners = /* @__PURE__ */ new Set();
|
|
1279
|
+
const termStdout = headless ? {
|
|
1280
|
+
columns: cols,
|
|
1281
|
+
rows,
|
|
1282
|
+
write: () => true,
|
|
1283
|
+
isTTY: false,
|
|
1284
|
+
on(event, handler) {
|
|
1285
|
+
if (event === "resize") resizeListeners.add(handler);
|
|
1286
|
+
return termStdout;
|
|
1287
|
+
},
|
|
1288
|
+
off(event, handler) {
|
|
1289
|
+
if (event === "resize") resizeListeners.delete(handler);
|
|
1290
|
+
return termStdout;
|
|
1291
|
+
}
|
|
1292
|
+
} : stdout;
|
|
1293
|
+
const termStdin = headless ? {
|
|
1294
|
+
isTTY: false,
|
|
1295
|
+
on: () => termStdin,
|
|
1296
|
+
off: () => termStdin,
|
|
1297
|
+
setRawMode: () => {},
|
|
1298
|
+
resume: () => {},
|
|
1299
|
+
pause: () => {},
|
|
1300
|
+
setEncoding: () => {}
|
|
1301
|
+
} : stdin;
|
|
1302
|
+
termProvider = createTermProvider(termStdin, termStdout, {
|
|
1303
|
+
cols,
|
|
1304
|
+
rows
|
|
1305
|
+
});
|
|
1306
|
+
providers.term = termProvider;
|
|
1307
|
+
providerCleanups.push(() => termProvider[Symbol.dispose]());
|
|
1308
|
+
if (headless && explicitOnResize) {
|
|
1309
|
+
const unsub = explicitOnResize((dims) => {
|
|
1310
|
+
currentDims = dims;
|
|
1311
|
+
termStdout.columns = dims.cols;
|
|
1312
|
+
termStdout.rows = dims.rows;
|
|
1313
|
+
for (const listener of resizeListeners) listener();
|
|
1314
|
+
});
|
|
1315
|
+
providerCleanups.push(unsub);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
for (const [name, value] of Object.entries(injectValues)) if (isFullProvider(value)) providers[name] = value;
|
|
1319
|
+
else plainValues[name] = value;
|
|
1320
|
+
const inject = {
|
|
1321
|
+
...providers,
|
|
1322
|
+
...plainValues
|
|
1323
|
+
};
|
|
1324
|
+
const stateUnsubscribes = [];
|
|
1325
|
+
const store = createStore((set, get, api) => {
|
|
1326
|
+
const mergedState = { ...factory(inject)(set, get, api) };
|
|
1327
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
1328
|
+
mergedState[name] = provider;
|
|
1329
|
+
if (isBasicProvider(provider)) {
|
|
1330
|
+
const unsub = provider.subscribe((_providerState) => {});
|
|
1331
|
+
stateUnsubscribes.push(unsub);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
for (const [name, value] of Object.entries(plainValues)) mergedState[name] = value;
|
|
1335
|
+
return mergedState;
|
|
1336
|
+
});
|
|
1337
|
+
let currentDims = {
|
|
1338
|
+
cols,
|
|
1339
|
+
rows
|
|
1340
|
+
};
|
|
1341
|
+
if (!headless) {
|
|
1342
|
+
const onStdoutResize = () => {
|
|
1343
|
+
currentDims = {
|
|
1344
|
+
cols: stdout.columns || 80,
|
|
1345
|
+
rows: stdout.rows || 24
|
|
1346
|
+
};
|
|
1347
|
+
for (const listener of mockTermSubscribers) listener(currentDims);
|
|
1348
|
+
};
|
|
1349
|
+
stdout.on("resize", onStdoutResize);
|
|
1350
|
+
providerCleanups.push(() => stdout.off("resize", onStdoutResize));
|
|
1351
|
+
}
|
|
1352
|
+
let shouldExit = false;
|
|
1353
|
+
let renderPaused = false;
|
|
1354
|
+
let isRendering = false;
|
|
1355
|
+
let inEventHandler = false;
|
|
1356
|
+
let pendingRerender = false;
|
|
1357
|
+
const _ansiTrace = !headless && process$1.env?.SILVERY_TRACE === "1";
|
|
1358
|
+
let _traceSeq = 0;
|
|
1359
|
+
const _traceStart = performance.now();
|
|
1360
|
+
let _origStdoutWrite;
|
|
1361
|
+
if (_ansiTrace) {
|
|
1362
|
+
const fs = __require("node:fs");
|
|
1363
|
+
fs.writeFileSync("/tmp/silvery-trace.log", `=== SILVERY TRACE START ===\n`);
|
|
1364
|
+
_origStdoutWrite = stdout.write.bind(stdout);
|
|
1365
|
+
const symbolize = (s) => s.replace(/\x1b\[\?1049h/g, "⟨ALT_ON⟩").replace(/\x1b\[\?1049l/g, "⟨ALT_OFF⟩").replace(/\x1b\[2J/g, "⟨CLEAR⟩").replace(/\x1b\[H/g, "⟨HOME⟩").replace(/\x1b\[\?25l/g, "⟨CUR_HIDE⟩").replace(/\x1b\[\?25h/g, "⟨CUR_SHOW⟩").replace(/\x1b\[\?2026h/g, "⟨SYNC_ON⟩").replace(/\x1b\[\?2026l/g, "⟨SYNC_OFF⟩").replace(/\x1b\[\?2004h/g, "⟨BPASTE_ON⟩").replace(/\x1b\[\?2004l/g, "⟨BPASTE_OFF⟩").replace(/\x1b\[0m/g, "⟨RST⟩").replace(/\x1b\[(\d+);(\d+)H/g, "⟨GO $1,$2⟩").replace(/\x1b\[38;5;(\d+)m/g, "⟨F$1⟩").replace(/\x1b\[48;5;(\d+)m/g, "⟨B$1⟩").replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, "⟨FR$1,$2,$3⟩").replace(/\x1b\[48;2;(\d+);(\d+);(\d+)m/g, "⟨BR$1,$2,$3⟩").replace(/\x1b\[1m/g, "⟨BOLD⟩").replace(/\x1b\[2m/g, "⟨DIM⟩").replace(/\x1b\[3m/g, "⟨ITAL⟩").replace(/\x1b\[4m/g, "⟨UL⟩").replace(/\x1b\[7m/g, "⟨INV⟩").replace(/\x1b\[22m/g, "⟨/BOLD⟩").replace(/\x1b\[23m/g, "⟨/ITAL⟩").replace(/\x1b\[24m/g, "⟨/UL⟩").replace(/\x1b\[27m/g, "⟨/INV⟩").replace(/\x1b\[39m/g, "⟨/FG⟩").replace(/\x1b\[49m/g, "⟨/BG⟩").replace(/\x1b\[([0-9;]*)([A-Za-z])/g, "⟨CSI $1$2⟩").replace(/\x1b([^\[])/, "⟨ESC $1⟩");
|
|
1366
|
+
const traceWrite = function(chunk, ...args) {
|
|
1367
|
+
const str = typeof chunk === "string" ? chunk : String(chunk);
|
|
1368
|
+
const seq = ++_traceSeq;
|
|
1369
|
+
const ms = (performance.now() - _traceStart).toFixed(0);
|
|
1370
|
+
const decoded = symbolize(str);
|
|
1371
|
+
const preview = decoded.length > 400 ? decoded.slice(0, 200) + ` ...[${decoded.length}ch]... ` + decoded.slice(-100) : decoded;
|
|
1372
|
+
fs.appendFileSync("/tmp/silvery-trace.log", `[${String(seq).padStart(4, "0")}] +${ms}ms (${str.length}b): ${preview}\n`);
|
|
1373
|
+
return _origStdoutWrite.call(this, chunk, ...args);
|
|
1374
|
+
};
|
|
1375
|
+
stdout.write = traceWrite;
|
|
1376
|
+
providerCleanups.push(() => {
|
|
1377
|
+
if (_origStdoutWrite) stdout.write = _origStdoutWrite;
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
const target = headless ? {
|
|
1381
|
+
write(frame) {
|
|
1382
|
+
if (explicitWritable) explicitWritable.write(frame);
|
|
1383
|
+
},
|
|
1384
|
+
getDims: () => currentDims
|
|
1385
|
+
} : {
|
|
1386
|
+
write(frame) {
|
|
1387
|
+
if (_perfLog) __require("node:fs").appendFileSync("/tmp/silvery-perf.log", `TARGET.write: ${frame.length} bytes (paused=${renderPaused})\n`);
|
|
1388
|
+
if (!renderPaused) if (outputGuard) outputGuard.writeStdout(frame);
|
|
1389
|
+
else stdout.write(frame);
|
|
1390
|
+
},
|
|
1391
|
+
getDims() {
|
|
1392
|
+
return currentDims;
|
|
1393
|
+
},
|
|
1394
|
+
onResize(handler) {
|
|
1395
|
+
const onResize = () => {
|
|
1396
|
+
currentDims = {
|
|
1397
|
+
cols: stdout.columns || 80,
|
|
1398
|
+
rows: stdout.rows || 24
|
|
1399
|
+
};
|
|
1400
|
+
handler(currentDims);
|
|
1401
|
+
};
|
|
1402
|
+
stdout.on("resize", onResize);
|
|
1403
|
+
return () => stdout.off("resize", onResize);
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const heuristicSupported = capsOption?.textSizingSupported ?? isTextSizingLikelySupported();
|
|
1407
|
+
const shouldProbe = textSizingOption === "probe" || textSizingOption === "auto" && heuristicSupported;
|
|
1408
|
+
const cachedProbe = shouldProbe ? getCachedProbeResult() : void 0;
|
|
1409
|
+
let textSizingEnabled;
|
|
1410
|
+
if (textSizingOption === true) textSizingEnabled = true;
|
|
1411
|
+
else if (textSizingOption === "probe") textSizingEnabled = cachedProbe?.supported ?? false;
|
|
1412
|
+
else if (textSizingOption === "auto") if (cachedProbe !== void 0) textSizingEnabled = cachedProbe.supported;
|
|
1413
|
+
else textSizingEnabled = heuristicSupported;
|
|
1414
|
+
else textSizingEnabled = false;
|
|
1415
|
+
const needsProbe = shouldProbe && cachedProbe === void 0 && !headless;
|
|
1416
|
+
const needsWidthDetection = !headless && (widthDetectionOption === true || widthDetectionOption === "auto" && capsOption != null);
|
|
1417
|
+
let effectiveCaps = capsOption ? {
|
|
1418
|
+
...capsOption,
|
|
1419
|
+
textSizingSupported: textSizingEnabled
|
|
1420
|
+
} : void 0;
|
|
1421
|
+
let pipelineConfig = effectiveCaps ? createPipeline({ caps: effectiveCaps }) : void 0;
|
|
1422
|
+
const runtime = createRuntime({
|
|
1423
|
+
target,
|
|
1424
|
+
signal,
|
|
1425
|
+
mode: alternateScreen ? "fullscreen" : "inline",
|
|
1426
|
+
outputPhaseFn: pipelineConfig?.outputPhaseFn
|
|
1427
|
+
});
|
|
1428
|
+
let cleanedUp = false;
|
|
1429
|
+
let storeUnsubscribeFn = null;
|
|
1430
|
+
let kittyEnabled = false;
|
|
1431
|
+
const defaultKittyFlags = KittyFlags.DISAMBIGUATE | KittyFlags.REPORT_EVENTS | KittyFlags.REPORT_ALL_KEYS;
|
|
1432
|
+
let kittyFlags = defaultKittyFlags;
|
|
1433
|
+
let mouseEnabled = false;
|
|
1434
|
+
let focusReportingEnabled = false;
|
|
1435
|
+
const selectionEnabled = selectionOption ?? false;
|
|
1436
|
+
let selectionState = createTerminalSelectionState();
|
|
1437
|
+
const selectionListeners = /* @__PURE__ */ new Set();
|
|
1438
|
+
/** Notify useSelection() subscribers that selection state changed. */
|
|
1439
|
+
function notifySelectionListeners() {
|
|
1440
|
+
for (const listener of selectionListeners) listener();
|
|
1441
|
+
}
|
|
1442
|
+
const capabilityRegistry = capabilityRegistryOption ?? createCapabilityRegistry();
|
|
1443
|
+
let selectionBridge;
|
|
1444
|
+
if (selectionEnabled) {
|
|
1445
|
+
selectionBridge = createSelectionBridge({
|
|
1446
|
+
getState: () => selectionState,
|
|
1447
|
+
subscribe: (listener) => {
|
|
1448
|
+
selectionListeners.add(listener);
|
|
1449
|
+
return () => {
|
|
1450
|
+
selectionListeners.delete(listener);
|
|
1451
|
+
};
|
|
1452
|
+
},
|
|
1453
|
+
setRange: (range) => {
|
|
1454
|
+
if (range === null) {
|
|
1455
|
+
const [next] = terminalSelectionUpdate({ type: "clear" }, selectionState);
|
|
1456
|
+
selectionState = next;
|
|
1457
|
+
} else {
|
|
1458
|
+
const [s1] = terminalSelectionUpdate({
|
|
1459
|
+
type: "start",
|
|
1460
|
+
col: range.anchor.col,
|
|
1461
|
+
row: range.anchor.row,
|
|
1462
|
+
source: "keyboard"
|
|
1463
|
+
}, selectionState);
|
|
1464
|
+
const [s2] = terminalSelectionUpdate({
|
|
1465
|
+
type: "extend",
|
|
1466
|
+
col: range.head.col,
|
|
1467
|
+
row: range.head.row
|
|
1468
|
+
}, s1);
|
|
1469
|
+
const [s3] = terminalSelectionUpdate({ type: "finish" }, s2);
|
|
1470
|
+
selectionState = s3;
|
|
1471
|
+
}
|
|
1472
|
+
notifySelectionListeners();
|
|
1473
|
+
if (currentBuffer) runtime.invalidate();
|
|
1474
|
+
},
|
|
1475
|
+
clear: () => {
|
|
1476
|
+
const [next] = terminalSelectionUpdate({ type: "clear" }, selectionState);
|
|
1477
|
+
selectionState = next;
|
|
1478
|
+
notifySelectionListeners();
|
|
1479
|
+
if (currentBuffer) runtime.invalidate();
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
capabilityRegistry.register(SELECTION_CAPABILITY, selectionBridge);
|
|
1483
|
+
}
|
|
1484
|
+
const scrollback = virtualInlineOption ? createVirtualScrollback() : null;
|
|
1485
|
+
let virtualScrollOffset = 0;
|
|
1486
|
+
let searchState = createSearchState();
|
|
1487
|
+
const focusManager = createFocusManager({ onFocusChange(oldNode, newNode, _origin) {
|
|
1488
|
+
if (oldNode) dispatchFocusEvent(createFocusEvent("blur", oldNode, newNode));
|
|
1489
|
+
if (newNode) dispatchFocusEvent(createFocusEvent("focus", newNode, oldNode));
|
|
1490
|
+
} });
|
|
1491
|
+
setOnNodeRemoved((removedNode) => focusManager.handleSubtreeRemoved(removedNode));
|
|
1492
|
+
const cursorStore = createCursorStore();
|
|
1493
|
+
const mouseEventState = createMouseEventProcessor({ focusManager });
|
|
1494
|
+
const cleanup = () => {
|
|
1495
|
+
if (cleanedUp) return;
|
|
1496
|
+
cleanedUp = true;
|
|
1497
|
+
logExitSummary();
|
|
1498
|
+
try {
|
|
1499
|
+
reconciler.updateContainerSync(null, fiberRoot, null, () => {});
|
|
1500
|
+
reconciler.flushSyncWork();
|
|
1501
|
+
} catch {}
|
|
1502
|
+
setOnNodeRemoved(null);
|
|
1503
|
+
if (storeUnsubscribeFn) storeUnsubscribeFn();
|
|
1504
|
+
stateUnsubscribes.forEach((unsub) => {
|
|
1505
|
+
try {
|
|
1506
|
+
unsub();
|
|
1507
|
+
} catch {}
|
|
1508
|
+
});
|
|
1509
|
+
if (outputGuard) {
|
|
1510
|
+
outputGuard.dispose();
|
|
1511
|
+
outputGuard = null;
|
|
1512
|
+
}
|
|
1513
|
+
if (!headless && stdin.isTTY) {
|
|
1514
|
+
stdin.removeAllListeners("data");
|
|
1515
|
+
stdin.pause();
|
|
1516
|
+
const sequences = [
|
|
1517
|
+
"\x1B[?1004l",
|
|
1518
|
+
disableMouse(),
|
|
1519
|
+
disableKittyKeyboard(),
|
|
1520
|
+
"\x1B[?2004l",
|
|
1521
|
+
"\x1B[0m",
|
|
1522
|
+
resetCursorStyle(),
|
|
1523
|
+
"\x1B[?25h",
|
|
1524
|
+
alternateScreen ? "\x1B[?1049l" : ""
|
|
1525
|
+
].join("");
|
|
1526
|
+
if (stdout === process$1.stdout) {
|
|
1527
|
+
try {
|
|
1528
|
+
writeSync(stdout.fd, sequences);
|
|
1529
|
+
} catch {
|
|
1530
|
+
try {
|
|
1531
|
+
stdout.write(sequences);
|
|
1532
|
+
} catch {}
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
stdin.resume();
|
|
1536
|
+
while (stdin.read() !== null);
|
|
1537
|
+
stdin.pause();
|
|
1538
|
+
} catch {}
|
|
1539
|
+
} else try {
|
|
1540
|
+
stdout.write(sequences);
|
|
1541
|
+
} catch {}
|
|
1542
|
+
try {
|
|
1543
|
+
stdin.setRawMode(false);
|
|
1544
|
+
} catch {}
|
|
1545
|
+
} else if (!headless) {
|
|
1546
|
+
const sequences = [
|
|
1547
|
+
"\x1B[?1004l",
|
|
1548
|
+
disableMouse(),
|
|
1549
|
+
disableKittyKeyboard(),
|
|
1550
|
+
"\x1B[?2004l",
|
|
1551
|
+
"\x1B[0m",
|
|
1552
|
+
resetCursorStyle(),
|
|
1553
|
+
"\x1B[?25h",
|
|
1554
|
+
alternateScreen ? "\x1B[?1049l" : ""
|
|
1555
|
+
].join("");
|
|
1556
|
+
try {
|
|
1557
|
+
stdout.write(sequences);
|
|
1558
|
+
} catch {}
|
|
1559
|
+
}
|
|
1560
|
+
providerCleanups.forEach((fn) => {
|
|
1561
|
+
try {
|
|
1562
|
+
fn();
|
|
1563
|
+
} catch {}
|
|
1564
|
+
});
|
|
1565
|
+
runtime[Symbol.dispose]();
|
|
1566
|
+
};
|
|
1567
|
+
let exit;
|
|
1568
|
+
const container = createContainer(() => {
|
|
1569
|
+
if (shouldExit) return;
|
|
1570
|
+
if (inEventHandler) {
|
|
1571
|
+
pendingRerender = true;
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (!pendingRerender) {
|
|
1575
|
+
pendingRerender = true;
|
|
1576
|
+
queueMicrotask(() => {
|
|
1577
|
+
if (!pendingRerender) return;
|
|
1578
|
+
pendingRerender = false;
|
|
1579
|
+
if (!shouldExit && !isRendering) {
|
|
1580
|
+
isRendering = true;
|
|
1581
|
+
try {
|
|
1582
|
+
currentBuffer = doRender();
|
|
1583
|
+
runtime.render(currentBuffer);
|
|
1584
|
+
} finally {
|
|
1585
|
+
isRendering = false;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
const fiberRoot = createFiberRoot(container);
|
|
1592
|
+
let currentBuffer;
|
|
1593
|
+
const mockStdout = {
|
|
1594
|
+
columns: cols,
|
|
1595
|
+
rows,
|
|
1596
|
+
write: () => true,
|
|
1597
|
+
isTTY: false,
|
|
1598
|
+
on: () => mockStdout,
|
|
1599
|
+
off: () => mockStdout,
|
|
1600
|
+
once: () => mockStdout,
|
|
1601
|
+
removeListener: () => mockStdout,
|
|
1602
|
+
addListener: () => mockStdout
|
|
1603
|
+
};
|
|
1604
|
+
const baseMockTerm = createTerm({ color: "truecolor" });
|
|
1605
|
+
const mockTermSubscribers = /* @__PURE__ */ new Set();
|
|
1606
|
+
const mockTerm = Object.create(baseMockTerm, {
|
|
1607
|
+
getState: { value: () => currentDims },
|
|
1608
|
+
subscribe: { value: (listener) => {
|
|
1609
|
+
mockTermSubscribers.add(listener);
|
|
1610
|
+
return () => mockTermSubscribers.delete(listener);
|
|
1611
|
+
} }
|
|
1612
|
+
});
|
|
1613
|
+
const runtimeInputListeners = [];
|
|
1614
|
+
const runtimePasteListeners = [];
|
|
1615
|
+
const runtimeFocusListeners = [];
|
|
1616
|
+
const runtimeEventListeners = /* @__PURE__ */ new Map();
|
|
1617
|
+
runtimeEventListeners.set("input", runtimeInputListeners);
|
|
1618
|
+
runtimeEventListeners.set("paste", runtimePasteListeners);
|
|
1619
|
+
runtimeEventListeners.set("focus", runtimeFocusListeners);
|
|
1620
|
+
const runtimeContextValue = {
|
|
1621
|
+
on(event, handler) {
|
|
1622
|
+
let listeners = runtimeEventListeners.get(event);
|
|
1623
|
+
if (!listeners) {
|
|
1624
|
+
listeners = [];
|
|
1625
|
+
runtimeEventListeners.set(event, listeners);
|
|
1626
|
+
}
|
|
1627
|
+
listeners.push(handler);
|
|
1628
|
+
return () => {
|
|
1629
|
+
const idx = listeners.indexOf(handler);
|
|
1630
|
+
if (idx >= 0) listeners.splice(idx, 1);
|
|
1631
|
+
};
|
|
1632
|
+
},
|
|
1633
|
+
emit(event, ...args) {
|
|
1634
|
+
const listeners = runtimeEventListeners.get(event);
|
|
1635
|
+
if (listeners) for (const listener of listeners) listener(...args);
|
|
1636
|
+
},
|
|
1637
|
+
exit: () => exit()
|
|
1638
|
+
};
|
|
1639
|
+
const Root = RootComponent ?? React.Fragment;
|
|
1640
|
+
const cacheBackend = !alternateScreen ? "terminal" : virtualInlineOption ? "virtual" : "retain";
|
|
1641
|
+
const wrappedElement = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SilveryErrorBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CursorProvider, {
|
|
1642
|
+
store: cursorStore,
|
|
1643
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CacheBackendContext.Provider, {
|
|
1644
|
+
value: cacheBackend,
|
|
1645
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TermContext.Provider, {
|
|
1646
|
+
value: mockTerm,
|
|
1647
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StdoutContext.Provider, {
|
|
1648
|
+
value: {
|
|
1649
|
+
stdout: mockStdout,
|
|
1650
|
+
write: () => {},
|
|
1651
|
+
notifyScrollback: (lines) => runtime.addScrollbackLines(lines),
|
|
1652
|
+
promoteScrollback: (content, lines) => runtime.promoteScrollback(content, lines),
|
|
1653
|
+
resetInlineCursor: () => runtime.resetInlineCursor(),
|
|
1654
|
+
getInlineCursorRow: () => runtime.getInlineCursorRow()
|
|
1655
|
+
},
|
|
1656
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StderrContext.Provider, {
|
|
1657
|
+
value: {
|
|
1658
|
+
stderr: process$1.stderr,
|
|
1659
|
+
write: (data) => {
|
|
1660
|
+
process$1.stderr.write(data);
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FocusManagerContext.Provider, {
|
|
1664
|
+
value: focusManager,
|
|
1665
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RuntimeContext.Provider, {
|
|
1666
|
+
value: runtimeContextValue,
|
|
1667
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CapabilityRegistryContext.Provider, {
|
|
1668
|
+
value: capabilityRegistry,
|
|
1669
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Root, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StoreContext.Provider, {
|
|
1670
|
+
value: store,
|
|
1671
|
+
children: element
|
|
1672
|
+
}) })
|
|
1673
|
+
})
|
|
1674
|
+
})
|
|
1675
|
+
})
|
|
1676
|
+
})
|
|
1677
|
+
})
|
|
1678
|
+
})
|
|
1679
|
+
})
|
|
1680
|
+
}) });
|
|
1681
|
+
let _renderCount = 0;
|
|
1682
|
+
let _eventStart = 0;
|
|
1683
|
+
const _perfLog = typeof process$1 !== "undefined" && process$1.env?.DEBUG?.includes("silvery:perf");
|
|
1684
|
+
const _noIncremental = NO_INCREMENTAL;
|
|
1685
|
+
let _ag = null;
|
|
1686
|
+
let _lastTermBuffer = null;
|
|
1687
|
+
function doRender() {
|
|
1688
|
+
_renderCount++;
|
|
1689
|
+
if (_ansiTrace) __require("node:fs").appendFileSync("/tmp/silvery-trace.log", `--- doRender #${_renderCount} (ag=${_ag ? "reuse" : "create"}, incremental=${!_noIncremental}) ---\n`);
|
|
1690
|
+
const renderStart = performance.now();
|
|
1691
|
+
reconciler.updateContainerSync(wrappedElement, fiberRoot, null, () => {});
|
|
1692
|
+
reconciler.flushSyncWork();
|
|
1693
|
+
const reconcileMs = performance.now() - renderStart;
|
|
1694
|
+
{
|
|
1695
|
+
const acc = globalThis.__silvery_bench_phases;
|
|
1696
|
+
if (acc) acc.reconcile += reconcileMs;
|
|
1697
|
+
}
|
|
1698
|
+
const pipelineStart = performance.now();
|
|
1699
|
+
const rootNode = getContainerRoot(container);
|
|
1700
|
+
const dims = runtime.getDims();
|
|
1701
|
+
const isInline = !alternateScreen;
|
|
1702
|
+
if (!_ag) _ag = createAg(rootNode, { measurer: pipelineConfig?.measurer });
|
|
1703
|
+
if (_ag) {
|
|
1704
|
+
const lastBuffer = _lastTermBuffer;
|
|
1705
|
+
if (lastBuffer) {
|
|
1706
|
+
const widthChanged = dims.cols !== lastBuffer.width;
|
|
1707
|
+
const heightChanged = !isInline && dims.rows !== lastBuffer.height;
|
|
1708
|
+
if (widthChanged || heightChanged) {
|
|
1709
|
+
_ag.resetBuffer();
|
|
1710
|
+
runtime.invalidate();
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (INSTRUMENTED) {
|
|
1715
|
+
globalThis.__silvery_content_all = void 0;
|
|
1716
|
+
globalThis.__silvery_node_trace = void 0;
|
|
1717
|
+
globalThis.__silvery_cell_debug = CELL_DEBUG !== null ? {
|
|
1718
|
+
x: CELL_DEBUG.x,
|
|
1719
|
+
y: CELL_DEBUG.y,
|
|
1720
|
+
log: []
|
|
1721
|
+
} : void 0;
|
|
1722
|
+
}
|
|
1723
|
+
const rootHasDirty = rootNode.layoutDirty || isAnyDirty(rootNode.dirtyBits, rootNode.dirtyEpoch);
|
|
1724
|
+
const dimsChanged = _lastTermBuffer != null && (dims.cols !== _lastTermBuffer.width || dims.rows !== _lastTermBuffer.height);
|
|
1725
|
+
if (!rootHasDirty && !dimsChanged && _lastTermBuffer && currentBuffer) return currentBuffer;
|
|
1726
|
+
if (_noIncremental) _ag.resetBuffer();
|
|
1727
|
+
_ag.layout(dims);
|
|
1728
|
+
const { buffer: termBuffer, prevBuffer: agPrevBuffer } = _ag.render();
|
|
1729
|
+
_lastTermBuffer = termBuffer;
|
|
1730
|
+
const wasIncremental = !_noIncremental && agPrevBuffer !== null;
|
|
1731
|
+
const pipelineMs = performance.now() - pipelineStart;
|
|
1732
|
+
globalThis.__silvery_last_pipeline = {
|
|
1733
|
+
layout: pipelineMs,
|
|
1734
|
+
output: 0,
|
|
1735
|
+
total: pipelineMs,
|
|
1736
|
+
incremental: wasIncremental
|
|
1737
|
+
};
|
|
1738
|
+
globalThis.__silvery_render_count = (globalThis.__silvery_render_count ?? 0) + 1;
|
|
1739
|
+
{
|
|
1740
|
+
const acc = globalThis.__silvery_bench_phases;
|
|
1741
|
+
if (acc) {
|
|
1742
|
+
acc.total += pipelineMs;
|
|
1743
|
+
acc.pipelineCalls += 1;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
if (STRICT_MODE && wasIncremental) {
|
|
1747
|
+
const { buffer: freshBuffer } = executeRender(rootNode, dims.cols, dims.rows, null, {
|
|
1748
|
+
skipLayoutNotifications: true,
|
|
1749
|
+
skipScrollStateUpdates: true
|
|
1750
|
+
}, pipelineConfig);
|
|
1751
|
+
const { cellEquals, bufferToText } = (init_buffer(), __toCommonJS(buffer_exports));
|
|
1752
|
+
for (let y = 0; y < termBuffer.height; y++) for (let x = 0; x < termBuffer.width; x++) {
|
|
1753
|
+
const a = termBuffer.getCell(x, y);
|
|
1754
|
+
const b = freshBuffer.getCell(x, y);
|
|
1755
|
+
if (!cellEquals(a, b)) {
|
|
1756
|
+
let cellDebugInfo = "";
|
|
1757
|
+
const savedCellDbg = globalThis.__silvery_cell_debug;
|
|
1758
|
+
if (savedCellDbg && savedCellDbg.x === x && savedCellDbg.y === y && savedCellDbg.log.length > 0) cellDebugInfo = `\nCELL DEBUG (${savedCellDbg.log.length} entries for (${x},${y})):\n${savedCellDbg.log.join("\n")}\n`;
|
|
1759
|
+
else if (savedCellDbg && savedCellDbg.x === x && savedCellDbg.y === y) cellDebugInfo = `\nCELL DEBUG: No nodes cover (${x},${y}) during incremental render\n`;
|
|
1760
|
+
else cellDebugInfo = `\nCELL DEBUG: Target cell (${x},${y}) differs from debug cell (${savedCellDbg?.x},${savedCellDbg?.y})\n`;
|
|
1761
|
+
let trapInfo = "";
|
|
1762
|
+
const trap = {
|
|
1763
|
+
x,
|
|
1764
|
+
y,
|
|
1765
|
+
log: []
|
|
1766
|
+
};
|
|
1767
|
+
globalThis.__silvery_write_trap = trap;
|
|
1768
|
+
try {
|
|
1769
|
+
executeRender(rootNode, dims.cols, dims.rows, null, {
|
|
1770
|
+
skipLayoutNotifications: true,
|
|
1771
|
+
skipScrollStateUpdates: true
|
|
1772
|
+
}, pipelineConfig);
|
|
1773
|
+
} catch {}
|
|
1774
|
+
globalThis.__silvery_write_trap = null;
|
|
1775
|
+
if (trap.log.length > 0) trapInfo = `\nWRITE TRAP (${trap.log.length} writes to (${x},${y})):\n${trap.log.join("\n")}\n`;
|
|
1776
|
+
else trapInfo = `\nWRITE TRAP: NO WRITES to (${x},${y})\n`;
|
|
1777
|
+
const incText = bufferToText(termBuffer);
|
|
1778
|
+
const freshText = bufferToText(freshBuffer);
|
|
1779
|
+
const cellStr = (c) => `char=${JSON.stringify(c.char)} fg=${c.fg} bg=${c.bg} ulColor=${c.underlineColor} wide=${c.wide} cont=${c.continuation} attrs={bold=${c.attrs.bold},dim=${c.attrs.dim},italic=${c.attrs.italic},ul=${c.attrs.underline},ulStyle=${c.attrs.underlineStyle},blink=${c.attrs.blink},inv=${c.attrs.inverse},hidden=${c.attrs.hidden},strike=${c.attrs.strikethrough}}`;
|
|
1780
|
+
const contentAll = globalThis.__silvery_content_all;
|
|
1781
|
+
const statsStr = contentAll ? `\n--- render phase stats (${contentAll.length} calls) ---\n` + contentAll.map((s, i) => ` #${i}: visited=${s.nodesVisited} rendered=${s.nodesRendered} skipped=${s.nodesSkipped} clearOps=${s.clearOps} cascade="${s.cascadeNodes}" flags={C=${s.flagContentDirty} P=${s.flagStylePropsDirty} L=${s.flagLayoutChanged} S=${s.flagSubtreeDirty} Ch=${s.flagChildrenDirty} CP=${s.flagChildPositionChanged} AL=${s.flagAncestorLayoutChanged} noPrev=${s.noPrevBuffer}} scroll={containers=${s.scrollContainerCount} cleared=${s.scrollViewportCleared} reason="${s.scrollClearReason}"} normalRepaint="${s.normalRepaintReason}" prevBuf={null=${s._prevBufferNull} dimMismatch=${s._prevBufferDimMismatch} hasPrev=${s._hasPrevBuffer} layout=${s._layoutW}x${s._layoutH} prev=${s._prevW}x${s._prevH}}`).join("\n") : "";
|
|
1782
|
+
const msg = `SILVERY_STRICT (createApp): MISMATCH at (${x}, ${y}) on render #${_renderCount}\n incremental: ${cellStr(a)}\n fresh: ${cellStr(b)}` + statsStr + (() => {
|
|
1783
|
+
const traces = globalThis.__silvery_node_trace;
|
|
1784
|
+
if (!traces || traces.length === 0) return "";
|
|
1785
|
+
let out = "\n--- node trace ---";
|
|
1786
|
+
for (let ti = 0; ti < traces.length; ti++) {
|
|
1787
|
+
out += `\n renderPhase #${ti}:`;
|
|
1788
|
+
for (const t of traces[ti]) {
|
|
1789
|
+
out += `\n ${t.decision} ${t.id}(${t.type})@${t.depth} rect=${t.rect} prev=${t.prevLayout}`;
|
|
1790
|
+
out += ` hasPrev=${t.hasPrev} ancClr=${t.ancestorCleared} flags=[${t.flags}] layout∆=${t.layoutChanged}`;
|
|
1791
|
+
if (t.decision === "RENDER") {
|
|
1792
|
+
out += ` caa=${t.contentAreaAffected} crc=${t.contentRegionCleared} cnfr=${t.childrenNeedFreshRender}`;
|
|
1793
|
+
out += ` childPrev=${t.childHasPrev} childAnc=${t.childAncestorCleared} skipBg=${t.skipBgFill} bg=${t.bgColor ?? "none"}`;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return out;
|
|
1798
|
+
})() + cellDebugInfo + trapInfo + `\n--- incremental ---\n${incText}\n--- fresh ---\n${freshText}`;
|
|
1799
|
+
__require("node:fs").appendFileSync("/tmp/silvery-perf.log", msg + "\n");
|
|
1800
|
+
throw new IncrementalRenderMismatchError(msg);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (_perfLog) __require("node:fs").appendFileSync("/tmp/silvery-perf.log", `SILVERY_STRICT (createApp): render #${_renderCount} OK\n`);
|
|
1804
|
+
}
|
|
1805
|
+
const buf = createBuffer(termBuffer, rootNode);
|
|
1806
|
+
if (_perfLog) {
|
|
1807
|
+
const renderDuration = performance.now() - renderStart;
|
|
1808
|
+
const phases = globalThis.__silvery_last_pipeline;
|
|
1809
|
+
const detail = globalThis.__silvery_content_detail;
|
|
1810
|
+
const phaseStr = phases ? ` [measure=${phases.measure.toFixed(1)} layout=${phases.layout.toFixed(1)} content=${phases.content.toFixed(1)} output=${phases.output.toFixed(1)}]` : "";
|
|
1811
|
+
const detailStr = detail ? ` {visited=${detail.nodesVisited} rendered=${detail.nodesRendered} skipped=${detail.nodesSkipped} noPrev=${detail.noPrevBuffer ?? 0} dirty=${detail.flagContentDirty ?? 0} paint=${detail.flagStylePropsDirty ?? 0} layoutChg=${detail.flagLayoutChanged ?? 0} subtree=${detail.flagSubtreeDirty ?? 0} children=${detail.flagChildrenDirty ?? 0} childPos=${detail.flagChildPositionChanged ?? 0} scroll=${detail.scrollContainerCount ?? 0}/${detail.scrollViewportCleared ?? 0}${detail.scrollClearReason ? `(${detail.scrollClearReason})` : ""}}${detail.cascadeNodes ? ` CASCADE[minDepth=${detail.cascadeMinDepth} ${detail.cascadeNodes}]` : ""}` : "";
|
|
1812
|
+
__require("node:fs").appendFileSync("/tmp/silvery-perf.log", `doRender #${_renderCount}: ${renderDuration.toFixed(1)}ms (reconcile=${reconcileMs.toFixed(1)}ms pipeline=${pipelineMs.toFixed(1)}ms ${dims.cols}x${dims.rows})${phaseStr}${detailStr}\n`);
|
|
1813
|
+
}
|
|
1814
|
+
return buf;
|
|
1815
|
+
}
|
|
1816
|
+
if (_ansiTrace) __require("node:fs").appendFileSync("/tmp/silvery-trace.log", "=== INITIAL RENDER ===\n");
|
|
1817
|
+
currentBuffer = doRender();
|
|
1818
|
+
if (!headless) {
|
|
1819
|
+
if (_ansiTrace) __require("node:fs").appendFileSync("/tmp/silvery-trace.log", "=== ALT SCREEN + CLEAR ===\n");
|
|
1820
|
+
if (alternateScreen) {
|
|
1821
|
+
stdout.write("\x1B[?1049h");
|
|
1822
|
+
stdout.write("\x1B[2J\x1B[H");
|
|
1823
|
+
}
|
|
1824
|
+
stdout.write("\x1B[?25l");
|
|
1825
|
+
if (kittyOption != null && kittyOption !== false) if (kittyOption === true) {
|
|
1826
|
+
if ((await detectKittyFromStdio(stdout, stdin)).supported) {
|
|
1827
|
+
stdout.write(enableKittyKeyboard(defaultKittyFlags));
|
|
1828
|
+
kittyEnabled = true;
|
|
1829
|
+
kittyFlags = defaultKittyFlags;
|
|
1830
|
+
}
|
|
1831
|
+
} else {
|
|
1832
|
+
stdout.write(enableKittyKeyboard(kittyOption));
|
|
1833
|
+
kittyEnabled = true;
|
|
1834
|
+
kittyFlags = kittyOption;
|
|
1835
|
+
}
|
|
1836
|
+
else if (kittyOption == null) {
|
|
1837
|
+
stdout.write(enableKittyKeyboard(defaultKittyFlags));
|
|
1838
|
+
kittyEnabled = true;
|
|
1839
|
+
kittyFlags = defaultKittyFlags;
|
|
1840
|
+
}
|
|
1841
|
+
if (mouseOption) {
|
|
1842
|
+
stdout.write(enableMouse());
|
|
1843
|
+
mouseEnabled = true;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (_ansiTrace) __require("node:fs").appendFileSync("/tmp/silvery-trace.log", "=== RUNTIME.RENDER (initial) ===\n");
|
|
1847
|
+
runtime.render(currentBuffer);
|
|
1848
|
+
if (_perfLog) __require("node:fs").appendFileSync("/tmp/silvery-perf.log", `STARTUP: initial render done (render #${_renderCount}, incremental=${!_noIncremental})\n`);
|
|
1849
|
+
if (shouldGuardOutput) outputGuard = createOutputGuard();
|
|
1850
|
+
if (!headless) {
|
|
1851
|
+
runtimeContextValue.pause = () => {
|
|
1852
|
+
renderPaused = true;
|
|
1853
|
+
if (outputGuard) {
|
|
1854
|
+
outputGuard.dispose();
|
|
1855
|
+
outputGuard = null;
|
|
1856
|
+
}
|
|
1857
|
+
if (alternateScreen) stdout.write(leaveAlternateScreen());
|
|
1858
|
+
};
|
|
1859
|
+
runtimeContextValue.resume = () => {
|
|
1860
|
+
if (alternateScreen) stdout.write(enterAlternateScreen());
|
|
1861
|
+
renderPaused = false;
|
|
1862
|
+
if (shouldGuardOutput && !outputGuard) outputGuard = createOutputGuard();
|
|
1863
|
+
runtime.invalidate();
|
|
1864
|
+
_ag?.resetBuffer();
|
|
1865
|
+
if (!isRendering) {
|
|
1866
|
+
currentBuffer = doRender();
|
|
1867
|
+
runtime.render(currentBuffer);
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
let exitResolve;
|
|
1872
|
+
let exitResolved = false;
|
|
1873
|
+
const exitPromise = new Promise((resolve) => {
|
|
1874
|
+
exitResolve = () => {
|
|
1875
|
+
if (!exitResolved) {
|
|
1876
|
+
exitResolved = true;
|
|
1877
|
+
resolve();
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
});
|
|
1881
|
+
exit = () => {
|
|
1882
|
+
if (shouldExit) return;
|
|
1883
|
+
shouldExit = true;
|
|
1884
|
+
if (!headless && stdout.isTTY) {
|
|
1885
|
+
const earlyDisable = [
|
|
1886
|
+
disableKittyKeyboard(),
|
|
1887
|
+
disableMouse(),
|
|
1888
|
+
"\x1B[?1004l"
|
|
1889
|
+
].join("");
|
|
1890
|
+
try {
|
|
1891
|
+
writeSync(stdout.fd, earlyDisable);
|
|
1892
|
+
} catch {
|
|
1893
|
+
try {
|
|
1894
|
+
stdout.write(earlyDisable);
|
|
1895
|
+
} catch {}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
controller.abort();
|
|
1899
|
+
if (!inEventHandler) {
|
|
1900
|
+
cleanup();
|
|
1901
|
+
exitResolve();
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
runtimeContextValue.exit = exit;
|
|
1905
|
+
let frameResolve = null;
|
|
1906
|
+
let framesDone = false;
|
|
1907
|
+
function emitFrame(buf) {
|
|
1908
|
+
if (frameResolve) {
|
|
1909
|
+
const resolve = frameResolve;
|
|
1910
|
+
frameResolve = null;
|
|
1911
|
+
resolve(buf);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
storeUnsubscribeFn = store.subscribe(() => {
|
|
1915
|
+
if (shouldExit) return;
|
|
1916
|
+
if (_ansiTrace) {
|
|
1917
|
+
const _case = inEventHandler ? "1:event" : isRendering ? "2:rendering" : "3:standalone";
|
|
1918
|
+
const stack = (/* @__PURE__ */ new Error()).stack?.split("\n").slice(1, 5).join("\n") ?? "";
|
|
1919
|
+
__require("node:fs").appendFileSync("/tmp/silvery-trace.log", `=== SUBSCRIPTION (case ${_case}, render #${_renderCount + 1}) ===\n${stack}\n`);
|
|
1920
|
+
}
|
|
1921
|
+
if (inEventHandler) {
|
|
1922
|
+
pendingRerender = true;
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (isRendering) {
|
|
1926
|
+
if (!pendingRerender) {
|
|
1927
|
+
pendingRerender = true;
|
|
1928
|
+
queueMicrotask(() => {
|
|
1929
|
+
if (!pendingRerender) return;
|
|
1930
|
+
pendingRerender = false;
|
|
1931
|
+
if (!shouldExit && !isRendering) {
|
|
1932
|
+
if (_perfLog) __require("node:fs").appendFileSync("/tmp/silvery-perf.log", `SUBSCRIPTION: deferred microtask render (case 2, render #${_renderCount + 1})\n`);
|
|
1933
|
+
isRendering = true;
|
|
1934
|
+
try {
|
|
1935
|
+
currentBuffer = doRender();
|
|
1936
|
+
runtime.render(currentBuffer);
|
|
1937
|
+
} finally {
|
|
1938
|
+
isRendering = false;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (_perfLog) __require("node:fs").appendFileSync("/tmp/silvery-perf.log", `SUBSCRIPTION: immediate render (case 3, render #${_renderCount + 1})\n`);
|
|
1946
|
+
isRendering = true;
|
|
1947
|
+
try {
|
|
1948
|
+
currentBuffer = doRender();
|
|
1949
|
+
runtime.render(currentBuffer);
|
|
1950
|
+
} finally {
|
|
1951
|
+
isRendering = false;
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
function createProviderEventStream(name, provider) {
|
|
1955
|
+
return map(provider.events(), (event) => ({
|
|
1956
|
+
type: `${name}:${String(event.type)}`,
|
|
1957
|
+
provider: name,
|
|
1958
|
+
event: String(event.type),
|
|
1959
|
+
data: event.data
|
|
1960
|
+
}));
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Write selection overlay to stdout after a render.
|
|
1964
|
+
* Appends inverse-video ANSI sequences over selected cells.
|
|
1965
|
+
*/
|
|
1966
|
+
function writeSelectionOverlay() {
|
|
1967
|
+
if (!selectionEnabled || !selectionState.range || !currentBuffer) return;
|
|
1968
|
+
const mode = alternateScreen ? "fullscreen" : "inline";
|
|
1969
|
+
const overlay = renderSelectionOverlay(selectionState.range, currentBuffer._buffer, mode, selectionState.scope);
|
|
1970
|
+
if (overlay) target.write(overlay);
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Push the current rendered frame to the virtual scrollback buffer.
|
|
1974
|
+
*/
|
|
1975
|
+
function pushToScrollback() {
|
|
1976
|
+
if (!scrollback || !currentBuffer) return;
|
|
1977
|
+
const lines = currentBuffer.text.split("\n");
|
|
1978
|
+
scrollback.push(lines);
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Render the virtual scrollback view (historical content) to the terminal.
|
|
1982
|
+
* When scrolled up, replaces the live app content with historical rows.
|
|
1983
|
+
*/
|
|
1984
|
+
function renderVirtualScrollbackView() {
|
|
1985
|
+
if (!scrollback || virtualScrollOffset <= 0) return;
|
|
1986
|
+
const dims = target.getDims();
|
|
1987
|
+
const rows = scrollback.getVisibleRows(virtualScrollOffset, dims.rows);
|
|
1988
|
+
let out = "";
|
|
1989
|
+
for (let row = 0; row < rows.length; row++) out += `\x1b[${row + 1};1H\x1b[2K${rows[row] ?? ""}`;
|
|
1990
|
+
const indicator = ` ↑ ${virtualScrollOffset} lines `;
|
|
1991
|
+
const indicatorCol = Math.max(1, dims.cols - indicator.length + 1);
|
|
1992
|
+
out += `\x1b[1;${indicatorCol}H\x1b[7m${indicator}\x1b[27m`;
|
|
1993
|
+
target.write(out);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Render search highlights for the current match with inverse video.
|
|
1997
|
+
*/
|
|
1998
|
+
function renderSearchHighlights() {
|
|
1999
|
+
if (!searchState.active || searchState.currentMatch < 0) return;
|
|
2000
|
+
const match = searchState.matches[searchState.currentMatch];
|
|
2001
|
+
if (!match) return;
|
|
2002
|
+
const dims = target.getDims();
|
|
2003
|
+
let screenRow;
|
|
2004
|
+
if (scrollback && virtualScrollOffset > 0) {
|
|
2005
|
+
const firstVisibleLine = scrollback.totalLines - virtualScrollOffset - dims.rows;
|
|
2006
|
+
screenRow = match.row - firstVisibleLine;
|
|
2007
|
+
} else screenRow = match.row;
|
|
2008
|
+
if (screenRow < 0 || screenRow >= dims.rows) return;
|
|
2009
|
+
let out = `\x1b[${screenRow + 1};${match.startCol + 1}H\x1b[7m`;
|
|
2010
|
+
for (let col = match.startCol; col <= match.endCol; col++) if (currentBuffer && virtualScrollOffset <= 0) out += currentBuffer._buffer.getCell(col, screenRow).char;
|
|
2011
|
+
else out += searchState.query[col - match.startCol] ?? " ";
|
|
2012
|
+
out += "\x1B[27m";
|
|
2013
|
+
target.write(out);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Render the search bar at the bottom of the screen.
|
|
2017
|
+
*/
|
|
2018
|
+
function renderSearchBarOverlay() {
|
|
2019
|
+
if (!searchState.active) return;
|
|
2020
|
+
const dims = target.getDims();
|
|
2021
|
+
const bar = renderSearchBar(searchState, dims.cols);
|
|
2022
|
+
target.write(`\x1b[${dims.rows};1H${bar}`);
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Search function for virtual scrollback — converts line matches to SearchMatch[].
|
|
2026
|
+
*/
|
|
2027
|
+
function searchScrollback(query) {
|
|
2028
|
+
if (!scrollback || !query) return [];
|
|
2029
|
+
const matchingLines = scrollback.search(query);
|
|
2030
|
+
const lowerQuery = query.toLowerCase();
|
|
2031
|
+
const matches = [];
|
|
2032
|
+
for (const lineIdx of matchingLines) {
|
|
2033
|
+
const plain = (scrollback.getVisibleRows(scrollback.totalLines - lineIdx - 1, 1)[0] ?? "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
2034
|
+
let col = plain.toLowerCase().indexOf(lowerQuery);
|
|
2035
|
+
while (col !== -1) {
|
|
2036
|
+
matches.push({
|
|
2037
|
+
row: lineIdx,
|
|
2038
|
+
startCol: col,
|
|
2039
|
+
endCol: col + query.length - 1
|
|
2040
|
+
});
|
|
2041
|
+
col = plain.toLowerCase().indexOf(lowerQuery, col + 1);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
return matches;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Run a single event's handler (state mutation only, no render).
|
|
2048
|
+
* Returns true if processing should continue, false if app should exit.
|
|
2049
|
+
*
|
|
2050
|
+
* Intercepts mouse events for selection and virtual inline mode.
|
|
2051
|
+
*/
|
|
2052
|
+
function runEventHandler(event) {
|
|
2053
|
+
if (scrollback && searchState.active && event.type === "term:key") {
|
|
2054
|
+
const data = event.data;
|
|
2055
|
+
if (data.key.escape) {
|
|
2056
|
+
const [next] = searchUpdate({ type: "close" }, searchState);
|
|
2057
|
+
searchState = next;
|
|
2058
|
+
virtualScrollOffset = 0;
|
|
2059
|
+
return true;
|
|
2060
|
+
}
|
|
2061
|
+
if (data.key.return && !data.key.shift) {
|
|
2062
|
+
const [next, effects] = searchUpdate({ type: "nextMatch" }, searchState, searchScrollback);
|
|
2063
|
+
searchState = next;
|
|
2064
|
+
for (const eff of effects) if (eff.type === "scrollTo") virtualScrollOffset = Math.max(0, scrollback.totalLines - eff.row - target.getDims().rows);
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
if (data.key.return && data.key.shift) {
|
|
2068
|
+
const [next, effects] = searchUpdate({ type: "prevMatch" }, searchState, searchScrollback);
|
|
2069
|
+
searchState = next;
|
|
2070
|
+
for (const eff of effects) if (eff.type === "scrollTo") virtualScrollOffset = Math.max(0, scrollback.totalLines - eff.row - target.getDims().rows);
|
|
2071
|
+
return true;
|
|
2072
|
+
}
|
|
2073
|
+
if (data.key.backspace) {
|
|
2074
|
+
const [next, effects] = searchUpdate({ type: "backspace" }, searchState, searchScrollback);
|
|
2075
|
+
searchState = next;
|
|
2076
|
+
for (const eff of effects) if (eff.type === "scrollTo") virtualScrollOffset = Math.max(0, scrollback.totalLines - eff.row - target.getDims().rows);
|
|
2077
|
+
return true;
|
|
2078
|
+
}
|
|
2079
|
+
if (data.key.leftArrow) {
|
|
2080
|
+
const [next] = searchUpdate({ type: "cursorLeft" }, searchState);
|
|
2081
|
+
searchState = next;
|
|
2082
|
+
return true;
|
|
2083
|
+
}
|
|
2084
|
+
if (data.key.rightArrow) {
|
|
2085
|
+
const [next] = searchUpdate({ type: "cursorRight" }, searchState);
|
|
2086
|
+
searchState = next;
|
|
2087
|
+
return true;
|
|
2088
|
+
}
|
|
2089
|
+
if (data.input && !data.key.ctrl && !data.key.meta) {
|
|
2090
|
+
const [next, effects] = searchUpdate({
|
|
2091
|
+
type: "input",
|
|
2092
|
+
char: data.input
|
|
2093
|
+
}, searchState, searchScrollback);
|
|
2094
|
+
searchState = next;
|
|
2095
|
+
for (const eff of effects) if (eff.type === "scrollTo") virtualScrollOffset = Math.max(0, scrollback.totalLines - eff.row - target.getDims().rows);
|
|
2096
|
+
return true;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (scrollback && event.type === "term:key") {
|
|
2100
|
+
const data = event.data;
|
|
2101
|
+
if (data.input === "f" && data.key.ctrl) {
|
|
2102
|
+
const [next] = searchUpdate({ type: "open" }, searchState);
|
|
2103
|
+
searchState = next;
|
|
2104
|
+
return true;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (scrollback && event.event === "mouse" && event.data) {
|
|
2108
|
+
const mouseData = event.data;
|
|
2109
|
+
if (mouseData.action === "wheel") {
|
|
2110
|
+
const scrollLines = 3;
|
|
2111
|
+
if (mouseData.delta && mouseData.delta < 0) virtualScrollOffset = Math.min(virtualScrollOffset + scrollLines, Math.max(0, scrollback.totalLines - target.getDims().rows));
|
|
2112
|
+
else virtualScrollOffset = Math.max(0, virtualScrollOffset - scrollLines);
|
|
2113
|
+
return true;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (selectionEnabled && event.event === "mouse" && event.data) {
|
|
2117
|
+
const mouseData = event.data;
|
|
2118
|
+
if (mouseData.button === 0) {
|
|
2119
|
+
if (mouseData.action === "down") {
|
|
2120
|
+
if (selectionState.range) {
|
|
2121
|
+
const [cleared] = terminalSelectionUpdate({ type: "clear" }, selectionState);
|
|
2122
|
+
selectionState = cleared;
|
|
2123
|
+
}
|
|
2124
|
+
const agRoot = getContainerRoot(container);
|
|
2125
|
+
const hit = agRoot ? selectionHitTest(agRoot, mouseData.x, mouseData.y) : null;
|
|
2126
|
+
const scope = hit ? findContainBoundary(hit) : null;
|
|
2127
|
+
const [next] = terminalSelectionUpdate({
|
|
2128
|
+
type: "start",
|
|
2129
|
+
col: mouseData.x,
|
|
2130
|
+
row: mouseData.y,
|
|
2131
|
+
scope
|
|
2132
|
+
}, selectionState);
|
|
2133
|
+
selectionState = next;
|
|
2134
|
+
notifySelectionListeners();
|
|
2135
|
+
if (currentBuffer) {
|
|
2136
|
+
runtime.invalidate();
|
|
2137
|
+
currentBuffer = doRender();
|
|
2138
|
+
runtime.render(currentBuffer);
|
|
2139
|
+
writeSelectionOverlay();
|
|
2140
|
+
}
|
|
2141
|
+
} else if (mouseData.action === "move" && selectionState.selecting) {
|
|
2142
|
+
const [next] = terminalSelectionUpdate({
|
|
2143
|
+
type: "extend",
|
|
2144
|
+
col: mouseData.x,
|
|
2145
|
+
row: mouseData.y
|
|
2146
|
+
}, selectionState);
|
|
2147
|
+
selectionState = next;
|
|
2148
|
+
notifySelectionListeners();
|
|
2149
|
+
if (currentBuffer) {
|
|
2150
|
+
runtime.render(currentBuffer);
|
|
2151
|
+
writeSelectionOverlay();
|
|
2152
|
+
}
|
|
2153
|
+
return true;
|
|
2154
|
+
} else if (mouseData.action === "up" && selectionState.selecting) {
|
|
2155
|
+
const [next] = terminalSelectionUpdate({ type: "finish" }, selectionState);
|
|
2156
|
+
selectionState = next;
|
|
2157
|
+
notifySelectionListeners();
|
|
2158
|
+
if (next.range && currentBuffer) {
|
|
2159
|
+
const text = extractText(currentBuffer._buffer, next.range, { scope: next.scope });
|
|
2160
|
+
if (text.length > 0) {
|
|
2161
|
+
const base64 = globalThis.Buffer.from(text).toString("base64");
|
|
2162
|
+
target.write(`\x1b]52;c;${base64}\x07`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (currentBuffer) {
|
|
2166
|
+
runtime.render(currentBuffer);
|
|
2167
|
+
writeSelectionOverlay();
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (selectionEnabled && event.type === "term:key" && selectionState.range) {
|
|
2173
|
+
const [next] = terminalSelectionUpdate({ type: "clear" }, selectionState);
|
|
2174
|
+
selectionState = next;
|
|
2175
|
+
notifySelectionListeners();
|
|
2176
|
+
if (currentBuffer) {
|
|
2177
|
+
runtime.invalidate();
|
|
2178
|
+
currentBuffer = doRender();
|
|
2179
|
+
runtime.render(currentBuffer);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
if (scrollback && virtualScrollOffset > 0 && event.type === "term:key") {
|
|
2183
|
+
virtualScrollOffset = 0;
|
|
2184
|
+
return true;
|
|
2185
|
+
}
|
|
2186
|
+
return invokeEventHandler(event, handlers, createHandlerContext(store, focusManager, container), mouseEventState, container);
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Process a batch of events — run all handlers, then render once.
|
|
2190
|
+
*
|
|
2191
|
+
* This is the key optimization for press-and-hold / auto-repeat keys.
|
|
2192
|
+
* When events arrive faster than renders (e.g., 30/sec auto-repeat vs
|
|
2193
|
+
* 50ms renders), we batch all pending handlers into a single render pass.
|
|
2194
|
+
*
|
|
2195
|
+
* For a batch of 3 'j' presses: handler1 → handler2 → handler3 → render.
|
|
2196
|
+
* The cursor moves 3 positions, but we only pay one render cost.
|
|
2197
|
+
*/
|
|
2198
|
+
async function processEventBatch(events) {
|
|
2199
|
+
try {
|
|
2200
|
+
var _usingCtx$1 = _usingCtx();
|
|
2201
|
+
if (shouldExit || events.length === 0) return null;
|
|
2202
|
+
_renderCount = 0;
|
|
2203
|
+
_eventStart = performance.now();
|
|
2204
|
+
const _perfSpan = _usingCtx$1.u(perfLog.span?.("keypress", (() => {
|
|
2205
|
+
startTracking();
|
|
2206
|
+
const keyEvents = events.filter((e) => e.type === "term:key");
|
|
2207
|
+
return { key: keyEvents.length > 0 ? keyEvents.map((e) => e.data.input).join(",") : events[0]?.type ?? "unknown" };
|
|
2208
|
+
})()));
|
|
2209
|
+
if (!headless) {
|
|
2210
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2211
|
+
const event = events[i];
|
|
2212
|
+
if (event.type !== "term:key") continue;
|
|
2213
|
+
const data = event.data;
|
|
2214
|
+
if (data.input === "z" && data.key.ctrl && suspendOption) if (!(onSuspendHook?.() === false)) {
|
|
2215
|
+
events.splice(i, 1);
|
|
2216
|
+
performSuspend(captureTerminalState({
|
|
2217
|
+
alternateScreen,
|
|
2218
|
+
cursorHidden: true,
|
|
2219
|
+
mouse: mouseEnabled,
|
|
2220
|
+
kitty: kittyEnabled,
|
|
2221
|
+
kittyFlags,
|
|
2222
|
+
bracketedPaste: true,
|
|
2223
|
+
rawMode: true,
|
|
2224
|
+
focusReporting: focusReportingEnabled
|
|
2225
|
+
}), stdout, stdin, () => {
|
|
2226
|
+
runtime.invalidate();
|
|
2227
|
+
onResumeHook?.();
|
|
2228
|
+
});
|
|
2229
|
+
} else events.splice(i, 1);
|
|
2230
|
+
if (data.input === "c" && data.key.ctrl && exitOnCtrlCOption) {
|
|
2231
|
+
if (!(onInterruptHook?.() === false)) {
|
|
2232
|
+
exit();
|
|
2233
|
+
return null;
|
|
2234
|
+
}
|
|
2235
|
+
events.splice(i, 1);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (events.length === 0) return null;
|
|
2239
|
+
}
|
|
2240
|
+
inEventHandler = true;
|
|
2241
|
+
isRendering = true;
|
|
2242
|
+
for (const event of events) {
|
|
2243
|
+
if (event.type === "term:key") {
|
|
2244
|
+
const { input, key: parsedKey } = event.data;
|
|
2245
|
+
updateKeyboardModifiers(mouseEventState, parsedKey);
|
|
2246
|
+
if (parsedKey.eventType === "release" || isModifierOnlyEvent(input, parsedKey)) {
|
|
2247
|
+
for (const listener of runtimeInputListeners) listener(input, parsedKey);
|
|
2248
|
+
if (shouldExit) {
|
|
2249
|
+
inEventHandler = false;
|
|
2250
|
+
return null;
|
|
2251
|
+
}
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
let focusConsumed = false;
|
|
2255
|
+
if (focusManager.activeElement) focusConsumed = handleFocusNavigation(input, parsedKey, focusManager, container) === "consumed";
|
|
2256
|
+
if (!focusConsumed) for (const listener of runtimeInputListeners) listener(input, parsedKey);
|
|
2257
|
+
} else if (event.type === "term:paste") {
|
|
2258
|
+
const { text } = event.data;
|
|
2259
|
+
for (const listener of runtimePasteListeners) listener(text);
|
|
2260
|
+
} else if (event.type === "term:focus") {
|
|
2261
|
+
const { focused } = event.data;
|
|
2262
|
+
for (const listener of runtimeFocusListeners) listener(focused);
|
|
2263
|
+
}
|
|
2264
|
+
if (shouldExit) {
|
|
2265
|
+
inEventHandler = false;
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
if (event.type === "term:key") {
|
|
2269
|
+
const { input, key: k } = event.data;
|
|
2270
|
+
if (k.eventType === "release") continue;
|
|
2271
|
+
if (isModifierOnlyEvent(input, k)) continue;
|
|
2272
|
+
}
|
|
2273
|
+
const result = runEventHandler(event);
|
|
2274
|
+
if (result === false) {
|
|
2275
|
+
isRendering = false;
|
|
2276
|
+
inEventHandler = false;
|
|
2277
|
+
exit();
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
if (result === "flush") {
|
|
2281
|
+
pendingRerender = false;
|
|
2282
|
+
currentBuffer = doRender();
|
|
2283
|
+
runtime.render(currentBuffer);
|
|
2284
|
+
await Promise.resolve();
|
|
2285
|
+
if (pendingRerender) {
|
|
2286
|
+
pendingRerender = false;
|
|
2287
|
+
currentBuffer = doRender();
|
|
2288
|
+
runtime.render(currentBuffer);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
pendingRerender = false;
|
|
2293
|
+
try {
|
|
2294
|
+
currentBuffer = doRender();
|
|
2295
|
+
} finally {
|
|
2296
|
+
isRendering = false;
|
|
2297
|
+
}
|
|
2298
|
+
let flushCount = 0;
|
|
2299
|
+
const maxFlushes = 5;
|
|
2300
|
+
while (flushCount < maxFlushes) {
|
|
2301
|
+
await Promise.resolve();
|
|
2302
|
+
if (!pendingRerender) break;
|
|
2303
|
+
pendingRerender = false;
|
|
2304
|
+
isRendering = true;
|
|
2305
|
+
try {
|
|
2306
|
+
currentBuffer = doRender();
|
|
2307
|
+
} finally {
|
|
2308
|
+
isRendering = false;
|
|
2309
|
+
}
|
|
2310
|
+
flushCount++;
|
|
2311
|
+
}
|
|
2312
|
+
currentBuffer._buffer.markAllRowsDirty();
|
|
2313
|
+
inEventHandler = false;
|
|
2314
|
+
const runtimeStart = performance.now();
|
|
2315
|
+
runtime.render(currentBuffer);
|
|
2316
|
+
pushToScrollback();
|
|
2317
|
+
if (virtualScrollOffset > 0) renderVirtualScrollbackView();
|
|
2318
|
+
writeSelectionOverlay();
|
|
2319
|
+
renderSearchHighlights();
|
|
2320
|
+
renderSearchBarOverlay();
|
|
2321
|
+
const runtimeMs = performance.now() - runtimeStart;
|
|
2322
|
+
if (_perfLog) {
|
|
2323
|
+
const totalMs = performance.now() - _eventStart;
|
|
2324
|
+
__require("node:fs").appendFileSync("/tmp/silvery-perf.log", `EVENT batch(${events.length} ${events[0]?.type}): ${totalMs.toFixed(1)}ms total, ${_renderCount} doRender() calls, runtime.render=${runtimeMs.toFixed(1)}ms\n---\n`);
|
|
2325
|
+
}
|
|
2326
|
+
if (_perfSpan) checkBudget(events[0]?.type ?? "batch", performance.now() - _eventStart);
|
|
2327
|
+
return currentBuffer;
|
|
2328
|
+
} catch (_) {
|
|
2329
|
+
_usingCtx$1.e = _;
|
|
2330
|
+
} finally {
|
|
2331
|
+
_usingCtx$1.d();
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
const eventQueue = [];
|
|
2335
|
+
let eventQueueResolve = null;
|
|
2336
|
+
const eventLoop = async () => {
|
|
2337
|
+
const allEvents = merge(...Object.entries(providers).map(([name, provider]) => createProviderEventStream(name, provider)));
|
|
2338
|
+
const pumpEvents = async () => {
|
|
2339
|
+
try {
|
|
2340
|
+
for await (const event of takeUntil(allEvents, signal)) {
|
|
2341
|
+
eventQueue.push(event);
|
|
2342
|
+
if (eventQueueResolve) {
|
|
2343
|
+
const resolve = eventQueueResolve;
|
|
2344
|
+
eventQueueResolve = null;
|
|
2345
|
+
resolve();
|
|
2346
|
+
}
|
|
2347
|
+
if (shouldExit) break;
|
|
2348
|
+
}
|
|
2349
|
+
} finally {
|
|
2350
|
+
if (eventQueueResolve) {
|
|
2351
|
+
const resolve = eventQueueResolve;
|
|
2352
|
+
eventQueueResolve = null;
|
|
2353
|
+
resolve();
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
if (needsProbe) try {
|
|
2358
|
+
const wasRaw = stdin.isRaw;
|
|
2359
|
+
if (stdin.isTTY && !wasRaw) {
|
|
2360
|
+
stdin.setRawMode(true);
|
|
2361
|
+
stdin.resume();
|
|
2362
|
+
stdin.setEncoding("utf8");
|
|
2363
|
+
}
|
|
2364
|
+
const probeRead = () => new Promise((resolve) => {
|
|
2365
|
+
const onData = (data) => {
|
|
2366
|
+
stdin.off("data", onData);
|
|
2367
|
+
resolve(data);
|
|
2368
|
+
};
|
|
2369
|
+
stdin.on("data", onData);
|
|
2370
|
+
});
|
|
2371
|
+
const probeResult = await detectTextSizingSupport((data) => outputGuard ? outputGuard.writeStdout(data) : stdout.write(data), probeRead, 500);
|
|
2372
|
+
if (probeResult.supported !== textSizingEnabled) {
|
|
2373
|
+
textSizingEnabled = probeResult.supported;
|
|
2374
|
+
if (effectiveCaps) {
|
|
2375
|
+
effectiveCaps = {
|
|
2376
|
+
...effectiveCaps,
|
|
2377
|
+
textSizingSupported: textSizingEnabled
|
|
2378
|
+
};
|
|
2379
|
+
pipelineConfig = createPipeline({ caps: effectiveCaps });
|
|
2380
|
+
runtime.setOutputPhaseFn(pipelineConfig.outputPhaseFn);
|
|
2381
|
+
}
|
|
2382
|
+
_ag = null;
|
|
2383
|
+
runtime.invalidate();
|
|
2384
|
+
if (!isRendering) {
|
|
2385
|
+
isRendering = true;
|
|
2386
|
+
try {
|
|
2387
|
+
currentBuffer = doRender();
|
|
2388
|
+
runtime.render(currentBuffer);
|
|
2389
|
+
} finally {
|
|
2390
|
+
isRendering = false;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (stdin.isTTY && !wasRaw) {
|
|
2395
|
+
stdin.setRawMode(false);
|
|
2396
|
+
stdin.pause();
|
|
2397
|
+
}
|
|
2398
|
+
} catch {}
|
|
2399
|
+
if (needsWidthDetection) try {
|
|
2400
|
+
const wasRaw = stdin.isRaw;
|
|
2401
|
+
if (stdin.isTTY && !wasRaw) {
|
|
2402
|
+
stdin.setRawMode(true);
|
|
2403
|
+
stdin.resume();
|
|
2404
|
+
stdin.setEncoding("utf8");
|
|
2405
|
+
}
|
|
2406
|
+
const stdinHandlers = [];
|
|
2407
|
+
const stdinListener = (data) => {
|
|
2408
|
+
for (const handler of stdinHandlers) handler(data);
|
|
2409
|
+
};
|
|
2410
|
+
stdin.on("data", stdinListener);
|
|
2411
|
+
const detector = createWidthDetector({
|
|
2412
|
+
write: (data) => outputGuard ? outputGuard.writeStdout(data) : stdout.write(data),
|
|
2413
|
+
onData: (handler) => {
|
|
2414
|
+
stdinHandlers.push(handler);
|
|
2415
|
+
return () => {
|
|
2416
|
+
const idx = stdinHandlers.indexOf(handler);
|
|
2417
|
+
if (idx >= 0) stdinHandlers.splice(idx, 1);
|
|
2418
|
+
};
|
|
2419
|
+
},
|
|
2420
|
+
timeoutMs: 200
|
|
2421
|
+
});
|
|
2422
|
+
const widthConfig = await detector.detect();
|
|
2423
|
+
detector.dispose();
|
|
2424
|
+
stdin.off("data", stdinListener);
|
|
2425
|
+
if (effectiveCaps) {
|
|
2426
|
+
const updatedCaps = applyWidthConfig(effectiveCaps, widthConfig);
|
|
2427
|
+
if (updatedCaps.textEmojiWide !== effectiveCaps.textEmojiWide || updatedCaps.textSizingSupported !== effectiveCaps.textSizingSupported) {
|
|
2428
|
+
effectiveCaps = updatedCaps;
|
|
2429
|
+
pipelineConfig = createPipeline({ caps: effectiveCaps });
|
|
2430
|
+
runtime.setOutputPhaseFn(pipelineConfig.outputPhaseFn);
|
|
2431
|
+
_ag = null;
|
|
2432
|
+
runtime.invalidate();
|
|
2433
|
+
if (!isRendering) {
|
|
2434
|
+
isRendering = true;
|
|
2435
|
+
try {
|
|
2436
|
+
currentBuffer = doRender();
|
|
2437
|
+
runtime.render(currentBuffer);
|
|
2438
|
+
} finally {
|
|
2439
|
+
isRendering = false;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
if (stdin.isTTY && !wasRaw) {
|
|
2445
|
+
stdin.setRawMode(false);
|
|
2446
|
+
stdin.pause();
|
|
2447
|
+
}
|
|
2448
|
+
} catch {}
|
|
2449
|
+
pumpEvents().catch((err) => log.error?.(`pumpEvents failed: ${err}`));
|
|
2450
|
+
if (focusReportingOption && !focusReportingEnabled) {
|
|
2451
|
+
enableFocusReporting((s) => outputGuard ? outputGuard.writeStdout(s) : stdout.write(s));
|
|
2452
|
+
focusReportingEnabled = true;
|
|
2453
|
+
}
|
|
2454
|
+
try {
|
|
2455
|
+
while (!shouldExit && !signal.aborted) {
|
|
2456
|
+
if (eventQueue.length === 0) await new Promise((resolve) => {
|
|
2457
|
+
eventQueueResolve = resolve;
|
|
2458
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
2459
|
+
});
|
|
2460
|
+
if (shouldExit || signal.aborted) break;
|
|
2461
|
+
if (eventQueue.length === 0) continue;
|
|
2462
|
+
const maxDrainSpins = 32;
|
|
2463
|
+
let drainSpins = 0;
|
|
2464
|
+
const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
2465
|
+
await yieldToEventLoop();
|
|
2466
|
+
let prevLen = eventQueue.length;
|
|
2467
|
+
while (drainSpins < maxDrainSpins) {
|
|
2468
|
+
await yieldToEventLoop();
|
|
2469
|
+
const curLen = eventQueue.length;
|
|
2470
|
+
if (curLen === prevLen) break;
|
|
2471
|
+
prevLen = curLen;
|
|
2472
|
+
drainSpins++;
|
|
2473
|
+
}
|
|
2474
|
+
if (_perfLog) __require("node:fs").appendFileSync("/tmp/silvery-perf.log", `DRAIN: spins=${drainSpins}, batch=${eventQueue.length}\n`);
|
|
2475
|
+
const _g = globalThis;
|
|
2476
|
+
_g.__silvery_last_drain_spins = drainSpins;
|
|
2477
|
+
_g.__silvery_last_batch_size = eventQueue.length;
|
|
2478
|
+
_g.__silvery_batch_count = (_g.__silvery_batch_count ?? 0) + 1;
|
|
2479
|
+
const buf = await processEventBatch(eventQueue.splice(0));
|
|
2480
|
+
if (buf) emitFrame(buf);
|
|
2481
|
+
}
|
|
2482
|
+
} finally {
|
|
2483
|
+
framesDone = true;
|
|
2484
|
+
if (frameResolve) {
|
|
2485
|
+
const resolve = frameResolve;
|
|
2486
|
+
frameResolve = null;
|
|
2487
|
+
resolve(null);
|
|
2488
|
+
}
|
|
2489
|
+
if (shouldExit && !cleanedUp && !headless && stdin.isTTY) try {
|
|
2490
|
+
stdin.removeAllListeners("data");
|
|
2491
|
+
stdin.resume();
|
|
2492
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
2493
|
+
while (stdin.read() !== null);
|
|
2494
|
+
stdin.pause();
|
|
2495
|
+
} catch {}
|
|
2496
|
+
cleanup();
|
|
2497
|
+
exitResolve();
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
eventLoop().catch((err) => log.error?.(`eventLoop failed: ${err}`));
|
|
2501
|
+
return {
|
|
2502
|
+
get text() {
|
|
2503
|
+
return currentBuffer.text;
|
|
2504
|
+
},
|
|
2505
|
+
get root() {
|
|
2506
|
+
return getContainerRoot(container);
|
|
2507
|
+
},
|
|
2508
|
+
get buffer() {
|
|
2509
|
+
return currentBuffer?._buffer ?? null;
|
|
2510
|
+
},
|
|
2511
|
+
get store() {
|
|
2512
|
+
return store;
|
|
2513
|
+
},
|
|
2514
|
+
waitUntilExit() {
|
|
2515
|
+
return exitPromise;
|
|
2516
|
+
},
|
|
2517
|
+
unmount() {
|
|
2518
|
+
exit();
|
|
2519
|
+
},
|
|
2520
|
+
[Symbol.dispose]() {
|
|
2521
|
+
exit();
|
|
2522
|
+
},
|
|
2523
|
+
async press(rawKey) {
|
|
2524
|
+
try {
|
|
2525
|
+
var _usingCtx3 = _usingCtx();
|
|
2526
|
+
const pressStart = performance.now();
|
|
2527
|
+
const [input, parsedKey] = parseKey(useKittyMode ? keyToKittyAnsi(rawKey) : keyToAnsi(rawKey));
|
|
2528
|
+
const _perfSpan = _usingCtx3.u(perfLog.span?.("keypress", (() => {
|
|
2529
|
+
startTracking();
|
|
2530
|
+
return { key: input || rawKey };
|
|
2531
|
+
})()));
|
|
2532
|
+
if (input === "c" && parsedKey.ctrl && exitOnCtrlCOption) {
|
|
2533
|
+
if (!(onInterruptHook?.() === false)) {
|
|
2534
|
+
exit();
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
for (const listener of runtimeInputListeners) listener(input, parsedKey);
|
|
2539
|
+
inEventHandler = true;
|
|
2540
|
+
isRendering = true;
|
|
2541
|
+
if (handleFocusNavigation(input, parsedKey, focusManager, container) === "consumed") {
|
|
2542
|
+
pendingRerender = false;
|
|
2543
|
+
isRendering = false;
|
|
2544
|
+
inEventHandler = false;
|
|
2545
|
+
doRender();
|
|
2546
|
+
await Promise.resolve();
|
|
2547
|
+
if (_perfSpan) checkBudget(input || rawKey, performance.now() - pressStart);
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
if (dispatchKeyToHandlers(input, parsedKey, handlers, createHandlerContext(store, focusManager, container)) === "exit") {
|
|
2551
|
+
isRendering = false;
|
|
2552
|
+
inEventHandler = false;
|
|
2553
|
+
exit();
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
pendingRerender = false;
|
|
2557
|
+
try {
|
|
2558
|
+
currentBuffer = doRender();
|
|
2559
|
+
} finally {
|
|
2560
|
+
isRendering = false;
|
|
2561
|
+
}
|
|
2562
|
+
let flushCount = 0;
|
|
2563
|
+
const maxFlushes = 5;
|
|
2564
|
+
while (flushCount < maxFlushes) {
|
|
2565
|
+
await Promise.resolve();
|
|
2566
|
+
if (!pendingRerender) break;
|
|
2567
|
+
pendingRerender = false;
|
|
2568
|
+
isRendering = true;
|
|
2569
|
+
try {
|
|
2570
|
+
currentBuffer = doRender();
|
|
2571
|
+
} finally {
|
|
2572
|
+
isRendering = false;
|
|
2573
|
+
}
|
|
2574
|
+
flushCount++;
|
|
2575
|
+
}
|
|
2576
|
+
if (flushCount > 0) currentBuffer._buffer.markAllRowsDirty();
|
|
2577
|
+
inEventHandler = false;
|
|
2578
|
+
runtime.render(currentBuffer);
|
|
2579
|
+
if (_perfSpan) checkBudget(input || rawKey, performance.now() - pressStart);
|
|
2580
|
+
} catch (_) {
|
|
2581
|
+
_usingCtx3.e = _;
|
|
2582
|
+
} finally {
|
|
2583
|
+
_usingCtx3.d();
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
[Symbol.asyncIterator]() {
|
|
2587
|
+
return {
|
|
2588
|
+
async next() {
|
|
2589
|
+
if (framesDone || shouldExit) return {
|
|
2590
|
+
done: true,
|
|
2591
|
+
value: void 0
|
|
2592
|
+
};
|
|
2593
|
+
const buf = await new Promise((resolve) => {
|
|
2594
|
+
if (framesDone || shouldExit) {
|
|
2595
|
+
resolve(null);
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
frameResolve = resolve;
|
|
2599
|
+
});
|
|
2600
|
+
if (!buf) return {
|
|
2601
|
+
done: true,
|
|
2602
|
+
value: void 0
|
|
2603
|
+
};
|
|
2604
|
+
return {
|
|
2605
|
+
done: false,
|
|
2606
|
+
value: buf
|
|
2607
|
+
};
|
|
2608
|
+
},
|
|
2609
|
+
async return() {
|
|
2610
|
+
exit();
|
|
2611
|
+
return {
|
|
2612
|
+
done: true,
|
|
2613
|
+
value: void 0
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
//#endregion
|
|
2621
|
+
//#region ../packages/ag-term/src/runtime/run.tsx
|
|
2622
|
+
init_detect();
|
|
2623
|
+
init_ThemeContext();
|
|
2624
|
+
//#endregion
|
|
2625
|
+
//#region ../packages/create/src/pipe.ts
|
|
2626
|
+
function pipe(base, ...plugins) {
|
|
2627
|
+
let result = base;
|
|
2628
|
+
for (const plugin of plugins) result = plugin(result);
|
|
2629
|
+
return result;
|
|
2630
|
+
}
|
|
2631
|
+
//#endregion
|
|
2632
|
+
//#region ../packages/ag-react/src/with-react.ts
|
|
2633
|
+
/**
|
|
2634
|
+
* Associate a React element with an app for rendering.
|
|
2635
|
+
*
|
|
2636
|
+
* In pipe() composition, this captures the element so that subsequent
|
|
2637
|
+
* plugins and the final run() know what to render.
|
|
2638
|
+
*
|
|
2639
|
+
* The plugin wraps `run()` to automatically pass the element:
|
|
2640
|
+
* - Before: `app.run(<Board />, options)`
|
|
2641
|
+
* - After: `app.run()` (element already bound)
|
|
2642
|
+
*
|
|
2643
|
+
* @param element - The React element to render
|
|
2644
|
+
* @returns Plugin function that binds the element to the app
|
|
2645
|
+
*/
|
|
2646
|
+
function withReact(element) {
|
|
2647
|
+
return (app) => {
|
|
2648
|
+
const originalRun = app.run;
|
|
2649
|
+
return Object.assign(Object.create(app), {
|
|
2650
|
+
element,
|
|
2651
|
+
run(...args) {
|
|
2652
|
+
if (args.length === 0 || typeof args[0] !== "object" || args[0] === null || !("type" in args[0])) return originalRun.call(app, element, ...args);
|
|
2653
|
+
return originalRun.apply(app, args);
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
//#endregion
|
|
2659
|
+
//#region ../packages/ag-term/src/features/clipboard-capability.ts
|
|
2660
|
+
/**
|
|
2661
|
+
* Create an OSC 52 clipboard capability.
|
|
2662
|
+
*
|
|
2663
|
+
* Encodes text as base64 and writes the OSC 52 sequence directly.
|
|
2664
|
+
* This is a standalone factory that doesn't require the full ClipboardBackend.
|
|
2665
|
+
*/
|
|
2666
|
+
function createOSC52Clipboard(write) {
|
|
2667
|
+
return { copy(text) {
|
|
2668
|
+
write(`\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
|
|
2669
|
+
} };
|
|
2670
|
+
}
|
|
2671
|
+
//#endregion
|
|
2672
|
+
//#region ../packages/ag-term/src/plugins/with-terminal.ts
|
|
2673
|
+
/**
|
|
2674
|
+
* withTerminal(process, opts?) — Plugin: ALL terminal I/O
|
|
2675
|
+
*
|
|
2676
|
+
* This plugin represents the terminal I/O layer in silvery's plugin
|
|
2677
|
+
* composition model. It wraps all terminal concerns:
|
|
2678
|
+
* - stdin → typed events (term:key, term:mouse, term:paste)
|
|
2679
|
+
* - stdout → alternate screen, raw mode, incremental diff output
|
|
2680
|
+
* - SIGWINCH → term:resize
|
|
2681
|
+
* - Lifecycle (Ctrl+Z suspend/resume, Ctrl+C exit)
|
|
2682
|
+
* - Protocols (SGR mouse, Kitty keyboard, bracketed paste)
|
|
2683
|
+
*
|
|
2684
|
+
* In the current architecture, terminal I/O is handled by createApp()
|
|
2685
|
+
* and the TermProvider. This plugin provides the declarative interface
|
|
2686
|
+
* for pipe() composition:
|
|
2687
|
+
*
|
|
2688
|
+
* ```tsx
|
|
2689
|
+
* const app = pipe(
|
|
2690
|
+
* createApp(store),
|
|
2691
|
+
* withReact(<Board />),
|
|
2692
|
+
* withTerminal(process, { mouse: true, kitty: true }),
|
|
2693
|
+
* withFocus(),
|
|
2694
|
+
* withDomEvents(),
|
|
2695
|
+
* )
|
|
2696
|
+
* ```
|
|
2697
|
+
*
|
|
2698
|
+
* @example
|
|
2699
|
+
* ```tsx
|
|
2700
|
+
* import { pipe, withTerminal } from '@silvery/create'
|
|
2701
|
+
*
|
|
2702
|
+
* // All protocols enabled by default
|
|
2703
|
+
* const app = pipe(baseApp, withTerminal(process))
|
|
2704
|
+
*
|
|
2705
|
+
* // Customize terminal options
|
|
2706
|
+
* const app = pipe(baseApp, withTerminal(process, {
|
|
2707
|
+
* mouse: true,
|
|
2708
|
+
* kitty: true,
|
|
2709
|
+
* paste: true,
|
|
2710
|
+
* onSuspend: () => saveState(),
|
|
2711
|
+
* onResume: () => restoreState(),
|
|
2712
|
+
* }))
|
|
2713
|
+
* ```
|
|
2714
|
+
*/
|
|
2715
|
+
/**
|
|
2716
|
+
* Configure terminal I/O for an app.
|
|
2717
|
+
*
|
|
2718
|
+
* In pipe() composition, this captures the process streams and options
|
|
2719
|
+
* so that run() configures terminal I/O correctly.
|
|
2720
|
+
*
|
|
2721
|
+
* The plugin wraps `run()` to inject terminal options:
|
|
2722
|
+
* - stdin/stdout from the process object
|
|
2723
|
+
* - Protocol options (mouse, kitty, paste)
|
|
2724
|
+
* - Lifecycle handlers (suspend, resume, interrupt)
|
|
2725
|
+
*
|
|
2726
|
+
* @param proc - Process object with stdin/stdout (typically `process`)
|
|
2727
|
+
* @param options - Terminal configuration
|
|
2728
|
+
* @returns Plugin function that binds terminal config to the app
|
|
2729
|
+
*/
|
|
2730
|
+
function withTerminal(proc, options = {}) {
|
|
2731
|
+
const termConfig = {
|
|
2732
|
+
mouse: options.mouse ?? true,
|
|
2733
|
+
kitty: options.kitty ?? true,
|
|
2734
|
+
paste: options.paste ?? true,
|
|
2735
|
+
alternateScreen: options.alternateScreen ?? true,
|
|
2736
|
+
suspendOnCtrlZ: options.suspendOnCtrlZ ?? true,
|
|
2737
|
+
exitOnCtrlC: options.exitOnCtrlC ?? true,
|
|
2738
|
+
...options,
|
|
2739
|
+
proc
|
|
2740
|
+
};
|
|
2741
|
+
return (app) => {
|
|
2742
|
+
const originalRun = app.run;
|
|
2743
|
+
const registry = app.capabilityRegistry ?? createCapabilityRegistry();
|
|
2744
|
+
const clipboard = createOSC52Clipboard((data) => {
|
|
2745
|
+
proc.stdout.write(data);
|
|
2746
|
+
});
|
|
2747
|
+
registry.register(CLIPBOARD_CAPABILITY, clipboard);
|
|
2748
|
+
const autoDetect = termConfig.autoDetect ?? false;
|
|
2749
|
+
const timeoutMs = termConfig.autoDetectTimeoutMs ?? 200;
|
|
2750
|
+
let colorSchemeDetector;
|
|
2751
|
+
let widthDetector;
|
|
2752
|
+
let detectionReady;
|
|
2753
|
+
if (autoDetect && proc.stdin.isTTY) {
|
|
2754
|
+
const write = (data) => {
|
|
2755
|
+
proc.stdout.write(data);
|
|
2756
|
+
};
|
|
2757
|
+
const onData = (handler) => {
|
|
2758
|
+
const bufferHandler = (chunk) => {
|
|
2759
|
+
handler(typeof chunk === "string" ? chunk : chunk.toString());
|
|
2760
|
+
};
|
|
2761
|
+
proc.stdin.on("data", bufferHandler);
|
|
2762
|
+
return () => {
|
|
2763
|
+
proc.stdin.removeListener("data", bufferHandler);
|
|
2764
|
+
};
|
|
2765
|
+
};
|
|
2766
|
+
colorSchemeDetector = createColorSchemeDetector({
|
|
2767
|
+
write,
|
|
2768
|
+
onData,
|
|
2769
|
+
timeoutMs
|
|
2770
|
+
});
|
|
2771
|
+
colorSchemeDetector.start();
|
|
2772
|
+
widthDetector = createWidthDetector({
|
|
2773
|
+
write,
|
|
2774
|
+
onData,
|
|
2775
|
+
timeoutMs
|
|
2776
|
+
});
|
|
2777
|
+
detectionReady = widthDetector.detect().then((config) => {
|
|
2778
|
+
if (enhanced.terminalOptions?.proc) enhanced._detectedWidthConfig = config;
|
|
2779
|
+
}).catch(() => {});
|
|
2780
|
+
} else detectionReady = Promise.resolve();
|
|
2781
|
+
const enhanced = Object.assign(Object.create(app), {
|
|
2782
|
+
terminalOptions: termConfig,
|
|
2783
|
+
capabilityRegistry: registry,
|
|
2784
|
+
clipboardCapability: clipboard,
|
|
2785
|
+
colorSchemeDetector,
|
|
2786
|
+
widthDetector,
|
|
2787
|
+
detectionReady,
|
|
2788
|
+
_detectedWidthConfig: null,
|
|
2789
|
+
run(...args) {
|
|
2790
|
+
const runOptions = {};
|
|
2791
|
+
let existingOptions;
|
|
2792
|
+
if (args.length > 0 && typeof args[args.length - 1] === "object" && args[args.length - 1] !== null) {
|
|
2793
|
+
existingOptions = args[args.length - 1];
|
|
2794
|
+
if ("type" in existingOptions && "props" in existingOptions) existingOptions = void 0;
|
|
2795
|
+
}
|
|
2796
|
+
Object.assign(runOptions, existingOptions ?? {}, {
|
|
2797
|
+
stdin: proc.stdin,
|
|
2798
|
+
stdout: proc.stdout,
|
|
2799
|
+
mouse: termConfig.mouse,
|
|
2800
|
+
kitty: termConfig.kitty,
|
|
2801
|
+
alternateScreen: termConfig.alternateScreen,
|
|
2802
|
+
suspendOnCtrlZ: termConfig.suspendOnCtrlZ,
|
|
2803
|
+
exitOnCtrlC: termConfig.exitOnCtrlC,
|
|
2804
|
+
textSizing: termConfig.textSizing,
|
|
2805
|
+
widthDetection: termConfig.widthDetection ?? "auto",
|
|
2806
|
+
focusReporting: termConfig.focusReporting,
|
|
2807
|
+
onSuspend: termConfig.onSuspend,
|
|
2808
|
+
onResume: termConfig.onResume,
|
|
2809
|
+
onInterrupt: termConfig.onInterrupt,
|
|
2810
|
+
capabilityRegistry: registry
|
|
2811
|
+
});
|
|
2812
|
+
if (existingOptions) {
|
|
2813
|
+
const newArgs = [...args];
|
|
2814
|
+
newArgs[newArgs.length - 1] = runOptions;
|
|
2815
|
+
return originalRun.apply(app, newArgs);
|
|
2816
|
+
}
|
|
2817
|
+
return originalRun.call(app, ...args, runOptions);
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
return enhanced;
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
//#endregion
|
|
2824
|
+
//#region ../packages/create/src/internal/input-router.ts
|
|
2825
|
+
/**
|
|
2826
|
+
* Compare handler entries for dispatch order:
|
|
2827
|
+
* - Higher priority dispatches first
|
|
2828
|
+
* - Same priority: lower insertion order (first registered) wins
|
|
2829
|
+
*/
|
|
2830
|
+
function compareEntries(a, b) {
|
|
2831
|
+
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
2832
|
+
return a.order - b.order;
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Create a priority-based input router.
|
|
2836
|
+
*
|
|
2837
|
+
* The `invalidate` callback is injected by the caller (typically wired to the
|
|
2838
|
+
* store/render pipeline by withDomEvents). This keeps the router decoupled
|
|
2839
|
+
* from silvery internals.
|
|
2840
|
+
*/
|
|
2841
|
+
function createInputRouter(options) {
|
|
2842
|
+
const { invalidate } = options;
|
|
2843
|
+
let nextOrder = 0;
|
|
2844
|
+
const mouseHandlers = [];
|
|
2845
|
+
const keyHandlers = [];
|
|
2846
|
+
const overlays = [];
|
|
2847
|
+
function addEntry(list, priority, handler) {
|
|
2848
|
+
const entry = {
|
|
2849
|
+
priority,
|
|
2850
|
+
order: nextOrder++,
|
|
2851
|
+
handler
|
|
2852
|
+
};
|
|
2853
|
+
list.push(entry);
|
|
2854
|
+
list.sort(compareEntries);
|
|
2855
|
+
return () => {
|
|
2856
|
+
const idx = list.indexOf(entry);
|
|
2857
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
function dispatch(list, event) {
|
|
2861
|
+
for (const entry of list) if (entry.handler(event)) return true;
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
return {
|
|
2865
|
+
registerMouseHandler(priority, handler) {
|
|
2866
|
+
return addEntry(mouseHandlers, priority, handler);
|
|
2867
|
+
},
|
|
2868
|
+
dispatchMouse(event) {
|
|
2869
|
+
return dispatch(mouseHandlers, event);
|
|
2870
|
+
},
|
|
2871
|
+
registerKeyHandler(priority, handler) {
|
|
2872
|
+
return addEntry(keyHandlers, priority, handler);
|
|
2873
|
+
},
|
|
2874
|
+
dispatchKey(event) {
|
|
2875
|
+
return dispatch(keyHandlers, event);
|
|
2876
|
+
},
|
|
2877
|
+
invalidate,
|
|
2878
|
+
registerOverlay(priority, renderer) {
|
|
2879
|
+
return addEntry(overlays, priority, renderer);
|
|
2880
|
+
},
|
|
2881
|
+
getOverlays() {
|
|
2882
|
+
return overlays.map((entry) => entry.handler);
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region ../packages/ag-term/src/plugins/with-dom-events.ts
|
|
2888
|
+
/**
|
|
2889
|
+
* Add DOM-style mouse event dispatch to an App.
|
|
2890
|
+
*
|
|
2891
|
+
* This plugin creates a mouse event processor and ensures that
|
|
2892
|
+
* click(), doubleClick(), and wheel() methods on the app dispatch
|
|
2893
|
+
* events through the render tree with proper bubbling.
|
|
2894
|
+
*
|
|
2895
|
+
* The App's buildApp() already sets up mouse event processing.
|
|
2896
|
+
* This plugin is provided for explicit composition via pipe()
|
|
2897
|
+
* and ensures the focus manager is connected for click-to-focus.
|
|
2898
|
+
*
|
|
2899
|
+
* @param options - Configuration (focusManager for click-to-focus)
|
|
2900
|
+
* @returns Plugin function that enhances an App with DOM event dispatch
|
|
2901
|
+
*/
|
|
2902
|
+
function withDomEvents(options = {}) {
|
|
2903
|
+
return (app) => {
|
|
2904
|
+
const fm = options.focusManager ?? app.focusManager;
|
|
2905
|
+
const processorOptions = {};
|
|
2906
|
+
if (fm) processorOptions.focusManager = fm;
|
|
2907
|
+
const mouseState = createMouseEventProcessor(processorOptions);
|
|
2908
|
+
const existingRegistry = app.capabilityRegistry;
|
|
2909
|
+
const registry = options.capabilityRegistry ?? existingRegistry ?? createCapabilityRegistry();
|
|
2910
|
+
let invalidateCallback = () => {};
|
|
2911
|
+
const router = createInputRouter({ invalidate: () => invalidateCallback() });
|
|
2912
|
+
registry.register(INPUT_ROUTER, router);
|
|
2913
|
+
const enhanced = new Proxy(app, { get(target, prop, receiver) {
|
|
2914
|
+
if (prop === "capabilityRegistry") return registry;
|
|
2915
|
+
if (prop === "inputRouter") return router;
|
|
2916
|
+
if (prop === "click") return async function enhancedClick(x, y, clickOptions) {
|
|
2917
|
+
const button = clickOptions?.button ?? 0;
|
|
2918
|
+
if (!router.dispatchMouse({
|
|
2919
|
+
x,
|
|
2920
|
+
y,
|
|
2921
|
+
button,
|
|
2922
|
+
type: "mousedown"
|
|
2923
|
+
})) {
|
|
2924
|
+
const root = target.getContainer();
|
|
2925
|
+
processMouseEvent(mouseState, {
|
|
2926
|
+
button,
|
|
2927
|
+
x,
|
|
2928
|
+
y,
|
|
2929
|
+
action: "down",
|
|
2930
|
+
shift: false,
|
|
2931
|
+
meta: false,
|
|
2932
|
+
ctrl: false
|
|
2933
|
+
}, root);
|
|
2934
|
+
}
|
|
2935
|
+
if (!router.dispatchMouse({
|
|
2936
|
+
x,
|
|
2937
|
+
y,
|
|
2938
|
+
button,
|
|
2939
|
+
type: "mouseup"
|
|
2940
|
+
})) {
|
|
2941
|
+
const root = target.getContainer();
|
|
2942
|
+
processMouseEvent(mouseState, {
|
|
2943
|
+
button,
|
|
2944
|
+
x,
|
|
2945
|
+
y,
|
|
2946
|
+
action: "up",
|
|
2947
|
+
shift: false,
|
|
2948
|
+
meta: false,
|
|
2949
|
+
ctrl: false
|
|
2950
|
+
}, root);
|
|
2951
|
+
}
|
|
2952
|
+
await Promise.resolve();
|
|
2953
|
+
return receiver;
|
|
2954
|
+
};
|
|
2955
|
+
if (prop === "doubleClick") return async function enhancedDoubleClick(x, y, clickOptions) {
|
|
2956
|
+
const button = clickOptions?.button ?? 0;
|
|
2957
|
+
const root = target.getContainer();
|
|
2958
|
+
const parsed = {
|
|
2959
|
+
button,
|
|
2960
|
+
x,
|
|
2961
|
+
y,
|
|
2962
|
+
action: "down",
|
|
2963
|
+
shift: false,
|
|
2964
|
+
meta: false,
|
|
2965
|
+
ctrl: false
|
|
2966
|
+
};
|
|
2967
|
+
processMouseEvent(mouseState, parsed, root);
|
|
2968
|
+
processMouseEvent(mouseState, {
|
|
2969
|
+
...parsed,
|
|
2970
|
+
action: "up"
|
|
2971
|
+
}, root);
|
|
2972
|
+
processMouseEvent(mouseState, parsed, root);
|
|
2973
|
+
processMouseEvent(mouseState, {
|
|
2974
|
+
...parsed,
|
|
2975
|
+
action: "up"
|
|
2976
|
+
}, root);
|
|
2977
|
+
await Promise.resolve();
|
|
2978
|
+
return receiver;
|
|
2979
|
+
};
|
|
2980
|
+
if (prop === "wheel") return async function enhancedWheel(x, y, delta) {
|
|
2981
|
+
if (!router.dispatchMouse({
|
|
2982
|
+
x,
|
|
2983
|
+
y,
|
|
2984
|
+
button: 0,
|
|
2985
|
+
type: "wheel"
|
|
2986
|
+
})) {
|
|
2987
|
+
const root = target.getContainer();
|
|
2988
|
+
processMouseEvent(mouseState, {
|
|
2989
|
+
button: 0,
|
|
2990
|
+
x,
|
|
2991
|
+
y,
|
|
2992
|
+
action: "wheel",
|
|
2993
|
+
delta,
|
|
2994
|
+
shift: false,
|
|
2995
|
+
meta: false,
|
|
2996
|
+
ctrl: false
|
|
2997
|
+
}, root);
|
|
2998
|
+
}
|
|
2999
|
+
await Promise.resolve();
|
|
3000
|
+
return receiver;
|
|
3001
|
+
};
|
|
3002
|
+
if (prop === "run") {
|
|
3003
|
+
const originalRun = Reflect.get(target, prop, receiver);
|
|
3004
|
+
if (typeof originalRun === "function") return function enhancedRun(...args) {
|
|
3005
|
+
const inject = { capabilityRegistry: registry };
|
|
3006
|
+
if (args.length === 0) return originalRun.call(target, inject);
|
|
3007
|
+
else if (args.length === 1) {
|
|
3008
|
+
const arg = args[0];
|
|
3009
|
+
if (arg && typeof arg === "object" && "type" in arg) return originalRun.call(target, arg, inject);
|
|
3010
|
+
else return originalRun.call(target, {
|
|
3011
|
+
...arg,
|
|
3012
|
+
...inject
|
|
3013
|
+
});
|
|
3014
|
+
} else {
|
|
3015
|
+
const opts = {
|
|
3016
|
+
...args[1],
|
|
3017
|
+
...inject
|
|
3018
|
+
};
|
|
3019
|
+
return originalRun.call(target, args[0], opts);
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
return Reflect.get(target, prop, receiver);
|
|
3024
|
+
} });
|
|
3025
|
+
const appAny = app;
|
|
3026
|
+
if (typeof appAny.store?.setState === "function") invalidateCallback = () => {
|
|
3027
|
+
appAny.store.setState((prev) => ({
|
|
3028
|
+
...prev,
|
|
3029
|
+
_inv: (prev._inv ?? 0) + 1
|
|
3030
|
+
}));
|
|
3031
|
+
};
|
|
3032
|
+
return enhanced;
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
//#endregion
|
|
3036
|
+
//#region ../packages/test/src/compare-buffers.ts
|
|
3037
|
+
init_buffer();
|
|
3038
|
+
//#endregion
|
|
3039
|
+
//#region ../packages/ag-term/src/plugins/with-diagnostics.ts
|
|
3040
|
+
init_buffer();
|
|
3041
|
+
createContext(null);
|
|
3042
|
+
createContext({
|
|
3043
|
+
activeId: void 0,
|
|
3044
|
+
isFocusEnabled: true,
|
|
3045
|
+
add() {},
|
|
3046
|
+
remove() {},
|
|
3047
|
+
activate() {},
|
|
3048
|
+
deactivate() {},
|
|
3049
|
+
enableFocus() {},
|
|
3050
|
+
disableFocus() {},
|
|
3051
|
+
focusNext() {},
|
|
3052
|
+
focusPrevious() {},
|
|
3053
|
+
focus() {},
|
|
3054
|
+
blur() {}
|
|
3055
|
+
});
|
|
3056
|
+
//#endregion
|
|
3057
|
+
export { createApp as a, pipe as i, withTerminal as n, useApp as o, withReact as r, withDomEvents as t };
|