@valyrianjs/terminal 0.2.2 → 0.2.3

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/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
 
@@ -234,6 +234,134 @@ export function toAnsiFrame(lines: string[], cursor: CursorPosition | null, span
234
234
  return `\u001b[?25l\u001b[H${ansiLines.join("\n")}${cursorCode}`;
235
235
  }
236
236
 
237
+ function commonPrefixCellWidth(previousLine: string, nextLine: string) {
238
+ const previousGraphemes = terminalGraphemes(previousLine);
239
+ const nextGraphemes = terminalGraphemes(nextLine);
240
+ const length = Math.min(previousGraphemes.length, nextGraphemes.length);
241
+ let width = 0;
242
+
243
+ for (let index = 0; index < length; index += 1) {
244
+ if (previousGraphemes[index] !== nextGraphemes[index]) {
245
+ return width;
246
+ }
247
+ if (previousGraphemes[index] !== "|") {
248
+ width += terminalCellWidth(previousGraphemes[index]);
249
+ }
250
+ }
251
+
252
+ return width;
253
+ }
254
+
255
+ function renderAnsiLineSuffix(line: string, spans: TerminalStyleSpan[], y: number, startColumn: number, theme?: TerminalTheme) {
256
+ let output = "";
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;
321
+ }
322
+
323
+
324
+ function sameSpanStyle(previous: TerminalStyleSpan["style"], next: TerminalStyleSpan["style"]) {
325
+ if (previous === next) {
326
+ return true;
327
+ }
328
+ if (!previous || !next) {
329
+ return false;
330
+ }
331
+ return previous.color === next.color
332
+ && previous.background === next.background
333
+ && previous.plainPrefix === next.plainPrefix
334
+ && previous.plainSuffix === next.plainSuffix
335
+ && JSON.stringify(previous.border) === JSON.stringify(next.border)
336
+ && JSON.stringify(previous.padding) === JSON.stringify(next.padding);
337
+ }
338
+
339
+ function sameLineSpans(previousSpans: TerminalStyleSpan[], nextSpans: TerminalStyleSpan[], y: number) {
340
+ if (previousSpans.length === 0 && nextSpans.length === 0) {
341
+ return true;
342
+ }
343
+
344
+ const previousRowSpans = lineSpans(previousSpans, y);
345
+ const nextRowSpans = lineSpans(nextSpans, y);
346
+ if (previousRowSpans.length !== nextRowSpans.length) {
347
+ return false;
348
+ }
349
+
350
+ for (let index = 0; index < previousRowSpans.length; index += 1) {
351
+ const previous = previousRowSpans[index];
352
+ const next = nextRowSpans[index];
353
+ if (previous.kind !== next.kind
354
+ || previous.x1 !== next.x1
355
+ || previous.x2 !== next.x2
356
+ || previous.y !== next.y
357
+ || !sameSpanStyle(previous.style, next.style)) {
358
+ return false;
359
+ }
360
+ }
361
+
362
+ return true;
363
+ }
364
+
237
365
  export function createAnsiFrameDiff(
238
366
  previousAnsiLines: string[],
239
367
  nextAnsiLines: string[],
@@ -267,13 +395,77 @@ export function createAnsiFrameDiff(
267
395
  };
268
396
  }
269
397
 
398
+ function createAnsiFramePatch(
399
+ previousLines: string[],
400
+ previousAnsiLines: string[],
401
+ nextLines: string[],
402
+ previousSpans: TerminalStyleSpan[],
403
+ nextSpans: TerminalStyleSpan[],
404
+ cursor: CursorPosition | null,
405
+ options: AnsiFrameOptions = {}
406
+ ): AnsiFrameDiffResult & { nextAnsiLines: string[] } {
407
+ const nextAnsiLines: string[] = [];
408
+ const changedLineIndexes: number[] = [];
409
+ const writes: string[] = ["\u001b[?25l"];
410
+ const maxLines = Math.max(previousAnsiLines.length, nextLines.length);
411
+
412
+ for (let i = 0; i < maxLines; i += 1) {
413
+ const previousLine = previousLines[i] || "";
414
+ const nextLine = nextLines[i] || "";
415
+ const previousAnsiLine = previousAnsiLines[i] || "";
416
+ const spansMatch = sameLineSpans(previousSpans, nextSpans, i + 1);
417
+ const nextAnsiLine = i < nextLines.length && previousLine === nextLine && spansMatch
418
+ ? previousAnsiLine
419
+ : i < nextLines.length
420
+ ? renderAnsiLine(nextLine, nextSpans, i + 1, options.theme)
421
+ : "";
422
+ nextAnsiLines[i] = nextAnsiLine;
423
+ if (nextAnsiLine === previousAnsiLine) {
424
+ continue;
425
+ }
426
+
427
+ changedLineIndexes.push(i);
428
+ const canPatchSuffix = cursor?.y === i + 1
429
+ && lineSpans(previousSpans, i + 1).length === 0
430
+ && lineSpans(nextSpans, i + 1).length === 0;
431
+ const prefixWidth = canPatchSuffix && i < previousLines.length && i < nextLines.length && previousLine !== nextLine
432
+ ? commonPrefixCellWidth(previousLine, nextLine)
433
+ : 0;
434
+
435
+ if (prefixWidth > 0) {
436
+ const startColumn = prefixWidth + 1;
437
+ const suffix = renderAnsiLineSuffix(nextLine, nextSpans, i + 1, startColumn, options.theme);
438
+ writes.push(`\u001b[${i + 1};${startColumn}H${suffix}\u001b[0m\u001b[K`);
439
+ } else {
440
+ writes.push(`\u001b[${i + 1};1H${nextAnsiLine}\u001b[0m\u001b[K`);
441
+ }
442
+ }
443
+
444
+ if (cursor) {
445
+ writes.push(`\u001b[${cursor.y};${cursor.x}H`);
446
+ }
447
+ if (options.showCursor !== false || (options.showCursorWhenFrameHasCursor === true && cursor)) {
448
+ writes.push(ANSI_SHOW_CURSOR);
449
+ }
450
+
451
+ return {
452
+ changedLineIndexes,
453
+ outputChunk: writes.join(""),
454
+ restoresCursor: Boolean(cursor),
455
+ nextAnsiLines: nextAnsiLines.slice(0, nextLines.length)
456
+ };
457
+ }
458
+
270
459
  export function createAnsiDiffWriter(options: AnsiFrameOptions = {}) {
460
+ let previousLines: string[] = [];
271
461
  let previousAnsiLines: string[] = [];
462
+ let previousSpans: TerminalStyleSpan[] = [];
272
463
 
273
464
  return function toAnsiDiff(lines: string[], cursor: CursorPosition | null, spans: TerminalStyleSpan[] = []) {
274
- const ansiLines = lines.map((line, index) => renderAnsiLine(line, spans, index + 1, options.theme));
275
- const diff = createAnsiFrameDiff(previousAnsiLines, ansiLines, cursor, options);
276
- previousAnsiLines = ansiLines.slice();
465
+ const diff = createAnsiFramePatch(previousLines, previousAnsiLines, lines, previousSpans, spans, cursor, options);
466
+ previousLines = lines.slice();
467
+ previousAnsiLines = diff.nextAnsiLines.slice();
468
+ previousSpans = spans.map((span) => ({ ...span }));
277
469
  return diff.outputChunk;
278
470
  };
279
471
  }