From 7c53413e40961c61cad18145bcdf8b8cf1b9d0a6 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Mon, 6 Jul 2026 22:06:55 +0200 Subject: [PATCH] add auto-upgrade settings, split into formulae and casks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent toggles, both off by default. When a scheduled fetch finds outdated packages, runAutoUpgrade runs "upgrade --formula" and/or "upgrade --cask" (with --greedy if that setting is on), then refetches. The refetch passes allowAutoUpgrade: false — packages can legitimately stay outdated after an upgrade (pinned formulae, greedy casks brew declines), and without the guard that would trigger upgrade → fetch → upgrade forever. Manual Upgrade All's final fetch gets the same guard. The casks checkbox carries a warning note in the settings window: a cask upgrade can close and replace the running app without warning. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 38 ++++++++++++++++++++++-- BrewBar/SettingsWindowController.swift | 41 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index f102df3..c6d9522 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -213,7 +213,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { runBrew("update") { _ in self.runBrew("upgrade") { _ in - self.fetchOutdated() + // everything was just upgraded; don't chain an auto-upgrade + self.fetchOutdated(allowAutoUpgrade: false) } } } @@ -242,7 +243,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { return alert.runModal() == .alertFirstButtonReturn } - func fetchOutdated() { + /// allowAutoUpgrade guards against an upgrade loop: packages can stay + /// outdated after an upgrade (pinned formulae, greedy casks brew declines), + /// so the fetch that follows an auto-upgrade must not trigger another one. + func fetchOutdated(allowAutoUpgrade: Bool = true) { setMenuBarIcon("brewbar-updating") // --greedy also lists casks that self-update (brew skips them by default) @@ -253,6 +257,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.cachedOutdated = lines + if allowAutoUpgrade, !lines.isEmpty, + Settings.autoUpgradeFormulae || Settings.autoUpgradeCasks + { + self.runAutoUpgrade() + return + } + self.updateOutdatedMenu(with: lines) self.updateStatus(count: lines.count) if lines.isEmpty { @@ -263,6 +274,29 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + func runAutoUpgrade() { + statusMenuItem.title = "Auto-upgrading..." + + let upgradeCasksThenRefresh = { + if Settings.autoUpgradeCasks { + let command = Settings.includeGreedyCasks ? "upgrade --cask --greedy" : "upgrade --cask" + self.runBrew(command) { _ in + self.fetchOutdated(allowAutoUpgrade: false) + } + } else { + self.fetchOutdated(allowAutoUpgrade: false) + } + } + + if Settings.autoUpgradeFormulae { + runBrew("upgrade --formula") { _ in + upgradeCasksThenRefresh() + } + } else { + upgradeCasksThenRefresh() + } + } + func updateOutdatedMenu(with lines: [String]) { outdatedSubmenu.removeAllItems() outdatedItem.isHidden = lines.isEmpty diff --git a/BrewBar/SettingsWindowController.swift b/BrewBar/SettingsWindowController.swift index bae5c85..198aa6c 100644 --- a/BrewBar/SettingsWindowController.swift +++ b/BrewBar/SettingsWindowController.swift @@ -16,6 +16,8 @@ class SettingsWindowController: NSWindowController { var customMinutesField: NSTextField! var launchAtLoginCheckbox: NSButton! var greedyCheckbox: NSButton! + var autoUpgradeFormulaeCheckbox: NSButton! + var autoUpgradeCasksCheckbox: NSButton! convenience init(appDelegate: AppDelegate) { let window = NSWindow( @@ -81,11 +83,40 @@ class SettingsWindowController: NSWindowController { action: #selector(toggleGreedy(_:)) ) + autoUpgradeFormulaeCheckbox = NSButton( + checkboxWithTitle: "Automatically upgrade formulae (command-line tools)", + target: self, + action: #selector(toggleAutoUpgradeFormulae(_:)) + ) + + autoUpgradeCasksCheckbox = NSButton( + checkboxWithTitle: "Automatically upgrade casks (apps)", + target: self, + action: #selector(toggleAutoUpgradeCasks(_:)) + ) + + let caskWarning = NSTextField( + wrappingLabelWithString: "⚠️ Upgrading a cask can close and replace the app while it is running (e.g. your browser), without warning." + ) + caskWarning.font = .systemFont(ofSize: NSFont.smallSystemFontSize) + caskWarning.textColor = .secondaryLabelColor + caskWarning.preferredMaxLayoutWidth = 320 + + // indent the note so it reads as belonging to the checkbox above + let spacer = NSView() + spacer.widthAnchor.constraint(equalToConstant: 18).isActive = true + let caskWarningRow = NSStackView(views: [spacer, caskWarning]) + caskWarningRow.orientation = .horizontal + let stack = NSStackView(views: [ frequencyRow, customRow, greedyCheckbox, separator(), + autoUpgradeFormulaeCheckbox, + autoUpgradeCasksCheckbox, + caskWarningRow, + separator(), launchAtLoginCheckbox, ]) stack.orientation = .vertical @@ -122,6 +153,8 @@ class SettingsWindowController: NSWindowController { // this in System Settings > Login Items while we weren't looking launchAtLoginCheckbox.state = SMAppService.mainApp.status == .enabled ? .on : .off greedyCheckbox.state = Settings.includeGreedyCasks ? .on : .off + autoUpgradeFormulaeCheckbox.state = Settings.autoUpgradeFormulae ? .on : .off + autoUpgradeCasksCheckbox.state = Settings.autoUpgradeCasks ? .on : .off } func separator() -> NSBox { @@ -159,6 +192,14 @@ class SettingsWindowController: NSWindowController { appDelegate?.fetchOutdated() // the list contents just changed meaning } + @objc func toggleAutoUpgradeFormulae(_ sender: NSButton) { + Settings.autoUpgradeFormulae = sender.state == .on + } + + @objc func toggleAutoUpgradeCasks(_ sender: NSButton) { + Settings.autoUpgradeCasks = sender.state == .on + } + @objc func toggleLaunchAtLogin(_ sender: NSButton) { do { if sender.state == .on {