@valyrianjs/terminal 0.1.2 → 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.
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +12 -0
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +6 -2
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keymap.d.ts.map +1 -1
- package/dist/keymap.js +4 -2
- package/dist/keymap.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -1
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +6 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +30 -16
- package/dist/mouse.js.map +1 -1
- package/dist/primitives.d.ts +8 -3
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +8 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +290 -51
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -11
- package/dist/runtime.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +434 -65
- package/dist/session.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +3 -0
- package/dist/theme.js.map +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +18 -4
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +61 -13
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +40 -28
- package/docs/cookbook.md +2 -2
- package/docs/core-concepts.md +1 -1
- package/docs/interaction-model.md +18 -6
- package/docs/primitive-gallery.md +19 -10
- package/llms-full.txt +80 -47
- package/package.json +1 -1
- package/src/ansi.ts +12 -0
- package/src/events.ts +4 -2
- package/src/index.ts +3 -0
- package/src/keymap.ts +4 -2
- package/src/layout.ts +2 -1
- package/src/mouse.ts +31 -15
- package/src/primitives.ts +15 -5
- package/src/render.ts +320 -52
- package/src/runtime.ts +13 -11
- package/src/session.ts +469 -59
- package/src/theme.ts +3 -0
- package/src/tree.ts +19 -4
- package/src/types.ts +72 -12
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;
|
|
@@ -14,6 +14,25 @@ export interface TerminalRenderContext {
|
|
|
14
14
|
theme?: TerminalTheme;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function validateRenderContextDimension(name: "cols" | "rows", value: number) {
|
|
18
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
19
|
+
throw new RangeError(`Invalid render context ${name}: expected an integer >= 1`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateRenderContext(context: TerminalRenderContext | undefined) {
|
|
25
|
+
if (typeof context === "undefined") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
cols: validateRenderContextDimension("cols", context.cols),
|
|
31
|
+
rows: validateRenderContextDimension("rows", context.rows),
|
|
32
|
+
theme: context.theme
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
const VISUAL_STATE_ORDER: TerminalVisualState[] = [
|
|
18
37
|
"disabled", "readonly", "loading", "empty", "muted", "error", "warning", "success", "invalid", "valid", "placeholder", "selection", "selected", "current", "expanded", "collapsed", "checked", "unchecked", "indeterminate", "editing", "submitted", "dragging", "dropTarget", "capturing", "focus", "hover", "pressed"
|
|
19
38
|
];
|
|
@@ -108,6 +127,20 @@ function styleSpan(kind: string, style?: TerminalStyleDefinition): ResolvedStyle
|
|
|
108
127
|
return typeof style === "undefined" ? { kind } : { kind, style };
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
const BASE_STYLE_KIND_BY_TAG: Partial<Record<TerminalElementNode["tag"], string>> = {
|
|
131
|
+
"terminal-button": "button.base",
|
|
132
|
+
"terminal-input": "input.base",
|
|
133
|
+
"terminal-editor": "editor.base",
|
|
134
|
+
"terminal-list": "list.base",
|
|
135
|
+
"terminal-scroll": "scroll.base",
|
|
136
|
+
"terminal-log-view": "log.base",
|
|
137
|
+
"terminal-overlay": "overlay.base"
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function baseStyleKindForNode(node: TerminalElementNode) {
|
|
141
|
+
return BASE_STYLE_KIND_BY_TAG[node.tag];
|
|
142
|
+
}
|
|
143
|
+
|
|
111
144
|
function nodeStates(node: TerminalElementNode) {
|
|
112
145
|
const declared = Array.isArray(node.props.state) ? node.props.state : typeof node.props.state === "string" ? [node.props.state] : [];
|
|
113
146
|
const states = new Set<TerminalVisualState>();
|
|
@@ -127,7 +160,7 @@ function resolveLayoutStyle(baseKind: string, node: TerminalElementNode, context
|
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
function resolveNodeLayoutStyle(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
130
|
-
return resolveTerminalStyle(node.props.style, context?.theme);
|
|
163
|
+
return mergeStyleDefinitions(resolveTerminalStyle(baseStyleKindForNode(node), context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
|
|
131
164
|
}
|
|
132
165
|
|
|
133
166
|
function decoratedControlFrame(content: string[], style: TerminalStyleDefinition | undefined) {
|
|
@@ -146,11 +179,17 @@ function fullFrameSpans(kinds: string[], width: number, height: number): Termina
|
|
|
146
179
|
return spans;
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
function resolveNodeStyle(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
182
|
+
function resolveNodeStyle(node: TerminalElementNode, context?: TerminalRenderContext, options: { includeBase?: boolean } = {}) {
|
|
183
|
+
const baseKind = options.includeBase === false ? undefined : baseStyleKindForNode(node);
|
|
184
|
+
let style = resolveTerminalStyle(baseKind, context?.theme);
|
|
185
|
+
const spanKinds: ResolvedStyleSpan[] = typeof baseKind === "undefined" ? [] : [styleSpan(baseKind)];
|
|
186
|
+
const explicitStyle = resolveTerminalStyle(node.props.style, context?.theme);
|
|
187
|
+
if (typeof node.props.style === "string") {
|
|
188
|
+
spanKinds.push(styleSpan(node.props.style));
|
|
189
|
+
} else if (explicitStyle) {
|
|
190
|
+
spanKinds.push(styleSpan("#style", explicitStyle));
|
|
191
|
+
}
|
|
192
|
+
style = mergeStyleDefinitions(style, explicitStyle);
|
|
154
193
|
for (const state of nodeStates(node)) {
|
|
155
194
|
const stateStyle = node.props.styles?.[state];
|
|
156
195
|
if (stateStyle) {
|
|
@@ -219,9 +258,22 @@ function addFullFrameSpans(frame: TerminalFrame, kinds: ResolvedStyleSpan[]) {
|
|
|
219
258
|
return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
|
|
220
259
|
}
|
|
221
260
|
|
|
222
|
-
function
|
|
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) {
|
|
223
275
|
if (!node.props.virtualized) {
|
|
224
|
-
return { start: 0, end: itemCount };
|
|
276
|
+
return { start: 0, end: itemCount, visibleStart: 0, viewportRows: itemCount || 1 };
|
|
225
277
|
}
|
|
226
278
|
|
|
227
279
|
if (typeof node.props.itemHeight !== "undefined" && node.props.itemHeight !== 1) {
|
|
@@ -229,14 +281,95 @@ function listVirtualRange(node: TerminalElementNode, itemCount: number, selected
|
|
|
229
281
|
}
|
|
230
282
|
|
|
231
283
|
const overscan = nonNegativeInteger(node.props.overscan, "List overscan");
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
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")));
|
|
236
287
|
const start = Math.max(0, visibleStart - overscan);
|
|
237
288
|
const end = Math.min(itemCount, visibleStart + viewportRows + overscan);
|
|
238
289
|
|
|
239
|
-
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;
|
|
240
373
|
}
|
|
241
374
|
|
|
242
375
|
function fixedPosition(value: unknown) {
|
|
@@ -745,18 +878,69 @@ function renderStandaloneFixedFrame(node: TerminalElementNode, context?: Termina
|
|
|
745
878
|
: constrainFrame(frame, { width: size });
|
|
746
879
|
}
|
|
747
880
|
|
|
748
|
-
function
|
|
881
|
+
function overlayMarginValue(value: unknown, axisSize: number, label: string) {
|
|
882
|
+
if (typeof value === "number") {
|
|
883
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
884
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
885
|
+
}
|
|
886
|
+
return value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (typeof value === "string") {
|
|
890
|
+
const match = value.match(/^(\d+(?:\.\d+)?)%$/);
|
|
891
|
+
if (!match) {
|
|
892
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const percent = Number(match[1]);
|
|
896
|
+
if (!Number.isFinite(percent) || percent < 0) {
|
|
897
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return Math.round(axisSize * percent / 100);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function overlayMargins(margin: unknown, width: number, height: number) {
|
|
907
|
+
if (typeof margin === "number" || typeof margin === "string") {
|
|
908
|
+
const x = overlayMarginValue(margin, width, "Overlay margin");
|
|
909
|
+
const y = overlayMarginValue(margin, height, "Overlay margin");
|
|
910
|
+
return { x, y };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (margin && typeof margin === "object" && !Array.isArray(margin)) {
|
|
914
|
+
const axes = margin as { x?: unknown; y?: unknown };
|
|
915
|
+
return {
|
|
916
|
+
x: overlayMarginValue(axes.x, width, "Overlay margin x"),
|
|
917
|
+
y: overlayMarginValue(axes.y, height, "Overlay margin y")
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
throw new RangeError("Overlay margin is required");
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function overlayGeometry(node: TerminalElementNode, width: number, height: number) {
|
|
925
|
+
const margin = overlayMargins(node.props.margin, width, height);
|
|
926
|
+
const overlayWidth = width - margin.x * 2;
|
|
927
|
+
const overlayHeight = height - margin.y * 2;
|
|
928
|
+
|
|
929
|
+
if (overlayWidth < 1 || overlayHeight < 1) {
|
|
930
|
+
throw new RangeError("Overlay margin leaves no renderable area");
|
|
931
|
+
}
|
|
932
|
+
|
|
749
933
|
return {
|
|
750
|
-
x:
|
|
751
|
-
y:
|
|
752
|
-
width:
|
|
753
|
-
height:
|
|
934
|
+
x: margin.x + 1,
|
|
935
|
+
y: margin.y + 1,
|
|
936
|
+
width: overlayWidth,
|
|
937
|
+
height: overlayHeight
|
|
754
938
|
};
|
|
755
939
|
}
|
|
756
940
|
|
|
757
|
-
function renderOverlayChildFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
758
|
-
const geometry = overlayGeometry(node);
|
|
759
|
-
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height }), { width: geometry.width, height: geometry.height });
|
|
941
|
+
function renderOverlayChildFrame(node: TerminalElementNode, width: number, height: number, context?: TerminalRenderContext) {
|
|
942
|
+
const geometry = overlayGeometry(node, width, height);
|
|
943
|
+
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height });
|
|
760
944
|
frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
|
|
761
945
|
if (node.props.id && isFocusable(node)) {
|
|
762
946
|
frame = addFocusableHitbox(frame, node as TerminalFocusNode);
|
|
@@ -764,10 +948,16 @@ function renderOverlayChildFrame(node: TerminalElementNode, context?: TerminalRe
|
|
|
764
948
|
return { frame, geometry };
|
|
765
949
|
}
|
|
766
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
|
+
|
|
767
955
|
function applyDirectOverlays(base: TerminalFrame, overlays: TerminalElementNode[], context?: TerminalRenderContext) {
|
|
768
956
|
let frame = base;
|
|
769
|
-
|
|
770
|
-
|
|
957
|
+
const width = Math.max(1, getFrameWidth(base));
|
|
958
|
+
const height = Math.max(1, getFrameHeight(base));
|
|
959
|
+
for (const overlay of orderedDirectOverlays(overlays)) {
|
|
960
|
+
const rendered = renderOverlayChildFrame(overlay, width, height, context);
|
|
771
961
|
frame = overlayFrame(frame, rendered.frame, rendered.geometry);
|
|
772
962
|
}
|
|
773
963
|
return frame;
|
|
@@ -829,8 +1019,12 @@ function renderPaneFrame(node: TerminalElementNode, context?: TerminalRenderCont
|
|
|
829
1019
|
return overlays.length ? applyDirectOverlays(frame, overlays, context) : frame;
|
|
830
1020
|
}
|
|
831
1021
|
|
|
832
|
-
function renderStandaloneOverlayFrame(node: TerminalElementNode) {
|
|
833
|
-
|
|
1022
|
+
function renderStandaloneOverlayFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
1023
|
+
if (!context) {
|
|
1024
|
+
throw new RangeError("Standalone Overlay requires exact render context dimensions");
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const rendered = renderOverlayChildFrame(node, context.cols, context.rows, context);
|
|
834
1028
|
return rendered.frame;
|
|
835
1029
|
}
|
|
836
1030
|
|
|
@@ -983,46 +1177,119 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
983
1177
|
case "terminal-fixed":
|
|
984
1178
|
return renderStandaloneFixedFrame(node, context);
|
|
985
1179
|
case "terminal-overlay":
|
|
986
|
-
return renderStandaloneOverlayFrame(node);
|
|
1180
|
+
return renderStandaloneOverlayFrame(node, context);
|
|
987
1181
|
case "terminal-log-view":
|
|
988
1182
|
return renderLogViewFrame(node, context);
|
|
989
1183
|
case "terminal-list": {
|
|
990
1184
|
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
991
|
-
const
|
|
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;
|
|
992
1187
|
const hoveredIndex = typeof node.props.__hoveredIndex === "number" ? Number(node.props.__hoveredIndex) : -1;
|
|
993
|
-
const range = listVirtualRange(node, items.length,
|
|
994
|
-
const lines: string[] = [];
|
|
995
|
-
for (let index = range.start; index < range.end; index += 1) {
|
|
996
|
-
const item = items[index];
|
|
997
|
-
const label = typeof node.props.renderItem === "function" ? plainText(node.props.renderItem(item, index)) : plainText(item);
|
|
998
|
-
lines.push(label);
|
|
999
|
-
}
|
|
1000
|
-
const visibleLines = lines.length ? lines : [""];
|
|
1188
|
+
const range = listVirtualRange(node, items.length, context);
|
|
1001
1189
|
const layoutStyle = resolveLayoutStyle("list.base", node, context);
|
|
1002
1190
|
const padding = normalizeSpacing(layoutStyle.padding, "List padding");
|
|
1003
1191
|
const border = normalizeBorder(layoutStyle.border);
|
|
1004
|
-
const
|
|
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);
|
|
1005
1237
|
const width = Math.max(1, getFrameWidth(decorated));
|
|
1006
1238
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1007
1239
|
const itemY = 1 + (border.top ? 1 : 0) + padding.top;
|
|
1008
1240
|
const spans: TerminalStyleSpan[] = [];
|
|
1009
|
-
for (let index = 0; index <
|
|
1010
|
-
const sourceIndex =
|
|
1241
|
+
for (let index = 0; index < frameLines.length; index += 1) {
|
|
1242
|
+
const sourceIndex = frameItemIndexes[index];
|
|
1011
1243
|
const y = itemY + index;
|
|
1012
1244
|
spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
|
|
1013
|
-
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) {
|
|
1014
1249
|
spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
|
|
1015
1250
|
}
|
|
1016
1251
|
if (sourceIndex === hoveredIndex) {
|
|
1017
1252
|
spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
|
|
1018
1253
|
}
|
|
1019
1254
|
}
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
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
|
+
}
|
|
1024
1289
|
}
|
|
1025
|
-
|
|
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;
|
|
1026
1293
|
}
|
|
1027
1294
|
case "terminal-table":
|
|
1028
1295
|
return renderTableFrame(node, context);
|
|
@@ -1039,7 +1306,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1039
1306
|
case "terminal-text": {
|
|
1040
1307
|
const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : plainText(node.children.map(textContent).join(""));
|
|
1041
1308
|
const frame = createFrame(value.split("\n"));
|
|
1042
|
-
return addFullFrameSpans(frame, resolveNodeStyle(node, context).spanKinds);
|
|
1309
|
+
return addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1043
1310
|
}
|
|
1044
1311
|
case "terminal-input": {
|
|
1045
1312
|
const value = typeof node.props.value !== "undefined" ? node.props.value : node.props.placeholder || "";
|
|
@@ -1058,7 +1325,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1058
1325
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1059
1326
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
|
|
1060
1327
|
const spans = fullFrameSpans(["input.base"], width, height);
|
|
1061
|
-
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
|
|
1328
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1062
1329
|
}
|
|
1063
1330
|
|
|
1064
1331
|
const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length }, inputPadding);
|
|
@@ -1067,7 +1334,7 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1067
1334
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1068
1335
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
|
|
1069
1336
|
const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
|
|
1070
|
-
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
|
|
1337
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1071
1338
|
}
|
|
1072
1339
|
case "terminal-editor":
|
|
1073
1340
|
return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
|
|
@@ -1077,10 +1344,10 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1077
1344
|
const decorated = decoratedControlFrame([String(label)], layoutStyle);
|
|
1078
1345
|
const width = Math.max(1, getFrameWidth(decorated));
|
|
1079
1346
|
const height = Math.max(1, getFrameHeight(decorated));
|
|
1080
|
-
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 }] : [];
|
|
1081
1348
|
const kinds = ["button.base", ...nodeStates(node).map((state) => `button.${state}`)];
|
|
1082
1349
|
const spans = fullFrameSpans(kinds, width, height);
|
|
1083
|
-
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context).spanKinds);
|
|
1350
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1084
1351
|
}
|
|
1085
1352
|
default:
|
|
1086
1353
|
return mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
|
|
@@ -1098,9 +1365,10 @@ export function renderTerminalNode(node: TerminalNode, context?: TerminalRenderC
|
|
|
1098
1365
|
return renderTerminalFrame(node, context).lines.join("\n");
|
|
1099
1366
|
}
|
|
1100
1367
|
|
|
1101
|
-
export function renderTerminal(input: any): string {
|
|
1368
|
+
export function renderTerminal(input: any, context?: TerminalRenderContext): string {
|
|
1369
|
+
const renderContext = validateRenderContext(context);
|
|
1102
1370
|
return renderValyrianTerminal(input)
|
|
1103
|
-
.map((node) => renderTerminalNode(node))
|
|
1371
|
+
.map((node) => renderTerminalNode(node, renderContext))
|
|
1104
1372
|
.filter(Boolean)
|
|
1105
1373
|
.join("\n")
|
|
1106
1374
|
.trimEnd();
|
package/src/runtime.ts
CHANGED
|
@@ -61,9 +61,6 @@ function hasEventListener(node: DomNode, eventName: string) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function resolveDispatchEventName(node: DomNode, eventName: string) {
|
|
64
|
-
if (eventName === "press" && !hasEventListener(node, "press") && hasEventListener(node, "click")) {
|
|
65
|
-
return "click";
|
|
66
|
-
}
|
|
67
64
|
if (eventName === "change" && !hasEventListener(node, "change") && hasEventListener(node, "input")) {
|
|
68
65
|
return "input";
|
|
69
66
|
}
|
|
@@ -147,20 +144,25 @@ function normalizeValyrianInput(input: any): any {
|
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
const props = { ...((input.props && input.props.__terminalProps) || input.props || {}) };
|
|
150
|
-
const
|
|
151
|
-
|
|
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) => {
|
|
152
156
|
if (props["v-for"] && typeof child === "function") {
|
|
153
157
|
return (...args: any[]) => normalizeValyrianInput(child(...args));
|
|
154
158
|
}
|
|
155
159
|
return normalizeValyrianInput(child);
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
});
|
|
161
|
+
if (tag === "terminal-list" && rawChildren.length === 1 && typeof rawChildren[0] === "function") {
|
|
162
|
+
props.__childrenRenderer = rawChildren[0];
|
|
163
|
+
}
|
|
159
164
|
if (typeof tag === "string" && isTerminalTag(tag)) {
|
|
160
165
|
const terminalProps = { ...props };
|
|
161
|
-
if (tag === "terminal-button" && typeof props.action === "function" && typeof props.onpress !== "function" && typeof props.onclick !== "function") {
|
|
162
|
-
props.onclick = props.action;
|
|
163
|
-
}
|
|
164
166
|
if (typeof props.style === "object") {
|
|
165
167
|
Reflect.deleteProperty(props, "style");
|
|
166
168
|
}
|