@valyrianjs/terminal 0.1.1 → 0.2.0

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