Skip to content

Instantly share code, notes, and snippets.

@knu
Last active December 10, 2024 15:23
Show Gist options
  • Save knu/80c703b06bf048251da7200eb26c89e6 to your computer and use it in GitHub Desktop.
Save knu/80c703b06bf048251da7200eb26c89e6 to your computer and use it in GitHub Desktop.
JXA script to open a URL in a specified Chromium-based browser
#!/usr/bin/osascript -l JavaScript
// -*- javascript -*-
//
// open-chromium: Open a URL in a Chromium-based browser in a specified profile
//
// usage: open-chromium [(-a | --application) <app>] [(-p | --profile) <profile>] <url>
//
// <app>: "Chromium" (default), "Google Chrome", "Microsoft Edge", etc.
// <profile>: "Default", "Profile 1", etc.
//
// If the URL is already opened in a tab, it is activated.
function first(collection) {
return collection.length > 0 ? collection[0] : null;
}
function activateWindowById(app, id) {
const window = app.windows.byId(id);
if (!window) {
console.log("window is gone!");
return false;
}
if (window.index() !== 1) window.index = 1;
return true;
}
function activateTabById(window, id) {
Array.from(window.tabs()).forEach((tab, i) => {
if (tab.id() === id) {
window.activeTabIndex = i + 1;
return true;
}
});
}
function findTabByURL(window, url) {
return Array.from(window.tabs()).find(
url instanceof RegExp ? (tab) => tab.url().match(url) : (tab) => tab.url() === url
);
}
function selectProfile(app, profile) {
const system = Application("System Events");
const browserAppProc = system.applicationProcesses.byName(app.name());
const menuBar = browserAppProc.menuBars[0];
const menu = first(
menuBar.menus.whose(
{
_or: [
{ name: "Profiles" },
// other languages
{ name: "プロファイル" },
],
},
{ considering: "case" },
),
);
const menuName = menu.name();
const menuItem = first(
menu.menuItems.whose(
{
_or: [
// Chrome, Chromium
{ name: { _endsWith: ` (${profile})` } },
// Microsoft Edge
{ name: profile },
],
},
{ considering: "case" },
),
);
if (!menuItem) return false;
const menuItemName = menuItem.name();
menuItem.click();
const isSelected = () =>
menuBar.menus
.byName(menuName)
.menuItems.byName(menuItemName)
.attributes.byName("AXMenuItemMarkChar")
.value() === "✓";
if (isSelected()) return true;
for (const id of Array.from(app.windows())
.slice(1)
.map((window) => window.id())) {
activateWindowById(app, id);
if (isSelected()) return true;
}
return false;
}
function openUrlInWindow(app, window, url, urlRegExp) {
const tab = findTabByURL(window, urlRegExp ?? url);
if (tab) {
activateTabById(window, tab.id());
} else {
const tab = first(
window.tabs.whose(
{
_or: [
// Chrome, Chromium
{ url: "chrome://newtab/" },
// Microsoft Edge
{ url: "edge://newtab/" },
],
},
{ considering: "case" },
),
);
if (tab) {
tab.url = url;
} else {
window.tabs.push(app.Tab({ url }));
}
}
activateWindowById(app, window.id());
}
function getWindowInMode(app, mode) {
return Array.from(app.windows()).find(
(window) => window.mode() === mode
) || app.Window({ mode }).make()
}
function openUrl(app, url, { urlRegExp, profile, mode }) {
if (profile) {
if (!selectProfile(app, profile)) console.log("profile not found");
if (!url) {
activateWindowById(app, getWindowInMode(app, mode).id());
return;
}
openUrlInWindow(app, getWindowInMode(app, mode), url, urlRegExp);
} else {
if (!url) {
activateWindowById(app, getWindowInMode(app, mode).id());
return;
}
Array.from(app.windows()).find((window, i) => {
if (window.mode() !== mode) return false;
const tab = findTabByURL(window, urlRegExp ?? url);
if (tab) {
activateWindowById(app, window.id());
activateTabById(window, tab.id());
return true;
}
return false;
}) || openUrlInWindow(app, getWindowInMode(app, mode), url, urlRegExp);
}
}
function urlPatternToRegExp(pattern) {
const re = pattern.replace(/\\(.)|(\*\*)|(\*)|([$()+.?[\]^{|}])|(.)/g, (_, esc, dstar, star, meta, safe) => {
if (esc) return esc;
if (dstar) return ".*";
if (star) return "[^/]*(?:\\?.*)?";
if (meta) return `\\${meta}`;
return safe;
});
return new RegExp(`^${re}$`);
}
function run(argv) {
let appName = "Chromium",
profile,
urlRegExp,
mode = "normal",
urls = [];
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
let m;
if ((m = /^(?:-p(.+)?|--profile(?:=(.*))?)$/.exec(arg))) {
profile = m[1] ?? m[2] ?? argv[++i];
} else if ((m = /^(?:-a(.+)?|--application(?:=(.*))?)$/.exec(arg))) {
appName = m[1] ?? m[2] ?? argv[++i];
} else if ((m = /^(?:-m(.+)?|--match(?:=(.*))?)$/.exec(arg))) {
urlRegExp = urlPatternToRegExp(m[1] ?? m[2] ?? argv[++i]);
} else if (arg === "--incognito" || arg === "-i") {
mode = "incognito";
} else {
urls.push(arg);
}
}
if (urls.length === 0) urls.push(null);
const app = Application(appName);
app.activate();
for (const url of urls) {
openUrl(app, url, { urlRegExp, profile, mode });
}
app.activate();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment