@zenalexa/unicli 0.216.3 → 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 (379) hide show
  1. package/AGENTS.md +7 -6
  2. package/README.md +67 -19
  3. package/README.zh-CN.md +44 -16
  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/_electron/desktop-shared.d.ts.map +1 -1
  25. package/dist/adapters/_electron/desktop-shared.js +13 -0
  26. package/dist/adapters/_electron/desktop-shared.js.map +1 -1
  27. package/dist/adapters/_electron/shared.d.ts +1 -0
  28. package/dist/adapters/_electron/shared.d.ts.map +1 -1
  29. package/dist/adapters/_electron/shared.js +49 -2
  30. package/dist/adapters/_electron/shared.js.map +1 -1
  31. package/dist/adapters/macos/actions.d.ts +9 -0
  32. package/dist/adapters/macos/actions.d.ts.map +1 -0
  33. package/dist/adapters/macos/actions.js +55 -0
  34. package/dist/adapters/macos/actions.js.map +1 -0
  35. package/dist/browser/bridge.d.ts +5 -1
  36. package/dist/browser/bridge.d.ts.map +1 -1
  37. package/dist/browser/bridge.js +86 -23
  38. package/dist/browser/bridge.js.map +1 -1
  39. package/dist/browser/cdp-client.d.ts +2 -0
  40. package/dist/browser/cdp-client.d.ts.map +1 -1
  41. package/dist/browser/cdp-client.js +7 -0
  42. package/dist/browser/cdp-client.js.map +1 -1
  43. package/dist/browser/page.d.ts +2 -0
  44. package/dist/browser/page.d.ts.map +1 -1
  45. package/dist/browser/page.js +35 -0
  46. package/dist/browser/page.js.map +1 -1
  47. package/dist/cli.d.ts.map +1 -1
  48. package/dist/cli.js +17 -2
  49. package/dist/cli.js.map +1 -1
  50. package/dist/commands/approvals.d.ts +3 -0
  51. package/dist/commands/approvals.d.ts.map +1 -0
  52. package/dist/commands/approvals.js +123 -0
  53. package/dist/commands/approvals.js.map +1 -0
  54. package/dist/commands/browser-operator-runtime.d.ts.map +1 -1
  55. package/dist/commands/browser-operator-runtime.js +5 -2
  56. package/dist/commands/browser-operator-runtime.js.map +1 -1
  57. package/dist/commands/browser-operator.d.ts.map +1 -1
  58. package/dist/commands/browser-operator.js +182 -38
  59. package/dist/commands/browser-operator.js.map +1 -1
  60. package/dist/commands/compute.d.ts +3 -0
  61. package/dist/commands/compute.d.ts.map +1 -0
  62. package/dist/commands/compute.js +324 -0
  63. package/dist/commands/compute.js.map +1 -0
  64. package/dist/commands/describe.d.ts.map +1 -1
  65. package/dist/commands/describe.js +20 -1
  66. package/dist/commands/describe.js.map +1 -1
  67. package/dist/commands/dispatch.d.ts +3 -0
  68. package/dist/commands/dispatch.d.ts.map +1 -1
  69. package/dist/commands/dispatch.js +76 -4
  70. package/dist/commands/dispatch.js.map +1 -1
  71. package/dist/commands/doctor-compute.d.ts +38 -0
  72. package/dist/commands/doctor-compute.d.ts.map +1 -0
  73. package/dist/commands/doctor-compute.js +376 -0
  74. package/dist/commands/doctor-compute.js.map +1 -0
  75. package/dist/commands/lint.d.ts.map +1 -1
  76. package/dist/commands/lint.js +69 -1
  77. package/dist/commands/lint.js.map +1 -1
  78. package/dist/commands/mcp.d.ts.map +1 -1
  79. package/dist/commands/mcp.js +4 -0
  80. package/dist/commands/mcp.js.map +1 -1
  81. package/dist/commands/runs.d.ts +3 -0
  82. package/dist/commands/runs.d.ts.map +1 -0
  83. package/dist/commands/runs.js +367 -0
  84. package/dist/commands/runs.js.map +1 -0
  85. package/dist/core/envelope.d.ts +8 -0
  86. package/dist/core/envelope.d.ts.map +1 -1
  87. package/dist/core/envelope.js +1 -0
  88. package/dist/core/envelope.js.map +1 -1
  89. package/dist/core/schema-v2.d.ts +2 -2
  90. package/dist/discovery/aliases.d.ts.map +1 -1
  91. package/dist/discovery/aliases.js +15 -0
  92. package/dist/discovery/aliases.js.map +1 -1
  93. package/dist/discovery/loader.d.ts.map +1 -1
  94. package/dist/discovery/loader.js +11 -0
  95. package/dist/discovery/loader.js.map +1 -1
  96. package/dist/discovery/macos-dynamic.d.ts +58 -0
  97. package/dist/discovery/macos-dynamic.d.ts.map +1 -0
  98. package/dist/discovery/macos-dynamic.js +429 -0
  99. package/dist/discovery/macos-dynamic.js.map +1 -0
  100. package/dist/discovery/search.d.ts.map +1 -1
  101. package/dist/discovery/search.js +152 -3
  102. package/dist/discovery/search.js.map +1 -1
  103. package/dist/electron-apps.d.ts +1 -0
  104. package/dist/electron-apps.d.ts.map +1 -1
  105. package/dist/electron-apps.js +1 -0
  106. package/dist/electron-apps.js.map +1 -1
  107. package/dist/engine/approval-store.d.ts +43 -0
  108. package/dist/engine/approval-store.d.ts.map +1 -0
  109. package/dist/engine/approval-store.js +193 -0
  110. package/dist/engine/approval-store.js.map +1 -0
  111. package/dist/engine/browser/action-evidence.d.ts +30 -0
  112. package/dist/engine/browser/action-evidence.d.ts.map +1 -0
  113. package/dist/engine/browser/action-evidence.js +354 -0
  114. package/dist/engine/browser/action-evidence.js.map +1 -0
  115. package/dist/engine/browser/evidence.d.ts +85 -0
  116. package/dist/engine/browser/evidence.d.ts.map +1 -0
  117. package/dist/engine/browser/evidence.js +373 -0
  118. package/dist/engine/browser/evidence.js.map +1 -0
  119. package/dist/engine/browser/session-lease.d.ts +53 -0
  120. package/dist/engine/browser/session-lease.d.ts.map +1 -0
  121. package/dist/engine/browser/session-lease.js +100 -0
  122. package/dist/engine/browser/session-lease.js.map +1 -0
  123. package/dist/engine/browser/session-lock.d.ts +17 -0
  124. package/dist/engine/browser/session-lock.d.ts.map +1 -0
  125. package/dist/engine/browser/session-lock.js +114 -0
  126. package/dist/engine/browser/session-lock.js.map +1 -0
  127. package/dist/engine/browser/session-runtime.d.ts +10 -0
  128. package/dist/engine/browser/session-runtime.d.ts.map +1 -0
  129. package/dist/engine/browser/session-runtime.js +87 -0
  130. package/dist/engine/browser/session-runtime.js.map +1 -0
  131. package/dist/engine/capability-policy.d.ts +50 -0
  132. package/dist/engine/capability-policy.d.ts.map +1 -0
  133. package/dist/engine/capability-policy.js +305 -0
  134. package/dist/engine/capability-policy.js.map +1 -0
  135. package/dist/engine/executor.d.ts +8 -3
  136. package/dist/engine/executor.d.ts.map +1 -1
  137. package/dist/engine/executor.js +9 -2
  138. package/dist/engine/executor.js.map +1 -1
  139. package/dist/engine/kernel/execute.d.ts +5 -1
  140. package/dist/engine/kernel/execute.d.ts.map +1 -1
  141. package/dist/engine/kernel/execute.js +215 -11
  142. package/dist/engine/kernel/execute.js.map +1 -1
  143. package/dist/engine/kernel/types.d.ts +15 -0
  144. package/dist/engine/kernel/types.d.ts.map +1 -1
  145. package/dist/engine/operation-policy.d.ts +60 -0
  146. package/dist/engine/operation-policy.d.ts.map +1 -0
  147. package/dist/engine/operation-policy.js +364 -0
  148. package/dist/engine/operation-policy.js.map +1 -0
  149. package/dist/engine/permission-rules.d.ts +43 -0
  150. package/dist/engine/permission-rules.d.ts.map +1 -0
  151. package/dist/engine/permission-rules.js +401 -0
  152. package/dist/engine/permission-rules.js.map +1 -0
  153. package/dist/engine/permission-runtime.d.ts +11 -0
  154. package/dist/engine/permission-runtime.d.ts.map +1 -0
  155. package/dist/engine/permission-runtime.js +21 -0
  156. package/dist/engine/permission-runtime.js.map +1 -0
  157. package/dist/engine/repair/remedies.d.ts +4 -0
  158. package/dist/engine/repair/remedies.d.ts.map +1 -0
  159. package/dist/engine/repair/remedies.js +169 -0
  160. package/dist/engine/repair/remedies.js.map +1 -0
  161. package/dist/engine/runtime-resource-guard.d.ts +23 -0
  162. package/dist/engine/runtime-resource-guard.d.ts.map +1 -0
  163. package/dist/engine/runtime-resource-guard.js +85 -0
  164. package/dist/engine/runtime-resource-guard.js.map +1 -0
  165. package/dist/engine/session/args.d.ts +3 -0
  166. package/dist/engine/session/args.d.ts.map +1 -0
  167. package/dist/engine/session/args.js +17 -0
  168. package/dist/engine/session/args.js.map +1 -0
  169. package/dist/engine/session/compare.d.ts +92 -0
  170. package/dist/engine/session/compare.d.ts.map +1 -0
  171. package/dist/engine/session/compare.js +324 -0
  172. package/dist/engine/session/compare.js.map +1 -0
  173. package/dist/engine/session/environment.d.ts +4 -0
  174. package/dist/engine/session/environment.d.ts.map +1 -0
  175. package/dist/engine/session/environment.js +25 -0
  176. package/dist/engine/session/environment.js.map +1 -0
  177. package/dist/engine/session/events.d.ts +24 -0
  178. package/dist/engine/session/events.d.ts.map +1 -0
  179. package/dist/engine/session/events.js +78 -0
  180. package/dist/engine/session/events.js.map +1 -0
  181. package/dist/engine/session/query.d.ts +47 -0
  182. package/dist/engine/session/query.d.ts.map +1 -0
  183. package/dist/engine/session/query.js +299 -0
  184. package/dist/engine/session/query.js.map +1 -0
  185. package/dist/engine/session/replay.d.ts +35 -0
  186. package/dist/engine/session/replay.d.ts.map +1 -0
  187. package/dist/engine/session/replay.js +144 -0
  188. package/dist/engine/session/replay.js.map +1 -0
  189. package/dist/engine/session/run-loop.d.ts +11 -0
  190. package/dist/engine/session/run-loop.d.ts.map +1 -0
  191. package/dist/engine/session/run-loop.js +212 -0
  192. package/dist/engine/session/run-loop.js.map +1 -0
  193. package/dist/engine/session/store.d.ts +26 -0
  194. package/dist/engine/session/store.d.ts.map +1 -0
  195. package/dist/engine/session/store.js +214 -0
  196. package/dist/engine/session/store.js.map +1 -0
  197. package/dist/engine/session/types.d.ts +39 -0
  198. package/dist/engine/session/types.d.ts.map +1 -0
  199. package/dist/engine/session/types.js +2 -0
  200. package/dist/engine/session/types.js.map +1 -0
  201. package/dist/engine/steps/compute.d.ts +41 -0
  202. package/dist/engine/steps/compute.d.ts.map +1 -0
  203. package/dist/engine/steps/compute.js +55 -0
  204. package/dist/engine/steps/compute.js.map +1 -0
  205. package/dist/engine/steps/desktop-ax.d.ts +8 -0
  206. package/dist/engine/steps/desktop-ax.d.ts.map +1 -1
  207. package/dist/engine/steps/desktop-ax.js +16 -0
  208. package/dist/engine/steps/desktop-ax.js.map +1 -1
  209. package/dist/engine/steps/desktop-sidecar.d.ts +49 -0
  210. package/dist/engine/steps/desktop-sidecar.d.ts.map +1 -0
  211. package/dist/engine/steps/desktop-sidecar.js +50 -0
  212. package/dist/engine/steps/desktop-sidecar.js.map +1 -0
  213. package/dist/engine/steps/download.d.ts +1 -1
  214. package/dist/engine/steps/download.d.ts.map +1 -1
  215. package/dist/engine/steps/download.js +24 -2
  216. package/dist/engine/steps/download.js.map +1 -1
  217. package/dist/engine/steps/exec.d.ts +1 -1
  218. package/dist/engine/steps/exec.d.ts.map +1 -1
  219. package/dist/engine/steps/exec.js +23 -7
  220. package/dist/engine/steps/exec.js.map +1 -1
  221. package/dist/engine/steps/fetch-text.d.ts +2 -2
  222. package/dist/engine/steps/fetch-text.d.ts.map +1 -1
  223. package/dist/engine/steps/fetch-text.js +61 -19
  224. package/dist/engine/steps/fetch-text.js.map +1 -1
  225. package/dist/engine/steps/fetch.d.ts +3 -1
  226. package/dist/engine/steps/fetch.d.ts.map +1 -1
  227. package/dist/engine/steps/fetch.js +36 -7
  228. package/dist/engine/steps/fetch.js.map +1 -1
  229. package/dist/engine/steps/index.d.ts +2 -0
  230. package/dist/engine/steps/index.d.ts.map +1 -1
  231. package/dist/engine/steps/index.js +2 -0
  232. package/dist/engine/steps/index.js.map +1 -1
  233. package/dist/engine/steps/navigate.d.ts +1 -1
  234. package/dist/engine/steps/navigate.d.ts.map +1 -1
  235. package/dist/engine/steps/navigate.js +29 -2
  236. package/dist/engine/steps/navigate.js.map +1 -1
  237. package/dist/engine/steps/parse-rss.d.ts.map +1 -1
  238. package/dist/engine/steps/parse-rss.js +9 -4
  239. package/dist/engine/steps/parse-rss.js.map +1 -1
  240. package/dist/engine/template.d.ts.map +1 -1
  241. package/dist/engine/template.js +2 -1
  242. package/dist/engine/template.js.map +1 -1
  243. package/dist/engine/text-normalize.d.ts +6 -0
  244. package/dist/engine/text-normalize.d.ts.map +1 -0
  245. package/dist/engine/text-normalize.js +63 -0
  246. package/dist/engine/text-normalize.js.map +1 -0
  247. package/dist/fast-path.d.ts.map +1 -1
  248. package/dist/fast-path.js +291 -8
  249. package/dist/fast-path.js.map +1 -1
  250. package/dist/main.d.ts +1 -1
  251. package/dist/main.js +1 -1
  252. package/dist/manifest-compact.txt +2 -2
  253. package/dist/manifest-search.json +1 -1
  254. package/dist/manifest.json +4313 -533
  255. package/dist/mcp/dispatch.d.ts +3 -3
  256. package/dist/mcp/dispatch.d.ts.map +1 -1
  257. package/dist/mcp/dispatch.js +6 -5
  258. package/dist/mcp/dispatch.js.map +1 -1
  259. package/dist/mcp/handler.d.ts +2 -2
  260. package/dist/mcp/handler.d.ts.map +1 -1
  261. package/dist/mcp/handler.js +59 -5
  262. package/dist/mcp/handler.js.map +1 -1
  263. package/dist/mcp/profiles/computer-use.d.ts +4 -0
  264. package/dist/mcp/profiles/computer-use.d.ts.map +1 -0
  265. package/dist/mcp/profiles/computer-use.js +305 -0
  266. package/dist/mcp/profiles/computer-use.js.map +1 -0
  267. package/dist/mcp/server.d.ts.map +1 -1
  268. package/dist/mcp/server.js +30 -6
  269. package/dist/mcp/server.js.map +1 -1
  270. package/dist/mcp/tools.d.ts +9 -0
  271. package/dist/mcp/tools.d.ts.map +1 -1
  272. package/dist/mcp/tools.js +20 -0
  273. package/dist/mcp/tools.js.map +1 -1
  274. package/dist/output/envelope.d.ts +6 -0
  275. package/dist/output/envelope.d.ts.map +1 -1
  276. package/dist/output/envelope.js.map +1 -1
  277. package/dist/output/error-map.d.ts.map +1 -1
  278. package/dist/output/error-map.js +25 -0
  279. package/dist/output/error-map.js.map +1 -1
  280. package/dist/protocol/acp-helpers.d.ts +2 -2
  281. package/dist/protocol/acp-helpers.d.ts.map +1 -1
  282. package/dist/protocol/acp-helpers.js +5 -4
  283. package/dist/protocol/acp-helpers.js.map +1 -1
  284. package/dist/registry.d.ts +4 -1
  285. package/dist/registry.d.ts.map +1 -1
  286. package/dist/registry.js +7 -0
  287. package/dist/registry.js.map +1 -1
  288. package/dist/transport/adapters/cdp-browser.d.ts +38 -2
  289. package/dist/transport/adapters/cdp-browser.d.ts.map +1 -1
  290. package/dist/transport/adapters/cdp-browser.js +349 -22
  291. package/dist/transport/adapters/cdp-browser.js.map +1 -1
  292. package/dist/transport/adapters/desktop-atspi.d.ts +23 -17
  293. package/dist/transport/adapters/desktop-atspi.d.ts.map +1 -1
  294. package/dist/transport/adapters/desktop-atspi.js +143 -32
  295. package/dist/transport/adapters/desktop-atspi.js.map +1 -1
  296. package/dist/transport/adapters/desktop-ax-helpers.d.ts +24 -0
  297. package/dist/transport/adapters/desktop-ax-helpers.d.ts.map +1 -0
  298. package/dist/transport/adapters/desktop-ax-helpers.js +190 -0
  299. package/dist/transport/adapters/desktop-ax-helpers.js.map +1 -0
  300. package/dist/transport/adapters/desktop-ax-swift.d.ts +13 -0
  301. package/dist/transport/adapters/desktop-ax-swift.d.ts.map +1 -1
  302. package/dist/transport/adapters/desktop-ax-swift.js +176 -2
  303. package/dist/transport/adapters/desktop-ax-swift.js.map +1 -1
  304. package/dist/transport/adapters/desktop-ax.d.ts +11 -2
  305. package/dist/transport/adapters/desktop-ax.d.ts.map +1 -1
  306. package/dist/transport/adapters/desktop-ax.js +131 -16
  307. package/dist/transport/adapters/desktop-ax.js.map +1 -1
  308. package/dist/transport/adapters/desktop-sidecar-errors.d.ts +3 -0
  309. package/dist/transport/adapters/desktop-sidecar-errors.d.ts.map +1 -0
  310. package/dist/transport/adapters/desktop-sidecar-errors.js +34 -0
  311. package/dist/transport/adapters/desktop-sidecar-errors.js.map +1 -0
  312. package/dist/transport/adapters/desktop-sidecar-snapshot.d.ts +10 -0
  313. package/dist/transport/adapters/desktop-sidecar-snapshot.d.ts.map +1 -0
  314. package/dist/transport/adapters/desktop-sidecar-snapshot.js +89 -0
  315. package/dist/transport/adapters/desktop-sidecar-snapshot.js.map +1 -0
  316. package/dist/transport/adapters/desktop-uia.d.ts +23 -17
  317. package/dist/transport/adapters/desktop-uia.d.ts.map +1 -1
  318. package/dist/transport/adapters/desktop-uia.js +142 -32
  319. package/dist/transport/adapters/desktop-uia.js.map +1 -1
  320. package/dist/transport/adapters/subprocess.d.ts +7 -0
  321. package/dist/transport/adapters/subprocess.d.ts.map +1 -1
  322. package/dist/transport/adapters/subprocess.js +64 -0
  323. package/dist/transport/adapters/subprocess.js.map +1 -1
  324. package/dist/transport/bus.d.ts +2 -0
  325. package/dist/transport/bus.d.ts.map +1 -1
  326. package/dist/transport/bus.js +7 -11
  327. package/dist/transport/bus.js.map +1 -1
  328. package/dist/transport/capability.d.ts.map +1 -1
  329. package/dist/transport/capability.js +123 -98
  330. package/dist/transport/capability.js.map +1 -1
  331. package/dist/transport/cascade.d.ts +5 -0
  332. package/dist/transport/cascade.d.ts.map +1 -0
  333. package/dist/transport/cascade.js +550 -0
  334. package/dist/transport/cascade.js.map +1 -0
  335. package/dist/transport/cdp-session.d.ts +11 -0
  336. package/dist/transport/cdp-session.d.ts.map +1 -0
  337. package/dist/transport/cdp-session.js +52 -0
  338. package/dist/transport/cdp-session.js.map +1 -0
  339. package/dist/transport/refs.d.ts +51 -0
  340. package/dist/transport/refs.d.ts.map +1 -0
  341. package/dist/transport/refs.js +135 -0
  342. package/dist/transport/refs.js.map +1 -0
  343. package/dist/transport/sidecar-binary.d.ts +18 -0
  344. package/dist/transport/sidecar-binary.d.ts.map +1 -0
  345. package/dist/transport/sidecar-binary.js +55 -0
  346. package/dist/transport/sidecar-binary.js.map +1 -0
  347. package/dist/transport/sidecar.d.ts +35 -0
  348. package/dist/transport/sidecar.d.ts.map +1 -0
  349. package/dist/transport/sidecar.js +134 -0
  350. package/dist/transport/sidecar.js.map +1 -0
  351. package/dist/transport/snapshot-encoder.d.ts +34 -0
  352. package/dist/transport/snapshot-encoder.d.ts.map +1 -0
  353. package/dist/transport/snapshot-encoder.js +139 -0
  354. package/dist/transport/snapshot-encoder.js.map +1 -0
  355. package/dist/transport/types.d.ts +6 -1
  356. package/dist/transport/types.d.ts.map +1 -1
  357. package/dist/types.d.ts +11 -1
  358. package/dist/types.d.ts.map +1 -1
  359. package/dist/types.js.map +1 -1
  360. package/docs/mcp/clients/claude-code.md +29 -0
  361. package/docs/mcp/clients/claude-desktop.md +47 -0
  362. package/docs/mcp/clients/codex.md +29 -0
  363. package/docs/mcp/clients/cursor.md +38 -0
  364. package/docs/mcp/clients/gemini-cli.md +38 -0
  365. package/docs/operate/compute.md +172 -0
  366. package/docs/operate/electron.md +87 -0
  367. package/docs/operate/focus-behavior.md +40 -0
  368. package/docs/operate/troubleshooting.md +379 -0
  369. package/package.json +44 -19
  370. package/src/adapters/36kr/news.yaml +4 -1
  371. package/src/adapters/_electron/desktop-shared.ts +14 -0
  372. package/src/adapters/_electron/shared.ts +54 -2
  373. package/src/adapters/juejin/hot.test.ts +25 -0
  374. package/src/adapters/juejin/hot.yaml +52 -0
  375. package/src/adapters/juejin/search.test.ts +27 -0
  376. package/src/adapters/juejin/search.yaml +58 -0
  377. package/src/adapters/leetcode/discuss-search.test.ts +29 -0
  378. package/src/adapters/leetcode/discuss-search.yaml +56 -0
  379. package/src/adapters/macos/actions.ts +63 -0
@@ -0,0 +1,756 @@
1
+ use std::env;
2
+ use std::fs;
3
+ use std::path::PathBuf;
4
+ use std::process::Command;
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+ use serde_json::Value;
8
+ use unicli_shared::SidecarRequest;
9
+
10
+ use crate::errors::{backend_unavailable, AtspiError, HandlerResult};
11
+ use crate::tree::{
12
+ enumerate_top_level_windows, resolve_descendant_element_ref, resolve_top_level_window_ref,
13
+ ElementBounds, ElementRecord, State, WindowRecord,
14
+ };
15
+
16
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
+ enum DisplayServer {
18
+ Wayland,
19
+ X11,
20
+ Headless,
21
+ }
22
+
23
+ #[derive(Debug, Clone, PartialEq, Eq)]
24
+ struct CommandPlan {
25
+ program: String,
26
+ args: Vec<String>,
27
+ }
28
+
29
+ pub fn handle(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
30
+ if !cfg!(target_os = "linux") {
31
+ return Err(backend_unavailable());
32
+ }
33
+
34
+ let stable = read_optional_stable_ref(&request.params)?;
35
+ if let Some(stable) = stable {
36
+ let windows = enumerate_top_level_windows()?;
37
+ if let Some((window, element, path)) = resolve_descendant_element_ref(&windows, &stable) {
38
+ let bounds = require_descendant_bounds(element, &stable)?;
39
+ let screenshot = capture_region_screenshot(&request.params, bounds)?;
40
+ return Ok(screenshot_response_for_descendant(
41
+ window, element, &stable, &path, screenshot,
42
+ ));
43
+ }
44
+ let window = resolve_top_level_window_ref(&windows, &stable)
45
+ .ok_or_else(|| AtspiError::no_element(stable.clone()))?;
46
+ let screenshot = capture_window_screenshot(&request.params, window)?;
47
+ return Ok(screenshot_response_for_window(window, &stable, screenshot));
48
+ }
49
+
50
+ capture_screenshot(&request.params)
51
+ }
52
+
53
+ fn capture_screenshot(params: &Value) -> HandlerResult {
54
+ let requested_path = read_path(params);
55
+ let path = requested_path
56
+ .clone()
57
+ .unwrap_or_else(|| temporary_screenshot_path().to_string_lossy().into_owned());
58
+ let plan = screenshot_command_for(display_server_from_env(), &path, command_exists)?;
59
+ run_command(&plan)?;
60
+
61
+ let response = if requested_path.is_some() {
62
+ serde_json::json!({
63
+ "path": path,
64
+ "mime": "image/png",
65
+ "backend": plan.program,
66
+ })
67
+ } else {
68
+ let bytes = fs::read(&path).map_err(|err| {
69
+ AtspiError::unavailable(format!("failed to read screenshot file {path}: {err}"))
70
+ })?;
71
+ let _ = fs::remove_file(&path);
72
+ serde_json::json!({
73
+ "base64": base64_encode(&bytes),
74
+ "mime": "image/png",
75
+ "bytes": bytes.len(),
76
+ "backend": plan.program,
77
+ })
78
+ };
79
+
80
+ Ok(response)
81
+ }
82
+
83
+ fn capture_window_screenshot(params: &Value, window: &WindowRecord) -> HandlerResult {
84
+ let requested_path = read_path(params);
85
+ let path = requested_path
86
+ .clone()
87
+ .unwrap_or_else(|| temporary_screenshot_path().to_string_lossy().into_owned());
88
+ let plan = window_screenshot_command_for(
89
+ display_server_from_env(),
90
+ &path,
91
+ &window.id,
92
+ window.bounds.as_ref(),
93
+ command_exists,
94
+ )?;
95
+
96
+ if !plan_targets_window(&plan, &window.id) {
97
+ crate::invoke::focus_top_level_window(window)?;
98
+ }
99
+
100
+ run_command(&plan)?;
101
+
102
+ let mut response = if requested_path.is_some() {
103
+ serde_json::json!({
104
+ "path": path,
105
+ "mime": "image/png",
106
+ "backend": plan.program,
107
+ })
108
+ } else {
109
+ let bytes = fs::read(&path).map_err(|err| {
110
+ AtspiError::unavailable(format!("failed to read screenshot file {path}: {err}"))
111
+ })?;
112
+ let _ = fs::remove_file(&path);
113
+ serde_json::json!({
114
+ "base64": base64_encode(&bytes),
115
+ "mime": "image/png",
116
+ "bytes": bytes.len(),
117
+ "backend": plan.program,
118
+ })
119
+ };
120
+
121
+ if plan_targets_window(&plan, &window.id) {
122
+ response["scope"] = serde_json::json!("window");
123
+ response["windowId"] = serde_json::json!(window.id);
124
+ } else {
125
+ response["scope"] = serde_json::json!("screen_after_focus");
126
+ }
127
+
128
+ Ok(response)
129
+ }
130
+
131
+ fn capture_region_screenshot(params: &Value, bounds: &ElementBounds) -> HandlerResult {
132
+ let requested_path = read_path(params);
133
+ let path = requested_path
134
+ .clone()
135
+ .unwrap_or_else(|| temporary_screenshot_path().to_string_lossy().into_owned());
136
+ let plan =
137
+ region_screenshot_command_for(display_server_from_env(), &path, bounds, command_exists)?;
138
+
139
+ run_command(&plan)?;
140
+
141
+ let mut response = if requested_path.is_some() {
142
+ serde_json::json!({
143
+ "path": path,
144
+ "mime": "image/png",
145
+ "backend": plan.program,
146
+ })
147
+ } else {
148
+ let bytes = fs::read(&path).map_err(|err| {
149
+ AtspiError::unavailable(format!("failed to read screenshot file {path}: {err}"))
150
+ })?;
151
+ let _ = fs::remove_file(&path);
152
+ serde_json::json!({
153
+ "base64": base64_encode(&bytes),
154
+ "mime": "image/png",
155
+ "bytes": bytes.len(),
156
+ "backend": plan.program,
157
+ })
158
+ };
159
+ response["scope"] = serde_json::json!("region");
160
+ response["bounds"] = bounds_node(bounds);
161
+ Ok(response)
162
+ }
163
+
164
+ fn read_optional_stable_ref(params: &Value) -> Result<Option<String>, AtspiError> {
165
+ let Some(value) = params
166
+ .get("stable")
167
+ .or_else(|| params.get("ref"))
168
+ .and_then(Value::as_str)
169
+ else {
170
+ return Ok(None);
171
+ };
172
+ if value.starts_with("desktop-atspi:") {
173
+ return Ok(Some(value.to_string()));
174
+ }
175
+ Err(AtspiError::invalid_input(
176
+ "atspi_screenshot requires a desktop-atspi stable top-level window ref when ref is provided",
177
+ ))
178
+ }
179
+
180
+ fn screenshot_response_for_window(
181
+ window: &WindowRecord,
182
+ stable: &str,
183
+ screenshot: serde_json::Value,
184
+ ) -> serde_json::Value {
185
+ serde_json::json!({
186
+ "captured": true,
187
+ "via": "top_level_window_screenshot_helper",
188
+ "stable": stable,
189
+ "id": window.id,
190
+ "pid": window.pid,
191
+ "title": window.title,
192
+ "screenshot": screenshot,
193
+ })
194
+ }
195
+
196
+ fn screenshot_response_for_descendant(
197
+ window: &WindowRecord,
198
+ element: &ElementRecord,
199
+ stable: &str,
200
+ path: &str,
201
+ screenshot: serde_json::Value,
202
+ ) -> serde_json::Value {
203
+ let mut target = descendant_target_node(element, path);
204
+ if let Some(bounds) = &element.bounds {
205
+ target["bounds"] = bounds_node(bounds);
206
+ }
207
+ serde_json::json!({
208
+ "captured": true,
209
+ "via": "descendant_bounds_screenshot_helper",
210
+ "stable": stable,
211
+ "id": window.id,
212
+ "pid": window.pid,
213
+ "title": window.title,
214
+ "target": target,
215
+ "screenshot": screenshot,
216
+ })
217
+ }
218
+
219
+ fn require_descendant_bounds<'a>(
220
+ element: &'a ElementRecord,
221
+ stable: &str,
222
+ ) -> Result<&'a ElementBounds, AtspiError> {
223
+ element
224
+ .bounds
225
+ .as_ref()
226
+ .ok_or_else(|| AtspiError::not_invokable(stable.to_string()))
227
+ }
228
+
229
+ fn descendant_target_node(element: &ElementRecord, path: &str) -> serde_json::Value {
230
+ let mut target = serde_json::json!({
231
+ "role": element.role,
232
+ "name": element.name,
233
+ "path": path,
234
+ });
235
+ if let Some(value) = &element.value {
236
+ target["value"] = serde_json::json!(value);
237
+ }
238
+ target
239
+ }
240
+
241
+ fn bounds_node(bounds: &ElementBounds) -> serde_json::Value {
242
+ serde_json::json!({
243
+ "x": bounds.x,
244
+ "y": bounds.y,
245
+ "width": bounds.width,
246
+ "height": bounds.height,
247
+ })
248
+ }
249
+
250
+ fn read_path(params: &Value) -> Option<String> {
251
+ params
252
+ .get("path")
253
+ .and_then(Value::as_str)
254
+ .map(str::trim)
255
+ .filter(|path| !path.is_empty())
256
+ .map(String::from)
257
+ }
258
+
259
+ fn display_server_from_env() -> DisplayServer {
260
+ display_server_from_iter(env::vars())
261
+ }
262
+
263
+ fn display_server_from_iter<K, V, I>(pairs: I) -> DisplayServer
264
+ where
265
+ K: AsRef<str>,
266
+ V: AsRef<str>,
267
+ I: IntoIterator<Item = (K, V)>,
268
+ {
269
+ let mut has_x11 = false;
270
+ for (key, value) in pairs {
271
+ let key = key.as_ref();
272
+ let value = value.as_ref();
273
+ if value.is_empty() {
274
+ continue;
275
+ }
276
+ if key == "WAYLAND_DISPLAY" {
277
+ return DisplayServer::Wayland;
278
+ }
279
+ if key == "DISPLAY" {
280
+ has_x11 = true;
281
+ }
282
+ }
283
+ if has_x11 {
284
+ DisplayServer::X11
285
+ } else {
286
+ DisplayServer::Headless
287
+ }
288
+ }
289
+
290
+ fn screenshot_command_for(
291
+ server: DisplayServer,
292
+ path: &str,
293
+ exists: impl Fn(&str) -> bool,
294
+ ) -> Result<CommandPlan, AtspiError> {
295
+ if exists("gnome-screenshot") {
296
+ return Ok(CommandPlan {
297
+ program: "gnome-screenshot".into(),
298
+ args: vec!["-f".into(), path.into()],
299
+ });
300
+ }
301
+
302
+ match server {
303
+ DisplayServer::Wayland => wayland_screenshot_command(path, exists),
304
+ DisplayServer::X11 => x11_screenshot_command(path, exists),
305
+ DisplayServer::Headless => Err(AtspiError::unavailable(
306
+ "no WAYLAND_DISPLAY or DISPLAY environment is available for screenshot capture",
307
+ )),
308
+ }
309
+ }
310
+
311
+ fn window_screenshot_command_for(
312
+ server: DisplayServer,
313
+ path: &str,
314
+ window_id: &str,
315
+ bounds: Option<&crate::tree::WindowBounds>,
316
+ exists: impl Fn(&str) -> bool,
317
+ ) -> Result<CommandPlan, AtspiError> {
318
+ if server == DisplayServer::X11 && exists("import") {
319
+ return Ok(CommandPlan {
320
+ program: "import".into(),
321
+ args: vec!["-window".into(), window_id.into(), path.into()],
322
+ });
323
+ }
324
+ if server == DisplayServer::Wayland {
325
+ if let Some(bounds) = bounds {
326
+ if exists("grim") {
327
+ return Ok(CommandPlan {
328
+ program: "grim".into(),
329
+ args: vec![
330
+ "-g".into(),
331
+ format!(
332
+ "{},{} {}x{}",
333
+ bounds.x, bounds.y, bounds.width, bounds.height
334
+ ),
335
+ path.into(),
336
+ ],
337
+ });
338
+ }
339
+ }
340
+ }
341
+
342
+ screenshot_command_for(server, path, exists)
343
+ }
344
+
345
+ fn region_screenshot_command_for(
346
+ server: DisplayServer,
347
+ path: &str,
348
+ bounds: &ElementBounds,
349
+ exists: impl Fn(&str) -> bool,
350
+ ) -> Result<CommandPlan, AtspiError> {
351
+ match server {
352
+ DisplayServer::X11 => x11_region_screenshot_command(path, bounds, exists),
353
+ DisplayServer::Wayland => wayland_region_screenshot_command(path, bounds, exists),
354
+ DisplayServer::Headless => Err(AtspiError::unavailable(
355
+ "no WAYLAND_DISPLAY or DISPLAY environment is available for region screenshot capture",
356
+ )),
357
+ }
358
+ }
359
+
360
+ fn plan_targets_window(plan: &CommandPlan, window_id: &str) -> bool {
361
+ (plan.program == "import"
362
+ && plan.args.first().map(String::as_str) == Some("-window")
363
+ && plan.args.get(1).map(String::as_str) == Some(window_id))
364
+ || (plan.program == "grim" && plan.args.first().map(String::as_str) == Some("-g"))
365
+ }
366
+
367
+ fn wayland_screenshot_command(
368
+ path: &str,
369
+ exists: impl Fn(&str) -> bool,
370
+ ) -> Result<CommandPlan, AtspiError> {
371
+ if exists("grim") {
372
+ return Ok(CommandPlan {
373
+ program: "grim".into(),
374
+ args: vec![path.into()],
375
+ });
376
+ }
377
+ Err(AtspiError::unavailable(
378
+ "gnome-screenshot or grim is required for AT-SPI Wayland screenshot capture",
379
+ ))
380
+ }
381
+
382
+ fn x11_screenshot_command(
383
+ path: &str,
384
+ exists: impl Fn(&str) -> bool,
385
+ ) -> Result<CommandPlan, AtspiError> {
386
+ if exists("import") {
387
+ return Ok(CommandPlan {
388
+ program: "import".into(),
389
+ args: vec!["-window".into(), "root".into(), path.into()],
390
+ });
391
+ }
392
+ Err(AtspiError::unavailable(
393
+ "gnome-screenshot or ImageMagick import is required for AT-SPI X11 screenshot capture",
394
+ ))
395
+ }
396
+
397
+ fn x11_region_screenshot_command(
398
+ path: &str,
399
+ bounds: &ElementBounds,
400
+ exists: impl Fn(&str) -> bool,
401
+ ) -> Result<CommandPlan, AtspiError> {
402
+ if exists("import") {
403
+ return Ok(CommandPlan {
404
+ program: "import".into(),
405
+ args: vec![
406
+ "-window".into(),
407
+ "root".into(),
408
+ "-crop".into(),
409
+ format!(
410
+ "{}x{}+{}+{}",
411
+ bounds.width, bounds.height, bounds.x, bounds.y
412
+ ),
413
+ path.into(),
414
+ ],
415
+ });
416
+ }
417
+ Err(AtspiError::unavailable(
418
+ "ImageMagick import is required for AT-SPI X11 region screenshot capture",
419
+ ))
420
+ }
421
+
422
+ fn wayland_region_screenshot_command(
423
+ path: &str,
424
+ bounds: &ElementBounds,
425
+ exists: impl Fn(&str) -> bool,
426
+ ) -> Result<CommandPlan, AtspiError> {
427
+ if exists("grim") {
428
+ return Ok(CommandPlan {
429
+ program: "grim".into(),
430
+ args: vec![
431
+ "-g".into(),
432
+ format!(
433
+ "{},{} {}x{}",
434
+ bounds.x, bounds.y, bounds.width, bounds.height
435
+ ),
436
+ path.into(),
437
+ ],
438
+ });
439
+ }
440
+ Err(AtspiError::unavailable(
441
+ "grim is required for AT-SPI Wayland region screenshot capture",
442
+ ))
443
+ }
444
+
445
+ fn temporary_screenshot_path() -> PathBuf {
446
+ let now = SystemTime::now()
447
+ .duration_since(UNIX_EPOCH)
448
+ .map(|duration| duration.as_millis())
449
+ .unwrap_or(0);
450
+ env::temp_dir().join(format!(
451
+ "unicli-atspi-screenshot-{}-{now}.png",
452
+ std::process::id()
453
+ ))
454
+ }
455
+
456
+ fn run_command(plan: &CommandPlan) -> Result<(), AtspiError> {
457
+ let status = Command::new(&plan.program)
458
+ .args(&plan.args)
459
+ .status()
460
+ .map_err(|err| {
461
+ AtspiError::unavailable(format!(
462
+ "failed to run screenshot helper {}: {err}",
463
+ plan.program
464
+ ))
465
+ })?;
466
+ if status.success() {
467
+ return Ok(());
468
+ }
469
+ Err(AtspiError::unavailable(format!(
470
+ "screenshot helper {} exited with status {status}",
471
+ plan.program
472
+ )))
473
+ }
474
+
475
+ fn command_exists(program: &str) -> bool {
476
+ let Some(paths) = env::var_os("PATH") else {
477
+ return false;
478
+ };
479
+ env::split_paths(&paths).any(|path| is_executable(path.join(program)))
480
+ }
481
+
482
+ fn is_executable(path: PathBuf) -> bool {
483
+ fs::metadata(path)
484
+ .map(|metadata| metadata.is_file())
485
+ .unwrap_or(false)
486
+ }
487
+
488
+ fn base64_encode(bytes: &[u8]) -> String {
489
+ const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
490
+ let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
491
+ for chunk in bytes.chunks(3) {
492
+ let b0 = chunk[0];
493
+ let b1 = *chunk.get(1).unwrap_or(&0);
494
+ let b2 = *chunk.get(2).unwrap_or(&0);
495
+ let n = (u32::from(b0) << 16) | (u32::from(b1) << 8) | u32::from(b2);
496
+ out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
497
+ out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
498
+ out.push(if chunk.len() > 1 {
499
+ TABLE[((n >> 6) & 0x3f) as usize] as char
500
+ } else {
501
+ '='
502
+ });
503
+ out.push(if chunk.len() > 2 {
504
+ TABLE[(n & 0x3f) as usize] as char
505
+ } else {
506
+ '='
507
+ });
508
+ }
509
+ out
510
+ }
511
+
512
+ #[cfg(test)]
513
+ mod tests {
514
+ use super::*;
515
+
516
+ #[test]
517
+ fn wayland_screenshot_uses_gnome_screenshot_when_available() {
518
+ let plan = screenshot_command_for(DisplayServer::Wayland, "/tmp/shot.png", |program| {
519
+ program == "gnome-screenshot"
520
+ })
521
+ .expect("wayland screenshot plan");
522
+
523
+ assert_eq!(
524
+ plan,
525
+ CommandPlan {
526
+ program: "gnome-screenshot".into(),
527
+ args: vec!["-f".into(), "/tmp/shot.png".into()],
528
+ },
529
+ );
530
+ }
531
+
532
+ #[test]
533
+ fn x11_screenshot_falls_back_to_import_when_gnome_screenshot_is_missing() {
534
+ let plan = screenshot_command_for(DisplayServer::X11, "/tmp/shot.png", |program| {
535
+ program == "import"
536
+ })
537
+ .expect("x11 screenshot plan");
538
+
539
+ assert_eq!(
540
+ plan,
541
+ CommandPlan {
542
+ program: "import".into(),
543
+ args: vec!["-window".into(), "root".into(), "/tmp/shot.png".into()],
544
+ },
545
+ );
546
+ }
547
+
548
+ #[test]
549
+ fn x11_window_screenshot_prefers_import_with_window_id() {
550
+ let plan = window_screenshot_command_for(
551
+ DisplayServer::X11,
552
+ "/tmp/shot.png",
553
+ "0x03a00008",
554
+ None,
555
+ |program| program == "gnome-screenshot" || program == "import",
556
+ )
557
+ .expect("x11 targeted screenshot plan");
558
+
559
+ assert_eq!(
560
+ plan,
561
+ CommandPlan {
562
+ program: "import".into(),
563
+ args: vec![
564
+ "-window".into(),
565
+ "0x03a00008".into(),
566
+ "/tmp/shot.png".into()
567
+ ],
568
+ },
569
+ );
570
+ }
571
+
572
+ #[test]
573
+ fn wayland_window_screenshot_uses_grim_geometry_when_bounds_are_known() {
574
+ let bounds = crate::tree::WindowBounds {
575
+ x: 10,
576
+ y: 20,
577
+ width: 640,
578
+ height: 480,
579
+ };
580
+ let plan = window_screenshot_command_for(
581
+ DisplayServer::Wayland,
582
+ "/tmp/shot.png",
583
+ "0x03a00008",
584
+ Some(&bounds),
585
+ |program| program == "grim",
586
+ )
587
+ .expect("wayland targeted screenshot plan");
588
+
589
+ assert_eq!(
590
+ plan,
591
+ CommandPlan {
592
+ program: "grim".into(),
593
+ args: vec!["-g".into(), "10,20 640x480".into(), "/tmp/shot.png".into()],
594
+ },
595
+ );
596
+ }
597
+
598
+ #[test]
599
+ fn x11_region_screenshot_uses_import_root_crop() {
600
+ let bounds = crate::tree::ElementBounds {
601
+ x: 20,
602
+ y: 30,
603
+ width: 40,
604
+ height: 50,
605
+ };
606
+ let plan = region_screenshot_command_for(
607
+ DisplayServer::X11,
608
+ "/tmp/element.png",
609
+ &bounds,
610
+ |program| program == "import",
611
+ )
612
+ .expect("x11 region screenshot plan");
613
+
614
+ assert_eq!(
615
+ plan,
616
+ CommandPlan {
617
+ program: "import".into(),
618
+ args: vec![
619
+ "-window".into(),
620
+ "root".into(),
621
+ "-crop".into(),
622
+ "40x50+20+30".into(),
623
+ "/tmp/element.png".into(),
624
+ ],
625
+ },
626
+ );
627
+ }
628
+
629
+ #[test]
630
+ fn wayland_region_screenshot_uses_grim_geometry() {
631
+ let bounds = crate::tree::ElementBounds {
632
+ x: 20,
633
+ y: 30,
634
+ width: 40,
635
+ height: 50,
636
+ };
637
+ let plan = region_screenshot_command_for(
638
+ DisplayServer::Wayland,
639
+ "/tmp/element.png",
640
+ &bounds,
641
+ |program| program == "grim",
642
+ )
643
+ .expect("wayland region screenshot plan");
644
+
645
+ assert_eq!(
646
+ plan,
647
+ CommandPlan {
648
+ program: "grim".into(),
649
+ args: vec!["-g".into(), "20,30 40x50".into(), "/tmp/element.png".into()],
650
+ },
651
+ );
652
+ }
653
+
654
+ #[test]
655
+ fn screenshot_response_includes_target_window_metadata() {
656
+ let response = screenshot_response_for_window(
657
+ &crate::tree::WindowRecord {
658
+ id: "0x03a00008".into(),
659
+ pid: 1234,
660
+ title: "Terminal Settings".into(),
661
+ desktop: "0".into(),
662
+ host: "host".into(),
663
+ bounds: None,
664
+ children: vec![],
665
+ },
666
+ "desktop-atspi:pid-1234:Window[1]",
667
+ serde_json::json!({
668
+ "path": "/tmp/shot.png",
669
+ "mime": "image/png",
670
+ "backend": "gnome-screenshot",
671
+ }),
672
+ );
673
+
674
+ assert_eq!(
675
+ response,
676
+ serde_json::json!({
677
+ "captured": true,
678
+ "via": "top_level_window_screenshot_helper",
679
+ "stable": "desktop-atspi:pid-1234:Window[1]",
680
+ "id": "0x03a00008",
681
+ "pid": 1234,
682
+ "title": "Terminal Settings",
683
+ "screenshot": {
684
+ "path": "/tmp/shot.png",
685
+ "mime": "image/png",
686
+ "backend": "gnome-screenshot",
687
+ },
688
+ }),
689
+ );
690
+ }
691
+
692
+ #[test]
693
+ fn screenshot_response_includes_descendant_target_metadata() {
694
+ let response = screenshot_response_for_descendant(
695
+ &crate::tree::WindowRecord {
696
+ id: "0x03a00008".into(),
697
+ pid: 1234,
698
+ title: "Calculator".into(),
699
+ desktop: "0".into(),
700
+ host: "host".into(),
701
+ bounds: None,
702
+ children: vec![],
703
+ },
704
+ &crate::tree::ElementRecord {
705
+ role: "push_button".into(),
706
+ name: "Seven".into(),
707
+ value: None,
708
+ bounds: Some(crate::tree::ElementBounds {
709
+ x: 20,
710
+ y: 30,
711
+ width: 40,
712
+ height: 50,
713
+ }),
714
+ states: vec!["enabled".into()],
715
+ children: vec![],
716
+ },
717
+ "desktop-atspi:pid-1234:Window[0]/push_button[1]",
718
+ "Window[0]/push_button[1]",
719
+ serde_json::json!({
720
+ "path": "/tmp/element.png",
721
+ "mime": "image/png",
722
+ "backend": "grim",
723
+ "scope": "region",
724
+ }),
725
+ );
726
+
727
+ assert_eq!(
728
+ response,
729
+ serde_json::json!({
730
+ "captured": true,
731
+ "via": "descendant_bounds_screenshot_helper",
732
+ "stable": "desktop-atspi:pid-1234:Window[0]/push_button[1]",
733
+ "id": "0x03a00008",
734
+ "pid": 1234,
735
+ "title": "Calculator",
736
+ "target": {
737
+ "role": "push_button",
738
+ "name": "Seven",
739
+ "path": "Window[0]/push_button[1]",
740
+ "bounds": {
741
+ "x": 20,
742
+ "y": 30,
743
+ "width": 40,
744
+ "height": 50,
745
+ },
746
+ },
747
+ "screenshot": {
748
+ "path": "/tmp/element.png",
749
+ "mime": "image/png",
750
+ "backend": "grim",
751
+ "scope": "region",
752
+ },
753
+ }),
754
+ );
755
+ }
756
+ }