Skip to content

Commit ee1369f

Browse files
authored
Merge pull request #103 from RomuloGatto/master
Add support for activating tabs with window focus
2 parents 100b4c3 + 955ac36 commit ee1369f

File tree

5 files changed

+152
-12
lines changed

5 files changed

+152
-12
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ More details [here](https://www.chromium.org/developers/applescript). Thanks to
8484
chrome-cli forward (Navigate forward in active tab)
8585
chrome-cli forward -t <id> (Navigate forward in specific tab)
8686
chrome-cli activate -t <id> (Activate specific tab)
87+
chrome-cli activate -t <id> --focus (Activate tab and bring its window to the front)
8788
chrome-cli activate -t <windowId>:<id> (Activate specific tab in a specific window — useful with multiple profiles)
89+
chrome-cli activate -t <windowId>:<id> --focus (Activate specific tab and bring that window to the front)
8890
chrome-cli presentation (Enter presentation mode with the active tab)
8991
chrome-cli presentation -t <id> (Enter presentation mode with a specific tab)
9092
chrome-cli presentation exit (Exit presentation mode)

chrome-cli.xcodeproj/project.pbxproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
2EB4623518A6690800211D5F /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EB4623418A6690800211D5F /* ScriptingBridge.framework */; };
1616
2EBFE41818A6822B008EC2DF /* Argonaut.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EBFE41718A6822B008EC2DF /* Argonaut.m */; };
1717
2EBFE41B18A68613008EC2DF /* App.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EBFE41A18A68613008EC2DF /* App.m */; };
18+
A10000032025082300000001 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10000022025082300000001 /* AppKit.framework */; };
19+
A10000012025082300000001 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10000002025082300000001 /* ApplicationServices.framework */; };
1820
/* End PBXBuildFile section */
1921

2022
/* Begin PBXCopyFilesBuildPhase section */
@@ -46,13 +48,17 @@
4648
2EBFE41718A6822B008EC2DF /* Argonaut.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Argonaut.m; sourceTree = "<group>"; };
4749
2EBFE41918A68613008EC2DF /* App.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = App.h; sourceTree = "<group>"; };
4850
2EBFE41A18A68613008EC2DF /* App.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = App.m; sourceTree = "<group>"; };
51+
A10000002025082300000001 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; };
52+
A10000022025082300000001 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
4953
/* End PBXFileReference section */
5054

5155
/* Begin PBXFrameworksBuildPhase section */
5256
2EB4622018A668F700211D5F /* Frameworks */ = {
5357
isa = PBXFrameworksBuildPhase;
5458
buildActionMask = 2147483647;
5559
files = (
60+
A10000032025082300000001 /* AppKit.framework in Frameworks */,
61+
A10000012025082300000001 /* ApplicationServices.framework in Frameworks */,
5662
2EB4623518A6690800211D5F /* ScriptingBridge.framework in Frameworks */,
5763
2EB4622718A668F700211D5F /* Foundation.framework in Frameworks */,
5864
);
@@ -94,11 +100,13 @@
94100
2EB4622518A668F700211D5F /* Frameworks */ = {
95101
isa = PBXGroup;
96102
children = (
103+
A10000022025082300000001 /* AppKit.framework */,
104+
A10000002025082300000001 /* ApplicationServices.framework */,
97105
2EB4623418A6690800211D5F /* ScriptingBridge.framework */,
98106
2EB4622618A668F700211D5F /* Foundation.framework */,
99107
);
100108
name = Frameworks;
101-
sourceTree = "<group>";
109+
sourceTree = SDKROOT;
102110
};
103111
2EB4622818A668F700211D5F /* chrome-cli */ = {
104112
isa = PBXGroup;

chrome-cli/App.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ typedef enum {
4343
- (void)goForwardActiveTab:(Arguments *)args;
4444
- (void)goForwardInTab:(Arguments *)args;
4545
- (void)activateTab:(Arguments *)args;
46+
- (void)activateTabAndFocus:(Arguments *)args;
4647
- (void)printActiveWindowSize:(Arguments *)args;
4748
- (void)printWindowSize:(Arguments *)args;
4849
- (void)setActiveWindowSize:(Arguments *)args;

chrome-cli/App.m

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
#import "App.h"
1010
#import "chrome.h"
11+
#import <ApplicationServices/ApplicationServices.h>
1112

1213

1314
static NSInteger const kMaxLaunchTimeInSeconds = 15;
14-
static NSString * const kVersion = @"1.10.3";
15+
static NSString * const kVersion = @"1.11.0";
1516
static NSString * const kJsPrintSource = @"(function() { return document.getElementsByTagName('html')[0].outerHTML })();";
1617

1718

@@ -411,34 +412,41 @@ - (void)goForwardInTab:(Arguments *)args {
411412
}
412413

413414
- (void)activateTab:(Arguments *)args {
414-
// Support two forms:
415+
// Support legacy and explicit forms:
415416
// 1) <tabId>
416-
// 2) <windowId>:<tabId>
417+
// 2) <windowId>:<tabId> (prefer window match, fallback to scanning)
417418
NSString *rawId = [args asString:@"id"];
418419
if (!rawId || rawId.length == 0) {
419420
return;
420421
}
421422

422423
NSRange sep = [rawId rangeOfString:@":"];
423424
if (sep.location != NSNotFound) {
424-
// window-specific activation
425425
NSString *winStr = [rawId substringToIndex:sep.location];
426426
NSString *tabStr = [rawId substringFromIndex:sep.location + 1];
427427

428428
NSInteger windowId = [winStr integerValue];
429429
NSInteger tabId = [tabStr integerValue];
430-
430+
BOOL done = NO;
431431
chromeWindow *window = [self findWindow:windowId];
432432
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+
}
434440
}
435441

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+
}
439449
}
440-
441-
[self setTabActive:tab inWindow:window];
442450
return;
443451
}
444452

@@ -449,6 +457,30 @@ - (void)activateTab:(Arguments *)args {
449457
[self setTabActive:tab inWindow:window];
450458
}
451459

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+
452484
- (void)printActiveWindowSize:(Arguments *)args {
453485
chromeWindow *window = [self activeWindow];
454486
CGSize size = window.bounds.size;
@@ -683,6 +715,74 @@ - (void)printVersion:(Arguments *)args {
683715

684716
#pragma mark Helper functions
685717

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+
686786
- (chromeWindow *)activeWindow {
687787
// The first object seems to alway be the active window
688788
chromeWindow *window = self.chrome.windows.firstObject;
@@ -763,6 +863,34 @@ - (NSInteger)findTabIndex:(chromeTab *)tab inWindow:(chromeWindow *)window {
763863
return NSNotFound;
764864
}
765865

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+
766894
- (void)printInfo:(chromeTab *)tab {
767895
if (!tab) {
768896
return;

chrome-cli/main.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ int main(int argc, const char * argv[])
6868
[argonaut add:@"forward -t <id>" target:app action:@selector(goForwardInTab:) description:@"Navigate forward in specific tab"];
6969

7070
[argonaut add:@"activate -t <id>" target:app action:@selector(activateTab:) description:@"Activate specific tab"];
71+
[argonaut add:@"activate -t <id> --focus" target:app action:@selector(activateTabAndFocus:) description:@"Activate tab and bring its window to front"];
7172

7273
[argonaut add:@"size" target:app action:@selector(printActiveWindowSize:) description:@"Print size of active window"];
7374
[argonaut add:@"size -w <id>" target:app action:@selector(printWindowSize:) description:@"Print size of specific window"];

0 commit comments

Comments
 (0)