node-mac-recorder 2.24.0 → 2.24.2

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) => ({
@@ -866,6 +883,31 @@ class MacRecorder extends EventEmitter {
866
883
  this.recordingStartTime = syncTimestamp;
867
884
  console.log(`🎯 CURSOR SYNC: Cursor tracking will use timestamp: ${syncTimestamp}`);
868
885
 
886
+ // CURSOR/VIDEO TIME ALIGNMENT:
887
+ // Cursor timeline'in t=0'i bu andir (syncTimestamp), ama videonun
888
+ // ILK KARESI daha once yakalanmis olabilir (ScreenCaptureKit
889
+ // baslatma gecikmesi + yukaridaki hazir-olma beklemesi). Editor
890
+ // "cursor'un ilk ornegi = video t=0" varsayarsa aradaki fark sabit
891
+ // bir zaman kaymasi olarak kalir. Native gercek video baslangicini
892
+ // biliyor; okuyup sakla ki stop'ta cursor JSON'una yazabilelim.
893
+ this.videoStartTimestamp = 0;
894
+ try {
895
+ const nativeVideoStart =
896
+ typeof nativeBinding.getVideoStartTimestamp === 'function'
897
+ ? Number(nativeBinding.getVideoStartTimestamp())
898
+ : 0;
899
+ if (Number.isFinite(nativeVideoStart) && nativeVideoStart > 0) {
900
+ this.videoStartTimestamp = nativeVideoStart;
901
+ console.log(
902
+ `🎯 SYNC: Video first-frame timestamp: ${nativeVideoStart} (cursor starts ${(syncTimestamp - nativeVideoStart).toFixed(0)}ms later)`
903
+ );
904
+ } else {
905
+ console.warn('⚠️ SYNC: Video start timestamp unavailable — cursor sync metadata will be skipped');
906
+ }
907
+ } catch (videoStartError) {
908
+ console.warn('⚠️ SYNC: Video start timestamp read failed:', videoStartError.message);
909
+ }
910
+
869
911
  const standardCursorOptions = {
870
912
  videoRelative: true,
871
913
  displayInfo: this.recordingDisplayInfo,
@@ -1709,12 +1751,20 @@ class MacRecorder extends EventEmitter {
1709
1751
  this.cursorCaptureInterval = null;
1710
1752
 
1711
1753
  // Dosyayı kapat
1754
+ let closedCursorFile = null;
1712
1755
  if (this.cursorCaptureFile) {
1713
1756
  const fs = require("fs");
1714
1757
  fs.appendFileSync(this.cursorCaptureFile, "]");
1758
+ closedCursorFile = this.cursorCaptureFile;
1715
1759
  this.cursorCaptureFile = null;
1716
1760
  }
1717
1761
 
1762
+ // Cursor/video zaman hizalama bilgisini dosyaya yaz (editör bunu
1763
+ // okuyup sabit kaymayı telafi ediyor).
1764
+ if (closedCursorFile) {
1765
+ this._writeCursorSyncMetadata(closedCursorFile);
1766
+ }
1767
+
1718
1768
  // Değişkenleri temizle
1719
1769
  this.lastCapturedData = null;
1720
1770
  this.cursorCaptureStartTime = null;
@@ -1729,6 +1779,58 @@ class MacRecorder extends EventEmitter {
1729
1779
  });
1730
1780
  }
1731
1781
 
1782
+ /**
1783
+ * Cursor JSON'una video/cursor zaman hizalama bilgisini yazar.
1784
+ *
1785
+ * NEDEN: Cursor timeline'inin t=0'i `syncTimestamp` (kayit hazir olduktan
1786
+ * sonraki an), videonun t=0'i ise ILK KARENIN yakalandigi an. ScreenCaptureKit
1787
+ * baslatma gecikmesi yuzunden bu ikisi ayni degil; fark bilinmezse oynatimda
1788
+ * sabit bir zaman kaymasi olusur (cursor video ile "sync tutmuyor").
1789
+ * Editor tarafi `_syncMetadata.videoStartTime` / `cursorStartTime` okuyup
1790
+ * tam telafi ediyor — burada sadece gercek degerleri yaziyoruz.
1791
+ *
1792
+ * Dosyanin ilk noktasina yazilir; okuyucu ilk metadata'yi bulup kullanir.
1793
+ */
1794
+ _writeCursorSyncMetadata(cursorFilePath) {
1795
+ const videoStartTime = Number(this.videoStartTimestamp);
1796
+ const cursorStartTime = Number(this.syncTimestamp);
1797
+ if (
1798
+ !Number.isFinite(videoStartTime) ||
1799
+ videoStartTime <= 0 ||
1800
+ !Number.isFinite(cursorStartTime) ||
1801
+ cursorStartTime <= 0
1802
+ ) {
1803
+ // Native video baslangici okunamadiysa metadata yazma — editor eski
1804
+ // (telafisiz) davranisa duser, yanlis bir offset uygulamaktan iyidir.
1805
+ return;
1806
+ }
1807
+
1808
+ try {
1809
+ const fs = require("fs");
1810
+ const raw = fs.readFileSync(cursorFilePath, "utf8");
1811
+ const positions = JSON.parse(raw);
1812
+ if (!Array.isArray(positions) || positions.length === 0) return;
1813
+
1814
+ positions[0]._syncMetadata = {
1815
+ videoStartTime,
1816
+ cursorStartTime,
1817
+ startDelayMs: cursorStartTime - videoStartTime,
1818
+ recordingType: this.options?.windowId
1819
+ ? "window"
1820
+ : this.options?.captureArea
1821
+ ? "area"
1822
+ : "display",
1823
+ };
1824
+
1825
+ fs.writeFileSync(cursorFilePath, JSON.stringify(positions));
1826
+ console.log(
1827
+ `🎯 SYNC: Cursor sync metadata written (video→cursor delay ${(cursorStartTime - videoStartTime).toFixed(0)}ms)`
1828
+ );
1829
+ } catch (error) {
1830
+ console.warn("⚠️ SYNC: Cursor sync metadata write failed:", error.message);
1831
+ }
1832
+ }
1833
+
1732
1834
  /**
1733
1835
  * Klavye kısayolu (shortcut) yakalamayı başlatır.
1734
1836
  * Native CGEventTap keyDown olaylarını dinler; yalnızca ⌘/⌃/⌥ modifier'lı
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.0",
3
+ "version": "2.24.2",
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];