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) and hasWindowServerUserWindow (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.swift invokes AutoQuitService.shared.syncWithPreferences() during application launch
  • Window Management: WindowActivator.swift calls recordProgrammaticCloseRequest(pid:) when programmatically closing windows
  • Uninstallation: SelfUninstall.swift triggers AutoQuitService.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 AXObserver instances 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →