@felipe-software/react-native-gpui 0.0.0-placeholder → 1.0.1

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.
Files changed (72) hide show
  1. package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
  2. package/LICENSE +21 -0
  3. package/README.md +658 -1
  4. package/cli/bin.ts +379 -0
  5. package/cli/bundle.ts +32 -0
  6. package/cli/commands/do.ts +213 -0
  7. package/cli/commands/flow.ts +283 -0
  8. package/cli/commands/get.ts +584 -0
  9. package/cli/commands/shot.ts +312 -0
  10. package/cli/commands/trace.ts +293 -0
  11. package/cli/host.ts +714 -0
  12. package/cli/hotReload.ts +210 -0
  13. package/cli/run.ts +66 -0
  14. package/cli/selectors.ts +205 -0
  15. package/dist/Animated.d.ts +120 -0
  16. package/dist/ChatList.d.ts +28 -0
  17. package/dist/Dimensions.d.ts +31 -0
  18. package/dist/StyleSheet.d.ts +18 -0
  19. package/dist/Window.d.ts +33 -0
  20. package/dist/apis.d.ts +165 -0
  21. package/dist/app-state.d.ts +27 -0
  22. package/dist/chat-list-types.d.ts +136 -0
  23. package/dist/colors.d.ts +32 -0
  24. package/dist/commands.d.ts +230 -0
  25. package/dist/components.d.ts +419 -0
  26. package/dist/hotModules.d.ts +1 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.development.js +14876 -0
  29. package/dist/index.js +12 -0
  30. package/dist/keyboard.d.ts +140 -0
  31. package/dist/native-menu.d.ts +7 -0
  32. package/dist/platform-driver.d.ts +32 -0
  33. package/dist/platform-services.d.ts +35 -0
  34. package/dist/portal.d.ts +43 -0
  35. package/dist/raf.d.ts +4 -0
  36. package/dist/reanimated/colors-processor-shim.d.ts +10 -0
  37. package/dist/reanimated/reanimated-host.d.ts +10 -0
  38. package/dist/reanimated/seam.d.ts +34 -0
  39. package/dist/reanimated/ui-entry.d.ts +2 -0
  40. package/dist/reanimated/worklet-channel.d.ts +10 -0
  41. package/dist/reanimated/worklet-runtime.d.ts +466 -0
  42. package/dist/reanimated/worklets.d.ts +199 -0
  43. package/dist/reconciler.d.ts +102 -0
  44. package/dist/refresh.d.ts +1 -0
  45. package/dist/render.d.ts +37 -0
  46. package/dist/runtime.d.ts +235 -0
  47. package/dist/surfaces.d.ts +51 -0
  48. package/dist/types.d.ts +276 -0
  49. package/dist/ui-runtime.js +42276 -0
  50. package/dist/window-context.d.ts +7 -0
  51. package/dist/window-host.d.ts +41 -0
  52. package/dist/wire-delta.d.ts +20 -0
  53. package/package.json +177 -1
  54. package/scripts/bundle-app.mjs +88 -0
  55. package/scripts/conformance-utils.mjs +293 -0
  56. package/scripts/hot-update-alias-plugin.mjs +88 -0
  57. package/scripts/native-package-exports-plugin.mjs +235 -0
  58. package/scripts/pixel.mjs +152 -0
  59. package/scripts/png.mjs +198 -0
  60. package/scripts/react-refresh-swc.mjs +35 -0
  61. package/scripts/reanimated-bun-plugin.mjs +188 -0
  62. package/scripts/service-binary.d.mts +27 -0
  63. package/scripts/service-binary.mjs +100 -0
  64. package/src/reanimated/colors-processor-shim.ts +119 -0
  65. package/src/reanimated/reanimated-host.ts +69 -0
  66. package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
  67. package/src/reanimated/seam.ts +566 -0
  68. package/src/reanimated/ui-entry.ts +80 -0
  69. package/src/reanimated/worklet-channel.ts +64 -0
  70. package/src/reanimated/worklet-runtime.ts +2569 -0
  71. package/src/reanimated/worklets.ts +1334 -0
  72. package/src/refresh.ts +33 -0
@@ -0,0 +1,584 @@
1
+ // `rngpui get …` — introspection commands. Everything here is read-only and reports
2
+ // what the node tree + a fresh pixel capture actually say, never a guess.
3
+
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { isDriveableHost, listServiceWindows, type AttachedHost, type DumpNode, type LaunchedHost } from "../host";
8
+ import { isVisible, nodeAtPoint, parsePoint, resolve, walk } from "../selectors";
9
+ import { averageColor, dominantColor, pixelAt } from "../../scripts/pixel.mjs";
10
+ import { readPng } from "../../scripts/png.mjs";
11
+ import type { RendererProvenance } from "../../src/runtime";
12
+
13
+ type Host = LaunchedHost | AttachedHost;
14
+
15
+ function out(json: boolean, human: () => void, data: unknown) {
16
+ if (json) console.log(JSON.stringify(data, null, 2));
17
+ else human();
18
+ }
19
+
20
+ function shortId(node: DumpNode): string {
21
+ const a = node.accessibility ?? {};
22
+ return a.testID ?? a.identifier ?? a.nativeID ?? a.label ?? (node.text ? `"${node.text.slice(0, 24)}"` : `#${node.globalId}`);
23
+ }
24
+
25
+ function boundsStr(node: DumpNode): string {
26
+ const b = node.bounds;
27
+ return b ? `${b.x.toFixed(0)},${b.y.toFixed(0)} ${b.width.toFixed(0)}x${b.height.toFixed(0)}` : "—";
28
+ }
29
+
30
+ // Sample dominant + average color within a node's bounds from a fresh capture. The
31
+ // capture is the WindowServer composite (full-opacity), so this is the ACTUAL rendered
32
+ // color, including occlusion by native child windows (WebView) — the whole point.
33
+ function sampleNode(host: Host, node: DumpNode): { dominant: string; average: string; coverage: number } | null {
34
+ const b = node.bounds;
35
+ if (!b || b.width < 1 || b.height < 1) return null;
36
+ const dir = mkdtempSync(join(tmpdir(), "rngpui-sample-"));
37
+ const png = join(dir, "shot.png");
38
+ try {
39
+ host.capture(png);
40
+ const img = readPng(png);
41
+ const scaleX = img.width / host.window.width;
42
+ const scaleY = img.height / host.window.height;
43
+ const rect = {
44
+ x: Math.round(b.x * scaleX),
45
+ y: Math.round(b.y * scaleY),
46
+ width: Math.max(1, Math.round(b.width * scaleX)),
47
+ height: Math.max(1, Math.round(b.height * scaleY)),
48
+ };
49
+ const dom = dominantColor(img, rect);
50
+ const avg = averageColor(img, rect);
51
+ return { dominant: dom.hex, average: avg.hex, coverage: dom.coverage };
52
+ } finally {
53
+ rmSync(dir, { recursive: true, force: true });
54
+ }
55
+ }
56
+
57
+ function renderTree(node: DumpNode, depth: number, lines: string[], filter?: (n: DumpNode) => boolean) {
58
+ const indent = " ".repeat(depth);
59
+ const id = shortId(node);
60
+ const ev = node.events?.length ? ` (${node.events.includes("press") || node.events.includes("click") ? "tap" : node.events[0]})` : "";
61
+ const vis = isVisible(node) ? "" : " [hidden]";
62
+ if (!filter || filter(node)) {
63
+ lines.push(`${indent}${node.type} ${id !== `#${node.globalId}` ? id + " " : ""}#${node.globalId} [${boundsStr(node)}]${ev}${vis}`);
64
+ }
65
+ for (const child of node.children ?? []) renderTree(child, depth + 1, lines, filter);
66
+ }
67
+
68
+ type ScreenSummary = {
69
+ target: {
70
+ mode: Host["mode"];
71
+ appName: string | null;
72
+ servicePid: number;
73
+ window: { id: number; width: number; height: number; owner: string; title: string };
74
+ driveable: boolean;
75
+ controlSocketPath: string | null;
76
+ };
77
+ stats: TreeStats;
78
+ webviews: Array<{ globalId: number; id: string; bounds: DumpNode["bounds"] | null; visible: boolean; source: string | null }>;
79
+ interactive: Array<{
80
+ globalId: number;
81
+ selector: string;
82
+ type: string;
83
+ bounds: DumpNode["bounds"] | null;
84
+ events: string[];
85
+ stateStyle: Record<string, unknown>;
86
+ }>;
87
+ };
88
+
89
+ function screenSummary(host: Host, dump: DumpNode): ScreenSummary {
90
+ const interactive = [...walk(dump)]
91
+ .filter((node) => isVisible(node) && !!node.events?.length)
92
+ .sort((a, b) => (a.bounds!.y - b.bounds!.y || a.bounds!.x - b.bounds!.x || areaOf(a) - areaOf(b)))
93
+ .slice(0, 40)
94
+ .map((node) => ({
95
+ globalId: node.globalId,
96
+ selector: selectorFor(node),
97
+ type: node.type,
98
+ bounds: node.bounds ?? null,
99
+ events: node.events ?? [],
100
+ stateStyle: stateStyleOf(node),
101
+ }));
102
+ const webviews = [...walk(dump)]
103
+ .filter((node) => node.type === "webview")
104
+ .map((node) => ({
105
+ globalId: node.globalId,
106
+ id: shortId(node),
107
+ bounds: node.bounds ?? null,
108
+ visible: isVisible(node),
109
+ source: node.src ?? (node.text ? `inline-html ${node.text.length}b` : null),
110
+ }));
111
+ return {
112
+ target: {
113
+ mode: host.mode,
114
+ appName: host.mode === "launch" ? host.appName : host.appName ?? null,
115
+ servicePid: host.servicePid,
116
+ window: {
117
+ id: host.window.id ?? host.window.window_id,
118
+ width: host.window.width,
119
+ height: host.window.height,
120
+ owner: host.window.owner,
121
+ title: host.window.title,
122
+ },
123
+ driveable: isDriveableHost(host),
124
+ controlSocketPath: host.mode === "attach" ? host.controlSocketPath ?? null : "(session)",
125
+ },
126
+ stats: treeStats(dump),
127
+ webviews,
128
+ interactive,
129
+ };
130
+ }
131
+
132
+ function printScreenSummary(summary: ScreenSummary) {
133
+ const t = summary.target;
134
+ console.log(
135
+ ` target: ${t.mode} ${t.appName ?? t.window.owner} pid=${t.servicePid} window=${t.window.width}x${t.window.height} driveable=${t.driveable}${t.controlSocketPath ? ` socket=${t.controlSocketPath}` : ""}`
136
+ );
137
+ console.log(
138
+ ` tree: nodes=${summary.stats.nodes} visible=${summary.stats.visible} hidden=${summary.stats.hidden} interactive=${summary.stats.interactive} webviews=${summary.stats.webviews.visible}/${summary.stats.webviews.total}`
139
+ );
140
+ if (summary.webviews.length) {
141
+ console.log(" webviews:");
142
+ for (const webview of summary.webviews.slice(0, 8)) {
143
+ console.log(` ${webview.id} #${webview.globalId} [${webview.bounds ? `${webview.bounds.x.toFixed(0)},${webview.bounds.y.toFixed(0)} ${webview.bounds.width.toFixed(0)}x${webview.bounds.height.toFixed(0)}` : "-"}] visible=${webview.visible} source=${webview.source ?? "(none)"}`);
144
+ }
145
+ }
146
+ console.log(" tappable selectors:");
147
+ for (const node of summary.interactive.slice(0, 24)) {
148
+ const state = formatStateStyle(node.stateStyle);
149
+ console.log(` ${JSON.stringify(node.selector)} #${node.globalId} ${node.type} [${node.bounds ? `${node.bounds.x.toFixed(0)},${node.bounds.y.toFixed(0)} ${node.bounds.width.toFixed(0)}x${node.bounds.height.toFixed(0)}` : "-"}] (${node.events.join(",")})${state ? ` ${state}` : ""}`);
150
+ }
151
+ }
152
+
153
+ function selectorFor(node: DumpNode): string {
154
+ const a = node.accessibility ?? {};
155
+ return cleanSelector(a.testID) ?? cleanSelector(a.identifier) ?? cleanSelector(a.nativeID) ?? cleanSelector(a.label) ?? cleanSelector(node.text) ?? shortId(node);
156
+ }
157
+
158
+ function cleanSelector(value: unknown): string | null {
159
+ if (typeof value !== "string") return null;
160
+ const cleaned = value.replace(/\s+/g, " ").trim();
161
+ return cleaned ? cleaned.slice(0, 120) : null;
162
+ }
163
+
164
+ function areaOf(node: DumpNode): number {
165
+ const b = node.bounds;
166
+ return b ? b.width * b.height : Infinity;
167
+ }
168
+
169
+ function stateStyleOf(node: DumpNode): Record<string, unknown> {
170
+ const style = node.style ?? {};
171
+ const out: Record<string, unknown> = {};
172
+ for (const key of ["backgroundColor", "borderColor", "boxShadow", "opacity", "display"]) {
173
+ const value = style[key];
174
+ if (value !== undefined && value !== "none" && value !== "#00000000") out[key] = value;
175
+ }
176
+ return out;
177
+ }
178
+
179
+ function formatStateStyle(style: Record<string, unknown>): string {
180
+ const entries = Object.entries(style);
181
+ return entries.length ? entries.map(([key, value]) => `${key}=${String(value)}`).join(" ") : "";
182
+ }
183
+
184
+ export async function runGet(host: Host, sub: string, args: string[], json: boolean): Promise<number> {
185
+ if (sub === "windows") {
186
+ const windows = await listServiceWindows(host);
187
+ out(
188
+ json,
189
+ () => {
190
+ for (const window of windows) {
191
+ const b = window.bounds;
192
+ const geometry = b ? `${b.width.toFixed(0)}x${b.height.toFixed(0)} at ${b.x.toFixed(0)},${b.y.toFixed(0)}${b.active ? " active" : ""}` : "—";
193
+ const name = window.testID ? ` ${window.testID}` : "";
194
+ const title = window.title ? ` "${window.title}"` : "";
195
+ console.log(` ${window.id} ${window.kind}${name}${title} ${geometry} nodes=${window.nodes} frames=${window.framesPainted}`);
196
+ }
197
+ },
198
+ windows,
199
+ );
200
+ return 0;
201
+ }
202
+ if (sub === "provenance") {
203
+ if (!isDriveableHost(host)) {
204
+ console.error(" get provenance needs a driveable target (control socket)");
205
+ return 1;
206
+ }
207
+ const result = await host.request<{ ok: boolean; error?: string } & Partial<RendererProvenance>>({
208
+ $cmd: "rendererProvenance",
209
+ });
210
+ if (!result.ok) {
211
+ console.error(` renderer provenance failed: ${result.error || "native command failed"}`);
212
+ return 1;
213
+ }
214
+ out(
215
+ json,
216
+ () => {
217
+ console.log(` renderer: ${result.renderer} ${result.rendererVersion}`);
218
+ console.log(` service: ${result.serviceVersion} GPUI: ${result.gpuiRevision} JS: ${result.jsEngine} ${result.jsEngineVersion}`);
219
+ console.log(` bundle: ${result.bundleUrl ?? "(embedded)"} development=${result.development === true}`);
220
+ },
221
+ result,
222
+ );
223
+ return 0;
224
+ }
225
+ if (sub === "frames") {
226
+ if (!isDriveableHost(host)) {
227
+ console.error(" get frames needs a driveable target (control socket)");
228
+ return 1;
229
+ }
230
+ const stats = await host.request<{ ok: boolean; framesPainted: number; fpsLast1s: number; lastFrameAgoMs: number | null; avgFrameGapMs: number | null }>({ $cmd: "frameStats" });
231
+ out(json, () => {
232
+ const ago = stats.lastFrameAgoMs == null ? "n/a" : `${stats.lastFrameAgoMs.toFixed(0)}ms ago`;
233
+ const gap = stats.avgFrameGapMs == null ? "n/a" : `${stats.avgFrameGapMs.toFixed(1)}ms`;
234
+ console.log(` frames painted: ${stats.framesPainted} — ${stats.fpsLast1s}fps last 1s, last frame ${ago}, avg gap ${gap}`);
235
+ }, stats);
236
+ return 0;
237
+ }
238
+ const dump = await host.dump();
239
+ if (!dump) {
240
+ if (sub === "color") {
241
+ const pt = parsePoint(args[0] ?? "");
242
+ if (!pt) {
243
+ console.error(" attached color sampling only supports a literal point: rngpui get color <x,y> --attach");
244
+ return 1;
245
+ }
246
+ return getColorAtPoint(host, pt.x, pt.y, json);
247
+ }
248
+ if (sub === "point") {
249
+ const pt = parsePoint(args[0] ?? "");
250
+ if (!pt) {
251
+ console.error(" usage: rngpui get point <x,y>");
252
+ return 1;
253
+ }
254
+ const color = getColorSampleAtPoint(host, pt.x, pt.y);
255
+ out(json, () => console.log(` topmost: (attach has no tree)\n pixel: ${color ?? "(unavailable)"}`), {
256
+ point: pt,
257
+ topmost: null,
258
+ pixel: color,
259
+ });
260
+ return color ? 0 : 1;
261
+ }
262
+ console.error(" no tree available (attach exposes trees only when the app publishes RNGPUI_CONTROL_SOCKET metadata)");
263
+ return 1;
264
+ }
265
+
266
+ switch (sub) {
267
+ case "screen": {
268
+ const summary = screenSummary(host, dump);
269
+ out(json, () => printScreenSummary(summary), summary);
270
+ return 0;
271
+ }
272
+
273
+ case "tree": {
274
+ if (json) {
275
+ console.log(JSON.stringify(dump, null, 2));
276
+ } else {
277
+ const lines: string[] = [];
278
+ renderTree(dump, 0, lines);
279
+ console.log(lines.join("\n"));
280
+ }
281
+ return 0;
282
+ }
283
+
284
+ case "stats": {
285
+ const selector = args[0];
286
+ const root = selector ? matchNodes(dump, selector)[0] : dump;
287
+ if (!root) {
288
+ console.error(` no node matched "${selector}"`);
289
+ return 1;
290
+ }
291
+ const stats = treeStats(root);
292
+ out(
293
+ json,
294
+ () => {
295
+ console.log(
296
+ ` nodes=${stats.nodes} visible=${stats.visible} hidden=${stats.hidden} interactive=${stats.interactive} maxDepth=${stats.maxDepth}`
297
+ );
298
+ console.log(
299
+ ` webviews=${stats.webviews.total} visible=${stats.webviews.visible} hidden=${stats.webviews.hidden}`
300
+ );
301
+ if (stats.duplicateGlobalIds.length) {
302
+ console.log(` duplicate globalIds: ${stats.duplicateGlobalIds.join(", ")}`);
303
+ }
304
+ console.log(` by type: ${formatCounts(stats.byType)}`);
305
+ if (Object.keys(stats.byNativeListGroup).length) {
306
+ console.log(` list groups: ${formatCounts(stats.byNativeListGroup)}`);
307
+ }
308
+ if (Object.keys(stats.unpaintedByType).length) {
309
+ console.log(` unpainted by type: ${formatCounts(stats.unpaintedByType)}`);
310
+ }
311
+ },
312
+ stats,
313
+ );
314
+ return 0;
315
+ }
316
+
317
+ case "webviews": {
318
+ const webviews = [...walk(dump)]
319
+ .filter((node) => node.type === "webview")
320
+ .map((node) => ({
321
+ globalId: node.globalId,
322
+ id: shortId(node),
323
+ source: node.src ? { kind: "uri", value: node.src } : node.text ? { kind: "html", bytes: node.text.length } : null,
324
+ bounds: node.bounds ?? null,
325
+ visible: isVisible(node),
326
+ display: node.style?.display ?? null,
327
+ }));
328
+ out(
329
+ json,
330
+ () => {
331
+ if (!webviews.length) {
332
+ console.log(" no webviews");
333
+ return;
334
+ }
335
+ for (const webview of webviews) {
336
+ const source =
337
+ webview.source?.kind === "uri"
338
+ ? webview.source.value
339
+ : webview.source?.kind === "html"
340
+ ? `inline-html ${webview.source.bytes}b`
341
+ : "(no source)";
342
+ console.log(
343
+ ` webview ${webview.id} #${webview.globalId} [${webview.bounds ? `${webview.bounds.x.toFixed(0)},${webview.bounds.y.toFixed(0)} ${webview.bounds.width.toFixed(0)}x${webview.bounds.height.toFixed(0)}` : "—"}] visible=${webview.visible} display=${String(webview.display ?? "default")} source=${source}`
344
+ );
345
+ }
346
+ },
347
+ webviews,
348
+ );
349
+ return 0;
350
+ }
351
+
352
+ case "describe": {
353
+ const selector = args[0];
354
+ if (!selector) {
355
+ const summary = screenSummary(host, dump);
356
+ out(json, () => printScreenSummary(summary), summary);
357
+ return 0;
358
+ }
359
+ const nodes: DumpNode[] = selector ? matchNodes(dump, selector) : [dump];
360
+ if (nodes.length === 0) {
361
+ console.error(` no node matched "${selector}"`);
362
+ return 1;
363
+ }
364
+ const results = nodes.slice(0, 8).map((node) => {
365
+ const color = isVisible(node) ? sampleNode(host, node) : null;
366
+ return {
367
+ globalId: node.globalId,
368
+ type: node.type,
369
+ testID: node.accessibility?.testID ?? null,
370
+ identifier: node.accessibility?.identifier ?? null,
371
+ nativeID: node.accessibility?.nativeID ?? null,
372
+ label: node.accessibility?.label ?? null,
373
+ text: node.text ?? null,
374
+ value: node.value ?? null,
375
+ events: node.events ?? [],
376
+ bounds: node.bounds ?? null,
377
+ visible: isVisible(node),
378
+ style: node.style ?? {},
379
+ sampledColor: color,
380
+ };
381
+ });
382
+ out(
383
+ json,
384
+ () => {
385
+ for (const r of results) {
386
+ console.log(` ${r.type} #${r.globalId}${r.testID ? ` testID=${r.testID}` : ""}${r.identifier ? ` id=${r.identifier}` : ""}${r.label ? ` label=${JSON.stringify(r.label)}` : ""}`);
387
+ if (r.text) console.log(` text: ${JSON.stringify(r.text)}`);
388
+ console.log(` bounds: ${r.bounds ? `${r.bounds.x.toFixed(0)},${r.bounds.y.toFixed(0)} ${r.bounds.width.toFixed(0)}x${r.bounds.height.toFixed(0)}` : "(not painted)"} visible=${r.visible}`);
389
+ if (r.events.length) console.log(` events: ${r.events.join(", ")}`);
390
+ const styleKeys = Object.keys(r.style);
391
+ if (styleKeys.length) console.log(` style: ${styleKeys.map((k) => `${k}=${(r.style as Record<string, unknown>)[k]}`).join(" ")}`);
392
+ if (r.sampledColor) console.log(` sampled: dominant=${r.sampledColor.dominant} (${(r.sampledColor.coverage * 100).toFixed(0)}%) average=${r.sampledColor.average}`);
393
+ }
394
+ },
395
+ results.length === 1 ? results[0] : results,
396
+ );
397
+ return 0;
398
+ }
399
+
400
+ case "layout": {
401
+ const selector = args[0];
402
+ const nodes = selector ? matchNodes(dump, selector) : [...walk(dump)].filter(isVisible);
403
+ const rows = nodes.slice(0, 50).map((n) => ({ globalId: n.globalId, type: n.type, id: shortId(n), bounds: n.bounds ?? null, visible: isVisible(n) }));
404
+ out(json, () => rows.forEach((r) => console.log(` ${r.type} ${r.id} #${r.globalId} [${r.bounds ? `${r.bounds.x.toFixed(0)},${r.bounds.y.toFixed(0)} ${r.bounds.width.toFixed(0)}x${r.bounds.height.toFixed(0)}` : "—"}]`)), rows.length === 1 ? rows[0] : rows);
405
+ return 0;
406
+ }
407
+
408
+ case "style": {
409
+ const selector = args[0];
410
+ if (!selector) {
411
+ console.error(" usage: rngpui get style <selector>");
412
+ return 1;
413
+ }
414
+ const nodes = matchNodes(dump, selector);
415
+ if (nodes.length === 0) {
416
+ console.error(` no node matched "${selector}"`);
417
+ return 1;
418
+ }
419
+ const rows = nodes.slice(0, 8).map((n) => ({ globalId: n.globalId, type: n.type, style: n.style ?? {} }));
420
+ out(json, () => rows.forEach((r) => console.log(` ${r.type} #${r.globalId}: ${JSON.stringify(r.style)}`)), rows.length === 1 ? rows[0] : rows);
421
+ return 0;
422
+ }
423
+
424
+ case "color": {
425
+ const selector = args[0];
426
+ if (!selector) {
427
+ console.error(" usage: rngpui get color <selector|x,y>");
428
+ return 1;
429
+ }
430
+ const pt = parsePoint(selector);
431
+ if (pt) return getColorAtPoint(host, pt.x, pt.y, json);
432
+ const nodes = matchNodes(dump, selector);
433
+ const node = nodes[0];
434
+ if (!node) {
435
+ console.error(` no node matched "${selector}"`);
436
+ return 1;
437
+ }
438
+ const color = sampleNode(host, node);
439
+ if (!color) {
440
+ console.error(` node ${shortId(node)} #${node.globalId} is not painted (no bounds) — nothing to sample`);
441
+ return 1;
442
+ }
443
+ out(json, () => console.log(` ${shortId(node)} #${node.globalId} [${boundsStr(node)}] dominant=${color.dominant} (${(color.coverage * 100).toFixed(0)}%) average=${color.average}`), { globalId: node.globalId, bounds: node.bounds, ...color });
444
+ return 0;
445
+ }
446
+
447
+ case "point": {
448
+ const pt = parsePoint(args[0] ?? "");
449
+ if (!pt) {
450
+ console.error(" usage: rngpui get point <x,y>");
451
+ return 1;
452
+ }
453
+ const node = nodeAtPoint(dump, pt.x, pt.y);
454
+ const color = getColorSampleAtPoint(host, pt.x, pt.y);
455
+ out(
456
+ json,
457
+ () => {
458
+ if (node) console.log(` topmost: ${node.type} ${shortId(node)} #${node.globalId} [${boundsStr(node)}]${node.events?.length ? ` (${node.events.join(",")})` : ""}`);
459
+ else console.log(" topmost: (no node at point)");
460
+ if (color) console.log(` pixel: ${color}`);
461
+ },
462
+ {
463
+ point: pt,
464
+ topmost: node ? { globalId: node.globalId, type: node.type, id: shortId(node), bounds: node.bounds, events: node.events ?? [] } : null,
465
+ pixel: color,
466
+ },
467
+ );
468
+ return 0;
469
+ }
470
+
471
+ default:
472
+ console.error(` unknown get subcommand: ${sub}`);
473
+ console.error(" available: provenance, frames, screen, tree, stats, webviews, describe, layout, style, color, point");
474
+ return 1;
475
+ }
476
+ }
477
+
478
+ type TreeStats = {
479
+ root: { globalId: number; type: string; id: string };
480
+ nodes: number;
481
+ visible: number;
482
+ hidden: number;
483
+ interactive: number;
484
+ maxDepth: number;
485
+ duplicateGlobalIds: number[];
486
+ byType: Record<string, number>;
487
+ unpaintedByType: Record<string, number>;
488
+ byNativeListGroup: Record<string, number>;
489
+ webviews: { total: number; visible: number; hidden: number };
490
+ };
491
+
492
+ function treeStats(root: DumpNode): TreeStats {
493
+ const seen = new Map<number, number>();
494
+ const byType: Record<string, number> = {};
495
+ const unpaintedByType: Record<string, number> = {};
496
+ const byNativeListGroup: Record<string, number> = {};
497
+ const webviews = { total: 0, visible: 0, hidden: 0 };
498
+ let nodes = 0;
499
+ let visible = 0;
500
+ let interactive = 0;
501
+ let maxDepth = 0;
502
+
503
+ const visit = (node: DumpNode, depth: number) => {
504
+ nodes += 1;
505
+ maxDepth = Math.max(maxDepth, depth);
506
+ seen.set(node.globalId, (seen.get(node.globalId) ?? 0) + 1);
507
+ byType[node.type] = (byType[node.type] ?? 0) + 1;
508
+ if (node.nativeListGroup) {
509
+ byNativeListGroup[node.nativeListGroup] = (byNativeListGroup[node.nativeListGroup] ?? 0) + 1;
510
+ }
511
+ if (node.events?.length) interactive += 1;
512
+ if (isVisible(node)) {
513
+ visible += 1;
514
+ } else {
515
+ unpaintedByType[node.type] = (unpaintedByType[node.type] ?? 0) + 1;
516
+ }
517
+ if (node.type === "webview") {
518
+ webviews.total += 1;
519
+ if (isVisible(node)) webviews.visible += 1;
520
+ else webviews.hidden += 1;
521
+ }
522
+ for (const child of node.children ?? []) visit(child, depth + 1);
523
+ };
524
+ visit(root, 0);
525
+
526
+ return {
527
+ root: { globalId: root.globalId, type: root.type, id: shortId(root) },
528
+ nodes,
529
+ visible,
530
+ hidden: nodes - visible,
531
+ interactive,
532
+ maxDepth,
533
+ duplicateGlobalIds: [...seen.entries()]
534
+ .filter(([, count]) => count > 1)
535
+ .map(([id]) => id)
536
+ .sort((a, b) => a - b),
537
+ byType: sortCounts(byType),
538
+ unpaintedByType: sortCounts(unpaintedByType),
539
+ byNativeListGroup: sortCounts(byNativeListGroup),
540
+ webviews,
541
+ };
542
+ }
543
+
544
+ function sortCounts(counts: Record<string, number>) {
545
+ return Object.fromEntries(Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
546
+ }
547
+
548
+ function formatCounts(counts: Record<string, number>) {
549
+ const entries = Object.entries(counts);
550
+ return entries.length ? entries.map(([key, value]) => `${key}=${value}`).join(" ") : "(none)";
551
+ }
552
+
553
+ function matchNodes(dump: DumpNode, selector: string): DumpNode[] {
554
+ const { best, candidates } = resolve(dump, selector);
555
+ if (!best) return [];
556
+ // return the best plus any equally-named siblings (same matchedValue) for context.
557
+ return candidates.filter((c) => c.matchedValue === best.matchedValue).map((c) => c.node);
558
+ }
559
+
560
+ function getColorSampleAtPoint(host: Host, x: number, y: number): string | null {
561
+ const dir = mkdtempSync(join(tmpdir(), "rngpui-px-"));
562
+ const png = join(dir, "shot.png");
563
+ try {
564
+ host.capture(png);
565
+ const img = readPng(png);
566
+ const scaleX = img.width / host.window.width;
567
+ const scaleY = img.height / host.window.height;
568
+ return pixelAt(img, Math.round(x * scaleX), Math.round(y * scaleY)).hex;
569
+ } catch {
570
+ return null;
571
+ } finally {
572
+ rmSync(dir, { recursive: true, force: true });
573
+ }
574
+ }
575
+
576
+ function getColorAtPoint(host: Host, x: number, y: number, json: boolean): number {
577
+ const hex = getColorSampleAtPoint(host, x, y);
578
+ if (!hex) {
579
+ console.error(` could not sample pixel at ${x},${y}`);
580
+ return 1;
581
+ }
582
+ out(json, () => console.log(` ${x},${y}: ${hex}`), { x, y, hex });
583
+ return 0;
584
+ }