From 0def852416f7e8a72d07425ce1b35069aed798c9 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Tue, 7 Jul 2026 05:44:32 +0200 Subject: [PATCH 1/2] let brew callers customize the sudo dialog message runBrew gains an optional askpassMessage; the dialog reads "BrewBar needs your administrator password to ." and defaults to naming the brew command. Groundwork for per-cask upgrade prompts that name the app instead of the command. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index c43e4e0..ce7fb7b 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -150,26 +150,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { /// prompt whenever brew needs admin rights (no terminal is attached to /// Process). If two brew commands overlap, the later one's text wins — /// harmless, since prompts realistically only appear during upgrades. - static func writeAskpassScript(for command: String) -> URL { + static func writeAskpassScript(message: String) -> URL { let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("BrewBar", isDirectory: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) // the text lands inside shell single quotes AND an AppleScript string; // whitelist characters that cannot break out of either - let safeCommand = command.filter { $0.isLetter || $0.isNumber || " ._@+=:/-".contains($0) } + let safeMessage = message.filter { $0.isLetter || $0.isNumber || " ._@+=:/-".contains($0) } let url = dir.appendingPathComponent("askpass.sh") let script = """ #!/bin/zsh - osascript -e 'display dialog "BrewBar needs your administrator password to run:\\n\\nbrew \(safeCommand)" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result' + osascript -e 'display dialog "BrewBar needs your administrator password to \(safeMessage)." default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result' """ try? script.write(to: url, atomically: true, encoding: .utf8) try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path) return url } - func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) { + /// askpassMessage customizes the sudo dialog ("...password to ."); + /// defaults to naming the brew command. + func runBrew(_ command: String, askpassMessage: String? = nil, completion: @escaping (String) -> Void = { _ in }) { DispatchQueue.global().async { let brew = self.resolveBrewPath() let cleaned = command.replacingOccurrences(of: "brew ", with: "") @@ -184,7 +186,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { process.arguments = arguments var environment = ProcessInfo.processInfo.environment - environment["SUDO_ASKPASS"] = Self.writeAskpassScript(for: cleaned).path + environment["SUDO_ASKPASS"] = Self.writeAskpassScript(message: askpassMessage ?? "run: brew \(cleaned)").path process.environment = environment let pipe = Pipe() From 0020316dab553ebdedc154708cdc0410cb7bcac0 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Tue, 7 Jul 2026 05:45:35 +0200 Subject: [PATCH 2/2] upgrade casks one at a time so sudo prompts name the app Batched cask upgrades give no per-package hook, so the password dialog could only echo the whole command. upgradeCasksSequentially chains one "upgrade --cask " per outdated cask (from the typed cache), each with an askpass message naming that cask; the status line follows along ("Upgrading tailscale-app..."). Used by both Upgrade All and auto-upgrade; formulae stay batched since they don't need sudo. Explicitly named casks upgrade even when self-updating, so the per-cask commands need no --greedy. Casks that turn outdated only after the preceding brew update surface in the final refetch. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 48 ++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index ce7fb7b..0499a28 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -225,15 +225,39 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusMenuItem.title = "Upgrading..." - // must match the outdated listing: greedy-listed casks are skipped by - // plain "upgrade" and would stay outdated forever - let command = Settings.includeGreedyCasks ? "upgrade --greedy" : "upgrade" - runBrew("update") { _ in - self.runBrew(command) { _ in - // everything was just upgraded; don't chain an auto-upgrade - self.fetchOutdated(allowAutoUpgrade: false) + // 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 upgradeCasksThenRefresh = { + self.upgradeCasksSequentially(caskNames) { + // everything was just upgraded; don't chain an auto-upgrade + self.fetchOutdated(allowAutoUpgrade: false) + } } + + if self.cachedOutdated.formulae.isEmpty { + upgradeCasksThenRefresh() + } else { + self.runBrew("upgrade --formula") { _ in + upgradeCasksThenRefresh() + } + } + } + } + + /// Upgrades casks one at a time so each sudo prompt can name the app it + /// is for — a batched "upgrade --cask" gives no per-package hook. + func upgradeCasksSequentially(_ names: [String], completion: @escaping () -> Void) { + guard let next = names.first else { + completion() + return + } + + statusMenuItem.title = "Upgrading \(next)..." + // an explicitly named cask upgrades even when self-updating, no --greedy needed + runBrew("upgrade --cask \(next)", askpassMessage: "upgrade \(next)") { _ in + self.upgradeCasksSequentially(Array(names.dropFirst()), completion: completion) } } @@ -321,13 +345,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { func runAutoUpgrade(formulae: Bool, casks: Bool) { statusMenuItem.title = "Auto-upgrading..." + let caskNames = casks ? cachedOutdated.casks.map(\.name) : [] let upgradeCasksThenRefresh = { - if casks { - let command = Settings.includeGreedyCasks ? "upgrade --cask --greedy" : "upgrade --cask" - self.runBrew(command) { _ in - self.fetchOutdated(allowAutoUpgrade: false) - } - } else { + self.upgradeCasksSequentially(caskNames) { self.fetchOutdated(allowAutoUpgrade: false) } } @@ -372,7 +392,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func upgradeSingle(_ sender: NSMenuItem) { guard let formula = sender.representedObject as? String else { return } - runBrew("upgrade \(formula)") { _ in + runBrew("upgrade \(formula)", askpassMessage: "upgrade \(formula)") { _ in self.refreshAll() } }