From 32d81423abeeaefb95743eb738bd3b10f5947b80 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Mon, 6 Jul 2026 22:08:10 +0200 Subject: [PATCH] add update notifications, only when new packages appear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Off by default. Enabling the checkbox requests notification permission (UNUserNotificationCenter); if denied, the checkbox reverts — the auth callback arrives on a background queue, so the UI update hops to main. notifyIfNeeded remembers the last notified set and stays silent while the outdated list is unchanged, so an hourly refresh doesn't re-ping about the same packages. The set resets as packages get upgraded. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 21 +++++++++++++++++++++ BrewBar/SettingsWindowController.swift | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index c6d9522..ba01324 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -1,4 +1,5 @@ import Cocoa +import UserNotifications @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -40,6 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { /// last known outdated list, shown in the Upgrade All confirmation var cachedOutdated: [String] = [] + var lastNotifiedOutdated: Set = [] // MARK: - Menu @@ -266,6 +268,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.updateOutdatedMenu(with: lines) self.updateStatus(count: lines.count) + self.notifyIfNeeded(outdated: lines) if lines.isEmpty { self.setMenuBarIcon("brewbar-uptodate") } else { @@ -274,6 +277,24 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + /// Notifies only when packages appear that weren't in the last notified + /// set — an unchanged list stays silent instead of pinging every refresh. + func notifyIfNeeded(outdated: [String]) { + let current = Set(outdated) + defer { lastNotifiedOutdated = current } + + guard Settings.notifyOnNewUpdates, + !current.subtracting(lastNotifiedOutdated).isEmpty else { return } + + let content = UNMutableNotificationContent() + content.title = "Homebrew updates available" + let shown = outdated.prefix(4).joined(separator: ", ") + content.body = outdated.count > 4 ? "\(shown) and \(outdated.count - 4) more" : shown + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + } + func runAutoUpgrade() { statusMenuItem.title = "Auto-upgrading..." diff --git a/BrewBar/SettingsWindowController.swift b/BrewBar/SettingsWindowController.swift index 198aa6c..8905f34 100644 --- a/BrewBar/SettingsWindowController.swift +++ b/BrewBar/SettingsWindowController.swift @@ -1,5 +1,6 @@ import Cocoa import ServiceManagement +import UserNotifications class SettingsWindowController: NSWindowController { weak var appDelegate: AppDelegate? @@ -18,6 +19,7 @@ class SettingsWindowController: NSWindowController { var greedyCheckbox: NSButton! var autoUpgradeFormulaeCheckbox: NSButton! var autoUpgradeCasksCheckbox: NSButton! + var notifyCheckbox: NSButton! convenience init(appDelegate: AppDelegate) { let window = NSWindow( @@ -108,6 +110,12 @@ class SettingsWindowController: NSWindowController { let caskWarningRow = NSStackView(views: [spacer, caskWarning]) caskWarningRow.orientation = .horizontal + notifyCheckbox = NSButton( + checkboxWithTitle: "Notify when new updates are found", + target: self, + action: #selector(toggleNotify(_:)) + ) + let stack = NSStackView(views: [ frequencyRow, customRow, @@ -117,6 +125,7 @@ class SettingsWindowController: NSWindowController { autoUpgradeCasksCheckbox, caskWarningRow, separator(), + notifyCheckbox, launchAtLoginCheckbox, ]) stack.orientation = .vertical @@ -155,6 +164,7 @@ class SettingsWindowController: NSWindowController { greedyCheckbox.state = Settings.includeGreedyCasks ? .on : .off autoUpgradeFormulaeCheckbox.state = Settings.autoUpgradeFormulae ? .on : .off autoUpgradeCasksCheckbox.state = Settings.autoUpgradeCasks ? .on : .off + notifyCheckbox.state = Settings.notifyOnNewUpdates ? .on : .off } func separator() -> NSBox { @@ -200,6 +210,21 @@ class SettingsWindowController: NSWindowController { Settings.autoUpgradeCasks = sender.state == .on } + @objc func toggleNotify(_ sender: NSButton) { + guard sender.state == .on else { + Settings.notifyOnNewUpdates = false + return + } + + UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in + // callback arrives on a background queue; UI needs main + DispatchQueue.main.async { + Settings.notifyOnNewUpdates = granted + sender.state = granted ? .on : .off + } + } + } + @objc func toggleLaunchAtLogin(_ sender: NSButton) { do { if sender.state == .on {