@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.js +14 -4806
- package/dist/index.js.map +1 -1
- package/package.json +4 -10
- package/dist/index.cjs +0 -4845
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -682
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
|