@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.
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +12 -0
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +6 -2
- package/dist/events.js.map +1 -1
- package/dist/keymap.d.ts.map +1 -1
- package/dist/keymap.js +4 -2
- package/dist/keymap.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -1
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +6 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +30 -16
- package/dist/mouse.js.map +1 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +8 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +184 -27
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -5
- package/dist/runtime.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +323 -83
- package/dist/session.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +3 -0
- package/dist/theme.js.map +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +18 -4
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +38 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +13 -6
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +7 -5
- package/docs/primitive-gallery.md +7 -3
- package/llms-full.txt +28 -15
- package/package.json +1 -1
- package/src/ansi.ts +12 -0
- package/src/events.ts +4 -2
- package/src/keymap.ts +4 -2
- package/src/layout.ts +2 -1
- package/src/mouse.ts +31 -15
- package/src/primitives.ts +8 -1
- package/src/render.ts +199 -28
- package/src/runtime.ts +13 -5
- package/src/session.ts +341 -79
- package/src/theme.ts +3 -0
- package/src/tree.ts +19 -4
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
666
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
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
|
|
61
|
-
props
|
|
62
|
-
children: normalizeChildren(
|
|
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
|
|