@valyrianjs/terminal 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +197 -50
- package/dist/ansi.js.map +1 -1
- package/dist/layout.d.ts +1 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +11 -1
- package/dist/layout.js.map +1 -1
- package/dist/render-internal.d.ts +10 -0
- package/dist/render-internal.d.ts.map +1 -0
- package/dist/render-internal.js +1401 -0
- package/dist/render-internal.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +16 -1244
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +112 -14
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +12 -1
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +22 -3
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +5 -4
- package/docs/cookbook.md +5 -3
- package/docs/core-concepts.md +1 -1
- package/docs/interaction-model.md +1 -1
- package/docs/primitive-gallery.md +1 -1
- package/docs/session-runtime.md +2 -2
- package/examples/docs/overlay-button.tsx +128 -0
- package/llms-full.txt +15 -12
- package/package.json +1 -1
- package/src/ansi.ts +239 -50
- package/src/layout.ts +13 -2
- package/src/render-internal.ts +1565 -0
- package/src/render.ts +18 -1368
- package/src/session.ts +136 -22
- package/src/text.ts +13 -1
- package/src/theme.ts +26 -3
- package/src/types.ts +1 -2
package/src/session.ts
CHANGED
|
@@ -7,18 +7,19 @@ import { mergeVertical } from "./layout.js";
|
|
|
7
7
|
import { cursorFromHitbox, parseTerminalInput, parseTerminalMousePrefix, resolvePointerTarget } from "./mouse.js";
|
|
8
8
|
import { createOutputWriter } from "./output-writer.js";
|
|
9
9
|
import { parseBracketedPaste } from "./paste.js";
|
|
10
|
-
import { renderTerminalFrame } from "./render.js";
|
|
10
|
+
import { renderTerminalFrame } from "./render-internal.js";
|
|
11
11
|
import { createRenderScheduler } from "./scheduler.js";
|
|
12
12
|
import { plainText, stripTerminalControls } from "./text.js";
|
|
13
13
|
import { collectActiveFocusScopeFocusableNodes, collectDirectOverlayFocusableNodes, collectFocusableNodes, findFocusableById, findFocused } from "./tree.js";
|
|
14
14
|
import { createValyrianTerminalRuntime } from "./runtime.js";
|
|
15
15
|
|
|
16
|
-
import type { TerminalRenderContext } from "./render.js";
|
|
16
|
+
import type { TerminalRenderContext } from "./render-internal.js";
|
|
17
17
|
|
|
18
18
|
import type { EditorState } from "./editor-state.js";
|
|
19
19
|
|
|
20
20
|
import type {
|
|
21
21
|
InputInteractionState,
|
|
22
|
+
TerminalButtonPressEventPayload,
|
|
22
23
|
TerminalCommand,
|
|
23
24
|
TerminalCommandContext,
|
|
24
25
|
TerminalCaptureEventPayload,
|
|
@@ -40,13 +41,18 @@ import type {
|
|
|
40
41
|
|
|
41
42
|
type TerminalInputStream = NonNullable<TerminalMountOptions["stdin"]>;
|
|
42
43
|
|
|
44
|
+
type InternalTerminalHitbox = TerminalHitbox & {
|
|
45
|
+
__listItemIndex?: number;
|
|
46
|
+
__pressHandler?: (event: TerminalButtonPressEventPayload) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
43
49
|
interface ResolvedRuntimeOptions {
|
|
44
50
|
runtime: "app" | "headless";
|
|
45
51
|
stdin?: TerminalInputStream;
|
|
46
52
|
stdout?: TerminalOutputStream;
|
|
47
53
|
alternateScreen: boolean;
|
|
48
54
|
hideCursor: boolean;
|
|
49
|
-
|
|
55
|
+
mouseInput: boolean;
|
|
50
56
|
writesAnsi: boolean;
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -82,6 +88,7 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
|
|
|
82
88
|
const ESCAPE = "\u001b";
|
|
83
89
|
const CSI_PREFIX = "\u001b[";
|
|
84
90
|
const DOUBLE_PRESS_INTERVAL_MS = 500;
|
|
91
|
+
const MODAL_OVERLAY_HITBOX_ID = "\u0000valyrian-overlay-modal-shield";
|
|
85
92
|
|
|
86
93
|
function isBracketedPasteStartPrefix(value: string) {
|
|
87
94
|
return value.length > 0 && value.length < BRACKETED_PASTE_START.length && BRACKETED_PASTE_START.startsWith(value);
|
|
@@ -95,6 +102,38 @@ function isKnownTerminalKeySequencePrefix(value: string) {
|
|
|
95
102
|
return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
function isSgrMouseSequencePrefix(value: string) {
|
|
106
|
+
return /^\u001b\[<(?:\d+)?(?:;(?:\d+)?)?(?:;(?:\d+)?)?$/.test(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isDigit(value: string) {
|
|
110
|
+
return value >= "0" && value <= "9";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function restAfterInvalidSgrMouseSequence(value: string) {
|
|
114
|
+
if (!value.startsWith("\u001b[<")) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let separators = 0;
|
|
119
|
+
for (let index = "\u001b[<".length; index < value.length; index += 1) {
|
|
120
|
+
const char = value[index];
|
|
121
|
+
if (isDigit(char)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (char === ";" && separators < 2) {
|
|
125
|
+
separators += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === "M" || char === "m") {
|
|
129
|
+
return value.slice(index + 1);
|
|
130
|
+
}
|
|
131
|
+
return value.slice(index);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
98
137
|
function canContinueEscapeSequence(value: string) {
|
|
99
138
|
return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
|
|
100
139
|
}
|
|
@@ -110,6 +149,20 @@ function isValidTerminalDimension(value: number | undefined): value is number {
|
|
|
110
149
|
return Number.isInteger(value) && Number(value) >= 1;
|
|
111
150
|
}
|
|
112
151
|
|
|
152
|
+
function isModalOverlayShieldHitbox(hitbox: TerminalHitbox | null | undefined) {
|
|
153
|
+
return hitbox?.id === MODAL_OVERLAY_HITBOX_ID;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hitboxIsAboveModalShield(hitboxes: TerminalHitbox[], id: string) {
|
|
157
|
+
const shieldIndex = hitboxes.findIndex((box) => isModalOverlayShieldHitbox(box));
|
|
158
|
+
if (shieldIndex < 0) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const hitboxIndex = hitboxes.findIndex((box) => box.id === id);
|
|
163
|
+
return hitboxIndex >= 0 && hitboxIndex < shieldIndex;
|
|
164
|
+
}
|
|
165
|
+
|
|
113
166
|
|
|
114
167
|
function getProcessStdin(): TerminalInputStream | undefined {
|
|
115
168
|
const candidate = globalThis.process?.stdin as TerminalInputStream | undefined;
|
|
@@ -140,7 +193,7 @@ function resolveRuntimeOptions(options: TerminalMountOptions): ResolvedRuntimeOp
|
|
|
140
193
|
stdout,
|
|
141
194
|
alternateScreen: options.alternateScreen ?? ownsInteractiveTTY,
|
|
142
195
|
hideCursor: options.hideCursor ?? ownsInteractiveTTY,
|
|
143
|
-
|
|
196
|
+
mouseInput: ownsInteractiveTTY,
|
|
144
197
|
writesAnsi: runtime === "app" && Boolean(stdout)
|
|
145
198
|
};
|
|
146
199
|
}
|
|
@@ -254,7 +307,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
254
307
|
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
255
308
|
let currentFrame = renderTreeFrame(currentTree);
|
|
256
309
|
let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
257
|
-
let currentHitboxes = currentFrame.hitboxes;
|
|
310
|
+
let currentHitboxes = currentFrame.hitboxes as InternalTerminalHitbox[];
|
|
258
311
|
|
|
259
312
|
const outputWriter = createOutputWriter(runtimeOptions.stdout);
|
|
260
313
|
const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
@@ -272,6 +325,28 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
272
325
|
return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
|
|
273
326
|
}
|
|
274
327
|
|
|
328
|
+
function publicTerminalNodes(nodes: TerminalNode[]): TerminalNode[] {
|
|
329
|
+
return nodes.map((node) => {
|
|
330
|
+
if (node.type === "text") {
|
|
331
|
+
return { ...node };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const publicProps: Record<string, any> = {};
|
|
335
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
336
|
+
if (!key.startsWith("__")) {
|
|
337
|
+
publicProps[key] = value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
type: node.type,
|
|
343
|
+
tag: node.tag,
|
|
344
|
+
props: publicProps,
|
|
345
|
+
children: publicTerminalNodes(node.children)
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
275
350
|
function emitLifecycleSetup() {
|
|
276
351
|
const writes: string[] = [];
|
|
277
352
|
if (runtimeOptions.alternateScreen) {
|
|
@@ -280,7 +355,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
280
355
|
if (runtimeOptions.hideCursor) {
|
|
281
356
|
writes.push(ANSI_HIDE_CURSOR);
|
|
282
357
|
}
|
|
283
|
-
if (runtimeOptions.
|
|
358
|
+
if (runtimeOptions.mouseInput) {
|
|
284
359
|
writes.push(ANSI_ENABLE_MOUSE_REPORTING);
|
|
285
360
|
}
|
|
286
361
|
if (writes.length > 0) {
|
|
@@ -293,7 +368,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
293
368
|
return;
|
|
294
369
|
}
|
|
295
370
|
const writes: string[] = [];
|
|
296
|
-
if (runtimeOptions.
|
|
371
|
+
if (runtimeOptions.mouseInput) {
|
|
297
372
|
writes.push(ANSI_DISABLE_MOUSE_REPORTING);
|
|
298
373
|
}
|
|
299
374
|
if (runtimeOptions.hideCursor) {
|
|
@@ -308,9 +383,10 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
308
383
|
}
|
|
309
384
|
|
|
310
385
|
function emitOutput() {
|
|
311
|
-
if (
|
|
312
|
-
|
|
386
|
+
if (destroyed) {
|
|
387
|
+
return;
|
|
313
388
|
}
|
|
389
|
+
outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
|
|
314
390
|
}
|
|
315
391
|
|
|
316
392
|
function renderNow() {
|
|
@@ -329,7 +405,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
329
405
|
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
330
406
|
currentFrame = renderTreeFrame(currentTree);
|
|
331
407
|
currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
332
|
-
currentHitboxes = currentFrame.hitboxes;
|
|
408
|
+
currentHitboxes = currentFrame.hitboxes as InternalTerminalHitbox[];
|
|
333
409
|
emitOutput();
|
|
334
410
|
return currentOutput;
|
|
335
411
|
}
|
|
@@ -480,7 +556,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
480
556
|
return 1;
|
|
481
557
|
}
|
|
482
558
|
|
|
483
|
-
function sourceRowFromHitbox(node: TerminalFocusNode, hitbox:
|
|
559
|
+
function sourceRowFromHitbox(node: TerminalFocusNode, hitbox: InternalTerminalHitbox, y: number) {
|
|
484
560
|
if (node.tag === "terminal-list" && typeof hitbox.__listItemIndex === "number" && y >= hitbox.y1 && y <= hitbox.y2) {
|
|
485
561
|
return Math.max(1, Math.min(rowCountForNode(node), hitbox.__listItemIndex + 1));
|
|
486
562
|
}
|
|
@@ -541,7 +617,6 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
541
617
|
}
|
|
542
618
|
}
|
|
543
619
|
|
|
544
|
-
|
|
545
620
|
function listItemKey(node: TerminalFocusNode, index: number) {
|
|
546
621
|
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
547
622
|
const item = items[index];
|
|
@@ -670,7 +745,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
670
745
|
return dispatchNodeEvent(node, type, { type, id });
|
|
671
746
|
}
|
|
672
747
|
|
|
673
|
-
function dispatchHitboxButtonPressEvent(hitbox:
|
|
748
|
+
function dispatchHitboxButtonPressEvent(hitbox: InternalTerminalHitbox, type: "press" | "doublepress" | "contextpress") {
|
|
674
749
|
if (type !== "press" || typeof hitbox.__pressHandler !== "function") {
|
|
675
750
|
return false;
|
|
676
751
|
}
|
|
@@ -804,11 +879,19 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
804
879
|
|
|
805
880
|
function hoverAt(x: number, y: number) {
|
|
806
881
|
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
807
|
-
if (!hitbox) {
|
|
808
|
-
if (pointerCaptureId) {
|
|
882
|
+
if (!hitbox || isModalOverlayShieldHitbox(hitbox)) {
|
|
883
|
+
if (pointerCaptureId && (!hitbox || hitboxIsAboveModalShield(currentHitboxes, pointerCaptureId))) {
|
|
809
884
|
setSemanticHoverFromHitbox(pointerCaptureId, x, y);
|
|
810
885
|
return rerender();
|
|
811
886
|
}
|
|
887
|
+
if (pointerCaptureId) {
|
|
888
|
+
const capturedHitbox = currentHitboxes.find((box) => box.id === pointerCaptureId);
|
|
889
|
+
const capturedNode = findFocusableById(currentTree, pointerCaptureId);
|
|
890
|
+
const capturedRow = capturedHitbox && capturedNode
|
|
891
|
+
? sourceRowFromHitbox(capturedNode, capturedHitbox, y)
|
|
892
|
+
: hoveredRowForNode(capturedNode);
|
|
893
|
+
setPointerCapture(null, "drag", capturedRow, x, y);
|
|
894
|
+
}
|
|
812
895
|
clearSemanticHover(undefined, x, y);
|
|
813
896
|
return rerender();
|
|
814
897
|
}
|
|
@@ -910,7 +993,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
910
993
|
|
|
911
994
|
function wheelAt(x: number, y: number, direction: -1 | 1) {
|
|
912
995
|
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
913
|
-
if (!hitbox) {
|
|
996
|
+
if (!hitbox || isModalOverlayShieldHitbox(hitbox)) {
|
|
914
997
|
return currentOutput;
|
|
915
998
|
}
|
|
916
999
|
|
|
@@ -1165,7 +1248,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1165
1248
|
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
1166
1249
|
},
|
|
1167
1250
|
tree() {
|
|
1168
|
-
return currentTree;
|
|
1251
|
+
return publicTerminalNodes(currentTree);
|
|
1169
1252
|
},
|
|
1170
1253
|
focus(id: string) {
|
|
1171
1254
|
const node = findFocusableById(currentTree, id);
|
|
@@ -1178,7 +1261,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1178
1261
|
},
|
|
1179
1262
|
focusAt(x: number, y: number) {
|
|
1180
1263
|
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
1181
|
-
if (!hitbox) {
|
|
1264
|
+
if (!hitbox || isModalOverlayShieldHitbox(hitbox)) {
|
|
1182
1265
|
clearSemanticHover(undefined, x, y);
|
|
1183
1266
|
return false;
|
|
1184
1267
|
}
|
|
@@ -1238,7 +1321,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1238
1321
|
},
|
|
1239
1322
|
clickAt(x: number, y: number) {
|
|
1240
1323
|
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
1241
|
-
if (!hitbox) {
|
|
1324
|
+
if (!hitbox || isModalOverlayShieldHitbox(hitbox)) {
|
|
1242
1325
|
clearSemanticHover(undefined, x, y);
|
|
1243
1326
|
return currentOutput;
|
|
1244
1327
|
}
|
|
@@ -1343,6 +1426,18 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1343
1426
|
return;
|
|
1344
1427
|
}
|
|
1345
1428
|
|
|
1429
|
+
const parsedMouse = parseTerminalMousePrefix(value);
|
|
1430
|
+
if (parsedMouse) {
|
|
1431
|
+
processParsedMouseInput(parsedMouse.input);
|
|
1432
|
+
processInputStream(parsedMouse.rest);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (isSgrMouseSequencePrefix(value)) {
|
|
1437
|
+
pendingKeyChunk = value;
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1346
1441
|
for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
|
|
1347
1442
|
if (value.startsWith(sequence)) {
|
|
1348
1443
|
session.dispatchKey(parseTerminalKey(sequence));
|
|
@@ -1400,7 +1495,6 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1400
1495
|
}
|
|
1401
1496
|
}
|
|
1402
1497
|
|
|
1403
|
-
|
|
1404
1498
|
function isPrimaryMouseButton(button: number) {
|
|
1405
1499
|
return button < 64 && (button & 3) === 0;
|
|
1406
1500
|
}
|
|
@@ -1442,7 +1536,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1442
1536
|
|
|
1443
1537
|
function contextPressAt(x: number, y: number) {
|
|
1444
1538
|
const hitbox = resolvePointerTarget(currentHitboxes, x, y);
|
|
1445
|
-
if (!hitbox) {
|
|
1539
|
+
if (!hitbox || isModalOverlayShieldHitbox(hitbox)) {
|
|
1446
1540
|
clearSemanticHover(undefined, x, y);
|
|
1447
1541
|
return currentOutput;
|
|
1448
1542
|
}
|
|
@@ -1514,7 +1608,12 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1514
1608
|
}
|
|
1515
1609
|
} else if (parsed.action === "drag") {
|
|
1516
1610
|
if (mouseSelectionId) {
|
|
1517
|
-
|
|
1611
|
+
if (!hitboxIsAboveModalShield(currentHitboxes, mouseSelectionId)) {
|
|
1612
|
+
mouseSelectionId = null;
|
|
1613
|
+
hoverAt(parsed.x, parsed.y);
|
|
1614
|
+
} else {
|
|
1615
|
+
setCursorFromHitbox(mouseSelectionId, parsed.x, true);
|
|
1616
|
+
}
|
|
1518
1617
|
} else {
|
|
1519
1618
|
hoverAt(parsed.x, parsed.y);
|
|
1520
1619
|
}
|
|
@@ -1574,6 +1673,21 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1574
1673
|
const buffered = pendingKeyChunk + value;
|
|
1575
1674
|
cancelPendingEscapeFlush();
|
|
1576
1675
|
pendingKeyChunk = "";
|
|
1676
|
+
if (isSgrMouseSequencePrefix(buffered)) {
|
|
1677
|
+
pendingKeyChunk = buffered;
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
const parsedBufferedMouse = parseTerminalMousePrefix(buffered);
|
|
1681
|
+
if (parsedBufferedMouse) {
|
|
1682
|
+
processParsedMouseInput(parsedBufferedMouse.input);
|
|
1683
|
+
processInputStream(parsedBufferedMouse.rest);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const restAfterInvalidSgrMouse = restAfterInvalidSgrMouseSequence(buffered);
|
|
1687
|
+
if (typeof restAfterInvalidSgrMouse === "string") {
|
|
1688
|
+
processInputStream(restAfterInvalidSgrMouse);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1577
1691
|
processKeyStream(buffered);
|
|
1578
1692
|
return;
|
|
1579
1693
|
}
|
package/src/text.ts
CHANGED
|
@@ -5,6 +5,8 @@ const C1_CSI_TERMINAL_CONTROL = /\u009b[0-?]*[ -/]*[@-~]/g;
|
|
|
5
5
|
const ESC_TERMINAL_CONTROL = /\u001b[ -/]*[0-~]/g;
|
|
6
6
|
const C1_TERMINAL_CONTROL = /[\u0080-\u009f]/g;
|
|
7
7
|
const C0_TERMINAL_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f]/g;
|
|
8
|
+
const CELL_WIDTH_CACHE_LIMIT = 4096;
|
|
9
|
+
const cellWidthCache = new Map<string, number>();
|
|
8
10
|
const COMBINING_MARK = /\p{Mark}/u;
|
|
9
11
|
const EMOJI_PRESENTATION = /\p{Extended_Pictographic}/u;
|
|
10
12
|
|
|
@@ -92,7 +94,17 @@ function graphemeCellWidth(grapheme: string) {
|
|
|
92
94
|
|
|
93
95
|
export function terminalCellWidth(value: unknown) {
|
|
94
96
|
const text = stripTerminalControls(value);
|
|
95
|
-
|
|
97
|
+
const cached = cellWidthCache.get(text);
|
|
98
|
+
if (typeof cached === "number") {
|
|
99
|
+
return cached;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const width = terminalGraphemes(text).reduce((total, grapheme) => total + graphemeCellWidth(grapheme), 0);
|
|
103
|
+
if (cellWidthCache.size >= CELL_WIDTH_CACHE_LIMIT) {
|
|
104
|
+
cellWidthCache.clear();
|
|
105
|
+
}
|
|
106
|
+
cellWidthCache.set(text, width);
|
|
107
|
+
return width;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
export function sliceTerminalCells(value: string, maxCells: number) {
|
package/src/theme.ts
CHANGED
|
@@ -80,7 +80,7 @@ export const defaultTerminalTheme: TerminalTheme = {
|
|
|
80
80
|
list: { base: { color: "#d8dee9" }, selected: { color: "#ffffff", background: "#2e3440" }, current: { color: "#ffffff", background: "#3b4252" }, hover: { color: "#ffffff", background: "#2b3137" }, empty: { color: "#777777" }, expanded: { color: "#b48ead" }, collapsed: { color: "#81a1c1" } },
|
|
81
81
|
scroll: { base: { color: "#d8dee9" }, hover: { color: "#dddddd" } },
|
|
82
82
|
log: { base: { color: "#d8dee9" }, empty: { color: "#777777" }, error: { color: "#bf616a" }, warning: { color: "#ebcb8b" }, success: { color: "#a3be8c" }, muted: { color: "#777777" } },
|
|
83
|
-
overlay: { base: { background: "#111111" }, dragging: { color: "#b48ead" }, dropTarget: { background: "#243b53" }, capturing: { color: "#d08770" } },
|
|
83
|
+
overlay: { base: { background: "#111111" }, backdrop: { background: "#000000" }, dragging: { color: "#b48ead" }, dropTarget: { background: "#243b53" }, capturing: { color: "#d08770" } },
|
|
84
84
|
pane: {
|
|
85
85
|
header: { background: "#3a3a3a", color: "#ffffff" },
|
|
86
86
|
transcript: { background: "#303030", color: "#dddddd" },
|
|
@@ -107,6 +107,9 @@ export const defaultTerminalTheme: TerminalTheme = {
|
|
|
107
107
|
focus: {
|
|
108
108
|
background: "#1f2328"
|
|
109
109
|
},
|
|
110
|
+
"editor.focus": {
|
|
111
|
+
background: "#161b22"
|
|
112
|
+
},
|
|
110
113
|
"current-row": reverseVideoToken,
|
|
111
114
|
hover: {
|
|
112
115
|
ansiOpen: "\u001b[4m",
|
|
@@ -183,6 +186,26 @@ export function mergeTerminalTheme(theme?: TerminalTheme): TerminalTheme {
|
|
|
183
186
|
};
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
const DEFAULT_MERGED_THEME = mergeTerminalTheme();
|
|
190
|
+
// Maintainer note: theme objects are treated as immutable for the CLI lifetime.
|
|
191
|
+
// Restart the CLI to apply theme changes instead of mutating a cached source theme.
|
|
192
|
+
const MERGED_THEME_BY_SOURCE = new WeakMap<TerminalTheme, TerminalTheme>();
|
|
193
|
+
|
|
194
|
+
function resolveMergedTerminalTheme(theme?: TerminalTheme): TerminalTheme {
|
|
195
|
+
if (typeof theme === "undefined") {
|
|
196
|
+
return DEFAULT_MERGED_THEME;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const cached = MERGED_THEME_BY_SOURCE.get(theme);
|
|
200
|
+
if (cached) {
|
|
201
|
+
return cached;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const merged = mergeTerminalTheme(theme);
|
|
205
|
+
MERGED_THEME_BY_SOURCE.set(theme, merged);
|
|
206
|
+
return merged;
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
187
210
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
188
211
|
}
|
|
@@ -239,7 +262,7 @@ export function resolveTerminalStyle(value: TerminalStyleValue | undefined, them
|
|
|
239
262
|
if (typeof value !== "string") {
|
|
240
263
|
return { ...value, border: isPlainObject(value.border) ? { ...value.border } : value.border, padding: isPlainObject(value.padding) ? { ...value.padding } : value.padding };
|
|
241
264
|
}
|
|
242
|
-
const merged =
|
|
265
|
+
const merged = resolveMergedTerminalTheme(theme);
|
|
243
266
|
const parts = value.split(".").filter(Boolean);
|
|
244
267
|
let node: unknown = merged.styles;
|
|
245
268
|
for (const part of parts) {
|
|
@@ -262,5 +285,5 @@ export function resolveTerminalStyleToken(
|
|
|
262
285
|
kind: TerminalSemanticStyleKind | (string & {}),
|
|
263
286
|
theme?: TerminalTheme
|
|
264
287
|
): TerminalStyleToken | undefined {
|
|
265
|
-
return
|
|
288
|
+
return resolveMergedTerminalTheme(theme).spans?.[kind];
|
|
266
289
|
}
|
package/src/types.ts
CHANGED
|
@@ -134,8 +134,6 @@ export interface TerminalHitbox {
|
|
|
134
134
|
itemIndexes?: number[];
|
|
135
135
|
contentY?: number;
|
|
136
136
|
pointerLayer?: number;
|
|
137
|
-
__listItemIndex?: number;
|
|
138
|
-
__pressHandler?: (event: TerminalButtonPressEventPayload) => void;
|
|
139
137
|
}
|
|
140
138
|
|
|
141
139
|
export interface CursorPosition {
|
|
@@ -507,6 +505,7 @@ export interface TerminalOverlayMarginAxes {
|
|
|
507
505
|
export interface TerminalOverlayProps extends TerminalFocusableProps, TerminalStyleProps {
|
|
508
506
|
margin: TerminalOverlayMarginValue | TerminalOverlayMarginAxes;
|
|
509
507
|
trapFocus?: boolean;
|
|
508
|
+
backdrop?: boolean | TerminalStyleValue;
|
|
510
509
|
}
|
|
511
510
|
|
|
512
511
|
export interface TerminalFocusScopeProps {
|