Zonogy

Implementation Details

Destroyed Window Detection

Beyond the self-evident path of app termination (which removes all windows for that PID immediately), Zonogy uses several mechanisms to detect individual window destruction. Not all applications emit didTerminateApplication or AX destroy notifications (e.g., Find My) so we need to cast a wider net. The sync-based mechanisms detect a window as destroyed when its (pid, CGWindowID) is missing from CGWindowListCopyWindowInfo, or its AXUIElement returns .invalidUIElement/.cannotComplete/.illegalArgument when probed.

Deferred Pruning

All window removal paths that treat the backing as gone except app termination use deferred pruning: instead of immediately discarding the window’s identity and recency info, the window is staged in a pending-prune store keyed by (pid, CGWindowID). The zone is vacated immediately (placeholder appears), but the bookkeeping is retained. This guards against false positives from transient AX unavailability (e.g., sleep/wake, screen topology changes, or spurious AXUIElementDestroyed notifications macOS can emit near sleep). Native-tab source collapse is different: the source managed record is removed because its backing was adopted by the destination managed record, not because the backing disappeared.

Floating Zone Protection Windows

When a window is placed in the floating zone, it receives a 0.5-second protection window during which focus/front-most changes will not trigger occlusion-based floating-zone minimization. If a spurious focus event occurs during this window (e.g., macOS activating a sibling window after the displaced occupant is minimized), the floating-zone occupant is reactivated/raised so it remains visible and interactive. This prevents a newly placed window from being immediately dismissed. Exception: if the floating window is currently minimized (per the Accessibility API), the protection-driven re-raise is skipped, so a user who quickly minimizes a just-placed floating window is not fought by a spurious unminimize.

The same protection mechanism applies when restoring layouts from sleep/wake recovery or WinShot snapshots, so that internal restore operations do not fight normal layout behavior.

For ActiveFit candidate zones during restore, we temporarily suppress ActiveFit during the restore layout pass and then evaluate it once for the active window after the restore settles.

Occlusion-Based Floating Zone Minimization

When a managed window assigned to a tiling zone becomes front-most on a screen, Zonogy checks whether that screen’s floating-zone occupant is occluded by that occupied tiling zone. If it is occluded, minimize the floating window; otherwise leave it unminimized. If a placeholder becomes front-most and its tiling zone’s frame overlaps the floating-zone occupant, promote the floating window into that placeholder’s tiling zone instead of minimizing it.

Implementation notes:

Directional Navigation Geometry

Both Control-Command directional gestures share one pure geometric selector, DirectionalRectNavigation: given a source rectangle and a direction, it returns the nearest rectangle ahead, preferring a neighbor that overlaps the source along the perpendicular edge and falling back to center distance so diagonally-placed displays stay reachable.

Displacement Minimization Strategy

When a placement displaces an existing zone occupant, Zonogy picks one of two ways to minimize the displaced window:

DeferredMinimizationCoordinator is also used by occlusion- and focus-driven floating-zone minimization and the floating-zone explicit minimizeOccupant path. Queued minimizations are cancelled if the window is reassigned to any zone before the timer fires.

Loop guard safety net

MinimizeLoopGuard catches the rare case where a synchronous-path minimize is rapidly re-unminimized by an app outside any launch burst. When two non-suppressed deminiaturize events arrive within 2 seconds for windows Zonogy programmatically minimized in the previous 0.5 seconds, the guard activates for 3 seconds; while active, minimizeWindowProgrammatically routes through the deferred queue regardless of the placement’s requested strategy.

Additional Notes

Slow AX Call Logging

Every synchronous Accessibility API call (e.g., AXUIElementCopyAttributeValue, AXUIElementSetAttributeValue, AXUIElementPerformAction, AXObserverCreate, AXObserverAddNotification) is wrapped in a timing helper. Calls exceeding 0.1s emit a single [SLOW-AX] line with the function name, attribute/action, duration (took Nms), AX status, target pid + bundle, and a thread=main/thread=bg tag; calls under the threshold are silent so normal operation adds no log noise. The thread= tag distinguishes main-thread blocks (which surface as freezes) from background-queue blocks (which show up as stalled UI updates).

To inspect slow calls in /tmp/zonogy-debug.log:

Reducing Accessibility API Cost

Each synchronous Accessibility API call is an inter-process request: the target application must be scheduled, read its own state, and reply. When Zonogy issues many such calls per second across many tracked windows, this drains battery both directly (Zonogy’s own CPU work) and indirectly (waking applications that macOS’s App Nap would otherwise leave idle). The mechanisms below keep that volume low without changing user-visible behavior.

Liveness-check cache for prune

The destroyed-window prune pass runs on every full sync. For each tracked window it first checks CGWindowListCopyWindowInfo (cheap, no per-app accessibility IPC); if the window is still listed, it falls back to an accessibility safety-net read for the rare “still listed but accessibility element invalid” case.

The safety-net is throttled by a per-window timestamp cache with a 5-second time-to-live. The cache is also refreshed at notification dispatch time: any incoming AX move, resize, miniaturize, deminiaturize, focus-change, or main-window-change notification for a tracked window is itself proof the element is alive, so the corresponding cache entry is refreshed without an additional read.

Skipping the safety-net for minimized windows

Even with the cache, the safety-net still fires for windows that haven’t received recent AX notifications. When such a window is minimized, the read is wasted work: Zonogy isn’t acting on its accessibility state, and the target application is more likely to be in App Nap, so the read forces a wake-up to confirm something the user can’t observe. The safety-net is skipped entirely for minimized windows. CGWindowListCopyWindowInfo still runs unconditionally, so the primary destruction signal is unchanged.

Single-pass window placement when the first pass settles

When applying a target frame, the placement code first chooses a position-vs-size order so the in-progress frame stays inside the visible screen, applies it, and reads back the resulting frame to verify. If the first pass produced the target frame, the move is done. Otherwise, it follows up with the opposite order as a recovery step, falling through to the existing retry chain if needed.

Reusing the current frame read

The move pipeline reads the window’s current frame once at entry. The same value serves both the skip-if-at-target check and the placement step’s apply-order decision.

Accessibility API Workarounds

Retry Mechanisms Tied to Accessibility

Zonogy uses five narrowly scoped retry/verification mechanisms to cope with AX timing and consistency issues: three are PID/application-scoped and two are per window. All of them are tied to concrete events (no global polling loops) and are explicitly cancelled when they are no longer needed or when the system goes to sleep.

AXWindowCreated for already-tracked windows (element rebinding)

Some applications can emit AXWindowCreated for a window Zonogy already tracks (same PID + CGWindowID) (eg Word). In these cases the notification may carry a fresh AXUIElement for the same underlying window. Zonogy must treat this as an element-rebind event (update the stored AX element, lookup mapping, and window notification registrations atomically), not as a new-window capture and not as a capture failure.

The same kind of false signal can also surface as an AXUIElementDestroyed notification: some applications (e.g. Finder) emit it while the window stays open — sometimes the original element keeps working, sometimes the app recycles in a fresh element. So before acting on AXUIElementDestroyed for a tracked window, Zonogy consults the WindowServer (CGWindowListCopyWindowInfo) — the ground truth for whether the window still exists. If the window is gone, it proceeds with deferred pruning. If the window is still present, Zonogy keeps it in its zone and makes sure it holds a live AX element: it keeps the element it already has when that element still resolves to the window, and otherwise rebinds to a fresh element the application recycled in. Only when the window appears present but no live element can be bound at all does it fall back to deferred pruning (which still recovers the window if its element reappears on a later capture pass).

User vs programmatic move/resize attribution

AX move/resize notifications (kAXMovedNotification, kAXResizedNotification) do not indicate whether the change was triggered by:

Zonogy handles this with two complementary mechanisms:

This attribution work is used by:

Window subrole for minimized windows

Some applications report the subrole for their minimized windows as AXDialogSubrole even if it later becomes kAXStandardWindowSubrole upon un-minimization. So for enumeration of windows to manage, we don’t check subrole for minimized windows.

Async unminimize after pre-positioning (“pre-move” feature)

When unminimizing a window that needs to appear at a specific position (e.g., restoring a WinShot snapshot or selecting a minimized window from Launcher), we first set the window’s position and size while the window is still minimized. However, if we unminimize synchronously right after setting position/size, the window sometimes visually appears at its old location before snapping to the correct position. To address this, we default to async mode for unminimization.

Focusing a specific window of another application

To focus a specific window, we set the window’s kAXMainAttribute, perform kAXRaiseAction, and only then call app.activate(). (The order matters. Requesting activation first seems to restore whichever window the application last had frontmost, overriding the raise.)

Floating zone activation workaround

When placing a window into the floating zone, the window may fail to receive focus and appear behind tiled windows. Since the floating zone floats above tiled zones, this is the only placement where another window can obscure the placed window. The workaround (in activateFloatingZoneWindow) is to call NSApp.activate(ignoringOtherApps: true) to activate Zonogy first, then yield to the run loop via DispatchQueue.main.async before the make-main / raise / app.activate() sequence described above.

Full-screen pause

Zonogy detects native macOS full-screen windows using the (undocumented)AXFullScreen accessibility attribute for native-full screen mode (ie green-button kind), and with additional detection for non-native-full screen. The big picture intent is to “pause” Zonogy (no UI, no targeting) on a screen in full-screen mode, and target another screen instead.

Native full-screen

We listen to kAXResizedNotification which fires when windows enter/exit full-screen mode, and query the AXFullScreen attribute via AXUIElementCopyAttributeValue. We use 250ms debounce. (Of course, we also handle window closure and app termination.)

At startup (after window capture) and after display reconfiguration, we also iterate all managed windows and check their AXFullScreen attribute. We also re-scan full-screen state after active Space changes, since some apps (e.g., Safari video) don’t emit resize events for their full-screen windows. This rescan is debounced (250ms) and uses the same AXFullScreen query pipeline.

When a screen has a native full-screen window, MacOS creates a new Space for it on that screen. Although Zonogy’s pipelines try to target another screen, we can’t completely stop windows opening in the screen that’s in full-screen mode. If a window opens on that screen, MacOS switches Spaces. This is undesired–instead we want to place any managed window opened in that way into a zone in another (non-full-screen) screen and go back to the full-screen Space so we are not interrupted. (For example, we are watching a movie in full-screen, and doing other things at the same time on another screen.) To get this behavior, we monitor Spaces through another private API (see “CGS Spaces membership query” below).

Non-native (heuristic) full screen

For managed apps with exception treatAXUnknownFullWidthAsFullScreen: for windows whose AX subrole is AXUnknown (some presentation-style windows like Keynote full-screen), we treat them as full-screen if their accessibility frame width matches the screen width exactly.

CGS Spaces membership query (native full-screen only)

Going back to the full-screen Space (per the previous section) depends on knowing whether that Space still exists while another Space is showing on the same screen. AXFullScreen tells us a window claims to be full-screen, but it doesn’t say whether its full-screen Space is the active one. The standard on-screen window list (CGWindowListCopyWindowInfo) doesn’t help either — it only includes windows in the currently active Space, so a window on an inactive full-screen Space looks the same as one that has exited full-screen.

CGS Spaces (CGSCopySpacesForWindows + CGSSpaceGetType == kCGSSpaceFullscreen) answers the question directly: it reports which Spaces a window currently belongs to. We rely on it in places where we would otherwise mistakenly drop Zonogy’s full-screen pause.


Timers

For a complete inventory of all timers and delay mechanisms (AX retries, debounce, protection windows, etc.), see SPECIFICATION-TIMERS.md.