node-mac-recorder 2.17.15 โ†’ 2.17.17

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.
@@ -6,7 +6,8 @@
6
6
  "Bash(cat:*)",
7
7
  "Bash(git checkout:*)",
8
8
  "WebSearch",
9
- "WebFetch(domain:stackoverflow.com)"
9
+ "WebFetch(domain:stackoverflow.com)",
10
+ "Bash(timeout:*)"
10
11
  ],
11
12
  "deny": [],
12
13
  "ask": []
package/index.js CHANGED
@@ -830,9 +830,11 @@ class MacRecorder extends EventEmitter {
830
830
 
831
831
  if (this.cursorDisplayInfo) {
832
832
  if (this.cursorDisplayInfo.windowRelative) {
833
- // Window recording: Transform global โ†’ window-relative coordinates
833
+ // Window recording: Use direct global-to-window transformation
834
+ // This works correctly for both primary and secondary displays
834
835
  x = position.x - this.cursorDisplayInfo.x;
835
836
  y = position.y - this.cursorDisplayInfo.y;
837
+
836
838
  coordinateSystem = "window-relative";
837
839
 
838
840
  // Window bounds check - skip if cursor is outside window
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.17.15",
3
+ "version": "2.17.17",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -5,6 +5,7 @@
5
5
  #import <ApplicationServices/ApplicationServices.h>
6
6
  #import <Carbon/Carbon.h>
7
7
  #import <Accessibility/Accessibility.h>
8
+ #import <CoreServices/CoreServices.h>
8
9
 
9
10
  // Global state for cursor tracking
10
11
  static bool g_isCursorTracking = false;
@@ -22,6 +23,78 @@ void cursorTimerCallback();
22
23
  void writeToFile(NSDictionary *cursorData);
23
24
  NSDictionary* getDisplayScalingInfo(CGPoint globalPoint);
24
25
 
26
+ // Core Graphics cursor detection as ultimate fallback
27
+ NSString* detectCursorTypeFromCoreGraphics() {
28
+ @try {
29
+ // Use Core Graphics to get cursor information
30
+ // This is more reliable than NSCursor in some cases
31
+
32
+ // Get the current cursor seed (changes when cursor changes)
33
+ static uint32_t lastCursorSeed = 0;
34
+ // Note: CGEventSourceGetUserData requires actual source, simplified for now
35
+ static uint32_t currentSeed = 0;
36
+ currentSeed++; // Simple increment for basic tracking
37
+
38
+ // If cursor hasn't changed, use cached result for performance
39
+ static NSString *cachedCGCursorType = nil;
40
+ if (currentSeed == lastCursorSeed && cachedCGCursorType) {
41
+ return cachedCGCursorType;
42
+ }
43
+
44
+ lastCursorSeed = currentSeed;
45
+
46
+ // Try to detect cursor based on system state
47
+ // Check if we're in a text editing context by looking at input methods
48
+ TISInputSourceRef currentInputSource = TISCopyCurrentKeyboardInputSource();
49
+ if (currentInputSource) {
50
+ // If input source is active, likely in text field
51
+ CFStringRef inputSourceID = (CFStringRef)TISGetInputSourceProperty(currentInputSource, kTISPropertyInputSourceID);
52
+ if (inputSourceID) {
53
+ // Text input is active, likely text cursor
54
+ cachedCGCursorType = @"text";
55
+ CFRelease(currentInputSource);
56
+ return cachedCGCursorType;
57
+ }
58
+ CFRelease(currentInputSource);
59
+ }
60
+
61
+ // Check current application and window state for context
62
+ NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
63
+ if (frontApp) {
64
+ NSString *bundleId = frontApp.bundleIdentifier;
65
+
66
+ // Web browsers often have custom cursor handling
67
+ if ([bundleId containsString:@"chrome"] ||
68
+ [bundleId containsString:@"firefox"] ||
69
+ [bundleId containsString:@"safari"] ||
70
+ [bundleId containsString:@"webkit"]) {
71
+
72
+ // In browsers, use heuristics based on recent patterns
73
+ // This is speculative but can improve accuracy
74
+ return nil; // Let other methods handle browser cursors
75
+ }
76
+
77
+ // Text editors typically show text cursors
78
+ if ([bundleId containsString:@"textedit"] ||
79
+ [bundleId containsString:@"xcode"] ||
80
+ [bundleId containsString:@"vscode"] ||
81
+ [bundleId containsString:@"sublime"] ||
82
+ [bundleId containsString:@"atom"]) {
83
+
84
+ cachedCGCursorType = @"text";
85
+ return cachedCGCursorType;
86
+ }
87
+ }
88
+
89
+ // No specific cursor detected
90
+ cachedCGCursorType = nil;
91
+ return nil;
92
+
93
+ } @catch (NSException *exception) {
94
+ return nil;
95
+ }
96
+ }
97
+
25
98
  // Timer helper class
26
99
  @interface CursorTimerTarget : NSObject
27
100
  - (void)timerCallback:(NSTimer *)timer;
@@ -43,9 +116,9 @@ static NSTimeInterval g_lastCursorCheckTime = 0;
43
116
  static int g_sameCursorDetectionCount = 0;
44
117
  static NSString *g_pendingCursorType = nil;
45
118
 
46
- // Cursor stability constants - minimal stability for real-time response
47
- static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.02; // 20ms (very fast)
48
- static const int CURSOR_CONFIRMATION_COUNT = 1; // Immediate response
119
+ // Enhanced cursor stability constants - ultra-responsive for real-time cursor tracking
120
+ static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.005; // 5ms (ultra fast response)
121
+ static const int CURSOR_CONFIRMATION_COUNT = 0; // No confirmation needed - immediate response
49
122
 
50
123
  // Mouse button state tracking
51
124
  static bool g_leftMouseDown = false;
@@ -57,15 +130,39 @@ static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEv
57
130
  return event;
58
131
  }
59
132
 
60
- // Enhanced cursor type detection with better NSCursor analysis
133
+ // Enhanced cursor type detection with multiple fallbacks and better image analysis
61
134
  NSString* detectCursorTypeFromNSCursor() {
62
135
  @try {
63
- NSCursor *currentCursor = [NSCursor currentSystemCursor];
136
+ // Try multiple methods to get current cursor since currentSystemCursor often fails
137
+ NSCursor *currentCursor = nil;
138
+
139
+ // Method 1: currentSystemCursor (often returns nil but try anyway)
140
+ currentCursor = [NSCursor currentSystemCursor];
141
+
142
+ // Method 2: If nil, try getting cursor from current window
143
+ if (!currentCursor) {
144
+ NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
145
+ if (keyWindow) {
146
+ // Try to get cursor from window's current event
147
+ NSEvent *currentEvent = [[NSApplication sharedApplication] currentEvent];
148
+ if (currentEvent) {
149
+ // This is a fallback - may not always work but worth trying
150
+ @try {
151
+ // Get window at mouse position
152
+ NSPoint mouseLocation = [NSEvent mouseLocation];
153
+ // This approach has limited success, fallback to image analysis
154
+ } @catch (NSException *e) {
155
+ // Ignore and continue
156
+ }
157
+ }
158
+ }
159
+ }
160
+
64
161
  if (!currentCursor) {
65
- return nil; // Return nil to indicate we should try other methods
162
+ return nil; // No cursor available, use contextual detection
66
163
  }
67
164
 
68
- // Compare with known system cursors using identity comparison
165
+ // Compare with known system cursors using identity comparison first
69
166
  if (currentCursor == [NSCursor arrowCursor]) {
70
167
  return @"default";
71
168
  } else if (currentCursor == [NSCursor IBeamCursor]) {
@@ -86,13 +183,73 @@ NSString* detectCursorTypeFromNSCursor() {
86
183
  return @"not-allowed";
87
184
  }
88
185
 
89
- // Check for additional resize cursors using image analysis
186
+ // Enhanced image analysis for better cursor detection
90
187
  NSImage *cursorImage = [currentCursor image];
91
188
  if (cursorImage) {
92
189
  NSSize imageSize = [cursorImage size];
93
190
  NSPoint hotSpot = [currentCursor hotSpot];
94
191
 
95
- // Analyze cursor image to detect resize cursors
192
+ // Get image data for more detailed analysis
193
+ CGImageRef cgImage = [cursorImage CGImageForProposedRect:NULL context:nil hints:nil];
194
+ if (cgImage) {
195
+ size_t width = CGImageGetWidth(cgImage);
196
+ size_t height = CGImageGetHeight(cgImage);
197
+
198
+ // More precise size-based detection
199
+ if (width > 0 && height > 0) {
200
+ float aspectRatio = (float)width / (float)height;
201
+
202
+ // Text cursor: Very narrow (I-beam)
203
+ if (width <= 3 && height >= 15) {
204
+ return @"text";
205
+ }
206
+
207
+ // Hand cursors: Usually square-ish with specific sizes
208
+ if (width >= 12 && width <= 20 && height >= 12 && height <= 20 && aspectRatio > 0.8 && aspectRatio < 1.2) {
209
+ // Try to distinguish hand types by hotspot
210
+ if (hotSpot.x > width * 0.6) { // hotspot towards right
211
+ return @"pointer";
212
+ } else if (hotSpot.x < width * 0.4) { // hotspot towards left
213
+ return @"grab";
214
+ }
215
+ return @"pointer"; // default to pointer for hand-like cursors
216
+ }
217
+
218
+ // Resize cursors: Usually larger and directional
219
+ if (width >= 10 && height >= 10) {
220
+ // Very wide: horizontal resize
221
+ if (aspectRatio > 2.0) {
222
+ return @"col-resize";
223
+ }
224
+ // Very tall: vertical resize
225
+ else if (aspectRatio < 0.5) {
226
+ return @"ns-resize";
227
+ }
228
+ // Square-ish but large: diagonal resize
229
+ else if (width >= 15 && height >= 15 && aspectRatio > 0.7 && aspectRatio < 1.4) {
230
+ // Use hotspot to determine diagonal direction
231
+ float hotSpotX = hotSpot.x / width;
232
+ float hotSpotY = hotSpot.y / height;
233
+
234
+ if ((hotSpotX < 0.5 && hotSpotY < 0.5) || (hotSpotX > 0.5 && hotSpotY > 0.5)) {
235
+ return @"nwse-resize"; // northwest-southeast
236
+ } else {
237
+ return @"nesw-resize"; // northeast-southwest
238
+ }
239
+ }
240
+ }
241
+
242
+ // Crosshair: Usually symmetric and medium sized
243
+ if (width >= 12 && width <= 25 && height >= 12 && height <= 25 &&
244
+ aspectRatio > 0.8 && aspectRatio < 1.2 &&
245
+ hotSpot.x > width * 0.4 && hotSpot.x < width * 0.6 &&
246
+ hotSpot.y > height * 0.4 && hotSpot.y < height * 0.6) {
247
+ return @"crosshair";
248
+ }
249
+ }
250
+ }
251
+
252
+ // Fallback to original size-based analysis
96
253
  if (imageSize.width > 10 && imageSize.height > 10) {
97
254
  // Check for diagonal resize cursors (corner resize)
98
255
  if (imageSize.width >= 15 && imageSize.height >= 15) {
@@ -136,15 +293,28 @@ NSString* getCursorType() {
136
293
  // Layer 1: Fast NSCursor detection (high confidence cases)
137
294
  NSString *nsCursorType = detectCursorTypeFromNSCursor();
138
295
 
139
- // Layer 2: Contextual detection using Accessibility API
296
+ // Layer 1.5: Core Graphics detection as primary fallback
297
+ NSString *cgCursorType = detectCursorTypeFromCoreGraphics();
298
+
299
+ // Layer 2: Optimized Accessibility API detection with caching
140
300
  NSString *contextualCursorType = nil;
141
301
 
142
- CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
143
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
302
+ // Get cursor position once and reuse
303
+ CGEventRef event = CGEventCreate(NULL);
304
+ CGPoint cursorPos = CGEventGetLocation(event);
305
+ CFRelease(event);
306
+
307
+ // Use cached system-wide reference for better performance
308
+ static AXUIElementRef cachedSystemWide = NULL;
309
+ if (!cachedSystemWide) {
310
+ cachedSystemWide = AXUIElementCreateSystemWide();
311
+ }
312
+
144
313
  AXUIElementRef elementAtPosition = NULL;
145
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
314
+ AXError error = AXUIElementCopyElementAtPosition(cachedSystemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
146
315
 
147
316
  if (error == kAXErrorSuccess && elementAtPosition) {
317
+ // Fast role detection with immediate processing
148
318
  CFStringRef role = NULL;
149
319
  error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
150
320
 
@@ -278,67 +448,53 @@ NSString* getCursorType() {
278
448
  }
279
449
  CFRelease(elementAtPosition);
280
450
  }
281
- if (systemWide) CFRelease(systemWide);
282
451
 
283
- // Layer 3: Contextual-first approach (NSCursor is unreliable)
452
+ // Layer 3: Enhanced multi-layer priority detection
284
453
  NSString *detectedCursorType = @"default";
285
454
 
286
- // Priority logic - contextual detection first:
287
- // 1. If contextual gives ANY specific cursor, use it
455
+ // Priority logic - multiple fallbacks for maximum accuracy:
456
+ // 1. If contextual (Accessibility) gives ANY specific cursor, use it (highest priority)
288
457
  if (contextualCursorType != nil) {
289
458
  detectedCursorType = contextualCursorType;
290
459
  }
291
- // 2. Fallback to NSCursor only for very specific cases
460
+ // 2. If NSCursor gives reliable results, use them
292
461
  else if (nsCursorType != nil &&
293
462
  ([nsCursorType isEqualToString:@"text"] ||
294
- [nsCursorType isEqualToString:@"pointer"])) {
295
- // Only trust NSCursor for text and pointer (most reliable)
463
+ [nsCursorType isEqualToString:@"pointer"] ||
464
+ [nsCursorType hasSuffix:@"resize"])) {
465
+ // Trust NSCursor for these specific types
466
+ detectedCursorType = nsCursorType;
467
+ }
468
+ // 3. Core Graphics context-based detection
469
+ else if (cgCursorType != nil) {
470
+ detectedCursorType = cgCursorType;
471
+ }
472
+ // 4. If NSCursor gives any result, use it as last resort
473
+ else if (nsCursorType != nil) {
296
474
  detectedCursorType = nsCursorType;
297
475
  }
298
- // 3. Everything else defaults to default
476
+ // 5. Everything else defaults to default
299
477
  else {
300
478
  detectedCursorType = @"default";
301
479
  }
302
480
 
303
- // Layer 4: Minimal stability - mostly real-time with quick default fallback
481
+ // Layer 4: Ultra-responsive detection with minimal stabilization
304
482
  NSString *finalCursorType = detectedCursorType;
305
483
 
306
- // Quick validation and immediate response for most cases
484
+ // Ultra-fast response - immediate updates for all cursor types
307
485
  if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
308
- // Update immediately for most cursor types
309
- if ([detectedCursorType isEqualToString:@"default"] ||
310
- [detectedCursorType isEqualToString:@"text"] ||
311
- [detectedCursorType isEqualToString:@"pointer"]) {
312
- // High-confidence cursor types - update immediately
313
- g_stableCursorType = detectedCursorType;
314
- finalCursorType = detectedCursorType;
315
- }
316
- // Resize cursors need tiny bit of stability to avoid jitter
317
- else if ([detectedCursorType hasSuffix:@"resize"]) {
318
- if ([detectedCursorType isEqualToString:g_pendingCursorType]) {
319
- g_sameCursorDetectionCount++;
320
- if (g_sameCursorDetectionCount >= CURSOR_CONFIRMATION_COUNT) {
321
- g_stableCursorType = detectedCursorType;
322
- finalCursorType = detectedCursorType;
323
- } else {
324
- finalCursorType = g_stableCursorType; // Keep previous
325
- }
326
- } else {
327
- g_pendingCursorType = detectedCursorType;
328
- g_sameCursorDetectionCount = 1;
329
- finalCursorType = g_stableCursorType; // Keep previous
330
- }
331
- }
332
- // Everything else - immediate update
333
- else {
334
- g_stableCursorType = detectedCursorType;
335
- finalCursorType = detectedCursorType;
336
- }
337
-
486
+ // Immediate update for all cursor types (no confirmation needed)
487
+ g_stableCursorType = detectedCursorType;
488
+ finalCursorType = detectedCursorType;
338
489
  g_lastCursorCheckTime = currentTime;
490
+
491
+ // Reset pending state since we're updating immediately
492
+ g_pendingCursorType = nil;
493
+ g_sameCursorDetectionCount = 0;
339
494
  } else {
340
- // Too soon - use stable cursor
341
- finalCursorType = g_stableCursorType;
495
+ // Still use immediate update for ultra-responsiveness
496
+ // Only apply minimal delay for very rapid changes
497
+ finalCursorType = detectedCursorType;
342
498
  }
343
499
 
344
500
  // Final fallback validation
@@ -119,8 +119,41 @@ static NSString *g_outputPath = nil;
119
119
  }
120
120
 
121
121
  if (targetWindow && targetApp) {
122
- NSLog(@"๐ŸชŸ Recording window: %@ (%ux%u)",
122
+ NSLog(@"๐ŸชŸ Recording window: %@ (%ux%u)",
123
123
  targetWindow.title, (unsigned)targetWindow.frame.size.width, (unsigned)targetWindow.frame.size.height);
124
+
125
+ // Find which display contains this window for proper cursor coordinate transformation
126
+ CGRect windowFrame = targetWindow.frame;
127
+ NSLog(@"๐Ÿ” Window frame: (%.0f,%.0f,%.0fx%.0f)",
128
+ windowFrame.origin.x, windowFrame.origin.y,
129
+ windowFrame.size.width, windowFrame.size.height);
130
+
131
+ // Find the display that contains the window's center point
132
+ CGPoint windowCenter = CGPointMake(
133
+ windowFrame.origin.x + windowFrame.size.width / 2,
134
+ windowFrame.origin.y + windowFrame.size.height / 2
135
+ );
136
+
137
+ for (SCDisplay *display in content.displays) {
138
+ CGRect displayFrame = display.frame;
139
+ NSLog(@"๐Ÿ” Checking if window center (%.0f,%.0f) is in display ID=%u frame (%.0f,%.0f,%.0fx%.0f)",
140
+ windowCenter.x, windowCenter.y, display.displayID,
141
+ displayFrame.origin.x, displayFrame.origin.y,
142
+ displayFrame.size.width, displayFrame.size.height);
143
+
144
+ if (CGRectContainsPoint(displayFrame, windowCenter)) {
145
+ targetDisplay = display;
146
+ NSLog(@"โœ… Window is on display ID=%u", display.displayID);
147
+ break;
148
+ }
149
+ }
150
+
151
+ if (!targetDisplay) {
152
+ // Fallback: use main display if no display contains the window center
153
+ targetDisplay = content.displays.firstObject;
154
+ NSLog(@"โš ๏ธ Window not contained in any display, using main display ID=%u as fallback", targetDisplay.displayID);
155
+ }
156
+
124
157
  filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
125
158
  recordingWidth = (NSInteger)targetWindow.frame.size.width;
126
159
  recordingHeight = (NSInteger)targetWindow.frame.size.height;
@@ -0,0 +1,105 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ async function testCursorCoordinates() {
4
+ console.log('๐Ÿงช Testing cursor coordinate fixes for window recording...');
5
+
6
+ const recorder = new MacRecorder();
7
+
8
+ try {
9
+ // Get available windows and displays
10
+ const windows = await recorder.getWindows();
11
+ const displays = await recorder.getDisplays();
12
+
13
+ console.log('\n๐Ÿ“ฑ Available displays:');
14
+ displays.forEach((display, i) => {
15
+ console.log(` ${i + 1}. Display ${display.id}: ${display.resolution} at (${display.x}, ${display.y}) ${display.isPrimary ? '(Primary)' : ''}`);
16
+ });
17
+
18
+ console.log('\n๐ŸชŸ Available windows:');
19
+ windows.slice(0, 5).forEach((window, i) => {
20
+ console.log(` ${i + 1}. ${window.title} (${window.app}) - ${window.width}x${window.height} at (${window.x}, ${window.y})`);
21
+ });
22
+
23
+ if (windows.length === 0) {
24
+ console.log('โŒ No windows available for testing');
25
+ return;
26
+ }
27
+
28
+ // Pick the first available window for testing
29
+ const testWindow = windows[0];
30
+ console.log(`\n๐ŸŽฏ Testing with window: ${testWindow.title}`);
31
+ console.log(` Window position: (${testWindow.x}, ${testWindow.y})`);
32
+ console.log(` Window size: ${testWindow.width}x${testWindow.height}`);
33
+
34
+ // Check current cursor position before recording
35
+ const cursorPos = recorder.getCursorPosition();
36
+ console.log(`\n๐Ÿ–ฑ๏ธ Current cursor position (global): (${cursorPos.x}, ${cursorPos.y})`);
37
+
38
+ // Setup recording options for window recording
39
+ recorder.options.windowId = testWindow.id;
40
+ recorder.options.captureCursor = true;
41
+
42
+ console.log('\n๐Ÿ”ง Starting cursor capture test for window recording...');
43
+
44
+ // Start cursor capture with window-relative coordinates
45
+ const outputFile = `./test-output/cursor-test-window-${Date.now()}.json`;
46
+
47
+ // Start cursor tracking first to see if coordinate transformation works
48
+ await recorder.startCursorCapture(outputFile, {
49
+ windowRelative: true,
50
+ windowInfo: {
51
+ x: testWindow.x,
52
+ y: testWindow.y,
53
+ width: testWindow.width,
54
+ height: testWindow.height,
55
+ displayId: null // Let it auto-detect
56
+ }
57
+ });
58
+
59
+ console.log('โœ… Cursor capture started successfully!');
60
+ console.log('๐Ÿ–ฑ๏ธ Move your mouse around the window for 5 seconds...');
61
+ console.log(` Window bounds: (0, 0) to (${testWindow.width}, ${testWindow.height})`);
62
+
63
+ // Wait for 5 seconds to capture cursor movements
64
+ await new Promise(resolve => setTimeout(resolve, 5000));
65
+
66
+ console.log('\n๐Ÿ›‘ Stopping cursor capture...');
67
+ await recorder.stopCursorCapture();
68
+
69
+ console.log(`โœ… Test completed! Check cursor data in: ${outputFile}`);
70
+
71
+ // Read and analyze some cursor data
72
+ const fs = require('fs');
73
+ if (fs.existsSync(outputFile)) {
74
+ const cursorData = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
75
+ console.log(`\n๐Ÿ“Š Captured ${cursorData.length} cursor events`);
76
+
77
+ if (cursorData.length > 0) {
78
+ const first = cursorData[0];
79
+ const last = cursorData[cursorData.length - 1];
80
+
81
+ console.log(` First event: (${first.x}, ${first.y}) - ${first.coordinateSystem}`);
82
+ console.log(` Last event: (${last.x}, ${last.y}) - ${last.coordinateSystem}`);
83
+
84
+ // Check if coordinates are within window bounds
85
+ const inBounds = cursorData.filter(event =>
86
+ event.x >= 0 && event.x < testWindow.width &&
87
+ event.y >= 0 && event.y < testWindow.height
88
+ );
89
+
90
+ console.log(` Events within window bounds: ${inBounds.length}/${cursorData.length} (${Math.round(inBounds.length/cursorData.length*100)}%)`);
91
+
92
+ if (inBounds.length > 0) {
93
+ console.log('โœ… Cursor coordinates appear to be correctly transformed to window-relative!');
94
+ } else {
95
+ console.log('โŒ No cursor coordinates are within window bounds - transformation may be incorrect');
96
+ }
97
+ }
98
+ }
99
+
100
+ } catch (error) {
101
+ console.error('โŒ Test failed:', error.message);
102
+ }
103
+ }
104
+
105
+ testCursorCoordinates().catch(console.error);
@@ -0,0 +1,117 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ async function testCursorTypes() {
4
+ console.log('๐Ÿงช Testing improved cursor type detection...');
5
+
6
+ const recorder = new MacRecorder();
7
+
8
+ try {
9
+ // Test basic cursor position and type detection
10
+ console.log('\n๐Ÿ“ Current cursor state:');
11
+ const cursorPos = recorder.getCursorPosition();
12
+ console.log(` Position: (${cursorPos.x}, ${cursorPos.y})`);
13
+ console.log(` Type: ${cursorPos.cursorType}`);
14
+ console.log(` Event: ${cursorPos.eventType}`);
15
+
16
+ console.log('\n๐Ÿ–ฑ๏ธ Starting continuous cursor type monitoring...');
17
+ console.log('๐Ÿ“‹ Instructions:');
18
+ console.log(' 1. Move mouse over different UI elements');
19
+ console.log(' 2. Hover over buttons, text fields, links, window borders');
20
+ console.log(' 3. Try resize handles on windows');
21
+ console.log(' 4. Move over different applications');
22
+ console.log(' 5. Test will run for 15 seconds');
23
+
24
+ const cursorData = [];
25
+ let lastReportedType = null;
26
+ let typeChangeCount = 0;
27
+
28
+ const startTime = Date.now();
29
+ const testDuration = 15000; // 15 seconds
30
+
31
+ // Continuous monitoring
32
+ const monitor = setInterval(() => {
33
+ const pos = recorder.getCursorPosition();
34
+ const currentType = pos.cursorType;
35
+
36
+ // Record all data for analysis
37
+ cursorData.push({
38
+ timestamp: Date.now() - startTime,
39
+ x: pos.x,
40
+ y: pos.y,
41
+ type: currentType,
42
+ event: pos.eventType
43
+ });
44
+
45
+ // Report type changes immediately
46
+ if (currentType !== lastReportedType) {
47
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
48
+ console.log(` [${elapsed}s] Cursor changed: ${lastReportedType || 'unknown'} โ†’ ${currentType} at (${pos.x}, ${pos.y})`);
49
+ lastReportedType = currentType;
50
+ typeChangeCount++;
51
+ }
52
+ }, 50); // Check every 50ms for high responsiveness
53
+
54
+ // Stop after test duration
55
+ setTimeout(() => {
56
+ clearInterval(monitor);
57
+
58
+ console.log('\n๐Ÿ“Š Test Results:');
59
+ console.log(` Total cursor events: ${cursorData.length}`);
60
+ console.log(` Cursor type changes: ${typeChangeCount}`);
61
+ console.log(` Test duration: ${testDuration/1000} seconds`);
62
+
63
+ // Analyze cursor types detected
64
+ const typeDistribution = {};
65
+ cursorData.forEach(data => {
66
+ typeDistribution[data.type] = (typeDistribution[data.type] || 0) + 1;
67
+ });
68
+
69
+ console.log('\n๐ŸŽฏ Cursor types detected:');
70
+ Object.entries(typeDistribution)
71
+ .sort(([,a], [,b]) => b - a)
72
+ .forEach(([type, count]) => {
73
+ const percentage = ((count / cursorData.length) * 100).toFixed(1);
74
+ console.log(` ${type}: ${count} times (${percentage}%)`);
75
+ });
76
+
77
+ // Responsiveness analysis
78
+ const typeChanges = cursorData.filter((data, i) =>
79
+ i === 0 || data.type !== cursorData[i-1].type
80
+ );
81
+
82
+ if (typeChanges.length > 1) {
83
+ console.log('\nโšก Responsiveness Analysis:');
84
+ console.log(` First type change at: ${typeChanges[1].timestamp}ms`);
85
+ console.log(` Last type change at: ${typeChanges[typeChanges.length-1].timestamp}ms`);
86
+ console.log(` Average detection frequency: ${(cursorData.length / (testDuration/1000)).toFixed(1)} checks/sec`);
87
+
88
+ if (typeChangeCount > 0) {
89
+ console.log('โœ… Cursor type detection is working and responsive!');
90
+ } else {
91
+ console.log('โš ๏ธ No cursor type changes detected - try moving over different UI elements');
92
+ }
93
+ }
94
+
95
+ // Save detailed log for analysis
96
+ const fs = require('fs');
97
+ const logFile = `./test-output/cursor-types-test-${Date.now()}.json`;
98
+ fs.writeFileSync(logFile, JSON.stringify({
99
+ summary: {
100
+ totalEvents: cursorData.length,
101
+ typeChanges: typeChangeCount,
102
+ duration: testDuration,
103
+ typeDistribution
104
+ },
105
+ events: cursorData
106
+ }, null, 2));
107
+
108
+ console.log(`\n๐Ÿ’พ Detailed log saved to: ${logFile}`);
109
+
110
+ }, testDuration);
111
+
112
+ } catch (error) {
113
+ console.error('โŒ Test failed:', error.message);
114
+ }
115
+ }
116
+
117
+ testCursorTypes().catch(console.error);
@@ -0,0 +1,72 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ async function testDisplayCoordinates() {
4
+ console.log('๐Ÿงช Testing cursor coordinate behavior across displays...');
5
+
6
+ const recorder = new MacRecorder();
7
+
8
+ try {
9
+ const displays = await recorder.getDisplays();
10
+
11
+ console.log('\n๐Ÿ“ฑ Display information:');
12
+ displays.forEach((display, i) => {
13
+ const type = display.isPrimary ? 'PRIMARY' : 'SECONDARY';
14
+ console.log(` ${type} Display ${display.id}: ${display.resolution} at (${display.x}, ${display.y})`);
15
+ });
16
+
17
+ console.log('\n๐Ÿ–ฑ๏ธ Real-time cursor position test:');
18
+ console.log('Move your mouse to different displays and observe coordinates...');
19
+ console.log('Press Ctrl+C to stop\n');
20
+
21
+ let lastDisplay = null;
22
+ const interval = setInterval(() => {
23
+ const pos = recorder.getCursorPosition();
24
+
25
+ // Determine which display cursor is on
26
+ let currentDisplay = null;
27
+ for (const display of displays) {
28
+ if (pos.x >= display.x &&
29
+ pos.x < display.x + display.width &&
30
+ pos.y >= display.y &&
31
+ pos.y < display.y + display.height) {
32
+ currentDisplay = display;
33
+ break;
34
+ }
35
+ }
36
+
37
+ if (currentDisplay && currentDisplay.id !== lastDisplay?.id) {
38
+ const type = currentDisplay.isPrimary ? 'PRIMARY' : 'SECONDARY';
39
+ console.log(`\n๐Ÿ“ Moved to ${type} Display ${currentDisplay.id}:`);
40
+ console.log(` Display bounds: (${currentDisplay.x}, ${currentDisplay.y}) to (${currentDisplay.x + currentDisplay.width}, ${currentDisplay.y + currentDisplay.height})`);
41
+ lastDisplay = currentDisplay;
42
+ }
43
+
44
+ if (currentDisplay) {
45
+ // Calculate display-relative coordinates
46
+ const displayRelativeX = pos.x - currentDisplay.x;
47
+ const displayRelativeY = pos.y - currentDisplay.y;
48
+
49
+ const type = currentDisplay.isPrimary ? 'PRI' : 'SEC';
50
+ console.log(`${type} | Global: (${pos.x}, ${pos.y}) | Display-rel: (${displayRelativeX}, ${displayRelativeY})`);
51
+ } else {
52
+ console.log(`??? | Global: (${pos.x}, ${pos.y}) | Not on any display`);
53
+ }
54
+ }, 200); // Every 200ms
55
+
56
+ // Handle Ctrl+C
57
+ process.on('SIGINT', () => {
58
+ clearInterval(interval);
59
+ console.log('\n\n๐Ÿ” Test completed. Look for any unusual patterns in the coordinates.');
60
+ console.log('\nKey things to check:');
61
+ console.log('1. Do global coordinates look correct on both displays?');
62
+ console.log('2. Do display-relative coordinates start from (0,0) on each display?');
63
+ console.log('3. Are there any unexpected offsets on the primary display?');
64
+ process.exit(0);
65
+ });
66
+
67
+ } catch (error) {
68
+ console.error('โŒ Test failed:', error.message);
69
+ }
70
+ }
71
+
72
+ testDisplayCoordinates().catch(console.error);
@@ -0,0 +1,110 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ async function testMenuBarOffset() {
4
+ console.log('๐Ÿงช Testing macOS menu bar coordinate offset...');
5
+
6
+ const recorder = new MacRecorder();
7
+
8
+ try {
9
+ const displays = await recorder.getDisplays();
10
+ const windows = await recorder.getWindows();
11
+
12
+ const primaryDisplay = displays.find(d => d.isPrimary);
13
+
14
+ console.log('\n๐Ÿ“ฑ Primary Display Info:');
15
+ console.log(` Bounds: (${primaryDisplay.x}, ${primaryDisplay.y}) ${primaryDisplay.width}x${primaryDisplay.height}`);
16
+
17
+ // Get menu bar height by checking screen bounds vs usable bounds
18
+ const { screen } = require('electron').screen || {};
19
+
20
+ // Alternative way to get menu bar info using native macOS APIs
21
+ console.log('\n๐Ÿ” Coordinate System Analysis:');
22
+
23
+ // Find windows on primary display
24
+ const primaryWindows = windows.filter(window => {
25
+ const windowCenterX = window.x + window.width / 2;
26
+ const windowCenterY = window.y + window.height / 2;
27
+ return (windowCenterX >= primaryDisplay.x &&
28
+ windowCenterX < primaryDisplay.x + primaryDisplay.width &&
29
+ windowCenterY >= primaryDisplay.y &&
30
+ windowCenterY < primaryDisplay.y + primaryDisplay.height);
31
+ });
32
+
33
+ console.log('\n๐ŸชŸ Windows on Primary Display:');
34
+ primaryWindows.slice(0, 3).forEach((window, i) => {
35
+ console.log(` ${i+1}. "${window.title}" at (${window.x}, ${window.y}) ${window.width}x${window.height}`);
36
+
37
+ // Check if window Y starts suspiciously high (indicating menu bar offset)
38
+ if (window.y > 20 && window.y < 100) {
39
+ console.log(` โš ๏ธ Window Y=${window.y} suggests menu bar offset of ~${window.y}px`);
40
+ }
41
+ });
42
+
43
+ // Test cursor coordinates at the very top of the screen
44
+ console.log('\n๐Ÿ–ฑ๏ธ Move mouse to the very TOP of the primary screen and observe:');
45
+ console.log('(This will help identify if menu bar affects coordinates)');
46
+
47
+ for (let i = 0; i < 10; i++) {
48
+ await new Promise(resolve => setTimeout(resolve, 500));
49
+ const pos = recorder.getCursorPosition();
50
+
51
+ if (pos.x >= primaryDisplay.x &&
52
+ pos.x < primaryDisplay.x + primaryDisplay.width &&
53
+ pos.y >= primaryDisplay.y &&
54
+ pos.y < primaryDisplay.y + primaryDisplay.height) {
55
+
56
+ console.log(` Test ${i+1}: Global (${pos.x}, ${pos.y}) | Y from top: ${pos.y - primaryDisplay.y}`);
57
+
58
+ // If cursor Y is very close to 0 but not exactly 0, there might be menu bar offset
59
+ if (pos.y >= primaryDisplay.y && pos.y <= primaryDisplay.y + 50) {
60
+ const yFromTop = pos.y - primaryDisplay.y;
61
+ if (yFromTop > 0 && yFromTop < 40) {
62
+ console.log(` ๐ŸŽฏ Potential menu bar detected: ${yFromTop}px from top`);
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // Test with a specific window on primary display
69
+ if (primaryWindows.length > 0) {
70
+ const testWindow = primaryWindows[0];
71
+
72
+ console.log(`\n๐Ÿงช Testing window coordinate transformation:`);
73
+ console.log(` Window: "${testWindow.title}"`);
74
+ console.log(` Global position: (${testWindow.x}, ${testWindow.y})`);
75
+ console.log(` Display offset: (${testWindow.x - primaryDisplay.x}, ${testWindow.y - primaryDisplay.y})`);
76
+
77
+ // Calculate what window-relative coordinates should be
78
+ console.log(`\n๐Ÿ“Š Expected coordinate transformation:`);
79
+ console.log(` If cursor at window's top-left (${testWindow.x}, ${testWindow.y}):`);
80
+ console.log(` Should become window-relative: (0, 0)`);
81
+ console.log(` Current transformation would give: (${testWindow.x - testWindow.x}, ${testWindow.y - testWindow.y})`);
82
+
83
+ // Check if there's a Y offset issue
84
+ const expectedZeroY = testWindow.y - testWindow.y; // Should be 0
85
+ if (expectedZeroY !== 0) {
86
+ console.log(` โŒ Y transformation issue detected!`);
87
+ }
88
+
89
+ // Now check what happens with display transformation
90
+ const globalToDisplayX = testWindow.x - primaryDisplay.x;
91
+ const globalToDisplayY = testWindow.y - primaryDisplay.y;
92
+ const displayToWindowX = globalToDisplayX - (testWindow.x - primaryDisplay.x);
93
+ const displayToWindowY = globalToDisplayY - (testWindow.y - primaryDisplay.y);
94
+
95
+ console.log(`\n๐Ÿ”ง Two-step transformation analysis:`);
96
+ console.log(` Step 1 - Global to display-relative: (${globalToDisplayX}, ${globalToDisplayY})`);
97
+ console.log(` Step 2 - Display-relative to window-relative: (${displayToWindowX}, ${displayToWindowY})`);
98
+
99
+ if (displayToWindowX !== 0 || displayToWindowY !== 0) {
100
+ console.log(` โŒ Two-step transformation creates offset!`);
101
+ console.log(` โœ… Should use direct transformation: global - window_global`);
102
+ }
103
+ }
104
+
105
+ } catch (error) {
106
+ console.error('โŒ Test failed:', error.message);
107
+ }
108
+ }
109
+
110
+ testMenuBarOffset().catch(console.error);
@@ -0,0 +1,154 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ async function testPrimaryCursor() {
4
+ console.log('๐Ÿงช Testing cursor coordinates on primary vs secondary displays...');
5
+
6
+ const recorder = new MacRecorder();
7
+
8
+ try {
9
+ // Get available displays and windows
10
+ const displays = await recorder.getDisplays();
11
+ const windows = await recorder.getWindows();
12
+
13
+ console.log('\n๐Ÿ“ฑ Available displays:');
14
+ displays.forEach((display, i) => {
15
+ const type = display.isPrimary ? 'PRIMARY' : 'SECONDARY';
16
+ console.log(` ${i + 1}. ${type} Display ${display.id}: ${display.resolution} at (${display.x}, ${display.y})`);
17
+ });
18
+
19
+ // Find windows on each display
20
+ const primaryDisplay = displays.find(d => d.isPrimary);
21
+ const secondaryDisplay = displays.find(d => !d.isPrimary);
22
+
23
+ console.log('\n๐ŸชŸ Finding windows on each display:');
24
+
25
+ const primaryWindows = windows.filter(window => {
26
+ const windowCenterX = window.x + window.width / 2;
27
+ const windowCenterY = window.y + window.height / 2;
28
+ return (windowCenterX >= primaryDisplay.x &&
29
+ windowCenterX < primaryDisplay.x + primaryDisplay.width &&
30
+ windowCenterY >= primaryDisplay.y &&
31
+ windowCenterY < primaryDisplay.y + primaryDisplay.height);
32
+ });
33
+
34
+ const secondaryWindows = secondaryDisplay ? windows.filter(window => {
35
+ const windowCenterX = window.x + window.width / 2;
36
+ const windowCenterY = window.y + window.height / 2;
37
+ return (windowCenterX >= secondaryDisplay.x &&
38
+ windowCenterX < secondaryDisplay.x + secondaryDisplay.width &&
39
+ windowCenterY >= secondaryDisplay.y &&
40
+ windowCenterY < secondaryDisplay.y + secondaryDisplay.height);
41
+ }) : [];
42
+
43
+ console.log(`\n๐Ÿ“Š Window distribution:`);
44
+ console.log(` Primary display: ${primaryWindows.length} windows`);
45
+ console.log(` Secondary display: ${secondaryWindows.length} windows`);
46
+
47
+ // Test each display type
48
+ for (const displayType of ['primary', 'secondary']) {
49
+ const display = displayType === 'primary' ? primaryDisplay : secondaryDisplay;
50
+ const windowList = displayType === 'primary' ? primaryWindows : secondaryWindows;
51
+
52
+ if (!display) {
53
+ console.log(`\nโš ๏ธ No ${displayType} display found, skipping...`);
54
+ continue;
55
+ }
56
+
57
+ if (windowList.length === 0) {
58
+ console.log(`\nโš ๏ธ No windows found on ${displayType} display, skipping...`);
59
+ continue;
60
+ }
61
+
62
+ const testWindow = windowList[0];
63
+ console.log(`\n๐ŸŽฏ Testing ${displayType.toUpperCase()} display (${display.id}):`);
64
+ console.log(` Display bounds: (${display.x}, ${display.y}) ${display.width}x${display.height}`);
65
+ console.log(` Test window: "${testWindow.title}" at (${testWindow.x}, ${testWindow.y}) ${testWindow.width}x${testWindow.height}`);
66
+
67
+ // Calculate window position relative to display
68
+ const windowRelativeX = testWindow.x - display.x;
69
+ const windowRelativeY = testWindow.y - display.y;
70
+ console.log(` Window relative to display: (${windowRelativeX}, ${windowRelativeY})`);
71
+
72
+ // Test cursor capture
73
+ const outputFile = `./test-output/cursor-${displayType}-${Date.now()}.json`;
74
+
75
+ console.log(`\n๐Ÿ–ฑ๏ธ Starting cursor test for ${displayType} display...`);
76
+ console.log(' Move mouse over the test window for 3 seconds');
77
+
78
+ await recorder.startCursorCapture(outputFile, {
79
+ windowRelative: true,
80
+ windowInfo: {
81
+ x: testWindow.x,
82
+ y: testWindow.y,
83
+ width: testWindow.width,
84
+ height: testWindow.height,
85
+ displayId: display.id,
86
+ targetDisplay: display
87
+ }
88
+ });
89
+
90
+ // Capture for 3 seconds
91
+ await new Promise(resolve => setTimeout(resolve, 3000));
92
+
93
+ await recorder.stopCursorCapture();
94
+
95
+ // Analyze results
96
+ const fs = require('fs');
97
+ if (fs.existsSync(outputFile)) {
98
+ const cursorData = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
99
+
100
+ console.log(`\n๐Ÿ“Š ${displayType.toUpperCase()} Display Results:`);
101
+ console.log(` Captured events: ${cursorData.length}`);
102
+
103
+ if (cursorData.length > 0) {
104
+ const inBounds = cursorData.filter(event =>
105
+ event.x >= 0 && event.x < testWindow.width &&
106
+ event.y >= 0 && event.y < testWindow.height
107
+ );
108
+
109
+ const outOfBounds = cursorData.filter(event =>
110
+ event.x < 0 || event.x >= testWindow.width ||
111
+ event.y < 0 || event.y >= testWindow.height
112
+ );
113
+
114
+ console.log(` Events in bounds: ${inBounds.length}/${cursorData.length} (${Math.round(inBounds.length/cursorData.length*100)}%)`);
115
+ console.log(` Events out of bounds: ${outOfBounds.length}`);
116
+
117
+ if (outOfBounds.length > 0) {
118
+ console.log(` โŒ ISSUE DETECTED on ${displayType} display!`);
119
+ console.log(` Sample out-of-bounds coordinates:`);
120
+ outOfBounds.slice(0, 5).forEach(event => {
121
+ console.log(` (${event.x}, ${event.y}) - should be 0-${testWindow.width}, 0-${testWindow.height}`);
122
+ });
123
+
124
+ // Analyze the offset pattern
125
+ const xOffsets = outOfBounds.map(e => e.x);
126
+ const yOffsets = outOfBounds.map(e => e.y);
127
+ const avgXOffset = xOffsets.reduce((a, b) => a + b, 0) / xOffsets.length;
128
+ const avgYOffset = yOffsets.reduce((a, b) => a + b, 0) / yOffsets.length;
129
+
130
+ console.log(` ๐Ÿ“Š Average coordinate offsets: X=${avgXOffset.toFixed(1)}, Y=${avgYOffset.toFixed(1)}`);
131
+ } else {
132
+ console.log(` โœ… All coordinates within bounds on ${displayType} display`);
133
+ }
134
+
135
+ // Show coordinate samples
136
+ console.log(` Sample coordinates (first 3):`);
137
+ cursorData.slice(0, 3).forEach((event, i) => {
138
+ console.log(` ${i+1}. (${event.x}, ${event.y}) - ${event.coordinateSystem}`);
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ console.log('\n๐Ÿ” Analysis Summary:');
145
+ console.log(' If primary display shows out-of-bounds coordinates,');
146
+ console.log(' but secondary display works correctly, this indicates');
147
+ console.log(' a coordinate transformation issue specific to primary displays.');
148
+
149
+ } catch (error) {
150
+ console.error('โŒ Test failed:', error.message);
151
+ }
152
+ }
153
+
154
+ testPrimaryCursor().catch(console.error);
@@ -0,0 +1,120 @@
1
+ const MacRecorder = require('./index.js');
2
+
3
+ async function testPrimaryFix() {
4
+ console.log('๐Ÿงช Testing fixed cursor coordinates on primary display...');
5
+
6
+ const recorder = new MacRecorder();
7
+
8
+ try {
9
+ const displays = await recorder.getDisplays();
10
+ const windows = await recorder.getWindows();
11
+
12
+ const primaryDisplay = displays.find(d => d.isPrimary);
13
+ const primaryWindows = windows.filter(window => {
14
+ const windowCenterX = window.x + window.width / 2;
15
+ const windowCenterY = window.y + window.height / 2;
16
+ return (windowCenterX >= primaryDisplay.x &&
17
+ windowCenterX < primaryDisplay.x + primaryDisplay.width &&
18
+ windowCenterY >= primaryDisplay.y &&
19
+ windowCenterY < primaryDisplay.y + primaryDisplay.height);
20
+ });
21
+
22
+ if (primaryWindows.length === 0) {
23
+ console.log('โŒ No windows found on primary display');
24
+ return;
25
+ }
26
+
27
+ const testWindow = primaryWindows[0];
28
+
29
+ console.log('\n๐ŸŽฏ Testing PRIMARY display window recording:');
30
+ console.log(` Display: ${primaryDisplay.width}x${primaryDisplay.height} at (${primaryDisplay.x}, ${primaryDisplay.y})`);
31
+ console.log(` Window: "${testWindow.title}" ${testWindow.width}x${testWindow.height} at (${testWindow.x}, ${testWindow.y})`);
32
+
33
+ // Calculate expected coordinates
34
+ console.log('\n๐Ÿ“Š Expected transformation:');
35
+ console.log(` Window top-left (${testWindow.x}, ${testWindow.y}) should become (0, 0)`);
36
+ console.log(` Window bottom-right (${testWindow.x + testWindow.width}, ${testWindow.y + testWindow.height}) should become (${testWindow.width}, ${testWindow.height})`);
37
+
38
+ // Start cursor capture
39
+ const outputFile = `./test-output/primary-fix-test-${Date.now()}.json`;
40
+
41
+ await recorder.startCursorCapture(outputFile, {
42
+ windowRelative: true,
43
+ windowInfo: {
44
+ x: testWindow.x,
45
+ y: testWindow.y,
46
+ width: testWindow.width,
47
+ height: testWindow.height,
48
+ displayId: primaryDisplay.id
49
+ }
50
+ });
51
+
52
+ console.log('\n๐Ÿ–ฑ๏ธ Move mouse over the test window for 5 seconds...');
53
+ console.log(` Try to move cursor to different corners of the window`);
54
+ console.log(` Window boundaries: (0,0) to (${testWindow.width}, ${testWindow.height})`);
55
+
56
+ await new Promise(resolve => setTimeout(resolve, 5000));
57
+
58
+ await recorder.stopCursorCapture();
59
+
60
+ // Analyze results
61
+ const fs = require('fs');
62
+ if (fs.existsSync(outputFile)) {
63
+ const cursorData = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
64
+
65
+ console.log('\n๐Ÿ“Š PRIMARY Display Test Results:');
66
+ console.log(` Captured events: ${cursorData.length}`);
67
+
68
+ if (cursorData.length > 0) {
69
+ const inBounds = cursorData.filter(event =>
70
+ event.x >= 0 && event.x < testWindow.width &&
71
+ event.y >= 0 && event.y < testWindow.height
72
+ );
73
+
74
+ const outOfBounds = cursorData.filter(event =>
75
+ event.x < 0 || event.x >= testWindow.width ||
76
+ event.y < 0 || event.y >= testWindow.height
77
+ );
78
+
79
+ console.log(` Events in bounds: ${inBounds.length}/${cursorData.length} (${Math.round(inBounds.length/cursorData.length*100)}%)`);
80
+
81
+ if (outOfBounds.length > 0) {
82
+ console.log(` โŒ STILL HAVE ISSUES: ${outOfBounds.length} events out of bounds`);
83
+ console.log(` Sample problematic coordinates:`);
84
+ outOfBounds.slice(0, 3).forEach(event => {
85
+ console.log(` (${event.x}, ${event.y}) - should be within 0-${testWindow.width}, 0-${testWindow.height}`);
86
+ });
87
+ } else {
88
+ console.log(` โœ… ALL COORDINATES WITHIN BOUNDS - Primary display fix successful!`);
89
+ }
90
+
91
+ // Show coordinate range
92
+ const xCoords = cursorData.map(e => e.x);
93
+ const yCoords = cursorData.map(e => e.y);
94
+ const minX = Math.min(...xCoords);
95
+ const maxX = Math.max(...xCoords);
96
+ const minY = Math.min(...yCoords);
97
+ const maxY = Math.max(...yCoords);
98
+
99
+ console.log(` Coordinate ranges: X=${minX} to ${maxX}, Y=${minY} to ${maxY}`);
100
+
101
+ // Check coordinate system
102
+ const firstEvent = cursorData[0];
103
+ console.log(` Coordinate system: ${firstEvent.coordinateSystem}`);
104
+
105
+ if (firstEvent.coordinateSystem === "window-relative") {
106
+ console.log(` โœ… Coordinate system correctly set`);
107
+ } else {
108
+ console.log(` โŒ Coordinate system incorrect: ${firstEvent.coordinateSystem}`);
109
+ }
110
+ }
111
+ } else {
112
+ console.log('โŒ No output file created');
113
+ }
114
+
115
+ } catch (error) {
116
+ console.error('โŒ Test failed:', error.message);
117
+ }
118
+ }
119
+
120
+ testPrimaryFix().catch(console.error);