node-mac-recorder 2.17.16 ā 2.17.18
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/.claude/settings.local.json +2 -1
- package/debug-primary-cursor.js +180 -0
- package/index.js +4 -19
- package/package.json +1 -1
- package/src/cursor_tracker.mm +223 -60
- package/test-cursor-types.js +117 -0
- package/test-display-coordinates.js +72 -0
- package/test-menubar-offset.js +110 -0
- package/test-primary-cursor.js +154 -0
- package/test-primary-fix.js +120 -0
- package/test-output/cursor-test-window-1758264693010.json +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const MacRecorder = require('./index.js');
|
|
2
|
+
|
|
3
|
+
async function debugPrimaryCursor() {
|
|
4
|
+
console.log('š Detailed debugging of primary display cursor coordinates...');
|
|
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
|
+
console.log('\nš± Primary Display:');
|
|
14
|
+
console.log(` Bounds: (${primaryDisplay.x}, ${primaryDisplay.y}) ${primaryDisplay.width}x${primaryDisplay.height}`);
|
|
15
|
+
|
|
16
|
+
// Find a window on primary display
|
|
17
|
+
const primaryWindows = windows.filter(window => {
|
|
18
|
+
const windowCenterX = window.x + window.width / 2;
|
|
19
|
+
const windowCenterY = window.y + window.height / 2;
|
|
20
|
+
return (windowCenterX >= primaryDisplay.x &&
|
|
21
|
+
windowCenterX < primaryDisplay.x + primaryDisplay.width &&
|
|
22
|
+
windowCenterY >= primaryDisplay.y &&
|
|
23
|
+
windowCenterY < primaryDisplay.y + primaryDisplay.height);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (primaryWindows.length === 0) {
|
|
27
|
+
console.log('ā No windows found on primary display');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const testWindow = primaryWindows[0];
|
|
32
|
+
console.log('\nšŖ Test Window:');
|
|
33
|
+
console.log(` Title: "${testWindow.title}"`);
|
|
34
|
+
console.log(` Global position: (${testWindow.x}, ${testWindow.y})`);
|
|
35
|
+
console.log(` Size: ${testWindow.width}x${testWindow.height}`);
|
|
36
|
+
console.log(` Global bounds: (${testWindow.x}, ${testWindow.y}) to (${testWindow.x + testWindow.width}, ${testWindow.y + testWindow.height})`);
|
|
37
|
+
|
|
38
|
+
// Manual cursor position check
|
|
39
|
+
console.log('\nš±ļø Current cursor position:');
|
|
40
|
+
const currentPos = recorder.getCursorPosition();
|
|
41
|
+
console.log(` Global cursor: (${currentPos.x}, ${currentPos.y})`);
|
|
42
|
+
|
|
43
|
+
// Manual transformation calculation
|
|
44
|
+
console.log('\nš§® Manual transformation calculation:');
|
|
45
|
+
const manualWindowRelativeX = currentPos.x - testWindow.x;
|
|
46
|
+
const manualWindowRelativeY = currentPos.y - testWindow.y;
|
|
47
|
+
console.log(` Manual window-relative: (${manualWindowRelativeX}, ${manualWindowRelativeY})`);
|
|
48
|
+
|
|
49
|
+
const isInWindow = (manualWindowRelativeX >= 0 && manualWindowRelativeX < testWindow.width &&
|
|
50
|
+
manualWindowRelativeY >= 0 && manualWindowRelativeY < testWindow.height);
|
|
51
|
+
console.log(` Is cursor in window? ${isInWindow ? 'ā
YES' : 'ā NO'}`);
|
|
52
|
+
|
|
53
|
+
if (!isInWindow) {
|
|
54
|
+
console.log('\nā ļø Move cursor to the test window for accurate testing...');
|
|
55
|
+
console.log(' Waiting 3 seconds for you to move cursor to the window...');
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
57
|
+
|
|
58
|
+
const newPos = recorder.getCursorPosition();
|
|
59
|
+
console.log(`\nš±ļø New cursor position: (${newPos.x}, ${newPos.y})`);
|
|
60
|
+
const newManualX = newPos.x - testWindow.x;
|
|
61
|
+
const newManualY = newPos.y - testWindow.y;
|
|
62
|
+
console.log(` New manual window-relative: (${newManualX}, ${newManualY})`);
|
|
63
|
+
|
|
64
|
+
const newIsInWindow = (newManualX >= 0 && newManualX < testWindow.width &&
|
|
65
|
+
newManualY >= 0 && newManualY < testWindow.height);
|
|
66
|
+
console.log(` Is cursor in window now? ${newIsInWindow ? 'ā
YES' : 'ā NO'}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Now test with cursor capture to see what actually gets recorded
|
|
70
|
+
console.log('\nš¬ Testing actual cursor capture...');
|
|
71
|
+
|
|
72
|
+
// Create a custom logger to see what coordinates are being processed
|
|
73
|
+
const originalLog = console.log;
|
|
74
|
+
const capturedLogs = [];
|
|
75
|
+
|
|
76
|
+
// Temporarily override console.log to capture coordinate logs
|
|
77
|
+
console.log = (...args) => {
|
|
78
|
+
const message = args.join(' ');
|
|
79
|
+
if (message.includes('cursor') || message.includes('position') || message.includes('coordinate')) {
|
|
80
|
+
capturedLogs.push(message);
|
|
81
|
+
}
|
|
82
|
+
originalLog(...args);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const outputFile = `./test-output/debug-primary-${Date.now()}.json`;
|
|
86
|
+
|
|
87
|
+
// Start capture with detailed logging
|
|
88
|
+
await recorder.startCursorCapture(outputFile, {
|
|
89
|
+
windowRelative: true,
|
|
90
|
+
windowInfo: {
|
|
91
|
+
x: testWindow.x,
|
|
92
|
+
y: testWindow.y,
|
|
93
|
+
width: testWindow.width,
|
|
94
|
+
height: testWindow.height,
|
|
95
|
+
displayId: primaryDisplay.id
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log('\nš Cursor capture started. Move mouse in the window for 3 seconds...');
|
|
100
|
+
console.log(` Expected window bounds: (0, 0) to (${testWindow.width}, ${testWindow.height})`);
|
|
101
|
+
|
|
102
|
+
// Capture for 3 seconds with manual checking
|
|
103
|
+
for (let i = 0; i < 6; i++) {
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
105
|
+
const pos = recorder.getCursorPosition();
|
|
106
|
+
const windowRelX = pos.x - testWindow.x;
|
|
107
|
+
const windowRelY = pos.y - testWindow.y;
|
|
108
|
+
|
|
109
|
+
console.log(` Sample ${i+1}: Global(${pos.x}, ${pos.y}) ā Window-rel(${windowRelX}, ${windowRelY})`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await recorder.stopCursorCapture();
|
|
113
|
+
|
|
114
|
+
// Restore console.log
|
|
115
|
+
console.log = originalLog;
|
|
116
|
+
|
|
117
|
+
// Analyze results
|
|
118
|
+
const fs = require('fs');
|
|
119
|
+
if (fs.existsSync(outputFile)) {
|
|
120
|
+
const cursorData = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
|
|
121
|
+
|
|
122
|
+
console.log('\nš Analysis Results:');
|
|
123
|
+
console.log(` Captured events: ${cursorData.length}`);
|
|
124
|
+
|
|
125
|
+
if (cursorData.length > 0) {
|
|
126
|
+
console.log('\nš Sample recorded coordinates:');
|
|
127
|
+
cursorData.slice(0, 5).forEach((event, i) => {
|
|
128
|
+
console.log(` Event ${i+1}: (${event.x}, ${event.y}) - ${event.coordinateSystem}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Check bounds
|
|
132
|
+
const inBounds = cursorData.filter(event =>
|
|
133
|
+
event.x >= 0 && event.x < testWindow.width &&
|
|
134
|
+
event.y >= 0 && event.y < testWindow.height
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const outOfBounds = cursorData.filter(event =>
|
|
138
|
+
event.x < 0 || event.x >= testWindow.width ||
|
|
139
|
+
event.y < 0 || event.y >= testWindow.height
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
console.log(`\nā
Events in bounds: ${inBounds.length}/${cursorData.length} (${Math.round(inBounds.length/cursorData.length*100)}%)`);
|
|
143
|
+
|
|
144
|
+
if (outOfBounds.length > 0) {
|
|
145
|
+
console.log(`ā Events out of bounds: ${outOfBounds.length}`);
|
|
146
|
+
console.log(' Problem coordinates:');
|
|
147
|
+
outOfBounds.slice(0, 3).forEach(event => {
|
|
148
|
+
console.log(` (${event.x}, ${event.y}) - Expected: 0-${testWindow.width}, 0-${testWindow.height}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Calculate offset pattern
|
|
152
|
+
const xOffsets = outOfBounds.map(e => e.x - (e.x >= 0 ? 0 : testWindow.width));
|
|
153
|
+
const yOffsets = outOfBounds.map(e => e.y - (e.y >= 0 ? 0 : testWindow.height));
|
|
154
|
+
|
|
155
|
+
console.log(`\nš Offset analysis:`);
|
|
156
|
+
console.log(` X offset pattern: ${xOffsets.slice(0, 3).join(', ')}`);
|
|
157
|
+
console.log(` Y offset pattern: ${yOffsets.slice(0, 3).join(', ')}`);
|
|
158
|
+
} else {
|
|
159
|
+
console.log('ā
All coordinates are within bounds!');
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log('ā No events captured - cursor might not be in window or transformation is dropping events');
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
console.log('ā No output file created');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('\nš Captured system logs:');
|
|
169
|
+
if (capturedLogs.length > 0) {
|
|
170
|
+
capturedLogs.forEach(log => console.log(` ${log}`));
|
|
171
|
+
} else {
|
|
172
|
+
console.log(' No relevant coordinate logs captured');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('ā Debug failed:', error.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
debugPrimaryCursor().catch(console.error);
|
package/index.js
CHANGED
|
@@ -830,25 +830,10 @@ class MacRecorder extends EventEmitter {
|
|
|
830
830
|
|
|
831
831
|
if (this.cursorDisplayInfo) {
|
|
832
832
|
if (this.cursorDisplayInfo.windowRelative) {
|
|
833
|
-
// Window recording:
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
// If we have target display info, first convert global coords to display-relative
|
|
838
|
-
if (this.cursorDisplayInfo.targetDisplay) {
|
|
839
|
-
adjustedX = position.x - this.cursorDisplayInfo.targetDisplay.x;
|
|
840
|
-
adjustedY = position.y - this.cursorDisplayInfo.targetDisplay.y;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Then convert display-relative coords to window-relative
|
|
844
|
-
if (this.cursorDisplayInfo.windowInfo && this.cursorDisplayInfo.windowInfo.displayOffsetX !== undefined) {
|
|
845
|
-
x = adjustedX - this.cursorDisplayInfo.windowInfo.displayOffsetX;
|
|
846
|
-
y = adjustedY - this.cursorDisplayInfo.windowInfo.displayOffsetY;
|
|
847
|
-
} else {
|
|
848
|
-
// Fallback: direct window offset from global coordinates
|
|
849
|
-
x = position.x - this.cursorDisplayInfo.x;
|
|
850
|
-
y = position.y - this.cursorDisplayInfo.y;
|
|
851
|
-
}
|
|
833
|
+
// Window recording: Use direct global-to-window transformation
|
|
834
|
+
// This works correctly for both primary and secondary displays
|
|
835
|
+
x = position.x - this.cursorDisplayInfo.x;
|
|
836
|
+
y = position.y - this.cursorDisplayInfo.y;
|
|
852
837
|
|
|
853
838
|
coordinateSystem = "window-relative";
|
|
854
839
|
|
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -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
|
-
//
|
|
47
|
-
static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.
|
|
48
|
-
static const int CURSOR_CONFIRMATION_COUNT =
|
|
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
|
|
133
|
+
// Enhanced cursor type detection with multiple fallbacks and better image analysis
|
|
61
134
|
NSString* detectCursorTypeFromNSCursor() {
|
|
62
135
|
@try {
|
|
63
|
-
|
|
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
|
|
64
143
|
if (!currentCursor) {
|
|
65
|
-
|
|
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
|
+
}
|
|
66
159
|
}
|
|
67
160
|
|
|
68
|
-
|
|
161
|
+
if (!currentCursor) {
|
|
162
|
+
return nil; // No cursor available, use contextual detection
|
|
163
|
+
}
|
|
164
|
+
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
143
|
-
|
|
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(
|
|
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:
|
|
452
|
+
// Layer 3: Enhanced multi-layer priority detection
|
|
284
453
|
NSString *detectedCursorType = @"default";
|
|
285
454
|
|
|
286
|
-
// Priority logic -
|
|
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.
|
|
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
|
-
|
|
463
|
+
[nsCursorType isEqualToString:@"pointer"] ||
|
|
464
|
+
[nsCursorType hasSuffix:@"resize"])) {
|
|
465
|
+
// Trust NSCursor for these specific types
|
|
296
466
|
detectedCursorType = nsCursorType;
|
|
297
467
|
}
|
|
298
|
-
// 3.
|
|
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) {
|
|
474
|
+
detectedCursorType = nsCursorType;
|
|
475
|
+
}
|
|
476
|
+
// 5. Everything else defaults to default
|
|
299
477
|
else {
|
|
300
478
|
detectedCursorType = @"default";
|
|
301
479
|
}
|
|
302
480
|
|
|
303
|
-
// Layer 4:
|
|
481
|
+
// Layer 4: Ultra-responsive detection with minimal stabilization
|
|
304
482
|
NSString *finalCursorType = detectedCursorType;
|
|
305
483
|
|
|
306
|
-
//
|
|
484
|
+
// Ultra-fast response - immediate updates for all cursor types
|
|
307
485
|
if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
//
|
|
341
|
-
|
|
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
|
|
@@ -480,20 +636,27 @@ void cursorTimerCallback() {
|
|
|
480
636
|
// Apply DPR scaling correction for Retina displays
|
|
481
637
|
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
482
638
|
CGPoint location = rawLocation;
|
|
483
|
-
|
|
639
|
+
|
|
484
640
|
if (scalingInfo) {
|
|
485
641
|
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
486
642
|
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
487
|
-
|
|
643
|
+
|
|
488
644
|
// Keep logical coordinates - no scaling needed here
|
|
489
645
|
location = rawLocation;
|
|
490
646
|
}
|
|
491
|
-
|
|
647
|
+
|
|
492
648
|
NSDate *currentDate = [NSDate date];
|
|
493
649
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
494
650
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
495
651
|
NSString *cursorType = getCursorType();
|
|
496
|
-
|
|
652
|
+
|
|
653
|
+
// CRITICAL: Apply coordinate transformation if needed
|
|
654
|
+
// This should match the transformation logic in index.js cursorTimerCallback
|
|
655
|
+
|
|
656
|
+
// Use transformed coordinates from the JavaScript layer if available
|
|
657
|
+
// The transformation is handled in index.js, so we need to get the transformed position
|
|
658
|
+
// For now, we'll write the raw coordinates and let the JS layer handle transformation
|
|
659
|
+
|
|
497
660
|
// Cursor data oluÅtur
|
|
498
661
|
NSDictionary *cursorInfo = @{
|
|
499
662
|
@"x": @((int)location.x),
|
|
@@ -503,7 +666,7 @@ void cursorTimerCallback() {
|
|
|
503
666
|
@"cursorType": cursorType,
|
|
504
667
|
@"type": @"move"
|
|
505
668
|
};
|
|
506
|
-
|
|
669
|
+
|
|
507
670
|
// Direkt dosyaya yaz
|
|
508
671
|
writeToFile(cursorInfo);
|
|
509
672
|
}
|
|
@@ -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);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[{"x":758,"y":1089,"timestamp":37,"unixTimeMs":1758264693047,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":758,"y":1089,"timestamp":80,"unixTimeMs":1758264693090,"cursorType":"default","type":"mousedown","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":758,"y":1089,"timestamp":99,"unixTimeMs":1758264693109,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":758,"y":1089,"timestamp":180,"unixTimeMs":1758264693190,"cursorType":"default","type":"mouseup","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":758,"y":1089,"timestamp":201,"unixTimeMs":1758264693211,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":774,"y":1071,"timestamp":1610,"unixTimeMs":1758264694620,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":818,"y":1030,"timestamp":1631,"unixTimeMs":1758264694641,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":893,"y":972,"timestamp":1654,"unixTimeMs":1758264694664,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1015,"y":886,"timestamp":1677,"unixTimeMs":1758264694687,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1087,"y":843,"timestamp":1696,"unixTimeMs":1758264694706,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1233,"y":793,"timestamp":1714,"unixTimeMs":1758264694724,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1301,"y":776,"timestamp":1733,"unixTimeMs":1758264694743,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1360,"y":765,"timestamp":1755,"unixTimeMs":1758264694765,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1366,"y":764,"timestamp":1774,"unixTimeMs":1758264694784,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1363,"y":751,"timestamp":1814,"unixTimeMs":1758264694824,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1343,"y":709,"timestamp":1838,"unixTimeMs":1758264694848,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1335,"y":691,"timestamp":1855,"unixTimeMs":1758264694865,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1271,"y":583,"timestamp":1878,"unixTimeMs":1758264694888,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1250,"y":558,"timestamp":1898,"unixTimeMs":1758264694908,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1242,"y":552,"timestamp":1917,"unixTimeMs":1758264694927,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1238,"y":552,"timestamp":1937,"unixTimeMs":1758264694947,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1234,"y":554,"timestamp":1959,"unixTimeMs":1758264694969,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1233,"y":556,"timestamp":2207,"unixTimeMs":1758264695217,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1232,"y":559,"timestamp":2231,"unixTimeMs":1758264695241,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1232,"y":561,"timestamp":2252,"unixTimeMs":1758264695262,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1231,"y":565,"timestamp":2272,"unixTimeMs":1758264695282,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1231,"y":567,"timestamp":2290,"unixTimeMs":1758264695300,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1231,"y":595,"timestamp":2311,"unixTimeMs":1758264695321,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1236,"y":629,"timestamp":2330,"unixTimeMs":1758264695341,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1242,"y":647,"timestamp":2352,"unixTimeMs":1758264695362,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1251,"y":670,"timestamp":2369,"unixTimeMs":1758264695379,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1259,"y":702,"timestamp":2391,"unixTimeMs":1758264695401,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1265,"y":746,"timestamp":2409,"unixTimeMs":1758264695419,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1276,"y":763,"timestamp":2430,"unixTimeMs":1758264695440,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1272,"y":760,"timestamp":2985,"unixTimeMs":1758264695995,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1270,"y":758,"timestamp":3005,"unixTimeMs":1758264696015,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1269,"y":756,"timestamp":3047,"unixTimeMs":1758264696057,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1268,"y":754,"timestamp":3089,"unixTimeMs":1758264696099,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1268,"y":752,"timestamp":3132,"unixTimeMs":1758264696142,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1267,"y":750,"timestamp":3173,"unixTimeMs":1758264696183,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1266,"y":747,"timestamp":3215,"unixTimeMs":1758264696225,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1265,"y":745,"timestamp":3238,"unixTimeMs":1758264696248,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1263,"y":742,"timestamp":3278,"unixTimeMs":1758264696288,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1264,"y":740,"timestamp":3482,"unixTimeMs":1758264696492,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1266,"y":739,"timestamp":3522,"unixTimeMs":1758264696532,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1268,"y":738,"timestamp":3541,"unixTimeMs":1758264696551,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1277,"y":731,"timestamp":3563,"unixTimeMs":1758264696573,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1279,"y":730,"timestamp":3583,"unixTimeMs":1758264696593,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1281,"y":729,"timestamp":3688,"unixTimeMs":1758264696698,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1283,"y":727,"timestamp":3708,"unixTimeMs":1758264696718,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1286,"y":718,"timestamp":3873,"unixTimeMs":1758264696883,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1287,"y":713,"timestamp":3893,"unixTimeMs":1758264696903,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1289,"y":713,"timestamp":4204,"unixTimeMs":1758264697214,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}},{"x":1291,"y":713,"timestamp":4330,"unixTimeMs":1758264697340,"cursorType":"default","type":"move","coordinateSystem":"window-relative","windowInfo":{"width":3440,"height":1440,"displayId":null}}]
|