@valyrianjs/terminal 0.2.0 → 0.2.1

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 (55) hide show
  1. package/dist/ansi.d.ts +2 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +12 -0
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/events.js +6 -2
  7. package/dist/events.js.map +1 -1
  8. package/dist/keymap.d.ts.map +1 -1
  9. package/dist/keymap.js +4 -2
  10. package/dist/keymap.js.map +1 -1
  11. package/dist/layout.d.ts.map +1 -1
  12. package/dist/layout.js +2 -1
  13. package/dist/layout.js.map +1 -1
  14. package/dist/mouse.d.ts +6 -0
  15. package/dist/mouse.d.ts.map +1 -1
  16. package/dist/mouse.js +30 -16
  17. package/dist/mouse.js.map +1 -1
  18. package/dist/primitives.d.ts.map +1 -1
  19. package/dist/primitives.js +8 -1
  20. package/dist/primitives.js.map +1 -1
  21. package/dist/render.d.ts.map +1 -1
  22. package/dist/render.js +184 -27
  23. package/dist/render.js.map +1 -1
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +13 -5
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/session.d.ts.map +1 -1
  28. package/dist/session.js +323 -83
  29. package/dist/session.js.map +1 -1
  30. package/dist/theme.d.ts.map +1 -1
  31. package/dist/theme.js +3 -0
  32. package/dist/theme.js.map +1 -1
  33. package/dist/tree.d.ts.map +1 -1
  34. package/dist/tree.js +18 -4
  35. package/dist/tree.js.map +1 -1
  36. package/dist/types.d.ts +38 -4
  37. package/dist/types.d.ts.map +1 -1
  38. package/docs/api-reference.md +13 -6
  39. package/docs/cookbook.md +1 -1
  40. package/docs/interaction-model.md +7 -5
  41. package/docs/primitive-gallery.md +7 -3
  42. package/llms-full.txt +28 -15
  43. package/package.json +1 -1
  44. package/src/ansi.ts +12 -0
  45. package/src/events.ts +4 -2
  46. package/src/keymap.ts +4 -2
  47. package/src/layout.ts +2 -1
  48. package/src/mouse.ts +31 -15
  49. package/src/primitives.ts +8 -1
  50. package/src/render.ts +199 -28
  51. package/src/runtime.ts +13 -5
  52. package/src/session.ts +341 -79
  53. package/src/theme.ts +3 -0
  54. package/src/tree.ts +19 -4
  55. package/src/types.ts +45 -3
package/src/render.ts CHANGED
@@ -6,7 +6,7 @@ import { renderValyrianTerminal } from "./runtime.js";
6
6
  import { plainText } from "./text.js";
7
7
  import { resolveTerminalStyle } from "./theme.js";
8
8
 
9
- import type { InputInteractionState, TerminalElementNode, TerminalFocusNode, TerminalFrame, TerminalNode, TerminalSpacing, TerminalSplitBreakpoint, TerminalSplitSize, TerminalStyleDefinition, TerminalStyleSpan, TerminalTheme, TerminalVisualState } from "./types.js";
9
+ import type { InputInteractionState, TerminalElementNode, TerminalFocusNode, TerminalFrame, TerminalHitbox, TerminalNode, TerminalSpacing, TerminalSplitBreakpoint, TerminalSplitSize, TerminalStyleDefinition, TerminalStyleSpan, TerminalTheme, TerminalVisualState } from "./types.js";
10
10
 
11
11
  export interface TerminalRenderContext {
12
12
  cols: number;
@@ -258,9 +258,22 @@ function addFullFrameSpans(frame: TerminalFrame, kinds: ResolvedStyleSpan[]) {
258
258
  return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
259
259
  }
260
260
 
261
- function listVirtualRange(node: TerminalElementNode, itemCount: number, selectedIndex: number, context?: TerminalRenderContext) {
261
+ function listViewportRows(node: TerminalElementNode, itemCount: number, context?: TerminalRenderContext) {
262
+ const explicitHeight = positiveDimension(node.props.height, "height");
263
+ const viewportSourceRows = explicitHeight ?? context?.rows ?? (itemCount || 1);
264
+ return Math.max(1, Math.min(itemCount || 1, positiveInteger(viewportSourceRows, "List viewport height")));
265
+ }
266
+
267
+ function clampListIndex(index: number, itemCount: number) {
268
+ if (itemCount <= 0) {
269
+ return 0;
270
+ }
271
+ return Math.max(0, Math.min(itemCount - 1, index));
272
+ }
273
+
274
+ function listVirtualRange(node: TerminalElementNode, itemCount: number, context?: TerminalRenderContext) {
262
275
  if (!node.props.virtualized) {
263
- return { start: 0, end: itemCount };
276
+ return { start: 0, end: itemCount, visibleStart: 0, viewportRows: itemCount || 1 };
264
277
  }
265
278
 
266
279
  if (typeof node.props.itemHeight !== "undefined" && node.props.itemHeight !== 1) {
@@ -268,14 +281,95 @@ function listVirtualRange(node: TerminalElementNode, itemCount: number, selected
268
281
  }
269
282
 
270
283
  const overscan = nonNegativeInteger(node.props.overscan, "List overscan");
271
- const viewportSourceRows = context?.rows ?? (itemCount || 1);
272
- const viewportRows = Math.max(1, Math.min(itemCount || 1, positiveInteger(viewportSourceRows, "List viewport height")));
273
- const selected = Math.max(0, Math.min(itemCount - 1, selectedIndex));
274
- const visibleStart = Math.max(0, Math.min(selected, selected - viewportRows + 1));
284
+ const viewportRows = listViewportRows(node, itemCount, context);
285
+ const maxOffset = Math.max(0, itemCount - viewportRows);
286
+ let visibleStart = Math.max(0, Math.min(maxOffset, nonNegativeInteger(node.props.__scrollOffset, "List viewport offset")));
275
287
  const start = Math.max(0, visibleStart - overscan);
276
288
  const end = Math.min(itemCount, visibleStart + viewportRows + overscan);
277
289
 
278
- return { start, end };
290
+ return { start, end, visibleStart, viewportRows };
291
+ }
292
+
293
+ function listItemKey(node: TerminalElementNode, item: unknown, index: number) {
294
+ if (typeof node.props.itemKey === "function") {
295
+ const key = node.props.itemKey(item, index);
296
+ if (typeof key !== "string" && typeof key !== "number") {
297
+ throw new RangeError("List itemKey must return a string or number");
298
+ }
299
+ return String(key);
300
+ }
301
+ return String(index);
302
+ }
303
+
304
+ function listItemRenderer(node: TerminalElementNode) {
305
+ if (typeof node.props.__childrenRenderer === "function") {
306
+ return { type: "children" as const, render: node.props.__childrenRenderer };
307
+ }
308
+ if (typeof node.props.renderItem === "function") {
309
+ return { type: "renderItem" as const, render: node.props.renderItem };
310
+ }
311
+ return undefined;
312
+ }
313
+
314
+ function wrapPlainText(value: string, width: number) {
315
+ if (!Number.isFinite(width) || !Number.isInteger(width) || width <= 0) {
316
+ return [""];
317
+ }
318
+
319
+ const rows: string[] = [];
320
+ const sourceRows = value.split("\n");
321
+ for (const sourceRow of sourceRows) {
322
+ if (sourceRow.length === 0) {
323
+ rows.push("");
324
+ continue;
325
+ }
326
+
327
+ let remaining = sourceRow;
328
+ while (remaining.length > width) {
329
+ const slice = remaining.slice(0, width);
330
+ const breakAt = slice.lastIndexOf(" ");
331
+ if (breakAt > 0 && breakAt >= Math.floor(width * 0.6)) {
332
+ rows.push(remaining.slice(0, breakAt));
333
+ remaining = remaining.slice(breakAt + 1);
334
+ } else {
335
+ rows.push(slice);
336
+ remaining = remaining.slice(width);
337
+ }
338
+ }
339
+ rows.push(remaining);
340
+ }
341
+
342
+ return rows.length ? rows : [""];
343
+ }
344
+
345
+ function renderListItemFrame(node: TerminalElementNode, item: unknown, index: number, viewportIndex: number, activeIndex: number, selectedIndex: number | null, wrapWidth: number, context?: TerminalRenderContext) {
346
+ const key = listItemKey(node, item, index);
347
+ const renderer = listItemRenderer(node);
348
+ if (!renderer) {
349
+ const label = plainText(item);
350
+ return createFrame(node.props.wrap === true ? wrapPlainText(label, wrapWidth) : label.split("\n"));
351
+ }
352
+
353
+ const ctx = {
354
+ index,
355
+ key,
356
+ active: index === activeIndex,
357
+ selected: selectedIndex !== null && index === selectedIndex,
358
+ viewportIndex,
359
+ item
360
+ };
361
+ const rendered = renderer.type === "children" ? renderer.render(item, ctx) : renderer.render(item, index);
362
+
363
+ if (typeof rendered === "string" || typeof rendered === "number") {
364
+ const label = plainText(rendered);
365
+ return createFrame(node.props.wrap === true ? wrapPlainText(label, wrapWidth) : label.split("\n"));
366
+ }
367
+
368
+ const frame = mergeVertical(renderValyrianTerminal(rendered).map((child) => renderTerminalFrame(child, context)));
369
+ if (node.props.wrap === true && frame.hitboxes.length === 0) {
370
+ return createFrame(frame.lines.flatMap((line) => wrapPlainText(line, wrapWidth)));
371
+ }
372
+ return frame;
279
373
  }
280
374
 
281
375
  function fixedPosition(value: unknown) {
@@ -854,11 +948,15 @@ function renderOverlayChildFrame(node: TerminalElementNode, width: number, heigh
854
948
  return { frame, geometry };
855
949
  }
856
950
 
951
+ function orderedDirectOverlays(overlays: TerminalElementNode[]) {
952
+ return overlays.map((overlay, sourceOrder) => ({ overlay, sourceOrder })).sort((a, b) => a.sourceOrder - b.sourceOrder).map(({ overlay }) => overlay);
953
+ }
954
+
857
955
  function applyDirectOverlays(base: TerminalFrame, overlays: TerminalElementNode[], context?: TerminalRenderContext) {
858
956
  let frame = base;
859
957
  const width = Math.max(1, getFrameWidth(base));
860
958
  const height = Math.max(1, getFrameHeight(base));
861
- for (const overlay of overlays) {
959
+ for (const overlay of orderedDirectOverlays(overlays)) {
862
960
  const rendered = renderOverlayChildFrame(overlay, width, height, context);
863
961
  frame = overlayFrame(frame, rendered.frame, rendered.geometry);
864
962
  }
@@ -1084,41 +1182,114 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
1084
1182
  return renderLogViewFrame(node, context);
1085
1183
  case "terminal-list": {
1086
1184
  const items = Array.isArray(node.props.items) ? node.props.items : [];
1087
- const selectedIndex = numeric(node.props.__selectedIndex, 0);
1185
+ const activeIndex = clampListIndex(numeric(node.props.__activeIndex ?? node.props.__selectedIndex, 0), items.length);
1186
+ const selectedIndex = typeof node.props.__selectedIndex === "number" ? clampListIndex(Number(node.props.__selectedIndex), items.length) : null;
1088
1187
  const hoveredIndex = typeof node.props.__hoveredIndex === "number" ? Number(node.props.__hoveredIndex) : -1;
1089
- const range = listVirtualRange(node, items.length, selectedIndex, context);
1090
- const lines: string[] = [];
1091
- for (let index = range.start; index < range.end; index += 1) {
1092
- const item = items[index];
1093
- const label = typeof node.props.renderItem === "function" ? plainText(node.props.renderItem(item, index)) : plainText(item);
1094
- lines.push(label);
1095
- }
1096
- const visibleLines = lines.length ? lines : [""];
1188
+ const range = listVirtualRange(node, items.length, context);
1097
1189
  const layoutStyle = resolveLayoutStyle("list.base", node, context);
1098
1190
  const padding = normalizeSpacing(layoutStyle.padding, "List padding");
1099
1191
  const border = normalizeBorder(layoutStyle.border);
1100
- const decorated = addBorder(padFrameSides(createFrame(visibleLines), padding), border);
1192
+ const horizontalDecoration = padding.left + padding.right + (border.left ? 1 : 0) + (border.right ? 1 : 0);
1193
+ const wrapWidth = typeof context?.cols === "number" ? Math.max(1, context.cols - horizontalDecoration) : 1;
1194
+ const visibleLines: string[] = [];
1195
+ const itemIndexes: number[] = [];
1196
+ const childHitboxes: TerminalHitbox[] = [];
1197
+ for (let index = range.start; index < range.end; index += 1) {
1198
+ const item = items[index];
1199
+ const itemFrame = renderListItemFrame(node, item, index, index - range.visibleStart, activeIndex, selectedIndex, wrapWidth, context);
1200
+ const rowOffset = visibleLines.length;
1201
+ visibleLines.push(...itemFrame.lines);
1202
+ for (let row = 0; row < itemFrame.lines.length; row += 1) {
1203
+ itemIndexes.push(index);
1204
+ }
1205
+ childHitboxes.push(...shiftFrame(itemFrame, 0, rowOffset).hitboxes);
1206
+ }
1207
+ if (!visibleLines.length) {
1208
+ visibleLines.push("");
1209
+ itemIndexes.push(0);
1210
+ }
1211
+
1212
+ const frameHeight = node.props.virtualized && node.props.wrap === true
1213
+ ? positiveDimension(node.props.height, "height") ?? (typeof context?.rows === "number" ? Math.max(1, context.rows) : undefined)
1214
+ : undefined;
1215
+ const contentHeight = typeof frameHeight === "number"
1216
+ ? Math.max(1, frameHeight - padding.top - padding.bottom - (border.top ? 1 : 0) - (border.bottom ? 1 : 0))
1217
+ : visibleLines.length;
1218
+ let visibleLineStart = 0;
1219
+ if (node.props.virtualized && node.props.wrap === true && visibleLines.length > contentHeight) {
1220
+ const activeLineIndex = itemIndexes.findIndex((sourceIndex) => sourceIndex === activeIndex);
1221
+ if (activeLineIndex >= contentHeight) {
1222
+ visibleLineStart = activeLineIndex - contentHeight + 1;
1223
+ }
1224
+ }
1225
+ const frameLines = visibleLines.slice(visibleLineStart, visibleLineStart + contentHeight);
1226
+ const frameItemIndexes = itemIndexes.slice(visibleLineStart, visibleLineStart + contentHeight);
1227
+ const frameChildHitboxes = childHitboxes
1228
+ .filter((box) => box.y2 > visibleLineStart && box.y1 <= visibleLineStart + contentHeight)
1229
+ .map((box) => ({
1230
+ ...box,
1231
+ y1: Math.max(1, box.y1 - visibleLineStart),
1232
+ y2: Math.min(contentHeight, box.y2 - visibleLineStart),
1233
+ contentY: typeof box.contentY === "number" ? Math.max(1, box.contentY - visibleLineStart) : undefined
1234
+ }));
1235
+
1236
+ const decorated = addBorder(padFrameSides(createFrame(frameLines, frameChildHitboxes), padding), border);
1101
1237
  const width = Math.max(1, getFrameWidth(decorated));
1102
1238
  const height = Math.max(1, getFrameHeight(decorated));
1103
1239
  const itemY = 1 + (border.top ? 1 : 0) + padding.top;
1104
1240
  const spans: TerminalStyleSpan[] = [];
1105
- for (let index = 0; index < visibleLines.length; index += 1) {
1106
- const sourceIndex = range.start + index;
1241
+ for (let index = 0; index < frameLines.length; index += 1) {
1242
+ const sourceIndex = frameItemIndexes[index];
1107
1243
  const y = itemY + index;
1108
1244
  spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
1109
- if (sourceIndex === selectedIndex) {
1245
+ if (selectedIndex !== null && selectedIndex !== activeIndex && sourceIndex === selectedIndex) {
1246
+ spans.push({ kind: "list.selected", x1: 1, x2: width + 1, y });
1247
+ }
1248
+ if (sourceIndex === activeIndex) {
1110
1249
  spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
1111
1250
  }
1112
1251
  if (sourceIndex === hoveredIndex) {
1113
1252
  spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
1114
1253
  }
1115
1254
  }
1116
- const frame = createFrame(decorated.lines, [], decorated.cursor, spans);
1117
- const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1118
- if (!node.props.id) {
1119
- return styled;
1255
+ const listHitboxes: TerminalHitbox[] = [];
1256
+ const itemHitboxes: TerminalHitbox[] = [];
1257
+ if (node.props.id) {
1258
+ listHitboxes.push({
1259
+ id: node.props.id,
1260
+ tag: node.tag,
1261
+ x1: 1,
1262
+ x2: width,
1263
+ y1: 1,
1264
+ y2: Math.min(height, typeof frameHeight === "number" ? frameHeight : height),
1265
+ itemOffset: range.start,
1266
+ itemIndexes: frameItemIndexes,
1267
+ contentY: itemY
1268
+ });
1269
+ let itemStart = 0;
1270
+ while (itemStart < frameItemIndexes.length) {
1271
+ const sourceIndex = frameItemIndexes[itemStart];
1272
+ let itemEnd = itemStart;
1273
+ while (itemEnd + 1 < frameItemIndexes.length && frameItemIndexes[itemEnd + 1] === sourceIndex) {
1274
+ itemEnd += 1;
1275
+ }
1276
+ itemHitboxes.push({
1277
+ id: node.props.id,
1278
+ tag: node.tag,
1279
+ x1: 1,
1280
+ x2: width,
1281
+ y1: itemY + itemStart,
1282
+ y2: itemY + itemEnd,
1283
+ itemOffset: range.start,
1284
+ __listItemIndex: sourceIndex,
1285
+ itemIndexes: new Array(itemEnd - itemStart + 1).fill(sourceIndex)
1286
+ });
1287
+ itemStart = itemEnd + 1;
1288
+ }
1120
1289
  }
1121
- return createFrame(styled.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, itemOffset: range.start }], styled.cursor, styled.spans);
1290
+ const frame = createFrame(decorated.lines, [...listHitboxes, ...itemHitboxes, ...decorated.hitboxes], decorated.cursor, spans);
1291
+ const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
1292
+ return typeof frameHeight === "number" ? constrainFrame(styled, { height: frameHeight }) : styled;
1122
1293
  }
1123
1294
  case "terminal-table":
1124
1295
  return renderTableFrame(node, context);
@@ -1173,7 +1344,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
1173
1344
  const decorated = decoratedControlFrame([String(label)], layoutStyle);
1174
1345
  const width = Math.max(1, getFrameWidth(decorated));
1175
1346
  const height = Math.max(1, getFrameHeight(decorated));
1176
- const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height }] : [];
1347
+ const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, __pressHandler: typeof node.props.onpress === "function" ? node.props.onpress : undefined }] : [];
1177
1348
  const kinds = ["button.base", ...nodeStates(node).map((state) => `button.${state}`)];
1178
1349
  const spans = fullFrameSpans(kinds, width, height);
1179
1350
  return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
package/src/runtime.ts CHANGED
@@ -144,15 +144,23 @@ function normalizeValyrianInput(input: any): any {
144
144
  }
145
145
 
146
146
  const props = { ...((input.props && input.props.__terminalProps) || input.props || {}) };
147
- const children = Array.isArray(input.children)
148
- ? input.children.map((child) => {
147
+ const rawChildren = Array.isArray(input.children) ? input.children : [];
148
+ const isListComponentWithRenderChild = typeof input.tag === "function" && input.tag.name === "TerminalList" && rawChildren.length === 1 && typeof rawChildren[0] === "function";
149
+ if (isListComponentWithRenderChild) {
150
+ props.__childrenRenderer = rawChildren[0];
151
+ }
152
+ const tag = typeof input.tag === "string" ? input.tag : wrapComponent(input.tag);
153
+ const children = isListComponentWithRenderChild || (tag === "terminal-list" && rawChildren.length === 1 && typeof rawChildren[0] === "function")
154
+ ? []
155
+ : rawChildren.map((child) => {
149
156
  if (props["v-for"] && typeof child === "function") {
150
157
  return (...args: any[]) => normalizeValyrianInput(child(...args));
151
158
  }
152
159
  return normalizeValyrianInput(child);
153
- })
154
- : [];
155
- const tag = typeof input.tag === "string" ? input.tag : wrapComponent(input.tag);
160
+ });
161
+ if (tag === "terminal-list" && rawChildren.length === 1 && typeof rawChildren[0] === "function") {
162
+ props.__childrenRenderer = rawChildren[0];
163
+ }
156
164
  if (typeof tag === "string" && isTerminalTag(tag)) {
157
165
  const terminalProps = { ...props };
158
166
  if (typeof props.style === "object") {