Merge branch 'feature/cleanup'

Resolved by combining both sides in upgradeSingle (two-param runBrew
closure from fix/offline-refresh + cleanup hook from feature/cleanup)
and adapting the cleanup branch's runBrew calls to the new signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-13 18:36:08 +02:00
commit 6a1c92a6fd
3 changed files with 62 additions and 4 deletions

View file

@ -82,6 +82,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
outdatedItem.submenu = outdatedSubmenu
outdatedItem.isHidden = true // shown once a fetch finds outdated packages
menu.addItem(outdatedItem)
menu.addItem(NSMenuItem(title: "🧹 Cleanup", action: #selector(cleanupAction), keyEquivalent: ""))
// isAlternate swaps this in for the item right above while is held
let purgeItem = NSMenuItem(title: "🧹 Cleanup (purge cache)", action: #selector(cleanupPurgeAction), keyEquivalent: "")
purgeItem.keyEquivalentModifierMask = .option
purgeItem.isAlternate = true
menu.addItem(purgeItem)
menu.addItem(NSMenuItem(title: "📜 Logs", action: #selector(showLogs), keyEquivalent: "l"))
menu.addItem(NSMenuItem.separator())
@ -271,6 +277,34 @@ class AppDelegate: NSObject, NSApplicationDelegate {
refreshAll()
}
@objc func cleanupAction() {
runCleanup(purgeCache: false)
}
@objc func cleanupPurgeAction() {
runCleanup(purgeCache: true)
}
/// --prune=all also deletes cached downloads brew would normally keep
/// (current versions); the only cost is re-downloading at next install
func runCleanup(purgeCache: Bool) {
statusMenuItem.title = "Cleaning up..."
runBrew(purgeCache ? "cleanup --prune=all" : "cleanup") { _, _ in
self.updateStatus(count: self.cachedOutdated.count)
}
}
/// Interposed between the end of an upgrade and its closing refetch;
/// passes straight through unless the user opted in.
func cleanupAfterUpgradeIfEnabled(completion: @escaping () -> Void) {
guard Settings.cleanupAfterUpgrade else {
completion()
return
}
statusMenuItem.title = "Cleaning up..."
runBrew("cleanup") { _, _ in completion() }
}
@objc func upgradeAll() {
if Settings.confirmBeforeUpgradeAll {
guard confirmUpgrade() else { return }
@ -284,10 +318,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let caskNames = self.cachedOutdated.casks.map(\.name)
let upgradeCasksThenRefresh = {
self.upgradeCasksSequentially(caskNames) {
self.cleanupAfterUpgradeIfEnabled {
// everything was just upgraded; don't chain an auto-upgrade
self.fetchOutdated(allowAutoUpgrade: false)
}
}
}
if self.cachedOutdated.formulae.isEmpty {
upgradeCasksThenRefresh()
@ -409,9 +445,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let caskNames = casks ? cachedOutdated.casks.map(\.name) : []
let upgradeCasksThenRefresh = {
self.upgradeCasksSequentially(caskNames) {
self.cleanupAfterUpgradeIfEnabled {
self.fetchOutdated(allowAutoUpgrade: false)
}
}
}
if formulae {
runBrew("upgrade --formula") { _, _ in
@ -454,9 +492,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
guard let formula = sender.representedObject as? String else { return }
runBrew("upgrade \(formula)", askpassMessage: "upgrade \(formula)") { _, _ in
self.cleanupAfterUpgradeIfEnabled {
self.refreshAll()
}
}
}
/// Refreshing offline would silently succeed on stale data: brew update
/// fails but its exit code was never checked, and brew outdated compares

View file

@ -44,4 +44,9 @@ enum Settings {
get { UserDefaults.standard.bool(forKey: "confirmBeforeUpgradeAll") }
set { UserDefaults.standard.set(newValue, forKey: "confirmBeforeUpgradeAll") }
}
static var cleanupAfterUpgrade: Bool {
get { UserDefaults.standard.bool(forKey: "cleanupAfterUpgrade") }
set { UserDefaults.standard.set(newValue, forKey: "cleanupAfterUpgrade") }
}
}

View file

@ -22,6 +22,7 @@ class SettingsWindowController: NSWindowController {
var notifyCheckbox: NSButton!
var showCountCheckbox: NSButton!
var confirmCheckbox: NSButton!
var cleanupCheckbox: NSButton!
convenience init(appDelegate: AppDelegate) {
let window = NSWindow(
@ -125,6 +126,12 @@ class SettingsWindowController: NSWindowController {
action: #selector(toggleConfirm(_:))
)
cleanupCheckbox = NSButton(
checkboxWithTitle: "Run brew cleanup after upgrades",
target: self,
action: #selector(toggleCleanup(_:))
)
let stack = NSStackView(views: [
frequencyRow,
customRow,
@ -138,6 +145,7 @@ class SettingsWindowController: NSWindowController {
notifyCheckbox,
showCountCheckbox,
confirmCheckbox,
cleanupCheckbox,
launchAtLoginCheckbox,
])
stack.orientation = .vertical
@ -179,6 +187,7 @@ class SettingsWindowController: NSWindowController {
notifyCheckbox.state = Settings.notifyOnNewUpdates ? .on : .off
showCountCheckbox.state = Settings.showCountInMenuBar ? .on : .off
confirmCheckbox.state = Settings.confirmBeforeUpgradeAll ? .on : .off
cleanupCheckbox.state = Settings.cleanupAfterUpgrade ? .on : .off
}
func separator() -> NSBox {
@ -311,6 +320,10 @@ class SettingsWindowController: NSWindowController {
Settings.confirmBeforeUpgradeAll = sender.state == .on
}
@objc func toggleCleanup(_ sender: NSButton) {
Settings.cleanupAfterUpgrade = sender.state == .on
}
@objc func toggleLaunchAtLogin(_ sender: NSButton) {
do {
if sender.state == .on {