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