@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 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
- length: () => length,
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
- percentage: () => percentage,
77
- ratio: () => ratio,
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(ratio2, large = false) {
331
+ function wcagLevel(ratio, large = false) {
311
332
  if (large) {
312
- if (ratio2 >= 4.5) return "AAA";
313
- if (ratio2 >= 3) return "AA";
333
+ if (ratio >= 4.5) return "AAA";
334
+ if (ratio >= 3) return "AA";
314
335
  return "fail";
315
336
  }
316
- if (ratio2 >= 7) return "AAA";
317
- if (ratio2 >= 4.5) return "AA";
318
- if (ratio2 >= 3) return "A";
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 ratio2 = contrastRatio(fgColor, bgColor);
338
- const level = wcagLevel(ratio2);
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(ratio2 * 100) / 100, level, required: "AA" });
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
- for (const handler of this._resizeHandlers) {
489
- handler(this._cols, this._rows);
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._restored = true;
575
- if (this._exitHandler) process.off("exit", this._exitHandler);
576
- if (this._sigintHandler) process.off("SIGINT", this._sigintHandler);
577
- if (this._sigtermHandler) process.off("SIGTERM", this._sigtermHandler);
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.disableMouse();
590
- this.exitAltScreen();
591
- this.exitRawMode();
592
- this.showCursor();
593
- this.write(reset);
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
- for (const handler of this._cleanupHandlers) {
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
- for (const char of str) {
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
- const cp = char.codePointAt(0);
770
- const isWide = this._isWideCodePoint(cp);
771
- const width = isWide ? 2 : 1;
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
- if (isWide && x + 1 < this._cols) {
778
- this.setCell(x + 1, row, {
779
- char: "",
780
- width: 0,
781
- ...style
782
- });
783
- x += 2;
784
- } else {
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
- const temp = this.front;
805
- this.front = this.back;
806
- this.back = temp;
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] = { ...emptyCell(), char: "\0" };
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
- _isWideCodePoint(cp) {
839
- return cp >= 19968 && cp <= 40959 || cp >= 13312 && cp <= 19903 || cp >= 63744 && cp <= 64255 || cp >= 44032 && cp <= 55215 || cp >= 12448 && cp <= 12543 || cp >= 12288 && cp <= 12351 || cp >= 12352 && cp <= 12447 || cp >= 65281 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 128512 && cp <= 128591 || cp >= 127744 && cp <= 128511 || cp >= 128640 && cp <= 128767 || cp >= 129280 && cp <= 129535 || cp >= 131072 && cp <= 173791;
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
- constructor(terminal, screen, fps = 30) {
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 { front, back, cols, rows } = this._screen;
907
- let output = beginSyncUpdate;
908
- let lastRow = -1;
909
- let lastCol = -1;
910
- for (let r = 0; r < rows; r++) {
911
- for (let c = 0; c < cols; c++) {
912
- const frontCell = front[r][c];
913
- const backCell = back[r][c];
914
- if (cellsEqual(frontCell, backCell)) continue;
915
- if (backCell.width === 0) continue;
916
- if (r !== lastRow || c !== lastCol) {
917
- output += moveTo(c, r);
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
- output += this._renderCell(backCell);
920
- lastRow = r;
921
- lastCol = c + (backCell.width === 2 ? 2 : 1);
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
- output += reset;
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
- seq += reset;
935
- if (cell.bold) seq += "\x1B[1m";
936
- if (cell.dim) seq += "\x1B[2m";
937
- if (cell.italic) seq += "\x1B[3m";
938
- if (cell.underline) seq += "\x1B[4m";
939
- if (cell.strikethrough) seq += "\x1B[9m";
940
- if (cell.inverse) seq += "\x1B[7m";
941
- seq += colorToAnsiFg(cell.fg, this._colorDepth);
942
- seq += colorToAnsiBg(cell.bg, this._colorDepth);
943
- seq += cell.char || " ";
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: 1, ...style });
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 = null;
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
- for (let r = dy; r < dy + dh && r < this._rows; r++) {
1074
- for (let c = dx; c < dx + dw && c < this._cols; c++) {
1075
- const cell = layer.cells[r][c];
1076
- if (isCellTransparent(cell)) continue;
1077
- screen.setCell(c, r, {
1078
- char: cell.char,
1079
- fg: cell.fg,
1080
- bg: cell.bg,
1081
- bold: cell.bold,
1082
- italic: cell.italic,
1083
- underline: cell.underline,
1084
- dim: cell.dim,
1085
- strikethrough: cell.strikethrough,
1086
- inverse: cell.inverse,
1087
- width: cell.width
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
- scrollDelta = buttonBits === 0 ? -1 : 1;
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
- if (this._escapeBuffer) {
1451
- this._escapeBuffer += str;
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(data);
2396
+ this._tryParseEscape();
1457
2397
  return;
1458
2398
  }
1459
2399
  if (str.startsWith("\x1B") && str.length === 1) {
1460
- this._escapeBuffer = str;
2400
+ this._escapeBuffer = data;
1461
2401
  this._escapeTimeout = setTimeout(() => {
1462
2402
  this._events.emit("key", createKeyEvent({
1463
2403
  key: "escape",
1464
- raw: Buffer.from(this._escapeBuffer),
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 = str;
1476
- this._tryParseEscape(data);
2415
+ this._escapeBuffer = data;
2416
+ this._tryParseEscape();
1477
2417
  return;
1478
2418
  }
1479
- for (let i = 0; i < str.length; i++) {
1480
- const ch = str[i];
1481
- const code = str.charCodeAt(i);
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(rawData) {
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: rawData,
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: rawData,
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
- function getBorderChars(style, customChars) {
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 / 2;
1734
- const innerY = padding.top + border.vertical / 2;
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 node.children) {
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") return value;
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, min2, max2) {
3480
+ function clampSize(value, min, max) {
1863
3481
  let result = value;
1864
- if (min2 !== void 0) result = Math.max(result, min2);
1865
- if (max2 !== void 0) result = Math.min(result, max2);
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
- this._events.emit("focus", { targetId: focusable.id, type: "focus" });
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.fullscreen) {
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 focusedId = this.focus.currentId;
2519
- if (focusedId) {
2520
- const chain = this._buildBubbleChain(focusedId);
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
- * Skips layout + render pass when the root widget reports no dirty state.
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._rootWidget.isDirty === false) {
2589
- return;
2590
- }
2591
- const layoutRoot = this._rootWidget.getLayoutNode();
2592
- computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
2593
- this._rootWidget.syncLayout?.();
2594
- this._buildWidgetMap(this._rootWidget);
2595
- this.screen.clear();
2596
- this._rootWidget.render(this.screen);
2597
- this._rootWidget.clearDirty?.();
2598
- this.layers.composite(this.screen);
2599
- this.renderer.requestFrame();
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) this._widgetById.set(widget.id, widget);
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
- // src/utils/unicode.ts
2662
- function isWideChar(codePoint) {
2663
- return (
2664
- // CJK Unified Ideographs (common Chinese/Japanese/Korean)
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 (cp < 32 || cp >= 127 && cp < 160) {
2718
- continue;
4446
+ if (changed) {
4447
+ this.requestRender();
2719
4448
  }
2720
- if (isCombining(cp)) {
2721
- continue;
4449
+ }
4450
+ _setWidgetFocused(id, focused) {
4451
+ const widget = this._widgetById.get(id);
4452
+ if (!widget) {
4453
+ return null;
2722
4454
  }
2723
- if (isWideChar(cp) || isEmoji(cp)) {
2724
- width += 2;
2725
- continue;
4455
+ if (!this._isFocusAwareWidget(widget) || widget.isFocused === focused) {
4456
+ return false;
2726
4457
  }
2727
- width += 1;
4458
+ widget.isFocused = focused;
4459
+ widget.markDirty?.();
4460
+ return true;
2728
4461
  }
2729
- return width;
2730
- }
2731
- function truncate(str, maxWidth, ellipsis = "\u2026") {
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 (inEscape) {
2750
- escapeBuffer += char;
2751
- if (cp >= 64 && cp <= 126 && cp !== 91) {
2752
- inEscape = false;
2753
- result += escapeBuffer;
2754
- escapeBuffer = "";
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
- let charW = 1;
2759
- if (isCombining(cp)) {
2760
- charW = 0;
2761
- } else if (isWideChar(cp) || isEmoji(cp)) {
2762
- charW = 2;
2763
- } else if (cp < 32 || cp >= 127 && cp < 160) {
2764
- charW = 0;
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
- if (width + charW > targetW) break;
2767
- width += charW;
2768
- result += char;
4506
+ lastArgs = null;
4507
+ return void 0;
2769
4508
  }
2770
- return result + ellipsis;
2771
- }
2772
- function stripAnsi(str) {
2773
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
2774
- }
2775
- function wordWrap(str, width) {
2776
- if (width <= 0) return str;
2777
- const lines = str.split("\n");
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
- let currentLine = "";
2785
- let currentWidth = 0;
2786
- const words = line.split(/(\s+)/);
2787
- for (const word of words) {
2788
- const wordW = stringWidth(word);
2789
- if (currentWidth + wordW <= width) {
2790
- currentLine += word;
2791
- currentWidth += wordW;
2792
- } else if (wordW > width) {
2793
- if (currentLine) {
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
- result.push(currentLine);
2811
- currentLine = word.trimStart();
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
- if (currentLine) {
2816
- result.push(currentLine);
4537
+ return void 0;
4538
+ };
4539
+ debounced.cancel = () => {
4540
+ if (timeoutId !== null) {
4541
+ clearTimeout(timeoutId);
2817
4542
  }
2818
- }
2819
- return result.join("\n");
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
- length,
2870
- max,
4616
+ mergeBorders,
2871
4617
  mergeStyles,
2872
- min,
2873
4618
  normalizeEdges,
4619
+ normalizeNavigationKey,
2874
4620
  parseColor,
2875
4621
  parseMouseEvent,
2876
- percentage,
2877
- ratio,
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,