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/binding.gyp CHANGED
@@ -17,6 +17,7 @@
17
17
  "libraries": [
18
18
  "dwmapi.lib",
19
19
  "windowsapp.lib",
20
+ "Shcore.lib",
20
21
  "-lgdi32",
21
22
  "-lgdiplus",
22
23
  "<!(node -p \"require('path').resolve('libs/libjpeg-turbo.lib')\")",
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
- declare const getWindowData: GetWindowData, captureWindowN: CaptureWindow, captureScreenAsync: CaptureScreenAsync, mouseMove: MouseMove, mouseClick: MouseClick, mouseDrag: MouseDrag, typeString: TypeString, textRecognition: TextRecognition;
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
- Math.round((Hue / 360) * 179), // Scale H from [0, 360] → [0, 179]
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.1.5",
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.3.1",
47
- "node-gyp": "^11.1.0",
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": "^20.2.5",
52
- "node-api-headers": "^1.5.0",
53
- "tap": "^21.1.0"
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
  }
@@ -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
- // Get the screen metrics
23
- int screenWidth = GetSystemMetrics(SM_CXSCREEN);
24
- int screenHeight = GetSystemMetrics(SM_CYSCREEN);
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
- SendInput(1, &mouseInput, sizeof(mouseInput));
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 mouseEvent = 0;
79
+ WORD downFlag = 0, upFlag = 0;
54
80
 
55
81
  if (button == "left")
56
82
  {
57
- mouseEvent = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
83
+ downFlag = MOUSEEVENTF_LEFTDOWN;
84
+ upFlag = MOUSEEVENTF_LEFTUP;
58
85
  }
59
86
  else if (button == "right")
60
87
  {
61
- mouseEvent = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP;
88
+ downFlag = MOUSEEVENTF_RIGHTDOWN;
89
+ upFlag = MOUSEEVENTF_RIGHTUP;
62
90
  }
63
91
  else if (button == "middle")
64
92
  {
65
- mouseEvent = MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP;
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 mouseInput = {0};
75
- mouseInput.type = INPUT_MOUSE;
76
- mouseInput.mi.dwFlags = mouseEvent;
77
- mouseInput.mi.time = 0; // System will provide the timestamp
78
-
79
- SendInput(1, &mouseInput, sizeof(mouseInput));
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 = static_cast<int>((65536 * startX) / screenWidth);
110
- int absoluteStartY = static_cast<int>((65536 * startY) / screenHeight);
111
- int absoluteEndX = static_cast<int>((65536 * endX) / screenWidth);
112
- int absoluteEndY = static_cast<int>((65536 * endY) / screenHeight);
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(startMouseInput));
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(mouseDownInput));
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 < steps; ++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(mouseMoveInput));
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
- Sleep(static_cast<DWORD>(duration / steps));
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(mouseUpInput));
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
  };