requestAuthorization only shows the system prompt once ever; when
permission is already denied it silently returns false and the checkbox
just snapped off with no explanation. toggleNotify now checks the
authorization status first: already-authorized enables directly,
not-determined triggers the system prompt (errors surfaced in an
alert), and denied shows an alert with an Open System Settings button
pointing at the Notifications pane.
Also adds a UNUserNotificationCenterDelegate so banners are shown even
while BrewBar is the active app — otherwise macOS suppresses them,
which reads as "notifications don't work" right after enabling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Monospaced font and semantic colors (dark-mode correct), visible
vertical scroller (was missing), text inset, centered window with a
proper title. Autoscroll now only follows the tail when already at the
bottom instead of yanking the scroll position on every log line.
Also sets isReleasedWhenClosed = false, fixing a latent crash on
close-then-reopen (the settings window already had this guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Accessory apps never appear in the Cmd-Tab switcher and macOS does not
activate them when they open windows — the log window opened behind
other apps (settings only worked via a manual activate call).
presentWindow(of:) now switches the activation policy to .regular
while a BrewBar window is open, giving a Dock icon and a Cmd-Tab entry;
a willClose observer drops back to .accessory once the last window is
gone. Used by both showLogs and showSettings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batched cask upgrades give no per-package hook, so the password dialog
could only echo the whole command. upgradeCasksSequentially chains one
"upgrade --cask <name>" 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 <noreply@anthropic.com>
runBrew gains an optional askpassMessage; the dialog reads "BrewBar
needs your administrator password to <message>." 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 <noreply@anthropic.com>
The askpass script is now rewritten before each brew invocation with
the current command baked into the dialog text ("...to run: brew
upgrade tailscale-app") instead of a generic message written once at
launch. The command text is whitelisted to characters that cannot
break out of the shell/AppleScript quoting it is embedded in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetchOutdated returned before refreshing the UI when auto-upgrade
triggered, so the menu showed stale state for the whole upgrade. Now
the fetched list lands in the menu, status line and menu bar count
first; runAutoUpgrade then overwrites the status with its own message.
Notification stays after the post-upgrade refetch — leftovers only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetchOutdated now uses --json and the typed parser, so cachedOutdated
knows formulae from casks. The auto-upgrade trigger checks each kind
against its own toggle: an outdated cask no longer starts a pointless
"upgrade --formula" run (previously any outdated package triggered
whichever auto-upgrades were enabled). runAutoUpgrade takes explicit
formulae/casks flags instead of re-reading settings.
Bonus from the version info now available: outdated menu items and the
Upgrade All confirmation show "name installed → current" instead of
bare names. The legacy line parser and its tests are removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parseOutdatedJSON decodes brew outdated --json into OutdatedPackages,
which separates formulae from casks — the plain-text output is just
names with no type information. Codable structs mirror the JSON shape,
with convertFromSnakeCase mapping installed_versions to Swift naming.
Undecodable input (brew error text, empty output) parses as .none.
The legacy line parser stays until callers switch over next commit.
Covered by 4 new unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a note under the greedy checkbox spelling out what self-updating
casks are and that the setting affects both the outdated listing and
upgrades (Upgrade All and auto-upgrade). The indented-gray-note layout
is extracted into a noteRow helper, now shared with the cask warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--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>