node-mac-recorder 2.24.1 → 2.24.3

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/index.js CHANGED
@@ -150,6 +150,23 @@ class MacRecorder extends EventEmitter {
150
150
  /**
151
151
  * macOS ekranlarını listeler
152
152
  */
153
+ /**
154
+ * ScreenCaptureKit'i onceden isitir (SCShareableContent cache).
155
+ * Kayit baslatmadan once cagrilirsa "start recorder" gecikmesi belirgin azalir.
156
+ * Asenkron ve guvenli: desteklenmeyen surumlerde sessizce false doner.
157
+ */
158
+ prewarmScreenCapture() {
159
+ try {
160
+ if (typeof nativeBinding.prewarmScreenCapture !== "function") {
161
+ return false;
162
+ }
163
+ return nativeBinding.prewarmScreenCapture();
164
+ } catch (error) {
165
+ console.warn("Prewarm basarisiz:", error.message);
166
+ return false;
167
+ }
168
+ }
169
+
153
170
  async getDisplays() {
154
171
  const displays = nativeBinding.getDisplays();
155
172
  return displays.map((display, index) => ({
@@ -874,6 +891,8 @@ class MacRecorder extends EventEmitter {
874
891
  // bir zaman kaymasi olarak kalir. Native gercek video baslangicini
875
892
  // biliyor; okuyup sakla ki stop'ta cursor JSON'una yazabilelim.
876
893
  this.videoStartTimestamp = 0;
894
+ // Onceki kayittan kalan referans sizmasin
895
+ this.timelineStartTimestamp = 0;
877
896
  try {
878
897
  const nativeVideoStart =
879
898
  typeof nativeBinding.getVideoStartTimestamp === 'function'
@@ -891,6 +910,28 @@ class MacRecorder extends EventEmitter {
891
910
  console.warn('⚠️ SYNC: Video start timestamp read failed:', videoStartError.message);
892
911
  }
893
912
 
913
+ // ZAMAN REFERANSI: cursor/klavye timeline'i VIDEONUN ilk karesine
914
+ // hizalanir, JS'in "kayit hazir" dedigi ana degil.
915
+ //
916
+ // NEDEN: syncTimestamp, yukaridaki hazir-olma dongusunden sonraki an.
917
+ // Bu an ile videonun gercek t=0'i arasindaki fark, baslatma hizina
918
+ // gore DEGISIYOR (ornegin ScreenCaptureKit isitilmissa video cok daha
919
+ // erken basliyor). Cursor bu degisken ana baglanirsa her kayitta
920
+ // farkli bir kayma olusuyor ve editordeki telafi 1sn limitine
921
+ // takilabiliyor. Videonun kendi baslangicini referans alinca fark
922
+ // yapisal olarak sifir olur; hizlanma senkronu bozmaz.
923
+ const timelineStartTimestamp =
924
+ this.videoStartTimestamp > 0
925
+ ? this.videoStartTimestamp
926
+ : syncTimestamp;
927
+ this.timelineStartTimestamp = timelineStartTimestamp;
928
+
929
+ if (timelineStartTimestamp !== syncTimestamp) {
930
+ console.log(
931
+ `🎯 SYNC: Timeline referansi video ilk karesine cekildi (${(syncTimestamp - timelineStartTimestamp).toFixed(0)}ms geri)`
932
+ );
933
+ }
934
+
894
935
  const standardCursorOptions = {
895
936
  videoRelative: true,
896
937
  displayInfo: this.recordingDisplayInfo,
@@ -898,11 +939,11 @@ class MacRecorder extends EventEmitter {
898
939
  this.options.captureArea ? 'area' : 'display',
899
940
  captureArea: this.options.captureArea,
900
941
  windowId: this.options.windowId,
901
- startTimestamp: syncTimestamp // Align cursor timeline to actual start
942
+ startTimestamp: timelineStartTimestamp
902
943
  };
903
944
 
904
945
  try {
905
- console.log('🎯 SYNC: Starting cursor tracking at timestamp:', syncTimestamp);
946
+ console.log('🎯 SYNC: Starting cursor tracking at timestamp:', timelineStartTimestamp);
906
947
  await this.startCursorCapture(cursorFilePath, standardCursorOptions);
907
948
  console.log('✅ SYNC: Cursor tracking started successfully');
908
949
  } catch (cursorError) {
@@ -912,8 +953,8 @@ class MacRecorder extends EventEmitter {
912
953
 
913
954
  // Klavye kısayolu yakalama (cursor ile AYNI zaman referansı)
914
955
  try {
915
- console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:', syncTimestamp);
916
- await this.startKeyboardCapture(keyboardFilePath, { startTimestamp: syncTimestamp });
956
+ console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:', timelineStartTimestamp);
957
+ await this.startKeyboardCapture(keyboardFilePath, { startTimestamp: timelineStartTimestamp });
917
958
  console.log('✅ SYNC: Keyboard shortcut capture started successfully');
918
959
  } catch (keyboardError) {
919
960
  console.warn('⚠️ Keyboard capture failed to start:', keyboardError.message);
@@ -1004,7 +1045,8 @@ class MacRecorder extends EventEmitter {
1004
1045
 
1005
1046
  // Native kayıt gerçekten başladığını kontrol etmek için polling başlat
1006
1047
  let recordingStartedEmitted = false;
1007
- const checkRecordingStatus = setInterval(() => {
1048
+ let checkRecordingStatus = null;
1049
+ const pollRecordingStatus = () => {
1008
1050
  try {
1009
1051
  const nativeStatus = nativeBinding.getRecordingStatus();
1010
1052
  if (nativeStatus && !recordingStartedEmitted) {
@@ -1050,8 +1092,10 @@ class MacRecorder extends EventEmitter {
1050
1092
  });
1051
1093
  }
1052
1094
  }
1053
- }, 50); // Her 50ms kontrol et
1054
-
1095
+ };
1096
+
1097
+ checkRecordingStatus = setInterval(pollRecordingStatus, 50);
1098
+
1055
1099
  // Timeout fallback - 5 saniye sonra hala başlamamışsa emit et
1056
1100
  setTimeout(() => {
1057
1101
  if (!recordingStartedEmitted) {
@@ -1683,12 +1727,30 @@ class MacRecorder extends EventEmitter {
1683
1727
  }
1684
1728
 
1685
1729
  // Add sync metadata to first event only
1730
+ //
1731
+ // KRITIK: `videoStartTime` SADECE native'in bildirdigi gercek video
1732
+ // baslangici varsa yazilir. Onceden buraya `cursorCaptureSessionTimestamp`
1733
+ // yaziliyordu; o videonun ilk karesi DEGIL, dosya adi icin uretilen
1734
+ // oturum damgasi. Editor (cursorPlaybackTimeline) bu alani video t=0
1735
+ // sanip birebir telafi uyguladigi icin cursor kayiyordu.
1736
+ // Gercek deger yoksa alani hic yazmiyoruz -> editor heuristik
1737
+ // hizalamaya duser, ki yanlis bir offset uygulamaktan iyidir.
1686
1738
  if (this.cursorCaptureFirstWrite && this.cursorCaptureSessionTimestamp) {
1687
- cursorData._syncMetadata = {
1688
- videoStartTime: this.cursorCaptureSessionTimestamp,
1689
- cursorStartTime: this.cursorCaptureStartTime,
1690
- offsetMs: this.cursorCaptureStartTime - this.cursorCaptureSessionTimestamp
1691
- };
1739
+ const knownVideoStart =
1740
+ Number(this.videoStartTimestamp) > 0
1741
+ ? Number(this.videoStartTimestamp)
1742
+ : null;
1743
+
1744
+ cursorData._syncMetadata = knownVideoStart
1745
+ ? {
1746
+ videoStartTime: knownVideoStart,
1747
+ cursorStartTime: this.cursorCaptureStartTime,
1748
+ offsetMs: this.cursorCaptureStartTime - knownVideoStart,
1749
+ }
1750
+ : {
1751
+ cursorStartTime: this.cursorCaptureStartTime,
1752
+ sessionTimestamp: this.cursorCaptureSessionTimestamp,
1753
+ };
1692
1754
  }
1693
1755
 
1694
1756
  // Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
@@ -1776,7 +1838,13 @@ class MacRecorder extends EventEmitter {
1776
1838
  */
1777
1839
  _writeCursorSyncMetadata(cursorFilePath) {
1778
1840
  const videoStartTime = Number(this.videoStartTimestamp);
1779
- const cursorStartTime = Number(this.syncTimestamp);
1841
+ // Cursor timeline'inin GERCEK referansi. Video baslangici okunabildiyse
1842
+ // timeline zaten ona hizalandi (delay 0); okunamadiysa syncTimestamp'e
1843
+ // dusuldu. Burada varsayim yapmak yerine kullanilan degeri yaziyoruz,
1844
+ // aksi halde editor var olmayan bir kaymayi telafi etmeye calisir.
1845
+ const cursorStartTime = Number(
1846
+ this.timelineStartTimestamp || this.syncTimestamp
1847
+ );
1780
1848
  if (
1781
1849
  !Number.isFinite(videoStartTime) ||
1782
1850
  videoStartTime <= 0 ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.1",
3
+ "version": "2.24.3",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -153,23 +153,7 @@ static uint64_t FNV1AHash(const unsigned char *data, size_t length) {
153
153
  return hash;
154
154
  }
155
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
-
156
+ static NSString* CursorImageFingerprintFromCGImage(CGImageRef cgImage, NSPoint hotspot) {
173
157
  if (!cgImage) {
174
158
  return nil;
175
159
  }
@@ -223,6 +207,67 @@ static NSString* CursorImageFingerprintFromImage(NSImage *image, NSPoint hotspot
223
207
  hash];
224
208
  }
225
209
 
210
+ // NSImage'in VARSAYILAN temsilinden fingerprint. Hangi temsilin seçildigi ekranin
211
+ // backing scale'ine bagli (Retina 2x vs harici 1x), bu yuzden calisma aninda
212
+ // uretilen deger ekran konfigurasyonuna gore degisebilir.
213
+ static NSString* CursorImageFingerprintFromImage(NSImage *image, NSPoint hotspot) {
214
+ if (!image) {
215
+ return nil;
216
+ }
217
+ NSRect imageRect = NSMakeRect(0, 0, [image size].width, [image size].height);
218
+ CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:nil hints:nil];
219
+ if (!cgImage) {
220
+ for (NSImageRep *rep in [image representations]) {
221
+ if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
222
+ cgImage = [(NSBitmapImageRep *)rep CGImage];
223
+ if (cgImage) {
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ return CursorImageFingerprintFromCGImage(cgImage, hotspot);
231
+ }
232
+
233
+ // Bir cursor imajinin TUM temsilleri icin fingerprint uretir.
234
+ // KRITIK: MacBook kapagi kapaliyken tek harici monitorde calisirken (clamshell)
235
+ // veya ekranlar arasi gecerken NSImage farkli bir temsil donduruyor; tek fingerprint
236
+ // kaydedilirse eslesme kayboluyor ve cursor tipi yanlis tespit ediliyordu.
237
+ // Tum temsilleri kaydedince calisma aninda hangisi secilirse secilsin eslesme tutar.
238
+ static NSArray<NSString *>* CursorImageFingerprintsAllReps(NSImage *image, NSPoint hotspot) {
239
+ if (!image) {
240
+ return @[];
241
+ }
242
+
243
+ NSMutableArray<NSString *> *fingerprints = [NSMutableArray array];
244
+
245
+ NSString *defaultFingerprint = CursorImageFingerprintFromImage(image, hotspot);
246
+ if (defaultFingerprint) {
247
+ [fingerprints addObject:defaultFingerprint];
248
+ }
249
+
250
+ for (NSImageRep *rep in [image representations]) {
251
+ CGImageRef repImage = NULL;
252
+ if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
253
+ repImage = [(NSBitmapImageRep *)rep CGImage];
254
+ } else {
255
+ NSRect repRect = NSMakeRect(0, 0, [rep pixelsWide], [rep pixelsHigh]);
256
+ if (repRect.size.width <= 0 || repRect.size.height <= 0) {
257
+ repRect = NSMakeRect(0, 0, [rep size].width, [rep size].height);
258
+ }
259
+ repImage = [rep CGImageForProposedRect:&repRect context:nil hints:nil];
260
+ }
261
+
262
+ NSString *repFingerprint = CursorImageFingerprintFromCGImage(repImage, hotspot);
263
+ if (repFingerprint && ![fingerprints containsObject:repFingerprint]) {
264
+ [fingerprints addObject:repFingerprint];
265
+ }
266
+ }
267
+
268
+ return fingerprints;
269
+ }
270
+
226
271
  static NSString* CursorImageFingerprintUnsafe(NSCursor *cursor) {
227
272
  if (!cursor) {
228
273
  return nil;
@@ -313,11 +358,17 @@ static void AddStandardCursorFingerprint(NSCursor *cursor, NSString *cursorType)
313
358
  if (!cursor || !cursorType) {
314
359
  return;
315
360
  }
316
- NSString *fingerprint = CursorImageFingerprintUnsafe(cursor);
317
- if (!fingerprint) {
318
- return;
361
+ // Tek bir temsil degil, tum temsiller kaydedilir -> ekran olcegi degisse bile
362
+ // (clamshell / harici monitor) eslesme korunur.
363
+ NSArray<NSString *> *fingerprints =
364
+ CursorImageFingerprintsAllReps([cursor image], [cursor hotSpot]);
365
+ for (NSString *fingerprint in fingerprints) {
366
+ // Ilk kayit kazanir: ayni fingerprint birden fazla cursor'a denk gelirse
367
+ // once eklenen (daha spesifik) tip korunur.
368
+ if (![g_cursorFingerprintMap objectForKey:fingerprint]) {
369
+ [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
370
+ }
319
371
  }
320
- [g_cursorFingerprintMap setObject:cursorType forKey:fingerprint];
321
372
  }
322
373
 
323
374
  static void AddCursorIfAvailable(SEL selector, NSString *cursorType) {
@@ -1344,6 +1344,19 @@ Napi::Value GetVideoStartTimestamp(const Napi::CallbackInfo& info) {
1344
1344
  }
1345
1345
 
1346
1346
  // NAPI Function: Get Recording Status
1347
+ // Kayit baslatma gecikmesini azaltmak icin SCShareableContent'i onceden isitir.
1348
+ // Asenkron; hemen doner ve hicbir seyi bloklamaz.
1349
+ Napi::Value PrewarmScreenCapture(const Napi::CallbackInfo& info) {
1350
+ Napi::Env env = info.Env();
1351
+
1352
+ if (@available(macOS 12.3, *)) {
1353
+ [ScreenCaptureKitRecorder prewarmShareableContent];
1354
+ return Napi::Boolean::New(env, true);
1355
+ }
1356
+
1357
+ return Napi::Boolean::New(env, false);
1358
+ }
1359
+
1347
1360
  Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
1348
1361
  Napi::Env env = info.Env();
1349
1362
 
@@ -1711,6 +1724,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1711
1724
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1712
1725
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1713
1726
  exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
1727
+ exports.Set(Napi::String::New(env, "prewarmScreenCapture"), Napi::Function::New(env, PrewarmScreenCapture));
1714
1728
  exports.Set(Napi::String::New(env, "getVideoStartTimestamp"), Napi::Function::New(env, GetVideoStartTimestamp));
1715
1729
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
1716
1730
 
@@ -7,6 +7,11 @@ API_AVAILABLE(macos(12.3))
7
7
 
8
8
  + (BOOL)isScreenCaptureKitAvailable;
9
9
 
10
+ // SCShareableContent'i onceden cekerek macOS ic cache'ini isitir.
11
+ // Kayit anindaki ilk cagri yavas oldugu icin (ozellikle ekran konfigurasyonu
12
+ // degistikten sonra) baslatma gecikmesini azaltir. Asenkron, hicbir seyi bloklamaz.
13
+ + (void)prewarmShareableContent;
14
+
10
15
  // MULTI-SESSION API: New session-based recording
11
16
  + (NSString *)startRecordingWithConfiguration:(NSDictionary *)config
12
17
  delegate:(id)delegate
@@ -1039,6 +1039,25 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1039
1039
  return NO;
1040
1040
  }
1041
1041
 
1042
+ + (void)prewarmShareableContent {
1043
+ if (@available(macOS 15.0, *)) {
1044
+ // Kayit sirasindaki ilk SCShareableContent cagrisi yavas olabiliyor
1045
+ // (TCC kontrolu + pencere/ekran envanteri). Kayit baslamadan once bir kez
1046
+ // cagirip macOS'un ic cache'ini isitiyoruz. Sonuc kullanilmiyor.
1047
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
1048
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1049
+ if (contentError) {
1050
+ MRLog(@"⚠️ Prewarm shareable content failed: %@", contentError.localizedDescription);
1051
+ return;
1052
+ }
1053
+ MRLog(@"🔥 Prewarm: shareable content hazir (%lu ekran, %lu pencere)",
1054
+ (unsigned long)content.displays.count,
1055
+ (unsigned long)content.windows.count);
1056
+ }];
1057
+ });
1058
+ }
1059
+ }
1060
+
1042
1061
  + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config delegate:(id)delegate error:(NSError **)error {
1043
1062
  if (!config) {
1044
1063
  return NO;
@@ -157,6 +157,24 @@ static void ApplyStartRecordButtonIcon(NSButton *button) {
157
157
  }
158
158
  }
159
159
 
160
+ // CGWindow (sol-üst orijin) <-> Cocoa (sol-alt orijin) dönüşümünde referans yükseklik
161
+ // DAİMA primary ekranın (screens[0]) yüksekliğidir. [NSScreen mainScreen] farenin/aktif
162
+ // pencerenin bulunduğu ekranı döner; çok ekranlı kurulumda kullanılırsa highlight kayar.
163
+ static CGFloat primaryScreenHeight() {
164
+ NSArray *screens = [NSScreen screens];
165
+ if ([screens count] == 0) return [[NSScreen mainScreen] frame].size.height;
166
+ return [[screens objectAtIndex:0] frame].size.height;
167
+ }
168
+
169
+ // Bir NSScreen frame'ini (Cocoa global) CGWindow koordinat uzayına çevirir
170
+ static NSRect screenFrameInCGSpace(NSScreen *screen) {
171
+ NSRect f = [screen frame];
172
+ return NSMakeRect(f.origin.x,
173
+ primaryScreenHeight() - NSMaxY(f),
174
+ f.size.width,
175
+ f.size.height);
176
+ }
177
+
160
178
  // Forward declarations
161
179
  void cleanupWindowSelector();
162
180
  void updateOverlay();
@@ -234,7 +252,8 @@ void updateScreenOverlays();
234
252
  }
235
253
 
236
254
  // Draw highlight rectangle for the selected window
237
- NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:self.highlightFrame
255
+ // 1px stroke'un yarısı dışarı taşmasın diye 0.5px içeri al -> pencereye tam otursun
256
+ NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.highlightFrame, 0.5, 0.5)
238
257
  xRadius:8.0
239
258
  yRadius:8.0];
240
259
 
@@ -852,9 +871,10 @@ void updateOverlay() {
852
871
  // Get current cursor position
853
872
  NSPoint mouseLocation = [NSEvent mouseLocation];
854
873
  // Convert from NSEvent coordinates (bottom-left) to CGWindow coordinates (top-left)
855
- NSScreen *mainScreen = [NSScreen mainScreen];
856
- CGFloat screenHeight = [mainScreen frame].size.height;
857
- CGPoint globalPoint = CGPointMake(mouseLocation.x, screenHeight - mouseLocation.y);
874
+ // NOT: referans DAİMA primary ekran yüksekliği; mainScreen kullanılırsa imleç
875
+ // farklı bir ekrana geçtiğinde koordinat kayar ve yanlış pencere seçilir.
876
+ CGPoint globalPoint = CGPointMake(mouseLocation.x,
877
+ primaryScreenHeight() - mouseLocation.y);
858
878
 
859
879
  // Find window under cursor (no need to refresh g_allWindows frequently since windows can't move)
860
880
  NSDictionary *windowUnderCursor = getWindowUnderCursor(globalPoint);
@@ -961,27 +981,21 @@ void updateOverlay() {
961
981
  CGFloat windowCenterY = y + height / 2;
962
982
 
963
983
  for (NSScreen *screen in screens) {
964
- NSRect screenFrame = [screen frame];
965
- // Convert screen frame to CGWindow coordinates
966
- CGFloat screenTop = screenFrame.origin.y + screenFrame.size.height;
967
- CGFloat screenBottom = screenFrame.origin.y;
968
- CGFloat screenLeft = screenFrame.origin.x;
969
- CGFloat screenRight = screenFrame.origin.x + screenFrame.size.width;
970
-
971
- if (windowCenterX >= screenLeft && windowCenterX <= screenRight &&
972
- windowCenterY >= screenBottom && windowCenterY <= screenTop) {
984
+ // Pencere koordinatları CGWindow uzayında; ekran frame'ini de aynı uzaya çevir
985
+ NSRect cgScreen = screenFrameInCGSpace(screen);
986
+
987
+ if (windowCenterX >= NSMinX(cgScreen) && windowCenterX <= NSMaxX(cgScreen) &&
988
+ windowCenterY >= NSMinY(cgScreen) && windowCenterY <= NSMaxY(cgScreen)) {
973
989
  windowScreen = screen;
974
990
  break;
975
991
  }
976
992
  }
977
-
993
+
978
994
  // Use main screen if no specific screen found
979
995
  if (!windowScreen) windowScreen = [NSScreen mainScreen];
980
-
981
- // Convert coordinates from CGWindow (top-left) to NSWindow (bottom-left) for the specific screen
982
- NSRect screenFrame = [windowScreen frame];
983
- CGFloat screenHeight = screenFrame.size.height;
984
- CGFloat adjustedY = screenHeight - y - height;
996
+
997
+ // Convert coordinates from CGWindow (top-left) to Cocoa global (bottom-left)
998
+ CGFloat adjustedY = primaryScreenHeight() - y - height;
985
999
 
986
1000
  // Window coordinates are in global space, overlay frame should be screen-relative
987
1001
  // Keep X coordinate as-is (already in global space which is what we want)
@@ -1014,19 +1028,19 @@ void updateOverlay() {
1014
1028
  NSInteger targetScreenIndex = -1;
1015
1029
  NSScreen *targetScreen = nil;
1016
1030
 
1017
- // Find screen containing this window
1031
+ // Find screen containing this window (karşılaştırma CGWindow uzayında yapılmalı)
1018
1032
  for (NSInteger i = 0; i < [allScreens count]; i++) {
1019
1033
  NSScreen *screen = [allScreens objectAtIndex:i];
1020
- NSRect screenFrame = [screen frame];
1021
-
1034
+ NSRect cgScreen = screenFrameInCGSpace(screen);
1035
+
1022
1036
  // Check if window center is within this screen bounds
1023
1037
  CGFloat windowCenterX = x + width/2;
1024
1038
  CGFloat windowCenterY = y + height/2;
1025
-
1026
- if (windowCenterX >= screenFrame.origin.x &&
1027
- windowCenterX <= screenFrame.origin.x + screenFrame.size.width &&
1028
- windowCenterY >= screenFrame.origin.y &&
1029
- windowCenterY <= screenFrame.origin.y + screenFrame.size.height) {
1039
+
1040
+ if (windowCenterX >= NSMinX(cgScreen) &&
1041
+ windowCenterX <= NSMaxX(cgScreen) &&
1042
+ windowCenterY >= NSMinY(cgScreen) &&
1043
+ windowCenterY <= NSMaxY(cgScreen)) {
1030
1044
  targetScreenIndex = i;
1031
1045
  targetScreen = screen;
1032
1046
  break;
@@ -1043,9 +1057,10 @@ void updateOverlay() {
1043
1057
  WindowSelectorOverlayView *targetOverlayView = [g_perScreenOverlayViews objectAtIndex:targetScreenIndex];
1044
1058
  NSRect targetScreenFrame = [targetScreen frame];
1045
1059
 
1046
- // Calculate LOCAL coordinates within target screen (much simpler!)
1060
+ // Calculate LOCAL coordinates within target screen
1061
+ // CG (sol-üst, primary orijinli) -> Cocoa global (sol-alt) -> ekran-yerel
1047
1062
  CGFloat localX = x - targetScreenFrame.origin.x;
1048
- CGFloat localY = (targetScreenFrame.size.height - (y - targetScreenFrame.origin.y)) - height;
1063
+ CGFloat localY = (primaryScreenHeight() - y - height) - targetScreenFrame.origin.y;
1049
1064
 
1050
1065
  NSLog(@"🎯 Window on Screen %ld: Global(%.0f,%.0f) → Local(%.0f,%.0f)",
1051
1066
  targetScreenIndex, (CGFloat)x, (CGFloat)y, localX, localY);
@@ -2177,12 +2192,16 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
2177
2192
  NSRect screenFrame = [screen frame];
2178
2193
 
2179
2194
  // Create per-screen overlay window
2195
+ // NOT: screen: parametresi non-nil verilirse contentRect o ekranın sol-alt köşesine
2196
+ // GÖRE yorumlanır; global screenFrame ile birlikte çift offset oluşur. nil verip
2197
+ // frame'i global koordinatta açıkça set ediyoruz.
2180
2198
  NSWindow *screenOverlay = [[NoFocusWindow alloc] initWithContentRect:screenFrame
2181
2199
  styleMask:NSWindowStyleMaskBorderless
2182
2200
  backing:NSBackingStoreBuffered
2183
2201
  defer:NO
2184
- screen:screen];
2185
-
2202
+ screen:nil];
2203
+ [screenOverlay setFrame:screenFrame display:NO];
2204
+
2186
2205
  // Create per-screen overlay view
2187
2206
  WindowSelectorOverlayView *overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:NSMakeRect(0, 0, screenFrame.size.width, screenFrame.size.height)];
2188
2207
  [screenOverlay setContentView:overlayView];