@valyrianjs/terminal 0.1.1 → 0.2.0
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/README.md +105 -55
- package/dist/ansi.d.ts +20 -4
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +171 -47
- package/dist/ansi.js.map +1 -1
- package/dist/editor-state.d.ts +22 -0
- package/dist/editor-state.d.ts.map +1 -0
- package/dist/editor-state.js +110 -0
- package/dist/editor-state.js.map +1 -0
- package/dist/events.d.ts +1 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +15 -38
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keymap.d.ts +7 -0
- package/dist/keymap.d.ts.map +1 -0
- package/dist/keymap.js +133 -0
- package/dist/keymap.js.map +1 -0
- package/dist/layout.d.ts +10 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +97 -7
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +1 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +24 -1
- package/dist/mouse.js.map +1 -1
- package/dist/output-writer.d.ts +9 -0
- package/dist/output-writer.d.ts.map +1 -0
- package/dist/output-writer.js +79 -0
- package/dist/output-writer.js.map +1 -0
- package/dist/paste.d.ts +7 -0
- package/dist/paste.d.ts.map +1 -0
- package/dist/paste.js +18 -0
- package/dist/paste.js.map +1 -0
- package/dist/primitives.d.ts +15 -3
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +9 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts +9 -4
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +923 -68
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +209 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +24 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +858 -199
- package/dist/session.js.map +1 -1
- package/dist/stream-log.d.ts +40 -0
- package/dist/stream-log.d.ts.map +1 -0
- package/dist/stream-log.js +73 -0
- package/dist/stream-log.js.map +1 -0
- package/dist/text.d.ts +3 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +19 -0
- package/dist/text.js.map +1 -0
- package/dist/theme.d.ts +7 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +254 -0
- package/dist/theme.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +42 -1
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +203 -24
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +313 -142
- package/docs/assets/quick-note.svg +13 -0
- package/docs/cookbook.md +296 -201
- package/docs/core-concepts.md +143 -55
- package/docs/getting-started.md +209 -90
- package/docs/interaction-model.md +98 -54
- package/docs/primitive-gallery.md +370 -0
- package/docs/session-runtime.md +131 -362
- package/docs/valyrian-modules.md +3196 -0
- package/llms-full.txt +5377 -0
- package/package.json +21 -8
- package/src/ansi.ts +269 -0
- package/src/clipboard.ts +76 -0
- package/src/editor-state.ts +162 -0
- package/src/events.ts +163 -0
- package/src/index.ts +95 -0
- package/src/keymap.ts +151 -0
- package/src/layout.ts +282 -0
- package/src/mouse.ts +68 -0
- package/src/output-writer.ts +93 -0
- package/src/paste.ts +23 -0
- package/src/primitives.ts +55 -0
- package/src/render.ts +1204 -0
- package/src/runtime.ts +267 -0
- package/src/scheduler.ts +33 -0
- package/src/session.ts +1408 -0
- package/src/stream-log.ts +96 -0
- package/src/text.ts +20 -0
- package/src/theme.ts +263 -0
- package/src/tree.ts +169 -0
- package/src/types.ts +541 -0
- package/tsconfig.json +4 -7
- package/docs/local-demo.md +0 -28
package/dist/session.js
CHANGED
|
@@ -1,11 +1,101 @@
|
|
|
1
|
-
import { createAnsiDiffWriter, formatPlainFrame, toAnsiFrame } from "./ansi.js";
|
|
1
|
+
import { 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
|
-
import {
|
|
3
|
+
import { createEditorState, insertEditorText, moveEditorCursor, removeEditorBackward, removeEditorForward } from "./editor-state.js";
|
|
4
|
+
import { copySelection, hasSelection, insertText, moveCursorEnd, moveCursorHome, moveCursorLeft, moveCursorRight, moveCursorWordLeft, moveCursorWordRight, normalizeInputState, parseTerminalKey, removeBackward, removeForward, selectAll } from "./events.js";
|
|
5
|
+
import { createResolvedTerminalKeymap, resolveTerminalKeyBinding } from "./keymap.js";
|
|
4
6
|
import { mergeVertical } from "./layout.js";
|
|
5
|
-
import { cursorFromHitbox,
|
|
7
|
+
import { cursorFromHitbox, parseTerminalInput, resolvePointerTarget } from "./mouse.js";
|
|
8
|
+
import { createOutputWriter } from "./output-writer.js";
|
|
9
|
+
import { parseBracketedPaste } from "./paste.js";
|
|
6
10
|
import { renderTerminalFrame } from "./render.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
11
|
+
import { createRenderScheduler } from "./scheduler.js";
|
|
12
|
+
import { plainText, stripTerminalControls } from "./text.js";
|
|
13
|
+
import { collectActiveFocusScopeFocusableNodes, collectDirectOverlayFocusableNodes, collectFocusableNodes, findFocusableById, findFocused } from "./tree.js";
|
|
14
|
+
import { createValyrianTerminalRuntime } from "./runtime.js";
|
|
15
|
+
const DEFAULT_TERMINAL_SIZE = { cols: 80, rows: 24 };
|
|
16
|
+
const BRACKETED_PASTE_START = "\u001b[200~";
|
|
17
|
+
const KNOWN_TERMINAL_KEY_SEQUENCES = [
|
|
18
|
+
"\u001b[13;2u",
|
|
19
|
+
"\u001b[13;130u",
|
|
20
|
+
"\u001b[13;129u",
|
|
21
|
+
"\u001b[27;2;13~",
|
|
22
|
+
"\u001b[13;2~",
|
|
23
|
+
"\u001b[1;2C",
|
|
24
|
+
"\u001b[1;2D",
|
|
25
|
+
"\u001b[1;3C",
|
|
26
|
+
"\u001b[1;3D",
|
|
27
|
+
"\u001b[3~",
|
|
28
|
+
"\u001b[Z",
|
|
29
|
+
"\u001b[A",
|
|
30
|
+
"\u001b[B",
|
|
31
|
+
"\u001b[C",
|
|
32
|
+
"\u001b[D",
|
|
33
|
+
"\u001b[H",
|
|
34
|
+
"\u001b[F",
|
|
35
|
+
"\u001bf",
|
|
36
|
+
"\u001bb"
|
|
37
|
+
];
|
|
38
|
+
const ESCAPE = "\u001b";
|
|
39
|
+
const CSI_PREFIX = "\u001b[";
|
|
40
|
+
const DOUBLE_PRESS_INTERVAL_MS = 500;
|
|
41
|
+
function isBracketedPasteStartPrefix(value) {
|
|
42
|
+
return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
|
|
43
|
+
}
|
|
44
|
+
function isDisambiguatedBracketedPasteStartPrefix(value) {
|
|
45
|
+
return isBracketedPasteStartPrefix(value) && value.length > CSI_PREFIX.length;
|
|
46
|
+
}
|
|
47
|
+
function isKnownTerminalKeySequencePrefix(value) {
|
|
48
|
+
return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
|
|
49
|
+
}
|
|
50
|
+
function canContinueEscapeSequence(value) {
|
|
51
|
+
return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
|
|
52
|
+
}
|
|
53
|
+
function validateTerminalDimension(name, value) {
|
|
54
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
55
|
+
throw new Error(`Invalid terminal ${name}: expected an integer >= 1`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function isValidTerminalDimension(value) {
|
|
60
|
+
return Number.isInteger(value) && Number(value) >= 1;
|
|
61
|
+
}
|
|
62
|
+
function getProcessStdin() {
|
|
63
|
+
const candidate = globalThis.process?.stdin;
|
|
64
|
+
return candidate && typeof candidate.on === "function" ? candidate : undefined;
|
|
65
|
+
}
|
|
66
|
+
function getProcessStdout() {
|
|
67
|
+
const candidate = globalThis.process?.stdout;
|
|
68
|
+
return candidate && typeof candidate.write === "function" ? candidate : undefined;
|
|
69
|
+
}
|
|
70
|
+
function isInteractiveTTY(stdin, stdout) {
|
|
71
|
+
return Boolean(stdin?.isTTY && stdout?.isTTY);
|
|
72
|
+
}
|
|
73
|
+
function resolveRuntimeOptions(options) {
|
|
74
|
+
const processStdin = getProcessStdin();
|
|
75
|
+
const processStdout = getProcessStdout();
|
|
76
|
+
const canUseProcessTTY = isInteractiveTTY(processStdin, processStdout);
|
|
77
|
+
const runtime = options.runtime ?? (canUseProcessTTY || options.stdin || options.stdout ? "app" : "headless");
|
|
78
|
+
const stdin = options.stdin ?? (runtime === "app" && canUseProcessTTY ? processStdin : undefined);
|
|
79
|
+
const stdout = options.stdout ?? (runtime === "app" && canUseProcessTTY ? processStdout : undefined);
|
|
80
|
+
const ownsInteractiveTTY = runtime === "app" && isInteractiveTTY(stdin, stdout);
|
|
81
|
+
return {
|
|
82
|
+
runtime,
|
|
83
|
+
stdin,
|
|
84
|
+
stdout,
|
|
85
|
+
alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
|
|
86
|
+
hideCursor: options.hideCursor ?? ownsInteractiveTTY,
|
|
87
|
+
writesAnsi: runtime === "app" && Boolean(stdout)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function resolveTerminalSize(options, stdout) {
|
|
91
|
+
const cols = options.cols ?? stdout?.columns ?? DEFAULT_TERMINAL_SIZE.cols;
|
|
92
|
+
const rows = options.rows ?? stdout?.rows ?? DEFAULT_TERMINAL_SIZE.rows;
|
|
93
|
+
return {
|
|
94
|
+
cols: validateTerminalDimension("cols", cols),
|
|
95
|
+
rows: validateTerminalDimension("rows", rows)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById) {
|
|
9
99
|
for (let i = 0; i < nodes.length; i += 1) {
|
|
10
100
|
const node = nodes[i];
|
|
11
101
|
if (node.type !== "element") {
|
|
@@ -14,11 +104,18 @@ function applyInteractiveState(nodes, focusedId, inputStateById, listIndexById,
|
|
|
14
104
|
const id = node.props.id;
|
|
15
105
|
node.props.__focused = Boolean(focusedId && id === focusedId);
|
|
16
106
|
if (node.tag === "terminal-input" && id) {
|
|
17
|
-
const value =
|
|
107
|
+
const value = plainText(node.props.value ?? "");
|
|
18
108
|
const current = normalizeInputState(inputStateById.get(id), value.length);
|
|
19
109
|
inputStateById.set(id, current);
|
|
20
110
|
node.props.__inputState = current;
|
|
21
111
|
}
|
|
112
|
+
if (node.tag === "terminal-editor" && id) {
|
|
113
|
+
const value = plainText(node.props.value ?? "");
|
|
114
|
+
const previous = editorStateById.get(id);
|
|
115
|
+
const current = createEditorState(value, previous?.cursor, previous?.desiredColumn);
|
|
116
|
+
editorStateById.set(id, current);
|
|
117
|
+
node.props.__editorState = current;
|
|
118
|
+
}
|
|
22
119
|
if (node.tag === "terminal-list" && id) {
|
|
23
120
|
node.props.__selectedIndex = listIndexById.get(id) || 0;
|
|
24
121
|
if (listHoverById.has(id)) {
|
|
@@ -31,46 +128,134 @@ function applyInteractiveState(nodes, focusedId, inputStateById, listIndexById,
|
|
|
31
128
|
node.props.__hoveredRow = scrollHoverRowById.get(id);
|
|
32
129
|
}
|
|
33
130
|
}
|
|
34
|
-
applyInteractiveState(node.children, focusedId, inputStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
131
|
+
applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
35
132
|
}
|
|
36
133
|
}
|
|
37
134
|
export function mountTerminal(input, options = {}) {
|
|
38
135
|
let focusedId = null;
|
|
136
|
+
let skipFocusContainmentOnce = false;
|
|
39
137
|
let clipboardValue = "";
|
|
40
138
|
let mouseSelectionId = null;
|
|
41
139
|
let pointerCaptureId = null;
|
|
140
|
+
let pendingPasteChunk = "";
|
|
141
|
+
let pendingKeyChunk = "";
|
|
142
|
+
let pendingEscapeFlush = null;
|
|
143
|
+
let lastPrimaryPress = null;
|
|
144
|
+
let destroyed = false;
|
|
145
|
+
let autoProjectionEnabled = false;
|
|
146
|
+
let suppressAutoProjection = false;
|
|
42
147
|
const clipboardAdapter = options.clipboard === false ? null : options.clipboard || createSystemClipboardAdapter();
|
|
43
148
|
const inputStateById = new Map();
|
|
149
|
+
const editorStateById = new Map();
|
|
44
150
|
const listIndexById = new Map();
|
|
45
151
|
const scrollOffsetById = new Map();
|
|
46
152
|
const listHoverById = new Map();
|
|
47
153
|
const scrollHoverRowById = new Map();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
154
|
+
const keyBindings = createResolvedTerminalKeymap(options.keymap?.bindings);
|
|
155
|
+
const runtimeOptions = resolveRuntimeOptions(options);
|
|
156
|
+
let terminalSize = resolveTerminalSize(options, runtimeOptions.stdout);
|
|
157
|
+
const terminalRuntime = createValyrianTerminalRuntime(input, () => {
|
|
158
|
+
if (!autoProjectionEnabled || suppressAutoProjection || destroyed) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
renderNow();
|
|
162
|
+
});
|
|
163
|
+
let currentTree = terminalRuntime.project();
|
|
164
|
+
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
165
|
+
let currentFrame = renderTreeFrame(currentTree);
|
|
166
|
+
let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
52
167
|
let currentHitboxes = currentFrame.hitboxes;
|
|
53
|
-
const
|
|
168
|
+
const outputWriter = createOutputWriter(runtimeOptions.stdout);
|
|
169
|
+
const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
170
|
+
const renderScheduler = createRenderScheduler(renderNow);
|
|
171
|
+
const outputResizeScheduler = createRenderScheduler(applyPendingOutputResize);
|
|
172
|
+
let cleanupOutputResize = null;
|
|
173
|
+
let pendingOutputResize = null;
|
|
174
|
+
function renderContext() {
|
|
175
|
+
return { ...terminalSize, theme: options.theme };
|
|
176
|
+
}
|
|
177
|
+
function renderTreeFrame(nodes) {
|
|
178
|
+
const context = renderContext();
|
|
179
|
+
return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
|
|
180
|
+
}
|
|
181
|
+
function emitLifecycleSetup() {
|
|
182
|
+
const writes = [];
|
|
183
|
+
if (runtimeOptions.alternateScreen) {
|
|
184
|
+
writes.push(ANSI_ENTER_ALTERNATE_SCREEN);
|
|
185
|
+
}
|
|
186
|
+
if (runtimeOptions.hideCursor) {
|
|
187
|
+
writes.push(ANSI_HIDE_CURSOR);
|
|
188
|
+
}
|
|
189
|
+
if (writes.length > 0) {
|
|
190
|
+
outputWriter.write(writes.join(""), { force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function emitLifecycleRestore() {
|
|
194
|
+
if (options.restoreOnDestroy === false) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const writes = [];
|
|
198
|
+
if (runtimeOptions.hideCursor) {
|
|
199
|
+
writes.push(ANSI_SHOW_CURSOR);
|
|
200
|
+
}
|
|
201
|
+
if (runtimeOptions.alternateScreen) {
|
|
202
|
+
writes.push(ANSI_EXIT_ALTERNATE_SCREEN);
|
|
203
|
+
}
|
|
204
|
+
if (writes.length > 0) {
|
|
205
|
+
outputWriter.write(writes.join(""), { force: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
54
208
|
function emitOutput() {
|
|
55
|
-
if (
|
|
56
|
-
|
|
209
|
+
if (!destroyed) {
|
|
210
|
+
outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
|
|
57
211
|
}
|
|
58
212
|
}
|
|
59
|
-
function
|
|
60
|
-
currentTree =
|
|
213
|
+
function renderNow() {
|
|
214
|
+
currentTree = terminalRuntime.project();
|
|
61
215
|
const focusables = [];
|
|
62
216
|
collectFocusableNodes(currentTree, focusables);
|
|
63
|
-
|
|
64
|
-
|
|
217
|
+
const overlayFocusables = [];
|
|
218
|
+
collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
|
|
219
|
+
const scopedFocusables = [];
|
|
220
|
+
collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
|
|
221
|
+
const activeFocusables = overlayFocusables.length ? overlayFocusables : scopedFocusables.length ? scopedFocusables : focusables;
|
|
222
|
+
if (!skipFocusContainmentOnce && focusedId && !activeFocusables.some((node) => node.props.id === focusedId)) {
|
|
223
|
+
focusedId = activeFocusables[0]?.props.id || null;
|
|
65
224
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
225
|
+
skipFocusContainmentOnce = false;
|
|
226
|
+
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
227
|
+
currentFrame = renderTreeFrame(currentTree);
|
|
228
|
+
currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
69
229
|
currentHitboxes = currentFrame.hitboxes;
|
|
70
230
|
emitOutput();
|
|
71
231
|
return currentOutput;
|
|
72
232
|
}
|
|
233
|
+
function rerender() {
|
|
234
|
+
if (destroyed) {
|
|
235
|
+
return currentOutput;
|
|
236
|
+
}
|
|
237
|
+
suppressAutoProjection = true;
|
|
238
|
+
try {
|
|
239
|
+
terminalRuntime.update();
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
suppressAutoProjection = false;
|
|
243
|
+
}
|
|
244
|
+
renderScheduler.requestRender();
|
|
245
|
+
renderScheduler.flush();
|
|
246
|
+
return currentOutput;
|
|
247
|
+
}
|
|
73
248
|
function orderedFocusables() {
|
|
249
|
+
const overlayFocusables = [];
|
|
250
|
+
collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
|
|
251
|
+
if (overlayFocusables.length) {
|
|
252
|
+
return overlayFocusables;
|
|
253
|
+
}
|
|
254
|
+
const scopedFocusables = [];
|
|
255
|
+
collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
|
|
256
|
+
if (scopedFocusables.length) {
|
|
257
|
+
return scopedFocusables;
|
|
258
|
+
}
|
|
74
259
|
const out = [];
|
|
75
260
|
collectFocusableNodes(currentTree, out);
|
|
76
261
|
return out;
|
|
@@ -105,21 +290,51 @@ export function mountTerminal(input, options = {}) {
|
|
|
105
290
|
}
|
|
106
291
|
function setInputCursor(id, cursor, extendSelection = false) {
|
|
107
292
|
const node = findFocusableById(currentTree, id);
|
|
108
|
-
const value =
|
|
293
|
+
const value = stripTerminalControls(node?.props.value ?? "");
|
|
109
294
|
const current = normalizeInputState(inputStateById.get(id), value.length);
|
|
110
295
|
inputStateById.set(id, extendSelection ? { cursor: Math.max(0, Math.min(value.length, cursor)), anchor: current.anchor } : { cursor: Math.max(0, Math.min(value.length, cursor)), anchor: Math.max(0, Math.min(value.length, cursor)) });
|
|
111
296
|
}
|
|
112
297
|
function replaceInputState(id, state) {
|
|
113
298
|
const node = findFocusableById(currentTree, id);
|
|
114
|
-
const value =
|
|
299
|
+
const value = stripTerminalControls(node?.props.value ?? "");
|
|
115
300
|
inputStateById.set(id, normalizeInputState(state, value.length));
|
|
116
301
|
}
|
|
302
|
+
function dispatchNodeEvent(node, type, payload) {
|
|
303
|
+
const id = String(node.props.id || "");
|
|
304
|
+
if (!id) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return terminalRuntime.dispatchTerminalEvent(id, type, payload);
|
|
308
|
+
}
|
|
117
309
|
function updateInputByState(node, next) {
|
|
118
310
|
const id = String(node.props.id || "");
|
|
119
|
-
|
|
311
|
+
dispatchNodeEvent(node, "change", { type: "change", id, value: next.value });
|
|
120
312
|
inputStateById.set(id, next.state);
|
|
121
313
|
return rerender();
|
|
122
314
|
}
|
|
315
|
+
function replaceEditorState(id, state) {
|
|
316
|
+
const node = findFocusableById(currentTree, id);
|
|
317
|
+
const value = plainText(node?.props.value ?? "");
|
|
318
|
+
editorStateById.set(id, createEditorState(value, state.cursor, state.desiredColumn));
|
|
319
|
+
}
|
|
320
|
+
function updateEditorValue(node, nextValue) {
|
|
321
|
+
const id = String(node.props.id || "");
|
|
322
|
+
dispatchNodeEvent(node, "change", { type: "change", id, value: plainText(nextValue) });
|
|
323
|
+
}
|
|
324
|
+
function updateEditorByState(node, next) {
|
|
325
|
+
const id = String(node.props.id || "");
|
|
326
|
+
updateEditorValue(node, next.value);
|
|
327
|
+
editorStateById.set(id, next.state);
|
|
328
|
+
return rerender();
|
|
329
|
+
}
|
|
330
|
+
function submitEditor(node) {
|
|
331
|
+
const id = String(node.props.id || "");
|
|
332
|
+
dispatchNodeEvent(node, "submit", { type: "submit", id, value: plainText(node.props.value ?? "") });
|
|
333
|
+
}
|
|
334
|
+
function cancelEditor(node) {
|
|
335
|
+
const id = String(node.props.id || "");
|
|
336
|
+
dispatchNodeEvent(node, "cancel", { type: "cancel", id, value: plainText(node.props.value ?? "") });
|
|
337
|
+
}
|
|
123
338
|
function setCursorFromHitbox(id, x, extendSelection = false) {
|
|
124
339
|
const hitbox = currentHitboxes.find((box) => box.id === id && box.tag === "terminal-input");
|
|
125
340
|
if (!hitbox) {
|
|
@@ -130,7 +345,8 @@ export function mountTerminal(input, options = {}) {
|
|
|
130
345
|
return rerender();
|
|
131
346
|
}
|
|
132
347
|
function visibleScrollLines(node) {
|
|
133
|
-
const
|
|
348
|
+
const context = renderContext();
|
|
349
|
+
const lines = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context))).lines;
|
|
134
350
|
const offset = Number(node.props.__scrollOffset || 0);
|
|
135
351
|
const height = Number(node.props.height || lines.length || 0);
|
|
136
352
|
return lines.slice(offset, offset + height).map((line) => line.trimEnd());
|
|
@@ -145,6 +361,13 @@ export function mountTerminal(input, options = {}) {
|
|
|
145
361
|
}
|
|
146
362
|
return 1;
|
|
147
363
|
}
|
|
364
|
+
function sourceRowFromHitbox(node, hitbox, y) {
|
|
365
|
+
const visibleRow = Math.max(1, y - hitbox.y1 + 1);
|
|
366
|
+
if (node.tag !== "terminal-list") {
|
|
367
|
+
return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
|
|
368
|
+
}
|
|
369
|
+
return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
|
|
370
|
+
}
|
|
148
371
|
function shouldPointerCapture(node) {
|
|
149
372
|
return Boolean(node.props.pointerCapture && (node.tag === "terminal-list" || node.tag === "terminal-scroll"));
|
|
150
373
|
}
|
|
@@ -166,12 +389,8 @@ export function mountTerminal(input, options = {}) {
|
|
|
166
389
|
if (!node || !node.props.id) {
|
|
167
390
|
return;
|
|
168
391
|
}
|
|
169
|
-
const handler = node.props[`on${type}`];
|
|
170
|
-
if (typeof handler !== "function") {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
392
|
const payload = { type, id: node.props.id, source, row, x, y };
|
|
174
|
-
|
|
393
|
+
dispatchNodeEvent(node, type, payload);
|
|
175
394
|
}
|
|
176
395
|
function setPointerCapture(id, source, row = null, x = null, y = null) {
|
|
177
396
|
if (pointerCaptureId === id) {
|
|
@@ -187,9 +406,53 @@ export function mountTerminal(input, options = {}) {
|
|
|
187
406
|
emitCaptureEvent(next, "capturestart", source, row ?? hoveredRowForNode(next), x, y);
|
|
188
407
|
}
|
|
189
408
|
}
|
|
409
|
+
function dispatchListPressEvent(node, type, index) {
|
|
410
|
+
const id = node.props.id;
|
|
411
|
+
if (!id) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
415
|
+
if (typeof items[index] === "undefined") {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
const payload = { type, id, index, value: items[index] };
|
|
419
|
+
return dispatchNodeEvent(node, type, payload);
|
|
420
|
+
}
|
|
421
|
+
function dispatchButtonPressEvent(node, type) {
|
|
422
|
+
const id = String(node.props.id || "");
|
|
423
|
+
if (!id) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return dispatchNodeEvent(node, type, { type, id });
|
|
427
|
+
}
|
|
428
|
+
function dispatchListPointerPressEvent(node, type, row) {
|
|
429
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
430
|
+
const index = Math.max(0, Math.min(items.length - 1, row - 1));
|
|
431
|
+
return dispatchListPressEvent(node, type, index);
|
|
432
|
+
}
|
|
433
|
+
function dispatchInputContextPressEvent(node, hitbox, x, y) {
|
|
434
|
+
const id = String(node.props.id || "");
|
|
435
|
+
if (!id) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const value = stripTerminalControls(node.props.value ?? "");
|
|
439
|
+
const current = normalizeInputState(inputStateById.get(id), value.length);
|
|
440
|
+
const cursor = typeof x === "number" ? cursorFromHitbox(hitbox, x) : current.cursor;
|
|
441
|
+
return dispatchNodeEvent(node, "contextpress", { type: "contextpress", id, value, cursor, x, y });
|
|
442
|
+
}
|
|
443
|
+
function dispatchScrollContextPressEvent(node, row, x, y) {
|
|
444
|
+
if (!node.props.id) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const lines = visibleScrollLines(node);
|
|
448
|
+
const index = Math.max(0, Math.min(lines.length - 1, row - 1));
|
|
449
|
+
if (typeof lines[index] === "undefined") {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return dispatchNodeEvent(node, "contextpress", { type: "contextpress", id: node.props.id, row: index + 1, value: lines[index], x, y });
|
|
453
|
+
}
|
|
190
454
|
function emitMouseRowEvent(node, type, row, x = null, y = null) {
|
|
191
|
-
|
|
192
|
-
if (typeof handler !== "function" || !node.props.id) {
|
|
455
|
+
if (!node.props.id) {
|
|
193
456
|
return;
|
|
194
457
|
}
|
|
195
458
|
if (node.tag === "terminal-list") {
|
|
@@ -199,7 +462,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
199
462
|
return;
|
|
200
463
|
}
|
|
201
464
|
const payload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
|
|
202
|
-
|
|
465
|
+
dispatchNodeEvent(node, type, payload);
|
|
203
466
|
return;
|
|
204
467
|
}
|
|
205
468
|
if (node.tag === "terminal-scroll") {
|
|
@@ -209,7 +472,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
209
472
|
return;
|
|
210
473
|
}
|
|
211
474
|
const payload = { type, id: node.props.id, row: index + 1, value: lines[index], x, y };
|
|
212
|
-
|
|
475
|
+
dispatchNodeEvent(node, type, payload);
|
|
213
476
|
}
|
|
214
477
|
}
|
|
215
478
|
function clearSemanticHover(exceptId, x = null, y = null) {
|
|
@@ -242,7 +505,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
242
505
|
return;
|
|
243
506
|
}
|
|
244
507
|
clearSemanticHover(id, x, y);
|
|
245
|
-
const row =
|
|
508
|
+
const row = sourceRowFromHitbox(node, hitbox, y);
|
|
246
509
|
if (node.tag === "terminal-list") {
|
|
247
510
|
const nextIndex = row - 1;
|
|
248
511
|
const prevIndex = listHoverById.get(id);
|
|
@@ -272,7 +535,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
272
535
|
}
|
|
273
536
|
}
|
|
274
537
|
function hoverAt(x, y) {
|
|
275
|
-
const hitbox =
|
|
538
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
276
539
|
if (!hitbox) {
|
|
277
540
|
if (pointerCaptureId) {
|
|
278
541
|
setSemanticHoverFromHitbox(pointerCaptureId, x, y);
|
|
@@ -283,12 +546,260 @@ export function mountTerminal(input, options = {}) {
|
|
|
283
546
|
}
|
|
284
547
|
const node = findFocusableById(currentTree, hitbox.id);
|
|
285
548
|
if (node && shouldPointerCapture(node)) {
|
|
286
|
-
setPointerCapture(hitbox.id, "drag",
|
|
549
|
+
setPointerCapture(hitbox.id, "drag", sourceRowFromHitbox(node, hitbox, y), x, y);
|
|
287
550
|
}
|
|
288
551
|
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
289
552
|
return rerender();
|
|
290
553
|
}
|
|
554
|
+
function changeListSelection(node, direction) {
|
|
555
|
+
const id = node.props.id;
|
|
556
|
+
if (!id) {
|
|
557
|
+
return currentOutput;
|
|
558
|
+
}
|
|
559
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
560
|
+
const currentIndex = listIndexById.get(id) || 0;
|
|
561
|
+
const nextIndex = direction < 0 ? Math.max(0, currentIndex - 1) : Math.min(items.length - 1, currentIndex + 1);
|
|
562
|
+
listIndexById.set(id, nextIndex);
|
|
563
|
+
const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
|
|
564
|
+
dispatchNodeEvent(node, "change", payload);
|
|
565
|
+
return rerender();
|
|
566
|
+
}
|
|
567
|
+
function pressListSelection(node) {
|
|
568
|
+
const id = node.props.id;
|
|
569
|
+
if (!id) {
|
|
570
|
+
return currentOutput;
|
|
571
|
+
}
|
|
572
|
+
const currentIndex = listIndexById.get(id) || 0;
|
|
573
|
+
dispatchListPressEvent(node, "press", currentIndex);
|
|
574
|
+
return rerender();
|
|
575
|
+
}
|
|
576
|
+
function scrollFocusedNode(node, direction) {
|
|
577
|
+
const id = node.props.id;
|
|
578
|
+
if (!id) {
|
|
579
|
+
return currentOutput;
|
|
580
|
+
}
|
|
581
|
+
const context = renderContext();
|
|
582
|
+
const rendered = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
|
|
583
|
+
const height = Number(node.props.height || rendered.lines.length || 0);
|
|
584
|
+
const currentOffset = scrollOffsetById.get(id) || 0;
|
|
585
|
+
const maxOffset = Math.max(0, rendered.lines.length - height);
|
|
586
|
+
scrollOffsetById.set(id, direction < 0 ? Math.max(0, currentOffset - 1) : Math.min(maxOffset, currentOffset + 1));
|
|
587
|
+
return rerender();
|
|
588
|
+
}
|
|
589
|
+
function wheelAt(x, y, direction) {
|
|
590
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
591
|
+
if (!hitbox) {
|
|
592
|
+
return currentOutput;
|
|
593
|
+
}
|
|
594
|
+
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
595
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
596
|
+
if (node?.tag === "terminal-scroll") {
|
|
597
|
+
return scrollFocusedNode(node, direction);
|
|
598
|
+
}
|
|
599
|
+
if (node?.tag === "terminal-list" && node.props.virtualized) {
|
|
600
|
+
return changeListSelection(node, direction);
|
|
601
|
+
}
|
|
602
|
+
return rerender();
|
|
603
|
+
}
|
|
604
|
+
function runInputCommand(node, command) {
|
|
605
|
+
const id = node.props.id;
|
|
606
|
+
if (!id) {
|
|
607
|
+
return currentOutput;
|
|
608
|
+
}
|
|
609
|
+
const currentValue = stripTerminalControls(node.props.value ?? "");
|
|
610
|
+
const state = normalizeInputState(inputStateById.get(id), currentValue.length);
|
|
611
|
+
switch (command.id) {
|
|
612
|
+
case "input.submit":
|
|
613
|
+
dispatchNodeEvent(node, "submit", { type: "submit", id, value: stripTerminalControls(node.props.value ?? "") });
|
|
614
|
+
return rerender();
|
|
615
|
+
case "input.cursorLeft":
|
|
616
|
+
replaceInputState(id, moveCursorLeft(state, currentValue.length));
|
|
617
|
+
return rerender();
|
|
618
|
+
case "input.cursorRight":
|
|
619
|
+
replaceInputState(id, moveCursorRight(state, currentValue.length));
|
|
620
|
+
return rerender();
|
|
621
|
+
case "input.selectLeft":
|
|
622
|
+
replaceInputState(id, moveCursorLeft(state, currentValue.length, true));
|
|
623
|
+
return rerender();
|
|
624
|
+
case "input.selectRight":
|
|
625
|
+
replaceInputState(id, moveCursorRight(state, currentValue.length, true));
|
|
626
|
+
return rerender();
|
|
627
|
+
case "input.wordLeft":
|
|
628
|
+
replaceInputState(id, moveCursorWordLeft(currentValue, state));
|
|
629
|
+
return rerender();
|
|
630
|
+
case "input.wordRight":
|
|
631
|
+
replaceInputState(id, moveCursorWordRight(currentValue, state));
|
|
632
|
+
return rerender();
|
|
633
|
+
case "input.home":
|
|
634
|
+
replaceInputState(id, moveCursorHome(state, currentValue.length));
|
|
635
|
+
return rerender();
|
|
636
|
+
case "input.end":
|
|
637
|
+
replaceInputState(id, moveCursorEnd(state, currentValue.length));
|
|
638
|
+
return rerender();
|
|
639
|
+
case "input.selectAll":
|
|
640
|
+
replaceInputState(id, selectAll(currentValue));
|
|
641
|
+
return rerender();
|
|
642
|
+
case "input.copy":
|
|
643
|
+
writeClipboard(copySelection(currentValue, state));
|
|
644
|
+
return rerender();
|
|
645
|
+
case "input.cut":
|
|
646
|
+
writeClipboard(copySelection(currentValue, state));
|
|
647
|
+
if (hasSelection(state)) {
|
|
648
|
+
return updateInputByState(node, insertText(currentValue, state, ""));
|
|
649
|
+
}
|
|
650
|
+
return currentOutput;
|
|
651
|
+
case "input.paste":
|
|
652
|
+
return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(readClipboard())));
|
|
653
|
+
case "input.pasteText":
|
|
654
|
+
return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
|
|
655
|
+
case "input.backspace":
|
|
656
|
+
return updateInputByState(node, removeBackward(currentValue, state));
|
|
657
|
+
case "input.delete":
|
|
658
|
+
return updateInputByState(node, removeForward(currentValue, state));
|
|
659
|
+
case "input.insertText":
|
|
660
|
+
return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
|
|
661
|
+
default:
|
|
662
|
+
return currentOutput;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function isBuiltInInputCommand(command) {
|
|
666
|
+
return command.id === "input.submit"
|
|
667
|
+
|| command.id === "input.cursorLeft"
|
|
668
|
+
|| command.id === "input.cursorRight"
|
|
669
|
+
|| command.id === "input.selectLeft"
|
|
670
|
+
|| command.id === "input.selectRight"
|
|
671
|
+
|| command.id === "input.wordLeft"
|
|
672
|
+
|| command.id === "input.wordRight"
|
|
673
|
+
|| command.id === "input.home"
|
|
674
|
+
|| command.id === "input.end"
|
|
675
|
+
|| command.id === "input.selectAll"
|
|
676
|
+
|| command.id === "input.copy"
|
|
677
|
+
|| command.id === "input.cut"
|
|
678
|
+
|| command.id === "input.paste"
|
|
679
|
+
|| command.id === "input.pasteText"
|
|
680
|
+
|| command.id === "input.backspace"
|
|
681
|
+
|| command.id === "input.delete"
|
|
682
|
+
|| command.id === "input.insertText";
|
|
683
|
+
}
|
|
684
|
+
function runEditorCommand(node, command) {
|
|
685
|
+
const id = node.props.id;
|
|
686
|
+
if (!id) {
|
|
687
|
+
return currentOutput;
|
|
688
|
+
}
|
|
689
|
+
const currentValue = plainText(node.props.value ?? "");
|
|
690
|
+
const previous = editorStateById.get(id);
|
|
691
|
+
const state = createEditorState(currentValue, previous?.cursor, previous?.desiredColumn);
|
|
692
|
+
switch (command.id) {
|
|
693
|
+
case "editor.submit":
|
|
694
|
+
submitEditor(node);
|
|
695
|
+
return rerender();
|
|
696
|
+
case "editor.cancel":
|
|
697
|
+
cancelEditor(node);
|
|
698
|
+
return rerender();
|
|
699
|
+
case "editor.newline":
|
|
700
|
+
return updateEditorByState(node, insertEditorText(currentValue, state, "\n"));
|
|
701
|
+
case "editor.backspace":
|
|
702
|
+
return updateEditorByState(node, removeEditorBackward(currentValue, state));
|
|
703
|
+
case "editor.delete":
|
|
704
|
+
return updateEditorByState(node, removeEditorForward(currentValue, state));
|
|
705
|
+
case "editor.cursorLeft":
|
|
706
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "left"));
|
|
707
|
+
return rerender();
|
|
708
|
+
case "editor.cursorRight":
|
|
709
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "right"));
|
|
710
|
+
return rerender();
|
|
711
|
+
case "editor.cursorUp":
|
|
712
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "up"));
|
|
713
|
+
return rerender();
|
|
714
|
+
case "editor.cursorDown":
|
|
715
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "down"));
|
|
716
|
+
return rerender();
|
|
717
|
+
case "editor.home":
|
|
718
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "home"));
|
|
719
|
+
return rerender();
|
|
720
|
+
case "editor.end":
|
|
721
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "end"));
|
|
722
|
+
return rerender();
|
|
723
|
+
case "editor.paste":
|
|
724
|
+
return updateEditorByState(node, insertEditorText(currentValue, state, plainText(readClipboard())));
|
|
725
|
+
case "editor.pasteText":
|
|
726
|
+
case "editor.insertText":
|
|
727
|
+
return updateEditorByState(node, insertEditorText(currentValue, state, plainText(command.text ?? "")));
|
|
728
|
+
default:
|
|
729
|
+
return currentOutput;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function isBuiltInEditorCommand(command) {
|
|
733
|
+
return command.id === "editor.submit"
|
|
734
|
+
|| command.id === "editor.cancel"
|
|
735
|
+
|| command.id === "editor.newline"
|
|
736
|
+
|| command.id === "editor.backspace"
|
|
737
|
+
|| command.id === "editor.delete"
|
|
738
|
+
|| command.id === "editor.cursorLeft"
|
|
739
|
+
|| command.id === "editor.cursorRight"
|
|
740
|
+
|| command.id === "editor.cursorUp"
|
|
741
|
+
|| command.id === "editor.cursorDown"
|
|
742
|
+
|| command.id === "editor.home"
|
|
743
|
+
|| command.id === "editor.end"
|
|
744
|
+
|| command.id === "editor.paste"
|
|
745
|
+
|| command.id === "editor.pasteText"
|
|
746
|
+
|| command.id === "editor.insertText";
|
|
747
|
+
}
|
|
748
|
+
function runCommand(command, node, context) {
|
|
749
|
+
switch (command.id) {
|
|
750
|
+
case "focus.next":
|
|
751
|
+
session.focusNext();
|
|
752
|
+
return currentOutput;
|
|
753
|
+
case "focus.prev":
|
|
754
|
+
session.focusPrev();
|
|
755
|
+
return currentOutput;
|
|
756
|
+
case "button.press":
|
|
757
|
+
if (node?.tag === "terminal-button") {
|
|
758
|
+
dispatchButtonPressEvent(node, "press");
|
|
759
|
+
return rerender();
|
|
760
|
+
}
|
|
761
|
+
return currentOutput;
|
|
762
|
+
case "list.prev":
|
|
763
|
+
return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
|
|
764
|
+
case "list.next":
|
|
765
|
+
return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
|
|
766
|
+
case "list.press":
|
|
767
|
+
return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
|
|
768
|
+
case "scroll.up":
|
|
769
|
+
return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, -1) : currentOutput;
|
|
770
|
+
case "scroll.down":
|
|
771
|
+
return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, 1) : currentOutput;
|
|
772
|
+
default:
|
|
773
|
+
if (node?.tag === "terminal-input" && isBuiltInInputCommand(command)) {
|
|
774
|
+
return runInputCommand(node, command);
|
|
775
|
+
}
|
|
776
|
+
if (node?.tag === "terminal-editor" && isBuiltInEditorCommand(command)) {
|
|
777
|
+
return runEditorCommand(node, command);
|
|
778
|
+
}
|
|
779
|
+
if (typeof options.keymap?.onCommand === "function") {
|
|
780
|
+
const consumed = options.keymap.onCommand(command, context);
|
|
781
|
+
return consumed ? rerender() : currentOutput;
|
|
782
|
+
}
|
|
783
|
+
return currentOutput;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
291
786
|
const session = {
|
|
787
|
+
size() {
|
|
788
|
+
return { ...terminalSize };
|
|
789
|
+
},
|
|
790
|
+
resize(cols, rows) {
|
|
791
|
+
const nextSize = {
|
|
792
|
+
cols: validateTerminalDimension("cols", cols),
|
|
793
|
+
rows: validateTerminalDimension("rows", rows)
|
|
794
|
+
};
|
|
795
|
+
pendingOutputResize = null;
|
|
796
|
+
outputResizeScheduler.cancel();
|
|
797
|
+
if (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
terminalSize = nextSize;
|
|
801
|
+
rerender();
|
|
802
|
+
},
|
|
292
803
|
update() {
|
|
293
804
|
return rerender();
|
|
294
805
|
},
|
|
@@ -296,7 +807,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
296
807
|
return currentOutput;
|
|
297
808
|
},
|
|
298
809
|
ansiOutput() {
|
|
299
|
-
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans);
|
|
810
|
+
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
300
811
|
},
|
|
301
812
|
tree() {
|
|
302
813
|
return currentTree;
|
|
@@ -311,13 +822,20 @@ export function mountTerminal(input, options = {}) {
|
|
|
311
822
|
return true;
|
|
312
823
|
},
|
|
313
824
|
focusAt(x, y) {
|
|
314
|
-
const hitbox =
|
|
825
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
315
826
|
if (!hitbox) {
|
|
316
827
|
clearSemanticHover(undefined, x, y);
|
|
317
828
|
return false;
|
|
318
829
|
}
|
|
319
830
|
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
320
|
-
|
|
831
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
832
|
+
if (!node) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
focusedId = node.props.id || focusedId;
|
|
836
|
+
skipFocusContainmentOnce = true;
|
|
837
|
+
rerender();
|
|
838
|
+
return true;
|
|
321
839
|
},
|
|
322
840
|
focusNext() {
|
|
323
841
|
const focusables = orderedFocusables();
|
|
@@ -341,143 +859,16 @@ export function mountTerminal(input, options = {}) {
|
|
|
341
859
|
return true;
|
|
342
860
|
},
|
|
343
861
|
dispatchKey(key) {
|
|
344
|
-
if (key === "TAB") {
|
|
345
|
-
this.focusNext();
|
|
346
|
-
return currentOutput;
|
|
347
|
-
}
|
|
348
|
-
if (key === "SHIFT_TAB") {
|
|
349
|
-
this.focusPrev();
|
|
350
|
-
return currentOutput;
|
|
351
|
-
}
|
|
352
862
|
const node = findFocused(currentTree, focusedId);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
invokeButton(node);
|
|
358
|
-
return rerender();
|
|
359
|
-
}
|
|
360
|
-
if (node.tag === "terminal-list") {
|
|
361
|
-
const id = node.props.id;
|
|
362
|
-
if (!id) {
|
|
363
|
-
return currentOutput;
|
|
364
|
-
}
|
|
365
|
-
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
366
|
-
const currentIndex = listIndexById.get(id) || 0;
|
|
367
|
-
if (key === "UP" || key === "LEFT") {
|
|
368
|
-
const nextIndex = Math.max(0, currentIndex - 1);
|
|
369
|
-
listIndexById.set(id, nextIndex);
|
|
370
|
-
if (typeof node.props.onchange === "function") {
|
|
371
|
-
const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
|
|
372
|
-
node.props.onchange(payload);
|
|
373
|
-
}
|
|
374
|
-
return rerender();
|
|
375
|
-
}
|
|
376
|
-
if (key === "DOWN" || key === "RIGHT") {
|
|
377
|
-
const nextIndex = Math.min(items.length - 1, currentIndex + 1);
|
|
378
|
-
listIndexById.set(id, nextIndex);
|
|
379
|
-
if (typeof node.props.onchange === "function") {
|
|
380
|
-
const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
|
|
381
|
-
node.props.onchange(payload);
|
|
382
|
-
}
|
|
383
|
-
return rerender();
|
|
384
|
-
}
|
|
385
|
-
if (key === "ENTER" && typeof node.props.onpress === "function") {
|
|
386
|
-
const payload = { type: "press", id, index: currentIndex, value: items[currentIndex] };
|
|
387
|
-
node.props.onpress(payload);
|
|
388
|
-
return rerender();
|
|
389
|
-
}
|
|
863
|
+
const context = { key, focusedId: node?.props.id, focusedTag: node?.tag };
|
|
864
|
+
let command = resolveTerminalKeyBinding(keyBindings, context);
|
|
865
|
+
if (!command && node?.tag === "terminal-editor" && key.length === 1) {
|
|
866
|
+
command = { id: "editor.insertText", text: key };
|
|
390
867
|
}
|
|
391
|
-
if (node
|
|
392
|
-
|
|
393
|
-
if (!id) {
|
|
394
|
-
return currentOutput;
|
|
395
|
-
}
|
|
396
|
-
const rendered = mergeVertical(node.children.map(renderTerminalFrame));
|
|
397
|
-
const height = Number(node.props.height || rendered.lines.length || 0);
|
|
398
|
-
const currentOffset = scrollOffsetById.get(id) || 0;
|
|
399
|
-
const maxOffset = Math.max(0, rendered.lines.length - height);
|
|
400
|
-
if (key === "UP") {
|
|
401
|
-
scrollOffsetById.set(id, Math.max(0, currentOffset - 1));
|
|
402
|
-
return rerender();
|
|
403
|
-
}
|
|
404
|
-
if (key === "DOWN") {
|
|
405
|
-
scrollOffsetById.set(id, Math.min(maxOffset, currentOffset + 1));
|
|
406
|
-
return rerender();
|
|
407
|
-
}
|
|
868
|
+
if (!command && node?.tag === "terminal-editor" && key === "CTRL_V") {
|
|
869
|
+
command = { id: "editor.paste" };
|
|
408
870
|
}
|
|
409
|
-
|
|
410
|
-
const id = node.props.id;
|
|
411
|
-
if (!id) {
|
|
412
|
-
return currentOutput;
|
|
413
|
-
}
|
|
414
|
-
const currentValue = String(node.props.value ?? "");
|
|
415
|
-
const state = normalizeInputState(inputStateById.get(id), currentValue.length);
|
|
416
|
-
if (key === "ENTER") {
|
|
417
|
-
submitInput(node);
|
|
418
|
-
return rerender();
|
|
419
|
-
}
|
|
420
|
-
if (key === "LEFT") {
|
|
421
|
-
replaceInputState(id, moveCursorLeft(state, currentValue.length));
|
|
422
|
-
return rerender();
|
|
423
|
-
}
|
|
424
|
-
if (key === "RIGHT") {
|
|
425
|
-
replaceInputState(id, moveCursorRight(state, currentValue.length));
|
|
426
|
-
return rerender();
|
|
427
|
-
}
|
|
428
|
-
if (key === "SHIFT_LEFT") {
|
|
429
|
-
replaceInputState(id, moveCursorLeft(state, currentValue.length, true));
|
|
430
|
-
return rerender();
|
|
431
|
-
}
|
|
432
|
-
if (key === "SHIFT_RIGHT") {
|
|
433
|
-
replaceInputState(id, moveCursorRight(state, currentValue.length, true));
|
|
434
|
-
return rerender();
|
|
435
|
-
}
|
|
436
|
-
if (key === "ALT_LEFT") {
|
|
437
|
-
replaceInputState(id, moveCursorWordLeft(currentValue, state));
|
|
438
|
-
return rerender();
|
|
439
|
-
}
|
|
440
|
-
if (key === "ALT_RIGHT") {
|
|
441
|
-
replaceInputState(id, moveCursorWordRight(currentValue, state));
|
|
442
|
-
return rerender();
|
|
443
|
-
}
|
|
444
|
-
if (key === "HOME") {
|
|
445
|
-
replaceInputState(id, moveCursorHome(state, currentValue.length));
|
|
446
|
-
return rerender();
|
|
447
|
-
}
|
|
448
|
-
if (key === "END") {
|
|
449
|
-
replaceInputState(id, moveCursorEnd(state, currentValue.length));
|
|
450
|
-
return rerender();
|
|
451
|
-
}
|
|
452
|
-
if (key === "CTRL_A") {
|
|
453
|
-
replaceInputState(id, selectAll(currentValue));
|
|
454
|
-
return rerender();
|
|
455
|
-
}
|
|
456
|
-
if (key === "CTRL_C") {
|
|
457
|
-
writeClipboard(copySelection(currentValue, state));
|
|
458
|
-
return rerender();
|
|
459
|
-
}
|
|
460
|
-
if (key === "CTRL_X") {
|
|
461
|
-
writeClipboard(copySelection(currentValue, state));
|
|
462
|
-
if (hasSelection(state)) {
|
|
463
|
-
return updateInputByState(node, insertText(currentValue, state, ""));
|
|
464
|
-
}
|
|
465
|
-
return currentOutput;
|
|
466
|
-
}
|
|
467
|
-
if (key === "CTRL_V") {
|
|
468
|
-
return updateInputByState(node, insertText(currentValue, state, readClipboard()));
|
|
469
|
-
}
|
|
470
|
-
if (key === "BACKSPACE") {
|
|
471
|
-
return updateInputByState(node, removeBackward(currentValue, state));
|
|
472
|
-
}
|
|
473
|
-
if (key === "DELETE") {
|
|
474
|
-
return updateInputByState(node, removeForward(currentValue, state));
|
|
475
|
-
}
|
|
476
|
-
if (key.length === 1) {
|
|
477
|
-
return updateInputByState(node, insertText(currentValue, state, key));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return currentOutput;
|
|
871
|
+
return command ? runCommand(command, node, context) : currentOutput;
|
|
481
872
|
},
|
|
482
873
|
click(id) {
|
|
483
874
|
const node = id ? findFocusableById(currentTree, id) : findFocused(currentTree, focusedId);
|
|
@@ -487,11 +878,11 @@ export function mountTerminal(input, options = {}) {
|
|
|
487
878
|
if (id) {
|
|
488
879
|
focusedId = node.props.id || focusedId;
|
|
489
880
|
}
|
|
490
|
-
|
|
881
|
+
dispatchButtonPressEvent(node, "press");
|
|
491
882
|
return rerender();
|
|
492
883
|
},
|
|
493
884
|
clickAt(x, y) {
|
|
494
|
-
const hitbox =
|
|
885
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
495
886
|
if (!hitbox) {
|
|
496
887
|
clearSemanticHover(undefined, x, y);
|
|
497
888
|
return currentOutput;
|
|
@@ -523,31 +914,259 @@ export function mountTerminal(input, options = {}) {
|
|
|
523
914
|
return rerender();
|
|
524
915
|
},
|
|
525
916
|
destroy() {
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
917
|
+
if (destroyed) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
destroyed = true;
|
|
921
|
+
cancelPendingEscapeFlush();
|
|
922
|
+
cleanupOutputResize?.();
|
|
923
|
+
cleanupOutputResize = null;
|
|
924
|
+
outputResizeScheduler.cancel();
|
|
925
|
+
renderScheduler.cancel();
|
|
926
|
+
terminalRuntime.destroy();
|
|
927
|
+
emitLifecycleRestore();
|
|
928
|
+
outputWriter.destroy();
|
|
929
|
+
if (runtimeOptions.stdin) {
|
|
930
|
+
if (typeof runtimeOptions.stdin.off === "function") {
|
|
931
|
+
runtimeOptions.stdin.off("data", onData);
|
|
529
932
|
}
|
|
530
|
-
else if (typeof
|
|
531
|
-
|
|
933
|
+
else if (typeof runtimeOptions.stdin.removeListener === "function") {
|
|
934
|
+
runtimeOptions.stdin.removeListener("data", onData);
|
|
532
935
|
}
|
|
533
|
-
|
|
534
|
-
|
|
936
|
+
runtimeOptions.stdin.setRawMode?.(false);
|
|
937
|
+
runtimeOptions.stdin.pause?.();
|
|
535
938
|
}
|
|
536
939
|
}
|
|
537
940
|
};
|
|
538
|
-
|
|
539
|
-
const
|
|
941
|
+
function dispatchPasteText(text) {
|
|
942
|
+
const node = findFocused(currentTree, focusedId);
|
|
943
|
+
if (node?.tag === "terminal-editor") {
|
|
944
|
+
runCommand({ id: "editor.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
|
|
945
|
+
}
|
|
946
|
+
else if (node?.tag === "terminal-input") {
|
|
947
|
+
runCommand({ id: "input.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function cancelPendingEscapeFlush() {
|
|
951
|
+
if (pendingEscapeFlush) {
|
|
952
|
+
clearTimeout(pendingEscapeFlush);
|
|
953
|
+
pendingEscapeFlush = null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function flushPendingEscape() {
|
|
957
|
+
cancelPendingEscapeFlush();
|
|
958
|
+
if (destroyed || !pendingKeyChunk.startsWith(ESCAPE)) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const buffered = pendingKeyChunk;
|
|
962
|
+
pendingKeyChunk = "";
|
|
963
|
+
session.dispatchKey(parseTerminalKey(ESCAPE));
|
|
964
|
+
processInputStream(buffered.slice(ESCAPE.length));
|
|
965
|
+
}
|
|
966
|
+
function schedulePendingEscapeFlush() {
|
|
967
|
+
cancelPendingEscapeFlush();
|
|
968
|
+
pendingEscapeFlush = setTimeout(flushPendingEscape, 0);
|
|
969
|
+
}
|
|
970
|
+
function processKeyStream(value) {
|
|
971
|
+
if (!value) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
|
|
975
|
+
if (value.startsWith(sequence)) {
|
|
976
|
+
session.dispatchKey(parseTerminalKey(sequence));
|
|
977
|
+
processInputStream(value.slice(sequence.length));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (value === ESCAPE) {
|
|
982
|
+
pendingKeyChunk = value;
|
|
983
|
+
schedulePendingEscapeFlush();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (isDisambiguatedBracketedPasteStartPrefix(value)) {
|
|
987
|
+
pendingPasteChunk = value;
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (isKnownTerminalKeySequencePrefix(value)) {
|
|
991
|
+
pendingKeyChunk = value;
|
|
992
|
+
schedulePendingEscapeFlush();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const paste = parseBracketedPaste(value);
|
|
996
|
+
if (paste) {
|
|
997
|
+
dispatchPasteText(paste.text);
|
|
998
|
+
processInputStream(paste.rest);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const escapeIndex = value.indexOf("\u001b");
|
|
1002
|
+
if (escapeIndex > 0) {
|
|
1003
|
+
for (const char of value.slice(0, escapeIndex)) {
|
|
1004
|
+
session.dispatchKey(parseTerminalKey(char));
|
|
1005
|
+
}
|
|
1006
|
+
processInputStream(value.slice(escapeIndex));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const parsedKey = parseTerminalKey(value);
|
|
1010
|
+
if (parsedKey !== value) {
|
|
1011
|
+
session.dispatchKey(parsedKey);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (value.startsWith("\u001b")) {
|
|
1015
|
+
session.dispatchKey(value);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
for (const char of value) {
|
|
1019
|
+
session.dispatchKey(parseTerminalKey(char));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function isPrimaryMouseButton(button) {
|
|
1023
|
+
return button < 64 && (button & 3) === 0;
|
|
1024
|
+
}
|
|
1025
|
+
function isContextMouseButton(button) {
|
|
1026
|
+
return button < 64 && (button & 3) === 2;
|
|
1027
|
+
}
|
|
1028
|
+
function isDoublePrimaryPress(hitbox, row) {
|
|
1029
|
+
const now = Date.now();
|
|
1030
|
+
const isDouble = Boolean(lastPrimaryPress
|
|
1031
|
+
&& lastPrimaryPress.id === hitbox.id
|
|
1032
|
+
&& lastPrimaryPress.tag === hitbox.tag
|
|
1033
|
+
&& lastPrimaryPress.row === row
|
|
1034
|
+
&& now - lastPrimaryPress.at <= DOUBLE_PRESS_INTERVAL_MS);
|
|
1035
|
+
lastPrimaryPress = { id: hitbox.id, tag: hitbox.tag, row, at: now };
|
|
1036
|
+
return isDouble;
|
|
1037
|
+
}
|
|
1038
|
+
function doublePressAt(hitbox, x, y) {
|
|
1039
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
1040
|
+
if (!node) {
|
|
1041
|
+
return currentOutput;
|
|
1042
|
+
}
|
|
1043
|
+
focusedId = hitbox.id;
|
|
1044
|
+
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
1045
|
+
if (node.tag === "terminal-button") {
|
|
1046
|
+
dispatchButtonPressEvent(node, "doublepress");
|
|
1047
|
+
return rerender();
|
|
1048
|
+
}
|
|
1049
|
+
if (node.tag === "terminal-list") {
|
|
1050
|
+
dispatchListPointerPressEvent(node, "doublepress", sourceRowFromHitbox(node, hitbox, y));
|
|
1051
|
+
return rerender();
|
|
1052
|
+
}
|
|
1053
|
+
return currentOutput;
|
|
1054
|
+
}
|
|
1055
|
+
function contextPressAt(x, y) {
|
|
1056
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
1057
|
+
if (!hitbox) {
|
|
1058
|
+
clearSemanticHover(undefined, x, y);
|
|
1059
|
+
return currentOutput;
|
|
1060
|
+
}
|
|
1061
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
1062
|
+
if (!node) {
|
|
1063
|
+
return currentOutput;
|
|
1064
|
+
}
|
|
1065
|
+
focusedId = hitbox.id;
|
|
1066
|
+
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
1067
|
+
if (node.tag === "terminal-button") {
|
|
1068
|
+
dispatchButtonPressEvent(node, "contextpress");
|
|
1069
|
+
return rerender();
|
|
1070
|
+
}
|
|
1071
|
+
if (node.tag === "terminal-list") {
|
|
1072
|
+
dispatchListPointerPressEvent(node, "contextpress", sourceRowFromHitbox(node, hitbox, y));
|
|
1073
|
+
return rerender();
|
|
1074
|
+
}
|
|
1075
|
+
if (node.tag === "terminal-input") {
|
|
1076
|
+
dispatchInputContextPressEvent(node, hitbox, x, y);
|
|
1077
|
+
return rerender();
|
|
1078
|
+
}
|
|
1079
|
+
if (node.tag === "terminal-scroll") {
|
|
1080
|
+
dispatchScrollContextPressEvent(node, sourceRowFromHitbox(node, hitbox, y), x, y);
|
|
1081
|
+
return rerender();
|
|
1082
|
+
}
|
|
1083
|
+
return currentOutput;
|
|
1084
|
+
}
|
|
1085
|
+
function processInputStream(value) {
|
|
1086
|
+
if (!value) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (pendingPasteChunk) {
|
|
1090
|
+
pendingPasteChunk += value;
|
|
1091
|
+
const paste = parseBracketedPaste(pendingPasteChunk);
|
|
1092
|
+
if (!paste && isBracketedPasteStartPrefix(pendingPasteChunk)) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (!paste) {
|
|
1096
|
+
const buffered = pendingPasteChunk;
|
|
1097
|
+
pendingPasteChunk = "";
|
|
1098
|
+
processKeyStream(buffered);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
pendingPasteChunk = "";
|
|
1102
|
+
dispatchPasteText(paste.text);
|
|
1103
|
+
processInputStream(paste.rest);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (pendingKeyChunk) {
|
|
1107
|
+
if (pendingKeyChunk === ESCAPE && !canContinueEscapeSequence(value)) {
|
|
1108
|
+
flushPendingEscape();
|
|
1109
|
+
processInputStream(value);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const buffered = pendingKeyChunk + value;
|
|
1113
|
+
cancelPendingEscapeFlush();
|
|
1114
|
+
pendingKeyChunk = "";
|
|
1115
|
+
processKeyStream(buffered);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (value === ESCAPE) {
|
|
1119
|
+
processKeyStream(value);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (isBracketedPasteStartPrefix(value)) {
|
|
1123
|
+
pendingPasteChunk = value;
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (value.startsWith(BRACKETED_PASTE_START)) {
|
|
1127
|
+
const paste = parseBracketedPaste(value);
|
|
1128
|
+
if (!paste) {
|
|
1129
|
+
pendingPasteChunk = value;
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
dispatchPasteText(paste.text);
|
|
1133
|
+
processInputStream(paste.rest);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const parsed = parseTerminalInput(value);
|
|
540
1137
|
if (parsed.type === "mouse") {
|
|
541
1138
|
if (parsed.action === "press") {
|
|
542
|
-
const hitbox =
|
|
1139
|
+
const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
1140
|
+
if (isContextMouseButton(parsed.button)) {
|
|
1141
|
+
lastPrimaryPress = null;
|
|
1142
|
+
contextPressAt(parsed.x, parsed.y);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
543
1145
|
if (hitbox?.tag === "terminal-input") {
|
|
544
1146
|
mouseSelectionId = hitbox.id;
|
|
545
1147
|
}
|
|
546
1148
|
const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
|
|
547
1149
|
if (hitbox && node && shouldPointerCapture(node)) {
|
|
548
|
-
setPointerCapture(hitbox.id, "press",
|
|
1150
|
+
setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
|
|
1151
|
+
}
|
|
1152
|
+
const isPrimaryPress = isPrimaryMouseButton(parsed.button);
|
|
1153
|
+
const isDoublePressEligible = Boolean(hitbox
|
|
1154
|
+
&& node
|
|
1155
|
+
&& (hitbox.tag === "terminal-button" || hitbox.tag === "terminal-list"));
|
|
1156
|
+
const primaryPressRow = hitbox && node && hitbox.tag === "terminal-list"
|
|
1157
|
+
? sourceRowFromHitbox(node, hitbox, parsed.y)
|
|
1158
|
+
: null;
|
|
1159
|
+
const shouldDispatchDoublePress = Boolean(isPrimaryPress
|
|
1160
|
+
&& isDoublePressEligible
|
|
1161
|
+
&& hitbox
|
|
1162
|
+
&& isDoublePrimaryPress(hitbox, primaryPressRow));
|
|
1163
|
+
if (isPrimaryPress && !isDoublePressEligible) {
|
|
1164
|
+
lastPrimaryPress = null;
|
|
549
1165
|
}
|
|
550
1166
|
session.clickAt(parsed.x, parsed.y);
|
|
1167
|
+
if (hitbox && shouldDispatchDoublePress) {
|
|
1168
|
+
doublePressAt(hitbox, parsed.x, parsed.y);
|
|
1169
|
+
}
|
|
551
1170
|
}
|
|
552
1171
|
else if (parsed.action === "drag") {
|
|
553
1172
|
if (mouseSelectionId) {
|
|
@@ -560,13 +1179,13 @@ export function mountTerminal(input, options = {}) {
|
|
|
560
1179
|
else if (parsed.action === "release") {
|
|
561
1180
|
mouseSelectionId = null;
|
|
562
1181
|
const capturedId = pointerCaptureId;
|
|
563
|
-
const releaseHitbox =
|
|
1182
|
+
const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
564
1183
|
const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
|
|
565
1184
|
const releaseRow = releaseHitbox && releaseNode
|
|
566
|
-
?
|
|
1185
|
+
? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
|
|
567
1186
|
: null;
|
|
568
1187
|
setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
|
|
569
|
-
const hitbox =
|
|
1188
|
+
const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
570
1189
|
if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
|
|
571
1190
|
setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
|
|
572
1191
|
rerender();
|
|
@@ -577,23 +1196,63 @@ export function mountTerminal(input, options = {}) {
|
|
|
577
1196
|
}
|
|
578
1197
|
}
|
|
579
1198
|
else if (parsed.action === "wheel-up") {
|
|
580
|
-
|
|
581
|
-
session.dispatchKey("UP");
|
|
1199
|
+
wheelAt(parsed.x, parsed.y, -1);
|
|
582
1200
|
}
|
|
583
1201
|
else if (parsed.action === "wheel-down") {
|
|
584
|
-
|
|
585
|
-
session.dispatchKey("DOWN");
|
|
1202
|
+
wheelAt(parsed.x, parsed.y, 1);
|
|
586
1203
|
}
|
|
587
1204
|
return;
|
|
588
1205
|
}
|
|
589
|
-
|
|
1206
|
+
processKeyStream(value);
|
|
1207
|
+
}
|
|
1208
|
+
const onData = (chunk) => {
|
|
1209
|
+
const value = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
1210
|
+
processInputStream(value);
|
|
590
1211
|
};
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1212
|
+
if (runtimeOptions.stdin) {
|
|
1213
|
+
runtimeOptions.stdin.on("data", onData);
|
|
1214
|
+
runtimeOptions.stdin.setRawMode?.(true);
|
|
1215
|
+
runtimeOptions.stdin.resume?.();
|
|
1216
|
+
}
|
|
1217
|
+
function subscribeOutputResize() {
|
|
1218
|
+
const stdout = runtimeOptions.stdout;
|
|
1219
|
+
if (!stdout || typeof stdout.on !== "function") {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const removeResizeListener = typeof stdout.off === "function" ? stdout.off.bind(stdout) : stdout.removeListener?.bind(stdout);
|
|
1223
|
+
if (!removeResizeListener) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const onResize = () => {
|
|
1227
|
+
const cols = stdout.columns;
|
|
1228
|
+
const rows = stdout.rows;
|
|
1229
|
+
if (!isValidTerminalDimension(cols) || !isValidTerminalDimension(rows)) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
pendingOutputResize = { cols, rows };
|
|
1233
|
+
outputResizeScheduler.requestRender();
|
|
1234
|
+
};
|
|
1235
|
+
try {
|
|
1236
|
+
stdout.on("resize", onResize);
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
cleanupOutputResize = () => removeResizeListener("resize", onResize);
|
|
1242
|
+
}
|
|
1243
|
+
function applyPendingOutputResize() {
|
|
1244
|
+
const nextSize = pendingOutputResize;
|
|
1245
|
+
pendingOutputResize = null;
|
|
1246
|
+
if (!nextSize || destroyed || (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows)) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
terminalSize = nextSize;
|
|
1250
|
+
rerender();
|
|
595
1251
|
}
|
|
1252
|
+
subscribeOutputResize();
|
|
1253
|
+
emitLifecycleSetup();
|
|
596
1254
|
emitOutput();
|
|
1255
|
+
autoProjectionEnabled = true;
|
|
597
1256
|
return session;
|
|
598
1257
|
}
|
|
599
1258
|
//# sourceMappingURL=session.js.map
|