@gtkx/testing 0.20.0 → 1.0.0-rc.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 (216) hide show
  1. package/README.md +122 -55
  2. package/dist/act.d.ts +19 -0
  3. package/dist/act.d.ts.map +1 -0
  4. package/dist/act.js +48 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +32 -0
  7. package/dist/bound-queries.d.ts.map +1 -0
  8. package/dist/bound-queries.js +2 -0
  9. package/dist/bound-queries.js.map +1 -0
  10. package/dist/build-queries.d.ts +16 -0
  11. package/dist/build-queries.d.ts.map +1 -0
  12. package/dist/build-queries.js +103 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +4 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +11 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +18 -51
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +9 -40
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +7 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +24 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +29 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +119 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +7 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +9 -29
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +3 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +30 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +16 -11
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +10 -6
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +55 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +103 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +21 -25
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +69 -115
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +130 -177
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +212 -325
  53. package/dist/queries.js.map +1 -1
  54. package/dist/register-test-hooks.d.ts +2 -0
  55. package/dist/register-test-hooks.d.ts.map +1 -0
  56. package/dist/register-test-hooks.js +15 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +14 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +8 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +11 -41
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +129 -118
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +16 -51
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +39 -58
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +6 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +21 -109
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +20 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +89 -48
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +22 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +56 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +10 -8
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +28 -13
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +103 -172
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/user-event/adjustment.d.ts +8 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +33 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +7 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +65 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +7 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +28 -0
  100. package/dist/user-event/controller.js.map +1 -0
  101. package/dist/user-event/dispatch.d.ts +7 -0
  102. package/dist/user-event/dispatch.d.ts.map +1 -0
  103. package/dist/user-event/dispatch.js +10 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +3 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +48 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +31 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +105 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +44 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +38 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +9 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +173 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +6 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +27 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +4 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +106 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +6 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +2 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +17 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +107 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +13 -32
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +62 -73
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +33 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +215 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +9 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +15 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +21 -12
  150. package/src/act.ts +68 -0
  151. package/src/bound-queries.ts +36 -0
  152. package/src/build-queries.ts +185 -0
  153. package/src/cleanup-registry.ts +14 -0
  154. package/src/config.ts +28 -59
  155. package/src/editable.ts +30 -0
  156. package/src/errors.ts +130 -0
  157. package/src/fire-event.ts +11 -38
  158. package/src/frame-sync.ts +34 -0
  159. package/src/index.ts +60 -18
  160. package/src/matchers.ts +200 -0
  161. package/src/pretty-widget.ts +99 -138
  162. package/src/queries.ts +321 -425
  163. package/src/register-test-hooks.ts +16 -0
  164. package/src/render-hook.tsx +31 -44
  165. package/src/render.tsx +161 -149
  166. package/src/role-helpers.ts +47 -67
  167. package/src/screen.ts +21 -114
  168. package/src/screenshot.ts +104 -55
  169. package/src/suggestions.ts +87 -0
  170. package/src/traversal.ts +32 -18
  171. package/src/types.ts +115 -176
  172. package/src/user-event/adjustment.ts +49 -0
  173. package/src/user-event/click.ts +70 -0
  174. package/src/user-event/controller.ts +44 -0
  175. package/src/user-event/dispatch.ts +23 -0
  176. package/src/user-event/event-wrapper.ts +54 -0
  177. package/src/user-event/gesture.ts +154 -0
  178. package/src/user-event/index.ts +74 -0
  179. package/src/user-event/keyboard.ts +204 -0
  180. package/src/user-event/pointer.ts +34 -0
  181. package/src/user-event/selection.ts +122 -0
  182. package/src/user-event/state.ts +6 -0
  183. package/src/user-event/text.ts +137 -0
  184. package/src/wait-for.ts +79 -80
  185. package/src/widget-accessible-properties.ts +244 -0
  186. package/src/within.ts +26 -27
  187. package/dist/bind-queries.d.ts +0 -14
  188. package/dist/bind-queries.d.ts.map +0 -1
  189. package/dist/bind-queries.js +0 -29
  190. package/dist/bind-queries.js.map +0 -1
  191. package/dist/error-builder.d.ts +0 -28
  192. package/dist/error-builder.d.ts.map +0 -1
  193. package/dist/error-builder.js +0 -81
  194. package/dist/error-builder.js.map +0 -1
  195. package/dist/timing.d.ts +0 -18
  196. package/dist/timing.d.ts.map +0 -1
  197. package/dist/timing.js +0 -18
  198. package/dist/timing.js.map +0 -1
  199. package/dist/user-event.d.ts +0 -138
  200. package/dist/user-event.d.ts.map +0 -1
  201. package/dist/user-event.js +0 -411
  202. package/dist/user-event.js.map +0 -1
  203. package/dist/widget-text.d.ts +0 -69
  204. package/dist/widget-text.d.ts.map +0 -1
  205. package/dist/widget-text.js +0 -177
  206. package/dist/widget-text.js.map +0 -1
  207. package/dist/widget.d.ts +0 -3
  208. package/dist/widget.d.ts.map +0 -1
  209. package/dist/widget.js +0 -13
  210. package/dist/widget.js.map +0 -1
  211. package/src/bind-queries.ts +0 -52
  212. package/src/error-builder.ts +0 -102
  213. package/src/timing.ts +0 -17
  214. package/src/user-event.ts +0 -492
  215. package/src/widget-text.ts +0 -202
  216. package/src/widget.ts +0 -15
@@ -1,45 +1,43 @@
1
- import { createRef } from "@gtkx/ffi";
2
- import * as Gsk from "@gtkx/ffi/gsk";
3
- import * as Gtk from "@gtkx/ffi/gtk";
4
- import { tick } from "./timing.js";
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import * as Gsk from "@gtkx/gi/gsk";
5
+ import * as Gtk from "@gtkx/gi/gtk";
5
6
  import { waitFor } from "./wait-for.js";
6
7
  const bytesToBase64 = (bytes) => {
7
8
  return Buffer.from(bytes).toString("base64");
8
9
  };
9
- const DEFAULT_SCREENSHOT_TIMEOUT = 100;
10
10
  const DEFAULT_SCREENSHOT_INTERVAL = 10;
11
- const captureSnapshot = (widget) => {
12
- const paintable = new Gtk.WidgetPaintable(widget);
11
+ const describeWidgetState = (widget) => `realized=${widget.getRealized()} mapped=${widget.getMapped()} visible=${widget.getVisible()}`;
12
+ const captureSnapshot = (widget, scale) => {
13
+ const paintable = new Gtk.WidgetPaintable({ widget });
13
14
  const width = paintable.getIntrinsicWidth();
14
15
  const height = paintable.getIntrinsicHeight();
15
16
  if (width <= 0 || height <= 0) {
16
- throw new Error("Widget has no size - ensure it is realized and visible");
17
+ throw new Error(`Widget has no size: ensure it is realized and visible (${describeWidgetState(widget)})`);
17
18
  }
18
19
  const snapshot = new Gtk.Snapshot();
20
+ snapshot.scale(scale, scale);
19
21
  paintable.snapshot(snapshot, width, height);
20
22
  const renderNode = snapshot.toNode();
21
23
  if (!renderNode) {
22
- throw new Error("Widget produced no render content");
24
+ throw new Error(`Widget produced no render content (${describeWidgetState(widget)})`);
23
25
  }
24
26
  const display = widget.getDisplay();
25
- if (!display) {
26
- throw new Error("Widget has no display - ensure it is realized");
27
- }
28
27
  const renderer = new Gsk.CairoRenderer();
29
28
  renderer.realizeForDisplay(display);
30
29
  try {
31
- const texture = renderer.renderTexture(renderNode);
30
+ const texture = renderer.renderTexture(renderNode, null);
32
31
  const pngBytes = texture.saveToPngBytes();
33
- const sizeRef = createRef(0);
34
- const data = pngBytes.getData(sizeRef);
32
+ const data = pngBytes.getData();
35
33
  if (!data) {
36
34
  throw new Error("Failed to serialize screenshot to PNG");
37
35
  }
38
36
  return {
39
37
  data: bytesToBase64(data),
40
38
  mimeType: "image/png",
41
- width,
42
- height,
39
+ width: Math.round(width * scale),
40
+ height: Math.round(height * scale),
43
41
  };
44
42
  }
45
43
  finally {
@@ -47,41 +45,84 @@ const captureSnapshot = (widget) => {
47
45
  }
48
46
  };
49
47
  /**
50
- * Captures a screenshot of a GTK widget as a PNG image.
51
- *
52
- * This function will retry multiple times if the widget hasn't finished
53
- * rendering, waiting for GTK to complete its paint cycle.
54
- *
55
- * @param widget - The widget to capture (typically a Window)
56
- * @param options - Optional timeout and interval configuration
57
- * @returns Screenshot result containing base64-encoded PNG data and dimensions
58
- * @throws Error if widget has no size, is not realized, or rendering fails after timeout
48
+ * Captures a PNG snapshot of a widget, retrying until it has a paintable size.
59
49
  *
60
- * @example
61
- * ```tsx
62
- * import { render, screenshot } from "@gtkx/testing";
63
- * import * as Gtk from "@gtkx/ffi/gtk";
64
- *
65
- * const { container } = await render(<MyApp />);
66
- * const window = container.getWindows()[0];
67
- * const result = await screenshot(window);
68
- * console.log(result.mimeType); // "image/png"
69
- * ```
50
+ * @param widget The widget to render to an image.
51
+ * @param options Optional scale, timeout, and retry interval.
52
+ * @returns The base64-encoded PNG data along with its mime type and dimensions.
70
53
  */
71
54
  export const screenshot = async (widget, options) => {
72
- await tick();
73
- return waitFor(() => captureSnapshot(widget), {
74
- timeout: options?.timeout ?? DEFAULT_SCREENSHOT_TIMEOUT,
55
+ const scale = options?.scale ?? 1;
56
+ if (!Number.isFinite(scale) || scale <= 0) {
57
+ throw new Error(`Screenshot scale must be a positive number, got ${scale}`);
58
+ }
59
+ return waitFor(() => captureSnapshot(widget, scale), {
60
+ timeout: options?.timeout,
75
61
  interval: options?.interval ?? DEFAULT_SCREENSHOT_INTERVAL,
76
- onTimeout: (error) => {
77
- const paintable = new Gtk.WidgetPaintable(widget);
78
- const width = paintable.getIntrinsicWidth();
79
- const height = paintable.getIntrinsicHeight();
80
- if (width <= 0 || height <= 0) {
81
- return new Error("Widget has no size - ensure it is realized and visible");
82
- }
83
- return new Error(`Widget produced no render content after waiting for paint cycle. ${error.message}`);
84
- },
85
62
  });
86
63
  };
64
+ const resolveWindow = (selector) => {
65
+ const windows = Gtk.Window.listToplevels();
66
+ if (windows.length === 0) {
67
+ throw new Error("No windows available for screenshot");
68
+ }
69
+ if (selector === undefined) {
70
+ const [first] = windows;
71
+ if (!(first instanceof Gtk.Window)) {
72
+ throw new TypeError("First toplevel is not a Window");
73
+ }
74
+ return first;
75
+ }
76
+ if (typeof selector === "number") {
77
+ const indexed = windows[selector];
78
+ if (!(indexed instanceof Gtk.Window)) {
79
+ throw new TypeError(`Window at index ${selector} not found`);
80
+ }
81
+ return indexed;
82
+ }
83
+ const isRegex = selector instanceof RegExp;
84
+ const found = windows.find((w) => {
85
+ if (!(w instanceof Gtk.Window))
86
+ return false;
87
+ const title = w.getTitle() ?? "";
88
+ return isRegex ? selector.test(title) : title.includes(selector);
89
+ });
90
+ if (!found) {
91
+ const pattern = isRegex ? selector.toString() : `"${selector}"`;
92
+ throw new Error(`No window found with title matching ${pattern}`);
93
+ }
94
+ return found;
95
+ };
96
+ const saveScreenshotToTempFile = (result) => {
97
+ const dir = join(tmpdir(), "gtkx-screenshots");
98
+ if (!existsSync(dir)) {
99
+ mkdirSync(dir, { recursive: true });
100
+ }
101
+ const filepath = join(dir, `${Date.now()}-screenshot.png`);
102
+ writeFileSync(filepath, Buffer.from(result.data, "base64"));
103
+ return filepath;
104
+ };
105
+ /**
106
+ * Prints a clickable file URL for a saved screenshot to the console.
107
+ *
108
+ * @param filepath Absolute path of the saved screenshot file.
109
+ */
110
+ export const logScreenshotPath = (filepath) => {
111
+ console.log(`Screenshot saved: file://${filepath}`);
112
+ };
113
+ /**
114
+ * Captures a screenshot of a toplevel window, writes it to a temporary file,
115
+ * logs its path, and returns the image data.
116
+ *
117
+ * @param selector Chooses the window by index, title substring, or regular
118
+ * expression; defaults to the first toplevel.
119
+ * @param options Optional scale, timeout, and retry interval.
120
+ * @returns The base64-encoded PNG data along with its mime type and dimensions.
121
+ */
122
+ export const captureAndSaveScreenshot = async (selector, options) => {
123
+ const target = resolveWindow(selector);
124
+ const result = await screenshot(target, options);
125
+ logScreenshotPath(saveScreenshotToTempFile(result));
126
+ return result;
127
+ };
87
128
  //# sourceMappingURL=screenshot.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport * as Gsk from \"@gtkx/gi/gsk\";\nimport * as Gtk from \"@gtkx/gi/gtk\";\nimport type { ScreenshotOptions, ScreenshotResult, WindowSelector } from \"./types.js\";\nimport { waitFor } from \"./wait-for.js\";\n\nconst bytesToBase64 = (bytes: number[]): string => {\n return Buffer.from(bytes).toString(\"base64\");\n};\n\nconst DEFAULT_SCREENSHOT_INTERVAL = 10;\n\nconst describeWidgetState = (widget: Gtk.Widget): string =>\n `realized=${widget.getRealized()} mapped=${widget.getMapped()} visible=${widget.getVisible()}`;\n\nconst captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult => {\n const paintable = new Gtk.WidgetPaintable({ widget });\n const width = paintable.getIntrinsicWidth();\n const height = paintable.getIntrinsicHeight();\n\n if (width <= 0 || height <= 0) {\n throw new Error(`Widget has no size: ensure it is realized and visible (${describeWidgetState(widget)})`);\n }\n\n const snapshot = new Gtk.Snapshot();\n snapshot.scale(scale, scale);\n paintable.snapshot(snapshot, width, height);\n const renderNode = snapshot.toNode();\n\n if (!renderNode) {\n throw new Error(`Widget produced no render content (${describeWidgetState(widget)})`);\n }\n\n const display = widget.getDisplay();\n\n const renderer = new Gsk.CairoRenderer();\n renderer.realizeForDisplay(display);\n\n try {\n const texture = renderer.renderTexture(renderNode, null);\n const pngBytes = texture.saveToPngBytes();\n const data = pngBytes.getData();\n\n if (!data) {\n throw new Error(\"Failed to serialize screenshot to PNG\");\n }\n\n return {\n data: bytesToBase64(data),\n mimeType: \"image/png\",\n width: Math.round(width * scale),\n height: Math.round(height * scale),\n };\n } finally {\n renderer.unrealize();\n }\n};\n\n/**\n * Captures a PNG snapshot of a widget, retrying until it has a paintable size.\n *\n * @param widget The widget to render to an image.\n * @param options Optional scale, timeout, and retry interval.\n * @returns The base64-encoded PNG data along with its mime type and dimensions.\n */\nexport const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions): Promise<ScreenshotResult> => {\n const scale = options?.scale ?? 1;\n\n if (!Number.isFinite(scale) || scale <= 0) {\n throw new Error(`Screenshot scale must be a positive number, got ${scale}`);\n }\n\n return waitFor(() => captureSnapshot(widget, scale), {\n timeout: options?.timeout,\n interval: options?.interval ?? DEFAULT_SCREENSHOT_INTERVAL,\n });\n};\n\nconst resolveWindow = (selector?: WindowSelector): Gtk.Window => {\n const windows = Gtk.Window.listToplevels();\n\n if (windows.length === 0) {\n throw new Error(\"No windows available for screenshot\");\n }\n\n if (selector === undefined) {\n const [first] = windows;\n if (!(first instanceof Gtk.Window)) {\n throw new TypeError(\"First toplevel is not a Window\");\n }\n return first;\n }\n\n if (typeof selector === \"number\") {\n const indexed = windows[selector];\n if (!(indexed instanceof Gtk.Window)) {\n throw new TypeError(`Window at index ${selector} not found`);\n }\n return indexed;\n }\n\n const isRegex = selector instanceof RegExp;\n const found = windows.find((w): w is Gtk.Window => {\n if (!(w instanceof Gtk.Window)) return false;\n const title = w.getTitle() ?? \"\";\n return isRegex ? selector.test(title) : title.includes(selector);\n });\n\n if (!found) {\n const pattern = isRegex ? selector.toString() : `\"${selector}\"`;\n throw new Error(`No window found with title matching ${pattern}`);\n }\n return found;\n};\n\nconst saveScreenshotToTempFile = (result: ScreenshotResult): string => {\n const dir = join(tmpdir(), \"gtkx-screenshots\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filepath = join(dir, `${Date.now()}-screenshot.png`);\n writeFileSync(filepath, Buffer.from(result.data, \"base64\"));\n return filepath;\n};\n\n/**\n * Prints a clickable file URL for a saved screenshot to the console.\n *\n * @param filepath Absolute path of the saved screenshot file.\n */\nexport const logScreenshotPath = (filepath: string): void => {\n console.log(`Screenshot saved: file://${filepath}`);\n};\n\n/**\n * Captures a screenshot of a toplevel window, writes it to a temporary file,\n * logs its path, and returns the image data.\n *\n * @param selector Chooses the window by index, title substring, or regular\n * expression; defaults to the first toplevel.\n * @param options Optional scale, timeout, and retry interval.\n * @returns The base64-encoded PNG data along with its mime type and dimensions.\n */\nexport const captureAndSaveScreenshot = async (\n selector?: WindowSelector,\n options?: ScreenshotOptions,\n): Promise<ScreenshotResult> => {\n const target = resolveWindow(selector);\n const result = await screenshot(target, options);\n logScreenshotPath(saveScreenshotToTempFile(result));\n return result;\n};\n"]}
@@ -0,0 +1,22 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ /** The query variant a suggestion targets. */
3
+ export type Variant = "get" | "getAll" | "query" | "queryAll" | "find" | "findAll";
4
+ /** The query family a suggestion targets. */
5
+ export type Method = "Role" | "LabelText" | "PlaceholderText" | "Text" | "DisplayValue" | "Name";
6
+ /** A suggested query for reaching a widget, including its family, variant, method name, and a `toString` that renders the full call. */
7
+ export type Suggestion = {
8
+ queryName: Method;
9
+ /** The full query function name, such as `getByRole`. */
10
+ queryMethod: string;
11
+ variant: Variant;
12
+ toString: () => string;
13
+ };
14
+ /**
15
+ * Computes the recommended query for reaching the given widget, preferring role, then label text, placeholder text, text, display value, and name.
16
+ * @param widget Widget to build a suggestion for.
17
+ * @param variant Query variant the suggestion should use.
18
+ * @param method Restrict the suggestion to a specific query family instead of choosing by priority.
19
+ * @returns The best available suggestion, or undefined when no query applies.
20
+ */
21
+ export declare const getSuggestedQuery: (widget: Gtk.Widget, variant?: Variant, method?: Method) => Suggestion | undefined;
22
+ //# sourceMappingURL=suggestions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggestions.d.ts","sourceRoot":"","sources":["../src/suggestions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,cAAc,CAAC;AAYpC,8CAA8C;AAC9C,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;AAEnF,6CAA6C;AAC7C,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,iBAAiB,GAAG,MAAM,GAAG,cAAc,GAAG,MAAM,CAAC;AAEjG,wIAAwI;AACxI,MAAM,MAAM,UAAU,GAAG;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,MAAM,CAAC;CAC1B,CAAC;AA6BF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,WAClB,GAAG,CAAC,MAAM,YACT,OAAO,WACP,MAAM,KAChB,UAAU,GAAG,SAqBf,CAAC"}
@@ -0,0 +1,56 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { formatRole } from "./role-helpers.js";
3
+ import { getWidgetAccessibleName, getWidgetDisplayValue, getWidgetLabelledByText, getWidgetLabelText, getWidgetName, getWidgetOwnLabel, getWidgetPlaceholderText, } from "./widget-accessible-properties.js";
4
+ const makeSuggestion = (queryName, variant, argsText) => {
5
+ const queryMethod = `${variant}By${queryName}`;
6
+ return {
7
+ queryName,
8
+ queryMethod,
9
+ variant,
10
+ toString: () => `${queryMethod}(${argsText})`,
11
+ };
12
+ };
13
+ const roleSuggestion = (widget, variant) => {
14
+ const role = widget.getAccessibleRole();
15
+ if (role === Gtk.AccessibleRole.NONE || role === Gtk.AccessibleRole.GENERIC)
16
+ return undefined;
17
+ const roleText = `Gtk.AccessibleRole.${formatRole(role).toUpperCase()}`;
18
+ const name = getWidgetAccessibleName(widget);
19
+ if (name === null) {
20
+ return makeSuggestion("Role", variant, roleText);
21
+ }
22
+ return makeSuggestion("Role", variant, `${roleText}, { name: '${name}' }`);
23
+ };
24
+ const textSuggestion = (queryName, variant, value) => {
25
+ if (value === null)
26
+ return undefined;
27
+ return makeSuggestion(queryName, variant, `'${value}'`);
28
+ };
29
+ /**
30
+ * Computes the recommended query for reaching the given widget, preferring role, then label text, placeholder text, text, display value, and name.
31
+ * @param widget Widget to build a suggestion for.
32
+ * @param variant Query variant the suggestion should use.
33
+ * @param method Restrict the suggestion to a specific query family instead of choosing by priority.
34
+ * @returns The best available suggestion, or undefined when no query applies.
35
+ */
36
+ export const getSuggestedQuery = (widget, variant = "get", method) => {
37
+ const builders = {
38
+ Role: () => roleSuggestion(widget, variant),
39
+ LabelText: () => textSuggestion("LabelText", variant, getWidgetOwnLabel(widget) ?? getWidgetLabelledByText(widget)),
40
+ PlaceholderText: () => textSuggestion("PlaceholderText", variant, getWidgetPlaceholderText(widget)),
41
+ Text: () => textSuggestion("Text", variant, getWidgetLabelText(widget)),
42
+ DisplayValue: () => textSuggestion("DisplayValue", variant, getWidgetDisplayValue(widget)),
43
+ Name: () => textSuggestion("Name", variant, getWidgetName(widget)),
44
+ };
45
+ if (method) {
46
+ return builders[method]();
47
+ }
48
+ const priority = ["Role", "LabelText", "PlaceholderText", "Text", "DisplayValue", "Name"];
49
+ for (const candidate of priority) {
50
+ const suggestion = builders[candidate]();
51
+ if (suggestion)
52
+ return suggestion;
53
+ }
54
+ return undefined;
55
+ };
56
+ //# sourceMappingURL=suggestions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggestions.js","sourceRoot":"","sources":["../src/suggestions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EACH,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,wBAAwB,GAC3B,MAAM,mCAAmC,CAAC;AAiB3C,MAAM,cAAc,GAAG,CAAC,SAAiB,EAAE,OAAgB,EAAE,QAAgB,EAAc,EAAE;IACzF,MAAM,WAAW,GAAG,GAAG,OAAO,KAAK,SAAS,EAAE,CAAC;IAC/C,OAAO;QACH,SAAS;QACT,WAAW;QACX,OAAO;QACP,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,WAAW,IAAI,QAAQ,GAAG;KAChD,CAAC;AACN,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,MAAkB,EAAE,OAAgB,EAA0B,EAAE;IACpF,MAAM,IAAI,GAAG,MAAM,CAAC,iBAAiB,EAAE,CAAC;IACxC,IAAI,IAAI,KAAK,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,IAAI,KAAK,GAAG,CAAC,cAAc,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAE9F,MAAM,QAAQ,GAAG,sBAAsB,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;IACxE,MAAM,IAAI,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAChB,OAAO,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,cAAc,IAAI,KAAK,CAAC,CAAC;AAC/E,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,SAAiB,EAAE,OAAgB,EAAE,KAAoB,EAA0B,EAAE;IACzG,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACrC,OAAO,cAAc,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC;AAC5D,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC7B,MAAkB,EAClB,OAAO,GAAY,KAAK,EACxB,MAAe,EACO,EAAE;IACxB,MAAM,QAAQ,GAAiD;QAC3D,IAAI,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC;QAC3C,SAAS,EAAE,GAAG,EAAE,CACZ,cAAc,CAAC,WAAW,EAAE,OAAO,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,uBAAuB,CAAC,MAAM,CAAC,CAAC;QACtG,eAAe,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,iBAAiB,EAAE,OAAO,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;QACnG,IAAI,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACvE,YAAY,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,cAAc,EAAE,OAAO,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;QAC1F,IAAI,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;KACrE,CAAC;IAEF,IAAI,MAAM,EAAE,CAAC;QACT,OAAO,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9B,CAAC;IAED,MAAM,QAAQ,GAAa,CAAC,MAAM,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC;IACpG,KAAK,MAAM,SAAS,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,IAAI,UAAU;YAAE,OAAO,UAAU,CAAC;IACtC,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC,CAAC","sourcesContent":["import * as Gtk from \"@gtkx/gi/gtk\";\nimport { formatRole } from \"./role-helpers.js\";\nimport {\n getWidgetAccessibleName,\n getWidgetDisplayValue,\n getWidgetLabelledByText,\n getWidgetLabelText,\n getWidgetName,\n getWidgetOwnLabel,\n getWidgetPlaceholderText,\n} from \"./widget-accessible-properties.js\";\n\n/** The query variant a suggestion targets. */\nexport type Variant = \"get\" | \"getAll\" | \"query\" | \"queryAll\" | \"find\" | \"findAll\";\n\n/** The query family a suggestion targets. */\nexport type Method = \"Role\" | \"LabelText\" | \"PlaceholderText\" | \"Text\" | \"DisplayValue\" | \"Name\";\n\n/** A suggested query for reaching a widget, including its family, variant, method name, and a `toString` that renders the full call. */\nexport type Suggestion = {\n queryName: Method;\n /** The full query function name, such as `getByRole`. */\n queryMethod: string;\n variant: Variant;\n toString: () => string;\n};\n\nconst makeSuggestion = (queryName: Method, variant: Variant, argsText: string): Suggestion => {\n const queryMethod = `${variant}By${queryName}`;\n return {\n queryName,\n queryMethod,\n variant,\n toString: () => `${queryMethod}(${argsText})`,\n };\n};\n\nconst roleSuggestion = (widget: Gtk.Widget, variant: Variant): Suggestion | undefined => {\n const role = widget.getAccessibleRole();\n if (role === Gtk.AccessibleRole.NONE || role === Gtk.AccessibleRole.GENERIC) return undefined;\n\n const roleText = `Gtk.AccessibleRole.${formatRole(role).toUpperCase()}`;\n const name = getWidgetAccessibleName(widget);\n if (name === null) {\n return makeSuggestion(\"Role\", variant, roleText);\n }\n return makeSuggestion(\"Role\", variant, `${roleText}, { name: '${name}' }`);\n};\n\nconst textSuggestion = (queryName: Method, variant: Variant, value: string | null): Suggestion | undefined => {\n if (value === null) return undefined;\n return makeSuggestion(queryName, variant, `'${value}'`);\n};\n\n/**\n * Computes the recommended query for reaching the given widget, preferring role, then label text, placeholder text, text, display value, and name.\n * @param widget Widget to build a suggestion for.\n * @param variant Query variant the suggestion should use.\n * @param method Restrict the suggestion to a specific query family instead of choosing by priority.\n * @returns The best available suggestion, or undefined when no query applies.\n */\nexport const getSuggestedQuery = (\n widget: Gtk.Widget,\n variant: Variant = \"get\",\n method?: Method,\n): Suggestion | undefined => {\n const builders: Record<Method, () => Suggestion | undefined> = {\n Role: () => roleSuggestion(widget, variant),\n LabelText: () =>\n textSuggestion(\"LabelText\", variant, getWidgetOwnLabel(widget) ?? getWidgetLabelledByText(widget)),\n PlaceholderText: () => textSuggestion(\"PlaceholderText\", variant, getWidgetPlaceholderText(widget)),\n Text: () => textSuggestion(\"Text\", variant, getWidgetLabelText(widget)),\n DisplayValue: () => textSuggestion(\"DisplayValue\", variant, getWidgetDisplayValue(widget)),\n Name: () => textSuggestion(\"Name\", variant, getWidgetName(widget)),\n };\n\n if (method) {\n return builders[method]();\n }\n\n const priority: Method[] = [\"Role\", \"LabelText\", \"PlaceholderText\", \"Text\", \"DisplayValue\", \"Name\"];\n for (const candidate of priority) {\n const suggestion = builders[candidate]();\n if (suggestion) return suggestion;\n }\n return undefined;\n};\n"]}
@@ -1,13 +1,15 @@
1
- import * as Gtk from "@gtkx/ffi/gtk";
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ export declare const TOPLEVELS: unique symbol;
3
+ type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
2
4
  /**
3
- * Root element for scoping queries.
4
- *
5
- * When a `Gtk.Application` is provided, queries search across all toplevel
6
- * windows. When a `Gtk.Widget` is provided, queries are scoped to that
7
- * widget's subtree.
5
+ * A scope that queries and traversal can run against: a widget, an event
6
+ * controller, a layout manager, a list item or header, an application, or the
7
+ * sentinel representing all current toplevel windows.
8
8
  */
9
- export type Container = Gtk.Application | Gtk.Widget;
10
- export declare const isApplication: (container: Container) => container is Gtk.Application;
9
+ export type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;
10
+ export declare const descendants: (widget: Gtk.Widget) => Generator<Gtk.Widget>;
11
+ export declare const roots: (container: Container) => Generator<Gtk.Widget>;
11
12
  export declare const traverse: (container: Container) => Generator<Gtk.Widget>;
12
13
  export declare const findAll: (container: Container, predicate: (node: Gtk.Widget) => boolean) => Gtk.Widget[];
14
+ export {};
13
15
  //# sourceMappingURL=traversal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"traversal.d.ts","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,eAAe,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC;AAErD,eAAO,MAAM,aAAa,GAAI,WAAW,SAAS,KAAG,SAAS,IAAI,GAAG,CAAC,WACK,CAAC;AAmB5E,eAAO,MAAM,QAAQ,GAAc,WAAW,SAAS,KAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAM7E,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,WAAW,SAAS,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,OAAO,KAAG,GAAG,CAAC,MAAM,EASlG,CAAC"}
1
+ {"version":3,"file":"traversal.d.ts","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,cAAc,CAAC;AAEpC,eAAO,MAAM,SAAS,EAAE,OAAO,MAAiC,CAAC;AAEjE,KAAK,cAAc,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,eAAe,GAAG,GAAG,CAAC,aAAa,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC;AAE3G;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,GAAG,CAAC,WAAW,GAAG,OAAO,SAAS,CAAC;AAc5E,eAAO,MAAM,WAAW,WAAsB,GAAG,CAAC,MAAM,KAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAI9E,CAAC;AAUF,eAAO,MAAM,KAAK,cAAyB,SAAS,KAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAO1E,CAAC;AAEF,eAAO,MAAM,QAAQ,cAAyB,SAAS,KAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAI7E,CAAC;AAEF,eAAO,MAAM,OAAO,cAAe,SAAS,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,OAAO,KAAG,GAAG,CAAC,MAAM,EAQlG,CAAC"}
package/dist/traversal.js CHANGED
@@ -1,5 +1,6 @@
1
- import * as Gtk from "@gtkx/ffi/gtk";
2
- export const isApplication = (container) => "getWindows" in container && typeof container.getWindows === "function";
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ export const TOPLEVELS = Symbol("gtkx.toplevels");
3
+ const isApplication = (container) => container instanceof Gtk.Application;
3
4
  const traverseWidgetTree = function* (root) {
4
5
  yield root;
5
6
  let child = root.getFirstChild();
@@ -8,25 +9,39 @@ const traverseWidgetTree = function* (root) {
8
9
  child = child.getNextSibling();
9
10
  }
10
11
  };
11
- const traverseWindows = function* () {
12
- const windows = Gtk.Window.listToplevels();
13
- for (const window of windows) {
14
- yield* traverseWidgetTree(window);
12
+ export const descendants = function* (widget) {
13
+ const tree = traverseWidgetTree(widget);
14
+ tree.next();
15
+ yield* tree;
16
+ };
17
+ const resolveRoot = (container) => {
18
+ if (container instanceof Gtk.Widget)
19
+ return container;
20
+ if (container instanceof Gtk.EventController)
21
+ return container.getWidget();
22
+ if (container instanceof Gtk.LayoutManager)
23
+ return container.getWidget();
24
+ if (container instanceof Gtk.ListItem || container instanceof Gtk.ListHeader)
25
+ return container.getChild();
26
+ return null;
27
+ };
28
+ export const roots = function* (container) {
29
+ if (container === TOPLEVELS || isApplication(container)) {
30
+ yield* Gtk.Window.listToplevels();
31
+ return;
15
32
  }
33
+ const root = resolveRoot(container);
34
+ if (root)
35
+ yield root;
16
36
  };
17
37
  export const traverse = function* (container) {
18
- if (isApplication(container)) {
19
- yield* traverseWindows();
20
- }
21
- else {
22
- yield* traverseWidgetTree(container);
38
+ for (const root of roots(container)) {
39
+ yield* traverseWidgetTree(root);
23
40
  }
24
41
  };
25
42
  export const findAll = (container, predicate) => {
26
43
  const results = [];
27
44
  for (const node of traverse(container)) {
28
- if (node.getAccessibleRole?.() === Gtk.AccessibleRole.LABEL)
29
- continue;
30
45
  if (predicate(node)) {
31
46
  results.push(node);
32
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"traversal.js","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,eAAe,CAAC;AAWrC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAAoB,EAAgC,EAAE,CAChF,YAAY,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU,CAAC;AAE5E,MAAM,kBAAkB,GAAG,QAAQ,CAAC,EAAE,IAAgB;IAClD,MAAM,IAAI,CAAC;IAEX,IAAI,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;IACjC,OAAO,KAAK,EAAE,CAAC;QACX,KAAK,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACjC,KAAK,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC;IACnC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,QAAQ,CAAC;IAC7B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;IAC3C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3B,KAAK,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,SAAoB;IACnD,IAAI,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,KAAK,CAAC,CAAC,eAAe,EAAE,CAAC;IAC7B,CAAC;SAAM,CAAC;QACJ,KAAK,CAAC,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAoB,EAAE,SAAwC,EAAgB,EAAE;IACpG,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,iBAAiB,EAAE,EAAE,KAAK,GAAG,CAAC,cAAc,CAAC,KAAK;YAAE,SAAS;QACtE,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC"}
1
+ {"version":3,"file":"traversal.js","sourceRoot":"","sources":["../src/traversal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,cAAc,CAAC;AAEpC,MAAM,CAAC,MAAM,SAAS,GAAkB,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAWjE,MAAM,aAAa,GAAG,CAAC,SAAoB,EAAgC,EAAE,CAAC,SAAS,YAAY,GAAG,CAAC,WAAW,CAAC;AAEnH,MAAM,kBAAkB,GAAG,QAAQ,CAAC,EAAE,IAAgB;IAClD,MAAM,IAAI,CAAC;IAEX,IAAI,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;IACjC,OAAO,KAAK,EAAE,CAAC;QACX,KAAK,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACjC,KAAK,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC;IACnC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC,EAAE,MAAkB;IACpD,MAAM,IAAI,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,IAAI,EAAE,CAAC;IACZ,KAAK,CAAC,CAAC,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,SAAyB,EAAqB,EAAE;IACjE,IAAI,SAAS,YAAY,GAAG,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACtD,IAAI,SAAS,YAAY,GAAG,CAAC,eAAe;QAAE,OAAO,SAAS,CAAC,SAAS,EAAE,CAAC;IAC3E,IAAI,SAAS,YAAY,GAAG,CAAC,aAAa;QAAE,OAAO,SAAS,CAAC,SAAS,EAAE,CAAC;IACzE,IAAI,SAAS,YAAY,GAAG,CAAC,QAAQ,IAAI,SAAS,YAAY,GAAG,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC,QAAQ,EAAE,CAAC;IAC1G,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,QAAQ,CAAC,EAAE,SAAoB;IAChD,IAAI,SAAS,KAAK,SAAS,IAAI,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;QACtD,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QAClC,OAAO;IACX,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,IAAI;QAAE,MAAM,IAAI,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,SAAoB;IACnD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAClC,KAAK,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAoB,EAAE,SAAwC,EAAgB,EAAE;IACpG,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACrC,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC","sourcesContent":["import * as Gtk from \"@gtkx/gi/gtk\";\n\nexport const TOPLEVELS: unique symbol = Symbol(\"gtkx.toplevels\");\n\ntype QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;\n\n/**\n * A scope that queries and traversal can run against: a widget, an event\n * controller, a layout manager, a list item or header, an application, or the\n * sentinel representing all current toplevel windows.\n */\nexport type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;\n\nconst isApplication = (container: Container): container is Gtk.Application => container instanceof Gtk.Application;\n\nconst traverseWidgetTree = function* (root: Gtk.Widget): Generator<Gtk.Widget> {\n yield root;\n\n let child = root.getFirstChild();\n while (child) {\n yield* traverseWidgetTree(child);\n child = child.getNextSibling();\n }\n};\n\nexport const descendants = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {\n const tree = traverseWidgetTree(widget);\n tree.next();\n yield* tree;\n};\n\nconst resolveRoot = (container: QueryContainer): Gtk.Widget | null => {\n if (container instanceof Gtk.Widget) return container;\n if (container instanceof Gtk.EventController) return container.getWidget();\n if (container instanceof Gtk.LayoutManager) return container.getWidget();\n if (container instanceof Gtk.ListItem || container instanceof Gtk.ListHeader) return container.getChild();\n return null;\n};\n\nexport const roots = function* (container: Container): Generator<Gtk.Widget> {\n if (container === TOPLEVELS || isApplication(container)) {\n yield* Gtk.Window.listToplevels();\n return;\n }\n const root = resolveRoot(container);\n if (root) yield root;\n};\n\nexport const traverse = function* (container: Container): Generator<Gtk.Widget> {\n for (const root of roots(container)) {\n yield* traverseWidgetTree(root);\n }\n};\n\nexport const findAll = (container: Container, predicate: (node: Gtk.Widget) => boolean): Gtk.Widget[] => {\n const results: Gtk.Widget[] = [];\n for (const node of traverse(container)) {\n if (predicate(node)) {\n results.push(node);\n }\n }\n return results;\n};\n"]}