Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ More details [here](https://www.chromium.org/developers/applescript). Thanks to
chrome-cli forward (Navigate forward in active tab)
chrome-cli forward -t <id> (Navigate forward in specific tab)
chrome-cli activate -t <id> (Activate specific tab)
chrome-cli activate -t <id> --focus (Activate tab and bring its window to the front)
chrome-cli activate -t <windowId>:<id> (Activate specific tab in a specific window — useful with multiple profiles)
chrome-cli activate -t <windowId>:<id> --focus (Activate specific tab and bring that window to the front)
chrome-cli presentation (Enter presentation mode with the active tab)
chrome-cli presentation -t <id> (Enter presentation mode with a specific tab)
chrome-cli presentation exit (Exit presentation mode)
Expand Down
10 changes: 9 additions & 1 deletion chrome-cli.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
2EB4623518A6690800211D5F /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EB4623418A6690800211D5F /* ScriptingBridge.framework */; };
2EBFE41818A6822B008EC2DF /* Argonaut.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EBFE41718A6822B008EC2DF /* Argonaut.m */; };
2EBFE41B18A68613008EC2DF /* App.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EBFE41A18A68613008EC2DF /* App.m */; };
A10000032025082300000001 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10000022025082300000001 /* AppKit.framework */; };
A10000012025082300000001 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10000002025082300000001 /* ApplicationServices.framework */; };
/* End PBXBuildFile section */

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

/* Begin PBXFrameworksBuildPhase section */
2EB4622018A668F700211D5F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A10000032025082300000001 /* AppKit.framework in Frameworks */,
A10000012025082300000001 /* ApplicationServices.framework in Frameworks */,
2EB4623518A6690800211D5F /* ScriptingBridge.framework in Frameworks */,
2EB4622718A668F700211D5F /* Foundation.framework in Frameworks */,
);
Expand Down Expand Up @@ -94,11 +100,13 @@
2EB4622518A668F700211D5F /* Frameworks */ = {
isa = PBXGroup;
children = (
A10000022025082300000001 /* AppKit.framework */,
A10000002025082300000001 /* ApplicationServices.framework */,
2EB4623418A6690800211D5F /* ScriptingBridge.framework */,
2EB4622618A668F700211D5F /* Foundation.framework */,
);
name = Frameworks;
sourceTree = "<group>";
sourceTree = SDKROOT;
};
2EB4622818A668F700211D5F /* chrome-cli */ = {
isa = PBXGroup;
Expand Down
1 change: 1 addition & 0 deletions chrome-cli/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ typedef enum {
- (void)goForwardActiveTab:(Arguments *)args;
- (void)goForwardInTab:(Arguments *)args;
- (void)activateTab:(Arguments *)args;
- (void)activateTabAndFocus:(Arguments *)args;
- (void)printActiveWindowSize:(Arguments *)args;
- (void)printWindowSize:(Arguments *)args;
- (void)setActiveWindowSize:(Arguments *)args;
Expand Down
150 changes: 139 additions & 11 deletions chrome-cli/App.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

#import "App.h"
#import "chrome.h"
#import <ApplicationServices/ApplicationServices.h>


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


Expand Down Expand Up @@ -411,34 +412,41 @@ - (void)goForwardInTab:(Arguments *)args {
}

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

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

NSInteger windowId = [winStr integerValue];
NSInteger tabId = [tabStr integerValue];

BOOL done = NO;
chromeWindow *window = [self findWindow:windowId];
if (!window) {
return;
} else {
chromeTab *tabInWindow = [self findTab:tabId inWindow:window];
if (tabInWindow) {
[self setTabActive:tabInWindow inWindow:window];
done = YES;
} else {
}
}

chromeTab *tab = [self findTab:tabId inWindow:window];
if (!tab) {
return;
if (!done) {
chromeTab *tabAny = [self findTab:tabId];
chromeWindow *winAny = tabAny ? [self findWindowWithTab:tabAny] : nil;
if (tabAny && winAny) {
[self setTabActive:tabAny inWindow:winAny];
} else {
}
}
Comment on lines +438 to 449
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty else blocks at lines 439 and 448 make the code unclear. Consider removing these empty blocks or adding comments explaining why no action is needed in these cases.

Copilot uses AI. Check for mistakes.

[self setTabActive:tab inWindow:window];
return;
}

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

// Same parsing as activateTab:, but ensures the window is brought to the foreground
- (void)activateTabAndFocus:(Arguments *)args {
[self activateTab:args];

NSString *rawId = [args asString:@"id"];
if (!rawId || rawId.length == 0) {
return;
}

// Determine tabId and attempt to focus the window containing it with retries
NSInteger tabId = 0;
NSRange sep = [rawId rangeOfString:@":"];
if (sep.location != NSNotFound) {
NSString *tabStr = [rawId substringFromIndex:sep.location + 1];
tabId = [tabStr integerValue];
} else {
tabId = [rawId integerValue];
}
Comment on lines +461 to +477
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tab ID parsing logic is duplicated from the activateTab: method. Consider extracting this into a helper method to avoid code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.

BOOL focused = [self focusWindowContainingTabId:tabId maxAttempts:12 sleepMs:150];
if (!focused) {
}
Comment on lines +480 to +481
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty conditional block serves no purpose. Consider removing this if statement or adding appropriate error handling/logging.

Suggested change
if (!focused) {
}
// If not focused, no action is taken.

Copilot uses AI. Check for mistakes.
}

- (void)printActiveWindowSize:(Arguments *)args {
chromeWindow *window = [self activeWindow];
CGSize size = window.bounds.size;
Expand Down Expand Up @@ -683,6 +715,74 @@ - (void)printVersion:(Arguments *)args {

#pragma mark Helper functions

// Best-effort focusing: Scripting Bridge bring-to-front plus Accessibility raise to switch Spaces/fullscreen
- (void)bringWindowToFrontBestEffort:(chromeWindow *)window targetTabTitle:(NSString *)tabTitleOrNil {
if (!window) { return; }

// First try via Scripting Bridge
@try {
window.minimized = NO;
window.visible = YES;
window.index = 1;
} @catch (NSException *exception) {
// Ignore SB quirks
}
[self.chrome activate];

// Then try Accessibility-based raise to ensure macOS switches to the Space containing this window
// Request trust (this will show the system prompt once if not granted)
NSDictionary *promptOpts = @{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @NO};
Boolean trusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)promptOpts);
if (!trusted) {
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)@{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @YES});
return;
}

// Find the running app for our bundle id
pid_t pid = 0;
NSArray<NSRunningApplication *> *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self->bundleIdentifier];
if (apps.count == 0) { return; }
// Prefer the frontmost or first
for (NSRunningApplication *app in apps) {
if (app.active) { pid = app.processIdentifier; break; }
}
if (pid == 0) { pid = apps.firstObject.processIdentifier; }

AXUIElementRef appRef = AXUIElementCreateApplication(pid);
if (!appRef) { return; }

CFTypeRef windowsValue = NULL;
if (AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute, &windowsValue) != kAXErrorSuccess || !windowsValue) {
CFRelease(appRef);
return;
}

NSArray *axWindows = CFBridgingRelease(windowsValue);
NSString *targetTitle = tabTitleOrNil ?: (window.name ?: @"");
for (id w in axWindows) {
AXUIElementRef winRef = (__bridge AXUIElementRef)w;
CFTypeRef titleValue = NULL;
if (AXUIElementCopyAttributeValue(winRef, kAXTitleAttribute, &titleValue) == kAXErrorSuccess && titleValue) {
NSString *title = CFBridgingRelease(titleValue);
BOOL matches = NO;
if (title && targetTitle.length > 0) {
if ([title isEqualToString:targetTitle]) {
matches = YES;
} else {
NSRange r = [title rangeOfString:targetTitle options:NSCaseInsensitiveSearch];
matches = (r.location != NSNotFound);
}
}
if (matches) {
AXUIElementPerformAction(winRef, kAXRaiseAction);
break;
}
}
}

CFRelease(appRef);
}

- (chromeWindow *)activeWindow {
// The first object seems to alway be the active window
chromeWindow *window = self.chrome.windows.firstObject;
Expand Down Expand Up @@ -763,6 +863,34 @@ - (NSInteger)findTabIndex:(chromeTab *)tab inWindow:(chromeWindow *)window {
return NSNotFound;
}

// Try to bring to front the window containing tabId, with retries (helps when Chrome is fullscreen across Spaces)
- (BOOL)focusWindowContainingTabId:(NSInteger)tabId maxAttempts:(int)attempts sleepMs:(int)ms {
for (int attempt = 1; attempt <= attempts; attempt++) {
NSArray *windowsSnapshot = [NSArray arrayWithArray:self.chrome.windows];
for (chromeWindow *w in windowsSnapshot) {
NSArray *tabsSnapshot = [NSArray arrayWithArray:w.tabs];
for (chromeTab *t in tabsSnapshot) {
if (t.id && [t.id integerValue] == tabId) {
// Ensure the tab is the active one in that window
[self setTabActive:t inWindow:w];
// Bring to front using both SB and AX with the active tab title (more reliable match)
[self bringWindowToFrontBestEffort:w targetTabTitle:(t.title ?: nil)];

// Give macOS a moment and verify
usleep((useconds_t)(ms * 1000));
chromeWindow *aw = [self activeWindow];
if (aw && [aw.id isEqualToString:w.id]) {
return YES;
} else {
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty else block serves no purpose. Consider removing this empty block or adding a comment explaining why no action is needed when the window focus verification fails.

Suggested change
} else {

Copilot uses AI. Check for mistakes.
}
}
}
}
usleep((useconds_t)(ms * 1000));
}
return NO;
}

- (void)printInfo:(chromeTab *)tab {
if (!tab) {
return;
Expand Down
1 change: 1 addition & 0 deletions chrome-cli/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ int main(int argc, const char * argv[])
[argonaut add:@"forward -t <id>" target:app action:@selector(goForwardInTab:) description:@"Navigate forward in specific tab"];

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

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