@valyrianjs/terminal 0.2.2 → 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 +197 -50
- 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 +10 -0
- package/dist/render-internal.d.ts.map +1 -0
- package/dist/render-internal.js +1401 -0
- package/dist/render-internal.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +16 -1244
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +112 -14
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +12 -1
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +22 -3
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +1 -2
- 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 +239 -50
- package/src/layout.ts +13 -2
- package/src/render-internal.ts +1565 -0
- package/src/render.ts +18 -1368
- package/src/session.ts +136 -22
- package/src/text.ts +13 -1
- package/src/theme.ts +26 -3
- package/src/types.ts +1 -2
package/src/ansi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { terminalCellWidth, terminalGraphemes } from "./text.js";
|
|
1
|
+
import { dropTerminalCells, terminalCellWidth, terminalGraphemes } from "./text.js";
|
|
2
2
|
import { resolveTerminalStyle, resolveTerminalStyleToken } from "./theme.js";
|
|
3
3
|
import type { CursorPosition, TerminalFrame, TerminalStyleSpan, TerminalTheme } from "./types.js";
|
|
4
4
|
|
|
@@ -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
|
-
}
|
|
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;
|
|
125
140
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if (closedSpan && visibleColumn <= terminalCellWidth(line)) {
|
|
133
|
-
for (const span of rowSpans) {
|
|
134
|
-
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
135
|
-
output += spanAnsiOpen(span, theme);
|
|
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
|
}
|
|
@@ -234,6 +295,70 @@ export function toAnsiFrame(lines: string[], cursor: CursorPosition | null, span
|
|
|
234
295
|
return `\u001b[?25l\u001b[H${ansiLines.join("\n")}${cursorCode}`;
|
|
235
296
|
}
|
|
236
297
|
|
|
298
|
+
function commonPrefixCellWidth(previousLine: string, nextLine: string) {
|
|
299
|
+
const previousGraphemes = terminalGraphemes(previousLine);
|
|
300
|
+
const nextGraphemes = terminalGraphemes(nextLine);
|
|
301
|
+
const length = Math.min(previousGraphemes.length, nextGraphemes.length);
|
|
302
|
+
let width = 0;
|
|
303
|
+
|
|
304
|
+
for (let index = 0; index < length; index += 1) {
|
|
305
|
+
if (previousGraphemes[index] !== nextGraphemes[index]) {
|
|
306
|
+
return width;
|
|
307
|
+
}
|
|
308
|
+
if (previousGraphemes[index] !== "|") {
|
|
309
|
+
width += terminalCellWidth(previousGraphemes[index]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return width;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function renderAnsiLineSuffix(line: string, spans: TerminalStyleSpan[], y: number, startColumn: number, theme?: TerminalTheme) {
|
|
317
|
+
return renderAnsiLineFromColumn(line, spans, y, startColumn, theme);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
function sameSpanStyle(previous: TerminalStyleSpan["style"], next: TerminalStyleSpan["style"]) {
|
|
322
|
+
if (previous === next) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
if (!previous || !next) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return previous.color === next.color
|
|
329
|
+
&& previous.background === next.background
|
|
330
|
+
&& previous.plainPrefix === next.plainPrefix
|
|
331
|
+
&& previous.plainSuffix === next.plainSuffix
|
|
332
|
+
&& JSON.stringify(previous.border) === JSON.stringify(next.border)
|
|
333
|
+
&& JSON.stringify(previous.padding) === JSON.stringify(next.padding);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function sameLineSpans(previousSpans: TerminalStyleSpan[], nextSpans: TerminalStyleSpan[], y: number) {
|
|
337
|
+
if (previousSpans.length === 0 && nextSpans.length === 0) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const previousRowSpans = lineSpans(previousSpans, y);
|
|
342
|
+
const nextRowSpans = lineSpans(nextSpans, y);
|
|
343
|
+
if (previousRowSpans.length !== nextRowSpans.length) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (let index = 0; index < previousRowSpans.length; index += 1) {
|
|
348
|
+
const previous = previousRowSpans[index];
|
|
349
|
+
const next = nextRowSpans[index];
|
|
350
|
+
if (previous.kind !== next.kind
|
|
351
|
+
|| previous.x1 !== next.x1
|
|
352
|
+
|| previous.x2 !== next.x2
|
|
353
|
+
|| previous.y !== next.y
|
|
354
|
+
|| !sameSpanStyle(previous.style, next.style)) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
237
362
|
export function createAnsiFrameDiff(
|
|
238
363
|
previousAnsiLines: string[],
|
|
239
364
|
nextAnsiLines: string[],
|
|
@@ -267,13 +392,77 @@ export function createAnsiFrameDiff(
|
|
|
267
392
|
};
|
|
268
393
|
}
|
|
269
394
|
|
|
395
|
+
function createAnsiFramePatch(
|
|
396
|
+
previousLines: string[],
|
|
397
|
+
previousAnsiLines: string[],
|
|
398
|
+
nextLines: string[],
|
|
399
|
+
previousSpans: TerminalStyleSpan[],
|
|
400
|
+
nextSpans: TerminalStyleSpan[],
|
|
401
|
+
cursor: CursorPosition | null,
|
|
402
|
+
options: AnsiFrameOptions = {}
|
|
403
|
+
): AnsiFrameDiffResult & { nextAnsiLines: string[] } {
|
|
404
|
+
const nextAnsiLines: string[] = [];
|
|
405
|
+
const changedLineIndexes: number[] = [];
|
|
406
|
+
const writes: string[] = ["\u001b[?25l"];
|
|
407
|
+
const maxLines = Math.max(previousAnsiLines.length, nextLines.length);
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < maxLines; i += 1) {
|
|
410
|
+
const previousLine = previousLines[i] || "";
|
|
411
|
+
const nextLine = nextLines[i] || "";
|
|
412
|
+
const previousAnsiLine = previousAnsiLines[i] || "";
|
|
413
|
+
const spansMatch = sameLineSpans(previousSpans, nextSpans, i + 1);
|
|
414
|
+
const nextAnsiLine = i < nextLines.length && previousLine === nextLine && spansMatch
|
|
415
|
+
? previousAnsiLine
|
|
416
|
+
: i < nextLines.length
|
|
417
|
+
? renderAnsiLine(nextLine, nextSpans, i + 1, options.theme)
|
|
418
|
+
: "";
|
|
419
|
+
nextAnsiLines[i] = nextAnsiLine;
|
|
420
|
+
if (nextAnsiLine === previousAnsiLine) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
changedLineIndexes.push(i);
|
|
425
|
+
const canPatchSuffix = cursor?.y === i + 1
|
|
426
|
+
&& lineSpans(previousSpans, i + 1).length === 0
|
|
427
|
+
&& lineSpans(nextSpans, i + 1).length === 0;
|
|
428
|
+
const prefixWidth = canPatchSuffix && i < previousLines.length && i < nextLines.length && previousLine !== nextLine
|
|
429
|
+
? commonPrefixCellWidth(previousLine, nextLine)
|
|
430
|
+
: 0;
|
|
431
|
+
|
|
432
|
+
if (prefixWidth > 0) {
|
|
433
|
+
const startColumn = prefixWidth + 1;
|
|
434
|
+
const suffix = renderAnsiLineSuffix(nextLine, nextSpans, i + 1, startColumn, options.theme);
|
|
435
|
+
writes.push(`\u001b[${i + 1};${startColumn}H${suffix}\u001b[0m\u001b[K`);
|
|
436
|
+
} else {
|
|
437
|
+
writes.push(`\u001b[${i + 1};1H${nextAnsiLine}\u001b[0m\u001b[K`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (cursor) {
|
|
442
|
+
writes.push(`\u001b[${cursor.y};${cursor.x}H`);
|
|
443
|
+
}
|
|
444
|
+
if (options.showCursor !== false || (options.showCursorWhenFrameHasCursor === true && cursor)) {
|
|
445
|
+
writes.push(ANSI_SHOW_CURSOR);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
changedLineIndexes,
|
|
450
|
+
outputChunk: writes.join(""),
|
|
451
|
+
restoresCursor: Boolean(cursor),
|
|
452
|
+
nextAnsiLines: nextAnsiLines.slice(0, nextLines.length)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
270
456
|
export function createAnsiDiffWriter(options: AnsiFrameOptions = {}) {
|
|
457
|
+
let previousLines: string[] = [];
|
|
271
458
|
let previousAnsiLines: string[] = [];
|
|
459
|
+
let previousSpans: TerminalStyleSpan[] = [];
|
|
272
460
|
|
|
273
461
|
return function toAnsiDiff(lines: string[], cursor: CursorPosition | null, spans: TerminalStyleSpan[] = []) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
previousAnsiLines =
|
|
462
|
+
const diff = createAnsiFramePatch(previousLines, previousAnsiLines, lines, previousSpans, spans, cursor, options);
|
|
463
|
+
previousLines = lines.slice();
|
|
464
|
+
previousAnsiLines = diff.nextAnsiLines.slice();
|
|
465
|
+
previousSpans = spans.map((span) => ({ ...span }));
|
|
277
466
|
return diff.outputChunk;
|
|
278
467
|
};
|
|
279
468
|
}
|
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) {
|