@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/src/session.ts 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";
@@ -27,9 +27,11 @@ import type {
27
27
  TerminalHitbox,
28
28
  TerminalListChangeEventPayload,
29
29
  TerminalListPressEventPayload,
30
+ TerminalListViewportChangeEventPayload,
30
31
  TerminalMountOptions,
31
32
  TerminalOutputStream,
32
33
  TerminalNode,
34
+ ParsedTerminalInput,
33
35
  TerminalPointerSource,
34
36
  TerminalRowPointerEventPayload,
35
37
  TerminalSession,
@@ -44,6 +46,7 @@ interface ResolvedRuntimeOptions {
44
46
  stdout?: TerminalOutputStream;
45
47
  alternateScreen: boolean;
46
48
  hideCursor: boolean;
49
+ mouseReporting: boolean;
47
50
  writesAnsi: boolean;
48
51
  }
49
52
 
@@ -55,11 +58,17 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
55
58
  "\u001b[13;129u",
56
59
  "\u001b[27;2;13~",
57
60
  "\u001b[13;2~",
61
+ "\u001b[1;2A",
62
+ "\u001b[1;2B",
58
63
  "\u001b[1;2C",
59
64
  "\u001b[1;2D",
60
65
  "\u001b[1;3C",
61
66
  "\u001b[1;3D",
62
67
  "\u001b[3~",
68
+ "\u001b[5~",
69
+ "\u001b[6~",
70
+ "\u001b[1~",
71
+ "\u001b[4~",
63
72
  "\u001b[Z",
64
73
  "\u001b[A",
65
74
  "\u001b[B",
@@ -131,6 +140,7 @@ function resolveRuntimeOptions(options: TerminalMountOptions): ResolvedRuntimeOp
131
140
  stdout,
132
141
  alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
133
142
  hideCursor: options.hideCursor ?? ownsInteractiveTTY,
143
+ mouseReporting: ownsInteractiveTTY,
134
144
  writesAnsi: runtime === "app" && Boolean(stdout)
135
145
  };
136
146
  }
@@ -150,6 +160,8 @@ function applyInteractiveState(
150
160
  inputStateById: Map<string, InputInteractionState>,
151
161
  editorStateById: Map<string, EditorState>,
152
162
  listIndexById: Map<string, number>,
163
+ listSelectedIndexById: Map<string, number | null>,
164
+ listViewportOffsetById: Map<string, number>,
153
165
  scrollOffsetById: Map<string, number>,
154
166
  listHoverById: Map<string, number>,
155
167
  scrollHoverRowById: Map<string, number>
@@ -176,7 +188,23 @@ function applyInteractiveState(
176
188
  node.props.__editorState = current;
177
189
  }
178
190
  if (node.tag === "terminal-list" && id) {
179
- node.props.__selectedIndex = listIndexById.get(id) || 0;
191
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
192
+ const activeIndex = listIndexById.get(id) || 0;
193
+ const clampedActiveIndex = Math.max(0, Math.min(Math.max(0, items.length - 1), activeIndex));
194
+ if (!listIndexById.has(id)) {
195
+ listIndexById.set(id, clampedActiveIndex);
196
+ }
197
+ const selectedIndex = node.props.showActive === false
198
+ ? null
199
+ : listSelectedIndexById.has(id)
200
+ ? listSelectedIndexById.get(id)!
201
+ : clampedActiveIndex;
202
+ if (!listSelectedIndexById.has(id)) {
203
+ listSelectedIndexById.set(id, selectedIndex === null ? null : Math.max(0, Math.min(Math.max(0, items.length - 1), selectedIndex)));
204
+ }
205
+ node.props.__activeIndex = clampedActiveIndex;
206
+ node.props.__selectedIndex = selectedIndex === null ? null : Math.max(0, Math.min(Math.max(0, items.length - 1), selectedIndex));
207
+ node.props.__scrollOffset = listViewportOffsetById.get(id) || 0;
180
208
  if (listHoverById.has(id)) {
181
209
  node.props.__hoveredIndex = listHoverById.get(id);
182
210
  }
@@ -187,7 +215,7 @@ function applyInteractiveState(
187
215
  node.props.__hoveredRow = scrollHoverRowById.get(id);
188
216
  }
189
217
  }
190
- applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
218
+ applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
191
219
  }
192
220
  }
193
221
 
@@ -208,6 +236,8 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
208
236
  const inputStateById = new Map<string, InputInteractionState>();
209
237
  const editorStateById = new Map<string, EditorState>();
210
238
  const listIndexById = new Map<string, number>();
239
+ const listSelectedIndexById = new Map<string, number | null>();
240
+ const listViewportOffsetById = new Map<string, number>();
211
241
  const scrollOffsetById = new Map<string, number>();
212
242
  const listHoverById = new Map<string, number>();
213
243
  const scrollHoverRowById = new Map<string, number>();
@@ -221,7 +251,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
221
251
  renderNow();
222
252
  });
223
253
  let currentTree = terminalRuntime.project();
224
- applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
254
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
225
255
  let currentFrame = renderTreeFrame(currentTree);
226
256
  let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
227
257
  let currentHitboxes = currentFrame.hitboxes;
@@ -250,6 +280,9 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
250
280
  if (runtimeOptions.hideCursor) {
251
281
  writes.push(ANSI_HIDE_CURSOR);
252
282
  }
283
+ if (runtimeOptions.mouseReporting) {
284
+ writes.push(ANSI_ENABLE_MOUSE_REPORTING);
285
+ }
253
286
  if (writes.length > 0) {
254
287
  outputWriter.write(writes.join(""), { force: true });
255
288
  }
@@ -260,6 +293,9 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
260
293
  return;
261
294
  }
262
295
  const writes: string[] = [];
296
+ if (runtimeOptions.mouseReporting) {
297
+ writes.push(ANSI_DISABLE_MOUSE_REPORTING);
298
+ }
263
299
  if (runtimeOptions.hideCursor) {
264
300
  writes.push(ANSI_SHOW_CURSOR);
265
301
  }
@@ -290,7 +326,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
290
326
  focusedId = activeFocusables[0]?.props.id || null;
291
327
  }
292
328
  skipFocusContainmentOnce = false;
293
- applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
329
+ applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
294
330
  currentFrame = renderTreeFrame(currentTree);
295
331
  currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
296
332
  currentHitboxes = currentFrame.hitboxes;
@@ -444,12 +480,22 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
444
480
  return 1;
445
481
  }
446
482
 
447
- function sourceRowFromHitbox(node: TerminalFocusNode, hitbox: { itemOffset?: number; y1: number }, y: number) {
448
- const visibleRow = Math.max(1, y - hitbox.y1 + 1);
483
+ function sourceRowFromHitbox(node: TerminalFocusNode, hitbox: { itemOffset?: number; itemIndexes?: number[]; y1: number; y2: number; contentY?: number; __listItemIndex?: number }, y: number) {
484
+ if (node.tag === "terminal-list" && typeof hitbox.__listItemIndex === "number" && y >= hitbox.y1 && y <= hitbox.y2) {
485
+ return Math.max(1, Math.min(rowCountForNode(node), hitbox.__listItemIndex + 1));
486
+ }
487
+
488
+ const sourceY = hitbox.contentY ?? hitbox.y1;
489
+ const visibleRow = Math.max(1, y - sourceY + 1);
449
490
  if (node.tag !== "terminal-list") {
450
491
  return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
451
492
  }
452
493
 
494
+ const mappedIndex = hitbox.itemIndexes?.[visibleRow - 1];
495
+ if (typeof mappedIndex === "number") {
496
+ return Math.max(1, Math.min(rowCountForNode(node), mappedIndex + 1));
497
+ }
498
+
453
499
  return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
454
500
  }
455
501
 
@@ -496,6 +542,110 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
496
542
  }
497
543
 
498
544
 
545
+ function listItemKey(node: TerminalFocusNode, index: number) {
546
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
547
+ const item = items[index];
548
+ if (typeof node.props.itemKey === "function" && typeof item !== "undefined") {
549
+ const key = node.props.itemKey(item, index);
550
+ if (typeof key === "string" || typeof key === "number") {
551
+ return String(key);
552
+ }
553
+ }
554
+ return undefined;
555
+ }
556
+
557
+ function listViewportRows(node: TerminalFocusNode) {
558
+ const context = renderContext();
559
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
560
+ const hitbox = node.props.id ? currentHitboxes.find((box) => box.id === node.props.id) : null;
561
+ const overscan = typeof node.props.overscan === "number" ? Math.max(0, Math.floor(node.props.overscan)) : 0;
562
+ const renderedRows = hitbox ? Math.max(1, hitbox.y2 - hitbox.y1 + 1 - overscan * 2) : null;
563
+ const sourceRows = Number(node.props.height || renderedRows || context.rows || items.length || 1);
564
+ if (!Number.isFinite(sourceRows) || !Number.isInteger(sourceRows) || sourceRows <= 0) {
565
+ return Math.max(1, items.length || 1);
566
+ }
567
+ return Math.max(1, Math.min(items.length || 1, sourceRows));
568
+ }
569
+
570
+ function clampListIndex(node: TerminalFocusNode, index: number) {
571
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
572
+ if (items.length === 0) {
573
+ return 0;
574
+ }
575
+ return Math.max(0, Math.min(items.length - 1, index));
576
+ }
577
+
578
+ function currentListActiveIndex(node: TerminalFocusNode) {
579
+ return clampListIndex(node, listIndexById.get(node.props.id || "") || 0);
580
+ }
581
+
582
+ function currentListSelectedIndex(node: TerminalFocusNode) {
583
+ const id = node.props.id || "";
584
+ if (node.props.showActive === false) {
585
+ return null;
586
+ }
587
+ if (listSelectedIndexById.has(id)) {
588
+ const selectedIndex = listSelectedIndexById.get(id);
589
+ return typeof selectedIndex === "number" ? clampListIndex(node, selectedIndex) : null;
590
+ }
591
+ return currentListActiveIndex(node);
592
+ }
593
+
594
+ function currentListViewportOffset(node: TerminalFocusNode) {
595
+ const id = node.props.id || "";
596
+ return Math.max(0, Math.min(listMaxViewportOffset(node), listViewportOffsetById.get(id) || 0));
597
+ }
598
+
599
+ function listStatePayload(node: TerminalFocusNode) {
600
+ return {
601
+ activeIndex: currentListActiveIndex(node),
602
+ selectedIndex: currentListSelectedIndex(node),
603
+ viewportOffset: currentListViewportOffset(node),
604
+ viewportRows: listViewportRows(node)
605
+ };
606
+ }
607
+
608
+ function listMaxViewportOffset(node: TerminalFocusNode) {
609
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
610
+ return Math.max(0, items.length - listViewportRows(node));
611
+ }
612
+
613
+ function setListViewportOffset(node: TerminalFocusNode, offset: number, emit = true) {
614
+ const id = node.props.id;
615
+ if (!id) {
616
+ return;
617
+ }
618
+ const nextOffset = Math.max(0, Math.min(listMaxViewportOffset(node), offset));
619
+ const previous = listViewportOffsetById.get(id) || 0;
620
+ listViewportOffsetById.set(id, nextOffset);
621
+ if (emit && nextOffset !== previous) {
622
+ const payload: TerminalListViewportChangeEventPayload = {
623
+ type: "viewportchange",
624
+ id,
625
+ offset: nextOffset,
626
+ rows: listViewportRows(node),
627
+ ...listStatePayload(node)
628
+ };
629
+ dispatchNodeEvent(node, "viewportchange", payload);
630
+ }
631
+ }
632
+
633
+ function ensureListActiveVisible(node: TerminalFocusNode, activeIndex: number) {
634
+ const id = node.props.id;
635
+ if (!id || !node.props.virtualized) {
636
+ return;
637
+ }
638
+ const currentOffset = listViewportOffsetById.get(id) || 0;
639
+ const rows = listViewportRows(node);
640
+ if (activeIndex < currentOffset) {
641
+ setListViewportOffset(node, activeIndex);
642
+ return;
643
+ }
644
+ if (activeIndex >= currentOffset + rows) {
645
+ setListViewportOffset(node, activeIndex - rows + 1);
646
+ }
647
+ }
648
+
499
649
  function dispatchListPressEvent(node: TerminalFocusNode, type: TerminalListPressEventPayload["type"], index: number) {
500
650
  const id = node.props.id;
501
651
  if (!id) {
@@ -505,7 +655,10 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
505
655
  if (typeof items[index] === "undefined") {
506
656
  return false;
507
657
  }
508
- const payload: TerminalListPressEventPayload = { type, id, index, value: items[index] };
658
+ const key = listItemKey(node, index);
659
+ const payload: TerminalListPressEventPayload = typeof key === "undefined"
660
+ ? { type, id, index, value: items[index], ...listStatePayload(node) }
661
+ : { type, id, index, key, value: items[index], ...listStatePayload(node) };
509
662
  return dispatchNodeEvent(node, type, payload);
510
663
  }
511
664
 
@@ -517,6 +670,14 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
517
670
  return dispatchNodeEvent(node, type, { type, id });
518
671
  }
519
672
 
673
+ function dispatchHitboxButtonPressEvent(hitbox: TerminalHitbox, type: "press" | "doublepress" | "contextpress") {
674
+ if (type !== "press" || typeof hitbox.__pressHandler !== "function") {
675
+ return false;
676
+ }
677
+ hitbox.__pressHandler({ type, id: hitbox.id });
678
+ return true;
679
+ }
680
+
520
681
  function dispatchListPointerPressEvent(node: TerminalFocusNode, type: "doublepress" | "contextpress", row: number) {
521
682
  const items = Array.isArray(node.props.items) ? node.props.items : [];
522
683
  const index = Math.max(0, Math.min(items.length - 1, row - 1));
@@ -557,7 +718,10 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
557
718
  if (typeof items[index] === "undefined") {
558
719
  return;
559
720
  }
560
- const payload: TerminalRowPointerEventPayload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
721
+ const key = listItemKey(node, index);
722
+ const payload: TerminalRowPointerEventPayload = typeof key === "undefined"
723
+ ? { type, id: node.props.id, row: index + 1, index, value: items[index], x, y }
724
+ : { type, id: node.props.id, row: index + 1, index, key, value: items[index], x, y };
561
725
  dispatchNodeEvent(node, type, payload);
562
726
  return;
563
727
  }
@@ -656,30 +820,80 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
656
820
  return rerender();
657
821
  }
658
822
 
659
- function changeListSelection(node: TerminalFocusNode, direction: -1 | 1) {
823
+ function moveListActiveTo(node: TerminalFocusNode, nextIndex: number) {
660
824
  const id = node.props.id;
661
825
  if (!id) {
662
826
  return currentOutput;
663
827
  }
664
828
  const items = Array.isArray(node.props.items) ? node.props.items : [];
665
- const currentIndex = listIndexById.get(id) || 0;
666
- const nextIndex = direction < 0 ? Math.max(0, currentIndex - 1) : Math.min(items.length - 1, currentIndex + 1);
829
+ const clampedIndex = clampListIndex(node, nextIndex);
830
+ listIndexById.set(id, clampedIndex);
831
+ ensureListActiveVisible(node, clampedIndex);
832
+ const key = listItemKey(node, clampedIndex);
833
+ const payload: TerminalListChangeEventPayload = typeof key === "undefined"
834
+ ? { type: "change", id, index: clampedIndex, value: items[clampedIndex], ...listStatePayload(node) }
835
+ : { type: "change", id, index: clampedIndex, key, value: items[clampedIndex], ...listStatePayload(node) };
836
+ dispatchNodeEvent(node, "change", payload);
837
+ return rerender();
838
+ }
839
+
840
+ function changeListSelection(node: TerminalFocusNode, direction: -1 | 1) {
841
+ return moveListActiveTo(node, currentListActiveIndex(node) + direction);
842
+ }
843
+
844
+ function pageListSelection(node: TerminalFocusNode, direction: -1 | 1) {
845
+ const id = node.props.id;
846
+ if (!id) {
847
+ return currentOutput;
848
+ }
849
+ const nextIndex = clampListIndex(node, currentListActiveIndex(node) + direction * listViewportRows(node));
667
850
  listIndexById.set(id, nextIndex);
668
- const payload: TerminalListChangeEventPayload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
851
+ setListViewportOffset(node, nextIndex);
852
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
853
+ const key = listItemKey(node, nextIndex);
854
+ const payload: TerminalListChangeEventPayload = typeof key === "undefined"
855
+ ? { type: "change", id, index: nextIndex, value: items[nextIndex], ...listStatePayload(node) }
856
+ : { type: "change", id, index: nextIndex, key, value: items[nextIndex], ...listStatePayload(node) };
669
857
  dispatchNodeEvent(node, "change", payload);
670
858
  return rerender();
671
859
  }
672
860
 
861
+ function moveListSelectionToBoundary(node: TerminalFocusNode, boundary: "start" | "end") {
862
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
863
+ return moveListActiveTo(node, boundary === "start" ? 0 : Math.max(0, items.length - 1));
864
+ }
865
+
673
866
  function pressListSelection(node: TerminalFocusNode) {
674
867
  const id = node.props.id;
675
868
  if (!id) {
676
869
  return currentOutput;
677
870
  }
678
- const currentIndex = listIndexById.get(id) || 0;
871
+ const currentIndex = currentListActiveIndex(node);
872
+ if (node.props.showActive !== false) {
873
+ listSelectedIndexById.set(id, currentIndex);
874
+ }
679
875
  dispatchListPressEvent(node, "press", currentIndex);
680
876
  return rerender();
681
877
  }
682
878
 
879
+ function pressListPointerSelection(node: TerminalFocusNode, row: number) {
880
+ const id = node.props.id;
881
+ if (!id) {
882
+ return currentOutput;
883
+ }
884
+ const items = Array.isArray(node.props.items) ? node.props.items : [];
885
+ const index = Math.max(0, Math.min(items.length - 1, row - 1));
886
+ if (typeof items[index] === "undefined") {
887
+ return currentOutput;
888
+ }
889
+ listIndexById.set(id, index);
890
+ if (node.props.showActive !== false) {
891
+ listSelectedIndexById.set(id, index);
892
+ }
893
+ dispatchListPressEvent(node, "press", index);
894
+ return rerender();
895
+ }
896
+
683
897
  function scrollFocusedNode(node: TerminalFocusNode, direction: -1 | 1) {
684
898
  const id = node.props.id;
685
899
  if (!id) {
@@ -706,7 +920,9 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
706
920
  return scrollFocusedNode(node, direction);
707
921
  }
708
922
  if (node?.tag === "terminal-list" && node.props.virtualized) {
709
- return changeListSelection(node, direction);
923
+ const currentOffset = listViewportOffsetById.get(node.props.id || "") || 0;
924
+ setListViewportOffset(node, currentOffset + direction);
925
+ return rerender();
710
926
  }
711
927
  return rerender();
712
928
  }
@@ -881,6 +1097,14 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
881
1097
  return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
882
1098
  case "list.next":
883
1099
  return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
1100
+ case "list.pageUp":
1101
+ return node?.tag === "terminal-list" ? pageListSelection(node, -1) : currentOutput;
1102
+ case "list.pageDown":
1103
+ return node?.tag === "terminal-list" ? pageListSelection(node, 1) : currentOutput;
1104
+ case "list.home":
1105
+ return node?.tag === "terminal-list" ? moveListSelectionToBoundary(node, "start") : currentOutput;
1106
+ case "list.end":
1107
+ return node?.tag === "terminal-list" ? moveListSelectionToBoundary(node, "end") : currentOutput;
884
1108
  case "list.press":
885
1109
  return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
886
1110
  case "scroll.up":
@@ -917,6 +1141,18 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
917
1141
  return;
918
1142
  }
919
1143
  terminalSize = nextSize;
1144
+ const focusedNode = findFocused(currentTree, focusedId);
1145
+ if (focusedNode?.tag === "terminal-list" && focusedNode.props.id && focusedNode.props.virtualized) {
1146
+ const items = Array.isArray(focusedNode.props.items) ? focusedNode.props.items : [];
1147
+ const rows = Math.max(1, Math.min(items.length || 1, Number(focusedNode.props.height || terminalSize.rows || items.length || 1)));
1148
+ const activeIndex = currentListActiveIndex(focusedNode);
1149
+ const currentOffset = listViewportOffsetById.get(focusedNode.props.id) || 0;
1150
+ if (activeIndex < currentOffset) {
1151
+ listViewportOffsetById.set(focusedNode.props.id, activeIndex);
1152
+ } else if (activeIndex >= currentOffset + rows) {
1153
+ listViewportOffsetById.set(focusedNode.props.id, Math.max(0, activeIndex - rows + 1));
1154
+ }
1155
+ }
920
1156
  rerender();
921
1157
  },
922
1158
  update() {
@@ -1007,7 +1243,14 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
1007
1243
  return currentOutput;
1008
1244
  }
1009
1245
  if (hitbox.tag === "terminal-button") {
1010
- return this.click(hitbox.id);
1246
+ const node = findFocusableById(currentTree, hitbox.id);
1247
+ if (node?.tag === "terminal-button") {
1248
+ return this.click(hitbox.id);
1249
+ }
1250
+ if (dispatchHitboxButtonPressEvent(hitbox, "press")) {
1251
+ return rerender();
1252
+ }
1253
+ return currentOutput;
1011
1254
  }
1012
1255
  setSemanticHoverFromHitbox(hitbox.id, x, y);
1013
1256
  focusedId = hitbox.id;
@@ -1015,6 +1258,12 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
1015
1258
  mouseSelectionId = hitbox.id;
1016
1259
  return setCursorFromHitbox(hitbox.id, x, false);
1017
1260
  }
1261
+ if (hitbox.tag === "terminal-list") {
1262
+ const node = findFocusableById(currentTree, hitbox.id);
1263
+ if (node?.tag === "terminal-list") {
1264
+ return pressListPointerSelection(node, sourceRowFromHitbox(node, hitbox, y));
1265
+ }
1266
+ }
1018
1267
  rerender();
1019
1268
  return currentOutput;
1020
1269
  },
@@ -1173,7 +1422,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
1173
1422
  return isDouble;
1174
1423
  }
1175
1424
 
1176
- function doublePressAt(hitbox: { id: string; tag: string; y1: number }, x: number, y: number) {
1425
+ function doublePressAt(hitbox: TerminalHitbox, x: number, y: number) {
1177
1426
  const node = findFocusableById(currentTree, hitbox.id);
1178
1427
  if (!node) {
1179
1428
  return currentOutput;
@@ -1222,65 +1471,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
1222
1471
  return currentOutput;
1223
1472
  }
1224
1473
 
1225
- function processInputStream(value: string) {
1226
- if (!value) {
1227
- return;
1228
- }
1229
-
1230
- if (pendingPasteChunk) {
1231
- pendingPasteChunk += value;
1232
- const paste = parseBracketedPaste(pendingPasteChunk);
1233
- if (!paste && isBracketedPasteStartPrefix(pendingPasteChunk)) {
1234
- return;
1235
- }
1236
- if (!paste) {
1237
- const buffered = pendingPasteChunk;
1238
- pendingPasteChunk = "";
1239
- processKeyStream(buffered);
1240
- return;
1241
- }
1242
- pendingPasteChunk = "";
1243
- dispatchPasteText(paste.text);
1244
- processInputStream(paste.rest);
1245
- return;
1246
- }
1247
-
1248
- if (pendingKeyChunk) {
1249
- if (pendingKeyChunk === ESCAPE && !canContinueEscapeSequence(value)) {
1250
- flushPendingEscape();
1251
- processInputStream(value);
1252
- return;
1253
- }
1254
- const buffered = pendingKeyChunk + value;
1255
- cancelPendingEscapeFlush();
1256
- pendingKeyChunk = "";
1257
- processKeyStream(buffered);
1258
- return;
1259
- }
1260
-
1261
- if (value === ESCAPE) {
1262
- processKeyStream(value);
1263
- return;
1264
- }
1265
-
1266
- if (isBracketedPasteStartPrefix(value)) {
1267
- pendingPasteChunk = value;
1268
- return;
1269
- }
1270
-
1271
- if (value.startsWith(BRACKETED_PASTE_START)) {
1272
- const paste = parseBracketedPaste(value);
1273
- if (!paste) {
1274
- pendingPasteChunk = value;
1275
- return;
1276
- }
1277
- dispatchPasteText(paste.text);
1278
- processInputStream(paste.rest);
1279
- return;
1280
- }
1281
-
1282
- const parsed = parseTerminalInput(value);
1283
- if (parsed.type === "mouse") {
1474
+ function processParsedMouseInput(parsed: Extract<ParsedTerminalInput, { type: "mouse" }>) {
1284
1475
  if (parsed.action === "press") {
1285
1476
  const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
1286
1477
  if (isContextMouseButton(parsed.button)) {
@@ -1313,9 +1504,13 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
1313
1504
  if (isPrimaryPress && !isDoublePressEligible) {
1314
1505
  lastPrimaryPress = null;
1315
1506
  }
1316
- session.clickAt(parsed.x, parsed.y);
1317
- if (hitbox && shouldDispatchDoublePress) {
1507
+ if (hitbox && shouldDispatchDoublePress && hitbox.tag === "terminal-list") {
1318
1508
  doublePressAt(hitbox, parsed.x, parsed.y);
1509
+ } else {
1510
+ session.clickAt(parsed.x, parsed.y);
1511
+ if (hitbox && shouldDispatchDoublePress) {
1512
+ doublePressAt(hitbox, parsed.x, parsed.y);
1513
+ }
1319
1514
  }
1320
1515
  } else if (parsed.action === "drag") {
1321
1516
  if (mouseSelectionId) {
@@ -1345,6 +1540,75 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
1345
1540
  } else if (parsed.action === "wheel-down") {
1346
1541
  wheelAt(parsed.x, parsed.y, 1);
1347
1542
  }
1543
+ }
1544
+
1545
+ function processInputStream(value: string) {
1546
+ if (!value) {
1547
+ return;
1548
+ }
1549
+
1550
+ if (pendingPasteChunk) {
1551
+ pendingPasteChunk += value;
1552
+ const paste = parseBracketedPaste(pendingPasteChunk);
1553
+ if (!paste && isBracketedPasteStartPrefix(pendingPasteChunk)) {
1554
+ return;
1555
+ }
1556
+ if (!paste) {
1557
+ const buffered = pendingPasteChunk;
1558
+ pendingPasteChunk = "";
1559
+ processKeyStream(buffered);
1560
+ return;
1561
+ }
1562
+ pendingPasteChunk = "";
1563
+ dispatchPasteText(paste.text);
1564
+ processInputStream(paste.rest);
1565
+ return;
1566
+ }
1567
+
1568
+ if (pendingKeyChunk) {
1569
+ if (pendingKeyChunk === ESCAPE && !canContinueEscapeSequence(value)) {
1570
+ flushPendingEscape();
1571
+ processInputStream(value);
1572
+ return;
1573
+ }
1574
+ const buffered = pendingKeyChunk + value;
1575
+ cancelPendingEscapeFlush();
1576
+ pendingKeyChunk = "";
1577
+ processKeyStream(buffered);
1578
+ return;
1579
+ }
1580
+
1581
+ if (value === ESCAPE) {
1582
+ processKeyStream(value);
1583
+ return;
1584
+ }
1585
+
1586
+ if (isBracketedPasteStartPrefix(value)) {
1587
+ pendingPasteChunk = value;
1588
+ return;
1589
+ }
1590
+
1591
+ if (value.startsWith(BRACKETED_PASTE_START)) {
1592
+ const paste = parseBracketedPaste(value);
1593
+ if (!paste) {
1594
+ pendingPasteChunk = value;
1595
+ return;
1596
+ }
1597
+ dispatchPasteText(paste.text);
1598
+ processInputStream(paste.rest);
1599
+ return;
1600
+ }
1601
+
1602
+ const parsedMouse = parseTerminalMousePrefix(value);
1603
+ if (parsedMouse) {
1604
+ processParsedMouseInput(parsedMouse.input);
1605
+ processInputStream(parsedMouse.rest);
1606
+ return;
1607
+ }
1608
+
1609
+ const parsed = parseTerminalInput(value);
1610
+ if (parsed.type === "mouse") {
1611
+ processParsedMouseInput(parsed);
1348
1612
  return;
1349
1613
  }
1350
1614