@nick-skriabin/glyph 0.1.38 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs DELETED
@@ -1,4845 +0,0 @@
1
- 'use strict';
2
-
3
- var React15 = require('react');
4
- var ReactReconciler = require('react-reconciler');
5
- var stringWidth = require('string-width');
6
- var Yoga = require('yoga-layout');
7
-
8
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
-
10
- var React15__default = /*#__PURE__*/_interopDefault(React15);
11
- var ReactReconciler__default = /*#__PURE__*/_interopDefault(ReactReconciler);
12
- var stringWidth__default = /*#__PURE__*/_interopDefault(stringWidth);
13
- var Yoga__default = /*#__PURE__*/_interopDefault(Yoga);
14
-
15
- // src/render.ts
16
-
17
- // src/reconciler/nodes.ts
18
- var nextFocusId = 0;
19
- function generateFocusId() {
20
- return `glyph-focus-${nextFocusId++}`;
21
- }
22
- function createGlyphNode(type, props) {
23
- const style = props.style ?? {};
24
- return {
25
- type,
26
- props,
27
- style,
28
- children: [],
29
- rawTextChildren: [],
30
- parent: null,
31
- yogaNode: null,
32
- text: null,
33
- layout: { x: 0, y: 0, width: 0, height: 0, innerX: 0, innerY: 0, innerWidth: 0, innerHeight: 0 },
34
- focusId: type === "input" ? generateFocusId() : props.focusable ? generateFocusId() : null,
35
- hidden: false
36
- };
37
- }
38
- function appendChild(parent, child) {
39
- child.parent = parent;
40
- parent.children.push(child);
41
- }
42
- function removeChild(parent, child) {
43
- const idx = parent.children.indexOf(child);
44
- if (idx !== -1) {
45
- parent.children.splice(idx, 1);
46
- child.parent = null;
47
- }
48
- }
49
- function insertBefore(parent, child, beforeChild) {
50
- child.parent = parent;
51
- const idx = parent.children.indexOf(beforeChild);
52
- if (idx !== -1) {
53
- parent.children.splice(idx, 0, child);
54
- } else {
55
- parent.children.push(child);
56
- }
57
- }
58
- function getInheritedTextStyle(node) {
59
- const result = {};
60
- let current = node;
61
- while (current) {
62
- const s = current.style;
63
- if (result.color === void 0 && s.color !== void 0) result.color = s.color;
64
- if (result.bg === void 0 && s.bg !== void 0) result.bg = s.bg;
65
- if (result.bold === void 0 && s.bold !== void 0) result.bold = s.bold;
66
- if (result.dim === void 0 && s.dim !== void 0) result.dim = s.dim;
67
- if (result.italic === void 0 && s.italic !== void 0) result.italic = s.italic;
68
- if (result.underline === void 0 && s.underline !== void 0) result.underline = s.underline;
69
- current = current.parent;
70
- }
71
- return result;
72
- }
73
- function collectTextContent(node) {
74
- if (node.text != null) return node.text;
75
- let result = "";
76
- for (const child of node.children) {
77
- result += collectTextContent(child);
78
- }
79
- return result;
80
- }
81
-
82
- // src/reconciler/hostConfig.ts
83
- var DefaultEventPriority = 32;
84
- var hostConfig = {
85
- supportsMutation: true,
86
- supportsPersistence: false,
87
- supportsHydration: false,
88
- isPrimaryRenderer: true,
89
- // Timeouts
90
- scheduleTimeout: setTimeout,
91
- cancelTimeout: clearTimeout,
92
- noTimeout: -1,
93
- supportsMicrotasks: true,
94
- scheduleMicrotask: queueMicrotask,
95
- // Priority & event methods required by react-reconciler v0.31
96
- getCurrentUpdatePriority: () => DefaultEventPriority,
97
- setCurrentUpdatePriority: (_priority) => {
98
- },
99
- resolveUpdatePriority: () => DefaultEventPriority,
100
- getCurrentEventPriority: () => DefaultEventPriority,
101
- resolveEventType: () => null,
102
- resolveEventTimeStamp: () => -1.1,
103
- shouldAttemptEagerTransition: () => false,
104
- getInstanceFromNode: () => null,
105
- beforeActiveInstanceBlur: () => {
106
- },
107
- afterActiveInstanceBlur: () => {
108
- },
109
- prepareScopeUpdate: () => {
110
- },
111
- getInstanceFromScope: () => null,
112
- detachDeletedInstance: () => {
113
- },
114
- requestPostPaintCallback: (_callback) => {
115
- },
116
- // Commit suspension stubs (required by react-reconciler v0.31)
117
- maySuspendCommit: (_type, _props) => false,
118
- preloadInstance: (_type, _props) => true,
119
- startSuspendingCommit: () => {
120
- },
121
- suspendInstance: (_type, _props) => {
122
- },
123
- waitForCommitToBeReady: () => null,
124
- // Transition stubs
125
- NotPendingTransition: null,
126
- HostTransitionContext: { $$typeof: /* @__PURE__ */ Symbol.for("react.context"), _currentValue: null },
127
- resetFormInstance: (_instance) => {
128
- },
129
- // Console binding
130
- bindToConsole: (methodName, args, _errorPrefix) => {
131
- return Function.prototype.bind.call(
132
- console[methodName],
133
- console,
134
- ...args
135
- );
136
- },
137
- // Resource/singleton stubs
138
- supportsResources: false,
139
- isHostHoistableType: (_type, _props) => false,
140
- supportsSingletons: false,
141
- isHostSingletonType: (_type) => false,
142
- supportsTestSelectors: false,
143
- createInstance(type, props, _rootContainer, _hostContext, _internalHandle) {
144
- return createGlyphNode(type, props);
145
- },
146
- createTextInstance(text, _rootContainer, _hostContext, _internalHandle) {
147
- return { type: "raw-text", text, parent: null };
148
- },
149
- appendInitialChild(parentInstance, child) {
150
- if (child.type === "raw-text") {
151
- const textChild = child;
152
- textChild.parent = parentInstance;
153
- parentInstance.rawTextChildren.push(textChild);
154
- parentInstance.text = parentInstance.rawTextChildren.map((t) => t.text).join("");
155
- } else {
156
- appendChild(parentInstance, child);
157
- }
158
- },
159
- finalizeInitialChildren(_instance, _type, _props, _rootContainer, _hostContext) {
160
- return false;
161
- },
162
- shouldSetTextContent(_type, _props) {
163
- return false;
164
- },
165
- getRootHostContext(_rootContainer) {
166
- return {};
167
- },
168
- getChildHostContext(parentHostContext, _type, _rootContainer) {
169
- return parentHostContext;
170
- },
171
- getPublicInstance(instance) {
172
- return instance;
173
- },
174
- prepareForCommit(_containerInfo) {
175
- return null;
176
- },
177
- resetAfterCommit(containerInfo) {
178
- containerInfo.onCommit();
179
- },
180
- preparePortalMount() {
181
- },
182
- // Mutation methods
183
- appendChild(parentInstance, child) {
184
- if (child.type === "raw-text") {
185
- const textChild = child;
186
- textChild.parent = parentInstance;
187
- parentInstance.rawTextChildren.push(textChild);
188
- parentInstance.text = parentInstance.rawTextChildren.map((t) => t.text).join("");
189
- } else {
190
- appendChild(parentInstance, child);
191
- }
192
- },
193
- appendChildToContainer(container, child) {
194
- if (child.type === "raw-text") return;
195
- const node = child;
196
- node.parent = null;
197
- container.children.push(node);
198
- },
199
- insertBefore(parentInstance, child, beforeChild) {
200
- if (child.type === "raw-text" || beforeChild.type === "raw-text") return;
201
- insertBefore(parentInstance, child, beforeChild);
202
- },
203
- insertInContainerBefore(container, child, beforeChild) {
204
- if (child.type === "raw-text" || beforeChild.type === "raw-text") return;
205
- const node = child;
206
- const before = beforeChild;
207
- const idx = container.children.indexOf(before);
208
- if (idx !== -1) {
209
- container.children.splice(idx, 0, node);
210
- } else {
211
- container.children.push(node);
212
- }
213
- },
214
- removeChild(parentInstance, child) {
215
- if (child.type === "raw-text") {
216
- const textChild = child;
217
- textChild.parent = null;
218
- const idx = parentInstance.rawTextChildren.indexOf(textChild);
219
- if (idx !== -1) parentInstance.rawTextChildren.splice(idx, 1);
220
- parentInstance.text = parentInstance.rawTextChildren.map((t) => t.text).join("") || null;
221
- return;
222
- }
223
- removeChild(parentInstance, child);
224
- },
225
- removeChildFromContainer(container, child) {
226
- if (child.type === "raw-text") return;
227
- const node = child;
228
- const idx = container.children.indexOf(node);
229
- if (idx !== -1) {
230
- container.children.splice(idx, 1);
231
- }
232
- },
233
- commitTextUpdate(textInstance, _oldText, newText) {
234
- textInstance.text = newText;
235
- if (textInstance.parent) {
236
- textInstance.parent.text = textInstance.parent.rawTextChildren.map((t) => t.text).join("");
237
- }
238
- },
239
- // v0.31 signature: (instance, type, oldProps, newProps, internalHandle)
240
- // updatePayload was removed in this version
241
- commitUpdate(instance, _type, _oldProps, newProps, _internalHandle) {
242
- instance.props = newProps;
243
- instance.style = newProps.style ?? {};
244
- if (newProps.focusable && !instance.focusId) {
245
- instance.focusId = `focus-${Math.random().toString(36).slice(2, 9)}`;
246
- }
247
- },
248
- hideInstance(instance) {
249
- instance.hidden = true;
250
- },
251
- hideTextInstance(textInstance) {
252
- textInstance.text = "";
253
- },
254
- unhideInstance(instance, _props) {
255
- instance.hidden = false;
256
- },
257
- unhideTextInstance(textInstance, text) {
258
- textInstance.text = text;
259
- },
260
- clearContainer(container) {
261
- container.children.length = 0;
262
- },
263
- resetTextContent(instance) {
264
- instance.text = null;
265
- }
266
- };
267
-
268
- // src/reconciler/reconciler.ts
269
- var reconciler = ReactReconciler__default.default(hostConfig);
270
- reconciler.injectIntoDevTools({
271
- bundleType: process.env.NODE_ENV === "production" ? 0 : 1,
272
- version: "0.1.0",
273
- rendererPackageName: "glyph"
274
- });
275
-
276
- // src/runtime/terminal.ts
277
- var ESC = "\x1B";
278
- var CSI = `${ESC}[`;
279
- var Terminal = class {
280
- stdout;
281
- stdin;
282
- wasRaw = false;
283
- cleanedUp = false;
284
- // Data handler dispatch - single stdin listener, filters OSC, dispatches clean data
285
- dataHandlers = /* @__PURE__ */ new Set();
286
- stdinAttached = false;
287
- // OSC response filtering state
288
- oscState = "normal";
289
- oscAccum = "";
290
- escFlushTimer = null;
291
- // Terminal palette (populated by queryPalette)
292
- palette = /* @__PURE__ */ new Map();
293
- paletteResolve = null;
294
- constructor(stdout = process.stdout, stdin = process.stdin) {
295
- this.stdout = stdout;
296
- this.stdin = stdin;
297
- }
298
- get columns() {
299
- return this.stdout.columns || 80;
300
- }
301
- get rows() {
302
- return this.stdout.rows || 24;
303
- }
304
- enterRawMode() {
305
- if (this.stdin.isTTY) {
306
- this.wasRaw = this.stdin.isRaw;
307
- this.stdin.setRawMode(true);
308
- this.stdin.resume();
309
- this.stdin.setEncoding("utf-8");
310
- }
311
- }
312
- exitRawMode() {
313
- if (this.stdin.isTTY && !this.wasRaw) {
314
- this.stdin.setRawMode(false);
315
- this.stdin.pause();
316
- }
317
- }
318
- write(data) {
319
- this.stdout.write(data);
320
- }
321
- hideCursor() {
322
- this.write(`${CSI}?25l`);
323
- }
324
- showCursor() {
325
- this.write(`${CSI}?25h`);
326
- }
327
- /** Move cursor to (x, y) position (0-indexed) */
328
- moveCursor(x, y) {
329
- this.write(`${CSI}${y + 1};${x + 1}H`);
330
- }
331
- /** Set cursor color using OSC 12 */
332
- setCursorColor(color) {
333
- this.write(`${ESC}]12;${color}\x07`);
334
- }
335
- /** Reset cursor color to terminal default */
336
- resetCursorColor() {
337
- this.write(`${ESC}]112\x07`);
338
- }
339
- enterAltScreen() {
340
- this.write(`${CSI}?1049h`);
341
- }
342
- exitAltScreen() {
343
- this.write(`${CSI}?1049l`);
344
- }
345
- clearScreen() {
346
- this.write(`${CSI}2J${CSI}H`);
347
- }
348
- resetStyles() {
349
- this.write(`${CSI}0m`);
350
- }
351
- /** Enable kitty keyboard protocol for enhanced key detection */
352
- enableKittyKeyboard() {
353
- this.write(`${CSI}>1u`);
354
- }
355
- /** Disable kitty keyboard protocol */
356
- disableKittyKeyboard() {
357
- this.write(`${CSI}<u`);
358
- }
359
- setup() {
360
- this.enterRawMode();
361
- this.enterAltScreen();
362
- this.enableKittyKeyboard();
363
- this.hideCursor();
364
- this.clearScreen();
365
- this.attachStdinListener();
366
- this.installCleanupHandlers();
367
- }
368
- cleanup() {
369
- if (this.cleanedUp) return;
370
- this.cleanedUp = true;
371
- if (this.escFlushTimer !== null) {
372
- clearTimeout(this.escFlushTimer);
373
- this.escFlushTimer = null;
374
- }
375
- this.resetStyles();
376
- this.resetCursorColor();
377
- this.disableKittyKeyboard();
378
- this.showCursor();
379
- this.exitAltScreen();
380
- this.exitRawMode();
381
- }
382
- /** Restore terminal state for background suspension (does NOT mark as cleaned up). */
383
- suspend() {
384
- if (this.escFlushTimer !== null) {
385
- clearTimeout(this.escFlushTimer);
386
- this.escFlushTimer = null;
387
- }
388
- this.oscState = "normal";
389
- this.oscAccum = "";
390
- this.resetStyles();
391
- this.resetCursorColor();
392
- this.disableKittyKeyboard();
393
- this.showCursor();
394
- this.exitAltScreen();
395
- this.exitRawMode();
396
- }
397
- /** Re-enter raw mode and alt screen after SIGCONT resume. */
398
- resume() {
399
- this.enterRawMode();
400
- this.enterAltScreen();
401
- this.enableKittyKeyboard();
402
- this.hideCursor();
403
- this.clearScreen();
404
- }
405
- // ---- Data handling with OSC filtering ----
406
- attachStdinListener() {
407
- if (this.stdinAttached) return;
408
- this.stdinAttached = true;
409
- this.stdin.on("data", (data) => {
410
- let str = typeof data === "string" ? data : data.toString("utf-8");
411
- this.dispatchFiltered(str);
412
- });
413
- }
414
- onData(handler) {
415
- this.dataHandlers.add(handler);
416
- return () => {
417
- this.dataHandlers.delete(handler);
418
- };
419
- }
420
- // ---- OSC response filtering ----
421
- dispatchFiltered(raw) {
422
- if (this.escFlushTimer !== null) {
423
- clearTimeout(this.escFlushTimer);
424
- this.escFlushTimer = null;
425
- }
426
- const clean = this.filterOsc(raw);
427
- if (clean.length > 0) {
428
- for (const handler of this.dataHandlers) {
429
- handler(clean);
430
- }
431
- }
432
- if (this.oscState === "esc") {
433
- this.escFlushTimer = setTimeout(() => {
434
- this.escFlushTimer = null;
435
- this.oscState = "normal";
436
- for (const handler of this.dataHandlers) {
437
- handler("\x1B");
438
- }
439
- }, 50);
440
- }
441
- }
442
- filterOsc(raw) {
443
- let clean = "";
444
- for (let i = 0; i < raw.length; i++) {
445
- const ch = raw[i];
446
- const code = raw.charCodeAt(i);
447
- switch (this.oscState) {
448
- case "normal":
449
- if (code === 27) {
450
- this.oscState = "esc";
451
- } else {
452
- clean += ch;
453
- }
454
- break;
455
- case "esc":
456
- if (ch === "]") {
457
- this.oscState = "osc";
458
- this.oscAccum = "";
459
- } else {
460
- clean += "\x1B" + ch;
461
- this.oscState = "normal";
462
- }
463
- break;
464
- case "osc":
465
- if (code === 7) {
466
- this.handleOscResponse(this.oscAccum);
467
- this.oscAccum = "";
468
- this.oscState = "normal";
469
- } else if (code === 27) {
470
- this.oscState = "osc_esc";
471
- } else {
472
- this.oscAccum += ch;
473
- }
474
- break;
475
- case "osc_esc":
476
- if (ch === "\\") {
477
- this.handleOscResponse(this.oscAccum);
478
- this.oscAccum = "";
479
- this.oscState = "normal";
480
- } else {
481
- this.oscAccum += "\x1B" + ch;
482
- this.oscState = "osc";
483
- }
484
- break;
485
- }
486
- }
487
- return clean;
488
- }
489
- handleOscResponse(data) {
490
- const match = data.match(
491
- /^4;(\d+);rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/
492
- );
493
- if (match) {
494
- const index = parseInt(match[1], 10);
495
- const r = parseInt(match[2].substring(0, 2), 16);
496
- const g = parseInt(match[3].substring(0, 2), 16);
497
- const b = parseInt(match[4].substring(0, 2), 16);
498
- this.palette.set(index, [r, g, b]);
499
- if (this.palette.size >= 16 && this.paletteResolve) {
500
- this.paletteResolve();
501
- this.paletteResolve = null;
502
- }
503
- }
504
- }
505
- // ---- Palette querying ----
506
- queryPalette() {
507
- return new Promise((resolve) => {
508
- const done = () => resolve(this.palette);
509
- const timeout = setTimeout(done, 200);
510
- this.paletteResolve = () => {
511
- clearTimeout(timeout);
512
- done();
513
- };
514
- let query = "";
515
- for (let i = 0; i < 16; i++) {
516
- query += `\x1B]4;${i};?\x07`;
517
- }
518
- this.write(query);
519
- });
520
- }
521
- // ---- Event handling ----
522
- onResize(handler) {
523
- this.stdout.on("resize", handler);
524
- return () => {
525
- this.stdout.off("resize", handler);
526
- };
527
- }
528
- installCleanupHandlers() {
529
- const doCleanup = () => this.cleanup();
530
- process.on("exit", doCleanup);
531
- const handleSignal = (signal) => {
532
- doCleanup();
533
- process.kill(process.pid, signal);
534
- };
535
- process.once("SIGINT", () => handleSignal("SIGINT"));
536
- process.once("SIGTERM", () => handleSignal("SIGTERM"));
537
- process.on("uncaughtException", (err) => {
538
- doCleanup();
539
- console.error(err);
540
- process.exit(1);
541
- });
542
- process.on("unhandledRejection", (err) => {
543
- doCleanup();
544
- console.error(err);
545
- process.exit(1);
546
- });
547
- }
548
- };
549
-
550
- // src/runtime/input.ts
551
- function getKeyNameFromCode(code) {
552
- switch (code) {
553
- // Standard ASCII
554
- case 9:
555
- return "tab";
556
- case 13:
557
- return "return";
558
- case 27:
559
- return "escape";
560
- case 32:
561
- return " ";
562
- case 127:
563
- return "backspace";
564
- // Kitty protocol special keys
565
- case 57358:
566
- return "capslock";
567
- case 57359:
568
- return "scrolllock";
569
- case 57360:
570
- return "numlock";
571
- case 57361:
572
- return "printscreen";
573
- case 57362:
574
- return "pause";
575
- case 57363:
576
- return "menu";
577
- // Function keys (kitty uses these codes)
578
- case 57364:
579
- return "f13";
580
- case 57365:
581
- return "f14";
582
- case 57366:
583
- return "f15";
584
- case 57367:
585
- return "f16";
586
- case 57368:
587
- return "f17";
588
- case 57369:
589
- return "f18";
590
- case 57370:
591
- return "f19";
592
- case 57371:
593
- return "f20";
594
- case 57372:
595
- return "f21";
596
- case 57373:
597
- return "f22";
598
- case 57374:
599
- return "f23";
600
- case 57375:
601
- return "f24";
602
- case 57376:
603
- return "f25";
604
- // Keypad keys
605
- case 57399:
606
- return "kp0";
607
- case 57400:
608
- return "kp1";
609
- case 57401:
610
- return "kp2";
611
- case 57402:
612
- return "kp3";
613
- case 57403:
614
- return "kp4";
615
- case 57404:
616
- return "kp5";
617
- case 57405:
618
- return "kp6";
619
- case 57406:
620
- return "kp7";
621
- case 57407:
622
- return "kp8";
623
- case 57408:
624
- return "kp9";
625
- case 57409:
626
- return "kpdecimal";
627
- case 57410:
628
- return "kpdivide";
629
- case 57411:
630
- return "kpmultiply";
631
- case 57412:
632
- return "kpminus";
633
- case 57413:
634
- return "kpplus";
635
- case 57414:
636
- return "kpenter";
637
- case 57415:
638
- return "kpequal";
639
- // Navigation (kitty protocol)
640
- case 57416:
641
- return "kpleft";
642
- case 57417:
643
- return "kpright";
644
- case 57418:
645
- return "kpup";
646
- case 57419:
647
- return "kpdown";
648
- case 57420:
649
- return "kppageup";
650
- case 57421:
651
- return "kppagedown";
652
- case 57422:
653
- return "kphome";
654
- case 57423:
655
- return "kpend";
656
- case 57424:
657
- return "kpinsert";
658
- case 57425:
659
- return "kpdelete";
660
- // Media keys
661
- case 57428:
662
- return "mediaplaypause";
663
- case 57429:
664
- return "mediastop";
665
- case 57430:
666
- return "mediaprev";
667
- case 57431:
668
- return "medianext";
669
- case 57432:
670
- return "mediarewind";
671
- case 57433:
672
- return "mediafastforward";
673
- case 57434:
674
- return "mediamute";
675
- case 57435:
676
- return "volumedown";
677
- case 57436:
678
- return "volumeup";
679
- default:
680
- if (code >= 32 && code <= 126) {
681
- return String.fromCharCode(code).toLowerCase();
682
- }
683
- return "unknown";
684
- }
685
- }
686
- function getTildeKeyName(param) {
687
- const baseParam = param.split(";")[0];
688
- switch (baseParam) {
689
- case "1":
690
- return "home";
691
- case "2":
692
- return "insert";
693
- case "3":
694
- return "delete";
695
- case "4":
696
- return "end";
697
- case "5":
698
- return "pageup";
699
- case "6":
700
- return "pagedown";
701
- case "7":
702
- return "home";
703
- case "8":
704
- return "end";
705
- case "11":
706
- return "f1";
707
- case "12":
708
- return "f2";
709
- case "13":
710
- return "f3";
711
- case "14":
712
- return "f4";
713
- case "15":
714
- return "f5";
715
- case "17":
716
- return "f6";
717
- case "18":
718
- return "f7";
719
- case "19":
720
- return "f8";
721
- case "20":
722
- return "f9";
723
- case "21":
724
- return "f10";
725
- case "23":
726
- return "f11";
727
- case "24":
728
- return "f12";
729
- case "25":
730
- return "f13";
731
- case "26":
732
- return "f14";
733
- case "28":
734
- return "f15";
735
- case "29":
736
- return "f16";
737
- case "31":
738
- return "f17";
739
- case "32":
740
- return "f18";
741
- case "33":
742
- return "f19";
743
- case "34":
744
- return "f20";
745
- default:
746
- return "unknown";
747
- }
748
- }
749
- function applyModifiers(key, mod) {
750
- const m = mod - 1;
751
- if (m & 1) key.shift = true;
752
- if (m & 2) key.alt = true;
753
- if (m & 4) key.ctrl = true;
754
- if (m & 8) key.meta = true;
755
- }
756
- function parseKeySequence(data) {
757
- const keys = [];
758
- let i = 0;
759
- while (i < data.length) {
760
- const ch = data[i];
761
- const code = data.charCodeAt(i);
762
- if (ch === "\x1B") {
763
- if (data[i + 1] === "[") {
764
- const seq = parseCsiSequence(data, i);
765
- if (seq) {
766
- keys.push(seq.key);
767
- i = seq.end;
768
- continue;
769
- }
770
- }
771
- if (data[i + 1] === "O") {
772
- const seq = parseSs3Sequence(data, i);
773
- if (seq) {
774
- keys.push(seq.key);
775
- i = seq.end;
776
- continue;
777
- }
778
- }
779
- if (i + 1 < data.length && data.charCodeAt(i + 1) >= 32) {
780
- keys.push({
781
- name: data[i + 1].toLowerCase(),
782
- sequence: data.substring(i, i + 2),
783
- alt: true
784
- });
785
- i += 2;
786
- continue;
787
- }
788
- keys.push({ name: "escape", sequence: "\x1B" });
789
- i++;
790
- continue;
791
- }
792
- if (code >= 1 && code <= 26) {
793
- const letter = String.fromCharCode(code + 96);
794
- if (code === 13) {
795
- keys.push({ name: "return", sequence: "\r" });
796
- } else if (code === 9) {
797
- keys.push({ name: "tab", sequence: " " });
798
- } else if (code === 8) {
799
- keys.push({ name: "backspace", sequence: "\b" });
800
- } else {
801
- keys.push({ name: letter, sequence: ch, ctrl: true });
802
- }
803
- i++;
804
- continue;
805
- }
806
- if (code === 127) {
807
- keys.push({ name: "backspace", sequence: ch });
808
- i++;
809
- continue;
810
- }
811
- keys.push({ name: ch, sequence: ch });
812
- i++;
813
- }
814
- return keys;
815
- }
816
- function parseSs3Sequence(data, start) {
817
- if (start + 2 >= data.length) return null;
818
- const final = data[start + 2];
819
- const sequence = data.substring(start, start + 3);
820
- let key;
821
- switch (final) {
822
- // Arrow keys (some terminals)
823
- case "A":
824
- key = { name: "up", sequence };
825
- break;
826
- case "B":
827
- key = { name: "down", sequence };
828
- break;
829
- case "C":
830
- key = { name: "right", sequence };
831
- break;
832
- case "D":
833
- key = { name: "left", sequence };
834
- break;
835
- // Home/End (some terminals)
836
- case "H":
837
- key = { name: "home", sequence };
838
- break;
839
- case "F":
840
- key = { name: "end", sequence };
841
- break;
842
- // Function keys F1-F4
843
- case "P":
844
- key = { name: "f1", sequence };
845
- break;
846
- case "Q":
847
- key = { name: "f2", sequence };
848
- break;
849
- case "R":
850
- key = { name: "f3", sequence };
851
- break;
852
- case "S":
853
- key = { name: "f4", sequence };
854
- break;
855
- // Keypad (application mode)
856
- case "j":
857
- key = { name: "kpmultiply", sequence };
858
- break;
859
- case "k":
860
- key = { name: "kpplus", sequence };
861
- break;
862
- case "l":
863
- key = { name: "kpcomma", sequence };
864
- break;
865
- case "m":
866
- key = { name: "kpminus", sequence };
867
- break;
868
- case "n":
869
- key = { name: "kpdecimal", sequence };
870
- break;
871
- case "o":
872
- key = { name: "kpdivide", sequence };
873
- break;
874
- case "p":
875
- key = { name: "kp0", sequence };
876
- break;
877
- case "q":
878
- key = { name: "kp1", sequence };
879
- break;
880
- case "r":
881
- key = { name: "kp2", sequence };
882
- break;
883
- case "s":
884
- key = { name: "kp3", sequence };
885
- break;
886
- case "t":
887
- key = { name: "kp4", sequence };
888
- break;
889
- case "u":
890
- key = { name: "kp5", sequence };
891
- break;
892
- case "v":
893
- key = { name: "kp6", sequence };
894
- break;
895
- case "w":
896
- key = { name: "kp7", sequence };
897
- break;
898
- case "x":
899
- key = { name: "kp8", sequence };
900
- break;
901
- case "y":
902
- key = { name: "kp9", sequence };
903
- break;
904
- case "M":
905
- key = { name: "kpenter", sequence };
906
- break;
907
- default:
908
- return null;
909
- }
910
- return { key, end: start + 3 };
911
- }
912
- function parseCsiSequence(data, start) {
913
- let i = start + 2;
914
- let params = "";
915
- while (i < data.length) {
916
- const code = data.charCodeAt(i);
917
- if (code >= 48 && code <= 63) {
918
- params += data[i];
919
- i++;
920
- } else {
921
- break;
922
- }
923
- }
924
- if (i >= data.length) return null;
925
- const final = data[i];
926
- const sequence = data.substring(start, i + 1);
927
- i++;
928
- let key;
929
- switch (final) {
930
- // Arrow keys
931
- case "A":
932
- key = { name: "up", sequence };
933
- break;
934
- case "B":
935
- key = { name: "down", sequence };
936
- break;
937
- case "C":
938
- key = { name: "right", sequence };
939
- break;
940
- case "D":
941
- key = { name: "left", sequence };
942
- break;
943
- // Home/End
944
- case "H":
945
- key = { name: "home", sequence };
946
- break;
947
- case "F":
948
- key = { name: "end", sequence };
949
- break;
950
- // Shift+Tab
951
- case "Z":
952
- key = { name: "tab", sequence, shift: true };
953
- break;
954
- // Function keys (some terminals)
955
- case "P":
956
- key = { name: "f1", sequence };
957
- break;
958
- case "Q":
959
- key = { name: "f2", sequence };
960
- break;
961
- case "R":
962
- key = { name: "f3", sequence };
963
- break;
964
- case "S":
965
- key = { name: "f4", sequence };
966
- break;
967
- // ~ terminated sequences (VT-style)
968
- case "~": {
969
- if (params.startsWith("27;")) {
970
- const modParts = params.split(";");
971
- const mod = parseInt(modParts[1] ?? "1", 10);
972
- const keyCode = parseInt(modParts[2] ?? "0", 10);
973
- key = { name: getKeyNameFromCode(keyCode), sequence };
974
- applyModifiers(key, mod);
975
- break;
976
- }
977
- key = { name: getTildeKeyName(params), sequence };
978
- if (params.includes(";")) {
979
- const parts = params.split(";");
980
- const mod = parseInt(parts[1] ?? "1", 10);
981
- applyModifiers(key, mod);
982
- }
983
- break;
984
- }
985
- // Kitty keyboard protocol: CSI code;mod u
986
- case "u": {
987
- const parts = params.split(";");
988
- const keyCode = parseInt(parts[0] ?? "0", 10);
989
- const mod = parseInt(parts[1] ?? "1", 10);
990
- key = { name: getKeyNameFromCode(keyCode), sequence };
991
- applyModifiers(key, mod);
992
- break;
993
- }
994
- // Focus events (if terminal reports them)
995
- case "I":
996
- key = { name: "focus", sequence };
997
- break;
998
- case "O":
999
- key = { name: "blur", sequence };
1000
- break;
1001
- default:
1002
- key = { name: "unknown", sequence };
1003
- }
1004
- if (params.includes(";") && !["~", "u"].includes(final)) {
1005
- const parts = params.split(";");
1006
- const mod = parseInt(parts[parts.length - 1] ?? "1", 10);
1007
- if (mod >= 1 && mod <= 16) {
1008
- applyModifiers(key, mod);
1009
- }
1010
- }
1011
- return { key, end: i };
1012
- }
1013
-
1014
- // src/paint/color.ts
1015
- var NAMED_FG = {
1016
- black: "\x1B[30m",
1017
- red: "\x1B[31m",
1018
- green: "\x1B[32m",
1019
- yellow: "\x1B[33m",
1020
- blue: "\x1B[34m",
1021
- magenta: "\x1B[35m",
1022
- cyan: "\x1B[36m",
1023
- white: "\x1B[37m",
1024
- blackBright: "\x1B[90m",
1025
- redBright: "\x1B[91m",
1026
- greenBright: "\x1B[92m",
1027
- yellowBright: "\x1B[93m",
1028
- blueBright: "\x1B[94m",
1029
- magentaBright: "\x1B[95m",
1030
- cyanBright: "\x1B[96m",
1031
- whiteBright: "\x1B[97m"
1032
- };
1033
- var NAMED_BG = {
1034
- black: "\x1B[40m",
1035
- red: "\x1B[41m",
1036
- green: "\x1B[42m",
1037
- yellow: "\x1B[43m",
1038
- blue: "\x1B[44m",
1039
- magenta: "\x1B[45m",
1040
- cyan: "\x1B[46m",
1041
- white: "\x1B[47m",
1042
- blackBright: "\x1B[100m",
1043
- redBright: "\x1B[101m",
1044
- greenBright: "\x1B[102m",
1045
- yellowBright: "\x1B[103m",
1046
- blueBright: "\x1B[104m",
1047
- magentaBright: "\x1B[105m",
1048
- cyanBright: "\x1B[106m",
1049
- whiteBright: "\x1B[107m"
1050
- };
1051
- function parseHex(hex) {
1052
- const h = hex.replace("#", "");
1053
- const r = parseInt(h.substring(0, 2), 16);
1054
- const g = parseInt(h.substring(2, 4), 16);
1055
- const b = parseInt(h.substring(4, 6), 16);
1056
- return { r, g, b };
1057
- }
1058
- function colorToFg(color) {
1059
- if (typeof color === "string") {
1060
- if (color.startsWith("#")) {
1061
- const { r: r2, g: g2, b: b2 } = parseHex(color);
1062
- return `\x1B[38;2;${r2};${g2};${b2}m`;
1063
- }
1064
- return NAMED_FG[color] ?? "\x1B[39m";
1065
- }
1066
- if (typeof color === "number") {
1067
- return `\x1B[38;5;${color}m`;
1068
- }
1069
- const { r, g, b } = color;
1070
- return `\x1B[38;2;${r};${g};${b}m`;
1071
- }
1072
- function colorToBg(color) {
1073
- if (typeof color === "string") {
1074
- if (color.startsWith("#")) {
1075
- const { r: r2, g: g2, b: b2 } = parseHex(color);
1076
- return `\x1B[48;2;${r2};${g2};${b2}m`;
1077
- }
1078
- return NAMED_BG[color] ?? "\x1B[49m";
1079
- }
1080
- if (typeof color === "number") {
1081
- return `\x1B[48;5;${color}m`;
1082
- }
1083
- const { r, g, b } = color;
1084
- return `\x1B[48;2;${r};${g};${b}m`;
1085
- }
1086
- var NAMED_RGB = {
1087
- black: [0, 0, 0],
1088
- red: [170, 0, 0],
1089
- green: [0, 170, 0],
1090
- yellow: [170, 170, 0],
1091
- blue: [0, 0, 170],
1092
- magenta: [170, 0, 170],
1093
- cyan: [0, 170, 170],
1094
- white: [170, 170, 170],
1095
- blackBright: [85, 85, 85],
1096
- redBright: [255, 85, 85],
1097
- greenBright: [85, 255, 85],
1098
- yellowBright: [255, 255, 85],
1099
- blueBright: [85, 85, 255],
1100
- magentaBright: [255, 85, 255],
1101
- cyanBright: [85, 255, 255],
1102
- whiteBright: [255, 255, 255]
1103
- };
1104
- var NAMED_INDEX = [
1105
- "black",
1106
- "red",
1107
- "green",
1108
- "yellow",
1109
- "blue",
1110
- "magenta",
1111
- "cyan",
1112
- "white",
1113
- "blackBright",
1114
- "redBright",
1115
- "greenBright",
1116
- "yellowBright",
1117
- "blueBright",
1118
- "magentaBright",
1119
- "cyanBright",
1120
- "whiteBright"
1121
- ];
1122
- var terminalPalette = null;
1123
- function setTerminalPalette(palette) {
1124
- if (palette.size > 0) {
1125
- terminalPalette = palette;
1126
- }
1127
- }
1128
- function resolveNamedRgb(name) {
1129
- if (terminalPalette) {
1130
- const idx = NAMED_INDEX.indexOf(name);
1131
- if (idx !== -1) {
1132
- const tp = terminalPalette.get(idx);
1133
- if (tp) return tp;
1134
- }
1135
- }
1136
- return NAMED_RGB[name] ?? null;
1137
- }
1138
- function colorToRgb(color) {
1139
- if (typeof color === "string") {
1140
- if (color.startsWith("#")) {
1141
- const c = parseHex(color);
1142
- return [c.r, c.g, c.b];
1143
- }
1144
- return resolveNamedRgb(color);
1145
- }
1146
- if (typeof color === "number") {
1147
- if (color < 16) {
1148
- if (terminalPalette) {
1149
- const tp = terminalPalette.get(color);
1150
- if (tp) return tp;
1151
- }
1152
- return NAMED_RGB[NAMED_INDEX[color]];
1153
- }
1154
- if (color >= 232) {
1155
- const g2 = (color - 232) * 10 + 8;
1156
- return [g2, g2, g2];
1157
- }
1158
- const idx = color - 16;
1159
- const b = idx % 6 * 51;
1160
- const g = Math.floor(idx / 6) % 6 * 51;
1161
- const r = Math.floor(idx / 36) * 51;
1162
- return [r, g, b];
1163
- }
1164
- return [color.r, color.g, color.b];
1165
- }
1166
- function isLightColor(color) {
1167
- const rgb = colorToRgb(color);
1168
- if (!rgb) return false;
1169
- const [r, g, b] = rgb.map((c) => {
1170
- const s = c / 255;
1171
- return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
1172
- });
1173
- const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
1174
- return luminance > 0.4;
1175
- }
1176
- function colorsEqual(a, b) {
1177
- if (a === b) return true;
1178
- if (a == null || b == null) return false;
1179
- if (typeof a === "object" && typeof b === "object") {
1180
- return a.r === b.r && a.g === b.g && a.b === b.b;
1181
- }
1182
- return false;
1183
- }
1184
- function getContrastCursorColor(bg) {
1185
- if (!bg) return "white";
1186
- return isLightColor(bg) ? "black" : "white";
1187
- }
1188
-
1189
- // src/paint/framebuffer.ts
1190
- var Framebuffer = class _Framebuffer {
1191
- width;
1192
- height;
1193
- cells;
1194
- constructor(width, height) {
1195
- this.width = width;
1196
- this.height = height;
1197
- this.cells = new Array(width * height);
1198
- this.clear();
1199
- }
1200
- clear() {
1201
- for (let i = 0; i < this.cells.length; i++) {
1202
- this.cells[i] = { ch: " " };
1203
- }
1204
- }
1205
- resize(width, height) {
1206
- this.width = width;
1207
- this.height = height;
1208
- this.cells = new Array(width * height);
1209
- this.clear();
1210
- }
1211
- get(x, y) {
1212
- if (x < 0 || x >= this.width || y < 0 || y >= this.height) return void 0;
1213
- return this.cells[y * this.width + x];
1214
- }
1215
- set(x, y, cell) {
1216
- if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
1217
- this.cells[y * this.width + x] = cell;
1218
- }
1219
- setChar(x, y, ch, fg, bg, bold, dim, italic, underline) {
1220
- if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
1221
- this.cells[y * this.width + x] = { ch, fg, bg, bold, dim, italic, underline };
1222
- }
1223
- fillRect(x, y, w, h, ch, fg, bg) {
1224
- for (let row = y; row < y + h; row++) {
1225
- for (let col = x; col < x + w; col++) {
1226
- this.setChar(col, row, ch, fg, bg);
1227
- }
1228
- }
1229
- }
1230
- clone() {
1231
- const fb = new _Framebuffer(this.width, this.height);
1232
- for (let i = 0; i < this.cells.length; i++) {
1233
- const c = this.cells[i];
1234
- fb.cells[i] = { ...c };
1235
- }
1236
- return fb;
1237
- }
1238
- cellsEqual(a, b) {
1239
- return a.ch === b.ch && colorsEqual(a.fg, b.fg) && colorsEqual(a.bg, b.bg) && (a.bold ?? false) === (b.bold ?? false) && (a.dim ?? false) === (b.dim ?? false) && (a.italic ?? false) === (b.italic ?? false) && (a.underline ?? false) === (b.underline ?? false);
1240
- }
1241
- };
1242
-
1243
- // src/paint/borders.ts
1244
- var BORDER_CHARS = {
1245
- single: {
1246
- topLeft: "\u250C",
1247
- topRight: "\u2510",
1248
- bottomLeft: "\u2514",
1249
- bottomRight: "\u2518",
1250
- horizontal: "\u2500",
1251
- vertical: "\u2502"
1252
- },
1253
- double: {
1254
- topLeft: "\u2554",
1255
- topRight: "\u2557",
1256
- bottomLeft: "\u255A",
1257
- bottomRight: "\u255D",
1258
- horizontal: "\u2550",
1259
- vertical: "\u2551"
1260
- },
1261
- round: {
1262
- topLeft: "\u256D",
1263
- topRight: "\u256E",
1264
- bottomLeft: "\u2570",
1265
- bottomRight: "\u256F",
1266
- horizontal: "\u2500",
1267
- vertical: "\u2502"
1268
- },
1269
- ascii: {
1270
- topLeft: "+",
1271
- topRight: "+",
1272
- bottomLeft: "+",
1273
- bottomRight: "+",
1274
- horizontal: "-",
1275
- vertical: "|"
1276
- }
1277
- };
1278
- function getBorderChars(style) {
1279
- if (style === "none") return null;
1280
- return BORDER_CHARS[style];
1281
- }
1282
- function measureText(text, maxWidth, widthMode, wrapMode) {
1283
- if (text.length === 0) {
1284
- return { width: 0, height: 0 };
1285
- }
1286
- const lines = text.split("\n");
1287
- if (widthMode === Yoga.MeasureMode.Undefined || wrapMode === "none") {
1288
- let maxW2 = 0;
1289
- for (const line of lines) {
1290
- const w = stringWidth__default.default(line);
1291
- if (w > maxW2) maxW2 = w;
1292
- }
1293
- return { width: maxW2, height: lines.length };
1294
- }
1295
- const availWidth = Math.max(1, Math.floor(maxWidth));
1296
- const wrappedLines = wrapLines(lines, availWidth, wrapMode);
1297
- let maxW = 0;
1298
- for (const line of wrappedLines) {
1299
- const w = stringWidth__default.default(line);
1300
- if (w > maxW) maxW = w;
1301
- }
1302
- return { width: maxW, height: wrappedLines.length };
1303
- }
1304
- function wrapLines(lines, maxWidth, wrapMode) {
1305
- const result = [];
1306
- for (const line of lines) {
1307
- const lineWidth = stringWidth__default.default(line);
1308
- if (lineWidth <= maxWidth) {
1309
- result.push(line);
1310
- continue;
1311
- }
1312
- if (wrapMode === "truncate") {
1313
- result.push(truncateLine(line, maxWidth));
1314
- continue;
1315
- }
1316
- if (wrapMode === "ellipsis") {
1317
- result.push(truncateWithEllipsis(line, maxWidth));
1318
- continue;
1319
- }
1320
- const wrapped = wordWrap(line, maxWidth);
1321
- result.push(...wrapped);
1322
- }
1323
- return result;
1324
- }
1325
- function truncateLine(text, maxWidth) {
1326
- let result = "";
1327
- let width = 0;
1328
- for (const char of text) {
1329
- const charWidth = stringWidth__default.default(char);
1330
- if (width + charWidth > maxWidth) break;
1331
- result += char;
1332
- width += charWidth;
1333
- }
1334
- return result;
1335
- }
1336
- function truncateWithEllipsis(text, maxWidth) {
1337
- if (maxWidth <= 1) {
1338
- return maxWidth === 1 ? "\u2026" : "";
1339
- }
1340
- const truncated = truncateLine(text, maxWidth - 1);
1341
- if (stringWidth__default.default(truncated) < stringWidth__default.default(text)) {
1342
- return truncated + "\u2026";
1343
- }
1344
- return text;
1345
- }
1346
- function wordWrap(text, maxWidth) {
1347
- const lines = [];
1348
- let currentLine = "";
1349
- let currentWidth = 0;
1350
- let wordBuffer = "";
1351
- let wordBufferWidth = 0;
1352
- for (let i = 0; i <= text.length; i++) {
1353
- const char = text[i];
1354
- const isEnd = i === text.length;
1355
- const isSpace = char === " ";
1356
- if (isEnd || isSpace) {
1357
- if (wordBuffer.length > 0) {
1358
- if (currentWidth + wordBufferWidth <= maxWidth) {
1359
- currentLine += wordBuffer;
1360
- currentWidth += wordBufferWidth;
1361
- } else if (wordBufferWidth <= maxWidth) {
1362
- if (currentLine.length > 0) {
1363
- lines.push(currentLine);
1364
- }
1365
- currentLine = wordBuffer;
1366
- currentWidth = wordBufferWidth;
1367
- } else {
1368
- for (const c of wordBuffer) {
1369
- const cw = stringWidth__default.default(c);
1370
- if (currentWidth + cw > maxWidth && currentLine.length > 0) {
1371
- lines.push(currentLine);
1372
- currentLine = "";
1373
- currentWidth = 0;
1374
- }
1375
- currentLine += c;
1376
- currentWidth += cw;
1377
- }
1378
- }
1379
- wordBuffer = "";
1380
- wordBufferWidth = 0;
1381
- }
1382
- if (isSpace) {
1383
- if (currentWidth + 1 <= maxWidth) {
1384
- currentLine += " ";
1385
- currentWidth += 1;
1386
- } else {
1387
- if (currentLine.length > 0) {
1388
- lines.push(currentLine);
1389
- }
1390
- currentLine = " ";
1391
- currentWidth = 1;
1392
- }
1393
- }
1394
- } else {
1395
- wordBuffer += char;
1396
- wordBufferWidth += stringWidth__default.default(char);
1397
- }
1398
- }
1399
- if (currentLine.length > 0) {
1400
- lines.push(currentLine);
1401
- }
1402
- return lines.length > 0 ? lines : [""];
1403
- }
1404
- function paintTree(roots, fb, options = {}) {
1405
- fb.clear();
1406
- const result = {};
1407
- const entries = [];
1408
- const screenClip = { x: 0, y: 0, width: fb.width, height: fb.height };
1409
- for (const root of roots) {
1410
- if (root.hidden) continue;
1411
- collectPaintEntries(root, screenClip, root.style.zIndex ?? 0, entries);
1412
- }
1413
- entries.sort((a, b) => a.zIndex - b.zIndex);
1414
- for (const entry of entries) {
1415
- const nodeResult = paintNode(entry.node, fb, entry.clip, options);
1416
- if (nodeResult?.cursorPosition) {
1417
- result.cursorPosition = nodeResult.cursorPosition;
1418
- }
1419
- }
1420
- return result;
1421
- }
1422
- function collectPaintEntries(node, parentClip, parentZ, entries) {
1423
- if (node.hidden) return;
1424
- const zIndex = node.style.zIndex ?? parentZ;
1425
- const clip = node.style.clip ? intersectClip(parentClip, {
1426
- x: node.layout.innerX,
1427
- y: node.layout.innerY,
1428
- width: node.layout.innerWidth,
1429
- height: node.layout.innerHeight
1430
- }) : parentClip;
1431
- entries.push({ node, clip: parentClip, zIndex });
1432
- if (node.type !== "text" && node.type !== "input") {
1433
- for (const child of node.children) {
1434
- collectPaintEntries(child, clip, zIndex, entries);
1435
- }
1436
- }
1437
- }
1438
- function intersectClip(a, b) {
1439
- const x = Math.max(a.x, b.x);
1440
- const y = Math.max(a.y, b.y);
1441
- const right = Math.min(a.x + a.width, b.x + b.width);
1442
- const bottom = Math.min(a.y + a.height, b.y + b.height);
1443
- return {
1444
- x,
1445
- y,
1446
- width: Math.max(0, right - x),
1447
- height: Math.max(0, bottom - y)
1448
- };
1449
- }
1450
- function isInClip(x, y, clip) {
1451
- return x >= clip.x && x < clip.x + clip.width && y >= clip.y && y < clip.y + clip.height;
1452
- }
1453
- function paintNode(node, fb, clip, options = {}) {
1454
- const { x, y, width, height, innerX, innerY, innerWidth, innerHeight } = node.layout;
1455
- const style = node.style;
1456
- if (width <= 0 || height <= 0) return;
1457
- const inherited = getInheritedTextStyle(node);
1458
- const effectiveBg = inherited.bg;
1459
- if (style.bg) {
1460
- for (let row = y; row < y + height; row++) {
1461
- for (let col = x; col < x + width; col++) {
1462
- if (isInClip(col, row, clip)) {
1463
- fb.setChar(col, row, " ", void 0, style.bg);
1464
- }
1465
- }
1466
- }
1467
- }
1468
- const borderChars = style.border ? getBorderChars(style.border) : null;
1469
- if (borderChars && width >= 2 && height >= 2) {
1470
- const bc = style.borderColor;
1471
- const bg = effectiveBg;
1472
- setClipped(fb, clip, x, y, borderChars.topLeft, bc, bg);
1473
- for (let col = x + 1; col < x + width - 1; col++) {
1474
- setClipped(fb, clip, col, y, borderChars.horizontal, bc, bg);
1475
- }
1476
- setClipped(fb, clip, x + width - 1, y, borderChars.topRight, bc, bg);
1477
- setClipped(fb, clip, x, y + height - 1, borderChars.bottomLeft, bc, bg);
1478
- for (let col = x + 1; col < x + width - 1; col++) {
1479
- setClipped(fb, clip, col, y + height - 1, borderChars.horizontal, bc, bg);
1480
- }
1481
- setClipped(fb, clip, x + width - 1, y + height - 1, borderChars.bottomRight, bc, bg);
1482
- for (let row = y + 1; row < y + height - 1; row++) {
1483
- setClipped(fb, clip, x, row, borderChars.vertical, bc, bg);
1484
- setClipped(fb, clip, x + width - 1, row, borderChars.vertical, bc, bg);
1485
- }
1486
- }
1487
- if (node.type === "text") {
1488
- paintText(node, fb, clip);
1489
- } else if (node.type === "input") {
1490
- return paintInput(node, fb, clip, options);
1491
- }
1492
- return void 0;
1493
- }
1494
- function setClipped(fb, clip, x, y, ch, fg, bg, bold, dim, italic, underline) {
1495
- if (isInClip(x, y, clip)) {
1496
- fb.setChar(x, y, ch, fg, bg, bold, dim, italic, underline);
1497
- }
1498
- }
1499
- function autoContrastFg(explicitColor, bg) {
1500
- if (explicitColor !== void 0) return explicitColor;
1501
- if (bg === void 0) return void 0;
1502
- return isLightColor(bg) ? "black" : "white";
1503
- }
1504
- function paintText(node, fb, clip) {
1505
- const { innerX, innerY, innerWidth, innerHeight } = node.layout;
1506
- const inherited = getInheritedTextStyle(node);
1507
- const text = collectTextContent(node);
1508
- if (!text) return;
1509
- const fg = autoContrastFg(inherited.color, inherited.bg);
1510
- const wrapMode = node.style.wrap ?? "wrap";
1511
- const textAlign = node.style.textAlign ?? "left";
1512
- const rawLines = text.split("\n");
1513
- const lines = wrapLines(rawLines, innerWidth, wrapMode);
1514
- for (let lineIdx = 0; lineIdx < lines.length && lineIdx < innerHeight; lineIdx++) {
1515
- const line = lines[lineIdx];
1516
- const lineWidth = stringWidth__default.default(line);
1517
- let offsetX = 0;
1518
- if (textAlign === "center") {
1519
- offsetX = Math.max(0, Math.floor((innerWidth - lineWidth) / 2));
1520
- } else if (textAlign === "right") {
1521
- offsetX = Math.max(0, innerWidth - lineWidth);
1522
- }
1523
- let col = 0;
1524
- for (const char of line) {
1525
- const charWidth = stringWidth__default.default(char);
1526
- if (charWidth > 0) {
1527
- setClipped(
1528
- fb,
1529
- clip,
1530
- innerX + offsetX + col,
1531
- innerY + lineIdx,
1532
- char,
1533
- fg,
1534
- inherited.bg,
1535
- inherited.bold,
1536
- inherited.dim,
1537
- inherited.italic,
1538
- inherited.underline
1539
- );
1540
- }
1541
- col += charWidth;
1542
- }
1543
- }
1544
- }
1545
- function paintInput(node, fb, clip, options = {}) {
1546
- const { cursorInfo, useNativeCursor } = options;
1547
- const { innerX, innerY, innerWidth, innerHeight } = node.layout;
1548
- if (innerWidth <= 0 || innerHeight <= 0) return;
1549
- const value = node.props.value ?? node.props.defaultValue ?? "";
1550
- const placeholder = node.props.placeholder ?? "";
1551
- const displayText = value || placeholder;
1552
- const isPlaceholder = !value && !!placeholder;
1553
- const multiline = node.props.multiline ?? false;
1554
- const inherited = getInheritedTextStyle(node);
1555
- const autoFg = autoContrastFg(inherited.color, inherited.bg);
1556
- const placeholderFg = inherited.bg ? isLightColor(inherited.bg) ? "blackBright" : "whiteBright" : "blackBright";
1557
- const fg = isPlaceholder ? placeholderFg : autoFg ?? inherited.color ?? node.style.color;
1558
- const textFg = isPlaceholder ? placeholderFg : fg;
1559
- const textDim = isPlaceholder ? true : inherited.dim;
1560
- const isFocused = cursorInfo && cursorInfo.nodeId === node.focusId;
1561
- let result;
1562
- if (multiline && !isPlaceholder) {
1563
- const wrapMode = node.style.wrap ?? "wrap";
1564
- const rawLines = displayText.split("\n");
1565
- const wrappedLines = wrapLines(rawLines, innerWidth, wrapMode);
1566
- let cursorScreenLine = 0;
1567
- let cursorScreenCol = 0;
1568
- if (isFocused) {
1569
- const pos = cursorInfo.position;
1570
- let logicalLine = 0;
1571
- let offsetInLogicalLine = pos;
1572
- let runningPos = 0;
1573
- for (let i = 0; i < rawLines.length; i++) {
1574
- const lineLen = rawLines[i].length;
1575
- if (pos <= runningPos + lineLen) {
1576
- logicalLine = i;
1577
- offsetInLogicalLine = pos - runningPos;
1578
- break;
1579
- }
1580
- runningPos += lineLen + 1;
1581
- }
1582
- let wrappedLinesBefore = 0;
1583
- for (let i = 0; i < logicalLine; i++) {
1584
- wrappedLinesBefore += wrapLines([rawLines[i]], innerWidth, wrapMode).length;
1585
- }
1586
- const wrappedCurrentLine = wrapLines([rawLines[logicalLine]], innerWidth, wrapMode);
1587
- let charsProcessed = 0;
1588
- let subLineIdx = 0;
1589
- for (let i = 0; i < wrappedCurrentLine.length; i++) {
1590
- const subLine = wrappedCurrentLine[i];
1591
- if (offsetInLogicalLine <= charsProcessed + subLine.length) {
1592
- subLineIdx = i;
1593
- break;
1594
- }
1595
- charsProcessed += subLine.length;
1596
- }
1597
- cursorScreenLine = wrappedLinesBefore + subLineIdx;
1598
- cursorScreenCol = stringWidth__default.default(rawLines[logicalLine].slice(charsProcessed, charsProcessed + (offsetInLogicalLine - charsProcessed)));
1599
- }
1600
- const scrollOffset = Math.max(0, cursorScreenLine - innerHeight + 1);
1601
- for (let rowIdx = 0; rowIdx < innerHeight; rowIdx++) {
1602
- const lineNum = scrollOffset + rowIdx;
1603
- if (lineNum >= wrappedLines.length) break;
1604
- const line = wrappedLines[lineNum];
1605
- let col = 0;
1606
- for (const char of line) {
1607
- if (col >= innerWidth) break;
1608
- const charWidth = stringWidth__default.default(char);
1609
- if (charWidth > 0) {
1610
- setClipped(
1611
- fb,
1612
- clip,
1613
- innerX + col,
1614
- innerY + rowIdx,
1615
- char,
1616
- textFg,
1617
- inherited.bg,
1618
- inherited.bold,
1619
- textDim,
1620
- inherited.italic,
1621
- inherited.underline
1622
- );
1623
- }
1624
- col += charWidth;
1625
- }
1626
- }
1627
- if (isFocused) {
1628
- const screenRow = cursorScreenLine - scrollOffset;
1629
- if (screenRow >= 0 && screenRow < innerHeight) {
1630
- const cCol = Math.min(cursorScreenCol, innerWidth - 1);
1631
- const cursorX = innerX + cCol;
1632
- const cursorY = innerY + screenRow;
1633
- if (isInClip(cursorX, cursorY, clip) && cursorX < innerX + innerWidth) {
1634
- if (useNativeCursor) {
1635
- result = { cursorPosition: { x: cursorX, y: cursorY, bg: inherited.bg } };
1636
- } else {
1637
- const existing = fb.get(cursorX, cursorY);
1638
- const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
1639
- const cursorFg = inherited.bg ?? "black";
1640
- const cursorBg = inherited.color ?? "white";
1641
- fb.setChar(
1642
- cursorX,
1643
- cursorY,
1644
- cursorChar,
1645
- cursorFg,
1646
- cursorBg,
1647
- existing?.bold,
1648
- existing?.dim,
1649
- existing?.italic,
1650
- false
1651
- );
1652
- }
1653
- }
1654
- }
1655
- }
1656
- } else {
1657
- let col = 0;
1658
- for (const char of displayText) {
1659
- if (col >= innerWidth) break;
1660
- const charWidth = stringWidth__default.default(char);
1661
- if (charWidth > 0) {
1662
- setClipped(
1663
- fb,
1664
- clip,
1665
- innerX + col,
1666
- innerY,
1667
- char,
1668
- textFg,
1669
- inherited.bg,
1670
- inherited.bold,
1671
- textDim,
1672
- inherited.italic,
1673
- inherited.underline
1674
- );
1675
- }
1676
- col += charWidth;
1677
- }
1678
- if (isFocused) {
1679
- const cursorCol = Math.min(cursorInfo.position, innerWidth - 1);
1680
- const cursorX = innerX + cursorCol;
1681
- if (isInClip(cursorX, innerY, clip) && cursorX < innerX + innerWidth) {
1682
- if (useNativeCursor) {
1683
- result = { cursorPosition: { x: cursorX, y: innerY, bg: inherited.bg } };
1684
- } else {
1685
- const existing = fb.get(cursorX, innerY);
1686
- const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
1687
- const cursorFg = inherited.bg ?? "black";
1688
- const cursorBg = inherited.color ?? "white";
1689
- fb.setChar(
1690
- cursorX,
1691
- innerY,
1692
- cursorChar,
1693
- cursorFg,
1694
- cursorBg,
1695
- existing?.bold,
1696
- existing?.dim,
1697
- existing?.italic,
1698
- false
1699
- );
1700
- }
1701
- }
1702
- }
1703
- }
1704
- return result;
1705
- }
1706
-
1707
- // src/paint/diff.ts
1708
- var ESC2 = "\x1B";
1709
- var CSI2 = `${ESC2}[`;
1710
- function moveCursor(x, y) {
1711
- return `${CSI2}${y + 1};${x + 1}H`;
1712
- }
1713
- function buildSGR(cell) {
1714
- let seq = `${CSI2}0m`;
1715
- if (cell.bold) seq += `${CSI2}1m`;
1716
- if (cell.dim) seq += `${CSI2}2m`;
1717
- if (cell.italic) seq += `${CSI2}3m`;
1718
- if (cell.underline) seq += `${CSI2}4m`;
1719
- if (cell.fg != null) seq += colorToFg(cell.fg);
1720
- if (cell.bg != null) seq += colorToBg(cell.bg);
1721
- return seq;
1722
- }
1723
- function diffFramebuffers(prev, next, fullRedraw) {
1724
- let out = "";
1725
- let lastX = -1;
1726
- let lastY = -1;
1727
- let lastSGR = "";
1728
- for (let y = 0; y < next.height; y++) {
1729
- for (let x = 0; x < next.width; x++) {
1730
- const nc = next.get(x, y);
1731
- if (!fullRedraw) {
1732
- const pc = prev.get(x, y);
1733
- if (pc && next.cellsEqual(nc, pc)) continue;
1734
- }
1735
- if (lastY !== y || lastX !== x) {
1736
- out += moveCursor(x, y);
1737
- }
1738
- const sgr = buildSGR(nc);
1739
- if (sgr !== lastSGR) {
1740
- out += sgr;
1741
- lastSGR = sgr;
1742
- }
1743
- out += nc.ch;
1744
- lastX = x + 1;
1745
- lastY = y;
1746
- }
1747
- }
1748
- if (out.length > 0) {
1749
- out += `${CSI2}0m`;
1750
- }
1751
- return out;
1752
- }
1753
- var FLEX_DIR_MAP = {
1754
- row: Yoga.FlexDirection.Row,
1755
- column: Yoga.FlexDirection.Column
1756
- };
1757
- var JUSTIFY_MAP = {
1758
- "flex-start": Yoga.Justify.FlexStart,
1759
- center: Yoga.Justify.Center,
1760
- "flex-end": Yoga.Justify.FlexEnd,
1761
- "space-between": Yoga.Justify.SpaceBetween,
1762
- "space-around": Yoga.Justify.SpaceAround
1763
- };
1764
- var ALIGN_MAP = {
1765
- "flex-start": Yoga.Align.FlexStart,
1766
- center: Yoga.Align.Center,
1767
- "flex-end": Yoga.Align.FlexEnd,
1768
- stretch: Yoga.Align.Stretch
1769
- };
1770
- function setDimension(node, setter, value) {
1771
- if (value === void 0) return;
1772
- if (typeof value === "string" && value.endsWith("%")) {
1773
- setter(value);
1774
- } else {
1775
- setter(value);
1776
- }
1777
- }
1778
- function setPosition(node, edge, value) {
1779
- if (value === void 0) return;
1780
- if (typeof value === "string" && value.endsWith("%")) {
1781
- node.setPositionPercent(edge, parseFloat(value));
1782
- } else {
1783
- node.setPosition(edge, value);
1784
- }
1785
- }
1786
- function applyStyleToYogaNode(yogaNode, style, nodeType) {
1787
- setDimension(yogaNode, (v) => yogaNode.setWidth(v), style.width);
1788
- setDimension(yogaNode, (v) => yogaNode.setHeight(v), style.height);
1789
- if (style.minWidth !== void 0) yogaNode.setMinWidth(style.minWidth);
1790
- if (style.minHeight !== void 0) yogaNode.setMinHeight(style.minHeight);
1791
- if (style.maxWidth !== void 0) yogaNode.setMaxWidth(style.maxWidth);
1792
- if (style.maxHeight !== void 0) yogaNode.setMaxHeight(style.maxHeight);
1793
- if (style.padding !== void 0) yogaNode.setPadding(Yoga.Edge.All, style.padding);
1794
- if (style.paddingX !== void 0) yogaNode.setPadding(Yoga.Edge.Horizontal, style.paddingX);
1795
- if (style.paddingY !== void 0) yogaNode.setPadding(Yoga.Edge.Vertical, style.paddingY);
1796
- if (style.paddingTop !== void 0) yogaNode.setPadding(Yoga.Edge.Top, style.paddingTop);
1797
- if (style.paddingRight !== void 0) yogaNode.setPadding(Yoga.Edge.Right, style.paddingRight);
1798
- if (style.paddingBottom !== void 0) yogaNode.setPadding(Yoga.Edge.Bottom, style.paddingBottom);
1799
- if (style.paddingLeft !== void 0) yogaNode.setPadding(Yoga.Edge.Left, style.paddingLeft);
1800
- const hasBorder = style.border != null && style.border !== "none";
1801
- yogaNode.setBorder(Yoga.Edge.All, hasBorder ? 1 : 0);
1802
- if (style.flexDirection) {
1803
- yogaNode.setFlexDirection(FLEX_DIR_MAP[style.flexDirection] ?? Yoga.FlexDirection.Column);
1804
- }
1805
- if (style.flexWrap) {
1806
- yogaNode.setFlexWrap(style.flexWrap === "wrap" ? Yoga.Wrap.Wrap : Yoga.Wrap.NoWrap);
1807
- }
1808
- if (style.justifyContent) {
1809
- yogaNode.setJustifyContent(JUSTIFY_MAP[style.justifyContent] ?? Yoga.Justify.FlexStart);
1810
- }
1811
- if (style.alignItems) {
1812
- yogaNode.setAlignItems(ALIGN_MAP[style.alignItems] ?? Yoga.Align.Stretch);
1813
- }
1814
- if (style.flexGrow !== void 0) yogaNode.setFlexGrow(style.flexGrow);
1815
- if (style.flexShrink !== void 0) yogaNode.setFlexShrink(style.flexShrink);
1816
- if (style.gap !== void 0) yogaNode.setGap(Yoga.Gutter.All, style.gap);
1817
- if (style.position === "absolute") {
1818
- yogaNode.setPositionType(Yoga.PositionType.Absolute);
1819
- } else {
1820
- yogaNode.setPositionType(Yoga.PositionType.Relative);
1821
- }
1822
- if (style.inset !== void 0) {
1823
- setPosition(yogaNode, Yoga.Edge.Top, style.inset);
1824
- setPosition(yogaNode, Yoga.Edge.Right, style.inset);
1825
- setPosition(yogaNode, Yoga.Edge.Bottom, style.inset);
1826
- setPosition(yogaNode, Yoga.Edge.Left, style.inset);
1827
- }
1828
- setPosition(yogaNode, Yoga.Edge.Top, style.top);
1829
- setPosition(yogaNode, Yoga.Edge.Right, style.right);
1830
- setPosition(yogaNode, Yoga.Edge.Bottom, style.bottom);
1831
- setPosition(yogaNode, Yoga.Edge.Left, style.left);
1832
- if (style.clip) {
1833
- yogaNode.setOverflow(Yoga.Overflow.Hidden);
1834
- }
1835
- }
1836
- function buildYogaTree(node) {
1837
- const yogaNode = Yoga__default.default.Node.create();
1838
- node.yogaNode = yogaNode;
1839
- applyStyleToYogaNode(yogaNode, node.style, node.type);
1840
- if (node.type === "text" || node.type === "input") {
1841
- yogaNode.setMeasureFunc((width, widthMode, height, heightMode) => {
1842
- let text;
1843
- if (node.type === "input") {
1844
- text = node.props.value ?? node.props.defaultValue ?? node.props.placeholder ?? "";
1845
- if (text.length === 0) text = " ";
1846
- } else {
1847
- text = collectAllText(node);
1848
- }
1849
- return measureText(
1850
- text,
1851
- width,
1852
- widthMode,
1853
- node.style.wrap ?? "wrap"
1854
- );
1855
- });
1856
- } else {
1857
- for (let i = 0; i < node.children.length; i++) {
1858
- const child = node.children[i];
1859
- if (child.hidden) continue;
1860
- buildYogaTree(child);
1861
- yogaNode.insertChild(child.yogaNode, yogaNode.getChildCount());
1862
- }
1863
- }
1864
- }
1865
- function collectAllText(node) {
1866
- if (node.text != null) return node.text;
1867
- let result = "";
1868
- for (const child of node.children) {
1869
- result += collectAllText(child);
1870
- }
1871
- if (result === "" && node.props.children != null) {
1872
- if (typeof node.props.children === "string") return node.props.children;
1873
- if (typeof node.props.children === "number") return String(node.props.children);
1874
- }
1875
- return result;
1876
- }
1877
- function extractLayout(node, parentX, parentY) {
1878
- const yn = node.yogaNode;
1879
- const computedLayout = yn.getComputedLayout();
1880
- const x = parentX + computedLayout.left;
1881
- const y = parentY + computedLayout.top;
1882
- const width = computedLayout.width;
1883
- const height = computedLayout.height;
1884
- const borderWidth = node.style.border && node.style.border !== "none" ? 1 : 0;
1885
- const paddingTop = yn.getComputedPadding(Yoga.Edge.Top);
1886
- const paddingRight = yn.getComputedPadding(Yoga.Edge.Right);
1887
- const paddingBottom = yn.getComputedPadding(Yoga.Edge.Bottom);
1888
- const paddingLeft = yn.getComputedPadding(Yoga.Edge.Left);
1889
- const innerX = x + borderWidth + paddingLeft;
1890
- const innerY = y + borderWidth + paddingTop;
1891
- const innerWidth = Math.max(0, width - borderWidth * 2 - paddingLeft - paddingRight);
1892
- const innerHeight = Math.max(0, height - borderWidth * 2 - paddingTop - paddingBottom);
1893
- node.layout = { x, y, width, height, innerX, innerY, innerWidth, innerHeight };
1894
- for (const child of node.children) {
1895
- if (child.hidden || !child.yogaNode) continue;
1896
- extractLayout(child, x, y);
1897
- }
1898
- }
1899
- function computeLayout(roots, screenWidth, screenHeight) {
1900
- const rootYoga = Yoga__default.default.Node.create();
1901
- rootYoga.setWidth(screenWidth);
1902
- rootYoga.setHeight(screenHeight);
1903
- rootYoga.setFlexDirection(Yoga.FlexDirection.Column);
1904
- for (const child of roots) {
1905
- if (child.hidden) continue;
1906
- buildYogaTree(child);
1907
- rootYoga.insertChild(child.yogaNode, rootYoga.getChildCount());
1908
- }
1909
- rootYoga.calculateLayout(screenWidth, screenHeight, Yoga.Direction.LTR);
1910
- for (const child of roots) {
1911
- if (child.hidden || !child.yogaNode) continue;
1912
- extractLayout(child, 0, 0);
1913
- }
1914
- rootYoga.freeRecursive();
1915
- clearYogaRefs(roots);
1916
- }
1917
- function clearYogaRefs(nodes) {
1918
- for (const node of nodes) {
1919
- node.yogaNode = null;
1920
- clearYogaRefs(node.children);
1921
- }
1922
- }
1923
- var InputContext = React15.createContext(null);
1924
- var FocusContext = React15.createContext(null);
1925
- var LayoutContext = React15.createContext(null);
1926
- var AppContext = React15.createContext(null);
1927
-
1928
- // src/render.ts
1929
- function render(element, opts = {}) {
1930
- const stdout = opts.stdout ?? process.stdout;
1931
- const stdin = opts.stdin ?? process.stdin;
1932
- const debug = opts.debug ?? false;
1933
- const useNativeCursor = opts.useNativeCursor ?? true;
1934
- const terminal = new Terminal(stdout, stdin);
1935
- terminal.setup();
1936
- let nativeCursorVisible = false;
1937
- terminal.queryPalette().then((palette) => {
1938
- setTerminalPalette(palette);
1939
- fullRedraw = true;
1940
- scheduleRender();
1941
- });
1942
- const prevFb = new Framebuffer(terminal.columns, terminal.rows);
1943
- const currentFb = new Framebuffer(terminal.columns, terminal.rows);
1944
- let fullRedraw = true;
1945
- const inputHandlers = /* @__PURE__ */ new Set();
1946
- const priorityHandlers = /* @__PURE__ */ new Set();
1947
- const focusedInputHandlers = /* @__PURE__ */ new Map();
1948
- const inputContextValue = {
1949
- subscribe(handler) {
1950
- inputHandlers.add(handler);
1951
- return () => inputHandlers.delete(handler);
1952
- },
1953
- subscribePriority(handler) {
1954
- priorityHandlers.add(handler);
1955
- return () => priorityHandlers.delete(handler);
1956
- },
1957
- registerInputHandler(focusId, handler) {
1958
- focusedInputHandlers.set(focusId, handler);
1959
- return () => focusedInputHandlers.delete(focusId);
1960
- }
1961
- };
1962
- let focusedId = null;
1963
- const focusRegistry = /* @__PURE__ */ new Map();
1964
- const focusOrder = [];
1965
- const skippableIds = /* @__PURE__ */ new Set();
1966
- let trapStack = [];
1967
- const focusChangeHandlers = /* @__PURE__ */ new Set();
1968
- function setFocusedId(id) {
1969
- if (focusedId !== id) {
1970
- focusedId = id;
1971
- scheduleRender();
1972
- for (const handler of focusChangeHandlers) {
1973
- handler(focusedId);
1974
- }
1975
- }
1976
- }
1977
- function getActiveFocusableIds() {
1978
- let ids = [...focusOrder];
1979
- if (trapStack.length > 0) {
1980
- const trap = trapStack[trapStack.length - 1];
1981
- ids = ids.filter((id) => trap.has(id));
1982
- }
1983
- ids = ids.filter((id) => !skippableIds.has(id));
1984
- ids.sort((a, b) => {
1985
- const nodeA = focusRegistry.get(a);
1986
- const nodeB = focusRegistry.get(b);
1987
- if (!nodeA || !nodeB) return 0;
1988
- const layoutA = nodeA.layout;
1989
- const layoutB = nodeB.layout;
1990
- if (layoutA.y !== layoutB.y) {
1991
- return layoutA.y - layoutB.y;
1992
- }
1993
- return layoutA.x - layoutB.x;
1994
- });
1995
- return ids;
1996
- }
1997
- const focusContextValue = {
1998
- get focusedId() {
1999
- return focusedId;
2000
- },
2001
- register(id, node) {
2002
- focusRegistry.set(id, node);
2003
- if (!focusOrder.includes(id)) {
2004
- focusOrder.push(id);
2005
- }
2006
- if (trapStack.length > 0) {
2007
- trapStack[trapStack.length - 1].add(id);
2008
- }
2009
- if (focusedId === null) {
2010
- const activeIds = getActiveFocusableIds();
2011
- if (activeIds.length > 0) {
2012
- setFocusedId(activeIds[0]);
2013
- }
2014
- }
2015
- return () => {
2016
- focusRegistry.delete(id);
2017
- const idx = focusOrder.indexOf(id);
2018
- if (idx !== -1) focusOrder.splice(idx, 1);
2019
- if (focusedId === id) {
2020
- const activeIds = getActiveFocusableIds();
2021
- setFocusedId(activeIds[0] ?? null);
2022
- }
2023
- };
2024
- },
2025
- requestFocus(id) {
2026
- setFocusedId(id);
2027
- },
2028
- focusNext() {
2029
- const ids = getActiveFocusableIds();
2030
- if (ids.length === 0) return;
2031
- const currentIdx = focusedId ? ids.indexOf(focusedId) : -1;
2032
- const nextIdx = (currentIdx + 1) % ids.length;
2033
- setFocusedId(ids[nextIdx]);
2034
- },
2035
- focusPrev() {
2036
- const ids = getActiveFocusableIds();
2037
- if (ids.length === 0) return;
2038
- const currentIdx = focusedId ? ids.indexOf(focusedId) : 0;
2039
- const prevIdx = (currentIdx - 1 + ids.length) % ids.length;
2040
- setFocusedId(ids[prevIdx]);
2041
- },
2042
- setSkippable(id, skippable) {
2043
- if (skippable) {
2044
- skippableIds.add(id);
2045
- if (focusedId === id) {
2046
- const ids = getActiveFocusableIds();
2047
- if (ids.length > 0) {
2048
- setFocusedId(ids[0]);
2049
- }
2050
- }
2051
- } else {
2052
- skippableIds.delete(id);
2053
- }
2054
- },
2055
- trapIds: null,
2056
- pushTrap(ids) {
2057
- trapStack.push(ids);
2058
- return () => {
2059
- const idx = trapStack.indexOf(ids);
2060
- if (idx !== -1) trapStack.splice(idx, 1);
2061
- };
2062
- },
2063
- onFocusChange(handler) {
2064
- focusChangeHandlers.add(handler);
2065
- return () => {
2066
- focusChangeHandlers.delete(handler);
2067
- };
2068
- },
2069
- getRegisteredElements() {
2070
- const result = [];
2071
- for (const id of focusOrder) {
2072
- if (skippableIds.has(id)) continue;
2073
- const node = focusRegistry.get(id);
2074
- if (node) {
2075
- result.push({ id, node });
2076
- }
2077
- }
2078
- return result;
2079
- },
2080
- getActiveElements() {
2081
- const activeIds = getActiveFocusableIds();
2082
- const result = [];
2083
- for (const id of activeIds) {
2084
- if (skippableIds.has(id)) continue;
2085
- const node = focusRegistry.get(id);
2086
- if (node) {
2087
- result.push({ id, node });
2088
- }
2089
- }
2090
- return result;
2091
- }
2092
- };
2093
- const layoutSubscriptions = /* @__PURE__ */ new Map();
2094
- const layoutContextValue = {
2095
- getLayout(node) {
2096
- return node.layout;
2097
- },
2098
- subscribe(node, handler) {
2099
- if (!layoutSubscriptions.has(node)) {
2100
- layoutSubscriptions.set(node, /* @__PURE__ */ new Set());
2101
- }
2102
- layoutSubscriptions.get(node).add(handler);
2103
- return () => {
2104
- const subs = layoutSubscriptions.get(node);
2105
- if (subs) {
2106
- subs.delete(handler);
2107
- if (subs.size === 0) layoutSubscriptions.delete(node);
2108
- }
2109
- };
2110
- }
2111
- };
2112
- const appContextValue = {
2113
- registerNode() {
2114
- },
2115
- unregisterNode() {
2116
- },
2117
- scheduleRender,
2118
- exit(code) {
2119
- handle.exit(code);
2120
- },
2121
- get columns() {
2122
- return terminal.columns;
2123
- },
2124
- get rows() {
2125
- return terminal.rows;
2126
- }
2127
- };
2128
- const container = {
2129
- type: "root",
2130
- children: [],
2131
- onCommit() {
2132
- scheduleRender();
2133
- }
2134
- };
2135
- let renderScheduled = false;
2136
- function scheduleRender() {
2137
- if (renderScheduled) return;
2138
- renderScheduled = true;
2139
- queueMicrotask(() => {
2140
- renderScheduled = false;
2141
- performRender();
2142
- });
2143
- }
2144
- function performRender() {
2145
- const cols = terminal.columns;
2146
- const rows = terminal.rows;
2147
- if (currentFb.width !== cols || currentFb.height !== rows) {
2148
- currentFb.resize(cols, rows);
2149
- prevFb.resize(cols, rows);
2150
- fullRedraw = true;
2151
- }
2152
- computeLayout(container.children, cols, rows);
2153
- notifyLayoutSubscribers(container.children);
2154
- let cursorInfo;
2155
- if (focusedId) {
2156
- const focusedNode = focusRegistry.get(focusedId);
2157
- if (focusedNode?.type === "input") {
2158
- cursorInfo = {
2159
- nodeId: focusedId,
2160
- position: focusedNode.props.cursorPosition ?? (focusedNode.props.value?.length ?? 0)
2161
- };
2162
- }
2163
- }
2164
- const paintResult = paintTree(container.children, currentFb, {
2165
- cursorInfo,
2166
- useNativeCursor
2167
- });
2168
- const output = diffFramebuffers(prevFb, currentFb, fullRedraw);
2169
- if (output.length > 0) {
2170
- terminal.write(output);
2171
- }
2172
- if (useNativeCursor) {
2173
- if (paintResult.cursorPosition) {
2174
- const cursorColor = getContrastCursorColor(paintResult.cursorPosition.bg);
2175
- terminal.setCursorColor(cursorColor);
2176
- terminal.moveCursor(paintResult.cursorPosition.x, paintResult.cursorPosition.y);
2177
- if (!nativeCursorVisible) {
2178
- terminal.showCursor();
2179
- nativeCursorVisible = true;
2180
- }
2181
- } else {
2182
- if (nativeCursorVisible) {
2183
- terminal.hideCursor();
2184
- nativeCursorVisible = false;
2185
- }
2186
- }
2187
- }
2188
- for (let i = 0; i < currentFb.cells.length; i++) {
2189
- prevFb.cells[i] = { ...currentFb.cells[i] };
2190
- }
2191
- fullRedraw = false;
2192
- }
2193
- function notifyLayoutSubscribers(nodes) {
2194
- for (const node of nodes) {
2195
- const subs = layoutSubscriptions.get(node);
2196
- if (subs) {
2197
- for (const handler of subs) {
2198
- handler(node.layout);
2199
- }
2200
- }
2201
- notifyLayoutSubscribers(node.children);
2202
- }
2203
- }
2204
- const removeDataListener = terminal.onData((data) => {
2205
- const keys = parseKeySequence(data);
2206
- for (const key of keys) {
2207
- if (key.ctrl && key.name === "c") {
2208
- handle.exit();
2209
- return;
2210
- }
2211
- if (key.ctrl && key.name === "z") {
2212
- terminal.suspend();
2213
- process.kill(0, "SIGSTOP");
2214
- return;
2215
- }
2216
- if (key.name === "tab" && !key.ctrl && !key.alt) {
2217
- if (key.shift) {
2218
- focusContextValue.focusPrev();
2219
- } else {
2220
- focusContextValue.focusNext();
2221
- }
2222
- continue;
2223
- }
2224
- let consumed = false;
2225
- for (const handler of priorityHandlers) {
2226
- if (handler(key)) {
2227
- consumed = true;
2228
- break;
2229
- }
2230
- }
2231
- if (!consumed && focusedId) {
2232
- const inputHandler = focusedInputHandlers.get(focusedId);
2233
- if (inputHandler) {
2234
- consumed = inputHandler(key);
2235
- }
2236
- }
2237
- if (!consumed) {
2238
- for (const handler of inputHandlers) {
2239
- handler(key);
2240
- }
2241
- }
2242
- }
2243
- });
2244
- const removeResizeListener = terminal.onResize(() => {
2245
- fullRedraw = true;
2246
- scheduleRender();
2247
- });
2248
- const handleSigcont = () => {
2249
- terminal.resume();
2250
- fullRedraw = true;
2251
- scheduleRender();
2252
- };
2253
- process.on("SIGCONT", handleSigcont);
2254
- const wrappedElement = React15__default.default.createElement(
2255
- AppContext.Provider,
2256
- { value: appContextValue },
2257
- React15__default.default.createElement(
2258
- InputContext.Provider,
2259
- { value: inputContextValue },
2260
- React15__default.default.createElement(
2261
- FocusContext.Provider,
2262
- { value: focusContextValue },
2263
- React15__default.default.createElement(
2264
- LayoutContext.Provider,
2265
- { value: layoutContextValue },
2266
- element
2267
- )
2268
- )
2269
- )
2270
- );
2271
- const onUncaughtError = (error) => {
2272
- if (debug) console.error("Uncaught error:", error);
2273
- };
2274
- const onCaughtError = (error) => {
2275
- if (debug) console.error("Error caught by boundary:", error);
2276
- };
2277
- const onRecoverableError = (error) => {
2278
- if (debug) console.error("Recoverable error:", error);
2279
- };
2280
- const root = reconciler.createContainer(
2281
- container,
2282
- 0,
2283
- // LegacyRoot tag
2284
- null,
2285
- // hydrationCallbacks
2286
- false,
2287
- // isStrictMode
2288
- null,
2289
- // concurrentUpdatesByDefaultOverride
2290
- "",
2291
- // identifierPrefix
2292
- onUncaughtError,
2293
- onCaughtError,
2294
- onRecoverableError,
2295
- null
2296
- // transitionCallbacks
2297
- );
2298
- reconciler.updateContainer(wrappedElement, root, null, null);
2299
- const handle = {
2300
- unmount() {
2301
- reconciler.updateContainer(null, root, null, null);
2302
- removeDataListener();
2303
- removeResizeListener();
2304
- process.off("SIGCONT", handleSigcont);
2305
- terminal.cleanup();
2306
- },
2307
- exit(code) {
2308
- handle.unmount();
2309
- process.exit(code ?? 0);
2310
- }
2311
- };
2312
- return handle;
2313
- }
2314
- var Box = React15.forwardRef(
2315
- function Box2({ children, style, focusable }, ref) {
2316
- return React15__default.default.createElement("box", { style, focusable, ref }, children);
2317
- }
2318
- );
2319
- var Text = React15.forwardRef(
2320
- function Text2({ children, style, wrap }, ref) {
2321
- const mergedStyle = wrap ? { ...style, wrap } : style;
2322
- return React15__default.default.createElement("text", { style: mergedStyle, ref }, children);
2323
- }
2324
- );
2325
- function cursorToVisualLine(text, pos, width) {
2326
- if (width <= 0) {
2327
- return { visualLine: 0, visualCol: pos, totalVisualLines: 1, lineStartOffset: 0, lineLength: text.length };
2328
- }
2329
- const logicalLines = text.split("\n");
2330
- const allVisualLines = [];
2331
- let logicalOffset = 0;
2332
- for (const logicalLine of logicalLines) {
2333
- const wrapped = wrapLines([logicalLine], width, "wrap");
2334
- let offsetInLogical = 0;
2335
- for (const wrappedLine of wrapped) {
2336
- allVisualLines.push({
2337
- text: wrappedLine,
2338
- logicalOffset: logicalOffset + offsetInLogical
2339
- });
2340
- offsetInLogical += wrappedLine.length;
2341
- }
2342
- logicalOffset += logicalLine.length + 1;
2343
- }
2344
- let charCount = 0;
2345
- for (let i = 0; i < allVisualLines.length; i++) {
2346
- const vl = allVisualLines[i];
2347
- const lineLen = vl.text.length;
2348
- const isEndOfLogicalLine = i + 1 < allVisualLines.length && allVisualLines[i + 1].logicalOffset !== vl.logicalOffset + lineLen;
2349
- const effectiveLen = lineLen + (isEndOfLogicalLine ? 1 : 0);
2350
- if (pos < charCount + lineLen || i === allVisualLines.length - 1) {
2351
- return {
2352
- visualLine: i,
2353
- visualCol: Math.min(pos - charCount, lineLen),
2354
- totalVisualLines: allVisualLines.length,
2355
- lineStartOffset: charCount,
2356
- lineLength: lineLen
2357
- };
2358
- }
2359
- charCount += effectiveLen;
2360
- }
2361
- const lastIdx = allVisualLines.length - 1;
2362
- return {
2363
- visualLine: lastIdx,
2364
- visualCol: allVisualLines[lastIdx].text.length,
2365
- totalVisualLines: allVisualLines.length,
2366
- lineStartOffset: charCount - allVisualLines[lastIdx].text.length,
2367
- lineLength: allVisualLines[lastIdx].text.length
2368
- };
2369
- }
2370
- function visualLineToCursor(text, visualLine, visualCol, width) {
2371
- if (width <= 0) {
2372
- return Math.min(visualCol, text.length);
2373
- }
2374
- const logicalLines = text.split("\n");
2375
- const allVisualLines = [];
2376
- let offset = 0;
2377
- for (const logicalLine of logicalLines) {
2378
- const wrapped = wrapLines([logicalLine], width, "wrap");
2379
- let offsetInLogical = 0;
2380
- for (const wrappedLine of wrapped) {
2381
- allVisualLines.push({
2382
- text: wrappedLine,
2383
- startOffset: offset + offsetInLogical
2384
- });
2385
- offsetInLogical += wrappedLine.length;
2386
- }
2387
- offset += logicalLine.length + 1;
2388
- }
2389
- const targetLine = Math.max(0, Math.min(visualLine, allVisualLines.length - 1));
2390
- const vl = allVisualLines[targetLine];
2391
- const col = Math.min(visualCol, vl.text.length);
2392
- return vl.startOffset + col;
2393
- }
2394
- function cursorToLineCol(text, pos) {
2395
- const lines = text.split("\n");
2396
- let remaining = pos;
2397
- for (let i = 0; i < lines.length; i++) {
2398
- if (remaining <= lines[i].length) {
2399
- return { line: i, col: remaining, lines };
2400
- }
2401
- remaining -= lines[i].length + 1;
2402
- }
2403
- const last = lines.length - 1;
2404
- return { line: last, col: lines[last].length, lines };
2405
- }
2406
- function lineColToCursor(lines, line, col) {
2407
- let pos = 0;
2408
- for (let i = 0; i < line && i < lines.length; i++) {
2409
- pos += lines[i].length + 1;
2410
- }
2411
- return pos + Math.min(col, lines[line]?.length ?? 0);
2412
- }
2413
- function Input(props) {
2414
- const {
2415
- value: controlledValue,
2416
- defaultValue = "",
2417
- onChange,
2418
- onKeyPress,
2419
- onBeforeChange,
2420
- placeholder,
2421
- style,
2422
- focusedStyle,
2423
- multiline,
2424
- autoFocus,
2425
- type = "text"
2426
- } = props;
2427
- const [internalValue, setInternalValue] = React15.useState(defaultValue);
2428
- const [cursorPos, setCursorPos] = React15.useState(defaultValue.length);
2429
- const [innerWidth, setInnerWidth] = React15.useState(0);
2430
- const [isFocused, setIsFocused] = React15.useState(false);
2431
- const [nodeReady, setNodeReady] = React15.useState(false);
2432
- const inputCtx = React15.useContext(InputContext);
2433
- const focusCtx = React15.useContext(FocusContext);
2434
- const layoutCtx = React15.useContext(LayoutContext);
2435
- const nodeRef = React15.useRef(null);
2436
- const focusIdRef = React15.useRef(null);
2437
- const isControlled = controlledValue !== void 0;
2438
- const value = isControlled ? controlledValue : internalValue;
2439
- React15.useEffect(() => {
2440
- if (!layoutCtx || !nodeRef.current) return;
2441
- const layout = layoutCtx.getLayout(nodeRef.current);
2442
- setInnerWidth(layout.innerWidth);
2443
- return layoutCtx.subscribe(nodeRef.current, (rect) => {
2444
- setInnerWidth(rect.innerWidth);
2445
- });
2446
- }, [layoutCtx]);
2447
- const workingValueRef = React15.useRef(value);
2448
- const workingCursorRef = React15.useRef(cursorPos);
2449
- React15.useEffect(() => {
2450
- workingValueRef.current = value;
2451
- if (workingCursorRef.current > value.length) {
2452
- workingCursorRef.current = value.length;
2453
- setCursorPos(value.length);
2454
- }
2455
- }, [value]);
2456
- React15.useEffect(() => {
2457
- workingCursorRef.current = cursorPos;
2458
- }, [cursorPos]);
2459
- const stateRef = React15.useRef({
2460
- isControlled,
2461
- onChange,
2462
- onKeyPress,
2463
- onBeforeChange,
2464
- multiline: multiline ?? false,
2465
- innerWidth,
2466
- type
2467
- });
2468
- stateRef.current = {
2469
- isControlled,
2470
- onChange,
2471
- onKeyPress,
2472
- onBeforeChange,
2473
- multiline: multiline ?? false,
2474
- innerWidth,
2475
- type
2476
- };
2477
- React15.useEffect(() => {
2478
- if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
2479
- return focusCtx.register(focusIdRef.current, nodeRef.current);
2480
- }, [focusCtx, nodeReady]);
2481
- const autoFocusedRef = React15.useRef(false);
2482
- React15.useEffect(() => {
2483
- if (autoFocus && !autoFocusedRef.current && focusCtx && focusIdRef.current) {
2484
- autoFocusedRef.current = true;
2485
- const fid = focusIdRef.current;
2486
- queueMicrotask(() => {
2487
- focusCtx.requestFocus(fid);
2488
- });
2489
- }
2490
- }, [autoFocus, focusCtx, nodeReady]);
2491
- React15.useEffect(() => {
2492
- if (!focusCtx || !focusIdRef.current) return;
2493
- const fid = focusIdRef.current;
2494
- setIsFocused(focusCtx.focusedId === fid);
2495
- return focusCtx.onFocusChange((newId) => {
2496
- setIsFocused(newId === fid);
2497
- });
2498
- }, [focusCtx, nodeReady]);
2499
- React15.useEffect(() => {
2500
- if (!inputCtx || !focusIdRef.current) return;
2501
- const fid = focusIdRef.current;
2502
- const handler = (key) => {
2503
- const {
2504
- isControlled: ctrl,
2505
- onChange: cb,
2506
- onKeyPress: onKey,
2507
- onBeforeChange: onBefore,
2508
- multiline: ml
2509
- } = stateRef.current;
2510
- if (onKey?.(key) === true) {
2511
- return true;
2512
- }
2513
- const val = workingValueRef.current;
2514
- const pos = workingCursorRef.current;
2515
- if (key.name === "escape") return false;
2516
- const updateValue = (newVal, newCursor) => {
2517
- let finalVal = newVal;
2518
- let finalCursor = newCursor;
2519
- if (onBefore) {
2520
- const result = onBefore(newVal, val);
2521
- if (result === false) {
2522
- return;
2523
- }
2524
- if (typeof result === "string") {
2525
- finalVal = result;
2526
- finalCursor = result.length;
2527
- }
2528
- }
2529
- workingValueRef.current = finalVal;
2530
- workingCursorRef.current = finalCursor;
2531
- if (!ctrl) setInternalValue(finalVal);
2532
- cb?.(finalVal);
2533
- setCursorPos(finalCursor);
2534
- };
2535
- const updateCursor = (newCursor) => {
2536
- workingCursorRef.current = newCursor;
2537
- setCursorPos(newCursor);
2538
- };
2539
- if (key.name === "return") {
2540
- if (ml) {
2541
- const newVal = val.slice(0, pos) + "\n" + val.slice(pos);
2542
- updateValue(newVal, pos + 1);
2543
- return true;
2544
- }
2545
- return false;
2546
- }
2547
- if (key.ctrl) {
2548
- if (key.name === "w") {
2549
- if (pos > 0) {
2550
- let i = pos;
2551
- while (i > 0 && val[i - 1] === " ") i--;
2552
- while (i > 0 && val[i - 1] !== " " && (!ml || val[i - 1] !== "\n"))
2553
- i--;
2554
- const newVal = val.slice(0, i) + val.slice(pos);
2555
- updateValue(newVal, i);
2556
- }
2557
- return true;
2558
- }
2559
- if (key.name === "a") {
2560
- if (ml) {
2561
- const { line, lines } = cursorToLineCol(val, pos);
2562
- updateCursor(lineColToCursor(lines, line, 0));
2563
- } else {
2564
- updateCursor(0);
2565
- }
2566
- return true;
2567
- }
2568
- if (key.name === "e") {
2569
- if (ml) {
2570
- const { line, lines } = cursorToLineCol(val, pos);
2571
- updateCursor(lineColToCursor(lines, line, lines[line].length));
2572
- } else {
2573
- updateCursor(val.length);
2574
- }
2575
- return true;
2576
- }
2577
- if (key.name === "k") {
2578
- if (ml) {
2579
- const { line, lines } = cursorToLineCol(val, pos);
2580
- const lineEnd = lineColToCursor(lines, line, lines[line].length);
2581
- if (pos < lineEnd) {
2582
- const newVal = val.slice(0, pos) + val.slice(lineEnd);
2583
- updateValue(newVal, pos);
2584
- }
2585
- } else {
2586
- if (pos < val.length) {
2587
- const newVal = val.slice(0, pos);
2588
- updateValue(newVal, pos);
2589
- }
2590
- }
2591
- return true;
2592
- }
2593
- return false;
2594
- }
2595
- if (key.alt) {
2596
- if (key.name === "left" || key.name === "b") {
2597
- let i = pos;
2598
- while (i > 0 && val[i - 1] === " ") i--;
2599
- while (i > 0 && val[i - 1] !== " " && val[i - 1] !== "\n") i--;
2600
- updateCursor(i);
2601
- return true;
2602
- }
2603
- if (key.name === "right" || key.name === "f") {
2604
- let i = pos;
2605
- while (i < val.length && val[i] !== " " && val[i] !== "\n") i++;
2606
- while (i < val.length && val[i] === " ") i++;
2607
- updateCursor(i);
2608
- return true;
2609
- }
2610
- if (key.name === "backspace" || key.name === "d") {
2611
- if (key.name === "backspace") {
2612
- if (pos > 0) {
2613
- let i = pos;
2614
- while (i > 0 && val[i - 1] === " ") i--;
2615
- while (i > 0 && val[i - 1] !== " " && val[i - 1] !== "\n") i--;
2616
- const newVal = val.slice(0, i) + val.slice(pos);
2617
- updateValue(newVal, i);
2618
- }
2619
- return true;
2620
- } else {
2621
- if (pos < val.length) {
2622
- let i = pos;
2623
- while (i < val.length && val[i] !== " " && val[i] !== "\n") i++;
2624
- while (i < val.length && val[i] === " ") i++;
2625
- const newVal = val.slice(0, pos) + val.slice(i);
2626
- updateValue(newVal, pos);
2627
- }
2628
- return true;
2629
- }
2630
- }
2631
- return false;
2632
- }
2633
- if (key.name === "left") {
2634
- updateCursor(Math.max(0, pos - 1));
2635
- return true;
2636
- }
2637
- if (key.name === "right") {
2638
- updateCursor(Math.min(val.length, pos + 1));
2639
- return true;
2640
- }
2641
- if (key.name === "up") {
2642
- const { innerWidth: w } = stateRef.current;
2643
- const info = cursorToVisualLine(val, pos, w);
2644
- if (info.visualLine > 0) {
2645
- updateCursor(visualLineToCursor(val, info.visualLine - 1, info.visualCol, w));
2646
- }
2647
- return true;
2648
- }
2649
- if (key.name === "down") {
2650
- const { innerWidth: w } = stateRef.current;
2651
- const info = cursorToVisualLine(val, pos, w);
2652
- if (info.visualLine < info.totalVisualLines - 1) {
2653
- updateCursor(visualLineToCursor(val, info.visualLine + 1, info.visualCol, w));
2654
- }
2655
- return true;
2656
- }
2657
- if (key.name === "home") {
2658
- if (ml) {
2659
- const { line, lines } = cursorToLineCol(val, pos);
2660
- updateCursor(lineColToCursor(lines, line, 0));
2661
- } else {
2662
- updateCursor(0);
2663
- }
2664
- return true;
2665
- }
2666
- if (key.name === "end") {
2667
- if (ml) {
2668
- const { line, lines } = cursorToLineCol(val, pos);
2669
- updateCursor(lineColToCursor(lines, line, lines[line].length));
2670
- } else {
2671
- updateCursor(val.length);
2672
- }
2673
- return true;
2674
- }
2675
- if (key.name === "backspace") {
2676
- if (pos > 0) {
2677
- const newVal = val.slice(0, pos - 1) + val.slice(pos);
2678
- updateValue(newVal, pos - 1);
2679
- }
2680
- return true;
2681
- }
2682
- if (key.name === "delete") {
2683
- if (pos < val.length) {
2684
- const newVal = val.slice(0, pos) + val.slice(pos + 1);
2685
- updateValue(newVal, pos);
2686
- }
2687
- return true;
2688
- }
2689
- if (key.name.length > 1) return false;
2690
- const ch = key.sequence;
2691
- if (ch.length === 1 && ch.charCodeAt(0) >= 32) {
2692
- const { type: inputType } = stateRef.current;
2693
- if (inputType === "number") {
2694
- const isDigit = /[0-9]/.test(ch);
2695
- const isDecimal = ch === "." && !val.includes(".");
2696
- const isMinus = ch === "-" && pos === 0 && !val.includes("-");
2697
- if (!isDigit && !isDecimal && !isMinus) {
2698
- return true;
2699
- }
2700
- }
2701
- const newVal = val.slice(0, pos) + ch + val.slice(pos);
2702
- updateValue(newVal, pos + 1);
2703
- return true;
2704
- }
2705
- return false;
2706
- };
2707
- return inputCtx.registerInputHandler(fid, handler);
2708
- }, [inputCtx, nodeReady]);
2709
- const mergedStyle = {
2710
- ...style,
2711
- ...isFocused && focusedStyle ? focusedStyle : {}
2712
- };
2713
- return React15__default.default.createElement("input", {
2714
- style: mergedStyle,
2715
- value,
2716
- defaultValue,
2717
- placeholder,
2718
- onChange,
2719
- cursorPosition: cursorPos,
2720
- multiline: multiline ?? false,
2721
- focused: isFocused,
2722
- ref: (node) => {
2723
- if (node) {
2724
- nodeRef.current = node;
2725
- focusIdRef.current = node.focusId;
2726
- setNodeReady(true);
2727
- } else {
2728
- nodeRef.current = null;
2729
- focusIdRef.current = null;
2730
- setNodeReady(false);
2731
- }
2732
- }
2733
- });
2734
- }
2735
- function FocusScope({ trap = false, children }) {
2736
- const focusCtx = React15.useContext(FocusContext);
2737
- const prevFocusRef = React15.useRef(null);
2738
- const scopeIdsRef = React15.useRef(/* @__PURE__ */ new Set());
2739
- React15.useLayoutEffect(() => {
2740
- if (!trap || !focusCtx) return;
2741
- prevFocusRef.current = focusCtx.focusedId;
2742
- const cleanup = focusCtx.pushTrap(scopeIdsRef.current);
2743
- return () => {
2744
- cleanup();
2745
- if (prevFocusRef.current) {
2746
- focusCtx.requestFocus(prevFocusRef.current);
2747
- }
2748
- };
2749
- }, [trap, focusCtx]);
2750
- React15.useEffect(() => {
2751
- if (!trap || !focusCtx) return;
2752
- if (scopeIdsRef.current.size > 0) {
2753
- const firstId = scopeIdsRef.current.values().next().value;
2754
- if (firstId) focusCtx.requestFocus(firstId);
2755
- }
2756
- }, [trap, focusCtx]);
2757
- return React15__default.default.createElement(React15__default.default.Fragment, null, children);
2758
- }
2759
- function Spacer({ size }) {
2760
- return React15__default.default.createElement("box", {
2761
- style: { flexGrow: size ?? 1 }
2762
- });
2763
- }
2764
- function parseKeyDescriptor(descriptor) {
2765
- const parts = descriptor.toLowerCase().split("+");
2766
- const name = parts[parts.length - 1];
2767
- return {
2768
- name,
2769
- ctrl: parts.includes("ctrl"),
2770
- alt: parts.includes("alt"),
2771
- shift: parts.includes("shift"),
2772
- meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("super") || parts.includes("win")
2773
- };
2774
- }
2775
- function matchesKey(matcher, key) {
2776
- if (key.name !== matcher.name) return false;
2777
- if (matcher.ctrl !== !!key.ctrl) return false;
2778
- if (matcher.alt !== !!key.alt) return false;
2779
- if (matcher.shift !== !!key.shift) return false;
2780
- if (matcher.meta !== !!key.meta) return false;
2781
- return true;
2782
- }
2783
- function Keybind({
2784
- keypress,
2785
- onPress,
2786
- whenFocused,
2787
- priority,
2788
- disabled
2789
- }) {
2790
- const inputCtx = React15.useContext(InputContext);
2791
- const focusCtx = React15.useContext(FocusContext);
2792
- const onPressRef = React15.useRef(onPress);
2793
- onPressRef.current = onPress;
2794
- const matcherRef = React15.useRef(parseKeyDescriptor(keypress));
2795
- matcherRef.current = parseKeyDescriptor(keypress);
2796
- React15.useEffect(() => {
2797
- if (!inputCtx || disabled) return;
2798
- if (priority) {
2799
- const handler = (key) => {
2800
- if (!matchesKey(matcherRef.current, key)) return false;
2801
- if (whenFocused && focusCtx?.focusedId !== whenFocused) return false;
2802
- onPressRef.current();
2803
- return true;
2804
- };
2805
- return inputCtx.subscribePriority(handler);
2806
- } else {
2807
- const handler = (key) => {
2808
- if (!matchesKey(matcherRef.current, key)) return;
2809
- if (whenFocused && focusCtx?.focusedId !== whenFocused) return;
2810
- onPressRef.current();
2811
- };
2812
- return inputCtx.subscribe(handler);
2813
- }
2814
- }, [inputCtx, focusCtx, whenFocused, priority, disabled]);
2815
- return null;
2816
- }
2817
- function Portal({ children, zIndex = 1e3 }) {
2818
- return React15__default.default.createElement(
2819
- "box",
2820
- {
2821
- style: {
2822
- position: "absolute",
2823
- top: 0,
2824
- left: 0,
2825
- width: "100%",
2826
- height: "100%",
2827
- zIndex
2828
- }
2829
- },
2830
- children
2831
- );
2832
- }
2833
- function Button({
2834
- onPress,
2835
- style,
2836
- focusedStyle,
2837
- children,
2838
- disabled
2839
- }) {
2840
- const focusCtx = React15.useContext(FocusContext);
2841
- const inputCtx = React15.useContext(InputContext);
2842
- const nodeRef = React15.useRef(null);
2843
- const focusIdRef = React15.useRef(null);
2844
- const onPressRef = React15.useRef(onPress);
2845
- onPressRef.current = onPress;
2846
- const [nodeReady, setNodeReady] = React15.useState(false);
2847
- const [isFocused, setIsFocused] = React15.useState(false);
2848
- React15.useEffect(() => {
2849
- if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
2850
- return focusCtx.register(focusIdRef.current, nodeRef.current);
2851
- }, [focusCtx, disabled, nodeReady]);
2852
- React15.useEffect(() => {
2853
- if (!focusCtx || !focusIdRef.current) return;
2854
- const fid = focusIdRef.current;
2855
- setIsFocused(focusCtx.focusedId === fid);
2856
- return focusCtx.onFocusChange((newId) => {
2857
- setIsFocused(newId === fid);
2858
- });
2859
- }, [focusCtx, nodeReady]);
2860
- React15.useEffect(() => {
2861
- if (!inputCtx || !focusIdRef.current || disabled) return;
2862
- const fid = focusIdRef.current;
2863
- const handler = (key) => {
2864
- if (focusCtx?.focusedId !== fid) return false;
2865
- if (key.name === "return" || key.name === " " || key.sequence === " ") {
2866
- onPressRef.current?.();
2867
- return true;
2868
- }
2869
- return false;
2870
- };
2871
- return inputCtx.registerInputHandler(fid, handler);
2872
- }, [inputCtx, focusCtx, disabled, nodeReady]);
2873
- const mergedStyle = {
2874
- ...style,
2875
- ...isFocused && focusedStyle ? focusedStyle : {}
2876
- };
2877
- return React15__default.default.createElement(
2878
- "box",
2879
- {
2880
- style: mergedStyle,
2881
- focusable: !disabled,
2882
- ref: (node) => {
2883
- if (node) {
2884
- nodeRef.current = node;
2885
- focusIdRef.current = node.focusId;
2886
- setNodeReady(true);
2887
- } else {
2888
- nodeRef.current = null;
2889
- focusIdRef.current = null;
2890
- setNodeReady(false);
2891
- }
2892
- }
2893
- },
2894
- children
2895
- );
2896
- }
2897
- var DEFAULT_RECT = {
2898
- x: 0,
2899
- y: 0,
2900
- width: 0,
2901
- height: 0,
2902
- innerX: 0,
2903
- innerY: 0,
2904
- innerWidth: 0,
2905
- innerHeight: 0
2906
- };
2907
- function useLayout(nodeRef) {
2908
- const ctx = React15.useContext(LayoutContext);
2909
- const [layout, setLayout] = React15.useState(DEFAULT_RECT);
2910
- React15.useEffect(() => {
2911
- if (!ctx || !nodeRef?.current) return;
2912
- setLayout(ctx.getLayout(nodeRef.current));
2913
- return ctx.subscribe(nodeRef.current, setLayout);
2914
- }, [ctx, nodeRef]);
2915
- return layout;
2916
- }
2917
- function useInput(handler, deps = []) {
2918
- const ctx = React15.useContext(InputContext);
2919
- React15.useEffect(() => {
2920
- if (!ctx) return;
2921
- return ctx.subscribe(handler);
2922
- }, [ctx, ...deps]);
2923
- }
2924
-
2925
- // src/components/ScrollView.tsx
2926
- function ScrollView({
2927
- children,
2928
- style,
2929
- scrollOffset: controlledOffset,
2930
- onScroll,
2931
- defaultScrollOffset = 0,
2932
- scrollStep = 1,
2933
- disableKeyboard,
2934
- scrollToFocus = true,
2935
- showScrollbar = true,
2936
- focusable = true,
2937
- focusedStyle
2938
- }) {
2939
- const isControlled = controlledOffset !== void 0;
2940
- const [internalOffset, setInternalOffset] = React15.useState(defaultScrollOffset);
2941
- const offset = isControlled ? controlledOffset : internalOffset;
2942
- const viewportRef = React15.useRef(null);
2943
- const contentRef = React15.useRef(null);
2944
- const viewportLayout = useLayout(viewportRef);
2945
- const contentLayout = useLayout(contentRef);
2946
- const focusCtx = React15.useContext(FocusContext);
2947
- const layoutCtx = React15.useContext(LayoutContext);
2948
- const focusIdRef = React15.useRef(null);
2949
- if (focusable && !focusIdRef.current) {
2950
- focusIdRef.current = `scrollview-${Math.random().toString(36).slice(2, 9)}`;
2951
- }
2952
- const focusId = focusable ? focusIdRef.current : null;
2953
- React15.useEffect(() => {
2954
- if (!focusable || !focusId || !focusCtx || !viewportRef.current) return;
2955
- return focusCtx.register(focusId, viewportRef.current);
2956
- }, [focusable, focusId, focusCtx]);
2957
- const isSelfFocused = focusable && focusId && focusCtx?.focusedId === focusId;
2958
- const viewportHeight = viewportLayout.innerHeight;
2959
- const contentHeight = contentLayout.height;
2960
- const maxOffset = Math.max(0, contentHeight - viewportHeight);
2961
- const effectiveOffset = Math.max(0, Math.min(offset, maxOffset));
2962
- const setOffset = React15.useCallback(
2963
- (next) => {
2964
- const clamped = Math.max(0, Math.min(next, maxOffset));
2965
- if (isControlled) {
2966
- onScroll?.(clamped);
2967
- } else {
2968
- setInternalOffset(clamped);
2969
- }
2970
- },
2971
- [isControlled, onScroll, maxOffset]
2972
- );
2973
- React15.useEffect(() => {
2974
- if (offset > maxOffset && maxOffset >= 0) {
2975
- setOffset(maxOffset);
2976
- }
2977
- }, [offset, maxOffset, setOffset]);
2978
- React15.useEffect(() => {
2979
- if (!scrollToFocus || !focusCtx || !layoutCtx || !contentRef.current) return;
2980
- const unsubscribe = focusCtx.onFocusChange((focusedId) => {
2981
- if (!focusedId || !contentRef.current) return;
2982
- const findNode = (node) => {
2983
- if (node.focusId === focusedId) return node;
2984
- for (const child of node.children) {
2985
- const found = findNode(child);
2986
- if (found) return found;
2987
- }
2988
- return null;
2989
- };
2990
- const focusedNode = findNode(contentRef.current);
2991
- if (!focusedNode) return;
2992
- const focusedLayout = layoutCtx.getLayout(focusedNode);
2993
- const contentTopY = contentRef.current.layout?.y ?? 0;
2994
- const elementTop = focusedLayout.y - contentTopY;
2995
- const elementBottom = elementTop + focusedLayout.height;
2996
- const visibleTop = offset;
2997
- const visibleBottom = offset + viewportHeight;
2998
- if (elementTop < visibleTop) {
2999
- setOffset(elementTop);
3000
- } else if (elementBottom > visibleBottom) {
3001
- setOffset(elementBottom - viewportHeight);
3002
- }
3003
- });
3004
- return unsubscribe;
3005
- }, [scrollToFocus, focusCtx, layoutCtx, offset, viewportHeight, setOffset]);
3006
- const containsFocus = React15.useCallback(() => {
3007
- if (!focusCtx) return false;
3008
- const currentFocusId = focusCtx.focusedId;
3009
- if (!currentFocusId) return false;
3010
- if (focusable && focusId && currentFocusId === focusId) return true;
3011
- if (!contentRef.current) return false;
3012
- const findNode = (node) => {
3013
- if (node.focusId === currentFocusId) return true;
3014
- for (const child of node.children) {
3015
- if (findNode(child)) return true;
3016
- }
3017
- return false;
3018
- };
3019
- return findNode(contentRef.current);
3020
- }, [focusCtx, focusable, focusId]);
3021
- useInput((key) => {
3022
- if (disableKeyboard) return;
3023
- if (!containsFocus()) return;
3024
- const halfPage = Math.max(1, Math.floor(viewportHeight / 2));
3025
- const fullPage = Math.max(1, viewportHeight);
3026
- switch (key.name) {
3027
- // Page keys - always safe, inputs don't use these
3028
- case "pageup":
3029
- setOffset(offset - fullPage);
3030
- break;
3031
- case "pagedown":
3032
- setOffset(offset + fullPage);
3033
- break;
3034
- default:
3035
- if (key.ctrl) {
3036
- if (key.name === "d") {
3037
- setOffset(offset + halfPage);
3038
- } else if (key.name === "u") {
3039
- setOffset(offset - halfPage);
3040
- } else if (key.name === "f") {
3041
- setOffset(offset + fullPage);
3042
- } else if (key.name === "b") {
3043
- setOffset(offset - fullPage);
3044
- }
3045
- }
3046
- break;
3047
- }
3048
- }, [offset, scrollStep, viewportHeight, maxOffset, disableKeyboard, setOffset, containsFocus]);
3049
- const {
3050
- padding: _pad,
3051
- paddingX: _px,
3052
- paddingY: _py,
3053
- paddingTop: _pt,
3054
- paddingRight: _pr,
3055
- paddingBottom: _pb,
3056
- paddingLeft: _pl,
3057
- ...styleRest
3058
- } = style ?? {};
3059
- const hasBorder = styleRest.border != null && styleRest.border !== "none";
3060
- const borderHeight = hasBorder ? 2 : 0;
3061
- const intrinsicHeight = contentHeight > 0 ? contentHeight + borderHeight : void 0;
3062
- const outerStyle = {
3063
- ...styleRest,
3064
- ...isSelfFocused ? focusedStyle : {},
3065
- clip: true,
3066
- // Only set intrinsic height if user didn't set explicit height
3067
- ...styleRest.height === void 0 && intrinsicHeight !== void 0 ? {
3068
- height: intrinsicHeight,
3069
- flexShrink: styleRest.flexShrink ?? 1,
3070
- minHeight: styleRest.minHeight ?? 0
3071
- } : {}
3072
- };
3073
- const innerStyle = {
3074
- position: "absolute",
3075
- top: -effectiveOffset,
3076
- left: 0,
3077
- right: 0,
3078
- flexDirection: "column",
3079
- ..._pad !== void 0 && { padding: _pad },
3080
- ..._px !== void 0 && { paddingX: _px },
3081
- ..._py !== void 0 && { paddingY: _py },
3082
- ..._pt !== void 0 && { paddingTop: _pt },
3083
- ..._pr !== void 0 && { paddingRight: _pr },
3084
- ..._pb !== void 0 && { paddingBottom: _pb },
3085
- ..._pl !== void 0 && { paddingLeft: _pl }
3086
- };
3087
- const isScrollable = contentHeight > viewportHeight && viewportHeight > 0;
3088
- const scrollbarVisible = showScrollbar && isScrollable;
3089
- const thumbHeight = Math.max(1, Math.floor(viewportHeight / contentHeight * viewportHeight));
3090
- const scrollableRange = contentHeight - viewportHeight;
3091
- const thumbPosition = scrollableRange > 0 ? Math.floor(effectiveOffset / scrollableRange * (viewportHeight - thumbHeight)) : 0;
3092
- const scrollbarChars = [];
3093
- if (scrollbarVisible) {
3094
- for (let i = 0; i < viewportHeight; i++) {
3095
- if (i >= thumbPosition && i < thumbPosition + thumbHeight) {
3096
- scrollbarChars.push("\u2588");
3097
- } else {
3098
- scrollbarChars.push("\u2591");
3099
- }
3100
- }
3101
- }
3102
- const scrollbarStyle = {
3103
- position: "absolute",
3104
- top: 0,
3105
- right: 0,
3106
- width: 1,
3107
- height: viewportHeight,
3108
- flexDirection: "column"
3109
- };
3110
- return React15__default.default.createElement(
3111
- "box",
3112
- {
3113
- style: outerStyle,
3114
- ref: (node) => {
3115
- viewportRef.current = node ?? null;
3116
- },
3117
- ...focusable ? { focusable: true, focusId } : {}
3118
- },
3119
- // Content (absolutely positioned, scrolls via top offset)
3120
- React15__default.default.createElement(
3121
- "box",
3122
- {
3123
- style: {
3124
- ...innerStyle,
3125
- // Reserve space for scrollbar when visible
3126
- paddingRight: scrollbarVisible ? (innerStyle.paddingRight ?? 0) + 1 : innerStyle.paddingRight
3127
- },
3128
- ref: (node) => {
3129
- contentRef.current = node ?? null;
3130
- }
3131
- },
3132
- children
3133
- ),
3134
- // Scrollbar
3135
- scrollbarVisible && React15__default.default.createElement(
3136
- "box",
3137
- { style: scrollbarStyle },
3138
- React15__default.default.createElement(
3139
- "text",
3140
- { style: { color: "blackBright" } },
3141
- scrollbarChars.join("\n")
3142
- )
3143
- )
3144
- );
3145
- }
3146
- function List({
3147
- count,
3148
- renderItem,
3149
- selectedIndex: controlledIndex,
3150
- onSelectionChange,
3151
- onSelect,
3152
- defaultSelectedIndex = 0,
3153
- disabledIndices,
3154
- style,
3155
- focusable = true
3156
- }) {
3157
- const isControlled = controlledIndex !== void 0;
3158
- const [internalIndex, setInternalIndex] = React15.useState(defaultSelectedIndex);
3159
- const selectedIndex = isControlled ? controlledIndex : internalIndex;
3160
- const focusCtx = React15.useContext(FocusContext);
3161
- const inputCtx = React15.useContext(InputContext);
3162
- const nodeRef = React15.useRef(null);
3163
- const focusIdRef = React15.useRef(null);
3164
- const onSelectRef = React15.useRef(onSelect);
3165
- onSelectRef.current = onSelect;
3166
- const [nodeReady, setNodeReady] = React15.useState(false);
3167
- const [isFocused, setIsFocused] = React15.useState(false);
3168
- const lastKeyRef = React15.useRef(null);
3169
- const setIndex = React15.useCallback(
3170
- (next) => {
3171
- const clamped = Math.max(0, Math.min(next, count - 1));
3172
- if (isControlled) {
3173
- onSelectionChange?.(clamped);
3174
- } else {
3175
- setInternalIndex(clamped);
3176
- }
3177
- },
3178
- [isControlled, onSelectionChange, count]
3179
- );
3180
- const findNextEnabled = React15.useCallback(
3181
- (from, direction) => {
3182
- if (!disabledIndices || disabledIndices.size === 0) {
3183
- return Math.max(0, Math.min(from + direction, count - 1));
3184
- }
3185
- let next = from + direction;
3186
- while (next >= 0 && next < count && disabledIndices.has(next)) {
3187
- next += direction;
3188
- }
3189
- if (next < 0 || next >= count) return from;
3190
- return next;
3191
- },
3192
- [disabledIndices, count]
3193
- );
3194
- React15.useEffect(() => {
3195
- if (!focusCtx || !focusIdRef.current || !nodeRef.current || !focusable) return;
3196
- return focusCtx.register(focusIdRef.current, nodeRef.current);
3197
- }, [focusCtx, focusable, nodeReady]);
3198
- React15.useEffect(() => {
3199
- if (!focusCtx || !focusIdRef.current) return;
3200
- const fid = focusIdRef.current;
3201
- setIsFocused(focusCtx.focusedId === fid);
3202
- return focusCtx.onFocusChange((newId) => {
3203
- setIsFocused(newId === fid);
3204
- });
3205
- }, [focusCtx, nodeReady]);
3206
- const findFirstEnabled = React15.useCallback(
3207
- (fromEnd) => {
3208
- const start = fromEnd ? count - 1 : 0;
3209
- const direction = fromEnd ? -1 : 1;
3210
- let index = start;
3211
- while (index >= 0 && index < count && disabledIndices?.has(index)) {
3212
- index += direction;
3213
- }
3214
- return index >= 0 && index < count ? index : fromEnd ? count - 1 : 0;
3215
- },
3216
- [disabledIndices, count]
3217
- );
3218
- React15.useEffect(() => {
3219
- if (!inputCtx || !focusIdRef.current || !focusable) return;
3220
- const fid = focusIdRef.current;
3221
- const handler = (key) => {
3222
- if (focusCtx?.focusedId !== fid) return false;
3223
- if (key.name === "g" && !key.ctrl && !key.alt) {
3224
- if (lastKeyRef.current === "g") {
3225
- setIndex(findFirstEnabled(false));
3226
- lastKeyRef.current = null;
3227
- return true;
3228
- }
3229
- lastKeyRef.current = "g";
3230
- return true;
3231
- }
3232
- if (key.name === "G" || key.name === "g" && key.shift) {
3233
- lastKeyRef.current = null;
3234
- setIndex(findFirstEnabled(true));
3235
- return true;
3236
- }
3237
- lastKeyRef.current = null;
3238
- if (key.name === "up" || key.name === "k") {
3239
- setIndex(findNextEnabled(selectedIndex, -1));
3240
- return true;
3241
- }
3242
- if (key.name === "down" || key.name === "j") {
3243
- setIndex(findNextEnabled(selectedIndex, 1));
3244
- return true;
3245
- }
3246
- if (key.name === "return") {
3247
- if (!disabledIndices?.has(selectedIndex)) {
3248
- onSelectRef.current?.(selectedIndex);
3249
- }
3250
- return true;
3251
- }
3252
- return false;
3253
- };
3254
- return inputCtx.registerInputHandler(fid, handler);
3255
- }, [inputCtx, focusCtx, focusable, selectedIndex, setIndex, findNextEnabled, findFirstEnabled, disabledIndices, nodeReady]);
3256
- const items = [];
3257
- for (let i = 0; i < count; i++) {
3258
- items.push(
3259
- React15__default.default.createElement(
3260
- React15__default.default.Fragment,
3261
- { key: i },
3262
- renderItem({ index: i, selected: i === selectedIndex, focused: isFocused })
3263
- )
3264
- );
3265
- }
3266
- return React15__default.default.createElement(
3267
- "box",
3268
- {
3269
- style: { flexDirection: "column", ...style },
3270
- focusable,
3271
- ref: (node) => {
3272
- if (node) {
3273
- nodeRef.current = node;
3274
- focusIdRef.current = node.focusId;
3275
- setNodeReady(true);
3276
- } else {
3277
- nodeRef.current = null;
3278
- focusIdRef.current = null;
3279
- setNodeReady(false);
3280
- }
3281
- }
3282
- },
3283
- ...items
3284
- );
3285
- }
3286
- function Menu({
3287
- items,
3288
- selectedIndex,
3289
- onSelectionChange,
3290
- onSelect,
3291
- defaultSelectedIndex = 0,
3292
- style,
3293
- highlightColor = "cyan",
3294
- focusable = true
3295
- }) {
3296
- const disabledIndices = /* @__PURE__ */ new Set();
3297
- for (let i = 0; i < items.length; i++) {
3298
- if (items[i].disabled) disabledIndices.add(i);
3299
- }
3300
- const handleSelect = (index) => {
3301
- const item = items[index];
3302
- if (item && !item.disabled) {
3303
- onSelect?.(item.value, index);
3304
- }
3305
- };
3306
- return React15__default.default.createElement(List, {
3307
- count: items.length,
3308
- selectedIndex,
3309
- onSelectionChange,
3310
- onSelect: handleSelect,
3311
- defaultSelectedIndex,
3312
- disabledIndices: disabledIndices.size > 0 ? disabledIndices : void 0,
3313
- style,
3314
- focusable,
3315
- renderItem: ({ index, selected, focused }) => {
3316
- const item = items[index];
3317
- const isDisabled = item.disabled;
3318
- const isHighlighted = selected && focused;
3319
- const indicator = selected ? ">" : " ";
3320
- return React15__default.default.createElement(
3321
- "box",
3322
- {
3323
- style: {
3324
- flexDirection: "row",
3325
- ...isHighlighted ? { bg: highlightColor } : {}
3326
- }
3327
- },
3328
- React15__default.default.createElement(
3329
- "text",
3330
- {
3331
- style: isHighlighted ? { bold: true, color: "black" } : isDisabled ? { dim: true } : {}
3332
- },
3333
- `${indicator} ${item.label}`
3334
- )
3335
- );
3336
- }
3337
- });
3338
- }
3339
- function Progress({
3340
- value,
3341
- indeterminate = false,
3342
- width = "100%",
3343
- label,
3344
- showPercent = false,
3345
- style,
3346
- filled = "\u2588",
3347
- empty = "\u2591"
3348
- }) {
3349
- const trackRef = React15.useRef(null);
3350
- const trackLayout = useLayout(trackRef);
3351
- const trackWidth = trackLayout.innerWidth;
3352
- const [indeterminatePos, setIndeterminatePos] = React15.useState(0);
3353
- React15.useEffect(() => {
3354
- if (!indeterminate) return;
3355
- const timer = setInterval(() => {
3356
- setIndeterminatePos((p) => (p + 1) % Math.max(1, trackWidth + 6));
3357
- }, 100);
3358
- return () => clearInterval(timer);
3359
- }, [indeterminate, trackWidth]);
3360
- const clamped = Math.max(0, Math.min(1, value ?? 0));
3361
- const pctText = showPercent ? ` ${Math.round(clamped * 100)}%` : "";
3362
- let barText = "";
3363
- if (trackWidth > 0) {
3364
- if (indeterminate && value === void 0) {
3365
- const chunkSize = Math.max(1, Math.min(3, Math.floor(trackWidth / 4)));
3366
- const chars = [];
3367
- for (let i = 0; i < trackWidth; i++) {
3368
- if (i >= indeterminatePos - chunkSize && i < indeterminatePos) {
3369
- chars.push(filled);
3370
- } else {
3371
- chars.push(empty);
3372
- }
3373
- }
3374
- barText = chars.join("");
3375
- } else {
3376
- const filledCount = Math.round(clamped * trackWidth);
3377
- barText = filled.repeat(filledCount) + empty.repeat(trackWidth - filledCount);
3378
- }
3379
- }
3380
- const children = [];
3381
- if (label) {
3382
- children.push(
3383
- React15__default.default.createElement("text", { key: "label", style: { bold: true } }, label + " ")
3384
- );
3385
- }
3386
- children.push(
3387
- React15__default.default.createElement(
3388
- "box",
3389
- {
3390
- key: "track",
3391
- style: { flexGrow: 1, flexShrink: 1 },
3392
- ref: (node) => {
3393
- trackRef.current = node ?? null;
3394
- }
3395
- },
3396
- React15__default.default.createElement("text", { key: "bar", style: {} }, barText)
3397
- )
3398
- );
3399
- if (showPercent) {
3400
- children.push(
3401
- React15__default.default.createElement("text", { key: "pct", style: { bold: true } }, pctText)
3402
- );
3403
- }
3404
- return React15__default.default.createElement(
3405
- "box",
3406
- {
3407
- style: {
3408
- flexDirection: "row",
3409
- width,
3410
- ...style
3411
- }
3412
- },
3413
- ...children
3414
- );
3415
- }
3416
- var BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3417
- function Spinner({
3418
- frames = BRAILLE_FRAMES,
3419
- intervalMs = 80,
3420
- label,
3421
- style
3422
- }) {
3423
- const [frameIndex, setFrameIndex] = React15.useState(0);
3424
- React15.useEffect(() => {
3425
- const timer = setInterval(() => {
3426
- setFrameIndex((i) => (i + 1) % frames.length);
3427
- }, intervalMs);
3428
- return () => clearInterval(timer);
3429
- }, [frames.length, intervalMs]);
3430
- const children = [
3431
- React15__default.default.createElement("text", { key: "frame", style }, frames[frameIndex])
3432
- ];
3433
- if (label) {
3434
- children.push(
3435
- React15__default.default.createElement("text", { key: "label", style: {} }, " " + label)
3436
- );
3437
- }
3438
- return React15__default.default.createElement(
3439
- "box",
3440
- { style: { flexDirection: "row" } },
3441
- ...children
3442
- );
3443
- }
3444
- var ToastContext = React15.createContext(null);
3445
- var nextToastId = 0;
3446
- function useToast() {
3447
- const ctx = React15.useContext(ToastContext);
3448
- if (!ctx) throw new Error("useToast must be used within a <ToastHost>");
3449
- return ctx.push;
3450
- }
3451
- var VARIANT_COLORS = {
3452
- info: { bg: "blackBright", title: "cyanBright", text: "white" },
3453
- success: { bg: "blackBright", title: "greenBright", text: "white" },
3454
- warning: { bg: "blackBright", title: "yellowBright", text: "white" },
3455
- error: { bg: "blackBright", title: "redBright", text: "white" }
3456
- };
3457
- function ToastHost({
3458
- position = "bottom-right",
3459
- maxVisible = 5,
3460
- children
3461
- }) {
3462
- const [toasts, setToasts] = React15.useState([]);
3463
- const timersRef = React15.useRef(/* @__PURE__ */ new Map());
3464
- const push = React15.useCallback((toast) => {
3465
- const id = `toast-${nextToastId++}`;
3466
- const full = { id, durationMs: 3e3, variant: "info", ...toast };
3467
- setToasts((prev) => [...prev, full]);
3468
- if (full.durationMs && full.durationMs > 0) {
3469
- const timer = setTimeout(() => {
3470
- timersRef.current.delete(id);
3471
- setToasts((prev) => prev.filter((t) => t.id !== id));
3472
- }, full.durationMs);
3473
- timersRef.current.set(id, timer);
3474
- }
3475
- }, []);
3476
- React15.useEffect(() => {
3477
- return () => {
3478
- for (const timer of timersRef.current.values()) {
3479
- clearTimeout(timer);
3480
- }
3481
- timersRef.current.clear();
3482
- };
3483
- }, []);
3484
- const ctxValue = React15.useRef({ push });
3485
- ctxValue.current.push = push;
3486
- const isTop = position.startsWith("top");
3487
- const isRight = position.endsWith("right");
3488
- const portalStyle = {
3489
- position: "absolute",
3490
- top: 0,
3491
- left: 0,
3492
- width: "100%",
3493
- height: "100%",
3494
- zIndex: 900,
3495
- flexDirection: "column",
3496
- justifyContent: isTop ? "flex-start" : "flex-end",
3497
- alignItems: isRight ? "flex-end" : "flex-start",
3498
- padding: 1
3499
- };
3500
- const visible = toasts.slice(-maxVisible);
3501
- const toastElements = visible.map((toast) => {
3502
- const variant = toast.variant ?? "info";
3503
- const colors = VARIANT_COLORS[variant];
3504
- const innerChildren = [];
3505
- if (toast.title) {
3506
- innerChildren.push(
3507
- React15__default.default.createElement("text", {
3508
- key: "title",
3509
- style: { bold: true, color: colors.title }
3510
- }, toast.title)
3511
- );
3512
- }
3513
- innerChildren.push(
3514
- React15__default.default.createElement("text", {
3515
- key: "msg",
3516
- style: { color: colors.text }
3517
- }, toast.message)
3518
- );
3519
- return React15__default.default.createElement(
3520
- "box",
3521
- {
3522
- key: toast.id,
3523
- style: {
3524
- bg: colors.bg,
3525
- paddingX: 1,
3526
- flexDirection: "column",
3527
- minWidth: 20,
3528
- maxWidth: 50
3529
- }
3530
- },
3531
- ...innerChildren
3532
- );
3533
- });
3534
- return React15__default.default.createElement(
3535
- ToastContext.Provider,
3536
- { value: ctxValue.current },
3537
- children,
3538
- toastElements.length > 0 ? React15__default.default.createElement("box", { style: portalStyle }, ...toastElements) : null
3539
- );
3540
- }
3541
- function Select({
3542
- items,
3543
- value,
3544
- onChange,
3545
- placeholder = "Select...",
3546
- style,
3547
- focusedStyle,
3548
- dropdownStyle,
3549
- highlightColor = "cyan",
3550
- maxVisible = 8,
3551
- searchable = true,
3552
- disabled
3553
- }) {
3554
- const focusCtx = React15.useContext(FocusContext);
3555
- const inputCtx = React15.useContext(InputContext);
3556
- const appCtx = React15.useContext(AppContext);
3557
- const nodeRef = React15.useRef(null);
3558
- const focusIdRef = React15.useRef(null);
3559
- const onChangeRef = React15.useRef(onChange);
3560
- onChangeRef.current = onChange;
3561
- const [nodeReady, setNodeReady] = React15.useState(false);
3562
- const [isFocused, setIsFocused] = React15.useState(false);
3563
- const [isOpen, setIsOpen] = React15.useState(false);
3564
- const [highlightIndex, setHighlightIndex] = React15.useState(0);
3565
- const [searchText, setSearchText] = React15.useState("");
3566
- const [scrollOffset, setScrollOffset] = React15.useState(0);
3567
- const triggerLayout = useLayout(nodeRef);
3568
- const screenRows = appCtx?.rows ?? 24;
3569
- const selectedItem = items.find((item) => item.value === value);
3570
- const selectedLabel = selectedItem?.label ?? "";
3571
- const filteredItems = React15.useMemo(() => {
3572
- if (!searchText) return items;
3573
- const lower = searchText.toLowerCase();
3574
- return items.filter((item) => item.label.toLowerCase().includes(lower));
3575
- }, [items, searchText]);
3576
- const visibleCount = Math.min(maxVisible, filteredItems.length);
3577
- const visibleItems = filteredItems.slice(
3578
- scrollOffset,
3579
- scrollOffset + visibleCount
3580
- );
3581
- React15.useEffect(() => {
3582
- setHighlightIndex(0);
3583
- setScrollOffset(0);
3584
- }, [searchText]);
3585
- React15.useEffect(() => {
3586
- if (!isFocused && isOpen) {
3587
- setIsOpen(false);
3588
- setSearchText("");
3589
- }
3590
- }, [isFocused, isOpen]);
3591
- React15.useEffect(() => {
3592
- if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
3593
- return focusCtx.register(focusIdRef.current, nodeRef.current);
3594
- }, [focusCtx, nodeReady]);
3595
- React15.useEffect(() => {
3596
- if (!focusCtx || !focusIdRef.current) return;
3597
- focusCtx.setSkippable(focusIdRef.current, !!disabled);
3598
- }, [focusCtx, disabled, nodeReady]);
3599
- React15.useEffect(() => {
3600
- if (!focusCtx || !focusIdRef.current) return;
3601
- const fid = focusIdRef.current;
3602
- setIsFocused(focusCtx.focusedId === fid);
3603
- return focusCtx.onFocusChange((newId) => {
3604
- setIsFocused(newId === fid);
3605
- });
3606
- }, [focusCtx, nodeReady]);
3607
- const findNextEnabled = React15.useCallback(
3608
- (from, direction) => {
3609
- let next = from + direction;
3610
- while (next >= 0 && next < filteredItems.length) {
3611
- if (!filteredItems[next].disabled) return next;
3612
- next += direction;
3613
- }
3614
- return from;
3615
- },
3616
- [filteredItems]
3617
- );
3618
- const ensureVisible = React15.useCallback(
3619
- (index) => {
3620
- if (index < scrollOffset) {
3621
- setScrollOffset(index);
3622
- } else if (index >= scrollOffset + visibleCount) {
3623
- setScrollOffset(index - visibleCount + 1);
3624
- }
3625
- },
3626
- [scrollOffset, visibleCount]
3627
- );
3628
- React15.useEffect(() => {
3629
- if (!inputCtx || !focusIdRef.current || disabled) return;
3630
- const fid = focusIdRef.current;
3631
- const handler = (key) => {
3632
- if (focusCtx?.focusedId !== fid) return false;
3633
- if (!isOpen) {
3634
- if (key.name === "return" || key.name === " " || key.sequence === " " || key.name === "down") {
3635
- setIsOpen(true);
3636
- setSearchText("");
3637
- const idx = filteredItems.findIndex((item) => item.value === value);
3638
- const start = idx >= 0 ? idx : 0;
3639
- setHighlightIndex(start);
3640
- setScrollOffset(
3641
- Math.max(0, start - Math.floor(maxVisible / 2))
3642
- );
3643
- return true;
3644
- }
3645
- return false;
3646
- }
3647
- if (key.name === "tab") {
3648
- setIsOpen(false);
3649
- setSearchText("");
3650
- return false;
3651
- }
3652
- if (key.name === "escape") {
3653
- setIsOpen(false);
3654
- setSearchText("");
3655
- return true;
3656
- }
3657
- if (key.name === "return") {
3658
- const item = filteredItems[highlightIndex];
3659
- if (item && !item.disabled) {
3660
- onChangeRef.current?.(item.value);
3661
- setIsOpen(false);
3662
- setSearchText("");
3663
- }
3664
- return true;
3665
- }
3666
- if (key.name === "up") {
3667
- const next = findNextEnabled(highlightIndex, -1);
3668
- setHighlightIndex(next);
3669
- ensureVisible(next);
3670
- return true;
3671
- }
3672
- if (key.name === "down") {
3673
- const next = findNextEnabled(highlightIndex, 1);
3674
- setHighlightIndex(next);
3675
- ensureVisible(next);
3676
- return true;
3677
- }
3678
- if (key.name === "backspace") {
3679
- if (searchable && searchText.length > 0) {
3680
- setSearchText((prev) => prev.slice(0, -1));
3681
- }
3682
- return true;
3683
- }
3684
- if (key.name === "home") {
3685
- const first = findNextEnabled(-1, 1);
3686
- setHighlightIndex(first);
3687
- ensureVisible(first);
3688
- return true;
3689
- }
3690
- if (key.name === "end") {
3691
- const last = findNextEnabled(filteredItems.length, -1);
3692
- setHighlightIndex(last);
3693
- ensureVisible(last);
3694
- return true;
3695
- }
3696
- if (searchable && key.sequence && key.sequence.length === 1 && !key.ctrl && !key.alt) {
3697
- const ch = key.sequence;
3698
- if (ch >= " " && ch <= "~") {
3699
- setSearchText((prev) => prev + ch);
3700
- return true;
3701
- }
3702
- }
3703
- return true;
3704
- };
3705
- return inputCtx.registerInputHandler(fid, handler);
3706
- }, [
3707
- inputCtx,
3708
- focusCtx,
3709
- disabled,
3710
- isOpen,
3711
- highlightIndex,
3712
- filteredItems,
3713
- value,
3714
- maxVisible,
3715
- searchable,
3716
- searchText,
3717
- findNextEnabled,
3718
- ensureVisible,
3719
- nodeReady
3720
- ]);
3721
- const useDefaultBorder = !style?.bg && style?.border === void 0;
3722
- const triggerStyle = {
3723
- flexDirection: "row",
3724
- width: "100%",
3725
- ...useDefaultBorder ? { border: "single" } : {},
3726
- ...style,
3727
- ...isFocused && focusedStyle ? focusedStyle : {}
3728
- };
3729
- const labelColor = selectedLabel ? style?.color ?? void 0 : "blackBright";
3730
- const triggerChildren = [
3731
- React15__default.default.createElement(
3732
- "text",
3733
- {
3734
- key: "label",
3735
- style: {
3736
- flexGrow: 1,
3737
- flexShrink: 1,
3738
- color: labelColor,
3739
- wrap: "ellipsis",
3740
- ...selectedLabel ? {} : { dim: true }
3741
- }
3742
- },
3743
- selectedLabel || placeholder
3744
- ),
3745
- React15__default.default.createElement(
3746
- "text",
3747
- {
3748
- key: "arrow",
3749
- style: { flexShrink: 0, color: isFocused ? highlightColor : "blackBright" }
3750
- },
3751
- isOpen ? " \u25B2" : " \u25BC"
3752
- )
3753
- ];
3754
- let dropdownElement = null;
3755
- if (isOpen) {
3756
- const dropdownChildren = [];
3757
- if (searchable && searchText) {
3758
- dropdownChildren.push(
3759
- React15__default.default.createElement(
3760
- "box",
3761
- { key: "search", style: { paddingX: 1 } },
3762
- React15__default.default.createElement(
3763
- "text",
3764
- { style: { color: "blackBright", dim: true } },
3765
- `/${searchText}`
3766
- )
3767
- )
3768
- );
3769
- }
3770
- if (filteredItems.length === 0) {
3771
- dropdownChildren.push(
3772
- React15__default.default.createElement(
3773
- "box",
3774
- { key: "empty", style: { paddingX: 1 } },
3775
- React15__default.default.createElement(
3776
- "text",
3777
- { style: { dim: true, color: "blackBright" } },
3778
- "No matches"
3779
- )
3780
- )
3781
- );
3782
- }
3783
- if (scrollOffset > 0) {
3784
- dropdownChildren.push(
3785
- React15__default.default.createElement(
3786
- "box",
3787
- {
3788
- key: "scroll-up",
3789
- style: { justifyContent: "center", alignItems: "center" }
3790
- },
3791
- React15__default.default.createElement(
3792
- "text",
3793
- { style: { dim: true, color: "blackBright" } },
3794
- "\u25B2"
3795
- )
3796
- )
3797
- );
3798
- }
3799
- visibleItems.forEach((item, vi) => {
3800
- const actualIndex = scrollOffset + vi;
3801
- const isHighlighted = actualIndex === highlightIndex;
3802
- const isDisabled = item.disabled;
3803
- const itemStyle = {
3804
- paddingX: 1,
3805
- ...isHighlighted && !isDisabled ? { bg: highlightColor } : {}
3806
- };
3807
- const textStyle = {
3808
- ...isHighlighted && !isDisabled ? { color: "black", bold: true } : {},
3809
- ...isDisabled ? { dim: true, color: "blackBright" } : {}
3810
- };
3811
- dropdownChildren.push(
3812
- React15__default.default.createElement(
3813
- "box",
3814
- { key: `item-${item.value}`, style: itemStyle },
3815
- React15__default.default.createElement(
3816
- "text",
3817
- { style: textStyle },
3818
- item.label
3819
- )
3820
- )
3821
- );
3822
- });
3823
- if (scrollOffset + visibleCount < filteredItems.length) {
3824
- dropdownChildren.push(
3825
- React15__default.default.createElement(
3826
- "box",
3827
- {
3828
- key: "scroll-down",
3829
- style: { justifyContent: "center", alignItems: "center" }
3830
- },
3831
- React15__default.default.createElement(
3832
- "text",
3833
- { style: { dim: true, color: "blackBright" } },
3834
- "\u25BC"
3835
- )
3836
- )
3837
- );
3838
- }
3839
- const hasScrollUp = scrollOffset > 0;
3840
- const hasScrollDown = scrollOffset + visibleCount < filteredItems.length;
3841
- const hasSearch = searchable && searchText;
3842
- const hasNoMatches = filteredItems.length === 0;
3843
- const useDropdownBorder = !dropdownStyle?.bg && dropdownStyle?.border === void 0;
3844
- const borderSize = useDropdownBorder ? 2 : 0;
3845
- let dropdownHeight = visibleCount + borderSize;
3846
- if (hasScrollUp) dropdownHeight += 1;
3847
- if (hasScrollDown) dropdownHeight += 1;
3848
- if (hasSearch) dropdownHeight += 1;
3849
- if (hasNoMatches) dropdownHeight += 1;
3850
- const triggerBottom = triggerLayout.y + triggerLayout.height;
3851
- const spaceBelow = screenRows - triggerBottom;
3852
- const spaceAbove = triggerLayout.y;
3853
- const openUpward = spaceBelow < dropdownHeight && spaceAbove >= dropdownHeight;
3854
- const dropdownTop = openUpward ? -dropdownHeight : triggerLayout.height || 1;
3855
- dropdownElement = React15__default.default.createElement(
3856
- "box",
3857
- {
3858
- style: {
3859
- position: "absolute",
3860
- top: dropdownTop,
3861
- left: 0,
3862
- right: 0,
3863
- zIndex: 9999,
3864
- ...useDropdownBorder ? { border: "single" } : {},
3865
- bg: "black",
3866
- flexDirection: "column",
3867
- ...dropdownStyle
3868
- }
3869
- },
3870
- ...dropdownChildren
3871
- );
3872
- }
3873
- const outerStyle = {
3874
- flexDirection: "column",
3875
- width: triggerStyle.width ?? "100%",
3876
- minWidth: triggerStyle.minWidth,
3877
- maxWidth: triggerStyle.maxWidth,
3878
- flexGrow: triggerStyle.flexGrow,
3879
- flexShrink: triggerStyle.flexShrink ?? 1
3880
- };
3881
- return React15__default.default.createElement(
3882
- "box",
3883
- { style: outerStyle },
3884
- // Trigger
3885
- React15__default.default.createElement(
3886
- "box",
3887
- {
3888
- style: triggerStyle,
3889
- // Always focusable - disabled state is handled in input handler
3890
- // This ensures focusId is assigned on mount, even if initially disabled
3891
- focusable: true,
3892
- ref: (node) => {
3893
- if (node) {
3894
- nodeRef.current = node;
3895
- focusIdRef.current = node.focusId;
3896
- setNodeReady(true);
3897
- } else {
3898
- nodeRef.current = null;
3899
- focusIdRef.current = null;
3900
- setNodeReady(false);
3901
- }
3902
- }
3903
- },
3904
- ...triggerChildren
3905
- ),
3906
- // Dropdown overlay
3907
- dropdownElement
3908
- );
3909
- }
3910
- function Checkbox({
3911
- checked,
3912
- onChange,
3913
- label,
3914
- style,
3915
- focusedStyle,
3916
- disabled,
3917
- checkedChar = "\u2713",
3918
- uncheckedChar = " "
3919
- }) {
3920
- const focusCtx = React15.useContext(FocusContext);
3921
- const inputCtx = React15.useContext(InputContext);
3922
- const nodeRef = React15.useRef(null);
3923
- const focusIdRef = React15.useRef(null);
3924
- const onChangeRef = React15.useRef(onChange);
3925
- onChangeRef.current = onChange;
3926
- const checkedRef = React15.useRef(checked);
3927
- checkedRef.current = checked;
3928
- const [nodeReady, setNodeReady] = React15.useState(false);
3929
- const [isFocused, setIsFocused] = React15.useState(false);
3930
- React15.useEffect(() => {
3931
- if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
3932
- return focusCtx.register(focusIdRef.current, nodeRef.current);
3933
- }, [focusCtx, disabled, nodeReady]);
3934
- React15.useEffect(() => {
3935
- if (!focusCtx || !focusIdRef.current) return;
3936
- const fid = focusIdRef.current;
3937
- setIsFocused(focusCtx.focusedId === fid);
3938
- return focusCtx.onFocusChange((newId) => {
3939
- setIsFocused(newId === fid);
3940
- });
3941
- }, [focusCtx, nodeReady]);
3942
- React15.useEffect(() => {
3943
- if (!inputCtx || !focusIdRef.current || disabled) return;
3944
- const fid = focusIdRef.current;
3945
- const handler = (key) => {
3946
- if (focusCtx?.focusedId !== fid) return false;
3947
- if (key.name === "return" || key.name === " " || key.sequence === " ") {
3948
- onChangeRef.current(!checkedRef.current);
3949
- return true;
3950
- }
3951
- return false;
3952
- };
3953
- return inputCtx.registerInputHandler(fid, handler);
3954
- }, [inputCtx, focusCtx, disabled, nodeReady]);
3955
- const mergedStyle = {
3956
- flexDirection: "row",
3957
- gap: 1,
3958
- ...style,
3959
- ...isFocused && focusedStyle ? focusedStyle : {}
3960
- };
3961
- const boxChar = checked ? checkedChar : uncheckedChar;
3962
- const boxStyle = {
3963
- color: disabled ? "blackBright" : isFocused ? "white" : style?.color
3964
- };
3965
- const labelStyle = {
3966
- color: disabled ? "blackBright" : style?.color
3967
- };
3968
- return React15__default.default.createElement(
3969
- "box",
3970
- {
3971
- style: mergedStyle,
3972
- focusable: !disabled,
3973
- ref: (node) => {
3974
- if (node) {
3975
- nodeRef.current = node;
3976
- focusIdRef.current = node.focusId;
3977
- setNodeReady(true);
3978
- } else {
3979
- nodeRef.current = null;
3980
- focusIdRef.current = null;
3981
- setNodeReady(false);
3982
- }
3983
- }
3984
- },
3985
- React15__default.default.createElement(
3986
- "text",
3987
- { key: "box", style: boxStyle },
3988
- `[${boxChar}]`
3989
- ),
3990
- label ? React15__default.default.createElement(
3991
- "text",
3992
- { key: "label", style: labelStyle },
3993
- label
3994
- ) : null
3995
- );
3996
- }
3997
- function Radio({
3998
- items,
3999
- value,
4000
- onChange,
4001
- style,
4002
- itemStyle,
4003
- focusedItemStyle,
4004
- selectedItemStyle,
4005
- disabled,
4006
- direction = "column",
4007
- gap = 0,
4008
- selectedChar = "\u25CF",
4009
- unselectedChar = "\u25CB"
4010
- }) {
4011
- const focusCtx = React15.useContext(FocusContext);
4012
- const inputCtx = React15.useContext(InputContext);
4013
- const nodeRef = React15.useRef(null);
4014
- const focusIdRef = React15.useRef(null);
4015
- const onChangeRef = React15.useRef(onChange);
4016
- onChangeRef.current = onChange;
4017
- const [nodeReady, setNodeReady] = React15.useState(false);
4018
- const [isFocused, setIsFocused] = React15.useState(false);
4019
- const [highlightedIndex, setHighlightedIndex] = React15.useState(() => {
4020
- const selectedIdx = items.findIndex((item) => item.value === value);
4021
- if (selectedIdx >= 0) return selectedIdx;
4022
- return items.findIndex((item) => !item.disabled);
4023
- });
4024
- const findNextEnabled = React15.useCallback(
4025
- (startIndex, direction2) => {
4026
- let index = startIndex;
4027
- for (let i = 0; i < items.length; i++) {
4028
- index = (index + direction2 + items.length) % items.length;
4029
- if (!items[index]?.disabled) return index;
4030
- }
4031
- return startIndex;
4032
- },
4033
- [items]
4034
- );
4035
- React15.useEffect(() => {
4036
- if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
4037
- return focusCtx.register(focusIdRef.current, nodeRef.current);
4038
- }, [focusCtx, disabled, nodeReady]);
4039
- React15.useEffect(() => {
4040
- if (!focusCtx || !focusIdRef.current) return;
4041
- const fid = focusIdRef.current;
4042
- setIsFocused(focusCtx.focusedId === fid);
4043
- return focusCtx.onFocusChange((newId) => {
4044
- setIsFocused(newId === fid);
4045
- });
4046
- }, [focusCtx, nodeReady]);
4047
- React15.useEffect(() => {
4048
- if (!inputCtx || !focusIdRef.current || disabled) return;
4049
- const fid = focusIdRef.current;
4050
- const handler = (key) => {
4051
- if (focusCtx?.focusedId !== fid) return false;
4052
- if (key.name === "up" || key.name === "left" || key.name === "k" || key.name === "tab" && key.shift) {
4053
- setHighlightedIndex((idx) => findNextEnabled(idx, -1));
4054
- return true;
4055
- }
4056
- if (key.name === "down" || key.name === "right" || key.name === "j" || key.name === "tab" && !key.shift) {
4057
- setHighlightedIndex((idx) => findNextEnabled(idx, 1));
4058
- return true;
4059
- }
4060
- if (key.name === "return" || key.name === " " || key.sequence === " ") {
4061
- const item = items[highlightedIndex];
4062
- if (item && !item.disabled) {
4063
- onChangeRef.current(item.value);
4064
- }
4065
- return true;
4066
- }
4067
- return false;
4068
- };
4069
- return inputCtx.registerInputHandler(fid, handler);
4070
- }, [inputCtx, focusCtx, disabled, items, highlightedIndex, findNextEnabled, nodeReady]);
4071
- React15.useEffect(() => {
4072
- const selectedIdx = items.findIndex((item) => item.value === value);
4073
- if (selectedIdx >= 0) {
4074
- setHighlightedIndex(selectedIdx);
4075
- }
4076
- }, [value, items]);
4077
- const containerStyle = {
4078
- flexDirection: direction,
4079
- gap,
4080
- ...style
4081
- };
4082
- const radioItems = items.map((item, index) => {
4083
- const isSelected = item.value === value;
4084
- const isHighlighted = index === highlightedIndex;
4085
- const isItemDisabled = disabled || item.disabled;
4086
- const radioChar = isSelected ? selectedChar : unselectedChar;
4087
- let computedStyle = {
4088
- flexDirection: "row",
4089
- gap: 1,
4090
- ...itemStyle
4091
- };
4092
- if (isSelected && selectedItemStyle) {
4093
- computedStyle = { ...computedStyle, ...selectedItemStyle };
4094
- }
4095
- if (isFocused && isHighlighted && focusedItemStyle) {
4096
- computedStyle = { ...computedStyle, ...focusedItemStyle };
4097
- }
4098
- const textColor = isItemDisabled ? "blackBright" : isFocused && isHighlighted ? focusedItemStyle?.color ?? "white" : isSelected ? selectedItemStyle?.color ?? itemStyle?.color : itemStyle?.color;
4099
- return React15__default.default.createElement(
4100
- "box",
4101
- { key: index, style: computedStyle },
4102
- React15__default.default.createElement(
4103
- "text",
4104
- { key: "radio", style: { color: textColor } },
4105
- `(${radioChar})`
4106
- ),
4107
- React15__default.default.createElement(
4108
- "text",
4109
- { key: "label", style: { color: textColor } },
4110
- item.label
4111
- )
4112
- );
4113
- });
4114
- return React15__default.default.createElement(
4115
- "box",
4116
- {
4117
- style: containerStyle,
4118
- focusable: !disabled,
4119
- ref: (node) => {
4120
- if (node) {
4121
- nodeRef.current = node;
4122
- focusIdRef.current = node.focusId;
4123
- setNodeReady(true);
4124
- } else {
4125
- nodeRef.current = null;
4126
- focusIdRef.current = null;
4127
- setNodeReady(false);
4128
- }
4129
- }
4130
- },
4131
- ...radioItems
4132
- );
4133
- }
4134
- var DialogContext = React15.createContext(null);
4135
- function useDialog() {
4136
- const ctx = React15.useContext(DialogContext);
4137
- if (!ctx) {
4138
- throw new Error("useDialog must be used within a DialogHost");
4139
- }
4140
- return ctx;
4141
- }
4142
- function DialogHost({ children }) {
4143
- const [dialogs, setDialogs] = React15.useState([]);
4144
- const idCounter = React15.useRef(0);
4145
- const alert = React15.useCallback((content, options) => {
4146
- return new Promise((resolve) => {
4147
- const id = ++idCounter.current;
4148
- setDialogs((prev) => [
4149
- ...prev,
4150
- {
4151
- id,
4152
- type: "alert",
4153
- content,
4154
- okText: options?.okText ?? "OK",
4155
- cancelText: "",
4156
- style: options?.style,
4157
- resolve: () => resolve()
4158
- }
4159
- ]);
4160
- });
4161
- }, []);
4162
- const confirm = React15.useCallback((content, options) => {
4163
- return new Promise((resolve) => {
4164
- const id = ++idCounter.current;
4165
- setDialogs((prev) => [
4166
- ...prev,
4167
- {
4168
- id,
4169
- type: "confirm",
4170
- content,
4171
- okText: options?.okText ?? "OK",
4172
- cancelText: options?.cancelText ?? "Cancel",
4173
- style: options?.style,
4174
- resolve
4175
- }
4176
- ]);
4177
- });
4178
- }, []);
4179
- const dismissDialog = React15.useCallback((id, result) => {
4180
- setDialogs((prev) => {
4181
- const dialog = prev.find((d) => d.id === id);
4182
- if (dialog) {
4183
- dialog.resolve(result);
4184
- }
4185
- return prev.filter((d) => d.id !== id);
4186
- });
4187
- }, []);
4188
- const contextValue = { alert, confirm };
4189
- const activeDialog = dialogs[dialogs.length - 1];
4190
- return React15__default.default.createElement(
4191
- DialogContext.Provider,
4192
- { value: contextValue },
4193
- children,
4194
- activeDialog && React15__default.default.createElement(DialogOverlay, {
4195
- key: activeDialog.id,
4196
- dialog: activeDialog,
4197
- onDismiss: dismissDialog
4198
- })
4199
- );
4200
- }
4201
- function DialogOverlay({ dialog, onDismiss }) {
4202
- const focusCtx = React15.useContext(FocusContext);
4203
- const okButtonRef = React15.useRef(null);
4204
- const cancelButtonRef = React15.useRef(null);
4205
- const okFocusIdRef = React15.useRef(null);
4206
- const cancelFocusIdRef = React15.useRef(null);
4207
- const [focusedButton, setFocusedButton] = React15.useState("ok");
4208
- const [refsReady, setRefsReady] = React15.useState(0);
4209
- React15.useEffect(() => {
4210
- if (!focusCtx || refsReady === 0) return;
4211
- const cleanups = [];
4212
- if (okButtonRef.current && okFocusIdRef.current) {
4213
- cleanups.push(focusCtx.register(okFocusIdRef.current, okButtonRef.current));
4214
- }
4215
- if (cancelButtonRef.current && cancelFocusIdRef.current) {
4216
- cleanups.push(focusCtx.register(cancelFocusIdRef.current, cancelButtonRef.current));
4217
- }
4218
- if (okFocusIdRef.current) {
4219
- focusCtx.requestFocus(okFocusIdRef.current);
4220
- }
4221
- return () => cleanups.forEach((fn) => fn());
4222
- }, [focusCtx, refsReady]);
4223
- React15.useEffect(() => {
4224
- if (!focusCtx) return;
4225
- return focusCtx.onFocusChange((id) => {
4226
- if (id === okFocusIdRef.current) {
4227
- setFocusedButton("ok");
4228
- } else if (id === cancelFocusIdRef.current) {
4229
- setFocusedButton("cancel");
4230
- }
4231
- });
4232
- }, [focusCtx]);
4233
- useInput((key) => {
4234
- if (key.name === "return" || key.name === "space") {
4235
- if (dialog.type === "alert") {
4236
- onDismiss(dialog.id, true);
4237
- } else {
4238
- onDismiss(dialog.id, focusedButton === "ok");
4239
- }
4240
- return;
4241
- }
4242
- if (key.name === "escape") {
4243
- onDismiss(dialog.id, false);
4244
- return;
4245
- }
4246
- if (dialog.type === "confirm" && focusCtx) {
4247
- if (key.name === "left" || key.name === "right") {
4248
- if (focusedButton === "ok" && cancelFocusIdRef.current) {
4249
- focusCtx.requestFocus(cancelFocusIdRef.current);
4250
- } else if (okFocusIdRef.current) {
4251
- focusCtx.requestFocus(okFocusIdRef.current);
4252
- }
4253
- }
4254
- }
4255
- }, [dialog, focusedButton, focusCtx, onDismiss]);
4256
- const contentIsString = typeof dialog.content === "string";
4257
- const contentLength = contentIsString ? dialog.content.length : 0;
4258
- const minWidth = Math.max(20, contentIsString ? Math.min(contentLength + 6, 50) : 30);
4259
- const boxStyle = {
4260
- minWidth,
4261
- maxWidth: 50,
4262
- bg: "black",
4263
- border: "round",
4264
- borderColor: "white",
4265
- padding: 1,
4266
- flexDirection: "column",
4267
- gap: 1,
4268
- ...dialog.style
4269
- };
4270
- const getButtonStyle = (isSelected) => ({
4271
- paddingX: 2,
4272
- bg: isSelected ? "white" : "blackBright",
4273
- color: isSelected ? "black" : "white",
4274
- bold: isSelected
4275
- });
4276
- return React15__default.default.createElement(
4277
- FocusScope,
4278
- { trap: true },
4279
- // Backdrop
4280
- React15__default.default.createElement("box", {
4281
- style: {
4282
- position: "absolute",
4283
- top: 0,
4284
- left: 0,
4285
- right: 0,
4286
- bottom: 0,
4287
- zIndex: 999
4288
- }
4289
- }),
4290
- // Centering wrapper
4291
- React15__default.default.createElement(
4292
- "box",
4293
- {
4294
- style: {
4295
- position: "absolute",
4296
- top: 0,
4297
- left: 0,
4298
- right: 0,
4299
- bottom: 0,
4300
- justifyContent: "center",
4301
- alignItems: "center",
4302
- zIndex: 1e3
4303
- }
4304
- },
4305
- // Dialog box
4306
- React15__default.default.createElement(
4307
- "box",
4308
- { style: boxStyle },
4309
- // Content
4310
- React15__default.default.createElement(
4311
- "box",
4312
- { style: { flexDirection: "column" } },
4313
- typeof dialog.content === "string" ? React15__default.default.createElement("text", null, dialog.content) : dialog.content
4314
- ),
4315
- // Buttons row
4316
- React15__default.default.createElement(
4317
- "box",
4318
- {
4319
- style: {
4320
- flexDirection: "row",
4321
- justifyContent: "flex-end",
4322
- gap: 1
4323
- }
4324
- },
4325
- // Cancel button (confirm only)
4326
- dialog.type === "confirm" && React15__default.default.createElement(
4327
- "box",
4328
- {
4329
- style: getButtonStyle(focusedButton === "cancel"),
4330
- focusable: true,
4331
- ref: (node) => {
4332
- if (node && node.focusId && !cancelFocusIdRef.current) {
4333
- cancelButtonRef.current = node;
4334
- cancelFocusIdRef.current = node.focusId;
4335
- setRefsReady((r) => r + 1);
4336
- }
4337
- }
4338
- },
4339
- React15__default.default.createElement("text", null, dialog.cancelText)
4340
- ),
4341
- // OK button
4342
- React15__default.default.createElement(
4343
- "box",
4344
- {
4345
- style: getButtonStyle(focusedButton === "ok"),
4346
- focusable: true,
4347
- ref: (node) => {
4348
- if (node && node.focusId && !okFocusIdRef.current) {
4349
- okButtonRef.current = node;
4350
- okFocusIdRef.current = node.focusId;
4351
- setRefsReady((r) => r + 1);
4352
- }
4353
- }
4354
- },
4355
- React15__default.default.createElement("text", null, dialog.okText)
4356
- )
4357
- )
4358
- )
4359
- )
4360
- );
4361
- }
4362
- function generateHints(count, chars) {
4363
- const hints = [];
4364
- const charList = chars.split("");
4365
- if (count <= charList.length) {
4366
- for (let i = 0; i < count; i++) {
4367
- hints.push(charList[i]);
4368
- }
4369
- } else {
4370
- for (let i = 0; i < charList.length && hints.length < count; i++) {
4371
- for (let j = 0; j < charList.length && hints.length < count; j++) {
4372
- hints.push(charList[i] + charList[j]);
4373
- }
4374
- }
4375
- }
4376
- return hints;
4377
- }
4378
- function JumpNav({
4379
- children,
4380
- activationKey = "ctrl+o",
4381
- hintStyle,
4382
- hintBg = "yellow",
4383
- hintFg = "black",
4384
- hintChars = "asdfghjklqwertyuiopzxcvbnm",
4385
- enabled = true,
4386
- debug = false
4387
- }) {
4388
- const log = debug ? (...args) => console.error("[JumpNav]", ...args) : () => {
4389
- };
4390
- const [isActive, setIsActive] = React15.useState(false);
4391
- const [inputBuffer, setInputBuffer] = React15.useState("");
4392
- const [elements, setElements] = React15.useState([]);
4393
- const inputCtx = React15.useContext(InputContext);
4394
- const focusCtx = React15.useContext(FocusContext);
4395
- const layoutCtx = React15.useContext(LayoutContext);
4396
- React15.useEffect(() => {
4397
- log("Mounted, inputCtx:", !!inputCtx, "focusCtx:", !!focusCtx, "enabled:", enabled);
4398
- }, []);
4399
- const parseKey = React15.useCallback((keyStr) => {
4400
- const parts = keyStr.toLowerCase().split("+");
4401
- return {
4402
- ctrl: parts.includes("ctrl"),
4403
- alt: parts.includes("alt"),
4404
- shift: parts.includes("shift"),
4405
- meta: parts.includes("meta"),
4406
- name: parts[parts.length - 1] ?? ""
4407
- };
4408
- }, []);
4409
- const activationKeyParsed = parseKey(activationKey);
4410
- const refreshElements = React15.useCallback(() => {
4411
- if (!focusCtx?.getActiveElements) {
4412
- log("refreshElements: no getActiveElements");
4413
- return;
4414
- }
4415
- const active = focusCtx.getActiveElements();
4416
- log("getActiveElements returned", active.length, "elements");
4417
- const mapped = active.map(({ id, node }) => ({
4418
- id,
4419
- node,
4420
- layout: layoutCtx?.getLayout(node) ?? node.layout
4421
- }));
4422
- mapped.sort((a, b) => {
4423
- if (a.layout.y !== b.layout.y) {
4424
- return a.layout.y - b.layout.y;
4425
- }
4426
- return a.layout.x - b.layout.x;
4427
- });
4428
- setElements(mapped);
4429
- }, [focusCtx, layoutCtx, log]);
4430
- const wasActiveRef = React15.useRef(false);
4431
- React15.useEffect(() => {
4432
- if (isActive && !wasActiveRef.current) {
4433
- log("Activated! Refreshing elements...");
4434
- refreshElements();
4435
- }
4436
- wasActiveRef.current = isActive;
4437
- }, [isActive, refreshElements, log]);
4438
- const visibleElements = elements.filter(
4439
- (el) => el.layout.width > 0 && el.layout.height > 0
4440
- );
4441
- const visibleHints = generateHints(visibleElements.length, hintChars);
4442
- const visibleHintMap = React15.useMemo(() => {
4443
- const map = /* @__PURE__ */ new Map();
4444
- visibleElements.forEach((el, i) => {
4445
- if (visibleHints[i]) {
4446
- map.set(visibleHints[i], el.id);
4447
- }
4448
- });
4449
- return map;
4450
- }, [visibleElements, visibleHints]);
4451
- React15.useEffect(() => {
4452
- if (!inputCtx || !enabled) {
4453
- log("Not subscribing - inputCtx:", !!inputCtx, "enabled:", enabled);
4454
- return;
4455
- }
4456
- log("Subscribing to priority input, activation key:", activationKey);
4457
- const handler = (key) => {
4458
- const nameMatch = key.name === activationKeyParsed.name;
4459
- const ctrlMatch = !!key.ctrl === activationKeyParsed.ctrl;
4460
- const altMatch = !!key.alt === activationKeyParsed.alt;
4461
- const shiftMatch = !!key.shift === activationKeyParsed.shift;
4462
- const metaMatch = !!key.meta === activationKeyParsed.meta;
4463
- if (!isActive && nameMatch && ctrlMatch && altMatch && shiftMatch && metaMatch) {
4464
- log("Activation key matched! Activating...");
4465
- setIsActive(true);
4466
- setInputBuffer("");
4467
- return true;
4468
- }
4469
- if (isActive) {
4470
- if (key.name === "escape") {
4471
- log("Escape pressed, deactivating");
4472
- setIsActive(false);
4473
- setInputBuffer("");
4474
- return true;
4475
- }
4476
- if (key.name === "backspace") {
4477
- setInputBuffer("");
4478
- return true;
4479
- }
4480
- if (key.sequence && key.sequence.length === 1 && /[a-z]/i.test(key.sequence)) {
4481
- const newBuffer = inputBuffer + key.sequence.toLowerCase();
4482
- log("Buffer:", newBuffer);
4483
- const targetId = visibleHintMap.get(newBuffer);
4484
- if (targetId) {
4485
- log("Jumping to", targetId);
4486
- focusCtx?.requestFocus(targetId);
4487
- setIsActive(false);
4488
- setInputBuffer("");
4489
- return true;
4490
- }
4491
- const hasPartialMatch = [...visibleHintMap.keys()].some((h) => h.startsWith(newBuffer));
4492
- if (hasPartialMatch) {
4493
- setInputBuffer(newBuffer);
4494
- return true;
4495
- }
4496
- setInputBuffer("");
4497
- return true;
4498
- }
4499
- return true;
4500
- }
4501
- return false;
4502
- };
4503
- return inputCtx.subscribePriority(handler);
4504
- }, [inputCtx, enabled, isActive, activationKeyParsed, inputBuffer, visibleHintMap, focusCtx, activationKey, log]);
4505
- const hintsOverlay = isActive ? React15__default.default.createElement(
4506
- "box",
4507
- {
4508
- // Portal-like wrapper - fullscreen absolute overlay
4509
- style: {
4510
- position: "absolute",
4511
- top: 0,
4512
- left: 0,
4513
- width: "100%",
4514
- height: "100%",
4515
- zIndex: 99998
4516
- }
4517
- },
4518
- ...visibleElements.map((el, i) => {
4519
- const hint = visibleHints[i];
4520
- if (!hint) return null;
4521
- const { x, y } = el.layout;
4522
- const isPartialMatch = hint.startsWith(inputBuffer) && inputBuffer.length > 0;
4523
- return React15__default.default.createElement(
4524
- "box",
4525
- {
4526
- key: el.id,
4527
- style: {
4528
- position: "absolute",
4529
- top: y,
4530
- left: Math.max(0, x - hint.length - 2),
4531
- bg: isPartialMatch ? "cyan" : hintBg,
4532
- color: hintFg,
4533
- paddingX: 1,
4534
- zIndex: 99999,
4535
- ...hintStyle
4536
- }
4537
- },
4538
- React15__default.default.createElement("text", {
4539
- style: { bold: true, color: hintFg }
4540
- }, hint)
4541
- );
4542
- }),
4543
- // Status bar at bottom
4544
- React15__default.default.createElement(
4545
- "box",
4546
- {
4547
- style: {
4548
- position: "absolute",
4549
- bottom: 0,
4550
- left: 0,
4551
- right: 0,
4552
- bg: "blackBright",
4553
- paddingX: 1,
4554
- zIndex: 99999
4555
- }
4556
- },
4557
- React15__default.default.createElement("text", {
4558
- style: { color: "white" }
4559
- }, inputBuffer ? `Jump: ${inputBuffer}_` : "Press a key to jump \u2022 ESC to cancel")
4560
- )
4561
- ) : null;
4562
- return React15__default.default.createElement(
4563
- React15__default.default.Fragment,
4564
- null,
4565
- children,
4566
- hintsOverlay
4567
- );
4568
- }
4569
- function useFocus(nodeRef) {
4570
- const focusCtx = React15.useContext(FocusContext);
4571
- const [id] = React15.useState(() => `focus-${Math.random().toString(36).slice(2, 9)}`);
4572
- const isFocused = focusCtx ? focusCtx.focusedId === id : false;
4573
- React15.useEffect(() => {
4574
- if (!focusCtx || !nodeRef?.current) return;
4575
- nodeRef.current.focusId = id;
4576
- return focusCtx.register(id, nodeRef.current);
4577
- }, [focusCtx, id, nodeRef]);
4578
- const focus = React15.useMemo(() => {
4579
- return () => {
4580
- focusCtx?.requestFocus(id);
4581
- };
4582
- }, [focusCtx, id]);
4583
- return { focused: isFocused, focus };
4584
- }
4585
- function useFocusable(options = {}) {
4586
- const { disabled, onFocus, onBlur, onKeyPress } = options;
4587
- const focusCtx = React15.useContext(FocusContext);
4588
- const inputCtx = React15.useContext(InputContext);
4589
- const nodeRef = React15.useRef(null);
4590
- const focusIdRef = React15.useRef(null);
4591
- const [isFocused, setIsFocused] = React15.useState(false);
4592
- const onFocusRef = React15.useRef(onFocus);
4593
- const onBlurRef = React15.useRef(onBlur);
4594
- const onKeyPressRef = React15.useRef(onKeyPress);
4595
- onFocusRef.current = onFocus;
4596
- onBlurRef.current = onBlur;
4597
- onKeyPressRef.current = onKeyPress;
4598
- const ref = React15.useCallback((node) => {
4599
- nodeRef.current = node;
4600
- if (node) {
4601
- focusIdRef.current = node.focusId ?? null;
4602
- } else {
4603
- focusIdRef.current = null;
4604
- }
4605
- }, []);
4606
- React15.useEffect(() => {
4607
- if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
4608
- return focusCtx.register(focusIdRef.current, nodeRef.current);
4609
- }, [focusCtx]);
4610
- React15.useEffect(() => {
4611
- if (!focusCtx || !focusIdRef.current) return;
4612
- focusCtx.setSkippable(focusIdRef.current, !!disabled);
4613
- }, [focusCtx, disabled]);
4614
- React15.useEffect(() => {
4615
- if (!focusCtx || !focusIdRef.current) return;
4616
- const fid = focusIdRef.current;
4617
- const initiallyFocused = focusCtx.focusedId === fid;
4618
- setIsFocused(initiallyFocused);
4619
- return focusCtx.onFocusChange((newId) => {
4620
- const nowFocused = newId === fid;
4621
- setIsFocused((wasFocused) => {
4622
- if (nowFocused && !wasFocused) {
4623
- onFocusRef.current?.();
4624
- } else if (!nowFocused && wasFocused) {
4625
- onBlurRef.current?.();
4626
- }
4627
- return nowFocused;
4628
- });
4629
- });
4630
- }, [focusCtx]);
4631
- React15.useEffect(() => {
4632
- if (!inputCtx || !focusIdRef.current || disabled) return;
4633
- const fid = focusIdRef.current;
4634
- const handler = (key) => {
4635
- if (focusCtx?.focusedId !== fid) return false;
4636
- return onKeyPressRef.current?.(key) === true;
4637
- };
4638
- return inputCtx.registerInputHandler(fid, handler);
4639
- }, [inputCtx, focusCtx, disabled]);
4640
- const focus = React15.useCallback(() => {
4641
- if (focusCtx && focusIdRef.current) {
4642
- focusCtx.requestFocus(focusIdRef.current);
4643
- }
4644
- }, [focusCtx]);
4645
- return {
4646
- ref,
4647
- isFocused,
4648
- focus,
4649
- focusId: focusIdRef.current
4650
- };
4651
- }
4652
- function useApp() {
4653
- const ctx = React15.useContext(AppContext);
4654
- if (!ctx) {
4655
- throw new Error("useApp must be used within a Glyph render tree");
4656
- }
4657
- return {
4658
- exit: ctx.exit,
4659
- get columns() {
4660
- return ctx.columns;
4661
- },
4662
- get rows() {
4663
- return ctx.rows;
4664
- }
4665
- };
4666
- }
4667
- function useFocusRegistry() {
4668
- const focusCtx = React15.useContext(FocusContext);
4669
- const layoutCtx = React15.useContext(LayoutContext);
4670
- const [elements, setElements] = React15.useState([]);
4671
- const updateRef = React15.useRef(() => {
4672
- });
4673
- const updateElements = React15.useCallback(() => {
4674
- if (!focusCtx) return;
4675
- const registered = focusCtx.getActiveElements?.() ?? focusCtx.getRegisteredElements?.() ?? [];
4676
- const mapped = registered.map(({ id, node }) => ({
4677
- id,
4678
- node,
4679
- layout: layoutCtx?.getLayout(node) ?? node.layout,
4680
- type: node.type
4681
- }));
4682
- mapped.sort((a, b) => {
4683
- if (a.layout.y !== b.layout.y) {
4684
- return a.layout.y - b.layout.y;
4685
- }
4686
- return a.layout.x - b.layout.x;
4687
- });
4688
- setElements(mapped);
4689
- }, [focusCtx, layoutCtx]);
4690
- updateRef.current = updateElements;
4691
- React15.useEffect(() => {
4692
- if (!focusCtx) return;
4693
- updateElements();
4694
- const unsubscribe = focusCtx.onFocusChange(() => {
4695
- updateElements();
4696
- });
4697
- const timer = setTimeout(updateElements, 50);
4698
- return () => {
4699
- unsubscribe();
4700
- clearTimeout(timer);
4701
- };
4702
- }, [focusCtx, layoutCtx, updateElements]);
4703
- if (!focusCtx) return null;
4704
- return {
4705
- elements,
4706
- focusedId: focusCtx.focusedId,
4707
- requestFocus: focusCtx.requestFocus,
4708
- focusNext: focusCtx.focusNext,
4709
- focusPrev: focusCtx.focusPrev,
4710
- refresh: () => updateRef.current()
4711
- };
4712
- }
4713
-
4714
- // src/utils/mask.ts
4715
- function parseMask(mask) {
4716
- const result = [];
4717
- for (const char of mask) {
4718
- switch (char) {
4719
- case "9":
4720
- result.push({ type: "digit", char });
4721
- break;
4722
- case "a":
4723
- result.push({ type: "letter", char });
4724
- break;
4725
- case "*":
4726
- result.push({ type: "alphanumeric", char });
4727
- break;
4728
- default:
4729
- result.push({ type: "literal", char });
4730
- break;
4731
- }
4732
- }
4733
- return result;
4734
- }
4735
- function isValidChar(char, type) {
4736
- switch (type) {
4737
- case "digit":
4738
- return /\d/.test(char);
4739
- case "letter":
4740
- return /[a-zA-Z]/.test(char);
4741
- case "alphanumeric":
4742
- return /[a-zA-Z0-9]/.test(char);
4743
- case "literal":
4744
- return true;
4745
- }
4746
- }
4747
- function createMask(maskOrOptions) {
4748
- const options = typeof maskOrOptions === "string" ? { mask: maskOrOptions } : maskOrOptions;
4749
- const { mask, placeholder = "_", showPlaceholder = false } = options;
4750
- const maskChars = parseMask(mask);
4751
- return (newValue, _oldValue) => {
4752
- const inputChars = [];
4753
- for (const char of newValue) {
4754
- if (char !== placeholder && !/[\s\-\(\)\/\.\:]/.test(char) || /[a-zA-Z0-9]/.test(char)) {
4755
- if (/[a-zA-Z0-9]/.test(char)) {
4756
- inputChars.push(char);
4757
- }
4758
- }
4759
- }
4760
- let result = "";
4761
- let inputIndex = 0;
4762
- for (const maskChar of maskChars) {
4763
- if (maskChar.type === "literal") {
4764
- if (inputIndex < inputChars.length || showPlaceholder) {
4765
- result += maskChar.char;
4766
- }
4767
- } else {
4768
- if (inputIndex < inputChars.length) {
4769
- const char = inputChars[inputIndex];
4770
- if (isValidChar(char, maskChar.type)) {
4771
- result += char;
4772
- inputIndex++;
4773
- } else {
4774
- inputIndex++;
4775
- continue;
4776
- }
4777
- } else if (showPlaceholder) {
4778
- result += placeholder;
4779
- }
4780
- }
4781
- }
4782
- return result;
4783
- };
4784
- }
4785
- var masks = {
4786
- /** US Phone: (123) 456-7890 */
4787
- usPhone: createMask("(999) 999-9999"),
4788
- /** International Phone: +1 234 567 8900 */
4789
- intlPhone: createMask("+9 999 999 9999"),
4790
- /** Date MM/DD/YYYY */
4791
- dateUS: createMask("99/99/9999"),
4792
- /** Date DD/MM/YYYY */
4793
- dateEU: createMask("99/99/9999"),
4794
- /** Date YYYY-MM-DD */
4795
- dateISO: createMask("9999-99-99"),
4796
- /** Time HH:MM */
4797
- time: createMask("99:99"),
4798
- /** Time HH:MM:SS */
4799
- timeFull: createMask("99:99:99"),
4800
- /** Credit Card: 1234 5678 9012 3456 */
4801
- creditCard: createMask("9999 9999 9999 9999"),
4802
- /** SSN: 123-45-6789 */
4803
- ssn: createMask("999-99-9999"),
4804
- /** ZIP Code: 12345 */
4805
- zip: createMask("99999"),
4806
- /** ZIP+4: 12345-6789 */
4807
- zipPlus4: createMask("99999-9999"),
4808
- /** IPv4: 192.168.001.001 */
4809
- ipv4: createMask("999.999.999.999"),
4810
- /** MAC Address: AA:BB:CC:DD:EE:FF */
4811
- mac: createMask("**:**:**:**:**:**")
4812
- };
4813
-
4814
- exports.Box = Box;
4815
- exports.Button = Button;
4816
- exports.Checkbox = Checkbox;
4817
- exports.DialogHost = DialogHost;
4818
- exports.FocusScope = FocusScope;
4819
- exports.Input = Input;
4820
- exports.JumpNav = JumpNav;
4821
- exports.Keybind = Keybind;
4822
- exports.List = List;
4823
- exports.Menu = Menu;
4824
- exports.Portal = Portal;
4825
- exports.Progress = Progress;
4826
- exports.Radio = Radio;
4827
- exports.ScrollView = ScrollView;
4828
- exports.Select = Select;
4829
- exports.Spacer = Spacer;
4830
- exports.Spinner = Spinner;
4831
- exports.Text = Text;
4832
- exports.ToastHost = ToastHost;
4833
- exports.createMask = createMask;
4834
- exports.masks = masks;
4835
- exports.render = render;
4836
- exports.useApp = useApp;
4837
- exports.useDialog = useDialog;
4838
- exports.useFocus = useFocus;
4839
- exports.useFocusRegistry = useFocusRegistry;
4840
- exports.useFocusable = useFocusable;
4841
- exports.useInput = useInput;
4842
- exports.useLayout = useLayout;
4843
- exports.useToast = useToast;
4844
- //# sourceMappingURL=index.cjs.map
4845
- //# sourceMappingURL=index.cjs.map