@zenalexa/unicli 0.217.0 → 0.217.3

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 (341) hide show
  1. package/AGENTS.md +7 -6
  2. package/README.md +59 -19
  3. package/README.zh-CN.md +36 -15
  4. package/crates/unicli-atspi/Cargo.toml +47 -0
  5. package/crates/unicli-atspi/README.md +6 -0
  6. package/crates/unicli-atspi/src/errors.rs +213 -0
  7. package/crates/unicli-atspi/src/input.rs +1004 -0
  8. package/crates/unicli-atspi/src/invoke.rs +1132 -0
  9. package/crates/unicli-atspi/src/main.rs +130 -0
  10. package/crates/unicli-atspi/src/refs.rs +24 -0
  11. package/crates/unicli-atspi/src/screenshot.rs +756 -0
  12. package/crates/unicli-atspi/src/tree.rs +2319 -0
  13. package/crates/unicli-shared/Cargo.toml +13 -0
  14. package/crates/unicli-shared/src/lib.rs +77 -0
  15. package/crates/unicli-uia/Cargo.toml +29 -0
  16. package/crates/unicli-uia/README.md +6 -0
  17. package/crates/unicli-uia/src/errors.rs +179 -0
  18. package/crates/unicli-uia/src/input.rs +790 -0
  19. package/crates/unicli-uia/src/invoke.rs +977 -0
  20. package/crates/unicli-uia/src/main.rs +130 -0
  21. package/crates/unicli-uia/src/refs.rs +24 -0
  22. package/crates/unicli-uia/src/screenshot.rs +685 -0
  23. package/crates/unicli-uia/src/tree.rs +2135 -0
  24. package/dist/adapters/macos/actions.d.ts +9 -0
  25. package/dist/adapters/macos/actions.d.ts.map +1 -0
  26. package/dist/adapters/macos/actions.js +55 -0
  27. package/dist/adapters/macos/actions.js.map +1 -0
  28. package/dist/browser/bridge.d.ts +2 -0
  29. package/dist/browser/bridge.d.ts.map +1 -1
  30. package/dist/browser/bridge.js +39 -0
  31. package/dist/browser/bridge.js.map +1 -1
  32. package/dist/browser/cdp-client.d.ts +2 -0
  33. package/dist/browser/cdp-client.d.ts.map +1 -1
  34. package/dist/browser/cdp-client.js +7 -0
  35. package/dist/browser/cdp-client.js.map +1 -1
  36. package/dist/browser/page.d.ts +2 -0
  37. package/dist/browser/page.d.ts.map +1 -1
  38. package/dist/browser/page.js +35 -0
  39. package/dist/browser/page.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +13 -1
  42. package/dist/cli.js.map +1 -1
  43. package/dist/commands/approvals.d.ts +3 -0
  44. package/dist/commands/approvals.d.ts.map +1 -0
  45. package/dist/commands/approvals.js +123 -0
  46. package/dist/commands/approvals.js.map +1 -0
  47. package/dist/commands/browser-operator-runtime.d.ts.map +1 -1
  48. package/dist/commands/browser-operator-runtime.js +1 -0
  49. package/dist/commands/browser-operator-runtime.js.map +1 -1
  50. package/dist/commands/browser-operator.d.ts.map +1 -1
  51. package/dist/commands/browser-operator.js +84 -12
  52. package/dist/commands/browser-operator.js.map +1 -1
  53. package/dist/commands/compute.d.ts +3 -0
  54. package/dist/commands/compute.d.ts.map +1 -0
  55. package/dist/commands/compute.js +324 -0
  56. package/dist/commands/compute.js.map +1 -0
  57. package/dist/commands/dispatch.d.ts.map +1 -1
  58. package/dist/commands/dispatch.js +10 -4
  59. package/dist/commands/dispatch.js.map +1 -1
  60. package/dist/commands/doctor-compute.d.ts +38 -0
  61. package/dist/commands/doctor-compute.d.ts.map +1 -0
  62. package/dist/commands/doctor-compute.js +376 -0
  63. package/dist/commands/doctor-compute.js.map +1 -0
  64. package/dist/commands/lint.d.ts.map +1 -1
  65. package/dist/commands/lint.js +69 -1
  66. package/dist/commands/lint.js.map +1 -1
  67. package/dist/commands/mcp.d.ts.map +1 -1
  68. package/dist/commands/mcp.js +4 -0
  69. package/dist/commands/mcp.js.map +1 -1
  70. package/dist/commands/runs.d.ts +3 -0
  71. package/dist/commands/runs.d.ts.map +1 -0
  72. package/dist/commands/runs.js +367 -0
  73. package/dist/commands/runs.js.map +1 -0
  74. package/dist/core/envelope.d.ts +8 -0
  75. package/dist/core/envelope.d.ts.map +1 -1
  76. package/dist/core/envelope.js +1 -0
  77. package/dist/core/envelope.js.map +1 -1
  78. package/dist/discovery/aliases.d.ts.map +1 -1
  79. package/dist/discovery/aliases.js +15 -0
  80. package/dist/discovery/aliases.js.map +1 -1
  81. package/dist/discovery/loader.d.ts.map +1 -1
  82. package/dist/discovery/loader.js +9 -0
  83. package/dist/discovery/loader.js.map +1 -1
  84. package/dist/discovery/macos-dynamic.d.ts +58 -0
  85. package/dist/discovery/macos-dynamic.d.ts.map +1 -0
  86. package/dist/discovery/macos-dynamic.js +429 -0
  87. package/dist/discovery/macos-dynamic.js.map +1 -0
  88. package/dist/discovery/search.d.ts.map +1 -1
  89. package/dist/discovery/search.js +152 -3
  90. package/dist/discovery/search.js.map +1 -1
  91. package/dist/electron-apps.d.ts +1 -0
  92. package/dist/electron-apps.d.ts.map +1 -1
  93. package/dist/electron-apps.js +1 -0
  94. package/dist/electron-apps.js.map +1 -1
  95. package/dist/engine/approval-store.d.ts +43 -0
  96. package/dist/engine/approval-store.d.ts.map +1 -0
  97. package/dist/engine/approval-store.js +193 -0
  98. package/dist/engine/approval-store.js.map +1 -0
  99. package/dist/engine/browser/action-evidence.d.ts +2 -0
  100. package/dist/engine/browser/action-evidence.d.ts.map +1 -1
  101. package/dist/engine/browser/action-evidence.js +35 -1
  102. package/dist/engine/browser/action-evidence.js.map +1 -1
  103. package/dist/engine/browser/evidence.d.ts +22 -0
  104. package/dist/engine/browser/evidence.d.ts.map +1 -1
  105. package/dist/engine/browser/evidence.js +72 -0
  106. package/dist/engine/browser/evidence.js.map +1 -1
  107. package/dist/engine/browser/session-lease.d.ts +53 -0
  108. package/dist/engine/browser/session-lease.d.ts.map +1 -0
  109. package/dist/engine/browser/session-lease.js +100 -0
  110. package/dist/engine/browser/session-lease.js.map +1 -0
  111. package/dist/engine/browser/session-lock.d.ts +17 -0
  112. package/dist/engine/browser/session-lock.d.ts.map +1 -0
  113. package/dist/engine/browser/session-lock.js +114 -0
  114. package/dist/engine/browser/session-lock.js.map +1 -0
  115. package/dist/engine/browser/session-runtime.d.ts +10 -0
  116. package/dist/engine/browser/session-runtime.d.ts.map +1 -0
  117. package/dist/engine/browser/session-runtime.js +87 -0
  118. package/dist/engine/browser/session-runtime.js.map +1 -0
  119. package/dist/engine/capability-policy.d.ts +13 -2
  120. package/dist/engine/capability-policy.d.ts.map +1 -1
  121. package/dist/engine/capability-policy.js +113 -3
  122. package/dist/engine/capability-policy.js.map +1 -1
  123. package/dist/engine/executor.d.ts +8 -3
  124. package/dist/engine/executor.d.ts.map +1 -1
  125. package/dist/engine/executor.js +9 -2
  126. package/dist/engine/executor.js.map +1 -1
  127. package/dist/engine/kernel/execute.d.ts +1 -0
  128. package/dist/engine/kernel/execute.d.ts.map +1 -1
  129. package/dist/engine/kernel/execute.js +125 -3
  130. package/dist/engine/kernel/execute.js.map +1 -1
  131. package/dist/engine/kernel/types.d.ts +13 -0
  132. package/dist/engine/kernel/types.d.ts.map +1 -1
  133. package/dist/engine/operation-policy.d.ts +9 -1
  134. package/dist/engine/operation-policy.d.ts.map +1 -1
  135. package/dist/engine/operation-policy.js +6 -2
  136. package/dist/engine/operation-policy.js.map +1 -1
  137. package/dist/engine/permission-rules.d.ts +43 -0
  138. package/dist/engine/permission-rules.d.ts.map +1 -0
  139. package/dist/engine/permission-rules.js +401 -0
  140. package/dist/engine/permission-rules.js.map +1 -0
  141. package/dist/engine/permission-runtime.d.ts +11 -0
  142. package/dist/engine/permission-runtime.d.ts.map +1 -0
  143. package/dist/engine/permission-runtime.js +21 -0
  144. package/dist/engine/permission-runtime.js.map +1 -0
  145. package/dist/engine/repair/remedies.d.ts +4 -0
  146. package/dist/engine/repair/remedies.d.ts.map +1 -0
  147. package/dist/engine/repair/remedies.js +169 -0
  148. package/dist/engine/repair/remedies.js.map +1 -0
  149. package/dist/engine/runtime-resource-guard.d.ts +23 -0
  150. package/dist/engine/runtime-resource-guard.d.ts.map +1 -0
  151. package/dist/engine/runtime-resource-guard.js +85 -0
  152. package/dist/engine/runtime-resource-guard.js.map +1 -0
  153. package/dist/engine/session/args.d.ts +3 -0
  154. package/dist/engine/session/args.d.ts.map +1 -0
  155. package/dist/engine/session/args.js +17 -0
  156. package/dist/engine/session/args.js.map +1 -0
  157. package/dist/engine/session/compare.d.ts +92 -0
  158. package/dist/engine/session/compare.d.ts.map +1 -0
  159. package/dist/engine/session/compare.js +324 -0
  160. package/dist/engine/session/compare.js.map +1 -0
  161. package/dist/engine/session/environment.d.ts +4 -0
  162. package/dist/engine/session/environment.d.ts.map +1 -0
  163. package/dist/engine/session/environment.js +25 -0
  164. package/dist/engine/session/environment.js.map +1 -0
  165. package/dist/engine/session/events.d.ts +2 -0
  166. package/dist/engine/session/events.d.ts.map +1 -1
  167. package/dist/engine/session/events.js +12 -0
  168. package/dist/engine/session/events.js.map +1 -1
  169. package/dist/engine/session/query.d.ts +47 -0
  170. package/dist/engine/session/query.d.ts.map +1 -0
  171. package/dist/engine/session/query.js +299 -0
  172. package/dist/engine/session/query.js.map +1 -0
  173. package/dist/engine/session/replay.d.ts +35 -0
  174. package/dist/engine/session/replay.d.ts.map +1 -0
  175. package/dist/engine/session/replay.js +144 -0
  176. package/dist/engine/session/replay.js.map +1 -0
  177. package/dist/engine/session/run-loop.d.ts.map +1 -1
  178. package/dist/engine/session/run-loop.js +62 -23
  179. package/dist/engine/session/run-loop.js.map +1 -1
  180. package/dist/engine/session/store.d.ts +7 -0
  181. package/dist/engine/session/store.d.ts.map +1 -1
  182. package/dist/engine/session/store.js +131 -1
  183. package/dist/engine/session/store.js.map +1 -1
  184. package/dist/engine/session/types.d.ts +3 -1
  185. package/dist/engine/session/types.d.ts.map +1 -1
  186. package/dist/engine/steps/compute.d.ts +41 -0
  187. package/dist/engine/steps/compute.d.ts.map +1 -0
  188. package/dist/engine/steps/compute.js +55 -0
  189. package/dist/engine/steps/compute.js.map +1 -0
  190. package/dist/engine/steps/desktop-ax.d.ts +8 -0
  191. package/dist/engine/steps/desktop-ax.d.ts.map +1 -1
  192. package/dist/engine/steps/desktop-ax.js +16 -0
  193. package/dist/engine/steps/desktop-ax.js.map +1 -1
  194. package/dist/engine/steps/desktop-sidecar.d.ts +49 -0
  195. package/dist/engine/steps/desktop-sidecar.d.ts.map +1 -0
  196. package/dist/engine/steps/desktop-sidecar.js +50 -0
  197. package/dist/engine/steps/desktop-sidecar.js.map +1 -0
  198. package/dist/engine/steps/download.d.ts +1 -1
  199. package/dist/engine/steps/download.d.ts.map +1 -1
  200. package/dist/engine/steps/download.js +24 -2
  201. package/dist/engine/steps/download.js.map +1 -1
  202. package/dist/engine/steps/exec.d.ts +1 -1
  203. package/dist/engine/steps/exec.d.ts.map +1 -1
  204. package/dist/engine/steps/exec.js +23 -7
  205. package/dist/engine/steps/exec.js.map +1 -1
  206. package/dist/engine/steps/fetch-text.d.ts +1 -1
  207. package/dist/engine/steps/fetch-text.d.ts.map +1 -1
  208. package/dist/engine/steps/fetch-text.js +12 -4
  209. package/dist/engine/steps/fetch-text.js.map +1 -1
  210. package/dist/engine/steps/fetch.d.ts +2 -1
  211. package/dist/engine/steps/fetch.d.ts.map +1 -1
  212. package/dist/engine/steps/fetch.js +29 -6
  213. package/dist/engine/steps/fetch.js.map +1 -1
  214. package/dist/engine/steps/index.d.ts +2 -0
  215. package/dist/engine/steps/index.d.ts.map +1 -1
  216. package/dist/engine/steps/index.js +2 -0
  217. package/dist/engine/steps/index.js.map +1 -1
  218. package/dist/engine/steps/navigate.d.ts +1 -1
  219. package/dist/engine/steps/navigate.d.ts.map +1 -1
  220. package/dist/engine/steps/navigate.js +29 -2
  221. package/dist/engine/steps/navigate.js.map +1 -1
  222. package/dist/fast-path.d.ts.map +1 -1
  223. package/dist/fast-path.js +96 -12
  224. package/dist/fast-path.js.map +1 -1
  225. package/dist/manifest-compact.txt +2 -2
  226. package/dist/manifest-search.json +1 -1
  227. package/dist/manifest.json +1024 -1
  228. package/dist/mcp/handler.d.ts +2 -2
  229. package/dist/mcp/handler.d.ts.map +1 -1
  230. package/dist/mcp/handler.js +59 -5
  231. package/dist/mcp/handler.js.map +1 -1
  232. package/dist/mcp/profiles/computer-use.d.ts +4 -0
  233. package/dist/mcp/profiles/computer-use.d.ts.map +1 -0
  234. package/dist/mcp/profiles/computer-use.js +305 -0
  235. package/dist/mcp/profiles/computer-use.js.map +1 -0
  236. package/dist/mcp/server.d.ts.map +1 -1
  237. package/dist/mcp/server.js +30 -6
  238. package/dist/mcp/server.js.map +1 -1
  239. package/dist/mcp/tools.d.ts +9 -0
  240. package/dist/mcp/tools.d.ts.map +1 -1
  241. package/dist/mcp/tools.js +20 -0
  242. package/dist/mcp/tools.js.map +1 -1
  243. package/dist/output/envelope.d.ts +6 -0
  244. package/dist/output/envelope.d.ts.map +1 -1
  245. package/dist/output/envelope.js.map +1 -1
  246. package/dist/output/error-map.d.ts.map +1 -1
  247. package/dist/output/error-map.js +4 -0
  248. package/dist/output/error-map.js.map +1 -1
  249. package/dist/registry.d.ts +1 -0
  250. package/dist/registry.d.ts.map +1 -1
  251. package/dist/registry.js +5 -0
  252. package/dist/registry.js.map +1 -1
  253. package/dist/transport/adapters/cdp-browser.d.ts +38 -2
  254. package/dist/transport/adapters/cdp-browser.d.ts.map +1 -1
  255. package/dist/transport/adapters/cdp-browser.js +349 -22
  256. package/dist/transport/adapters/cdp-browser.js.map +1 -1
  257. package/dist/transport/adapters/desktop-atspi.d.ts +23 -17
  258. package/dist/transport/adapters/desktop-atspi.d.ts.map +1 -1
  259. package/dist/transport/adapters/desktop-atspi.js +143 -32
  260. package/dist/transport/adapters/desktop-atspi.js.map +1 -1
  261. package/dist/transport/adapters/desktop-ax-helpers.d.ts +24 -0
  262. package/dist/transport/adapters/desktop-ax-helpers.d.ts.map +1 -0
  263. package/dist/transport/adapters/desktop-ax-helpers.js +190 -0
  264. package/dist/transport/adapters/desktop-ax-helpers.js.map +1 -0
  265. package/dist/transport/adapters/desktop-ax-swift.d.ts +13 -0
  266. package/dist/transport/adapters/desktop-ax-swift.d.ts.map +1 -1
  267. package/dist/transport/adapters/desktop-ax-swift.js +176 -2
  268. package/dist/transport/adapters/desktop-ax-swift.js.map +1 -1
  269. package/dist/transport/adapters/desktop-ax.d.ts +11 -2
  270. package/dist/transport/adapters/desktop-ax.d.ts.map +1 -1
  271. package/dist/transport/adapters/desktop-ax.js +131 -16
  272. package/dist/transport/adapters/desktop-ax.js.map +1 -1
  273. package/dist/transport/adapters/desktop-sidecar-errors.d.ts +3 -0
  274. package/dist/transport/adapters/desktop-sidecar-errors.d.ts.map +1 -0
  275. package/dist/transport/adapters/desktop-sidecar-errors.js +34 -0
  276. package/dist/transport/adapters/desktop-sidecar-errors.js.map +1 -0
  277. package/dist/transport/adapters/desktop-sidecar-snapshot.d.ts +10 -0
  278. package/dist/transport/adapters/desktop-sidecar-snapshot.d.ts.map +1 -0
  279. package/dist/transport/adapters/desktop-sidecar-snapshot.js +89 -0
  280. package/dist/transport/adapters/desktop-sidecar-snapshot.js.map +1 -0
  281. package/dist/transport/adapters/desktop-uia.d.ts +23 -17
  282. package/dist/transport/adapters/desktop-uia.d.ts.map +1 -1
  283. package/dist/transport/adapters/desktop-uia.js +142 -32
  284. package/dist/transport/adapters/desktop-uia.js.map +1 -1
  285. package/dist/transport/adapters/subprocess.d.ts +7 -0
  286. package/dist/transport/adapters/subprocess.d.ts.map +1 -1
  287. package/dist/transport/adapters/subprocess.js +64 -0
  288. package/dist/transport/adapters/subprocess.js.map +1 -1
  289. package/dist/transport/bus.d.ts +2 -0
  290. package/dist/transport/bus.d.ts.map +1 -1
  291. package/dist/transport/bus.js +7 -11
  292. package/dist/transport/bus.js.map +1 -1
  293. package/dist/transport/capability.d.ts.map +1 -1
  294. package/dist/transport/capability.js +123 -98
  295. package/dist/transport/capability.js.map +1 -1
  296. package/dist/transport/cascade.d.ts +5 -0
  297. package/dist/transport/cascade.d.ts.map +1 -0
  298. package/dist/transport/cascade.js +550 -0
  299. package/dist/transport/cascade.js.map +1 -0
  300. package/dist/transport/cdp-session.d.ts +11 -0
  301. package/dist/transport/cdp-session.d.ts.map +1 -0
  302. package/dist/transport/cdp-session.js +52 -0
  303. package/dist/transport/cdp-session.js.map +1 -0
  304. package/dist/transport/refs.d.ts +51 -0
  305. package/dist/transport/refs.d.ts.map +1 -0
  306. package/dist/transport/refs.js +135 -0
  307. package/dist/transport/refs.js.map +1 -0
  308. package/dist/transport/sidecar-binary.d.ts +18 -0
  309. package/dist/transport/sidecar-binary.d.ts.map +1 -0
  310. package/dist/transport/sidecar-binary.js +55 -0
  311. package/dist/transport/sidecar-binary.js.map +1 -0
  312. package/dist/transport/sidecar.d.ts +35 -0
  313. package/dist/transport/sidecar.d.ts.map +1 -0
  314. package/dist/transport/sidecar.js +134 -0
  315. package/dist/transport/sidecar.js.map +1 -0
  316. package/dist/transport/snapshot-encoder.d.ts +34 -0
  317. package/dist/transport/snapshot-encoder.d.ts.map +1 -0
  318. package/dist/transport/snapshot-encoder.js +139 -0
  319. package/dist/transport/snapshot-encoder.js.map +1 -0
  320. package/dist/transport/types.d.ts +6 -1
  321. package/dist/transport/types.d.ts.map +1 -1
  322. package/dist/types.d.ts +2 -0
  323. package/dist/types.d.ts.map +1 -1
  324. package/dist/types.js.map +1 -1
  325. package/docs/mcp/clients/claude-code.md +29 -0
  326. package/docs/mcp/clients/claude-desktop.md +47 -0
  327. package/docs/mcp/clients/codex.md +29 -0
  328. package/docs/mcp/clients/cursor.md +38 -0
  329. package/docs/mcp/clients/gemini-cli.md +38 -0
  330. package/docs/operate/compute.md +172 -0
  331. package/docs/operate/electron.md +87 -0
  332. package/docs/operate/focus-behavior.md +40 -0
  333. package/docs/operate/troubleshooting.md +379 -0
  334. package/package.json +29 -4
  335. package/src/adapters/juejin/hot.test.ts +25 -0
  336. package/src/adapters/juejin/hot.yaml +52 -0
  337. package/src/adapters/juejin/search.test.ts +27 -0
  338. package/src/adapters/juejin/search.yaml +58 -0
  339. package/src/adapters/leetcode/discuss-search.test.ts +29 -0
  340. package/src/adapters/leetcode/discuss-search.yaml +56 -0
  341. package/src/adapters/macos/actions.ts +63 -0
@@ -0,0 +1,685 @@
1
+ use serde_json::Value;
2
+ use unicli_shared::SidecarRequest;
3
+
4
+ use crate::errors::{backend_unavailable, HandlerResult, UiaError};
5
+ use crate::tree::{
6
+ enumerate_top_level_windows, resolve_descendant_element_ref, resolve_top_level_window_ref,
7
+ ElementBounds, ElementRecord, State, WindowRecord,
8
+ };
9
+
10
+ #[cfg(any(target_os = "windows", test))]
11
+ const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
12
+
13
+ pub fn handle(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
14
+ if !cfg!(target_os = "windows") {
15
+ return Err(backend_unavailable());
16
+ }
17
+
18
+ let windows = enumerate_top_level_windows()?;
19
+ if let Some(stable) = read_stable_ref(&request.params) {
20
+ if let Some((window, element, path)) = resolve_descendant_element_ref(&windows, stable) {
21
+ let bounds = require_descendant_bounds(element, stable)?;
22
+ let capture = capture_descendant_png(window, bounds)?;
23
+ return Ok(screenshot_response_for_descendant(
24
+ window, element, stable, &path, capture,
25
+ ));
26
+ }
27
+ }
28
+ let window = resolve_requested_window(&windows, &request.params)?;
29
+ let capture = capture_window_png(window)?;
30
+ Ok(serde_json::json!({
31
+ "base64": base64_encode(&capture.bytes),
32
+ "mime": "image/png",
33
+ "width": capture.width,
34
+ "height": capture.height,
35
+ "stable": stable_ref_for_window(&windows, window),
36
+ "hwnd": window.hwnd,
37
+ "pid": window.pid,
38
+ "title": window.title,
39
+ }))
40
+ }
41
+
42
+ fn read_stable_ref(params: &Value) -> Option<&str> {
43
+ params
44
+ .get("stable")
45
+ .or_else(|| params.get("ref"))
46
+ .and_then(Value::as_str)
47
+ .filter(|value| value.starts_with("desktop-uia:"))
48
+ }
49
+
50
+ fn resolve_requested_window<'a>(
51
+ windows: &'a [WindowRecord],
52
+ params: &Value,
53
+ ) -> Result<&'a WindowRecord, UiaError> {
54
+ if let Some(stable) = params
55
+ .get("stable")
56
+ .or_else(|| params.get("ref"))
57
+ .and_then(Value::as_str)
58
+ {
59
+ return resolve_top_level_window_ref(windows, stable)
60
+ .ok_or_else(|| UiaError::no_element(stable.to_string()));
61
+ }
62
+
63
+ windows
64
+ .iter()
65
+ .find(|window| window_matches_params(window, params))
66
+ .ok_or_else(|| UiaError::no_element("top-level window screenshot query"))
67
+ }
68
+
69
+ fn window_matches_params(window: &WindowRecord, params: &Value) -> bool {
70
+ let pid_filter = params
71
+ .get("pid")
72
+ .and_then(Value::as_u64)
73
+ .map(|pid| pid as u32);
74
+ let app_filter = params
75
+ .get("app")
76
+ .and_then(Value::as_str)
77
+ .map(|app| app.to_ascii_lowercase());
78
+
79
+ pid_filter.map_or(true, |pid| window.pid == pid)
80
+ && app_filter
81
+ .as_ref()
82
+ .map_or(true, |app| window.title.to_ascii_lowercase().contains(app))
83
+ }
84
+
85
+ fn stable_ref_for_window(windows: &[WindowRecord], target: &WindowRecord) -> String {
86
+ let index = windows
87
+ .iter()
88
+ .filter(|window| window.pid == target.pid)
89
+ .position(|window| window.hwnd == target.hwnd)
90
+ .unwrap_or(0);
91
+ format!("desktop-uia:pid-{}:Window[{index}]", target.pid)
92
+ }
93
+
94
+ fn screenshot_response_for_descendant(
95
+ window: &WindowRecord,
96
+ element: &ElementRecord,
97
+ stable: &str,
98
+ path: &str,
99
+ capture: CapturedPng,
100
+ ) -> serde_json::Value {
101
+ let mut target = descendant_target_node(element, path);
102
+ if let Some(bounds) = &element.bounds {
103
+ target["bounds"] = bounds_node(bounds);
104
+ }
105
+ serde_json::json!({
106
+ "captured": true,
107
+ "via": "descendant_bounds_screenshot",
108
+ "base64": base64_encode(&capture.bytes),
109
+ "mime": "image/png",
110
+ "width": capture.width,
111
+ "height": capture.height,
112
+ "stable": stable,
113
+ "hwnd": window.hwnd,
114
+ "pid": window.pid,
115
+ "title": window.title,
116
+ "target": target,
117
+ })
118
+ }
119
+
120
+ fn require_descendant_bounds<'a>(
121
+ element: &'a ElementRecord,
122
+ stable: &str,
123
+ ) -> Result<&'a ElementBounds, UiaError> {
124
+ element
125
+ .bounds
126
+ .as_ref()
127
+ .ok_or_else(|| UiaError::not_invokable(stable.to_string()))
128
+ }
129
+
130
+ fn descendant_target_node(element: &ElementRecord, path: &str) -> serde_json::Value {
131
+ let mut target = serde_json::json!({
132
+ "role": element.role,
133
+ "name": element.name,
134
+ "path": path,
135
+ });
136
+ if let Some(value) = &element.value {
137
+ target["value"] = serde_json::json!(value);
138
+ }
139
+ target
140
+ }
141
+
142
+ fn bounds_node(bounds: &ElementBounds) -> serde_json::Value {
143
+ serde_json::json!({
144
+ "x": bounds.x,
145
+ "y": bounds.y,
146
+ "width": bounds.width,
147
+ "height": bounds.height,
148
+ })
149
+ }
150
+
151
+ struct CapturedPng {
152
+ bytes: Vec<u8>,
153
+ width: u32,
154
+ height: u32,
155
+ }
156
+
157
+ #[cfg(target_os = "windows")]
158
+ fn capture_window_png(window: &WindowRecord) -> Result<CapturedPng, UiaError> {
159
+ let hwnd = parse_hwnd(&window.hwnd)?;
160
+ captured_png_from_bgra(capture_window_bgra(hwnd)?)
161
+ }
162
+
163
+ #[cfg(not(target_os = "windows"))]
164
+ fn capture_window_png(_window: &WindowRecord) -> Result<CapturedPng, UiaError> {
165
+ Err(backend_unavailable())
166
+ }
167
+
168
+ #[cfg(target_os = "windows")]
169
+ fn capture_descendant_png(
170
+ window: &WindowRecord,
171
+ bounds: &ElementBounds,
172
+ ) -> Result<CapturedPng, UiaError> {
173
+ let hwnd = parse_hwnd(&window.hwnd)?;
174
+ captured_png_from_bgra(crop_bgra_to_bounds(&capture_window_bgra(hwnd)?, bounds)?)
175
+ }
176
+
177
+ #[cfg(not(target_os = "windows"))]
178
+ fn capture_descendant_png(
179
+ _window: &WindowRecord,
180
+ _bounds: &ElementBounds,
181
+ ) -> Result<CapturedPng, UiaError> {
182
+ Err(backend_unavailable())
183
+ }
184
+
185
+ #[cfg(target_os = "windows")]
186
+ fn captured_png_from_bgra(bitmap: BgraImage) -> Result<CapturedPng, UiaError> {
187
+ let bytes = png_bytes_from_bgra(bitmap.width, bitmap.height, &bitmap.bgra)?;
188
+ Ok(CapturedPng {
189
+ bytes,
190
+ width: bitmap.width,
191
+ height: bitmap.height,
192
+ })
193
+ }
194
+
195
+ #[cfg(any(target_os = "windows", test))]
196
+ fn crop_bgra_to_bounds(image: &BgraImage, bounds: &ElementBounds) -> Result<BgraImage, UiaError> {
197
+ if bounds.width == 0 || bounds.height == 0 {
198
+ return Err(UiaError::invalid_input(
199
+ "descendant screenshot bounds are empty",
200
+ ));
201
+ }
202
+ let left = bounds.x - image.origin_x;
203
+ let top = bounds.y - image.origin_y;
204
+ if left < 0 || top < 0 {
205
+ return Err(UiaError::invalid_input(
206
+ "descendant screenshot bounds start outside captured window",
207
+ ));
208
+ }
209
+ let left = left as u32;
210
+ let top = top as u32;
211
+ if left + bounds.width > image.width || top + bounds.height > image.height {
212
+ return Err(UiaError::invalid_input(
213
+ "descendant screenshot bounds exceed captured window",
214
+ ));
215
+ }
216
+
217
+ let source_stride = image.width as usize * 4;
218
+ let row_bytes = bounds.width as usize * 4;
219
+ let mut bgra = Vec::with_capacity(row_bytes * bounds.height as usize);
220
+ for row in 0..bounds.height {
221
+ let start = ((top + row) as usize * source_stride) + (left as usize * 4);
222
+ bgra.extend_from_slice(&image.bgra[start..start + row_bytes]);
223
+ }
224
+ Ok(BgraImage {
225
+ bgra,
226
+ width: bounds.width,
227
+ height: bounds.height,
228
+ origin_x: bounds.x,
229
+ origin_y: bounds.y,
230
+ })
231
+ }
232
+
233
+ #[cfg(any(target_os = "windows", test))]
234
+ fn png_bytes_from_bgra(width: u32, height: u32, bgra: &[u8]) -> Result<Vec<u8>, UiaError> {
235
+ let expected_len = width as usize * height as usize * 4;
236
+ if bgra.len() != expected_len {
237
+ return Err(UiaError::invalid_input(format!(
238
+ "BGRA buffer length {} does not match {width}x{height}",
239
+ bgra.len()
240
+ )));
241
+ }
242
+
243
+ let stride = width as usize * 4;
244
+ let mut raw = Vec::with_capacity((stride + 1) * height as usize);
245
+ for row in bgra.chunks_exact(stride) {
246
+ raw.push(0);
247
+ for pixel in row.chunks_exact(4) {
248
+ raw.extend_from_slice(&[pixel[2], pixel[1], pixel[0], pixel[3]]);
249
+ }
250
+ }
251
+
252
+ let mut png = Vec::new();
253
+ png.extend_from_slice(PNG_SIGNATURE);
254
+ let mut ihdr = Vec::with_capacity(13);
255
+ ihdr.extend_from_slice(&width.to_be_bytes());
256
+ ihdr.extend_from_slice(&height.to_be_bytes());
257
+ ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
258
+ append_png_chunk(&mut png, b"IHDR", &ihdr);
259
+ append_png_chunk(&mut png, b"IDAT", &zlib_uncompressed(&raw));
260
+ append_png_chunk(&mut png, b"IEND", &[]);
261
+ Ok(png)
262
+ }
263
+
264
+ #[cfg(any(target_os = "windows", test))]
265
+ fn append_png_chunk(png: &mut Vec<u8>, name: &[u8; 4], data: &[u8]) {
266
+ png.extend_from_slice(&(data.len() as u32).to_be_bytes());
267
+ png.extend_from_slice(name);
268
+ png.extend_from_slice(data);
269
+ let mut crc_input = Vec::with_capacity(name.len() + data.len());
270
+ crc_input.extend_from_slice(name);
271
+ crc_input.extend_from_slice(data);
272
+ png.extend_from_slice(&crc32(&crc_input).to_be_bytes());
273
+ }
274
+
275
+ #[cfg(any(target_os = "windows", test))]
276
+ fn zlib_uncompressed(data: &[u8]) -> Vec<u8> {
277
+ let mut out = Vec::with_capacity(data.len() + 6 + (data.len() / 65_535) * 5);
278
+ out.extend_from_slice(&[0x78, 0x01]);
279
+ for (index, chunk) in data.chunks(65_535).enumerate() {
280
+ let final_block = (index + 1) * 65_535 >= data.len();
281
+ out.push(if final_block { 0x01 } else { 0x00 });
282
+ let len = chunk.len() as u16;
283
+ out.extend_from_slice(&len.to_le_bytes());
284
+ out.extend_from_slice(&(!len).to_le_bytes());
285
+ out.extend_from_slice(chunk);
286
+ }
287
+ out.extend_from_slice(&adler32(data).to_be_bytes());
288
+ out
289
+ }
290
+
291
+ #[cfg(any(target_os = "windows", test))]
292
+ fn adler32(data: &[u8]) -> u32 {
293
+ const MOD: u32 = 65_521;
294
+ let mut a = 1_u32;
295
+ let mut b = 0_u32;
296
+ for byte in data {
297
+ a = (a + u32::from(*byte)) % MOD;
298
+ b = (b + a) % MOD;
299
+ }
300
+ (b << 16) | a
301
+ }
302
+
303
+ #[cfg(any(target_os = "windows", test))]
304
+ fn crc32(data: &[u8]) -> u32 {
305
+ let mut crc = 0xffff_ffff_u32;
306
+ for byte in data {
307
+ crc ^= u32::from(*byte);
308
+ for _ in 0..8 {
309
+ let mask = 0_u32.wrapping_sub(crc & 1);
310
+ crc = (crc >> 1) ^ (0xedb8_8320 & mask);
311
+ }
312
+ }
313
+ !crc
314
+ }
315
+
316
+ fn base64_encode(bytes: &[u8]) -> String {
317
+ const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
318
+ let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
319
+ for chunk in bytes.chunks(3) {
320
+ let b0 = chunk[0];
321
+ let b1 = *chunk.get(1).unwrap_or(&0);
322
+ let b2 = *chunk.get(2).unwrap_or(&0);
323
+ let n = (u32::from(b0) << 16) | (u32::from(b1) << 8) | u32::from(b2);
324
+ out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
325
+ out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
326
+ out.push(if chunk.len() > 1 {
327
+ TABLE[((n >> 6) & 0x3f) as usize] as char
328
+ } else {
329
+ '='
330
+ });
331
+ out.push(if chunk.len() > 2 {
332
+ TABLE[(n & 0x3f) as usize] as char
333
+ } else {
334
+ '='
335
+ });
336
+ }
337
+ out
338
+ }
339
+
340
+ #[cfg(any(target_os = "windows", test))]
341
+ struct BgraImage {
342
+ bgra: Vec<u8>,
343
+ width: u32,
344
+ height: u32,
345
+ origin_x: i32,
346
+ origin_y: i32,
347
+ }
348
+
349
+ #[cfg(target_os = "windows")]
350
+ fn capture_window_bgra(hwnd: isize) -> Result<BgraImage, UiaError> {
351
+ let mut rect = win32::Rect::default();
352
+ let ok = unsafe { win32::GetWindowRect(hwnd, &mut rect) };
353
+ if ok == 0 {
354
+ return Err(UiaError::permission(format!(
355
+ "GetWindowRect failed: {}",
356
+ std::io::Error::last_os_error()
357
+ )));
358
+ }
359
+ let width = (rect.right - rect.left).max(0) as u32;
360
+ let height = (rect.bottom - rect.top).max(0) as u32;
361
+ if width == 0 || height == 0 {
362
+ return Err(UiaError::invalid_input("target window has empty bounds"));
363
+ }
364
+
365
+ let window_dc = unsafe { win32::GetWindowDC(hwnd) };
366
+ if window_dc == 0 {
367
+ return Err(UiaError::permission(format!(
368
+ "GetWindowDC failed: {}",
369
+ std::io::Error::last_os_error()
370
+ )));
371
+ }
372
+
373
+ let result = capture_from_window_dc(window_dc, width, height, rect.left, rect.top);
374
+ unsafe {
375
+ win32::ReleaseDC(hwnd, window_dc);
376
+ }
377
+ result
378
+ }
379
+
380
+ #[cfg(target_os = "windows")]
381
+ fn capture_from_window_dc(
382
+ window_dc: isize,
383
+ width: u32,
384
+ height: u32,
385
+ origin_x: i32,
386
+ origin_y: i32,
387
+ ) -> Result<BgraImage, UiaError> {
388
+ let memory_dc = unsafe { win32::CreateCompatibleDC(window_dc) };
389
+ if memory_dc == 0 {
390
+ return Err(UiaError::permission(format!(
391
+ "CreateCompatibleDC failed: {}",
392
+ std::io::Error::last_os_error()
393
+ )));
394
+ }
395
+
396
+ let bitmap = unsafe { win32::CreateCompatibleBitmap(window_dc, width as i32, height as i32) };
397
+ if bitmap == 0 {
398
+ unsafe {
399
+ win32::DeleteDC(memory_dc);
400
+ }
401
+ return Err(UiaError::permission(format!(
402
+ "CreateCompatibleBitmap failed: {}",
403
+ std::io::Error::last_os_error()
404
+ )));
405
+ }
406
+
407
+ let old_object = unsafe { win32::SelectObject(memory_dc, bitmap) };
408
+ let copied = unsafe {
409
+ win32::BitBlt(
410
+ memory_dc,
411
+ 0,
412
+ 0,
413
+ width as i32,
414
+ height as i32,
415
+ window_dc,
416
+ 0,
417
+ 0,
418
+ win32::SRCCOPY,
419
+ )
420
+ };
421
+ let mut bgra = vec![0_u8; width as usize * height as usize * 4];
422
+ let mut info = win32::BitmapInfo::top_down_bgra(width, height);
423
+ let scan_lines = if copied != 0 {
424
+ unsafe {
425
+ win32::GetDIBits(
426
+ memory_dc,
427
+ bitmap,
428
+ 0,
429
+ height,
430
+ bgra.as_mut_ptr().cast(),
431
+ &mut info,
432
+ win32::DIB_RGB_COLORS,
433
+ )
434
+ }
435
+ } else {
436
+ 0
437
+ };
438
+
439
+ unsafe {
440
+ if old_object != 0 {
441
+ win32::SelectObject(memory_dc, old_object);
442
+ }
443
+ win32::DeleteObject(bitmap);
444
+ win32::DeleteDC(memory_dc);
445
+ }
446
+
447
+ if copied == 0 {
448
+ return Err(UiaError::permission(format!(
449
+ "BitBlt failed: {}",
450
+ std::io::Error::last_os_error()
451
+ )));
452
+ }
453
+ if scan_lines == 0 {
454
+ return Err(UiaError::permission(format!(
455
+ "GetDIBits failed: {}",
456
+ std::io::Error::last_os_error()
457
+ )));
458
+ }
459
+
460
+ Ok(BgraImage {
461
+ bgra,
462
+ width,
463
+ height,
464
+ origin_x,
465
+ origin_y,
466
+ })
467
+ }
468
+
469
+ #[cfg(target_os = "windows")]
470
+ fn parse_hwnd(value: &str) -> Result<isize, UiaError> {
471
+ let raw = value.strip_prefix("0x").unwrap_or(value);
472
+ isize::from_str_radix(raw, 16)
473
+ .map_err(|_| UiaError::invalid_input(format!("invalid window handle {value}")))
474
+ }
475
+
476
+ #[cfg(target_os = "windows")]
477
+ mod win32 {
478
+ pub const SRCCOPY: u32 = 0x00cc_0020;
479
+ pub const DIB_RGB_COLORS: u32 = 0;
480
+ const BI_RGB: u32 = 0;
481
+
482
+ #[derive(Default)]
483
+ #[repr(C)]
484
+ pub struct Rect {
485
+ pub left: i32,
486
+ pub top: i32,
487
+ pub right: i32,
488
+ pub bottom: i32,
489
+ }
490
+
491
+ #[repr(C)]
492
+ pub struct BitmapInfo {
493
+ pub header: BitmapInfoHeader,
494
+ pub colors: [RgbQuad; 1],
495
+ }
496
+
497
+ impl BitmapInfo {
498
+ pub fn top_down_bgra(width: u32, height: u32) -> Self {
499
+ Self {
500
+ header: BitmapInfoHeader {
501
+ size: std::mem::size_of::<BitmapInfoHeader>() as u32,
502
+ width: width as i32,
503
+ height: -(height as i32),
504
+ planes: 1,
505
+ bit_count: 32,
506
+ compression: BI_RGB,
507
+ size_image: width * height * 4,
508
+ x_pels_per_meter: 0,
509
+ y_pels_per_meter: 0,
510
+ clr_used: 0,
511
+ clr_important: 0,
512
+ },
513
+ colors: [RgbQuad::default()],
514
+ }
515
+ }
516
+ }
517
+
518
+ #[repr(C)]
519
+ pub struct BitmapInfoHeader {
520
+ pub size: u32,
521
+ pub width: i32,
522
+ pub height: i32,
523
+ pub planes: u16,
524
+ pub bit_count: u16,
525
+ pub compression: u32,
526
+ pub size_image: u32,
527
+ pub x_pels_per_meter: i32,
528
+ pub y_pels_per_meter: i32,
529
+ pub clr_used: u32,
530
+ pub clr_important: u32,
531
+ }
532
+
533
+ #[derive(Clone, Copy, Default)]
534
+ #[repr(C)]
535
+ pub struct RgbQuad {
536
+ pub blue: u8,
537
+ pub green: u8,
538
+ pub red: u8,
539
+ pub reserved: u8,
540
+ }
541
+
542
+ #[link(name = "user32")]
543
+ extern "system" {
544
+ pub fn GetWindowRect(hwnd: isize, rect: *mut Rect) -> i32;
545
+ pub fn GetWindowDC(hwnd: isize) -> isize;
546
+ pub fn ReleaseDC(hwnd: isize, dc: isize) -> i32;
547
+ }
548
+
549
+ #[link(name = "gdi32")]
550
+ extern "system" {
551
+ pub fn CreateCompatibleDC(dc: isize) -> isize;
552
+ pub fn CreateCompatibleBitmap(dc: isize, width: i32, height: i32) -> isize;
553
+ pub fn SelectObject(dc: isize, object: isize) -> isize;
554
+ pub fn BitBlt(
555
+ dc: isize,
556
+ x: i32,
557
+ y: i32,
558
+ width: i32,
559
+ height: i32,
560
+ source_dc: isize,
561
+ source_x: i32,
562
+ source_y: i32,
563
+ raster_op: u32,
564
+ ) -> i32;
565
+ pub fn GetDIBits(
566
+ dc: isize,
567
+ bitmap: isize,
568
+ start_scan: u32,
569
+ scan_lines: u32,
570
+ bits: *mut std::ffi::c_void,
571
+ info: *mut BitmapInfo,
572
+ usage: u32,
573
+ ) -> i32;
574
+ pub fn DeleteObject(object: isize) -> i32;
575
+ pub fn DeleteDC(dc: isize) -> i32;
576
+ }
577
+ }
578
+
579
+ #[cfg(test)]
580
+ mod tests {
581
+ use super::*;
582
+ use crate::tree::{ElementBounds, ElementRecord, WindowRecord};
583
+
584
+ #[test]
585
+ fn png_encoder_wraps_bgra_pixels_as_rgba_png() {
586
+ let png = png_bytes_from_bgra(1, 1, &[0x33, 0x22, 0x11, 0xff]).expect("png");
587
+
588
+ assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
589
+ assert_eq!(&png[12..16], b"IHDR");
590
+ assert_eq!(&png[16..20], 1_u32.to_be_bytes());
591
+ assert_eq!(&png[20..24], 1_u32.to_be_bytes());
592
+ assert_eq!(base64_encode(&png[..4]), "iVBORw==");
593
+ }
594
+
595
+ #[test]
596
+ fn crops_bgra_to_descendant_bounds_relative_to_window_origin() {
597
+ let image = BgraImage {
598
+ bgra: vec![
599
+ 0, 0, 0, 255, 1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255, 5, 0, 0, 255,
600
+ 6, 0, 0, 255, 7, 0, 0, 255, 8, 0, 0, 255,
601
+ ],
602
+ width: 3,
603
+ height: 3,
604
+ origin_x: 10,
605
+ origin_y: 20,
606
+ };
607
+
608
+ let cropped = crop_bgra_to_bounds(
609
+ &image,
610
+ &ElementBounds {
611
+ x: 11,
612
+ y: 21,
613
+ width: 2,
614
+ height: 2,
615
+ },
616
+ )
617
+ .expect("cropped descendant image");
618
+
619
+ assert_eq!(cropped.width, 2);
620
+ assert_eq!(cropped.height, 2);
621
+ assert_eq!(
622
+ cropped.bgra,
623
+ vec![4, 0, 0, 255, 5, 0, 0, 255, 7, 0, 0, 255, 8, 0, 0, 255],
624
+ );
625
+ }
626
+
627
+ #[test]
628
+ fn screenshot_response_includes_descendant_target_metadata() {
629
+ let response = screenshot_response_for_descendant(
630
+ &WindowRecord {
631
+ hwnd: "0x2a".into(),
632
+ pid: 42,
633
+ title: "Calculator".into(),
634
+ children: vec![],
635
+ },
636
+ &ElementRecord {
637
+ role: "Button".into(),
638
+ name: "Seven".into(),
639
+ value: None,
640
+ bounds: Some(ElementBounds {
641
+ x: 20,
642
+ y: 30,
643
+ width: 40,
644
+ height: 50,
645
+ }),
646
+ states: vec!["enabled".into()],
647
+ children: vec![],
648
+ },
649
+ "desktop-uia:pid-42:Window[0]/Button[1]",
650
+ "Window[0]/Button[1]",
651
+ CapturedPng {
652
+ bytes: vec![137, 80, 78, 71],
653
+ width: 40,
654
+ height: 50,
655
+ },
656
+ );
657
+
658
+ assert_eq!(
659
+ response,
660
+ serde_json::json!({
661
+ "captured": true,
662
+ "via": "descendant_bounds_screenshot",
663
+ "base64": "iVBORw==",
664
+ "mime": "image/png",
665
+ "width": 40,
666
+ "height": 50,
667
+ "stable": "desktop-uia:pid-42:Window[0]/Button[1]",
668
+ "hwnd": "0x2a",
669
+ "pid": 42,
670
+ "title": "Calculator",
671
+ "target": {
672
+ "role": "Button",
673
+ "name": "Seven",
674
+ "path": "Window[0]/Button[1]",
675
+ "bounds": {
676
+ "x": 20,
677
+ "y": 30,
678
+ "width": 40,
679
+ "height": 50,
680
+ },
681
+ },
682
+ }),
683
+ );
684
+ }
685
+ }