@termuijs/core 0.1.0

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 ADDED
@@ -0,0 +1,2365 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ App: () => App,
24
+ BORDER_CHARS: () => BORDER_CHARS,
25
+ CTRL_KEYS: () => CTRL_KEYS,
26
+ ColorDepth: () => ColorDepth,
27
+ ESCAPE_SEQUENCES: () => ESCAPE_SEQUENCES,
28
+ EventEmitter: () => EventEmitter,
29
+ FocusManager: () => FocusManager,
30
+ InputParser: () => InputParser,
31
+ LayerManager: () => LayerManager,
32
+ Renderer: () => Renderer,
33
+ SPECIAL_KEYS: () => SPECIAL_KEYS,
34
+ Screen: () => Screen,
35
+ Terminal: () => Terminal,
36
+ ansi: () => ansi_exports,
37
+ borderSize: () => borderSize,
38
+ cellsEqual: () => cellsEqual,
39
+ colorToAnsiBg: () => colorToAnsiBg,
40
+ colorToAnsiFg: () => colorToAnsiFg,
41
+ colorToRgb: () => colorToRgb,
42
+ computeLayout: () => computeLayout,
43
+ containsPoint: () => containsPoint,
44
+ createKeyEvent: () => createKeyEvent,
45
+ createLayoutNode: () => createLayoutNode,
46
+ defaultStyle: () => defaultStyle,
47
+ detectColorDepth: () => detectColorDepth,
48
+ emptyCell: () => emptyCell,
49
+ emptyRect: () => emptyRect,
50
+ getBorderChars: () => getBorderChars,
51
+ intersectRect: () => intersectRect,
52
+ isMouseSequence: () => isMouseSequence,
53
+ mergeStyles: () => mergeStyles,
54
+ normalizeEdges: () => normalizeEdges,
55
+ parseColor: () => parseColor,
56
+ parseMouseEvent: () => parseMouseEvent,
57
+ renderFallback: () => renderFallback,
58
+ shouldUseFallback: () => shouldUseFallback,
59
+ shrinkRect: () => shrinkRect,
60
+ stringWidth: () => stringWidth,
61
+ stripAnsi: () => stripAnsi,
62
+ styleToCellAttrs: () => styleToCellAttrs,
63
+ truncate: () => truncate,
64
+ unionRect: () => unionRect,
65
+ wordWrap: () => wordWrap
66
+ });
67
+ module.exports = __toCommonJS(index_exports);
68
+
69
+ // src/style/Color.ts
70
+ var ColorDepth = /* @__PURE__ */ ((ColorDepth3) => {
71
+ ColorDepth3[ColorDepth3["None"] = 0] = "None";
72
+ ColorDepth3[ColorDepth3["Basic"] = 4] = "Basic";
73
+ ColorDepth3[ColorDepth3["Ansi256"] = 8] = "Ansi256";
74
+ ColorDepth3[ColorDepth3["TrueColor"] = 24] = "TrueColor";
75
+ return ColorDepth3;
76
+ })(ColorDepth || {});
77
+ var NAMED_TO_ANSI = {
78
+ black: 0,
79
+ red: 1,
80
+ green: 2,
81
+ yellow: 3,
82
+ blue: 4,
83
+ magenta: 5,
84
+ cyan: 6,
85
+ white: 7,
86
+ brightBlack: 8,
87
+ brightRed: 9,
88
+ brightGreen: 10,
89
+ brightYellow: 11,
90
+ brightBlue: 12,
91
+ brightMagenta: 13,
92
+ brightCyan: 14,
93
+ brightWhite: 15
94
+ };
95
+ var NAMED_TO_RGB = {
96
+ black: [0, 0, 0],
97
+ red: [170, 0, 0],
98
+ green: [0, 170, 0],
99
+ yellow: [170, 170, 0],
100
+ blue: [0, 0, 170],
101
+ magenta: [170, 0, 170],
102
+ cyan: [0, 170, 170],
103
+ white: [170, 170, 170],
104
+ brightBlack: [85, 85, 85],
105
+ brightRed: [255, 85, 85],
106
+ brightGreen: [85, 255, 85],
107
+ brightYellow: [255, 255, 85],
108
+ brightBlue: [85, 85, 255],
109
+ brightMagenta: [255, 85, 255],
110
+ brightCyan: [85, 255, 255],
111
+ brightWhite: [255, 255, 255]
112
+ };
113
+ function parseColor(input) {
114
+ if (input === "none" || input === "") {
115
+ return { type: "none" };
116
+ }
117
+ if (input in NAMED_TO_ANSI) {
118
+ return { type: "named", name: input };
119
+ }
120
+ if (input.startsWith("#")) {
121
+ let hex = input.slice(1);
122
+ if (hex.length === 3) {
123
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
124
+ }
125
+ if (hex.length === 6 && /^[0-9a-fA-F]{6}$/.test(hex)) {
126
+ return { type: "hex", hex: "#" + hex.toLowerCase() };
127
+ }
128
+ throw new Error(`Invalid hex color: ${input}`);
129
+ }
130
+ const rgbMatch = input.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/);
131
+ if (rgbMatch) {
132
+ const r = Math.min(255, parseInt(rgbMatch[1], 10));
133
+ const g = Math.min(255, parseInt(rgbMatch[2], 10));
134
+ const b = Math.min(255, parseInt(rgbMatch[3], 10));
135
+ return { type: "rgb", r, g, b };
136
+ }
137
+ const ansi256Match = input.match(/^ansi256\(\s*(\d{1,3})\s*\)$/);
138
+ if (ansi256Match) {
139
+ const code = Math.min(255, parseInt(ansi256Match[1], 10));
140
+ return { type: "ansi256", code };
141
+ }
142
+ throw new Error(`Unknown color format: ${input}`);
143
+ }
144
+ function colorToRgb(color) {
145
+ switch (color.type) {
146
+ case "none":
147
+ return [0, 0, 0];
148
+ case "named":
149
+ return NAMED_TO_RGB[color.name];
150
+ case "rgb":
151
+ return [color.r, color.g, color.b];
152
+ case "hex": {
153
+ const hex = color.hex.slice(1);
154
+ return [
155
+ parseInt(hex.slice(0, 2), 16),
156
+ parseInt(hex.slice(2, 4), 16),
157
+ parseInt(hex.slice(4, 6), 16)
158
+ ];
159
+ }
160
+ case "ansi256":
161
+ return ansi256ToRgb(color.code);
162
+ }
163
+ }
164
+ function ansi256ToRgb(code) {
165
+ if (code < 16) {
166
+ const names = Object.keys(NAMED_TO_RGB);
167
+ return NAMED_TO_RGB[names[code]];
168
+ }
169
+ if (code < 232) {
170
+ const idx = code - 16;
171
+ const b = idx % 6 * 51;
172
+ const g = Math.floor(idx / 6) % 6 * 51;
173
+ const r = Math.floor(idx / 36) * 51;
174
+ return [r, g, b];
175
+ }
176
+ const gray = (code - 232) * 10 + 8;
177
+ return [gray, gray, gray];
178
+ }
179
+ function rgbToAnsi256(r, g, b) {
180
+ if (r === g && g === b) {
181
+ if (r < 8) return 16;
182
+ if (r > 248) return 231;
183
+ return Math.round((r - 8) / 247 * 24) + 232;
184
+ }
185
+ return 16 + 36 * Math.round(r / 255 * 5) + 6 * Math.round(g / 255 * 5) + Math.round(b / 255 * 5);
186
+ }
187
+ function rgbToBasic(r, g, b) {
188
+ let minDist = Infinity;
189
+ let best = 0;
190
+ const names = Object.keys(NAMED_TO_RGB);
191
+ for (let i = 0; i < names.length; i++) {
192
+ const [cr, cg, cb] = NAMED_TO_RGB[names[i]];
193
+ const dist = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
194
+ if (dist < minDist) {
195
+ minDist = dist;
196
+ best = i;
197
+ }
198
+ }
199
+ return best;
200
+ }
201
+ function detectColorDepth() {
202
+ const env = process.env;
203
+ if (env["NO_COLOR"] !== void 0) {
204
+ return 0 /* None */;
205
+ }
206
+ if (env["FORCE_COLOR"] !== void 0) {
207
+ const level = parseInt(env["FORCE_COLOR"], 10);
208
+ if (level === 0) return 0 /* None */;
209
+ if (level === 1) return 4 /* Basic */;
210
+ if (level === 2) return 8 /* Ansi256 */;
211
+ if (level >= 3) return 24 /* TrueColor */;
212
+ }
213
+ const colorterm = env["COLORTERM"];
214
+ if (colorterm === "truecolor" || colorterm === "24bit") {
215
+ return 24 /* TrueColor */;
216
+ }
217
+ const term = env["TERM"] || "";
218
+ if (term === "dumb") {
219
+ return 0 /* None */;
220
+ }
221
+ if (term.includes("256color") || term.includes("256")) {
222
+ return 8 /* Ansi256 */;
223
+ }
224
+ if (env["TERM_PROGRAM"] === "iTerm.app" || env["TERM_PROGRAM"] === "Hyper") {
225
+ return 24 /* TrueColor */;
226
+ }
227
+ if (process.stdout?.isTTY) {
228
+ return 4 /* Basic */;
229
+ }
230
+ return 0 /* None */;
231
+ }
232
+ function colorToAnsiFg(color, depth) {
233
+ if (color.type === "none" || depth === 0 /* None */) return "";
234
+ const [r, g, b] = colorToRgb(color);
235
+ switch (depth) {
236
+ case 24 /* TrueColor */:
237
+ return `\x1B[38;2;${r};${g};${b}m`;
238
+ case 8 /* Ansi256 */:
239
+ return `\x1B[38;5;${rgbToAnsi256(r, g, b)}m`;
240
+ case 4 /* Basic */: {
241
+ const idx = rgbToBasic(r, g, b);
242
+ return idx < 8 ? `\x1B[${30 + idx}m` : `\x1B[${90 + idx - 8}m`;
243
+ }
244
+ default:
245
+ return "";
246
+ }
247
+ }
248
+ function colorToAnsiBg(color, depth) {
249
+ if (color.type === "none" || depth === 0 /* None */) return "";
250
+ const [r, g, b] = colorToRgb(color);
251
+ switch (depth) {
252
+ case 24 /* TrueColor */:
253
+ return `\x1B[48;2;${r};${g};${b}m`;
254
+ case 8 /* Ansi256 */:
255
+ return `\x1B[48;5;${rgbToAnsi256(r, g, b)}m`;
256
+ case 4 /* Basic */: {
257
+ const idx = rgbToBasic(r, g, b);
258
+ return idx < 8 ? `\x1B[${40 + idx}m` : `\x1B[${100 + idx - 8}m`;
259
+ }
260
+ default:
261
+ return "";
262
+ }
263
+ }
264
+
265
+ // src/utils/ansi.ts
266
+ var ansi_exports = {};
267
+ __export(ansi_exports, {
268
+ CSI: () => CSI,
269
+ ESC: () => ESC,
270
+ OSC: () => OSC,
271
+ beginSyncUpdate: () => beginSyncUpdate,
272
+ blink: () => blink,
273
+ bold: () => bold,
274
+ clearDown: () => clearDown,
275
+ clearLine: () => clearLine,
276
+ clearLineToEnd: () => clearLineToEnd,
277
+ clearLineToStart: () => clearLineToStart,
278
+ clearScreen: () => clearScreen,
279
+ clearUp: () => clearUp,
280
+ dim: () => dim,
281
+ disableBracketedPaste: () => disableBracketedPaste,
282
+ disableMouse: () => disableMouse,
283
+ enableBracketedPaste: () => enableBracketedPaste,
284
+ enableMouse: () => enableMouse,
285
+ endSyncUpdate: () => endSyncUpdate,
286
+ enterAltScreen: () => enterAltScreen,
287
+ exitAltScreen: () => exitAltScreen,
288
+ hideCursor: () => hideCursor,
289
+ inverse: () => inverse,
290
+ italic: () => italic,
291
+ moveDown: () => moveDown,
292
+ moveLeft: () => moveLeft,
293
+ moveRight: () => moveRight,
294
+ moveTo: () => moveTo,
295
+ moveUp: () => moveUp,
296
+ reset: () => reset,
297
+ resetBlink: () => resetBlink,
298
+ resetBold: () => resetBold,
299
+ resetDim: () => resetDim,
300
+ resetInverse: () => resetInverse,
301
+ resetItalic: () => resetItalic,
302
+ resetScrollRegion: () => resetScrollRegion,
303
+ resetStrikethrough: () => resetStrikethrough,
304
+ resetUnderline: () => resetUnderline,
305
+ restoreCursorPosition: () => restoreCursorPosition,
306
+ saveCursorPosition: () => saveCursorPosition,
307
+ setScrollRegion: () => setScrollRegion,
308
+ setTitle: () => setTitle,
309
+ showCursor: () => showCursor,
310
+ strikethrough: () => strikethrough,
311
+ underline: () => underline
312
+ });
313
+ var CSI = "\x1B[";
314
+ var OSC = "\x1B]";
315
+ var ESC = "\x1B";
316
+ var hideCursor = `${CSI}?25l`;
317
+ var showCursor = `${CSI}?25h`;
318
+ var saveCursorPosition = `${CSI}s`;
319
+ var restoreCursorPosition = `${CSI}u`;
320
+ function moveTo(col, row) {
321
+ return `${CSI}${row + 1};${col + 1}H`;
322
+ }
323
+ function moveUp(n = 1) {
324
+ return `${CSI}${n}A`;
325
+ }
326
+ function moveDown(n = 1) {
327
+ return `${CSI}${n}B`;
328
+ }
329
+ function moveRight(n = 1) {
330
+ return `${CSI}${n}C`;
331
+ }
332
+ function moveLeft(n = 1) {
333
+ return `${CSI}${n}D`;
334
+ }
335
+ var clearScreen = `${CSI}2J`;
336
+ var clearLine = `${CSI}2K`;
337
+ var clearLineToEnd = `${CSI}0K`;
338
+ var clearLineToStart = `${CSI}1K`;
339
+ var clearDown = `${CSI}J`;
340
+ var clearUp = `${CSI}1J`;
341
+ var enterAltScreen = `${CSI}?1049h`;
342
+ var exitAltScreen = `${CSI}?1049l`;
343
+ var beginSyncUpdate = `${CSI}?2026h`;
344
+ var endSyncUpdate = `${CSI}?2026l`;
345
+ var enableMouse = `${CSI}?1000h${CSI}?1002h${CSI}?1006h`;
346
+ var disableMouse = `${CSI}?1000l${CSI}?1002l${CSI}?1006l`;
347
+ var enableBracketedPaste = `${CSI}?2004h`;
348
+ var disableBracketedPaste = `${CSI}?2004l`;
349
+ var reset = `${CSI}0m`;
350
+ var bold = `${CSI}1m`;
351
+ var dim = `${CSI}2m`;
352
+ var italic = `${CSI}3m`;
353
+ var underline = `${CSI}4m`;
354
+ var blink = `${CSI}5m`;
355
+ var inverse = `${CSI}7m`;
356
+ var strikethrough = `${CSI}9m`;
357
+ var resetBold = `${CSI}22m`;
358
+ var resetDim = `${CSI}22m`;
359
+ var resetItalic = `${CSI}23m`;
360
+ var resetUnderline = `${CSI}24m`;
361
+ var resetBlink = `${CSI}25m`;
362
+ var resetInverse = `${CSI}27m`;
363
+ var resetStrikethrough = `${CSI}29m`;
364
+ function setScrollRegion(top, bottom) {
365
+ return `${CSI}${top + 1};${bottom + 1}r`;
366
+ }
367
+ var resetScrollRegion = `${CSI}r`;
368
+ function setTitle(title) {
369
+ return `${OSC}0;${title}\x07`;
370
+ }
371
+
372
+ // src/terminal/Terminal.ts
373
+ var Terminal = class {
374
+ stdout;
375
+ stdin;
376
+ colorDepth;
377
+ _cols;
378
+ _rows;
379
+ _isRawMode = false;
380
+ _isAltScreen = false;
381
+ _isMouseEnabled = false;
382
+ _resizeHandlers = [];
383
+ _cleanupHandlers = [];
384
+ _originalRawMode;
385
+ constructor(options = {}) {
386
+ this.stdout = options.stdout ?? process.stdout;
387
+ this.stdin = options.stdin ?? process.stdin;
388
+ this.colorDepth = options.colorDepth ?? detectColorDepth();
389
+ this._cols = this.stdout.columns ?? 80;
390
+ this._rows = this.stdout.rows ?? 24;
391
+ this.stdout.on("resize", () => {
392
+ this._cols = this.stdout.columns ?? 80;
393
+ this._rows = this.stdout.rows ?? 24;
394
+ for (const handler of this._resizeHandlers) {
395
+ handler(this._cols, this._rows);
396
+ }
397
+ });
398
+ this._setupCleanup();
399
+ }
400
+ /** Current terminal width in columns */
401
+ get cols() {
402
+ return this._cols;
403
+ }
404
+ /** Current terminal height in rows */
405
+ get rows() {
406
+ return this._rows;
407
+ }
408
+ /** Whether stdin is a TTY (interactive) */
409
+ isInteractive() {
410
+ return Boolean(this.stdin.isTTY) && !process.env["CI"];
411
+ }
412
+ /** Whether the terminal supports raw mode */
413
+ supportsRawMode() {
414
+ return Boolean(this.stdin.isTTY && typeof this.stdin.setRawMode === "function");
415
+ }
416
+ // ── Raw Mode ────────────────────────────────────────
417
+ enterRawMode() {
418
+ if (this._isRawMode || !this.supportsRawMode()) return;
419
+ this._originalRawMode = this.stdin.isRaw;
420
+ this.stdin.setRawMode(true);
421
+ this.stdin.resume();
422
+ this._isRawMode = true;
423
+ }
424
+ exitRawMode() {
425
+ if (!this._isRawMode) return;
426
+ this.stdin.setRawMode(this._originalRawMode ?? false);
427
+ this.stdin.pause();
428
+ this._isRawMode = false;
429
+ }
430
+ // ── Alternate Screen ────────────────────────────────
431
+ enterAltScreen() {
432
+ if (this._isAltScreen) return;
433
+ this.write(enterAltScreen);
434
+ this._isAltScreen = true;
435
+ }
436
+ exitAltScreen() {
437
+ if (!this._isAltScreen) return;
438
+ this.write(exitAltScreen);
439
+ this._isAltScreen = false;
440
+ }
441
+ // ── Mouse ───────────────────────────────────────────
442
+ enableMouse() {
443
+ if (this._isMouseEnabled) return;
444
+ this.write(enableMouse);
445
+ this._isMouseEnabled = true;
446
+ }
447
+ disableMouse() {
448
+ if (!this._isMouseEnabled) return;
449
+ this.write(disableMouse);
450
+ this._isMouseEnabled = false;
451
+ }
452
+ // ── Cursor ──────────────────────────────────────────
453
+ hideCursor() {
454
+ this.write(hideCursor);
455
+ }
456
+ showCursor() {
457
+ this.write(showCursor);
458
+ }
459
+ // ── Output ──────────────────────────────────────────
460
+ write(data) {
461
+ this.stdout.write(data);
462
+ }
463
+ // ── Resize ──────────────────────────────────────────
464
+ onResize(handler) {
465
+ this._resizeHandlers.push(handler);
466
+ return () => {
467
+ const idx = this._resizeHandlers.indexOf(handler);
468
+ if (idx >= 0) this._resizeHandlers.splice(idx, 1);
469
+ };
470
+ }
471
+ // ── Cleanup ─────────────────────────────────────────
472
+ /**
473
+ * Restore terminal to its original state.
474
+ * Called automatically on SIGINT, SIGTERM, process exit.
475
+ */
476
+ restore() {
477
+ this.disableMouse();
478
+ this.exitAltScreen();
479
+ this.exitRawMode();
480
+ this.showCursor();
481
+ this.write(reset);
482
+ }
483
+ /**
484
+ * Register a custom cleanup handler that runs on terminal restore.
485
+ */
486
+ onCleanup(handler) {
487
+ this._cleanupHandlers.push(handler);
488
+ }
489
+ _setupCleanup() {
490
+ const cleanup = () => {
491
+ for (const handler of this._cleanupHandlers) {
492
+ try {
493
+ handler();
494
+ } catch {
495
+ }
496
+ }
497
+ this.restore();
498
+ };
499
+ process.on("exit", cleanup);
500
+ process.on("SIGINT", () => {
501
+ cleanup();
502
+ process.exit(130);
503
+ });
504
+ process.on("SIGTERM", () => {
505
+ cleanup();
506
+ process.exit(143);
507
+ });
508
+ process.on("uncaughtException", (err) => {
509
+ cleanup();
510
+ console.error(err);
511
+ process.exit(1);
512
+ });
513
+ }
514
+ };
515
+
516
+ // src/terminal/Screen.ts
517
+ function emptyCell() {
518
+ return {
519
+ char: " ",
520
+ fg: { type: "none" },
521
+ bg: { type: "none" },
522
+ bold: false,
523
+ italic: false,
524
+ underline: false,
525
+ dim: false,
526
+ strikethrough: false,
527
+ inverse: false,
528
+ width: 1
529
+ };
530
+ }
531
+ function cellsEqual(a, b) {
532
+ 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);
533
+ }
534
+ function colorsEqual(a, b) {
535
+ if (a.type !== b.type) return false;
536
+ switch (a.type) {
537
+ case "none":
538
+ return true;
539
+ case "named":
540
+ return a.name === b.name;
541
+ case "ansi256":
542
+ return a.code === b.code;
543
+ case "rgb":
544
+ return a.r === b.r && a.g === b.g && a.b === b.b;
545
+ case "hex":
546
+ return a.hex === b.hex;
547
+ }
548
+ }
549
+ var Screen = class {
550
+ _cols;
551
+ _rows;
552
+ front;
553
+ back;
554
+ /**
555
+ * Stack of clipping regions. When non-empty, setCell/writeString
556
+ * only write to cells within the topmost clip rectangle.
557
+ */
558
+ _clipStack = [];
559
+ constructor(cols, rows) {
560
+ this._cols = cols;
561
+ this._rows = rows;
562
+ this.front = this._createGrid(cols, rows);
563
+ this.back = this._createGrid(cols, rows);
564
+ }
565
+ get cols() {
566
+ return this._cols;
567
+ }
568
+ get rows() {
569
+ return this._rows;
570
+ }
571
+ /**
572
+ * Push a clipping region onto the stack.
573
+ * All subsequent setCell/writeString calls will be constrained
574
+ * to cells within this rectangle. Clips are intersected with
575
+ * any parent clip already on the stack (nested clipping).
576
+ */
577
+ pushClip(region) {
578
+ if (this._clipStack.length > 0) {
579
+ const parent = this._clipStack[this._clipStack.length - 1];
580
+ const x = Math.max(region.x, parent.x);
581
+ const y = Math.max(region.y, parent.y);
582
+ const right = Math.min(region.x + region.width, parent.x + parent.width);
583
+ const bottom = Math.min(region.y + region.height, parent.y + parent.height);
584
+ if (right <= x || bottom <= y) {
585
+ this._clipStack.push({ x: 0, y: 0, width: 0, height: 0 });
586
+ } else {
587
+ this._clipStack.push({ x, y, width: right - x, height: bottom - y });
588
+ }
589
+ } else {
590
+ this._clipStack.push({ ...region });
591
+ }
592
+ }
593
+ /**
594
+ * Pop the most recent clipping region from the stack.
595
+ */
596
+ popClip() {
597
+ this._clipStack.pop();
598
+ }
599
+ /**
600
+ * Get the current active clip region, or null if no clip is active.
601
+ */
602
+ get activeClip() {
603
+ return this._clipStack.length > 0 ? this._clipStack[this._clipStack.length - 1] : null;
604
+ }
605
+ /**
606
+ * Write a cell to the back buffer at position (col, row).
607
+ */
608
+ setCell(col, row, cell) {
609
+ col = Math.floor(col);
610
+ row = Math.floor(row);
611
+ if (!(col >= 0 && col < this._cols && row >= 0 && row < this._rows)) return;
612
+ if (this._clipStack.length > 0) {
613
+ const clip = this._clipStack[this._clipStack.length - 1];
614
+ if (col < clip.x || col >= clip.x + clip.width || row < clip.y || row >= clip.y + clip.height) {
615
+ return;
616
+ }
617
+ }
618
+ const existing = this.back[row][col];
619
+ Object.assign(existing, cell);
620
+ }
621
+ /**
622
+ * Write a string to the back buffer starting at (col, row).
623
+ * Applies the provided style attributes to each character.
624
+ */
625
+ writeString(col, row, str, style = {}) {
626
+ row = Math.floor(row);
627
+ col = Math.floor(col);
628
+ if (!(row >= 0 && row < this._rows)) return;
629
+ let x = col;
630
+ for (const char of str) {
631
+ if (x >= this._cols) break;
632
+ if (x < 0) {
633
+ x++;
634
+ continue;
635
+ }
636
+ const cp = char.codePointAt(0);
637
+ const isWide = this._isWideCodePoint(cp);
638
+ const width = isWide ? 2 : 1;
639
+ this.setCell(x, row, {
640
+ char,
641
+ width,
642
+ ...style
643
+ });
644
+ if (isWide && x + 1 < this._cols) {
645
+ this.setCell(x + 1, row, {
646
+ char: "",
647
+ width: 0,
648
+ ...style
649
+ });
650
+ x += 2;
651
+ } else {
652
+ x += 1;
653
+ }
654
+ }
655
+ }
656
+ /**
657
+ * Clear the back buffer to all empty cells.
658
+ */
659
+ clear() {
660
+ for (let r = 0; r < this._rows; r++) {
661
+ for (let c = 0; c < this._cols; c++) {
662
+ this.back[r][c] = emptyCell();
663
+ }
664
+ }
665
+ }
666
+ /**
667
+ * Swap front and back buffers. Called after rendering diffs.
668
+ */
669
+ swap() {
670
+ const temp = this.front;
671
+ this.front = this.back;
672
+ this.back = temp;
673
+ }
674
+ /**
675
+ * Resize the screen. Clears both buffers.
676
+ */
677
+ resize(cols, rows) {
678
+ this._cols = cols;
679
+ this._rows = rows;
680
+ this.front = this._createGrid(cols, rows);
681
+ this.back = this._createGrid(cols, rows);
682
+ }
683
+ /**
684
+ * Clear the front buffer (marks everything as "needs redraw").
685
+ */
686
+ invalidate() {
687
+ for (let r = 0; r < this._rows; r++) {
688
+ for (let c = 0; c < this._cols; c++) {
689
+ this.front[r][c] = { ...emptyCell(), char: "\0" };
690
+ }
691
+ }
692
+ }
693
+ _createGrid(cols, rows) {
694
+ const grid = [];
695
+ for (let r = 0; r < rows; r++) {
696
+ const row = [];
697
+ for (let c = 0; c < cols; c++) {
698
+ row.push(emptyCell());
699
+ }
700
+ grid.push(row);
701
+ }
702
+ return grid;
703
+ }
704
+ _isWideCodePoint(cp) {
705
+ return cp >= 19968 && cp <= 40959 || cp >= 13312 && cp <= 19903 || cp >= 63744 && cp <= 64255 || cp >= 44032 && cp <= 55215 || cp >= 12448 && cp <= 12543 || cp >= 12288 && cp <= 12351 || cp >= 12352 && cp <= 12447 || cp >= 65281 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 128512 && cp <= 128591 || cp >= 127744 && cp <= 128511 || cp >= 128640 && cp <= 128767 || cp >= 129280 && cp <= 129535 || cp >= 131072 && cp <= 173791;
706
+ }
707
+ };
708
+
709
+ // src/terminal/Renderer.ts
710
+ var Renderer = class {
711
+ _terminal;
712
+ _screen;
713
+ _fps;
714
+ _frameTimer = null;
715
+ _renderRequested = false;
716
+ _colorDepth;
717
+ constructor(terminal, screen, fps = 30) {
718
+ this._terminal = terminal;
719
+ this._screen = screen;
720
+ this._fps = fps;
721
+ this._colorDepth = terminal.colorDepth;
722
+ }
723
+ /** Change the rendering frame rate cap */
724
+ setFPS(fps) {
725
+ this._fps = fps;
726
+ if (this._frameTimer) {
727
+ this.stop();
728
+ this.start();
729
+ }
730
+ }
731
+ /** Start the render loop */
732
+ start() {
733
+ if (this._frameTimer) return;
734
+ const interval = Math.floor(1e3 / this._fps);
735
+ this._frameTimer = setInterval(() => {
736
+ if (this._renderRequested) {
737
+ this._renderRequested = false;
738
+ this._flush();
739
+ }
740
+ }, interval);
741
+ }
742
+ /** Stop the render loop */
743
+ stop() {
744
+ if (this._frameTimer) {
745
+ clearInterval(this._frameTimer);
746
+ this._frameTimer = null;
747
+ }
748
+ }
749
+ /** Request a render on the next frame */
750
+ requestFrame() {
751
+ this._renderRequested = true;
752
+ }
753
+ /** Force an immediate render (bypass frame rate) */
754
+ renderNow() {
755
+ this._flush();
756
+ }
757
+ /**
758
+ * Full-screen clear and redraw (first render or after resize).
759
+ */
760
+ fullRender() {
761
+ this._screen.invalidate();
762
+ this._flush();
763
+ }
764
+ /**
765
+ * Core diff and flush: compare front vs back buffer,
766
+ * emit only changed cells.
767
+ */
768
+ _flush() {
769
+ const { front, back, cols, rows } = this._screen;
770
+ let output = beginSyncUpdate;
771
+ let lastRow = -1;
772
+ let lastCol = -1;
773
+ for (let r = 0; r < rows; r++) {
774
+ for (let c = 0; c < cols; c++) {
775
+ const frontCell = front[r][c];
776
+ const backCell = back[r][c];
777
+ if (cellsEqual(frontCell, backCell)) continue;
778
+ if (backCell.width === 0) continue;
779
+ if (r !== lastRow || c !== lastCol) {
780
+ output += moveTo(c, r);
781
+ }
782
+ output += this._renderCell(backCell);
783
+ lastRow = r;
784
+ lastCol = c + (backCell.width === 2 ? 2 : 1);
785
+ }
786
+ }
787
+ output += reset;
788
+ output += endSyncUpdate;
789
+ this._terminal.write(output);
790
+ this._screen.swap();
791
+ }
792
+ /**
793
+ * Generate the ANSI escape sequence to render a single cell.
794
+ */
795
+ _renderCell(cell) {
796
+ let seq = "";
797
+ seq += reset;
798
+ if (cell.bold) seq += "\x1B[1m";
799
+ if (cell.dim) seq += "\x1B[2m";
800
+ if (cell.italic) seq += "\x1B[3m";
801
+ if (cell.underline) seq += "\x1B[4m";
802
+ if (cell.strikethrough) seq += "\x1B[9m";
803
+ if (cell.inverse) seq += "\x1B[7m";
804
+ seq += colorToAnsiFg(cell.fg, this._colorDepth);
805
+ seq += colorToAnsiBg(cell.bg, this._colorDepth);
806
+ seq += cell.char || " ";
807
+ return seq;
808
+ }
809
+ };
810
+
811
+ // src/terminal/LayerManager.ts
812
+ function isCellTransparent(cell) {
813
+ return cell.char === " " && cell.fg.type === "none" && cell.bg.type === "none" && !cell.bold && !cell.italic && !cell.underline && !cell.dim && !cell.strikethrough && !cell.inverse;
814
+ }
815
+ var LayerManager = class {
816
+ _layers = /* @__PURE__ */ new Map();
817
+ _cols;
818
+ _rows;
819
+ constructor(cols, rows) {
820
+ this._cols = cols;
821
+ this._rows = rows;
822
+ }
823
+ get cols() {
824
+ return this._cols;
825
+ }
826
+ get rows() {
827
+ return this._rows;
828
+ }
829
+ /**
830
+ * Create a new overlay layer.
831
+ * @param id Unique identifier (e.g. 'modal', 'select-dropdown', 'toast')
832
+ * @param zIndex Stacking order (higher = rendered on top)
833
+ */
834
+ createLayer(id, zIndex) {
835
+ if (this._layers.has(id)) {
836
+ return this._layers.get(id);
837
+ }
838
+ const layer = {
839
+ id,
840
+ zIndex,
841
+ cells: this._createGrid(),
842
+ visible: true,
843
+ dirtyRegion: null
844
+ };
845
+ this._layers.set(id, layer);
846
+ return layer;
847
+ }
848
+ /**
849
+ * Remove an overlay layer and clean up its resources.
850
+ */
851
+ removeLayer(id) {
852
+ this._layers.delete(id);
853
+ }
854
+ /**
855
+ * Get a layer by ID.
856
+ */
857
+ getLayer(id) {
858
+ return this._layers.get(id);
859
+ }
860
+ /**
861
+ * Check if a layer exists.
862
+ */
863
+ hasLayer(id) {
864
+ return this._layers.has(id);
865
+ }
866
+ /**
867
+ * Get all layers sorted by z-index (ascending).
868
+ */
869
+ getSortedLayers() {
870
+ return Array.from(this._layers.values()).filter((l) => l.visible).sort((a, b) => a.zIndex - b.zIndex);
871
+ }
872
+ /**
873
+ * Write a cell to a specific layer.
874
+ */
875
+ setCell(layerId, col, row, cell) {
876
+ const layer = this._layers.get(layerId);
877
+ if (!layer) return;
878
+ col = Math.floor(col);
879
+ row = Math.floor(row);
880
+ if (!(col >= 0 && col < this._cols && row >= 0 && row < this._rows)) return;
881
+ const existing = layer.cells[row][col];
882
+ Object.assign(existing, cell);
883
+ this._expandDirty(layer, col, row);
884
+ }
885
+ /**
886
+ * Write a string to a specific layer at position (col, row).
887
+ */
888
+ writeString(layerId, col, row, str, style = {}) {
889
+ const layer = this._layers.get(layerId);
890
+ if (!layer) return;
891
+ row = Math.floor(row);
892
+ col = Math.floor(col);
893
+ if (!(row >= 0 && row < this._rows)) return;
894
+ let x = col;
895
+ for (const char of str) {
896
+ if (x >= this._cols) break;
897
+ if (x < 0) {
898
+ x++;
899
+ continue;
900
+ }
901
+ this.setCell(layerId, x, row, { char, width: 1, ...style });
902
+ x++;
903
+ }
904
+ }
905
+ /**
906
+ * Clear all cells in a specific layer.
907
+ */
908
+ clearLayer(id) {
909
+ const layer = this._layers.get(id);
910
+ if (!layer) return;
911
+ for (let r = 0; r < this._rows; r++) {
912
+ for (let c = 0; c < this._cols; c++) {
913
+ layer.cells[r][c] = emptyCell();
914
+ }
915
+ }
916
+ layer.dirtyRegion = null;
917
+ }
918
+ /**
919
+ * Clear all overlay layers.
920
+ */
921
+ clearAll() {
922
+ for (const layer of this._layers.values()) {
923
+ this.clearLayer(layer.id);
924
+ }
925
+ }
926
+ /**
927
+ * Composite all overlay layers onto the Screen's back buffer.
928
+ * Layers are applied in z-index order (lowest first).
929
+ * Transparent cells (empty with no colors) are skipped.
930
+ */
931
+ composite(screen) {
932
+ const sorted = this.getSortedLayers();
933
+ for (const layer of sorted) {
934
+ if (!layer.dirtyRegion) continue;
935
+ const { x: dx, y: dy, width: dw, height: dh } = layer.dirtyRegion;
936
+ for (let r = dy; r < dy + dh && r < this._rows; r++) {
937
+ for (let c = dx; c < dx + dw && c < this._cols; c++) {
938
+ const cell = layer.cells[r][c];
939
+ if (isCellTransparent(cell)) continue;
940
+ screen.setCell(c, r, {
941
+ char: cell.char,
942
+ fg: cell.fg,
943
+ bg: cell.bg,
944
+ bold: cell.bold,
945
+ italic: cell.italic,
946
+ underline: cell.underline,
947
+ dim: cell.dim,
948
+ strikethrough: cell.strikethrough,
949
+ inverse: cell.inverse,
950
+ width: cell.width
951
+ });
952
+ }
953
+ }
954
+ }
955
+ }
956
+ /**
957
+ * Resize all layers when the terminal is resized.
958
+ */
959
+ resize(cols, rows) {
960
+ this._cols = cols;
961
+ this._rows = rows;
962
+ for (const layer of this._layers.values()) {
963
+ layer.cells = this._createGrid();
964
+ layer.dirtyRegion = null;
965
+ }
966
+ }
967
+ /**
968
+ * Create an empty cell grid.
969
+ */
970
+ _createGrid() {
971
+ const grid = [];
972
+ for (let r = 0; r < this._rows; r++) {
973
+ const row = [];
974
+ for (let c = 0; c < this._cols; c++) {
975
+ row.push(emptyCell());
976
+ }
977
+ grid.push(row);
978
+ }
979
+ return grid;
980
+ }
981
+ /**
982
+ * Expand the dirty region of a layer to include the given cell.
983
+ */
984
+ _expandDirty(layer, col, row) {
985
+ if (!layer.dirtyRegion) {
986
+ layer.dirtyRegion = { x: col, y: row, width: 1, height: 1 };
987
+ return;
988
+ }
989
+ const r = layer.dirtyRegion;
990
+ const newX = Math.min(r.x, col);
991
+ const newY = Math.min(r.y, row);
992
+ const newRight = Math.max(r.x + r.width, col + 1);
993
+ const newBottom = Math.max(r.y + r.height, row + 1);
994
+ r.x = newX;
995
+ r.y = newY;
996
+ r.width = newRight - newX;
997
+ r.height = newBottom - newY;
998
+ }
999
+ };
1000
+
1001
+ // src/events/types.ts
1002
+ function createKeyEvent(base) {
1003
+ const event = {
1004
+ ...base,
1005
+ _propagationStopped: false,
1006
+ _defaultPrevented: false,
1007
+ stopPropagation() {
1008
+ this._propagationStopped = true;
1009
+ },
1010
+ preventDefault() {
1011
+ this._defaultPrevented = true;
1012
+ }
1013
+ };
1014
+ return event;
1015
+ }
1016
+
1017
+ // src/input/KeyMap.ts
1018
+ var ESCAPE_SEQUENCES = {
1019
+ // Arrow keys
1020
+ "\x1B[A": "up",
1021
+ "\x1B[B": "down",
1022
+ "\x1B[C": "right",
1023
+ "\x1B[D": "left",
1024
+ // Shift+Arrow
1025
+ "\x1B[1;2A": "shift+up",
1026
+ "\x1B[1;2B": "shift+down",
1027
+ "\x1B[1;2C": "shift+right",
1028
+ "\x1B[1;2D": "shift+left",
1029
+ // Ctrl+Arrow
1030
+ "\x1B[1;5A": "ctrl+up",
1031
+ "\x1B[1;5B": "ctrl+down",
1032
+ "\x1B[1;5C": "ctrl+right",
1033
+ "\x1B[1;5D": "ctrl+left",
1034
+ // Alt+Arrow
1035
+ "\x1B[1;3A": "alt+up",
1036
+ "\x1B[1;3B": "alt+down",
1037
+ "\x1B[1;3C": "alt+right",
1038
+ "\x1B[1;3D": "alt+left",
1039
+ // Home/End/Insert/Delete/PageUp/PageDown
1040
+ "\x1B[H": "home",
1041
+ "\x1B[F": "end",
1042
+ "\x1B[2~": "insert",
1043
+ "\x1B[3~": "delete",
1044
+ "\x1B[5~": "pageup",
1045
+ "\x1B[6~": "pagedown",
1046
+ // Alternate Home/End (some terminals)
1047
+ "\x1B[1~": "home",
1048
+ "\x1B[4~": "end",
1049
+ "\x1BOH": "home",
1050
+ "\x1BOF": "end",
1051
+ // Function keys
1052
+ "\x1BOP": "f1",
1053
+ "\x1BOQ": "f2",
1054
+ "\x1BOR": "f3",
1055
+ "\x1BOS": "f4",
1056
+ "\x1B[15~": "f5",
1057
+ "\x1B[17~": "f6",
1058
+ "\x1B[18~": "f7",
1059
+ "\x1B[19~": "f8",
1060
+ "\x1B[20~": "f9",
1061
+ "\x1B[21~": "f10",
1062
+ "\x1B[23~": "f11",
1063
+ "\x1B[24~": "f12",
1064
+ // Special keys
1065
+ "\x1B[Z": "shift+tab"
1066
+ };
1067
+ var CTRL_KEYS = {
1068
+ 1: "a",
1069
+ 2: "b",
1070
+ 3: "c",
1071
+ 4: "d",
1072
+ 5: "e",
1073
+ 6: "f",
1074
+ 7: "g",
1075
+ 8: "backspace",
1076
+ 9: "tab",
1077
+ 10: "enter",
1078
+ 11: "k",
1079
+ 12: "l",
1080
+ 13: "enter",
1081
+ 14: "n",
1082
+ 15: "o",
1083
+ 16: "p",
1084
+ 17: "q",
1085
+ 18: "r",
1086
+ 19: "s",
1087
+ 20: "t",
1088
+ 21: "u",
1089
+ 22: "v",
1090
+ 23: "w",
1091
+ 24: "x",
1092
+ 25: "y",
1093
+ 26: "z"
1094
+ };
1095
+ var SPECIAL_KEYS = {
1096
+ 27: "escape",
1097
+ 127: "backspace",
1098
+ 9: "tab",
1099
+ 13: "enter",
1100
+ 10: "enter",
1101
+ 32: "space"
1102
+ };
1103
+
1104
+ // src/input/MouseParser.ts
1105
+ function parseMouseEvent(data) {
1106
+ const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
1107
+ if (!match) return null;
1108
+ const cb = parseInt(match[1], 10);
1109
+ const cx = parseInt(match[2], 10) - 1;
1110
+ const cy = parseInt(match[3], 10) - 1;
1111
+ const isRelease = match[4] === "m";
1112
+ let button;
1113
+ let type;
1114
+ let scrollDelta;
1115
+ const buttonBits = cb & 3;
1116
+ const motion = (cb & 32) !== 0;
1117
+ const isScroll = (cb & 64) !== 0;
1118
+ if (isScroll) {
1119
+ button = "none";
1120
+ type = "scroll";
1121
+ scrollDelta = buttonBits === 0 ? -1 : 1;
1122
+ } else if (motion) {
1123
+ type = "mousemove";
1124
+ button = decodeButton(buttonBits);
1125
+ } else if (isRelease) {
1126
+ type = "mouseup";
1127
+ button = decodeButton(buttonBits);
1128
+ } else {
1129
+ type = "mousedown";
1130
+ button = decodeButton(buttonBits);
1131
+ }
1132
+ return {
1133
+ x: cx,
1134
+ y: cy,
1135
+ button,
1136
+ type,
1137
+ scrollDelta
1138
+ };
1139
+ }
1140
+ function decodeButton(bits) {
1141
+ switch (bits) {
1142
+ case 0:
1143
+ return "left";
1144
+ case 1:
1145
+ return "middle";
1146
+ case 2:
1147
+ return "right";
1148
+ default:
1149
+ return "none";
1150
+ }
1151
+ }
1152
+ function isMouseSequence(data) {
1153
+ return data.startsWith("\x1B[<");
1154
+ }
1155
+
1156
+ // src/events/EventEmitter.ts
1157
+ var EventEmitter = class {
1158
+ _handlers = /* @__PURE__ */ new Map();
1159
+ _onceHandlers = /* @__PURE__ */ new Map();
1160
+ /**
1161
+ * Subscribe to an event.
1162
+ * @returns Unsubscribe function.
1163
+ */
1164
+ on(event, handler) {
1165
+ if (!this._handlers.has(event)) {
1166
+ this._handlers.set(event, /* @__PURE__ */ new Set());
1167
+ }
1168
+ this._handlers.get(event).add(handler);
1169
+ return () => this.off(event, handler);
1170
+ }
1171
+ /**
1172
+ * Subscribe to an event, but only fire once.
1173
+ */
1174
+ once(event, handler) {
1175
+ if (!this._onceHandlers.has(event)) {
1176
+ this._onceHandlers.set(event, /* @__PURE__ */ new Set());
1177
+ }
1178
+ this._onceHandlers.get(event).add(handler);
1179
+ return () => {
1180
+ this._onceHandlers.get(event)?.delete(handler);
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Unsubscribe from an event.
1185
+ */
1186
+ off(event, handler) {
1187
+ this._handlers.get(event)?.delete(handler);
1188
+ this._onceHandlers.get(event)?.delete(handler);
1189
+ }
1190
+ /**
1191
+ * Emit an event to all subscribed handlers.
1192
+ */
1193
+ emit(event, data) {
1194
+ const handlers = this._handlers.get(event);
1195
+ if (handlers) {
1196
+ for (const handler of handlers) {
1197
+ handler(data);
1198
+ }
1199
+ }
1200
+ const onceHandlers = this._onceHandlers.get(event);
1201
+ if (onceHandlers) {
1202
+ for (const handler of onceHandlers) {
1203
+ handler(data);
1204
+ }
1205
+ onceHandlers.clear();
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Remove all handlers for a specific event, or all events if no event specified.
1210
+ */
1211
+ removeAll(event) {
1212
+ if (event) {
1213
+ this._handlers.delete(event);
1214
+ this._onceHandlers.delete(event);
1215
+ } else {
1216
+ this._handlers.clear();
1217
+ this._onceHandlers.clear();
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Check if there are any handlers for an event.
1222
+ */
1223
+ hasListeners(event) {
1224
+ return (this._handlers.get(event)?.size ?? 0) > 0 || (this._onceHandlers.get(event)?.size ?? 0) > 0;
1225
+ }
1226
+ };
1227
+
1228
+ // src/input/InputParser.ts
1229
+ var InputParser = class {
1230
+ _events = new EventEmitter();
1231
+ _stdin;
1232
+ _handler = null;
1233
+ _escapeTimeout = null;
1234
+ _escapeBuffer = "";
1235
+ constructor(stdin) {
1236
+ this._stdin = stdin;
1237
+ }
1238
+ /** Subscribe to key events */
1239
+ onKey(handler) {
1240
+ return this._events.on("key", handler);
1241
+ }
1242
+ /** Subscribe to mouse events */
1243
+ onMouse(handler) {
1244
+ return this._events.on("mouse", handler);
1245
+ }
1246
+ /** Start listening for input */
1247
+ start() {
1248
+ if (this._handler) return;
1249
+ this._handler = (data) => {
1250
+ this._processInput(data);
1251
+ };
1252
+ this._stdin.on("data", this._handler);
1253
+ }
1254
+ /** Stop listening for input */
1255
+ stop() {
1256
+ if (this._handler) {
1257
+ this._stdin.off("data", this._handler);
1258
+ this._handler = null;
1259
+ }
1260
+ if (this._escapeTimeout) {
1261
+ clearTimeout(this._escapeTimeout);
1262
+ this._escapeTimeout = null;
1263
+ }
1264
+ this._escapeBuffer = "";
1265
+ }
1266
+ /**
1267
+ * Process a chunk of raw input bytes.
1268
+ */
1269
+ _processInput(data) {
1270
+ const str = data.toString("utf8");
1271
+ if (this._escapeBuffer) {
1272
+ this._escapeBuffer += str;
1273
+ if (this._escapeTimeout) {
1274
+ clearTimeout(this._escapeTimeout);
1275
+ this._escapeTimeout = null;
1276
+ }
1277
+ this._tryParseEscape(data);
1278
+ return;
1279
+ }
1280
+ if (str.startsWith("\x1B") && str.length === 1) {
1281
+ this._escapeBuffer = str;
1282
+ this._escapeTimeout = setTimeout(() => {
1283
+ this._events.emit("key", createKeyEvent({
1284
+ key: "escape",
1285
+ raw: Buffer.from(this._escapeBuffer),
1286
+ ctrl: false,
1287
+ alt: false,
1288
+ shift: false
1289
+ }));
1290
+ this._escapeBuffer = "";
1291
+ this._escapeTimeout = null;
1292
+ }, 50);
1293
+ return;
1294
+ }
1295
+ if (str.startsWith("\x1B")) {
1296
+ this._escapeBuffer = str;
1297
+ this._tryParseEscape(data);
1298
+ return;
1299
+ }
1300
+ for (let i = 0; i < str.length; i++) {
1301
+ const ch = str[i];
1302
+ const code = str.charCodeAt(i);
1303
+ const raw = Buffer.from(ch, "utf8");
1304
+ if (code >= 1 && code <= 26) {
1305
+ const keyName = CTRL_KEYS[code];
1306
+ const isCtrl = code !== 9 && code !== 13 && code !== 10;
1307
+ this._events.emit("key", createKeyEvent({
1308
+ key: keyName || String.fromCharCode(code + 96),
1309
+ raw,
1310
+ ctrl: isCtrl,
1311
+ alt: false,
1312
+ shift: false
1313
+ }));
1314
+ continue;
1315
+ }
1316
+ if (code in SPECIAL_KEYS) {
1317
+ this._events.emit("key", createKeyEvent({
1318
+ key: SPECIAL_KEYS[code],
1319
+ raw,
1320
+ ctrl: false,
1321
+ alt: false,
1322
+ shift: false
1323
+ }));
1324
+ continue;
1325
+ }
1326
+ if (code >= 32) {
1327
+ this._events.emit("key", createKeyEvent({
1328
+ key: ch,
1329
+ raw,
1330
+ ctrl: false,
1331
+ alt: false,
1332
+ shift: ch !== ch.toLowerCase() && ch === ch.toUpperCase()
1333
+ }));
1334
+ }
1335
+ }
1336
+ }
1337
+ /**
1338
+ * Try to parse buffered escape sequence.
1339
+ */
1340
+ _tryParseEscape(rawData) {
1341
+ const seq = this._escapeBuffer;
1342
+ if (isMouseSequence(seq)) {
1343
+ const mouseEvt = parseMouseEvent(seq);
1344
+ if (mouseEvt) {
1345
+ this._events.emit("mouse", mouseEvt);
1346
+ this._escapeBuffer = "";
1347
+ return;
1348
+ }
1349
+ if (seq.length < 20) {
1350
+ this._escapeTimeout = setTimeout(() => {
1351
+ this._escapeBuffer = "";
1352
+ this._escapeTimeout = null;
1353
+ }, 100);
1354
+ return;
1355
+ }
1356
+ }
1357
+ if (seq in ESCAPE_SEQUENCES) {
1358
+ const keyName = ESCAPE_SEQUENCES[seq];
1359
+ const isShift = keyName.startsWith("shift+");
1360
+ const isCtrl = keyName.startsWith("ctrl+");
1361
+ const isAlt = keyName.startsWith("alt+");
1362
+ const cleanKey = keyName.replace(/^(shift|ctrl|alt)\+/, "");
1363
+ this._events.emit("key", createKeyEvent({
1364
+ key: cleanKey,
1365
+ raw: rawData,
1366
+ ctrl: isCtrl,
1367
+ alt: isAlt,
1368
+ shift: isShift
1369
+ }));
1370
+ this._escapeBuffer = "";
1371
+ return;
1372
+ }
1373
+ if (seq.length === 2 && seq[0] === "\x1B") {
1374
+ const ch = seq[1];
1375
+ this._events.emit("key", createKeyEvent({
1376
+ key: ch,
1377
+ raw: rawData,
1378
+ ctrl: false,
1379
+ alt: true,
1380
+ shift: ch !== ch.toLowerCase() && ch === ch.toUpperCase()
1381
+ }));
1382
+ this._escapeBuffer = "";
1383
+ return;
1384
+ }
1385
+ if (seq.length > 20) {
1386
+ this._escapeBuffer = "";
1387
+ return;
1388
+ }
1389
+ this._escapeTimeout = setTimeout(() => {
1390
+ this._escapeBuffer = "";
1391
+ this._escapeTimeout = null;
1392
+ }, 100);
1393
+ }
1394
+ };
1395
+
1396
+ // src/style/Style.ts
1397
+ function normalizeEdges(value) {
1398
+ if (value === void 0) return { top: 0, right: 0, bottom: 0, left: 0 };
1399
+ if (typeof value === "number") {
1400
+ return { top: value, right: value, bottom: value, left: value };
1401
+ }
1402
+ return {
1403
+ top: value.top ?? 0,
1404
+ right: value.right ?? 0,
1405
+ bottom: value.bottom ?? 0,
1406
+ left: value.left ?? 0
1407
+ };
1408
+ }
1409
+ function mergeStyles(base, override) {
1410
+ const result = { ...base };
1411
+ for (const key of Object.keys(override)) {
1412
+ const val = override[key];
1413
+ if (val !== void 0) {
1414
+ result[key] = val;
1415
+ }
1416
+ }
1417
+ return result;
1418
+ }
1419
+ function defaultStyle() {
1420
+ return {
1421
+ visible: true,
1422
+ flexDirection: "column",
1423
+ justifyContent: "flex-start",
1424
+ alignItems: "stretch",
1425
+ flexGrow: 0,
1426
+ flexShrink: 1,
1427
+ flexWrap: "nowrap",
1428
+ overflow: "hidden",
1429
+ gap: 0
1430
+ };
1431
+ }
1432
+ function styleToCellAttrs(style) {
1433
+ return {
1434
+ fg: style.fg ?? { type: "none" },
1435
+ bg: style.bg ?? { type: "none" },
1436
+ bold: style.bold ?? false,
1437
+ italic: style.italic ?? false,
1438
+ underline: style.underline ?? false,
1439
+ dim: style.dim ?? false,
1440
+ strikethrough: style.strikethrough ?? false,
1441
+ inverse: style.inverse ?? false
1442
+ };
1443
+ }
1444
+
1445
+ // src/style/Border.ts
1446
+ var BORDER_CHARS = {
1447
+ single: {
1448
+ topLeft: "\u250C",
1449
+ top: "\u2500",
1450
+ topRight: "\u2510",
1451
+ right: "\u2502",
1452
+ bottomRight: "\u2518",
1453
+ bottom: "\u2500",
1454
+ bottomLeft: "\u2514",
1455
+ left: "\u2502"
1456
+ },
1457
+ double: {
1458
+ topLeft: "\u2554",
1459
+ top: "\u2550",
1460
+ topRight: "\u2557",
1461
+ right: "\u2551",
1462
+ bottomRight: "\u255D",
1463
+ bottom: "\u2550",
1464
+ bottomLeft: "\u255A",
1465
+ left: "\u2551"
1466
+ },
1467
+ round: {
1468
+ topLeft: "\u256D",
1469
+ top: "\u2500",
1470
+ topRight: "\u256E",
1471
+ right: "\u2502",
1472
+ bottomRight: "\u256F",
1473
+ bottom: "\u2500",
1474
+ bottomLeft: "\u2570",
1475
+ left: "\u2502"
1476
+ },
1477
+ heavy: {
1478
+ topLeft: "\u250F",
1479
+ top: "\u2501",
1480
+ topRight: "\u2513",
1481
+ right: "\u2503",
1482
+ bottomRight: "\u251B",
1483
+ bottom: "\u2501",
1484
+ bottomLeft: "\u2517",
1485
+ left: "\u2503"
1486
+ },
1487
+ dashed: {
1488
+ topLeft: "\u250C",
1489
+ top: "\u2504",
1490
+ topRight: "\u2510",
1491
+ right: "\u2506",
1492
+ bottomRight: "\u2518",
1493
+ bottom: "\u2504",
1494
+ bottomLeft: "\u2514",
1495
+ left: "\u2506"
1496
+ }
1497
+ };
1498
+ function getBorderChars(style, customChars) {
1499
+ if (style === "none") return null;
1500
+ if (style === "custom") {
1501
+ const base = BORDER_CHARS.single;
1502
+ return { ...base, ...customChars };
1503
+ }
1504
+ return BORDER_CHARS[style];
1505
+ }
1506
+ function borderSize(style) {
1507
+ if (style === "none") return { horizontal: 0, vertical: 0 };
1508
+ return { horizontal: 2, vertical: 2 };
1509
+ }
1510
+
1511
+ // src/layout/LayoutEngine.ts
1512
+ function createLayoutNode(id, style, children = []) {
1513
+ return {
1514
+ id,
1515
+ style,
1516
+ children,
1517
+ computed: { x: 0, y: 0, width: 0, height: 0 }
1518
+ };
1519
+ }
1520
+ function computeLayout(root, containerWidth, containerHeight) {
1521
+ root.computed = { x: 0, y: 0, width: containerWidth, height: containerHeight };
1522
+ layoutNode(root, containerWidth, containerHeight);
1523
+ }
1524
+ function layoutNode(node, availWidth, availHeight, precomputed = false) {
1525
+ const style = node.style;
1526
+ const padding = normalizeEdges(style.padding);
1527
+ const margin = normalizeEdges(style.margin);
1528
+ const border = borderSize(style.border ?? "none");
1529
+ if (!precomputed) {
1530
+ let nodeWidth2 = resolveSize(style.width, availWidth);
1531
+ let nodeHeight2 = resolveSize(style.height, availHeight);
1532
+ if (nodeWidth2 === void 0) nodeWidth2 = availWidth - margin.left - margin.right;
1533
+ if (nodeHeight2 === void 0) nodeHeight2 = availHeight - margin.top - margin.bottom;
1534
+ nodeWidth2 = clampSize(nodeWidth2, style.minWidth, style.maxWidth);
1535
+ nodeHeight2 = clampSize(nodeHeight2, style.minHeight, style.maxHeight);
1536
+ node.computed.width = nodeWidth2;
1537
+ node.computed.height = nodeHeight2;
1538
+ }
1539
+ if (node.children.length === 0) return;
1540
+ const nodeWidth = node.computed.width;
1541
+ const nodeHeight = node.computed.height;
1542
+ const innerX = padding.left + border.horizontal / 2;
1543
+ const innerY = padding.top + border.vertical / 2;
1544
+ const innerWidth = Math.max(0, nodeWidth - padding.left - padding.right - border.horizontal);
1545
+ const innerHeight = Math.max(0, nodeHeight - padding.top - padding.bottom - border.vertical);
1546
+ const direction = style.flexDirection ?? "column";
1547
+ const isRow = direction === "row";
1548
+ const gap = style.gap ?? 0;
1549
+ const childInfos = [];
1550
+ let totalFixed = 0;
1551
+ let totalGrow = 0;
1552
+ let totalShrink = 0;
1553
+ for (const child of node.children) {
1554
+ if (child.style.visible === false) continue;
1555
+ const childMargin = normalizeEdges(child.style.margin);
1556
+ const childBorder = borderSize(child.style.border ?? "none");
1557
+ const grow = child.style.flexGrow ?? 0;
1558
+ const shrink = child.style.flexShrink ?? 1;
1559
+ let mainSize;
1560
+ let crossSize;
1561
+ if (isRow) {
1562
+ mainSize = resolveSize(child.style.width, innerWidth) ?? 0;
1563
+ crossSize = resolveSize(child.style.height, innerHeight) ?? innerHeight;
1564
+ mainSize += childMargin.left + childMargin.right;
1565
+ crossSize = clampSize(crossSize, child.style.minHeight, child.style.maxHeight);
1566
+ } else {
1567
+ mainSize = resolveSize(child.style.height, innerHeight) ?? 0;
1568
+ crossSize = resolveSize(child.style.width, innerWidth) ?? innerWidth;
1569
+ mainSize += childMargin.top + childMargin.bottom;
1570
+ crossSize = clampSize(crossSize, child.style.minWidth, child.style.maxWidth);
1571
+ }
1572
+ totalFixed += mainSize;
1573
+ totalGrow += grow;
1574
+ totalShrink += shrink;
1575
+ childInfos.push({ node: child, mainSize, crossSize, flexGrow: grow, flexShrink: shrink, margin: childMargin });
1576
+ }
1577
+ const totalGaps = Math.max(0, childInfos.length - 1) * gap;
1578
+ const mainAvail = isRow ? innerWidth : innerHeight;
1579
+ const freeSpace = mainAvail - totalFixed - totalGaps;
1580
+ if (freeSpace > 0 && totalGrow > 0) {
1581
+ for (const info of childInfos) {
1582
+ if (info.flexGrow > 0) {
1583
+ info.mainSize += info.flexGrow / totalGrow * freeSpace;
1584
+ }
1585
+ }
1586
+ } else if (freeSpace < 0 && totalShrink > 0) {
1587
+ for (const info of childInfos) {
1588
+ if (info.flexShrink > 0) {
1589
+ info.mainSize += info.flexShrink / totalShrink * freeSpace;
1590
+ info.mainSize = Math.max(0, info.mainSize);
1591
+ }
1592
+ }
1593
+ }
1594
+ const totalMainUsed = childInfos.reduce((sum, i) => sum + i.mainSize, 0) + totalGaps;
1595
+ const remainingSpace = Math.max(0, mainAvail - totalMainUsed);
1596
+ let mainOffset;
1597
+ let spaceBetween = 0;
1598
+ const justify = style.justifyContent ?? "flex-start";
1599
+ switch (justify) {
1600
+ case "flex-start":
1601
+ mainOffset = 0;
1602
+ break;
1603
+ case "flex-end":
1604
+ mainOffset = remainingSpace;
1605
+ break;
1606
+ case "center":
1607
+ mainOffset = remainingSpace / 2;
1608
+ break;
1609
+ case "space-between":
1610
+ mainOffset = 0;
1611
+ spaceBetween = childInfos.length > 1 ? remainingSpace / (childInfos.length - 1) : 0;
1612
+ break;
1613
+ case "space-around":
1614
+ spaceBetween = childInfos.length > 0 ? remainingSpace / childInfos.length : 0;
1615
+ mainOffset = spaceBetween / 2;
1616
+ break;
1617
+ default:
1618
+ mainOffset = 0;
1619
+ }
1620
+ const crossAvail = isRow ? innerHeight : innerWidth;
1621
+ const align = style.alignItems ?? "stretch";
1622
+ for (const info of childInfos) {
1623
+ let crossOffset;
1624
+ let finalCrossSize = info.crossSize;
1625
+ switch (align) {
1626
+ case "flex-start":
1627
+ crossOffset = 0;
1628
+ break;
1629
+ case "flex-end":
1630
+ crossOffset = crossAvail - finalCrossSize;
1631
+ break;
1632
+ case "center":
1633
+ crossOffset = (crossAvail - finalCrossSize) / 2;
1634
+ break;
1635
+ case "stretch":
1636
+ crossOffset = 0;
1637
+ finalCrossSize = crossAvail;
1638
+ break;
1639
+ default:
1640
+ crossOffset = 0;
1641
+ }
1642
+ if (isRow) {
1643
+ info.node.computed = {
1644
+ x: node.computed.x + innerX + mainOffset + info.margin.left,
1645
+ y: node.computed.y + innerY + crossOffset + info.margin.top,
1646
+ width: Math.max(0, info.mainSize - info.margin.left - info.margin.right),
1647
+ height: Math.max(0, finalCrossSize - info.margin.top - info.margin.bottom)
1648
+ };
1649
+ } else {
1650
+ info.node.computed = {
1651
+ x: node.computed.x + innerX + crossOffset + info.margin.left,
1652
+ y: node.computed.y + innerY + mainOffset + info.margin.top,
1653
+ width: Math.max(0, finalCrossSize - info.margin.left - info.margin.right),
1654
+ height: Math.max(0, info.mainSize - info.margin.top - info.margin.bottom)
1655
+ };
1656
+ }
1657
+ mainOffset += info.mainSize + gap + spaceBetween;
1658
+ layoutNode(info.node, info.node.computed.width, info.node.computed.height, true);
1659
+ }
1660
+ }
1661
+ function resolveSize(value, available) {
1662
+ if (value === void 0) return void 0;
1663
+ if (typeof value === "number") return value;
1664
+ if (typeof value === "string" && value.endsWith("%")) {
1665
+ const pct = parseFloat(value) / 100;
1666
+ return Math.floor(available * pct);
1667
+ }
1668
+ return void 0;
1669
+ }
1670
+ function clampSize(value, min, max) {
1671
+ let result = value;
1672
+ if (min !== void 0) result = Math.max(result, min);
1673
+ if (max !== void 0) result = Math.min(result, max);
1674
+ return result;
1675
+ }
1676
+
1677
+ // src/layout/Rect.ts
1678
+ function emptyRect() {
1679
+ return { x: 0, y: 0, width: 0, height: 0 };
1680
+ }
1681
+ function containsPoint(rect, x, y) {
1682
+ return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
1683
+ }
1684
+ function shrinkRect(rect, top, right, bottom, left) {
1685
+ return {
1686
+ x: rect.x + left,
1687
+ y: rect.y + top,
1688
+ width: Math.max(0, rect.width - left - right),
1689
+ height: Math.max(0, rect.height - top - bottom)
1690
+ };
1691
+ }
1692
+ function intersectRect(a, b) {
1693
+ const x = Math.max(a.x, b.x);
1694
+ const y = Math.max(a.y, b.y);
1695
+ const r = Math.min(a.x + a.width, b.x + b.width);
1696
+ const bot = Math.min(a.y + a.height, b.y + b.height);
1697
+ if (r <= x || bot <= y) return null;
1698
+ return { x, y, width: r - x, height: bot - y };
1699
+ }
1700
+ function unionRect(a, b) {
1701
+ const x = Math.min(a.x, b.x);
1702
+ const y = Math.min(a.y, b.y);
1703
+ const r = Math.max(a.x + a.width, b.x + b.width);
1704
+ const bot = Math.max(a.y + a.height, b.y + b.height);
1705
+ return { x, y, width: r - x, height: bot - y };
1706
+ }
1707
+
1708
+ // src/events/FocusManager.ts
1709
+ var FocusManager = class {
1710
+ _focusables = [];
1711
+ _currentIndex = -1;
1712
+ _events = new EventEmitter();
1713
+ /**
1714
+ * Stack of trap container IDs. When non-empty, only focusables
1715
+ * that belong to the topmost trap container are reachable via Tab.
1716
+ */
1717
+ _trapStack = [];
1718
+ /**
1719
+ * Map of container ID → child widget IDs that belong to it.
1720
+ * Used for trap membership lookup.
1721
+ */
1722
+ _containerMembers = /* @__PURE__ */ new Map();
1723
+ /**
1724
+ * Named focus groups. Arrow keys move within a group, Tab moves between groups.
1725
+ * Maps groupId → ordered list of widget IDs.
1726
+ */
1727
+ _groups = /* @__PURE__ */ new Map();
1728
+ /** Currently focused widget ID, or null if none */
1729
+ get currentId() {
1730
+ if (this._currentIndex < 0 || this._currentIndex >= this._focusables.length) {
1731
+ return null;
1732
+ }
1733
+ return this._focusables[this._currentIndex].id;
1734
+ }
1735
+ /** Subscribe to focus/blur events */
1736
+ on(event, handler) {
1737
+ return this._events.on(event, handler);
1738
+ }
1739
+ /**
1740
+ * Register a focusable widget.
1741
+ * Widgets are ordered by tabIndex (ascending), then insertion order.
1742
+ */
1743
+ register(focusable) {
1744
+ this._focusables.push(focusable);
1745
+ this._focusables.sort((a, b) => a.tabIndex - b.tabIndex);
1746
+ if (this._currentIndex < 0 && focusable.focusable) {
1747
+ this._currentIndex = this._focusables.indexOf(focusable);
1748
+ this._events.emit("focus", { targetId: focusable.id, type: "focus" });
1749
+ }
1750
+ }
1751
+ /**
1752
+ * Unregister a focusable widget.
1753
+ */
1754
+ unregister(id) {
1755
+ const idx = this._focusables.findIndex((f) => f.id === id);
1756
+ if (idx < 0) return;
1757
+ const wasFocused = idx === this._currentIndex;
1758
+ this._focusables.splice(idx, 1);
1759
+ if (wasFocused) {
1760
+ this._events.emit("blur", { targetId: id, type: "blur" });
1761
+ if (this._focusables.length > 0) {
1762
+ this._currentIndex = Math.min(this._currentIndex, this._focusables.length - 1);
1763
+ this._events.emit("focus", {
1764
+ targetId: this._focusables[this._currentIndex].id,
1765
+ type: "focus"
1766
+ });
1767
+ } else {
1768
+ this._currentIndex = -1;
1769
+ }
1770
+ } else if (idx < this._currentIndex) {
1771
+ this._currentIndex--;
1772
+ }
1773
+ }
1774
+ /**
1775
+ * Move focus to the next focusable widget (wraps around).
1776
+ * Respects focus traps — if a trap is active, only cycles within it.
1777
+ */
1778
+ focusNext() {
1779
+ const candidates = this._getActiveFocusables();
1780
+ if (candidates.length === 0) return;
1781
+ const currentInCandidates = this.currentId ? candidates.findIndex((f) => f.id === this.currentId) : -1;
1782
+ let next = (currentInCandidates + 1) % candidates.length;
1783
+ const start = next;
1784
+ while (!candidates[next].focusable) {
1785
+ next = (next + 1) % candidates.length;
1786
+ if (next === start) return;
1787
+ }
1788
+ const masterIdx = this._focusables.findIndex((f) => f.id === candidates[next].id);
1789
+ if (masterIdx >= 0) this._changeFocus(masterIdx);
1790
+ }
1791
+ /**
1792
+ * Move focus to the previous focusable widget (wraps around).
1793
+ * Respects focus traps.
1794
+ */
1795
+ focusPrev() {
1796
+ const candidates = this._getActiveFocusables();
1797
+ if (candidates.length === 0) return;
1798
+ const currentInCandidates = this.currentId ? candidates.findIndex((f) => f.id === this.currentId) : 0;
1799
+ let prev = (currentInCandidates - 1 + candidates.length) % candidates.length;
1800
+ const start = prev;
1801
+ while (!candidates[prev].focusable) {
1802
+ prev = (prev - 1 + candidates.length) % candidates.length;
1803
+ if (prev === start) return;
1804
+ }
1805
+ const masterIdx = this._focusables.findIndex((f) => f.id === candidates[prev].id);
1806
+ if (masterIdx >= 0) this._changeFocus(masterIdx);
1807
+ }
1808
+ /**
1809
+ * Focus a specific widget by ID.
1810
+ */
1811
+ focusWidget(id) {
1812
+ const idx = this._focusables.findIndex((f) => f.id === id);
1813
+ if (idx < 0 || !this._focusables[idx].focusable) return;
1814
+ this._changeFocus(idx);
1815
+ }
1816
+ /**
1817
+ * Check if a specific widget currently has focus.
1818
+ */
1819
+ isFocused(id) {
1820
+ return this.currentId === id;
1821
+ }
1822
+ // ── Focus Trap ──────────────────────────────────────
1823
+ /**
1824
+ * Register widget IDs as members of a container (for trap lookup).
1825
+ */
1826
+ registerContainerMembers(containerId, memberIds) {
1827
+ const set = this._containerMembers.get(containerId) ?? /* @__PURE__ */ new Set();
1828
+ for (const id of memberIds) set.add(id);
1829
+ this._containerMembers.set(containerId, set);
1830
+ }
1831
+ /**
1832
+ * Unregister a container's member list.
1833
+ */
1834
+ unregisterContainerMembers(containerId) {
1835
+ this._containerMembers.delete(containerId);
1836
+ }
1837
+ /**
1838
+ * Trap focus within a container. Only focusables inside the
1839
+ * container are reachable via Tab. Traps are stacked —
1840
+ * nested modals create nested traps.
1841
+ */
1842
+ trap(containerId) {
1843
+ this._trapStack.push(containerId);
1844
+ const trapped = this._getActiveFocusables();
1845
+ if (trapped.length > 0) {
1846
+ const first = trapped.find((f) => f.focusable);
1847
+ if (first) {
1848
+ const idx = this._focusables.findIndex((f) => f.id === first.id);
1849
+ if (idx >= 0) this._changeFocus(idx);
1850
+ }
1851
+ }
1852
+ }
1853
+ /**
1854
+ * Release the current focus trap. Restores previous trap or free navigation.
1855
+ */
1856
+ release() {
1857
+ this._trapStack.pop();
1858
+ }
1859
+ /** Whether a focus trap is currently active */
1860
+ get isTrapped() {
1861
+ return this._trapStack.length > 0;
1862
+ }
1863
+ /** ID of the current trap container, or null */
1864
+ get currentTrapId() {
1865
+ return this._trapStack.length > 0 ? this._trapStack[this._trapStack.length - 1] : null;
1866
+ }
1867
+ // ── Focus Groups ────────────────────────────────────
1868
+ /**
1869
+ * Register a focus group. Arrow keys move within the group.
1870
+ * @param groupId Unique group identifier
1871
+ * @param widgetIds Ordered list of widget IDs in the group
1872
+ */
1873
+ registerGroup(groupId, widgetIds) {
1874
+ this._groups.set(groupId, widgetIds);
1875
+ }
1876
+ /**
1877
+ * Unregister a focus group.
1878
+ */
1879
+ unregisterGroup(groupId) {
1880
+ this._groups.delete(groupId);
1881
+ }
1882
+ /**
1883
+ * Move focus to the next widget within the same group.
1884
+ * Returns true if focus was moved, false if the widget isn't in a group.
1885
+ */
1886
+ focusNextInGroup() {
1887
+ const currentId = this.currentId;
1888
+ if (!currentId) return false;
1889
+ for (const [, ids] of this._groups) {
1890
+ const idx = ids.indexOf(currentId);
1891
+ if (idx >= 0) {
1892
+ const nextIdx = (idx + 1) % ids.length;
1893
+ this.focusWidget(ids[nextIdx]);
1894
+ return true;
1895
+ }
1896
+ }
1897
+ return false;
1898
+ }
1899
+ /**
1900
+ * Move focus to the previous widget within the same group.
1901
+ */
1902
+ focusPrevInGroup() {
1903
+ const currentId = this.currentId;
1904
+ if (!currentId) return false;
1905
+ for (const [, ids] of this._groups) {
1906
+ const idx = ids.indexOf(currentId);
1907
+ if (idx >= 0) {
1908
+ const prevIdx = (idx - 1 + ids.length) % ids.length;
1909
+ this.focusWidget(ids[prevIdx]);
1910
+ return true;
1911
+ }
1912
+ }
1913
+ return false;
1914
+ }
1915
+ // ── Private ──────────────────────────────────────────
1916
+ /**
1917
+ * Get the active focusables, filtered by the current trap if any.
1918
+ */
1919
+ _getActiveFocusables() {
1920
+ if (this._trapStack.length === 0) return this._focusables;
1921
+ const containerId = this._trapStack[this._trapStack.length - 1];
1922
+ const members = this._containerMembers.get(containerId);
1923
+ if (!members) return this._focusables;
1924
+ return this._focusables.filter((f) => members.has(f.id));
1925
+ }
1926
+ _changeFocus(newIndex) {
1927
+ const oldId = this.currentId;
1928
+ if (oldId) {
1929
+ this._events.emit("blur", { targetId: oldId, type: "blur" });
1930
+ }
1931
+ this._currentIndex = newIndex;
1932
+ this._events.emit("focus", {
1933
+ targetId: this._focusables[newIndex].id,
1934
+ type: "focus"
1935
+ });
1936
+ }
1937
+ };
1938
+
1939
+ // src/app/Fallback.ts
1940
+ function shouldUseFallback() {
1941
+ if (!process.stdout.isTTY) return true;
1942
+ if (process.env["CI"]) return true;
1943
+ if (process.env["TERM"] === "dumb") return true;
1944
+ return false;
1945
+ }
1946
+ function renderFallback(screen) {
1947
+ const lines = [];
1948
+ for (let r = 0; r < screen.rows; r++) {
1949
+ let line = "";
1950
+ for (let c = 0; c < screen.cols; c++) {
1951
+ const cell = screen.back[r][c];
1952
+ if (cell.width === 0) continue;
1953
+ line += cell.char || " ";
1954
+ }
1955
+ lines.push(line.trimEnd());
1956
+ }
1957
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
1958
+ lines.pop();
1959
+ }
1960
+ return lines.join("\n");
1961
+ }
1962
+
1963
+ // src/app/App.ts
1964
+ var App = class {
1965
+ terminal;
1966
+ screen;
1967
+ renderer;
1968
+ input;
1969
+ focus;
1970
+ events;
1971
+ layers;
1972
+ _rootWidget;
1973
+ _options;
1974
+ _mounted = false;
1975
+ _exitResolve = null;
1976
+ constructor(rootWidget, options = {}) {
1977
+ this._rootWidget = rootWidget;
1978
+ this._options = {
1979
+ fullscreen: true,
1980
+ mouse: false,
1981
+ fps: 30,
1982
+ ...options
1983
+ };
1984
+ this.terminal = new Terminal(options);
1985
+ this.screen = new Screen(this.terminal.cols, this.terminal.rows);
1986
+ this.renderer = new Renderer(this.terminal, this.screen, this._options.fps);
1987
+ this.input = new InputParser(this.terminal.stdin);
1988
+ this.focus = new FocusManager();
1989
+ this.events = new EventEmitter();
1990
+ this.layers = new LayerManager(this.terminal.cols, this.terminal.rows);
1991
+ }
1992
+ /**
1993
+ * Start the application.
1994
+ * Sets up the terminal, starts the render loop, and mounts the root widget.
1995
+ * Returns a promise that resolves when exit() is called.
1996
+ */
1997
+ async mount() {
1998
+ if (this._mounted) return 0;
1999
+ if (this._options.forceFallback || shouldUseFallback()) {
2000
+ this._renderFallback();
2001
+ return 0;
2002
+ }
2003
+ this._mounted = true;
2004
+ this.terminal.enterRawMode();
2005
+ if (this._options.fullscreen) {
2006
+ this.terminal.enterAltScreen();
2007
+ }
2008
+ this.terminal.hideCursor();
2009
+ if (this._options.mouse) {
2010
+ this.terminal.enableMouse();
2011
+ }
2012
+ if (this._options.title) {
2013
+ this.terminal.write(`\x1B]0;${this._options.title}\x07`);
2014
+ }
2015
+ this.terminal.onResize((cols, rows) => {
2016
+ this.screen.resize(cols, rows);
2017
+ this.screen.invalidate();
2018
+ this.layers.resize(cols, rows);
2019
+ this.events.emit("resize", { cols, rows });
2020
+ this.requestRender();
2021
+ });
2022
+ this.input.start();
2023
+ this.input.onKey((rawEvent) => {
2024
+ const event = createKeyEvent({
2025
+ ...rawEvent,
2026
+ targetId: this.focus.currentId ?? void 0
2027
+ });
2028
+ const focusedId = this.focus.currentId;
2029
+ if (focusedId) {
2030
+ const chain = this._buildBubbleChain(focusedId);
2031
+ for (const widget of chain) {
2032
+ widget.events.emit("key", event);
2033
+ if (event._propagationStopped) break;
2034
+ }
2035
+ }
2036
+ if (!event._defaultPrevented) {
2037
+ if (event.key === "tab" && !event.ctrl && !event.alt) {
2038
+ if (event.shift) {
2039
+ this.focus.focusPrev();
2040
+ } else {
2041
+ this.focus.focusNext();
2042
+ }
2043
+ }
2044
+ }
2045
+ if (!event._propagationStopped) {
2046
+ this.events.emit("key", event);
2047
+ }
2048
+ });
2049
+ this.input.onMouse((event) => {
2050
+ this.events.emit("mouse", event);
2051
+ });
2052
+ this.renderer.start();
2053
+ this._rootWidget.mount?.();
2054
+ this.events.emit("mount", void 0);
2055
+ this.screen.invalidate();
2056
+ this.requestRender();
2057
+ return new Promise((resolve) => {
2058
+ this._exitResolve = resolve;
2059
+ });
2060
+ }
2061
+ /**
2062
+ * Stop the application and restore terminal state.
2063
+ */
2064
+ unmount() {
2065
+ if (!this._mounted) return;
2066
+ this._mounted = false;
2067
+ this._rootWidget.unmount?.();
2068
+ this.events.emit("unmount", void 0);
2069
+ this.renderer.stop();
2070
+ this.input.stop();
2071
+ this.terminal.restore();
2072
+ this.events.removeAll();
2073
+ }
2074
+ /**
2075
+ * Create an overlay layer for rendering above normal widgets.
2076
+ * @param id Unique layer identifier (e.g. 'modal', 'select-dropdown', 'toast')
2077
+ * @param zIndex Stacking order (higher = rendered on top). Default: 100
2078
+ */
2079
+ addOverlay(id, zIndex = 100) {
2080
+ this.layers.createLayer(id, zIndex);
2081
+ }
2082
+ /**
2083
+ * Remove an overlay layer.
2084
+ */
2085
+ removeOverlay(id) {
2086
+ this.layers.removeLayer(id);
2087
+ }
2088
+ /**
2089
+ * Request a re-render on the next frame.
2090
+ */
2091
+ requestRender() {
2092
+ if (!this._mounted) return;
2093
+ const layoutRoot = this._rootWidget.getLayoutNode();
2094
+ computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
2095
+ this._rootWidget.syncLayout?.();
2096
+ this.screen.clear();
2097
+ this._rootWidget.render(this.screen);
2098
+ this.layers.composite(this.screen);
2099
+ this.renderer.requestFrame();
2100
+ }
2101
+ /**
2102
+ * Exit the app (convenience method).
2103
+ */
2104
+ exit(code = 0) {
2105
+ this.unmount();
2106
+ if (this._exitResolve) {
2107
+ this._exitResolve(code);
2108
+ this._exitResolve = null;
2109
+ } else {
2110
+ process.exit(code);
2111
+ }
2112
+ }
2113
+ /**
2114
+ * Render in fallback (static) mode for non-interactive environments.
2115
+ */
2116
+ _renderFallback() {
2117
+ const layoutRoot = this._rootWidget.getLayoutNode();
2118
+ computeLayout(layoutRoot, this.terminal.cols, this.terminal.rows);
2119
+ this._rootWidget.syncLayout?.();
2120
+ this.screen.clear();
2121
+ this._rootWidget.render(this.screen);
2122
+ const output = renderFallback(this.screen);
2123
+ this.terminal.write(output + "\n");
2124
+ }
2125
+ /**
2126
+ * Build the bubble chain for keyboard events.
2127
+ * Returns an array: [focused widget, parent, grandparent, ..., root]
2128
+ */
2129
+ _buildBubbleChain(widgetId) {
2130
+ const chain = [];
2131
+ const widget = this._findWidgetById(this._rootWidget, widgetId);
2132
+ if (!widget) return chain;
2133
+ let current = widget;
2134
+ while (current) {
2135
+ if (current.events) {
2136
+ chain.push(current);
2137
+ }
2138
+ current = current.parent ?? null;
2139
+ }
2140
+ return chain;
2141
+ }
2142
+ /**
2143
+ * Find a widget by ID in the widget tree (DFS).
2144
+ * Uses duck-typing to work with any object that has id/children.
2145
+ */
2146
+ _findWidgetById(root, id) {
2147
+ if (root.id === id) return root;
2148
+ const children = root._children ?? root.children ?? [];
2149
+ if (Array.isArray(children)) {
2150
+ for (const child of children) {
2151
+ const found = this._findWidgetById(child, id);
2152
+ if (found) return found;
2153
+ }
2154
+ }
2155
+ return null;
2156
+ }
2157
+ };
2158
+
2159
+ // src/utils/unicode.ts
2160
+ function isWideChar(codePoint) {
2161
+ return (
2162
+ // CJK Unified Ideographs (common Chinese/Japanese/Korean)
2163
+ codePoint >= 19968 && codePoint <= 40959 || // CJK Unified Ideographs Extension A
2164
+ codePoint >= 13312 && codePoint <= 19903 || // CJK Compatibility Ideographs
2165
+ codePoint >= 63744 && codePoint <= 64255 || // Hangul Syllables
2166
+ codePoint >= 44032 && codePoint <= 55215 || // Katakana
2167
+ codePoint >= 12448 && codePoint <= 12543 || // CJK Symbols and Punctuation
2168
+ codePoint >= 12288 && codePoint <= 12351 || // Hiragana
2169
+ codePoint >= 12352 && codePoint <= 12447 || // Fullwidth Forms
2170
+ codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || // CJK Unified Ideographs Extension B
2171
+ codePoint >= 131072 && codePoint <= 173791 || // CJK Unified Ideographs Extension C,D,E,F
2172
+ codePoint >= 173824 && codePoint <= 191471 || // CJK Compatibility Ideographs Supplement
2173
+ codePoint >= 194560 && codePoint <= 195103
2174
+ );
2175
+ }
2176
+ function isCombining(codePoint) {
2177
+ return (
2178
+ // Combining Diacritical Marks
2179
+ codePoint >= 768 && codePoint <= 879 || // Combining Diacritical Marks Extended
2180
+ codePoint >= 6832 && codePoint <= 6911 || // Combining Diacritical Marks Supplement
2181
+ codePoint >= 7616 && codePoint <= 7679 || // Combining Diacritical Marks for Symbols
2182
+ codePoint >= 8400 && codePoint <= 8447 || // Combining Half Marks
2183
+ codePoint >= 65056 && codePoint <= 65071 || // Variation selectors
2184
+ codePoint >= 65024 && codePoint <= 65039 || // Zero-width joiner / non-joiner
2185
+ codePoint === 8203 || codePoint === 8204 || codePoint === 8205 || codePoint === 65279
2186
+ );
2187
+ }
2188
+ function isEmoji(codePoint) {
2189
+ return (
2190
+ // Emoticons
2191
+ codePoint >= 128512 && codePoint <= 128591 || // Misc Symbols and Pictographs
2192
+ codePoint >= 127744 && codePoint <= 128511 || // Transport and Map
2193
+ codePoint >= 128640 && codePoint <= 128767 || // Supplemental Symbols
2194
+ codePoint >= 129280 && codePoint <= 129535 || // Misc symbols
2195
+ codePoint >= 9728 && codePoint <= 9983 || // Dingbats
2196
+ codePoint >= 9984 && codePoint <= 10175 || // Flags
2197
+ codePoint >= 127456 && codePoint <= 127487
2198
+ );
2199
+ }
2200
+ function stringWidth(str) {
2201
+ let width = 0;
2202
+ let inEscape = false;
2203
+ for (const char of str) {
2204
+ const cp = char.codePointAt(0);
2205
+ if (cp === 27) {
2206
+ inEscape = true;
2207
+ continue;
2208
+ }
2209
+ if (inEscape) {
2210
+ if (cp >= 64 && cp <= 126 && cp !== 91) {
2211
+ inEscape = false;
2212
+ }
2213
+ continue;
2214
+ }
2215
+ if (cp < 32 || cp >= 127 && cp < 160) {
2216
+ continue;
2217
+ }
2218
+ if (isCombining(cp)) {
2219
+ continue;
2220
+ }
2221
+ if (isWideChar(cp) || isEmoji(cp)) {
2222
+ width += 2;
2223
+ continue;
2224
+ }
2225
+ width += 1;
2226
+ }
2227
+ return width;
2228
+ }
2229
+ function truncate(str, maxWidth, ellipsis = "\u2026") {
2230
+ if (maxWidth <= 0) return "";
2231
+ const strW = stringWidth(str);
2232
+ if (strW <= maxWidth) return str;
2233
+ const ellipsisW = stringWidth(ellipsis);
2234
+ const targetW = maxWidth - ellipsisW;
2235
+ if (targetW <= 0) return ellipsis.slice(0, maxWidth);
2236
+ let width = 0;
2237
+ let result = "";
2238
+ let inEscape = false;
2239
+ let escapeBuffer = "";
2240
+ for (const char of str) {
2241
+ const cp = char.codePointAt(0);
2242
+ if (cp === 27) {
2243
+ inEscape = true;
2244
+ escapeBuffer += char;
2245
+ continue;
2246
+ }
2247
+ if (inEscape) {
2248
+ escapeBuffer += char;
2249
+ if (cp >= 64 && cp <= 126 && cp !== 91) {
2250
+ inEscape = false;
2251
+ result += escapeBuffer;
2252
+ escapeBuffer = "";
2253
+ }
2254
+ continue;
2255
+ }
2256
+ let charW = 1;
2257
+ if (isCombining(cp)) {
2258
+ charW = 0;
2259
+ } else if (isWideChar(cp) || isEmoji(cp)) {
2260
+ charW = 2;
2261
+ } else if (cp < 32 || cp >= 127 && cp < 160) {
2262
+ charW = 0;
2263
+ }
2264
+ if (width + charW > targetW) break;
2265
+ width += charW;
2266
+ result += char;
2267
+ }
2268
+ return result + ellipsis;
2269
+ }
2270
+ function stripAnsi(str) {
2271
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
2272
+ }
2273
+ function wordWrap(str, width) {
2274
+ if (width <= 0) return str;
2275
+ const lines = str.split("\n");
2276
+ const result = [];
2277
+ for (const line of lines) {
2278
+ if (stringWidth(line) <= width) {
2279
+ result.push(line);
2280
+ continue;
2281
+ }
2282
+ let currentLine = "";
2283
+ let currentWidth = 0;
2284
+ const words = line.split(/(\s+)/);
2285
+ for (const word of words) {
2286
+ const wordW = stringWidth(word);
2287
+ if (currentWidth + wordW <= width) {
2288
+ currentLine += word;
2289
+ currentWidth += wordW;
2290
+ } else if (wordW > width) {
2291
+ if (currentLine) {
2292
+ result.push(currentLine);
2293
+ currentLine = "";
2294
+ currentWidth = 0;
2295
+ }
2296
+ for (const char of word) {
2297
+ const cp = char.codePointAt(0);
2298
+ const charW = isWideChar(cp) || isEmoji(cp) ? 2 : isCombining(cp) ? 0 : 1;
2299
+ if (currentWidth + charW > width) {
2300
+ result.push(currentLine);
2301
+ currentLine = "";
2302
+ currentWidth = 0;
2303
+ }
2304
+ currentLine += char;
2305
+ currentWidth += charW;
2306
+ }
2307
+ } else {
2308
+ result.push(currentLine);
2309
+ currentLine = word.trimStart();
2310
+ currentWidth = stringWidth(currentLine);
2311
+ }
2312
+ }
2313
+ if (currentLine) {
2314
+ result.push(currentLine);
2315
+ }
2316
+ }
2317
+ return result.join("\n");
2318
+ }
2319
+ // Annotate the CommonJS export names for ESM import in node:
2320
+ 0 && (module.exports = {
2321
+ App,
2322
+ BORDER_CHARS,
2323
+ CTRL_KEYS,
2324
+ ColorDepth,
2325
+ ESCAPE_SEQUENCES,
2326
+ EventEmitter,
2327
+ FocusManager,
2328
+ InputParser,
2329
+ LayerManager,
2330
+ Renderer,
2331
+ SPECIAL_KEYS,
2332
+ Screen,
2333
+ Terminal,
2334
+ ansi,
2335
+ borderSize,
2336
+ cellsEqual,
2337
+ colorToAnsiBg,
2338
+ colorToAnsiFg,
2339
+ colorToRgb,
2340
+ computeLayout,
2341
+ containsPoint,
2342
+ createKeyEvent,
2343
+ createLayoutNode,
2344
+ defaultStyle,
2345
+ detectColorDepth,
2346
+ emptyCell,
2347
+ emptyRect,
2348
+ getBorderChars,
2349
+ intersectRect,
2350
+ isMouseSequence,
2351
+ mergeStyles,
2352
+ normalizeEdges,
2353
+ parseColor,
2354
+ parseMouseEvent,
2355
+ renderFallback,
2356
+ shouldUseFallback,
2357
+ shrinkRect,
2358
+ stringWidth,
2359
+ stripAnsi,
2360
+ styleToCellAttrs,
2361
+ truncate,
2362
+ unionRect,
2363
+ wordWrap
2364
+ });
2365
+ //# sourceMappingURL=index.cjs.map