
Photo by Alexander Andrews on Unsplash
This is the third post in what I now realise is a series. When the Stars Align was the triumph — instant space switching, about six hours before we found out it was broken on single-monitor setups. When the Stars Align (Redux) was the fix — an invisible 640×480 virtual display to force macOS off its “Main” display identifier and onto proper UUIDs, restoring the Dock’s ability to composite correctly.
“The user never knows it’s there,” I wrote then. In fairness to April-11-me, I had just spent the better part of twenty hours convinced the compositor was a sealed black box, and arriving at a working solution at 5am does tend to produce overconfident prose.
As it turns out, the user does know it’s there. Not because the display is visible — it isn’t — but because the 640×480 rectangle of coordinate space that macOS thinks the virtual display occupies is emphatically not inert territory, no matter what the word “virtual” might suggest.
For about a week after the Redux, ActiveSpace felt fine. Instant space switching, intact menu bars, working compositor. Every now and then I’d notice my Dock had briefly disappeared. Always in the same kind of moment — waking from sleep, plugging in an external monitor, switching back to laptop-only — and always self-healing within a few seconds. I put it down to general macOS wake flakiness.
Then one Saturday morning, with ActiveSpace freshly running in a debug build, the Dock didn’t come back. I ran killall Dock and it returned, but by that point the pattern had clicked into place. The Dock went missing when the virtual display was present. Never when it wasn’t.
I dragged my cursor to the right edge of my main display. It kept going. Off the edge. Into the 640×480 invisible region where the virtual lived. I couldn’t see the cursor any more, but I could watch it come back when I pulled it to the left.
“That’s not as virtual as we thought,” I said out loud, to no-one.
CGVirtualDisplay creates a display that has no physical hardware and no surface — but structurally, it’s a real display as far as WindowServer is concerned. It sits in NSScreen.screens. It has an origin, a size, a UUID, an entry in ~/Library/Preferences/com.apple.spaces.plist. The Dock, looking for somewhere to anchor itself when the display arrangement changes, sees a valid display — 640 by 480, adjacent to the main — and sometimes decides that’s a perfectly fine place to render. The cursor, on hitting the right edge of main, does what cursors always do when there’s an adjacent display: it crosses onto it.
What my virtual-display trick actually produces isn’t an invisible display. It’s an uninhabited visible display. macOS doesn’t have a concept of “real display with no framebuffer” separate from “logical display.” Everything it knows about how to treat a display comes from the display list, and our virtual is in the list.
I’d handed the Dock a perfectly plausible second monitor and then been surprised when it took me up on the offer.
I won’t dwell on all the intermediate attempts — they’d make this post longer than it deserves — but three specific fixes are worth naming because each resolved a distinct way the virtual display failed to stay out of the user’s way.
The drift reposition. At creation time I was placing the virtual display adjacent to the right edge of the main display, on the theory that macOS wouldn’t reposition a secondary display that was already in the position it naturally wants. Apparently macOS disagrees. On several workflows — particularly the brief window after display reconfiguration — it would silently move the virtual from (2560, 0) (right of main) to (-640, 0) (left of main). That leftward move was what dragged the Dock off with it, because whatever heuristic picks the Dock’s display prefers the leftmost. Fix: after every didChangeScreenParametersNotification, check whether the virtual has drifted, and if so re-apply CGConfigureDisplayOrigin with a rate-limit and a small retry cap to stop us fighting macOS in a loop. In practice one attempt is always enough.
The cursor fence. The drift fix keeps the virtual anchored where I want it, but the cursor can still cross onto it at the contiguous right edge. I spent the best part of a day hunting through the introspection dump of CGVirtualDisplaySettings for a headless or isInternal flag that would mark the display as non-routable for UI. There wasn’t one — the only candidate, isReference, affects nothing useful. So: a session-level CGEventTap watching mouseMoved / mouseDragged, which warps the cursor back to the inside edge of main whenever an event location lands inside the virtual’s rect. The fence is arrangement-agnostic (clamps left or right depending on which side of main the virtual lives on) and fast enough to run on every mouse event without any visible lag.
The restart mechanism that didn’t need to exist. With the drift reposition and cursor fence in place, the virtual was finally behaving like “invisible” was meant to imply. But I’d also convinced myself that ActiveSpace needed a self-restart mechanism for edge-case drift conditions — the app should terminate cleanly on “dock ended up on virtual” or “spaces got reordered by screensaver wake” and let a launchd keep-alive agent respawn it with a fresh slate. I built the whole thing: a six-source reconfiguration observer, a drift classifier, a launchd agent plist with KeepAlive: SuccessfulExit=false and a 30-second throttle, a trigger-class-aware grace window, a post-restart cooldown to prevent loops. It took the better part of a day. I dogfooded it in dry-run mode for two days.
Across those two days of real use — laptop-only, dual-external, screensaver cycles, lid-close sleep/wake, adding and deleting spaces — the classifier fired seven times. In every case the other mitigations (the drift reposition, the cursor fence, SpaceObserver.refresh() re-querying CGS from scratch on every poll tick) had already dealt with the observable problem by the time a restart would have kicked in. ActiveSpace’s in-memory state never became unrecoverably stale. There was no poisoned cache. There was no wedge. A restart wouldn’t have improved anything; it would have just interrupted my work for 300ms and reset my Switcher MRU stack.
So I deleted the restart. The observer and classifier stayed — they’re genuinely useful for logging and future debugging — but the RestartCoordinator became a DriftMonitor that writes drift-observed(…) to ~/Library/Logs/ActiveSpace.log and does nothing else. The launchd keep-alive agent stayed too, for its unrelated benefit: if ActiveSpace ever does crash, launchd respawns it. That’s worth having. But the “self-restart on drift” framing was a sledgehammer for a nail that two smaller hammers had already hit.
One last thing. The Switcher HUD — the commandtab overlay that lets you cycle between apps on the current space — had a subtle lag. The first time you opened it after launching a new app, the app’s icon took a second or so to appear. Not catastrophic, but noticeable.
NSRunningApplication.icon is lazy. First access goes out to icon services to resolve the bundle’s icon file. Then NSImageView does the scale-down-to-96pt render, on the main thread, during HUD assembly. Both costs were being paid inline. The fix is a small in-memory cache: at app startup, we pre-warm icons for every currently-running regular app by fetching them and pre-drawing into a bitmap at the display size. Then we observe NSWorkspace.didLaunchApplicationNotification and warm newly-launched apps as they come online. In the HUD path, we serve from the cache instead of asking NSRunningApplication every time.
No more lag. A week of small fixes adds up to an app that feels fundamentally different to use.
The lesson I keep coming back to across this series is that “I’ve fixed it” is rarely true the first time you say it, and often not the second. The original post was right that instant switching was a real win. The Redux was right that the virtual-display trick made it work on every display configuration. But “works” and “ships cleanly” are not the same thing, and there was a whole week of subtle misbehaviour standing between them that only daily use could surface.
The other lesson is that engineers — me, specifically, in this case — reach for complicated solutions when simple ones would do. The self-restart mechanism was beautiful and correct and completely unnecessary. I enjoyed building it; I enjoyed deleting it rather more. The data showed it wasn’t needed, so it went.
ActiveSpace is shipped. It’s the first version I’m genuinely happy to hand a user who doesn’t know how to debug ~/Library/Logs. The virtual display stays where I put it. The cursor doesn’t wander off. The Dock stays home. The Switcher HUD feels snappy from the first keypress. Space switching is instant in every display configuration I’ve tested.
The stars aligned. Then they fell apart. Then we hung a new one in the sky. Then we discovered the new one had its own orbit, fought with it for a week, and finally got it to sit still.
Third time’s the charm. I hope.