Desktop assignment lets you bind profiles to specific macOS desktops (Spaces). When you swipe to a desktop, HopTab automatically activates the assigned profile and updates your pinned apps.
How it works
Each macOS Space has a unique numeric ID. HopTab maintains a mapping from Space IDs to profile IDs. When the active Space changes, HopTab looks up the assigned profile and switches to it automatically.
The mapping is stored in PinnedAppsStore.swift:14:
/// Maps Space ID (Int) → Profile ID (UUID string). Persisted in UserDefaults.
@Published var spaceMapping: [ Int : UUID] = [ : ]
Assigning profiles to desktops
Swipe to the target desktop
Use Mission Control or a swipe gesture to navigate to the desktop you want to configure
Open Settings > Profiles
Click the HopTab menu bar icon and select Settings , then go to the Profiles tab
Assign the profile
Click “Assign to this desktop” next to the profile you want to activate on this Space
Repeat for other desktops
Swipe to other desktops and assign profiles as needed
Assigning implementation
When you click “Assign to this desktop”, HopTab calls assignProfileToCurrentSpace (PinnedAppsStore.swift:64-70):
func assignProfileToCurrentSpace ( profileId : UUID) {
guard let spaceId = SpaceService.currentSpaceId else { return }
// Remove any existing mapping for this profile (one profile per space)
spaceMapping = spaceMapping. filter { $0 . value != profileId }
spaceMapping[spaceId] = profileId
save ()
}
Each profile can only be assigned to one desktop at a time. Assigning a profile to a new desktop automatically removes its previous assignment.
Automatic profile switching
HopTab listens for NSWorkspace.activeSpaceDidChangeNotification to detect when you switch desktops (AppState.swift:343-354):
let spaceObserver = center. addObserver (
forName : NSWorkspace. activeSpaceDidChangeNotification ,
object : nil ,
queue : . main
) { [ weak self ] _ in
Task { @MainActor in
self ? . handleSpaceChange ()
}
}
When the notification fires, handleSpaceChange looks up the profile and activates it (AppState.swift:356-361):
private func handleSpaceChange () {
guard let spaceId = SpaceService.currentSpaceId,
let profileId = store. profileForSpace (spaceId)
else { return }
store. setActiveProfile ( id : profileId)
}
Getting the current Space ID
HopTab uses a private macOS API to get the currently active Space ID. The SpaceService wrapper (SpaceService.swift:10-16) calls CGSGetActiveSpace:
static var currentSpaceId: Int ? {
let conn = CGSMainConnectionID ()
guard conn > 0 else { return nil }
let spaceId = CGSGetActiveSpace (conn)
guard spaceId > 0 else { return nil }
return spaceId
}
The private API functions are declared using @_silgen_name (SpaceService.swift:22-27):
@_silgen_name ( "CGSMainConnectionID" )
private func CGSMainConnectionID () -> Int32
@_silgen_name ( "CGSGetActiveSpace" )
private func CGSGetActiveSpace ( _ cid : Int32 ) -> Int
Space IDs are session-local and can change after a reboot or when you add/remove desktops in Mission Control. If your assignments stop working, just reassign them — it only takes a few seconds.
Example workflow
Set up context-specific workspaces across multiple desktops:
Desktop 1: Coding Profile : CodingApps : Xcode, Simulator, Terminal
Desktop 2: Design Profile : DesignApps : Figma, Safari, Preview
Desktop 3: Communication Profile : CommsApps : Slack, Mail, Calendar
Now when you swipe between desktops:
Swipe to Desktop 1 → HopTab activates the “Coding” profile
Press Option + Tab → cycle through Xcode, Simulator, Terminal
Swipe to Desktop 3 → HopTab switches to “Comms” profile
Press Option + Tab → now cycling through Slack, Mail, Calendar
Your workspace adapts automatically. No manual profile switching needed.
Looking up assignments
You can check which profile is assigned to a Space, or which Space is assigned to a profile:
Profile for a Space
func profileForSpace ( _ spaceId : Int ) -> UUID ? {
guard let profileId = spaceMapping[spaceId],
profiles. contains ( where : { $0 . id == profileId })
else { return nil }
return profileId
}
Space for a profile
func spaceForProfile ( _ profileId : UUID) -> Int ? {
spaceMapping. first { $0 . value == profileId } ? . key
}
Removing assignments
To unassign a profile from its desktop (PinnedAppsStore.swift:73-76):
func unassignProfileFromSpace ( profileId : UUID) {
spaceMapping = spaceMapping. filter { $0 . value != profileId }
save ()
}
When you delete a profile, any desktop assignments are automatically removed (PinnedAppsStore.swift:46-53):
func deleteProfile ( id : UUID) {
profiles. removeAll { $0 . id == id }
spaceMapping = spaceMapping. filter { $0 . value != id }
if activeProfileId == id {
activeProfileId = profiles. first ? . id
}
save ()
}
Persistence
The Space-to-profile mapping is saved to UserDefaults as a dictionary of strings (PinnedAppsStore.swift:148-149):
let stringMap = Dictionary ( uniqueKeysWithValues : spaceMapping. map { ( " \( $0 . key ) " , $0 . value . uuidString ) })
UserDefaults. standard . set (stringMap, forKey : Self . spaceMappingKey )
On load, the string dictionary is converted back to [Int: UUID] (PinnedAppsStore.swift:154-161):
if let stringMap = UserDefaults.standard. dictionary ( forKey : Self . spaceMappingKey ) as? [ String : String ] {
spaceMapping = [ : ]
for (spaceStr, profileStr) in stringMap {
if let spaceId = Int (spaceStr), let profileId = UUID ( uuidString : profileStr) {
spaceMapping[spaceId] = profileId
}
}
}
Profiles Learn how to create and manage profiles
App switching Understand the app switcher behavior