@epsilon-asi/actors 0.0.22 → 0.0.33
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/dist/browser/RuntimeConfig.d.ts +26 -0
- package/dist/browser/RuntimeConfig.d.ts.map +1 -1
- package/dist/browser/RuntimeConfig.js +29 -1
- package/dist/browser/RuntimeConfig.js.map +1 -1
- package/dist/core/ActorContext.d.ts +2 -0
- package/dist/core/ActorContext.d.ts.map +1 -1
- package/dist/core/ActorRunner.d.ts +3 -0
- package/dist/core/ActorRunner.d.ts.map +1 -1
- package/dist/core/ActorRunner.js +11 -1
- package/dist/core/ActorRunner.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/native/CompositeNativeWindowDriver.d.ts +11 -0
- package/dist/native/CompositeNativeWindowDriver.d.ts.map +1 -0
- package/dist/native/CompositeNativeWindowDriver.js +31 -0
- package/dist/native/CompositeNativeWindowDriver.js.map +1 -0
- package/dist/native/NativeActionRegistry.d.ts +14 -0
- package/dist/native/NativeActionRegistry.d.ts.map +1 -0
- package/dist/native/NativeActionRegistry.js +101 -0
- package/dist/native/NativeActionRegistry.js.map +1 -0
- package/dist/native/NativeAutomation.d.ts +3 -0
- package/dist/native/NativeAutomation.d.ts.map +1 -0
- package/dist/native/NativeAutomation.js +12 -0
- package/dist/native/NativeAutomation.js.map +1 -0
- package/dist/native/NativeCoordinateMapper.d.ts +23 -0
- package/dist/native/NativeCoordinateMapper.d.ts.map +1 -0
- package/dist/native/NativeCoordinateMapper.js +201 -0
- package/dist/native/NativeCoordinateMapper.js.map +1 -0
- package/dist/native/NativeFileDialogService.d.ts +26 -0
- package/dist/native/NativeFileDialogService.d.ts.map +1 -0
- package/dist/native/NativeFileDialogService.js +121 -0
- package/dist/native/NativeFileDialogService.js.map +1 -0
- package/dist/native/NativeImageFinder.d.ts +12 -0
- package/dist/native/NativeImageFinder.d.ts.map +1 -0
- package/dist/native/NativeImageFinder.js +29 -0
- package/dist/native/NativeImageFinder.js.map +1 -0
- package/dist/native/NativeKeyboard.d.ts +10 -0
- package/dist/native/NativeKeyboard.d.ts.map +1 -0
- package/dist/native/NativeKeyboard.js +16 -0
- package/dist/native/NativeKeyboard.js.map +1 -0
- package/dist/native/NativeMouse.d.ts +38 -0
- package/dist/native/NativeMouse.d.ts.map +1 -0
- package/dist/native/NativeMouse.js +82 -0
- package/dist/native/NativeMouse.js.map +1 -0
- package/dist/native/NativeWindowService.d.ts +31 -0
- package/dist/native/NativeWindowService.d.ts.map +1 -0
- package/dist/native/NativeWindowService.js +183 -0
- package/dist/native/NativeWindowService.js.map +1 -0
- package/dist/native/UnsupportedNativeAutomation.d.ts +4 -0
- package/dist/native/UnsupportedNativeAutomation.d.ts.map +1 -0
- package/dist/native/UnsupportedNativeAutomation.js +77 -0
- package/dist/native/UnsupportedNativeAutomation.js.map +1 -0
- package/dist/native/WindowMatcher.d.ts +4 -0
- package/dist/native/WindowMatcher.d.ts.map +1 -0
- package/dist/native/WindowMatcher.js +39 -0
- package/dist/native/WindowMatcher.js.map +1 -0
- package/dist/native/drivers.d.ts +37 -0
- package/dist/native/drivers.d.ts.map +1 -0
- package/dist/native/drivers.js +2 -0
- package/dist/native/drivers.js.map +1 -0
- package/dist/native/errors.d.ts +23 -0
- package/dist/native/errors.d.ts.map +1 -0
- package/dist/native/errors.js +45 -0
- package/dist/native/errors.js.map +1 -0
- package/dist/native/index.d.ts +13 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +13 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts +11 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts.map +1 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.js +180 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.js.map +1 -0
- package/dist/native/macos/MacOSAppleScriptClient.d.ts +24 -0
- package/dist/native/macos/MacOSAppleScriptClient.d.ts.map +1 -0
- package/dist/native/macos/MacOSAppleScriptClient.js +163 -0
- package/dist/native/macos/MacOSAppleScriptClient.js.map +1 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts +10 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts.map +1 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js +12 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js.map +1 -0
- package/dist/native/macos/MacOSNativeAutomation.d.ts +3 -0
- package/dist/native/macos/MacOSNativeAutomation.d.ts.map +1 -0
- package/dist/native/macos/MacOSNativeAutomation.js +88 -0
- package/dist/native/macos/MacOSNativeAutomation.js.map +1 -0
- package/dist/native/nut/NutNativeImageFinder.d.ts +17 -0
- package/dist/native/nut/NutNativeImageFinder.d.ts.map +1 -0
- package/dist/native/nut/NutNativeImageFinder.js +84 -0
- package/dist/native/nut/NutNativeImageFinder.js.map +1 -0
- package/dist/native/nut/NutNativeKeyboardDriver.d.ts +8 -0
- package/dist/native/nut/NutNativeKeyboardDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeKeyboardDriver.js +39 -0
- package/dist/native/nut/NutNativeKeyboardDriver.js.map +1 -0
- package/dist/native/nut/NutNativeMouseDriver.d.ts +8 -0
- package/dist/native/nut/NutNativeMouseDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeMouseDriver.js +24 -0
- package/dist/native/nut/NutNativeMouseDriver.js.map +1 -0
- package/dist/native/nut/NutNativeScreenDriver.d.ts +6 -0
- package/dist/native/nut/NutNativeScreenDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeScreenDriver.js +12 -0
- package/dist/native/nut/NutNativeScreenDriver.js.map +1 -0
- package/dist/native/nut/NutNativeWindowDriver.d.ts +6 -0
- package/dist/native/nut/NutNativeWindowDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeWindowDriver.js +53 -0
- package/dist/native/nut/NutNativeWindowDriver.js.map +1 -0
- package/dist/native/nut/loadNut.d.ts +58 -0
- package/dist/native/nut/loadNut.d.ts.map +1 -0
- package/dist/native/nut/loadNut.js +25 -0
- package/dist/native/nut/loadNut.js.map +1 -0
- package/dist/native/types.d.ts +194 -0
- package/dist/native/types.d.ts.map +1 -0
- package/dist/native/types.js +2 -0
- package/dist/native/types.js.map +1 -0
- package/dist/native/utils/appleScriptEscape.d.ts +7 -0
- package/dist/native/utils/appleScriptEscape.d.ts.map +1 -0
- package/dist/native/utils/appleScriptEscape.js +11 -0
- package/dist/native/utils/appleScriptEscape.js.map +1 -0
- package/dist/native/utils/geometry.d.ts +12 -0
- package/dist/native/utils/geometry.d.ts.map +1 -0
- package/dist/native/utils/geometry.js +77 -0
- package/dist/native/utils/geometry.js.map +1 -0
- package/dist/native/utils/redactNative.d.ts +2 -0
- package/dist/native/utils/redactNative.d.ts.map +1 -0
- package/dist/native/utils/redactNative.js +7 -0
- package/dist/native/utils/redactNative.js.map +1 -0
- package/dist/native/utils/waitFor.d.ts +7 -0
- package/dist/native/utils/waitFor.d.ts.map +1 -0
- package/dist/native/utils/waitFor.js +17 -0
- package/dist/native/utils/waitFor.js.map +1 -0
- package/dist/sites/upwork-com/upwork-com.actor.d.ts +4 -1
- package/dist/sites/upwork-com/upwork-com.actor.d.ts.map +1 -1
- package/dist/sites/upwork-com/upwork-com.actor.js +31 -11
- package/dist/sites/upwork-com/upwork-com.actor.js.map +1 -1
- package/dist/sites/upwork-com/upwork-com.types.d.ts +3 -1
- package/dist/sites/upwork-com/upwork-com.types.d.ts.map +1 -1
- package/dist/sites/upwork-com/upwork-com.types.js.map +1 -1
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts +70 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts.map +1 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.js +334 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.js.map +1 -0
- package/npmrc +5 -0
- package/package.json +5 -1
- package/src/browser/RuntimeConfig.ts +57 -1
- package/src/core/ActorContext.ts +2 -0
- package/src/core/ActorRunner.ts +13 -1
- package/src/index.ts +2 -0
- package/src/native/CompositeNativeWindowDriver.ts +30 -0
- package/src/native/NativeActionRegistry.ts +114 -0
- package/src/native/NativeAutomation.ts +15 -0
- package/src/native/NativeCoordinateMapper.ts +258 -0
- package/src/native/NativeFileDialogService.ts +138 -0
- package/src/native/NativeImageFinder.ts +33 -0
- package/src/native/NativeKeyboard.ts +18 -0
- package/src/native/NativeMouse.ts +116 -0
- package/src/native/NativeWindowService.ts +229 -0
- package/src/native/UnsupportedNativeAutomation.ts +92 -0
- package/src/native/WindowMatcher.ts +31 -0
- package/src/native/drivers.ts +38 -0
- package/src/native/errors.ts +51 -0
- package/src/native/index.ts +12 -0
- package/src/native/macos/MacOSAccessibilityWindowDriver.ts +183 -0
- package/src/native/macos/MacOSAppleScriptClient.ts +182 -0
- package/src/native/macos/MacOSFileDialogAccessibilityStrategy.ts +11 -0
- package/src/native/macos/MacOSNativeAutomation.ts +86 -0
- package/src/native/nut/NutNativeImageFinder.ts +98 -0
- package/src/native/nut/NutNativeKeyboardDriver.ts +38 -0
- package/src/native/nut/NutNativeMouseDriver.ts +27 -0
- package/src/native/nut/NutNativeScreenDriver.ts +14 -0
- package/src/native/nut/NutNativeWindowDriver.ts +61 -0
- package/src/native/nut/loadNut.ts +86 -0
- package/src/native/types.ts +224 -0
- package/src/native/utils/appleScriptEscape.ts +11 -0
- package/src/native/utils/geometry.ts +88 -0
- package/src/native/utils/redactNative.ts +6 -0
- package/src/native/utils/waitFor.ts +25 -0
- package/src/sites/upwork-com/upwork-com.actor.ts +47 -14
- package/src/sites/upwork-com/upwork-com.types.ts +4 -1
- package/src/sites/upwork-com/util/parseJobApplicationDetails.ts +622 -0
- package/tests/fixtures/makeContext.ts +7 -2
- package/tests/fixtures/native/FakeNativeAutomation.ts +138 -0
- package/tests/unit/browser/RuntimeConfig.native.test.ts +63 -0
- package/tests/unit/core/ActorRunner.native.test.ts +69 -0
- package/tests/unit/native/MacOSAppleScriptClient.test.ts +35 -0
- package/tests/unit/native/NativeActionRegistry.test.ts +34 -0
- package/tests/unit/native/NativeCoordinateMapper.test.ts +92 -0
- package/tests/unit/native/NativeFileDialogService.test.ts +91 -0
- package/tests/unit/native/NativeMouse.test.ts +91 -0
- package/tests/unit/native/NativeWindowService.test.ts +87 -0
- package/tests/unit/native/WindowMatcher.test.ts +32 -0
- package/tests/unit/native/appleScriptEscape.test.ts +9 -0
- package/tests/unit/sites/myvistage-com.login.test.ts +1 -1
- package/tests/unit/sites/myvistage-com.postComment.test.ts +0 -1
- package/tests/unit/sites/upwork-com.login.test.ts +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NativeMouseButton, NativePoint, NativeRegion, NativeSize, NativeWindowInfo } from './types.js';
|
|
2
|
+
|
|
3
|
+
export interface NativeMouseDriver {
|
|
4
|
+
getPosition(): Promise<NativePoint>;
|
|
5
|
+
move(point: NativePoint): Promise<void>;
|
|
6
|
+
click(button: NativeMouseButton): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface NativeKeyboardDriver {
|
|
10
|
+
type(text: string, options?: { delayMs?: number }): Promise<void>;
|
|
11
|
+
pressKey(key: string): Promise<void>;
|
|
12
|
+
pressShortcut(keys: readonly string[]): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NativeScreenDriver {
|
|
16
|
+
width(): Promise<number>;
|
|
17
|
+
height(): Promise<number>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NativeWindowHandle {
|
|
21
|
+
info(): Promise<NativeWindowInfo>;
|
|
22
|
+
focus(): Promise<void>;
|
|
23
|
+
minimize(): Promise<void>;
|
|
24
|
+
restore(): Promise<void>;
|
|
25
|
+
move(point: NativePoint): Promise<void>;
|
|
26
|
+
resize(size: NativeSize): Promise<void>;
|
|
27
|
+
setFullscreen?(fullscreen: boolean): Promise<void>;
|
|
28
|
+
toggleFullscreen?(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NativeWindowDriver {
|
|
32
|
+
list(): Promise<NativeWindowHandle[]>;
|
|
33
|
+
active?(): Promise<NativeWindowHandle | null>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AppleScriptExecutor {
|
|
37
|
+
execute(script: string, options?: { timeoutMs?: number }): Promise<string>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class NativeAutomationError extends Error {
|
|
2
|
+
constructor(message: string, readonly meta: Record<string, unknown> = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'NativeAutomationError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class UnsupportedPlatformError extends NativeAutomationError {
|
|
9
|
+
constructor(platform: NodeJS.Platform) {
|
|
10
|
+
super(`Native automation is not supported on platform: ${platform}.`, { platform });
|
|
11
|
+
this.name = 'UnsupportedPlatformError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class NativeDependencyError extends NativeAutomationError {
|
|
16
|
+
constructor(packageName: string, cause?: unknown) {
|
|
17
|
+
super(
|
|
18
|
+
`Native automation dependency ${packageName} is not available. Install the optional native automation peer dependencies before using this feature.`,
|
|
19
|
+
cause === undefined ? { packageName } : { packageName, cause }
|
|
20
|
+
);
|
|
21
|
+
this.name = 'NativeDependencyError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class NativeWindowNotFoundError extends NativeAutomationError {
|
|
26
|
+
constructor(meta: Record<string, unknown>) {
|
|
27
|
+
super('Native window was not found.', meta);
|
|
28
|
+
this.name = 'NativeWindowNotFoundError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class NativeImageNotFoundError extends NativeAutomationError {
|
|
33
|
+
constructor(meta: Record<string, unknown>) {
|
|
34
|
+
super('Native image/template was not found on screen.', meta);
|
|
35
|
+
this.name = 'NativeImageNotFoundError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class NativeFileDialogError extends NativeAutomationError {
|
|
40
|
+
constructor(message: string, meta: Record<string, unknown> = {}) {
|
|
41
|
+
super(message, meta);
|
|
42
|
+
this.name = 'NativeFileDialogError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class CoordinateCalibrationError extends NativeAutomationError {
|
|
47
|
+
constructor(message: string, meta: Record<string, unknown> = {}) {
|
|
48
|
+
super(message, meta);
|
|
49
|
+
this.name = 'CoordinateCalibrationError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './drivers.js';
|
|
4
|
+
export * from './NativeActionRegistry.js';
|
|
5
|
+
export * from './NativeAutomation.js';
|
|
6
|
+
export * from './NativeCoordinateMapper.js';
|
|
7
|
+
export * from './NativeFileDialogService.js';
|
|
8
|
+
export * from './NativeImageFinder.js';
|
|
9
|
+
export * from './NativeKeyboard.js';
|
|
10
|
+
export * from './NativeMouse.js';
|
|
11
|
+
export * from './NativeWindowService.js';
|
|
12
|
+
export * from './WindowMatcher.js';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Logger } from '../../logging/Logger.js';
|
|
2
|
+
import type { NativeWindowDriver, NativeWindowHandle } from '../drivers.js';
|
|
3
|
+
import type { NativePoint, NativeRegion, NativeSize, NativeWindowInfo } from '../types.js';
|
|
4
|
+
import { appleScriptString } from '../utils/appleScriptEscape.js';
|
|
5
|
+
import { MacOSAppleScriptClient } from './MacOSAppleScriptClient.js';
|
|
6
|
+
|
|
7
|
+
const FIELD_SEPARATOR = '\u001f';
|
|
8
|
+
const ROW_SEPARATOR = '\u001e';
|
|
9
|
+
|
|
10
|
+
function parseBoolean(value: string): boolean | undefined {
|
|
11
|
+
if (value === 'true') return true;
|
|
12
|
+
if (value === 'false') return false;
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseNumber(value: string): number {
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class MacOSAccessibilityWindowHandle implements NativeWindowHandle {
|
|
22
|
+
constructor(private readonly client: MacOSAppleScriptClient, private currentInfo: NativeWindowInfo) {}
|
|
23
|
+
|
|
24
|
+
async info(): Promise<NativeWindowInfo> {
|
|
25
|
+
return this.currentInfo;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async focus(): Promise<void> {
|
|
29
|
+
await this.client.focusWindow(this.matcher(), this.currentInfo.appName ?? 'Google Chrome');
|
|
30
|
+
this.currentInfo = { ...this.currentInfo, isFocused: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async minimize(): Promise<void> {
|
|
34
|
+
await this.client.minimize(this.matcher(), this.currentInfo.appName ?? 'Google Chrome');
|
|
35
|
+
this.currentInfo = { ...this.currentInfo, isMinimized: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async restore(): Promise<void> {
|
|
39
|
+
await this.executeOnWindow(`set value of attribute "AXMinimized" of candidateWindow to false`);
|
|
40
|
+
this.currentInfo = { ...this.currentInfo, isMinimized: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async move(point: NativePoint): Promise<void> {
|
|
44
|
+
await this.executeOnWindow(`set position of candidateWindow to {${Math.round(point.x)}, ${Math.round(point.y)}}`);
|
|
45
|
+
this.currentInfo = {
|
|
46
|
+
...this.currentInfo,
|
|
47
|
+
region: { ...this.currentInfo.region, x: Math.round(point.x), y: Math.round(point.y) }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async resize(size: NativeSize): Promise<void> {
|
|
52
|
+
await this.executeOnWindow(`set size of candidateWindow to {${Math.round(size.width)}, ${Math.round(size.height)}}`);
|
|
53
|
+
this.currentInfo = {
|
|
54
|
+
...this.currentInfo,
|
|
55
|
+
region: { ...this.currentInfo.region, width: Math.round(size.width), height: Math.round(size.height) }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async setFullscreen(fullscreen: boolean): Promise<void> {
|
|
60
|
+
await this.client.setFullscreen(this.matcher(), fullscreen, this.currentInfo.appName ?? 'Google Chrome');
|
|
61
|
+
this.currentInfo = { ...this.currentInfo, isFullscreen: fullscreen };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async toggleFullscreen(): Promise<void> {
|
|
65
|
+
await this.client.toggleFullscreen(this.matcher(), this.currentInfo.appName ?? 'Google Chrome');
|
|
66
|
+
if (this.currentInfo.isFullscreen === undefined) {
|
|
67
|
+
this.currentInfo = { ...this.currentInfo };
|
|
68
|
+
} else {
|
|
69
|
+
this.currentInfo = { ...this.currentInfo, isFullscreen: !this.currentInfo.isFullscreen };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private matcher() {
|
|
74
|
+
const matcher: { appName?: string; title?: string } = {};
|
|
75
|
+
if (this.currentInfo.appName !== undefined) matcher.appName = this.currentInfo.appName;
|
|
76
|
+
matcher.title = this.currentInfo.title;
|
|
77
|
+
return matcher;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async executeOnWindow(action: string): Promise<void> {
|
|
81
|
+
const appName = this.currentInfo.appName ?? 'Google Chrome';
|
|
82
|
+
const title = this.currentInfo.title;
|
|
83
|
+
const script = `
|
|
84
|
+
tell application "System Events"
|
|
85
|
+
tell process ${appleScriptString(appName)}
|
|
86
|
+
repeat with candidateWindow in windows
|
|
87
|
+
if name of candidateWindow is ${appleScriptString(title)} then
|
|
88
|
+
${action}
|
|
89
|
+
return
|
|
90
|
+
end if
|
|
91
|
+
end repeat
|
|
92
|
+
end tell
|
|
93
|
+
end tell
|
|
94
|
+
`;
|
|
95
|
+
await this.client.execute(script);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class MacOSAccessibilityWindowDriver implements NativeWindowDriver {
|
|
100
|
+
constructor(private readonly client: MacOSAppleScriptClient, private readonly logger?: Logger) {}
|
|
101
|
+
|
|
102
|
+
async list(): Promise<NativeWindowHandle[]> {
|
|
103
|
+
const script = `
|
|
104
|
+
set fieldSeparator to ASCII character 31
|
|
105
|
+
set rowSeparator to ASCII character 30
|
|
106
|
+
set rows to {}
|
|
107
|
+
tell application "System Events"
|
|
108
|
+
repeat with appProcess in application processes
|
|
109
|
+
set appName to name of appProcess
|
|
110
|
+
repeat with candidateWindow in windows of appProcess
|
|
111
|
+
set windowTitle to ""
|
|
112
|
+
set px to 0
|
|
113
|
+
set py to 0
|
|
114
|
+
set windowWidth to 0
|
|
115
|
+
set windowHeight to 0
|
|
116
|
+
set minimizedValue to ""
|
|
117
|
+
set fullscreenValue to ""
|
|
118
|
+
try
|
|
119
|
+
set windowTitle to name of candidateWindow
|
|
120
|
+
end try
|
|
121
|
+
try
|
|
122
|
+
set windowPosition to position of candidateWindow
|
|
123
|
+
set px to item 1 of windowPosition
|
|
124
|
+
set py to item 2 of windowPosition
|
|
125
|
+
end try
|
|
126
|
+
try
|
|
127
|
+
set windowSize to size of candidateWindow
|
|
128
|
+
set windowWidth to item 1 of windowSize
|
|
129
|
+
set windowHeight to item 2 of windowSize
|
|
130
|
+
end try
|
|
131
|
+
try
|
|
132
|
+
set minimizedValue to (value of attribute "AXMinimized" of candidateWindow) as text
|
|
133
|
+
end try
|
|
134
|
+
try
|
|
135
|
+
set fullscreenValue to (value of attribute "AXFullScreen" of candidateWindow) as text
|
|
136
|
+
end try
|
|
137
|
+
copy (appName & fieldSeparator & windowTitle & fieldSeparator & px & fieldSeparator & py & fieldSeparator & windowWidth & fieldSeparator & windowHeight & fieldSeparator & minimizedValue & fieldSeparator & fullscreenValue) to end of rows
|
|
138
|
+
end repeat
|
|
139
|
+
end repeat
|
|
140
|
+
end tell
|
|
141
|
+
set AppleScript's text item delimiters to rowSeparator
|
|
142
|
+
return rows as text
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const output = await this.client.execute(script);
|
|
147
|
+
if (output.length === 0) return [];
|
|
148
|
+
return output.split(ROW_SEPARATOR)
|
|
149
|
+
.map(row => this.parseRow(row))
|
|
150
|
+
.filter((info): info is NativeWindowInfo => info !== null)
|
|
151
|
+
.map(info => new MacOSAccessibilityWindowHandle(this.client, info));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.logger?.debug('Failed to list windows through macOS Accessibility.', { error });
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private parseRow(row: string): NativeWindowInfo | null {
|
|
159
|
+
const fields = row.split(FIELD_SEPARATOR);
|
|
160
|
+
if (fields.length < 8) return null;
|
|
161
|
+
const [appName, title, xRaw, yRaw, widthRaw, heightRaw, minimizedRaw, fullscreenRaw] = fields;
|
|
162
|
+
if (appName === undefined || title === undefined || xRaw === undefined || yRaw === undefined || widthRaw === undefined || heightRaw === undefined) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const region: NativeRegion = {
|
|
167
|
+
x: parseNumber(xRaw),
|
|
168
|
+
y: parseNumber(yRaw),
|
|
169
|
+
width: parseNumber(widthRaw),
|
|
170
|
+
height: parseNumber(heightRaw)
|
|
171
|
+
};
|
|
172
|
+
const info: NativeWindowInfo = {
|
|
173
|
+
appName,
|
|
174
|
+
title,
|
|
175
|
+
region
|
|
176
|
+
};
|
|
177
|
+
const isMinimized = parseBoolean(minimizedRaw ?? '');
|
|
178
|
+
const isFullscreen = parseBoolean(fullscreenRaw ?? '');
|
|
179
|
+
if (isMinimized !== undefined) info.isMinimized = isMinimized;
|
|
180
|
+
if (isFullscreen !== undefined) info.isFullscreen = isFullscreen;
|
|
181
|
+
return info;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import type { AppleScriptExecutor } from '../drivers.js';
|
|
4
|
+
import type { WindowMatcher } from '../types.js';
|
|
5
|
+
import { appleScriptString } from '../utils/appleScriptEscape.js';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export class OsaScriptExecutor implements AppleScriptExecutor {
|
|
10
|
+
async execute(script: string, options: { timeoutMs?: number } = {}): Promise<string> {
|
|
11
|
+
const { stdout } = await execFileAsync('osascript', ['-l', 'AppleScript', '-e', script], {
|
|
12
|
+
timeout: options.timeoutMs ?? 15_000,
|
|
13
|
+
maxBuffer: 1024 * 1024
|
|
14
|
+
});
|
|
15
|
+
return stdout.trim();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MacOSAppleScriptClientOptions {
|
|
20
|
+
executor?: AppleScriptExecutor;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function matcherCondition(matcher: WindowMatcher): string {
|
|
25
|
+
const conditions: string[] = [];
|
|
26
|
+
if (matcher.title !== undefined) {
|
|
27
|
+
conditions.push(`name of candidateWindow is ${appleScriptString(matcher.title)}`);
|
|
28
|
+
}
|
|
29
|
+
if (matcher.titleIncludes !== undefined) {
|
|
30
|
+
conditions.push(`name of candidateWindow contains ${appleScriptString(matcher.titleIncludes)}`);
|
|
31
|
+
}
|
|
32
|
+
if (matcher.role !== undefined) {
|
|
33
|
+
conditions.push(`role of candidateWindow is ${appleScriptString(matcher.role)}`);
|
|
34
|
+
}
|
|
35
|
+
return conditions.length === 0 ? 'true' : conditions.join(' and ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function processTarget(matcher: WindowMatcher, fallbackAppName: string): string {
|
|
39
|
+
return appleScriptString(matcher.appName ?? fallbackAppName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class MacOSAppleScriptClient {
|
|
43
|
+
private readonly executor: AppleScriptExecutor;
|
|
44
|
+
private readonly timeoutMs: number;
|
|
45
|
+
|
|
46
|
+
constructor(options: MacOSAppleScriptClientOptions = {}) {
|
|
47
|
+
this.executor = options.executor ?? new OsaScriptExecutor();
|
|
48
|
+
this.timeoutMs = options.timeoutMs ?? 15_000;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async execute(script: string, timeoutMs?: number): Promise<string> {
|
|
52
|
+
return this.executor.execute(script, { timeoutMs: timeoutMs ?? this.timeoutMs });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async isWindowOpen(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<boolean> {
|
|
56
|
+
const script = `
|
|
57
|
+
tell application "System Events"
|
|
58
|
+
if not (exists process ${processTarget(matcher, fallbackAppName)}) then return "false"
|
|
59
|
+
tell process ${processTarget(matcher, fallbackAppName)}
|
|
60
|
+
repeat with candidateWindow in windows
|
|
61
|
+
if ${matcherCondition(matcher)} then return "true"
|
|
62
|
+
end repeat
|
|
63
|
+
end tell
|
|
64
|
+
end tell
|
|
65
|
+
return "false"
|
|
66
|
+
`;
|
|
67
|
+
return (await this.execute(script)) === 'true';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async focusWindow(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<void> {
|
|
71
|
+
const script = `
|
|
72
|
+
tell application ${processTarget(matcher, fallbackAppName)} to activate
|
|
73
|
+
tell application "System Events"
|
|
74
|
+
tell process ${processTarget(matcher, fallbackAppName)}
|
|
75
|
+
repeat with candidateWindow in windows
|
|
76
|
+
if ${matcherCondition(matcher)} then
|
|
77
|
+
perform action "AXRaise" of candidateWindow
|
|
78
|
+
return
|
|
79
|
+
end if
|
|
80
|
+
end repeat
|
|
81
|
+
end tell
|
|
82
|
+
end tell
|
|
83
|
+
`;
|
|
84
|
+
await this.execute(script);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async setFullscreen(matcher: WindowMatcher, fullscreen: boolean, fallbackAppName = 'Google Chrome'): Promise<void> {
|
|
88
|
+
const desired = fullscreen ? 'true' : 'false';
|
|
89
|
+
const script = `
|
|
90
|
+
tell application ${processTarget(matcher, fallbackAppName)} to activate
|
|
91
|
+
tell application "System Events"
|
|
92
|
+
tell process ${processTarget(matcher, fallbackAppName)}
|
|
93
|
+
repeat with candidateWindow in windows
|
|
94
|
+
if ${matcherCondition(matcher)} then
|
|
95
|
+
set currentFullscreen to false
|
|
96
|
+
try
|
|
97
|
+
set currentFullscreen to value of attribute "AXFullScreen" of candidateWindow
|
|
98
|
+
end try
|
|
99
|
+
if currentFullscreen is not ${desired} then
|
|
100
|
+
try
|
|
101
|
+
perform action "AXPress" of (first button of candidateWindow whose subrole is "AXFullScreenButton")
|
|
102
|
+
on error
|
|
103
|
+
click (first button of candidateWindow whose subrole is "AXFullScreenButton")
|
|
104
|
+
end try
|
|
105
|
+
end if
|
|
106
|
+
return
|
|
107
|
+
end if
|
|
108
|
+
end repeat
|
|
109
|
+
end tell
|
|
110
|
+
end tell
|
|
111
|
+
`;
|
|
112
|
+
await this.execute(script);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async toggleFullscreen(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<void> {
|
|
116
|
+
const script = `
|
|
117
|
+
tell application ${processTarget(matcher, fallbackAppName)} to activate
|
|
118
|
+
tell application "System Events"
|
|
119
|
+
tell process ${processTarget(matcher, fallbackAppName)}
|
|
120
|
+
repeat with candidateWindow in windows
|
|
121
|
+
if ${matcherCondition(matcher)} then
|
|
122
|
+
try
|
|
123
|
+
perform action "AXPress" of (first button of candidateWindow whose subrole is "AXFullScreenButton")
|
|
124
|
+
on error
|
|
125
|
+
click (first button of candidateWindow whose subrole is "AXFullScreenButton")
|
|
126
|
+
end try
|
|
127
|
+
return
|
|
128
|
+
end if
|
|
129
|
+
end repeat
|
|
130
|
+
end tell
|
|
131
|
+
end tell
|
|
132
|
+
`;
|
|
133
|
+
await this.execute(script);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async minimize(matcher: WindowMatcher, fallbackAppName = 'Google Chrome'): Promise<void> {
|
|
137
|
+
const script = `
|
|
138
|
+
tell application "System Events"
|
|
139
|
+
tell process ${processTarget(matcher, fallbackAppName)}
|
|
140
|
+
repeat with candidateWindow in windows
|
|
141
|
+
if ${matcherCondition(matcher)} then
|
|
142
|
+
try
|
|
143
|
+
perform action "AXPress" of (first button of candidateWindow whose subrole is "AXMinimizeButton")
|
|
144
|
+
on error
|
|
145
|
+
set value of attribute "AXMinimized" of candidateWindow to true
|
|
146
|
+
end try
|
|
147
|
+
return
|
|
148
|
+
end if
|
|
149
|
+
end repeat
|
|
150
|
+
end tell
|
|
151
|
+
end tell
|
|
152
|
+
`;
|
|
153
|
+
await this.execute(script);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async selectFileInOpenDialog(filePath: string, appName = 'Google Chrome', timeoutMs?: number): Promise<void> {
|
|
157
|
+
const script = `
|
|
158
|
+
set targetPath to ${appleScriptString(filePath)}
|
|
159
|
+
tell application ${appleScriptString(appName)} to activate
|
|
160
|
+
tell application "System Events"
|
|
161
|
+
tell process ${appleScriptString(appName)}
|
|
162
|
+
set frontmost to true
|
|
163
|
+
set waited to 0
|
|
164
|
+
repeat while waited < 100
|
|
165
|
+
if (exists window 1) then exit repeat
|
|
166
|
+
delay 0.1
|
|
167
|
+
set waited to waited + 1
|
|
168
|
+
end repeat
|
|
169
|
+
keystroke "g" using {command down, shift down}
|
|
170
|
+
delay 0.2
|
|
171
|
+
set the clipboard to targetPath
|
|
172
|
+
keystroke "v" using {command down}
|
|
173
|
+
delay 0.1
|
|
174
|
+
key code 36
|
|
175
|
+
delay 0.2
|
|
176
|
+
key code 36
|
|
177
|
+
end tell
|
|
178
|
+
end tell
|
|
179
|
+
`;
|
|
180
|
+
await this.execute(script, timeoutMs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SelectFileOptions } from '../types.js';
|
|
2
|
+
import type { FileDialogAccessibilityStrategy } from '../NativeFileDialogService.js';
|
|
3
|
+
import { MacOSAppleScriptClient } from './MacOSAppleScriptClient.js';
|
|
4
|
+
|
|
5
|
+
export class MacOSFileDialogAccessibilityStrategy implements FileDialogAccessibilityStrategy {
|
|
6
|
+
constructor(private readonly client: MacOSAppleScriptClient, private readonly defaultAppName = 'Google Chrome') {}
|
|
7
|
+
|
|
8
|
+
async selectFile(filePath: string, options: SelectFileOptions): Promise<void> {
|
|
9
|
+
await this.client.selectFileInOpenDialog(filePath, options.appName ?? this.defaultAppName, options.timeoutMs);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { NativeAutomation, NativeAutomationFactoryArgs } from '../types.js';
|
|
2
|
+
import { DefaultNativeActionRegistry, registerDefaultNativeActions } from '../NativeActionRegistry.js';
|
|
3
|
+
import { HumanizedNativeMouse, type HumanizedNativeMouseOptions } from '../NativeMouse.js';
|
|
4
|
+
import { DefaultNativeKeyboard } from '../NativeKeyboard.js';
|
|
5
|
+
import { DefaultNativeWindowService, type NativeWindowServiceDependencies } from '../NativeWindowService.js';
|
|
6
|
+
import { DefaultNativeImageFinder } from '../NativeImageFinder.js';
|
|
7
|
+
import { ImageBasedCoordinateMapper, type ImageBasedCoordinateMapperOptions } from '../NativeCoordinateMapper.js';
|
|
8
|
+
import { DefaultNativeFileDialogService, type NativeFileDialogServiceDependencies } from '../NativeFileDialogService.js';
|
|
9
|
+
import { NutNativeMouseDriver } from '../nut/NutNativeMouseDriver.js';
|
|
10
|
+
import { NutNativeKeyboardDriver } from '../nut/NutNativeKeyboardDriver.js';
|
|
11
|
+
import { NutNativeScreenDriver } from '../nut/NutNativeScreenDriver.js';
|
|
12
|
+
import { NutNativeWindowDriver } from '../nut/NutNativeWindowDriver.js';
|
|
13
|
+
import { FallbackNativeWindowDriver } from '../CompositeNativeWindowDriver.js';
|
|
14
|
+
import { MacOSAccessibilityWindowDriver } from './MacOSAccessibilityWindowDriver.js';
|
|
15
|
+
import { NutNativeImageFinderDriver, type NutNativeImageFinderOptions } from '../nut/NutNativeImageFinder.js';
|
|
16
|
+
import { MacOSAppleScriptClient, type MacOSAppleScriptClientOptions } from './MacOSAppleScriptClient.js';
|
|
17
|
+
import { MacOSFileDialogAccessibilityStrategy } from './MacOSFileDialogAccessibilityStrategy.js';
|
|
18
|
+
|
|
19
|
+
export function createMacOSNativeAutomation(args: NativeAutomationFactoryArgs): NativeAutomation {
|
|
20
|
+
const nativeConfig = args.config.native;
|
|
21
|
+
const mouseDriver = new NutNativeMouseDriver();
|
|
22
|
+
const keyboardDriver = new NutNativeKeyboardDriver();
|
|
23
|
+
const screenDriver = new NutNativeScreenDriver();
|
|
24
|
+
const appleScriptOptions: MacOSAppleScriptClientOptions = {};
|
|
25
|
+
const osascriptTimeoutMs = nativeConfig?.macos?.osascriptTimeoutMs ?? args.config.defaultTimeoutMs;
|
|
26
|
+
if (osascriptTimeoutMs !== undefined) appleScriptOptions.timeoutMs = osascriptTimeoutMs;
|
|
27
|
+
const appleScriptClient = new MacOSAppleScriptClient(appleScriptOptions);
|
|
28
|
+
const windowDriver = new FallbackNativeWindowDriver(
|
|
29
|
+
new MacOSAccessibilityWindowDriver(appleScriptClient, args.logger),
|
|
30
|
+
new NutNativeWindowDriver(),
|
|
31
|
+
args.logger
|
|
32
|
+
);
|
|
33
|
+
const keyboard = new DefaultNativeKeyboard(keyboardDriver);
|
|
34
|
+
const imageDriverOptions: NutNativeImageFinderOptions = { enableNlMatcher: true };
|
|
35
|
+
if (nativeConfig?.templatesDir !== undefined) imageDriverOptions.templatesDir = nativeConfig.templatesDir;
|
|
36
|
+
if (nativeConfig?.imageConfidence !== undefined) imageDriverOptions.confidence = nativeConfig.imageConfidence;
|
|
37
|
+
const imageDriver = new NutNativeImageFinderDriver(imageDriverOptions);
|
|
38
|
+
const images = new DefaultNativeImageFinder(imageDriver, args.config.defaultTimeoutMs);
|
|
39
|
+
const coordinateOptions: ImageBasedCoordinateMapperOptions = {
|
|
40
|
+
images,
|
|
41
|
+
logger: args.logger
|
|
42
|
+
};
|
|
43
|
+
const coordinateTimeoutMs = nativeConfig?.coordinateCalibration?.timeoutMs ?? args.config.defaultTimeoutMs;
|
|
44
|
+
if (coordinateTimeoutMs !== undefined) coordinateOptions.defaultTimeoutMs = coordinateTimeoutMs;
|
|
45
|
+
if (nativeConfig?.coordinateCalibration?.cacheTtlMs !== undefined) {
|
|
46
|
+
coordinateOptions.defaultCacheTtlMs = nativeConfig.coordinateCalibration.cacheTtlMs;
|
|
47
|
+
}
|
|
48
|
+
const coordinates = new ImageBasedCoordinateMapper(coordinateOptions);
|
|
49
|
+
const mouseOptions: HumanizedNativeMouseOptions = { coordinateMapper: coordinates };
|
|
50
|
+
if (nativeConfig?.mouse?.jitterPixels !== undefined) mouseOptions.jitterPixels = nativeConfig.mouse.jitterPixels;
|
|
51
|
+
if (nativeConfig?.mouse?.minJitterPixels !== undefined) mouseOptions.minJitterPixels = nativeConfig.mouse.minJitterPixels;
|
|
52
|
+
if (nativeConfig?.mouse?.moveSpeed !== undefined) mouseOptions.moveSpeed = nativeConfig.mouse.moveSpeed;
|
|
53
|
+
const mouse = new HumanizedNativeMouse(mouseDriver, mouseOptions);
|
|
54
|
+
const windowOptions: NativeWindowServiceDependencies = {
|
|
55
|
+
driver: windowDriver,
|
|
56
|
+
screen: screenDriver,
|
|
57
|
+
mouse,
|
|
58
|
+
images,
|
|
59
|
+
logger: args.logger
|
|
60
|
+
};
|
|
61
|
+
if (args.config.defaultTimeoutMs !== undefined) windowOptions.defaultTimeoutMs = args.config.defaultTimeoutMs;
|
|
62
|
+
const windows = new DefaultNativeWindowService(windowOptions);
|
|
63
|
+
const fileDialogOptions: NativeFileDialogServiceDependencies = {
|
|
64
|
+
keyboard,
|
|
65
|
+
mouse,
|
|
66
|
+
images,
|
|
67
|
+
accessibility: new MacOSFileDialogAccessibilityStrategy(appleScriptClient, nativeConfig?.macos?.targetApplicationName ?? 'Google Chrome'),
|
|
68
|
+
logger: args.logger
|
|
69
|
+
};
|
|
70
|
+
if (args.config.defaultTimeoutMs !== undefined) fileDialogOptions.defaultTimeoutMs = args.config.defaultTimeoutMs;
|
|
71
|
+
const fileDialogs = new DefaultNativeFileDialogService(fileDialogOptions);
|
|
72
|
+
|
|
73
|
+
const automation = {
|
|
74
|
+
windows,
|
|
75
|
+
fileDialogs,
|
|
76
|
+
mouse,
|
|
77
|
+
keyboard,
|
|
78
|
+
coordinates,
|
|
79
|
+
images,
|
|
80
|
+
actions: undefined as unknown as DefaultNativeActionRegistry
|
|
81
|
+
} satisfies Omit<NativeAutomation, 'actions'> & { actions: DefaultNativeActionRegistry };
|
|
82
|
+
|
|
83
|
+
const registry = new DefaultNativeActionRegistry(() => ({ automation, logger: args.logger }));
|
|
84
|
+
automation.actions = registerDefaultNativeActions(registry, automation, args.logger);
|
|
85
|
+
return automation;
|
|
86
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { NativeImageFinderDriver } from '../NativeImageFinder.js';
|
|
5
|
+
import type { NativeImageMatch, NativeImageSearchOptions, NativeRegion, NativeTemplateRef } from '../types.js';
|
|
6
|
+
import { loadNlMatcher, loadNut, type NutRegion } from './loadNut.js';
|
|
7
|
+
|
|
8
|
+
export interface NutNativeImageFinderOptions {
|
|
9
|
+
templatesDir?: string;
|
|
10
|
+
confidence?: number;
|
|
11
|
+
enableNlMatcher?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toRegion(region: NutRegion): NativeRegion {
|
|
15
|
+
return {
|
|
16
|
+
x: region.left ?? region.x ?? 0,
|
|
17
|
+
y: region.top ?? region.y ?? 0,
|
|
18
|
+
width: region.width,
|
|
19
|
+
height: region.height
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function templateName(template: string | NativeTemplateRef): string {
|
|
24
|
+
return typeof template === 'string' ? template : template.name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NutNativeImageFinderDriver implements NativeImageFinderDriver {
|
|
28
|
+
private initialized = false;
|
|
29
|
+
|
|
30
|
+
constructor(private readonly options: NutNativeImageFinderOptions = {}) {}
|
|
31
|
+
|
|
32
|
+
async findTemplate(template: string | NativeTemplateRef, options: NativeImageSearchOptions = {}): Promise<NativeImageMatch | null> {
|
|
33
|
+
const nut = await this.initialize();
|
|
34
|
+
if (nut.screen.find === undefined || nut.imageResource === undefined) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const resourcePath = await this.materializeTemplate(template);
|
|
39
|
+
const searchOptions = this.buildSearchOptions(options);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const region = await nut.screen.find(nut.imageResource(resourcePath), searchOptions);
|
|
43
|
+
const match: NativeImageMatch = { region: toRegion(region) };
|
|
44
|
+
if (options.confidence !== undefined) match.confidence = options.confidence;
|
|
45
|
+
return match;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async initialize() {
|
|
52
|
+
const nut = await loadNut();
|
|
53
|
+
if (!this.initialized) {
|
|
54
|
+
if (this.options.enableNlMatcher ?? true) {
|
|
55
|
+
await loadNlMatcher();
|
|
56
|
+
}
|
|
57
|
+
if (this.options.templatesDir !== undefined && nut.screen.config !== undefined) {
|
|
58
|
+
nut.screen.config.resourceDirectory = this.options.templatesDir;
|
|
59
|
+
}
|
|
60
|
+
if (this.options.confidence !== undefined && nut.screen.config !== undefined) {
|
|
61
|
+
nut.screen.config.confidence = this.options.confidence;
|
|
62
|
+
}
|
|
63
|
+
this.initialized = true;
|
|
64
|
+
}
|
|
65
|
+
return nut;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private buildSearchOptions(options: NativeImageSearchOptions): Record<string, unknown> | undefined {
|
|
69
|
+
const searchOptions: Record<string, unknown> = {};
|
|
70
|
+
if (options.searchRegion !== undefined) {
|
|
71
|
+
searchOptions.searchRegion = {
|
|
72
|
+
left: options.searchRegion.x,
|
|
73
|
+
top: options.searchRegion.y,
|
|
74
|
+
width: options.searchRegion.width,
|
|
75
|
+
height: options.searchRegion.height
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (options.confidence !== undefined) {
|
|
79
|
+
searchOptions.confidence = options.confidence;
|
|
80
|
+
}
|
|
81
|
+
return Object.keys(searchOptions).length === 0 ? undefined : searchOptions;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async materializeTemplate(template: string | NativeTemplateRef): Promise<string> {
|
|
85
|
+
if (typeof template === 'string') {
|
|
86
|
+
return this.options.templatesDir === undefined ? template : path.join(this.options.templatesDir, template);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (template.buffer === undefined) {
|
|
90
|
+
return this.options.templatesDir === undefined ? template.name : path.join(this.options.templatesDir, template.name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fileName = `${template.name.replace(/[^a-z0-9._-]/gi, '_')}`;
|
|
94
|
+
const filePath = path.join(os.tmpdir(), fileName);
|
|
95
|
+
await fs.writeFile(filePath, template.buffer);
|
|
96
|
+
return filePath;
|
|
97
|
+
}
|
|
98
|
+
}
|