@termuijs/core 0.1.0 → 0.1.2

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
@@ -63,7 +63,7 @@ function parseColor(input) {
63
63
  if (hex.length === 6 && /^[0-9a-fA-F]{6}$/.test(hex)) {
64
64
  return { type: "hex", hex: "#" + hex.toLowerCase() };
65
65
  }
66
- throw new Error(`Invalid hex color: ${input}`);
66
+ return { type: "none" };
67
67
  }
68
68
  const rgbMatch = input.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/);
69
69
  if (rgbMatch) {
@@ -77,7 +77,7 @@ function parseColor(input) {
77
77
  const code = Math.min(255, parseInt(ansi256Match[1], 10));
78
78
  return { type: "ansi256", code };
79
79
  }
80
- throw new Error(`Unknown color format: ${input}`);
80
+ return { type: "none" };
81
81
  }
82
82
  function colorToRgb(color) {
83
83
  switch (color.type) {
@@ -320,19 +320,26 @@ var Terminal = class {
320
320
  _resizeHandlers = [];
321
321
  _cleanupHandlers = [];
322
322
  _originalRawMode;
323
+ // Stored handler references for proper cleanup
324
+ _resizeHandler = null;
325
+ _exitHandler = null;
326
+ _sigintHandler = null;
327
+ _sigtermHandler = null;
328
+ _restored = false;
323
329
  constructor(options = {}) {
324
330
  this.stdout = options.stdout ?? process.stdout;
325
331
  this.stdin = options.stdin ?? process.stdin;
326
332
  this.colorDepth = options.colorDepth ?? detectColorDepth();
327
333
  this._cols = this.stdout.columns ?? 80;
328
334
  this._rows = this.stdout.rows ?? 24;
329
- this.stdout.on("resize", () => {
335
+ this._resizeHandler = () => {
330
336
  this._cols = this.stdout.columns ?? 80;
331
337
  this._rows = this.stdout.rows ?? 24;
332
338
  for (const handler of this._resizeHandlers) {
333
339
  handler(this._cols, this._rows);
334
340
  }
335
- });
341
+ };
342
+ this.stdout.on("resize", this._resizeHandler);
336
343
  this._setupCleanup();
337
344
  }
338
345
  /** Current terminal width in columns */
@@ -409,9 +416,18 @@ var Terminal = class {
409
416
  // ── Cleanup ─────────────────────────────────────────
410
417
  /**
411
418
  * Restore terminal to its original state.
419
+ * Removes all process signal handlers to prevent leaks.
412
420
  * Called automatically on SIGINT, SIGTERM, process exit.
413
421
  */
414
422
  restore() {
423
+ if (this._restored) return;
424
+ this._restored = true;
425
+ if (this._exitHandler) process.off("exit", this._exitHandler);
426
+ if (this._sigintHandler) process.off("SIGINT", this._sigintHandler);
427
+ if (this._sigtermHandler) process.off("SIGTERM", this._sigtermHandler);
428
+ if (this._resizeHandler) {
429
+ this.stdout.off("resize", this._resizeHandler);
430
+ }
415
431
  this.disableMouse();
416
432
  this.exitAltScreen();
417
433
  this.exitRawMode();
@@ -425,7 +441,7 @@ var Terminal = class {
425
441
  this._cleanupHandlers.push(handler);
426
442
  }
427
443
  _setupCleanup() {
428
- const cleanup = () => {
444
+ const runCleanupHandlers = () => {
429
445
  for (const handler of this._cleanupHandlers) {
430
446
  try {
431
447
  handler();
@@ -434,20 +450,18 @@ var Terminal = class {
434
450
  }
435
451
  this.restore();
436
452
  };
437
- process.on("exit", cleanup);
438
- process.on("SIGINT", () => {
439
- cleanup();
453
+ this._exitHandler = runCleanupHandlers;
454
+ this._sigintHandler = () => {
455
+ runCleanupHandlers();
440
456
  process.exit(130);
441
- });
442
- process.on("SIGTERM", () => {
443
- cleanup();
457
+ };
458
+ this._sigtermHandler = () => {
459
+ runCleanupHandlers();
444
460
  process.exit(143);
445
- });
446
- process.on("uncaughtException", (err) => {
447
- cleanup();
448
- console.error(err);
449
- process.exit(1);
450
- });
461
+ };
462
+ process.on("exit", this._exitHandler);
463
+ process.on("SIGINT", this._sigintHandler);
464
+ process.on("SIGTERM", this._sigtermHandler);
451
465
  }
452
466
  };
453
467
 
@@ -466,6 +480,18 @@ function emptyCell() {
466
480
  width: 1
467
481
  };
468
482
  }
483
+ function resetCell(cell) {
484
+ cell.char = " ";
485
+ cell.fg = { type: "none" };
486
+ cell.bg = { type: "none" };
487
+ cell.bold = false;
488
+ cell.italic = false;
489
+ cell.underline = false;
490
+ cell.dim = false;
491
+ cell.strikethrough = false;
492
+ cell.inverse = false;
493
+ cell.width = 1;
494
+ }
469
495
  function cellsEqual(a, b) {
470
496
  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);
471
497
  }
@@ -597,7 +623,7 @@ var Screen = class {
597
623
  clear() {
598
624
  for (let r = 0; r < this._rows; r++) {
599
625
  for (let c = 0; c < this._cols; c++) {
600
- this.back[r][c] = emptyCell();
626
+ resetCell(this.back[r][c]);
601
627
  }
602
628
  }
603
629
  }
@@ -1132,13 +1158,19 @@ var EventEmitter = class {
1132
1158
  const handlers = this._handlers.get(event);
1133
1159
  if (handlers) {
1134
1160
  for (const handler of handlers) {
1135
- handler(data);
1161
+ try {
1162
+ handler(data);
1163
+ } catch {
1164
+ }
1136
1165
  }
1137
1166
  }
1138
1167
  const onceHandlers = this._onceHandlers.get(event);
1139
1168
  if (onceHandlers) {
1140
1169
  for (const handler of onceHandlers) {
1141
- handler(data);
1170
+ try {
1171
+ handler(data);
1172
+ } catch {
1173
+ }
1142
1174
  }
1143
1175
  onceHandlers.clear();
1144
1176
  }
@@ -1579,17 +1611,17 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
1579
1611
  }
1580
1612
  if (isRow) {
1581
1613
  info.node.computed = {
1582
- x: node.computed.x + innerX + mainOffset + info.margin.left,
1583
- y: node.computed.y + innerY + crossOffset + info.margin.top,
1584
- width: Math.max(0, info.mainSize - info.margin.left - info.margin.right),
1585
- height: Math.max(0, finalCrossSize - info.margin.top - info.margin.bottom)
1614
+ x: Math.floor(node.computed.x + innerX + mainOffset + info.margin.left),
1615
+ y: Math.floor(node.computed.y + innerY + crossOffset + info.margin.top),
1616
+ width: Math.round(Math.max(0, info.mainSize - info.margin.left - info.margin.right)),
1617
+ height: Math.round(Math.max(0, finalCrossSize - info.margin.top - info.margin.bottom))
1586
1618
  };
1587
1619
  } else {
1588
1620
  info.node.computed = {
1589
- x: node.computed.x + innerX + crossOffset + info.margin.left,
1590
- y: node.computed.y + innerY + mainOffset + info.margin.top,
1591
- width: Math.max(0, finalCrossSize - info.margin.left - info.margin.right),
1592
- height: Math.max(0, info.mainSize - info.margin.top - info.margin.bottom)
1621
+ x: Math.floor(node.computed.x + innerX + crossOffset + info.margin.left),
1622
+ y: Math.floor(node.computed.y + innerY + mainOffset + info.margin.top),
1623
+ width: Math.round(Math.max(0, finalCrossSize - info.margin.left - info.margin.right)),
1624
+ height: Math.round(Math.max(0, info.mainSize - info.margin.top - info.margin.bottom))
1593
1625
  };
1594
1626
  }
1595
1627
  mainOffset += info.mainSize + gap + spaceBetween;
@@ -1605,10 +1637,10 @@ function resolveSize(value, available) {
1605
1637
  }
1606
1638
  return void 0;
1607
1639
  }
1608
- function clampSize(value, min, max) {
1640
+ function clampSize(value, min2, max2) {
1609
1641
  let result = value;
1610
- if (min !== void 0) result = Math.max(result, min);
1611
- if (max !== void 0) result = Math.min(result, max);
1642
+ if (min2 !== void 0) result = Math.max(result, min2);
1643
+ if (max2 !== void 0) result = Math.min(result, max2);
1612
1644
  return result;
1613
1645
  }
1614
1646
 
@@ -1643,6 +1675,94 @@ function unionRect(a, b) {
1643
1675
  return { x, y, width: r - x, height: bot - y };
1644
1676
  }
1645
1677
 
1678
+ // src/layout/ConstraintLayout.ts
1679
+ var length = (n) => ({ type: "length", value: n });
1680
+ var percentage = (n) => ({ type: "percentage", value: n });
1681
+ var ratio = (num, den) => ({ type: "ratio", num, den });
1682
+ var min = (n) => ({ type: "min", value: n });
1683
+ var max = (n) => ({ type: "max", value: n });
1684
+ var fill = (weight = 1) => ({ type: "fill", weight });
1685
+ function resolveSize2(constraint, available) {
1686
+ switch (constraint.type) {
1687
+ case "length":
1688
+ return Math.min(constraint.value, available);
1689
+ case "percentage":
1690
+ return Math.min(Math.floor(available * constraint.value / 100), available);
1691
+ case "ratio":
1692
+ return constraint.den === 0 ? 0 : Math.min(
1693
+ Math.floor(available * constraint.num / constraint.den),
1694
+ available
1695
+ );
1696
+ case "min":
1697
+ return constraint.value;
1698
+ case "max":
1699
+ return Math.min(constraint.value, available);
1700
+ case "fill":
1701
+ return 0;
1702
+ }
1703
+ }
1704
+ function splitRect(rect, constraints, direction = "vertical", gap = 0) {
1705
+ if (constraints.length === 0) return [];
1706
+ const totalAvailable = direction === "horizontal" ? rect.width : rect.height;
1707
+ const count = constraints.length;
1708
+ const totalGaps = count > 1 ? gap * (count - 1) : 0;
1709
+ const availableForConstraints = Math.max(0, totalAvailable - totalGaps);
1710
+ const sizes = [];
1711
+ let usedSpace = 0;
1712
+ let fillWeightSum = 0;
1713
+ for (const constraint of constraints) {
1714
+ if (constraint.type === "fill") {
1715
+ sizes.push(0);
1716
+ fillWeightSum += Math.max(1, constraint.weight);
1717
+ } else {
1718
+ const size = resolveSize2(constraint, availableForConstraints);
1719
+ sizes.push(size);
1720
+ usedSpace += size;
1721
+ }
1722
+ }
1723
+ if (fillWeightSum > 0) {
1724
+ const remaining = Math.max(0, availableForConstraints - usedSpace);
1725
+ let distributed = 0;
1726
+ for (let i = 0; i < count; i++) {
1727
+ const constraint = constraints[i];
1728
+ if (!constraint || constraint.type !== "fill") continue;
1729
+ const weight = Math.max(1, constraint.weight);
1730
+ const share = Math.floor(remaining * weight / fillWeightSum);
1731
+ sizes[i] = share;
1732
+ distributed += share;
1733
+ }
1734
+ const leftover = remaining - distributed;
1735
+ if (leftover > 0) {
1736
+ for (let i = count - 1; i >= 0; i--) {
1737
+ const constraint = constraints[i];
1738
+ if (constraint && constraint.type === "fill") {
1739
+ sizes[i] = (sizes[i] ?? 0) + leftover;
1740
+ break;
1741
+ }
1742
+ }
1743
+ }
1744
+ }
1745
+ let totalUsed = 0;
1746
+ for (let i = 0; i < count; i++) {
1747
+ const size = sizes[i] ?? 0;
1748
+ const clamped = Math.max(0, Math.min(size, availableForConstraints - totalUsed));
1749
+ sizes[i] = clamped;
1750
+ totalUsed += clamped;
1751
+ }
1752
+ const results = [];
1753
+ let offset = 0;
1754
+ for (let i = 0; i < count; i++) {
1755
+ const size = sizes[i] ?? 0;
1756
+ if (direction === "horizontal") {
1757
+ results.push({ x: rect.x + offset, y: rect.y, width: size, height: rect.height });
1758
+ } else {
1759
+ results.push({ x: rect.x, y: rect.y + offset, width: rect.width, height: size });
1760
+ }
1761
+ offset += size + gap;
1762
+ }
1763
+ return results;
1764
+ }
1765
+
1646
1766
  // src/events/FocusManager.ts
1647
1767
  var FocusManager = class {
1648
1768
  _focusables = [];
@@ -1874,6 +1994,212 @@ var FocusManager = class {
1874
1994
  }
1875
1995
  };
1876
1996
 
1997
+ // src/style/symbols.ts
1998
+ var BorderSets = {
1999
+ PLAIN: {
2000
+ topLeft: "\u250C",
2001
+ topRight: "\u2510",
2002
+ bottomLeft: "\u2514",
2003
+ bottomRight: "\u2518",
2004
+ horizontal: "\u2500",
2005
+ vertical: "\u2502",
2006
+ cross: "\u253C"
2007
+ },
2008
+ ROUNDED: {
2009
+ topLeft: "\u256D",
2010
+ topRight: "\u256E",
2011
+ bottomLeft: "\u2570",
2012
+ bottomRight: "\u256F",
2013
+ horizontal: "\u2500",
2014
+ vertical: "\u2502",
2015
+ cross: "\u253C"
2016
+ },
2017
+ DOUBLE: {
2018
+ topLeft: "\u2554",
2019
+ topRight: "\u2557",
2020
+ bottomLeft: "\u255A",
2021
+ bottomRight: "\u255D",
2022
+ horizontal: "\u2550",
2023
+ vertical: "\u2551",
2024
+ cross: "\u256C"
2025
+ },
2026
+ THICK: {
2027
+ topLeft: "\u250F",
2028
+ topRight: "\u2513",
2029
+ bottomLeft: "\u2517",
2030
+ bottomRight: "\u251B",
2031
+ horizontal: "\u2501",
2032
+ vertical: "\u2503",
2033
+ cross: "\u254B"
2034
+ },
2035
+ QUADRANT_INSIDE: {
2036
+ topLeft: "\u2597",
2037
+ topRight: "\u2596",
2038
+ bottomLeft: "\u259D",
2039
+ bottomRight: "\u2598",
2040
+ horizontal: "\u2580",
2041
+ vertical: "\u2590",
2042
+ cross: "\u2588"
2043
+ },
2044
+ QUADRANT_OUTSIDE: {
2045
+ topLeft: "\u259B",
2046
+ topRight: "\u259C",
2047
+ bottomLeft: "\u2599",
2048
+ bottomRight: "\u259F",
2049
+ horizontal: "\u2580",
2050
+ vertical: "\u258C",
2051
+ cross: "\u2588"
2052
+ },
2053
+ EMPTY: {
2054
+ topLeft: " ",
2055
+ topRight: " ",
2056
+ bottomLeft: " ",
2057
+ bottomRight: " ",
2058
+ horizontal: " ",
2059
+ vertical: " ",
2060
+ cross: " "
2061
+ }
2062
+ };
2063
+ var BarSets = {
2064
+ NINE_LEVELS: {
2065
+ full: "\u2588",
2066
+ sevenEighths: "\u2587",
2067
+ threeQuarters: "\u2586",
2068
+ fiveEighths: "\u2585",
2069
+ half: "\u2584",
2070
+ threeEighths: "\u2583",
2071
+ oneQuarter: "\u2582",
2072
+ oneEighth: "\u2581",
2073
+ empty: " "
2074
+ },
2075
+ THREE_LEVELS: {
2076
+ full: "\u2588",
2077
+ sevenEighths: "\u2591",
2078
+ threeQuarters: "\u2591",
2079
+ fiveEighths: "\u2591",
2080
+ half: "\u2584",
2081
+ threeEighths: "\u2591",
2082
+ oneQuarter: "\u2591",
2083
+ oneEighth: "\u2591",
2084
+ empty: " "
2085
+ },
2086
+ ASCII: {
2087
+ full: "#",
2088
+ sevenEighths: "#",
2089
+ threeQuarters: "#",
2090
+ fiveEighths: "#",
2091
+ half: "#",
2092
+ threeEighths: "-",
2093
+ oneQuarter: "-",
2094
+ oneEighth: "-",
2095
+ empty: " "
2096
+ }
2097
+ };
2098
+ var VERTICAL_BAR_SYMBOLS = [
2099
+ " ",
2100
+ "\u2581",
2101
+ "\u2582",
2102
+ "\u2583",
2103
+ "\u2584",
2104
+ "\u2585",
2105
+ "\u2586",
2106
+ "\u2587",
2107
+ "\u2588"
2108
+ ];
2109
+ var HORIZONTAL_BAR_SYMBOLS = [
2110
+ " ",
2111
+ "\u258F",
2112
+ "\u258E",
2113
+ "\u258D",
2114
+ "\u258C",
2115
+ "\u258B",
2116
+ "\u258A",
2117
+ "\u2589",
2118
+ "\u2588"
2119
+ ];
2120
+ var ScrollbarSets = {
2121
+ VERTICAL: { track: "\u2502", thumb: "\u2588", begin: "\u2191", end: "\u2193" },
2122
+ HORIZONTAL: { track: "\u2500", thumb: "\u2588", begin: "\u2190", end: "\u2192" },
2123
+ DOUBLE_VERTICAL: { track: "\u2551", thumb: "\u2590", begin: "\u25B2", end: "\u25BC" },
2124
+ DOUBLE_HORIZONTAL: { track: "\u2550", thumb: "\u258C", begin: "\u25C4", end: "\u25BA" }
2125
+ };
2126
+ var LineSets = {
2127
+ NORMAL: { horizontal: "\u2500", vertical: "\u2502", cross: "\u253C" },
2128
+ THICK: { horizontal: "\u2501", vertical: "\u2503", cross: "\u254B" },
2129
+ DOUBLE: { horizontal: "\u2550", vertical: "\u2551", cross: "\u256C" }
2130
+ };
2131
+ var Shade = {
2132
+ FULL: "\u2588",
2133
+ DARK: "\u2593",
2134
+ MEDIUM: "\u2592",
2135
+ LIGHT: "\u2591",
2136
+ EMPTY: " "
2137
+ };
2138
+ var BRAILLE_OFFSET = 10240;
2139
+ var BRAILLE_DOTS = [
2140
+ [1, 8],
2141
+ [2, 16],
2142
+ [4, 32],
2143
+ [64, 128]
2144
+ ];
2145
+
2146
+ // src/terminal/TestBackend.ts
2147
+ function createTestScreen(width, height) {
2148
+ const cells = [];
2149
+ for (let row = 0; row < height; row++) {
2150
+ const rowCells = [];
2151
+ for (let col = 0; col < width; col++) {
2152
+ rowCells.push(emptyCell());
2153
+ }
2154
+ cells.push(rowCells);
2155
+ }
2156
+ return { width, height, cells };
2157
+ }
2158
+ function testScreenSetCell(screen, x, y, cell) {
2159
+ if (x < 0 || x >= screen.width || y < 0 || y >= screen.height) {
2160
+ return;
2161
+ }
2162
+ const row = screen.cells[y];
2163
+ if (row) {
2164
+ row[x] = cell;
2165
+ }
2166
+ }
2167
+ function testScreenGetCell(screen, x, y) {
2168
+ if (x < 0 || x >= screen.width || y < 0 || y >= screen.height) {
2169
+ return void 0;
2170
+ }
2171
+ const row = screen.cells[y];
2172
+ return row ? row[x] : void 0;
2173
+ }
2174
+ function testScreenToString(screen) {
2175
+ return screen.cells.map((row) => row.map((cell) => cell.char).join("")).join("\n");
2176
+ }
2177
+ function testScreenClear(screen) {
2178
+ for (let y = 0; y < screen.height; y++) {
2179
+ const row = screen.cells[y];
2180
+ if (row) {
2181
+ for (let x = 0; x < screen.width; x++) {
2182
+ row[x] = emptyCell();
2183
+ }
2184
+ }
2185
+ }
2186
+ }
2187
+ function testScreenSetString(screen, x, y, str) {
2188
+ let cx = x;
2189
+ for (const ch of str) {
2190
+ if (cx >= screen.width) break;
2191
+ if (cx >= 0 && y >= 0 && y < screen.height) {
2192
+ const row = screen.cells[y];
2193
+ if (row) {
2194
+ const cell = emptyCell();
2195
+ cell.char = ch;
2196
+ row[cx] = cell;
2197
+ }
2198
+ }
2199
+ cx++;
2200
+ }
2201
+ }
2202
+
1877
2203
  // src/app/Fallback.ts
1878
2204
  function shouldUseFallback() {
1879
2205
  if (!process.stdout.isTTY) return true;
@@ -1934,7 +2260,7 @@ var App = class {
1934
2260
  */
1935
2261
  async mount() {
1936
2262
  if (this._mounted) return 0;
1937
- if (this._options.forceFallback || shouldUseFallback()) {
2263
+ if (this._options.forceFallback || !this._options.skipFallback && shouldUseFallback()) {
1938
2264
  this._renderFallback();
1939
2265
  return 0;
1940
2266
  }
@@ -2044,8 +2370,6 @@ var App = class {
2044
2370
  if (this._exitResolve) {
2045
2371
  this._exitResolve(code);
2046
2372
  this._exitResolve = null;
2047
- } else {
2048
- process.exit(code);
2049
2373
  }
2050
2374
  }
2051
2375
  /**
@@ -2257,17 +2581,26 @@ function wordWrap(str, width) {
2257
2581
  export {
2258
2582
  App,
2259
2583
  BORDER_CHARS,
2584
+ BRAILLE_DOTS,
2585
+ BRAILLE_OFFSET,
2586
+ BarSets,
2587
+ BorderSets,
2260
2588
  CTRL_KEYS,
2261
2589
  ColorDepth,
2262
2590
  ESCAPE_SEQUENCES,
2263
2591
  EventEmitter,
2264
2592
  FocusManager,
2593
+ HORIZONTAL_BAR_SYMBOLS,
2265
2594
  InputParser,
2266
2595
  LayerManager,
2596
+ LineSets,
2267
2597
  Renderer,
2268
2598
  SPECIAL_KEYS,
2269
2599
  Screen,
2600
+ ScrollbarSets,
2601
+ Shade,
2270
2602
  Terminal,
2603
+ VERTICAL_BAR_SYMBOLS,
2271
2604
  ansi_exports as ansi,
2272
2605
  borderSize,
2273
2606
  cellsEqual,
@@ -2278,23 +2611,36 @@ export {
2278
2611
  containsPoint,
2279
2612
  createKeyEvent,
2280
2613
  createLayoutNode,
2614
+ createTestScreen,
2281
2615
  defaultStyle,
2282
2616
  detectColorDepth,
2283
2617
  emptyCell,
2284
2618
  emptyRect,
2619
+ fill,
2285
2620
  getBorderChars,
2286
2621
  intersectRect,
2287
2622
  isMouseSequence,
2623
+ length,
2624
+ max,
2288
2625
  mergeStyles,
2626
+ min,
2289
2627
  normalizeEdges,
2290
2628
  parseColor,
2291
2629
  parseMouseEvent,
2630
+ percentage,
2631
+ ratio,
2292
2632
  renderFallback,
2293
2633
  shouldUseFallback,
2294
2634
  shrinkRect,
2635
+ splitRect,
2295
2636
  stringWidth,
2296
2637
  stripAnsi,
2297
2638
  styleToCellAttrs,
2639
+ testScreenClear,
2640
+ testScreenGetCell,
2641
+ testScreenSetCell,
2642
+ testScreenSetString,
2643
+ testScreenToString,
2298
2644
  truncate,
2299
2645
  unionRect,
2300
2646
  wordWrap