@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.cjs +2210 -460
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +698 -187
- package/dist/index.d.ts +698 -187
- package/dist/index.js +2182 -453
- package/dist/index.js.map +1 -1
- package/package.json +14 -10
- package/LICENSE +0 -21
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(
|
|
217
|
+
function wcagLevel(ratio, large = false) {
|
|
218
218
|
if (large) {
|
|
219
|
-
if (
|
|
220
|
-
if (
|
|
219
|
+
if (ratio >= 4.5) return "AAA";
|
|
220
|
+
if (ratio >= 3) return "AA";
|
|
221
221
|
return "fail";
|
|
222
222
|
}
|
|
223
|
-
if (
|
|
224
|
-
if (
|
|
225
|
-
if (
|
|
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
|
|
245
|
-
const level = wcagLevel(
|
|
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(
|
|
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
|
-
|
|
396
|
-
|
|
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.
|
|
482
|
-
if (this.
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
497
|
-
this.
|
|
498
|
-
this.
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
712
|
-
this.
|
|
713
|
-
|
|
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]
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1358
|
-
|
|
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(
|
|
2282
|
+
this._tryParseEscape();
|
|
1364
2283
|
return;
|
|
1365
2284
|
}
|
|
1366
2285
|
if (str.startsWith("\x1B") && str.length === 1) {
|
|
1367
|
-
this._escapeBuffer =
|
|
2286
|
+
this._escapeBuffer = data;
|
|
1368
2287
|
this._escapeTimeout = setTimeout(() => {
|
|
1369
2288
|
this._events.emit("key", createKeyEvent({
|
|
1370
2289
|
key: "escape",
|
|
1371
|
-
raw:
|
|
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 =
|
|
1383
|
-
this._tryParseEscape(
|
|
2301
|
+
this._escapeBuffer = data;
|
|
2302
|
+
this._tryParseEscape();
|
|
1384
2303
|
return;
|
|
1385
2304
|
}
|
|
1386
|
-
for (
|
|
1387
|
-
const
|
|
1388
|
-
const
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
1641
|
-
const innerY = padding.top + border.vertical
|
|
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
|
|
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")
|
|
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,
|
|
3366
|
+
function clampSize(value, min, max) {
|
|
1770
3367
|
let result = value;
|
|
1771
|
-
if (
|
|
1772
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
|
2426
|
-
if (
|
|
2427
|
-
const chain = this._buildBubbleChain(
|
|
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
|
-
*
|
|
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.
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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)
|
|
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
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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 (
|
|
2625
|
-
|
|
4332
|
+
if (changed) {
|
|
4333
|
+
this.requestRender();
|
|
2626
4334
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
4335
|
+
}
|
|
4336
|
+
_setWidgetFocused(id, focused) {
|
|
4337
|
+
const widget = this._widgetById.get(id);
|
|
4338
|
+
if (!widget) {
|
|
4339
|
+
return null;
|
|
2629
4340
|
}
|
|
2630
|
-
if (
|
|
2631
|
-
|
|
2632
|
-
continue;
|
|
4341
|
+
if (!this._isFocusAwareWidget(widget) || widget.isFocused === focused) {
|
|
4342
|
+
return false;
|
|
2633
4343
|
}
|
|
2634
|
-
|
|
4344
|
+
widget.isFocused = focused;
|
|
4345
|
+
widget.markDirty?.();
|
|
4346
|
+
return true;
|
|
2635
4347
|
}
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
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 (
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
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
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
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
|
-
|
|
2674
|
-
|
|
2675
|
-
result += char;
|
|
4392
|
+
lastArgs = null;
|
|
4393
|
+
return void 0;
|
|
2676
4394
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
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
|
-
|
|
2692
|
-
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
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
|
-
|
|
2718
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
4423
|
+
return void 0;
|
|
4424
|
+
};
|
|
4425
|
+
debounced.cancel = () => {
|
|
4426
|
+
if (timeoutId !== null) {
|
|
4427
|
+
clearTimeout(timeoutId);
|
|
2724
4428
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
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
|
-
|
|
2776
|
-
max,
|
|
4501
|
+
mergeBorders,
|
|
2777
4502
|
mergeStyles,
|
|
2778
|
-
min,
|
|
2779
4503
|
normalizeEdges,
|
|
4504
|
+
normalizeNavigationKey,
|
|
2780
4505
|
parseColor,
|
|
2781
4506
|
parseMouseEvent,
|
|
2782
|
-
|
|
2783
|
-
|
|
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,
|