@valyrianjs/terminal 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +89 -104
- package/dist/ansi.js.map +1 -1
- package/dist/layout.d.ts +1 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +11 -1
- package/dist/layout.js.map +1 -1
- package/dist/render-internal.d.ts.map +1 -1
- package/dist/render-internal.js +133 -27
- package/dist/render-internal.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +4 -1
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +36 -10
- package/dist/session.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +4 -1
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +5 -4
- package/docs/cookbook.md +5 -3
- package/docs/core-concepts.md +1 -1
- package/docs/interaction-model.md +1 -1
- package/docs/primitive-gallery.md +1 -1
- package/docs/session-runtime.md +2 -2
- package/examples/docs/overlay-button.tsx +128 -0
- package/llms-full.txt +15 -12
- package/package.json +1 -1
- package/src/ansi.ts +108 -111
- package/src/layout.ts +13 -2
- package/src/render-internal.ts +153 -29
- package/src/render.ts +5 -1
- package/src/session.ts +40 -11
- package/src/theme.ts +4 -1
- package/src/types.ts +1 -0
package/src/ansi.ts
CHANGED
|
@@ -29,12 +29,28 @@ function spanLayerPriority(span: TerminalStyleSpan) {
|
|
|
29
29
|
return span.kind === "focus" ? 0 : 1;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
type RowSpanEntry = {
|
|
33
|
+
span: TerminalStyleSpan;
|
|
34
|
+
index: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function compareSpanStart(a: RowSpanEntry, b: RowSpanEntry) {
|
|
38
|
+
return a.span.x1 - b.span.x1 || compareSpanPaintOrder(a, b) || b.span.x2 - a.span.x2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compareSpanPaintOrder(a: RowSpanEntry, b: RowSpanEntry) {
|
|
42
|
+
return spanLayerPriority(a.span) - spanLayerPriority(b.span) || a.index - b.index;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function lineSpanEntries(spans: TerminalStyleSpan[], y: number) {
|
|
33
46
|
return spans
|
|
34
47
|
.map((span, index) => ({ span, index }))
|
|
35
48
|
.filter(({ span }) => span.y === y)
|
|
36
|
-
.sort(
|
|
37
|
-
|
|
49
|
+
.sort(compareSpanStart);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lineSpans(spans: TerminalStyleSpan[], y: number) {
|
|
53
|
+
return lineSpanEntries(spans, y).map(({ span }) => span);
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
function tokenStyle(token: ReturnType<typeof resolveTerminalStyleToken>) {
|
|
@@ -95,65 +111,110 @@ function colorAnsi(value: string | undefined, background: boolean) {
|
|
|
95
111
|
return `\u001b[${background ? 48 : 38};2;${red};${green};${blue}m`;
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
function sameActiveAnsiStack(previous: RowSpanEntry[], next: RowSpanEntry[]) {
|
|
115
|
+
if (previous.length !== next.length) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return previous.every((entry, index) => entry.index === next[index]?.index);
|
|
119
|
+
}
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
activeSpanIndex += 1;
|
|
108
|
-
}
|
|
121
|
+
function spanContains(container: TerminalStyleSpan, contained: TerminalStyleSpan) {
|
|
122
|
+
return container.y === contained.y && container.x1 <= contained.x1 && container.x2 >= contained.x2 && (container.x1 < contained.x1 || container.x2 > contained.x2);
|
|
123
|
+
}
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
function hasLaterContainedSpan(entry: RowSpanEntry, rowEntries: RowSpanEntry[]) {
|
|
126
|
+
return rowEntries.some((candidate) => candidate.index > entry.index && spanContains(entry.span, candidate.span));
|
|
127
|
+
}
|
|
113
128
|
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
function compareActiveSpanPaintOrder(rowEntries: RowSpanEntry[], a: RowSpanEntry, b: RowSpanEntry) {
|
|
130
|
+
const priority = spanLayerPriority(a.span) - spanLayerPriority(b.span);
|
|
131
|
+
if (priority !== 0) {
|
|
132
|
+
return priority;
|
|
133
|
+
}
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
closedSpan = true;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
for (const span of rowSpans) {
|
|
127
|
-
if (span.x2 === visibleColumn && span.kind === "focus") {
|
|
128
|
-
output += spanAnsiClose(span, theme);
|
|
129
|
-
closedSpan = true;
|
|
130
|
-
}
|
|
135
|
+
const aContainsB = spanContains(a.span, b.span);
|
|
136
|
+
const bContainsA = spanContains(b.span, a.span);
|
|
137
|
+
if (aContainsB && !bContainsA) {
|
|
138
|
+
if (a.index < b.index) {
|
|
139
|
+
return -1;
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
141
|
+
return hasLaterContainedSpan(a, rowEntries) ? 1 : -1;
|
|
142
|
+
}
|
|
143
|
+
if (bContainsA && !aContainsB) {
|
|
144
|
+
if (b.index < a.index) {
|
|
145
|
+
return 1;
|
|
138
146
|
}
|
|
147
|
+
return hasLaterContainedSpan(b, rowEntries) ? -1 : 1;
|
|
139
148
|
}
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
return a.index - b.index;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function activeAnsiStack(rowEntries: RowSpanEntry[], column: number) {
|
|
154
|
+
return rowEntries
|
|
155
|
+
.filter(({ span }) => span.x1 <= column && span.x2 > column)
|
|
156
|
+
.sort((a, b) => compareActiveSpanPaintOrder(rowEntries, a, b));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function commonAnsiStackPrefixLength(previous: RowSpanEntry[], next: RowSpanEntry[]) {
|
|
160
|
+
const length = Math.min(previous.length, next.length);
|
|
161
|
+
for (let index = 0; index < length; index += 1) {
|
|
162
|
+
if (previous[index]?.index !== next[index]?.index) {
|
|
163
|
+
return index;
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
return length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function compareAnsiCloseOrder(a: RowSpanEntry, b: RowSpanEntry) {
|
|
170
|
+
const aFocus = a.span.kind === "focus" ? 1 : 0;
|
|
171
|
+
const bFocus = b.span.kind === "focus" ? 1 : 0;
|
|
172
|
+
return aFocus - bFocus || a.index - b.index;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function switchAnsiStack(previous: RowSpanEntry[], next: RowSpanEntry[], theme?: TerminalTheme) {
|
|
176
|
+
if (sameActiveAnsiStack(previous, next)) {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const commonPrefixLength = commonAnsiStackPrefixLength(previous, next);
|
|
181
|
+
const removed = previous.slice(commonPrefixLength).sort(compareAnsiCloseOrder);
|
|
182
|
+
const commonPrefix = previous.slice(0, commonPrefixLength);
|
|
183
|
+
return [
|
|
184
|
+
...removed.map(({ span }) => spanAnsiClose(span, theme)),
|
|
185
|
+
...(removed.length > 0 ? commonPrefix.map(({ span }) => spanAnsiOpen(span, theme)) : []),
|
|
186
|
+
...next.slice(commonPrefixLength).map(({ span }) => spanAnsiOpen(span, theme))
|
|
187
|
+
].join("");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderAnsiLineFromColumn(line: string, spans: TerminalStyleSpan[], y: number, startColumn: number, theme?: TerminalTheme) {
|
|
191
|
+
let output = "";
|
|
192
|
+
let visibleColumn = Math.max(1, startColumn);
|
|
193
|
+
const rowEntries = lineSpanEntries(spans, y);
|
|
194
|
+
let currentStack: RowSpanEntry[] = [];
|
|
195
|
+
|
|
196
|
+
for (const grapheme of terminalGraphemes(dropTerminalCells(line, visibleColumn - 1))) {
|
|
197
|
+
const nextStack = activeAnsiStack(rowEntries, visibleColumn);
|
|
198
|
+
output += switchAnsiStack(currentStack, nextStack, theme);
|
|
199
|
+
currentStack = nextStack;
|
|
200
|
+
|
|
201
|
+
if (grapheme === "|") {
|
|
202
|
+
continue;
|
|
151
203
|
}
|
|
204
|
+
|
|
205
|
+
output += grapheme;
|
|
206
|
+
visibleColumn += terminalCellWidth(grapheme);
|
|
152
207
|
}
|
|
153
208
|
|
|
209
|
+
output += switchAnsiStack(currentStack, [], theme);
|
|
210
|
+
|
|
154
211
|
return output;
|
|
155
212
|
}
|
|
156
213
|
|
|
214
|
+
function renderAnsiLine(line: string, spans: TerminalStyleSpan[], y: number, theme?: TerminalTheme) {
|
|
215
|
+
return renderAnsiLineFromColumn(line, spans, y, 1, theme);
|
|
216
|
+
}
|
|
217
|
+
|
|
157
218
|
function hasPlainAffordance(token: ReturnType<typeof resolveTerminalStyleToken>) {
|
|
158
219
|
return typeof token !== "undefined" && ("plainPrefix" in token || "plainSuffix" in token);
|
|
159
220
|
}
|
|
@@ -253,71 +314,7 @@ function commonPrefixCellWidth(previousLine: string, nextLine: string) {
|
|
|
253
314
|
}
|
|
254
315
|
|
|
255
316
|
function renderAnsiLineSuffix(line: string, spans: TerminalStyleSpan[], y: number, startColumn: number, theme?: TerminalTheme) {
|
|
256
|
-
|
|
257
|
-
let visibleColumn = Math.max(1, startColumn);
|
|
258
|
-
const rowSpans = lineSpans(spans, y);
|
|
259
|
-
let activeSpanIndex = rowSpans.findIndex((span) => span.x1 >= visibleColumn);
|
|
260
|
-
if (activeSpanIndex < 0) {
|
|
261
|
-
activeSpanIndex = rowSpans.length;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
for (const span of rowSpans) {
|
|
265
|
-
if (span.x1 <= visibleColumn && span.x2 > visibleColumn) {
|
|
266
|
-
output += spanAnsiOpen(span, theme);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const grapheme of terminalGraphemes(dropTerminalCells(line, visibleColumn - 1))) {
|
|
271
|
-
while (rowSpans[activeSpanIndex]?.x1 === visibleColumn) {
|
|
272
|
-
output += spanAnsiOpen(rowSpans[activeSpanIndex], theme);
|
|
273
|
-
activeSpanIndex += 1;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (grapheme === "|") {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
output += grapheme;
|
|
281
|
-
visibleColumn += terminalCellWidth(grapheme);
|
|
282
|
-
|
|
283
|
-
let closedSpan = false;
|
|
284
|
-
for (const span of rowSpans) {
|
|
285
|
-
if (span.x2 === visibleColumn) {
|
|
286
|
-
if (span.kind !== "focus") {
|
|
287
|
-
output += spanAnsiClose(span, theme);
|
|
288
|
-
closedSpan = true;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
for (const span of rowSpans) {
|
|
293
|
-
if (span.x2 === visibleColumn && span.kind === "focus") {
|
|
294
|
-
output += spanAnsiClose(span, theme);
|
|
295
|
-
closedSpan = true;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (closedSpan && visibleColumn <= terminalCellWidth(line)) {
|
|
299
|
-
for (const span of rowSpans) {
|
|
300
|
-
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
301
|
-
output += spanAnsiOpen(span, theme);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
for (const span of rowSpans) {
|
|
308
|
-
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
309
|
-
if (span.kind !== "focus") {
|
|
310
|
-
output += spanAnsiClose(span, theme);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
for (const span of rowSpans) {
|
|
315
|
-
if (span.x1 < visibleColumn && span.x2 > visibleColumn && span.kind === "focus") {
|
|
316
|
-
output += spanAnsiClose(span, theme);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return output;
|
|
317
|
+
return renderAnsiLineFromColumn(line, spans, y, startColumn, theme);
|
|
321
318
|
}
|
|
322
319
|
|
|
323
320
|
|
package/src/layout.ts
CHANGED
|
@@ -242,6 +242,8 @@ function overlayMetric(value: number, name: string) {
|
|
|
242
242
|
return metric;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
const MODAL_OVERLAY_HITBOX_ID = "\u0000valyrian-overlay-modal-shield";
|
|
246
|
+
|
|
245
247
|
function nextPointerLayer(hitboxes: TerminalHitbox[]) {
|
|
246
248
|
let layer = 0;
|
|
247
249
|
for (const box of hitboxes) {
|
|
@@ -250,7 +252,7 @@ function nextPointerLayer(hitboxes: TerminalHitbox[]) {
|
|
|
250
252
|
return layer + 1;
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
export function overlayFrame(base: TerminalFrame, overlay: TerminalFrame, options: { x: number; y: number; width: number; height: number }): TerminalFrame {
|
|
255
|
+
export function overlayFrame(base: TerminalFrame, overlay: TerminalFrame, options: { x: number; y: number; width: number; height: number; backdropSpans?: TerminalStyleSpan[] }): TerminalFrame {
|
|
254
256
|
const x = overlayMetric(options.x, "Overlay x");
|
|
255
257
|
const y = overlayMetric(options.y, "Overlay y");
|
|
256
258
|
const width = overlayMetric(options.width, "Overlay width");
|
|
@@ -289,9 +291,18 @@ export function overlayFrame(base: TerminalFrame, overlay: TerminalFrame, option
|
|
|
289
291
|
...box,
|
|
290
292
|
pointerLayer: overlayLayer + (box.pointerLayer ?? 0)
|
|
291
293
|
}));
|
|
294
|
+
const overlayShield = {
|
|
295
|
+
id: MODAL_OVERLAY_HITBOX_ID,
|
|
296
|
+
tag: "terminal-overlay" as const,
|
|
297
|
+
x1: 1,
|
|
298
|
+
x2: Math.max(1, getFrameWidth(base)),
|
|
299
|
+
y1: 1,
|
|
300
|
+
y2: Math.max(1, getFrameHeight(base)),
|
|
301
|
+
pointerLayer: overlayLayer - 0.5
|
|
302
|
+
};
|
|
292
303
|
const cursor = visibleOverlay.cursor || base.cursor;
|
|
293
304
|
|
|
294
|
-
return createFrame(lines, [...overlayHitboxes, ...base.hitboxes], cursor, [...base.spans, ...visibleOverlay.spans]);
|
|
305
|
+
return createFrame(lines, [...overlayHitboxes, overlayShield, ...base.hitboxes], cursor, [...base.spans, ...(options.backdropSpans ?? []), ...visibleOverlay.spans]);
|
|
295
306
|
}
|
|
296
307
|
|
|
297
308
|
export function cropFrame(frame: TerminalFrame, offset: number, height: number) {
|
package/src/render-internal.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { renderValyrianTerminal } from "./runtime.js";
|
|
|
7
7
|
import { cursorCellOffset, dropTerminalCells, padEndTerminalCells, plainText, sliceTerminalCells, terminalCellToStringIndex, terminalCellWidth, terminalGraphemes } from "./text.js";
|
|
8
8
|
import { resolveTerminalStyle } from "./theme.js";
|
|
9
9
|
|
|
10
|
-
import type { InputInteractionState, TerminalButtonPressEventPayload, TerminalElementNode, TerminalFocusNode, TerminalFrame, TerminalHitbox, TerminalNode, TerminalSpacing, TerminalSplitBreakpoint, TerminalSplitSize, TerminalStyleDefinition, TerminalStyleSpan, TerminalTheme, TerminalVisualState } from "./types.js";
|
|
10
|
+
import type { InputInteractionState, TerminalButtonPressEventPayload, TerminalElementNode, TerminalFocusNode, TerminalFrame, TerminalHitbox, TerminalNode, TerminalSpacing, TerminalSplitBreakpoint, TerminalSplitSize, TerminalStyleDefinition, TerminalStyleSpan, TerminalStyleValue, TerminalTheme, TerminalVisualState } from "./types.js";
|
|
11
11
|
|
|
12
12
|
export interface TerminalRenderContext {
|
|
13
13
|
cols: number;
|
|
@@ -579,6 +579,24 @@ function renderInputLine(value: string, inputState: InputInteractionState, paddi
|
|
|
579
579
|
};
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
function scrollFocusedInputLine(rendered: ReturnType<typeof renderInputLine>, visibleWidth: number) {
|
|
583
|
+
const width = Math.max(1, Math.trunc(visibleWidth));
|
|
584
|
+
const scrollOffset = Math.max(0, rendered.cursor.x - width);
|
|
585
|
+
if (scrollOffset === 0) {
|
|
586
|
+
return rendered;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
line: dropTerminalCells(rendered.line, scrollOffset),
|
|
591
|
+
cursor: { x: rendered.cursor.x - scrollOffset, y: rendered.cursor.y },
|
|
592
|
+
spans: rendered.spans.map((span) => ({
|
|
593
|
+
...span,
|
|
594
|
+
x1: span.x1 - scrollOffset,
|
|
595
|
+
x2: span.x2 - scrollOffset
|
|
596
|
+
}))
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
582
600
|
function resolveEditorDimensions(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
583
601
|
const explicitWidth = positiveDimension(node.props.width, "width");
|
|
584
602
|
const explicitHeight = positiveDimension(node.props.height, "height");
|
|
@@ -587,7 +605,7 @@ function resolveEditorDimensions(node: TerminalElementNode, context?: TerminalRe
|
|
|
587
605
|
? explicitWidth
|
|
588
606
|
: node.props.fill === true
|
|
589
607
|
? fillContextDimension(context, "width", "Editor")
|
|
590
|
-
:
|
|
608
|
+
: contextBackedDimension(context, "width", "Editor"),
|
|
591
609
|
height: typeof explicitHeight !== "undefined"
|
|
592
610
|
? explicitHeight
|
|
593
611
|
: node.props.fill === true
|
|
@@ -596,6 +614,93 @@ function resolveEditorDimensions(node: TerminalElementNode, context?: TerminalRe
|
|
|
596
614
|
};
|
|
597
615
|
}
|
|
598
616
|
|
|
617
|
+
type WrappedEditorSegment = { text: string; sourceLine: number; start: number; end: number };
|
|
618
|
+
|
|
619
|
+
function wrapEditorLineSegments(value: string, width: number, sourceLine: number): WrappedEditorSegment[] {
|
|
620
|
+
if (!Number.isFinite(width) || !Number.isInteger(width) || width <= 0) {
|
|
621
|
+
return [{ text: "", sourceLine, start: 0, end: 0 }];
|
|
622
|
+
}
|
|
623
|
+
if (value.length === 0) {
|
|
624
|
+
return [{ text: "", sourceLine, start: 0, end: 0 }];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const segments: WrappedEditorSegment[] = [];
|
|
628
|
+
let remaining = value;
|
|
629
|
+
let offset = 0;
|
|
630
|
+
while (terminalCellWidth(remaining) > width) {
|
|
631
|
+
const slice = sliceTerminalCells(remaining, width);
|
|
632
|
+
if (slice.length === 0) {
|
|
633
|
+
const [firstGrapheme = ""] = terminalGraphemes(remaining);
|
|
634
|
+
segments.push({ text: firstGrapheme, sourceLine, start: offset, end: offset + firstGrapheme.length });
|
|
635
|
+
remaining = remaining.slice(firstGrapheme.length);
|
|
636
|
+
offset += firstGrapheme.length;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const breakAt = slice.lastIndexOf(" ");
|
|
641
|
+
const useWordBreak = breakAt > 0 && breakAt >= Math.floor(width * 0.6);
|
|
642
|
+
const segmentText = useWordBreak ? remaining.slice(0, breakAt) : slice;
|
|
643
|
+
segments.push({ text: segmentText, sourceLine, start: offset, end: offset + segmentText.length });
|
|
644
|
+
const consumed = useWordBreak ? breakAt + 1 : slice.length;
|
|
645
|
+
remaining = remaining.slice(consumed);
|
|
646
|
+
offset += consumed;
|
|
647
|
+
}
|
|
648
|
+
segments.push({ text: remaining, sourceLine, start: offset, end: offset + remaining.length });
|
|
649
|
+
return segments;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function wrapEditorLines(lines: string[], width: number): WrappedEditorSegment[] {
|
|
653
|
+
return lines.flatMap((line, sourceLine) => wrapEditorLineSegments(line, width, sourceLine));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function insertTrailingEditorCursorSegment(segments: WrappedEditorSegment[], line: number, column: number, contentWidth?: number) {
|
|
657
|
+
if (typeof contentWidth !== "number") {
|
|
658
|
+
return segments;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
662
|
+
const segment = segments[index];
|
|
663
|
+
if (segment.sourceLine !== line || column !== segment.end || segment.start === segment.end) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const nextSegment = segments[index + 1];
|
|
668
|
+
if (nextSegment && nextSegment.sourceLine === line && nextSegment.start === column) {
|
|
669
|
+
return segments;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (terminalCellWidth(segment.text) < contentWidth) {
|
|
673
|
+
return segments;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const cursorSegment = { text: "", sourceLine: line, start: column, end: column };
|
|
677
|
+
return [...segments.slice(0, index + 1), cursorSegment, ...segments.slice(index + 1)];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return segments;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function visualEditorCursor(segments: WrappedEditorSegment[], line: number, column: number) {
|
|
684
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
685
|
+
const segment = segments[index];
|
|
686
|
+
if (segment.sourceLine !== line) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const nextSegment = segments[index + 1];
|
|
691
|
+
if (column === segment.end && nextSegment && nextSegment.sourceLine === line && nextSegment.start === column) {
|
|
692
|
+
return { index: index + 1, column: 0 };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const containsCursor = column >= segment.start && column <= segment.end;
|
|
696
|
+
const isTrailingEmpty = segment.start === segment.end && column === segment.start;
|
|
697
|
+
if (containsCursor || isTrailingEmpty) {
|
|
698
|
+
return { index, column: Math.max(0, Math.min(column - segment.start, segment.text.length)) };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return { index: Math.max(0, segments.length - 1), column: 0 };
|
|
702
|
+
}
|
|
703
|
+
|
|
599
704
|
function renderEditorFrame(node: TerminalElementNode, context?: TerminalRenderContext) {
|
|
600
705
|
const value = typeof node.props.value !== "undefined" ? plainText(node.props.value) : "";
|
|
601
706
|
const placeholder = typeof node.props.placeholder !== "undefined" ? plainText(node.props.placeholder) : "";
|
|
@@ -604,29 +709,24 @@ function renderEditorFrame(node: TerminalElementNode, context?: TerminalRenderCo
|
|
|
604
709
|
const focusedState = createEditorState(value, node.props.__editorState?.cursor);
|
|
605
710
|
const focusedLine = focusedState.cursor.line;
|
|
606
711
|
const focusedColumn = focusedState.cursor.column;
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
712
|
+
const dimensions = resolveEditorDimensions(node, context);
|
|
713
|
+
const contentWidth = typeof dimensions.width === "number" ? Math.max(1, dimensions.width - 2) : undefined;
|
|
714
|
+
const baseSegments = typeof contentWidth === "number" ? wrapEditorLines(state.lines, contentWidth) : state.lines.map((line, sourceLine) => ({ text: line, sourceLine, start: 0, end: line.length }));
|
|
715
|
+
const segments = node.props.__focused ? insertTrailingEditorCursorSegment(baseSegments, focusedLine, focusedColumn, contentWidth) : baseSegments;
|
|
716
|
+
const focusedVisualCursor = visualEditorCursor(segments, focusedLine, focusedColumn);
|
|
717
|
+
const lines = segments.map((segment, index) => {
|
|
718
|
+
if (!node.props.__focused || index !== focusedVisualCursor.index) {
|
|
719
|
+
return ` ${segment.text}`;
|
|
610
720
|
}
|
|
611
721
|
|
|
612
|
-
return ` ${
|
|
722
|
+
return ` ${segment.text.slice(0, focusedVisualCursor.column)}|${segment.text.slice(focusedVisualCursor.column)}`;
|
|
613
723
|
});
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
let cursor = node.props.__focused ? { x: 3 + cursorCellOffset(focusedState.lines[focusedLine], focusedColumn), y: focusedLine + 1 } : null;
|
|
617
|
-
let spans: TerminalStyleSpan[] = node.props.__focused ? [{ kind: "focus", x1: 1, x2: Math.max(2, terminalCellWidth(lines[focusedLine]) + 1), y: focusedLine + 1 }] : [];
|
|
618
|
-
|
|
619
|
-
const availableWidth = dimensions.width ?? context?.cols;
|
|
620
|
-
if (node.props.__focused && cursor && typeof availableWidth === "number" && cursor.x > availableWidth) {
|
|
621
|
-
const horizontalOffset = cursor.x - availableWidth;
|
|
622
|
-
renderedLines = lines.map((line) => dropTerminalCells(line, horizontalOffset));
|
|
623
|
-
cursor = { x: availableWidth, y: cursor.y };
|
|
624
|
-
spans = [{ kind: "focus", x1: 1, x2: Math.max(2, Math.min(availableWidth + 1, terminalCellWidth(renderedLines[focusedLine]) + 1)), y: focusedLine + 1 }];
|
|
625
|
-
}
|
|
724
|
+
const cursor = node.props.__focused ? { x: 3 + cursorCellOffset(segments[focusedVisualCursor.index]?.text ?? "", focusedVisualCursor.column), y: focusedVisualCursor.index + 1 } : null;
|
|
725
|
+
const spans: TerminalStyleSpan[] = node.props.__focused ? [{ kind: "editor.focus", x1: 1, x2: Math.max(2, terminalCellWidth(lines[focusedVisualCursor.index] ?? "") + 1), y: focusedVisualCursor.index + 1 }] : [];
|
|
626
726
|
|
|
627
|
-
const frame = createFrame(
|
|
727
|
+
const frame = createFrame(lines, [], cursor, spans);
|
|
628
728
|
const scrollOffset = node.props.__focused && typeof dimensions.height !== "undefined"
|
|
629
|
-
? Math.min(Math.max(0,
|
|
729
|
+
? Math.min(Math.max(0, focusedVisualCursor.index - dimensions.height + 1), Math.max(0, lines.length - dimensions.height))
|
|
630
730
|
: 0;
|
|
631
731
|
const croppedFrame = typeof dimensions.height === "undefined"
|
|
632
732
|
? frame
|
|
@@ -1004,6 +1104,18 @@ function overlayGeometry(node: TerminalElementNode, width: number, height: numbe
|
|
|
1004
1104
|
};
|
|
1005
1105
|
}
|
|
1006
1106
|
|
|
1107
|
+
function resolveOverlayBackdropSpans(node: TerminalElementNode, width: number, height: number, context?: TerminalRenderContext) {
|
|
1108
|
+
const backdrop = node.props.backdrop;
|
|
1109
|
+
if (backdrop === false || typeof backdrop === "undefined") {
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const styleValue: TerminalStyleValue = backdrop === true ? "overlay.backdrop" : backdrop;
|
|
1114
|
+
const resolvedStyle = resolveTerminalStyle(styleValue, context?.theme);
|
|
1115
|
+
const span = typeof styleValue === "string" ? styleSpan(styleValue) : styleSpan("#style", resolvedStyle);
|
|
1116
|
+
return fullFrameSpans([span.kind], width, height).map((fullSpan) => (typeof span.style === "undefined" ? fullSpan : { ...fullSpan, style: span.style }));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1007
1119
|
function renderOverlayChildFrame(node: TerminalElementNode, width: number, height: number, context?: TerminalRenderContext) {
|
|
1008
1120
|
const geometry = overlayGeometry(node, width, height);
|
|
1009
1121
|
let frame = constrainFrame(renderBodyFrame(node.children, {}, { cols: geometry.width, rows: geometry.height, theme: context?.theme }), { width: geometry.width, height: geometry.height, expandFullRowSpans: true });
|
|
@@ -1024,7 +1136,10 @@ function applyDirectOverlays(base: TerminalFrame, overlays: TerminalElementNode[
|
|
|
1024
1136
|
const height = Math.max(1, getFrameHeight(base));
|
|
1025
1137
|
for (const overlay of orderedDirectOverlays(overlays)) {
|
|
1026
1138
|
const rendered = renderOverlayChildFrame(overlay, width, height, context);
|
|
1027
|
-
frame = overlayFrame(frame, rendered.frame,
|
|
1139
|
+
frame = overlayFrame(frame, rendered.frame, {
|
|
1140
|
+
...rendered.geometry,
|
|
1141
|
+
backdropSpans: resolveOverlayBackdropSpans(overlay, width, height, context)
|
|
1142
|
+
});
|
|
1028
1143
|
}
|
|
1029
1144
|
return frame;
|
|
1030
1145
|
}
|
|
@@ -1383,24 +1498,33 @@ function renderElementFrame(node: TerminalElementNode, context?: TerminalRenderC
|
|
|
1383
1498
|
const layoutStyle = resolveLayoutStyle("input.base", node, context);
|
|
1384
1499
|
const inputPadding = normalizeSpacing(layoutStyle.padding, "Input padding");
|
|
1385
1500
|
const inputBorder = normalizeBorder(layoutStyle.border);
|
|
1501
|
+
const dimensions = {
|
|
1502
|
+
width: positiveDimension(node.props.width, "width") ?? contextBackedDimension(context, "width", "Input")
|
|
1503
|
+
};
|
|
1386
1504
|
const textStartX = (inputBorder.left ? 1 : 0) + inputPadding.left + 1;
|
|
1387
1505
|
if (!node.props.__focused) {
|
|
1388
1506
|
const line = `${" ".repeat(inputPadding.left)}${displayValue}${" ".repeat(inputPadding.right)}`;
|
|
1389
1507
|
const decorated = addBorder(createFrame([line]), inputBorder);
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1508
|
+
const constrained = typeof dimensions.width === "number" ? constrainFrame(decorated, { width: dimensions.width, expandFullFrameSpans: true }) : decorated;
|
|
1509
|
+
const width = Math.max(1, getFrameWidth(constrained));
|
|
1510
|
+
const height = Math.max(1, getFrameHeight(constrained));
|
|
1392
1511
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, ...interactiveTextMetadata(stringValue) }] : [];
|
|
1393
1512
|
const spans = fullFrameSpans(["input.base"], width, height);
|
|
1394
|
-
return addFullFrameSpans(createFrame(
|
|
1513
|
+
return addFullFrameSpans(createFrame(constrained.lines, hitboxes, constrained.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1395
1514
|
}
|
|
1396
1515
|
|
|
1397
1516
|
const rendered = renderInputLine(stringValue, node.props.__inputState || { cursor: stringValue.length, anchor: stringValue.length }, inputPadding);
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1517
|
+
const visibleInputWidth = typeof dimensions.width === "number"
|
|
1518
|
+
? Math.max(1, dimensions.width - (inputBorder.left ? 1 : 0) - (inputBorder.right ? 1 : 0))
|
|
1519
|
+
: getFrameWidth(createFrame([rendered.line]));
|
|
1520
|
+
const visibleRendered = scrollFocusedInputLine(rendered, visibleInputWidth);
|
|
1521
|
+
const decorated = addBorder(createFrame([visibleRendered.line], [], visibleRendered.cursor, visibleRendered.spans), inputBorder);
|
|
1522
|
+
const constrained = typeof dimensions.width === "number" ? constrainFrame(decorated, { width: dimensions.width, expandFullFrameSpans: true }) : decorated;
|
|
1523
|
+
const width = Math.max(1, getFrameWidth(constrained));
|
|
1524
|
+
const height = Math.max(1, getFrameHeight(constrained));
|
|
1401
1525
|
const hitboxes = node.props.id ? [{ id: node.props.id, tag: node.tag, x1: 1, x2: width, y1: 1, y2: height, textStartX, ...interactiveTextMetadata(stringValue) }] : [];
|
|
1402
|
-
const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...
|
|
1403
|
-
return addFullFrameSpans(createFrame(
|
|
1526
|
+
const spans = [...fullFrameSpans(["input.base", "input.focus"], width, height), ...constrained.spans];
|
|
1527
|
+
return addFullFrameSpans(createFrame(constrained.lines, hitboxes, constrained.cursor, spans), resolveNodeStyle(node, context, { includeBase: false }).spanKinds);
|
|
1404
1528
|
}
|
|
1405
1529
|
case "terminal-editor":
|
|
1406
1530
|
return addFullFrameSpans(renderEditorFrame(node, context), resolveNodeStyle(node, context).spanKinds);
|
package/src/render.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { renderTerminalFrame as renderTerminalFrameInternal } from "./render-int
|
|
|
3
3
|
|
|
4
4
|
import type { TerminalFrame, TerminalHitbox, TerminalNode, TerminalTheme } from "./types.js";
|
|
5
5
|
|
|
6
|
+
const MODAL_OVERLAY_HITBOX_ID = "\u0000valyrian-overlay-modal-shield";
|
|
7
|
+
|
|
6
8
|
export interface TerminalRenderContext {
|
|
7
9
|
cols: number;
|
|
8
10
|
rows: number;
|
|
@@ -41,7 +43,9 @@ function stripDoubleUnderscoreFields<T extends Record<string, any>>(value: T): R
|
|
|
41
43
|
function publicTerminalFrame(frame: TerminalFrame): TerminalFrame {
|
|
42
44
|
return {
|
|
43
45
|
lines: frame.lines,
|
|
44
|
-
hitboxes: frame.hitboxes
|
|
46
|
+
hitboxes: frame.hitboxes
|
|
47
|
+
.filter((hitbox) => hitbox.id !== MODAL_OVERLAY_HITBOX_ID)
|
|
48
|
+
.map((hitbox) => stripDoubleUnderscoreFields(hitbox) as TerminalHitbox),
|
|
45
49
|
cursor: frame.cursor ? { ...frame.cursor } : null,
|
|
46
50
|
spans: frame.spans
|
|
47
51
|
};
|