node-mac-recorder 2.21.43 → 2.21.45

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.
@@ -7,11 +7,117 @@
7
7
  #import <Accessibility/Accessibility.h>
8
8
  #import <dispatch/dispatch.h>
9
9
  #import "logging.h"
10
+ #include <vector>
11
+ #include <math.h>
10
12
 
11
13
  #ifndef kAXHitTestParameterizedAttribute
12
14
  #define kAXHitTestParameterizedAttribute CFSTR("AXHitTest")
13
15
  #endif
14
16
 
17
+ // Private CoreGraphics API for cursor detection
18
+ #include <dlfcn.h>
19
+
20
+ typedef int (*CGSCurrentCursorSeed_t)(void);
21
+ typedef CFStringRef (*CGSCopyCurrentCursorName_t)(void);
22
+
23
+ static void *g_coreGraphicsHandle = NULL;
24
+ static void *g_skyLightHandle = NULL;
25
+ static dispatch_once_t g_coreGraphicsHandleInitToken;
26
+ static dispatch_once_t g_skyLightHandleInitToken;
27
+ static CGSCurrentCursorSeed_t CGSCurrentCursorSeed_func = NULL;
28
+ static CGSCopyCurrentCursorName_t CGSCopyCurrentCursorName_func = NULL;
29
+ static dispatch_once_t cgsSeedInitToken;
30
+ static dispatch_once_t cgsCursorNameInitToken;
31
+
32
+ static void* LoadCoreGraphicsHandle() {
33
+ dispatch_once(&g_coreGraphicsHandleInitToken, ^{
34
+ g_coreGraphicsHandle = dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", RTLD_LAZY);
35
+ if (!g_coreGraphicsHandle) {
36
+ NSLog(@"⚠️ Failed to open CoreGraphics framework: %s", dlerror());
37
+ }
38
+ });
39
+ return g_coreGraphicsHandle;
40
+ }
41
+
42
+ static void* LoadSkyLightHandle() {
43
+ dispatch_once(&g_skyLightHandleInitToken, ^{
44
+ g_skyLightHandle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY);
45
+ if (!g_skyLightHandle) {
46
+ NSLog(@"⚠️ Failed to open SkyLight framework: %s", dlerror());
47
+ }
48
+ });
49
+ return g_skyLightHandle;
50
+ }
51
+
52
+ static void initCGSCurrentCursorSeed() {
53
+ dispatch_once(&cgsSeedInitToken, ^{
54
+ void *handle = LoadCoreGraphicsHandle();
55
+ if (handle) {
56
+ CGSCurrentCursorSeed_func = (CGSCurrentCursorSeed_t)dlsym(handle, "CGSCurrentCursorSeed");
57
+ if (!CGSCurrentCursorSeed_func) {
58
+ NSLog(@"⚠️ Failed to load CGSCurrentCursorSeed: %s", dlerror());
59
+ }
60
+ }
61
+ });
62
+ }
63
+
64
+ static void initCGSCursorNameFunc() {
65
+ dispatch_once(&cgsCursorNameInitToken, ^{
66
+ void *handle = LoadSkyLightHandle();
67
+ if (!handle) {
68
+ handle = LoadCoreGraphicsHandle();
69
+ }
70
+ if (handle) {
71
+ const char *symbolCandidates[] = {
72
+ "CGSCopyCurrentCursorName",
73
+ "CGSCopyGlobalCursorName",
74
+ "SLSCopyCurrentCursorName",
75
+ "SLSCopyGlobalCursorName",
76
+ "CGSCopyCurrentCursor",
77
+ "SLSCopyCurrentCursor"
78
+ };
79
+ size_t candidateCount = sizeof(symbolCandidates) / sizeof(symbolCandidates[0]);
80
+ for (size_t i = 0; i < candidateCount; ++i) {
81
+ CGSCopyCurrentCursorName_func = (CGSCopyCurrentCursorName_t)dlsym(handle, symbolCandidates[i]);
82
+ if (CGSCopyCurrentCursorName_func) {
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ if (!CGSCopyCurrentCursorName_func) {
88
+ NSLog(@"⚠️ Failed to load CGSCopyCurrentCursorName (CGS/SLS) symbol");
89
+ }
90
+ });
91
+ }
92
+
93
+ static int SafeCGSCurrentCursorSeed() {
94
+ initCGSCurrentCursorSeed();
95
+ if (CGSCurrentCursorSeed_func) {
96
+ int seed = CGSCurrentCursorSeed_func();
97
+ return seed;
98
+ } else {
99
+ static dispatch_once_t warnToken;
100
+ dispatch_once(&warnToken, ^{
101
+ NSLog(@"⚠️ CGSCurrentCursorSeed function not loaded!");
102
+ });
103
+ }
104
+ return -1;
105
+ }
106
+
107
+ static NSString* CopyCurrentCursorNameFromCGS(void) {
108
+ initCGSCursorNameFunc();
109
+ if (!CGSCopyCurrentCursorName_func) {
110
+ return nil;
111
+ }
112
+ CFStringRef cgsName = CGSCopyCurrentCursorName_func();
113
+ if (!cgsName) {
114
+ return nil;
115
+ }
116
+ NSString *name = [NSString stringWithString:(NSString *)cgsName];
117
+ CFRelease(cgsName);
118
+ return name;
119
+ }
120
+
15
121
  // Global state for cursor tracking
16
122
  static bool g_isCursorTracking = false;
17
123
  static CFMachPortRef g_eventTap = NULL;
@@ -22,6 +128,313 @@ static NSTimer *g_cursorTimer = nil;
22
128
  static int g_debugCallbackCount = 0;
23
129
  static NSFileHandle *g_fileHandle = nil;
24
130
  static bool g_isFirstWrite = true;
131
+ static NSMutableDictionary<NSString*, NSString*> *g_cursorFingerprintMap = nil;
132
+ static NSMutableDictionary<NSValue*, NSString*> *g_cursorPointerCache = nil;
133
+ static NSMutableDictionary<NSString*, NSString*> *g_cursorNameMap = nil;
134
+ static dispatch_once_t g_cursorFingerprintInitToken;
135
+ static void LoadSystemCursorResourceFingerprints(void);
136
+ static void LoadCursorMappingOverrides(void);
137
+ static NSMutableDictionary<NSNumber*, NSString*> *g_seedOverrides = nil;
138
+
139
+ typedef NSCursor* (*CursorFactoryFunc)(id, SEL);
140
+ typedef NSString* (*CursorNameFunc)(id, SEL);
141
+
142
+ static uint64_t FNV1AHash(const unsigned char *data, size_t length) {
143
+ const uint64_t kOffset = 1469598103934665603ULL;
144
+ const uint64_t kPrime = 1099511628211ULL;
145
+ uint64_t hash = kOffset;
146
+ if (!data || length == 0) {
147
+ return hash;
148
+ }
149
+ for (size_t i = 0; i < length; ++i) {
150
+ hash ^= data[i];
151
+ hash *= kPrime;
152
+ }
153
+ return hash;
154
+ }
155
+
156
+ static NSString* CursorImageFingerprintFromImage(NSImage *image, NSPoint hotspot) {
157
+ if (!image) {
158
+ return nil;
159
+ }
160
+ NSRect imageRect = NSMakeRect(0, 0, [image size].width, [image size].height);
161
+ CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:nil hints:nil];
162
+ if (!cgImage) {
163
+ for (NSImageRep *rep in [image representations]) {
164
+ if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
165
+ cgImage = [(NSBitmapImageRep *)rep CGImage];
166
+ if (cgImage) {
167
+ break;
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ if (!cgImage) {
174
+ return nil;
175
+ }
176
+
177
+ size_t width = CGImageGetWidth(cgImage);
178
+ size_t height = CGImageGetHeight(cgImage);
179
+ if (width == 0 || height == 0) {
180
+ return nil;
181
+ }
182
+
183
+ size_t bytesPerPixel = 4;
184
+ size_t bytesPerRow = width * bytesPerPixel;
185
+ size_t bufferSize = bytesPerRow * height;
186
+ if (bufferSize == 0) {
187
+ return nil;
188
+ }
189
+
190
+ std::vector<unsigned char> buffer(bufferSize);
191
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
192
+ if (!colorSpace) {
193
+ return nil;
194
+ }
195
+
196
+ CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedLast;
197
+ CGContextRef context = CGBitmapContextCreate(buffer.data(),
198
+ width,
199
+ height,
200
+ 8,
201
+ bytesPerRow,
202
+ colorSpace,
203
+ bitmapInfo);
204
+ CGColorSpaceRelease(colorSpace);
205
+
206
+ if (!context) {
207
+ return nil;
208
+ }
209
+
210
+ CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
211
+ CGContextRelease(context);
212
+
213
+ uint64_t hash = FNV1AHash(buffer.data(), buffer.size());
214
+
215
+ double relX = width > 0 ? hotspot.x / (double)width : 0.0;
216
+ double relY = height > 0 ? hotspot.y / (double)height : 0.0;
217
+
218
+ return [NSString stringWithFormat:@"%zux%zu-%.4f-%.4f-%016llx",
219
+ width,
220
+ height,
221
+ relX,
222
+ relY,
223
+ hash];
224
+ }
225
+
226
+ static NSString* CursorImageFingerprintUnsafe(NSCursor *cursor) {
227
+ if (!cursor) {
228
+ return nil;
229
+ }
230
+ return CursorImageFingerprintFromImage([cursor image], [cursor hotSpot]);
231
+ }
232
+
233
+ static NSString* CursorImageFingerprint(NSCursor *cursor) {
234
+ if (!cursor) {
235
+ return nil;
236
+ }
237
+ if ([NSThread isMainThread]) {
238
+ return CursorImageFingerprintUnsafe(cursor);
239
+ }
240
+
241
+ __block NSString *fingerprint = nil;
242
+ dispatch_sync(dispatch_get_main_queue(), ^{
243
+ fingerprint = CursorImageFingerprintUnsafe(cursor);
244
+ });
245
+ return fingerprint;
246
+ }
247
+
248
+ static NSString* CursorNameFromNSCursor(NSCursor *cursor) {
249
+ if (!cursor) {
250
+ return nil;
251
+ }
252
+
253
+ NSArray<NSString *> *selectorNames = @[
254
+ @"_name",
255
+ @"name",
256
+ @"cursorName",
257
+ @"_cursorName",
258
+ @"identifier",
259
+ @"_identifier",
260
+ @"cursorIdentifier"
261
+ ];
262
+
263
+ for (NSString *selectorName in selectorNames) {
264
+ SEL selector = NSSelectorFromString(selectorName);
265
+ if (selector && [cursor respondsToSelector:selector]) {
266
+ IMP imp = [cursor methodForSelector:selector];
267
+ if (!imp) {
268
+ continue;
269
+ }
270
+ CursorNameFunc func = (CursorNameFunc)imp;
271
+ NSString *value = func(cursor, selector);
272
+ if (value && [value isKindOfClass:[NSString class]] && [value length] > 0) {
273
+ return value;
274
+ }
275
+ }
276
+ }
277
+
278
+ NSArray<NSString *> *kvcKeys = @[ @"_name", @"name", @"cursorName", @"_cursorName", @"identifier", @"_identifier" ];
279
+ for (NSString *key in kvcKeys) {
280
+ @try {
281
+ id value = [cursor valueForKey:key];
282
+ if (value && [value isKindOfClass:[NSString class]] && [value length] > 0) {
283
+ return (NSString *)value;
284
+ }
285
+ } @catch (NSException *exception) {
286
+ // Ignore KVC exceptions
287
+ }
288
+ }
289
+ return nil;
290
+ }
291
+
292
+ static NSString* NormalizeCursorName(NSString *name) {
293
+ if (!name) {
294
+ return nil;
295
+ }
296
+ NSString *trimmed = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
297
+ return [[trimmed stringByReplacingOccurrencesOfString:@"\n" withString:@" "] lowercaseString];
298
+ }
299
+
300
+ static NSCursor* CursorFromSelector(SEL selector) {
301
+ if (!selector || ![NSCursor respondsToSelector:selector]) {
302
+ return nil;
303
+ }
304
+ IMP imp = [NSCursor methodForSelector:selector];
305
+ if (!imp) {
306
+ return nil;
307
+ }
308
+ CursorFactoryFunc func = (CursorFactoryFunc)imp;
309
+ return func([NSCursor class], selector);
310
+ }
311
+
312
+ static void AddStandardCursorFingerprint(NSCursor *cursor, NSString *cursorType) {
313
+ if (!cursor || !cursorType) {
314
+ return;
315
+ }
316
+ NSString *fingerprint = CursorImageFingerprintUnsafe(cursor);
317
+ if (!fingerprint) {
318
+ return;
319
+ }
320
+ [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
321
+ }
322
+
323
+ static void AddCursorIfAvailable(SEL selector, NSString *cursorType) {
324
+ if (!cursorType || !selector) {
325
+ return;
326
+ }
327
+ NSCursor *cursor = CursorFromSelector(selector);
328
+ if (cursor) {
329
+ AddStandardCursorFingerprint(cursor, cursorType);
330
+ }
331
+ }
332
+
333
+ static void AddCursorIfAvailableByName(NSString *selectorName, NSString *cursorType) {
334
+ if (!selectorName) {
335
+ return;
336
+ }
337
+ SEL selector = NSSelectorFromString(selectorName);
338
+ AddCursorIfAvailable(selector, cursorType);
339
+ }
340
+
341
+ static void InitializeCursorFingerprintMap(void) {
342
+ dispatch_once(&g_cursorFingerprintInitToken, ^{
343
+ g_cursorFingerprintMap = [[NSMutableDictionary alloc] init];
344
+ g_cursorPointerCache = [[NSMutableDictionary alloc] init];
345
+ g_cursorNameMap = [[NSMutableDictionary alloc] init];
346
+
347
+ void (^buildMap)(void) = ^{
348
+ AddStandardCursorFingerprint([NSCursor arrowCursor], @"default");
349
+ AddStandardCursorFingerprint([NSCursor pointingHandCursor], @"pointer");
350
+ AddStandardCursorFingerprint([NSCursor IBeamCursor], @"text");
351
+ if ([NSCursor respondsToSelector:@selector(IBeamCursorForVerticalLayout)]) {
352
+ AddStandardCursorFingerprint([NSCursor IBeamCursorForVerticalLayout], @"text");
353
+ }
354
+ AddStandardCursorFingerprint([NSCursor crosshairCursor], @"crosshair");
355
+ AddCursorIfAvailable(@selector(openHandCursor), @"grab");
356
+ AddCursorIfAvailable(@selector(closedHandCursor), @"grabbing");
357
+ AddCursorIfAvailable(@selector(operationNotAllowedCursor), @"not-allowed");
358
+ AddCursorIfAvailable(@selector(contextualMenuCursor), @"context-menu");
359
+ AddCursorIfAvailable(@selector(dragCopyCursor), @"copy");
360
+ AddCursorIfAvailable(@selector(dragLinkCursor), @"alias");
361
+ AddCursorIfAvailable(@selector(resizeLeftRightCursor), @"col-resize");
362
+ AddCursorIfAvailable(@selector(resizeUpDownCursor), @"row-resize");
363
+ AddCursorIfAvailableByName(@"resizeLeftCursor", @"w-resize");
364
+ AddCursorIfAvailableByName(@"resizeRightCursor", @"e-resize");
365
+ AddCursorIfAvailableByName(@"resizeUpCursor", @"n-resize");
366
+ AddCursorIfAvailableByName(@"resizeDownCursor", @"s-resize");
367
+ AddCursorIfAvailableByName(@"resizeNorthWestSouthEastCursor", @"nwse-resize");
368
+ AddCursorIfAvailableByName(@"resizeNorthEastSouthWestCursor", @"nesw-resize");
369
+ AddCursorIfAvailable(@selector(zoomInCursor), @"zoom-in");
370
+ AddCursorIfAvailable(@selector(zoomOutCursor), @"zoom-out");
371
+ AddCursorIfAvailable(@selector(columnResizeCursor), @"col-resize");
372
+ AddCursorIfAvailable(@selector(rowResizeCursor), @"row-resize");
373
+
374
+ LoadSystemCursorResourceFingerprints();
375
+ LoadCursorMappingOverrides();
376
+ };
377
+
378
+ if ([NSThread isMainThread]) {
379
+ buildMap();
380
+ } else {
381
+ dispatch_sync(dispatch_get_main_queue(), buildMap);
382
+ }
383
+ });
384
+ }
385
+
386
+ static NSString* LookupCursorTypeByFingerprint(NSCursor *cursor, NSString **outFingerprint) {
387
+ if (!cursor) {
388
+ return nil;
389
+ }
390
+ InitializeCursorFingerprintMap();
391
+
392
+ NSValue *pointerKey = [NSValue valueWithPointer:(__bridge const void *)cursor];
393
+ NSString *cachedType = [g_cursorPointerCache objectForKey:pointerKey];
394
+ if (cachedType) {
395
+ return cachedType;
396
+ }
397
+
398
+ NSString *fingerprint = CursorImageFingerprint(cursor);
399
+ if (!fingerprint) {
400
+ return nil;
401
+ }
402
+
403
+ if (outFingerprint) {
404
+ *outFingerprint = fingerprint;
405
+ }
406
+
407
+ NSString *mappedType = [g_cursorFingerprintMap objectForKey:fingerprint];
408
+ if (mappedType) {
409
+ if (pointerKey) {
410
+ [g_cursorPointerCache setObject:mappedType forKey:pointerKey];
411
+ }
412
+ return mappedType;
413
+ }
414
+
415
+ return nil;
416
+ }
417
+
418
+ static void CacheCursorFingerprint(NSCursor *cursor, NSString *cursorType, NSString *knownFingerprint) {
419
+ if (!cursor || !cursorType || [cursorType length] == 0) {
420
+ return;
421
+ }
422
+ InitializeCursorFingerprintMap();
423
+ NSString *fingerprint = knownFingerprint;
424
+ if (!fingerprint) {
425
+ fingerprint = CursorImageFingerprint(cursor);
426
+ }
427
+ if (!fingerprint) {
428
+ return;
429
+ }
430
+ if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
431
+ [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
432
+ }
433
+ NSValue *pointerKey = [NSValue valueWithPointer:(__bridge const void *)cursor];
434
+ if (pointerKey && g_cursorPointerCache) {
435
+ [g_cursorPointerCache setObject:cursorType forKey:pointerKey];
436
+ }
437
+ }
25
438
 
26
439
  // Forward declaration
27
440
  void cursorTimerCallback();
@@ -44,6 +457,75 @@ static CursorTimerTarget *g_timerTarget = nil;
44
457
  // Global cursor state tracking
45
458
  static NSString *g_lastDetectedCursorType = nil;
46
459
  static int g_cursorTypeCounter = 0;
460
+ static int g_lastCursorSeed = -1; // Track cursor seed for change detection
461
+ static BOOL g_hasLastCursorEvent = NO;
462
+ static CGPoint g_lastCursorLocation = {0, 0};
463
+ static NSString *g_lastCursorType = nil;
464
+ static NSString *g_lastCursorEventType = nil;
465
+
466
+ static inline BOOL StringsEqual(NSString *a, NSString *b) {
467
+ if (a == b) {
468
+ return YES;
469
+ }
470
+ if (!a || !b) {
471
+ return NO;
472
+ }
473
+ return [a isEqualToString:b];
474
+ }
475
+
476
+ static void ResetCursorEventHistory(void) {
477
+ g_hasLastCursorEvent = NO;
478
+ g_lastCursorLocation = CGPointZero;
479
+ if (g_lastCursorType) {
480
+ [g_lastCursorType release];
481
+ g_lastCursorType = nil;
482
+ }
483
+ if (g_lastCursorEventType) {
484
+ [g_lastCursorEventType release];
485
+ g_lastCursorEventType = nil;
486
+ }
487
+ }
488
+
489
+ static BOOL ShouldEmitCursorEvent(CGPoint location, NSString *cursorType, NSString *eventType) {
490
+ if (!g_hasLastCursorEvent) {
491
+ return YES;
492
+ }
493
+
494
+ const CGFloat movementThreshold = 1.5; // Require ~2px change to treat as movement
495
+ BOOL moved = fabs(location.x - g_lastCursorLocation.x) >= movementThreshold ||
496
+ fabs(location.y - g_lastCursorLocation.y) >= movementThreshold;
497
+ BOOL eventChanged = !StringsEqual(eventType, g_lastCursorEventType);
498
+ BOOL isMoveEvent = StringsEqual(eventType, @"move") || StringsEqual(eventType, @"drag");
499
+ BOOL isClickEvent = StringsEqual(eventType, @"mousedown") ||
500
+ StringsEqual(eventType, @"mouseup") ||
501
+ StringsEqual(eventType, @"rightmousedown") ||
502
+ StringsEqual(eventType, @"rightmouseup");
503
+
504
+ if (isMoveEvent) {
505
+ return moved;
506
+ }
507
+
508
+ if (isClickEvent) {
509
+ return eventChanged || moved;
510
+ }
511
+
512
+ // Fallback: only emit when something actually changed
513
+ BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
514
+ return moved || cursorChanged || eventChanged;
515
+ }
516
+
517
+ static void RememberCursorEvent(CGPoint location, NSString *cursorType, NSString *eventType) {
518
+ g_lastCursorLocation = location;
519
+ if (g_lastCursorType != cursorType) {
520
+ [g_lastCursorType release];
521
+ g_lastCursorType = cursorType ? [cursorType copy] : nil;
522
+ }
523
+ if (g_lastCursorEventType != eventType) {
524
+ [g_lastCursorEventType release];
525
+ g_lastCursorEventType = eventType ? [eventType copy] : nil;
526
+ }
527
+ g_hasLastCursorEvent = YES;
528
+ }
47
529
 
48
530
  static NSString* CopyAndReleaseCFString(CFStringRef value) {
49
531
  if (!value) {
@@ -514,12 +996,497 @@ static NSString* cursorTypeFromCursorName(NSString *value) {
514
996
  return nil;
515
997
  }
516
998
 
999
+ typedef struct {
1000
+ const char *cursorType;
1001
+ const char *resourceName;
1002
+ } CursorResourceEntry;
1003
+
1004
+ static void AddCursorFingerprintFromResource(const CursorResourceEntry &entry) {
1005
+ if (!entry.cursorType || !entry.resourceName) {
1006
+ return;
1007
+ }
1008
+
1009
+ NSString *cursorType = [NSString stringWithUTF8String:entry.cursorType];
1010
+ NSString *resourceName = [NSString stringWithUTF8String:entry.resourceName];
1011
+ if (!cursorType || !resourceName) {
1012
+ return;
1013
+ }
1014
+
1015
+ NSString *basePath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:resourceName];
1016
+ NSFileManager *fm = [NSFileManager defaultManager];
1017
+ NSArray<NSString *> *imageCandidates = @[ @"cursor_1only_.png", @"cursor.png", @"cursor.pdf" ];
1018
+ NSString *imagePath = nil;
1019
+ for (NSString *candidate in imageCandidates) {
1020
+ NSString *fullPath = [basePath stringByAppendingPathComponent:candidate];
1021
+ if ([fm fileExistsAtPath:fullPath]) {
1022
+ imagePath = fullPath;
1023
+ break;
1024
+ }
1025
+ }
1026
+ if (!imagePath) {
1027
+ return;
1028
+ }
1029
+
1030
+ NSImage *image = [[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease];
1031
+ if (!image) {
1032
+ return;
1033
+ }
1034
+
1035
+ NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[basePath stringByAppendingPathComponent:@"info.plist"]];
1036
+ double hotx = [[info objectForKey:@"hotx"] doubleValue];
1037
+ double hoty = [[info objectForKey:@"hoty"] doubleValue];
1038
+ NSPoint hotspot = NSMakePoint(hotx, hoty);
1039
+
1040
+ NSCursor *tempCursor = [[[NSCursor alloc] initWithImage:image hotSpot:hotspot] autorelease];
1041
+ if (!tempCursor) {
1042
+ return;
1043
+ }
1044
+
1045
+ NSString *fingerprint = CursorImageFingerprintUnsafe(tempCursor);
1046
+ if (!fingerprint) {
1047
+ return;
1048
+ }
1049
+
1050
+ if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
1051
+ [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
1052
+ }
1053
+ }
1054
+
1055
+ static void LoadSystemCursorResourceFingerprints(void) {
1056
+ static const CursorResourceEntry kResourceEntries[] = {
1057
+ {"progress", "busybutclickable"},
1058
+ {"wait", "countinguphand"},
1059
+ {"wait", "countingdownhand"},
1060
+ {"wait", "countingupanddownhand"},
1061
+ {"context-menu", "contextualmenu"},
1062
+ {"copy", "copy"},
1063
+ {"alias", "makealias"},
1064
+ {"not-allowed", "notallowed"},
1065
+ {"no-drop", "notallowed"},
1066
+ {"help", "help"},
1067
+ {"cell", "cell"},
1068
+ {"crosshair", "cross"},
1069
+ {"grab", "openhand"},
1070
+ {"grabbing", "closedhand"},
1071
+ {"pointer", "pointinghand"},
1072
+ {"move", "move"},
1073
+ {"all-scroll", "move"},
1074
+ {"zoom-in", "zoomin"},
1075
+ {"zoom-out", "zoomout"},
1076
+ {"text", "ibeamhorizontal"},
1077
+ {"vertical-text", "ibeamvertical"},
1078
+ {"col-resize", "resizeleftright"},
1079
+ {"col-resize", "resizeeastwest"},
1080
+ {"row-resize", "resizeupdown"},
1081
+ {"row-resize", "resizenorthsouth"},
1082
+ {"ew-resize", "resizeeastwest"},
1083
+ {"ew-resize", "resizeleftright"},
1084
+ {"ns-resize", "resizenorthsouth"},
1085
+ {"ns-resize", "resizeupdown"},
1086
+ {"n-resize", "resizenorth"},
1087
+ {"s-resize", "resizesouth"},
1088
+ {"e-resize", "resizeeast"},
1089
+ {"w-resize", "resizewest"},
1090
+ {"ne-resize", "resizenortheast"},
1091
+ {"nw-resize", "resizenorthwest"},
1092
+ {"se-resize", "resizesoutheast"},
1093
+ {"sw-resize", "resizesouthwest"},
1094
+ {"nesw-resize", "resizenortheastsouthwest"},
1095
+ {"nwse-resize", "resizenorthwestsoutheast"}
1096
+ };
1097
+
1098
+ size_t count = sizeof(kResourceEntries) / sizeof(kResourceEntries[0]);
1099
+ for (size_t i = 0; i < count; ++i) {
1100
+ AddCursorFingerprintFromResource(kResourceEntries[i]);
1101
+ }
1102
+ }
1103
+
1104
+ static void RegisterCursorNameMapping(NSString *name, NSString *cursorType) {
1105
+ if (!name || !cursorType) {
1106
+ return;
1107
+ }
1108
+ NSString *normalized = NormalizeCursorName(name);
1109
+ if (!normalized || [normalized length] == 0) {
1110
+ return;
1111
+ }
1112
+ if (![g_cursorNameMap objectForKey:normalized]) {
1113
+ [g_cursorNameMap setObject:cursorType forKey:normalized];
1114
+ }
1115
+ }
1116
+
1117
+ static void RegisterSeedMapping(NSNumber *seedValue, NSString *cursorType) {
1118
+ if (!seedValue || !cursorType) {
1119
+ return;
1120
+ }
1121
+ if (!g_seedOverrides) {
1122
+ g_seedOverrides = [[NSMutableDictionary alloc] init];
1123
+ }
1124
+ if (![g_seedOverrides objectForKey:seedValue]) {
1125
+ [g_seedOverrides setObject:cursorType forKey:seedValue];
1126
+ }
1127
+ }
1128
+
1129
+ static NSString* FindCursorMappingFile(void) {
1130
+ NSMutableArray<NSString *> *candidates = [NSMutableArray array];
1131
+ const char *envPath = getenv("MAC_RECORDER_CURSOR_MAP");
1132
+ if (envPath) {
1133
+ [candidates addObject:[NSString stringWithUTF8String:envPath]];
1134
+ }
1135
+
1136
+ NSString *cwd = [[NSFileManager defaultManager] currentDirectoryPath];
1137
+ if (cwd) {
1138
+ [candidates addObject:[cwd stringByAppendingPathComponent:@"cursor-nscursor-mapping.json"]];
1139
+ }
1140
+
1141
+ Dl_info info;
1142
+ if (dladdr((const void *)&FindCursorMappingFile, &info)) {
1143
+ if (info.dli_fname) {
1144
+ NSString *modulePath = [NSString stringWithUTF8String:info.dli_fname];
1145
+ NSString *moduleDir = [modulePath stringByDeletingLastPathComponent];
1146
+ if (moduleDir) {
1147
+ [candidates addObject:[moduleDir stringByAppendingPathComponent:@"cursor-nscursor-mapping.json"]];
1148
+ NSString *parent = [moduleDir stringByDeletingLastPathComponent];
1149
+ if (parent) {
1150
+ [candidates addObject:[parent stringByAppendingPathComponent:@"cursor-nscursor-mapping.json"]];
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ NSBundle *bundle = [NSBundle bundleForClass:[CursorTimerTarget class]];
1157
+ if (bundle) {
1158
+ NSString *resourcePath = [bundle resourcePath];
1159
+ if (resourcePath) {
1160
+ [candidates addObject:[resourcePath stringByAppendingPathComponent:@"cursor-nscursor-mapping.json"]];
1161
+ }
1162
+ NSString *bundlePath = [bundle bundlePath];
1163
+ if (bundlePath) {
1164
+ [candidates addObject:[bundlePath stringByAppendingPathComponent:@"cursor-nscursor-mapping.json"]];
1165
+ }
1166
+ }
1167
+
1168
+ for (NSString *candidate in candidates) {
1169
+ if (candidate && [[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
1170
+ return candidate;
1171
+ }
1172
+ }
1173
+ return nil;
1174
+ }
1175
+
1176
+ static void LoadCursorMappingOverrides(void) {
1177
+ NSString *mappingPath = FindCursorMappingFile();
1178
+ if (!mappingPath) {
1179
+ return;
1180
+ }
1181
+
1182
+ NSData *data = [NSData dataWithContentsOfFile:mappingPath];
1183
+ if (!data) {
1184
+ return;
1185
+ }
1186
+
1187
+ NSError *error = nil;
1188
+ NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
1189
+ if (error || ![json isKindOfClass:[NSDictionary class]]) {
1190
+ return;
1191
+ }
1192
+
1193
+ NSDictionary *cursorMapping = json[@"cursorMapping"];
1194
+ if (![cursorMapping isKindOfClass:[NSDictionary class]]) {
1195
+ return;
1196
+ }
1197
+
1198
+ [cursorMapping enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
1199
+ NSString *cursorType = (NSString *)key;
1200
+ NSDictionary *entry = (NSDictionary *)obj;
1201
+ if (![cursorType isKindOfClass:[NSString class]] || ![entry isKindOfClass:[NSDictionary class]]) {
1202
+ return;
1203
+ }
1204
+
1205
+ NSString *fingerprint = entry[@"fingerprint"];
1206
+ if ([fingerprint isKindOfClass:[NSString class]] && [fingerprint length] > 0) {
1207
+ if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
1208
+ [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
1209
+ }
1210
+ }
1211
+
1212
+ NSString *privateName = entry[@"privateName"];
1213
+ if ([privateName isKindOfClass:[NSString class]] && [privateName length] > 0) {
1214
+ RegisterCursorNameMapping(privateName, cursorType);
1215
+ }
1216
+
1217
+ NSNumber *seed = entry[@"seed"];
1218
+ if ([seed isKindOfClass:[NSNumber class]]) {
1219
+ RegisterSeedMapping(seed, cursorType);
1220
+ }
1221
+ }];
1222
+ }
1223
+
1224
+ // Runtime seed mapping - built dynamically on first use
1225
+ // Seeds change between app launches, so we build the mapping at runtime by querying NSCursor objects
1226
+ static NSMutableDictionary<NSNumber*, NSString*> *g_seedToTypeMap = nil;
1227
+ static dispatch_once_t g_seedMapInitToken;
1228
+
1229
+ static void buildRuntimeSeedMapping() {
1230
+ dispatch_once(&g_seedMapInitToken, ^{
1231
+ g_seedToTypeMap = [NSMutableDictionary dictionary];
1232
+
1233
+ // Instead of trying to build mapping upfront (which crashes),
1234
+ // we'll build it lazily as we encounter cursors during actual usage
1235
+ // For now, just initialize the empty map
1236
+
1237
+ NSLog(@"✅ Runtime seed mapping initialized (will build lazily)");
1238
+ });
1239
+ }
1240
+
1241
+ // Add a cursor seed to the runtime mapping
1242
+ static void addCursorToSeedMap(NSCursor *cursor, NSString *detectedType, int seed) {
1243
+ if (seed <= 0 || !cursor || !detectedType) return;
1244
+
1245
+ buildRuntimeSeedMapping(); // Ensure map is initialized
1246
+
1247
+ // Only add if we don't have this seed yet
1248
+ if (![g_seedToTypeMap objectForKey:@(seed)]) {
1249
+ g_seedToTypeMap[@(seed)] = detectedType;
1250
+ // Log only first 10 learned mappings to avoid spam
1251
+ if ([g_seedToTypeMap count] <= 10) {
1252
+ NSLog(@"📝 Learned seed mapping: %d -> %@", seed, detectedType);
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ static NSString* cursorTypeFromSeed(int seed) {
1258
+ if (seed > 0) {
1259
+ NSNumber *key = @(seed);
1260
+ NSString *override = [g_seedOverrides objectForKey:key];
1261
+ if (override) {
1262
+ return override;
1263
+ }
1264
+ }
1265
+ switch(seed) {
1266
+ case 741324: return @"auto";
1267
+ case 741336: return @"none";
1268
+ case 741338: return @"context-menu";
1269
+ case 741339: return @"pointer";
1270
+ case 741341: return @"progress";
1271
+ case 741343: return @"wait";
1272
+ case 741345: return @"cell";
1273
+ case 741347: return @"crosshair";
1274
+ case 741357: return @"text";
1275
+ case 741359: return @"vertical-text";
1276
+ case 741361: return @"alias";
1277
+ case 741362: return @"copy";
1278
+ case 741364: return @"move";
1279
+ case 741368: return @"no-drop";
1280
+ case 741370: return @"not-allowed";
1281
+ case 741381: return @"grab";
1282
+ case 741385: return @"grabbing";
1283
+ case 741389: return @"col-resize";
1284
+ case 741393: return @"row-resize";
1285
+ case 741397: return @"n-resize";
1286
+ case 741398: return @"e-resize";
1287
+ case 741409: return @"s-resize";
1288
+ case 741413: return @"w-resize";
1289
+ case 741417: return @"ne-resize";
1290
+ case 741418: return @"nw-resize";
1291
+ case 741420: return @"se-resize";
1292
+ case 741424: return @"sw-resize";
1293
+ case 741426: return @"ew-resize";
1294
+ case 741436: return @"ns-resize";
1295
+ case 741438: return @"nesw-resize";
1296
+ case 741442: return @"nwse-resize";
1297
+ case 741444: return @"zoom-in";
1298
+ case 741446: return @"zoom-out";
1299
+ default: return nil;
1300
+ }
1301
+ }
1302
+
1303
+ // Image-based cursor detection using known patterns from mapping
1304
+ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, NSCursor *cursor) {
1305
+ if (!image) {
1306
+ return nil;
1307
+ }
1308
+
1309
+ NSSize size = [image size];
1310
+ CGFloat width = size.width;
1311
+ CGFloat height = size.height;
1312
+ CGFloat aspectRatio = width > 0 ? width / height : 0;
1313
+ CGFloat relativeX = width > 0 ? hotspot.x / width : 0;
1314
+ CGFloat relativeY = height > 0 ? hotspot.y / height : 0;
1315
+
1316
+ // Tolerance for floating point comparison
1317
+ CGFloat tolerance = 0.05;
1318
+ CGFloat tightTolerance = 0.02; // For precise hotspot matching
1319
+
1320
+ // Helper lambda for approximate comparison
1321
+ auto approx = [tolerance](CGFloat a, CGFloat b) -> BOOL {
1322
+ return fabs(a - b) < tolerance;
1323
+ };
1324
+
1325
+ auto approxTight = [tightTolerance](CGFloat a, CGFloat b) -> BOOL {
1326
+ return fabs(a - b) < tightTolerance;
1327
+ };
1328
+
1329
+ // Pattern matching based on cursor-nscursor-mapping.json
1330
+
1331
+ // none: 1x1, ratio=1.0, hotspot=(0,0)
1332
+ if (approx(width, 1) && approx(height, 1)) {
1333
+ return @"none";
1334
+ }
1335
+
1336
+ // text: 22x23, ratio=0.956, hotspot rel=(0.52, 0.48)
1337
+ if (approx(width, 22) && approx(height, 23) && approx(aspectRatio, 0.956)) {
1338
+ return @"text";
1339
+ }
1340
+
1341
+ // vertical-text: 22x21, ratio=1.047, hotspot rel=(0.5, 0.476)
1342
+ if (approx(width, 22) && approx(height, 21) && approx(aspectRatio, 1.047)) {
1343
+ return @"vertical-text";
1344
+ }
1345
+
1346
+ // pointer: 32x32, ratio=1.0, hotspot rel=(0.406, 0.25)
1347
+ if (approx(width, 32) && approx(height, 32) && approx(relativeY, 0.25)) {
1348
+ return @"pointer";
1349
+ }
1350
+
1351
+ // grab/grabbing: 32x32, ratio=1.0, hotspot rel=(0.5, 0.531)
1352
+ // Distinguished by pointer equality
1353
+ if (approx(width, 32) && approx(height, 32) && approx(relativeY, 0.531)) {
1354
+ if (cursor) {
1355
+ if (cursor == [NSCursor closedHandCursor]) {
1356
+ return @"grabbing";
1357
+ }
1358
+ if (cursor == [NSCursor openHandCursor]) {
1359
+ return @"grab";
1360
+ }
1361
+ }
1362
+ return @"grab"; // Default to grab if can't distinguish
1363
+ }
1364
+
1365
+ // 24x24 cursors: crosshair vs move/all-scroll
1366
+ // Distinguished by precise hotspot position
1367
+ if (approx(width, 24) && approx(height, 24)) {
1368
+ // crosshair: hotspot rel=(0.458, 0.458)
1369
+ if (approxTight(relativeX, 0.458) && approxTight(relativeY, 0.458)) {
1370
+ return @"crosshair";
1371
+ }
1372
+ // move/all-scroll: hotspot rel=(0.5, 0.5)
1373
+ if (approxTight(relativeX, 0.5) && approxTight(relativeY, 0.5)) {
1374
+ return @"move"; // or all-scroll, they're identical
1375
+ }
1376
+ // Fallback for 24x24
1377
+ return @"crosshair";
1378
+ }
1379
+
1380
+ // help/cell: 18x18, ratio=1.0, hotspot rel=(0.5, 0.5)
1381
+ // NOTE: Cannot distinguish between help and cell by image alone
1382
+ if (approx(width, 18) && approx(height, 18)) {
1383
+ return @"cell"; // Default to cell for compatibility
1384
+ }
1385
+
1386
+ // col-resize: 30x24, ratio=1.25, hotspot rel=(0.5, 0.5)
1387
+ if (approx(width, 30) && approx(height, 24) && approx(aspectRatio, 1.25)) {
1388
+ return @"col-resize";
1389
+ }
1390
+
1391
+ // e-resize/w-resize/ew-resize: 24x18, ratio=1.333, hotspot rel=(0.5, 0.5)
1392
+ // Distinguish using pointer equality
1393
+ if (approx(width, 24) && approx(height, 18) && approx(aspectRatio, 1.333)) {
1394
+ if (cursor) {
1395
+ if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] &&
1396
+ cursor == [NSCursor resizeLeftCursor]) {
1397
+ return @"w-resize";
1398
+ }
1399
+ if ([NSCursor respondsToSelector:@selector(resizeRightCursor)] &&
1400
+ cursor == [NSCursor resizeRightCursor]) {
1401
+ return @"e-resize";
1402
+ }
1403
+ if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)] &&
1404
+ cursor == [NSCursor resizeLeftRightCursor]) {
1405
+ return @"ew-resize";
1406
+ }
1407
+ }
1408
+ return @"ew-resize"; // Default to ew-resize
1409
+ }
1410
+
1411
+ // row-resize: 24x28, ratio=0.857, hotspot rel=(0.5, 0.5)
1412
+ if (approx(width, 24) && approx(height, 28) && approx(aspectRatio, 0.857)) {
1413
+ return @"row-resize";
1414
+ }
1415
+
1416
+ // n-resize/s-resize/ns-resize: 18x28, ratio=0.643, hotspot rel=(0.5, 0.5)
1417
+ // Distinguish using pointer equality
1418
+ if (approx(width, 18) && approx(height, 28) && approx(aspectRatio, 0.643)) {
1419
+ if (cursor) {
1420
+ if ([NSCursor respondsToSelector:@selector(resizeUpCursor)] &&
1421
+ cursor == [NSCursor resizeUpCursor]) {
1422
+ return @"n-resize";
1423
+ }
1424
+ if ([NSCursor respondsToSelector:@selector(resizeDownCursor)] &&
1425
+ cursor == [NSCursor resizeDownCursor]) {
1426
+ return @"s-resize";
1427
+ }
1428
+ if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)] &&
1429
+ cursor == [NSCursor resizeUpDownCursor]) {
1430
+ return @"ns-resize";
1431
+ }
1432
+ }
1433
+ return @"ns-resize"; // Default to ns-resize
1434
+ }
1435
+
1436
+ // ne-resize/nw-resize/se-resize/sw-resize/nesw-resize/nwse-resize: 22x22, ratio=1.0, hotspot rel=(0.5, 0.5)
1437
+ if (approx(width, 22) && approx(height, 22)) {
1438
+ return @"nwse-resize"; // Default to nwse-resize for all diagonal cursors
1439
+ }
1440
+
1441
+ // zoom-in/zoom-out: 28x26, ratio=1.077, hotspot rel=(0.428, 0.423)
1442
+ // NOTE: Cannot distinguish between zoom-in and zoom-out by image or pointer alone
1443
+ // They use the same image and there's no standard NSCursor for zoom
1444
+ if (approx(width, 28) && approx(height, 26) && approx(aspectRatio, 1.077)) {
1445
+ return @"zoom-in"; // Default to zoom-in (cannot distinguish from zoom-out)
1446
+ }
1447
+
1448
+ // alias: 16x21, ratio=0.762, hotspot rel=(0.688, 0.143)
1449
+ if (approx(width, 16) && approx(height, 21) && approx(aspectRatio, 0.762)) {
1450
+ return @"alias";
1451
+ }
1452
+
1453
+ // 28x40 cursors: default/auto vs context-menu/progress/wait/copy/no-drop/not-allowed
1454
+ // Distinguished by precise hotspot position and pointer equality
1455
+ if (approx(width, 28) && approx(height, 40) && approx(aspectRatio, 0.7)) {
1456
+ // auto/default: hotspot rel=(0.161, 0.1) - hotspot at (4.5, 4)
1457
+ if (approxTight(relativeX, 0.161) && approxTight(relativeY, 0.1)) {
1458
+ return @"default";
1459
+ }
1460
+ // context-menu/progress/wait/copy/no-drop/not-allowed: hotspot rel=(0.179, 0.125) - hotspot at (5, 5)
1461
+ if (approxTight(relativeX, 0.179) && approxTight(relativeY, 0.125)) {
1462
+ // Try pointer equality for standard cursors
1463
+ if (cursor) {
1464
+ if (cursor == [NSCursor contextualMenuCursor]) {
1465
+ return @"context-menu";
1466
+ }
1467
+ if (cursor == [NSCursor dragCopyCursor]) {
1468
+ return @"copy";
1469
+ }
1470
+ if (cursor == [NSCursor operationNotAllowedCursor]) {
1471
+ return @"not-allowed";
1472
+ }
1473
+ // NOTE: progress, wait, no-drop don't have standard NSCursor pointers
1474
+ // They are visually identical and cannot be distinguished
1475
+ }
1476
+ return @"default"; // Fallback to default
1477
+ }
1478
+ return @"default";
1479
+ }
1480
+
1481
+ return nil;
1482
+ }
1483
+
517
1484
  static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
518
1485
  if (!cursor) {
519
1486
  return @"default";
520
1487
  }
521
1488
 
522
- // PRIORITY: Standard macOS cursor pointer equality (most reliable)
1489
+ // PRIORITY 1: Standard macOS cursor pointer equality (fastest and most reliable)
523
1490
  if (cursor == [NSCursor arrowCursor]) {
524
1491
  return @"default";
525
1492
  }
@@ -552,68 +1519,61 @@ static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
552
1519
  return @"alias";
553
1520
  }
554
1521
  if (cursor == [NSCursor contextualMenuCursor]) {
555
- return @"pointer"; // Electron uses 'pointer' for context menu
1522
+ return @"context-menu";
556
1523
  }
557
1524
 
558
- // Resize cursors - improved detection with Electron CSS cursor names
1525
+ // Resize cursors
559
1526
  if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)]) {
560
1527
  if (cursor == [NSCursor resizeLeftRightCursor]) {
561
- return @"col-resize"; // ew-resize
562
- }
563
- }
564
- if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)]) {
565
- if (cursor == [NSCursor resizeLeftCursor]) {
566
- return @"col-resize";
567
- }
568
- }
569
- if ([NSCursor respondsToSelector:@selector(resizeRightCursor)]) {
570
- if (cursor == [NSCursor resizeRightCursor]) {
571
1528
  return @"col-resize";
572
1529
  }
573
1530
  }
574
1531
  if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)]) {
575
1532
  if (cursor == [NSCursor resizeUpDownCursor]) {
576
- return @"row-resize"; // Changed from ns-resize to match Electron
1533
+ return @"row-resize";
577
1534
  }
578
1535
  }
579
- if ([NSCursor respondsToSelector:@selector(resizeUpCursor)]) {
580
- if (cursor == [NSCursor resizeUpCursor]) {
581
- return @"row-resize"; // Changed from ns-resize
1536
+
1537
+ NSString *privateCursorName = CursorNameFromNSCursor(cursor);
1538
+ if (privateCursorName) {
1539
+ NSString *normalizedName = NormalizeCursorName(privateCursorName);
1540
+ NSString *mappedType = normalizedName ? [g_cursorNameMap objectForKey:normalizedName] : nil;
1541
+ if (mappedType) {
1542
+ CacheCursorFingerprint(cursor, mappedType, nil);
1543
+ return mappedType;
582
1544
  }
583
- }
584
- if ([NSCursor respondsToSelector:@selector(resizeDownCursor)]) {
585
- if (cursor == [NSCursor resizeDownCursor]) {
586
- return @"row-resize"; // Changed from ns-resize
1545
+ NSString *typeFromName = cursorTypeFromCursorName(privateCursorName);
1546
+ if (typeFromName) {
1547
+ RegisterCursorNameMapping(privateCursorName, typeFromName);
1548
+ CacheCursorFingerprint(cursor, typeFromName, nil);
1549
+ return typeFromName;
587
1550
  }
588
1551
  }
589
1552
 
590
- if ([NSCursor respondsToSelector:@selector(disappearingItemCursor)] &&
591
- cursor == [NSCursor disappearingItemCursor]) {
592
- return @"default";
1553
+ NSString *fingerprintHint = nil;
1554
+ NSString *fingerprintMatch = LookupCursorTypeByFingerprint(cursor, &fingerprintHint);
1555
+ if (fingerprintMatch) {
1556
+ return fingerprintMatch;
593
1557
  }
594
1558
 
595
- // Try to get class name and description for debugging
596
- NSString *className = NSStringFromClass([cursor class]);
597
- NSString *description = [cursor description];
598
-
599
- // Debug: Check for pointer cursor patterns
600
- if (className && ([className containsString:@"pointing"] || [className containsString:@"Hand"])) {
601
- NSLog(@"🔍 POINTER CLASS: %@", className);
602
- return @"pointer";
603
- }
604
- if (description && ([description containsString:@"pointing"] || [description containsString:@"hand"])) {
605
- NSLog(@"🔍 POINTER DESC: %@", description);
606
- return @"pointer";
1559
+ // PRIORITY 2: Image-based detection (for browser custom cursors)
1560
+ NSImage *cursorImage = [cursor image];
1561
+ NSPoint hotspot = [cursor hotSpot];
1562
+ NSString *imageBasedType = cursorTypeFromImageSignature(cursorImage, hotspot, cursor);
1563
+ if (imageBasedType) {
1564
+ if (![imageBasedType isEqualToString:@"default"]) {
1565
+ CacheCursorFingerprint(cursor, imageBasedType, fingerprintHint);
1566
+ }
1567
+ return imageBasedType;
607
1568
  }
608
1569
 
609
- // Try name-based detection
1570
+ // PRIORITY 3: Name-based detection
1571
+ NSString *className = NSStringFromClass([cursor class]);
610
1572
  NSString *derived = cursorTypeFromCursorName(className);
611
1573
  if (derived) {
612
- return derived;
613
- }
614
-
615
- derived = cursorTypeFromCursorName(description);
616
- if (derived) {
1574
+ if (![derived isEqualToString:@"default"]) {
1575
+ CacheCursorFingerprint(cursor, derived, fingerprintHint);
1576
+ }
617
1577
  return derived;
618
1578
  }
619
1579
 
@@ -622,7 +1582,31 @@ static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
622
1582
  }
623
1583
 
624
1584
  static NSString* detectSystemCursorType(void) {
1585
+ InitializeCursorFingerprintMap();
625
1586
  __block NSString *cursorType = nil;
1587
+ __block NSCursor *detectedCursor = nil;
1588
+
1589
+ NSString *cgsName = CopyCurrentCursorNameFromCGS();
1590
+ if (cgsName && [cgsName length] > 0) {
1591
+ NSString *normalized = NormalizeCursorName(cgsName);
1592
+ NSString *mapped = normalized ? [g_cursorNameMap objectForKey:normalized] : nil;
1593
+ if (mapped) {
1594
+ return mapped;
1595
+ }
1596
+ NSString *derivedFromName = cursorTypeFromCursorName(cgsName);
1597
+ if (derivedFromName) {
1598
+ RegisterCursorNameMapping(cgsName, derivedFromName);
1599
+ return derivedFromName;
1600
+ }
1601
+ }
1602
+
1603
+ int cursorSeed = SafeCGSCurrentCursorSeed();
1604
+ if (cursorSeed > 0) {
1605
+ NSString *seedType = cursorTypeFromSeed(cursorSeed);
1606
+ if (seedType) {
1607
+ return seedType;
1608
+ }
1609
+ }
626
1610
 
627
1611
  void (^fetchCursorBlock)(void) = ^{
628
1612
  NSCursor *currentCursor = nil;
@@ -636,6 +1620,8 @@ static NSString* detectSystemCursorType(void) {
636
1620
  currentCursor = [NSCursor currentCursor];
637
1621
  }
638
1622
 
1623
+ detectedCursor = currentCursor; // Save for seed learning
1624
+
639
1625
  if (currentCursor) {
640
1626
  NSString *directType = cursorTypeFromNSCursor(currentCursor);
641
1627
  NSString *fallbackType = directType;
@@ -708,6 +1694,10 @@ static NSString* detectSystemCursorType(void) {
708
1694
  // NSLog(@"🎯 FALLBACK TO DEFAULT (will check AX)");
709
1695
  }
710
1696
  }
1697
+
1698
+ if (cursorType && ![cursorType isEqualToString:@"default"]) {
1699
+ CacheCursorFingerprint(currentCursor, cursorType, nil);
1700
+ }
711
1701
  } else {
712
1702
  // NSLog(@"🖱️ No current cursor found");
713
1703
  cursorType = @"default";
@@ -720,6 +1710,11 @@ static NSString* detectSystemCursorType(void) {
720
1710
  dispatch_sync(dispatch_get_main_queue(), fetchCursorBlock);
721
1711
  }
722
1712
 
1713
+ // Seed learning disabled - using hardcoded mapping instead
1714
+ // if (cursorType && ![cursorType isEqualToString:@"default"] && cursorSeed > 0 && detectedCursor) {
1715
+ // addCursorToSeedMap(detectedCursor, cursorType, cursorSeed);
1716
+ // }
1717
+
723
1718
  return cursorType;
724
1719
  }
725
1720
 
@@ -752,44 +1747,23 @@ NSString* getCursorType() {
752
1747
  }
753
1748
  }
754
1749
 
755
- // Try multiple detection methods
756
- NSString *systemCursorType = detectSystemCursorType();
757
- NSString *axCursorType = nil;
758
-
759
- if (hasCursorPosition) {
760
- axCursorType = detectCursorTypeUsingAccessibility(cursorPos);
761
- }
762
-
763
- NSString *finalType = @"default";
1750
+ // Get seed and save to global variable for getCursorPosition()
1751
+ int currentSeed = SafeCGSCurrentCursorSeed();
1752
+ g_lastCursorSeed = currentSeed; // Save for getCursorPosition()
764
1753
 
765
-
766
- // SYSTEM CURSOR PRIORITY - trust visual state over accessibility
767
- if (systemCursorType && [systemCursorType length] > 0) {
768
- // ALWAYS use system cursor when available - it reflects visual state
769
- finalType = systemCursorType;
770
-
771
- // Special cases: allow AX to override when system reports default but AX has richer info
772
- if ([systemCursorType isEqualToString:@"default"] && axCursorType && [axCursorType length] > 0) {
773
- BOOL axIsResize = [axCursorType containsString:@"resize"];
774
- BOOL axIsText = [axCursorType isEqualToString:@"text"] || [axCursorType containsString:@"text"];
775
- BOOL axIsPointer = [axCursorType isEqualToString:@"pointer"];
776
- if (axIsResize || axIsText || axIsPointer) {
777
- finalType = axCursorType;
778
- }
779
- }
780
- }
781
- // Only if system completely fails, use AX
782
- else if (axCursorType && [axCursorType length] > 0) {
783
- finalType = axCursorType;
784
- }
785
- else {
786
- finalType = @"default";
787
- }
1754
+ // Use cursorTypeFromNSCursor for detection (pointer equality + image-based)
1755
+ // DO NOT use accessibility detection as it's unreliable and causes false positives
1756
+ NSString *systemCursorType = detectSystemCursorType();
1757
+ NSString *finalType = systemCursorType && [systemCursorType length] > 0 ? systemCursorType : @"default";
788
1758
 
789
1759
  // Only log when cursor type changes
790
1760
  static NSString *lastLoggedType = nil;
791
1761
  if (![finalType isEqualToString:lastLoggedType]) {
792
- NSLog(@"🎯 %@", finalType);
1762
+ if (currentSeed > 0) {
1763
+ NSLog(@"🎯 %@ (seed: %d)", finalType, currentSeed);
1764
+ } else {
1765
+ NSLog(@"🎯 %@", finalType);
1766
+ }
793
1767
  lastLoggedType = [finalType copy];
794
1768
  }
795
1769
  return finalType;
@@ -847,6 +1821,9 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
847
1821
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
848
1822
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
849
1823
  NSString *cursorType = getCursorType();
1824
+ if (!cursorType) {
1825
+ cursorType = @"default";
1826
+ }
850
1827
  // (already captured above)
851
1828
  NSString *eventType = @"move";
852
1829
 
@@ -872,6 +1849,10 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
872
1849
  eventType = @"move";
873
1850
  break;
874
1851
  }
1852
+
1853
+ if (!ShouldEmitCursorEvent(location, cursorType, eventType)) {
1854
+ return event;
1855
+ }
875
1856
 
876
1857
  // Cursor data oluştur
877
1858
  NSDictionary *cursorInfo = @{
@@ -885,6 +1866,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
885
1866
 
886
1867
  // Direkt dosyaya yaz
887
1868
  writeToFile(cursorInfo);
1869
+ RememberCursorEvent(location, cursorType, eventType);
888
1870
 
889
1871
  return event;
890
1872
  }
@@ -913,6 +1895,14 @@ void cursorTimerCallback() {
913
1895
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
914
1896
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
915
1897
  NSString *cursorType = getCursorType();
1898
+ if (!cursorType) {
1899
+ cursorType = @"default";
1900
+ }
1901
+ NSString *eventType = @"move";
1902
+
1903
+ if (!ShouldEmitCursorEvent(location, cursorType, eventType)) {
1904
+ return;
1905
+ }
916
1906
 
917
1907
  // Cursor data oluştur
918
1908
  NSDictionary *cursorInfo = @{
@@ -921,11 +1911,12 @@ void cursorTimerCallback() {
921
1911
  @"timestamp": @(timestamp),
922
1912
  @"unixTimeMs": @(unixTimeMs),
923
1913
  @"cursorType": cursorType,
924
- @"type": @"move"
1914
+ @"type": eventType
925
1915
  };
926
1916
 
927
1917
  // Direkt dosyaya yaz
928
1918
  writeToFile(cursorInfo);
1919
+ RememberCursorEvent(location, cursorType, eventType);
929
1920
  }
930
1921
  }
931
1922
 
@@ -980,6 +1971,7 @@ void cleanupCursorTracking() {
980
1971
  g_lastDetectedCursorType = nil;
981
1972
  g_cursorTypeCounter = 0;
982
1973
  g_isFirstWrite = true;
1974
+ ResetCursorEventHistory();
983
1975
  }
984
1976
 
985
1977
  // NAPI Function: Start Cursor Tracking
@@ -1017,6 +2009,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
1017
2009
  g_isFirstWrite = true;
1018
2010
 
1019
2011
  g_trackingStartTime = [NSDate date];
2012
+ ResetCursorEventHistory();
1020
2013
 
1021
2014
  // Create event tap for mouse events
1022
2015
  CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
@@ -1030,6 +2023,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
1030
2023
  CGEventMaskBit(kCGEventRightMouseDragged) |
1031
2024
  CGEventMaskBit(kCGEventOtherMouseDragged));
1032
2025
 
2026
+ bool eventTapActive = false;
1033
2027
  g_eventTap = CGEventTapCreate(kCGSessionEventTap,
1034
2028
  kCGHeadInsertEventTap,
1035
2029
  kCGEventTapOptionListenOnly,
@@ -1042,19 +2036,25 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
1042
2036
  g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
1043
2037
  CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
1044
2038
  CGEventTapEnable(g_eventTap, true);
2039
+ eventTapActive = true;
2040
+ NSLog(@"✅ Cursor event tap active - event-driven tracking");
2041
+ } else {
2042
+ NSLog(@"⚠️ Failed to create cursor event tap; falling back to timer-based tracking (requires Accessibility permission)");
1045
2043
  }
1046
2044
 
1047
- // NSTimer kullan (main thread'de çalışır)
1048
- g_timerTarget = [[CursorTimerTarget alloc] init];
1049
-
1050
- g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
1051
- target:g_timerTarget
1052
- selector:@selector(timerCallback:)
1053
- userInfo:nil
1054
- repeats:YES];
1055
-
1056
- // Main run loop'a ekle
1057
- [[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
2045
+ if (!eventTapActive) {
2046
+ // NSTimer fallback (main thread)
2047
+ g_timerTarget = [[CursorTimerTarget alloc] init];
2048
+
2049
+ g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
2050
+ target:g_timerTarget
2051
+ selector:@selector(timerCallback:)
2052
+ userInfo:nil
2053
+ repeats:YES];
2054
+
2055
+ // Main run loop'a ekle
2056
+ [[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
2057
+ }
1058
2058
 
1059
2059
  g_isCursorTracking = true;
1060
2060
  return Napi::Boolean::New(env, true);
@@ -1196,14 +2196,17 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
1196
2196
  result.Set("y", Napi::Number::New(env, (int)logicalLocation.y));
1197
2197
  result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
1198
2198
  result.Set("eventType", Napi::String::New(env, [eventType UTF8String]));
1199
-
2199
+
2200
+ // Add cursor seed (from global variable set by getCursorType())
2201
+ result.Set("seed", Napi::Number::New(env, g_lastCursorSeed));
2202
+
1200
2203
  // Basic display info
1201
2204
  NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
1202
2205
  if (scalingInfo) {
1203
2206
  CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
1204
2207
  result.Set("scaleFactor", Napi::Number::New(env, scaleFactor));
1205
2208
  }
1206
-
2209
+
1207
2210
  return result;
1208
2211
 
1209
2212
  } @catch (NSException *exception) {
@@ -1214,7 +2217,7 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
1214
2217
  // NAPI Function: Get Cursor Tracking Status
1215
2218
  Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
1216
2219
  Napi::Env env = info.Env();
1217
-
2220
+
1218
2221
  Napi::Object result = Napi::Object::New(env);
1219
2222
  result.Set("isTracking", Napi::Boolean::New(env, g_isCursorTracking));
1220
2223
  result.Set("hasEventTap", Napi::Boolean::New(env, g_eventTap != NULL));
@@ -1223,16 +2226,126 @@ Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
1223
2226
  result.Set("hasTimer", Napi::Boolean::New(env, g_cursorTimer != NULL));
1224
2227
  result.Set("debugCallbackCount", Napi::Number::New(env, g_debugCallbackCount));
1225
2228
  result.Set("cursorTypeCounter", Napi::Number::New(env, g_cursorTypeCounter));
1226
-
2229
+
1227
2230
  return result;
1228
2231
  }
1229
2232
 
2233
+ // NAPI Function: Get Detailed Cursor Debug Info
2234
+ Napi::Value GetCursorDebugInfo(const Napi::CallbackInfo& info) {
2235
+ Napi::Env env = info.Env();
2236
+
2237
+ @try {
2238
+ __block Napi::Object result = Napi::Object::New(env);
2239
+
2240
+ void (^debugBlock)(void) = ^{
2241
+ NSCursor *currentCursor = nil;
2242
+
2243
+ if ([NSCursor respondsToSelector:@selector(currentSystemCursor)]) {
2244
+ currentCursor = [NSCursor currentSystemCursor];
2245
+ }
2246
+ if (!currentCursor) {
2247
+ currentCursor = [NSCursor currentCursor];
2248
+ }
2249
+
2250
+ if (currentCursor) {
2251
+ NSString *className = NSStringFromClass([currentCursor class]);
2252
+ NSString *description = [currentCursor description];
2253
+ NSImage *cursorImage = [currentCursor image];
2254
+ NSPoint hotspot = [currentCursor hotSpot];
2255
+ NSSize imageSize = [cursorImage size];
2256
+ NSString *privateName = CursorNameFromNSCursor(currentCursor);
2257
+ NSString *fingerprint = CursorImageFingerprintUnsafe(currentCursor);
2258
+
2259
+ CGFloat aspectRatio = imageSize.width > 0 ? imageSize.width / imageSize.height : 0;
2260
+ CGFloat relativeHotspotX = imageSize.width > 0 ? hotspot.x / imageSize.width : 0;
2261
+ CGFloat relativeHotspotY = imageSize.height > 0 ? hotspot.y / imageSize.height : 0;
2262
+
2263
+ // Cursor identity - pointer address, hash, and seed
2264
+ uintptr_t cursorPointer = (uintptr_t)currentCursor;
2265
+ NSUInteger cursorHash = [currentCursor hash];
2266
+ int cursorSeed = SafeCGSCurrentCursorSeed();
2267
+
2268
+ // Basic info
2269
+ result.Set("className", Napi::String::New(env, [className UTF8String]));
2270
+ result.Set("description", Napi::String::New(env, [description UTF8String]));
2271
+ if (privateName) {
2272
+ result.Set("privateName", Napi::String::New(env, [privateName UTF8String]));
2273
+ } else {
2274
+ result.Set("privateName", env.Null());
2275
+ }
2276
+ result.Set("pointerAddress", Napi::Number::New(env, cursorPointer));
2277
+ result.Set("hash", Napi::Number::New(env, cursorHash));
2278
+ result.Set("seed", Napi::Number::New(env, cursorSeed));
2279
+ if (fingerprint) {
2280
+ result.Set("fingerprint", Napi::String::New(env, [fingerprint UTF8String]));
2281
+ } else {
2282
+ result.Set("fingerprint", env.Null());
2283
+ }
2284
+
2285
+ // Image info
2286
+ Napi::Object imageInfo = Napi::Object::New(env);
2287
+ imageInfo.Set("width", Napi::Number::New(env, imageSize.width));
2288
+ imageInfo.Set("height", Napi::Number::New(env, imageSize.height));
2289
+ imageInfo.Set("aspectRatio", Napi::Number::New(env, aspectRatio));
2290
+ result.Set("image", imageInfo);
2291
+
2292
+ // Hotspot info
2293
+ Napi::Object hotspotInfo = Napi::Object::New(env);
2294
+ hotspotInfo.Set("x", Napi::Number::New(env, hotspot.x));
2295
+ hotspotInfo.Set("y", Napi::Number::New(env, hotspot.y));
2296
+ hotspotInfo.Set("relativeX", Napi::Number::New(env, relativeHotspotX));
2297
+ hotspotInfo.Set("relativeY", Napi::Number::New(env, relativeHotspotY));
2298
+ result.Set("hotspot", hotspotInfo);
2299
+
2300
+ // Detection results
2301
+ NSString *directType = cursorTypeFromNSCursor(currentCursor);
2302
+ NSString *systemType = detectSystemCursorType();
2303
+
2304
+ result.Set("directDetection", Napi::String::New(env, [directType UTF8String]));
2305
+ result.Set("systemDetection", Napi::String::New(env, [systemType UTF8String]));
2306
+
2307
+ // Get cursor position and AX detection
2308
+ CGEventRef event = CGEventCreate(NULL);
2309
+ if (event) {
2310
+ CGPoint cursorPos = CGEventGetLocation(event);
2311
+ CFRelease(event);
2312
+
2313
+ NSString *axType = detectCursorTypeUsingAccessibility(cursorPos);
2314
+ if (axType) {
2315
+ result.Set("axDetection", Napi::String::New(env, [axType UTF8String]));
2316
+ } else {
2317
+ result.Set("axDetection", env.Null());
2318
+ }
2319
+
2320
+ NSString *finalType = getCursorType();
2321
+ result.Set("finalType", Napi::String::New(env, [finalType UTF8String]));
2322
+ }
2323
+ } else {
2324
+ result.Set("error", Napi::String::New(env, "No cursor found"));
2325
+ }
2326
+ };
2327
+
2328
+ if ([NSThread isMainThread]) {
2329
+ debugBlock();
2330
+ } else {
2331
+ dispatch_sync(dispatch_get_main_queue(), debugBlock);
2332
+ }
2333
+
2334
+ return result;
2335
+ } @catch (NSException *exception) {
2336
+ Napi::Object errorResult = Napi::Object::New(env);
2337
+ errorResult.Set("error", Napi::String::New(env, [[exception description] UTF8String]));
2338
+ return errorResult;
2339
+ }
2340
+ }
2341
+
1230
2342
  // Export functions
1231
2343
  Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
1232
2344
  exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
1233
2345
  exports.Set("stopCursorTracking", Napi::Function::New(env, StopCursorTracking));
1234
2346
  exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
1235
2347
  exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
1236
-
2348
+ exports.Set("getCursorDebugInfo", Napi::Function::New(env, GetCursorDebugInfo));
2349
+
1237
2350
  return exports;
1238
2351
  }