# How AutoQuitService Quits Applications When Their Last Window Closes in vorssaint-utils

> Discover how AutoQuitService in vorssaint-utils uses Accessibility observers to automatically quit macOS apps when their last window closes, ensuring efficient resource management.

- Repository: [vorssaint/vorssaint-utils](https://github.com/vorssaint/vorssaint-utils)
- Tags: internals
- Published: 2026-09-11

---

**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`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/Services/AutoQuit/AutoQuitService.swift).

### Stage 1: Attaching Accessibility Observers

When the service initializes via `syncWithPreferences()`—called from [`FeatureRuntime.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/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`](https://github.com/vorssaint/vorssaint-utils/blob/main/FeatureRuntime.swift) invokes `AutoQuitService.shared.syncWithPreferences()` during application launch
- **Window Management**: [`WindowActivator.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/WindowActivator.swift) calls `recordProgrammaticCloseRequest(pid:)` when programmatically closing windows
- **Uninstallation**: [`SelfUninstall.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/SelfUninstall.swift) triggers `AutoQuitService.shared.suspend()` to clean up observers before removal

## Implementation Examples

```swift
// 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.