· 6 min read ·

The Private API Behind Instant Space Switching on macOS

Source: hackernews

Apple’s Mission Control space animation takes roughly 400 milliseconds. That is not a long time in an absolute sense, but it is long enough to break the feeling of seamless context switching. The animation slides the entire display left or right, and there is no supported way to disable it. Enabling “Reduce Motion” in Accessibility settings swaps the slide for a cross-fade, but the wait remains. Apple has never shipped a System Settings toggle to eliminate the transition, and there is no public API to trigger a space switch programmatically.

What this technique exploits is the gap between what macOS exposes to developers and what its own window server can do internally.

SkyLight and the CGSPrivate Surface

MacOS manages virtual desktops through the WindowServer process, which owns a private framework called SkyLight.framework located at /System/Library/PrivateFrameworks/SkyLight.framework. SkyLight is the layer below CoreGraphics that handles compositing, display management, and space state. It exposes a set of C functions, collectively known in the community as CGSPrivate, that allow direct manipulation of the window server’s internal state.

These symbols are not documented, not stable across macOS versions, and not available through any public SDK. But they are real, and they work. The function at the center of instant space switching is:

void CGSManagedDisplaySetCurrentSpace(
    CGSConnectionID cid,
    CFStringRef displayID,
    CGSSpaceID spaceID
);

Calling this function moves the window server directly to the target space. No animation is queued. No animation thread is scheduled. The display simply shows the contents of the requested space on the next frame. To a user, it is indistinguishable from the display update that follows pressing a key in a text editor.

The supporting calls needed to use it look like this:

typedef int CGSConnectionID;
typedef uint64_t CGSSpaceID;

extern CGSConnectionID CGSMainConnectionID(void);
extern CFArrayRef CGSCopyManagedDisplaySpaces(CGSConnectionID cid);
extern void CGSManagedDisplaySetCurrentSpace(
    CGSConnectionID cid,
    CFStringRef displayID,
    CGSSpaceID spaceID
);

These symbols can be resolved at runtime via dlopen and dlsym against SkyLight.framework or CoreGraphics.framework, which re-exports some of them. Linking directly against the private framework works for personal tools but is off-limits for App Store submissions.

Why Space IDs Are Not What You Expect

The CGS space ID is an opaque integer assigned when a space is created. It persists across login sessions but is not guaranteed to survive a reboot. Crucially, it has no fixed relationship to the user-visible ordering shown in Mission Control. Space 1 in Mission Control is not necessarily CGSSpaceID 1. The IDs are assigned in creation order, not display order, and they do not renumber when you delete or reorder spaces.

This means any tool that switches spaces by number needs to do a translation step. The canonical approach is to call CGSCopyManagedDisplaySpaces, which returns a CFArray of dictionaries describing each display and its associated space list in current order. From that data you can build a map from user-visible index to CGS space ID.

The structure returned looks roughly like this in Swift:

let info = CGSCopyManagedDisplaySpaces(connection) as! [[String: Any]]
for display in info {
    let displayID = display["Display Identifier"] as! String
    let spaces = display["Spaces"] as! [[String: Any]]
    for (index, space) in spaces.enumerated() {
        let spaceID = space["id64"] as! Int
        print("Display \(displayID) space \(index + 1) has CGS ID \(spaceID)")
    }
}

Multi-display setups add another layer: each physical display has its own independent space list, and CGSManagedDisplaySetCurrentSpace requires you to provide the correct display UUID string. Getting the display UUID from a CGDirectDisplayID (the CoreGraphics display handle) requires mapping through CGSCopyBestManagedDisplayForPoint or parsing the CGSCopyManagedDisplaySpaces output.

This Is Not New, Just Underused

The CGS space-switching trick has been in circulation for a long time. yabai, the tiling window manager that many macOS power users run, uses CGSManagedDisplaySetCurrentSpace for its yabai -m space --focus N command. The source lives in src/space.c. yabai has historically required disabling System Integrity Protection (SIP) for certain features, but space switching itself has not required SIP off since at least macOS Monterey.

AeroSpace, the newer tiling window manager written in Swift by Nikita Prokopov, also depends on the same function and makes a point of not requiring SIP to be disabled. Its implementation is cleaner, taking advantage of Swift’s interoperability with C to call the CGS functions directly.

Hammerspoon exposed this capability through its hs.spaces module, added around 2021. The Lua API abstracts away the display UUID and space ID mapping:

local spaces = require("hs.spaces")
local allSpaces = spaces.allSpaces()
-- allSpaces is keyed by display UUID, values are arrays of space IDs
spaces.gotoSpace(allSpaces["<display-uuid>"][2])

The hs.spaces module required significant reverse engineering work before it stabilized, partly because the CGS API surface shifted between macOS versions.

What the arhan.sh article contributes is a minimal, self-contained framing: you do not need a full window manager to get instant switching. A small binary or script that calls the CGS function directly is sufficient. This is useful if you want just this one behavior without adopting yabai’s tiling model or AeroSpace’s workspace philosophy.

What Apple Could Do and Has Not

Apple has consistently declined to expose any public API for programmatic space switching. NSWorkspace, AppKit, and the Accessibility framework offer no surface here. The closest thing to official support is CGSGetActiveSpace, which is not even in any public header but appears in some Apple sample code.

There is a defaults write key that existed in early macOS versions:

defaults write com.apple.dock workspaces-swoosh-animation-off -bool true
killall Dock

This key was removed years ago and no longer has any effect. The animation is now baked into the WindowServer animation pipeline, not the Dock process, which makes a plist override ineffective.

Apple’s reasoning for not providing a public API is not stated anywhere officially. The common interpretation is that the animation is considered part of the spatial metaphor that makes spaces comprehensible: the slide communicates direction and helps users maintain a mental map of their desktop layout. Letting applications bypass it would undermine that UX consistency argument. Whether that reasoning is sound depends on how much you value that mental model versus raw switching speed.

The Stability Question

Using CGSManagedDisplaySetCurrentSpace in a personal tool carries the standard private API risks. Apple has changed the function’s behavior, added guards, or shifted which framework re-exports it on multiple occasions across macOS versions. Tools that depend on it tend to carry version-specific shims. AeroSpace and yabai both maintain compatibility tables and release updates alongside major macOS releases.

The most notable recent restriction came with macOS Sequoia (15.x), which tightened certain CGS window-manipulation calls. Space switching itself remained functional, but calls for moving windows between spaces became more restricted without SIP off. This kind of incremental tightening is the persistent cost of building on private APIs.

For a personal tool that you maintain yourself, this is manageable. The fix when something breaks is usually a one-line change to a function name or a new dlsym lookup path. For something you ship to users, it becomes a support burden every October.

After the Switch

One detail that the minimal implementation often gets wrong is focus handling. After calling CGSManagedDisplaySetCurrentSpace, the display shows the new space, but keyboard focus may remain with whatever application was active on the previous space. The window server moves the view; it does not automatically transfer application activation.

The correct follow-up is an explicit activation call. In Swift:

if let app = NSWorkspace.shared.frontmostApplication {
    // This is the frontmost app on the new space
    app.activate(options: .activateIgnoringOtherApps)
}

Or, if you want to focus a specific window on the destination space, you need an AXUIElement call to raise the target window after the space switch completes.

This two-step pattern, switch space then explicitly activate, is what yabai and AeroSpace both do. Skipping the activation step gives you the visual switch but leaves input in an inconsistent state.

The Broader Pattern

The CGS space-switching story is one instance of a recurring pattern in macOS power-user tooling: Apple builds a capable system internally, ships a consumer-facing version of that capability with intentional limitations, and the developer community reverse-engineers the private layer to recover what was left out. The Accessibility Inspector, scripting additions, CGEventTap, and the CGS functions are all part of this same substrate.

The tools built on this substrate work well. The question is whether Apple will eventually close the gaps through public APIs or incrementally restrict access until the private approach stops being viable. Based on the trajectory over the past several macOS releases, the answer looks like neither: the private APIs stay marginally accessible, the public APIs stay absent, and the community keeps shipping patches every fall.

Was this interesting?