How AutoQuitService Quits Applications When Their Last Window Closes in vorssaint-utils
The AutoQuitService monitors every regular macOS application via Accessibility observers and automatically terminates them using NSRunningApplication.terminate() when delayed verification confirms zero user-facing windows remain.
The vorssaint-utils repository provides a sophisticated window management system that handles application lifecycle automatically. Understanding how AutoQuitService quits applications when their last window closes helps developers leverage macOS Accessibility APIs for intelligent process management.
The Five-Stage Termination Pipeline
The AutoQuitService operates through a coordinated sequence of observation, delay, and verification stages defined in Sources/Vorssaint/Services/AutoQuit/AutoQuitService.swift.
Stage 1: Attaching Accessibility Observers
When the service initializes via syncWithPreferences()—called from FeatureRuntime.swift at startup—it enumerates all running applications and registers an AXObserver for each regular app where NSRunningApplication.activationPolicy == .regular. The attach(_:) method creates these observers and registers notifications listed in appNotifications and windowNotifications arrays (lines 97-30).
Stage 2: Listening to Window Lifecycle Events
For every attached application, the service receives Accessibility notifications such as kAXUIElementDestroyedNotification, kAXWindowCreatedNotification, and kAXApplicationHiddenNotification. The handleAX(_:element:notification:) method processes these events (lines 70-78) and schedules verification checks when refresh-type notifications indicate possible window state changes.
Stage 3: Scheduling Delayed Verification Checks
Because macOS windows may still be fading out during animation, the service implements a triple-check mechanism via scheduleWindowChecks(pid:) (lines 33-42). This method uses closeCheckOffsets = [0.35, 1.0, 2.2] to schedule three successive verifications at 0.35 seconds, 1.0 second, and 2.2 seconds after a close-related event fires, ensuring the window count has stabilized before termination decisions.
Stage 4: Determining Quit Eligibility
During each verification cycle, checkWindows(pid:confirm:) (lines 45-104) performs multi-layer validation:
- Historical Check: Confirms the app previously had windows using the
hadWindows[pid]dictionary - Exception Validation: Verifies the app's bundle identifier is not present in the exceptions list
- Window Detection: Queries for user-facing windows via
standardWindows(of:)(Accessibility API) andhasWindowServerUserWindow(Window Server) - State Assessment: Evaluates hidden-by-close-request states and minimized window status
If the app has zero visible windows and passes all filters, the service invokes NSRunningApplication.terminate(), allowing macOS to display any unsaved-changes dialog before the process exits.
Stage 5: Monitoring Close Button Clicks and Shortcuts
The service distinguishes between crashes and intentional closes through startCloseRequestMonitor() and handleCloseRequestEvent (lines 46-71). When users click window close buttons or press ⌘W, markCloseButtonRequest registers the intent. Programmatic closes from other services like WindowActivator.swift call recordProgrammaticCloseRequest(pid:). Both paths feed into the same delayed-check pipeline.
Exception Handling and Configuration
Applications can be exempted from automatic termination through a persisted list of bundle identifiers stored in user defaults. The reloadExceptions(), addException(_:), and removeException(_:) methods (lines 44-72) manage this blocklist, while mandatory exceptions (including the utility itself) remain hard-coded. The UI layer in AutoQuitSettings.swift exposes these controls to users.
System Integration Points
The AutoQuitService integrates with the broader vorssaint-utils architecture through several key touchpoints:
- Startup:
FeatureRuntime.swiftinvokesAutoQuitService.shared.syncWithPreferences()during application launch - Window Management:
WindowActivator.swiftcallsrecordProgrammaticCloseRequest(pid:)when programmatically closing windows - Uninstallation:
SelfUninstall.swifttriggersAutoQuitService.shared.suspend()to clean up observers before removal
Implementation Examples
// Enable the Auto-Quit feature (typically called by Preferences UI)
AutoQuitService.shared.syncWithPreferences()
// Add Safari to the exception list to prevent auto-termination
AutoQuitService.shared.addException("com.apple.Safari")
// Remove an exception (if not mandatory)
AutoQuitService.shared.removeException("com.apple.Safari")
// Record a programmatic close from another service
if let pid = targetApp.processIdentifier {
AutoQuitService.shared.recordProgrammaticCloseRequest(pid: pid)
}
Summary
- Observer Pattern: The service creates per-process
AXObserverinstances for every regular application to receive Accessibility notifications - Delayed Verification: Three timed checks at 0.35s, 1.0s, and 2.2s prevent premature termination during window animations
- Eligibility Logic: Termination requires zero user-facing windows, historical window presence, and absence from the exception list
- Graceful Exit: Uses
NSRunningApplication.terminate()to trigger standard macOS quit dialogs rather than forceful killing - Close Detection: Distinguishes user-intentional closes from crashes via button-click and programmatic request tracking
Frequently Asked Questions
How does AutoQuitService handle applications that minimize windows instead of closing them?
The checkWindows(pid:confirm:) method specifically evaluates minimized window states using both Accessibility APIs and Window Server queries. Minimized windows count as user-facing windows, preventing termination of apps that simply hide their interface rather than destroy windows.
Can the AutoQuitService terminate system applications or background agents?
No. The attach(_:) method filters for NSRunningApplication.activationPolicy == .regular, which excludes system applications, background agents, and menu bar extras. Only standard user-facing applications with regular activation policies receive observers.
What happens if an application has unsaved changes when AutoQuitService attempts termination?
The service calls NSRunningApplication.terminate(), which triggers the standard macOS application termination sequence. macOS intercepts this call to display save dialogs, allowing users to preserve unsaved work before the application exits.
How does the service distinguish between a window closing and an application crashing?
The service monitors intentional close signals through markCloseButtonRequest (physical button clicks) and recordProgrammaticCloseRequest (API-driven closes). These requests set state flags that checkWindows(pid:confirm:) evaluates alongside window existence. Crashes generate accessibility destruction notifications without corresponding close requests, preventing automatic relaunch or termination confusion.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →