node-native-win-utils 2.1.5 → 2.2.2
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/README.md +113 -633
- package/binding.gyp +1 -0
- package/dist/index.d.mts +14 -3
- package/dist/index.mjs +3 -6
- package/package.json +7 -12
- package/prebuilds/win32-x64/node-native-win-utils.node +0 -0
- package/src/cpp/getWindowData.cpp +73 -0
- package/src/cpp/main.cpp +5 -1
- package/src/cpp/mouse.cpp +271 -38
- package/src/index.mts +14 -2
package/binding.gyp
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -75,6 +75,16 @@ export type Color = [r: number, g: number, b: number];
|
|
|
75
75
|
export type ROI = [x: number, y: number, width: number, height: number];
|
|
76
76
|
export type Imread = (path: string) => ImageData;
|
|
77
77
|
export type Imwrite = (image: ImageData) => Buffer;
|
|
78
|
+
export type StartMouseListener = (callback: (data: {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
type: string;
|
|
82
|
+
}) => void) => void;
|
|
83
|
+
export type StopMouseListener = () => {
|
|
84
|
+
success: boolean;
|
|
85
|
+
error?: string;
|
|
86
|
+
errorCode?: number;
|
|
87
|
+
};
|
|
78
88
|
export type MatchTemplate = (image: ImageData, template: ImageData, method?: number | null, mask?: ImageData) => MatchData;
|
|
79
89
|
export type Blur = (image: ImageData, sizeX: number, sizeY: number) => ImageData;
|
|
80
90
|
export type BgrToGray = (image: ImageData) => ImageData;
|
|
@@ -85,7 +95,8 @@ export type DrawRectangle = (image: ImageData, start: Point, end: Point, rgb: Co
|
|
|
85
95
|
export type GetRegion = (image: ImageData, region: ROI) => ImageData;
|
|
86
96
|
export type TextRecognition = (trainedDataPath: string, dataLang: string, imagePath: string) => string;
|
|
87
97
|
export type CaptureScreenAsync = () => Promise<Buffer>;
|
|
88
|
-
|
|
98
|
+
export type BringWindowToFront = (windowName: string) => boolean;
|
|
99
|
+
declare const getWindowData: GetWindowData, captureWindowN: CaptureWindow, captureScreenAsync: CaptureScreenAsync, mouseMove: MouseMove, mouseClick: MouseClick, mouseDrag: MouseDrag, typeString: TypeString, textRecognition: TextRecognition, bringWindowToFront: BringWindowToFront, startMouseListener: StartMouseListener, stopMouseListener: StopMouseListener;
|
|
89
100
|
declare const rawPressKey: PressKey;
|
|
90
101
|
/**
|
|
91
102
|
* Captures a window and saves it to a file.
|
|
@@ -252,7 +263,7 @@ declare class OpenCV {
|
|
|
252
263
|
* @returns A new OpenCV instance with the equalized image data.
|
|
253
264
|
*/
|
|
254
265
|
equalizeHist(): OpenCV;
|
|
255
|
-
rgbToHsv(rgb: ColorBound): number[];
|
|
266
|
+
rgbToHsv(rgb: ColorBound): number[][];
|
|
256
267
|
darkenColor(lowerBound: ColorBound, upperBound: ColorBound, darkenFactor: number): OpenCV;
|
|
257
268
|
/**
|
|
258
269
|
* Draws a rectangle on the image.
|
|
@@ -276,4 +287,4 @@ declare class OpenCV {
|
|
|
276
287
|
imwrite(path: string): void;
|
|
277
288
|
}
|
|
278
289
|
declare function keyPress(keyCode: number, repeat?: number): Promise<boolean>;
|
|
279
|
-
export { getWindowData, captureWindow, captureWindowN, mouseMove, mouseClick, mouseDrag, typeString, keyPress, rawPressKey, KeyCodeHelper, textRecognition, captureScreenToFile, captureScreenAsync, KeyboardListener, OpenCV };
|
|
290
|
+
export { getWindowData, bringWindowToFront, captureWindow, captureWindowN, mouseMove, mouseClick, mouseDrag, typeString, keyPress, rawPressKey, KeyCodeHelper, textRecognition, captureScreenToFile, captureScreenAsync, startMouseListener, stopMouseListener, KeyboardListener, OpenCV };
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const nodeGypBuild = __require("node-gyp-build");
|
|
|
8
8
|
import { keyCodes, KeyCodeHelper } from "./keyCodes.mjs";
|
|
9
9
|
import { __dirnameLocal } from "./dirnameLocal.mjs";
|
|
10
10
|
const bindings = nodeGypBuild(path.resolve(__dirnameLocal, ".."));
|
|
11
|
-
const { setKeyDownCallback, setKeyUpCallback, unsetKeyDownCallback, unsetKeyUpCallback, getWindowData, captureWindowN, captureScreenAsync, mouseMove, mouseClick, mouseDrag, typeString, pressKey, imread, imwrite, matchTemplate, blur, bgrToGray, drawRectangle, getRegion, textRecognition, equalizeHist, darkenColor } = bindings;
|
|
11
|
+
const { setKeyDownCallback, setKeyUpCallback, unsetKeyDownCallback, unsetKeyUpCallback, getWindowData, captureWindowN, captureScreenAsync, mouseMove, mouseClick, mouseDrag, typeString, pressKey, imread, imwrite, matchTemplate, blur, bgrToGray, drawRectangle, getRegion, textRecognition, equalizeHist, darkenColor, bringWindowToFront, startMouseListener, stopMouseListener } = bindings;
|
|
12
12
|
const rawPressKey = pressKey;
|
|
13
13
|
/**
|
|
14
14
|
* Captures a window and saves it to a file.
|
|
@@ -209,11 +209,8 @@ class OpenCV {
|
|
|
209
209
|
if (Cmax !== 0)
|
|
210
210
|
Saturation = (delta / Cmax) * 100;
|
|
211
211
|
let Value = Cmax * 100;
|
|
212
|
-
// 🛠 Convert to OpenCV format
|
|
213
212
|
return [
|
|
214
|
-
|
|
215
|
-
Math.round((Saturation / 100) * 255), // Scale S from [0, 100] → [0, 255]
|
|
216
|
-
Math.round((Value / 100) * 255) // Scale V from [0, 100] → [0, 255]
|
|
213
|
+
[Hue, Saturation, Value]
|
|
217
214
|
];
|
|
218
215
|
}
|
|
219
216
|
darkenColor(lowerBound, upperBound, darkenFactor) {
|
|
@@ -265,4 +262,4 @@ function keyPress(keyCode, repeat) {
|
|
|
265
262
|
return resolve(true);
|
|
266
263
|
});
|
|
267
264
|
}
|
|
268
|
-
export { getWindowData, captureWindow, captureWindowN, mouseMove, mouseClick, mouseDrag, typeString, keyPress, rawPressKey, KeyCodeHelper, textRecognition, captureScreenToFile, captureScreenAsync, KeyboardListener, OpenCV };
|
|
265
|
+
export { getWindowData, bringWindowToFront, captureWindow, captureWindowN, mouseMove, mouseClick, mouseDrag, typeString, keyPress, rawPressKey, KeyCodeHelper, textRecognition, captureScreenToFile, captureScreenAsync, startMouseListener, stopMouseListener, KeyboardListener, OpenCV };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-native-win-utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"author": "Andrew K.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/T-Rumibul/node-native-win-utils.git",
|
|
@@ -40,21 +40,16 @@
|
|
|
40
40
|
"install": "node-gyp-build",
|
|
41
41
|
"build": "node-gyp clean && npx prebuildify --napi && node ./buildHelpers/dllCopy.js && npm run buildts",
|
|
42
42
|
"buildts": "node ./buildHelpers/cjsCompatBuild.js",
|
|
43
|
-
"test": "tap run
|
|
43
|
+
"test": "tap run"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"node-addon-api": "^8.
|
|
47
|
-
"node-gyp": "^
|
|
46
|
+
"node-addon-api": "^8.6.0",
|
|
47
|
+
"node-gyp": "^12.2.0",
|
|
48
48
|
"node-gyp-build": "^4.8.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"node-api-headers": "^1.
|
|
53
|
-
"tap": "^21.
|
|
54
|
-
},
|
|
55
|
-
"tap": {
|
|
56
|
-
"plugin": [
|
|
57
|
-
"@tapjs/sinon"
|
|
58
|
-
]
|
|
51
|
+
"@types/node": "^25.3.5",
|
|
52
|
+
"node-api-headers": "^1.8.0",
|
|
53
|
+
"tap": "^21.6.2"
|
|
59
54
|
}
|
|
60
55
|
}
|
|
Binary file
|
|
@@ -42,5 +42,78 @@ Napi::Value GetWindowData(const Napi::CallbackInfo &info)
|
|
|
42
42
|
result.Set("x", Napi::Number::New(env, x));
|
|
43
43
|
result.Set("y", Napi::Number::New(env, y));
|
|
44
44
|
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Napi::Value BringWindowToFront(const Napi::CallbackInfo &info)
|
|
49
|
+
{
|
|
50
|
+
Napi::Env env = info.Env();
|
|
51
|
+
Napi::Object result = Napi::Object::New(env);
|
|
52
|
+
|
|
53
|
+
auto returnError = [&](const std::string& message, DWORD errorCode = 0) {
|
|
54
|
+
result.Set("success", false);
|
|
55
|
+
result.Set("error", message);
|
|
56
|
+
if (errorCode != 0) {
|
|
57
|
+
result.Set("errorCode", Napi::Number::New(env, errorCode));
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (info.Length() < 1 || !info[0].IsString())
|
|
63
|
+
{
|
|
64
|
+
return returnError("Window name must be provided as a string");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
std::string windowName = info[0].As<Napi::String>().Utf8Value();
|
|
68
|
+
|
|
69
|
+
HWND hWnd = GetWindowByName(windowName.c_str());
|
|
70
|
+
if (!hWnd || !IsWindow(hWnd))
|
|
71
|
+
{
|
|
72
|
+
return returnError("Window not found or invalid: " + windowName);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Restore if minimized
|
|
76
|
+
if (IsIconic(hWnd))
|
|
77
|
+
{
|
|
78
|
+
ShowWindow(hWnd, SW_RESTORE);
|
|
79
|
+
// Give the window a moment to restore before trying to focus it
|
|
80
|
+
Sleep(50);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
HWND fg = GetForegroundWindow();
|
|
84
|
+
|
|
85
|
+
// Bug fix 1: must attach the TARGET window's thread, not just the foreground
|
|
86
|
+
// window's thread. We need a triangle: cur <-> fg thread <-> target thread.
|
|
87
|
+
DWORD fgThread = fg ? GetWindowThreadProcessId(fg, nullptr) : 0;
|
|
88
|
+
DWORD tgtThread = GetWindowThreadProcessId(hWnd, nullptr);
|
|
89
|
+
DWORD curThread = GetCurrentThreadId();
|
|
90
|
+
|
|
91
|
+
// Bug fix 2: attach all relevant thread pairs so input state is fully shared
|
|
92
|
+
bool attachedFg = (fgThread && fgThread != curThread) ? AttachThreadInput(curThread, fgThread, TRUE) != 0 : false;
|
|
93
|
+
bool attachedTgt = (tgtThread && tgtThread != curThread) ? AttachThreadInput(curThread, tgtThread, TRUE) != 0 : false;
|
|
94
|
+
|
|
95
|
+
// Bug fix 3: use AllowSetForegroundWindow to explicitly grant permission,
|
|
96
|
+
// which bypasses Windows' focus-stealing prevention for cross-process calls
|
|
97
|
+
AllowSetForegroundWindow(ASFW_ANY);
|
|
98
|
+
|
|
99
|
+
// Bring the window to front using the full sequence
|
|
100
|
+
SetForegroundWindow(hWnd);
|
|
101
|
+
SetActiveWindow(hWnd);
|
|
102
|
+
BringWindowToTop(hWnd);
|
|
103
|
+
// Bug fix 4: SetFocus only works reliably once input queues are attached
|
|
104
|
+
SetFocus(hWnd);
|
|
105
|
+
|
|
106
|
+
// Always detach — don't leave threads attached on any code path
|
|
107
|
+
if (attachedFg) AttachThreadInput(curThread, fgThread, FALSE);
|
|
108
|
+
if (attachedTgt) AttachThreadInput(curThread, tgtThread, FALSE);
|
|
109
|
+
|
|
110
|
+
// Verify it actually worked
|
|
111
|
+
HWND newFg = GetForegroundWindow();
|
|
112
|
+
if (newFg != hWnd)
|
|
113
|
+
{
|
|
114
|
+
return returnError("Window did not come to foreground (focus-stealing prevention may be active)");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result.Set("success", true);
|
|
45
118
|
return result;
|
|
46
119
|
}
|
package/src/cpp/main.cpp
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
#include <mouse.cpp>
|
|
7
7
|
#include <opencv.cpp>
|
|
8
8
|
#include <tesseract.cpp>
|
|
9
|
-
|
|
9
|
+
#include <ShellScalingApi.h>
|
|
10
10
|
Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
11
11
|
{
|
|
12
|
+
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
|
12
13
|
exports.Set("getWindowData", Napi::Function::New(env, GetWindowData));
|
|
13
14
|
exports.Set("captureWindowN", Napi::Function::New(env, CaptureWindow));
|
|
14
15
|
exports.Set("captureScreenAsync", Napi::Function::New(env, CaptureScreenAsync));
|
|
@@ -31,6 +32,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
|
31
32
|
exports.Set("drawRectangle", Napi::Function::New(env, DrawRectangle));
|
|
32
33
|
exports.Set("getRegion", Napi::Function::New(env, GetRegion));
|
|
33
34
|
exports.Set("textRecognition", Napi::Function::New(env, TextRecognition));
|
|
35
|
+
exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
|
|
36
|
+
exports.Set("startMouseListener", Napi::Function::New(env, StartMouseListener));
|
|
37
|
+
exports.Set("stopMouseListener", Napi::Function::New(env, StopMouseListener));
|
|
34
38
|
static GdiPlusInitializer gdiplusInitializer;
|
|
35
39
|
// Register cleanup hook
|
|
36
40
|
env.AddCleanupHook(CleanupHook);
|
package/src/cpp/mouse.cpp
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
// patrially used code from https://github.com/octalmage/robotjs witch is under MIT License Copyright (c) 2014 Jason Stallings
|
|
2
2
|
#include <napi.h>
|
|
3
3
|
#include <windows.h>
|
|
4
|
+
#include <atomic>
|
|
5
|
+
#include <cmath>
|
|
6
|
+
|
|
7
|
+
HHOOK mouseHook;
|
|
8
|
+
Napi::ThreadSafeFunction tsfn;
|
|
9
|
+
static HANDLE hHookThread = NULL;
|
|
10
|
+
static DWORD hookThreadId = 0;
|
|
11
|
+
static std::atomic_flag listenerRunning = ATOMIC_FLAG_INIT;
|
|
12
|
+
static std::atomic<bool> listenerStopping = false;
|
|
13
|
+
static HANDLE hHookReady = NULL;
|
|
14
|
+
|
|
15
|
+
struct Position
|
|
16
|
+
{
|
|
17
|
+
int x;
|
|
18
|
+
int y;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
Position calcAbsolutePosition(int x, int y)
|
|
22
|
+
{
|
|
23
|
+
int vLeft = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
|
24
|
+
int vTop = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
|
25
|
+
int vWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
|
26
|
+
int vHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
|
27
|
+
if (vWidth <= 1 || vHeight <= 1)
|
|
28
|
+
return {0, 0}; // No virtual screen
|
|
29
|
+
int absoluteX = static_cast<int>((static_cast<long long>(x - vLeft) * 65535) / (vWidth - 1));
|
|
30
|
+
int absoluteY = static_cast<int>((static_cast<long long>(y - vTop) * 65535) / (vHeight - 1));
|
|
31
|
+
return {absoluteX, absoluteY};
|
|
32
|
+
}
|
|
33
|
+
|
|
4
34
|
/**
|
|
5
35
|
* Move the mouse to a specific point.
|
|
6
36
|
* @param point The coordinates to move the mouse to (x, y).
|
|
@@ -19,13 +49,9 @@ Napi::Value MoveMouse(const Napi::CallbackInfo &info)
|
|
|
19
49
|
int posX = info[0].As<Napi::Number>();
|
|
20
50
|
int posY = info[1].As<Napi::Number>();
|
|
21
51
|
|
|
22
|
-
|
|
23
|
-
int
|
|
24
|
-
int
|
|
25
|
-
|
|
26
|
-
// Convert coordinates to absolute values
|
|
27
|
-
int absoluteX = static_cast<int>((65536 * posX) / screenWidth);
|
|
28
|
-
int absoluteY = static_cast<int>((65536 * posY) / screenHeight);
|
|
52
|
+
Position pos = calcAbsolutePosition(posX, posY);
|
|
53
|
+
int absoluteX = pos.x;
|
|
54
|
+
int absoluteY = pos.y;
|
|
29
55
|
|
|
30
56
|
// Move the mouse
|
|
31
57
|
INPUT mouseInput = {0};
|
|
@@ -34,9 +60,9 @@ Napi::Value MoveMouse(const Napi::CallbackInfo &info)
|
|
|
34
60
|
mouseInput.mi.dy = absoluteY;
|
|
35
61
|
mouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
36
62
|
mouseInput.mi.time = 0; // System will provide the timestamp
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
UINT sent = SendInput(1, &mouseInput, sizeof(INPUT));
|
|
64
|
+
if (sent == 0)
|
|
65
|
+
return Napi::Boolean::New(env, false);
|
|
40
66
|
return Napi::Boolean::New(env, true);
|
|
41
67
|
}
|
|
42
68
|
|
|
@@ -50,19 +76,22 @@ Napi::Value ClickMouse(const Napi::CallbackInfo &info)
|
|
|
50
76
|
else
|
|
51
77
|
button = info[0].As<Napi::String>();
|
|
52
78
|
|
|
53
|
-
WORD
|
|
79
|
+
WORD downFlag = 0, upFlag = 0;
|
|
54
80
|
|
|
55
81
|
if (button == "left")
|
|
56
82
|
{
|
|
57
|
-
|
|
83
|
+
downFlag = MOUSEEVENTF_LEFTDOWN;
|
|
84
|
+
upFlag = MOUSEEVENTF_LEFTUP;
|
|
58
85
|
}
|
|
59
86
|
else if (button == "right")
|
|
60
87
|
{
|
|
61
|
-
|
|
88
|
+
downFlag = MOUSEEVENTF_RIGHTDOWN;
|
|
89
|
+
upFlag = MOUSEEVENTF_RIGHTUP;
|
|
62
90
|
}
|
|
63
91
|
else if (button == "middle")
|
|
64
92
|
{
|
|
65
|
-
|
|
93
|
+
downFlag = MOUSEEVENTF_MIDDLEDOWN;
|
|
94
|
+
upFlag = MOUSEEVENTF_MIDDLEUP;
|
|
66
95
|
}
|
|
67
96
|
else
|
|
68
97
|
{
|
|
@@ -71,13 +100,15 @@ Napi::Value ClickMouse(const Napi::CallbackInfo &info)
|
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
// Perform the mouse click
|
|
74
|
-
INPUT
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
INPUT inputs[2] = {};
|
|
104
|
+
inputs[0].type = INPUT_MOUSE;
|
|
105
|
+
inputs[0].mi.dwFlags = downFlag;
|
|
106
|
+
inputs[1].type = INPUT_MOUSE;
|
|
107
|
+
inputs[1].mi.dwFlags = upFlag;
|
|
108
|
+
|
|
109
|
+
UINT sent = SendInput(2, inputs, sizeof(INPUT));
|
|
110
|
+
if (sent == 0)
|
|
111
|
+
return Napi::Boolean::New(env, false);
|
|
81
112
|
return Napi::Boolean::New(env, true);
|
|
82
113
|
}
|
|
83
114
|
|
|
@@ -90,7 +121,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
90
121
|
Napi::TypeError::New(env, "You should provide startX, startY, endX, endY").ThrowAsJavaScriptException();
|
|
91
122
|
return env.Null();
|
|
92
123
|
}
|
|
93
|
-
|
|
124
|
+
bool success = true;
|
|
94
125
|
int startX = info[0].As<Napi::Number>();
|
|
95
126
|
int startY = info[1].As<Napi::Number>();
|
|
96
127
|
int endX = info[2].As<Napi::Number>();
|
|
@@ -100,23 +131,26 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
100
131
|
{
|
|
101
132
|
speed = info[4].As<Napi::Number>();
|
|
102
133
|
}
|
|
103
|
-
|
|
134
|
+
// Use pixel coords for timing
|
|
135
|
+
double pixelDistX = endX - startX;
|
|
136
|
+
double pixelDistY = endY - startY;
|
|
137
|
+
double pixelDistance = sqrt(pixelDistX * pixelDistX + pixelDistY * pixelDistY);
|
|
138
|
+
if (speed <= 0)
|
|
139
|
+
speed = 1; // guard div/0
|
|
140
|
+
double duration = pixelDistance / speed;
|
|
104
141
|
// Get the screen metrics
|
|
105
|
-
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
|
|
106
|
-
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
|
|
107
142
|
|
|
143
|
+
Position startPos = calcAbsolutePosition(startX, startY);
|
|
144
|
+
Position endPos = calcAbsolutePosition(endX, endY);
|
|
108
145
|
// Convert coordinates to absolute values
|
|
109
|
-
int absoluteStartX =
|
|
110
|
-
int absoluteStartY =
|
|
111
|
-
int absoluteEndX =
|
|
112
|
-
int absoluteEndY =
|
|
146
|
+
int absoluteStartX = startPos.x;
|
|
147
|
+
int absoluteStartY = startPos.y;
|
|
148
|
+
int absoluteEndX = endPos.x;
|
|
149
|
+
int absoluteEndY = endPos.y;
|
|
113
150
|
|
|
114
151
|
// Calculate the distance and duration based on speed
|
|
115
152
|
double distanceX = absoluteEndX - absoluteStartX;
|
|
116
153
|
double distanceY = absoluteEndY - absoluteStartY;
|
|
117
|
-
double distance = sqrt(distanceX * distanceX + distanceY * distanceY);
|
|
118
|
-
double duration = distance / speed;
|
|
119
|
-
|
|
120
154
|
// Move the mouse to the starting position
|
|
121
155
|
INPUT startMouseInput = {0};
|
|
122
156
|
startMouseInput.type = INPUT_MOUSE;
|
|
@@ -125,7 +159,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
125
159
|
startMouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
126
160
|
startMouseInput.mi.time = 0; // System will provide the timestamp
|
|
127
161
|
|
|
128
|
-
SendInput(1, &startMouseInput, sizeof(
|
|
162
|
+
if (SendInput(1, &startMouseInput, sizeof(INPUT)) == 0)
|
|
163
|
+
return Napi::Boolean::New(env, false);
|
|
164
|
+
;
|
|
129
165
|
|
|
130
166
|
// Perform mouse button down event
|
|
131
167
|
INPUT mouseDownInput = {0};
|
|
@@ -133,7 +169,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
133
169
|
mouseDownInput.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
|
134
170
|
mouseDownInput.mi.time = 0; // System will provide the timestamp
|
|
135
171
|
|
|
136
|
-
SendInput(1, &mouseDownInput, sizeof(
|
|
172
|
+
if (SendInput(1, &mouseDownInput, sizeof(INPUT)) == 0)
|
|
173
|
+
return Napi::Boolean::New(env, false);
|
|
174
|
+
;
|
|
137
175
|
|
|
138
176
|
// Calculate the number of steps based on the duration and desired speed
|
|
139
177
|
const int steps = 100; // Adjust the number of steps for smoother movement
|
|
@@ -143,7 +181,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
143
181
|
double stepY = distanceY / steps;
|
|
144
182
|
|
|
145
183
|
// Move the mouse in increments to simulate dragging with speed control
|
|
146
|
-
for (int i = 0; i
|
|
184
|
+
for (int i = 0; i <= steps; ++i)
|
|
147
185
|
{
|
|
148
186
|
// Calculate the position for the current step
|
|
149
187
|
int currentX = static_cast<int>(absoluteStartX + (stepX * i));
|
|
@@ -157,10 +195,15 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
157
195
|
mouseMoveInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
158
196
|
mouseMoveInput.mi.time = 0; // System will provide the timestamp
|
|
159
197
|
|
|
160
|
-
SendInput(1, &mouseMoveInput, sizeof(
|
|
198
|
+
if (SendInput(1, &mouseMoveInput, sizeof(INPUT)) == 0)
|
|
199
|
+
return Napi::Boolean::New(env, false);
|
|
200
|
+
;
|
|
161
201
|
|
|
162
202
|
// Sleep for a short duration to control the speed
|
|
163
|
-
|
|
203
|
+
if (i < steps)
|
|
204
|
+
{
|
|
205
|
+
Sleep(static_cast<DWORD>(duration / steps));
|
|
206
|
+
}
|
|
164
207
|
}
|
|
165
208
|
|
|
166
209
|
// Perform mouse button up event
|
|
@@ -169,7 +212,197 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
169
212
|
mouseUpInput.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
|
170
213
|
mouseUpInput.mi.time = 0; // System will provide the timestamp
|
|
171
214
|
|
|
172
|
-
SendInput(1, &mouseUpInput, sizeof(
|
|
215
|
+
if (SendInput(1, &mouseUpInput, sizeof(INPUT)) == 0)
|
|
216
|
+
return Napi::Boolean::New(env, false);
|
|
217
|
+
return Napi::Boolean::New(env, success);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
221
|
+
{
|
|
222
|
+
if (nCode >= 0 && !listenerStopping.load())
|
|
223
|
+
{
|
|
224
|
+
MSLLHOOKSTRUCT *mouse = (MSLLHOOKSTRUCT *)lParam;
|
|
225
|
+
|
|
226
|
+
int x = mouse->pt.x;
|
|
227
|
+
int y = mouse->pt.y;
|
|
228
|
+
|
|
229
|
+
std::string type = "move";
|
|
230
|
+
|
|
231
|
+
switch (wParam)
|
|
232
|
+
{
|
|
233
|
+
case WM_LBUTTONDOWN:
|
|
234
|
+
type = "leftDown";
|
|
235
|
+
break;
|
|
236
|
+
case WM_LBUTTONUP:
|
|
237
|
+
type = "leftUp";
|
|
238
|
+
break;
|
|
239
|
+
case WM_RBUTTONDOWN:
|
|
240
|
+
type = "rightDown";
|
|
241
|
+
break;
|
|
242
|
+
case WM_RBUTTONUP:
|
|
243
|
+
type = "rightUp";
|
|
244
|
+
break;
|
|
245
|
+
case WM_MBUTTONDOWN:
|
|
246
|
+
type = "middleDown";
|
|
247
|
+
break;
|
|
248
|
+
case WM_MBUTTONUP:
|
|
249
|
+
type = "middleUp";
|
|
250
|
+
break;
|
|
251
|
+
case WM_MOUSEMOVE:
|
|
252
|
+
type = "move";
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
auto callback = [x, y, type](Napi::Env env, Napi::Function jsCallback)
|
|
257
|
+
{
|
|
258
|
+
Napi::Object event = Napi::Object::New(env);
|
|
259
|
+
event.Set("x", x);
|
|
260
|
+
event.Set("y", y);
|
|
261
|
+
event.Set("type", type);
|
|
262
|
+
|
|
263
|
+
jsCallback.Call({event});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
napi_status status = tsfn.NonBlockingCall(callback);
|
|
267
|
+
if (status != napi_ok)
|
|
268
|
+
{
|
|
269
|
+
// tsfn is already closing
|
|
270
|
+
return CallNextHookEx(mouseHook, nCode, wParam, lParam);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return CallNextHookEx(mouseHook, nCode, wParam, lParam);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
DWORD WINAPI HookThread(LPVOID)
|
|
278
|
+
{
|
|
279
|
+
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);
|
|
280
|
+
SetEvent(hHookReady);
|
|
281
|
+
if (mouseHook == NULL)
|
|
282
|
+
return 1;
|
|
283
|
+
MSG msg;
|
|
284
|
+
while (GetMessage(&msg, NULL, 0, 0))
|
|
285
|
+
{
|
|
286
|
+
TranslateMessage(&msg);
|
|
287
|
+
DispatchMessage(&msg);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
UnhookWindowsHookEx(mouseHook);
|
|
291
|
+
mouseHook = NULL;
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
|
|
296
|
+
{
|
|
297
|
+
Napi::Env env = info.Env();
|
|
173
298
|
|
|
299
|
+
if (!info[0].IsFunction())
|
|
300
|
+
{
|
|
301
|
+
Napi::TypeError::New(env, "Callback expected").ThrowAsJavaScriptException();
|
|
302
|
+
return env.Null();
|
|
303
|
+
}
|
|
304
|
+
// Bug fix: prevent double-start leaking a thread and hook
|
|
305
|
+
if (listenerRunning.test_and_set())
|
|
306
|
+
{
|
|
307
|
+
Napi::TypeError::New(env, "Mouse listener already running").ThrowAsJavaScriptException();
|
|
308
|
+
return env.Null();
|
|
309
|
+
}
|
|
310
|
+
Napi::Function callback = info[0].As<Napi::Function>();
|
|
311
|
+
|
|
312
|
+
tsfn = Napi::ThreadSafeFunction::New(
|
|
313
|
+
env,
|
|
314
|
+
callback,
|
|
315
|
+
"MouseListener",
|
|
316
|
+
0,
|
|
317
|
+
1);
|
|
318
|
+
|
|
319
|
+
// Store handle and thread ID so StopMouseListener can signal and wait
|
|
320
|
+
hHookReady = CreateEvent(NULL, TRUE, FALSE, NULL);
|
|
321
|
+
if (hHookReady == NULL)
|
|
322
|
+
{
|
|
323
|
+
tsfn.Release();
|
|
324
|
+
listenerRunning.clear();
|
|
325
|
+
Napi::Error::New(env, "Failed to create sync event").ThrowAsJavaScriptException();
|
|
326
|
+
return env.Null();
|
|
327
|
+
}
|
|
328
|
+
hHookThread = CreateThread(NULL, 0, HookThread, NULL, 0, &hookThreadId);
|
|
329
|
+
if (hHookThread == NULL)
|
|
330
|
+
{
|
|
331
|
+
tsfn.Release();
|
|
332
|
+
CloseHandle(hHookReady);
|
|
333
|
+
hHookReady = NULL;
|
|
334
|
+
listenerRunning.clear(); // release the guard so caller can retry
|
|
335
|
+
Napi::Error::New(env, "Failed to create hook thread").ThrowAsJavaScriptException();
|
|
336
|
+
return env.Null();
|
|
337
|
+
}
|
|
338
|
+
WaitForSingleObject(hHookReady, 2000);
|
|
339
|
+
CloseHandle(hHookReady);
|
|
340
|
+
hHookReady = NULL;
|
|
341
|
+
DWORD exitCode = 0;
|
|
342
|
+
GetExitCodeThread(hHookThread, &exitCode);
|
|
343
|
+
if (exitCode == 1)
|
|
344
|
+
{
|
|
345
|
+
WaitForSingleObject(hHookThread, 1000);
|
|
346
|
+
CloseHandle(hHookThread);
|
|
347
|
+
hHookThread = NULL;
|
|
348
|
+
hookThreadId = 0;
|
|
349
|
+
tsfn.Release();
|
|
350
|
+
listenerRunning.clear();
|
|
351
|
+
Napi::Error::New(env, "Failed to install mouse hook").ThrowAsJavaScriptException();
|
|
352
|
+
return env.Null();
|
|
353
|
+
}
|
|
174
354
|
return Napi::Boolean::New(env, true);
|
|
175
355
|
}
|
|
356
|
+
|
|
357
|
+
Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
358
|
+
{
|
|
359
|
+
Napi::Env env = info.Env();
|
|
360
|
+
Napi::Object result = Napi::Object::New(env);
|
|
361
|
+
|
|
362
|
+
if (hHookThread == NULL)
|
|
363
|
+
{
|
|
364
|
+
result.Set("success", false);
|
|
365
|
+
result.Set("error", "No listener is running");
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
listenerStopping.store(true);
|
|
370
|
+
// PostThreadMessage with WM_QUIT breaks the GetMessage loop in HookThread,
|
|
371
|
+
// which then runs UnhookWindowsHookEx and exits
|
|
372
|
+
if (!PostThreadMessage(hookThreadId, WM_QUIT, 0, 0))
|
|
373
|
+
{
|
|
374
|
+
listenerStopping.store(false);
|
|
375
|
+
listenerRunning.clear();
|
|
376
|
+
result.Set("success", false);
|
|
377
|
+
result.Set("error", "Failed to signal hook thread");
|
|
378
|
+
result.Set("errorCode", Napi::Number::New(env, GetLastError()));
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Wait for the thread to fully exit before releasing resources
|
|
383
|
+
DWORD waitResult = WaitForSingleObject(hHookThread, 3000);
|
|
384
|
+
if (waitResult != WAIT_OBJECT_0)
|
|
385
|
+
{
|
|
386
|
+
// Thread didn't exit in time - kill it
|
|
387
|
+
TerminateThread(hHookThread, 1);
|
|
388
|
+
// Best-effort: reduce chance a concurrent NonBlockingCall is still
|
|
389
|
+
// in-flight before tsfn.Release() below. Not a hard guarantee.
|
|
390
|
+
Sleep(50);
|
|
391
|
+
result.Set("success", false);
|
|
392
|
+
result.Set("error", "Hook thread did not exit cleanly, was forcefully terminated");
|
|
393
|
+
}
|
|
394
|
+
else
|
|
395
|
+
{
|
|
396
|
+
result.Set("success", true);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Release the TSFN so Node.js can garbage collect the JS callback
|
|
400
|
+
tsfn.Release();
|
|
401
|
+
|
|
402
|
+
CloseHandle(hHookThread);
|
|
403
|
+
hHookThread = NULL;
|
|
404
|
+
hookThreadId = 0;
|
|
405
|
+
listenerRunning.clear();
|
|
406
|
+
listenerStopping.store(false);
|
|
407
|
+
return result;
|
|
408
|
+
}
|
package/src/index.mts
CHANGED
|
@@ -107,6 +107,8 @@ export type Imread = (path: string) => ImageData;
|
|
|
107
107
|
|
|
108
108
|
export type Imwrite = (image: ImageData) => Buffer;
|
|
109
109
|
|
|
110
|
+
export type StartMouseListener = (callback: (data: {x: number, y: number, type: string}) => void) => void;
|
|
111
|
+
export type StopMouseListener = () => {success: boolean, error?: string, errorCode?: number};
|
|
110
112
|
export type MatchTemplate = (
|
|
111
113
|
image: ImageData,
|
|
112
114
|
template: ImageData,
|
|
@@ -135,7 +137,7 @@ export type GetRegion = (image: ImageData, region: ROI) => ImageData;
|
|
|
135
137
|
|
|
136
138
|
export type TextRecognition = (trainedDataPath: string, dataLang: string, imagePath: string) => string;
|
|
137
139
|
export type CaptureScreenAsync = () => Promise<Buffer>;
|
|
138
|
-
|
|
140
|
+
export type BringWindowToFront = (windowName: string) => boolean;
|
|
139
141
|
const {
|
|
140
142
|
setKeyDownCallback,
|
|
141
143
|
setKeyUpCallback,
|
|
@@ -158,7 +160,11 @@ const {
|
|
|
158
160
|
getRegion,
|
|
159
161
|
textRecognition,
|
|
160
162
|
equalizeHist,
|
|
161
|
-
darkenColor
|
|
163
|
+
darkenColor,
|
|
164
|
+
bringWindowToFront,
|
|
165
|
+
startMouseListener,
|
|
166
|
+
stopMouseListener
|
|
167
|
+
|
|
162
168
|
}: {
|
|
163
169
|
setKeyDownCallback: SetKeyCallback;
|
|
164
170
|
setKeyUpCallback: SetKeyCallback;
|
|
@@ -182,6 +188,9 @@ const {
|
|
|
182
188
|
captureScreenAsync: CaptureScreenAsync;
|
|
183
189
|
equalizeHist: EqualizeHist;
|
|
184
190
|
darkenColor: DarkenColor;
|
|
191
|
+
bringWindowToFront: BringWindowToFront;
|
|
192
|
+
startMouseListener: StartMouseListener;
|
|
193
|
+
stopMouseListener: StopMouseListener
|
|
185
194
|
} = bindings;
|
|
186
195
|
|
|
187
196
|
const rawPressKey = pressKey;
|
|
@@ -525,6 +534,7 @@ function keyPress(keyCode: number, repeat?: number): Promise<boolean> {
|
|
|
525
534
|
|
|
526
535
|
export {
|
|
527
536
|
getWindowData,
|
|
537
|
+
bringWindowToFront,
|
|
528
538
|
captureWindow,
|
|
529
539
|
captureWindowN,
|
|
530
540
|
mouseMove,
|
|
@@ -537,6 +547,8 @@ export {
|
|
|
537
547
|
textRecognition,
|
|
538
548
|
captureScreenToFile,
|
|
539
549
|
captureScreenAsync,
|
|
550
|
+
startMouseListener,
|
|
551
|
+
stopMouseListener,
|
|
540
552
|
KeyboardListener,
|
|
541
553
|
OpenCV
|
|
542
554
|
};
|