node-native-win-utils 2.2.0 → 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 -622
- package/binding.gyp +1 -0
- package/dist/index.d.mts +2 -2
- package/package.json +2 -2
- package/prebuilds/win32-x64/node-native-win-utils.node +0 -0
- package/src/cpp/main.cpp +2 -2
- package/src/cpp/mouse.cpp +132 -51
package/README.md
CHANGED
|
@@ -1,684 +1,175 @@
|
|
|
1
|
-
|
|
2
|
-
[![
|
|
3
|
-
![
|
|
1
|
+
[![License][license-src]][license-href]
|
|
2
|
+
[](https://nodejs.org/api/n-api.html)
|
|
3
|
+
[](https://www.npmjs.com/package/node-native-win-utils)
|
|
4
|
+
[](https://www.npmjs.com/package/node-native-win-utils)
|
|
5
|
+
[](https://github.com/T-Rumibul/node-native-win-utils)
|
|
4
6
|
|
|
5
7
|
# Node Native Win Utils
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
9
|
I did it for myself because I didn't feel like dealing with libraries like 'node-ffi' to implement this functionality. Maybe someone will find it useful. It's WINDOWS OS ONLY
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
This package is a native addon for Node.js that allows you to perform various utility operations on Windows systems. It includes key event listeners, window data retrieval, window screenshot capture functionality, mouse movement, mouse click, mouse drag, and typing functionality, also I included precompiled libs of OpenCV(core, imgcodecs, imgproc)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Installation
|
|
17
|
-
|
|
18
|
-
|
|
19
11
|
|
|
20
|
-
|
|
12
|
+
## Features
|
|
21
13
|
|
|
22
|
-
|
|
14
|
+
- Global keyboard event listener (`keyDown` / `keyUp`)
|
|
15
|
+
- Window information & screenshots
|
|
16
|
+
- Mouse movement, clicks, drag & drop
|
|
17
|
+
- Keyboard emulation (`keyPress`, `typeString`)
|
|
18
|
+
- OpenCV integration (template matching, blur, grayscale, histogram equalization, color manipulation, ROI, drawing)
|
|
19
|
+
- Tesseract OCR text recognition
|
|
20
|
+
- Screen capture
|
|
21
|
+
- Mouse event listener
|
|
22
|
+
- Prebuilt binaries (no Python or build tools required on Windows)
|
|
23
|
+
- ESM + CommonJS support
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
## Requirements
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
- **Windows 10 or later** (x64)
|
|
28
|
+
- Node.js >= 18 (prebuilts for recent versions via `prebuildify`)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
## Installation
|
|
31
31
|
|
|
32
|
+
```bash
|
|
33
|
+
npm install node-native-win-utils
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Usage
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Importing the Package
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
To use the package, import the necessary functions, types, and classes:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```javascript
|
|
36
|
+
## Usage
|
|
49
37
|
|
|
38
|
+
```ts
|
|
50
39
|
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
getWindowData,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
OpenCV,
|
|
69
|
-
|
|
70
|
-
} from "node-native-win-utils";
|
|
71
|
-
|
|
40
|
+
KeyboardListener,
|
|
41
|
+
KeyCodeHelper,
|
|
42
|
+
KeyListener,
|
|
43
|
+
getWindowData,
|
|
44
|
+
captureWindow,
|
|
45
|
+
captureWindowN,
|
|
46
|
+
captureScreenToFile,
|
|
47
|
+
mouseMove,
|
|
48
|
+
mouseClick,
|
|
49
|
+
mouseDrag,
|
|
50
|
+
typeString,
|
|
51
|
+
keyPress,
|
|
52
|
+
textRecognition,
|
|
53
|
+
OpenCV,
|
|
54
|
+
startMouseListener,
|
|
55
|
+
stopMouseListener,
|
|
56
|
+
} from "node-native-win-utils";
|
|
72
57
|
```
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## Keyboard Event Listener
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
The package provides KeyboardListener singletone class, which allow you to register event listeners for key down and key up events, respectively. The event callbacks receive the `keyCode` as a parameter:
|
|
59
|
+
### Keyboard
|
|
81
60
|
|
|
82
|
-
|
|
61
|
+
```ts
|
|
62
|
+
// Singleton listener
|
|
63
|
+
const listener = KeyboardListener.listener();
|
|
64
|
+
listener.on("keyDown", (data) => console.log("Down:", data.keyName));
|
|
65
|
+
listener.on("keyUp", (data) => console.log("Up:", data.keyName));
|
|
83
66
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
listener.on("keyDown", (data: {
|
|
89
|
-
keyCode: number;
|
|
90
|
-
keyName: string;
|
|
91
|
-
}) => {
|
|
92
|
-
//your code
|
|
93
|
-
})
|
|
94
|
-
listener.on("keyUp", (data: {
|
|
95
|
-
keyCode: number;
|
|
96
|
-
keyName: string;
|
|
97
|
-
}) => {
|
|
98
|
-
//your code
|
|
99
|
-
})
|
|
67
|
+
// Simulate key press
|
|
68
|
+
keyPress(KeyCodeHelper.A); // once
|
|
69
|
+
keyPress(KeyCodeHelper.Enter, 2); // twice
|
|
100
70
|
```
|
|
101
71
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
The keyPress function allows you to simulate a key press event. Provide the keyCode as a parameter and optionally the number of times to press the key:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```javascript
|
|
72
|
+
### Mouse & Typing
|
|
111
73
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
74
|
+
```ts
|
|
75
|
+
mouseMove(500, 300);
|
|
76
|
+
mouseClick(); // left click
|
|
77
|
+
mouseClick("right");
|
|
78
|
+
mouseDrag(100, 100, 800, 600, 50); // optional speed
|
|
117
79
|
|
|
80
|
+
typeString("Hello from Node!", 30); // 30ms delay per char
|
|
118
81
|
```
|
|
119
82
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
The KeyCodeHelper enum provides a set of key codes that can be used with key event functions:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```javascript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
import { KeyCodeHelper } from "node-native-win-utils";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.log(KeyCodeHelper.A); // Outputs the key code for 'A'
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
## Window Data
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
The `getWindowData` function retrieves information about a specific window identified by its name. It returns an object with properties `width`, `height`, `x`, and `y`, representing the window dimensions and position:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```javascript
|
|
151
|
-
|
|
152
|
-
const windowData = getWindowData("Window Name");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log("Window data:", windowData);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Window data: { width: 800, height: 600, x: 50, y: 50 }
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
## Window Capture
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
The `captureWindow` function allows you to capture a screenshot of a specific window identified by its name. Provide the window name and the output path as parameters:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```javascript
|
|
175
|
-
|
|
176
|
-
captureWindow("Window Name", "output.png");
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Output: output.png with a screenshot of the window
|
|
181
|
-
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
## Mouse Movement
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
The `mouseMove` function allows you to move the mouse to a specific position on the screen. Provide the `posX` and `posY` coordinates as parameters:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
```javascript
|
|
83
|
+
### Window Operations
|
|
195
84
|
|
|
196
|
-
|
|
85
|
+
```ts
|
|
86
|
+
const data = getWindowData("Notepad");
|
|
87
|
+
console.log(data); // { width, height, x, y }
|
|
197
88
|
|
|
89
|
+
captureWindow("Notepad", "screenshot.png");
|
|
90
|
+
const buffer = captureWindowN("Notepad"); // Buffer
|
|
198
91
|
```
|
|
199
92
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
## Mouse Click
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
The `mouseClick` function allows you to perform a mouse click event. Optionally, you can specify the mouse button as a parameter (`"left"`, `"middle"`, or `"right"`). If no button is specified, a left mouse click is performed by default:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
```javascript
|
|
211
|
-
|
|
212
|
-
mouseClick(); // Left mouse click
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
mouseClick("right"); // Right mouse click
|
|
93
|
+
### Screen Capture
|
|
217
94
|
|
|
95
|
+
```ts
|
|
96
|
+
await captureScreenToFile("full-screen.png");
|
|
218
97
|
```
|
|
219
98
|
|
|
220
|
-
|
|
99
|
+
### Text Recognition (Tesseract OCR)
|
|
221
100
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
The `mouseDrag` function allows you to simulate dragging the mouse from one position to another. Provide the starting and ending coordinates (`startX`, `startY`, `endX`, `endY`) as parameters. Optionally, you can specify the speed at which the mouse should be dragged:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
```javascript
|
|
231
|
-
|
|
232
|
-
mouseDrag(100, 200, 300, 400);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
mouseDrag(100, 200, 300, 400, 100); // Drag with speed 100
|
|
101
|
+
```ts
|
|
102
|
+
import path from "path";
|
|
237
103
|
|
|
104
|
+
const text = textRecognition(
|
|
105
|
+
path.join(__dirname, "traineddata"), // path to .traineddata files
|
|
106
|
+
"eng",
|
|
107
|
+
path.join(__dirname, "image.png")
|
|
108
|
+
);
|
|
109
|
+
console.log(text);
|
|
238
110
|
```
|
|
239
111
|
|
|
240
|
-
|
|
112
|
+
### OpenCV (image processing)
|
|
241
113
|
|
|
242
|
-
|
|
114
|
+
```ts
|
|
115
|
+
import { OpenCV } from "node-native-win-utils";
|
|
243
116
|
|
|
244
|
-
|
|
117
|
+
const img = new OpenCV("image.png"); // or ImageData { width, height, data: Uint8Array }
|
|
245
118
|
|
|
246
|
-
|
|
119
|
+
// Chainable methods
|
|
120
|
+
const processed = img
|
|
121
|
+
.blur(5, 5)
|
|
122
|
+
.bgrToGray()
|
|
123
|
+
.equalizeHist()
|
|
124
|
+
.darkenColor([240, 240, 240], [255, 255, 255], 0.5) // lower, upper, factor
|
|
125
|
+
.drawRectangle([10, 10], [200, 100], [255, 0, 0], 3)
|
|
126
|
+
.getRegion([50, 50, 300, 200]);
|
|
247
127
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
a delay between each character (in milliseconds) using the `delay` parameter:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
```javascript
|
|
255
|
-
|
|
256
|
-
typeString("Hello, world!");
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
typeString("Hello, world!", 100); // Type with a delay of 100ms between characters
|
|
128
|
+
processed.imwrite("processed.png");
|
|
261
129
|
|
|
130
|
+
// Template matching
|
|
131
|
+
const match = img.matchTemplate(templateImage, /* method */, /* mask */);
|
|
132
|
+
console.log(match); // { minValue, maxValue, minLocation, maxLocation }
|
|
262
133
|
```
|
|
263
134
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
## Key Listener Class
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
The `KeyListener` class extends the EventEmitter class and simplifies working with the `keyDownHandler` and `keyUpHandler` functions. You can register event listeners for the "keyDown" and "keyUp" events using the `on` method:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
```javascript
|
|
275
|
-
|
|
276
|
-
const listener = new KeyListener();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
listener.on("keyDown", (data) => {
|
|
281
|
-
|
|
282
|
-
console.log("Key down:", data.keyCode, data.keyName);
|
|
135
|
+
### Mouse Event Listener
|
|
283
136
|
|
|
137
|
+
```ts
|
|
138
|
+
startMouseListener(({ x, y, type }) => {
|
|
139
|
+
console.log(`${type} at ${x},${y}`); // move at 400 300
|
|
284
140
|
});
|
|
285
141
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// Key down: 8 Backspace
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
listener.on("keyUp", (data) => {
|
|
293
|
-
|
|
294
|
-
console.log("Key up:", data.keyCode, data.keyName);
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// Key up: 8 Backspace
|
|
301
|
-
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
## OpenCV
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
The `OpenCV` class extends the capabilities of the native addon package by providing various image processing functionalities. It allows users to perform operations such as matching templates, blurring images, converting color formats, drawing rectangles, getting image regions, and writing images to files.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
#### Constructor
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
|
|
320
|
-
const image = new OpenCV(image: string | ImageData)
|
|
142
|
+
mouseClick("left"); // trigger event
|
|
321
143
|
|
|
144
|
+
// terminates a thread
|
|
145
|
+
stopMouseListener();
|
|
322
146
|
```
|
|
323
147
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
Creates a new instance of the `OpenCV` class with the specified image data. The `image` parameter can be either a file path (string) or an existing `ImageData` object.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
#### Properties
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
##### `imageData: ImageData`
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Holds the underlying image data that will be used for image processing operations.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
##### `width: number`
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
Read-only property that returns the width of the image in pixels.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
##### `height: number`
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
Read-only property that returns the height of the image in pixels.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
#### Methods
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
##### `matchTemplate(template: ImageData, method?: number | null, mask?: ImageData): OpenCV`
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
Matches a template image with the current image and returns a new `OpenCV` instance containing the result.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
- `template: ImageData`: The template image data to be matched.
|
|
371
|
-
|
|
372
|
-
- `method?: number | null`: (Optional) The matching method to be used. If not provided, the default method will be used.(currently no implemented)
|
|
373
|
-
|
|
374
|
-
- `mask?: ImageData`: (Optional) An optional mask image data to be used during the matching process.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
##### `blur(sizeX: number, sizeY: number): OpenCV`
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
Applies a blur filter to the current image and returns a new `OpenCV` instance containing the blurred result.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
- `sizeX: number`: The size of the blur kernel in the X direction.
|
|
387
|
-
|
|
388
|
-
- `sizeY: number`: The size of the blur kernel in the Y direction.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
##### `bgrToGray(): OpenCV`
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
Converts the current image from the BGR color format to grayscale and returns a new `OpenCV` instance containing the grayscale result.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
##### `drawRectangle(start: Point, end: Point, rgb: Color, thickness: number): OpenCV`
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
Draws a rectangle on the current image and returns a new `OpenCV` instance containing the modified result.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
- `start: Point`: The starting point (top-left) of the rectangle.
|
|
409
|
-
|
|
410
|
-
- `end: Point`: The ending point (bottom-right) of the rectangle.
|
|
411
|
-
|
|
412
|
-
- `rgb: Color`: The color of the rectangle in the RGB format (e.g., `{ r: 255, g: 0, b: 0 }` for red).
|
|
413
|
-
|
|
414
|
-
- `thickness: number`: The thickness of the rectangle's border.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
##### `getRegion(region: ROI): OpenCV`
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
Extracts a region of interest (ROI) from the current image and returns a new `OpenCV` instance containing the extracted region.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
- `region: ROI`: An object specifying the region of interest with properties `x`, `y`, `width`, `height`.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
##### `imwrite(path: string): void`
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
Writes the current image to a file specified by the `path`.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
- `path: string`: The file path where the image will be saved.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
## Functions
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
| Function | Parameters | Return Type |
|
|
447
|
-
|-----------------|----------------------------------------------------------------------------------------------|-------------|
|
|
448
|
-
| getWindowData | `windowName: string` | `WindowData`|
|
|
449
|
-
| captureWindow | `windowName: string, outputPath: string` | `void` |
|
|
450
|
-
| mouseMove | `posX: number, posY: number` | `boolean` |
|
|
451
|
-
| mouseClick | `button?: "left" \| "middle" \| "right"` | `boolean` |
|
|
452
|
-
| mouseDrag | `startX: number, startY: number, endX: number, endY: number, speed?: number` | `boolean` |
|
|
453
|
-
| typeString | `stringToType: string, delay?: number` | `boolean` |
|
|
454
|
-
| captureWindowN | `windowName: string` | `Buffer` |
|
|
455
|
-
| keyPress | `keyCode: number, repeat?: number` | `boolean` |
|
|
456
|
-
| textRecognition | `trainedDataPath: string, dataLang: string, imagePath: string` | `string` |
|
|
457
|
-
| captureScreenToFile | `path: string` | `boolean` |
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
## Examples
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
Here are some examples of using the package:
|
|
465
|
-
|
|
466
|
-
```javascript
|
|
467
|
-
console.log(textRecognition(path.join(__dirname, 'traineddata'), 'eng', path.join(__dirname, 'images', '1.jpg'))) // ---> <recognized text>
|
|
148
|
+
## Building from source
|
|
468
149
|
|
|
150
|
+
```bash
|
|
151
|
+
npm install
|
|
152
|
+
npm run build
|
|
469
153
|
```
|
|
470
154
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// Example usage of the OpenCV class
|
|
474
|
-
|
|
475
|
-
import { OpenCV } from "node-native-win-utils";
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const image = new OpenCV("path/to/image.png");
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const template = new OpenCV("path/to/template.png");
|
|
484
|
-
|
|
485
|
-
const matchedImage = image.matchTemplate(template.imageData);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const blurredImage = image.blur(5, 5);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const grayscaleImage = image.bgrToGray();
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const regionOfInterest = { x: 100, y: 100, width: 200, height: 150 };
|
|
498
|
-
|
|
499
|
-
const regionImage = image.getRegion(regionOfInterest);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const redColor = { r: 255, g: 0, b: 0 };
|
|
504
|
-
|
|
505
|
-
const thickRectangle = image.drawRectangle(
|
|
506
|
-
|
|
507
|
-
{ x: 50, y: 50 },
|
|
508
|
-
|
|
509
|
-
{ x: 150, y: 150 },
|
|
510
|
-
|
|
511
|
-
redColor,
|
|
512
|
-
|
|
513
|
-
3
|
|
514
|
-
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
matchedImage.imwrite("output/matched.png");
|
|
520
|
-
|
|
521
|
-
blurredImage.imwrite("output/blurred.png");
|
|
522
|
-
|
|
523
|
-
grayscaleImage.imwrite("output/grayscale.png");
|
|
524
|
-
|
|
525
|
-
regionImage.imwrite("output/region.png");
|
|
526
|
-
|
|
527
|
-
thickRectangle.imwrite("output/thick_rectangle.png");
|
|
528
|
-
|
|
155
|
+
#### Requires:
|
|
529
156
|
```
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
```javascript
|
|
534
|
-
|
|
535
|
-
// If you want to aply blur and convert to gray then do it that order:
|
|
536
|
-
|
|
537
|
-
image.blur(5, 5).bgrToGray();
|
|
538
|
-
|
|
539
|
-
// Otherwise you will get an error.
|
|
540
|
-
|
|
157
|
+
Visual Studio Build Tools (C++).
|
|
158
|
+
Python 3
|
|
541
159
|
```
|
|
542
160
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
Please note that the above example demonstrates the usage of different methods available in the `OpenCV` class. Make sure to replace `"path/to/image.png"` and `"path/to/template.png"` with actual image file paths.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
```javascript
|
|
550
|
-
|
|
551
|
-
import {
|
|
552
|
-
|
|
553
|
-
getWindowData,
|
|
554
|
-
|
|
555
|
-
captureWindow,
|
|
556
|
-
|
|
557
|
-
mouseMove,
|
|
558
|
-
|
|
559
|
-
mouseClick,
|
|
560
|
-
|
|
561
|
-
mouseDrag,
|
|
562
|
-
|
|
563
|
-
typeString,
|
|
564
|
-
|
|
565
|
-
KeyListener,
|
|
566
|
-
|
|
567
|
-
} from "node-native-win-utils";
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// Retrieve window data
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const windowData = getWindowData("My Window");
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
console.log("Window data:", windowData);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// Window data: { width: 1024, height: 768, x: 100, y: 100 }
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// Capture window screenshot
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
captureWindow("My Window", "output.png");
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
// Output: output.png with a screenshot of the window
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
// Move the mouse
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
mouseMove(100, 200);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
// Perform mouse click
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
mouseClick(); // Left mouse click
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
mouseClick("right"); // Right mouse click
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
// Simulate mouse drag
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
mouseDrag(100, 200, 300, 400);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
mouseDrag(100, 200, 300, 400, 100); // Drag with speed 100
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// Simulate typing
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
typeString("Hello, world!");
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
typeString("Hello, world!", 100); // Type with a delay of 100ms between characters
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
// Use KeyboardListener class
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const listener = KeyboardListener.listener();
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
listener.on("keyDown", (data) => {
|
|
652
|
-
|
|
653
|
-
console.log("Key down:", data.keyCode, data.keyName);
|
|
654
|
-
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
// Key down: keyCode keyName
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
listener.on("keyUp", (data) => {
|
|
664
|
-
|
|
665
|
-
console.log("Key up:", data.keyCode, data.keyName);
|
|
666
|
-
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// Key up: keyCode keyName
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
```
|
|
161
|
+
## License
|
|
162
|
+
--
|
|
677
163
|
|
|
678
164
|
[OpenCV License](https://github.com/opencv/opencv/blob/master/LICENSE)
|
|
165
|
+
|
|
679
166
|
[MIT License](https://github.com/T-Rumibul/node-native-win-utils/blob/main/LICENSE)
|
|
680
167
|
|
|
681
168
|
|
|
682
|
-
[license-src]: https://img.shields.io/
|
|
169
|
+
[license-src]: https://img.shields.io/badge/License-MIT-yellow.svg
|
|
683
170
|
|
|
684
171
|
[license-href]: https://github.com/T-Rumibul/node-native-win-utils/blob/main/LICENSE
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
**Made with ❤️ for Windows automation.**
|
|
175
|
+
Issues / PRs welcome!
|
package/binding.gyp
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -80,11 +80,11 @@ export type StartMouseListener = (callback: (data: {
|
|
|
80
80
|
y: number;
|
|
81
81
|
type: string;
|
|
82
82
|
}) => void) => void;
|
|
83
|
-
export type StopMouseListener = (
|
|
83
|
+
export type StopMouseListener = () => {
|
|
84
84
|
success: boolean;
|
|
85
85
|
error?: string;
|
|
86
86
|
errorCode?: number;
|
|
87
|
-
}
|
|
87
|
+
};
|
|
88
88
|
export type MatchTemplate = (image: ImageData, template: ImageData, method?: number | null, mask?: ImageData) => MatchData;
|
|
89
89
|
export type Blur = (image: ImageData, sizeX: number, sizeY: number) => ImageData;
|
|
90
90
|
export type BgrToGray = (image: ImageData) => ImageData;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-native-win-utils",
|
|
3
|
-
"version": "2.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,7 +40,7 @@
|
|
|
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
46
|
"node-addon-api": "^8.6.0",
|
|
Binary file
|
package/src/cpp/main.cpp
CHANGED
|
@@ -6,10 +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
|
-
|
|
12
|
+
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
|
|
13
13
|
exports.Set("getWindowData", Napi::Function::New(env, GetWindowData));
|
|
14
14
|
exports.Set("captureWindowN", Napi::Function::New(env, CaptureWindow));
|
|
15
15
|
exports.Set("captureScreenAsync", Napi::Function::New(env, CaptureScreenAsync));
|
package/src/cpp/mouse.cpp
CHANGED
|
@@ -1,11 +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>
|
|
4
6
|
|
|
5
7
|
HHOOK mouseHook;
|
|
6
8
|
Napi::ThreadSafeFunction tsfn;
|
|
7
9
|
static HANDLE hHookThread = NULL;
|
|
8
|
-
static DWORD
|
|
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
|
+
|
|
9
34
|
/**
|
|
10
35
|
* Move the mouse to a specific point.
|
|
11
36
|
* @param point The coordinates to move the mouse to (x, y).
|
|
@@ -24,13 +49,9 @@ Napi::Value MoveMouse(const Napi::CallbackInfo &info)
|
|
|
24
49
|
int posX = info[0].As<Napi::Number>();
|
|
25
50
|
int posY = info[1].As<Napi::Number>();
|
|
26
51
|
|
|
27
|
-
|
|
28
|
-
int
|
|
29
|
-
int
|
|
30
|
-
|
|
31
|
-
// Convert coordinates to absolute values
|
|
32
|
-
int absoluteX = static_cast<int>((65536 * posX) / screenWidth);
|
|
33
|
-
int absoluteY = static_cast<int>((65536 * posY) / screenHeight);
|
|
52
|
+
Position pos = calcAbsolutePosition(posX, posY);
|
|
53
|
+
int absoluteX = pos.x;
|
|
54
|
+
int absoluteY = pos.y;
|
|
34
55
|
|
|
35
56
|
// Move the mouse
|
|
36
57
|
INPUT mouseInput = {0};
|
|
@@ -39,9 +60,9 @@ Napi::Value MoveMouse(const Napi::CallbackInfo &info)
|
|
|
39
60
|
mouseInput.mi.dy = absoluteY;
|
|
40
61
|
mouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
41
62
|
mouseInput.mi.time = 0; // System will provide the timestamp
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
UINT sent = SendInput(1, &mouseInput, sizeof(INPUT));
|
|
64
|
+
if (sent == 0)
|
|
65
|
+
return Napi::Boolean::New(env, false);
|
|
45
66
|
return Napi::Boolean::New(env, true);
|
|
46
67
|
}
|
|
47
68
|
|
|
@@ -55,19 +76,22 @@ Napi::Value ClickMouse(const Napi::CallbackInfo &info)
|
|
|
55
76
|
else
|
|
56
77
|
button = info[0].As<Napi::String>();
|
|
57
78
|
|
|
58
|
-
WORD
|
|
79
|
+
WORD downFlag = 0, upFlag = 0;
|
|
59
80
|
|
|
60
81
|
if (button == "left")
|
|
61
82
|
{
|
|
62
|
-
|
|
83
|
+
downFlag = MOUSEEVENTF_LEFTDOWN;
|
|
84
|
+
upFlag = MOUSEEVENTF_LEFTUP;
|
|
63
85
|
}
|
|
64
86
|
else if (button == "right")
|
|
65
87
|
{
|
|
66
|
-
|
|
88
|
+
downFlag = MOUSEEVENTF_RIGHTDOWN;
|
|
89
|
+
upFlag = MOUSEEVENTF_RIGHTUP;
|
|
67
90
|
}
|
|
68
91
|
else if (button == "middle")
|
|
69
92
|
{
|
|
70
|
-
|
|
93
|
+
downFlag = MOUSEEVENTF_MIDDLEDOWN;
|
|
94
|
+
upFlag = MOUSEEVENTF_MIDDLEUP;
|
|
71
95
|
}
|
|
72
96
|
else
|
|
73
97
|
{
|
|
@@ -76,13 +100,15 @@ Napi::Value ClickMouse(const Napi::CallbackInfo &info)
|
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
// Perform the mouse click
|
|
79
|
-
INPUT
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
86
112
|
return Napi::Boolean::New(env, true);
|
|
87
113
|
}
|
|
88
114
|
|
|
@@ -95,7 +121,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
95
121
|
Napi::TypeError::New(env, "You should provide startX, startY, endX, endY").ThrowAsJavaScriptException();
|
|
96
122
|
return env.Null();
|
|
97
123
|
}
|
|
98
|
-
|
|
124
|
+
bool success = true;
|
|
99
125
|
int startX = info[0].As<Napi::Number>();
|
|
100
126
|
int startY = info[1].As<Napi::Number>();
|
|
101
127
|
int endX = info[2].As<Napi::Number>();
|
|
@@ -105,23 +131,26 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
105
131
|
{
|
|
106
132
|
speed = info[4].As<Napi::Number>();
|
|
107
133
|
}
|
|
108
|
-
|
|
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;
|
|
109
141
|
// Get the screen metrics
|
|
110
|
-
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
|
|
111
|
-
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
|
|
112
142
|
|
|
143
|
+
Position startPos = calcAbsolutePosition(startX, startY);
|
|
144
|
+
Position endPos = calcAbsolutePosition(endX, endY);
|
|
113
145
|
// Convert coordinates to absolute values
|
|
114
|
-
int absoluteStartX =
|
|
115
|
-
int absoluteStartY =
|
|
116
|
-
int absoluteEndX =
|
|
117
|
-
int absoluteEndY =
|
|
146
|
+
int absoluteStartX = startPos.x;
|
|
147
|
+
int absoluteStartY = startPos.y;
|
|
148
|
+
int absoluteEndX = endPos.x;
|
|
149
|
+
int absoluteEndY = endPos.y;
|
|
118
150
|
|
|
119
151
|
// Calculate the distance and duration based on speed
|
|
120
152
|
double distanceX = absoluteEndX - absoluteStartX;
|
|
121
153
|
double distanceY = absoluteEndY - absoluteStartY;
|
|
122
|
-
double distance = sqrt(distanceX * distanceX + distanceY * distanceY);
|
|
123
|
-
double duration = distance / speed;
|
|
124
|
-
|
|
125
154
|
// Move the mouse to the starting position
|
|
126
155
|
INPUT startMouseInput = {0};
|
|
127
156
|
startMouseInput.type = INPUT_MOUSE;
|
|
@@ -130,7 +159,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
130
159
|
startMouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
131
160
|
startMouseInput.mi.time = 0; // System will provide the timestamp
|
|
132
161
|
|
|
133
|
-
SendInput(1, &startMouseInput, sizeof(
|
|
162
|
+
if (SendInput(1, &startMouseInput, sizeof(INPUT)) == 0)
|
|
163
|
+
return Napi::Boolean::New(env, false);
|
|
164
|
+
;
|
|
134
165
|
|
|
135
166
|
// Perform mouse button down event
|
|
136
167
|
INPUT mouseDownInput = {0};
|
|
@@ -138,7 +169,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
138
169
|
mouseDownInput.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
|
139
170
|
mouseDownInput.mi.time = 0; // System will provide the timestamp
|
|
140
171
|
|
|
141
|
-
SendInput(1, &mouseDownInput, sizeof(
|
|
172
|
+
if (SendInput(1, &mouseDownInput, sizeof(INPUT)) == 0)
|
|
173
|
+
return Napi::Boolean::New(env, false);
|
|
174
|
+
;
|
|
142
175
|
|
|
143
176
|
// Calculate the number of steps based on the duration and desired speed
|
|
144
177
|
const int steps = 100; // Adjust the number of steps for smoother movement
|
|
@@ -148,7 +181,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
148
181
|
double stepY = distanceY / steps;
|
|
149
182
|
|
|
150
183
|
// Move the mouse in increments to simulate dragging with speed control
|
|
151
|
-
for (int i = 0; i
|
|
184
|
+
for (int i = 0; i <= steps; ++i)
|
|
152
185
|
{
|
|
153
186
|
// Calculate the position for the current step
|
|
154
187
|
int currentX = static_cast<int>(absoluteStartX + (stepX * i));
|
|
@@ -162,10 +195,15 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
162
195
|
mouseMoveInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
163
196
|
mouseMoveInput.mi.time = 0; // System will provide the timestamp
|
|
164
197
|
|
|
165
|
-
SendInput(1, &mouseMoveInput, sizeof(
|
|
198
|
+
if (SendInput(1, &mouseMoveInput, sizeof(INPUT)) == 0)
|
|
199
|
+
return Napi::Boolean::New(env, false);
|
|
200
|
+
;
|
|
166
201
|
|
|
167
202
|
// Sleep for a short duration to control the speed
|
|
168
|
-
|
|
203
|
+
if (i < steps)
|
|
204
|
+
{
|
|
205
|
+
Sleep(static_cast<DWORD>(duration / steps));
|
|
206
|
+
}
|
|
169
207
|
}
|
|
170
208
|
|
|
171
209
|
// Perform mouse button up event
|
|
@@ -174,15 +212,14 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
174
212
|
mouseUpInput.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
|
175
213
|
mouseUpInput.mi.time = 0; // System will provide the timestamp
|
|
176
214
|
|
|
177
|
-
SendInput(1, &mouseUpInput, sizeof(
|
|
178
|
-
|
|
179
|
-
return Napi::Boolean::New(env,
|
|
215
|
+
if (SendInput(1, &mouseUpInput, sizeof(INPUT)) == 0)
|
|
216
|
+
return Napi::Boolean::New(env, false);
|
|
217
|
+
return Napi::Boolean::New(env, success);
|
|
180
218
|
}
|
|
181
219
|
|
|
182
|
-
|
|
183
220
|
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
184
221
|
{
|
|
185
|
-
if (nCode >= 0)
|
|
222
|
+
if (nCode >= 0 && !listenerStopping.load())
|
|
186
223
|
{
|
|
187
224
|
MSLLHOOKSTRUCT *mouse = (MSLLHOOKSTRUCT *)lParam;
|
|
188
225
|
|
|
@@ -226,17 +263,23 @@ LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
|
226
263
|
jsCallback.Call({event});
|
|
227
264
|
};
|
|
228
265
|
|
|
229
|
-
tsfn.
|
|
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
|
+
}
|
|
230
272
|
}
|
|
231
273
|
|
|
232
274
|
return CallNextHookEx(mouseHook, nCode, wParam, lParam);
|
|
233
275
|
}
|
|
234
276
|
|
|
235
|
-
|
|
236
277
|
DWORD WINAPI HookThread(LPVOID)
|
|
237
278
|
{
|
|
238
279
|
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);
|
|
239
|
-
|
|
280
|
+
SetEvent(hHookReady);
|
|
281
|
+
if (mouseHook == NULL)
|
|
282
|
+
return 1;
|
|
240
283
|
MSG msg;
|
|
241
284
|
while (GetMessage(&msg, NULL, 0, 0))
|
|
242
285
|
{
|
|
@@ -259,7 +302,7 @@ Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
|
|
|
259
302
|
return env.Null();
|
|
260
303
|
}
|
|
261
304
|
// Bug fix: prevent double-start leaking a thread and hook
|
|
262
|
-
if (
|
|
305
|
+
if (listenerRunning.test_and_set())
|
|
263
306
|
{
|
|
264
307
|
Napi::TypeError::New(env, "Mouse listener already running").ThrowAsJavaScriptException();
|
|
265
308
|
return env.Null();
|
|
@@ -274,12 +317,43 @@ Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
|
|
|
274
317
|
1);
|
|
275
318
|
|
|
276
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
|
+
}
|
|
277
328
|
hHookThread = CreateThread(NULL, 0, HookThread, NULL, 0, &hookThreadId);
|
|
278
|
-
|
|
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
|
+
}
|
|
279
354
|
return Napi::Boolean::New(env, true);
|
|
280
355
|
}
|
|
281
356
|
|
|
282
|
-
|
|
283
357
|
Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
284
358
|
{
|
|
285
359
|
Napi::Env env = info.Env();
|
|
@@ -292,10 +366,13 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
|
292
366
|
return result;
|
|
293
367
|
}
|
|
294
368
|
|
|
369
|
+
listenerStopping.store(true);
|
|
295
370
|
// PostThreadMessage with WM_QUIT breaks the GetMessage loop in HookThread,
|
|
296
371
|
// which then runs UnhookWindowsHookEx and exits
|
|
297
372
|
if (!PostThreadMessage(hookThreadId, WM_QUIT, 0, 0))
|
|
298
373
|
{
|
|
374
|
+
listenerStopping.store(false);
|
|
375
|
+
listenerRunning.clear();
|
|
299
376
|
result.Set("success", false);
|
|
300
377
|
result.Set("error", "Failed to signal hook thread");
|
|
301
378
|
result.Set("errorCode", Napi::Number::New(env, GetLastError()));
|
|
@@ -306,8 +383,11 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
|
306
383
|
DWORD waitResult = WaitForSingleObject(hHookThread, 3000);
|
|
307
384
|
if (waitResult != WAIT_OBJECT_0)
|
|
308
385
|
{
|
|
309
|
-
// Thread didn't exit in time
|
|
386
|
+
// Thread didn't exit in time - kill it
|
|
310
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);
|
|
311
391
|
result.Set("success", false);
|
|
312
392
|
result.Set("error", "Hook thread did not exit cleanly, was forcefully terminated");
|
|
313
393
|
}
|
|
@@ -322,6 +402,7 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
|
322
402
|
CloseHandle(hHookThread);
|
|
323
403
|
hHookThread = NULL;
|
|
324
404
|
hookThreadId = 0;
|
|
325
|
-
|
|
405
|
+
listenerRunning.clear();
|
|
406
|
+
listenerStopping.store(false);
|
|
326
407
|
return result;
|
|
327
408
|
}
|