@oh-my-pi/pi-tui 18.0.8 → 18.0.9
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/CHANGELOG.md +18 -0
- package/dist/types/components/box.d.ts +2 -0
- package/dist/types/components/cancellable-loader.d.ts +2 -0
- package/dist/types/components/editor.d.ts +4 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/input.d.ts +2 -0
- package/dist/types/components/loader.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +2 -0
- package/dist/types/components/scroll-view.d.ts +2 -0
- package/dist/types/components/select-list.d.ts +2 -0
- package/dist/types/components/settings-list.d.ts +4 -0
- package/dist/types/components/spacer.d.ts +2 -0
- package/dist/types/components/tab-bar.d.ts +2 -0
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/components/truncated-text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/latex-to-unicode.d.ts +4 -4
- package/dist/types/tui.d.ts +23 -0
- package/package.json +3 -3
- package/src/components/box.ts +10 -0
- package/src/components/cancellable-loader.ts +7 -0
- package/src/components/editor.ts +28 -0
- package/src/components/image.ts +11 -0
- package/src/components/input.ts +13 -0
- package/src/components/loader.ts +10 -0
- package/src/components/markdown.ts +39 -10
- package/src/components/scroll-view.ts +12 -0
- package/src/components/select-list.ts +13 -0
- package/src/components/settings-list.ts +19 -0
- package/src/components/spacer.ts +4 -0
- package/src/components/tab-bar.ts +11 -0
- package/src/components/text.ts +11 -0
- package/src/components/truncated-text.ts +15 -1
- package/src/debug-server.ts +422 -0
- package/src/index.ts +2 -0
- package/src/latex-to-unicode.ts +401 -172
- package/src/tui.ts +105 -12
- package/src/utils.ts +10 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { createServer, type Server, type Socket } from "node:net";
|
|
3
|
+
import { type Component, isFocusable, type OverlayOptions, type TUI } from "./tui";
|
|
4
|
+
import { replaceTabs } from "./utils";
|
|
5
|
+
|
|
6
|
+
export interface TuiDebugTreeNode {
|
|
7
|
+
kind: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
rect?: [x: number, y: number, w: number, h: number];
|
|
10
|
+
focused?: boolean;
|
|
11
|
+
focusable?: boolean;
|
|
12
|
+
hidden?: boolean;
|
|
13
|
+
children?: TuiDebugTreeNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TuiDebugOverlayNode {
|
|
17
|
+
overlay: number;
|
|
18
|
+
band?: unknown;
|
|
19
|
+
hidden?: boolean;
|
|
20
|
+
root: TuiDebugTreeNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TuiDebugTree {
|
|
24
|
+
root: TuiDebugTreeNode;
|
|
25
|
+
overlays: TuiDebugOverlayNode[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type DebugRequest = Record<string, unknown> & { op?: unknown };
|
|
29
|
+
type DebugResponse = { ok: boolean; [key: string]: unknown };
|
|
30
|
+
|
|
31
|
+
const SPECIAL_KEYS: Readonly<Record<string, string>> = {
|
|
32
|
+
enter: "\r",
|
|
33
|
+
return: "\r",
|
|
34
|
+
tab: "\t",
|
|
35
|
+
esc: "\x1b",
|
|
36
|
+
escape: "\x1b",
|
|
37
|
+
space: " ",
|
|
38
|
+
backspace: "\x7f",
|
|
39
|
+
delete: "\x1b[3~",
|
|
40
|
+
del: "\x1b[3~",
|
|
41
|
+
up: "\x1b[A",
|
|
42
|
+
down: "\x1b[B",
|
|
43
|
+
right: "\x1b[C",
|
|
44
|
+
left: "\x1b[D",
|
|
45
|
+
home: "\x1b[H",
|
|
46
|
+
end: "\x1b[F",
|
|
47
|
+
pgup: "\x1b[5~",
|
|
48
|
+
pageup: "\x1b[5~",
|
|
49
|
+
pgdn: "\x1b[6~",
|
|
50
|
+
pagedown: "\x1b[6~",
|
|
51
|
+
insert: "\x1b[2~",
|
|
52
|
+
f1: "\x1bOP",
|
|
53
|
+
f2: "\x1bOQ",
|
|
54
|
+
f3: "\x1bOR",
|
|
55
|
+
f4: "\x1bOS",
|
|
56
|
+
f5: "\x1b[15~",
|
|
57
|
+
f6: "\x1b[17~",
|
|
58
|
+
f7: "\x1b[18~",
|
|
59
|
+
f8: "\x1b[19~",
|
|
60
|
+
f9: "\x1b[20~",
|
|
61
|
+
f10: "\x1b[21~",
|
|
62
|
+
f11: "\x1b[23~",
|
|
63
|
+
f12: "\x1b[24~",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function plainLine(line: string): string {
|
|
67
|
+
return replaceTabs(Bun.stripANSI(line)).replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function errorMessage(error: unknown): string {
|
|
71
|
+
try {
|
|
72
|
+
return error instanceof Error ? error.message : String(error);
|
|
73
|
+
} catch {
|
|
74
|
+
return "debug operation failed";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function componentChildren(component: Component): readonly Component[] {
|
|
79
|
+
if (component.debugChildren !== undefined) return component.debugChildren;
|
|
80
|
+
if (!("children" in component)) return [];
|
|
81
|
+
const children = (component as Component & { children?: unknown }).children;
|
|
82
|
+
if (!Array.isArray(children)) return [];
|
|
83
|
+
return children.filter((child): child is Component => {
|
|
84
|
+
return typeof child === "object" && child !== null && "render" in child && typeof child.render === "function";
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function componentKind(component: Component): string {
|
|
89
|
+
return component.debugKind ?? component.constructor.name;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function serializableBand(options: OverlayOptions | undefined): unknown {
|
|
93
|
+
if (options === undefined) return undefined;
|
|
94
|
+
const { visible: _visible, ...band } = options;
|
|
95
|
+
return band;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function modifiedSpecial(sequence: string, modifier: number): string {
|
|
99
|
+
if (modifier === 1) return sequence;
|
|
100
|
+
if (sequence === "\t" && (modifier & 1) === 0) {
|
|
101
|
+
return modifier === 2 ? "\x1b[Z" : `\x1b[1;${modifier}Z`;
|
|
102
|
+
}
|
|
103
|
+
const csiFinal = sequence.match(/^\x1b\[([ABCDHF])$/);
|
|
104
|
+
if (csiFinal) return `\x1b[1;${modifier}${csiFinal[1]}`;
|
|
105
|
+
const ss3Final = sequence.match(/^\x1bO([PQRS])$/);
|
|
106
|
+
if (ss3Final) return `\x1b[1;${modifier}${ss3Final[1]}`;
|
|
107
|
+
const tilde = sequence.match(/^\x1b\[(\d+)~$/);
|
|
108
|
+
if (tilde) return `\x1b[${tilde[1]};${modifier}~`;
|
|
109
|
+
return sequence;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ctrlCharacter(character: string): string {
|
|
113
|
+
const code = character.toUpperCase().charCodeAt(0);
|
|
114
|
+
if ((code >= 64 && code <= 95) || code === 63) return String.fromCharCode(code === 63 ? 127 : code & 31);
|
|
115
|
+
throw new Error(`cannot encode ctrl chord ${character}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function encodeChord(token: string): string {
|
|
119
|
+
let rest = token;
|
|
120
|
+
let ctrl = false;
|
|
121
|
+
let alt = false;
|
|
122
|
+
let shift = false;
|
|
123
|
+
if (/^C-/i.test(rest)) {
|
|
124
|
+
ctrl = true;
|
|
125
|
+
rest = rest.slice(2);
|
|
126
|
+
}
|
|
127
|
+
if (/^(?:A|M)-/i.test(rest)) {
|
|
128
|
+
alt = true;
|
|
129
|
+
rest = rest.slice(2);
|
|
130
|
+
}
|
|
131
|
+
if (/^S-/i.test(rest)) {
|
|
132
|
+
shift = true;
|
|
133
|
+
rest = rest.slice(2);
|
|
134
|
+
}
|
|
135
|
+
if (rest.length === 0 || /^(?:C-|A-|M-|S-)/i.test(rest)) throw new Error(`invalid key token ${token}`);
|
|
136
|
+
|
|
137
|
+
const name = rest.toLowerCase();
|
|
138
|
+
const special = SPECIAL_KEYS[name];
|
|
139
|
+
if (special !== undefined) {
|
|
140
|
+
if (ctrl && name === "space") return alt ? "\x1b\x00" : "\x00";
|
|
141
|
+
if (ctrl && name === "backspace") return alt ? "\x1b\x08" : "\x08";
|
|
142
|
+
const modifier = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
|
|
143
|
+
let encoded = modifiedSpecial(special, modifier);
|
|
144
|
+
if (alt && encoded === special) encoded = `\x1b${encoded}`;
|
|
145
|
+
return encoded;
|
|
146
|
+
}
|
|
147
|
+
if (Array.from(rest).length !== 1) throw new Error(`unknown key ${rest}`);
|
|
148
|
+
let character = rest;
|
|
149
|
+
if (shift && /^[a-z]$/i.test(character)) character = character.toUpperCase();
|
|
150
|
+
if (ctrl) character = ctrlCharacter(character);
|
|
151
|
+
return alt ? `\x1b${character}` : character;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseKeyTokens(source: string): { sequences: string[]; events: number } {
|
|
155
|
+
let offset = 0;
|
|
156
|
+
const sequences: string[] = [];
|
|
157
|
+
let events = 0;
|
|
158
|
+
while (offset < source.length) {
|
|
159
|
+
while (/\s/.test(source[offset] ?? "")) offset++;
|
|
160
|
+
if (offset >= source.length) break;
|
|
161
|
+
const quote = source[offset];
|
|
162
|
+
if (quote === "'" || quote === '"') {
|
|
163
|
+
offset++;
|
|
164
|
+
let literal = "";
|
|
165
|
+
while (offset < source.length && source[offset] !== quote) {
|
|
166
|
+
if (source[offset] === "\\" && offset + 1 < source.length) offset++;
|
|
167
|
+
literal += source[offset++];
|
|
168
|
+
}
|
|
169
|
+
if (source[offset] !== quote) throw new Error("unterminated quoted key literal");
|
|
170
|
+
offset++;
|
|
171
|
+
sequences.push(...Array.from(literal));
|
|
172
|
+
events += Array.from(literal).length;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const start = offset;
|
|
176
|
+
while (offset < source.length && !/\s/.test(source[offset] ?? "")) offset++;
|
|
177
|
+
sequences.push(encodeChord(source.slice(start, offset)));
|
|
178
|
+
events++;
|
|
179
|
+
}
|
|
180
|
+
return { sequences, events };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mouseSequence(x: number, y: number, action: string): string {
|
|
184
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0)
|
|
185
|
+
throw new Error("mouse coordinates must be non-negative integers");
|
|
186
|
+
const at = (button: number, release = false): string => `\x1b[<${button};${x + 1};${y + 1}${release ? "m" : "M"}`;
|
|
187
|
+
switch (action) {
|
|
188
|
+
case "click":
|
|
189
|
+
return at(0) + at(0, true);
|
|
190
|
+
case "right-click":
|
|
191
|
+
return at(2) + at(2, true);
|
|
192
|
+
case "middle-click":
|
|
193
|
+
return at(1) + at(1, true);
|
|
194
|
+
case "move":
|
|
195
|
+
return at(35);
|
|
196
|
+
case "drag":
|
|
197
|
+
return at(32);
|
|
198
|
+
case "release":
|
|
199
|
+
return at(0, true);
|
|
200
|
+
case "wheel-up":
|
|
201
|
+
return at(64);
|
|
202
|
+
case "wheel-down":
|
|
203
|
+
return at(65);
|
|
204
|
+
default:
|
|
205
|
+
throw new Error(`unknown mouse action ${action}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** NDJSON debug and input server enabled by the `OMP_TUI_DEBUG` socket path. */
|
|
210
|
+
export class TuiDebugServer {
|
|
211
|
+
readonly #tui: TUI;
|
|
212
|
+
readonly #path: string;
|
|
213
|
+
#server: Server | undefined;
|
|
214
|
+
#sockets = new Set<Socket>();
|
|
215
|
+
|
|
216
|
+
constructor(tui: TUI, path: string) {
|
|
217
|
+
this.#tui = tui;
|
|
218
|
+
this.#path = path;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
start(): void {
|
|
222
|
+
if (this.#server !== undefined) return;
|
|
223
|
+
try {
|
|
224
|
+
if (existsSync(this.#path)) unlinkSync(this.#path);
|
|
225
|
+
} catch {
|
|
226
|
+
// The listen error below is isolated from the host application.
|
|
227
|
+
}
|
|
228
|
+
const server = createServer(socket => this.#accept(socket));
|
|
229
|
+
this.#server = server;
|
|
230
|
+
server.on("error", error => {
|
|
231
|
+
void error;
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
server.listen(this.#path);
|
|
235
|
+
} catch {
|
|
236
|
+
this.#server = undefined;
|
|
237
|
+
server.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
stop(): void {
|
|
242
|
+
const server = this.#server;
|
|
243
|
+
this.#server = undefined;
|
|
244
|
+
for (const socket of this.#sockets) socket.destroy();
|
|
245
|
+
this.#sockets.clear();
|
|
246
|
+
try {
|
|
247
|
+
server?.close();
|
|
248
|
+
} catch {
|
|
249
|
+
// A failed or already-closed listener still needs its stale path removed.
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
if (existsSync(this.#path)) unlinkSync(this.#path);
|
|
253
|
+
} catch {
|
|
254
|
+
// Teardown must not affect the host application.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#accept(socket: Socket): void {
|
|
259
|
+
this.#sockets.add(socket);
|
|
260
|
+
let buffer = "";
|
|
261
|
+
socket.setEncoding("utf8");
|
|
262
|
+
socket.on("data", chunk => {
|
|
263
|
+
buffer += chunk;
|
|
264
|
+
let newline = buffer.indexOf("\n");
|
|
265
|
+
while (newline !== -1) {
|
|
266
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
267
|
+
buffer = buffer.slice(newline + 1);
|
|
268
|
+
if (line.length > 0) this.#handleLine(socket, line);
|
|
269
|
+
newline = buffer.indexOf("\n");
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
socket.on("error", error => {
|
|
273
|
+
void error;
|
|
274
|
+
});
|
|
275
|
+
socket.on("close", () => this.#sockets.delete(socket));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#handleLine(socket: Socket, line: string): void {
|
|
279
|
+
let request: DebugRequest;
|
|
280
|
+
try {
|
|
281
|
+
const parsed: unknown = JSON.parse(line);
|
|
282
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
283
|
+
throw new Error("request must be an object");
|
|
284
|
+
request = parsed as DebugRequest;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.#write(socket, { ok: false, error: `malformed JSON: ${errorMessage(error)}` });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const response = this.#dispatch(request);
|
|
291
|
+
if (request.op === "quit" && response.ok) {
|
|
292
|
+
this.#write(socket, response, () => {
|
|
293
|
+
this.#tui.stop();
|
|
294
|
+
process.exit(0);
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.#write(socket, response);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.#write(socket, { ok: false, error: errorMessage(error) });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#write(socket: Socket, response: DebugResponse, flushed?: () => void): void {
|
|
305
|
+
let line: string;
|
|
306
|
+
try {
|
|
307
|
+
line = JSON.stringify(response);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
line = JSON.stringify({ ok: false, error: errorMessage(error) });
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
socket.write(`${line}\n`, flushed);
|
|
313
|
+
} catch {
|
|
314
|
+
// Broken clients cannot affect the TUI.
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#dispatch(request: DebugRequest): DebugResponse {
|
|
319
|
+
if (typeof request.op !== "string") return { ok: false, error: `unknown op ${String(request.op)}` };
|
|
320
|
+
switch (request.op) {
|
|
321
|
+
case "text": {
|
|
322
|
+
const paint = this.#tui.getDebugPaint();
|
|
323
|
+
if (paint === undefined) return { ok: false, error: "no frame painted yet" };
|
|
324
|
+
return {
|
|
325
|
+
ok: true,
|
|
326
|
+
lines: paint.lines.map(plainLine),
|
|
327
|
+
window_top: paint.windowTop,
|
|
328
|
+
alt_screen: paint.altScreen,
|
|
329
|
+
...(paint.cursor === undefined ? {} : { cursor: paint.cursor }),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case "frame":
|
|
333
|
+
return { ok: true, lines: this.#tui.getDebugDocument().map(plainLine) };
|
|
334
|
+
case "tree":
|
|
335
|
+
return { ok: true, tree: this.#tree() };
|
|
336
|
+
case "values":
|
|
337
|
+
return { ok: true, values: this.#values() };
|
|
338
|
+
case "info": {
|
|
339
|
+
const paint = this.#tui.getDebugPaint();
|
|
340
|
+
const focused = this.#tui.getFocused();
|
|
341
|
+
return {
|
|
342
|
+
ok: true,
|
|
343
|
+
columns: this.#tui.terminal.columns,
|
|
344
|
+
rows: this.#tui.terminal.rows,
|
|
345
|
+
pid: process.pid,
|
|
346
|
+
overlays: this.#tui.overlayStack.length,
|
|
347
|
+
focused: focused === null ? null : componentKind(focused),
|
|
348
|
+
alt_screen: paint?.altScreen ?? false,
|
|
349
|
+
cursor: paint?.cursor ?? { visible: false },
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
case "keys": {
|
|
353
|
+
if (typeof request.keys !== "string") throw new Error("keys must be a string");
|
|
354
|
+
const parsed = parseKeyTokens(request.keys);
|
|
355
|
+
for (const sequence of parsed.sequences) this.#tui.injectDebugInput(sequence);
|
|
356
|
+
return { ok: true, injected: parsed.events };
|
|
357
|
+
}
|
|
358
|
+
case "bytes":
|
|
359
|
+
if (typeof request.data !== "string") throw new Error("data must be a string");
|
|
360
|
+
this.#tui.injectDebugInput(request.data);
|
|
361
|
+
return { ok: true };
|
|
362
|
+
case "paste":
|
|
363
|
+
if (typeof request.text !== "string") throw new Error("text must be a string");
|
|
364
|
+
this.#tui.injectDebugInput(`\x1b[200~${request.text}\x1b[201~`);
|
|
365
|
+
return { ok: true };
|
|
366
|
+
case "mouse": {
|
|
367
|
+
if (typeof request.x !== "number" || typeof request.y !== "number")
|
|
368
|
+
throw new Error("mouse x and y must be numbers");
|
|
369
|
+
const action = request.action === undefined ? "click" : request.action;
|
|
370
|
+
if (typeof action !== "string") throw new Error("mouse action must be a string");
|
|
371
|
+
this.#tui.injectDebugInput(mouseSequence(request.x, request.y, action));
|
|
372
|
+
return { ok: true };
|
|
373
|
+
}
|
|
374
|
+
case "quit":
|
|
375
|
+
return { ok: true };
|
|
376
|
+
default:
|
|
377
|
+
return { ok: false, error: `unknown op ${request.op}` };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#node(component: Component): TuiDebugTreeNode {
|
|
382
|
+
const children = componentChildren(component);
|
|
383
|
+
const focusable = isFocusable(component);
|
|
384
|
+
return {
|
|
385
|
+
kind: componentKind(component),
|
|
386
|
+
...(component.debugId === undefined ? {} : { id: component.debugId }),
|
|
387
|
+
...(focusable ? { focusable: true, focused: component === this.#tui.getFocused() } : {}),
|
|
388
|
+
...(children.length === 0 ? {} : { children: children.map(child => this.#node(child)) }),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#tree(): TuiDebugTree {
|
|
393
|
+
return {
|
|
394
|
+
root: this.#node(this.#tui),
|
|
395
|
+
overlays: this.#tui.overlayStack.map((entry, overlay) => ({
|
|
396
|
+
overlay,
|
|
397
|
+
...(entry.options === undefined ? {} : { band: serializableBand(entry.options) }),
|
|
398
|
+
...(entry.hidden ? { hidden: true } : {}),
|
|
399
|
+
root: this.#node(entry.component),
|
|
400
|
+
})),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#values(): Record<string, unknown> {
|
|
405
|
+
const values: Record<string, unknown> = {};
|
|
406
|
+
const visit = (component: Component, path: readonly number[]): void => {
|
|
407
|
+
const kind = componentKind(component);
|
|
408
|
+
if (component.debugState !== undefined) {
|
|
409
|
+
const key = component.debugId === undefined ? `${kind}[${path.join(".")}]` : `${kind}#${component.debugId}`;
|
|
410
|
+
values[key] = component.debugState();
|
|
411
|
+
}
|
|
412
|
+
componentChildren(component).forEach((child, index) => {
|
|
413
|
+
visit(child, [...path, index]);
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
visit(this.#tui, []);
|
|
417
|
+
this.#tui.overlayStack.forEach((entry, index) => {
|
|
418
|
+
visit(entry.component, [this.#tui.children.length + index]);
|
|
419
|
+
});
|
|
420
|
+
return values;
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export * from "./components/spacer";
|
|
|
18
18
|
export * from "./components/tab-bar";
|
|
19
19
|
export * from "./components/text";
|
|
20
20
|
export * from "./components/truncated-text";
|
|
21
|
+
// Headless debug socket server
|
|
22
|
+
export * from "./debug-server";
|
|
21
23
|
// DECCARA rectangular-SGR background-fill optimizer
|
|
22
24
|
export * from "./deccara";
|
|
23
25
|
// Desktop notifications via D-Bus (Linux freedesktop notifications)
|