@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,977 @@
1
+ use std::process::Command;
2
+
3
+ use unicli_shared::SidecarRequest;
4
+
5
+ use crate::errors::{HandlerResult, UiaError};
6
+ use crate::input::send_text_input;
7
+ #[cfg(target_os = "windows")]
8
+ use crate::tree::resolve_live_descendant_element;
9
+ use crate::tree::{
10
+ enumerate_top_level_windows, resolve_descendant_element_ref, resolve_top_level_window_ref,
11
+ ElementBounds, ElementRecord, State, WindowRecord,
12
+ };
13
+
14
+ pub fn handle_invoke(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
15
+ let stable = read_stable_ref(&request.params, "uia_invoke")?;
16
+ let windows = enumerate_top_level_windows()?;
17
+ if let Some((window, element, path)) = resolve_descendant_element_ref(&windows, &stable) {
18
+ focus_top_level_window(window)?;
19
+ if let Some(action) = try_native_invoke_descendant(window, &stable)? {
20
+ return Ok(invoke_response_for_descendant(
21
+ window,
22
+ element,
23
+ &stable,
24
+ &path,
25
+ native_invoke_action_via(action),
26
+ ));
27
+ }
28
+ let bounds = require_descendant_bounds(element, &stable)?;
29
+ post_click_descendant(window, bounds)?;
30
+ return Ok(invoke_response_for_descendant(
31
+ window,
32
+ element,
33
+ &stable,
34
+ &path,
35
+ "post_message",
36
+ ));
37
+ }
38
+ let stable = require_top_level_stable_ref(stable)?;
39
+ let window = resolve_top_level_window_ref(&windows, &stable)
40
+ .ok_or_else(|| UiaError::no_element(stable.clone()))?;
41
+ focus_top_level_window(window)?;
42
+ Ok(invoke_response_for_window(window, &stable))
43
+ }
44
+
45
+ pub fn handle_set_value(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
46
+ let stable = read_stable_ref(&request.params, "uia_set_value")?;
47
+ let text = read_text_value(&request.params)?;
48
+ let windows = enumerate_top_level_windows()?;
49
+ if let Some((window, element, path)) = resolve_descendant_element_ref(&windows, &stable) {
50
+ focus_top_level_window(window)?;
51
+ if let Some(action) = try_native_set_value_descendant(window, &stable, &text)? {
52
+ return Ok(set_value_response_for_descendant(
53
+ window,
54
+ element,
55
+ &stable,
56
+ &path,
57
+ &text,
58
+ native_set_value_action_via(action),
59
+ ));
60
+ }
61
+ let bounds = require_descendant_bounds(element, &stable)?;
62
+ post_click_descendant(window, bounds)?;
63
+ send_text_input(&text)?;
64
+ return Ok(set_value_response_for_descendant(
65
+ window,
66
+ element,
67
+ &stable,
68
+ &path,
69
+ &text,
70
+ "descendant_post_message_sendinput",
71
+ ));
72
+ }
73
+ let stable = require_top_level_stable_ref(stable)?;
74
+ let window = resolve_top_level_window_ref(&windows, &stable)
75
+ .ok_or_else(|| UiaError::no_element(stable.clone()))?;
76
+ focus_top_level_window(window)?;
77
+ send_text_input(&text)?;
78
+ Ok(set_value_response_for_window(window, &stable, &text))
79
+ }
80
+
81
+ pub fn handle_focus(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
82
+ let stable = read_stable_ref(&request.params, "uia_focus")?;
83
+ let windows = enumerate_top_level_windows()?;
84
+ if let Some((window, element, path)) = resolve_descendant_element_ref(&windows, &stable) {
85
+ focus_top_level_window(window)?;
86
+ if try_native_focus_descendant(window, &stable)? {
87
+ return Ok(focus_response_for_descendant(
88
+ window,
89
+ element,
90
+ &stable,
91
+ &path,
92
+ "uia_set_focus",
93
+ ));
94
+ }
95
+ let bounds = require_descendant_bounds(element, &stable)?;
96
+ post_click_descendant(window, bounds)?;
97
+ return Ok(focus_response_for_descendant(
98
+ window,
99
+ element,
100
+ &stable,
101
+ &path,
102
+ "descendant_post_message",
103
+ ));
104
+ }
105
+ let stable = require_top_level_stable_ref(stable)?;
106
+ let window = resolve_top_level_window_ref(&windows, &stable)
107
+ .ok_or_else(|| UiaError::no_element(stable.clone()))?;
108
+ focus_top_level_window(window)?;
109
+ Ok(focus_response_for_window(window, &stable))
110
+ }
111
+
112
+ pub fn handle_launch_app(_state: &mut State, request: &SidecarRequest) -> HandlerResult {
113
+ let app = read_app_name(&request.params, "launch_app")?;
114
+ let args = read_args(&request.params);
115
+ let debug_port = read_debug_port(&request.params);
116
+ let plan = launch_plan_for_app(&app, &args, debug_port);
117
+ run_launch_plan(&plan)?;
118
+ Ok(serde_json::json!({
119
+ "launched": true,
120
+ "via": "start_process",
121
+ "app": app,
122
+ }))
123
+ }
124
+
125
+ fn read_stable_ref(params: &serde_json::Value, action: &str) -> Result<String, UiaError> {
126
+ params
127
+ .get("stable")
128
+ .or_else(|| params.get("ref"))
129
+ .and_then(serde_json::Value::as_str)
130
+ .filter(|value| value.starts_with("desktop-uia:"))
131
+ .map(str::to_string)
132
+ .ok_or_else(|| {
133
+ UiaError::invalid_input(format!(
134
+ "{action} requires a desktop-uia stable top-level window ref"
135
+ ))
136
+ })
137
+ }
138
+
139
+ fn require_top_level_stable_ref(stable: String) -> Result<String, UiaError> {
140
+ if stable
141
+ .split_once(':')
142
+ .and_then(|(_, tail)| tail.split_once(':'))
143
+ .map_or(false, |(_, path)| path.contains('/'))
144
+ {
145
+ return Err(UiaError::not_invokable(stable));
146
+ }
147
+ Ok(stable)
148
+ }
149
+
150
+ fn read_app_name(params: &serde_json::Value, action: &str) -> Result<String, UiaError> {
151
+ params
152
+ .get("app")
153
+ .or_else(|| params.get("name"))
154
+ .and_then(serde_json::Value::as_str)
155
+ .map(str::trim)
156
+ .filter(|value| !value.is_empty())
157
+ .map(str::to_string)
158
+ .ok_or_else(|| UiaError::invalid_input(format!("{action} requires app or name")))
159
+ }
160
+
161
+ fn read_args(params: &serde_json::Value) -> Vec<String> {
162
+ params
163
+ .get("args")
164
+ .and_then(serde_json::Value::as_array)
165
+ .map(|args| {
166
+ args.iter()
167
+ .filter_map(serde_json::Value::as_str)
168
+ .map(str::to_string)
169
+ .collect()
170
+ })
171
+ .unwrap_or_default()
172
+ }
173
+
174
+ fn read_debug_port(params: &serde_json::Value) -> Option<u16> {
175
+ params
176
+ .get("debugPort")
177
+ .or_else(|| params.get("debug_port"))
178
+ .and_then(serde_json::Value::as_u64)
179
+ .and_then(|value| u16::try_from(value).ok())
180
+ }
181
+
182
+ fn read_text_value(params: &serde_json::Value) -> Result<String, UiaError> {
183
+ params
184
+ .get("text")
185
+ .or_else(|| params.get("value"))
186
+ .and_then(serde_json::Value::as_str)
187
+ .map(str::to_string)
188
+ .ok_or_else(|| UiaError::invalid_input("uia_set_value requires text or value"))
189
+ }
190
+
191
+ #[derive(Debug, Clone, PartialEq, Eq)]
192
+ struct LaunchPlan {
193
+ program: &'static str,
194
+ args: Vec<String>,
195
+ }
196
+
197
+ fn launch_plan_for_app(app: &str, args: &[String], debug_port: Option<u16>) -> LaunchPlan {
198
+ let launch_args: Vec<String> = args
199
+ .iter()
200
+ .cloned()
201
+ .chain(debug_port.map(|port| format!("--remote-debugging-port={port}")))
202
+ .collect();
203
+ let mut plan_args = vec![
204
+ "-NoProfile".into(),
205
+ "-NonInteractive".into(),
206
+ "-Command".into(),
207
+ "Start-Process -FilePath $args[0] -ArgumentList $args[1]".into(),
208
+ app.into(),
209
+ launch_args.join(" "),
210
+ ];
211
+ if launch_args.is_empty() {
212
+ plan_args[3] = "Start-Process -FilePath $args[0]".into();
213
+ plan_args.truncate(5);
214
+ }
215
+ LaunchPlan {
216
+ program: "powershell.exe",
217
+ args: plan_args,
218
+ }
219
+ }
220
+
221
+ fn run_launch_plan(plan: &LaunchPlan) -> Result<(), UiaError> {
222
+ let status = Command::new(plan.program)
223
+ .args(&plan.args)
224
+ .status()
225
+ .map_err(|err| UiaError::unavailable(format!("failed to run app launcher: {err}")))?;
226
+ if status.success() {
227
+ return Ok(());
228
+ }
229
+ Err(UiaError::unavailable(format!(
230
+ "app launcher {} exited with status {status}",
231
+ plan.program
232
+ )))
233
+ }
234
+
235
+ fn focus_response_for_window(window: &WindowRecord, stable: &str) -> serde_json::Value {
236
+ serde_json::json!({
237
+ "focused": true,
238
+ "via": "set_foreground_window",
239
+ "stable": stable,
240
+ "hwnd": window.hwnd,
241
+ "pid": window.pid,
242
+ "title": window.title,
243
+ })
244
+ }
245
+
246
+ fn invoke_response_for_window(window: &WindowRecord, stable: &str) -> serde_json::Value {
247
+ serde_json::json!({
248
+ "invoked": true,
249
+ "via": "top_level_window",
250
+ "stable": stable,
251
+ "hwnd": window.hwnd,
252
+ "pid": window.pid,
253
+ "title": window.title,
254
+ })
255
+ }
256
+
257
+ fn invoke_response_for_descendant(
258
+ window: &WindowRecord,
259
+ element: &ElementRecord,
260
+ stable: &str,
261
+ path: &str,
262
+ via: &str,
263
+ ) -> serde_json::Value {
264
+ let mut target = descendant_target_node(element, path);
265
+ if let Some(bounds) = &element.bounds {
266
+ target["bounds"] = bounds_node(bounds);
267
+ }
268
+ serde_json::json!({
269
+ "invoked": true,
270
+ "via": via,
271
+ "stable": stable,
272
+ "hwnd": window.hwnd,
273
+ "pid": window.pid,
274
+ "title": window.title,
275
+ "target": target,
276
+ })
277
+ }
278
+
279
+ fn set_value_response_for_window(
280
+ window: &WindowRecord,
281
+ stable: &str,
282
+ text: &str,
283
+ ) -> serde_json::Value {
284
+ serde_json::json!({
285
+ "set": true,
286
+ "via": "top_level_window_sendinput",
287
+ "stable": stable,
288
+ "hwnd": window.hwnd,
289
+ "pid": window.pid,
290
+ "title": window.title,
291
+ "chars": text.chars().count(),
292
+ })
293
+ }
294
+
295
+ fn set_value_response_for_descendant(
296
+ window: &WindowRecord,
297
+ element: &ElementRecord,
298
+ stable: &str,
299
+ path: &str,
300
+ text: &str,
301
+ via: &str,
302
+ ) -> serde_json::Value {
303
+ let mut target = descendant_target_node(element, path);
304
+ if let Some(bounds) = &element.bounds {
305
+ target["bounds"] = bounds_node(bounds);
306
+ }
307
+ serde_json::json!({
308
+ "set": true,
309
+ "via": via,
310
+ "stable": stable,
311
+ "hwnd": window.hwnd,
312
+ "pid": window.pid,
313
+ "title": window.title,
314
+ "target": target,
315
+ "chars": text.chars().count(),
316
+ })
317
+ }
318
+
319
+ fn focus_response_for_descendant(
320
+ window: &WindowRecord,
321
+ element: &ElementRecord,
322
+ stable: &str,
323
+ path: &str,
324
+ via: &str,
325
+ ) -> serde_json::Value {
326
+ let mut target = descendant_target_node(element, path);
327
+ if let Some(bounds) = &element.bounds {
328
+ target["bounds"] = bounds_node(bounds);
329
+ }
330
+ serde_json::json!({
331
+ "focused": true,
332
+ "via": via,
333
+ "stable": stable,
334
+ "hwnd": window.hwnd,
335
+ "pid": window.pid,
336
+ "title": window.title,
337
+ "target": target,
338
+ })
339
+ }
340
+
341
+ fn require_descendant_bounds<'a>(
342
+ element: &'a ElementRecord,
343
+ stable: &str,
344
+ ) -> Result<&'a ElementBounds, UiaError> {
345
+ element
346
+ .bounds
347
+ .as_ref()
348
+ .ok_or_else(|| UiaError::not_invokable(stable.to_string()))
349
+ }
350
+
351
+ fn descendant_target_node(element: &ElementRecord, path: &str) -> serde_json::Value {
352
+ let mut target = serde_json::json!({
353
+ "role": element.role,
354
+ "name": element.name,
355
+ "path": path,
356
+ });
357
+ if let Some(value) = &element.value {
358
+ target["value"] = serde_json::json!(value);
359
+ }
360
+ target
361
+ }
362
+
363
+ fn bounds_node(bounds: &ElementBounds) -> serde_json::Value {
364
+ serde_json::json!({
365
+ "x": bounds.x,
366
+ "y": bounds.y,
367
+ "width": bounds.width,
368
+ "height": bounds.height,
369
+ })
370
+ }
371
+
372
+ #[cfg(target_os = "windows")]
373
+ fn try_native_invoke_descendant(
374
+ window: &WindowRecord,
375
+ stable: &str,
376
+ ) -> Result<Option<NativeInvokeAction>, UiaError> {
377
+ let element = match resolve_live_descendant_element(window, stable) {
378
+ Ok(element) => element,
379
+ Err(_) => return Ok(None),
380
+ };
381
+ for action in native_invoke_actions() {
382
+ if try_windows_native_invoke_action(&element, action) {
383
+ return Ok(Some(action));
384
+ }
385
+ }
386
+ Ok(None)
387
+ }
388
+
389
+ #[cfg(not(target_os = "windows"))]
390
+ fn try_native_invoke_descendant(
391
+ _window: &WindowRecord,
392
+ _stable: &str,
393
+ ) -> Result<Option<NativeInvokeAction>, UiaError> {
394
+ Ok(None)
395
+ }
396
+
397
+ #[cfg_attr(not(any(target_os = "windows", test)), allow(dead_code))]
398
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
399
+ enum NativeInvokeAction {
400
+ Invoke,
401
+ Toggle,
402
+ SelectionItem,
403
+ }
404
+
405
+ #[cfg(any(target_os = "windows", test))]
406
+ fn native_invoke_actions() -> Vec<NativeInvokeAction> {
407
+ vec![
408
+ NativeInvokeAction::Invoke,
409
+ NativeInvokeAction::Toggle,
410
+ NativeInvokeAction::SelectionItem,
411
+ ]
412
+ }
413
+
414
+ fn native_invoke_action_via(action: NativeInvokeAction) -> &'static str {
415
+ match action {
416
+ NativeInvokeAction::Invoke => "uia_invoke_pattern",
417
+ NativeInvokeAction::Toggle => "uia_toggle_pattern",
418
+ NativeInvokeAction::SelectionItem => "uia_selection_item_pattern",
419
+ }
420
+ }
421
+
422
+ #[cfg(target_os = "windows")]
423
+ fn try_windows_native_invoke_action(
424
+ element: &windows::Win32::UI::Accessibility::IUIAutomationElement,
425
+ action: NativeInvokeAction,
426
+ ) -> bool {
427
+ use windows::Win32::UI::Accessibility::{
428
+ IUIAutomationInvokePattern, IUIAutomationSelectionItemPattern, IUIAutomationTogglePattern,
429
+ UIA_InvokePatternId, UIA_SelectionItemPatternId, UIA_TogglePatternId,
430
+ };
431
+
432
+ match action {
433
+ NativeInvokeAction::Invoke => {
434
+ let pattern = unsafe {
435
+ element.GetCurrentPatternAs::<IUIAutomationInvokePattern>(UIA_InvokePatternId)
436
+ };
437
+ pattern
438
+ .map(|pattern| unsafe { pattern.Invoke() }.is_ok())
439
+ .unwrap_or(false)
440
+ }
441
+ NativeInvokeAction::Toggle => {
442
+ let pattern = unsafe {
443
+ element.GetCurrentPatternAs::<IUIAutomationTogglePattern>(UIA_TogglePatternId)
444
+ };
445
+ pattern
446
+ .map(|pattern| unsafe { pattern.Toggle() }.is_ok())
447
+ .unwrap_or(false)
448
+ }
449
+ NativeInvokeAction::SelectionItem => {
450
+ let pattern = unsafe {
451
+ element.GetCurrentPatternAs::<IUIAutomationSelectionItemPattern>(
452
+ UIA_SelectionItemPatternId,
453
+ )
454
+ };
455
+ pattern
456
+ .map(|pattern| unsafe { pattern.Select() }.is_ok())
457
+ .unwrap_or(false)
458
+ }
459
+ }
460
+ }
461
+
462
+ #[cfg(target_os = "windows")]
463
+ fn try_native_set_value_descendant(
464
+ window: &WindowRecord,
465
+ stable: &str,
466
+ text: &str,
467
+ ) -> Result<Option<NativeSetValueAction>, UiaError> {
468
+ let element = match resolve_live_descendant_element(window, stable) {
469
+ Ok(element) => element,
470
+ Err(_) => return Ok(None),
471
+ };
472
+ for action in native_set_value_actions_for_text(text) {
473
+ if try_windows_native_set_value_action(&element, action, text) {
474
+ return Ok(Some(action));
475
+ }
476
+ }
477
+ Ok(None)
478
+ }
479
+
480
+ #[cfg(not(target_os = "windows"))]
481
+ fn try_native_set_value_descendant(
482
+ _window: &WindowRecord,
483
+ _stable: &str,
484
+ _text: &str,
485
+ ) -> Result<Option<NativeSetValueAction>, UiaError> {
486
+ Ok(None)
487
+ }
488
+
489
+ #[cfg_attr(not(any(target_os = "windows", test)), allow(dead_code))]
490
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
491
+ enum NativeSetValueAction {
492
+ Value,
493
+ RangeValue,
494
+ }
495
+
496
+ #[cfg(any(target_os = "windows", test))]
497
+ fn native_set_value_actions_for_text(text: &str) -> Vec<NativeSetValueAction> {
498
+ let mut actions = vec![NativeSetValueAction::Value];
499
+ if text.trim().parse::<f64>().is_ok() {
500
+ actions.push(NativeSetValueAction::RangeValue);
501
+ }
502
+ actions
503
+ }
504
+
505
+ fn native_set_value_action_via(action: NativeSetValueAction) -> &'static str {
506
+ match action {
507
+ NativeSetValueAction::Value => "uia_value_pattern",
508
+ NativeSetValueAction::RangeValue => "uia_range_value_pattern",
509
+ }
510
+ }
511
+
512
+ #[cfg(target_os = "windows")]
513
+ fn try_windows_native_set_value_action(
514
+ element: &windows::Win32::UI::Accessibility::IUIAutomationElement,
515
+ action: NativeSetValueAction,
516
+ text: &str,
517
+ ) -> bool {
518
+ use windows::Win32::UI::Accessibility::{
519
+ IUIAutomationRangeValuePattern, IUIAutomationValuePattern, UIA_RangeValuePatternId,
520
+ UIA_ValuePatternId,
521
+ };
522
+
523
+ match action {
524
+ NativeSetValueAction::Value => {
525
+ let pattern = unsafe {
526
+ element.GetCurrentPatternAs::<IUIAutomationValuePattern>(UIA_ValuePatternId)
527
+ };
528
+ let value = windows::core::BSTR::from(text);
529
+ pattern
530
+ .map(|pattern| unsafe { pattern.SetValue(&value) }.is_ok())
531
+ .unwrap_or(false)
532
+ }
533
+ NativeSetValueAction::RangeValue => {
534
+ let Ok(value) = text.trim().parse::<f64>() else {
535
+ return false;
536
+ };
537
+ let pattern = unsafe {
538
+ element
539
+ .GetCurrentPatternAs::<IUIAutomationRangeValuePattern>(UIA_RangeValuePatternId)
540
+ };
541
+ pattern
542
+ .map(|pattern| unsafe { pattern.SetValue(value) }.is_ok())
543
+ .unwrap_or(false)
544
+ }
545
+ }
546
+ }
547
+
548
+ #[cfg(target_os = "windows")]
549
+ fn try_native_focus_descendant(window: &WindowRecord, stable: &str) -> Result<bool, UiaError> {
550
+ let element = match resolve_live_descendant_element(window, stable) {
551
+ Ok(element) => element,
552
+ Err(_) => return Ok(false),
553
+ };
554
+ Ok(unsafe { element.SetFocus() }.is_ok())
555
+ }
556
+
557
+ #[cfg(not(target_os = "windows"))]
558
+ fn try_native_focus_descendant(_window: &WindowRecord, _stable: &str) -> Result<bool, UiaError> {
559
+ Ok(false)
560
+ }
561
+
562
+ #[cfg(target_os = "windows")]
563
+ pub(crate) fn focus_top_level_window(window: &WindowRecord) -> HandlerResult {
564
+ let hwnd = parse_hwnd(&window.hwnd)?;
565
+ let ok = unsafe { win32::set_foreground_window(hwnd) };
566
+ if ok != 0 {
567
+ return Ok(serde_json::json!({ "focused": true }));
568
+ }
569
+
570
+ Err(UiaError::permission(format!(
571
+ "SetForegroundWindow failed for {}: {}",
572
+ window.hwnd,
573
+ std::io::Error::last_os_error()
574
+ )))
575
+ }
576
+
577
+ #[cfg(not(target_os = "windows"))]
578
+ pub(crate) fn focus_top_level_window(_window: &WindowRecord) -> HandlerResult {
579
+ Err(crate::errors::backend_unavailable())
580
+ }
581
+
582
+ #[cfg(target_os = "windows")]
583
+ fn post_click_descendant(window: &WindowRecord, bounds: &ElementBounds) -> HandlerResult {
584
+ let hwnd = parse_hwnd(&window.hwnd)?;
585
+ let point = client_point_for_bounds(hwnd, bounds)?;
586
+ let lparam = make_lparam(point.x, point.y);
587
+ let down =
588
+ unsafe { win32::post_message(hwnd, win32::WM_LBUTTONDOWN, win32::MK_LBUTTON, lparam) };
589
+ let up = unsafe { win32::post_message(hwnd, win32::WM_LBUTTONUP, 0, lparam) };
590
+ if down != 0 && up != 0 {
591
+ return Ok(serde_json::json!({ "clicked": true }));
592
+ }
593
+ Err(UiaError::permission(format!(
594
+ "PostMessage click failed for {}: {}",
595
+ window.hwnd,
596
+ std::io::Error::last_os_error()
597
+ )))
598
+ }
599
+
600
+ #[cfg(not(target_os = "windows"))]
601
+ fn post_click_descendant(_window: &WindowRecord, _bounds: &ElementBounds) -> HandlerResult {
602
+ Err(crate::errors::backend_unavailable())
603
+ }
604
+
605
+ #[cfg(target_os = "windows")]
606
+ fn parse_hwnd(value: &str) -> Result<isize, UiaError> {
607
+ let raw = value.strip_prefix("0x").unwrap_or(value);
608
+ isize::from_str_radix(raw, 16)
609
+ .map_err(|_| UiaError::invalid_input(format!("invalid window handle {value}")))
610
+ }
611
+
612
+ #[cfg(target_os = "windows")]
613
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
614
+ struct ClientPoint {
615
+ x: i32,
616
+ y: i32,
617
+ }
618
+
619
+ #[cfg(target_os = "windows")]
620
+ fn client_point_for_bounds(hwnd: isize, bounds: &ElementBounds) -> Result<ClientPoint, UiaError> {
621
+ let mut rect = win32::Rect::default();
622
+ let ok = unsafe { win32::get_window_rect(hwnd, &mut rect) };
623
+ if ok == 0 {
624
+ return Err(UiaError::permission(format!(
625
+ "GetWindowRect failed: {}",
626
+ std::io::Error::last_os_error()
627
+ )));
628
+ }
629
+ Ok(ClientPoint {
630
+ x: bounds.x + (bounds.width as i32 / 2) - rect.left,
631
+ y: bounds.y + (bounds.height as i32 / 2) - rect.top,
632
+ })
633
+ }
634
+
635
+ #[cfg(target_os = "windows")]
636
+ fn make_lparam(x: i32, y: i32) -> isize {
637
+ let x = (x as u16) as u32;
638
+ let y = (y as u16) as u32;
639
+ ((y << 16) | x) as isize
640
+ }
641
+
642
+ #[cfg(target_os = "windows")]
643
+ mod win32 {
644
+ pub const WM_LBUTTONDOWN: u32 = 0x0201;
645
+ pub const WM_LBUTTONUP: u32 = 0x0202;
646
+ pub const MK_LBUTTON: isize = 0x0001;
647
+
648
+ #[derive(Default)]
649
+ #[repr(C)]
650
+ pub struct Rect {
651
+ pub left: i32,
652
+ pub top: i32,
653
+ pub right: i32,
654
+ pub bottom: i32,
655
+ }
656
+
657
+ #[link(name = "user32")]
658
+ extern "system" {
659
+ #[link_name = "SetForegroundWindow"]
660
+ pub fn set_foreground_window(hwnd: isize) -> i32;
661
+ #[link_name = "GetWindowRect"]
662
+ pub fn get_window_rect(hwnd: isize, rect: *mut Rect) -> i32;
663
+ #[link_name = "PostMessageW"]
664
+ pub fn post_message(hwnd: isize, msg: u32, wparam: isize, lparam: isize) -> i32;
665
+ }
666
+ }
667
+
668
+ #[cfg(test)]
669
+ mod tests {
670
+ use super::*;
671
+ use crate::errors::IntoSidecarResponse;
672
+ use crate::tree::{ElementBounds, ElementRecord, WindowRecord};
673
+
674
+ #[test]
675
+ fn focus_response_includes_top_level_window_target_metadata() {
676
+ let response = focus_response_for_window(
677
+ &WindowRecord {
678
+ hwnd: "0x2a".into(),
679
+ pid: 42,
680
+ title: "Calculator".into(),
681
+ children: vec![],
682
+ },
683
+ "desktop-uia:pid-42:Window[0]",
684
+ );
685
+
686
+ assert_eq!(
687
+ response,
688
+ serde_json::json!({
689
+ "focused": true,
690
+ "via": "set_foreground_window",
691
+ "stable": "desktop-uia:pid-42:Window[0]",
692
+ "hwnd": "0x2a",
693
+ "pid": 42,
694
+ "title": "Calculator",
695
+ }),
696
+ );
697
+ }
698
+
699
+ #[test]
700
+ fn invoke_response_marks_top_level_window_activation() {
701
+ let response = invoke_response_for_window(
702
+ &WindowRecord {
703
+ hwnd: "0x2a".into(),
704
+ pid: 42,
705
+ title: "Calculator".into(),
706
+ children: vec![],
707
+ },
708
+ "desktop-uia:pid-42:Window[0]",
709
+ );
710
+
711
+ assert_eq!(
712
+ response,
713
+ serde_json::json!({
714
+ "invoked": true,
715
+ "via": "top_level_window",
716
+ "stable": "desktop-uia:pid-42:Window[0]",
717
+ "hwnd": "0x2a",
718
+ "pid": 42,
719
+ "title": "Calculator",
720
+ }),
721
+ );
722
+ }
723
+
724
+ #[test]
725
+ fn invoke_response_includes_descendant_target_metadata() {
726
+ let response = invoke_response_for_descendant(
727
+ &WindowRecord {
728
+ hwnd: "0x2a".into(),
729
+ pid: 42,
730
+ title: "Calculator".into(),
731
+ children: vec![],
732
+ },
733
+ &ElementRecord {
734
+ role: "Button".into(),
735
+ name: "Seven".into(),
736
+ value: None,
737
+ bounds: Some(ElementBounds {
738
+ x: 20,
739
+ y: 30,
740
+ width: 40,
741
+ height: 50,
742
+ }),
743
+ states: vec!["enabled".into()],
744
+ children: vec![],
745
+ },
746
+ "desktop-uia:pid-42:Window[0]/Button[1]",
747
+ "Window[0]/Button[1]",
748
+ "post_message",
749
+ );
750
+
751
+ assert_eq!(
752
+ response,
753
+ serde_json::json!({
754
+ "invoked": true,
755
+ "via": "post_message",
756
+ "stable": "desktop-uia:pid-42:Window[0]/Button[1]",
757
+ "hwnd": "0x2a",
758
+ "pid": 42,
759
+ "title": "Calculator",
760
+ "target": {
761
+ "role": "Button",
762
+ "name": "Seven",
763
+ "path": "Window[0]/Button[1]",
764
+ "bounds": {
765
+ "x": 20,
766
+ "y": 30,
767
+ "width": 40,
768
+ "height": 50,
769
+ },
770
+ },
771
+ }),
772
+ );
773
+ }
774
+
775
+ #[test]
776
+ fn native_invoke_actions_try_invoke_toggle_then_selection_item() {
777
+ assert_eq!(
778
+ native_invoke_actions(),
779
+ vec![
780
+ NativeInvokeAction::Invoke,
781
+ NativeInvokeAction::Toggle,
782
+ NativeInvokeAction::SelectionItem,
783
+ ],
784
+ );
785
+ }
786
+
787
+ #[test]
788
+ fn invoke_response_can_report_native_toggle_and_selection_item_patterns() {
789
+ let window = WindowRecord {
790
+ hwnd: "0x2a".into(),
791
+ pid: 42,
792
+ title: "Settings".into(),
793
+ children: vec![],
794
+ };
795
+ let element = ElementRecord {
796
+ role: "CheckBox".into(),
797
+ name: "Enable Sync".into(),
798
+ value: None,
799
+ bounds: None,
800
+ states: vec!["enabled".into()],
801
+ children: vec![],
802
+ };
803
+
804
+ let toggle = invoke_response_for_descendant(
805
+ &window,
806
+ &element,
807
+ "desktop-uia:pid-42:Window[0]/CheckBox[0]",
808
+ "Window[0]/CheckBox[0]",
809
+ native_invoke_action_via(NativeInvokeAction::Toggle),
810
+ );
811
+ let selection = invoke_response_for_descendant(
812
+ &window,
813
+ &element,
814
+ "desktop-uia:pid-42:Window[0]/CheckBox[0]",
815
+ "Window[0]/CheckBox[0]",
816
+ native_invoke_action_via(NativeInvokeAction::SelectionItem),
817
+ );
818
+
819
+ assert_eq!(toggle["via"], "uia_toggle_pattern");
820
+ assert_eq!(selection["via"], "uia_selection_item_pattern");
821
+ }
822
+
823
+ #[test]
824
+ fn descendant_refs_are_rejected_when_no_action_bounds_exist() {
825
+ let error = require_descendant_bounds(
826
+ &ElementRecord {
827
+ role: "Button".into(),
828
+ name: "Seven".into(),
829
+ value: None,
830
+ bounds: None,
831
+ states: vec!["enabled".into()],
832
+ children: vec![],
833
+ },
834
+ "desktop-uia:pid-42:Window[0]/Button[1]",
835
+ )
836
+ .expect_err("descendant action needs target bounds");
837
+
838
+ let response = Err::<serde_json::Value, _>(error).into_response(1, "uia_invoke".into());
839
+ let error = response.error.expect("error envelope");
840
+ assert_eq!(error.minimum_capability, "desktop-uia.not_invokable");
841
+ assert_eq!(
842
+ error.r#ref.as_deref(),
843
+ Some("desktop-uia:pid-42:Window[0]/Button[1]"),
844
+ );
845
+ }
846
+
847
+ #[test]
848
+ fn set_value_response_can_report_native_value_pattern() {
849
+ let response = set_value_response_for_descendant(
850
+ &WindowRecord {
851
+ hwnd: "0x2a".into(),
852
+ pid: 42,
853
+ title: "Notepad".into(),
854
+ children: vec![],
855
+ },
856
+ &ElementRecord {
857
+ role: "Edit".into(),
858
+ name: "Document".into(),
859
+ value: Some("old".into()),
860
+ bounds: None,
861
+ states: vec!["enabled".into(), "focusable".into()],
862
+ children: vec![],
863
+ },
864
+ "desktop-uia:pid-42:Window[0]/Edit[0]",
865
+ "Window[0]/Edit[0]",
866
+ "hello",
867
+ "uia_value_pattern",
868
+ );
869
+
870
+ assert_eq!(response["via"], "uia_value_pattern");
871
+ assert_eq!(response["target"]["value"], "old");
872
+ assert_eq!(response["chars"], 5);
873
+ }
874
+
875
+ #[test]
876
+ fn native_set_value_actions_include_range_value_for_numeric_text() {
877
+ assert_eq!(
878
+ native_set_value_actions_for_text("42.5"),
879
+ vec![
880
+ NativeSetValueAction::Value,
881
+ NativeSetValueAction::RangeValue
882
+ ],
883
+ );
884
+ assert_eq!(
885
+ native_set_value_actions_for_text("hello"),
886
+ vec![NativeSetValueAction::Value],
887
+ );
888
+ }
889
+
890
+ #[test]
891
+ fn set_value_response_can_report_native_range_value_pattern() {
892
+ let response = set_value_response_for_descendant(
893
+ &WindowRecord {
894
+ hwnd: "0x2a".into(),
895
+ pid: 42,
896
+ title: "Volume Mixer".into(),
897
+ children: vec![],
898
+ },
899
+ &ElementRecord {
900
+ role: "Slider".into(),
901
+ name: "Output Volume".into(),
902
+ value: Some("25".into()),
903
+ bounds: None,
904
+ states: vec!["enabled".into(), "focusable".into()],
905
+ children: vec![],
906
+ },
907
+ "desktop-uia:pid-42:Window[0]/Slider[0]",
908
+ "Window[0]/Slider[0]",
909
+ "50",
910
+ native_set_value_action_via(NativeSetValueAction::RangeValue),
911
+ );
912
+
913
+ assert_eq!(response["via"], "uia_range_value_pattern");
914
+ assert_eq!(response["chars"], 2);
915
+ }
916
+
917
+ #[test]
918
+ fn focus_response_can_report_native_set_focus() {
919
+ let response = focus_response_for_descendant(
920
+ &WindowRecord {
921
+ hwnd: "0x2a".into(),
922
+ pid: 42,
923
+ title: "Notepad".into(),
924
+ children: vec![],
925
+ },
926
+ &ElementRecord {
927
+ role: "Edit".into(),
928
+ name: "Document".into(),
929
+ value: None,
930
+ bounds: None,
931
+ states: vec!["enabled".into(), "focusable".into()],
932
+ children: vec![],
933
+ },
934
+ "desktop-uia:pid-42:Window[0]/Edit[0]",
935
+ "Window[0]/Edit[0]",
936
+ "uia_set_focus",
937
+ );
938
+
939
+ assert_eq!(response["via"], "uia_set_focus");
940
+ }
941
+
942
+ #[test]
943
+ fn launch_plan_uses_powershell_start_process() {
944
+ let plan = launch_plan_for_app("notepad", &["--safe-mode".into()], None);
945
+
946
+ assert_eq!(plan.program, "powershell.exe");
947
+ assert_eq!(
948
+ plan.args,
949
+ vec![
950
+ "-NoProfile",
951
+ "-NonInteractive",
952
+ "-Command",
953
+ "Start-Process -FilePath $args[0] -ArgumentList $args[1]",
954
+ "notepad",
955
+ "--safe-mode",
956
+ ],
957
+ );
958
+ }
959
+
960
+ #[test]
961
+ fn launch_plan_appends_debug_port_argument() {
962
+ let plan = launch_plan_for_app("Code.exe", &[], Some(9230));
963
+
964
+ assert_eq!(plan.program, "powershell.exe");
965
+ assert_eq!(
966
+ plan.args,
967
+ vec![
968
+ "-NoProfile",
969
+ "-NonInteractive",
970
+ "-Command",
971
+ "Start-Process -FilePath $args[0] -ArgumentList $args[1]",
972
+ "Code.exe",
973
+ "--remote-debugging-port=9230",
974
+ ],
975
+ );
976
+ }
977
+ }