@valyrianjs/terminal 0.2.0 → 0.2.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 (102) hide show
  1. package/dist/ansi.d.ts +2 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +23 -13
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/events.js +10 -2
  7. package/dist/events.js.map +1 -1
  8. package/dist/frame-style.d.ts +7 -0
  9. package/dist/frame-style.d.ts.map +1 -0
  10. package/dist/frame-style.js +27 -0
  11. package/dist/frame-style.js.map +1 -0
  12. package/dist/keymap.d.ts.map +1 -1
  13. package/dist/keymap.js +4 -2
  14. package/dist/keymap.js.map +1 -1
  15. package/dist/layout.d.ts +5 -1
  16. package/dist/layout.d.ts.map +1 -1
  17. package/dist/layout.js +55 -24
  18. package/dist/layout.js.map +1 -1
  19. package/dist/mouse.d.ts +6 -0
  20. package/dist/mouse.d.ts.map +1 -1
  21. package/dist/mouse.js +38 -17
  22. package/dist/mouse.js.map +1 -1
  23. package/dist/primitives.d.ts.map +1 -1
  24. package/dist/primitives.js +8 -1
  25. package/dist/primitives.js.map +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +266 -70
  28. package/dist/render.js.map +1 -1
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +13 -5
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/session.js +325 -83
  34. package/dist/session.js.map +1 -1
  35. package/dist/text.d.ts +7 -0
  36. package/dist/text.d.ts.map +1 -1
  37. package/dist/text.js +114 -0
  38. package/dist/text.js.map +1 -1
  39. package/dist/theme.d.ts.map +1 -1
  40. package/dist/theme.js +3 -0
  41. package/dist/theme.js.map +1 -1
  42. package/dist/tree.d.ts.map +1 -1
  43. package/dist/tree.js +18 -4
  44. package/dist/tree.js.map +1 -1
  45. package/dist/types.d.ts +41 -4
  46. package/dist/types.d.ts.map +1 -1
  47. package/docs/api-reference.md +18 -8
  48. package/docs/cookbook.md +1 -1
  49. package/docs/interaction-model.md +10 -8
  50. package/docs/primitive-gallery.md +9 -5
  51. package/examples/basic.tsx +22 -0
  52. package/examples/cli.tsx +55 -0
  53. package/examples/demo.tsx +98 -0
  54. package/examples/docs/background-fill.tsx +107 -0
  55. package/examples/docs/component-composition.tsx +140 -0
  56. package/examples/docs/cursor.tsx +121 -0
  57. package/examples/docs/employees-list.tsx +138 -0
  58. package/examples/docs/hello.tsx +98 -0
  59. package/examples/docs/interactive-note.tsx +111 -0
  60. package/examples/docs/module-api-dashboard.tsx +307 -0
  61. package/examples/docs/module-flux-store.tsx +181 -0
  62. package/examples/docs/module-form-workflow.tsx +339 -0
  63. package/examples/docs/module-forms.tsx +218 -0
  64. package/examples/docs/module-money.tsx +175 -0
  65. package/examples/docs/module-native-store.tsx +188 -0
  66. package/examples/docs/module-pulses.tsx +142 -0
  67. package/examples/docs/module-query.tsx +209 -0
  68. package/examples/docs/module-request.tsx +194 -0
  69. package/examples/docs/module-state-workbench.tsx +283 -0
  70. package/examples/docs/module-tasks.tsx +223 -0
  71. package/examples/docs/module-translate.tsx +194 -0
  72. package/examples/docs/module-utils.tsx +168 -0
  73. package/examples/docs/module-valyrian-core.tsx +159 -0
  74. package/examples/docs/pizza-builder.tsx +463 -0
  75. package/examples/docs/primitive-activity-console.tsx +113 -0
  76. package/examples/docs/primitive-command-panel.tsx +186 -0
  77. package/examples/docs/primitive-data-explorer.tsx +155 -0
  78. package/examples/docs/primitive-input-workbench.tsx +128 -0
  79. package/examples/docs/primitive-layout-shell.tsx +115 -0
  80. package/examples/docs/responsive-split.tsx +186 -0
  81. package/examples/docs/style-system.tsx +209 -0
  82. package/examples/docs/theme-colors.tsx +225 -0
  83. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  84. package/examples/opencode-dogfood-app.tsx +215 -0
  85. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  86. package/examples/opencode-dogfood.tsx +11 -0
  87. package/llms-full.txt +38 -22
  88. package/package.json +3 -2
  89. package/src/ansi.ts +23 -13
  90. package/src/events.ts +6 -2
  91. package/src/frame-style.ts +36 -0
  92. package/src/keymap.ts +4 -2
  93. package/src/layout.ts +59 -25
  94. package/src/mouse.ts +41 -16
  95. package/src/primitives.ts +8 -1
  96. package/src/render.ts +286 -71
  97. package/src/runtime.ts +13 -5
  98. package/src/session.ts +343 -79
  99. package/src/text.ts +148 -0
  100. package/src/theme.ts +3 -0
  101. package/src/tree.ts +19 -4
  102. package/src/types.ts +48 -3
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";
@@ -20,11 +20,17 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
20
20
  "\u001b[13;129u",
21
21
  "\u001b[27;2;13~",
22
22
  "\u001b[13;2~",
23
+ "\u001b[1;2A",
24
+ "\u001b[1;2B",
23
25
  "\u001b[1;2C",
24
26
  "\u001b[1;2D",
25
27
  "\u001b[1;3C",
26
28
  "\u001b[1;3D",
27
29
  "\u001b[3~",
30
+ "\u001b[5~",
31
+ "\u001b[6~",
32
+ "\u001b[1~",
33
+ "\u001b[4~",
28
34
  "\u001b[Z",
29
35
  "\u001b[A",
30
36
  "\u001b[B",
@@ -84,6 +90,7 @@ function resolveRuntimeOptions(options) {
84
90
  stdout,
85
91
  alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
86
92
  hideCursor: options.hideCursor ?? ownsInteractiveTTY,
93
+ mouseReporting: ownsInteractiveTTY,
87
94
  writesAnsi: runtime === "app" && Boolean(stdout)
88
95
  };
89
96
  }
@@ -95,7 +102,7 @@ function resolveTerminalSize(options, stdout) {
95
102
  rows: validateTerminalDimension("rows", rows)
96
103
  };
97
104
  }
98
- function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById) {
105
+ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById) {
99
106
  for (let i = 0; i < nodes.length; i += 1) {
100
107
  const node = nodes[i];
101
108
  if (node.type !== "element") {
@@ -117,7 +124,23 @@ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById
117
124
  node.props.__editorState = current;
118
125
  }
119
126
  if (node.tag === "terminal-list" && id) {
120
- node.props.__selectedIndex = listIndexById.get(id) || 0;
127
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
128
+ const activeIndex = listIndexById.get(id) || 0;
129
+ const clampedActiveIndex = Math.max(0, Math.min(Math.max(0, items.length - 1), activeIndex));
130
+ if (!listIndexById.has(id)) {
131
+ listIndexById.set(id, clampedActiveIndex);
132
+ }
133
+ const selectedIndex = node.props.showActive === false
134
+ ? null
135
+ : listSelectedIndexById.has(id)
136
+ ? listSelectedIndexById.get(id)
137
+ : clampedActiveIndex;
138
+ if (!listSelectedIndexById.has(id)) {
139
+ listSelectedIndexById.set(id, selectedIndex === null ? null : Math.max(0, Math.min(Math.max(0, items.length - 1), selectedIndex)));
140
+ }
141
+ node.props.__activeIndex = clampedActiveIndex;
142
+ node.props.__selectedIndex = selectedIndex === null ? null : Math.max(0, Math.min(Math.max(0, items.length - 1), selectedIndex));
143
+ node.props.__scrollOffset = listViewportOffsetById.get(id) || 0;
121
144
  if (listHoverById.has(id)) {
122
145
  node.props.__hoveredIndex = listHoverById.get(id);
123
146
  }
@@ -128,7 +151,7 @@ function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById
128
151
  node.props.__hoveredRow = scrollHoverRowById.get(id);
129
152
  }
130
153
  }
131
- applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
154
+ applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
132
155
  }
133
156
  }
134
157
  export function mountTerminal(input, options = {}) {
@@ -148,6 +171,8 @@ export function mountTerminal(input, options = {}) {
148
171
  const inputStateById = new Map();
149
172
  const editorStateById = new Map();
150
173
  const listIndexById = new Map();
174
+ const listSelectedIndexById = new Map();
175
+ const listViewportOffsetById = new Map();
151
176
  const scrollOffsetById = new Map();
152
177
  const listHoverById = new Map();
153
178
  const scrollHoverRowById = new Map();
@@ -161,7 +186,7 @@ export function mountTerminal(input, options = {}) {
161
186
  renderNow();
162
187
  });
163
188
  let currentTree = terminalRuntime.project();
164
- applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
189
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
165
190
  let currentFrame = renderTreeFrame(currentTree);
166
191
  let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
167
192
  let currentHitboxes = currentFrame.hitboxes;
@@ -186,6 +211,9 @@ export function mountTerminal(input, options = {}) {
186
211
  if (runtimeOptions.hideCursor) {
187
212
  writes.push(ANSI_HIDE_CURSOR);
188
213
  }
214
+ if (runtimeOptions.mouseReporting) {
215
+ writes.push(ANSI_ENABLE_MOUSE_REPORTING);
216
+ }
189
217
  if (writes.length > 0) {
190
218
  outputWriter.write(writes.join(""), { force: true });
191
219
  }
@@ -195,6 +223,9 @@ export function mountTerminal(input, options = {}) {
195
223
  return;
196
224
  }
197
225
  const writes = [];
226
+ if (runtimeOptions.mouseReporting) {
227
+ writes.push(ANSI_DISABLE_MOUSE_REPORTING);
228
+ }
198
229
  if (runtimeOptions.hideCursor) {
199
230
  writes.push(ANSI_SHOW_CURSOR);
200
231
  }
@@ -223,7 +254,7 @@ export function mountTerminal(input, options = {}) {
223
254
  focusedId = activeFocusables[0]?.props.id || null;
224
255
  }
225
256
  skipFocusContainmentOnce = false;
226
- applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
257
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
227
258
  currentFrame = renderTreeFrame(currentTree);
228
259
  currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
229
260
  currentHitboxes = currentFrame.hitboxes;
@@ -362,10 +393,18 @@ export function mountTerminal(input, options = {}) {
362
393
  return 1;
363
394
  }
364
395
  function sourceRowFromHitbox(node, hitbox, y) {
365
- const visibleRow = Math.max(1, y - hitbox.y1 + 1);
396
+ if (node.tag === "terminal-list" && typeof hitbox.__listItemIndex === "number" && y >= hitbox.y1 && y <= hitbox.y2) {
397
+ return Math.max(1, Math.min(rowCountForNode(node), hitbox.__listItemIndex + 1));
398
+ }
399
+ const sourceY = hitbox.contentY ?? hitbox.y1;
400
+ const visibleRow = Math.max(1, y - sourceY + 1);
366
401
  if (node.tag !== "terminal-list") {
367
402
  return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
368
403
  }
404
+ const mappedIndex = hitbox.itemIndexes?.[visibleRow - 1];
405
+ if (typeof mappedIndex === "number") {
406
+ return Math.max(1, Math.min(rowCountForNode(node), mappedIndex + 1));
407
+ }
369
408
  return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
370
409
  }
371
410
  function shouldPointerCapture(node) {
@@ -406,6 +445,100 @@ export function mountTerminal(input, options = {}) {
406
445
  emitCaptureEvent(next, "capturestart", source, row ?? hoveredRowForNode(next), x, y);
407
446
  }
408
447
  }
448
+ function listItemKey(node, index) {
449
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
450
+ const item = items[index];
451
+ if (typeof node.props.itemKey === "function" && typeof item !== "undefined") {
452
+ const key = node.props.itemKey(item, index);
453
+ if (typeof key === "string" || typeof key === "number") {
454
+ return String(key);
455
+ }
456
+ }
457
+ return undefined;
458
+ }
459
+ function listViewportRows(node) {
460
+ const context = renderContext();
461
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
462
+ const hitbox = node.props.id ? currentHitboxes.find((box) => box.id === node.props.id) : null;
463
+ const overscan = typeof node.props.overscan === "number" ? Math.max(0, Math.floor(node.props.overscan)) : 0;
464
+ const renderedRows = hitbox ? Math.max(1, hitbox.y2 - hitbox.y1 + 1 - overscan * 2) : null;
465
+ const sourceRows = Number(node.props.height || renderedRows || context.rows || items.length || 1);
466
+ if (!Number.isFinite(sourceRows) || !Number.isInteger(sourceRows) || sourceRows <= 0) {
467
+ return Math.max(1, items.length || 1);
468
+ }
469
+ return Math.max(1, Math.min(items.length || 1, sourceRows));
470
+ }
471
+ function clampListIndex(node, index) {
472
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
473
+ if (items.length === 0) {
474
+ return 0;
475
+ }
476
+ return Math.max(0, Math.min(items.length - 1, index));
477
+ }
478
+ function currentListActiveIndex(node) {
479
+ return clampListIndex(node, listIndexById.get(node.props.id || "") || 0);
480
+ }
481
+ function currentListSelectedIndex(node) {
482
+ const id = node.props.id || "";
483
+ if (node.props.showActive === false) {
484
+ return null;
485
+ }
486
+ if (listSelectedIndexById.has(id)) {
487
+ const selectedIndex = listSelectedIndexById.get(id);
488
+ return typeof selectedIndex === "number" ? clampListIndex(node, selectedIndex) : null;
489
+ }
490
+ return currentListActiveIndex(node);
491
+ }
492
+ function currentListViewportOffset(node) {
493
+ const id = node.props.id || "";
494
+ return Math.max(0, Math.min(listMaxViewportOffset(node), listViewportOffsetById.get(id) || 0));
495
+ }
496
+ function listStatePayload(node) {
497
+ return {
498
+ activeIndex: currentListActiveIndex(node),
499
+ selectedIndex: currentListSelectedIndex(node),
500
+ viewportOffset: currentListViewportOffset(node),
501
+ viewportRows: listViewportRows(node)
502
+ };
503
+ }
504
+ function listMaxViewportOffset(node) {
505
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
506
+ return Math.max(0, items.length - listViewportRows(node));
507
+ }
508
+ function setListViewportOffset(node, offset, emit = true) {
509
+ const id = node.props.id;
510
+ if (!id) {
511
+ return;
512
+ }
513
+ const nextOffset = Math.max(0, Math.min(listMaxViewportOffset(node), offset));
514
+ const previous = listViewportOffsetById.get(id) || 0;
515
+ listViewportOffsetById.set(id, nextOffset);
516
+ if (emit && nextOffset !== previous) {
517
+ const payload = {
518
+ type: "viewportchange",
519
+ id,
520
+ offset: nextOffset,
521
+ rows: listViewportRows(node),
522
+ ...listStatePayload(node)
523
+ };
524
+ dispatchNodeEvent(node, "viewportchange", payload);
525
+ }
526
+ }
527
+ function ensureListActiveVisible(node, activeIndex) {
528
+ const id = node.props.id;
529
+ if (!id || !node.props.virtualized) {
530
+ return;
531
+ }
532
+ const currentOffset = listViewportOffsetById.get(id) || 0;
533
+ const rows = listViewportRows(node);
534
+ if (activeIndex < currentOffset) {
535
+ setListViewportOffset(node, activeIndex);
536
+ return;
537
+ }
538
+ if (activeIndex >= currentOffset + rows) {
539
+ setListViewportOffset(node, activeIndex - rows + 1);
540
+ }
541
+ }
409
542
  function dispatchListPressEvent(node, type, index) {
410
543
  const id = node.props.id;
411
544
  if (!id) {
@@ -415,7 +548,10 @@ export function mountTerminal(input, options = {}) {
415
548
  if (typeof items[index] === "undefined") {
416
549
  return false;
417
550
  }
418
- const payload = { type, id, index, value: items[index] };
551
+ const key = listItemKey(node, index);
552
+ const payload = typeof key === "undefined"
553
+ ? { type, id, index, value: items[index], ...listStatePayload(node) }
554
+ : { type, id, index, key, value: items[index], ...listStatePayload(node) };
419
555
  return dispatchNodeEvent(node, type, payload);
420
556
  }
421
557
  function dispatchButtonPressEvent(node, type) {
@@ -425,6 +561,13 @@ export function mountTerminal(input, options = {}) {
425
561
  }
426
562
  return dispatchNodeEvent(node, type, { type, id });
427
563
  }
564
+ function dispatchHitboxButtonPressEvent(hitbox, type) {
565
+ if (type !== "press" || typeof hitbox.__pressHandler !== "function") {
566
+ return false;
567
+ }
568
+ hitbox.__pressHandler({ type, id: hitbox.id });
569
+ return true;
570
+ }
428
571
  function dispatchListPointerPressEvent(node, type, row) {
429
572
  const items = Array.isArray(node.props.items) ? node.props.items : [];
430
573
  const index = Math.max(0, Math.min(items.length - 1, row - 1));
@@ -461,7 +604,10 @@ export function mountTerminal(input, options = {}) {
461
604
  if (typeof items[index] === "undefined") {
462
605
  return;
463
606
  }
464
- const payload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
607
+ const key = listItemKey(node, index);
608
+ const payload = typeof key === "undefined"
609
+ ? { type, id: node.props.id, row: index + 1, index, value: items[index], x, y }
610
+ : { type, id: node.props.id, row: index + 1, index, key, value: items[index], x, y };
465
611
  dispatchNodeEvent(node, type, payload);
466
612
  return;
467
613
  }
@@ -551,28 +697,74 @@ export function mountTerminal(input, options = {}) {
551
697
  setSemanticHoverFromHitbox(hitbox.id, x, y);
552
698
  return rerender();
553
699
  }
554
- function changeListSelection(node, direction) {
700
+ function moveListActiveTo(node, nextIndex) {
555
701
  const id = node.props.id;
556
702
  if (!id) {
557
703
  return currentOutput;
558
704
  }
559
705
  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);
706
+ const clampedIndex = clampListIndex(node, nextIndex);
707
+ listIndexById.set(id, clampedIndex);
708
+ ensureListActiveVisible(node, clampedIndex);
709
+ const key = listItemKey(node, clampedIndex);
710
+ const payload = typeof key === "undefined"
711
+ ? { type: "change", id, index: clampedIndex, value: items[clampedIndex], ...listStatePayload(node) }
712
+ : { type: "change", id, index: clampedIndex, key, value: items[clampedIndex], ...listStatePayload(node) };
713
+ dispatchNodeEvent(node, "change", payload);
714
+ return rerender();
715
+ }
716
+ function changeListSelection(node, direction) {
717
+ return moveListActiveTo(node, currentListActiveIndex(node) + direction);
718
+ }
719
+ function pageListSelection(node, direction) {
720
+ const id = node.props.id;
721
+ if (!id) {
722
+ return currentOutput;
723
+ }
724
+ const nextIndex = clampListIndex(node, currentListActiveIndex(node) + direction * listViewportRows(node));
562
725
  listIndexById.set(id, nextIndex);
563
- const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
726
+ setListViewportOffset(node, nextIndex);
727
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
728
+ const key = listItemKey(node, nextIndex);
729
+ const payload = typeof key === "undefined"
730
+ ? { type: "change", id, index: nextIndex, value: items[nextIndex], ...listStatePayload(node) }
731
+ : { type: "change", id, index: nextIndex, key, value: items[nextIndex], ...listStatePayload(node) };
564
732
  dispatchNodeEvent(node, "change", payload);
565
733
  return rerender();
566
734
  }
735
+ function moveListSelectionToBoundary(node, boundary) {
736
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
737
+ return moveListActiveTo(node, boundary === "start" ? 0 : Math.max(0, items.length - 1));
738
+ }
567
739
  function pressListSelection(node) {
568
740
  const id = node.props.id;
569
741
  if (!id) {
570
742
  return currentOutput;
571
743
  }
572
- const currentIndex = listIndexById.get(id) || 0;
744
+ const currentIndex = currentListActiveIndex(node);
745
+ if (node.props.showActive !== false) {
746
+ listSelectedIndexById.set(id, currentIndex);
747
+ }
573
748
  dispatchListPressEvent(node, "press", currentIndex);
574
749
  return rerender();
575
750
  }
751
+ function pressListPointerSelection(node, row) {
752
+ const id = node.props.id;
753
+ if (!id) {
754
+ return currentOutput;
755
+ }
756
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
757
+ const index = Math.max(0, Math.min(items.length - 1, row - 1));
758
+ if (typeof items[index] === "undefined") {
759
+ return currentOutput;
760
+ }
761
+ listIndexById.set(id, index);
762
+ if (node.props.showActive !== false) {
763
+ listSelectedIndexById.set(id, index);
764
+ }
765
+ dispatchListPressEvent(node, "press", index);
766
+ return rerender();
767
+ }
576
768
  function scrollFocusedNode(node, direction) {
577
769
  const id = node.props.id;
578
770
  if (!id) {
@@ -597,7 +789,9 @@ export function mountTerminal(input, options = {}) {
597
789
  return scrollFocusedNode(node, direction);
598
790
  }
599
791
  if (node?.tag === "terminal-list" && node.props.virtualized) {
600
- return changeListSelection(node, direction);
792
+ const currentOffset = listViewportOffsetById.get(node.props.id || "") || 0;
793
+ setListViewportOffset(node, currentOffset + direction);
794
+ return rerender();
601
795
  }
602
796
  return rerender();
603
797
  }
@@ -763,6 +957,14 @@ export function mountTerminal(input, options = {}) {
763
957
  return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
764
958
  case "list.next":
765
959
  return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
960
+ case "list.pageUp":
961
+ return node?.tag === "terminal-list" ? pageListSelection(node, -1) : currentOutput;
962
+ case "list.pageDown":
963
+ return node?.tag === "terminal-list" ? pageListSelection(node, 1) : currentOutput;
964
+ case "list.home":
965
+ return node?.tag === "terminal-list" ? moveListSelectionToBoundary(node, "start") : currentOutput;
966
+ case "list.end":
967
+ return node?.tag === "terminal-list" ? moveListSelectionToBoundary(node, "end") : currentOutput;
766
968
  case "list.press":
767
969
  return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
768
970
  case "scroll.up":
@@ -798,6 +1000,19 @@ export function mountTerminal(input, options = {}) {
798
1000
  return;
799
1001
  }
800
1002
  terminalSize = nextSize;
1003
+ const focusedNode = findFocused(currentTree, focusedId);
1004
+ if (focusedNode?.tag === "terminal-list" && focusedNode.props.id && focusedNode.props.virtualized) {
1005
+ const items = Array.isArray(focusedNode.props.items) ? focusedNode.props.items : [];
1006
+ const rows = Math.max(1, Math.min(items.length || 1, Number(focusedNode.props.height || terminalSize.rows || items.length || 1)));
1007
+ const activeIndex = currentListActiveIndex(focusedNode);
1008
+ const currentOffset = listViewportOffsetById.get(focusedNode.props.id) || 0;
1009
+ if (activeIndex < currentOffset) {
1010
+ listViewportOffsetById.set(focusedNode.props.id, activeIndex);
1011
+ }
1012
+ else if (activeIndex >= currentOffset + rows) {
1013
+ listViewportOffsetById.set(focusedNode.props.id, Math.max(0, activeIndex - rows + 1));
1014
+ }
1015
+ }
801
1016
  rerender();
802
1017
  },
803
1018
  update() {
@@ -888,7 +1103,14 @@ export function mountTerminal(input, options = {}) {
888
1103
  return currentOutput;
889
1104
  }
890
1105
  if (hitbox.tag === "terminal-button") {
891
- return this.click(hitbox.id);
1106
+ const node = findFocusableById(currentTree, hitbox.id);
1107
+ if (node?.tag === "terminal-button") {
1108
+ return this.click(hitbox.id);
1109
+ }
1110
+ if (dispatchHitboxButtonPressEvent(hitbox, "press")) {
1111
+ return rerender();
1112
+ }
1113
+ return currentOutput;
892
1114
  }
893
1115
  setSemanticHoverFromHitbox(hitbox.id, x, y);
894
1116
  focusedId = hitbox.id;
@@ -896,6 +1118,12 @@ export function mountTerminal(input, options = {}) {
896
1118
  mouseSelectionId = hitbox.id;
897
1119
  return setCursorFromHitbox(hitbox.id, x, false);
898
1120
  }
1121
+ if (hitbox.tag === "terminal-list") {
1122
+ const node = findFocusableById(currentTree, hitbox.id);
1123
+ if (node?.tag === "terminal-list") {
1124
+ return pressListPointerSelection(node, sourceRowFromHitbox(node, hitbox, y));
1125
+ }
1126
+ }
899
1127
  rerender();
900
1128
  return currentOutput;
901
1129
  },
@@ -1082,6 +1310,79 @@ export function mountTerminal(input, options = {}) {
1082
1310
  }
1083
1311
  return currentOutput;
1084
1312
  }
1313
+ function processParsedMouseInput(parsed) {
1314
+ if (parsed.action === "press") {
1315
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1316
+ if (isContextMouseButton(parsed.button)) {
1317
+ lastPrimaryPress = null;
1318
+ contextPressAt(parsed.x, parsed.y);
1319
+ return;
1320
+ }
1321
+ if (hitbox?.tag === "terminal-input") {
1322
+ mouseSelectionId = hitbox.id;
1323
+ }
1324
+ const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
1325
+ if (hitbox && node && shouldPointerCapture(node)) {
1326
+ setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
1327
+ }
1328
+ const isPrimaryPress = isPrimaryMouseButton(parsed.button);
1329
+ const isDoublePressEligible = Boolean(hitbox
1330
+ && node
1331
+ && (hitbox.tag === "terminal-button" || hitbox.tag === "terminal-list"));
1332
+ const primaryPressRow = hitbox && node && hitbox.tag === "terminal-list"
1333
+ ? sourceRowFromHitbox(node, hitbox, parsed.y)
1334
+ : null;
1335
+ const shouldDispatchDoublePress = Boolean(isPrimaryPress
1336
+ && isDoublePressEligible
1337
+ && hitbox
1338
+ && isDoublePrimaryPress(hitbox, primaryPressRow));
1339
+ if (isPrimaryPress && !isDoublePressEligible) {
1340
+ lastPrimaryPress = null;
1341
+ }
1342
+ if (hitbox && shouldDispatchDoublePress && hitbox.tag === "terminal-list") {
1343
+ doublePressAt(hitbox, parsed.x, parsed.y);
1344
+ }
1345
+ else {
1346
+ session.clickAt(parsed.x, parsed.y);
1347
+ if (hitbox && shouldDispatchDoublePress) {
1348
+ doublePressAt(hitbox, parsed.x, parsed.y);
1349
+ }
1350
+ }
1351
+ }
1352
+ else if (parsed.action === "drag") {
1353
+ if (mouseSelectionId) {
1354
+ setCursorFromHitbox(mouseSelectionId, parsed.x, true);
1355
+ }
1356
+ else {
1357
+ hoverAt(parsed.x, parsed.y);
1358
+ }
1359
+ }
1360
+ else if (parsed.action === "release") {
1361
+ mouseSelectionId = null;
1362
+ const capturedId = pointerCaptureId;
1363
+ const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1364
+ const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
1365
+ const releaseRow = releaseHitbox && releaseNode
1366
+ ? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
1367
+ : null;
1368
+ setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
1369
+ const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1370
+ if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
1371
+ setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
1372
+ rerender();
1373
+ }
1374
+ else {
1375
+ clearSemanticHover(undefined, parsed.x, parsed.y);
1376
+ rerender();
1377
+ }
1378
+ }
1379
+ else if (parsed.action === "wheel-up") {
1380
+ wheelAt(parsed.x, parsed.y, -1);
1381
+ }
1382
+ else if (parsed.action === "wheel-down") {
1383
+ wheelAt(parsed.x, parsed.y, 1);
1384
+ }
1385
+ }
1085
1386
  function processInputStream(value) {
1086
1387
  if (!value) {
1087
1388
  return;
@@ -1133,74 +1434,15 @@ export function mountTerminal(input, options = {}) {
1133
1434
  processInputStream(paste.rest);
1134
1435
  return;
1135
1436
  }
1437
+ const parsedMouse = parseTerminalMousePrefix(value);
1438
+ if (parsedMouse) {
1439
+ processParsedMouseInput(parsedMouse.input);
1440
+ processInputStream(parsedMouse.rest);
1441
+ return;
1442
+ }
1136
1443
  const parsed = parseTerminalInput(value);
1137
1444
  if (parsed.type === "mouse") {
1138
- if (parsed.action === "press") {
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
- }
1145
- if (hitbox?.tag === "terminal-input") {
1146
- mouseSelectionId = hitbox.id;
1147
- }
1148
- const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
1149
- if (hitbox && node && shouldPointerCapture(node)) {
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;
1165
- }
1166
- session.clickAt(parsed.x, parsed.y);
1167
- if (hitbox && shouldDispatchDoublePress) {
1168
- doublePressAt(hitbox, parsed.x, parsed.y);
1169
- }
1170
- }
1171
- else if (parsed.action === "drag") {
1172
- if (mouseSelectionId) {
1173
- setCursorFromHitbox(mouseSelectionId, parsed.x, true);
1174
- }
1175
- else {
1176
- hoverAt(parsed.x, parsed.y);
1177
- }
1178
- }
1179
- else if (parsed.action === "release") {
1180
- mouseSelectionId = null;
1181
- const capturedId = pointerCaptureId;
1182
- const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1183
- const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
1184
- const releaseRow = releaseHitbox && releaseNode
1185
- ? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
1186
- : null;
1187
- setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
1188
- const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1189
- if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
1190
- setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
1191
- rerender();
1192
- }
1193
- else {
1194
- clearSemanticHover(undefined, parsed.x, parsed.y);
1195
- rerender();
1196
- }
1197
- }
1198
- else if (parsed.action === "wheel-up") {
1199
- wheelAt(parsed.x, parsed.y, -1);
1200
- }
1201
- else if (parsed.action === "wheel-down") {
1202
- wheelAt(parsed.x, parsed.y, 1);
1203
- }
1445
+ processParsedMouseInput(parsed);
1204
1446
  return;
1205
1447
  }
1206
1448
  processKeyStream(value);