When the Stars Align (Redux)

Yesterday I published “When the Stars Align,” in which I described how I’d implemented instant space switching in ActiveSpace — my menu bar utility that shows which macOS Space you’re on and lets you switch between them. The technique, adapted from two open-source projects (Joshua Li’s iss - Instant Space Switcher and Justin Proulx’s InstantSpaceSwitcher), used synthetic dock-swipe gesture events to trick the Dock into switching spaces without the sliding animation. It was fast. It was clean. It worked perfectly.

For about six hours.

The Problem

The morning session had been conducted on my office setup: a MacBook Pro connected to two Dell U2723QE monitors, laptop lid closed. In that configuration, the synthetic gesture technique was flawless. Instant transitions, full desktop restoration, menu bar intact, keyboard shortcuts responsive. I shipped it, wrote the blog post, and went home feeling like I’d cracked something genuinely useful.

At home, I plugged into a single LG UltraFine display. Laptop lid closed. One monitor instead of two. Everything else identical.

The space switching was instant — the space indicator in the menu bar updated immediately. But the desktop didn’t follow. The windows from the target space didn’t appear. The menu bar lost its application menus. Switching back showed an empty desktop. The CGS layer knew the space had changed, but the compositor simply refused to repaint.

I clicked where a window should have been. It appeared. I clicked another empty area where another window should have been. That appeared too. The windows were there — the window server knew about them, they were in the right z-order, at the right coordinates — but the compositor wasn’t rendering their surfaces until they received a direct interaction event.

This was not a code bug. This was a macOS 16 behaviour difference between display configurations.

The Investigation

The first thing I checked was the display identifier. On the two-monitor setup, CGSCopyManagedDisplaySpaces returned UUID-based display identifiers — long hex strings like B8E129CC-DDDF-44D9-A583-6DE0FB39319E. On the single monitor, it returned the literal string Main. That felt significant, but I didn’t yet understand why.

I built a standalone C test tool that replicated the gesture posting code exactly — same event fields, same tap registration, same posting order. It didn’t work either. The space switched. The desktop didn’t restore. This ruled out any Swift-specific issue. The technique itself was broken on this display configuration.

The Things We Tried

What followed was a systematic attempt to force the window server to do its job. I’m documenting these not because they worked — none of them did — but because each failure revealed something about how macOS 16’s compositor and Dock interact, and because someone else will inevitably walk this same path.

CGSManagedDisplaySetCurrentSpace. The direct CGS call that tells the window server to change the active space. Used alone, it switched the space but didn’t restore windows. Combined with the gesture events, it brought back the frontmost window but not the others. The window server was updating its internal tracking without triggering a full recomposite.

SLSShowSpaces / SLSHideSpaces. The SkyLight framework functions that yabai uses for space switching. These explicitly manage window layer visibility: show the destination space’s windows, hide the source space’s windows. When called from our process (rather than injected into the Dock like yabai does), they partially worked — windows appeared but in wrong positions, and windows from the source space bled through to the destination.

SLSDisableUpdate / SLSReenableUpdate. The classic “hold all compositor changes, then flush” pattern. Wrapped around the gesture posting, this should have forced a full recomposite on re-enable. It didn’t. The windows still required manual clicks to appear.

SLSSetWindowAlpha. Setting window alpha to 0.0 and back to 1.0 should force the compositor to repaint each surface. The calls returned success. The alpha didn’t change. macOS 16 silently ignores alpha changes from external processes.

SLSSetWindowLevel. Pushing windows below the desktop layer and back. Same result — success return code, no visible effect.

CGSOrderWindow. Re-ordering each window to force a surface repaint. No effect on the compositor.

Synthetic mouse cursor sweep. Moving the cursor across the entire screen in a zigzag pattern to force hit-testing and surface repaints. Mouse movement didn’t trigger repaints. Only clicks did.

AXRaise on every window. This was the first thing that actually made windows appear. The Accessibility API’s kAXRaiseAction is the programmatic equivalent of clicking a window’s title bar — it tells the app to orderFront: the window, which forces the window server to repaint its surface. It worked, but with problems: it was visibly sequential (windows appeared one at a time), it raised minimised and hidden windows that should have stayed hidden, it didn’t restore the correct focus order, and it didn’t fix the menu bar.

Mission Control nudge. Briefly triggering and dismissing Mission Control (control, then escape) to force the Dock to rebuild its internal state. This had no effect — the Dock’s state was already corrupt and Mission Control didn’t reset it.

killall Dock. This worked. Killing the Dock forced it to reinitialise its internal display/space tracking from scratch, and after that the gesture technique worked again — briefly. But killall Dock is destructive: it resets window positions, opens minimised windows, and disrupts the user’s workflow. Not a viable solution for production.

The Root Cause

The research eventually pointed to the answer. When macOS transitions between display configurations — from two monitors to one, or from laptop screen to external — the Dock process rebuilds its internal _displaySpaces array. This array tracks which windows belong to which space on each display, and which app is frontmost on each space.

On a multi-monitor setup, each display gets a UUID-based identifier. The Dock’s gesture processing pipeline is designed for this configuration and works correctly.

On a single-monitor setup, the display identifier becomes the literal string Main. The Dock’s internal state management handles this differently, and the synthetic gesture events — while still successfully switching the space at the CGS layer — don’t fully trigger the Dock’s window show/hide cycle. The Dock changes the active space but doesn’t tell the compositor to repaint the window surfaces.

This is, in essence, a bug in macOS 16’s Dock. The same gesture events produce different behaviour depending on whether the display identifier is a UUID or the string Main. The space switches. The compositor doesn’t follow.

The Phantom Display

The solution came from a moment of lateral thinking at 5am, after twelve hours of increasingly creative (and increasingly desperate) attempts to force the compositor to do its job.

If the technique works when macOS sees multiple displays, and fails when it sees one… what if we gave it two?

macOS has a private API called CGVirtualDisplay. It’s the same mechanism that apps like BetterDisplay use to create virtual monitors. You provide a descriptor (resolution, refresh rate, vendor ID) and the system creates a display that exists in the window server’s display list but has no physical hardware behind it.

The implementation is about thirty lines of Objective-C:

  1. Create a CGVirtualDisplayMode (640×480, 60Hz)
  2. Create a CGVirtualDisplayDescriptor with a name, vendor ID, and the mode
  3. Call initWithDescriptor: on CGVirtualDisplay
  4. Hold the returned object in a static variable for the app’s lifetime

That’s it. The virtual display is invisible. The user never sees it. It doesn’t appear in System Settings as a second monitor. But its mere existence forces macOS to switch from the Main display identifier to UUID-based identifiers — and the Dock’s gesture processing pipeline works correctly with UUIDs.

The first test was electric. The space switched. The desktop restored. Every window appeared. The menu bar updated. I switched back. Everything was there. I switched again. And again. And again. No corruption. No missing surfaces. No phantom windows. No click-to-reveal.

One invisible 640×480 display, held in memory, fixes the entire Dock state corruption issue. The synthetic gesture technique that worked on two monitors now works on one. The keyboard shortcuts work. The wrap-around works. The menu bar works.

The Final Polish

There was one edge case to clean up. When a user moves a window from one space to another (via Mission Control), the source space can end up with no application windows. In that state, the Dock doesn’t know which app should own the menu bar, and the application-specific menus (File, Edit, View, etc.) disappear.

The fix: after each space switch, check whether the target space has any visible application windows. If it doesn’t, briefly activate Finder — which forces the Dock to refresh its menu bar ownership. On spaces that do have windows, this step is skipped entirely and the switch is purely instant.

Reflection

I almost gave up on this. Twice, actually. The first time was after the AXRaise experiments proved that the compositor could be forced to repaint individual surfaces but couldn’t be made to do a full space transition. The second was when even killall Dock only worked with an additional manual click. At that point I’d tried every private API I could find, every compositor trick, every window server nudge. The technique appeared to be fundamentally broken on single-monitor configurations.

The virtual display solution isn’t the kind of thing you find in documentation. It isn’t in any Stack Overflow answer or GitHub issue. It came from staring at the problem long enough to notice the one variable that changed between the working and broken configurations — not the gesture code, not the event tap, not the window list, but the display identifier string — and then asking the obvious question: what if we just… added another display?

There’s a lesson in that. Sometimes the fix for a system-level bug isn’t to work around the symptom but to change the conditions under which the bug manifests. The Dock doesn’t handle Main correctly. So don’t give it Main. Give it a UUID. Give it two displays. One of them doesn’t need to be real.

ActiveSpace 1.4.0+ ships with instant space switching that works on any display configuration that I have been able to test on — single monitor, dual monitors, laptop screen, external display, lid open, lid closed. The virtual display is created silently on launch and held for the app’s lifetime. The user never knows it’s there.

The stars aligned. Then they fell apart. Then we hung a new one in the sky and put them back.