@valyrianjs/terminal 0.1.1 → 0.2.0
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/README.md +105 -55
- package/dist/ansi.d.ts +20 -4
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +171 -47
- package/dist/ansi.js.map +1 -1
- package/dist/editor-state.d.ts +22 -0
- package/dist/editor-state.d.ts.map +1 -0
- package/dist/editor-state.js +110 -0
- package/dist/editor-state.js.map +1 -0
- package/dist/events.d.ts +1 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +15 -38
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keymap.d.ts +7 -0
- package/dist/keymap.d.ts.map +1 -0
- package/dist/keymap.js +133 -0
- package/dist/keymap.js.map +1 -0
- package/dist/layout.d.ts +10 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +97 -7
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +1 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +24 -1
- package/dist/mouse.js.map +1 -1
- package/dist/output-writer.d.ts +9 -0
- package/dist/output-writer.d.ts.map +1 -0
- package/dist/output-writer.js +79 -0
- package/dist/output-writer.js.map +1 -0
- package/dist/paste.d.ts +7 -0
- package/dist/paste.d.ts.map +1 -0
- package/dist/paste.js +18 -0
- package/dist/paste.js.map +1 -0
- package/dist/primitives.d.ts +15 -3
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +9 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts +9 -4
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +923 -68
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +209 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +24 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +858 -199
- package/dist/session.js.map +1 -1
- package/dist/stream-log.d.ts +40 -0
- package/dist/stream-log.d.ts.map +1 -0
- package/dist/stream-log.js +73 -0
- package/dist/stream-log.js.map +1 -0
- package/dist/text.d.ts +3 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +19 -0
- package/dist/text.js.map +1 -0
- package/dist/theme.d.ts +7 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +254 -0
- package/dist/theme.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +42 -1
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +203 -24
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +313 -142
- package/docs/assets/quick-note.svg +13 -0
- package/docs/cookbook.md +296 -201
- package/docs/core-concepts.md +143 -55
- package/docs/getting-started.md +209 -90
- package/docs/interaction-model.md +98 -54
- package/docs/primitive-gallery.md +370 -0
- package/docs/session-runtime.md +131 -362
- package/docs/valyrian-modules.md +3196 -0
- package/llms-full.txt +5377 -0
- package/package.json +21 -8
- package/src/ansi.ts +269 -0
- package/src/clipboard.ts +76 -0
- package/src/editor-state.ts +162 -0
- package/src/events.ts +163 -0
- package/src/index.ts +95 -0
- package/src/keymap.ts +151 -0
- package/src/layout.ts +282 -0
- package/src/mouse.ts +68 -0
- package/src/output-writer.ts +93 -0
- package/src/paste.ts +23 -0
- package/src/primitives.ts +55 -0
- package/src/render.ts +1204 -0
- package/src/runtime.ts +267 -0
- package/src/scheduler.ts +33 -0
- package/src/session.ts +1408 -0
- package/src/stream-log.ts +96 -0
- package/src/text.ts +20 -0
- package/src/theme.ts +263 -0
- package/src/tree.ts +169 -0
- package/src/types.ts +541 -0
- package/tsconfig.json +4 -7
- package/docs/local-demo.md +0 -28
package/dist/render.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constrainFrame, createFrame, cropFrame, fitFrame, getFrameHeight, getFrameWidth, mergeHorizontal, mergeVertical, overlayFrame, shiftFrame } from "./layout.js";
|
|
2
2
|
import { getSelectionRange, normalizeInputState } from "./events.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createEditorState } from "./editor-state.js";
|
|
4
|
+
import { isFocusable, textContent } from "./tree.js";
|
|
5
|
+
import { renderValyrianTerminal } from "./runtime.js";
|
|
6
|
+
import { plainText } from "./text.js";
|
|
7
|
+
import { resolveTerminalStyle } from "./theme.js";
|
|
8
|
+
function validateRenderContextDimension(name, value) {
|
|
9
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
10
|
+
throw new RangeError(`Invalid render context ${name}: expected an integer >= 1`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function validateRenderContext(context) {
|
|
15
|
+
if (typeof context === "undefined") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
cols: validateRenderContextDimension("cols", context.cols),
|
|
20
|
+
rows: validateRenderContextDimension("rows", context.rows),
|
|
21
|
+
theme: context.theme
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const VISUAL_STATE_ORDER = [
|
|
25
|
+
"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"
|
|
26
|
+
];
|
|
4
27
|
function numeric(value, fallback = 0) {
|
|
5
28
|
const result = Number(value);
|
|
6
29
|
return Number.isFinite(result) ? result : fallback;
|
|
@@ -14,35 +37,412 @@ function addFocusableHitbox(frame, node) {
|
|
|
14
37
|
frame.hitboxes.unshift({ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height });
|
|
15
38
|
return frame;
|
|
16
39
|
}
|
|
17
|
-
function
|
|
40
|
+
function positiveDimension(value, prop) {
|
|
41
|
+
if (typeof value === "undefined") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const size = Number(value);
|
|
45
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
46
|
+
throw new RangeError(`${prop} must be greater than zero`);
|
|
47
|
+
}
|
|
48
|
+
return size;
|
|
49
|
+
}
|
|
50
|
+
function positiveInteger(value, prop) {
|
|
51
|
+
const size = Number(value);
|
|
52
|
+
if (!Number.isFinite(size) || !Number.isInteger(size) || size <= 0) {
|
|
53
|
+
throw new RangeError(`${prop} must be a positive finite integer`);
|
|
54
|
+
}
|
|
55
|
+
return size;
|
|
56
|
+
}
|
|
57
|
+
function nonNegativeInteger(value, prop) {
|
|
58
|
+
const size = typeof value === "undefined" ? 0 : Number(value);
|
|
59
|
+
if (!Number.isFinite(size) || !Number.isInteger(size) || size < 0) {
|
|
60
|
+
throw new RangeError(`${prop} must be a non-negative finite integer`);
|
|
61
|
+
}
|
|
62
|
+
return size;
|
|
63
|
+
}
|
|
64
|
+
function normalizeSpacing(value, prop) {
|
|
65
|
+
if (typeof value === "undefined")
|
|
66
|
+
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
67
|
+
if (typeof value === "number") {
|
|
68
|
+
const size = nonNegativeInteger(value, prop);
|
|
69
|
+
return { top: size, right: size, bottom: size, left: size };
|
|
70
|
+
}
|
|
71
|
+
const x = nonNegativeInteger(value.x, `${prop}.x`);
|
|
72
|
+
const y = nonNegativeInteger(value.y, `${prop}.y`);
|
|
73
|
+
return {
|
|
74
|
+
top: typeof value.top === "undefined" ? y : nonNegativeInteger(value.top, `${prop}.top`),
|
|
75
|
+
right: typeof value.right === "undefined" ? x : nonNegativeInteger(value.right, `${prop}.right`),
|
|
76
|
+
bottom: typeof value.bottom === "undefined" ? y : nonNegativeInteger(value.bottom, `${prop}.bottom`),
|
|
77
|
+
left: typeof value.left === "undefined" ? x : nonNegativeInteger(value.left, `${prop}.left`)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function normalizeBorder(value) {
|
|
81
|
+
if (value === true)
|
|
82
|
+
return { top: true, right: true, bottom: true, left: true, style: "solid" };
|
|
83
|
+
if (typeof value === "string")
|
|
84
|
+
return { top: true, right: true, bottom: true, left: true, style: value };
|
|
85
|
+
if (value && typeof value === "object") {
|
|
86
|
+
return { top: value.top === true, right: value.right === true, bottom: value.bottom === true, left: value.left === true, style: value.style || "solid", color: value.color };
|
|
87
|
+
}
|
|
88
|
+
return { top: false, right: false, bottom: false, left: false, style: "solid" };
|
|
89
|
+
}
|
|
90
|
+
function borderChars(style) {
|
|
91
|
+
if (style === "solid")
|
|
92
|
+
return { topLeft: "┌", topRight: "┐", bottomLeft: "└", bottomRight: "┘", horizontal: "─", vertical: "│" };
|
|
93
|
+
if (style === "dotted")
|
|
94
|
+
return { topLeft: "+", topRight: "+", bottomLeft: "+", bottomRight: "+", horizontal: "·", vertical: "⋮" };
|
|
95
|
+
if (style === "double")
|
|
96
|
+
return { topLeft: "╔", topRight: "╗", bottomLeft: "╚", bottomRight: "╝", horizontal: "═", vertical: "║" };
|
|
97
|
+
throw new RangeError(`Unknown terminal border style: ${style}`);
|
|
98
|
+
}
|
|
99
|
+
function mergeStyleDefinitions(base, next) {
|
|
100
|
+
return { ...base, ...next };
|
|
101
|
+
}
|
|
102
|
+
function styleSpan(kind, style) {
|
|
103
|
+
return typeof style === "undefined" ? { kind } : { kind, style };
|
|
104
|
+
}
|
|
105
|
+
const BASE_STYLE_KIND_BY_TAG = {
|
|
106
|
+
"terminal-button": "button.base",
|
|
107
|
+
"terminal-input": "input.base",
|
|
108
|
+
"terminal-editor": "editor.base",
|
|
109
|
+
"terminal-list": "list.base",
|
|
110
|
+
"terminal-scroll": "scroll.base",
|
|
111
|
+
"terminal-log-view": "log.base",
|
|
112
|
+
"terminal-overlay": "overlay.base"
|
|
113
|
+
};
|
|
114
|
+
function baseStyleKindForNode(node) {
|
|
115
|
+
return BASE_STYLE_KIND_BY_TAG[node.tag];
|
|
116
|
+
}
|
|
117
|
+
function nodeStates(node) {
|
|
118
|
+
const declared = Array.isArray(node.props.state) ? node.props.state : typeof node.props.state === "string" ? [node.props.state] : [];
|
|
119
|
+
const states = new Set();
|
|
120
|
+
for (const state of declared)
|
|
121
|
+
states.add(state);
|
|
122
|
+
if (node.props.disabled === true)
|
|
123
|
+
states.add("disabled");
|
|
124
|
+
if (node.props.pressed === true)
|
|
125
|
+
states.add("pressed");
|
|
126
|
+
if (node.props.__focused === true)
|
|
127
|
+
states.add("focus");
|
|
128
|
+
if (typeof node.props.__hoveredIndex === "number" || typeof node.props.__hoveredRow === "number")
|
|
129
|
+
states.add("hover");
|
|
130
|
+
if (Array.isArray(node.props.items) && node.props.items.length === 0)
|
|
131
|
+
states.add("empty");
|
|
132
|
+
if (Array.isArray(node.props.entries) && node.props.entries.length === 0)
|
|
133
|
+
states.add("empty");
|
|
134
|
+
return VISUAL_STATE_ORDER.filter((state) => states.has(state));
|
|
135
|
+
}
|
|
136
|
+
function resolveLayoutStyle(baseKind, node, context) {
|
|
137
|
+
return mergeStyleDefinitions(resolveTerminalStyle(baseKind, context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
|
|
138
|
+
}
|
|
139
|
+
function resolveNodeLayoutStyle(node, context) {
|
|
140
|
+
return mergeStyleDefinitions(resolveTerminalStyle(baseStyleKindForNode(node), context?.theme), resolveTerminalStyle(node.props.style, context?.theme));
|
|
141
|
+
}
|
|
142
|
+
function decoratedControlFrame(content, style) {
|
|
143
|
+
const padding = normalizeSpacing(style?.padding, "Control padding");
|
|
144
|
+
const border = normalizeBorder(style?.border);
|
|
145
|
+
return addBorder(padFrameSides(createFrame(content), padding), border);
|
|
146
|
+
}
|
|
147
|
+
function fullFrameSpans(kinds, width, height) {
|
|
148
|
+
const spans = [];
|
|
149
|
+
for (const kind of kinds) {
|
|
150
|
+
for (let y = 1; y <= height; y += 1) {
|
|
151
|
+
spans.push({ kind, x1: 1, x2: width + 1, y });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return spans;
|
|
155
|
+
}
|
|
156
|
+
function resolveNodeStyle(node, context, options = {}) {
|
|
157
|
+
const baseKind = options.includeBase === false ? undefined : baseStyleKindForNode(node);
|
|
158
|
+
let style = resolveTerminalStyle(baseKind, context?.theme);
|
|
159
|
+
const spanKinds = typeof baseKind === "undefined" ? [] : [styleSpan(baseKind)];
|
|
160
|
+
const explicitStyle = resolveTerminalStyle(node.props.style, context?.theme);
|
|
161
|
+
if (typeof node.props.style === "string") {
|
|
162
|
+
spanKinds.push(styleSpan(node.props.style));
|
|
163
|
+
}
|
|
164
|
+
else if (explicitStyle) {
|
|
165
|
+
spanKinds.push(styleSpan("#style", explicitStyle));
|
|
166
|
+
}
|
|
167
|
+
style = mergeStyleDefinitions(style, explicitStyle);
|
|
168
|
+
for (const state of nodeStates(node)) {
|
|
169
|
+
const stateStyle = node.props.styles?.[state];
|
|
170
|
+
if (stateStyle) {
|
|
171
|
+
const resolvedStateStyle = resolveTerminalStyle(stateStyle, context?.theme);
|
|
172
|
+
style = mergeStyleDefinitions(style, resolvedStateStyle);
|
|
173
|
+
spanKinds.push(typeof stateStyle === "string" ? styleSpan(stateStyle) : styleSpan("#style", resolvedStateStyle));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { style, spanKinds };
|
|
177
|
+
}
|
|
178
|
+
function padFrameSides(frame, padding) {
|
|
179
|
+
if (!padding.top && !padding.right && !padding.bottom && !padding.left)
|
|
180
|
+
return frame;
|
|
181
|
+
const width = Math.max(1, getFrameWidth(frame));
|
|
182
|
+
const contentWidth = width + padding.left + padding.right;
|
|
183
|
+
const topLines = new Array(padding.top).fill(" ".repeat(contentWidth));
|
|
184
|
+
const bottomLines = new Array(padding.bottom).fill(" ".repeat(contentWidth));
|
|
185
|
+
const lines = [
|
|
186
|
+
...topLines,
|
|
187
|
+
...frame.lines.map((line) => `${" ".repeat(padding.left)}${line.padEnd(width, " ")}${" ".repeat(padding.right)}`),
|
|
188
|
+
...bottomLines
|
|
189
|
+
];
|
|
190
|
+
return shiftFrame(createFrame(lines, frame.hitboxes, frame.cursor, frame.spans), padding.left, padding.top);
|
|
191
|
+
}
|
|
192
|
+
function addBorder(frame, border) {
|
|
193
|
+
if (!border.top && !border.right && !border.bottom && !border.left)
|
|
194
|
+
return frame;
|
|
195
|
+
const chars = borderChars(border.style);
|
|
196
|
+
const innerWidth = getFrameWidth(frame);
|
|
197
|
+
const left = border.left ? 1 : 0;
|
|
198
|
+
const right = border.right ? 1 : 0;
|
|
199
|
+
const width = innerWidth + left + right;
|
|
200
|
+
const lines = [];
|
|
201
|
+
if (border.top) {
|
|
202
|
+
lines.push(`${border.left ? chars.topLeft : chars.horizontal}${chars.horizontal.repeat(Math.max(0, width - left - right))}${border.right ? chars.topRight : chars.horizontal}`.slice(0, width));
|
|
203
|
+
}
|
|
204
|
+
for (const line of frame.lines) {
|
|
205
|
+
lines.push(`${border.left ? chars.vertical : ""}${line.padEnd(innerWidth, " ")}${border.right ? chars.vertical : ""}`);
|
|
206
|
+
}
|
|
207
|
+
if (border.bottom) {
|
|
208
|
+
lines.push(`${border.left ? chars.bottomLeft : chars.horizontal}${chars.horizontal.repeat(Math.max(0, width - left - right))}${border.right ? chars.bottomRight : chars.horizontal}`.slice(0, width));
|
|
209
|
+
}
|
|
210
|
+
return shiftFrame(createFrame(lines, frame.hitboxes, frame.cursor, frame.spans), left, border.top ? 1 : 0);
|
|
211
|
+
}
|
|
212
|
+
function containerDecorationSize(node, context) {
|
|
213
|
+
const layoutStyle = resolveNodeLayoutStyle(node, context);
|
|
214
|
+
const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, `${node.tag} padding`);
|
|
215
|
+
const border = normalizeBorder(layoutStyle?.border);
|
|
216
|
+
return {
|
|
217
|
+
horizontal: padding.left + padding.right + (border.left ? 1 : 0) + (border.right ? 1 : 0),
|
|
218
|
+
vertical: padding.top + padding.bottom + (border.top ? 1 : 0) + (border.bottom ? 1 : 0)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function addFullFrameSpans(frame, kinds) {
|
|
222
|
+
const width = Math.max(1, getFrameWidth(frame));
|
|
223
|
+
const height = getFrameHeight(frame);
|
|
224
|
+
if (width <= 0 || height <= 0 || kinds.length === 0)
|
|
225
|
+
return frame;
|
|
226
|
+
const spans = frame.spans.slice();
|
|
227
|
+
for (const span of kinds) {
|
|
228
|
+
for (let y = 1; y <= height; y += 1) {
|
|
229
|
+
spans.push({ ...span, x1: 1, x2: width + 1, y });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
|
|
233
|
+
}
|
|
234
|
+
function listVirtualRange(node, itemCount, selectedIndex, context) {
|
|
235
|
+
if (!node.props.virtualized) {
|
|
236
|
+
return { start: 0, end: itemCount };
|
|
237
|
+
}
|
|
238
|
+
if (typeof node.props.itemHeight !== "undefined" && node.props.itemHeight !== 1) {
|
|
239
|
+
throw new RangeError("List itemHeight must be 1");
|
|
240
|
+
}
|
|
241
|
+
const overscan = nonNegativeInteger(node.props.overscan, "List overscan");
|
|
242
|
+
const viewportSourceRows = context?.rows ?? (itemCount || 1);
|
|
243
|
+
const viewportRows = Math.max(1, Math.min(itemCount || 1, positiveInteger(viewportSourceRows, "List viewport height")));
|
|
244
|
+
const selected = Math.max(0, Math.min(itemCount - 1, selectedIndex));
|
|
245
|
+
const visibleStart = Math.max(0, Math.min(selected, selected - viewportRows + 1));
|
|
246
|
+
const start = Math.max(0, visibleStart - overscan);
|
|
247
|
+
const end = Math.min(itemCount, visibleStart + viewportRows + overscan);
|
|
248
|
+
return { start, end };
|
|
249
|
+
}
|
|
250
|
+
function fixedPosition(value) {
|
|
251
|
+
if (value === "top" || value === "bottom" || value === "left" || value === "right") {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
throw new RangeError("Fixed position must be top, bottom, left, or right");
|
|
255
|
+
}
|
|
256
|
+
function fillContextDimension(context, prop, label) {
|
|
257
|
+
if (!context) {
|
|
258
|
+
throw new RangeError(`${label} fill requires render context to resolve ${prop}`);
|
|
259
|
+
}
|
|
260
|
+
return positiveInteger(prop === "width" ? context.cols : context.rows, `${label} ${prop}`);
|
|
261
|
+
}
|
|
262
|
+
function resolveLayoutDimension(node, prop, label, context) {
|
|
263
|
+
const explicit = positiveDimension(node.props[prop], prop);
|
|
264
|
+
if (typeof explicit !== "undefined") {
|
|
265
|
+
return explicit;
|
|
266
|
+
}
|
|
267
|
+
if (node.props.fill === true) {
|
|
268
|
+
return fillContextDimension(context, prop, label);
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
function resolveLayoutDimensions(node, label, context) {
|
|
273
|
+
return {
|
|
274
|
+
width: resolveLayoutDimension(node, "width", label, context),
|
|
275
|
+
height: resolveLayoutDimension(node, "height", label, context)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function contextBackedDimension(context, prop, label) {
|
|
279
|
+
if (!context) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
return positiveInteger(prop === "width" ? context.cols : context.rows, `${label} ${prop}`);
|
|
283
|
+
}
|
|
284
|
+
function resolveBlockLayoutDimensions(node, label, context, options = {}) {
|
|
285
|
+
const explicitWidth = positiveDimension(node.props.width, "width");
|
|
286
|
+
const explicitHeight = positiveDimension(node.props.height, "height");
|
|
287
|
+
return {
|
|
288
|
+
width: typeof explicitWidth !== "undefined"
|
|
289
|
+
? explicitWidth
|
|
290
|
+
: node.props.fill === true
|
|
291
|
+
? fillContextDimension(context, "width", label)
|
|
292
|
+
: contextBackedDimension(context, "width", label),
|
|
293
|
+
height: typeof explicitHeight !== "undefined"
|
|
294
|
+
? explicitHeight
|
|
295
|
+
: node.props.fill === true
|
|
296
|
+
? fillContextDimension(context, "height", label)
|
|
297
|
+
: options.exactHeightFromContext === true
|
|
298
|
+
? contextBackedDimension(context, "height", label)
|
|
299
|
+
: undefined
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function resolveSplitDimension(node, prop, context) {
|
|
303
|
+
const explicit = positiveDimension(node.props[prop], prop);
|
|
304
|
+
if (typeof explicit !== "undefined") {
|
|
305
|
+
return explicit;
|
|
306
|
+
}
|
|
307
|
+
if (node.props.fill === true) {
|
|
308
|
+
return fillContextDimension(context, prop, "Split");
|
|
309
|
+
}
|
|
310
|
+
const fromContext = contextBackedDimension(context, prop, "Split");
|
|
311
|
+
if (typeof fromContext !== "undefined") {
|
|
312
|
+
return fromContext;
|
|
313
|
+
}
|
|
314
|
+
throw new RangeError("Split requires width/height or render context");
|
|
315
|
+
}
|
|
316
|
+
function decorateContainerFrame(frame, node, options = {}, context) {
|
|
18
317
|
let next = frame;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
318
|
+
const resolved = resolveNodeStyle(node, context);
|
|
319
|
+
const layoutStyle = resolveNodeLayoutStyle(node, context);
|
|
320
|
+
const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, `${node.tag} padding`);
|
|
321
|
+
const border = normalizeBorder(layoutStyle?.border);
|
|
322
|
+
const decoration = containerDecorationSize(node, context);
|
|
323
|
+
if (!options.constrain) {
|
|
324
|
+
next = fitFrame(next, numeric(options.width ?? node.props.width, 0), numeric(options.height ?? node.props.height, 0));
|
|
325
|
+
}
|
|
326
|
+
else if (node.tag === "terminal-pane" && (typeof node.props.width !== "undefined" || typeof node.props.height !== "undefined")) {
|
|
327
|
+
const width = typeof node.props.width === "undefined" || typeof options.width === "undefined" ? undefined : options.width - decoration.horizontal;
|
|
328
|
+
const height = typeof node.props.height === "undefined" || typeof options.height === "undefined" ? undefined : options.height - decoration.vertical;
|
|
329
|
+
if ((typeof width === "number" && width > 0) || (typeof height === "number" && height > 0)) {
|
|
330
|
+
next = constrainFrame(next, {
|
|
331
|
+
width: typeof width === "number" && width > 0 ? width : undefined,
|
|
332
|
+
height: typeof height === "number" && height > 0 ? height : undefined
|
|
333
|
+
});
|
|
334
|
+
}
|
|
22
335
|
}
|
|
23
|
-
|
|
24
|
-
|
|
336
|
+
next = padFrameSides(next, padding);
|
|
337
|
+
next = addBorder(next, border);
|
|
338
|
+
if (options.constrain) {
|
|
339
|
+
next = constrainFrame(next, {
|
|
340
|
+
width: typeof options.width === "undefined" ? positiveDimension(node.props.width, "width") : options.width,
|
|
341
|
+
height: typeof options.height === "undefined" ? positiveDimension(node.props.height, "height") : options.height
|
|
342
|
+
});
|
|
25
343
|
}
|
|
344
|
+
next = addContainerStyleSpans(next, node, resolved);
|
|
26
345
|
if (node.props.id && isFocusable(node)) {
|
|
27
346
|
next = addFocusableHitbox(next, node);
|
|
28
347
|
}
|
|
29
348
|
return next;
|
|
30
349
|
}
|
|
31
|
-
function
|
|
350
|
+
function addContainerStyleSpans(frame, node, resolved = resolveNodeStyle(node)) {
|
|
351
|
+
const width = getFrameWidth(frame);
|
|
352
|
+
const height = getFrameHeight(frame);
|
|
353
|
+
if (width <= 0 || height <= 0) {
|
|
354
|
+
return frame;
|
|
355
|
+
}
|
|
356
|
+
const spans = frame.spans.slice();
|
|
357
|
+
const containerSpans = resolved.spanKinds.slice();
|
|
358
|
+
const border = normalizeBorder(resolved.style?.border);
|
|
359
|
+
if (!resolved.style?.color && border.color) {
|
|
360
|
+
containerSpans.push(styleSpan("#style", { color: border.color }));
|
|
361
|
+
}
|
|
362
|
+
for (const span of containerSpans) {
|
|
363
|
+
for (let y = 1; y <= height; y += 1) {
|
|
364
|
+
spans.push({ ...span, x1: 1, x2: width + 1, y });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (node.props.__focused && isFocusable(node)) {
|
|
368
|
+
for (let y = 1; y <= height; y += 1) {
|
|
369
|
+
spans.push({ kind: "focus", x1: 1, x2: width + 1, y });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return createFrame(frame.lines, frame.hitboxes, frame.cursor, spans);
|
|
373
|
+
}
|
|
374
|
+
function decoratedContentDimension(node, prop, outer, context) {
|
|
375
|
+
positiveInteger(outer, `Pane ${prop}`);
|
|
376
|
+
const decoration = containerDecorationSize(node, context);
|
|
377
|
+
const inner = outer - (prop === "width" ? decoration.horizontal : decoration.vertical);
|
|
378
|
+
if (!Number.isFinite(inner) || !Number.isInteger(inner) || inner <= 0) {
|
|
379
|
+
throw new RangeError(`Pane ${prop} leaves no room for Fixed content`);
|
|
380
|
+
}
|
|
381
|
+
return inner;
|
|
382
|
+
}
|
|
383
|
+
function resolveContainerChildContext(node, dimensions, context) {
|
|
384
|
+
if (typeof dimensions.width !== "number" && typeof dimensions.height !== "number") {
|
|
385
|
+
return context;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
cols: typeof dimensions.width === "number" ? decoratedContentDimension(node, "width", dimensions.width, context) : context?.cols ?? 1,
|
|
389
|
+
rows: typeof dimensions.height === "number" ? decoratedContentDimension(node, "height", dimensions.height, context) : context?.rows ?? 1,
|
|
390
|
+
theme: context?.theme
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function renderInputLine(value, inputState, padding = { top: 0, right: 0, bottom: 0, left: 0 }) {
|
|
32
394
|
const state = normalizeInputState(inputState, value.length);
|
|
33
395
|
const { start, end } = getSelectionRange(state);
|
|
396
|
+
const line = `${value.slice(0, state.cursor)}|${value.slice(state.cursor)}`;
|
|
397
|
+
const paddedLine = `${" ".repeat(padding.left)}${line}${" ".repeat(padding.right)}`;
|
|
398
|
+
const textStart = padding.left + 1;
|
|
34
399
|
return {
|
|
35
|
-
line:
|
|
36
|
-
cursor: { x:
|
|
37
|
-
spans: start === end ? [] : [{ kind: "selection", x1:
|
|
400
|
+
line: paddedLine,
|
|
401
|
+
cursor: { x: textStart + state.cursor, y: 1 },
|
|
402
|
+
spans: start === end ? [] : [{ kind: "input.selection", x1: textStart + start, x2: textStart + end, y: 1 }]
|
|
38
403
|
};
|
|
39
404
|
}
|
|
40
|
-
function
|
|
405
|
+
function renderEditorFrame(node) {
|
|
406
|
+
const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : "";
|
|
407
|
+
const placeholder = typeof node.props.placeholder !== "undefined" ? plainText(node.props.placeholder) : "";
|
|
408
|
+
const displayValue = value.length === 0 && !node.props.__focused && placeholder ? placeholder : value;
|
|
409
|
+
const state = createEditorState(displayValue, node.props.__editorState?.cursor);
|
|
410
|
+
const focusedState = createEditorState(value, node.props.__editorState?.cursor);
|
|
411
|
+
const focusedLine = focusedState.cursor.line;
|
|
412
|
+
const focusedColumn = focusedState.cursor.column;
|
|
413
|
+
const lines = state.lines.map((line, index) => {
|
|
414
|
+
if (!node.props.__focused || index !== focusedLine) {
|
|
415
|
+
return ` ${line}`;
|
|
416
|
+
}
|
|
417
|
+
return `> ${line.slice(0, focusedColumn)}|${line.slice(focusedColumn)}`;
|
|
418
|
+
});
|
|
419
|
+
const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
420
|
+
const cursor = node.props.__focused ? { x: 3 + focusedColumn, y: focusedLine + 1 } : null;
|
|
421
|
+
const spans = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, lines[focusedLine].length + 1), y: focusedLine + 1 }] : [];
|
|
422
|
+
const height = typeof node.props.height === "undefined" ? undefined : positiveDimension(node.props.height, "height");
|
|
423
|
+
const frame = createFrame(lines, [], cursor, spans);
|
|
424
|
+
const scrollOffset = node.props.__focused && typeof height !== "undefined"
|
|
425
|
+
? Math.min(Math.max(0, focusedLine - height + 1), Math.max(0, lines.length - height))
|
|
426
|
+
: 0;
|
|
427
|
+
const constrainedFrame = typeof height === "undefined"
|
|
428
|
+
? frame
|
|
429
|
+
: constrainFrame(scrollOffset > 0 ? cropFrame(frame, scrollOffset, height) : frame, { height });
|
|
430
|
+
if (!node.props.id) {
|
|
431
|
+
return constrainedFrame;
|
|
432
|
+
}
|
|
433
|
+
return createFrame(constrainedFrame.lines, [{ id: node.props.id, tag: node.tag, x1: 1, x2: Math.max(1, getFrameWidth(constrainedFrame), width), y1: 1, y2: getFrameHeight(constrainedFrame), textStartX: 3, textLength: value.length }], constrainedFrame.cursor, constrainedFrame.spans);
|
|
434
|
+
}
|
|
435
|
+
function notifyLayoutContextProbe(node, context) {
|
|
436
|
+
if (context && typeof node.props.__layoutContextProbe === "function") {
|
|
437
|
+
node.props.__layoutContextProbe(context);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function renderTableFrame(node, context) {
|
|
41
441
|
const rowNodes = node.children.filter((child) => child.type === "element");
|
|
42
442
|
if (!rowNodes.length) {
|
|
43
443
|
return createFrame([""]);
|
|
44
444
|
}
|
|
45
|
-
const rowFrames = rowNodes.map((row) => row.children.map(renderTerminalFrame));
|
|
445
|
+
const rowFrames = rowNodes.map((row) => row.children.map((child) => renderTerminalFrame(child, context)));
|
|
46
446
|
const columnCount = rowFrames.reduce((max, row) => Math.max(max, row.length), 0);
|
|
47
447
|
const columnWidths = new Array(columnCount).fill(0);
|
|
48
448
|
for (const row of rowFrames) {
|
|
@@ -82,6 +482,407 @@ function renderTableFrame(node) {
|
|
|
82
482
|
}
|
|
83
483
|
return createFrame(lines, hitboxes, cursor, spans);
|
|
84
484
|
}
|
|
485
|
+
function resolveSplitBreakpoint(node, width, height) {
|
|
486
|
+
const breakpoints = Array.isArray(node.props.breakpoints) ? node.props.breakpoints : [];
|
|
487
|
+
for (const breakpoint of breakpoints) {
|
|
488
|
+
if (typeof breakpoint.maxCols === "number" && width > breakpoint.maxCols)
|
|
489
|
+
continue;
|
|
490
|
+
if (typeof breakpoint.maxRows === "number" && height > breakpoint.maxRows)
|
|
491
|
+
continue;
|
|
492
|
+
return breakpoint;
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
function parseSplitSize(value, index) {
|
|
497
|
+
if (typeof value === "number") {
|
|
498
|
+
return { type: "absolute", value: positiveInteger(value, `Split sizes[${index}]`) };
|
|
499
|
+
}
|
|
500
|
+
if (typeof value !== "string") {
|
|
501
|
+
throw new RangeError(`Split sizes[${index}] must be a number, percent, or fr value`);
|
|
502
|
+
}
|
|
503
|
+
const percent = value.match(/^([+-]?\d+(?:\.\d+)?)%$/);
|
|
504
|
+
if (percent) {
|
|
505
|
+
const amount = Number(percent[1]);
|
|
506
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
507
|
+
throw new RangeError(`Split sizes[${index}] percent must be greater than zero`);
|
|
508
|
+
return { type: "percent", value: amount };
|
|
509
|
+
}
|
|
510
|
+
const fraction = value.match(/^([+-]?\d+(?:\.\d+)?)fr$/);
|
|
511
|
+
if (fraction) {
|
|
512
|
+
const amount = Number(fraction[1]);
|
|
513
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
514
|
+
throw new RangeError(`Split sizes[${index}] fr must be greater than zero`);
|
|
515
|
+
return { type: "fr", value: amount };
|
|
516
|
+
}
|
|
517
|
+
throw new RangeError(`Invalid Split sizes[${index}]: ${value}`);
|
|
518
|
+
}
|
|
519
|
+
function allocateDecimals(ideals, total) {
|
|
520
|
+
const sizes = ideals.map((value) => Math.floor(value));
|
|
521
|
+
let remainder = total - sizes.reduce((sum, value) => sum + value, 0);
|
|
522
|
+
const order = ideals.map((value, index) => ({ index, fraction: value - Math.floor(value) }))
|
|
523
|
+
.sort((a, b) => b.fraction - a.fraction || a.index - b.index);
|
|
524
|
+
for (let index = 0; index < order.length && remainder > 0; index += 1) {
|
|
525
|
+
sizes[order[index].index] += 1;
|
|
526
|
+
remainder -= 1;
|
|
527
|
+
}
|
|
528
|
+
return sizes;
|
|
529
|
+
}
|
|
530
|
+
function resolveSplitSizes(sizesInput, childCount, available) {
|
|
531
|
+
if (!Array.isArray(sizesInput)) {
|
|
532
|
+
const base = Math.floor(available / childCount);
|
|
533
|
+
const remainder = available % childCount;
|
|
534
|
+
const sizes = new Array(childCount);
|
|
535
|
+
for (let index = 0; index < childCount; index += 1)
|
|
536
|
+
sizes[index] = base + (index < remainder ? 1 : 0);
|
|
537
|
+
return sizes;
|
|
538
|
+
}
|
|
539
|
+
if (sizesInput.length !== childCount)
|
|
540
|
+
throw new RangeError("Split sizes length must match child count");
|
|
541
|
+
const parsed = sizesInput.map(parseSplitSize);
|
|
542
|
+
const percentTotal = parsed.reduce((sum, size) => sum + (size.type === "percent" ? size.value : 0), 0);
|
|
543
|
+
if (percentTotal > 100)
|
|
544
|
+
throw new RangeError("Split percentage sizes must not exceed 100%");
|
|
545
|
+
const ideals = new Array(childCount).fill(0);
|
|
546
|
+
let absoluteUsed = 0;
|
|
547
|
+
for (let index = 0; index < parsed.length; index += 1) {
|
|
548
|
+
const size = parsed[index];
|
|
549
|
+
if (size.type === "absolute") {
|
|
550
|
+
ideals[index] = size.value;
|
|
551
|
+
absoluteUsed += size.value;
|
|
552
|
+
}
|
|
553
|
+
else if (size.type === "percent") {
|
|
554
|
+
ideals[index] = available * (size.value / 100);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const percentUsed = ideals.reduce((sum, value, index) => sum + (parsed[index].type === "percent" ? value : 0), 0);
|
|
558
|
+
const remaining = available - absoluteUsed - percentUsed;
|
|
559
|
+
if (remaining < -0.000001)
|
|
560
|
+
throw new RangeError("Split sizes plus gaps must fit within the major axis");
|
|
561
|
+
const frTotal = parsed.reduce((sum, size) => sum + (size.type === "fr" ? size.value : 0), 0);
|
|
562
|
+
for (let index = 0; index < parsed.length; index += 1) {
|
|
563
|
+
const size = parsed[index];
|
|
564
|
+
if (size.type === "fr")
|
|
565
|
+
ideals[index] = frTotal > 0 ? remaining * (size.value / frTotal) : 0;
|
|
566
|
+
}
|
|
567
|
+
const allocationTotal = frTotal > 0 ? available : Math.round(ideals.reduce((sum, value) => sum + value, 0));
|
|
568
|
+
const allocated = allocateDecimals(ideals, allocationTotal);
|
|
569
|
+
if (allocated.reduce((sum, size) => sum + size, 0) > available)
|
|
570
|
+
throw new RangeError("Split sizes plus gaps must fit within the major axis");
|
|
571
|
+
return allocated;
|
|
572
|
+
}
|
|
573
|
+
function isDirectSplitLayoutContainer(node) {
|
|
574
|
+
return node.type === "element" && (node.tag === "terminal-pane" || node.tag === "terminal-box" || node.tag === "terminal-view" || node.tag === "terminal-scroll");
|
|
575
|
+
}
|
|
576
|
+
function renderSplitChildFrame(child, cellWidth, cellHeight, context) {
|
|
577
|
+
const childContext = { cols: cellWidth, rows: cellHeight, theme: context?.theme };
|
|
578
|
+
if (!isDirectSplitLayoutContainer(child)) {
|
|
579
|
+
return renderTerminalFrame(child, childContext);
|
|
580
|
+
}
|
|
581
|
+
const props = { ...child.props };
|
|
582
|
+
if (typeof props.width === "undefined" && props.fill !== true) {
|
|
583
|
+
props.width = cellWidth;
|
|
584
|
+
}
|
|
585
|
+
if (typeof props.height === "undefined" && props.fill !== true) {
|
|
586
|
+
props.height = cellHeight;
|
|
587
|
+
}
|
|
588
|
+
return renderTerminalFrame({ ...child, props }, childContext);
|
|
589
|
+
}
|
|
590
|
+
function renderSplitFrame(node, context) {
|
|
591
|
+
const width = resolveSplitDimension(node, "width", context);
|
|
592
|
+
const height = resolveSplitDimension(node, "height", context);
|
|
593
|
+
const breakpoint = resolveSplitBreakpoint(node, width, height);
|
|
594
|
+
const gap = nonNegativeInteger(breakpoint?.gap ?? node.props.gap, "Split gap");
|
|
595
|
+
const direction = (breakpoint?.direction ?? node.props.direction) === "column" ? "column" : "row";
|
|
596
|
+
const childCount = node.children.length;
|
|
597
|
+
if (!childCount) {
|
|
598
|
+
const frame = createFrame(new Array(height).fill(" ".repeat(width)));
|
|
599
|
+
return node.props.id && isFocusable(node) ? addFocusableHitbox(frame, node) : frame;
|
|
600
|
+
}
|
|
601
|
+
const majorSize = direction === "row" ? width : height;
|
|
602
|
+
const available = majorSize - gap * (childCount - 1);
|
|
603
|
+
if (available < 0) {
|
|
604
|
+
throw new RangeError("Split gap leaves insufficient space for children");
|
|
605
|
+
}
|
|
606
|
+
const sizes = resolveSplitSizes(breakpoint?.sizes ?? node.props.sizes, childCount, available);
|
|
607
|
+
const frames = [];
|
|
608
|
+
for (let index = 0; index < childCount; index += 1) {
|
|
609
|
+
const cellWidth = direction === "row" ? sizes[index] : width;
|
|
610
|
+
const cellHeight = direction === "row" ? height : sizes[index];
|
|
611
|
+
if (cellWidth <= 0 || cellHeight <= 0) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
frames.push(constrainFrame(renderSplitChildFrame(node.children[index], cellWidth, cellHeight, context), { width: cellWidth, height: cellHeight }));
|
|
615
|
+
}
|
|
616
|
+
const frame = frames.length
|
|
617
|
+
? direction === "row" ? mergeHorizontal(frames, { gap }) : mergeVertical(frames, { gap })
|
|
618
|
+
: createFrame(new Array(height).fill(" ".repeat(width)));
|
|
619
|
+
const constrained = constrainFrame(frame, { width, height });
|
|
620
|
+
return node.props.id && isFocusable(node) ? addFocusableHitbox(constrained, node) : constrained;
|
|
621
|
+
}
|
|
622
|
+
function hasDirectFixedChildren(node) {
|
|
623
|
+
return node.children.some((child) => child.type === "element" && child.tag === "terminal-fixed");
|
|
624
|
+
}
|
|
625
|
+
function hasDirectOverlayChildren(node) {
|
|
626
|
+
return node.children.some((child) => child.type === "element" && child.tag === "terminal-overlay");
|
|
627
|
+
}
|
|
628
|
+
function splitOverlayChildren(children) {
|
|
629
|
+
const baseChildren = [];
|
|
630
|
+
const overlays = [];
|
|
631
|
+
for (const child of children) {
|
|
632
|
+
if (child.type === "element" && child.tag === "terminal-overlay") {
|
|
633
|
+
overlays.push(child);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
baseChildren.push(child);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return { baseChildren, overlays };
|
|
640
|
+
}
|
|
641
|
+
function renderBodyFrame(children, props, context) {
|
|
642
|
+
const direction = props.direction === "row" ? "row" : "column";
|
|
643
|
+
const gap = numeric(props.gap, 0);
|
|
644
|
+
const frames = children.map((child) => renderTerminalFrame(child, context));
|
|
645
|
+
return direction === "row" ? mergeHorizontal(frames, { gap }) : mergeVertical(frames, { gap });
|
|
646
|
+
}
|
|
647
|
+
function renderFixedChildFrame(node, width, height) {
|
|
648
|
+
return constrainFrame(renderBodyFrame(node.children, {}, { cols: width, rows: height }), { width, height });
|
|
649
|
+
}
|
|
650
|
+
function renderFixedCompositionFrame(node, width, height) {
|
|
651
|
+
if (!Number.isFinite(width) || !Number.isInteger(width) || width <= 0 || !Number.isFinite(height) || !Number.isInteger(height) || height <= 0) {
|
|
652
|
+
throw new RangeError("Fixed composition requires exact positive parent dimensions");
|
|
653
|
+
}
|
|
654
|
+
const fixedNodes = { top: [], bottom: [], left: [], right: [] };
|
|
655
|
+
const bodyChildren = [];
|
|
656
|
+
for (const child of node.children) {
|
|
657
|
+
if (child.type === "element" && child.tag === "terminal-fixed") {
|
|
658
|
+
fixedNodes[fixedPosition(child.props.position)].push(child);
|
|
659
|
+
}
|
|
660
|
+
else if (child.type === "element" && child.tag === "terminal-overlay") {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
bodyChildren.push(child);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const topSize = fixedNodes.top.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
|
|
668
|
+
const bottomSize = fixedNodes.bottom.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
|
|
669
|
+
const middleHeight = height - topSize - bottomSize;
|
|
670
|
+
if (middleHeight < 0) {
|
|
671
|
+
throw new RangeError("Fixed top and bottom regions exceed parent height");
|
|
672
|
+
}
|
|
673
|
+
const leftSize = fixedNodes.left.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
|
|
674
|
+
const rightSize = fixedNodes.right.reduce((total, fixed) => total + positiveInteger(fixed.props.size, "Fixed size"), 0);
|
|
675
|
+
const bodyWidth = width - leftSize - rightSize;
|
|
676
|
+
if (bodyWidth < 0) {
|
|
677
|
+
throw new RangeError("Fixed left and right regions exceed parent width");
|
|
678
|
+
}
|
|
679
|
+
const topFrames = fixedNodes.top.map((fixed) => renderFixedChildFrame(fixed, width, positiveInteger(fixed.props.size, "Fixed size")));
|
|
680
|
+
const bottomFrames = fixedNodes.bottom.map((fixed) => renderFixedChildFrame(fixed, width, positiveInteger(fixed.props.size, "Fixed size")));
|
|
681
|
+
const middleFrames = [];
|
|
682
|
+
if (middleHeight > 0) {
|
|
683
|
+
const leftFrames = fixedNodes.left.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
|
|
684
|
+
const rightFrames = fixedNodes.right.map((fixed) => renderFixedChildFrame(fixed, positiveInteger(fixed.props.size, "Fixed size"), middleHeight));
|
|
685
|
+
const bodyFrames = bodyWidth > 0
|
|
686
|
+
? [constrainFrame(renderBodyFrame(bodyChildren, node.props, { cols: bodyWidth, rows: middleHeight }), { width: bodyWidth, height: middleHeight })]
|
|
687
|
+
: [];
|
|
688
|
+
middleFrames.push(constrainFrame(mergeHorizontal([...leftFrames, ...bodyFrames, ...rightFrames]), { width, height: middleHeight }));
|
|
689
|
+
}
|
|
690
|
+
const frame = mergeVertical([...topFrames, ...middleFrames, ...bottomFrames]);
|
|
691
|
+
return constrainFrame(frame, { width, height });
|
|
692
|
+
}
|
|
693
|
+
function renderStandaloneFixedFrame(node, context) {
|
|
694
|
+
const position = fixedPosition(node.props.position);
|
|
695
|
+
const size = positiveInteger(node.props.size, "Fixed size");
|
|
696
|
+
const frame = renderBodyFrame(node.children, {}, context);
|
|
697
|
+
return position === "top" || position === "bottom"
|
|
698
|
+
? constrainFrame(frame, { height: size })
|
|
699
|
+
: constrainFrame(frame, { width: size });
|
|
700
|
+
}
|
|
701
|
+
function overlayMarginValue(value, axisSize, label) {
|
|
702
|
+
if (typeof value === "number") {
|
|
703
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
704
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
705
|
+
}
|
|
706
|
+
return value;
|
|
707
|
+
}
|
|
708
|
+
if (typeof value === "string") {
|
|
709
|
+
const match = value.match(/^(\d+(?:\.\d+)?)%$/);
|
|
710
|
+
if (!match) {
|
|
711
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
712
|
+
}
|
|
713
|
+
const percent = Number(match[1]);
|
|
714
|
+
if (!Number.isFinite(percent) || percent < 0) {
|
|
715
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
716
|
+
}
|
|
717
|
+
return Math.round(axisSize * percent / 100);
|
|
718
|
+
}
|
|
719
|
+
throw new RangeError(`${label} must be a non-negative finite integer or percentage string`);
|
|
720
|
+
}
|
|
721
|
+
function overlayMargins(margin, width, height) {
|
|
722
|
+
if (typeof margin === "number" || typeof margin === "string") {
|
|
723
|
+
const x = overlayMarginValue(margin, width, "Overlay margin");
|
|
724
|
+
const y = overlayMarginValue(margin, height, "Overlay margin");
|
|
725
|
+
return { x, y };
|
|
726
|
+
}
|
|
727
|
+
if (margin && typeof margin === "object" && !Array.isArray(margin)) {
|
|
728
|
+
const axes = margin;
|
|
729
|
+
return {
|
|
730
|
+
x: overlayMarginValue(axes.x, width, "Overlay margin x"),
|
|
731
|
+
y: overlayMarginValue(axes.y, height, "Overlay margin y")
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
throw new RangeError("Overlay margin is required");
|
|
735
|
+
}
|
|
736
|
+
function overlayGeometry(node, width, height) {
|
|
737
|
+
const margin = overlayMargins(node.props.margin, width, height);
|
|
738
|
+
const overlayWidth = width - margin.x * 2;
|
|
739
|
+
const overlayHeight = height - margin.y * 2;
|
|
740
|
+
if (overlayWidth < 1 || overlayHeight < 1) {
|
|
741
|
+
throw new RangeError("Overlay margin leaves no renderable area");
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
x: margin.x + 1,
|
|
745
|
+
y: margin.y + 1,
|
|
746
|
+
width: overlayWidth,
|
|
747
|
+
height: overlayHeight
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function renderOverlayChildFrame(node, width, height, context) {
|
|
751
|
+
const geometry = overlayGeometry(node, width, height);
|
|
752
|
+
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height });
|
|
753
|
+
frame = addContainerStyleSpans(frame, node, resolveNodeStyle(node, context));
|
|
754
|
+
if (node.props.id && isFocusable(node)) {
|
|
755
|
+
frame = addFocusableHitbox(frame, node);
|
|
756
|
+
}
|
|
757
|
+
return { frame, geometry };
|
|
758
|
+
}
|
|
759
|
+
function applyDirectOverlays(base, overlays, context) {
|
|
760
|
+
let frame = base;
|
|
761
|
+
const width = Math.max(1, getFrameWidth(base));
|
|
762
|
+
const height = Math.max(1, getFrameHeight(base));
|
|
763
|
+
for (const overlay of overlays) {
|
|
764
|
+
const rendered = renderOverlayChildFrame(overlay, width, height, context);
|
|
765
|
+
frame = overlayFrame(frame, rendered.frame, rendered.geometry);
|
|
766
|
+
}
|
|
767
|
+
return frame;
|
|
768
|
+
}
|
|
769
|
+
function renderScreenFrame(node, context) {
|
|
770
|
+
const { baseChildren, overlays } = splitOverlayChildren(node.children);
|
|
771
|
+
let base;
|
|
772
|
+
if (hasDirectFixedChildren(node)) {
|
|
773
|
+
if (!context) {
|
|
774
|
+
throw new RangeError("Screen with direct Fixed children requires exact render context dimensions");
|
|
775
|
+
}
|
|
776
|
+
if (node.props.title) {
|
|
777
|
+
const title = constrainFrame(createFrame([plainText(node.props.title)]), { width: context.cols, height: 1 });
|
|
778
|
+
const remainingRows = context.rows - 1;
|
|
779
|
+
base = remainingRows > 0
|
|
780
|
+
? constrainFrame(mergeVertical([title, renderFixedCompositionFrame(node, context.cols, remainingRows)]), { width: context.cols, height: context.rows })
|
|
781
|
+
: constrainFrame(title, { width: context.cols, height: context.rows });
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
base = renderFixedCompositionFrame(node, context.cols, context.rows);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
const parts = [];
|
|
789
|
+
if (node.props.title) {
|
|
790
|
+
parts.push(createFrame([plainText(node.props.title)]));
|
|
791
|
+
}
|
|
792
|
+
const childContext = context && node.props.title
|
|
793
|
+
? { cols: context.cols, rows: context.rows - 1, theme: context.theme }
|
|
794
|
+
: context;
|
|
795
|
+
if (!context || !node.props.title || context.rows > 1) {
|
|
796
|
+
parts.push(...baseChildren.map((child) => renderTerminalFrame(child, childContext)));
|
|
797
|
+
}
|
|
798
|
+
base = mergeVertical(parts);
|
|
799
|
+
if (context && overlays.length) {
|
|
800
|
+
base = constrainFrame(base, { width: context.cols, height: context.rows });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return overlays.length ? applyDirectOverlays(base, overlays, context) : base;
|
|
804
|
+
}
|
|
805
|
+
function renderPaneFrame(node, context) {
|
|
806
|
+
const { baseChildren, overlays } = splitOverlayChildren(node.children);
|
|
807
|
+
const dimensions = resolveBlockLayoutDimensions(node, "Pane", context, { exactHeightFromContext: hasDirectFixedChildren(node) });
|
|
808
|
+
let frame;
|
|
809
|
+
if (hasDirectFixedChildren(node)) {
|
|
810
|
+
const width = decoratedContentDimension(node, "width", dimensions.width ?? positiveInteger(node.props.width, "Pane width"), context);
|
|
811
|
+
const height = decoratedContentDimension(node, "height", dimensions.height ?? positiveInteger(node.props.height, "Pane height"), context);
|
|
812
|
+
frame = renderFixedCompositionFrame(node, width, height);
|
|
813
|
+
frame = decorateContainerFrame(frame, node, { constrain: true, ...dimensions }, context);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
const childContext = resolveContainerChildContext(node, dimensions, context);
|
|
817
|
+
frame = renderBodyFrame(baseChildren, node.props, childContext);
|
|
818
|
+
frame = decorateContainerFrame(frame, node, { constrain: true, ...dimensions }, context);
|
|
819
|
+
}
|
|
820
|
+
return overlays.length ? applyDirectOverlays(frame, overlays, context) : frame;
|
|
821
|
+
}
|
|
822
|
+
function renderStandaloneOverlayFrame(node, context) {
|
|
823
|
+
if (!context) {
|
|
824
|
+
throw new RangeError("Standalone Overlay requires exact render context dimensions");
|
|
825
|
+
}
|
|
826
|
+
const rendered = renderOverlayChildFrame(node, context.cols, context.rows, context);
|
|
827
|
+
return rendered.frame;
|
|
828
|
+
}
|
|
829
|
+
function renderLogViewFrame(node, context) {
|
|
830
|
+
const dimensions = resolveLayoutDimensions(node, "LogView", context);
|
|
831
|
+
const entries = Array.isArray(node.props.entries) ? node.props.entries : [];
|
|
832
|
+
const lines = [];
|
|
833
|
+
if (entries.length === 0) {
|
|
834
|
+
lines.push(plainText(node.props.emptyText ?? ""));
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
838
|
+
const entry = entries[index];
|
|
839
|
+
const value = typeof node.props.renderEntry === "function"
|
|
840
|
+
? node.props.renderEntry(entry, index)
|
|
841
|
+
: entry?.content ?? "";
|
|
842
|
+
lines.push(...plainText(value).split("\n"));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const resolved = resolveNodeStyle(node, context);
|
|
846
|
+
const layoutStyle = resolveNodeLayoutStyle(node, context);
|
|
847
|
+
const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, "LogView padding");
|
|
848
|
+
const border = normalizeBorder(layoutStyle?.border);
|
|
849
|
+
const horizontalDecoration = padding.left + padding.right + (border.left ? 1 : 0) + (border.right ? 1 : 0);
|
|
850
|
+
const verticalDecoration = padding.top + padding.bottom + (border.top ? 1 : 0) + (border.bottom ? 1 : 0);
|
|
851
|
+
const innerWidth = typeof dimensions.width === "undefined" ? undefined : dimensions.width - horizontalDecoration;
|
|
852
|
+
const innerHeight = typeof dimensions.height === "undefined" ? undefined : dimensions.height - verticalDecoration;
|
|
853
|
+
let frame = createFrame(lines);
|
|
854
|
+
if ((typeof innerWidth === "number" && innerWidth <= 0) || (typeof innerHeight === "number" && innerHeight <= 0)) {
|
|
855
|
+
frame = createFrame([""]);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
if (typeof innerHeight !== "undefined") {
|
|
859
|
+
const offset = node.props.followTail === true ? Math.max(0, getFrameHeight(frame) - innerHeight) : 0;
|
|
860
|
+
frame = cropFrame(frame, offset, innerHeight);
|
|
861
|
+
while (getFrameHeight(frame) < innerHeight) {
|
|
862
|
+
frame = createFrame([...frame.lines, ""], frame.hitboxes, frame.cursor, frame.spans);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (typeof innerWidth !== "undefined") {
|
|
866
|
+
frame = constrainFrame(frame, { width: innerWidth, height: innerHeight });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
frame = padFrameSides(frame, padding);
|
|
870
|
+
frame = addBorder(frame, border);
|
|
871
|
+
if (typeof dimensions.width !== "undefined") {
|
|
872
|
+
frame = constrainFrame(frame, { width: dimensions.width, height: dimensions.height });
|
|
873
|
+
}
|
|
874
|
+
else if (typeof dimensions.height !== "undefined") {
|
|
875
|
+
frame = cropFrame(frame, 0, dimensions.height);
|
|
876
|
+
while (typeof dimensions.height !== "undefined" && getFrameHeight(frame) < dimensions.height) {
|
|
877
|
+
frame = createFrame([...frame.lines, ""], frame.hitboxes, frame.cursor, frame.spans);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
frame = addContainerStyleSpans(frame, node, resolved);
|
|
881
|
+
if (node.props.id && isFocusable(node)) {
|
|
882
|
+
frame = addFocusableHitbox(frame, node);
|
|
883
|
+
}
|
|
884
|
+
return frame;
|
|
885
|
+
}
|
|
85
886
|
function renderSeparatedRowFrame(frames, separator = " | ") {
|
|
86
887
|
if (!frames.length) {
|
|
87
888
|
return createFrame([""]);
|
|
@@ -112,28 +913,33 @@ function renderSeparatedRowFrame(frames, separator = " | ") {
|
|
|
112
913
|
}
|
|
113
914
|
return createFrame(lines, hitboxes, cursor, spans);
|
|
114
915
|
}
|
|
115
|
-
function renderElementFrame(node) {
|
|
916
|
+
function renderElementFrame(node, context) {
|
|
917
|
+
notifyLayoutContextProbe(node, context);
|
|
116
918
|
switch (node.tag) {
|
|
117
919
|
case "terminal-screen": {
|
|
118
|
-
|
|
119
|
-
if (node.props.title) {
|
|
120
|
-
parts.push(createFrame([String(node.props.title)]));
|
|
121
|
-
}
|
|
122
|
-
parts.push(...node.children.map(renderTerminalFrame));
|
|
123
|
-
return mergeVertical(parts);
|
|
920
|
+
return renderScreenFrame(node, context);
|
|
124
921
|
}
|
|
125
922
|
case "terminal-box":
|
|
126
923
|
case "terminal-view":
|
|
924
|
+
case "terminal-pane":
|
|
127
925
|
case "terminal-scroll": {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
926
|
+
let frame;
|
|
927
|
+
if (node.tag === "terminal-pane" && (hasDirectFixedChildren(node) || hasDirectOverlayChildren(node))) {
|
|
928
|
+
return renderPaneFrame(node, context);
|
|
929
|
+
}
|
|
930
|
+
const label = node.tag === "terminal-box" ? "Box" : node.tag === "terminal-view" ? "View" : node.tag === "terminal-scroll" ? "ScrollView" : "Pane";
|
|
931
|
+
const dimensions = node.tag === "terminal-scroll"
|
|
932
|
+
? resolveLayoutDimensions(node, label, context)
|
|
933
|
+
: resolveBlockLayoutDimensions(node, label, context);
|
|
934
|
+
const childContext = resolveContainerChildContext(node, dimensions, context);
|
|
935
|
+
frame = renderBodyFrame(node.children, node.props, childContext);
|
|
936
|
+
frame = decorateContainerFrame(frame, node, { constrain: node.tag === "terminal-box" || node.tag === "terminal-view" || node.tag === "terminal-pane", ...dimensions }, context);
|
|
134
937
|
if (node.tag === "terminal-scroll") {
|
|
135
938
|
const offset = numeric(node.props.__scrollOffset, 0);
|
|
136
|
-
|
|
939
|
+
if (typeof dimensions.width !== "undefined") {
|
|
940
|
+
frame = constrainFrame(frame, { width: dimensions.width });
|
|
941
|
+
}
|
|
942
|
+
const height = numeric(dimensions.height ?? node.props.height, getFrameHeight(frame));
|
|
137
943
|
frame = cropFrame(frame, offset, height || getFrameHeight(frame));
|
|
138
944
|
const highlightRows = Array.isArray(node.props.highlightRows) ? node.props.highlightRows.map((value) => Number(value)) : [];
|
|
139
945
|
const hoveredRow = typeof node.props.__hoveredRow === "number" ? Number(node.props.__hoveredRow) : -1;
|
|
@@ -152,77 +958,126 @@ function renderElementFrame(node) {
|
|
|
152
958
|
}
|
|
153
959
|
return frame;
|
|
154
960
|
}
|
|
961
|
+
case "terminal-split":
|
|
962
|
+
return renderSplitFrame(node, context);
|
|
963
|
+
case "terminal-fixed":
|
|
964
|
+
return renderStandaloneFixedFrame(node, context);
|
|
965
|
+
case "terminal-overlay":
|
|
966
|
+
return renderStandaloneOverlayFrame(node, context);
|
|
967
|
+
case "terminal-log-view":
|
|
968
|
+
return renderLogViewFrame(node, context);
|
|
155
969
|
case "terminal-list": {
|
|
156
970
|
const items = Array.isArray(node.props.items) ? node.props.items : [];
|
|
157
971
|
const selectedIndex = numeric(node.props.__selectedIndex, 0);
|
|
158
972
|
const hoveredIndex = typeof node.props.__hoveredIndex === "number" ? Number(node.props.__hoveredIndex) : -1;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
973
|
+
const range = listVirtualRange(node, items.length, selectedIndex, context);
|
|
974
|
+
const lines = [];
|
|
975
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
976
|
+
const item = items[index];
|
|
977
|
+
const label = typeof node.props.renderItem === "function" ? plainText(node.props.renderItem(item, index)) : plainText(item);
|
|
978
|
+
lines.push(label);
|
|
979
|
+
}
|
|
980
|
+
const visibleLines = lines.length ? lines : [""];
|
|
981
|
+
const layoutStyle = resolveLayoutStyle("list.base", node, context);
|
|
982
|
+
const padding = normalizeSpacing(layoutStyle.padding, "List padding");
|
|
983
|
+
const border = normalizeBorder(layoutStyle.border);
|
|
984
|
+
const decorated = addBorder(padFrameSides(createFrame(visibleLines), padding), border);
|
|
985
|
+
const width = Math.max(1, getFrameWidth(decorated));
|
|
986
|
+
const height = Math.max(1, getFrameHeight(decorated));
|
|
987
|
+
const itemY = 1 + (border.top ? 1 : 0) + padding.top;
|
|
163
988
|
const spans = [];
|
|
164
|
-
for (let index = 0; index <
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
989
|
+
for (let index = 0; index < visibleLines.length; index += 1) {
|
|
990
|
+
const sourceIndex = range.start + index;
|
|
991
|
+
const y = itemY + index;
|
|
992
|
+
spans.push({ kind: "list.base", x1: 1, x2: width + 1, y });
|
|
993
|
+
if (sourceIndex === selectedIndex) {
|
|
994
|
+
spans.push({ kind: "list.current", x1: 1, x2: width + 1, y });
|
|
168
995
|
}
|
|
169
|
-
if (
|
|
170
|
-
spans.push({ kind: "hover", x1: 1, x2: width
|
|
996
|
+
if (sourceIndex === hoveredIndex) {
|
|
997
|
+
spans.push({ kind: "list.hover", x1: 1, x2: width + 1, y });
|
|
171
998
|
}
|
|
172
999
|
}
|
|
173
|
-
const frame = createFrame(
|
|
174
|
-
|
|
1000
|
+
const frame = createFrame(decorated.lines, [], decorated.cursor, spans);
|
|
1001
|
+
const styled = addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1002
|
+
if (!node.props.id) {
|
|
1003
|
+
return styled;
|
|
1004
|
+
}
|
|
1005
|
+
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);
|
|
175
1006
|
}
|
|
176
1007
|
case "terminal-table":
|
|
177
|
-
return renderTableFrame(node);
|
|
1008
|
+
return renderTableFrame(node, context);
|
|
178
1009
|
case "terminal-row":
|
|
179
|
-
return renderSeparatedRowFrame(node.children.map(renderTerminalFrame),
|
|
1010
|
+
return renderSeparatedRowFrame(node.children.map((child) => renderTerminalFrame(child, context)), plainText(node.props.separator || " | "));
|
|
180
1011
|
case "terminal-td": {
|
|
181
|
-
const frame = mergeVertical(node.children.map(renderTerminalFrame));
|
|
182
|
-
|
|
1012
|
+
const frame = mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
|
|
1013
|
+
const resolved = resolveNodeStyle(node, context);
|
|
1014
|
+
const layoutStyle = resolveNodeLayoutStyle(node, context);
|
|
1015
|
+
const padding = normalizeSpacing(layoutStyle?.padding ?? node.props.padding, "Td padding");
|
|
1016
|
+
const border = normalizeBorder(layoutStyle?.border);
|
|
1017
|
+
return addFullFrameSpans(addBorder(padFrameSides(frame, padding), border), resolved.spanKinds);
|
|
183
1018
|
}
|
|
184
1019
|
case "terminal-text": {
|
|
185
|
-
const value = typeof node.props.value !== "undefined" ?
|
|
186
|
-
|
|
1020
|
+
const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : plainText(node.children.map(textContent).join(""));
|
|
1021
|
+
const frame = createFrame(value.split("\n"));
|
|
1022
|
+
return addFullFrameSpans(frame, resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
187
1023
|
}
|
|
188
1024
|
case "terminal-input": {
|
|
189
1025
|
const value = typeof node.props.value !== "undefined" ? node.props.value : node.props.placeholder || "";
|
|
190
|
-
const stringValue =
|
|
1026
|
+
const stringValue = plainText(value);
|
|
191
1027
|
const displayValue = !node.props.__focused && stringValue.length === 0 && typeof node.props.placeholder !== "undefined"
|
|
192
|
-
?
|
|
1028
|
+
? plainText(node.props.placeholder)
|
|
193
1029
|
: stringValue;
|
|
1030
|
+
const layoutStyle = resolveLayoutStyle("input.base", node, context);
|
|
1031
|
+
const inputPadding = normalizeSpacing(layoutStyle.padding, "Input padding");
|
|
1032
|
+
const inputBorder = normalizeBorder(layoutStyle.border);
|
|
1033
|
+
const textStartX = (inputBorder.left ? 1 : 0) + inputPadding.left + 1;
|
|
194
1034
|
if (!node.props.__focused) {
|
|
195
|
-
const line =
|
|
196
|
-
const
|
|
197
|
-
|
|
1035
|
+
const line = `${" ".repeat(inputPadding.left)}${displayValue}${" ".repeat(inputPadding.right)}`;
|
|
1036
|
+
const decorated = addBorder(createFrame([line]), inputBorder);
|
|
1037
|
+
const width = Math.max(1, getFrameWidth(decorated));
|
|
1038
|
+
const height = Math.max(1, getFrameHeight(decorated));
|
|
1039
|
+
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
|
|
1040
|
+
const spans = fullFrameSpans(["input.base"], width, height);
|
|
1041
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
198
1042
|
}
|
|
199
|
-
const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length });
|
|
200
|
-
const
|
|
201
|
-
|
|
1043
|
+
const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length }, inputPadding);
|
|
1044
|
+
const decorated = addBorder(createFrame([rendered.line], [], rendered.cursor, rendered.spans), inputBorder);
|
|
1045
|
+
const width = Math.max(1, getFrameWidth(decorated));
|
|
1046
|
+
const height = Math.max(1, getFrameHeight(decorated));
|
|
1047
|
+
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, textLength: stringValue.length }] : [];
|
|
1048
|
+
const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...decorated.spans];
|
|
1049
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
202
1050
|
}
|
|
1051
|
+
case "terminal-editor":
|
|
1052
|
+
return addFullFrameSpans(renderEditorFrame(node), resolveNodeStyle(node, context).spanKinds);
|
|
203
1053
|
case "terminal-button": {
|
|
204
|
-
const label = typeof node.props.label !== "undefined" ? node.props.label : node.children.map(textContent).join("");
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
1054
|
+
const label = typeof node.props.label !== "undefined" ? plainText(node.props.label) : plainText(node.children.map(textContent).join(""));
|
|
1055
|
+
const layoutStyle = resolveLayoutStyle("button.base", node, context);
|
|
1056
|
+
const decorated = decoratedControlFrame([String(label)], layoutStyle);
|
|
1057
|
+
const width = Math.max(1, getFrameWidth(decorated));
|
|
1058
|
+
const height = Math.max(1, getFrameHeight(decorated));
|
|
1059
|
+
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height }] : [];
|
|
1060
|
+
const kinds = ["button.base", ...nodeStates(node).map((state) => `button.${state}`)];
|
|
1061
|
+
const spans = fullFrameSpans(kinds, width, height);
|
|
1062
|
+
return addFullFrameSpans(createFrame(decorated.lines, hitboxes, decorated.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
209
1063
|
}
|
|
210
1064
|
default:
|
|
211
|
-
return mergeVertical(node.children.map(renderTerminalFrame));
|
|
1065
|
+
return mergeVertical(node.children.map((child) => renderTerminalFrame(child, context)));
|
|
212
1066
|
}
|
|
213
1067
|
}
|
|
214
|
-
export function renderTerminalFrame(node) {
|
|
1068
|
+
export function renderTerminalFrame(node, context) {
|
|
215
1069
|
if (node.type === "text") {
|
|
216
|
-
return createFrame([node.value]);
|
|
1070
|
+
return createFrame([plainText(node.value)]);
|
|
217
1071
|
}
|
|
218
|
-
return renderElementFrame(node);
|
|
1072
|
+
return renderElementFrame(node, context);
|
|
219
1073
|
}
|
|
220
|
-
export function renderTerminalNode(node) {
|
|
221
|
-
return renderTerminalFrame(node).lines.join("\n");
|
|
1074
|
+
export function renderTerminalNode(node, context) {
|
|
1075
|
+
return renderTerminalFrame(node, context).lines.join("\n");
|
|
222
1076
|
}
|
|
223
|
-
export function renderTerminal(input) {
|
|
224
|
-
|
|
225
|
-
|
|
1077
|
+
export function renderTerminal(input, context) {
|
|
1078
|
+
const renderContext = validateRenderContext(context);
|
|
1079
|
+
return renderValyrianTerminal(input)
|
|
1080
|
+
.map((node) => renderTerminalNode(node, renderContext))
|
|
226
1081
|
.filter(Boolean)
|
|
227
1082
|
.join("\n")
|
|
228
1083
|
.trimEnd();
|