node-native-win-utils 2.2.0 → 2.2.3
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 +115 -622
- package/binding.gyp +1 -0
- package/dist/index.d.mts +3 -3
- 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 +148 -58
- package/src/index.mts +1 -1
package/README.md
CHANGED
|
@@ -1,684 +1,177 @@
|
|
|
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
|
+
mouseClick("left", "down") // left button down
|
|
79
|
+
mouseClick("left", "up") // left button up
|
|
80
|
+
mouseDrag(100, 100, 800, 600, 50); // optional speed
|
|
117
81
|
|
|
82
|
+
typeString("Hello from Node!", 30); // 30ms delay per char
|
|
118
83
|
```
|
|
119
84
|
|
|
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
|
|
85
|
+
### Window Operations
|
|
195
86
|
|
|
196
|
-
|
|
87
|
+
```ts
|
|
88
|
+
const data = getWindowData("Notepad");
|
|
89
|
+
console.log(data); // { width, height, x, y }
|
|
197
90
|
|
|
91
|
+
captureWindow("Notepad", "screenshot.png");
|
|
92
|
+
const buffer = captureWindowN("Notepad"); // Buffer
|
|
198
93
|
```
|
|
199
94
|
|
|
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
|
|
95
|
+
### Screen Capture
|
|
217
96
|
|
|
97
|
+
```ts
|
|
98
|
+
await captureScreenToFile("full-screen.png");
|
|
218
99
|
```
|
|
219
100
|
|
|
220
|
-
|
|
101
|
+
### Text Recognition (Tesseract OCR)
|
|
221
102
|
|
|
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
|
|
103
|
+
```ts
|
|
104
|
+
import path from "path";
|
|
237
105
|
|
|
106
|
+
const text = textRecognition(
|
|
107
|
+
path.join(__dirname, "traineddata"), // path to .traineddata files
|
|
108
|
+
"eng",
|
|
109
|
+
path.join(__dirname, "image.png")
|
|
110
|
+
);
|
|
111
|
+
console.log(text);
|
|
238
112
|
```
|
|
239
113
|
|
|
240
|
-
|
|
114
|
+
### OpenCV (image processing)
|
|
241
115
|
|
|
242
|
-
|
|
116
|
+
```ts
|
|
117
|
+
import { OpenCV } from "node-native-win-utils";
|
|
243
118
|
|
|
244
|
-
|
|
119
|
+
const img = new OpenCV("image.png"); // or ImageData { width, height, data: Uint8Array }
|
|
245
120
|
|
|
246
|
-
|
|
121
|
+
// Chainable methods
|
|
122
|
+
const processed = img
|
|
123
|
+
.blur(5, 5)
|
|
124
|
+
.bgrToGray()
|
|
125
|
+
.equalizeHist()
|
|
126
|
+
.darkenColor([240, 240, 240], [255, 255, 255], 0.5) // lower, upper, factor
|
|
127
|
+
.drawRectangle([10, 10], [200, 100], [255, 0, 0], 3)
|
|
128
|
+
.getRegion([50, 50, 300, 200]);
|
|
247
129
|
|
|
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
|
|
130
|
+
processed.imwrite("processed.png");
|
|
261
131
|
|
|
132
|
+
// Template matching
|
|
133
|
+
const match = img.matchTemplate(templateImage, /* method */, /* mask */);
|
|
134
|
+
console.log(match); // { minValue, maxValue, minLocation, maxLocation }
|
|
262
135
|
```
|
|
263
136
|
|
|
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);
|
|
137
|
+
### Mouse Event Listener
|
|
283
138
|
|
|
139
|
+
```ts
|
|
140
|
+
startMouseListener(({ x, y, type }) => {
|
|
141
|
+
console.log(`${type} at ${x},${y}`); // move at 400 300
|
|
284
142
|
});
|
|
285
143
|
|
|
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)
|
|
144
|
+
mouseClick("left"); // trigger event
|
|
321
145
|
|
|
146
|
+
// terminates a thread
|
|
147
|
+
stopMouseListener();
|
|
322
148
|
```
|
|
323
149
|
|
|
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>
|
|
150
|
+
## Building from source
|
|
468
151
|
|
|
152
|
+
```bash
|
|
153
|
+
npm install
|
|
154
|
+
npm run build
|
|
469
155
|
```
|
|
470
156
|
|
|
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
|
-
|
|
157
|
+
#### Requires:
|
|
529
158
|
```
|
|
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
|
-
|
|
159
|
+
Visual Studio Build Tools (C++).
|
|
160
|
+
Python 3
|
|
541
161
|
```
|
|
542
162
|
|
|
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
|
-
```
|
|
163
|
+
## License
|
|
164
|
+
--
|
|
677
165
|
|
|
678
166
|
[OpenCV License](https://github.com/opencv/opencv/blob/master/LICENSE)
|
|
167
|
+
|
|
679
168
|
[MIT License](https://github.com/T-Rumibul/node-native-win-utils/blob/main/LICENSE)
|
|
680
169
|
|
|
681
170
|
|
|
682
|
-
[license-src]: https://img.shields.io/
|
|
171
|
+
[license-src]: https://img.shields.io/badge/License-MIT-yellow.svg
|
|
683
172
|
|
|
684
173
|
[license-href]: https://github.com/T-Rumibul/node-native-win-utils/blob/main/LICENSE
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
**Made with ❤️ for Windows automation.**
|
|
177
|
+
Issues / PRs welcome!
|
package/binding.gyp
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -51,7 +51,7 @@ export type MouseMove = (posX: number, posY: number) => boolean;
|
|
|
51
51
|
/**
|
|
52
52
|
* Function type for simulating a mouse click.
|
|
53
53
|
*/
|
|
54
|
-
export type MouseClick = (button?: "left" | "middle" | "right") => boolean;
|
|
54
|
+
export type MouseClick = (button?: "left" | "middle" | "right", type?: "down" | "up" | "click") => boolean;
|
|
55
55
|
/**
|
|
56
56
|
* Function type for simulating typing.
|
|
57
57
|
*/
|
|
@@ -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.3",
|
|
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,50 +60,64 @@ 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
|
|
|
48
69
|
Napi::Value ClickMouse(const Napi::CallbackInfo &info)
|
|
49
70
|
{
|
|
50
71
|
Napi::Env env = info.Env();
|
|
51
|
-
std::string button;
|
|
52
|
-
|
|
53
|
-
if (info.Length()
|
|
54
|
-
button = "left";
|
|
55
|
-
else
|
|
72
|
+
std::string button = "left";
|
|
73
|
+
std::string type = "click";
|
|
74
|
+
if (info.Length() >= 1 && info[0].IsString())
|
|
56
75
|
button = info[0].As<Napi::String>();
|
|
76
|
+
if(info.Length() >= 2 && info[1].IsString())
|
|
77
|
+
type = info[1].As<Napi::String>();
|
|
78
|
+
|
|
57
79
|
|
|
58
|
-
WORD
|
|
80
|
+
WORD downFlag = 0, upFlag = 0;
|
|
59
81
|
|
|
60
82
|
if (button == "left")
|
|
61
83
|
{
|
|
62
|
-
|
|
84
|
+
downFlag = MOUSEEVENTF_LEFTDOWN;
|
|
85
|
+
upFlag = MOUSEEVENTF_LEFTUP;
|
|
63
86
|
}
|
|
64
87
|
else if (button == "right")
|
|
65
88
|
{
|
|
66
|
-
|
|
89
|
+
downFlag = MOUSEEVENTF_RIGHTDOWN;
|
|
90
|
+
upFlag = MOUSEEVENTF_RIGHTUP;
|
|
67
91
|
}
|
|
68
92
|
else if (button == "middle")
|
|
69
93
|
{
|
|
70
|
-
|
|
94
|
+
downFlag = MOUSEEVENTF_MIDDLEDOWN;
|
|
95
|
+
upFlag = MOUSEEVENTF_MIDDLEUP;
|
|
71
96
|
}
|
|
72
97
|
else
|
|
73
98
|
{
|
|
74
99
|
Napi::TypeError::New(env, "Invalid button name").ThrowAsJavaScriptException();
|
|
75
100
|
return env.Null();
|
|
76
101
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
UINT sent = 0;
|
|
103
|
+
INPUT inputsClick[2] = {};
|
|
104
|
+
inputsClick[0].type = INPUT_MOUSE;
|
|
105
|
+
inputsClick[0].mi.dwFlags = downFlag;
|
|
106
|
+
inputsClick[1].type = INPUT_MOUSE;
|
|
107
|
+
inputsClick[1].mi.dwFlags = upFlag;
|
|
108
|
+
inputsClick[1].mi.time = 0;
|
|
109
|
+
inputsClick[0].mi.time = 0;
|
|
110
|
+
INPUT singleInput[1] = {};
|
|
111
|
+
singleInput[0].type = INPUT_MOUSE;
|
|
112
|
+
WORD flag = (type == "down") ? downFlag : upFlag;
|
|
113
|
+
singleInput[0].mi.dwFlags = flag;
|
|
114
|
+
|
|
115
|
+
if(type == "click")
|
|
116
|
+
sent = SendInput(2, inputsClick, sizeof(INPUT));
|
|
117
|
+
else
|
|
118
|
+
sent = SendInput(1, singleInput, sizeof(INPUT));
|
|
119
|
+
if (sent == 0)
|
|
120
|
+
return Napi::Boolean::New(env, false);
|
|
86
121
|
return Napi::Boolean::New(env, true);
|
|
87
122
|
}
|
|
88
123
|
|
|
@@ -95,7 +130,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
95
130
|
Napi::TypeError::New(env, "You should provide startX, startY, endX, endY").ThrowAsJavaScriptException();
|
|
96
131
|
return env.Null();
|
|
97
132
|
}
|
|
98
|
-
|
|
133
|
+
bool success = true;
|
|
99
134
|
int startX = info[0].As<Napi::Number>();
|
|
100
135
|
int startY = info[1].As<Napi::Number>();
|
|
101
136
|
int endX = info[2].As<Napi::Number>();
|
|
@@ -105,23 +140,26 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
105
140
|
{
|
|
106
141
|
speed = info[4].As<Napi::Number>();
|
|
107
142
|
}
|
|
108
|
-
|
|
143
|
+
// Use pixel coords for timing
|
|
144
|
+
double pixelDistX = endX - startX;
|
|
145
|
+
double pixelDistY = endY - startY;
|
|
146
|
+
double pixelDistance = sqrt(pixelDistX * pixelDistX + pixelDistY * pixelDistY);
|
|
147
|
+
if (speed <= 0)
|
|
148
|
+
speed = 1; // guard div/0
|
|
149
|
+
double duration = pixelDistance / speed;
|
|
109
150
|
// Get the screen metrics
|
|
110
|
-
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
|
|
111
|
-
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
|
|
112
151
|
|
|
152
|
+
Position startPos = calcAbsolutePosition(startX, startY);
|
|
153
|
+
Position endPos = calcAbsolutePosition(endX, endY);
|
|
113
154
|
// Convert coordinates to absolute values
|
|
114
|
-
int absoluteStartX =
|
|
115
|
-
int absoluteStartY =
|
|
116
|
-
int absoluteEndX =
|
|
117
|
-
int absoluteEndY =
|
|
155
|
+
int absoluteStartX = startPos.x;
|
|
156
|
+
int absoluteStartY = startPos.y;
|
|
157
|
+
int absoluteEndX = endPos.x;
|
|
158
|
+
int absoluteEndY = endPos.y;
|
|
118
159
|
|
|
119
160
|
// Calculate the distance and duration based on speed
|
|
120
161
|
double distanceX = absoluteEndX - absoluteStartX;
|
|
121
162
|
double distanceY = absoluteEndY - absoluteStartY;
|
|
122
|
-
double distance = sqrt(distanceX * distanceX + distanceY * distanceY);
|
|
123
|
-
double duration = distance / speed;
|
|
124
|
-
|
|
125
163
|
// Move the mouse to the starting position
|
|
126
164
|
INPUT startMouseInput = {0};
|
|
127
165
|
startMouseInput.type = INPUT_MOUSE;
|
|
@@ -130,7 +168,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
130
168
|
startMouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
131
169
|
startMouseInput.mi.time = 0; // System will provide the timestamp
|
|
132
170
|
|
|
133
|
-
SendInput(1, &startMouseInput, sizeof(
|
|
171
|
+
if (SendInput(1, &startMouseInput, sizeof(INPUT)) == 0)
|
|
172
|
+
return Napi::Boolean::New(env, false);
|
|
173
|
+
;
|
|
134
174
|
|
|
135
175
|
// Perform mouse button down event
|
|
136
176
|
INPUT mouseDownInput = {0};
|
|
@@ -138,7 +178,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
138
178
|
mouseDownInput.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
|
139
179
|
mouseDownInput.mi.time = 0; // System will provide the timestamp
|
|
140
180
|
|
|
141
|
-
SendInput(1, &mouseDownInput, sizeof(
|
|
181
|
+
if (SendInput(1, &mouseDownInput, sizeof(INPUT)) == 0)
|
|
182
|
+
return Napi::Boolean::New(env, false);
|
|
183
|
+
;
|
|
142
184
|
|
|
143
185
|
// Calculate the number of steps based on the duration and desired speed
|
|
144
186
|
const int steps = 100; // Adjust the number of steps for smoother movement
|
|
@@ -148,7 +190,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
148
190
|
double stepY = distanceY / steps;
|
|
149
191
|
|
|
150
192
|
// Move the mouse in increments to simulate dragging with speed control
|
|
151
|
-
for (int i = 0; i
|
|
193
|
+
for (int i = 0; i <= steps; ++i)
|
|
152
194
|
{
|
|
153
195
|
// Calculate the position for the current step
|
|
154
196
|
int currentX = static_cast<int>(absoluteStartX + (stepX * i));
|
|
@@ -162,10 +204,15 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
162
204
|
mouseMoveInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
|
|
163
205
|
mouseMoveInput.mi.time = 0; // System will provide the timestamp
|
|
164
206
|
|
|
165
|
-
SendInput(1, &mouseMoveInput, sizeof(
|
|
207
|
+
if (SendInput(1, &mouseMoveInput, sizeof(INPUT)) == 0)
|
|
208
|
+
return Napi::Boolean::New(env, false);
|
|
209
|
+
;
|
|
166
210
|
|
|
167
211
|
// Sleep for a short duration to control the speed
|
|
168
|
-
|
|
212
|
+
if (i < steps)
|
|
213
|
+
{
|
|
214
|
+
Sleep(static_cast<DWORD>(duration / steps));
|
|
215
|
+
}
|
|
169
216
|
}
|
|
170
217
|
|
|
171
218
|
// Perform mouse button up event
|
|
@@ -174,15 +221,14 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
|
|
|
174
221
|
mouseUpInput.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
|
175
222
|
mouseUpInput.mi.time = 0; // System will provide the timestamp
|
|
176
223
|
|
|
177
|
-
SendInput(1, &mouseUpInput, sizeof(
|
|
178
|
-
|
|
179
|
-
return Napi::Boolean::New(env,
|
|
224
|
+
if (SendInput(1, &mouseUpInput, sizeof(INPUT)) == 0)
|
|
225
|
+
return Napi::Boolean::New(env, false);
|
|
226
|
+
return Napi::Boolean::New(env, success);
|
|
180
227
|
}
|
|
181
228
|
|
|
182
|
-
|
|
183
229
|
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
184
230
|
{
|
|
185
|
-
if (nCode >= 0)
|
|
231
|
+
if (nCode >= 0 && !listenerStopping.load())
|
|
186
232
|
{
|
|
187
233
|
MSLLHOOKSTRUCT *mouse = (MSLLHOOKSTRUCT *)lParam;
|
|
188
234
|
|
|
@@ -226,17 +272,23 @@ LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
|
226
272
|
jsCallback.Call({event});
|
|
227
273
|
};
|
|
228
274
|
|
|
229
|
-
tsfn.
|
|
275
|
+
napi_status status = tsfn.NonBlockingCall(callback);
|
|
276
|
+
if (status != napi_ok)
|
|
277
|
+
{
|
|
278
|
+
// tsfn is already closing
|
|
279
|
+
return CallNextHookEx(mouseHook, nCode, wParam, lParam);
|
|
280
|
+
}
|
|
230
281
|
}
|
|
231
282
|
|
|
232
283
|
return CallNextHookEx(mouseHook, nCode, wParam, lParam);
|
|
233
284
|
}
|
|
234
285
|
|
|
235
|
-
|
|
236
286
|
DWORD WINAPI HookThread(LPVOID)
|
|
237
287
|
{
|
|
238
288
|
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);
|
|
239
|
-
|
|
289
|
+
SetEvent(hHookReady);
|
|
290
|
+
if (mouseHook == NULL)
|
|
291
|
+
return 1;
|
|
240
292
|
MSG msg;
|
|
241
293
|
while (GetMessage(&msg, NULL, 0, 0))
|
|
242
294
|
{
|
|
@@ -259,7 +311,7 @@ Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
|
|
|
259
311
|
return env.Null();
|
|
260
312
|
}
|
|
261
313
|
// Bug fix: prevent double-start leaking a thread and hook
|
|
262
|
-
if (
|
|
314
|
+
if (listenerRunning.test_and_set())
|
|
263
315
|
{
|
|
264
316
|
Napi::TypeError::New(env, "Mouse listener already running").ThrowAsJavaScriptException();
|
|
265
317
|
return env.Null();
|
|
@@ -274,12 +326,43 @@ Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
|
|
|
274
326
|
1);
|
|
275
327
|
|
|
276
328
|
// Store handle and thread ID so StopMouseListener can signal and wait
|
|
329
|
+
hHookReady = CreateEvent(NULL, TRUE, FALSE, NULL);
|
|
330
|
+
if (hHookReady == NULL)
|
|
331
|
+
{
|
|
332
|
+
tsfn.Release();
|
|
333
|
+
listenerRunning.clear();
|
|
334
|
+
Napi::Error::New(env, "Failed to create sync event").ThrowAsJavaScriptException();
|
|
335
|
+
return env.Null();
|
|
336
|
+
}
|
|
277
337
|
hHookThread = CreateThread(NULL, 0, HookThread, NULL, 0, &hookThreadId);
|
|
278
|
-
|
|
338
|
+
if (hHookThread == NULL)
|
|
339
|
+
{
|
|
340
|
+
tsfn.Release();
|
|
341
|
+
CloseHandle(hHookReady);
|
|
342
|
+
hHookReady = NULL;
|
|
343
|
+
listenerRunning.clear(); // release the guard so caller can retry
|
|
344
|
+
Napi::Error::New(env, "Failed to create hook thread").ThrowAsJavaScriptException();
|
|
345
|
+
return env.Null();
|
|
346
|
+
}
|
|
347
|
+
WaitForSingleObject(hHookReady, 2000);
|
|
348
|
+
CloseHandle(hHookReady);
|
|
349
|
+
hHookReady = NULL;
|
|
350
|
+
DWORD exitCode = 0;
|
|
351
|
+
GetExitCodeThread(hHookThread, &exitCode);
|
|
352
|
+
if (exitCode == 1)
|
|
353
|
+
{
|
|
354
|
+
WaitForSingleObject(hHookThread, 1000);
|
|
355
|
+
CloseHandle(hHookThread);
|
|
356
|
+
hHookThread = NULL;
|
|
357
|
+
hookThreadId = 0;
|
|
358
|
+
tsfn.Release();
|
|
359
|
+
listenerRunning.clear();
|
|
360
|
+
Napi::Error::New(env, "Failed to install mouse hook").ThrowAsJavaScriptException();
|
|
361
|
+
return env.Null();
|
|
362
|
+
}
|
|
279
363
|
return Napi::Boolean::New(env, true);
|
|
280
364
|
}
|
|
281
365
|
|
|
282
|
-
|
|
283
366
|
Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
284
367
|
{
|
|
285
368
|
Napi::Env env = info.Env();
|
|
@@ -292,10 +375,13 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
|
292
375
|
return result;
|
|
293
376
|
}
|
|
294
377
|
|
|
378
|
+
listenerStopping.store(true);
|
|
295
379
|
// PostThreadMessage with WM_QUIT breaks the GetMessage loop in HookThread,
|
|
296
380
|
// which then runs UnhookWindowsHookEx and exits
|
|
297
381
|
if (!PostThreadMessage(hookThreadId, WM_QUIT, 0, 0))
|
|
298
382
|
{
|
|
383
|
+
listenerStopping.store(false);
|
|
384
|
+
listenerRunning.clear();
|
|
299
385
|
result.Set("success", false);
|
|
300
386
|
result.Set("error", "Failed to signal hook thread");
|
|
301
387
|
result.Set("errorCode", Napi::Number::New(env, GetLastError()));
|
|
@@ -306,8 +392,11 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
|
306
392
|
DWORD waitResult = WaitForSingleObject(hHookThread, 3000);
|
|
307
393
|
if (waitResult != WAIT_OBJECT_0)
|
|
308
394
|
{
|
|
309
|
-
// Thread didn't exit in time
|
|
395
|
+
// Thread didn't exit in time - kill it
|
|
310
396
|
TerminateThread(hHookThread, 1);
|
|
397
|
+
// Best-effort: reduce chance a concurrent NonBlockingCall is still
|
|
398
|
+
// in-flight before tsfn.Release() below. Not a hard guarantee.
|
|
399
|
+
Sleep(50);
|
|
311
400
|
result.Set("success", false);
|
|
312
401
|
result.Set("error", "Hook thread did not exit cleanly, was forcefully terminated");
|
|
313
402
|
}
|
|
@@ -322,6 +411,7 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
|
|
|
322
411
|
CloseHandle(hHookThread);
|
|
323
412
|
hHookThread = NULL;
|
|
324
413
|
hookThreadId = 0;
|
|
325
|
-
|
|
414
|
+
listenerRunning.clear();
|
|
415
|
+
listenerStopping.store(false);
|
|
326
416
|
return result;
|
|
327
417
|
}
|
package/src/index.mts
CHANGED
|
@@ -68,7 +68,7 @@ export type MouseMove = (posX: number, posY: number) => boolean;
|
|
|
68
68
|
/**
|
|
69
69
|
* Function type for simulating a mouse click.
|
|
70
70
|
*/
|
|
71
|
-
export type MouseClick = (button?: "left" | "middle" | "right") => boolean;
|
|
71
|
+
export type MouseClick = (button?: "left" | "middle" | "right", type?: "down" | "up" | "click") => boolean;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Function type for simulating typing.
|