node-mac-recorder 2.24.15 → 2.24.16

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.
@@ -72,9 +72,7 @@ static void initCGSCursorNameFunc() {
72
72
  "CGSCopyCurrentCursorName",
73
73
  "CGSCopyGlobalCursorName",
74
74
  "SLSCopyCurrentCursorName",
75
- "SLSCopyGlobalCursorName",
76
- "CGSCopyCurrentCursor",
77
- "SLSCopyCurrentCursor"
75
+ "SLSCopyGlobalCursorName"
78
76
  };
79
77
  size_t candidateCount = sizeof(symbolCandidates) / sizeof(symbolCandidates[0]);
80
78
  for (size_t i = 0; i < candidateCount; ++i) {
@@ -113,6 +111,10 @@ static NSString* CopyCurrentCursorNameFromCGS(void) {
113
111
  if (!cgsName) {
114
112
  return nil;
115
113
  }
114
+ if (CFGetTypeID(cgsName) != CFStringGetTypeID()) {
115
+ CFRelease(cgsName);
116
+ return nil;
117
+ }
116
118
  NSString *name = [NSString stringWithString:(NSString *)cgsName];
117
119
  CFRelease(cgsName);
118
120
  return name;
@@ -129,12 +131,10 @@ static int g_debugCallbackCount = 0;
129
131
  static NSFileHandle *g_fileHandle = nil;
130
132
  static bool g_isFirstWrite = true;
131
133
  static NSMutableDictionary<NSString*, NSString*> *g_cursorFingerprintMap = nil;
132
- static NSMutableDictionary<NSValue*, NSString*> *g_cursorPointerCache = nil;
133
134
  static NSMutableDictionary<NSString*, NSString*> *g_cursorNameMap = nil;
134
135
  static dispatch_once_t g_cursorFingerprintInitToken;
135
136
  static void LoadSystemCursorResourceFingerprints(void);
136
137
  static void LoadCursorMappingOverrides(void);
137
- static NSMutableDictionary<NSNumber*, NSString*> *g_seedOverrides = nil;
138
138
 
139
139
  typedef NSCursor* (*CursorFactoryFunc)(id, SEL);
140
140
  typedef NSString* (*CursorNameFunc)(id, SEL);
@@ -265,6 +265,28 @@ static NSArray<NSString *>* CursorImageFingerprintsAllReps(NSImage *image, NSPoi
265
265
  }
266
266
  }
267
267
 
268
+ NSSize size = image.size;
269
+ if (size.width > 0 && size.height > 0) {
270
+ for (NSUInteger scale = 1; scale <= 2; scale++) {
271
+ NSBitmapImageRep *bitmap = [[[NSBitmapImageRep alloc]
272
+ initWithBitmapDataPlanes:NULL pixelsWide:lround(size.width * scale)
273
+ pixelsHigh:lround(size.height * scale) bitsPerSample:8 samplesPerPixel:4
274
+ hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace
275
+ bytesPerRow:0 bitsPerPixel:0] autorelease];
276
+ if (!bitmap) continue;
277
+ bitmap.size = size;
278
+ [NSGraphicsContext saveGraphicsState];
279
+ [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap]];
280
+ [image drawInRect:NSMakeRect(0, 0, size.width, size.height) fromRect:NSZeroRect
281
+ operation:NSCompositingOperationCopy fraction:1.0];
282
+ [NSGraphicsContext restoreGraphicsState];
283
+ NSString *fingerprint = CursorImageFingerprintFromCGImage(bitmap.CGImage, hotspot);
284
+ if (fingerprint && ![fingerprints containsObject:fingerprint]) {
285
+ [fingerprints addObject:fingerprint];
286
+ }
287
+ }
288
+ }
289
+
268
290
  return fingerprints;
269
291
  }
270
292
 
@@ -308,6 +330,10 @@ static NSString* CursorNameFromNSCursor(NSCursor *cursor) {
308
330
  for (NSString *selectorName in selectorNames) {
309
331
  SEL selector = NSSelectorFromString(selectorName);
310
332
  if (selector && [cursor respondsToSelector:selector]) {
333
+ NSMethodSignature *signature = [cursor methodSignatureForSelector:selector];
334
+ if (!signature || signature.methodReturnType[0] != '@') {
335
+ continue;
336
+ }
311
337
  IMP imp = [cursor methodForSelector:selector];
312
338
  if (!imp) {
313
339
  continue;
@@ -389,13 +415,81 @@ static void AddCursorIfAvailableByName(NSString *selectorName, NSString *cursorT
389
415
  AddCursorIfAvailable(selector, cursorType);
390
416
  }
391
417
 
418
+ // Chromium and WebKit use CoreCursor for types with no public NSCursor factory.
419
+ // Ask AppKit to render these references, including the system's shadow; the raw
420
+ // HIServices PDF is not pixel-identical to the cursor returned by WindowServer.
421
+ // CoreCursor's enum is distinct from CGSCurrentCursorSeed (a change counter).
422
+ // https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/mac/CursorMac.mm
423
+ @interface MRSystemReferenceCursor : NSCursor {
424
+ NSInteger _referenceType;
425
+ }
426
+ - (instancetype)initWithCoreType:(NSInteger)type;
427
+ @end
428
+
429
+ @implementation MRSystemReferenceCursor
430
+ - (instancetype)initWithCoreType:(NSInteger)type {
431
+ self = [super init];
432
+ if (self) _referenceType = type;
433
+ return self;
434
+ }
435
+ - (NSInteger)_coreCursorType { return _referenceType; }
436
+ @end
437
+
438
+ static void AddCoreCursorFingerprints(void) {
439
+ // This SPI is optional. Keep public factories and resource matching on
440
+ // macOS versions where AppKit no longer exposes it.
441
+ if (![NSCursor instancesRespondToSelector:NSSelectorFromString(@"_coreCursorType")]) return;
442
+ NSDictionary<NSNumber *, NSString *> *types = @{
443
+ @4: @"progress", @11: @"grabbing", @12: @"grab",
444
+ @27: @"ew-resize", @28: @"ew-resize", @29: @"nesw-resize",
445
+ @30: @"nesw-resize", @31: @"ns-resize", @32: @"ns-resize",
446
+ @33: @"nwse-resize", @34: @"nwse-resize", @35: @"nwse-resize",
447
+ @36: @"ns-resize", @37: @"nesw-resize", @38: @"ew-resize",
448
+ @39: @"all-scroll", @40: @"help", @41: @"crosshair",
449
+ @42: @"zoom-in", @43: @"zoom-out"
450
+ };
451
+ for (NSNumber *type in types) {
452
+ @try {
453
+ NSCursor *cursor = [[[MRSystemReferenceCursor alloc] initWithCoreType:type.integerValue] autorelease];
454
+ AddStandardCursorFingerprint(cursor, types[type]);
455
+ } @catch (NSException *exception) {
456
+ MRLog(@"CoreCursor reference %@ unavailable: %@", type, exception.reason);
457
+ }
458
+ }
459
+ }
460
+
461
+ static void AddFrameResizeCursorFingerprints(void) {
462
+ #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000
463
+ if (@available(macOS 15.0, *)) {
464
+ const NSCursorFrameResizePosition positions[] = {
465
+ NSCursorFrameResizePositionTop, NSCursorFrameResizePositionBottom,
466
+ NSCursorFrameResizePositionLeft, NSCursorFrameResizePositionRight,
467
+ NSCursorFrameResizePositionTopLeft, NSCursorFrameResizePositionBottomRight,
468
+ NSCursorFrameResizePositionTopRight, NSCursorFrameResizePositionBottomLeft
469
+ };
470
+ NSArray<NSString *> *types = @[@"ns-resize", @"ns-resize", @"ew-resize", @"ew-resize",
471
+ @"nwse-resize", @"nwse-resize", @"nesw-resize", @"nesw-resize"];
472
+ for (NSUInteger direction = 1; direction <= 3; direction++) {
473
+ AddStandardCursorFingerprint([NSCursor columnResizeCursorInDirections:(NSHorizontalDirections)direction], @"col-resize");
474
+ AddStandardCursorFingerprint([NSCursor rowResizeCursorInDirections:(NSVerticalDirections)direction], @"row-resize");
475
+ for (NSUInteger i = 0; i < sizeof(positions) / sizeof(positions[0]); i++) {
476
+ AddStandardCursorFingerprint([NSCursor frameResizeCursorFromPosition:positions[i]
477
+ inDirections:(NSCursorFrameResizeDirections)direction], types[i]);
478
+ }
479
+ }
480
+ }
481
+ #endif
482
+ }
483
+
392
484
  static void InitializeCursorFingerprintMap(void) {
393
485
  dispatch_once(&g_cursorFingerprintInitToken, ^{
394
486
  g_cursorFingerprintMap = [[NSMutableDictionary alloc] init];
395
- g_cursorPointerCache = [[NSMutableDictionary alloc] init];
396
487
  g_cursorNameMap = [[NSMutableDictionary alloc] init];
397
488
 
398
489
  void (^buildMap)(void) = ^{
490
+ // Node worker processes may not have initialized AppKit yet. Without
491
+ // it arrow/I-beam images can be empty and stay absent from this map.
492
+ [NSApplication sharedApplication];
399
493
  AddStandardCursorFingerprint([NSCursor arrowCursor], @"default");
400
494
  AddStandardCursorFingerprint([NSCursor pointingHandCursor], @"pointer");
401
495
  AddStandardCursorFingerprint([NSCursor IBeamCursor], @"text");
@@ -413,8 +507,8 @@ static void InitializeCursorFingerprintMap(void) {
413
507
  AddCursorIfAvailable(@selector(resizeUpDownCursor), @"row-resize");
414
508
  AddCursorIfAvailableByName(@"resizeLeftCursor", @"col-resize");
415
509
  AddCursorIfAvailableByName(@"resizeRightCursor", @"col-resize");
416
- AddCursorIfAvailableByName(@"resizeUpCursor", @"ns-resize");
417
- AddCursorIfAvailableByName(@"resizeDownCursor", @"ns-resize");
510
+ AddCursorIfAvailableByName(@"resizeUpCursor", @"row-resize");
511
+ AddCursorIfAvailableByName(@"resizeDownCursor", @"row-resize");
418
512
  AddCursorIfAvailableByName(@"resizeNorthWestSouthEastCursor", @"nwse-resize");
419
513
  AddCursorIfAvailableByName(@"resizeNorthEastSouthWestCursor", @"nesw-resize");
420
514
  AddCursorIfAvailable(@selector(zoomInCursor), @"zoom-in");
@@ -422,6 +516,8 @@ static void InitializeCursorFingerprintMap(void) {
422
516
  AddCursorIfAvailable(@selector(columnResizeCursor), @"col-resize");
423
517
  AddCursorIfAvailable(@selector(rowResizeCursor), @"row-resize");
424
518
 
519
+ AddFrameResizeCursorFingerprints();
520
+ AddCoreCursorFingerprints();
425
521
  LoadSystemCursorResourceFingerprints();
426
522
  LoadCursorMappingOverrides();
427
523
  };
@@ -440,16 +536,6 @@ static NSString* LookupCursorTypeByFingerprint(NSCursor *cursor, NSString **outF
440
536
  }
441
537
  InitializeCursorFingerprintMap();
442
538
 
443
- NSValue *pointerKey = [NSValue valueWithPointer:(__bridge const void *)cursor];
444
-
445
- // DISABLED: Pointer cache causes stale cursor type detection
446
- // Since macOS may reuse the same NSCursor object for different contexts,
447
- // we need to check the actual cursor state every time for real-time accuracy
448
- // NSString *cachedType = [g_cursorPointerCache objectForKey:pointerKey];
449
- // if (cachedType) {
450
- // return cachedType;
451
- // }
452
-
453
539
  NSString *fingerprint = CursorImageFingerprint(cursor);
454
540
  if (!fingerprint) {
455
541
  return nil;
@@ -459,39 +545,7 @@ static NSString* LookupCursorTypeByFingerprint(NSCursor *cursor, NSString **outF
459
545
  *outFingerprint = fingerprint;
460
546
  }
461
547
 
462
- NSString *mappedType = [g_cursorFingerprintMap objectForKey:fingerprint];
463
- if (mappedType) {
464
- // DISABLED: Don't cache by pointer for real-time detection
465
- // if (pointerKey) {
466
- // [g_cursorPointerCache setObject:mappedType forKey:pointerKey];
467
- // }
468
- return mappedType;
469
- }
470
-
471
- return nil;
472
- }
473
-
474
- static void CacheCursorFingerprint(NSCursor *cursor, NSString *cursorType, NSString *knownFingerprint) {
475
- if (!cursor || !cursorType || [cursorType length] == 0) {
476
- return;
477
- }
478
- InitializeCursorFingerprintMap();
479
- NSString *fingerprint = knownFingerprint;
480
- if (!fingerprint) {
481
- fingerprint = CursorImageFingerprint(cursor);
482
- }
483
- if (!fingerprint) {
484
- return;
485
- }
486
- // Only cache fingerprint mapping (image hash -> type), not pointer mapping
487
- if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
488
- [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
489
- }
490
- // DISABLED: Pointer cache for real-time detection
491
- // NSValue *pointerKey = [NSValue valueWithPointer:(__bridge const void *)cursor];
492
- // if (pointerKey && g_cursorPointerCache) {
493
- // [g_cursorPointerCache setObject:cursorType forKey:pointerKey];
494
- // }
548
+ return [g_cursorFingerprintMap objectForKey:fingerprint];
495
549
  }
496
550
 
497
551
  // Forward declaration
@@ -553,22 +607,22 @@ static BOOL ShouldEmitCursorEvent(CGPoint location, NSString *cursorType, NSStri
553
607
  BOOL moved = fabs(location.x - g_lastCursorLocation.x) >= movementThreshold ||
554
608
  fabs(location.y - g_lastCursorLocation.y) >= movementThreshold;
555
609
  BOOL eventChanged = !StringsEqual(eventType, g_lastCursorEventType);
556
- BOOL isMoveEvent = StringsEqual(eventType, @"move") || StringsEqual(eventType, @"drag");
610
+ BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
611
+ BOOL isMoveEvent = StringsEqual(eventType, @"move") || StringsEqual(eventType, @"drag") || StringsEqual(eventType, @"rightdrag");
557
612
  BOOL isClickEvent = StringsEqual(eventType, @"mousedown") ||
558
613
  StringsEqual(eventType, @"mouseup") ||
559
614
  StringsEqual(eventType, @"rightmousedown") ||
560
615
  StringsEqual(eventType, @"rightmouseup");
561
616
 
562
617
  if (isMoveEvent) {
563
- return moved;
618
+ return moved || cursorChanged || eventChanged;
564
619
  }
565
620
 
566
621
  if (isClickEvent) {
567
- return eventChanged || moved;
622
+ return eventChanged || moved || cursorChanged;
568
623
  }
569
624
 
570
625
  // Fallback: only emit when something actually changed
571
- BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
572
626
  return moved || cursorChanged || eventChanged;
573
627
  }
574
628
 
@@ -932,163 +986,45 @@ static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
932
986
  }
933
987
 
934
988
  static NSString* cursorTypeFromCursorName(NSString *value) {
935
- if (!value || [value length] == 0) {
936
- return nil;
937
- }
938
-
939
- NSString *normalized = [[value stringByReplacingOccurrencesOfString:@"_" withString:@"-"] lowercaseString];
940
-
941
- // Arrow cursor patterns
942
- if ([normalized containsString:@"arrow"] || [normalized containsString:@"default"]) {
943
- return @"default";
944
- }
945
-
946
- // Text cursor patterns
947
- if ([normalized containsString:@"ibeam"] ||
948
- [normalized containsString:@"insertion"] ||
949
- [normalized containsString:@"text"] ||
950
- [normalized containsString:@"edit"]) {
951
- return @"text";
952
- }
953
-
954
- // Hand cursors
955
- if ([normalized containsString:@"openhand"] || [normalized containsString:@"open-hand"]) {
956
- return @"grab";
957
- }
958
- if ([normalized containsString:@"closedhand"] || [normalized containsString:@"closed-hand"]) {
959
- return @"grabbing";
960
- }
961
-
962
- // Pointer cursor patterns
963
- if ([normalized containsString:@"pointing"] ||
964
- [normalized containsString:@"pointinghand"] ||
965
- ([normalized containsString:@"hand"] && ![normalized containsString:@"closed"] && ![normalized containsString:@"open"]) ||
966
- [normalized containsString:@"link"] ||
967
- [normalized containsString:@"button"]) {
968
- return @"pointer";
969
- }
970
-
971
- // Crosshair patterns
972
- if ([normalized containsString:@"crosshair"] || [normalized containsString:@"cross-hair"]) {
973
- return @"crosshair";
974
- }
975
-
976
- // Not allowed patterns
977
- if ([normalized containsString:@"not-allowed"] ||
978
- [normalized containsString:@"notallowed"] ||
979
- [normalized containsString:@"forbidden"] ||
980
- [normalized containsString:@"operation-not-allowed"]) {
981
- return @"not-allowed";
982
- }
983
-
984
- // Copy cursor patterns
985
- if ([normalized containsString:@"dragcopy"] ||
986
- [normalized containsString:@"drag-copy"] ||
987
- [normalized containsString:@"copy"]) {
988
- return @"copy";
989
- }
990
-
991
- // Alias cursor patterns
992
- if ([normalized containsString:@"draglink"] ||
993
- [normalized containsString:@"drag-link"] ||
994
- [normalized containsString:@"alias"]) {
995
- return @"alias";
996
- }
997
-
998
- // Context menu patterns
999
- if (([normalized containsString:@"context"] && [normalized containsString:@"menu"]) ||
1000
- [normalized containsString:@"contextual-menu"]) {
1001
- return @"context-menu";
1002
- }
1003
-
1004
- // Zoom patterns
1005
- if ([normalized containsString:@"zoom"]) {
1006
- if ([normalized containsString:@"out"]) {
1007
- return @"zoom-out";
1008
- }
1009
- return @"zoom-in";
1010
- }
1011
-
1012
- // All-scroll pattern (move in all directions)
1013
- if ([normalized containsString:@"all-scroll"] ||
1014
- [normalized containsString:@"allscroll"] ||
1015
- ([normalized containsString:@"move"] && [normalized containsString:@"all"]) ||
1016
- [normalized containsString:@"omnidirectional"]) {
1017
- return @"all-scroll";
1018
- }
1019
-
1020
- // Resize cursor patterns - more comprehensive with Electron CSS names
1021
- if ([normalized containsString:@"resize"] || [normalized containsString:@"size"]) {
1022
- // Check for specific directional patterns first
1023
- // North-East/South-West diagonal
1024
- if ([normalized containsString:@"nesw"] ||
1025
- ([normalized containsString:@"northeast"] && [normalized containsString:@"southwest"]) ||
1026
- ([normalized containsString:@"ne"] && [normalized containsString:@"sw"])) {
1027
- return @"nesw-resize";
1028
- }
1029
-
1030
- // North-West/South-East diagonal
1031
- if ([normalized containsString:@"nwse"] ||
1032
- ([normalized containsString:@"northwest"] && [normalized containsString:@"southeast"]) ||
1033
- ([normalized containsString:@"nw"] && [normalized containsString:@"se"])) {
1034
- return @"nwse-resize";
1035
- }
1036
-
1037
- // Generic diagonal patterns
1038
- BOOL diagonalUp = [normalized containsString:@"diagonalup"] ||
1039
- [normalized containsString:@"diagonal-up"];
1040
- BOOL diagonalDown = [normalized containsString:@"diagonaldown"] ||
1041
- [normalized containsString:@"diagonal-down"];
1042
-
1043
- // Horizontal resize (East-West)
1044
- BOOL horizontal = [normalized containsString:@"ew-resize"] ||
1045
- [normalized containsString:@"ewresize"] ||
1046
- [normalized containsString:@"leftright"] ||
1047
- [normalized containsString:@"left-right"] ||
1048
- [normalized containsString:@"horizontal"] ||
1049
- ([normalized containsString:@"left"] && [normalized containsString:@"right"]) ||
1050
- [normalized containsString:@"col-resize"] ||
1051
- [normalized containsString:@"column"];
1052
-
1053
- // Vertical resize (North-South)
1054
- BOOL vertical = [normalized containsString:@"ns-resize"] ||
1055
- [normalized containsString:@"nsresize"] ||
1056
- [normalized containsString:@"updown"] ||
1057
- [normalized containsString:@"up-down"] ||
1058
- [normalized containsString:@"vertical"] ||
1059
- ([normalized containsString:@"up"] && [normalized containsString:@"down"]) ||
1060
- [normalized containsString:@"row-resize"];
1061
-
1062
- if (diagonalUp) {
1063
- return @"nesw-resize";
1064
- }
1065
- if (diagonalDown) {
1066
- return @"nwse-resize";
1067
- }
1068
- if (horizontal) {
1069
- return @"col-resize"; // Desktop SVG var: col-resize
1070
- }
1071
- if (vertical) {
1072
- return @"ns-resize"; // Desktop SVG var: ns-resize
1073
- }
1074
-
1075
- // If contains "resize" but no specific direction, return generic resize
1076
- // This catches window resize cursors
1077
- return @"nwse-resize"; // Default to diagonal for generic resize
1078
- }
1079
-
1080
- // Progress/wait patterns - Electron uses 'progress'
1081
- if ([normalized containsString:@"wait"] ||
1082
- [normalized containsString:@"busy"] ||
1083
- [normalized containsString:@"progress"]) {
1084
- return @"progress";
1085
- }
1086
-
1087
- // Help pattern
1088
- if ([normalized containsString:@"help"] || [normalized containsString:@"question"]) {
1089
- return @"help";
1090
- }
1091
-
989
+ if (![value isKindOfClass:[NSString class]] || value.length == 0) return nil;
990
+ NSString *name = NormalizeCursorName(value);
991
+ // Treat separators uniformly, without letting "text" match "context" or
992
+ // "link" consume dragLink before the alias rule.
993
+ NSString *compact = [[name componentsSeparatedByCharactersInSet:
994
+ [[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@""];
995
+ if ([compact hasSuffix:@"cursor"]) compact = [compact substringToIndex:compact.length - 6];
996
+
997
+ if ([compact containsString:@"resize"]) {
998
+ if ([compact containsString:@"column"] || [compact isEqualToString:@"colresize"]) return @"col-resize";
999
+ if ([compact containsString:@"row"]) return @"row-resize";
1000
+ NSString *direction = [compact stringByReplacingOccurrencesOfString:@"resize" withString:@""];
1001
+ if ([direction hasPrefix:@"frame"]) direction = [direction substringFromIndex:5];
1002
+ if (StringEqualsAny(direction, @[@"nesw", @"ne", @"sw", @"northeast", @"southwest",
1003
+ @"northeastsouthwest", @"topright", @"bottomleft", @"diagonalup"])) return @"nesw-resize";
1004
+ if (StringEqualsAny(direction, @[@"nwse", @"nw", @"se", @"northwest", @"southeast",
1005
+ @"northwestsoutheast", @"topleft", @"bottomright", @"diagonaldown"])) return @"nwse-resize";
1006
+ if (StringEqualsAny(direction, @[@"ew", @"e", @"w", @"east", @"west", @"eastwest",
1007
+ @"left", @"right", @"leftright", @"horizontal"])) return @"ew-resize";
1008
+ if (StringEqualsAny(direction, @[@"ns", @"n", @"s", @"north", @"south", @"northsouth",
1009
+ @"up", @"down", @"updown", @"top", @"bottom", @"vertical"])) return @"ns-resize";
1010
+ return nil; // A name without an axis cannot identify a diagonal.
1011
+ }
1012
+ if ([compact containsString:@"contextualmenu"] || [compact containsString:@"contextmenu"]) return @"context-menu";
1013
+ if ([compact containsString:@"draglink"] || [compact containsString:@"alias"]) return @"alias";
1014
+ if ([compact containsString:@"copy"]) return @"copy";
1015
+ if ([compact containsString:@"notallowed"] || [compact containsString:@"nodrop"]) return @"not-allowed";
1016
+ if ([compact containsString:@"closedhand"] || [compact isEqualToString:@"grabbing"]) return @"grabbing";
1017
+ if ([compact containsString:@"openhand"] || [compact isEqualToString:@"grab"]) return @"grab";
1018
+ if ([compact containsString:@"pointinghand"] || StringEqualsAny(compact, @[@"pointer", @"hand", @"link"])) return @"pointer";
1019
+ if ([compact containsString:@"ibeam"] || [compact containsString:@"insertion"] ||
1020
+ StringEqualsAny(compact, @[@"text", @"verticaltext"])) return @"text";
1021
+ if ([compact containsString:@"zoomout"]) return @"zoom-out";
1022
+ if ([compact containsString:@"zoomin"]) return @"zoom-in";
1023
+ if (StringEqualsAny(compact, @[@"move", @"allscroll", @"moveall", @"omnidirectional"])) return @"all-scroll";
1024
+ if ([compact containsString:@"crosshair"] || StringEqualsAny(compact, @[@"cross", @"cell"])) return @"crosshair";
1025
+ if ([compact containsString:@"wait"] || [compact containsString:@"busy"] || [compact containsString:@"progress"]) return @"progress";
1026
+ if ([compact containsString:@"help"]) return @"help";
1027
+ if (StringEqualsAny(compact, @[@"arrow", @"default", @"auto", @"none"])) return @"default";
1092
1028
  return nil;
1093
1029
  }
1094
1030
 
@@ -1111,40 +1047,19 @@ static void AddCursorFingerprintFromResource(const CursorResourceEntry &entry) {
1111
1047
  NSString *basePath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:resourceName];
1112
1048
  NSFileManager *fm = [NSFileManager defaultManager];
1113
1049
  NSArray<NSString *> *imageCandidates = @[ @"cursor_1only_.png", @"cursor.png", @"cursor.pdf" ];
1114
- NSString *imagePath = nil;
1115
- for (NSString *candidate in imageCandidates) {
1116
- NSString *fullPath = [basePath stringByAppendingPathComponent:candidate];
1117
- if ([fm fileExistsAtPath:fullPath]) {
1118
- imagePath = fullPath;
1119
- break;
1120
- }
1121
- }
1122
- if (!imagePath) {
1123
- return;
1124
- }
1125
-
1126
- NSImage *image = [[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease];
1127
- if (!image) {
1128
- return;
1129
- }
1130
-
1131
1050
  NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[basePath stringByAppendingPathComponent:@"info.plist"]];
1132
1051
  double hotx = [[info objectForKey:@"hotx"] doubleValue];
1133
1052
  double hoty = [[info objectForKey:@"hoty"] doubleValue];
1134
1053
  NSPoint hotspot = NSMakePoint(hotx, hoty);
1135
1054
 
1136
- NSCursor *tempCursor = [[[NSCursor alloc] initWithImage:image hotSpot:hotspot] autorelease];
1137
- if (!tempCursor) {
1138
- return;
1139
- }
1140
-
1141
- NSString *fingerprint = CursorImageFingerprintUnsafe(tempCursor);
1142
- if (!fingerprint) {
1143
- return;
1144
- }
1145
-
1146
- if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
1147
- [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
1055
+ // PNG and PDF versions can differ (notably help). Both are used by apps.
1056
+ for (NSString *candidate in imageCandidates) {
1057
+ NSString *imagePath = [basePath stringByAppendingPathComponent:candidate];
1058
+ if (![fm fileExistsAtPath:imagePath]) continue;
1059
+ NSImage *image = [[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease];
1060
+ if (!image) continue;
1061
+ NSCursor *cursor = [[[NSCursor alloc] initWithImage:image hotSpot:hotspot] autorelease];
1062
+ AddStandardCursorFingerprint(cursor, cursorType);
1148
1063
  }
1149
1064
  }
1150
1065
 
@@ -1153,7 +1068,7 @@ static void LoadSystemCursorResourceFingerprints(void) {
1153
1068
  {"progress", "busybutclickable"},
1154
1069
  {"wait", "countinguphand"},
1155
1070
  {"wait", "countingdownhand"},
1156
- {"wait", "countingupanddownhand"},
1071
+ {"wait", "countingupandownhand"},
1157
1072
  {"context-menu", "contextualmenu"},
1158
1073
  {"copy", "copy"},
1159
1074
  {"alias", "makealias"},
@@ -1171,10 +1086,14 @@ static void LoadSystemCursorResourceFingerprints(void) {
1171
1086
  {"zoom-out", "zoomout"},
1172
1087
  {"text", "ibeamhorizontal"},
1173
1088
  {"vertical-text", "ibeamvertical"},
1089
+ {"col-resize", "resizeleft"},
1090
+ {"col-resize", "resizeright"},
1091
+ {"row-resize", "resizeup"},
1092
+ {"row-resize", "resizedown"},
1174
1093
  {"col-resize", "resizeleftright"},
1175
1094
  {"col-resize", "resizeeastwest"},
1176
1095
  {"row-resize", "resizeupdown"},
1177
- {"row-resize", "resizenorthsouth"},
1096
+ {"ns-resize", "resizenorthsouth"},
1178
1097
  {"ew-resize", "resizeeastwest"},
1179
1098
  {"ew-resize", "resizeleftright"},
1180
1099
  {"ns-resize", "resizenorthsouth"},
@@ -1210,18 +1129,6 @@ static void RegisterCursorNameMapping(NSString *name, NSString *cursorType) {
1210
1129
  }
1211
1130
  }
1212
1131
 
1213
- static void RegisterSeedMapping(NSNumber *seedValue, NSString *cursorType) {
1214
- if (!seedValue || !cursorType) {
1215
- return;
1216
- }
1217
- if (!g_seedOverrides) {
1218
- g_seedOverrides = [[NSMutableDictionary alloc] init];
1219
- }
1220
- if (![g_seedOverrides objectForKey:seedValue]) {
1221
- [g_seedOverrides setObject:cursorType forKey:seedValue];
1222
- }
1223
- }
1224
-
1225
1132
  static NSString* FindCursorMappingFile(void) {
1226
1133
  NSMutableArray<NSString *> *candidates = [NSMutableArray array];
1227
1134
  const char *envPath = getenv("MAC_RECORDER_CURSOR_MAP");
@@ -1310,554 +1217,57 @@ static void LoadCursorMappingOverrides(void) {
1310
1217
  RegisterCursorNameMapping(privateName, cursorType);
1311
1218
  }
1312
1219
 
1313
- NSNumber *seed = entry[@"seed"];
1314
- if ([seed isKindOfClass:[NSNumber class]]) {
1315
- RegisterSeedMapping(seed, cursorType);
1316
- }
1220
+ // Cursor seeds are change counters, not stable cursor identities.
1221
+ // Old calibration files may contain them; only image/name entries apply.
1317
1222
  }];
1318
1223
  }
1319
1224
 
1320
- // Runtime seed mapping - built dynamically on first use
1321
- // Seeds change between app launches, so we build the mapping at runtime by querying NSCursor objects
1322
- // SAFETY: Protected with try-catch to prevent crashes in Electron environments
1323
- static BOOL g_enableSeedLearning = YES; // Runtime seed learning enabled with crash protection
1324
- static NSMutableDictionary<NSNumber*, NSString*> *g_seedToTypeMap = nil;
1325
- static dispatch_once_t g_seedMapInitToken;
1326
-
1327
- static void buildRuntimeSeedMapping() {
1328
- dispatch_once(&g_seedMapInitToken, ^{
1329
- @try {
1330
- @autoreleasepool {
1331
- g_seedToTypeMap = [[NSMutableDictionary alloc] init];
1332
-
1333
- // Instead of trying to build mapping upfront (which crashes),
1334
- // we'll build it lazily as we encounter cursors during actual usage
1335
- // For now, just initialize the empty map
1336
-
1337
- NSLog(@"✅ Runtime seed mapping initialized (will build lazily)");
1338
- }
1339
- } @catch (NSException *exception) {
1340
- NSLog(@"⚠️ Failed to initialize runtime seed mapping: %@", exception.reason);
1341
- g_seedToTypeMap = nil;
1342
- }
1343
- });
1344
- }
1345
-
1346
- // Add a cursor seed to the runtime mapping
1347
- // NOTE: We don't pass cursor object to avoid potential crashes - we only need seed and type
1348
- static void addCursorToSeedMap(NSString *detectedType, int seed) {
1349
- // Safety: Check if learning is enabled
1350
- if (!g_enableSeedLearning) return;
1351
-
1352
- if (seed <= 0 || !detectedType || [detectedType length] == 0) return;
1353
-
1354
- @try {
1355
- @autoreleasepool {
1356
- buildRuntimeSeedMapping(); // Ensure map is initialized
1357
-
1358
- // If initialization failed, don't proceed
1359
- if (!g_seedToTypeMap) return;
1360
-
1361
- NSNumber *key = @(seed);
1362
-
1363
- // Only add if we don't have this seed yet
1364
- if (![g_seedToTypeMap objectForKey:key]) {
1365
- [g_seedToTypeMap setObject:detectedType forKey:key];
1366
- // Always log new seed mappings for debugging
1367
- NSLog(@"📝 Learned seed mapping: %d -> %@", seed, detectedType);
1368
- }
1369
- }
1370
- } @catch (NSException *exception) {
1371
- // Silently fail - don't crash the app for cursor learning
1372
- NSLog(@"⚠️ Failed to add cursor seed mapping: %@", exception.reason);
1373
- } @catch (...) {
1374
- NSLog(@"⚠️ Failed to add cursor seed mapping (unknown exception)");
1375
- }
1376
- }
1377
-
1378
- static NSString* cursorTypeFromSeed(int seed) {
1379
- if (seed > 0) {
1380
- @try {
1381
- @autoreleasepool {
1382
- NSNumber *key = @(seed);
1383
- NSString *override = [g_seedOverrides objectForKey:key];
1384
- if (override) {
1385
- return override;
1386
- }
1387
-
1388
- // Only check runtime mappings if learning is enabled
1389
- if (g_enableSeedLearning) {
1390
- buildRuntimeSeedMapping();
1391
- if (g_seedToTypeMap) {
1392
- NSString *runtime = [g_seedToTypeMap objectForKey:key];
1393
- if (runtime) {
1394
- return runtime;
1395
- }
1396
- }
1397
- }
1398
- }
1399
- } @catch (NSException *exception) {
1400
- // Silently fail - don't crash for cursor lookup
1401
- NSLog(@"⚠️ Exception in cursorTypeFromSeed: %@", exception.reason);
1402
- } @catch (...) {
1403
- NSLog(@"⚠️ Unknown exception in cursorTypeFromSeed");
1404
- }
1405
- }
1406
- switch(seed) {
1407
- // Desktop'ta SVG karşılığı olan tiplere normalize edilmiş seed map
1408
- case 741324: return @"default"; // auto → default
1409
- case 741336: return @"default"; // none → default (gizli cursor kayıtta default gösterilir)
1410
- case 741338: return @"default"; // context-menu → default (SVG yok)
1411
- case 741339: return @"pointer";
1412
- case 741341: return @"progress";
1413
- case 741343: return @"progress"; // wait → progress
1414
- case 741345: return @"crosshair"; // cell → crosshair (en yakın SVG)
1415
- case 741347: return @"crosshair";
1416
- case 741357: return @"text";
1417
- case 741359: return @"text"; // vertical-text → text
1418
- case 741361: return @"alias";
1419
- case 741362: return @"copy";
1420
- case 741364: return @"all-scroll"; // move → all-scroll
1421
- case 741368: return @"not-allowed"; // no-drop → not-allowed
1422
- case 741370: return @"not-allowed";
1423
- case 741381: return @"grab";
1424
- case 741385: return @"grabbing";
1425
- case 741389: return @"col-resize";
1426
- case 741393: return @"row-resize";
1427
- case 741397: return @"ns-resize"; // n-resize → ns-resize
1428
- case 741398: return @"col-resize"; // e-resize → col-resize
1429
- case 741409: return @"ns-resize"; // s-resize → ns-resize
1430
- case 741413: return @"col-resize"; // w-resize → col-resize
1431
- case 741417: return @"nesw-resize"; // ne-resize → nesw-resize
1432
- case 741418: return @"nwse-resize"; // nw-resize → nwse-resize
1433
- case 741420: return @"nwse-resize"; // se-resize → nwse-resize
1434
- case 741424: return @"nesw-resize"; // sw-resize → nesw-resize
1435
- case 741426: return @"col-resize"; // ew-resize → col-resize
1436
- case 741436: return @"ns-resize";
1437
- case 741438: return @"nesw-resize";
1438
- case 741442: return @"nwse-resize";
1439
- case 741444: return @"zoom-in";
1440
- case 741446: return @"zoom-out";
1441
- default: return nil;
1442
- }
1443
- }
1444
-
1445
- // Image-based cursor detection using known patterns from mapping
1446
- static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, NSCursor *cursor) {
1447
- if (!image) {
1448
- return nil;
1449
- }
1450
-
1451
- NSSize size = [image size];
1452
- CGFloat width = size.width;
1453
- CGFloat height = size.height;
1454
- CGFloat aspectRatio = width > 0 ? width / height : 0;
1455
- CGFloat relativeX = width > 0 ? hotspot.x / width : 0;
1456
- CGFloat relativeY = height > 0 ? hotspot.y / height : 0;
1457
-
1458
- // Tolerance for floating point comparison
1459
- CGFloat tolerance = 0.05;
1460
- CGFloat tightTolerance = 0.02; // For precise hotspot matching
1461
-
1462
- // Helper lambda for approximate comparison
1463
- auto approx = [tolerance](CGFloat a, CGFloat b) -> BOOL {
1464
- return fabs(a - b) < tolerance;
1465
- };
1466
-
1467
- auto approxTight = [tightTolerance](CGFloat a, CGFloat b) -> BOOL {
1468
- return fabs(a - b) < tightTolerance;
1469
- };
1470
-
1471
- // Pattern matching based on cursor-nscursor-mapping.json
1472
-
1473
- // none: 1x1, ratio=1.0, hotspot=(0,0)
1474
- if (approx(width, 1) && approx(height, 1)) {
1475
- return @"none";
1476
- }
1477
-
1478
- // text: 22x23, ratio=0.956, hotspot rel=(0.52, 0.48)
1479
- if (approx(width, 22) && approx(height, 23) && approx(aspectRatio, 0.956)) {
1480
- return @"text";
1481
- }
1482
-
1483
- // vertical-text: 22x21, ratio=1.047, hotspot rel=(0.5, 0.476)
1484
- if (approx(width, 22) && approx(height, 21) && approx(aspectRatio, 1.047)) {
1485
- return @"vertical-text";
1486
- }
1487
-
1488
- // pointer: 32x32, ratio=1.0, hotspot rel=(0.406, 0.25)
1489
- if (approx(width, 32) && approx(height, 32) && approx(relativeY, 0.25)) {
1490
- return @"pointer";
1491
- }
1492
-
1493
- // grab/grabbing: 32x32, ratio=1.0, hotspot rel=(0.5, 0.531)
1494
- // Distinguished by pointer equality
1495
- if (approx(width, 32) && approx(height, 32) && approx(relativeY, 0.531)) {
1496
- if (cursor) {
1497
- if (cursor == [NSCursor closedHandCursor]) {
1498
- return @"grabbing";
1499
- }
1500
- if (cursor == [NSCursor openHandCursor]) {
1501
- return @"grab";
1502
- }
1503
- }
1504
- return @"grab"; // Default to grab if can't distinguish
1505
- }
1506
-
1507
- // 24x24 cursors: crosshair vs move/all-scroll
1508
- // Distinguished by precise hotspot position
1509
- if (approx(width, 24) && approx(height, 24)) {
1510
- // crosshair: hotspot rel=(0.458, 0.458)
1511
- if (approxTight(relativeX, 0.458) && approxTight(relativeY, 0.458)) {
1512
- return @"crosshair";
1513
- }
1514
- // move/all-scroll: hotspot rel=(0.5, 0.5)
1515
- if (approxTight(relativeX, 0.5) && approxTight(relativeY, 0.5)) {
1516
- return @"move"; // or all-scroll, they're identical
1517
- }
1518
- // Fallback for 24x24
1519
- return @"crosshair";
1520
- }
1521
-
1522
- // help/cell: 18x18, ratio=1.0, hotspot rel=(0.5, 0.5)
1523
- // NOTE: Cannot distinguish between help and cell by image alone
1524
- if (approx(width, 18) && approx(height, 18)) {
1525
- return @"cell"; // Default to cell for compatibility
1526
- }
1527
-
1528
- // col-resize: 30x24, ratio=1.25, hotspot rel=(0.5, 0.5)
1529
- if (approx(width, 30) && approx(height, 24) && approx(aspectRatio, 1.25)) {
1530
- return @"col-resize";
1531
- }
1532
-
1533
- // e-resize/w-resize/ew-resize: 24x18, ratio=1.333, hotspot rel=(0.5, 0.5)
1534
- // Distinguish using pointer equality
1535
- if (approx(width, 24) && approx(height, 18) && approx(aspectRatio, 1.333)) {
1536
- if (cursor) {
1537
- if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] &&
1538
- cursor == [NSCursor resizeLeftCursor]) {
1539
- return @"w-resize";
1540
- }
1541
- if ([NSCursor respondsToSelector:@selector(resizeRightCursor)] &&
1542
- cursor == [NSCursor resizeRightCursor]) {
1543
- return @"e-resize";
1544
- }
1545
- if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)] &&
1546
- cursor == [NSCursor resizeLeftRightCursor]) {
1547
- return @"ew-resize";
1548
- }
1549
- }
1550
- return @"ew-resize"; // Default to ew-resize
1551
- }
1552
-
1553
- // row-resize: 24x28, ratio=0.857, hotspot rel=(0.5, 0.5)
1554
- if (approx(width, 24) && approx(height, 28) && approx(aspectRatio, 0.857)) {
1555
- return @"row-resize";
1556
- }
1557
-
1558
- // n-resize/s-resize/ns-resize: 18x28, ratio=0.643, hotspot rel=(0.5, 0.5)
1559
- // Distinguish using pointer equality
1560
- if (approx(width, 18) && approx(height, 28) && approx(aspectRatio, 0.643)) {
1561
- if (cursor) {
1562
- if ([NSCursor respondsToSelector:@selector(resizeUpCursor)] &&
1563
- cursor == [NSCursor resizeUpCursor]) {
1564
- return @"n-resize";
1565
- }
1566
- if ([NSCursor respondsToSelector:@selector(resizeDownCursor)] &&
1567
- cursor == [NSCursor resizeDownCursor]) {
1568
- return @"s-resize";
1569
- }
1570
- if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)] &&
1571
- cursor == [NSCursor resizeUpDownCursor]) {
1572
- return @"ns-resize";
1573
- }
1574
- }
1575
- return @"ns-resize"; // Default to ns-resize
1576
- }
1577
-
1578
- // ne-resize/nw-resize/se-resize/sw-resize/nesw-resize/nwse-resize: 22x22, ratio=1.0, hotspot rel=(0.5, 0.5)
1579
- if (approx(width, 22) && approx(height, 22)) {
1580
- return @"nwse-resize"; // Default to nwse-resize for all diagonal cursors
1581
- }
1582
-
1583
- // zoom-in/zoom-out: 28x26, ratio=1.077, hotspot rel=(0.428, 0.423)
1584
- // NOTE: Cannot distinguish between zoom-in and zoom-out by image or pointer alone
1585
- // They use the same image and there's no standard NSCursor for zoom
1586
- if (approx(width, 28) && approx(height, 26) && approx(aspectRatio, 1.077)) {
1587
- return @"zoom-in"; // Default to zoom-in (cannot distinguish from zoom-out)
1588
- }
1589
-
1590
- // alias: 16x21, ratio=0.762, hotspot rel=(0.688, 0.143)
1591
- if (approx(width, 16) && approx(height, 21) && approx(aspectRatio, 0.762)) {
1592
- return @"alias";
1593
- }
1594
-
1595
- // 28x40 cursors: default/auto vs context-menu/progress/wait/copy/no-drop/not-allowed
1596
- // Distinguished by precise hotspot position and pointer equality
1597
- if (approx(width, 28) && approx(height, 40) && approx(aspectRatio, 0.7)) {
1598
- // auto/default: hotspot rel=(0.161, 0.1) - hotspot at (4.5, 4)
1599
- if (approxTight(relativeX, 0.161) && approxTight(relativeY, 0.1)) {
1600
- return @"default";
1601
- }
1602
- // context-menu/progress/wait/copy/no-drop/not-allowed: hotspot rel=(0.179, 0.125) - hotspot at (5, 5)
1603
- if (approxTight(relativeX, 0.179) && approxTight(relativeY, 0.125)) {
1604
- // Try pointer equality for standard cursors
1605
- if (cursor) {
1606
- if (cursor == [NSCursor contextualMenuCursor]) {
1607
- return @"context-menu";
1608
- }
1609
- if (cursor == [NSCursor dragCopyCursor]) {
1610
- return @"copy";
1611
- }
1612
- if (cursor == [NSCursor operationNotAllowedCursor]) {
1613
- return @"not-allowed";
1614
- }
1615
- }
1616
- // NOTE: progress, wait, no-drop don't have standard NSCursor pointers
1617
- // Return "progress" as default for this hotspot pattern (better than "default")
1618
- // Let cursor name detection in caller distinguish between progress/wait
1619
- return @"progress";
1620
- }
1621
- return @"default";
1622
- }
1623
-
1624
- return nil;
1625
- }
1626
-
1627
1225
  static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
1628
- if (!cursor) {
1629
- return @"default";
1630
- }
1631
-
1632
- // PRIORITY 1: Standard macOS cursor pointer equality (fastest and most reliable)
1633
- if (cursor == [NSCursor arrowCursor]) {
1634
- return @"default";
1635
- }
1636
- if (cursor == [NSCursor IBeamCursor]) {
1637
- return @"text";
1638
- }
1639
- if ([NSCursor respondsToSelector:@selector(IBeamCursorForVerticalLayout)] &&
1640
- cursor == [NSCursor IBeamCursorForVerticalLayout]) {
1641
- return @"text";
1642
- }
1643
- if (cursor == [NSCursor pointingHandCursor]) {
1644
- return @"pointer";
1645
- }
1646
- if (cursor == [NSCursor crosshairCursor]) {
1647
- return @"crosshair";
1648
- }
1649
- if (cursor == [NSCursor openHandCursor]) {
1650
- return @"grab";
1651
- }
1652
- if (cursor == [NSCursor closedHandCursor]) {
1653
- return @"grabbing";
1654
- }
1655
- if (cursor == [NSCursor operationNotAllowedCursor]) {
1656
- return @"not-allowed";
1657
- }
1658
- if (cursor == [NSCursor dragCopyCursor]) {
1659
- return @"copy";
1660
- }
1661
- if (cursor == [NSCursor dragLinkCursor]) {
1662
- return @"alias";
1663
- }
1664
- if (cursor == [NSCursor contextualMenuCursor]) {
1665
- return @"context-menu";
1666
- }
1667
-
1668
- // Resize cursors
1669
- if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)]) {
1670
- if (cursor == [NSCursor resizeLeftRightCursor]) {
1671
- return @"col-resize";
1672
- }
1673
- }
1674
- if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)]) {
1675
- if (cursor == [NSCursor resizeUpDownCursor]) {
1676
- return @"row-resize";
1677
- }
1678
- }
1679
-
1680
- NSString *privateCursorName = CursorNameFromNSCursor(cursor);
1681
- if (privateCursorName) {
1682
- NSString *normalizedName = NormalizeCursorName(privateCursorName);
1683
- NSString *mappedType = normalizedName ? [g_cursorNameMap objectForKey:normalizedName] : nil;
1684
- if (mappedType) {
1685
- CacheCursorFingerprint(cursor, mappedType, nil);
1686
- return mappedType;
1687
- }
1688
- NSString *typeFromName = cursorTypeFromCursorName(privateCursorName);
1689
- if (typeFromName) {
1690
- RegisterCursorNameMapping(privateCursorName, typeFromName);
1691
- CacheCursorFingerprint(cursor, typeFromName, nil);
1692
- return typeFromName;
1693
- }
1694
- }
1226
+ if (!cursor) return @"default";
1227
+ InitializeCursorFingerprintMap();
1695
1228
 
1696
- NSString *fingerprintHint = nil;
1697
- NSString *fingerprintMatch = LookupCursorTypeByFingerprint(cursor, &fingerprintHint);
1698
- if (fingerprintMatch) {
1699
- return fingerprintMatch;
1700
- }
1229
+ // Image evidence is authoritative. currentSystemCursor returns detached
1230
+ // NSCursor objects, so pointer identity and image dimensions cannot identify
1231
+ // the shape (opposite diagonals and zoom +/- share the same dimensions).
1232
+ NSString *fingerprintType = LookupCursorTypeByFingerprint(cursor, NULL);
1233
+ if (fingerprintType) return fingerprintType;
1701
1234
 
1702
- // PRIORITY 2: Image-based detection (for browser custom cursors)
1703
- NSImage *cursorImage = [cursor image];
1704
- NSPoint hotspot = [cursor hotSpot];
1705
- NSString *imageBasedType = cursorTypeFromImageSignature(cursorImage, hotspot, cursor);
1706
- if (imageBasedType) {
1707
- if (![imageBasedType isEqualToString:@"default"]) {
1708
- CacheCursorFingerprint(cursor, imageBasedType, fingerprintHint);
1709
- }
1710
- return imageBasedType;
1711
- }
1235
+ // Some standard cursors have lazily populated images in headless Node hosts.
1236
+ if (cursor == [NSCursor arrowCursor]) return @"default";
1237
+ if (cursor == [NSCursor IBeamCursor] || cursor == [NSCursor IBeamCursorForVerticalLayout]) return @"text";
1712
1238
 
1713
- // PRIORITY 3: Name-based detection
1714
- NSString *className = NSStringFromClass([cursor class]);
1715
- NSString *derived = cursorTypeFromCursorName(className);
1716
- if (derived) {
1717
- if (![derived isEqualToString:@"default"]) {
1718
- CacheCursorFingerprint(cursor, derived, fingerprintHint);
1719
- }
1720
- return derived;
1721
- }
1722
-
1723
- // Default fallback
1724
- return @"default";
1239
+ NSString *name = CursorNameFromNSCursor(cursor);
1240
+ NSString *mapped = name ? [g_cursorNameMap objectForKey:NormalizeCursorName(name)] : nil;
1241
+ return mapped ?: cursorTypeFromCursorName(name) ?: @"default";
1725
1242
  }
1726
1243
 
1727
1244
  static NSString* detectSystemCursorType(void) {
1728
1245
  InitializeCursorFingerprintMap();
1729
1246
  __block NSString *cursorType = nil;
1730
- __block NSCursor *detectedCursor = nil;
1731
-
1732
- NSString *cgsName = CopyCurrentCursorNameFromCGS();
1733
- if (cgsName && [cgsName length] > 0) {
1734
- NSString *normalized = NormalizeCursorName(cgsName);
1735
- NSString *mapped = normalized ? [g_cursorNameMap objectForKey:normalized] : nil;
1736
- if (mapped) {
1737
- return mapped;
1738
- }
1739
- NSString *derivedFromName = cursorTypeFromCursorName(cgsName);
1740
- if (derivedFromName) {
1741
- RegisterCursorNameMapping(cgsName, derivedFromName);
1742
- return derivedFromName;
1743
- }
1744
- }
1745
-
1746
- int cursorSeed = SafeCGSCurrentCursorSeed();
1747
- if (cursorSeed > 0) {
1748
- NSString *seedType = cursorTypeFromSeed(cursorSeed);
1749
- if (seedType) {
1750
- return seedType;
1751
- }
1752
- }
1753
-
1754
1247
  void (^fetchCursorBlock)(void) = ^{
1755
- NSCursor *currentCursor = nil;
1756
-
1757
- // Try different methods to get current cursor
1248
+ NSCursor *cursor = nil;
1758
1249
  if ([NSCursor respondsToSelector:@selector(currentSystemCursor)]) {
1759
- currentCursor = [NSCursor currentSystemCursor];
1250
+ cursor = [NSCursor currentSystemCursor];
1760
1251
  }
1761
-
1762
- if (!currentCursor) {
1763
- currentCursor = [NSCursor currentCursor];
1252
+ if (cursor && cursor.image.size.width > 0 && cursor.image.size.height > 0) {
1253
+ cursorType = cursorTypeFromNSCursor(cursor);
1254
+ return;
1764
1255
  }
1765
-
1766
- detectedCursor = currentCursor; // Save for seed learning
1767
-
1768
- if (currentCursor) {
1769
- NSString *directType = cursorTypeFromNSCursor(currentCursor);
1770
- NSString *fallbackType = directType;
1771
-
1772
- if (directType && ![directType isEqualToString:@"default"]) {
1773
- cursorType = directType;
1774
- return;
1775
- }
1776
-
1777
- NSString *className = NSStringFromClass([currentCursor class]);
1778
- NSString *description = [currentCursor description];
1779
- // Use more direct cursor detection approach
1780
- NSImage *cursorImage = [currentCursor image];
1781
- NSPoint hotspot = [currentCursor hotSpot];
1782
- NSSize imageSize = [cursorImage size];
1783
-
1784
- // ROBUST cursor detection - works with any cursor size
1785
- CGFloat aspectRatio = imageSize.width / imageSize.height;
1786
- CGFloat relativeHotspotX = hotspot.x / imageSize.width;
1787
- CGFloat relativeHotspotY = hotspot.y / imageSize.height;
1788
- BOOL fallbackDefaults = (fallbackType && [fallbackType isEqualToString:@"default"]);
1789
- BOOL cursorNameSuggestsText = NO;
1790
- if (className && ([className localizedCaseInsensitiveContainsString:@"ibeam"] ||
1791
- [className localizedCaseInsensitiveContainsString:@"text"])) {
1792
- cursorNameSuggestsText = YES;
1793
- }
1794
- if (description && ([description localizedCaseInsensitiveContainsString:@"ibeam"] ||
1795
- [description localizedCaseInsensitiveContainsString:@"text"])) {
1796
- cursorNameSuggestsText = YES;
1797
- }
1798
-
1799
-
1800
-
1801
- // UPDATED with real cursor data:
1802
- // Arrow: 17x23 ratio=0.74 hotspot=(0.24,0.17)
1803
- // Text: 9x18 ratio=0.50 hotspot=(0.44,0.50)
1804
- // Pointer: 32x32 ratio=1.00 hotspot=(0.41,0.25)
1805
-
1806
- // 1. TEXT/I-BEAM CURSOR - narrow ratio, center hotspot
1807
- if (aspectRatio >= 0.45 && aspectRatio <= 0.60 && // Narrow (0.50 typical)
1808
- relativeHotspotX >= 0.35 && relativeHotspotX <= 0.55 && // Center X (0.44 typical)
1809
- relativeHotspotY >= 0.40 && relativeHotspotY <= 0.60) { // Center Y (0.50 typical)
1810
- BOOL directSaysText = (directType && [directType isEqualToString:@"text"]);
1811
- if (cursorNameSuggestsText || directSaysText) {
1812
- cursorType = @"text";
1813
- } else {
1814
- cursorType = fallbackType ?: @"default";
1815
- }
1816
- }
1817
- // 2. ARROW CURSOR - medium ratio, top-left hotspot
1818
- else if (aspectRatio >= 0.65 && aspectRatio <= 0.85 && // Medium (0.74 typical)
1819
- relativeHotspotX >= 0.15 && relativeHotspotX <= 0.35 && // Left side (0.24 typical)
1820
- relativeHotspotY >= 0.10 && relativeHotspotY <= 0.25) { // Top area (0.17 typical)
1821
- cursorType = @"default";
1822
- }
1823
- // 3. POINTER CURSOR - square ratio, left-center hotspot
1824
- else if (aspectRatio >= 0.90 && aspectRatio <= 1.10 && // Square (1.00 typical)
1825
- relativeHotspotX >= 0.30 && relativeHotspotX <= 0.50 && // Left-center (0.41 typical)
1826
- relativeHotspotY >= 0.15 && relativeHotspotY <= 0.35) { // Upper area (0.25 typical)
1827
- cursorType = @"pointer";
1828
- }
1829
- else {
1830
- // Try to use a different approach - cursor name introspection
1831
- NSString *derived = cursorTypeFromNSCursor(currentCursor);
1832
- if (derived && ![derived isEqualToString:@"default"]) {
1833
- cursorType = derived;
1834
- // NSLog(@"🎯 DERIVED FROM ANALYSIS: %@", cursorType);
1835
- } else {
1836
- cursorType = fallbackType ?: @"default";
1837
- // NSLog(@"🎯 FALLBACK TO DEFAULT (will check AX)");
1838
- }
1839
- }
1840
-
1841
- if (cursorType && ![cursorType isEqualToString:@"default"]) {
1842
- CacheCursorFingerprint(currentCursor, cursorType, nil);
1843
- }
1844
- } else {
1845
- // NSLog(@"🖱️ No current cursor found");
1846
- cursorType = @"default";
1256
+ NSString *name = CopyCurrentCursorNameFromCGS();
1257
+ NSString *mapped = name ? [g_cursorNameMap objectForKey:NormalizeCursorName(name)] : nil;
1258
+ cursorType = mapped ?: cursorTypeFromCursorName(name);
1259
+ // currentCursor belongs to this process. It must never replace another
1260
+ // application's system cursor while the recorder runs in the background.
1261
+ if (!cursorType && NSApp.isActive) {
1262
+ cursorType = cursorTypeFromNSCursor([NSCursor currentCursor]);
1847
1263
  }
1848
1264
  };
1849
-
1850
1265
  if ([NSThread isMainThread]) {
1851
1266
  fetchCursorBlock();
1852
1267
  } else {
1853
1268
  dispatch_sync(dispatch_get_main_queue(), fetchCursorBlock);
1854
1269
  }
1855
-
1856
- if (cursorType && ![cursorType isEqualToString:@"default"] && cursorSeed > 0) {
1857
- addCursorToSeedMap(cursorType, cursorSeed);
1858
- }
1859
-
1860
- return cursorType;
1270
+ return cursorType ?: @"default";
1861
1271
  }
1862
1272
 
1863
1273
  // Desktop'ta SVG karşılığı olmayan cursor tiplerini desteklenen tiplere normalize et
@@ -1950,6 +1360,7 @@ NSString* getCursorType() {
1950
1360
  } else {
1951
1361
  NSLog(@"🎯 %@", finalType);
1952
1362
  }
1363
+ [lastLoggedType release];
1953
1364
  lastLoggedType = [finalType copy];
1954
1365
  }
1955
1366
  return finalType;