AutoQuit is a macOS menu bar app (not iOS) that automatically quits apps which have been idle longer than a configurable threshold (default 8h). It lives in the menu bar only (LSUIElement = YES, no Dock icon) via SwiftUI's MenuBarExtra. Each running app shows a countdown and a per-app opt-out checkbox.
Open AutoQuit.xcodeproj in Xcode (scheme AutoQuit), or from CLI:
# Build (DEVELOPMENT_TEAM is blank, so a CLI build needs signing disabled)
xcodebuild -project AutoQuit.xcodeproj -scheme AutoQuit -configuration Debug \
build CODE_SIGNING_ALLOWED=NO
# Test the logic suite (single test: append /AutoQuitTests/testName)
xcodebuild test -project AutoQuit.xcodeproj -scheme AutoQuit \
-destination 'platform=macOS' -only-testing:AutoQuitTests CODE_SIGNING_ALLOWED=NO- Target: macOS 13.0+, Swift 5.0, bundle id
com.AutoQuit. Info.plistis empty on disk — keys are generated from build settings (GENERATE_INFOPLIST_FILE = YES). Edit Info keys in the target build settings, not the plist.AutoQuitTestscovers the pure logic —QuitDecision.shouldQuit(idle/opt-out/launch boundaries) andIdleTimeformatting (locale pinned toen_USso the asserted strings don't depend on the host locale). The UI-test targets are still Xcode-template stubs, and their runner can't launch unsigned — that's why the test command scopes to-only-testing:AutoQuitTests.
Almost everything lives in ContentView.swift; AutoQuitApp.swift is just the @main shell.
-
Single global manager.
AutoQuitApp.swiftcreates one file-scopelet runningAppsManager = RunningAppsManager()and injects it into every view. There is no DI — this global is the source of truth. -
RunningAppsManager(ObservableObject) is the whole engine (ContentView.swift:31):runningApps: [NSRunningApplication: Date]maps each app to its last-active timestamp. An app is quit whennow - timestamp > hoursUntilClose.- A 1-second
TimercallscheckOpenApps()(ContentView.swift:172) — but only when one of the app's windows is key/main, or at least once every 60s otherwise (tracked vialastChecked). This keeps the countdown UI live while the menu is open without polling constantly in the background. This is the core loop; the terminate decision is here. - Subscribes to
NSWorkspace.didDeactivateApplicationNotificationto stamp the moment an app loses focus. isBlockedApp()excludes background/menu-bar-only apps (activationPolicy != .regular), AutoQuit itself, and a hardcoded list of Apple system bundle ids (Finder, Dock, Spotlight, Siri, etc.). This is what the CHANGELOG fixes refer to — apps like Bartender/CleanShot were wrongly terminated before this filter.
-
Opt-out is keyed by
bundleIdentifier(falling back tolocalizedName, then"") viaNSRunningApplication.toggleKey. Reads (willAutoQuit) also check a legacylocalizedNamekey so opt-outs saved before this change still apply.toggleStatus: [String: Bool]is JSON-encoded into an@AppStorageDatablob (com.AutoQuit.toggleStatus). MutatingtoggleStatusauto-persists via itsdidSet(which callssaveToggleStatus()) — callers no longer invokesaveToggleStatus()by hand. -
Settings open in a separate
NSWindowmanaged bySettingsWindowController, a singleton tracked via its static.currentso a second click re-focuses the existing window instead of opening another. -
Popover footer bulk actions. Close all selected / Force close all selected (above Settings in the footer) terminate every app whose toggle is on —
runningApps.keysfiltered bywillAutoQuit— viaterminate()/forceTerminate(), the same calls the per-row buttons use. No manual cleanup: the 1s timer prunes apps once they've quit. Both disable when nothing is selected.
All state is @AppStorage (UserDefaults): hoursUntilClose: Int, showCloseButton: Bool, and the encoded toggleStatus blob.
The default for hoursUntilClose lives in one place — AppDefaults.hoursUntilClose (8) — and every @AppStorage("hoursUntilClose") declaration (manager, AppRow, SettingsView) references it, so the defaults can't drift apart.
None. Launch-at-login is handled by LaunchAtLoginToggle (ContentView.swift), a small native wrapper around SMAppService.mainApp (ServiceManagement, macOS 13+). This replaced the former LaunchAtLogin-Modern SPM package.