@valyrianjs/terminal 0.2.2 → 0.2.3
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 +166 -4
- package/dist/ansi.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 +1295 -0
- package/dist/render-internal.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +13 -1244
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +76 -4
- 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 +18 -2
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ansi.ts +196 -4
- package/src/render-internal.ts +1441 -0
- package/src/render.ts +14 -1368
- package/src/session.ts +97 -12
- package/src/text.ts +13 -1
- package/src/theme.ts +22 -2
- package/src/types.ts +0 -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,6 +41,11 @@ 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;
|
|
@@ -95,6 +101,38 @@ function isKnownTerminalKeySequencePrefix(value: string) {
|
|
|
95
101
|
return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
function isSgrMouseSequencePrefix(value: string) {
|
|
105
|
+
return /^\u001b\[<(?:\d+)?(?:;(?:\d+)?)?(?:;(?:\d+)?)?$/.test(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isDigit(value: string) {
|
|
109
|
+
return value >= "0" && value <= "9";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function restAfterInvalidSgrMouseSequence(value: string) {
|
|
113
|
+
if (!value.startsWith("\u001b[<")) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let separators = 0;
|
|
118
|
+
for (let index = "\u001b[<".length; index < value.length; index += 1) {
|
|
119
|
+
const char = value[index];
|
|
120
|
+
if (isDigit(char)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (char === ";" && separators < 2) {
|
|
124
|
+
separators += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (char === "M" || char === "m") {
|
|
128
|
+
return value.slice(index + 1);
|
|
129
|
+
}
|
|
130
|
+
return value.slice(index);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
function canContinueEscapeSequence(value: string) {
|
|
99
137
|
return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
|
|
100
138
|
}
|
|
@@ -110,7 +148,6 @@ function isValidTerminalDimension(value: number | undefined): value is number {
|
|
|
110
148
|
return Number.isInteger(value) && Number(value) >= 1;
|
|
111
149
|
}
|
|
112
150
|
|
|
113
|
-
|
|
114
151
|
function getProcessStdin(): TerminalInputStream | undefined {
|
|
115
152
|
const candidate = globalThis.process?.stdin as TerminalInputStream | undefined;
|
|
116
153
|
return candidate && typeof candidate.on === "function" ? candidate : undefined;
|
|
@@ -254,7 +291,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
254
291
|
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
255
292
|
let currentFrame = renderTreeFrame(currentTree);
|
|
256
293
|
let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
257
|
-
let currentHitboxes = currentFrame.hitboxes;
|
|
294
|
+
let currentHitboxes = currentFrame.hitboxes as InternalTerminalHitbox[];
|
|
258
295
|
|
|
259
296
|
const outputWriter = createOutputWriter(runtimeOptions.stdout);
|
|
260
297
|
const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
@@ -272,6 +309,28 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
272
309
|
return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
|
|
273
310
|
}
|
|
274
311
|
|
|
312
|
+
function publicTerminalNodes(nodes: TerminalNode[]): TerminalNode[] {
|
|
313
|
+
return nodes.map((node) => {
|
|
314
|
+
if (node.type === "text") {
|
|
315
|
+
return { ...node };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const publicProps: Record<string, any> = {};
|
|
319
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
320
|
+
if (!key.startsWith("__")) {
|
|
321
|
+
publicProps[key] = value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
type: node.type,
|
|
327
|
+
tag: node.tag,
|
|
328
|
+
props: publicProps,
|
|
329
|
+
children: publicTerminalNodes(node.children)
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
275
334
|
function emitLifecycleSetup() {
|
|
276
335
|
const writes: string[] = [];
|
|
277
336
|
if (runtimeOptions.alternateScreen) {
|
|
@@ -308,9 +367,10 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
308
367
|
}
|
|
309
368
|
|
|
310
369
|
function emitOutput() {
|
|
311
|
-
if (
|
|
312
|
-
|
|
370
|
+
if (destroyed) {
|
|
371
|
+
return;
|
|
313
372
|
}
|
|
373
|
+
outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
|
|
314
374
|
}
|
|
315
375
|
|
|
316
376
|
function renderNow() {
|
|
@@ -329,7 +389,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
329
389
|
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
330
390
|
currentFrame = renderTreeFrame(currentTree);
|
|
331
391
|
currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
332
|
-
currentHitboxes = currentFrame.hitboxes;
|
|
392
|
+
currentHitboxes = currentFrame.hitboxes as InternalTerminalHitbox[];
|
|
333
393
|
emitOutput();
|
|
334
394
|
return currentOutput;
|
|
335
395
|
}
|
|
@@ -480,7 +540,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
480
540
|
return 1;
|
|
481
541
|
}
|
|
482
542
|
|
|
483
|
-
function sourceRowFromHitbox(node: TerminalFocusNode, hitbox:
|
|
543
|
+
function sourceRowFromHitbox(node: TerminalFocusNode, hitbox: InternalTerminalHitbox, y: number) {
|
|
484
544
|
if (node.tag === "terminal-list" && typeof hitbox.__listItemIndex === "number" && y >= hitbox.y1 && y <= hitbox.y2) {
|
|
485
545
|
return Math.max(1, Math.min(rowCountForNode(node), hitbox.__listItemIndex + 1));
|
|
486
546
|
}
|
|
@@ -541,7 +601,6 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
541
601
|
}
|
|
542
602
|
}
|
|
543
603
|
|
|
544
|
-
|
|
545
604
|
function listItemKey(node: TerminalFocusNode, index: number) {
|
|
546
605
|
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
547
606
|
const item = items[index];
|
|
@@ -670,7 +729,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
670
729
|
return dispatchNodeEvent(node, type, { type, id });
|
|
671
730
|
}
|
|
672
731
|
|
|
673
|
-
function dispatchHitboxButtonPressEvent(hitbox:
|
|
732
|
+
function dispatchHitboxButtonPressEvent(hitbox: InternalTerminalHitbox, type: "press" | "doublepress" | "contextpress") {
|
|
674
733
|
if (type !== "press" || typeof hitbox.__pressHandler !== "function") {
|
|
675
734
|
return false;
|
|
676
735
|
}
|
|
@@ -1165,7 +1224,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1165
1224
|
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
1166
1225
|
},
|
|
1167
1226
|
tree() {
|
|
1168
|
-
return currentTree;
|
|
1227
|
+
return publicTerminalNodes(currentTree);
|
|
1169
1228
|
},
|
|
1170
1229
|
focus(id: string) {
|
|
1171
1230
|
const node = findFocusableById(currentTree, id);
|
|
@@ -1343,6 +1402,18 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1343
1402
|
return;
|
|
1344
1403
|
}
|
|
1345
1404
|
|
|
1405
|
+
const parsedMouse = parseTerminalMousePrefix(value);
|
|
1406
|
+
if (parsedMouse) {
|
|
1407
|
+
processParsedMouseInput(parsedMouse.input);
|
|
1408
|
+
processInputStream(parsedMouse.rest);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (isSgrMouseSequencePrefix(value)) {
|
|
1413
|
+
pendingKeyChunk = value;
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1346
1417
|
for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
|
|
1347
1418
|
if (value.startsWith(sequence)) {
|
|
1348
1419
|
session.dispatchKey(parseTerminalKey(sequence));
|
|
@@ -1400,7 +1471,6 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1400
1471
|
}
|
|
1401
1472
|
}
|
|
1402
1473
|
|
|
1403
|
-
|
|
1404
1474
|
function isPrimaryMouseButton(button: number) {
|
|
1405
1475
|
return button < 64 && (button & 3) === 0;
|
|
1406
1476
|
}
|
|
@@ -1574,6 +1644,21 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1574
1644
|
const buffered = pendingKeyChunk + value;
|
|
1575
1645
|
cancelPendingEscapeFlush();
|
|
1576
1646
|
pendingKeyChunk = "";
|
|
1647
|
+
if (isSgrMouseSequencePrefix(buffered)) {
|
|
1648
|
+
pendingKeyChunk = buffered;
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const parsedBufferedMouse = parseTerminalMousePrefix(buffered);
|
|
1652
|
+
if (parsedBufferedMouse) {
|
|
1653
|
+
processParsedMouseInput(parsedBufferedMouse.input);
|
|
1654
|
+
processInputStream(parsedBufferedMouse.rest);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const restAfterInvalidSgrMouse = restAfterInvalidSgrMouseSequence(buffered);
|
|
1658
|
+
if (typeof restAfterInvalidSgrMouse === "string") {
|
|
1659
|
+
processInputStream(restAfterInvalidSgrMouse);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1577
1662
|
processKeyStream(buffered);
|
|
1578
1663
|
return;
|
|
1579
1664
|
}
|
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
|
@@ -183,6 +183,26 @@ export function mergeTerminalTheme(theme?: TerminalTheme): TerminalTheme {
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
const DEFAULT_MERGED_THEME = mergeTerminalTheme();
|
|
187
|
+
// Maintainer note: theme objects are treated as immutable for the CLI lifetime.
|
|
188
|
+
// Restart the CLI to apply theme changes instead of mutating a cached source theme.
|
|
189
|
+
const MERGED_THEME_BY_SOURCE = new WeakMap<TerminalTheme, TerminalTheme>();
|
|
190
|
+
|
|
191
|
+
function resolveMergedTerminalTheme(theme?: TerminalTheme): TerminalTheme {
|
|
192
|
+
if (typeof theme === "undefined") {
|
|
193
|
+
return DEFAULT_MERGED_THEME;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const cached = MERGED_THEME_BY_SOURCE.get(theme);
|
|
197
|
+
if (cached) {
|
|
198
|
+
return cached;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const merged = mergeTerminalTheme(theme);
|
|
202
|
+
MERGED_THEME_BY_SOURCE.set(theme, merged);
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
187
207
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
188
208
|
}
|
|
@@ -239,7 +259,7 @@ export function resolveTerminalStyle(value: TerminalStyleValue | undefined, them
|
|
|
239
259
|
if (typeof value !== "string") {
|
|
240
260
|
return { ...value, border: isPlainObject(value.border) ? { ...value.border } : value.border, padding: isPlainObject(value.padding) ? { ...value.padding } : value.padding };
|
|
241
261
|
}
|
|
242
|
-
const merged =
|
|
262
|
+
const merged = resolveMergedTerminalTheme(theme);
|
|
243
263
|
const parts = value.split(".").filter(Boolean);
|
|
244
264
|
let node: unknown = merged.styles;
|
|
245
265
|
for (const part of parts) {
|
|
@@ -262,5 +282,5 @@ export function resolveTerminalStyleToken(
|
|
|
262
282
|
kind: TerminalSemanticStyleKind | (string & {}),
|
|
263
283
|
theme?: TerminalTheme
|
|
264
284
|
): TerminalStyleToken | undefined {
|
|
265
|
-
return
|
|
285
|
+
return resolveMergedTerminalTheme(theme).spans?.[kind];
|
|
266
286
|
}
|
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 {
|