@valyrianjs/terminal 0.1.2 → 0.2.1

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 (62) hide show
  1. package/dist/ansi.d.ts +2 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +12 -0
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/events.js +6 -2
  7. package/dist/events.js.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/keymap.d.ts.map +1 -1
  12. package/dist/keymap.js +4 -2
  13. package/dist/keymap.js.map +1 -1
  14. package/dist/layout.d.ts.map +1 -1
  15. package/dist/layout.js +2 -1
  16. package/dist/layout.js.map +1 -1
  17. package/dist/mouse.d.ts +6 -0
  18. package/dist/mouse.d.ts.map +1 -1
  19. package/dist/mouse.js +30 -16
  20. package/dist/mouse.js.map +1 -1
  21. package/dist/primitives.d.ts +8 -3
  22. package/dist/primitives.d.ts.map +1 -1
  23. package/dist/primitives.js +8 -1
  24. package/dist/primitives.js.map +1 -1
  25. package/dist/render.d.ts +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +290 -51
  28. package/dist/render.js.map +1 -1
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +13 -11
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/session.js +434 -65
  34. package/dist/session.js.map +1 -1
  35. package/dist/theme.d.ts.map +1 -1
  36. package/dist/theme.js +3 -0
  37. package/dist/theme.js.map +1 -1
  38. package/dist/tree.d.ts.map +1 -1
  39. package/dist/tree.js +18 -4
  40. package/dist/tree.js.map +1 -1
  41. package/dist/types.d.ts +61 -13
  42. package/dist/types.d.ts.map +1 -1
  43. package/docs/api-reference.md +40 -28
  44. package/docs/cookbook.md +2 -2
  45. package/docs/core-concepts.md +1 -1
  46. package/docs/interaction-model.md +18 -6
  47. package/docs/primitive-gallery.md +19 -10
  48. package/llms-full.txt +80 -47
  49. package/package.json +1 -1
  50. package/src/ansi.ts +12 -0
  51. package/src/events.ts +4 -2
  52. package/src/index.ts +3 -0
  53. package/src/keymap.ts +4 -2
  54. package/src/layout.ts +2 -1
  55. package/src/mouse.ts +31 -15
  56. package/src/primitives.ts +15 -5
  57. package/src/render.ts +320 -52
  58. package/src/runtime.ts +13 -11
  59. package/src/session.ts +469 -59
  60. package/src/theme.ts +3 -0
  61. package/src/tree.ts +19 -4
  62. package/src/types.ts +72 -12
package/dist/session.js CHANGED
@@ -1,10 +1,10 @@
1
- import { ANSI_ENTER_ALTERNATE_SCREEN, ANSI_EXIT_ALTERNATE_SCREEN, ANSI_HIDE_CURSOR, ANSI_SHOW_CURSOR, createAnsiDiffWriter, formatPlainFrame, toAnsiFrame } from "./ansi.js";
1
+ import { ANSI_DISABLE_MOUSE_REPORTING, ANSI_ENABLE_MOUSE_REPORTING, 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
3
  import { createEditorState, insertEditorText, moveEditorCursor, removeEditorBackward, removeEditorForward } from "./editor-state.js";
4
4
  import { copySelection, hasSelection, insertText, moveCursorEnd, moveCursorHome, moveCursorLeft, moveCursorRight, moveCursorWordLeft, moveCursorWordRight, normalizeInputState, parseTerminalKey, removeBackward, removeForward, selectAll } from "./events.js";
5
5
  import { createResolvedTerminalKeymap, resolveTerminalKeyBinding } from "./keymap.js";
6
6
  import { mergeVertical } from "./layout.js";
7
- import { cursorFromHitbox, parseTerminalInput, resolvePointerTarget } from "./mouse.js";
7
+ import { cursorFromHitbox, parseTerminalInput, parseTerminalMousePrefix, resolvePointerTarget } from "./mouse.js";
8
8
  import { createOutputWriter } from "./output-writer.js";
9
9
  import { parseBracketedPaste } from "./paste.js";
10
10
  import { renderTerminalFrame } from "./render.js";
@@ -25,6 +25,10 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
25
25
  "\u001b[1;3C",
26
26
  "\u001b[1;3D",
27
27
  "\u001b[3~",
28
+ "\u001b[5~",
29
+ "\u001b[6~",
30
+ "\u001b[1~",
31
+ "\u001b[4~",
28
32
  "\u001b[Z",
29
33
  "\u001b[A",
30
34
  "\u001b[B",
@@ -37,6 +41,7 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
37
41
  ];
38
42
  const ESCAPE = "\u001b";
39
43
  const CSI_PREFIX = "\u001b[";
44
+ const DOUBLE_PRESS_INTERVAL_MS = 500;
40
45
  function isBracketedPasteStartPrefix(value) {
41
46
  return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
42
47
  }
@@ -83,6 +88,7 @@ function resolveRuntimeOptions(options) {
83
88
  stdout,
84
89
  alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
85
90
  hideCursor: options.hideCursor ?? ownsInteractiveTTY,
91
+ mouseReporting: ownsInteractiveTTY,
86
92
  writesAnsi: runtime === "app" && Boolean(stdout)
87
93
  };
88
94
  }
@@ -94,7 +100,7 @@ function resolveTerminalSize(options, stdout) {
94
100
  rows: validateTerminalDimension("rows", rows)
95
101
  };
96
102
  }
97
- function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById) {
103
+ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById) {
98
104
  for (let i = 0; i < nodes.length; i += 1) {
99
105
  const node = nodes[i];
100
106
  if (node.type !== "element") {
@@ -116,7 +122,23 @@ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById
116
122
  node.props.__editorState = current;
117
123
  }
118
124
  if (node.tag === "terminal-list" && id) {
119
- node.props.__selectedIndex = listIndexById.get(id) || 0;
125
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
126
+ const activeIndex = listIndexById.get(id) || 0;
127
+ const clampedActiveIndex = Math.max(0, Math.min(Math.max(0, items.length - 1), activeIndex));
128
+ if (!listIndexById.has(id)) {
129
+ listIndexById.set(id, clampedActiveIndex);
130
+ }
131
+ const selectedIndex = node.props.showActive === false
132
+ ? null
133
+ : listSelectedIndexById.has(id)
134
+ ? listSelectedIndexById.get(id)
135
+ : clampedActiveIndex;
136
+ if (!listSelectedIndexById.has(id)) {
137
+ listSelectedIndexById.set(id, selectedIndex === null ? null : Math.max(0, Math.min(Math.max(0, items.length - 1), selectedIndex)));
138
+ }
139
+ node.props.__activeIndex = clampedActiveIndex;
140
+ node.props.__selectedIndex = selectedIndex === null ? null : Math.max(0, Math.min(Math.max(0, items.length - 1), selectedIndex));
141
+ node.props.__scrollOffset = listViewportOffsetById.get(id) || 0;
120
142
  if (listHoverById.has(id)) {
121
143
  node.props.__hoveredIndex = listHoverById.get(id);
122
144
  }
@@ -127,7 +149,7 @@ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById
127
149
  node.props.__hoveredRow = scrollHoverRowById.get(id);
128
150
  }
129
151
  }
130
- applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
152
+ applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
131
153
  }
132
154
  }
133
155
  export function mountTerminal(input, options = {}) {
@@ -139,6 +161,7 @@ export function mountTerminal(input, options = {}) {
139
161
  let pendingPasteChunk = "";
140
162
  let pendingKeyChunk = "";
141
163
  let pendingEscapeFlush = null;
164
+ let lastPrimaryPress = null;
142
165
  let destroyed = false;
143
166
  let autoProjectionEnabled = false;
144
167
  let suppressAutoProjection = false;
@@ -146,6 +169,8 @@ export function mountTerminal(input, options = {}) {
146
169
  const inputStateById = new Map();
147
170
  const editorStateById = new Map();
148
171
  const listIndexById = new Map();
172
+ const listSelectedIndexById = new Map();
173
+ const listViewportOffsetById = new Map();
149
174
  const scrollOffsetById = new Map();
150
175
  const listHoverById = new Map();
151
176
  const scrollHoverRowById = new Map();
@@ -159,7 +184,7 @@ export function mountTerminal(input, options = {}) {
159
184
  renderNow();
160
185
  });
161
186
  let currentTree = terminalRuntime.project();
162
- applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
187
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
163
188
  let currentFrame = renderTreeFrame(currentTree);
164
189
  let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
165
190
  let currentHitboxes = currentFrame.hitboxes;
@@ -184,6 +209,9 @@ export function mountTerminal(input, options = {}) {
184
209
  if (runtimeOptions.hideCursor) {
185
210
  writes.push(ANSI_HIDE_CURSOR);
186
211
  }
212
+ if (runtimeOptions.mouseReporting) {
213
+ writes.push(ANSI_ENABLE_MOUSE_REPORTING);
214
+ }
187
215
  if (writes.length > 0) {
188
216
  outputWriter.write(writes.join(""), { force: true });
189
217
  }
@@ -193,6 +221,9 @@ export function mountTerminal(input, options = {}) {
193
221
  return;
194
222
  }
195
223
  const writes = [];
224
+ if (runtimeOptions.mouseReporting) {
225
+ writes.push(ANSI_DISABLE_MOUSE_REPORTING);
226
+ }
196
227
  if (runtimeOptions.hideCursor) {
197
228
  writes.push(ANSI_SHOW_CURSOR);
198
229
  }
@@ -221,7 +252,7 @@ export function mountTerminal(input, options = {}) {
221
252
  focusedId = activeFocusables[0]?.props.id || null;
222
253
  }
223
254
  skipFocusContainmentOnce = false;
224
- applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
255
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
225
256
  currentFrame = renderTreeFrame(currentTree);
226
257
  currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
227
258
  currentHitboxes = currentFrame.hitboxes;
@@ -360,10 +391,18 @@ export function mountTerminal(input, options = {}) {
360
391
  return 1;
361
392
  }
362
393
  function sourceRowFromHitbox(node, hitbox, y) {
363
- const visibleRow = Math.max(1, y - hitbox.y1 + 1);
394
+ if (node.tag === "terminal-list" && typeof hitbox.__listItemIndex === "number" && y >= hitbox.y1 && y <= hitbox.y2) {
395
+ return Math.max(1, Math.min(rowCountForNode(node), hitbox.__listItemIndex + 1));
396
+ }
397
+ const sourceY = hitbox.contentY ?? hitbox.y1;
398
+ const visibleRow = Math.max(1, y - sourceY + 1);
364
399
  if (node.tag !== "terminal-list") {
365
400
  return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
366
401
  }
402
+ const mappedIndex = hitbox.itemIndexes?.[visibleRow - 1];
403
+ if (typeof mappedIndex === "number") {
404
+ return Math.max(1, Math.min(rowCountForNode(node), mappedIndex + 1));
405
+ }
367
406
  return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
368
407
  }
369
408
  function shouldPointerCapture(node) {
@@ -404,6 +443,155 @@ export function mountTerminal(input, options = {}) {
404
443
  emitCaptureEvent(next, "capturestart", source, row ?? hoveredRowForNode(next), x, y);
405
444
  }
406
445
  }
446
+ function listItemKey(node, index) {
447
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
448
+ const item = items[index];
449
+ if (typeof node.props.itemKey === "function" && typeof item !== "undefined") {
450
+ const key = node.props.itemKey(item, index);
451
+ if (typeof key === "string" || typeof key === "number") {
452
+ return String(key);
453
+ }
454
+ }
455
+ return undefined;
456
+ }
457
+ function listViewportRows(node) {
458
+ const context = renderContext();
459
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
460
+ const hitbox = node.props.id ? currentHitboxes.find((box) => box.id === node.props.id) : null;
461
+ const overscan = typeof node.props.overscan === "number" ? Math.max(0, Math.floor(node.props.overscan)) : 0;
462
+ const renderedRows = hitbox ? Math.max(1, hitbox.y2 - hitbox.y1 + 1 - overscan * 2) : null;
463
+ const sourceRows = Number(node.props.height || renderedRows || context.rows || items.length || 1);
464
+ if (!Number.isFinite(sourceRows) || !Number.isInteger(sourceRows) || sourceRows <= 0) {
465
+ return Math.max(1, items.length || 1);
466
+ }
467
+ return Math.max(1, Math.min(items.length || 1, sourceRows));
468
+ }
469
+ function clampListIndex(node, index) {
470
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
471
+ if (items.length === 0) {
472
+ return 0;
473
+ }
474
+ return Math.max(0, Math.min(items.length - 1, index));
475
+ }
476
+ function currentListActiveIndex(node) {
477
+ return clampListIndex(node, listIndexById.get(node.props.id || "") || 0);
478
+ }
479
+ function currentListSelectedIndex(node) {
480
+ const id = node.props.id || "";
481
+ if (node.props.showActive === false) {
482
+ return null;
483
+ }
484
+ if (listSelectedIndexById.has(id)) {
485
+ const selectedIndex = listSelectedIndexById.get(id);
486
+ return typeof selectedIndex === "number" ? clampListIndex(node, selectedIndex) : null;
487
+ }
488
+ return currentListActiveIndex(node);
489
+ }
490
+ function currentListViewportOffset(node) {
491
+ const id = node.props.id || "";
492
+ return Math.max(0, Math.min(listMaxViewportOffset(node), listViewportOffsetById.get(id) || 0));
493
+ }
494
+ function listStatePayload(node) {
495
+ return {
496
+ activeIndex: currentListActiveIndex(node),
497
+ selectedIndex: currentListSelectedIndex(node),
498
+ viewportOffset: currentListViewportOffset(node),
499
+ viewportRows: listViewportRows(node)
500
+ };
501
+ }
502
+ function listMaxViewportOffset(node) {
503
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
504
+ return Math.max(0, items.length - listViewportRows(node));
505
+ }
506
+ function setListViewportOffset(node, offset, emit = true) {
507
+ const id = node.props.id;
508
+ if (!id) {
509
+ return;
510
+ }
511
+ const nextOffset = Math.max(0, Math.min(listMaxViewportOffset(node), offset));
512
+ const previous = listViewportOffsetById.get(id) || 0;
513
+ listViewportOffsetById.set(id, nextOffset);
514
+ if (emit && nextOffset !== previous) {
515
+ const payload = {
516
+ type: "viewportchange",
517
+ id,
518
+ offset: nextOffset,
519
+ rows: listViewportRows(node),
520
+ ...listStatePayload(node)
521
+ };
522
+ dispatchNodeEvent(node, "viewportchange", payload);
523
+ }
524
+ }
525
+ function ensureListActiveVisible(node, activeIndex) {
526
+ const id = node.props.id;
527
+ if (!id || !node.props.virtualized) {
528
+ return;
529
+ }
530
+ const currentOffset = listViewportOffsetById.get(id) || 0;
531
+ const rows = listViewportRows(node);
532
+ if (activeIndex < currentOffset) {
533
+ setListViewportOffset(node, activeIndex);
534
+ return;
535
+ }
536
+ if (activeIndex >= currentOffset + rows) {
537
+ setListViewportOffset(node, activeIndex - rows + 1);
538
+ }
539
+ }
540
+ function dispatchListPressEvent(node, type, index) {
541
+ const id = node.props.id;
542
+ if (!id) {
543
+ return false;
544
+ }
545
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
546
+ if (typeof items[index] === "undefined") {
547
+ return false;
548
+ }
549
+ const key = listItemKey(node, index);
550
+ const payload = typeof key === "undefined"
551
+ ? { type, id, index, value: items[index], ...listStatePayload(node) }
552
+ : { type, id, index, key, value: items[index], ...listStatePayload(node) };
553
+ return dispatchNodeEvent(node, type, payload);
554
+ }
555
+ function dispatchButtonPressEvent(node, type) {
556
+ const id = String(node.props.id || "");
557
+ if (!id) {
558
+ return false;
559
+ }
560
+ return dispatchNodeEvent(node, type, { type, id });
561
+ }
562
+ function dispatchHitboxButtonPressEvent(hitbox, type) {
563
+ if (type !== "press" || typeof hitbox.__pressHandler !== "function") {
564
+ return false;
565
+ }
566
+ hitbox.__pressHandler({ type, id: hitbox.id });
567
+ return true;
568
+ }
569
+ function dispatchListPointerPressEvent(node, type, row) {
570
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
571
+ const index = Math.max(0, Math.min(items.length - 1, row - 1));
572
+ return dispatchListPressEvent(node, type, index);
573
+ }
574
+ function dispatchInputContextPressEvent(node, hitbox, x, y) {
575
+ const id = String(node.props.id || "");
576
+ if (!id) {
577
+ return false;
578
+ }
579
+ const value = stripTerminalControls(node.props.value ?? "");
580
+ const current = normalizeInputState(inputStateById.get(id), value.length);
581
+ const cursor = typeof x === "number" ? cursorFromHitbox(hitbox, x) : current.cursor;
582
+ return dispatchNodeEvent(node, "contextpress", { type: "contextpress", id, value, cursor, x, y });
583
+ }
584
+ function dispatchScrollContextPressEvent(node, row, x, y) {
585
+ if (!node.props.id) {
586
+ return false;
587
+ }
588
+ const lines = visibleScrollLines(node);
589
+ const index = Math.max(0, Math.min(lines.length - 1, row - 1));
590
+ if (typeof lines[index] === "undefined") {
591
+ return false;
592
+ }
593
+ return dispatchNodeEvent(node, "contextpress", { type: "contextpress", id: node.props.id, row: index + 1, value: lines[index], x, y });
594
+ }
407
595
  function emitMouseRowEvent(node, type, row, x = null, y = null) {
408
596
  if (!node.props.id) {
409
597
  return;
@@ -414,7 +602,10 @@ export function mountTerminal(input, options = {}) {
414
602
  if (typeof items[index] === "undefined") {
415
603
  return;
416
604
  }
417
- const payload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
605
+ const key = listItemKey(node, index);
606
+ const payload = typeof key === "undefined"
607
+ ? { type, id: node.props.id, row: index + 1, index, value: items[index], x, y }
608
+ : { type, id: node.props.id, row: index + 1, index, key, value: items[index], x, y };
418
609
  dispatchNodeEvent(node, type, payload);
419
610
  return;
420
611
  }
@@ -504,28 +695,72 @@ export function mountTerminal(input, options = {}) {
504
695
  setSemanticHoverFromHitbox(hitbox.id, x, y);
505
696
  return rerender();
506
697
  }
507
- function changeListSelection(node, direction) {
698
+ function moveListActiveTo(node, nextIndex) {
508
699
  const id = node.props.id;
509
700
  if (!id) {
510
701
  return currentOutput;
511
702
  }
512
703
  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);
704
+ const clampedIndex = clampListIndex(node, nextIndex);
705
+ listIndexById.set(id, clampedIndex);
706
+ ensureListActiveVisible(node, clampedIndex);
707
+ const key = listItemKey(node, clampedIndex);
708
+ const payload = typeof key === "undefined"
709
+ ? { type: "change", id, index: clampedIndex, value: items[clampedIndex], ...listStatePayload(node) }
710
+ : { type: "change", id, index: clampedIndex, key, value: items[clampedIndex], ...listStatePayload(node) };
711
+ dispatchNodeEvent(node, "change", payload);
712
+ return rerender();
713
+ }
714
+ function changeListSelection(node, direction) {
715
+ return moveListActiveTo(node, currentListActiveIndex(node) + direction);
716
+ }
717
+ function pageListSelection(node, direction) {
718
+ const id = node.props.id;
719
+ if (!id) {
720
+ return currentOutput;
721
+ }
722
+ const nextIndex = clampListIndex(node, currentListActiveIndex(node) + direction * listViewportRows(node));
515
723
  listIndexById.set(id, nextIndex);
516
- const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
724
+ setListViewportOffset(node, nextIndex);
725
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
726
+ const key = listItemKey(node, nextIndex);
727
+ const payload = typeof key === "undefined"
728
+ ? { type: "change", id, index: nextIndex, value: items[nextIndex], ...listStatePayload(node) }
729
+ : { type: "change", id, index: nextIndex, key, value: items[nextIndex], ...listStatePayload(node) };
517
730
  dispatchNodeEvent(node, "change", payload);
518
731
  return rerender();
519
732
  }
733
+ function moveListSelectionToBoundary(node, boundary) {
734
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
735
+ return moveListActiveTo(node, boundary === "start" ? 0 : Math.max(0, items.length - 1));
736
+ }
520
737
  function pressListSelection(node) {
738
+ const id = node.props.id;
739
+ if (!id) {
740
+ return currentOutput;
741
+ }
742
+ const currentIndex = currentListActiveIndex(node);
743
+ if (node.props.showActive !== false) {
744
+ listSelectedIndexById.set(id, currentIndex);
745
+ }
746
+ dispatchListPressEvent(node, "press", currentIndex);
747
+ return rerender();
748
+ }
749
+ function pressListPointerSelection(node, row) {
521
750
  const id = node.props.id;
522
751
  if (!id) {
523
752
  return currentOutput;
524
753
  }
525
754
  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);
755
+ const index = Math.max(0, Math.min(items.length - 1, row - 1));
756
+ if (typeof items[index] === "undefined") {
757
+ return currentOutput;
758
+ }
759
+ listIndexById.set(id, index);
760
+ if (node.props.showActive !== false) {
761
+ listSelectedIndexById.set(id, index);
762
+ }
763
+ dispatchListPressEvent(node, "press", index);
529
764
  return rerender();
530
765
  }
531
766
  function scrollFocusedNode(node, direction) {
@@ -552,7 +787,9 @@ export function mountTerminal(input, options = {}) {
552
787
  return scrollFocusedNode(node, direction);
553
788
  }
554
789
  if (node?.tag === "terminal-list" && node.props.virtualized) {
555
- return changeListSelection(node, direction);
790
+ const currentOffset = listViewportOffsetById.get(node.props.id || "") || 0;
791
+ setListViewportOffset(node, currentOffset + direction);
792
+ return rerender();
556
793
  }
557
794
  return rerender();
558
795
  }
@@ -710,8 +947,7 @@ export function mountTerminal(input, options = {}) {
710
947
  return currentOutput;
711
948
  case "button.press":
712
949
  if (node?.tag === "terminal-button") {
713
- const id = String(node.props.id || "");
714
- dispatchNodeEvent(node, "press", { type: "press", id });
950
+ dispatchButtonPressEvent(node, "press");
715
951
  return rerender();
716
952
  }
717
953
  return currentOutput;
@@ -719,6 +955,14 @@ export function mountTerminal(input, options = {}) {
719
955
  return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
720
956
  case "list.next":
721
957
  return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
958
+ case "list.pageUp":
959
+ return node?.tag === "terminal-list" ? pageListSelection(node, -1) : currentOutput;
960
+ case "list.pageDown":
961
+ return node?.tag === "terminal-list" ? pageListSelection(node, 1) : currentOutput;
962
+ case "list.home":
963
+ return node?.tag === "terminal-list" ? moveListSelectionToBoundary(node, "start") : currentOutput;
964
+ case "list.end":
965
+ return node?.tag === "terminal-list" ? moveListSelectionToBoundary(node, "end") : currentOutput;
722
966
  case "list.press":
723
967
  return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
724
968
  case "scroll.up":
@@ -754,6 +998,19 @@ export function mountTerminal(input, options = {}) {
754
998
  return;
755
999
  }
756
1000
  terminalSize = nextSize;
1001
+ const focusedNode = findFocused(currentTree, focusedId);
1002
+ if (focusedNode?.tag === "terminal-list" && focusedNode.props.id && focusedNode.props.virtualized) {
1003
+ const items = Array.isArray(focusedNode.props.items) ? focusedNode.props.items : [];
1004
+ const rows = Math.max(1, Math.min(items.length || 1, Number(focusedNode.props.height || terminalSize.rows || items.length || 1)));
1005
+ const activeIndex = currentListActiveIndex(focusedNode);
1006
+ const currentOffset = listViewportOffsetById.get(focusedNode.props.id) || 0;
1007
+ if (activeIndex < currentOffset) {
1008
+ listViewportOffsetById.set(focusedNode.props.id, activeIndex);
1009
+ }
1010
+ else if (activeIndex >= currentOffset + rows) {
1011
+ listViewportOffsetById.set(focusedNode.props.id, Math.max(0, activeIndex - rows + 1));
1012
+ }
1013
+ }
757
1014
  rerender();
758
1015
  },
759
1016
  update() {
@@ -834,7 +1091,7 @@ export function mountTerminal(input, options = {}) {
834
1091
  if (id) {
835
1092
  focusedId = node.props.id || focusedId;
836
1093
  }
837
- dispatchNodeEvent(node, "press", { type: "press", id: String(node.props.id || "") });
1094
+ dispatchButtonPressEvent(node, "press");
838
1095
  return rerender();
839
1096
  },
840
1097
  clickAt(x, y) {
@@ -844,7 +1101,14 @@ export function mountTerminal(input, options = {}) {
844
1101
  return currentOutput;
845
1102
  }
846
1103
  if (hitbox.tag === "terminal-button") {
847
- return this.click(hitbox.id);
1104
+ const node = findFocusableById(currentTree, hitbox.id);
1105
+ if (node?.tag === "terminal-button") {
1106
+ return this.click(hitbox.id);
1107
+ }
1108
+ if (dispatchHitboxButtonPressEvent(hitbox, "press")) {
1109
+ return rerender();
1110
+ }
1111
+ return currentOutput;
848
1112
  }
849
1113
  setSemanticHoverFromHitbox(hitbox.id, x, y);
850
1114
  focusedId = hitbox.id;
@@ -852,6 +1116,12 @@ export function mountTerminal(input, options = {}) {
852
1116
  mouseSelectionId = hitbox.id;
853
1117
  return setCursorFromHitbox(hitbox.id, x, false);
854
1118
  }
1119
+ if (hitbox.tag === "terminal-list") {
1120
+ const node = findFocusableById(currentTree, hitbox.id);
1121
+ if (node?.tag === "terminal-list") {
1122
+ return pressListPointerSelection(node, sourceRowFromHitbox(node, hitbox, y));
1123
+ }
1124
+ }
855
1125
  rerender();
856
1126
  return currentOutput;
857
1127
  },
@@ -975,6 +1245,142 @@ export function mountTerminal(input, options = {}) {
975
1245
  session.dispatchKey(parseTerminalKey(char));
976
1246
  }
977
1247
  }
1248
+ function isPrimaryMouseButton(button) {
1249
+ return button < 64 && (button & 3) === 0;
1250
+ }
1251
+ function isContextMouseButton(button) {
1252
+ return button < 64 && (button & 3) === 2;
1253
+ }
1254
+ function isDoublePrimaryPress(hitbox, row) {
1255
+ const now = Date.now();
1256
+ const isDouble = Boolean(lastPrimaryPress
1257
+ && lastPrimaryPress.id === hitbox.id
1258
+ && lastPrimaryPress.tag === hitbox.tag
1259
+ && lastPrimaryPress.row === row
1260
+ && now - lastPrimaryPress.at <= DOUBLE_PRESS_INTERVAL_MS);
1261
+ lastPrimaryPress = { id: hitbox.id, tag: hitbox.tag, row, at: now };
1262
+ return isDouble;
1263
+ }
1264
+ function doublePressAt(hitbox, x, y) {
1265
+ const node = findFocusableById(currentTree, hitbox.id);
1266
+ if (!node) {
1267
+ return currentOutput;
1268
+ }
1269
+ focusedId = hitbox.id;
1270
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
1271
+ if (node.tag === "terminal-button") {
1272
+ dispatchButtonPressEvent(node, "doublepress");
1273
+ return rerender();
1274
+ }
1275
+ if (node.tag === "terminal-list") {
1276
+ dispatchListPointerPressEvent(node, "doublepress", sourceRowFromHitbox(node, hitbox, y));
1277
+ return rerender();
1278
+ }
1279
+ return currentOutput;
1280
+ }
1281
+ function contextPressAt(x, y) {
1282
+ const hitbox = resolvePointerTarget(currentHitboxes, x, y);
1283
+ if (!hitbox) {
1284
+ clearSemanticHover(undefined, x, y);
1285
+ return currentOutput;
1286
+ }
1287
+ const node = findFocusableById(currentTree, hitbox.id);
1288
+ if (!node) {
1289
+ return currentOutput;
1290
+ }
1291
+ focusedId = hitbox.id;
1292
+ setSemanticHoverFromHitbox(hitbox.id, x, y);
1293
+ if (node.tag === "terminal-button") {
1294
+ dispatchButtonPressEvent(node, "contextpress");
1295
+ return rerender();
1296
+ }
1297
+ if (node.tag === "terminal-list") {
1298
+ dispatchListPointerPressEvent(node, "contextpress", sourceRowFromHitbox(node, hitbox, y));
1299
+ return rerender();
1300
+ }
1301
+ if (node.tag === "terminal-input") {
1302
+ dispatchInputContextPressEvent(node, hitbox, x, y);
1303
+ return rerender();
1304
+ }
1305
+ if (node.tag === "terminal-scroll") {
1306
+ dispatchScrollContextPressEvent(node, sourceRowFromHitbox(node, hitbox, y), x, y);
1307
+ return rerender();
1308
+ }
1309
+ return currentOutput;
1310
+ }
1311
+ function processParsedMouseInput(parsed) {
1312
+ if (parsed.action === "press") {
1313
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1314
+ if (isContextMouseButton(parsed.button)) {
1315
+ lastPrimaryPress = null;
1316
+ contextPressAt(parsed.x, parsed.y);
1317
+ return;
1318
+ }
1319
+ if (hitbox?.tag === "terminal-input") {
1320
+ mouseSelectionId = hitbox.id;
1321
+ }
1322
+ const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
1323
+ if (hitbox && node && shouldPointerCapture(node)) {
1324
+ setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
1325
+ }
1326
+ const isPrimaryPress = isPrimaryMouseButton(parsed.button);
1327
+ const isDoublePressEligible = Boolean(hitbox
1328
+ && node
1329
+ && (hitbox.tag === "terminal-button" || hitbox.tag === "terminal-list"));
1330
+ const primaryPressRow = hitbox && node && hitbox.tag === "terminal-list"
1331
+ ? sourceRowFromHitbox(node, hitbox, parsed.y)
1332
+ : null;
1333
+ const shouldDispatchDoublePress = Boolean(isPrimaryPress
1334
+ && isDoublePressEligible
1335
+ && hitbox
1336
+ && isDoublePrimaryPress(hitbox, primaryPressRow));
1337
+ if (isPrimaryPress && !isDoublePressEligible) {
1338
+ lastPrimaryPress = null;
1339
+ }
1340
+ if (hitbox && shouldDispatchDoublePress && hitbox.tag === "terminal-list") {
1341
+ doublePressAt(hitbox, parsed.x, parsed.y);
1342
+ }
1343
+ else {
1344
+ session.clickAt(parsed.x, parsed.y);
1345
+ if (hitbox && shouldDispatchDoublePress) {
1346
+ doublePressAt(hitbox, parsed.x, parsed.y);
1347
+ }
1348
+ }
1349
+ }
1350
+ else if (parsed.action === "drag") {
1351
+ if (mouseSelectionId) {
1352
+ setCursorFromHitbox(mouseSelectionId, parsed.x, true);
1353
+ }
1354
+ else {
1355
+ hoverAt(parsed.x, parsed.y);
1356
+ }
1357
+ }
1358
+ else if (parsed.action === "release") {
1359
+ mouseSelectionId = null;
1360
+ const capturedId = pointerCaptureId;
1361
+ const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1362
+ const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
1363
+ const releaseRow = releaseHitbox && releaseNode
1364
+ ? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
1365
+ : null;
1366
+ setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
1367
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1368
+ if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
1369
+ setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
1370
+ rerender();
1371
+ }
1372
+ else {
1373
+ clearSemanticHover(undefined, parsed.x, parsed.y);
1374
+ rerender();
1375
+ }
1376
+ }
1377
+ else if (parsed.action === "wheel-up") {
1378
+ wheelAt(parsed.x, parsed.y, -1);
1379
+ }
1380
+ else if (parsed.action === "wheel-down") {
1381
+ wheelAt(parsed.x, parsed.y, 1);
1382
+ }
1383
+ }
978
1384
  function processInputStream(value) {
979
1385
  if (!value) {
980
1386
  return;
@@ -1026,52 +1432,15 @@ export function mountTerminal(input, options = {}) {
1026
1432
  processInputStream(paste.rest);
1027
1433
  return;
1028
1434
  }
1435
+ const parsedMouse = parseTerminalMousePrefix(value);
1436
+ if (parsedMouse) {
1437
+ processParsedMouseInput(parsedMouse.input);
1438
+ processInputStream(parsedMouse.rest);
1439
+ return;
1440
+ }
1029
1441
  const parsed = parseTerminalInput(value);
1030
1442
  if (parsed.type === "mouse") {
1031
- if (parsed.action === "press") {
1032
- const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1033
- if (hitbox?.tag === "terminal-input") {
1034
- mouseSelectionId = hitbox.id;
1035
- }
1036
- const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
1037
- if (hitbox && node && shouldPointerCapture(node)) {
1038
- setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
1039
- }
1040
- session.clickAt(parsed.x, parsed.y);
1041
- }
1042
- else if (parsed.action === "drag") {
1043
- if (mouseSelectionId) {
1044
- setCursorFromHitbox(mouseSelectionId, parsed.x, true);
1045
- }
1046
- else {
1047
- hoverAt(parsed.x, parsed.y);
1048
- }
1049
- }
1050
- else if (parsed.action === "release") {
1051
- mouseSelectionId = null;
1052
- const capturedId = pointerCaptureId;
1053
- const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1054
- const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
1055
- const releaseRow = releaseHitbox && releaseNode
1056
- ? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
1057
- : null;
1058
- setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
1059
- const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1060
- if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
1061
- setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
1062
- rerender();
1063
- }
1064
- else {
1065
- clearSemanticHover(undefined, parsed.x, parsed.y);
1066
- rerender();
1067
- }
1068
- }
1069
- else if (parsed.action === "wheel-up") {
1070
- wheelAt(parsed.x, parsed.y, -1);
1071
- }
1072
- else if (parsed.action === "wheel-down") {
1073
- wheelAt(parsed.x, parsed.y, 1);
1074
- }
1443
+ processParsedMouseInput(parsed);
1075
1444
  return;
1076
1445
  }
1077
1446
  processKeyStream(value);