The following shows how you can tell macOS to assign an application to show on one specific desktop, all desktops, or just a single desktop.
- Announcement: https://twitter.com/_devalias/status/1734131769631498652
⇒ defaults read com.apple.spaces app-bindings
{
"com.apple.activitymonitor" = AllSpaces;
"com.googlecode.iterm2" = AllSpaces;
"com.riotgames.riotgames.riotclientux" = AllSpaces;
"com.toggl.toggldesktop.toggldesktop" = AllSpaces;
"org.audacityteam.audacity" = "7B8802D8-756C-445B-AE72-23FCABFC74C4";
"org.whispersystems.signal-desktop" = AllSpaces;
"ru.keepcoder.telegram" = AllSpaces;
}
⇒ /usr/libexec/PlistBuddy -c "Print :app-bindings" ~/Library/Preferences/com.apple.spaces.plist
Dict {
ru.keepcoder.telegram = AllSpaces
com.googlecode.iterm2 = AllSpaces
com.riotgames.riotgames.riotclientux = AllSpaces
com.toggl.toggldesktop.toggldesktop = AllSpaces
org.whispersystems.signal-desktop = AllSpaces
com.apple.activitymonitor = AllSpaces
org.audacityteam.audacity = 7B8802D8-756C-445B-AE72-23FCABFC74C4
}
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '."app-bindings"'
{
"ru.keepcoder.telegram": "AllSpaces",
"com.googlecode.iterm2": "AllSpaces",
"com.riotgames.riotgames.riotclientux": "AllSpaces",
"com.toggl.toggldesktop.toggldesktop": "AllSpaces",
"org.whispersystems.signal-desktop": "AllSpaces",
"com.apple.activitymonitor": "AllSpaces",
"org.audacityteam.audacity": "7B8802D8-756C-445B-AE72-23FCABFC74C4"
}
⇒ /usr/libexec/PlistBuddy -c "Delete :app-bindings:im.beeper" ~/Library/Preferences/com.apple.spaces.plist
# successfully deleted
⇒ killall Dock
# to ensure it is updated properly based on the plist changes
⇒ /usr/libexec/PlistBuddy -c "Delete :app-bindings:im.beeper" ~/Library/Preferences/com.apple.spaces.plist
Delete: Entry, ":app-bindings:im.beeper", Does Not Exist
⇒ /usr/libexec/PlistBuddy -c "Add :app-bindings:im.beeper string AllSpaces" ~/Library/Preferences/com.apple.spaces.plist
⇒ killall Dock
# to ensure it is updated properly based on the plist changes
Note: I haven't tested this
⇒ /usr/libexec/PlistBuddy -c "Add :app-bindings:im.beeper string TODO-THE-UUID-OF-THE-DESKTOP" ~/Library/Preferences/com.apple.spaces.plist
⇒ killall Dock
# to ensure it is updated properly based on the plist changes
To get the current desktop space:
⇒ /usr/libexec/PlistBuddy -c "Print :SpacesDisplayConfiguration:'Management Data':Monitors:0:'Current Space'" ~/Library/Preferences/com.apple.spaces.plist
Dict {
id64 = 617
ManagedSpaceID = 617
type = 0
uuid = 671BB3AE-0E2C-42C9-B259-C5004B30DEE1
}
Or specifically the uuid
for it:
⇒ /usr/libexec/PlistBuddy -c "Print :SpacesDisplayConfiguration:'Management Data':Monitors:0:'Current Space':uuid" ~/Library/Preferences/com.apple.spaces.plist
671BB3AE-0E2C-42C9-B259-C5004B30DEE1
The uuid
s for the other spaces are available as well, but it's harder to filter for them specifically with PlistBuddy
alone:
⇒ /usr/libexec/PlistBuddy -c "Print :SpacesDisplayConfiguration:'Management Data':Monitors:0:Spaces" ~/Library/Preferences/com.apple.spaces.plist
# Lots of data
You can crudely filter for this with a grep
like this, but it still leaves a lot of noise:
⇒ /usr/libexec/PlistBuddy -c "Print :SpacesDisplayConfiguration:'Management Data':Monitors:0:Spaces" ~/Library/Preferences/com.apple.spaces.plist | grep -B 4 'uuid ='
# More focussed data, but still noisy
You could also potentially get the uuid
s for all of the spaces with a command like this, but it doesn't leave a lot of context as to which one maps to which:
⇒ /usr/libexec/PlistBuddy -c "Print :SpacesDisplayConfiguration:'Space Properties'" ~/Library/Preferences/com.apple.spaces.plist
Array {
Dict {
name = 9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78
windows = Array {
74392
325
142
6672
41189
45074
14759
}
}
Dict {
name = 60D3C0EA-F836-4033-90C3-461D667C13FA
windows = Array {
74392
325
142
264
268
14761
}
}
# ..snip..
Another way to achieve this is by using plutil
to convert the plist to JSON, then pipe that to jq
, which allows for much more powerful filtering/manipulation.
Get the current space:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors[0]."Current Space"'
{
"id64": 617,
"ManagedSpaceID": 617,
"type": 0,
"uuid": "671BB3AE-0E2C-42C9-B259-C5004B30DEE1"
}
Or just the uuid
of it:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors[0]."Current Space".uuid'
"671BB3AE-0E2C-42C9-B259-C5004B30DEE1"
# Or if we want to make this more robust even if the first item in the array isn't the one with the 'Current Space'
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))."Current Space") | first | .uuid'
"671BB3AE-0E2C-42C9-B259-C5004B30DEE1"
We can also be a little bit fancier, and get the uuid
s from all of the elements within the Management Data
key, as well as the Display Identifier
associated with that space:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors[] | {displayIdentifier: ."Display Identifier", uuid: (if has("Current Space") then ."Current Space".uuid else ."Collapsed Space".uuid end)}'
{
"displayIdentifier": "Main",
"uuid": "671BB3AE-0E2C-42C9-B259-C5004B30DEE1"
}
{
"displayIdentifier": "62F91CDC-6EA4-194B-17B7-E58AA676B272",
"uuid": "AA7F793A-5D1D-4752-B46E-39FFBEBDEA09"
}
{
"displayIdentifier": "B17DC16B-9DA0-4B53-B720-A946ADC56B8C",
"uuid": "AFFB5DFA-AD24-4ED7-A1DA-A44BEFDDFEE0"
}
{
"displayIdentifier": "C8E42782-7DDB-C015-81BD-04B21E4C01F2",
"uuid": "8911EACB-5CED-48B0-8FA5-88994B43C732"
}
We can see details about all of the current spaces that are open with:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces'
# Lots of data
But to narrow that down more, we can seperate between 'normal' desktop spaces (which seem to be type: 0
):
# Full data
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(has("TileLayoutManager") | not))'
[
{
"id64": 617,
"ManagedSpaceID": 617,
"type": 0,
"uuid": "671BB3AE-0E2C-42C9-B259-C5004B30DEE1"
},
{
"id64": 14,
"ManagedSpaceID": 14,
"type": 0,
"uuid": "9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78"
},
# ..snip..
]
# Just the UUIDs
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(has("TileLayoutManager") | not).uuid)'
[
"671BB3AE-0E2C-42C9-B259-C5004B30DEE1",
"9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78",
"6A913ADE-99CD-47CA-94AC-F7D3D41C3475",
"60D3C0EA-F836-4033-90C3-461D667C13FA",
"56BB3123-46D8-410A-AC13-C1702362A25C",
"02F34F40-3FE7-40E6-9DDF-A3B2AA65B862",
"6D19DEE8-FD58-44EB-B1C2-5ED709AA7A6F",
"7B8802D8-756C-445B-AE72-23FCABFC74C4"
]
And those that are based on a maximised window (or multiple maximised windows):
# Full data
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(has("TileLayoutManager")))'
[
{
"fs_wid": 168,
"id64": 27,
"TileLayoutManager": { /* ..snip..*/ },
"uuid": "0ADEC86B-0C91-47A4-892E-995CA8CC008A",
"ManagedSpaceID": 27,
"type": 4,
"WallSpace": {
"id64": 28,
"ManagedSpaceID": 27,
"type": 6,
"uuid": "C3DB5651-B361-4392-B0F6-C8716F9FADA2"
},
"pid": 727
},
{
"id64": 98,
"TileLayoutManager": { /* ..snip..*/ },
"uuid": "918481C6-3272-4BE6-B981-107BA85FA8D9",
"ManagedSpaceID": 98,
"type": 4,
"WallSpace": {
"id64": 99,
"ManagedSpaceID": 98,
"type": 6,
"uuid": "2C1163D2-081E-4484-9DD0-9385640649BB"
},
"pid": [
78743,
14348
]
},
// ..snip..
]
# Just the UUIDs
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(has("TileLayoutManager")).uuid)'
[
"0ADEC86B-0C91-47A4-892E-995CA8CC008A",
"918481C6-3272-4BE6-B981-107BA85FA8D9",
"9E77A145-5756-44D2-9877-3CD3F3FBB01A"
]
We can see that the spaces of maximised windows (which appear to be type: 4
) additionally contain a TileLayoutManager
object, a WallSpace
object, the pid
of the maximised window(s) (either integer, or array of integers), a fs_wid
The TileSpaces
in particular seems to contain interesting data, so let's filter in on that.
Here is an example of the TileLayoutManager
data for a space that contains a single maximised window:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(has("TileLayoutManager") and (.pid | type != "array"))) | first | .TileLayoutManager'
{
"Age": 483877.12677943,
"PreferredLayout": [
{
"FirstTileSize": {
"Width": 1792,
"Height": 1120
},
"DisplaySize": {
"Width": 1792,
"Height": 1120
}
},
{
"FirstTileSize": {
"Width": 3440,
"Height": 1440
},
"DisplaySize": {
"Width": 3440,
"Height": 1440
}
}
],
"Layout Rect": {
"Width": 3440,
"Height": 1440,
"Y": 0,
"X": 0
},
"Max Tile Config": {
"Width": 2,
"Height": 1
},
"ReservedArea": {
"Right": 0,
"Left": 0,
"Bottom": 0,
"Top": 0
},
"TileSpaces": [
{
"uuid": "242659BA-8816-433B-BB28-27506B129806",
"id64": 29,
"TileLimitedClipping": true,
"TileType": "Primary",
"ManagedSpaceID": 27,
"fromSpace": "9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78",
"TileWindowID": 168,
"fs_wid": 168,
"type": 5,
"SizeConstraints": {
"Min": {
"Width": 86,
"Height": 29
},
"Max": {
"Width": 100000,
"Height": 100000
},
"Preferred": {
"Width": 1600,
"Height": 928
}
},
"appName": "Spotify",
"TileRect": {
"Width": 3440,
"Height": 1440,
"Y": 0,
"X": 0
},
"pid": 727,
"name": "Spotify Premium",
"com.apple.appkit.disablemcexit": false
}
],
"ConfigAge": 483877.126187566,
"Inter-Tile Spacing": {
"Width": 12,
"Height": 12
}
}
And this is an example of the TileLayoutManager
data for a space that contains a multiple maximised windows:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(has("TileLayoutManager") and (.pid | type == "array"))) | first | .TileLayoutManager'
{
"Age": 480896.491663617,
"PreferredLayout": [
{
"FirstTileSize": {
"Width": 1965,
"Height": 1440
},
"DisplaySize": {
"Width": 3440,
"Height": 1440
}
}
],
"Layout Rect": {
"Width": 3440,
"Height": 1440,
"Y": 0,
"X": 0
},
"Max Tile Config": {
"Width": 2,
"Height": 1
},
"ReservedArea": {
"Right": 0,
"Left": 0,
"Bottom": 0,
"Top": 0
},
"TileSpaces": [
{
"uuid": "95AB747E-C62C-4ABE-B274-687250EC2592",
"id64": 818,
"TileLimitedClipping": true,
"TileType": "Primary",
"ManagedSpaceID": 98,
"fromSpace": "671BB3AE-0E2C-42C9-B259-C5004B30DEE1",
"TileWindowID": 42954,
"fs_wid": 42954,
"type": 5,
"SizeConstraints": {
"Min": {
"Width": 727,
"Height": 480
},
"Max": {
"Width": 100000,
"Height": 100000
},
"Preferred": {
"Width": 1965,
"Height": 1440
}
},
"appName": "Fantastical",
"TileRect": {
"Width": 1965,
"Height": 1440,
"Y": 0,
"X": 0
},
"pid": 78743,
"name": "Fantastical",
"com.apple.appkit.disablemcexit": false
},
{
"uuid": "94253DF6-03DB-49FB-8608-BD86932B4267",
"id64": 108,
"TileLimitedClipping": true,
"TileType": "Primary",
"ManagedSpaceID": 98,
"fromSpace": "9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78",
"TileWindowID": 1111,
"fs_wid": 1111,
"type": 5,
"SizeConstraints": {
"Min": {
"Width": 351,
"Height": 524
},
"Max": {
"Width": 100000,
"Height": 100000
},
"Preferred": {
"Width": 1463,
"Height": 1440
}
},
"appName": "Reminders",
"TileRect": {
"Width": 1463,
"Height": 1440,
"Y": 0,
"X": 1977
},
"pid": 14348,
"name": "Reminders",
"com.apple.appkit.disablemcexit": false
}
],
"ConfigAge": 216451.82905787101,
"Inter-Tile Spacing": {
"Width": 12,
"Height": 12
}
}
Notice that the TileLayoutManager
's TileSpaces
array appears to have an entry for each maximised window, and they appear to be of type: 5
. Within that are keys including:
SizeContraints
object that hasMin
/Max
/Preferred
objects, each with aWidth
/Height
TileRect
object contains theWidth
/Height
/X
/Y
of each window (which can also be used to determine which maximised window is on the left, and which on the right)pid
/name
/appName
contains the process id / name of the applicationfromSpace
appears to contain the UUID that the window was maximised from- etc
Based on the above, we can filter some of the noise down to easily see the which apps are the maximised window(s) that are in each of these spaces, the window size, the uuid
of the space the windows came from, and the uuid
for that 'maximised window' space, among other things:
⇒ plutil -convert json -o - ~/Library/Preferences/com.apple.spaces.plist | jq '.SpacesDisplayConfiguration."Management Data".Monitors | map(select(has("Current Space"))) | first | .Spaces | map(select(.TileLayoutManager) | (.TileLayoutManager.TileSpaces | map(pick(.type, .uuid, .fromSpace, .pid, .appName, .name, .TileRect))) as $TileSpaces | pick(.type, .uuid) + { TileLayoutManager: { $TileSpaces } } )'
[
{
"type": 4,
"uuid": "0ADEC86B-0C91-47A4-892E-995CA8CC008A",
"TileLayoutManager": {
"TileSpaces": [
{
"type": 5,
"uuid": "242659BA-8816-433B-BB28-27506B129806",
"fromSpace": "9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78",
"pid": 727,
"appName": "Spotify",
"name": "Spotify Premium",
"TileRect": {
"Width": 3440,
"Height": 1440,
"Y": 0,
"X": 0
}
}
]
}
},
{
"type": 4,
"uuid": "918481C6-3272-4BE6-B981-107BA85FA8D9",
"TileLayoutManager": {
"TileSpaces": [
{
"type": 5,
"uuid": "95AB747E-C62C-4ABE-B274-687250EC2592",
"fromSpace": "671BB3AE-0E2C-42C9-B259-C5004B30DEE1",
"pid": 78743,
"appName": "Fantastical",
"name": "Fantastical",
"TileRect": {
"Width": 1965,
"Height": 1440,
"Y": 0,
"X": 0
}
},
{
"type": 5,
"uuid": "94253DF6-03DB-49FB-8608-BD86932B4267",
"fromSpace": "9160DD0A-9F52-4B1E-A4C9-B4F6ADD92A78",
"pid": 14348,
"appName": "Reminders",
"name": "Reminders",
"TileRect": {
"Width": 1463,
"Height": 1440,
"Y": 0,
"X": 1977
}
}
]
}
},
// ..snip..
]
@vsuharnikov True true. I don't use yabai (I tend to just do my window management with BetterTouchTool), so unfortunately can't help figure that part out at all.
@vsuharnikov Interesting that there are 2 checked options there.. looking in the plists, can you see 2 entries that might explain that? Or if you dump the whole plist, then manually change it in the macOS GUI so only a single tick is shown (ideally a 3rd option that wasn't already ticked), then dump the plist again and diff them; that might help show what entries were there and potentially causing the weird state.
@vsuharnikov Oh interesting. That's good to know that we need to kill/close the app as well to make it apply.
I'm not 100% sure if the
killall Dock
part was something that I figured out actually needed to be done, or if I was just assuming that it might be based on the plist files we were changing (and how in some other cases like that we need to kill the process to force it to re-read them from disk rather than use what it may have cached in memory already)I guess in some sense it kind of makes sense that the app may need to be closed as well before that setting will 'take effect', particularly if the app (or more likely the underlying UI framework) is looking at those plist settings to determine where to position itself. Would probably need to do some deeper digging/reverse engineering if we wanted to try and figure out/confirm the specific mechanisms there.
What is interesting though, is that if I make a change through the macOS GUI, it seems to apply straight away, without needing to close/kill the Dock or the app itself; so it would be interesting to figure out what method is used to 'communicate' that change to the app to make it move straight away.
eg. If I open Notes on my 'Desktop 1' space, switch to 'Desktop 2' space, and then from the Dock, right click on Notes -> Options -> This Desktop; it will assign it to 'Desktop 2' (since that is the space I am currently on), and the Notes window jumps to that space. If I then switch back to the 'Desktop 1' space, and look at the Dock menu again, it shows the options: All Desktops, This Desktop, Desktop 2 (which is ticked), None.
@vsuharnikov Haha, quite possibly. Though thankfully many of the 'deeper internals' type things I've looked into over the years often don't change too dramatically; so even if it does, hopefully it won't take much extra effort to figure it out :)