node-mac-recorder 1.1.0 ā 1.2.1
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 +153 -0
- package/binding.gyp +6 -2
- package/cursor-data.json +93 -0
- package/cursor-test.js +50 -0
- package/index.js +161 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +457 -0
- package/src/mac_recorder.mm +6 -0
- package/preview-test.js +0 -329
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A powerful native macOS screen recording Node.js package with advanced window se
|
|
|
11
11
|
- šÆ **Area Selection** - Record custom screen regions
|
|
12
12
|
- š±ļø **Multi-Display Support** - Automatic display detection and selection
|
|
13
13
|
- šØ **Cursor Control** - Toggle cursor visibility in recordings
|
|
14
|
+
- š±ļø **Cursor Tracking** - Track mouse position, cursor types, and click events
|
|
14
15
|
|
|
15
16
|
šµ **Granular Audio Controls**
|
|
16
17
|
|
|
@@ -231,6 +232,64 @@ const thumbnail = await recorder.getDisplayThumbnail(0, {
|
|
|
231
232
|
// Perfect for display selection UI
|
|
232
233
|
```
|
|
233
234
|
|
|
235
|
+
### Cursor Tracking Methods
|
|
236
|
+
|
|
237
|
+
#### `startCursorTracking(outputPath)`
|
|
238
|
+
|
|
239
|
+
Starts tracking cursor movements and saves data to JSON file.
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
await recorder.startCursorTracking("./cursor-data.json");
|
|
243
|
+
// Cursor tracking started - will record position, cursor type, and events
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### `stopCursorTracking()`
|
|
247
|
+
|
|
248
|
+
Stops cursor tracking and saves collected data.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
await recorder.stopCursorTracking();
|
|
252
|
+
// Data saved to specified JSON file
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `getCursorPosition()`
|
|
256
|
+
|
|
257
|
+
Gets current cursor position and type.
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const position = recorder.getCursorPosition();
|
|
261
|
+
console.log(position);
|
|
262
|
+
// {
|
|
263
|
+
// x: 1234,
|
|
264
|
+
// y: 567,
|
|
265
|
+
// cursorType: "default" // "default", "pointer", "grabbing", "text"
|
|
266
|
+
// }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### `getCursorTrackingStatus()`
|
|
270
|
+
|
|
271
|
+
Returns cursor tracking status and data count.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const status = recorder.getCursorTrackingStatus();
|
|
275
|
+
console.log(status);
|
|
276
|
+
// {
|
|
277
|
+
// isTracking: true,
|
|
278
|
+
// dataCount: 1250,
|
|
279
|
+
// hasEventTap: true,
|
|
280
|
+
// hasRunLoopSource: true
|
|
281
|
+
// }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### `saveCursorData(outputPath)`
|
|
285
|
+
|
|
286
|
+
Manually saves current cursor data to file.
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
await recorder.saveCursorData("./cursor-backup.json");
|
|
290
|
+
// Data saved to file
|
|
291
|
+
```
|
|
292
|
+
|
|
234
293
|
## Usage Examples
|
|
235
294
|
|
|
236
295
|
### Window-Specific Recording
|
|
@@ -404,6 +463,99 @@ async function createDisplaySelector() {
|
|
|
404
463
|
}
|
|
405
464
|
```
|
|
406
465
|
|
|
466
|
+
### Cursor Tracking Usage
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
const recorder = new MacRecorder();
|
|
470
|
+
|
|
471
|
+
async function trackUserInteraction() {
|
|
472
|
+
// Start cursor tracking
|
|
473
|
+
await recorder.startCursorTracking("./user-interactions.json");
|
|
474
|
+
console.log("Cursor tracking started...");
|
|
475
|
+
|
|
476
|
+
// Monitor real-time cursor position
|
|
477
|
+
const monitorInterval = setInterval(() => {
|
|
478
|
+
const position = recorder.getCursorPosition();
|
|
479
|
+
console.log(
|
|
480
|
+
`Cursor: ${position.x}, ${position.y} (${position.cursorType})`
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const status = recorder.getCursorTrackingStatus();
|
|
484
|
+
console.log(`Tracking status: ${status.dataCount} positions recorded`);
|
|
485
|
+
}, 100); // Check every 100ms
|
|
486
|
+
|
|
487
|
+
// Track for 10 seconds
|
|
488
|
+
setTimeout(async () => {
|
|
489
|
+
clearInterval(monitorInterval);
|
|
490
|
+
|
|
491
|
+
// Stop tracking and save data
|
|
492
|
+
await recorder.stopCursorTracking();
|
|
493
|
+
console.log("Cursor tracking completed!");
|
|
494
|
+
|
|
495
|
+
// Load and analyze the data
|
|
496
|
+
const fs = require("fs");
|
|
497
|
+
const data = JSON.parse(
|
|
498
|
+
fs.readFileSync("./user-interactions.json", "utf8")
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
console.log(`Total interactions recorded: ${data.length}`);
|
|
502
|
+
|
|
503
|
+
// Analyze cursor types
|
|
504
|
+
const cursorTypes = {};
|
|
505
|
+
data.forEach((item) => {
|
|
506
|
+
cursorTypes[item.cursorType] = (cursorTypes[item.cursorType] || 0) + 1;
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
console.log("Cursor types distribution:", cursorTypes);
|
|
510
|
+
|
|
511
|
+
// Analyze event types
|
|
512
|
+
const eventTypes = {};
|
|
513
|
+
data.forEach((item) => {
|
|
514
|
+
eventTypes[item.type] = (eventTypes[item.type] || 0) + 1;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
console.log("Event types distribution:", eventTypes);
|
|
518
|
+
}, 10000);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
trackUserInteraction();
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Combined Screen Recording + Cursor Tracking
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
const recorder = new MacRecorder();
|
|
528
|
+
|
|
529
|
+
async function recordWithCursorTracking() {
|
|
530
|
+
// Start both screen recording and cursor tracking
|
|
531
|
+
await Promise.all([
|
|
532
|
+
recorder.startRecording("./screen-recording.mov", {
|
|
533
|
+
captureCursor: false, // Don't show cursor in video
|
|
534
|
+
includeSystemAudio: true,
|
|
535
|
+
quality: "high",
|
|
536
|
+
}),
|
|
537
|
+
recorder.startCursorTracking("./cursor-data.json"),
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
console.log("Recording screen and tracking cursor...");
|
|
541
|
+
|
|
542
|
+
// Record for 30 seconds
|
|
543
|
+
setTimeout(async () => {
|
|
544
|
+
await Promise.all([
|
|
545
|
+
recorder.stopRecording(),
|
|
546
|
+
recorder.stopCursorTracking(),
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
console.log("Screen recording and cursor tracking completed!");
|
|
550
|
+
console.log("Files created:");
|
|
551
|
+
console.log("- screen-recording.mov");
|
|
552
|
+
console.log("- cursor-data.json");
|
|
553
|
+
}, 30000);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
recordWithCursorTracking();
|
|
557
|
+
```
|
|
558
|
+
|
|
407
559
|
## Integration Examples
|
|
408
560
|
|
|
409
561
|
### Electron Integration
|
|
@@ -577,6 +729,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
577
729
|
|
|
578
730
|
### Latest Updates
|
|
579
731
|
|
|
732
|
+
- ā
**Cursor Tracking**: Track mouse position, cursor types, and click events with JSON export
|
|
580
733
|
- ā
**Window Recording**: Automatic coordinate conversion for multi-display setups
|
|
581
734
|
- ā
**Audio Controls**: Separate microphone and system audio controls
|
|
582
735
|
- ā
**Display Selection**: Multi-monitor support with automatic detection
|
package/binding.gyp
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"sources": [
|
|
6
6
|
"src/mac_recorder.mm",
|
|
7
7
|
"src/screen_capture.mm",
|
|
8
|
-
"src/audio_capture.mm"
|
|
8
|
+
"src/audio_capture.mm",
|
|
9
|
+
"src/cursor_tracker.mm"
|
|
9
10
|
],
|
|
10
11
|
"include_dirs": [
|
|
11
12
|
"<!@(node -p \"require('node-addon-api').include\")"
|
|
@@ -30,7 +31,10 @@
|
|
|
30
31
|
"-framework CoreVideo",
|
|
31
32
|
"-framework Foundation",
|
|
32
33
|
"-framework AppKit",
|
|
33
|
-
"-framework ScreenCaptureKit"
|
|
34
|
+
"-framework ScreenCaptureKit",
|
|
35
|
+
"-framework ApplicationServices",
|
|
36
|
+
"-framework Carbon",
|
|
37
|
+
"-framework Accessibility"
|
|
34
38
|
]
|
|
35
39
|
},
|
|
36
40
|
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
|
package/cursor-data.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"x": 1119,
|
|
4
|
+
"y": 1090,
|
|
5
|
+
"timestamp": 106,
|
|
6
|
+
"cursorType": "default",
|
|
7
|
+
"type": "move"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"x": 1119,
|
|
11
|
+
"y": 1080,
|
|
12
|
+
"timestamp": 407,
|
|
13
|
+
"cursorType": "default",
|
|
14
|
+
"type": "move"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"x": 1119,
|
|
18
|
+
"y": 1066,
|
|
19
|
+
"timestamp": 423,
|
|
20
|
+
"cursorType": "default",
|
|
21
|
+
"type": "move"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"x": 1119,
|
|
25
|
+
"y": 1042,
|
|
26
|
+
"timestamp": 446,
|
|
27
|
+
"cursorType": "default",
|
|
28
|
+
"type": "move"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"x": 1119,
|
|
32
|
+
"y": 968,
|
|
33
|
+
"timestamp": 466,
|
|
34
|
+
"cursorType": "default",
|
|
35
|
+
"type": "move"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"x": 1129,
|
|
39
|
+
"y": 906,
|
|
40
|
+
"timestamp": 487,
|
|
41
|
+
"cursorType": "default",
|
|
42
|
+
"type": "move"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"x": 1153,
|
|
46
|
+
"y": 820,
|
|
47
|
+
"timestamp": 507,
|
|
48
|
+
"cursorType": "default",
|
|
49
|
+
"type": "move"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"x": 1168,
|
|
53
|
+
"y": 782,
|
|
54
|
+
"timestamp": 528,
|
|
55
|
+
"cursorType": "default",
|
|
56
|
+
"type": "move"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"x": 1176,
|
|
60
|
+
"y": 764,
|
|
61
|
+
"timestamp": 549,
|
|
62
|
+
"cursorType": "default",
|
|
63
|
+
"type": "move"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"x": 1176,
|
|
67
|
+
"y": 764,
|
|
68
|
+
"timestamp": 2578,
|
|
69
|
+
"cursorType": "default",
|
|
70
|
+
"type": "mousedown"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"x": 1176,
|
|
74
|
+
"y": 764,
|
|
75
|
+
"timestamp": 2599,
|
|
76
|
+
"cursorType": "default",
|
|
77
|
+
"type": "move"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"x": 1176,
|
|
81
|
+
"y": 764,
|
|
82
|
+
"timestamp": 2724,
|
|
83
|
+
"cursorType": "default",
|
|
84
|
+
"type": "mouseup"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"x": 1176,
|
|
88
|
+
"y": 764,
|
|
89
|
+
"timestamp": 2744,
|
|
90
|
+
"cursorType": "default",
|
|
91
|
+
"type": "move"
|
|
92
|
+
}
|
|
93
|
+
]
|
package/cursor-test.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MacRecorder = require("./index");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
async function testCursorCapture() {
|
|
6
|
+
console.log("šÆ Cursor Capture Demo\n");
|
|
7
|
+
|
|
8
|
+
const recorder = new MacRecorder();
|
|
9
|
+
const outputPath = path.join(__dirname, "cursor-data.json");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// BaÅlat
|
|
13
|
+
await recorder.startCursorCapture(outputPath);
|
|
14
|
+
console.log("ā
Kayıt baÅladı...");
|
|
15
|
+
|
|
16
|
+
// 5 saniye bekle
|
|
17
|
+
console.log("š± 5 saniye hareket ettirin, tıklayın...");
|
|
18
|
+
|
|
19
|
+
for (let i = 5; i > 0; i--) {
|
|
20
|
+
process.stdout.write(`ā³ ${i}... `);
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
22
|
+
}
|
|
23
|
+
console.log("\n");
|
|
24
|
+
|
|
25
|
+
// Durdur
|
|
26
|
+
await recorder.stopCursorCapture();
|
|
27
|
+
console.log("ā
Kayıt tamamlandı!");
|
|
28
|
+
|
|
29
|
+
// SonuƧ
|
|
30
|
+
if (fs.existsSync(outputPath)) {
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(outputPath, "utf8"));
|
|
32
|
+
console.log(`š ${data.length} event kaydedildi -> ${outputPath}`);
|
|
33
|
+
|
|
34
|
+
// Basit istatistik
|
|
35
|
+
const clicks = data.filter((d) => d.type === "mousedown").length;
|
|
36
|
+
if (clicks > 0) {
|
|
37
|
+
console.log(`š±ļø ${clicks} click algılandı`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("ā Hata:", error.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Direkt ƧalıÅtır
|
|
46
|
+
if (require.main === module) {
|
|
47
|
+
testCursorCapture().catch(console.error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { testCursorCapture };
|
package/index.js
CHANGED
|
@@ -25,6 +25,14 @@ class MacRecorder extends EventEmitter {
|
|
|
25
25
|
this.outputPath = null;
|
|
26
26
|
this.recordingTimer = null;
|
|
27
27
|
this.recordingStartTime = null;
|
|
28
|
+
|
|
29
|
+
// Cursor capture variables
|
|
30
|
+
this.cursorCaptureInterval = null;
|
|
31
|
+
this.cursorCaptureFile = null;
|
|
32
|
+
this.cursorCaptureStartTime = null;
|
|
33
|
+
this.cursorCaptureFirstWrite = true;
|
|
34
|
+
this.lastCapturedData = null;
|
|
35
|
+
|
|
28
36
|
this.options = {
|
|
29
37
|
includeMicrophone: false, // Default olarak mikrofon kapalı
|
|
30
38
|
includeSystemAudio: true, // Default olarak sistem sesi aƧık
|
|
@@ -413,6 +421,159 @@ class MacRecorder extends EventEmitter {
|
|
|
413
421
|
});
|
|
414
422
|
}
|
|
415
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Event'in kaydedilip kaydedilmeyeceÄini belirler
|
|
426
|
+
*/
|
|
427
|
+
shouldCaptureEvent(currentData) {
|
|
428
|
+
if (!this.lastCapturedData) {
|
|
429
|
+
return true; // İlk event
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const last = this.lastCapturedData;
|
|
433
|
+
|
|
434
|
+
// Event type deÄiÅmiÅse
|
|
435
|
+
if (currentData.type !== last.type) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Pozisyon deÄiÅmiÅse (minimum 2 pixel tolerans)
|
|
440
|
+
if (
|
|
441
|
+
Math.abs(currentData.x - last.x) >= 2 ||
|
|
442
|
+
Math.abs(currentData.y - last.y) >= 2
|
|
443
|
+
) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Cursor type deÄiÅmiÅse
|
|
448
|
+
if (currentData.cursorType !== last.cursorType) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// HiƧbir deÄiÅiklik yoksa kaydetme
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Cursor capture baÅlatır - otomatik olarak dosyaya yazmaya baÅlar
|
|
458
|
+
*/
|
|
459
|
+
async startCursorCapture(filepath) {
|
|
460
|
+
if (!filepath) {
|
|
461
|
+
throw new Error("File path is required");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (this.cursorCaptureInterval) {
|
|
465
|
+
throw new Error("Cursor capture is already running");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
try {
|
|
470
|
+
// Dosyayı oluÅtur ve temizle
|
|
471
|
+
const fs = require("fs");
|
|
472
|
+
fs.writeFileSync(filepath, "[");
|
|
473
|
+
|
|
474
|
+
this.cursorCaptureFile = filepath;
|
|
475
|
+
this.cursorCaptureStartTime = Date.now();
|
|
476
|
+
this.cursorCaptureFirstWrite = true;
|
|
477
|
+
this.lastCapturedData = null;
|
|
478
|
+
|
|
479
|
+
// JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak iƧin)
|
|
480
|
+
this.cursorCaptureInterval = setInterval(() => {
|
|
481
|
+
try {
|
|
482
|
+
const position = nativeBinding.getCursorPosition();
|
|
483
|
+
const timestamp = Date.now() - this.cursorCaptureStartTime;
|
|
484
|
+
|
|
485
|
+
const cursorData = {
|
|
486
|
+
x: position.x,
|
|
487
|
+
y: position.y,
|
|
488
|
+
timestamp: timestamp,
|
|
489
|
+
cursorType: position.cursorType,
|
|
490
|
+
type: position.eventType || "move",
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Sadece eventType deÄiÅtiÄinde veya pozisyon deÄiÅtiÄinde kaydet
|
|
494
|
+
if (this.shouldCaptureEvent(cursorData)) {
|
|
495
|
+
// Dosyaya ekle
|
|
496
|
+
const jsonString = JSON.stringify(cursorData);
|
|
497
|
+
|
|
498
|
+
if (this.cursorCaptureFirstWrite) {
|
|
499
|
+
fs.appendFileSync(filepath, jsonString);
|
|
500
|
+
this.cursorCaptureFirstWrite = false;
|
|
501
|
+
} else {
|
|
502
|
+
fs.appendFileSync(filepath, "," + jsonString);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Son pozisyonu sakla
|
|
506
|
+
this.lastCapturedData = { ...cursorData };
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error("Cursor capture error:", error);
|
|
510
|
+
}
|
|
511
|
+
}, 20); // 50 FPS - mouse event'leri yakalamak iƧin daha hızlı
|
|
512
|
+
|
|
513
|
+
this.emit("cursorCaptureStarted", filepath);
|
|
514
|
+
resolve(true);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
reject(error);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Cursor capture durdurur - dosya yazma iÅlemini sonlandırır
|
|
523
|
+
*/
|
|
524
|
+
async stopCursorCapture() {
|
|
525
|
+
return new Promise((resolve, reject) => {
|
|
526
|
+
try {
|
|
527
|
+
if (!this.cursorCaptureInterval) {
|
|
528
|
+
return resolve(false);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Interval'ı durdur
|
|
532
|
+
clearInterval(this.cursorCaptureInterval);
|
|
533
|
+
this.cursorCaptureInterval = null;
|
|
534
|
+
|
|
535
|
+
// Dosyayı kapat
|
|
536
|
+
if (this.cursorCaptureFile) {
|
|
537
|
+
const fs = require("fs");
|
|
538
|
+
fs.appendFileSync(this.cursorCaptureFile, "]");
|
|
539
|
+
this.cursorCaptureFile = null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// DeÄiÅkenleri temizle
|
|
543
|
+
this.lastCapturedData = null;
|
|
544
|
+
this.cursorCaptureStartTime = null;
|
|
545
|
+
this.cursorCaptureFirstWrite = true;
|
|
546
|
+
|
|
547
|
+
this.emit("cursorCaptureStopped");
|
|
548
|
+
resolve(true);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
reject(error);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Anlık cursor pozisyonunu ve tipini döndürür
|
|
557
|
+
*/
|
|
558
|
+
getCursorPosition() {
|
|
559
|
+
try {
|
|
560
|
+
return nativeBinding.getCursorPosition();
|
|
561
|
+
} catch (error) {
|
|
562
|
+
throw new Error("Failed to get cursor position: " + error.message);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Cursor capture durumunu döndürür
|
|
568
|
+
*/
|
|
569
|
+
getCursorCaptureStatus() {
|
|
570
|
+
return {
|
|
571
|
+
isCapturing: !!this.cursorCaptureInterval,
|
|
572
|
+
outputFile: this.cursorCaptureFile || null,
|
|
573
|
+
startTime: this.cursorCaptureStartTime || null,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
416
577
|
/**
|
|
417
578
|
* Native modül bilgilerini döndürür
|
|
418
579
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#import <napi.h>
|
|
2
|
+
#import <AppKit/AppKit.h>
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
5
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
6
|
+
#import <Carbon/Carbon.h>
|
|
7
|
+
#import <Accessibility/Accessibility.h>
|
|
8
|
+
|
|
9
|
+
// Global state for cursor tracking
|
|
10
|
+
static bool g_isCursorTracking = false;
|
|
11
|
+
static CFMachPortRef g_eventTap = NULL;
|
|
12
|
+
static CFRunLoopSourceRef g_runLoopSource = NULL;
|
|
13
|
+
static NSDate *g_trackingStartTime = nil;
|
|
14
|
+
static NSString *g_outputPath = nil;
|
|
15
|
+
static NSTimer *g_cursorTimer = nil;
|
|
16
|
+
static int g_debugCallbackCount = 0;
|
|
17
|
+
static NSFileHandle *g_fileHandle = nil;
|
|
18
|
+
static bool g_isFirstWrite = true;
|
|
19
|
+
|
|
20
|
+
// Forward declaration
|
|
21
|
+
void cursorTimerCallback();
|
|
22
|
+
void writeToFile(NSDictionary *cursorData);
|
|
23
|
+
|
|
24
|
+
// Timer helper class
|
|
25
|
+
@interface CursorTimerTarget : NSObject
|
|
26
|
+
- (void)timerCallback:(NSTimer *)timer;
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
@implementation CursorTimerTarget
|
|
30
|
+
- (void)timerCallback:(NSTimer *)timer {
|
|
31
|
+
cursorTimerCallback();
|
|
32
|
+
}
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
static CursorTimerTarget *g_timerTarget = nil;
|
|
36
|
+
|
|
37
|
+
// Global cursor state tracking
|
|
38
|
+
static NSString *g_lastDetectedCursorType = nil;
|
|
39
|
+
static int g_cursorTypeCounter = 0;
|
|
40
|
+
|
|
41
|
+
// Mouse button state tracking
|
|
42
|
+
static bool g_leftMouseDown = false;
|
|
43
|
+
static bool g_rightMouseDown = false;
|
|
44
|
+
static NSString *g_lastEventType = @"move";
|
|
45
|
+
|
|
46
|
+
// Cursor type detection helper - gerƧek cursor type'ı al
|
|
47
|
+
NSString* getCursorType() {
|
|
48
|
+
@autoreleasepool {
|
|
49
|
+
g_cursorTypeCounter++;
|
|
50
|
+
|
|
51
|
+
@try {
|
|
52
|
+
// NSCursor.currentCursor kullanarak gerƧek cursor type'ı al
|
|
53
|
+
NSCursor *currentCursor = [NSCursor currentCursor];
|
|
54
|
+
|
|
55
|
+
if (currentCursor == [NSCursor arrowCursor]) {
|
|
56
|
+
g_lastDetectedCursorType = @"default";
|
|
57
|
+
return @"default";
|
|
58
|
+
} else if (currentCursor == [NSCursor pointingHandCursor]) {
|
|
59
|
+
g_lastDetectedCursorType = @"pointer";
|
|
60
|
+
return @"pointer";
|
|
61
|
+
} else if (currentCursor == [NSCursor IBeamCursor]) {
|
|
62
|
+
g_lastDetectedCursorType = @"text";
|
|
63
|
+
return @"text";
|
|
64
|
+
} else if (currentCursor == [NSCursor openHandCursor]) {
|
|
65
|
+
g_lastDetectedCursorType = @"grab";
|
|
66
|
+
return @"grab";
|
|
67
|
+
} else if (currentCursor == [NSCursor closedHandCursor]) {
|
|
68
|
+
g_lastDetectedCursorType = @"grabbing";
|
|
69
|
+
return @"grabbing";
|
|
70
|
+
} else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
|
|
71
|
+
g_lastDetectedCursorType = @"ew-resize";
|
|
72
|
+
return @"ew-resize";
|
|
73
|
+
} else if (currentCursor == [NSCursor resizeUpDownCursor]) {
|
|
74
|
+
g_lastDetectedCursorType = @"ns-resize";
|
|
75
|
+
return @"ns-resize";
|
|
76
|
+
} else if (currentCursor == [NSCursor crosshairCursor]) {
|
|
77
|
+
g_lastDetectedCursorType = @"crosshair";
|
|
78
|
+
return @"crosshair";
|
|
79
|
+
} else {
|
|
80
|
+
// Bilinmeyen cursor - default olarak dƶn
|
|
81
|
+
g_lastDetectedCursorType = @"default";
|
|
82
|
+
return @"default";
|
|
83
|
+
}
|
|
84
|
+
} @catch (NSException *exception) {
|
|
85
|
+
// Hata durumunda default dƶn
|
|
86
|
+
g_lastDetectedCursorType = @"default";
|
|
87
|
+
return @"default";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Dosyaya yazma helper fonksiyonu
|
|
93
|
+
void writeToFile(NSDictionary *cursorData) {
|
|
94
|
+
@autoreleasepool {
|
|
95
|
+
if (!g_fileHandle || !cursorData) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@try {
|
|
100
|
+
NSError *error;
|
|
101
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:cursorData
|
|
102
|
+
options:0
|
|
103
|
+
error:&error];
|
|
104
|
+
if (jsonData && !error) {
|
|
105
|
+
NSString *jsonString = [[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] autorelease];
|
|
106
|
+
|
|
107
|
+
if (g_isFirstWrite) {
|
|
108
|
+
// İlk yazma - array baÅlat
|
|
109
|
+
[g_fileHandle writeData:[@"[" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
110
|
+
[g_fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
|
|
111
|
+
g_isFirstWrite = false;
|
|
112
|
+
} else {
|
|
113
|
+
// Sonraki yazmalar - virgül + json
|
|
114
|
+
[g_fileHandle writeData:[@"," dataUsingEncoding:NSUTF8StringEncoding]];
|
|
115
|
+
[g_fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[g_fileHandle synchronizeFile];
|
|
119
|
+
}
|
|
120
|
+
} @catch (NSException *exception) {
|
|
121
|
+
// Hata durumunda sessizce devam et
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Event callback for mouse events
|
|
127
|
+
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
|
128
|
+
@autoreleasepool {
|
|
129
|
+
g_debugCallbackCount++; // Callback ƧaÄrıldıÄını say
|
|
130
|
+
|
|
131
|
+
if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
|
|
132
|
+
return event;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
CGPoint location = CGEventGetLocation(event);
|
|
136
|
+
NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
137
|
+
NSString *cursorType = getCursorType();
|
|
138
|
+
NSString *eventType = @"move";
|
|
139
|
+
|
|
140
|
+
// Event tipini belirle
|
|
141
|
+
switch (type) {
|
|
142
|
+
case kCGEventLeftMouseDown:
|
|
143
|
+
case kCGEventRightMouseDown:
|
|
144
|
+
case kCGEventOtherMouseDown:
|
|
145
|
+
eventType = @"mousedown";
|
|
146
|
+
break;
|
|
147
|
+
case kCGEventLeftMouseUp:
|
|
148
|
+
case kCGEventRightMouseUp:
|
|
149
|
+
case kCGEventOtherMouseUp:
|
|
150
|
+
eventType = @"mouseup";
|
|
151
|
+
break;
|
|
152
|
+
case kCGEventLeftMouseDragged:
|
|
153
|
+
case kCGEventRightMouseDragged:
|
|
154
|
+
case kCGEventOtherMouseDragged:
|
|
155
|
+
eventType = @"drag";
|
|
156
|
+
break;
|
|
157
|
+
case kCGEventMouseMoved:
|
|
158
|
+
default:
|
|
159
|
+
eventType = @"move";
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Cursor data oluÅtur
|
|
164
|
+
NSDictionary *cursorInfo = @{
|
|
165
|
+
@"x": @((int)location.x),
|
|
166
|
+
@"y": @((int)location.y),
|
|
167
|
+
@"timestamp": @((int)timestamp),
|
|
168
|
+
@"cursorType": cursorType,
|
|
169
|
+
@"type": eventType
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Direkt dosyaya yaz
|
|
173
|
+
writeToFile(cursorInfo);
|
|
174
|
+
|
|
175
|
+
return event;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Timer callback for periodic cursor position updates
|
|
180
|
+
void cursorTimerCallback() {
|
|
181
|
+
@autoreleasepool {
|
|
182
|
+
g_debugCallbackCount++; // Timer callback ƧaÄrıldıÄını say
|
|
183
|
+
|
|
184
|
+
if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Ana thread'de mouse pozisyonu al
|
|
189
|
+
__block NSPoint mouseLocation;
|
|
190
|
+
__block CGPoint location;
|
|
191
|
+
|
|
192
|
+
if ([NSThread isMainThread]) {
|
|
193
|
+
mouseLocation = [NSEvent mouseLocation];
|
|
194
|
+
} else {
|
|
195
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
196
|
+
mouseLocation = [NSEvent mouseLocation];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
201
|
+
size_t displayHeight = CGDisplayPixelsHigh(mainDisplay);
|
|
202
|
+
location = CGPointMake(mouseLocation.x, displayHeight - mouseLocation.y);
|
|
203
|
+
|
|
204
|
+
NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
205
|
+
NSString *cursorType = getCursorType();
|
|
206
|
+
|
|
207
|
+
// Cursor data oluÅtur
|
|
208
|
+
NSDictionary *cursorInfo = @{
|
|
209
|
+
@"x": @((int)location.x),
|
|
210
|
+
@"y": @((int)location.y),
|
|
211
|
+
@"timestamp": @((int)timestamp),
|
|
212
|
+
@"cursorType": cursorType,
|
|
213
|
+
@"type": @"move"
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Direkt dosyaya yaz
|
|
217
|
+
writeToFile(cursorInfo);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Helper function to cleanup cursor tracking
|
|
222
|
+
void cleanupCursorTracking() {
|
|
223
|
+
g_isCursorTracking = false;
|
|
224
|
+
|
|
225
|
+
// Timer temizle
|
|
226
|
+
if (g_cursorTimer) {
|
|
227
|
+
[g_cursorTimer invalidate];
|
|
228
|
+
g_cursorTimer = nil;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (g_timerTarget) {
|
|
232
|
+
[g_timerTarget autorelease];
|
|
233
|
+
g_timerTarget = nil;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Dosyayı ƶnce kapat (en ƶnemli iÅlem)
|
|
237
|
+
if (g_fileHandle) {
|
|
238
|
+
@try {
|
|
239
|
+
if (g_isFirstWrite) {
|
|
240
|
+
// HiƧ veri yazılmamıÅsa boÅ array
|
|
241
|
+
[g_fileHandle writeData:[@"[]" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
242
|
+
} else {
|
|
243
|
+
// JSON array'i kapat
|
|
244
|
+
[g_fileHandle writeData:[@"]" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
245
|
+
}
|
|
246
|
+
[g_fileHandle synchronizeFile];
|
|
247
|
+
[g_fileHandle closeFile];
|
|
248
|
+
} @catch (NSException *exception) {
|
|
249
|
+
// Dosya iÅlemi hata verirse sessizce devam et
|
|
250
|
+
}
|
|
251
|
+
g_fileHandle = nil;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Event tap'i durdur (non-blocking)
|
|
255
|
+
if (g_eventTap) {
|
|
256
|
+
CGEventTapEnable(g_eventTap, false);
|
|
257
|
+
g_eventTap = NULL; // CFRelease iÅlemini yapmıyoruz - system handle etsin
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Run loop source'unu kaldır (non-blocking)
|
|
261
|
+
if (g_runLoopSource) {
|
|
262
|
+
g_runLoopSource = NULL; // CFRelease iÅlemini yapmıyoruz
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Global deÄiÅkenleri sıfırla
|
|
266
|
+
g_trackingStartTime = nil;
|
|
267
|
+
g_outputPath = nil;
|
|
268
|
+
g_debugCallbackCount = 0;
|
|
269
|
+
g_lastDetectedCursorType = nil;
|
|
270
|
+
g_cursorTypeCounter = 0;
|
|
271
|
+
g_isFirstWrite = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// NAPI Function: Start Cursor Tracking
|
|
275
|
+
Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
276
|
+
Napi::Env env = info.Env();
|
|
277
|
+
|
|
278
|
+
if (info.Length() < 1) {
|
|
279
|
+
Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
|
|
280
|
+
return env.Null();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (g_isCursorTracking) {
|
|
284
|
+
return Napi::Boolean::New(env, false);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
288
|
+
|
|
289
|
+
@try {
|
|
290
|
+
// Dosyayı oluÅtur ve aƧ
|
|
291
|
+
g_outputPath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
292
|
+
g_fileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
|
|
293
|
+
|
|
294
|
+
if (!g_fileHandle) {
|
|
295
|
+
// Dosya yoksa oluÅtur
|
|
296
|
+
[[NSFileManager defaultManager] createFileAtPath:g_outputPath contents:nil attributes:nil];
|
|
297
|
+
g_fileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!g_fileHandle) {
|
|
301
|
+
return Napi::Boolean::New(env, false);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Dosyayı temizle (baÅtan baÅla)
|
|
305
|
+
[g_fileHandle truncateFileAtOffset:0];
|
|
306
|
+
g_isFirstWrite = true;
|
|
307
|
+
|
|
308
|
+
g_trackingStartTime = [NSDate date];
|
|
309
|
+
|
|
310
|
+
// Create event tap for mouse events
|
|
311
|
+
CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
312
|
+
CGEventMaskBit(kCGEventLeftMouseUp) |
|
|
313
|
+
CGEventMaskBit(kCGEventRightMouseDown) |
|
|
314
|
+
CGEventMaskBit(kCGEventRightMouseUp) |
|
|
315
|
+
CGEventMaskBit(kCGEventOtherMouseDown) |
|
|
316
|
+
CGEventMaskBit(kCGEventOtherMouseUp) |
|
|
317
|
+
CGEventMaskBit(kCGEventMouseMoved) |
|
|
318
|
+
CGEventMaskBit(kCGEventLeftMouseDragged) |
|
|
319
|
+
CGEventMaskBit(kCGEventRightMouseDragged) |
|
|
320
|
+
CGEventMaskBit(kCGEventOtherMouseDragged));
|
|
321
|
+
|
|
322
|
+
g_eventTap = CGEventTapCreate(kCGSessionEventTap,
|
|
323
|
+
kCGHeadInsertEventTap,
|
|
324
|
+
kCGEventTapOptionListenOnly,
|
|
325
|
+
eventMask,
|
|
326
|
+
eventCallback,
|
|
327
|
+
NULL);
|
|
328
|
+
|
|
329
|
+
if (g_eventTap) {
|
|
330
|
+
// Event tap baÅarılı - detaylı event tracking aktif
|
|
331
|
+
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
|
|
332
|
+
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
|
|
333
|
+
CGEventTapEnable(g_eventTap, true);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// NSTimer kullan (main thread'de ƧalıÅır)
|
|
337
|
+
g_timerTarget = [[CursorTimerTarget alloc] init];
|
|
338
|
+
|
|
339
|
+
g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
|
|
340
|
+
target:g_timerTarget
|
|
341
|
+
selector:@selector(timerCallback:)
|
|
342
|
+
userInfo:nil
|
|
343
|
+
repeats:YES];
|
|
344
|
+
|
|
345
|
+
// Main run loop'a ekle
|
|
346
|
+
[[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
|
|
347
|
+
|
|
348
|
+
g_isCursorTracking = true;
|
|
349
|
+
return Napi::Boolean::New(env, true);
|
|
350
|
+
|
|
351
|
+
} @catch (NSException *exception) {
|
|
352
|
+
cleanupCursorTracking();
|
|
353
|
+
return Napi::Boolean::New(env, false);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// NAPI Function: Stop Cursor Tracking
|
|
358
|
+
Napi::Value StopCursorTracking(const Napi::CallbackInfo& info) {
|
|
359
|
+
Napi::Env env = info.Env();
|
|
360
|
+
|
|
361
|
+
if (!g_isCursorTracking) {
|
|
362
|
+
return Napi::Boolean::New(env, false);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@try {
|
|
366
|
+
cleanupCursorTracking();
|
|
367
|
+
return Napi::Boolean::New(env, true);
|
|
368
|
+
|
|
369
|
+
} @catch (NSException *exception) {
|
|
370
|
+
cleanupCursorTracking();
|
|
371
|
+
return Napi::Boolean::New(env, false);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// NAPI Function: Get Current Cursor Position
|
|
376
|
+
Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
|
|
377
|
+
Napi::Env env = info.Env();
|
|
378
|
+
|
|
379
|
+
@try {
|
|
380
|
+
// NSEvent kullanarak mouse pozisyonu al (daha güvenli)
|
|
381
|
+
NSPoint mouseLocation = [NSEvent mouseLocation];
|
|
382
|
+
|
|
383
|
+
// CGDisplayPixelsHigh ve CGDisplayPixelsWide ile koordinat dƶnüÅümü
|
|
384
|
+
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
385
|
+
size_t displayHeight = CGDisplayPixelsHigh(mainDisplay);
|
|
386
|
+
|
|
387
|
+
// macOS coordinate system (bottom-left origin) to screen coordinates (top-left origin)
|
|
388
|
+
CGPoint location = CGPointMake(mouseLocation.x, displayHeight - mouseLocation.y);
|
|
389
|
+
|
|
390
|
+
NSString *cursorType = getCursorType();
|
|
391
|
+
|
|
392
|
+
// Mouse button state'ini kontrol et
|
|
393
|
+
bool currentLeftMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonLeft);
|
|
394
|
+
bool currentRightMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonRight);
|
|
395
|
+
|
|
396
|
+
NSString *eventType = @"move";
|
|
397
|
+
|
|
398
|
+
// Mouse button state deÄiÅikliklerini tespit et
|
|
399
|
+
if (currentLeftMouseDown && !g_leftMouseDown) {
|
|
400
|
+
eventType = @"mousedown";
|
|
401
|
+
g_lastEventType = @"mousedown";
|
|
402
|
+
} else if (!currentLeftMouseDown && g_leftMouseDown) {
|
|
403
|
+
eventType = @"mouseup";
|
|
404
|
+
g_lastEventType = @"mouseup";
|
|
405
|
+
} else if (currentRightMouseDown && !g_rightMouseDown) {
|
|
406
|
+
eventType = @"rightmousedown";
|
|
407
|
+
g_lastEventType = @"rightmousedown";
|
|
408
|
+
} else if (!currentRightMouseDown && g_rightMouseDown) {
|
|
409
|
+
eventType = @"rightmouseup";
|
|
410
|
+
g_lastEventType = @"rightmouseup";
|
|
411
|
+
} else {
|
|
412
|
+
eventType = @"move";
|
|
413
|
+
g_lastEventType = @"move";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// State'i güncelle
|
|
417
|
+
g_leftMouseDown = currentLeftMouseDown;
|
|
418
|
+
g_rightMouseDown = currentRightMouseDown;
|
|
419
|
+
|
|
420
|
+
Napi::Object result = Napi::Object::New(env);
|
|
421
|
+
result.Set("x", Napi::Number::New(env, (int)location.x));
|
|
422
|
+
result.Set("y", Napi::Number::New(env, (int)location.y));
|
|
423
|
+
result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
|
|
424
|
+
result.Set("eventType", Napi::String::New(env, [eventType UTF8String]));
|
|
425
|
+
|
|
426
|
+
return result;
|
|
427
|
+
|
|
428
|
+
} @catch (NSException *exception) {
|
|
429
|
+
return env.Null();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// NAPI Function: Get Cursor Tracking Status
|
|
434
|
+
Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
|
|
435
|
+
Napi::Env env = info.Env();
|
|
436
|
+
|
|
437
|
+
Napi::Object result = Napi::Object::New(env);
|
|
438
|
+
result.Set("isTracking", Napi::Boolean::New(env, g_isCursorTracking));
|
|
439
|
+
result.Set("hasEventTap", Napi::Boolean::New(env, g_eventTap != NULL));
|
|
440
|
+
result.Set("hasRunLoopSource", Napi::Boolean::New(env, g_runLoopSource != NULL));
|
|
441
|
+
result.Set("hasFileHandle", Napi::Boolean::New(env, g_fileHandle != NULL));
|
|
442
|
+
result.Set("hasTimer", Napi::Boolean::New(env, g_cursorTimer != NULL));
|
|
443
|
+
result.Set("debugCallbackCount", Napi::Number::New(env, g_debugCallbackCount));
|
|
444
|
+
result.Set("cursorTypeCounter", Napi::Number::New(env, g_cursorTypeCounter));
|
|
445
|
+
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Export functions
|
|
450
|
+
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
|
|
451
|
+
exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
|
|
452
|
+
exports.Set("stopCursorTracking", Napi::Function::New(env, StopCursorTracking));
|
|
453
|
+
exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
|
|
454
|
+
exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
|
|
455
|
+
|
|
456
|
+
return exports;
|
|
457
|
+
}
|
package/src/mac_recorder.mm
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
#import <ImageIO/ImageIO.h>
|
|
8
8
|
#import <CoreAudio/CoreAudio.h>
|
|
9
9
|
|
|
10
|
+
// Cursor tracker function declarations
|
|
11
|
+
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
12
|
+
|
|
10
13
|
@interface MacRecorderDelegate : NSObject <AVCaptureFileOutputRecordingDelegate>
|
|
11
14
|
@property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
|
|
12
15
|
@end
|
|
@@ -659,6 +662,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
659
662
|
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
660
663
|
exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
|
|
661
664
|
|
|
665
|
+
// Initialize cursor tracker
|
|
666
|
+
InitCursorTracker(env, exports);
|
|
667
|
+
|
|
662
668
|
return exports;
|
|
663
669
|
}
|
|
664
670
|
|
package/preview-test.js
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
const MacRecorder = require("./index.js");
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
|
|
4
|
-
async function testPreviews() {
|
|
5
|
-
const recorder = new MacRecorder();
|
|
6
|
-
|
|
7
|
-
console.log("š¼ļø Thumbnail Preview Test\n");
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
// 1. Display Previews
|
|
11
|
-
console.log("šŗ Display Thumbnails Test...");
|
|
12
|
-
const displays = await recorder.getDisplays();
|
|
13
|
-
|
|
14
|
-
console.log(`Found ${displays.length} displays:`);
|
|
15
|
-
for (let i = 0; i < displays.length; i++) {
|
|
16
|
-
const display = displays[i];
|
|
17
|
-
console.log(
|
|
18
|
-
` Display ${i}: ${display.resolution} ${
|
|
19
|
-
display.isPrimary ? "(Primary)" : ""
|
|
20
|
-
}`
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
console.log(` šø Capturing thumbnail...`);
|
|
25
|
-
const thumbnail = await recorder.getDisplayThumbnail(display.id, {
|
|
26
|
-
maxWidth: 300,
|
|
27
|
-
maxHeight: 200,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
console.log(` ā
Success: ${thumbnail.length} chars`);
|
|
31
|
-
|
|
32
|
-
// Save as HTML file to view
|
|
33
|
-
const htmlContent = `
|
|
34
|
-
<!DOCTYPE html>
|
|
35
|
-
<html>
|
|
36
|
-
<head>
|
|
37
|
-
<title>Display ${i} Preview</title>
|
|
38
|
-
<style>
|
|
39
|
-
body {
|
|
40
|
-
font-family: Arial, sans-serif;
|
|
41
|
-
text-align: center;
|
|
42
|
-
padding: 20px;
|
|
43
|
-
background: #f5f5f5;
|
|
44
|
-
}
|
|
45
|
-
.preview-card {
|
|
46
|
-
background: white;
|
|
47
|
-
padding: 20px;
|
|
48
|
-
border-radius: 12px;
|
|
49
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
50
|
-
max-width: 400px;
|
|
51
|
-
margin: 0 auto;
|
|
52
|
-
}
|
|
53
|
-
img {
|
|
54
|
-
border: 2px solid #007AFF;
|
|
55
|
-
border-radius: 8px;
|
|
56
|
-
max-width: 100%;
|
|
57
|
-
}
|
|
58
|
-
.info {
|
|
59
|
-
background: #f0f0f0;
|
|
60
|
-
padding: 15px;
|
|
61
|
-
border-radius: 8px;
|
|
62
|
-
margin: 15px 0;
|
|
63
|
-
text-align: left;
|
|
64
|
-
}
|
|
65
|
-
h1 { color: #007AFF; }
|
|
66
|
-
</style>
|
|
67
|
-
</head>
|
|
68
|
-
<body>
|
|
69
|
-
<div class="preview-card">
|
|
70
|
-
<h1>šŗ Display ${i} Preview</h1>
|
|
71
|
-
<div class="info">
|
|
72
|
-
<strong>Resolution:</strong> ${display.resolution}<br>
|
|
73
|
-
<strong>Position:</strong> (${display.x}, ${display.y})<br>
|
|
74
|
-
<strong>Primary:</strong> ${display.isPrimary ? "Yes" : "No"}<br>
|
|
75
|
-
<strong>Display ID:</strong> ${display.id}
|
|
76
|
-
</div>
|
|
77
|
-
<img src="${thumbnail}" alt="Display ${i} Preview" />
|
|
78
|
-
<p><small>Captured: ${new Date().toLocaleString()}</small></p>
|
|
79
|
-
</div>
|
|
80
|
-
</body>
|
|
81
|
-
</html>`;
|
|
82
|
-
|
|
83
|
-
fs.writeFileSync(`display-${i}-preview.html`, htmlContent);
|
|
84
|
-
console.log(` š¾ Saved: display-${i}-preview.html\n`);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.log(` ā Failed: ${error.message}\n`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 2. Window Previews
|
|
91
|
-
console.log("šŖ Window Thumbnails Test...");
|
|
92
|
-
const windows = await recorder.getWindows();
|
|
93
|
-
|
|
94
|
-
// Test first 3 windows
|
|
95
|
-
const testWindows = windows.slice(0, 3);
|
|
96
|
-
console.log(`Testing ${testWindows.length} windows:`);
|
|
97
|
-
|
|
98
|
-
for (let i = 0; i < testWindows.length; i++) {
|
|
99
|
-
const window = testWindows[i];
|
|
100
|
-
console.log(` Window ${i}: [${window.appName}] ${window.name}`);
|
|
101
|
-
console.log(` Size: ${window.width}x${window.height}`);
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
console.log(` šø Capturing thumbnail...`);
|
|
105
|
-
const thumbnail = await recorder.getWindowThumbnail(window.id, {
|
|
106
|
-
maxWidth: 300,
|
|
107
|
-
maxHeight: 200,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
console.log(` ā
Success: ${thumbnail.length} chars`);
|
|
111
|
-
|
|
112
|
-
// Save as HTML file to view
|
|
113
|
-
const htmlContent = `
|
|
114
|
-
<!DOCTYPE html>
|
|
115
|
-
<html>
|
|
116
|
-
<head>
|
|
117
|
-
<title>${window.appName} Preview</title>
|
|
118
|
-
<style>
|
|
119
|
-
body {
|
|
120
|
-
font-family: Arial, sans-serif;
|
|
121
|
-
text-align: center;
|
|
122
|
-
padding: 20px;
|
|
123
|
-
background: #f5f5f5;
|
|
124
|
-
}
|
|
125
|
-
.preview-card {
|
|
126
|
-
background: white;
|
|
127
|
-
padding: 20px;
|
|
128
|
-
border-radius: 12px;
|
|
129
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
130
|
-
max-width: 400px;
|
|
131
|
-
margin: 0 auto;
|
|
132
|
-
}
|
|
133
|
-
img {
|
|
134
|
-
border: 2px solid #FF3B30;
|
|
135
|
-
border-radius: 8px;
|
|
136
|
-
max-width: 100%;
|
|
137
|
-
}
|
|
138
|
-
.info {
|
|
139
|
-
background: #f0f0f0;
|
|
140
|
-
padding: 15px;
|
|
141
|
-
border-radius: 8px;
|
|
142
|
-
margin: 15px 0;
|
|
143
|
-
text-align: left;
|
|
144
|
-
}
|
|
145
|
-
h1 { color: #FF3B30; }
|
|
146
|
-
.app-name { color: #007AFF; font-weight: bold; }
|
|
147
|
-
</style>
|
|
148
|
-
</head>
|
|
149
|
-
<body>
|
|
150
|
-
<div class="preview-card">
|
|
151
|
-
<h1>šŖ <span class="app-name">${window.appName}</span></h1>
|
|
152
|
-
<div class="info">
|
|
153
|
-
<strong>Window:</strong> ${window.name}<br>
|
|
154
|
-
<strong>Size:</strong> ${window.width}x${window.height}<br>
|
|
155
|
-
<strong>Position:</strong> (${window.x}, ${window.y})<br>
|
|
156
|
-
<strong>Window ID:</strong> ${window.id}
|
|
157
|
-
</div>
|
|
158
|
-
<img src="${thumbnail}" alt="${window.appName} Preview" />
|
|
159
|
-
<p><small>Captured: ${new Date().toLocaleString()}</small></p>
|
|
160
|
-
</div>
|
|
161
|
-
</body>
|
|
162
|
-
</html>`;
|
|
163
|
-
|
|
164
|
-
const fileName = `window-${window.appName.replace(
|
|
165
|
-
/[^a-zA-Z0-9]/g,
|
|
166
|
-
""
|
|
167
|
-
)}-${i}-preview.html`;
|
|
168
|
-
fs.writeFileSync(fileName, htmlContent);
|
|
169
|
-
console.log(` š¾ Saved: ${fileName}\n`);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
console.log(` ā Failed: ${error.message}\n`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 3. Create Gallery
|
|
176
|
-
console.log("šØ Creating Preview Gallery...");
|
|
177
|
-
const previewFiles = fs
|
|
178
|
-
.readdirSync(".")
|
|
179
|
-
.filter((file) => file.endsWith("-preview.html"))
|
|
180
|
-
.sort();
|
|
181
|
-
|
|
182
|
-
if (previewFiles.length > 0) {
|
|
183
|
-
const galleryContent = `
|
|
184
|
-
<!DOCTYPE html>
|
|
185
|
-
<html>
|
|
186
|
-
<head>
|
|
187
|
-
<title>šø Thumbnail Preview Gallery</title>
|
|
188
|
-
<style>
|
|
189
|
-
body {
|
|
190
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
191
|
-
padding: 20px;
|
|
192
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
193
|
-
color: white;
|
|
194
|
-
min-height: 100vh;
|
|
195
|
-
}
|
|
196
|
-
.header {
|
|
197
|
-
text-align: center;
|
|
198
|
-
margin-bottom: 30px;
|
|
199
|
-
}
|
|
200
|
-
.gallery {
|
|
201
|
-
display: grid;
|
|
202
|
-
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
203
|
-
gap: 20px;
|
|
204
|
-
margin-top: 20px;
|
|
205
|
-
}
|
|
206
|
-
.preview-card {
|
|
207
|
-
background: rgba(255,255,255,0.1);
|
|
208
|
-
border-radius: 16px;
|
|
209
|
-
overflow: hidden;
|
|
210
|
-
backdrop-filter: blur(10px);
|
|
211
|
-
border: 1px solid rgba(255,255,255,0.2);
|
|
212
|
-
transition: transform 0.3s ease;
|
|
213
|
-
}
|
|
214
|
-
.preview-card:hover {
|
|
215
|
-
transform: translateY(-8px);
|
|
216
|
-
}
|
|
217
|
-
.card-header {
|
|
218
|
-
padding: 20px;
|
|
219
|
-
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
220
|
-
}
|
|
221
|
-
.card-title {
|
|
222
|
-
margin: 0;
|
|
223
|
-
font-size: 18px;
|
|
224
|
-
font-weight: 600;
|
|
225
|
-
}
|
|
226
|
-
.card-subtitle {
|
|
227
|
-
margin: 8px 0 0 0;
|
|
228
|
-
opacity: 0.8;
|
|
229
|
-
font-size: 14px;
|
|
230
|
-
}
|
|
231
|
-
iframe {
|
|
232
|
-
width: 100%;
|
|
233
|
-
height: 400px;
|
|
234
|
-
border: none;
|
|
235
|
-
background: white;
|
|
236
|
-
}
|
|
237
|
-
.stats {
|
|
238
|
-
background: rgba(255,255,255,0.2);
|
|
239
|
-
padding: 20px;
|
|
240
|
-
border-radius: 12px;
|
|
241
|
-
text-align: center;
|
|
242
|
-
margin-bottom: 30px;
|
|
243
|
-
backdrop-filter: blur(10px);
|
|
244
|
-
}
|
|
245
|
-
.btn {
|
|
246
|
-
background: rgba(255,255,255,0.2);
|
|
247
|
-
color: white;
|
|
248
|
-
padding: 10px 20px;
|
|
249
|
-
border: none;
|
|
250
|
-
border-radius: 8px;
|
|
251
|
-
cursor: pointer;
|
|
252
|
-
margin: 5px;
|
|
253
|
-
text-decoration: none;
|
|
254
|
-
display: inline-block;
|
|
255
|
-
transition: all 0.3s ease;
|
|
256
|
-
}
|
|
257
|
-
.btn:hover {
|
|
258
|
-
background: rgba(255,255,255,0.3);
|
|
259
|
-
transform: translateY(-2px);
|
|
260
|
-
}
|
|
261
|
-
</style>
|
|
262
|
-
</head>
|
|
263
|
-
<body>
|
|
264
|
-
<div class="header">
|
|
265
|
-
<h1>šø macOS Thumbnail Preview Gallery</h1>
|
|
266
|
-
<p>Screen and window thumbnails generated with node-mac-recorder</p>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
<div class="stats">
|
|
270
|
-
<strong>${previewFiles.length}</strong> Thumbnails Generated<br>
|
|
271
|
-
<small>Created at ${new Date().toLocaleString()}</small>
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
<div style="text-align: center; margin-bottom: 20px;">
|
|
275
|
-
${previewFiles
|
|
276
|
-
.map(
|
|
277
|
-
(file) =>
|
|
278
|
-
`<a href="${file}" class="btn" target="_blank">${file
|
|
279
|
-
.replace("-preview.html", "")
|
|
280
|
-
.replace(/-/g, " ")}</a>`
|
|
281
|
-
)
|
|
282
|
-
.join("")}
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
<div class="gallery">
|
|
286
|
-
${previewFiles
|
|
287
|
-
.map(
|
|
288
|
-
(file) => `
|
|
289
|
-
<div class="preview-card">
|
|
290
|
-
<div class="card-header">
|
|
291
|
-
<h3 class="card-title">${file
|
|
292
|
-
.replace("-preview.html", "")
|
|
293
|
-
.replace(/-/g, " ")
|
|
294
|
-
.toUpperCase()}</h3>
|
|
295
|
-
<p class="card-subtitle">${file}</p>
|
|
296
|
-
</div>
|
|
297
|
-
<iframe src="${file}"></iframe>
|
|
298
|
-
</div>
|
|
299
|
-
`
|
|
300
|
-
)
|
|
301
|
-
.join("")}
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
<div style="text-align: center; margin-top: 40px; opacity: 0.8;">
|
|
305
|
-
<p>š Generated with <strong>node-mac-recorder v1.1.0</strong></p>
|
|
306
|
-
</div>
|
|
307
|
-
</body>
|
|
308
|
-
</html>`;
|
|
309
|
-
|
|
310
|
-
fs.writeFileSync("preview-gallery.html", galleryContent);
|
|
311
|
-
console.log(`ā
Gallery created: preview-gallery.html`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
console.log("\nš Preview Test Completed!");
|
|
315
|
-
console.log("\nš Generated Files:");
|
|
316
|
-
previewFiles.forEach((file) => console.log(` - ${file}`));
|
|
317
|
-
console.log(" - preview-gallery.html (main gallery)");
|
|
318
|
-
|
|
319
|
-
console.log("\nš View Results:");
|
|
320
|
-
console.log(" open preview-gallery.html");
|
|
321
|
-
console.log("\nš” Individual files can be opened directly in browser!");
|
|
322
|
-
} catch (error) {
|
|
323
|
-
console.error("ā Preview test failed:", error.message);
|
|
324
|
-
console.error(error.stack);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Run preview test
|
|
329
|
-
testPreviews();
|