Skip to content

Add brewui:// URL scheme for external deep linking #204

Description

@raulmoracode

Before submitting

  • I have searched the issues and pull requests for duplicates.
  • I am proposing a specific improvement to BrewUI with a use case and a clear outcome.

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:

  1. 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).

  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions