Before submitting
Problem and use case
BrewUI has no way to be opened or controlled from outside the app itself.
This means:
- Terminal users can't jump from
brew info node to BrewUI to inspect or manage that package visually.
- Launcher tools like Alfred or Raycast can't search installed packages and open the matching one in BrewUI.
- Scripts and CI/CD pipelines can't trigger BrewUI to show a specific package after an install/upgrade step, for visual verification.
- Documentation (READMEs, wikis, internal docs) can't link directly to a package view inside BrewUI — the app can only be opened from the Dock.
- There's no way to build companion apps (e.g. menubar status tools) that deep-link into BrewUI for full management.
Every other major macOS GUI app exposes a custom URL scheme for this kind of external interop. BrewUI currently doesn't, which makes it an "island" that other tools and workflows can't integrate with.
This affects the current version of BrewUI (no URL scheme exists at all today).
Proposed behaviour
Add a brewui:// custom URL scheme covering the entire app. The scheme mirrors BrewUI's full navigation hierarchy — every sidebar destination and every scope picker — so any part of the app is reachable from outside:
Brew UI brewui scheme
│
├─→ Installed brewui://installed
│ ├─→ All (root) brewui://installed
│ ├─→ Formulae brewui://installed/formulae
│ └─→ Casks brewui://installed/casks
│
├─→ Upgrades brewui://upgrades
│ ├─→ All (root) brewui://upgrades
│ ├─→ Formulae brewui://upgrades/formulae
│ └─→ Casks brewui://upgrades/casks
│
├─→ Discover brewui://discover
│ ├─→ All (root) brewui://discover
│ ├─→ Formulae brewui://discover/formulae
│ └─→ Casks brewui://discover/casks
│
├─→ Doctor brewui://doctor
│
├─→ Configuration brewui://config
│
├─→ Select formula brewui://formula/<name>
└─→ Select cask brewui://cask/<token>
Route table
Tabs — all five sidebar destinations (SidebarItem):
| URL |
Behaviour |
brewui://installed |
Switch to Installed tab (scope All) |
brewui://upgrades |
Switch to Upgrades tab (scope All) |
brewui://discover |
Switch to Discover tab (scope All) |
brewui://doctor |
Switch to Doctor tab |
brewui://config |
Switch to Configuration tab |
Scope filters — every picker the UI exposes, as optional sub-routes:
| URL |
Behaviour |
brewui://installed/formulae |
Installed tab, scope Formulae |
brewui://installed/casks |
Installed tab, scope Casks |
brewui://upgrades/formulae |
Upgrades tab, scope Formulae |
brewui://upgrades/casks |
Upgrades tab, scope Casks |
brewui://discover/formulae |
Discover tab, scope Formulae |
brewui://discover/casks |
Discover tab, scope Casks |
/all is the implicit default — brewui://installed and brewui://installed/all are equivalent (and both accepted).
Package selection:
| URL |
Behaviour |
brewui://formula/<name> |
Open Installed tab, select formula |
brewui://cask/<token> |
Open Installed tab, select cask |
Example flow: a user runs open "brewui://formula/node" from Terminal, Alfred, a script, or a clicked link — BrewUI launches (or comes to foreground if already running), opens the Installed tab, and selects the node formula.
This can be considered complete when:
- Every route above works from a cold launch and from a running instance.
- Invalid or malformed URLs are handled gracefully (no crash, no-op or a clear fallback).
- Existing internal navigation is unaffected.
- The full hierarchy maps to every
SidebarItem and every scope picker (InstalledPackageScope in Installed/Upgrades, DiscoverSearchScope in Discover).
Implementation should be small and non-invasive:
-
Register the scheme. Create a partial Info.plist containing only CFBundleURLTypes and set INFOPLIST_FILE to it. With GENERATE_INFOPLIST_FILE = YES, Xcode merges the file's content with the generated property list:
<!-- Configurations/URLSchemes.plist -->
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>sh.brew.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>brewui</string>
</array>
</dict>
</array>
</dict>
Note: INFOPLIST_KEY_CFBundleURLSchemes is not a supported build setting — URL types are a nested array-of-dictionaries, and Xcode's INFOPLIST_KEY_ mechanism only handles scalar values for a curated set of keys. A partial plist merged via INFOPLIST_FILE is the reliable way when GENERATE_INFOPLIST_FILE = YES (which is how this project is already configured).
-
Add an .onOpenURL handler in Homebrew/BrewApp.swift that parses the incoming URL and routes to the existing pendingInstalledSelection / selectedSidebarItem navigation in MainWindowView — the same mechanism used for internal deep links today.
The five tab destinations map directly to the existing SidebarItem enum (installed, upgrades, discover, doctor, configuration), so tab routing needs no new navigation logic.
Phasing
The scope routes require a deep-link mechanism for the view models' scope state, which currently lives as local @State inside the feature containers. To keep the review small, scope support lands in follow-up phases:
- Phase 1 — tabs + package selection:
installed, upgrades, discover, doctor, config, formula/<name>, cask/<token>. Reuses SidebarItem + pendingInstalledSelection. Small diff.
- Phase 2 — Installed scopes:
installed/formulae, installed/casks. New binding into InstalledViewModel.scope.
- Phase 3 — Upgrades + Discover scopes:
upgrades/formulae, upgrades/casks, discover/formulae, discover/casks. Same mechanism, two more view models (UpgradesViewModel.scope, DiscoverViewModel.scope).
Alternatives considered
- Manually switching to BrewUI and searching for the package by hand. This works but breaks flow for terminal-first users and makes any kind of automation or scripting impossible.
- Using
brew info <name> in Terminal instead of BrewUI. This gives text output but loses the visual management (upgrade status, GUI actions) that BrewUI provides — it's not a substitute, just a workaround for users who give up on opening the GUI at all.
- Building a fully separate companion app that duplicates BrewUI's package list/state instead of deep-linking into it. This would duplicate logic and diverge from BrewUI's own data, which is worse for maintenance and consistency than reusing BrewUI's existing views.
- Narrow URL schemes that only expose one tab (e.g. only
installed). These leave Discover, Doctor and Configuration unreachable from outside, so launchers and scripts still have to fall back to manual navigation.
None of these are sufficient substitutes for actual external interop; they're all workarounds for the lack of a URL scheme.
macOS and Homebrew context
This is a standard macOS integration pattern: URL schemes registered via CFBundleURLSchemes are the conventional way for macOS apps to expose deep links to Terminal (open), Spotlight-adjacent launchers (Alfred, Raycast), Shortcuts, and other apps. No new macOS APIs beyond .onOpenURL (SwiftUI) are needed, and no minimum macOS version change is expected beyond what BrewUI already requires.
This does not expose new Homebrew commands directly — it exposes existing BrewUI navigation (every sidebar destination, every scope filter, and package selection) to external callers. A likely follow-up (not required for this issue) would be a "View in BrewUI" affordance from brew info itself, but that's out of scope here.
As a separate, independent effort, an Alfred workflow could be built that uses brew list --formula --cask as a Script Filter and calls open "brewui://formula/<name>" on selection — but that would live in its own repo and isn't part of this BrewUI-side request.
Before submitting
Problem and use case
BrewUI has no way to be opened or controlled from outside the app itself.
This means:
brew info nodeto BrewUI to inspect or manage that package visually.Every other major macOS GUI app exposes a custom URL scheme for this kind of external interop. BrewUI currently doesn't, which makes it an "island" that other tools and workflows can't integrate with.
This affects the current version of BrewUI (no URL scheme exists at all today).
Proposed behaviour
Add a
brewui://custom URL scheme covering the entire app. The scheme mirrors BrewUI's full navigation hierarchy — every sidebar destination and every scope picker — so any part of the app is reachable from outside:Route table
Tabs — all five sidebar destinations (
SidebarItem):brewui://installedbrewui://upgradesbrewui://discoverbrewui://doctorbrewui://configScope filters — every picker the UI exposes, as optional sub-routes:
brewui://installed/formulaebrewui://installed/casksbrewui://upgrades/formulaebrewui://upgrades/casksbrewui://discover/formulaebrewui://discover/casks/allis the implicit default —brewui://installedandbrewui://installed/allare equivalent (and both accepted).Package selection:
brewui://formula/<name>brewui://cask/<token>Example flow: a user runs
open "brewui://formula/node"from Terminal, Alfred, a script, or a clicked link — BrewUI launches (or comes to foreground if already running), opens the Installed tab, and selects thenodeformula.This can be considered complete when:
SidebarItemand every scope picker (InstalledPackageScopein Installed/Upgrades,DiscoverSearchScopein Discover).Implementation should be small and non-invasive:
Register the scheme. Create a partial Info.plist containing only
CFBundleURLTypesand setINFOPLIST_FILEto it. WithGENERATE_INFOPLIST_FILE = YES, Xcode merges the file's content with the generated property list:Note:
INFOPLIST_KEY_CFBundleURLSchemesis not a supported build setting — URL types are a nested array-of-dictionaries, and Xcode'sINFOPLIST_KEY_mechanism only handles scalar values for a curated set of keys. A partial plist merged viaINFOPLIST_FILEis the reliable way whenGENERATE_INFOPLIST_FILE = YES(which is how this project is already configured).Add an
.onOpenURLhandler inHomebrew/BrewApp.swiftthat parses the incoming URL and routes to the existingpendingInstalledSelection/selectedSidebarItemnavigation inMainWindowView— the same mechanism used for internal deep links today.The five tab destinations map directly to the existing
SidebarItemenum (installed,upgrades,discover,doctor,configuration), so tab routing needs no new navigation logic.Phasing
The scope routes require a deep-link mechanism for the view models'
scopestate, which currently lives as local@Stateinside the feature containers. To keep the review small, scope support lands in follow-up phases:installed,upgrades,discover,doctor,config,formula/<name>,cask/<token>. ReusesSidebarItem+pendingInstalledSelection. Small diff.installed/formulae,installed/casks. New binding intoInstalledViewModel.scope.upgrades/formulae,upgrades/casks,discover/formulae,discover/casks. Same mechanism, two more view models (UpgradesViewModel.scope,DiscoverViewModel.scope).Alternatives considered
brew info <name>in Terminal instead of BrewUI. This gives text output but loses the visual management (upgrade status, GUI actions) that BrewUI provides — it's not a substitute, just a workaround for users who give up on opening the GUI at all.installed). These leave Discover, Doctor and Configuration unreachable from outside, so launchers and scripts still have to fall back to manual navigation.None of these are sufficient substitutes for actual external interop; they're all workarounds for the lack of a URL scheme.
macOS and Homebrew context
This is a standard macOS integration pattern: URL schemes registered via
CFBundleURLSchemesare the conventional way for macOS apps to expose deep links to Terminal (open), Spotlight-adjacent launchers (Alfred, Raycast), Shortcuts, and other apps. No new macOS APIs beyond.onOpenURL(SwiftUI) are needed, and no minimum macOS version change is expected beyond what BrewUI already requires.This does not expose new Homebrew commands directly — it exposes existing BrewUI navigation (every sidebar destination, every scope filter, and package selection) to external callers. A likely follow-up (not required for this issue) would be a "View in BrewUI" affordance from
brew infoitself, but that's out of scope here.As a separate, independent effort, an Alfred workflow could be built that uses
brew list --formula --caskas a Script Filter and callsopen "brewui://formula/<name>"on selection — but that would live in its own repo and isn't part of this BrewUI-side request.