@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/layout.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { cloneStyleSpan, isFullFrameSpan, isFullRowSpan } from "./frame-style.js";
2
+ import { dropTerminalCells, padEndTerminalCells, sliceTerminalCells, terminalCellWidth } from "./text.js";
1
3
  import type { CursorPosition, TerminalFrame, TerminalHitbox, TerminalStyleSpan } from "./types.js";
2
4
 
3
5
  function repeat(char: string, count: number) {
@@ -9,7 +11,7 @@ export function createFrame(lines: string[], hitboxes: TerminalHitbox[] = [], cu
9
11
  }
10
12
 
11
13
  export function getFrameWidth(frame: TerminalFrame) {
12
- return frame.lines.reduce((max, line) => Math.max(max, line.length), 0);
14
+ return frame.lines.reduce((max, line) => Math.max(max, terminalCellWidth(line)), 0);
13
15
  }
14
16
 
15
17
  export function getFrameHeight(frame: TerminalFrame) {
@@ -29,12 +31,12 @@ export function shiftFrame(frame: TerminalFrame, dx: number, dy: number): Termin
29
31
  contentY: typeof box.contentY === "number" ? box.contentY + dy : undefined
30
32
  })),
31
33
  cursor: frame.cursor ? { x: frame.cursor.x + dx, y: frame.cursor.y + dy } : null,
32
- spans: frame.spans.map((span) => ({ ...span, x1: span.x1 + dx, x2: span.x2 + dx, y: span.y + dy }))
34
+ spans: frame.spans.map((span) => cloneStyleSpan(span, { x1: span.x1 + dx, x2: span.x2 + dx, y: span.y + dy }))
33
35
  };
34
36
  }
35
37
 
36
38
  function normalizeLines(frame: TerminalFrame, width: number, height: number) {
37
- const lines = frame.lines.map((line) => line.padEnd(width, " "));
39
+ const lines = frame.lines.map((line) => padEndTerminalCells(line, width));
38
40
  while (lines.length < height) {
39
41
  lines.push(repeat(" ", width));
40
42
  }
@@ -57,15 +59,15 @@ export function mergeVertical(frames: TerminalFrame[], options: { gap?: number }
57
59
 
58
60
  for (let i = 0; i < filtered.length; i += 1) {
59
61
  const frame = filtered[i];
60
- const normalized = normalizeLines(frame, width, getFrameHeight(frame));
61
- lines.push(...normalized);
62
- const shifted = shiftFrame(frame, 0, rowOffset);
62
+ const normalizedFrame = fitFrame(frame, width, getFrameHeight(frame), { expandFullFrameSpans: true });
63
+ lines.push(...normalizedFrame.lines);
64
+ const shifted = shiftFrame(normalizedFrame, 0, rowOffset);
63
65
  hitboxes.push(...shifted.hitboxes);
64
66
  spans.push(...shifted.spans);
65
67
  if (!cursor && frame.cursor) {
66
68
  cursor = { x: frame.cursor.x, y: frame.cursor.y + rowOffset };
67
69
  }
68
- rowOffset += normalized.length;
70
+ rowOffset += normalizedFrame.lines.length;
69
71
  if (i < filtered.length - 1 && gap > 0) {
70
72
  for (let j = 0; j < gap; j += 1) {
71
73
  lines.push(repeat(" ", width));
@@ -87,9 +89,8 @@ export function mergeHorizontal(frames: TerminalFrame[], options: { gap?: number
87
89
  const widths = filtered.map(getFrameWidth);
88
90
  const height = filtered.reduce((max, frame) => Math.max(max, getFrameHeight(frame)), 0);
89
91
  const normalizedFrames = filtered.map((frame, index) => ({
90
- frame,
91
- width: widths[index],
92
- lines: normalizeLines(frame, widths[index], height)
92
+ frame: fitFrame(frame, widths[index], height, { expandFullFrameSpans: true }),
93
+ width: widths[index]
93
94
  }));
94
95
  const gapText = repeat(" ", gap);
95
96
  const lines = new Array<string>(height).fill("");
@@ -101,7 +102,7 @@ export function mergeHorizontal(frames: TerminalFrame[], options: { gap?: number
101
102
  for (let index = 0; index < normalizedFrames.length; index += 1) {
102
103
  const part = normalizedFrames[index];
103
104
  for (let row = 0; row < height; row += 1) {
104
- lines[row] += part.lines[row];
105
+ lines[row] += part.frame.lines[row];
105
106
  if (index < normalizedFrames.length - 1) {
106
107
  lines[row] += gapText;
107
108
  }
@@ -125,20 +126,20 @@ export function padFrame(frame: TerminalFrame, padding: number) {
125
126
  }
126
127
 
127
128
  const width = getFrameWidth(frame);
128
- const middle = frame.lines.map((line) => `${repeat(" ", amount)}${line.padEnd(width, " ")}${repeat(" ", amount)}`);
129
+ const middle = frame.lines.map((line) => `${repeat(" ", amount)}${padEndTerminalCells(line, width)}${repeat(" ", amount)}`);
129
130
  const blank = repeat(" ", width + amount * 2);
130
131
  const lines = [...new Array<string>(amount).fill(blank), ...middle, ...new Array<string>(amount).fill(blank)];
131
132
  const shifted = shiftFrame(frame, amount, amount);
132
133
  return createFrame(lines, shifted.hitboxes, frame.cursor ? { x: frame.cursor.x + amount, y: frame.cursor.y + amount } : null, shifted.spans);
133
134
  }
134
135
 
135
- export function fitFrame(frame: TerminalFrame, width?: number, height?: number) {
136
+ export function fitFrame(frame: TerminalFrame, width?: number, height?: number, options: { expandFullFrameSpans?: boolean } = {}) {
136
137
  const nextWidth = Math.max(getFrameWidth(frame), Number(width || 0));
137
138
  const nextHeight = Math.max(getFrameHeight(frame), Number(height || 0));
138
139
  if (nextWidth === getFrameWidth(frame) && nextHeight === getFrameHeight(frame)) {
139
140
  return frame;
140
141
  }
141
- return createFrame(normalizeLines(frame, nextWidth, nextHeight), frame.hitboxes, frame.cursor, frame.spans);
142
+ return constrainFrame(frame, { width: nextWidth, height: nextHeight, expandFullFrameSpans: options.expandFullFrameSpans });
142
143
  }
143
144
 
144
145
  function constraintSize(value: number | undefined, fallback: number) {
@@ -160,13 +161,13 @@ function clamp(value: number, min: number, max: number) {
160
161
  return Math.min(max, Math.max(min, value));
161
162
  }
162
163
 
163
- export function constrainFrame(frame: TerminalFrame, options: { width?: number; height?: number } = {}): TerminalFrame {
164
+ export function constrainFrame(frame: TerminalFrame, options: { width?: number; height?: number; expandFullFrameSpans?: boolean; expandFullRowSpans?: boolean } = {}): TerminalFrame {
164
165
  const width = constraintSize(options.width, getFrameWidth(frame));
165
166
  const height = constraintSize(options.height, getFrameHeight(frame));
166
167
  const lines: string[] = [];
167
168
 
168
169
  for (let row = 0; row < height; row += 1) {
169
- lines.push((frame.lines[row] || "").slice(0, width).padEnd(width, " "));
170
+ lines.push(padEndTerminalCells(sliceTerminalCells(frame.lines[row] || "", width), width));
170
171
  }
171
172
 
172
173
  const hitboxes = frame.hitboxes
@@ -184,8 +185,26 @@ export function constrainFrame(frame: TerminalFrame, options: { width?: number;
184
185
  ? { x: frame.cursor.x, y: frame.cursor.y }
185
186
  : null;
186
187
 
188
+ const originalWidth = getFrameWidth(frame);
189
+ const originalHeight = getFrameHeight(frame);
187
190
  const spanRightEdge = width + 1;
188
191
  const spans = frame.spans.flatMap((span) => {
192
+ const coversCurrentFrame = options.expandFullFrameSpans === true && isFullFrameSpan(span) && span.x1 === 1 && span.x2 === originalWidth + 1;
193
+ if (coversCurrentFrame) {
194
+ if (width <= 0 || span.y < 1 || span.y > Math.min(originalHeight, height)) {
195
+ return [];
196
+ }
197
+ return [cloneStyleSpan(span, { x1: 1, x2: spanRightEdge })];
198
+ }
199
+
200
+ const coversCurrentRow = (options.expandFullFrameSpans === true || options.expandFullRowSpans === true) && isFullRowSpan(span) && span.x1 === 1 && span.x2 === originalWidth + 1;
201
+ if (coversCurrentRow) {
202
+ if (width <= 0 || span.y < 1 || span.y > Math.min(originalHeight, height)) {
203
+ return [];
204
+ }
205
+ return [cloneStyleSpan(span, { x1: 1, x2: spanRightEdge })];
206
+ }
207
+
189
208
  if (width <= 0 || span.y < 1 || span.y > height || span.x2 <= 1 || span.x1 >= spanRightEdge) {
190
209
  return [];
191
210
  }
@@ -196,9 +215,22 @@ export function constrainFrame(frame: TerminalFrame, options: { width?: number;
196
215
  return [];
197
216
  }
198
217
 
199
- return [{ ...span, x1: clippedX1, x2: clippedX2 }];
218
+ return [cloneStyleSpan(span, { x1: clippedX1, x2: clippedX2 })];
200
219
  });
201
220
 
221
+ const fullFrameSpans = options.expandFullFrameSpans === true
222
+ ? frame.spans.filter((span) => isFullFrameSpan(span) && span.x1 === 1 && span.x2 === originalWidth + 1)
223
+ : [];
224
+ const firstFullFrameRow = fullFrameSpans.reduce<number | null>((first, span) => first === null ? span.y : Math.min(first, span.y), null);
225
+ if (firstFullFrameRow !== null) {
226
+ const templates = fullFrameSpans.filter((span) => span.y === firstFullFrameRow);
227
+ for (let y = originalHeight + 1; y <= height; y += 1) {
228
+ for (const template of templates) {
229
+ spans.push(cloneStyleSpan(template, { x1: 1, x2: spanRightEdge, y }));
230
+ }
231
+ }
232
+ }
233
+
202
234
  return createFrame(lines, hitboxes, cursor, spans);
203
235
  }
204
236
 
@@ -234,16 +266,20 @@ export function overlayFrame(base: TerminalFrame, overlay: TerminalFrame, option
234
266
 
235
267
  const baseLine = lines[baseRow];
236
268
  const start = x - 1;
237
- if (start >= baseLine.length) {
269
+ if (start >= terminalCellWidth(baseLine)) {
238
270
  continue;
239
271
  }
240
272
 
241
- const visibleWidth = Math.min(constrainedOverlay.lines[row].length, baseLine.length - start);
273
+ const visibleWidth = Math.min(terminalCellWidth(constrainedOverlay.lines[row]), terminalCellWidth(baseLine) - start);
242
274
  if (visibleWidth <= 0) {
243
275
  continue;
244
276
  }
245
277
 
246
- lines[baseRow] = `${baseLine.slice(0, start)}${constrainedOverlay.lines[row].slice(0, visibleWidth)}${baseLine.slice(start + visibleWidth)}`;
278
+ const prefix = sliceTerminalCells(baseLine, start);
279
+ const suffix = dropTerminalCells(baseLine, start + visibleWidth);
280
+ const insert = sliceTerminalCells(constrainedOverlay.lines[row], visibleWidth);
281
+ const removedWidth = terminalCellWidth(baseLine) - terminalCellWidth(prefix) - terminalCellWidth(suffix);
282
+ lines[baseRow] = `${prefix}${padEndTerminalCells(insert, removedWidth)}${suffix}`;
247
283
  }
248
284
 
249
285
  const shiftedOverlay = shiftFrame(constrainedOverlay, x - 1, y - 1);
@@ -271,10 +307,7 @@ export function cropFrame(frame: TerminalFrame, offset: number, height: number)
271
307
  }));
272
308
  const spans = frame.spans
273
309
  .filter((span) => span.y > start && span.y <= start + size)
274
- .map((span) => ({
275
- ...span,
276
- y: span.y - start
277
- }));
310
+ .map((span) => cloneStyleSpan(span, { y: span.y - start }));
278
311
  const cursor = frame.cursor && frame.cursor.y > start && frame.cursor.y <= start + size
279
312
  ? { x: frame.cursor.x, y: frame.cursor.y - start }
280
313
  : null;
package/src/mouse.ts CHANGED
@@ -79,6 +79,15 @@ export function cursorFromHitbox(hitbox: TerminalHitbox, x: number) {
79
79
  if (typeof hitbox.textStartX !== "number") {
80
80
  return 0;
81
81
  }
82
+
82
83
  const textLength = Number(hitbox.textLength || 0);
83
- return Math.max(0, Math.min(textLength, x - hitbox.textStartX));
84
+ const cellOffset = Math.max(0, x - hitbox.textStartX);
85
+ if (Array.isArray(hitbox.textCellToStringIndex) && hitbox.textCellToStringIndex.length > 0) {
86
+ const index = hitbox.textCellToStringIndex[Math.min(cellOffset, hitbox.textCellToStringIndex.length - 1)];
87
+ if (typeof index === "number" && Number.isFinite(index)) {
88
+ return Math.max(0, Math.min(textLength, index));
89
+ }
90
+ }
91
+
92
+ return Math.max(0, Math.min(textLength, cellOffset));
84
93
  }