@solid-tui/runtime 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.
@@ -0,0 +1,4661 @@
1
+ import { batch, catchError, children, createComponent as createComponent$1, createContext, createEffect, createMemo, createRenderEffect, createRoot as createRoot$1, createSignal, mergeProps as mergeProps$1, on, onCleanup, splitProps, untrack, useContext } from "solid-js/dist/solid.js";
2
+ import Yoga from "better-yoga-layout";
3
+ import cliTruncate from "cli-truncate";
4
+ import sliceAnsi from "slice-ansi";
5
+ import stringWidth from "string-width";
6
+ import wrapAnsi from "wrap-ansi";
7
+ import { EventEmitter } from "node:events";
8
+ import isInCi from "is-in-ci";
9
+ import { onExit } from "signal-exit";
10
+ import patchConsoleFn from "patch-console";
11
+ import ansiEscapes from "ansi-escapes";
12
+ import * as fs from "node:fs";
13
+ import { writeSync } from "node:fs";
14
+ import cliBoxes from "cli-boxes";
15
+ import { styledCharsFromTokens, styledCharsToString, tokenize } from "@alcalzone/ansi-tokenize";
16
+ import chalk from "chalk";
17
+ import { cwd } from "node:process";
18
+ import StackUtils from "stack-utils";
19
+ import codeExcerpt from "code-excerpt";
20
+ import terminalSize from "terminal-size";
21
+ //#region src/solid-universal.ts
22
+ const memo$1 = (fn) => createMemo(() => fn());
23
+ function createRenderer({ createElement, createTextNode, isTextNode, replaceText, insertNode, removeNode, setProperty, getParentNode, getFirstChild, getNextSibling }) {
24
+ function insert(parent, accessor, marker, initial) {
25
+ if (marker !== void 0 && !initial) initial = [];
26
+ if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);
27
+ createRenderEffect((current) => insertExpression(parent, accessor(), current, marker), initial);
28
+ return parent;
29
+ }
30
+ function insertExpression(parent, value, current, marker, unwrapArray) {
31
+ while (typeof current === "function") current = current();
32
+ if (value === current) return current;
33
+ const valueType = typeof value;
34
+ const multi = marker !== void 0;
35
+ if (valueType === "string" || valueType === "number") {
36
+ const text = valueType === "number" ? String(value) : value;
37
+ if (multi) {
38
+ let node = current?.[0];
39
+ if (node && isTextNode(node)) replaceText(node, text);
40
+ else node = createTextNode(text);
41
+ current = cleanChildren(parent, current, marker, node);
42
+ } else if (current !== "" && typeof current === "string") {
43
+ const first = getFirstChild(parent);
44
+ if (first) replaceText(first, text);
45
+ current = text;
46
+ } else {
47
+ cleanChildren(parent, current, marker, createTextNode(text));
48
+ current = text;
49
+ }
50
+ } else if (value == null || valueType === "boolean") current = cleanChildren(parent, current, marker);
51
+ else if (valueType === "function") {
52
+ createRenderEffect(() => {
53
+ let v = value();
54
+ while (typeof v === "function") v = v();
55
+ current = insertExpression(parent, v, current, marker);
56
+ });
57
+ return () => current;
58
+ } else if (Array.isArray(value)) {
59
+ const array = [];
60
+ if (normalizeIncomingArray(array, value, unwrapArray)) {
61
+ createRenderEffect(() => {
62
+ current = insertExpression(parent, array, current, marker, true);
63
+ });
64
+ return () => current;
65
+ }
66
+ if (array.length === 0) {
67
+ const replacement = cleanChildren(parent, current, marker);
68
+ if (multi) return current = replacement;
69
+ } else if (Array.isArray(current)) if (current.length === 0) appendNodes(parent, array, marker);
70
+ else reconcileArrays(parent, current, array);
71
+ else if (current == null || current === "") appendNodes(parent, array, marker);
72
+ else {
73
+ const first = getFirstChild(parent);
74
+ reconcileArrays(parent, multi && current || (first ? [first] : []), array);
75
+ }
76
+ current = array;
77
+ } else {
78
+ const node = value;
79
+ if (Array.isArray(current)) {
80
+ if (multi) return current = cleanChildren(parent, current, marker, node);
81
+ cleanChildren(parent, current, null, node);
82
+ } else if (current == null || current === "" || !getFirstChild(parent)) insertNode(parent, node);
83
+ else {
84
+ const first = getFirstChild(parent);
85
+ if (first) replaceNode(parent, node, first);
86
+ }
87
+ current = node;
88
+ }
89
+ return current;
90
+ }
91
+ function normalizeIncomingArray(normalized, array, unwrap) {
92
+ let dynamic = false;
93
+ for (let i = 0, len = array.length; i < len; i++) {
94
+ let item = array[i];
95
+ let t;
96
+ if (item == null || item === true || item === false) continue;
97
+ else if (Array.isArray(item)) dynamic = normalizeIncomingArray(normalized, item) || dynamic;
98
+ else if ((t = typeof item) === "string" || t === "number") normalized.push(createTextNode(String(item)));
99
+ else if (t === "function") if (unwrap) {
100
+ while (typeof item === "function") item = item();
101
+ dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item]) || dynamic;
102
+ } else {
103
+ normalized.push(item);
104
+ dynamic = true;
105
+ }
106
+ else normalized.push(item);
107
+ }
108
+ return dynamic;
109
+ }
110
+ function reconcileArrays(parentNode, a, b) {
111
+ let bLength = b.length;
112
+ let aEnd = a.length;
113
+ let bEnd = bLength;
114
+ let aStart = 0;
115
+ let bStart = 0;
116
+ const after = getNextSibling(a[aEnd - 1]);
117
+ let map = null;
118
+ while (aStart < aEnd || bStart < bEnd) {
119
+ if (a[aStart] === b[bStart]) {
120
+ aStart++;
121
+ bStart++;
122
+ continue;
123
+ }
124
+ while (a[aEnd - 1] === b[bEnd - 1]) {
125
+ aEnd--;
126
+ bEnd--;
127
+ }
128
+ if (aEnd === aStart) {
129
+ const node = bEnd < bLength ? bStart ? getNextSibling(b[bStart - 1]) : b[bEnd - bStart] : after;
130
+ while (bStart < bEnd) insertNode(parentNode, b[bStart++], node);
131
+ } else if (bEnd === bStart) while (aStart < aEnd) {
132
+ if (!map || !map.has(a[aStart])) removeNode(parentNode, a[aStart]);
133
+ aStart++;
134
+ }
135
+ else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
136
+ const node = getNextSibling(a[--aEnd]);
137
+ insertNode(parentNode, b[bStart++], getNextSibling(a[aStart++]));
138
+ insertNode(parentNode, b[--bEnd], node);
139
+ a[aEnd] = b[bEnd];
140
+ } else {
141
+ if (!map) {
142
+ map = /* @__PURE__ */ new Map();
143
+ let i = bStart;
144
+ while (i < bEnd) map.set(b[i], i++);
145
+ }
146
+ const index = map.get(a[aStart]);
147
+ if (index != null) if (bStart < index && index < bEnd) {
148
+ let i = aStart;
149
+ let sequence = 1;
150
+ let t;
151
+ while (++i < aEnd && i < bEnd) {
152
+ t = map.get(a[i]);
153
+ if (t == null || t !== index + sequence) break;
154
+ sequence++;
155
+ }
156
+ if (sequence > index - bStart) {
157
+ const node = a[aStart];
158
+ while (bStart < index) insertNode(parentNode, b[bStart++], node);
159
+ } else replaceNode(parentNode, b[bStart++], a[aStart++]);
160
+ } else aStart++;
161
+ else removeNode(parentNode, a[aStart++]);
162
+ }
163
+ }
164
+ }
165
+ function cleanChildren(parent, current, marker, replacement) {
166
+ if (marker === void 0) {
167
+ let removed;
168
+ while (removed = getFirstChild(parent)) removeNode(parent, removed);
169
+ if (replacement) insertNode(parent, replacement);
170
+ return "";
171
+ }
172
+ const node = replacement || createTextNode("");
173
+ const currentArray = Array.isArray(current) ? current : [];
174
+ if (currentArray.length) {
175
+ let inserted = false;
176
+ for (let i = currentArray.length - 1; i >= 0; i--) {
177
+ const el = currentArray[i];
178
+ if (node !== el) {
179
+ const isParent = getParentNode(el) === parent;
180
+ if (!inserted && !i) if (isParent) replaceNode(parent, node, el);
181
+ else insertNode(parent, node, marker ?? void 0);
182
+ else if (isParent) removeNode(parent, el);
183
+ } else inserted = true;
184
+ }
185
+ } else insertNode(parent, node, marker ?? void 0);
186
+ return [node];
187
+ }
188
+ function appendNodes(parent, array, marker) {
189
+ for (let i = 0, len = array.length; i < len; i++) insertNode(parent, array[i], marker ?? void 0);
190
+ }
191
+ function replaceNode(parent, newNode, oldNode) {
192
+ insertNode(parent, newNode, oldNode);
193
+ removeNode(parent, oldNode);
194
+ }
195
+ function spreadExpression(node, props, prevProps = {}, skipChildren) {
196
+ props ||= {};
197
+ if (!skipChildren) createRenderEffect(() => {
198
+ prevProps.children = insertExpression(node, props.children, prevProps.children);
199
+ });
200
+ createRenderEffect(() => {
201
+ if (typeof props.ref === "function") props.ref(node);
202
+ });
203
+ createRenderEffect(() => {
204
+ for (const prop in props) {
205
+ if (prop === "children" || prop === "ref") continue;
206
+ const value = props[prop];
207
+ if (value === prevProps[prop]) continue;
208
+ setProperty(node, prop, value, prevProps[prop]);
209
+ prevProps[prop] = value;
210
+ }
211
+ });
212
+ return prevProps;
213
+ }
214
+ return {
215
+ render(code, element) {
216
+ let disposer;
217
+ createRoot$1((dispose) => {
218
+ disposer = dispose;
219
+ insert(element, code());
220
+ });
221
+ return () => disposer?.();
222
+ },
223
+ insert,
224
+ spread(node, accessor, skipChildren) {
225
+ if (typeof accessor === "function") createRenderEffect((current) => spreadExpression(node, accessor(), current, skipChildren));
226
+ else spreadExpression(node, accessor, void 0, skipChildren);
227
+ },
228
+ createElement,
229
+ createTextNode,
230
+ insertNode,
231
+ setProp(node, name, value, prev) {
232
+ setProperty(node, name, value, prev);
233
+ return value;
234
+ },
235
+ mergeProps: mergeProps$1,
236
+ effect: createRenderEffect,
237
+ memo: memo$1,
238
+ createComponent: createComponent$1,
239
+ use(fn, element, arg) {
240
+ return untrack(() => fn(element, arg));
241
+ }
242
+ };
243
+ }
244
+ //#endregion
245
+ //#region src/host/nodes.ts
246
+ const UNATTACHED_YOGA = Symbol("solid-tui:yoga-unattached");
247
+ function createRoot(appContext) {
248
+ return {
249
+ type: "root",
250
+ parent: null,
251
+ children: [],
252
+ yoga: UNATTACHED_YOGA,
253
+ appContext,
254
+ layoutListeners: /* @__PURE__ */ new Set()
255
+ };
256
+ }
257
+ /**
258
+ * Register a callback to be invoked after every layout calculation.
259
+ * Returns an unsubscribe function.
260
+ */
261
+ function addLayoutListener(root, listener) {
262
+ root.layoutListeners.add(listener);
263
+ return () => {
264
+ root.layoutListeners.delete(listener);
265
+ };
266
+ }
267
+ /** Invoke all registered layout listeners. Called after `yoga.calculateLayout`. */
268
+ function emitLayoutListeners(root) {
269
+ for (const listener of root.layoutListeners) listener();
270
+ }
271
+ function createBox() {
272
+ return {
273
+ type: "tui-box",
274
+ parent: null,
275
+ children: [],
276
+ yoga: UNATTACHED_YOGA,
277
+ props: {},
278
+ paintDirty: true
279
+ };
280
+ }
281
+ function createText() {
282
+ return {
283
+ type: "tui-text",
284
+ parent: null,
285
+ children: [],
286
+ yoga: UNATTACHED_YOGA,
287
+ props: {}
288
+ };
289
+ }
290
+ function createVirtualText() {
291
+ return {
292
+ type: "tui-virtual-text",
293
+ parent: null,
294
+ children: [],
295
+ props: {}
296
+ };
297
+ }
298
+ function createTextLeaf(value) {
299
+ return {
300
+ type: "text-leaf",
301
+ parent: null,
302
+ value: typeof value === "string" ? value : String(value)
303
+ };
304
+ }
305
+ function createStatic() {
306
+ return {
307
+ type: "tui-static",
308
+ parent: null,
309
+ children: [],
310
+ yoga: UNATTACHED_YOGA,
311
+ props: {},
312
+ writtenNodes: /* @__PURE__ */ new Set()
313
+ };
314
+ }
315
+ function createTransform(fn) {
316
+ return {
317
+ type: "tui-transform",
318
+ parent: null,
319
+ children: [],
320
+ yoga: UNATTACHED_YOGA,
321
+ transform: fn
322
+ };
323
+ }
324
+ function isContainer(node) {
325
+ return node.type !== "text-leaf" && node.type !== "comment";
326
+ }
327
+ /**
328
+ * Whether a child counts as a positional line for transform/squash indexing.
329
+ * Mirrors Ink: `null`/`''` children are not rendered as childNodes, so neither a
330
+ * `comment` (Solid's null/false/v-if placeholder) nor an EMPTY `text-leaf` (an
331
+ * empty-string `{''}` child, or a template `<slot/>` boundary anchor) advances the
332
+ * index. Verified against real Ink v7.0.4 (`a{''}<Transform>b` → `ab[1]`, not
333
+ * `ab[2]`). The inverse of static-channel's `isInertStaticAnchor`.
334
+ */
335
+ function advancesLineIndex(child) {
336
+ return child.type !== "comment" && !(child.type === "text-leaf" && child.value === "");
337
+ }
338
+ //#endregion
339
+ //#region src/paint/ansi-tokenizer.ts
340
+ const bellCharacter = "\x07";
341
+ const escapeCharacter = "\x1B";
342
+ const stringTerminatorCharacter = "œ";
343
+ const csiCharacter = "›";
344
+ const oscCharacter = "";
345
+ const dcsCharacter = "";
346
+ const pmCharacter = "ž";
347
+ const apcCharacter = "Ÿ";
348
+ const sosCharacter = "˜";
349
+ const isCsiParameterCharacter = (character) => {
350
+ const codePoint = character.codePointAt(0);
351
+ return codePoint !== void 0 && codePoint >= 48 && codePoint <= 63;
352
+ };
353
+ const isCsiIntermediateCharacter = (character) => {
354
+ const codePoint = character.codePointAt(0);
355
+ return codePoint !== void 0 && codePoint >= 32 && codePoint <= 47;
356
+ };
357
+ const isCsiFinalCharacter = (character) => {
358
+ const codePoint = character.codePointAt(0);
359
+ return codePoint !== void 0 && codePoint >= 64 && codePoint <= 126;
360
+ };
361
+ const isEscapeIntermediateCharacter = (character) => {
362
+ const codePoint = character.codePointAt(0);
363
+ return codePoint !== void 0 && codePoint >= 32 && codePoint <= 47;
364
+ };
365
+ const isEscapeFinalCharacter = (character) => {
366
+ const codePoint = character.codePointAt(0);
367
+ return codePoint !== void 0 && codePoint >= 48 && codePoint <= 126;
368
+ };
369
+ const isC1ControlCharacter = (character) => {
370
+ const codePoint = character.codePointAt(0);
371
+ return codePoint !== void 0 && codePoint >= 128 && codePoint <= 159;
372
+ };
373
+ const readCsiSequence = (text, fromIndex) => {
374
+ let index = fromIndex;
375
+ while (index < text.length) {
376
+ const character = text[index];
377
+ if (!isCsiParameterCharacter(character)) break;
378
+ index++;
379
+ }
380
+ const parameterString = text.slice(fromIndex, index);
381
+ const intermediateStartIndex = index;
382
+ while (index < text.length) {
383
+ const character = text[index];
384
+ if (!isCsiIntermediateCharacter(character)) break;
385
+ index++;
386
+ }
387
+ const intermediateString = text.slice(intermediateStartIndex, index);
388
+ const finalCharacter = text[index];
389
+ if (finalCharacter === void 0 || !isCsiFinalCharacter(finalCharacter)) return;
390
+ return {
391
+ endIndex: index + 1,
392
+ parameterString,
393
+ intermediateString,
394
+ finalCharacter
395
+ };
396
+ };
397
+ const findControlStringTerminatorIndex = (text, fromIndex, allowBellTerminator) => {
398
+ for (let index = fromIndex; index < text.length; index++) {
399
+ const character = text[index];
400
+ if (allowBellTerminator && character === bellCharacter) return index + 1;
401
+ if (character === stringTerminatorCharacter) return index + 1;
402
+ if (character === escapeCharacter) {
403
+ const followingCharacter = text[index + 1];
404
+ if (followingCharacter === escapeCharacter) {
405
+ index++;
406
+ continue;
407
+ }
408
+ if (followingCharacter === "\\") return index + 2;
409
+ }
410
+ }
411
+ };
412
+ const readEscapeSequence = (text, fromIndex) => {
413
+ let index = fromIndex;
414
+ while (index < text.length) {
415
+ const character = text[index];
416
+ if (!isEscapeIntermediateCharacter(character)) break;
417
+ index++;
418
+ }
419
+ const intermediateString = text.slice(fromIndex, index);
420
+ const finalCharacter = text[index];
421
+ if (finalCharacter === void 0 || !isEscapeFinalCharacter(finalCharacter)) return;
422
+ return {
423
+ endIndex: index + 1,
424
+ intermediateString,
425
+ finalCharacter
426
+ };
427
+ };
428
+ const getControlStringFromEscapeIntroducer = (character) => {
429
+ switch (character) {
430
+ case "]": return {
431
+ type: "osc",
432
+ allowBellTerminator: true
433
+ };
434
+ case "P": return {
435
+ type: "dcs",
436
+ allowBellTerminator: false
437
+ };
438
+ case "^": return {
439
+ type: "pm",
440
+ allowBellTerminator: false
441
+ };
442
+ case "_": return {
443
+ type: "apc",
444
+ allowBellTerminator: false
445
+ };
446
+ case "X": return {
447
+ type: "sos",
448
+ allowBellTerminator: false
449
+ };
450
+ default: return;
451
+ }
452
+ };
453
+ const getControlStringFromC1Introducer = (character) => {
454
+ switch (character) {
455
+ case oscCharacter: return {
456
+ type: "osc",
457
+ allowBellTerminator: true
458
+ };
459
+ case dcsCharacter: return {
460
+ type: "dcs",
461
+ allowBellTerminator: false
462
+ };
463
+ case pmCharacter: return {
464
+ type: "pm",
465
+ allowBellTerminator: false
466
+ };
467
+ case apcCharacter: return {
468
+ type: "apc",
469
+ allowBellTerminator: false
470
+ };
471
+ case sosCharacter: return {
472
+ type: "sos",
473
+ allowBellTerminator: false
474
+ };
475
+ default: return;
476
+ }
477
+ };
478
+ const hasAnsiControlCharacters = (text) => {
479
+ if (text.includes(escapeCharacter)) return true;
480
+ for (const character of text) if (isC1ControlCharacter(character)) return true;
481
+ return false;
482
+ };
483
+ const malformedFromIndex = (tokens, text, textStartIndex, fromIndex) => {
484
+ if (fromIndex > textStartIndex) tokens.push({
485
+ type: "text",
486
+ value: text.slice(textStartIndex, fromIndex)
487
+ });
488
+ tokens.push({
489
+ type: "invalid",
490
+ value: text.slice(fromIndex)
491
+ });
492
+ return tokens;
493
+ };
494
+ const tokenizeAnsi = (text) => {
495
+ if (!hasAnsiControlCharacters(text)) return [{
496
+ type: "text",
497
+ value: text
498
+ }];
499
+ const tokens = [];
500
+ let textStartIndex = 0;
501
+ for (let index = 0; index < text.length;) {
502
+ const character = text[index];
503
+ if (character === void 0) break;
504
+ if (character === escapeCharacter) {
505
+ const followingCharacter = text[index + 1];
506
+ if (followingCharacter === void 0) return malformedFromIndex(tokens, text, textStartIndex, index);
507
+ if (followingCharacter === "[") {
508
+ const csiSequence = readCsiSequence(text, index + 2);
509
+ if (csiSequence === void 0) return malformedFromIndex(tokens, text, textStartIndex, index);
510
+ if (index > textStartIndex) tokens.push({
511
+ type: "text",
512
+ value: text.slice(textStartIndex, index)
513
+ });
514
+ tokens.push({
515
+ type: "csi",
516
+ value: text.slice(index, csiSequence.endIndex),
517
+ parameterString: csiSequence.parameterString,
518
+ intermediateString: csiSequence.intermediateString,
519
+ finalCharacter: csiSequence.finalCharacter
520
+ });
521
+ index = csiSequence.endIndex;
522
+ textStartIndex = index;
523
+ continue;
524
+ }
525
+ const escapeControlString = getControlStringFromEscapeIntroducer(followingCharacter);
526
+ if (escapeControlString !== void 0) {
527
+ const controlStringTerminatorIndex = findControlStringTerminatorIndex(text, index + 2, escapeControlString.allowBellTerminator);
528
+ if (controlStringTerminatorIndex === void 0) return malformedFromIndex(tokens, text, textStartIndex, index);
529
+ if (index > textStartIndex) tokens.push({
530
+ type: "text",
531
+ value: text.slice(textStartIndex, index)
532
+ });
533
+ tokens.push({
534
+ type: escapeControlString.type,
535
+ value: text.slice(index, controlStringTerminatorIndex)
536
+ });
537
+ index = controlStringTerminatorIndex;
538
+ textStartIndex = index;
539
+ continue;
540
+ }
541
+ const escapeSequence = readEscapeSequence(text, index + 1);
542
+ if (escapeSequence === void 0) {
543
+ if (isEscapeIntermediateCharacter(followingCharacter)) return malformedFromIndex(tokens, text, textStartIndex, index);
544
+ if (index > textStartIndex) tokens.push({
545
+ type: "text",
546
+ value: text.slice(textStartIndex, index)
547
+ });
548
+ index++;
549
+ textStartIndex = index;
550
+ continue;
551
+ }
552
+ if (index > textStartIndex) tokens.push({
553
+ type: "text",
554
+ value: text.slice(textStartIndex, index)
555
+ });
556
+ tokens.push({
557
+ type: "esc",
558
+ value: text.slice(index, escapeSequence.endIndex),
559
+ intermediateString: escapeSequence.intermediateString,
560
+ finalCharacter: escapeSequence.finalCharacter
561
+ });
562
+ index = escapeSequence.endIndex;
563
+ textStartIndex = index;
564
+ continue;
565
+ }
566
+ if (character === csiCharacter) {
567
+ const csiSequence = readCsiSequence(text, index + 1);
568
+ if (csiSequence === void 0) return malformedFromIndex(tokens, text, textStartIndex, index);
569
+ if (index > textStartIndex) tokens.push({
570
+ type: "text",
571
+ value: text.slice(textStartIndex, index)
572
+ });
573
+ tokens.push({
574
+ type: "csi",
575
+ value: text.slice(index, csiSequence.endIndex),
576
+ parameterString: csiSequence.parameterString,
577
+ intermediateString: csiSequence.intermediateString,
578
+ finalCharacter: csiSequence.finalCharacter
579
+ });
580
+ index = csiSequence.endIndex;
581
+ textStartIndex = index;
582
+ continue;
583
+ }
584
+ const c1ControlString = getControlStringFromC1Introducer(character);
585
+ if (c1ControlString !== void 0) {
586
+ const controlStringTerminatorIndex = findControlStringTerminatorIndex(text, index + 1, c1ControlString.allowBellTerminator);
587
+ if (controlStringTerminatorIndex === void 0) return malformedFromIndex(tokens, text, textStartIndex, index);
588
+ if (index > textStartIndex) tokens.push({
589
+ type: "text",
590
+ value: text.slice(textStartIndex, index)
591
+ });
592
+ tokens.push({
593
+ type: c1ControlString.type,
594
+ value: text.slice(index, controlStringTerminatorIndex)
595
+ });
596
+ index = controlStringTerminatorIndex;
597
+ textStartIndex = index;
598
+ continue;
599
+ }
600
+ if (character === stringTerminatorCharacter) {
601
+ if (index > textStartIndex) tokens.push({
602
+ type: "text",
603
+ value: text.slice(textStartIndex, index)
604
+ });
605
+ tokens.push({
606
+ type: "st",
607
+ value: character
608
+ });
609
+ index++;
610
+ textStartIndex = index;
611
+ continue;
612
+ }
613
+ if (isC1ControlCharacter(character)) {
614
+ if (index > textStartIndex) tokens.push({
615
+ type: "text",
616
+ value: text.slice(textStartIndex, index)
617
+ });
618
+ tokens.push({
619
+ type: "c1",
620
+ value: character
621
+ });
622
+ index++;
623
+ textStartIndex = index;
624
+ continue;
625
+ }
626
+ index++;
627
+ }
628
+ if (textStartIndex < text.length) tokens.push({
629
+ type: "text",
630
+ value: text.slice(textStartIndex)
631
+ });
632
+ return tokens;
633
+ };
634
+ //#endregion
635
+ //#region src/paint/sanitize-ansi.ts
636
+ const sgrParametersRegex = /^[\d:;]*$/;
637
+ function sanitizeAnsi(text) {
638
+ if (!hasAnsiControlCharacters(text)) return text;
639
+ let output = "";
640
+ for (const token of tokenizeAnsi(text)) {
641
+ if (token.type === "text" || token.type === "osc") {
642
+ output += token.value;
643
+ continue;
644
+ }
645
+ if (token.type === "csi" && token.finalCharacter === "m" && token.intermediateString === "" && sgrParametersRegex.test(token.parameterString)) output += token.value;
646
+ }
647
+ return output;
648
+ }
649
+ //#endregion
650
+ //#region src/host/text-measure.ts
651
+ function flattenLeaves(node) {
652
+ if (!node.children || node.children.length === 0) return "";
653
+ let out = "";
654
+ let transformIndex = 0;
655
+ for (const child of node.children) {
656
+ out += squashTransformChild$1(child, transformIndex);
657
+ if (advancesLineIndex(child)) transformIndex++;
658
+ }
659
+ return sanitizeAnsi(out);
660
+ }
661
+ function squashTransformChild$1(child, index) {
662
+ if (child.type === "text-leaf") return child.value;
663
+ if (child.type === "tui-virtual-text" || child.type === "tui-text") return flattenLeaves(child);
664
+ if (child.type === "tui-transform") {
665
+ let innerText = "";
666
+ let grandIndex = 0;
667
+ for (const grandchild of child.children) {
668
+ innerText += squashTransformChild$1(grandchild, grandIndex);
669
+ if (advancesLineIndex(grandchild)) grandIndex++;
670
+ }
671
+ if (innerText.length > 0 && child.transform) innerText = child.transform(innerText, index);
672
+ return innerText;
673
+ }
674
+ return "";
675
+ }
676
+ function flattenTransformLeaves(node) {
677
+ if (!node.children || node.children.length === 0) return "";
678
+ let out = "";
679
+ let transformIndex = 0;
680
+ for (const child of node.children) {
681
+ out += squashTransformChild$1(child, transformIndex);
682
+ if (advancesLineIndex(child)) transformIndex++;
683
+ }
684
+ return sanitizeAnsi(out);
685
+ }
686
+ /**
687
+ * Slice `text` from the start so the result is at most `maxCols` columns wide.
688
+ * `slice-ansi` can overshoot when a wide character straddles the boundary, so
689
+ * we reduce the slice position until the result fits.
690
+ */
691
+ function safeSliceEnd(text, maxCols) {
692
+ if (maxCols <= 0) return "";
693
+ let end = maxCols;
694
+ let sliced = sliceAnsi(text, 0, end);
695
+ let w = stringWidth(sliced);
696
+ while (w > maxCols && end > 0) {
697
+ end--;
698
+ sliced = sliceAnsi(text, 0, end);
699
+ w = stringWidth(sliced);
700
+ }
701
+ return sliced;
702
+ }
703
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
704
+ /**
705
+ * Strip ALL ANSI from `text`, returning only its visible code points. Reuses the paint
706
+ * tokenizer (the same one sanitizeAnsi uses) rather than a strip-ansi regex dep: every
707
+ * non-`text` token — SGR, OSC hyperlinks, control strings — is dropped, so the result is the
708
+ * exact visible string wrap-ansi must lay out. (wrap-ansi recognises SGR/OSC8 and would
709
+ * byte-split SGR at width<=0; feeding it the plain string sidesteps that bug entirely.)
710
+ */
711
+ function stripAnsi(text) {
712
+ let out = "";
713
+ for (const token of tokenizeAnsi(text)) if (token.type === "text") out += token.value;
714
+ return out;
715
+ }
716
+ /**
717
+ * Replicate wrap-ansi's width<=0 layout for a (possibly STYLED) string, ANSI-awarely.
718
+ *
719
+ * The output's LINE STRUCTURE exactly equals wrap-ansi's authoritative width-0 layout for the
720
+ * given wrap `mode` — `wrap` uses `{hard:true, trim:false}`, `hard` uses
721
+ * `{hard:true, trim:false, wordWrap:false}`, mirroring Ink's wrap-text.ts (the SOLE difference
722
+ * between the two modes). At width 0 `wordWrap:false` makes wrap-ansi emit an EXTRA blank row
723
+ * before each interior word's first grapheme (wrapAnsi("a b c",0,…,wordWrap:false) =
724
+ * ["","a"," ","","b"," ","","c"] vs `wrap`'s ["","a"," ","b"," ","c"]), so `hard` measures
725
+ * taller than `wrap` — which is exactly Ink's behavior. Threading `mode` here keeps both
726
+ * line-counts in lockstep with Ink instead of measuring `hard` with `wrap` structure.
727
+ *
728
+ * wrap-ansi breaks BEFORE each grapheme it cannot fit, so an interior zero-width grapheme
729
+ * (ZWSP/ZWNJ/ZWJ, combining mark, VS16, soft-hyphen, BOM) lands on its OWN row with the
730
+ * surrounding `""` blanks — but a TRAILING zero-width run (nothing visible after it) stays glued
731
+ * to the preceding grapheme's row (wrapAnsi("中​",0)=["","中​"], not ["","中","​"]). Deriving
732
+ * structure from wrap-ansi on the plain string reproduces both cases for free; the old
733
+ * column-stepping `slice(col,col+1)` could not (it glued an interior zero-width onto the next
734
+ * grapheme → line-count too low, and `break`-ed on a leading zero-width + wide glyph → dropped
735
+ * the rest of the line).
736
+ *
737
+ * Styling is re-applied in lockstep: the plain and styled strings share an identical grapheme
738
+ * sequence (SGR/OSC are zero-width), so each NON-EMPTY plain line maps to a contiguous run of
739
+ * graphemes (USUALLY one, but a trailing zero-width run makes it several — e.g. "中​"). We slice
740
+ * that run out of the STYLED text with slice-ansi via the same slot model wrap-ansi's plain
741
+ * layout implies, so slice-ansi re-emits the active SGR span around it (e.g. "\x1b[41mA\x1b[49m")
742
+ * and keeps a wide glyph whole — matching Ink's per-grapheme colored output. We never let
743
+ * wrap-ansi touch the styled string (it byte-splits the escapes at width<=0); we only ask it
744
+ * for structure.
745
+ */
746
+ function wrapZeroWidthAnsi(text, mode) {
747
+ text = text.normalize("NFC");
748
+ const result = [];
749
+ const styledLines = text.split("\n");
750
+ const wrapOptions = mode === "hard" ? {
751
+ hard: true,
752
+ trim: false,
753
+ wordWrap: false
754
+ } : {
755
+ hard: true,
756
+ trim: false
757
+ };
758
+ for (const styledLine of styledLines) {
759
+ const plainLine = stripAnsi(styledLine);
760
+ const plainLines = wrapAnsi(plainLine, 0, wrapOptions).split("\n");
761
+ const slotEnds = [];
762
+ let slot = 0;
763
+ for (const { segment } of graphemeSegmenter.segment(plainLine)) {
764
+ slot += Math.max(1, stringWidth(segment));
765
+ slotEnds.push(slot);
766
+ }
767
+ let graphemeIndex = 0;
768
+ for (const line of plainLines) {
769
+ if (line === "") {
770
+ result.push("");
771
+ continue;
772
+ }
773
+ const startSlot = graphemeIndex === 0 ? 0 : slotEnds[graphemeIndex - 1];
774
+ const graphemeCount = [...graphemeSegmenter.segment(line)].length;
775
+ graphemeIndex += graphemeCount;
776
+ const endSlot = slotEnds[graphemeIndex - 1] ?? startSlot;
777
+ result.push(sliceAnsi(styledLine, startSlot, endSlot));
778
+ }
779
+ }
780
+ return result;
781
+ }
782
+ function wrapText(text, width, mode = "wrap") {
783
+ if (mode === "wrap" || mode === "hard") {
784
+ if (measureTextNatural(text).width <= width) return text.split("\n");
785
+ if (width <= 0) return wrapZeroWidthAnsi(text, mode);
786
+ if (mode === "wrap") return wrapAnsi(text, width, {
787
+ hard: true,
788
+ trim: false
789
+ }).split("\n");
790
+ return wrapAnsi(text, width, {
791
+ hard: true,
792
+ trim: false,
793
+ wordWrap: false
794
+ }).split("\n");
795
+ }
796
+ const lines = text.split("\n");
797
+ if (lines.every((l) => stringWidth(l) <= width)) return lines;
798
+ return cliTruncate(text, width, { position: mode === "truncate-start" ? "start" : mode === "truncate-middle" ? "middle" : "end" }).split("\n");
799
+ }
800
+ /**
801
+ * Natural (unwrapped) dimensions of `text`, mode-independent. Mirrors Ink's
802
+ * measure-text.js: width = widest line, height = number of \n-separated lines.
803
+ */
804
+ function measureTextNatural(text) {
805
+ const lines = text.split("\n");
806
+ let width = 0;
807
+ for (const line of lines) width = Math.max(width, stringWidth(line));
808
+ return {
809
+ width,
810
+ height: lines.length
811
+ };
812
+ }
813
+ //#endregion
814
+ //#region src/host/yoga.ts
815
+ let _createCount = 0;
816
+ let _freeCount = 0;
817
+ function createYogaNode() {
818
+ _createCount++;
819
+ return Yoga.Node.create();
820
+ }
821
+ function freeYogaNode(node) {
822
+ _freeCount++;
823
+ node.free();
824
+ }
825
+ const yogaNodeTracker = {
826
+ reset() {
827
+ _createCount = 0;
828
+ _freeCount = 0;
829
+ },
830
+ snapshot() {
831
+ return {
832
+ created: _createCount,
833
+ freed: _freeCount,
834
+ live: _createCount - _freeCount
835
+ };
836
+ }
837
+ };
838
+ function hasYoga$1(node) {
839
+ return node.type === "root" || node.type === "tui-box" || node.type === "tui-text" || node.type === "tui-static" || node.type === "tui-transform";
840
+ }
841
+ function attachYoga(node) {
842
+ node.yoga = createYogaNode();
843
+ if (node.type === "tui-static") node.yoga.setDisplay(Yoga.DISPLAY_NONE);
844
+ if (node.type === "tui-box") {
845
+ node.yoga.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
846
+ node.yoga.setFlexShrink(1);
847
+ node.yoga.setFlexWrap(Yoga.WRAP_NO_WRAP);
848
+ node.yoga.setFlexGrow(0);
849
+ }
850
+ if (node.type === "tui-text") {
851
+ node.yoga.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
852
+ node.yoga.setFlexShrink(1);
853
+ node.yoga.setFlexGrow(0);
854
+ }
855
+ if (node.type === "tui-transform") {
856
+ node.yoga.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
857
+ node.yoga.setFlexShrink(1);
858
+ node.yoga.setFlexGrow(0);
859
+ bindTransformMeasure(node);
860
+ }
861
+ }
862
+ /** A transform yoga node has at least one yoga-carrying child (e.g. a <Text>). */
863
+ function transformHasYogaChild(node) {
864
+ return node.yoga.getChildCount() > 0;
865
+ }
866
+ function detachYoga(node) {
867
+ freeYogaNode(node.yoga);
868
+ }
869
+ function yogaIndexFor(parent, child) {
870
+ const isTextParent = parent.type === "tui-text" || parent.type === "tui-virtual-text" || parent.type === "tui-transform";
871
+ let yIdx = 0;
872
+ for (const sibling of parent.children) {
873
+ if (sibling === child) return yIdx;
874
+ if (hasYoga$1(sibling)) {
875
+ if (isTextParent && sibling.type === "tui-transform") continue;
876
+ yIdx++;
877
+ }
878
+ }
879
+ return yIdx;
880
+ }
881
+ function insertYogaChild(parent, child, _domIndex) {
882
+ if (!hasYoga$1(parent) || !hasYoga$1(child)) return;
883
+ if (child.type === "tui-transform" && (parent.type === "tui-text" || parent.type === "tui-transform")) return;
884
+ if (parent.type === "tui-transform") parent.yoga.unsetMeasureFunc();
885
+ const childYoga = child.yoga;
886
+ const currentYogaParent = childYoga.getParent();
887
+ if (currentYogaParent) currentYogaParent.removeChild(childYoga);
888
+ const yIdx = yogaIndexFor(parent, child);
889
+ parent.yoga.insertChild(childYoga, yIdx);
890
+ }
891
+ function removeYogaChild(parent, child) {
892
+ if (!hasYoga$1(parent) || !hasYoga$1(child)) return;
893
+ if (child.type === "tui-transform" && (parent.type === "tui-text" || parent.type === "tui-transform")) return;
894
+ const childYoga = child.yoga;
895
+ const currentYogaParent = childYoga.getParent();
896
+ if (currentYogaParent) currentYogaParent.removeChild(childYoga);
897
+ if (parent.type === "tui-transform" && parent.yoga.getChildCount() === 0) bindTransformMeasure(parent);
898
+ }
899
+ const YOGA_PROP_SETTERS = {
900
+ width: (n, v) => {
901
+ if (typeof v === "number") n.setWidth(v);
902
+ else if (typeof v === "string") n.setWidthPercent(Number.parseInt(v, 10));
903
+ else n.setWidthAuto();
904
+ },
905
+ height: (n, v) => {
906
+ if (typeof v === "number") n.setHeight(v);
907
+ else if (typeof v === "string") n.setHeightPercent(Number.parseInt(v, 10));
908
+ else n.setHeightAuto();
909
+ },
910
+ minWidth: (n, v) => typeof v === "string" ? n.setMinWidthPercent(Number.parseInt(v, 10)) : n.setMinWidth(v == null ? 0 : v),
911
+ minHeight: (n, v) => typeof v === "string" ? n.setMinHeightPercent(Number.parseInt(v, 10)) : n.setMinHeight(v == null ? 0 : v),
912
+ flexGrow: (n, v) => n.setFlexGrow(v == null ? 0 : v),
913
+ flexShrink: (n, v) => n.setFlexShrink(v == null ? 1 : v),
914
+ flexBasis: (n, v) => {
915
+ if (typeof v === "number") n.setFlexBasis(v);
916
+ else if (typeof v === "string") n.setFlexBasisPercent(Number.parseInt(v, 10));
917
+ else n.setFlexBasisAuto();
918
+ },
919
+ flexDirection: (n, v) => n.setFlexDirection(v == null ? Yoga.FLEX_DIRECTION_ROW : toFlexDirection(v)),
920
+ flexWrap: (n, v) => n.setFlexWrap(v == null ? Yoga.WRAP_NO_WRAP : toFlexWrap(v)),
921
+ alignItems: (n, v) => n.setAlignItems(v == null ? Yoga.ALIGN_STRETCH : toAlign(v)),
922
+ alignSelf: (n, v) => n.setAlignSelf(v == null ? Yoga.ALIGN_AUTO : toAlign(v)),
923
+ justifyContent: (n, v) => n.setJustifyContent(v == null ? Yoga.JUSTIFY_FLEX_START : toJustify(v)),
924
+ gap: (n, v) => n.setGap(Yoga.GUTTER_ALL, v == null ? 0 : v),
925
+ columnGap: (n, v) => n.setGap(Yoga.GUTTER_COLUMN, v == null ? 0 : v),
926
+ rowGap: (n, v) => n.setGap(Yoga.GUTTER_ROW, v == null ? 0 : v),
927
+ margin: () => {},
928
+ marginX: () => {},
929
+ marginY: () => {},
930
+ marginTop: () => {},
931
+ marginBottom: () => {},
932
+ marginLeft: () => {},
933
+ marginRight: () => {},
934
+ padding: () => {},
935
+ paddingX: () => {},
936
+ paddingY: () => {},
937
+ paddingTop: () => {},
938
+ paddingBottom: () => {},
939
+ paddingLeft: () => {},
940
+ paddingRight: () => {},
941
+ borderStyle: () => {},
942
+ borderTop: () => {},
943
+ borderBottom: () => {},
944
+ borderLeft: () => {},
945
+ borderRight: () => {},
946
+ display: (n, v) => n.setDisplay(v != null && v !== "flex" ? Yoga.DISPLAY_NONE : Yoga.DISPLAY_FLEX),
947
+ overflow: (_n, _v) => {},
948
+ overflowX: (_n, _v) => {},
949
+ overflowY: (_n, _v) => {},
950
+ maxWidth: (n, v) => typeof v === "string" ? n.setMaxWidthPercent(Number.parseInt(v, 10)) : n.setMaxWidth(v == null ? NaN : v),
951
+ maxHeight: (n, v) => typeof v === "string" ? n.setMaxHeightPercent(Number.parseInt(v, 10)) : n.setMaxHeight(v == null ? NaN : v),
952
+ aspectRatio: (n, v) => v == null ? n.setAspectRatio(void 0) : n.setAspectRatio(v),
953
+ alignContent: (n, v) => v == null ? n.setAlignContent(Yoga.ALIGN_FLEX_START) : n.setAlignContent(toAlign(v)),
954
+ position: (n, v) => n.setPositionType(toPosition(v)),
955
+ top: (n, v) => typeof v === "string" ? n.setPositionPercent(Yoga.EDGE_TOP, Number.parseFloat(v)) : n.setPosition(Yoga.EDGE_TOP, v == null ? NaN : v),
956
+ right: (n, v) => typeof v === "string" ? n.setPositionPercent(Yoga.EDGE_RIGHT, Number.parseFloat(v)) : n.setPosition(Yoga.EDGE_RIGHT, v == null ? NaN : v),
957
+ bottom: (n, v) => typeof v === "string" ? n.setPositionPercent(Yoga.EDGE_BOTTOM, Number.parseFloat(v)) : n.setPosition(Yoga.EDGE_BOTTOM, v == null ? NaN : v),
958
+ left: (n, v) => typeof v === "string" ? n.setPositionPercent(Yoga.EDGE_LEFT, Number.parseFloat(v)) : n.setPosition(Yoga.EDGE_LEFT, v == null ? NaN : v)
959
+ };
960
+ function toFlexDirection(v) {
961
+ return {
962
+ row: Yoga.FLEX_DIRECTION_ROW,
963
+ "row-reverse": Yoga.FLEX_DIRECTION_ROW_REVERSE,
964
+ column: Yoga.FLEX_DIRECTION_COLUMN,
965
+ "column-reverse": Yoga.FLEX_DIRECTION_COLUMN_REVERSE
966
+ }[v];
967
+ }
968
+ function toFlexWrap(v) {
969
+ return {
970
+ nowrap: Yoga.WRAP_NO_WRAP,
971
+ wrap: Yoga.WRAP_WRAP,
972
+ "wrap-reverse": Yoga.WRAP_WRAP_REVERSE
973
+ }[v];
974
+ }
975
+ function toAlign(v) {
976
+ return {
977
+ auto: Yoga.ALIGN_AUTO,
978
+ "flex-start": Yoga.ALIGN_FLEX_START,
979
+ center: Yoga.ALIGN_CENTER,
980
+ "flex-end": Yoga.ALIGN_FLEX_END,
981
+ stretch: Yoga.ALIGN_STRETCH,
982
+ baseline: Yoga.ALIGN_BASELINE,
983
+ "space-between": Yoga.ALIGN_SPACE_BETWEEN,
984
+ "space-around": Yoga.ALIGN_SPACE_AROUND,
985
+ "space-evenly": Yoga.ALIGN_SPACE_EVENLY
986
+ }[v];
987
+ }
988
+ function toPosition(v) {
989
+ if (!v || v === "relative") return Yoga.POSITION_TYPE_RELATIVE;
990
+ if (v === "absolute") return Yoga.POSITION_TYPE_ABSOLUTE;
991
+ return Yoga.POSITION_TYPE_STATIC;
992
+ }
993
+ function toJustify(v) {
994
+ return {
995
+ "flex-start": Yoga.JUSTIFY_FLEX_START,
996
+ center: Yoga.JUSTIFY_CENTER,
997
+ "flex-end": Yoga.JUSTIFY_FLEX_END,
998
+ "space-between": Yoga.JUSTIFY_SPACE_BETWEEN,
999
+ "space-around": Yoga.JUSTIFY_SPACE_AROUND,
1000
+ "space-evenly": Yoga.JUSTIFY_SPACE_EVENLY
1001
+ }[v];
1002
+ }
1003
+ function isYogaProp(key) {
1004
+ return Object.hasOwn(YOGA_PROP_SETTERS, key);
1005
+ }
1006
+ /** Props whose change requires recomputing the yoga border-edge widths. */
1007
+ const BORDER_PROPS = new Set([
1008
+ "borderStyle",
1009
+ "borderTop",
1010
+ "borderBottom",
1011
+ "borderLeft",
1012
+ "borderRight"
1013
+ ]);
1014
+ /**
1015
+ * Recompute all four yoga border-edge widths from a box's full prop set, mirroring
1016
+ * Ink's applyBorderStyles (styles.ts:729-763): the per-side width is
1017
+ * `borderStyle ? 1 : 0`, then each edge is forced to 0 when that edge's per-edge
1018
+ * prop is explicitly `false`. So a per-edge toggle can only SUBTRACT an edge — it
1019
+ * can NEVER add width without a borderStyle. This is the joint computation a
1020
+ * single yoga setter cannot do (it sees only one value), and it must run on ANY
1021
+ * border-prop change, including borderStyle flipping in EITHER direction
1022
+ * (set→unset re-zeroes, unset→set re-reserves) — otherwise a per-edge toggle made
1023
+ * while borderStyle stays unset would leave a spurious 1-cell inset with no border
1024
+ * drawn (the per-edge props default to `true`).
1025
+ */
1026
+ function reconcileBorderEdges(node, props) {
1027
+ const y = node.yoga;
1028
+ const borderWidth = props["borderStyle"] ? 1 : 0;
1029
+ y.setBorder(Yoga.EDGE_TOP, props["borderTop"] === false ? 0 : borderWidth);
1030
+ y.setBorder(Yoga.EDGE_BOTTOM, props["borderBottom"] === false ? 0 : borderWidth);
1031
+ y.setBorder(Yoga.EDGE_LEFT, props["borderLeft"] === false ? 0 : borderWidth);
1032
+ y.setBorder(Yoga.EDGE_RIGHT, props["borderRight"] === false ? 0 : borderWidth);
1033
+ }
1034
+ /** Props whose change requires recomputing the yoga margin edges. */
1035
+ const MARGIN_PROPS = new Set([
1036
+ "margin",
1037
+ "marginX",
1038
+ "marginY",
1039
+ "marginTop",
1040
+ "marginBottom",
1041
+ "marginLeft",
1042
+ "marginRight"
1043
+ ]);
1044
+ /** Props whose change requires recomputing the yoga padding edges. */
1045
+ const PADDING_PROPS = new Set([
1046
+ "padding",
1047
+ "paddingX",
1048
+ "paddingY",
1049
+ "paddingTop",
1050
+ "paddingBottom",
1051
+ "paddingLeft",
1052
+ "paddingRight"
1053
+ ]);
1054
+ function present(props, key) {
1055
+ const v = props[key];
1056
+ return v != null && v !== "" && Number.isFinite(Number(v));
1057
+ }
1058
+ /**
1059
+ * Recompute all four PHYSICAL margin edges from a box's full prop set. Each edge
1060
+ * resolves with most-specific-wins precedence (specific edge → axis → all → 0):
1061
+ * top = marginTop ?? marginY ?? margin ?? 0 (etc.)
1062
+ * then the four physical edges are set and the composite edges (ALL/HORIZONTAL/
1063
+ * VERTICAL) are ZEROED so nothing layers on top of them.
1064
+ *
1065
+ * Why this and not the obvious per-setter mapping (margin→EDGE_ALL, marginX→
1066
+ * EDGE_HORIZONTAL, marginTop→EDGE_TOP, …): an edge depends on up to THREE props
1067
+ * together and a single yoga setter sees only one. Per yoga edge precedence a more
1068
+ * specific edge OVERRIDES a composite EVEN WHEN SET TO 0, so resetting a withdrawn
1069
+ * `marginTop` to 0 (the old code) still beats a surviving `margin={5}` →
1070
+ * `getComputedMargin(TOP)` collapsed to 0 instead of 5. Resolving every physical
1071
+ * edge from el.props and zeroing the composites removes that layering entirely, so
1072
+ * a withdrawn override falls back to whatever shorthand still applies. Verified
1073
+ * against yoga-layout@3.2.1 to produce identical getComputedMargin for the SET path
1074
+ * as the old per-setter code across representative combinations. (G19)
1075
+ *
1076
+ * NOTE the EDGE_START/END (not LEFT/RIGHT) mapping for left/right is preserved from
1077
+ * the prior margin setters — margin uses start/end edges, padding uses left/right.
1078
+ */
1079
+ function reconcileMarginEdges(node, props) {
1080
+ const y = node.yoga;
1081
+ const pick = (specific, axis) => {
1082
+ if (present(props, specific)) return Number(props[specific]);
1083
+ if (present(props, axis)) return Number(props[axis]);
1084
+ if (present(props, "margin")) return Number(props["margin"]);
1085
+ return 0;
1086
+ };
1087
+ y.setMargin(Yoga.EDGE_TOP, pick("marginTop", "marginY"));
1088
+ y.setMargin(Yoga.EDGE_BOTTOM, pick("marginBottom", "marginY"));
1089
+ y.setMargin(Yoga.EDGE_START, pick("marginLeft", "marginX"));
1090
+ y.setMargin(Yoga.EDGE_END, pick("marginRight", "marginX"));
1091
+ y.setMargin(Yoga.EDGE_ALL, 0);
1092
+ y.setMargin(Yoga.EDGE_HORIZONTAL, 0);
1093
+ y.setMargin(Yoga.EDGE_VERTICAL, 0);
1094
+ }
1095
+ /**
1096
+ * Padding analogue of {@link reconcileMarginEdges}. Same precedence and composite-
1097
+ * zeroing, but padding maps left/right to EDGE_LEFT/EDGE_RIGHT (margin uses
1098
+ * START/END) — preserving the prior padding setters' edge mapping. (G19)
1099
+ */
1100
+ function reconcilePaddingEdges(node, props) {
1101
+ const y = node.yoga;
1102
+ const pick = (specific, axis) => {
1103
+ if (present(props, specific)) return Number(props[specific]);
1104
+ if (present(props, axis)) return Number(props[axis]);
1105
+ if (present(props, "padding")) return Number(props["padding"]);
1106
+ return 0;
1107
+ };
1108
+ y.setPadding(Yoga.EDGE_TOP, pick("paddingTop", "paddingY"));
1109
+ y.setPadding(Yoga.EDGE_BOTTOM, pick("paddingBottom", "paddingY"));
1110
+ y.setPadding(Yoga.EDGE_LEFT, pick("paddingLeft", "paddingX"));
1111
+ y.setPadding(Yoga.EDGE_RIGHT, pick("paddingRight", "paddingX"));
1112
+ y.setPadding(Yoga.EDGE_ALL, 0);
1113
+ y.setPadding(Yoga.EDGE_HORIZONTAL, 0);
1114
+ y.setPadding(Yoga.EDGE_VERTICAL, 0);
1115
+ }
1116
+ const RESETTABLE_PROPS = new Set([
1117
+ "width",
1118
+ "height",
1119
+ "maxWidth",
1120
+ "maxHeight",
1121
+ "aspectRatio",
1122
+ "alignContent",
1123
+ "top",
1124
+ "right",
1125
+ "bottom",
1126
+ "left",
1127
+ "minWidth",
1128
+ "minHeight",
1129
+ "flexGrow",
1130
+ "flexShrink",
1131
+ "flexBasis",
1132
+ "flexDirection",
1133
+ "flexWrap",
1134
+ "alignItems",
1135
+ "alignSelf",
1136
+ "justifyContent",
1137
+ "gap",
1138
+ "columnGap",
1139
+ "rowGap",
1140
+ "margin",
1141
+ "marginX",
1142
+ "marginY",
1143
+ "marginTop",
1144
+ "marginBottom",
1145
+ "marginLeft",
1146
+ "marginRight",
1147
+ "padding",
1148
+ "paddingX",
1149
+ "paddingY",
1150
+ "paddingTop",
1151
+ "paddingBottom",
1152
+ "paddingLeft",
1153
+ "paddingRight",
1154
+ "position",
1155
+ "display"
1156
+ ]);
1157
+ function applyYogaProp(node, key, value, prev) {
1158
+ const setter = YOGA_PROP_SETTERS[key];
1159
+ if (!setter) return;
1160
+ if (value == null) if (key === "borderStyle") {} else if (RESETTABLE_PROPS.has(key) && prev !== null && prev !== void 0) {} else return;
1161
+ setter(node.yoga, value);
1162
+ }
1163
+ function bindTextMeasure(text) {
1164
+ text.yoga.setMeasureFunc((availableWidth) => {
1165
+ const raw = flattenLeaves(text);
1166
+ text.measuredCache = raw;
1167
+ if (raw === "") return {
1168
+ width: 0,
1169
+ height: 0
1170
+ };
1171
+ const natural = measureTextNatural(raw);
1172
+ if (natural.width <= availableWidth) return natural;
1173
+ if (natural.width >= 1 && availableWidth > 0 && availableWidth < 1) return natural;
1174
+ return measureTextNatural(wrapText(raw, availableWidth, text.props.wrap ?? "wrap").join("\n"));
1175
+ });
1176
+ }
1177
+ function markTextDirty(text) {
1178
+ text.yoga.markDirty();
1179
+ }
1180
+ function bindTransformMeasure(node) {
1181
+ node.yoga.setMeasureFunc((availableWidth) => {
1182
+ const raw = flattenTransformLeaves(node);
1183
+ if (raw === "") return {
1184
+ width: 0,
1185
+ height: 0
1186
+ };
1187
+ const natural = measureTextNatural(raw);
1188
+ if (natural.width <= availableWidth) return natural;
1189
+ if (natural.width >= 1 && availableWidth > 0 && availableWidth < 1) return natural;
1190
+ return measureTextNatural(wrapText(raw, availableWidth, "wrap").join("\n"));
1191
+ });
1192
+ }
1193
+ function markTransformDirty(node) {
1194
+ node.yoga.markDirty();
1195
+ }
1196
+ //#endregion
1197
+ //#region src/host/node-ops.ts
1198
+ const STYLE_PROPS = new Set([
1199
+ "color",
1200
+ "backgroundColor",
1201
+ "dimColor",
1202
+ "bold",
1203
+ "italic",
1204
+ "underline",
1205
+ "strikethrough",
1206
+ "inverse",
1207
+ "wrap",
1208
+ "borderStyle",
1209
+ "borderColor",
1210
+ "borderDimColor",
1211
+ "borderTopColor",
1212
+ "borderBottomColor",
1213
+ "borderLeftColor",
1214
+ "borderRightColor",
1215
+ "borderTopDimColor",
1216
+ "borderBottomDimColor",
1217
+ "borderLeftDimColor",
1218
+ "borderRightDimColor",
1219
+ "borderBackgroundColor",
1220
+ "borderTopBackgroundColor",
1221
+ "borderBottomBackgroundColor",
1222
+ "borderLeftBackgroundColor",
1223
+ "borderRightBackgroundColor",
1224
+ "borderTop",
1225
+ "borderBottom",
1226
+ "borderLeft",
1227
+ "borderRight",
1228
+ "overflow",
1229
+ "overflowX",
1230
+ "overflowY",
1231
+ "margin",
1232
+ "marginX",
1233
+ "marginY",
1234
+ "marginTop",
1235
+ "marginBottom",
1236
+ "marginLeft",
1237
+ "marginRight",
1238
+ "padding",
1239
+ "paddingX",
1240
+ "paddingY",
1241
+ "paddingTop",
1242
+ "paddingBottom",
1243
+ "paddingLeft",
1244
+ "paddingRight"
1245
+ ]);
1246
+ /** Walk up the DOM tree to find the root node. */
1247
+ function findRoot(node) {
1248
+ let current = node;
1249
+ while (current) {
1250
+ if (current.type === "root") return current;
1251
+ current = current.parent;
1252
+ }
1253
+ return null;
1254
+ }
1255
+ /**
1256
+ * Walk up the DOM tree to check if we're inside a text context. Treats a
1257
+ * <Transform> as a text context too: Ink models <Transform> as an ink-text host
1258
+ * (its reconciler sets isInsideText for ink-text), so this is the solid-tui
1259
+ * equivalent of Ink's hostContext.isInsideText. It governs BOTH text-context
1260
+ * guards, exactly as Ink's single isInsideText flag does:
1261
+ *
1262
+ * - a bare-string / <Newline> child directly inside a <Transform> is valid
1263
+ * inline text and must NOT trip the "text must be rendered inside <Text>"
1264
+ * guard (G58); and
1265
+ * - a <Box> directly inside a <Transform> MUST throw the same dev error as a
1266
+ * <Box> inside a <Text> (Ink reconciler.ts:205 throws for any isInsideText
1267
+ * context, ink-text included) (G58 should-fix).
1268
+ */
1269
+ function isInsideTextOrTransformContext(node) {
1270
+ let current = node;
1271
+ while (current) {
1272
+ if (current.type === "tui-text" || current.type === "tui-virtual-text" || current.type === "tui-transform") return true;
1273
+ current = current.parent;
1274
+ }
1275
+ return false;
1276
+ }
1277
+ /**
1278
+ * Find the nearest ancestor (inclusive of `start`) that OWNS the yoga measure
1279
+ * func used to size inline text — i.e. the node yoga must re-measure when a
1280
+ * descendant text-leaf changes. Mirrors Ink's findClosestYogaNode (dom.ts:248),
1281
+ * adapted for the fact that solid-tui attaches a yoga node to every <Transform>:
1282
+ *
1283
+ * - A <Text> always owns its measure func.
1284
+ * - A <Transform> owns the measure func ONLY when it is STANDALONE: it has no
1285
+ * yoga-bearing child (so it still carries bindTransformMeasure) AND it is not
1286
+ * itself nested in a text/transform context (an inline transform is squashed
1287
+ * into the enclosing measure owner, just as Ink's ink-virtual-text — which has
1288
+ * no yoga node — is climbed past). An inline transform is therefore skipped and
1289
+ * the walk continues to the enclosing <Text>/standalone <Transform>. (G58)
1290
+ */
1291
+ function findMeasureOwner(start) {
1292
+ let p = start;
1293
+ while (p) {
1294
+ if (p.type === "tui-text") return p;
1295
+ if (p.type === "tui-transform") {
1296
+ const inlineInTextContext = p.parent != null && isContainer(p.parent) && isInsideTextOrTransformContext(p.parent);
1297
+ if (!transformHasYogaChild(p) && !inlineInTextContext) return p;
1298
+ }
1299
+ p = p.parent;
1300
+ }
1301
+ return null;
1302
+ }
1303
+ /**
1304
+ * Dirty the measure owner of a text-context parent after a STRUCTURAL change to
1305
+ * its children (insert / remove / move). Mirrors Ink marking the parent dirty in
1306
+ * appendChildNode / insertBeforeNode / removeChildNode for ink-text AND
1307
+ * ink-virtual-text parents (dom.ts:132,165,185) then climbing to the closest
1308
+ * yoga node (findClosestYogaNode, dom.ts:248). A no-op when `parent` is not a
1309
+ * text context (box / root / static structural changes are sized by yoga
1310
+ * directly, no measure func to invalidate).
1311
+ */
1312
+ function dirtyTextMeasureOwner(parent) {
1313
+ if (parent.type !== "tui-text" && parent.type !== "tui-virtual-text" && parent.type !== "tui-transform") return;
1314
+ const owner = findMeasureOwner(parent);
1315
+ if (owner?.type === "tui-transform") markTransformDirty(owner);
1316
+ else if (owner?.type === "tui-text") markTextDirty(owner);
1317
+ }
1318
+ /**
1319
+ * Whether a text-leaf carrying `value` would be REJECTED by the text-context
1320
+ * guard if inserted into `parent`. A bare string must live inside a <Text>
1321
+ * context; an EMPTY text-leaf is exempt (Solid uses empty text-leaves as fragment
1322
+ * anchors / its common clear path). Shared by `insert()` (its existing guard)
1323
+ * and `setElementText()` (its pre-remove validation) so the two cannot drift —
1324
+ * the condition must stay identical in both call sites.
1325
+ */
1326
+ function rejectsTextLeaf(parent, value) {
1327
+ return value !== "" && (parent.type === "tui-box" || parent.type === "root" || parent.type === "tui-static") && !isInsideTextOrTransformContext(parent);
1328
+ }
1329
+ function buildNodeOps(options) {
1330
+ const { onCommit } = options;
1331
+ function createElement(type) {
1332
+ switch (type) {
1333
+ case "tui-box": {
1334
+ const n = createBox();
1335
+ attachYoga(n);
1336
+ return n;
1337
+ }
1338
+ case "tui-text": {
1339
+ const n = createText();
1340
+ attachYoga(n);
1341
+ bindTextMeasure(n);
1342
+ return n;
1343
+ }
1344
+ case "tui-virtual-text": return createVirtualText();
1345
+ case "tui-static": {
1346
+ const n = createStatic();
1347
+ attachYoga(n);
1348
+ return n;
1349
+ }
1350
+ case "tui-transform": {
1351
+ const n = createTransform((line) => line);
1352
+ attachYoga(n);
1353
+ return n;
1354
+ }
1355
+ default: throw new Error(`Unknown solid-tui element type: ${type}`);
1356
+ }
1357
+ }
1358
+ function createTextNode(text) {
1359
+ return createTextLeaf(text);
1360
+ }
1361
+ function setText(node, text) {
1362
+ if (node.type !== "text-leaf") throw new Error(`Cannot setText on ${node.type}`);
1363
+ node.value = typeof text === "string" ? text : String(text);
1364
+ const parent = node.parent;
1365
+ if (parent != null && isContainer(parent) && rejectsTextLeaf(parent, node.value)) throw new Error(`Text string "${node.value}" must be rendered inside <Text> component`);
1366
+ const owner = findMeasureOwner(node.parent);
1367
+ if (owner?.type === "tui-text") markTextDirty(owner);
1368
+ else if (owner?.type === "tui-transform") markTransformDirty(owner);
1369
+ onCommit();
1370
+ }
1371
+ function insert(child, parent, anchor) {
1372
+ if (!isContainer(parent)) throw new Error(`Cannot insert into ${parent.type}`);
1373
+ const parentC = parent;
1374
+ if (child.type === "tui-box" && isInsideTextOrTransformContext(parentC)) throw new Error("<Box> can’t be nested inside <Text> component");
1375
+ if (child.type === "text-leaf" && rejectsTextLeaf(parentC, child.value)) throw new Error(`Text string "${child.value}" must be rendered inside <Text> component`);
1376
+ if (child.parent) {
1377
+ const oldParent = child.parent;
1378
+ const oldIdx = oldParent.children.indexOf(child);
1379
+ if (oldIdx >= 0) oldParent.children.splice(oldIdx, 1);
1380
+ removeYogaChild(oldParent, child);
1381
+ dirtyTextMeasureOwner(oldParent);
1382
+ }
1383
+ const idx = anchor ? parentC.children.indexOf(anchor) : parentC.children.length;
1384
+ parentC.children.splice(idx < 0 ? parentC.children.length : idx, 0, child);
1385
+ child.parent = parentC;
1386
+ insertYogaChild(parentC, child, idx);
1387
+ dirtyTextMeasureOwner(parentC);
1388
+ if (child.type === "tui-static") {
1389
+ const root = findRoot(child);
1390
+ if (root) root.staticNode = child;
1391
+ }
1392
+ onCommit();
1393
+ }
1394
+ function remove(child) {
1395
+ const parent = child.parent;
1396
+ if (!parent) return;
1397
+ if (child.type === "tui-static") {
1398
+ const root = findRoot(child);
1399
+ if (root && root.staticNode === child) root.staticNode = void 0;
1400
+ }
1401
+ const idx = parent.children.indexOf(child);
1402
+ if (idx >= 0) parent.children.splice(idx, 1);
1403
+ removeYogaChild(parent, child);
1404
+ freeSubtreeYoga(child);
1405
+ child.parent = null;
1406
+ dirtyTextMeasureOwner(parent);
1407
+ onCommit();
1408
+ }
1409
+ /** Recursively free yoga nodes for all yoga-carrying descendants, then the node itself. */
1410
+ function freeSubtreeYoga(node) {
1411
+ if (isContainer(node)) for (const child of node.children) freeSubtreeYoga(child);
1412
+ if (node.type === "tui-box" || node.type === "tui-text" || node.type === "tui-static" || node.type === "tui-transform") detachYoga(node);
1413
+ }
1414
+ function parentNode(node) {
1415
+ return node.parent ?? null;
1416
+ }
1417
+ function nextSibling(node) {
1418
+ const p = node.parent;
1419
+ if (!p) return null;
1420
+ const i = p.children.indexOf(node);
1421
+ if (i < 0) return null;
1422
+ return p.children[i + 1] ?? null;
1423
+ }
1424
+ function patchProp(el, key, prev, next) {
1425
+ if (el.type === "tui-transform") {
1426
+ if (key === "transform" && typeof next === "function") el.transform = next;
1427
+ onCommit();
1428
+ return;
1429
+ }
1430
+ if (el.type === "tui-static" && key === "internal_onWritten") {
1431
+ el.onWritten = typeof next === "function" ? next : void 0;
1432
+ onCommit();
1433
+ return;
1434
+ }
1435
+ if (el.type === "tui-box" || el.type === "tui-text" || el.type === "tui-static" || el.type === "root") {
1436
+ if (isYogaProp(key)) {
1437
+ applyYogaProp(el, key, next, prev);
1438
+ if (STYLE_PROPS.has(key)) el.props[key] = next;
1439
+ if (BORDER_PROPS.has(key)) reconcileBorderEdges(el, el.props);
1440
+ if (MARGIN_PROPS.has(key)) reconcileMarginEdges(el, el.props);
1441
+ if (PADDING_PROPS.has(key)) reconcilePaddingEdges(el, el.props);
1442
+ } else if (STYLE_PROPS.has(key)) {
1443
+ el.props[key] = next;
1444
+ if (key === "wrap" && el.type === "tui-text") markTextDirty(el);
1445
+ } else if (key === "aria-role" || key === "ariaRole") {
1446
+ if (el.type === "tui-box") {
1447
+ el.internal_accessibility ??= {};
1448
+ el.internal_accessibility.role = next;
1449
+ }
1450
+ } else if (key === "aria-state" || key === "ariaState") {
1451
+ if (el.type === "tui-box") {
1452
+ el.internal_accessibility ??= {};
1453
+ el.internal_accessibility.state = next;
1454
+ }
1455
+ } else if (key === "aria-label" || key === "ariaLabel" || key === "aria-hidden" || key === "ariaHidden" || key === "accessibilityLabel") {} else if (key === "key" || key === "ref" || key.startsWith("on")) {} else if (process.env["NODE_ENV"] !== "production") console.warn(`[solid-tui] unknown prop "${key}" on <${el.type}>`);
1456
+ onCommit();
1457
+ return;
1458
+ }
1459
+ if (el.type === "tui-virtual-text" && STYLE_PROPS.has(key)) {
1460
+ el.props[key] = next;
1461
+ onCommit();
1462
+ }
1463
+ }
1464
+ return {
1465
+ createElement,
1466
+ createTextNode,
1467
+ replaceText: setText,
1468
+ isTextNode: (node) => node.type === "text-leaf",
1469
+ setProperty: (node, name, value, prev) => patchProp(node, name, prev, value),
1470
+ insertNode: (parent, node, anchor) => insert(node, parent, anchor ?? null),
1471
+ removeNode: (_parent, node) => remove(node),
1472
+ getParentNode: (node) => parentNode(node) ?? void 0,
1473
+ getFirstChild: (node) => isContainer(node) ? node.children[0] : void 0,
1474
+ getNextSibling: (node) => nextSibling(node) ?? void 0
1475
+ };
1476
+ }
1477
+ //#endregion
1478
+ //#region src/renderer.ts
1479
+ let onCommit = () => {};
1480
+ function setRendererCommit(fn) {
1481
+ const previous = onCommit;
1482
+ onCommit = fn;
1483
+ return () => {
1484
+ onCommit = previous;
1485
+ };
1486
+ }
1487
+ const tuiRenderer = createRenderer(buildNodeOps({ onCommit: () => onCommit() }));
1488
+ const renderSolidRoot = tuiRenderer.render;
1489
+ const createElement = tuiRenderer.createElement;
1490
+ const createTextNode = tuiRenderer.createTextNode;
1491
+ const insertNode = tuiRenderer.insertNode;
1492
+ const insert = tuiRenderer.insert;
1493
+ const spread = tuiRenderer.spread;
1494
+ const setProp = tuiRenderer.setProp;
1495
+ const mergeProps = tuiRenderer.mergeProps;
1496
+ const effect = tuiRenderer.effect;
1497
+ const memo = tuiRenderer.memo;
1498
+ const createComponent = tuiRenderer.createComponent;
1499
+ const use = tuiRenderer.use;
1500
+ //#endregion
1501
+ //#region src/io/input-parser.ts
1502
+ const escape = "\x1B";
1503
+ const pasteStart = "\x1B[200~";
1504
+ const pasteEnd = "\x1B[201~";
1505
+ const isCsiParameterByte = (byte) => {
1506
+ return byte >= 48 && byte <= 63;
1507
+ };
1508
+ const isCsiIntermediateByte = (byte) => {
1509
+ return byte >= 32 && byte <= 47;
1510
+ };
1511
+ const isCsiFinalByte = (byte) => {
1512
+ return byte >= 64 && byte <= 126;
1513
+ };
1514
+ const parseCsiSequence = (input, startIndex, prefixLength) => {
1515
+ const csiPayloadStart = startIndex + prefixLength + 1;
1516
+ let index = csiPayloadStart;
1517
+ for (; index < input.length; index++) {
1518
+ const byte = input.codePointAt(index);
1519
+ if (byte === void 0) return "pending";
1520
+ if (isCsiParameterByte(byte) || isCsiIntermediateByte(byte)) continue;
1521
+ if (byte === 91 && index === csiPayloadStart) continue;
1522
+ if (isCsiFinalByte(byte)) return {
1523
+ sequence: input.slice(startIndex, index + 1),
1524
+ nextIndex: index + 1
1525
+ };
1526
+ return;
1527
+ }
1528
+ return "pending";
1529
+ };
1530
+ const parseSs3Sequence = (input, startIndex, prefixLength) => {
1531
+ const nextIndex = startIndex + prefixLength + 2;
1532
+ if (nextIndex > input.length) return "pending";
1533
+ const finalByte = input.codePointAt(nextIndex - 1);
1534
+ if (finalByte === void 0 || !isCsiFinalByte(finalByte)) return;
1535
+ return {
1536
+ sequence: input.slice(startIndex, nextIndex),
1537
+ nextIndex
1538
+ };
1539
+ };
1540
+ const parseControlSequence = (input, startIndex, prefixLength) => {
1541
+ const sequenceType = input[startIndex + prefixLength];
1542
+ if (sequenceType === void 0) return "pending";
1543
+ if (sequenceType === "[") return parseCsiSequence(input, startIndex, prefixLength);
1544
+ if (sequenceType === "O") return parseSs3Sequence(input, startIndex, prefixLength);
1545
+ };
1546
+ const parseEscapedCodePoint = (input, escapeIndex) => {
1547
+ const nextCodePoint = input.codePointAt(escapeIndex + 1);
1548
+ const nextCodePointLength = nextCodePoint !== void 0 && nextCodePoint > 65535 ? 2 : 1;
1549
+ const nextIndex = escapeIndex + 1 + nextCodePointLength;
1550
+ return {
1551
+ sequence: input.slice(escapeIndex, nextIndex),
1552
+ nextIndex
1553
+ };
1554
+ };
1555
+ const parseEscapeSequence = (input, escapeIndex) => {
1556
+ if (escapeIndex === input.length - 1) return "pending";
1557
+ if (input[escapeIndex + 1] === escape) {
1558
+ if (escapeIndex + 2 >= input.length) return "pending";
1559
+ const doubleEscapeSequence = parseControlSequence(input, escapeIndex, 2);
1560
+ if (doubleEscapeSequence === "pending") return "pending";
1561
+ if (doubleEscapeSequence) return doubleEscapeSequence;
1562
+ return {
1563
+ sequence: input.slice(escapeIndex, escapeIndex + 2),
1564
+ nextIndex: escapeIndex + 2
1565
+ };
1566
+ }
1567
+ const controlSequence = parseControlSequence(input, escapeIndex, 1);
1568
+ if (controlSequence === "pending") return "pending";
1569
+ if (controlSequence) return controlSequence;
1570
+ return parseEscapedCodePoint(input, escapeIndex);
1571
+ };
1572
+ /**
1573
+ * Split a chunk of non-escape text so that backspace bytes (`0x7F` and `0x08`)
1574
+ * become individual events. When a user holds the backspace key, the terminal
1575
+ * sends repeated bytes in a single stdin chunk. Without splitting,
1576
+ * `parseKeypress` receives the multi-byte string and fails to recognize it as a
1577
+ * key event, corrupting the input state.
1578
+ *
1579
+ * Other control characters like `\r` and `\t` are NOT split because they can
1580
+ * legitimately appear inside pasted text.
1581
+ */
1582
+ const splitBackspaceBytes = (text, events) => {
1583
+ let textSegmentStart = 0;
1584
+ for (let index = 0; index < text.length; index++) {
1585
+ const character = text[index];
1586
+ if (character === "" || character === "\b") {
1587
+ if (index > textSegmentStart) events.push(text.slice(textSegmentStart, index));
1588
+ events.push(character);
1589
+ textSegmentStart = index + 1;
1590
+ }
1591
+ }
1592
+ if (textSegmentStart < text.length) events.push(text.slice(textSegmentStart));
1593
+ };
1594
+ const parseKeypresses = (input) => {
1595
+ const events = [];
1596
+ let index = 0;
1597
+ const pendingFrom = (pendingStartIndex) => ({
1598
+ events,
1599
+ pending: input.slice(pendingStartIndex)
1600
+ });
1601
+ while (index < input.length) {
1602
+ const escapeIndex = input.indexOf(escape, index);
1603
+ if (escapeIndex === -1) {
1604
+ splitBackspaceBytes(input.slice(index), events);
1605
+ return {
1606
+ events,
1607
+ pending: ""
1608
+ };
1609
+ }
1610
+ if (escapeIndex > index) splitBackspaceBytes(input.slice(index, escapeIndex), events);
1611
+ const parsedEscapeSequence = parseEscapeSequence(input, escapeIndex);
1612
+ if (parsedEscapeSequence === "pending") return pendingFrom(escapeIndex);
1613
+ if (parsedEscapeSequence.sequence === pasteStart) {
1614
+ const afterStart = parsedEscapeSequence.nextIndex;
1615
+ const endIndex = input.indexOf(pasteEnd, afterStart);
1616
+ if (endIndex === -1) return pendingFrom(escapeIndex);
1617
+ events.push({ paste: input.slice(afterStart, endIndex) });
1618
+ index = endIndex + 6;
1619
+ continue;
1620
+ }
1621
+ events.push(parsedEscapeSequence.sequence);
1622
+ index = parsedEscapeSequence.nextIndex;
1623
+ }
1624
+ return {
1625
+ events,
1626
+ pending: ""
1627
+ };
1628
+ };
1629
+ const createInputParser = () => {
1630
+ let pending = "";
1631
+ return {
1632
+ push(chunk) {
1633
+ const parsedInput = parseKeypresses(pending + chunk);
1634
+ pending = parsedInput.pending;
1635
+ return parsedInput.events;
1636
+ },
1637
+ hasPendingEscape() {
1638
+ return pending.startsWith(escape) && !pending.startsWith(pasteStart) && pending !== "\x1B[200";
1639
+ },
1640
+ flushPendingEscape() {
1641
+ if (!pending.startsWith(escape)) return;
1642
+ const pendingEscape = pending;
1643
+ pending = "";
1644
+ return pendingEscape;
1645
+ },
1646
+ reset() {
1647
+ pending = "";
1648
+ }
1649
+ };
1650
+ };
1651
+ //#endregion
1652
+ //#region src/io/kitty-keyboard.ts
1653
+ const textEncoder = new TextEncoder();
1654
+ const kittyFlags = {
1655
+ disambiguateEscapeCodes: 1,
1656
+ reportEventTypes: 2,
1657
+ reportAlternateKeys: 4,
1658
+ reportAllKeysAsEscapeCodes: 8,
1659
+ reportAssociatedText: 16
1660
+ };
1661
+ const kittyModifiers = {
1662
+ shift: 1,
1663
+ alt: 2,
1664
+ ctrl: 4,
1665
+ super: 8,
1666
+ hyper: 16,
1667
+ meta: 32,
1668
+ capsLock: 64,
1669
+ numLock: 128
1670
+ };
1671
+ function resolveFlags(flags) {
1672
+ let result = 0;
1673
+ for (const flag of flags) result |= kittyFlags[flag];
1674
+ return result;
1675
+ }
1676
+ const ESC = 27;
1677
+ const OPEN_BRACKET = 91;
1678
+ const QUESTION_MARK = 63;
1679
+ const LETTER_U = 117;
1680
+ const ZERO = 48;
1681
+ const NINE = 57;
1682
+ const isDigitByte = (byte) => byte >= ZERO && byte <= NINE;
1683
+ function matchKittyQueryResponse(buffer, startIndex) {
1684
+ if (buffer[startIndex] !== ESC || buffer[startIndex + 1] !== OPEN_BRACKET || buffer[startIndex + 2] !== QUESTION_MARK) return;
1685
+ let index = startIndex + 3;
1686
+ const digitsStart = index;
1687
+ while (index < buffer.length && isDigitByte(buffer[index])) index++;
1688
+ if (index === digitsStart) return;
1689
+ if (index === buffer.length) return { state: "partial" };
1690
+ if (buffer[index] === LETTER_U) return {
1691
+ state: "complete",
1692
+ endIndex: index
1693
+ };
1694
+ }
1695
+ function hasCompleteKittyQueryResponse(buffer) {
1696
+ for (let index = 0; index < buffer.length; index++) if (matchKittyQueryResponse(buffer, index)?.state === "complete") return true;
1697
+ return false;
1698
+ }
1699
+ function stripKittyQueryResponsesAndTrailingPartial(buffer) {
1700
+ const kept = [];
1701
+ let index = 0;
1702
+ while (index < buffer.length) {
1703
+ const match = matchKittyQueryResponse(buffer, index);
1704
+ if (match?.state === "complete") {
1705
+ index = match.endIndex + 1;
1706
+ continue;
1707
+ }
1708
+ if (match?.state === "partial") break;
1709
+ kept.push(buffer[index]);
1710
+ index++;
1711
+ }
1712
+ return kept;
1713
+ }
1714
+ function createKittyKeyboardController(stdin, stdout) {
1715
+ let enabled = false;
1716
+ let disposed = false;
1717
+ let cancelDetection;
1718
+ function enableProtocol(flags) {
1719
+ stdout.write(`\x1b[>${resolveFlags(flags)}u`);
1720
+ enabled = true;
1721
+ }
1722
+ function confirmKittySupport(flags) {
1723
+ let responseBuffer = [];
1724
+ const cleanup = () => {
1725
+ cancelDetection = void 0;
1726
+ clearTimeout(timer);
1727
+ stdin.removeListener("data", onData);
1728
+ const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
1729
+ responseBuffer = [];
1730
+ if (remaining.length > 0) stdin.unshift(Uint8Array.from(remaining));
1731
+ };
1732
+ const onData = (data) => {
1733
+ const chunk = typeof data === "string" ? textEncoder.encode(data) : data;
1734
+ for (const byte of chunk) responseBuffer.push(byte);
1735
+ if (hasCompleteKittyQueryResponse(responseBuffer)) {
1736
+ cleanup();
1737
+ if (!disposed) enableProtocol(flags);
1738
+ }
1739
+ };
1740
+ stdin.on("data", onData);
1741
+ const timer = setTimeout(cleanup, 200);
1742
+ cancelDetection = cleanup;
1743
+ stdout.write("\x1B[?u");
1744
+ }
1745
+ return {
1746
+ get isEnabled() {
1747
+ return enabled;
1748
+ },
1749
+ init(options, interactive) {
1750
+ if (!options) return;
1751
+ const mode = options.mode ?? "auto";
1752
+ if (mode === "disabled") return;
1753
+ const flags = options.flags ?? ["disambiguateEscapeCodes"];
1754
+ if (mode === "enabled") {
1755
+ if (stdin.isTTY && stdout.isTTY) enableProtocol(flags);
1756
+ return;
1757
+ }
1758
+ if (!interactive || !stdin.isTTY || !stdout.isTTY) return;
1759
+ confirmKittySupport(flags);
1760
+ },
1761
+ dispose(sync = false) {
1762
+ disposed = true;
1763
+ if (cancelDetection) cancelDetection();
1764
+ if (enabled) {
1765
+ if (sync) try {
1766
+ const streamFd = stdout.fd;
1767
+ writeSync(typeof streamFd === "number" ? streamFd : 1, "\x1B[<u");
1768
+ } catch {}
1769
+ else if (!stdout.destroyed && !stdout.writableEnded) stdout.write("\x1B[<u");
1770
+ enabled = false;
1771
+ }
1772
+ }
1773
+ };
1774
+ }
1775
+ //#endregion
1776
+ //#region src/io/parse-keypress.ts
1777
+ const textDecoder = new TextDecoder();
1778
+ const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
1779
+ const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
1780
+ const kittyQueryResponseRe = /^\x1b\[\?\d+u$/;
1781
+ const keyName = {
1782
+ OP: "f1",
1783
+ OQ: "f2",
1784
+ OR: "f3",
1785
+ OS: "f4",
1786
+ "[P": "f1",
1787
+ "[Q": "f2",
1788
+ "[R": "f3",
1789
+ "[S": "f4",
1790
+ "[11~": "f1",
1791
+ "[12~": "f2",
1792
+ "[13~": "f3",
1793
+ "[14~": "f4",
1794
+ "[[A": "f1",
1795
+ "[[B": "f2",
1796
+ "[[C": "f3",
1797
+ "[[D": "f4",
1798
+ "[[E": "f5",
1799
+ "[15~": "f5",
1800
+ "[17~": "f6",
1801
+ "[18~": "f7",
1802
+ "[19~": "f8",
1803
+ "[20~": "f9",
1804
+ "[21~": "f10",
1805
+ "[23~": "f11",
1806
+ "[24~": "f12",
1807
+ "[A": "up",
1808
+ "[B": "down",
1809
+ "[C": "right",
1810
+ "[D": "left",
1811
+ "[E": "clear",
1812
+ "[F": "end",
1813
+ "[H": "home",
1814
+ OA: "up",
1815
+ OB: "down",
1816
+ OC: "right",
1817
+ OD: "left",
1818
+ OE: "clear",
1819
+ OF: "end",
1820
+ OH: "home",
1821
+ "[1~": "home",
1822
+ "[2~": "insert",
1823
+ "[3~": "delete",
1824
+ "[4~": "end",
1825
+ "[5~": "pageup",
1826
+ "[6~": "pagedown",
1827
+ "[[5~": "pageup",
1828
+ "[[6~": "pagedown",
1829
+ "[7~": "home",
1830
+ "[8~": "end",
1831
+ "[a": "up",
1832
+ "[b": "down",
1833
+ "[c": "right",
1834
+ "[d": "left",
1835
+ "[e": "clear",
1836
+ "[2$": "insert",
1837
+ "[3$": "delete",
1838
+ "[5$": "pageup",
1839
+ "[6$": "pagedown",
1840
+ "[7$": "home",
1841
+ "[8$": "end",
1842
+ Oa: "up",
1843
+ Ob: "down",
1844
+ Oc: "right",
1845
+ Od: "left",
1846
+ Oe: "clear",
1847
+ "[2^": "insert",
1848
+ "[3^": "delete",
1849
+ "[5^": "pageup",
1850
+ "[6^": "pagedown",
1851
+ "[7^": "home",
1852
+ "[8^": "end",
1853
+ "[Z": "tab"
1854
+ };
1855
+ const nonAlphanumericKeys = [...Object.values(keyName), "backspace"];
1856
+ const isShiftKey = (code) => {
1857
+ return [
1858
+ "[a",
1859
+ "[b",
1860
+ "[c",
1861
+ "[d",
1862
+ "[e",
1863
+ "[2$",
1864
+ "[3$",
1865
+ "[5$",
1866
+ "[6$",
1867
+ "[7$",
1868
+ "[8$",
1869
+ "[Z"
1870
+ ].includes(code);
1871
+ };
1872
+ const isCtrlKey = (code) => {
1873
+ return [
1874
+ "Oa",
1875
+ "Ob",
1876
+ "Oc",
1877
+ "Od",
1878
+ "Oe",
1879
+ "[2^",
1880
+ "[3^",
1881
+ "[5^",
1882
+ "[6^",
1883
+ "[7^",
1884
+ "[8^"
1885
+ ].includes(code);
1886
+ };
1887
+ const kittyKeyRe = /^\x1b\[(\d+)(?:;(\d+)(?::(\d+))?(?:;([\d:]+))?)?u$/;
1888
+ const kittySpecialKeyRe = /^\x1b\[(\d+);(\d+):(\d+)([A-Za-z~])$/;
1889
+ const kittySpecialLetterKeys = {
1890
+ A: "up",
1891
+ B: "down",
1892
+ C: "right",
1893
+ D: "left",
1894
+ E: "clear",
1895
+ F: "end",
1896
+ H: "home",
1897
+ P: "f1",
1898
+ Q: "f2",
1899
+ R: "f3",
1900
+ S: "f4"
1901
+ };
1902
+ const kittySpecialNumberKeys = {
1903
+ 2: "insert",
1904
+ 3: "delete",
1905
+ 5: "pageup",
1906
+ 6: "pagedown",
1907
+ 7: "home",
1908
+ 8: "end",
1909
+ 11: "f1",
1910
+ 12: "f2",
1911
+ 13: "f3",
1912
+ 14: "f4",
1913
+ 15: "f5",
1914
+ 17: "f6",
1915
+ 18: "f7",
1916
+ 19: "f8",
1917
+ 20: "f9",
1918
+ 21: "f10",
1919
+ 23: "f11",
1920
+ 24: "f12"
1921
+ };
1922
+ const kittyCodepointNames = {
1923
+ 27: "escape",
1924
+ 9: "tab",
1925
+ 127: "backspace",
1926
+ 8: "backspace",
1927
+ 57358: "capslock",
1928
+ 57359: "scrolllock",
1929
+ 57360: "numlock",
1930
+ 57361: "printscreen",
1931
+ 57362: "pause",
1932
+ 57363: "menu",
1933
+ 57376: "f13",
1934
+ 57377: "f14",
1935
+ 57378: "f15",
1936
+ 57379: "f16",
1937
+ 57380: "f17",
1938
+ 57381: "f18",
1939
+ 57382: "f19",
1940
+ 57383: "f20",
1941
+ 57384: "f21",
1942
+ 57385: "f22",
1943
+ 57386: "f23",
1944
+ 57387: "f24",
1945
+ 57388: "f25",
1946
+ 57389: "f26",
1947
+ 57390: "f27",
1948
+ 57391: "f28",
1949
+ 57392: "f29",
1950
+ 57393: "f30",
1951
+ 57394: "f31",
1952
+ 57395: "f32",
1953
+ 57396: "f33",
1954
+ 57397: "f34",
1955
+ 57398: "f35",
1956
+ 57399: "kp0",
1957
+ 57400: "kp1",
1958
+ 57401: "kp2",
1959
+ 57402: "kp3",
1960
+ 57403: "kp4",
1961
+ 57404: "kp5",
1962
+ 57405: "kp6",
1963
+ 57406: "kp7",
1964
+ 57407: "kp8",
1965
+ 57408: "kp9",
1966
+ 57409: "kpdecimal",
1967
+ 57410: "kpdivide",
1968
+ 57411: "kpmultiply",
1969
+ 57412: "kpsubtract",
1970
+ 57413: "kpadd",
1971
+ 57414: "kpenter",
1972
+ 57415: "kpequal",
1973
+ 57416: "kpseparator",
1974
+ 57417: "kpleft",
1975
+ 57418: "kpright",
1976
+ 57419: "kpup",
1977
+ 57420: "kpdown",
1978
+ 57421: "kppageup",
1979
+ 57422: "kppagedown",
1980
+ 57423: "kphome",
1981
+ 57424: "kpend",
1982
+ 57425: "kpinsert",
1983
+ 57426: "kpdelete",
1984
+ 57427: "kpbegin",
1985
+ 57428: "mediaplay",
1986
+ 57429: "mediapause",
1987
+ 57430: "mediaplaypause",
1988
+ 57431: "mediareverse",
1989
+ 57432: "mediastop",
1990
+ 57433: "mediafastforward",
1991
+ 57434: "mediarewind",
1992
+ 57435: "mediatracknext",
1993
+ 57436: "mediatrackprevious",
1994
+ 57437: "mediarecord",
1995
+ 57438: "lowervolume",
1996
+ 57439: "raisevolume",
1997
+ 57440: "mutevolume",
1998
+ 57441: "leftshift",
1999
+ 57442: "leftcontrol",
2000
+ 57443: "leftalt",
2001
+ 57444: "leftsuper",
2002
+ 57445: "lefthyper",
2003
+ 57446: "leftmeta",
2004
+ 57447: "rightshift",
2005
+ 57448: "rightcontrol",
2006
+ 57449: "rightalt",
2007
+ 57450: "rightsuper",
2008
+ 57451: "righthyper",
2009
+ 57452: "rightmeta",
2010
+ 57453: "isoLevel3Shift",
2011
+ 57454: "isoLevel5Shift"
2012
+ };
2013
+ const isValidCodepoint = (cp) => cp >= 0 && cp <= 1114111 && !(cp >= 55296 && cp <= 57343);
2014
+ const safeFromCodePoint = (cp) => isValidCodepoint(cp) ? String.fromCodePoint(cp) : "?";
2015
+ function resolveEventType(value) {
2016
+ if (value === 3) return "release";
2017
+ if (value === 2) return "repeat";
2018
+ return "press";
2019
+ }
2020
+ function parseKittyModifiers(modifiers) {
2021
+ return {
2022
+ ctrl: !!(modifiers & kittyModifiers.ctrl),
2023
+ shift: !!(modifiers & kittyModifiers.shift),
2024
+ meta: !!(modifiers & (kittyModifiers.meta | kittyModifiers.alt)),
2025
+ super: !!(modifiers & kittyModifiers.super),
2026
+ hyper: !!(modifiers & kittyModifiers.hyper),
2027
+ capsLock: !!(modifiers & kittyModifiers.capsLock),
2028
+ numLock: !!(modifiers & kittyModifiers.numLock)
2029
+ };
2030
+ }
2031
+ const parseKittyKeypress = (s) => {
2032
+ const match = kittyKeyRe.exec(s);
2033
+ if (!match) return null;
2034
+ const codepoint = parseInt(match[1], 10);
2035
+ const modifiers = match[2] ? Math.max(0, parseInt(match[2], 10) - 1) : 0;
2036
+ const eventType = match[3] ? parseInt(match[3], 10) : 1;
2037
+ const textField = match[4];
2038
+ if (!isValidCodepoint(codepoint)) return null;
2039
+ let text;
2040
+ if (textField) text = textField.split(":").map((cp) => safeFromCodePoint(parseInt(cp, 10))).join("");
2041
+ let name;
2042
+ let isPrintable;
2043
+ if (codepoint === 32) {
2044
+ name = "space";
2045
+ isPrintable = true;
2046
+ } else if (codepoint === 13) {
2047
+ name = "return";
2048
+ isPrintable = true;
2049
+ } else if (kittyCodepointNames[codepoint]) {
2050
+ name = kittyCodepointNames[codepoint];
2051
+ isPrintable = false;
2052
+ } else if (codepoint >= 1 && codepoint <= 26) {
2053
+ name = String.fromCodePoint(codepoint + 96);
2054
+ isPrintable = false;
2055
+ } else {
2056
+ name = safeFromCodePoint(codepoint).toLowerCase();
2057
+ isPrintable = true;
2058
+ }
2059
+ if (isPrintable && !text) text = safeFromCodePoint(codepoint);
2060
+ return {
2061
+ name,
2062
+ ...parseKittyModifiers(modifiers),
2063
+ eventType: resolveEventType(eventType),
2064
+ sequence: s,
2065
+ raw: s,
2066
+ isKittyProtocol: true,
2067
+ isPrintable,
2068
+ text
2069
+ };
2070
+ };
2071
+ const parseKittySpecialKey = (s) => {
2072
+ const match = kittySpecialKeyRe.exec(s);
2073
+ if (!match) return null;
2074
+ const number = parseInt(match[1], 10);
2075
+ const modifiers = Math.max(0, parseInt(match[2], 10) - 1);
2076
+ const eventType = parseInt(match[3], 10);
2077
+ const terminator = match[4];
2078
+ const name = terminator === "~" ? kittySpecialNumberKeys[number] : kittySpecialLetterKeys[terminator];
2079
+ if (!name) return null;
2080
+ return {
2081
+ name,
2082
+ ...parseKittyModifiers(modifiers),
2083
+ eventType: resolveEventType(eventType),
2084
+ sequence: s,
2085
+ raw: s,
2086
+ isKittyProtocol: true,
2087
+ isPrintable: false
2088
+ };
2089
+ };
2090
+ function parseKeypress(s = "") {
2091
+ let parts;
2092
+ if (s instanceof Uint8Array) if (s[0] > 127 && s[1] === void 0) {
2093
+ s[0] -= 128;
2094
+ s = "\x1B" + textDecoder.decode(s);
2095
+ } else s = textDecoder.decode(s);
2096
+ else if (s !== void 0 && typeof s !== "string") s = String(s);
2097
+ else if (!s) s = "";
2098
+ if (kittyQueryResponseRe.test(s)) return {
2099
+ name: "",
2100
+ ctrl: false,
2101
+ meta: false,
2102
+ shift: false,
2103
+ sequence: s,
2104
+ raw: s,
2105
+ ignore: true
2106
+ };
2107
+ const kittyResult = parseKittyKeypress(s);
2108
+ if (kittyResult) return kittyResult;
2109
+ const kittySpecialResult = parseKittySpecialKey(s);
2110
+ if (kittySpecialResult) return kittySpecialResult;
2111
+ if (kittyKeyRe.test(s)) return {
2112
+ name: "",
2113
+ ctrl: false,
2114
+ meta: false,
2115
+ shift: false,
2116
+ sequence: s,
2117
+ raw: s,
2118
+ isKittyProtocol: true,
2119
+ isPrintable: false
2120
+ };
2121
+ const key = {
2122
+ name: "",
2123
+ ctrl: false,
2124
+ meta: false,
2125
+ shift: false,
2126
+ sequence: s,
2127
+ raw: s
2128
+ };
2129
+ key.sequence = key.sequence || s || key.name;
2130
+ if (s === "\r" || s === "\x1B\r") {
2131
+ key.raw = void 0;
2132
+ key.name = "return";
2133
+ key.meta = s.length === 2;
2134
+ } else if (s === "\n") key.name = "enter";
2135
+ else if (s === " ") key.name = "tab";
2136
+ else if (s === "\b" || s === "\x1B\b") {
2137
+ key.name = "backspace";
2138
+ key.meta = s.charAt(0) === "\x1B";
2139
+ } else if (s === "" || s === "\x1B") {
2140
+ key.name = "backspace";
2141
+ key.meta = s.charAt(0) === "\x1B";
2142
+ } else if (s === "\x1B" || s === "\x1B\x1B") {
2143
+ key.name = "escape";
2144
+ key.meta = s.length === 2;
2145
+ } else if (s === " " || s === "\x1B ") {
2146
+ key.name = "space";
2147
+ key.meta = s.length === 2;
2148
+ } else if (s.length === 1 && s <= "") {
2149
+ key.name = String.fromCharCode(s.charCodeAt(0) + "a".charCodeAt(0) - 1);
2150
+ key.ctrl = true;
2151
+ } else if (s.length === 1 && s >= "0" && s <= "9") key.name = "number";
2152
+ else if (s.length === 1 && s >= "a" && s <= "z") key.name = s;
2153
+ else if (s.length === 1 && s >= "A" && s <= "Z") {
2154
+ key.name = s.toLowerCase();
2155
+ key.shift = true;
2156
+ } else if (parts = metaKeyCodeRe.exec(s)) {
2157
+ key.name = parts[1].toLowerCase();
2158
+ key.meta = true;
2159
+ key.shift = /^[A-Z]$/.test(parts[1]);
2160
+ } else if (parts = fnKeyRe.exec(s)) {
2161
+ const segs = [...s];
2162
+ if (segs[0] === "\x1B" && segs[1] === "\x1B") key.meta = true;
2163
+ const code = [
2164
+ parts[1],
2165
+ parts[2],
2166
+ parts[4],
2167
+ parts[6]
2168
+ ].filter(Boolean).join("");
2169
+ const modifier = (parts[3] || parts[5] || 1) - 1;
2170
+ key.ctrl = !!(modifier & 4);
2171
+ key.meta = key.meta || !!(modifier & 10);
2172
+ key.shift = !!(modifier & 1);
2173
+ key.code = code;
2174
+ key.name = keyName[code] ?? "";
2175
+ key.shift = isShiftKey(code) || key.shift;
2176
+ key.ctrl = isCtrlKey(code) || key.ctrl;
2177
+ }
2178
+ return key;
2179
+ }
2180
+ //#endregion
2181
+ //#region src/host/layout-guards.ts
2182
+ function hasYoga(node) {
2183
+ return node.type === "root" || node.type === "tui-box" || node.type === "tui-text" || node.type === "tui-static" || node.type === "tui-transform";
2184
+ }
2185
+ function hasChildren(node) {
2186
+ return node.type === "root" || node.type === "tui-box" || node.type === "tui-text" || node.type === "tui-static" || node.type === "tui-transform";
2187
+ }
2188
+ function getBoxInnerSize(node) {
2189
+ const layout = node.yoga.getComputedLayout();
2190
+ const width = Math.max(0, Math.floor(layout.width));
2191
+ const height = Math.max(0, Math.floor(layout.height));
2192
+ const left = node.yoga.getComputedBorder(Yoga.EDGE_LEFT) + node.yoga.getComputedPadding(Yoga.EDGE_LEFT);
2193
+ const right = node.yoga.getComputedBorder(Yoga.EDGE_RIGHT) + node.yoga.getComputedPadding(Yoga.EDGE_RIGHT);
2194
+ const top = node.yoga.getComputedBorder(Yoga.EDGE_TOP) + node.yoga.getComputedPadding(Yoga.EDGE_TOP);
2195
+ const bottom = node.yoga.getComputedBorder(Yoga.EDGE_BOTTOM) + node.yoga.getComputedPadding(Yoga.EDGE_BOTTOM);
2196
+ return {
2197
+ width: Math.max(0, Math.floor(width - left - right)),
2198
+ height: Math.max(0, Math.floor(height - top - bottom))
2199
+ };
2200
+ }
2201
+ function hideYogaChild(child, guarded) {
2202
+ if (!hasYoga(child)) return false;
2203
+ if (guarded.has(child.yoga)) return false;
2204
+ const display = child.yoga.getDisplay();
2205
+ if (display === Yoga.DISPLAY_NONE) return false;
2206
+ guarded.set(child.yoga, display);
2207
+ child.yoga.setDisplay(Yoga.DISPLAY_NONE);
2208
+ return true;
2209
+ }
2210
+ function applyZeroContentGuards(node, guarded) {
2211
+ if (hasYoga(node) && node.yoga.getDisplay() === Yoga.DISPLAY_NONE) return false;
2212
+ let changed = false;
2213
+ if (node.type === "tui-box") {
2214
+ const inner = getBoxInnerSize(node);
2215
+ if (inner.width === 0 || inner.height === 0) {
2216
+ for (const child of node.children) {
2217
+ if (hasYoga(child) && child.yoga.getPositionType() === Yoga.POSITION_TYPE_ABSOLUTE) continue;
2218
+ changed = hideYogaChild(child, guarded) || changed;
2219
+ }
2220
+ return changed;
2221
+ }
2222
+ }
2223
+ if (!hasChildren(node)) return changed;
2224
+ for (const child of node.children) changed = applyZeroContentGuards(child, guarded) || changed;
2225
+ return changed;
2226
+ }
2227
+ function calculateLayoutWithContentGuards(root, width, height, direction = Yoga.DIRECTION_LTR) {
2228
+ const guarded = /* @__PURE__ */ new Map();
2229
+ const restore = () => {
2230
+ for (const [node, display] of [...guarded].reverse()) node.setDisplay(display);
2231
+ };
2232
+ try {
2233
+ for (;;) {
2234
+ root.yoga.calculateLayout(width, height, direction);
2235
+ if (!applyZeroContentGuards(root, guarded)) break;
2236
+ }
2237
+ } catch (err) {
2238
+ restore();
2239
+ throw err;
2240
+ }
2241
+ return restore;
2242
+ }
2243
+ //#endregion
2244
+ //#region src/scheduler.ts
2245
+ function createCommitScheduler(commit, options) {
2246
+ const immediate = options.immediate ?? false;
2247
+ const throttleMs = options.throttleMs;
2248
+ let scheduled = false;
2249
+ let flushResolvers = [];
2250
+ function drainFlushResolvers() {
2251
+ if (flushResolvers.length === 0) return;
2252
+ const resolvers = flushResolvers;
2253
+ flushResolvers = [];
2254
+ for (const resolve of resolvers) resolve();
2255
+ }
2256
+ let trailingTimer = null;
2257
+ let hasPendingFlag = false;
2258
+ let pendingAt = null;
2259
+ function doCommit() {
2260
+ scheduled = false;
2261
+ hasPendingFlag = false;
2262
+ pendingAt = null;
2263
+ try {
2264
+ commit();
2265
+ } finally {
2266
+ drainFlushResolvers();
2267
+ }
2268
+ }
2269
+ function armTrailingWindow() {
2270
+ if (trailingTimer) clearTimeout(trailingTimer);
2271
+ trailingTimer = setTimeout(() => {
2272
+ trailingTimer = null;
2273
+ if (hasPendingFlag) doCommit();
2274
+ }, throttleMs);
2275
+ }
2276
+ function schedule() {
2277
+ if (scheduled) return;
2278
+ scheduled = true;
2279
+ queueMicrotask(() => {
2280
+ if (!scheduled) return;
2281
+ scheduled = false;
2282
+ if (immediate) {
2283
+ doCommit();
2284
+ return;
2285
+ }
2286
+ const now = Date.now();
2287
+ if (pendingAt === null) pendingAt = now;
2288
+ if (now - pendingAt >= throttleMs) {
2289
+ doCommit();
2290
+ pendingAt = Date.now();
2291
+ armTrailingWindow();
2292
+ return;
2293
+ }
2294
+ const isWindowActive = trailingTimer !== null;
2295
+ armTrailingWindow();
2296
+ if (isWindowActive) hasPendingFlag = true;
2297
+ else doCommit();
2298
+ });
2299
+ }
2300
+ function flush() {
2301
+ if (!scheduled && !hasPendingFlag) return Promise.resolve();
2302
+ return new Promise((resolve) => {
2303
+ flushResolvers.push(resolve);
2304
+ });
2305
+ }
2306
+ function hasPending() {
2307
+ return hasPendingFlag;
2308
+ }
2309
+ function cancel() {
2310
+ if (trailingTimer) {
2311
+ clearTimeout(trailingTimer);
2312
+ trailingTimer = null;
2313
+ }
2314
+ hasPendingFlag = false;
2315
+ scheduled = false;
2316
+ pendingAt = null;
2317
+ drainFlushResolvers();
2318
+ }
2319
+ return {
2320
+ schedule,
2321
+ flush,
2322
+ hasPending,
2323
+ cancel
2324
+ };
2325
+ }
2326
+ //#endregion
2327
+ //#region src/animation-scheduler.ts
2328
+ const DEFAULT_INTERVAL = 100;
2329
+ const MAX_TIMER_INTERVAL = 2147483647;
2330
+ function normalizeInterval(interval) {
2331
+ if (interval === void 0 || !Number.isFinite(interval)) return DEFAULT_INTERVAL;
2332
+ return Math.min(Math.max(1, interval), MAX_TIMER_INTERVAL);
2333
+ }
2334
+ function createAnimationScheduler(renderThrottleMs = 0) {
2335
+ const subscribers = /* @__PURE__ */ new Set();
2336
+ let timer;
2337
+ let scheduledDueTime = Number.POSITIVE_INFINITY;
2338
+ let isDispatching = false;
2339
+ const pending = [];
2340
+ function clearTimer() {
2341
+ if (timer !== void 0) {
2342
+ clearTimeout(timer);
2343
+ timer = void 0;
2344
+ }
2345
+ scheduledDueTime = Number.POSITIVE_INFINITY;
2346
+ }
2347
+ function schedule() {
2348
+ clearTimer();
2349
+ let earliest = Number.POSITIVE_INFINITY;
2350
+ for (const s of subscribers) if (!s.cancelled) earliest = Math.min(earliest, s.nextDueTime);
2351
+ if (earliest === Number.POSITIVE_INFINITY) return;
2352
+ scheduledDueTime = earliest;
2353
+ const delay = Math.ceil(Math.max(0, earliest - performance.now()));
2354
+ timer = setTimeout(onTick, delay);
2355
+ }
2356
+ function onTick() {
2357
+ timer = void 0;
2358
+ scheduledDueTime = Number.POSITIVE_INFINITY;
2359
+ const now = performance.now();
2360
+ isDispatching = true;
2361
+ try {
2362
+ for (const s of subscribers) {
2363
+ if (s.cancelled || now < s.nextDueTime) continue;
2364
+ const elapsedFrames = Math.floor((now - s.startTime) / s.interval) + 1;
2365
+ s.nextDueTime = s.startTime + elapsedFrames * s.interval;
2366
+ s.callback(now);
2367
+ }
2368
+ } finally {
2369
+ isDispatching = false;
2370
+ if (pending.length > 0) for (const op of pending.splice(0)) op();
2371
+ schedule();
2372
+ }
2373
+ }
2374
+ function subscribe(callback, intervalRaw) {
2375
+ const interval = normalizeInterval(intervalRaw);
2376
+ const startTime = performance.now();
2377
+ const sub = {
2378
+ callback,
2379
+ interval,
2380
+ startTime,
2381
+ nextDueTime: startTime + interval,
2382
+ cancelled: false
2383
+ };
2384
+ const add = () => {
2385
+ subscribers.add(sub);
2386
+ if (timer === void 0 || sub.nextDueTime < scheduledDueTime) schedule();
2387
+ };
2388
+ if (isDispatching) pending.push(add);
2389
+ else add();
2390
+ return {
2391
+ startTime,
2392
+ unsubscribe() {
2393
+ sub.cancelled = true;
2394
+ const remove = () => {
2395
+ subscribers.delete(sub);
2396
+ if (subscribers.size === 0) clearTimer();
2397
+ else schedule();
2398
+ };
2399
+ if (isDispatching) pending.push(remove);
2400
+ else remove();
2401
+ }
2402
+ };
2403
+ }
2404
+ function dispose() {
2405
+ clearTimer();
2406
+ subscribers.clear();
2407
+ pending.length = 0;
2408
+ }
2409
+ return {
2410
+ renderThrottleMs,
2411
+ subscribe,
2412
+ dispose
2413
+ };
2414
+ }
2415
+ function createNoOpAnimationScheduler() {
2416
+ return {
2417
+ renderThrottleMs: 0,
2418
+ subscribe() {
2419
+ return {
2420
+ startTime: 0,
2421
+ unsubscribe() {}
2422
+ };
2423
+ },
2424
+ dispose() {}
2425
+ };
2426
+ }
2427
+ //#endregion
2428
+ //#region src/paint/text-style.ts
2429
+ const rgbRegex = /^rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/;
2430
+ const ansi256Regex = /^ansi256\(\s?(\d+)\s?\)$/;
2431
+ function applyColor(c, color, bg) {
2432
+ if (typeof color !== "string") return c;
2433
+ const named = c[bg ? bgKey(color) : color];
2434
+ if (typeof named === "function") return named;
2435
+ if (color.startsWith("#")) return bg ? c.bgHex(color) : c.hex(color);
2436
+ if (color.startsWith("ansi256")) {
2437
+ const m = ansi256Regex.exec(color);
2438
+ if (!m) return c;
2439
+ const n = Number(m[1]);
2440
+ return bg ? c.bgAnsi256(n) : c.ansi256(n);
2441
+ }
2442
+ if (color.startsWith("rgb")) {
2443
+ const m = rgbRegex.exec(color);
2444
+ if (!m) return c;
2445
+ const [r, g, b] = [
2446
+ Number(m[1]),
2447
+ Number(m[2]),
2448
+ Number(m[3])
2449
+ ];
2450
+ return bg ? c.bgRgb(r, g, b) : c.rgb(r, g, b);
2451
+ }
2452
+ return c;
2453
+ }
2454
+ function bgKey(name) {
2455
+ return "bg" + name.charAt(0).toUpperCase() + name.slice(1);
2456
+ }
2457
+ /**
2458
+ * Detect a backgroundColor value that Ink's `colorize` would THROW on.
2459
+ *
2460
+ * Ink colorize.ts (commit 40b3a75): for a BACKGROUND it tests `isNamedColor` =
2461
+ * `color in chalk`; if so it builds `bg${Capitalize(color)}` and calls
2462
+ * `chalk[methodName]`. A chalk MODIFIER name (`bold`/`dim`/`italic`/`underline`/
2463
+ * `inverse`/`hidden`/`strikethrough`/`reset`/`overline`/`visible`) is `in chalk`
2464
+ * but has NO `bg*` method, so the call is `chalk[undefined-method](str)` and throws
2465
+ * "chalk.bgBold is not a function". A chalk COLOR name resolves to a real `bg*`
2466
+ * method (works); a string NOT in chalk falls through to bare text (no throw).
2467
+ *
2468
+ * solid-tui mirrors that throw, but VALIDATES here at component-render time (not in
2469
+ * paint): a raw throw in the post-flush paint pass unwinds through Solid's
2470
+ * flushPostFlushCbs and wedges the scheduler, where onErrorCaptured can't catch it
2471
+ * (cf. the borderStyle fix #124). Returning a flag lets the component throw during
2472
+ * render so solid-tui's error boundary (onErrorCaptured → ErrorOverview) handles it.
2473
+ *
2474
+ * Only the in-chalk-but-no-bg-method case is rejected; valid colors, hex,
2475
+ * ansi256, rgb strings, and unknown non-chalk strings all return false.
2476
+ */
2477
+ function isInvalidBackgroundColor(color) {
2478
+ if (typeof color !== "string" || color.length === 0) return false;
2479
+ if (!(color in chalk)) return false;
2480
+ return typeof chalk[bgKey(color)] !== "function";
2481
+ }
2482
+ /**
2483
+ * Detect a foreground color value that Ink's `colorize` would THROW on.
2484
+ *
2485
+ * Ink's foreground path calls `chalk[color](str)` when `color in chalk`. That
2486
+ * works for real colors and modifiers (`red`, `bold`) but throws for non-method
2487
+ * chalk properties such as `level`.
2488
+ */
2489
+ function isInvalidForegroundColor(color) {
2490
+ if (typeof color !== "string" || color.length === 0) return false;
2491
+ const method = chalk[color];
2492
+ return color in chalk && typeof method !== "function";
2493
+ }
2494
+ /**
2495
+ * Throw (during component render) if `color` is a chalk-modifier-name
2496
+ * backgroundColor — the exact case Ink's colorize.ts throws on. No-op for every
2497
+ * valid background form. `label` names the offending prop in the message.
2498
+ */
2499
+ function assertValidBackgroundColor(color, label = "backgroundColor") {
2500
+ if (isInvalidBackgroundColor(color)) throw new Error(`Invalid ${label}: ${JSON.stringify(color)} (chalk has no bg method for it — it is a text modifier, not a background color)`);
2501
+ }
2502
+ /**
2503
+ * Throw during component render for foreground color names that Ink's paint path
2504
+ * would throw on. `label` names the offending prop in the message.
2505
+ */
2506
+ function assertValidForegroundColor(color, label = "color") {
2507
+ if (isInvalidForegroundColor(color)) throw new Error(`Invalid ${label}: ${JSON.stringify(color)} (chalk has this key but it is not a color method)`);
2508
+ }
2509
+ function applyChalk(text, props) {
2510
+ let s = text;
2511
+ if (props.dimColor) s = chalk.dim(s);
2512
+ if (props.color) s = applyColor(chalk, props.color, false)(s);
2513
+ if (props.backgroundColor) s = applyColor(chalk, props.backgroundColor, true)(s);
2514
+ if (props.bold) s = chalk.bold(s);
2515
+ if (props.italic) s = chalk.italic(s);
2516
+ if (props.underline) s = chalk.underline(s);
2517
+ if (props.strikethrough) s = chalk.strikethrough(s);
2518
+ if (props.inverse) s = chalk.inverse(s);
2519
+ return s;
2520
+ }
2521
+ //#endregion
2522
+ //#region src/paint/paint.ts
2523
+ var OutputCaches = class {
2524
+ constructor() {
2525
+ this.widths = /* @__PURE__ */ new Map();
2526
+ this.blockWidths = /* @__PURE__ */ new Map();
2527
+ this.styledCharsCache = /* @__PURE__ */ new Map();
2528
+ }
2529
+ getStyledChars(line) {
2530
+ let cached = this.styledCharsCache.get(line);
2531
+ if (cached === void 0) {
2532
+ cached = styledCharsFromTokens(tokenize(line));
2533
+ this.styledCharsCache.set(line, cached);
2534
+ }
2535
+ return cached;
2536
+ }
2537
+ getStringWidth(text) {
2538
+ let cached = this.widths.get(text);
2539
+ if (cached === void 0) {
2540
+ cached = stringWidth(text);
2541
+ this.widths.set(text, cached);
2542
+ }
2543
+ return cached;
2544
+ }
2545
+ getWidestLine(text) {
2546
+ let cached = this.blockWidths.get(text);
2547
+ if (cached === void 0) {
2548
+ let lineWidth = 0;
2549
+ for (const line of text.split("\n")) lineWidth = Math.max(lineWidth, this.getStringWidth(line));
2550
+ cached = lineWidth;
2551
+ this.blockWidths.set(text, cached);
2552
+ }
2553
+ return cached;
2554
+ }
2555
+ };
2556
+ var Output = class {
2557
+ constructor(width, height) {
2558
+ this.ops = [];
2559
+ this.caches = new OutputCaches();
2560
+ this.width = width;
2561
+ this.height = height;
2562
+ }
2563
+ write(x, y, lines, transformers) {
2564
+ this.ops.push({
2565
+ type: "write",
2566
+ x,
2567
+ y,
2568
+ lines,
2569
+ transformers
2570
+ });
2571
+ }
2572
+ clip(rect) {
2573
+ this.ops.push({
2574
+ type: "clip",
2575
+ clip: rect
2576
+ });
2577
+ }
2578
+ unclip() {
2579
+ this.ops.push({ type: "unclip" });
2580
+ }
2581
+ get() {
2582
+ const output = [];
2583
+ for (let y = 0; y < this.height; y++) {
2584
+ const row = [];
2585
+ for (let x = 0; x < this.width; x++) row.push({
2586
+ type: "char",
2587
+ value: " ",
2588
+ fullWidth: false,
2589
+ styles: []
2590
+ });
2591
+ output.push(row);
2592
+ }
2593
+ const clips = [];
2594
+ for (const op of this.ops) {
2595
+ if (op.type === "clip") {
2596
+ clips.push(op.clip);
2597
+ continue;
2598
+ }
2599
+ if (op.type === "unclip") {
2600
+ clips.pop();
2601
+ continue;
2602
+ }
2603
+ const { transformers } = op;
2604
+ let { x, y } = op;
2605
+ let lines = op.lines;
2606
+ const clip = clips.at(-1);
2607
+ if (clip) {
2608
+ const clipV = typeof clip.y1 === "number" && typeof clip.y2 === "number";
2609
+ if (clipV) {
2610
+ const height = lines.length;
2611
+ if (y + height < clip.y1 || y > clip.y2) continue;
2612
+ }
2613
+ if (clipV) {
2614
+ const from = y < clip.y1 ? clip.y1 - y : 0;
2615
+ const height = lines.length;
2616
+ const to = y + height > clip.y2 ? clip.y2 - y : height;
2617
+ lines = lines.slice(from, to);
2618
+ if (y < clip.y1) y = clip.y1;
2619
+ }
2620
+ }
2621
+ const clipH = clip && typeof clip.x1 === "number" && typeof clip.x2 === "number" ? {
2622
+ x1: clip.x1,
2623
+ x2: clip.x2
2624
+ } : null;
2625
+ if (clipH && x > clipH.x2) continue;
2626
+ let offsetY = 0;
2627
+ for (let [index, line] of lines.entries()) {
2628
+ const currentLine = output[y + offsetY];
2629
+ if (!currentLine) continue;
2630
+ let lineX = x;
2631
+ if (clipH) {
2632
+ const lineWidth = this.caches.getStringWidth(line);
2633
+ if (lineX + lineWidth < clipH.x1 || lineX > clipH.x2) {
2634
+ offsetY++;
2635
+ continue;
2636
+ }
2637
+ const from = lineX < clipH.x1 ? clipH.x1 - lineX : 0;
2638
+ const to = lineX + lineWidth > clipH.x2 ? clipH.x2 - lineX : lineWidth;
2639
+ if (lineX < clipH.x1) lineX = clipH.x1;
2640
+ const maxWidth = clipH.x2 - lineX;
2641
+ line = safeSliceEnd(sliceAnsi(line, from, to), maxWidth);
2642
+ }
2643
+ for (const transformer of transformers) line = transformer(line, index);
2644
+ const characters = this.caches.getStyledChars(line);
2645
+ let offsetX = lineX;
2646
+ if (characters.length === 0) {
2647
+ offsetY++;
2648
+ continue;
2649
+ }
2650
+ const spaceCell = {
2651
+ type: "char",
2652
+ value: " ",
2653
+ fullWidth: false,
2654
+ styles: []
2655
+ };
2656
+ if (currentLine[offsetX]?.value === "" && offsetX > 0 && this.caches.getStringWidth(currentLine[offsetX - 1]?.value ?? "") > 1) currentLine[offsetX - 1] = spaceCell;
2657
+ for (const character of characters) {
2658
+ const characterWidth = Math.max(1, this.caches.getStringWidth(character.value));
2659
+ currentLine[offsetX] = character;
2660
+ if (characterWidth > 1) for (let i = 1; i < characterWidth; i++) currentLine[offsetX + i] = {
2661
+ type: "char",
2662
+ value: "",
2663
+ fullWidth: false,
2664
+ styles: character.styles
2665
+ };
2666
+ offsetX += characterWidth;
2667
+ }
2668
+ if (currentLine[offsetX]?.value === "") currentLine[offsetX] = spaceCell;
2669
+ offsetY++;
2670
+ }
2671
+ }
2672
+ return {
2673
+ output: output.map((line) => {
2674
+ return styledCharsToString(line.filter((item) => item !== void 0)).trimEnd();
2675
+ }).join("\n"),
2676
+ height: output.length
2677
+ };
2678
+ }
2679
+ };
2680
+ function renderTextWithInlineStyles(node, inheritedBg) {
2681
+ if (!node.children || node.children.length === 0) return "";
2682
+ const inner = squashInlineChildren(node.children, inheritedBg);
2683
+ return sanitizeAnsi(applyOwnStyle(node.props, inner, inheritedBg));
2684
+ }
2685
+ function applyOwnStyle(props, inner, inheritedBg) {
2686
+ if (inner.length === 0) return inner;
2687
+ const defined = Object.fromEntries(Object.entries(props).filter(([, v]) => v !== void 0));
2688
+ const ownBg = defined.backgroundColor;
2689
+ const effectiveBg = typeof ownBg === "string" ? ownBg : inheritedBg;
2690
+ return applyChalk(inner, {
2691
+ ...defined,
2692
+ backgroundColor: effectiveBg
2693
+ });
2694
+ }
2695
+ function squashInlineChildren(children, inheritedBg) {
2696
+ let out = "";
2697
+ let transformIndex = 0;
2698
+ for (const child of children) {
2699
+ out += squashTransformChild(child, transformIndex, inheritedBg);
2700
+ if (advancesLineIndex(child)) transformIndex++;
2701
+ }
2702
+ return out;
2703
+ }
2704
+ function renderTransformAsText(node, inheritedBg) {
2705
+ if (!node.children || node.children.length === 0) return "";
2706
+ return sanitizeAnsi(squashInlineChildren(node.children, inheritedBg));
2707
+ }
2708
+ function squashTransformChild(child, index, inheritedBg) {
2709
+ if (child.type === "text-leaf") return child.value;
2710
+ if (child.type === "tui-virtual-text" || child.type === "tui-text") return renderTextWithInlineStyles(child, inheritedBg);
2711
+ if (child.type === "tui-transform") {
2712
+ let innerText = "";
2713
+ let grandIndex = 0;
2714
+ for (const grandchild of child.children) {
2715
+ innerText += squashTransformChild(grandchild, grandIndex, inheritedBg);
2716
+ if (advancesLineIndex(grandchild)) grandIndex++;
2717
+ }
2718
+ if (innerText.length > 0 && child.transform) innerText = child.transform(innerText, index);
2719
+ return innerText;
2720
+ }
2721
+ return "";
2722
+ }
2723
+ function drawBorder(output, x, y, w, h, props, transformers) {
2724
+ const style = props["borderStyle"];
2725
+ if (!style) return;
2726
+ const chars = typeof style === "string" ? cliBoxes[style] : style;
2727
+ if (!chars) return;
2728
+ if (w < 1 || h < 1) return;
2729
+ const top = props["borderTop"] !== false;
2730
+ const bottom = props["borderBottom"] !== false;
2731
+ const left = props["borderLeft"] !== false;
2732
+ const right = props["borderRight"] !== false;
2733
+ const stringProp = (name) => {
2734
+ const value = props[name];
2735
+ return typeof value === "string" ? value : void 0;
2736
+ };
2737
+ const borderColor = stringProp("borderColor");
2738
+ const generalDim = props["borderDimColor"];
2739
+ const borderBackgroundColor = stringProp("borderBackgroundColor");
2740
+ function colorizeEdge(s, edge) {
2741
+ const capEdge = edge.charAt(0).toUpperCase() + edge.slice(1);
2742
+ const edgeColor = stringProp(`border${capEdge}Color`) ?? borderColor;
2743
+ const edgeDim = props[`border${capEdge}DimColor`] ?? generalDim;
2744
+ const edgeBg = stringProp(`border${capEdge}BackgroundColor`) ?? borderBackgroundColor;
2745
+ let styled = s;
2746
+ if (edgeColor) styled = applyColor(chalk, edgeColor, false)(styled);
2747
+ if (edgeBg) styled = applyColor(chalk, edgeBg, true)(styled);
2748
+ if (edgeDim) styled = chalk.dim(styled);
2749
+ return styled;
2750
+ }
2751
+ if (top) {
2752
+ const tl = left ? chars.topLeft : chars.top;
2753
+ const tr = right ? chars.topRight : chars.top;
2754
+ const fill = Math.max(0, w - stringWidth(tl) - stringWidth(tr));
2755
+ const raw = tl + chars.top.repeat(fill) + tr;
2756
+ output.write(x, y, [colorizeEdge(safeSliceEnd(raw, w), "top")], transformers);
2757
+ }
2758
+ if (bottom) {
2759
+ const bl = left ? chars.bottomLeft : chars.bottom;
2760
+ const br = right ? chars.bottomRight : chars.bottom;
2761
+ const fill = Math.max(0, w - stringWidth(bl) - stringWidth(br));
2762
+ const raw = bl + chars.bottom.repeat(fill) + br;
2763
+ output.write(x, y + h - 1, [colorizeEdge(safeSliceEnd(raw, w), "bottom")], transformers);
2764
+ }
2765
+ const offsetY = top ? 1 : 0;
2766
+ const verticalRun = Math.max(0, h - (top ? 1 : 0) - (bottom ? 1 : 0));
2767
+ for (let i = 0; i < verticalRun; i++) {
2768
+ if (left) output.write(x, y + offsetY + i, [colorizeEdge(chars.left, "left")], transformers);
2769
+ if (right) output.write(x + w - 1, y + offsetY + i, [colorizeEdge(chars.right, "right")], transformers);
2770
+ }
2771
+ }
2772
+ function getBoxContentMetrics(node, w, h) {
2773
+ const left = node.yoga.getComputedBorder(Yoga.EDGE_LEFT) + node.yoga.getComputedPadding(Yoga.EDGE_LEFT);
2774
+ const right = node.yoga.getComputedBorder(Yoga.EDGE_RIGHT) + node.yoga.getComputedPadding(Yoga.EDGE_RIGHT);
2775
+ const top = node.yoga.getComputedBorder(Yoga.EDGE_TOP) + node.yoga.getComputedPadding(Yoga.EDGE_TOP);
2776
+ const bottom = node.yoga.getComputedBorder(Yoga.EDGE_BOTTOM) + node.yoga.getComputedPadding(Yoga.EDGE_BOTTOM);
2777
+ const frameWidth = left + right;
2778
+ const frameHeight = top + bottom;
2779
+ return {
2780
+ width: Math.max(0, Math.floor(w - frameWidth)),
2781
+ height: Math.max(0, Math.floor(h - frameHeight))
2782
+ };
2783
+ }
2784
+ function fillBackground(output, x, y, w, h, color, transformers) {
2785
+ if (!color) return;
2786
+ const width = Math.max(0, Math.floor(w));
2787
+ const height = Math.max(0, Math.floor(h));
2788
+ if (width === 0 || height === 0) return;
2789
+ const line = applyChalk(" ".repeat(width), { backgroundColor: color });
2790
+ for (let i = 0; i < height; i++) output.write(x, y + i, [line], transformers);
2791
+ }
2792
+ function paint(root) {
2793
+ if (root.type !== "root") throw new Error("paint expects TuiRoot");
2794
+ const layout = root.yoga.getComputedLayout();
2795
+ const out = new Output(Math.max(1, Math.floor(layout.width)), Math.max(1, Math.floor(layout.height)));
2796
+ paintNode(root, out, 0, 0, []);
2797
+ return out.get().output;
2798
+ }
2799
+ function paintNode(node, output, x0, y0, transformers, inheritedBg) {
2800
+ if (node.yoga?.getDisplay?.() === Yoga.DISPLAY_NONE) return;
2801
+ switch (node.type) {
2802
+ case "root":
2803
+ for (const child of node.children) paintNode(child, output, x0, y0, transformers);
2804
+ return;
2805
+ case "tui-box": {
2806
+ const layout = node.yoga.getComputedLayout();
2807
+ const x = x0 + layout.left;
2808
+ const y = y0 + layout.top;
2809
+ const w = Math.max(0, Math.floor(layout.width));
2810
+ const h = Math.max(0, Math.floor(layout.height));
2811
+ const rawBg = node.props["backgroundColor"];
2812
+ const ownBg = typeof rawBg === "string" ? rawBg : void 0;
2813
+ const childBg = ownBg ? ownBg : inheritedBg;
2814
+ if (node.props["borderStyle"]) drawBorder(output, x, y, w, h, node.props, transformers);
2815
+ if (ownBg) {
2816
+ const hasBorder = !!node.props["borderStyle"];
2817
+ const bt = hasBorder && node.props["borderTop"] !== false ? 1 : 0;
2818
+ const bb = hasBorder && node.props["borderBottom"] !== false ? 1 : 0;
2819
+ const bl = hasBorder && node.props["borderLeft"] !== false ? 1 : 0;
2820
+ const br = hasBorder && node.props["borderRight"] !== false ? 1 : 0;
2821
+ fillBackground(output, x + bl, y + bt, w - bl - br, h - bt - bb, ownBg, transformers);
2822
+ }
2823
+ let clipped = false;
2824
+ const overflow = node.props["overflow"];
2825
+ const clipH = overflow === "hidden" || node.props["overflowX"] === "hidden";
2826
+ const clipV = overflow === "hidden" || node.props["overflowY"] === "hidden";
2827
+ if (clipH || clipV) {
2828
+ const bl = node.yoga.getComputedBorder(Yoga.EDGE_LEFT);
2829
+ const br = node.yoga.getComputedBorder(Yoga.EDGE_RIGHT);
2830
+ const bt = node.yoga.getComputedBorder(Yoga.EDGE_TOP);
2831
+ const bb = node.yoga.getComputedBorder(Yoga.EDGE_BOTTOM);
2832
+ output.clip({
2833
+ x1: clipH ? x + bl : void 0,
2834
+ x2: clipH ? x + w - br : void 0,
2835
+ y1: clipV ? y + bt : void 0,
2836
+ y2: clipV ? y + h - bb : void 0
2837
+ });
2838
+ clipped = true;
2839
+ }
2840
+ const contentMetrics = getBoxContentMetrics(node, w, h);
2841
+ if (contentMetrics.width === 0 || contentMetrics.height === 0) {
2842
+ for (const child of node.children) if (child.yoga?.getPositionType?.() === Yoga.POSITION_TYPE_ABSOLUTE) paintNode(child, output, x, y, transformers, childBg);
2843
+ if (clipped) output.unclip();
2844
+ return;
2845
+ }
2846
+ for (const child of node.children) paintNode(child, output, x, y, transformers, childBg);
2847
+ if (clipped) output.unclip();
2848
+ return;
2849
+ }
2850
+ case "tui-text": {
2851
+ const layout = node.yoga.getComputedLayout();
2852
+ const text = renderTextWithInlineStyles(node, inheritedBg);
2853
+ if (text === "") return;
2854
+ const wrapWidth = Math.floor(layout.width);
2855
+ const wrapped = wrapText(text, wrapWidth, node.props.wrap ?? "wrap");
2856
+ if (inheritedBg) {
2857
+ const padProps = { backgroundColor: inheritedBg };
2858
+ for (let i = 0; i < wrapped.length; i++) {
2859
+ const pad = wrapWidth - stringWidth(wrapped[i]);
2860
+ if (pad > 0) wrapped[i] = wrapped[i] + applyChalk(" ".repeat(pad), padProps);
2861
+ }
2862
+ }
2863
+ output.write(x0 + layout.left, y0 + layout.top, wrapped, transformers);
2864
+ return;
2865
+ }
2866
+ case "tui-static": return;
2867
+ case "tui-transform": {
2868
+ const layout = node.yoga.getComputedLayout();
2869
+ const x = x0 + layout.left;
2870
+ const y = y0 + layout.top;
2871
+ const next = [node.transform, ...transformers];
2872
+ if (!transformHasYogaChild(node)) {
2873
+ const text = renderTransformAsText(node, inheritedBg);
2874
+ if (text === "") return;
2875
+ const wrapped = wrapText(text, Math.max(1, Math.floor(layout.width)), "wrap");
2876
+ output.write(x, y, wrapped, next);
2877
+ return;
2878
+ }
2879
+ for (const child of node.children) paintNode(child, output, x, y, next, inheritedBg);
2880
+ return;
2881
+ }
2882
+ case "tui-virtual-text":
2883
+ case "text-leaf":
2884
+ case "comment": return;
2885
+ }
2886
+ }
2887
+ function paintIsolated(nodes, width, staticNode) {
2888
+ if (!staticNode) return paintUnderRoot(nodes, width, (iso) => iso.yoga.setWidth(width));
2889
+ const iso = createRoot({});
2890
+ attachYoga(iso);
2891
+ iso.yoga.setWidth(width);
2892
+ const staticBox = createBox();
2893
+ attachYoga(staticBox);
2894
+ staticBox.yoga.copyStyle(staticNode.yoga);
2895
+ staticBox.yoga.setDisplay(Yoga.DISPLAY_FLEX);
2896
+ staticBox.yoga.setPositionType(Yoga.POSITION_TYPE_ABSOLUTE);
2897
+ iso.yoga.insertChild(staticBox.yoga, 0);
2898
+ iso.children.push(staticBox);
2899
+ const yogaAdded = [];
2900
+ let yIdx = 0;
2901
+ for (let i = 0; i < nodes.length; i++) {
2902
+ const node = nodes[i];
2903
+ staticBox.children.push(node);
2904
+ const yCarrier = node;
2905
+ if (!yCarrier.yoga || typeof yCarrier.yoga === "symbol") continue;
2906
+ const yParent = yCarrier.yoga.getParent ? yCarrier.yoga.getParent() : null;
2907
+ const origIndex = yParent ? findYogaIndex(yParent, yCarrier.yoga) : 0;
2908
+ if (yParent) yParent.removeChild(yCarrier.yoga);
2909
+ staticBox.yoga.insertChild(yCarrier.yoga, yIdx);
2910
+ yogaAdded.push({
2911
+ yc: yCarrier,
2912
+ origParent: yParent,
2913
+ origIndex
2914
+ });
2915
+ yIdx++;
2916
+ }
2917
+ let restoreLayoutGuards = () => {};
2918
+ try {
2919
+ restoreLayoutGuards = calculateLayoutWithContentGuards(iso, width, void 0, Yoga.DIRECTION_LTR);
2920
+ const boxLayout = staticBox.yoga.getComputedLayout();
2921
+ const out = new Output(Math.max(1, Math.floor(boxLayout.width)), Math.max(1, Math.floor(boxLayout.height)));
2922
+ const x0 = -Math.floor(boxLayout.left);
2923
+ const y0 = -Math.floor(boxLayout.top);
2924
+ for (const child of staticBox.children) paintNode(child, out, x0, y0, []);
2925
+ return out.get().output;
2926
+ } finally {
2927
+ restoreLayoutGuards();
2928
+ for (const { yc, origParent, origIndex } of yogaAdded.slice().reverse()) {
2929
+ staticBox.yoga.removeChild(yc.yoga);
2930
+ if (origParent) origParent.insertChild(yc.yoga, origIndex);
2931
+ }
2932
+ staticBox.children.length = 0;
2933
+ iso.yoga.removeChild(staticBox.yoga);
2934
+ detachYoga(staticBox);
2935
+ iso.children.length = 0;
2936
+ detachYoga(iso);
2937
+ }
2938
+ }
2939
+ function paintUnderRoot(nodes, width, configureRoot) {
2940
+ const iso = createRoot({});
2941
+ attachYoga(iso);
2942
+ configureRoot(iso);
2943
+ const yogaAdded = [];
2944
+ let yIdx = 0;
2945
+ for (let i = 0; i < nodes.length; i++) {
2946
+ const node = nodes[i];
2947
+ iso.children.push(node);
2948
+ const yCarrier = node;
2949
+ if (!yCarrier.yoga || typeof yCarrier.yoga === "symbol") continue;
2950
+ const yParent = yCarrier.yoga.getParent ? yCarrier.yoga.getParent() : null;
2951
+ const origIndex = yParent ? findYogaIndex(yParent, yCarrier.yoga) : 0;
2952
+ if (yParent) yParent.removeChild(yCarrier.yoga);
2953
+ iso.yoga.insertChild(yCarrier.yoga, yIdx);
2954
+ yogaAdded.push({
2955
+ yc: yCarrier,
2956
+ origParent: yParent,
2957
+ origIndex
2958
+ });
2959
+ yIdx++;
2960
+ }
2961
+ let restoreLayoutGuards = () => {};
2962
+ try {
2963
+ restoreLayoutGuards = calculateLayoutWithContentGuards(iso, width, void 0, Yoga.DIRECTION_LTR);
2964
+ return paint(iso);
2965
+ } finally {
2966
+ restoreLayoutGuards();
2967
+ for (const { yc, origParent, origIndex } of yogaAdded.slice().reverse()) {
2968
+ iso.yoga.removeChild(yc.yoga);
2969
+ if (origParent) origParent.insertChild(yc.yoga, origIndex);
2970
+ }
2971
+ iso.children.length = 0;
2972
+ detachYoga(iso);
2973
+ }
2974
+ }
2975
+ function findYogaIndex(parent, child) {
2976
+ for (let i = 0; i < parent.getChildCount(); i++) if (parent.getChild(i) === child) return i;
2977
+ return 0;
2978
+ }
2979
+ //#endregion
2980
+ //#region src/paint/screen-reader.ts
2981
+ /**
2982
+ * Squash a single child of a text/transform context into plain SR text,
2983
+ * recursing GENERICALLY into transform-typed children to ANY depth and APPLYING
2984
+ * each CHILD transform's own fn. This is the SR twin of paint.ts /
2985
+ * text-measure.ts squashTransformChild and mirrors Ink's squashTextNodes
2986
+ * (squash-text-nodes.ts:22-39): it applies `internal_transform` for child nodes
2987
+ * (line 34) but NEVER for the top-level node it is handed — that node's own fn is
2988
+ * applied by the caller (Output), not by squash. So a nested <Transform>'s own fn
2989
+ * IS applied here (it is a child), while a STANDALONE <Transform>'s own fn is left
2990
+ * to the transform-branch caller below. (G58 MF3 / G32)
2991
+ */
2992
+ function squashChildSR(child, index) {
2993
+ if (child.type === "text-leaf") return child.value;
2994
+ if (child.type === "tui-virtual-text" || child.type === "tui-text") return squashTextContent(child);
2995
+ if (child.type === "tui-transform") {
2996
+ let innerText = "";
2997
+ let grandIndex = 0;
2998
+ for (const grandchild of child.children) {
2999
+ innerText += squashChildSR(grandchild, grandIndex);
3000
+ if (advancesLineIndex(grandchild)) grandIndex++;
3001
+ }
3002
+ if (innerText.length > 0 && child.transform) innerText = child.transform(innerText, index);
3003
+ return innerText;
3004
+ }
3005
+ return "";
3006
+ }
3007
+ /**
3008
+ * Squash text content from a text/virtual-text node tree into plain text
3009
+ * (no ANSI styling), suitable for screen reader output.
3010
+ */
3011
+ function squashTextContent(node) {
3012
+ let text = "";
3013
+ let index = 0;
3014
+ for (const child of node.children) {
3015
+ text += squashChildSR(child, index);
3016
+ if (advancesLineIndex(child)) index++;
3017
+ }
3018
+ return sanitizeAnsi(text);
3019
+ }
3020
+ /**
3021
+ * Resolve a box node's flexDirection as the string form the SR separator logic
3022
+ * compares against ("row" | "row-reverse" | "column" | "column-reverse").
3023
+ *
3024
+ * Prefer an explicit `props.flexDirection` when present (used by unit-test
3025
+ * fixtures that build nodes directly without a live yoga layout), otherwise read
3026
+ * the resolved direction back from the yoga node. node-ops applies flexDirection
3027
+ * to yoga but does NOT mirror it into `props` (it is not in STYLE_PROPS), and the
3028
+ * yoga node holds the Box default of row (host/yoga.ts sets FLEX_DIRECTION_ROW).
3029
+ * Mirrors static-channel.ts's resolvedFlexDirection so both SR linearization
3030
+ * paths derive the separator identically. (Ink parity, G39.)
3031
+ */
3032
+ function resolveBoxFlexDirection(node) {
3033
+ const fromProps = node.props["flexDirection"];
3034
+ if (fromProps !== void 0) return fromProps;
3035
+ switch (node.yoga.getFlexDirection()) {
3036
+ case Yoga.FLEX_DIRECTION_ROW: return "row";
3037
+ case Yoga.FLEX_DIRECTION_ROW_REVERSE: return "row-reverse";
3038
+ case Yoga.FLEX_DIRECTION_COLUMN_REVERSE: return "column-reverse";
3039
+ default: return "column";
3040
+ }
3041
+ }
3042
+ /**
3043
+ * Render a TUI node tree to a plain-text string suitable for screen readers.
3044
+ *
3045
+ * Ported from Ink's `renderNodeToScreenReaderOutput`.
3046
+ *
3047
+ * - `display: none` nodes are skipped.
3048
+ * - Text nodes have their content squashed (no ANSI).
3049
+ * - Box/root nodes recursively render children, joined by separator based on flexDirection.
3050
+ * - Nodes with `internal_accessibility` get role and state info prepended.
3051
+ */
3052
+ function renderScreenReaderOutput(node, options = {}) {
3053
+ if (options.skipStaticElements && node.type === "tui-static") return "";
3054
+ if ((node.type === "tui-box" || node.type === "tui-text" || node.type === "root" || node.type === "tui-transform") && node.yoga.getDisplay() === Yoga.DISPLAY_NONE) return "";
3055
+ let output = "";
3056
+ if (node.type === "tui-text") output = squashTextContent(node);
3057
+ else if (node.type === "tui-box" || node.type === "root") {
3058
+ const flexDirection = node.type === "tui-box" ? resolveBoxFlexDirection(node) : void 0;
3059
+ const separator = flexDirection === "row" || flexDirection === "row-reverse" ? " " : "\n";
3060
+ const children = flexDirection === "row-reverse" || flexDirection === "column-reverse" ? [...node.children].reverse() : node.children;
3061
+ const parentRole = node.internal_accessibility?.role;
3062
+ output = children.map((childNode) => renderScreenReaderOutput(childNode, {
3063
+ parentRole,
3064
+ skipStaticElements: options.skipStaticElements
3065
+ })).filter(Boolean).join(separator);
3066
+ } else if (node.type === "tui-transform") {
3067
+ let index = 0;
3068
+ let squashed = "";
3069
+ for (const childNode of node.children) {
3070
+ squashed += squashChildSR(childNode, index);
3071
+ if (advancesLineIndex(childNode)) index++;
3072
+ }
3073
+ output = sanitizeAnsi(squashed);
3074
+ }
3075
+ if (node.type === "tui-box") {
3076
+ const accessibility = node.internal_accessibility;
3077
+ if (accessibility) {
3078
+ const { role, state } = accessibility;
3079
+ if (state) {
3080
+ const stateDescription = Object.keys(state).filter((key) => state[key]).join(", ");
3081
+ if (stateDescription) output = `(${stateDescription}) ${output}`;
3082
+ }
3083
+ if (role && role !== options.parentRole) output = `${role}: ${output}`;
3084
+ }
3085
+ }
3086
+ return output;
3087
+ }
3088
+ //#endregion
3089
+ //#region src/paint/static-channel.ts
3090
+ /**
3091
+ * Read a static node's resolved flexDirection as the string form
3092
+ * screen-reader.ts compares against ("row" | "row-reverse" | "column" |
3093
+ * "column-reverse"). node-ops applies flexDirection to yoga but does NOT mirror
3094
+ * it into `props` (it's not in STYLE_PROPS), so we read it back from the yoga
3095
+ * node — which holds the resolved direction including the <Static> default of
3096
+ * column. This keeps separator/order derivation identical to how
3097
+ * screen-reader.ts (screen-reader.ts:73-82) would linearize a container.
3098
+ */
3099
+ function resolvedFlexDirection(stat) {
3100
+ switch (stat.yoga.getFlexDirection()) {
3101
+ case Yoga.FLEX_DIRECTION_ROW: return "row";
3102
+ case Yoga.FLEX_DIRECTION_ROW_REVERSE: return "row-reverse";
3103
+ case Yoga.FLEX_DIRECTION_COLUMN_REVERSE: return "column-reverse";
3104
+ default: return "column";
3105
+ }
3106
+ }
3107
+ function findStatics(root, out = []) {
3108
+ if (root.type === "tui-static") out.push(root);
3109
+ if (root.type !== "text-leaf" && root.type !== "comment") {
3110
+ const containerChildren = root.children;
3111
+ for (const child of containerChildren) findStatics(child, out);
3112
+ }
3113
+ return out;
3114
+ }
3115
+ function isInertStaticAnchor(child) {
3116
+ return child.type === "comment" || child.type === "text-leaf" && child.value === "";
3117
+ }
3118
+ /**
3119
+ * Paint the not-yet-written children of a single <Static> node and record them
3120
+ * as written. Returns the painted frame (without trailing "\n"), or "" when
3121
+ * there is nothing fresh to write.
3122
+ *
3123
+ * Static items are write-once. `stat.children` only ever holds the currently-
3124
+ * mounted (un-written) items because the <Static> component slices written ones
3125
+ * out — but between a write and the component's cursor advance, the just-written
3126
+ * children are still mounted, so we must skip any child already in `writtenNodes`
3127
+ * (identity-tracked, since one item = several host nodes incl. fragment anchors).
3128
+ * After painting the fresh children we call `onWritten` so the component advances
3129
+ * its cursor and unmounts them (the post-commit step mirroring Ink's
3130
+ * `useLayoutEffect(setIndex)`).
3131
+ */
3132
+ function paintStaticNode(stat, columns, isScreenReaderEnabled = false) {
3133
+ const fresh = stat.children.filter((child) => !stat.writtenNodes.has(child));
3134
+ const paintableFresh = fresh.filter((child) => !isInertStaticAnchor(child));
3135
+ let frame = "";
3136
+ if (paintableFresh.length > 0) {
3137
+ if (isScreenReaderEnabled) {
3138
+ const flexDirection = resolvedFlexDirection(stat);
3139
+ const separator = flexDirection === "row" || flexDirection === "row-reverse" ? " " : "\n";
3140
+ frame = (flexDirection === "row-reverse" || flexDirection === "column-reverse" ? [...paintableFresh].reverse() : paintableFresh).map((child) => renderScreenReaderOutput(child, { skipStaticElements: false })).filter(Boolean).join(separator);
3141
+ } else frame = paintIsolated(paintableFresh, columns, stat);
3142
+ for (const child of paintableFresh) stat.writtenNodes.add(child);
3143
+ }
3144
+ for (const child of fresh) if (isInertStaticAnchor(child)) stat.writtenNodes.add(child);
3145
+ if (stat.writtenNodes.size > stat.children.length) {
3146
+ const live = new Set(stat.children);
3147
+ for (const node of stat.writtenNodes) if (!live.has(node)) stat.writtenNodes.delete(node);
3148
+ }
3149
+ stat.onWritten?.();
3150
+ return frame;
3151
+ }
3152
+ //#endregion
3153
+ //#region src/io/cursor-helpers.ts
3154
+ const showCursorEscape = "\x1B[?25h";
3155
+ const hideCursorEscape = "\x1B[?25l";
3156
+ /**
3157
+ * Compare two cursor positions. Returns true if they differ.
3158
+ */
3159
+ const cursorPositionChanged = (a, b) => a?.x !== b?.x || a?.y !== b?.y;
3160
+ /**
3161
+ * Build escape sequence to move cursor from bottom of output to the target
3162
+ * position and show it.
3163
+ *
3164
+ * The starting row depends on the trailing newline. A frame written WITH a
3165
+ * trailing newline leaves the cursor on the blank row just past the content
3166
+ * (row `visibleLineCount`); a fullscreen frame is written WITHOUT a trailing
3167
+ * newline (render.ts:962 `isFullscreen ? output : output + "\n"`), so the cursor
3168
+ * stays on the LAST visible row (`visibleLineCount - 1`). `hasTrailingNewline`
3169
+ * selects the correct basis — using `visibleLineCount` for a no-trailing-newline
3170
+ * frame would move up one row too many, misplacing the declared caret and then
3171
+ * desyncing the next frame's buildReturnToBottom (it would undershoot the true
3172
+ * bottom, leaving stale rows). Defaults to true to preserve the trailing-newline
3173
+ * callers byte-for-byte.
3174
+ *
3175
+ * The position is clamped to the visible region before emitting: under the
3176
+ * persistent-declaration re-emit (the caret is re-asserted every commit until
3177
+ * the declaration changes), a stale {x,y} left over from a larger frame —
3178
+ * after a resize, overflow, or content shrink — must not produce an
3179
+ * out-of-range move. `y` is clamped to `[0, cursorRow]` (so a y past the
3180
+ * shrunk content lands on the last visible line, never below it) and `x` to
3181
+ * `[0, width - 1]` when `width` is known (so a column past the terminal edge
3182
+ * lands at the rightmost cell, not beyond it). This is D5 in the cursor design
3183
+ * study; D1 (a stale-but-in-range coordinate that no longer tracks content) is
3184
+ * accepted residue, not corrected here.
3185
+ */
3186
+ const buildCursorSuffix = (visibleLineCount, cursorPosition, width, hasTrailingNewline = true) => {
3187
+ if (!cursorPosition) return "";
3188
+ const cursorRow = hasTrailingNewline ? visibleLineCount : Math.max(0, visibleLineCount - 1);
3189
+ const clampedY = Math.max(0, Math.min(cursorPosition.y, cursorRow));
3190
+ const clampedX = width !== void 0 && width > 0 ? Math.max(0, Math.min(cursorPosition.x, width - 1)) : Math.max(0, cursorPosition.x);
3191
+ const moveUp = cursorRow - clampedY;
3192
+ return (moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : "") + ansiEscapes.cursorTo(clampedX) + showCursorEscape;
3193
+ };
3194
+ /**
3195
+ * Build escape sequence to move cursor from previousCursorPosition back to the
3196
+ * bottom of output.
3197
+ * This must be done before eraseLines or any operation that assumes cursor is
3198
+ * at the bottom.
3199
+ */
3200
+ const buildReturnToBottom = (previousLineCount, previousCursorPosition) => {
3201
+ if (!previousCursorPosition) return "";
3202
+ const bottomLine = previousLineCount - 1;
3203
+ const down = bottomLine - Math.max(0, Math.min(previousCursorPosition.y, bottomLine));
3204
+ return (down > 0 ? ansiEscapes.cursorDown(down) : "") + ansiEscapes.cursorTo(0);
3205
+ };
3206
+ /**
3207
+ * Build the escape sequence for cursor-only updates (output unchanged, cursor
3208
+ * moved). Hides cursor if it was previously shown, returns to bottom, then
3209
+ * repositions.
3210
+ */
3211
+ const buildCursorOnlySequence = (input) => {
3212
+ const hidePrefix = input.cursorWasShown ? hideCursorEscape : "";
3213
+ const returnToBottom = buildReturnToBottom(input.previousLineCount, input.previousCursorPosition);
3214
+ const cursorSuffix = buildCursorSuffix(input.visibleLineCount, input.cursorPosition, input.width, input.hasTrailingNewline ?? true);
3215
+ return hidePrefix + returnToBottom + cursorSuffix;
3216
+ };
3217
+ /**
3218
+ * Build the prefix that hides cursor and returns to bottom before erasing or
3219
+ * rewriting. Returns empty string if cursor was not shown.
3220
+ */
3221
+ const buildReturnToBottomPrefix = (cursorWasShown, previousLineCount, previousCursorPosition) => {
3222
+ if (!cursorWasShown) return "";
3223
+ return hideCursorEscape + buildReturnToBottom(previousLineCount, previousCursorPosition);
3224
+ };
3225
+ //#endregion
3226
+ //#region src/io/log-update.ts
3227
+ const visibleLineCount = (lines, str) => str.endsWith("\n") ? lines.length - 1 : lines.length;
3228
+ const isTtyStream = (stream) => Boolean(stream.isTTY);
3229
+ const streamWidth = (stream) => stream.columns;
3230
+ const canWriteToStream = (stream) => !stream.destroyed && !stream.writableEnded;
3231
+ const hideCursor = (stream) => {
3232
+ if (!isTtyStream(stream) || !canWriteToStream(stream)) return;
3233
+ stream.write(hideCursorEscape);
3234
+ };
3235
+ const showCursor = (stream) => {
3236
+ if (!isTtyStream(stream) || !canWriteToStream(stream)) return;
3237
+ stream.write(showCursorEscape);
3238
+ };
3239
+ const createStandard = (stream, { showCursor: showCursorOption = false } = {}) => {
3240
+ let previousLineCount = 0;
3241
+ let previousOutput = "";
3242
+ let hasHiddenCursor = false;
3243
+ let cursorPosition;
3244
+ let cursorDirty = false;
3245
+ let previousCursorPosition;
3246
+ let cursorWasShown = false;
3247
+ const getActiveCursor = () => cursorPosition;
3248
+ const hasChanges = (str, activeCursor) => {
3249
+ const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
3250
+ return str !== previousOutput || cursorChanged;
3251
+ };
3252
+ const render = (str) => {
3253
+ if (!showCursorOption && !hasHiddenCursor) {
3254
+ hideCursor(stream);
3255
+ hasHiddenCursor = true;
3256
+ }
3257
+ const activeCursor = getActiveCursor();
3258
+ cursorDirty = false;
3259
+ const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
3260
+ if (!hasChanges(str, activeCursor)) return false;
3261
+ const lines = str.split("\n");
3262
+ const visibleCount = visibleLineCount(lines, str);
3263
+ const hasTrailingNewline = str.endsWith("\n");
3264
+ const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor, streamWidth(stream), hasTrailingNewline);
3265
+ if (str === previousOutput && cursorChanged) stream.write(buildCursorOnlySequence({
3266
+ cursorWasShown,
3267
+ previousLineCount,
3268
+ previousCursorPosition,
3269
+ visibleLineCount: visibleCount,
3270
+ cursorPosition: activeCursor,
3271
+ width: streamWidth(stream),
3272
+ hasTrailingNewline
3273
+ }));
3274
+ else {
3275
+ previousOutput = str;
3276
+ const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition);
3277
+ stream.write(returnPrefix + ansiEscapes.eraseLines(previousLineCount) + str + cursorSuffix);
3278
+ previousLineCount = lines.length;
3279
+ }
3280
+ previousCursorPosition = activeCursor ? { ...activeCursor } : void 0;
3281
+ cursorWasShown = activeCursor !== void 0;
3282
+ return true;
3283
+ };
3284
+ render.clear = () => {
3285
+ const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition);
3286
+ stream.write(prefix + ansiEscapes.eraseLines(previousLineCount));
3287
+ previousOutput = "";
3288
+ previousLineCount = 0;
3289
+ previousCursorPosition = void 0;
3290
+ cursorWasShown = false;
3291
+ };
3292
+ render.done = () => {
3293
+ previousOutput = "";
3294
+ previousLineCount = 0;
3295
+ previousCursorPosition = void 0;
3296
+ cursorWasShown = false;
3297
+ if (!showCursorOption) {
3298
+ showCursor(stream);
3299
+ hasHiddenCursor = false;
3300
+ }
3301
+ };
3302
+ render.reset = () => {
3303
+ previousOutput = "";
3304
+ previousLineCount = 0;
3305
+ previousCursorPosition = void 0;
3306
+ cursorWasShown = false;
3307
+ };
3308
+ render.sync = (str, options) => {
3309
+ const activeCursor = options?.cursor === false ? void 0 : getActiveCursor();
3310
+ cursorDirty = false;
3311
+ const lines = str.split("\n");
3312
+ previousOutput = str;
3313
+ previousLineCount = lines.length;
3314
+ if (!activeCursor && cursorWasShown) stream.write(hideCursorEscape);
3315
+ if (activeCursor) stream.write(buildCursorSuffix(visibleLineCount(lines, str), activeCursor, streamWidth(stream), str.endsWith("\n")));
3316
+ previousCursorPosition = activeCursor ? { ...activeCursor } : void 0;
3317
+ cursorWasShown = activeCursor !== void 0;
3318
+ };
3319
+ render.setCursorPosition = (position) => {
3320
+ cursorPosition = position;
3321
+ cursorDirty = true;
3322
+ };
3323
+ render.isCursorDirty = () => cursorDirty;
3324
+ render.willRender = (str) => hasChanges(str, getActiveCursor());
3325
+ return render;
3326
+ };
3327
+ const createIncremental = (stream, { showCursor: showCursorOption = false } = {}) => {
3328
+ let previousLines = [];
3329
+ let previousOutput = "";
3330
+ let hasHiddenCursor = false;
3331
+ let cursorPosition;
3332
+ let cursorDirty = false;
3333
+ let previousCursorPosition;
3334
+ let cursorWasShown = false;
3335
+ const getActiveCursor = () => cursorPosition;
3336
+ const hasChanges = (str, activeCursor) => {
3337
+ const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
3338
+ return str !== previousOutput || cursorChanged;
3339
+ };
3340
+ const render = (str) => {
3341
+ if (!showCursorOption && !hasHiddenCursor) {
3342
+ hideCursor(stream);
3343
+ hasHiddenCursor = true;
3344
+ }
3345
+ const activeCursor = getActiveCursor();
3346
+ cursorDirty = false;
3347
+ const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition);
3348
+ if (!hasChanges(str, activeCursor)) return false;
3349
+ const nextLines = str.split("\n");
3350
+ const visibleCount = visibleLineCount(nextLines, str);
3351
+ const previousVisible = visibleLineCount(previousLines, previousOutput);
3352
+ if (str === previousOutput && cursorChanged) {
3353
+ stream.write(buildCursorOnlySequence({
3354
+ cursorWasShown,
3355
+ previousLineCount: previousLines.length,
3356
+ previousCursorPosition,
3357
+ visibleLineCount: visibleCount,
3358
+ cursorPosition: activeCursor,
3359
+ width: streamWidth(stream),
3360
+ hasTrailingNewline: str.endsWith("\n")
3361
+ }));
3362
+ previousCursorPosition = activeCursor ? { ...activeCursor } : void 0;
3363
+ cursorWasShown = activeCursor !== void 0;
3364
+ return true;
3365
+ }
3366
+ const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
3367
+ if (str === "\n" || previousOutput.length === 0) {
3368
+ const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor, streamWidth(stream), str.endsWith("\n"));
3369
+ stream.write(returnPrefix + ansiEscapes.eraseLines(previousLines.length) + str + cursorSuffix);
3370
+ cursorWasShown = activeCursor !== void 0;
3371
+ previousCursorPosition = activeCursor ? { ...activeCursor } : void 0;
3372
+ previousOutput = str;
3373
+ previousLines = nextLines;
3374
+ return true;
3375
+ }
3376
+ const hasTrailingNewline = str.endsWith("\n");
3377
+ const buffer = [];
3378
+ buffer.push(returnPrefix);
3379
+ if (visibleCount < previousVisible) {
3380
+ const extraSlot = previousOutput.endsWith("\n") ? 1 : 0;
3381
+ buffer.push(ansiEscapes.eraseLines(previousVisible - visibleCount + extraSlot), ansiEscapes.cursorUp(visibleCount));
3382
+ } else buffer.push(ansiEscapes.cursorUp(previousLines.length - 1));
3383
+ for (let i = 0; i < visibleCount; i++) {
3384
+ const isLastLine = i === visibleCount - 1;
3385
+ if (nextLines[i] === previousLines[i]) {
3386
+ if (!isLastLine || hasTrailingNewline) buffer.push(ansiEscapes.cursorNextLine);
3387
+ continue;
3388
+ }
3389
+ buffer.push(ansiEscapes.cursorTo(0) + nextLines[i] + ansiEscapes.eraseEndLine + (isLastLine && !hasTrailingNewline ? "" : "\n"));
3390
+ }
3391
+ const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor, streamWidth(stream), hasTrailingNewline);
3392
+ buffer.push(cursorSuffix);
3393
+ stream.write(buffer.join(""));
3394
+ cursorWasShown = activeCursor !== void 0;
3395
+ previousCursorPosition = activeCursor ? { ...activeCursor } : void 0;
3396
+ previousOutput = str;
3397
+ previousLines = nextLines;
3398
+ return true;
3399
+ };
3400
+ render.clear = () => {
3401
+ const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition);
3402
+ stream.write(prefix + ansiEscapes.eraseLines(previousLines.length));
3403
+ previousOutput = "";
3404
+ previousLines = [];
3405
+ previousCursorPosition = void 0;
3406
+ cursorWasShown = false;
3407
+ };
3408
+ render.done = () => {
3409
+ previousOutput = "";
3410
+ previousLines = [];
3411
+ previousCursorPosition = void 0;
3412
+ cursorWasShown = false;
3413
+ if (!showCursorOption) {
3414
+ showCursor(stream);
3415
+ hasHiddenCursor = false;
3416
+ }
3417
+ };
3418
+ render.reset = () => {
3419
+ previousOutput = "";
3420
+ previousLines = [];
3421
+ previousCursorPosition = void 0;
3422
+ cursorWasShown = false;
3423
+ };
3424
+ render.sync = (str, options) => {
3425
+ const activeCursor = options?.cursor === false ? void 0 : getActiveCursor();
3426
+ cursorDirty = false;
3427
+ const lines = str.split("\n");
3428
+ previousOutput = str;
3429
+ previousLines = lines;
3430
+ if (!activeCursor && cursorWasShown) stream.write(hideCursorEscape);
3431
+ if (activeCursor) stream.write(buildCursorSuffix(visibleLineCount(lines, str), activeCursor, streamWidth(stream), str.endsWith("\n")));
3432
+ previousCursorPosition = activeCursor ? { ...activeCursor } : void 0;
3433
+ cursorWasShown = activeCursor !== void 0;
3434
+ };
3435
+ render.setCursorPosition = (position) => {
3436
+ cursorPosition = position;
3437
+ cursorDirty = true;
3438
+ };
3439
+ render.isCursorDirty = () => cursorDirty;
3440
+ render.willRender = (str) => hasChanges(str, getActiveCursor());
3441
+ return render;
3442
+ };
3443
+ const create = (stream, { showCursor: showCursorOption = false, incremental = false } = {}) => {
3444
+ if (incremental) return createIncremental(stream, { showCursor: showCursorOption });
3445
+ return createStandard(stream, { showCursor: showCursorOption });
3446
+ };
3447
+ const logUpdate = { create };
3448
+ //#endregion
3449
+ //#region src/io/frame-writer.ts
3450
+ function createFrameWriter(stream, options) {
3451
+ let lastFrame = null;
3452
+ const debug = options.debug ?? false;
3453
+ const log = debug ? null : logUpdate.create(stream, { incremental: options.incremental });
3454
+ return {
3455
+ write(frame) {
3456
+ if (frame === lastFrame && !(log && log.isCursorDirty())) return;
3457
+ lastFrame = frame;
3458
+ if (debug) stream.write(frame + "\n");
3459
+ else log(frame);
3460
+ },
3461
+ done() {
3462
+ if (log) log.done();
3463
+ },
3464
+ clear() {
3465
+ lastFrame = null;
3466
+ if (log) log.clear();
3467
+ },
3468
+ sync(frame, options) {
3469
+ lastFrame = frame;
3470
+ if (log) log.sync(frame, options);
3471
+ },
3472
+ setCursorPosition(pos) {
3473
+ if (log) log.setCursorPosition(pos);
3474
+ },
3475
+ isCursorDirty() {
3476
+ return log ? log.isCursorDirty() : false;
3477
+ },
3478
+ willRender(frame) {
3479
+ return log ? log.willRender(frame) : true;
3480
+ }
3481
+ };
3482
+ }
3483
+ //#endregion
3484
+ //#region src/io/frame-sink.ts
3485
+ /**
3486
+ * Symbol key for the internal frame sink on the mount options object. Unique
3487
+ * (created via `Symbol(...)`, not `Symbol.for(...)`) so it can never collide
3488
+ * with a user-supplied key and is invisible to normal property enumeration.
3489
+ */
3490
+ const INTERNAL_FRAME_SINK = Symbol("solid-tui.internal.frameSink");
3491
+ //#endregion
3492
+ //#region src/io/write-synchronized.ts
3493
+ const bsu = "\x1B[?2026h";
3494
+ const esu = "\x1B[?2026l";
3495
+ function shouldSynchronize(stream, interactive) {
3496
+ return "isTTY" in stream && stream.isTTY && (interactive ?? !isInCi);
3497
+ }
3498
+ //#endregion
3499
+ //#region src/context.ts
3500
+ const AppContextKey = createContext();
3501
+ const FocusContextKey = createContext();
3502
+ const StdinContextKey = createContext();
3503
+ const AnimationSchedulerKey = createContext();
3504
+ const TextContextKey = createContext(false);
3505
+ //#endregion
3506
+ //#region src/components/box-validate.ts
3507
+ const BOX_STYLE_GLYPHS = [
3508
+ "top",
3509
+ "bottom",
3510
+ "left",
3511
+ "right",
3512
+ "topLeft",
3513
+ "topRight",
3514
+ "bottomLeft",
3515
+ "bottomRight"
3516
+ ];
3517
+ /**
3518
+ * True only when `value` is a real BoxStyle: an object carrying every glyph
3519
+ * paint reads, each a string. Rejects undefined (unknown preset name), the
3520
+ * cli-boxes `default` self-key / prototype members (objects without string
3521
+ * glyphs), partial custom objects, and truthy non-objects (e.g. a number).
3522
+ */
3523
+ function isValidBoxStyleShape(value) {
3524
+ if (typeof value !== "object" || value === null) return false;
3525
+ const box = value;
3526
+ return BOX_STYLE_GLYPHS.every((glyph) => typeof box[glyph] === "string");
3527
+ }
3528
+ /**
3529
+ * Eager render-time validation for `<Box>`. Runs every render and throws into the
3530
+ * error boundary on invalid input — exactly as box.ts's render fn did. Returns
3531
+ * `true` so it can gate a `v-if`.
3532
+ *
3533
+ * Everything validated here is PAINT-TIME VISUAL input (own/border background colors,
3534
+ * per-edge border foreground colors, borderStyle shape) — it only matters when the
3535
+ * Box is actually painted. There is no structural validation here. So callers must
3536
+ * skip it whenever the Box's visuals are never painted:
3537
+ * - a screen-reader-HIDDEN Box (a non-emitted node never colorizes — same ordering
3538
+ * as box.ts), and
3539
+ * - GLOBAL screen-reader mode (the whole tree is linearized to plain text; solid-tui,
3540
+ * like Ink, never colorizes / draws borders for ANY node, so it never throws on an
3541
+ * invalid color — verified against Ink v7.0.4). See box.ts.
3542
+ */
3543
+ function assertBoxValid(props) {
3544
+ assertValidBackgroundColor(props.backgroundColor);
3545
+ if (props.borderStyle) {
3546
+ const stringStyle = (value) => typeof value === "string" ? value : void 0;
3547
+ const generalBg = stringStyle(props.borderBackgroundColor);
3548
+ const generalFg = stringStyle(props.borderColor);
3549
+ if (props.borderTop !== false) {
3550
+ assertValidForegroundColor(stringStyle(props.borderTopColor) ?? generalFg, "borderTopColor");
3551
+ assertValidBackgroundColor(stringStyle(props.borderTopBackgroundColor) ?? generalBg, "borderTopBackgroundColor");
3552
+ }
3553
+ if (props.borderBottom !== false) {
3554
+ assertValidForegroundColor(stringStyle(props.borderBottomColor) ?? generalFg, "borderBottomColor");
3555
+ assertValidBackgroundColor(stringStyle(props.borderBottomBackgroundColor) ?? generalBg, "borderBottomBackgroundColor");
3556
+ }
3557
+ if (props.borderLeft !== false) {
3558
+ assertValidForegroundColor(stringStyle(props.borderLeftColor) ?? generalFg, "borderLeftColor");
3559
+ assertValidBackgroundColor(stringStyle(props.borderLeftBackgroundColor) ?? generalBg, "borderLeftBackgroundColor");
3560
+ }
3561
+ if (props.borderRight !== false) {
3562
+ assertValidForegroundColor(stringStyle(props.borderRightColor) ?? generalFg, "borderRightColor");
3563
+ assertValidBackgroundColor(stringStyle(props.borderRightBackgroundColor) ?? generalBg, "borderRightBackgroundColor");
3564
+ }
3565
+ }
3566
+ const borderStyle = props.borderStyle;
3567
+ if (borderStyle) {
3568
+ if (typeof borderStyle === "string") {
3569
+ const resolved = cliBoxes[borderStyle];
3570
+ if (!isValidBoxStyleShape(resolved)) throw new Error(`Unknown borderStyle: ${JSON.stringify(borderStyle)}`);
3571
+ } else if (!isValidBoxStyleShape(borderStyle)) throw new Error(`Invalid borderStyle: ${JSON.stringify(borderStyle)}`);
3572
+ }
3573
+ return true;
3574
+ }
3575
+ //#endregion
3576
+ //#region src/components/box.ts
3577
+ function Box(props) {
3578
+ const appCtx = useContext(AppContextKey);
3579
+ const [local, rest] = splitProps(props, [
3580
+ "children",
3581
+ "ariaHidden",
3582
+ "ariaLabel"
3583
+ ]);
3584
+ const el = createElement("tui-box");
3585
+ spread(el, rest, true);
3586
+ insert(el, () => {
3587
+ return (appCtx?.isScreenReaderEnabled ?? false) && local.ariaLabel ? local.ariaLabel : local.children;
3588
+ });
3589
+ return (() => {
3590
+ const srEnabled = appCtx?.isScreenReaderEnabled ?? false;
3591
+ if (srEnabled && local.ariaHidden) return null;
3592
+ if (!srEnabled) assertBoxValid(props);
3593
+ return el;
3594
+ });
3595
+ }
3596
+ //#endregion
3597
+ //#region src/components/text.ts
3598
+ function Text(props) {
3599
+ const insideText = useContext(TextContextKey);
3600
+ const appCtx = useContext(AppContextKey);
3601
+ const [local, rest] = splitProps(props, [
3602
+ "children",
3603
+ "ariaHidden",
3604
+ "ariaLabel"
3605
+ ]);
3606
+ const el = createElement(insideText ? "tui-virtual-text" : "tui-text");
3607
+ const content = createComponent$1(TextContextKey.Provider, {
3608
+ value: true,
3609
+ get children() {
3610
+ return local.children;
3611
+ }
3612
+ });
3613
+ spread(el, () => ({
3614
+ ...rest,
3615
+ wrap: rest.wrap ?? "wrap",
3616
+ ...insideText ? null : { flexShrink: 1 }
3617
+ }), true);
3618
+ insert(el, () => {
3619
+ return (appCtx?.isScreenReaderEnabled ?? false) && local.ariaLabel ? local.ariaLabel : content;
3620
+ });
3621
+ return (() => {
3622
+ const srEnabled = appCtx?.isScreenReaderEnabled ?? false;
3623
+ if (srEnabled && local.ariaHidden) return null;
3624
+ if (!srEnabled) {
3625
+ assertValidForegroundColor(rest.color);
3626
+ assertValidBackgroundColor(rest.backgroundColor);
3627
+ }
3628
+ return srEnabled && local.ariaLabel != null || local.children != null ? el : null;
3629
+ });
3630
+ }
3631
+ //#endregion
3632
+ //#region src/components/error-overview.ts
3633
+ const cleanupPath = (path) => {
3634
+ return path?.replace(`file://${cwd()}/`, "");
3635
+ };
3636
+ const stackUtils = new StackUtils({
3637
+ cwd: cwd(),
3638
+ internals: StackUtils.nodeInternals()
3639
+ });
3640
+ const safeString = (value) => {
3641
+ try {
3642
+ return String(value);
3643
+ } catch {
3644
+ return "[unserializable value]";
3645
+ }
3646
+ };
3647
+ function messageForNonError(value) {
3648
+ let message;
3649
+ try {
3650
+ message = value?.message;
3651
+ } catch {
3652
+ return safeString(value);
3653
+ }
3654
+ return typeof message === "string" ? message : safeString(value);
3655
+ }
3656
+ function isErrorInput(value) {
3657
+ try {
3658
+ return value instanceof Error || Object.prototype.toString.call(value) === "[object Error]";
3659
+ } catch {
3660
+ return false;
3661
+ }
3662
+ }
3663
+ function ErrorOverview(props) {
3664
+ return createComponent$1(Box, {
3665
+ flexDirection: "column",
3666
+ padding: 1,
3667
+ get children() {
3668
+ const error = props.error;
3669
+ let errorStack;
3670
+ try {
3671
+ const rawStack = error?.stack;
3672
+ errorStack = typeof rawStack === "string" ? rawStack : void 0;
3673
+ } catch {
3674
+ errorStack = void 0;
3675
+ }
3676
+ const errorMessage = messageForNonError(error);
3677
+ const stack = errorStack ? errorStack.split("\n").slice(1) : void 0;
3678
+ const origin = stack ? stackUtils.parseLine(stack[0]) : void 0;
3679
+ const filePath = cleanupPath(origin?.file);
3680
+ let excerpt;
3681
+ let lineWidth = 0;
3682
+ if (filePath && origin?.line && fs.existsSync(filePath)) try {
3683
+ excerpt = codeExcerpt(fs.readFileSync(filePath, "utf8"), origin.line);
3684
+ if (excerpt) for (const { line } of excerpt) lineWidth = Math.max(lineWidth, String(line).length);
3685
+ } catch {
3686
+ excerpt = void 0;
3687
+ }
3688
+ const parts = [createComponent$1(Box, { get children() {
3689
+ return [createComponent$1(Text, {
3690
+ backgroundColor: "red",
3691
+ color: "white",
3692
+ children: " ERROR "
3693
+ }), createComponent$1(Text, { children: ` ${errorMessage}` })];
3694
+ } })];
3695
+ if (origin && filePath) parts.push(createComponent$1(Box, {
3696
+ marginTop: 1,
3697
+ get children() {
3698
+ return createComponent$1(Text, {
3699
+ dimColor: true,
3700
+ children: `${filePath}:${origin.line}:${origin.column}`
3701
+ });
3702
+ }
3703
+ }));
3704
+ if (origin && excerpt) parts.push(createComponent$1(Box, {
3705
+ marginTop: 1,
3706
+ flexDirection: "column",
3707
+ get children() {
3708
+ return excerpt.map(({ line, value }) => createComponent$1(Box, { get children() {
3709
+ return [createComponent$1(Box, {
3710
+ width: lineWidth + 1,
3711
+ get children() {
3712
+ return createComponent$1(Text, {
3713
+ dimColor: line !== origin.line,
3714
+ backgroundColor: line === origin.line ? "red" : void 0,
3715
+ color: line === origin.line ? "white" : void 0,
3716
+ ariaLabel: line === origin.line ? `Line ${line}, error` : `Line ${line}`,
3717
+ children: `${String(line).padStart(lineWidth, " ")}:`
3718
+ });
3719
+ }
3720
+ }), createComponent$1(Text, {
3721
+ backgroundColor: line === origin.line ? "red" : void 0,
3722
+ color: line === origin.line ? "white" : void 0,
3723
+ children: ` ${value}`
3724
+ })];
3725
+ } }));
3726
+ }
3727
+ }));
3728
+ if (errorStack) parts.push(createComponent$1(Box, {
3729
+ marginTop: 1,
3730
+ flexDirection: "column",
3731
+ get children() {
3732
+ return errorStack.split("\n").slice(1).map((line) => {
3733
+ const parsedLine = stackUtils.parseLine(line);
3734
+ if (!parsedLine) return createComponent$1(Box, { get children() {
3735
+ return [createComponent$1(Text, {
3736
+ dimColor: true,
3737
+ children: "- "
3738
+ }), createComponent$1(Text, {
3739
+ dimColor: true,
3740
+ bold: true,
3741
+ children: line + "\\t "
3742
+ })];
3743
+ } });
3744
+ const file = cleanupPath(parsedLine.file) ?? "";
3745
+ return createComponent$1(Box, { get children() {
3746
+ return [
3747
+ createComponent$1(Text, {
3748
+ dimColor: true,
3749
+ children: "- "
3750
+ }),
3751
+ createComponent$1(Text, {
3752
+ dimColor: true,
3753
+ bold: true,
3754
+ children: parsedLine.function
3755
+ }),
3756
+ createComponent$1(Text, {
3757
+ dimColor: true,
3758
+ color: "gray",
3759
+ ariaLabel: `at ${file} line ${parsedLine.line} column ${parsedLine.column}`,
3760
+ children: ` (${file}:${parsedLine.line}:${parsedLine.column})`
3761
+ })
3762
+ ];
3763
+ } });
3764
+ });
3765
+ }
3766
+ }));
3767
+ return parts;
3768
+ }
3769
+ });
3770
+ }
3771
+ //#endregion
3772
+ //#region src/hooks/useWindowSize.ts
3773
+ function resolveSize(stdout) {
3774
+ const cols = stdout.columns;
3775
+ const rowsVal = stdout.rows;
3776
+ if (cols && rowsVal) return {
3777
+ columns: cols,
3778
+ rows: rowsVal
3779
+ };
3780
+ const fallback = terminalSize();
3781
+ return {
3782
+ columns: cols || fallback.columns || 80,
3783
+ rows: rowsVal || fallback.rows || 24
3784
+ };
3785
+ }
3786
+ function useWindowSize() {
3787
+ const ctx = useContext(AppContextKey);
3788
+ if (!ctx) throw new Error("useWindowSize() must be called inside a solid-tui render tree");
3789
+ const appCtx = ctx;
3790
+ const initial = resolveSize(appCtx.stdout);
3791
+ const [columns, setColumns] = createSignal(initial.columns);
3792
+ const [rows, setRows] = createSignal(initial.rows);
3793
+ function onResize() {
3794
+ const size = resolveSize(appCtx.stdout);
3795
+ setColumns(size.columns);
3796
+ setRows(size.rows);
3797
+ }
3798
+ appCtx.stdout.on("resize", onResize);
3799
+ onCleanup(() => appCtx.stdout.off("resize", onResize));
3800
+ return {
3801
+ columns,
3802
+ rows
3803
+ };
3804
+ }
3805
+ //#endregion
3806
+ //#region src/hmr.ts
3807
+ const [readDevState, writeDevState] = createSignal({ type: "ok" });
3808
+ const DevStateKey = createContext();
3809
+ const devState = {
3810
+ get value() {
3811
+ return readDevState();
3812
+ },
3813
+ set value(next) {
3814
+ writeDevState(next);
3815
+ },
3816
+ read: readDevState
3817
+ };
3818
+ const realHot = import.meta.hot;
3819
+ let bridgedHot;
3820
+ let currentDevAppTeardown;
3821
+ let pendingResetTimer;
3822
+ let devConnected = false;
3823
+ function registerDevApp(teardown) {
3824
+ currentDevAppTeardown = teardown;
3825
+ }
3826
+ function unregisterDevApp(teardown) {
3827
+ if (currentDevAppTeardown === teardown) currentDevAppTeardown = void 0;
3828
+ }
3829
+ function initHmrBridge(hot = realHot) {
3830
+ if (!hot || hot === bridgedHot) return;
3831
+ bridgedHot = hot;
3832
+ hot.on("vite:error", (payload) => {
3833
+ if (pendingResetTimer !== void 0) {
3834
+ clearTimeout(pendingResetTimer);
3835
+ pendingResetTimer = void 0;
3836
+ }
3837
+ devState.value = {
3838
+ type: "error",
3839
+ error: payload.err
3840
+ };
3841
+ });
3842
+ hot.on("vite:beforeUpdate", (payload) => {
3843
+ if (pendingResetTimer !== void 0) {
3844
+ clearTimeout(pendingResetTimer);
3845
+ pendingResetTimer = void 0;
3846
+ }
3847
+ devState.value = {
3848
+ type: "update",
3849
+ paths: payload.updates.map((u) => u.path)
3850
+ };
3851
+ const timer = setTimeout(() => {
3852
+ pendingResetTimer = void 0;
3853
+ if (devState.value.type === "update") devState.value = { type: "ok" };
3854
+ }, 2e3);
3855
+ pendingResetTimer = timer;
3856
+ timer.unref?.();
3857
+ });
3858
+ hot.on("vite:beforeFullReload", () => {
3859
+ currentDevAppTeardown?.();
3860
+ currentDevAppTeardown = void 0;
3861
+ hot.send("solid-tui:request-reload");
3862
+ });
3863
+ }
3864
+ function isDevConnected() {
3865
+ return devConnected;
3866
+ }
3867
+ function connectDevtools(hot) {
3868
+ devConnected = true;
3869
+ initHmrBridge(hot);
3870
+ }
3871
+ function notifyDevExit() {
3872
+ bridgedHot?.send("solid-tui:exit");
3873
+ }
3874
+ function resetDevState() {
3875
+ devState.value = { type: "ok" };
3876
+ }
3877
+ //#endregion
3878
+ //#region src/overlay.ts
3879
+ function ErrorDisplay(props) {
3880
+ return createComponent$1(Box, {
3881
+ flexDirection: "column",
3882
+ borderStyle: "single",
3883
+ borderColor: "red",
3884
+ paddingX: 1,
3885
+ get children() {
3886
+ return [
3887
+ createComponent$1(Text, {
3888
+ color: "red",
3889
+ bold: true,
3890
+ children: "Build Error"
3891
+ }),
3892
+ createComponent$1(Text, { get children() {
3893
+ return props.error.error.message;
3894
+ } }),
3895
+ props.error.error.loc ? createComponent$1(Text, {
3896
+ dimColor: true,
3897
+ get children() {
3898
+ const loc = props.error.error.loc;
3899
+ return `${loc.file}:${loc.line}:${loc.column}`;
3900
+ }
3901
+ }) : null
3902
+ ];
3903
+ }
3904
+ });
3905
+ }
3906
+ function StatusLine(props) {
3907
+ return createComponent$1(Text, {
3908
+ dimColor: true,
3909
+ get children() {
3910
+ return `[HMR] updated: ${props.paths.join(", ")}`;
3911
+ }
3912
+ });
3913
+ }
3914
+ function createDevOverlayWrapper(Root, rootProps) {
3915
+ return function DevOverlayWrapper() {
3916
+ const state = useContext(DevStateKey);
3917
+ return (() => {
3918
+ const current = state?.() ?? { type: "ok" };
3919
+ if (current.type === "error") return createComponent$1(ErrorDisplay, { error: current });
3920
+ return createComponent$1(Box, {
3921
+ flexDirection: "column",
3922
+ flexGrow: 1,
3923
+ get children() {
3924
+ return [createComponent$1(Box, {
3925
+ flexGrow: 1,
3926
+ get children() {
3927
+ return createComponent$1(Root, rootProps ?? {});
3928
+ }
3929
+ }), current.type === "update" ? createComponent$1(StatusLine, { paths: current.paths }) : null];
3930
+ }
3931
+ });
3932
+ });
3933
+ };
3934
+ }
3935
+ //#endregion
3936
+ //#region src/render.ts
3937
+ const liveInstances = /* @__PURE__ */ new WeakMap();
3938
+ function getWritableStreamState(stdout) {
3939
+ return {
3940
+ canWriteToStdout: !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true),
3941
+ hasWritableState: stdout._writableState !== void 0 || stdout.writableLength !== void 0
3942
+ };
3943
+ }
3944
+ function isComponent(value) {
3945
+ return typeof value === "function";
3946
+ }
3947
+ function createApp(root, rootProps) {
3948
+ let exitResolve;
3949
+ let exitReject;
3950
+ const exitPromise = new Promise((res, rej) => {
3951
+ exitResolve = res;
3952
+ exitReject = rej;
3953
+ });
3954
+ exitPromise.catch(() => {});
3955
+ let mountedRoot = null;
3956
+ let mountedDispose = null;
3957
+ let mountedScheduler = null;
3958
+ let mountedAnimationScheduler = null;
3959
+ let mountedStdinController = null;
3960
+ let mountedKittyController = null;
3961
+ let mountedRestoreRendererCommit = null;
3962
+ let mountedRestoreConsole = null;
3963
+ let mountedResizeHandler = null;
3964
+ let mountedUnsubscribeExit = null;
3965
+ let mountedExitListener = null;
3966
+ let mountedDevTeardown = null;
3967
+ let mountedClear = null;
3968
+ let mountedAppContext = null;
3969
+ let mountedStdout = null;
3970
+ let mountedGetLastOutput = null;
3971
+ let mountedAsOwner = false;
3972
+ let teardownStarted = false;
3973
+ let exitInitiated = false;
3974
+ let pendingExitError;
3975
+ let pendingExitResult;
3976
+ function resolveExit() {
3977
+ const finish = () => {
3978
+ if (isErrorInput(pendingExitError)) exitReject(pendingExitError);
3979
+ else exitResolve(pendingExitResult);
3980
+ };
3981
+ const stdout = mountedAppContext?.stdout;
3982
+ if (!stdout) {
3983
+ setImmediate(finish);
3984
+ return;
3985
+ }
3986
+ const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
3987
+ if (canWriteToStdout && hasWritableState) stdout.write("", finish);
3988
+ else setImmediate(finish);
3989
+ }
3990
+ function teardown() {
3991
+ if (teardownStarted) return;
3992
+ teardownStarted = true;
3993
+ mountedScheduler?.cancel();
3994
+ mountedRestoreRendererCommit?.();
3995
+ mountedRestoreRendererCommit = null;
3996
+ mountedClear = null;
3997
+ mountedGetLastOutput = null;
3998
+ if (mountedRestoreConsole) {
3999
+ mountedRestoreConsole();
4000
+ mountedRestoreConsole = null;
4001
+ }
4002
+ mountedDispose?.();
4003
+ mountedDispose = null;
4004
+ mountedAnimationScheduler?.dispose();
4005
+ mountedAnimationScheduler = null;
4006
+ mountedKittyController?.dispose();
4007
+ mountedKittyController = null;
4008
+ mountedStdinController?.dispose();
4009
+ mountedStdinController = null;
4010
+ if (mountedRoot) {
4011
+ detachYoga(mountedRoot);
4012
+ mountedRoot = null;
4013
+ }
4014
+ if (mountedResizeHandler && mountedAppContext) mountedAppContext.stdout.off("resize", mountedResizeHandler);
4015
+ mountedResizeHandler = null;
4016
+ if (mountedUnsubscribeExit) {
4017
+ mountedUnsubscribeExit();
4018
+ mountedUnsubscribeExit = null;
4019
+ }
4020
+ if (mountedExitListener) {
4021
+ process.off("exit", mountedExitListener);
4022
+ mountedExitListener = null;
4023
+ }
4024
+ if (mountedDevTeardown) {
4025
+ unregisterDevApp(mountedDevTeardown);
4026
+ mountedDevTeardown = null;
4027
+ }
4028
+ if (mountedAppContext?.interactive && mountedAppContext.stdout.isTTY) {
4029
+ mountedAppContext.stdout.write("\x1B[?25h");
4030
+ if (mountedAppContext.stdout.isTTY) mountedAppContext.stdout.write(ansiEscapes.exitAlternativeScreen);
4031
+ }
4032
+ if (mountedAsOwner && mountedStdout) {
4033
+ liveInstances.delete(mountedStdout);
4034
+ mountedAsOwner = false;
4035
+ }
4036
+ }
4037
+ const app = {
4038
+ mount(options = {}) {
4039
+ if (mountedRoot) return {};
4040
+ const stdout = options.stdout ?? process.stdout;
4041
+ const stdin = options.stdin ?? process.stdin;
4042
+ const stderr = options.stderr ?? process.stderr;
4043
+ const debug = options.debug ?? false;
4044
+ const frameSink = options[INTERNAL_FRAME_SINK];
4045
+ if (liveInstances.has(stdout)) {
4046
+ process.stderr.write("Warning: this stdout already has a live app, so this mount() was ignored. Unmount the existing app before mounting another one.\n");
4047
+ return {};
4048
+ }
4049
+ liveInstances.set(stdout, app);
4050
+ mountedAsOwner = true;
4051
+ mountedStdout = stdout;
4052
+ teardownStarted = false;
4053
+ const interactive = options.interactive ?? (!isInCi && Boolean(stdout.isTTY));
4054
+ const isScreenReaderEnabled = options.isScreenReaderEnabled ?? process.env["INK_SCREEN_READER"] === "true";
4055
+ const maxFps = options.maxFps ?? 30;
4056
+ const unthrottled = debug || isScreenReaderEnabled;
4057
+ const renderThrottleMs = !unthrottled && maxFps > 0 ? Math.max(1, Math.ceil(1e3 / maxFps)) : 0;
4058
+ const frameState = {
4059
+ lastOutput: "",
4060
+ lastOutputToRender: "",
4061
+ outputHeight: 0,
4062
+ fullStaticOutput: ""
4063
+ };
4064
+ mountedGetLastOutput = () => frameState.lastOutput;
4065
+ let cursorPosition;
4066
+ const writer = createFrameWriter(stdout, {
4067
+ debug,
4068
+ incremental: options.incrementalRendering
4069
+ });
4070
+ const synchronize = shouldSynchronize(stdout, interactive);
4071
+ function writeToStdout(data) {
4072
+ if (teardownStarted) return;
4073
+ if (debug || !interactive) {
4074
+ stdout.write(data);
4075
+ return;
4076
+ }
4077
+ if (synchronize) stdout.write(bsu);
4078
+ writer.clear();
4079
+ stdout.write(data);
4080
+ writer.write(frameState.lastOutputToRender || frameState.lastOutput + "\n");
4081
+ if (synchronize) stdout.write(esu);
4082
+ }
4083
+ function writeToStderr(data) {
4084
+ if (teardownStarted) return;
4085
+ if (debug || !interactive) {
4086
+ stderr.write(data);
4087
+ return;
4088
+ }
4089
+ if (synchronize) stdout.write(bsu);
4090
+ writer.clear();
4091
+ stderr.write(data);
4092
+ writer.write(frameState.lastOutputToRender || frameState.lastOutput + "\n");
4093
+ if (synchronize) stdout.write(esu);
4094
+ }
4095
+ const appContext = {
4096
+ exit(errorOrResult) {
4097
+ if (exitInitiated || teardownStarted) return;
4098
+ exitInitiated = true;
4099
+ if (isErrorInput(errorOrResult)) pendingExitError = errorOrResult;
4100
+ else pendingExitResult = errorOrResult;
4101
+ queueMicrotask(() => {
4102
+ teardown();
4103
+ resolveExit();
4104
+ });
4105
+ },
4106
+ waitUntilRenderFlush,
4107
+ stdout,
4108
+ stderr,
4109
+ stdin,
4110
+ debug,
4111
+ interactive,
4112
+ isScreenReaderEnabled,
4113
+ isRawModeSupported: !!stdin.isTTY,
4114
+ setRawMode(mode) {
4115
+ const raw = stdin;
4116
+ if (typeof raw.setRawMode === "function") raw.setRawMode(mode);
4117
+ },
4118
+ writeToStdout,
4119
+ writeToStderr,
4120
+ cursorPosition: void 0,
4121
+ setCursorPosition(pos) {
4122
+ cursorPosition = pos;
4123
+ appContext.cursorPosition = pos;
4124
+ writer.setCursorPosition(pos);
4125
+ }
4126
+ };
4127
+ mountedAppContext = appContext;
4128
+ const focusContext = createFocusController();
4129
+ const stdinController = createStdinController(stdin, {
4130
+ exitOnCtrlC: options.exitOnCtrlC ?? true,
4131
+ appCtx: appContext,
4132
+ focusContext
4133
+ });
4134
+ mountedStdinController = stdinController;
4135
+ if ((options.rawMode ?? "always") === "always" && interactive && stdinController.isRawModeSupported) stdinController.holdRawModeForLifetime();
4136
+ const kittyController = createKittyKeyboardController(stdin, stdout);
4137
+ mountedKittyController = kittyController;
4138
+ kittyController.init(options.kittyKeyboard, interactive);
4139
+ const tuiRoot = createRoot(appContext);
4140
+ attachYoga(tuiRoot);
4141
+ tuiRoot.yoga.setWidth(resolveSize(stdout).columns);
4142
+ mountedRoot = tuiRoot;
4143
+ const animationScheduler = createAnimationScheduler(renderThrottleMs);
4144
+ mountedAnimationScheduler = animationScheduler;
4145
+ const [caughtError, setCaughtError] = createSignal();
4146
+ const devConnected = isDevConnected();
4147
+ let userRoot = root;
4148
+ let userRootProps = rootProps;
4149
+ if (devConnected) {
4150
+ resetDevState();
4151
+ userRoot = createDevOverlayWrapper(userRoot, userRootProps ?? void 0);
4152
+ userRootProps = void 0;
4153
+ }
4154
+ const Root = () => catchError(() => createComponent$1(AppContextKey.Provider, {
4155
+ value: appContext,
4156
+ get children() {
4157
+ const tree = () => createComponent$1(FocusContextKey.Provider, {
4158
+ value: focusContext,
4159
+ get children() {
4160
+ return createComponent$1(StdinContextKey.Provider, {
4161
+ value: stdinController,
4162
+ get children() {
4163
+ return createComponent$1(AnimationSchedulerKey.Provider, {
4164
+ value: animationScheduler,
4165
+ get children() {
4166
+ const err = caughtError();
4167
+ if (err !== void 0) return createComponent$1(ErrorOverview, { error: err });
4168
+ return isComponent(userRoot) ? createComponent$1(userRoot, userRootProps ?? {}) : userRoot;
4169
+ }
4170
+ });
4171
+ }
4172
+ });
4173
+ }
4174
+ });
4175
+ if (!devConnected) return tree();
4176
+ return createComponent$1(DevStateKey.Provider, {
4177
+ value: devState.read,
4178
+ get children() {
4179
+ return tree();
4180
+ }
4181
+ });
4182
+ }
4183
+ }), (err) => {
4184
+ const error = isErrorInput(err) ? err : new Error(messageForNonError(err));
4185
+ pendingExitError ??= error;
4186
+ setCaughtError(err);
4187
+ queueMicrotask(() => appContext.exit(error));
4188
+ });
4189
+ function renderFrame(width) {
4190
+ if (!isScreenReaderEnabled) return paint(tuiRoot);
4191
+ return wrapAnsi(renderScreenReaderOutput(tuiRoot, { skipStaticElements: true }), width, {
4192
+ trim: false,
4193
+ hard: true
4194
+ });
4195
+ }
4196
+ function commit() {
4197
+ const start = options.onRender ? performance.now() : 0;
4198
+ const width = resolveSize(stdout).columns;
4199
+ tuiRoot.yoga.setWidth(width);
4200
+ const restoreLayoutGuards = calculateLayoutWithContentGuards(tuiRoot, width, void 0, Yoga.DIRECTION_LTR);
4201
+ try {
4202
+ emitLayoutListeners(tuiRoot);
4203
+ let staticOutput = "";
4204
+ for (const stat of findStatics(tuiRoot)) {
4205
+ const staticFrame = paintStaticNode(stat, width, isScreenReaderEnabled);
4206
+ if (staticFrame.length > 0) staticOutput += staticFrame + "\n";
4207
+ }
4208
+ if (staticOutput) frameState.fullStaticOutput += staticOutput;
4209
+ const frame = renderFrame(width);
4210
+ const outputHeight = frame === "" ? 0 : frame.split("\n").length;
4211
+ frameState.lastOutput = frame;
4212
+ frameState.outputHeight = outputHeight;
4213
+ frameState.lastOutputToRender = interactive && !isScreenReaderEnabled ? frame + "\n" : frame;
4214
+ if (options.onRender) options.onRender({ renderTime: performance.now() - start });
4215
+ if (debug) {
4216
+ if (frameState.fullStaticOutput) {
4217
+ stdout.write(frameState.fullStaticOutput);
4218
+ if (!teardownStarted) frameSink?.(frameState.fullStaticOutput);
4219
+ }
4220
+ stdout.write(frame);
4221
+ if (!teardownStarted) frameSink?.(frame);
4222
+ return;
4223
+ }
4224
+ if (!interactive) {
4225
+ if (staticOutput) stdout.write(staticOutput);
4226
+ return;
4227
+ }
4228
+ if (staticOutput) stdout.write(staticOutput);
4229
+ if (cursorPosition) writer.setCursorPosition(cursorPosition);
4230
+ const outputToRender = isScreenReaderEnabled ? frame : frame + "\n";
4231
+ writer.write(outputToRender);
4232
+ } finally {
4233
+ restoreLayoutGuards();
4234
+ }
4235
+ }
4236
+ const scheduler = createCommitScheduler(commit, {
4237
+ immediate: unthrottled,
4238
+ throttleMs: renderThrottleMs
4239
+ });
4240
+ mountedScheduler = scheduler;
4241
+ mountedRestoreRendererCommit = setRendererCommit(scheduler.schedule);
4242
+ if (options.alternateScreen && interactive && stdout.isTTY) {
4243
+ stdout.write(ansiEscapes.enterAlternativeScreen);
4244
+ stdout.write("\x1B[?25l");
4245
+ }
4246
+ if (options.patchConsole !== false && !debug) mountedRestoreConsole = patchConsoleFn((stream, data) => {
4247
+ if (stream === "stdout") appContext.writeToStdout(data);
4248
+ if (stream === "stderr") appContext.writeToStderr(data);
4249
+ });
4250
+ mountedClear = () => {
4251
+ if (!interactive || debug) return;
4252
+ writer.clear();
4253
+ };
4254
+ mountedDispose = renderSolidRoot(() => createComponent$1(Root, {}), tuiRoot);
4255
+ scheduler.schedule();
4256
+ if (devConnected) {
4257
+ mountedDevTeardown = () => teardown();
4258
+ registerDevApp(mountedDevTeardown);
4259
+ exitPromise.finally(() => notifyDevExit()).catch(() => {});
4260
+ }
4261
+ if (interactive) {
4262
+ const onResize = () => {
4263
+ scheduler.cancel();
4264
+ commit();
4265
+ };
4266
+ stdout.on("resize", onResize);
4267
+ mountedResizeHandler = onResize;
4268
+ }
4269
+ mountedExitListener = () => teardown();
4270
+ process.on("exit", mountedExitListener);
4271
+ if (interactive) mountedUnsubscribeExit = onExit(() => teardown(), { alwaysLast: false });
4272
+ return {};
4273
+ },
4274
+ unmount() {
4275
+ if (!mountedAppContext) {
4276
+ resolveExit();
4277
+ return;
4278
+ }
4279
+ if (!mountedAppContext.interactive && !mountedAppContext.debug) {
4280
+ const frame = mountedGetLastOutput?.() ?? "";
4281
+ if (frame) mountedAppContext.stdout.write(frame + "\n");
4282
+ }
4283
+ teardown();
4284
+ resolveExit();
4285
+ },
4286
+ waitUntilExit() {
4287
+ return exitPromise;
4288
+ },
4289
+ waitUntilRenderFlush,
4290
+ clear() {
4291
+ mountedClear?.();
4292
+ }
4293
+ };
4294
+ async function waitUntilRenderFlush() {
4295
+ await mountedScheduler?.flush();
4296
+ const stream = mountedAppContext?.stdout ?? process.stdout;
4297
+ const { canWriteToStdout, hasWritableState } = getWritableStreamState(stream);
4298
+ await new Promise((resolve) => {
4299
+ if (!canWriteToStdout || !hasWritableState) {
4300
+ setImmediate(resolve);
4301
+ return;
4302
+ }
4303
+ try {
4304
+ stream.write("", () => resolve());
4305
+ } catch {
4306
+ setImmediate(resolve);
4307
+ }
4308
+ });
4309
+ }
4310
+ return app;
4311
+ }
4312
+ function createFocusController() {
4313
+ const focusables = [];
4314
+ const subs = /* @__PURE__ */ new Map();
4315
+ let activeFocusable = null;
4316
+ let activeId = null;
4317
+ const [activeIdValue, setActiveIdValue] = createSignal(null);
4318
+ function notify(id, focused) {
4319
+ subs.get(id)?.forEach((fn) => fn(focused));
4320
+ }
4321
+ function setActiveFocusable(next) {
4322
+ if (next !== null && !focusables.includes(next)) next = null;
4323
+ if (activeFocusable === next) return;
4324
+ const prev = activeId;
4325
+ activeFocusable = next;
4326
+ activeId = next?.id ?? null;
4327
+ ctx.activeId = activeId;
4328
+ setActiveIdValue(activeId);
4329
+ if (prev) notify(prev, false);
4330
+ if (activeId) notify(activeId, true);
4331
+ }
4332
+ function findNextActive(startIdx, direction) {
4333
+ const len = focusables.length;
4334
+ for (let i = 0; i < len; i++) {
4335
+ const idx = (startIdx + direction * (i + 1) + len * len) % len;
4336
+ if (focusables[idx].isActive) return focusables[idx];
4337
+ }
4338
+ return null;
4339
+ }
4340
+ function startSearchIndex(direction) {
4341
+ if (activeFocusable) {
4342
+ const i = focusables.indexOf(activeFocusable);
4343
+ if (i >= 0) return i;
4344
+ }
4345
+ if (activeId) {
4346
+ const i = focusables.findIndex((f) => f.id === activeId);
4347
+ if (i >= 0) return i;
4348
+ }
4349
+ return direction === 1 ? -1 : focusables.length;
4350
+ }
4351
+ const ctx = {
4352
+ activeId: null,
4353
+ activeIdValue,
4354
+ enabled: true,
4355
+ enableFocus() {
4356
+ ctx.enabled = true;
4357
+ },
4358
+ disableFocus() {
4359
+ ctx.enabled = false;
4360
+ },
4361
+ focusNext() {
4362
+ if (focusables.length === 0) return;
4363
+ setActiveFocusable(findNextActive(startSearchIndex(1), 1));
4364
+ },
4365
+ focusPrevious() {
4366
+ if (focusables.length === 0) return;
4367
+ setActiveFocusable(findNextActive(startSearchIndex(-1), -1));
4368
+ },
4369
+ focus(id) {
4370
+ const entry = focusables.find((f) => f.id === id);
4371
+ if (entry) setActiveFocusable(entry);
4372
+ },
4373
+ blur() {
4374
+ setActiveFocusable(null);
4375
+ },
4376
+ add(id, options) {
4377
+ const entry = {
4378
+ id,
4379
+ isActive: true
4380
+ };
4381
+ focusables.push(entry);
4382
+ if (options.autoFocus && activeFocusable == null) setActiveFocusable(entry);
4383
+ },
4384
+ remove(id) {
4385
+ const removingActive = activeFocusable?.id === id;
4386
+ for (let i = focusables.length - 1; i >= 0; i--) if (focusables[i].id === id) focusables.splice(i, 1);
4387
+ if (removingActive) setActiveFocusable(null);
4388
+ },
4389
+ activate(id) {
4390
+ for (const entry of focusables) if (entry.id === id) entry.isActive = true;
4391
+ },
4392
+ deactivate(id) {
4393
+ let changed = false;
4394
+ for (const entry of focusables) if (entry.id === id) {
4395
+ entry.isActive = false;
4396
+ changed = true;
4397
+ }
4398
+ if (changed && activeFocusable?.id === id) setActiveFocusable(null);
4399
+ },
4400
+ subscribe(id, fn) {
4401
+ let set = subs.get(id);
4402
+ if (!set) {
4403
+ set = /* @__PURE__ */ new Set();
4404
+ subs.set(id, set);
4405
+ }
4406
+ set.add(fn);
4407
+ return () => {
4408
+ set.delete(fn);
4409
+ if (set.size === 0 && subs.get(id) === set) subs.delete(id);
4410
+ };
4411
+ }
4412
+ };
4413
+ return Object.assign(ctx, { __subscriberMapSize: () => subs.size });
4414
+ }
4415
+ function createStdinController(stdin, opts) {
4416
+ const { appCtx, focusContext } = opts;
4417
+ const emitter = new EventEmitter();
4418
+ emitter.setMaxListeners(Infinity);
4419
+ const inputParser = createInputParser();
4420
+ let refs = 0;
4421
+ let bracketedPasteModeCount = 0;
4422
+ let pendingFlushTimer;
4423
+ const FLUSH_DELAY = 20;
4424
+ function clearPendingFlush() {
4425
+ if (pendingFlushTimer !== void 0) {
4426
+ clearTimeout(pendingFlushTimer);
4427
+ pendingFlushTimer = void 0;
4428
+ }
4429
+ }
4430
+ function emitInput(input) {
4431
+ if (opts.exitOnCtrlC) {
4432
+ if (input === "") {
4433
+ appCtx.exit();
4434
+ return;
4435
+ }
4436
+ if (input.charCodeAt(0) === 27) {
4437
+ const key = parseKeypress(input);
4438
+ if (key.name === "c" && key.ctrl && !key.shift && key.eventType !== "release") {
4439
+ appCtx.exit();
4440
+ return;
4441
+ }
4442
+ }
4443
+ }
4444
+ if (input === "\x1B" && focusContext.enabled) focusContext.blur();
4445
+ emitter.emit("input", input);
4446
+ }
4447
+ function schedulePendingFlush() {
4448
+ clearPendingFlush();
4449
+ pendingFlushTimer = setTimeout(() => {
4450
+ pendingFlushTimer = void 0;
4451
+ const pending = inputParser.flushPendingEscape();
4452
+ if (pending) emitInput(pending);
4453
+ }, FLUSH_DELAY);
4454
+ }
4455
+ function handleData(chunk) {
4456
+ clearPendingFlush();
4457
+ const data = typeof chunk === "string" ? chunk : chunk.toString();
4458
+ const events = inputParser.push(data);
4459
+ for (const event of events) if (typeof event === "string") emitInput(event);
4460
+ else if (emitter.listenerCount("paste") > 0) emitter.emit("paste", event.paste);
4461
+ else emitInput(event.paste);
4462
+ if (inputParser.hasPendingEscape()) schedulePendingFlush();
4463
+ }
4464
+ const focusInputListener = (data) => {
4465
+ if (!focusContext.enabled) return;
4466
+ if (data === " ") focusContext.focusNext();
4467
+ else if (data === "\x1B[Z") focusContext.focusPrevious();
4468
+ };
4469
+ emitter.on("input", focusInputListener);
4470
+ const controller = {
4471
+ stdin,
4472
+ isRawModeSupported: appCtx.isRawModeSupported,
4473
+ internal_eventEmitter: emitter,
4474
+ internal_exitOnCtrlC: opts.exitOnCtrlC,
4475
+ setRawMode(mode) {
4476
+ if (!appCtx.isRawModeSupported) throw new Error("Raw mode is not supported on the configured stdin stream.");
4477
+ if (mode) controller.acquireRawMode();
4478
+ else controller.releaseRawMode();
4479
+ },
4480
+ acquireRawMode() {
4481
+ if (!appCtx.isRawModeSupported) throw new Error("Raw mode is not supported on the configured stdin stream.");
4482
+ if (refs === 0) {
4483
+ appCtx.setRawMode(true);
4484
+ if (typeof stdin.ref === "function") stdin.ref();
4485
+ if (typeof stdin.setEncoding === "function") stdin.setEncoding("utf8");
4486
+ stdin.on("data", handleData);
4487
+ }
4488
+ refs++;
4489
+ },
4490
+ releaseRawMode() {
4491
+ if (!appCtx.isRawModeSupported || refs === 0) return;
4492
+ refs--;
4493
+ if (refs === 0) {
4494
+ inputParser.reset();
4495
+ clearPendingFlush();
4496
+ stdin.off("data", handleData);
4497
+ appCtx.setRawMode(false);
4498
+ if (typeof stdin.unref === "function") stdin.unref();
4499
+ }
4500
+ },
4501
+ holdRawModeForLifetime() {
4502
+ controller.acquireRawMode();
4503
+ },
4504
+ setBracketedPasteMode(enabled) {
4505
+ if (enabled) {
4506
+ if (bracketedPasteModeCount === 0 && appCtx.stdout.isTTY) appCtx.stdout.write("\x1B[?2004h");
4507
+ bracketedPasteModeCount++;
4508
+ } else {
4509
+ if (bracketedPasteModeCount === 0) return;
4510
+ bracketedPasteModeCount--;
4511
+ if (bracketedPasteModeCount === 0 && appCtx.stdout.isTTY) appCtx.stdout.write("\x1B[?2004l");
4512
+ }
4513
+ },
4514
+ dispose() {
4515
+ clearPendingFlush();
4516
+ stdin.off("data", handleData);
4517
+ emitter.off("input", focusInputListener);
4518
+ if (bracketedPasteModeCount > 0 && appCtx.stdout.isTTY) appCtx.stdout.write("\x1B[?2004l");
4519
+ bracketedPasteModeCount = 0;
4520
+ if (refs > 0 && appCtx.isRawModeSupported) {
4521
+ refs = 0;
4522
+ appCtx.setRawMode(false);
4523
+ if (typeof stdin.unref === "function") stdin.unref();
4524
+ }
4525
+ }
4526
+ };
4527
+ return controller;
4528
+ }
4529
+ //#endregion
4530
+ //#region src/render-to-string.ts
4531
+ function renderToString(component, options) {
4532
+ return renderToStringInternal(component, options);
4533
+ }
4534
+ function renderToStringWithScreenReader(component, options) {
4535
+ return renderToStringInternal(component, options);
4536
+ }
4537
+ function renderToStringInternal(component, options) {
4538
+ const columns = options?.columns ?? 80;
4539
+ const isScreenReaderEnabled = options?.isScreenReaderEnabled ?? false;
4540
+ const appContext = createNoOpAppContext(isScreenReaderEnabled);
4541
+ const root = createRoot(appContext);
4542
+ attachYoga(root);
4543
+ root.yoga.setWidth(columns);
4544
+ let capturedStaticOutput = "";
4545
+ let output = "";
4546
+ let errored = false;
4547
+ let caught;
4548
+ const restoreCommit = setRendererCommit(() => {
4549
+ const restoreLayoutGuards = calculateLayoutWithContentGuards(root, columns, void 0, Yoga.DIRECTION_LTR);
4550
+ try {
4551
+ for (const stat of findStatics(root)) {
4552
+ const staticFrame = paintStaticNode(stat, columns, isScreenReaderEnabled);
4553
+ if (staticFrame && staticFrame !== "\n") capturedStaticOutput += staticFrame + "\n";
4554
+ }
4555
+ output = isScreenReaderEnabled ? renderScreenReaderOutput(root, { skipStaticElements: true }) : paint(root);
4556
+ } finally {
4557
+ restoreLayoutGuards();
4558
+ }
4559
+ });
4560
+ let dispose;
4561
+ try {
4562
+ const Root = () => catchError(() => createComponent$1(AppContextKey.Provider, {
4563
+ value: appContext,
4564
+ get children() {
4565
+ return createComponent$1(FocusContextKey.Provider, {
4566
+ value: createNoOpFocusContext(),
4567
+ get children() {
4568
+ return createComponent$1(StdinContextKey.Provider, {
4569
+ value: createNoOpStdinContext(),
4570
+ get children() {
4571
+ return createComponent$1(AnimationSchedulerKey.Provider, {
4572
+ value: createNoOpAnimationScheduler(),
4573
+ get children() {
4574
+ return createComponent$1(component, {});
4575
+ }
4576
+ });
4577
+ }
4578
+ });
4579
+ }
4580
+ });
4581
+ }
4582
+ }), (err) => {
4583
+ errored = true;
4584
+ caught = err;
4585
+ });
4586
+ dispose = renderSolidRoot(() => createComponent$1(Root, {}), root);
4587
+ const restoreLayoutGuards = calculateLayoutWithContentGuards(root, columns, void 0, Yoga.DIRECTION_LTR);
4588
+ try {
4589
+ for (const stat of findStatics(root)) {
4590
+ const staticFrame = paintStaticNode(stat, columns, isScreenReaderEnabled);
4591
+ if (staticFrame && staticFrame !== "\n") capturedStaticOutput += staticFrame + "\n";
4592
+ }
4593
+ output = isScreenReaderEnabled ? renderScreenReaderOutput(root, { skipStaticElements: true }) : paint(root);
4594
+ } finally {
4595
+ restoreLayoutGuards();
4596
+ }
4597
+ dispose();
4598
+ dispose = void 0;
4599
+ root.yoga.freeRecursive();
4600
+ if (errored) throw isErrorInput(caught) ? caught : new Error(messageForNonError(caught));
4601
+ const normalizedStaticOutput = capturedStaticOutput.endsWith("\n") ? capturedStaticOutput.slice(0, -1) : capturedStaticOutput;
4602
+ if (normalizedStaticOutput && output) return normalizedStaticOutput + "\n" + output;
4603
+ return normalizedStaticOutput || output;
4604
+ } finally {
4605
+ restoreCommit();
4606
+ try {
4607
+ dispose?.();
4608
+ } catch {}
4609
+ }
4610
+ }
4611
+ function createNoOpAppContext(isScreenReaderEnabled = false) {
4612
+ return {
4613
+ exit: () => {},
4614
+ waitUntilRenderFlush: async () => {},
4615
+ stdout: process.stdout,
4616
+ stderr: process.stderr,
4617
+ stdin: process.stdin,
4618
+ debug: false,
4619
+ interactive: false,
4620
+ isScreenReaderEnabled,
4621
+ isRawModeSupported: false,
4622
+ setRawMode: () => {},
4623
+ writeToStdout: () => {},
4624
+ writeToStderr: () => {},
4625
+ cursorPosition: void 0,
4626
+ setCursorPosition: () => {}
4627
+ };
4628
+ }
4629
+ function createNoOpFocusContext() {
4630
+ const [activeIdValue] = createSignal(null);
4631
+ return {
4632
+ activeId: null,
4633
+ activeIdValue,
4634
+ enabled: false,
4635
+ enableFocus: () => {},
4636
+ disableFocus: () => {},
4637
+ focusNext: () => {},
4638
+ focusPrevious: () => {},
4639
+ focus: () => {},
4640
+ blur: () => {},
4641
+ add: () => {},
4642
+ remove: () => {},
4643
+ activate: () => {},
4644
+ deactivate: () => {},
4645
+ subscribe: () => () => {}
4646
+ };
4647
+ }
4648
+ function createNoOpStdinContext() {
4649
+ return {
4650
+ stdin: process.stdin,
4651
+ setRawMode: () => {},
4652
+ isRawModeSupported: false,
4653
+ internal_eventEmitter: new EventEmitter(),
4654
+ internal_exitOnCtrlC: false,
4655
+ acquireRawMode: () => {},
4656
+ releaseRawMode: () => {},
4657
+ setBracketedPasteMode: () => {}
4658
+ };
4659
+ }
4660
+ //#endregion
4661
+ export { createMemo as $, createElement as A, attachYoga as B, hasCompleteKittyQueryResponse as C, resolveFlags as D, matchKittyQueryResponse as E, memo as F, createText as G, addLayoutListener as H, mergeProps as I, catchError as J, createTextLeaf as K, setProp as L, effect as M, insert as N, stripKittyQueryResponsesAndTrailingPartial as O, insertNode as P, createEffect as Q, spread as R, createKittyKeyboardController as S, kittyModifiers as T, createBox as U, yogaNodeTracker as V, createRoot as W, createComponent$1 as X, children as Y, createContext as Z, renderScreenReaderOutput as _, connectDevtools as a, splitProps as at, nonAlphanumericKeys as b, messageForNonError as c, AnimationSchedulerKey as d, createRenderEffect as et, AppContextKey as f, INTERNAL_FRAME_SINK as g, TextContextKey as h, createFocusController as i, onCleanup as it, createTextNode as j, createComponent as k, Text as l, StdinContextKey as m, renderToStringWithScreenReader as n, createSignal as nt, isDevConnected as o, untrack as ot, FocusContextKey as p, batch as q, createApp as r, on as rt, useWindowSize as s, useContext as st, renderToString as t, createRoot$1 as tt, Box as u, createAnimationScheduler as v, kittyFlags as w, parseKeypress as x, normalizeInterval as y, use as z };