node-mac-recorder 2.17.11 → 2.17.12

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.12",
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,145 @@ 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 multi-layer approach and stability
61
+ NSString* detectCursorTypeFromNSCursor() {
62
+ @try {
63
+ NSCursor *currentCursor = [NSCursor currentSystemCursor];
64
+ if (!currentCursor) {
65
+ return @"default";
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
+ // Fallback to image-based comparison for custom cursors
90
+ NSImage *cursorImage = [currentCursor image];
91
+ if (cursorImage) {
92
+ NSSize imageSize = [cursorImage size];
93
+
94
+ // Text cursors typically have I-beam shape (narrow width)
95
+ if (imageSize.width < 8 && imageSize.height > 15) {
96
+ return @"text";
97
+ }
98
+ // Pointer cursors are typically hand-shaped
99
+ else if (imageSize.width > 15 && imageSize.height > 15) {
100
+ return @"pointer";
101
+ }
102
+ }
103
+
104
+ return @"default";
105
+ } @catch (NSException *exception) {
106
+ return @"default";
107
+ }
108
+ }
109
+
110
+ // Improved cursor type detection with stability and multi-layer approach
53
111
  NSString* getCursorType() {
54
112
  @autoreleasepool {
55
113
  g_cursorTypeCounter++;
56
-
57
- @try {
58
- // ACCESSIBILITY API BASED CURSOR DETECTION
59
- // Determine cursor type based on the UI element under the cursor
60
-
61
- CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
62
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
63
- AXUIElementRef elementAtPosition = NULL;
64
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
65
-
66
- NSString *cursorType = @"default"; // Default fallback
67
-
68
- if (error == kAXErrorSuccess && elementAtPosition) {
69
- CFStringRef role = NULL;
70
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
71
-
72
- if (error == kAXErrorSuccess && role) {
73
- NSString *elementRole = (__bridge_transfer NSString*)role;
74
- NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
75
-
76
- // TEXT CURSORS
77
- if ([elementRole isEqualToString:@"AXTextField"] ||
78
- [elementRole isEqualToString:@"AXTextArea"] ||
79
- [elementRole isEqualToString:@"AXStaticText"] ||
80
- [elementRole isEqualToString:@"AXSearchField"]) {
81
- cursorType = @"text";
82
- }
83
- // POINTER CURSORS (clickable elements)
84
- else if ([elementRole isEqualToString:@"AXLink"] ||
85
- [elementRole isEqualToString:@"AXButton"] ||
86
- [elementRole isEqualToString:@"AXMenuItem"] ||
87
- [elementRole isEqualToString:@"AXRadioButton"] ||
88
- [elementRole isEqualToString:@"AXCheckBox"] ||
89
- [elementRole isEqualToString:@"AXPopUpButton"] ||
90
- [elementRole isEqualToString:@"AXTab"]) {
91
- cursorType = @"pointer";
92
- }
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";
103
- }
104
- }
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
116
- else if ([elementRole isEqualToString:@"AXSplitter"]) {
117
- // Get splitter orientation to determine resize direction
118
- CFStringRef orientation = NULL;
119
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
120
- if (error == kAXErrorSuccess && orientation) {
121
- NSString *orientationStr = (__bridge_transfer NSString*)orientation;
122
- if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
123
- cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
124
- } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
125
- cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
126
- } else {
127
- cursorType = @"default"; // Bilinmeyen orientation
128
- }
129
- } else {
130
- cursorType = @"default"; // Orientation alınamazsa default
131
- }
132
- }
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";
149
- }
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
238
- else {
239
- // Bilinmeyen elementler için her zaman default
240
- cursorType = @"default";
241
- }
114
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
242
115
 
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";
116
+ @try {
117
+ // Layer 1: Fast NSCursor detection (most reliable)
118
+ NSString *nsCursorType = detectCursorTypeFromNSCursor();
119
+
120
+ // Layer 2: Accessibility API for context (when NSCursor isn't enough)
121
+ NSString *contextualCursorType = nsCursorType;
122
+
123
+ // Only use expensive Accessibility API if NSCursor gives us "default"
124
+ if ([nsCursorType isEqualToString:@"default"]) {
125
+ CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
126
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
127
+ AXUIElementRef elementAtPosition = NULL;
128
+ AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
129
+
130
+ if (error == kAXErrorSuccess && elementAtPosition) {
131
+ CFStringRef role = NULL;
132
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
133
+
134
+ if (error == kAXErrorSuccess && role) {
135
+ NSString *elementRole = (__bridge_transfer NSString*)role;
136
+
137
+ // Simplified, high-confidence role mappings only
138
+ if ([elementRole isEqualToString:@"AXTextField"] ||
139
+ [elementRole isEqualToString:@"AXTextArea"] ||
140
+ [elementRole isEqualToString:@"AXSearchField"]) {
141
+ contextualCursorType = @"text";
271
142
  }
272
- // Zoom controls - sadece spesifik zoom butonları için
273
- else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
274
- cursorType = @"zoom-in";
143
+ else if ([elementRole isEqualToString:@"AXLink"] ||
144
+ [elementRole isEqualToString:@"AXButton"] ||
145
+ [elementRole isEqualToString:@"AXMenuItem"]) {
146
+ contextualCursorType = @"pointer";
275
147
  }
276
- else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
277
- cursorType = @"zoom-out";
148
+ else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
149
+ contextualCursorType = @"progress";
278
150
  }
279
- // Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
280
151
  }
152
+ CFRelease(elementAtPosition);
281
153
  }
154
+ if (systemWide) CFRelease(systemWide);
155
+ }
282
156
 
283
- CFRelease(elementAtPosition);
157
+ // Layer 3: Stability filtering to prevent oscillation
158
+ NSString *detectedCursorType = contextualCursorType;
159
+
160
+ // Time-based stability check
161
+ if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
162
+ // Enough time has passed, reset counters
163
+ g_sameCursorDetectionCount = 0;
164
+ g_pendingCursorType = detectedCursorType;
284
165
  }
285
166
 
286
- if (systemWide) {
287
- CFRelease(systemWide);
167
+ // Check if detected cursor matches pending cursor
168
+ if ([detectedCursorType isEqualToString:g_pendingCursorType]) {
169
+ g_sameCursorDetectionCount++;
170
+
171
+ // If we have enough confirmations, update stable cursor
172
+ if (g_sameCursorDetectionCount >= CURSOR_CONFIRMATION_COUNT) {
173
+ g_stableCursorType = detectedCursorType;
174
+ g_lastDetectedCursorType = detectedCursorType;
175
+ }
176
+ } else {
177
+ // Different cursor detected, start new pending
178
+ g_pendingCursorType = detectedCursorType;
179
+ g_sameCursorDetectionCount = 1;
288
180
  }
289
181
 
290
- // Son güvence - eğer cursorType hala nil veya geçersizse default'a çevir
291
- if (!cursorType || [cursorType length] == 0) {
292
- cursorType = @"default";
182
+ g_lastCursorCheckTime = currentTime;
183
+
184
+ // Final validation
185
+ NSString *finalCursorType = g_stableCursorType;
186
+ if (!finalCursorType || [finalCursorType length] == 0) {
187
+ finalCursorType = @"default";
293
188
  }
294
189
 
295
- NSLog(@"🎯 FINAL CURSOR TYPE: %@", cursorType);
296
- return cursorType;
297
-
190
+ // Debug logging for stability tracking
191
+ NSLog(@"🎯 CURSOR DETECTION - NSCursor: %@, Contextual: %@, Stable: %@, Count: %d",
192
+ nsCursorType, contextualCursorType, finalCursorType, g_sameCursorDetectionCount);
193
+
194
+ return finalCursorType;
195
+
298
196
  } @catch (NSException *exception) {
299
197
  NSLog(@"Error in getCursorType: %@", exception);
300
- return @"default";
198
+ return g_stableCursorType ?: @"default";
301
199
  }
302
200
  }
303
201
  }
@@ -501,7 +399,11 @@ void cleanupCursorTracking() {
501
399
  g_outputPath = nil;
502
400
  g_debugCallbackCount = 0;
503
401
  g_lastDetectedCursorType = nil;
402
+ g_stableCursorType = @"default";
504
403
  g_cursorTypeCounter = 0;
404
+ g_lastCursorCheckTime = 0;
405
+ g_sameCursorDetectionCount = 0;
406
+ g_pendingCursorType = nil;
505
407
  g_isFirstWrite = true;
506
408
  }
507
409