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.
- package/.claude/settings.local.json +8 -1
- package/analyze-cursors.js +90 -0
- package/build-seed-map.js +219 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +1205 -92
package/src/cursor_tracker.mm
CHANGED
|
@@ -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 @"
|
|
1522
|
+
return @"context-menu";
|
|
556
1523
|
}
|
|
557
1524
|
|
|
558
|
-
// Resize cursors
|
|
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";
|
|
1533
|
+
return @"row-resize";
|
|
577
1534
|
}
|
|
578
1535
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1553
|
+
NSString *fingerprintHint = nil;
|
|
1554
|
+
NSString *fingerprintMatch = LookupCursorTypeByFingerprint(cursor, &fingerprintHint);
|
|
1555
|
+
if (fingerprintMatch) {
|
|
1556
|
+
return fingerprintMatch;
|
|
593
1557
|
}
|
|
594
1558
|
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
//
|
|
1570
|
+
// PRIORITY 3: Name-based detection
|
|
1571
|
+
NSString *className = NSStringFromClass([cursor class]);
|
|
610
1572
|
NSString *derived = cursorTypeFromCursorName(className);
|
|
611
1573
|
if (derived) {
|
|
612
|
-
|
|
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
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
}
|