Skip to content

About

macOS link picker: open every link in the right Zen workspace, Chrome or Firefox profile, or browser

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Repository files navigation

Browserdoor app icon: a door standing ajar with warm light

Browserdoor

A link picker for macOS. Click a link anywhere, then choose the Zen space, Chrome or Firefox profile, or browser it opens in.

schowave.github.io/browserdoor

CI Release License: MIT OpenSSF Best Practices

Animated demo: a link is clicked in a chat app, the Browserdoor picker appears at the pointer listing Zen spaces, Chrome profiles and Safari; the down arrow selects the Client space, Return opens it, and Zen shows the new tab in the Client space

Why I built it

I use Zen with one space per client, each with its own container. Links from Slack, Mail or Teams always opened in whatever space happened to be active, so I kept ending up in the wrong account and moving tabs around by hand.

Browserosaurus and Finicky solve this for browsers, but not for spaces inside a browser. Browserdoor shows each Zen space as its own entry, next to Chrome and Firefox profiles and your other browsers. You pick one with a number key or a click.

What it does:

  • reads your Zen spaces (names, icons, colors) and opens links with Zen's --space flag (Zen 1.24 or later)
  • lists every profile of Chrome, Brave, Edge, Vivaldi and Chromium separately
  • lists every Firefox profile separately, including the ones from Firefox's new profile manager
  • only shows real browsers, so terminals, web-app wrappers and test builds stay out of the list
  • collects links that arrive at the same moment into one picker, so one choice opens them all
  • runs as a small menu bar app (Swift/SwiftUI). The only network request is the update check, which you can turn off.

Requirements

  • macOS 14 (Sonoma) or later
  • No Xcode on macOS 15 or later (Apple Silicon and Intel): Homebrew installs a prebuilt bottle. On macOS 14 it builds from source and needs Xcode 16.
  • For spaces: Zen 1.24 or later. Older versions show up as a single entry.

Installation

brew install schowave/tap/browserdoor
browserdoor

browserdoor starts the app and always points at the installed version.

Without Homebrew: download Browserdoor.dmg (older versions are on the releases page), open it and drag Browserdoor to Applications. The DMG is signed and notarized by Apple, so it opens without a warning. This build updates itself: it checks once a day (switch it off under Settings), shows what's new and installs the update when you agree.

On first launch Browserdoor asks whether it should become your default browser (macOS asks you to confirm) and adds a login item. A Homebrew install also gets a link at ~/Applications/Browserdoor.app. That link keeps working after brew upgrade, and Browserdoor restarts itself once an upgrade is in. Upgrading from 0.6.1 or earlier, quit Browserdoor from the menu bar once and run browserdoor again.

Upgrading from Doorman (0.2.x) or Browserdoor 0.4.x

Browserdoor was called Doorman up to version 0.2.6. Homebrew only loads formulae you trust, and that trust is tied to the formula name, so trust the new name once before upgrading:

brew trust --formula schowave/tap/browserdoor
brew upgrade doorman && browserdoor

brew upgrade moves the doorman install over by itself. Your settings and ~/.config/doorman (now ~/.config/browserdoor) carry over, and the old command doorman keeps working.

Version 0.5.0 moved the app's bundle ID from io.github.schowave.doorman to io.github.schowave.browserdoor. The first start after updating from 0.4.x or earlier carries your settings, login item and menu bar position over. macOS ties the default browser to the bundle ID, so it asks you once to confirm Browserdoor again.

Usage

Click a link in any app and the picker opens at the mouse pointer:

Key Action
1 … 9, 0, or a custom key Open the link in that target
↑ ↓, then Enter Move the selection, then open
⌘C Copy the URL(s)
Esc, or a click outside the panel Cancel

The menu bar icon has Settings… (⌘,) and Check for Updates…. If Browserdoor isn't your default browser, the menu tells you and offers to fix it. The app is in English or German, depending on your system language.

Configuration

Open Settings… from the menu bar, or run browserdoor while it's already running. Under Targets you can drag entries into a different order, hide them, rename them, give them a key and star the one Enter should open. + adds any app, − removes it again. General has the default browser, login item and update check, Advanced the Zen profile and the config file.

Everything ends up in ~/.config/browserdoor/config.json, so you can also edit that by hand or keep it in your dotfiles:

{
  "defaultTarget": "zen:{b4ac0402-9e47-4873-89b6-5dfeffe77a7e}",
  "order": ["zen:{b4ac0402-9e47-4873-89b6-5dfeffe77a7e}", "app:com.apple.Safari"],
  "hidden": ["app:com.google.Chrome.beta"],
  "extraApps": ["com.googlecode.iterm2"],
  "targets": {
    "zen:{b4ac0402-9e47-4873-89b6-5dfeffe77a7e}": { "label": "Personal", "hotkey": "p" }
  },
  "zen": { "profilePath": null }
}
Key Meaning
defaultTarget Target that Enter opens. Without it, the first entry.
order Targets listed first, in this order. All other targets follow in the default order: Zen workspaces, then Chromium profiles, then Firefox profiles, then other browsers A–Z.
hidden Targets left out of the picker.
extraApps Bundle IDs of apps that aren't known browsers but should appear anyway.
targets.<id>.label Display name for the target.
targets.<id>.hotkey A single key that opens this target. Targets without one get number keys. If two targets have the same key, the first one in the list keeps it.
zen.profilePath Zen profile to use when it isn't the default profile from profiles.ini.

Target IDs come in four forms:

ID Target
zen:{uuid} A Zen workspace
chromium:<bundleId>:<profileDir> A Chromium profile, e.g. chromium:com.google.Chrome:Profile 2
firefox:<bundleId>:<profilePath> A Firefox profile, e.g. firefox:org.mozilla.firefox:Profiles/x1y2.default-release
app:<bundleId> Any other browser

Browserdoor reads the file each time the picker opens, so changes apply right away. If the file is broken, the picker shows a warning and falls back to the defaults.

How it works

Zen keeps its spaces in zen-sessions.jsonlz4 in the profile folder (Mozilla's LZ4 format). Browserdoor reads that file and opens a link with zen --space "{uuid}" <url>. If Zen is already running, the command goes to that instance; if not, Browserdoor starts Zen through LaunchServices and it opens in the chosen space. The space icons come from Zen's omni.ja and are cached per Zen version.

Chromium profiles come from each browser's Local State file, and links open with open -na <App> --args --profile-directory=<dir> <url>.

Firefox profiles come from profiles.ini, and the ones made with Firefox's profile manager from its database in Profile Groups/. Links open with open -na Firefox --args --profile <dir> <url>: if that profile is already open, the link goes to its window, otherwise Firefox starts with it. With only one profile, Firefox stays a single entry. Firefox Developer Edition and Nightly share Firefox's profiles and show up as single entries.

The details are in SPEC.md.

Troubleshooting

A Zen workspace I just created is missing

Zen doesn't write its session file after every change. The space shows up once Zen has saved, which usually doesn't take long.

Zen shows up as one entry without workspaces

You need Zen 1.24 or later, since that's where --space came in. If you don't use Zen's default profile, pick yours under Settings → Advanced.

Firefox shows profiles I never use

Firefox creates a profile for every installation it has seen, and old ones stay in profiles.ini. Hide them under Settings → Targets, or delete them in about:profiles. Browserdoor already leaves out the empty default profile Firefox creates next to default-release.

A browser is missing from the list

Browserdoor only lists known browsers from /Applications, ~/Applications and /System. Add anything else with + under Settings → Targets.

"Start at login" is on, but Browserdoor doesn't start at login

The checkmark only means the LaunchAgent exists. macOS can still block it, so check System Settings → General → Login Items & Extensions and allow Browserdoor under Allow in the Background.

Links still open in my old browser

Open the menu bar menu. If it says Browserdoor isn't your default browser, choose Make Default Browser and confirm the macOS dialog.

Something else? Open an issue. The bug report form asks for the output of this command, which lists every target Browserdoor found and the command it opens a link with:

"$(brew --prefix)/opt/browserdoor/Browserdoor.app/Contents/MacOS/Browserdoor" --list-targets

Updates

Browserdoor looks for a new release on GitHub once a day (you can switch that off in the settings). What changed is in CHANGELOG.md. If there is one, Update in Terminal runs brew upgrade browserdoor. Or do it yourself:

brew upgrade browserdoor

A running Browserdoor notices the new version a few seconds later and restarts itself.

Uninstall

Quit Browserdoor from the menu bar, then remove the app: brew uninstall browserdoor, or for the download, move /Applications/Browserdoor.app to the Trash. Then remove what it left behind:

rm -f ~/Applications/Browserdoor.app ~/Library/LaunchAgents/io.github.schowave.browserdoor.plist
rm -rf ~/.config/browserdoor ~/Library/Caches/io.github.schowave.browserdoor
defaults delete io.github.schowave.browserdoor

Then pick another default browser in System Settings → Desktop & Dock → Default web browser.

Development

Tooling and tasks run through mise; Swift comes from Xcode. CONTRIBUTING.md has the checklist for pull requests, and SECURITY.md how to report a vulnerability. Everyone taking part follows the Code of Conduct.

mise install        # actionlint, shellcheck, swiftformat (pinned in mise.toml)
mise tasks          # list all tasks
mise run test       # unit tests
mise run run        # build build/Browserdoor.app and start it
mise run format     # swiftformat
mise run targets    # print the targets found on this Mac and the command each opens a link with
mise run ci         # lint, unit tests, docs check, release-script tests, app bundle, formula check
mise run demo-gif   # re-render docs/demo.gif from docs/demo/frames.py
mise run social-preview  # re-render docs/social/preview.png (GitHub social preview)
mise run site       # assemble the homepage (site/) in build/site; pages.yml publishes it
mise run check-dmg  # the download build (Sparkle) as an ad-hoc signed DMG in build/

The logic lives in Sources/BrowserdoorCore and is tested without the UI; Sources/Browserdoor is the app itself. GLOSSARY.md explains the terms used in the code. The tasks in .mise/tasks/ are plain shell scripts, and the Homebrew formula calls .mise/tasks/install directly, so installing doesn't need mise.

Releases and dependencies

mise run release vX.Y.Z checks that main is pushed and clean, runs mise run ci, pushes the tag and puts the version's section from CHANGELOG.md on top of the release notes. The tag runs release.yml: tests, build, GitHub release, bottles for Apple Silicon and Intel (built on macOS 15) attached to the release, and an updated Formula/browserdoor.rb with a bottle do block in schowave/homebrew-tap. CI also builds the formula from a source tarball as a bottle (mise run bottle), pours that bottle again and runs brew test and brew audit --strict.

Next to the bottles, the release builds the download build: a DMG signed with Developer ID, notarized, and updating itself with Sparkle from the appcast.xml attached to the release. Until its secrets exist it skips itself; release.yml lists them, and setting them up takes one Apple Developer account:

  1. Developer ID certificate: in Xcode, Settings → Accounts → Manage Certificates → + → Developer ID Application, then export it from Keychain Access as .p12 with a password. Secrets DEVELOPER_ID_CERT_P12 (base64 -i cert.p12) and DEVELOPER_ID_CERT_PASSWORD.
  2. Notarization: an App Store Connect API key (Users and Access → Integrations → Team Keys, role Developer). Secrets NOTARY_API_KEY (the .p8 file's text), NOTARY_API_KEY_ID and NOTARY_API_ISSUER.
  3. Sparkle keys: after mise run check-dmg, .build/sparkle/artifacts/sparkle/Sparkle/bin/generate_keys puts a key pair into your login keychain and prints the public key: repository variable SPARKLE_PUBLIC_KEY. generate_keys -x sparkle.key exports the private key: secret SPARKLE_PRIVATE_KEY, then delete the file. Keep the keychain item: losing the private key means the installed apps can't update any more.

Renovate opens a weekly pull request for GitHub Actions and one for the mise tools, waits seven days after a release before picking it up, and asks before major updates. It merges once CI is green. If a merge changes the app itself, auto-release.yml tags and releases the next patch version.

Acknowledgements

Thanks to Browserosaurus and Finicky for the idea, and to the Zen team for the --space flag that makes this possible.

License

MIT © David Schowalter

About

macOS link picker: open every link in the right Zen workspace, Chrome or Firefox profile, or browser

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages