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 CHANGED
@@ -1,684 +1,175 @@
1
-
2
- [![License][license-src]][license-href]
3
- ![Node-API v8 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v8%20Badge.svg)
1
+ [![License][license-src]][license-href]
2
+ [![Node-API v8](https://raw.githubusercontent.com/nodejs/abi-stable-node/b062e657e639aad36280e0c6f54e181563ef4b19/assets/Node-API%20v8%20Badge.svg)](https://nodejs.org/api/n-api.html)
3
+ [![npm version](https://img.shields.io/npm/v/node-native-win-utils.svg)](https://www.npmjs.com/package/node-native-win-utils)
4
+ [![npm downloads](https://img.shields.io/npm/dm/node-native-win-utils.svg)](https://www.npmjs.com/package/node-native-win-utils)
5
+ [![Platform: Windows only](https://img.shields.io/badge/platform-Windows%20only-blue.svg)](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
- You can install the package using npm:
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
- ```shell
25
+ ## Requirements
25
26
 
26
-
27
+ - **Windows 10 or later** (x64)
28
+ - Node.js >= 18 (prebuilts for recent versions via `prebuildify`)
27
29
 
28
- npm install node-native-win-utils
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
- KeyboardListener
53
-
54
- getWindowData,
55
-
56
- captureWindow,
57
-
58
- captureWindowN,
59
-
60
- mouseMove,
61
-
62
- mouseClick,
63
-
64
- mouseDrag,
65
-
66
- typeString,
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
- ```javascript
85
-
86
- const listener = KeyboardListener.listener()
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
- ## Key Press
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
- keyPress(65); // Press 'A' key once
113
-
114
-
115
-
116
- keyPress(65, 3); // Press 'A' key three times
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
- ## Key Code Helper
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
- mouseMove(100, 200);
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
- ## Mouse Drag
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
- ## Typing
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
- The `typeString` function allows you to simulate typing a string of characters. Provide the string to type as the `stringToType` parameter. Optionally, you can specify
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
- ```javascript
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/github/license/nuxt-modules/icon.svg?style=for-the-badge&colorA=18181B&colorB=28CF8D
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
@@ -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
@@ -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 = (error: {
83
+ export type StopMouseListener = () => {
84
84
  success: boolean;
85
85
  error?: string;
86
86
  errorCode?: number;
87
- }) => void;
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.0",
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",
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 hookThreadId = 0;
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
- // Get the screen metrics
28
- int screenWidth = GetSystemMetrics(SM_CXSCREEN);
29
- int screenHeight = GetSystemMetrics(SM_CYSCREEN);
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
- SendInput(1, &mouseInput, sizeof(mouseInput));
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 mouseEvent = 0;
79
+ WORD downFlag = 0, upFlag = 0;
59
80
 
60
81
  if (button == "left")
61
82
  {
62
- mouseEvent = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
83
+ downFlag = MOUSEEVENTF_LEFTDOWN;
84
+ upFlag = MOUSEEVENTF_LEFTUP;
63
85
  }
64
86
  else if (button == "right")
65
87
  {
66
- mouseEvent = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP;
88
+ downFlag = MOUSEEVENTF_RIGHTDOWN;
89
+ upFlag = MOUSEEVENTF_RIGHTUP;
67
90
  }
68
91
  else if (button == "middle")
69
92
  {
70
- mouseEvent = MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP;
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 mouseInput = {0};
80
- mouseInput.type = INPUT_MOUSE;
81
- mouseInput.mi.dwFlags = mouseEvent;
82
- mouseInput.mi.time = 0; // System will provide the timestamp
83
-
84
- SendInput(1, &mouseInput, sizeof(mouseInput));
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 = static_cast<int>((65536 * startX) / screenWidth);
115
- int absoluteStartY = static_cast<int>((65536 * startY) / screenHeight);
116
- int absoluteEndX = static_cast<int>((65536 * endX) / screenWidth);
117
- 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;
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(startMouseInput));
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(mouseDownInput));
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 < steps; ++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(mouseMoveInput));
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
- Sleep(static_cast<DWORD>(duration / steps));
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(mouseUpInput));
178
-
179
- return Napi::Boolean::New(env, true);
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.BlockingCall(callback);
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 (hHookThread != NULL)
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 force it and warn
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
  }