@termuijs/core 0.1.2 → 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 +66 -26
- 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 +6 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @termuijs/core
|
|
2
2
|
|
|
3
|
-
The rendering engine
|
|
3
|
+
The rendering engine at the bottom of the TermUI stack. Screen buffers, flexbox layout, input parsing, events, styling, string utilities, and capability flags. Everything else in the framework builds on this.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,41 +10,71 @@ npm install @termuijs/core
|
|
|
10
10
|
|
|
11
11
|
## What's in the box
|
|
12
12
|
|
|
13
|
-
- **Screen**
|
|
14
|
-
- **LayoutEngine**
|
|
15
|
-
- **InputParser**
|
|
16
|
-
- **EventEmitter**
|
|
17
|
-
- **FocusManager**
|
|
18
|
-
- **Style**
|
|
19
|
-
- **LayerManager**
|
|
20
|
-
- **App**
|
|
13
|
+
- **Screen** - Double-buffered cell grid. Diffs the previous frame against the new one so only changed cells get written to stdout.
|
|
14
|
+
- **LayoutEngine** - Flexbox positioning: `flexDirection`, `flexGrow`, `flexShrink`, `alignItems`, `justifyContent`, percentage sizing. All calculated in character cells.
|
|
15
|
+
- **InputParser** - Converts raw stdin bytes into typed `KeyEvent` objects. Handles escape sequences, Ctrl combos, and multi-byte UTF-8.
|
|
16
|
+
- **EventEmitter** - Type-safe `on`, `off`, `once`, `emit`. Events bubble from the focused widget up through parents.
|
|
17
|
+
- **FocusManager** - Tab cycling between widgets, focus traps for modals, focus groups for arrow key navigation.
|
|
18
|
+
- **Style** - Colors (RGB, hex, named), border styles (single, double, rounded, bold), padding, margin.
|
|
19
|
+
- **LayerManager** - Z-indexed overlays. Modals and dropdowns render above the base layer without z-fighting.
|
|
20
|
+
- **App** - Mounts your widget tree, starts the render loop, and routes input to the focused widget.
|
|
21
|
+
- **Timer pool** - Shared tick pool for animations. All intervals share one `setInterval` at 16ms.
|
|
22
|
+
- **caps flags** - Runtime capability detection for unicode, motion, and color support.
|
|
23
|
+
- **String utilities** - `stringWidth`, `truncate`, `wordWrap`, `stripAnsi` for CJK-aware terminal text.
|
|
24
|
+
- **WCAG utilities** - `contrastRatio`, `meetsAA`, `meetsAAA` for accessible color combinations.
|
|
21
25
|
|
|
22
|
-
##
|
|
26
|
+
## Capability flags
|
|
27
|
+
|
|
28
|
+
The `caps` object reports what the current terminal environment supports:
|
|
23
29
|
|
|
24
30
|
```typescript
|
|
25
|
-
import {
|
|
31
|
+
import { caps } from '@termuijs/core'
|
|
32
|
+
|
|
33
|
+
caps.unicode // false when NO_UNICODE=1 — use ASCII fallbacks
|
|
34
|
+
caps.motion // false when NO_MOTION=1 — skip animations
|
|
35
|
+
caps.color // false when NO_COLOR=1 — skip ANSI color codes
|
|
36
|
+
```
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
These are evaluated once at module load. All built-in widgets check them automatically. Use them in your own code to provide ASCII fallbacks:
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
screen.setCell(0, 0, { char: 'H', fg: 'red' })
|
|
40
|
+
```typescript
|
|
41
|
+
import { caps } from '@termuijs/core'
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
const bullet = caps.unicode ? '●' : '*'
|
|
44
|
+
const bar = caps.unicode ? '█' : '#'
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
Set `NO_UNICODE=1 NO_MOTION=1` in CI to test ASCII output without a real terminal.
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
## String utilities
|
|
40
50
|
|
|
41
51
|
```typescript
|
|
42
|
-
import {
|
|
52
|
+
import { stringWidth, truncate, wordWrap, stripAnsi } from '@termuijs/core'
|
|
53
|
+
|
|
54
|
+
stringWidth('你好') // 4 (each CJK char = 2 columns)
|
|
55
|
+
truncate('Hello World', 8) // 'Hello W…'
|
|
56
|
+
wordWrap('The quick brown fox', 10) // wraps at word boundaries
|
|
57
|
+
stripAnsi('\x1b[32mHello\x1b[0m') // 'Hello'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## WCAG color utilities
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { contrastRatio, meetsAA, meetsAAA } from '@termuijs/core'
|
|
64
|
+
|
|
65
|
+
contrastRatio('#ffffff', '#000000') // 21
|
|
66
|
+
meetsAA('#00ff88', '#0a0a0f') // true (>= 4.5:1)
|
|
67
|
+
meetsAAA('#ffffff', '#333333') // false (< 7:1)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Event bubbling
|
|
43
71
|
|
|
72
|
+
Key events start at the focused widget and bubble up through its parents.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
44
75
|
widget.on('key', (event) => {
|
|
45
76
|
if (event.key === 'enter') {
|
|
46
77
|
event.stopPropagation()
|
|
47
|
-
// handled here, parents won't see it
|
|
48
78
|
}
|
|
49
79
|
})
|
|
50
80
|
```
|
|
@@ -55,17 +85,27 @@ Widgets clip their children by default. Nothing renders outside a widget's bound
|
|
|
55
85
|
|
|
56
86
|
```typescript
|
|
57
87
|
screen.pushClip({ x: 5, y: 5, width: 20, height: 10 })
|
|
58
|
-
// setCell calls outside this rect are silently discarded
|
|
59
88
|
screen.popClip()
|
|
60
89
|
```
|
|
61
90
|
|
|
62
|
-
##
|
|
91
|
+
## Timer pool
|
|
92
|
+
|
|
93
|
+
Use `timerPoolSubscribe` instead of `setInterval` for animations. All subscribers share one underlying timer, reducing CPU overhead.
|
|
63
94
|
|
|
64
|
-
|
|
95
|
+
```typescript
|
|
96
|
+
import { timerPoolSubscribe } from '@termuijs/core'
|
|
97
|
+
|
|
98
|
+
const unsub = timerPoolSubscribe(16, () => {
|
|
99
|
+
// runs every ~16ms (60fps)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Clean up
|
|
103
|
+
unsub()
|
|
104
|
+
```
|
|
65
105
|
|
|
66
|
-
##
|
|
106
|
+
## Documentation
|
|
67
107
|
|
|
68
|
-
Full docs at [
|
|
108
|
+
Full docs at [www.termui.io/docs/core/overview](https://www.termui.io/docs/core/overview).
|
|
69
109
|
|
|
70
110
|
## License
|
|
71
111
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,9 +21,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
App: () => App,
|
|
24
|
+
BLOCK: () => BLOCK,
|
|
24
25
|
BORDER_CHARS: () => BORDER_CHARS,
|
|
26
|
+
BOX: () => BOX,
|
|
25
27
|
BRAILLE_DOTS: () => BRAILLE_DOTS,
|
|
26
28
|
BRAILLE_OFFSET: () => BRAILLE_OFFSET,
|
|
29
|
+
BRAILLE_SPIN: () => BRAILLE_SPIN,
|
|
27
30
|
BarSets: () => BarSets,
|
|
28
31
|
BorderSets: () => BorderSets,
|
|
29
32
|
CTRL_KEYS: () => CTRL_KEYS,
|
|
@@ -44,12 +47,14 @@ __export(index_exports, {
|
|
|
44
47
|
VERTICAL_BAR_SYMBOLS: () => VERTICAL_BAR_SYMBOLS,
|
|
45
48
|
ansi: () => ansi_exports,
|
|
46
49
|
borderSize: () => borderSize,
|
|
50
|
+
caps: () => caps,
|
|
47
51
|
cellsEqual: () => cellsEqual,
|
|
48
52
|
colorToAnsiBg: () => colorToAnsiBg,
|
|
49
53
|
colorToAnsiFg: () => colorToAnsiFg,
|
|
50
54
|
colorToRgb: () => colorToRgb,
|
|
51
55
|
computeLayout: () => computeLayout,
|
|
52
56
|
containsPoint: () => containsPoint,
|
|
57
|
+
contrastRatio: () => contrastRatio,
|
|
53
58
|
createKeyEvent: () => createKeyEvent,
|
|
54
59
|
createLayoutNode: () => createLayoutNode,
|
|
55
60
|
createTestScreen: () => createTestScreen,
|
|
@@ -70,6 +75,7 @@ __export(index_exports, {
|
|
|
70
75
|
parseMouseEvent: () => parseMouseEvent,
|
|
71
76
|
percentage: () => percentage,
|
|
72
77
|
ratio: () => ratio,
|
|
78
|
+
relativeLuminance: () => relativeLuminance,
|
|
73
79
|
renderFallback: () => renderFallback,
|
|
74
80
|
shouldUseFallback: () => shouldUseFallback,
|
|
75
81
|
shrinkRect: () => shrinkRect,
|
|
@@ -84,7 +90,10 @@ __export(index_exports, {
|
|
|
84
90
|
testScreenToString: () => testScreenToString,
|
|
85
91
|
truncate: () => truncate,
|
|
86
92
|
unionRect: () => unionRect,
|
|
87
|
-
|
|
93
|
+
validateThemeContrast: () => validateThemeContrast,
|
|
94
|
+
wcagLevel: () => wcagLevel,
|
|
95
|
+
wordWrap: () => wordWrap,
|
|
96
|
+
writeClipboard: () => writeClipboard
|
|
88
97
|
});
|
|
89
98
|
module.exports = __toCommonJS(index_exports);
|
|
90
99
|
|
|
@@ -283,6 +292,56 @@ function colorToAnsiBg(color, depth) {
|
|
|
283
292
|
return "";
|
|
284
293
|
}
|
|
285
294
|
}
|
|
295
|
+
function relativeLuminance(color) {
|
|
296
|
+
const [r, g, b] = colorToRgb(color);
|
|
297
|
+
const linearize = (c) => {
|
|
298
|
+
const sRGB = c / 255;
|
|
299
|
+
return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
|
300
|
+
};
|
|
301
|
+
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
|
|
302
|
+
}
|
|
303
|
+
function contrastRatio(fg, bg) {
|
|
304
|
+
const l1 = relativeLuminance(fg);
|
|
305
|
+
const l2 = relativeLuminance(bg);
|
|
306
|
+
const lighter = Math.max(l1, l2);
|
|
307
|
+
const darker = Math.min(l1, l2);
|
|
308
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
309
|
+
}
|
|
310
|
+
function wcagLevel(ratio2, large = false) {
|
|
311
|
+
if (large) {
|
|
312
|
+
if (ratio2 >= 4.5) return "AAA";
|
|
313
|
+
if (ratio2 >= 3) return "AA";
|
|
314
|
+
return "fail";
|
|
315
|
+
}
|
|
316
|
+
if (ratio2 >= 7) return "AAA";
|
|
317
|
+
if (ratio2 >= 4.5) return "AA";
|
|
318
|
+
if (ratio2 >= 3) return "A";
|
|
319
|
+
return "fail";
|
|
320
|
+
}
|
|
321
|
+
function validateThemeContrast(theme) {
|
|
322
|
+
const failures = [];
|
|
323
|
+
const bg = theme["bg"];
|
|
324
|
+
if (!bg) return failures;
|
|
325
|
+
const bgColor = parseColor(bg);
|
|
326
|
+
const pairs = [
|
|
327
|
+
["fg on bg", theme["fg"]],
|
|
328
|
+
["primary on bg", theme["primary"]],
|
|
329
|
+
["error on bg", theme["error"]],
|
|
330
|
+
["success on bg", theme["success"]],
|
|
331
|
+
["warning on bg", theme["warning"]],
|
|
332
|
+
["muted on bg", theme["muted"]]
|
|
333
|
+
];
|
|
334
|
+
for (const [label, hex] of pairs) {
|
|
335
|
+
if (!hex) continue;
|
|
336
|
+
const fgColor = parseColor(hex);
|
|
337
|
+
const ratio2 = contrastRatio(fgColor, bgColor);
|
|
338
|
+
const level = wcagLevel(ratio2);
|
|
339
|
+
if (level !== "AAA" && level !== "AA") {
|
|
340
|
+
failures.push({ pair: label, ratio: Math.round(ratio2 * 100) / 100, level, required: "AA" });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return failures;
|
|
344
|
+
}
|
|
286
345
|
|
|
287
346
|
// src/utils/ansi.ts
|
|
288
347
|
var ansi_exports = {};
|
|
@@ -330,7 +389,8 @@ __export(ansi_exports, {
|
|
|
330
389
|
setTitle: () => setTitle,
|
|
331
390
|
showCursor: () => showCursor,
|
|
332
391
|
strikethrough: () => strikethrough,
|
|
333
|
-
underline: () => underline
|
|
392
|
+
underline: () => underline,
|
|
393
|
+
writeClipboard: () => writeClipboard
|
|
334
394
|
});
|
|
335
395
|
var CSI = "\x1B[";
|
|
336
396
|
var OSC = "\x1B]";
|
|
@@ -390,6 +450,10 @@ var resetScrollRegion = `${CSI}r`;
|
|
|
390
450
|
function setTitle(title) {
|
|
391
451
|
return `${OSC}0;${title}\x07`;
|
|
392
452
|
}
|
|
453
|
+
function writeClipboard(text, stdout = process.stdout) {
|
|
454
|
+
const encoded = Buffer.from(text, "utf8").toString("base64");
|
|
455
|
+
stdout.write(`${OSC}52;c;${encoded}\x07`);
|
|
456
|
+
}
|
|
393
457
|
|
|
394
458
|
// src/terminal/Terminal.ts
|
|
395
459
|
var Terminal = class {
|
|
@@ -409,6 +473,8 @@ var Terminal = class {
|
|
|
409
473
|
_exitHandler = null;
|
|
410
474
|
_sigintHandler = null;
|
|
411
475
|
_sigtermHandler = null;
|
|
476
|
+
_uncaughtExceptionHandler = null;
|
|
477
|
+
_unhandledRejectionHandler = null;
|
|
412
478
|
_restored = false;
|
|
413
479
|
constructor(options = {}) {
|
|
414
480
|
this.stdout = options.stdout ?? process.stdout;
|
|
@@ -509,6 +575,14 @@ var Terminal = class {
|
|
|
509
575
|
if (this._exitHandler) process.off("exit", this._exitHandler);
|
|
510
576
|
if (this._sigintHandler) process.off("SIGINT", this._sigintHandler);
|
|
511
577
|
if (this._sigtermHandler) process.off("SIGTERM", this._sigtermHandler);
|
|
578
|
+
if (this._uncaughtExceptionHandler) {
|
|
579
|
+
process.off("uncaughtException", this._uncaughtExceptionHandler);
|
|
580
|
+
this._uncaughtExceptionHandler = null;
|
|
581
|
+
}
|
|
582
|
+
if (this._unhandledRejectionHandler) {
|
|
583
|
+
process.off("unhandledRejection", this._unhandledRejectionHandler);
|
|
584
|
+
this._unhandledRejectionHandler = null;
|
|
585
|
+
}
|
|
512
586
|
if (this._resizeHandler) {
|
|
513
587
|
this.stdout.off("resize", this._resizeHandler);
|
|
514
588
|
}
|
|
@@ -546,15 +620,26 @@ var Terminal = class {
|
|
|
546
620
|
process.on("exit", this._exitHandler);
|
|
547
621
|
process.on("SIGINT", this._sigintHandler);
|
|
548
622
|
process.on("SIGTERM", this._sigtermHandler);
|
|
623
|
+
this._uncaughtExceptionHandler = (err) => {
|
|
624
|
+
this.restore();
|
|
625
|
+
process.exit(1);
|
|
626
|
+
};
|
|
627
|
+
this._unhandledRejectionHandler = () => {
|
|
628
|
+
this.restore();
|
|
629
|
+
process.exit(1);
|
|
630
|
+
};
|
|
631
|
+
process.on("uncaughtException", this._uncaughtExceptionHandler);
|
|
632
|
+
process.on("unhandledRejection", this._unhandledRejectionHandler);
|
|
549
633
|
}
|
|
550
634
|
};
|
|
551
635
|
|
|
552
636
|
// src/terminal/Screen.ts
|
|
637
|
+
var EMPTY_COLOR = Object.freeze({ type: "none" });
|
|
553
638
|
function emptyCell() {
|
|
554
639
|
return {
|
|
555
640
|
char: " ",
|
|
556
|
-
fg:
|
|
557
|
-
bg:
|
|
641
|
+
fg: EMPTY_COLOR,
|
|
642
|
+
bg: EMPTY_COLOR,
|
|
558
643
|
bold: false,
|
|
559
644
|
italic: false,
|
|
560
645
|
underline: false,
|
|
@@ -566,8 +651,8 @@ function emptyCell() {
|
|
|
566
651
|
}
|
|
567
652
|
function resetCell(cell) {
|
|
568
653
|
cell.char = " ";
|
|
569
|
-
cell.fg =
|
|
570
|
-
cell.bg =
|
|
654
|
+
cell.fg = EMPTY_COLOR;
|
|
655
|
+
cell.bg = EMPTY_COLOR;
|
|
571
656
|
cell.bold = false;
|
|
572
657
|
cell.italic = false;
|
|
573
658
|
cell.underline = false;
|
|
@@ -705,6 +790,7 @@ var Screen = class {
|
|
|
705
790
|
* Clear the back buffer to all empty cells.
|
|
706
791
|
*/
|
|
707
792
|
clear() {
|
|
793
|
+
this._clipStack = [];
|
|
708
794
|
for (let r = 0; r < this._rows; r++) {
|
|
709
795
|
for (let c = 0; c < this._cols; c++) {
|
|
710
796
|
resetCell(this.back[r][c]);
|
|
@@ -762,6 +848,7 @@ var Renderer = class {
|
|
|
762
848
|
_frameTimer = null;
|
|
763
849
|
_renderRequested = false;
|
|
764
850
|
_colorDepth;
|
|
851
|
+
_onTick = null;
|
|
765
852
|
constructor(terminal, screen, fps = 30) {
|
|
766
853
|
this._terminal = terminal;
|
|
767
854
|
this._screen = screen;
|
|
@@ -773,14 +860,16 @@ var Renderer = class {
|
|
|
773
860
|
this._fps = fps;
|
|
774
861
|
if (this._frameTimer) {
|
|
775
862
|
this.stop();
|
|
776
|
-
this.start();
|
|
863
|
+
this.start(this._onTick ?? void 0);
|
|
777
864
|
}
|
|
778
865
|
}
|
|
779
866
|
/** Start the render loop */
|
|
780
|
-
start() {
|
|
867
|
+
start(onTick) {
|
|
781
868
|
if (this._frameTimer) return;
|
|
869
|
+
this._onTick = onTick ?? null;
|
|
782
870
|
const interval = Math.floor(1e3 / this._fps);
|
|
783
871
|
this._frameTimer = setInterval(() => {
|
|
872
|
+
this._onTick?.();
|
|
784
873
|
if (this._renderRequested) {
|
|
785
874
|
this._renderRequested = false;
|
|
786
875
|
this._flush();
|
|
@@ -1046,6 +1135,42 @@ var LayerManager = class {
|
|
|
1046
1135
|
}
|
|
1047
1136
|
};
|
|
1048
1137
|
|
|
1138
|
+
// src/terminal/env-caps.ts
|
|
1139
|
+
var caps = {
|
|
1140
|
+
color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
|
|
1141
|
+
unicode: !process.env.NO_UNICODE && process.env.TERM !== "dumb",
|
|
1142
|
+
motion: !process.env.NO_MOTION && !process.env.CI,
|
|
1143
|
+
ci: !!process.env.CI
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/terminal/ascii-map.ts
|
|
1147
|
+
var BOX = {
|
|
1148
|
+
"\u250C": "+",
|
|
1149
|
+
"\u2510": "+",
|
|
1150
|
+
"\u2514": "+",
|
|
1151
|
+
"\u2518": "+",
|
|
1152
|
+
"\u2500": "-",
|
|
1153
|
+
"\u2502": "|",
|
|
1154
|
+
"\u251C": "+",
|
|
1155
|
+
"\u2524": "+",
|
|
1156
|
+
"\u252C": "+",
|
|
1157
|
+
"\u2534": "+",
|
|
1158
|
+
"\u253C": "+",
|
|
1159
|
+
"\u2550": "=",
|
|
1160
|
+
"\u2551": "|",
|
|
1161
|
+
"\u2554": "+",
|
|
1162
|
+
"\u2557": "+",
|
|
1163
|
+
"\u255A": "+",
|
|
1164
|
+
"\u255D": "+",
|
|
1165
|
+
"\u2560": "+",
|
|
1166
|
+
"\u2563": "+",
|
|
1167
|
+
"\u2566": "+",
|
|
1168
|
+
"\u2569": "+",
|
|
1169
|
+
"\u256C": "+"
|
|
1170
|
+
};
|
|
1171
|
+
var BRAILLE_SPIN = ["|", "/", "-", "\\"];
|
|
1172
|
+
var BLOCK = { full: "#", empty: " ", partial: "-" };
|
|
1173
|
+
|
|
1049
1174
|
// src/events/types.ts
|
|
1050
1175
|
function createKeyEvent(base) {
|
|
1051
1176
|
const event = {
|
|
@@ -1401,6 +1526,10 @@ var InputParser = class {
|
|
|
1401
1526
|
return;
|
|
1402
1527
|
}
|
|
1403
1528
|
if (seq.length < 20) {
|
|
1529
|
+
if (this._escapeTimeout) {
|
|
1530
|
+
clearTimeout(this._escapeTimeout);
|
|
1531
|
+
this._escapeTimeout = null;
|
|
1532
|
+
}
|
|
1404
1533
|
this._escapeTimeout = setTimeout(() => {
|
|
1405
1534
|
this._escapeBuffer = "";
|
|
1406
1535
|
this._escapeTimeout = null;
|
|
@@ -1440,6 +1569,10 @@ var InputParser = class {
|
|
|
1440
1569
|
this._escapeBuffer = "";
|
|
1441
1570
|
return;
|
|
1442
1571
|
}
|
|
1572
|
+
if (this._escapeTimeout) {
|
|
1573
|
+
clearTimeout(this._escapeTimeout);
|
|
1574
|
+
this._escapeTimeout = null;
|
|
1575
|
+
}
|
|
1443
1576
|
this._escapeTimeout = setTimeout(() => {
|
|
1444
1577
|
this._escapeBuffer = "";
|
|
1445
1578
|
this._escapeTimeout = null;
|
|
@@ -1568,7 +1701,8 @@ function createLayoutNode(id, style, children = []) {
|
|
|
1568
1701
|
id,
|
|
1569
1702
|
style,
|
|
1570
1703
|
children,
|
|
1571
|
-
computed: { x: 0, y: 0, width: 0, height: 0 }
|
|
1704
|
+
computed: { x: 0, y: 0, width: 0, height: 0 },
|
|
1705
|
+
_dirty: true
|
|
1572
1706
|
};
|
|
1573
1707
|
}
|
|
1574
1708
|
function computeLayout(root, containerWidth, containerHeight) {
|
|
@@ -1590,7 +1724,10 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1590
1724
|
node.computed.width = nodeWidth2;
|
|
1591
1725
|
node.computed.height = nodeHeight2;
|
|
1592
1726
|
}
|
|
1593
|
-
if (node.children.length === 0)
|
|
1727
|
+
if (node.children.length === 0) {
|
|
1728
|
+
node._dirty = false;
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1594
1731
|
const nodeWidth = node.computed.width;
|
|
1595
1732
|
const nodeHeight = node.computed.height;
|
|
1596
1733
|
const innerX = padding.left + border.horizontal / 2;
|
|
@@ -1711,6 +1848,7 @@ function layoutNode(node, availWidth, availHeight, precomputed = false) {
|
|
|
1711
1848
|
mainOffset += info.mainSize + gap + spaceBetween;
|
|
1712
1849
|
layoutNode(info.node, info.node.computed.width, info.node.computed.height, true);
|
|
1713
1850
|
}
|
|
1851
|
+
node._dirty = false;
|
|
1714
1852
|
}
|
|
1715
1853
|
function resolveSize(value, available) {
|
|
1716
1854
|
if (value === void 0) return void 0;
|
|
@@ -2321,6 +2459,9 @@ var App = class {
|
|
|
2321
2459
|
_options;
|
|
2322
2460
|
_mounted = false;
|
|
2323
2461
|
_exitResolve = null;
|
|
2462
|
+
_unsubKey = null;
|
|
2463
|
+
_unsubMouse = null;
|
|
2464
|
+
_widgetById = /* @__PURE__ */ new Map();
|
|
2324
2465
|
constructor(rootWidget, options = {}) {
|
|
2325
2466
|
this._rootWidget = rootWidget;
|
|
2326
2467
|
this._options = {
|
|
@@ -2365,10 +2506,11 @@ var App = class {
|
|
|
2365
2506
|
this.screen.invalidate();
|
|
2366
2507
|
this.layers.resize(cols, rows);
|
|
2367
2508
|
this.events.emit("resize", { cols, rows });
|
|
2509
|
+
this._rootWidget.markDirty?.();
|
|
2368
2510
|
this.requestRender();
|
|
2369
2511
|
});
|
|
2370
2512
|
this.input.start();
|
|
2371
|
-
this.input.onKey((rawEvent) => {
|
|
2513
|
+
this._unsubKey = this.input.onKey((rawEvent) => {
|
|
2372
2514
|
const event = createKeyEvent({
|
|
2373
2515
|
...rawEvent,
|
|
2374
2516
|
targetId: this.focus.currentId ?? void 0
|
|
@@ -2394,10 +2536,10 @@ var App = class {
|
|
|
2394
2536
|
this.events.emit("key", event);
|
|
2395
2537
|
}
|
|
2396
2538
|
});
|
|
2397
|
-
this.input.onMouse((event) => {
|
|
2539
|
+
this._unsubMouse = this.input.onMouse((event) => {
|
|
2398
2540
|
this.events.emit("mouse", event);
|
|
2399
2541
|
});
|
|
2400
|
-
this.renderer.start();
|
|
2542
|
+
this.renderer.start(() => this.requestRender());
|
|
2401
2543
|
this._rootWidget.mount?.();
|
|
2402
2544
|
this.events.emit("mount", void 0);
|
|
2403
2545
|
this.screen.invalidate();
|
|
@@ -2414,6 +2556,10 @@ var App = class {
|
|
|
2414
2556
|
this._mounted = false;
|
|
2415
2557
|
this._rootWidget.unmount?.();
|
|
2416
2558
|
this.events.emit("unmount", void 0);
|
|
2559
|
+
this._unsubKey?.();
|
|
2560
|
+
this._unsubKey = null;
|
|
2561
|
+
this._unsubMouse?.();
|
|
2562
|
+
this._unsubMouse = null;
|
|
2417
2563
|
this.renderer.stop();
|
|
2418
2564
|
this.input.stop();
|
|
2419
2565
|
this.terminal.restore();
|
|
@@ -2435,14 +2581,20 @@ var App = class {
|
|
|
2435
2581
|
}
|
|
2436
2582
|
/**
|
|
2437
2583
|
* Request a re-render on the next frame.
|
|
2584
|
+
* Skips layout + render pass when the root widget reports no dirty state.
|
|
2438
2585
|
*/
|
|
2439
2586
|
requestRender() {
|
|
2440
2587
|
if (!this._mounted) return;
|
|
2588
|
+
if (this._rootWidget.isDirty === false) {
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2441
2591
|
const layoutRoot = this._rootWidget.getLayoutNode();
|
|
2442
2592
|
computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
|
|
2443
2593
|
this._rootWidget.syncLayout?.();
|
|
2594
|
+
this._buildWidgetMap(this._rootWidget);
|
|
2444
2595
|
this.screen.clear();
|
|
2445
2596
|
this._rootWidget.render(this.screen);
|
|
2597
|
+
this._rootWidget.clearDirty?.();
|
|
2446
2598
|
this.layers.composite(this.screen);
|
|
2447
2599
|
this.renderer.requestFrame();
|
|
2448
2600
|
}
|
|
@@ -2471,10 +2623,11 @@ var App = class {
|
|
|
2471
2623
|
/**
|
|
2472
2624
|
* Build the bubble chain for keyboard events.
|
|
2473
2625
|
* Returns an array: [focused widget, parent, grandparent, ..., root]
|
|
2626
|
+
* Uses the cached _widgetById map for O(1) lookup instead of DFS.
|
|
2474
2627
|
*/
|
|
2475
2628
|
_buildBubbleChain(widgetId) {
|
|
2476
2629
|
const chain = [];
|
|
2477
|
-
const widget = this.
|
|
2630
|
+
const widget = this._widgetById.get(widgetId);
|
|
2478
2631
|
if (!widget) return chain;
|
|
2479
2632
|
let current = widget;
|
|
2480
2633
|
while (current) {
|
|
@@ -2486,19 +2639,22 @@ var App = class {
|
|
|
2486
2639
|
return chain;
|
|
2487
2640
|
}
|
|
2488
2641
|
/**
|
|
2489
|
-
*
|
|
2490
|
-
*
|
|
2642
|
+
* Rebuild the widget ID cache by walking the entire widget tree.
|
|
2643
|
+
* Called after syncLayout() so the map stays current.
|
|
2491
2644
|
*/
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2645
|
+
_buildWidgetMap(root) {
|
|
2646
|
+
this._widgetById.clear();
|
|
2647
|
+
this._walkWidget(root);
|
|
2648
|
+
}
|
|
2649
|
+
_walkWidget(widget) {
|
|
2650
|
+
if (!widget) return;
|
|
2651
|
+
if (widget.id) this._widgetById.set(widget.id, widget);
|
|
2652
|
+
const children = widget._children ?? widget.children ?? [];
|
|
2495
2653
|
if (Array.isArray(children)) {
|
|
2496
2654
|
for (const child of children) {
|
|
2497
|
-
|
|
2498
|
-
if (found) return found;
|
|
2655
|
+
this._walkWidget(child);
|
|
2499
2656
|
}
|
|
2500
2657
|
}
|
|
2501
|
-
return null;
|
|
2502
2658
|
}
|
|
2503
2659
|
};
|
|
2504
2660
|
|
|
@@ -2665,9 +2821,12 @@ function wordWrap(str, width) {
|
|
|
2665
2821
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2666
2822
|
0 && (module.exports = {
|
|
2667
2823
|
App,
|
|
2824
|
+
BLOCK,
|
|
2668
2825
|
BORDER_CHARS,
|
|
2826
|
+
BOX,
|
|
2669
2827
|
BRAILLE_DOTS,
|
|
2670
2828
|
BRAILLE_OFFSET,
|
|
2829
|
+
BRAILLE_SPIN,
|
|
2671
2830
|
BarSets,
|
|
2672
2831
|
BorderSets,
|
|
2673
2832
|
CTRL_KEYS,
|
|
@@ -2688,12 +2847,14 @@ function wordWrap(str, width) {
|
|
|
2688
2847
|
VERTICAL_BAR_SYMBOLS,
|
|
2689
2848
|
ansi,
|
|
2690
2849
|
borderSize,
|
|
2850
|
+
caps,
|
|
2691
2851
|
cellsEqual,
|
|
2692
2852
|
colorToAnsiBg,
|
|
2693
2853
|
colorToAnsiFg,
|
|
2694
2854
|
colorToRgb,
|
|
2695
2855
|
computeLayout,
|
|
2696
2856
|
containsPoint,
|
|
2857
|
+
contrastRatio,
|
|
2697
2858
|
createKeyEvent,
|
|
2698
2859
|
createLayoutNode,
|
|
2699
2860
|
createTestScreen,
|
|
@@ -2714,6 +2875,7 @@ function wordWrap(str, width) {
|
|
|
2714
2875
|
parseMouseEvent,
|
|
2715
2876
|
percentage,
|
|
2716
2877
|
ratio,
|
|
2878
|
+
relativeLuminance,
|
|
2717
2879
|
renderFallback,
|
|
2718
2880
|
shouldUseFallback,
|
|
2719
2881
|
shrinkRect,
|
|
@@ -2728,6 +2890,9 @@ function wordWrap(str, width) {
|
|
|
2728
2890
|
testScreenToString,
|
|
2729
2891
|
truncate,
|
|
2730
2892
|
unionRect,
|
|
2731
|
-
|
|
2893
|
+
validateThemeContrast,
|
|
2894
|
+
wcagLevel,
|
|
2895
|
+
wordWrap,
|
|
2896
|
+
writeClipboard
|
|
2732
2897
|
});
|
|
2733
2898
|
//# sourceMappingURL=index.cjs.map
|