How I Navigate Hundreds of Tabs on Chrome with JXA and Alfred

Renan Cakirerk

Having hundreds of open tabs has always been an issue for me. OK maybe not hundreds, but definitely close to a hundred. The more tabs I open, the more duplicates I have. In the end it just spirals out of control to a point where I give up and hit the close button on Chrome for a fresh start. Every time I do that, there are plenty of things I lose that I wanted to explore at some point.

I think it’s fair to partially blame today’s browsers for offering a pretty bad and ancient UX. Strictly talking from a UI/UX perspective, there’s pretty much no innovation I can see other than making the browser look prettier.

For instance if you want to visit a previously visited URL, the address bar search does a terrible job finding the URL when I start typing keywords that I partially remember. In Chrome, it seems like there is some sort of fuzzy search going on, but it’s not quite what I expect it to be. I’d much prefer something like the wonderful fzf command.

Using fuzzy search via fzf in iTerm2

Another example can be dealing with many tabs. For instance, Chrome doesn’t offer a way to search tabs, and for some reason it doesn’t allow stacking them vertically which results in tiny tabs where you only can see the icons. Also there’s no way to get rid of duplicate tabs.

Now I can can hear you telling me to use a Chrome extension for that; but call me paranoid, I don’t use any extensions that require any of the following permissions:

Insane permissions needed by Chrome extensions these days

This embarrassingly reveals that I don’t use any Chrome extensions no matter how useful they are. OK, some of them are open source, but open source doesn’t mean safe when you’re too lazy to inspect the code.

Open source doesn’t mean safe when you’re too lazy to inspect the code.

So I decided to solve the problem of dealing with tabs myself, since I’m never lazy for DIY.

Choosing the Right Tool for the Job

The first thing I needed to do was to find a way to control Chrome. Here are a couple of alternatives I’ve considered:

  • Writing a Chrome Extension: I didn’t want to write a Chrome extension because extensions are only useful when the browser is focused. This is bad because when I’m focused on another window, such as my terminal, and I want to jump to a specific tab, I’d have to open Chrome then start using my extension. Too much work.
  • Using AppleScript: AppleScript is a language used for controlling applications with Apple Events. It looked promising at first until I played with some examples. Using languages like C, C++, Python, Java, Go, and JavaScript my entire career, I instantly felt out of my comfort zone and decided to get back to this if I couldn’t find a better alternative.
  • Using Python: I was pretty excited about learning that there are some libraries that you can use to talk to MacOS. They ended up being super outdated with no proper documentation. So unfortunately this was a no-go situation for me, since I wanted to move fast.

Not giving up easily, I did some more research and stumbled upon JXA.

What is JXA?

JXA stands for JavaScript for Automation. It’s natively supported by Apple, it allows you to control apps with it instead of AppleScript, it supports ES6 syntax, it overall sounds too good to be true… Well it was, in the sense of that it has the worst documentation I’ve ever seen produced by Apple.

I still don’t know what the X stands for. Perhaps MacOS X? ¯\_(ツ)_/¯

I took a look at the hilarious release notes (written by Apple as JXA documentation…) and then thought hey, JavaScript, how hard can it be to take a brute-force approach and discover things myself?

Oh boy, I had no idea what was coming.

Finding where to write the code, what the file extension should be, how to run the file, and discovering the API took me hours.

But in the end, it was all worth the effort.

Getting Started With JXA

Most articles out there point to writing JXA in the Script Editor App or the Automator App that comes with MacOS. Trying them out for a bit and almost throwing my laptop out the window, I decided to go with the editor I use for everything: VSCode.

After being a power Vim user for ~4 years, using JetBrains IDE’s, Atom, and Sublime for a year, I ended up on VSCode, and after 4 years of daily usage with mostly Go, and occasional Python, JavaScript, and Markdown, I still think it’s the best editor out there; and everyone should try it at least for a month or two before judging.

Here’s what you need to know for getting started quickly:

  • Create your file with a regular js extension. (e.g. script.js)
  • Paste this line as the first line in your file:
    #!/usr/bin/env osascript -l JavaScript
  • Make your script executable:
    chmod +x ./script.js
  • And simply run it with:
    ./script.js

Here’s a sample you can try:

console.log("Hello multiverse 👽")

Connecting to Chrome

The biggest issue I had was figuring out how to connect to applications. There’s no autocomplete for methods and trying to print the Object fields and methods didn’t work.

I then accidentally stumbled on an article that mentioned using a feature in the Script Editor: Open Dictionary.

Opening a dictionary in Script Editor

And voila! This was exactly what I was looking for; a small booklet for each application that lists the methods and objects; and Chrome was in there!

Opening Google Chrome dictionary in Script Editor

Here’s what the interface looks like.

Google Chrome Dictionary in Script Editor

It’s not the best, but it’s still something that I can work with.

So the first thing I needed to do was to get a Chrome instance:

const chrome = Application('Google Chrome')
chrome.includeStandardAdditions = true

includeStandardAdditions is used for adding standard methods such as displayDialog to the application instance.

Then I had to figure out how to interact with it. It ended up being pretty easy.
Here’s how to list all the tabs in all the windows:

chrome.windows().forEach((window, winIdx) => {
window.tabs().forEach((tab, tabIdx) => {
console.log(tab.title(), tab.url())
})
})

Closing tabs was also pretty easy:

chrome.windows[winIdx].tabs[tabIdx].close()

Focusing on a specific tab in a specific window was a little tricky:

chrome.windows[winIdx].visible = true
chrome.windows[winIdx].activeTabIndex = tabIdx
chrome.windows[winIdx].index = 1
chrome.activate()

I then wanted to print out a JSON formatted text to pass it to the life saver jq command, and that’s when I realized that the console.log was printing to stderr and there wasn’t any function to print to stdout and I didn’t want to redirect stderr to stdout.

Fortunately I found out that I can import Objective C into JXA. That’s when I decided to write my own print function:

ObjC.import('stdlib')
ObjC.import('Foundation')
const print = function (msg) {
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
$.NSString.alloc.initWithString(String(msg))
.dataUsingEncoding($.NSUTF8StringEncoding)
)
}

It took me a while, but understanding how that stuff works was rewarding. Luckily, I had a little bit of Objective C experience that helped from my Watch Movie Trailers on Posters with iPad via Augmented Reality project back in 2011–2012 when AR was still at its infancy on iOS and I had big dreams…

After that, for getting input from the terminal, I had to write my custom input function as well:

const input = function (msg) {
print(msg)
return $.NSString.alloc.initWithDataEncoding(
$.NSFileHandle.fileHandleWithStandardInput.availableData,
$.NSUTF8StringEncoding
).js.trim()
}

Finally, I needed a way to display a prompt on Chrome so it can ask me if I really want to close the listed tabs:

chrome.displayDialog(msg)

A New Project is Born: Chrome Control

I gathered everything under a project and named it Chrome Control. Perhaps someone can use it to integrate it with their favorite tool. Perhaps a vim or fzf integration? You can find the source code on GitHub.

Here’s what Chrome Control looks like:

Chrome Control in iTerm2

Now that I had a way to do everything I needed, it was time for using Chrome Control via the best productivity app in the multiverse: Alfred.

Seriously, Apple should consider acquiring Alfred and deprecating Spotlight.

Using Chrome Control with Alfred

Alfred is one of the best productivity applications out there. It makes me 10x productive 🦄 at work without exaggeration. I have created dozens of workflows that help speed up my day-to-day tasks.

For this project, my dream was to hit alt + t anywhere in MacOS and start typing a tab title to instantly see a list of tabs filtered by fuzzy search, then I would simply highlight the tab I want and hit enter instantly to go to that tab.

First, I needed to create a new workflow in Alfred.

Creating a new workflow

I made Chrome Control output the list of tabs as JSON for enabling integrations. This helps because if I wanted to list the tabs in Alfred, I had to use a Script Filter, and a Script Filter requires JSON output with some additional special fields.

Here’s a sample output of the list command:

{
"items": [
{
"title": "Inbox (1) - <hidden>@gmail.com - Gmail",
"url": "https://mail.google.com/mail/u/0/#inbox",
"winIdx": 0,
"tabIdx": 0,
"arg": "0,0",
"subtitle": "https://mail.google.com/mail/u/0/#inbox"
},
{
"title": "iPhone - Apple",
"url": "https://www.apple.com/iphone/",
"winIdx": 0,
"tabIdx": 1,
"arg": "0,1",
"subtitle": "https://www.apple.com/iphone/"
}
]
}

The arg and subtitle fields are required for the Script Filter. We’ll get into that in a bit.

Adding a new Script Filter to the workflow

Creating a script filter opens up this window:

Setting up the Script Filter

I wanted the keyword to be tabs so whenever I type tabs I’d see a list of all tabs in all windows.

There’s a bunch of other things that are going on here. First thing to notice is the with space argument optional. This tells Alfred to expect only the tabs command or an optional argument such as tabs apple which can show all Apple related tabs.

Alfred has a wonderful out-of-the box fuzzy search feature. To enable that, I simply checked the Alfred filters results checkbox.

On the script section, I told Alfred to run my list command.
And finally, I dragged the icon I created with Photoshop.

Here’s what the output looks like:

Chrome Control on Alfred

The second step is to bind the alt + t key to this command. Doing that is super simple in Alfred using the hotkey trigger:

Creating a Hotkey Trigger

Set it up to be alt + t:

Setting up the Hotkey Trigger

Then connect it to the Script Filter by dragging the connection cable:

Connecting the Hotkey Trigger to the Script Filter

Now that I had a way to hit a hotkey and filter a tab, it was time to focus on the selected tab when I hit enter.

Focusing on a Selected Tab

This part was a little tricky to get it work. I needed to tell Alfred to run another script with the result of the Script Filter.

Remember the arg value that I output in the JSON? It looked something like 0,1. The first value is the Window Index and the second value is the Tab Index. I needed to pass this arg value to the ./chrome.js focus command.

Luckily Alfred uses this arg value to pass it down as a {query} template variable to further actions that you connect to the Script Filter.

This means I can connect the output of the Script Filter to a new Run Script action, and pass the arg value to the focus command.

Connecting the Script Filter output to a Run Script action

Then simply run the ./chrome.js focus {query} command whenever an item is selected from the list.

Configuring the Script Filter to run the `focus` command when an item is selected

And it worked!

I also wanted a way to close the highlighted tabs. For this, I could use the alt key, which is the modifier key in MacOS.

If you didn’t know this feature try clicking on your wifi icon or the speaker icon in the menu bar while holding the alt key, for some additional features.

For that to happen, I needed to connect the Script Filter to my close command.

Connecting the tabs command to the Chrome Control close command

To tell Alfred that this script should run only when the alt key is pressed, you need to right click on the connection and configure it.

Opening the connection configuration
Setting up the alt key behavior

I wanted the Close this tab text to appear when I hold the alt key. The result looks like this:

Holding `alt` and hitting enter will close this tab

Once this was done, I was hungry to add more features. That’s when I thought of doing something about my duplicate tabs problem.

Deduping Open Tabs

One of my main problems was duplicate tabs. To solve that issue, I wanted to add a dedup command to Chrome Control.

It simply iterates all the open tabs, finds the duplicates, then closes them.

One small problem though, I thought it would be a disaster if I accidentally closed a tab which had unsaved work, such as a semi-filled form, an unsaved doc, half written email, … etc.

For that reason I wanted a prompt that showed me a list of tabs that were about to get closed and ask me if I was really sure about closing them. I made this the default option, and added a --yes flag, so if I ever wanted to force all the tabs to be closed without a prompt, I could.

Here’s an example of deduping five Hacker News tabs I’ve opened:

Deduping tabs with Chrome Control

I then connected this to Alfred. But then I realized that I needed a prompt inside the browser now. So I added a --ui flag. When this flag is provided, Chrome Control will ask the questions inside the browser instead of the terminal.

Connecting the dedup command to Chrome Control

I simply created a keyword trigger then connected to ./chrome.js dedup. Now a dialog appears on Chrome to ask me if I’m sure!

Chrome asking me if I’m sure about closing the duplicate tabs

This is why we added the following line all the way at the beginning of our script. It allowed us to show this dialog.

chrome.includeStandardAdditions = true

Closing Tabs by Keywords

One other feature I really wanted was to close tabs by keywords. The keywords could exist in the url or the title of the tab.

For instance if I had a gazillion Google Docs open, I could simply type ./chrome.js close --url docs.google. Chrome Control would then find all the urls that included this string and close the tabs.

I also wanted this to work with tab title as well. For instance, if I was doing a research about the latest iPhone, perhaps I could bulk close all the titles that included iPhone such as ./chrome.js close --title iphone.

So I went ahead and implemented those two commands as well.

Closing tabs by title
Closing tabs by URL

These commands can take multiple keywords divided with a space. Or if the phrase itself includes a space then wrapping the phrase with double quotes would work, such as "this is a phrase with spaces".

And of course I connected them to Alfred. This time an argument was required.

Connecting the `Close URL` command with Alfred

And then added a Run Script action and this time used with input as argv. This allowed me to use $@ which sends all the keywords I typed to Chrome Control as arguments.

Setting up the `close url` command on Alfred
Connecting the `close url` keyword to Chrome Control

Here’s how I close all the tabs that contain apple or doc in them.

Closing tabs with URLs that contain either `apple` or `doc`

I repeated the same for creating a close title command on Alfred.

I also added additional hotkeys to trigger other commands:

  • alt + t List all tabs
  • alt + d Dedup tabs
  • alt + c Close tabs by URL
  • alt + shift + c Close tabs by title

And here’s what the final workflow looks like:

Chrome Control workflow on Alfred

Source Code and Chrome Control Alfred Workflow

You can find additional documentation about Chrome Control and all the source code mentioned here on GitHub.

If you feel like you can use something like Chrome Control Workflow in your life, feel free to download it here also on GitHub.

Alfred doesn’t allow workflows export hotkeys to protect users from conflicting shortcuts. After importing, you’ll have to manually set the hotkeys, but it should take no longer than 60 seconds.

Conclusion

JXA combined with Alfred is an extremely powerful tool to build really cool workflows that are limited to your imagination. It was really painful to get started and to find good documentation, but in the end I was able to build what I’ve dreamt of and I couldn’t be happier.

Of course it would be excellent if Chrome provided features like this out-of-the-box, but I’m not very optimistic about that.

Have fun navigating your tabs!

Renan Cakirerk

Written by

backend engineer @uber • freethinker • code juggler • pixel bender • music maker • aspiring polymath • ren.io

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade