node-mac-recorder 2.7.1 → 2.7.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.
@@ -0,0 +1,2051 @@
1
+ #import <napi.h>
2
+ #import <AppKit/AppKit.h>
3
+ #import <Foundation/Foundation.h>
4
+ #import <CoreGraphics/CoreGraphics.h>
5
+ #import <ApplicationServices/ApplicationServices.h>
6
+ #import <Carbon/Carbon.h>
7
+ #import <Accessibility/Accessibility.h>
8
+ #import <QuartzCore/QuartzCore.h>
9
+
10
+ // Forward declarations
11
+ @class WindowSelectorOverlayView;
12
+
13
+ // Global state for window selection
14
+ static bool g_isWindowSelecting = false;
15
+ static NSWindow *g_overlayWindow = nil;
16
+ static NSView *g_overlayView = nil;
17
+ static WindowSelectorOverlayView *g_selectedOverlayView = nil; // Track selected overlay
18
+ static NSButton *g_selectButton = nil;
19
+ static NSTimer *g_trackingTimer = nil;
20
+ static NSDictionary *g_selectedWindowInfo = nil;
21
+ static NSMutableArray *g_allWindows = nil;
22
+ static NSDictionary *g_currentWindowUnderCursor = nil;
23
+ static bool g_bringToFrontEnabled = true; // Default enabled
24
+ static id g_windowKeyEventMonitor = nil;
25
+
26
+ // Recording preview overlay state
27
+ static NSWindow *g_recordingPreviewWindow = nil;
28
+ static NSView *g_recordingPreviewView = nil;
29
+ static NSDictionary *g_recordingWindowInfo = nil;
30
+
31
+ // Screen selection overlay state
32
+ static bool g_isScreenSelecting = false;
33
+ static NSMutableArray *g_screenOverlayWindows = nil;
34
+ static NSDictionary *g_selectedScreenInfo = nil;
35
+ static NSArray *g_allScreens = nil;
36
+ static id g_screenKeyEventMonitor = nil;
37
+ static NSTimer *g_screenTrackingTimer = nil;
38
+ static NSInteger g_currentActiveScreenIndex = -1;
39
+
40
+ // Forward declarations
41
+ void cleanupWindowSelector();
42
+ void updateOverlay();
43
+ NSDictionary* getWindowUnderCursor(CGPoint point);
44
+ NSArray* getAllSelectableWindows();
45
+ bool bringWindowToFront(int windowId);
46
+ void cleanupRecordingPreview();
47
+ bool showRecordingPreview(NSDictionary *windowInfo);
48
+ bool hideRecordingPreview();
49
+ void cleanupScreenSelector();
50
+ bool startScreenSelection();
51
+ bool stopScreenSelection();
52
+ NSDictionary* getSelectedScreenInfo();
53
+ bool showScreenRecordingPreview(NSDictionary *screenInfo);
54
+ bool hideScreenRecordingPreview();
55
+ void updateScreenOverlays();
56
+
57
+ // Custom overlay view class
58
+ @interface WindowSelectorOverlayView : NSView
59
+ @property (nonatomic, strong) NSDictionary *windowInfo;
60
+ @property (nonatomic) BOOL isActiveWindow;
61
+ @property (nonatomic) BOOL isSelectedWindow;
62
+ @end
63
+
64
+ @implementation WindowSelectorOverlayView
65
+
66
+ - (instancetype)initWithFrame:(NSRect)frameRect {
67
+ self = [super initWithFrame:frameRect];
68
+ if (self) {
69
+ // Use layer for background instead of custom drawing
70
+ self.wantsLayer = YES;
71
+ self.isActiveWindow = NO; // Default to inactive (no mouse highlighting)
72
+ self.isSelectedWindow = NO; // Default to not selected
73
+
74
+ // Set initial appearance
75
+ [self updateAppearance];
76
+
77
+ // Window selector overlay view created
78
+ }
79
+ return self;
80
+ }
81
+
82
+ - (void)updateAppearance {
83
+ if (self.isSelectedWindow) {
84
+ // Selected window: bright background with thick border
85
+ self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.6] CGColor];
86
+ self.layer.borderColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:1.0] CGColor];
87
+ self.layer.borderWidth = 3.0; // Thick border for selected window
88
+ // Selected window appearance set
89
+ } else if (self.isActiveWindow) {
90
+ // Active window: brighter background with thin border
91
+ self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] CGColor];
92
+ self.layer.borderColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.8] CGColor];
93
+ self.layer.borderWidth = 1.0; // Thin border for active window
94
+ // Active window appearance set
95
+ } else {
96
+ // Inactive window: dimmer background with no border
97
+ self.layer.backgroundColor = [[NSColor colorWithRed:0.4 green:0.2 blue:0.6 alpha:0.25] CGColor];
98
+ self.layer.borderColor = [[NSColor clearColor] CGColor];
99
+ self.layer.borderWidth = 0.0; // No border for inactive window
100
+ // Inactive window appearance set
101
+ }
102
+
103
+ // Common styling
104
+ self.layer.cornerRadius = 8.0;
105
+ self.layer.masksToBounds = YES;
106
+ self.layer.shadowOpacity = 0.0;
107
+ self.layer.shadowRadius = 0.0;
108
+ self.layer.shadowOffset = NSMakeSize(0, 0);
109
+ }
110
+
111
+ - (void)setIsActiveWindow:(BOOL)isActiveWindow {
112
+ if (_isActiveWindow != isActiveWindow) {
113
+ _isActiveWindow = isActiveWindow;
114
+ [self updateAppearance];
115
+ }
116
+ }
117
+
118
+ - (void)setIsSelectedWindow:(BOOL)isSelectedWindow {
119
+ if (_isSelectedWindow != isSelectedWindow) {
120
+ _isSelectedWindow = isSelectedWindow;
121
+ [self updateAppearance];
122
+ }
123
+ }
124
+
125
+ - (void)mouseDown:(NSEvent *)event {
126
+ // Handle mouse click to select this window
127
+ if (self.windowInfo) {
128
+ // Set this overlay as selected
129
+ self.isSelectedWindow = YES;
130
+
131
+ // Update global selected window info
132
+ if (g_selectedWindowInfo) {
133
+ [g_selectedWindowInfo release];
134
+ }
135
+ g_selectedWindowInfo = [self.windowInfo retain];
136
+
137
+ // Update global selected overlay reference
138
+ g_selectedOverlayView = self;
139
+
140
+ // Update overlay appearance
141
+ [self updateAppearance];
142
+
143
+ NSLog(@"🎯 WINDOW SELECTED VIA CLICK: %@ - \"%@\"",
144
+ [self.windowInfo objectForKey:@"appName"] ?: @"Unknown",
145
+ [self.windowInfo objectForKey:@"title"] ?: @"Untitled");
146
+ }
147
+ }
148
+
149
+ // Layer-based approach, no custom drawing needed
150
+
151
+ @end
152
+
153
+ // Recording preview overlay view - full screen with cutout
154
+ @interface RecordingPreviewView : NSView
155
+ @property (nonatomic, strong) NSDictionary *recordingWindowInfo;
156
+ @end
157
+
158
+ @implementation RecordingPreviewView
159
+
160
+ - (instancetype)initWithFrame:(NSRect)frameRect {
161
+ self = [super initWithFrame:frameRect];
162
+ if (self) {
163
+ self.wantsLayer = YES;
164
+ self.layer.backgroundColor = [[NSColor clearColor] CGColor];
165
+ // Ensure no borders or decorations
166
+ self.layer.borderWidth = 0.0;
167
+ self.layer.cornerRadius = 8.0;
168
+ self.layer.masksToBounds = YES;
169
+ self.layer.shadowOpacity = 0.0;
170
+ self.layer.shadowRadius = 0.0;
171
+ self.layer.shadowOffset = NSMakeSize(0, 0);
172
+ }
173
+ return self;
174
+ }
175
+
176
+ - (void)drawRect:(NSRect)dirtyRect {
177
+ [super drawRect:dirtyRect];
178
+
179
+ if (!self.recordingWindowInfo) {
180
+ // No window info, fill with semi-transparent black
181
+ [[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
182
+ NSRectFill(dirtyRect);
183
+ return;
184
+ }
185
+
186
+ // Get window coordinates
187
+ int windowX = [[self.recordingWindowInfo objectForKey:@"x"] intValue];
188
+ int windowY = [[self.recordingWindowInfo objectForKey:@"y"] intValue];
189
+ int windowWidth = [[self.recordingWindowInfo objectForKey:@"width"] intValue];
190
+ int windowHeight = [[self.recordingWindowInfo objectForKey:@"height"] intValue];
191
+
192
+ // Convert from CGWindow coordinates (top-left) to NSView coordinates (bottom-left)
193
+ NSScreen *mainScreen = [NSScreen mainScreen];
194
+ CGFloat screenHeight = [mainScreen frame].size.height;
195
+ CGFloat convertedY = screenHeight - windowY - windowHeight;
196
+
197
+ NSRect windowRect = NSMakeRect(windowX, convertedY, windowWidth, windowHeight);
198
+
199
+ // Create a path that covers the entire view but excludes the window area
200
+ NSBezierPath *maskPath = [NSBezierPath bezierPathWithRect:self.bounds];
201
+ NSBezierPath *windowPath = [NSBezierPath bezierPathWithRect:windowRect];
202
+ [maskPath appendBezierPath:windowPath];
203
+ [maskPath setWindingRule:NSWindingRuleEvenOdd]; // Creates hole effect
204
+
205
+ // Fill with semi-transparent black, excluding window area
206
+ [[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
207
+ [maskPath fill];
208
+ }
209
+
210
+ @end
211
+
212
+ // Screen selection overlay view
213
+ @interface ScreenSelectorOverlayView : NSView
214
+ @property (nonatomic, strong) NSDictionary *screenInfo;
215
+ @property (nonatomic) BOOL isActiveScreen;
216
+ @end
217
+
218
+ @implementation ScreenSelectorOverlayView
219
+
220
+ - (instancetype)initWithFrame:(NSRect)frameRect {
221
+ self = [super initWithFrame:frameRect];
222
+ if (self) {
223
+ self.wantsLayer = YES;
224
+ self.layer.backgroundColor = [[NSColor clearColor] CGColor];
225
+ // Ensure no borders or decorations
226
+ self.layer.borderWidth = 0.0;
227
+ self.layer.cornerRadius = 8.0;
228
+ self.layer.masksToBounds = YES;
229
+ self.layer.shadowOpacity = 0.0;
230
+ self.layer.shadowRadius = 0.0;
231
+ self.layer.shadowOffset = NSMakeSize(0, 0);
232
+ self.isActiveScreen = NO;
233
+ }
234
+ return self;
235
+ }
236
+
237
+ - (void)drawRect:(NSRect)dirtyRect {
238
+ [super drawRect:dirtyRect];
239
+
240
+ if (!self.screenInfo) return;
241
+
242
+ // Background with transparency - purple tone varies by active state
243
+ if (self.isActiveScreen) {
244
+ // Active screen: brighter, more opaque
245
+ [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] setFill];
246
+ } else {
247
+ // Inactive screen: dimmer, less opaque
248
+ [[NSColor colorWithRed:0.4 green:0.2 blue:0.6 alpha:0.25] setFill];
249
+ }
250
+ NSRectFill(self.bounds);
251
+
252
+ // No border for clean look
253
+ }
254
+
255
+ @end
256
+
257
+ // Button action handler and timer target
258
+ @interface WindowSelectorDelegate : NSObject
259
+ - (void)selectButtonClicked:(id)sender;
260
+ - (void)screenSelectButtonClicked:(id)sender;
261
+ - (void)cancelButtonClicked:(id)sender;
262
+ - (void)timerUpdate:(NSTimer *)timer;
263
+ @end
264
+
265
+ @implementation WindowSelectorDelegate
266
+ - (void)selectButtonClicked:(id)sender {
267
+ if (g_selectedWindowInfo) {
268
+ // Use the selected window info (from click) instead of cursor position
269
+ NSLog(@"🎯 START RECORDING: Selected window confirmed - %@ - \"%@\"",
270
+ [g_selectedWindowInfo objectForKey:@"appName"] ?: @"Unknown",
271
+ [g_selectedWindowInfo objectForKey:@"title"] ?: @"Untitled");
272
+ cleanupWindowSelector();
273
+ } else {
274
+ NSLog(@"⚠️ No window selected - cannot start recording");
275
+ }
276
+ }
277
+
278
+ - (void)screenSelectButtonClicked:(id)sender {
279
+ NSButton *button = (NSButton *)sender;
280
+ NSInteger screenIndex = [button tag];
281
+
282
+ // Get screen info from global array using button tag
283
+ if (g_allScreens && screenIndex >= 0 && screenIndex < [g_allScreens count]) {
284
+ NSDictionary *screenInfo = [g_allScreens objectAtIndex:screenIndex];
285
+ g_selectedScreenInfo = [screenInfo retain];
286
+
287
+ NSLog(@"🖥️ SCREEN SELECTED: %@ (ID: %@)",
288
+ [screenInfo objectForKey:@"name"],
289
+ [screenInfo objectForKey:@"id"]);
290
+
291
+ cleanupScreenSelector();
292
+ }
293
+ }
294
+
295
+ - (void)cancelButtonClicked:(id)sender {
296
+ NSLog(@"🚫 CANCEL BUTTON CLICKED: Selection cancelled");
297
+ // Clean up without selecting anything
298
+ if (g_isScreenSelecting) {
299
+ cleanupScreenSelector();
300
+ } else {
301
+ cleanupWindowSelector();
302
+ }
303
+ }
304
+
305
+ - (void)timerUpdate:(NSTimer *)timer {
306
+ updateOverlay();
307
+ }
308
+
309
+ - (void)screenTimerUpdate:(NSTimer *)timer {
310
+ updateScreenOverlays();
311
+ }
312
+ @end
313
+
314
+ static WindowSelectorDelegate *g_delegate = nil;
315
+
316
+ // Bring window to front using Accessibility API
317
+ bool bringWindowToFront(int windowId) {
318
+ @autoreleasepool {
319
+ @try {
320
+ // Method 1: Using Accessibility API (most reliable)
321
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
322
+ if (!systemWide) return false;
323
+
324
+ CFArrayRef windowList = NULL;
325
+ AXError error = AXUIElementCopyAttributeValue(systemWide, kAXWindowsAttribute, (CFTypeRef*)&windowList);
326
+
327
+ if (error == kAXErrorSuccess && windowList) {
328
+ CFIndex windowCount = CFArrayGetCount(windowList);
329
+
330
+ for (CFIndex i = 0; i < windowCount; i++) {
331
+ AXUIElementRef windowElement = (AXUIElementRef)CFArrayGetValueAtIndex(windowList, i);
332
+
333
+ // Get window ID by comparing with CGWindowList
334
+ // Since _AXUIElementGetWindow is not available, we'll use app PID approach
335
+ pid_t windowPid;
336
+ error = AXUIElementGetPid(windowElement, &windowPid);
337
+
338
+ if (error == kAXErrorSuccess) {
339
+ // Get window info for this PID from CGWindowList
340
+ CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
341
+ if (cgWindowList) {
342
+ NSArray *windowArray = (__bridge NSArray *)cgWindowList;
343
+
344
+ for (NSDictionary *windowInfo in windowArray) {
345
+ NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
346
+ NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
347
+
348
+ if ([cgWindowId intValue] == windowId && [processId intValue] == windowPid) {
349
+ // Found the window, bring it to front
350
+ NSLog(@"🔝 BRINGING TO FRONT: Window ID %d (PID: %d)", windowId, windowPid);
351
+
352
+ // Method 1: Raise specific window (not the whole app)
353
+ error = AXUIElementPerformAction(windowElement, kAXRaiseAction);
354
+ if (error == kAXErrorSuccess) {
355
+ NSLog(@" ✅ Specific window raised successfully");
356
+ } else {
357
+ NSLog(@" ⚠️ Raise action failed: %d", error);
358
+ }
359
+
360
+ // Method 2: Focus specific window (not main window)
361
+ error = AXUIElementSetAttributeValue(windowElement, kAXFocusedAttribute, kCFBooleanTrue);
362
+ if (error == kAXErrorSuccess) {
363
+ NSLog(@" ✅ Specific window focused");
364
+ } else {
365
+ NSLog(@" ⚠️ Focus failed: %d", error);
366
+ }
367
+
368
+ CFRelease(cgWindowList);
369
+ CFRelease(windowList);
370
+ CFRelease(systemWide);
371
+ return true;
372
+ }
373
+ }
374
+ CFRelease(cgWindowList);
375
+ }
376
+ }
377
+ }
378
+ CFRelease(windowList);
379
+ }
380
+
381
+ CFRelease(systemWide);
382
+
383
+ // Method 2: Light activation fallback (minimal app activation)
384
+ NSLog(@" 🔄 Trying minimal activation for window %d", windowId);
385
+
386
+ // Get window info to find the process
387
+ CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
388
+ if (cgWindowList) {
389
+ NSArray *windowArray = (__bridge NSArray *)cgWindowList;
390
+
391
+ for (NSDictionary *windowInfo in windowArray) {
392
+ NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
393
+ if ([cgWindowId intValue] == windowId) {
394
+ // Get process ID
395
+ NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
396
+ if (processId) {
397
+ // Light activation - only bring app to front, don't activate all windows
398
+ NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:[processId intValue]];
399
+ if (app) {
400
+ // Use NSApplicationActivateIgnoringOtherApps only (no NSApplicationActivateAllWindows)
401
+ [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
402
+ NSLog(@" ✅ App minimally activated: PID %d (specific window should be frontmost)", [processId intValue]);
403
+ CFRelease(cgWindowList);
404
+ return true;
405
+ }
406
+ }
407
+ break;
408
+ }
409
+ }
410
+ CFRelease(cgWindowList);
411
+ }
412
+
413
+ return false;
414
+
415
+ } @catch (NSException *exception) {
416
+ NSLog(@"❌ Error bringing window to front: %@", exception);
417
+ return false;
418
+ }
419
+ }
420
+ }
421
+
422
+ // Get all selectable windows
423
+ NSArray* getAllSelectableWindows() {
424
+ @autoreleasepool {
425
+ NSMutableArray *windows = [NSMutableArray array];
426
+
427
+ // Get all windows using CGWindowListCopyWindowInfo
428
+ CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
429
+
430
+ if (windowList) {
431
+ NSArray *windowArray = (__bridge NSArray *)windowList;
432
+
433
+ for (NSDictionary *windowInfo in windowArray) {
434
+ NSString *windowOwner = [windowInfo objectForKey:(NSString *)kCGWindowOwnerName];
435
+ NSString *windowName = [windowInfo objectForKey:(NSString *)kCGWindowName];
436
+ NSNumber *windowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
437
+ NSNumber *windowLayer = [windowInfo objectForKey:(NSString *)kCGWindowLayer];
438
+ NSDictionary *bounds = [windowInfo objectForKey:(NSString *)kCGWindowBounds];
439
+
440
+ // Skip system windows, dock, menu bar, etc.
441
+ if ([windowLayer intValue] != 0) continue; // Only normal windows
442
+ if (!windowOwner || [windowOwner length] == 0) continue;
443
+ if ([windowOwner isEqualToString:@"WindowServer"]) continue;
444
+ if ([windowOwner isEqualToString:@"Dock"]) continue;
445
+
446
+ // Extract bounds
447
+ int x = [[bounds objectForKey:@"X"] intValue];
448
+ int y = [[bounds objectForKey:@"Y"] intValue];
449
+ int width = [[bounds objectForKey:@"Width"] intValue];
450
+ int height = [[bounds objectForKey:@"Height"] intValue];
451
+
452
+ // Skip too small windows
453
+ if (width < 50 || height < 50) continue;
454
+
455
+ NSDictionary *window = @{
456
+ @"id": windowId ?: @(0),
457
+ @"title": windowName ?: @"Untitled",
458
+ @"appName": windowOwner,
459
+ @"x": @(x),
460
+ @"y": @(y),
461
+ @"width": @(width),
462
+ @"height": @(height)
463
+ };
464
+
465
+ [windows addObject:window];
466
+ }
467
+
468
+ CFRelease(windowList);
469
+ }
470
+
471
+ return [windows copy];
472
+ }
473
+ }
474
+
475
+ // Get window under cursor point
476
+ NSDictionary* getWindowUnderCursor(CGPoint point) {
477
+ @autoreleasepool {
478
+ if (!g_allWindows) return nil;
479
+
480
+ // Find window that contains the cursor point
481
+ for (NSDictionary *window in g_allWindows) {
482
+ int x = [[window objectForKey:@"x"] intValue];
483
+ int y = [[window objectForKey:@"y"] intValue];
484
+ int width = [[window objectForKey:@"width"] intValue];
485
+ int height = [[window objectForKey:@"height"] intValue];
486
+
487
+ if (point.x >= x && point.x <= x + width &&
488
+ point.y >= y && point.y <= y + height) {
489
+ return window;
490
+ }
491
+ }
492
+
493
+ return nil;
494
+ }
495
+ }
496
+
497
+ // Update overlay to show selected window (no more mouse-based highlighting)
498
+ void updateOverlay() {
499
+ @autoreleasepool {
500
+ if (!g_isWindowSelecting || !g_overlayWindow) return;
501
+
502
+ // Check if we have a selected window or if we should show overlay for selection
503
+ NSDictionary *windowUnderCursor = nil;
504
+ if (!g_selectedWindowInfo) {
505
+ // No window selected yet - show overlay for the first available window
506
+ if (g_allWindows && [g_allWindows count] > 0) {
507
+ windowUnderCursor = [g_allWindows objectAtIndex:0];
508
+ } else {
509
+ return; // No windows available
510
+ }
511
+ } else {
512
+ // Use selected window info
513
+ windowUnderCursor = g_selectedWindowInfo;
514
+ }
515
+
516
+ // Update overlay position and size
517
+ int x = [[windowUnderCursor objectForKey:@"x"] intValue];
518
+ int y = [[windowUnderCursor objectForKey:@"y"] intValue];
519
+ int width = [[windowUnderCursor objectForKey:@"width"] intValue];
520
+ int height = [[windowUnderCursor objectForKey:@"height"] intValue];
521
+
522
+ // Find which screen contains the window center
523
+ NSArray *screens = [NSScreen screens];
524
+ NSScreen *windowScreen = nil;
525
+ CGFloat windowCenterX = x + width / 2;
526
+ CGFloat windowCenterY = y + height / 2;
527
+
528
+ for (NSScreen *screen in screens) {
529
+ NSRect screenFrame = [screen frame];
530
+ // Convert screen frame to CGWindow coordinates
531
+ CGFloat screenTop = screenFrame.origin.y + screenFrame.size.height;
532
+ CGFloat screenBottom = screenFrame.origin.y;
533
+ CGFloat screenLeft = screenFrame.origin.x;
534
+ CGFloat screenRight = screenFrame.origin.x + screenFrame.size.width;
535
+
536
+ if (windowCenterX >= screenLeft && windowCenterX <= screenRight &&
537
+ windowCenterY >= screenBottom && windowCenterY <= screenTop) {
538
+ windowScreen = screen;
539
+ break;
540
+ }
541
+ }
542
+
543
+ // Use main screen if no specific screen found
544
+ if (!windowScreen) windowScreen = [NSScreen mainScreen];
545
+
546
+ // Convert coordinates from CGWindow (top-left) to NSWindow (bottom-left) for the specific screen
547
+ NSRect screenFrame = [windowScreen frame];
548
+ CGFloat screenHeight = screenFrame.size.height;
549
+ CGFloat adjustedY = screenHeight - y - height;
550
+
551
+ // Window coordinates are in global space, overlay frame should be screen-relative
552
+ // Keep X coordinate as-is (already in global space which is what we want)
553
+ // Only convert Y from top-left to bottom-left coordinate system
554
+ NSRect overlayFrame = NSMakeRect(x, adjustedY, width, height);
555
+
556
+ NSString *windowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Untitled";
557
+ NSString *appName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
558
+
559
+ NSLog(@"🎯 WINDOW DETECTED: %@ - \"%@\"", appName, windowTitle);
560
+ NSLog(@" 📍 Position: (%d, %d) 📏 Size: %d × %d", x, y, width, height);
561
+ NSLog(@" 🖥️ NSRect: (%.0f, %.0f, %.0f, %.0f) 🔝 Level: %ld",
562
+ overlayFrame.origin.x, overlayFrame.origin.y,
563
+ overlayFrame.size.width, overlayFrame.size.height,
564
+ [g_overlayWindow level]);
565
+
566
+ // Bring window to front if enabled
567
+ if (g_bringToFrontEnabled) {
568
+ int windowId = [[windowUnderCursor objectForKey:@"id"] intValue];
569
+ if (windowId > 0) {
570
+ bool success = bringWindowToFront(windowId);
571
+ if (!success) {
572
+ NSLog(@" ⚠️ Failed to bring window to front");
573
+ }
574
+ }
575
+ }
576
+
577
+ // Ensure overlay is on the correct screen
578
+ [g_overlayWindow setFrame:overlayFrame display:YES];
579
+
580
+ // Update overlay view window info
581
+ [(WindowSelectorOverlayView *)g_overlayView setWindowInfo:windowUnderCursor];
582
+
583
+ // Don't automatically mark as selected - wait for user click
584
+ [(WindowSelectorOverlayView *)g_overlayView setIsSelectedWindow:NO];
585
+ [(WindowSelectorOverlayView *)g_overlayView setIsActiveWindow:NO];
586
+
587
+ // Add/update info label above button
588
+ NSTextField *infoLabel = nil;
589
+ for (NSView *subview in [g_overlayWindow.contentView subviews]) {
590
+ if ([subview isKindOfClass:[NSTextField class]]) {
591
+ infoLabel = (NSTextField*)subview;
592
+ break;
593
+ }
594
+ }
595
+
596
+ if (!infoLabel) {
597
+ infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, width - 40, 60)];
598
+ [infoLabel setEditable:NO];
599
+ [infoLabel setSelectable:NO];
600
+ [infoLabel setBezeled:NO];
601
+ [infoLabel setDrawsBackground:NO];
602
+ [infoLabel setAlignment:NSTextAlignmentCenter];
603
+ [infoLabel setFont:[NSFont systemFontOfSize:18 weight:NSFontWeightMedium]];
604
+ [infoLabel setTextColor:[NSColor whiteColor]];
605
+
606
+ // Force no borders on info label
607
+ [infoLabel setWantsLayer:YES];
608
+ infoLabel.layer.borderWidth = 0.0;
609
+ infoLabel.layer.borderColor = [[NSColor clearColor] CGColor];
610
+ infoLabel.layer.cornerRadius = 0.0;
611
+ infoLabel.layer.masksToBounds = YES;
612
+
613
+ [g_overlayWindow.contentView addSubview:infoLabel];
614
+ }
615
+
616
+ // Add/update app icon
617
+ NSImageView *appIconView = nil;
618
+ for (NSView *subview in [g_overlayWindow.contentView subviews]) {
619
+ if ([subview isKindOfClass:[NSImageView class]]) {
620
+ appIconView = (NSImageView*)subview;
621
+ break;
622
+ }
623
+ }
624
+
625
+ if (!appIconView) {
626
+ appIconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 96, 96)];
627
+ [appIconView setImageScaling:NSImageScaleProportionallyUpOrDown];
628
+ [appIconView setWantsLayer:YES];
629
+ [appIconView.layer setCornerRadius:8.0];
630
+ [appIconView.layer setMasksToBounds:YES];
631
+ [appIconView.layer setBackgroundColor:[[NSColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.3] CGColor]]; // Debug background
632
+
633
+ // Force no borders on app icon view
634
+ appIconView.layer.borderWidth = 0.0;
635
+ appIconView.layer.borderColor = [[NSColor clearColor] CGColor];
636
+ appIconView.layer.shadowOpacity = 0.0;
637
+ appIconView.layer.shadowRadius = 0.0;
638
+ appIconView.layer.shadowOffset = NSMakeSize(0, 0);
639
+
640
+ [g_overlayWindow.contentView addSubview:appIconView];
641
+ NSLog(@"🖼️ Created app icon view at frame: (%.0f, %.0f, %.0f, %.0f)",
642
+ appIconView.frame.origin.x, appIconView.frame.origin.y,
643
+ appIconView.frame.size.width, appIconView.frame.size.height);
644
+ }
645
+
646
+ // Get app icon using NSWorkspace
647
+ NSString *iconAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
648
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
649
+ NSArray *runningApps = [workspace runningApplications];
650
+ NSImage *appIcon = nil;
651
+
652
+ for (NSRunningApplication *app in runningApps) {
653
+ if ([[app localizedName] isEqualToString:iconAppName] || [[app bundleIdentifier] containsString:iconAppName]) {
654
+ appIcon = [app icon];
655
+ break;
656
+ }
657
+ }
658
+
659
+ // Fallback to generic app icon if not found
660
+ if (!appIcon) {
661
+ appIcon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)];
662
+ NSLog(@"⚠️ Using fallback icon for app: %@", iconAppName);
663
+ } else {
664
+ NSLog(@"✅ Found app icon for: %@", iconAppName);
665
+ }
666
+
667
+ [appIconView setImage:appIcon];
668
+ NSLog(@"🖼️ Set icon image, size: %.0fx%.0f", [appIcon size].width, [appIcon size].height);
669
+
670
+ // Update label text
671
+ NSString *labelWindowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Unknown Window";
672
+ NSString *labelAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown App";
673
+ [infoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", labelAppName, labelWindowTitle]];
674
+
675
+ // Position buttons - Start Record in center, Cancel below it
676
+ if (g_selectButton) {
677
+ NSSize buttonSize = [g_selectButton frame].size;
678
+ NSPoint buttonCenter = NSMakePoint(
679
+ (width - buttonSize.width) / 2,
680
+ (height - buttonSize.height) / 2 + 15 // Slightly above center
681
+ );
682
+ [g_selectButton setFrameOrigin:buttonCenter];
683
+
684
+ // Position app icon above text label
685
+ NSPoint iconCenter = NSMakePoint(
686
+ (width - 96) / 2, // Center horizontally (icon is 96px wide)
687
+ buttonCenter.y + buttonSize.height + 60 + 10 // Above label + text height + margin
688
+ );
689
+ [appIconView setFrameOrigin:iconCenter];
690
+ NSLog(@"🎯 Positioning app icon at: (%.0f, %.0f) for window size: (%.0f, %.0f)",
691
+ iconCenter.x, iconCenter.y, (float)width, (float)height);
692
+
693
+ // Add fast horizontal floating animation after positioning
694
+ [appIconView.layer removeAnimationForKey:@"floatAnimationX"];
695
+ [appIconView.layer removeAnimationForKey:@"floatAnimationY"];
696
+
697
+ // Faster horizontal float animation only
698
+ CABasicAnimation *floatAnimationX = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
699
+ floatAnimationX.fromValue = @(-4.0);
700
+ floatAnimationX.toValue = @(4.0);
701
+ floatAnimationX.duration = 1.0; // Much faster animation
702
+ floatAnimationX.repeatCount = HUGE_VALF;
703
+ floatAnimationX.autoreverses = YES;
704
+ floatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
705
+ [appIconView.layer addAnimation:floatAnimationX forKey:@"floatAnimationX"];
706
+
707
+ // Position info label at overlay center, above button
708
+ NSPoint labelCenter = NSMakePoint(
709
+ (width - [infoLabel frame].size.width) / 2, // Center horizontally
710
+ buttonCenter.y + buttonSize.height + 10 // 10px above button, below icon
711
+ );
712
+ [infoLabel setFrameOrigin:labelCenter];
713
+
714
+ // Position cancel button below the main button
715
+ NSButton *cancelButton = nil;
716
+ for (NSView *subview in [g_overlayWindow.contentView subviews]) {
717
+ if ([subview isKindOfClass:[NSButton class]] &&
718
+ [[(NSButton*)subview title] isEqualToString:@"Cancel"]) {
719
+ cancelButton = (NSButton*)subview;
720
+ break;
721
+ }
722
+ }
723
+
724
+ if (cancelButton) {
725
+ NSSize cancelButtonSize = [cancelButton frame].size;
726
+ NSPoint cancelButtonCenter = NSMakePoint(
727
+ (width - cancelButtonSize.width) / 2,
728
+ buttonCenter.y - buttonSize.height - 20 // 20px below main button
729
+ );
730
+ [cancelButton setFrameOrigin:cancelButtonCenter];
731
+ }
732
+ }
733
+
734
+ // Show overlay for the current window (will be selected when clicked)
735
+ [g_overlayWindow orderFront:nil];
736
+ [g_overlayWindow makeKeyAndOrderFront:nil];
737
+
738
+ // Ensure all subviews have no borders after positioning, but preserve corner radius for buttons and icons
739
+ for (NSView *subview in [g_overlayWindow.contentView subviews]) {
740
+ if ([subview respondsToSelector:@selector(setWantsLayer:)]) {
741
+ [subview setWantsLayer:YES];
742
+ if (subview.layer) {
743
+ subview.layer.borderWidth = 0.0;
744
+ subview.layer.borderColor = [[NSColor clearColor] CGColor];
745
+ subview.layer.masksToBounds = YES;
746
+ subview.layer.shadowOpacity = 0.0;
747
+ subview.layer.shadowRadius = 0.0;
748
+ subview.layer.shadowOffset = NSMakeSize(0, 0);
749
+ }
750
+ }
751
+ }
752
+
753
+ NSLog(@" ✅ Overlay Status: Level=%ld, Alpha=%.1f, Visible=%s, Frame Set=YES",
754
+ [g_overlayWindow level], [g_overlayWindow alphaValue],
755
+ [g_overlayWindow isVisible] ? "YES" : "NO");
756
+
757
+ // Update overlay view states - check if this window is selected
758
+ if (g_overlayView && [g_overlayView isKindOfClass:[WindowSelectorOverlayView class]]) {
759
+ WindowSelectorOverlayView *overlayView = (WindowSelectorOverlayView *)g_overlayView;
760
+
761
+ // Check if this is the selected window (true if we have selected window info)
762
+ BOOL isSelected = (g_selectedWindowInfo != nil);
763
+
764
+ // Update states
765
+ overlayView.isActiveWindow = NO; // No more mouse-based highlighting
766
+ overlayView.isSelectedWindow = isSelected;
767
+
768
+ NSLog(@"🎯 Overlay State Updated: Active=NO, Selected=%s", isSelected ? "YES" : "NO");
769
+ }
770
+ } else if (!windowUnderCursor) {
771
+ // No selected window, hide overlay
772
+ [g_overlayWindow orderOut:nil];
773
+ }
774
+ }
775
+ }
776
+
777
+ // Cleanup function
778
+ void cleanupWindowSelector() {
779
+ g_isWindowSelecting = false;
780
+
781
+ // Stop tracking timer
782
+ if (g_trackingTimer) {
783
+ [g_trackingTimer invalidate];
784
+ g_trackingTimer = nil;
785
+ }
786
+
787
+ // Remove key event monitor
788
+ if (g_windowKeyEventMonitor) {
789
+ [NSEvent removeMonitor:g_windowKeyEventMonitor];
790
+ g_windowKeyEventMonitor = nil;
791
+ }
792
+
793
+ // Close overlay window
794
+ if (g_overlayWindow) {
795
+ [g_overlayWindow close];
796
+ g_overlayWindow = nil;
797
+ g_overlayView = nil;
798
+ g_selectButton = nil;
799
+ }
800
+
801
+ // Clean up delegate
802
+ if (g_delegate) {
803
+ [g_delegate release];
804
+ g_delegate = nil;
805
+ }
806
+
807
+ // Clean up data
808
+ if (g_allWindows) {
809
+ [g_allWindows release];
810
+ g_allWindows = nil;
811
+ }
812
+
813
+ // Clean up selected overlay
814
+ if (g_selectedOverlayView) {
815
+ g_selectedOverlayView.isSelectedWindow = NO;
816
+ g_selectedOverlayView = nil;
817
+ }
818
+ }
819
+
820
+ // Recording preview functions
821
+ void cleanupRecordingPreview() {
822
+ if (g_recordingPreviewWindow) {
823
+ [g_recordingPreviewWindow close];
824
+ g_recordingPreviewWindow = nil;
825
+ g_recordingPreviewView = nil;
826
+ }
827
+
828
+ if (g_recordingWindowInfo) {
829
+ [g_recordingWindowInfo release];
830
+ g_recordingWindowInfo = nil;
831
+ }
832
+ }
833
+
834
+ bool showRecordingPreview(NSDictionary *windowInfo) {
835
+ @try {
836
+ // Clean up any existing preview
837
+ cleanupRecordingPreview();
838
+
839
+ if (!windowInfo) return false;
840
+
841
+ // Store window info
842
+ g_recordingWindowInfo = [windowInfo retain];
843
+
844
+ // Get main screen bounds for full screen overlay
845
+ NSScreen *mainScreen = [NSScreen mainScreen];
846
+ NSRect screenFrame = [mainScreen frame];
847
+
848
+ // Create full-screen overlay window
849
+ g_recordingPreviewWindow = [[NSWindow alloc] initWithContentRect:screenFrame
850
+ styleMask:NSWindowStyleMaskBorderless
851
+ backing:NSBackingStoreBuffered
852
+ defer:NO];
853
+
854
+ [g_recordingPreviewWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)]; // High level but below selection
855
+ [g_recordingPreviewWindow setOpaque:NO];
856
+ [g_recordingPreviewWindow setBackgroundColor:[NSColor clearColor]];
857
+ [g_recordingPreviewWindow setIgnoresMouseEvents:YES]; // Don't interfere with user interaction
858
+ [g_recordingPreviewWindow setAcceptsMouseMovedEvents:NO];
859
+ [g_recordingPreviewWindow setHasShadow:NO];
860
+ [g_recordingPreviewWindow setAlphaValue:1.0];
861
+ [g_recordingPreviewWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
862
+
863
+ // Remove any default window decorations and borders
864
+ [g_recordingPreviewWindow setTitlebarAppearsTransparent:YES];
865
+ [g_recordingPreviewWindow setTitleVisibility:NSWindowTitleHidden];
866
+ [g_recordingPreviewWindow setMovable:NO];
867
+ [g_recordingPreviewWindow setMovableByWindowBackground:NO];
868
+
869
+ // Create preview view
870
+ g_recordingPreviewView = [[RecordingPreviewView alloc] initWithFrame:screenFrame];
871
+ [(RecordingPreviewView *)g_recordingPreviewView setRecordingWindowInfo:windowInfo];
872
+ [g_recordingPreviewWindow setContentView:g_recordingPreviewView];
873
+
874
+ // Show the preview
875
+ [g_recordingPreviewWindow orderFront:nil];
876
+ [g_recordingPreviewWindow makeKeyAndOrderFront:nil];
877
+
878
+ NSLog(@"🎬 RECORDING PREVIEW: Showing overlay for %@ - \"%@\"",
879
+ [windowInfo objectForKey:@"appName"],
880
+ [windowInfo objectForKey:@"title"]);
881
+
882
+ return true;
883
+
884
+ } @catch (NSException *exception) {
885
+ NSLog(@"❌ Error showing recording preview: %@", exception);
886
+ cleanupRecordingPreview();
887
+ return false;
888
+ }
889
+ }
890
+
891
+ bool hideRecordingPreview() {
892
+ @try {
893
+ if (g_recordingPreviewWindow) {
894
+ NSLog(@"🎬 RECORDING PREVIEW: Hiding overlay");
895
+ cleanupRecordingPreview();
896
+ return true;
897
+ }
898
+ return false;
899
+
900
+ } @catch (NSException *exception) {
901
+ NSLog(@"❌ Error hiding recording preview: %@", exception);
902
+ return false;
903
+ }
904
+ }
905
+
906
+ // Update screen overlays based on mouse position
907
+ void updateScreenOverlays() {
908
+ @autoreleasepool {
909
+ if (!g_isScreenSelecting || !g_screenOverlayWindows || !g_allScreens) return;
910
+
911
+ // Get current mouse position
912
+ NSPoint mouseLocation = [NSEvent mouseLocation];
913
+ // Convert from NSEvent coordinates (bottom-left) to screen coordinates
914
+ NSArray *screens = [NSScreen screens];
915
+ NSScreen *mouseScreen = nil;
916
+ NSInteger mouseScreenIndex = -1;
917
+
918
+ // Find which screen contains the mouse
919
+ for (NSInteger i = 0; i < [screens count]; i++) {
920
+ NSScreen *screen = [screens objectAtIndex:i];
921
+ NSRect screenFrame = [screen frame];
922
+
923
+ if (NSPointInRect(mouseLocation, screenFrame)) {
924
+ mouseScreen = screen;
925
+ mouseScreenIndex = i;
926
+ break;
927
+ }
928
+ }
929
+
930
+ // If mouse screen changed, update overlays
931
+ if (mouseScreenIndex != g_currentActiveScreenIndex) {
932
+ g_currentActiveScreenIndex = mouseScreenIndex;
933
+
934
+ // Update all screen overlays
935
+ for (NSInteger i = 0; i < [g_screenOverlayWindows count] && i < [screens count]; i++) {
936
+ NSWindow *overlayWindow = [g_screenOverlayWindows objectAtIndex:i];
937
+ ScreenSelectorOverlayView *overlayView = (ScreenSelectorOverlayView *)[overlayWindow contentView];
938
+
939
+ // Update overlay appearance based on whether it's the active screen
940
+ bool isActiveScreen = (i == mouseScreenIndex);
941
+
942
+ // Update overlay state and appearance
943
+ [overlayView setIsActiveScreen:isActiveScreen];
944
+ [overlayView setNeedsDisplay:YES];
945
+
946
+ // Update UI elements based on active state
947
+ for (NSView *subview in [overlayView subviews]) {
948
+ if ([subview isKindOfClass:[NSButton class]]) {
949
+ NSButton *button = (NSButton *)subview;
950
+ if ([button.title isEqualToString:@"Start Record"]) {
951
+ if (isActiveScreen) {
952
+ // Active screen: bright, prominent button with new RGB color
953
+ [button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:1.0] CGColor]];
954
+ [button setAlphaValue:1.0];
955
+ } else {
956
+ // Inactive screen: dimmer button with new RGB color
957
+ [button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.6] CGColor]];
958
+ [button setAlphaValue:0.7];
959
+ }
960
+ }
961
+ }
962
+ if ([subview isKindOfClass:[NSTextField class]]) {
963
+ NSTextField *label = (NSTextField *)subview;
964
+ if (isActiveScreen) {
965
+ [label setTextColor:[NSColor whiteColor]];
966
+ [label setAlphaValue:1.0];
967
+ } else {
968
+ [label setTextColor:[NSColor colorWithWhite:0.8 alpha:0.8]];
969
+ [label setAlphaValue:0.7];
970
+ }
971
+ }
972
+ if ([subview isKindOfClass:[NSImageView class]]) {
973
+ NSImageView *imageView = (NSImageView *)subview;
974
+ if (isActiveScreen) {
975
+ [imageView setAlphaValue:1.0];
976
+ } else {
977
+ [imageView setAlphaValue:0.6];
978
+ }
979
+ }
980
+ }
981
+
982
+ // Log active screen changes for debugging (optional)
983
+ if (isActiveScreen) {
984
+ NSLog(@"🖥️ Active screen: Display %ld", (long)(i + 1));
985
+ }
986
+
987
+ // Ensure ALL overlays are visible, but active one is on top
988
+ [overlayWindow orderFront:nil];
989
+ if (isActiveScreen) {
990
+ [overlayWindow makeKeyAndOrderFront:nil];
991
+ } else {
992
+ [overlayWindow orderFront:nil]; // Keep inactive screens visible too
993
+ }
994
+ }
995
+ }
996
+ }
997
+ }
998
+
999
+ // Screen selection functions
1000
+ void cleanupScreenSelector() {
1001
+ g_isScreenSelecting = false;
1002
+
1003
+ // Stop screen tracking timer
1004
+ if (g_screenTrackingTimer) {
1005
+ [g_screenTrackingTimer invalidate];
1006
+ g_screenTrackingTimer = nil;
1007
+ }
1008
+
1009
+ // Remove key event monitor
1010
+ if (g_screenKeyEventMonitor) {
1011
+ [NSEvent removeMonitor:g_screenKeyEventMonitor];
1012
+ g_screenKeyEventMonitor = nil;
1013
+ }
1014
+
1015
+ // Close all screen overlay windows
1016
+ if (g_screenOverlayWindows) {
1017
+ for (NSWindow *overlayWindow in g_screenOverlayWindows) {
1018
+ [overlayWindow close];
1019
+ }
1020
+ [g_screenOverlayWindows release];
1021
+ g_screenOverlayWindows = nil;
1022
+ }
1023
+
1024
+ // Clean up screen data
1025
+ if (g_allScreens) {
1026
+ [g_allScreens release];
1027
+ g_allScreens = nil;
1028
+ }
1029
+
1030
+ // Reset active screen tracking
1031
+ g_currentActiveScreenIndex = -1;
1032
+ }
1033
+
1034
+ bool startScreenSelection() {
1035
+ @try {
1036
+ if (g_isScreenSelecting) return false;
1037
+
1038
+ // Get all available screens
1039
+ NSArray *screens = [NSScreen screens];
1040
+ if (!screens || [screens count] == 0) return false;
1041
+
1042
+ // Create screen info array
1043
+ NSMutableArray *screenInfoArray = [[NSMutableArray alloc] init];
1044
+ g_screenOverlayWindows = [[NSMutableArray alloc] init];
1045
+
1046
+ // Get real display IDs like MacRecorder does
1047
+ CGDirectDisplayID activeDisplays[32];
1048
+ uint32_t displayCount;
1049
+ CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
1050
+
1051
+ if (err != kCGErrorSuccess) {
1052
+ NSLog(@"❌ Failed to get active display list: %d", err);
1053
+ return false;
1054
+ }
1055
+
1056
+ for (NSInteger i = 0; i < [screens count]; i++) {
1057
+ NSScreen *screen = [screens objectAtIndex:i];
1058
+ NSRect screenFrame = [screen frame];
1059
+
1060
+ // Get the real CGDirectDisplayID for this screen by matching frame
1061
+ CGDirectDisplayID displayID = 0;
1062
+
1063
+ // Find matching display by comparing bounds
1064
+ for (uint32_t j = 0; j < displayCount; j++) {
1065
+ CGDirectDisplayID candidateID = activeDisplays[j];
1066
+ CGRect displayBounds = CGDisplayBounds(candidateID);
1067
+
1068
+ // Compare screen frame with display bounds
1069
+ if (fabs(screenFrame.origin.x - displayBounds.origin.x) < 1.0 &&
1070
+ fabs(screenFrame.origin.y - displayBounds.origin.y) < 1.0 &&
1071
+ fabs(screenFrame.size.width - displayBounds.size.width) < 1.0 &&
1072
+ fabs(screenFrame.size.height - displayBounds.size.height) < 1.0) {
1073
+ displayID = candidateID;
1074
+ // Screen matched to display ID
1075
+ break;
1076
+ }
1077
+ }
1078
+
1079
+ // Fallback: use array index if no match found
1080
+ if (displayID == 0 && i < displayCount) {
1081
+ displayID = activeDisplays[i];
1082
+ // Used fallback display ID
1083
+ } else if (displayID == 0) {
1084
+ NSLog(@"❌ Screen %ld could not get Display ID", (long)i);
1085
+ }
1086
+
1087
+ // Create screen info dictionary with real display ID
1088
+ NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
1089
+ [screenInfo setObject:[NSNumber numberWithUnsignedInt:displayID] forKey:@"id"]; // Real display ID
1090
+ [screenInfo setObject:[NSString stringWithFormat:@"Display %ld", (long)(i + 1)] forKey:@"name"];
1091
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.x] forKey:@"x"];
1092
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.y] forKey:@"y"];
1093
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.width] forKey:@"width"];
1094
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.height] forKey:@"height"];
1095
+ [screenInfo setObject:[NSString stringWithFormat:@"%.0fx%.0f", screenFrame.size.width, screenFrame.size.height] forKey:@"resolution"];
1096
+ [screenInfo setObject:[NSNumber numberWithBool:(displayID == CGMainDisplayID())] forKey:@"isPrimary"]; // Real primary check
1097
+ [screenInfoArray addObject:screenInfo];
1098
+
1099
+ // Create overlay window for this screen (FULL screen including menu bar)
1100
+ // For secondary screens, don't specify screen parameter to avoid issues
1101
+ NSWindow *overlayWindow;
1102
+ if (i == 0) {
1103
+ // Primary screen - use screen parameter
1104
+ overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
1105
+ styleMask:NSWindowStyleMaskBorderless
1106
+ backing:NSBackingStoreBuffered
1107
+ defer:NO
1108
+ screen:screen];
1109
+ } else {
1110
+ // Secondary screens - create without screen param, set frame manually
1111
+ overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
1112
+ styleMask:NSWindowStyleMaskBorderless
1113
+ backing:NSBackingStoreBuffered
1114
+ defer:NO];
1115
+ // Force specific positioning for secondary screen
1116
+ [overlayWindow setFrameOrigin:screenFrame.origin];
1117
+ }
1118
+
1119
+ // Window created for specific screen
1120
+
1121
+ // Use maximum level to match g_overlayWindow
1122
+ [overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
1123
+ [overlayWindow setOpaque:NO];
1124
+ [overlayWindow setBackgroundColor:[NSColor clearColor]];
1125
+ [overlayWindow setIgnoresMouseEvents:NO];
1126
+ [overlayWindow setAcceptsMouseMovedEvents:YES];
1127
+ [overlayWindow setHasShadow:NO];
1128
+ [overlayWindow setAlphaValue:1.0];
1129
+ // Ensure window appears on all spaces and stays put - match g_overlayWindow
1130
+ [overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
1131
+
1132
+ // Remove any default window decorations and borders
1133
+ [overlayWindow setTitlebarAppearsTransparent:YES];
1134
+ [overlayWindow setTitleVisibility:NSWindowTitleHidden];
1135
+ [overlayWindow setMovable:NO];
1136
+ [overlayWindow setMovableByWindowBackground:NO];
1137
+
1138
+ // Force remove all borders and decorations
1139
+ [overlayWindow setHasShadow:NO];
1140
+ [overlayWindow setOpaque:NO];
1141
+ [overlayWindow setBackgroundColor:[NSColor clearColor]];
1142
+
1143
+ // Create overlay view
1144
+ ScreenSelectorOverlayView *overlayView = [[ScreenSelectorOverlayView alloc] initWithFrame:screenFrame];
1145
+ [overlayView setScreenInfo:screenInfo];
1146
+ [overlayWindow setContentView:overlayView];
1147
+
1148
+ // Note: NSWindow doesn't have setWantsLayer method, only NSView does
1149
+
1150
+ // Create select button with more padding
1151
+ NSButton *selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 60)];
1152
+ [selectButton setTitle:@"Start Record"];
1153
+ [selectButton setButtonType:NSButtonTypeMomentaryPushIn];
1154
+ [selectButton setBordered:NO];
1155
+ [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1156
+ [selectButton setTag:i]; // Set screen index as tag
1157
+
1158
+ // Modern button styling with new RGB color
1159
+ [selectButton setWantsLayer:YES];
1160
+ [selectButton.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.95] CGColor]];
1161
+ [selectButton.layer setCornerRadius:8.0];
1162
+ [selectButton.layer setBorderWidth:0.0];
1163
+
1164
+ // Remove all button borders and decorations
1165
+ [selectButton.layer setShadowOpacity:0.0];
1166
+ [selectButton.layer setShadowRadius:0.0];
1167
+ [selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
1168
+ [selectButton.layer setMasksToBounds:YES];
1169
+
1170
+ // Clean white text - normal weight
1171
+ [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1172
+ [selectButton setTitle:@"Start Record"];
1173
+ NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
1174
+ initWithString:[selectButton title]];
1175
+ [titleString addAttribute:NSForegroundColorAttributeName
1176
+ value:[NSColor whiteColor]
1177
+ range:NSMakeRange(0, [titleString length])];
1178
+ [selectButton setAttributedTitle:titleString];
1179
+
1180
+ // Clean button - no shadows or highlights
1181
+
1182
+ // Set button target and action (reuse global delegate)
1183
+ if (!g_delegate) {
1184
+ g_delegate = [[WindowSelectorDelegate alloc] init];
1185
+ }
1186
+ [selectButton setTarget:g_delegate];
1187
+ [selectButton setAction:@selector(screenSelectButtonClicked:)];
1188
+
1189
+ // Remove focus ring and other default button behaviors
1190
+ [selectButton setFocusRingType:NSFocusRingTypeNone];
1191
+ [selectButton setShowsBorderOnlyWhileMouseInside:NO];
1192
+
1193
+ // Create cancel button for screen selection
1194
+ NSButton *screenCancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
1195
+ [screenCancelButton setTitle:@"Cancel"];
1196
+ [screenCancelButton setButtonType:NSButtonTypeMomentaryPushIn];
1197
+ [screenCancelButton setBordered:NO];
1198
+ [screenCancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
1199
+
1200
+ // Modern cancel button styling - darker gray, clean
1201
+ [screenCancelButton setWantsLayer:YES];
1202
+ [screenCancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.35 green:0.35 blue:0.4 alpha:0.9] CGColor]];
1203
+ [screenCancelButton.layer setCornerRadius:8.0];
1204
+ [screenCancelButton.layer setBorderWidth:0.0];
1205
+
1206
+ // Remove all button borders and decorations
1207
+ [screenCancelButton.layer setShadowOpacity:0.0];
1208
+ [screenCancelButton.layer setShadowRadius:0.0];
1209
+ [screenCancelButton.layer setShadowOffset:NSMakeSize(0, 0)];
1210
+ [screenCancelButton.layer setMasksToBounds:YES];
1211
+
1212
+ // Clean white text for cancel button
1213
+ [screenCancelButton setFont:[NSFont systemFontOfSize:15 weight:NSFontWeightRegular]];
1214
+ NSMutableAttributedString *screenCancelTitleString = [[NSMutableAttributedString alloc]
1215
+ initWithString:[screenCancelButton title]];
1216
+ [screenCancelTitleString addAttribute:NSForegroundColorAttributeName
1217
+ value:[NSColor whiteColor]
1218
+ range:NSMakeRange(0, [screenCancelTitleString length])];
1219
+ [screenCancelButton setAttributedTitle:screenCancelTitleString];
1220
+
1221
+ [screenCancelButton setTarget:g_delegate];
1222
+ [screenCancelButton setAction:@selector(cancelButtonClicked:)];
1223
+
1224
+ // Remove focus ring and other default button behaviors
1225
+ [screenCancelButton setFocusRingType:NSFocusRingTypeNone];
1226
+ [screenCancelButton setShowsBorderOnlyWhileMouseInside:NO];
1227
+
1228
+ // Create info label for screen
1229
+ NSTextField *screenInfoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, screenFrame.size.width - 40, 60)];
1230
+ [screenInfoLabel setEditable:NO];
1231
+ [screenInfoLabel setSelectable:NO];
1232
+ [screenInfoLabel setBezeled:NO];
1233
+ [screenInfoLabel setDrawsBackground:NO];
1234
+ [screenInfoLabel setAlignment:NSTextAlignmentCenter];
1235
+ [screenInfoLabel setFont:[NSFont systemFontOfSize:20 weight:NSFontWeightMedium]];
1236
+ [screenInfoLabel setTextColor:[NSColor whiteColor]];
1237
+
1238
+ // Create screen icon (display icon)
1239
+ NSImageView *screenIconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 96, 96)];
1240
+ [screenIconView setImageScaling:NSImageScaleProportionallyUpOrDown];
1241
+ [screenIconView setWantsLayer:YES];
1242
+ [screenIconView.layer setCornerRadius:8.0];
1243
+ [screenIconView.layer setMasksToBounds:YES];
1244
+
1245
+ // Set display icon
1246
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
1247
+ NSImage *displayIcon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kComputerIcon)];
1248
+ [screenIconView setImage:displayIcon];
1249
+
1250
+ // Set screen info text
1251
+ NSString *screenName = [screenInfo objectForKey:@"name"] ?: @"Unknown Screen";
1252
+ NSString *resolution = [screenInfo objectForKey:@"resolution"] ?: @"Unknown Resolution";
1253
+ [screenInfoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", screenName, resolution]];
1254
+
1255
+ // Position buttons - Start Record in center, Cancel below it
1256
+ NSPoint buttonCenter = NSMakePoint(
1257
+ (screenFrame.size.width - [selectButton frame].size.width) / 2,
1258
+ (screenFrame.size.height - [selectButton frame].size.height) / 2 + 15 // Slightly above center
1259
+ );
1260
+ [selectButton setFrameOrigin:buttonCenter];
1261
+
1262
+ // Position screen icon above text label
1263
+ NSPoint iconCenter = NSMakePoint(
1264
+ (screenFrame.size.width - 96) / 2, // Center horizontally (icon is 96px wide)
1265
+ buttonCenter.y + [selectButton frame].size.height + 60 + 10 // Above label + text height + margin
1266
+ );
1267
+ [screenIconView setFrameOrigin:iconCenter];
1268
+
1269
+ // Add fast horizontal floating animation to screen icon
1270
+ CABasicAnimation *screenFloatAnimationX = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
1271
+ screenFloatAnimationX.fromValue = @(-4.0);
1272
+ screenFloatAnimationX.toValue = @(4.0);
1273
+ screenFloatAnimationX.duration = 1.2; // Much faster animation
1274
+ screenFloatAnimationX.repeatCount = HUGE_VALF;
1275
+ screenFloatAnimationX.autoreverses = YES;
1276
+ screenFloatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
1277
+ [screenIconView.layer addAnimation:screenFloatAnimationX forKey:@"floatAnimationX"];
1278
+
1279
+ // Position info label at screen center, above button
1280
+ NSPoint labelCenter = NSMakePoint(
1281
+ (screenFrame.size.width - [screenInfoLabel frame].size.width) / 2, // Center horizontally
1282
+ buttonCenter.y + [selectButton frame].size.height + 10 // 10px above button, below icon
1283
+ );
1284
+ [screenInfoLabel setFrameOrigin:labelCenter];
1285
+
1286
+ NSPoint cancelButtonCenter = NSMakePoint(
1287
+ (screenFrame.size.width - [screenCancelButton frame].size.width) / 2,
1288
+ buttonCenter.y - [selectButton frame].size.height - 20 // 20px below main button
1289
+ );
1290
+ [screenCancelButton setFrameOrigin:cancelButtonCenter];
1291
+
1292
+ [overlayView addSubview:screenIconView];
1293
+ [overlayView addSubview:screenInfoLabel];
1294
+ [overlayView addSubview:selectButton];
1295
+ [overlayView addSubview:screenCancelButton];
1296
+
1297
+ // Ensure window frame is correct for this screen
1298
+ [overlayWindow setFrame:screenFrame display:YES animate:NO];
1299
+
1300
+ // Show overlay - different strategy for secondary screens
1301
+ if (i == 0) {
1302
+ // Primary screen
1303
+ [overlayWindow makeKeyAndOrderFront:nil];
1304
+ // Primary screen overlay shown
1305
+ } else {
1306
+ // Secondary screens - more aggressive approach
1307
+ [overlayWindow orderFront:nil];
1308
+ [overlayWindow makeKeyAndOrderFront:nil]; // Try makeKey too
1309
+ [overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Match g_overlayWindow level
1310
+
1311
+ // Secondary screen overlay shown
1312
+
1313
+ // Double-check with delayed re-show
1314
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
1315
+ [overlayWindow orderFront:nil];
1316
+ [overlayWindow makeKeyAndOrderFront:nil];
1317
+ });
1318
+ }
1319
+
1320
+ // Additional visibility settings
1321
+ [overlayWindow setAlphaValue:1.0];
1322
+ [overlayWindow setIsVisible:YES];
1323
+
1324
+ // Overlay window is now ready and visible
1325
+
1326
+ [g_screenOverlayWindows addObject:overlayWindow];
1327
+ [screenInfo release];
1328
+ }
1329
+
1330
+ g_allScreens = [screenInfoArray retain];
1331
+ [screenInfoArray release];
1332
+ g_isScreenSelecting = true;
1333
+ g_currentActiveScreenIndex = -1;
1334
+
1335
+ // Add ESC key event monitor to cancel selection
1336
+ g_screenKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
1337
+ handler:^(NSEvent *event) {
1338
+ if ([event keyCode] == 53) { // ESC key
1339
+ NSLog(@"🖥️ SCREEN SELECTION: ESC pressed - cancelling selection");
1340
+ cleanupScreenSelector();
1341
+ }
1342
+ }];
1343
+
1344
+ // Start screen tracking timer to update overlays based on mouse position
1345
+ if (!g_delegate) {
1346
+ g_delegate = [[WindowSelectorDelegate alloc] init];
1347
+ }
1348
+ g_screenTrackingTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 // 20 FPS
1349
+ target:g_delegate
1350
+ selector:@selector(screenTimerUpdate:)
1351
+ userInfo:nil
1352
+ repeats:YES];
1353
+
1354
+ // Initial update to set correct highlighting
1355
+ updateScreenOverlays();
1356
+
1357
+ NSLog(@"🖥️ SCREEN SELECTION: Started with %lu screens (ESC to cancel)", (unsigned long)[screens count]);
1358
+ NSLog(@"🖥️ SCREEN TRACKING: Timer started for overlay updates");
1359
+
1360
+ return true;
1361
+
1362
+ } @catch (NSException *exception) {
1363
+ NSLog(@"❌ Error starting screen selection: %@", exception);
1364
+ cleanupScreenSelector();
1365
+ return false;
1366
+ }
1367
+ }
1368
+
1369
+ bool stopScreenSelection() {
1370
+ @try {
1371
+ if (!g_isScreenSelecting) return false;
1372
+
1373
+ cleanupScreenSelector();
1374
+ NSLog(@"🖥️ SCREEN SELECTION: Stopped");
1375
+ return true;
1376
+
1377
+ } @catch (NSException *exception) {
1378
+ NSLog(@"❌ Error stopping screen selection: %@", exception);
1379
+ return false;
1380
+ }
1381
+ }
1382
+
1383
+ NSDictionary* getSelectedScreenInfo() {
1384
+ if (!g_selectedScreenInfo) return nil;
1385
+
1386
+ NSDictionary *result = [g_selectedScreenInfo retain];
1387
+ [g_selectedScreenInfo release];
1388
+ g_selectedScreenInfo = nil;
1389
+
1390
+ return [result autorelease];
1391
+ }
1392
+
1393
+ bool showScreenRecordingPreview(NSDictionary *screenInfo) {
1394
+ @try {
1395
+ // Clean up any existing preview
1396
+ cleanupRecordingPreview();
1397
+
1398
+ if (!screenInfo) return false;
1399
+
1400
+ // For screen recording preview, we show all OTHER screens as black overlay
1401
+ // and keep the selected screen transparent
1402
+ NSArray *screens = [NSScreen screens];
1403
+ if (!screens || [screens count] == 0) return false;
1404
+
1405
+ int selectedScreenId = [[screenInfo objectForKey:@"id"] intValue];
1406
+
1407
+ // Create overlay for each screen except the selected one
1408
+ for (NSInteger i = 0; i < [screens count]; i++) {
1409
+ if (i == selectedScreenId) continue; // Skip selected screen
1410
+
1411
+ NSScreen *screen = [screens objectAtIndex:i];
1412
+ NSRect screenFrame = [screen frame];
1413
+
1414
+ // Create full-screen black overlay for non-selected screens
1415
+ NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
1416
+ styleMask:NSWindowStyleMaskBorderless
1417
+ backing:NSBackingStoreBuffered
1418
+ defer:NO
1419
+ screen:screen];
1420
+
1421
+ [overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
1422
+ [overlayWindow setOpaque:NO];
1423
+ [overlayWindow setBackgroundColor:[NSColor clearColor]];
1424
+ [overlayWindow setIgnoresMouseEvents:NO];
1425
+ [overlayWindow setAcceptsMouseMovedEvents:YES];
1426
+ [overlayWindow setHasShadow:NO];
1427
+ // no border
1428
+ [overlayWindow setStyleMask:NSWindowStyleMaskBorderless];
1429
+ [overlayWindow setAlphaValue:1.0];
1430
+ [overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
1431
+
1432
+
1433
+ // Force content view to have no borders
1434
+ overlayWindow.contentView.wantsLayer = YES;
1435
+ overlayWindow.contentView.layer.borderWidth = 0.0;
1436
+ overlayWindow.contentView.layer.borderColor = [[NSColor clearColor] CGColor];
1437
+ overlayWindow.contentView.layer.cornerRadius = 0.0;
1438
+ overlayWindow.contentView.layer.masksToBounds = YES;
1439
+
1440
+ // Remove any default window decorations and borders
1441
+ [overlayWindow setTitlebarAppearsTransparent:YES];
1442
+ [overlayWindow setTitleVisibility:NSWindowTitleHidden];
1443
+ [overlayWindow setMovable:NO];
1444
+ [overlayWindow setMovableByWindowBackground:NO];
1445
+
1446
+ // Force remove all borders and decorations
1447
+ [overlayWindow setHasShadow:NO];
1448
+ [overlayWindow setOpaque:NO];
1449
+ [overlayWindow setBackgroundColor:[NSColor clearColor]];
1450
+
1451
+
1452
+ [overlayWindow orderFront:nil];
1453
+ [overlayWindow makeKeyAndOrderFront:nil];
1454
+
1455
+ // Note: NSWindow doesn't have setWantsLayer method, only NSView does
1456
+
1457
+ // Store for cleanup (reuse recording preview window variable)
1458
+ if (!g_recordingPreviewWindow) {
1459
+ g_recordingPreviewWindow = overlayWindow;
1460
+ }
1461
+ }
1462
+
1463
+ NSLog(@"🎬 SCREEN RECORDING PREVIEW: Showing overlay for Screen %d", selectedScreenId);
1464
+
1465
+ return true;
1466
+
1467
+ } @catch (NSException *exception) {
1468
+ NSLog(@"❌ Error showing screen recording preview: %@", exception);
1469
+ return false;
1470
+ }
1471
+ }
1472
+
1473
+ bool hideScreenRecordingPreview() {
1474
+ return hideRecordingPreview(); // Reuse existing function
1475
+ }
1476
+
1477
+ // NAPI Function: Start Window Selection
1478
+ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
1479
+ Napi::Env env = info.Env();
1480
+
1481
+ if (g_isWindowSelecting) {
1482
+ Napi::TypeError::New(env, "Window selection already in progress").ThrowAsJavaScriptException();
1483
+ return env.Null();
1484
+ }
1485
+
1486
+ @try {
1487
+ // Get all windows
1488
+ g_allWindows = [getAllSelectableWindows() retain];
1489
+
1490
+ if (!g_allWindows || [g_allWindows count] == 0) {
1491
+ Napi::Error::New(env, "No selectable windows found").ThrowAsJavaScriptException();
1492
+ return env.Null();
1493
+ }
1494
+
1495
+ // Clear any previous selection
1496
+ if (g_selectedWindowInfo) {
1497
+ [g_selectedWindowInfo release];
1498
+ g_selectedWindowInfo = nil;
1499
+ }
1500
+ if (g_selectedOverlayView) {
1501
+ g_selectedOverlayView.isSelectedWindow = NO;
1502
+ g_selectedOverlayView = nil;
1503
+ }
1504
+ if (g_currentWindowUnderCursor) {
1505
+ [g_currentWindowUnderCursor release];
1506
+ g_currentWindowUnderCursor = nil;
1507
+ }
1508
+
1509
+ // Create overlay window (initially hidden)
1510
+ NSRect initialFrame = NSMakeRect(0, 0, 100, 100);
1511
+ g_overlayWindow = [[NSWindow alloc] initWithContentRect:initialFrame
1512
+ styleMask:NSWindowStyleMaskBorderless
1513
+ backing:NSBackingStoreBuffered
1514
+ defer:NO];
1515
+
1516
+ // Force completely borderless appearance
1517
+ [g_overlayWindow setStyleMask:NSWindowStyleMaskBorderless];
1518
+
1519
+ [g_overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Absolute highest level
1520
+ [g_overlayWindow setOpaque:NO];
1521
+ [g_overlayWindow setBackgroundColor:[NSColor clearColor]];
1522
+ [g_overlayWindow setIgnoresMouseEvents:NO];
1523
+ [g_overlayWindow setAcceptsMouseMovedEvents:YES];
1524
+ [g_overlayWindow setHasShadow:NO];
1525
+ [g_overlayWindow setAlphaValue:1.0];
1526
+ [g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
1527
+
1528
+ // Remove any default window decorations and borders
1529
+ [g_overlayWindow setTitlebarAppearsTransparent:YES];
1530
+ [g_overlayWindow setTitleVisibility:NSWindowTitleHidden];
1531
+ [g_overlayWindow setMovable:NO];
1532
+ [g_overlayWindow setMovableByWindowBackground:NO];
1533
+
1534
+ // Force remove all borders and decorations
1535
+ [g_overlayWindow setHasShadow:NO];
1536
+ [g_overlayWindow setOpaque:NO];
1537
+ [g_overlayWindow setBackgroundColor:[NSColor clearColor]];
1538
+
1539
+ // Create overlay view
1540
+ g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:initialFrame];
1541
+ [g_overlayWindow setContentView:g_overlayView];
1542
+
1543
+ // Initialize overlay view properties
1544
+ [(WindowSelectorOverlayView *)g_overlayView setIsActiveWindow:NO];
1545
+ [(WindowSelectorOverlayView *)g_overlayView setIsSelectedWindow:NO];
1546
+
1547
+ // Note: NSWindow doesn't have setWantsLayer method, only NSView does
1548
+
1549
+ // Force content view to have no borders
1550
+ g_overlayWindow.contentView.wantsLayer = YES;
1551
+ g_overlayWindow.contentView.layer.borderWidth = 0.0;
1552
+ g_overlayWindow.contentView.layer.borderColor = [[NSColor clearColor] CGColor];
1553
+ g_overlayWindow.contentView.layer.cornerRadius = 0.0;
1554
+ g_overlayWindow.contentView.layer.masksToBounds = YES;
1555
+
1556
+ // Additional window styling to ensure no borders or decorations
1557
+ [g_overlayWindow setMovable:NO];
1558
+ [g_overlayWindow setMovableByWindowBackground:NO];
1559
+
1560
+ // Create select button with purple theme
1561
+ g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 60)];
1562
+ [g_selectButton setTitle:@"Start Record"];
1563
+ [g_selectButton setButtonType:NSButtonTypeMomentaryPushIn];
1564
+ [g_selectButton setBordered:NO];
1565
+ [g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1566
+
1567
+ // Modern button styling with new RGB color
1568
+ [g_selectButton setWantsLayer:YES];
1569
+ [g_selectButton.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.95] CGColor]];
1570
+ [g_selectButton.layer setCornerRadius:8.0];
1571
+ [g_selectButton.layer setBorderWidth:0.0];
1572
+
1573
+ // Remove all button borders and decorations
1574
+ [g_selectButton.layer setShadowOpacity:0.0];
1575
+ [g_selectButton.layer setShadowRadius:0.0];
1576
+ [g_selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
1577
+ [g_selectButton.layer setMasksToBounds:YES];
1578
+ [g_selectButton.layer setBorderWidth:0.0];
1579
+ [g_selectButton.layer setBorderColor:[[NSColor clearColor] CGColor]];
1580
+
1581
+ // Clean white text - normal weight
1582
+ NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
1583
+ initWithString:[g_selectButton title]];
1584
+ [titleString addAttribute:NSForegroundColorAttributeName
1585
+ value:[NSColor whiteColor]
1586
+ range:NSMakeRange(0, [titleString length])];
1587
+ [g_selectButton setAttributedTitle:titleString];
1588
+
1589
+ // Create delegate for button action and timer
1590
+ g_delegate = [[WindowSelectorDelegate alloc] init];
1591
+ [g_selectButton setTarget:g_delegate];
1592
+ [g_selectButton setAction:@selector(selectButtonClicked:)];
1593
+
1594
+ // Remove focus ring and other default button behaviors
1595
+ [g_selectButton setFocusRingType:NSFocusRingTypeNone];
1596
+ [g_selectButton setShowsBorderOnlyWhileMouseInside:NO];
1597
+
1598
+ // Add select button directly to window (not view) for proper layering
1599
+ [g_overlayWindow.contentView addSubview:g_selectButton];
1600
+
1601
+ // Create cancel button
1602
+ NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
1603
+ [cancelButton setTitle:@"Cancel"];
1604
+ [cancelButton setButtonType:NSButtonTypeMomentaryPushIn];
1605
+ [cancelButton setBordered:NO];
1606
+ [cancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightRegular]];
1607
+
1608
+ // Modern cancel button styling - darker gray, clean
1609
+ [cancelButton setWantsLayer:YES];
1610
+ [cancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.35 green:0.35 blue:0.4 alpha:0.9] CGColor]];
1611
+ [cancelButton.layer setCornerRadius:8.0];
1612
+ [cancelButton.layer setBorderWidth:0.0];
1613
+
1614
+ // Remove all button borders and decorations
1615
+ [cancelButton.layer setShadowOpacity:0.0];
1616
+ [cancelButton.layer setShadowRadius:0.0];
1617
+ [cancelButton.layer setShadowOffset:NSMakeSize(0, 0)];
1618
+ [cancelButton.layer setMasksToBounds:YES];
1619
+ [cancelButton.layer setBorderWidth:0.0];
1620
+ [cancelButton.layer setBorderColor:[[NSColor clearColor] CGColor]];
1621
+
1622
+ // Clean white text for cancel button
1623
+ NSMutableAttributedString *cancelTitleString = [[NSMutableAttributedString alloc]
1624
+ initWithString:[cancelButton title]];
1625
+ [cancelTitleString addAttribute:NSForegroundColorAttributeName
1626
+ value:[NSColor whiteColor]
1627
+ range:NSMakeRange(0, [cancelTitleString length])];
1628
+ [cancelButton setAttributedTitle:cancelTitleString];
1629
+
1630
+ [cancelButton setTarget:g_delegate];
1631
+ [cancelButton setAction:@selector(cancelButtonClicked:)];
1632
+
1633
+ // Remove focus ring and other default button behaviors
1634
+ [cancelButton setFocusRingType:NSFocusRingTypeNone];
1635
+ [cancelButton setShowsBorderOnlyWhileMouseInside:NO];
1636
+
1637
+ // Add cancel button to window
1638
+ [g_overlayWindow.contentView addSubview:cancelButton];
1639
+
1640
+ // Force all subviews to have no borders, but preserve corner radius for buttons and icons
1641
+ for (NSView *subview in [g_overlayWindow.contentView subviews]) {
1642
+ if ([subview respondsToSelector:@selector(setWantsLayer:)]) {
1643
+ [subview setWantsLayer:YES];
1644
+ if (subview.layer) {
1645
+ subview.layer.borderWidth = 0.0;
1646
+ subview.layer.borderColor = [[NSColor clearColor] CGColor];
1647
+ subview.layer.masksToBounds = YES;
1648
+ subview.layer.shadowOpacity = 0.0;
1649
+ subview.layer.shadowRadius = 0.0;
1650
+ subview.layer.shadowOffset = NSMakeSize(0, 0);
1651
+ }
1652
+ }
1653
+ }
1654
+
1655
+ // Cancel button reference will be found dynamically in positioning code
1656
+
1657
+ // Timer approach doesn't work well with Node.js
1658
+ // Instead, we'll use JavaScript polling via getWindowSelectionStatus
1659
+ // The JS side will call this function repeatedly to trigger overlay updates
1660
+ g_trackingTimer = nil; // No timer for now
1661
+
1662
+ // Add ESC key event monitor to cancel selection
1663
+ g_windowKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
1664
+ handler:^(NSEvent *event) {
1665
+ if ([event keyCode] == 53) { // ESC key
1666
+ NSLog(@"🪟 WINDOW SELECTION: ESC pressed - cancelling selection");
1667
+ cleanupWindowSelector();
1668
+ }
1669
+ }];
1670
+
1671
+ g_isWindowSelecting = true;
1672
+ g_selectedWindowInfo = nil;
1673
+
1674
+ return Napi::Boolean::New(env, true);
1675
+
1676
+ } @catch (NSException *exception) {
1677
+ cleanupWindowSelector();
1678
+ Napi::Error::New(env, [[exception reason] UTF8String]).ThrowAsJavaScriptException();
1679
+ return env.Null();
1680
+ }
1681
+ }
1682
+
1683
+ // NAPI Function: Stop Window Selection
1684
+ Napi::Value StopWindowSelection(const Napi::CallbackInfo& info) {
1685
+ Napi::Env env = info.Env();
1686
+
1687
+ if (!g_isWindowSelecting) {
1688
+ return Napi::Boolean::New(env, false);
1689
+ }
1690
+
1691
+ cleanupWindowSelector();
1692
+ return Napi::Boolean::New(env, true);
1693
+ }
1694
+
1695
+ // NAPI Function: Get Selected Window Info
1696
+ Napi::Value GetSelectedWindowInfo(const Napi::CallbackInfo& info) {
1697
+ Napi::Env env = info.Env();
1698
+
1699
+ if (!g_selectedWindowInfo) {
1700
+ return env.Null();
1701
+ }
1702
+
1703
+ @try {
1704
+ Napi::Object result = Napi::Object::New(env);
1705
+ result.Set("id", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"id"] intValue]));
1706
+ result.Set("title", Napi::String::New(env, [[g_selectedWindowInfo objectForKey:@"title"] UTF8String]));
1707
+ result.Set("appName", Napi::String::New(env, [[g_selectedWindowInfo objectForKey:@"appName"] UTF8String]));
1708
+ // Original CGWindow coordinates
1709
+ result.Set("x", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"x"] intValue]));
1710
+ result.Set("y", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"y"] intValue]));
1711
+ result.Set("width", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"width"] intValue]));
1712
+ result.Set("height", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"height"] intValue]));
1713
+
1714
+ // Add overlay coordinates for direct use in recording
1715
+ // These are the exact coordinates used by the recording preview overlay
1716
+ int windowX = [[g_selectedWindowInfo objectForKey:@"x"] intValue];
1717
+ int windowY = [[g_selectedWindowInfo objectForKey:@"y"] intValue];
1718
+ int windowWidth = [[g_selectedWindowInfo objectForKey:@"width"] intValue];
1719
+ int windowHeight = [[g_selectedWindowInfo objectForKey:@"height"] intValue];
1720
+
1721
+ result.Set("overlayX", Napi::Number::New(env, windowX));
1722
+ result.Set("overlayY", Napi::Number::New(env, windowY));
1723
+ result.Set("overlayWidth", Napi::Number::New(env, windowWidth));
1724
+ result.Set("overlayHeight", Napi::Number::New(env, windowHeight));
1725
+
1726
+ // Determine which screen this window is on
1727
+ int x = [[g_selectedWindowInfo objectForKey:@"x"] intValue];
1728
+ int y = [[g_selectedWindowInfo objectForKey:@"y"] intValue];
1729
+ int width = [[g_selectedWindowInfo objectForKey:@"width"] intValue];
1730
+ int height = [[g_selectedWindowInfo objectForKey:@"height"] intValue];
1731
+
1732
+ NSLog(@"🎯 WINDOW SELECTED: %@ - \"%@\"",
1733
+ [g_selectedWindowInfo objectForKey:@"appName"],
1734
+ [g_selectedWindowInfo objectForKey:@"title"]);
1735
+ NSLog(@" 📊 Details: ID=%@, Pos=(%d,%d), Size=%dx%d",
1736
+ [g_selectedWindowInfo objectForKey:@"id"], x, y, width, height);
1737
+
1738
+ // Get all screens
1739
+ NSArray *screens = [NSScreen screens];
1740
+ NSScreen *windowScreen = nil;
1741
+ NSScreen *mainScreen = [NSScreen mainScreen];
1742
+
1743
+ for (NSScreen *screen in screens) {
1744
+ NSRect screenFrame = [screen frame];
1745
+
1746
+ // Convert window coordinates to screen-relative
1747
+ if (x >= screenFrame.origin.x &&
1748
+ x < screenFrame.origin.x + screenFrame.size.width &&
1749
+ y >= screenFrame.origin.y &&
1750
+ y < screenFrame.origin.y + screenFrame.size.height) {
1751
+ windowScreen = screen;
1752
+ break;
1753
+ }
1754
+ }
1755
+
1756
+ if (!windowScreen) {
1757
+ windowScreen = mainScreen;
1758
+ }
1759
+
1760
+ // Add screen information
1761
+ NSRect screenFrame = [windowScreen frame];
1762
+ result.Set("screenId", Napi::Number::New(env, [[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] ?
1763
+ [[[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue] : 0));
1764
+ result.Set("screenX", Napi::Number::New(env, (int)screenFrame.origin.x));
1765
+ result.Set("screenY", Napi::Number::New(env, (int)screenFrame.origin.y));
1766
+ result.Set("screenWidth", Napi::Number::New(env, (int)screenFrame.size.width));
1767
+ result.Set("screenHeight", Napi::Number::New(env, (int)screenFrame.size.height));
1768
+
1769
+ // Clear selected window info after reading
1770
+ [g_selectedWindowInfo release];
1771
+ g_selectedWindowInfo = nil;
1772
+
1773
+ return result;
1774
+
1775
+ } @catch (NSException *exception) {
1776
+ return env.Null();
1777
+ }
1778
+ }
1779
+
1780
+ // NAPI Function: Bring Window To Front
1781
+ Napi::Value BringWindowToFront(const Napi::CallbackInfo& info) {
1782
+ Napi::Env env = info.Env();
1783
+
1784
+ if (info.Length() < 1) {
1785
+ Napi::TypeError::New(env, "Window ID required").ThrowAsJavaScriptException();
1786
+ return env.Null();
1787
+ }
1788
+
1789
+ int windowId = info[0].As<Napi::Number>().Int32Value();
1790
+
1791
+ @try {
1792
+ bool success = bringWindowToFront(windowId);
1793
+ return Napi::Boolean::New(env, success);
1794
+
1795
+ } @catch (NSException *exception) {
1796
+ return Napi::Boolean::New(env, false);
1797
+ }
1798
+ }
1799
+
1800
+ // NAPI Function: Enable/Disable Auto Bring To Front
1801
+ Napi::Value SetBringToFrontEnabled(const Napi::CallbackInfo& info) {
1802
+ Napi::Env env = info.Env();
1803
+
1804
+ if (info.Length() < 1) {
1805
+ Napi::TypeError::New(env, "Boolean value required").ThrowAsJavaScriptException();
1806
+ return env.Null();
1807
+ }
1808
+
1809
+ bool enabled = info[0].As<Napi::Boolean>();
1810
+ g_bringToFrontEnabled = enabled;
1811
+
1812
+ NSLog(@"🔄 Auto bring-to-front: %s", enabled ? "ENABLED" : "DISABLED");
1813
+
1814
+ return Napi::Boolean::New(env, true);
1815
+ }
1816
+
1817
+ // NAPI Function: Get Window Selection Status
1818
+ Napi::Value GetWindowSelectionStatus(const Napi::CallbackInfo& info) {
1819
+ Napi::Env env = info.Env();
1820
+
1821
+ // Update overlay each time status is requested (JavaScript polling approach)
1822
+ if (g_isWindowSelecting) {
1823
+ updateOverlay();
1824
+ }
1825
+
1826
+ Napi::Object result = Napi::Object::New(env);
1827
+ result.Set("isSelecting", Napi::Boolean::New(env, g_isWindowSelecting));
1828
+ result.Set("hasSelectedWindow", Napi::Boolean::New(env, g_selectedWindowInfo != nil));
1829
+ result.Set("windowCount", Napi::Number::New(env, g_allWindows ? [g_allWindows count] : 0));
1830
+ result.Set("hasOverlay", Napi::Boolean::New(env, g_overlayWindow != nil));
1831
+
1832
+ if (g_currentWindowUnderCursor) {
1833
+ Napi::Object currentWindow = Napi::Object::New(env);
1834
+ currentWindow.Set("id", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"id"] intValue]));
1835
+ currentWindow.Set("title", Napi::String::New(env, [[g_currentWindowUnderCursor objectForKey:@"title"] UTF8String]));
1836
+ currentWindow.Set("appName", Napi::String::New(env, [[g_currentWindowUnderCursor objectForKey:@"appName"] UTF8String]));
1837
+ currentWindow.Set("x", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"x"] intValue]));
1838
+ currentWindow.Set("y", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"y"] intValue]));
1839
+ currentWindow.Set("width", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"width"] intValue]));
1840
+ currentWindow.Set("height", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"height"] intValue]));
1841
+ result.Set("currentWindow", currentWindow);
1842
+ }
1843
+
1844
+ return result;
1845
+ }
1846
+
1847
+ // NAPI Function: Show Recording Preview
1848
+ Napi::Value ShowRecordingPreview(const Napi::CallbackInfo& info) {
1849
+ Napi::Env env = info.Env();
1850
+
1851
+ if (info.Length() < 1) {
1852
+ Napi::TypeError::New(env, "Window info object required").ThrowAsJavaScriptException();
1853
+ return env.Null();
1854
+ }
1855
+
1856
+ if (!info[0].IsObject()) {
1857
+ Napi::TypeError::New(env, "Window info must be an object").ThrowAsJavaScriptException();
1858
+ return env.Null();
1859
+ }
1860
+
1861
+ @try {
1862
+ Napi::Object windowInfoObj = info[0].As<Napi::Object>();
1863
+
1864
+ // Convert NAPI object to NSDictionary
1865
+ NSMutableDictionary *windowInfo = [[NSMutableDictionary alloc] init];
1866
+
1867
+ if (windowInfoObj.Has("id")) {
1868
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
1869
+ }
1870
+ if (windowInfoObj.Has("title")) {
1871
+ [windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("title").As<Napi::String>().Utf8Value().c_str()] forKey:@"title"];
1872
+ }
1873
+ if (windowInfoObj.Has("appName")) {
1874
+ [windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("appName").As<Napi::String>().Utf8Value().c_str()] forKey:@"appName"];
1875
+ }
1876
+ if (windowInfoObj.Has("x")) {
1877
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
1878
+ }
1879
+ if (windowInfoObj.Has("y")) {
1880
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
1881
+ }
1882
+ if (windowInfoObj.Has("width")) {
1883
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
1884
+ }
1885
+ if (windowInfoObj.Has("height")) {
1886
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
1887
+ }
1888
+
1889
+ bool success = showRecordingPreview(windowInfo);
1890
+ [windowInfo release];
1891
+
1892
+ return Napi::Boolean::New(env, success);
1893
+
1894
+ } @catch (NSException *exception) {
1895
+ return Napi::Boolean::New(env, false);
1896
+ }
1897
+ }
1898
+
1899
+ // NAPI Function: Hide Recording Preview
1900
+ Napi::Value HideRecordingPreview(const Napi::CallbackInfo& info) {
1901
+ Napi::Env env = info.Env();
1902
+
1903
+ @try {
1904
+ bool success = hideRecordingPreview();
1905
+ return Napi::Boolean::New(env, success);
1906
+
1907
+ } @catch (NSException *exception) {
1908
+ return Napi::Boolean::New(env, false);
1909
+ }
1910
+ }
1911
+
1912
+ // NAPI Function: Start Screen Selection
1913
+ Napi::Value StartScreenSelection(const Napi::CallbackInfo& info) {
1914
+ Napi::Env env = info.Env();
1915
+
1916
+ @try {
1917
+ bool success = startScreenSelection();
1918
+ return Napi::Boolean::New(env, success);
1919
+
1920
+ } @catch (NSException *exception) {
1921
+ return Napi::Boolean::New(env, false);
1922
+ }
1923
+ }
1924
+
1925
+ // NAPI Function: Stop Screen Selection
1926
+ Napi::Value StopScreenSelection(const Napi::CallbackInfo& info) {
1927
+ Napi::Env env = info.Env();
1928
+
1929
+ @try {
1930
+ bool success = stopScreenSelection();
1931
+ return Napi::Boolean::New(env, success);
1932
+
1933
+ } @catch (NSException *exception) {
1934
+ return Napi::Boolean::New(env, false);
1935
+ }
1936
+ }
1937
+
1938
+ // NAPI Function: Get Selected Screen Info
1939
+ Napi::Value GetSelectedScreenInfo(const Napi::CallbackInfo& info) {
1940
+ Napi::Env env = info.Env();
1941
+
1942
+ @try {
1943
+ NSDictionary *screenInfo = getSelectedScreenInfo();
1944
+ if (!screenInfo) {
1945
+ return env.Null();
1946
+ }
1947
+
1948
+ Napi::Object result = Napi::Object::New(env);
1949
+ result.Set("id", Napi::Number::New(env, [[screenInfo objectForKey:@"id"] intValue]));
1950
+ result.Set("name", Napi::String::New(env, [[screenInfo objectForKey:@"name"] UTF8String]));
1951
+ result.Set("x", Napi::Number::New(env, [[screenInfo objectForKey:@"x"] intValue]));
1952
+ result.Set("y", Napi::Number::New(env, [[screenInfo objectForKey:@"y"] intValue]));
1953
+ result.Set("width", Napi::Number::New(env, [[screenInfo objectForKey:@"width"] intValue]));
1954
+ result.Set("height", Napi::Number::New(env, [[screenInfo objectForKey:@"height"] intValue]));
1955
+ result.Set("resolution", Napi::String::New(env, [[screenInfo objectForKey:@"resolution"] UTF8String]));
1956
+ result.Set("isPrimary", Napi::Boolean::New(env, [[screenInfo objectForKey:@"isPrimary"] boolValue]));
1957
+
1958
+ NSLog(@"🖥️ SCREEN SELECTED: %@ (%@)",
1959
+ [screenInfo objectForKey:@"name"],
1960
+ [screenInfo objectForKey:@"resolution"]);
1961
+
1962
+ return result;
1963
+
1964
+ } @catch (NSException *exception) {
1965
+ return env.Null();
1966
+ }
1967
+ }
1968
+
1969
+ // NAPI Function: Show Screen Recording Preview
1970
+ Napi::Value ShowScreenRecordingPreview(const Napi::CallbackInfo& info) {
1971
+ Napi::Env env = info.Env();
1972
+
1973
+ if (info.Length() < 1) {
1974
+ Napi::TypeError::New(env, "Screen info object required").ThrowAsJavaScriptException();
1975
+ return env.Null();
1976
+ }
1977
+
1978
+ if (!info[0].IsObject()) {
1979
+ Napi::TypeError::New(env, "Screen info must be an object").ThrowAsJavaScriptException();
1980
+ return env.Null();
1981
+ }
1982
+
1983
+ @try {
1984
+ Napi::Object screenInfoObj = info[0].As<Napi::Object>();
1985
+
1986
+ // Convert NAPI object to NSDictionary
1987
+ NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
1988
+
1989
+ if (screenInfoObj.Has("id")) {
1990
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
1991
+ }
1992
+ if (screenInfoObj.Has("name")) {
1993
+ [screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("name").As<Napi::String>().Utf8Value().c_str()] forKey:@"name"];
1994
+ }
1995
+ if (screenInfoObj.Has("resolution")) {
1996
+ [screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("resolution").As<Napi::String>().Utf8Value().c_str()] forKey:@"resolution"];
1997
+ }
1998
+ if (screenInfoObj.Has("x")) {
1999
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
2000
+ }
2001
+ if (screenInfoObj.Has("y")) {
2002
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
2003
+ }
2004
+ if (screenInfoObj.Has("width")) {
2005
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
2006
+ }
2007
+ if (screenInfoObj.Has("height")) {
2008
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
2009
+ }
2010
+
2011
+ bool success = showScreenRecordingPreview(screenInfo);
2012
+ [screenInfo release];
2013
+
2014
+ return Napi::Boolean::New(env, success);
2015
+
2016
+ } @catch (NSException *exception) {
2017
+ return Napi::Boolean::New(env, false);
2018
+ }
2019
+ }
2020
+
2021
+ // NAPI Function: Hide Screen Recording Preview
2022
+ Napi::Value HideScreenRecordingPreview(const Napi::CallbackInfo& info) {
2023
+ Napi::Env env = info.Env();
2024
+
2025
+ @try {
2026
+ bool success = hideScreenRecordingPreview();
2027
+ return Napi::Boolean::New(env, success);
2028
+
2029
+ } @catch (NSException *exception) {
2030
+ return Napi::Boolean::New(env, false);
2031
+ }
2032
+ }
2033
+
2034
+ // Export functions
2035
+ Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
2036
+ exports.Set("startWindowSelection", Napi::Function::New(env, StartWindowSelection));
2037
+ exports.Set("stopWindowSelection", Napi::Function::New(env, StopWindowSelection));
2038
+ exports.Set("getSelectedWindowInfo", Napi::Function::New(env, GetSelectedWindowInfo));
2039
+ exports.Set("getWindowSelectionStatus", Napi::Function::New(env, GetWindowSelectionStatus));
2040
+ exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
2041
+ exports.Set("setBringToFrontEnabled", Napi::Function::New(env, SetBringToFrontEnabled));
2042
+ exports.Set("showRecordingPreview", Napi::Function::New(env, ShowRecordingPreview));
2043
+ exports.Set("hideRecordingPreview", Napi::Function::New(env, HideRecordingPreview));
2044
+ exports.Set("startScreenSelection", Napi::Function::New(env, StartScreenSelection));
2045
+ exports.Set("stopScreenSelection", Napi::Function::New(env, StopScreenSelection));
2046
+ exports.Set("getSelectedScreenInfo", Napi::Function::New(env, GetSelectedScreenInfo));
2047
+ exports.Set("showScreenRecordingPreview", Napi::Function::New(env, ShowScreenRecordingPreview));
2048
+ exports.Set("hideScreenRecordingPreview", Napi::Function::New(env, HideScreenRecordingPreview));
2049
+
2050
+ return exports;
2051
+ }