@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,1253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Linux PlatformAdapter — all Linux-specific code lives here.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* - Mouse + keyboard: nut-js directly (same approach as Windows).
|
|
7
|
+
* - Screenshot: nut-js screen.grab() → sharp for PNG encode / resize.
|
|
8
|
+
* - Screen size: xrandr --query; HiDPI via GDK_SCALE / QT_SCALE_FACTOR env.
|
|
9
|
+
* - Windows: wmctrl -lG for listing; xdotool for active-window detection.
|
|
10
|
+
* - A11y: AT-SPI bridge not yet implemented — graceful empty returns.
|
|
11
|
+
* - Clipboard: xclip -selection clipboard (X11 assumption).
|
|
12
|
+
* - openApp: spawn by name, falling back to xdg-open.
|
|
13
|
+
*
|
|
14
|
+
* All tool invocations tolerate missing binaries — if wmctrl/xclip/xdotool
|
|
15
|
+
* are not installed the adapter still loads and methods return empty defaults.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.LinuxAdapter = void 0;
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const util_1 = require("util");
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
59
|
+
const nut_js_1 = require("@nut-tree-fork/nut-js");
|
|
60
|
+
const wayland_backend_1 = require("./wayland-backend");
|
|
61
|
+
const launch_poll_1 = require("./launch-poll");
|
|
62
|
+
const paths_1 = require("../paths");
|
|
63
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
64
|
+
// Tunables
|
|
65
|
+
const TOOL_TIMEOUT_MS = 3_000;
|
|
66
|
+
const SCREENSHOT_TIMEOUT_MS = 10_000;
|
|
67
|
+
/**
|
|
68
|
+
* AT-SPI tree walks can be slow on big apps — give them a longer budget
|
|
69
|
+
* than the generic tool timeout. Python bridge caps its own traversal at
|
|
70
|
+
* MAX_TREE_NODES to prevent runaway calls.
|
|
71
|
+
*/
|
|
72
|
+
const A11Y_TIMEOUT_MS = 10_000;
|
|
73
|
+
class LinuxAdapter {
|
|
74
|
+
platform = 'linux';
|
|
75
|
+
/**
|
|
76
|
+
* Wayland-vs-X11 detection runs at init(). Wayland blocks many X11-era
|
|
77
|
+
* input primitives (global mouse coords, cross-window drag, synthetic
|
|
78
|
+
* modifier injection) — callers use this flag to decide whether to
|
|
79
|
+
* surface graceful "not supported on Wayland" errors instead of silently
|
|
80
|
+
* misfiring through nut-js.
|
|
81
|
+
*/
|
|
82
|
+
environment = detectLinuxEnvironment();
|
|
83
|
+
screenSize = null;
|
|
84
|
+
binaryCache = new Map();
|
|
85
|
+
lastCursor = null;
|
|
86
|
+
/**
|
|
87
|
+
* Wayland input backend. On X11 this stays `kind:'none'` and all input
|
|
88
|
+
* flows through nut-js as before. On Wayland, ydotool takes over mouse +
|
|
89
|
+
* keyboard (if present); wtype is a keyboard-only fallback. Without
|
|
90
|
+
* either, we fall through to nut-js (which silently fails) and the
|
|
91
|
+
* adapter's permission probe reports input=false.
|
|
92
|
+
*/
|
|
93
|
+
wayland = new wayland_backend_1.WaylandBackend('none');
|
|
94
|
+
/**
|
|
95
|
+
* AT-SPI D-Bus a11y bridge state (Tranche 4b). The bridge is a
|
|
96
|
+
* self-contained Python script (scripts/linux/atspi-bridge.py) that
|
|
97
|
+
* wraps gi.repository.Atspi. We probe its availability at init —
|
|
98
|
+
* requires python3 + python3-gi + gir1.2-atspi-2.0. When unavailable
|
|
99
|
+
* every a11y method returns its pre-existing safe empty response so
|
|
100
|
+
* nothing regresses on boxes without AT-SPI.
|
|
101
|
+
*/
|
|
102
|
+
atspiAvailable = false;
|
|
103
|
+
atspiScript = '';
|
|
104
|
+
async init() {
|
|
105
|
+
// Tighten nut-js defaults (mirrors Windows path in legacy code).
|
|
106
|
+
nut_js_1.mouse.config.mouseSpeed = 2000;
|
|
107
|
+
nut_js_1.mouse.config.autoDelayMs = 0;
|
|
108
|
+
nut_js_1.keyboard.config.autoDelayMs = 0;
|
|
109
|
+
// Warm binary presence cache (non-fatal if none present).
|
|
110
|
+
await Promise.all([
|
|
111
|
+
this.hasBinary('wmctrl'),
|
|
112
|
+
this.hasBinary('xdotool'),
|
|
113
|
+
this.hasBinary('xclip'),
|
|
114
|
+
this.hasBinary('xrandr'),
|
|
115
|
+
this.hasBinary('xdg-open'),
|
|
116
|
+
// Wayland-era replacements — detected here so Tranche 4a's routing
|
|
117
|
+
// decisions at init time are fast.
|
|
118
|
+
this.hasBinary('ydotool'),
|
|
119
|
+
this.hasBinary('wtype'),
|
|
120
|
+
this.hasBinary('wl-copy'),
|
|
121
|
+
]);
|
|
122
|
+
// If we're on Wayland, initialize the backend. No-op on X11.
|
|
123
|
+
if (this.environment === 'wayland') {
|
|
124
|
+
this.wayland = await wayland_backend_1.WaylandBackend.detect(name => this.hasBinary(name));
|
|
125
|
+
}
|
|
126
|
+
// Probe the AT-SPI bridge (Tranche 4b). Two conditions must be met:
|
|
127
|
+
// python3 must be on PATH, AND `from gi.repository import Atspi` must
|
|
128
|
+
// succeed (requires python3-gi + gir1.2-atspi-2.0). We run the probe
|
|
129
|
+
// with a short timeout so boots stay snappy when neither is installed.
|
|
130
|
+
this.atspiScript = path.resolve((0, paths_1.getPackageRoot)(), 'scripts', 'linux', 'atspi-bridge.py');
|
|
131
|
+
if (await this.hasBinary('python3')) {
|
|
132
|
+
try {
|
|
133
|
+
await execFileAsync('python3', ['-c', 'import gi; gi.require_version("Atspi","2.0"); from gi.repository import Atspi'], { timeout: 2_000 });
|
|
134
|
+
this.atspiAvailable = true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Probe failed — gi.repository.Atspi isn't installed. Keep stubs.
|
|
138
|
+
this.atspiAvailable = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Pre-warm screen size so first capture is fast.
|
|
142
|
+
await this.getScreenSize().catch(() => null);
|
|
143
|
+
}
|
|
144
|
+
async shutdown() {
|
|
145
|
+
// No long-lived processes to clean up.
|
|
146
|
+
}
|
|
147
|
+
// ─── PERMISSIONS ──────────────────────────────────────────────────
|
|
148
|
+
async checkPermissions() {
|
|
149
|
+
// X11: implicit user-level input access. Wayland: synthetic-input APIs
|
|
150
|
+
// are blocked by compositors unless the user runs ydotool (kernel
|
|
151
|
+
// uinput daemon).
|
|
152
|
+
// Accessibility: now reflects whether the AT-SPI bridge (Tranche 4b)
|
|
153
|
+
// is available — true when python3 + python3-gi + gir1.2-atspi-2.0
|
|
154
|
+
// are installed and the probe at init() succeeded.
|
|
155
|
+
if (this.environment === 'wayland') {
|
|
156
|
+
const canInject = await this.hasBinary('ydotool');
|
|
157
|
+
return { input: canInject, accessibility: this.atspiAvailable, screenRecording: true };
|
|
158
|
+
}
|
|
159
|
+
return { input: true, accessibility: this.atspiAvailable, screenRecording: true };
|
|
160
|
+
}
|
|
161
|
+
async requestPermissions() {
|
|
162
|
+
return this.checkPermissions();
|
|
163
|
+
}
|
|
164
|
+
// ─── DISPLAY ──────────────────────────────────────────────────────
|
|
165
|
+
async getScreenSize() {
|
|
166
|
+
if (this.screenSize)
|
|
167
|
+
return this.screenSize;
|
|
168
|
+
// Prefer xrandr for logical geometry — matches mouse coordinate space.
|
|
169
|
+
let logicalWidth = 0;
|
|
170
|
+
let logicalHeight = 0;
|
|
171
|
+
try {
|
|
172
|
+
const { stdout } = await execFileAsync('xrandr', ['--query'], {
|
|
173
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
174
|
+
});
|
|
175
|
+
// Match "primary AxB" first; else the first "connected AxB+x+y" entry.
|
|
176
|
+
const primary = stdout.match(/\bconnected\s+primary\s+(\d+)x(\d+)/);
|
|
177
|
+
const first = stdout.match(/\bconnected(?:\s+primary)?\s+(\d+)x(\d+)/);
|
|
178
|
+
const m = primary ?? first;
|
|
179
|
+
if (m) {
|
|
180
|
+
logicalWidth = parseInt(m[1], 10) || 0;
|
|
181
|
+
logicalHeight = parseInt(m[2], 10) || 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
/* xrandr missing or failed — fall through to nut-js */
|
|
186
|
+
}
|
|
187
|
+
// Physical dimensions — nut-js screen.grab returns hardware pixels.
|
|
188
|
+
let physicalWidth = logicalWidth;
|
|
189
|
+
let physicalHeight = logicalHeight;
|
|
190
|
+
try {
|
|
191
|
+
const w = await nut_js_1.screen.width();
|
|
192
|
+
const h = await nut_js_1.screen.height();
|
|
193
|
+
if (w > 0 && h > 0) {
|
|
194
|
+
physicalWidth = w;
|
|
195
|
+
physicalHeight = h;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
/* nut-js unavailable — keep logical dims as physical */
|
|
200
|
+
}
|
|
201
|
+
// If xrandr gave us nothing, assume physical == logical.
|
|
202
|
+
if (!logicalWidth)
|
|
203
|
+
logicalWidth = physicalWidth;
|
|
204
|
+
if (!logicalHeight)
|
|
205
|
+
logicalHeight = physicalHeight;
|
|
206
|
+
// HiDPI hints from desktop environment env vars.
|
|
207
|
+
const gdkScale = parseInt(process.env.GDK_SCALE || '1', 10);
|
|
208
|
+
const qtScale = parseFloat(process.env.QT_SCALE_FACTOR || '1');
|
|
209
|
+
const envScale = Math.max(Number.isFinite(gdkScale) ? gdkScale : 1, Number.isFinite(qtScale) ? qtScale : 1, 1);
|
|
210
|
+
let dpiRatio = 1;
|
|
211
|
+
if (physicalWidth > 0 && logicalWidth > 0 && physicalWidth > logicalWidth) {
|
|
212
|
+
dpiRatio = physicalWidth / logicalWidth;
|
|
213
|
+
}
|
|
214
|
+
else if (envScale > 1) {
|
|
215
|
+
dpiRatio = envScale;
|
|
216
|
+
// Derive physical dims from env scale when xrandr/nut didn't disagree.
|
|
217
|
+
if (physicalWidth === logicalWidth) {
|
|
218
|
+
physicalWidth = Math.round(logicalWidth * envScale);
|
|
219
|
+
physicalHeight = Math.round(logicalHeight * envScale);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.screenSize = {
|
|
223
|
+
physicalWidth,
|
|
224
|
+
physicalHeight,
|
|
225
|
+
logicalWidth,
|
|
226
|
+
logicalHeight,
|
|
227
|
+
dpiRatio,
|
|
228
|
+
};
|
|
229
|
+
return this.screenSize;
|
|
230
|
+
}
|
|
231
|
+
async listDisplays() {
|
|
232
|
+
// xrandr --query lists every connected output with geometry.
|
|
233
|
+
if (!(await this.hasBinary('xrandr'))) {
|
|
234
|
+
const size = await this.getScreenSize();
|
|
235
|
+
return [{
|
|
236
|
+
index: 0,
|
|
237
|
+
label: 'Primary',
|
|
238
|
+
primary: true,
|
|
239
|
+
bounds: { x: 0, y: 0, width: size.logicalWidth, height: size.logicalHeight },
|
|
240
|
+
physicalSize: { width: size.physicalWidth, height: size.physicalHeight },
|
|
241
|
+
dpiRatio: size.dpiRatio,
|
|
242
|
+
}];
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const { stdout } = await execFileAsync('xrandr', ['--query'], {
|
|
246
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
247
|
+
});
|
|
248
|
+
const displays = [];
|
|
249
|
+
// Match lines like: "HDMI-1 connected primary 1920x1080+0+0 ..."
|
|
250
|
+
const re = /^(\S+)\s+connected\s+(primary\s+)?(\d+)x(\d+)\+(-?\d+)\+(-?\d+)/gm;
|
|
251
|
+
let m;
|
|
252
|
+
let idx = 0;
|
|
253
|
+
const size = await this.getScreenSize();
|
|
254
|
+
while ((m = re.exec(stdout)) !== null) {
|
|
255
|
+
const [, name, primaryFlag, w, h, x, y] = m;
|
|
256
|
+
const width = parseInt(w, 10);
|
|
257
|
+
const height = parseInt(h, 10);
|
|
258
|
+
displays.push({
|
|
259
|
+
index: idx++,
|
|
260
|
+
label: name,
|
|
261
|
+
primary: !!primaryFlag,
|
|
262
|
+
bounds: {
|
|
263
|
+
x: parseInt(x, 10),
|
|
264
|
+
y: parseInt(y, 10),
|
|
265
|
+
width,
|
|
266
|
+
height,
|
|
267
|
+
},
|
|
268
|
+
physicalSize: {
|
|
269
|
+
width: Math.round(width * size.dpiRatio),
|
|
270
|
+
height: Math.round(height * size.dpiRatio),
|
|
271
|
+
},
|
|
272
|
+
dpiRatio: size.dpiRatio,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (displays.length === 0) {
|
|
276
|
+
// xrandr present but parsed nothing — single-display fallback.
|
|
277
|
+
return [{
|
|
278
|
+
index: 0,
|
|
279
|
+
label: 'Primary',
|
|
280
|
+
primary: true,
|
|
281
|
+
bounds: { x: 0, y: 0, width: size.logicalWidth, height: size.logicalHeight },
|
|
282
|
+
physicalSize: { width: size.physicalWidth, height: size.physicalHeight },
|
|
283
|
+
dpiRatio: size.dpiRatio,
|
|
284
|
+
}];
|
|
285
|
+
}
|
|
286
|
+
// Ensure exactly one primary — prefer xrandr's flag; fallback to index 0.
|
|
287
|
+
if (!displays.some(d => d.primary))
|
|
288
|
+
displays[0].primary = true;
|
|
289
|
+
return displays;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
const size = await this.getScreenSize();
|
|
293
|
+
return [{
|
|
294
|
+
index: 0,
|
|
295
|
+
label: 'Primary',
|
|
296
|
+
primary: true,
|
|
297
|
+
bounds: { x: 0, y: 0, width: size.logicalWidth, height: size.logicalHeight },
|
|
298
|
+
physicalSize: { width: size.physicalWidth, height: size.physicalHeight },
|
|
299
|
+
dpiRatio: size.dpiRatio,
|
|
300
|
+
}];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async screenshot(opts) {
|
|
304
|
+
const img = await this.grabScreen();
|
|
305
|
+
let pipeline = (0, sharp_1.default)(img.data, {
|
|
306
|
+
raw: { width: img.width, height: img.height, channels: 4 },
|
|
307
|
+
});
|
|
308
|
+
let width = img.width;
|
|
309
|
+
let height = img.height;
|
|
310
|
+
let scaleFactor = 1;
|
|
311
|
+
// Display index → crop to that display's bounds (xrandr geometry).
|
|
312
|
+
if (opts?.displayIndex !== undefined && opts.displayIndex > 0) {
|
|
313
|
+
const displays = await this.listDisplays();
|
|
314
|
+
const target = displays[opts.displayIndex];
|
|
315
|
+
if (target) {
|
|
316
|
+
const r = target.dpiRatio || 1;
|
|
317
|
+
const left = Math.max(0, Math.round(target.bounds.x * r));
|
|
318
|
+
const top = Math.max(0, Math.round(target.bounds.y * r));
|
|
319
|
+
const w = Math.max(1, Math.min(Math.round(target.bounds.width * r), img.width - left));
|
|
320
|
+
const h = Math.max(1, Math.min(Math.round(target.bounds.height * r), img.height - top));
|
|
321
|
+
pipeline = pipeline.extract({ left, top, width: w, height: h });
|
|
322
|
+
width = w;
|
|
323
|
+
height = h;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (opts?.maxWidth && width > opts.maxWidth) {
|
|
327
|
+
scaleFactor = width / opts.maxWidth;
|
|
328
|
+
const newH = Math.round(height / scaleFactor);
|
|
329
|
+
pipeline = pipeline.resize(opts.maxWidth, newH, { fit: 'fill' });
|
|
330
|
+
width = opts.maxWidth;
|
|
331
|
+
height = newH;
|
|
332
|
+
}
|
|
333
|
+
const buffer = await pipeline.png().toBuffer();
|
|
334
|
+
// Release raw RGBA buffer now that sharp has consumed it.
|
|
335
|
+
img.data = null;
|
|
336
|
+
return { buffer, width, height, scaleFactor };
|
|
337
|
+
}
|
|
338
|
+
async screenshotRegion(x, y, w, h) {
|
|
339
|
+
const img = await this.grabScreen();
|
|
340
|
+
try {
|
|
341
|
+
// Clamp to image bounds so sharp doesn't throw.
|
|
342
|
+
const left = Math.max(0, Math.min(x, img.width - 1));
|
|
343
|
+
const top = Math.max(0, Math.min(y, img.height - 1));
|
|
344
|
+
const width = Math.max(1, Math.min(w, img.width - left));
|
|
345
|
+
const height = Math.max(1, Math.min(h, img.height - top));
|
|
346
|
+
const buffer = await (0, sharp_1.default)(img.data, {
|
|
347
|
+
raw: { width: img.width, height: img.height, channels: 4 },
|
|
348
|
+
})
|
|
349
|
+
.extract({ left, top, width, height })
|
|
350
|
+
.png()
|
|
351
|
+
.toBuffer();
|
|
352
|
+
return { buffer, width, height, scaleFactor: 1 };
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
img.data = null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async grabScreen() {
|
|
359
|
+
return await this.withTimeout(nut_js_1.screen.grab(), SCREENSHOT_TIMEOUT_MS, 'nut-js screen.grab');
|
|
360
|
+
}
|
|
361
|
+
// ─── WINDOWS ──────────────────────────────────────────────────────
|
|
362
|
+
async listWindows() {
|
|
363
|
+
if (!(await this.hasBinary('wmctrl')))
|
|
364
|
+
return [];
|
|
365
|
+
try {
|
|
366
|
+
// -l -G -p: <id> <desktop> <pid> <x> <y> <w> <h> <host> <title...>
|
|
367
|
+
const { stdout } = await execFileAsync('wmctrl', ['-l', '-G', '-p'], {
|
|
368
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
369
|
+
});
|
|
370
|
+
return this.parseWmctrlOutput(stdout);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async getActiveWindow() {
|
|
377
|
+
// xdotool gives us the active window id directly; we cross-reference wmctrl
|
|
378
|
+
// for title/pid/bounds. Missing xdotool → fall back to first non-minimized.
|
|
379
|
+
const all = await this.listWindows();
|
|
380
|
+
if (!all.length)
|
|
381
|
+
return null;
|
|
382
|
+
if (await this.hasBinary('xdotool')) {
|
|
383
|
+
try {
|
|
384
|
+
const { stdout } = await execFileAsync('xdotool', ['getactivewindow'], {
|
|
385
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
386
|
+
});
|
|
387
|
+
const id = parseInt(stdout.trim(), 10);
|
|
388
|
+
if (Number.isFinite(id)) {
|
|
389
|
+
const match = all.find(w => typeof w.handle === 'number' && w.handle === id);
|
|
390
|
+
if (match)
|
|
391
|
+
return match;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
/* fall through */
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return all[0] ?? null;
|
|
399
|
+
}
|
|
400
|
+
async focusWindow(query) {
|
|
401
|
+
if (!(await this.hasBinary('wmctrl')))
|
|
402
|
+
return false;
|
|
403
|
+
// Prefer pid-based focus when we know it (wmctrl -i -a expects window id,
|
|
404
|
+
// not pid, so we find the matching id from the list first).
|
|
405
|
+
try {
|
|
406
|
+
const windows = await this.listWindows();
|
|
407
|
+
const match = windows.find(w => {
|
|
408
|
+
if (query.processId !== undefined && w.processId === query.processId)
|
|
409
|
+
return true;
|
|
410
|
+
if (query.processName && w.processName.toLowerCase() === query.processName.toLowerCase())
|
|
411
|
+
return true;
|
|
412
|
+
if (query.title && w.title.toLowerCase().includes(query.title.toLowerCase()))
|
|
413
|
+
return true;
|
|
414
|
+
return false;
|
|
415
|
+
});
|
|
416
|
+
if (match?.handle !== undefined) {
|
|
417
|
+
const handleHex = typeof match.handle === 'number'
|
|
418
|
+
? '0x' + match.handle.toString(16)
|
|
419
|
+
: String(match.handle);
|
|
420
|
+
await execFileAsync('wmctrl', ['-i', '-a', handleHex], { timeout: TOOL_TIMEOUT_MS });
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
// Fallback: wmctrl -a <title substring>
|
|
424
|
+
if (query.title) {
|
|
425
|
+
await execFileAsync('wmctrl', ['-a', query.title], { timeout: TOOL_TIMEOUT_MS });
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async maximizeWindow() {
|
|
435
|
+
if (!(await this.hasBinary('wmctrl')))
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
await execFileAsync('wmctrl', ['-r', ':ACTIVE:', '-b', 'add,maximized_vert,maximized_horz'], { timeout: TOOL_TIMEOUT_MS });
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
/* non-fatal */
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async setWindowState(state, query) {
|
|
445
|
+
if (!(await this.hasBinary('wmctrl')))
|
|
446
|
+
return false;
|
|
447
|
+
const target = await this.resolveWindowHandle(query);
|
|
448
|
+
if (!target)
|
|
449
|
+
return false;
|
|
450
|
+
try {
|
|
451
|
+
if (state === 'maximize') {
|
|
452
|
+
await execFileAsync('wmctrl', ['-i', '-r', target, '-b', 'add,maximized_vert,maximized_horz'], { timeout: TOOL_TIMEOUT_MS });
|
|
453
|
+
}
|
|
454
|
+
else if (state === 'minimize') {
|
|
455
|
+
await execFileAsync('wmctrl', ['-i', '-r', target, '-b', 'add,hidden'], { timeout: TOOL_TIMEOUT_MS });
|
|
456
|
+
}
|
|
457
|
+
else if (state === 'normal') {
|
|
458
|
+
// Remove maximize + hidden so the window returns to its original bounds.
|
|
459
|
+
await execFileAsync('wmctrl', ['-i', '-r', target, '-b', 'remove,maximized_vert,maximized_horz,hidden'], { timeout: TOOL_TIMEOUT_MS });
|
|
460
|
+
}
|
|
461
|
+
else if (state === 'close') {
|
|
462
|
+
// wmctrl -c sends _NET_CLOSE_WINDOW — the app can prompt / refuse.
|
|
463
|
+
// wmctrl's -c takes a name-substring, not a window id, so we hand
|
|
464
|
+
// it a title match where we can, else fall back to a generic match.
|
|
465
|
+
if (query?.title) {
|
|
466
|
+
await execFileAsync('wmctrl', ['-c', query.title], { timeout: TOOL_TIMEOUT_MS });
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// xdotool lets us close by window id directly.
|
|
470
|
+
if (await this.hasBinary('xdotool')) {
|
|
471
|
+
await execFileAsync('xdotool', ['windowclose', target], { timeout: TOOL_TIMEOUT_MS });
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async setWindowBounds(bounds, query) {
|
|
485
|
+
if (!(await this.hasBinary('wmctrl')))
|
|
486
|
+
return false;
|
|
487
|
+
const target = await this.resolveWindowHandle(query);
|
|
488
|
+
if (!target)
|
|
489
|
+
return false;
|
|
490
|
+
try {
|
|
491
|
+
// Read current bounds for fields not supplied.
|
|
492
|
+
const windows = await this.listWindows();
|
|
493
|
+
const current = windows.find(w => typeof w.handle === 'number' && '0x' + w.handle.toString(16) === target);
|
|
494
|
+
const cur = current?.bounds ?? { x: 0, y: 0, width: 0, height: 0 };
|
|
495
|
+
const x = bounds.x ?? cur.x;
|
|
496
|
+
const y = bounds.y ?? cur.y;
|
|
497
|
+
const w = bounds.width ?? cur.width;
|
|
498
|
+
const h = bounds.height ?? cur.height;
|
|
499
|
+
// wmctrl -e format: gravity,x,y,width,height — 0 = default gravity.
|
|
500
|
+
await execFileAsync('wmctrl', ['-i', '-r', target, '-e', `0,${x},${y},${w},${h}`], { timeout: TOOL_TIMEOUT_MS });
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async resolveWindowHandle(query) {
|
|
508
|
+
if (!query) {
|
|
509
|
+
// Active window handle via xdotool.
|
|
510
|
+
if (await this.hasBinary('xdotool')) {
|
|
511
|
+
try {
|
|
512
|
+
const { stdout } = await execFileAsync('xdotool', ['getactivewindow'], {
|
|
513
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
514
|
+
});
|
|
515
|
+
const id = parseInt(stdout.trim(), 10);
|
|
516
|
+
if (Number.isFinite(id))
|
|
517
|
+
return '0x' + id.toString(16);
|
|
518
|
+
}
|
|
519
|
+
catch { /* fall through */ }
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const windows = await this.listWindows();
|
|
524
|
+
const match = windows.find(w => {
|
|
525
|
+
if (query.processId !== undefined && w.processId === query.processId)
|
|
526
|
+
return true;
|
|
527
|
+
if (query.processName && w.processName.toLowerCase() === query.processName.toLowerCase())
|
|
528
|
+
return true;
|
|
529
|
+
if (query.title && w.title.toLowerCase().includes(query.title.toLowerCase()))
|
|
530
|
+
return true;
|
|
531
|
+
return false;
|
|
532
|
+
});
|
|
533
|
+
if (match && typeof match.handle === 'number') {
|
|
534
|
+
return '0x' + match.handle.toString(16);
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
// ─── ACCESSIBILITY ────────────────────────────────────────────────
|
|
539
|
+
//
|
|
540
|
+
// Tranche 4b — AT-SPI D-Bus bridge (READ-ONLY first pass).
|
|
541
|
+
//
|
|
542
|
+
// When the bridge is available (python3 + python3-gi + Atspi), we
|
|
543
|
+
// spawn `atspi-bridge.py` to answer getUiTree / findElements /
|
|
544
|
+
// getFocusedElement / waitForElement. The script emits JSON with the
|
|
545
|
+
// same UiElement shape used on Windows / macOS.
|
|
546
|
+
//
|
|
547
|
+
// `invokeElement` stays stubbed — action dispatch (click / focus /
|
|
548
|
+
// set-value / expand / ...) needs per-role handling via AT-SPI's
|
|
549
|
+
// Action / EditableText / Value interfaces. Scoped out of this pass
|
|
550
|
+
// so we can land READ support for Linux now and iterate. When the
|
|
551
|
+
// bridge isn't available, every method falls back to the same safe
|
|
552
|
+
// empty responses as before — zero regression on boxes without AT-SPI.
|
|
553
|
+
async getUiTree(processId) {
|
|
554
|
+
if (!this.atspiAvailable)
|
|
555
|
+
return [];
|
|
556
|
+
try {
|
|
557
|
+
const args = ['--cmd', 'get-tree'];
|
|
558
|
+
// Default to the active window's pid when the caller omits it, so an
|
|
559
|
+
// unscoped read_screen targets the focused app (parity with the Windows
|
|
560
|
+
// adapter) instead of walking the whole desktop tree.
|
|
561
|
+
let pid = processId;
|
|
562
|
+
if (typeof pid !== 'number') {
|
|
563
|
+
const fg = await this.getActiveWindow().catch(() => null);
|
|
564
|
+
if (fg?.processId)
|
|
565
|
+
pid = fg.processId;
|
|
566
|
+
}
|
|
567
|
+
if (typeof pid === 'number')
|
|
568
|
+
args.push('--process-id', String(pid));
|
|
569
|
+
const { stdout } = await execFileAsync('python3', [this.atspiScript, ...args], {
|
|
570
|
+
timeout: A11Y_TIMEOUT_MS,
|
|
571
|
+
});
|
|
572
|
+
const data = JSON.parse(stdout);
|
|
573
|
+
const raw = Array.isArray(data.elements) ? data.elements : [];
|
|
574
|
+
return raw.map(this.normalizeAtspiElement);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async findElements(query) {
|
|
581
|
+
if (!this.atspiAvailable)
|
|
582
|
+
return [];
|
|
583
|
+
try {
|
|
584
|
+
const args = ['--cmd', 'find'];
|
|
585
|
+
if (query.name)
|
|
586
|
+
args.push('--name', query.name);
|
|
587
|
+
if (query.controlType)
|
|
588
|
+
args.push('--role', query.controlType);
|
|
589
|
+
if (typeof query.processId === 'number')
|
|
590
|
+
args.push('--process-id', String(query.processId));
|
|
591
|
+
const { stdout } = await execFileAsync('python3', [this.atspiScript, ...args], {
|
|
592
|
+
timeout: A11Y_TIMEOUT_MS,
|
|
593
|
+
});
|
|
594
|
+
const data = JSON.parse(stdout);
|
|
595
|
+
const raw = Array.isArray(data.elements) ? data.elements : [];
|
|
596
|
+
return raw.map(this.normalizeAtspiElement);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async getFocusedElement() {
|
|
603
|
+
if (!this.atspiAvailable)
|
|
604
|
+
return null;
|
|
605
|
+
try {
|
|
606
|
+
const { stdout } = await execFileAsync('python3', [this.atspiScript, '--cmd', 'focused'], {
|
|
607
|
+
timeout: A11Y_TIMEOUT_MS,
|
|
608
|
+
});
|
|
609
|
+
const data = JSON.parse(stdout);
|
|
610
|
+
return data.element ? this.normalizeAtspiElement(data.element) : null;
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async invokeElement(_query) {
|
|
617
|
+
// Action dispatch is the next AT-SPI step — needs per-role AT-SPI
|
|
618
|
+
// Action / Value / EditableText interface handling. Until then,
|
|
619
|
+
// Linux agents use getUiTree + coord click as a coarse fallback.
|
|
620
|
+
return { success: false };
|
|
621
|
+
}
|
|
622
|
+
async waitForElement(query, timeoutMs) {
|
|
623
|
+
if (!this.atspiAvailable)
|
|
624
|
+
return null;
|
|
625
|
+
const interval = query.intervalMs ?? 250;
|
|
626
|
+
const deadline = Date.now() + timeoutMs;
|
|
627
|
+
while (Date.now() < deadline) {
|
|
628
|
+
const hits = await this.findElements({
|
|
629
|
+
name: query.name, controlType: query.controlType, processId: query.processId,
|
|
630
|
+
});
|
|
631
|
+
if (hits.length > 0)
|
|
632
|
+
return hits[0];
|
|
633
|
+
await this.delay(interval);
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Normalize one element record from the Python bridge into the shared
|
|
639
|
+
* UiElement shape used by the Windows + macOS adapters. Missing bounds
|
|
640
|
+
* default to zero; missing state flags pass through as undefined.
|
|
641
|
+
*/
|
|
642
|
+
normalizeAtspiElement = (raw) => {
|
|
643
|
+
const enabled = typeof raw?.enabled === 'boolean' ? raw.enabled : undefined;
|
|
644
|
+
return {
|
|
645
|
+
name: raw?.name ?? '',
|
|
646
|
+
controlType: raw?.controlType ?? '',
|
|
647
|
+
bounds: raw?.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
|
|
648
|
+
value: typeof raw?.value === 'string' ? raw.value : undefined,
|
|
649
|
+
enabled,
|
|
650
|
+
focused: raw?.focused,
|
|
651
|
+
selected: raw?.selected,
|
|
652
|
+
disabled: enabled === false ? true : undefined,
|
|
653
|
+
busy: raw?.busy,
|
|
654
|
+
offscreen: raw?.offscreen,
|
|
655
|
+
automationId: raw?.automationId ?? undefined,
|
|
656
|
+
processId: typeof raw?.processId === 'number' ? raw.processId : undefined,
|
|
657
|
+
};
|
|
658
|
+
};
|
|
659
|
+
// ─── INPUT (mouse) ────────────────────────────────────────────────
|
|
660
|
+
toNutButton(button) {
|
|
661
|
+
if (button === 'right')
|
|
662
|
+
return nut_js_1.Button.RIGHT;
|
|
663
|
+
if (button === 'middle')
|
|
664
|
+
return nut_js_1.Button.MIDDLE;
|
|
665
|
+
return nut_js_1.Button.LEFT;
|
|
666
|
+
}
|
|
667
|
+
async mouseClick(x, y, opts) {
|
|
668
|
+
const count = opts?.count ?? 1;
|
|
669
|
+
const btn = opts?.button ?? 'left';
|
|
670
|
+
// Wayland: ydotool handles both move + click.
|
|
671
|
+
if (this.wayland.canMouse()) {
|
|
672
|
+
await this.wayland.mouseMoveAbsolute(x, y);
|
|
673
|
+
this.lastCursor = { x, y };
|
|
674
|
+
await this.delay(30);
|
|
675
|
+
await this.wayland.mouseClick(btn, count);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
// X11 (or Wayland without ydotool): nut-js.
|
|
679
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
|
|
680
|
+
this.lastCursor = { x, y };
|
|
681
|
+
await this.delay(30);
|
|
682
|
+
const nutBtn = this.toNutButton(btn);
|
|
683
|
+
for (let i = 0; i < count; i++) {
|
|
684
|
+
if (nutBtn === nut_js_1.Button.RIGHT)
|
|
685
|
+
await nut_js_1.mouse.rightClick();
|
|
686
|
+
else if (nutBtn === nut_js_1.Button.MIDDLE) {
|
|
687
|
+
await nut_js_1.mouse.pressButton(nut_js_1.Button.MIDDLE);
|
|
688
|
+
await this.delay(30);
|
|
689
|
+
await nut_js_1.mouse.releaseButton(nut_js_1.Button.MIDDLE);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
await nut_js_1.mouse.click(nut_js_1.Button.LEFT);
|
|
693
|
+
}
|
|
694
|
+
if (i < count - 1)
|
|
695
|
+
await this.delay(50);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async mouseMove(x, y) {
|
|
699
|
+
if (this.wayland.canMouse()) {
|
|
700
|
+
await this.wayland.mouseMoveAbsolute(x, y);
|
|
701
|
+
this.lastCursor = { x, y };
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
|
|
705
|
+
this.lastCursor = { x, y };
|
|
706
|
+
}
|
|
707
|
+
async mouseMoveRelative(dx, dy) {
|
|
708
|
+
if (this.wayland.canMouse()) {
|
|
709
|
+
// ydotool supports relative move natively.
|
|
710
|
+
await this.wayland.mouseMoveRelative(dx, dy);
|
|
711
|
+
if (this.lastCursor) {
|
|
712
|
+
this.lastCursor = { x: this.lastCursor.x + dx, y: this.lastCursor.y + dy };
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// On X11, nut-js getPosition() works. Without ydotool on Wayland it
|
|
717
|
+
// returns (0,0) — we degrade to the cached target from our last
|
|
718
|
+
// mouseMove / mouseClick.
|
|
719
|
+
if (this.environment === 'x11') {
|
|
720
|
+
try {
|
|
721
|
+
const pos = await nut_js_1.mouse.getPosition();
|
|
722
|
+
const nx = Math.round(pos.x + dx);
|
|
723
|
+
const ny = Math.round(pos.y + dy);
|
|
724
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
|
|
725
|
+
this.lastCursor = { x: nx, y: ny };
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
catch { /* fall through */ }
|
|
729
|
+
}
|
|
730
|
+
if (this.lastCursor) {
|
|
731
|
+
const nx = this.lastCursor.x + dx;
|
|
732
|
+
const ny = this.lastCursor.y + dy;
|
|
733
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
|
|
734
|
+
this.lastCursor = { x: nx, y: ny };
|
|
735
|
+
}
|
|
736
|
+
// No cache, no query — silently no-op rather than warp the cursor to (0,0).
|
|
737
|
+
}
|
|
738
|
+
async mouseDrag(x1, y1, x2, y2) {
|
|
739
|
+
if (this.wayland.canMouse()) {
|
|
740
|
+
// Wayland: absolute move to start, down, interpolated moves, up.
|
|
741
|
+
await this.wayland.mouseMoveAbsolute(x1, y1);
|
|
742
|
+
this.lastCursor = { x: x1, y: y1 };
|
|
743
|
+
await this.delay(50);
|
|
744
|
+
await this.wayland.mouseDown('left');
|
|
745
|
+
await this.delay(80);
|
|
746
|
+
const steps = Math.max(8, Math.floor(Math.hypot(x2 - x1, y2 - y1) / 18));
|
|
747
|
+
for (let i = 1; i <= steps; i++) {
|
|
748
|
+
const t = i / steps;
|
|
749
|
+
const nx = Math.round(x1 + (x2 - x1) * t);
|
|
750
|
+
const ny = Math.round(y1 + (y2 - y1) * t);
|
|
751
|
+
await this.wayland.mouseMoveAbsolute(nx, ny);
|
|
752
|
+
this.lastCursor = { x: nx, y: ny };
|
|
753
|
+
await this.delay(10);
|
|
754
|
+
}
|
|
755
|
+
await this.wayland.mouseUp('left');
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
// X11 path — unchanged nut-js.
|
|
759
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x1, y1));
|
|
760
|
+
this.lastCursor = { x: x1, y: y1 };
|
|
761
|
+
await this.delay(50);
|
|
762
|
+
await nut_js_1.mouse.pressButton(nut_js_1.Button.LEFT);
|
|
763
|
+
await this.delay(80);
|
|
764
|
+
const steps = Math.max(8, Math.floor(Math.hypot(x2 - x1, y2 - y1) / 18));
|
|
765
|
+
for (let i = 1; i <= steps; i++) {
|
|
766
|
+
const t = i / steps;
|
|
767
|
+
const nx = Math.round(x1 + (x2 - x1) * t);
|
|
768
|
+
const ny = Math.round(y1 + (y2 - y1) * t);
|
|
769
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
|
|
770
|
+
this.lastCursor = { x: nx, y: ny };
|
|
771
|
+
await this.delay(10);
|
|
772
|
+
}
|
|
773
|
+
await nut_js_1.mouse.releaseButton(nut_js_1.Button.LEFT);
|
|
774
|
+
}
|
|
775
|
+
async mouseScroll(x, y, direction, amount = 3) {
|
|
776
|
+
if (this.wayland.canMouse()) {
|
|
777
|
+
await this.wayland.mouseMoveAbsolute(x, y);
|
|
778
|
+
this.lastCursor = { x, y };
|
|
779
|
+
await this.delay(30);
|
|
780
|
+
await this.wayland.mouseScroll(direction, amount);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
|
|
784
|
+
this.lastCursor = { x, y };
|
|
785
|
+
await this.delay(30);
|
|
786
|
+
if (direction === 'down')
|
|
787
|
+
await nut_js_1.mouse.scrollDown(amount);
|
|
788
|
+
else if (direction === 'up')
|
|
789
|
+
await nut_js_1.mouse.scrollUp(amount);
|
|
790
|
+
else {
|
|
791
|
+
// Horizontal: xdotool uses buttons 6 (left) / 7 (right) for horizontal wheel.
|
|
792
|
+
// Fall back to Shift+wheel where xdotool is missing.
|
|
793
|
+
if (await this.hasBinary('xdotool')) {
|
|
794
|
+
const btn = direction === 'left' ? '6' : '7';
|
|
795
|
+
try {
|
|
796
|
+
for (let i = 0; i < amount; i++) {
|
|
797
|
+
await execFileAsync('xdotool', ['click', btn], { timeout: TOOL_TIMEOUT_MS });
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
catch { /* fall through */ }
|
|
802
|
+
}
|
|
803
|
+
await nut_js_1.keyboard.pressKey(nut_js_1.Key.LeftShift);
|
|
804
|
+
try {
|
|
805
|
+
if (direction === 'left')
|
|
806
|
+
await nut_js_1.mouse.scrollUp(amount);
|
|
807
|
+
else
|
|
808
|
+
await nut_js_1.mouse.scrollDown(amount);
|
|
809
|
+
}
|
|
810
|
+
finally {
|
|
811
|
+
await nut_js_1.keyboard.releaseKey(nut_js_1.Key.LeftShift);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async mouseDown(button) {
|
|
816
|
+
if (this.wayland.canMouse()) {
|
|
817
|
+
await this.wayland.mouseDown(button ?? 'left');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
await nut_js_1.mouse.pressButton(this.toNutButton(button));
|
|
821
|
+
}
|
|
822
|
+
async mouseUp(button) {
|
|
823
|
+
if (this.wayland.canMouse()) {
|
|
824
|
+
await this.wayland.mouseUp(button ?? 'left');
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await nut_js_1.mouse.releaseButton(this.toNutButton(button));
|
|
828
|
+
}
|
|
829
|
+
// ─── INPUT (keyboard) ─────────────────────────────────────────────
|
|
830
|
+
async typeText(text) {
|
|
831
|
+
if (!text)
|
|
832
|
+
return;
|
|
833
|
+
if (this.wayland.canKeyboard()) {
|
|
834
|
+
await this.wayland.typeText(text);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
await nut_js_1.keyboard.type(text);
|
|
838
|
+
}
|
|
839
|
+
async keyPress(combo) {
|
|
840
|
+
if (!combo)
|
|
841
|
+
return;
|
|
842
|
+
if (this.wayland.canKeyboard()) {
|
|
843
|
+
await this.wayland.keyPress(combo);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
// Literal "+" — can't split on it.
|
|
847
|
+
if (combo === '+') {
|
|
848
|
+
await nut_js_1.keyboard.type('+');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const parts = combo.split('+').map(s => s.trim()).filter(Boolean);
|
|
852
|
+
const keyName = parts[parts.length - 1];
|
|
853
|
+
const modNames = parts.slice(0, -1);
|
|
854
|
+
const modKeys = modNames.map(m => this.resolveModifier(m)).filter((k) => k !== null);
|
|
855
|
+
const mainKey = this.resolveKey(keyName);
|
|
856
|
+
if (mainKey === null) {
|
|
857
|
+
// Unknown multi-char key — best-effort type as text.
|
|
858
|
+
if (modKeys.length === 0 && keyName.length > 0) {
|
|
859
|
+
await nut_js_1.keyboard.type(keyName);
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
if (modKeys.length > 0) {
|
|
865
|
+
await nut_js_1.keyboard.pressKey(...modKeys, mainKey);
|
|
866
|
+
await nut_js_1.keyboard.releaseKey(...modKeys, mainKey);
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
await nut_js_1.keyboard.pressKey(mainKey);
|
|
870
|
+
await nut_js_1.keyboard.releaseKey(mainKey);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
/* non-fatal — caller likely has a retry or fallback */
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async keyDown(key) {
|
|
878
|
+
if (this.wayland.canKeyboard()) {
|
|
879
|
+
await this.wayland.keyDown(key);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const lower = key.trim().toLowerCase();
|
|
883
|
+
const modKey = this.resolveModifier(lower);
|
|
884
|
+
if (modKey !== null) {
|
|
885
|
+
await nut_js_1.keyboard.pressKey(modKey).catch(() => { });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const k = this.resolveKey(lower.length === 1 ? lower : lower);
|
|
889
|
+
if (k !== null) {
|
|
890
|
+
await nut_js_1.keyboard.pressKey(k).catch(() => { });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
// Printable char without nut-js Key mapping — type it (no hold semantics).
|
|
894
|
+
if (key.length === 1) {
|
|
895
|
+
await nut_js_1.keyboard.type(key).catch(() => { });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async keyUp(key) {
|
|
899
|
+
if (this.wayland.canKeyboard()) {
|
|
900
|
+
await this.wayland.keyUp(key);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const lower = key.trim().toLowerCase();
|
|
904
|
+
const modKey = this.resolveModifier(lower);
|
|
905
|
+
if (modKey !== null) {
|
|
906
|
+
await nut_js_1.keyboard.releaseKey(modKey).catch(() => { });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const k = this.resolveKey(lower.length === 1 ? lower : lower);
|
|
910
|
+
if (k !== null) {
|
|
911
|
+
await nut_js_1.keyboard.releaseKey(k).catch(() => { });
|
|
912
|
+
}
|
|
913
|
+
// Printable non-mapped: no-op — nothing was held.
|
|
914
|
+
}
|
|
915
|
+
resolveModifier(name) {
|
|
916
|
+
const m = name.toLowerCase();
|
|
917
|
+
// "mod" on Linux → Control (brief §9).
|
|
918
|
+
if (m === 'mod' || m === 'ctrl' || m === 'control')
|
|
919
|
+
return nut_js_1.Key.LeftControl;
|
|
920
|
+
if (m === 'shift')
|
|
921
|
+
return nut_js_1.Key.LeftShift;
|
|
922
|
+
if (m === 'alt' || m === 'option' || m === 'opt')
|
|
923
|
+
return nut_js_1.Key.LeftAlt;
|
|
924
|
+
if (m === 'super' || m === 'cmd' || m === 'command' || m === 'meta' || m === 'win')
|
|
925
|
+
return nut_js_1.Key.LeftSuper;
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
resolveKey(name) {
|
|
929
|
+
if (!name)
|
|
930
|
+
return null;
|
|
931
|
+
const lower = name.toLowerCase();
|
|
932
|
+
// Special / named keys.
|
|
933
|
+
const named = LINUX_SPECIAL_KEYS[lower];
|
|
934
|
+
if (named !== undefined)
|
|
935
|
+
return named;
|
|
936
|
+
// Single printable char: letters a-z, digits 0-9, a few punctuation.
|
|
937
|
+
if (name.length === 1) {
|
|
938
|
+
const code = name.toUpperCase().charCodeAt(0);
|
|
939
|
+
// Letters A-Z → Key.A .. Key.Z
|
|
940
|
+
if (code >= 65 && code <= 90) {
|
|
941
|
+
const keyName = name.toUpperCase();
|
|
942
|
+
const k = nut_js_1.Key[keyName];
|
|
943
|
+
if (typeof k === 'number')
|
|
944
|
+
return k;
|
|
945
|
+
}
|
|
946
|
+
// Digits 0-9 → Key.Num0 .. Key.Num9
|
|
947
|
+
if (code >= 0x30 && code <= 0x39) {
|
|
948
|
+
const keyName = `Num${name}`;
|
|
949
|
+
const k = nut_js_1.Key[keyName];
|
|
950
|
+
if (typeof k === 'number')
|
|
951
|
+
return k;
|
|
952
|
+
}
|
|
953
|
+
// Common punctuation
|
|
954
|
+
const punct = PUNCTUATION_KEYS[name];
|
|
955
|
+
if (punct !== undefined)
|
|
956
|
+
return punct;
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
// ─── CLIPBOARD ────────────────────────────────────────────────────
|
|
961
|
+
async readClipboard() {
|
|
962
|
+
// Prefer wl-paste on Wayland; xclip on X11. Fall through when neither
|
|
963
|
+
// is installed — clipboard is best-effort, same contract as macOS.
|
|
964
|
+
if (this.environment === 'wayland' && (await this.hasBinary('wl-paste'))) {
|
|
965
|
+
try {
|
|
966
|
+
const { stdout } = await execFileAsync('wl-paste', ['--no-newline'], { timeout: TOOL_TIMEOUT_MS });
|
|
967
|
+
return stdout;
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
return '';
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (!(await this.hasBinary('xclip')))
|
|
974
|
+
return '';
|
|
975
|
+
try {
|
|
976
|
+
const { stdout } = await execFileAsync('xclip', ['-selection', 'clipboard', '-o'], { timeout: TOOL_TIMEOUT_MS });
|
|
977
|
+
return stdout;
|
|
978
|
+
}
|
|
979
|
+
catch {
|
|
980
|
+
return '';
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async writeClipboard(text) {
|
|
984
|
+
const tool = this.environment === 'wayland' && (await this.hasBinary('wl-copy')) ? 'wl-copy'
|
|
985
|
+
: (await this.hasBinary('xclip')) ? 'xclip' : null;
|
|
986
|
+
if (!tool)
|
|
987
|
+
return;
|
|
988
|
+
const args = tool === 'wl-copy' ? [] : ['-selection', 'clipboard'];
|
|
989
|
+
await new Promise((resolve) => {
|
|
990
|
+
try {
|
|
991
|
+
const proc = (0, child_process_1.spawn)(tool, args);
|
|
992
|
+
const timer = setTimeout(() => {
|
|
993
|
+
proc.kill();
|
|
994
|
+
resolve();
|
|
995
|
+
}, TOOL_TIMEOUT_MS);
|
|
996
|
+
proc.on('close', () => {
|
|
997
|
+
clearTimeout(timer);
|
|
998
|
+
resolve();
|
|
999
|
+
});
|
|
1000
|
+
proc.on('error', () => {
|
|
1001
|
+
clearTimeout(timer);
|
|
1002
|
+
resolve();
|
|
1003
|
+
});
|
|
1004
|
+
proc.stdin.write(text);
|
|
1005
|
+
proc.stdin.end();
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
resolve();
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
// ─── APPS ─────────────────────────────────────────────────────────
|
|
1013
|
+
/**
|
|
1014
|
+
* Thin shim — delegates straight to `launchApp` with no alias resolution.
|
|
1015
|
+
* The platform layer is alias-data-agnostic; cross-OS name mapping (e.g.
|
|
1016
|
+
* Windows-style "Notepad" → Linux "gedit") happens in the caller above
|
|
1017
|
+
* (the agent's `open_app` tool, the router's `handleOpenApp`).
|
|
1018
|
+
*/
|
|
1019
|
+
async openApp(name, opts) {
|
|
1020
|
+
return this.launchApp(name, opts);
|
|
1021
|
+
}
|
|
1022
|
+
async launchApp(name, opts) {
|
|
1023
|
+
// uwpAppId is Windows-only — ignore on Linux. searchTerm currently has no
|
|
1024
|
+
// universal Linux launcher to drive (krunner / Activities / wmenu vary
|
|
1025
|
+
// by DE), so we simply accept it for interface parity.
|
|
1026
|
+
void opts?.uwpAppId;
|
|
1027
|
+
void opts?.searchTerm;
|
|
1028
|
+
if (/[\r\n\t\x00-\x1f]/.test(name)) {
|
|
1029
|
+
throw new Error('launchApp: illegal characters in app name');
|
|
1030
|
+
}
|
|
1031
|
+
// Snapshot windows BEFORE any spawn so the post-launch diff-and-poll
|
|
1032
|
+
// helper can ignore them. Reused for the idempotency check.
|
|
1033
|
+
let windowsBefore = [];
|
|
1034
|
+
try {
|
|
1035
|
+
windowsBefore = await this.listWindows();
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
// Non-fatal — empty before-set is a safe default.
|
|
1039
|
+
}
|
|
1040
|
+
// v0.8.3 — idempotency. Check for an existing window before spawning.
|
|
1041
|
+
// Linux doesn't have macOS's "open -a" activation semantics, so a
|
|
1042
|
+
// second spawn of the same binary would normally create a second
|
|
1043
|
+
// instance. Focus the existing one and return its pid instead.
|
|
1044
|
+
if (!opts?.alwaysNewInstance && !opts?.url) {
|
|
1045
|
+
const target = name.trim().toLowerCase();
|
|
1046
|
+
const existing = windowsBefore.find(w => w.processName.toLowerCase() === target ||
|
|
1047
|
+
w.processName.toLowerCase().includes(target) ||
|
|
1048
|
+
w.title.toLowerCase().includes(target));
|
|
1049
|
+
if (existing) {
|
|
1050
|
+
await this.focusWindow({ processId: existing.processId }).catch(() => { });
|
|
1051
|
+
return { pid: existing.processId, title: existing.title, handle: existing.handle };
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// 1) Try the bare executable name directly (detached so it survives us).
|
|
1055
|
+
const directArgs = [];
|
|
1056
|
+
if (opts?.url)
|
|
1057
|
+
directArgs.push(opts.url);
|
|
1058
|
+
const direct = await this.spawnDetached(name, directArgs, opts?.cwd);
|
|
1059
|
+
if (direct.ok) {
|
|
1060
|
+
const match = await this.findSpawnedWindow(name, windowsBefore, direct.pid);
|
|
1061
|
+
return match ?? { pid: direct.pid };
|
|
1062
|
+
}
|
|
1063
|
+
// 2) Fallback to xdg-open (handles desktop-file names, URLs, file paths).
|
|
1064
|
+
const target = opts?.url ?? name;
|
|
1065
|
+
if (await this.hasBinary('xdg-open')) {
|
|
1066
|
+
const fallback = await this.spawnDetached('xdg-open', [target], opts?.cwd);
|
|
1067
|
+
if (fallback.ok) {
|
|
1068
|
+
// xdg-open's PID isn't the eventual app's PID — drop the spawn pid
|
|
1069
|
+
// and rely on the predicate.
|
|
1070
|
+
const match = await this.findSpawnedWindow(name, windowsBefore);
|
|
1071
|
+
return match ?? {};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return {};
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* After a launch, wait for the new window to surface using diff-and-poll.
|
|
1078
|
+
* Replaces the previous fixed-delay-then-single-scan, which lost slow-
|
|
1079
|
+
* starting apps (Electron-based apps, Java apps, Wine apps) and made the
|
|
1080
|
+
* agent fall back to keyboard shortcuts that are blocked by safety.
|
|
1081
|
+
*
|
|
1082
|
+
* Linux's `spawn(name, …)` directly produces the target app's PID (unlike
|
|
1083
|
+
* Windows / macOS where the spawn target is a launcher), so when `pid` is
|
|
1084
|
+
* supplied the helper will preferentially match it — fastest and most
|
|
1085
|
+
* reliable when the binary spawns its own window.
|
|
1086
|
+
*/
|
|
1087
|
+
async findSpawnedWindow(name, windowsBefore, pid) {
|
|
1088
|
+
const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), { spawnPid: pid });
|
|
1089
|
+
return win
|
|
1090
|
+
? { pid: win.processId, title: win.title, handle: win.handle }
|
|
1091
|
+
: null;
|
|
1092
|
+
}
|
|
1093
|
+
spawnDetached(cmd, args, cwd) {
|
|
1094
|
+
return new Promise((resolve) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const proc = (0, child_process_1.spawn)(cmd, args, { detached: true, stdio: 'ignore', cwd });
|
|
1097
|
+
let settled = false;
|
|
1098
|
+
// "error" fires synchronously-ish for ENOENT.
|
|
1099
|
+
proc.on('error', () => {
|
|
1100
|
+
if (!settled) {
|
|
1101
|
+
settled = true;
|
|
1102
|
+
resolve({ ok: false });
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
// If it launches cleanly, we get a pid immediately.
|
|
1106
|
+
setImmediate(() => {
|
|
1107
|
+
if (settled)
|
|
1108
|
+
return;
|
|
1109
|
+
if (proc.pid) {
|
|
1110
|
+
settled = true;
|
|
1111
|
+
try {
|
|
1112
|
+
proc.unref();
|
|
1113
|
+
}
|
|
1114
|
+
catch { /* */ }
|
|
1115
|
+
resolve({ ok: true, pid: proc.pid });
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
settled = true;
|
|
1119
|
+
resolve({ ok: false });
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
resolve({ ok: false });
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
// ─── INTERNAL HELPERS ─────────────────────────────────────────────
|
|
1129
|
+
async hasBinary(name) {
|
|
1130
|
+
const cached = this.binaryCache.get(name);
|
|
1131
|
+
if (cached !== undefined)
|
|
1132
|
+
return cached;
|
|
1133
|
+
try {
|
|
1134
|
+
await execFileAsync('command', ['-v', name], { timeout: 1_500, shell: '/bin/sh' });
|
|
1135
|
+
this.binaryCache.set(name, true);
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
// Fallback: try `which`.
|
|
1140
|
+
try {
|
|
1141
|
+
await execFileAsync('which', [name], { timeout: 1_500 });
|
|
1142
|
+
this.binaryCache.set(name, true);
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
this.binaryCache.set(name, false);
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
parseWmctrlOutput(stdout) {
|
|
1152
|
+
const results = [];
|
|
1153
|
+
for (const rawLine of stdout.split('\n')) {
|
|
1154
|
+
const line = rawLine.trim();
|
|
1155
|
+
if (!line)
|
|
1156
|
+
continue;
|
|
1157
|
+
// With -l -G -p: windowId desktop pid x y width height host title...
|
|
1158
|
+
const parts = line.split(/\s+/);
|
|
1159
|
+
if (parts.length < 9)
|
|
1160
|
+
continue;
|
|
1161
|
+
const windowId = parseInt(parts[0], 16);
|
|
1162
|
+
const desktop = parseInt(parts[1], 10);
|
|
1163
|
+
const pid = parseInt(parts[2], 10);
|
|
1164
|
+
const x = parseInt(parts[3], 10);
|
|
1165
|
+
const y = parseInt(parts[4], 10);
|
|
1166
|
+
const w = parseInt(parts[5], 10);
|
|
1167
|
+
const h = parseInt(parts[6], 10);
|
|
1168
|
+
// parts[7] = host; title is everything after.
|
|
1169
|
+
const title = parts.slice(8).join(' ');
|
|
1170
|
+
if (!title || title === 'Desktop')
|
|
1171
|
+
continue;
|
|
1172
|
+
results.push({
|
|
1173
|
+
title,
|
|
1174
|
+
processName: '', // wmctrl doesn't expose process name
|
|
1175
|
+
processId: Number.isFinite(pid) ? pid : 0,
|
|
1176
|
+
bounds: {
|
|
1177
|
+
x: Number.isFinite(x) ? x : 0,
|
|
1178
|
+
y: Number.isFinite(y) ? y : 0,
|
|
1179
|
+
width: Number.isFinite(w) ? w : 0,
|
|
1180
|
+
height: Number.isFinite(h) ? h : 0,
|
|
1181
|
+
},
|
|
1182
|
+
// wmctrl -l -G -p doesn't report minimized state directly; desktop=-1 is "all",
|
|
1183
|
+
// not minimized. Minimized windows still appear in this list.
|
|
1184
|
+
isMinimized: desktop === -1 ? false : false,
|
|
1185
|
+
handle: Number.isFinite(windowId) ? windowId : undefined,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return results;
|
|
1189
|
+
}
|
|
1190
|
+
withTimeout(p, ms, label) {
|
|
1191
|
+
return new Promise((resolve, reject) => {
|
|
1192
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
1193
|
+
p.then(v => { clearTimeout(timer); resolve(v); })
|
|
1194
|
+
.catch(e => { clearTimeout(timer); reject(e); });
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
delay(ms) {
|
|
1198
|
+
return new Promise(r => setTimeout(r, ms));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
exports.LinuxAdapter = LinuxAdapter;
|
|
1202
|
+
/**
|
|
1203
|
+
* Detect Linux display server. Wayland reports itself via `XDG_SESSION_TYPE`
|
|
1204
|
+
* or `WAYLAND_DISPLAY`; everything else defaults to X11. `detect-once-at-init`
|
|
1205
|
+
* semantics — the compositor doesn't change mid-session.
|
|
1206
|
+
*/
|
|
1207
|
+
function detectLinuxEnvironment() {
|
|
1208
|
+
const sessionType = (process.env.XDG_SESSION_TYPE || '').toLowerCase();
|
|
1209
|
+
if (sessionType === 'wayland')
|
|
1210
|
+
return 'wayland';
|
|
1211
|
+
if (sessionType === 'x11')
|
|
1212
|
+
return 'x11';
|
|
1213
|
+
if (process.env.WAYLAND_DISPLAY)
|
|
1214
|
+
return 'wayland';
|
|
1215
|
+
return 'x11';
|
|
1216
|
+
}
|
|
1217
|
+
// Named-key table — lowercase lookup, maps to nut-js Key enum.
|
|
1218
|
+
const LINUX_SPECIAL_KEYS = {
|
|
1219
|
+
'return': nut_js_1.Key.Return, 'enter': nut_js_1.Key.Enter,
|
|
1220
|
+
'tab': nut_js_1.Key.Tab,
|
|
1221
|
+
'space': nut_js_1.Key.Space,
|
|
1222
|
+
'backspace': nut_js_1.Key.Backspace,
|
|
1223
|
+
'delete': nut_js_1.Key.Delete,
|
|
1224
|
+
'escape': nut_js_1.Key.Escape, 'esc': nut_js_1.Key.Escape,
|
|
1225
|
+
'left': nut_js_1.Key.Left, 'right': nut_js_1.Key.Right, 'up': nut_js_1.Key.Up, 'down': nut_js_1.Key.Down,
|
|
1226
|
+
'home': nut_js_1.Key.Home, 'end': nut_js_1.Key.End,
|
|
1227
|
+
'pageup': nut_js_1.Key.PageUp, 'pagedown': nut_js_1.Key.PageDown,
|
|
1228
|
+
'insert': nut_js_1.Key.Insert,
|
|
1229
|
+
'capslock': nut_js_1.Key.CapsLock,
|
|
1230
|
+
'numlock': nut_js_1.Key.NumLock,
|
|
1231
|
+
'scrolllock': nut_js_1.Key.ScrollLock,
|
|
1232
|
+
'pause': nut_js_1.Key.Pause,
|
|
1233
|
+
'print': nut_js_1.Key.Print,
|
|
1234
|
+
'menu': nut_js_1.Key.Menu,
|
|
1235
|
+
'f1': nut_js_1.Key.F1, 'f2': nut_js_1.Key.F2, 'f3': nut_js_1.Key.F3, 'f4': nut_js_1.Key.F4,
|
|
1236
|
+
'f5': nut_js_1.Key.F5, 'f6': nut_js_1.Key.F6, 'f7': nut_js_1.Key.F7, 'f8': nut_js_1.Key.F8,
|
|
1237
|
+
'f9': nut_js_1.Key.F9, 'f10': nut_js_1.Key.F10, 'f11': nut_js_1.Key.F11, 'f12': nut_js_1.Key.F12,
|
|
1238
|
+
};
|
|
1239
|
+
// Punctuation keys — nut-js enum names differ from the character.
|
|
1240
|
+
const PUNCTUATION_KEYS = {
|
|
1241
|
+
'-': nut_js_1.Key.Minus,
|
|
1242
|
+
'=': nut_js_1.Key.Equal,
|
|
1243
|
+
'[': nut_js_1.Key.LeftBracket,
|
|
1244
|
+
']': nut_js_1.Key.RightBracket,
|
|
1245
|
+
'\\': nut_js_1.Key.Backslash,
|
|
1246
|
+
';': nut_js_1.Key.Semicolon,
|
|
1247
|
+
"'": nut_js_1.Key.Quote,
|
|
1248
|
+
',': nut_js_1.Key.Comma,
|
|
1249
|
+
'.': nut_js_1.Key.Period,
|
|
1250
|
+
'/': nut_js_1.Key.Slash,
|
|
1251
|
+
'`': nut_js_1.Key.Grave,
|
|
1252
|
+
};
|
|
1253
|
+
//# sourceMappingURL=linux.js.map
|