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