add auto-upgrade settings, split into formulae and casks
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 <noreply@anthropic.com>
This commit is contained in:
parent
bd19bee52e
commit
7c53413e40
|
|
@ -213,7 +213,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
runBrew("update") { _ in
|
runBrew("update") { _ in
|
||||||
self.runBrew("upgrade") { _ 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
|
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")
|
setMenuBarIcon("brewbar-updating")
|
||||||
|
|
||||||
// --greedy also lists casks that self-update (brew skips them by default)
|
// --greedy also lists casks that self-update (brew skips them by default)
|
||||||
|
|
@ -253,6 +257,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
self.cachedOutdated = lines
|
self.cachedOutdated = lines
|
||||||
|
|
||||||
|
if allowAutoUpgrade, !lines.isEmpty,
|
||||||
|
Settings.autoUpgradeFormulae || Settings.autoUpgradeCasks
|
||||||
|
{
|
||||||
|
self.runAutoUpgrade()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.updateOutdatedMenu(with: lines)
|
self.updateOutdatedMenu(with: lines)
|
||||||
self.updateStatus(count: lines.count)
|
self.updateStatus(count: lines.count)
|
||||||
if lines.isEmpty {
|
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]) {
|
func updateOutdatedMenu(with lines: [String]) {
|
||||||
outdatedSubmenu.removeAllItems()
|
outdatedSubmenu.removeAllItems()
|
||||||
outdatedItem.isHidden = lines.isEmpty
|
outdatedItem.isHidden = lines.isEmpty
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ class SettingsWindowController: NSWindowController {
|
||||||
var customMinutesField: NSTextField!
|
var customMinutesField: NSTextField!
|
||||||
var launchAtLoginCheckbox: NSButton!
|
var launchAtLoginCheckbox: NSButton!
|
||||||
var greedyCheckbox: NSButton!
|
var greedyCheckbox: NSButton!
|
||||||
|
var autoUpgradeFormulaeCheckbox: NSButton!
|
||||||
|
var autoUpgradeCasksCheckbox: NSButton!
|
||||||
|
|
||||||
convenience init(appDelegate: AppDelegate) {
|
convenience init(appDelegate: AppDelegate) {
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
|
|
@ -81,11 +83,40 @@ class SettingsWindowController: NSWindowController {
|
||||||
action: #selector(toggleGreedy(_:))
|
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: [
|
let stack = NSStackView(views: [
|
||||||
frequencyRow,
|
frequencyRow,
|
||||||
customRow,
|
customRow,
|
||||||
greedyCheckbox,
|
greedyCheckbox,
|
||||||
separator(),
|
separator(),
|
||||||
|
autoUpgradeFormulaeCheckbox,
|
||||||
|
autoUpgradeCasksCheckbox,
|
||||||
|
caskWarningRow,
|
||||||
|
separator(),
|
||||||
launchAtLoginCheckbox,
|
launchAtLoginCheckbox,
|
||||||
])
|
])
|
||||||
stack.orientation = .vertical
|
stack.orientation = .vertical
|
||||||
|
|
@ -122,6 +153,8 @@ class SettingsWindowController: NSWindowController {
|
||||||
// this in System Settings > Login Items while we weren't looking
|
// this in System Settings > Login Items while we weren't looking
|
||||||
launchAtLoginCheckbox.state = SMAppService.mainApp.status == .enabled ? .on : .off
|
launchAtLoginCheckbox.state = SMAppService.mainApp.status == .enabled ? .on : .off
|
||||||
greedyCheckbox.state = Settings.includeGreedyCasks ? .on : .off
|
greedyCheckbox.state = Settings.includeGreedyCasks ? .on : .off
|
||||||
|
autoUpgradeFormulaeCheckbox.state = Settings.autoUpgradeFormulae ? .on : .off
|
||||||
|
autoUpgradeCasksCheckbox.state = Settings.autoUpgradeCasks ? .on : .off
|
||||||
}
|
}
|
||||||
|
|
||||||
func separator() -> NSBox {
|
func separator() -> NSBox {
|
||||||
|
|
@ -159,6 +192,14 @@ class SettingsWindowController: NSWindowController {
|
||||||
appDelegate?.fetchOutdated() // the list contents just changed meaning
|
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) {
|
@objc func toggleLaunchAtLogin(_ sender: NSButton) {
|
||||||
do {
|
do {
|
||||||
if sender.state == .on {
|
if sender.state == .on {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue