--greedy exists on both brew commands: outdated --greedy lists
self-updating casks, upgrade --greedy actually upgrades them (plain
upgrade skips them, per brew's own help). The listing and auto-upgrade
paths had the flag but manual Upgrade All didn't, so greedy-listed
casks would survive Upgrade All and reappear as outdated forever.
Single-package upgrades don't need it: brew upgrades an explicitly
named cask even when it self-updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New "Ask for confirmation before Upgrade All" checkbox; when disabled,
Upgrade All runs immediately. Auto-upgrade is unaffected either way —
it never prompts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Off by default. The status item button shows both image and title
(imagePosition = .imageLeft), so the icon stays visible with the count
beside it. Cleared when up to date or when the setting is off; toggling
applies immediately from the cached list without refetching.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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>
brew outdated normally skips casks that auto-update themselves; with
includeGreedyCasks enabled (the default) BrewBar passes --greedy so they
show up too. Toggling the checkbox refetches immediately since the list
semantics change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkbox registers/unregisters SMAppService.mainApp (macOS 13+ API, no
helper app needed). Deliberately not mirrored in UserDefaults: the
service status is the single source of truth, and syncUI reads it live
so changes made in System Settings > Login Items stay in sync. Failures
revert the checkbox and surface the error in an alert.
Note: registration points at the app bundle's current path, so it only
behaves properly once BrewBar runs from a stable location like
/Applications — debug builds may register the build folder copy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SettingsWindowController (programmatic NSWindow, same pattern as
LogWindowController) with a preset popup and a custom-minutes field for
the refresh frequency. Changes apply immediately, per macOS convention.
The ⏱ Refresh Interval submenu, its custom text-field-in-menu view and
the two backing actions are removed in favor of "⚙️ Settings…" (Cmd+,).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings enum holds all upcoming settings-window preferences. Booleans
default to false (disabled by default); includeGreedyCasks and
confirmBeforeUpgradeAll default to true via register(defaults:), which
provides fallbacks without writing to disk. Launch-at-login is not
stored here — SMAppService will be its source of truth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same treatment as the Outdated item: upgradeAllItem becomes a property,
starts hidden, and updateOutdatedMenu shows it only when the fetch
returns outdated packages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote outdatedItem to a property and toggle isHidden from
updateOutdatedMenu: hidden at launch and whenever a fetch returns no
outdated packages, visible otherwise. The "All up to date" placeholder
inside the submenu is dropped — the status line already says it, and
the item is invisible when the list is empty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The main Refresh action already refetches everything; the submenu
button duplicated it. Its only handler, forceRefreshOutdated, is
removed too as nothing else referenced it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
runBrew built a command string and ran it through zsh -c, so the shell
interpreted the whole line — any metacharacter in an interpolated
package name (;, $(), backticks) would have been executed. Names come
from brew itself so exploitation was unlikely, but the injection class
is now gone: Process gets an argument array via /usr/bin/env, which
resolves brew on PATH (covering the bare "brew" fallback) and execs it
with no shell in between. Also slightly faster per invocation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every brew command appended its full output to logBuffer and nothing
ever trimmed it, so with the hourly refresh timer the buffer grew
forever. Memory aside, each log() call re-renders the entire buffer
into the log window's NSTextView, so logging got slower over time.
Keep only the most recent 100k characters, dropping the oldest content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refreshOutdatedList (the 5-second cache path) was never called by any
menu item or code path, so the cache logic never ran. lastOutdatedFetch
was only written. cachedOutdated stays: the Upgrade All confirmation
dialog uses it to list pending packages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brew upgrade also upgrades casks, and replacing a cask quits the running
app (e.g. the browser) with no warning. Upgrade All now shows an NSAlert
listing the outdated packages and warning that running apps may be
closed, with a Cancel option.
NSApp.activate is needed because BrewBar is an accessory app (no Dock
icon) — without it the modal alert can appear behind other windows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brew runs inside a Process with no terminal attached, so any sudo prompt
(cask upgrades, some installers) would hang or fail silently — there was
nowhere to type the password.
Set SUDO_ASKPASS to a small helper script written to Application Support
at launch. When sudo detects no tty, it runs the helper, which shows a
native macOS password dialog via osascript and prints the answer for
sudo to consume. Cancelling the dialog makes sudo fail cleanly instead
of hanging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completions were invoked on a background queue. Most call sites wrapped
their UI work in DispatchQueue.main.async, but upgradeSingle's completion
calls refreshAll(), which sets statusMenuItem.title directly — an AppKit
mutation off the main thread. It also raced on cachedOutdated /
lastOutdatedFetch (written from background, read from main).
runBrew now always dispatches its completion to the main queue, so every
caller can safely touch UI, and the now-redundant inner main.async hops
are removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract brew output parsing into BrewParser.swift (pure Swift, no AppKit)
and add BrewBarTests target with 7 passing tests covering parseOutdated
and parseVersion edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>