@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.
Files changed (89) hide show
  1. package/dist/ansi.d.ts.map +1 -1
  2. package/dist/ansi.js +177 -17
  3. package/dist/ansi.js.map +1 -1
  4. package/dist/events.d.ts.map +1 -1
  5. package/dist/events.js +4 -0
  6. package/dist/events.js.map +1 -1
  7. package/dist/frame-style.d.ts +7 -0
  8. package/dist/frame-style.d.ts.map +1 -0
  9. package/dist/frame-style.js +27 -0
  10. package/dist/frame-style.js.map +1 -0
  11. package/dist/layout.d.ts +5 -1
  12. package/dist/layout.d.ts.map +1 -1
  13. package/dist/layout.js +53 -23
  14. package/dist/layout.js.map +1 -1
  15. package/dist/mouse.d.ts.map +1 -1
  16. package/dist/mouse.js +8 -1
  17. package/dist/mouse.js.map +1 -1
  18. package/dist/render-internal.d.ts +10 -0
  19. package/dist/render-internal.d.ts.map +1 -0
  20. package/dist/render-internal.js +1295 -0
  21. package/dist/render-internal.js.map +1 -0
  22. package/dist/render.d.ts.map +1 -1
  23. package/dist/render.js +13 -1205
  24. package/dist/render.js.map +1 -1
  25. package/dist/session.d.ts.map +1 -1
  26. package/dist/session.js +78 -4
  27. package/dist/session.js.map +1 -1
  28. package/dist/text.d.ts +7 -0
  29. package/dist/text.d.ts.map +1 -1
  30. package/dist/text.js +125 -0
  31. package/dist/text.js.map +1 -1
  32. package/dist/theme.d.ts.map +1 -1
  33. package/dist/theme.js +18 -2
  34. package/dist/theme.js.map +1 -1
  35. package/dist/types.d.ts +3 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/docs/api-reference.md +6 -3
  38. package/docs/cookbook.md +1 -1
  39. package/docs/interaction-model.md +5 -5
  40. package/docs/primitive-gallery.md +4 -4
  41. package/examples/basic.tsx +22 -0
  42. package/examples/cli.tsx +55 -0
  43. package/examples/demo.tsx +98 -0
  44. package/examples/docs/background-fill.tsx +107 -0
  45. package/examples/docs/component-composition.tsx +140 -0
  46. package/examples/docs/cursor.tsx +121 -0
  47. package/examples/docs/employees-list.tsx +138 -0
  48. package/examples/docs/hello.tsx +98 -0
  49. package/examples/docs/interactive-note.tsx +111 -0
  50. package/examples/docs/module-api-dashboard.tsx +307 -0
  51. package/examples/docs/module-flux-store.tsx +181 -0
  52. package/examples/docs/module-form-workflow.tsx +339 -0
  53. package/examples/docs/module-forms.tsx +218 -0
  54. package/examples/docs/module-money.tsx +175 -0
  55. package/examples/docs/module-native-store.tsx +188 -0
  56. package/examples/docs/module-pulses.tsx +142 -0
  57. package/examples/docs/module-query.tsx +209 -0
  58. package/examples/docs/module-request.tsx +194 -0
  59. package/examples/docs/module-state-workbench.tsx +283 -0
  60. package/examples/docs/module-tasks.tsx +223 -0
  61. package/examples/docs/module-translate.tsx +194 -0
  62. package/examples/docs/module-utils.tsx +168 -0
  63. package/examples/docs/module-valyrian-core.tsx +159 -0
  64. package/examples/docs/pizza-builder.tsx +463 -0
  65. package/examples/docs/primitive-activity-console.tsx +113 -0
  66. package/examples/docs/primitive-command-panel.tsx +186 -0
  67. package/examples/docs/primitive-data-explorer.tsx +155 -0
  68. package/examples/docs/primitive-input-workbench.tsx +128 -0
  69. package/examples/docs/primitive-layout-shell.tsx +115 -0
  70. package/examples/docs/responsive-split.tsx +186 -0
  71. package/examples/docs/style-system.tsx +209 -0
  72. package/examples/docs/theme-colors.tsx +225 -0
  73. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  74. package/examples/opencode-dogfood-app.tsx +215 -0
  75. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  76. package/examples/opencode-dogfood.tsx +11 -0
  77. package/llms-full.txt +16 -13
  78. package/package.json +3 -2
  79. package/src/ansi.ts +207 -17
  80. package/src/events.ts +2 -0
  81. package/src/frame-style.ts +36 -0
  82. package/src/layout.ts +57 -24
  83. package/src/mouse.ts +10 -1
  84. package/src/render-internal.ts +1441 -0
  85. package/src/render.ts +14 -1324
  86. package/src/session.ts +99 -12
  87. package/src/text.ts +160 -0
  88. package/src/theme.ts +22 -2
  89. 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 (!destroyed) {
310
- outputWriter.write(runtimeOptions.writesAnsi ? toAnsiDiff(currentFrame.lines, currentFrame.cursor, currentFrame.spans) : currentOutput);
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: { itemOffset?: number; itemIndexes?: number[]; y1: number; y2: number; contentY?: number; __listItemIndex?: number }, y: number) {
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: TerminalHitbox, type: "press" | "doublepress" | "contextpress") {
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 = mergeTerminalTheme(theme);
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 mergeTerminalTheme(theme).spans?.[kind];
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;