Macのデスクトップ割当をCUIで設定する方法

#はじめに
私は新しいmacOSが出るたびにクリーンインストールをしています。自動的に環境を構築できるようにするために、シェルスクリプトを書いています。
また、下記の記事のようにアプリケーション毎に仮想デスクトップを割り当てており、それをGUIで設定するのが意外と面倒だったので、仮想デスクトップの割り当てについていろいろ調べてみました。
仮想デスクトップの割り当てを自動化するのが本記事のゴールです。
[macOSでディスプレイ1枚で作業する技術](https://qiita.com/saboyutaka/items/d6cfd2a2b60f1a374d60)

## 実行環境
仮想デスクトップを10画面用意し、以下のように各アプリケーションを割り当てています。

|アプリケーション  | 仮想デスクトップ割当番号 |
|:--|:-:|
|Google Chrome  |2  |
|Spark(メーラー)  |3  |
|Evernote | 4|
|Fantastical 2 | 5|
|iTerm2 | 7|
|Tweetbot | 9 | 
|Line | 9 | 
|Slack| 9|
|VSCode | 10 |

* Macbook Air Early2015
* mac OS Mojave10.14.3（18D109）
* ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin18]

#com.apple.spaces.plistの構造
仮想デスクトップに関する設定項目は${HOME}/Library/Preferences/com.apple.spaces.plistに記述されていますが、バイナリ形式のため、そのままでは見ることができません。よって、defaultsコマンドを使うか、Xcodeで開くことで見れるようになります。

```Bash:com.apple.spaces.plist
$ defaults read com.apple.spaces
SpacesDisplayConfiguration =     {
        "Management Data" =         {
            Age = "43266.030552205";
            Double =             {
                CreatedCount = 0;
                DeletedCount = 0;
                LifetimeEntryCount = 0;
                LifetimeMax = 0;
                LifetimeMin = 0;
                LifetimeSum = 0;
<略>

$ open ${HOME}/Library/Preferences/com.apple.spaces.plist  //デフォルトだとxcodeで開かれるはず
```

plistは以下のようにツリー構造をしているのがわかります。私の環境では、下記のような構造になっていました。

```:com.apple.spaces.plistの構造（抜粋）
SpacesDisplayConfiguration
├ Space Properties
     └ 0
       └ name => ""
     └ 1
       └ name => "B9A0EA1C-76A0-417D-BD30-4832630F6222"
     └ 2
       └ name => "91D6C0A1-645C-465A-9E5B-713E71FBD5F7"
         ︙
     └ 9
       └ name => "B5223836-F5DB-403A-B24C-48491006CFD2"


└ app-bindings
     └ "com.google.chrome" => "B9A0EA1C-76A0-417D-BD30-4832630F6222"
     └ "com.readdle.smartemail-mac" => "91D6C0A1-645C-465A-9E5B-713E71FBD5F7"
         ︙
```
ここで、GUI上の仮想デスクトップ番号（1~10)と.plistファイル上の割当番号(0~9)のように1つずれていることに注意してください。
すなわち、
*SpacesDisplayConfiguration => Space Properties => 各割当番号 => name*
の値でUUIDが決定され、それを 
*app-bindings => 各アプリケーション => UUID*
と記述することで、各アプリケーションを仮想デスクトップに割り当てていることがわかります。
よって、これらの紐付けをCUI上で実施することで、各アプリケーションを仮想デスクトップへの割り当てが自動で行うことができます。

## com.apple.spaces.plistへ書き込む方法
defaultsコマンドではplistファイルの入れ子になっている要素を操作することができず、難儀しました。（これの調査で1日潰れました。）結論から言うと、/usr/libexec/PlistBuddyコマンドを使うことで、入れ子になった要素を操作することができました。
以下の形式でコマンドを実行します。

```:com.apple.spaces.plistへの要素の追加
$ /usr/libexec/PlistBuddy -c "add 名前 値" ${HOME}/Library/Preferences/com.apple.spaces.plist
```

入れ子の区切り文字はコロン(:)です。
例えば、iTerm2(com.googlecode.iterm2)を仮想デスクトップ7番(UUID:8310FB96-4D15-48F4-AD49-4280575AD2A5)に割り当てたい場合は、以下のコマンドを実行します。

```:com.apple.spaces.plistへの要素の追加例
$ /usr/libexec/PlistBuddy -c "add :app-bindings:com.googlecode.iterm2 8310FB96-4D15-48F4-AD49-4280575AD2A5" ${HOME}/Library/Preferences/com.apple.spaces.plist
```

## 仮想デスクトップ画面のUUIDのみ取り出す方法
仮想デスクトップ2番のUUIDを取り出すためには、以下のコマンドを実行します。前述のように、GUI上の仮想デスクトップ2番に対応するplistファイル上の番号は1番であることに注意してください。

```
$ /usr/libexec/PlistBuddy -c "print :SpacesDisplayConfiguration:Space\ Properties:1:name" ${HOME}/Library/Preferences/com.apple.spaces.plist
B9A0EA1C-76A0-417D-BD30-4832630F6222
```

これは、${HOME}/Library/Preferences/com.apple.spaces.plist の SpacesDisplayConfiguration => Space Properties => 1 => nameの値を出力するものです。入れ子の区切り文字はコロン(:)です。また、スペースはエスケープシーケンスで入力してください。

## 仮想デスクトップ割当の自動化
これらを踏まえると以下のように自動化できます。

```Ruby:SetAppSpace.rb
require 'open3'

NUMBER_OF_VIRTUAL_DESKTOP = 9
uuid=[] #仮想デスクトップのUUID一覧

# アプリケーションと仮想デスクトップ番号の組み合わせ
appDomains = [
    {"domain"=>"com.google.chrome","num"=>2"uuid"=>nil}
    {"domain"=>"com.readdle.smartemail-mac","num"=>3"uuid"=>nil}
    {"domain"=>"com.evernote.evernote","num"=>4"uuid"=>nil}
    {"domain"=>"com.flexibits.fantastical2.mac","num"=>5"uuid"=>nil}
    {"domain"=>"com.googlecode.iterm2","num"=>7"uuid"=>nil}
    {"domain"=>"com.tapbots.tweetbotmac","num"=>9"uuid"=>nil}
    {"domain"=>"jp.naver.line.mac","num"=>9"uuid"=>nil}
    {"domain"=>"com.tinyspeck.slackmacgap","num"=>9"uuid"=>nil}
    {"domain"=>"com.microsoft.vscode","num"=>10"uuid"=>nil}
]

# 各仮想デスクトップのUUIDを取得
for spaceNum in 1..(NUMBER_OF_VIRTUAL_DESKTOP + 1) do
    ## spaceNum-1、NUMBER_OF_VIRTUAL_DESKTOP + 1はGUI上の番号とplistの補正
    cmd = %Q(/usr/libexec/PlistBuddy -c "print :SpacesDisplayConfiguration:Space\\ Properties:#{spaceNum-1}:name" ${HOME}/Library/Preferences/com.apple.spaces.plist) 
    uuid[spaceNum],  = Open3.capture3(cmd) #1つ目の戻り値のみ格納し、それ以外は捨てる
    uuid[spaceNum] = uuid[spaceNum].chomp
end

# 紐付け実行
appDomains.each{|app|
    app["uuid"] = uuid[app["num"]]
    cmd = %Q(/usr/libexec/PlistBuddy -c "add :app-bindings:#{app["domain"]} string #{app["uuid"]} " ${HOME}/Library/Preferences/com.apple.spaces.plist)
    Open3.capture3(cmd)
}
```

# 参考
[plistをコマンドラインから作成・編集する[PlistBuddy]](https://qiita.com/tani-shi/items/4a304990856764719f5b)