@valyrianjs/terminal 0.2.1 → 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 +177 -17
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.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 -1205
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +78 -4
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +125 -0
- 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 +3 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +207 -17
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render-internal.ts +1441 -0
- package/src/render.ts +14 -1324
- package/src/session.ts +99 -12
- package/src/text.ts +160 -0
- package/src/theme.ts +22 -2
- package/src/types.ts +3 -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;
|
|
@@ -58,6 +64,8 @@ const KNOWN_TERMINAL_KEY_SEQUENCES = [
|
|
|
58
64
|
"\u001b[13;129u",
|
|
59
65
|
"\u001b[27;2;13~",
|
|
60
66
|
"\u001b[13;2~",
|
|
67
|
+
"\u001b[1;2A",
|
|
68
|
+
"\u001b[1;2B",
|
|
61
69
|
"\u001b[1;2C",
|
|
62
70
|
"\u001b[1;2D",
|
|
63
71
|
"\u001b[1;3C",
|
|
@@ -93,6 +101,38 @@ function isKnownTerminalKeySequencePrefix(value: string) {
|
|
|
93
101
|
return KNOWN_TERMINAL_KEY_SEQUENCES.some((sequence) => value.length > 0 && value.length < sequence.length && sequence.startsWith(value));
|
|
94
102
|
}
|
|
95
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
|
+
|
|
96
136
|
function canContinueEscapeSequence(value: string) {
|
|
97
137
|
return value.startsWith("[") || value.startsWith("b") || value.startsWith("f");
|
|
98
138
|
}
|
|
@@ -108,7 +148,6 @@ function isValidTerminalDimension(value: number | undefined): value is number {
|
|
|
108
148
|
return Number.isInteger(value) && Number(value) >= 1;
|
|
109
149
|
}
|
|
110
150
|
|
|
111
|
-
|
|
112
151
|
function getProcessStdin(): TerminalInputStream | undefined {
|
|
113
152
|
const candidate = globalThis.process?.stdin as TerminalInputStream | undefined;
|
|
114
153
|
return candidate && typeof candidate.on === "function" ? candidate : undefined;
|
|
@@ -252,7 +291,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
252
291
|
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
253
292
|
let currentFrame = renderTreeFrame(currentTree);
|
|
254
293
|
let currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
255
|
-
let currentHitboxes = currentFrame.hitboxes;
|
|
294
|
+
let currentHitboxes = currentFrame.hitboxes as InternalTerminalHitbox[];
|
|
256
295
|
|
|
257
296
|
const outputWriter = createOutputWriter(runtimeOptions.stdout);
|
|
258
297
|
const toAnsiDiff = createAnsiDiffWriter({ showCursor: !runtimeOptions.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
@@ -270,6 +309,28 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
270
309
|
return mergeVertical(nodes.map((node) => renderTerminalFrame(node, context)));
|
|
271
310
|
}
|
|
272
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
|
+
|
|
273
334
|
function emitLifecycleSetup() {
|
|
274
335
|
const writes: string[] = [];
|
|
275
336
|
if (runtimeOptions.alternateScreen) {
|
|
@@ -306,9 +367,10 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
306
367
|
}
|
|
307
368
|
|
|
308
369
|
function emitOutput() {
|
|
309
|
-
if (
|
|
310
|
-
|
|
370
|
+
if (destroyed) {
|
|
371
|
+
return;
|
|
311
372
|
}
|
|
373
|
+
outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
|
|
312
374
|
}
|
|
313
375
|
|
|
314
376
|
function renderNow() {
|
|
@@ -327,7 +389,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
327
389
|
applyInteractiveState(currentTree, focusedId, inputStateById, editorStateById, listIndexById, listSelectedIndexById, listViewportOffsetById, scrollOffsetById, listHoverById, scrollHoverRowById);
|
|
328
390
|
currentFrame = renderTreeFrame(currentTree);
|
|
329
391
|
currentOutput = formatPlainFrame(currentFrame, { theme: options.theme }).trimEnd();
|
|
330
|
-
currentHitboxes = currentFrame.hitboxes;
|
|
392
|
+
currentHitboxes = currentFrame.hitboxes as InternalTerminalHitbox[];
|
|
331
393
|
emitOutput();
|
|
332
394
|
return currentOutput;
|
|
333
395
|
}
|
|
@@ -478,7 +540,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
478
540
|
return 1;
|
|
479
541
|
}
|
|
480
542
|
|
|
481
|
-
function sourceRowFromHitbox(node: TerminalFocusNode, hitbox:
|
|
543
|
+
function sourceRowFromHitbox(node: TerminalFocusNode, hitbox: InternalTerminalHitbox, y: number) {
|
|
482
544
|
if (node.tag === "terminal-list" && typeof hitbox.__listItemIndex === "number" && y >= hitbox.y1 && y <= hitbox.y2) {
|
|
483
545
|
return Math.max(1, Math.min(rowCountForNode(node), hitbox.__listItemIndex + 1));
|
|
484
546
|
}
|
|
@@ -539,7 +601,6 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
539
601
|
}
|
|
540
602
|
}
|
|
541
603
|
|
|
542
|
-
|
|
543
604
|
function listItemKey(node: TerminalFocusNode, index: number) {
|
|
544
605
|
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
545
606
|
const item = items[index];
|
|
@@ -668,7 +729,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
668
729
|
return dispatchNodeEvent(node, type, { type, id });
|
|
669
730
|
}
|
|
670
731
|
|
|
671
|
-
function dispatchHitboxButtonPressEvent(hitbox:
|
|
732
|
+
function dispatchHitboxButtonPressEvent(hitbox: InternalTerminalHitbox, type: "press" | "doublepress" | "contextpress") {
|
|
672
733
|
if (type !== "press" || typeof hitbox.__pressHandler !== "function") {
|
|
673
734
|
return false;
|
|
674
735
|
}
|
|
@@ -1163,7 +1224,7 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1163
1224
|
return toAnsiFrame(currentFrame.lines, currentFrame.cursor, currentFrame.spans, { showCursor: !options.hideCursor, showCursorWhenFrameHasCursor: true, theme: options.theme });
|
|
1164
1225
|
},
|
|
1165
1226
|
tree() {
|
|
1166
|
-
return currentTree;
|
|
1227
|
+
return publicTerminalNodes(currentTree);
|
|
1167
1228
|
},
|
|
1168
1229
|
focus(id: string) {
|
|
1169
1230
|
const node = findFocusableById(currentTree, id);
|
|
@@ -1341,6 +1402,18 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1341
1402
|
return;
|
|
1342
1403
|
}
|
|
1343
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
|
+
|
|
1344
1417
|
for (const sequence of KNOWN_TERMINAL_KEY_SEQUENCES) {
|
|
1345
1418
|
if (value.startsWith(sequence)) {
|
|
1346
1419
|
session.dispatchKey(parseTerminalKey(sequence));
|
|
@@ -1398,7 +1471,6 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1398
1471
|
}
|
|
1399
1472
|
}
|
|
1400
1473
|
|
|
1401
|
-
|
|
1402
1474
|
function isPrimaryMouseButton(button: number) {
|
|
1403
1475
|
return button < 64 && (button & 3) === 0;
|
|
1404
1476
|
}
|
|
@@ -1572,6 +1644,21 @@ export function mountTerminal(input: any, options: TerminalMountOptions = {}): T
|
|
|
1572
1644
|
const buffered = pendingKeyChunk + value;
|
|
1573
1645
|
cancelPendingEscapeFlush();
|
|
1574
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
|
+
}
|
|
1575
1662
|
processKeyStream(buffered);
|
|
1576
1663
|
return;
|
|
1577
1664
|
}
|
package/src/text.ts
CHANGED
|
@@ -5,6 +5,22 @@ 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>();
|
|
10
|
+
const COMBINING_MARK = /\p{Mark}/u;
|
|
11
|
+
const EMOJI_PRESENTATION = /\p{Extended_Pictographic}/u;
|
|
12
|
+
|
|
13
|
+
type GraphemeSegmenter = {
|
|
14
|
+
segment(value: string): Iterable<{ segment: string }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Segmenter = (Intl as unknown as {
|
|
18
|
+
Segmenter?: new (locale?: string, options?: { granularity: "grapheme" }) => GraphemeSegmenter;
|
|
19
|
+
}).Segmenter;
|
|
20
|
+
|
|
21
|
+
const GRAPHEME_SEGMENTER = typeof Segmenter === "function"
|
|
22
|
+
? new Segmenter(undefined, { granularity: "grapheme" })
|
|
23
|
+
: null;
|
|
8
24
|
|
|
9
25
|
export function stripTerminalControls(value: unknown) {
|
|
10
26
|
return String(value)
|
|
@@ -18,3 +34,147 @@ export function stripTerminalControls(value: unknown) {
|
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
export const plainText = stripTerminalControls;
|
|
37
|
+
|
|
38
|
+
export function terminalGraphemes(value: string): string[] {
|
|
39
|
+
if (GRAPHEME_SEGMENTER !== null) {
|
|
40
|
+
return Array.from(GRAPHEME_SEGMENTER.segment(value), (part) => part.segment);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Array.from(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isWideCodePoint(codePoint: number) {
|
|
47
|
+
return (
|
|
48
|
+
codePoint >= 0x1100 && (
|
|
49
|
+
codePoint <= 0x115f
|
|
50
|
+
|| codePoint === 0x2329
|
|
51
|
+
|| codePoint === 0x232a
|
|
52
|
+
|| (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
|
|
53
|
+
|| (codePoint >= 0xac00 && codePoint <= 0xd7a3)
|
|
54
|
+
|| (codePoint >= 0xf900 && codePoint <= 0xfaff)
|
|
55
|
+
|| (codePoint >= 0xfe10 && codePoint <= 0xfe19)
|
|
56
|
+
|| (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
|
|
57
|
+
|| (codePoint >= 0xff00 && codePoint <= 0xff60)
|
|
58
|
+
|| (codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
59
|
+
|| (codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isZeroWidthCodePoint(codePoint: number, char: string) {
|
|
65
|
+
return codePoint === 0x200d
|
|
66
|
+
|| (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
|
|
67
|
+
|| (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
|
|
68
|
+
|| COMBINING_MARK.test(char);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function graphemeCellWidth(grapheme: string) {
|
|
72
|
+
if (grapheme.length === 0) {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (grapheme.includes("\u200d") || EMOJI_PRESENTATION.test(grapheme)) {
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let width = 0;
|
|
81
|
+
for (const char of Array.from(grapheme)) {
|
|
82
|
+
const codePoint = char.codePointAt(0);
|
|
83
|
+
if (typeof codePoint !== "number") {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (isZeroWidthCodePoint(codePoint, char)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
width = Math.max(width, isWideCodePoint(codePoint) ? 2 : 1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return width;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function terminalCellWidth(value: unknown) {
|
|
96
|
+
const text = stripTerminalControls(value);
|
|
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;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sliceTerminalCells(value: string, maxCells: number) {
|
|
111
|
+
const limit = Math.max(0, Math.trunc(Number(maxCells) || 0));
|
|
112
|
+
let width = 0;
|
|
113
|
+
let output = "";
|
|
114
|
+
|
|
115
|
+
for (const grapheme of terminalGraphemes(value)) {
|
|
116
|
+
const graphemeWidth = graphemeCellWidth(grapheme);
|
|
117
|
+
if (graphemeWidth > 0 && width + graphemeWidth > limit) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
output += grapheme;
|
|
121
|
+
width += graphemeWidth;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function dropTerminalCells(value: string, cells: number) {
|
|
128
|
+
const limit = Math.max(0, Math.trunc(Number(cells) || 0));
|
|
129
|
+
let width = 0;
|
|
130
|
+
let output = "";
|
|
131
|
+
let dropping = true;
|
|
132
|
+
|
|
133
|
+
for (const grapheme of terminalGraphemes(value)) {
|
|
134
|
+
if (dropping) {
|
|
135
|
+
const graphemeWidth = graphemeCellWidth(grapheme);
|
|
136
|
+
if (width < limit || (graphemeWidth === 0 && width <= limit)) {
|
|
137
|
+
width += graphemeWidth;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
dropping = false;
|
|
141
|
+
}
|
|
142
|
+
output += grapheme;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function padEndTerminalCells(value: string, width: number) {
|
|
149
|
+
const size = Math.max(0, Math.trunc(Number(width) || 0));
|
|
150
|
+
const visibleWidth = terminalCellWidth(value);
|
|
151
|
+
return visibleWidth >= size ? value : `${value}${" ".repeat(size - visibleWidth)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function terminalCellToStringIndex(value: string) {
|
|
155
|
+
const indexes: number[] = [0];
|
|
156
|
+
let cellOffset = 0;
|
|
157
|
+
let stringIndex = 0;
|
|
158
|
+
|
|
159
|
+
for (const grapheme of terminalGraphemes(value)) {
|
|
160
|
+
const graphemeWidth = graphemeCellWidth(grapheme);
|
|
161
|
+
const nextStringIndex = stringIndex + grapheme.length;
|
|
162
|
+
|
|
163
|
+
if (graphemeWidth > 0) {
|
|
164
|
+
for (let cell = 1; cell < graphemeWidth; cell += 1) {
|
|
165
|
+
indexes[cellOffset + cell] = nextStringIndex;
|
|
166
|
+
}
|
|
167
|
+
cellOffset += graphemeWidth;
|
|
168
|
+
indexes[cellOffset] = nextStringIndex;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
stringIndex = nextStringIndex;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
indexes[cellOffset] = stringIndex;
|
|
175
|
+
return indexes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function cursorCellOffset(value: string, cursor: number) {
|
|
179
|
+
return terminalCellWidth(value.slice(0, Math.max(0, Math.trunc(Number(cursor) || 0))));
|
|
180
|
+
}
|
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
|
@@ -129,12 +129,11 @@ export interface TerminalHitbox {
|
|
|
129
129
|
y2: number;
|
|
130
130
|
textStartX?: number;
|
|
131
131
|
textLength?: number;
|
|
132
|
+
textCellToStringIndex?: number[];
|
|
132
133
|
itemOffset?: number;
|
|
133
134
|
itemIndexes?: number[];
|
|
134
135
|
contentY?: number;
|
|
135
136
|
pointerLayer?: number;
|
|
136
|
-
__listItemIndex?: number;
|
|
137
|
-
__pressHandler?: (event: TerminalButtonPressEventPayload) => void;
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
export interface CursorPosition {
|
|
@@ -387,7 +386,9 @@ export interface TerminalInputProps extends TerminalFocusableProps, TerminalStyl
|
|
|
387
386
|
export interface TerminalEditorProps extends TerminalFocusableProps, TerminalStyleProps {
|
|
388
387
|
value?: string;
|
|
389
388
|
placeholder?: string;
|
|
389
|
+
width?: number;
|
|
390
390
|
height?: number;
|
|
391
|
+
fill?: boolean;
|
|
391
392
|
onchange?(event: TerminalEditorChangeEventPayload): void;
|
|
392
393
|
oninput?(event: TerminalEditorChangeEventPayload): void;
|
|
393
394
|
onsubmit?(event: TerminalEditorSubmitEventPayload): void;
|