8
8
9
9
#import " App.h"
10
10
#import " chrome.h"
11
+ #import < ApplicationServices/ApplicationServices.h>
11
12
12
13
13
14
static NSInteger const kMaxLaunchTimeInSeconds = 15 ;
14
- static NSString * const kVersion = @" 1.10.3 " ;
15
+ static NSString * const kVersion = @" 1.11.0 " ;
15
16
static NSString * const kJsPrintSource = @" (function() { return document.getElementsByTagName('html')[0].outerHTML })();" ;
16
17
17
18
@@ -411,34 +412,41 @@ - (void)goForwardInTab:(Arguments *)args {
411
412
}
412
413
413
414
- (void )activateTab : (Arguments *)args {
414
- // Support two forms:
415
+ // Support legacy and explicit forms:
415
416
// 1) <tabId>
416
- // 2) <windowId>:<tabId>
417
+ // 2) <windowId>:<tabId> (prefer window match, fallback to scanning)
417
418
NSString *rawId = [args asString: @" id" ];
418
419
if (!rawId || rawId.length == 0 ) {
419
420
return ;
420
421
}
421
422
422
423
NSRange sep = [rawId rangeOfString: @" :" ];
423
424
if (sep.location != NSNotFound ) {
424
- // window-specific activation
425
425
NSString *winStr = [rawId substringToIndex: sep.location];
426
426
NSString *tabStr = [rawId substringFromIndex: sep.location + 1 ];
427
427
428
428
NSInteger windowId = [winStr integerValue ];
429
429
NSInteger tabId = [tabStr integerValue ];
430
-
430
+ BOOL done = NO ;
431
431
chromeWindow *window = [self findWindow: windowId];
432
432
if (!window) {
433
- return ;
433
+ } else {
434
+ chromeTab *tabInWindow = [self findTab: tabId inWindow: window];
435
+ if (tabInWindow) {
436
+ [self setTabActive: tabInWindow inWindow: window];
437
+ done = YES ;
438
+ } else {
439
+ }
434
440
}
435
441
436
- chromeTab *tab = [self findTab: tabId inWindow: window];
437
- if (!tab) {
438
- return ;
442
+ if (!done) {
443
+ chromeTab *tabAny = [self findTab: tabId];
444
+ chromeWindow *winAny = tabAny ? [self findWindowWithTab: tabAny] : nil ;
445
+ if (tabAny && winAny) {
446
+ [self setTabActive: tabAny inWindow: winAny];
447
+ } else {
448
+ }
439
449
}
440
-
441
- [self setTabActive: tab inWindow: window];
442
450
return ;
443
451
}
444
452
@@ -449,6 +457,30 @@ - (void)activateTab:(Arguments *)args {
449
457
[self setTabActive: tab inWindow: window];
450
458
}
451
459
460
+ // Same parsing as activateTab:, but ensures the window is brought to the foreground
461
+ - (void )activateTabAndFocus : (Arguments *)args {
462
+ [self activateTab: args];
463
+
464
+ NSString *rawId = [args asString: @" id" ];
465
+ if (!rawId || rawId.length == 0 ) {
466
+ return ;
467
+ }
468
+
469
+ // Determine tabId and attempt to focus the window containing it with retries
470
+ NSInteger tabId = 0 ;
471
+ NSRange sep = [rawId rangeOfString: @" :" ];
472
+ if (sep.location != NSNotFound ) {
473
+ NSString *tabStr = [rawId substringFromIndex: sep.location + 1 ];
474
+ tabId = [tabStr integerValue ];
475
+ } else {
476
+ tabId = [rawId integerValue ];
477
+ }
478
+
479
+ BOOL focused = [self focusWindowContainingTabId: tabId maxAttempts: 12 sleepMs: 150 ];
480
+ if (!focused) {
481
+ }
482
+ }
483
+
452
484
- (void )printActiveWindowSize : (Arguments *)args {
453
485
chromeWindow *window = [self activeWindow ];
454
486
CGSize size = window.bounds .size ;
@@ -683,6 +715,74 @@ - (void)printVersion:(Arguments *)args {
683
715
684
716
#pragma mark Helper functions
685
717
718
+ // Best-effort focusing: Scripting Bridge bring-to-front plus Accessibility raise to switch Spaces/fullscreen
719
+ - (void )bringWindowToFrontBestEffort : (chromeWindow *)window targetTabTitle : (NSString *)tabTitleOrNil {
720
+ if (!window) { return ; }
721
+
722
+ // First try via Scripting Bridge
723
+ @try {
724
+ window.minimized = NO ;
725
+ window.visible = YES ;
726
+ window.index = 1 ;
727
+ } @catch (NSException *exception) {
728
+ // Ignore SB quirks
729
+ }
730
+ [self .chrome activate ];
731
+
732
+ // Then try Accessibility-based raise to ensure macOS switches to the Space containing this window
733
+ // Request trust (this will show the system prompt once if not granted)
734
+ NSDictionary *promptOpts = @{(__bridge NSString *)kAXTrustedCheckOptionPrompt : @NO };
735
+ Boolean trusted = AXIsProcessTrustedWithOptions ((__bridge CFDictionaryRef)promptOpts);
736
+ if (!trusted) {
737
+ AXIsProcessTrustedWithOptions ((__bridge CFDictionaryRef)@{(__bridge NSString *)kAXTrustedCheckOptionPrompt : @YES });
738
+ return ;
739
+ }
740
+
741
+ // Find the running app for our bundle id
742
+ pid_t pid = 0 ;
743
+ NSArray <NSRunningApplication *> *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier: self ->bundleIdentifier];
744
+ if (apps.count == 0 ) { return ; }
745
+ // Prefer the frontmost or first
746
+ for (NSRunningApplication *app in apps) {
747
+ if (app.active ) { pid = app.processIdentifier ; break ; }
748
+ }
749
+ if (pid == 0 ) { pid = apps.firstObject .processIdentifier ; }
750
+
751
+ AXUIElementRef appRef = AXUIElementCreateApplication (pid);
752
+ if (!appRef) { return ; }
753
+
754
+ CFTypeRef windowsValue = NULL ;
755
+ if (AXUIElementCopyAttributeValue (appRef, kAXWindowsAttribute , &windowsValue) != kAXErrorSuccess || !windowsValue) {
756
+ CFRelease (appRef);
757
+ return ;
758
+ }
759
+
760
+ NSArray *axWindows = CFBridgingRelease (windowsValue);
761
+ NSString *targetTitle = tabTitleOrNil ?: (window.name ?: @" " );
762
+ for (id w in axWindows) {
763
+ AXUIElementRef winRef = (__bridge AXUIElementRef)w;
764
+ CFTypeRef titleValue = NULL ;
765
+ if (AXUIElementCopyAttributeValue (winRef, kAXTitleAttribute , &titleValue) == kAXErrorSuccess && titleValue) {
766
+ NSString *title = CFBridgingRelease (titleValue);
767
+ BOOL matches = NO ;
768
+ if (title && targetTitle.length > 0 ) {
769
+ if ([title isEqualToString: targetTitle]) {
770
+ matches = YES ;
771
+ } else {
772
+ NSRange r = [title rangeOfString: targetTitle options: NSCaseInsensitiveSearch];
773
+ matches = (r.location != NSNotFound );
774
+ }
775
+ }
776
+ if (matches) {
777
+ AXUIElementPerformAction (winRef, kAXRaiseAction );
778
+ break ;
779
+ }
780
+ }
781
+ }
782
+
783
+ CFRelease (appRef);
784
+ }
785
+
686
786
- (chromeWindow *)activeWindow {
687
787
// The first object seems to alway be the active window
688
788
chromeWindow *window = self.chrome .windows .firstObject ;
@@ -763,6 +863,34 @@ - (NSInteger)findTabIndex:(chromeTab *)tab inWindow:(chromeWindow *)window {
763
863
return NSNotFound ;
764
864
}
765
865
866
+ // Try to bring to front the window containing tabId, with retries (helps when Chrome is fullscreen across Spaces)
867
+ - (BOOL )focusWindowContainingTabId : (NSInteger )tabId maxAttempts : (int )attempts sleepMs : (int )ms {
868
+ for (int attempt = 1 ; attempt <= attempts; attempt++) {
869
+ NSArray *windowsSnapshot = [NSArray arrayWithArray: self .chrome.windows];
870
+ for (chromeWindow *w in windowsSnapshot) {
871
+ NSArray *tabsSnapshot = [NSArray arrayWithArray: w.tabs];
872
+ for (chromeTab *t in tabsSnapshot) {
873
+ if (t.id && [t.id integerValue ] == tabId) {
874
+ // Ensure the tab is the active one in that window
875
+ [self setTabActive: t inWindow: w];
876
+ // Bring to front using both SB and AX with the active tab title (more reliable match)
877
+ [self bringWindowToFrontBestEffort: w targetTabTitle: (t.title ?: nil )];
878
+
879
+ // Give macOS a moment and verify
880
+ usleep ((useconds_t )(ms * 1000 ));
881
+ chromeWindow *aw = [self activeWindow ];
882
+ if (aw && [aw.id isEqualToString: w.id ]) {
883
+ return YES ;
884
+ } else {
885
+ }
886
+ }
887
+ }
888
+ }
889
+ usleep ((useconds_t )(ms * 1000 ));
890
+ }
891
+ return NO ;
892
+ }
893
+
766
894
- (void )printInfo : (chromeTab *)tab {
767
895
if (!tab) {
768
896
return ;
0 commit comments