@mseep/clawdcursor 1.5.5
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.
- package/CHANGELOG.md +2264 -0
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/SECURITY.md +44 -0
- package/SKILL.md +503 -0
- package/dist/core/agent-loop/agent.d.ts +42 -0
- package/dist/core/agent-loop/agent.js +1023 -0
- package/dist/core/agent-loop/agent.js.map +1 -0
- package/dist/core/agent-loop/batch-tool.d.ts +25 -0
- package/dist/core/agent-loop/batch-tool.js +218 -0
- package/dist/core/agent-loop/batch-tool.js.map +1 -0
- package/dist/core/agent-loop/coord-scale.d.ts +72 -0
- package/dist/core/agent-loop/coord-scale.js +89 -0
- package/dist/core/agent-loop/coord-scale.js.map +1 -0
- package/dist/core/agent-loop/focus-guard.d.ts +24 -0
- package/dist/core/agent-loop/focus-guard.js +29 -0
- package/dist/core/agent-loop/focus-guard.js.map +1 -0
- package/dist/core/agent-loop/project-mcp.d.ts +97 -0
- package/dist/core/agent-loop/project-mcp.js +253 -0
- package/dist/core/agent-loop/project-mcp.js.map +1 -0
- package/dist/core/agent-loop/prompt.d.ts +45 -0
- package/dist/core/agent-loop/prompt.js +426 -0
- package/dist/core/agent-loop/prompt.js.map +1 -0
- package/dist/core/agent-loop/tool-meta.d.ts +93 -0
- package/dist/core/agent-loop/tool-meta.js +651 -0
- package/dist/core/agent-loop/tool-meta.js.map +1 -0
- package/dist/core/agent-loop/tools.d.ts +38 -0
- package/dist/core/agent-loop/tools.js +2134 -0
- package/dist/core/agent-loop/tools.js.map +1 -0
- package/dist/core/agent-loop/types.d.ts +170 -0
- package/dist/core/agent-loop/types.js +12 -0
- package/dist/core/agent-loop/types.js.map +1 -0
- package/dist/core/agent.d.ts +51 -0
- package/dist/core/agent.js +245 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/app-categories.d.ts +67 -0
- package/dist/core/app-categories.js +108 -0
- package/dist/core/app-categories.js.map +1 -0
- package/dist/core/banner.d.ts +70 -0
- package/dist/core/banner.js +245 -0
- package/dist/core/banner.js.map +1 -0
- package/dist/core/classify/capability.d.ts +45 -0
- package/dist/core/classify/capability.js +78 -0
- package/dist/core/classify/capability.js.map +1 -0
- package/dist/core/decompose/llm-decomposer.d.ts +35 -0
- package/dist/core/decompose/llm-decomposer.js +156 -0
- package/dist/core/decompose/llm-decomposer.js.map +1 -0
- package/dist/core/decompose/parser.d.ts +27 -0
- package/dist/core/decompose/parser.js +101 -0
- package/dist/core/decompose/parser.js.map +1 -0
- package/dist/core/observability/correlation.d.ts +19 -0
- package/dist/core/observability/correlation.js +36 -0
- package/dist/core/observability/correlation.js.map +1 -0
- package/dist/core/observability/cost-meter.d.ts +51 -0
- package/dist/core/observability/cost-meter.js +134 -0
- package/dist/core/observability/cost-meter.js.map +1 -0
- package/dist/core/observability/logger.d.ts +61 -0
- package/dist/core/observability/logger.js +550 -0
- package/dist/core/observability/logger.js.map +1 -0
- package/dist/core/router/aliases.d.ts +50 -0
- package/dist/core/router/aliases.js +104 -0
- package/dist/core/router/aliases.js.map +1 -0
- package/dist/core/router/normalize.d.ts +41 -0
- package/dist/core/router/normalize.js +80 -0
- package/dist/core/router/normalize.js.map +1 -0
- package/dist/core/safety.d.ts +126 -0
- package/dist/core/safety.js +568 -0
- package/dist/core/safety.js.map +1 -0
- package/dist/core/sense/a11y-resolver.d.ts +73 -0
- package/dist/core/sense/a11y-resolver.js +76 -0
- package/dist/core/sense/a11y-resolver.js.map +1 -0
- package/dist/core/sense/fingerprint.d.ts +41 -0
- package/dist/core/sense/fingerprint.js +123 -0
- package/dist/core/sense/fingerprint.js.map +1 -0
- package/dist/core/sense/rank.d.ts +70 -0
- package/dist/core/sense/rank.js +192 -0
- package/dist/core/sense/rank.js.map +1 -0
- package/dist/core/sense/reactive-check.d.ts +40 -0
- package/dist/core/sense/reactive-check.js +48 -0
- package/dist/core/sense/reactive-check.js.map +1 -0
- package/dist/core/sense/snapshot.d.ts +19 -0
- package/dist/core/sense/snapshot.js +100 -0
- package/dist/core/sense/snapshot.js.map +1 -0
- package/dist/core/sense/types.d.ts +66 -0
- package/dist/core/sense/types.js +9 -0
- package/dist/core/sense/types.js.map +1 -0
- package/dist/core/sense/ui-map-anchors.d.ts +7 -0
- package/dist/core/sense/ui-map-anchors.js +24 -0
- package/dist/core/sense/ui-map-anchors.js.map +1 -0
- package/dist/core/sense/ui-map-elements.d.ts +5 -0
- package/dist/core/sense/ui-map-elements.js +33 -0
- package/dist/core/sense/ui-map-elements.js.map +1 -0
- package/dist/core/sense/ui-map-find.d.ts +56 -0
- package/dist/core/sense/ui-map-find.js +153 -0
- package/dist/core/sense/ui-map-find.js.map +1 -0
- package/dist/core/sense/ui-map-fuse.d.ts +4 -0
- package/dist/core/sense/ui-map-fuse.js +44 -0
- package/dist/core/sense/ui-map-fuse.js.map +1 -0
- package/dist/core/sense/ui-map-geom.d.ts +3 -0
- package/dist/core/sense/ui-map-geom.js +16 -0
- package/dist/core/sense/ui-map-geom.js.map +1 -0
- package/dist/core/sense/ui-map-holder.d.ts +58 -0
- package/dist/core/sense/ui-map-holder.js +87 -0
- package/dist/core/sense/ui-map-holder.js.map +1 -0
- package/dist/core/sense/ui-map-normalize.d.ts +19 -0
- package/dist/core/sense/ui-map-normalize.js +65 -0
- package/dist/core/sense/ui-map-normalize.js.map +1 -0
- package/dist/core/sense/ui-map-render.d.ts +4 -0
- package/dist/core/sense/ui-map-render.js +34 -0
- package/dist/core/sense/ui-map-render.js.map +1 -0
- package/dist/core/sense/ui-map-resolve.d.ts +41 -0
- package/dist/core/sense/ui-map-resolve.js +59 -0
- package/dist/core/sense/ui-map-resolve.js.map +1 -0
- package/dist/core/sense/ui-map-types.d.ts +66 -0
- package/dist/core/sense/ui-map-types.js +11 -0
- package/dist/core/sense/ui-map-types.js.map +1 -0
- package/dist/core/sense/ui-map.d.ts +29 -0
- package/dist/core/sense/ui-map.js +113 -0
- package/dist/core/sense/ui-map.js.map +1 -0
- package/dist/core/verify/assertions.d.ts +132 -0
- package/dist/core/verify/assertions.js +284 -0
- package/dist/core/verify/assertions.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/browser-config.d.ts +36 -0
- package/dist/llm/browser-config.js +83 -0
- package/dist/llm/browser-config.js.map +1 -0
- package/dist/llm/client.d.ts +268 -0
- package/dist/llm/client.js +1094 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/config.d.ts +79 -0
- package/dist/llm/config.js +375 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/credentials.d.ts +35 -0
- package/dist/llm/credentials.js +491 -0
- package/dist/llm/credentials.js.map +1 -0
- package/dist/llm/external-creds.d.ts +42 -0
- package/dist/llm/external-creds.js +169 -0
- package/dist/llm/external-creds.js.map +1 -0
- package/dist/llm/providers.d.ts +123 -0
- package/dist/llm/providers.js +717 -0
- package/dist/llm/providers.js.map +1 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +147 -0
- package/dist/paths.js.map +1 -0
- package/dist/platform/accessibility.d.ts +139 -0
- package/dist/platform/accessibility.js +670 -0
- package/dist/platform/accessibility.js.map +1 -0
- package/dist/platform/cdp-driver.d.ts +318 -0
- package/dist/platform/cdp-driver.js +1179 -0
- package/dist/platform/cdp-driver.js.map +1 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +69 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/keys.d.ts +17 -0
- package/dist/platform/keys.js +129 -0
- package/dist/platform/keys.js.map +1 -0
- package/dist/platform/launch-poll.d.ts +101 -0
- package/dist/platform/launch-poll.js +177 -0
- package/dist/platform/launch-poll.js.map +1 -0
- package/dist/platform/linux.d.ts +173 -0
- package/dist/platform/linux.js +1253 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +136 -0
- package/dist/platform/macos.js +976 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/native-desktop.d.ts +145 -0
- package/dist/platform/native-desktop.js +936 -0
- package/dist/platform/native-desktop.js.map +1 -0
- package/dist/platform/native-helper.d.ts +130 -0
- package/dist/platform/native-helper.js +592 -0
- package/dist/platform/native-helper.js.map +1 -0
- package/dist/platform/ocr-engine.d.ts +78 -0
- package/dist/platform/ocr-engine.js +363 -0
- package/dist/platform/ocr-engine.js.map +1 -0
- package/dist/platform/ps-runner.d.ts +28 -0
- package/dist/platform/ps-runner.js +228 -0
- package/dist/platform/ps-runner.js.map +1 -0
- package/dist/platform/types.d.ts +397 -0
- package/dist/platform/types.js +15 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/platform/uri-handler.d.ts +75 -0
- package/dist/platform/uri-handler.js +273 -0
- package/dist/platform/uri-handler.js.map +1 -0
- package/dist/platform/wayland-backend.d.ts +53 -0
- package/dist/platform/wayland-backend.js +348 -0
- package/dist/platform/wayland-backend.js.map +1 -0
- package/dist/platform/windows.d.ts +232 -0
- package/dist/platform/windows.js +1210 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/postbuild.d.ts +10 -0
- package/dist/postbuild.js +98 -0
- package/dist/postbuild.js.map +1 -0
- package/dist/schema/snapshot.d.ts +33 -0
- package/dist/schema/snapshot.js +90 -0
- package/dist/schema/snapshot.js.map +1 -0
- package/dist/shortcuts.d.ts +30 -0
- package/dist/shortcuts.js +261 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/surface/cli.d.ts +7 -0
- package/dist/surface/cli.js +1556 -0
- package/dist/surface/cli.js.map +1 -0
- package/dist/surface/dashboard.d.ts +8 -0
- package/dist/surface/dashboard.js +1193 -0
- package/dist/surface/dashboard.js.map +1 -0
- package/dist/surface/doctor.d.ts +29 -0
- package/dist/surface/doctor.js +1514 -0
- package/dist/surface/doctor.js.map +1 -0
- package/dist/surface/format.d.ts +10 -0
- package/dist/surface/format.js +37 -0
- package/dist/surface/format.js.map +1 -0
- package/dist/surface/http-utility.d.ts +65 -0
- package/dist/surface/http-utility.js +336 -0
- package/dist/surface/http-utility.js.map +1 -0
- package/dist/surface/mcp-server.d.ts +91 -0
- package/dist/surface/mcp-server.js +280 -0
- package/dist/surface/mcp-server.js.map +1 -0
- package/dist/surface/onboarding.d.ts +15 -0
- package/dist/surface/onboarding.js +184 -0
- package/dist/surface/onboarding.js.map +1 -0
- package/dist/surface/pidfile.d.ts +79 -0
- package/dist/surface/pidfile.js +263 -0
- package/dist/surface/pidfile.js.map +1 -0
- package/dist/surface/readiness.d.ts +45 -0
- package/dist/surface/readiness.js +230 -0
- package/dist/surface/readiness.js.map +1 -0
- package/dist/surface/report.d.ts +68 -0
- package/dist/surface/report.js +341 -0
- package/dist/surface/report.js.map +1 -0
- package/dist/surface/skill-register.d.ts +14 -0
- package/dist/surface/skill-register.js +150 -0
- package/dist/surface/skill-register.js.map +1 -0
- package/dist/surface/version.d.ts +6 -0
- package/dist/surface/version.js +27 -0
- package/dist/surface/version.js.map +1 -0
- package/dist/tools/a11y.d.ts +8 -0
- package/dist/tools/a11y.js +545 -0
- package/dist/tools/a11y.js.map +1 -0
- package/dist/tools/a11y_depth.d.ts +19 -0
- package/dist/tools/a11y_depth.js +455 -0
- package/dist/tools/a11y_depth.js.map +1 -0
- package/dist/tools/agent.d.ts +15 -0
- package/dist/tools/agent.js +248 -0
- package/dist/tools/agent.js.map +1 -0
- package/dist/tools/batch.d.ts +46 -0
- package/dist/tools/batch.js +230 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/cdp.d.ts +8 -0
- package/dist/tools/cdp.js +233 -0
- package/dist/tools/cdp.js.map +1 -0
- package/dist/tools/compact.d.ts +63 -0
- package/dist/tools/compact.js +418 -0
- package/dist/tools/compact.js.map +1 -0
- package/dist/tools/cost-class.d.ts +38 -0
- package/dist/tools/cost-class.js +117 -0
- package/dist/tools/cost-class.js.map +1 -0
- package/dist/tools/desktop.d.ts +9 -0
- package/dist/tools/desktop.js +346 -0
- package/dist/tools/desktop.js.map +1 -0
- package/dist/tools/electron_bridge.d.ts +41 -0
- package/dist/tools/electron_bridge.js +261 -0
- package/dist/tools/electron_bridge.js.map +1 -0
- package/dist/tools/extras.d.ts +22 -0
- package/dist/tools/extras.js +942 -0
- package/dist/tools/extras.js.map +1 -0
- package/dist/tools/favorites.d.ts +13 -0
- package/dist/tools/favorites.js +137 -0
- package/dist/tools/favorites.js.map +1 -0
- package/dist/tools/introspection.d.ts +13 -0
- package/dist/tools/introspection.js +55 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/ocr.d.ts +8 -0
- package/dist/tools/ocr.js +66 -0
- package/dist/tools/ocr.js.map +1 -0
- package/dist/tools/orchestration.d.ts +7 -0
- package/dist/tools/orchestration.js +377 -0
- package/dist/tools/orchestration.js.map +1 -0
- package/dist/tools/playbooks/extract-compose.d.ts +22 -0
- package/dist/tools/playbooks/extract-compose.js +85 -0
- package/dist/tools/playbooks/extract-compose.js.map +1 -0
- package/dist/tools/playbooks/find-replace.d.ts +11 -0
- package/dist/tools/playbooks/find-replace.js +56 -0
- package/dist/tools/playbooks/find-replace.js.map +1 -0
- package/dist/tools/playbooks/index.d.ts +63 -0
- package/dist/tools/playbooks/index.js +70 -0
- package/dist/tools/playbooks/index.js.map +1 -0
- package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
- package/dist/tools/playbooks/keys-blocklist.js +89 -0
- package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
- package/dist/tools/registry.d.ts +40 -0
- package/dist/tools/registry.js +560 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/safety-gate.d.ts +16 -0
- package/dist/tools/safety-gate.js +70 -0
- package/dist/tools/safety-gate.js.map +1 -0
- package/dist/tools/scheduler.d.ts +76 -0
- package/dist/tools/scheduler.js +413 -0
- package/dist/tools/scheduler.js.map +1 -0
- package/dist/tools/shortcuts.d.ts +13 -0
- package/dist/tools/shortcuts.js +205 -0
- package/dist/tools/shortcuts.js.map +1 -0
- package/dist/tools/smart.d.ts +15 -0
- package/dist/tools/smart.js +785 -0
- package/dist/tools/smart.js.map +1 -0
- package/dist/tools/types.d.ts +174 -0
- package/dist/tools/types.js +67 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/window-text.d.ts +15 -0
- package/dist/tools/window-text.js +39 -0
- package/dist/tools/window-text.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +41 -0
- package/dist/types.js.map +1 -0
- package/native/Package.swift +38 -0
- package/native/README.md +113 -0
- package/native/Sources/ClawdCursorHelper/main.swift +602 -0
- package/native/Sources/ClawdCursorHost/main.swift +182 -0
- package/native/Sources/PermissionCheck/main.swift +53 -0
- package/native/Sources/ScreenshotHelper/main.swift +219 -0
- package/native/build.sh +139 -0
- package/native/entitlements.plist +12 -0
- package/package.json +115 -0
- package/scripts/banner.ps1 +112 -0
- package/scripts/coord-accuracy.ps1 +140 -0
- package/scripts/coord-uwp.ps1 +80 -0
- package/scripts/edge-glow.ps1 +180 -0
- package/scripts/find-element.ps1 +198 -0
- package/scripts/get-foreground-window.ps1 +71 -0
- package/scripts/get-screen-context.ps1 +183 -0
- package/scripts/get-windows.ps1 +66 -0
- package/scripts/install-panic-hotkey.ps1 +46 -0
- package/scripts/interact-element.ps1 +431 -0
- package/scripts/invoke-element.ps1 +314 -0
- package/scripts/linux/atspi-bridge.py +356 -0
- package/scripts/linux/ocr-recognize.py +154 -0
- package/scripts/mac/_window-picker.jxa +163 -0
- package/scripts/mac/find-element.jxa +0 -0
- package/scripts/mac/find-element.sh +161 -0
- package/scripts/mac/focus-window.jxa +284 -0
- package/scripts/mac/get-focused-element.jxa +102 -0
- package/scripts/mac/get-foreground-window.jxa +173 -0
- package/scripts/mac/get-screen-context.jxa +197 -0
- package/scripts/mac/get-ui-tree.sh +141 -0
- package/scripts/mac/get-windows.jxa +117 -0
- package/scripts/mac/interact-element.sh +235 -0
- package/scripts/mac/invoke-element.jxa +408 -0
- package/scripts/mac/ocr-recognize.swift +124 -0
- package/scripts/ocr-recognize.ps1 +102 -0
- package/scripts/postinstall-native.js +48 -0
- package/scripts/ps-bridge.ps1 +830 -0
- package/scripts/smoke-mcp.ps1 +119 -0
- package/scripts/sync-version.ts +178 -0
- package/scripts/verify-install.js +81 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Finds a UI element and invokes an action on it using UIA Patterns.
|
|
4
|
+
.PARAMETER AutomationId
|
|
5
|
+
Find the element by AutomationId (exact match).
|
|
6
|
+
.PARAMETER Name
|
|
7
|
+
Find the element by Name (exact match). Used if AutomationId is not specified.
|
|
8
|
+
.PARAMETER ControlType
|
|
9
|
+
Optional. Filter by ControlType to narrow the search (e.g. "Button", "Edit").
|
|
10
|
+
.PARAMETER ProcessId
|
|
11
|
+
Required. The process ID of the target window.
|
|
12
|
+
.PARAMETER Action
|
|
13
|
+
The action to perform: "click", "set-value", "get-value", "focus", "expand", "collapse", "toggle", "select".
|
|
14
|
+
.PARAMETER Value
|
|
15
|
+
The value to set (only used with "set-value" action).
|
|
16
|
+
#>
|
|
17
|
+
param(
|
|
18
|
+
[string]$AutomationId = "",
|
|
19
|
+
[string]$Name = "",
|
|
20
|
+
[string]$ControlType = "",
|
|
21
|
+
[Parameter(Mandatory=$true)]
|
|
22
|
+
[int]$ProcessId,
|
|
23
|
+
[Parameter(Mandatory=$true)]
|
|
24
|
+
[ValidateSet("click", "set-value", "get-value", "focus", "expand", "collapse", "toggle", "select")]
|
|
25
|
+
[string]$Action,
|
|
26
|
+
[string]$Value = ""
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
31
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
32
|
+
} catch {
|
|
33
|
+
[Console]::Out.Write((@{ success = $false; error = "Failed to load UI Automation assemblies: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
|
|
34
|
+
exit 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
$ErrorActionPreference = 'Stop'
|
|
38
|
+
|
|
39
|
+
# Control type mapping
|
|
40
|
+
$ctMap = @{
|
|
41
|
+
"Button" = [System.Windows.Automation.ControlType]::Button
|
|
42
|
+
"CheckBox" = [System.Windows.Automation.ControlType]::CheckBox
|
|
43
|
+
"ComboBox" = [System.Windows.Automation.ControlType]::ComboBox
|
|
44
|
+
"Custom" = [System.Windows.Automation.ControlType]::Custom
|
|
45
|
+
"DataItem" = [System.Windows.Automation.ControlType]::DataItem
|
|
46
|
+
"Document" = [System.Windows.Automation.ControlType]::Document
|
|
47
|
+
"Edit" = [System.Windows.Automation.ControlType]::Edit
|
|
48
|
+
"Group" = [System.Windows.Automation.ControlType]::Group
|
|
49
|
+
"Hyperlink" = [System.Windows.Automation.ControlType]::Hyperlink
|
|
50
|
+
"List" = [System.Windows.Automation.ControlType]::List
|
|
51
|
+
"ListItem" = [System.Windows.Automation.ControlType]::ListItem
|
|
52
|
+
"Menu" = [System.Windows.Automation.ControlType]::Menu
|
|
53
|
+
"MenuBar" = [System.Windows.Automation.ControlType]::MenuBar
|
|
54
|
+
"MenuItem" = [System.Windows.Automation.ControlType]::MenuItem
|
|
55
|
+
"Pane" = [System.Windows.Automation.ControlType]::Pane
|
|
56
|
+
"RadioButton" = [System.Windows.Automation.ControlType]::RadioButton
|
|
57
|
+
"ScrollBar" = [System.Windows.Automation.ControlType]::ScrollBar
|
|
58
|
+
"Slider" = [System.Windows.Automation.ControlType]::Slider
|
|
59
|
+
"Spinner" = [System.Windows.Automation.ControlType]::Spinner
|
|
60
|
+
"SplitButton" = [System.Windows.Automation.ControlType]::SplitButton
|
|
61
|
+
"Tab" = [System.Windows.Automation.ControlType]::Tab
|
|
62
|
+
"TabItem" = [System.Windows.Automation.ControlType]::TabItem
|
|
63
|
+
"Text" = [System.Windows.Automation.ControlType]::Text
|
|
64
|
+
"ToolBar" = [System.Windows.Automation.ControlType]::ToolBar
|
|
65
|
+
"Tree" = [System.Windows.Automation.ControlType]::Tree
|
|
66
|
+
"TreeItem" = [System.Windows.Automation.ControlType]::TreeItem
|
|
67
|
+
"Window" = [System.Windows.Automation.ControlType]::Window
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
$root = [System.Windows.Automation.AutomationElement]::RootElement
|
|
72
|
+
|
|
73
|
+
# Find the target window
|
|
74
|
+
$procCondition = New-Object System.Windows.Automation.PropertyCondition(
|
|
75
|
+
[System.Windows.Automation.AutomationElement]::ProcessIdProperty,
|
|
76
|
+
$ProcessId
|
|
77
|
+
)
|
|
78
|
+
$window = $root.FindFirst(
|
|
79
|
+
[System.Windows.Automation.TreeScope]::Children,
|
|
80
|
+
$procCondition
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if ($null -eq $window) {
|
|
84
|
+
[Console]::Out.Write((@{ success = $false; error = "No window found for ProcessId $ProcessId" } | ConvertTo-Json -Compress))
|
|
85
|
+
exit 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Build condition (without name — fuzzy name matching done below)
|
|
89
|
+
$conditions = @()
|
|
90
|
+
if ($AutomationId -ne "") {
|
|
91
|
+
$conditions += New-Object System.Windows.Automation.PropertyCondition(
|
|
92
|
+
[System.Windows.Automation.AutomationElement]::AutomationIdProperty, $AutomationId
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
if ($ControlType -ne "" -and $ctMap.ContainsKey($ControlType)) {
|
|
96
|
+
$conditions += New-Object System.Windows.Automation.PropertyCondition(
|
|
97
|
+
[System.Windows.Automation.AutomationElement]::ControlTypeProperty, $ctMap[$ControlType]
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
if ($conditions.Count -eq 0 -and $Name -eq "") {
|
|
101
|
+
[Console]::Out.Write((@{ success = $false; error = "Must specify at least -AutomationId or -Name" } | ConvertTo-Json -Compress))
|
|
102
|
+
exit 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$searchCondition = if ($conditions.Count -eq 0) { [System.Windows.Automation.Condition]::TrueCondition }
|
|
106
|
+
elseif ($conditions.Count -eq 1) { $conditions[0] }
|
|
107
|
+
else { New-Object System.Windows.Automation.AndCondition([System.Windows.Automation.Condition[]]$conditions) }
|
|
108
|
+
|
|
109
|
+
$element = $null
|
|
110
|
+
|
|
111
|
+
# Fast path: exact automationId match
|
|
112
|
+
if ($AutomationId -ne "" -and $conditions.Count -gt 0) {
|
|
113
|
+
$element = $window.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $searchCondition)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Fuzzy name match: strip keyboard shortcut suffix ("Save\tCtrl+S" → "save")
|
|
117
|
+
if ($null -eq $element -and $Name -ne "") {
|
|
118
|
+
$nameLower = $Name.ToLower()
|
|
119
|
+
$candidates = $window.FindAll([System.Windows.Automation.TreeScope]::Descendants, $searchCondition)
|
|
120
|
+
# First pass: exact stripped match
|
|
121
|
+
foreach ($el in $candidates) {
|
|
122
|
+
try {
|
|
123
|
+
$elName = ($el.Current.Name -replace '\t.*$', '').Trim().ToLower()
|
|
124
|
+
if ($elName -eq $nameLower -and $elName.Length -gt 0) { $element = $el; break }
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
# Second pass: contains match
|
|
128
|
+
if ($null -eq $element) {
|
|
129
|
+
foreach ($el in $candidates) {
|
|
130
|
+
try {
|
|
131
|
+
$elName = ($el.Current.Name -replace '\t.*$', '').Trim().ToLower()
|
|
132
|
+
if ($elName.Length -gt 0 -and ($elName.Contains($nameLower) -or $nameLower.Contains($elName))) {
|
|
133
|
+
$element = $el; break
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ($null -eq $element) {
|
|
141
|
+
$searchDesc = ""
|
|
142
|
+
if ($AutomationId -ne "") { $searchDesc += "AutomationId='$AutomationId' " }
|
|
143
|
+
if ($Name -ne "") { $searchDesc += "Name='$Name' " }
|
|
144
|
+
if ($ControlType -ne "") { $searchDesc += "ControlType='$ControlType' " }
|
|
145
|
+
[Console]::Out.Write((@{ success = $false; error = "Element not found: $($searchDesc.Trim())" } | ConvertTo-Json -Compress))
|
|
146
|
+
exit 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Execute the requested action
|
|
150
|
+
switch ($Action) {
|
|
151
|
+
"click" {
|
|
152
|
+
# Some web/Electron buttons advertise InvokePattern but block on Invoke()
|
|
153
|
+
# without ever throwing — caller would hang indefinitely. Wrap the pattern
|
|
154
|
+
# call in a Task with a 2s timeout, then fall through to the bounds-fallback
|
|
155
|
+
# JSON the legacy catch path already produces. See issue #71.
|
|
156
|
+
$rect = $element.Current.BoundingRectangle
|
|
157
|
+
$clickX = [int]($rect.X + $rect.Width / 2)
|
|
158
|
+
$clickY = [int]($rect.Y + $rect.Height / 2)
|
|
159
|
+
|
|
160
|
+
# Result is mutated by the Task action via reference (PSCustomObject).
|
|
161
|
+
# Closure capture (.GetNewClosure()) keeps $element and $invokeResult
|
|
162
|
+
# references valid inside the Task delegate.
|
|
163
|
+
$invokeResult = [PSCustomObject]@{ Method = $null; Error = $null }
|
|
164
|
+
$localElement = $element
|
|
165
|
+
$invokeBlock = {
|
|
166
|
+
try {
|
|
167
|
+
$p = $localElement.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)
|
|
168
|
+
$p.Invoke()
|
|
169
|
+
$invokeResult.Method = "InvokePattern"
|
|
170
|
+
} catch {
|
|
171
|
+
try {
|
|
172
|
+
$p = $localElement.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)
|
|
173
|
+
$p.Toggle()
|
|
174
|
+
$invokeResult.Method = "TogglePattern"
|
|
175
|
+
} catch {
|
|
176
|
+
$invokeResult.Error = $_.Exception.Message
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}.GetNewClosure()
|
|
180
|
+
|
|
181
|
+
$task = [System.Threading.Tasks.Task]::Run([System.Action]$invokeBlock)
|
|
182
|
+
$completedInTime = $task.Wait(2000)
|
|
183
|
+
|
|
184
|
+
if ($completedInTime -and $invokeResult.Method) {
|
|
185
|
+
# Pattern call returned within timeout — success
|
|
186
|
+
[Console]::Out.Write((@{ success = $true; action = "click"; method = $invokeResult.Method } | ConvertTo-Json -Compress))
|
|
187
|
+
} elseif ($completedInTime) {
|
|
188
|
+
# Pattern call returned but threw on both Invoke and Toggle — bounds fallback (legacy behaviour)
|
|
189
|
+
[Console]::Out.Write((@{
|
|
190
|
+
success = $false
|
|
191
|
+
action = "click"
|
|
192
|
+
error = "No InvokePattern or TogglePattern supported. Use coordinate click."
|
|
193
|
+
clickPoint = @{ x = $clickX; y = $clickY }
|
|
194
|
+
} | ConvertTo-Json -Depth 5 -Compress))
|
|
195
|
+
} else {
|
|
196
|
+
# Hung past 2s — element advertises a pattern but does not honour it (typical for
|
|
197
|
+
# React/Chromium buttons wired to onclick). The Task is left to finish on its own;
|
|
198
|
+
# the PowerShell process will exit shortly and clean it up.
|
|
199
|
+
[Console]::Out.Write((@{
|
|
200
|
+
success = $false
|
|
201
|
+
action = "click"
|
|
202
|
+
error = "InvokePattern timed out after 2s (element advertises pattern but blocks on Invoke). Use coordinate click."
|
|
203
|
+
clickPoint = @{ x = $clickX; y = $clickY }
|
|
204
|
+
} | ConvertTo-Json -Depth 5 -Compress))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
"set-value" {
|
|
208
|
+
if ($Value -eq "") {
|
|
209
|
+
[Console]::Out.Write((@{ success = $false; error = "Value parameter required for set-value action" } | ConvertTo-Json -Compress))
|
|
210
|
+
exit 0
|
|
211
|
+
}
|
|
212
|
+
# Try ValuePattern directly on the located element first.
|
|
213
|
+
# If it fails (common for ComboBox composites where the ValuePattern
|
|
214
|
+
# lives on the inner Edit child, not the ComboBox wrapper), fall back
|
|
215
|
+
# to the first Edit child — this covers the Win11 Save-As filename
|
|
216
|
+
# field which is a ComboBox containing an Edit (automation-id 1001).
|
|
217
|
+
$targetElement = $element
|
|
218
|
+
$setError = $null
|
|
219
|
+
try {
|
|
220
|
+
$pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
|
|
221
|
+
$pattern.SetValue($Value)
|
|
222
|
+
} catch {
|
|
223
|
+
$setError = $_.Exception.Message
|
|
224
|
+
# Fallback: locate the first Edit child and try there.
|
|
225
|
+
try {
|
|
226
|
+
$editCond = New-Object System.Windows.Automation.PropertyCondition(
|
|
227
|
+
[System.Windows.Automation.AutomationElement]::ControlTypeProperty,
|
|
228
|
+
[System.Windows.Automation.ControlType]::Edit
|
|
229
|
+
)
|
|
230
|
+
$editChild = $targetElement.FindFirst([System.Windows.Automation.TreeScope]::Children, $editCond)
|
|
231
|
+
if ($null -ne $editChild) {
|
|
232
|
+
$childPattern = $editChild.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
|
|
233
|
+
$childPattern.SetValue($Value)
|
|
234
|
+
$setError = $null # success via child
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
$setError = "ValuePattern not supported on element or inner Edit child: $($_.Exception.Message)"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if ($null -eq $setError) {
|
|
241
|
+
[Console]::Out.Write((@{ success = $true; action = "set-value"; value = $Value } | ConvertTo-Json -Compress))
|
|
242
|
+
} else {
|
|
243
|
+
[Console]::Out.Write((@{ success = $false; error = $setError } | ConvertTo-Json -Compress))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
"get-value" {
|
|
247
|
+
try {
|
|
248
|
+
$pattern = $element.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
|
|
249
|
+
$val = $pattern.Current.Value
|
|
250
|
+
[Console]::Out.Write((@{ success = $true; action = "get-value"; value = $val } | ConvertTo-Json -Compress))
|
|
251
|
+
} catch {
|
|
252
|
+
# Try TextPattern as fallback
|
|
253
|
+
try {
|
|
254
|
+
$textPattern = $element.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern)
|
|
255
|
+
$range = $textPattern.DocumentRange
|
|
256
|
+
$val = $range.GetText(-1)
|
|
257
|
+
[Console]::Out.Write((@{ success = $true; action = "get-value"; value = $val; method = "TextPattern" } | ConvertTo-Json -Compress))
|
|
258
|
+
} catch {
|
|
259
|
+
# Last resort: return the element's Name
|
|
260
|
+
$val = $element.Current.Name
|
|
261
|
+
[Console]::Out.Write((@{ success = $true; action = "get-value"; value = $val; method = "NameProperty" } | ConvertTo-Json -Compress))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
"focus" {
|
|
266
|
+
try {
|
|
267
|
+
$element.SetFocus()
|
|
268
|
+
[Console]::Out.Write((@{ success = $true; action = "focus" } | ConvertTo-Json -Compress))
|
|
269
|
+
} catch {
|
|
270
|
+
[Console]::Out.Write((@{ success = $false; error = "Failed to set focus: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
"expand" {
|
|
274
|
+
try {
|
|
275
|
+
$pattern = $element.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
|
|
276
|
+
$pattern.Expand()
|
|
277
|
+
[Console]::Out.Write((@{ success = $true; action = "expand" } | ConvertTo-Json -Compress))
|
|
278
|
+
} catch {
|
|
279
|
+
[Console]::Out.Write((@{ success = $false; error = "ExpandCollapsePattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
"collapse" {
|
|
283
|
+
try {
|
|
284
|
+
$pattern = $element.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
|
|
285
|
+
$pattern.Collapse()
|
|
286
|
+
[Console]::Out.Write((@{ success = $true; action = "collapse" } | ConvertTo-Json -Compress))
|
|
287
|
+
} catch {
|
|
288
|
+
[Console]::Out.Write((@{ success = $false; error = "ExpandCollapsePattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
"toggle" {
|
|
292
|
+
try {
|
|
293
|
+
$pattern = $element.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)
|
|
294
|
+
$pattern.Toggle()
|
|
295
|
+
$state = $pattern.Current.ToggleState.ToString()
|
|
296
|
+
[Console]::Out.Write((@{ success = $true; action = "toggle"; toggleState = $state } | ConvertTo-Json -Compress))
|
|
297
|
+
} catch {
|
|
298
|
+
[Console]::Out.Write((@{ success = $false; error = "TogglePattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
"select" {
|
|
302
|
+
try {
|
|
303
|
+
$pattern = $element.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)
|
|
304
|
+
$pattern.Select()
|
|
305
|
+
[Console]::Out.Write((@{ success = $true; action = "select" } | ConvertTo-Json -Compress))
|
|
306
|
+
} catch {
|
|
307
|
+
[Console]::Out.Write((@{ success = $false; error = "SelectionItemPattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
[Console]::Out.Write((@{ success = $false; error = $_.Exception.Message } | ConvertTo-Json -Compress))
|
|
313
|
+
exit 1
|
|
314
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AT-SPI bridge — read-only first pass (Tranche 4b).
|
|
4
|
+
|
|
5
|
+
Wraps GNOME's AT-SPI D-Bus a11y API via gobject-introspection's Atspi
|
|
6
|
+
binding. Used by LinuxAdapter to answer getUiTree / findElements /
|
|
7
|
+
getFocusedElement when the host is a Linux box with at-spi2 running
|
|
8
|
+
(every modern GNOME / KDE session with accessibility enabled).
|
|
9
|
+
|
|
10
|
+
Contract: same JSON shape as scripts/ps-bridge.ps1 (Windows) and
|
|
11
|
+
scripts/mac/*.jxa (macOS) — one JSON blob to stdout, exit 0 on
|
|
12
|
+
success, exit 1 with {"error": "..."} on failure.
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
--cmd get-tree [--process-id N]
|
|
16
|
+
Walk the a11y tree of the active window (or the given process
|
|
17
|
+
when --process-id is set). Returns a flat list of elements.
|
|
18
|
+
|
|
19
|
+
--cmd find [--name N] [--role R] [--process-id N]
|
|
20
|
+
Find elements matching a name substring and/or role. Returns
|
|
21
|
+
a flat list.
|
|
22
|
+
|
|
23
|
+
--cmd focused
|
|
24
|
+
Return the currently-focused a11y element (or null).
|
|
25
|
+
|
|
26
|
+
NOT IMPLEMENTED in this pass (stays at the LinuxAdapter level as a
|
|
27
|
+
{success:false} response):
|
|
28
|
+
--cmd invoke (click/focus/set-value/expand/...) — action dispatch
|
|
29
|
+
requires AT-SPI Action interface handling per-role. Follow-up.
|
|
30
|
+
|
|
31
|
+
Dependencies:
|
|
32
|
+
python3 (3.6+) with:
|
|
33
|
+
- python3-gi (Debian/Ubuntu) or equivalent
|
|
34
|
+
- gir1.2-atspi-2.0 (Debian/Ubuntu) or libatspi / atspi
|
|
35
|
+
|
|
36
|
+
Dependency probe runs on the Node side (`hasBinary('python3')` +
|
|
37
|
+
a `python3 -c "from gi.repository import Atspi"` check). When the
|
|
38
|
+
probe fails, the LinuxAdapter's a11y methods keep returning empty
|
|
39
|
+
gracefully — same behavior as before this bridge existed.
|
|
40
|
+
|
|
41
|
+
Safety: every AT-SPI call is wrapped in try/except so one bad
|
|
42
|
+
element (stale reference, permission denial, app process died)
|
|
43
|
+
doesn't take down the whole tree walk.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import json
|
|
50
|
+
import sys
|
|
51
|
+
from typing import Any, Optional
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import gi
|
|
55
|
+
gi.require_version('Atspi', '2.0')
|
|
56
|
+
from gi.repository import Atspi # type: ignore[import-not-found]
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
sys.stdout.write(json.dumps({
|
|
59
|
+
"error": "pyatspi/gi.repository.Atspi not available",
|
|
60
|
+
"detail": str(exc),
|
|
61
|
+
"hint": "apt-get install python3-gi gir1.2-atspi-2.0 (or distro equivalent)",
|
|
62
|
+
}))
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Helpers ────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
MAX_TREE_DEPTH = 12
|
|
69
|
+
MAX_TREE_NODES = 800 # stop after this many elements to bound cost
|
|
70
|
+
INTERACTIVE_ROLES = {
|
|
71
|
+
# Roles whose state/value the agent is most likely to care about.
|
|
72
|
+
# Used to prefer these over structural containers when truncating.
|
|
73
|
+
'push button', 'toggle button', 'check box', 'radio button',
|
|
74
|
+
'menu item', 'check menu item', 'radio menu item',
|
|
75
|
+
'link', 'hyperlink',
|
|
76
|
+
'text', 'entry', 'password text', 'editable text', 'combo box',
|
|
77
|
+
'list item', 'tree item', 'tab',
|
|
78
|
+
'slider', 'spin button', 'scroll bar',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def safe(fn, default=None):
|
|
83
|
+
"""Call fn(), return default if it raises (stale ref, perm denied, etc.)."""
|
|
84
|
+
try:
|
|
85
|
+
return fn()
|
|
86
|
+
except Exception:
|
|
87
|
+
return default
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def node_to_dict(acc: Any) -> Optional[dict]:
|
|
91
|
+
"""
|
|
92
|
+
Convert an Atspi.Accessible node into the shared UiElement JSON shape.
|
|
93
|
+
Returns None when the node lacks both a name AND a role — skip those.
|
|
94
|
+
"""
|
|
95
|
+
if acc is None:
|
|
96
|
+
return None
|
|
97
|
+
name = safe(lambda: acc.get_name(), '') or ''
|
|
98
|
+
role_name = safe(lambda: acc.get_role_name(), '') or ''
|
|
99
|
+
if not name and not role_name:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Bounds via Component interface. Missing → zero rect.
|
|
103
|
+
x, y, w, h = 0, 0, 0, 0
|
|
104
|
+
try:
|
|
105
|
+
comp = acc.get_component_iface()
|
|
106
|
+
if comp:
|
|
107
|
+
extents = comp.get_extents(Atspi.CoordType.SCREEN)
|
|
108
|
+
x, y, w, h = extents.x, extents.y, extents.width, extents.height
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# State flags
|
|
113
|
+
focused = False
|
|
114
|
+
enabled = True
|
|
115
|
+
selected = False
|
|
116
|
+
busy = False
|
|
117
|
+
offscreen = False
|
|
118
|
+
try:
|
|
119
|
+
ss = acc.get_state_set()
|
|
120
|
+
if ss is not None:
|
|
121
|
+
focused = ss.contains(Atspi.StateType.FOCUSED)
|
|
122
|
+
enabled = ss.contains(Atspi.StateType.ENABLED) and ss.contains(Atspi.StateType.SENSITIVE)
|
|
123
|
+
selected = ss.contains(Atspi.StateType.SELECTED)
|
|
124
|
+
busy = ss.contains(Atspi.StateType.BUSY)
|
|
125
|
+
offscreen = not ss.contains(Atspi.StateType.VISIBLE) or not ss.contains(Atspi.StateType.SHOWING)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
# Value via Value or Text interface (whichever applies).
|
|
130
|
+
value = None
|
|
131
|
+
try:
|
|
132
|
+
v = acc.get_value_iface()
|
|
133
|
+
if v:
|
|
134
|
+
value = str(v.get_current_value())
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
if value is None:
|
|
138
|
+
try:
|
|
139
|
+
txt = acc.get_text_iface()
|
|
140
|
+
if txt:
|
|
141
|
+
char_count = txt.get_character_count()
|
|
142
|
+
if char_count > 0:
|
|
143
|
+
value = txt.get_text(0, min(char_count, 512))
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Process id
|
|
148
|
+
pid = None
|
|
149
|
+
try:
|
|
150
|
+
pid = acc.get_process_id()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# AutomationId analogue — Atspi exposes "accessible-id" on some apps.
|
|
155
|
+
automation_id = safe(lambda: acc.get_accessible_id(), None)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"name": name,
|
|
159
|
+
"controlType": role_name,
|
|
160
|
+
"bounds": {"x": x, "y": y, "width": w, "height": h},
|
|
161
|
+
"value": value,
|
|
162
|
+
"enabled": enabled,
|
|
163
|
+
"focused": focused,
|
|
164
|
+
"selected": selected,
|
|
165
|
+
"disabled": not enabled if enabled is not None else None,
|
|
166
|
+
"busy": busy,
|
|
167
|
+
"offscreen": offscreen,
|
|
168
|
+
"processId": pid,
|
|
169
|
+
"automationId": automation_id,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def walk(acc: Any, out: list, depth: int = 0) -> None:
|
|
174
|
+
"""Depth-first flatten with caps on depth + total node count."""
|
|
175
|
+
if acc is None: return
|
|
176
|
+
if depth > MAX_TREE_DEPTH: return
|
|
177
|
+
if len(out) > MAX_TREE_NODES: return
|
|
178
|
+
|
|
179
|
+
node = node_to_dict(acc)
|
|
180
|
+
if node is not None:
|
|
181
|
+
out.append(node)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
child_count = acc.get_child_count()
|
|
185
|
+
except Exception:
|
|
186
|
+
return
|
|
187
|
+
for i in range(child_count):
|
|
188
|
+
try:
|
|
189
|
+
child = acc.get_child_at_index(i)
|
|
190
|
+
except Exception:
|
|
191
|
+
continue
|
|
192
|
+
walk(child, out, depth + 1)
|
|
193
|
+
if len(out) > MAX_TREE_NODES: return
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def active_application(process_id: Optional[int] = None) -> Optional[Any]:
|
|
197
|
+
"""Pick an Atspi.Accessible application root to walk.
|
|
198
|
+
|
|
199
|
+
Without process_id: prefer the app whose name matches the active
|
|
200
|
+
window title (heuristic — AT-SPI doesn't have a direct 'active app'
|
|
201
|
+
concept). Fall back to the first app.
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
desktop = Atspi.get_desktop(0)
|
|
205
|
+
except Exception:
|
|
206
|
+
return None
|
|
207
|
+
try:
|
|
208
|
+
n = desktop.get_child_count()
|
|
209
|
+
except Exception:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# If caller supplied a pid, match on it.
|
|
213
|
+
if process_id is not None:
|
|
214
|
+
for i in range(n):
|
|
215
|
+
app = safe(lambda i=i: desktop.get_child_at_index(i))
|
|
216
|
+
if app is None:
|
|
217
|
+
continue
|
|
218
|
+
pid = safe(lambda app=app: app.get_process_id())
|
|
219
|
+
if pid == process_id:
|
|
220
|
+
return app
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# Heuristic: find the app that has a FOCUSED descendant.
|
|
224
|
+
for i in range(n):
|
|
225
|
+
app = safe(lambda i=i: desktop.get_child_at_index(i))
|
|
226
|
+
if app is None:
|
|
227
|
+
continue
|
|
228
|
+
if has_focused_descendant(app):
|
|
229
|
+
return app
|
|
230
|
+
|
|
231
|
+
# Fallback: first app.
|
|
232
|
+
return safe(lambda: desktop.get_child_at_index(0))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def has_focused_descendant(acc: Any, depth: int = 0) -> bool:
|
|
236
|
+
if acc is None or depth > 6:
|
|
237
|
+
return False
|
|
238
|
+
try:
|
|
239
|
+
ss = acc.get_state_set()
|
|
240
|
+
if ss is not None and ss.contains(Atspi.StateType.FOCUSED):
|
|
241
|
+
return True
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
try:
|
|
245
|
+
n = acc.get_child_count()
|
|
246
|
+
except Exception:
|
|
247
|
+
return False
|
|
248
|
+
for i in range(n):
|
|
249
|
+
child = safe(lambda i=i: acc.get_child_at_index(i))
|
|
250
|
+
if has_focused_descendant(child, depth + 1):
|
|
251
|
+
return True
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def focused_element() -> Optional[dict]:
|
|
256
|
+
try:
|
|
257
|
+
desktop = Atspi.get_desktop(0)
|
|
258
|
+
n = desktop.get_child_count()
|
|
259
|
+
except Exception:
|
|
260
|
+
return None
|
|
261
|
+
for i in range(n):
|
|
262
|
+
app = safe(lambda i=i: desktop.get_child_at_index(i))
|
|
263
|
+
if app is None: continue
|
|
264
|
+
hit = _find_focused(app, 0)
|
|
265
|
+
if hit is not None:
|
|
266
|
+
return node_to_dict(hit)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _find_focused(acc: Any, depth: int) -> Optional[Any]:
|
|
271
|
+
if acc is None or depth > 12: return None
|
|
272
|
+
try:
|
|
273
|
+
ss = acc.get_state_set()
|
|
274
|
+
if ss is not None and ss.contains(Atspi.StateType.FOCUSED):
|
|
275
|
+
return acc
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
try:
|
|
279
|
+
n = acc.get_child_count()
|
|
280
|
+
except Exception:
|
|
281
|
+
return None
|
|
282
|
+
for i in range(n):
|
|
283
|
+
hit = _find_focused(safe(lambda i=i: acc.get_child_at_index(i)), depth + 1)
|
|
284
|
+
if hit is not None:
|
|
285
|
+
return hit
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ── Command dispatch ─────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def cmd_get_tree(process_id: Optional[int]) -> dict:
|
|
292
|
+
app = active_application(process_id)
|
|
293
|
+
out: list = []
|
|
294
|
+
if app is not None:
|
|
295
|
+
walk(app, out)
|
|
296
|
+
return {"elements": out, "truncated": len(out) > MAX_TREE_NODES}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cmd_find(name: Optional[str], role: Optional[str], process_id: Optional[int]) -> dict:
|
|
300
|
+
# Implement find as a post-filter over the tree walk — simpler than
|
|
301
|
+
# deep-diving the collection interface and more predictable.
|
|
302
|
+
tree = cmd_get_tree(process_id).get("elements", [])
|
|
303
|
+
if name is None and role is None:
|
|
304
|
+
return {"elements": tree}
|
|
305
|
+
|
|
306
|
+
name_l = name.lower() if name else None
|
|
307
|
+
role_l = role.lower() if role else None
|
|
308
|
+
|
|
309
|
+
def matches(el: dict) -> bool:
|
|
310
|
+
if name_l is not None:
|
|
311
|
+
el_name = (el.get("name") or "").lower()
|
|
312
|
+
if name_l not in el_name:
|
|
313
|
+
return False
|
|
314
|
+
if role_l is not None:
|
|
315
|
+
el_role = (el.get("controlType") or "").lower()
|
|
316
|
+
if role_l not in el_role:
|
|
317
|
+
return False
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
hits = [e for e in tree if matches(e)]
|
|
321
|
+
return {"elements": hits}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def cmd_focused() -> dict:
|
|
325
|
+
el = focused_element()
|
|
326
|
+
return {"element": el}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def main() -> int:
|
|
330
|
+
p = argparse.ArgumentParser()
|
|
331
|
+
p.add_argument('--cmd', required=True, choices=['get-tree', 'find', 'focused'])
|
|
332
|
+
p.add_argument('--name', default=None)
|
|
333
|
+
p.add_argument('--role', default=None)
|
|
334
|
+
p.add_argument('--process-id', type=int, default=None)
|
|
335
|
+
args = p.parse_args()
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
if args.cmd == 'get-tree':
|
|
339
|
+
result = cmd_get_tree(args.process_id)
|
|
340
|
+
elif args.cmd == 'find':
|
|
341
|
+
result = cmd_find(args.name, args.role, args.process_id)
|
|
342
|
+
elif args.cmd == 'focused':
|
|
343
|
+
result = cmd_focused()
|
|
344
|
+
else:
|
|
345
|
+
result = {"error": f"unknown command: {args.cmd}"}
|
|
346
|
+
except Exception as exc:
|
|
347
|
+
result = {"error": str(exc)}
|
|
348
|
+
sys.stdout.write(json.dumps(result))
|
|
349
|
+
return 1
|
|
350
|
+
|
|
351
|
+
sys.stdout.write(json.dumps(result))
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == '__main__':
|
|
356
|
+
sys.exit(main())
|