@valyrianjs/terminal 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -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 +8 -1
- 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 +8 -3
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +840 -67
- 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 +215 -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 +729 -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 +183 -18
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +302 -136
- package/docs/assets/quick-note.svg +13 -0
- package/docs/cookbook.md +297 -202
- package/docs/core-concepts.md +143 -55
- package/docs/getting-started.md +209 -90
- package/docs/interaction-model.md +95 -61
- package/docs/primitive-gallery.md +365 -0
- package/docs/session-runtime.md +132 -363
- package/docs/valyrian-modules.md +3196 -0
- package/llms-full.txt +5357 -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 +92 -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 +52 -0
- package/src/render.ts +1107 -0
- package/src/runtime.ts +273 -0
- package/src/scheduler.ts +33 -0
- package/src/session.ts +1260 -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 +523 -0
- package/tsconfig.json +4 -7
- package/docs/local-demo.md +0 -28
package/dist/session.js
CHANGED
|
@@ -1,11 +1,100 @@
|
|
|
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
|
+
function isBracketedPasteStartPrefix(value) {
|
|
41
|
+
return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
|
|
42
|
+
}
|
|
43
|
+
function isDisambiguatedBracketedPasteStartPrefix(value) {
|
|
44
|
+
return isBracketedPasteStartPrefix(value) && value.length > CSI_PREFIX.length;
|
|
45
|
+
}
|
|
46
|
+
function isKnownTerminalKeySequencePrefix(value) {
|
|
47
|
+
return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
|
|
48
|
+
}
|
|
49
|
+
function canContinueEscapeSequence(value) {
|
|
50
|
+
return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
|
|
51
|
+
}
|
|
52
|
+
function validateTerminalDimension(name, value) {
|
|
53
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
54
|
+
throw new Error(`Invalid terminal ${name}: expected an integer >= 1`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function isValidTerminalDimension(value) {
|
|
59
|
+
return Number.isInteger(value) && Number(value) >= 1;
|
|
60
|
+
}
|
|
61
|
+
function getProcessStdin() {
|
|
62
|
+
const candidate = globalThis.process?.stdin;
|
|
63
|
+
return candidate && typeof candidate.on === "function" ? candidate : undefined;
|
|
64
|
+
}
|
|
65
|
+
function getProcessStdout() {
|
|
66
|
+
const candidate = globalThis.process?.stdout;
|
|
67
|
+
return candidate && typeof candidate.write === "function" ? candidate : undefined;
|
|
68
|
+
}
|
|
69
|
+
function isInteractiveTTY(stdin, stdout) {
|
|
70
|
+
return Boolean(stdin?.isTTY && stdout?.isTTY);
|
|
71
|
+
}
|
|
72
|
+
function resolveRuntimeOptions(options) {
|
|
73
|
+
const processStdin = getProcessStdin();
|
|
74
|
+
const processStdout = getProcessStdout();
|
|
75
|
+
const canUseProcessTTY = isInteractiveTTY(processStdin, processStdout);
|
|
76
|
+
const runtime = options.runtime ?? (canUseProcessTTY || options.stdin || options.stdout ? "app" : "headless");
|
|
77
|
+
const stdin = options.stdin ?? (runtime === "app" && canUseProcessTTY ? processStdin : undefined);
|
|
78
|
+
const stdout = options.stdout ?? (runtime === "app" && canUseProcessTTY ? processStdout : undefined);
|
|
79
|
+
const ownsInteractiveTTY = runtime === "app" && isInteractiveTTY(stdin, stdout);
|
|
80
|
+
return {
|
|
81
|
+
runtime,
|
|
82
|
+
stdin,
|
|
83
|
+
stdout,
|
|
84
|
+
alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
|
|
85
|
+
hideCursor: options.hideCursor ?? ownsInteractiveTTY,
|
|
86
|
+
writesAnsi: runtime === "app" && Boolean(stdout)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function resolveTerminalSize(options, stdout) {
|
|
90
|
+
const cols = options.cols ?? stdout?.columns ?? DEFAULT_TERMINAL_SIZE.cols;
|
|
91
|
+
const rows = options.rows ?? stdout?.rows ?? DEFAULT_TERMINAL_SIZE.rows;
|
|
92
|
+
return {
|
|
93
|
+
cols: validateTerminalDimension("cols", cols),
|
|
94
|
+
rows: validateTerminalDimension("rows", rows)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function applyInteractiveState(nodes, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById) {
|
|
9
98
|
for (let i = 0; i < nodes.length; i += 1) {
|
|
10
99
|
const node = nodes[i];
|
|
11
100
|
if (node.type !== "element") {
|
|
@@ -14,11 +103,18 @@ function applyInteractiveState(nodes, focusedId, inputStateById, listIndexById,
|
|
|
14
103
|
const id = node.props.id;
|
|
15
104
|
node.props.__focused = Boolean(focusedId && id === focusedId);
|
|
16
105
|
if (node.tag === "terminal-input" && id) {
|
|
17
|
-
const value =
|
|
106
|
+
const value = plainText(node.props.value ?? "");
|
|
18
107
|
const current = normalizeInputState(inputStateById.get(id), value.length);
|
|
19
108
|
inputStateById.set(id, current);
|
|
20
109
|
node.props.__inputState = current;
|
|
21
110
|
}
|
|
111
|
+
if (node.tag === "terminal-editor" && id) {
|
|
112
|
+
const value = plainText(node.props.value ?? "");
|
|
113
|
+
const previous = editorStateById.get(id);
|
|
114
|
+
const current = createEditorState(value, previous?.cursor, previous?.desiredColumn);
|
|
115
|
+
editorStateById.set(id, current);
|
|
116
|
+
node.props.__editorState = current;
|
|
117
|
+
}
|
|
22
118
|
if (node.tag === "terminal-list" && id) {
|
|
23
119
|
node.props.__selectedIndex = listIndexById.get(id) || 0;
|
|
24
120
|
if (listHoverById.has(id)) {
|
|
@@ -31,46 +127,133 @@ function applyInteractiveState(nodes, focusedId, inputStateById, listIndexById,
|
|
|
31
127
|
node.props.__hoveredRow = scrollHoverRowById.get(id);
|
|
32
128
|
}
|
|
33
129
|
}
|
|
34
|
-
applyInteractiveState(node.children, focusedId, inputStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
130
|
+
applyInteractiveState(node.children, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
35
131
|
}
|
|
36
132
|
}
|
|
37
133
|
export function mountTerminal(input, options = {}) {
|
|
38
134
|
let focusedId = null;
|
|
135
|
+
let skipFocusContainmentOnce = false;
|
|
39
136
|
let clipboardValue = "";
|
|
40
137
|
let mouseSelectionId = null;
|
|
41
138
|
let pointerCaptureId = null;
|
|
139
|
+
let pendingPasteChunk = "";
|
|
140
|
+
let pendingKeyChunk = "";
|
|
141
|
+
let pendingEscapeFlush = null;
|
|
142
|
+
let destroyed = false;
|
|
143
|
+
let autoProjectionEnabled = false;
|
|
144
|
+
let suppressAutoProjection = false;
|
|
42
145
|
const clipboardAdapter = options.clipboard === false ? null : options.clipboard || createSystemClipboardAdapter();
|
|
43
146
|
const inputStateById = new Map();
|
|
147
|
+
const editorStateById = new Map();
|
|
44
148
|
const listIndexById = new Map();
|
|
45
149
|
const scrollOffsetById = new Map();
|
|
46
150
|
const listHoverById = new Map();
|
|
47
151
|
const scrollHoverRowById = new Map();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
152
|
+
const keyBindings = createResolvedTerminalKeymap(options.keymap?.bindings);
|
|
153
|
+
const runtimeOptions = resolveRuntimeOptions(options);
|
|
154
|
+
let terminalSize = resolveTerminalSize(options, runtimeOptions.stdout);
|
|
155
|
+
const terminalRuntime = createValyrianTerminalRuntime(input, () => {
|
|
156
|
+
if (!autoProjectionEnabled || suppressAutoProjection || destroyed) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
renderNow();
|
|
160
|
+
});
|
|
161
|
+
let currentTree = terminalRuntime.project();
|
|
162
|
+
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
163
|
+
let currentFrame = renderTreeFrame(currentTree);
|
|
164
|
+
let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
52
165
|
let currentHitboxes = currentFrame.hitboxes;
|
|
53
|
-
const
|
|
166
|
+
const outputWriter = createOutputWriter(runtimeOptions.stdout);
|
|
167
|
+
const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
168
|
+
const renderScheduler = createRenderScheduler(renderNow);
|
|
169
|
+
const outputResizeScheduler = createRenderScheduler(applyPendingOutputResize);
|
|
170
|
+
let cleanupOutputResize = null;
|
|
171
|
+
let pendingOutputResize = null;
|
|
172
|
+
function renderContext() {
|
|
173
|
+
return { ...terminalSize, theme: options.theme };
|
|
174
|
+
}
|
|
175
|
+
function renderTreeFrame(nodes) {
|
|
176
|
+
const context = renderContext();
|
|
177
|
+
return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
|
|
178
|
+
}
|
|
179
|
+
function emitLifecycleSetup() {
|
|
180
|
+
const writes = [];
|
|
181
|
+
if (runtimeOptions.alternateScreen) {
|
|
182
|
+
writes.push(ANSI_ENTER_ALTERNATE_SCREEN);
|
|
183
|
+
}
|
|
184
|
+
if (runtimeOptions.hideCursor) {
|
|
185
|
+
writes.push(ANSI_HIDE_CURSOR);
|
|
186
|
+
}
|
|
187
|
+
if (writes.length > 0) {
|
|
188
|
+
outputWriter.write(writes.join(""), { force: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function emitLifecycleRestore() {
|
|
192
|
+
if (options.restoreOnDestroy === false) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const writes = [];
|
|
196
|
+
if (runtimeOptions.hideCursor) {
|
|
197
|
+
writes.push(ANSI_SHOW_CURSOR);
|
|
198
|
+
}
|
|
199
|
+
if (runtimeOptions.alternateScreen) {
|
|
200
|
+
writes.push(ANSI_EXIT_ALTERNATE_SCREEN);
|
|
201
|
+
}
|
|
202
|
+
if (writes.length > 0) {
|
|
203
|
+
outputWriter.write(writes.join(""), { force: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
54
206
|
function emitOutput() {
|
|
55
|
-
if (
|
|
56
|
-
|
|
207
|
+
if (!destroyed) {
|
|
208
|
+
outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
|
|
57
209
|
}
|
|
58
210
|
}
|
|
59
|
-
function
|
|
60
|
-
currentTree =
|
|
211
|
+
function renderNow() {
|
|
212
|
+
currentTree = terminalRuntime.project();
|
|
61
213
|
const focusables = [];
|
|
62
214
|
collectFocusableNodes(currentTree, focusables);
|
|
63
|
-
|
|
64
|
-
|
|
215
|
+
const overlayFocusables = [];
|
|
216
|
+
collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
|
|
217
|
+
const scopedFocusables = [];
|
|
218
|
+
collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
|
|
219
|
+
const activeFocusables = overlayFocusables.length ? overlayFocusables : scopedFocusables.length ? scopedFocusables : focusables;
|
|
220
|
+
if (!skipFocusContainmentOnce && focusedId && !activeFocusables.some((node) => node.props.id === focusedId)) {
|
|
221
|
+
focusedId = activeFocusables[0]?.props.id || null;
|
|
65
222
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
223
|
+
skipFocusContainmentOnce = false;
|
|
224
|
+
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
225
|
+
currentFrame = renderTreeFrame(currentTree);
|
|
226
|
+
currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
69
227
|
currentHitboxes = currentFrame.hitboxes;
|
|
70
228
|
emitOutput();
|
|
71
229
|
return currentOutput;
|
|
72
230
|
}
|
|
231
|
+
function rerender() {
|
|
232
|
+
if (destroyed) {
|
|
233
|
+
return currentOutput;
|
|
234
|
+
}
|
|
235
|
+
suppressAutoProjection = true;
|
|
236
|
+
try {
|
|
237
|
+
terminalRuntime.update();
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
suppressAutoProjection = false;
|
|
241
|
+
}
|
|
242
|
+
renderScheduler.requestRender();
|
|
243
|
+
renderScheduler.flush();
|
|
244
|
+
return currentOutput;
|
|
245
|
+
}
|
|
73
246
|
function orderedFocusables() {
|
|
247
|
+
const overlayFocusables = [];
|
|
248
|
+
collectDirectOverlayFocusableNodes(currentTree, overlayFocusables);
|
|
249
|
+
if (overlayFocusables.length) {
|
|
250
|
+
return overlayFocusables;
|
|
251
|
+
}
|
|
252
|
+
const scopedFocusables = [];
|
|
253
|
+
collectActiveFocusScopeFocusableNodes(currentTree, focusedId, scopedFocusables);
|
|
254
|
+
if (scopedFocusables.length) {
|
|
255
|
+
return scopedFocusables;
|
|
256
|
+
}
|
|
74
257
|
const out = [];
|
|
75
258
|
collectFocusableNodes(currentTree, out);
|
|
76
259
|
return out;
|
|
@@ -105,21 +288,51 @@ export function mountTerminal(input, options = {}) {
|
|
|
105
288
|
}
|
|
106
289
|
function setInputCursor(id, cursor, extendSelection = false) {
|
|
107
290
|
const node = findFocusableById(currentTree, id);
|
|
108
|
-
const value =
|
|
291
|
+
const value = stripTerminalControls(node?.props.value ?? "");
|
|
109
292
|
const current = normalizeInputState(inputStateById.get(id), value.length);
|
|
110
293
|
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
294
|
}
|
|
112
295
|
function replaceInputState(id, state) {
|
|
113
296
|
const node = findFocusableById(currentTree, id);
|
|
114
|
-
const value =
|
|
297
|
+
const value = stripTerminalControls(node?.props.value ?? "");
|
|
115
298
|
inputStateById.set(id, normalizeInputState(state, value.length));
|
|
116
299
|
}
|
|
300
|
+
function dispatchNodeEvent(node, type, payload) {
|
|
301
|
+
const id = String(node.props.id || "");
|
|
302
|
+
if (!id) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return terminalRuntime.dispatchTerminalEvent(id, type, payload);
|
|
306
|
+
}
|
|
117
307
|
function updateInputByState(node, next) {
|
|
118
308
|
const id = String(node.props.id || "");
|
|
119
|
-
|
|
309
|
+
dispatchNodeEvent(node, "change", { type: "change", id, value: next.value });
|
|
120
310
|
inputStateById.set(id, next.state);
|
|
121
311
|
return rerender();
|
|
122
312
|
}
|
|
313
|
+
function replaceEditorState(id, state) {
|
|
314
|
+
const node = findFocusableById(currentTree, id);
|
|
315
|
+
const value = plainText(node?.props.value ?? "");
|
|
316
|
+
editorStateById.set(id, createEditorState(value, state.cursor, state.desiredColumn));
|
|
317
|
+
}
|
|
318
|
+
function updateEditorValue(node, nextValue) {
|
|
319
|
+
const id = String(node.props.id || "");
|
|
320
|
+
dispatchNodeEvent(node, "change", { type: "change", id, value: plainText(nextValue) });
|
|
321
|
+
}
|
|
322
|
+
function updateEditorByState(node, next) {
|
|
323
|
+
const id = String(node.props.id || "");
|
|
324
|
+
updateEditorValue(node, next.value);
|
|
325
|
+
editorStateById.set(id, next.state);
|
|
326
|
+
return rerender();
|
|
327
|
+
}
|
|
328
|
+
function submitEditor(node) {
|
|
329
|
+
const id = String(node.props.id || "");
|
|
330
|
+
dispatchNodeEvent(node, "submit", { type: "submit", id, value: plainText(node.props.value ?? "") });
|
|
331
|
+
}
|
|
332
|
+
function cancelEditor(node) {
|
|
333
|
+
const id = String(node.props.id || "");
|
|
334
|
+
dispatchNodeEvent(node, "cancel", { type: "cancel", id, value: plainText(node.props.value ?? "") });
|
|
335
|
+
}
|
|
123
336
|
function setCursorFromHitbox(id, x, extendSelection = false) {
|
|
124
337
|
const hitbox = currentHitboxes.find((box) => box.id === id && box.tag === "terminal-input");
|
|
125
338
|
if (!hitbox) {
|
|
@@ -130,7 +343,8 @@ export function mountTerminal(input, options = {}) {
|
|
|
130
343
|
return rerender();
|
|
131
344
|
}
|
|
132
345
|
function visibleScrollLines(node) {
|
|
133
|
-
const
|
|
346
|
+
const context = renderContext();
|
|
347
|
+
const lines = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context))).lines;
|
|
134
348
|
const offset = Number(node.props.__scrollOffset || 0);
|
|
135
349
|
const height = Number(node.props.height || lines.length || 0);
|
|
136
350
|
return lines.slice(offset, offset + height).map((line) => line.trimEnd());
|
|
@@ -145,6 +359,13 @@ export function mountTerminal(input, options = {}) {
|
|
|
145
359
|
}
|
|
146
360
|
return 1;
|
|
147
361
|
}
|
|
362
|
+
function sourceRowFromHitbox(node, hitbox, y) {
|
|
363
|
+
const visibleRow = Math.max(1, y - hitbox.y1 + 1);
|
|
364
|
+
if (node.tag !== "terminal-list") {
|
|
365
|
+
return Math.max(1, Math.min(rowCountForNode(node), visibleRow));
|
|
366
|
+
}
|
|
367
|
+
return Math.max(1, Math.min(rowCountForNode(node), visibleRow + (hitbox.itemOffset || 0)));
|
|
368
|
+
}
|
|
148
369
|
function shouldPointerCapture(node) {
|
|
149
370
|
return Boolean(node.props.pointerCapture && (node.tag === "terminal-list" || node.tag === "terminal-scroll"));
|
|
150
371
|
}
|
|
@@ -166,12 +387,8 @@ export function mountTerminal(input, options = {}) {
|
|
|
166
387
|
if (!node || !node.props.id) {
|
|
167
388
|
return;
|
|
168
389
|
}
|
|
169
|
-
const handler = node.props[`on${type}`];
|
|
170
|
-
if (typeof handler !== "function") {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
390
|
const payload = { type, id: node.props.id, source, row, x, y };
|
|
174
|
-
|
|
391
|
+
dispatchNodeEvent(node, type, payload);
|
|
175
392
|
}
|
|
176
393
|
function setPointerCapture(id, source, row = null, x = null, y = null) {
|
|
177
394
|
if (pointerCaptureId === id) {
|
|
@@ -188,8 +405,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
188
405
|
}
|
|
189
406
|
}
|
|
190
407
|
function emitMouseRowEvent(node, type, row, x = null, y = null) {
|
|
191
|
-
|
|
192
|
-
if (typeof handler !== "function" || !node.props.id) {
|
|
408
|
+
if (!node.props.id) {
|
|
193
409
|
return;
|
|
194
410
|
}
|
|
195
411
|
if (node.tag === "terminal-list") {
|
|
@@ -199,7 +415,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
199
415
|
return;
|
|
200
416
|
}
|
|
201
417
|
const payload = { type, id: node.props.id, row: index + 1, index, value: items[index], x, y };
|
|
202
|
-
|
|
418
|
+
dispatchNodeEvent(node, type, payload);
|
|
203
419
|
return;
|
|
204
420
|
}
|
|
205
421
|
if (node.tag === "terminal-scroll") {
|
|
@@ -209,7 +425,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
209
425
|
return;
|
|
210
426
|
}
|
|
211
427
|
const payload = { type, id: node.props.id, row: index + 1, value: lines[index], x, y };
|
|
212
|
-
|
|
428
|
+
dispatchNodeEvent(node, type, payload);
|
|
213
429
|
}
|
|
214
430
|
}
|
|
215
431
|
function clearSemanticHover(exceptId, x = null, y = null) {
|
|
@@ -242,7 +458,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
242
458
|
return;
|
|
243
459
|
}
|
|
244
460
|
clearSemanticHover(id, x, y);
|
|
245
|
-
const row =
|
|
461
|
+
const row = sourceRowFromHitbox(node, hitbox, y);
|
|
246
462
|
if (node.tag === "terminal-list") {
|
|
247
463
|
const nextIndex = row - 1;
|
|
248
464
|
const prevIndex = listHoverById.get(id);
|
|
@@ -272,7 +488,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
272
488
|
}
|
|
273
489
|
}
|
|
274
490
|
function hoverAt(x, y) {
|
|
275
|
-
const hitbox =
|
|
491
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
276
492
|
if (!hitbox) {
|
|
277
493
|
if (pointerCaptureId) {
|
|
278
494
|
setSemanticHoverFromHitbox(pointerCaptureId, x, y);
|
|
@@ -283,12 +499,263 @@ export function mountTerminal(input, options = {}) {
|
|
|
283
499
|
}
|
|
284
500
|
const node = findFocusableById(currentTree, hitbox.id);
|
|
285
501
|
if (node && shouldPointerCapture(node)) {
|
|
286
|
-
setPointerCapture(hitbox.id, "drag",
|
|
502
|
+
setPointerCapture(hitbox.id, "drag", sourceRowFromHitbox(node, hitbox, y), x, y);
|
|
287
503
|
}
|
|
288
504
|
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
289
505
|
return rerender();
|
|
290
506
|
}
|
|
507
|
+
function changeListSelection(node, direction) {
|
|
508
|
+
const id = node.props.id;
|
|
509
|
+
if (!id) {
|
|
510
|
+
return currentOutput;
|
|
511
|
+
}
|
|
512
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
513
|
+
const currentIndex = listIndexById.get(id) || 0;
|
|
514
|
+
const nextIndex = direction < 0 ? Math.max(0, currentIndex - 1) : Math.min(items.length - 1, currentIndex + 1);
|
|
515
|
+
listIndexById.set(id, nextIndex);
|
|
516
|
+
const payload = { type: "change", id, index: nextIndex, value: items[nextIndex] };
|
|
517
|
+
dispatchNodeEvent(node, "change", payload);
|
|
518
|
+
return rerender();
|
|
519
|
+
}
|
|
520
|
+
function pressListSelection(node) {
|
|
521
|
+
const id = node.props.id;
|
|
522
|
+
if (!id) {
|
|
523
|
+
return currentOutput;
|
|
524
|
+
}
|
|
525
|
+
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
526
|
+
const currentIndex = listIndexById.get(id) || 0;
|
|
527
|
+
const payload = { type: "press", id, index: currentIndex, value: items[currentIndex] };
|
|
528
|
+
dispatchNodeEvent(node, "press", payload);
|
|
529
|
+
return rerender();
|
|
530
|
+
}
|
|
531
|
+
function scrollFocusedNode(node, direction) {
|
|
532
|
+
const id = node.props.id;
|
|
533
|
+
if (!id) {
|
|
534
|
+
return currentOutput;
|
|
535
|
+
}
|
|
536
|
+
const context = renderContext();
|
|
537
|
+
const rendered = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
|
|
538
|
+
const height = Number(node.props.height || rendered.lines.length || 0);
|
|
539
|
+
const currentOffset = scrollOffsetById.get(id) || 0;
|
|
540
|
+
const maxOffset = Math.max(0, rendered.lines.length - height);
|
|
541
|
+
scrollOffsetById.set(id, direction < 0 ? Math.max(0, currentOffset - 1) : Math.min(maxOffset, currentOffset + 1));
|
|
542
|
+
return rerender();
|
|
543
|
+
}
|
|
544
|
+
function wheelAt(x, y, direction) {
|
|
545
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
546
|
+
if (!hitbox) {
|
|
547
|
+
return currentOutput;
|
|
548
|
+
}
|
|
549
|
+
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
550
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
551
|
+
if (node?.tag === "terminal-scroll") {
|
|
552
|
+
return scrollFocusedNode(node, direction);
|
|
553
|
+
}
|
|
554
|
+
if (node?.tag === "terminal-list" && node.props.virtualized) {
|
|
555
|
+
return changeListSelection(node, direction);
|
|
556
|
+
}
|
|
557
|
+
return rerender();
|
|
558
|
+
}
|
|
559
|
+
function runInputCommand(node, command) {
|
|
560
|
+
const id = node.props.id;
|
|
561
|
+
if (!id) {
|
|
562
|
+
return currentOutput;
|
|
563
|
+
}
|
|
564
|
+
const currentValue = stripTerminalControls(node.props.value ?? "");
|
|
565
|
+
const state = normalizeInputState(inputStateById.get(id), currentValue.length);
|
|
566
|
+
switch (command.id) {
|
|
567
|
+
case "input.submit":
|
|
568
|
+
dispatchNodeEvent(node, "submit", { type: "submit", id, value: stripTerminalControls(node.props.value ?? "") });
|
|
569
|
+
return rerender();
|
|
570
|
+
case "input.cursorLeft":
|
|
571
|
+
replaceInputState(id, moveCursorLeft(state, currentValue.length));
|
|
572
|
+
return rerender();
|
|
573
|
+
case "input.cursorRight":
|
|
574
|
+
replaceInputState(id, moveCursorRight(state, currentValue.length));
|
|
575
|
+
return rerender();
|
|
576
|
+
case "input.selectLeft":
|
|
577
|
+
replaceInputState(id, moveCursorLeft(state, currentValue.length, true));
|
|
578
|
+
return rerender();
|
|
579
|
+
case "input.selectRight":
|
|
580
|
+
replaceInputState(id, moveCursorRight(state, currentValue.length, true));
|
|
581
|
+
return rerender();
|
|
582
|
+
case "input.wordLeft":
|
|
583
|
+
replaceInputState(id, moveCursorWordLeft(currentValue, state));
|
|
584
|
+
return rerender();
|
|
585
|
+
case "input.wordRight":
|
|
586
|
+
replaceInputState(id, moveCursorWordRight(currentValue, state));
|
|
587
|
+
return rerender();
|
|
588
|
+
case "input.home":
|
|
589
|
+
replaceInputState(id, moveCursorHome(state, currentValue.length));
|
|
590
|
+
return rerender();
|
|
591
|
+
case "input.end":
|
|
592
|
+
replaceInputState(id, moveCursorEnd(state, currentValue.length));
|
|
593
|
+
return rerender();
|
|
594
|
+
case "input.selectAll":
|
|
595
|
+
replaceInputState(id, selectAll(currentValue));
|
|
596
|
+
return rerender();
|
|
597
|
+
case "input.copy":
|
|
598
|
+
writeClipboard(copySelection(currentValue, state));
|
|
599
|
+
return rerender();
|
|
600
|
+
case "input.cut":
|
|
601
|
+
writeClipboard(copySelection(currentValue, state));
|
|
602
|
+
if (hasSelection(state)) {
|
|
603
|
+
return updateInputByState(node, insertText(currentValue, state, ""));
|
|
604
|
+
}
|
|
605
|
+
return currentOutput;
|
|
606
|
+
case "input.paste":
|
|
607
|
+
return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(readClipboard())));
|
|
608
|
+
case "input.pasteText":
|
|
609
|
+
return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
|
|
610
|
+
case "input.backspace":
|
|
611
|
+
return updateInputByState(node, removeBackward(currentValue, state));
|
|
612
|
+
case "input.delete":
|
|
613
|
+
return updateInputByState(node, removeForward(currentValue, state));
|
|
614
|
+
case "input.insertText":
|
|
615
|
+
return updateInputByState(node, insertText(currentValue, state, stripTerminalControls(command.text ?? "")));
|
|
616
|
+
default:
|
|
617
|
+
return currentOutput;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function isBuiltInInputCommand(command) {
|
|
621
|
+
return command.id === "input.submit"
|
|
622
|
+
|| command.id === "input.cursorLeft"
|
|
623
|
+
|| command.id === "input.cursorRight"
|
|
624
|
+
|| command.id === "input.selectLeft"
|
|
625
|
+
|| command.id === "input.selectRight"
|
|
626
|
+
|| command.id === "input.wordLeft"
|
|
627
|
+
|| command.id === "input.wordRight"
|
|
628
|
+
|| command.id === "input.home"
|
|
629
|
+
|| command.id === "input.end"
|
|
630
|
+
|| command.id === "input.selectAll"
|
|
631
|
+
|| command.id === "input.copy"
|
|
632
|
+
|| command.id === "input.cut"
|
|
633
|
+
|| command.id === "input.paste"
|
|
634
|
+
|| command.id === "input.pasteText"
|
|
635
|
+
|| command.id === "input.backspace"
|
|
636
|
+
|| command.id === "input.delete"
|
|
637
|
+
|| command.id === "input.insertText";
|
|
638
|
+
}
|
|
639
|
+
function runEditorCommand(node, command) {
|
|
640
|
+
const id = node.props.id;
|
|
641
|
+
if (!id) {
|
|
642
|
+
return currentOutput;
|
|
643
|
+
}
|
|
644
|
+
const currentValue = plainText(node.props.value ?? "");
|
|
645
|
+
const previous = editorStateById.get(id);
|
|
646
|
+
const state = createEditorState(currentValue, previous?.cursor, previous?.desiredColumn);
|
|
647
|
+
switch (command.id) {
|
|
648
|
+
case "editor.submit":
|
|
649
|
+
submitEditor(node);
|
|
650
|
+
return rerender();
|
|
651
|
+
case "editor.cancel":
|
|
652
|
+
cancelEditor(node);
|
|
653
|
+
return rerender();
|
|
654
|
+
case "editor.newline":
|
|
655
|
+
return updateEditorByState(node, insertEditorText(currentValue, state, "\n"));
|
|
656
|
+
case "editor.backspace":
|
|
657
|
+
return updateEditorByState(node, removeEditorBackward(currentValue, state));
|
|
658
|
+
case "editor.delete":
|
|
659
|
+
return updateEditorByState(node, removeEditorForward(currentValue, state));
|
|
660
|
+
case "editor.cursorLeft":
|
|
661
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "left"));
|
|
662
|
+
return rerender();
|
|
663
|
+
case "editor.cursorRight":
|
|
664
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "right"));
|
|
665
|
+
return rerender();
|
|
666
|
+
case "editor.cursorUp":
|
|
667
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "up"));
|
|
668
|
+
return rerender();
|
|
669
|
+
case "editor.cursorDown":
|
|
670
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "down"));
|
|
671
|
+
return rerender();
|
|
672
|
+
case "editor.home":
|
|
673
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "home"));
|
|
674
|
+
return rerender();
|
|
675
|
+
case "editor.end":
|
|
676
|
+
replaceEditorState(id, moveEditorCursor(currentValue, state, "end"));
|
|
677
|
+
return rerender();
|
|
678
|
+
case "editor.paste":
|
|
679
|
+
return updateEditorByState(node, insertEditorText(currentValue, state, plainText(readClipboard())));
|
|
680
|
+
case "editor.pasteText":
|
|
681
|
+
case "editor.insertText":
|
|
682
|
+
return updateEditorByState(node, insertEditorText(currentValue, state, plainText(command.text ?? "")));
|
|
683
|
+
default:
|
|
684
|
+
return currentOutput;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function isBuiltInEditorCommand(command) {
|
|
688
|
+
return command.id === "editor.submit"
|
|
689
|
+
|| command.id === "editor.cancel"
|
|
690
|
+
|| command.id === "editor.newline"
|
|
691
|
+
|| command.id === "editor.backspace"
|
|
692
|
+
|| command.id === "editor.delete"
|
|
693
|
+
|| command.id === "editor.cursorLeft"
|
|
694
|
+
|| command.id === "editor.cursorRight"
|
|
695
|
+
|| command.id === "editor.cursorUp"
|
|
696
|
+
|| command.id === "editor.cursorDown"
|
|
697
|
+
|| command.id === "editor.home"
|
|
698
|
+
|| command.id === "editor.end"
|
|
699
|
+
|| command.id === "editor.paste"
|
|
700
|
+
|| command.id === "editor.pasteText"
|
|
701
|
+
|| command.id === "editor.insertText";
|
|
702
|
+
}
|
|
703
|
+
function runCommand(command, node, context) {
|
|
704
|
+
switch (command.id) {
|
|
705
|
+
case "focus.next":
|
|
706
|
+
session.focusNext();
|
|
707
|
+
return currentOutput;
|
|
708
|
+
case "focus.prev":
|
|
709
|
+
session.focusPrev();
|
|
710
|
+
return currentOutput;
|
|
711
|
+
case "button.press":
|
|
712
|
+
if (node?.tag === "terminal-button") {
|
|
713
|
+
const id = String(node.props.id || "");
|
|
714
|
+
dispatchNodeEvent(node, "press", { type: "press", id });
|
|
715
|
+
return rerender();
|
|
716
|
+
}
|
|
717
|
+
return currentOutput;
|
|
718
|
+
case "list.prev":
|
|
719
|
+
return node?.tag === "terminal-list" ? changeListSelection(node, -1) : currentOutput;
|
|
720
|
+
case "list.next":
|
|
721
|
+
return node?.tag === "terminal-list" ? changeListSelection(node, 1) : currentOutput;
|
|
722
|
+
case "list.press":
|
|
723
|
+
return node?.tag === "terminal-list" ? pressListSelection(node) : currentOutput;
|
|
724
|
+
case "scroll.up":
|
|
725
|
+
return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, -1) : currentOutput;
|
|
726
|
+
case "scroll.down":
|
|
727
|
+
return node?.tag === "terminal-scroll" ? scrollFocusedNode(node, 1) : currentOutput;
|
|
728
|
+
default:
|
|
729
|
+
if (node?.tag === "terminal-input" && isBuiltInInputCommand(command)) {
|
|
730
|
+
return runInputCommand(node, command);
|
|
731
|
+
}
|
|
732
|
+
if (node?.tag === "terminal-editor" && isBuiltInEditorCommand(command)) {
|
|
733
|
+
return runEditorCommand(node, command);
|
|
734
|
+
}
|
|
735
|
+
if (typeof options.keymap?.onCommand === "function") {
|
|
736
|
+
const consumed = options.keymap.onCommand(command, context);
|
|
737
|
+
return consumed ? rerender() : currentOutput;
|
|
738
|
+
}
|
|
739
|
+
return currentOutput;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
291
742
|
const session = {
|
|
743
|
+
size() {
|
|
744
|
+
return { ...terminalSize };
|
|
745
|
+
},
|
|
746
|
+
resize(cols, rows) {
|
|
747
|
+
const nextSize = {
|
|
748
|
+
cols: validateTerminalDimension("cols", cols),
|
|
749
|
+
rows: validateTerminalDimension("rows", rows)
|
|
750
|
+
};
|
|
751
|
+
pendingOutputResize = null;
|
|
752
|
+
outputResizeScheduler.cancel();
|
|
753
|
+
if (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
terminalSize = nextSize;
|
|
757
|
+
rerender();
|
|
758
|
+
},
|
|
292
759
|
update() {
|
|
293
760
|
return rerender();
|
|
294
761
|
},
|
|
@@ -296,7 +763,7 @@ export function mountTerminal(input, options = {}) {
|
|
|
296
763
|
return currentOutput;
|
|
297
764
|
},
|
|
298
765
|
ansiOutput() {
|
|
299
|
-
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans);
|
|
766
|
+
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
300
767
|
},
|
|
301
768
|
tree() {
|
|
302
769
|
return currentTree;
|
|
@@ -311,13 +778,20 @@ export function mountTerminal(input, options = {}) {
|
|
|
311
778
|
return true;
|
|
312
779
|
},
|
|
313
780
|
focusAt(x, y) {
|
|
314
|
-
const hitbox =
|
|
781
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
315
782
|
if (!hitbox) {
|
|
316
783
|
clearSemanticHover(undefined, x, y);
|
|
317
784
|
return false;
|
|
318
785
|
}
|
|
319
786
|
setSemanticHoverFromHitbox(hitbox.id, x, y);
|
|
320
|
-
|
|
787
|
+
const node = findFocusableById(currentTree, hitbox.id);
|
|
788
|
+
if (!node) {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
focusedId = node.props.id || focusedId;
|
|
792
|
+
skipFocusContainmentOnce = true;
|
|
793
|
+
rerender();
|
|
794
|
+
return true;
|
|
321
795
|
},
|
|
322
796
|
focusNext() {
|
|
323
797
|
const focusables = orderedFocusables();
|
|
@@ -341,143 +815,16 @@ export function mountTerminal(input, options = {}) {
|
|
|
341
815
|
return true;
|
|
342
816
|
},
|
|
343
817
|
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
818
|
const node = findFocused(currentTree, focusedId);
|
|
353
|
-
|
|
354
|
-
|
|
819
|
+
const context = { key, focusedId: node?.props.id, focusedTag: node?.tag };
|
|
820
|
+
let command = resolveTerminalKeyBinding(keyBindings, context);
|
|
821
|
+
if (!command && node?.tag === "terminal-editor" && key.length === 1) {
|
|
822
|
+
command = { id: "editor.insertText", text: key };
|
|
355
823
|
}
|
|
356
|
-
if (node
|
|
357
|
-
|
|
358
|
-
return rerender();
|
|
824
|
+
if (!command && node?.tag === "terminal-editor" && key === "CTRL_V") {
|
|
825
|
+
command = { id: "editor.paste" };
|
|
359
826
|
}
|
|
360
|
-
|
|
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
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (node.tag === "terminal-scroll") {
|
|
392
|
-
const id = node.props.id;
|
|
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
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (node.tag === "terminal-input") {
|
|
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;
|
|
827
|
+
return command ? runCommand(command, node, context) : currentOutput;
|
|
481
828
|
},
|
|
482
829
|
click(id) {
|
|
483
830
|
const node = id ? findFocusableById(currentTree, id) : findFocused(currentTree, focusedId);
|
|
@@ -487,11 +834,11 @@ export function mountTerminal(input, options = {}) {
|
|
|
487
834
|
if (id) {
|
|
488
835
|
focusedId = node.props.id || focusedId;
|
|
489
836
|
}
|
|
490
|
-
|
|
837
|
+
dispatchNodeEvent(node, "press", { type: "press", id: String(node.props.id || "") });
|
|
491
838
|
return rerender();
|
|
492
839
|
},
|
|
493
840
|
clickAt(x, y) {
|
|
494
|
-
const hitbox =
|
|
841
|
+
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
495
842
|
if (!hitbox) {
|
|
496
843
|
clearSemanticHover(undefined, x, y);
|
|
497
844
|
return currentOutput;
|
|
@@ -523,29 +870,172 @@ export function mountTerminal(input, options = {}) {
|
|
|
523
870
|
return rerender();
|
|
524
871
|
},
|
|
525
872
|
destroy() {
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
873
|
+
if (destroyed) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
destroyed = true;
|
|
877
|
+
cancelPendingEscapeFlush();
|
|
878
|
+
cleanupOutputResize?.();
|
|
879
|
+
cleanupOutputResize = null;
|
|
880
|
+
outputResizeScheduler.cancel();
|
|
881
|
+
renderScheduler.cancel();
|
|
882
|
+
terminalRuntime.destroy();
|
|
883
|
+
emitLifecycleRestore();
|
|
884
|
+
outputWriter.destroy();
|
|
885
|
+
if (runtimeOptions.stdin) {
|
|
886
|
+
if (typeof runtimeOptions.stdin.off === "function") {
|
|
887
|
+
runtimeOptions.stdin.off("data", onData);
|
|
529
888
|
}
|
|
530
|
-
else if (typeof
|
|
531
|
-
|
|
889
|
+
else if (typeof runtimeOptions.stdin.removeListener === "function") {
|
|
890
|
+
runtimeOptions.stdin.removeListener("data", onData);
|
|
532
891
|
}
|
|
533
|
-
|
|
534
|
-
|
|
892
|
+
runtimeOptions.stdin.setRawMode?.(false);
|
|
893
|
+
runtimeOptions.stdin.pause?.();
|
|
535
894
|
}
|
|
536
895
|
}
|
|
537
896
|
};
|
|
538
|
-
|
|
539
|
-
const
|
|
897
|
+
function dispatchPasteText(text) {
|
|
898
|
+
const node = findFocused(currentTree, focusedId);
|
|
899
|
+
if (node?.tag === "terminal-editor") {
|
|
900
|
+
runCommand({ id: "editor.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
|
|
901
|
+
}
|
|
902
|
+
else if (node?.tag === "terminal-input") {
|
|
903
|
+
runCommand({ id: "input.pasteText", text }, node, { key: text, focusedId: node.props.id, focusedTag: node.tag });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function cancelPendingEscapeFlush() {
|
|
907
|
+
if (pendingEscapeFlush) {
|
|
908
|
+
clearTimeout(pendingEscapeFlush);
|
|
909
|
+
pendingEscapeFlush = null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function flushPendingEscape() {
|
|
913
|
+
cancelPendingEscapeFlush();
|
|
914
|
+
if (destroyed || !pendingKeyChunk.startsWith(ESCAPE)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const buffered = pendingKeyChunk;
|
|
918
|
+
pendingKeyChunk = "";
|
|
919
|
+
session.dispatchKey(parseTerminalKey(ESCAPE));
|
|
920
|
+
processInputStream(buffered.slice(ESCAPE.length));
|
|
921
|
+
}
|
|
922
|
+
function schedulePendingEscapeFlush() {
|
|
923
|
+
cancelPendingEscapeFlush();
|
|
924
|
+
pendingEscapeFlush = setTimeout(flushPendingEscape, 0);
|
|
925
|
+
}
|
|
926
|
+
function processKeyStream(value) {
|
|
927
|
+
if (!value) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
|
|
931
|
+
if (value.startsWith(sequence)) {
|
|
932
|
+
session.dispatchKey(parseTerminalKey(sequence));
|
|
933
|
+
processInputStream(value.slice(sequence.length));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (value === ESCAPE) {
|
|
938
|
+
pendingKeyChunk = value;
|
|
939
|
+
schedulePendingEscapeFlush();
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (isDisambiguatedBracketedPasteStartPrefix(value)) {
|
|
943
|
+
pendingPasteChunk = value;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (isKnownTerminalKeySequencePrefix(value)) {
|
|
947
|
+
pendingKeyChunk = value;
|
|
948
|
+
schedulePendingEscapeFlush();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const paste = parseBracketedPaste(value);
|
|
952
|
+
if (paste) {
|
|
953
|
+
dispatchPasteText(paste.text);
|
|
954
|
+
processInputStream(paste.rest);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const escapeIndex = value.indexOf("\u001b");
|
|
958
|
+
if (escapeIndex > 0) {
|
|
959
|
+
for (const char of value.slice(0, escapeIndex)) {
|
|
960
|
+
session.dispatchKey(parseTerminalKey(char));
|
|
961
|
+
}
|
|
962
|
+
processInputStream(value.slice(escapeIndex));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const parsedKey = parseTerminalKey(value);
|
|
966
|
+
if (parsedKey !== value) {
|
|
967
|
+
session.dispatchKey(parsedKey);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (value.startsWith("\u001b")) {
|
|
971
|
+
session.dispatchKey(value);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
for (const char of value) {
|
|
975
|
+
session.dispatchKey(parseTerminalKey(char));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function processInputStream(value) {
|
|
979
|
+
if (!value) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (pendingPasteChunk) {
|
|
983
|
+
pendingPasteChunk += value;
|
|
984
|
+
const paste = parseBracketedPaste(pendingPasteChunk);
|
|
985
|
+
if (!paste && isBracketedPasteStartPrefix(pendingPasteChunk)) {
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (!paste) {
|
|
989
|
+
const buffered = pendingPasteChunk;
|
|
990
|
+
pendingPasteChunk = "";
|
|
991
|
+
processKeyStream(buffered);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
pendingPasteChunk = "";
|
|
995
|
+
dispatchPasteText(paste.text);
|
|
996
|
+
processInputStream(paste.rest);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (pendingKeyChunk) {
|
|
1000
|
+
if (pendingKeyChunk === ESCAPE && !canContinueEscapeSequence(value)) {
|
|
1001
|
+
flushPendingEscape();
|
|
1002
|
+
processInputStream(value);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const buffered = pendingKeyChunk + value;
|
|
1006
|
+
cancelPendingEscapeFlush();
|
|
1007
|
+
pendingKeyChunk = "";
|
|
1008
|
+
processKeyStream(buffered);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (value === ESCAPE) {
|
|
1012
|
+
processKeyStream(value);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (isBracketedPasteStartPrefix(value)) {
|
|
1016
|
+
pendingPasteChunk = value;
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (value.startsWith(BRACKETED_PASTE_START)) {
|
|
1020
|
+
const paste = parseBracketedPaste(value);
|
|
1021
|
+
if (!paste) {
|
|
1022
|
+
pendingPasteChunk = value;
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
dispatchPasteText(paste.text);
|
|
1026
|
+
processInputStream(paste.rest);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const parsed = parseTerminalInput(value);
|
|
540
1030
|
if (parsed.type === "mouse") {
|
|
541
1031
|
if (parsed.action === "press") {
|
|
542
|
-
const hitbox =
|
|
1032
|
+
const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
543
1033
|
if (hitbox?.tag === "terminal-input") {
|
|
544
1034
|
mouseSelectionId = hitbox.id;
|
|
545
1035
|
}
|
|
546
1036
|
const node = hitbox ? findFocusableById(currentTree, hitbox.id) : null;
|
|
547
1037
|
if (hitbox && node && shouldPointerCapture(node)) {
|
|
548
|
-
setPointerCapture(hitbox.id, "press",
|
|
1038
|
+
setPointerCapture(hitbox.id, "press", sourceRowFromHitbox(node, hitbox, parsed.y), parsed.x, parsed.y);
|
|
549
1039
|
}
|
|
550
1040
|
session.clickAt(parsed.x, parsed.y);
|
|
551
1041
|
}
|
|
@@ -560,13 +1050,13 @@ export function mountTerminal(input, options = {}) {
|
|
|
560
1050
|
else if (parsed.action === "release") {
|
|
561
1051
|
mouseSelectionId = null;
|
|
562
1052
|
const capturedId = pointerCaptureId;
|
|
563
|
-
const releaseHitbox =
|
|
1053
|
+
const releaseHitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
564
1054
|
const releaseNode = releaseHitbox ? findFocusableById(currentTree, releaseHitbox.id) : null;
|
|
565
1055
|
const releaseRow = releaseHitbox && releaseNode
|
|
566
|
-
?
|
|
1056
|
+
? sourceRowFromHitbox(releaseNode, releaseHitbox, parsed.y)
|
|
567
1057
|
: null;
|
|
568
1058
|
setPointerCapture(null, "release", capturedId && releaseHitbox?.id === capturedId ? releaseRow : null, parsed.x, parsed.y);
|
|
569
|
-
const hitbox =
|
|
1059
|
+
const hitbox = resolvePointerTarget(currentHitboxes, parsed.x, parsed.y);
|
|
570
1060
|
if (capturedId && hitbox && hitbox.id === capturedId && (hitbox.tag === "terminal-list" || hitbox.tag === "terminal-scroll")) {
|
|
571
1061
|
setSemanticHoverFromHitbox(hitbox.id, parsed.x, parsed.y);
|
|
572
1062
|
rerender();
|
|
@@ -577,23 +1067,63 @@ export function mountTerminal(input, options = {}) {
|
|
|
577
1067
|
}
|
|
578
1068
|
}
|
|
579
1069
|
else if (parsed.action === "wheel-up") {
|
|
580
|
-
|
|
581
|
-
session.dispatchKey("UP");
|
|
1070
|
+
wheelAt(parsed.x, parsed.y, -1);
|
|
582
1071
|
}
|
|
583
1072
|
else if (parsed.action === "wheel-down") {
|
|
584
|
-
|
|
585
|
-
session.dispatchKey("DOWN");
|
|
1073
|
+
wheelAt(parsed.x, parsed.y, 1);
|
|
586
1074
|
}
|
|
587
1075
|
return;
|
|
588
1076
|
}
|
|
589
|
-
|
|
1077
|
+
processKeyStream(value);
|
|
1078
|
+
}
|
|
1079
|
+
const onData = (chunk) => {
|
|
1080
|
+
const value = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
1081
|
+
processInputStream(value);
|
|
590
1082
|
};
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1083
|
+
if (runtimeOptions.stdin) {
|
|
1084
|
+
runtimeOptions.stdin.on("data", onData);
|
|
1085
|
+
runtimeOptions.stdin.setRawMode?.(true);
|
|
1086
|
+
runtimeOptions.stdin.resume?.();
|
|
1087
|
+
}
|
|
1088
|
+
function subscribeOutputResize() {
|
|
1089
|
+
const stdout = runtimeOptions.stdout;
|
|
1090
|
+
if (!stdout || typeof stdout.on !== "function") {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const removeResizeListener = typeof stdout.off === "function" ? stdout.off.bind(stdout) : stdout.removeListener?.bind(stdout);
|
|
1094
|
+
if (!removeResizeListener) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const onResize = () => {
|
|
1098
|
+
const cols = stdout.columns;
|
|
1099
|
+
const rows = stdout.rows;
|
|
1100
|
+
if (!isValidTerminalDimension(cols) || !isValidTerminalDimension(rows)) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
pendingOutputResize = { cols, rows };
|
|
1104
|
+
outputResizeScheduler.requestRender();
|
|
1105
|
+
};
|
|
1106
|
+
try {
|
|
1107
|
+
stdout.on("resize", onResize);
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
cleanupOutputResize = () => removeResizeListener("resize", onResize);
|
|
1113
|
+
}
|
|
1114
|
+
function applyPendingOutputResize() {
|
|
1115
|
+
const nextSize = pendingOutputResize;
|
|
1116
|
+
pendingOutputResize = null;
|
|
1117
|
+
if (!nextSize || destroyed || (nextSize.cols === terminalSize.cols && nextSize.rows === terminalSize.rows)) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
terminalSize = nextSize;
|
|
1121
|
+
rerender();
|
|
595
1122
|
}
|
|
1123
|
+
subscribeOutputResize();
|
|
1124
|
+
emitLifecycleSetup();
|
|
596
1125
|
emitOutput();
|
|
1126
|
+
autoProjectionEnabled = true;
|
|
597
1127
|
return session;
|
|
598
1128
|
}
|
|
599
1129
|
//# sourceMappingURL=session.js.map
|