@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,1210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Windows PlatformAdapter — all Windows-specific code lives here.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* - Mouse + keyboard: nut-js directly (no TCC blocking as on macOS)
|
|
7
|
+
* - Screenshot: nut-js screen.grab() — no special helper binary
|
|
8
|
+
* - Screen size + DPI: System.Windows.Forms.Screen via PowerShell for logical px,
|
|
9
|
+
* compared with nut-js physical px to derive dpiRatio
|
|
10
|
+
* - Windows + A11y: persistent PSRunner (../../ps-runner.ts) driving UI Automation
|
|
11
|
+
* - Clipboard: Get-Clipboard / Set-Clipboard via PowerShell
|
|
12
|
+
* - App launch: Start-Process via PowerShell
|
|
13
|
+
*
|
|
14
|
+
* Permissions: Windows has no TCC-style gate — returns all-true.
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.WindowsAdapter = void 0;
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
const util_1 = require("util");
|
|
23
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
24
|
+
const nut_js_1 = require("@nut-tree-fork/nut-js");
|
|
25
|
+
const ps_runner_1 = require("./ps-runner");
|
|
26
|
+
const launch_poll_1 = require("./launch-poll");
|
|
27
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
28
|
+
// Tunables
|
|
29
|
+
const PS_TIMEOUT_MS = 8_000;
|
|
30
|
+
const CLIPBOARD_TIMEOUT_MS = 3_000;
|
|
31
|
+
class WindowsAdapter {
|
|
32
|
+
platform = 'win32';
|
|
33
|
+
screenSize = null;
|
|
34
|
+
async init() {
|
|
35
|
+
// Configure nut-js for snappy input; same tuning as native-desktop.ts.
|
|
36
|
+
nut_js_1.mouse.config.mouseSpeed = 2000;
|
|
37
|
+
nut_js_1.mouse.config.autoDelayMs = 0;
|
|
38
|
+
nut_js_1.keyboard.config.autoDelayMs = 0;
|
|
39
|
+
// Kick off the PowerShell bridge so the ~800ms UIA assembly load happens
|
|
40
|
+
// in the background. Errors surface on first real a11y call.
|
|
41
|
+
ps_runner_1.psRunner.start().catch(() => { });
|
|
42
|
+
// Pre-warm screen size so the first capture / first click isn't paying for it.
|
|
43
|
+
await this.getScreenSize().catch(() => null);
|
|
44
|
+
}
|
|
45
|
+
async shutdown() {
|
|
46
|
+
try {
|
|
47
|
+
ps_runner_1.psRunner.stop();
|
|
48
|
+
}
|
|
49
|
+
catch { /* */ }
|
|
50
|
+
}
|
|
51
|
+
// ─── PERMISSIONS ──────────────────────────────────────────────────
|
|
52
|
+
async checkPermissions() {
|
|
53
|
+
// Windows doesn't gate any of these behind TCC-style prompts. If the
|
|
54
|
+
// user can run the binary at all, they can do input / capture / a11y.
|
|
55
|
+
return { input: true, accessibility: true, screenRecording: true };
|
|
56
|
+
}
|
|
57
|
+
async requestPermissions() {
|
|
58
|
+
return this.checkPermissions();
|
|
59
|
+
}
|
|
60
|
+
// ─── DISPLAY ──────────────────────────────────────────────────────
|
|
61
|
+
async getScreenSize() {
|
|
62
|
+
if (this.screenSize)
|
|
63
|
+
return this.screenSize;
|
|
64
|
+
// nut-js screen.grab() returns PHYSICAL pixels on Windows.
|
|
65
|
+
let physicalWidth = 0, physicalHeight = 0;
|
|
66
|
+
try {
|
|
67
|
+
const img = await nut_js_1.screen.grab();
|
|
68
|
+
physicalWidth = img.width;
|
|
69
|
+
physicalHeight = img.height;
|
|
70
|
+
img.data = null;
|
|
71
|
+
}
|
|
72
|
+
catch { /* fall through with zeros */ }
|
|
73
|
+
// System.Windows.Forms.Screen returns LOGICAL (DPI-scaled) pixels on Win —
|
|
74
|
+
// that's the coordinate space nut-js mouse API expects.
|
|
75
|
+
let logicalWidth = physicalWidth;
|
|
76
|
+
let logicalHeight = physicalHeight;
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await execFileAsync('powershell.exe', [
|
|
79
|
+
'-NoProfile',
|
|
80
|
+
'-Command',
|
|
81
|
+
'Add-Type -AssemblyName System.Windows.Forms; ' +
|
|
82
|
+
'$s=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; ' +
|
|
83
|
+
'"$($s.Width),$($s.Height)"',
|
|
84
|
+
], { timeout: PS_TIMEOUT_MS });
|
|
85
|
+
const [w, h] = stdout.trim().split(',').map(s => parseInt(s, 10));
|
|
86
|
+
if (w > 0 && h > 0) {
|
|
87
|
+
logicalWidth = w;
|
|
88
|
+
logicalHeight = h;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { /* non-fatal — fall back to physical */ }
|
|
92
|
+
if (!physicalWidth)
|
|
93
|
+
physicalWidth = logicalWidth;
|
|
94
|
+
if (!physicalHeight)
|
|
95
|
+
physicalHeight = logicalHeight;
|
|
96
|
+
const dpiRatio = physicalWidth > logicalWidth ? physicalWidth / logicalWidth : 1;
|
|
97
|
+
this.screenSize = {
|
|
98
|
+
physicalWidth,
|
|
99
|
+
physicalHeight,
|
|
100
|
+
logicalWidth,
|
|
101
|
+
logicalHeight,
|
|
102
|
+
dpiRatio,
|
|
103
|
+
};
|
|
104
|
+
return this.screenSize;
|
|
105
|
+
}
|
|
106
|
+
async listDisplays() {
|
|
107
|
+
// System.Windows.Forms.Screen.AllScreens enumerates every connected
|
|
108
|
+
// display with bounds + primary flag. We call it via the PS UIA path
|
|
109
|
+
// we already have warmed up.
|
|
110
|
+
try {
|
|
111
|
+
const { stdout } = await execFileAsync('powershell.exe', [
|
|
112
|
+
'-NoProfile',
|
|
113
|
+
'-Command',
|
|
114
|
+
'Add-Type -AssemblyName System.Windows.Forms; ' +
|
|
115
|
+
'[System.Windows.Forms.Screen]::AllScreens | ForEach-Object { ' +
|
|
116
|
+
' $b = $_.Bounds; ' +
|
|
117
|
+
' [pscustomobject]@{ ' +
|
|
118
|
+
' name = $_.DeviceName; ' +
|
|
119
|
+
' primary = $_.Primary; ' +
|
|
120
|
+
' x = $b.X; y = $b.Y; w = $b.Width; h = $b.Height ' +
|
|
121
|
+
' } ' +
|
|
122
|
+
'} | ConvertTo-Json -Compress',
|
|
123
|
+
], { timeout: PS_TIMEOUT_MS });
|
|
124
|
+
const raw = JSON.parse(stdout.trim() || '[]');
|
|
125
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
126
|
+
// Physical pixel dimensions: we can only confidently compute these for
|
|
127
|
+
// the primary display (via our cached ScreenSize). For secondaries we
|
|
128
|
+
// assume the same dpiRatio — accurate on homogeneous setups, a safe
|
|
129
|
+
// approximation on mixed-DPI (caller can override per-monitor later).
|
|
130
|
+
const size = await this.getScreenSize();
|
|
131
|
+
return arr.map((s, i) => {
|
|
132
|
+
const w = Number(s.w) || 0;
|
|
133
|
+
const h = Number(s.h) || 0;
|
|
134
|
+
return {
|
|
135
|
+
index: i,
|
|
136
|
+
label: String(s.name || `Display ${i + 1}`),
|
|
137
|
+
primary: !!s.primary,
|
|
138
|
+
bounds: { x: Number(s.x) || 0, y: Number(s.y) || 0, width: w, height: h },
|
|
139
|
+
physicalSize: {
|
|
140
|
+
width: Math.round(w * size.dpiRatio),
|
|
141
|
+
height: Math.round(h * size.dpiRatio),
|
|
142
|
+
},
|
|
143
|
+
dpiRatio: size.dpiRatio,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Fallback to single display so callers don't have to special-case.
|
|
149
|
+
const size = await this.getScreenSize();
|
|
150
|
+
return [{
|
|
151
|
+
index: 0,
|
|
152
|
+
label: 'Display 1',
|
|
153
|
+
primary: true,
|
|
154
|
+
bounds: { x: 0, y: 0, width: size.logicalWidth, height: size.logicalHeight },
|
|
155
|
+
physicalSize: { width: size.physicalWidth, height: size.physicalHeight },
|
|
156
|
+
dpiRatio: size.dpiRatio,
|
|
157
|
+
}];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async screenshot(opts) {
|
|
161
|
+
// displayIndex is plumbed through but nut-js's screen.grab() always
|
|
162
|
+
// captures ALL displays combined. For index selection on Windows,
|
|
163
|
+
// we crop to the target display's bounds after the grab.
|
|
164
|
+
const img = await nut_js_1.screen.grab();
|
|
165
|
+
let srcWidth = img.width;
|
|
166
|
+
let srcHeight = img.height;
|
|
167
|
+
let rgba = img.data;
|
|
168
|
+
let pipeline;
|
|
169
|
+
if (opts?.displayIndex !== undefined && opts.displayIndex > 0) {
|
|
170
|
+
const displays = await this.listDisplays();
|
|
171
|
+
const target = displays[opts.displayIndex];
|
|
172
|
+
if (target) {
|
|
173
|
+
// Translate logical bounds into the physical image (nut-js returns
|
|
174
|
+
// hardware pixels; multiply by dpiRatio).
|
|
175
|
+
const r = target.dpiRatio || 1;
|
|
176
|
+
const left = Math.max(0, Math.round(target.bounds.x * r));
|
|
177
|
+
const top = Math.max(0, Math.round(target.bounds.y * r));
|
|
178
|
+
const width = Math.max(1, Math.min(Math.round(target.bounds.width * r), img.width - left));
|
|
179
|
+
const height = Math.max(1, Math.min(Math.round(target.bounds.height * r), img.height - top));
|
|
180
|
+
pipeline = (0, sharp_1.default)(rgba, { raw: { width: img.width, height: img.height, channels: 4 } })
|
|
181
|
+
.extract({ left, top, width, height });
|
|
182
|
+
srcWidth = width;
|
|
183
|
+
srcHeight = height;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
pipeline = (0, sharp_1.default)(rgba, { raw: { width: srcWidth, height: srcHeight, channels: 4 } });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
pipeline = (0, sharp_1.default)(rgba, { raw: { width: srcWidth, height: srcHeight, channels: 4 } });
|
|
191
|
+
}
|
|
192
|
+
let width = srcWidth;
|
|
193
|
+
let height = srcHeight;
|
|
194
|
+
let scaleFactor = 1;
|
|
195
|
+
if (opts?.maxWidth && srcWidth > opts.maxWidth) {
|
|
196
|
+
scaleFactor = srcWidth / opts.maxWidth;
|
|
197
|
+
const newH = Math.round(srcHeight / scaleFactor);
|
|
198
|
+
pipeline = pipeline.resize(opts.maxWidth, newH, { fit: 'fill', kernel: 'lanczos3' });
|
|
199
|
+
width = opts.maxWidth;
|
|
200
|
+
height = newH;
|
|
201
|
+
}
|
|
202
|
+
const buffer = await pipeline.png().toBuffer();
|
|
203
|
+
img.data = null;
|
|
204
|
+
return { buffer, width, height, scaleFactor };
|
|
205
|
+
}
|
|
206
|
+
async screenshotRegion(x, y, w, h) {
|
|
207
|
+
const img = await nut_js_1.screen.grab();
|
|
208
|
+
const rx = Math.max(0, Math.min(x, img.width - 1));
|
|
209
|
+
const ry = Math.max(0, Math.min(y, img.height - 1));
|
|
210
|
+
const rw = Math.min(w, img.width - rx);
|
|
211
|
+
const rh = Math.min(h, img.height - ry);
|
|
212
|
+
const buffer = await (0, sharp_1.default)(img.data, {
|
|
213
|
+
raw: { width: img.width, height: img.height, channels: 4 },
|
|
214
|
+
})
|
|
215
|
+
.extract({ left: rx, top: ry, width: rw, height: rh })
|
|
216
|
+
.png()
|
|
217
|
+
.toBuffer();
|
|
218
|
+
img.data = null;
|
|
219
|
+
return { buffer, width: rw, height: rh, scaleFactor: 1 };
|
|
220
|
+
}
|
|
221
|
+
// ─── WINDOWS ──────────────────────────────────────────────────────
|
|
222
|
+
async listWindows() {
|
|
223
|
+
try {
|
|
224
|
+
const result = await ps_runner_1.psRunner.run({ cmd: 'get-screen-context', maxDepth: 0 });
|
|
225
|
+
const raw = Array.isArray(result?.windows) ? result.windows : [];
|
|
226
|
+
return raw.map(this.normalizeWindow);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async getActiveWindow() {
|
|
233
|
+
try {
|
|
234
|
+
const fg = await ps_runner_1.psRunner.run({ cmd: 'get-foreground-window' });
|
|
235
|
+
if (!fg || fg.success === false)
|
|
236
|
+
return null;
|
|
237
|
+
// Try to find the same window in the full list so we get bounds/minimized.
|
|
238
|
+
const all = await this.listWindows();
|
|
239
|
+
const match = all.find(w => w.processId === fg.processId);
|
|
240
|
+
if (match)
|
|
241
|
+
return match;
|
|
242
|
+
return this.normalizeWindow({
|
|
243
|
+
title: fg.title ?? '',
|
|
244
|
+
processName: fg.processName ?? '',
|
|
245
|
+
processId: fg.processId ?? 0,
|
|
246
|
+
handle: fg.handle,
|
|
247
|
+
bounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
248
|
+
isMinimized: false,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async focusWindow(query) {
|
|
256
|
+
// The PSRunner focus-window command takes title and/or processId. Look up by
|
|
257
|
+
// processName first so callers can pass just that.
|
|
258
|
+
let processId = query.processId;
|
|
259
|
+
let title = query.title;
|
|
260
|
+
if (processId === undefined && query.processName) {
|
|
261
|
+
const target = query.processName.toLowerCase();
|
|
262
|
+
const windows = await this.listWindows();
|
|
263
|
+
const hit = windows.find(w => w.processName.toLowerCase() === target)
|
|
264
|
+
?? windows.find(w => w.processName.toLowerCase().includes(target));
|
|
265
|
+
if (hit)
|
|
266
|
+
processId = hit.processId;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const result = await ps_runner_1.psRunner.run({
|
|
270
|
+
cmd: 'focus-window',
|
|
271
|
+
restore: true,
|
|
272
|
+
...(title !== undefined ? { title } : {}),
|
|
273
|
+
...(processId !== undefined ? { processId } : {}),
|
|
274
|
+
});
|
|
275
|
+
// The PS script reports `success` (target window was found and SetFocus
|
|
276
|
+
// was attempted) and `foreground` (Win32 SetForegroundWindow actually
|
|
277
|
+
// promoted the window). We need foreground=true for subsequent keystroke
|
|
278
|
+
// tools to land on the right app, so treat foreground=false as a focus
|
|
279
|
+
// failure even if SetFocus succeeded. This is the difference between
|
|
280
|
+
// "a11y-focused" and "will receive global SendInput keystrokes".
|
|
281
|
+
if (result?.success !== true)
|
|
282
|
+
return false;
|
|
283
|
+
if (result?.foreground === false)
|
|
284
|
+
return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async maximizeWindow() {
|
|
292
|
+
// Win+Up is the portable Windows maximize shortcut.
|
|
293
|
+
await this.keyPress('super+up').catch(() => { });
|
|
294
|
+
}
|
|
295
|
+
async setWindowState(state, query) {
|
|
296
|
+
// Resolve the target: either the caller-supplied window or the
|
|
297
|
+
// foreground one. We drive the transition through a single PowerShell
|
|
298
|
+
// call that wraps Win32 ShowWindow / PostMessage so we don't depend
|
|
299
|
+
// on focus timing of a key-press chord.
|
|
300
|
+
let pid;
|
|
301
|
+
let hwnd;
|
|
302
|
+
if (query) {
|
|
303
|
+
// Prefer pid resolution when we can — cheaper than listWindows.
|
|
304
|
+
pid = query.processId;
|
|
305
|
+
if (pid === undefined) {
|
|
306
|
+
const match = await this.resolveWindow(query);
|
|
307
|
+
if (match) {
|
|
308
|
+
pid = match.processId;
|
|
309
|
+
const handle = match.handle;
|
|
310
|
+
if (typeof handle === 'number')
|
|
311
|
+
hwnd = handle;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const showCmd = state === 'maximize' ? 3 // SW_MAXIMIZE
|
|
316
|
+
: state === 'minimize' ? 6 // SW_MINIMIZE
|
|
317
|
+
: state === 'normal' ? 9 // SW_RESTORE
|
|
318
|
+
: null;
|
|
319
|
+
const target = hwnd !== undefined
|
|
320
|
+
? `[IntPtr]${hwnd}`
|
|
321
|
+
: pid !== undefined
|
|
322
|
+
? `(Get-Process -Id ${pid}).MainWindowHandle`
|
|
323
|
+
: '[NativeMethods]::GetForegroundWindow()';
|
|
324
|
+
try {
|
|
325
|
+
if (state === 'close') {
|
|
326
|
+
// WM_CLOSE — polite close request. App may prompt, we return true
|
|
327
|
+
// when the message was posted, not when the window actually closed.
|
|
328
|
+
const ps =
|
|
329
|
+
// Single-quoted -MemberDefinition (not a here-string) — a here-string header
|
|
330
|
+
// is illegal in a single-line `-Command` and fails to parse (see #153).
|
|
331
|
+
"Add-Type -Name NativeMethods -Namespace Win32 -MemberDefinition '" +
|
|
332
|
+
'[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();' +
|
|
333
|
+
'[DllImport("user32.dll")] public static extern bool PostMessage(System.IntPtr hWnd, uint Msg, System.IntPtr wParam, System.IntPtr lParam);' +
|
|
334
|
+
"' -PassThru | Out-Null;" +
|
|
335
|
+
`$h = ${target};` +
|
|
336
|
+
'if ($h -ne [System.IntPtr]::Zero) { [Win32.NativeMethods]::PostMessage($h, 0x0010, [System.IntPtr]::Zero, [System.IntPtr]::Zero) | Out-Null; "ok" } else { "no-window" }';
|
|
337
|
+
const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', ps], { timeout: PS_TIMEOUT_MS });
|
|
338
|
+
return stdout.trim() === 'ok';
|
|
339
|
+
}
|
|
340
|
+
if (showCmd !== null) {
|
|
341
|
+
// UWP windows hosted by ApplicationFrameHost ignore a cross-process
|
|
342
|
+
// Win32 ShowWindow(SW_MINIMIZE) — it silently no-ops (#153: minimize
|
|
343
|
+
// failed for Calculator/Settings while maximize/restore worked). Drive
|
|
344
|
+
// the transition through the UIA WindowPattern (the supported
|
|
345
|
+
// cross-process way, and what we already use for restore), which works
|
|
346
|
+
// for UWP *and* Win32. Fall back to ShowWindowAsync (plus SW_FORCEMINIMIZE
|
|
347
|
+
// for the minimize case) only if the pattern isn't available.
|
|
348
|
+
const visualState = state === 'maximize' ? 'Maximized'
|
|
349
|
+
: state === 'minimize' ? 'Minimized'
|
|
350
|
+
: 'Normal';
|
|
351
|
+
const titleQ = this.psQuote(query?.title ?? '');
|
|
352
|
+
const forceMin = state === 'minimize'
|
|
353
|
+
? ' [Win32.NativeMethods]::ShowWindowAsync($nwh, 11) | Out-Null;'
|
|
354
|
+
: '';
|
|
355
|
+
const ps =
|
|
356
|
+
// NB: a here-string header (@"...) is illegal in a single-line `-Command`
|
|
357
|
+
// ("No characters are allowed after a here-string header before the end of
|
|
358
|
+
// the line") — it fails to PARSE, so the whole script silently produced no
|
|
359
|
+
// output and minimize returned false (#153). Use a PS single-quoted
|
|
360
|
+
// -MemberDefinition instead: the C# double-quotes are literal inside it, and
|
|
361
|
+
// Node handles the wire-escaping of those quotes for us.
|
|
362
|
+
'Add-Type -AssemblyName UIAutomationClient,UIAutomationTypes | Out-Null;' +
|
|
363
|
+
"Add-Type -Name NativeMethods -Namespace Win32 -MemberDefinition '" +
|
|
364
|
+
'[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();' +
|
|
365
|
+
'[DllImport("user32.dll")] public static extern bool ShowWindowAsync(System.IntPtr hWnd, int nCmdShow);' +
|
|
366
|
+
"' -PassThru | Out-Null;" +
|
|
367
|
+
`$title = ${titleQ};` +
|
|
368
|
+
`$h = ${target};` +
|
|
369
|
+
'$el = $null;' +
|
|
370
|
+
// Strategy A — find the top-level window by title via UIA. This is the
|
|
371
|
+
// ONLY reliable handle for UWP / ApplicationFrameHost apps, whose
|
|
372
|
+
// visible window is owned by ApplicationFrameHost (so pid→MainWindowHandle
|
|
373
|
+
// is 0/wrong) and whose cross-process ShowWindow(SW_MINIMIZE) no-ops (#153).
|
|
374
|
+
'if ($title -ne "") {' +
|
|
375
|
+
' $root = [System.Windows.Automation.AutomationElement]::RootElement;' +
|
|
376
|
+
' $cond = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, [System.Windows.Automation.ControlType]::Window);' +
|
|
377
|
+
' foreach ($w in $root.FindAll([System.Windows.Automation.TreeScope]::Children, $cond)) {' +
|
|
378
|
+
' $n = $w.Current.Name; if ($n -and $n.ToLower().Contains($title.ToLower())) { $el = $w; break }' +
|
|
379
|
+
' }' +
|
|
380
|
+
'}' +
|
|
381
|
+
// Strategy B — the caller-resolved handle. Strategy C — foreground.
|
|
382
|
+
'if ($el -eq $null -and $h -ne [System.IntPtr]::Zero) { try { $el = [System.Windows.Automation.AutomationElement]::FromHandle($h) } catch {} }' +
|
|
383
|
+
'if ($el -eq $null) { $fg = [Win32.NativeMethods]::GetForegroundWindow(); if ($fg -ne [System.IntPtr]::Zero) { try { $el = [System.Windows.Automation.AutomationElement]::FromHandle($fg) } catch {} } }' +
|
|
384
|
+
'if ($el -eq $null) { "no-window" } else {' +
|
|
385
|
+
' $ok = $false;' +
|
|
386
|
+
` try { $wp = $el.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern); $wp.SetWindowVisualState([System.Windows.Automation.WindowVisualState]::${visualState}); $ok = $true } catch { $ok = $false }` +
|
|
387
|
+
' if (-not $ok) { $nwh = [System.IntPtr]$el.Current.NativeWindowHandle; if ($nwh -ne [System.IntPtr]::Zero) {' +
|
|
388
|
+
` [Win32.NativeMethods]::ShowWindowAsync($nwh, ${showCmd}) | Out-Null;${forceMin}` +
|
|
389
|
+
' $ok = $true } }' +
|
|
390
|
+
' if ($ok) { "ok" } else { "no-window" } }';
|
|
391
|
+
const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', ps], { timeout: PS_TIMEOUT_MS });
|
|
392
|
+
return stdout.trim() === 'ok';
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async setWindowBounds(bounds, query) {
|
|
401
|
+
// SetWindowPos takes hwnd + x/y/w/h. Use SWP_NOZORDER to keep z-order.
|
|
402
|
+
let hwnd;
|
|
403
|
+
if (query) {
|
|
404
|
+
const match = await this.resolveWindow(query);
|
|
405
|
+
if (match && typeof match.handle === 'number')
|
|
406
|
+
hwnd = match.handle;
|
|
407
|
+
}
|
|
408
|
+
const handleExpr = hwnd !== undefined
|
|
409
|
+
? `[IntPtr]${hwnd}`
|
|
410
|
+
: '[Win32.NativeMethods]::GetForegroundWindow()';
|
|
411
|
+
try {
|
|
412
|
+
const x = bounds.x ?? -1;
|
|
413
|
+
const y = bounds.y ?? -1;
|
|
414
|
+
const w = bounds.width ?? -1;
|
|
415
|
+
const h = bounds.height ?? -1;
|
|
416
|
+
// When a dim is -1, we read the current rect and preserve it.
|
|
417
|
+
const ps =
|
|
418
|
+
// Single-quoted -MemberDefinition (not a here-string) — a here-string header
|
|
419
|
+
// is illegal in a single-line `-Command` and fails to parse (see #153).
|
|
420
|
+
"Add-Type -Name NativeMethods -Namespace Win32 -MemberDefinition '" +
|
|
421
|
+
'[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();' +
|
|
422
|
+
'[DllImport("user32.dll")] public static extern bool GetWindowRect(System.IntPtr hWnd, out System.Drawing.Rectangle rect);' +
|
|
423
|
+
'[DllImport("user32.dll")] public static extern bool SetWindowPos(System.IntPtr hWnd, System.IntPtr hWndAfter, int X, int Y, int cx, int cy, uint uFlags);' +
|
|
424
|
+
"' -ReferencedAssemblies System.Drawing -PassThru | Out-Null;" +
|
|
425
|
+
`$h = ${handleExpr};` +
|
|
426
|
+
'if ($h -eq [System.IntPtr]::Zero) { "no-window"; exit }' +
|
|
427
|
+
'$r = New-Object System.Drawing.Rectangle;' +
|
|
428
|
+
'[Win32.NativeMethods]::GetWindowRect($h, [ref] $r) | Out-Null;' +
|
|
429
|
+
`$nx = ${x}; $ny = ${y}; $nw = ${w}; $nh = ${h};` +
|
|
430
|
+
'if ($nx -lt 0) { $nx = $r.X }' +
|
|
431
|
+
'if ($ny -lt 0) { $ny = $r.Y }' +
|
|
432
|
+
'if ($nw -lt 0) { $nw = $r.Width - $r.X }' +
|
|
433
|
+
'if ($nh -lt 0) { $nh = $r.Height - $r.Y }' +
|
|
434
|
+
'[Win32.NativeMethods]::SetWindowPos($h, [System.IntPtr]::Zero, $nx, $ny, $nw, $nh, 0x0004) | Out-Null;' +
|
|
435
|
+
'"ok"';
|
|
436
|
+
const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', ps], { timeout: PS_TIMEOUT_MS });
|
|
437
|
+
return stdout.trim() === 'ok';
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Internal helper — resolve a focusWindow-style query to a single
|
|
445
|
+
* WindowInfo. Same precedence the public `focusWindow` uses.
|
|
446
|
+
*/
|
|
447
|
+
async resolveWindow(query) {
|
|
448
|
+
const windows = await this.listWindows();
|
|
449
|
+
return windows.find(w => {
|
|
450
|
+
if (query.processId !== undefined && w.processId === query.processId)
|
|
451
|
+
return true;
|
|
452
|
+
if (query.processName && w.processName.toLowerCase() === query.processName.toLowerCase())
|
|
453
|
+
return true;
|
|
454
|
+
if (query.title && w.title.toLowerCase().includes(query.title.toLowerCase()))
|
|
455
|
+
return true;
|
|
456
|
+
return false;
|
|
457
|
+
}) ?? null;
|
|
458
|
+
}
|
|
459
|
+
// ─── ACCESSIBILITY ────────────────────────────────────────────────
|
|
460
|
+
async getUiTree(processId) {
|
|
461
|
+
// Default to the foreground window's pid when the caller omits it — exactly
|
|
462
|
+
// as findElements does below. Without this, get-screen-context is called
|
|
463
|
+
// with focusedPid=0 and the bridge returns NO tree (Cmd-GetScreenContext
|
|
464
|
+
// only walks a window when focusedPid>0), so read_screen over MCP came back
|
|
465
|
+
// "(empty a11y tree)" for EVERY app — a regression once the pid-resolving
|
|
466
|
+
// System-A read_screen was projected away in favor of this path.
|
|
467
|
+
let pid = processId;
|
|
468
|
+
if (pid === undefined) {
|
|
469
|
+
const fg = await this.getActiveWindow().catch(() => null);
|
|
470
|
+
if (fg?.processId)
|
|
471
|
+
pid = fg.processId;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const result = await ps_runner_1.psRunner.run({
|
|
475
|
+
cmd: 'get-screen-context',
|
|
476
|
+
maxDepth: 8,
|
|
477
|
+
...(pid !== undefined ? { focusedProcessId: pid } : {}),
|
|
478
|
+
});
|
|
479
|
+
const tree = result?.uiTree;
|
|
480
|
+
if (!tree)
|
|
481
|
+
return [];
|
|
482
|
+
const nodes = Array.isArray(tree) ? tree : [tree];
|
|
483
|
+
const flat = [];
|
|
484
|
+
for (const n of nodes)
|
|
485
|
+
this.flattenTree(n, flat);
|
|
486
|
+
return flat;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async findElements(query) {
|
|
493
|
+
// Default to the foreground window's pid when caller omits processId.
|
|
494
|
+
// Without this, the PSBridge searches from the desktop root across ALL
|
|
495
|
+
// windows and hits its 20-element cap before finding deep targets. The
|
|
496
|
+
// foreground window is almost always the right scope for an unscoped
|
|
497
|
+
// "find me X" query coming from the agent.
|
|
498
|
+
let processId = query.processId;
|
|
499
|
+
if (processId === undefined) {
|
|
500
|
+
const fg = await this.getActiveWindow();
|
|
501
|
+
if (fg?.processId)
|
|
502
|
+
processId = fg.processId;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const result = await ps_runner_1.psRunner.run({
|
|
506
|
+
cmd: 'find-element',
|
|
507
|
+
...(query.name !== undefined ? { name: query.name } : {}),
|
|
508
|
+
...(query.controlType !== undefined ? { controlType: query.controlType } : {}),
|
|
509
|
+
...(processId !== undefined ? { processId } : {}),
|
|
510
|
+
});
|
|
511
|
+
const raw = Array.isArray(result) ? result : [];
|
|
512
|
+
return raw.map(this.normalizeElement);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async getFocusedElement() {
|
|
519
|
+
try {
|
|
520
|
+
const result = await ps_runner_1.psRunner.run({ cmd: 'get-focused-element' });
|
|
521
|
+
if (!result || result.success === false)
|
|
522
|
+
return null;
|
|
523
|
+
return this.normalizeElement(result);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async invokeElement(query) {
|
|
530
|
+
// The underlying PS bridge requires a processId for invoke-element.
|
|
531
|
+
// Resolution order when caller omits processId:
|
|
532
|
+
// 1. Foreground window (the agent's usual implicit scope).
|
|
533
|
+
// 2. Fall back to find-element scan if the foreground window has no match.
|
|
534
|
+
// Without this, find-element ran from the desktop root and could miss
|
|
535
|
+
// deeply-nested targets due to the PSBridge 20-result cap.
|
|
536
|
+
let processId = query.processId;
|
|
537
|
+
if (processId === undefined && query.name) {
|
|
538
|
+
const fg = await this.getActiveWindow();
|
|
539
|
+
if (fg?.processId) {
|
|
540
|
+
processId = fg.processId;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const candidates = await this.findElements({
|
|
544
|
+
name: query.name,
|
|
545
|
+
controlType: query.controlType,
|
|
546
|
+
});
|
|
547
|
+
if (candidates.length === 0)
|
|
548
|
+
return { success: false };
|
|
549
|
+
processId = candidates[0].processId
|
|
550
|
+
?? candidates[0].pid;
|
|
551
|
+
// If still no pid but we have bounds, caller can fall back to a coord click.
|
|
552
|
+
if (processId === undefined) {
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
bounds: candidates[0].bounds,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (processId === undefined)
|
|
561
|
+
return { success: false };
|
|
562
|
+
try {
|
|
563
|
+
const result = await ps_runner_1.psRunner.run({
|
|
564
|
+
cmd: 'invoke-element',
|
|
565
|
+
processId,
|
|
566
|
+
action: query.action ?? 'click',
|
|
567
|
+
...(query.name !== undefined ? { name: query.name } : {}),
|
|
568
|
+
...(query.controlType !== undefined ? { controlType: query.controlType } : {}),
|
|
569
|
+
...(query.value !== undefined ? { value: query.value } : {}),
|
|
570
|
+
});
|
|
571
|
+
return {
|
|
572
|
+
success: result?.success === true,
|
|
573
|
+
bounds: result?.bounds,
|
|
574
|
+
// The bridge returns get-value's payload at the TOP level
|
|
575
|
+
// ({success, action, value, method}), not nested under .data — but
|
|
576
|
+
// every consumer reads res.data?.value. Surface it so a11y_get_value /
|
|
577
|
+
// element_value_contains actually see the value (review 2026-06-11).
|
|
578
|
+
data: result?.data ?? (result?.value !== undefined ? { value: result.value } : undefined),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return { success: false };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async waitForElement(query, timeoutMs) {
|
|
586
|
+
const interval = query.intervalMs ?? 250;
|
|
587
|
+
const deadline = Date.now() + timeoutMs;
|
|
588
|
+
while (Date.now() < deadline) {
|
|
589
|
+
const hits = await this.findElements({
|
|
590
|
+
name: query.name,
|
|
591
|
+
controlType: query.controlType,
|
|
592
|
+
processId: query.processId,
|
|
593
|
+
});
|
|
594
|
+
if (hits.length > 0)
|
|
595
|
+
return hits[0];
|
|
596
|
+
await this.delay(interval);
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
// ─── INPUT (mouse) ────────────────────────────────────────────────
|
|
601
|
+
// All coords are LOGICAL pixels — nut-js mouse API lives in that space on Win.
|
|
602
|
+
/** Cursor cache for mouseMoveRelative — last known target. */
|
|
603
|
+
lastCursor = null;
|
|
604
|
+
/**
|
|
605
|
+
* Ensure the window at (x, y) is the foreground window before clicking.
|
|
606
|
+
*
|
|
607
|
+
* Problem: On Windows, nut-js sends mouse input via SendInput which
|
|
608
|
+
* delivers to whatever window is topmost at those coordinates — not
|
|
609
|
+
* necessarily the foreground window. When a Save As dialog sits over a
|
|
610
|
+
* File Explorer window (or any background window), a click intended for
|
|
611
|
+
* the dialog's filename field can land on the Explorer window if the
|
|
612
|
+
* dialog's owning process lost foreground between the screenshot and the
|
|
613
|
+
* click (race) or if the click coords are slightly outside the dialog rect
|
|
614
|
+
* due to DPI-related rounding.
|
|
615
|
+
*
|
|
616
|
+
* Fix: use Win32 WindowFromPoint (via the warm psRunner bridge) to
|
|
617
|
+
* identify the window at the target coords. If it is not the current
|
|
618
|
+
* foreground window, call SetForegroundWindow to bring it forward before
|
|
619
|
+
* the click lands. Non-fatal — if the PS call fails we proceed anyway.
|
|
620
|
+
*/
|
|
621
|
+
async ensureForegroundAtPoint(x, y) {
|
|
622
|
+
try {
|
|
623
|
+
const r = await ps_runner_1.psRunner.run({ cmd: 'activate-at-point', x, y });
|
|
624
|
+
// 'noop' reasons mean nothing to promote (no window, or already foreground)
|
|
625
|
+
// — both are fine, treat as activated.
|
|
626
|
+
const activated = r?.activated !== false;
|
|
627
|
+
return { activated, title: r?.title, processName: r?.processName, reason: r?.reason };
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Non-fatal — click proceeds regardless. We do not want to block
|
|
631
|
+
// mouse input if the foreground check fails. Undefined = unknown.
|
|
632
|
+
return undefined;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
toNutButton(button) {
|
|
636
|
+
if (button === 'right')
|
|
637
|
+
return nut_js_1.Button.RIGHT;
|
|
638
|
+
if (button === 'middle')
|
|
639
|
+
return nut_js_1.Button.MIDDLE;
|
|
640
|
+
return nut_js_1.Button.LEFT;
|
|
641
|
+
}
|
|
642
|
+
async mouseClick(x, y, opts) {
|
|
643
|
+
// Bring the window at (x, y) to the foreground before sending any
|
|
644
|
+
// button events. Without this, a click intended for a Save As dialog
|
|
645
|
+
// can land on a background Explorer window when the dialog lost focus
|
|
646
|
+
// between the screenshot and the click (z-order / activation race).
|
|
647
|
+
// The activation verdict flows back to the caller so a FAILED raise
|
|
648
|
+
// (foreground-lock) is visible instead of a silent wrong-window click.
|
|
649
|
+
const activation = await this.ensureForegroundAtPoint(x, y);
|
|
650
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
|
|
651
|
+
this.lastCursor = { x, y };
|
|
652
|
+
await this.delay(40);
|
|
653
|
+
const count = opts?.count ?? 1;
|
|
654
|
+
const btn = this.toNutButton(opts?.button);
|
|
655
|
+
for (let i = 0; i < count; i++) {
|
|
656
|
+
if (btn === nut_js_1.Button.RIGHT)
|
|
657
|
+
await nut_js_1.mouse.rightClick();
|
|
658
|
+
else if (btn === nut_js_1.Button.MIDDLE) {
|
|
659
|
+
// nut-js has no direct middleClick helper; press+release.
|
|
660
|
+
await nut_js_1.mouse.pressButton(nut_js_1.Button.MIDDLE);
|
|
661
|
+
await this.delay(30);
|
|
662
|
+
await nut_js_1.mouse.releaseButton(nut_js_1.Button.MIDDLE);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
await nut_js_1.mouse.click(nut_js_1.Button.LEFT);
|
|
666
|
+
}
|
|
667
|
+
if (i < count - 1)
|
|
668
|
+
await this.delay(60);
|
|
669
|
+
}
|
|
670
|
+
return activation;
|
|
671
|
+
}
|
|
672
|
+
async mouseMove(x, y) {
|
|
673
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
|
|
674
|
+
this.lastCursor = { x, y };
|
|
675
|
+
}
|
|
676
|
+
async mouseMoveRelative(dx, dy) {
|
|
677
|
+
// nut-js `getPosition()` works reliably on Windows — prefer that over
|
|
678
|
+
// the cache. Fall back to the cache if the query fails.
|
|
679
|
+
try {
|
|
680
|
+
const pos = await nut_js_1.mouse.getPosition();
|
|
681
|
+
const nx = Math.round(pos.x + dx);
|
|
682
|
+
const ny = Math.round(pos.y + dy);
|
|
683
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
|
|
684
|
+
this.lastCursor = { x: nx, y: ny };
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
if (this.lastCursor) {
|
|
688
|
+
const nx = this.lastCursor.x + dx;
|
|
689
|
+
const ny = this.lastCursor.y + dy;
|
|
690
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
|
|
691
|
+
this.lastCursor = { x: nx, y: ny };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async mouseDrag(x1, y1, x2, y2) {
|
|
696
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x1, y1));
|
|
697
|
+
this.lastCursor = { x: x1, y: y1 };
|
|
698
|
+
await this.delay(50);
|
|
699
|
+
await nut_js_1.mouse.pressButton(nut_js_1.Button.LEFT);
|
|
700
|
+
await this.delay(80);
|
|
701
|
+
const steps = Math.max(8, Math.floor(Math.hypot(x2 - x1, y2 - y1) / 18));
|
|
702
|
+
for (let i = 1; i <= steps; i++) {
|
|
703
|
+
const t = i / steps;
|
|
704
|
+
const nx = Math.round(x1 + (x2 - x1) * t);
|
|
705
|
+
const ny = Math.round(y1 + (y2 - y1) * t);
|
|
706
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
|
|
707
|
+
this.lastCursor = { x: nx, y: ny };
|
|
708
|
+
await this.delay(10);
|
|
709
|
+
}
|
|
710
|
+
await nut_js_1.mouse.releaseButton(nut_js_1.Button.LEFT);
|
|
711
|
+
}
|
|
712
|
+
async mouseScroll(x, y, direction, amount = 3) {
|
|
713
|
+
await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
|
|
714
|
+
this.lastCursor = { x, y };
|
|
715
|
+
await this.delay(30);
|
|
716
|
+
// nut-js only exposes scrollUp/scrollDown natively. For horizontal,
|
|
717
|
+
// fall back to Shift+scroll which most apps interpret as horizontal.
|
|
718
|
+
if (direction === 'down')
|
|
719
|
+
await nut_js_1.mouse.scrollDown(amount);
|
|
720
|
+
else if (direction === 'up')
|
|
721
|
+
await nut_js_1.mouse.scrollUp(amount);
|
|
722
|
+
else {
|
|
723
|
+
// Horizontal: hold Shift, scroll vertically.
|
|
724
|
+
await nut_js_1.keyboard.pressKey(nut_js_1.Key.LeftShift);
|
|
725
|
+
try {
|
|
726
|
+
if (direction === 'left')
|
|
727
|
+
await nut_js_1.mouse.scrollUp(amount);
|
|
728
|
+
else
|
|
729
|
+
await nut_js_1.mouse.scrollDown(amount);
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
await nut_js_1.keyboard.releaseKey(nut_js_1.Key.LeftShift);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async mouseDown(button) {
|
|
737
|
+
await nut_js_1.mouse.pressButton(this.toNutButton(button));
|
|
738
|
+
}
|
|
739
|
+
async mouseUp(button) {
|
|
740
|
+
await nut_js_1.mouse.releaseButton(this.toNutButton(button));
|
|
741
|
+
}
|
|
742
|
+
// ─── INPUT (keyboard) ─────────────────────────────────────────────
|
|
743
|
+
async typeText(text) {
|
|
744
|
+
if (!text)
|
|
745
|
+
return;
|
|
746
|
+
await nut_js_1.keyboard.type(text);
|
|
747
|
+
}
|
|
748
|
+
async keyPress(combo) {
|
|
749
|
+
if (!combo)
|
|
750
|
+
return;
|
|
751
|
+
// Literal "+" — can't split on "+" since it IS the separator.
|
|
752
|
+
if (combo === '+') {
|
|
753
|
+
await nut_js_1.keyboard.type('+');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const parts = combo.split('+').map(s => s.trim()).filter(Boolean);
|
|
757
|
+
if (parts.length === 0)
|
|
758
|
+
return;
|
|
759
|
+
// Convert "mod" → "ctrl" on Windows, leave the rest of the combo alone.
|
|
760
|
+
const normalized = parts.map(p => {
|
|
761
|
+
const l = p.toLowerCase();
|
|
762
|
+
if (l === 'mod' || l === 'cmd' || l === 'command' || l === 'meta')
|
|
763
|
+
return 'ctrl';
|
|
764
|
+
return p;
|
|
765
|
+
});
|
|
766
|
+
// Map every part to a nut-js Key enum value, or 'TYPE_CHAR' for printable chars
|
|
767
|
+
// like '*', '+', '.' that have no direct enum entry.
|
|
768
|
+
const mapped = normalized.map(p => this.mapKey(p));
|
|
769
|
+
// Single-key: either type it as a character or press+release the mapped key.
|
|
770
|
+
if (mapped.length === 1) {
|
|
771
|
+
if (mapped[0] === 'TYPE_CHAR') {
|
|
772
|
+
await nut_js_1.keyboard.type(normalized[0]);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
await nut_js_1.keyboard.pressKey(mapped[0]);
|
|
776
|
+
await this.delay(30);
|
|
777
|
+
await nut_js_1.keyboard.releaseKey(mapped[0]);
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
// Combo: press each modifier (or type the printable char), then release in reverse.
|
|
782
|
+
for (let i = 0; i < mapped.length; i++) {
|
|
783
|
+
const k = mapped[i];
|
|
784
|
+
if (k === 'TYPE_CHAR') {
|
|
785
|
+
await nut_js_1.keyboard.type(normalized[i]);
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
await nut_js_1.keyboard.pressKey(k);
|
|
789
|
+
}
|
|
790
|
+
await this.delay(30);
|
|
791
|
+
}
|
|
792
|
+
for (let i = mapped.length - 1; i >= 0; i--) {
|
|
793
|
+
const k = mapped[i];
|
|
794
|
+
if (k !== 'TYPE_CHAR') {
|
|
795
|
+
await nut_js_1.keyboard.releaseKey(k);
|
|
796
|
+
}
|
|
797
|
+
await this.delay(30);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async keyDown(key) {
|
|
801
|
+
const mapped = this.mapKey(key);
|
|
802
|
+
if (mapped === 'TYPE_CHAR') {
|
|
803
|
+
// Single printable char without modifier semantics — treat as type.
|
|
804
|
+
await nut_js_1.keyboard.type(key);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
await nut_js_1.keyboard.pressKey(mapped);
|
|
808
|
+
}
|
|
809
|
+
async keyUp(key) {
|
|
810
|
+
const mapped = this.mapKey(key);
|
|
811
|
+
if (mapped === 'TYPE_CHAR')
|
|
812
|
+
return; // no-op — typing isn't held
|
|
813
|
+
await nut_js_1.keyboard.releaseKey(mapped);
|
|
814
|
+
}
|
|
815
|
+
// ─── CLIPBOARD ────────────────────────────────────────────────────
|
|
816
|
+
async readClipboard() {
|
|
817
|
+
try {
|
|
818
|
+
const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', 'Get-Clipboard'], { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
819
|
+
// Get-Clipboard tacks on a trailing CRLF — trim for consistency with macOS.
|
|
820
|
+
return stdout?.replace(/\r?\n$/, '') ?? '';
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
return '';
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async writeClipboard(text) {
|
|
827
|
+
// Pack the command as UTF-16LE base64 so arbitrary characters (quotes,
|
|
828
|
+
// newlines, non-ASCII) survive without any escaping dance.
|
|
829
|
+
const utf16 = Buffer.from(`Set-Clipboard -Value '${text.replace(/'/g, "''")}'`, 'utf16le');
|
|
830
|
+
try {
|
|
831
|
+
await execFileAsync('powershell.exe', ['-NoProfile', '-EncodedCommand', utf16.toString('base64')], { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
// Silent — clipboard is best-effort (same contract as macOS adapter).
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// ─── APPS ─────────────────────────────────────────────────────────
|
|
838
|
+
/**
|
|
839
|
+
* Thin shim — delegates straight to `launchApp` with no alias resolution.
|
|
840
|
+
* The platform layer is alias-data-agnostic; alias resolution lives in
|
|
841
|
+
* the caller (the agent's `open_app` tool, the router's `handleOpenApp`).
|
|
842
|
+
* Callers that want UWP / executable / searchTerm hints must pass them
|
|
843
|
+
* via `launchApp` directly.
|
|
844
|
+
*/
|
|
845
|
+
async openApp(name, opts) {
|
|
846
|
+
return this.launchApp(name, opts);
|
|
847
|
+
}
|
|
848
|
+
async launchApp(name, opts) {
|
|
849
|
+
// Reject control chars / backticks / $() that can escape PowerShell quoting
|
|
850
|
+
// regardless of how we serialize.
|
|
851
|
+
if (/[\r\n\t\x00-\x1f]/.test(name) || /[`$]/.test(name)) {
|
|
852
|
+
throw new Error('launchApp: illegal characters in app name');
|
|
853
|
+
}
|
|
854
|
+
// Snapshot existing windows ONCE before any spawn so the diff-and-poll
|
|
855
|
+
// helper can ignore them. Reused by the idempotency check below — saves
|
|
856
|
+
// a redundant `listWindows()` round-trip through the PS bridge.
|
|
857
|
+
let windowsBefore = [];
|
|
858
|
+
try {
|
|
859
|
+
windowsBefore = await this.listWindows();
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Non-fatal — empty before-set means everything looks "new".
|
|
863
|
+
}
|
|
864
|
+
// v0.8.3 — idempotency: if the app is already running AND caller didn't
|
|
865
|
+
// ask for a fresh instance, FOCUS the existing window instead of spawning
|
|
866
|
+
// another. This closes the "Outlook keeps opening" bug: a retry loop that
|
|
867
|
+
// launches Outlook every iteration used to spawn a new instance each time
|
|
868
|
+
// (Start-Process -FilePath outlook with Outlook already running launches
|
|
869
|
+
// a fresh window).
|
|
870
|
+
if (!opts?.alwaysNewInstance && !opts?.url) {
|
|
871
|
+
const existing = this.findExistingAppWindowIn(windowsBefore, name, opts?.uwpAppId);
|
|
872
|
+
if (existing) {
|
|
873
|
+
// Focus it so it surfaces like a launch would, then return its identity.
|
|
874
|
+
await this.focusWindow({ processId: existing.processId }).catch(() => { });
|
|
875
|
+
return { pid: existing.processId, title: existing.title, handle: existing.handle };
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Route 1: UWP apps via explorer shell:AppsFolder\<id>. This is the Windows-
|
|
879
|
+
// sanctioned way to launch UWP / Store apps and is rock-solid — Calculator,
|
|
880
|
+
// Notepad-Win11, Photos, etc. all work.
|
|
881
|
+
if (opts?.uwpAppId) {
|
|
882
|
+
const id = opts.uwpAppId;
|
|
883
|
+
// App ID format is `<PackageFamily>_<Hash>!<AppId>`. Valid characters are
|
|
884
|
+
// alphanumerics, dots, underscores, hyphens, and a single `!`. Reject anything
|
|
885
|
+
// else to keep the shell: path from interpreting metacharacters.
|
|
886
|
+
if (!/^[A-Za-z0-9_.\-]+![A-Za-z0-9_.\-]+$/.test(id)) {
|
|
887
|
+
throw new Error(`launchApp: illegal uwpAppId "${id}"`);
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const child = (0, child_process_1.spawn)('explorer.exe', [`shell:AppsFolder\\${id}`], {
|
|
891
|
+
stdio: 'ignore', detached: true, windowsHide: true,
|
|
892
|
+
});
|
|
893
|
+
child.unref();
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
// Non-fatal — continue and look for the window anyway.
|
|
897
|
+
}
|
|
898
|
+
// Shorter primary budget so we have headroom for the Start-Menu
|
|
899
|
+
// fallback if shell:AppsFolder didn't surface a window — matches
|
|
900
|
+
// the router's strategy ladder.
|
|
901
|
+
const uwpResult = await this.findLaunchedWindow(name, windowsBefore, 4_000);
|
|
902
|
+
if (uwpResult.title)
|
|
903
|
+
return this.foregroundLaunched(uwpResult);
|
|
904
|
+
if (opts?.noStartMenuFallback)
|
|
905
|
+
return uwpResult; // {} — caller verifies
|
|
906
|
+
return this.foregroundLaunched(await this.launchViaStartMenuSearch(name, opts?.searchTerm, windowsBefore));
|
|
907
|
+
}
|
|
908
|
+
// Route 2: classic Start-Process via PowerShell with safely quoted args.
|
|
909
|
+
const args = ['-NoProfile', '-Command'];
|
|
910
|
+
const cmdParts = ['Start-Process'];
|
|
911
|
+
cmdParts.push('-FilePath', this.psQuote(name));
|
|
912
|
+
if (opts?.url && !/[\r\n\t\x00-\x1f"'`$]/.test(opts.url)) {
|
|
913
|
+
cmdParts.push('-ArgumentList', this.psQuote(opts.url));
|
|
914
|
+
}
|
|
915
|
+
if (opts?.cwd && !/[\r\n\t\x00-\x1f"'`$]/.test(opts.cwd)) {
|
|
916
|
+
cmdParts.push('-WorkingDirectory', this.psQuote(opts.cwd));
|
|
917
|
+
}
|
|
918
|
+
args.push(cmdParts.join(' '));
|
|
919
|
+
try {
|
|
920
|
+
const child = (0, child_process_1.spawn)('powershell.exe', args, {
|
|
921
|
+
stdio: 'ignore', detached: true, windowsHide: true,
|
|
922
|
+
});
|
|
923
|
+
child.unref();
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// Fall through to the lookup — the app may already be running.
|
|
927
|
+
}
|
|
928
|
+
// Try the primary Start-Process result with a shorter budget so we have
|
|
929
|
+
// time for the Start-Menu fallback if it returns empty. Edge / VS Code /
|
|
930
|
+
// any binary not on PATH but Start-Menu-indexed will recover here.
|
|
931
|
+
const direct = await this.findLaunchedWindow(name, windowsBefore, 4_000);
|
|
932
|
+
if (direct.title)
|
|
933
|
+
return this.foregroundLaunched(direct);
|
|
934
|
+
if (opts?.noStartMenuFallback)
|
|
935
|
+
return direct; // {} — caller verifies (open_file)
|
|
936
|
+
// Route 3: Start Menu search fallback — universal for any app indexed by
|
|
937
|
+
// Windows. Press the Win key, type the app name, press Enter. This is
|
|
938
|
+
// the same pattern the router's zero-LLM fast path uses; ported here so
|
|
939
|
+
// every caller of launchApp (agent's open_app, MCP, REST) gets the
|
|
940
|
+
// reliability without duplicating router logic.
|
|
941
|
+
return this.foregroundLaunched(await this.launchViaStartMenuSearch(name, opts?.searchTerm, windowsBefore));
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Bring a freshly-launched window to the foreground. A detached spawn opens
|
|
945
|
+
* the app BEHIND the current foreground (Windows foreground-lock), so without
|
|
946
|
+
* this `open_app("calc")` left Calculator in the background and every
|
|
947
|
+
* subsequent focused-window op (read_screen, find_element) targeted the wrong
|
|
948
|
+
* window. The idempotency path already focuses; this gives fresh launches the
|
|
949
|
+
* same contract. Best-effort — never throws, the launch already succeeded.
|
|
950
|
+
*/
|
|
951
|
+
async foregroundLaunched(result) {
|
|
952
|
+
if (result?.pid) {
|
|
953
|
+
await this.focusWindow({ processId: result.pid, title: result.title }).catch(() => { });
|
|
954
|
+
}
|
|
955
|
+
return result;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Last-resort launch via Windows' own Start Menu search. Works for any
|
|
959
|
+
* app the user can find by name in the Start Menu (apps, settings panes,
|
|
960
|
+
* UWP without a known AppsFolder ID, third-party Win32 binaries with an
|
|
961
|
+
* App Paths entry). The keyboard primitives we use here go through the
|
|
962
|
+
* adapter directly, NOT through the safety layer — this is internal
|
|
963
|
+
* platform logic, not an agent action.
|
|
964
|
+
*
|
|
965
|
+
* Tuned to the same cadence as the router's startMenuSearch helper.
|
|
966
|
+
*/
|
|
967
|
+
async launchViaStartMenuSearch(name, searchTermHint, windowsBefore) {
|
|
968
|
+
// Pick the term Windows Search will actually rank correctly. The alias's
|
|
969
|
+
// `searchTerm` (when provided) is the human-friendly name an end user
|
|
970
|
+
// would type — "Edge", "VS Code", "File Explorer". For names without an
|
|
971
|
+
// alias, fall back to stripping the file-system suffix off `name`:
|
|
972
|
+
// `msedge.exe` → `msedge`, `notepad.exe` → `notepad`, etc. Without this
|
|
973
|
+
// distinction, typing the binary name in Start Menu can surface the
|
|
974
|
+
// wrong app (e.g. "msedge" → Microsoft Store as the closest match).
|
|
975
|
+
const searchText = (searchTermHint && searchTermHint.trim())
|
|
976
|
+
? searchTermHint.trim()
|
|
977
|
+
: name.replace(/\.(exe|com)$/i, '');
|
|
978
|
+
try {
|
|
979
|
+
// Close any in-progress Start Menu / search overlay so the Win key
|
|
980
|
+
// reliably opens a fresh one.
|
|
981
|
+
await this.keyPress('Escape').catch(() => { });
|
|
982
|
+
await this.delay(120);
|
|
983
|
+
await this.keyPress('Super');
|
|
984
|
+
await this.delay(600);
|
|
985
|
+
await this.typeText(searchText);
|
|
986
|
+
await this.delay(700);
|
|
987
|
+
await this.keyPress('Return');
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
// Keyboard layer flaky — caller will see empty result and decide.
|
|
991
|
+
}
|
|
992
|
+
// The post-launch predicate still uses the launched binary `name`
|
|
993
|
+
// because that's what the new window's processName will look like
|
|
994
|
+
// (msedge.exe → process "msedge"); the searchText only drives what
|
|
995
|
+
// Windows Search resolves to.
|
|
996
|
+
const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), { timeoutMs: 4_000 });
|
|
997
|
+
return win
|
|
998
|
+
? { pid: win.processId, title: win.title, handle: win.handle }
|
|
999
|
+
: {};
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* After a launch, wait for the new window to surface. Uses the shared
|
|
1003
|
+
* `waitForLaunchedWindow` diff-and-poll helper so the budget is spent
|
|
1004
|
+
* doing useful work (polling every 300ms) rather than a single fixed
|
|
1005
|
+
* settle. Returns `{}` when the deadline elapses with no match — caller
|
|
1006
|
+
* can interpret that as a real "this strategy didn't work" signal and
|
|
1007
|
+
* try the next strategy.
|
|
1008
|
+
*
|
|
1009
|
+
* On Windows, neither the UWP shell:AppsFolder spawn nor the classic
|
|
1010
|
+
* Start-Process spawn returns the eventual app's PID (we spawn explorer /
|
|
1011
|
+
* powershell, not the target binary), so we don't pass `spawnPid`.
|
|
1012
|
+
* The predicate matches by process name + title, same as the old
|
|
1013
|
+
* single-shot logic — just polled.
|
|
1014
|
+
*/
|
|
1015
|
+
async findLaunchedWindow(name, windowsBefore, timeoutMs) {
|
|
1016
|
+
const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), timeoutMs ? { timeoutMs } : undefined);
|
|
1017
|
+
return win
|
|
1018
|
+
? { pid: win.processId, title: win.title, handle: win.handle }
|
|
1019
|
+
: {};
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* v0.8.3 — check whether an app matching `name` or `uwpAppId` already has
|
|
1023
|
+
* a visible top-level window. Used by `launchApp` to short-circuit when
|
|
1024
|
+
* the user / agent asks to "open Outlook" but Outlook is already running.
|
|
1025
|
+
*
|
|
1026
|
+
* Match policy: case-insensitive process-name / title substring, which
|
|
1027
|
+
* matches the same alias set the router uses. A `uwpAppId` like
|
|
1028
|
+
* `Microsoft.WindowsCalculator_8wekyb3d8bbwe!App` is reduced to its App
|
|
1029
|
+
* token (`App`, `Calculator`) and matched against window titles as a
|
|
1030
|
+
* fallback.
|
|
1031
|
+
*
|
|
1032
|
+
* Returns `null` when no matching window is found — caller proceeds with
|
|
1033
|
+
* a normal launch.
|
|
1034
|
+
*/
|
|
1035
|
+
async findExistingAppWindow(name, uwpAppId) {
|
|
1036
|
+
try {
|
|
1037
|
+
const windows = await this.listWindows();
|
|
1038
|
+
return this.findExistingAppWindowIn(windows, name, uwpAppId);
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Same matching logic as `findExistingAppWindow` but takes an already-fetched
|
|
1046
|
+
* window list. Lets `launchApp` reuse the snapshot it captures for the
|
|
1047
|
+
* post-spawn diff-and-poll, avoiding a redundant PS-bridge round-trip.
|
|
1048
|
+
*/
|
|
1049
|
+
findExistingAppWindowIn(windows, name, uwpAppId) {
|
|
1050
|
+
if (windows.length === 0)
|
|
1051
|
+
return null;
|
|
1052
|
+
const target = name.trim().toLowerCase();
|
|
1053
|
+
// Strip any trailing `.exe` so `outlook.exe` still matches `outlook`.
|
|
1054
|
+
const targetStem = target.replace(/\.(exe|com|app)$/, '');
|
|
1055
|
+
// Tier 1: exact processName match.
|
|
1056
|
+
let hit = windows.find(w => w.processName.toLowerCase() === targetStem);
|
|
1057
|
+
// Tier 2: processName substring (handles olk ↔ outlook etc.).
|
|
1058
|
+
if (!hit)
|
|
1059
|
+
hit = windows.find(w => w.processName.toLowerCase().includes(targetStem));
|
|
1060
|
+
// Tier 3: reverse — targetStem contains processName (e.g. name="msedge.exe", proc="msedge").
|
|
1061
|
+
if (!hit)
|
|
1062
|
+
hit = windows.find(w => targetStem.includes(w.processName.toLowerCase()) && w.processName.length >= 3);
|
|
1063
|
+
// Tier 4: title substring.
|
|
1064
|
+
if (!hit)
|
|
1065
|
+
hit = windows.find(w => w.title.toLowerCase().includes(targetStem));
|
|
1066
|
+
// UWP fallback — check the AppsFolder id's last segment against titles.
|
|
1067
|
+
if (!hit && uwpAppId) {
|
|
1068
|
+
const uwpTail = uwpAppId.split('!').pop()?.toLowerCase() ?? '';
|
|
1069
|
+
if (uwpTail)
|
|
1070
|
+
hit = windows.find(w => w.title.toLowerCase().includes(uwpTail));
|
|
1071
|
+
}
|
|
1072
|
+
// Skip minimized windows — if the user hid it, they probably want a
|
|
1073
|
+
// "fresh" focus, but we still return it so focusWindow can restore.
|
|
1074
|
+
return hit ?? null;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* PowerShell single-quoted string escape. Inside single quotes, the only
|
|
1078
|
+
* special char is the single quote itself, which doubles to escape.
|
|
1079
|
+
* This is the only safe way to pass a user-controlled string as a
|
|
1080
|
+
* PowerShell argument.
|
|
1081
|
+
*/
|
|
1082
|
+
psQuote(s) {
|
|
1083
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
1084
|
+
}
|
|
1085
|
+
// ─── INTERNAL HELPERS ─────────────────────────────────────────────
|
|
1086
|
+
normalizeWindow = (raw) => ({
|
|
1087
|
+
title: raw?.title ?? '',
|
|
1088
|
+
processName: raw?.processName ?? '',
|
|
1089
|
+
processId: raw?.processId ?? 0,
|
|
1090
|
+
bounds: raw?.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
|
|
1091
|
+
isMinimized: raw?.isMinimized ?? false,
|
|
1092
|
+
handle: raw?.handle ?? raw?.processId,
|
|
1093
|
+
});
|
|
1094
|
+
normalizeElement = (raw) => {
|
|
1095
|
+
const enabled = raw?.isEnabled ?? raw?.enabled;
|
|
1096
|
+
return {
|
|
1097
|
+
name: raw?.name ?? '',
|
|
1098
|
+
controlType: (raw?.controlType ?? '').replace(/^ControlType\./, ''),
|
|
1099
|
+
bounds: raw?.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
|
|
1100
|
+
value: raw?.value,
|
|
1101
|
+
enabled,
|
|
1102
|
+
focused: raw?.focused,
|
|
1103
|
+
// Tranche 1A: richer state fields from ps-bridge.
|
|
1104
|
+
selected: raw?.selected ?? raw?.isSelected,
|
|
1105
|
+
disabled: enabled === false ? true : undefined,
|
|
1106
|
+
busy: raw?.busy ?? raw?.isBusy,
|
|
1107
|
+
offscreen: raw?.offscreen ?? raw?.isOffscreen,
|
|
1108
|
+
expandable: raw?.expandable,
|
|
1109
|
+
expanded: raw?.expanded,
|
|
1110
|
+
automationId: raw?.automationId,
|
|
1111
|
+
processId: raw?.processId ?? raw?.pid,
|
|
1112
|
+
};
|
|
1113
|
+
};
|
|
1114
|
+
/**
|
|
1115
|
+
* Flatten the UIA tree into a single list, matching the macOS adapter's
|
|
1116
|
+
* contract. Drops purely structural unnamed nodes to keep the list useful.
|
|
1117
|
+
*/
|
|
1118
|
+
flattenTree(node, acc) {
|
|
1119
|
+
if (!node)
|
|
1120
|
+
return;
|
|
1121
|
+
// ConvertTo-UINode may return an array of children when it skipped an
|
|
1122
|
+
// unnamed container — just recurse through those.
|
|
1123
|
+
if (Array.isArray(node)) {
|
|
1124
|
+
for (const n of node)
|
|
1125
|
+
this.flattenTree(n, acc);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (node.controlType || node.name)
|
|
1129
|
+
acc.push(this.normalizeElement(node));
|
|
1130
|
+
if (Array.isArray(node.children)) {
|
|
1131
|
+
for (const child of node.children)
|
|
1132
|
+
this.flattenTree(child, acc);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Map a portable key token to the nut-js Key enum (or 'TYPE_CHAR' for
|
|
1137
|
+
* printable ASCII symbols that don't have a direct enum entry).
|
|
1138
|
+
*/
|
|
1139
|
+
mapKey(name) {
|
|
1140
|
+
const direct = WIN_KEY_MAP[name] ?? WIN_KEY_MAP[name.toLowerCase()];
|
|
1141
|
+
if (direct !== undefined)
|
|
1142
|
+
return direct;
|
|
1143
|
+
if (name.length === 1) {
|
|
1144
|
+
const ch = name;
|
|
1145
|
+
const upper = ch.toUpperCase();
|
|
1146
|
+
// A-Z
|
|
1147
|
+
if (upper >= 'A' && upper <= 'Z') {
|
|
1148
|
+
const k = nut_js_1.Key[upper];
|
|
1149
|
+
if (k !== undefined)
|
|
1150
|
+
return k;
|
|
1151
|
+
}
|
|
1152
|
+
// 0-9 → nut-js uses Num1..Num9, Num0 for the top-row digits.
|
|
1153
|
+
if (upper >= '0' && upper <= '9') {
|
|
1154
|
+
const k = nut_js_1.Key[`Num${upper}`];
|
|
1155
|
+
if (k !== undefined)
|
|
1156
|
+
return k;
|
|
1157
|
+
}
|
|
1158
|
+
// Any other printable ASCII — ask keyboard.type() to handle it.
|
|
1159
|
+
if (ch.charCodeAt(0) >= 32 && ch.charCodeAt(0) <= 126)
|
|
1160
|
+
return 'TYPE_CHAR';
|
|
1161
|
+
}
|
|
1162
|
+
// Last resort: direct enum name match (e.g. "F13", "NumPad5").
|
|
1163
|
+
const enumVal = nut_js_1.Key[name];
|
|
1164
|
+
if (enumVal !== undefined)
|
|
1165
|
+
return enumVal;
|
|
1166
|
+
throw new Error(`Unknown key: "${name}"`);
|
|
1167
|
+
}
|
|
1168
|
+
delay(ms) {
|
|
1169
|
+
return new Promise(r => setTimeout(r, ms));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
exports.WindowsAdapter = WindowsAdapter;
|
|
1173
|
+
// Portable-token → nut-js Key lookup. Lowercase keys are checked as a
|
|
1174
|
+
// fallback so "Return"/"return", "Shift"/"shift", etc. all resolve.
|
|
1175
|
+
const WIN_KEY_MAP = {
|
|
1176
|
+
// Modifiers
|
|
1177
|
+
ctrl: nut_js_1.Key.LeftControl, control: nut_js_1.Key.LeftControl, Control: nut_js_1.Key.LeftControl,
|
|
1178
|
+
shift: nut_js_1.Key.LeftShift, Shift: nut_js_1.Key.LeftShift,
|
|
1179
|
+
alt: nut_js_1.Key.LeftAlt, Alt: nut_js_1.Key.LeftAlt, option: nut_js_1.Key.LeftAlt, opt: nut_js_1.Key.LeftAlt,
|
|
1180
|
+
super: nut_js_1.Key.LeftSuper, Super: nut_js_1.Key.LeftSuper, win: nut_js_1.Key.LeftSuper, windows: nut_js_1.Key.LeftSuper, meta: nut_js_1.Key.LeftSuper,
|
|
1181
|
+
// Navigation / editing
|
|
1182
|
+
return: nut_js_1.Key.Enter, Return: nut_js_1.Key.Enter, enter: nut_js_1.Key.Enter, Enter: nut_js_1.Key.Enter,
|
|
1183
|
+
tab: nut_js_1.Key.Tab, Tab: nut_js_1.Key.Tab,
|
|
1184
|
+
escape: nut_js_1.Key.Escape, Escape: nut_js_1.Key.Escape, esc: nut_js_1.Key.Escape, Esc: nut_js_1.Key.Escape,
|
|
1185
|
+
backspace: nut_js_1.Key.Backspace, Backspace: nut_js_1.Key.Backspace,
|
|
1186
|
+
delete: nut_js_1.Key.Delete, Delete: nut_js_1.Key.Delete, forwarddelete: nut_js_1.Key.Delete,
|
|
1187
|
+
space: nut_js_1.Key.Space, Space: nut_js_1.Key.Space,
|
|
1188
|
+
home: nut_js_1.Key.Home, Home: nut_js_1.Key.Home,
|
|
1189
|
+
end: nut_js_1.Key.End, End: nut_js_1.Key.End,
|
|
1190
|
+
pageup: nut_js_1.Key.PageUp, PageUp: nut_js_1.Key.PageUp,
|
|
1191
|
+
pagedown: nut_js_1.Key.PageDown, PageDown: nut_js_1.Key.PageDown,
|
|
1192
|
+
insert: nut_js_1.Key.Insert, Insert: nut_js_1.Key.Insert,
|
|
1193
|
+
// Arrows
|
|
1194
|
+
left: nut_js_1.Key.Left, Left: nut_js_1.Key.Left,
|
|
1195
|
+
right: nut_js_1.Key.Right, Right: nut_js_1.Key.Right,
|
|
1196
|
+
up: nut_js_1.Key.Up, Up: nut_js_1.Key.Up,
|
|
1197
|
+
down: nut_js_1.Key.Down, Down: nut_js_1.Key.Down,
|
|
1198
|
+
// F-keys
|
|
1199
|
+
f1: nut_js_1.Key.F1, F1: nut_js_1.Key.F1, f2: nut_js_1.Key.F2, F2: nut_js_1.Key.F2, f3: nut_js_1.Key.F3, F3: nut_js_1.Key.F3,
|
|
1200
|
+
f4: nut_js_1.Key.F4, F4: nut_js_1.Key.F4, f5: nut_js_1.Key.F5, F5: nut_js_1.Key.F5, f6: nut_js_1.Key.F6, F6: nut_js_1.Key.F6,
|
|
1201
|
+
f7: nut_js_1.Key.F7, F7: nut_js_1.Key.F7, f8: nut_js_1.Key.F8, F8: nut_js_1.Key.F8, f9: nut_js_1.Key.F9, F9: nut_js_1.Key.F9,
|
|
1202
|
+
f10: nut_js_1.Key.F10, F10: nut_js_1.Key.F10, f11: nut_js_1.Key.F11, F11: nut_js_1.Key.F11, f12: nut_js_1.Key.F12, F12: nut_js_1.Key.F12,
|
|
1203
|
+
// Symbol keys reachable as single chars in combos like "ctrl++" / "ctrl+-"
|
|
1204
|
+
'=': nut_js_1.Key.Equal,
|
|
1205
|
+
'+': nut_js_1.Key.Equal,
|
|
1206
|
+
'-': nut_js_1.Key.Minus,
|
|
1207
|
+
'_': nut_js_1.Key.Minus,
|
|
1208
|
+
'`': nut_js_1.Key.Grave,
|
|
1209
|
+
};
|
|
1210
|
+
//# sourceMappingURL=windows.js.map
|