@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/dist/session.js CHANGED
@@ -1,11 +1,100 @@
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
+ function isBracketedPasteStartPrefix(value) {
41
+ return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
42
+ }
43
+ function isDisambiguatedBracketedPasteStartPrefix(value) {
44
+ return isBracketedPasteStartPrefix(value) && value.length > CSI_PREFIX.length;
45
+ }
46
+ function isKnownTerminalKeySequencePrefix(value) {
47
+ return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
48
+ }
49
+ function canContinueEscapeSequence(value) {
50
+ return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
51
+ }
52
+ function validateTerminalDimension(name, value) {
53
+ if (!Number.isInteger(value) || value < 1) {
54
+ throw new Error(`Invalid terminal ${name}: expected an integer >= 1`);
55
+ }
56
+ return value;
57
+ }
58
+ function isValidTerminalDimension(value) {
59
+ return Number.isInteger(value) && Number(value) >= 1;
60
+ }
61
+ function getProcessStdin() {
62
+ const candidate = globalThis.process?.stdin;
63
+ return candidate && typeof candidate.on === "function" ? candidate : undefined;
64
+ }
65
+ function getProcessStdout() {
66
+ const candidate = globalThis.process?.stdout;
67
+ return candidate && typeof candidate.write === "function" ? candidate : undefined;
68
+ }
69
+ function isInteractiveTTY(stdin, stdout) {
70
+ return Boolean(stdin?.isTTY && stdout?.isTTY);
71
+ }
72
+ function resolveRuntimeOptions(options) {
73
+ const processStdin = getProcessStdin();
74
+ const processStdout = getProcessStdout();
75
+ const canUseProcessTTY = isInteractiveTTY(processStdin, processStdout);
76
+ const runtime = options.runtime ?? (canUseProcessTTY || options.stdin || options.stdout ? "app" : "headless");
77
+ const stdin = options.stdin ?? (runtime === "app" && canUseProcessTTY ? processStdin : undefined);
78
+ const stdout = options.stdout ?? (runtime === "app" && canUseProcessTTY ? processStdout : undefined);
79
+ const ownsInteractiveTTY = runtime === "app" && isInteractiveTTY(stdin, stdout);
80
+ return {
81
+ runtime,
82
+ stdin,
83
+ stdout,
84
+ alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
85
+ hideCursor: options.hideCursor ?? ownsInteractiveTTY,
86
+ writesAnsi: runtime === "app" && Boolean(stdout)
87
+ };
88
+ }
89
+ function resolveTerminalSize(options, stdout) {
90
+ const cols = options.cols ?? stdout?.columns ?? DEFAULT_TERMINAL_SIZE.cols;
91
+ const rows = options.rows ?? stdout?.rows ?? DEFAULT_TERMINAL_SIZE.rows;
92
+ return {
93
+ cols: validateTerminalDimension("cols", cols),
94
+ rows: validateTerminalDimension("rows", rows)
95
+ };
96
+ }
97
+ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById) {
9
98
  for (let i = 0; i < nodes.length; i += 1) {
10
99
  const node = nodes[i];
11
100
  if (node.type !== "element") {
@@ -14,11 +103,18 @@ function applyInteractiveState(nodes, focusedId, inputStateById, listIndexById,
14
103
  const id = node.props.id;
15
104
  node.props.__focused = Boolean(focusedId && id === focusedId);
16
105
  if (node.tag === "terminal-input" && id) {
17
- const value = String(node.props.value ?? "");
106
+ const value = plainText(node.props.value ?? "");
18
107
  const current = normalizeInputState(inputStateById.get(id), value.length);
19
108
  inputStateById.set(id, current);
20
109
  node.props.__inputState = current;
21
110
  }
111
+ if (node.tag === "terminal-editor" && id) {
112
+ const value = plainText(node.props.value ?? "");
113
+ const previous = editorStateById.get(id);
114
+ const current = createEditorState(value, previous?.cursor, previous?.desiredColumn);
115
+ editorStateById.set(id, current);
116
+ node.props.__editorState = current;
117
+ }
22
118
  if (node.tag === "terminal-list" && id) {
23
119
  node.props.__selectedIndex = listIndexById.get(id) || 0;
24
120
  if (listHoverById.has(id)) {
@@ -31,46 +127,133 @@ function applyInteractiveState(nodes, focusedId, inputStateById, listIndexById,
31
127
  node.props.__hoveredRow = scrollHoverRowById.get(id);
32
128
  }
33
129
  }
34
- applyInteractiveState(node.children, focusedId, inputStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
130
+ applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
35
131
  }
36
132
  }
37
133
  export function mountTerminal(input, options = {}) {
38
134
  let focusedId = null;
135
+ let skipFocusContainmentOnce = false;
39
136
  let clipboardValue = "";
40
137
  let mouseSelectionId = null;
41
138
  let pointerCaptureId = null;
139
+ let pendingPasteChunk = "";
140
+ let pendingKeyChunk = "";
141
+ let pendingEscapeFlush = null;
142
+ let destroyed = false;
143
+ let autoProjectionEnabled = false;
144
+ let suppressAutoProjection = false;
42
145
  const clipboardAdapter = options.clipboard === false ? null : options.clipboard || createSystemClipboardAdapter();
43
146
  const inputStateById = new Map();
147
+ const editorStateById = new Map();
44
148
  const listIndexById = new Map();
45
149
  const scrollOffsetById = new Map();
46
150
  const listHoverById = new Map();
47
151
  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();
152
+ const keyBindings = createResolvedTerminalKeymap(options.keymap?.bindings);
153
+ const runtimeOptions = resolveRuntimeOptions(options);
154
+ let terminalSize = resolveTerminalSize(options, runtimeOptions.stdout);
155
+ const terminalRuntime = createValyrianTerminalRuntime(input, () => {
156
+ if (!autoProjectionEnabled || suppressAutoProjection || destroyed) {
157
+ return;
158
+ }
159
+ renderNow();
160
+ });
161
+ let currentTree = terminalRuntime.project();
162
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
163
+ let currentFrame = renderTreeFrame(currentTree);
164
+ let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
52
165
  let currentHitboxes = currentFrame.hitboxes;
53
- const toAnsiDiff = createAnsiDiffWriter();
166
+ const outputWriter = createOutputWriter(runtimeOptions.stdout);
167
+ const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
168
+ const renderScheduler = createRenderScheduler(renderNow);
169
+ const outputResizeScheduler = createRenderScheduler(applyPendingOutputResize);
170
+ let cleanupOutputResize = null;
171
+ let pendingOutputResize = null;
172
+ function renderContext() {
173
+ return { ...terminalSize, theme: options.theme };
174
+ }
175
+ function renderTreeFrame(nodes) {
176
+ const context = renderContext();
177
+ return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
178
+ }
179
+ function emitLifecycleSetup() {
180
+ const writes = [];
181
+ if (runtimeOptions.alternateScreen) {
182
+ writes.push(ANSI_ENTER_ALTERNATE_SCREEN);
183
+ }
184
+ if (runtimeOptions.hideCursor) {
185
+ writes.push(ANSI_HIDE_CURSOR);
186
+ }
187
+ if (writes.length > 0) {
188
+ outputWriter.write(writes.join(""), { force: true });
189
+ }
190
+ }
191
+ function emitLifecycleRestore() {
192
+ if (options.restoreOnDestroy === false) {
193
+ return;
194
+ }
195
+ const writes = [];
196
+ if (runtimeOptions.hideCursor) {
197
+ writes.push(ANSI_SHOW_CURSOR);
198
+ }
199
+ if (runtimeOptions.alternateScreen) {
200
+ writes.push(ANSI_EXIT_ALTERNATE_SCREEN);
201
+ }
202
+ if (writes.length > 0) {
203
+ outputWriter.write(writes.join(""), { force: true });
204
+ }
205
+ }
54
206
  function emitOutput() {
55
- if (options.stdout) {
56
- options.stdout.write(options.ansi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
207
+ if (!destroyed) {
208
+ outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
57
209
  }
58
210
  }
59
- function rerender() {
60
- currentTree = resolveRoot(input);
211
+ function renderNow() {
212
+ currentTree = terminalRuntime.project();
61
213
  const focusables = [];
62
214
  collectFocusableNodes(currentTree, focusables);
63
- if (focusedId && !focusables.some((node) => node.props.id === focusedId)) {
64
- focusedId = focusables[0]?.props.id || null;
215
+ const overlayFocusables = [];
216
+ collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
217
+ const scopedFocusables = [];
218
+ collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
219
+ const activeFocusables = overlayFocusables.length ? overlayFocusables : scopedFocusables.length ? scopedFocusables : focusables;
220
+ if (!skipFocusContainmentOnce && focusedId && !activeFocusables.some((node) => node.props.id === focusedId)) {
221
+ focusedId = activeFocusables[0]?.props.id || null;
65
222
  }
66
- applyInteractiveState(currentTree, focusedId, inputStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
67
- currentFrame = mergeVertical(currentTree.map(renderTerminalFrame));
68
- currentOutput = formatPlainFrame(currentFrame).trimEnd();
223
+ skipFocusContainmentOnce = false;
224
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
225
+ currentFrame = renderTreeFrame(currentTree);
226
+ currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
69
227
  currentHitboxes = currentFrame.hitboxes;
70
228
  emitOutput();
71
229
  return currentOutput;
72
230
  }
231
+ function rerender() {
232
+ if (destroyed) {
233
+ return currentOutput;
234
+ }
235
+ suppressAutoProjection = true;
236
+ try {
237
+ terminalRuntime.update();
238
+ }
239
+ finally {
240
+ suppressAutoProjection = false;
241
+ }
242
+ renderScheduler.requestRender();
243
+ renderScheduler.flush();
244
+ return currentOutput;
245
+ }
73
246
  function orderedFocusables() {
247
+ const overlayFocusables = [];
248
+ collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
249
+ if (overlayFocusables.length) {
250
+ return overlayFocusables;
251
+ }
252
+ const scopedFocusables = [];
253
+ collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
254
+ if (scopedFocusables.length) {
255
+ return scopedFocusables;
256
+ }
74
257
  const out = [];
75
258
  collectFocusableNodes(currentTree, out);
76
259
  return out;
@@ -105,21 +288,51 @@ export function mountTerminal(input, options = {}) {
105
288
  }
106
289
  function setInputCursor(id, cursor, extendSelection = false) {
107
290
  const node = findFocusableById(currentTree, id);
108
- const value = String(node?.props.value ?? "");
291
+ const value = stripTerminalControls(node?.props.value ?? "");
109
292
  const current = normalizeInputState(inputStateById.get(id), value.length);
110
293
  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
294
  }
112
295
  function replaceInputState(id, state) {
113
296
  const node = findFocusableById(currentTree, id);
114
- const value = String(node?.props.value ?? "");
297
+ const value = stripTerminalControls(node?.props.value ?? "");
115
298
  inputStateById.set(id, normalizeInputState(state, value.length));
116
299
  }
300
+ function dispatchNodeEvent(node, type, payload) {
301
+ const id = String(node.props.id || "");
302
+ if (!id) {
303
+ return false;
304
+ }
305
+ return terminalRuntime.dispatchTerminalEvent(id, type, payload);
306
+ }
117
307
  function updateInputByState(node, next) {
118
308
  const id = String(node.props.id || "");
119
- updateInputValue(node, next.value);
309
+ dispatchNodeEvent(node, "change", { type: "change", id, value: next.value });
120
310
  inputStateById.set(id, next.state);
121
311
  return rerender();
122
312
  }
313
+ function replaceEditorState(id, state) {
314
+ const node = findFocusableById(currentTree, id);
315
+ const value = plainText(node?.props.value ?? "");
316
+ editorStateById.set(id, createEditorState(value, state.cursor, state.desiredColumn));
317
+ }
318
+ function updateEditorValue(node, nextValue) {
319
+ const id = String(node.props.id || "");
320
+ dispatchNodeEvent(node, "change", { type: "change", id, value: plainText(nextValue) });
321
+ }
322
+ function updateEditorByState(node, next) {
323
+ const id = String(node.props.id || "");
324
+ updateEditorValue(node, next.value);
325
+ editorStateById.set(id, next.state);
326
+ return rerender();
327
+ }
328
+ function submitEditor(node) {
329
+ const id = String(node.props.id || "");
330
+ dispatchNodeEvent(node, "submit", { type: "submit", id, value: plainText(node.props.value ?? "") });
331
+ }
332
+ function cancelEditor(node) {
333
+ const id = String(node.props.id || "");
334
+ dispatchNodeEvent(node, "cancel", { type: "cancel", id, value: plainText(node.props.value ?? "") });
335
+ }
123
336
  function setCursorFromHitbox(id, x, extendSelection = false) {
124
337
  const hitbox = currentHitboxes.find((box) => box.id === id && box.tag === "terminal-input");
125
338
  if (!hitbox) {
@@ -130,7 +343,8 @@ export function mountTerminal(input, options = {}) {
130
343
  return rerender();
131
344
  }
132
345
  function visibleScrollLines(node) {
133
- const lines = mergeVertical(node.children.map(renderTerminalFrame)).lines;
346
+ const context = renderContext();
347
+ const lines = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context))).lines;
134
348
  const offset = Number(node.props.__scrollOffset || 0);
135
349
  const height = Number(node.props.height || lines.length || 0);
136
350
  return lines.slice(offset, offset + height).map((line) => line.trimEnd());
@@ -145,6 +359,13 @@ export function mountTerminal(input, options = {}) {
145
359
  }
146
360
  return 1;
147
361
  }
362
+ function sourceRowFromHitbox(node, hitbox, y) {
363
+ const visibleRow = Math.max(1, y - hitbox.y1 + 1);
364
+ if (node.tag !== "terminal-list") {
365
+ return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
366
+ }
367
+ return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
368
+ }
148
369
  function shouldPointerCapture(node) {
149
370
  return Boolean(node.props.pointerCapture && (node.tag === "terminal-list" || node.tag === "terminal-scroll"));
150
371
  }
@@ -166,12 +387,8 @@ export function mountTerminal(input, options = {}) {
166
387
  if (!node || !node.props.id) {
167
388
  return;
168
389
  }
169
- const handler = node.props[`on${type}`];
170
- if (typeof handler !== "function") {
171
- return;
172
- }
173
390
  const payload = { type, id: node.props.id, source, row, x, y };
174
- handler(payload);
391
+ dispatchNodeEvent(node, type, payload);
175
392
  }
176
393
  function setPointerCapture(id, source, row = null, x = null, y = null) {
177
394
  if (pointerCaptureId === id) {
@@ -188,8 +405,7 @@ export function mountTerminal(input, options = {}) {
188
405
  }
189
406
  }
190
407
  function emitMouseRowEvent(node, type, row, x = null, y = null) {
191
- const handler = node.props[`on${type}`];
192
- if (typeof handler !== "function" || !node.props.id) {
408
+ if (!node.props.id) {
193
409
  return;
194
410
  }
195
411
  if (node.tag === "terminal-list") {
@@ -199,7 +415,7 @@ export function mountTerminal(input, options = {}) {
199
415
  return;
200
416
  }
201
417
  const payload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
202
- handler(payload);
418
+ dispatchNodeEvent(node, type, payload);
203
419
  return;
204
420
  }
205
421
  if (node.tag === "terminal-scroll") {
@@ -209,7 +425,7 @@ export function mountTerminal(input, options = {}) {
209
425
  return;
210
426
  }
211
427
  const payload = { type, id: node.props.id, row: index + 1, value: lines[index], x, y };
212
- handler(payload);
428
+ dispatchNodeEvent(node, type, payload);
213
429
  }
214
430
  }
215
431
  function clearSemanticHover(exceptId, x = null, y = null) {
@@ -242,7 +458,7 @@ export function mountTerminal(input, options = {}) {
242
458
  return;
243
459
  }
244
460
  clearSemanticHover(id, x, y);
245
- const row = Math.max(1, Math.min(rowCountForNode(node), y - hitbox.y1 + 1));
461
+ const row = sourceRowFromHitbox(node, hitbox, y);
246
462
  if (node.tag === "terminal-list") {
247
463
  const nextIndex = row - 1;
248
464
  const prevIndex = listHoverById.get(id);
@@ -272,7 +488,7 @@ export function mountTerminal(input, options = {}) {
272
488
  }
273
489
  }
274
490
  function hoverAt(x, y) {
275
- const hitbox = findHitboxAt(currentHitboxes, x, y);
491
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
276
492
  if (!hitbox) {
277
493
  if (pointerCaptureId) {
278
494
  setSemanticHoverFromHitbox(pointerCaptureId, x, y);
@@ -283,12 +499,263 @@ export function mountTerminal(input, options = {}) {
283
499
  }
284
500
  const node = findFocusableById(currentTree, hitbox.id);
285
501
  if (node && shouldPointerCapture(node)) {
286
- setPointerCapture(hitbox.id, "drag", Math.max(1, Math.min(rowCountForNode(node), y - hitbox.y1 + 1)), x, y);
502
+ setPointerCapture(hitbox.id, "drag", sourceRowFromHitbox(node, hitbox, y), x, y);
287
503
  }
288
504
  setSemanticHoverFromHitbox(hitbox.id, x, y);
289
505
  return rerender();
290
506
  }
507
+ function changeListSelection(node, direction) {
508
+ const id = node.props.id;
509
+ if (!id) {
510
+ return currentOutput;
511
+ }
512
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
513
+ const currentIndex = listIndexById.get(id) || 0;
514
+ const nextIndex = direction < 0 ? Math.max(0, currentIndex - 1) : Math.min(items.length - 1, currentIndex + 1);
515
+ listIndexById.set(id, nextIndex);
516
+ const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
517
+ dispatchNodeEvent(node, "change", payload);
518
+ return rerender();
519
+ }
520
+ function pressListSelection(node) {
521
+ const id = node.props.id;
522
+ if (!id) {
523
+ return currentOutput;
524
+ }
525
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
526
+ const currentIndex = listIndexById.get(id) || 0;
527
+ const payload = { type: "press", id, index: currentIndex, value: items[currentIndex] };
528
+ dispatchNodeEvent(node, "press", payload);
529
+ return rerender();
530
+ }
531
+ function scrollFocusedNode(node, direction) {
532
+ const id = node.props.id;
533
+ if (!id) {
534
+ return currentOutput;
535
+ }
536
+ const context = renderContext();
537
+ const rendered = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
538
+ const height = Number(node.props.height || rendered.lines.length || 0);
539
+ const currentOffset = scrollOffsetById.get(id) || 0;
540
+ const maxOffset = Math.max(0, rendered.lines.length - height);
541
+ scrollOffsetById.set(id, direction < 0 ? Math.max(0, currentOffset - 1) : Math.min(maxOffset, currentOffset + 1));
542
+ return rerender();
543
+ }
544
+ function wheelAt(x, y, direction) {
545
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
546
+ if (!hitbox) {
547
+ return currentOutput;
548
+ }
549
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
550
+ const node = findFocusableById(currentTree, hitbox.id);
551
+ if (node?.tag === "terminal-scroll") {
552
+ return scrollFocusedNode(node, direction);
553
+ }
554
+ if (node?.tag === "terminal-list" && node.props.virtualized) {
555
+ return changeListSelection(node, direction);
556
+ }
557
+ return rerender();
558
+ }
559
+ function runInputCommand(node, command) {
560
+ const id = node.props.id;
561
+ if (!id) {
562
+ return currentOutput;
563
+ }
564
+ const currentValue = stripTerminalControls(node.props.value ?? "");
565
+ const state = normalizeInputState(inputStateById.get(id), currentValue.length);
566
+ switch (command.id) {
567
+ case "input.submit":
568
+ dispatchNodeEvent(node, "submit", { type: "submit", id, value: stripTerminalControls(node.props.value ?? "") });
569
+ return rerender();
570
+ case "input.cursorLeft":
571
+ replaceInputState(id, moveCursorLeft(state, currentValue.length));
572
+ return rerender();
573
+ case "input.cursorRight":
574
+ replaceInputState(id, moveCursorRight(state, currentValue.length));
575
+ return rerender();
576
+ case "input.selectLeft":
577
+ replaceInputState(id, moveCursorLeft(state, currentValue.length, true));
578
+ return rerender();
579
+ case "input.selectRight":
580
+ replaceInputState(id, moveCursorRight(state, currentValue.length, true));
581
+ return rerender();
582
+ case "input.wordLeft":
583
+ replaceInputState(id, moveCursorWordLeft(currentValue, state));
584
+ return rerender();
585
+ case "input.wordRight":
586
+ replaceInputState(id, moveCursorWordRight(currentValue, state));
587
+ return rerender();
588
+ case "input.home":
589
+ replaceInputState(id, moveCursorHome(state, currentValue.length));
590
+ return rerender();
591
+ case "input.end":
592
+ replaceInputState(id, moveCursorEnd(state, currentValue.length));
593
+ return rerender();
594
+ case "input.selectAll":
595
+ replaceInputState(id, selectAll(currentValue));
596
+ return rerender();
597
+ case "input.copy":
598
+ writeClipboard(copySelection(currentValue, state));
599
+ return rerender();
600
+ case "input.cut":
601
+ writeClipboard(copySelection(currentValue, state));
602
+ if (hasSelection(state)) {
603
+ return updateInputByState(node, insertText(currentValue, state, ""));
604
+ }
605
+ return currentOutput;
606
+ case "input.paste":
607
+ return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(readClipboard())));
608
+ case "input.pasteText":
609
+ return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
610
+ case "input.backspace":
611
+ return updateInputByState(node, removeBackward(currentValue, state));
612
+ case "input.delete":
613
+ return updateInputByState(node, removeForward(currentValue, state));
614
+ case "input.insertText":
615
+ return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
616
+ default:
617
+ return currentOutput;
618
+ }
619
+ }
620
+ function isBuiltInInputCommand(command) {
621
+ return command.id === "input.submit"
622
+ || command.id === "input.cursorLeft"
623
+ || command.id === "input.cursorRight"
624
+ || command.id === "input.selectLeft"
625
+ || command.id === "input.selectRight"
626
+ || command.id === "input.wordLeft"
627
+ || command.id === "input.wordRight"
628
+ || command.id === "input.home"
629
+ || command.id === "input.end"
630
+ || command.id === "input.selectAll"
631
+ || command.id === "input.copy"
632
+ || command.id === "input.cut"
633
+ || command.id === "input.paste"
634
+ || command.id === "input.pasteText"
635
+ || command.id === "input.backspace"
636
+ || command.id === "input.delete"
637
+ || command.id === "input.insertText";
638
+ }
639
+ function runEditorCommand(node, command) {
640
+ const id = node.props.id;
641
+ if (!id) {
642
+ return currentOutput;
643
+ }
644
+ const currentValue = plainText(node.props.value ?? "");
645
+ const previous = editorStateById.get(id);
646
+ const state = createEditorState(currentValue, previous?.cursor, previous?.desiredColumn);
647
+ switch (command.id) {
648
+ case "editor.submit":
649
+ submitEditor(node);
650
+ return rerender();
651
+ case "editor.cancel":
652
+ cancelEditor(node);
653
+ return rerender();
654
+ case "editor.newline":
655
+ return updateEditorByState(node, insertEditorText(currentValue, state, "\n"));
656
+ case "editor.backspace":
657
+ return updateEditorByState(node, removeEditorBackward(currentValue, state));
658
+ case "editor.delete":
659
+ return updateEditorByState(node, removeEditorForward(currentValue, state));
660
+ case "editor.cursorLeft":
661
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "left"));
662
+ return rerender();
663
+ case "editor.cursorRight":
664
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "right"));
665
+ return rerender();
666
+ case "editor.cursorUp":
667
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "up"));
668
+ return rerender();
669
+ case "editor.cursorDown":
670
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "down"));
671
+ return rerender();
672
+ case "editor.home":
673
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "home"));
674
+ return rerender();
675
+ case "editor.end":
676
+ replaceEditorState(id, moveEditorCursor(currentValue, state, "end"));
677
+ return rerender();
678
+ case "editor.paste":
679
+ return updateEditorByState(node, insertEditorText(currentValue, state, plainText(readClipboard())));
680
+ case "editor.pasteText":
681
+ case "editor.insertText":
682
+ return updateEditorByState(node, insertEditorText(currentValue, state, plainText(command.text ?? "")));
683
+ default:
684
+ return currentOutput;
685
+ }
686
+ }
687
+ function isBuiltInEditorCommand(command) {
688
+ return command.id === "editor.submit"
689
+ || command.id === "editor.cancel"
690
+ || command.id === "editor.newline"
691
+ || command.id === "editor.backspace"
692
+ || command.id === "editor.delete"
693
+ || command.id === "editor.cursorLeft"
694
+ || command.id === "editor.cursorRight"
695
+ || command.id === "editor.cursorUp"
696
+ || command.id === "editor.cursorDown"
697
+ || command.id === "editor.home"
698
+ || command.id === "editor.end"
699
+ || command.id === "editor.paste"
700
+ || command.id === "editor.pasteText"
701
+ || command.id === "editor.insertText";
702
+ }
703
+ function runCommand(command, node, context) {
704
+ switch (command.id) {
705
+ case "focus.next":
706
+ session.focusNext();
707
+ return currentOutput;
708
+ case "focus.prev":
709
+ session.focusPrev();
710
+ return currentOutput;
711
+ case "button.press":
712
+ if (node?.tag === "terminal-button") {
713
+ const id = String(node.props.id || "");
714
+ dispatchNodeEvent(node, "press", { type: "press", id });
715
+ return rerender();
716
+ }
717
+ return currentOutput;
718
+ case "list.prev":
719
+ return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
720
+ case "list.next":
721
+ return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
722
+ case "list.press":
723
+ return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
724
+ case "scroll.up":
725
+ return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, -1) : currentOutput;
726
+ case "scroll.down":
727
+ return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, 1) : currentOutput;
728
+ default:
729
+ if (node?.tag === "terminal-input" && isBuiltInInputCommand(command)) {
730
+ return runInputCommand(node, command);
731
+ }
732
+ if (node?.tag === "terminal-editor" && isBuiltInEditorCommand(command)) {
733
+ return runEditorCommand(node, command);
734
+ }
735
+ if (typeof options.keymap?.onCommand === "function") {
736
+ const consumed = options.keymap.onCommand(command, context);
737
+ return consumed ? rerender() : currentOutput;
738
+ }
739
+ return currentOutput;
740
+ }
741
+ }
291
742
  const session = {
743
+ size() {
744
+ return { ...terminalSize };
745
+ },
746
+ resize(cols, rows) {
747
+ const nextSize = {
748
+ cols: validateTerminalDimension("cols", cols),
749
+ rows: validateTerminalDimension("rows", rows)
750
+ };
751
+ pendingOutputResize = null;
752
+ outputResizeScheduler.cancel();
753
+ if (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows) {
754
+ return;
755
+ }
756
+ terminalSize = nextSize;
757
+ rerender();
758
+ },
292
759
  update() {
293
760
  return rerender();
294
761
  },
@@ -296,7 +763,7 @@ export function mountTerminal(input, options = {}) {
296
763
  return currentOutput;
297
764
  },
298
765
  ansiOutput() {
299
- return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans);
766
+ return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
300
767
  },
301
768
  tree() {
302
769
  return currentTree;
@@ -311,13 +778,20 @@ export function mountTerminal(input, options = {}) {
311
778
  return true;
312
779
  },
313
780
  focusAt(x, y) {
314
- const hitbox = findHitboxAt(currentHitboxes, x, y);
781
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
315
782
  if (!hitbox) {
316
783
  clearSemanticHover(undefined, x, y);
317
784
  return false;
318
785
  }
319
786
  setSemanticHoverFromHitbox(hitbox.id, x, y);
320
- return this.focus(hitbox.id);
787
+ const node = findFocusableById(currentTree, hitbox.id);
788
+ if (!node) {
789
+ return false;
790
+ }
791
+ focusedId = node.props.id || focusedId;
792
+ skipFocusContainmentOnce = true;
793
+ rerender();
794
+ return true;
321
795
  },
322
796
  focusNext() {
323
797
  const focusables = orderedFocusables();
@@ -341,143 +815,16 @@ export function mountTerminal(input, options = {}) {
341
815
  return true;
342
816
  },
343
817
  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
818
  const node = findFocused(currentTree, focusedId);
353
- if (!node) {
354
- return currentOutput;
819
+ const context = { key, focusedId: node?.props.id, focusedTag: node?.tag };
820
+ let command = resolveTerminalKeyBinding(keyBindings, context);
821
+ if (!command && node?.tag === "terminal-editor" && key.length === 1) {
822
+ command = { id: "editor.insertText", text: key };
355
823
  }
356
- if (node.tag === "terminal-button" && (key === "ENTER" || key === "SPACE")) {
357
- invokeButton(node);
358
- return rerender();
824
+ if (!command && node?.tag === "terminal-editor" && key === "CTRL_V") {
825
+ command = { id: "editor.paste" };
359
826
  }
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
- }
390
- }
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
- }
408
- }
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;
827
+ return command ? runCommand(command, node, context) : currentOutput;
481
828
  },
482
829
  click(id) {
483
830
  const node = id ? findFocusableById(currentTree, id) : findFocused(currentTree, focusedId);
@@ -487,11 +834,11 @@ export function mountTerminal(input, options = {}) {
487
834
  if (id) {
488
835
  focusedId = node.props.id || focusedId;
489
836
  }
490
- invokeButton(node);
837
+ dispatchNodeEvent(node, "press", { type: "press", id: String(node.props.id || "") });
491
838
  return rerender();
492
839
  },
493
840
  clickAt(x, y) {
494
- const hitbox = findHitboxAt(currentHitboxes, x, y);
841
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
495
842
  if (!hitbox) {
496
843
  clearSemanticHover(undefined, x, y);
497
844
  return currentOutput;
@@ -523,29 +870,172 @@ export function mountTerminal(input, options = {}) {
523
870
  return rerender();
524
871
  },
525
872
  destroy() {
526
- if (options.stdin) {
527
- if (typeof options.stdin.off === "function") {
528
- options.stdin.off("data", onData);
873
+ if (destroyed) {
874
+ return;
875
+ }
876
+ destroyed = true;
877
+ cancelPendingEscapeFlush();
878
+ cleanupOutputResize?.();
879
+ cleanupOutputResize = null;
880
+ outputResizeScheduler.cancel();
881
+ renderScheduler.cancel();
882
+ terminalRuntime.destroy();
883
+ emitLifecycleRestore();
884
+ outputWriter.destroy();
885
+ if (runtimeOptions.stdin) {
886
+ if (typeof runtimeOptions.stdin.off === "function") {
887
+ runtimeOptions.stdin.off("data", onData);
529
888
  }
530
- else if (typeof options.stdin.removeListener === "function") {
531
- options.stdin.removeListener("data", onData);
889
+ else if (typeof runtimeOptions.stdin.removeListener === "function") {
890
+ runtimeOptions.stdin.removeListener("data", onData);
532
891
  }
533
- options.stdin.setRawMode?.(false);
534
- options.stdin.pause?.();
892
+ runtimeOptions.stdin.setRawMode?.(false);
893
+ runtimeOptions.stdin.pause?.();
535
894
  }
536
895
  }
537
896
  };
538
- const onData = (chunk) => {
539
- const parsed = parseTerminalInput(chunk);
897
+ function dispatchPasteText(text) {
898
+ const node = findFocused(currentTree, focusedId);
899
+ if (node?.tag === "terminal-editor") {
900
+ runCommand({ id: "editor.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
901
+ }
902
+ else if (node?.tag === "terminal-input") {
903
+ runCommand({ id: "input.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
904
+ }
905
+ }
906
+ function cancelPendingEscapeFlush() {
907
+ if (pendingEscapeFlush) {
908
+ clearTimeout(pendingEscapeFlush);
909
+ pendingEscapeFlush = null;
910
+ }
911
+ }
912
+ function flushPendingEscape() {
913
+ cancelPendingEscapeFlush();
914
+ if (destroyed || !pendingKeyChunk.startsWith(ESCAPE)) {
915
+ return;
916
+ }
917
+ const buffered = pendingKeyChunk;
918
+ pendingKeyChunk = "";
919
+ session.dispatchKey(parseTerminalKey(ESCAPE));
920
+ processInputStream(buffered.slice(ESCAPE.length));
921
+ }
922
+ function schedulePendingEscapeFlush() {
923
+ cancelPendingEscapeFlush();
924
+ pendingEscapeFlush = setTimeout(flushPendingEscape, 0);
925
+ }
926
+ function processKeyStream(value) {
927
+ if (!value) {
928
+ return;
929
+ }
930
+ for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
931
+ if (value.startsWith(sequence)) {
932
+ session.dispatchKey(parseTerminalKey(sequence));
933
+ processInputStream(value.slice(sequence.length));
934
+ return;
935
+ }
936
+ }
937
+ if (value === ESCAPE) {
938
+ pendingKeyChunk = value;
939
+ schedulePendingEscapeFlush();
940
+ return;
941
+ }
942
+ if (isDisambiguatedBracketedPasteStartPrefix(value)) {
943
+ pendingPasteChunk = value;
944
+ return;
945
+ }
946
+ if (isKnownTerminalKeySequencePrefix(value)) {
947
+ pendingKeyChunk = value;
948
+ schedulePendingEscapeFlush();
949
+ return;
950
+ }
951
+ const paste = parseBracketedPaste(value);
952
+ if (paste) {
953
+ dispatchPasteText(paste.text);
954
+ processInputStream(paste.rest);
955
+ return;
956
+ }
957
+ const escapeIndex = value.indexOf("\u001b");
958
+ if (escapeIndex > 0) {
959
+ for (const char of value.slice(0, escapeIndex)) {
960
+ session.dispatchKey(parseTerminalKey(char));
961
+ }
962
+ processInputStream(value.slice(escapeIndex));
963
+ return;
964
+ }
965
+ const parsedKey = parseTerminalKey(value);
966
+ if (parsedKey !== value) {
967
+ session.dispatchKey(parsedKey);
968
+ return;
969
+ }
970
+ if (value.startsWith("\u001b")) {
971
+ session.dispatchKey(value);
972
+ return;
973
+ }
974
+ for (const char of value) {
975
+ session.dispatchKey(parseTerminalKey(char));
976
+ }
977
+ }
978
+ function processInputStream(value) {
979
+ if (!value) {
980
+ return;
981
+ }
982
+ if (pendingPasteChunk) {
983
+ pendingPasteChunk += value;
984
+ const paste = parseBracketedPaste(pendingPasteChunk);
985
+ if (!paste && isBracketedPasteStartPrefix(pendingPasteChunk)) {
986
+ return;
987
+ }
988
+ if (!paste) {
989
+ const buffered = pendingPasteChunk;
990
+ pendingPasteChunk = "";
991
+ processKeyStream(buffered);
992
+ return;
993
+ }
994
+ pendingPasteChunk = "";
995
+ dispatchPasteText(paste.text);
996
+ processInputStream(paste.rest);
997
+ return;
998
+ }
999
+ if (pendingKeyChunk) {
1000
+ if (pendingKeyChunk === ESCAPE && !canContinueEscapeSequence(value)) {
1001
+ flushPendingEscape();
1002
+ processInputStream(value);
1003
+ return;
1004
+ }
1005
+ const buffered = pendingKeyChunk + value;
1006
+ cancelPendingEscapeFlush();
1007
+ pendingKeyChunk = "";
1008
+ processKeyStream(buffered);
1009
+ return;
1010
+ }
1011
+ if (value === ESCAPE) {
1012
+ processKeyStream(value);
1013
+ return;
1014
+ }
1015
+ if (isBracketedPasteStartPrefix(value)) {
1016
+ pendingPasteChunk = value;
1017
+ return;
1018
+ }
1019
+ if (value.startsWith(BRACKETED_PASTE_START)) {
1020
+ const paste = parseBracketedPaste(value);
1021
+ if (!paste) {
1022
+ pendingPasteChunk = value;
1023
+ return;
1024
+ }
1025
+ dispatchPasteText(paste.text);
1026
+ processInputStream(paste.rest);
1027
+ return;
1028
+ }
1029
+ const parsed = parseTerminalInput(value);
540
1030
  if (parsed.type === "mouse") {
541
1031
  if (parsed.action === "press") {
542
- const hitbox = findHitboxAt(currentHitboxes, parsed.x, parsed.y);
1032
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
543
1033
  if (hitbox?.tag === "terminal-input") {
544
1034
  mouseSelectionId = hitbox.id;
545
1035
  }
546
1036
  const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
547
1037
  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);
1038
+ setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
549
1039
  }
550
1040
  session.clickAt(parsed.x, parsed.y);
551
1041
  }
@@ -560,13 +1050,13 @@ export function mountTerminal(input, options = {}) {
560
1050
  else if (parsed.action === "release") {
561
1051
  mouseSelectionId = null;
562
1052
  const capturedId = pointerCaptureId;
563
- const releaseHitbox = findHitboxAt(currentHitboxes, parsed.x, parsed.y);
1053
+ const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
564
1054
  const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
565
1055
  const releaseRow = releaseHitbox && releaseNode
566
- ? Math.max(1, Math.min(rowCountForNode(releaseNode), parsed.y - releaseHitbox.y1 + 1))
1056
+ ? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
567
1057
  : null;
568
1058
  setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
569
- const hitbox = findHitboxAt(currentHitboxes, parsed.x, parsed.y);
1059
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
570
1060
  if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
571
1061
  setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
572
1062
  rerender();
@@ -577,23 +1067,63 @@ export function mountTerminal(input, options = {}) {
577
1067
  }
578
1068
  }
579
1069
  else if (parsed.action === "wheel-up") {
580
- session.focusAt(parsed.x, parsed.y);
581
- session.dispatchKey("UP");
1070
+ wheelAt(parsed.x, parsed.y, -1);
582
1071
  }
583
1072
  else if (parsed.action === "wheel-down") {
584
- session.focusAt(parsed.x, parsed.y);
585
- session.dispatchKey("DOWN");
1073
+ wheelAt(parsed.x, parsed.y, 1);
586
1074
  }
587
1075
  return;
588
1076
  }
589
- session.dispatchKey(parsed.key || parseTerminalKey(chunk));
1077
+ processKeyStream(value);
1078
+ }
1079
+ const onData = (chunk) => {
1080
+ const value = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
1081
+ processInputStream(value);
590
1082
  };
591
- if (options.stdin) {
592
- options.stdin.on("data", onData);
593
- options.stdin.setRawMode?.(true);
594
- options.stdin.resume?.();
1083
+ if (runtimeOptions.stdin) {
1084
+ runtimeOptions.stdin.on("data", onData);
1085
+ runtimeOptions.stdin.setRawMode?.(true);
1086
+ runtimeOptions.stdin.resume?.();
1087
+ }
1088
+ function subscribeOutputResize() {
1089
+ const stdout = runtimeOptions.stdout;
1090
+ if (!stdout || typeof stdout.on !== "function") {
1091
+ return;
1092
+ }
1093
+ const removeResizeListener = typeof stdout.off === "function" ? stdout.off.bind(stdout) : stdout.removeListener?.bind(stdout);
1094
+ if (!removeResizeListener) {
1095
+ return;
1096
+ }
1097
+ const onResize = () => {
1098
+ const cols = stdout.columns;
1099
+ const rows = stdout.rows;
1100
+ if (!isValidTerminalDimension(cols) || !isValidTerminalDimension(rows)) {
1101
+ return;
1102
+ }
1103
+ pendingOutputResize = { cols, rows };
1104
+ outputResizeScheduler.requestRender();
1105
+ };
1106
+ try {
1107
+ stdout.on("resize", onResize);
1108
+ }
1109
+ catch {
1110
+ return;
1111
+ }
1112
+ cleanupOutputResize = () => removeResizeListener("resize", onResize);
1113
+ }
1114
+ function applyPendingOutputResize() {
1115
+ const nextSize = pendingOutputResize;
1116
+ pendingOutputResize = null;
1117
+ if (!nextSize || destroyed || (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows)) {
1118
+ return;
1119
+ }
1120
+ terminalSize = nextSize;
1121
+ rerender();
595
1122
  }
1123
+ subscribeOutputResize();
1124
+ emitLifecycleSetup();
596
1125
  emitOutput();
1126
+ autoProjectionEnabled = true;
597
1127
  return session;
598
1128
  }
599
1129
  //# sourceMappingURL=session.js.map