@termuijs/core 0.1.4 → 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.js CHANGED
@@ -214,15 +214,15 @@ function contrastRatio(fg, bg) {
214
214
  const darker = Math.min(l1, l2);
215
215
  return (lighter + 0.05) / (darker + 0.05);
216
216
  }
217
- function wcagLevel(ratio2, large = false) {
217
+ function wcagLevel(ratio, large = false) {
218
218
  if (large) {
219
- if (ratio2 >= 4.5) return "AAA";
220
- if (ratio2 >= 3) return "AA";
219
+ if (ratio >= 4.5) return "AAA";
220
+ if (ratio >= 3) return "AA";
221
221
  return "fail";
222
222
  }
223
- if (ratio2 >= 7) return "AAA";
224
- if (ratio2 >= 4.5) return "AA";
225
- if (ratio2 >= 3) return "A";
223
+ if (ratio >= 7) return "AAA";
224
+ if (ratio >= 4.5) return "AA";
225
+ if (ratio >= 3) return "A";
226
226
  return "fail";
227
227
  }
228
228
  function validateThemeContrast(theme) {
@@ -241,10 +241,10 @@ function validateThemeContrast(theme) {
241
241
  for (const [label, hex] of pairs) {
242
242
  if (!hex) continue;
243
243
  const fgColor = parseColor(hex);
244
- const ratio2 = contrastRatio(fgColor, bgColor);
245
- const level = wcagLevel(ratio2);
244
+ const ratio = contrastRatio(fgColor, bgColor);
245
+ const level = wcagLevel(ratio);
246
246
  if (level !== "AAA" && level !== "AA") {
247
- failures.push({ pair: label, ratio: Math.round(ratio2 * 100) / 100, level, required: "AA" });
247
+ failures.push({ pair: label, ratio: Math.round(ratio * 100) / 100, level, required: "AA" });
248
248
  }
249
249
  }
250
250
  return failures;
@@ -257,6 +257,7 @@ __export(ansi_exports, {
257
257
  ESC: () => ESC,
258
258
  OSC: () => OSC,
259
259
  beginSyncUpdate: () => beginSyncUpdate,
260
+ bell: () => bell,
260
261
  blink: () => blink,
261
262
  bold: () => bold,
262
263
  clearDown: () => clearDown,
@@ -265,15 +266,21 @@ __export(ansi_exports, {
265
266
  clearLineToStart: () => clearLineToStart,
266
267
  clearScreen: () => clearScreen,
267
268
  clearUp: () => clearUp,
269
+ clipboard: () => clipboard,
270
+ cursorShape: () => cursorShape,
268
271
  dim: () => dim,
269
272
  disableBracketedPaste: () => disableBracketedPaste,
273
+ disableFocusTracking: () => disableFocusTracking,
270
274
  disableMouse: () => disableMouse,
271
275
  enableBracketedPaste: () => enableBracketedPaste,
276
+ enableFocusTracking: () => enableFocusTracking,
272
277
  enableMouse: () => enableMouse,
273
278
  endSyncUpdate: () => endSyncUpdate,
274
279
  enterAltScreen: () => enterAltScreen,
275
280
  exitAltScreen: () => exitAltScreen,
276
281
  hideCursor: () => hideCursor,
282
+ hyperlinkClose: () => hyperlinkClose,
283
+ hyperlinkOpen: () => hyperlinkOpen,
277
284
  inverse: () => inverse,
278
285
  italic: () => italic,
279
286
  moveDown: () => moveDown,
@@ -281,6 +288,9 @@ __export(ansi_exports, {
281
288
  moveRight: () => moveRight,
282
289
  moveTo: () => moveTo,
283
290
  moveUp: () => moveUp,
291
+ notify: () => notify,
292
+ readClipboard: () => readClipboard,
293
+ requestCursorPosition: () => requestCursorPosition,
284
294
  reset: () => reset,
285
295
  resetBlink: () => resetBlink,
286
296
  resetBold: () => resetBold,
@@ -296,6 +306,7 @@ __export(ansi_exports, {
296
306
  setTitle: () => setTitle,
297
307
  showCursor: () => showCursor,
298
308
  strikethrough: () => strikethrough,
309
+ stripAnsiControl: () => stripAnsiControl,
299
310
  underline: () => underline,
300
311
  writeClipboard: () => writeClipboard
301
312
  });
@@ -306,6 +317,15 @@ var hideCursor = `${CSI}?25l`;
306
317
  var showCursor = `${CSI}?25h`;
307
318
  var saveCursorPosition = `${CSI}s`;
308
319
  var restoreCursorPosition = `${CSI}u`;
320
+ function cursorShape(shape, blink2 = true) {
321
+ const codes = {
322
+ block: 1,
323
+ underline: 3,
324
+ bar: 5
325
+ };
326
+ const code = codes[shape] + (blink2 ? 0 : 1);
327
+ return `${CSI}${code} q`;
328
+ }
309
329
  function moveTo(col, row) {
310
330
  return `${CSI}${row + 1};${col + 1}H`;
311
331
  }
@@ -321,6 +341,7 @@ function moveRight(n = 1) {
321
341
  function moveLeft(n = 1) {
322
342
  return `${CSI}${n}D`;
323
343
  }
344
+ var requestCursorPosition = `${CSI}6n`;
324
345
  var clearScreen = `${CSI}2J`;
325
346
  var clearLine = `${CSI}2K`;
326
347
  var clearLineToEnd = `${CSI}0K`;
@@ -335,6 +356,8 @@ var enableMouse = `${CSI}?1000h${CSI}?1002h${CSI}?1006h`;
335
356
  var disableMouse = `${CSI}?1000l${CSI}?1002l${CSI}?1006l`;
336
357
  var enableBracketedPaste = `${CSI}?2004h`;
337
358
  var disableBracketedPaste = `${CSI}?2004l`;
359
+ var enableFocusTracking = `${CSI}?1004h`;
360
+ var disableFocusTracking = `${CSI}?1004l`;
338
361
  var reset = `${CSI}0m`;
339
362
  var bold = `${CSI}1m`;
340
363
  var dim = `${CSI}2m`;
@@ -357,10 +380,52 @@ var resetScrollRegion = `${CSI}r`;
357
380
  function setTitle(title) {
358
381
  return `${OSC}0;${title}\x07`;
359
382
  }
383
+ function hyperlinkOpen(url) {
384
+ if (!/^(https?|file):\/\//i.test(url)) return "";
385
+ const safeUrl = url.replace(/[\u0000-\u001F\u007F-\u009F\u001B]/g, "");
386
+ return `\x1B]8;;${safeUrl}\x1B\\`;
387
+ }
388
+ var hyperlinkClose = "\x1B]8;;\x1B\\";
389
+ var bell = "\x07";
390
+ function notify(text) {
391
+ const safeText = text.replace(/[\u0000-\u001F\u007F-\u009F\u001B]/g, "");
392
+ return `${OSC}9;${safeText}${bell}`;
393
+ }
394
+ function stripAnsiControl(str) {
395
+ let out = str.replace(
396
+ /\x1b(?:[@-Z\\-_]|\[[0-9;]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[PX^_][^\x1b]*\x1b\\|.)/g,
397
+ ""
398
+ );
399
+ out = out.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
400
+ return out;
401
+ }
360
402
  function writeClipboard(text, stdout = process.stdout) {
361
403
  const encoded = Buffer.from(text, "utf8").toString("base64");
362
404
  stdout.write(`${OSC}52;c;${encoded}\x07`);
363
405
  }
406
+ function readClipboard(stdin = process.stdin, stdout = process.stdout) {
407
+ return new Promise((resolve, reject) => {
408
+ const handler = (data) => {
409
+ const str = data.toString("utf8");
410
+ const match = str.match(/\x1b\]52;c;([^\x07]+)\x07/);
411
+ if (!match) return;
412
+ stdin.off("data", handler);
413
+ try {
414
+ resolve(
415
+ Buffer.from(match[1], "base64").toString("utf8")
416
+ );
417
+ } catch (err) {
418
+ reject(err);
419
+ }
420
+ };
421
+ stdin.on("data", handler);
422
+ stdout.write(`${OSC}52;c;?\x07`);
423
+ });
424
+ }
425
+ var clipboard = {
426
+ write: writeClipboard,
427
+ read: readClipboard
428
+ };
364
429
 
365
430
  // src/terminal/Terminal.ts
366
431
  var Terminal = class {
@@ -372,32 +437,55 @@ var Terminal = class {
372
437
  _isRawMode = false;
373
438
  _isAltScreen = false;
374
439
  _isMouseEnabled = false;
440
+ _isBracketedPasteEnabled = false;
375
441
  _resizeHandlers = [];
376
442
  _cleanupHandlers = [];
377
443
  _originalRawMode;
444
+ // Debounce state properties
445
+ _resizeDebounceMs;
446
+ _resizeTimer = null;
447
+ _lastDispatchedCols;
448
+ _lastDispatchedRows;
378
449
  // Stored handler references for proper cleanup
379
450
  _resizeHandler = null;
380
451
  _exitHandler = null;
381
- _sigintHandler = null;
382
- _sigtermHandler = null;
383
- _uncaughtExceptionHandler = null;
384
- _unhandledRejectionHandler = null;
385
452
  _restored = false;
453
+ _restoring = false;
454
+ // Stream write queue state to prevent interleaving backpressure fragmentation
455
+ _writeQueue = [];
456
+ _isWriting = false;
386
457
  constructor(options = {}) {
387
458
  this.stdout = options.stdout ?? process.stdout;
388
459
  this.stdin = options.stdin ?? process.stdin;
389
460
  this.colorDepth = options.colorDepth ?? detectColorDepth();
390
461
  this._cols = this.stdout.columns ?? 80;
391
462
  this._rows = this.stdout.rows ?? 24;
463
+ this._resizeDebounceMs = options.resizeDebounceMs ?? 16;
464
+ this._lastDispatchedCols = this._cols;
465
+ this._lastDispatchedRows = this._rows;
392
466
  this._resizeHandler = () => {
393
467
  this._cols = this.stdout.columns ?? 80;
394
468
  this._rows = this.stdout.rows ?? 24;
395
- for (const handler of this._resizeHandlers) {
396
- handler(this._cols, this._rows);
469
+ if (this._resizeTimer) {
470
+ clearTimeout(this._resizeTimer);
397
471
  }
472
+ this._resizeTimer = setTimeout(() => {
473
+ this._resizeTimer = null;
474
+ if (this._cols !== this._lastDispatchedCols || this._rows !== this._lastDispatchedRows) {
475
+ this._lastDispatchedCols = this._cols;
476
+ this._lastDispatchedRows = this._rows;
477
+ const handlers = [...this._resizeHandlers];
478
+ for (const handler of handlers) {
479
+ handler(this._cols, this._rows);
480
+ }
481
+ }
482
+ }, this._resizeDebounceMs);
398
483
  };
399
484
  this.stdout.on("resize", this._resizeHandler);
400
485
  this._setupCleanup();
486
+ if (options.bracketedPaste) {
487
+ this.enableBracketedPaste();
488
+ }
401
489
  }
402
490
  /** Current terminal width in columns */
403
491
  get cols() {
@@ -451,6 +539,18 @@ var Terminal = class {
451
539
  this.write(disableMouse);
452
540
  this._isMouseEnabled = false;
453
541
  }
542
+ /** Emit the enable sequence (CSI ?2004h). Idempotent. */
543
+ enableBracketedPaste() {
544
+ if (this._isBracketedPasteEnabled) return;
545
+ this.write(enableBracketedPaste);
546
+ this._isBracketedPasteEnabled = true;
547
+ }
548
+ /** Emit the disable sequence (CSI ?2004l). Idempotent. */
549
+ disableBracketedPaste() {
550
+ if (!this._isBracketedPasteEnabled) return;
551
+ this.write(disableBracketedPaste);
552
+ this._isBracketedPasteEnabled = false;
553
+ }
454
554
  // ── Cursor ──────────────────────────────────────────
455
555
  hideCursor() {
456
556
  this.write(hideCursor);
@@ -458,10 +558,71 @@ var Terminal = class {
458
558
  showCursor() {
459
559
  this.write(showCursor);
460
560
  }
561
+ /** Set the cursor shape via DECSCUSR. Default blink = true. */
562
+ setCursorShape(shape, blink2) {
563
+ this.write(cursorShape(shape, blink2));
564
+ }
565
+ /** Ring the terminal bell (BEL). */
566
+ bell() {
567
+ this.write(bell);
568
+ }
569
+ /** Send an OSC 9 desktop notification. Body is appended after a separator. */
570
+ notify(title, body) {
571
+ const payload = body === void 0 ? title : `${title}: ${body}`;
572
+ this.write(notify(payload));
573
+ }
461
574
  // ── Output ──────────────────────────────────────────
575
+ /**
576
+ * Writes chunked string data to stdout.
577
+ * Enforces queue serialization to ensure atomic ANSI escape execution.
578
+ */
462
579
  write(data) {
580
+ if (!data) return;
581
+ this._writeQueue.push(data);
582
+ if (this._isWriting) return;
583
+ this._processWriteQueue();
584
+ }
585
+ /**
586
+ * Writes data to stdout synchronously, bypassing the write queue.
587
+ * Used by the renderer during frame flush to avoid races with the
588
+ * async queue lifecycle. Only use for render-path output.
589
+ */
590
+ writeSync(data) {
591
+ if (!data) return;
463
592
  this.stdout.write(data);
464
593
  }
594
+ /**
595
+ * Sequentially unshifts and drains string frames to stdout safely.
596
+ */
597
+ _processWriteQueue() {
598
+ if (this._writeQueue.length === 0) {
599
+ this._isWriting = false;
600
+ return;
601
+ }
602
+ this._isWriting = true;
603
+ const chunk = this._writeQueue.shift();
604
+ const canContinue = this.stdout.write(chunk);
605
+ if (!canContinue) {
606
+ this.stdout.once("drain", () => {
607
+ this._processWriteQueue();
608
+ });
609
+ } else {
610
+ this._processWriteQueue();
611
+ }
612
+ }
613
+ // ── Clipboard ───────────────────────────────────────
614
+ /**
615
+ * Read text from the system clipboard via OSC 52.
616
+ */
617
+ readClipboard() {
618
+ return readClipboard(this.stdin, this.stdout);
619
+ }
620
+ /**
621
+ * Write text to the system clipboard via OSC 52.
622
+ */
623
+ writeClipboard(text) {
624
+ writeClipboard(text, this.stdout);
625
+ }
465
626
  // ── Resize ──────────────────────────────────────────
466
627
  onResize(handler) {
467
628
  this._resizeHandlers.push(handler);
@@ -477,27 +638,35 @@ var Terminal = class {
477
638
  * Called automatically on SIGINT, SIGTERM, process exit.
478
639
  */
479
640
  restore() {
480
- if (this._restored) return;
481
- this._restored = true;
482
- if (this._exitHandler) process.off("exit", this._exitHandler);
483
- if (this._sigintHandler) process.off("SIGINT", this._sigintHandler);
484
- if (this._sigtermHandler) process.off("SIGTERM", this._sigtermHandler);
485
- if (this._uncaughtExceptionHandler) {
486
- process.off("uncaughtException", this._uncaughtExceptionHandler);
487
- this._uncaughtExceptionHandler = null;
488
- }
489
- if (this._unhandledRejectionHandler) {
490
- process.off("unhandledRejection", this._unhandledRejectionHandler);
491
- this._unhandledRejectionHandler = null;
641
+ if (this._restored || this._restoring) return;
642
+ this._restoring = true;
643
+ if (this._resizeTimer) {
644
+ clearTimeout(this._resizeTimer);
645
+ this._resizeTimer = null;
492
646
  }
647
+ this._writeQueue = [];
648
+ this._isWriting = false;
649
+ if (this._exitHandler) process.off("exit", this._exitHandler);
493
650
  if (this._resizeHandler) {
494
651
  this.stdout.off("resize", this._resizeHandler);
495
652
  }
496
- this.disableMouse();
497
- this.exitAltScreen();
498
- this.exitRawMode();
499
- this.showCursor();
500
- this.write(reset);
653
+ const directWrite = this.stdout.write.bind(this.stdout);
654
+ const savedWrite = this.write;
655
+ this.write = (s) => {
656
+ directWrite(s);
657
+ };
658
+ try {
659
+ this.disableBracketedPaste();
660
+ this.disableMouse();
661
+ this.exitAltScreen();
662
+ this.exitRawMode();
663
+ this.showCursor();
664
+ this.write(reset);
665
+ this._restored = true;
666
+ } finally {
667
+ this.write = savedWrite;
668
+ this._restoring = false;
669
+ }
501
670
  }
502
671
  /**
503
672
  * Register a custom cleanup handler that runs on terminal restore.
@@ -507,7 +676,8 @@ var Terminal = class {
507
676
  }
508
677
  _setupCleanup() {
509
678
  const runCleanupHandlers = () => {
510
- for (const handler of this._cleanupHandlers) {
679
+ const handlers = [...this._cleanupHandlers];
680
+ for (const handler of handlers) {
511
681
  try {
512
682
  handler();
513
683
  } catch {
@@ -516,30 +686,209 @@ var Terminal = class {
516
686
  this.restore();
517
687
  };
518
688
  this._exitHandler = runCleanupHandlers;
519
- this._sigintHandler = () => {
520
- runCleanupHandlers();
521
- process.exit(130);
522
- };
523
- this._sigtermHandler = () => {
524
- runCleanupHandlers();
525
- process.exit(143);
526
- };
527
689
  process.on("exit", this._exitHandler);
528
- process.on("SIGINT", this._sigintHandler);
529
- process.on("SIGTERM", this._sigtermHandler);
530
- this._uncaughtExceptionHandler = (err) => {
531
- this.restore();
532
- process.exit(1);
533
- };
534
- this._unhandledRejectionHandler = () => {
535
- this.restore();
536
- process.exit(1);
537
- };
538
- process.on("uncaughtException", this._uncaughtExceptionHandler);
539
- process.on("unhandledRejection", this._unhandledRejectionHandler);
540
690
  }
541
691
  };
542
692
 
693
+ // src/utils/unicode.ts
694
+ function isWideChar(codePoint) {
695
+ return (
696
+ // CJK Unified Ideographs (common Chinese/Japanese/Korean)
697
+ codePoint >= 19968 && codePoint <= 40959 || // CJK Unified Ideographs Extension A
698
+ codePoint >= 13312 && codePoint <= 19903 || // CJK Compatibility Ideographs
699
+ codePoint >= 63744 && codePoint <= 64255 || // Hangul Syllables
700
+ codePoint >= 44032 && codePoint <= 55215 || // Katakana
701
+ codePoint >= 12448 && codePoint <= 12543 || // CJK Symbols and Punctuation
702
+ codePoint >= 12288 && codePoint <= 12351 || // Hiragana
703
+ codePoint >= 12352 && codePoint <= 12447 || // Fullwidth Forms
704
+ codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || // CJK Unified Ideographs Extension B
705
+ codePoint >= 131072 && codePoint <= 173791 || // CJK Unified Ideographs Extension C,D,E,F
706
+ codePoint >= 173824 && codePoint <= 191471 || // CJK Compatibility Ideographs Supplement
707
+ codePoint >= 194560 && codePoint <= 195103
708
+ );
709
+ }
710
+ function isCombining(codePoint) {
711
+ return (
712
+ // Combining Diacritical Marks
713
+ codePoint >= 768 && codePoint <= 879 || // Combining Diacritical Marks Extended
714
+ codePoint >= 6832 && codePoint <= 6911 || // Combining Diacritical Marks Supplement
715
+ codePoint >= 7616 && codePoint <= 7679 || // Combining Diacritical Marks for Symbols
716
+ codePoint >= 8400 && codePoint <= 8447 || // Combining Half Marks
717
+ codePoint >= 65056 && codePoint <= 65071 || // Variation selectors
718
+ codePoint >= 65024 && codePoint <= 65039 || // Zero-width joiner / non-joiner
719
+ codePoint === 8203 || codePoint === 8204 || codePoint === 8205 || codePoint === 65279
720
+ );
721
+ }
722
+ function isEmoji(codePoint) {
723
+ return (
724
+ // Emoticons
725
+ codePoint >= 128512 && codePoint <= 128591 || // Misc Symbols and Pictographs
726
+ codePoint >= 127744 && codePoint <= 128511 || // Transport and Map
727
+ codePoint >= 128640 && codePoint <= 128767 || // Supplemental Symbols
728
+ codePoint >= 129280 && codePoint <= 129535 || // Misc symbols
729
+ codePoint >= 9728 && codePoint <= 9983 || // Dingbats
730
+ codePoint >= 9984 && codePoint <= 10175 || // Flags
731
+ codePoint >= 127456 && codePoint <= 127487
732
+ );
733
+ }
734
+ var segmenter = new Intl.Segmenter();
735
+ function segmentWidth(segment) {
736
+ const cp = segment.codePointAt(0);
737
+ if (cp < 32 || cp >= 127 && cp < 160) {
738
+ return 0;
739
+ }
740
+ if (isCombining(cp)) {
741
+ return 0;
742
+ }
743
+ const charCount = [...segment].length;
744
+ let isMultiCpWide = false;
745
+ if (charCount > 1) {
746
+ const cps = [...segment].map((c) => c.codePointAt(0));
747
+ isMultiCpWide = cps.slice(1).some((c) => !isCombining(c));
748
+ }
749
+ if (isWideChar(cp) || isEmoji(cp) || isMultiCpWide) {
750
+ return 2;
751
+ }
752
+ return 1;
753
+ }
754
+ function stringWidth(str) {
755
+ let width = 0;
756
+ let inEscape = false;
757
+ const segments = segmenter.segment(str);
758
+ for (const { segment } of segments) {
759
+ const cp = segment.codePointAt(0);
760
+ if (cp === 27) {
761
+ inEscape = true;
762
+ continue;
763
+ }
764
+ if (inEscape) {
765
+ if (cp >= 64 && cp <= 126 && cp !== 91) {
766
+ inEscape = false;
767
+ }
768
+ continue;
769
+ }
770
+ width += segmentWidth(segment);
771
+ }
772
+ return width;
773
+ }
774
+ function truncate(str, maxWidth, ellipsis = "\u2026") {
775
+ if (maxWidth <= 0) return "";
776
+ const strW = stringWidth(str);
777
+ if (strW <= maxWidth) return str;
778
+ const ellipsisW = stringWidth(ellipsis);
779
+ const targetW = maxWidth - ellipsisW;
780
+ if (targetW <= 0) return ellipsis.slice(0, maxWidth);
781
+ let width = 0;
782
+ let result = "";
783
+ let inEscape = false;
784
+ let escapeBuffer = "";
785
+ const segments = segmenter.segment(str);
786
+ for (const { segment } of segments) {
787
+ const cp = segment.codePointAt(0);
788
+ if (cp === 27) {
789
+ inEscape = true;
790
+ escapeBuffer += segment;
791
+ continue;
792
+ }
793
+ if (inEscape) {
794
+ escapeBuffer += segment;
795
+ if (cp >= 64 && cp <= 126 && cp !== 91) {
796
+ inEscape = false;
797
+ result += escapeBuffer;
798
+ escapeBuffer = "";
799
+ }
800
+ continue;
801
+ }
802
+ let charW = segmentWidth(segment);
803
+ if (width + charW > targetW) break;
804
+ width += charW;
805
+ result += segment;
806
+ }
807
+ return result + ellipsis;
808
+ }
809
+ function stripAnsi(str) {
810
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
811
+ }
812
+ function wordWrap(str, width) {
813
+ if (width <= 0) return str;
814
+ const lines = str.split("\n");
815
+ const result = [];
816
+ for (const line of lines) {
817
+ if (stringWidth(line) <= width) {
818
+ result.push(line);
819
+ continue;
820
+ }
821
+ let currentLine = "";
822
+ let currentWidth = 0;
823
+ const words = line.split(/(\s+)/);
824
+ for (const word of words) {
825
+ const wordW = stringWidth(word);
826
+ if (currentWidth + wordW <= width) {
827
+ currentLine += word;
828
+ currentWidth += wordW;
829
+ } else if (wordW > width) {
830
+ if (currentLine) {
831
+ result.push(currentLine);
832
+ currentLine = "";
833
+ currentWidth = 0;
834
+ }
835
+ const wordSegments = segmenter.segment(word);
836
+ for (const { segment } of wordSegments) {
837
+ const charW = segmentWidth(segment);
838
+ if (currentWidth + charW > width) {
839
+ result.push(currentLine);
840
+ currentLine = "";
841
+ currentWidth = 0;
842
+ }
843
+ currentLine += segment;
844
+ currentWidth += charW;
845
+ }
846
+ } else {
847
+ result.push(currentLine);
848
+ currentLine = word.trimStart();
849
+ currentWidth = stringWidth(currentLine);
850
+ }
851
+ }
852
+ if (currentLine) {
853
+ result.push(currentLine);
854
+ }
855
+ }
856
+ return result.join("\n");
857
+ }
858
+
859
+ // src/terminal/env-caps.ts
860
+ var caps = {
861
+ color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
862
+ unicode: !process.env.NO_UNICODE && process.env.TERM !== "dumb",
863
+ motion: !process.env.NO_MOTION && !process.env.CI,
864
+ ci: !!process.env.CI,
865
+ get background() {
866
+ if (process.env.TERM_BACKGROUND === "light") return "light";
867
+ if (process.env.TERM_BACKGROUND === "dark") return "dark";
868
+ const colorfgbg = process.env.COLORFGBG;
869
+ if (colorfgbg) {
870
+ const parts = colorfgbg.split(";");
871
+ const bg = parseInt(parts[parts.length - 1], 10);
872
+ if (!Number.isNaN(bg)) return bg < 8 ? "dark" : "light";
873
+ }
874
+ return "dark";
875
+ },
876
+ get keybindingMode() {
877
+ const mode = process.env.TERMUI_KEYBINDINGS;
878
+ if (mode === "vim" || mode === "emacs") return mode;
879
+ return "default";
880
+ }
881
+ };
882
+ function prefersReducedMotion() {
883
+ return !caps.motion;
884
+ }
885
+ function shouldUseColor() {
886
+ return caps.color;
887
+ }
888
+ function prefersHighContrast() {
889
+ return process.env.HIGH_CONTRAST === "1";
890
+ }
891
+
543
892
  // src/terminal/Screen.ts
544
893
  var EMPTY_COLOR = Object.freeze({ type: "none" });
545
894
  function emptyCell() {
@@ -553,7 +902,8 @@ function emptyCell() {
553
902
  dim: false,
554
903
  strikethrough: false,
555
904
  inverse: false,
556
- width: 1
905
+ width: 1,
906
+ link: void 0
557
907
  };
558
908
  }
559
909
  function resetCell(cell) {
@@ -567,9 +917,10 @@ function resetCell(cell) {
567
917
  cell.strikethrough = false;
568
918
  cell.inverse = false;
569
919
  cell.width = 1;
920
+ cell.link = void 0;
570
921
  }
571
922
  function cellsEqual(a, b) {
572
- 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);
923
+ 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);
573
924
  }
574
925
  function colorsEqual(a, b) {
575
926
  if (a.type !== b.type) return false;
@@ -583,25 +934,98 @@ function colorsEqual(a, b) {
583
934
  case "rgb":
584
935
  return a.r === b.r && a.g === b.g && a.b === b.b;
585
936
  case "hex":
586
- return a.hex === b.hex;
937
+ return a.hex.toLowerCase() === b.hex.toLowerCase();
587
938
  }
588
939
  }
589
940
  var Screen = class {
590
941
  _cols;
591
942
  _rows;
943
+ _previousLines = [];
944
+ _lastRenderedHeight = 0;
945
+ get lastRenderedHeight() {
946
+ return this._lastRenderedHeight;
947
+ }
948
+ set lastRenderedHeight(value) {
949
+ this._lastRenderedHeight = value;
950
+ }
951
+ _previousStyleLines = [];
592
952
  front;
593
953
  back;
954
+ /**
955
+ * Render epoch counter. Incremented on every swap so downstream consumers
956
+ * (e.g. Renderer._flush) can detect and skip stale frames from a previous
957
+ * epoch, preventing double-swap corruption.
958
+ */
959
+ _epoch = 0;
960
+ /** True while swap() is executing to prevent re-entrant double-swap corruption. */
961
+ _swapping = false;
962
+ /** The epoch captured at the start of the current flush cycle. */
963
+ _flushEpoch = -1;
594
964
  /**
595
965
  * Stack of clipping regions. When non-empty, setCell/writeString
596
966
  * only write to cells within the topmost clip rectangle.
597
967
  */
598
968
  _clipStack = [];
969
+ _translateYStack = [];
970
+ _translateY = 0;
599
971
  constructor(cols, rows) {
600
972
  this._cols = cols;
601
973
  this._rows = rows;
602
974
  this.front = this._createGrid(cols, rows);
603
975
  this.back = this._createGrid(cols, rows);
604
976
  }
977
+ /** Retrieve a read-only copy of the cell at (x, y) from the back buffer. */
978
+ getCell(x, y) {
979
+ x = Math.floor(x);
980
+ y = Math.floor(y);
981
+ if (!(x >= 0 && x < this._cols && y >= 0 && y < this._rows)) return void 0;
982
+ return this.back[y][x];
983
+ }
984
+ /** Serialize a back-buffer row to a plain string (skips continuation cells). */
985
+ getLine(row) {
986
+ if (row < 0 || row >= this._rows) return "";
987
+ return this.back[row].filter((cell) => cell.width !== 0).map((cell) => cell.char || " ").join("");
988
+ }
989
+ /**
990
+ * Serialize the style attributes of a back-buffer row into a
991
+ * fingerprint string. When the characters are identical but the
992
+ * styles differ (color, bold, italic, etc.), this fingerprint
993
+ * changes, allowing the diff renderer to detect style-only updates.
994
+ */
995
+ getStyleLine(row) {
996
+ if (row < 0 || row >= this._rows) return "";
997
+ let hash = 0;
998
+ for (const cell of this.back[row]) {
999
+ if (cell.width === 0) continue;
1000
+ const fg = cell.fg.type;
1001
+ const bg = cell.bg.type;
1002
+ 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);
1003
+ const seed = fg.charCodeAt(0) * 65536 + bg.charCodeAt(0) * 4096 + bits;
1004
+ hash = (hash << 7) - hash + seed | 0;
1005
+ if (cell.link) {
1006
+ for (let i = 0; i < cell.link.length; i++)
1007
+ hash = (hash << 5) - hash + cell.link.charCodeAt(i) | 0;
1008
+ }
1009
+ }
1010
+ return String(hash);
1011
+ }
1012
+ /** Return the saved line string for the given row (empty before first saveLines call). */
1013
+ getPreviousLine(row) {
1014
+ return this._previousLines[row] ?? "";
1015
+ }
1016
+ /** Return the saved style fingerprint for the given row. */
1017
+ getPreviousStyleLine(row) {
1018
+ return this._previousStyleLines[row] ?? "";
1019
+ }
1020
+ /** Snapshot the current back-buffer line strings for use by diffRenderer. */
1021
+ saveLines() {
1022
+ this._previousLines = [];
1023
+ this._previousStyleLines = [];
1024
+ for (let r = 0; r < this._rows; r++) {
1025
+ this._previousLines.push(this.getLine(r));
1026
+ this._previousStyleLines.push(this.getStyleLine(r));
1027
+ }
1028
+ }
605
1029
  get cols() {
606
1030
  return this._cols;
607
1031
  }
@@ -642,12 +1066,21 @@ var Screen = class {
642
1066
  get activeClip() {
643
1067
  return this._clipStack.length > 0 ? this._clipStack[this._clipStack.length - 1] : null;
644
1068
  }
1069
+ pushTranslateY(offset) {
1070
+ this._translateYStack.push(offset);
1071
+ this._translateY += offset;
1072
+ }
1073
+ popTranslateY() {
1074
+ const offset = this._translateYStack.pop() ?? 0;
1075
+ this._translateY -= offset;
1076
+ }
645
1077
  /**
646
1078
  * Write a cell to the back buffer at position (col, row).
647
1079
  */
648
1080
  setCell(col, row, cell) {
649
1081
  col = Math.floor(col);
650
1082
  row = Math.floor(row);
1083
+ row += this._translateY;
651
1084
  if (!(col >= 0 && col < this._cols && row >= 0 && row < this._rows)) return;
652
1085
  if (this._clipStack.length > 0) {
653
1086
  const clip = this._clipStack[this._clipStack.length - 1];
@@ -656,6 +1089,9 @@ var Screen = class {
656
1089
  }
657
1090
  }
658
1091
  const existing = this.back[row][col];
1092
+ if (cell.char !== void 0) {
1093
+ cell = { ...cell, char: stripAnsiControl(cell.char) };
1094
+ }
659
1095
  Object.assign(existing, cell);
660
1096
  }
661
1097
  /**
@@ -666,31 +1102,37 @@ var Screen = class {
666
1102
  row = Math.floor(row);
667
1103
  col = Math.floor(col);
668
1104
  if (!(row >= 0 && row < this._rows)) return;
1105
+ const safeStr = stripAnsiControl(str);
669
1106
  let x = col;
670
- for (const char of str) {
1107
+ const segments = segmenter.segment(safeStr);
1108
+ for (const { segment } of segments) {
671
1109
  if (x >= this._cols) break;
1110
+ let finalChar = segment;
1111
+ let width = stringWidth(segment);
672
1112
  if (x < 0) {
673
- x++;
1113
+ x += width;
674
1114
  continue;
675
1115
  }
676
- const cp = char.codePointAt(0);
677
- const isWide = this._isWideCodePoint(cp);
678
- const width = isWide ? 2 : 1;
1116
+ if (width > 1 && !caps.unicode) {
1117
+ finalChar = "*";
1118
+ width = 1;
1119
+ }
1120
+ if (width === 0) continue;
679
1121
  this.setCell(x, row, {
680
- char,
1122
+ char: finalChar,
681
1123
  width,
682
1124
  ...style
683
1125
  });
684
- if (isWide && x + 1 < this._cols) {
685
- this.setCell(x + 1, row, {
686
- char: "",
687
- width: 0,
688
- ...style
689
- });
690
- x += 2;
691
- } else {
692
- x += 1;
1126
+ for (let i = 1; i < width; i++) {
1127
+ if (x + i < this._cols) {
1128
+ this.setCell(x + i, row, {
1129
+ char: "",
1130
+ width: 0,
1131
+ ...style
1132
+ });
1133
+ }
693
1134
  }
1135
+ x += width;
694
1136
  }
695
1137
  }
696
1138
  /**
@@ -704,13 +1146,34 @@ var Screen = class {
704
1146
  }
705
1147
  }
706
1148
  }
1149
+ /** Current render epoch — incremented after each swap. */
1150
+ get epoch() {
1151
+ return this._epoch;
1152
+ }
1153
+ /** The epoch captured at the start of the current flush cycle. */
1154
+ get flushEpoch() {
1155
+ return this._flushEpoch;
1156
+ }
1157
+ set flushEpoch(value) {
1158
+ this._flushEpoch = value;
1159
+ }
707
1160
  /**
708
1161
  * Swap front and back buffers. Called after rendering diffs.
1162
+ * Uses mutual exclusion to prevent double-swap corruption when
1163
+ * _flush() is called concurrently (e.g. from duplicate setImmediate
1164
+ * callbacks).
709
1165
  */
710
1166
  swap() {
711
- const temp = this.front;
712
- this.front = this.back;
713
- this.back = temp;
1167
+ if (this._swapping) return;
1168
+ this._swapping = true;
1169
+ try {
1170
+ const temp = this.front;
1171
+ this.front = this.back;
1172
+ this.back = temp;
1173
+ this._epoch++;
1174
+ } finally {
1175
+ this._swapping = false;
1176
+ }
714
1177
  }
715
1178
  /**
716
1179
  * Resize the screen. Clears both buffers.
@@ -720,17 +1183,43 @@ var Screen = class {
720
1183
  this._rows = rows;
721
1184
  this.front = this._createGrid(cols, rows);
722
1185
  this.back = this._createGrid(cols, rows);
1186
+ this._previousLines = [];
723
1187
  }
724
1188
  /**
725
1189
  * Clear the front buffer (marks everything as "needs redraw").
1190
+ * Mutates cells in-place to avoid GC pressure from object allocation.
726
1191
  */
727
1192
  invalidate() {
728
1193
  for (let r = 0; r < this._rows; r++) {
729
1194
  for (let c = 0; c < this._cols; c++) {
730
- this.front[r][c] = { ...emptyCell(), char: "\0" };
1195
+ resetCell(this.front[r][c]);
1196
+ this.front[r][c].char = "\0";
731
1197
  }
732
1198
  }
733
1199
  }
1200
+ /**
1201
+ * Export current screen as ANSI snapshot text.
1202
+ */
1203
+ exportANSI() {
1204
+ const lines = [];
1205
+ for (let r = 0; r < this._rows; r++) {
1206
+ lines.push(this.getLine(r));
1207
+ }
1208
+ return lines.join("\n");
1209
+ }
1210
+ /**
1211
+ * Export current screen as SVG.
1212
+ */
1213
+ exportSVG() {
1214
+ return `
1215
+ <svg xmlns="http://www.w3.org/2000/svg"
1216
+ width="${this._cols * 8}"
1217
+ height="${this._rows * 16}">
1218
+ <text x="10" y="20">
1219
+ Terminal Export
1220
+ </text>
1221
+ </svg>`;
1222
+ }
734
1223
  _createGrid(cols, rows) {
735
1224
  const grid = [];
736
1225
  for (let r = 0; r < rows; r++) {
@@ -742,25 +1231,74 @@ var Screen = class {
742
1231
  }
743
1232
  return grid;
744
1233
  }
745
- _isWideCodePoint(cp) {
746
- 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;
1234
+ };
1235
+
1236
+ // src/renderer/render-hook.ts
1237
+ var RenderHook = class {
1238
+ _buffer = [];
1239
+ _isActive = false;
1240
+ _originalConsole = {};
1241
+ // any[]: console methods accept arbitrary argument shapes
1242
+ /** Check if the hook is currently intercepting console output */
1243
+ get isActive() {
1244
+ return this._isActive;
1245
+ }
1246
+ /** Wrap console.log/warn/error to buffer external logs instead of writing to stdout */
1247
+ start() {
1248
+ if (this._isActive) return;
1249
+ this._isActive = true;
1250
+ const methods = ["log", "warn", "error"];
1251
+ for (const method of methods) {
1252
+ this._originalConsole[method] = console[method];
1253
+ const hook = this;
1254
+ console[method] = function(...args) {
1255
+ const text = args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
1256
+ hook._buffer.push(text + "\n");
1257
+ };
1258
+ }
1259
+ }
1260
+ /** Restore original console methods */
1261
+ stop() {
1262
+ if (!this._isActive) return;
1263
+ this._isActive = false;
1264
+ for (const [method, original] of Object.entries(this._originalConsole)) {
1265
+ console[method] = original;
1266
+ }
1267
+ this._originalConsole = {};
1268
+ }
1269
+ /** Retrieve and clear the buffered logs */
1270
+ flush() {
1271
+ if (this._buffer.length === 0) return "";
1272
+ const out = this._buffer.join("");
1273
+ this._buffer = [];
1274
+ return out;
1275
+ }
1276
+ /** Write directly to process.stdout, bypassing any buffering */
1277
+ writeRaw(text) {
1278
+ process.stdout.write(text);
747
1279
  }
748
1280
  };
749
1281
 
750
1282
  // src/terminal/Renderer.ts
751
- var Renderer = class {
1283
+ var Renderer = class _Renderer {
752
1284
  _terminal;
753
1285
  _screen;
754
1286
  _fps;
755
1287
  _frameTimer = null;
756
1288
  _renderRequested = false;
757
1289
  _colorDepth;
1290
+ _diffRenderer;
758
1291
  _onTick = null;
759
- constructor(terminal, screen, fps = 30) {
1292
+ _callbacks = /* @__PURE__ */ new Set();
1293
+ /** The stdout interceptor hook for buffering external logs */
1294
+ hook;
1295
+ constructor(terminal, screen, fps = 30, diffRenderer = true) {
760
1296
  this._terminal = terminal;
761
1297
  this._screen = screen;
762
1298
  this._fps = fps;
763
1299
  this._colorDepth = terminal.colorDepth;
1300
+ this._diffRenderer = diffRenderer;
1301
+ this.hook = new RenderHook();
764
1302
  }
765
1303
  /** Change the rendering frame rate cap */
766
1304
  setFPS(fps) {
@@ -777,10 +1315,6 @@ var Renderer = class {
777
1315
  const interval = Math.floor(1e3 / this._fps);
778
1316
  this._frameTimer = setInterval(() => {
779
1317
  this._onTick?.();
780
- if (this._renderRequested) {
781
- this._renderRequested = false;
782
- this._flush();
783
- }
784
1318
  }, interval);
785
1319
  }
786
1320
  /** Stop the render loop */
@@ -798,6 +1332,13 @@ var Renderer = class {
798
1332
  renderNow() {
799
1333
  this._flush();
800
1334
  }
1335
+ /** Register a per-frame profiling callback. Returns an unsubscribe function. */
1336
+ onFrame(cb) {
1337
+ this._callbacks.add(cb);
1338
+ return () => {
1339
+ this._callbacks.delete(cb);
1340
+ };
1341
+ }
801
1342
  /**
802
1343
  * Full-screen clear and redraw (first render or after resize).
803
1344
  */
@@ -805,51 +1346,210 @@ var Renderer = class {
805
1346
  this._screen.invalidate();
806
1347
  this._flush();
807
1348
  }
1349
+ /** ANSI sequence to save cursor position */
1350
+ static _CURSOR_SAVE = "\x1B[s";
1351
+ /** ANSI sequence to restore cursor position */
1352
+ static _CURSOR_RESTORE = "\x1B[u";
808
1353
  /**
809
1354
  * Core diff and flush: compare front vs back buffer,
810
1355
  * emit only changed cells.
811
1356
  */
812
1357
  _flush() {
813
- const { front, back, cols, rows } = this._screen;
814
- let output = beginSyncUpdate;
815
- let lastRow = -1;
816
- let lastCol = -1;
817
- for (let r = 0; r < rows; r++) {
818
- for (let c = 0; c < cols; c++) {
819
- const frontCell = front[r][c];
820
- const backCell = back[r][c];
821
- if (cellsEqual(frontCell, backCell)) continue;
822
- if (backCell.width === 0) continue;
823
- if (r !== lastRow || c !== lastCol) {
824
- output += moveTo(c, r);
1358
+ const epoch = this._screen.epoch;
1359
+ if (this._screen.flushEpoch === epoch) return;
1360
+ this._screen.flushEpoch = epoch;
1361
+ const start = this._callbacks.size > 0 ? performance.now() : 0;
1362
+ const bufferedLogs = this.hook.flush();
1363
+ if (bufferedLogs) {
1364
+ this._screen.invalidate();
1365
+ }
1366
+ try {
1367
+ const { front, back, cols, rows } = this._screen;
1368
+ let output = beginSyncUpdate;
1369
+ if (this._diffRenderer) {
1370
+ this._lastStyleFingerprint = null;
1371
+ for (let r = 0; r < rows; r++) {
1372
+ output += this._renderDiffLine(r, front, back, cols);
1373
+ }
1374
+ output += reset;
1375
+ output += endSyncUpdate;
1376
+ if (bufferedLogs) {
1377
+ this._terminal.writeSync(_Renderer._CURSOR_SAVE + bufferedLogs + _Renderer._CURSOR_RESTORE);
825
1378
  }
826
- output += this._renderCell(backCell);
827
- lastRow = r;
828
- lastCol = c + (backCell.width === 2 ? 2 : 1);
1379
+ this._terminal.writeSync(output);
1380
+ this._screen.saveLines();
1381
+ this._emitStats(start, bufferedLogs, output);
1382
+ this._screen.swap();
1383
+ return;
1384
+ }
1385
+ for (let r = 0; r < rows; r++) {
1386
+ if (this._screen.getLine(r) === this._screen.getPreviousLine(r) && this._screen.getStyleLine(r) === this._screen.getPreviousStyleLine(r)) continue;
1387
+ output += moveTo(0, r);
1388
+ output += this._renderLine(r);
829
1389
  }
1390
+ output += reset;
1391
+ output += endSyncUpdate;
1392
+ if (bufferedLogs) {
1393
+ this._terminal.writeSync(_Renderer._CURSOR_SAVE + bufferedLogs + _Renderer._CURSOR_RESTORE);
1394
+ }
1395
+ this._terminal.writeSync(output);
1396
+ this._emitStats(start, bufferedLogs, output);
1397
+ this._screen.saveLines();
1398
+ this._screen.swap();
1399
+ } catch (_err) {
1400
+ this._renderRequested = true;
1401
+ this._lastStyleFingerprint = null;
1402
+ }
1403
+ }
1404
+ /** Style fingerprint of the last rendered cell (to suppress redundant ANSI reset/apply). */
1405
+ _lastStyleFingerprint = null;
1406
+ /** Build a stable style fingerprint string for a cell (avoids allocation-heavy object comparison). */
1407
+ _styleFingerprint(cell) {
1408
+ const fg = cell.fg;
1409
+ const bg = cell.bg;
1410
+ let fgKey;
1411
+ switch (fg.type) {
1412
+ case "none":
1413
+ fgKey = "n";
1414
+ break;
1415
+ case "named":
1416
+ fgKey = `N:${fg.name}`;
1417
+ break;
1418
+ case "ansi256":
1419
+ fgKey = `A:${fg.code}`;
1420
+ break;
1421
+ case "rgb":
1422
+ fgKey = `R:${fg.r},${fg.g},${fg.b}`;
1423
+ break;
1424
+ case "hex":
1425
+ fgKey = `H:${fg.hex.toLowerCase()}`;
1426
+ break;
1427
+ default:
1428
+ fgKey = "n";
1429
+ }
1430
+ let bgKey;
1431
+ switch (bg.type) {
1432
+ case "none":
1433
+ bgKey = "n";
1434
+ break;
1435
+ case "named":
1436
+ bgKey = `N:${bg.name}`;
1437
+ break;
1438
+ case "ansi256":
1439
+ bgKey = `A:${bg.code}`;
1440
+ break;
1441
+ case "rgb":
1442
+ bgKey = `R:${bg.r},${bg.g},${bg.b}`;
1443
+ break;
1444
+ case "hex":
1445
+ bgKey = `H:${bg.hex.toLowerCase()}`;
1446
+ break;
1447
+ default:
1448
+ bgKey = "n";
830
1449
  }
831
- output += reset;
832
- output += endSyncUpdate;
833
- this._terminal.write(output);
834
- this._screen.swap();
1450
+ return `${cell.bold ? "B" : ""}${cell.dim ? "D" : ""}${cell.italic ? "I" : ""}${cell.underline ? "U" : ""}${cell.strikethrough ? "S" : ""}${cell.inverse ? "V" : ""}|${fgKey}|${bgKey}`;
835
1451
  }
836
1452
  /**
837
1453
  * Generate the ANSI escape sequence to render a single cell.
1454
+ * Skips ansiReset + re-apply when the adjacent cell has identical style.
838
1455
  */
839
1456
  _renderCell(cell) {
840
1457
  let seq = "";
841
- seq += reset;
842
- if (cell.bold) seq += "\x1B[1m";
843
- if (cell.dim) seq += "\x1B[2m";
844
- if (cell.italic) seq += "\x1B[3m";
845
- if (cell.underline) seq += "\x1B[4m";
846
- if (cell.strikethrough) seq += "\x1B[9m";
847
- if (cell.inverse) seq += "\x1B[7m";
848
- seq += colorToAnsiFg(cell.fg, this._colorDepth);
849
- seq += colorToAnsiBg(cell.bg, this._colorDepth);
850
- seq += cell.char || " ";
1458
+ const fp = this._styleFingerprint(cell);
1459
+ if (fp !== this._lastStyleFingerprint) {
1460
+ seq += reset;
1461
+ if (cell.bold) seq += "\x1B[1m";
1462
+ if (cell.dim) seq += "\x1B[2m";
1463
+ if (cell.italic) seq += "\x1B[3m";
1464
+ if (cell.underline) seq += "\x1B[4m";
1465
+ if (cell.strikethrough) seq += "\x1B[9m";
1466
+ if (cell.inverse) seq += "\x1B[7m";
1467
+ seq += colorToAnsiFg(cell.fg, this._colorDepth);
1468
+ seq += colorToAnsiBg(cell.bg, this._colorDepth);
1469
+ this._lastStyleFingerprint = fp;
1470
+ }
1471
+ seq += stripAnsiControl(cell.char) || " ";
851
1472
  return seq;
852
1473
  }
1474
+ /**
1475
+ * If a span starts at a width-0 continuation cell (the second half of a
1476
+ * wide character), adjust backward to the preceding cell so the cursor
1477
+ * is placed at a valid column boundary.
1478
+ */
1479
+ static _adjustSpanStart(col, row) {
1480
+ while (col > 0 && row[col].width === 0) {
1481
+ col--;
1482
+ }
1483
+ return col;
1484
+ }
1485
+ /**
1486
+ * Render only the changed spans within a single row (cell-level granularity).
1487
+ * Uses moveTo to position the cursor at the start of each changed span.
1488
+ */
1489
+ _renderDiffLine(row, front, back, cols) {
1490
+ let output = "";
1491
+ let spanStart = -1;
1492
+ for (let c = 0; c < cols; c++) {
1493
+ if (back[row][c].width === 0) continue;
1494
+ const changed = !cellsEqual(front[row][c], back[row][c]);
1495
+ if (changed && spanStart === -1) {
1496
+ spanStart = c;
1497
+ } else if (!changed && spanStart !== -1) {
1498
+ const adjustedStart = _Renderer._adjustSpanStart(spanStart, back[row]);
1499
+ output += moveTo(adjustedStart, row);
1500
+ for (let sc = spanStart; sc < c; sc++) {
1501
+ const cell = back[row][sc];
1502
+ if (cell.width === 0) continue;
1503
+ output += this._renderCell(cell);
1504
+ }
1505
+ spanStart = -1;
1506
+ }
1507
+ }
1508
+ if (spanStart !== -1) {
1509
+ const adjustedStart = _Renderer._adjustSpanStart(spanStart, back[row]);
1510
+ output += moveTo(adjustedStart, row);
1511
+ for (let sc = spanStart; sc < cols; sc++) {
1512
+ const cell = back[row][sc];
1513
+ if (cell.width === 0) continue;
1514
+ output += this._renderCell(cell);
1515
+ }
1516
+ }
1517
+ return output;
1518
+ }
1519
+ _renderLine(row) {
1520
+ let output = "";
1521
+ for (let c = 0; c < this._screen.cols; c++) {
1522
+ const cell = this._screen.back[row][c];
1523
+ if (cell.width === 0) continue;
1524
+ output += this._renderCell(cell);
1525
+ }
1526
+ return output;
1527
+ }
1528
+ _emitStats(start, bufferedLogs, output) {
1529
+ if (this._callbacks.size === 0) return;
1530
+ const durationMs = performance.now() - start;
1531
+ const { front, back, cols, rows } = this._screen;
1532
+ let cellsChanged = 0;
1533
+ for (let r = 0; r < rows; r++) {
1534
+ for (let c = 0; c < cols; c++) {
1535
+ if (!cellsEqual(front[r][c], back[r][c])) {
1536
+ cellsChanged++;
1537
+ }
1538
+ }
1539
+ }
1540
+ const bytesWritten = (bufferedLogs ? Buffer.byteLength(bufferedLogs) : 0) + Buffer.byteLength(output);
1541
+ const stats = {
1542
+ cellsChanged,
1543
+ bytesWritten,
1544
+ durationMs: Math.max(0, durationMs)
1545
+ };
1546
+ for (const cb of this._callbacks) {
1547
+ try {
1548
+ cb(stats);
1549
+ } catch {
1550
+ }
1551
+ }
1552
+ }
853
1553
  };
854
1554
 
855
1555
  // src/terminal/LayerManager.ts
@@ -860,9 +1560,12 @@ var LayerManager = class {
860
1560
  _layers = /* @__PURE__ */ new Map();
861
1561
  _cols;
862
1562
  _rows;
1563
+ _hitWidgetGrid;
1564
+ _hitZGrid;
863
1565
  constructor(cols, rows) {
864
1566
  this._cols = cols;
865
1567
  this._rows = rows;
1568
+ this._allocateHitGrids();
866
1569
  }
867
1570
  get cols() {
868
1571
  return this._cols;
@@ -936,15 +1639,30 @@ var LayerManager = class {
936
1639
  col = Math.floor(col);
937
1640
  if (!(row >= 0 && row < this._rows)) return;
938
1641
  let x = col;
939
- for (const char of str) {
1642
+ for (const { segment: char } of segmenter.segment(str)) {
940
1643
  if (x >= this._cols) break;
1644
+ const charWidth = segmentWidth(char);
941
1645
  if (x < 0) {
942
- x++;
1646
+ x += charWidth;
943
1647
  continue;
944
1648
  }
945
- this.setCell(layerId, x, row, { char, width: 1, ...style });
946
- x++;
1649
+ this.setCell(layerId, x, row, { char, width: charWidth, ...style });
1650
+ if (charWidth === 2 && x + 1 < this._cols) {
1651
+ this.setCell(layerId, x + 1, row, { char: " ", width: 1, ...style });
1652
+ }
1653
+ x += charWidth;
1654
+ }
1655
+ }
1656
+ /**
1657
+ * Check whether any visible layer has pending dirty changes.
1658
+ */
1659
+ hasDirtyLayers() {
1660
+ for (const layer of this._layers.values()) {
1661
+ if (layer.visible && layer.dirtyRegion) {
1662
+ return true;
1663
+ }
947
1664
  }
1665
+ return false;
948
1666
  }
949
1667
  /**
950
1668
  * Clear all cells in a specific layer.
@@ -957,7 +1675,7 @@ var LayerManager = class {
957
1675
  layer.cells[r][c] = emptyCell();
958
1676
  }
959
1677
  }
960
- layer.dirtyRegion = null;
1678
+ layer.dirtyRegion = { x: 0, y: 0, width: this._cols, height: this._rows };
961
1679
  }
962
1680
  /**
963
1681
  * Clear all overlay layers.
@@ -971,28 +1689,29 @@ var LayerManager = class {
971
1689
  * Composite all overlay layers onto the Screen's back buffer.
972
1690
  * Layers are applied in z-index order (lowest first).
973
1691
  * Transparent cells (empty with no colors) are skipped.
1692
+ * Writes directly to screen.back to avoid setCell overhead
1693
+ * (bounds/clip checks are already satisfied by dirtyRegion).
974
1694
  */
975
1695
  composite(screen) {
976
1696
  const sorted = this.getSortedLayers();
977
1697
  for (const layer of sorted) {
978
1698
  if (!layer.dirtyRegion) continue;
979
1699
  const { x: dx, y: dy, width: dw, height: dh } = layer.dirtyRegion;
980
- for (let r = dy; r < dy + dh && r < this._rows; r++) {
981
- for (let c = dx; c < dx + dw && c < this._cols; c++) {
982
- const cell = layer.cells[r][c];
983
- if (isCellTransparent(cell)) continue;
984
- screen.setCell(c, r, {
985
- char: cell.char,
986
- fg: cell.fg,
987
- bg: cell.bg,
988
- bold: cell.bold,
989
- italic: cell.italic,
990
- underline: cell.underline,
991
- dim: cell.dim,
992
- strikethrough: cell.strikethrough,
993
- inverse: cell.inverse,
994
- width: cell.width
995
- });
1700
+ const maxRow = Math.min(dy + dh, this._rows);
1701
+ const maxCol = Math.min(dx + dw, this._cols);
1702
+ for (let r = dy; r < maxRow; r++) {
1703
+ const backRow = screen.back[r];
1704
+ const layerRow = layer.cells[r];
1705
+ if (!backRow || !layerRow) continue;
1706
+ let c = dx;
1707
+ while (c < maxCol) {
1708
+ const cell = layerRow[c];
1709
+ if (isCellTransparent(cell)) {
1710
+ c++;
1711
+ continue;
1712
+ }
1713
+ Object.assign(backRow[c], cell);
1714
+ c++;
996
1715
  }
997
1716
  }
998
1717
  }
@@ -1007,6 +1726,41 @@ var LayerManager = class {
1007
1726
  layer.cells = this._createGrid();
1008
1727
  layer.dirtyRegion = null;
1009
1728
  }
1729
+ this._allocateHitGrids();
1730
+ }
1731
+ /** Reset the hit grid. Call once at the start of each frame. */
1732
+ clearHitGrid() {
1733
+ this._allocateHitGrids();
1734
+ }
1735
+ /**
1736
+ * Mark a rectangular region as owned by a widget at a given z-index.
1737
+ * Higher z wins when regions overlap.
1738
+ */
1739
+ setHitRegion(widgetId, x, y, w, h, z) {
1740
+ const zVal = z ?? 0;
1741
+ const startX = Math.floor(x);
1742
+ const startY = Math.floor(y);
1743
+ const width = Math.floor(w);
1744
+ const height = Math.floor(h);
1745
+ for (let r = startY; r < startY + height; r++) {
1746
+ if (r < 0 || r >= this._rows) continue;
1747
+ for (let c = startX; c < startX + width; c++) {
1748
+ if (c < 0 || c >= this._cols) continue;
1749
+ if (zVal >= this._hitZGrid[r][c]) {
1750
+ this._hitWidgetGrid[r][c] = widgetId;
1751
+ this._hitZGrid[r][c] = zVal;
1752
+ }
1753
+ }
1754
+ }
1755
+ }
1756
+ /** Return the topmost widget id at a cell, or null. */
1757
+ hitTest(col, row) {
1758
+ const c = Math.floor(col);
1759
+ const r = Math.floor(row);
1760
+ if (c < 0 || c >= this._cols || r < 0 || r >= this._rows) {
1761
+ return null;
1762
+ }
1763
+ return this._hitWidgetGrid[r][c];
1010
1764
  }
1011
1765
  /**
1012
1766
  * Create an empty cell grid.
@@ -1022,6 +1776,23 @@ var LayerManager = class {
1022
1776
  }
1023
1777
  return grid;
1024
1778
  }
1779
+ /**
1780
+ * Allocate parallel hit grid and z-index grid.
1781
+ */
1782
+ _allocateHitGrids() {
1783
+ this._hitWidgetGrid = [];
1784
+ this._hitZGrid = [];
1785
+ for (let r = 0; r < this._rows; r++) {
1786
+ const widgetRow = [];
1787
+ const zRow = [];
1788
+ for (let c = 0; c < this._cols; c++) {
1789
+ widgetRow.push(null);
1790
+ zRow.push(-Infinity);
1791
+ }
1792
+ this._hitWidgetGrid.push(widgetRow);
1793
+ this._hitZGrid.push(zRow);
1794
+ }
1795
+ }
1025
1796
  /**
1026
1797
  * Expand the dirty region of a layer to include the given cell.
1027
1798
  */
@@ -1042,14 +1813,6 @@ var LayerManager = class {
1042
1813
  }
1043
1814
  };
1044
1815
 
1045
- // src/terminal/env-caps.ts
1046
- var caps = {
1047
- color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1048
- unicode: !process.env.NO_UNICODE && process.env.TERM !== "dumb",
1049
- motion: !process.env.NO_MOTION && !process.env.CI,
1050
- ci: !!process.env.CI
1051
- };
1052
-
1053
1816
  // src/terminal/ascii-map.ts
1054
1817
  var BOX = {
1055
1818
  "\u250C": "+",
@@ -1078,6 +1841,93 @@ var BOX = {
1078
1841
  var BRAILLE_SPIN = ["|", "/", "-", "\\"];
1079
1842
  var BLOCK = { full: "#", empty: " ", partial: "-" };
1080
1843
 
1844
+ // src/terminal/bell.ts
1845
+ function bell2() {
1846
+ if (typeof process !== "undefined" && process.stdout && process.stdout.write) {
1847
+ process.stdout.write("\x07");
1848
+ }
1849
+ }
1850
+
1851
+ // src/renderer/border-merge.ts
1852
+ var VERTICAL = /* @__PURE__ */ new Set(["\u2502", "|"]);
1853
+ var HORIZONTAL = /* @__PURE__ */ new Set(["\u2500", "-"]);
1854
+ function isVertical(char) {
1855
+ return VERTICAL.has(char);
1856
+ }
1857
+ function isHorizontal(char) {
1858
+ return HORIZONTAL.has(char);
1859
+ }
1860
+ var UNICODE_JUNCTIONS = {
1861
+ LRTB: "\u253C",
1862
+ RTB: "\u251C",
1863
+ LTB: "\u2524",
1864
+ LRB: "\u252C",
1865
+ LRT: "\u2534",
1866
+ RB: "\u250C",
1867
+ LB: "\u2510",
1868
+ RT: "\u2514",
1869
+ LT: "\u2518",
1870
+ TB: "\u2502",
1871
+ LR: "\u2500",
1872
+ R: "\u2500",
1873
+ L: "\u2500",
1874
+ T: "\u2502",
1875
+ B: "\u2502"
1876
+ };
1877
+ var ASCII_JUNCTIONS = {
1878
+ LRTB: "+",
1879
+ RTB: "+",
1880
+ LTB: "+",
1881
+ LRB: "+",
1882
+ LRT: "+",
1883
+ RB: "+",
1884
+ LB: "+",
1885
+ RT: "+",
1886
+ LT: "+",
1887
+ TB: "|",
1888
+ LR: "-",
1889
+ R: "-",
1890
+ L: "-",
1891
+ T: "|",
1892
+ B: "|"
1893
+ };
1894
+ function getJunctions() {
1895
+ return caps.unicode ? UNICODE_JUNCTIONS : ASCII_JUNCTIONS;
1896
+ }
1897
+ function mergeBorders(screen) {
1898
+ const grid = screen.back;
1899
+ const junctions = getJunctions();
1900
+ const updates = [];
1901
+ for (let row = 0; row < screen.rows; row++) {
1902
+ for (let col = 0; col < screen.cols; col++) {
1903
+ const cell = grid[row][col];
1904
+ const top = row > 0 ? grid[row - 1][col].char : "";
1905
+ const bottom = row < screen.rows - 1 ? grid[row + 1][col].char : "";
1906
+ const left = col > 0 ? grid[row][col - 1].char : "";
1907
+ const right = col < screen.cols - 1 ? grid[row][col + 1].char : "";
1908
+ const hasTop = isVertical(top);
1909
+ const hasBottom = isVertical(bottom);
1910
+ const hasLeft = isHorizontal(left);
1911
+ const hasRight = isHorizontal(right);
1912
+ const key = (hasLeft ? "L" : "") + (hasRight ? "R" : "") + (hasTop ? "T" : "") + (hasBottom ? "B" : "");
1913
+ const merged = junctions[key];
1914
+ if (merged) {
1915
+ updates.push({
1916
+ row,
1917
+ col,
1918
+ char: merged
1919
+ });
1920
+ }
1921
+ }
1922
+ }
1923
+ for (const update of updates) {
1924
+ grid[update.row][update.col].char = update.char;
1925
+ }
1926
+ }
1927
+
1928
+ // src/input/InputParser.ts
1929
+ import { Buffer as Buffer2 } from "buffer";
1930
+
1081
1931
  // src/events/types.ts
1082
1932
  function createKeyEvent(base) {
1083
1933
  const event = {
@@ -1180,6 +2030,19 @@ var SPECIAL_KEYS = {
1180
2030
  10: "enter",
1181
2031
  32: "space"
1182
2032
  };
2033
+ function normalizeNavigationKey(keyName) {
2034
+ const mode = caps.keybindingMode;
2035
+ if (mode === "vim") {
2036
+ if (keyName === "k") return "up";
2037
+ if (keyName === "j") return "down";
2038
+ if (keyName === "h") return "left";
2039
+ if (keyName === "l") return "right";
2040
+ } else if (mode === "emacs") {
2041
+ if (keyName === "ctrl+p") return "up";
2042
+ if (keyName === "ctrl+n") return "down";
2043
+ }
2044
+ return keyName;
2045
+ }
1183
2046
 
1184
2047
  // src/input/MouseParser.ts
1185
2048
  function parseMouseEvent(data) {
@@ -1192,13 +2055,28 @@ function parseMouseEvent(data) {
1192
2055
  let button;
1193
2056
  let type;
1194
2057
  let scrollDelta;
2058
+ let scrollDeltaX;
2059
+ let scrollAxis;
1195
2060
  const buttonBits = cb & 3;
1196
2061
  const motion = (cb & 32) !== 0;
1197
2062
  const isScroll = (cb & 64) !== 0;
2063
+ const shift = (cb & 4) !== 0;
2064
+ const alt = (cb & 8) !== 0;
2065
+ const ctrl = (cb & 16) !== 0;
1198
2066
  if (isScroll) {
1199
2067
  button = "none";
1200
2068
  type = "scroll";
1201
- scrollDelta = buttonBits === 0 ? -1 : 1;
2069
+ const lowBits = cb & 7;
2070
+ if (lowBits === 6) {
2071
+ scrollAxis = "horizontal";
2072
+ scrollDeltaX = -1;
2073
+ } else if (lowBits === 7) {
2074
+ scrollAxis = "horizontal";
2075
+ scrollDeltaX = 1;
2076
+ } else {
2077
+ scrollAxis = "vertical";
2078
+ scrollDelta = buttonBits === 0 ? -1 : 1;
2079
+ }
1202
2080
  } else if (motion) {
1203
2081
  type = "mousemove";
1204
2082
  button = decodeButton(buttonBits);
@@ -1214,7 +2092,12 @@ function parseMouseEvent(data) {
1214
2092
  y: cy,
1215
2093
  button,
1216
2094
  type,
1217
- scrollDelta
2095
+ ...scrollDelta !== void 0 && { scrollDelta },
2096
+ ...scrollDeltaX !== void 0 && { scrollDeltaX },
2097
+ ...scrollAxis !== void 0 && { scrollAxis },
2098
+ shift,
2099
+ alt,
2100
+ ctrl
1218
2101
  };
1219
2102
  }
1220
2103
  function decodeButton(bits) {
@@ -1236,7 +2119,9 @@ function isMouseSequence(data) {
1236
2119
  // src/events/EventEmitter.ts
1237
2120
  var EventEmitter = class {
1238
2121
  _handlers = /* @__PURE__ */ new Map();
2122
+ // any: handler type erased here; callers constrain via generics
1239
2123
  _onceHandlers = /* @__PURE__ */ new Map();
2124
+ // any: handler type erased here; callers constrain via generics
1240
2125
  /**
1241
2126
  * Subscribe to an event.
1242
2127
  * @returns Unsubscribe function.
@@ -1276,7 +2161,7 @@ var EventEmitter = class {
1276
2161
  for (const handler of handlers) {
1277
2162
  try {
1278
2163
  handler(data);
1279
- } catch {
2164
+ } catch (_err) {
1280
2165
  }
1281
2166
  }
1282
2167
  }
@@ -1285,7 +2170,7 @@ var EventEmitter = class {
1285
2170
  for (const handler of onceHandlers) {
1286
2171
  try {
1287
2172
  handler(data);
1288
- } catch {
2173
+ } catch (_err) {
1289
2174
  }
1290
2175
  }
1291
2176
  onceHandlers.clear();
@@ -1317,7 +2202,10 @@ var InputParser = class {
1317
2202
  _stdin;
1318
2203
  _handler = null;
1319
2204
  _escapeTimeout = null;
1320
- _escapeBuffer = "";
2205
+ _escapeBuffer = Buffer2.alloc(0);
2206
+ _isPasting = false;
2207
+ _pasteBuffer = "";
2208
+ _cursorRequests = [];
1321
2209
  constructor(stdin) {
1322
2210
  this._stdin = stdin;
1323
2211
  }
@@ -1329,6 +2217,25 @@ var InputParser = class {
1329
2217
  onMouse(handler) {
1330
2218
  return this._events.on("mouse", handler);
1331
2219
  }
2220
+ /** Subscribe to terminal focus-in (true) / focus-out (false) reports. */
2221
+ onFocusChange(handler) {
2222
+ return this._events.on("focuschange", handler);
2223
+ }
2224
+ onPaste(handler) {
2225
+ return this._events.on("paste", handler);
2226
+ }
2227
+ requestCursorPosition(timeoutMs = 200) {
2228
+ return new Promise((resolve, reject) => {
2229
+ const timeout = setTimeout(() => {
2230
+ const idx = this._cursorRequests.findIndex((item) => item.reject === reject);
2231
+ if (idx !== -1) {
2232
+ this._cursorRequests.splice(idx, 1);
2233
+ }
2234
+ reject(new Error("Cursor position request timed out"));
2235
+ }, timeoutMs);
2236
+ this._cursorRequests.push({ resolve, reject, timeout });
2237
+ });
2238
+ }
1332
2239
  /** Start listening for input */
1333
2240
  start() {
1334
2241
  if (this._handler) return;
@@ -1347,46 +2254,57 @@ var InputParser = class {
1347
2254
  clearTimeout(this._escapeTimeout);
1348
2255
  this._escapeTimeout = null;
1349
2256
  }
1350
- this._escapeBuffer = "";
2257
+ this._escapeBuffer = Buffer2.alloc(0);
2258
+ for (const req of this._cursorRequests) {
2259
+ clearTimeout(req.timeout);
2260
+ req.reject(new Error("InputParser stopped"));
2261
+ }
2262
+ this._cursorRequests = [];
1351
2263
  }
1352
2264
  /**
1353
2265
  * Process a chunk of raw input bytes.
1354
2266
  */
1355
2267
  _processInput(data) {
1356
2268
  const str = data.toString("utf8");
1357
- if (this._escapeBuffer) {
1358
- this._escapeBuffer += str;
2269
+ const PASTE_START = "\x1B[200~";
2270
+ const PASTE_END = "\x1B[201~";
2271
+ if (str.includes(PASTE_START) && str.includes(PASTE_END)) {
2272
+ const pastedText = str.replace(PASTE_START, "").replace(PASTE_END, "");
2273
+ this._events.emit("paste", pastedText);
2274
+ return;
2275
+ }
2276
+ if (this._escapeBuffer.length > 0) {
2277
+ this._escapeBuffer = Buffer2.concat([this._escapeBuffer, data]);
1359
2278
  if (this._escapeTimeout) {
1360
2279
  clearTimeout(this._escapeTimeout);
1361
2280
  this._escapeTimeout = null;
1362
2281
  }
1363
- this._tryParseEscape(data);
2282
+ this._tryParseEscape();
1364
2283
  return;
1365
2284
  }
1366
2285
  if (str.startsWith("\x1B") && str.length === 1) {
1367
- this._escapeBuffer = str;
2286
+ this._escapeBuffer = data;
1368
2287
  this._escapeTimeout = setTimeout(() => {
1369
2288
  this._events.emit("key", createKeyEvent({
1370
2289
  key: "escape",
1371
- raw: Buffer.from(this._escapeBuffer),
2290
+ raw: this._escapeBuffer,
1372
2291
  ctrl: false,
1373
2292
  alt: false,
1374
2293
  shift: false
1375
2294
  }));
1376
- this._escapeBuffer = "";
2295
+ this._escapeBuffer = Buffer2.alloc(0);
1377
2296
  this._escapeTimeout = null;
1378
2297
  }, 50);
1379
2298
  return;
1380
2299
  }
1381
2300
  if (str.startsWith("\x1B")) {
1382
- this._escapeBuffer = str;
1383
- this._tryParseEscape(data);
2301
+ this._escapeBuffer = data;
2302
+ this._tryParseEscape();
1384
2303
  return;
1385
2304
  }
1386
- for (let i = 0; i < str.length; i++) {
1387
- const ch = str[i];
1388
- const code = str.charCodeAt(i);
1389
- const raw = Buffer.from(ch, "utf8");
2305
+ for (const ch of str) {
2306
+ const code = ch.codePointAt(0);
2307
+ const raw = Buffer2.from(ch, "utf8");
1390
2308
  if (code >= 1 && code <= 26) {
1391
2309
  const keyName = CTRL_KEYS[code];
1392
2310
  const isCtrl = code !== 9 && code !== 13 && code !== 10;
@@ -1423,13 +2341,13 @@ var InputParser = class {
1423
2341
  /**
1424
2342
  * Try to parse buffered escape sequence.
1425
2343
  */
1426
- _tryParseEscape(rawData) {
1427
- const seq = this._escapeBuffer;
2344
+ _tryParseEscape() {
2345
+ const seq = this._escapeBuffer.toString("utf8");
1428
2346
  if (isMouseSequence(seq)) {
1429
2347
  const mouseEvt = parseMouseEvent(seq);
1430
2348
  if (mouseEvt) {
1431
2349
  this._events.emit("mouse", mouseEvt);
1432
- this._escapeBuffer = "";
2350
+ this._escapeBuffer = Buffer2.alloc(0);
1433
2351
  return;
1434
2352
  }
1435
2353
  if (seq.length < 20) {
@@ -1438,12 +2356,35 @@ var InputParser = class {
1438
2356
  this._escapeTimeout = null;
1439
2357
  }
1440
2358
  this._escapeTimeout = setTimeout(() => {
1441
- this._escapeBuffer = "";
2359
+ this._escapeBuffer = Buffer2.alloc(0);
1442
2360
  this._escapeTimeout = null;
1443
2361
  }, 100);
1444
2362
  return;
1445
2363
  }
1446
2364
  }
2365
+ const cursorMatch = seq.match(/^\x1b\[(\d+);(\d+)R$/);
2366
+ if (cursorMatch) {
2367
+ const row = parseInt(cursorMatch[1], 10);
2368
+ const col = parseInt(cursorMatch[2], 10);
2369
+ const position = { row, col };
2370
+ for (const request of this._cursorRequests) {
2371
+ clearTimeout(request.timeout);
2372
+ request.resolve(position);
2373
+ }
2374
+ this._cursorRequests = [];
2375
+ this._escapeBuffer = Buffer2.alloc(0);
2376
+ return;
2377
+ }
2378
+ if (seq === "\x1B[I") {
2379
+ this._events.emit("focuschange", true);
2380
+ this._escapeBuffer = Buffer2.alloc(0);
2381
+ return;
2382
+ }
2383
+ if (seq === "\x1B[O") {
2384
+ this._events.emit("focuschange", false);
2385
+ this._escapeBuffer = Buffer2.alloc(0);
2386
+ return;
2387
+ }
1447
2388
  if (seq in ESCAPE_SEQUENCES) {
1448
2389
  const keyName = ESCAPE_SEQUENCES[seq];
1449
2390
  const isShift = keyName.startsWith("shift+");
@@ -1452,28 +2393,28 @@ var InputParser = class {
1452
2393
  const cleanKey = keyName.replace(/^(shift|ctrl|alt)\+/, "");
1453
2394
  this._events.emit("key", createKeyEvent({
1454
2395
  key: cleanKey,
1455
- raw: rawData,
2396
+ raw: this._escapeBuffer,
1456
2397
  ctrl: isCtrl,
1457
2398
  alt: isAlt,
1458
2399
  shift: isShift
1459
2400
  }));
1460
- this._escapeBuffer = "";
2401
+ this._escapeBuffer = Buffer2.alloc(0);
1461
2402
  return;
1462
2403
  }
1463
- if (seq.length === 2 && seq[0] === "\x1B") {
2404
+ if (seq.length === 2 && seq[0] === "\x1B" && seq[1] !== "[" && seq[1] !== "O") {
1464
2405
  const ch = seq[1];
1465
2406
  this._events.emit("key", createKeyEvent({
1466
2407
  key: ch,
1467
- raw: rawData,
2408
+ raw: this._escapeBuffer,
1468
2409
  ctrl: false,
1469
2410
  alt: true,
1470
2411
  shift: ch !== ch.toLowerCase() && ch === ch.toUpperCase()
1471
2412
  }));
1472
- this._escapeBuffer = "";
2413
+ this._escapeBuffer = Buffer2.alloc(0);
1473
2414
  return;
1474
2415
  }
1475
2416
  if (seq.length > 20) {
1476
- this._escapeBuffer = "";
2417
+ this._escapeBuffer = Buffer2.alloc(0);
1477
2418
  return;
1478
2419
  }
1479
2420
  if (this._escapeTimeout) {
@@ -1481,12 +2422,184 @@ var InputParser = class {
1481
2422
  this._escapeTimeout = null;
1482
2423
  }
1483
2424
  this._escapeTimeout = setTimeout(() => {
1484
- this._escapeBuffer = "";
2425
+ this._escapeBuffer = Buffer2.alloc(0);
1485
2426
  this._escapeTimeout = null;
1486
2427
  }, 100);
1487
2428
  }
1488
2429
  };
1489
2430
 
2431
+ // src/input/MouseGestures.ts
2432
+ var MouseGestures = class {
2433
+ doubleClickMs;
2434
+ lastMouseDown = null;
2435
+ activeDragButton = null;
2436
+ wasDragging = false;
2437
+ constructor(opts) {
2438
+ this.doubleClickMs = opts?.doubleClickMs ?? 300;
2439
+ }
2440
+ /**
2441
+ * Feed a raw MouseEvent. Returns synthesized events to emit
2442
+ * (may be empty). Does not mutate the input event.
2443
+ */
2444
+ feed(event) {
2445
+ const synthesized = [];
2446
+ if (event.type === "mousedown") {
2447
+ const now = Date.now();
2448
+ if (this.lastMouseDown && this.lastMouseDown.x === event.x && this.lastMouseDown.y === event.y && this.lastMouseDown.button === event.button && now - this.lastMouseDown.time <= this.doubleClickMs) {
2449
+ synthesized.push({
2450
+ x: event.x,
2451
+ y: event.y,
2452
+ button: event.button,
2453
+ type: "dblclick"
2454
+ });
2455
+ this.lastMouseDown = null;
2456
+ } else {
2457
+ this.lastMouseDown = {
2458
+ x: event.x,
2459
+ y: event.y,
2460
+ button: event.button,
2461
+ time: now
2462
+ };
2463
+ }
2464
+ this.activeDragButton = event.button;
2465
+ this.wasDragging = false;
2466
+ } else if (event.type === "mousemove") {
2467
+ if (this.activeDragButton !== null) {
2468
+ this.wasDragging = true;
2469
+ synthesized.push({
2470
+ x: event.x,
2471
+ y: event.y,
2472
+ button: this.activeDragButton,
2473
+ type: "drag"
2474
+ });
2475
+ }
2476
+ } else if (event.type === "mouseup") {
2477
+ if (this.activeDragButton !== null) {
2478
+ if (this.wasDragging) {
2479
+ synthesized.push({
2480
+ x: event.x,
2481
+ y: event.y,
2482
+ button: event.button,
2483
+ type: "dragend"
2484
+ });
2485
+ }
2486
+ this.activeDragButton = null;
2487
+ this.wasDragging = false;
2488
+ }
2489
+ }
2490
+ return synthesized;
2491
+ }
2492
+ };
2493
+
2494
+ // src/input/ChordMatcher.ts
2495
+ function getKeyEventToken(event) {
2496
+ let token = "";
2497
+ if (event.ctrl) token += "ctrl+";
2498
+ if (event.alt) token += "alt+";
2499
+ if (event.shift) token += "shift+";
2500
+ token += event.key.toLowerCase();
2501
+ return token;
2502
+ }
2503
+ var ChordMatcher = class {
2504
+ _bindings = [];
2505
+ _nextId = 0;
2506
+ _buffer = [];
2507
+ _timeoutMs;
2508
+ _timer = null;
2509
+ constructor(opts) {
2510
+ this._timeoutMs = opts?.timeoutMs ?? 800;
2511
+ }
2512
+ bind(keys, handler) {
2513
+ const id = this._nextId++;
2514
+ this._bindings.push({ keys, handler, id });
2515
+ return () => {
2516
+ this._bindings = this._bindings.filter((b) => b.id !== id);
2517
+ };
2518
+ }
2519
+ feed(event) {
2520
+ if (this._timer) {
2521
+ clearTimeout(this._timer);
2522
+ this._timer = null;
2523
+ }
2524
+ const token = getKeyEventToken(event);
2525
+ let candidate = [...this._buffer, token];
2526
+ let matchingBindings = this._getMatchingBindings(candidate);
2527
+ if (matchingBindings.length === 0) {
2528
+ this._buffer = [];
2529
+ candidate = [token];
2530
+ matchingBindings = this._getMatchingBindings(candidate);
2531
+ if (matchingBindings.length === 0) {
2532
+ return false;
2533
+ }
2534
+ }
2535
+ this._buffer = candidate;
2536
+ const completedBindings = matchingBindings.filter(
2537
+ (binding) => binding.keys.length === candidate.length
2538
+ );
2539
+ if (completedBindings.length > 0) {
2540
+ for (const binding of completedBindings) {
2541
+ binding.handler();
2542
+ }
2543
+ this._buffer = [];
2544
+ return true;
2545
+ }
2546
+ this._timer = setTimeout(() => {
2547
+ this._buffer = [];
2548
+ this._timer = null;
2549
+ }, this._timeoutMs);
2550
+ return true;
2551
+ }
2552
+ _getMatchingBindings(candidate) {
2553
+ return this._bindings.filter((binding) => {
2554
+ if (binding.keys.length < candidate.length) return false;
2555
+ for (let i = 0; i < candidate.length; i++) {
2556
+ if (binding.keys[i] !== candidate[i]) return false;
2557
+ }
2558
+ return true;
2559
+ });
2560
+ }
2561
+ };
2562
+
2563
+ // src/renderer/live-render.ts
2564
+ var LiveRender = class {
2565
+ constructor(terminal, screen) {
2566
+ this.terminal = terminal;
2567
+ this.screen = screen;
2568
+ }
2569
+ terminal;
2570
+ screen;
2571
+ getHeight(frame) {
2572
+ if (frame.length === 0) {
2573
+ return 0;
2574
+ }
2575
+ return frame.split("\n").length;
2576
+ }
2577
+ /**
2578
+ * Renders a serialized screen buffer.
2579
+ *
2580
+ * Widgets render into a Screen object first.
2581
+ * Callers should serialize the Screen contents into a string
2582
+ * before passing it to LiveRender.render().
2583
+ */
2584
+ render(frame) {
2585
+ let output = "";
2586
+ const previousHeight = this.screen.lastRenderedHeight;
2587
+ if (previousHeight > 0) {
2588
+ output += moveUp(previousHeight);
2589
+ for (let i = 0; i < previousHeight; i++) {
2590
+ output += clearLine;
2591
+ if (i < previousHeight - 1) {
2592
+ output += "\n";
2593
+ }
2594
+ }
2595
+ output += "\r";
2596
+ }
2597
+ output += frame;
2598
+ this.terminal.write(output);
2599
+ this.screen.lastRenderedHeight = this.getHeight(frame);
2600
+ }
2601
+ };
2602
+
1490
2603
  // src/style/Style.ts
1491
2604
  function normalizeEdges(value) {
1492
2605
  if (value === void 0) return { top: 0, right: 0, bottom: 0, left: 0 };
@@ -1523,6 +2636,32 @@ function defaultStyle() {
1523
2636
  gap: 0
1524
2637
  };
1525
2638
  }
2639
+ var LAYOUT_PROPS = /* @__PURE__ */ new Set([
2640
+ "width",
2641
+ "height",
2642
+ "minWidth",
2643
+ "minHeight",
2644
+ "maxWidth",
2645
+ "maxHeight",
2646
+ "padding",
2647
+ "margin",
2648
+ "border",
2649
+ "flexDirection",
2650
+ "justifyContent",
2651
+ "alignItems",
2652
+ "flexGrow",
2653
+ "flexShrink",
2654
+ "flexWrap",
2655
+ "gap",
2656
+ "overflow",
2657
+ "visible"
2658
+ ]);
2659
+ function hasLayoutChanges(oldStyle, newStyle) {
2660
+ for (const key of LAYOUT_PROPS) {
2661
+ if (oldStyle[key] !== newStyle[key]) return true;
2662
+ }
2663
+ return false;
2664
+ }
1526
2665
  function styleToCellAttrs(style) {
1527
2666
  return {
1528
2667
  fg: style.fg ?? { type: "none" },
@@ -1589,8 +2728,19 @@ var BORDER_CHARS = {
1589
2728
  left: "\u2506"
1590
2729
  }
1591
2730
  };
1592
- function getBorderChars(style, customChars) {
2731
+ var ASCII_BORDER_CHARS = {
2732
+ topLeft: "+",
2733
+ top: "-",
2734
+ topRight: "+",
2735
+ right: "|",
2736
+ bottomRight: "+",
2737
+ bottom: "-",
2738
+ bottomLeft: "+",
2739
+ left: "|"
2740
+ };
2741
+ function getBorderChars(style, customChars, asciiOnly = false) {
1593
2742
  if (style === "none") return null;
2743
+ if (asciiOnly) return ASCII_BORDER_CHARS;
1594
2744
  if (style === "custom") {
1595
2745
  const base = BORDER_CHARS.single;
1596
2746
  return { ...base, ...customChars };
@@ -1602,6 +2752,333 @@ function borderSize(style) {
1602
2752
  return { horizontal: 2, vertical: 2 };
1603
2753
  }
1604
2754
 
2755
+ // src/layout/pos.ts
2756
+ var Pos = class {
2757
+ /** Center the element within its parent */
2758
+ static center() {
2759
+ return new PosCenter();
2760
+ }
2761
+ /** Anchor the element `margin` units away from the end (right/bottom) */
2762
+ static anchorEnd(margin = 0) {
2763
+ return new PosAnchorEnd(margin);
2764
+ }
2765
+ /** Align multiple siblings as a group */
2766
+ static align(alignment, groupId) {
2767
+ return new PosAlign(alignment, groupId);
2768
+ }
2769
+ };
2770
+ var PosCenter = class extends Pos {
2771
+ dependencies() {
2772
+ return ["parentSize", "elementSize"];
2773
+ }
2774
+ evaluate(ctx) {
2775
+ const pSize = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
2776
+ const eSize = ctx.axis === "horizontal" ? ctx.elementWidth : ctx.elementHeight;
2777
+ return Math.floor((pSize - eSize) / 2);
2778
+ }
2779
+ };
2780
+ var PosAnchorEnd = class extends Pos {
2781
+ constructor(margin) {
2782
+ super();
2783
+ this.margin = margin;
2784
+ }
2785
+ margin;
2786
+ dependencies() {
2787
+ return ["parentSize", "elementSize"];
2788
+ }
2789
+ evaluate(ctx) {
2790
+ const pSize = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
2791
+ const eSize = ctx.axis === "horizontal" ? ctx.elementWidth : ctx.elementHeight;
2792
+ return pSize - eSize - this.margin;
2793
+ }
2794
+ };
2795
+ var PosAlign = class extends Pos {
2796
+ constructor(alignment, groupId) {
2797
+ super();
2798
+ this.alignment = alignment;
2799
+ this.groupId = groupId;
2800
+ }
2801
+ alignment;
2802
+ groupId;
2803
+ dependencies() {
2804
+ return ["group:" + this.groupId, "elementSize"];
2805
+ }
2806
+ evaluate(ctx) {
2807
+ const groupSize = ctx.getGroupSize(this.groupId);
2808
+ if (groupSize === 0) return 0;
2809
+ const pSize = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
2810
+ const eSize = ctx.axis === "horizontal" ? ctx.elementWidth : ctx.elementHeight;
2811
+ let groupOffset = 0;
2812
+ if (this.alignment === "center") {
2813
+ groupOffset = Math.floor((pSize - groupSize) / 2);
2814
+ } else if (this.alignment === "end") {
2815
+ groupOffset = pSize - groupSize;
2816
+ }
2817
+ const localOffset = Math.floor((groupSize - eSize) / 2);
2818
+ return groupOffset + localOffset;
2819
+ }
2820
+ };
2821
+
2822
+ // src/layout/dim.ts
2823
+ var Dim = class {
2824
+ /** Size to the intrinsic content of the element */
2825
+ static auto() {
2826
+ return new DimAuto();
2827
+ }
2828
+ /** Fill remaining space in the parent, minus an optional margin */
2829
+ static fill(margin = 0) {
2830
+ return new DimFill(margin);
2831
+ }
2832
+ /** Custom function to determine size */
2833
+ static func(fn) {
2834
+ return new DimFunc(fn);
2835
+ }
2836
+ };
2837
+ var DimAuto = class extends Dim {
2838
+ dependencies() {
2839
+ return ["contentSize"];
2840
+ }
2841
+ evaluate(ctx) {
2842
+ return ctx.axis === "horizontal" ? ctx.contentWidth : ctx.contentHeight;
2843
+ }
2844
+ };
2845
+ var DimFill = class extends Dim {
2846
+ constructor(margin) {
2847
+ super();
2848
+ this.margin = margin;
2849
+ }
2850
+ margin;
2851
+ dependencies() {
2852
+ return ["parentSize"];
2853
+ }
2854
+ evaluate(ctx) {
2855
+ const avail = ctx.axis === "horizontal" ? ctx.parentWidth : ctx.parentHeight;
2856
+ return Math.max(0, avail - this.margin);
2857
+ }
2858
+ };
2859
+ var DimFunc = class extends Dim {
2860
+ constructor(fn) {
2861
+ super();
2862
+ this.fn = fn;
2863
+ }
2864
+ fn;
2865
+ dependencies() {
2866
+ return [];
2867
+ }
2868
+ evaluate(ctx) {
2869
+ return this.fn(ctx);
2870
+ }
2871
+ };
2872
+
2873
+ // src/layout/constraint.ts
2874
+ var Flex = /* @__PURE__ */ ((Flex2) => {
2875
+ Flex2["Start"] = "start";
2876
+ Flex2["Center"] = "center";
2877
+ Flex2["End"] = "end";
2878
+ Flex2["SpaceBetween"] = "space-between";
2879
+ Flex2["SpaceAround"] = "space-around";
2880
+ return Flex2;
2881
+ })(Flex || {});
2882
+ var Constraint = class {
2883
+ /** Fixed length in columns/rows */
2884
+ static Length(n) {
2885
+ return new LengthConstraint(n);
2886
+ }
2887
+ /** Percentage of available space (0-100) */
2888
+ static Percentage(n) {
2889
+ return new PercentageConstraint(n);
2890
+ }
2891
+ /** Minimum length */
2892
+ static Min(n) {
2893
+ return new MinConstraint(n);
2894
+ }
2895
+ /** Maximum length */
2896
+ static Max(n) {
2897
+ return new MaxConstraint(n);
2898
+ }
2899
+ /** Fills remaining space, with a flex weight */
2900
+ static Fill(weight = 1) {
2901
+ return new FillConstraint(weight);
2902
+ }
2903
+ };
2904
+ var LengthConstraint = class extends Constraint {
2905
+ constructor(value) {
2906
+ super();
2907
+ this.value = value;
2908
+ }
2909
+ value;
2910
+ };
2911
+ var PercentageConstraint = class extends Constraint {
2912
+ constructor(value) {
2913
+ super();
2914
+ this.value = value;
2915
+ }
2916
+ value;
2917
+ };
2918
+ var MinConstraint = class extends Constraint {
2919
+ constructor(value) {
2920
+ super();
2921
+ this.value = value;
2922
+ }
2923
+ value;
2924
+ };
2925
+ var MaxConstraint = class extends Constraint {
2926
+ constructor(value) {
2927
+ super();
2928
+ this.value = value;
2929
+ }
2930
+ value;
2931
+ };
2932
+ var FillConstraint = class extends Constraint {
2933
+ constructor(weight) {
2934
+ super();
2935
+ this.weight = weight;
2936
+ }
2937
+ weight;
2938
+ };
2939
+ function resolveConstraints(available, constraints, flex = "start" /* Start */, gap = 0) {
2940
+ const n = constraints.length;
2941
+ if (n === 0) return [];
2942
+ const sizes = new Array(n).fill(0);
2943
+ const minSizes = new Array(n).fill(0);
2944
+ const maxSizes = new Array(n).fill(Infinity);
2945
+ let totalFixed = 0;
2946
+ let fillWeightSum = 0;
2947
+ for (let i = 0; i < n; i++) {
2948
+ const c = constraints[i];
2949
+ if (c instanceof LengthConstraint) {
2950
+ sizes[i] = c.value;
2951
+ totalFixed += c.value;
2952
+ } else if (c instanceof PercentageConstraint) {
2953
+ sizes[i] = Math.floor(available * c.value / 100);
2954
+ totalFixed += sizes[i];
2955
+ } else if (c instanceof MinConstraint) {
2956
+ minSizes[i] = c.value;
2957
+ } else if (c instanceof MaxConstraint) {
2958
+ maxSizes[i] = c.value;
2959
+ } else if (c instanceof FillConstraint) {
2960
+ fillWeightSum += c.weight;
2961
+ }
2962
+ }
2963
+ for (let i = 0; i < n; i++) {
2964
+ if (constraints[i] instanceof MinConstraint) {
2965
+ sizes[i] = minSizes[i];
2966
+ totalFixed += sizes[i];
2967
+ }
2968
+ }
2969
+ const totalGaps = gap * (n - 1);
2970
+ let remaining = Math.max(0, available - totalFixed - totalGaps);
2971
+ if (fillWeightSum > 0) {
2972
+ let distributed = 0;
2973
+ for (let i = 0; i < n; i++) {
2974
+ const c = constraints[i];
2975
+ if (c instanceof FillConstraint) {
2976
+ const share = Math.floor(remaining * c.weight / fillWeightSum);
2977
+ sizes[i] = share;
2978
+ distributed += share;
2979
+ }
2980
+ }
2981
+ let leftover = remaining - distributed;
2982
+ if (leftover > 0) {
2983
+ for (let i = n - 1; i >= 0; i--) {
2984
+ if (constraints[i] instanceof FillConstraint) {
2985
+ sizes[i] += leftover;
2986
+ break;
2987
+ }
2988
+ }
2989
+ }
2990
+ }
2991
+ const totalUsed = sizes.reduce((a, b) => a + b, 0) + totalGaps;
2992
+ const freeSpace = Math.max(0, available - totalUsed);
2993
+ const results = [];
2994
+ let offset = 0;
2995
+ let spaceBetween = 0;
2996
+ switch (flex) {
2997
+ case "start" /* Start */:
2998
+ break;
2999
+ case "end" /* End */:
3000
+ offset = freeSpace;
3001
+ break;
3002
+ case "center" /* Center */:
3003
+ offset = freeSpace / 2;
3004
+ break;
3005
+ case "space-between" /* SpaceBetween */:
3006
+ if (n > 1) spaceBetween = freeSpace / (n - 1);
3007
+ break;
3008
+ case "space-around" /* SpaceAround */:
3009
+ if (n > 0) {
3010
+ spaceBetween = freeSpace / n;
3011
+ offset = spaceBetween / 2;
3012
+ }
3013
+ break;
3014
+ }
3015
+ for (let i = 0; i < n; i++) {
3016
+ results.push({ offset: Math.floor(offset), size: sizes[i] });
3017
+ offset += sizes[i] + gap + spaceBetween;
3018
+ }
3019
+ return results;
3020
+ }
3021
+ function resolveLayoutVariables(nodes, parentWidth, parentHeight) {
3022
+ const state = /* @__PURE__ */ new Map();
3023
+ function evaluateVariable(node, varName) {
3024
+ const key = `${node.id}:${varName}`;
3025
+ const existing = state.get(key);
3026
+ if (existing === "computing") {
3027
+ throw new Error(`Cycle detected resolving ${key}`);
3028
+ }
3029
+ if (typeof existing === "number") {
3030
+ return existing;
3031
+ }
3032
+ state.set(key, "computing");
3033
+ let result = 0;
3034
+ const val = node[varName];
3035
+ const ctx = {
3036
+ parentWidth,
3037
+ parentHeight,
3038
+ axis: varName === "x" || varName === "width" ? "horizontal" : "vertical",
3039
+ contentWidth: node.contentWidth,
3040
+ contentHeight: node.contentHeight,
3041
+ get elementWidth() {
3042
+ return evaluateVariable(node, "width");
3043
+ },
3044
+ get elementHeight() {
3045
+ return evaluateVariable(node, "height");
3046
+ },
3047
+ get elementX() {
3048
+ return evaluateVariable(node, "x");
3049
+ },
3050
+ get elementY() {
3051
+ return evaluateVariable(node, "y");
3052
+ },
3053
+ getGroupSize(groupId) {
3054
+ const groupNodes = nodes.filter((n) => n.groupId === groupId);
3055
+ let maxSize = 0;
3056
+ for (const gNode of groupNodes) {
3057
+ const size = evaluateVariable(gNode, this.axis === "horizontal" ? "width" : "height");
3058
+ maxSize = Math.max(maxSize, size);
3059
+ }
3060
+ return maxSize;
3061
+ }
3062
+ };
3063
+ if (val instanceof Pos) {
3064
+ result = val.evaluate(ctx);
3065
+ } else if (val instanceof Dim) {
3066
+ result = val.evaluate(ctx);
3067
+ } else if (typeof val === "number") {
3068
+ result = val;
3069
+ }
3070
+ state.set(key, result);
3071
+ node.computed[varName] = result;
3072
+ return result;
3073
+ }
3074
+ for (const node of nodes) {
3075
+ evaluateVariable(node, "width");
3076
+ evaluateVariable(node, "height");
3077
+ evaluateVariable(node, "x");
3078
+ evaluateVariable(node, "y");
3079
+ }
3080
+ }
3081
+
1605
3082
  // src/layout/LayoutEngine.ts
1606
3083
  function createLayoutNode(id, style, children = []) {
1607
3084
  return {
@@ -1609,14 +3086,44 @@ function createLayoutNode(id, style, children = []) {
1609
3086
  style,
1610
3087
  children,
1611
3088
  computed: { x: 0, y: 0, width: 0, height: 0 },
1612
- _dirty: true
3089
+ _dirty: true,
3090
+ _lastContainerWidth: 0,
3091
+ _lastContainerHeight: 0,
3092
+ _lastComputedWidth: 0,
3093
+ _lastComputedHeight: 0,
3094
+ _draggable: false,
3095
+ _dragging: false
1613
3096
  };
1614
3097
  }
1615
3098
  function computeLayout(root, containerWidth, containerHeight) {
3099
+ const sizeChanged = root._lastContainerWidth !== containerWidth || root._lastContainerHeight !== containerHeight;
3100
+ if (!sizeChanged && !root._dirty && !hasDirtyChild(root)) {
3101
+ return;
3102
+ }
3103
+ root._lastContainerWidth = containerWidth;
3104
+ root._lastContainerHeight = containerHeight;
1616
3105
  root.computed = { x: 0, y: 0, width: containerWidth, height: containerHeight };
1617
3106
  layoutNode(root, containerWidth, containerHeight);
3107
+ root.computed.width = containerWidth;
3108
+ root.computed.height = containerHeight;
3109
+ }
3110
+ function invalidateLayout(node) {
3111
+ node._dirty = true;
3112
+ for (const child of node.children) {
3113
+ invalidateLayout(child);
3114
+ }
3115
+ }
3116
+ function hasDirtyChild(node) {
3117
+ if (node._dirty) return true;
3118
+ for (const child of node.children) {
3119
+ if (hasDirtyChild(child)) return true;
3120
+ }
3121
+ return false;
1618
3122
  }
1619
3123
  function layoutNode(node, availWidth, availHeight, precomputed = false) {
3124
+ if (!node._dirty && node._lastComputedWidth === node.computed.width && node._lastComputedHeight === node.computed.height) {
3125
+ return;
3126
+ }
1620
3127
  const style = node.style;
1621
3128
  const padding = normalizeEdges(style.padding);
1622
3129
  const margin = normalizeEdges(style.margin);
@@ -1626,6 +3133,8 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
1626
3133
  let nodeHeight2 = resolveSize(style.height, availHeight);
1627
3134
  if (nodeWidth2 === void 0) nodeWidth2 = availWidth - margin.left - margin.right;
1628
3135
  if (nodeHeight2 === void 0) nodeHeight2 = availHeight - margin.top - margin.bottom;
3136
+ if (!Number.isFinite(nodeWidth2)) nodeWidth2 = 0;
3137
+ if (!Number.isFinite(nodeHeight2)) nodeHeight2 = 0;
1629
3138
  nodeWidth2 = clampSize(nodeWidth2, style.minWidth, style.maxWidth);
1630
3139
  nodeHeight2 = clampSize(nodeHeight2, style.minHeight, style.maxHeight);
1631
3140
  node.computed.width = nodeWidth2;
@@ -1633,23 +3142,105 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
1633
3142
  }
1634
3143
  if (node.children.length === 0) {
1635
3144
  node._dirty = false;
3145
+ node._lastComputedWidth = node.computed.width;
3146
+ node._lastComputedHeight = node.computed.height;
1636
3147
  return;
1637
3148
  }
1638
3149
  const nodeWidth = node.computed.width;
1639
3150
  const nodeHeight = node.computed.height;
1640
- const innerX = padding.left + border.horizontal / 2;
1641
- const innerY = padding.top + border.vertical / 2;
3151
+ const innerX = padding.left + (border.horizontal > 0 ? 1 : 0);
3152
+ const innerY = padding.top + (border.vertical > 0 ? 1 : 0);
1642
3153
  const innerWidth = Math.max(0, nodeWidth - padding.left - padding.right - border.horizontal);
1643
3154
  const innerHeight = Math.max(0, nodeHeight - padding.top - padding.bottom - border.vertical);
1644
3155
  const direction = style.flexDirection ?? "column";
1645
3156
  const isRow = direction === "row";
1646
3157
  const gap = style.gap ?? 0;
3158
+ if (style.constraints && style.constraints.length > 0) {
3159
+ const mainAvail2 = isRow ? innerWidth : innerHeight;
3160
+ let flexJustify = "start" /* Start */;
3161
+ if (style.justifyContent === "space-between") flexJustify = "space-between" /* SpaceBetween */;
3162
+ else if (style.justifyContent === "space-around") flexJustify = "space-around" /* SpaceAround */;
3163
+ else if (style.justifyContent === "center") flexJustify = "center" /* Center */;
3164
+ else if (style.justifyContent === "flex-end") flexJustify = "end" /* End */;
3165
+ const results = resolveConstraints(mainAvail2, style.constraints, flexJustify);
3166
+ let visibleIndex = 0;
3167
+ for (let i = 0; i < node.children.length; i++) {
3168
+ const child = node.children[i];
3169
+ if (visibleIndex >= results.length) break;
3170
+ if (child.style.visible === false) continue;
3171
+ const res = results[visibleIndex];
3172
+ const childMargin = normalizeEdges(child.style.margin);
3173
+ if (isRow) {
3174
+ child.computed = {
3175
+ x: Math.floor(node.computed.x + innerX + res.offset + childMargin.left),
3176
+ y: Math.floor(node.computed.y + innerY + childMargin.top),
3177
+ width: Math.round(Math.max(0, res.size - childMargin.left - childMargin.right)),
3178
+ height: Math.round(Math.max(0, innerHeight - childMargin.top - childMargin.bottom))
3179
+ };
3180
+ } else {
3181
+ child.computed = {
3182
+ x: Math.floor(node.computed.x + innerX + childMargin.left),
3183
+ y: Math.floor(node.computed.y + innerY + res.offset + childMargin.top),
3184
+ width: Math.round(Math.max(0, innerWidth - childMargin.left - childMargin.right)),
3185
+ height: Math.round(Math.max(0, res.size - childMargin.top - childMargin.bottom))
3186
+ };
3187
+ }
3188
+ layoutNode(child, child.computed.width, child.computed.height, true);
3189
+ visibleIndex++;
3190
+ }
3191
+ node._dirty = false;
3192
+ node._lastComputedWidth = node.computed.width;
3193
+ node._lastComputedHeight = node.computed.height;
3194
+ return;
3195
+ }
3196
+ const topologicalChildren = [];
3197
+ const flexChildren = [];
3198
+ for (const child of node.children) {
3199
+ if (child.style.visible === false) continue;
3200
+ const s = child.style;
3201
+ if (s.x instanceof Pos || s.y instanceof Pos || s.width instanceof Dim || s.height instanceof Dim || s.groupId != null) {
3202
+ topologicalChildren.push(child);
3203
+ } else {
3204
+ flexChildren.push(child);
3205
+ }
3206
+ }
3207
+ if (topologicalChildren.length > 0) {
3208
+ const resolvableNodes = topologicalChildren.map((child) => {
3209
+ const s = child.style;
3210
+ let cw = 0, ch = 0;
3211
+ if (typeof s.width === "number") cw = s.width;
3212
+ if (typeof s.height === "number") ch = s.height;
3213
+ return {
3214
+ id: child.id,
3215
+ x: s.x,
3216
+ y: s.y,
3217
+ width: typeof s.width === "string" ? void 0 : s.width,
3218
+ height: typeof s.height === "string" ? void 0 : s.height,
3219
+ contentWidth: cw,
3220
+ contentHeight: ch,
3221
+ groupId: s.groupId,
3222
+ computed: { x: 0, y: 0, width: 0, height: 0 },
3223
+ _originalNode: child
3224
+ // keep reference
3225
+ };
3226
+ });
3227
+ resolveLayoutVariables(resolvableNodes, innerWidth, innerHeight);
3228
+ for (const rNode of resolvableNodes) {
3229
+ const child = rNode._originalNode;
3230
+ child.computed = {
3231
+ x: Math.floor(node.computed.x + innerX + rNode.computed.x),
3232
+ y: Math.floor(node.computed.y + innerY + rNode.computed.y),
3233
+ width: Math.round(Math.max(0, rNode.computed.width)),
3234
+ height: Math.round(Math.max(0, rNode.computed.height))
3235
+ };
3236
+ layoutNode(child, child.computed.width, child.computed.height, true);
3237
+ }
3238
+ }
1647
3239
  const childInfos = [];
1648
3240
  let totalFixed = 0;
1649
3241
  let totalGrow = 0;
1650
3242
  let totalShrink = 0;
1651
- for (const child of node.children) {
1652
- if (child.style.visible === false) continue;
3243
+ for (const child of flexChildren) {
1653
3244
  const childMargin = normalizeEdges(child.style.margin);
1654
3245
  const childBorder = borderSize(child.style.border ?? "none");
1655
3246
  const grow = child.style.flexGrow ?? 0;
@@ -1756,20 +3347,26 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
1756
3347
  layoutNode(info.node, info.node.computed.width, info.node.computed.height, true);
1757
3348
  }
1758
3349
  node._dirty = false;
3350
+ node._lastComputedWidth = node.computed.width;
3351
+ node._lastComputedHeight = node.computed.height;
1759
3352
  }
1760
3353
  function resolveSize(value, available) {
1761
3354
  if (value === void 0) return void 0;
1762
- if (typeof value === "number") return value;
3355
+ if (typeof value === "number") {
3356
+ if (!Number.isFinite(value) || value < 0) return 0;
3357
+ return value;
3358
+ }
1763
3359
  if (typeof value === "string" && value.endsWith("%")) {
1764
3360
  const pct = parseFloat(value) / 100;
3361
+ if (!Number.isFinite(pct)) return 0;
1765
3362
  return Math.floor(available * pct);
1766
3363
  }
1767
3364
  return void 0;
1768
3365
  }
1769
- function clampSize(value, min2, max2) {
3366
+ function clampSize(value, min, max) {
1770
3367
  let result = value;
1771
- if (min2 !== void 0) result = Math.max(result, min2);
1772
- if (max2 !== void 0) result = Math.min(result, max2);
3368
+ if (min !== void 0) result = Math.max(result, min);
3369
+ if (max !== void 0) result = Math.min(result, max);
1773
3370
  return result;
1774
3371
  }
1775
3372
 
@@ -1804,94 +3401,6 @@ function unionRect(a, b) {
1804
3401
  return { x, y, width: r - x, height: bot - y };
1805
3402
  }
1806
3403
 
1807
- // src/layout/ConstraintLayout.ts
1808
- var length = (n) => ({ type: "length", value: n });
1809
- var percentage = (n) => ({ type: "percentage", value: n });
1810
- var ratio = (num, den) => ({ type: "ratio", num, den });
1811
- var min = (n) => ({ type: "min", value: n });
1812
- var max = (n) => ({ type: "max", value: n });
1813
- var fill = (weight = 1) => ({ type: "fill", weight });
1814
- function resolveSize2(constraint, available) {
1815
- switch (constraint.type) {
1816
- case "length":
1817
- return Math.min(constraint.value, available);
1818
- case "percentage":
1819
- return Math.min(Math.floor(available * constraint.value / 100), available);
1820
- case "ratio":
1821
- return constraint.den === 0 ? 0 : Math.min(
1822
- Math.floor(available * constraint.num / constraint.den),
1823
- available
1824
- );
1825
- case "min":
1826
- return constraint.value;
1827
- case "max":
1828
- return Math.min(constraint.value, available);
1829
- case "fill":
1830
- return 0;
1831
- }
1832
- }
1833
- function splitRect(rect, constraints, direction = "vertical", gap = 0) {
1834
- if (constraints.length === 0) return [];
1835
- const totalAvailable = direction === "horizontal" ? rect.width : rect.height;
1836
- const count = constraints.length;
1837
- const totalGaps = count > 1 ? gap * (count - 1) : 0;
1838
- const availableForConstraints = Math.max(0, totalAvailable - totalGaps);
1839
- const sizes = [];
1840
- let usedSpace = 0;
1841
- let fillWeightSum = 0;
1842
- for (const constraint of constraints) {
1843
- if (constraint.type === "fill") {
1844
- sizes.push(0);
1845
- fillWeightSum += Math.max(1, constraint.weight);
1846
- } else {
1847
- const size = resolveSize2(constraint, availableForConstraints);
1848
- sizes.push(size);
1849
- usedSpace += size;
1850
- }
1851
- }
1852
- if (fillWeightSum > 0) {
1853
- const remaining = Math.max(0, availableForConstraints - usedSpace);
1854
- let distributed = 0;
1855
- for (let i = 0; i < count; i++) {
1856
- const constraint = constraints[i];
1857
- if (!constraint || constraint.type !== "fill") continue;
1858
- const weight = Math.max(1, constraint.weight);
1859
- const share = Math.floor(remaining * weight / fillWeightSum);
1860
- sizes[i] = share;
1861
- distributed += share;
1862
- }
1863
- const leftover = remaining - distributed;
1864
- if (leftover > 0) {
1865
- for (let i = count - 1; i >= 0; i--) {
1866
- const constraint = constraints[i];
1867
- if (constraint && constraint.type === "fill") {
1868
- sizes[i] = (sizes[i] ?? 0) + leftover;
1869
- break;
1870
- }
1871
- }
1872
- }
1873
- }
1874
- let totalUsed = 0;
1875
- for (let i = 0; i < count; i++) {
1876
- const size = sizes[i] ?? 0;
1877
- const clamped = Math.max(0, Math.min(size, availableForConstraints - totalUsed));
1878
- sizes[i] = clamped;
1879
- totalUsed += clamped;
1880
- }
1881
- const results = [];
1882
- let offset = 0;
1883
- for (let i = 0; i < count; i++) {
1884
- const size = sizes[i] ?? 0;
1885
- if (direction === "horizontal") {
1886
- results.push({ x: rect.x + offset, y: rect.y, width: size, height: rect.height });
1887
- } else {
1888
- results.push({ x: rect.x, y: rect.y + offset, width: rect.width, height: size });
1889
- }
1890
- offset += size + gap;
1891
- }
1892
- return results;
1893
- }
1894
-
1895
3404
  // src/events/FocusManager.ts
1896
3405
  var FocusManager = class {
1897
3406
  _focusables = [];
@@ -1912,6 +3421,16 @@ var FocusManager = class {
1912
3421
  * Maps groupId → ordered list of widget IDs.
1913
3422
  */
1914
3423
  _groups = /* @__PURE__ */ new Map();
3424
+ /**
3425
+ * Record of on-screen rects for widgets, used for spatial navigation.
3426
+ */
3427
+ _rects = /* @__PURE__ */ new Map();
3428
+ /** Monotonically increasing epoch for ordered event sequencing */
3429
+ _epoch = 0;
3430
+ /** Queue of focus state changes accumulated before start() is called */
3431
+ _pendingQueue = [];
3432
+ /** True once start() has been called — enables event emission */
3433
+ _started = false;
1915
3434
  /** Currently focused widget ID, or null if none */
1916
3435
  get currentId() {
1917
3436
  if (this._currentIndex < 0 || this._currentIndex >= this._focusables.length) {
@@ -1923,16 +3442,35 @@ var FocusManager = class {
1923
3442
  on(event, handler) {
1924
3443
  return this._events.on(event, handler);
1925
3444
  }
3445
+ /**
3446
+ * Enable event emission and replay any queued focus events.
3447
+ * Call this from App.mount() after _subscribeFocusEvents().
3448
+ */
3449
+ start() {
3450
+ if (this._started) return;
3451
+ this._started = true;
3452
+ for (const evt of this._pendingQueue) {
3453
+ this._events.emit(evt.type, evt);
3454
+ }
3455
+ this._pendingQueue = [];
3456
+ }
1926
3457
  /**
1927
3458
  * Register a focusable widget.
1928
3459
  * Widgets are ordered by tabIndex (ascending), then insertion order.
3460
+ * Before start() is called, events are queued rather than emitted so
3461
+ * they are not lost when App has not yet subscribed to them.
1929
3462
  */
1930
3463
  register(focusable) {
1931
3464
  this._focusables.push(focusable);
1932
3465
  this._focusables.sort((a, b) => a.tabIndex - b.tabIndex);
1933
3466
  if (this._currentIndex < 0 && focusable.focusable) {
1934
3467
  this._currentIndex = this._focusables.indexOf(focusable);
1935
- this._events.emit("focus", { targetId: focusable.id, type: "focus" });
3468
+ const event = { targetId: focusable.id, type: "focus", epoch: this._epoch++ };
3469
+ if (this._started) {
3470
+ this._events.emit("focus", event);
3471
+ } else {
3472
+ this._pendingQueue.push(event);
3473
+ }
1936
3474
  }
1937
3475
  }
1938
3476
  /**
@@ -1941,21 +3479,31 @@ var FocusManager = class {
1941
3479
  unregister(id) {
1942
3480
  const idx = this._focusables.findIndex((f) => f.id === id);
1943
3481
  if (idx < 0) return;
3482
+ this._rects.delete(id);
1944
3483
  const wasFocused = idx === this._currentIndex;
1945
3484
  this._focusables.splice(idx, 1);
1946
3485
  if (wasFocused) {
1947
- this._events.emit("blur", { targetId: id, type: "blur" });
3486
+ this._events.emit("blur", { targetId: id, type: "blur", epoch: this._epoch++ });
1948
3487
  if (this._focusables.length > 0) {
1949
3488
  this._currentIndex = Math.min(this._currentIndex, this._focusables.length - 1);
1950
3489
  this._events.emit("focus", {
1951
3490
  targetId: this._focusables[this._currentIndex].id,
1952
- type: "focus"
3491
+ type: "focus",
3492
+ epoch: this._epoch++
1953
3493
  });
1954
3494
  } else {
1955
3495
  this._currentIndex = -1;
1956
3496
  }
1957
3497
  } else if (idx < this._currentIndex) {
1958
3498
  this._currentIndex--;
3499
+ this._events.emit("blur", { targetId: id, type: "blur", epoch: this._epoch++ });
3500
+ if (this._currentIndex >= 0 && this._currentIndex < this._focusables.length) {
3501
+ this._events.emit("focus", {
3502
+ targetId: this._focusables[this._currentIndex].id,
3503
+ type: "focus",
3504
+ epoch: this._epoch++
3505
+ });
3506
+ }
1959
3507
  }
1960
3508
  }
1961
3509
  /**
@@ -2099,6 +3647,69 @@ var FocusManager = class {
2099
3647
  }
2100
3648
  return false;
2101
3649
  }
3650
+ // ── Spatial Navigation ──────────────────────────────
3651
+ /** Record the on-screen rect for a widget, used for spatial navigation. */
3652
+ setRect(id, rect) {
3653
+ this._rects.set(id, rect);
3654
+ }
3655
+ _spatialFocus(isValid, calcDistance) {
3656
+ const currentId = this.currentId;
3657
+ if (!currentId) return false;
3658
+ const currentRect = this._rects.get(currentId);
3659
+ if (!currentRect) return false;
3660
+ const cx = currentRect.x + currentRect.width / 2;
3661
+ const cy = currentRect.y + currentRect.height / 2;
3662
+ let bestId = null;
3663
+ let minDistance = Infinity;
3664
+ const candidates = this._getActiveFocusables();
3665
+ for (const node of candidates) {
3666
+ if (!node.focusable || node.id === currentId) continue;
3667
+ const targetRect = this._rects.get(node.id);
3668
+ if (!targetRect) continue;
3669
+ const tx = targetRect.x + targetRect.width / 2;
3670
+ const ty = targetRect.y + targetRect.height / 2;
3671
+ if (isValid(cx, cy, tx, ty)) {
3672
+ const dist = calcDistance(cx, cy, tx, ty);
3673
+ if (dist < minDistance) {
3674
+ minDistance = dist;
3675
+ bestId = node.id;
3676
+ }
3677
+ }
3678
+ }
3679
+ if (bestId) {
3680
+ this.focusWidget(bestId);
3681
+ return true;
3682
+ }
3683
+ return false;
3684
+ }
3685
+ /** Move focus to the nearest focusable widget above the current one. */
3686
+ focusUp() {
3687
+ return this._spatialFocus(
3688
+ (cx, cy, tx, ty) => ty < cy,
3689
+ (cx, cy, tx, ty) => cy - ty + Math.abs(tx - cx)
3690
+ );
3691
+ }
3692
+ /** Move focus to the nearest focusable widget below the current one. */
3693
+ focusDown() {
3694
+ return this._spatialFocus(
3695
+ (cx, cy, tx, ty) => ty > cy,
3696
+ (cx, cy, tx, ty) => ty - cy + Math.abs(tx - cx)
3697
+ );
3698
+ }
3699
+ /** Move focus to the nearest focusable widget to the left of the current one. */
3700
+ focusLeft() {
3701
+ return this._spatialFocus(
3702
+ (cx, cy, tx, ty) => tx < cx,
3703
+ (cx, cy, tx, ty) => cx - tx + Math.abs(ty - cy)
3704
+ );
3705
+ }
3706
+ /** Move focus to the nearest focusable widget to the right of the current one. */
3707
+ focusRight() {
3708
+ return this._spatialFocus(
3709
+ (cx, cy, tx, ty) => tx > cx,
3710
+ (cx, cy, tx, ty) => tx - cx + Math.abs(ty - cy)
3711
+ );
3712
+ }
2102
3713
  // ── Private ──────────────────────────────────────────
2103
3714
  /**
2104
3715
  * Get the active focusables, filtered by the current trap if any.
@@ -2113,12 +3724,13 @@ var FocusManager = class {
2113
3724
  _changeFocus(newIndex) {
2114
3725
  const oldId = this.currentId;
2115
3726
  if (oldId) {
2116
- this._events.emit("blur", { targetId: oldId, type: "blur" });
3727
+ this._events.emit("blur", { targetId: oldId, type: "blur", epoch: this._epoch++ });
2117
3728
  }
2118
3729
  this._currentIndex = newIndex;
2119
3730
  this._events.emit("focus", {
2120
3731
  targetId: this._focusables[newIndex].id,
2121
- type: "focus"
3732
+ type: "focus",
3733
+ epoch: this._epoch++
2122
3734
  });
2123
3735
  }
2124
3736
  };
@@ -2353,6 +3965,23 @@ function renderFallback(screen) {
2353
3965
  return lines.join("\n");
2354
3966
  }
2355
3967
 
3968
+ // src/inline-viewport.ts
3969
+ function renderInlineToTerminal(terminal, screen, rows) {
3970
+ const totalRows = screen.rows;
3971
+ const start = Math.max(0, totalRows - rows);
3972
+ const lines = [];
3973
+ for (let r = start; r < totalRows; r++) {
3974
+ const row = screen.back[r];
3975
+ if (!row) continue;
3976
+ lines.push(row.map((c) => c.char || " ").join(""));
3977
+ }
3978
+ if (lines.length === 0) return;
3979
+ terminal.write(lines.join("\n") + "\n");
3980
+ }
3981
+ function createInlineViewport(opts) {
3982
+ return { rows: opts.rows };
3983
+ }
3984
+
2356
3985
  // src/app/App.ts
2357
3986
  var App = class {
2358
3987
  terminal;
@@ -2368,18 +3997,38 @@ var App = class {
2368
3997
  _exitResolve = null;
2369
3998
  _unsubKey = null;
2370
3999
  _unsubMouse = null;
4000
+ _unsubPaste = null;
4001
+ _unsubFocus = null;
4002
+ _unsubBlur = null;
4003
+ _unsubSigInt = null;
4004
+ _unsubSigTerm = null;
4005
+ _unsubUncaughtException = null;
4006
+ _unsubUnhandledRejection = null;
2371
4007
  _widgetById = /* @__PURE__ */ new Map();
4008
+ // any: Widget shape varies; narrowed at retrieval
4009
+ _pendingFocusState = /* @__PURE__ */ new Map();
4010
+ _consecutiveRenderFailures = 0;
4011
+ static MAX_RENDER_FAILURES = 5;
4012
+ // Lines to insert before inline viewport output. Each entry: { id: symbol, text: string }
4013
+ _insertBefore = [];
4014
+ // Core fix patch: Track if a paint task has been queued for the next event loop tick
4015
+ _isRenderPending = false;
2372
4016
  constructor(rootWidget, options = {}) {
2373
4017
  this._rootWidget = rootWidget;
2374
4018
  this._options = {
2375
4019
  fullscreen: true,
2376
4020
  mouse: false,
2377
4021
  fps: 30,
4022
+ dockBorders: false,
4023
+ diffRenderer: true,
4024
+ // Default screenMode: if fullscreen explicitly disabled, treat as 'main', otherwise 'alternate'
4025
+ screenMode: options.fullscreen === false ? "main" : "alternate",
4026
+ inlineRows: 0,
2378
4027
  ...options
2379
4028
  };
2380
4029
  this.terminal = new Terminal(options);
2381
4030
  this.screen = new Screen(this.terminal.cols, this.terminal.rows);
2382
- this.renderer = new Renderer(this.terminal, this.screen, this._options.fps);
4031
+ this.renderer = new Renderer(this.terminal, this.screen, this._options.fps, this._options.diffRenderer);
2383
4032
  this.input = new InputParser(this.terminal.stdin);
2384
4033
  this.focus = new FocusManager();
2385
4034
  this.events = new EventEmitter();
@@ -2387,8 +4036,6 @@ var App = class {
2387
4036
  }
2388
4037
  /**
2389
4038
  * Start the application.
2390
- * Sets up the terminal, starts the render loop, and mounts the root widget.
2391
- * Returns a promise that resolves when exit() is called.
2392
4039
  */
2393
4040
  async mount() {
2394
4041
  if (this._mounted) return 0;
@@ -2397,8 +4044,15 @@ var App = class {
2397
4044
  return 0;
2398
4045
  }
2399
4046
  this._mounted = true;
4047
+ this._subscribeFocusEvents();
4048
+ this.focus.start();
4049
+ const focusedId = this.focus.currentId;
4050
+ if (focusedId) {
4051
+ this._pendingFocusState.set(focusedId, true);
4052
+ }
4053
+ this.renderer.hook.start();
2400
4054
  this.terminal.enterRawMode();
2401
- if (this._options.fullscreen) {
4055
+ if (this._options.screenMode === "alternate") {
2402
4056
  this.terminal.enterAltScreen();
2403
4057
  }
2404
4058
  this.terminal.hideCursor();
@@ -2422,9 +4076,9 @@ var App = class {
2422
4076
  ...rawEvent,
2423
4077
  targetId: this.focus.currentId ?? void 0
2424
4078
  });
2425
- const focusedId = this.focus.currentId;
2426
- if (focusedId) {
2427
- const chain = this._buildBubbleChain(focusedId);
4079
+ const focusedId2 = this.focus.currentId;
4080
+ if (focusedId2) {
4081
+ const chain = this._buildBubbleChain(focusedId2);
2428
4082
  for (const widget of chain) {
2429
4083
  widget.events.emit("key", event);
2430
4084
  if (event._propagationStopped) break;
@@ -2446,6 +4100,43 @@ var App = class {
2446
4100
  this._unsubMouse = this.input.onMouse((event) => {
2447
4101
  this.events.emit("mouse", event);
2448
4102
  });
4103
+ this._unsubPaste = this.input.onPaste((text) => {
4104
+ this.events.emit("paste", text);
4105
+ });
4106
+ const onSigInt = () => {
4107
+ this.exit(130);
4108
+ };
4109
+ const onSigTerm = () => {
4110
+ this.exit(143);
4111
+ };
4112
+ process.on("SIGINT", onSigInt);
4113
+ process.on("SIGTERM", onSigTerm);
4114
+ this._unsubSigInt = () => process.off("SIGINT", onSigInt);
4115
+ this._unsubSigTerm = () => process.off("SIGTERM", onSigTerm);
4116
+ this.terminal.onCleanup(() => {
4117
+ this.renderer.hook.stop();
4118
+ });
4119
+ const onUncaughtException = (err) => {
4120
+ this.renderer.hook.stop();
4121
+ this.renderer.hook.writeRaw(this.renderer.hook.flush());
4122
+ this.renderer.hook.writeRaw(`Uncaught exception: ${err.message}
4123
+ ${err.stack}
4124
+ `);
4125
+ this.terminal.restore();
4126
+ process.exit(1);
4127
+ };
4128
+ process.on("uncaughtException", onUncaughtException);
4129
+ this._unsubUncaughtException = () => process.off("uncaughtException", onUncaughtException);
4130
+ const onUnhandledRejection = (reason) => {
4131
+ this.renderer.hook.stop();
4132
+ this.renderer.hook.writeRaw(this.renderer.hook.flush());
4133
+ this.renderer.hook.writeRaw(`Unhandled rejection: ${reason}
4134
+ `);
4135
+ this.terminal.restore();
4136
+ process.exit(1);
4137
+ };
4138
+ process.on("unhandledRejection", onUnhandledRejection);
4139
+ this._unsubUnhandledRejection = () => process.off("unhandledRejection", onUnhandledRejection);
2449
4140
  this.renderer.start(() => this.requestRender());
2450
4141
  this._rootWidget.mount?.();
2451
4142
  this.events.emit("mount", void 0);
@@ -2458,24 +4149,41 @@ var App = class {
2458
4149
  /**
2459
4150
  * Stop the application and restore terminal state.
2460
4151
  */
2461
- unmount() {
4152
+ unmount(exitCode = 0) {
2462
4153
  if (!this._mounted) return;
2463
4154
  this._mounted = false;
2464
4155
  this._rootWidget.unmount?.();
2465
4156
  this.events.emit("unmount", void 0);
4157
+ this._unsubSigInt?.();
4158
+ this._unsubSigInt = null;
4159
+ this._unsubSigTerm?.();
4160
+ this._unsubSigTerm = null;
2466
4161
  this._unsubKey?.();
2467
4162
  this._unsubKey = null;
2468
4163
  this._unsubMouse?.();
2469
4164
  this._unsubMouse = null;
4165
+ this._unsubFocus?.();
4166
+ this._unsubFocus = null;
4167
+ this._unsubBlur?.();
4168
+ this._unsubBlur = null;
4169
+ this._unsubPaste?.();
4170
+ this._unsubPaste = null;
4171
+ this._unsubUncaughtException?.();
4172
+ this._unsubUncaughtException = null;
4173
+ this._unsubUnhandledRejection?.();
4174
+ this._unsubUnhandledRejection = null;
4175
+ this.renderer.hook.stop();
2470
4176
  this.renderer.stop();
2471
4177
  this.input.stop();
2472
4178
  this.terminal.restore();
2473
4179
  this.events.removeAll();
4180
+ if (this._exitResolve) {
4181
+ this._exitResolve(exitCode);
4182
+ this._exitResolve = null;
4183
+ }
2474
4184
  }
2475
4185
  /**
2476
4186
  * Create an overlay layer for rendering above normal widgets.
2477
- * @param id Unique layer identifier (e.g. 'modal', 'select-dropdown', 'toast')
2478
- * @param zIndex Stacking order (higher = rendered on top). Default: 100
2479
4187
  */
2480
4188
  addOverlay(id, zIndex = 100) {
2481
4189
  this.layers.createLayer(id, zIndex);
@@ -2488,33 +4196,84 @@ var App = class {
2488
4196
  }
2489
4197
  /**
2490
4198
  * Request a re-render on the next frame.
2491
- * Skips layout + render pass when the root widget reports no dirty state.
4199
+ * Batches rapid structural updates via setImmediate scheduling so that
4200
+ * multiple synchronous state mutations collapse into a single render frame.
2492
4201
  */
2493
4202
  requestRender() {
2494
4203
  if (!this._mounted) return;
2495
- if (this._rootWidget.isDirty === false) {
2496
- return;
2497
- }
2498
- const layoutRoot = this._rootWidget.getLayoutNode();
2499
- computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
2500
- this._rootWidget.syncLayout?.();
2501
- this._buildWidgetMap(this._rootWidget);
2502
- this.screen.clear();
2503
- this._rootWidget.render(this.screen);
2504
- this._rootWidget.clearDirty?.();
2505
- this.layers.composite(this.screen);
2506
- this.renderer.requestFrame();
4204
+ if (this._isRenderPending) return;
4205
+ this._isRenderPending = true;
4206
+ setImmediate(() => {
4207
+ if (!this._mounted) {
4208
+ this._isRenderPending = false;
4209
+ return;
4210
+ }
4211
+ try {
4212
+ if (this._rootWidget.isDirty === false && !this.layers.hasDirtyLayers()) {
4213
+ return;
4214
+ }
4215
+ if (this._rootWidget.isDirty !== false) {
4216
+ const layoutRoot = this._rootWidget.getLayoutNode();
4217
+ computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
4218
+ this._rootWidget.syncLayout?.();
4219
+ this._buildWidgetMap(this._rootWidget);
4220
+ this.screen.clear();
4221
+ this._rootWidget.render(this.screen);
4222
+ if (this._options.dockBorders) {
4223
+ mergeBorders(this.screen);
4224
+ }
4225
+ this._rootWidget.clearDirty?.();
4226
+ }
4227
+ this.layers.composite(this.screen);
4228
+ if (this._options.screenMode === "inline") {
4229
+ for (const item of this._insertBefore) {
4230
+ this.terminal.write(item.text + "\n");
4231
+ }
4232
+ renderInlineToTerminal(this.terminal, this.screen, this._options.inlineRows ?? 0);
4233
+ } else {
4234
+ this.renderer.requestFrame();
4235
+ }
4236
+ } finally {
4237
+ this._isRenderPending = false;
4238
+ if (this._rootWidget.isDirty === true) {
4239
+ this.requestRender();
4240
+ }
4241
+ }
4242
+ });
2507
4243
  }
2508
4244
  /**
2509
4245
  * Exit the app (convenience method).
2510
4246
  */
2511
4247
  exit(code = 0) {
2512
- this.unmount();
4248
+ this.unmount(code);
2513
4249
  if (this._exitResolve) {
2514
4250
  this._exitResolve(code);
2515
4251
  this._exitResolve = null;
2516
4252
  }
2517
4253
  }
4254
+ /**
4255
+ * Read from the system clipboard.
4256
+ */
4257
+ readClipboard() {
4258
+ return this.terminal.readClipboard();
4259
+ }
4260
+ /**
4261
+ * Write to the system clipboard.
4262
+ */
4263
+ writeClipboard(text) {
4264
+ this.terminal.writeClipboard(text);
4265
+ }
4266
+ /**
4267
+ * Register a persistent line to be written above inline viewport output.
4268
+ */
4269
+ insertBefore(line) {
4270
+ const id = /* @__PURE__ */ Symbol();
4271
+ this._insertBefore.push({ id, text: line });
4272
+ return () => {
4273
+ const idx = this._insertBefore.findIndex((x) => x.id === id);
4274
+ if (idx >= 0) this._insertBefore.splice(idx, 1);
4275
+ };
4276
+ }
2518
4277
  /**
2519
4278
  * Render in fallback (static) mode for non-interactive environments.
2520
4279
  */
@@ -2529,8 +4288,6 @@ var App = class {
2529
4288
  }
2530
4289
  /**
2531
4290
  * Build the bubble chain for keyboard events.
2532
- * Returns an array: [focused widget, parent, grandparent, ..., root]
2533
- * Uses the cached _widgetById map for O(1) lookup instead of DFS.
2534
4291
  */
2535
4292
  _buildBubbleChain(widgetId) {
2536
4293
  const chain = [];
@@ -2547,15 +4304,17 @@ var App = class {
2547
4304
  }
2548
4305
  /**
2549
4306
  * Rebuild the widget ID cache by walking the entire widget tree.
2550
- * Called after syncLayout() so the map stays current.
2551
4307
  */
2552
4308
  _buildWidgetMap(root) {
2553
4309
  this._widgetById.clear();
2554
4310
  this._walkWidget(root);
4311
+ this._applyPendingFocusState();
2555
4312
  }
2556
4313
  _walkWidget(widget) {
2557
4314
  if (!widget) return;
2558
- if (widget.id) this._widgetById.set(widget.id, widget);
4315
+ if (widget.id) {
4316
+ this._widgetById.set(widget.id, widget);
4317
+ }
2559
4318
  const children = widget._children ?? widget.children ?? [];
2560
4319
  if (Array.isArray(children)) {
2561
4320
  for (const child of children) {
@@ -2563,167 +4322,116 @@ var App = class {
2563
4322
  }
2564
4323
  }
2565
4324
  }
2566
- };
2567
-
2568
- // src/utils/unicode.ts
2569
- function isWideChar(codePoint) {
2570
- return (
2571
- // CJK Unified Ideographs (common Chinese/Japanese/Korean)
2572
- codePoint >= 19968 && codePoint <= 40959 || // CJK Unified Ideographs Extension A
2573
- codePoint >= 13312 && codePoint <= 19903 || // CJK Compatibility Ideographs
2574
- codePoint >= 63744 && codePoint <= 64255 || // Hangul Syllables
2575
- codePoint >= 44032 && codePoint <= 55215 || // Katakana
2576
- codePoint >= 12448 && codePoint <= 12543 || // CJK Symbols and Punctuation
2577
- codePoint >= 12288 && codePoint <= 12351 || // Hiragana
2578
- codePoint >= 12352 && codePoint <= 12447 || // Fullwidth Forms
2579
- codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || // CJK Unified Ideographs Extension B
2580
- codePoint >= 131072 && codePoint <= 173791 || // CJK Unified Ideographs Extension C,D,E,F
2581
- codePoint >= 173824 && codePoint <= 191471 || // CJK Compatibility Ideographs Supplement
2582
- codePoint >= 194560 && codePoint <= 195103
2583
- );
2584
- }
2585
- function isCombining(codePoint) {
2586
- return (
2587
- // Combining Diacritical Marks
2588
- codePoint >= 768 && codePoint <= 879 || // Combining Diacritical Marks Extended
2589
- codePoint >= 6832 && codePoint <= 6911 || // Combining Diacritical Marks Supplement
2590
- codePoint >= 7616 && codePoint <= 7679 || // Combining Diacritical Marks for Symbols
2591
- codePoint >= 8400 && codePoint <= 8447 || // Combining Half Marks
2592
- codePoint >= 65056 && codePoint <= 65071 || // Variation selectors
2593
- codePoint >= 65024 && codePoint <= 65039 || // Zero-width joiner / non-joiner
2594
- codePoint === 8203 || codePoint === 8204 || codePoint === 8205 || codePoint === 65279
2595
- );
2596
- }
2597
- function isEmoji(codePoint) {
2598
- return (
2599
- // Emoticons
2600
- codePoint >= 128512 && codePoint <= 128591 || // Misc Symbols and Pictographs
2601
- codePoint >= 127744 && codePoint <= 128511 || // Transport and Map
2602
- codePoint >= 128640 && codePoint <= 128767 || // Supplemental Symbols
2603
- codePoint >= 129280 && codePoint <= 129535 || // Misc symbols
2604
- codePoint >= 9728 && codePoint <= 9983 || // Dingbats
2605
- codePoint >= 9984 && codePoint <= 10175 || // Flags
2606
- codePoint >= 127456 && codePoint <= 127487
2607
- );
2608
- }
2609
- function stringWidth(str) {
2610
- let width = 0;
2611
- let inEscape = false;
2612
- for (const char of str) {
2613
- const cp = char.codePointAt(0);
2614
- if (cp === 27) {
2615
- inEscape = true;
2616
- continue;
2617
- }
2618
- if (inEscape) {
2619
- if (cp >= 64 && cp <= 126 && cp !== 91) {
2620
- inEscape = false;
2621
- }
2622
- continue;
4325
+ _handleFocusEvent(event) {
4326
+ const focused = event.type === "focus";
4327
+ const changed = this._setWidgetFocused(event.targetId, focused);
4328
+ if (changed === null) {
4329
+ this._pendingFocusState.set(event.targetId, focused);
4330
+ return;
2623
4331
  }
2624
- if (cp < 32 || cp >= 127 && cp < 160) {
2625
- continue;
4332
+ if (changed) {
4333
+ this.requestRender();
2626
4334
  }
2627
- if (isCombining(cp)) {
2628
- continue;
4335
+ }
4336
+ _setWidgetFocused(id, focused) {
4337
+ const widget = this._widgetById.get(id);
4338
+ if (!widget) {
4339
+ return null;
2629
4340
  }
2630
- if (isWideChar(cp) || isEmoji(cp)) {
2631
- width += 2;
2632
- continue;
4341
+ if (!this._isFocusAwareWidget(widget) || widget.isFocused === focused) {
4342
+ return false;
2633
4343
  }
2634
- width += 1;
4344
+ widget.isFocused = focused;
4345
+ widget.markDirty?.();
4346
+ return true;
2635
4347
  }
2636
- return width;
2637
- }
2638
- function truncate(str, maxWidth, ellipsis = "\u2026") {
2639
- if (maxWidth <= 0) return "";
2640
- const strW = stringWidth(str);
2641
- if (strW <= maxWidth) return str;
2642
- const ellipsisW = stringWidth(ellipsis);
2643
- const targetW = maxWidth - ellipsisW;
2644
- if (targetW <= 0) return ellipsis.slice(0, maxWidth);
2645
- let width = 0;
2646
- let result = "";
2647
- let inEscape = false;
2648
- let escapeBuffer = "";
2649
- for (const char of str) {
2650
- const cp = char.codePointAt(0);
2651
- if (cp === 27) {
2652
- inEscape = true;
2653
- escapeBuffer += char;
2654
- continue;
4348
+ _subscribeFocusEvents() {
4349
+ if (!this._unsubFocus) {
4350
+ this._unsubFocus = this.focus.on("focus", (event) => this._handleFocusEvent(event));
2655
4351
  }
2656
- if (inEscape) {
2657
- escapeBuffer += char;
2658
- if (cp >= 64 && cp <= 126 && cp !== 91) {
2659
- inEscape = false;
2660
- result += escapeBuffer;
2661
- escapeBuffer = "";
4352
+ if (!this._unsubBlur) {
4353
+ this._unsubBlur = this.focus.on("blur", (event) => this._handleFocusEvent(event));
4354
+ }
4355
+ }
4356
+ _applyPendingFocusState() {
4357
+ for (const [id, focused] of this._pendingFocusState) {
4358
+ const stateChanged = this._setWidgetFocused(id, focused);
4359
+ if (stateChanged !== null) {
4360
+ this._pendingFocusState.delete(id);
2662
4361
  }
2663
- continue;
2664
4362
  }
2665
- let charW = 1;
2666
- if (isCombining(cp)) {
2667
- charW = 0;
2668
- } else if (isWideChar(cp) || isEmoji(cp)) {
2669
- charW = 2;
2670
- } else if (cp < 32 || cp >= 127 && cp < 160) {
2671
- charW = 0;
4363
+ }
4364
+ _isFocusAwareWidget(widget) {
4365
+ return typeof widget === "object" && widget !== null && "id" in widget && typeof widget.id === "string" && "isFocused" in widget && typeof widget.isFocused === "boolean";
4366
+ }
4367
+ };
4368
+
4369
+ // src/utils/debounce.ts
4370
+ function debounce(func, wait, options = {}) {
4371
+ const { leading = false, trailing = true } = options;
4372
+ let timeoutId = null;
4373
+ let lastArgs = null;
4374
+ let lastCallTime = null;
4375
+ let lastInvokeTime = 0;
4376
+ function invokeFunc(time) {
4377
+ const args = lastArgs;
4378
+ lastArgs = null;
4379
+ lastInvokeTime = time;
4380
+ return func(...args);
4381
+ }
4382
+ function shouldInvoke(time) {
4383
+ const timeSinceLastCall = time - (lastCallTime ?? 0);
4384
+ const timeSinceLastInvoke = time - lastInvokeTime;
4385
+ return lastCallTime === null || timeSinceLastCall >= wait || timeSinceLastCall < 0 || timeSinceLastInvoke >= wait;
4386
+ }
4387
+ function trailingEdge() {
4388
+ timeoutId = null;
4389
+ if (trailing && lastArgs) {
4390
+ return invokeFunc(Date.now());
2672
4391
  }
2673
- if (width + charW > targetW) break;
2674
- width += charW;
2675
- result += char;
4392
+ lastArgs = null;
4393
+ return void 0;
2676
4394
  }
2677
- return result + ellipsis;
2678
- }
2679
- function stripAnsi(str) {
2680
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
2681
- }
2682
- function wordWrap(str, width) {
2683
- if (width <= 0) return str;
2684
- const lines = str.split("\n");
2685
- const result = [];
2686
- for (const line of lines) {
2687
- if (stringWidth(line) <= width) {
2688
- result.push(line);
2689
- continue;
4395
+ function timerExpired() {
4396
+ const time = Date.now();
4397
+ if (shouldInvoke(time)) {
4398
+ trailingEdge();
4399
+ } else {
4400
+ const timeSinceLastCall = time - (lastCallTime ?? 0);
4401
+ const timeWaiting = wait - timeSinceLastCall;
4402
+ timeoutId = setTimeout(timerExpired, timeWaiting);
2690
4403
  }
2691
- let currentLine = "";
2692
- let currentWidth = 0;
2693
- const words = line.split(/(\s+)/);
2694
- for (const word of words) {
2695
- const wordW = stringWidth(word);
2696
- if (currentWidth + wordW <= width) {
2697
- currentLine += word;
2698
- currentWidth += wordW;
2699
- } else if (wordW > width) {
2700
- if (currentLine) {
2701
- result.push(currentLine);
2702
- currentLine = "";
2703
- currentWidth = 0;
2704
- }
2705
- for (const char of word) {
2706
- const cp = char.codePointAt(0);
2707
- const charW = isWideChar(cp) || isEmoji(cp) ? 2 : isCombining(cp) ? 0 : 1;
2708
- if (currentWidth + charW > width) {
2709
- result.push(currentLine);
2710
- currentLine = "";
2711
- currentWidth = 0;
2712
- }
2713
- currentLine += char;
2714
- currentWidth += charW;
4404
+ }
4405
+ const debounced = function(...args) {
4406
+ const time = Date.now();
4407
+ const isInvoking = shouldInvoke(time);
4408
+ lastArgs = args;
4409
+ lastCallTime = time;
4410
+ if (isInvoking) {
4411
+ if (timeoutId === null) {
4412
+ if (leading) {
4413
+ return invokeFunc(time);
2715
4414
  }
4415
+ timeoutId = setTimeout(timerExpired, wait);
2716
4416
  } else {
2717
- result.push(currentLine);
2718
- currentLine = word.trimStart();
2719
- currentWidth = stringWidth(currentLine);
4417
+ clearTimeout(timeoutId);
4418
+ timeoutId = setTimeout(timerExpired, wait);
2720
4419
  }
4420
+ } else if (timeoutId === null && trailing) {
4421
+ timeoutId = setTimeout(timerExpired, wait);
2721
4422
  }
2722
- if (currentLine) {
2723
- result.push(currentLine);
4423
+ return void 0;
4424
+ };
4425
+ debounced.cancel = () => {
4426
+ if (timeoutId !== null) {
4427
+ clearTimeout(timeoutId);
2724
4428
  }
2725
- }
2726
- return result.join("\n");
4429
+ lastInvokeTime = 0;
4430
+ lastArgs = null;
4431
+ lastCallTime = null;
4432
+ timeoutId = null;
4433
+ };
4434
+ return debounced;
2727
4435
  }
2728
4436
  export {
2729
4437
  App,
@@ -2736,14 +4444,27 @@ export {
2736
4444
  BarSets,
2737
4445
  BorderSets,
2738
4446
  CTRL_KEYS,
4447
+ ChordMatcher,
2739
4448
  ColorDepth,
4449
+ Constraint,
4450
+ Dim,
2740
4451
  ESCAPE_SEQUENCES,
2741
4452
  EventEmitter,
4453
+ FillConstraint,
4454
+ Flex,
2742
4455
  FocusManager,
2743
4456
  HORIZONTAL_BAR_SYMBOLS,
2744
4457
  InputParser,
2745
4458
  LayerManager,
4459
+ LengthConstraint,
2746
4460
  LineSets,
4461
+ LiveRender,
4462
+ MaxConstraint,
4463
+ MinConstraint,
4464
+ MouseGestures,
4465
+ PercentageConstraint,
4466
+ Pos,
4467
+ RenderHook,
2747
4468
  Renderer,
2748
4469
  SPECIAL_KEYS,
2749
4470
  Screen,
@@ -2752,40 +4473,48 @@ export {
2752
4473
  Terminal,
2753
4474
  VERTICAL_BAR_SYMBOLS,
2754
4475
  ansi_exports as ansi,
4476
+ bell2 as bell,
2755
4477
  borderSize,
2756
4478
  caps,
2757
4479
  cellsEqual,
4480
+ clipboard,
2758
4481
  colorToAnsiBg,
2759
4482
  colorToAnsiFg,
2760
4483
  colorToRgb,
2761
4484
  computeLayout,
2762
4485
  containsPoint,
2763
4486
  contrastRatio,
4487
+ createInlineViewport,
2764
4488
  createKeyEvent,
2765
4489
  createLayoutNode,
2766
4490
  createTestScreen,
4491
+ debounce,
2767
4492
  defaultStyle,
2768
4493
  detectColorDepth,
2769
4494
  emptyCell,
2770
4495
  emptyRect,
2771
- fill,
2772
4496
  getBorderChars,
4497
+ hasLayoutChanges,
2773
4498
  intersectRect,
4499
+ invalidateLayout,
2774
4500
  isMouseSequence,
2775
- length,
2776
- max,
4501
+ mergeBorders,
2777
4502
  mergeStyles,
2778
- min,
2779
4503
  normalizeEdges,
4504
+ normalizeNavigationKey,
2780
4505
  parseColor,
2781
4506
  parseMouseEvent,
2782
- percentage,
2783
- ratio,
4507
+ prefersHighContrast,
4508
+ prefersReducedMotion,
4509
+ readClipboard,
2784
4510
  relativeLuminance,
2785
4511
  renderFallback,
4512
+ renderInlineToTerminal,
4513
+ resolveConstraints,
4514
+ resolveLayoutVariables,
4515
+ shouldUseColor,
2786
4516
  shouldUseFallback,
2787
4517
  shrinkRect,
2788
- splitRect,
2789
4518
  stringWidth,
2790
4519
  stripAnsi,
2791
4520
  styleToCellAttrs,