node-mac-recorder 2.17.11 → 2.17.13

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.
@@ -4,7 +4,9 @@
4
4
  "Bash(node:*)",
5
5
  "Bash(chmod:*)",
6
6
  "Bash(cat:*)",
7
- "Bash(git checkout:*)"
7
+ "Bash(git checkout:*)",
8
+ "WebSearch",
9
+ "WebFetch(domain:stackoverflow.com)"
8
10
  ],
9
11
  "deny": [],
10
12
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.17.11",
3
+ "version": "2.17.13",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -35,9 +35,17 @@ NSDictionary* getDisplayScalingInfo(CGPoint globalPoint);
35
35
 
36
36
  static CursorTimerTarget *g_timerTarget = nil;
37
37
 
38
- // Global cursor state tracking
38
+ // Enhanced cursor state tracking with stability
39
39
  static NSString *g_lastDetectedCursorType = nil;
40
+ static NSString *g_stableCursorType = @"default";
40
41
  static int g_cursorTypeCounter = 0;
42
+ static NSTimeInterval g_lastCursorCheckTime = 0;
43
+ static int g_sameCursorDetectionCount = 0;
44
+ static NSString *g_pendingCursorType = nil;
45
+
46
+ // Cursor stability constants
47
+ static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.1; // 100ms
48
+ static const int CURSOR_CONFIRMATION_COUNT = 2; // Need 2 consecutive detections
41
49
 
42
50
  // Mouse button state tracking
43
51
  static bool g_leftMouseDown = false;
@@ -49,255 +57,246 @@ static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEv
49
57
  return event;
50
58
  }
51
59
 
52
- // Cursor type detection helper - sistem genelindeki cursor type'ı al
60
+ // Enhanced cursor type detection with better NSCursor analysis
61
+ NSString* detectCursorTypeFromNSCursor() {
62
+ @try {
63
+ NSCursor *currentCursor = [NSCursor currentSystemCursor];
64
+ if (!currentCursor) {
65
+ return nil; // Return nil to indicate we should try other methods
66
+ }
67
+
68
+ // Compare with known system cursors using identity comparison
69
+ if (currentCursor == [NSCursor arrowCursor]) {
70
+ return @"default";
71
+ } else if (currentCursor == [NSCursor IBeamCursor]) {
72
+ return @"text";
73
+ } else if (currentCursor == [NSCursor pointingHandCursor]) {
74
+ return @"pointer";
75
+ } else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
76
+ return @"col-resize";
77
+ } else if (currentCursor == [NSCursor resizeUpDownCursor]) {
78
+ return @"ns-resize";
79
+ } else if (currentCursor == [NSCursor crosshairCursor]) {
80
+ return @"crosshair";
81
+ } else if (currentCursor == [NSCursor openHandCursor]) {
82
+ return @"grab";
83
+ } else if (currentCursor == [NSCursor closedHandCursor]) {
84
+ return @"grabbing";
85
+ } else if (currentCursor == [NSCursor operationNotAllowedCursor]) {
86
+ return @"not-allowed";
87
+ }
88
+
89
+ // Check for additional resize cursors using image analysis
90
+ NSImage *cursorImage = [currentCursor image];
91
+ if (cursorImage) {
92
+ NSSize imageSize = [cursorImage size];
93
+ NSPoint hotSpot = [currentCursor hotSpot];
94
+
95
+ // Analyze cursor image to detect resize cursors
96
+ if (imageSize.width > 10 && imageSize.height > 10) {
97
+ // Check for diagonal resize cursors (corner resize)
98
+ if (imageSize.width >= 15 && imageSize.height >= 15) {
99
+ // These are likely diagonal resize cursors
100
+ // hotSpot can help distinguish between nwse and nesw
101
+ if (hotSpot.x < imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
102
+ return @"nwse-resize"; // northwest-southeast
103
+ } else if (hotSpot.x > imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
104
+ return @"nesw-resize"; // northeast-southwest
105
+ }
106
+ }
107
+
108
+ // Check for horizontal/vertical resize cursors
109
+ if (imageSize.width > imageSize.height + 5) {
110
+ return @"col-resize"; // horizontal resize
111
+ } else if (imageSize.height > imageSize.width + 5) {
112
+ return @"ns-resize"; // vertical resize
113
+ }
114
+ }
115
+
116
+ // Text cursors typically have I-beam shape (narrow width)
117
+ if (imageSize.width < 8 && imageSize.height > 15) {
118
+ return @"text";
119
+ }
120
+ }
121
+
122
+ // Return nil to indicate we should use contextual detection
123
+ return nil;
124
+ } @catch (NSException *exception) {
125
+ return nil;
126
+ }
127
+ }
128
+
129
+ // Improved cursor type detection with stability and multi-layer approach
53
130
  NSString* getCursorType() {
54
131
  @autoreleasepool {
55
132
  g_cursorTypeCounter++;
56
-
133
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
134
+
57
135
  @try {
58
- // ACCESSIBILITY API BASED CURSOR DETECTION
59
- // Determine cursor type based on the UI element under the cursor
136
+ // Layer 1: Fast NSCursor detection (high confidence cases)
137
+ NSString *nsCursorType = detectCursorTypeFromNSCursor();
138
+
139
+ // Layer 2: Contextual detection using Accessibility API
140
+ NSString *contextualCursorType = nil;
60
141
 
61
142
  CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
62
143
  AXUIElementRef systemWide = AXUIElementCreateSystemWide();
63
144
  AXUIElementRef elementAtPosition = NULL;
64
145
  AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
65
146
 
66
- NSString *cursorType = @"default"; // Default fallback
67
-
68
147
  if (error == kAXErrorSuccess && elementAtPosition) {
69
148
  CFStringRef role = NULL;
70
149
  error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
71
150
 
72
151
  if (error == kAXErrorSuccess && role) {
73
152
  NSString *elementRole = (__bridge_transfer NSString*)role;
74
- NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
75
153
 
76
- // TEXT CURSORS
154
+ // TEXT CURSORS - high priority
77
155
  if ([elementRole isEqualToString:@"AXTextField"] ||
78
156
  [elementRole isEqualToString:@"AXTextArea"] ||
79
- [elementRole isEqualToString:@"AXStaticText"] ||
80
157
  [elementRole isEqualToString:@"AXSearchField"]) {
81
- cursorType = @"text";
158
+ contextualCursorType = @"text";
82
159
  }
83
- // POINTER CURSORS (clickable elements)
160
+ // POINTER CURSORS - only for interactive elements
84
161
  else if ([elementRole isEqualToString:@"AXLink"] ||
85
162
  [elementRole isEqualToString:@"AXButton"] ||
86
163
  [elementRole isEqualToString:@"AXMenuItem"] ||
87
164
  [elementRole isEqualToString:@"AXRadioButton"] ||
88
- [elementRole isEqualToString:@"AXCheckBox"] ||
89
- [elementRole isEqualToString:@"AXPopUpButton"] ||
90
- [elementRole isEqualToString:@"AXTab"]) {
91
- cursorType = @"pointer";
165
+ [elementRole isEqualToString:@"AXCheckBox"]) {
166
+ contextualCursorType = @"pointer";
92
167
  }
93
- // GRAB CURSORS (draggable elements)
94
- else if ([elementRole isEqualToString:@"AXImage"] ||
95
- [elementRole isEqualToString:@"AXGroup"]) {
96
- // Check if element is draggable
97
- CFBooleanRef draggable = NULL;
98
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
99
- if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
100
- cursorType = @"grab";
101
- } else {
102
- cursorType = @"default";
168
+ // WINDOW BORDER RESIZE - critical for resize detection
169
+ else if ([elementRole isEqualToString:@"AXWindow"]) {
170
+ CFTypeRef position = NULL;
171
+ CFTypeRef size = NULL;
172
+ AXError posErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
173
+ AXError sizeErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
174
+
175
+ if (posErr == kAXErrorSuccess && sizeErr == kAXErrorSuccess && position && size) {
176
+ CGPoint windowPos;
177
+ CGSize windowSize;
178
+ AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
179
+ AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
180
+
181
+ CGFloat x = cursorPos.x - windowPos.x;
182
+ CGFloat y = cursorPos.y - windowPos.y;
183
+ CGFloat w = windowSize.width;
184
+ CGFloat h = windowSize.height;
185
+ CGFloat edge = 5.0; // 5px edge detection
186
+
187
+ // Corner resize detection
188
+ if (x <= edge && y <= edge) {
189
+ contextualCursorType = @"nwse-resize";
190
+ }
191
+ else if (x >= w-edge && y <= edge) {
192
+ contextualCursorType = @"nesw-resize";
193
+ }
194
+ else if (x <= edge && y >= h-edge) {
195
+ contextualCursorType = @"nesw-resize";
196
+ }
197
+ else if (x >= w-edge && y >= h-edge) {
198
+ contextualCursorType = @"nwse-resize";
199
+ }
200
+ // Edge resize detection
201
+ else if (x <= edge || x >= w-edge) {
202
+ contextualCursorType = @"col-resize";
203
+ }
204
+ else if (y <= edge || y >= h-edge) {
205
+ contextualCursorType = @"ns-resize";
206
+ }
207
+
208
+ if (position) CFRelease(position);
209
+ if (size) CFRelease(size);
103
210
  }
104
211
  }
105
- // PROGRESS CURSORS (loading/busy elements)
106
- else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
107
- [elementRole isEqualToString:@"AXBusyIndicator"]) {
108
- cursorType = @"progress";
109
- }
110
- // HELP CURSORS (help buttons/tooltips)
111
- else if ([elementRole isEqualToString:@"AXHelpTag"] ||
112
- [elementRole isEqualToString:@"AXTooltip"]) {
113
- cursorType = @"help";
114
- }
115
- // RESIZE CURSORS - sadece AXSplitter için
212
+ // SPLITTER RESIZE
116
213
  else if ([elementRole isEqualToString:@"AXSplitter"]) {
117
- // Get splitter orientation to determine resize direction
118
214
  CFStringRef orientation = NULL;
119
215
  error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
120
216
  if (error == kAXErrorSuccess && orientation) {
121
217
  NSString *orientationStr = (__bridge_transfer NSString*)orientation;
122
218
  if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
123
- cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
219
+ contextualCursorType = @"ns-resize";
124
220
  } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
125
- cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
126
- } else {
127
- cursorType = @"default"; // Bilinmeyen orientation
221
+ contextualCursorType = @"col-resize";
128
222
  }
129
- } else {
130
- cursorType = @"default"; // Orientation alınamazsa default
131
223
  }
132
224
  }
133
- // SCROLL CURSORS - hep default olsun, all-scroll görünmesin
134
- else if ([elementRole isEqualToString:@"AXScrollBar"]) {
135
- cursorType = @"default"; // ScrollBar'lar için de default
136
- }
137
- // AXScrollArea - hep default
138
- else if ([elementRole isEqualToString:@"AXScrollArea"]) {
139
- cursorType = @"default"; // ScrollArea her zaman default
140
- }
141
- // CROSSHAIR CURSORS (drawing/selection tools)
142
- else if ([elementRole isEqualToString:@"AXCanvas"] ||
143
- [elementRole isEqualToString:@"AXDrawingArea"]) {
144
- cursorType = @"crosshair";
145
- }
146
- // ZOOM CURSORS (zoom controls)
147
- else if ([elementRole isEqualToString:@"AXZoomButton"]) {
148
- cursorType = @"zoom-in";
225
+ // PROGRESS INDICATORS
226
+ else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
227
+ contextualCursorType = @"progress";
149
228
  }
150
- // NOT-ALLOWED CURSORS (disabled elements)
151
- else if ([elementRole isEqualToString:@"AXStaticText"] ||
152
- [elementRole isEqualToString:@"AXGroup"]) {
153
- // Check if element is disabled/readonly
154
- CFBooleanRef enabled = NULL;
155
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
156
- if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
157
- cursorType = @"not-allowed";
158
- }
159
- }
160
- // WINDOW BORDER RESIZE - sadece pencere kenarlarında
161
- else if ([elementRole isEqualToString:@"AXWindow"]) {
162
- // Check window attributes to see if it's resizable
163
- CFBooleanRef resizable = NULL;
164
- AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
165
-
166
- // Sadece resize edilebilir pencereler için cursor değişimi
167
- if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
168
- CFTypeRef position = NULL;
169
- CFTypeRef size = NULL;
170
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
171
- AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
172
-
173
- if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
174
- CGPoint windowPos;
175
- CGSize windowSize;
176
- AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
177
- AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
178
-
179
- CGFloat x = cursorPos.x - windowPos.x;
180
- CGFloat y = cursorPos.y - windowPos.y;
181
- CGFloat w = windowSize.width;
182
- CGFloat h = windowSize.height;
183
- CGFloat edge = 3.0; // Daha küçük edge detection (3px)
184
-
185
- // Sadece çok kenar köşelerde resize cursor'ı göster
186
- BOOL isOnBorder = NO;
187
-
188
- // Corner resize detection - çok dar alanda, doğru açılar
189
- if (x <= edge && y <= edge) {
190
- cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
191
- isOnBorder = YES;
192
- }
193
- else if (x >= w-edge && y <= edge) {
194
- cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
195
- isOnBorder = YES;
196
- }
197
- else if (x <= edge && y >= h-edge) {
198
- cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
199
- isOnBorder = YES;
200
- }
201
- else if (x >= w-edge && y >= h-edge) {
202
- cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
203
- isOnBorder = YES;
204
- }
205
- // Edge resize detection - sadece çok kenarlarda
206
- else if (x <= edge && y > edge && y < h-edge) {
207
- cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
208
- isOnBorder = YES;
209
- }
210
- else if (x >= w-edge && y > edge && y < h-edge) {
211
- cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
212
- isOnBorder = YES;
213
- }
214
- else if (y <= edge && x > edge && x < w-edge) {
215
- cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
216
- isOnBorder = YES;
217
- }
218
- else if (y >= h-edge && x > edge && x < w-edge) {
219
- cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
220
- isOnBorder = YES;
221
- }
222
-
223
- // Eğer border'da değilse default
224
- if (!isOnBorder) {
225
- cursorType = @"default";
226
- }
227
-
228
- if (position) CFRelease(position);
229
- if (size) CFRelease(size);
230
- } else {
231
- cursorType = @"default";
232
- }
233
- } else {
234
- cursorType = @"default";
235
- }
236
- }
237
- // HER DURUM İÇİN DEFAULT FALLBACK
229
+ // OTHER ELEMENTS - be conservative, don't assume pointer
238
230
  else {
239
- // Bilinmeyen elementler için her zaman default
240
- cursorType = @"default";
241
- }
242
-
243
- // Check subroles for additional context
244
- CFStringRef subrole = NULL;
245
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
246
- if (error == kAXErrorSuccess && subrole) {
247
- NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
248
- NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
249
-
250
- // Subrole override'ları - sadece çok spesifik durumlar için
251
- if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
252
- [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
253
- [elementSubrole isEqualToString:@"AXZoomButton"] ||
254
- [elementSubrole isEqualToString:@"AXToolbarButton"]) {
255
- cursorType = @"pointer";
256
- }
257
- // Copy/alias subroles - sadece bu durumlar için override
258
- else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
259
- [elementSubrole isEqualToString:@"AXDropTarget"]) {
260
- cursorType = @"copy";
261
- }
262
- // Alias/shortcut subroles
263
- else if ([elementSubrole isEqualToString:@"AXAlias"] ||
264
- [elementSubrole isEqualToString:@"AXShortcut"]) {
265
- cursorType = @"alias";
266
- }
267
- // Grabbing state (being dragged) - sadece gerçek drag sırasında
268
- else if ([elementSubrole isEqualToString:@"AXDragging"] ||
269
- [elementSubrole isEqualToString:@"AXMoving"]) {
270
- cursorType = @"grabbing";
271
- }
272
- // Zoom controls - sadece spesifik zoom butonları için
273
- else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
274
- cursorType = @"zoom-in";
275
- }
276
- else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
277
- cursorType = @"zoom-out";
278
- }
279
- // Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
231
+ // Don't override NSCursor for unknown elements
232
+ contextualCursorType = nil;
280
233
  }
281
234
  }
282
-
283
235
  CFRelease(elementAtPosition);
284
236
  }
237
+ if (systemWide) CFRelease(systemWide);
238
+
239
+ // Layer 3: Intelligent fusion of NSCursor and contextual results
240
+ NSString *detectedCursorType = @"default";
241
+
242
+ // Priority logic:
243
+ // 1. If contextual gives resize cursor, always use it (resize has highest priority)
244
+ if (contextualCursorType != nil &&
245
+ ([contextualCursorType hasSuffix:@"resize"] ||
246
+ [contextualCursorType isEqualToString:@"col-resize"] ||
247
+ [contextualCursorType isEqualToString:@"ns-resize"])) {
248
+ detectedCursorType = contextualCursorType;
249
+ }
250
+ // 2. If NSCursor gives a definitive answer and no resize context, use it
251
+ else if (nsCursorType != nil) {
252
+ detectedCursorType = nsCursorType;
253
+ }
254
+ // 3. If NSCursor is nil/unknown, but contextual is available, use contextual
255
+ else if (contextualCursorType != nil) {
256
+ detectedCursorType = contextualCursorType;
257
+ }
258
+
259
+ // Layer 4: Stability filtering to prevent oscillation
285
260
 
286
- if (systemWide) {
287
- CFRelease(systemWide);
261
+ // Time-based stability check
262
+ if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
263
+ // Enough time has passed, reset counters
264
+ g_sameCursorDetectionCount = 0;
265
+ g_pendingCursorType = detectedCursorType;
288
266
  }
289
267
 
290
- // Son güvence - eğer cursorType hala nil veya geçersizse default'a çevir
291
- if (!cursorType || [cursorType length] == 0) {
292
- cursorType = @"default";
268
+ // Check if detected cursor matches pending cursor
269
+ if ([detectedCursorType isEqualToString:g_pendingCursorType]) {
270
+ g_sameCursorDetectionCount++;
271
+
272
+ // If we have enough confirmations, update stable cursor
273
+ if (g_sameCursorDetectionCount >= CURSOR_CONFIRMATION_COUNT) {
274
+ g_stableCursorType = detectedCursorType;
275
+ g_lastDetectedCursorType = detectedCursorType;
276
+ }
277
+ } else {
278
+ // Different cursor detected, start new pending
279
+ g_pendingCursorType = detectedCursorType;
280
+ g_sameCursorDetectionCount = 1;
293
281
  }
294
282
 
295
- NSLog(@"🎯 FINAL CURSOR TYPE: %@", cursorType);
296
- return cursorType;
297
-
283
+ g_lastCursorCheckTime = currentTime;
284
+
285
+ // Final validation
286
+ NSString *finalCursorType = g_stableCursorType;
287
+ if (!finalCursorType || [finalCursorType length] == 0) {
288
+ finalCursorType = @"default";
289
+ }
290
+
291
+ // Debug logging for stability tracking
292
+ NSLog(@"🎯 CURSOR DETECTION - NSCursor: %@, Contextual: %@, Stable: %@, Count: %d",
293
+ nsCursorType, contextualCursorType, finalCursorType, g_sameCursorDetectionCount);
294
+
295
+ return finalCursorType;
296
+
298
297
  } @catch (NSException *exception) {
299
298
  NSLog(@"Error in getCursorType: %@", exception);
300
- return @"default";
299
+ return g_stableCursorType ?: @"default";
301
300
  }
302
301
  }
303
302
  }
@@ -501,7 +500,11 @@ void cleanupCursorTracking() {
501
500
  g_outputPath = nil;
502
501
  g_debugCallbackCount = 0;
503
502
  g_lastDetectedCursorType = nil;
503
+ g_stableCursorType = @"default";
504
504
  g_cursorTypeCounter = 0;
505
+ g_lastCursorCheckTime = 0;
506
+ g_sameCursorDetectionCount = 0;
507
+ g_pendingCursorType = nil;
505
508
  g_isFirstWrite = true;
506
509
  }
507
510