@nick-skriabin/glyph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +3570 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +407 -0
- package/dist/index.d.ts +407 -0
- package/dist/index.js +3539 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3570 @@
|
|
|
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
|
+
enterAltScreen() {
|
|
328
|
+
this.write(`${CSI}?1049h`);
|
|
329
|
+
}
|
|
330
|
+
exitAltScreen() {
|
|
331
|
+
this.write(`${CSI}?1049l`);
|
|
332
|
+
}
|
|
333
|
+
clearScreen() {
|
|
334
|
+
this.write(`${CSI}2J${CSI}H`);
|
|
335
|
+
}
|
|
336
|
+
resetStyles() {
|
|
337
|
+
this.write(`${CSI}0m`);
|
|
338
|
+
}
|
|
339
|
+
setup() {
|
|
340
|
+
this.enterRawMode();
|
|
341
|
+
this.enterAltScreen();
|
|
342
|
+
this.hideCursor();
|
|
343
|
+
this.clearScreen();
|
|
344
|
+
this.attachStdinListener();
|
|
345
|
+
this.installCleanupHandlers();
|
|
346
|
+
}
|
|
347
|
+
cleanup() {
|
|
348
|
+
if (this.cleanedUp) return;
|
|
349
|
+
this.cleanedUp = true;
|
|
350
|
+
if (this.escFlushTimer !== null) {
|
|
351
|
+
clearTimeout(this.escFlushTimer);
|
|
352
|
+
this.escFlushTimer = null;
|
|
353
|
+
}
|
|
354
|
+
this.resetStyles();
|
|
355
|
+
this.showCursor();
|
|
356
|
+
this.exitAltScreen();
|
|
357
|
+
this.exitRawMode();
|
|
358
|
+
}
|
|
359
|
+
/** Restore terminal state for background suspension (does NOT mark as cleaned up). */
|
|
360
|
+
suspend() {
|
|
361
|
+
if (this.escFlushTimer !== null) {
|
|
362
|
+
clearTimeout(this.escFlushTimer);
|
|
363
|
+
this.escFlushTimer = null;
|
|
364
|
+
}
|
|
365
|
+
this.oscState = "normal";
|
|
366
|
+
this.oscAccum = "";
|
|
367
|
+
this.resetStyles();
|
|
368
|
+
this.showCursor();
|
|
369
|
+
this.exitAltScreen();
|
|
370
|
+
this.exitRawMode();
|
|
371
|
+
}
|
|
372
|
+
/** Re-enter raw mode and alt screen after SIGCONT resume. */
|
|
373
|
+
resume() {
|
|
374
|
+
this.enterRawMode();
|
|
375
|
+
this.enterAltScreen();
|
|
376
|
+
this.hideCursor();
|
|
377
|
+
this.clearScreen();
|
|
378
|
+
}
|
|
379
|
+
// ---- Data handling with OSC filtering ----
|
|
380
|
+
attachStdinListener() {
|
|
381
|
+
if (this.stdinAttached) return;
|
|
382
|
+
this.stdinAttached = true;
|
|
383
|
+
this.stdin.on("data", (data) => {
|
|
384
|
+
let str = typeof data === "string" ? data : data.toString("utf-8");
|
|
385
|
+
this.dispatchFiltered(str);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
onData(handler) {
|
|
389
|
+
this.dataHandlers.add(handler);
|
|
390
|
+
return () => {
|
|
391
|
+
this.dataHandlers.delete(handler);
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// ---- OSC response filtering ----
|
|
395
|
+
dispatchFiltered(raw) {
|
|
396
|
+
if (this.escFlushTimer !== null) {
|
|
397
|
+
clearTimeout(this.escFlushTimer);
|
|
398
|
+
this.escFlushTimer = null;
|
|
399
|
+
}
|
|
400
|
+
const clean = this.filterOsc(raw);
|
|
401
|
+
if (clean.length > 0) {
|
|
402
|
+
for (const handler of this.dataHandlers) {
|
|
403
|
+
handler(clean);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (this.oscState === "esc") {
|
|
407
|
+
this.escFlushTimer = setTimeout(() => {
|
|
408
|
+
this.escFlushTimer = null;
|
|
409
|
+
this.oscState = "normal";
|
|
410
|
+
for (const handler of this.dataHandlers) {
|
|
411
|
+
handler("\x1B");
|
|
412
|
+
}
|
|
413
|
+
}, 50);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
filterOsc(raw) {
|
|
417
|
+
let clean = "";
|
|
418
|
+
for (let i = 0; i < raw.length; i++) {
|
|
419
|
+
const ch = raw[i];
|
|
420
|
+
const code = raw.charCodeAt(i);
|
|
421
|
+
switch (this.oscState) {
|
|
422
|
+
case "normal":
|
|
423
|
+
if (code === 27) {
|
|
424
|
+
this.oscState = "esc";
|
|
425
|
+
} else {
|
|
426
|
+
clean += ch;
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
case "esc":
|
|
430
|
+
if (ch === "]") {
|
|
431
|
+
this.oscState = "osc";
|
|
432
|
+
this.oscAccum = "";
|
|
433
|
+
} else {
|
|
434
|
+
clean += "\x1B" + ch;
|
|
435
|
+
this.oscState = "normal";
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
case "osc":
|
|
439
|
+
if (code === 7) {
|
|
440
|
+
this.handleOscResponse(this.oscAccum);
|
|
441
|
+
this.oscAccum = "";
|
|
442
|
+
this.oscState = "normal";
|
|
443
|
+
} else if (code === 27) {
|
|
444
|
+
this.oscState = "osc_esc";
|
|
445
|
+
} else {
|
|
446
|
+
this.oscAccum += ch;
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
case "osc_esc":
|
|
450
|
+
if (ch === "\\") {
|
|
451
|
+
this.handleOscResponse(this.oscAccum);
|
|
452
|
+
this.oscAccum = "";
|
|
453
|
+
this.oscState = "normal";
|
|
454
|
+
} else {
|
|
455
|
+
this.oscAccum += "\x1B" + ch;
|
|
456
|
+
this.oscState = "osc";
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return clean;
|
|
462
|
+
}
|
|
463
|
+
handleOscResponse(data) {
|
|
464
|
+
const match = data.match(
|
|
465
|
+
/^4;(\d+);rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/
|
|
466
|
+
);
|
|
467
|
+
if (match) {
|
|
468
|
+
const index = parseInt(match[1], 10);
|
|
469
|
+
const r = parseInt(match[2].substring(0, 2), 16);
|
|
470
|
+
const g = parseInt(match[3].substring(0, 2), 16);
|
|
471
|
+
const b = parseInt(match[4].substring(0, 2), 16);
|
|
472
|
+
this.palette.set(index, [r, g, b]);
|
|
473
|
+
if (this.palette.size >= 16 && this.paletteResolve) {
|
|
474
|
+
this.paletteResolve();
|
|
475
|
+
this.paletteResolve = null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// ---- Palette querying ----
|
|
480
|
+
queryPalette() {
|
|
481
|
+
return new Promise((resolve) => {
|
|
482
|
+
const done = () => resolve(this.palette);
|
|
483
|
+
const timeout = setTimeout(done, 200);
|
|
484
|
+
this.paletteResolve = () => {
|
|
485
|
+
clearTimeout(timeout);
|
|
486
|
+
done();
|
|
487
|
+
};
|
|
488
|
+
let query = "";
|
|
489
|
+
for (let i = 0; i < 16; i++) {
|
|
490
|
+
query += `\x1B]4;${i};?\x07`;
|
|
491
|
+
}
|
|
492
|
+
this.write(query);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
// ---- Event handling ----
|
|
496
|
+
onResize(handler) {
|
|
497
|
+
this.stdout.on("resize", handler);
|
|
498
|
+
return () => {
|
|
499
|
+
this.stdout.off("resize", handler);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
installCleanupHandlers() {
|
|
503
|
+
const doCleanup = () => this.cleanup();
|
|
504
|
+
process.on("exit", doCleanup);
|
|
505
|
+
const handleSignal = (signal) => {
|
|
506
|
+
doCleanup();
|
|
507
|
+
process.kill(process.pid, signal);
|
|
508
|
+
};
|
|
509
|
+
process.once("SIGINT", () => handleSignal("SIGINT"));
|
|
510
|
+
process.once("SIGTERM", () => handleSignal("SIGTERM"));
|
|
511
|
+
process.on("uncaughtException", (err) => {
|
|
512
|
+
doCleanup();
|
|
513
|
+
console.error(err);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
});
|
|
516
|
+
process.on("unhandledRejection", (err) => {
|
|
517
|
+
doCleanup();
|
|
518
|
+
console.error(err);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/runtime/input.ts
|
|
525
|
+
function parseKeySequence(data) {
|
|
526
|
+
const keys = [];
|
|
527
|
+
let i = 0;
|
|
528
|
+
while (i < data.length) {
|
|
529
|
+
const ch = data[i];
|
|
530
|
+
const code = data.charCodeAt(i);
|
|
531
|
+
if (ch === "\x1B") {
|
|
532
|
+
if (data[i + 1] === "[") {
|
|
533
|
+
const seq = parseCsiSequence(data, i);
|
|
534
|
+
if (seq) {
|
|
535
|
+
keys.push(seq.key);
|
|
536
|
+
i = seq.end;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (i + 1 < data.length && data.charCodeAt(i + 1) >= 32) {
|
|
541
|
+
keys.push({
|
|
542
|
+
name: data[i + 1],
|
|
543
|
+
sequence: data.substring(i, i + 2),
|
|
544
|
+
alt: true
|
|
545
|
+
});
|
|
546
|
+
i += 2;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
keys.push({ name: "escape", sequence: "\x1B" });
|
|
550
|
+
i++;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (code >= 1 && code <= 26) {
|
|
554
|
+
const letter = String.fromCharCode(code + 96);
|
|
555
|
+
if (code === 13) {
|
|
556
|
+
keys.push({ name: "return", sequence: "\r" });
|
|
557
|
+
} else if (code === 9) {
|
|
558
|
+
keys.push({ name: "tab", sequence: " " });
|
|
559
|
+
} else if (code === 8) {
|
|
560
|
+
keys.push({ name: "backspace", sequence: "\b" });
|
|
561
|
+
} else {
|
|
562
|
+
keys.push({ name: letter, sequence: ch, ctrl: true });
|
|
563
|
+
}
|
|
564
|
+
i++;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (code === 127) {
|
|
568
|
+
keys.push({ name: "backspace", sequence: ch });
|
|
569
|
+
i++;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
keys.push({ name: ch, sequence: ch });
|
|
573
|
+
i++;
|
|
574
|
+
}
|
|
575
|
+
return keys;
|
|
576
|
+
}
|
|
577
|
+
function parseCsiSequence(data, start) {
|
|
578
|
+
let i = start + 2;
|
|
579
|
+
let params = "";
|
|
580
|
+
while (i < data.length) {
|
|
581
|
+
const code = data.charCodeAt(i);
|
|
582
|
+
if (code >= 48 && code <= 63) {
|
|
583
|
+
params += data[i];
|
|
584
|
+
i++;
|
|
585
|
+
} else {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (i >= data.length) return null;
|
|
590
|
+
const final = data[i];
|
|
591
|
+
const sequence = data.substring(start, i + 1);
|
|
592
|
+
i++;
|
|
593
|
+
let key;
|
|
594
|
+
switch (final) {
|
|
595
|
+
case "A":
|
|
596
|
+
key = { name: "up", sequence };
|
|
597
|
+
break;
|
|
598
|
+
case "B":
|
|
599
|
+
key = { name: "down", sequence };
|
|
600
|
+
break;
|
|
601
|
+
case "C":
|
|
602
|
+
key = { name: "right", sequence };
|
|
603
|
+
break;
|
|
604
|
+
case "D":
|
|
605
|
+
key = { name: "left", sequence };
|
|
606
|
+
break;
|
|
607
|
+
case "H":
|
|
608
|
+
key = { name: "home", sequence };
|
|
609
|
+
break;
|
|
610
|
+
case "F":
|
|
611
|
+
key = { name: "end", sequence };
|
|
612
|
+
break;
|
|
613
|
+
case "Z":
|
|
614
|
+
key = { name: "tab", sequence, shift: true };
|
|
615
|
+
break;
|
|
616
|
+
case "~": {
|
|
617
|
+
switch (params) {
|
|
618
|
+
case "2":
|
|
619
|
+
key = { name: "insert", sequence };
|
|
620
|
+
break;
|
|
621
|
+
case "3":
|
|
622
|
+
key = { name: "delete", sequence };
|
|
623
|
+
break;
|
|
624
|
+
case "5":
|
|
625
|
+
key = { name: "pageup", sequence };
|
|
626
|
+
break;
|
|
627
|
+
case "6":
|
|
628
|
+
key = { name: "pagedown", sequence };
|
|
629
|
+
break;
|
|
630
|
+
default:
|
|
631
|
+
key = { name: "unknown", sequence };
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
default:
|
|
636
|
+
key = { name: "unknown", sequence };
|
|
637
|
+
}
|
|
638
|
+
if (params.includes(";")) {
|
|
639
|
+
const parts = params.split(";");
|
|
640
|
+
const mod = parseInt(parts[1] ?? "1", 10) - 1;
|
|
641
|
+
if (mod & 1) key.shift = true;
|
|
642
|
+
if (mod & 2) key.alt = true;
|
|
643
|
+
if (mod & 4) key.ctrl = true;
|
|
644
|
+
}
|
|
645
|
+
return { key, end: i };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/paint/color.ts
|
|
649
|
+
var NAMED_FG = {
|
|
650
|
+
black: "\x1B[30m",
|
|
651
|
+
red: "\x1B[31m",
|
|
652
|
+
green: "\x1B[32m",
|
|
653
|
+
yellow: "\x1B[33m",
|
|
654
|
+
blue: "\x1B[34m",
|
|
655
|
+
magenta: "\x1B[35m",
|
|
656
|
+
cyan: "\x1B[36m",
|
|
657
|
+
white: "\x1B[37m",
|
|
658
|
+
blackBright: "\x1B[90m",
|
|
659
|
+
redBright: "\x1B[91m",
|
|
660
|
+
greenBright: "\x1B[92m",
|
|
661
|
+
yellowBright: "\x1B[93m",
|
|
662
|
+
blueBright: "\x1B[94m",
|
|
663
|
+
magentaBright: "\x1B[95m",
|
|
664
|
+
cyanBright: "\x1B[96m",
|
|
665
|
+
whiteBright: "\x1B[97m"
|
|
666
|
+
};
|
|
667
|
+
var NAMED_BG = {
|
|
668
|
+
black: "\x1B[40m",
|
|
669
|
+
red: "\x1B[41m",
|
|
670
|
+
green: "\x1B[42m",
|
|
671
|
+
yellow: "\x1B[43m",
|
|
672
|
+
blue: "\x1B[44m",
|
|
673
|
+
magenta: "\x1B[45m",
|
|
674
|
+
cyan: "\x1B[46m",
|
|
675
|
+
white: "\x1B[47m",
|
|
676
|
+
blackBright: "\x1B[100m",
|
|
677
|
+
redBright: "\x1B[101m",
|
|
678
|
+
greenBright: "\x1B[102m",
|
|
679
|
+
yellowBright: "\x1B[103m",
|
|
680
|
+
blueBright: "\x1B[104m",
|
|
681
|
+
magentaBright: "\x1B[105m",
|
|
682
|
+
cyanBright: "\x1B[106m",
|
|
683
|
+
whiteBright: "\x1B[107m"
|
|
684
|
+
};
|
|
685
|
+
function parseHex(hex) {
|
|
686
|
+
const h = hex.replace("#", "");
|
|
687
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
688
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
689
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
690
|
+
return { r, g, b };
|
|
691
|
+
}
|
|
692
|
+
function colorToFg(color) {
|
|
693
|
+
if (typeof color === "string") {
|
|
694
|
+
if (color.startsWith("#")) {
|
|
695
|
+
const { r: r2, g: g2, b: b2 } = parseHex(color);
|
|
696
|
+
return `\x1B[38;2;${r2};${g2};${b2}m`;
|
|
697
|
+
}
|
|
698
|
+
return NAMED_FG[color] ?? "\x1B[39m";
|
|
699
|
+
}
|
|
700
|
+
if (typeof color === "number") {
|
|
701
|
+
return `\x1B[38;5;${color}m`;
|
|
702
|
+
}
|
|
703
|
+
const { r, g, b } = color;
|
|
704
|
+
return `\x1B[38;2;${r};${g};${b}m`;
|
|
705
|
+
}
|
|
706
|
+
function colorToBg(color) {
|
|
707
|
+
if (typeof color === "string") {
|
|
708
|
+
if (color.startsWith("#")) {
|
|
709
|
+
const { r: r2, g: g2, b: b2 } = parseHex(color);
|
|
710
|
+
return `\x1B[48;2;${r2};${g2};${b2}m`;
|
|
711
|
+
}
|
|
712
|
+
return NAMED_BG[color] ?? "\x1B[49m";
|
|
713
|
+
}
|
|
714
|
+
if (typeof color === "number") {
|
|
715
|
+
return `\x1B[48;5;${color}m`;
|
|
716
|
+
}
|
|
717
|
+
const { r, g, b } = color;
|
|
718
|
+
return `\x1B[48;2;${r};${g};${b}m`;
|
|
719
|
+
}
|
|
720
|
+
var NAMED_RGB = {
|
|
721
|
+
black: [0, 0, 0],
|
|
722
|
+
red: [170, 0, 0],
|
|
723
|
+
green: [0, 170, 0],
|
|
724
|
+
yellow: [170, 170, 0],
|
|
725
|
+
blue: [0, 0, 170],
|
|
726
|
+
magenta: [170, 0, 170],
|
|
727
|
+
cyan: [0, 170, 170],
|
|
728
|
+
white: [170, 170, 170],
|
|
729
|
+
blackBright: [85, 85, 85],
|
|
730
|
+
redBright: [255, 85, 85],
|
|
731
|
+
greenBright: [85, 255, 85],
|
|
732
|
+
yellowBright: [255, 255, 85],
|
|
733
|
+
blueBright: [85, 85, 255],
|
|
734
|
+
magentaBright: [255, 85, 255],
|
|
735
|
+
cyanBright: [85, 255, 255],
|
|
736
|
+
whiteBright: [255, 255, 255]
|
|
737
|
+
};
|
|
738
|
+
var NAMED_INDEX = [
|
|
739
|
+
"black",
|
|
740
|
+
"red",
|
|
741
|
+
"green",
|
|
742
|
+
"yellow",
|
|
743
|
+
"blue",
|
|
744
|
+
"magenta",
|
|
745
|
+
"cyan",
|
|
746
|
+
"white",
|
|
747
|
+
"blackBright",
|
|
748
|
+
"redBright",
|
|
749
|
+
"greenBright",
|
|
750
|
+
"yellowBright",
|
|
751
|
+
"blueBright",
|
|
752
|
+
"magentaBright",
|
|
753
|
+
"cyanBright",
|
|
754
|
+
"whiteBright"
|
|
755
|
+
];
|
|
756
|
+
var terminalPalette = null;
|
|
757
|
+
function setTerminalPalette(palette) {
|
|
758
|
+
if (palette.size > 0) {
|
|
759
|
+
terminalPalette = palette;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function resolveNamedRgb(name) {
|
|
763
|
+
if (terminalPalette) {
|
|
764
|
+
const idx = NAMED_INDEX.indexOf(name);
|
|
765
|
+
if (idx !== -1) {
|
|
766
|
+
const tp = terminalPalette.get(idx);
|
|
767
|
+
if (tp) return tp;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return NAMED_RGB[name] ?? null;
|
|
771
|
+
}
|
|
772
|
+
function colorToRgb(color) {
|
|
773
|
+
if (typeof color === "string") {
|
|
774
|
+
if (color.startsWith("#")) {
|
|
775
|
+
const c = parseHex(color);
|
|
776
|
+
return [c.r, c.g, c.b];
|
|
777
|
+
}
|
|
778
|
+
return resolveNamedRgb(color);
|
|
779
|
+
}
|
|
780
|
+
if (typeof color === "number") {
|
|
781
|
+
if (color < 16) {
|
|
782
|
+
if (terminalPalette) {
|
|
783
|
+
const tp = terminalPalette.get(color);
|
|
784
|
+
if (tp) return tp;
|
|
785
|
+
}
|
|
786
|
+
return NAMED_RGB[NAMED_INDEX[color]];
|
|
787
|
+
}
|
|
788
|
+
if (color >= 232) {
|
|
789
|
+
const g2 = (color - 232) * 10 + 8;
|
|
790
|
+
return [g2, g2, g2];
|
|
791
|
+
}
|
|
792
|
+
const idx = color - 16;
|
|
793
|
+
const b = idx % 6 * 51;
|
|
794
|
+
const g = Math.floor(idx / 6) % 6 * 51;
|
|
795
|
+
const r = Math.floor(idx / 36) * 51;
|
|
796
|
+
return [r, g, b];
|
|
797
|
+
}
|
|
798
|
+
return [color.r, color.g, color.b];
|
|
799
|
+
}
|
|
800
|
+
function isLightColor(color) {
|
|
801
|
+
const rgb = colorToRgb(color);
|
|
802
|
+
if (!rgb) return false;
|
|
803
|
+
const [r, g, b] = rgb.map((c) => {
|
|
804
|
+
const s = c / 255;
|
|
805
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
806
|
+
});
|
|
807
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
808
|
+
return luminance > 0.4;
|
|
809
|
+
}
|
|
810
|
+
function colorsEqual(a, b) {
|
|
811
|
+
if (a === b) return true;
|
|
812
|
+
if (a == null || b == null) return false;
|
|
813
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
814
|
+
return a.r === b.r && a.g === b.g && a.b === b.b;
|
|
815
|
+
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/paint/framebuffer.ts
|
|
820
|
+
var Framebuffer = class _Framebuffer {
|
|
821
|
+
width;
|
|
822
|
+
height;
|
|
823
|
+
cells;
|
|
824
|
+
constructor(width, height) {
|
|
825
|
+
this.width = width;
|
|
826
|
+
this.height = height;
|
|
827
|
+
this.cells = new Array(width * height);
|
|
828
|
+
this.clear();
|
|
829
|
+
}
|
|
830
|
+
clear() {
|
|
831
|
+
for (let i = 0; i < this.cells.length; i++) {
|
|
832
|
+
this.cells[i] = { ch: " " };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
resize(width, height) {
|
|
836
|
+
this.width = width;
|
|
837
|
+
this.height = height;
|
|
838
|
+
this.cells = new Array(width * height);
|
|
839
|
+
this.clear();
|
|
840
|
+
}
|
|
841
|
+
get(x, y) {
|
|
842
|
+
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return void 0;
|
|
843
|
+
return this.cells[y * this.width + x];
|
|
844
|
+
}
|
|
845
|
+
set(x, y, cell) {
|
|
846
|
+
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
|
|
847
|
+
this.cells[y * this.width + x] = cell;
|
|
848
|
+
}
|
|
849
|
+
setChar(x, y, ch, fg, bg, bold, dim, italic, underline) {
|
|
850
|
+
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
|
|
851
|
+
this.cells[y * this.width + x] = { ch, fg, bg, bold, dim, italic, underline };
|
|
852
|
+
}
|
|
853
|
+
fillRect(x, y, w, h, ch, fg, bg) {
|
|
854
|
+
for (let row = y; row < y + h; row++) {
|
|
855
|
+
for (let col = x; col < x + w; col++) {
|
|
856
|
+
this.setChar(col, row, ch, fg, bg);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
clone() {
|
|
861
|
+
const fb = new _Framebuffer(this.width, this.height);
|
|
862
|
+
for (let i = 0; i < this.cells.length; i++) {
|
|
863
|
+
const c = this.cells[i];
|
|
864
|
+
fb.cells[i] = { ...c };
|
|
865
|
+
}
|
|
866
|
+
return fb;
|
|
867
|
+
}
|
|
868
|
+
cellsEqual(a, b) {
|
|
869
|
+
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);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
// src/paint/borders.ts
|
|
874
|
+
var BORDER_CHARS = {
|
|
875
|
+
single: {
|
|
876
|
+
topLeft: "\u250C",
|
|
877
|
+
topRight: "\u2510",
|
|
878
|
+
bottomLeft: "\u2514",
|
|
879
|
+
bottomRight: "\u2518",
|
|
880
|
+
horizontal: "\u2500",
|
|
881
|
+
vertical: "\u2502"
|
|
882
|
+
},
|
|
883
|
+
double: {
|
|
884
|
+
topLeft: "\u2554",
|
|
885
|
+
topRight: "\u2557",
|
|
886
|
+
bottomLeft: "\u255A",
|
|
887
|
+
bottomRight: "\u255D",
|
|
888
|
+
horizontal: "\u2550",
|
|
889
|
+
vertical: "\u2551"
|
|
890
|
+
},
|
|
891
|
+
round: {
|
|
892
|
+
topLeft: "\u256D",
|
|
893
|
+
topRight: "\u256E",
|
|
894
|
+
bottomLeft: "\u2570",
|
|
895
|
+
bottomRight: "\u256F",
|
|
896
|
+
horizontal: "\u2500",
|
|
897
|
+
vertical: "\u2502"
|
|
898
|
+
},
|
|
899
|
+
ascii: {
|
|
900
|
+
topLeft: "+",
|
|
901
|
+
topRight: "+",
|
|
902
|
+
bottomLeft: "+",
|
|
903
|
+
bottomRight: "+",
|
|
904
|
+
horizontal: "-",
|
|
905
|
+
vertical: "|"
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
function getBorderChars(style) {
|
|
909
|
+
if (style === "none") return null;
|
|
910
|
+
return BORDER_CHARS[style];
|
|
911
|
+
}
|
|
912
|
+
function measureText(text, maxWidth, widthMode, wrapMode) {
|
|
913
|
+
if (text.length === 0) {
|
|
914
|
+
return { width: 0, height: 0 };
|
|
915
|
+
}
|
|
916
|
+
const lines = text.split("\n");
|
|
917
|
+
if (widthMode === Yoga.MeasureMode.Undefined || wrapMode === "none") {
|
|
918
|
+
let maxW2 = 0;
|
|
919
|
+
for (const line of lines) {
|
|
920
|
+
const w = stringWidth__default.default(line);
|
|
921
|
+
if (w > maxW2) maxW2 = w;
|
|
922
|
+
}
|
|
923
|
+
return { width: maxW2, height: lines.length };
|
|
924
|
+
}
|
|
925
|
+
const availWidth = Math.max(1, Math.floor(maxWidth));
|
|
926
|
+
const wrappedLines = wrapLines(lines, availWidth, wrapMode);
|
|
927
|
+
let maxW = 0;
|
|
928
|
+
for (const line of wrappedLines) {
|
|
929
|
+
const w = stringWidth__default.default(line);
|
|
930
|
+
if (w > maxW) maxW = w;
|
|
931
|
+
}
|
|
932
|
+
return { width: maxW, height: wrappedLines.length };
|
|
933
|
+
}
|
|
934
|
+
function wrapLines(lines, maxWidth, wrapMode) {
|
|
935
|
+
const result = [];
|
|
936
|
+
for (const line of lines) {
|
|
937
|
+
const lineWidth = stringWidth__default.default(line);
|
|
938
|
+
if (lineWidth <= maxWidth) {
|
|
939
|
+
result.push(line);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (wrapMode === "truncate") {
|
|
943
|
+
result.push(truncateLine(line, maxWidth));
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (wrapMode === "ellipsis") {
|
|
947
|
+
result.push(truncateWithEllipsis(line, maxWidth));
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const wrapped = wordWrap(line, maxWidth);
|
|
951
|
+
result.push(...wrapped);
|
|
952
|
+
}
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
955
|
+
function truncateLine(text, maxWidth) {
|
|
956
|
+
let result = "";
|
|
957
|
+
let width = 0;
|
|
958
|
+
for (const char of text) {
|
|
959
|
+
const charWidth = stringWidth__default.default(char);
|
|
960
|
+
if (width + charWidth > maxWidth) break;
|
|
961
|
+
result += char;
|
|
962
|
+
width += charWidth;
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
function truncateWithEllipsis(text, maxWidth) {
|
|
967
|
+
if (maxWidth <= 1) {
|
|
968
|
+
return maxWidth === 1 ? "\u2026" : "";
|
|
969
|
+
}
|
|
970
|
+
const truncated = truncateLine(text, maxWidth - 1);
|
|
971
|
+
if (stringWidth__default.default(truncated) < stringWidth__default.default(text)) {
|
|
972
|
+
return truncated + "\u2026";
|
|
973
|
+
}
|
|
974
|
+
return text;
|
|
975
|
+
}
|
|
976
|
+
function wordWrap(text, maxWidth) {
|
|
977
|
+
const lines = [];
|
|
978
|
+
let currentLine = "";
|
|
979
|
+
let currentWidth = 0;
|
|
980
|
+
let wordBuffer = "";
|
|
981
|
+
let wordBufferWidth = 0;
|
|
982
|
+
for (let i = 0; i <= text.length; i++) {
|
|
983
|
+
const char = text[i];
|
|
984
|
+
const isEnd = i === text.length;
|
|
985
|
+
const isSpace = char === " ";
|
|
986
|
+
if (isEnd || isSpace) {
|
|
987
|
+
if (wordBuffer.length > 0) {
|
|
988
|
+
if (currentWidth + wordBufferWidth <= maxWidth) {
|
|
989
|
+
currentLine += wordBuffer;
|
|
990
|
+
currentWidth += wordBufferWidth;
|
|
991
|
+
} else if (wordBufferWidth <= maxWidth) {
|
|
992
|
+
if (currentLine.length > 0) {
|
|
993
|
+
lines.push(currentLine);
|
|
994
|
+
}
|
|
995
|
+
currentLine = wordBuffer;
|
|
996
|
+
currentWidth = wordBufferWidth;
|
|
997
|
+
} else {
|
|
998
|
+
for (const c of wordBuffer) {
|
|
999
|
+
const cw = stringWidth__default.default(c);
|
|
1000
|
+
if (currentWidth + cw > maxWidth && currentLine.length > 0) {
|
|
1001
|
+
lines.push(currentLine);
|
|
1002
|
+
currentLine = "";
|
|
1003
|
+
currentWidth = 0;
|
|
1004
|
+
}
|
|
1005
|
+
currentLine += c;
|
|
1006
|
+
currentWidth += cw;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
wordBuffer = "";
|
|
1010
|
+
wordBufferWidth = 0;
|
|
1011
|
+
}
|
|
1012
|
+
if (isSpace) {
|
|
1013
|
+
if (currentWidth + 1 <= maxWidth) {
|
|
1014
|
+
currentLine += " ";
|
|
1015
|
+
currentWidth += 1;
|
|
1016
|
+
} else {
|
|
1017
|
+
if (currentLine.length > 0) {
|
|
1018
|
+
lines.push(currentLine);
|
|
1019
|
+
}
|
|
1020
|
+
currentLine = " ";
|
|
1021
|
+
currentWidth = 1;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} else {
|
|
1025
|
+
wordBuffer += char;
|
|
1026
|
+
wordBufferWidth += stringWidth__default.default(char);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (currentLine.length > 0) {
|
|
1030
|
+
lines.push(currentLine);
|
|
1031
|
+
}
|
|
1032
|
+
return lines.length > 0 ? lines : [""];
|
|
1033
|
+
}
|
|
1034
|
+
function paintTree(roots, fb, cursorInfo) {
|
|
1035
|
+
fb.clear();
|
|
1036
|
+
const entries = [];
|
|
1037
|
+
const screenClip = { x: 0, y: 0, width: fb.width, height: fb.height };
|
|
1038
|
+
for (const root of roots) {
|
|
1039
|
+
if (root.hidden) continue;
|
|
1040
|
+
collectPaintEntries(root, screenClip, root.style.zIndex ?? 0, entries);
|
|
1041
|
+
}
|
|
1042
|
+
entries.sort((a, b) => a.zIndex - b.zIndex);
|
|
1043
|
+
for (const entry of entries) {
|
|
1044
|
+
paintNode(entry.node, fb, entry.clip, cursorInfo);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function collectPaintEntries(node, parentClip, parentZ, entries) {
|
|
1048
|
+
if (node.hidden) return;
|
|
1049
|
+
const zIndex = node.style.zIndex ?? parentZ;
|
|
1050
|
+
const clip = node.style.clip ? intersectClip(parentClip, {
|
|
1051
|
+
x: node.layout.innerX,
|
|
1052
|
+
y: node.layout.innerY,
|
|
1053
|
+
width: node.layout.innerWidth,
|
|
1054
|
+
height: node.layout.innerHeight
|
|
1055
|
+
}) : parentClip;
|
|
1056
|
+
entries.push({ node, clip: parentClip, zIndex });
|
|
1057
|
+
if (node.type !== "text" && node.type !== "input") {
|
|
1058
|
+
for (const child of node.children) {
|
|
1059
|
+
collectPaintEntries(child, clip, zIndex, entries);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function intersectClip(a, b) {
|
|
1064
|
+
const x = Math.max(a.x, b.x);
|
|
1065
|
+
const y = Math.max(a.y, b.y);
|
|
1066
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
1067
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
1068
|
+
return {
|
|
1069
|
+
x,
|
|
1070
|
+
y,
|
|
1071
|
+
width: Math.max(0, right - x),
|
|
1072
|
+
height: Math.max(0, bottom - y)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function isInClip(x, y, clip) {
|
|
1076
|
+
return x >= clip.x && x < clip.x + clip.width && y >= clip.y && y < clip.y + clip.height;
|
|
1077
|
+
}
|
|
1078
|
+
function paintNode(node, fb, clip, cursorInfo) {
|
|
1079
|
+
const { x, y, width, height, innerX, innerY, innerWidth, innerHeight } = node.layout;
|
|
1080
|
+
const style = node.style;
|
|
1081
|
+
if (width <= 0 || height <= 0) return;
|
|
1082
|
+
const inherited = getInheritedTextStyle(node);
|
|
1083
|
+
const effectiveBg = inherited.bg;
|
|
1084
|
+
if (style.bg) {
|
|
1085
|
+
for (let row = y; row < y + height; row++) {
|
|
1086
|
+
for (let col = x; col < x + width; col++) {
|
|
1087
|
+
if (isInClip(col, row, clip)) {
|
|
1088
|
+
fb.setChar(col, row, " ", void 0, style.bg);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const borderChars = style.border ? getBorderChars(style.border) : null;
|
|
1094
|
+
if (borderChars && width >= 2 && height >= 2) {
|
|
1095
|
+
const bc = style.borderColor;
|
|
1096
|
+
const bg = effectiveBg;
|
|
1097
|
+
setClipped(fb, clip, x, y, borderChars.topLeft, bc, bg);
|
|
1098
|
+
for (let col = x + 1; col < x + width - 1; col++) {
|
|
1099
|
+
setClipped(fb, clip, col, y, borderChars.horizontal, bc, bg);
|
|
1100
|
+
}
|
|
1101
|
+
setClipped(fb, clip, x + width - 1, y, borderChars.topRight, bc, bg);
|
|
1102
|
+
setClipped(fb, clip, x, y + height - 1, borderChars.bottomLeft, bc, bg);
|
|
1103
|
+
for (let col = x + 1; col < x + width - 1; col++) {
|
|
1104
|
+
setClipped(fb, clip, col, y + height - 1, borderChars.horizontal, bc, bg);
|
|
1105
|
+
}
|
|
1106
|
+
setClipped(fb, clip, x + width - 1, y + height - 1, borderChars.bottomRight, bc, bg);
|
|
1107
|
+
for (let row = y + 1; row < y + height - 1; row++) {
|
|
1108
|
+
setClipped(fb, clip, x, row, borderChars.vertical, bc, bg);
|
|
1109
|
+
setClipped(fb, clip, x + width - 1, row, borderChars.vertical, bc, bg);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (node.type === "text") {
|
|
1113
|
+
paintText(node, fb, clip);
|
|
1114
|
+
} else if (node.type === "input") {
|
|
1115
|
+
paintInput(node, fb, clip, cursorInfo);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function setClipped(fb, clip, x, y, ch, fg, bg, bold, dim, italic, underline) {
|
|
1119
|
+
if (isInClip(x, y, clip)) {
|
|
1120
|
+
fb.setChar(x, y, ch, fg, bg, bold, dim, italic, underline);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function autoContrastFg(explicitColor, bg) {
|
|
1124
|
+
if (explicitColor !== void 0) return explicitColor;
|
|
1125
|
+
if (bg === void 0) return void 0;
|
|
1126
|
+
return isLightColor(bg) ? "black" : "white";
|
|
1127
|
+
}
|
|
1128
|
+
function paintText(node, fb, clip) {
|
|
1129
|
+
const { innerX, innerY, innerWidth, innerHeight } = node.layout;
|
|
1130
|
+
const inherited = getInheritedTextStyle(node);
|
|
1131
|
+
const text = collectTextContent(node);
|
|
1132
|
+
if (!text) return;
|
|
1133
|
+
const fg = autoContrastFg(inherited.color, inherited.bg);
|
|
1134
|
+
const wrapMode = node.style.wrap ?? "wrap";
|
|
1135
|
+
const textAlign = node.style.textAlign ?? "left";
|
|
1136
|
+
const rawLines = text.split("\n");
|
|
1137
|
+
const lines = wrapLines(rawLines, innerWidth, wrapMode);
|
|
1138
|
+
for (let lineIdx = 0; lineIdx < lines.length && lineIdx < innerHeight; lineIdx++) {
|
|
1139
|
+
const line = lines[lineIdx];
|
|
1140
|
+
const lineWidth = stringWidth__default.default(line);
|
|
1141
|
+
let offsetX = 0;
|
|
1142
|
+
if (textAlign === "center") {
|
|
1143
|
+
offsetX = Math.max(0, Math.floor((innerWidth - lineWidth) / 2));
|
|
1144
|
+
} else if (textAlign === "right") {
|
|
1145
|
+
offsetX = Math.max(0, innerWidth - lineWidth);
|
|
1146
|
+
}
|
|
1147
|
+
let col = 0;
|
|
1148
|
+
for (const char of line) {
|
|
1149
|
+
const charWidth = stringWidth__default.default(char);
|
|
1150
|
+
if (charWidth > 0) {
|
|
1151
|
+
setClipped(
|
|
1152
|
+
fb,
|
|
1153
|
+
clip,
|
|
1154
|
+
innerX + offsetX + col,
|
|
1155
|
+
innerY + lineIdx,
|
|
1156
|
+
char,
|
|
1157
|
+
fg,
|
|
1158
|
+
inherited.bg,
|
|
1159
|
+
inherited.bold,
|
|
1160
|
+
inherited.dim,
|
|
1161
|
+
inherited.italic,
|
|
1162
|
+
inherited.underline
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
col += charWidth;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function paintInput(node, fb, clip, cursorInfo) {
|
|
1170
|
+
const { innerX, innerY, innerWidth, innerHeight } = node.layout;
|
|
1171
|
+
if (innerWidth <= 0 || innerHeight <= 0) return;
|
|
1172
|
+
const value = node.props.value ?? node.props.defaultValue ?? "";
|
|
1173
|
+
const placeholder = node.props.placeholder ?? "";
|
|
1174
|
+
const displayText = value || placeholder;
|
|
1175
|
+
const isPlaceholder = !value && !!placeholder;
|
|
1176
|
+
const multiline = node.props.multiline ?? false;
|
|
1177
|
+
const inherited = getInheritedTextStyle(node);
|
|
1178
|
+
const autoFg = autoContrastFg(inherited.color, inherited.bg);
|
|
1179
|
+
const placeholderFg = inherited.bg ? isLightColor(inherited.bg) ? "blackBright" : "whiteBright" : "blackBright";
|
|
1180
|
+
const fg = isPlaceholder ? placeholderFg : autoFg ?? inherited.color ?? node.style.color;
|
|
1181
|
+
const textFg = isPlaceholder ? placeholderFg : fg;
|
|
1182
|
+
const textDim = isPlaceholder ? true : inherited.dim;
|
|
1183
|
+
if (multiline && !isPlaceholder) {
|
|
1184
|
+
const wrapMode = node.style.wrap ?? "wrap";
|
|
1185
|
+
const rawLines = displayText.split("\n");
|
|
1186
|
+
const wrappedLines = wrapLines(rawLines, innerWidth, wrapMode);
|
|
1187
|
+
let cursorScreenLine = 0;
|
|
1188
|
+
let cursorScreenCol = 0;
|
|
1189
|
+
if (cursorInfo && cursorInfo.nodeId === node.focusId) {
|
|
1190
|
+
const pos = cursorInfo.position;
|
|
1191
|
+
let logicalLine = 0;
|
|
1192
|
+
let offsetInLogicalLine = pos;
|
|
1193
|
+
let runningPos = 0;
|
|
1194
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
1195
|
+
const lineLen = rawLines[i].length;
|
|
1196
|
+
if (pos <= runningPos + lineLen) {
|
|
1197
|
+
logicalLine = i;
|
|
1198
|
+
offsetInLogicalLine = pos - runningPos;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
runningPos += lineLen + 1;
|
|
1202
|
+
}
|
|
1203
|
+
let wrappedLinesBefore = 0;
|
|
1204
|
+
for (let i = 0; i < logicalLine; i++) {
|
|
1205
|
+
wrappedLinesBefore += wrapLines([rawLines[i]], innerWidth, wrapMode).length;
|
|
1206
|
+
}
|
|
1207
|
+
const wrappedCurrentLine = wrapLines([rawLines[logicalLine]], innerWidth, wrapMode);
|
|
1208
|
+
let charsProcessed = 0;
|
|
1209
|
+
let subLineIdx = 0;
|
|
1210
|
+
for (let i = 0; i < wrappedCurrentLine.length; i++) {
|
|
1211
|
+
const subLine = wrappedCurrentLine[i];
|
|
1212
|
+
if (offsetInLogicalLine <= charsProcessed + subLine.length) {
|
|
1213
|
+
subLineIdx = i;
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
charsProcessed += subLine.length;
|
|
1217
|
+
}
|
|
1218
|
+
cursorScreenLine = wrappedLinesBefore + subLineIdx;
|
|
1219
|
+
cursorScreenCol = stringWidth__default.default(rawLines[logicalLine].slice(charsProcessed, charsProcessed + (offsetInLogicalLine - charsProcessed)));
|
|
1220
|
+
}
|
|
1221
|
+
const scrollOffset = Math.max(0, cursorScreenLine - innerHeight + 1);
|
|
1222
|
+
for (let rowIdx = 0; rowIdx < innerHeight; rowIdx++) {
|
|
1223
|
+
const lineNum = scrollOffset + rowIdx;
|
|
1224
|
+
if (lineNum >= wrappedLines.length) break;
|
|
1225
|
+
const line = wrappedLines[lineNum];
|
|
1226
|
+
let col = 0;
|
|
1227
|
+
for (const char of line) {
|
|
1228
|
+
if (col >= innerWidth) break;
|
|
1229
|
+
const charWidth = stringWidth__default.default(char);
|
|
1230
|
+
if (charWidth > 0) {
|
|
1231
|
+
setClipped(
|
|
1232
|
+
fb,
|
|
1233
|
+
clip,
|
|
1234
|
+
innerX + col,
|
|
1235
|
+
innerY + rowIdx,
|
|
1236
|
+
char,
|
|
1237
|
+
textFg,
|
|
1238
|
+
inherited.bg,
|
|
1239
|
+
inherited.bold,
|
|
1240
|
+
textDim,
|
|
1241
|
+
inherited.italic,
|
|
1242
|
+
inherited.underline
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
col += charWidth;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (cursorInfo && cursorInfo.nodeId === node.focusId) {
|
|
1249
|
+
const screenRow = cursorScreenLine - scrollOffset;
|
|
1250
|
+
if (screenRow >= 0 && screenRow < innerHeight) {
|
|
1251
|
+
const cCol = Math.min(cursorScreenCol, innerWidth - 1);
|
|
1252
|
+
const cursorX = innerX + cCol;
|
|
1253
|
+
const cursorY = innerY + screenRow;
|
|
1254
|
+
if (isInClip(cursorX, cursorY, clip) && cursorX < innerX + innerWidth) {
|
|
1255
|
+
const existing = fb.get(cursorX, cursorY);
|
|
1256
|
+
const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
|
|
1257
|
+
const cursorFg = inherited.bg ?? "black";
|
|
1258
|
+
const cursorBg = inherited.color ?? "white";
|
|
1259
|
+
fb.setChar(
|
|
1260
|
+
cursorX,
|
|
1261
|
+
cursorY,
|
|
1262
|
+
cursorChar,
|
|
1263
|
+
cursorFg,
|
|
1264
|
+
cursorBg,
|
|
1265
|
+
existing?.bold,
|
|
1266
|
+
existing?.dim,
|
|
1267
|
+
existing?.italic,
|
|
1268
|
+
false
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
let col = 0;
|
|
1275
|
+
for (const char of displayText) {
|
|
1276
|
+
if (col >= innerWidth) break;
|
|
1277
|
+
const charWidth = stringWidth__default.default(char);
|
|
1278
|
+
if (charWidth > 0) {
|
|
1279
|
+
setClipped(
|
|
1280
|
+
fb,
|
|
1281
|
+
clip,
|
|
1282
|
+
innerX + col,
|
|
1283
|
+
innerY,
|
|
1284
|
+
char,
|
|
1285
|
+
textFg,
|
|
1286
|
+
inherited.bg,
|
|
1287
|
+
inherited.bold,
|
|
1288
|
+
textDim,
|
|
1289
|
+
inherited.italic,
|
|
1290
|
+
inherited.underline
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
col += charWidth;
|
|
1294
|
+
}
|
|
1295
|
+
if (cursorInfo && cursorInfo.nodeId === node.focusId) {
|
|
1296
|
+
const cursorCol = Math.min(cursorInfo.position, innerWidth - 1);
|
|
1297
|
+
const cursorX = innerX + cursorCol;
|
|
1298
|
+
if (isInClip(cursorX, innerY, clip) && cursorX < innerX + innerWidth) {
|
|
1299
|
+
const existing = fb.get(cursorX, innerY);
|
|
1300
|
+
const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
|
|
1301
|
+
const cursorFg = inherited.bg ?? "black";
|
|
1302
|
+
const cursorBg = inherited.color ?? "white";
|
|
1303
|
+
fb.setChar(
|
|
1304
|
+
cursorX,
|
|
1305
|
+
innerY,
|
|
1306
|
+
cursorChar,
|
|
1307
|
+
cursorFg,
|
|
1308
|
+
cursorBg,
|
|
1309
|
+
existing?.bold,
|
|
1310
|
+
existing?.dim,
|
|
1311
|
+
existing?.italic,
|
|
1312
|
+
false
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/paint/diff.ts
|
|
1320
|
+
var ESC2 = "\x1B";
|
|
1321
|
+
var CSI2 = `${ESC2}[`;
|
|
1322
|
+
function moveCursor(x, y) {
|
|
1323
|
+
return `${CSI2}${y + 1};${x + 1}H`;
|
|
1324
|
+
}
|
|
1325
|
+
function buildSGR(cell) {
|
|
1326
|
+
let seq = `${CSI2}0m`;
|
|
1327
|
+
if (cell.bold) seq += `${CSI2}1m`;
|
|
1328
|
+
if (cell.dim) seq += `${CSI2}2m`;
|
|
1329
|
+
if (cell.italic) seq += `${CSI2}3m`;
|
|
1330
|
+
if (cell.underline) seq += `${CSI2}4m`;
|
|
1331
|
+
if (cell.fg != null) seq += colorToFg(cell.fg);
|
|
1332
|
+
if (cell.bg != null) seq += colorToBg(cell.bg);
|
|
1333
|
+
return seq;
|
|
1334
|
+
}
|
|
1335
|
+
function diffFramebuffers(prev, next, fullRedraw) {
|
|
1336
|
+
let out = "";
|
|
1337
|
+
let lastX = -1;
|
|
1338
|
+
let lastY = -1;
|
|
1339
|
+
let lastSGR = "";
|
|
1340
|
+
for (let y = 0; y < next.height; y++) {
|
|
1341
|
+
for (let x = 0; x < next.width; x++) {
|
|
1342
|
+
const nc = next.get(x, y);
|
|
1343
|
+
if (!fullRedraw) {
|
|
1344
|
+
const pc = prev.get(x, y);
|
|
1345
|
+
if (pc && next.cellsEqual(nc, pc)) continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (lastY !== y || lastX !== x) {
|
|
1348
|
+
out += moveCursor(x, y);
|
|
1349
|
+
}
|
|
1350
|
+
const sgr = buildSGR(nc);
|
|
1351
|
+
if (sgr !== lastSGR) {
|
|
1352
|
+
out += sgr;
|
|
1353
|
+
lastSGR = sgr;
|
|
1354
|
+
}
|
|
1355
|
+
out += nc.ch;
|
|
1356
|
+
lastX = x + 1;
|
|
1357
|
+
lastY = y;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (out.length > 0) {
|
|
1361
|
+
out += `${CSI2}0m`;
|
|
1362
|
+
}
|
|
1363
|
+
return out;
|
|
1364
|
+
}
|
|
1365
|
+
var FLEX_DIR_MAP = {
|
|
1366
|
+
row: Yoga.FlexDirection.Row,
|
|
1367
|
+
column: Yoga.FlexDirection.Column
|
|
1368
|
+
};
|
|
1369
|
+
var JUSTIFY_MAP = {
|
|
1370
|
+
"flex-start": Yoga.Justify.FlexStart,
|
|
1371
|
+
center: Yoga.Justify.Center,
|
|
1372
|
+
"flex-end": Yoga.Justify.FlexEnd,
|
|
1373
|
+
"space-between": Yoga.Justify.SpaceBetween,
|
|
1374
|
+
"space-around": Yoga.Justify.SpaceAround
|
|
1375
|
+
};
|
|
1376
|
+
var ALIGN_MAP = {
|
|
1377
|
+
"flex-start": Yoga.Align.FlexStart,
|
|
1378
|
+
center: Yoga.Align.Center,
|
|
1379
|
+
"flex-end": Yoga.Align.FlexEnd,
|
|
1380
|
+
stretch: Yoga.Align.Stretch
|
|
1381
|
+
};
|
|
1382
|
+
function setDimension(node, setter, value) {
|
|
1383
|
+
if (value === void 0) return;
|
|
1384
|
+
if (typeof value === "string" && value.endsWith("%")) {
|
|
1385
|
+
setter(value);
|
|
1386
|
+
} else {
|
|
1387
|
+
setter(value);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function setPosition(node, edge, value) {
|
|
1391
|
+
if (value === void 0) return;
|
|
1392
|
+
if (typeof value === "string" && value.endsWith("%")) {
|
|
1393
|
+
node.setPositionPercent(edge, parseFloat(value));
|
|
1394
|
+
} else {
|
|
1395
|
+
node.setPosition(edge, value);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function applyStyleToYogaNode(yogaNode, style, nodeType) {
|
|
1399
|
+
setDimension(yogaNode, (v) => yogaNode.setWidth(v), style.width);
|
|
1400
|
+
setDimension(yogaNode, (v) => yogaNode.setHeight(v), style.height);
|
|
1401
|
+
if (style.minWidth !== void 0) yogaNode.setMinWidth(style.minWidth);
|
|
1402
|
+
if (style.minHeight !== void 0) yogaNode.setMinHeight(style.minHeight);
|
|
1403
|
+
if (style.maxWidth !== void 0) yogaNode.setMaxWidth(style.maxWidth);
|
|
1404
|
+
if (style.maxHeight !== void 0) yogaNode.setMaxHeight(style.maxHeight);
|
|
1405
|
+
if (style.padding !== void 0) yogaNode.setPadding(Yoga.Edge.All, style.padding);
|
|
1406
|
+
if (style.paddingX !== void 0) yogaNode.setPadding(Yoga.Edge.Horizontal, style.paddingX);
|
|
1407
|
+
if (style.paddingY !== void 0) yogaNode.setPadding(Yoga.Edge.Vertical, style.paddingY);
|
|
1408
|
+
if (style.paddingTop !== void 0) yogaNode.setPadding(Yoga.Edge.Top, style.paddingTop);
|
|
1409
|
+
if (style.paddingRight !== void 0) yogaNode.setPadding(Yoga.Edge.Right, style.paddingRight);
|
|
1410
|
+
if (style.paddingBottom !== void 0) yogaNode.setPadding(Yoga.Edge.Bottom, style.paddingBottom);
|
|
1411
|
+
if (style.paddingLeft !== void 0) yogaNode.setPadding(Yoga.Edge.Left, style.paddingLeft);
|
|
1412
|
+
const hasBorder = style.border != null && style.border !== "none";
|
|
1413
|
+
yogaNode.setBorder(Yoga.Edge.All, hasBorder ? 1 : 0);
|
|
1414
|
+
if (style.flexDirection) {
|
|
1415
|
+
yogaNode.setFlexDirection(FLEX_DIR_MAP[style.flexDirection] ?? Yoga.FlexDirection.Column);
|
|
1416
|
+
}
|
|
1417
|
+
if (style.flexWrap) {
|
|
1418
|
+
yogaNode.setFlexWrap(style.flexWrap === "wrap" ? Yoga.Wrap.Wrap : Yoga.Wrap.NoWrap);
|
|
1419
|
+
}
|
|
1420
|
+
if (style.justifyContent) {
|
|
1421
|
+
yogaNode.setJustifyContent(JUSTIFY_MAP[style.justifyContent] ?? Yoga.Justify.FlexStart);
|
|
1422
|
+
}
|
|
1423
|
+
if (style.alignItems) {
|
|
1424
|
+
yogaNode.setAlignItems(ALIGN_MAP[style.alignItems] ?? Yoga.Align.Stretch);
|
|
1425
|
+
}
|
|
1426
|
+
if (style.flexGrow !== void 0) yogaNode.setFlexGrow(style.flexGrow);
|
|
1427
|
+
if (style.flexShrink !== void 0) yogaNode.setFlexShrink(style.flexShrink);
|
|
1428
|
+
if (style.gap !== void 0) yogaNode.setGap(Yoga.Gutter.All, style.gap);
|
|
1429
|
+
if (style.position === "absolute") {
|
|
1430
|
+
yogaNode.setPositionType(Yoga.PositionType.Absolute);
|
|
1431
|
+
} else {
|
|
1432
|
+
yogaNode.setPositionType(Yoga.PositionType.Relative);
|
|
1433
|
+
}
|
|
1434
|
+
if (style.inset !== void 0) {
|
|
1435
|
+
setPosition(yogaNode, Yoga.Edge.Top, style.inset);
|
|
1436
|
+
setPosition(yogaNode, Yoga.Edge.Right, style.inset);
|
|
1437
|
+
setPosition(yogaNode, Yoga.Edge.Bottom, style.inset);
|
|
1438
|
+
setPosition(yogaNode, Yoga.Edge.Left, style.inset);
|
|
1439
|
+
}
|
|
1440
|
+
setPosition(yogaNode, Yoga.Edge.Top, style.top);
|
|
1441
|
+
setPosition(yogaNode, Yoga.Edge.Right, style.right);
|
|
1442
|
+
setPosition(yogaNode, Yoga.Edge.Bottom, style.bottom);
|
|
1443
|
+
setPosition(yogaNode, Yoga.Edge.Left, style.left);
|
|
1444
|
+
if (style.clip) {
|
|
1445
|
+
yogaNode.setOverflow(Yoga.Overflow.Hidden);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function buildYogaTree(node) {
|
|
1449
|
+
const yogaNode = Yoga__default.default.Node.create();
|
|
1450
|
+
node.yogaNode = yogaNode;
|
|
1451
|
+
applyStyleToYogaNode(yogaNode, node.style, node.type);
|
|
1452
|
+
if (node.type === "text" || node.type === "input") {
|
|
1453
|
+
yogaNode.setMeasureFunc((width, widthMode, height, heightMode) => {
|
|
1454
|
+
let text;
|
|
1455
|
+
if (node.type === "input") {
|
|
1456
|
+
text = node.props.value ?? node.props.defaultValue ?? node.props.placeholder ?? "";
|
|
1457
|
+
if (text.length === 0) text = " ";
|
|
1458
|
+
} else {
|
|
1459
|
+
text = collectAllText(node);
|
|
1460
|
+
}
|
|
1461
|
+
return measureText(
|
|
1462
|
+
text,
|
|
1463
|
+
width,
|
|
1464
|
+
widthMode,
|
|
1465
|
+
node.style.wrap ?? "wrap"
|
|
1466
|
+
);
|
|
1467
|
+
});
|
|
1468
|
+
} else {
|
|
1469
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
1470
|
+
const child = node.children[i];
|
|
1471
|
+
if (child.hidden) continue;
|
|
1472
|
+
buildYogaTree(child);
|
|
1473
|
+
yogaNode.insertChild(child.yogaNode, yogaNode.getChildCount());
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function collectAllText(node) {
|
|
1478
|
+
if (node.text != null) return node.text;
|
|
1479
|
+
let result = "";
|
|
1480
|
+
for (const child of node.children) {
|
|
1481
|
+
result += collectAllText(child);
|
|
1482
|
+
}
|
|
1483
|
+
if (result === "" && node.props.children != null) {
|
|
1484
|
+
if (typeof node.props.children === "string") return node.props.children;
|
|
1485
|
+
if (typeof node.props.children === "number") return String(node.props.children);
|
|
1486
|
+
}
|
|
1487
|
+
return result;
|
|
1488
|
+
}
|
|
1489
|
+
function extractLayout(node, parentX, parentY) {
|
|
1490
|
+
const yn = node.yogaNode;
|
|
1491
|
+
const computedLayout = yn.getComputedLayout();
|
|
1492
|
+
const x = parentX + computedLayout.left;
|
|
1493
|
+
const y = parentY + computedLayout.top;
|
|
1494
|
+
const width = computedLayout.width;
|
|
1495
|
+
const height = computedLayout.height;
|
|
1496
|
+
const borderWidth = node.style.border && node.style.border !== "none" ? 1 : 0;
|
|
1497
|
+
const paddingTop = yn.getComputedPadding(Yoga.Edge.Top);
|
|
1498
|
+
const paddingRight = yn.getComputedPadding(Yoga.Edge.Right);
|
|
1499
|
+
const paddingBottom = yn.getComputedPadding(Yoga.Edge.Bottom);
|
|
1500
|
+
const paddingLeft = yn.getComputedPadding(Yoga.Edge.Left);
|
|
1501
|
+
const innerX = x + borderWidth + paddingLeft;
|
|
1502
|
+
const innerY = y + borderWidth + paddingTop;
|
|
1503
|
+
const innerWidth = Math.max(0, width - borderWidth * 2 - paddingLeft - paddingRight);
|
|
1504
|
+
const innerHeight = Math.max(0, height - borderWidth * 2 - paddingTop - paddingBottom);
|
|
1505
|
+
node.layout = { x, y, width, height, innerX, innerY, innerWidth, innerHeight };
|
|
1506
|
+
for (const child of node.children) {
|
|
1507
|
+
if (child.hidden || !child.yogaNode) continue;
|
|
1508
|
+
extractLayout(child, x, y);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
function computeLayout(roots, screenWidth, screenHeight) {
|
|
1512
|
+
const rootYoga = Yoga__default.default.Node.create();
|
|
1513
|
+
rootYoga.setWidth(screenWidth);
|
|
1514
|
+
rootYoga.setHeight(screenHeight);
|
|
1515
|
+
rootYoga.setFlexDirection(Yoga.FlexDirection.Column);
|
|
1516
|
+
for (const child of roots) {
|
|
1517
|
+
if (child.hidden) continue;
|
|
1518
|
+
buildYogaTree(child);
|
|
1519
|
+
rootYoga.insertChild(child.yogaNode, rootYoga.getChildCount());
|
|
1520
|
+
}
|
|
1521
|
+
rootYoga.calculateLayout(screenWidth, screenHeight, Yoga.Direction.LTR);
|
|
1522
|
+
for (const child of roots) {
|
|
1523
|
+
if (child.hidden || !child.yogaNode) continue;
|
|
1524
|
+
extractLayout(child, 0, 0);
|
|
1525
|
+
}
|
|
1526
|
+
rootYoga.freeRecursive();
|
|
1527
|
+
clearYogaRefs(roots);
|
|
1528
|
+
}
|
|
1529
|
+
function clearYogaRefs(nodes) {
|
|
1530
|
+
for (const node of nodes) {
|
|
1531
|
+
node.yogaNode = null;
|
|
1532
|
+
clearYogaRefs(node.children);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
var InputContext = React15.createContext(null);
|
|
1536
|
+
var FocusContext = React15.createContext(null);
|
|
1537
|
+
var LayoutContext = React15.createContext(null);
|
|
1538
|
+
var AppContext = React15.createContext(null);
|
|
1539
|
+
|
|
1540
|
+
// src/render.ts
|
|
1541
|
+
function render(element, opts = {}) {
|
|
1542
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
1543
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
1544
|
+
const debug = opts.debug ?? false;
|
|
1545
|
+
const terminal = new Terminal(stdout, stdin);
|
|
1546
|
+
terminal.setup();
|
|
1547
|
+
terminal.queryPalette().then((palette) => {
|
|
1548
|
+
setTerminalPalette(palette);
|
|
1549
|
+
fullRedraw = true;
|
|
1550
|
+
scheduleRender();
|
|
1551
|
+
});
|
|
1552
|
+
const prevFb = new Framebuffer(terminal.columns, terminal.rows);
|
|
1553
|
+
const currentFb = new Framebuffer(terminal.columns, terminal.rows);
|
|
1554
|
+
let fullRedraw = true;
|
|
1555
|
+
const inputHandlers = /* @__PURE__ */ new Set();
|
|
1556
|
+
const focusedInputHandlers = /* @__PURE__ */ new Map();
|
|
1557
|
+
const inputContextValue = {
|
|
1558
|
+
subscribe(handler) {
|
|
1559
|
+
inputHandlers.add(handler);
|
|
1560
|
+
return () => inputHandlers.delete(handler);
|
|
1561
|
+
},
|
|
1562
|
+
registerInputHandler(focusId, handler) {
|
|
1563
|
+
focusedInputHandlers.set(focusId, handler);
|
|
1564
|
+
return () => focusedInputHandlers.delete(focusId);
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
let focusedId = null;
|
|
1568
|
+
const focusRegistry = /* @__PURE__ */ new Map();
|
|
1569
|
+
const focusOrder = [];
|
|
1570
|
+
let trapStack = [];
|
|
1571
|
+
const focusChangeHandlers = /* @__PURE__ */ new Set();
|
|
1572
|
+
function setFocusedId(id) {
|
|
1573
|
+
if (focusedId !== id) {
|
|
1574
|
+
focusedId = id;
|
|
1575
|
+
scheduleRender();
|
|
1576
|
+
for (const handler of focusChangeHandlers) {
|
|
1577
|
+
handler(focusedId);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function getActiveFocusableIds() {
|
|
1582
|
+
if (trapStack.length > 0) {
|
|
1583
|
+
const trap = trapStack[trapStack.length - 1];
|
|
1584
|
+
return focusOrder.filter((id) => trap.has(id));
|
|
1585
|
+
}
|
|
1586
|
+
return focusOrder;
|
|
1587
|
+
}
|
|
1588
|
+
const focusContextValue = {
|
|
1589
|
+
get focusedId() {
|
|
1590
|
+
return focusedId;
|
|
1591
|
+
},
|
|
1592
|
+
register(id, node) {
|
|
1593
|
+
focusRegistry.set(id, node);
|
|
1594
|
+
if (!focusOrder.includes(id)) {
|
|
1595
|
+
focusOrder.push(id);
|
|
1596
|
+
}
|
|
1597
|
+
if (trapStack.length > 0) {
|
|
1598
|
+
trapStack[trapStack.length - 1].add(id);
|
|
1599
|
+
}
|
|
1600
|
+
if (focusedId === null) {
|
|
1601
|
+
setFocusedId(id);
|
|
1602
|
+
}
|
|
1603
|
+
return () => {
|
|
1604
|
+
focusRegistry.delete(id);
|
|
1605
|
+
const idx = focusOrder.indexOf(id);
|
|
1606
|
+
if (idx !== -1) focusOrder.splice(idx, 1);
|
|
1607
|
+
if (focusedId === id) {
|
|
1608
|
+
setFocusedId(focusOrder[0] ?? null);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
},
|
|
1612
|
+
requestFocus(id) {
|
|
1613
|
+
setFocusedId(id);
|
|
1614
|
+
},
|
|
1615
|
+
focusNext() {
|
|
1616
|
+
const ids = getActiveFocusableIds();
|
|
1617
|
+
if (ids.length === 0) return;
|
|
1618
|
+
const currentIdx = focusedId ? ids.indexOf(focusedId) : -1;
|
|
1619
|
+
const nextIdx = (currentIdx + 1) % ids.length;
|
|
1620
|
+
setFocusedId(ids[nextIdx]);
|
|
1621
|
+
},
|
|
1622
|
+
focusPrev() {
|
|
1623
|
+
const ids = getActiveFocusableIds();
|
|
1624
|
+
if (ids.length === 0) return;
|
|
1625
|
+
const currentIdx = focusedId ? ids.indexOf(focusedId) : 0;
|
|
1626
|
+
const prevIdx = (currentIdx - 1 + ids.length) % ids.length;
|
|
1627
|
+
setFocusedId(ids[prevIdx]);
|
|
1628
|
+
},
|
|
1629
|
+
trapIds: null,
|
|
1630
|
+
pushTrap(ids) {
|
|
1631
|
+
trapStack.push(ids);
|
|
1632
|
+
return () => {
|
|
1633
|
+
const idx = trapStack.indexOf(ids);
|
|
1634
|
+
if (idx !== -1) trapStack.splice(idx, 1);
|
|
1635
|
+
};
|
|
1636
|
+
},
|
|
1637
|
+
onFocusChange(handler) {
|
|
1638
|
+
focusChangeHandlers.add(handler);
|
|
1639
|
+
return () => {
|
|
1640
|
+
focusChangeHandlers.delete(handler);
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
const layoutSubscriptions = /* @__PURE__ */ new Map();
|
|
1645
|
+
const layoutContextValue = {
|
|
1646
|
+
getLayout(node) {
|
|
1647
|
+
return node.layout;
|
|
1648
|
+
},
|
|
1649
|
+
subscribe(node, handler) {
|
|
1650
|
+
if (!layoutSubscriptions.has(node)) {
|
|
1651
|
+
layoutSubscriptions.set(node, /* @__PURE__ */ new Set());
|
|
1652
|
+
}
|
|
1653
|
+
layoutSubscriptions.get(node).add(handler);
|
|
1654
|
+
return () => {
|
|
1655
|
+
const subs = layoutSubscriptions.get(node);
|
|
1656
|
+
if (subs) {
|
|
1657
|
+
subs.delete(handler);
|
|
1658
|
+
if (subs.size === 0) layoutSubscriptions.delete(node);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
const appContextValue = {
|
|
1664
|
+
registerNode() {
|
|
1665
|
+
},
|
|
1666
|
+
unregisterNode() {
|
|
1667
|
+
},
|
|
1668
|
+
scheduleRender,
|
|
1669
|
+
exit(code) {
|
|
1670
|
+
handle.exit(code);
|
|
1671
|
+
},
|
|
1672
|
+
get columns() {
|
|
1673
|
+
return terminal.columns;
|
|
1674
|
+
},
|
|
1675
|
+
get rows() {
|
|
1676
|
+
return terminal.rows;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
const container = {
|
|
1680
|
+
type: "root",
|
|
1681
|
+
children: [],
|
|
1682
|
+
onCommit() {
|
|
1683
|
+
scheduleRender();
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
let renderScheduled = false;
|
|
1687
|
+
function scheduleRender() {
|
|
1688
|
+
if (renderScheduled) return;
|
|
1689
|
+
renderScheduled = true;
|
|
1690
|
+
queueMicrotask(() => {
|
|
1691
|
+
renderScheduled = false;
|
|
1692
|
+
performRender();
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
function performRender() {
|
|
1696
|
+
const cols = terminal.columns;
|
|
1697
|
+
const rows = terminal.rows;
|
|
1698
|
+
if (currentFb.width !== cols || currentFb.height !== rows) {
|
|
1699
|
+
currentFb.resize(cols, rows);
|
|
1700
|
+
prevFb.resize(cols, rows);
|
|
1701
|
+
fullRedraw = true;
|
|
1702
|
+
}
|
|
1703
|
+
computeLayout(container.children, cols, rows);
|
|
1704
|
+
notifyLayoutSubscribers(container.children);
|
|
1705
|
+
let cursorInfo;
|
|
1706
|
+
if (focusedId) {
|
|
1707
|
+
const focusedNode = focusRegistry.get(focusedId);
|
|
1708
|
+
if (focusedNode?.type === "input") {
|
|
1709
|
+
cursorInfo = {
|
|
1710
|
+
nodeId: focusedId,
|
|
1711
|
+
position: focusedNode.props.cursorPosition ?? (focusedNode.props.value?.length ?? 0)
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
paintTree(container.children, currentFb, cursorInfo);
|
|
1716
|
+
const output = diffFramebuffers(prevFb, currentFb, fullRedraw);
|
|
1717
|
+
if (output.length > 0) {
|
|
1718
|
+
terminal.write(output);
|
|
1719
|
+
}
|
|
1720
|
+
for (let i = 0; i < currentFb.cells.length; i++) {
|
|
1721
|
+
prevFb.cells[i] = { ...currentFb.cells[i] };
|
|
1722
|
+
}
|
|
1723
|
+
fullRedraw = false;
|
|
1724
|
+
}
|
|
1725
|
+
function notifyLayoutSubscribers(nodes) {
|
|
1726
|
+
for (const node of nodes) {
|
|
1727
|
+
const subs = layoutSubscriptions.get(node);
|
|
1728
|
+
if (subs) {
|
|
1729
|
+
for (const handler of subs) {
|
|
1730
|
+
handler(node.layout);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
notifyLayoutSubscribers(node.children);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const removeDataListener = terminal.onData((data) => {
|
|
1737
|
+
const keys = parseKeySequence(data);
|
|
1738
|
+
for (const key of keys) {
|
|
1739
|
+
if (key.ctrl && key.name === "c") {
|
|
1740
|
+
handle.exit();
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (key.ctrl && key.name === "z") {
|
|
1744
|
+
terminal.suspend();
|
|
1745
|
+
process.kill(0, "SIGSTOP");
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (key.name === "tab" && !key.ctrl && !key.alt) {
|
|
1749
|
+
if (key.shift) {
|
|
1750
|
+
focusContextValue.focusPrev();
|
|
1751
|
+
} else {
|
|
1752
|
+
focusContextValue.focusNext();
|
|
1753
|
+
}
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
let consumed = false;
|
|
1757
|
+
if (focusedId) {
|
|
1758
|
+
const inputHandler = focusedInputHandlers.get(focusedId);
|
|
1759
|
+
if (inputHandler) {
|
|
1760
|
+
consumed = inputHandler(key);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (!consumed) {
|
|
1764
|
+
for (const handler of inputHandlers) {
|
|
1765
|
+
handler(key);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
const removeResizeListener = terminal.onResize(() => {
|
|
1771
|
+
fullRedraw = true;
|
|
1772
|
+
scheduleRender();
|
|
1773
|
+
});
|
|
1774
|
+
const handleSigcont = () => {
|
|
1775
|
+
terminal.resume();
|
|
1776
|
+
fullRedraw = true;
|
|
1777
|
+
scheduleRender();
|
|
1778
|
+
};
|
|
1779
|
+
process.on("SIGCONT", handleSigcont);
|
|
1780
|
+
const wrappedElement = React15__default.default.createElement(
|
|
1781
|
+
AppContext.Provider,
|
|
1782
|
+
{ value: appContextValue },
|
|
1783
|
+
React15__default.default.createElement(
|
|
1784
|
+
InputContext.Provider,
|
|
1785
|
+
{ value: inputContextValue },
|
|
1786
|
+
React15__default.default.createElement(
|
|
1787
|
+
FocusContext.Provider,
|
|
1788
|
+
{ value: focusContextValue },
|
|
1789
|
+
React15__default.default.createElement(
|
|
1790
|
+
LayoutContext.Provider,
|
|
1791
|
+
{ value: layoutContextValue },
|
|
1792
|
+
element
|
|
1793
|
+
)
|
|
1794
|
+
)
|
|
1795
|
+
)
|
|
1796
|
+
);
|
|
1797
|
+
const root = reconciler.createContainer(
|
|
1798
|
+
container,
|
|
1799
|
+
0,
|
|
1800
|
+
// ConcurrentRoot tag = 0 (LegacyRoot)
|
|
1801
|
+
null,
|
|
1802
|
+
false,
|
|
1803
|
+
null,
|
|
1804
|
+
"",
|
|
1805
|
+
(err) => {
|
|
1806
|
+
if (debug) console.error("Recoverable error:", err);
|
|
1807
|
+
},
|
|
1808
|
+
null
|
|
1809
|
+
);
|
|
1810
|
+
reconciler.updateContainer(wrappedElement, root, null, null);
|
|
1811
|
+
const handle = {
|
|
1812
|
+
unmount() {
|
|
1813
|
+
reconciler.updateContainer(null, root, null, null);
|
|
1814
|
+
removeDataListener();
|
|
1815
|
+
removeResizeListener();
|
|
1816
|
+
process.off("SIGCONT", handleSigcont);
|
|
1817
|
+
terminal.cleanup();
|
|
1818
|
+
},
|
|
1819
|
+
exit(code) {
|
|
1820
|
+
handle.unmount();
|
|
1821
|
+
process.exit(code ?? 0);
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
return handle;
|
|
1825
|
+
}
|
|
1826
|
+
function Box({ children, style, focusable }) {
|
|
1827
|
+
return React15__default.default.createElement("box", { style, focusable }, children);
|
|
1828
|
+
}
|
|
1829
|
+
function Text({ children, style, wrap }) {
|
|
1830
|
+
const mergedStyle = wrap ? { ...style, wrap } : style;
|
|
1831
|
+
return React15__default.default.createElement("text", { style: mergedStyle }, children);
|
|
1832
|
+
}
|
|
1833
|
+
function cursorToVisualLine(text, pos, width) {
|
|
1834
|
+
if (width <= 0) {
|
|
1835
|
+
return { visualLine: 0, visualCol: pos, totalVisualLines: 1, lineStartOffset: 0, lineLength: text.length };
|
|
1836
|
+
}
|
|
1837
|
+
const logicalLines = text.split("\n");
|
|
1838
|
+
const allVisualLines = [];
|
|
1839
|
+
let logicalOffset = 0;
|
|
1840
|
+
for (const logicalLine of logicalLines) {
|
|
1841
|
+
const wrapped = wrapLines([logicalLine], width, "wrap");
|
|
1842
|
+
let offsetInLogical = 0;
|
|
1843
|
+
for (const wrappedLine of wrapped) {
|
|
1844
|
+
allVisualLines.push({
|
|
1845
|
+
text: wrappedLine,
|
|
1846
|
+
logicalOffset: logicalOffset + offsetInLogical
|
|
1847
|
+
});
|
|
1848
|
+
offsetInLogical += wrappedLine.length;
|
|
1849
|
+
}
|
|
1850
|
+
logicalOffset += logicalLine.length + 1;
|
|
1851
|
+
}
|
|
1852
|
+
let charCount = 0;
|
|
1853
|
+
for (let i = 0; i < allVisualLines.length; i++) {
|
|
1854
|
+
const vl = allVisualLines[i];
|
|
1855
|
+
const lineLen = vl.text.length;
|
|
1856
|
+
const isEndOfLogicalLine = i + 1 < allVisualLines.length && allVisualLines[i + 1].logicalOffset !== vl.logicalOffset + lineLen;
|
|
1857
|
+
const effectiveLen = lineLen + (isEndOfLogicalLine ? 1 : 0);
|
|
1858
|
+
if (pos < charCount + lineLen || i === allVisualLines.length - 1) {
|
|
1859
|
+
return {
|
|
1860
|
+
visualLine: i,
|
|
1861
|
+
visualCol: Math.min(pos - charCount, lineLen),
|
|
1862
|
+
totalVisualLines: allVisualLines.length,
|
|
1863
|
+
lineStartOffset: charCount,
|
|
1864
|
+
lineLength: lineLen
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
charCount += effectiveLen;
|
|
1868
|
+
}
|
|
1869
|
+
const lastIdx = allVisualLines.length - 1;
|
|
1870
|
+
return {
|
|
1871
|
+
visualLine: lastIdx,
|
|
1872
|
+
visualCol: allVisualLines[lastIdx].text.length,
|
|
1873
|
+
totalVisualLines: allVisualLines.length,
|
|
1874
|
+
lineStartOffset: charCount - allVisualLines[lastIdx].text.length,
|
|
1875
|
+
lineLength: allVisualLines[lastIdx].text.length
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
function visualLineToCursor(text, visualLine, visualCol, width) {
|
|
1879
|
+
if (width <= 0) {
|
|
1880
|
+
return Math.min(visualCol, text.length);
|
|
1881
|
+
}
|
|
1882
|
+
const logicalLines = text.split("\n");
|
|
1883
|
+
const allVisualLines = [];
|
|
1884
|
+
let offset = 0;
|
|
1885
|
+
for (const logicalLine of logicalLines) {
|
|
1886
|
+
const wrapped = wrapLines([logicalLine], width, "wrap");
|
|
1887
|
+
let offsetInLogical = 0;
|
|
1888
|
+
for (const wrappedLine of wrapped) {
|
|
1889
|
+
allVisualLines.push({
|
|
1890
|
+
text: wrappedLine,
|
|
1891
|
+
startOffset: offset + offsetInLogical
|
|
1892
|
+
});
|
|
1893
|
+
offsetInLogical += wrappedLine.length;
|
|
1894
|
+
}
|
|
1895
|
+
offset += logicalLine.length + 1;
|
|
1896
|
+
}
|
|
1897
|
+
const targetLine = Math.max(0, Math.min(visualLine, allVisualLines.length - 1));
|
|
1898
|
+
const vl = allVisualLines[targetLine];
|
|
1899
|
+
const col = Math.min(visualCol, vl.text.length);
|
|
1900
|
+
return vl.startOffset + col;
|
|
1901
|
+
}
|
|
1902
|
+
function cursorToLineCol(text, pos) {
|
|
1903
|
+
const lines = text.split("\n");
|
|
1904
|
+
let remaining = pos;
|
|
1905
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1906
|
+
if (remaining <= lines[i].length) {
|
|
1907
|
+
return { line: i, col: remaining, lines };
|
|
1908
|
+
}
|
|
1909
|
+
remaining -= lines[i].length + 1;
|
|
1910
|
+
}
|
|
1911
|
+
const last = lines.length - 1;
|
|
1912
|
+
return { line: last, col: lines[last].length, lines };
|
|
1913
|
+
}
|
|
1914
|
+
function lineColToCursor(lines, line, col) {
|
|
1915
|
+
let pos = 0;
|
|
1916
|
+
for (let i = 0; i < line && i < lines.length; i++) {
|
|
1917
|
+
pos += lines[i].length + 1;
|
|
1918
|
+
}
|
|
1919
|
+
return pos + Math.min(col, lines[line]?.length ?? 0);
|
|
1920
|
+
}
|
|
1921
|
+
function Input(props) {
|
|
1922
|
+
const {
|
|
1923
|
+
value: controlledValue,
|
|
1924
|
+
defaultValue = "",
|
|
1925
|
+
onChange,
|
|
1926
|
+
placeholder,
|
|
1927
|
+
style,
|
|
1928
|
+
focusedStyle,
|
|
1929
|
+
multiline
|
|
1930
|
+
} = props;
|
|
1931
|
+
const [internalValue, setInternalValue] = React15.useState(defaultValue);
|
|
1932
|
+
const [cursorPos, setCursorPos] = React15.useState(defaultValue.length);
|
|
1933
|
+
const [innerWidth, setInnerWidth] = React15.useState(0);
|
|
1934
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
1935
|
+
const inputCtx = React15.useContext(InputContext);
|
|
1936
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
1937
|
+
const layoutCtx = React15.useContext(LayoutContext);
|
|
1938
|
+
const nodeRef = React15.useRef(null);
|
|
1939
|
+
const focusIdRef = React15.useRef(null);
|
|
1940
|
+
const isControlled = controlledValue !== void 0;
|
|
1941
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
1942
|
+
React15.useEffect(() => {
|
|
1943
|
+
if (!layoutCtx || !nodeRef.current) return;
|
|
1944
|
+
const layout = layoutCtx.getLayout(nodeRef.current);
|
|
1945
|
+
setInnerWidth(layout.innerWidth);
|
|
1946
|
+
return layoutCtx.subscribe(nodeRef.current, (rect) => {
|
|
1947
|
+
setInnerWidth(rect.innerWidth);
|
|
1948
|
+
});
|
|
1949
|
+
}, [layoutCtx]);
|
|
1950
|
+
const workingValueRef = React15.useRef(value);
|
|
1951
|
+
const workingCursorRef = React15.useRef(cursorPos);
|
|
1952
|
+
React15.useEffect(() => {
|
|
1953
|
+
workingValueRef.current = value;
|
|
1954
|
+
}, [value]);
|
|
1955
|
+
React15.useEffect(() => {
|
|
1956
|
+
workingCursorRef.current = cursorPos;
|
|
1957
|
+
}, [cursorPos]);
|
|
1958
|
+
const stateRef = React15.useRef({
|
|
1959
|
+
isControlled,
|
|
1960
|
+
onChange,
|
|
1961
|
+
multiline: multiline ?? false,
|
|
1962
|
+
innerWidth
|
|
1963
|
+
});
|
|
1964
|
+
stateRef.current = {
|
|
1965
|
+
isControlled,
|
|
1966
|
+
onChange,
|
|
1967
|
+
multiline: multiline ?? false,
|
|
1968
|
+
innerWidth
|
|
1969
|
+
};
|
|
1970
|
+
React15.useEffect(() => {
|
|
1971
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
|
|
1972
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
1973
|
+
}, [focusCtx]);
|
|
1974
|
+
React15.useEffect(() => {
|
|
1975
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
1976
|
+
const fid = focusIdRef.current;
|
|
1977
|
+
setIsFocused(focusCtx.focusedId === fid);
|
|
1978
|
+
return focusCtx.onFocusChange((newId) => {
|
|
1979
|
+
setIsFocused(newId === fid);
|
|
1980
|
+
});
|
|
1981
|
+
}, [focusCtx]);
|
|
1982
|
+
React15.useEffect(() => {
|
|
1983
|
+
if (!inputCtx || !focusIdRef.current) return;
|
|
1984
|
+
const fid = focusIdRef.current;
|
|
1985
|
+
const handler = (key) => {
|
|
1986
|
+
const {
|
|
1987
|
+
isControlled: ctrl,
|
|
1988
|
+
onChange: cb,
|
|
1989
|
+
multiline: ml
|
|
1990
|
+
} = stateRef.current;
|
|
1991
|
+
const val = workingValueRef.current;
|
|
1992
|
+
const pos = workingCursorRef.current;
|
|
1993
|
+
if (key.name === "escape") return false;
|
|
1994
|
+
const updateValue = (newVal, newCursor) => {
|
|
1995
|
+
workingValueRef.current = newVal;
|
|
1996
|
+
workingCursorRef.current = newCursor;
|
|
1997
|
+
if (!ctrl) setInternalValue(newVal);
|
|
1998
|
+
cb?.(newVal);
|
|
1999
|
+
setCursorPos(newCursor);
|
|
2000
|
+
};
|
|
2001
|
+
const updateCursor = (newCursor) => {
|
|
2002
|
+
workingCursorRef.current = newCursor;
|
|
2003
|
+
setCursorPos(newCursor);
|
|
2004
|
+
};
|
|
2005
|
+
if (key.name === "return") {
|
|
2006
|
+
if (ml) {
|
|
2007
|
+
const newVal = val.slice(0, pos) + "\n" + val.slice(pos);
|
|
2008
|
+
updateValue(newVal, pos + 1);
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
if (key.ctrl) {
|
|
2014
|
+
if (key.name === "w") {
|
|
2015
|
+
if (pos > 0) {
|
|
2016
|
+
let i = pos;
|
|
2017
|
+
while (i > 0 && val[i - 1] === " ") i--;
|
|
2018
|
+
while (i > 0 && val[i - 1] !== " " && (!ml || val[i - 1] !== "\n"))
|
|
2019
|
+
i--;
|
|
2020
|
+
const newVal = val.slice(0, i) + val.slice(pos);
|
|
2021
|
+
updateValue(newVal, i);
|
|
2022
|
+
}
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
if (key.name === "a") {
|
|
2026
|
+
if (ml) {
|
|
2027
|
+
const { line, lines } = cursorToLineCol(val, pos);
|
|
2028
|
+
updateCursor(lineColToCursor(lines, line, 0));
|
|
2029
|
+
} else {
|
|
2030
|
+
updateCursor(0);
|
|
2031
|
+
}
|
|
2032
|
+
return true;
|
|
2033
|
+
}
|
|
2034
|
+
if (key.name === "e") {
|
|
2035
|
+
if (ml) {
|
|
2036
|
+
const { line, lines } = cursorToLineCol(val, pos);
|
|
2037
|
+
updateCursor(lineColToCursor(lines, line, lines[line].length));
|
|
2038
|
+
} else {
|
|
2039
|
+
updateCursor(val.length);
|
|
2040
|
+
}
|
|
2041
|
+
return true;
|
|
2042
|
+
}
|
|
2043
|
+
if (key.name === "u") {
|
|
2044
|
+
if (ml) {
|
|
2045
|
+
const { line, lines } = cursorToLineCol(val, pos);
|
|
2046
|
+
const lineStart = lineColToCursor(lines, line, 0);
|
|
2047
|
+
if (pos > lineStart) {
|
|
2048
|
+
const newVal = val.slice(0, lineStart) + val.slice(pos);
|
|
2049
|
+
updateValue(newVal, lineStart);
|
|
2050
|
+
}
|
|
2051
|
+
} else {
|
|
2052
|
+
if (pos > 0) {
|
|
2053
|
+
const newVal = val.slice(pos);
|
|
2054
|
+
updateValue(newVal, 0);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return true;
|
|
2058
|
+
}
|
|
2059
|
+
if (key.name === "k") {
|
|
2060
|
+
if (ml) {
|
|
2061
|
+
const { line, lines } = cursorToLineCol(val, pos);
|
|
2062
|
+
const lineEnd = lineColToCursor(lines, line, lines[line].length);
|
|
2063
|
+
if (pos < lineEnd) {
|
|
2064
|
+
const newVal = val.slice(0, pos) + val.slice(lineEnd);
|
|
2065
|
+
updateValue(newVal, pos);
|
|
2066
|
+
}
|
|
2067
|
+
} else {
|
|
2068
|
+
if (pos < val.length) {
|
|
2069
|
+
const newVal = val.slice(0, pos);
|
|
2070
|
+
updateValue(newVal, pos);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
if (key.alt) {
|
|
2078
|
+
if (key.name === "left" || key.name === "b") {
|
|
2079
|
+
let i = pos;
|
|
2080
|
+
while (i > 0 && val[i - 1] === " ") i--;
|
|
2081
|
+
while (i > 0 && val[i - 1] !== " " && val[i - 1] !== "\n") i--;
|
|
2082
|
+
updateCursor(i);
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
if (key.name === "right" || key.name === "f") {
|
|
2086
|
+
let i = pos;
|
|
2087
|
+
while (i < val.length && val[i] !== " " && val[i] !== "\n") i++;
|
|
2088
|
+
while (i < val.length && val[i] === " ") i++;
|
|
2089
|
+
updateCursor(i);
|
|
2090
|
+
return true;
|
|
2091
|
+
}
|
|
2092
|
+
if (key.name === "backspace" || key.name === "d") {
|
|
2093
|
+
if (key.name === "backspace") {
|
|
2094
|
+
if (pos > 0) {
|
|
2095
|
+
let i = pos;
|
|
2096
|
+
while (i > 0 && val[i - 1] === " ") i--;
|
|
2097
|
+
while (i > 0 && val[i - 1] !== " " && val[i - 1] !== "\n") i--;
|
|
2098
|
+
const newVal = val.slice(0, i) + val.slice(pos);
|
|
2099
|
+
updateValue(newVal, i);
|
|
2100
|
+
}
|
|
2101
|
+
return true;
|
|
2102
|
+
} else {
|
|
2103
|
+
if (pos < val.length) {
|
|
2104
|
+
let i = pos;
|
|
2105
|
+
while (i < val.length && val[i] !== " " && val[i] !== "\n") i++;
|
|
2106
|
+
while (i < val.length && val[i] === " ") i++;
|
|
2107
|
+
const newVal = val.slice(0, pos) + val.slice(i);
|
|
2108
|
+
updateValue(newVal, pos);
|
|
2109
|
+
}
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return false;
|
|
2114
|
+
}
|
|
2115
|
+
if (key.name === "left") {
|
|
2116
|
+
updateCursor(Math.max(0, pos - 1));
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
if (key.name === "right") {
|
|
2120
|
+
updateCursor(Math.min(val.length, pos + 1));
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
if (key.name === "up") {
|
|
2124
|
+
const { innerWidth: w } = stateRef.current;
|
|
2125
|
+
const info = cursorToVisualLine(val, pos, w);
|
|
2126
|
+
if (info.visualLine > 0) {
|
|
2127
|
+
updateCursor(visualLineToCursor(val, info.visualLine - 1, info.visualCol, w));
|
|
2128
|
+
}
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
if (key.name === "down") {
|
|
2132
|
+
const { innerWidth: w } = stateRef.current;
|
|
2133
|
+
const info = cursorToVisualLine(val, pos, w);
|
|
2134
|
+
if (info.visualLine < info.totalVisualLines - 1) {
|
|
2135
|
+
updateCursor(visualLineToCursor(val, info.visualLine + 1, info.visualCol, w));
|
|
2136
|
+
}
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
if (key.name === "home") {
|
|
2140
|
+
if (ml) {
|
|
2141
|
+
const { line, lines } = cursorToLineCol(val, pos);
|
|
2142
|
+
updateCursor(lineColToCursor(lines, line, 0));
|
|
2143
|
+
} else {
|
|
2144
|
+
updateCursor(0);
|
|
2145
|
+
}
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
if (key.name === "end") {
|
|
2149
|
+
if (ml) {
|
|
2150
|
+
const { line, lines } = cursorToLineCol(val, pos);
|
|
2151
|
+
updateCursor(lineColToCursor(lines, line, lines[line].length));
|
|
2152
|
+
} else {
|
|
2153
|
+
updateCursor(val.length);
|
|
2154
|
+
}
|
|
2155
|
+
return true;
|
|
2156
|
+
}
|
|
2157
|
+
if (key.name === "backspace") {
|
|
2158
|
+
if (pos > 0) {
|
|
2159
|
+
const newVal = val.slice(0, pos - 1) + val.slice(pos);
|
|
2160
|
+
updateValue(newVal, pos - 1);
|
|
2161
|
+
}
|
|
2162
|
+
return true;
|
|
2163
|
+
}
|
|
2164
|
+
if (key.name === "delete") {
|
|
2165
|
+
if (pos < val.length) {
|
|
2166
|
+
const newVal = val.slice(0, pos) + val.slice(pos + 1);
|
|
2167
|
+
updateValue(newVal, pos);
|
|
2168
|
+
}
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
if (key.name.length > 1) return false;
|
|
2172
|
+
const ch = key.sequence;
|
|
2173
|
+
if (ch.length === 1 && ch.charCodeAt(0) >= 32) {
|
|
2174
|
+
const newVal = val.slice(0, pos) + ch + val.slice(pos);
|
|
2175
|
+
updateValue(newVal, pos + 1);
|
|
2176
|
+
return true;
|
|
2177
|
+
}
|
|
2178
|
+
return false;
|
|
2179
|
+
};
|
|
2180
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
2181
|
+
}, [inputCtx]);
|
|
2182
|
+
const mergedStyle = {
|
|
2183
|
+
...style,
|
|
2184
|
+
...isFocused && focusedStyle ? focusedStyle : {}
|
|
2185
|
+
};
|
|
2186
|
+
return React15__default.default.createElement("input", {
|
|
2187
|
+
style: mergedStyle,
|
|
2188
|
+
value,
|
|
2189
|
+
defaultValue,
|
|
2190
|
+
placeholder,
|
|
2191
|
+
onChange,
|
|
2192
|
+
cursorPosition: cursorPos,
|
|
2193
|
+
multiline: multiline ?? false,
|
|
2194
|
+
focused: isFocused,
|
|
2195
|
+
ref: (node) => {
|
|
2196
|
+
if (node) {
|
|
2197
|
+
nodeRef.current = node;
|
|
2198
|
+
focusIdRef.current = node.focusId;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
function FocusScope({ trap = false, children }) {
|
|
2204
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
2205
|
+
const prevFocusRef = React15.useRef(null);
|
|
2206
|
+
const scopeIdsRef = React15.useRef(/* @__PURE__ */ new Set());
|
|
2207
|
+
React15.useLayoutEffect(() => {
|
|
2208
|
+
if (!trap || !focusCtx) return;
|
|
2209
|
+
prevFocusRef.current = focusCtx.focusedId;
|
|
2210
|
+
const cleanup = focusCtx.pushTrap(scopeIdsRef.current);
|
|
2211
|
+
return () => {
|
|
2212
|
+
cleanup();
|
|
2213
|
+
if (prevFocusRef.current) {
|
|
2214
|
+
focusCtx.requestFocus(prevFocusRef.current);
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
}, [trap, focusCtx]);
|
|
2218
|
+
React15.useEffect(() => {
|
|
2219
|
+
if (!trap || !focusCtx) return;
|
|
2220
|
+
if (scopeIdsRef.current.size > 0) {
|
|
2221
|
+
const firstId = scopeIdsRef.current.values().next().value;
|
|
2222
|
+
if (firstId) focusCtx.requestFocus(firstId);
|
|
2223
|
+
}
|
|
2224
|
+
}, [trap, focusCtx]);
|
|
2225
|
+
return React15__default.default.createElement(React15__default.default.Fragment, null, children);
|
|
2226
|
+
}
|
|
2227
|
+
function Spacer({ size }) {
|
|
2228
|
+
return React15__default.default.createElement("box", {
|
|
2229
|
+
style: { flexGrow: size ?? 1 }
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
function parseKeyDescriptor(descriptor) {
|
|
2233
|
+
const parts = descriptor.toLowerCase().split("+");
|
|
2234
|
+
const name = parts[parts.length - 1];
|
|
2235
|
+
return {
|
|
2236
|
+
name,
|
|
2237
|
+
ctrl: parts.includes("ctrl"),
|
|
2238
|
+
alt: parts.includes("alt"),
|
|
2239
|
+
shift: parts.includes("shift")
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function matchesKey(matcher, key) {
|
|
2243
|
+
if (key.name !== matcher.name) return false;
|
|
2244
|
+
if (matcher.ctrl !== !!key.ctrl) return false;
|
|
2245
|
+
if (matcher.alt !== !!key.alt) return false;
|
|
2246
|
+
if (matcher.shift !== !!key.shift) return false;
|
|
2247
|
+
return true;
|
|
2248
|
+
}
|
|
2249
|
+
function Keybind({
|
|
2250
|
+
keypress,
|
|
2251
|
+
onPress,
|
|
2252
|
+
whenFocused,
|
|
2253
|
+
disabled
|
|
2254
|
+
}) {
|
|
2255
|
+
const inputCtx = React15.useContext(InputContext);
|
|
2256
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
2257
|
+
const onPressRef = React15.useRef(onPress);
|
|
2258
|
+
onPressRef.current = onPress;
|
|
2259
|
+
const matcherRef = React15.useRef(parseKeyDescriptor(keypress));
|
|
2260
|
+
matcherRef.current = parseKeyDescriptor(keypress);
|
|
2261
|
+
React15.useEffect(() => {
|
|
2262
|
+
if (!inputCtx || disabled) return;
|
|
2263
|
+
const handler = (key) => {
|
|
2264
|
+
if (!matchesKey(matcherRef.current, key)) return;
|
|
2265
|
+
if (whenFocused && focusCtx?.focusedId !== whenFocused) return;
|
|
2266
|
+
onPressRef.current();
|
|
2267
|
+
};
|
|
2268
|
+
return inputCtx.subscribe(handler);
|
|
2269
|
+
}, [inputCtx, focusCtx, whenFocused, disabled]);
|
|
2270
|
+
return null;
|
|
2271
|
+
}
|
|
2272
|
+
function Portal({ children, zIndex = 1e3 }) {
|
|
2273
|
+
return React15__default.default.createElement(
|
|
2274
|
+
"box",
|
|
2275
|
+
{
|
|
2276
|
+
style: {
|
|
2277
|
+
position: "absolute",
|
|
2278
|
+
top: 0,
|
|
2279
|
+
left: 0,
|
|
2280
|
+
width: "100%",
|
|
2281
|
+
height: "100%",
|
|
2282
|
+
zIndex
|
|
2283
|
+
}
|
|
2284
|
+
},
|
|
2285
|
+
children
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
function Button({
|
|
2289
|
+
onPress,
|
|
2290
|
+
style,
|
|
2291
|
+
focusedStyle,
|
|
2292
|
+
children,
|
|
2293
|
+
disabled
|
|
2294
|
+
}) {
|
|
2295
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
2296
|
+
const inputCtx = React15.useContext(InputContext);
|
|
2297
|
+
const nodeRef = React15.useRef(null);
|
|
2298
|
+
const focusIdRef = React15.useRef(null);
|
|
2299
|
+
const onPressRef = React15.useRef(onPress);
|
|
2300
|
+
onPressRef.current = onPress;
|
|
2301
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
2302
|
+
React15.useEffect(() => {
|
|
2303
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
|
|
2304
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
2305
|
+
}, [focusCtx, disabled]);
|
|
2306
|
+
React15.useEffect(() => {
|
|
2307
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
2308
|
+
const fid = focusIdRef.current;
|
|
2309
|
+
setIsFocused(focusCtx.focusedId === fid);
|
|
2310
|
+
return focusCtx.onFocusChange((newId) => {
|
|
2311
|
+
setIsFocused(newId === fid);
|
|
2312
|
+
});
|
|
2313
|
+
}, [focusCtx]);
|
|
2314
|
+
React15.useEffect(() => {
|
|
2315
|
+
if (!inputCtx || !focusIdRef.current || disabled) return;
|
|
2316
|
+
const fid = focusIdRef.current;
|
|
2317
|
+
const handler = (key) => {
|
|
2318
|
+
if (focusCtx?.focusedId !== fid) return false;
|
|
2319
|
+
if (key.name === "return" || key.name === " " || key.sequence === " ") {
|
|
2320
|
+
onPressRef.current?.();
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
return false;
|
|
2324
|
+
};
|
|
2325
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
2326
|
+
}, [inputCtx, focusCtx, disabled]);
|
|
2327
|
+
const mergedStyle = {
|
|
2328
|
+
...style,
|
|
2329
|
+
...isFocused && focusedStyle ? focusedStyle : {}
|
|
2330
|
+
};
|
|
2331
|
+
return React15__default.default.createElement(
|
|
2332
|
+
"box",
|
|
2333
|
+
{
|
|
2334
|
+
style: mergedStyle,
|
|
2335
|
+
focusable: !disabled,
|
|
2336
|
+
ref: (node) => {
|
|
2337
|
+
if (node) {
|
|
2338
|
+
nodeRef.current = node;
|
|
2339
|
+
focusIdRef.current = node.focusId;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2343
|
+
children
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
var DEFAULT_RECT = {
|
|
2347
|
+
x: 0,
|
|
2348
|
+
y: 0,
|
|
2349
|
+
width: 0,
|
|
2350
|
+
height: 0,
|
|
2351
|
+
innerX: 0,
|
|
2352
|
+
innerY: 0,
|
|
2353
|
+
innerWidth: 0,
|
|
2354
|
+
innerHeight: 0
|
|
2355
|
+
};
|
|
2356
|
+
function useLayout(nodeRef) {
|
|
2357
|
+
const ctx = React15.useContext(LayoutContext);
|
|
2358
|
+
const [layout, setLayout] = React15.useState(DEFAULT_RECT);
|
|
2359
|
+
React15.useEffect(() => {
|
|
2360
|
+
if (!ctx || !nodeRef?.current) return;
|
|
2361
|
+
setLayout(ctx.getLayout(nodeRef.current));
|
|
2362
|
+
return ctx.subscribe(nodeRef.current, setLayout);
|
|
2363
|
+
}, [ctx, nodeRef]);
|
|
2364
|
+
return layout;
|
|
2365
|
+
}
|
|
2366
|
+
function useInput(handler, deps = []) {
|
|
2367
|
+
const ctx = React15.useContext(InputContext);
|
|
2368
|
+
React15.useEffect(() => {
|
|
2369
|
+
if (!ctx) return;
|
|
2370
|
+
return ctx.subscribe(handler);
|
|
2371
|
+
}, [ctx, ...deps]);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// src/components/ScrollView.tsx
|
|
2375
|
+
function ScrollView({
|
|
2376
|
+
children,
|
|
2377
|
+
style,
|
|
2378
|
+
scrollOffset: controlledOffset,
|
|
2379
|
+
onScroll,
|
|
2380
|
+
defaultScrollOffset = 0,
|
|
2381
|
+
scrollStep = 1,
|
|
2382
|
+
disableKeyboard,
|
|
2383
|
+
scrollToFocus = true,
|
|
2384
|
+
showScrollbar = true
|
|
2385
|
+
}) {
|
|
2386
|
+
const isControlled = controlledOffset !== void 0;
|
|
2387
|
+
const [internalOffset, setInternalOffset] = React15.useState(defaultScrollOffset);
|
|
2388
|
+
const offset = isControlled ? controlledOffset : internalOffset;
|
|
2389
|
+
const viewportRef = React15.useRef(null);
|
|
2390
|
+
const contentRef = React15.useRef(null);
|
|
2391
|
+
const viewportLayout = useLayout(viewportRef);
|
|
2392
|
+
const contentLayout = useLayout(contentRef);
|
|
2393
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
2394
|
+
const layoutCtx = React15.useContext(LayoutContext);
|
|
2395
|
+
const viewportHeight = viewportLayout.innerHeight;
|
|
2396
|
+
const contentHeight = contentLayout.height;
|
|
2397
|
+
const maxOffset = Math.max(0, contentHeight - viewportHeight);
|
|
2398
|
+
const setOffset = React15.useCallback(
|
|
2399
|
+
(next) => {
|
|
2400
|
+
const clamped = Math.max(0, Math.min(next, maxOffset));
|
|
2401
|
+
if (isControlled) {
|
|
2402
|
+
onScroll?.(clamped);
|
|
2403
|
+
} else {
|
|
2404
|
+
setInternalOffset(clamped);
|
|
2405
|
+
}
|
|
2406
|
+
},
|
|
2407
|
+
[isControlled, onScroll, maxOffset]
|
|
2408
|
+
);
|
|
2409
|
+
React15.useEffect(() => {
|
|
2410
|
+
if (offset > maxOffset && maxOffset >= 0) {
|
|
2411
|
+
setOffset(maxOffset);
|
|
2412
|
+
}
|
|
2413
|
+
}, [offset, maxOffset, setOffset]);
|
|
2414
|
+
React15.useEffect(() => {
|
|
2415
|
+
if (!scrollToFocus || !focusCtx || !layoutCtx || !contentRef.current) return;
|
|
2416
|
+
const unsubscribe = focusCtx.onFocusChange((focusedId) => {
|
|
2417
|
+
if (!focusedId || !contentRef.current) return;
|
|
2418
|
+
const findNode = (node) => {
|
|
2419
|
+
if (node.focusId === focusedId) return node;
|
|
2420
|
+
for (const child of node.children) {
|
|
2421
|
+
const found = findNode(child);
|
|
2422
|
+
if (found) return found;
|
|
2423
|
+
}
|
|
2424
|
+
return null;
|
|
2425
|
+
};
|
|
2426
|
+
const focusedNode = findNode(contentRef.current);
|
|
2427
|
+
if (!focusedNode) return;
|
|
2428
|
+
const focusedLayout = layoutCtx.getLayout(focusedNode);
|
|
2429
|
+
const contentTopY = contentRef.current.layout?.y ?? 0;
|
|
2430
|
+
const elementTop = focusedLayout.y - contentTopY;
|
|
2431
|
+
const elementBottom = elementTop + focusedLayout.height;
|
|
2432
|
+
const visibleTop = offset;
|
|
2433
|
+
const visibleBottom = offset + viewportHeight;
|
|
2434
|
+
if (elementTop < visibleTop) {
|
|
2435
|
+
setOffset(elementTop);
|
|
2436
|
+
} else if (elementBottom > visibleBottom) {
|
|
2437
|
+
setOffset(elementBottom - viewportHeight);
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
return unsubscribe;
|
|
2441
|
+
}, [scrollToFocus, focusCtx, layoutCtx, offset, viewportHeight, setOffset]);
|
|
2442
|
+
useInput((key) => {
|
|
2443
|
+
if (disableKeyboard) return;
|
|
2444
|
+
const halfPage = Math.max(1, Math.floor(viewportHeight / 2));
|
|
2445
|
+
const fullPage = Math.max(1, viewportHeight);
|
|
2446
|
+
switch (key.name) {
|
|
2447
|
+
// Page keys - always safe, inputs don't use these
|
|
2448
|
+
case "pageup":
|
|
2449
|
+
setOffset(offset - fullPage);
|
|
2450
|
+
break;
|
|
2451
|
+
case "pagedown":
|
|
2452
|
+
setOffset(offset + fullPage);
|
|
2453
|
+
break;
|
|
2454
|
+
default:
|
|
2455
|
+
if (key.ctrl) {
|
|
2456
|
+
if (key.name === "d") {
|
|
2457
|
+
setOffset(offset + halfPage);
|
|
2458
|
+
} else if (key.name === "u") {
|
|
2459
|
+
if (key.shift) {
|
|
2460
|
+
setOffset(offset - halfPage);
|
|
2461
|
+
}
|
|
2462
|
+
} else if (key.name === "f") {
|
|
2463
|
+
setOffset(offset + fullPage);
|
|
2464
|
+
} else if (key.name === "b") {
|
|
2465
|
+
setOffset(offset - fullPage);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
}, [offset, scrollStep, viewportHeight, maxOffset, disableKeyboard, setOffset]);
|
|
2471
|
+
const {
|
|
2472
|
+
padding: _pad,
|
|
2473
|
+
paddingX: _px,
|
|
2474
|
+
paddingY: _py,
|
|
2475
|
+
paddingTop: _pt,
|
|
2476
|
+
paddingRight: _pr,
|
|
2477
|
+
paddingBottom: _pb,
|
|
2478
|
+
paddingLeft: _pl,
|
|
2479
|
+
...styleRest
|
|
2480
|
+
} = style ?? {};
|
|
2481
|
+
const outerStyle = {
|
|
2482
|
+
...styleRest,
|
|
2483
|
+
clip: true
|
|
2484
|
+
};
|
|
2485
|
+
const innerStyle = {
|
|
2486
|
+
position: "absolute",
|
|
2487
|
+
top: -offset,
|
|
2488
|
+
left: 0,
|
|
2489
|
+
right: 0,
|
|
2490
|
+
flexDirection: "column",
|
|
2491
|
+
..._pad !== void 0 && { padding: _pad },
|
|
2492
|
+
..._px !== void 0 && { paddingX: _px },
|
|
2493
|
+
..._py !== void 0 && { paddingY: _py },
|
|
2494
|
+
..._pt !== void 0 && { paddingTop: _pt },
|
|
2495
|
+
..._pr !== void 0 && { paddingRight: _pr },
|
|
2496
|
+
..._pb !== void 0 && { paddingBottom: _pb },
|
|
2497
|
+
..._pl !== void 0 && { paddingLeft: _pl }
|
|
2498
|
+
};
|
|
2499
|
+
const isScrollable = contentHeight > viewportHeight && viewportHeight > 0;
|
|
2500
|
+
const scrollbarVisible = showScrollbar && isScrollable;
|
|
2501
|
+
const thumbHeight = Math.max(1, Math.floor(viewportHeight / contentHeight * viewportHeight));
|
|
2502
|
+
const scrollableRange = contentHeight - viewportHeight;
|
|
2503
|
+
const thumbPosition = scrollableRange > 0 ? Math.floor(offset / scrollableRange * (viewportHeight - thumbHeight)) : 0;
|
|
2504
|
+
const scrollbarChars = [];
|
|
2505
|
+
if (scrollbarVisible) {
|
|
2506
|
+
for (let i = 0; i < viewportHeight; i++) {
|
|
2507
|
+
if (i >= thumbPosition && i < thumbPosition + thumbHeight) {
|
|
2508
|
+
scrollbarChars.push("\u2588");
|
|
2509
|
+
} else {
|
|
2510
|
+
scrollbarChars.push("\u2591");
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const scrollbarStyle = {
|
|
2515
|
+
position: "absolute",
|
|
2516
|
+
top: 0,
|
|
2517
|
+
right: 0,
|
|
2518
|
+
width: 1,
|
|
2519
|
+
height: viewportHeight,
|
|
2520
|
+
flexDirection: "column"
|
|
2521
|
+
};
|
|
2522
|
+
return React15__default.default.createElement(
|
|
2523
|
+
"box",
|
|
2524
|
+
{
|
|
2525
|
+
style: outerStyle,
|
|
2526
|
+
ref: (node) => {
|
|
2527
|
+
viewportRef.current = node ?? null;
|
|
2528
|
+
}
|
|
2529
|
+
},
|
|
2530
|
+
// Content
|
|
2531
|
+
React15__default.default.createElement(
|
|
2532
|
+
"box",
|
|
2533
|
+
{
|
|
2534
|
+
style: {
|
|
2535
|
+
...innerStyle,
|
|
2536
|
+
// Reserve space for scrollbar when visible
|
|
2537
|
+
paddingRight: scrollbarVisible ? (innerStyle.paddingRight ?? 0) + 1 : innerStyle.paddingRight
|
|
2538
|
+
},
|
|
2539
|
+
ref: (node) => {
|
|
2540
|
+
contentRef.current = node ?? null;
|
|
2541
|
+
}
|
|
2542
|
+
},
|
|
2543
|
+
children
|
|
2544
|
+
),
|
|
2545
|
+
// Scrollbar
|
|
2546
|
+
scrollbarVisible && React15__default.default.createElement(
|
|
2547
|
+
"box",
|
|
2548
|
+
{ style: scrollbarStyle },
|
|
2549
|
+
React15__default.default.createElement(
|
|
2550
|
+
"text",
|
|
2551
|
+
{ style: { color: "blackBright" } },
|
|
2552
|
+
scrollbarChars.join("\n")
|
|
2553
|
+
)
|
|
2554
|
+
)
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
function List({
|
|
2558
|
+
count,
|
|
2559
|
+
renderItem,
|
|
2560
|
+
selectedIndex: controlledIndex,
|
|
2561
|
+
onSelectionChange,
|
|
2562
|
+
onSelect,
|
|
2563
|
+
defaultSelectedIndex = 0,
|
|
2564
|
+
disabledIndices,
|
|
2565
|
+
style,
|
|
2566
|
+
focusable = true
|
|
2567
|
+
}) {
|
|
2568
|
+
const isControlled = controlledIndex !== void 0;
|
|
2569
|
+
const [internalIndex, setInternalIndex] = React15.useState(defaultSelectedIndex);
|
|
2570
|
+
const selectedIndex = isControlled ? controlledIndex : internalIndex;
|
|
2571
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
2572
|
+
const inputCtx = React15.useContext(InputContext);
|
|
2573
|
+
const nodeRef = React15.useRef(null);
|
|
2574
|
+
const focusIdRef = React15.useRef(null);
|
|
2575
|
+
const onSelectRef = React15.useRef(onSelect);
|
|
2576
|
+
onSelectRef.current = onSelect;
|
|
2577
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
2578
|
+
const lastKeyRef = React15.useRef(null);
|
|
2579
|
+
const setIndex = React15.useCallback(
|
|
2580
|
+
(next) => {
|
|
2581
|
+
const clamped = Math.max(0, Math.min(next, count - 1));
|
|
2582
|
+
if (isControlled) {
|
|
2583
|
+
onSelectionChange?.(clamped);
|
|
2584
|
+
} else {
|
|
2585
|
+
setInternalIndex(clamped);
|
|
2586
|
+
}
|
|
2587
|
+
},
|
|
2588
|
+
[isControlled, onSelectionChange, count]
|
|
2589
|
+
);
|
|
2590
|
+
const findNextEnabled = React15.useCallback(
|
|
2591
|
+
(from, direction) => {
|
|
2592
|
+
if (!disabledIndices || disabledIndices.size === 0) {
|
|
2593
|
+
return Math.max(0, Math.min(from + direction, count - 1));
|
|
2594
|
+
}
|
|
2595
|
+
let next = from + direction;
|
|
2596
|
+
while (next >= 0 && next < count && disabledIndices.has(next)) {
|
|
2597
|
+
next += direction;
|
|
2598
|
+
}
|
|
2599
|
+
if (next < 0 || next >= count) return from;
|
|
2600
|
+
return next;
|
|
2601
|
+
},
|
|
2602
|
+
[disabledIndices, count]
|
|
2603
|
+
);
|
|
2604
|
+
React15.useEffect(() => {
|
|
2605
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current || !focusable) return;
|
|
2606
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
2607
|
+
}, [focusCtx, focusable]);
|
|
2608
|
+
React15.useEffect(() => {
|
|
2609
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
2610
|
+
const fid = focusIdRef.current;
|
|
2611
|
+
setIsFocused(focusCtx.focusedId === fid);
|
|
2612
|
+
return focusCtx.onFocusChange((newId) => {
|
|
2613
|
+
setIsFocused(newId === fid);
|
|
2614
|
+
});
|
|
2615
|
+
}, [focusCtx]);
|
|
2616
|
+
const findFirstEnabled = React15.useCallback(
|
|
2617
|
+
(fromEnd) => {
|
|
2618
|
+
const start = fromEnd ? count - 1 : 0;
|
|
2619
|
+
const direction = fromEnd ? -1 : 1;
|
|
2620
|
+
let index = start;
|
|
2621
|
+
while (index >= 0 && index < count && disabledIndices?.has(index)) {
|
|
2622
|
+
index += direction;
|
|
2623
|
+
}
|
|
2624
|
+
return index >= 0 && index < count ? index : fromEnd ? count - 1 : 0;
|
|
2625
|
+
},
|
|
2626
|
+
[disabledIndices, count]
|
|
2627
|
+
);
|
|
2628
|
+
React15.useEffect(() => {
|
|
2629
|
+
if (!inputCtx || !focusIdRef.current || !focusable) return;
|
|
2630
|
+
const fid = focusIdRef.current;
|
|
2631
|
+
const handler = (key) => {
|
|
2632
|
+
if (focusCtx?.focusedId !== fid) return false;
|
|
2633
|
+
if (key.name === "g" && !key.ctrl && !key.alt) {
|
|
2634
|
+
if (lastKeyRef.current === "g") {
|
|
2635
|
+
setIndex(findFirstEnabled(false));
|
|
2636
|
+
lastKeyRef.current = null;
|
|
2637
|
+
return true;
|
|
2638
|
+
}
|
|
2639
|
+
lastKeyRef.current = "g";
|
|
2640
|
+
return true;
|
|
2641
|
+
}
|
|
2642
|
+
if (key.name === "G" || key.name === "g" && key.shift) {
|
|
2643
|
+
lastKeyRef.current = null;
|
|
2644
|
+
setIndex(findFirstEnabled(true));
|
|
2645
|
+
return true;
|
|
2646
|
+
}
|
|
2647
|
+
lastKeyRef.current = null;
|
|
2648
|
+
if (key.name === "up" || key.name === "k") {
|
|
2649
|
+
setIndex(findNextEnabled(selectedIndex, -1));
|
|
2650
|
+
return true;
|
|
2651
|
+
}
|
|
2652
|
+
if (key.name === "down" || key.name === "j") {
|
|
2653
|
+
setIndex(findNextEnabled(selectedIndex, 1));
|
|
2654
|
+
return true;
|
|
2655
|
+
}
|
|
2656
|
+
if (key.name === "return") {
|
|
2657
|
+
if (!disabledIndices?.has(selectedIndex)) {
|
|
2658
|
+
onSelectRef.current?.(selectedIndex);
|
|
2659
|
+
}
|
|
2660
|
+
return true;
|
|
2661
|
+
}
|
|
2662
|
+
return false;
|
|
2663
|
+
};
|
|
2664
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
2665
|
+
}, [inputCtx, focusCtx, focusable, selectedIndex, setIndex, findNextEnabled, findFirstEnabled, disabledIndices]);
|
|
2666
|
+
const items = [];
|
|
2667
|
+
for (let i = 0; i < count; i++) {
|
|
2668
|
+
items.push(
|
|
2669
|
+
React15__default.default.createElement(
|
|
2670
|
+
React15__default.default.Fragment,
|
|
2671
|
+
{ key: i },
|
|
2672
|
+
renderItem({ index: i, selected: i === selectedIndex, focused: isFocused })
|
|
2673
|
+
)
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
return React15__default.default.createElement(
|
|
2677
|
+
"box",
|
|
2678
|
+
{
|
|
2679
|
+
style: { flexDirection: "column", ...style },
|
|
2680
|
+
focusable,
|
|
2681
|
+
ref: (node) => {
|
|
2682
|
+
if (node) {
|
|
2683
|
+
nodeRef.current = node;
|
|
2684
|
+
focusIdRef.current = node.focusId;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
},
|
|
2688
|
+
...items
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
function Menu({
|
|
2692
|
+
items,
|
|
2693
|
+
selectedIndex,
|
|
2694
|
+
onSelectionChange,
|
|
2695
|
+
onSelect,
|
|
2696
|
+
defaultSelectedIndex = 0,
|
|
2697
|
+
style,
|
|
2698
|
+
highlightColor = "cyan",
|
|
2699
|
+
focusable = true
|
|
2700
|
+
}) {
|
|
2701
|
+
const disabledIndices = /* @__PURE__ */ new Set();
|
|
2702
|
+
for (let i = 0; i < items.length; i++) {
|
|
2703
|
+
if (items[i].disabled) disabledIndices.add(i);
|
|
2704
|
+
}
|
|
2705
|
+
const handleSelect = (index) => {
|
|
2706
|
+
const item = items[index];
|
|
2707
|
+
if (item && !item.disabled) {
|
|
2708
|
+
onSelect?.(item.value, index);
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
return React15__default.default.createElement(List, {
|
|
2712
|
+
count: items.length,
|
|
2713
|
+
selectedIndex,
|
|
2714
|
+
onSelectionChange,
|
|
2715
|
+
onSelect: handleSelect,
|
|
2716
|
+
defaultSelectedIndex,
|
|
2717
|
+
disabledIndices: disabledIndices.size > 0 ? disabledIndices : void 0,
|
|
2718
|
+
style,
|
|
2719
|
+
focusable,
|
|
2720
|
+
renderItem: ({ index, selected, focused }) => {
|
|
2721
|
+
const item = items[index];
|
|
2722
|
+
const isDisabled = item.disabled;
|
|
2723
|
+
const isHighlighted = selected && focused;
|
|
2724
|
+
const indicator = selected ? ">" : " ";
|
|
2725
|
+
return React15__default.default.createElement(
|
|
2726
|
+
"box",
|
|
2727
|
+
{
|
|
2728
|
+
style: {
|
|
2729
|
+
flexDirection: "row",
|
|
2730
|
+
...isHighlighted ? { bg: highlightColor } : {}
|
|
2731
|
+
}
|
|
2732
|
+
},
|
|
2733
|
+
React15__default.default.createElement(
|
|
2734
|
+
"text",
|
|
2735
|
+
{
|
|
2736
|
+
style: isHighlighted ? { bold: true, color: "black" } : isDisabled ? { dim: true } : {}
|
|
2737
|
+
},
|
|
2738
|
+
`${indicator} ${item.label}`
|
|
2739
|
+
)
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
function Progress({
|
|
2745
|
+
value,
|
|
2746
|
+
indeterminate = false,
|
|
2747
|
+
width = "100%",
|
|
2748
|
+
label,
|
|
2749
|
+
showPercent = false,
|
|
2750
|
+
style,
|
|
2751
|
+
filled = "\u2588",
|
|
2752
|
+
empty = "\u2591"
|
|
2753
|
+
}) {
|
|
2754
|
+
const trackRef = React15.useRef(null);
|
|
2755
|
+
const trackLayout = useLayout(trackRef);
|
|
2756
|
+
const trackWidth = trackLayout.innerWidth;
|
|
2757
|
+
const [indeterminatePos, setIndeterminatePos] = React15.useState(0);
|
|
2758
|
+
React15.useEffect(() => {
|
|
2759
|
+
if (!indeterminate) return;
|
|
2760
|
+
const timer = setInterval(() => {
|
|
2761
|
+
setIndeterminatePos((p) => (p + 1) % Math.max(1, trackWidth + 6));
|
|
2762
|
+
}, 100);
|
|
2763
|
+
return () => clearInterval(timer);
|
|
2764
|
+
}, [indeterminate, trackWidth]);
|
|
2765
|
+
const clamped = Math.max(0, Math.min(1, value ?? 0));
|
|
2766
|
+
const pctText = showPercent ? ` ${Math.round(clamped * 100)}%` : "";
|
|
2767
|
+
let barText = "";
|
|
2768
|
+
if (trackWidth > 0) {
|
|
2769
|
+
if (indeterminate && value === void 0) {
|
|
2770
|
+
const chunkSize = Math.max(1, Math.min(3, Math.floor(trackWidth / 4)));
|
|
2771
|
+
const chars = [];
|
|
2772
|
+
for (let i = 0; i < trackWidth; i++) {
|
|
2773
|
+
if (i >= indeterminatePos - chunkSize && i < indeterminatePos) {
|
|
2774
|
+
chars.push(filled);
|
|
2775
|
+
} else {
|
|
2776
|
+
chars.push(empty);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
barText = chars.join("");
|
|
2780
|
+
} else {
|
|
2781
|
+
const filledCount = Math.round(clamped * trackWidth);
|
|
2782
|
+
barText = filled.repeat(filledCount) + empty.repeat(trackWidth - filledCount);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
const children = [];
|
|
2786
|
+
if (label) {
|
|
2787
|
+
children.push(
|
|
2788
|
+
React15__default.default.createElement("text", { key: "label", style: { bold: true } }, label + " ")
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
children.push(
|
|
2792
|
+
React15__default.default.createElement(
|
|
2793
|
+
"box",
|
|
2794
|
+
{
|
|
2795
|
+
key: "track",
|
|
2796
|
+
style: { flexGrow: 1, flexShrink: 1 },
|
|
2797
|
+
ref: (node) => {
|
|
2798
|
+
trackRef.current = node ?? null;
|
|
2799
|
+
}
|
|
2800
|
+
},
|
|
2801
|
+
React15__default.default.createElement("text", { key: "bar", style: {} }, barText)
|
|
2802
|
+
)
|
|
2803
|
+
);
|
|
2804
|
+
if (showPercent) {
|
|
2805
|
+
children.push(
|
|
2806
|
+
React15__default.default.createElement("text", { key: "pct", style: { bold: true } }, pctText)
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
return React15__default.default.createElement(
|
|
2810
|
+
"box",
|
|
2811
|
+
{
|
|
2812
|
+
style: {
|
|
2813
|
+
flexDirection: "row",
|
|
2814
|
+
width,
|
|
2815
|
+
...style
|
|
2816
|
+
}
|
|
2817
|
+
},
|
|
2818
|
+
...children
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
var BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2822
|
+
function Spinner({
|
|
2823
|
+
frames = BRAILLE_FRAMES,
|
|
2824
|
+
intervalMs = 80,
|
|
2825
|
+
label,
|
|
2826
|
+
style
|
|
2827
|
+
}) {
|
|
2828
|
+
const [frameIndex, setFrameIndex] = React15.useState(0);
|
|
2829
|
+
React15.useEffect(() => {
|
|
2830
|
+
const timer = setInterval(() => {
|
|
2831
|
+
setFrameIndex((i) => (i + 1) % frames.length);
|
|
2832
|
+
}, intervalMs);
|
|
2833
|
+
return () => clearInterval(timer);
|
|
2834
|
+
}, [frames.length, intervalMs]);
|
|
2835
|
+
const children = [
|
|
2836
|
+
React15__default.default.createElement("text", { key: "frame", style }, frames[frameIndex])
|
|
2837
|
+
];
|
|
2838
|
+
if (label) {
|
|
2839
|
+
children.push(
|
|
2840
|
+
React15__default.default.createElement("text", { key: "label", style: {} }, " " + label)
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
return React15__default.default.createElement(
|
|
2844
|
+
"box",
|
|
2845
|
+
{ style: { flexDirection: "row" } },
|
|
2846
|
+
...children
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
var ToastContext = React15.createContext(null);
|
|
2850
|
+
var nextToastId = 0;
|
|
2851
|
+
function useToast() {
|
|
2852
|
+
const ctx = React15.useContext(ToastContext);
|
|
2853
|
+
if (!ctx) throw new Error("useToast must be used within a <ToastHost>");
|
|
2854
|
+
return ctx.push;
|
|
2855
|
+
}
|
|
2856
|
+
var VARIANT_COLORS = {
|
|
2857
|
+
info: { bg: "blackBright", title: "cyanBright", text: "white" },
|
|
2858
|
+
success: { bg: "blackBright", title: "greenBright", text: "white" },
|
|
2859
|
+
warning: { bg: "blackBright", title: "yellowBright", text: "white" },
|
|
2860
|
+
error: { bg: "blackBright", title: "redBright", text: "white" }
|
|
2861
|
+
};
|
|
2862
|
+
function ToastHost({
|
|
2863
|
+
position = "bottom-right",
|
|
2864
|
+
maxVisible = 5,
|
|
2865
|
+
children
|
|
2866
|
+
}) {
|
|
2867
|
+
const [toasts, setToasts] = React15.useState([]);
|
|
2868
|
+
const timersRef = React15.useRef(/* @__PURE__ */ new Map());
|
|
2869
|
+
const push = React15.useCallback((toast) => {
|
|
2870
|
+
const id = `toast-${nextToastId++}`;
|
|
2871
|
+
const full = { id, durationMs: 3e3, variant: "info", ...toast };
|
|
2872
|
+
setToasts((prev) => [...prev, full]);
|
|
2873
|
+
if (full.durationMs && full.durationMs > 0) {
|
|
2874
|
+
const timer = setTimeout(() => {
|
|
2875
|
+
timersRef.current.delete(id);
|
|
2876
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
2877
|
+
}, full.durationMs);
|
|
2878
|
+
timersRef.current.set(id, timer);
|
|
2879
|
+
}
|
|
2880
|
+
}, []);
|
|
2881
|
+
React15.useEffect(() => {
|
|
2882
|
+
return () => {
|
|
2883
|
+
for (const timer of timersRef.current.values()) {
|
|
2884
|
+
clearTimeout(timer);
|
|
2885
|
+
}
|
|
2886
|
+
timersRef.current.clear();
|
|
2887
|
+
};
|
|
2888
|
+
}, []);
|
|
2889
|
+
const ctxValue = React15.useRef({ push });
|
|
2890
|
+
ctxValue.current.push = push;
|
|
2891
|
+
const isTop = position.startsWith("top");
|
|
2892
|
+
const isRight = position.endsWith("right");
|
|
2893
|
+
const portalStyle = {
|
|
2894
|
+
position: "absolute",
|
|
2895
|
+
top: 0,
|
|
2896
|
+
left: 0,
|
|
2897
|
+
width: "100%",
|
|
2898
|
+
height: "100%",
|
|
2899
|
+
zIndex: 900,
|
|
2900
|
+
flexDirection: "column",
|
|
2901
|
+
justifyContent: isTop ? "flex-start" : "flex-end",
|
|
2902
|
+
alignItems: isRight ? "flex-end" : "flex-start",
|
|
2903
|
+
padding: 1
|
|
2904
|
+
};
|
|
2905
|
+
const visible = toasts.slice(-maxVisible);
|
|
2906
|
+
const toastElements = visible.map((toast) => {
|
|
2907
|
+
const variant = toast.variant ?? "info";
|
|
2908
|
+
const colors = VARIANT_COLORS[variant];
|
|
2909
|
+
const innerChildren = [];
|
|
2910
|
+
if (toast.title) {
|
|
2911
|
+
innerChildren.push(
|
|
2912
|
+
React15__default.default.createElement("text", {
|
|
2913
|
+
key: "title",
|
|
2914
|
+
style: { bold: true, color: colors.title }
|
|
2915
|
+
}, toast.title)
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
innerChildren.push(
|
|
2919
|
+
React15__default.default.createElement("text", {
|
|
2920
|
+
key: "msg",
|
|
2921
|
+
style: { color: colors.text }
|
|
2922
|
+
}, toast.message)
|
|
2923
|
+
);
|
|
2924
|
+
return React15__default.default.createElement(
|
|
2925
|
+
"box",
|
|
2926
|
+
{
|
|
2927
|
+
key: toast.id,
|
|
2928
|
+
style: {
|
|
2929
|
+
bg: colors.bg,
|
|
2930
|
+
paddingX: 1,
|
|
2931
|
+
flexDirection: "column",
|
|
2932
|
+
minWidth: 20,
|
|
2933
|
+
maxWidth: 50
|
|
2934
|
+
}
|
|
2935
|
+
},
|
|
2936
|
+
...innerChildren
|
|
2937
|
+
);
|
|
2938
|
+
});
|
|
2939
|
+
return React15__default.default.createElement(
|
|
2940
|
+
ToastContext.Provider,
|
|
2941
|
+
{ value: ctxValue.current },
|
|
2942
|
+
children,
|
|
2943
|
+
toastElements.length > 0 ? React15__default.default.createElement("box", { style: portalStyle }, ...toastElements) : null
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
function Select({
|
|
2947
|
+
items,
|
|
2948
|
+
value,
|
|
2949
|
+
onChange,
|
|
2950
|
+
placeholder = "Select...",
|
|
2951
|
+
style,
|
|
2952
|
+
focusedStyle,
|
|
2953
|
+
dropdownStyle,
|
|
2954
|
+
highlightColor = "cyan",
|
|
2955
|
+
maxVisible = 8,
|
|
2956
|
+
searchable = true,
|
|
2957
|
+
disabled
|
|
2958
|
+
}) {
|
|
2959
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
2960
|
+
const inputCtx = React15.useContext(InputContext);
|
|
2961
|
+
const appCtx = React15.useContext(AppContext);
|
|
2962
|
+
const nodeRef = React15.useRef(null);
|
|
2963
|
+
const focusIdRef = React15.useRef(null);
|
|
2964
|
+
const onChangeRef = React15.useRef(onChange);
|
|
2965
|
+
onChangeRef.current = onChange;
|
|
2966
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
2967
|
+
const [isOpen, setIsOpen] = React15.useState(false);
|
|
2968
|
+
const [highlightIndex, setHighlightIndex] = React15.useState(0);
|
|
2969
|
+
const [searchText, setSearchText] = React15.useState("");
|
|
2970
|
+
const [scrollOffset, setScrollOffset] = React15.useState(0);
|
|
2971
|
+
const triggerLayout = useLayout(nodeRef);
|
|
2972
|
+
const screenRows = appCtx?.rows ?? 24;
|
|
2973
|
+
const selectedItem = items.find((item) => item.value === value);
|
|
2974
|
+
const selectedLabel = selectedItem?.label ?? "";
|
|
2975
|
+
const filteredItems = React15.useMemo(() => {
|
|
2976
|
+
if (!searchText) return items;
|
|
2977
|
+
const lower = searchText.toLowerCase();
|
|
2978
|
+
return items.filter((item) => item.label.toLowerCase().includes(lower));
|
|
2979
|
+
}, [items, searchText]);
|
|
2980
|
+
const visibleCount = Math.min(maxVisible, filteredItems.length);
|
|
2981
|
+
const visibleItems = filteredItems.slice(
|
|
2982
|
+
scrollOffset,
|
|
2983
|
+
scrollOffset + visibleCount
|
|
2984
|
+
);
|
|
2985
|
+
React15.useEffect(() => {
|
|
2986
|
+
setHighlightIndex(0);
|
|
2987
|
+
setScrollOffset(0);
|
|
2988
|
+
}, [searchText]);
|
|
2989
|
+
React15.useEffect(() => {
|
|
2990
|
+
if (!isFocused && isOpen) {
|
|
2991
|
+
setIsOpen(false);
|
|
2992
|
+
setSearchText("");
|
|
2993
|
+
}
|
|
2994
|
+
}, [isFocused, isOpen]);
|
|
2995
|
+
React15.useEffect(() => {
|
|
2996
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
|
|
2997
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
2998
|
+
}, [focusCtx, disabled]);
|
|
2999
|
+
React15.useEffect(() => {
|
|
3000
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
3001
|
+
const fid = focusIdRef.current;
|
|
3002
|
+
setIsFocused(focusCtx.focusedId === fid);
|
|
3003
|
+
return focusCtx.onFocusChange((newId) => {
|
|
3004
|
+
setIsFocused(newId === fid);
|
|
3005
|
+
});
|
|
3006
|
+
}, [focusCtx]);
|
|
3007
|
+
const findNextEnabled = React15.useCallback(
|
|
3008
|
+
(from, direction) => {
|
|
3009
|
+
let next = from + direction;
|
|
3010
|
+
while (next >= 0 && next < filteredItems.length) {
|
|
3011
|
+
if (!filteredItems[next].disabled) return next;
|
|
3012
|
+
next += direction;
|
|
3013
|
+
}
|
|
3014
|
+
return from;
|
|
3015
|
+
},
|
|
3016
|
+
[filteredItems]
|
|
3017
|
+
);
|
|
3018
|
+
const ensureVisible = React15.useCallback(
|
|
3019
|
+
(index) => {
|
|
3020
|
+
if (index < scrollOffset) {
|
|
3021
|
+
setScrollOffset(index);
|
|
3022
|
+
} else if (index >= scrollOffset + visibleCount) {
|
|
3023
|
+
setScrollOffset(index - visibleCount + 1);
|
|
3024
|
+
}
|
|
3025
|
+
},
|
|
3026
|
+
[scrollOffset, visibleCount]
|
|
3027
|
+
);
|
|
3028
|
+
React15.useEffect(() => {
|
|
3029
|
+
if (!inputCtx || !focusIdRef.current || disabled) return;
|
|
3030
|
+
const fid = focusIdRef.current;
|
|
3031
|
+
const handler = (key) => {
|
|
3032
|
+
if (focusCtx?.focusedId !== fid) return false;
|
|
3033
|
+
if (!isOpen) {
|
|
3034
|
+
if (key.name === "return" || key.name === " " || key.sequence === " " || key.name === "down") {
|
|
3035
|
+
setIsOpen(true);
|
|
3036
|
+
setSearchText("");
|
|
3037
|
+
const idx = filteredItems.findIndex((item) => item.value === value);
|
|
3038
|
+
const start = idx >= 0 ? idx : 0;
|
|
3039
|
+
setHighlightIndex(start);
|
|
3040
|
+
setScrollOffset(
|
|
3041
|
+
Math.max(0, start - Math.floor(maxVisible / 2))
|
|
3042
|
+
);
|
|
3043
|
+
return true;
|
|
3044
|
+
}
|
|
3045
|
+
return false;
|
|
3046
|
+
}
|
|
3047
|
+
if (key.name === "tab") {
|
|
3048
|
+
setIsOpen(false);
|
|
3049
|
+
setSearchText("");
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
if (key.name === "escape") {
|
|
3053
|
+
setIsOpen(false);
|
|
3054
|
+
setSearchText("");
|
|
3055
|
+
return true;
|
|
3056
|
+
}
|
|
3057
|
+
if (key.name === "return") {
|
|
3058
|
+
const item = filteredItems[highlightIndex];
|
|
3059
|
+
if (item && !item.disabled) {
|
|
3060
|
+
onChangeRef.current?.(item.value);
|
|
3061
|
+
setIsOpen(false);
|
|
3062
|
+
setSearchText("");
|
|
3063
|
+
}
|
|
3064
|
+
return true;
|
|
3065
|
+
}
|
|
3066
|
+
if (key.name === "up") {
|
|
3067
|
+
const next = findNextEnabled(highlightIndex, -1);
|
|
3068
|
+
setHighlightIndex(next);
|
|
3069
|
+
ensureVisible(next);
|
|
3070
|
+
return true;
|
|
3071
|
+
}
|
|
3072
|
+
if (key.name === "down") {
|
|
3073
|
+
const next = findNextEnabled(highlightIndex, 1);
|
|
3074
|
+
setHighlightIndex(next);
|
|
3075
|
+
ensureVisible(next);
|
|
3076
|
+
return true;
|
|
3077
|
+
}
|
|
3078
|
+
if (key.name === "backspace") {
|
|
3079
|
+
if (searchable && searchText.length > 0) {
|
|
3080
|
+
setSearchText((prev) => prev.slice(0, -1));
|
|
3081
|
+
}
|
|
3082
|
+
return true;
|
|
3083
|
+
}
|
|
3084
|
+
if (key.name === "home") {
|
|
3085
|
+
const first = findNextEnabled(-1, 1);
|
|
3086
|
+
setHighlightIndex(first);
|
|
3087
|
+
ensureVisible(first);
|
|
3088
|
+
return true;
|
|
3089
|
+
}
|
|
3090
|
+
if (key.name === "end") {
|
|
3091
|
+
const last = findNextEnabled(filteredItems.length, -1);
|
|
3092
|
+
setHighlightIndex(last);
|
|
3093
|
+
ensureVisible(last);
|
|
3094
|
+
return true;
|
|
3095
|
+
}
|
|
3096
|
+
if (searchable && key.sequence && key.sequence.length === 1 && !key.ctrl && !key.alt) {
|
|
3097
|
+
const ch = key.sequence;
|
|
3098
|
+
if (ch >= " " && ch <= "~") {
|
|
3099
|
+
setSearchText((prev) => prev + ch);
|
|
3100
|
+
return true;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
return true;
|
|
3104
|
+
};
|
|
3105
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
3106
|
+
}, [
|
|
3107
|
+
inputCtx,
|
|
3108
|
+
focusCtx,
|
|
3109
|
+
disabled,
|
|
3110
|
+
isOpen,
|
|
3111
|
+
highlightIndex,
|
|
3112
|
+
filteredItems,
|
|
3113
|
+
value,
|
|
3114
|
+
maxVisible,
|
|
3115
|
+
searchable,
|
|
3116
|
+
searchText,
|
|
3117
|
+
findNextEnabled,
|
|
3118
|
+
ensureVisible
|
|
3119
|
+
]);
|
|
3120
|
+
const useDefaultBorder = !style?.bg && style?.border === void 0;
|
|
3121
|
+
const triggerStyle = {
|
|
3122
|
+
flexDirection: "row",
|
|
3123
|
+
width: "100%",
|
|
3124
|
+
...useDefaultBorder ? { border: "single" } : {},
|
|
3125
|
+
...style,
|
|
3126
|
+
...isFocused && focusedStyle ? focusedStyle : {}
|
|
3127
|
+
};
|
|
3128
|
+
const labelColor = selectedLabel ? style?.color ?? void 0 : "blackBright";
|
|
3129
|
+
const triggerChildren = [
|
|
3130
|
+
React15__default.default.createElement(
|
|
3131
|
+
"text",
|
|
3132
|
+
{
|
|
3133
|
+
key: "label",
|
|
3134
|
+
style: {
|
|
3135
|
+
flexGrow: 1,
|
|
3136
|
+
flexShrink: 1,
|
|
3137
|
+
color: labelColor,
|
|
3138
|
+
wrap: "ellipsis",
|
|
3139
|
+
...selectedLabel ? {} : { dim: true }
|
|
3140
|
+
}
|
|
3141
|
+
},
|
|
3142
|
+
selectedLabel || placeholder
|
|
3143
|
+
),
|
|
3144
|
+
React15__default.default.createElement(
|
|
3145
|
+
"text",
|
|
3146
|
+
{
|
|
3147
|
+
key: "arrow",
|
|
3148
|
+
style: { flexShrink: 0, color: isFocused ? highlightColor : "blackBright" }
|
|
3149
|
+
},
|
|
3150
|
+
isOpen ? " \u25B2" : " \u25BC"
|
|
3151
|
+
)
|
|
3152
|
+
];
|
|
3153
|
+
let dropdownElement = null;
|
|
3154
|
+
if (isOpen) {
|
|
3155
|
+
const dropdownChildren = [];
|
|
3156
|
+
if (searchable && searchText) {
|
|
3157
|
+
dropdownChildren.push(
|
|
3158
|
+
React15__default.default.createElement(
|
|
3159
|
+
"box",
|
|
3160
|
+
{ key: "search", style: { paddingX: 1 } },
|
|
3161
|
+
React15__default.default.createElement(
|
|
3162
|
+
"text",
|
|
3163
|
+
{ style: { color: "blackBright", dim: true } },
|
|
3164
|
+
`/${searchText}`
|
|
3165
|
+
)
|
|
3166
|
+
)
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
if (filteredItems.length === 0) {
|
|
3170
|
+
dropdownChildren.push(
|
|
3171
|
+
React15__default.default.createElement(
|
|
3172
|
+
"box",
|
|
3173
|
+
{ key: "empty", style: { paddingX: 1 } },
|
|
3174
|
+
React15__default.default.createElement(
|
|
3175
|
+
"text",
|
|
3176
|
+
{ style: { dim: true, color: "blackBright" } },
|
|
3177
|
+
"No matches"
|
|
3178
|
+
)
|
|
3179
|
+
)
|
|
3180
|
+
);
|
|
3181
|
+
}
|
|
3182
|
+
if (scrollOffset > 0) {
|
|
3183
|
+
dropdownChildren.push(
|
|
3184
|
+
React15__default.default.createElement(
|
|
3185
|
+
"box",
|
|
3186
|
+
{
|
|
3187
|
+
key: "scroll-up",
|
|
3188
|
+
style: { justifyContent: "center", alignItems: "center" }
|
|
3189
|
+
},
|
|
3190
|
+
React15__default.default.createElement(
|
|
3191
|
+
"text",
|
|
3192
|
+
{ style: { dim: true, color: "blackBright" } },
|
|
3193
|
+
"\u25B2"
|
|
3194
|
+
)
|
|
3195
|
+
)
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
visibleItems.forEach((item, vi) => {
|
|
3199
|
+
const actualIndex = scrollOffset + vi;
|
|
3200
|
+
const isHighlighted = actualIndex === highlightIndex;
|
|
3201
|
+
const isDisabled = item.disabled;
|
|
3202
|
+
const itemStyle = {
|
|
3203
|
+
paddingX: 1,
|
|
3204
|
+
...isHighlighted && !isDisabled ? { bg: highlightColor } : {}
|
|
3205
|
+
};
|
|
3206
|
+
const textStyle = {
|
|
3207
|
+
...isHighlighted && !isDisabled ? { color: "black", bold: true } : {},
|
|
3208
|
+
...isDisabled ? { dim: true, color: "blackBright" } : {}
|
|
3209
|
+
};
|
|
3210
|
+
dropdownChildren.push(
|
|
3211
|
+
React15__default.default.createElement(
|
|
3212
|
+
"box",
|
|
3213
|
+
{ key: `item-${item.value}`, style: itemStyle },
|
|
3214
|
+
React15__default.default.createElement(
|
|
3215
|
+
"text",
|
|
3216
|
+
{ style: textStyle },
|
|
3217
|
+
item.label
|
|
3218
|
+
)
|
|
3219
|
+
)
|
|
3220
|
+
);
|
|
3221
|
+
});
|
|
3222
|
+
if (scrollOffset + visibleCount < filteredItems.length) {
|
|
3223
|
+
dropdownChildren.push(
|
|
3224
|
+
React15__default.default.createElement(
|
|
3225
|
+
"box",
|
|
3226
|
+
{
|
|
3227
|
+
key: "scroll-down",
|
|
3228
|
+
style: { justifyContent: "center", alignItems: "center" }
|
|
3229
|
+
},
|
|
3230
|
+
React15__default.default.createElement(
|
|
3231
|
+
"text",
|
|
3232
|
+
{ style: { dim: true, color: "blackBright" } },
|
|
3233
|
+
"\u25BC"
|
|
3234
|
+
)
|
|
3235
|
+
)
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
const hasScrollUp = scrollOffset > 0;
|
|
3239
|
+
const hasScrollDown = scrollOffset + visibleCount < filteredItems.length;
|
|
3240
|
+
const hasSearch = searchable && searchText;
|
|
3241
|
+
const hasNoMatches = filteredItems.length === 0;
|
|
3242
|
+
const useDropdownBorder = !dropdownStyle?.bg && dropdownStyle?.border === void 0;
|
|
3243
|
+
const borderSize = useDropdownBorder ? 2 : 0;
|
|
3244
|
+
let dropdownHeight = visibleCount + borderSize;
|
|
3245
|
+
if (hasScrollUp) dropdownHeight += 1;
|
|
3246
|
+
if (hasScrollDown) dropdownHeight += 1;
|
|
3247
|
+
if (hasSearch) dropdownHeight += 1;
|
|
3248
|
+
if (hasNoMatches) dropdownHeight += 1;
|
|
3249
|
+
const triggerBottom = triggerLayout.y + triggerLayout.height;
|
|
3250
|
+
const spaceBelow = screenRows - triggerBottom;
|
|
3251
|
+
const spaceAbove = triggerLayout.y;
|
|
3252
|
+
const openUpward = spaceBelow < dropdownHeight && spaceAbove >= dropdownHeight;
|
|
3253
|
+
const dropdownTop = openUpward ? -dropdownHeight : triggerLayout.height || 1;
|
|
3254
|
+
dropdownElement = React15__default.default.createElement(
|
|
3255
|
+
"box",
|
|
3256
|
+
{
|
|
3257
|
+
style: {
|
|
3258
|
+
position: "absolute",
|
|
3259
|
+
top: dropdownTop,
|
|
3260
|
+
left: 0,
|
|
3261
|
+
right: 0,
|
|
3262
|
+
zIndex: 9999,
|
|
3263
|
+
...useDropdownBorder ? { border: "single" } : {},
|
|
3264
|
+
bg: "black",
|
|
3265
|
+
flexDirection: "column",
|
|
3266
|
+
...dropdownStyle
|
|
3267
|
+
}
|
|
3268
|
+
},
|
|
3269
|
+
...dropdownChildren
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
const outerStyle = {
|
|
3273
|
+
flexDirection: "column",
|
|
3274
|
+
width: triggerStyle.width ?? "100%",
|
|
3275
|
+
minWidth: triggerStyle.minWidth,
|
|
3276
|
+
maxWidth: triggerStyle.maxWidth,
|
|
3277
|
+
flexGrow: triggerStyle.flexGrow,
|
|
3278
|
+
flexShrink: triggerStyle.flexShrink ?? 1
|
|
3279
|
+
};
|
|
3280
|
+
return React15__default.default.createElement(
|
|
3281
|
+
"box",
|
|
3282
|
+
{ style: outerStyle },
|
|
3283
|
+
// Trigger
|
|
3284
|
+
React15__default.default.createElement(
|
|
3285
|
+
"box",
|
|
3286
|
+
{
|
|
3287
|
+
style: triggerStyle,
|
|
3288
|
+
focusable: !disabled,
|
|
3289
|
+
ref: (node) => {
|
|
3290
|
+
nodeRef.current = node ?? null;
|
|
3291
|
+
if (node) {
|
|
3292
|
+
focusIdRef.current = node.focusId;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
...triggerChildren
|
|
3297
|
+
),
|
|
3298
|
+
// Dropdown overlay
|
|
3299
|
+
dropdownElement
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
function Checkbox({
|
|
3303
|
+
checked,
|
|
3304
|
+
onChange,
|
|
3305
|
+
label,
|
|
3306
|
+
style,
|
|
3307
|
+
focusedStyle,
|
|
3308
|
+
disabled,
|
|
3309
|
+
checkedChar = "\u2713",
|
|
3310
|
+
uncheckedChar = " "
|
|
3311
|
+
}) {
|
|
3312
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
3313
|
+
const inputCtx = React15.useContext(InputContext);
|
|
3314
|
+
const nodeRef = React15.useRef(null);
|
|
3315
|
+
const focusIdRef = React15.useRef(null);
|
|
3316
|
+
const onChangeRef = React15.useRef(onChange);
|
|
3317
|
+
onChangeRef.current = onChange;
|
|
3318
|
+
const checkedRef = React15.useRef(checked);
|
|
3319
|
+
checkedRef.current = checked;
|
|
3320
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
3321
|
+
React15.useEffect(() => {
|
|
3322
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
|
|
3323
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
3324
|
+
}, [focusCtx, disabled]);
|
|
3325
|
+
React15.useEffect(() => {
|
|
3326
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
3327
|
+
const fid = focusIdRef.current;
|
|
3328
|
+
setIsFocused(focusCtx.focusedId === fid);
|
|
3329
|
+
return focusCtx.onFocusChange((newId) => {
|
|
3330
|
+
setIsFocused(newId === fid);
|
|
3331
|
+
});
|
|
3332
|
+
}, [focusCtx]);
|
|
3333
|
+
React15.useEffect(() => {
|
|
3334
|
+
if (!inputCtx || !focusIdRef.current || disabled) return;
|
|
3335
|
+
const fid = focusIdRef.current;
|
|
3336
|
+
const handler = (key) => {
|
|
3337
|
+
if (focusCtx?.focusedId !== fid) return false;
|
|
3338
|
+
if (key.name === "return" || key.name === " " || key.sequence === " ") {
|
|
3339
|
+
onChangeRef.current(!checkedRef.current);
|
|
3340
|
+
return true;
|
|
3341
|
+
}
|
|
3342
|
+
return false;
|
|
3343
|
+
};
|
|
3344
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
3345
|
+
}, [inputCtx, focusCtx, disabled]);
|
|
3346
|
+
const mergedStyle = {
|
|
3347
|
+
flexDirection: "row",
|
|
3348
|
+
gap: 1,
|
|
3349
|
+
...style,
|
|
3350
|
+
...isFocused && focusedStyle ? focusedStyle : {}
|
|
3351
|
+
};
|
|
3352
|
+
const boxChar = checked ? checkedChar : uncheckedChar;
|
|
3353
|
+
const boxStyle = {
|
|
3354
|
+
color: disabled ? "blackBright" : isFocused ? "white" : style?.color
|
|
3355
|
+
};
|
|
3356
|
+
const labelStyle = {
|
|
3357
|
+
color: disabled ? "blackBright" : style?.color
|
|
3358
|
+
};
|
|
3359
|
+
return React15__default.default.createElement(
|
|
3360
|
+
"box",
|
|
3361
|
+
{
|
|
3362
|
+
style: mergedStyle,
|
|
3363
|
+
focusable: !disabled,
|
|
3364
|
+
ref: (node) => {
|
|
3365
|
+
if (node) {
|
|
3366
|
+
nodeRef.current = node;
|
|
3367
|
+
focusIdRef.current = node.focusId;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
},
|
|
3371
|
+
React15__default.default.createElement(
|
|
3372
|
+
"text",
|
|
3373
|
+
{ key: "box", style: boxStyle },
|
|
3374
|
+
`[${boxChar}]`
|
|
3375
|
+
),
|
|
3376
|
+
label ? React15__default.default.createElement(
|
|
3377
|
+
"text",
|
|
3378
|
+
{ key: "label", style: labelStyle },
|
|
3379
|
+
label
|
|
3380
|
+
) : null
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3383
|
+
function Radio({
|
|
3384
|
+
items,
|
|
3385
|
+
value,
|
|
3386
|
+
onChange,
|
|
3387
|
+
style,
|
|
3388
|
+
itemStyle,
|
|
3389
|
+
focusedItemStyle,
|
|
3390
|
+
selectedItemStyle,
|
|
3391
|
+
disabled,
|
|
3392
|
+
direction = "column",
|
|
3393
|
+
gap = 0,
|
|
3394
|
+
selectedChar = "\u25CF",
|
|
3395
|
+
unselectedChar = "\u25CB"
|
|
3396
|
+
}) {
|
|
3397
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
3398
|
+
const inputCtx = React15.useContext(InputContext);
|
|
3399
|
+
const nodeRef = React15.useRef(null);
|
|
3400
|
+
const focusIdRef = React15.useRef(null);
|
|
3401
|
+
const onChangeRef = React15.useRef(onChange);
|
|
3402
|
+
onChangeRef.current = onChange;
|
|
3403
|
+
const [isFocused, setIsFocused] = React15.useState(false);
|
|
3404
|
+
const [highlightedIndex, setHighlightedIndex] = React15.useState(() => {
|
|
3405
|
+
const selectedIdx = items.findIndex((item) => item.value === value);
|
|
3406
|
+
if (selectedIdx >= 0) return selectedIdx;
|
|
3407
|
+
return items.findIndex((item) => !item.disabled);
|
|
3408
|
+
});
|
|
3409
|
+
const findNextEnabled = React15.useCallback(
|
|
3410
|
+
(startIndex, direction2) => {
|
|
3411
|
+
let index = startIndex;
|
|
3412
|
+
for (let i = 0; i < items.length; i++) {
|
|
3413
|
+
index = (index + direction2 + items.length) % items.length;
|
|
3414
|
+
if (!items[index]?.disabled) return index;
|
|
3415
|
+
}
|
|
3416
|
+
return startIndex;
|
|
3417
|
+
},
|
|
3418
|
+
[items]
|
|
3419
|
+
);
|
|
3420
|
+
React15.useEffect(() => {
|
|
3421
|
+
if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
|
|
3422
|
+
return focusCtx.register(focusIdRef.current, nodeRef.current);
|
|
3423
|
+
}, [focusCtx, disabled]);
|
|
3424
|
+
React15.useEffect(() => {
|
|
3425
|
+
if (!focusCtx || !focusIdRef.current) return;
|
|
3426
|
+
const fid = focusIdRef.current;
|
|
3427
|
+
setIsFocused(focusCtx.focusedId === fid);
|
|
3428
|
+
return focusCtx.onFocusChange((newId) => {
|
|
3429
|
+
setIsFocused(newId === fid);
|
|
3430
|
+
});
|
|
3431
|
+
}, [focusCtx]);
|
|
3432
|
+
React15.useEffect(() => {
|
|
3433
|
+
if (!inputCtx || !focusIdRef.current || disabled) return;
|
|
3434
|
+
const fid = focusIdRef.current;
|
|
3435
|
+
const handler = (key) => {
|
|
3436
|
+
if (focusCtx?.focusedId !== fid) return false;
|
|
3437
|
+
if (key.name === "up" || key.name === "left" || key.name === "k" || key.name === "tab" && key.shift) {
|
|
3438
|
+
setHighlightedIndex((idx) => findNextEnabled(idx, -1));
|
|
3439
|
+
return true;
|
|
3440
|
+
}
|
|
3441
|
+
if (key.name === "down" || key.name === "right" || key.name === "j" || key.name === "tab" && !key.shift) {
|
|
3442
|
+
setHighlightedIndex((idx) => findNextEnabled(idx, 1));
|
|
3443
|
+
return true;
|
|
3444
|
+
}
|
|
3445
|
+
if (key.name === "return" || key.name === " " || key.sequence === " ") {
|
|
3446
|
+
const item = items[highlightedIndex];
|
|
3447
|
+
if (item && !item.disabled) {
|
|
3448
|
+
onChangeRef.current(item.value);
|
|
3449
|
+
}
|
|
3450
|
+
return true;
|
|
3451
|
+
}
|
|
3452
|
+
return false;
|
|
3453
|
+
};
|
|
3454
|
+
return inputCtx.registerInputHandler(fid, handler);
|
|
3455
|
+
}, [inputCtx, focusCtx, disabled, items, highlightedIndex, findNextEnabled]);
|
|
3456
|
+
React15.useEffect(() => {
|
|
3457
|
+
const selectedIdx = items.findIndex((item) => item.value === value);
|
|
3458
|
+
if (selectedIdx >= 0) {
|
|
3459
|
+
setHighlightedIndex(selectedIdx);
|
|
3460
|
+
}
|
|
3461
|
+
}, [value, items]);
|
|
3462
|
+
const containerStyle = {
|
|
3463
|
+
flexDirection: direction,
|
|
3464
|
+
gap,
|
|
3465
|
+
...style
|
|
3466
|
+
};
|
|
3467
|
+
const radioItems = items.map((item, index) => {
|
|
3468
|
+
const isSelected = item.value === value;
|
|
3469
|
+
const isHighlighted = index === highlightedIndex;
|
|
3470
|
+
const isItemDisabled = disabled || item.disabled;
|
|
3471
|
+
const radioChar = isSelected ? selectedChar : unselectedChar;
|
|
3472
|
+
let computedStyle = {
|
|
3473
|
+
flexDirection: "row",
|
|
3474
|
+
gap: 1,
|
|
3475
|
+
...itemStyle
|
|
3476
|
+
};
|
|
3477
|
+
if (isSelected && selectedItemStyle) {
|
|
3478
|
+
computedStyle = { ...computedStyle, ...selectedItemStyle };
|
|
3479
|
+
}
|
|
3480
|
+
if (isFocused && isHighlighted && focusedItemStyle) {
|
|
3481
|
+
computedStyle = { ...computedStyle, ...focusedItemStyle };
|
|
3482
|
+
}
|
|
3483
|
+
const textColor = isItemDisabled ? "blackBright" : isFocused && isHighlighted ? focusedItemStyle?.color ?? "white" : isSelected ? selectedItemStyle?.color ?? itemStyle?.color : itemStyle?.color;
|
|
3484
|
+
return React15__default.default.createElement(
|
|
3485
|
+
"box",
|
|
3486
|
+
{ key: index, style: computedStyle },
|
|
3487
|
+
React15__default.default.createElement(
|
|
3488
|
+
"text",
|
|
3489
|
+
{ key: "radio", style: { color: textColor } },
|
|
3490
|
+
`(${radioChar})`
|
|
3491
|
+
),
|
|
3492
|
+
React15__default.default.createElement(
|
|
3493
|
+
"text",
|
|
3494
|
+
{ key: "label", style: { color: textColor } },
|
|
3495
|
+
item.label
|
|
3496
|
+
)
|
|
3497
|
+
);
|
|
3498
|
+
});
|
|
3499
|
+
return React15__default.default.createElement(
|
|
3500
|
+
"box",
|
|
3501
|
+
{
|
|
3502
|
+
style: containerStyle,
|
|
3503
|
+
focusable: !disabled,
|
|
3504
|
+
ref: (node) => {
|
|
3505
|
+
if (node) {
|
|
3506
|
+
nodeRef.current = node;
|
|
3507
|
+
focusIdRef.current = node.focusId;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
},
|
|
3511
|
+
...radioItems
|
|
3512
|
+
);
|
|
3513
|
+
}
|
|
3514
|
+
function useFocus(nodeRef) {
|
|
3515
|
+
const focusCtx = React15.useContext(FocusContext);
|
|
3516
|
+
const [id] = React15.useState(() => `focus-${Math.random().toString(36).slice(2, 9)}`);
|
|
3517
|
+
const isFocused = focusCtx ? focusCtx.focusedId === id : false;
|
|
3518
|
+
React15.useEffect(() => {
|
|
3519
|
+
if (!focusCtx || !nodeRef?.current) return;
|
|
3520
|
+
nodeRef.current.focusId = id;
|
|
3521
|
+
return focusCtx.register(id, nodeRef.current);
|
|
3522
|
+
}, [focusCtx, id, nodeRef]);
|
|
3523
|
+
const focus = React15.useMemo(() => {
|
|
3524
|
+
return () => {
|
|
3525
|
+
focusCtx?.requestFocus(id);
|
|
3526
|
+
};
|
|
3527
|
+
}, [focusCtx, id]);
|
|
3528
|
+
return { focused: isFocused, focus };
|
|
3529
|
+
}
|
|
3530
|
+
function useApp() {
|
|
3531
|
+
const ctx = React15.useContext(AppContext);
|
|
3532
|
+
if (!ctx) {
|
|
3533
|
+
throw new Error("useApp must be used within a Glyph render tree");
|
|
3534
|
+
}
|
|
3535
|
+
return {
|
|
3536
|
+
exit: ctx.exit,
|
|
3537
|
+
get columns() {
|
|
3538
|
+
return ctx.columns;
|
|
3539
|
+
},
|
|
3540
|
+
get rows() {
|
|
3541
|
+
return ctx.rows;
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
exports.Box = Box;
|
|
3547
|
+
exports.Button = Button;
|
|
3548
|
+
exports.Checkbox = Checkbox;
|
|
3549
|
+
exports.FocusScope = FocusScope;
|
|
3550
|
+
exports.Input = Input;
|
|
3551
|
+
exports.Keybind = Keybind;
|
|
3552
|
+
exports.List = List;
|
|
3553
|
+
exports.Menu = Menu;
|
|
3554
|
+
exports.Portal = Portal;
|
|
3555
|
+
exports.Progress = Progress;
|
|
3556
|
+
exports.Radio = Radio;
|
|
3557
|
+
exports.ScrollView = ScrollView;
|
|
3558
|
+
exports.Select = Select;
|
|
3559
|
+
exports.Spacer = Spacer;
|
|
3560
|
+
exports.Spinner = Spinner;
|
|
3561
|
+
exports.Text = Text;
|
|
3562
|
+
exports.ToastHost = ToastHost;
|
|
3563
|
+
exports.render = render;
|
|
3564
|
+
exports.useApp = useApp;
|
|
3565
|
+
exports.useFocus = useFocus;
|
|
3566
|
+
exports.useInput = useInput;
|
|
3567
|
+
exports.useLayout = useLayout;
|
|
3568
|
+
exports.useToast = useToast;
|
|
3569
|
+
//# sourceMappingURL=index.cjs.map
|
|
3570
|
+
//# sourceMappingURL=index.cjs.map
|