What It Does
macOS doesn’t remember how you arranged things — just where each window currently sits. The moment you plug into an external display, come home to a single-screen setup, or switch between spaces, you’re rearranging windows again.
SpaceMan lets you capture a layout as a named snapshot and apply it later, with a menu bar that only ever lists snapshots applicable to where you are right now.
How It Works
A “workspace” is identified by the combination of display count, sorted display UUIDs, and current Mission Control space. The same physical space-number on different display configurations is treated as distinct — snapshots don’t cross-contaminate.
Snapshots are named and manual. You choose when to capture and when to restore. No automatic captures, no auto-restore on display changes.
Taking a Snapshot
- Arrange your windows the way you want on the current space
- Click the rocket icon in the menu bar and choose Snapshot current workspace…
- Give it a short label
That’s it. The snapshot is tied to exactly this workspace fingerprint and will only surface again when you’re on the same space with the same display configuration.
Restoring a Snapshot
- Navigate to the target Mission Control space first
- Plug in or unplug any displays needed for the target workspace
- Open the menu — the snapshots listed are the ones captured on exactly this workspace fingerprint
- Click one
SpaceMan will find each app in the snapshot, launch it in the background if it’s not running, spawn extra windows if needed, and apply the captured position, size, and minimised/hidden state. A summary dialog reports what happened — positioned, launched, spawned, and skipped.
Full State Restoration
| Scenario | Behaviour |
|---|---|
| App already running | Existing windows are moved into place |
| App not running | Launched in the background, no focus steal |
| Fewer windows than the snapshot | Extra windows spawned via menu-bar driving |
| Minimised or hidden state | Preserved per-window; hidden falls back to minimised if AX can’t re-hide |
| App not installed | Skipped and reported in the summary dialog |
The management window lists every snapshot grouped by display configuration, with inline rename and delete.
Technical Details
Workspace fingerprint
WorkspaceFingerprint is (displayCount, sorted display UUIDs, managedSpaceID). Display UUIDs come from CGDisplayCreateUUIDFromDisplayID; the space ID is read from the private CGSCopyManagedDisplaySpaces API.
Space IDs are stable while a space exists but change if you delete and recreate one — an accepted limitation. Snapshots tied to a deleted space invalidate silently.
Window capture
CoreGraphics’s optionOnScreenOnly window list is scoped to the current Mission Control space. AX (kAXWindowsAttribute) enumerates windows across all spaces. SpaceMan cross-references the two — using the private _AXUIElementGetWindow — so a snapshot captures exactly the windows visible on the current space, and nothing more.
Each window record stores bundle ID, app name, title, frame, display UUID, state (normal / minimised / hidden), and orderInApp — the window’s zero-based index within its app’s AX list at capture time. This order is used to match records back to live windows at restore time.
Spawning windows
No universal AX action exists for “give this app another window”. WindowSpawner walks the app’s AX menu bar and presses the first enabled item whose title matches New Window, New Finder Window, New Browser Window, and so on — first against a preferred-match list, then against a broader “starts with New, contains Window” heuristic.
Architecture
| File | Purpose |
|---|---|
AppDelegate.swift | Owns the NSStatusItem, builds the menu, routes actions |
MenuBuilder.swift | Builds the menu, filtered by current workspace fingerprint |
WorkspaceFingerprint.swift | Computes (displayCount, UUIDs, spaceID) identity |
WindowCapture.swift | CG/AX cross-reference to enumerate current-space windows |
SnapshotModel.swift | Codable snapshot representation and window records |
SnapshotStore.swift | Load/save JSON with .bak rotation and corrupt-file recovery |
SnapshotRestore.swift | Async restore pipeline on the main actor |
WindowSpawner.swift | AX menu-bar driving to spawn extra app windows |
SnapshotManagementView.swift | SwiftUI management window: rename, delete, group by config |
CGSPrivate.swift | Swift bindings for private CoreGraphics space APIs |
Storage
Snapshots live in ~/Library/Application Support/JorvikSpaceMan/snapshots.json. A rolling .bak is written before every save; corrupt files are preserved to a timestamped copy rather than silently overwritten. Five snapshots per workspace, maximum — exceeding the cap evicts the oldest.
Installation
Two formats on every release — both signed and notarised, pick whichever suits:
- Installer (
.pkg) — recommended for first-time installs. Double-click to run; macOS Installer places the app in/Applicationswithout quarantine or App Translocation. - Download (
.zip) — unzip and dragSpaceMan.appto your Applications folder.
On first launch, grant Accessibility permission when prompted.
Settings
Right-click the rocket icon and choose Settings… to configure:
- Permissions — Accessibility status with grant button
- Menu bar icon pill — optional grey background for stronger contrast on busy or wallpaper-tinted menu bars (off by default)
- Launch at Login — start automatically when you log in
Auto-updates are handled by Sparkle. Use the “Check for Updates…” menu item to check on demand; Sparkle’s prompt offers an “Automatically download and install updates in the future” checkbox the first time an update is available. SpaceMan checks for new versions automatically once a day in the background.
Permissions
Accessibility is required. Without it, SpaceMan can’t read window state or apply position changes. macOS will prompt on first launch; permission status and a grant button are also available in the Settings panel.
Building from Source
- Clone the repo:
git clone https://github.com/PerpetualBeta/SpaceMan.git - Run
gmake build - Launch with
open .build/SpaceMan.app
gmake build compiles with swiftc -O and produces a Developer ID–signed .app bundle.
Requirements
macOS 14 (Sonoma) or later. Universal binary (Apple Silicon and Intel).