@termuijs/core 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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: { type: "none" },
473
- bg: { type: "none" },
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 = { type: "none" };
486
- cell.bg = { type: "none" };
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) return;
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._findWidgetById(this._rootWidget, widgetId);
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
- * Find a widget by ID in the widget tree (DFS).
2406
- * Uses duck-typing to work with any object that has id/children.
2549
+ * Rebuild the widget ID cache by walking the entire widget tree.
2550
+ * Called after syncLayout() so the map stays current.
2407
2551
  */
2408
- _findWidgetById(root, id) {
2409
- if (root.id === id) return root;
2410
- const children = root._children ?? root.children ?? [];
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
- const found = this._findWidgetById(child, id);
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
- wordWrap
2799
+ validateThemeContrast,
2800
+ wcagLevel,
2801
+ wordWrap,
2802
+ writeClipboard
2647
2803
  };
2648
2804
  //# sourceMappingURL=index.js.map