@termuijs/core 0.1.3 → 0.1.4
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/README.md +65 -25
- package/dist/index.cjs +188 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -6
- package/dist/index.d.ts +69 -6
- package/dist/index.js +178 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -199,6 +199,56 @@ function colorToAnsiBg(color, depth) {
|
|
|
199
199
|
return "";
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
+
function relativeLuminance(color) {
|
|
203
|
+
const [r, g, b] = colorToRgb(color);
|
|
204
|
+
const linearize = (c) => {
|
|
205
|
+
const sRGB = c / 255;
|
|
206
|
+
return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
|
207
|
+
};
|
|
208
|
+
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
|
|
209
|
+
}
|
|
210
|
+
function contrastRatio(fg, bg) {
|
|
211
|
+
const l1 = relativeLuminance(fg);
|
|
212
|
+
const l2 = relativeLuminance(bg);
|
|
213
|
+
const lighter = Math.max(l1, l2);
|
|
214
|
+
const darker = Math.min(l1, l2);
|
|
215
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
216
|
+
}
|
|
217
|
+
function wcagLevel(ratio2, large = false) {
|
|
218
|
+
if (large) {
|
|
219
|
+
if (ratio2 >= 4.5) return "AAA";
|
|
220
|
+
if (ratio2 >= 3) return "AA";
|
|
221
|
+
return "fail";
|
|
222
|
+
}
|
|
223
|
+
if (ratio2 >= 7) return "AAA";
|
|
224
|
+
if (ratio2 >= 4.5) return "AA";
|
|
225
|
+
if (ratio2 >= 3) return "A";
|
|
226
|
+
return "fail";
|
|
227
|
+
}
|
|
228
|
+
function validateThemeContrast(theme) {
|
|
229
|
+
const failures = [];
|
|
230
|
+
const bg = theme["bg"];
|
|
231
|
+
if (!bg) return failures;
|
|
232
|
+
const bgColor = parseColor(bg);
|
|
233
|
+
const pairs = [
|
|
234
|
+
["fg on bg", theme["fg"]],
|
|
235
|
+
["primary on bg", theme["primary"]],
|
|
236
|
+
["error on bg", theme["error"]],
|
|
237
|
+
["success on bg", theme["success"]],
|
|
238
|
+
["warning on bg", theme["warning"]],
|
|
239
|
+
["muted on bg", theme["muted"]]
|
|
240
|
+
];
|
|
241
|
+
for (const [label, hex] of pairs) {
|
|
242
|
+
if (!hex) continue;
|
|
243
|
+
const fgColor = parseColor(hex);
|
|
244
|
+
const ratio2 = contrastRatio(fgColor, bgColor);
|
|
245
|
+
const level = wcagLevel(ratio2);
|
|
246
|
+
if (level !== "AAA" && level !== "AA") {
|
|
247
|
+
failures.push({ pair: label, ratio: Math.round(ratio2 * 100) / 100, level, required: "AA" });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return failures;
|
|
251
|
+
}
|
|
202
252
|
|
|
203
253
|
// src/utils/ansi.ts
|
|
204
254
|
var ansi_exports = {};
|
|
@@ -246,7 +296,8 @@ __export(ansi_exports, {
|
|
|
246
296
|
setTitle: () => setTitle,
|
|
247
297
|
showCursor: () => showCursor,
|
|
248
298
|
strikethrough: () => strikethrough,
|
|
249
|
-
underline: () => underline
|
|
299
|
+
underline: () => underline,
|
|
300
|
+
writeClipboard: () => writeClipboard
|
|
250
301
|
});
|
|
251
302
|
var CSI = "\x1B[";
|
|
252
303
|
var OSC = "\x1B]";
|
|
@@ -306,6 +357,10 @@ var resetScrollRegion = `${CSI}r`;
|
|
|
306
357
|
function setTitle(title) {
|
|
307
358
|
return `${OSC}0;${title}\x07`;
|
|
308
359
|
}
|
|
360
|
+
function writeClipboard(text, stdout = process.stdout) {
|
|
361
|
+
const encoded = Buffer.from(text, "utf8").toString("base64");
|
|
362
|
+
stdout.write(`${OSC}52;c;${encoded}\x07`);
|
|
363
|
+
}
|
|
309
364
|
|
|
310
365
|
// src/terminal/Terminal.ts
|
|
311
366
|
var Terminal = class {
|
|
@@ -325,6 +380,8 @@ var Terminal = class {
|
|
|
325
380
|
_exitHandler = null;
|
|
326
381
|
_sigintHandler = null;
|
|
327
382
|
_sigtermHandler = null;
|
|
383
|
+
_uncaughtExceptionHandler = null;
|
|
384
|
+
_unhandledRejectionHandler = null;
|
|
328
385
|
_restored = false;
|
|
329
386
|
constructor(options = {}) {
|
|
330
387
|
this.stdout = options.stdout ?? process.stdout;
|
|
@@ -425,6 +482,14 @@ var Terminal = class {
|
|
|
425
482
|
if (this._exitHandler) process.off("exit", this._exitHandler);
|
|
426
483
|
if (this._sigintHandler) process.off("SIGINT", this._sigintHandler);
|
|
427
484
|
if (this._sigtermHandler) process.off("SIGTERM", this._sigtermHandler);
|
|
485
|
+
if (this._uncaughtExceptionHandler) {
|
|
486
|
+
process.off("uncaughtException", this._uncaughtExceptionHandler);
|
|
487
|
+
this._uncaughtExceptionHandler = null;
|
|
488
|
+
}
|
|
489
|
+
if (this._unhandledRejectionHandler) {
|
|
490
|
+
process.off("unhandledRejection", this._unhandledRejectionHandler);
|
|
491
|
+
this._unhandledRejectionHandler = null;
|
|
492
|
+
}
|
|
428
493
|
if (this._resizeHandler) {
|
|
429
494
|
this.stdout.off("resize", this._resizeHandler);
|
|
430
495
|
}
|
|
@@ -462,15 +527,26 @@ var Terminal = class {
|
|
|
462
527
|
process.on("exit", this._exitHandler);
|
|
463
528
|
process.on("SIGINT", this._sigintHandler);
|
|
464
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);
|
|
465
540
|
}
|
|
466
541
|
};
|
|
467
542
|
|
|
468
543
|
// src/terminal/Screen.ts
|
|
544
|
+
var EMPTY_COLOR = Object.freeze({ type: "none" });
|
|
469
545
|
function emptyCell() {
|
|
470
546
|
return {
|
|
471
547
|
char: " ",
|
|
472
|
-
fg:
|
|
473
|
-
bg:
|
|
548
|
+
fg: EMPTY_COLOR,
|
|
549
|
+
bg: EMPTY_COLOR,
|
|
474
550
|
bold: false,
|
|
475
551
|
italic: false,
|
|
476
552
|
underline: false,
|
|
@@ -482,8 +558,8 @@ function emptyCell() {
|
|
|
482
558
|
}
|
|
483
559
|
function resetCell(cell) {
|
|
484
560
|
cell.char = " ";
|
|
485
|
-
cell.fg =
|
|
486
|
-
cell.bg =
|
|
561
|
+
cell.fg = EMPTY_COLOR;
|
|
562
|
+
cell.bg = EMPTY_COLOR;
|
|
487
563
|
cell.bold = false;
|
|
488
564
|
cell.italic = false;
|
|
489
565
|
cell.underline = false;
|
|
@@ -621,6 +697,7 @@ var Screen = class {
|
|
|
621
697
|
* Clear the back buffer to all empty cells.
|
|
622
698
|
*/
|
|
623
699
|
clear() {
|
|
700
|
+
this._clipStack = [];
|
|
624
701
|
for (let r = 0; r < this._rows; r++) {
|
|
625
702
|
for (let c = 0; c < this._cols; c++) {
|
|
626
703
|
resetCell(this.back[r][c]);
|
|
@@ -678,6 +755,7 @@ var Renderer = class {
|
|
|
678
755
|
_frameTimer = null;
|
|
679
756
|
_renderRequested = false;
|
|
680
757
|
_colorDepth;
|
|
758
|
+
_onTick = null;
|
|
681
759
|
constructor(terminal, screen, fps = 30) {
|
|
682
760
|
this._terminal = terminal;
|
|
683
761
|
this._screen = screen;
|
|
@@ -689,14 +767,16 @@ var Renderer = class {
|
|
|
689
767
|
this._fps = fps;
|
|
690
768
|
if (this._frameTimer) {
|
|
691
769
|
this.stop();
|
|
692
|
-
this.start();
|
|
770
|
+
this.start(this._onTick ?? void 0);
|
|
693
771
|
}
|
|
694
772
|
}
|
|
695
773
|
/** Start the render loop */
|
|
696
|
-
start() {
|
|
774
|
+
start(onTick) {
|
|
697
775
|
if (this._frameTimer) return;
|
|
776
|
+
this._onTick = onTick ?? null;
|
|
698
777
|
const interval = Math.floor(1e3 / this._fps);
|
|
699
778
|
this._frameTimer = setInterval(() => {
|
|
779
|
+
this._onTick?.();
|
|
700
780
|
if (this._renderRequested) {
|
|
701
781
|
this._renderRequested = false;
|
|
702
782
|
this._flush();
|
|
@@ -962,6 +1042,42 @@ var LayerManager = class {
|
|
|
962
1042
|
}
|
|
963
1043
|
};
|
|
964
1044
|
|
|
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
|
+
// src/terminal/ascii-map.ts
|
|
1054
|
+
var BOX = {
|
|
1055
|
+
"\u250C": "+",
|
|
1056
|
+
"\u2510": "+",
|
|
1057
|
+
"\u2514": "+",
|
|
1058
|
+
"\u2518": "+",
|
|
1059
|
+
"\u2500": "-",
|
|
1060
|
+
"\u2502": "|",
|
|
1061
|
+
"\u251C": "+",
|
|
1062
|
+
"\u2524": "+",
|
|
1063
|
+
"\u252C": "+",
|
|
1064
|
+
"\u2534": "+",
|
|
1065
|
+
"\u253C": "+",
|
|
1066
|
+
"\u2550": "=",
|
|
1067
|
+
"\u2551": "|",
|
|
1068
|
+
"\u2554": "+",
|
|
1069
|
+
"\u2557": "+",
|
|
1070
|
+
"\u255A": "+",
|
|
1071
|
+
"\u255D": "+",
|
|
1072
|
+
"\u2560": "+",
|
|
1073
|
+
"\u2563": "+",
|
|
1074
|
+
"\u2566": "+",
|
|
1075
|
+
"\u2569": "+",
|
|
1076
|
+
"\u256C": "+"
|
|
1077
|
+
};
|
|
1078
|
+
var BRAILLE_SPIN = ["|", "/", "-", "\\"];
|
|
1079
|
+
var BLOCK = { full: "#", empty: " ", partial: "-" };
|
|
1080
|
+
|
|
965
1081
|
// src/events/types.ts
|
|
966
1082
|
function createKeyEvent(base) {
|
|
967
1083
|
const event = {
|
|
@@ -1317,6 +1433,10 @@ var InputParser = class {
|
|
|
1317
1433
|
return;
|
|
1318
1434
|
}
|
|
1319
1435
|
if (seq.length < 20) {
|
|
1436
|
+
if (this._escapeTimeout) {
|
|
1437
|
+
clearTimeout(this._escapeTimeout);
|
|
1438
|
+
this._escapeTimeout = null;
|
|
1439
|
+
}
|
|
1320
1440
|
this._escapeTimeout = setTimeout(() => {
|
|
1321
1441
|
this._escapeBuffer = "";
|
|
1322
1442
|
this._escapeTimeout = null;
|
|
@@ -1356,6 +1476,10 @@ var InputParser = class {
|
|
|
1356
1476
|
this._escapeBuffer = "";
|
|
1357
1477
|
return;
|
|
1358
1478
|
}
|
|
1479
|
+
if (this._escapeTimeout) {
|
|
1480
|
+
clearTimeout(this._escapeTimeout);
|
|
1481
|
+
this._escapeTimeout = null;
|
|
1482
|
+
}
|
|
1359
1483
|
this._escapeTimeout = setTimeout(() => {
|
|
1360
1484
|
this._escapeBuffer = "";
|
|
1361
1485
|
this._escapeTimeout = null;
|
|
@@ -1484,7 +1608,8 @@ function createLayoutNode(id, style, children = []) {
|
|
|
1484
1608
|
id,
|
|
1485
1609
|
style,
|
|
1486
1610
|
children,
|
|
1487
|
-
computed: { x: 0, y: 0, width: 0, height: 0 }
|
|
1611
|
+
computed: { x: 0, y: 0, width: 0, height: 0 },
|
|
1612
|
+
_dirty: true
|
|
1488
1613
|
};
|
|
1489
1614
|
}
|
|
1490
1615
|
function computeLayout(root, containerWidth, containerHeight) {
|
|
@@ -1506,7 +1631,10 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1506
1631
|
node.computed.width = nodeWidth2;
|
|
1507
1632
|
node.computed.height = nodeHeight2;
|
|
1508
1633
|
}
|
|
1509
|
-
if (node.children.length === 0)
|
|
1634
|
+
if (node.children.length === 0) {
|
|
1635
|
+
node._dirty = false;
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1510
1638
|
const nodeWidth = node.computed.width;
|
|
1511
1639
|
const nodeHeight = node.computed.height;
|
|
1512
1640
|
const innerX = padding.left + border.horizontal / 2;
|
|
@@ -1627,6 +1755,7 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1627
1755
|
mainOffset += info.mainSize + gap + spaceBetween;
|
|
1628
1756
|
layoutNode(info.node, info.node.computed.width, info.node.computed.height, true);
|
|
1629
1757
|
}
|
|
1758
|
+
node._dirty = false;
|
|
1630
1759
|
}
|
|
1631
1760
|
function resolveSize(value, available) {
|
|
1632
1761
|
if (value === void 0) return void 0;
|
|
@@ -2237,6 +2366,9 @@ var App = class {
|
|
|
2237
2366
|
_options;
|
|
2238
2367
|
_mounted = false;
|
|
2239
2368
|
_exitResolve = null;
|
|
2369
|
+
_unsubKey = null;
|
|
2370
|
+
_unsubMouse = null;
|
|
2371
|
+
_widgetById = /* @__PURE__ */ new Map();
|
|
2240
2372
|
constructor(rootWidget, options = {}) {
|
|
2241
2373
|
this._rootWidget = rootWidget;
|
|
2242
2374
|
this._options = {
|
|
@@ -2281,10 +2413,11 @@ var App = class {
|
|
|
2281
2413
|
this.screen.invalidate();
|
|
2282
2414
|
this.layers.resize(cols, rows);
|
|
2283
2415
|
this.events.emit("resize", { cols, rows });
|
|
2416
|
+
this._rootWidget.markDirty?.();
|
|
2284
2417
|
this.requestRender();
|
|
2285
2418
|
});
|
|
2286
2419
|
this.input.start();
|
|
2287
|
-
this.input.onKey((rawEvent) => {
|
|
2420
|
+
this._unsubKey = this.input.onKey((rawEvent) => {
|
|
2288
2421
|
const event = createKeyEvent({
|
|
2289
2422
|
...rawEvent,
|
|
2290
2423
|
targetId: this.focus.currentId ?? void 0
|
|
@@ -2310,10 +2443,10 @@ var App = class {
|
|
|
2310
2443
|
this.events.emit("key", event);
|
|
2311
2444
|
}
|
|
2312
2445
|
});
|
|
2313
|
-
this.input.onMouse((event) => {
|
|
2446
|
+
this._unsubMouse = this.input.onMouse((event) => {
|
|
2314
2447
|
this.events.emit("mouse", event);
|
|
2315
2448
|
});
|
|
2316
|
-
this.renderer.start();
|
|
2449
|
+
this.renderer.start(() => this.requestRender());
|
|
2317
2450
|
this._rootWidget.mount?.();
|
|
2318
2451
|
this.events.emit("mount", void 0);
|
|
2319
2452
|
this.screen.invalidate();
|
|
@@ -2330,6 +2463,10 @@ var App = class {
|
|
|
2330
2463
|
this._mounted = false;
|
|
2331
2464
|
this._rootWidget.unmount?.();
|
|
2332
2465
|
this.events.emit("unmount", void 0);
|
|
2466
|
+
this._unsubKey?.();
|
|
2467
|
+
this._unsubKey = null;
|
|
2468
|
+
this._unsubMouse?.();
|
|
2469
|
+
this._unsubMouse = null;
|
|
2333
2470
|
this.renderer.stop();
|
|
2334
2471
|
this.input.stop();
|
|
2335
2472
|
this.terminal.restore();
|
|
@@ -2351,14 +2488,20 @@ var App = class {
|
|
|
2351
2488
|
}
|
|
2352
2489
|
/**
|
|
2353
2490
|
* Request a re-render on the next frame.
|
|
2491
|
+
* Skips layout + render pass when the root widget reports no dirty state.
|
|
2354
2492
|
*/
|
|
2355
2493
|
requestRender() {
|
|
2356
2494
|
if (!this._mounted) return;
|
|
2495
|
+
if (this._rootWidget.isDirty === false) {
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2357
2498
|
const layoutRoot = this._rootWidget.getLayoutNode();
|
|
2358
2499
|
computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
|
|
2359
2500
|
this._rootWidget.syncLayout?.();
|
|
2501
|
+
this._buildWidgetMap(this._rootWidget);
|
|
2360
2502
|
this.screen.clear();
|
|
2361
2503
|
this._rootWidget.render(this.screen);
|
|
2504
|
+
this._rootWidget.clearDirty?.();
|
|
2362
2505
|
this.layers.composite(this.screen);
|
|
2363
2506
|
this.renderer.requestFrame();
|
|
2364
2507
|
}
|
|
@@ -2387,10 +2530,11 @@ var App = class {
|
|
|
2387
2530
|
/**
|
|
2388
2531
|
* Build the bubble chain for keyboard events.
|
|
2389
2532
|
* Returns an array: [focused widget, parent, grandparent, ..., root]
|
|
2533
|
+
* Uses the cached _widgetById map for O(1) lookup instead of DFS.
|
|
2390
2534
|
*/
|
|
2391
2535
|
_buildBubbleChain(widgetId) {
|
|
2392
2536
|
const chain = [];
|
|
2393
|
-
const widget = this.
|
|
2537
|
+
const widget = this._widgetById.get(widgetId);
|
|
2394
2538
|
if (!widget) return chain;
|
|
2395
2539
|
let current = widget;
|
|
2396
2540
|
while (current) {
|
|
@@ -2402,19 +2546,22 @@ var App = class {
|
|
|
2402
2546
|
return chain;
|
|
2403
2547
|
}
|
|
2404
2548
|
/**
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2549
|
+
* Rebuild the widget ID cache by walking the entire widget tree.
|
|
2550
|
+
* Called after syncLayout() so the map stays current.
|
|
2407
2551
|
*/
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2552
|
+
_buildWidgetMap(root) {
|
|
2553
|
+
this._widgetById.clear();
|
|
2554
|
+
this._walkWidget(root);
|
|
2555
|
+
}
|
|
2556
|
+
_walkWidget(widget) {
|
|
2557
|
+
if (!widget) return;
|
|
2558
|
+
if (widget.id) this._widgetById.set(widget.id, widget);
|
|
2559
|
+
const children = widget._children ?? widget.children ?? [];
|
|
2411
2560
|
if (Array.isArray(children)) {
|
|
2412
2561
|
for (const child of children) {
|
|
2413
|
-
|
|
2414
|
-
if (found) return found;
|
|
2562
|
+
this._walkWidget(child);
|
|
2415
2563
|
}
|
|
2416
2564
|
}
|
|
2417
|
-
return null;
|
|
2418
2565
|
}
|
|
2419
2566
|
};
|
|
2420
2567
|
|
|
@@ -2580,9 +2727,12 @@ function wordWrap(str, width) {
|
|
|
2580
2727
|
}
|
|
2581
2728
|
export {
|
|
2582
2729
|
App,
|
|
2730
|
+
BLOCK,
|
|
2583
2731
|
BORDER_CHARS,
|
|
2732
|
+
BOX,
|
|
2584
2733
|
BRAILLE_DOTS,
|
|
2585
2734
|
BRAILLE_OFFSET,
|
|
2735
|
+
BRAILLE_SPIN,
|
|
2586
2736
|
BarSets,
|
|
2587
2737
|
BorderSets,
|
|
2588
2738
|
CTRL_KEYS,
|
|
@@ -2603,12 +2753,14 @@ export {
|
|
|
2603
2753
|
VERTICAL_BAR_SYMBOLS,
|
|
2604
2754
|
ansi_exports as ansi,
|
|
2605
2755
|
borderSize,
|
|
2756
|
+
caps,
|
|
2606
2757
|
cellsEqual,
|
|
2607
2758
|
colorToAnsiBg,
|
|
2608
2759
|
colorToAnsiFg,
|
|
2609
2760
|
colorToRgb,
|
|
2610
2761
|
computeLayout,
|
|
2611
2762
|
containsPoint,
|
|
2763
|
+
contrastRatio,
|
|
2612
2764
|
createKeyEvent,
|
|
2613
2765
|
createLayoutNode,
|
|
2614
2766
|
createTestScreen,
|
|
@@ -2629,6 +2781,7 @@ export {
|
|
|
2629
2781
|
parseMouseEvent,
|
|
2630
2782
|
percentage,
|
|
2631
2783
|
ratio,
|
|
2784
|
+
relativeLuminance,
|
|
2632
2785
|
renderFallback,
|
|
2633
2786
|
shouldUseFallback,
|
|
2634
2787
|
shrinkRect,
|
|
@@ -2643,6 +2796,9 @@ export {
|
|
|
2643
2796
|
testScreenToString,
|
|
2644
2797
|
truncate,
|
|
2645
2798
|
unionRect,
|
|
2646
|
-
|
|
2799
|
+
validateThemeContrast,
|
|
2800
|
+
wcagLevel,
|
|
2801
|
+
wordWrap,
|
|
2802
|
+
writeClipboard
|
|
2647
2803
|
};
|
|
2648
2804
|
//# sourceMappingURL=index.js.map
|