node-mac-recorder 2.22.32 → 2.22.33

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.
@@ -223,6 +223,7 @@ class MultiWindowRecorder extends EventEmitter {
223
223
  } : null,
224
224
  recordingType: 'multi-window', // Multi-window recording type
225
225
  startTimestamp: startTimestamp, // Use same timestamp as video
226
+ interval: 33,
226
227
  // Pass window information for location detection
227
228
  multiWindowBounds: windowBounds
228
229
  };
package/index.js CHANGED
@@ -869,7 +869,8 @@ class MacRecorder extends EventEmitter {
869
869
  this.options.captureArea ? 'area' : 'display',
870
870
  captureArea: this.options.captureArea,
871
871
  windowId: this.options.windowId,
872
- startTimestamp: syncTimestamp // Align cursor timeline to actual start
872
+ startTimestamp: syncTimestamp,
873
+ interval: 33,
873
874
  };
874
875
 
875
876
  try {
@@ -1348,6 +1349,13 @@ class MacRecorder extends EventEmitter {
1348
1349
  return true; // İlk event
1349
1350
  }
1350
1351
 
1352
+ if (
1353
+ currentData.type === "drag" ||
1354
+ currentData.type === "rightdrag"
1355
+ ) {
1356
+ return true;
1357
+ }
1358
+
1351
1359
  const last = this.lastCapturedData;
1352
1360
 
1353
1361
  // Event type değişmişse
@@ -1382,10 +1390,11 @@ class MacRecorder extends EventEmitter {
1382
1390
  * @param {Object} options.captureArea - Capture area for area recording coordinate transformation
1383
1391
  * @param {number} options.windowId - Window ID for window recording coordinate transformation
1384
1392
  * @param {number} options.startTimestamp - Pre-defined start timestamp for synchronization (optional)
1393
+ * @param {number} options.interval - Örnekleme aralığı (ms), filepath ile çağrıda kullanılır; varsayılan 33ms (~30 Hz)
1385
1394
  */
1386
1395
  async startCursorCapture(intervalOrFilepath = 100, options = {}) {
1387
1396
  let filepath;
1388
- let interval = 20; // Default 50 FPS
1397
+ let interval = 33;
1389
1398
 
1390
1399
  // Parameter parsing: number = interval, string = filepath
1391
1400
  if (typeof intervalOrFilepath === "number") {
@@ -1399,6 +1408,10 @@ class MacRecorder extends EventEmitter {
1399
1408
  );
1400
1409
  }
1401
1410
 
1411
+ if (typeof options.interval === "number") {
1412
+ interval = Math.max(10, options.interval);
1413
+ }
1414
+
1402
1415
  if (this.cursorCaptureInterval) {
1403
1416
  throw new Error("Cursor capture is already running");
1404
1417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.22.32",
3
+ "version": "2.22.33",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -43,7 +43,9 @@
43
43
  "build:electron-safe": "node build-electron-safe.js",
44
44
  "test:electron-safe": "node test-electron-safe.js",
45
45
  "clean:electron-safe": "node-gyp clean && rm -rf build",
46
- "canvas": "node make-canvas.js"
46
+ "canvas": "node make-canvas.js",
47
+ "cursor:live": "node scripts/cursor-type-live.js",
48
+ "test:cursor-types": "node scripts/cursor-types-15s-test.js"
47
49
  },
48
50
  "dependencies": {
49
51
  "node-addon-api": "^7.0.0"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ const MacRecorder = require('../index.js');
4
+
5
+ const INTERVAL_MS = 33;
6
+
7
+ const recorder = new MacRecorder();
8
+
9
+ process.stderr.write(
10
+ `Canlı cursor tipi (${INTERVAL_MS}ms). Çıkmak: Ctrl+C\n`
11
+ );
12
+
13
+ function tick() {
14
+ try {
15
+ const p = recorder.getCursorPosition();
16
+ const type = p.cursorType != null ? String(p.cursorType) : '?';
17
+ const seed =
18
+ typeof p.seed === 'number' && p.seed > 0 ? ` seed:${p.seed}` : '';
19
+ process.stdout.write(`\r\x1b[KcursorType: ${type}${seed}`);
20
+ } catch (e) {
21
+ process.stdout.write(`\r\x1b[K${e.message || e}`);
22
+ }
23
+ }
24
+
25
+ const id = setInterval(tick, INTERVAL_MS);
26
+ tick();
27
+
28
+ process.on('SIGINT', () => {
29
+ clearInterval(id);
30
+ process.stdout.write('\n');
31
+ process.exit(0);
32
+ });
@@ -0,0 +1,122 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const MacRecorder = require('../index.js');
5
+
6
+ const OUT_DIR = path.join(__dirname, '..', 'test-output');
7
+ const DURATION_SEC = 15;
8
+
9
+ async function sleep(ms) {
10
+ return new Promise((r) => setTimeout(r, ms));
11
+ }
12
+
13
+ function summarizeCursorTypes(cursorJsonPath) {
14
+ if (!cursorJsonPath || !fs.existsSync(cursorJsonPath)) {
15
+ return { counts: {}, ordered: [] };
16
+ }
17
+ const raw = fs.readFileSync(cursorJsonPath, 'utf8');
18
+ let events;
19
+ try {
20
+ events = JSON.parse(raw);
21
+ } catch {
22
+ return { counts: {}, ordered: [] };
23
+ }
24
+ if (!Array.isArray(events)) {
25
+ return { counts: {}, ordered: [] };
26
+ }
27
+ const counts = {};
28
+ const order = [];
29
+ for (const ev of events) {
30
+ const t = ev && ev.cursorType ? String(ev.cursorType) : '?';
31
+ if (counts[t] === undefined) {
32
+ counts[t] = 0;
33
+ order.push(t);
34
+ }
35
+ counts[t]++;
36
+ }
37
+ return { counts, ordered: order };
38
+ }
39
+
40
+ async function main() {
41
+ fs.mkdirSync(OUT_DIR, { recursive: true });
42
+
43
+ const stamp = Date.now();
44
+ const screenPath = path.join(OUT_DIR, `cursor-types-test-${stamp}.mov`);
45
+
46
+ const recorder = new MacRecorder();
47
+
48
+ const displays = await recorder.getDisplays();
49
+ const cameras = await recorder.getCameraDevices();
50
+ const audioDevices = await recorder.getAudioDevices();
51
+
52
+ if (!displays.length) {
53
+ process.stderr.write('No displays.\n');
54
+ process.exit(1);
55
+ }
56
+
57
+ const display = displays[0];
58
+
59
+ let cursorPathFromStart = null;
60
+ recorder.once('recordingStarted', (info) => {
61
+ cursorPathFromStart = info.cursorOutputPath || null;
62
+ });
63
+
64
+ const options = {
65
+ displayId: display.id,
66
+ captureCamera: cameras.length > 0,
67
+ cameraDeviceId: cameras.length > 0 ? cameras[0].id : undefined,
68
+ includeMicrophone: audioDevices.length > 0,
69
+ audioDeviceId: audioDevices.length > 0 ? audioDevices[0].id : undefined,
70
+ includeSystemAudio: true,
71
+ captureCursor: true,
72
+ frameRate: 60,
73
+ };
74
+
75
+ process.stdout.write(
76
+ `Kayıt ${DURATION_SEC}s: ekran + sistem sesi + mik ${
77
+ options.includeMicrophone ? 'açık' : 'kapalı'
78
+ } + kamera ${options.captureCamera ? 'açık' : 'kapalı'} + cursor JSON.\n` +
79
+ `Kenarlarda resize, köşelerde çapraz, grab/grabbing ve crosshair deneyin.\n\n`
80
+ );
81
+
82
+ await recorder.startRecording(screenPath, options);
83
+
84
+ const cursorPath = cursorPathFromStart;
85
+
86
+ for (let i = 1; i <= DURATION_SEC; i++) {
87
+ await sleep(1000);
88
+ process.stdout.write(`${i}s `);
89
+ }
90
+ process.stdout.write('\n');
91
+
92
+ await recorder.stopRecording();
93
+
94
+ process.stdout.write('\nDosyalar:\n');
95
+ process.stdout.write(` Ekran: ${screenPath}\n`);
96
+ if (options.captureCamera && cameras.length) {
97
+ process.stdout.write(
98
+ ` Kamera: kayıt başladıktan sonra native temp_camera_* ile aynı oturum\n`
99
+ );
100
+ }
101
+ if (options.includeMicrophone || options.includeSystemAudio) {
102
+ process.stdout.write(
103
+ ` Ses: kayıt başladıktan sonra native temp_audio_* ile aynı oturum\n`
104
+ );
105
+ }
106
+ process.stdout.write(` Cursor JSON: ${cursorPath || 'bilinmiyor'}\n`);
107
+
108
+ if (cursorPath && fs.existsSync(cursorPath)) {
109
+ const { counts, ordered } = summarizeCursorTypes(cursorPath);
110
+ process.stdout.write('\nCursor JSON — cursorType özeti (ilk görülme sırası):\n');
111
+ for (const t of ordered) {
112
+ process.stdout.write(` ${t}: ${counts[t]}\n`);
113
+ }
114
+ }
115
+
116
+ recorder.removeAllListeners();
117
+ }
118
+
119
+ main().catch((e) => {
120
+ process.stderr.write(String(e && e.message ? e.message : e) + '\n');
121
+ process.exit(1);
122
+ });
@@ -8,6 +8,7 @@
8
8
  #import <dispatch/dispatch.h>
9
9
  #import "logging.h"
10
10
  #include <vector>
11
+ #include <cstring>
11
12
  #include <math.h>
12
13
 
13
14
  #ifndef kAXHitTestParameterizedAttribute
@@ -309,6 +310,180 @@ static NSCursor* CursorFromSelector(SEL selector) {
309
310
  return func([NSCursor class], selector);
310
311
  }
311
312
 
313
+ static BOOL CursorEqualsFactoryNamed(NSCursor *cursor, NSString *factoryMethodName) {
314
+ if (!cursor || !factoryMethodName || [factoryMethodName length] == 0) {
315
+ return NO;
316
+ }
317
+ SEL sel = NSSelectorFromString(factoryMethodName);
318
+ NSCursor *ref = CursorFromSelector(sel);
319
+ return ref != nil && cursor == ref;
320
+ }
321
+
322
+ static uint64_t CursorImagePixelHash(NSImage *image) {
323
+ if (!image) {
324
+ return 0;
325
+ }
326
+ NSRect imageRect = NSMakeRect(0, 0, [image size].width, [image size].height);
327
+ CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:nil hints:nil];
328
+ if (!cgImage) {
329
+ for (NSImageRep *rep in [image representations]) {
330
+ if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
331
+ cgImage = [(NSBitmapImageRep *)rep CGImage];
332
+ if (cgImage) {
333
+ break;
334
+ }
335
+ }
336
+ }
337
+ }
338
+ if (!cgImage) {
339
+ return 0;
340
+ }
341
+ size_t width = CGImageGetWidth(cgImage);
342
+ size_t height = CGImageGetHeight(cgImage);
343
+ if (width == 0 || height == 0) {
344
+ return 0;
345
+ }
346
+ size_t bytesPerPixel = 4;
347
+ size_t bytesPerRow = width * bytesPerPixel;
348
+ size_t bufferSize = bytesPerRow * height;
349
+ if (bufferSize == 0) {
350
+ return 0;
351
+ }
352
+ std::vector<unsigned char> buffer(bufferSize);
353
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
354
+ if (!colorSpace) {
355
+ return 0;
356
+ }
357
+ CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedLast;
358
+ CGContextRef context = CGBitmapContextCreate(buffer.data(),
359
+ width,
360
+ height,
361
+ 8,
362
+ bytesPerRow,
363
+ colorSpace,
364
+ bitmapInfo);
365
+ CGColorSpaceRelease(colorSpace);
366
+ if (!context) {
367
+ return 0;
368
+ }
369
+ CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
370
+ CGContextRelease(context);
371
+ return FNV1AHash(buffer.data(), buffer.size());
372
+ }
373
+
374
+ static uint64_t CursorImagePixelHashNormalized32(NSImage *image) {
375
+ if (!image) {
376
+ return 0;
377
+ }
378
+ const int dim = 32;
379
+ size_t bytesPerPixel = 4;
380
+ size_t bytesPerRow = (size_t)dim * bytesPerPixel;
381
+ std::vector<unsigned char> buffer(bytesPerRow * (size_t)dim);
382
+ memset(buffer.data(), 0, buffer.size());
383
+
384
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
385
+ if (!colorSpace) {
386
+ return 0;
387
+ }
388
+ CGContextRef ctx = CGBitmapContextCreate(buffer.data(),
389
+ (size_t)dim,
390
+ (size_t)dim,
391
+ 8,
392
+ bytesPerRow,
393
+ colorSpace,
394
+ kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
395
+ CGColorSpaceRelease(colorSpace);
396
+ if (!ctx) {
397
+ return 0;
398
+ }
399
+
400
+ CGContextClearRect(ctx, CGRectMake(0, 0, dim, dim));
401
+ NSGraphicsContext *nsgc = [NSGraphicsContext graphicsContextWithCGContext:ctx flipped:YES];
402
+ [NSGraphicsContext saveGraphicsState];
403
+ [NSGraphicsContext setCurrentContext:nsgc];
404
+
405
+ NSSize sz = [image size];
406
+ if (sz.width <= 0 || sz.height <= 0) {
407
+ [NSGraphicsContext restoreGraphicsState];
408
+ CGContextRelease(ctx);
409
+ return 0;
410
+ }
411
+ CGFloat scale = MIN((CGFloat)dim / sz.width, (CGFloat)dim / sz.height);
412
+ CGFloat rw = sz.width * scale;
413
+ CGFloat rh = sz.height * scale;
414
+ CGFloat ox = ((CGFloat)dim - rw) / 2.0;
415
+ CGFloat oy = ((CGFloat)dim - rh) / 2.0;
416
+ [image drawInRect:NSMakeRect(ox, oy, rw, rh)
417
+ fromRect:NSZeroRect
418
+ operation:NSCompositingOperationSourceOver
419
+ fraction:1.0
420
+ respectFlipped:YES
421
+ hints:nil];
422
+
423
+ [NSGraphicsContext restoreGraphicsState];
424
+ CGContextRelease(ctx);
425
+ return FNV1AHash(buffer.data(), buffer.size());
426
+ }
427
+
428
+ static NSString* DiagonalResizeTypeByVisualMatch(NSCursor *cursor) {
429
+ if (!cursor) {
430
+ return nil;
431
+ }
432
+ if (CursorEqualsFactoryNamed(cursor, @"resizeNorthWestSouthEastCursor")) {
433
+ return @"nwse-resize";
434
+ }
435
+ if (CursorEqualsFactoryNamed(cursor, @"resizeNorthEastSouthWestCursor")) {
436
+ return @"nesw-resize";
437
+ }
438
+
439
+ NSImage *curImg = [cursor image];
440
+ if (!curImg) {
441
+ return nil;
442
+ }
443
+
444
+ NSData *curTIFF = [curImg TIFFRepresentation];
445
+ uint64_t curRaw = CursorImagePixelHash(curImg);
446
+ uint64_t curNorm = CursorImagePixelHashNormalized32(curImg);
447
+
448
+ struct DiagonalFactoryEntry {
449
+ const char *selectorName;
450
+ const char *type;
451
+ };
452
+ static const DiagonalFactoryEntry kDiagonalFactories[] = {
453
+ { "resizeNorthWestSouthEastCursor", "nwse-resize" },
454
+ { "resizeNorthEastSouthWestCursor", "nesw-resize" },
455
+ };
456
+
457
+ for (size_t i = 0; i < sizeof(kDiagonalFactories) / sizeof(kDiagonalFactories[0]); i++) {
458
+ NSString *selStr = [NSString stringWithUTF8String:kDiagonalFactories[i].selectorName];
459
+ SEL sel = NSSelectorFromString(selStr);
460
+ if (![NSCursor respondsToSelector:sel]) {
461
+ continue;
462
+ }
463
+ NSCursor *ref = CursorFromSelector(sel);
464
+ if (!ref) {
465
+ continue;
466
+ }
467
+ NSImage *refImg = [ref image];
468
+ if (!refImg) {
469
+ continue;
470
+ }
471
+ NSData *refTIFF = [refImg TIFFRepresentation];
472
+ if (curTIFF && refTIFF && [curTIFF isEqualToData:refTIFF]) {
473
+ return [NSString stringWithUTF8String:kDiagonalFactories[i].type];
474
+ }
475
+ uint64_t refRaw = CursorImagePixelHash(refImg);
476
+ if (curRaw != 0 && curRaw == refRaw) {
477
+ return [NSString stringWithUTF8String:kDiagonalFactories[i].type];
478
+ }
479
+ uint64_t refNorm = CursorImagePixelHashNormalized32(refImg);
480
+ if (curNorm != 0 && curNorm == refNorm) {
481
+ return [NSString stringWithUTF8String:kDiagonalFactories[i].type];
482
+ }
483
+ }
484
+ return nil;
485
+ }
486
+
312
487
  static void AddStandardCursorFingerprint(NSCursor *cursor, NSString *cursorType) {
313
488
  if (!cursor || !cursorType) {
314
489
  return;
@@ -359,7 +534,7 @@ static void InitializeCursorFingerprintMap(void) {
359
534
  AddCursorIfAvailable(@selector(dragCopyCursor), @"copy");
360
535
  AddCursorIfAvailable(@selector(dragLinkCursor), @"alias");
361
536
  AddCursorIfAvailable(@selector(resizeLeftRightCursor), @"col-resize");
362
- AddCursorIfAvailable(@selector(resizeUpDownCursor), @"row-resize");
537
+ AddCursorIfAvailable(@selector(resizeUpDownCursor), @"ns-resize");
363
538
  AddCursorIfAvailableByName(@"resizeLeftCursor", @"col-resize");
364
539
  AddCursorIfAvailableByName(@"resizeRightCursor", @"col-resize");
365
540
  AddCursorIfAvailableByName(@"resizeUpCursor", @"ns-resize");
@@ -502,21 +677,26 @@ static BOOL ShouldEmitCursorEvent(CGPoint location, NSString *cursorType, NSStri
502
677
  BOOL moved = fabs(location.x - g_lastCursorLocation.x) >= movementThreshold ||
503
678
  fabs(location.y - g_lastCursorLocation.y) >= movementThreshold;
504
679
  BOOL eventChanged = !StringsEqual(eventType, g_lastCursorEventType);
505
- BOOL isMoveEvent = StringsEqual(eventType, @"move") || StringsEqual(eventType, @"drag");
680
+ BOOL isDragEvent = StringsEqual(eventType, @"drag") || StringsEqual(eventType, @"rightdrag");
681
+ BOOL isMoveOnly = StringsEqual(eventType, @"move");
506
682
  BOOL isClickEvent = StringsEqual(eventType, @"mousedown") ||
507
683
  StringsEqual(eventType, @"mouseup") ||
508
684
  StringsEqual(eventType, @"rightmousedown") ||
509
685
  StringsEqual(eventType, @"rightmouseup");
510
686
 
511
- if (isMoveEvent) {
512
- return moved;
687
+ if (isDragEvent) {
688
+ return YES;
689
+ }
690
+
691
+ if (isMoveOnly) {
692
+ BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
693
+ return moved || cursorChanged;
513
694
  }
514
695
 
515
696
  if (isClickEvent) {
516
697
  return eventChanged || moved;
517
698
  }
518
699
 
519
- // Fallback: only emit when something actually changed
520
700
  BOOL cursorChanged = !StringsEqual(cursorType, g_lastCursorType);
521
701
  return moved || cursorChanged || eventChanged;
522
702
  }
@@ -670,7 +850,10 @@ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint curso
670
850
  CFRelease(positionValue);
671
851
  CFRelease(sizeValue);
672
852
 
673
- CGFloat edge = 4.0;
853
+ // Köşe: köşe noktasına uzaklık (kutu sınırında col ↔ diagonal sıçramasını azaltır).
854
+ const CGFloat cornerRadius = 9.0;
855
+ const CGFloat edgeInset = 12.0;
856
+
674
857
  CGFloat x = cursorPos.x - windowOrigin.x;
675
858
  CGFloat y = cursorPos.y - windowOrigin.y;
676
859
  CGFloat w = windowSize.width;
@@ -680,17 +863,44 @@ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint curso
680
863
  return nil;
681
864
  }
682
865
 
683
- BOOL nearLeft = (x >= 0 && x <= edge);
684
- BOOL nearRight = (x >= w - edge && x <= w);
685
- BOOL nearTop = (y >= 0 && y <= edge);
686
- BOOL nearBottom = (y >= h - edge && y <= h);
866
+ if (w < cornerRadius * 2.1 || h < cornerRadius * 2.1) {
867
+ CGFloat edge = edgeInset;
868
+ BOOL nearLeft = (x >= 0 && x <= edge);
869
+ BOOL nearRight = (x >= w - edge && x <= w);
870
+ BOOL nearTop = (y >= 0 && y <= edge);
871
+ BOOL nearBottom = (y >= h - edge && y <= h);
872
+ if ((nearLeft && nearTop) || (nearRight && nearBottom)) {
873
+ return @"nwse-resize";
874
+ }
875
+ if ((nearRight && nearTop) || (nearLeft && nearBottom)) {
876
+ return @"nesw-resize";
877
+ }
878
+ if (nearLeft || nearRight) {
879
+ return @"col-resize";
880
+ }
881
+ if (nearTop || nearBottom) {
882
+ return @"ns-resize";
883
+ }
884
+ return nil;
885
+ }
886
+
887
+ CGFloat dBottomLeft = hypot(x, y);
888
+ CGFloat dTopRight = hypot(w - x, h - y);
889
+ CGFloat dBottomRight = hypot(w - x, y);
890
+ CGFloat dTopLeft = hypot(x, h - y);
687
891
 
688
- if ((nearLeft && nearTop) || (nearRight && nearBottom)) {
892
+ if (dBottomLeft <= cornerRadius || dTopRight <= cornerRadius) {
689
893
  return @"nwse-resize";
690
894
  }
691
- if ((nearRight && nearTop) || (nearLeft && nearBottom)) {
895
+ if (dBottomRight <= cornerRadius || dTopLeft <= cornerRadius) {
692
896
  return @"nesw-resize";
693
897
  }
898
+
899
+ BOOL nearLeft = (x >= 0 && x <= edgeInset);
900
+ BOOL nearRight = (x >= w - edgeInset && x <= w);
901
+ BOOL nearTop = (y >= 0 && y <= edgeInset);
902
+ BOOL nearBottom = (y >= h - edgeInset && y <= h);
903
+
694
904
  if (nearLeft || nearRight) {
695
905
  return @"col-resize";
696
906
  }
@@ -701,6 +911,42 @@ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint curso
701
911
  return nil;
702
912
  }
703
913
 
914
+ static NSString* ResizeTypeFromWindowUnderCursor(CGPoint cursorPos) {
915
+ @autoreleasepool {
916
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
917
+ if (!systemWide) {
918
+ return nil;
919
+ }
920
+ AXUIElementRef hit = NULL;
921
+ AXError err = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &hit);
922
+ CFRelease(systemWide);
923
+ if (err != kAXErrorSuccess || !hit) {
924
+ if (hit) {
925
+ CFRelease(hit);
926
+ }
927
+ return nil;
928
+ }
929
+
930
+ AXUIElementRef node = hit;
931
+ for (int depth = 0; depth < 24 && node; depth++) {
932
+ NSString *role = CopyAttributeString(node, kAXRoleAttribute);
933
+ if ([role isEqualToString:@"AXWindow"]) {
934
+ NSString *borderType = CursorTypeForWindowBorder(node, cursorPos);
935
+ CFRelease(node);
936
+ return borderType;
937
+ }
938
+ AXUIElementRef parent = NULL;
939
+ AXError perr = AXUIElementCopyAttributeValue(node, kAXParentAttribute, (CFTypeRef *)&parent);
940
+ CFRelease(node);
941
+ node = NULL;
942
+ if (perr == kAXErrorSuccess && parent) {
943
+ node = parent;
944
+ }
945
+ }
946
+ return nil;
947
+ }
948
+ }
949
+
704
950
  static NSString* CursorTypeFromAccessibilityElement(AXUIElementRef element, CGPoint cursorPos) {
705
951
  if (!element) {
706
952
  return nil;
@@ -937,6 +1183,14 @@ static NSString* cursorTypeFromCursorName(NSString *value) {
937
1183
  return @"zoom-in";
938
1184
  }
939
1185
 
1186
+ // CGS / tema kısa diagonal tokenları (nwse / nesw)
1187
+ if ([normalized containsString:@"nesw"]) {
1188
+ return @"nesw-resize";
1189
+ }
1190
+ if ([normalized containsString:@"nwse"]) {
1191
+ return @"nwse-resize";
1192
+ }
1193
+
940
1194
  // All-scroll pattern (move in all directions)
941
1195
  if ([normalized containsString:@"all-scroll"] ||
942
1196
  [normalized containsString:@"allscroll"] ||
@@ -1383,6 +1637,13 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1383
1637
  CGFloat relativeX = width > 0 ? hotspot.x / width : 0;
1384
1638
  CGFloat relativeY = height > 0 ? hotspot.y / height : 0;
1385
1639
 
1640
+ if (cursor) {
1641
+ NSString *diag = DiagonalResizeTypeByVisualMatch(cursor);
1642
+ if (diag) {
1643
+ return diag;
1644
+ }
1645
+ }
1646
+
1386
1647
  // Tolerance for floating point comparison
1387
1648
  CGFloat tolerance = 0.05;
1388
1649
  CGFloat tightTolerance = 0.02; // For precise hotspot matching
@@ -1422,29 +1683,35 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1422
1683
  // Distinguished by pointer equality
1423
1684
  if (approx(width, 32) && approx(height, 32) && approx(relativeY, 0.531)) {
1424
1685
  if (cursor) {
1425
- if (cursor == [NSCursor closedHandCursor]) {
1686
+ if ([NSCursor respondsToSelector:@selector(closedHandCursor)] &&
1687
+ cursor == [NSCursor closedHandCursor]) {
1426
1688
  return @"grabbing";
1427
1689
  }
1428
- if (cursor == [NSCursor openHandCursor]) {
1690
+ if ([NSCursor respondsToSelector:@selector(openHandCursor)] &&
1691
+ cursor == [NSCursor openHandCursor]) {
1429
1692
  return @"grab";
1430
1693
  }
1431
1694
  }
1432
1695
  return @"grab"; // Default to grab if can't distinguish
1433
1696
  }
1434
1697
 
1435
- // 24x24 cursors: crosshair vs move/all-scroll
1436
- // Distinguished by precise hotspot position
1698
+ // 24x24: köşe diagonal resize ile crosshair/move aynı boyutta — önce diagonal fabrika
1437
1699
  if (approx(width, 24) && approx(height, 24)) {
1700
+ if (cursor) {
1701
+ NSString *diag = DiagonalResizeTypeByVisualMatch(cursor);
1702
+ if (diag) {
1703
+ return diag;
1704
+ }
1705
+ }
1438
1706
  // crosshair: hotspot rel=(0.458, 0.458)
1439
1707
  if (approxTight(relativeX, 0.458) && approxTight(relativeY, 0.458)) {
1440
1708
  return @"crosshair";
1441
1709
  }
1442
1710
  // move/all-scroll: hotspot rel=(0.5, 0.5)
1443
1711
  if (approxTight(relativeX, 0.5) && approxTight(relativeY, 0.5)) {
1444
- return @"move"; // or all-scroll, they're identical
1712
+ return @"move";
1445
1713
  }
1446
- // Fallback for 24x24
1447
- return @"crosshair";
1714
+ return nil;
1448
1715
  }
1449
1716
 
1450
1717
  // help/cell: 18x18, ratio=1.0, hotspot rel=(0.5, 0.5)
@@ -1505,7 +1772,29 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1505
1772
 
1506
1773
  // ne-resize/nw-resize/se-resize/sw-resize/nesw-resize/nwse-resize: 22x22, ratio=1.0, hotspot rel=(0.5, 0.5)
1507
1774
  if (approx(width, 22) && approx(height, 22)) {
1508
- return @"nwse-resize"; // Default to nwse-resize for all diagonal cursors
1775
+ if (cursor) {
1776
+ NSString *diag = DiagonalResizeTypeByVisualMatch(cursor);
1777
+ if (diag) {
1778
+ return diag;
1779
+ }
1780
+ if ([NSCursor respondsToSelector:@selector(resizeUpCursor)] &&
1781
+ cursor == [NSCursor resizeUpCursor]) {
1782
+ return @"n-resize";
1783
+ }
1784
+ if ([NSCursor respondsToSelector:@selector(resizeDownCursor)] &&
1785
+ cursor == [NSCursor resizeDownCursor]) {
1786
+ return @"s-resize";
1787
+ }
1788
+ if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] &&
1789
+ cursor == [NSCursor resizeLeftCursor]) {
1790
+ return @"w-resize";
1791
+ }
1792
+ if ([NSCursor respondsToSelector:@selector(resizeRightCursor)] &&
1793
+ cursor == [NSCursor resizeRightCursor]) {
1794
+ return @"e-resize";
1795
+ }
1796
+ }
1797
+ return nil;
1509
1798
  }
1510
1799
 
1511
1800
  // zoom-in/zoom-out: 28x26, ratio=1.077, hotspot rel=(0.428, 0.423)
@@ -1527,10 +1816,12 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1527
1816
  if (approxTight(relativeX, 0.161) && approxTight(relativeY, 0.1)) {
1528
1817
  return @"default";
1529
1818
  }
1530
- // context-menu/progress/wait/copy/no-drop/not-allowed: hotspot rel=(0.179, 0.125) - hotspot at (5, 5)
1819
+ // Ortak 28x40 ok/beachball hotspot bandı: spinner ile karışmasın diye önce ok
1531
1820
  if (approxTight(relativeX, 0.179) && approxTight(relativeY, 0.125)) {
1532
- // Try pointer equality for standard cursors
1533
1821
  if (cursor) {
1822
+ if (cursor == [NSCursor arrowCursor]) {
1823
+ return @"default";
1824
+ }
1534
1825
  if (cursor == [NSCursor contextualMenuCursor]) {
1535
1826
  return @"context-menu";
1536
1827
  }
@@ -1541,10 +1832,7 @@ static NSString* cursorTypeFromImageSignature(NSImage *image, NSPoint hotspot, N
1541
1832
  return @"not-allowed";
1542
1833
  }
1543
1834
  }
1544
- // NOTE: progress, wait, no-drop don't have standard NSCursor pointers
1545
- // Return "progress" as default for this hotspot pattern (better than "default")
1546
- // Let cursor name detection in caller distinguish between progress/wait
1547
- return @"progress";
1835
+ return @"default";
1548
1836
  }
1549
1837
  return @"default";
1550
1838
  }
@@ -1571,13 +1859,16 @@ static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
1571
1859
  if (cursor == [NSCursor pointingHandCursor]) {
1572
1860
  return @"pointer";
1573
1861
  }
1574
- if (cursor == [NSCursor crosshairCursor]) {
1862
+ if ([NSCursor respondsToSelector:@selector(crosshairCursor)] &&
1863
+ cursor == [NSCursor crosshairCursor]) {
1575
1864
  return @"crosshair";
1576
1865
  }
1577
- if (cursor == [NSCursor openHandCursor]) {
1866
+ if ([NSCursor respondsToSelector:@selector(openHandCursor)] &&
1867
+ cursor == [NSCursor openHandCursor]) {
1578
1868
  return @"grab";
1579
1869
  }
1580
- if (cursor == [NSCursor closedHandCursor]) {
1870
+ if ([NSCursor respondsToSelector:@selector(closedHandCursor)] &&
1871
+ cursor == [NSCursor closedHandCursor]) {
1581
1872
  return @"grabbing";
1582
1873
  }
1583
1874
  if (cursor == [NSCursor operationNotAllowedCursor]) {
@@ -1593,14 +1884,42 @@ static NSString* cursorTypeFromNSCursor(NSCursor *cursor) {
1593
1884
  return @"context-menu";
1594
1885
  }
1595
1886
 
1596
- // Resize cursors
1597
- if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)]) {
1598
- if (cursor == [NSCursor resizeLeftRightCursor]) {
1887
+ NSString *diagonalVisual = DiagonalResizeTypeByVisualMatch(cursor);
1888
+ if (diagonalVisual) {
1889
+ return diagonalVisual;
1890
+ }
1891
+
1892
+ if ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] &&
1893
+ cursor == [NSCursor resizeLeftCursor]) {
1894
+ return @"col-resize";
1895
+ }
1896
+ if ([NSCursor respondsToSelector:@selector(resizeRightCursor)] &&
1897
+ cursor == [NSCursor resizeRightCursor]) {
1898
+ return @"col-resize";
1899
+ }
1900
+ if ([NSCursor respondsToSelector:@selector(resizeUpCursor)] &&
1901
+ cursor == [NSCursor resizeUpCursor]) {
1902
+ return @"n-resize";
1903
+ }
1904
+ if ([NSCursor respondsToSelector:@selector(resizeDownCursor)] &&
1905
+ cursor == [NSCursor resizeDownCursor]) {
1906
+ return @"s-resize";
1907
+ }
1908
+ if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)] &&
1909
+ cursor == [NSCursor resizeLeftRightCursor]) {
1910
+ return @"col-resize";
1911
+ }
1912
+ if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)] &&
1913
+ cursor == [NSCursor resizeUpDownCursor]) {
1914
+ return @"ns-resize";
1915
+ }
1916
+ if (@available(macOS 15.0, *)) {
1917
+ if ([NSCursor respondsToSelector:@selector(columnResizeCursor)] &&
1918
+ cursor == [NSCursor columnResizeCursor]) {
1599
1919
  return @"col-resize";
1600
1920
  }
1601
- }
1602
- if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)]) {
1603
- if (cursor == [NSCursor resizeUpDownCursor]) {
1921
+ if ([NSCursor respondsToSelector:@selector(rowResizeCursor)] &&
1922
+ cursor == [NSCursor rowResizeCursor]) {
1604
1923
  return @"row-resize";
1605
1924
  }
1606
1925
  }
@@ -1672,12 +1991,6 @@ static NSString* detectSystemCursorType(void) {
1672
1991
  }
1673
1992
 
1674
1993
  int cursorSeed = SafeCGSCurrentCursorSeed();
1675
- if (cursorSeed > 0) {
1676
- NSString *seedType = cursorTypeFromSeed(cursorSeed);
1677
- if (seedType) {
1678
- return seedType;
1679
- }
1680
- }
1681
1994
 
1682
1995
  void (^fetchCursorBlock)(void) = ^{
1683
1996
  NSCursor *currentCursor = nil;
@@ -1781,14 +2094,23 @@ static NSString* detectSystemCursorType(void) {
1781
2094
  dispatch_sync(dispatch_get_main_queue(), fetchCursorBlock);
1782
2095
  }
1783
2096
 
1784
- if (cursorType && ![cursorType isEqualToString:@"default"] && cursorSeed > 0) {
1785
- addCursorToSeedMap(cursorType, cursorSeed);
2097
+ if (cursorType && ![cursorType isEqualToString:@"default"]) {
2098
+ if (cursorSeed > 0) {
2099
+ addCursorToSeedMap(cursorType, cursorSeed);
2100
+ }
2101
+ return cursorType;
1786
2102
  }
1787
2103
 
1788
- return cursorType;
2104
+ if (cursorSeed > 0) {
2105
+ NSString *seedType = cursorTypeFromSeed(cursorSeed);
2106
+ if (seedType) {
2107
+ return seedType;
2108
+ }
2109
+ }
2110
+
2111
+ return cursorType ?: @"default";
1789
2112
  }
1790
2113
 
1791
- // Desktop'ta SVG karşılığı olmayan cursor tiplerini desteklenen tiplere normalize et
1792
2114
  static NSString* normalizeCursorTypeForDesktop(NSString *cursorType) {
1793
2115
  if (!cursorType || [cursorType length] == 0) {
1794
2116
  return @"default";
@@ -1886,11 +2208,23 @@ NSString* getCursorType() {
1886
2208
  int currentSeed = SafeCGSCurrentCursorSeed();
1887
2209
  g_lastCursorSeed = currentSeed; // Save for getCursorPosition()
1888
2210
 
1889
- // Use cursorTypeFromNSCursor for detection (pointer equality + image-based)
1890
- // DO NOT use accessibility detection as it's unreliable and causes false positives
2211
+ // Önce NSCursor/CGS; köşe diagonal için sistem imleci güvenilir olmayabiliyor — AX ile pencere çerçevesi düzeltmesi
1891
2212
  NSString *systemCursorType = detectSystemCursorType();
1892
2213
  NSString *rawType = systemCursorType && [systemCursorType length] > 0 ? systemCursorType : @"default";
1893
2214
 
2215
+ __block NSString *chromeResize = nil;
2216
+ if ([NSThread isMainThread]) {
2217
+ chromeResize = ResizeTypeFromWindowUnderCursor(cursorPos);
2218
+ } else {
2219
+ dispatch_sync(dispatch_get_main_queue(), ^{
2220
+ chromeResize = ResizeTypeFromWindowUnderCursor(cursorPos);
2221
+ });
2222
+ }
2223
+
2224
+ if (chromeResize) {
2225
+ rawType = chromeResize;
2226
+ }
2227
+
1894
2228
  // Desktop SVG'lerine uyumlu tipe normalize et
1895
2229
  NSString *finalType = normalizeCursorTypeForDesktop(rawType);
1896
2230
 
@@ -3,6 +3,22 @@
3
3
  #import <AppKit/AppKit.h>
4
4
  #import "../logging.h"
5
5
 
6
+ static NSCursor *CursorFactoryNamed(NSString *name) {
7
+ if (!name || [name length] == 0) return nil;
8
+ SEL sel = NSSelectorFromString(name);
9
+ if (!sel || ![NSCursor respondsToSelector:sel]) return nil;
10
+ IMP imp = [NSCursor methodForSelector:sel];
11
+ if (!imp) return nil;
12
+ typedef NSCursor *(*CursorFactoryFunc)(id, SEL);
13
+ CursorFactoryFunc fn = (CursorFactoryFunc)imp;
14
+ return fn([NSCursor class], sel);
15
+ }
16
+
17
+ static BOOL CursorEqualsFactoryNamed(NSCursor *cursor, NSString *factoryName) {
18
+ NSCursor *ref = CursorFactoryNamed(factoryName);
19
+ return ref != nil && cursor == ref;
20
+ }
21
+
6
22
  // Thread-safe cursor tracking for Electron
7
23
  static dispatch_queue_t g_cursorQueue = nil;
8
24
 
@@ -23,18 +39,34 @@ static NSString* MapCursorToType(NSCursor *cursor) {
23
39
  if (cursor == [NSCursor pointingHandCursor]) return @"pointer";
24
40
  if ([NSCursor respondsToSelector:@selector(resizeLeftRightCursor)]) {
25
41
  if (cursor == [NSCursor resizeLeftRightCursor] ||
26
- [NSCursor instancesRespondToSelector:@selector(resizeLeftCursor)] && (cursor == [NSCursor resizeLeftCursor]) ||
27
- [NSCursor instancesRespondToSelector:@selector(resizeRightCursor)] && (cursor == [NSCursor resizeRightCursor])) {
42
+ ([NSCursor respondsToSelector:@selector(resizeLeftCursor)] && cursor == [NSCursor resizeLeftCursor]) ||
43
+ ([NSCursor respondsToSelector:@selector(resizeRightCursor)] && cursor == [NSCursor resizeRightCursor])) {
28
44
  return @"col-resize";
29
45
  }
30
46
  }
31
47
  if ([NSCursor respondsToSelector:@selector(resizeUpDownCursor)]) {
32
48
  if (cursor == [NSCursor resizeUpDownCursor] ||
33
- [NSCursor instancesRespondToSelector:@selector(resizeUpCursor)] && (cursor == [NSCursor resizeUpCursor]) ||
34
- [NSCursor instancesRespondToSelector:@selector(resizeDownCursor)] && (cursor == [NSCursor resizeDownCursor])) {
49
+ ([NSCursor respondsToSelector:@selector(resizeUpCursor)] && cursor == [NSCursor resizeUpCursor]) ||
50
+ ([NSCursor respondsToSelector:@selector(resizeDownCursor)] && cursor == [NSCursor resizeDownCursor])) {
35
51
  return @"ns-resize";
36
52
  }
37
53
  }
54
+ if (CursorEqualsFactoryNamed(cursor, @"resizeNorthWestSouthEastCursor")) {
55
+ return @"nwse-resize";
56
+ }
57
+ if (CursorEqualsFactoryNamed(cursor, @"resizeNorthEastSouthWestCursor")) {
58
+ return @"nesw-resize";
59
+ }
60
+ if (@available(macOS 15.0, *)) {
61
+ if ([NSCursor respondsToSelector:@selector(columnResizeCursor)] &&
62
+ cursor == [NSCursor columnResizeCursor]) {
63
+ return @"col-resize";
64
+ }
65
+ if ([NSCursor respondsToSelector:@selector(rowResizeCursor)] &&
66
+ cursor == [NSCursor rowResizeCursor]) {
67
+ return @"row-resize";
68
+ }
69
+ }
38
70
  if ([NSCursor respondsToSelector:@selector(openHandCursor)] && cursor == [NSCursor openHandCursor]) return @"grab";
39
71
  if ([NSCursor respondsToSelector:@selector(closedHandCursor)] && cursor == [NSCursor closedHandCursor]) return @"grabbing";
40
72
  if ([NSCursor respondsToSelector:@selector(crosshairCursor)] && cursor == [NSCursor crosshairCursor]) return @"crosshair";