@termuijs/core 0.1.5 → 0.1.6
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/index.cjs +2210 -460
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +698 -187
- package/dist/index.d.ts +698 -187
- package/dist/index.js +2182 -453
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -30,14 +30,27 @@ __export(index_exports, {
|
|
|
30
30
|
BarSets: () => BarSets,
|
|
31
31
|
BorderSets: () => BorderSets,
|
|
32
32
|
CTRL_KEYS: () => CTRL_KEYS,
|
|
33
|
+
ChordMatcher: () => ChordMatcher,
|
|
33
34
|
ColorDepth: () => ColorDepth,
|
|
35
|
+
Constraint: () => Constraint,
|
|
36
|
+
Dim: () => Dim,
|
|
34
37
|
ESCAPE_SEQUENCES: () => ESCAPE_SEQUENCES,
|
|
35
38
|
EventEmitter: () => EventEmitter,
|
|
39
|
+
FillConstraint: () => FillConstraint,
|
|
40
|
+
Flex: () => Flex,
|
|
36
41
|
FocusManager: () => FocusManager,
|
|
37
42
|
HORIZONTAL_BAR_SYMBOLS: () => HORIZONTAL_BAR_SYMBOLS,
|
|
38
43
|
InputParser: () => InputParser,
|
|
39
44
|
LayerManager: () => LayerManager,
|
|
45
|
+
LengthConstraint: () => LengthConstraint,
|
|
40
46
|
LineSets: () => LineSets,
|
|
47
|
+
LiveRender: () => LiveRender,
|
|
48
|
+
MaxConstraint: () => MaxConstraint,
|
|
49
|
+
MinConstraint: () => MinConstraint,
|
|
50
|
+
MouseGestures: () => MouseGestures,
|
|
51
|
+
PercentageConstraint: () => PercentageConstraint,
|
|
52
|
+
Pos: () => Pos,
|
|
53
|
+
RenderHook: () => RenderHook,
|
|
41
54
|
Renderer: () => Renderer,
|
|
42
55
|
SPECIAL_KEYS: () => SPECIAL_KEYS,
|
|
43
56
|
Screen: () => Screen,
|
|
@@ -46,40 +59,48 @@ __export(index_exports, {
|
|
|
46
59
|
Terminal: () => Terminal,
|
|
47
60
|
VERTICAL_BAR_SYMBOLS: () => VERTICAL_BAR_SYMBOLS,
|
|
48
61
|
ansi: () => ansi_exports,
|
|
62
|
+
bell: () => bell2,
|
|
49
63
|
borderSize: () => borderSize,
|
|
50
64
|
caps: () => caps,
|
|
51
65
|
cellsEqual: () => cellsEqual,
|
|
66
|
+
clipboard: () => clipboard,
|
|
52
67
|
colorToAnsiBg: () => colorToAnsiBg,
|
|
53
68
|
colorToAnsiFg: () => colorToAnsiFg,
|
|
54
69
|
colorToRgb: () => colorToRgb,
|
|
55
70
|
computeLayout: () => computeLayout,
|
|
56
71
|
containsPoint: () => containsPoint,
|
|
57
72
|
contrastRatio: () => contrastRatio,
|
|
73
|
+
createInlineViewport: () => createInlineViewport,
|
|
58
74
|
createKeyEvent: () => createKeyEvent,
|
|
59
75
|
createLayoutNode: () => createLayoutNode,
|
|
60
76
|
createTestScreen: () => createTestScreen,
|
|
77
|
+
debounce: () => debounce,
|
|
61
78
|
defaultStyle: () => defaultStyle,
|
|
62
79
|
detectColorDepth: () => detectColorDepth,
|
|
63
80
|
emptyCell: () => emptyCell,
|
|
64
81
|
emptyRect: () => emptyRect,
|
|
65
|
-
fill: () => fill,
|
|
66
82
|
getBorderChars: () => getBorderChars,
|
|
83
|
+
hasLayoutChanges: () => hasLayoutChanges,
|
|
67
84
|
intersectRect: () => intersectRect,
|
|
85
|
+
invalidateLayout: () => invalidateLayout,
|
|
68
86
|
isMouseSequence: () => isMouseSequence,
|
|
69
|
-
|
|
70
|
-
max: () => max,
|
|
87
|
+
mergeBorders: () => mergeBorders,
|
|
71
88
|
mergeStyles: () => mergeStyles,
|
|
72
|
-
min: () => min,
|
|
73
89
|
normalizeEdges: () => normalizeEdges,
|
|
90
|
+
normalizeNavigationKey: () => normalizeNavigationKey,
|
|
74
91
|
parseColor: () => parseColor,
|
|
75
92
|
parseMouseEvent: () => parseMouseEvent,
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
prefersHighContrast: () => prefersHighContrast,
|
|
94
|
+
prefersReducedMotion: () => prefersReducedMotion,
|
|
95
|
+
readClipboard: () => readClipboard,
|
|
78
96
|
relativeLuminance: () => relativeLuminance,
|
|
79
97
|
renderFallback: () => renderFallback,
|
|
98
|
+
renderInlineToTerminal: () => renderInlineToTerminal,
|
|
99
|
+
resolveConstraints: () => resolveConstraints,
|
|
100
|
+
resolveLayoutVariables: () => resolveLayoutVariables,
|
|
101
|
+
shouldUseColor: () => shouldUseColor,
|
|
80
102
|
shouldUseFallback: () => shouldUseFallback,
|
|
81
103
|
shrinkRect: () => shrinkRect,
|
|
82
|
-
splitRect: () => splitRect,
|
|
83
104
|
stringWidth: () => stringWidth,
|
|
84
105
|
stripAnsi: () => stripAnsi,
|
|
85
106
|
styleToCellAttrs: () => styleToCellAttrs,
|
|
@@ -307,15 +328,15 @@ function contrastRatio(fg, bg) {
|
|
|
307
328
|
const darker = Math.min(l1, l2);
|
|
308
329
|
return (lighter + 0.05) / (darker + 0.05);
|
|
309
330
|
}
|
|
310
|
-
function wcagLevel(
|
|
331
|
+
function wcagLevel(ratio, large = false) {
|
|
311
332
|
if (large) {
|
|
312
|
-
if (
|
|
313
|
-
if (
|
|
333
|
+
if (ratio >= 4.5) return "AAA";
|
|
334
|
+
if (ratio >= 3) return "AA";
|
|
314
335
|
return "fail";
|
|
315
336
|
}
|
|
316
|
-
if (
|
|
317
|
-
if (
|
|
318
|
-
if (
|
|
337
|
+
if (ratio >= 7) return "AAA";
|
|
338
|
+
if (ratio >= 4.5) return "AA";
|
|
339
|
+
if (ratio >= 3) return "A";
|
|
319
340
|
return "fail";
|
|
320
341
|
}
|
|
321
342
|
function validateThemeContrast(theme) {
|
|
@@ -334,10 +355,10 @@ function validateThemeContrast(theme) {
|
|
|
334
355
|
for (const [label, hex] of pairs) {
|
|
335
356
|
if (!hex) continue;
|
|
336
357
|
const fgColor = parseColor(hex);
|
|
337
|
-
const
|
|
338
|
-
const level = wcagLevel(
|
|
358
|
+
const ratio = contrastRatio(fgColor, bgColor);
|
|
359
|
+
const level = wcagLevel(ratio);
|
|
339
360
|
if (level !== "AAA" && level !== "AA") {
|
|
340
|
-
failures.push({ pair: label, ratio: Math.round(
|
|
361
|
+
failures.push({ pair: label, ratio: Math.round(ratio * 100) / 100, level, required: "AA" });
|
|
341
362
|
}
|
|
342
363
|
}
|
|
343
364
|
return failures;
|
|
@@ -350,6 +371,7 @@ __export(ansi_exports, {
|
|
|
350
371
|
ESC: () => ESC,
|
|
351
372
|
OSC: () => OSC,
|
|
352
373
|
beginSyncUpdate: () => beginSyncUpdate,
|
|
374
|
+
bell: () => bell,
|
|
353
375
|
blink: () => blink,
|
|
354
376
|
bold: () => bold,
|
|
355
377
|
clearDown: () => clearDown,
|
|
@@ -358,15 +380,21 @@ __export(ansi_exports, {
|
|
|
358
380
|
clearLineToStart: () => clearLineToStart,
|
|
359
381
|
clearScreen: () => clearScreen,
|
|
360
382
|
clearUp: () => clearUp,
|
|
383
|
+
clipboard: () => clipboard,
|
|
384
|
+
cursorShape: () => cursorShape,
|
|
361
385
|
dim: () => dim,
|
|
362
386
|
disableBracketedPaste: () => disableBracketedPaste,
|
|
387
|
+
disableFocusTracking: () => disableFocusTracking,
|
|
363
388
|
disableMouse: () => disableMouse,
|
|
364
389
|
enableBracketedPaste: () => enableBracketedPaste,
|
|
390
|
+
enableFocusTracking: () => enableFocusTracking,
|
|
365
391
|
enableMouse: () => enableMouse,
|
|
366
392
|
endSyncUpdate: () => endSyncUpdate,
|
|
367
393
|
enterAltScreen: () => enterAltScreen,
|
|
368
394
|
exitAltScreen: () => exitAltScreen,
|
|
369
395
|
hideCursor: () => hideCursor,
|
|
396
|
+
hyperlinkClose: () => hyperlinkClose,
|
|
397
|
+
hyperlinkOpen: () => hyperlinkOpen,
|
|
370
398
|
inverse: () => inverse,
|
|
371
399
|
italic: () => italic,
|
|
372
400
|
moveDown: () => moveDown,
|
|
@@ -374,6 +402,9 @@ __export(ansi_exports, {
|
|
|
374
402
|
moveRight: () => moveRight,
|
|
375
403
|
moveTo: () => moveTo,
|
|
376
404
|
moveUp: () => moveUp,
|
|
405
|
+
notify: () => notify,
|
|
406
|
+
readClipboard: () => readClipboard,
|
|
407
|
+
requestCursorPosition: () => requestCursorPosition,
|
|
377
408
|
reset: () => reset,
|
|
378
409
|
resetBlink: () => resetBlink,
|
|
379
410
|
resetBold: () => resetBold,
|
|
@@ -389,6 +420,7 @@ __export(ansi_exports, {
|
|
|
389
420
|
setTitle: () => setTitle,
|
|
390
421
|
showCursor: () => showCursor,
|
|
391
422
|
strikethrough: () => strikethrough,
|
|
423
|
+
stripAnsiControl: () => stripAnsiControl,
|
|
392
424
|
underline: () => underline,
|
|
393
425
|
writeClipboard: () => writeClipboard
|
|
394
426
|
});
|
|
@@ -399,6 +431,15 @@ var hideCursor = `${CSI}?25l`;
|
|
|
399
431
|
var showCursor = `${CSI}?25h`;
|
|
400
432
|
var saveCursorPosition = `${CSI}s`;
|
|
401
433
|
var restoreCursorPosition = `${CSI}u`;
|
|
434
|
+
function cursorShape(shape, blink2 = true) {
|
|
435
|
+
const codes = {
|
|
436
|
+
block: 1,
|
|
437
|
+
underline: 3,
|
|
438
|
+
bar: 5
|
|
439
|
+
};
|
|
440
|
+
const code = codes[shape] + (blink2 ? 0 : 1);
|
|
441
|
+
return `${CSI}${code} q`;
|
|
442
|
+
}
|
|
402
443
|
function moveTo(col, row) {
|
|
403
444
|
return `${CSI}${row + 1};${col + 1}H`;
|
|
404
445
|
}
|
|
@@ -414,6 +455,7 @@ function moveRight(n = 1) {
|
|
|
414
455
|
function moveLeft(n = 1) {
|
|
415
456
|
return `${CSI}${n}D`;
|
|
416
457
|
}
|
|
458
|
+
var requestCursorPosition = `${CSI}6n`;
|
|
417
459
|
var clearScreen = `${CSI}2J`;
|
|
418
460
|
var clearLine = `${CSI}2K`;
|
|
419
461
|
var clearLineToEnd = `${CSI}0K`;
|
|
@@ -428,6 +470,8 @@ var enableMouse = `${CSI}?1000h${CSI}?1002h${CSI}?1006h`;
|
|
|
428
470
|
var disableMouse = `${CSI}?1000l${CSI}?1002l${CSI}?1006l`;
|
|
429
471
|
var enableBracketedPaste = `${CSI}?2004h`;
|
|
430
472
|
var disableBracketedPaste = `${CSI}?2004l`;
|
|
473
|
+
var enableFocusTracking = `${CSI}?1004h`;
|
|
474
|
+
var disableFocusTracking = `${CSI}?1004l`;
|
|
431
475
|
var reset = `${CSI}0m`;
|
|
432
476
|
var bold = `${CSI}1m`;
|
|
433
477
|
var dim = `${CSI}2m`;
|
|
@@ -450,10 +494,52 @@ var resetScrollRegion = `${CSI}r`;
|
|
|
450
494
|
function setTitle(title) {
|
|
451
495
|
return `${OSC}0;${title}\x07`;
|
|
452
496
|
}
|
|
497
|
+
function hyperlinkOpen(url) {
|
|
498
|
+
if (!/^(https?|file):\/\//i.test(url)) return "";
|
|
499
|
+
const safeUrl = url.replace(/[\u0000-\u001F\u007F-\u009F\u001B]/g, "");
|
|
500
|
+
return `\x1B]8;;${safeUrl}\x1B\\`;
|
|
501
|
+
}
|
|
502
|
+
var hyperlinkClose = "\x1B]8;;\x1B\\";
|
|
503
|
+
var bell = "\x07";
|
|
504
|
+
function notify(text) {
|
|
505
|
+
const safeText = text.replace(/[\u0000-\u001F\u007F-\u009F\u001B]/g, "");
|
|
506
|
+
return `${OSC}9;${safeText}${bell}`;
|
|
507
|
+
}
|
|
508
|
+
function stripAnsiControl(str) {
|
|
509
|
+
let out = str.replace(
|
|
510
|
+
/\x1b(?:[@-Z\\-_]|\[[0-9;]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[PX^_][^\x1b]*\x1b\\|.)/g,
|
|
511
|
+
""
|
|
512
|
+
);
|
|
513
|
+
out = out.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
453
516
|
function writeClipboard(text, stdout = process.stdout) {
|
|
454
517
|
const encoded = Buffer.from(text, "utf8").toString("base64");
|
|
455
518
|
stdout.write(`${OSC}52;c;${encoded}\x07`);
|
|
456
519
|
}
|
|
520
|
+
function readClipboard(stdin = process.stdin, stdout = process.stdout) {
|
|
521
|
+
return new Promise((resolve, reject) => {
|
|
522
|
+
const handler = (data) => {
|
|
523
|
+
const str = data.toString("utf8");
|
|
524
|
+
const match = str.match(/\x1b\]52;c;([^\x07]+)\x07/);
|
|
525
|
+
if (!match) return;
|
|
526
|
+
stdin.off("data", handler);
|
|
527
|
+
try {
|
|
528
|
+
resolve(
|
|
529
|
+
Buffer.from(match[1], "base64").toString("utf8")
|
|
530
|
+
);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
reject(err);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
stdin.on("data", handler);
|
|
536
|
+
stdout.write(`${OSC}52;c;?\x07`);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
var clipboard = {
|
|
540
|
+
write: writeClipboard,
|
|
541
|
+
read: readClipboard
|
|
542
|
+
};
|
|
457
543
|
|
|
458
544
|
// src/terminal/Terminal.ts
|
|
459
545
|
var Terminal = class {
|
|
@@ -465,32 +551,55 @@ var Terminal = class {
|
|
|
465
551
|
_isRawMode = false;
|
|
466
552
|
_isAltScreen = false;
|
|
467
553
|
_isMouseEnabled = false;
|
|
554
|
+
_isBracketedPasteEnabled = false;
|
|
468
555
|
_resizeHandlers = [];
|
|
469
556
|
_cleanupHandlers = [];
|
|
470
557
|
_originalRawMode;
|
|
558
|
+
// Debounce state properties
|
|
559
|
+
_resizeDebounceMs;
|
|
560
|
+
_resizeTimer = null;
|
|
561
|
+
_lastDispatchedCols;
|
|
562
|
+
_lastDispatchedRows;
|
|
471
563
|
// Stored handler references for proper cleanup
|
|
472
564
|
_resizeHandler = null;
|
|
473
565
|
_exitHandler = null;
|
|
474
|
-
_sigintHandler = null;
|
|
475
|
-
_sigtermHandler = null;
|
|
476
|
-
_uncaughtExceptionHandler = null;
|
|
477
|
-
_unhandledRejectionHandler = null;
|
|
478
566
|
_restored = false;
|
|
567
|
+
_restoring = false;
|
|
568
|
+
// Stream write queue state to prevent interleaving backpressure fragmentation
|
|
569
|
+
_writeQueue = [];
|
|
570
|
+
_isWriting = false;
|
|
479
571
|
constructor(options = {}) {
|
|
480
572
|
this.stdout = options.stdout ?? process.stdout;
|
|
481
573
|
this.stdin = options.stdin ?? process.stdin;
|
|
482
574
|
this.colorDepth = options.colorDepth ?? detectColorDepth();
|
|
483
575
|
this._cols = this.stdout.columns ?? 80;
|
|
484
576
|
this._rows = this.stdout.rows ?? 24;
|
|
577
|
+
this._resizeDebounceMs = options.resizeDebounceMs ?? 16;
|
|
578
|
+
this._lastDispatchedCols = this._cols;
|
|
579
|
+
this._lastDispatchedRows = this._rows;
|
|
485
580
|
this._resizeHandler = () => {
|
|
486
581
|
this._cols = this.stdout.columns ?? 80;
|
|
487
582
|
this._rows = this.stdout.rows ?? 24;
|
|
488
|
-
|
|
489
|
-
|
|
583
|
+
if (this._resizeTimer) {
|
|
584
|
+
clearTimeout(this._resizeTimer);
|
|
490
585
|
}
|
|
586
|
+
this._resizeTimer = setTimeout(() => {
|
|
587
|
+
this._resizeTimer = null;
|
|
588
|
+
if (this._cols !== this._lastDispatchedCols || this._rows !== this._lastDispatchedRows) {
|
|
589
|
+
this._lastDispatchedCols = this._cols;
|
|
590
|
+
this._lastDispatchedRows = this._rows;
|
|
591
|
+
const handlers = [...this._resizeHandlers];
|
|
592
|
+
for (const handler of handlers) {
|
|
593
|
+
handler(this._cols, this._rows);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}, this._resizeDebounceMs);
|
|
491
597
|
};
|
|
492
598
|
this.stdout.on("resize", this._resizeHandler);
|
|
493
599
|
this._setupCleanup();
|
|
600
|
+
if (options.bracketedPaste) {
|
|
601
|
+
this.enableBracketedPaste();
|
|
602
|
+
}
|
|
494
603
|
}
|
|
495
604
|
/** Current terminal width in columns */
|
|
496
605
|
get cols() {
|
|
@@ -544,6 +653,18 @@ var Terminal = class {
|
|
|
544
653
|
this.write(disableMouse);
|
|
545
654
|
this._isMouseEnabled = false;
|
|
546
655
|
}
|
|
656
|
+
/** Emit the enable sequence (CSI ?2004h). Idempotent. */
|
|
657
|
+
enableBracketedPaste() {
|
|
658
|
+
if (this._isBracketedPasteEnabled) return;
|
|
659
|
+
this.write(enableBracketedPaste);
|
|
660
|
+
this._isBracketedPasteEnabled = true;
|
|
661
|
+
}
|
|
662
|
+
/** Emit the disable sequence (CSI ?2004l). Idempotent. */
|
|
663
|
+
disableBracketedPaste() {
|
|
664
|
+
if (!this._isBracketedPasteEnabled) return;
|
|
665
|
+
this.write(disableBracketedPaste);
|
|
666
|
+
this._isBracketedPasteEnabled = false;
|
|
667
|
+
}
|
|
547
668
|
// ── Cursor ──────────────────────────────────────────
|
|
548
669
|
hideCursor() {
|
|
549
670
|
this.write(hideCursor);
|
|
@@ -551,10 +672,71 @@ var Terminal = class {
|
|
|
551
672
|
showCursor() {
|
|
552
673
|
this.write(showCursor);
|
|
553
674
|
}
|
|
675
|
+
/** Set the cursor shape via DECSCUSR. Default blink = true. */
|
|
676
|
+
setCursorShape(shape, blink2) {
|
|
677
|
+
this.write(cursorShape(shape, blink2));
|
|
678
|
+
}
|
|
679
|
+
/** Ring the terminal bell (BEL). */
|
|
680
|
+
bell() {
|
|
681
|
+
this.write(bell);
|
|
682
|
+
}
|
|
683
|
+
/** Send an OSC 9 desktop notification. Body is appended after a separator. */
|
|
684
|
+
notify(title, body) {
|
|
685
|
+
const payload = body === void 0 ? title : `${title}: ${body}`;
|
|
686
|
+
this.write(notify(payload));
|
|
687
|
+
}
|
|
554
688
|
// ── Output ──────────────────────────────────────────
|
|
689
|
+
/**
|
|
690
|
+
* Writes chunked string data to stdout.
|
|
691
|
+
* Enforces queue serialization to ensure atomic ANSI escape execution.
|
|
692
|
+
*/
|
|
555
693
|
write(data) {
|
|
694
|
+
if (!data) return;
|
|
695
|
+
this._writeQueue.push(data);
|
|
696
|
+
if (this._isWriting) return;
|
|
697
|
+
this._processWriteQueue();
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Writes data to stdout synchronously, bypassing the write queue.
|
|
701
|
+
* Used by the renderer during frame flush to avoid races with the
|
|
702
|
+
* async queue lifecycle. Only use for render-path output.
|
|
703
|
+
*/
|
|
704
|
+
writeSync(data) {
|
|
705
|
+
if (!data) return;
|
|
556
706
|
this.stdout.write(data);
|
|
557
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* Sequentially unshifts and drains string frames to stdout safely.
|
|
710
|
+
*/
|
|
711
|
+
_processWriteQueue() {
|
|
712
|
+
if (this._writeQueue.length === 0) {
|
|
713
|
+
this._isWriting = false;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this._isWriting = true;
|
|
717
|
+
const chunk = this._writeQueue.shift();
|
|
718
|
+
const canContinue = this.stdout.write(chunk);
|
|
719
|
+
if (!canContinue) {
|
|
720
|
+
this.stdout.once("drain", () => {
|
|
721
|
+
this._processWriteQueue();
|
|
722
|
+
});
|
|
723
|
+
} else {
|
|
724
|
+
this._processWriteQueue();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// ── Clipboard ───────────────────────────────────────
|
|
728
|
+
/**
|
|
729
|
+
* Read text from the system clipboard via OSC 52.
|
|
730
|
+
*/
|
|
731
|
+
readClipboard() {
|
|
732
|
+
return readClipboard(this.stdin, this.stdout);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Write text to the system clipboard via OSC 52.
|
|
736
|
+
*/
|
|
737
|
+
writeClipboard(text) {
|
|
738
|
+
writeClipboard(text, this.stdout);
|
|
739
|
+
}
|
|
558
740
|
// ── Resize ──────────────────────────────────────────
|
|
559
741
|
onResize(handler) {
|
|
560
742
|
this._resizeHandlers.push(handler);
|
|
@@ -570,27 +752,35 @@ var Terminal = class {
|
|
|
570
752
|
* Called automatically on SIGINT, SIGTERM, process exit.
|
|
571
753
|
*/
|
|
572
754
|
restore() {
|
|
573
|
-
if (this._restored) return;
|
|
574
|
-
this.
|
|
575
|
-
if (this.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (this._uncaughtExceptionHandler) {
|
|
579
|
-
process.off("uncaughtException", this._uncaughtExceptionHandler);
|
|
580
|
-
this._uncaughtExceptionHandler = null;
|
|
581
|
-
}
|
|
582
|
-
if (this._unhandledRejectionHandler) {
|
|
583
|
-
process.off("unhandledRejection", this._unhandledRejectionHandler);
|
|
584
|
-
this._unhandledRejectionHandler = null;
|
|
755
|
+
if (this._restored || this._restoring) return;
|
|
756
|
+
this._restoring = true;
|
|
757
|
+
if (this._resizeTimer) {
|
|
758
|
+
clearTimeout(this._resizeTimer);
|
|
759
|
+
this._resizeTimer = null;
|
|
585
760
|
}
|
|
761
|
+
this._writeQueue = [];
|
|
762
|
+
this._isWriting = false;
|
|
763
|
+
if (this._exitHandler) process.off("exit", this._exitHandler);
|
|
586
764
|
if (this._resizeHandler) {
|
|
587
765
|
this.stdout.off("resize", this._resizeHandler);
|
|
588
766
|
}
|
|
589
|
-
this.
|
|
590
|
-
this.
|
|
591
|
-
this.
|
|
592
|
-
|
|
593
|
-
|
|
767
|
+
const directWrite = this.stdout.write.bind(this.stdout);
|
|
768
|
+
const savedWrite = this.write;
|
|
769
|
+
this.write = (s) => {
|
|
770
|
+
directWrite(s);
|
|
771
|
+
};
|
|
772
|
+
try {
|
|
773
|
+
this.disableBracketedPaste();
|
|
774
|
+
this.disableMouse();
|
|
775
|
+
this.exitAltScreen();
|
|
776
|
+
this.exitRawMode();
|
|
777
|
+
this.showCursor();
|
|
778
|
+
this.write(reset);
|
|
779
|
+
this._restored = true;
|
|
780
|
+
} finally {
|
|
781
|
+
this.write = savedWrite;
|
|
782
|
+
this._restoring = false;
|
|
783
|
+
}
|
|
594
784
|
}
|
|
595
785
|
/**
|
|
596
786
|
* Register a custom cleanup handler that runs on terminal restore.
|
|
@@ -600,7 +790,8 @@ var Terminal = class {
|
|
|
600
790
|
}
|
|
601
791
|
_setupCleanup() {
|
|
602
792
|
const runCleanupHandlers = () => {
|
|
603
|
-
|
|
793
|
+
const handlers = [...this._cleanupHandlers];
|
|
794
|
+
for (const handler of handlers) {
|
|
604
795
|
try {
|
|
605
796
|
handler();
|
|
606
797
|
} catch {
|
|
@@ -609,30 +800,209 @@ var Terminal = class {
|
|
|
609
800
|
this.restore();
|
|
610
801
|
};
|
|
611
802
|
this._exitHandler = runCleanupHandlers;
|
|
612
|
-
this._sigintHandler = () => {
|
|
613
|
-
runCleanupHandlers();
|
|
614
|
-
process.exit(130);
|
|
615
|
-
};
|
|
616
|
-
this._sigtermHandler = () => {
|
|
617
|
-
runCleanupHandlers();
|
|
618
|
-
process.exit(143);
|
|
619
|
-
};
|
|
620
803
|
process.on("exit", this._exitHandler);
|
|
621
|
-
process.on("SIGINT", this._sigintHandler);
|
|
622
|
-
process.on("SIGTERM", this._sigtermHandler);
|
|
623
|
-
this._uncaughtExceptionHandler = (err) => {
|
|
624
|
-
this.restore();
|
|
625
|
-
process.exit(1);
|
|
626
|
-
};
|
|
627
|
-
this._unhandledRejectionHandler = () => {
|
|
628
|
-
this.restore();
|
|
629
|
-
process.exit(1);
|
|
630
|
-
};
|
|
631
|
-
process.on("uncaughtException", this._uncaughtExceptionHandler);
|
|
632
|
-
process.on("unhandledRejection", this._unhandledRejectionHandler);
|
|
633
804
|
}
|
|
634
805
|
};
|
|
635
806
|
|
|
807
|
+
// src/utils/unicode.ts
|
|
808
|
+
function isWideChar(codePoint) {
|
|
809
|
+
return (
|
|
810
|
+
// CJK Unified Ideographs (common Chinese/Japanese/Korean)
|
|
811
|
+
codePoint >= 19968 && codePoint <= 40959 || // CJK Unified Ideographs Extension A
|
|
812
|
+
codePoint >= 13312 && codePoint <= 19903 || // CJK Compatibility Ideographs
|
|
813
|
+
codePoint >= 63744 && codePoint <= 64255 || // Hangul Syllables
|
|
814
|
+
codePoint >= 44032 && codePoint <= 55215 || // Katakana
|
|
815
|
+
codePoint >= 12448 && codePoint <= 12543 || // CJK Symbols and Punctuation
|
|
816
|
+
codePoint >= 12288 && codePoint <= 12351 || // Hiragana
|
|
817
|
+
codePoint >= 12352 && codePoint <= 12447 || // Fullwidth Forms
|
|
818
|
+
codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || // CJK Unified Ideographs Extension B
|
|
819
|
+
codePoint >= 131072 && codePoint <= 173791 || // CJK Unified Ideographs Extension C,D,E,F
|
|
820
|
+
codePoint >= 173824 && codePoint <= 191471 || // CJK Compatibility Ideographs Supplement
|
|
821
|
+
codePoint >= 194560 && codePoint <= 195103
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
function isCombining(codePoint) {
|
|
825
|
+
return (
|
|
826
|
+
// Combining Diacritical Marks
|
|
827
|
+
codePoint >= 768 && codePoint <= 879 || // Combining Diacritical Marks Extended
|
|
828
|
+
codePoint >= 6832 && codePoint <= 6911 || // Combining Diacritical Marks Supplement
|
|
829
|
+
codePoint >= 7616 && codePoint <= 7679 || // Combining Diacritical Marks for Symbols
|
|
830
|
+
codePoint >= 8400 && codePoint <= 8447 || // Combining Half Marks
|
|
831
|
+
codePoint >= 65056 && codePoint <= 65071 || // Variation selectors
|
|
832
|
+
codePoint >= 65024 && codePoint <= 65039 || // Zero-width joiner / non-joiner
|
|
833
|
+
codePoint === 8203 || codePoint === 8204 || codePoint === 8205 || codePoint === 65279
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
function isEmoji(codePoint) {
|
|
837
|
+
return (
|
|
838
|
+
// Emoticons
|
|
839
|
+
codePoint >= 128512 && codePoint <= 128591 || // Misc Symbols and Pictographs
|
|
840
|
+
codePoint >= 127744 && codePoint <= 128511 || // Transport and Map
|
|
841
|
+
codePoint >= 128640 && codePoint <= 128767 || // Supplemental Symbols
|
|
842
|
+
codePoint >= 129280 && codePoint <= 129535 || // Misc symbols
|
|
843
|
+
codePoint >= 9728 && codePoint <= 9983 || // Dingbats
|
|
844
|
+
codePoint >= 9984 && codePoint <= 10175 || // Flags
|
|
845
|
+
codePoint >= 127456 && codePoint <= 127487
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
var segmenter = new Intl.Segmenter();
|
|
849
|
+
function segmentWidth(segment) {
|
|
850
|
+
const cp = segment.codePointAt(0);
|
|
851
|
+
if (cp < 32 || cp >= 127 && cp < 160) {
|
|
852
|
+
return 0;
|
|
853
|
+
}
|
|
854
|
+
if (isCombining(cp)) {
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
const charCount = [...segment].length;
|
|
858
|
+
let isMultiCpWide = false;
|
|
859
|
+
if (charCount > 1) {
|
|
860
|
+
const cps = [...segment].map((c) => c.codePointAt(0));
|
|
861
|
+
isMultiCpWide = cps.slice(1).some((c) => !isCombining(c));
|
|
862
|
+
}
|
|
863
|
+
if (isWideChar(cp) || isEmoji(cp) || isMultiCpWide) {
|
|
864
|
+
return 2;
|
|
865
|
+
}
|
|
866
|
+
return 1;
|
|
867
|
+
}
|
|
868
|
+
function stringWidth(str) {
|
|
869
|
+
let width = 0;
|
|
870
|
+
let inEscape = false;
|
|
871
|
+
const segments = segmenter.segment(str);
|
|
872
|
+
for (const { segment } of segments) {
|
|
873
|
+
const cp = segment.codePointAt(0);
|
|
874
|
+
if (cp === 27) {
|
|
875
|
+
inEscape = true;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (inEscape) {
|
|
879
|
+
if (cp >= 64 && cp <= 126 && cp !== 91) {
|
|
880
|
+
inEscape = false;
|
|
881
|
+
}
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
width += segmentWidth(segment);
|
|
885
|
+
}
|
|
886
|
+
return width;
|
|
887
|
+
}
|
|
888
|
+
function truncate(str, maxWidth, ellipsis = "\u2026") {
|
|
889
|
+
if (maxWidth <= 0) return "";
|
|
890
|
+
const strW = stringWidth(str);
|
|
891
|
+
if (strW <= maxWidth) return str;
|
|
892
|
+
const ellipsisW = stringWidth(ellipsis);
|
|
893
|
+
const targetW = maxWidth - ellipsisW;
|
|
894
|
+
if (targetW <= 0) return ellipsis.slice(0, maxWidth);
|
|
895
|
+
let width = 0;
|
|
896
|
+
let result = "";
|
|
897
|
+
let inEscape = false;
|
|
898
|
+
let escapeBuffer = "";
|
|
899
|
+
const segments = segmenter.segment(str);
|
|
900
|
+
for (const { segment } of segments) {
|
|
901
|
+
const cp = segment.codePointAt(0);
|
|
902
|
+
if (cp === 27) {
|
|
903
|
+
inEscape = true;
|
|
904
|
+
escapeBuffer += segment;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (inEscape) {
|
|
908
|
+
escapeBuffer += segment;
|
|
909
|
+
if (cp >= 64 && cp <= 126 && cp !== 91) {
|
|
910
|
+
inEscape = false;
|
|
911
|
+
result += escapeBuffer;
|
|
912
|
+
escapeBuffer = "";
|
|
913
|
+
}
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
let charW = segmentWidth(segment);
|
|
917
|
+
if (width + charW > targetW) break;
|
|
918
|
+
width += charW;
|
|
919
|
+
result += segment;
|
|
920
|
+
}
|
|
921
|
+
return result + ellipsis;
|
|
922
|
+
}
|
|
923
|
+
function stripAnsi(str) {
|
|
924
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
925
|
+
}
|
|
926
|
+
function wordWrap(str, width) {
|
|
927
|
+
if (width <= 0) return str;
|
|
928
|
+
const lines = str.split("\n");
|
|
929
|
+
const result = [];
|
|
930
|
+
for (const line of lines) {
|
|
931
|
+
if (stringWidth(line) <= width) {
|
|
932
|
+
result.push(line);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
let currentLine = "";
|
|
936
|
+
let currentWidth = 0;
|
|
937
|
+
const words = line.split(/(\s+)/);
|
|
938
|
+
for (const word of words) {
|
|
939
|
+
const wordW = stringWidth(word);
|
|
940
|
+
if (currentWidth + wordW <= width) {
|
|
941
|
+
currentLine += word;
|
|
942
|
+
currentWidth += wordW;
|
|
943
|
+
} else if (wordW > width) {
|
|
944
|
+
if (currentLine) {
|
|
945
|
+
result.push(currentLine);
|
|
946
|
+
currentLine = "";
|
|
947
|
+
currentWidth = 0;
|
|
948
|
+
}
|
|
949
|
+
const wordSegments = segmenter.segment(word);
|
|
950
|
+
for (const { segment } of wordSegments) {
|
|
951
|
+
const charW = segmentWidth(segment);
|
|
952
|
+
if (currentWidth + charW > width) {
|
|
953
|
+
result.push(currentLine);
|
|
954
|
+
currentLine = "";
|
|
955
|
+
currentWidth = 0;
|
|
956
|
+
}
|
|
957
|
+
currentLine += segment;
|
|
958
|
+
currentWidth += charW;
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
result.push(currentLine);
|
|
962
|
+
currentLine = word.trimStart();
|
|
963
|
+
currentWidth = stringWidth(currentLine);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (currentLine) {
|
|
967
|
+
result.push(currentLine);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return result.join("\n");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/terminal/env-caps.ts
|
|
974
|
+
var caps = {
|
|
975
|
+
color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
|
|
976
|
+
unicode: !process.env.NO_UNICODE && process.env.TERM !== "dumb",
|
|
977
|
+
motion: !process.env.NO_MOTION && !process.env.CI,
|
|
978
|
+
ci: !!process.env.CI,
|
|
979
|
+
get background() {
|
|
980
|
+
if (process.env.TERM_BACKGROUND === "light") return "light";
|
|
981
|
+
if (process.env.TERM_BACKGROUND === "dark") return "dark";
|
|
982
|
+
const colorfgbg = process.env.COLORFGBG;
|
|
983
|
+
if (colorfgbg) {
|
|
984
|
+
const parts = colorfgbg.split(";");
|
|
985
|
+
const bg = parseInt(parts[parts.length - 1], 10);
|
|
986
|
+
if (!Number.isNaN(bg)) return bg < 8 ? "dark" : "light";
|
|
987
|
+
}
|
|
988
|
+
return "dark";
|
|
989
|
+
},
|
|
990
|
+
get keybindingMode() {
|
|
991
|
+
const mode = process.env.TERMUI_KEYBINDINGS;
|
|
992
|
+
if (mode === "vim" || mode === "emacs") return mode;
|
|
993
|
+
return "default";
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
function prefersReducedMotion() {
|
|
997
|
+
return !caps.motion;
|
|
998
|
+
}
|
|
999
|
+
function shouldUseColor() {
|
|
1000
|
+
return caps.color;
|
|
1001
|
+
}
|
|
1002
|
+
function prefersHighContrast() {
|
|
1003
|
+
return process.env.HIGH_CONTRAST === "1";
|
|
1004
|
+
}
|
|
1005
|
+
|
|
636
1006
|
// src/terminal/Screen.ts
|
|
637
1007
|
var EMPTY_COLOR = Object.freeze({ type: "none" });
|
|
638
1008
|
function emptyCell() {
|
|
@@ -646,7 +1016,8 @@ function emptyCell() {
|
|
|
646
1016
|
dim: false,
|
|
647
1017
|
strikethrough: false,
|
|
648
1018
|
inverse: false,
|
|
649
|
-
width: 1
|
|
1019
|
+
width: 1,
|
|
1020
|
+
link: void 0
|
|
650
1021
|
};
|
|
651
1022
|
}
|
|
652
1023
|
function resetCell(cell) {
|
|
@@ -660,9 +1031,10 @@ function resetCell(cell) {
|
|
|
660
1031
|
cell.strikethrough = false;
|
|
661
1032
|
cell.inverse = false;
|
|
662
1033
|
cell.width = 1;
|
|
1034
|
+
cell.link = void 0;
|
|
663
1035
|
}
|
|
664
1036
|
function cellsEqual(a, b) {
|
|
665
|
-
return a.char === b.char && a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.dim === b.dim && a.strikethrough === b.strikethrough && a.inverse === b.inverse && a.width === b.width && colorsEqual(a.fg, b.fg) && colorsEqual(a.bg, b.bg);
|
|
1037
|
+
return a.char === b.char && a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.dim === b.dim && a.strikethrough === b.strikethrough && a.inverse === b.inverse && a.width === b.width && a.link === b.link && colorsEqual(a.fg, b.fg) && colorsEqual(a.bg, b.bg);
|
|
666
1038
|
}
|
|
667
1039
|
function colorsEqual(a, b) {
|
|
668
1040
|
if (a.type !== b.type) return false;
|
|
@@ -676,25 +1048,98 @@ function colorsEqual(a, b) {
|
|
|
676
1048
|
case "rgb":
|
|
677
1049
|
return a.r === b.r && a.g === b.g && a.b === b.b;
|
|
678
1050
|
case "hex":
|
|
679
|
-
return a.hex === b.hex;
|
|
1051
|
+
return a.hex.toLowerCase() === b.hex.toLowerCase();
|
|
680
1052
|
}
|
|
681
1053
|
}
|
|
682
1054
|
var Screen = class {
|
|
683
1055
|
_cols;
|
|
684
1056
|
_rows;
|
|
1057
|
+
_previousLines = [];
|
|
1058
|
+
_lastRenderedHeight = 0;
|
|
1059
|
+
get lastRenderedHeight() {
|
|
1060
|
+
return this._lastRenderedHeight;
|
|
1061
|
+
}
|
|
1062
|
+
set lastRenderedHeight(value) {
|
|
1063
|
+
this._lastRenderedHeight = value;
|
|
1064
|
+
}
|
|
1065
|
+
_previousStyleLines = [];
|
|
685
1066
|
front;
|
|
686
1067
|
back;
|
|
1068
|
+
/**
|
|
1069
|
+
* Render epoch counter. Incremented on every swap so downstream consumers
|
|
1070
|
+
* (e.g. Renderer._flush) can detect and skip stale frames from a previous
|
|
1071
|
+
* epoch, preventing double-swap corruption.
|
|
1072
|
+
*/
|
|
1073
|
+
_epoch = 0;
|
|
1074
|
+
/** True while swap() is executing to prevent re-entrant double-swap corruption. */
|
|
1075
|
+
_swapping = false;
|
|
1076
|
+
/** The epoch captured at the start of the current flush cycle. */
|
|
1077
|
+
_flushEpoch = -1;
|
|
687
1078
|
/**
|
|
688
1079
|
* Stack of clipping regions. When non-empty, setCell/writeString
|
|
689
1080
|
* only write to cells within the topmost clip rectangle.
|
|
690
1081
|
*/
|
|
691
1082
|
_clipStack = [];
|
|
1083
|
+
_translateYStack = [];
|
|
1084
|
+
_translateY = 0;
|
|
692
1085
|
constructor(cols, rows) {
|
|
693
1086
|
this._cols = cols;
|
|
694
1087
|
this._rows = rows;
|
|
695
1088
|
this.front = this._createGrid(cols, rows);
|
|
696
1089
|
this.back = this._createGrid(cols, rows);
|
|
697
1090
|
}
|
|
1091
|
+
/** Retrieve a read-only copy of the cell at (x, y) from the back buffer. */
|
|
1092
|
+
getCell(x, y) {
|
|
1093
|
+
x = Math.floor(x);
|
|
1094
|
+
y = Math.floor(y);
|
|
1095
|
+
if (!(x >= 0 && x < this._cols && y >= 0 && y < this._rows)) return void 0;
|
|
1096
|
+
return this.back[y][x];
|
|
1097
|
+
}
|
|
1098
|
+
/** Serialize a back-buffer row to a plain string (skips continuation cells). */
|
|
1099
|
+
getLine(row) {
|
|
1100
|
+
if (row < 0 || row >= this._rows) return "";
|
|
1101
|
+
return this.back[row].filter((cell) => cell.width !== 0).map((cell) => cell.char || " ").join("");
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Serialize the style attributes of a back-buffer row into a
|
|
1105
|
+
* fingerprint string. When the characters are identical but the
|
|
1106
|
+
* styles differ (color, bold, italic, etc.), this fingerprint
|
|
1107
|
+
* changes, allowing the diff renderer to detect style-only updates.
|
|
1108
|
+
*/
|
|
1109
|
+
getStyleLine(row) {
|
|
1110
|
+
if (row < 0 || row >= this._rows) return "";
|
|
1111
|
+
let hash = 0;
|
|
1112
|
+
for (const cell of this.back[row]) {
|
|
1113
|
+
if (cell.width === 0) continue;
|
|
1114
|
+
const fg = cell.fg.type;
|
|
1115
|
+
const bg = cell.bg.type;
|
|
1116
|
+
const bits = (cell.bold ? 1 : 0) | (cell.italic ? 2 : 0) | (cell.underline ? 4 : 0) | (cell.dim ? 8 : 0) | (cell.strikethrough ? 16 : 0) | (cell.inverse ? 32 : 0);
|
|
1117
|
+
const seed = fg.charCodeAt(0) * 65536 + bg.charCodeAt(0) * 4096 + bits;
|
|
1118
|
+
hash = (hash << 7) - hash + seed | 0;
|
|
1119
|
+
if (cell.link) {
|
|
1120
|
+
for (let i = 0; i < cell.link.length; i++)
|
|
1121
|
+
hash = (hash << 5) - hash + cell.link.charCodeAt(i) | 0;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return String(hash);
|
|
1125
|
+
}
|
|
1126
|
+
/** Return the saved line string for the given row (empty before first saveLines call). */
|
|
1127
|
+
getPreviousLine(row) {
|
|
1128
|
+
return this._previousLines[row] ?? "";
|
|
1129
|
+
}
|
|
1130
|
+
/** Return the saved style fingerprint for the given row. */
|
|
1131
|
+
getPreviousStyleLine(row) {
|
|
1132
|
+
return this._previousStyleLines[row] ?? "";
|
|
1133
|
+
}
|
|
1134
|
+
/** Snapshot the current back-buffer line strings for use by diffRenderer. */
|
|
1135
|
+
saveLines() {
|
|
1136
|
+
this._previousLines = [];
|
|
1137
|
+
this._previousStyleLines = [];
|
|
1138
|
+
for (let r = 0; r < this._rows; r++) {
|
|
1139
|
+
this._previousLines.push(this.getLine(r));
|
|
1140
|
+
this._previousStyleLines.push(this.getStyleLine(r));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
698
1143
|
get cols() {
|
|
699
1144
|
return this._cols;
|
|
700
1145
|
}
|
|
@@ -735,12 +1180,21 @@ var Screen = class {
|
|
|
735
1180
|
get activeClip() {
|
|
736
1181
|
return this._clipStack.length > 0 ? this._clipStack[this._clipStack.length - 1] : null;
|
|
737
1182
|
}
|
|
1183
|
+
pushTranslateY(offset) {
|
|
1184
|
+
this._translateYStack.push(offset);
|
|
1185
|
+
this._translateY += offset;
|
|
1186
|
+
}
|
|
1187
|
+
popTranslateY() {
|
|
1188
|
+
const offset = this._translateYStack.pop() ?? 0;
|
|
1189
|
+
this._translateY -= offset;
|
|
1190
|
+
}
|
|
738
1191
|
/**
|
|
739
1192
|
* Write a cell to the back buffer at position (col, row).
|
|
740
1193
|
*/
|
|
741
1194
|
setCell(col, row, cell) {
|
|
742
1195
|
col = Math.floor(col);
|
|
743
1196
|
row = Math.floor(row);
|
|
1197
|
+
row += this._translateY;
|
|
744
1198
|
if (!(col >= 0 && col < this._cols && row >= 0 && row < this._rows)) return;
|
|
745
1199
|
if (this._clipStack.length > 0) {
|
|
746
1200
|
const clip = this._clipStack[this._clipStack.length - 1];
|
|
@@ -749,6 +1203,9 @@ var Screen = class {
|
|
|
749
1203
|
}
|
|
750
1204
|
}
|
|
751
1205
|
const existing = this.back[row][col];
|
|
1206
|
+
if (cell.char !== void 0) {
|
|
1207
|
+
cell = { ...cell, char: stripAnsiControl(cell.char) };
|
|
1208
|
+
}
|
|
752
1209
|
Object.assign(existing, cell);
|
|
753
1210
|
}
|
|
754
1211
|
/**
|
|
@@ -759,31 +1216,37 @@ var Screen = class {
|
|
|
759
1216
|
row = Math.floor(row);
|
|
760
1217
|
col = Math.floor(col);
|
|
761
1218
|
if (!(row >= 0 && row < this._rows)) return;
|
|
1219
|
+
const safeStr = stripAnsiControl(str);
|
|
762
1220
|
let x = col;
|
|
763
|
-
|
|
1221
|
+
const segments = segmenter.segment(safeStr);
|
|
1222
|
+
for (const { segment } of segments) {
|
|
764
1223
|
if (x >= this._cols) break;
|
|
1224
|
+
let finalChar = segment;
|
|
1225
|
+
let width = stringWidth(segment);
|
|
765
1226
|
if (x < 0) {
|
|
766
|
-
x
|
|
1227
|
+
x += width;
|
|
767
1228
|
continue;
|
|
768
1229
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1230
|
+
if (width > 1 && !caps.unicode) {
|
|
1231
|
+
finalChar = "*";
|
|
1232
|
+
width = 1;
|
|
1233
|
+
}
|
|
1234
|
+
if (width === 0) continue;
|
|
772
1235
|
this.setCell(x, row, {
|
|
773
|
-
char,
|
|
1236
|
+
char: finalChar,
|
|
774
1237
|
width,
|
|
775
1238
|
...style
|
|
776
1239
|
});
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
x += 1;
|
|
1240
|
+
for (let i = 1; i < width; i++) {
|
|
1241
|
+
if (x + i < this._cols) {
|
|
1242
|
+
this.setCell(x + i, row, {
|
|
1243
|
+
char: "",
|
|
1244
|
+
width: 0,
|
|
1245
|
+
...style
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
786
1248
|
}
|
|
1249
|
+
x += width;
|
|
787
1250
|
}
|
|
788
1251
|
}
|
|
789
1252
|
/**
|
|
@@ -797,13 +1260,34 @@ var Screen = class {
|
|
|
797
1260
|
}
|
|
798
1261
|
}
|
|
799
1262
|
}
|
|
1263
|
+
/** Current render epoch — incremented after each swap. */
|
|
1264
|
+
get epoch() {
|
|
1265
|
+
return this._epoch;
|
|
1266
|
+
}
|
|
1267
|
+
/** The epoch captured at the start of the current flush cycle. */
|
|
1268
|
+
get flushEpoch() {
|
|
1269
|
+
return this._flushEpoch;
|
|
1270
|
+
}
|
|
1271
|
+
set flushEpoch(value) {
|
|
1272
|
+
this._flushEpoch = value;
|
|
1273
|
+
}
|
|
800
1274
|
/**
|
|
801
1275
|
* Swap front and back buffers. Called after rendering diffs.
|
|
1276
|
+
* Uses mutual exclusion to prevent double-swap corruption when
|
|
1277
|
+
* _flush() is called concurrently (e.g. from duplicate setImmediate
|
|
1278
|
+
* callbacks).
|
|
802
1279
|
*/
|
|
803
1280
|
swap() {
|
|
804
|
-
|
|
805
|
-
this.
|
|
806
|
-
|
|
1281
|
+
if (this._swapping) return;
|
|
1282
|
+
this._swapping = true;
|
|
1283
|
+
try {
|
|
1284
|
+
const temp = this.front;
|
|
1285
|
+
this.front = this.back;
|
|
1286
|
+
this.back = temp;
|
|
1287
|
+
this._epoch++;
|
|
1288
|
+
} finally {
|
|
1289
|
+
this._swapping = false;
|
|
1290
|
+
}
|
|
807
1291
|
}
|
|
808
1292
|
/**
|
|
809
1293
|
* Resize the screen. Clears both buffers.
|
|
@@ -813,17 +1297,43 @@ var Screen = class {
|
|
|
813
1297
|
this._rows = rows;
|
|
814
1298
|
this.front = this._createGrid(cols, rows);
|
|
815
1299
|
this.back = this._createGrid(cols, rows);
|
|
1300
|
+
this._previousLines = [];
|
|
816
1301
|
}
|
|
817
1302
|
/**
|
|
818
1303
|
* Clear the front buffer (marks everything as "needs redraw").
|
|
1304
|
+
* Mutates cells in-place to avoid GC pressure from object allocation.
|
|
819
1305
|
*/
|
|
820
1306
|
invalidate() {
|
|
821
1307
|
for (let r = 0; r < this._rows; r++) {
|
|
822
1308
|
for (let c = 0; c < this._cols; c++) {
|
|
823
|
-
this.front[r][c]
|
|
1309
|
+
resetCell(this.front[r][c]);
|
|
1310
|
+
this.front[r][c].char = "\0";
|
|
824
1311
|
}
|
|
825
1312
|
}
|
|
826
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Export current screen as ANSI snapshot text.
|
|
1316
|
+
*/
|
|
1317
|
+
exportANSI() {
|
|
1318
|
+
const lines = [];
|
|
1319
|
+
for (let r = 0; r < this._rows; r++) {
|
|
1320
|
+
lines.push(this.getLine(r));
|
|
1321
|
+
}
|
|
1322
|
+
return lines.join("\n");
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Export current screen as SVG.
|
|
1326
|
+
*/
|
|
1327
|
+
exportSVG() {
|
|
1328
|
+
return `
|
|
1329
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
1330
|
+
width="${this._cols * 8}"
|
|
1331
|
+
height="${this._rows * 16}">
|
|
1332
|
+
<text x="10" y="20">
|
|
1333
|
+
Terminal Export
|
|
1334
|
+
</text>
|
|
1335
|
+
</svg>`;
|
|
1336
|
+
}
|
|
827
1337
|
_createGrid(cols, rows) {
|
|
828
1338
|
const grid = [];
|
|
829
1339
|
for (let r = 0; r < rows; r++) {
|
|
@@ -835,25 +1345,74 @@ var Screen = class {
|
|
|
835
1345
|
}
|
|
836
1346
|
return grid;
|
|
837
1347
|
}
|
|
838
|
-
|
|
839
|
-
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// src/renderer/render-hook.ts
|
|
1351
|
+
var RenderHook = class {
|
|
1352
|
+
_buffer = [];
|
|
1353
|
+
_isActive = false;
|
|
1354
|
+
_originalConsole = {};
|
|
1355
|
+
// any[]: console methods accept arbitrary argument shapes
|
|
1356
|
+
/** Check if the hook is currently intercepting console output */
|
|
1357
|
+
get isActive() {
|
|
1358
|
+
return this._isActive;
|
|
1359
|
+
}
|
|
1360
|
+
/** Wrap console.log/warn/error to buffer external logs instead of writing to stdout */
|
|
1361
|
+
start() {
|
|
1362
|
+
if (this._isActive) return;
|
|
1363
|
+
this._isActive = true;
|
|
1364
|
+
const methods = ["log", "warn", "error"];
|
|
1365
|
+
for (const method of methods) {
|
|
1366
|
+
this._originalConsole[method] = console[method];
|
|
1367
|
+
const hook = this;
|
|
1368
|
+
console[method] = function(...args) {
|
|
1369
|
+
const text = args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
|
|
1370
|
+
hook._buffer.push(text + "\n");
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
/** Restore original console methods */
|
|
1375
|
+
stop() {
|
|
1376
|
+
if (!this._isActive) return;
|
|
1377
|
+
this._isActive = false;
|
|
1378
|
+
for (const [method, original] of Object.entries(this._originalConsole)) {
|
|
1379
|
+
console[method] = original;
|
|
1380
|
+
}
|
|
1381
|
+
this._originalConsole = {};
|
|
1382
|
+
}
|
|
1383
|
+
/** Retrieve and clear the buffered logs */
|
|
1384
|
+
flush() {
|
|
1385
|
+
if (this._buffer.length === 0) return "";
|
|
1386
|
+
const out = this._buffer.join("");
|
|
1387
|
+
this._buffer = [];
|
|
1388
|
+
return out;
|
|
1389
|
+
}
|
|
1390
|
+
/** Write directly to process.stdout, bypassing any buffering */
|
|
1391
|
+
writeRaw(text) {
|
|
1392
|
+
process.stdout.write(text);
|
|
840
1393
|
}
|
|
841
1394
|
};
|
|
842
1395
|
|
|
843
1396
|
// src/terminal/Renderer.ts
|
|
844
|
-
var Renderer = class {
|
|
1397
|
+
var Renderer = class _Renderer {
|
|
845
1398
|
_terminal;
|
|
846
1399
|
_screen;
|
|
847
1400
|
_fps;
|
|
848
1401
|
_frameTimer = null;
|
|
849
1402
|
_renderRequested = false;
|
|
850
1403
|
_colorDepth;
|
|
1404
|
+
_diffRenderer;
|
|
851
1405
|
_onTick = null;
|
|
852
|
-
|
|
1406
|
+
_callbacks = /* @__PURE__ */ new Set();
|
|
1407
|
+
/** The stdout interceptor hook for buffering external logs */
|
|
1408
|
+
hook;
|
|
1409
|
+
constructor(terminal, screen, fps = 30, diffRenderer = true) {
|
|
853
1410
|
this._terminal = terminal;
|
|
854
1411
|
this._screen = screen;
|
|
855
1412
|
this._fps = fps;
|
|
856
1413
|
this._colorDepth = terminal.colorDepth;
|
|
1414
|
+
this._diffRenderer = diffRenderer;
|
|
1415
|
+
this.hook = new RenderHook();
|
|
857
1416
|
}
|
|
858
1417
|
/** Change the rendering frame rate cap */
|
|
859
1418
|
setFPS(fps) {
|
|
@@ -870,10 +1429,6 @@ var Renderer = class {
|
|
|
870
1429
|
const interval = Math.floor(1e3 / this._fps);
|
|
871
1430
|
this._frameTimer = setInterval(() => {
|
|
872
1431
|
this._onTick?.();
|
|
873
|
-
if (this._renderRequested) {
|
|
874
|
-
this._renderRequested = false;
|
|
875
|
-
this._flush();
|
|
876
|
-
}
|
|
877
1432
|
}, interval);
|
|
878
1433
|
}
|
|
879
1434
|
/** Stop the render loop */
|
|
@@ -891,6 +1446,13 @@ var Renderer = class {
|
|
|
891
1446
|
renderNow() {
|
|
892
1447
|
this._flush();
|
|
893
1448
|
}
|
|
1449
|
+
/** Register a per-frame profiling callback. Returns an unsubscribe function. */
|
|
1450
|
+
onFrame(cb) {
|
|
1451
|
+
this._callbacks.add(cb);
|
|
1452
|
+
return () => {
|
|
1453
|
+
this._callbacks.delete(cb);
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
894
1456
|
/**
|
|
895
1457
|
* Full-screen clear and redraw (first render or after resize).
|
|
896
1458
|
*/
|
|
@@ -898,51 +1460,210 @@ var Renderer = class {
|
|
|
898
1460
|
this._screen.invalidate();
|
|
899
1461
|
this._flush();
|
|
900
1462
|
}
|
|
1463
|
+
/** ANSI sequence to save cursor position */
|
|
1464
|
+
static _CURSOR_SAVE = "\x1B[s";
|
|
1465
|
+
/** ANSI sequence to restore cursor position */
|
|
1466
|
+
static _CURSOR_RESTORE = "\x1B[u";
|
|
901
1467
|
/**
|
|
902
1468
|
* Core diff and flush: compare front vs back buffer,
|
|
903
1469
|
* emit only changed cells.
|
|
904
1470
|
*/
|
|
905
1471
|
_flush() {
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1472
|
+
const epoch = this._screen.epoch;
|
|
1473
|
+
if (this._screen.flushEpoch === epoch) return;
|
|
1474
|
+
this._screen.flushEpoch = epoch;
|
|
1475
|
+
const start = this._callbacks.size > 0 ? performance.now() : 0;
|
|
1476
|
+
const bufferedLogs = this.hook.flush();
|
|
1477
|
+
if (bufferedLogs) {
|
|
1478
|
+
this._screen.invalidate();
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
const { front, back, cols, rows } = this._screen;
|
|
1482
|
+
let output = beginSyncUpdate;
|
|
1483
|
+
if (this._diffRenderer) {
|
|
1484
|
+
this._lastStyleFingerprint = null;
|
|
1485
|
+
for (let r = 0; r < rows; r++) {
|
|
1486
|
+
output += this._renderDiffLine(r, front, back, cols);
|
|
1487
|
+
}
|
|
1488
|
+
output += reset;
|
|
1489
|
+
output += endSyncUpdate;
|
|
1490
|
+
if (bufferedLogs) {
|
|
1491
|
+
this._terminal.writeSync(_Renderer._CURSOR_SAVE + bufferedLogs + _Renderer._CURSOR_RESTORE);
|
|
918
1492
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1493
|
+
this._terminal.writeSync(output);
|
|
1494
|
+
this._screen.saveLines();
|
|
1495
|
+
this._emitStats(start, bufferedLogs, output);
|
|
1496
|
+
this._screen.swap();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
for (let r = 0; r < rows; r++) {
|
|
1500
|
+
if (this._screen.getLine(r) === this._screen.getPreviousLine(r) && this._screen.getStyleLine(r) === this._screen.getPreviousStyleLine(r)) continue;
|
|
1501
|
+
output += moveTo(0, r);
|
|
1502
|
+
output += this._renderLine(r);
|
|
922
1503
|
}
|
|
1504
|
+
output += reset;
|
|
1505
|
+
output += endSyncUpdate;
|
|
1506
|
+
if (bufferedLogs) {
|
|
1507
|
+
this._terminal.writeSync(_Renderer._CURSOR_SAVE + bufferedLogs + _Renderer._CURSOR_RESTORE);
|
|
1508
|
+
}
|
|
1509
|
+
this._terminal.writeSync(output);
|
|
1510
|
+
this._emitStats(start, bufferedLogs, output);
|
|
1511
|
+
this._screen.saveLines();
|
|
1512
|
+
this._screen.swap();
|
|
1513
|
+
} catch (_err) {
|
|
1514
|
+
this._renderRequested = true;
|
|
1515
|
+
this._lastStyleFingerprint = null;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/** Style fingerprint of the last rendered cell (to suppress redundant ANSI reset/apply). */
|
|
1519
|
+
_lastStyleFingerprint = null;
|
|
1520
|
+
/** Build a stable style fingerprint string for a cell (avoids allocation-heavy object comparison). */
|
|
1521
|
+
_styleFingerprint(cell) {
|
|
1522
|
+
const fg = cell.fg;
|
|
1523
|
+
const bg = cell.bg;
|
|
1524
|
+
let fgKey;
|
|
1525
|
+
switch (fg.type) {
|
|
1526
|
+
case "none":
|
|
1527
|
+
fgKey = "n";
|
|
1528
|
+
break;
|
|
1529
|
+
case "named":
|
|
1530
|
+
fgKey = `N:${fg.name}`;
|
|
1531
|
+
break;
|
|
1532
|
+
case "ansi256":
|
|
1533
|
+
fgKey = `A:${fg.code}`;
|
|
1534
|
+
break;
|
|
1535
|
+
case "rgb":
|
|
1536
|
+
fgKey = `R:${fg.r},${fg.g},${fg.b}`;
|
|
1537
|
+
break;
|
|
1538
|
+
case "hex":
|
|
1539
|
+
fgKey = `H:${fg.hex.toLowerCase()}`;
|
|
1540
|
+
break;
|
|
1541
|
+
default:
|
|
1542
|
+
fgKey = "n";
|
|
1543
|
+
}
|
|
1544
|
+
let bgKey;
|
|
1545
|
+
switch (bg.type) {
|
|
1546
|
+
case "none":
|
|
1547
|
+
bgKey = "n";
|
|
1548
|
+
break;
|
|
1549
|
+
case "named":
|
|
1550
|
+
bgKey = `N:${bg.name}`;
|
|
1551
|
+
break;
|
|
1552
|
+
case "ansi256":
|
|
1553
|
+
bgKey = `A:${bg.code}`;
|
|
1554
|
+
break;
|
|
1555
|
+
case "rgb":
|
|
1556
|
+
bgKey = `R:${bg.r},${bg.g},${bg.b}`;
|
|
1557
|
+
break;
|
|
1558
|
+
case "hex":
|
|
1559
|
+
bgKey = `H:${bg.hex.toLowerCase()}`;
|
|
1560
|
+
break;
|
|
1561
|
+
default:
|
|
1562
|
+
bgKey = "n";
|
|
923
1563
|
}
|
|
924
|
-
|
|
925
|
-
output += endSyncUpdate;
|
|
926
|
-
this._terminal.write(output);
|
|
927
|
-
this._screen.swap();
|
|
1564
|
+
return `${cell.bold ? "B" : ""}${cell.dim ? "D" : ""}${cell.italic ? "I" : ""}${cell.underline ? "U" : ""}${cell.strikethrough ? "S" : ""}${cell.inverse ? "V" : ""}|${fgKey}|${bgKey}`;
|
|
928
1565
|
}
|
|
929
1566
|
/**
|
|
930
1567
|
* Generate the ANSI escape sequence to render a single cell.
|
|
1568
|
+
* Skips ansiReset + re-apply when the adjacent cell has identical style.
|
|
931
1569
|
*/
|
|
932
1570
|
_renderCell(cell) {
|
|
933
1571
|
let seq = "";
|
|
934
|
-
|
|
935
|
-
if (
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1572
|
+
const fp = this._styleFingerprint(cell);
|
|
1573
|
+
if (fp !== this._lastStyleFingerprint) {
|
|
1574
|
+
seq += reset;
|
|
1575
|
+
if (cell.bold) seq += "\x1B[1m";
|
|
1576
|
+
if (cell.dim) seq += "\x1B[2m";
|
|
1577
|
+
if (cell.italic) seq += "\x1B[3m";
|
|
1578
|
+
if (cell.underline) seq += "\x1B[4m";
|
|
1579
|
+
if (cell.strikethrough) seq += "\x1B[9m";
|
|
1580
|
+
if (cell.inverse) seq += "\x1B[7m";
|
|
1581
|
+
seq += colorToAnsiFg(cell.fg, this._colorDepth);
|
|
1582
|
+
seq += colorToAnsiBg(cell.bg, this._colorDepth);
|
|
1583
|
+
this._lastStyleFingerprint = fp;
|
|
1584
|
+
}
|
|
1585
|
+
seq += stripAnsiControl(cell.char) || " ";
|
|
944
1586
|
return seq;
|
|
945
1587
|
}
|
|
1588
|
+
/**
|
|
1589
|
+
* If a span starts at a width-0 continuation cell (the second half of a
|
|
1590
|
+
* wide character), adjust backward to the preceding cell so the cursor
|
|
1591
|
+
* is placed at a valid column boundary.
|
|
1592
|
+
*/
|
|
1593
|
+
static _adjustSpanStart(col, row) {
|
|
1594
|
+
while (col > 0 && row[col].width === 0) {
|
|
1595
|
+
col--;
|
|
1596
|
+
}
|
|
1597
|
+
return col;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Render only the changed spans within a single row (cell-level granularity).
|
|
1601
|
+
* Uses moveTo to position the cursor at the start of each changed span.
|
|
1602
|
+
*/
|
|
1603
|
+
_renderDiffLine(row, front, back, cols) {
|
|
1604
|
+
let output = "";
|
|
1605
|
+
let spanStart = -1;
|
|
1606
|
+
for (let c = 0; c < cols; c++) {
|
|
1607
|
+
if (back[row][c].width === 0) continue;
|
|
1608
|
+
const changed = !cellsEqual(front[row][c], back[row][c]);
|
|
1609
|
+
if (changed && spanStart === -1) {
|
|
1610
|
+
spanStart = c;
|
|
1611
|
+
} else if (!changed && spanStart !== -1) {
|
|
1612
|
+
const adjustedStart = _Renderer._adjustSpanStart(spanStart, back[row]);
|
|
1613
|
+
output += moveTo(adjustedStart, row);
|
|
1614
|
+
for (let sc = spanStart; sc < c; sc++) {
|
|
1615
|
+
const cell = back[row][sc];
|
|
1616
|
+
if (cell.width === 0) continue;
|
|
1617
|
+
output += this._renderCell(cell);
|
|
1618
|
+
}
|
|
1619
|
+
spanStart = -1;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (spanStart !== -1) {
|
|
1623
|
+
const adjustedStart = _Renderer._adjustSpanStart(spanStart, back[row]);
|
|
1624
|
+
output += moveTo(adjustedStart, row);
|
|
1625
|
+
for (let sc = spanStart; sc < cols; sc++) {
|
|
1626
|
+
const cell = back[row][sc];
|
|
1627
|
+
if (cell.width === 0) continue;
|
|
1628
|
+
output += this._renderCell(cell);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return output;
|
|
1632
|
+
}
|
|
1633
|
+
_renderLine(row) {
|
|
1634
|
+
let output = "";
|
|
1635
|
+
for (let c = 0; c < this._screen.cols; c++) {
|
|
1636
|
+
const cell = this._screen.back[row][c];
|
|
1637
|
+
if (cell.width === 0) continue;
|
|
1638
|
+
output += this._renderCell(cell);
|
|
1639
|
+
}
|
|
1640
|
+
return output;
|
|
1641
|
+
}
|
|
1642
|
+
_emitStats(start, bufferedLogs, output) {
|
|
1643
|
+
if (this._callbacks.size === 0) return;
|
|
1644
|
+
const durationMs = performance.now() - start;
|
|
1645
|
+
const { front, back, cols, rows } = this._screen;
|
|
1646
|
+
let cellsChanged = 0;
|
|
1647
|
+
for (let r = 0; r < rows; r++) {
|
|
1648
|
+
for (let c = 0; c < cols; c++) {
|
|
1649
|
+
if (!cellsEqual(front[r][c], back[r][c])) {
|
|
1650
|
+
cellsChanged++;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const bytesWritten = (bufferedLogs ? Buffer.byteLength(bufferedLogs) : 0) + Buffer.byteLength(output);
|
|
1655
|
+
const stats = {
|
|
1656
|
+
cellsChanged,
|
|
1657
|
+
bytesWritten,
|
|
1658
|
+
durationMs: Math.max(0, durationMs)
|
|
1659
|
+
};
|
|
1660
|
+
for (const cb of this._callbacks) {
|
|
1661
|
+
try {
|
|
1662
|
+
cb(stats);
|
|
1663
|
+
} catch {
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
946
1667
|
};
|
|
947
1668
|
|
|
948
1669
|
// src/terminal/LayerManager.ts
|
|
@@ -953,9 +1674,12 @@ var LayerManager = class {
|
|
|
953
1674
|
_layers = /* @__PURE__ */ new Map();
|
|
954
1675
|
_cols;
|
|
955
1676
|
_rows;
|
|
1677
|
+
_hitWidgetGrid;
|
|
1678
|
+
_hitZGrid;
|
|
956
1679
|
constructor(cols, rows) {
|
|
957
1680
|
this._cols = cols;
|
|
958
1681
|
this._rows = rows;
|
|
1682
|
+
this._allocateHitGrids();
|
|
959
1683
|
}
|
|
960
1684
|
get cols() {
|
|
961
1685
|
return this._cols;
|
|
@@ -1029,15 +1753,30 @@ var LayerManager = class {
|
|
|
1029
1753
|
col = Math.floor(col);
|
|
1030
1754
|
if (!(row >= 0 && row < this._rows)) return;
|
|
1031
1755
|
let x = col;
|
|
1032
|
-
for (const char of str) {
|
|
1756
|
+
for (const { segment: char } of segmenter.segment(str)) {
|
|
1033
1757
|
if (x >= this._cols) break;
|
|
1758
|
+
const charWidth = segmentWidth(char);
|
|
1034
1759
|
if (x < 0) {
|
|
1035
|
-
x
|
|
1760
|
+
x += charWidth;
|
|
1036
1761
|
continue;
|
|
1037
1762
|
}
|
|
1038
|
-
this.setCell(layerId, x, row, { char, width:
|
|
1039
|
-
x
|
|
1763
|
+
this.setCell(layerId, x, row, { char, width: charWidth, ...style });
|
|
1764
|
+
if (charWidth === 2 && x + 1 < this._cols) {
|
|
1765
|
+
this.setCell(layerId, x + 1, row, { char: " ", width: 1, ...style });
|
|
1766
|
+
}
|
|
1767
|
+
x += charWidth;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Check whether any visible layer has pending dirty changes.
|
|
1772
|
+
*/
|
|
1773
|
+
hasDirtyLayers() {
|
|
1774
|
+
for (const layer of this._layers.values()) {
|
|
1775
|
+
if (layer.visible && layer.dirtyRegion) {
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1040
1778
|
}
|
|
1779
|
+
return false;
|
|
1041
1780
|
}
|
|
1042
1781
|
/**
|
|
1043
1782
|
* Clear all cells in a specific layer.
|
|
@@ -1050,7 +1789,7 @@ var LayerManager = class {
|
|
|
1050
1789
|
layer.cells[r][c] = emptyCell();
|
|
1051
1790
|
}
|
|
1052
1791
|
}
|
|
1053
|
-
layer.dirtyRegion =
|
|
1792
|
+
layer.dirtyRegion = { x: 0, y: 0, width: this._cols, height: this._rows };
|
|
1054
1793
|
}
|
|
1055
1794
|
/**
|
|
1056
1795
|
* Clear all overlay layers.
|
|
@@ -1064,28 +1803,29 @@ var LayerManager = class {
|
|
|
1064
1803
|
* Composite all overlay layers onto the Screen's back buffer.
|
|
1065
1804
|
* Layers are applied in z-index order (lowest first).
|
|
1066
1805
|
* Transparent cells (empty with no colors) are skipped.
|
|
1806
|
+
* Writes directly to screen.back to avoid setCell overhead
|
|
1807
|
+
* (bounds/clip checks are already satisfied by dirtyRegion).
|
|
1067
1808
|
*/
|
|
1068
1809
|
composite(screen) {
|
|
1069
1810
|
const sorted = this.getSortedLayers();
|
|
1070
1811
|
for (const layer of sorted) {
|
|
1071
1812
|
if (!layer.dirtyRegion) continue;
|
|
1072
1813
|
const { x: dx, y: dy, width: dw, height: dh } = layer.dirtyRegion;
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
});
|
|
1814
|
+
const maxRow = Math.min(dy + dh, this._rows);
|
|
1815
|
+
const maxCol = Math.min(dx + dw, this._cols);
|
|
1816
|
+
for (let r = dy; r < maxRow; r++) {
|
|
1817
|
+
const backRow = screen.back[r];
|
|
1818
|
+
const layerRow = layer.cells[r];
|
|
1819
|
+
if (!backRow || !layerRow) continue;
|
|
1820
|
+
let c = dx;
|
|
1821
|
+
while (c < maxCol) {
|
|
1822
|
+
const cell = layerRow[c];
|
|
1823
|
+
if (isCellTransparent(cell)) {
|
|
1824
|
+
c++;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
Object.assign(backRow[c], cell);
|
|
1828
|
+
c++;
|
|
1089
1829
|
}
|
|
1090
1830
|
}
|
|
1091
1831
|
}
|
|
@@ -1100,6 +1840,41 @@ var LayerManager = class {
|
|
|
1100
1840
|
layer.cells = this._createGrid();
|
|
1101
1841
|
layer.dirtyRegion = null;
|
|
1102
1842
|
}
|
|
1843
|
+
this._allocateHitGrids();
|
|
1844
|
+
}
|
|
1845
|
+
/** Reset the hit grid. Call once at the start of each frame. */
|
|
1846
|
+
clearHitGrid() {
|
|
1847
|
+
this._allocateHitGrids();
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Mark a rectangular region as owned by a widget at a given z-index.
|
|
1851
|
+
* Higher z wins when regions overlap.
|
|
1852
|
+
*/
|
|
1853
|
+
setHitRegion(widgetId, x, y, w, h, z) {
|
|
1854
|
+
const zVal = z ?? 0;
|
|
1855
|
+
const startX = Math.floor(x);
|
|
1856
|
+
const startY = Math.floor(y);
|
|
1857
|
+
const width = Math.floor(w);
|
|
1858
|
+
const height = Math.floor(h);
|
|
1859
|
+
for (let r = startY; r < startY + height; r++) {
|
|
1860
|
+
if (r < 0 || r >= this._rows) continue;
|
|
1861
|
+
for (let c = startX; c < startX + width; c++) {
|
|
1862
|
+
if (c < 0 || c >= this._cols) continue;
|
|
1863
|
+
if (zVal >= this._hitZGrid[r][c]) {
|
|
1864
|
+
this._hitWidgetGrid[r][c] = widgetId;
|
|
1865
|
+
this._hitZGrid[r][c] = zVal;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
/** Return the topmost widget id at a cell, or null. */
|
|
1871
|
+
hitTest(col, row) {
|
|
1872
|
+
const c = Math.floor(col);
|
|
1873
|
+
const r = Math.floor(row);
|
|
1874
|
+
if (c < 0 || c >= this._cols || r < 0 || r >= this._rows) {
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
return this._hitWidgetGrid[r][c];
|
|
1103
1878
|
}
|
|
1104
1879
|
/**
|
|
1105
1880
|
* Create an empty cell grid.
|
|
@@ -1115,6 +1890,23 @@ var LayerManager = class {
|
|
|
1115
1890
|
}
|
|
1116
1891
|
return grid;
|
|
1117
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Allocate parallel hit grid and z-index grid.
|
|
1895
|
+
*/
|
|
1896
|
+
_allocateHitGrids() {
|
|
1897
|
+
this._hitWidgetGrid = [];
|
|
1898
|
+
this._hitZGrid = [];
|
|
1899
|
+
for (let r = 0; r < this._rows; r++) {
|
|
1900
|
+
const widgetRow = [];
|
|
1901
|
+
const zRow = [];
|
|
1902
|
+
for (let c = 0; c < this._cols; c++) {
|
|
1903
|
+
widgetRow.push(null);
|
|
1904
|
+
zRow.push(-Infinity);
|
|
1905
|
+
}
|
|
1906
|
+
this._hitWidgetGrid.push(widgetRow);
|
|
1907
|
+
this._hitZGrid.push(zRow);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1118
1910
|
/**
|
|
1119
1911
|
* Expand the dirty region of a layer to include the given cell.
|
|
1120
1912
|
*/
|
|
@@ -1135,14 +1927,6 @@ var LayerManager = class {
|
|
|
1135
1927
|
}
|
|
1136
1928
|
};
|
|
1137
1929
|
|
|
1138
|
-
// src/terminal/env-caps.ts
|
|
1139
|
-
var caps = {
|
|
1140
|
-
color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
|
|
1141
|
-
unicode: !process.env.NO_UNICODE && process.env.TERM !== "dumb",
|
|
1142
|
-
motion: !process.env.NO_MOTION && !process.env.CI,
|
|
1143
|
-
ci: !!process.env.CI
|
|
1144
|
-
};
|
|
1145
|
-
|
|
1146
1930
|
// src/terminal/ascii-map.ts
|
|
1147
1931
|
var BOX = {
|
|
1148
1932
|
"\u250C": "+",
|
|
@@ -1171,6 +1955,93 @@ var BOX = {
|
|
|
1171
1955
|
var BRAILLE_SPIN = ["|", "/", "-", "\\"];
|
|
1172
1956
|
var BLOCK = { full: "#", empty: " ", partial: "-" };
|
|
1173
1957
|
|
|
1958
|
+
// src/terminal/bell.ts
|
|
1959
|
+
function bell2() {
|
|
1960
|
+
if (typeof process !== "undefined" && process.stdout && process.stdout.write) {
|
|
1961
|
+
process.stdout.write("\x07");
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/renderer/border-merge.ts
|
|
1966
|
+
var VERTICAL = /* @__PURE__ */ new Set(["\u2502", "|"]);
|
|
1967
|
+
var HORIZONTAL = /* @__PURE__ */ new Set(["\u2500", "-"]);
|
|
1968
|
+
function isVertical(char) {
|
|
1969
|
+
return VERTICAL.has(char);
|
|
1970
|
+
}
|
|
1971
|
+
function isHorizontal(char) {
|
|
1972
|
+
return HORIZONTAL.has(char);
|
|
1973
|
+
}
|
|
1974
|
+
var UNICODE_JUNCTIONS = {
|
|
1975
|
+
LRTB: "\u253C",
|
|
1976
|
+
RTB: "\u251C",
|
|
1977
|
+
LTB: "\u2524",
|
|
1978
|
+
LRB: "\u252C",
|
|
1979
|
+
LRT: "\u2534",
|
|
1980
|
+
RB: "\u250C",
|
|
1981
|
+
LB: "\u2510",
|
|
1982
|
+
RT: "\u2514",
|
|
1983
|
+
LT: "\u2518",
|
|
1984
|
+
TB: "\u2502",
|
|
1985
|
+
LR: "\u2500",
|
|
1986
|
+
R: "\u2500",
|
|
1987
|
+
L: "\u2500",
|
|
1988
|
+
T: "\u2502",
|
|
1989
|
+
B: "\u2502"
|
|
1990
|
+
};
|
|
1991
|
+
var ASCII_JUNCTIONS = {
|
|
1992
|
+
LRTB: "+",
|
|
1993
|
+
RTB: "+",
|
|
1994
|
+
LTB: "+",
|
|
1995
|
+
LRB: "+",
|
|
1996
|
+
LRT: "+",
|
|
1997
|
+
RB: "+",
|
|
1998
|
+
LB: "+",
|
|
1999
|
+
RT: "+",
|
|
2000
|
+
LT: "+",
|
|
2001
|
+
TB: "|",
|
|
2002
|
+
LR: "-",
|
|
2003
|
+
R: "-",
|
|
2004
|
+
L: "-",
|
|
2005
|
+
T: "|",
|
|
2006
|
+
B: "|"
|
|
2007
|
+
};
|
|
2008
|
+
function getJunctions() {
|
|
2009
|
+
return caps.unicode ? UNICODE_JUNCTIONS : ASCII_JUNCTIONS;
|
|
2010
|
+
}
|
|
2011
|
+
function mergeBorders(screen) {
|
|
2012
|
+
const grid = screen.back;
|
|
2013
|
+
const junctions = getJunctions();
|
|
2014
|
+
const updates = [];
|
|
2015
|
+
for (let row = 0; row < screen.rows; row++) {
|
|
2016
|
+
for (let col = 0; col < screen.cols; col++) {
|
|
2017
|
+
const cell = grid[row][col];
|
|
2018
|
+
const top = row > 0 ? grid[row - 1][col].char : "";
|
|
2019
|
+
const bottom = row < screen.rows - 1 ? grid[row + 1][col].char : "";
|
|
2020
|
+
const left = col > 0 ? grid[row][col - 1].char : "";
|
|
2021
|
+
const right = col < screen.cols - 1 ? grid[row][col + 1].char : "";
|
|
2022
|
+
const hasTop = isVertical(top);
|
|
2023
|
+
const hasBottom = isVertical(bottom);
|
|
2024
|
+
const hasLeft = isHorizontal(left);
|
|
2025
|
+
const hasRight = isHorizontal(right);
|
|
2026
|
+
const key = (hasLeft ? "L" : "") + (hasRight ? "R" : "") + (hasTop ? "T" : "") + (hasBottom ? "B" : "");
|
|
2027
|
+
const merged = junctions[key];
|
|
2028
|
+
if (merged) {
|
|
2029
|
+
updates.push({
|
|
2030
|
+
row,
|
|
2031
|
+
col,
|
|
2032
|
+
char: merged
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
for (const update of updates) {
|
|
2038
|
+
grid[update.row][update.col].char = update.char;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// src/input/InputParser.ts
|
|
2043
|
+
var import_node_buffer = require("buffer");
|
|
2044
|
+
|
|
1174
2045
|
// src/events/types.ts
|
|
1175
2046
|
function createKeyEvent(base) {
|
|
1176
2047
|
const event = {
|
|
@@ -1273,6 +2144,19 @@ var SPECIAL_KEYS = {
|
|
|
1273
2144
|
10: "enter",
|
|
1274
2145
|
32: "space"
|
|
1275
2146
|
};
|
|
2147
|
+
function normalizeNavigationKey(keyName) {
|
|
2148
|
+
const mode = caps.keybindingMode;
|
|
2149
|
+
if (mode === "vim") {
|
|
2150
|
+
if (keyName === "k") return "up";
|
|
2151
|
+
if (keyName === "j") return "down";
|
|
2152
|
+
if (keyName === "h") return "left";
|
|
2153
|
+
if (keyName === "l") return "right";
|
|
2154
|
+
} else if (mode === "emacs") {
|
|
2155
|
+
if (keyName === "ctrl+p") return "up";
|
|
2156
|
+
if (keyName === "ctrl+n") return "down";
|
|
2157
|
+
}
|
|
2158
|
+
return keyName;
|
|
2159
|
+
}
|
|
1276
2160
|
|
|
1277
2161
|
// src/input/MouseParser.ts
|
|
1278
2162
|
function parseMouseEvent(data) {
|
|
@@ -1285,13 +2169,28 @@ function parseMouseEvent(data) {
|
|
|
1285
2169
|
let button;
|
|
1286
2170
|
let type;
|
|
1287
2171
|
let scrollDelta;
|
|
2172
|
+
let scrollDeltaX;
|
|
2173
|
+
let scrollAxis;
|
|
1288
2174
|
const buttonBits = cb & 3;
|
|
1289
2175
|
const motion = (cb & 32) !== 0;
|
|
1290
2176
|
const isScroll = (cb & 64) !== 0;
|
|
2177
|
+
const shift = (cb & 4) !== 0;
|
|
2178
|
+
const alt = (cb & 8) !== 0;
|
|
2179
|
+
const ctrl = (cb & 16) !== 0;
|
|
1291
2180
|
if (isScroll) {
|
|
1292
2181
|
button = "none";
|
|
1293
2182
|
type = "scroll";
|
|
1294
|
-
|
|
2183
|
+
const lowBits = cb & 7;
|
|
2184
|
+
if (lowBits === 6) {
|
|
2185
|
+
scrollAxis = "horizontal";
|
|
2186
|
+
scrollDeltaX = -1;
|
|
2187
|
+
} else if (lowBits === 7) {
|
|
2188
|
+
scrollAxis = "horizontal";
|
|
2189
|
+
scrollDeltaX = 1;
|
|
2190
|
+
} else {
|
|
2191
|
+
scrollAxis = "vertical";
|
|
2192
|
+
scrollDelta = buttonBits === 0 ? -1 : 1;
|
|
2193
|
+
}
|
|
1295
2194
|
} else if (motion) {
|
|
1296
2195
|
type = "mousemove";
|
|
1297
2196
|
button = decodeButton(buttonBits);
|
|
@@ -1307,7 +2206,12 @@ function parseMouseEvent(data) {
|
|
|
1307
2206
|
y: cy,
|
|
1308
2207
|
button,
|
|
1309
2208
|
type,
|
|
1310
|
-
scrollDelta
|
|
2209
|
+
...scrollDelta !== void 0 && { scrollDelta },
|
|
2210
|
+
...scrollDeltaX !== void 0 && { scrollDeltaX },
|
|
2211
|
+
...scrollAxis !== void 0 && { scrollAxis },
|
|
2212
|
+
shift,
|
|
2213
|
+
alt,
|
|
2214
|
+
ctrl
|
|
1311
2215
|
};
|
|
1312
2216
|
}
|
|
1313
2217
|
function decodeButton(bits) {
|
|
@@ -1329,7 +2233,9 @@ function isMouseSequence(data) {
|
|
|
1329
2233
|
// src/events/EventEmitter.ts
|
|
1330
2234
|
var EventEmitter = class {
|
|
1331
2235
|
_handlers = /* @__PURE__ */ new Map();
|
|
2236
|
+
// any: handler type erased here; callers constrain via generics
|
|
1332
2237
|
_onceHandlers = /* @__PURE__ */ new Map();
|
|
2238
|
+
// any: handler type erased here; callers constrain via generics
|
|
1333
2239
|
/**
|
|
1334
2240
|
* Subscribe to an event.
|
|
1335
2241
|
* @returns Unsubscribe function.
|
|
@@ -1369,7 +2275,7 @@ var EventEmitter = class {
|
|
|
1369
2275
|
for (const handler of handlers) {
|
|
1370
2276
|
try {
|
|
1371
2277
|
handler(data);
|
|
1372
|
-
} catch {
|
|
2278
|
+
} catch (_err) {
|
|
1373
2279
|
}
|
|
1374
2280
|
}
|
|
1375
2281
|
}
|
|
@@ -1378,7 +2284,7 @@ var EventEmitter = class {
|
|
|
1378
2284
|
for (const handler of onceHandlers) {
|
|
1379
2285
|
try {
|
|
1380
2286
|
handler(data);
|
|
1381
|
-
} catch {
|
|
2287
|
+
} catch (_err) {
|
|
1382
2288
|
}
|
|
1383
2289
|
}
|
|
1384
2290
|
onceHandlers.clear();
|
|
@@ -1410,7 +2316,10 @@ var InputParser = class {
|
|
|
1410
2316
|
_stdin;
|
|
1411
2317
|
_handler = null;
|
|
1412
2318
|
_escapeTimeout = null;
|
|
1413
|
-
_escapeBuffer =
|
|
2319
|
+
_escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
2320
|
+
_isPasting = false;
|
|
2321
|
+
_pasteBuffer = "";
|
|
2322
|
+
_cursorRequests = [];
|
|
1414
2323
|
constructor(stdin) {
|
|
1415
2324
|
this._stdin = stdin;
|
|
1416
2325
|
}
|
|
@@ -1422,6 +2331,25 @@ var InputParser = class {
|
|
|
1422
2331
|
onMouse(handler) {
|
|
1423
2332
|
return this._events.on("mouse", handler);
|
|
1424
2333
|
}
|
|
2334
|
+
/** Subscribe to terminal focus-in (true) / focus-out (false) reports. */
|
|
2335
|
+
onFocusChange(handler) {
|
|
2336
|
+
return this._events.on("focuschange", handler);
|
|
2337
|
+
}
|
|
2338
|
+
onPaste(handler) {
|
|
2339
|
+
return this._events.on("paste", handler);
|
|
2340
|
+
}
|
|
2341
|
+
requestCursorPosition(timeoutMs = 200) {
|
|
2342
|
+
return new Promise((resolve, reject) => {
|
|
2343
|
+
const timeout = setTimeout(() => {
|
|
2344
|
+
const idx = this._cursorRequests.findIndex((item) => item.reject === reject);
|
|
2345
|
+
if (idx !== -1) {
|
|
2346
|
+
this._cursorRequests.splice(idx, 1);
|
|
2347
|
+
}
|
|
2348
|
+
reject(new Error("Cursor position request timed out"));
|
|
2349
|
+
}, timeoutMs);
|
|
2350
|
+
this._cursorRequests.push({ resolve, reject, timeout });
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
1425
2353
|
/** Start listening for input */
|
|
1426
2354
|
start() {
|
|
1427
2355
|
if (this._handler) return;
|
|
@@ -1440,46 +2368,57 @@ var InputParser = class {
|
|
|
1440
2368
|
clearTimeout(this._escapeTimeout);
|
|
1441
2369
|
this._escapeTimeout = null;
|
|
1442
2370
|
}
|
|
1443
|
-
this._escapeBuffer =
|
|
2371
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
2372
|
+
for (const req of this._cursorRequests) {
|
|
2373
|
+
clearTimeout(req.timeout);
|
|
2374
|
+
req.reject(new Error("InputParser stopped"));
|
|
2375
|
+
}
|
|
2376
|
+
this._cursorRequests = [];
|
|
1444
2377
|
}
|
|
1445
2378
|
/**
|
|
1446
2379
|
* Process a chunk of raw input bytes.
|
|
1447
2380
|
*/
|
|
1448
2381
|
_processInput(data) {
|
|
1449
2382
|
const str = data.toString("utf8");
|
|
1450
|
-
|
|
1451
|
-
|
|
2383
|
+
const PASTE_START = "\x1B[200~";
|
|
2384
|
+
const PASTE_END = "\x1B[201~";
|
|
2385
|
+
if (str.includes(PASTE_START) && str.includes(PASTE_END)) {
|
|
2386
|
+
const pastedText = str.replace(PASTE_START, "").replace(PASTE_END, "");
|
|
2387
|
+
this._events.emit("paste", pastedText);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (this._escapeBuffer.length > 0) {
|
|
2391
|
+
this._escapeBuffer = import_node_buffer.Buffer.concat([this._escapeBuffer, data]);
|
|
1452
2392
|
if (this._escapeTimeout) {
|
|
1453
2393
|
clearTimeout(this._escapeTimeout);
|
|
1454
2394
|
this._escapeTimeout = null;
|
|
1455
2395
|
}
|
|
1456
|
-
this._tryParseEscape(
|
|
2396
|
+
this._tryParseEscape();
|
|
1457
2397
|
return;
|
|
1458
2398
|
}
|
|
1459
2399
|
if (str.startsWith("\x1B") && str.length === 1) {
|
|
1460
|
-
this._escapeBuffer =
|
|
2400
|
+
this._escapeBuffer = data;
|
|
1461
2401
|
this._escapeTimeout = setTimeout(() => {
|
|
1462
2402
|
this._events.emit("key", createKeyEvent({
|
|
1463
2403
|
key: "escape",
|
|
1464
|
-
raw:
|
|
2404
|
+
raw: this._escapeBuffer,
|
|
1465
2405
|
ctrl: false,
|
|
1466
2406
|
alt: false,
|
|
1467
2407
|
shift: false
|
|
1468
2408
|
}));
|
|
1469
|
-
this._escapeBuffer =
|
|
2409
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1470
2410
|
this._escapeTimeout = null;
|
|
1471
2411
|
}, 50);
|
|
1472
2412
|
return;
|
|
1473
2413
|
}
|
|
1474
2414
|
if (str.startsWith("\x1B")) {
|
|
1475
|
-
this._escapeBuffer =
|
|
1476
|
-
this._tryParseEscape(
|
|
2415
|
+
this._escapeBuffer = data;
|
|
2416
|
+
this._tryParseEscape();
|
|
1477
2417
|
return;
|
|
1478
2418
|
}
|
|
1479
|
-
for (
|
|
1480
|
-
const
|
|
1481
|
-
const
|
|
1482
|
-
const raw = Buffer.from(ch, "utf8");
|
|
2419
|
+
for (const ch of str) {
|
|
2420
|
+
const code = ch.codePointAt(0);
|
|
2421
|
+
const raw = import_node_buffer.Buffer.from(ch, "utf8");
|
|
1483
2422
|
if (code >= 1 && code <= 26) {
|
|
1484
2423
|
const keyName = CTRL_KEYS[code];
|
|
1485
2424
|
const isCtrl = code !== 9 && code !== 13 && code !== 10;
|
|
@@ -1516,13 +2455,13 @@ var InputParser = class {
|
|
|
1516
2455
|
/**
|
|
1517
2456
|
* Try to parse buffered escape sequence.
|
|
1518
2457
|
*/
|
|
1519
|
-
_tryParseEscape(
|
|
1520
|
-
const seq = this._escapeBuffer;
|
|
2458
|
+
_tryParseEscape() {
|
|
2459
|
+
const seq = this._escapeBuffer.toString("utf8");
|
|
1521
2460
|
if (isMouseSequence(seq)) {
|
|
1522
2461
|
const mouseEvt = parseMouseEvent(seq);
|
|
1523
2462
|
if (mouseEvt) {
|
|
1524
2463
|
this._events.emit("mouse", mouseEvt);
|
|
1525
|
-
this._escapeBuffer =
|
|
2464
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1526
2465
|
return;
|
|
1527
2466
|
}
|
|
1528
2467
|
if (seq.length < 20) {
|
|
@@ -1531,12 +2470,35 @@ var InputParser = class {
|
|
|
1531
2470
|
this._escapeTimeout = null;
|
|
1532
2471
|
}
|
|
1533
2472
|
this._escapeTimeout = setTimeout(() => {
|
|
1534
|
-
this._escapeBuffer =
|
|
2473
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1535
2474
|
this._escapeTimeout = null;
|
|
1536
2475
|
}, 100);
|
|
1537
2476
|
return;
|
|
1538
2477
|
}
|
|
1539
2478
|
}
|
|
2479
|
+
const cursorMatch = seq.match(/^\x1b\[(\d+);(\d+)R$/);
|
|
2480
|
+
if (cursorMatch) {
|
|
2481
|
+
const row = parseInt(cursorMatch[1], 10);
|
|
2482
|
+
const col = parseInt(cursorMatch[2], 10);
|
|
2483
|
+
const position = { row, col };
|
|
2484
|
+
for (const request of this._cursorRequests) {
|
|
2485
|
+
clearTimeout(request.timeout);
|
|
2486
|
+
request.resolve(position);
|
|
2487
|
+
}
|
|
2488
|
+
this._cursorRequests = [];
|
|
2489
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
if (seq === "\x1B[I") {
|
|
2493
|
+
this._events.emit("focuschange", true);
|
|
2494
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
if (seq === "\x1B[O") {
|
|
2498
|
+
this._events.emit("focuschange", false);
|
|
2499
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
1540
2502
|
if (seq in ESCAPE_SEQUENCES) {
|
|
1541
2503
|
const keyName = ESCAPE_SEQUENCES[seq];
|
|
1542
2504
|
const isShift = keyName.startsWith("shift+");
|
|
@@ -1545,28 +2507,28 @@ var InputParser = class {
|
|
|
1545
2507
|
const cleanKey = keyName.replace(/^(shift|ctrl|alt)\+/, "");
|
|
1546
2508
|
this._events.emit("key", createKeyEvent({
|
|
1547
2509
|
key: cleanKey,
|
|
1548
|
-
raw:
|
|
2510
|
+
raw: this._escapeBuffer,
|
|
1549
2511
|
ctrl: isCtrl,
|
|
1550
2512
|
alt: isAlt,
|
|
1551
2513
|
shift: isShift
|
|
1552
2514
|
}));
|
|
1553
|
-
this._escapeBuffer =
|
|
2515
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1554
2516
|
return;
|
|
1555
2517
|
}
|
|
1556
|
-
if (seq.length === 2 && seq[0] === "\x1B") {
|
|
2518
|
+
if (seq.length === 2 && seq[0] === "\x1B" && seq[1] !== "[" && seq[1] !== "O") {
|
|
1557
2519
|
const ch = seq[1];
|
|
1558
2520
|
this._events.emit("key", createKeyEvent({
|
|
1559
2521
|
key: ch,
|
|
1560
|
-
raw:
|
|
2522
|
+
raw: this._escapeBuffer,
|
|
1561
2523
|
ctrl: false,
|
|
1562
2524
|
alt: true,
|
|
1563
2525
|
shift: ch !== ch.toLowerCase() && ch === ch.toUpperCase()
|
|
1564
2526
|
}));
|
|
1565
|
-
this._escapeBuffer =
|
|
2527
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1566
2528
|
return;
|
|
1567
2529
|
}
|
|
1568
2530
|
if (seq.length > 20) {
|
|
1569
|
-
this._escapeBuffer =
|
|
2531
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1570
2532
|
return;
|
|
1571
2533
|
}
|
|
1572
2534
|
if (this._escapeTimeout) {
|
|
@@ -1574,12 +2536,184 @@ var InputParser = class {
|
|
|
1574
2536
|
this._escapeTimeout = null;
|
|
1575
2537
|
}
|
|
1576
2538
|
this._escapeTimeout = setTimeout(() => {
|
|
1577
|
-
this._escapeBuffer =
|
|
2539
|
+
this._escapeBuffer = import_node_buffer.Buffer.alloc(0);
|
|
1578
2540
|
this._escapeTimeout = null;
|
|
1579
2541
|
}, 100);
|
|
1580
2542
|
}
|
|
1581
2543
|
};
|
|
1582
2544
|
|
|
2545
|
+
// src/input/MouseGestures.ts
|
|
2546
|
+
var MouseGestures = class {
|
|
2547
|
+
doubleClickMs;
|
|
2548
|
+
lastMouseDown = null;
|
|
2549
|
+
activeDragButton = null;
|
|
2550
|
+
wasDragging = false;
|
|
2551
|
+
constructor(opts) {
|
|
2552
|
+
this.doubleClickMs = opts?.doubleClickMs ?? 300;
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Feed a raw MouseEvent. Returns synthesized events to emit
|
|
2556
|
+
* (may be empty). Does not mutate the input event.
|
|
2557
|
+
*/
|
|
2558
|
+
feed(event) {
|
|
2559
|
+
const synthesized = [];
|
|
2560
|
+
if (event.type === "mousedown") {
|
|
2561
|
+
const now = Date.now();
|
|
2562
|
+
if (this.lastMouseDown && this.lastMouseDown.x === event.x && this.lastMouseDown.y === event.y && this.lastMouseDown.button === event.button && now - this.lastMouseDown.time <= this.doubleClickMs) {
|
|
2563
|
+
synthesized.push({
|
|
2564
|
+
x: event.x,
|
|
2565
|
+
y: event.y,
|
|
2566
|
+
button: event.button,
|
|
2567
|
+
type: "dblclick"
|
|
2568
|
+
});
|
|
2569
|
+
this.lastMouseDown = null;
|
|
2570
|
+
} else {
|
|
2571
|
+
this.lastMouseDown = {
|
|
2572
|
+
x: event.x,
|
|
2573
|
+
y: event.y,
|
|
2574
|
+
button: event.button,
|
|
2575
|
+
time: now
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
this.activeDragButton = event.button;
|
|
2579
|
+
this.wasDragging = false;
|
|
2580
|
+
} else if (event.type === "mousemove") {
|
|
2581
|
+
if (this.activeDragButton !== null) {
|
|
2582
|
+
this.wasDragging = true;
|
|
2583
|
+
synthesized.push({
|
|
2584
|
+
x: event.x,
|
|
2585
|
+
y: event.y,
|
|
2586
|
+
button: this.activeDragButton,
|
|
2587
|
+
type: "drag"
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
} else if (event.type === "mouseup") {
|
|
2591
|
+
if (this.activeDragButton !== null) {
|
|
2592
|
+
if (this.wasDragging) {
|
|
2593
|
+
synthesized.push({
|
|
2594
|
+
x: event.x,
|
|
2595
|
+
y: event.y,
|
|
2596
|
+
button: event.button,
|
|
2597
|
+
type: "dragend"
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
this.activeDragButton = null;
|
|
2601
|
+
this.wasDragging = false;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return synthesized;
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
|
|
2608
|
+
// src/input/ChordMatcher.ts
|
|
2609
|
+
function getKeyEventToken(event) {
|
|
2610
|
+
let token = "";
|
|
2611
|
+
if (event.ctrl) token += "ctrl+";
|
|
2612
|
+
if (event.alt) token += "alt+";
|
|
2613
|
+
if (event.shift) token += "shift+";
|
|
2614
|
+
token += event.key.toLowerCase();
|
|
2615
|
+
return token;
|
|
2616
|
+
}
|
|
2617
|
+
var ChordMatcher = class {
|
|
2618
|
+
_bindings = [];
|
|
2619
|
+
_nextId = 0;
|
|
2620
|
+
_buffer = [];
|
|
2621
|
+
_timeoutMs;
|
|
2622
|
+
_timer = null;
|
|
2623
|
+
constructor(opts) {
|
|
2624
|
+
this._timeoutMs = opts?.timeoutMs ?? 800;
|
|
2625
|
+
}
|
|
2626
|
+
bind(keys, handler) {
|
|
2627
|
+
const id = this._nextId++;
|
|
2628
|
+
this._bindings.push({ keys, handler, id });
|
|
2629
|
+
return () => {
|
|
2630
|
+
this._bindings = this._bindings.filter((b) => b.id !== id);
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
feed(event) {
|
|
2634
|
+
if (this._timer) {
|
|
2635
|
+
clearTimeout(this._timer);
|
|
2636
|
+
this._timer = null;
|
|
2637
|
+
}
|
|
2638
|
+
const token = getKeyEventToken(event);
|
|
2639
|
+
let candidate = [...this._buffer, token];
|
|
2640
|
+
let matchingBindings = this._getMatchingBindings(candidate);
|
|
2641
|
+
if (matchingBindings.length === 0) {
|
|
2642
|
+
this._buffer = [];
|
|
2643
|
+
candidate = [token];
|
|
2644
|
+
matchingBindings = this._getMatchingBindings(candidate);
|
|
2645
|
+
if (matchingBindings.length === 0) {
|
|
2646
|
+
return false;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
this._buffer = candidate;
|
|
2650
|
+
const completedBindings = matchingBindings.filter(
|
|
2651
|
+
(binding) => binding.keys.length === candidate.length
|
|
2652
|
+
);
|
|
2653
|
+
if (completedBindings.length > 0) {
|
|
2654
|
+
for (const binding of completedBindings) {
|
|
2655
|
+
binding.handler();
|
|
2656
|
+
}
|
|
2657
|
+
this._buffer = [];
|
|
2658
|
+
return true;
|
|
2659
|
+
}
|
|
2660
|
+
this._timer = setTimeout(() => {
|
|
2661
|
+
this._buffer = [];
|
|
2662
|
+
this._timer = null;
|
|
2663
|
+
}, this._timeoutMs);
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
_getMatchingBindings(candidate) {
|
|
2667
|
+
return this._bindings.filter((binding) => {
|
|
2668
|
+
if (binding.keys.length < candidate.length) return false;
|
|
2669
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
2670
|
+
if (binding.keys[i] !== candidate[i]) return false;
|
|
2671
|
+
}
|
|
2672
|
+
return true;
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
// src/renderer/live-render.ts
|
|
2678
|
+
var LiveRender = class {
|
|
2679
|
+
constructor(terminal, screen) {
|
|
2680
|
+
this.terminal = terminal;
|
|
2681
|
+
this.screen = screen;
|
|
2682
|
+
}
|
|
2683
|
+
terminal;
|
|
2684
|
+
screen;
|
|
2685
|
+
getHeight(frame) {
|
|
2686
|
+
if (frame.length === 0) {
|
|
2687
|
+
return 0;
|
|
2688
|
+
}
|
|
2689
|
+
return frame.split("\n").length;
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Renders a serialized screen buffer.
|
|
2693
|
+
*
|
|
2694
|
+
* Widgets render into a Screen object first.
|
|
2695
|
+
* Callers should serialize the Screen contents into a string
|
|
2696
|
+
* before passing it to LiveRender.render().
|
|
2697
|
+
*/
|
|
2698
|
+
render(frame) {
|
|
2699
|
+
let output = "";
|
|
2700
|
+
const previousHeight = this.screen.lastRenderedHeight;
|
|
2701
|
+
if (previousHeight > 0) {
|
|
2702
|
+
output += moveUp(previousHeight);
|
|
2703
|
+
for (let i = 0; i < previousHeight; i++) {
|
|
2704
|
+
output += clearLine;
|
|
2705
|
+
if (i < previousHeight - 1) {
|
|
2706
|
+
output += "\n";
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
output += "\r";
|
|
2710
|
+
}
|
|
2711
|
+
output += frame;
|
|
2712
|
+
this.terminal.write(output);
|
|
2713
|
+
this.screen.lastRenderedHeight = this.getHeight(frame);
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
|
|
1583
2717
|
// src/style/Style.ts
|
|
1584
2718
|
function normalizeEdges(value) {
|
|
1585
2719
|
if (value === void 0) return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
@@ -1616,6 +2750,32 @@ function defaultStyle() {
|
|
|
1616
2750
|
gap: 0
|
|
1617
2751
|
};
|
|
1618
2752
|
}
|
|
2753
|
+
var LAYOUT_PROPS = /* @__PURE__ */ new Set([
|
|
2754
|
+
"width",
|
|
2755
|
+
"height",
|
|
2756
|
+
"minWidth",
|
|
2757
|
+
"minHeight",
|
|
2758
|
+
"maxWidth",
|
|
2759
|
+
"maxHeight",
|
|
2760
|
+
"padding",
|
|
2761
|
+
"margin",
|
|
2762
|
+
"border",
|
|
2763
|
+
"flexDirection",
|
|
2764
|
+
"justifyContent",
|
|
2765
|
+
"alignItems",
|
|
2766
|
+
"flexGrow",
|
|
2767
|
+
"flexShrink",
|
|
2768
|
+
"flexWrap",
|
|
2769
|
+
"gap",
|
|
2770
|
+
"overflow",
|
|
2771
|
+
"visible"
|
|
2772
|
+
]);
|
|
2773
|
+
function hasLayoutChanges(oldStyle, newStyle) {
|
|
2774
|
+
for (const key of LAYOUT_PROPS) {
|
|
2775
|
+
if (oldStyle[key] !== newStyle[key]) return true;
|
|
2776
|
+
}
|
|
2777
|
+
return false;
|
|
2778
|
+
}
|
|
1619
2779
|
function styleToCellAttrs(style) {
|
|
1620
2780
|
return {
|
|
1621
2781
|
fg: style.fg ?? { type: "none" },
|
|
@@ -1682,8 +2842,19 @@ var BORDER_CHARS = {
|
|
|
1682
2842
|
left: "\u2506"
|
|
1683
2843
|
}
|
|
1684
2844
|
};
|
|
1685
|
-
|
|
2845
|
+
var ASCII_BORDER_CHARS = {
|
|
2846
|
+
topLeft: "+",
|
|
2847
|
+
top: "-",
|
|
2848
|
+
topRight: "+",
|
|
2849
|
+
right: "|",
|
|
2850
|
+
bottomRight: "+",
|
|
2851
|
+
bottom: "-",
|
|
2852
|
+
bottomLeft: "+",
|
|
2853
|
+
left: "|"
|
|
2854
|
+
};
|
|
2855
|
+
function getBorderChars(style, customChars, asciiOnly = false) {
|
|
1686
2856
|
if (style === "none") return null;
|
|
2857
|
+
if (asciiOnly) return ASCII_BORDER_CHARS;
|
|
1687
2858
|
if (style === "custom") {
|
|
1688
2859
|
const base = BORDER_CHARS.single;
|
|
1689
2860
|
return { ...base, ...customChars };
|
|
@@ -1695,6 +2866,333 @@ function borderSize(style) {
|
|
|
1695
2866
|
return { horizontal: 2, vertical: 2 };
|
|
1696
2867
|
}
|
|
1697
2868
|
|
|
2869
|
+
// src/layout/pos.ts
|
|
2870
|
+
var Pos = class {
|
|
2871
|
+
/** Center the element within its parent */
|
|
2872
|
+
static center() {
|
|
2873
|
+
return new PosCenter();
|
|
2874
|
+
}
|
|
2875
|
+
/** Anchor the element `margin` units away from the end (right/bottom) */
|
|
2876
|
+
static anchorEnd(margin = 0) {
|
|
2877
|
+
return new PosAnchorEnd(margin);
|
|
2878
|
+
}
|
|
2879
|
+
/** Align multiple siblings as a group */
|
|
2880
|
+
static align(alignment, groupId) {
|
|
2881
|
+
return new PosAlign(alignment, groupId);
|
|
2882
|
+
}
|
|
2883
|
+
};
|
|
2884
|
+
var PosCenter = class extends Pos {
|
|
2885
|
+
dependencies() {
|
|
2886
|
+
return ["parentSize", "elementSize"];
|
|
2887
|
+
}
|
|
2888
|
+
evaluate(ctx) {
|
|
2889
|
+
const pSize = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
|
|
2890
|
+
const eSize = ctx.axis === "horizontal" ? ctx.elementWidth : ctx.elementHeight;
|
|
2891
|
+
return Math.floor((pSize - eSize) / 2);
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
var PosAnchorEnd = class extends Pos {
|
|
2895
|
+
constructor(margin) {
|
|
2896
|
+
super();
|
|
2897
|
+
this.margin = margin;
|
|
2898
|
+
}
|
|
2899
|
+
margin;
|
|
2900
|
+
dependencies() {
|
|
2901
|
+
return ["parentSize", "elementSize"];
|
|
2902
|
+
}
|
|
2903
|
+
evaluate(ctx) {
|
|
2904
|
+
const pSize = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
|
|
2905
|
+
const eSize = ctx.axis === "horizontal" ? ctx.elementWidth : ctx.elementHeight;
|
|
2906
|
+
return pSize - eSize - this.margin;
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
var PosAlign = class extends Pos {
|
|
2910
|
+
constructor(alignment, groupId) {
|
|
2911
|
+
super();
|
|
2912
|
+
this.alignment = alignment;
|
|
2913
|
+
this.groupId = groupId;
|
|
2914
|
+
}
|
|
2915
|
+
alignment;
|
|
2916
|
+
groupId;
|
|
2917
|
+
dependencies() {
|
|
2918
|
+
return ["group:" + this.groupId, "elementSize"];
|
|
2919
|
+
}
|
|
2920
|
+
evaluate(ctx) {
|
|
2921
|
+
const groupSize = ctx.getGroupSize(this.groupId);
|
|
2922
|
+
if (groupSize === 0) return 0;
|
|
2923
|
+
const pSize = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
|
|
2924
|
+
const eSize = ctx.axis === "horizontal" ? ctx.elementWidth : ctx.elementHeight;
|
|
2925
|
+
let groupOffset = 0;
|
|
2926
|
+
if (this.alignment === "center") {
|
|
2927
|
+
groupOffset = Math.floor((pSize - groupSize) / 2);
|
|
2928
|
+
} else if (this.alignment === "end") {
|
|
2929
|
+
groupOffset = pSize - groupSize;
|
|
2930
|
+
}
|
|
2931
|
+
const localOffset = Math.floor((groupSize - eSize) / 2);
|
|
2932
|
+
return groupOffset + localOffset;
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
|
|
2936
|
+
// src/layout/dim.ts
|
|
2937
|
+
var Dim = class {
|
|
2938
|
+
/** Size to the intrinsic content of the element */
|
|
2939
|
+
static auto() {
|
|
2940
|
+
return new DimAuto();
|
|
2941
|
+
}
|
|
2942
|
+
/** Fill remaining space in the parent, minus an optional margin */
|
|
2943
|
+
static fill(margin = 0) {
|
|
2944
|
+
return new DimFill(margin);
|
|
2945
|
+
}
|
|
2946
|
+
/** Custom function to determine size */
|
|
2947
|
+
static func(fn) {
|
|
2948
|
+
return new DimFunc(fn);
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
var DimAuto = class extends Dim {
|
|
2952
|
+
dependencies() {
|
|
2953
|
+
return ["contentSize"];
|
|
2954
|
+
}
|
|
2955
|
+
evaluate(ctx) {
|
|
2956
|
+
return ctx.axis === "horizontal" ? ctx.contentWidth : ctx.contentHeight;
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
var DimFill = class extends Dim {
|
|
2960
|
+
constructor(margin) {
|
|
2961
|
+
super();
|
|
2962
|
+
this.margin = margin;
|
|
2963
|
+
}
|
|
2964
|
+
margin;
|
|
2965
|
+
dependencies() {
|
|
2966
|
+
return ["parentSize"];
|
|
2967
|
+
}
|
|
2968
|
+
evaluate(ctx) {
|
|
2969
|
+
const avail = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
|
|
2970
|
+
return Math.max(0, avail - this.margin);
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
var DimFunc = class extends Dim {
|
|
2974
|
+
constructor(fn) {
|
|
2975
|
+
super();
|
|
2976
|
+
this.fn = fn;
|
|
2977
|
+
}
|
|
2978
|
+
fn;
|
|
2979
|
+
dependencies() {
|
|
2980
|
+
return [];
|
|
2981
|
+
}
|
|
2982
|
+
evaluate(ctx) {
|
|
2983
|
+
return this.fn(ctx);
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
|
|
2987
|
+
// src/layout/constraint.ts
|
|
2988
|
+
var Flex = /* @__PURE__ */ ((Flex2) => {
|
|
2989
|
+
Flex2["Start"] = "start";
|
|
2990
|
+
Flex2["Center"] = "center";
|
|
2991
|
+
Flex2["End"] = "end";
|
|
2992
|
+
Flex2["SpaceBetween"] = "space-between";
|
|
2993
|
+
Flex2["SpaceAround"] = "space-around";
|
|
2994
|
+
return Flex2;
|
|
2995
|
+
})(Flex || {});
|
|
2996
|
+
var Constraint = class {
|
|
2997
|
+
/** Fixed length in columns/rows */
|
|
2998
|
+
static Length(n) {
|
|
2999
|
+
return new LengthConstraint(n);
|
|
3000
|
+
}
|
|
3001
|
+
/** Percentage of available space (0-100) */
|
|
3002
|
+
static Percentage(n) {
|
|
3003
|
+
return new PercentageConstraint(n);
|
|
3004
|
+
}
|
|
3005
|
+
/** Minimum length */
|
|
3006
|
+
static Min(n) {
|
|
3007
|
+
return new MinConstraint(n);
|
|
3008
|
+
}
|
|
3009
|
+
/** Maximum length */
|
|
3010
|
+
static Max(n) {
|
|
3011
|
+
return new MaxConstraint(n);
|
|
3012
|
+
}
|
|
3013
|
+
/** Fills remaining space, with a flex weight */
|
|
3014
|
+
static Fill(weight = 1) {
|
|
3015
|
+
return new FillConstraint(weight);
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
var LengthConstraint = class extends Constraint {
|
|
3019
|
+
constructor(value) {
|
|
3020
|
+
super();
|
|
3021
|
+
this.value = value;
|
|
3022
|
+
}
|
|
3023
|
+
value;
|
|
3024
|
+
};
|
|
3025
|
+
var PercentageConstraint = class extends Constraint {
|
|
3026
|
+
constructor(value) {
|
|
3027
|
+
super();
|
|
3028
|
+
this.value = value;
|
|
3029
|
+
}
|
|
3030
|
+
value;
|
|
3031
|
+
};
|
|
3032
|
+
var MinConstraint = class extends Constraint {
|
|
3033
|
+
constructor(value) {
|
|
3034
|
+
super();
|
|
3035
|
+
this.value = value;
|
|
3036
|
+
}
|
|
3037
|
+
value;
|
|
3038
|
+
};
|
|
3039
|
+
var MaxConstraint = class extends Constraint {
|
|
3040
|
+
constructor(value) {
|
|
3041
|
+
super();
|
|
3042
|
+
this.value = value;
|
|
3043
|
+
}
|
|
3044
|
+
value;
|
|
3045
|
+
};
|
|
3046
|
+
var FillConstraint = class extends Constraint {
|
|
3047
|
+
constructor(weight) {
|
|
3048
|
+
super();
|
|
3049
|
+
this.weight = weight;
|
|
3050
|
+
}
|
|
3051
|
+
weight;
|
|
3052
|
+
};
|
|
3053
|
+
function resolveConstraints(available, constraints, flex = "start" /* Start */, gap = 0) {
|
|
3054
|
+
const n = constraints.length;
|
|
3055
|
+
if (n === 0) return [];
|
|
3056
|
+
const sizes = new Array(n).fill(0);
|
|
3057
|
+
const minSizes = new Array(n).fill(0);
|
|
3058
|
+
const maxSizes = new Array(n).fill(Infinity);
|
|
3059
|
+
let totalFixed = 0;
|
|
3060
|
+
let fillWeightSum = 0;
|
|
3061
|
+
for (let i = 0; i < n; i++) {
|
|
3062
|
+
const c = constraints[i];
|
|
3063
|
+
if (c instanceof LengthConstraint) {
|
|
3064
|
+
sizes[i] = c.value;
|
|
3065
|
+
totalFixed += c.value;
|
|
3066
|
+
} else if (c instanceof PercentageConstraint) {
|
|
3067
|
+
sizes[i] = Math.floor(available * c.value / 100);
|
|
3068
|
+
totalFixed += sizes[i];
|
|
3069
|
+
} else if (c instanceof MinConstraint) {
|
|
3070
|
+
minSizes[i] = c.value;
|
|
3071
|
+
} else if (c instanceof MaxConstraint) {
|
|
3072
|
+
maxSizes[i] = c.value;
|
|
3073
|
+
} else if (c instanceof FillConstraint) {
|
|
3074
|
+
fillWeightSum += c.weight;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
for (let i = 0; i < n; i++) {
|
|
3078
|
+
if (constraints[i] instanceof MinConstraint) {
|
|
3079
|
+
sizes[i] = minSizes[i];
|
|
3080
|
+
totalFixed += sizes[i];
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
const totalGaps = gap * (n - 1);
|
|
3084
|
+
let remaining = Math.max(0, available - totalFixed - totalGaps);
|
|
3085
|
+
if (fillWeightSum > 0) {
|
|
3086
|
+
let distributed = 0;
|
|
3087
|
+
for (let i = 0; i < n; i++) {
|
|
3088
|
+
const c = constraints[i];
|
|
3089
|
+
if (c instanceof FillConstraint) {
|
|
3090
|
+
const share = Math.floor(remaining * c.weight / fillWeightSum);
|
|
3091
|
+
sizes[i] = share;
|
|
3092
|
+
distributed += share;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
let leftover = remaining - distributed;
|
|
3096
|
+
if (leftover > 0) {
|
|
3097
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
3098
|
+
if (constraints[i] instanceof FillConstraint) {
|
|
3099
|
+
sizes[i] += leftover;
|
|
3100
|
+
break;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
const totalUsed = sizes.reduce((a, b) => a + b, 0) + totalGaps;
|
|
3106
|
+
const freeSpace = Math.max(0, available - totalUsed);
|
|
3107
|
+
const results = [];
|
|
3108
|
+
let offset = 0;
|
|
3109
|
+
let spaceBetween = 0;
|
|
3110
|
+
switch (flex) {
|
|
3111
|
+
case "start" /* Start */:
|
|
3112
|
+
break;
|
|
3113
|
+
case "end" /* End */:
|
|
3114
|
+
offset = freeSpace;
|
|
3115
|
+
break;
|
|
3116
|
+
case "center" /* Center */:
|
|
3117
|
+
offset = freeSpace / 2;
|
|
3118
|
+
break;
|
|
3119
|
+
case "space-between" /* SpaceBetween */:
|
|
3120
|
+
if (n > 1) spaceBetween = freeSpace / (n - 1);
|
|
3121
|
+
break;
|
|
3122
|
+
case "space-around" /* SpaceAround */:
|
|
3123
|
+
if (n > 0) {
|
|
3124
|
+
spaceBetween = freeSpace / n;
|
|
3125
|
+
offset = spaceBetween / 2;
|
|
3126
|
+
}
|
|
3127
|
+
break;
|
|
3128
|
+
}
|
|
3129
|
+
for (let i = 0; i < n; i++) {
|
|
3130
|
+
results.push({ offset: Math.floor(offset), size: sizes[i] });
|
|
3131
|
+
offset += sizes[i] + gap + spaceBetween;
|
|
3132
|
+
}
|
|
3133
|
+
return results;
|
|
3134
|
+
}
|
|
3135
|
+
function resolveLayoutVariables(nodes, parentWidth, parentHeight) {
|
|
3136
|
+
const state = /* @__PURE__ */ new Map();
|
|
3137
|
+
function evaluateVariable(node, varName) {
|
|
3138
|
+
const key = `${node.id}:${varName}`;
|
|
3139
|
+
const existing = state.get(key);
|
|
3140
|
+
if (existing === "computing") {
|
|
3141
|
+
throw new Error(`Cycle detected resolving ${key}`);
|
|
3142
|
+
}
|
|
3143
|
+
if (typeof existing === "number") {
|
|
3144
|
+
return existing;
|
|
3145
|
+
}
|
|
3146
|
+
state.set(key, "computing");
|
|
3147
|
+
let result = 0;
|
|
3148
|
+
const val = node[varName];
|
|
3149
|
+
const ctx = {
|
|
3150
|
+
parentWidth,
|
|
3151
|
+
parentHeight,
|
|
3152
|
+
axis: varName === "x" || varName === "width" ? "horizontal" : "vertical",
|
|
3153
|
+
contentWidth: node.contentWidth,
|
|
3154
|
+
contentHeight: node.contentHeight,
|
|
3155
|
+
get elementWidth() {
|
|
3156
|
+
return evaluateVariable(node, "width");
|
|
3157
|
+
},
|
|
3158
|
+
get elementHeight() {
|
|
3159
|
+
return evaluateVariable(node, "height");
|
|
3160
|
+
},
|
|
3161
|
+
get elementX() {
|
|
3162
|
+
return evaluateVariable(node, "x");
|
|
3163
|
+
},
|
|
3164
|
+
get elementY() {
|
|
3165
|
+
return evaluateVariable(node, "y");
|
|
3166
|
+
},
|
|
3167
|
+
getGroupSize(groupId) {
|
|
3168
|
+
const groupNodes = nodes.filter((n) => n.groupId === groupId);
|
|
3169
|
+
let maxSize = 0;
|
|
3170
|
+
for (const gNode of groupNodes) {
|
|
3171
|
+
const size = evaluateVariable(gNode, this.axis === "horizontal" ? "width" : "height");
|
|
3172
|
+
maxSize = Math.max(maxSize, size);
|
|
3173
|
+
}
|
|
3174
|
+
return maxSize;
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
if (val instanceof Pos) {
|
|
3178
|
+
result = val.evaluate(ctx);
|
|
3179
|
+
} else if (val instanceof Dim) {
|
|
3180
|
+
result = val.evaluate(ctx);
|
|
3181
|
+
} else if (typeof val === "number") {
|
|
3182
|
+
result = val;
|
|
3183
|
+
}
|
|
3184
|
+
state.set(key, result);
|
|
3185
|
+
node.computed[varName] = result;
|
|
3186
|
+
return result;
|
|
3187
|
+
}
|
|
3188
|
+
for (const node of nodes) {
|
|
3189
|
+
evaluateVariable(node, "width");
|
|
3190
|
+
evaluateVariable(node, "height");
|
|
3191
|
+
evaluateVariable(node, "x");
|
|
3192
|
+
evaluateVariable(node, "y");
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
1698
3196
|
// src/layout/LayoutEngine.ts
|
|
1699
3197
|
function createLayoutNode(id, style, children = []) {
|
|
1700
3198
|
return {
|
|
@@ -1702,14 +3200,44 @@ function createLayoutNode(id, style, children = []) {
|
|
|
1702
3200
|
style,
|
|
1703
3201
|
children,
|
|
1704
3202
|
computed: { x: 0, y: 0, width: 0, height: 0 },
|
|
1705
|
-
_dirty: true
|
|
3203
|
+
_dirty: true,
|
|
3204
|
+
_lastContainerWidth: 0,
|
|
3205
|
+
_lastContainerHeight: 0,
|
|
3206
|
+
_lastComputedWidth: 0,
|
|
3207
|
+
_lastComputedHeight: 0,
|
|
3208
|
+
_draggable: false,
|
|
3209
|
+
_dragging: false
|
|
1706
3210
|
};
|
|
1707
3211
|
}
|
|
1708
3212
|
function computeLayout(root, containerWidth, containerHeight) {
|
|
3213
|
+
const sizeChanged = root._lastContainerWidth !== containerWidth || root._lastContainerHeight !== containerHeight;
|
|
3214
|
+
if (!sizeChanged && !root._dirty && !hasDirtyChild(root)) {
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
root._lastContainerWidth = containerWidth;
|
|
3218
|
+
root._lastContainerHeight = containerHeight;
|
|
1709
3219
|
root.computed = { x: 0, y: 0, width: containerWidth, height: containerHeight };
|
|
1710
3220
|
layoutNode(root, containerWidth, containerHeight);
|
|
3221
|
+
root.computed.width = containerWidth;
|
|
3222
|
+
root.computed.height = containerHeight;
|
|
3223
|
+
}
|
|
3224
|
+
function invalidateLayout(node) {
|
|
3225
|
+
node._dirty = true;
|
|
3226
|
+
for (const child of node.children) {
|
|
3227
|
+
invalidateLayout(child);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
function hasDirtyChild(node) {
|
|
3231
|
+
if (node._dirty) return true;
|
|
3232
|
+
for (const child of node.children) {
|
|
3233
|
+
if (hasDirtyChild(child)) return true;
|
|
3234
|
+
}
|
|
3235
|
+
return false;
|
|
1711
3236
|
}
|
|
1712
3237
|
function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
3238
|
+
if (!node._dirty && node._lastComputedWidth === node.computed.width && node._lastComputedHeight === node.computed.height) {
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
1713
3241
|
const style = node.style;
|
|
1714
3242
|
const padding = normalizeEdges(style.padding);
|
|
1715
3243
|
const margin = normalizeEdges(style.margin);
|
|
@@ -1719,6 +3247,8 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1719
3247
|
let nodeHeight2 = resolveSize(style.height, availHeight);
|
|
1720
3248
|
if (nodeWidth2 === void 0) nodeWidth2 = availWidth - margin.left - margin.right;
|
|
1721
3249
|
if (nodeHeight2 === void 0) nodeHeight2 = availHeight - margin.top - margin.bottom;
|
|
3250
|
+
if (!Number.isFinite(nodeWidth2)) nodeWidth2 = 0;
|
|
3251
|
+
if (!Number.isFinite(nodeHeight2)) nodeHeight2 = 0;
|
|
1722
3252
|
nodeWidth2 = clampSize(nodeWidth2, style.minWidth, style.maxWidth);
|
|
1723
3253
|
nodeHeight2 = clampSize(nodeHeight2, style.minHeight, style.maxHeight);
|
|
1724
3254
|
node.computed.width = nodeWidth2;
|
|
@@ -1726,23 +3256,105 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1726
3256
|
}
|
|
1727
3257
|
if (node.children.length === 0) {
|
|
1728
3258
|
node._dirty = false;
|
|
3259
|
+
node._lastComputedWidth = node.computed.width;
|
|
3260
|
+
node._lastComputedHeight = node.computed.height;
|
|
1729
3261
|
return;
|
|
1730
3262
|
}
|
|
1731
3263
|
const nodeWidth = node.computed.width;
|
|
1732
3264
|
const nodeHeight = node.computed.height;
|
|
1733
|
-
const innerX = padding.left + border.horizontal
|
|
1734
|
-
const innerY = padding.top + border.vertical
|
|
3265
|
+
const innerX = padding.left + (border.horizontal > 0 ? 1 : 0);
|
|
3266
|
+
const innerY = padding.top + (border.vertical > 0 ? 1 : 0);
|
|
1735
3267
|
const innerWidth = Math.max(0, nodeWidth - padding.left - padding.right - border.horizontal);
|
|
1736
3268
|
const innerHeight = Math.max(0, nodeHeight - padding.top - padding.bottom - border.vertical);
|
|
1737
3269
|
const direction = style.flexDirection ?? "column";
|
|
1738
3270
|
const isRow = direction === "row";
|
|
1739
3271
|
const gap = style.gap ?? 0;
|
|
3272
|
+
if (style.constraints && style.constraints.length > 0) {
|
|
3273
|
+
const mainAvail2 = isRow ? innerWidth : innerHeight;
|
|
3274
|
+
let flexJustify = "start" /* Start */;
|
|
3275
|
+
if (style.justifyContent === "space-between") flexJustify = "space-between" /* SpaceBetween */;
|
|
3276
|
+
else if (style.justifyContent === "space-around") flexJustify = "space-around" /* SpaceAround */;
|
|
3277
|
+
else if (style.justifyContent === "center") flexJustify = "center" /* Center */;
|
|
3278
|
+
else if (style.justifyContent === "flex-end") flexJustify = "end" /* End */;
|
|
3279
|
+
const results = resolveConstraints(mainAvail2, style.constraints, flexJustify);
|
|
3280
|
+
let visibleIndex = 0;
|
|
3281
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
3282
|
+
const child = node.children[i];
|
|
3283
|
+
if (visibleIndex >= results.length) break;
|
|
3284
|
+
if (child.style.visible === false) continue;
|
|
3285
|
+
const res = results[visibleIndex];
|
|
3286
|
+
const childMargin = normalizeEdges(child.style.margin);
|
|
3287
|
+
if (isRow) {
|
|
3288
|
+
child.computed = {
|
|
3289
|
+
x: Math.floor(node.computed.x + innerX + res.offset + childMargin.left),
|
|
3290
|
+
y: Math.floor(node.computed.y + innerY + childMargin.top),
|
|
3291
|
+
width: Math.round(Math.max(0, res.size - childMargin.left - childMargin.right)),
|
|
3292
|
+
height: Math.round(Math.max(0, innerHeight - childMargin.top - childMargin.bottom))
|
|
3293
|
+
};
|
|
3294
|
+
} else {
|
|
3295
|
+
child.computed = {
|
|
3296
|
+
x: Math.floor(node.computed.x + innerX + childMargin.left),
|
|
3297
|
+
y: Math.floor(node.computed.y + innerY + res.offset + childMargin.top),
|
|
3298
|
+
width: Math.round(Math.max(0, innerWidth - childMargin.left - childMargin.right)),
|
|
3299
|
+
height: Math.round(Math.max(0, res.size - childMargin.top - childMargin.bottom))
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
layoutNode(child, child.computed.width, child.computed.height, true);
|
|
3303
|
+
visibleIndex++;
|
|
3304
|
+
}
|
|
3305
|
+
node._dirty = false;
|
|
3306
|
+
node._lastComputedWidth = node.computed.width;
|
|
3307
|
+
node._lastComputedHeight = node.computed.height;
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
const topologicalChildren = [];
|
|
3311
|
+
const flexChildren = [];
|
|
3312
|
+
for (const child of node.children) {
|
|
3313
|
+
if (child.style.visible === false) continue;
|
|
3314
|
+
const s = child.style;
|
|
3315
|
+
if (s.x instanceof Pos || s.y instanceof Pos || s.width instanceof Dim || s.height instanceof Dim || s.groupId != null) {
|
|
3316
|
+
topologicalChildren.push(child);
|
|
3317
|
+
} else {
|
|
3318
|
+
flexChildren.push(child);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
if (topologicalChildren.length > 0) {
|
|
3322
|
+
const resolvableNodes = topologicalChildren.map((child) => {
|
|
3323
|
+
const s = child.style;
|
|
3324
|
+
let cw = 0, ch = 0;
|
|
3325
|
+
if (typeof s.width === "number") cw = s.width;
|
|
3326
|
+
if (typeof s.height === "number") ch = s.height;
|
|
3327
|
+
return {
|
|
3328
|
+
id: child.id,
|
|
3329
|
+
x: s.x,
|
|
3330
|
+
y: s.y,
|
|
3331
|
+
width: typeof s.width === "string" ? void 0 : s.width,
|
|
3332
|
+
height: typeof s.height === "string" ? void 0 : s.height,
|
|
3333
|
+
contentWidth: cw,
|
|
3334
|
+
contentHeight: ch,
|
|
3335
|
+
groupId: s.groupId,
|
|
3336
|
+
computed: { x: 0, y: 0, width: 0, height: 0 },
|
|
3337
|
+
_originalNode: child
|
|
3338
|
+
// keep reference
|
|
3339
|
+
};
|
|
3340
|
+
});
|
|
3341
|
+
resolveLayoutVariables(resolvableNodes, innerWidth, innerHeight);
|
|
3342
|
+
for (const rNode of resolvableNodes) {
|
|
3343
|
+
const child = rNode._originalNode;
|
|
3344
|
+
child.computed = {
|
|
3345
|
+
x: Math.floor(node.computed.x + innerX + rNode.computed.x),
|
|
3346
|
+
y: Math.floor(node.computed.y + innerY + rNode.computed.y),
|
|
3347
|
+
width: Math.round(Math.max(0, rNode.computed.width)),
|
|
3348
|
+
height: Math.round(Math.max(0, rNode.computed.height))
|
|
3349
|
+
};
|
|
3350
|
+
layoutNode(child, child.computed.width, child.computed.height, true);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
1740
3353
|
const childInfos = [];
|
|
1741
3354
|
let totalFixed = 0;
|
|
1742
3355
|
let totalGrow = 0;
|
|
1743
3356
|
let totalShrink = 0;
|
|
1744
|
-
for (const child of
|
|
1745
|
-
if (child.style.visible === false) continue;
|
|
3357
|
+
for (const child of flexChildren) {
|
|
1746
3358
|
const childMargin = normalizeEdges(child.style.margin);
|
|
1747
3359
|
const childBorder = borderSize(child.style.border ?? "none");
|
|
1748
3360
|
const grow = child.style.flexGrow ?? 0;
|
|
@@ -1849,20 +3461,26 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1849
3461
|
layoutNode(info.node, info.node.computed.width, info.node.computed.height, true);
|
|
1850
3462
|
}
|
|
1851
3463
|
node._dirty = false;
|
|
3464
|
+
node._lastComputedWidth = node.computed.width;
|
|
3465
|
+
node._lastComputedHeight = node.computed.height;
|
|
1852
3466
|
}
|
|
1853
3467
|
function resolveSize(value, available) {
|
|
1854
3468
|
if (value === void 0) return void 0;
|
|
1855
|
-
if (typeof value === "number")
|
|
3469
|
+
if (typeof value === "number") {
|
|
3470
|
+
if (!Number.isFinite(value) || value < 0) return 0;
|
|
3471
|
+
return value;
|
|
3472
|
+
}
|
|
1856
3473
|
if (typeof value === "string" && value.endsWith("%")) {
|
|
1857
3474
|
const pct = parseFloat(value) / 100;
|
|
3475
|
+
if (!Number.isFinite(pct)) return 0;
|
|
1858
3476
|
return Math.floor(available * pct);
|
|
1859
3477
|
}
|
|
1860
3478
|
return void 0;
|
|
1861
3479
|
}
|
|
1862
|
-
function clampSize(value,
|
|
3480
|
+
function clampSize(value, min, max) {
|
|
1863
3481
|
let result = value;
|
|
1864
|
-
if (
|
|
1865
|
-
if (
|
|
3482
|
+
if (min !== void 0) result = Math.max(result, min);
|
|
3483
|
+
if (max !== void 0) result = Math.min(result, max);
|
|
1866
3484
|
return result;
|
|
1867
3485
|
}
|
|
1868
3486
|
|
|
@@ -1897,94 +3515,6 @@ function unionRect(a, b) {
|
|
|
1897
3515
|
return { x, y, width: r - x, height: bot - y };
|
|
1898
3516
|
}
|
|
1899
3517
|
|
|
1900
|
-
// src/layout/ConstraintLayout.ts
|
|
1901
|
-
var length = (n) => ({ type: "length", value: n });
|
|
1902
|
-
var percentage = (n) => ({ type: "percentage", value: n });
|
|
1903
|
-
var ratio = (num, den) => ({ type: "ratio", num, den });
|
|
1904
|
-
var min = (n) => ({ type: "min", value: n });
|
|
1905
|
-
var max = (n) => ({ type: "max", value: n });
|
|
1906
|
-
var fill = (weight = 1) => ({ type: "fill", weight });
|
|
1907
|
-
function resolveSize2(constraint, available) {
|
|
1908
|
-
switch (constraint.type) {
|
|
1909
|
-
case "length":
|
|
1910
|
-
return Math.min(constraint.value, available);
|
|
1911
|
-
case "percentage":
|
|
1912
|
-
return Math.min(Math.floor(available * constraint.value / 100), available);
|
|
1913
|
-
case "ratio":
|
|
1914
|
-
return constraint.den === 0 ? 0 : Math.min(
|
|
1915
|
-
Math.floor(available * constraint.num / constraint.den),
|
|
1916
|
-
available
|
|
1917
|
-
);
|
|
1918
|
-
case "min":
|
|
1919
|
-
return constraint.value;
|
|
1920
|
-
case "max":
|
|
1921
|
-
return Math.min(constraint.value, available);
|
|
1922
|
-
case "fill":
|
|
1923
|
-
return 0;
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
function splitRect(rect, constraints, direction = "vertical", gap = 0) {
|
|
1927
|
-
if (constraints.length === 0) return [];
|
|
1928
|
-
const totalAvailable = direction === "horizontal" ? rect.width : rect.height;
|
|
1929
|
-
const count = constraints.length;
|
|
1930
|
-
const totalGaps = count > 1 ? gap * (count - 1) : 0;
|
|
1931
|
-
const availableForConstraints = Math.max(0, totalAvailable - totalGaps);
|
|
1932
|
-
const sizes = [];
|
|
1933
|
-
let usedSpace = 0;
|
|
1934
|
-
let fillWeightSum = 0;
|
|
1935
|
-
for (const constraint of constraints) {
|
|
1936
|
-
if (constraint.type === "fill") {
|
|
1937
|
-
sizes.push(0);
|
|
1938
|
-
fillWeightSum += Math.max(1, constraint.weight);
|
|
1939
|
-
} else {
|
|
1940
|
-
const size = resolveSize2(constraint, availableForConstraints);
|
|
1941
|
-
sizes.push(size);
|
|
1942
|
-
usedSpace += size;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
if (fillWeightSum > 0) {
|
|
1946
|
-
const remaining = Math.max(0, availableForConstraints - usedSpace);
|
|
1947
|
-
let distributed = 0;
|
|
1948
|
-
for (let i = 0; i < count; i++) {
|
|
1949
|
-
const constraint = constraints[i];
|
|
1950
|
-
if (!constraint || constraint.type !== "fill") continue;
|
|
1951
|
-
const weight = Math.max(1, constraint.weight);
|
|
1952
|
-
const share = Math.floor(remaining * weight / fillWeightSum);
|
|
1953
|
-
sizes[i] = share;
|
|
1954
|
-
distributed += share;
|
|
1955
|
-
}
|
|
1956
|
-
const leftover = remaining - distributed;
|
|
1957
|
-
if (leftover > 0) {
|
|
1958
|
-
for (let i = count - 1; i >= 0; i--) {
|
|
1959
|
-
const constraint = constraints[i];
|
|
1960
|
-
if (constraint && constraint.type === "fill") {
|
|
1961
|
-
sizes[i] = (sizes[i] ?? 0) + leftover;
|
|
1962
|
-
break;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
let totalUsed = 0;
|
|
1968
|
-
for (let i = 0; i < count; i++) {
|
|
1969
|
-
const size = sizes[i] ?? 0;
|
|
1970
|
-
const clamped = Math.max(0, Math.min(size, availableForConstraints - totalUsed));
|
|
1971
|
-
sizes[i] = clamped;
|
|
1972
|
-
totalUsed += clamped;
|
|
1973
|
-
}
|
|
1974
|
-
const results = [];
|
|
1975
|
-
let offset = 0;
|
|
1976
|
-
for (let i = 0; i < count; i++) {
|
|
1977
|
-
const size = sizes[i] ?? 0;
|
|
1978
|
-
if (direction === "horizontal") {
|
|
1979
|
-
results.push({ x: rect.x + offset, y: rect.y, width: size, height: rect.height });
|
|
1980
|
-
} else {
|
|
1981
|
-
results.push({ x: rect.x, y: rect.y + offset, width: rect.width, height: size });
|
|
1982
|
-
}
|
|
1983
|
-
offset += size + gap;
|
|
1984
|
-
}
|
|
1985
|
-
return results;
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
3518
|
// src/events/FocusManager.ts
|
|
1989
3519
|
var FocusManager = class {
|
|
1990
3520
|
_focusables = [];
|
|
@@ -2005,6 +3535,16 @@ var FocusManager = class {
|
|
|
2005
3535
|
* Maps groupId → ordered list of widget IDs.
|
|
2006
3536
|
*/
|
|
2007
3537
|
_groups = /* @__PURE__ */ new Map();
|
|
3538
|
+
/**
|
|
3539
|
+
* Record of on-screen rects for widgets, used for spatial navigation.
|
|
3540
|
+
*/
|
|
3541
|
+
_rects = /* @__PURE__ */ new Map();
|
|
3542
|
+
/** Monotonically increasing epoch for ordered event sequencing */
|
|
3543
|
+
_epoch = 0;
|
|
3544
|
+
/** Queue of focus state changes accumulated before start() is called */
|
|
3545
|
+
_pendingQueue = [];
|
|
3546
|
+
/** True once start() has been called — enables event emission */
|
|
3547
|
+
_started = false;
|
|
2008
3548
|
/** Currently focused widget ID, or null if none */
|
|
2009
3549
|
get currentId() {
|
|
2010
3550
|
if (this._currentIndex < 0 || this._currentIndex >= this._focusables.length) {
|
|
@@ -2016,16 +3556,35 @@ var FocusManager = class {
|
|
|
2016
3556
|
on(event, handler) {
|
|
2017
3557
|
return this._events.on(event, handler);
|
|
2018
3558
|
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Enable event emission and replay any queued focus events.
|
|
3561
|
+
* Call this from App.mount() after _subscribeFocusEvents().
|
|
3562
|
+
*/
|
|
3563
|
+
start() {
|
|
3564
|
+
if (this._started) return;
|
|
3565
|
+
this._started = true;
|
|
3566
|
+
for (const evt of this._pendingQueue) {
|
|
3567
|
+
this._events.emit(evt.type, evt);
|
|
3568
|
+
}
|
|
3569
|
+
this._pendingQueue = [];
|
|
3570
|
+
}
|
|
2019
3571
|
/**
|
|
2020
3572
|
* Register a focusable widget.
|
|
2021
3573
|
* Widgets are ordered by tabIndex (ascending), then insertion order.
|
|
3574
|
+
* Before start() is called, events are queued rather than emitted so
|
|
3575
|
+
* they are not lost when App has not yet subscribed to them.
|
|
2022
3576
|
*/
|
|
2023
3577
|
register(focusable) {
|
|
2024
3578
|
this._focusables.push(focusable);
|
|
2025
3579
|
this._focusables.sort((a, b) => a.tabIndex - b.tabIndex);
|
|
2026
3580
|
if (this._currentIndex < 0 && focusable.focusable) {
|
|
2027
3581
|
this._currentIndex = this._focusables.indexOf(focusable);
|
|
2028
|
-
|
|
3582
|
+
const event = { targetId: focusable.id, type: "focus", epoch: this._epoch++ };
|
|
3583
|
+
if (this._started) {
|
|
3584
|
+
this._events.emit("focus", event);
|
|
3585
|
+
} else {
|
|
3586
|
+
this._pendingQueue.push(event);
|
|
3587
|
+
}
|
|
2029
3588
|
}
|
|
2030
3589
|
}
|
|
2031
3590
|
/**
|
|
@@ -2034,21 +3593,31 @@ var FocusManager = class {
|
|
|
2034
3593
|
unregister(id) {
|
|
2035
3594
|
const idx = this._focusables.findIndex((f) => f.id === id);
|
|
2036
3595
|
if (idx < 0) return;
|
|
3596
|
+
this._rects.delete(id);
|
|
2037
3597
|
const wasFocused = idx === this._currentIndex;
|
|
2038
3598
|
this._focusables.splice(idx, 1);
|
|
2039
3599
|
if (wasFocused) {
|
|
2040
|
-
this._events.emit("blur", { targetId: id, type: "blur" });
|
|
3600
|
+
this._events.emit("blur", { targetId: id, type: "blur", epoch: this._epoch++ });
|
|
2041
3601
|
if (this._focusables.length > 0) {
|
|
2042
3602
|
this._currentIndex = Math.min(this._currentIndex, this._focusables.length - 1);
|
|
2043
3603
|
this._events.emit("focus", {
|
|
2044
3604
|
targetId: this._focusables[this._currentIndex].id,
|
|
2045
|
-
type: "focus"
|
|
3605
|
+
type: "focus",
|
|
3606
|
+
epoch: this._epoch++
|
|
2046
3607
|
});
|
|
2047
3608
|
} else {
|
|
2048
3609
|
this._currentIndex = -1;
|
|
2049
3610
|
}
|
|
2050
3611
|
} else if (idx < this._currentIndex) {
|
|
2051
3612
|
this._currentIndex--;
|
|
3613
|
+
this._events.emit("blur", { targetId: id, type: "blur", epoch: this._epoch++ });
|
|
3614
|
+
if (this._currentIndex >= 0 && this._currentIndex < this._focusables.length) {
|
|
3615
|
+
this._events.emit("focus", {
|
|
3616
|
+
targetId: this._focusables[this._currentIndex].id,
|
|
3617
|
+
type: "focus",
|
|
3618
|
+
epoch: this._epoch++
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
2052
3621
|
}
|
|
2053
3622
|
}
|
|
2054
3623
|
/**
|
|
@@ -2192,6 +3761,69 @@ var FocusManager = class {
|
|
|
2192
3761
|
}
|
|
2193
3762
|
return false;
|
|
2194
3763
|
}
|
|
3764
|
+
// ── Spatial Navigation ──────────────────────────────
|
|
3765
|
+
/** Record the on-screen rect for a widget, used for spatial navigation. */
|
|
3766
|
+
setRect(id, rect) {
|
|
3767
|
+
this._rects.set(id, rect);
|
|
3768
|
+
}
|
|
3769
|
+
_spatialFocus(isValid, calcDistance) {
|
|
3770
|
+
const currentId = this.currentId;
|
|
3771
|
+
if (!currentId) return false;
|
|
3772
|
+
const currentRect = this._rects.get(currentId);
|
|
3773
|
+
if (!currentRect) return false;
|
|
3774
|
+
const cx = currentRect.x + currentRect.width / 2;
|
|
3775
|
+
const cy = currentRect.y + currentRect.height / 2;
|
|
3776
|
+
let bestId = null;
|
|
3777
|
+
let minDistance = Infinity;
|
|
3778
|
+
const candidates = this._getActiveFocusables();
|
|
3779
|
+
for (const node of candidates) {
|
|
3780
|
+
if (!node.focusable || node.id === currentId) continue;
|
|
3781
|
+
const targetRect = this._rects.get(node.id);
|
|
3782
|
+
if (!targetRect) continue;
|
|
3783
|
+
const tx = targetRect.x + targetRect.width / 2;
|
|
3784
|
+
const ty = targetRect.y + targetRect.height / 2;
|
|
3785
|
+
if (isValid(cx, cy, tx, ty)) {
|
|
3786
|
+
const dist = calcDistance(cx, cy, tx, ty);
|
|
3787
|
+
if (dist < minDistance) {
|
|
3788
|
+
minDistance = dist;
|
|
3789
|
+
bestId = node.id;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
if (bestId) {
|
|
3794
|
+
this.focusWidget(bestId);
|
|
3795
|
+
return true;
|
|
3796
|
+
}
|
|
3797
|
+
return false;
|
|
3798
|
+
}
|
|
3799
|
+
/** Move focus to the nearest focusable widget above the current one. */
|
|
3800
|
+
focusUp() {
|
|
3801
|
+
return this._spatialFocus(
|
|
3802
|
+
(cx, cy, tx, ty) => ty < cy,
|
|
3803
|
+
(cx, cy, tx, ty) => cy - ty + Math.abs(tx - cx)
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
/** Move focus to the nearest focusable widget below the current one. */
|
|
3807
|
+
focusDown() {
|
|
3808
|
+
return this._spatialFocus(
|
|
3809
|
+
(cx, cy, tx, ty) => ty > cy,
|
|
3810
|
+
(cx, cy, tx, ty) => ty - cy + Math.abs(tx - cx)
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
/** Move focus to the nearest focusable widget to the left of the current one. */
|
|
3814
|
+
focusLeft() {
|
|
3815
|
+
return this._spatialFocus(
|
|
3816
|
+
(cx, cy, tx, ty) => tx < cx,
|
|
3817
|
+
(cx, cy, tx, ty) => cx - tx + Math.abs(ty - cy)
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
/** Move focus to the nearest focusable widget to the right of the current one. */
|
|
3821
|
+
focusRight() {
|
|
3822
|
+
return this._spatialFocus(
|
|
3823
|
+
(cx, cy, tx, ty) => tx > cx,
|
|
3824
|
+
(cx, cy, tx, ty) => tx - cx + Math.abs(ty - cy)
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
2195
3827
|
// ── Private ──────────────────────────────────────────
|
|
2196
3828
|
/**
|
|
2197
3829
|
* Get the active focusables, filtered by the current trap if any.
|
|
@@ -2206,12 +3838,13 @@ var FocusManager = class {
|
|
|
2206
3838
|
_changeFocus(newIndex) {
|
|
2207
3839
|
const oldId = this.currentId;
|
|
2208
3840
|
if (oldId) {
|
|
2209
|
-
this._events.emit("blur", { targetId: oldId, type: "blur" });
|
|
3841
|
+
this._events.emit("blur", { targetId: oldId, type: "blur", epoch: this._epoch++ });
|
|
2210
3842
|
}
|
|
2211
3843
|
this._currentIndex = newIndex;
|
|
2212
3844
|
this._events.emit("focus", {
|
|
2213
3845
|
targetId: this._focusables[newIndex].id,
|
|
2214
|
-
type: "focus"
|
|
3846
|
+
type: "focus",
|
|
3847
|
+
epoch: this._epoch++
|
|
2215
3848
|
});
|
|
2216
3849
|
}
|
|
2217
3850
|
};
|
|
@@ -2446,6 +4079,23 @@ function renderFallback(screen) {
|
|
|
2446
4079
|
return lines.join("\n");
|
|
2447
4080
|
}
|
|
2448
4081
|
|
|
4082
|
+
// src/inline-viewport.ts
|
|
4083
|
+
function renderInlineToTerminal(terminal, screen, rows) {
|
|
4084
|
+
const totalRows = screen.rows;
|
|
4085
|
+
const start = Math.max(0, totalRows - rows);
|
|
4086
|
+
const lines = [];
|
|
4087
|
+
for (let r = start; r < totalRows; r++) {
|
|
4088
|
+
const row = screen.back[r];
|
|
4089
|
+
if (!row) continue;
|
|
4090
|
+
lines.push(row.map((c) => c.char || " ").join(""));
|
|
4091
|
+
}
|
|
4092
|
+
if (lines.length === 0) return;
|
|
4093
|
+
terminal.write(lines.join("\n") + "\n");
|
|
4094
|
+
}
|
|
4095
|
+
function createInlineViewport(opts) {
|
|
4096
|
+
return { rows: opts.rows };
|
|
4097
|
+
}
|
|
4098
|
+
|
|
2449
4099
|
// src/app/App.ts
|
|
2450
4100
|
var App = class {
|
|
2451
4101
|
terminal;
|
|
@@ -2461,18 +4111,38 @@ var App = class {
|
|
|
2461
4111
|
_exitResolve = null;
|
|
2462
4112
|
_unsubKey = null;
|
|
2463
4113
|
_unsubMouse = null;
|
|
4114
|
+
_unsubPaste = null;
|
|
4115
|
+
_unsubFocus = null;
|
|
4116
|
+
_unsubBlur = null;
|
|
4117
|
+
_unsubSigInt = null;
|
|
4118
|
+
_unsubSigTerm = null;
|
|
4119
|
+
_unsubUncaughtException = null;
|
|
4120
|
+
_unsubUnhandledRejection = null;
|
|
2464
4121
|
_widgetById = /* @__PURE__ */ new Map();
|
|
4122
|
+
// any: Widget shape varies; narrowed at retrieval
|
|
4123
|
+
_pendingFocusState = /* @__PURE__ */ new Map();
|
|
4124
|
+
_consecutiveRenderFailures = 0;
|
|
4125
|
+
static MAX_RENDER_FAILURES = 5;
|
|
4126
|
+
// Lines to insert before inline viewport output. Each entry: { id: symbol, text: string }
|
|
4127
|
+
_insertBefore = [];
|
|
4128
|
+
// Core fix patch: Track if a paint task has been queued for the next event loop tick
|
|
4129
|
+
_isRenderPending = false;
|
|
2465
4130
|
constructor(rootWidget, options = {}) {
|
|
2466
4131
|
this._rootWidget = rootWidget;
|
|
2467
4132
|
this._options = {
|
|
2468
4133
|
fullscreen: true,
|
|
2469
4134
|
mouse: false,
|
|
2470
4135
|
fps: 30,
|
|
4136
|
+
dockBorders: false,
|
|
4137
|
+
diffRenderer: true,
|
|
4138
|
+
// Default screenMode: if fullscreen explicitly disabled, treat as 'main', otherwise 'alternate'
|
|
4139
|
+
screenMode: options.fullscreen === false ? "main" : "alternate",
|
|
4140
|
+
inlineRows: 0,
|
|
2471
4141
|
...options
|
|
2472
4142
|
};
|
|
2473
4143
|
this.terminal = new Terminal(options);
|
|
2474
4144
|
this.screen = new Screen(this.terminal.cols, this.terminal.rows);
|
|
2475
|
-
this.renderer = new Renderer(this.terminal, this.screen, this._options.fps);
|
|
4145
|
+
this.renderer = new Renderer(this.terminal, this.screen, this._options.fps, this._options.diffRenderer);
|
|
2476
4146
|
this.input = new InputParser(this.terminal.stdin);
|
|
2477
4147
|
this.focus = new FocusManager();
|
|
2478
4148
|
this.events = new EventEmitter();
|
|
@@ -2480,8 +4150,6 @@ var App = class {
|
|
|
2480
4150
|
}
|
|
2481
4151
|
/**
|
|
2482
4152
|
* Start the application.
|
|
2483
|
-
* Sets up the terminal, starts the render loop, and mounts the root widget.
|
|
2484
|
-
* Returns a promise that resolves when exit() is called.
|
|
2485
4153
|
*/
|
|
2486
4154
|
async mount() {
|
|
2487
4155
|
if (this._mounted) return 0;
|
|
@@ -2490,8 +4158,15 @@ var App = class {
|
|
|
2490
4158
|
return 0;
|
|
2491
4159
|
}
|
|
2492
4160
|
this._mounted = true;
|
|
4161
|
+
this._subscribeFocusEvents();
|
|
4162
|
+
this.focus.start();
|
|
4163
|
+
const focusedId = this.focus.currentId;
|
|
4164
|
+
if (focusedId) {
|
|
4165
|
+
this._pendingFocusState.set(focusedId, true);
|
|
4166
|
+
}
|
|
4167
|
+
this.renderer.hook.start();
|
|
2493
4168
|
this.terminal.enterRawMode();
|
|
2494
|
-
if (this._options.
|
|
4169
|
+
if (this._options.screenMode === "alternate") {
|
|
2495
4170
|
this.terminal.enterAltScreen();
|
|
2496
4171
|
}
|
|
2497
4172
|
this.terminal.hideCursor();
|
|
@@ -2515,9 +4190,9 @@ var App = class {
|
|
|
2515
4190
|
...rawEvent,
|
|
2516
4191
|
targetId: this.focus.currentId ?? void 0
|
|
2517
4192
|
});
|
|
2518
|
-
const
|
|
2519
|
-
if (
|
|
2520
|
-
const chain = this._buildBubbleChain(
|
|
4193
|
+
const focusedId2 = this.focus.currentId;
|
|
4194
|
+
if (focusedId2) {
|
|
4195
|
+
const chain = this._buildBubbleChain(focusedId2);
|
|
2521
4196
|
for (const widget of chain) {
|
|
2522
4197
|
widget.events.emit("key", event);
|
|
2523
4198
|
if (event._propagationStopped) break;
|
|
@@ -2539,6 +4214,43 @@ var App = class {
|
|
|
2539
4214
|
this._unsubMouse = this.input.onMouse((event) => {
|
|
2540
4215
|
this.events.emit("mouse", event);
|
|
2541
4216
|
});
|
|
4217
|
+
this._unsubPaste = this.input.onPaste((text) => {
|
|
4218
|
+
this.events.emit("paste", text);
|
|
4219
|
+
});
|
|
4220
|
+
const onSigInt = () => {
|
|
4221
|
+
this.exit(130);
|
|
4222
|
+
};
|
|
4223
|
+
const onSigTerm = () => {
|
|
4224
|
+
this.exit(143);
|
|
4225
|
+
};
|
|
4226
|
+
process.on("SIGINT", onSigInt);
|
|
4227
|
+
process.on("SIGTERM", onSigTerm);
|
|
4228
|
+
this._unsubSigInt = () => process.off("SIGINT", onSigInt);
|
|
4229
|
+
this._unsubSigTerm = () => process.off("SIGTERM", onSigTerm);
|
|
4230
|
+
this.terminal.onCleanup(() => {
|
|
4231
|
+
this.renderer.hook.stop();
|
|
4232
|
+
});
|
|
4233
|
+
const onUncaughtException = (err) => {
|
|
4234
|
+
this.renderer.hook.stop();
|
|
4235
|
+
this.renderer.hook.writeRaw(this.renderer.hook.flush());
|
|
4236
|
+
this.renderer.hook.writeRaw(`Uncaught exception: ${err.message}
|
|
4237
|
+
${err.stack}
|
|
4238
|
+
`);
|
|
4239
|
+
this.terminal.restore();
|
|
4240
|
+
process.exit(1);
|
|
4241
|
+
};
|
|
4242
|
+
process.on("uncaughtException", onUncaughtException);
|
|
4243
|
+
this._unsubUncaughtException = () => process.off("uncaughtException", onUncaughtException);
|
|
4244
|
+
const onUnhandledRejection = (reason) => {
|
|
4245
|
+
this.renderer.hook.stop();
|
|
4246
|
+
this.renderer.hook.writeRaw(this.renderer.hook.flush());
|
|
4247
|
+
this.renderer.hook.writeRaw(`Unhandled rejection: ${reason}
|
|
4248
|
+
`);
|
|
4249
|
+
this.terminal.restore();
|
|
4250
|
+
process.exit(1);
|
|
4251
|
+
};
|
|
4252
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
4253
|
+
this._unsubUnhandledRejection = () => process.off("unhandledRejection", onUnhandledRejection);
|
|
2542
4254
|
this.renderer.start(() => this.requestRender());
|
|
2543
4255
|
this._rootWidget.mount?.();
|
|
2544
4256
|
this.events.emit("mount", void 0);
|
|
@@ -2551,24 +4263,41 @@ var App = class {
|
|
|
2551
4263
|
/**
|
|
2552
4264
|
* Stop the application and restore terminal state.
|
|
2553
4265
|
*/
|
|
2554
|
-
unmount() {
|
|
4266
|
+
unmount(exitCode = 0) {
|
|
2555
4267
|
if (!this._mounted) return;
|
|
2556
4268
|
this._mounted = false;
|
|
2557
4269
|
this._rootWidget.unmount?.();
|
|
2558
4270
|
this.events.emit("unmount", void 0);
|
|
4271
|
+
this._unsubSigInt?.();
|
|
4272
|
+
this._unsubSigInt = null;
|
|
4273
|
+
this._unsubSigTerm?.();
|
|
4274
|
+
this._unsubSigTerm = null;
|
|
2559
4275
|
this._unsubKey?.();
|
|
2560
4276
|
this._unsubKey = null;
|
|
2561
4277
|
this._unsubMouse?.();
|
|
2562
4278
|
this._unsubMouse = null;
|
|
4279
|
+
this._unsubFocus?.();
|
|
4280
|
+
this._unsubFocus = null;
|
|
4281
|
+
this._unsubBlur?.();
|
|
4282
|
+
this._unsubBlur = null;
|
|
4283
|
+
this._unsubPaste?.();
|
|
4284
|
+
this._unsubPaste = null;
|
|
4285
|
+
this._unsubUncaughtException?.();
|
|
4286
|
+
this._unsubUncaughtException = null;
|
|
4287
|
+
this._unsubUnhandledRejection?.();
|
|
4288
|
+
this._unsubUnhandledRejection = null;
|
|
4289
|
+
this.renderer.hook.stop();
|
|
2563
4290
|
this.renderer.stop();
|
|
2564
4291
|
this.input.stop();
|
|
2565
4292
|
this.terminal.restore();
|
|
2566
4293
|
this.events.removeAll();
|
|
4294
|
+
if (this._exitResolve) {
|
|
4295
|
+
this._exitResolve(exitCode);
|
|
4296
|
+
this._exitResolve = null;
|
|
4297
|
+
}
|
|
2567
4298
|
}
|
|
2568
4299
|
/**
|
|
2569
4300
|
* Create an overlay layer for rendering above normal widgets.
|
|
2570
|
-
* @param id Unique layer identifier (e.g. 'modal', 'select-dropdown', 'toast')
|
|
2571
|
-
* @param zIndex Stacking order (higher = rendered on top). Default: 100
|
|
2572
4301
|
*/
|
|
2573
4302
|
addOverlay(id, zIndex = 100) {
|
|
2574
4303
|
this.layers.createLayer(id, zIndex);
|
|
@@ -2581,33 +4310,84 @@ var App = class {
|
|
|
2581
4310
|
}
|
|
2582
4311
|
/**
|
|
2583
4312
|
* Request a re-render on the next frame.
|
|
2584
|
-
*
|
|
4313
|
+
* Batches rapid structural updates via setImmediate scheduling so that
|
|
4314
|
+
* multiple synchronous state mutations collapse into a single render frame.
|
|
2585
4315
|
*/
|
|
2586
4316
|
requestRender() {
|
|
2587
4317
|
if (!this._mounted) return;
|
|
2588
|
-
if (this.
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
4318
|
+
if (this._isRenderPending) return;
|
|
4319
|
+
this._isRenderPending = true;
|
|
4320
|
+
setImmediate(() => {
|
|
4321
|
+
if (!this._mounted) {
|
|
4322
|
+
this._isRenderPending = false;
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
try {
|
|
4326
|
+
if (this._rootWidget.isDirty === false && !this.layers.hasDirtyLayers()) {
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
if (this._rootWidget.isDirty !== false) {
|
|
4330
|
+
const layoutRoot = this._rootWidget.getLayoutNode();
|
|
4331
|
+
computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
|
|
4332
|
+
this._rootWidget.syncLayout?.();
|
|
4333
|
+
this._buildWidgetMap(this._rootWidget);
|
|
4334
|
+
this.screen.clear();
|
|
4335
|
+
this._rootWidget.render(this.screen);
|
|
4336
|
+
if (this._options.dockBorders) {
|
|
4337
|
+
mergeBorders(this.screen);
|
|
4338
|
+
}
|
|
4339
|
+
this._rootWidget.clearDirty?.();
|
|
4340
|
+
}
|
|
4341
|
+
this.layers.composite(this.screen);
|
|
4342
|
+
if (this._options.screenMode === "inline") {
|
|
4343
|
+
for (const item of this._insertBefore) {
|
|
4344
|
+
this.terminal.write(item.text + "\n");
|
|
4345
|
+
}
|
|
4346
|
+
renderInlineToTerminal(this.terminal, this.screen, this._options.inlineRows ?? 0);
|
|
4347
|
+
} else {
|
|
4348
|
+
this.renderer.requestFrame();
|
|
4349
|
+
}
|
|
4350
|
+
} finally {
|
|
4351
|
+
this._isRenderPending = false;
|
|
4352
|
+
if (this._rootWidget.isDirty === true) {
|
|
4353
|
+
this.requestRender();
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
});
|
|
2600
4357
|
}
|
|
2601
4358
|
/**
|
|
2602
4359
|
* Exit the app (convenience method).
|
|
2603
4360
|
*/
|
|
2604
4361
|
exit(code = 0) {
|
|
2605
|
-
this.unmount();
|
|
4362
|
+
this.unmount(code);
|
|
2606
4363
|
if (this._exitResolve) {
|
|
2607
4364
|
this._exitResolve(code);
|
|
2608
4365
|
this._exitResolve = null;
|
|
2609
4366
|
}
|
|
2610
4367
|
}
|
|
4368
|
+
/**
|
|
4369
|
+
* Read from the system clipboard.
|
|
4370
|
+
*/
|
|
4371
|
+
readClipboard() {
|
|
4372
|
+
return this.terminal.readClipboard();
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Write to the system clipboard.
|
|
4376
|
+
*/
|
|
4377
|
+
writeClipboard(text) {
|
|
4378
|
+
this.terminal.writeClipboard(text);
|
|
4379
|
+
}
|
|
4380
|
+
/**
|
|
4381
|
+
* Register a persistent line to be written above inline viewport output.
|
|
4382
|
+
*/
|
|
4383
|
+
insertBefore(line) {
|
|
4384
|
+
const id = /* @__PURE__ */ Symbol();
|
|
4385
|
+
this._insertBefore.push({ id, text: line });
|
|
4386
|
+
return () => {
|
|
4387
|
+
const idx = this._insertBefore.findIndex((x) => x.id === id);
|
|
4388
|
+
if (idx >= 0) this._insertBefore.splice(idx, 1);
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
2611
4391
|
/**
|
|
2612
4392
|
* Render in fallback (static) mode for non-interactive environments.
|
|
2613
4393
|
*/
|
|
@@ -2622,8 +4402,6 @@ var App = class {
|
|
|
2622
4402
|
}
|
|
2623
4403
|
/**
|
|
2624
4404
|
* Build the bubble chain for keyboard events.
|
|
2625
|
-
* Returns an array: [focused widget, parent, grandparent, ..., root]
|
|
2626
|
-
* Uses the cached _widgetById map for O(1) lookup instead of DFS.
|
|
2627
4405
|
*/
|
|
2628
4406
|
_buildBubbleChain(widgetId) {
|
|
2629
4407
|
const chain = [];
|
|
@@ -2640,15 +4418,17 @@ var App = class {
|
|
|
2640
4418
|
}
|
|
2641
4419
|
/**
|
|
2642
4420
|
* Rebuild the widget ID cache by walking the entire widget tree.
|
|
2643
|
-
* Called after syncLayout() so the map stays current.
|
|
2644
4421
|
*/
|
|
2645
4422
|
_buildWidgetMap(root) {
|
|
2646
4423
|
this._widgetById.clear();
|
|
2647
4424
|
this._walkWidget(root);
|
|
4425
|
+
this._applyPendingFocusState();
|
|
2648
4426
|
}
|
|
2649
4427
|
_walkWidget(widget) {
|
|
2650
4428
|
if (!widget) return;
|
|
2651
|
-
if (widget.id)
|
|
4429
|
+
if (widget.id) {
|
|
4430
|
+
this._widgetById.set(widget.id, widget);
|
|
4431
|
+
}
|
|
2652
4432
|
const children = widget._children ?? widget.children ?? [];
|
|
2653
4433
|
if (Array.isArray(children)) {
|
|
2654
4434
|
for (const child of children) {
|
|
@@ -2656,167 +4436,116 @@ var App = class {
|
|
|
2656
4436
|
}
|
|
2657
4437
|
}
|
|
2658
4438
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
codePoint >= 19968 && codePoint <= 40959 || // CJK Unified Ideographs Extension A
|
|
2666
|
-
codePoint >= 13312 && codePoint <= 19903 || // CJK Compatibility Ideographs
|
|
2667
|
-
codePoint >= 63744 && codePoint <= 64255 || // Hangul Syllables
|
|
2668
|
-
codePoint >= 44032 && codePoint <= 55215 || // Katakana
|
|
2669
|
-
codePoint >= 12448 && codePoint <= 12543 || // CJK Symbols and Punctuation
|
|
2670
|
-
codePoint >= 12288 && codePoint <= 12351 || // Hiragana
|
|
2671
|
-
codePoint >= 12352 && codePoint <= 12447 || // Fullwidth Forms
|
|
2672
|
-
codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || // CJK Unified Ideographs Extension B
|
|
2673
|
-
codePoint >= 131072 && codePoint <= 173791 || // CJK Unified Ideographs Extension C,D,E,F
|
|
2674
|
-
codePoint >= 173824 && codePoint <= 191471 || // CJK Compatibility Ideographs Supplement
|
|
2675
|
-
codePoint >= 194560 && codePoint <= 195103
|
|
2676
|
-
);
|
|
2677
|
-
}
|
|
2678
|
-
function isCombining(codePoint) {
|
|
2679
|
-
return (
|
|
2680
|
-
// Combining Diacritical Marks
|
|
2681
|
-
codePoint >= 768 && codePoint <= 879 || // Combining Diacritical Marks Extended
|
|
2682
|
-
codePoint >= 6832 && codePoint <= 6911 || // Combining Diacritical Marks Supplement
|
|
2683
|
-
codePoint >= 7616 && codePoint <= 7679 || // Combining Diacritical Marks for Symbols
|
|
2684
|
-
codePoint >= 8400 && codePoint <= 8447 || // Combining Half Marks
|
|
2685
|
-
codePoint >= 65056 && codePoint <= 65071 || // Variation selectors
|
|
2686
|
-
codePoint >= 65024 && codePoint <= 65039 || // Zero-width joiner / non-joiner
|
|
2687
|
-
codePoint === 8203 || codePoint === 8204 || codePoint === 8205 || codePoint === 65279
|
|
2688
|
-
);
|
|
2689
|
-
}
|
|
2690
|
-
function isEmoji(codePoint) {
|
|
2691
|
-
return (
|
|
2692
|
-
// Emoticons
|
|
2693
|
-
codePoint >= 128512 && codePoint <= 128591 || // Misc Symbols and Pictographs
|
|
2694
|
-
codePoint >= 127744 && codePoint <= 128511 || // Transport and Map
|
|
2695
|
-
codePoint >= 128640 && codePoint <= 128767 || // Supplemental Symbols
|
|
2696
|
-
codePoint >= 129280 && codePoint <= 129535 || // Misc symbols
|
|
2697
|
-
codePoint >= 9728 && codePoint <= 9983 || // Dingbats
|
|
2698
|
-
codePoint >= 9984 && codePoint <= 10175 || // Flags
|
|
2699
|
-
codePoint >= 127456 && codePoint <= 127487
|
|
2700
|
-
);
|
|
2701
|
-
}
|
|
2702
|
-
function stringWidth(str) {
|
|
2703
|
-
let width = 0;
|
|
2704
|
-
let inEscape = false;
|
|
2705
|
-
for (const char of str) {
|
|
2706
|
-
const cp = char.codePointAt(0);
|
|
2707
|
-
if (cp === 27) {
|
|
2708
|
-
inEscape = true;
|
|
2709
|
-
continue;
|
|
2710
|
-
}
|
|
2711
|
-
if (inEscape) {
|
|
2712
|
-
if (cp >= 64 && cp <= 126 && cp !== 91) {
|
|
2713
|
-
inEscape = false;
|
|
2714
|
-
}
|
|
2715
|
-
continue;
|
|
4439
|
+
_handleFocusEvent(event) {
|
|
4440
|
+
const focused = event.type === "focus";
|
|
4441
|
+
const changed = this._setWidgetFocused(event.targetId, focused);
|
|
4442
|
+
if (changed === null) {
|
|
4443
|
+
this._pendingFocusState.set(event.targetId, focused);
|
|
4444
|
+
return;
|
|
2716
4445
|
}
|
|
2717
|
-
if (
|
|
2718
|
-
|
|
4446
|
+
if (changed) {
|
|
4447
|
+
this.requestRender();
|
|
2719
4448
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
4449
|
+
}
|
|
4450
|
+
_setWidgetFocused(id, focused) {
|
|
4451
|
+
const widget = this._widgetById.get(id);
|
|
4452
|
+
if (!widget) {
|
|
4453
|
+
return null;
|
|
2722
4454
|
}
|
|
2723
|
-
if (
|
|
2724
|
-
|
|
2725
|
-
continue;
|
|
4455
|
+
if (!this._isFocusAwareWidget(widget) || widget.isFocused === focused) {
|
|
4456
|
+
return false;
|
|
2726
4457
|
}
|
|
2727
|
-
|
|
4458
|
+
widget.isFocused = focused;
|
|
4459
|
+
widget.markDirty?.();
|
|
4460
|
+
return true;
|
|
2728
4461
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
if (maxWidth <= 0) return "";
|
|
2733
|
-
const strW = stringWidth(str);
|
|
2734
|
-
if (strW <= maxWidth) return str;
|
|
2735
|
-
const ellipsisW = stringWidth(ellipsis);
|
|
2736
|
-
const targetW = maxWidth - ellipsisW;
|
|
2737
|
-
if (targetW <= 0) return ellipsis.slice(0, maxWidth);
|
|
2738
|
-
let width = 0;
|
|
2739
|
-
let result = "";
|
|
2740
|
-
let inEscape = false;
|
|
2741
|
-
let escapeBuffer = "";
|
|
2742
|
-
for (const char of str) {
|
|
2743
|
-
const cp = char.codePointAt(0);
|
|
2744
|
-
if (cp === 27) {
|
|
2745
|
-
inEscape = true;
|
|
2746
|
-
escapeBuffer += char;
|
|
2747
|
-
continue;
|
|
4462
|
+
_subscribeFocusEvents() {
|
|
4463
|
+
if (!this._unsubFocus) {
|
|
4464
|
+
this._unsubFocus = this.focus.on("focus", (event) => this._handleFocusEvent(event));
|
|
2748
4465
|
}
|
|
2749
|
-
if (
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
4466
|
+
if (!this._unsubBlur) {
|
|
4467
|
+
this._unsubBlur = this.focus.on("blur", (event) => this._handleFocusEvent(event));
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
_applyPendingFocusState() {
|
|
4471
|
+
for (const [id, focused] of this._pendingFocusState) {
|
|
4472
|
+
const stateChanged = this._setWidgetFocused(id, focused);
|
|
4473
|
+
if (stateChanged !== null) {
|
|
4474
|
+
this._pendingFocusState.delete(id);
|
|
2755
4475
|
}
|
|
2756
|
-
continue;
|
|
2757
4476
|
}
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
4477
|
+
}
|
|
4478
|
+
_isFocusAwareWidget(widget) {
|
|
4479
|
+
return typeof widget === "object" && widget !== null && "id" in widget && typeof widget.id === "string" && "isFocused" in widget && typeof widget.isFocused === "boolean";
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
|
|
4483
|
+
// src/utils/debounce.ts
|
|
4484
|
+
function debounce(func, wait, options = {}) {
|
|
4485
|
+
const { leading = false, trailing = true } = options;
|
|
4486
|
+
let timeoutId = null;
|
|
4487
|
+
let lastArgs = null;
|
|
4488
|
+
let lastCallTime = null;
|
|
4489
|
+
let lastInvokeTime = 0;
|
|
4490
|
+
function invokeFunc(time) {
|
|
4491
|
+
const args = lastArgs;
|
|
4492
|
+
lastArgs = null;
|
|
4493
|
+
lastInvokeTime = time;
|
|
4494
|
+
return func(...args);
|
|
4495
|
+
}
|
|
4496
|
+
function shouldInvoke(time) {
|
|
4497
|
+
const timeSinceLastCall = time - (lastCallTime ?? 0);
|
|
4498
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
4499
|
+
return lastCallTime === null || timeSinceLastCall >= wait || timeSinceLastCall < 0 || timeSinceLastInvoke >= wait;
|
|
4500
|
+
}
|
|
4501
|
+
function trailingEdge() {
|
|
4502
|
+
timeoutId = null;
|
|
4503
|
+
if (trailing && lastArgs) {
|
|
4504
|
+
return invokeFunc(Date.now());
|
|
2765
4505
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
result += char;
|
|
4506
|
+
lastArgs = null;
|
|
4507
|
+
return void 0;
|
|
2769
4508
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const result = [];
|
|
2779
|
-
for (const line of lines) {
|
|
2780
|
-
if (stringWidth(line) <= width) {
|
|
2781
|
-
result.push(line);
|
|
2782
|
-
continue;
|
|
4509
|
+
function timerExpired() {
|
|
4510
|
+
const time = Date.now();
|
|
4511
|
+
if (shouldInvoke(time)) {
|
|
4512
|
+
trailingEdge();
|
|
4513
|
+
} else {
|
|
4514
|
+
const timeSinceLastCall = time - (lastCallTime ?? 0);
|
|
4515
|
+
const timeWaiting = wait - timeSinceLastCall;
|
|
4516
|
+
timeoutId = setTimeout(timerExpired, timeWaiting);
|
|
2783
4517
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
const
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
result.push(currentLine);
|
|
2795
|
-
currentLine = "";
|
|
2796
|
-
currentWidth = 0;
|
|
2797
|
-
}
|
|
2798
|
-
for (const char of word) {
|
|
2799
|
-
const cp = char.codePointAt(0);
|
|
2800
|
-
const charW = isWideChar(cp) || isEmoji(cp) ? 2 : isCombining(cp) ? 0 : 1;
|
|
2801
|
-
if (currentWidth + charW > width) {
|
|
2802
|
-
result.push(currentLine);
|
|
2803
|
-
currentLine = "";
|
|
2804
|
-
currentWidth = 0;
|
|
2805
|
-
}
|
|
2806
|
-
currentLine += char;
|
|
2807
|
-
currentWidth += charW;
|
|
4518
|
+
}
|
|
4519
|
+
const debounced = function(...args) {
|
|
4520
|
+
const time = Date.now();
|
|
4521
|
+
const isInvoking = shouldInvoke(time);
|
|
4522
|
+
lastArgs = args;
|
|
4523
|
+
lastCallTime = time;
|
|
4524
|
+
if (isInvoking) {
|
|
4525
|
+
if (timeoutId === null) {
|
|
4526
|
+
if (leading) {
|
|
4527
|
+
return invokeFunc(time);
|
|
2808
4528
|
}
|
|
4529
|
+
timeoutId = setTimeout(timerExpired, wait);
|
|
2809
4530
|
} else {
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
currentWidth = stringWidth(currentLine);
|
|
4531
|
+
clearTimeout(timeoutId);
|
|
4532
|
+
timeoutId = setTimeout(timerExpired, wait);
|
|
2813
4533
|
}
|
|
4534
|
+
} else if (timeoutId === null && trailing) {
|
|
4535
|
+
timeoutId = setTimeout(timerExpired, wait);
|
|
2814
4536
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
4537
|
+
return void 0;
|
|
4538
|
+
};
|
|
4539
|
+
debounced.cancel = () => {
|
|
4540
|
+
if (timeoutId !== null) {
|
|
4541
|
+
clearTimeout(timeoutId);
|
|
2817
4542
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
4543
|
+
lastInvokeTime = 0;
|
|
4544
|
+
lastArgs = null;
|
|
4545
|
+
lastCallTime = null;
|
|
4546
|
+
timeoutId = null;
|
|
4547
|
+
};
|
|
4548
|
+
return debounced;
|
|
2820
4549
|
}
|
|
2821
4550
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2822
4551
|
0 && (module.exports = {
|
|
@@ -2830,14 +4559,27 @@ function wordWrap(str, width) {
|
|
|
2830
4559
|
BarSets,
|
|
2831
4560
|
BorderSets,
|
|
2832
4561
|
CTRL_KEYS,
|
|
4562
|
+
ChordMatcher,
|
|
2833
4563
|
ColorDepth,
|
|
4564
|
+
Constraint,
|
|
4565
|
+
Dim,
|
|
2834
4566
|
ESCAPE_SEQUENCES,
|
|
2835
4567
|
EventEmitter,
|
|
4568
|
+
FillConstraint,
|
|
4569
|
+
Flex,
|
|
2836
4570
|
FocusManager,
|
|
2837
4571
|
HORIZONTAL_BAR_SYMBOLS,
|
|
2838
4572
|
InputParser,
|
|
2839
4573
|
LayerManager,
|
|
4574
|
+
LengthConstraint,
|
|
2840
4575
|
LineSets,
|
|
4576
|
+
LiveRender,
|
|
4577
|
+
MaxConstraint,
|
|
4578
|
+
MinConstraint,
|
|
4579
|
+
MouseGestures,
|
|
4580
|
+
PercentageConstraint,
|
|
4581
|
+
Pos,
|
|
4582
|
+
RenderHook,
|
|
2841
4583
|
Renderer,
|
|
2842
4584
|
SPECIAL_KEYS,
|
|
2843
4585
|
Screen,
|
|
@@ -2846,40 +4588,48 @@ function wordWrap(str, width) {
|
|
|
2846
4588
|
Terminal,
|
|
2847
4589
|
VERTICAL_BAR_SYMBOLS,
|
|
2848
4590
|
ansi,
|
|
4591
|
+
bell,
|
|
2849
4592
|
borderSize,
|
|
2850
4593
|
caps,
|
|
2851
4594
|
cellsEqual,
|
|
4595
|
+
clipboard,
|
|
2852
4596
|
colorToAnsiBg,
|
|
2853
4597
|
colorToAnsiFg,
|
|
2854
4598
|
colorToRgb,
|
|
2855
4599
|
computeLayout,
|
|
2856
4600
|
containsPoint,
|
|
2857
4601
|
contrastRatio,
|
|
4602
|
+
createInlineViewport,
|
|
2858
4603
|
createKeyEvent,
|
|
2859
4604
|
createLayoutNode,
|
|
2860
4605
|
createTestScreen,
|
|
4606
|
+
debounce,
|
|
2861
4607
|
defaultStyle,
|
|
2862
4608
|
detectColorDepth,
|
|
2863
4609
|
emptyCell,
|
|
2864
4610
|
emptyRect,
|
|
2865
|
-
fill,
|
|
2866
4611
|
getBorderChars,
|
|
4612
|
+
hasLayoutChanges,
|
|
2867
4613
|
intersectRect,
|
|
4614
|
+
invalidateLayout,
|
|
2868
4615
|
isMouseSequence,
|
|
2869
|
-
|
|
2870
|
-
max,
|
|
4616
|
+
mergeBorders,
|
|
2871
4617
|
mergeStyles,
|
|
2872
|
-
min,
|
|
2873
4618
|
normalizeEdges,
|
|
4619
|
+
normalizeNavigationKey,
|
|
2874
4620
|
parseColor,
|
|
2875
4621
|
parseMouseEvent,
|
|
2876
|
-
|
|
2877
|
-
|
|
4622
|
+
prefersHighContrast,
|
|
4623
|
+
prefersReducedMotion,
|
|
4624
|
+
readClipboard,
|
|
2878
4625
|
relativeLuminance,
|
|
2879
4626
|
renderFallback,
|
|
4627
|
+
renderInlineToTerminal,
|
|
4628
|
+
resolveConstraints,
|
|
4629
|
+
resolveLayoutVariables,
|
|
4630
|
+
shouldUseColor,
|
|
2880
4631
|
shouldUseFallback,
|
|
2881
4632
|
shrinkRect,
|
|
2882
|
-
splitRect,
|
|
2883
4633
|
stringWidth,
|
|
2884
4634
|
stripAnsi,
|
|
2885
4635
|
styleToCellAttrs,
|