@valyrianjs/terminal 0.1.0 → 0.1.2

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.
Files changed (107) hide show
  1. package/README.md +105 -55
  2. package/dist/ansi.d.ts +20 -4
  3. package/dist/ansi.d.ts.map +1 -1
  4. package/dist/ansi.js +171 -47
  5. package/dist/ansi.js.map +1 -1
  6. package/dist/editor-state.d.ts +22 -0
  7. package/dist/editor-state.d.ts.map +1 -0
  8. package/dist/editor-state.js +110 -0
  9. package/dist/editor-state.js.map +1 -0
  10. package/dist/events.d.ts +1 -4
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/events.js +15 -38
  13. package/dist/events.js.map +1 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/keymap.d.ts +7 -0
  19. package/dist/keymap.d.ts.map +1 -0
  20. package/dist/keymap.js +133 -0
  21. package/dist/keymap.js.map +1 -0
  22. package/dist/layout.d.ts +10 -1
  23. package/dist/layout.d.ts.map +1 -1
  24. package/dist/layout.js +97 -7
  25. package/dist/layout.js.map +1 -1
  26. package/dist/mouse.d.ts +1 -0
  27. package/dist/mouse.d.ts.map +1 -1
  28. package/dist/mouse.js +24 -1
  29. package/dist/mouse.js.map +1 -1
  30. package/dist/output-writer.d.ts +9 -0
  31. package/dist/output-writer.d.ts.map +1 -0
  32. package/dist/output-writer.js +79 -0
  33. package/dist/output-writer.js.map +1 -0
  34. package/dist/paste.d.ts +7 -0
  35. package/dist/paste.d.ts.map +1 -0
  36. package/dist/paste.js +18 -0
  37. package/dist/paste.js.map +1 -0
  38. package/dist/primitives.d.ts +8 -1
  39. package/dist/primitives.d.ts.map +1 -1
  40. package/dist/primitives.js +9 -1
  41. package/dist/primitives.js.map +1 -1
  42. package/dist/render.d.ts +8 -3
  43. package/dist/render.d.ts.map +1 -1
  44. package/dist/render.js +840 -67
  45. package/dist/render.js.map +1 -1
  46. package/dist/runtime.d.ts +29 -0
  47. package/dist/runtime.d.ts.map +1 -0
  48. package/dist/runtime.js +215 -0
  49. package/dist/runtime.js.map +1 -0
  50. package/dist/scheduler.d.ts +8 -0
  51. package/dist/scheduler.d.ts.map +1 -0
  52. package/dist/scheduler.js +24 -0
  53. package/dist/scheduler.js.map +1 -0
  54. package/dist/session.d.ts.map +1 -1
  55. package/dist/session.js +729 -199
  56. package/dist/session.js.map +1 -1
  57. package/dist/stream-log.d.ts +40 -0
  58. package/dist/stream-log.d.ts.map +1 -0
  59. package/dist/stream-log.js +73 -0
  60. package/dist/stream-log.js.map +1 -0
  61. package/dist/text.d.ts +3 -0
  62. package/dist/text.d.ts.map +1 -0
  63. package/dist/text.js +19 -0
  64. package/dist/text.js.map +1 -0
  65. package/dist/theme.d.ts +7 -0
  66. package/dist/theme.d.ts.map +1 -0
  67. package/dist/theme.js +254 -0
  68. package/dist/theme.js.map +1 -0
  69. package/dist/tree.d.ts +2 -0
  70. package/dist/tree.d.ts.map +1 -1
  71. package/dist/tree.js +42 -1
  72. package/dist/tree.js.map +1 -1
  73. package/dist/types.d.ts +183 -18
  74. package/dist/types.d.ts.map +1 -1
  75. package/docs/api-reference.md +302 -136
  76. package/docs/assets/quick-note.svg +13 -0
  77. package/docs/cookbook.md +297 -202
  78. package/docs/core-concepts.md +143 -55
  79. package/docs/getting-started.md +209 -90
  80. package/docs/interaction-model.md +95 -61
  81. package/docs/primitive-gallery.md +365 -0
  82. package/docs/session-runtime.md +132 -363
  83. package/docs/valyrian-modules.md +3196 -0
  84. package/llms-full.txt +5357 -0
  85. package/package.json +21 -8
  86. package/src/ansi.ts +269 -0
  87. package/src/clipboard.ts +76 -0
  88. package/src/editor-state.ts +162 -0
  89. package/src/events.ts +163 -0
  90. package/src/index.ts +92 -0
  91. package/src/keymap.ts +151 -0
  92. package/src/layout.ts +282 -0
  93. package/src/mouse.ts +68 -0
  94. package/src/output-writer.ts +93 -0
  95. package/src/paste.ts +23 -0
  96. package/src/primitives.ts +52 -0
  97. package/src/render.ts +1107 -0
  98. package/src/runtime.ts +273 -0
  99. package/src/scheduler.ts +33 -0
  100. package/src/session.ts +1260 -0
  101. package/src/stream-log.ts +96 -0
  102. package/src/text.ts +20 -0
  103. package/src/theme.ts +263 -0
  104. package/src/tree.ts +169 -0
  105. package/src/types.ts +523 -0
  106. package/tsconfig.json +4 -7
  107. package/docs/local-demo.md +0 -28
package/src/session.ts ADDED
@@ -0,0 +1,1260 @@
1
+ import { ANSI_ENTER_ALTERNATE_SCREEN, ANSI_EXIT_ALTERNATE_SCREEN, ANSI_HIDE_CURSOR, ANSI_SHOW_CURSOR, createAnsiDiffWriter, formatPlainFrame, toAnsiFrame } from "./ansi.js";
2
+ import { createSystemClipboardAdapter } from "./clipboard.js";
3
+ import { createEditorState, insertEditorText, moveEditorCursor, removeEditorBackward, removeEditorForward } from "./editor-state.js";
4
+ import { copySelection, hasSelection, insertText, moveCursorEnd, moveCursorHome, moveCursorLeft, moveCursorRight, moveCursorWordLeft, moveCursorWordRight, normalizeInputState, parseTerminalKey, removeBackward, removeForward, selectAll } from "./events.js";
5
+ import { createResolvedTerminalKeymap, resolveTerminalKeyBinding } from "./keymap.js";
6
+ import { mergeVertical } from "./layout.js";
7
+ import { cursorFromHitbox, parseTerminalInput, resolvePointerTarget } from "./mouse.js";
8
+ import { createOutputWriter } from "./output-writer.js";
9
+ import { parseBracketedPaste } from "./paste.js";
10
+ import { renderTerminalFrame } from "./render.js";
11
+ import { createRenderScheduler } from "./scheduler.js";
12
+ import { plainText, stripTerminalControls } from "./text.js";
13
+ import { collectActiveFocusScopeFocusableNodes, collectDirectOverlayFocusableNodes, collectFocusableNodes, findFocusableById, findFocused } from "./tree.js";
14
+ import { createValyrianTerminalRuntime } from "./runtime.js";
15
+
16
+ import type { TerminalRenderContext } from "./render.js";
17
+
18
+ import type { EditorState } from "./editor-state.js";
19
+
20
+ import type {
21
+ InputInteractionState,
22
+ TerminalCommand,
23
+ TerminalCommandContext,
24
+ TerminalCaptureEventPayload,
25
+ TerminalClipboardAdapter,
26
+ TerminalFocusNode,
27
+ TerminalListChangeEventPayload,
28
+ TerminalListPressEventPayload,
29
+ TerminalMountOptions,
30
+ TerminalOutputStream,
31
+ TerminalNode,
32
+ TerminalPointerSource,
33
+ TerminalRowPointerEventPayload,
34
+ TerminalSession,
35
+ TerminalSize
36
+ } from "./types.js";
37
+
38
+ type TerminalInputStream = NonNullable<TerminalMountOptions["stdin"]>;
39
+
40
+ interface ResolvedRuntimeOptions {
41
+ runtime: "app" | "headless";
42
+ stdin?: TerminalInputStream;
43
+ stdout?: TerminalOutputStream;
44
+ alternateScreen: boolean;
45
+ hideCursor: boolean;
46
+ writesAnsi: boolean;
47
+ }
48
+
49
+ const DEFAULT_TERMINAL_SIZE: TerminalSize = { cols: 80, rows: 24 };
50
+ const BRACKETED_PASTE_START = "\u001b[200~";
51
+ const KNOWN_TERMINAL_KEY_SEQUENCES = [
52
+ "\u001b[13;2u",
53
+ "\u001b[13;130u",
54
+ "\u001b[13;129u",
55
+ "\u001b[27;2;13~",
56
+ "\u001b[13;2~",
57
+ "\u001b[1;2C",
58
+ "\u001b[1;2D",
59
+ "\u001b[1;3C",
60
+ "\u001b[1;3D",
61
+ "\u001b[3~",
62
+ "\u001b[Z",
63
+ "\u001b[A",
64
+ "\u001b[B",
65
+ "\u001b[C",
66
+ "\u001b[D",
67
+ "\u001b[H",
68
+ "\u001b[F",
69
+ "\u001bf",
70
+ "\u001bb"
71
+ ];
72
+ const ESCAPE = "\u001b";
73
+ const CSI_PREFIX = "\u001b[";
74
+
75
+ function isBracketedPasteStartPrefix(value: string) {
76
+ return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
77
+ }
78
+
79
+ function isDisambiguatedBracketedPasteStartPrefix(value: string) {
80
+ return isBracketedPasteStartPrefix(value) && value.length > CSI_PREFIX.length;
81
+ }
82
+
83
+ function isKnownTerminalKeySequencePrefix(value: string) {
84
+ return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
85
+ }
86
+
87
+ function canContinueEscapeSequence(value: string) {
88
+ return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
89
+ }
90
+
91
+ function validateTerminalDimension(name: "cols" | "rows", value: number) {
92
+ if (!Number.isInteger(value) || value < 1) {
93
+ throw new Error(`Invalid terminal ${name}: expected an integer >= 1`);
94
+ }
95
+ return value;
96
+ }
97
+
98
+ function isValidTerminalDimension(value: number | undefined): value is number {
99
+ return Number.isInteger(value) && Number(value) >= 1;
100
+ }
101
+
102
+
103
+ function getProcessStdin(): TerminalInputStream | undefined {
104
+ const candidate = globalThis.process?.stdin as TerminalInputStream | undefined;
105
+ return candidate && typeof candidate.on === "function" ? candidate : undefined;
106
+ }
107
+
108
+ function getProcessStdout(): TerminalOutputStream | undefined {
109
+ const candidate = globalThis.process?.stdout as TerminalOutputStream | undefined;
110
+ return candidate && typeof candidate.write === "function" ? candidate : undefined;
111
+ }
112
+
113
+ function isInteractiveTTY(stdin: TerminalInputStream | undefined, stdout: TerminalOutputStream | undefined) {
114
+ return Boolean(stdin?.isTTY && stdout?.isTTY);
115
+ }
116
+
117
+ function resolveRuntimeOptions(options: TerminalMountOptions): ResolvedRuntimeOptions {
118
+ const processStdin = getProcessStdin();
119
+ const processStdout = getProcessStdout();
120
+ const canUseProcessTTY = isInteractiveTTY(processStdin, processStdout);
121
+ const runtime = options.runtime ?? (canUseProcessTTY || options.stdin || options.stdout ? "app" : "headless");
122
+ const stdin = options.stdin ?? (runtime === "app" && canUseProcessTTY ? processStdin : undefined);
123
+ const stdout = options.stdout ?? (runtime === "app" && canUseProcessTTY ? processStdout : undefined);
124
+ const ownsInteractiveTTY = runtime === "app" && isInteractiveTTY(stdin, stdout);
125
+
126
+ return {
127
+ runtime,
128
+ stdin,
129
+ stdout,
130
+ alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
131
+ hideCursor: options.hideCursor ?? ownsInteractiveTTY,
132
+ writesAnsi: runtime === "app" && Boolean(stdout)
133
+ };
134
+ }
135
+
136
+ function resolveTerminalSize(options: TerminalMountOptions, stdout?: TerminalOutputStream): TerminalSize {
137
+ const cols = options.cols ?? stdout?.columns ?? DEFAULT_TERMINAL_SIZE.cols;
138
+ const rows = options.rows ?? stdout?.rows ?? DEFAULT_TERMINAL_SIZE.rows;
139
+ return {
140
+ cols: validateTerminalDimension("cols", cols),
141
+ rows: validateTerminalDimension("rows", rows)
142
+ };
143
+ }
144
+
145
+ function applyInteractiveState(
146
+ nodes: TerminalNode[],
147
+ focusedId: string | null,
148
+ inputStateById: Map<string, InputInteractionState>,
149
+ editorStateById: Map<string, EditorState>,
150
+ listIndexById: Map<string, number>,
151
+ scrollOffsetById: Map<string, number>,
152
+ listHoverById: Map<string, number>,
153
+ scrollHoverRowById: Map<string, number>
154
+ ) {
155
+ for (let i = 0; i < nodes.length; i += 1) {
156
+ const node = nodes[i];
157
+ if (node.type !== "element") {
158
+ continue;
159
+ }
160
+
161
+ const id = node.props.id;
162
+ node.props.__focused = Boolean(focusedId && id === focusedId);
163
+ if (node.tag === "terminal-input" && id) {
164
+ const value = plainText(node.props.value ?? "");
165
+ const current = normalizeInputState(inputStateById.get(id), value.length);
166
+ inputStateById.set(id, current);
167
+ node.props.__inputState = current;
168
+ }
169
+ if (node.tag === "terminal-editor" && id) {
170
+ const value = plainText(node.props.value ?? "");
171
+ const previous = editorStateById.get(id);
172
+ const current = createEditorState(value, previous?.cursor, previous?.desiredColumn);
173
+ editorStateById.set(id, current);
174
+ node.props.__editorState = current;
175
+ }
176
+ if (node.tag === "terminal-list" && id) {
177
+ node.props.__selectedIndex = listIndexById.get(id) || 0;
178
+ if (listHoverById.has(id)) {
179
+ node.props.__hoveredIndex = listHoverById.get(id);
180
+ }
181
+ }
182
+ if (node.tag === "terminal-scroll" && id) {
183
+ node.props.__scrollOffset = scrollOffsetById.get(id) || 0;
184
+ if (scrollHoverRowById.has(id)) {
185
+ node.props.__hoveredRow = scrollHoverRowById.get(id);
186
+ }
187
+ }
188
+ applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
189
+ }
190
+ }
191
+
192
+ export function mountTerminal(input: any, options: TerminalMountOptions = {}): TerminalSession {
193
+ let focusedId: string | null = null;
194
+ let skipFocusContainmentOnce = false;
195
+ let clipboardValue = "";
196
+ let mouseSelectionId: string | null = null;
197
+ let pointerCaptureId: string | null = null;
198
+ let pendingPasteChunk = "";
199
+ let pendingKeyChunk = "";
200
+ let pendingEscapeFlush: ReturnType<typeof setTimeout> | null = null;
201
+ let destroyed = false;
202
+ let autoProjectionEnabled = false;
203
+ let suppressAutoProjection = false;
204
+ const clipboardAdapter: TerminalClipboardAdapter | null = options.clipboard === false ? null : options.clipboard || createSystemClipboardAdapter();
205
+ const inputStateById = new Map<string, InputInteractionState>();
206
+ const editorStateById = new Map<string, EditorState>();
207
+ const listIndexById = new Map<string, number>();
208
+ const scrollOffsetById = new Map<string, number>();
209
+ const listHoverById = new Map<string, number>();
210
+ const scrollHoverRowById = new Map<string, number>();
211
+ const keyBindings = createResolvedTerminalKeymap(options.keymap?.bindings);
212
+ const runtimeOptions = resolveRuntimeOptions(options);
213
+ let terminalSize = resolveTerminalSize(options, runtimeOptions.stdout);
214
+ const terminalRuntime = createValyrianTerminalRuntime(input, () => {
215
+ if (!autoProjectionEnabled || suppressAutoProjection || destroyed) {
216
+ return;
217
+ }
218
+ renderNow();
219
+ });
220
+ let currentTree = terminalRuntime.project();
221
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
222
+ let currentFrame = renderTreeFrame(currentTree);
223
+ let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
224
+ let currentHitboxes = currentFrame.hitboxes;
225
+
226
+ const outputWriter = createOutputWriter(runtimeOptions.stdout);
227
+ const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
228
+ const renderScheduler = createRenderScheduler(renderNow);
229
+ const outputResizeScheduler = createRenderScheduler(applyPendingOutputResize);
230
+ let cleanupOutputResize: (() => void) | null = null;
231
+ let pendingOutputResize: TerminalSize | null = null;
232
+
233
+ function renderContext(): TerminalRenderContext {
234
+ return { ...terminalSize, theme: options.theme };
235
+ }
236
+
237
+ function renderTreeFrame(nodes: TerminalNode[]) {
238
+ const context = renderContext();
239
+ return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
240
+ }
241
+
242
+ function emitLifecycleSetup() {
243
+ const writes: string[] = [];
244
+ if (runtimeOptions.alternateScreen) {
245
+ writes.push(ANSI_ENTER_ALTERNATE_SCREEN);
246
+ }
247
+ if (runtimeOptions.hideCursor) {
248
+ writes.push(ANSI_HIDE_CURSOR);
249
+ }
250
+ if (writes.length > 0) {
251
+ outputWriter.write(writes.join(""), { force: true });
252
+ }
253
+ }
254
+
255
+ function emitLifecycleRestore() {
256
+ if (options.restoreOnDestroy === false) {
257
+ return;
258
+ }
259
+ const writes: string[] = [];
260
+ if (runtimeOptions.hideCursor) {
261
+ writes.push(ANSI_SHOW_CURSOR);
262
+ }
263
+ if (runtimeOptions.alternateScreen) {
264
+ writes.push(ANSI_EXIT_ALTERNATE_SCREEN);
265
+ }
266
+ if (writes.length > 0) {
267
+ outputWriter.write(writes.join(""), { force: true });
268
+ }
269
+ }
270
+
271
+ function emitOutput() {
272
+ if (!destroyed) {
273
+ outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
274
+ }
275
+ }
276
+
277
+ function renderNow() {
278
+ currentTree = terminalRuntime.project();
279
+ const focusables: TerminalFocusNode[] = [];
280
+ collectFocusableNodes(currentTree, focusables);
281
+ const overlayFocusables: TerminalFocusNode[] = [];
282
+ collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
283
+ const scopedFocusables: TerminalFocusNode[] = [];
284
+ collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
285
+ const activeFocusables = overlayFocusables.length ? overlayFocusables : scopedFocusables.length ? scopedFocusables : focusables;
286
+ if (!skipFocusContainmentOnce && focusedId && !activeFocusables.some((node) => node.props.id === focusedId)) {
287
+ focusedId = activeFocusables[0]?.props.id || null;
288
+ }
289
+ skipFocusContainmentOnce = false;
290
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
291
+ currentFrame = renderTreeFrame(currentTree);
292
+ currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
293
+ currentHitboxes = currentFrame.hitboxes;
294
+ emitOutput();
295
+ return currentOutput;
296
+ }
297
+
298
+ function rerender() {
299
+ if (destroyed) {
300
+ return currentOutput;
301
+ }
302
+ suppressAutoProjection = true;
303
+ try {
304
+ terminalRuntime.update();
305
+ } finally {
306
+ suppressAutoProjection = false;
307
+ }
308
+ renderScheduler.requestRender();
309
+ renderScheduler.flush();
310
+ return currentOutput;
311
+ }
312
+
313
+ function orderedFocusables() {
314
+ const overlayFocusables: TerminalFocusNode[] = [];
315
+ collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
316
+ if (overlayFocusables.length) {
317
+ return overlayFocusables;
318
+ }
319
+
320
+ const scopedFocusables: TerminalFocusNode[] = [];
321
+ collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
322
+ if (scopedFocusables.length) {
323
+ return scopedFocusables;
324
+ }
325
+
326
+ const out: TerminalFocusNode[] = [];
327
+ collectFocusableNodes(currentTree, out);
328
+ return out;
329
+ }
330
+
331
+ function readClipboard() {
332
+ if (!clipboardAdapter || typeof clipboardAdapter.readText !== "function") {
333
+ return clipboardValue;
334
+ }
335
+ try {
336
+ const nextValue = clipboardAdapter.readText();
337
+ if (typeof nextValue === "string") {
338
+ clipboardValue = nextValue;
339
+ }
340
+ } catch {
341
+ return clipboardValue;
342
+ }
343
+ return clipboardValue;
344
+ }
345
+
346
+ function writeClipboard(value: string) {
347
+ clipboardValue = String(value);
348
+ if (!clipboardAdapter || typeof clipboardAdapter.writeText !== "function") {
349
+ return clipboardValue;
350
+ }
351
+ try {
352
+ clipboardAdapter.writeText(clipboardValue);
353
+ } catch {
354
+ return clipboardValue;
355
+ }
356
+ return clipboardValue;
357
+ }
358
+
359
+ function setInputCursor(id: string, cursor: number, extendSelection = false) {
360
+ const node = findFocusableById(currentTree, id);
361
+ const value = stripTerminalControls(node?.props.value ?? "");
362
+ const current = normalizeInputState(inputStateById.get(id), value.length);
363
+ inputStateById.set(id, extendSelection ? { cursor: Math.max(0, Math.min(value.length, cursor)), anchor: current.anchor } : { cursor: Math.max(0, Math.min(value.length, cursor)), anchor: Math.max(0, Math.min(value.length, cursor)) });
364
+ }
365
+
366
+ function replaceInputState(id: string, state: InputInteractionState) {
367
+ const node = findFocusableById(currentTree, id);
368
+ const value = stripTerminalControls(node?.props.value ?? "");
369
+ inputStateById.set(id, normalizeInputState(state, value.length));
370
+ }
371
+
372
+ function dispatchNodeEvent(node: TerminalFocusNode, type: string, payload: Record<string, any>) {
373
+ const id = String(node.props.id || "");
374
+ if (!id) {
375
+ return false;
376
+ }
377
+ return terminalRuntime.dispatchTerminalEvent(id, type, payload);
378
+ }
379
+
380
+ function updateInputByState(node: TerminalFocusNode, next: { value: string; state: InputInteractionState }) {
381
+ const id = String(node.props.id || "");
382
+ dispatchNodeEvent(node, "change", { type: "change", id, value: next.value });
383
+ inputStateById.set(id, next.state);
384
+ return rerender();
385
+ }
386
+
387
+ function replaceEditorState(id: string, state: EditorState) {
388
+ const node = findFocusableById(currentTree, id);
389
+ const value = plainText(node?.props.value ?? "");
390
+ editorStateById.set(id, createEditorState(value, state.cursor, state.desiredColumn));
391
+ }
392
+
393
+ function updateEditorValue(node: TerminalFocusNode, nextValue: string) {
394
+ const id = String(node.props.id || "");
395
+ dispatchNodeEvent(node, "change", { type: "change", id, value: plainText(nextValue) });
396
+ }
397
+
398
+ function updateEditorByState(node: TerminalFocusNode, next: { value: string; state: EditorState }) {
399
+ const id = String(node.props.id || "");
400
+ updateEditorValue(node, next.value);
401
+ editorStateById.set(id, next.state);
402
+ return rerender();
403
+ }
404
+
405
+ function submitEditor(node: TerminalFocusNode) {
406
+ const id = String(node.props.id || "");
407
+ dispatchNodeEvent(node, "submit", { type: "submit", id, value: plainText(node.props.value ?? "") });
408
+ }
409
+
410
+ function cancelEditor(node: TerminalFocusNode) {
411
+ const id = String(node.props.id || "");
412
+ dispatchNodeEvent(node, "cancel", { type: "cancel", id, value: plainText(node.props.value ?? "") });
413
+ }
414
+
415
+ function setCursorFromHitbox(id: string, x: number, extendSelection = false) {
416
+ const hitbox = currentHitboxes.find((box) => box.id === id && box.tag === "terminal-input");
417
+ if (!hitbox) {
418
+ return currentOutput;
419
+ }
420
+ const nextCursor = cursorFromHitbox(hitbox, x);
421
+ setInputCursor(id, nextCursor, extendSelection);
422
+ return rerender();
423
+ }
424
+
425
+ function visibleScrollLines(node: TerminalFocusNode) {
426
+ const context = renderContext();
427
+ const lines = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context))).lines;
428
+ const offset = Number(node.props.__scrollOffset || 0);
429
+ const height = Number(node.props.height || lines.length || 0);
430
+ return lines.slice(offset, offset + height).map((line) => line.trimEnd());
431
+ }
432
+
433
+ function rowCountForNode(node: TerminalFocusNode) {
434
+ if (node.tag === "terminal-list") {
435
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
436
+ return Math.max(1, items.length);
437
+ }
438
+ if (node.tag === "terminal-scroll") {
439
+ return Math.max(1, visibleScrollLines(node).length);
440
+ }
441
+ return 1;
442
+ }
443
+
444
+ function sourceRowFromHitbox(node: TerminalFocusNode, hitbox: { itemOffset?: number; y1: number }, y: number) {
445
+ const visibleRow = Math.max(1, y - hitbox.y1 + 1);
446
+ if (node.tag !== "terminal-list") {
447
+ return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
448
+ }
449
+
450
+ return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
451
+ }
452
+
453
+ function shouldPointerCapture(node: TerminalFocusNode) {
454
+ return Boolean(node.props.pointerCapture && (node.tag === "terminal-list" || node.tag === "terminal-scroll"));
455
+ }
456
+
457
+ function hoveredRowForNode(node: TerminalFocusNode | null) {
458
+ if (!node?.props.id) {
459
+ return null;
460
+ }
461
+ if (node.tag === "terminal-list") {
462
+ const hoveredIndex = listHoverById.get(node.props.id);
463
+ return typeof hoveredIndex === "number" ? hoveredIndex + 1 : null;
464
+ }
465
+ if (node.tag === "terminal-scroll") {
466
+ const hoveredRow = scrollHoverRowById.get(node.props.id);
467
+ return typeof hoveredRow === "number" ? hoveredRow : null;
468
+ }
469
+ return null;
470
+ }
471
+
472
+ function emitCaptureEvent(node: TerminalFocusNode | null, type: TerminalCaptureEventPayload["type"], source: TerminalPointerSource, row: number | null = null, x: number | null = null, y: number | null = null) {
473
+ if (!node || !node.props.id) {
474
+ return;
475
+ }
476
+ const payload: TerminalCaptureEventPayload = { type, id: node.props.id, source, row, x, y };
477
+ dispatchNodeEvent(node, type, payload);
478
+ }
479
+
480
+ function setPointerCapture(id: string | null, source: TerminalPointerSource, row: number | null = null, x: number | null = null, y: number | null = null) {
481
+ if (pointerCaptureId === id) {
482
+ return;
483
+ }
484
+ const previous = pointerCaptureId ? findFocusableById(currentTree, pointerCaptureId) : null;
485
+ pointerCaptureId = id;
486
+ const next = id ? findFocusableById(currentTree, id) : null;
487
+ if (previous && previous.props.id !== next?.props.id) {
488
+ emitCaptureEvent(previous, "captureend", source, row ?? hoveredRowForNode(previous), x, y);
489
+ }
490
+ if (next && previous?.props.id !== next.props.id) {
491
+ emitCaptureEvent(next, "capturestart", source, row ?? hoveredRowForNode(next), x, y);
492
+ }
493
+ }
494
+
495
+ function emitMouseRowEvent(node: TerminalFocusNode, type: TerminalRowPointerEventPayload["type"], row: number, x: number | null = null, y: number | null = null) {
496
+ if (!node.props.id) {
497
+ return;
498
+ }
499
+
500
+ if (node.tag === "terminal-list") {
501
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
502
+ const index = Math.max(0, Math.min(items.length - 1, row - 1));
503
+ if (typeof items[index] === "undefined") {
504
+ return;
505
+ }
506
+ const payload: TerminalRowPointerEventPayload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
507
+ dispatchNodeEvent(node, type, payload);
508
+ return;
509
+ }
510
+
511
+ if (node.tag === "terminal-scroll") {
512
+ const lines = visibleScrollLines(node);
513
+ const index = Math.max(0, Math.min(lines.length - 1, row - 1));
514
+ if (typeof lines[index] === "undefined") {
515
+ return;
516
+ }
517
+ const payload: TerminalRowPointerEventPayload = { type, id: node.props.id, row: index + 1, value: lines[index], x, y };
518
+ dispatchNodeEvent(node, type, payload);
519
+ }
520
+ }
521
+
522
+ function clearSemanticHover(exceptId?: string, x: number | null = null, y: number | null = null) {
523
+ for (const [id, hoveredIndex] of listHoverById.entries()) {
524
+ if (id === exceptId) {
525
+ continue;
526
+ }
527
+ const node = findFocusableById(currentTree, id);
528
+ if (node) {
529
+ emitMouseRowEvent(node, "rowleave", hoveredIndex + 1, x, y);
530
+ }
531
+ listHoverById.delete(id);
532
+ }
533
+
534
+ for (const [id, hoveredRow] of scrollHoverRowById.entries()) {
535
+ if (id === exceptId) {
536
+ continue;
537
+ }
538
+ const node = findFocusableById(currentTree, id);
539
+ if (node) {
540
+ emitMouseRowEvent(node, "rowleave", hoveredRow, x, y);
541
+ }
542
+ scrollHoverRowById.delete(id);
543
+ }
544
+ }
545
+
546
+ function setSemanticHoverFromHitbox(id: string, x: number | null, y: number) {
547
+ const hitbox = currentHitboxes.find((box) => box.id === id);
548
+ const node = findFocusableById(currentTree, id);
549
+ if (!hitbox || !node) {
550
+ clearSemanticHover(undefined, x, y);
551
+ return;
552
+ }
553
+
554
+ clearSemanticHover(id, x, y);
555
+
556
+ const row = sourceRowFromHitbox(node, hitbox, y);
557
+ if (node.tag === "terminal-list") {
558
+ const nextIndex = row - 1;
559
+ const prevIndex = listHoverById.get(id);
560
+ if (typeof prevIndex === "number" && prevIndex !== nextIndex) {
561
+ emitMouseRowEvent(node, "rowleave", prevIndex + 1, x, y);
562
+ }
563
+ if (prevIndex !== nextIndex) {
564
+ listHoverById.set(id, nextIndex);
565
+ emitMouseRowEvent(node, "rowenter", row, x, y);
566
+ }
567
+ emitMouseRowEvent(node, "hover", row, x, y);
568
+ }
569
+ if (node.tag === "terminal-scroll") {
570
+ const prevRow = scrollHoverRowById.get(id);
571
+ if (typeof prevRow === "number" && prevRow !== row) {
572
+ emitMouseRowEvent(node, "rowleave", prevRow, x, y);
573
+ }
574
+ if (prevRow !== row) {
575
+ scrollHoverRowById.set(id, row);
576
+ emitMouseRowEvent(node, "rowenter", row, x, y);
577
+ }
578
+ emitMouseRowEvent(node, "hover", row, x, y);
579
+ return;
580
+ }
581
+
582
+ if (node.tag !== "terminal-list") {
583
+ clearSemanticHover(undefined, x, y);
584
+ }
585
+ }
586
+
587
+ function hoverAt(x: number, y: number) {
588
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
589
+ if (!hitbox) {
590
+ if (pointerCaptureId) {
591
+ setSemanticHoverFromHitbox(pointerCaptureId, x, y);
592
+ return rerender();
593
+ }
594
+ clearSemanticHover(undefined, x, y);
595
+ return rerender();
596
+ }
597
+ const node = findFocusableById(currentTree, hitbox.id);
598
+ if (node && shouldPointerCapture(node)) {
599
+ setPointerCapture(hitbox.id, "drag", sourceRowFromHitbox(node, hitbox, y), x, y);
600
+ }
601
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
602
+ return rerender();
603
+ }
604
+
605
+ function changeListSelection(node: TerminalFocusNode, direction: -1 | 1) {
606
+ const id = node.props.id;
607
+ if (!id) {
608
+ return currentOutput;
609
+ }
610
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
611
+ const currentIndex = listIndexById.get(id) || 0;
612
+ const nextIndex = direction < 0 ? Math.max(0, currentIndex - 1) : Math.min(items.length - 1, currentIndex + 1);
613
+ listIndexById.set(id, nextIndex);
614
+ const payload: TerminalListChangeEventPayload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
615
+ dispatchNodeEvent(node, "change", payload);
616
+ return rerender();
617
+ }
618
+
619
+ function pressListSelection(node: TerminalFocusNode) {
620
+ const id = node.props.id;
621
+ if (!id) {
622
+ return currentOutput;
623
+ }
624
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
625
+ const currentIndex = listIndexById.get(id) || 0;
626
+ const payload: TerminalListPressEventPayload = { type: "press", id, index: currentIndex, value: items[currentIndex] };
627
+ dispatchNodeEvent(node, "press", payload);
628
+ return rerender();
629
+ }
630
+
631
+ function scrollFocusedNode(node: TerminalFocusNode, direction: -1 | 1) {
632
+ const id = node.props.id;
633
+ if (!id) {
634
+ return currentOutput;
635
+ }
636
+ const context = renderContext();
637
+ const rendered = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
638
+ const height = Number(node.props.height || rendered.lines.length || 0);
639
+ const currentOffset = scrollOffsetById.get(id) || 0;
640
+ const maxOffset = Math.max(0, rendered.lines.length - height);
641
+ scrollOffsetById.set(id, direction < 0 ? Math.max(0, currentOffset - 1) : Math.min(maxOffset, currentOffset + 1));
642
+ return rerender();
643
+ }
644
+
645
+ function wheelAt(x: number, y: number, direction: -1 | 1) {
646
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
647
+ if (!hitbox) {
648
+ return currentOutput;
649
+ }
650
+
651
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
652
+ const node = findFocusableById(currentTree, hitbox.id);
653
+ if (node?.tag === "terminal-scroll") {
654
+ return scrollFocusedNode(node, direction);
655
+ }
656
+ if (node?.tag === "terminal-list" && node.props.virtualized) {
657
+ return changeListSelection(node, direction);
658
+ }
659
+ return rerender();
660
+ }
661
+
662
+ function runInputCommand(node: TerminalFocusNode, command: TerminalCommand) {
663
+ const id = node.props.id;
664
+ if (!id) {
665
+ return currentOutput;
666
+ }
667
+
668
+ const currentValue = stripTerminalControls(node.props.value ?? "");
669
+ const state = normalizeInputState(inputStateById.get(id), currentValue.length);
670
+
671
+ switch (command.id) {
672
+ case "input.submit":
673
+ dispatchNodeEvent(node, "submit", { type: "submit", id, value: stripTerminalControls(node.props.value ?? "") });
674
+ return rerender();
675
+ case "input.cursorLeft":
676
+ replaceInputState(id, moveCursorLeft(state, currentValue.length));
677
+ return rerender();
678
+ case "input.cursorRight":
679
+ replaceInputState(id, moveCursorRight(state, currentValue.length));
680
+ return rerender();
681
+ case "input.selectLeft":
682
+ replaceInputState(id, moveCursorLeft(state, currentValue.length, true));
683
+ return rerender();
684
+ case "input.selectRight":
685
+ replaceInputState(id, moveCursorRight(state, currentValue.length, true));
686
+ return rerender();
687
+ case "input.wordLeft":
688
+ replaceInputState(id, moveCursorWordLeft(currentValue, state));
689
+ return rerender();
690
+ case "input.wordRight":
691
+ replaceInputState(id, moveCursorWordRight(currentValue, state));
692
+ return rerender();
693
+ case "input.home":
694
+ replaceInputState(id, moveCursorHome(state, currentValue.length));
695
+ return rerender();
696
+ case "input.end":
697
+ replaceInputState(id, moveCursorEnd(state, currentValue.length));
698
+ return rerender();
699
+ case "input.selectAll":
700
+ replaceInputState(id, selectAll(currentValue));
701
+ return rerender();
702
+ case "input.copy":
703
+ writeClipboard(copySelection(currentValue, state));
704
+ return rerender();
705
+ case "input.cut":
706
+ writeClipboard(copySelection(currentValue, state));
707
+ if (hasSelection(state)) {
708
+ return updateInputByState(node, insertText(currentValue, state, ""));
709
+ }
710
+ return currentOutput;
711
+ case "input.paste":
712
+ return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(readClipboard())));
713
+ case "input.pasteText":
714
+ return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
715
+ case "input.backspace":
716
+ return updateInputByState(node, removeBackward(currentValue, state));
717
+ case "input.delete":
718
+ return updateInputByState(node, removeForward(currentValue, state));
719
+ case "input.insertText":
720
+ return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
721
+ default:
722
+ return currentOutput;
723
+ }
724
+ }
725
+
726
+ function isBuiltInInputCommand(command: TerminalCommand) {
727
+ return command.id === "input.submit"
728
+ || command.id === "input.cursorLeft"
729
+ || command.id === "input.cursorRight"
730
+ || command.id === "input.selectLeft"
731
+ || command.id === "input.selectRight"
732
+ || command.id === "input.wordLeft"
733
+ || command.id === "input.wordRight"
734
+ || command.id === "input.home"
735
+ || command.id === "input.end"
736
+ || command.id === "input.selectAll"
737
+ || command.id === "input.copy"
738
+ || command.id === "input.cut"
739
+ || command.id === "input.paste"
740
+ || command.id === "input.pasteText"
741
+ || command.id === "input.backspace"
742
+ || command.id === "input.delete"
743
+ || command.id === "input.insertText";
744
+ }
745
+
746
+ function runEditorCommand(node: TerminalFocusNode, command: TerminalCommand) {
747
+ const id = node.props.id;
748
+ if (!id) {
749
+ return currentOutput;
750
+ }
751
+
752
+ const currentValue = plainText(node.props.value ?? "");
753
+ const previous = editorStateById.get(id);
754
+ const state = createEditorState(currentValue, previous?.cursor, previous?.desiredColumn);
755
+
756
+ switch (command.id) {
757
+ case "editor.submit":
758
+ submitEditor(node);
759
+ return rerender();
760
+ case "editor.cancel":
761
+ cancelEditor(node);
762
+ return rerender();
763
+ case "editor.newline":
764
+ return updateEditorByState(node, insertEditorText(currentValue, state, "\n"));
765
+ case "editor.backspace":
766
+ return updateEditorByState(node, removeEditorBackward(currentValue, state));
767
+ case "editor.delete":
768
+ return updateEditorByState(node, removeEditorForward(currentValue, state));
769
+ case "editor.cursorLeft":
770
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "left"));
771
+ return rerender();
772
+ case "editor.cursorRight":
773
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "right"));
774
+ return rerender();
775
+ case "editor.cursorUp":
776
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "up"));
777
+ return rerender();
778
+ case "editor.cursorDown":
779
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "down"));
780
+ return rerender();
781
+ case "editor.home":
782
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "home"));
783
+ return rerender();
784
+ case "editor.end":
785
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "end"));
786
+ return rerender();
787
+ case "editor.paste":
788
+ return updateEditorByState(node, insertEditorText(currentValue, state, plainText(readClipboard())));
789
+ case "editor.pasteText":
790
+ case "editor.insertText":
791
+ return updateEditorByState(node, insertEditorText(currentValue, state, plainText(command.text ?? "")));
792
+ default:
793
+ return currentOutput;
794
+ }
795
+ }
796
+
797
+ function isBuiltInEditorCommand(command: TerminalCommand) {
798
+ return command.id === "editor.submit"
799
+ || command.id === "editor.cancel"
800
+ || command.id === "editor.newline"
801
+ || command.id === "editor.backspace"
802
+ || command.id === "editor.delete"
803
+ || command.id === "editor.cursorLeft"
804
+ || command.id === "editor.cursorRight"
805
+ || command.id === "editor.cursorUp"
806
+ || command.id === "editor.cursorDown"
807
+ || command.id === "editor.home"
808
+ || command.id === "editor.end"
809
+ || command.id === "editor.paste"
810
+ || command.id === "editor.pasteText"
811
+ || command.id === "editor.insertText";
812
+ }
813
+
814
+ function runCommand(command: TerminalCommand, node: TerminalFocusNode | null, context: TerminalCommandContext) {
815
+ switch (command.id) {
816
+ case "focus.next":
817
+ session.focusNext();
818
+ return currentOutput;
819
+ case "focus.prev":
820
+ session.focusPrev();
821
+ return currentOutput;
822
+ case "button.press":
823
+ if (node?.tag === "terminal-button") {
824
+ const id = String(node.props.id || "");
825
+ dispatchNodeEvent(node, "press", { type: "press", id });
826
+ return rerender();
827
+ }
828
+ return currentOutput;
829
+ case "list.prev":
830
+ return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
831
+ case "list.next":
832
+ return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
833
+ case "list.press":
834
+ return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
835
+ case "scroll.up":
836
+ return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, -1) : currentOutput;
837
+ case "scroll.down":
838
+ return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, 1) : currentOutput;
839
+ default:
840
+ if (node?.tag === "terminal-input" && isBuiltInInputCommand(command)) {
841
+ return runInputCommand(node, command);
842
+ }
843
+ if (node?.tag === "terminal-editor" && isBuiltInEditorCommand(command)) {
844
+ return runEditorCommand(node, command);
845
+ }
846
+ if (typeof options.keymap?.onCommand === "function") {
847
+ const consumed = options.keymap.onCommand(command, context);
848
+ return consumed ? rerender() : currentOutput;
849
+ }
850
+ return currentOutput;
851
+ }
852
+ }
853
+
854
+ const session: TerminalSession = {
855
+ size() {
856
+ return { ...terminalSize };
857
+ },
858
+ resize(cols: number, rows: number) {
859
+ const nextSize = {
860
+ cols: validateTerminalDimension("cols", cols),
861
+ rows: validateTerminalDimension("rows", rows)
862
+ };
863
+ pendingOutputResize = null;
864
+ outputResizeScheduler.cancel();
865
+ if (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows) {
866
+ return;
867
+ }
868
+ terminalSize = nextSize;
869
+ rerender();
870
+ },
871
+ update() {
872
+ return rerender();
873
+ },
874
+ output() {
875
+ return currentOutput;
876
+ },
877
+ ansiOutput() {
878
+ return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
879
+ },
880
+ tree() {
881
+ return currentTree;
882
+ },
883
+ focus(id: string) {
884
+ const node = findFocusableById(currentTree, id);
885
+ if (!node) {
886
+ return false;
887
+ }
888
+ focusedId = id;
889
+ rerender();
890
+ return true;
891
+ },
892
+ focusAt(x: number, y: number) {
893
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
894
+ if (!hitbox) {
895
+ clearSemanticHover(undefined, x, y);
896
+ return false;
897
+ }
898
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
899
+ const node = findFocusableById(currentTree, hitbox.id);
900
+ if (!node) {
901
+ return false;
902
+ }
903
+ focusedId = node.props.id || focusedId;
904
+ skipFocusContainmentOnce = true;
905
+ rerender();
906
+ return true;
907
+ },
908
+ focusNext() {
909
+ const focusables = orderedFocusables();
910
+ if (!focusables.length) {
911
+ return false;
912
+ }
913
+ const currentIndex = focusables.findIndex((node) => node.props.id === focusedId);
914
+ focusedId = focusables[(currentIndex + 1 + focusables.length) % focusables.length].props.id || null;
915
+ rerender();
916
+ return true;
917
+ },
918
+ focusPrev() {
919
+ const focusables = orderedFocusables();
920
+ if (!focusables.length) {
921
+ return false;
922
+ }
923
+ const currentIndex = focusables.findIndex((node) => node.props.id === focusedId);
924
+ const nextIndex = currentIndex <= 0 ? focusables.length - 1 : currentIndex - 1;
925
+ focusedId = focusables[nextIndex].props.id || null;
926
+ rerender();
927
+ return true;
928
+ },
929
+ dispatchKey(key: string) {
930
+ const node = findFocused(currentTree, focusedId);
931
+ const context: TerminalCommandContext = { key, focusedId: node?.props.id, focusedTag: node?.tag };
932
+ let command = resolveTerminalKeyBinding(keyBindings, context);
933
+ if (!command && node?.tag === "terminal-editor" && key.length === 1) {
934
+ command = { id: "editor.insertText", text: key };
935
+ }
936
+ if (!command && node?.tag === "terminal-editor" && key === "CTRL_V") {
937
+ command = { id: "editor.paste" };
938
+ }
939
+ return command ? runCommand(command, node, context) : currentOutput;
940
+ },
941
+ click(id?: string) {
942
+ const node = id ? findFocusableById(currentTree, id) : findFocused(currentTree, focusedId);
943
+ if (!node || node.tag !== "terminal-button") {
944
+ return currentOutput;
945
+ }
946
+ if (id) {
947
+ focusedId = node.props.id || focusedId;
948
+ }
949
+ dispatchNodeEvent(node, "press", { type: "press", id: String(node.props.id || "") });
950
+ return rerender();
951
+ },
952
+ clickAt(x: number, y: number) {
953
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
954
+ if (!hitbox) {
955
+ clearSemanticHover(undefined, x, y);
956
+ return currentOutput;
957
+ }
958
+ if (hitbox.tag === "terminal-button") {
959
+ return this.click(hitbox.id);
960
+ }
961
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
962
+ focusedId = hitbox.id;
963
+ if (hitbox.tag === "terminal-input") {
964
+ mouseSelectionId = hitbox.id;
965
+ return setCursorFromHitbox(hitbox.id, x, false);
966
+ }
967
+ rerender();
968
+ return currentOutput;
969
+ },
970
+ clipboard() {
971
+ return clipboardValue;
972
+ },
973
+ setClipboard(value: string) {
974
+ return writeClipboard(value);
975
+ },
976
+ selectAll() {
977
+ const node = findFocused(currentTree, focusedId);
978
+ if (!node || node.tag !== "terminal-input" || !node.props.id) {
979
+ return currentOutput;
980
+ }
981
+ replaceInputState(node.props.id, selectAll(String(node.props.value ?? "")));
982
+ return rerender();
983
+ },
984
+ destroy() {
985
+ if (destroyed) {
986
+ return;
987
+ }
988
+ destroyed = true;
989
+ cancelPendingEscapeFlush();
990
+ cleanupOutputResize?.();
991
+ cleanupOutputResize = null;
992
+ outputResizeScheduler.cancel();
993
+ renderScheduler.cancel();
994
+ terminalRuntime.destroy();
995
+ emitLifecycleRestore();
996
+ outputWriter.destroy();
997
+ if (runtimeOptions.stdin) {
998
+ if (typeof runtimeOptions.stdin.off === "function") {
999
+ runtimeOptions.stdin.off("data", onData);
1000
+ } else if (typeof runtimeOptions.stdin.removeListener === "function") {
1001
+ runtimeOptions.stdin.removeListener("data", onData);
1002
+ }
1003
+ runtimeOptions.stdin.setRawMode?.(false);
1004
+ runtimeOptions.stdin.pause?.();
1005
+ }
1006
+ }
1007
+ };
1008
+
1009
+ function dispatchPasteText(text: string) {
1010
+ const node = findFocused(currentTree, focusedId);
1011
+ if (node?.tag === "terminal-editor") {
1012
+ runCommand({ id: "editor.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
1013
+ } else if (node?.tag === "terminal-input") {
1014
+ runCommand({ id: "input.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
1015
+ }
1016
+ }
1017
+
1018
+ function cancelPendingEscapeFlush() {
1019
+ if (pendingEscapeFlush) {
1020
+ clearTimeout(pendingEscapeFlush);
1021
+ pendingEscapeFlush = null;
1022
+ }
1023
+ }
1024
+
1025
+ function flushPendingEscape() {
1026
+ cancelPendingEscapeFlush();
1027
+ if (destroyed || !pendingKeyChunk.startsWith(ESCAPE)) {
1028
+ return;
1029
+ }
1030
+ const buffered = pendingKeyChunk;
1031
+ pendingKeyChunk = "";
1032
+ session.dispatchKey(parseTerminalKey(ESCAPE));
1033
+ processInputStream(buffered.slice(ESCAPE.length));
1034
+ }
1035
+
1036
+ function schedulePendingEscapeFlush() {
1037
+ cancelPendingEscapeFlush();
1038
+ pendingEscapeFlush = setTimeout(flushPendingEscape, 0);
1039
+ }
1040
+
1041
+ function processKeyStream(value: string) {
1042
+ if (!value) {
1043
+ return;
1044
+ }
1045
+
1046
+ for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
1047
+ if (value.startsWith(sequence)) {
1048
+ session.dispatchKey(parseTerminalKey(sequence));
1049
+ processInputStream(value.slice(sequence.length));
1050
+ return;
1051
+ }
1052
+ }
1053
+
1054
+ if (value === ESCAPE) {
1055
+ pendingKeyChunk = value;
1056
+ schedulePendingEscapeFlush();
1057
+ return;
1058
+ }
1059
+
1060
+ if (isDisambiguatedBracketedPasteStartPrefix(value)) {
1061
+ pendingPasteChunk = value;
1062
+ return;
1063
+ }
1064
+
1065
+ if (isKnownTerminalKeySequencePrefix(value)) {
1066
+ pendingKeyChunk = value;
1067
+ schedulePendingEscapeFlush();
1068
+ return;
1069
+ }
1070
+
1071
+ const paste = parseBracketedPaste(value);
1072
+ if (paste) {
1073
+ dispatchPasteText(paste.text);
1074
+ processInputStream(paste.rest);
1075
+ return;
1076
+ }
1077
+
1078
+ const escapeIndex = value.indexOf("\u001b");
1079
+ if (escapeIndex > 0) {
1080
+ for (const char of value.slice(0, escapeIndex)) {
1081
+ session.dispatchKey(parseTerminalKey(char));
1082
+ }
1083
+ processInputStream(value.slice(escapeIndex));
1084
+ return;
1085
+ }
1086
+
1087
+ const parsedKey = parseTerminalKey(value);
1088
+ if (parsedKey !== value) {
1089
+ session.dispatchKey(parsedKey);
1090
+ return;
1091
+ }
1092
+
1093
+ if (value.startsWith("\u001b")) {
1094
+ session.dispatchKey(value);
1095
+ return;
1096
+ }
1097
+
1098
+ for (const char of value) {
1099
+ session.dispatchKey(parseTerminalKey(char));
1100
+ }
1101
+ }
1102
+
1103
+ function processInputStream(value: string) {
1104
+ if (!value) {
1105
+ return;
1106
+ }
1107
+
1108
+ if (pendingPasteChunk) {
1109
+ pendingPasteChunk += value;
1110
+ const paste = parseBracketedPaste(pendingPasteChunk);
1111
+ if (!paste && isBracketedPasteStartPrefix(pendingPasteChunk)) {
1112
+ return;
1113
+ }
1114
+ if (!paste) {
1115
+ const buffered = pendingPasteChunk;
1116
+ pendingPasteChunk = "";
1117
+ processKeyStream(buffered);
1118
+ return;
1119
+ }
1120
+ pendingPasteChunk = "";
1121
+ dispatchPasteText(paste.text);
1122
+ processInputStream(paste.rest);
1123
+ return;
1124
+ }
1125
+
1126
+ if (pendingKeyChunk) {
1127
+ if (pendingKeyChunk === ESCAPE && !canContinueEscapeSequence(value)) {
1128
+ flushPendingEscape();
1129
+ processInputStream(value);
1130
+ return;
1131
+ }
1132
+ const buffered = pendingKeyChunk + value;
1133
+ cancelPendingEscapeFlush();
1134
+ pendingKeyChunk = "";
1135
+ processKeyStream(buffered);
1136
+ return;
1137
+ }
1138
+
1139
+ if (value === ESCAPE) {
1140
+ processKeyStream(value);
1141
+ return;
1142
+ }
1143
+
1144
+ if (isBracketedPasteStartPrefix(value)) {
1145
+ pendingPasteChunk = value;
1146
+ return;
1147
+ }
1148
+
1149
+ if (value.startsWith(BRACKETED_PASTE_START)) {
1150
+ const paste = parseBracketedPaste(value);
1151
+ if (!paste) {
1152
+ pendingPasteChunk = value;
1153
+ return;
1154
+ }
1155
+ dispatchPasteText(paste.text);
1156
+ processInputStream(paste.rest);
1157
+ return;
1158
+ }
1159
+
1160
+ const parsed = parseTerminalInput(value);
1161
+ if (parsed.type === "mouse") {
1162
+ if (parsed.action === "press") {
1163
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1164
+ if (hitbox?.tag === "terminal-input") {
1165
+ mouseSelectionId = hitbox.id;
1166
+ }
1167
+ const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
1168
+ if (hitbox && node && shouldPointerCapture(node)) {
1169
+ setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
1170
+ }
1171
+ session.clickAt(parsed.x, parsed.y);
1172
+ } else if (parsed.action === "drag") {
1173
+ if (mouseSelectionId) {
1174
+ setCursorFromHitbox(mouseSelectionId, parsed.x, true);
1175
+ } else {
1176
+ hoverAt(parsed.x, parsed.y);
1177
+ }
1178
+ } else if (parsed.action === "release") {
1179
+ mouseSelectionId = null;
1180
+ const capturedId = pointerCaptureId;
1181
+ const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1182
+ const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
1183
+ const releaseRow = releaseHitbox && releaseNode
1184
+ ? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
1185
+ : null;
1186
+ setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
1187
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1188
+ if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
1189
+ setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
1190
+ rerender();
1191
+ } else {
1192
+ clearSemanticHover(undefined, parsed.x, parsed.y);
1193
+ rerender();
1194
+ }
1195
+ } else if (parsed.action === "wheel-up") {
1196
+ wheelAt(parsed.x, parsed.y, -1);
1197
+ } else if (parsed.action === "wheel-down") {
1198
+ wheelAt(parsed.x, parsed.y, 1);
1199
+ }
1200
+ return;
1201
+ }
1202
+
1203
+ processKeyStream(value);
1204
+ }
1205
+
1206
+ const onData = (chunk: string | Uint8Array) => {
1207
+ const value = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
1208
+ processInputStream(value);
1209
+ };
1210
+
1211
+ if (runtimeOptions.stdin) {
1212
+ runtimeOptions.stdin.on("data", onData);
1213
+ runtimeOptions.stdin.setRawMode?.(true);
1214
+ runtimeOptions.stdin.resume?.();
1215
+ }
1216
+
1217
+ function subscribeOutputResize() {
1218
+ const stdout = runtimeOptions.stdout;
1219
+ if (!stdout || typeof stdout.on !== "function") {
1220
+ return;
1221
+ }
1222
+ const removeResizeListener = typeof stdout.off === "function" ? stdout.off.bind(stdout) : stdout.removeListener?.bind(stdout);
1223
+ if (!removeResizeListener) {
1224
+ return;
1225
+ }
1226
+ const onResize = () => {
1227
+ const cols = stdout.columns;
1228
+ const rows = stdout.rows;
1229
+ if (!isValidTerminalDimension(cols) || !isValidTerminalDimension(rows)) {
1230
+ return;
1231
+ }
1232
+ pendingOutputResize = { cols, rows };
1233
+ outputResizeScheduler.requestRender();
1234
+ };
1235
+ try {
1236
+ stdout.on("resize", onResize);
1237
+ } catch {
1238
+ return;
1239
+ }
1240
+ cleanupOutputResize = () => removeResizeListener("resize", onResize);
1241
+ }
1242
+
1243
+ function applyPendingOutputResize() {
1244
+ const nextSize = pendingOutputResize;
1245
+ pendingOutputResize = null;
1246
+ if (!nextSize || destroyed || (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows)) {
1247
+ return;
1248
+ }
1249
+ terminalSize = nextSize;
1250
+ rerender();
1251
+ }
1252
+
1253
+ subscribeOutputResize();
1254
+
1255
+ emitLifecycleSetup();
1256
+ emitOutput();
1257
+ autoProjectionEnabled = true;
1258
+
1259
+ return session;
1260
+ }