From c4baca221d66b898f9d754e6d52eeda74ee22253 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Mon, 13 Jul 2026 18:57:18 +0200 Subject: [PATCH 1/2] split Upgrade All into formulae & casks plus selective upgrades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Upgrade All" becomes "Upgrade formulae & casks", and an "Upgrade only…" submenu offers Formulae/Casks with live counts β€” shown only when both kinds are outdated, since with a single kind the main item already upgrades exactly what there is. All three actions share one parametrized upgrade(formulae:casks:) flow; the confirmation dialog lists only the selected packages and drops the running-app warning when no casks are involved. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 63 ++++++++++++++++++++------ BrewBar/SettingsWindowController.swift | 2 +- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index 909b414..27f9255 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -18,6 +18,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { var statusMenuItem: NSMenuItem! var outdatedItem: NSMenuItem! var upgradeAllItem: NSMenuItem! + var upgradeOnlyItem: NSMenuItem! + var upgradeFormulaeItem: NSMenuItem! + var upgradeCasksItem: NSMenuItem! var outdatedSubmenu: NSMenu! var versionItem: NSMenuItem! @@ -74,9 +77,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "πŸ”„ Refresh", action: #selector(refreshAction), keyEquivalent: "r")) - upgradeAllItem = NSMenuItem(title: "πŸ“¦ Upgrade All", action: #selector(upgradeAll), keyEquivalent: "u") + upgradeAllItem = NSMenuItem(title: "πŸ“¦ Upgrade formulae & casks", action: #selector(upgradeAll), keyEquivalent: "u") upgradeAllItem.isHidden = true // shown once a fetch finds outdated packages menu.addItem(upgradeAllItem) + upgradeOnlyItem = NSMenuItem(title: "🎯 Upgrade only…", action: nil, keyEquivalent: "") + let upgradeOnlySubmenu = NSMenu() + upgradeFormulaeItem = NSMenuItem(title: "Formulae", action: #selector(upgradeFormulaeOnly), keyEquivalent: "") + upgradeCasksItem = NSMenuItem(title: "Casks", action: #selector(upgradeCasksOnly), keyEquivalent: "") + upgradeOnlySubmenu.addItem(upgradeFormulaeItem) + upgradeOnlySubmenu.addItem(upgradeCasksItem) + upgradeOnlyItem.submenu = upgradeOnlySubmenu + upgradeOnlyItem.isHidden = true // shown only when both kinds are outdated + menu.addItem(upgradeOnlyItem) outdatedItem = NSMenuItem(title: "πŸ“‹ Outdated", action: nil, keyEquivalent: "") outdatedSubmenu = NSMenu() outdatedItem.submenu = outdatedSubmenu @@ -306,8 +318,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { } @objc func upgradeAll() { + upgrade(formulae: true, casks: true) + } + + @objc func upgradeFormulaeOnly() { + upgrade(formulae: true, casks: false) + } + + @objc func upgradeCasksOnly() { + upgrade(formulae: false, casks: true) + } + + func upgrade(formulae: Bool, casks: Bool) { if Settings.confirmBeforeUpgradeAll { - guard confirmUpgrade() else { return } + guard confirmUpgrade(formulae: formulae, casks: casks) else { return } } statusMenuItem.title = "Upgrading..." @@ -315,22 +339,22 @@ class AppDelegate: NSObject, NSApplicationDelegate { runBrew("update") { _, _ in // packages that turn outdated only after this brew update are not // in cachedOutdated yet; the final refetch will surface them - let caskNames = self.cachedOutdated.casks.map(\.name) + let caskNames = casks ? self.cachedOutdated.casks.map(\.name) : [] let upgradeCasksThenRefresh = { self.upgradeCasksSequentially(caskNames) { self.cleanupAfterUpgradeIfEnabled { - // everything was just upgraded; don't chain an auto-upgrade + // a manual upgrade just ran; don't chain an auto-upgrade self.fetchOutdated(allowAutoUpgrade: false) } } } - if self.cachedOutdated.formulae.isEmpty { - upgradeCasksThenRefresh() - } else { + if formulae, !self.cachedOutdated.formulae.isEmpty { self.runBrew("upgrade --formula") { _, _ in upgradeCasksThenRefresh() } + } else { + upgradeCasksThenRefresh() } } } @@ -350,21 +374,27 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - func confirmUpgrade() -> Bool { + func confirmUpgrade(formulae: Bool, casks: Bool) -> Bool { + let packages = (formulae ? cachedOutdated.formulae : []) + (casks ? cachedOutdated.casks : []) + let alert = NSAlert() alert.alertStyle = .warning - if cachedOutdated.isEmpty { + if packages.isEmpty { alert.messageText = "Upgrade all outdated packages?" } else { - alert.messageText = "Upgrade \(cachedOutdated.count) package\(cachedOutdated.count == 1 ? "" : "s")?" + alert.messageText = "Upgrade \(packages.count) package\(packages.count == 1 ? "" : "s")?" } - var info = "Apps upgraded as casks (like a web browser) may be closed and replaced while running, without further warning. Save your work first." - if !cachedOutdated.isEmpty { - info += "\n\n" + cachedOutdated.all.map(\.label).joined(separator: "\n") + // the running-app warning only applies when casks are being upgraded + var lines: [String] = [] + if casks { + lines.append("Apps upgraded as casks (like a web browser) may be closed and replaced while running, without further warning. Save your work first.") } - alert.informativeText = info + if !packages.isEmpty { + lines.append(packages.map(\.label).joined(separator: "\n")) + } + alert.informativeText = lines.joined(separator: "\n\n") alert.addButton(withTitle: "Upgrade") alert.addButton(withTitle: "Cancel") @@ -464,6 +494,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { outdatedSubmenu.removeAllItems() outdatedItem.isHidden = outdated.isEmpty upgradeAllItem.isHidden = outdated.isEmpty + // "only" is meaningful only when both kinds are outdated; with a + // single kind, the main item already upgrades exactly what there is + upgradeOnlyItem.isHidden = outdated.formulae.isEmpty || outdated.casks.isEmpty + upgradeFormulaeItem.title = "Formulae (\(outdated.formulae.count))" + upgradeCasksItem.title = "Casks (\(outdated.casks.count))" for package in outdated.all { let item = NSMenuItem(title: package.label, action: #selector(upgradeSingle(_:)), keyEquivalent: "") diff --git a/BrewBar/SettingsWindowController.swift b/BrewBar/SettingsWindowController.swift index 935abd5..0e84065 100644 --- a/BrewBar/SettingsWindowController.swift +++ b/BrewBar/SettingsWindowController.swift @@ -121,7 +121,7 @@ class SettingsWindowController: NSWindowController { ) confirmCheckbox = NSButton( - checkboxWithTitle: "Ask for confirmation before Upgrade All", + checkboxWithTitle: "Ask for confirmation before bulk upgrades", target: self, action: #selector(toggleConfirm(_:)) ) From 2931b800749f007b8d9bc0fcce5839b2e1310618 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Mon, 13 Jul 2026 18:58:32 +0200 Subject: [PATCH 2/2] fall back to reinstall when a cask cannot be upgraded as-is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-updating apps (Zen, Chrome, …) drift from what brew installed; brew then refuses to upgrade the cask in place, prints a warning and does nothing, so the package stayed outdated forever. Detect the warning and run the fix brew itself suggests (reinstall --cask --force). Single-package upgrades now also route casks through upgrade --cask instead of a bare brew upgrade. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index 27f9255..45e2a19 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -368,12 +368,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { } statusMenuItem.title = "Upgrading \(next)..." - // an explicitly named cask upgrades even when self-updating, no --greedy needed - runBrew("upgrade --cask \(next)", askpassMessage: "upgrade \(next)") { _, _ in + upgradeCask(next) { self.upgradeCasksSequentially(Array(names.dropFirst()), completion: completion) } } + /// Self-updating apps (Zen, Chrome, …) can drift from what brew + /// installed, and brew then refuses to upgrade the cask in place β€” + /// it only prints a warning naming the fix. Run that fix for it. + /// (An explicitly named cask upgrades even when self-updating, no + /// --greedy needed.) + func upgradeCask(_ name: String, completion: @escaping () -> Void) { + runBrew("upgrade --cask \(name)", askpassMessage: "upgrade \(name)") { output, _ in + guard output.contains("cannot be upgraded as-is") else { + completion() + return + } + self.runBrew("reinstall --cask --force \(name)", askpassMessage: "reinstall \(name)") { _, _ in + completion() + } + } + } + func confirmUpgrade(formulae: Bool, casks: Bool) -> Bool { let packages = (formulae ? cachedOutdated.formulae : []) + (casks ? cachedOutdated.casks : []) @@ -524,13 +540,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { } @objc func upgradeSingle(_ sender: NSMenuItem) { - guard let formula = sender.representedObject as? String else { return } + guard let name = sender.representedObject as? String else { return } - runBrew("upgrade \(formula)", askpassMessage: "upgrade \(formula)") { _, _ in + let finish = { self.cleanupAfterUpgradeIfEnabled { self.refreshAll() } } + + // casks go through upgradeCask for the reinstall fallback; a bare + // "brew upgrade " would hit the same warning and do nothing + if cachedOutdated.casks.contains(where: { $0.name == name }) { + upgradeCask(name, completion: finish) + } else { + runBrew("upgrade \(name)", askpassMessage: "upgrade \(name)") { _, _ in finish() } + } } /// Refreshing offline would silently succeed on stale data: brew update