macOSで現在の仮想デスクトップ番号と仮想デスクトップの総数をデスクトップ間を移動したときに通知させるJXAスクリプト

# TL;DR
現在の仮想デスクトップの番号と開いている仮想デスクトップの総数を、デスクトップを跨いだときに、OSの通知欄に通知するアプリをJavascript for Automation (JXA)で作成した。

![2022_08_08_03_03_15__.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/5c9147e0-9335-8474-a3a7-15041a3a44cb.gif)

右上に注目すると、確かにデスクトップが切り替わるタイミングで通知のところが変わっている。
![2022_08_08_03_37_50.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/f142b87f-bba4-2b32-d560-4eaad62f2d42.gif)



# Motivation
OS X(macOS)では、仮想デスクトップ（これは正確にはWindowsでの呼び方？で、OS Xでは[Spaces](https://support.apple.com/ja-jp/guide/mac-help/mh14112/mac) という）を複数作成でき、3本指で左右にスワイプするか、デフォルトではCtrl+矢印キー（←, →）で移動できる。デスクトップNに何かを割り当てていたり、仮想デスクトップを作りすぎて所望のウィンドウがどこにあるか分からなくなったときに、3本指で上にスワイプするかCtrl+↑でMission Controlを開いて上の方にマウスを持っていって移動する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/78923835-aab3-e095-5af1-890819f26585.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/fda9d8f1-b466-5d64-7ac4-050b383c3af9.png)
この作業が面倒で、Mission Controlを開かずとも現在のデスクトップの番号を知りたくなるときがある。そこで今回はJXAで現在の仮想デスクトップの番号を知らせるスクリプトを書いた。

# Source
`/Library/Preferences/com.apple.spaces.plist`からCurrent Spaceを取得する。このときに`SLSCopyManagedDisplaySpaces`とかいう、SpaceのAPI（非公開）を使う。
`defaults read com.apple.spaces`で分かるが、com.apple.spaces.plistは、SpacesDisplayConfiguration＞Management Data>Monitorsという階層構造になっている。

```
if(user === ""){currentUserPath = fileManager.homeDirectoryForCurrentUser.fileSystemRepresentation;}
else{currentUserPath = "/Users/" + user;}
```
これで、userのところに自分のユーザー名を入れなくても動くようになっているはず。

Full Sourceは以下。
```
var sys = Application("System Events");
sys.includeStandardAdditions = true;
function file_exists(path){
	let fileManager = $.NSFileManager.defaultManager;
	return fileManager.fileExistsAtPath($(path));
}
let user=""//ここはあなたの$Userの名前
let fileManager = $.NSFileManager.defaultManager;
let currentUserPath = ""; 
if(user === ""){currentUserPath = fileManager.homeDirectoryForCurrentUser.fileSystemRepresentation;}
else{currentUserPath = "/Users/" + user;}
var currentSpaceNumber = -100
DELAY_SECONDS = 0.5;
function idle(){
    ObjC.bindFunction('SLSMainConnectionID', ['int', []])
    ObjC.bindFunction('SLSCopyManagedDisplaySpaces', ['id', ['int']])
    let spaces = ObjC.deepUnwrap($.SLSCopyManagedDisplaySpaces($.SLSMainConnectionID()))
    let output = {};
	let dict = $.NSMutableDictionary.alloc.initWithContentsOfFile(currentUserPath + "/Library/Preferences/com.apple.spaces.plist");
    let contents = ObjC.deepUnwrap(dict);
	let monitors = contents['SpacesDisplayConfiguration']['Management Data']['Monitors'];
    if (!file_exists(currentUserPath + "/Library/Preferences/com.apple.spaces.plist")) {
        sys.displayDialog(JSON.stringify({"Spaces_Check": "Required File(s) Not Found"}));
    }
	var activeSpaceID = -1
    activeSpaceID = spaces[0]['Current Space'].ManagedSpaceID; 
    if (activeSpaceID == -1){
        sys.displayDialog("Can't find current space")
    }
    for(let i = 0; i < monitors.length; i++){
        if(monitors[i]['Display Identifier'] == "Main"){
            const currentSpaceID = spaces[0]['Current Space'].ManagedSpaceID;
            let currentSpacePlace = 0;
            let totalSpaces = monitors[i]['Spaces'].length;
            for(let j = 0; j < monitors[i]['Spaces'].length; j++){
                if(currentSpaceID == monitors[i]['Spaces'][j]['ManagedSpaceID']){
                    currentSpacePlace = j + 1;
                }
            }
            output['Main Desktop'] = {};
            output['Main Desktop']['Current Space'] = currentSpacePlace;
            output['Main Desktop']['Total Spaces'] = totalSpaces;
        }
    }
    var app = Application.currentApplication()
    app.includeStandardAdditions = true
    if (currentSpaceNumber>0){
        if (currentSpaceNumber!==output['Main Desktop']['Current Space']){
            app.displayNotification("現在のデスクトップ："+String(JSON.stringify(output['Main Desktop']['Current Space'], null, 1))+" / "+String(JSON.stringify(output['Main Desktop']['Total Spaces'], null, 1)), {
            })//通知
        }
    }
	currentSpaceNumber = output['Main Desktop']['Current Space']
    return DELAY_SECONDS;
}

```
通知音はなくした。
一定時間ごとに繰り返し実行するためには、
```
function idle(){
    return DELAY_SECONDS;
}
```
で、DELAY_SECONDSごとに繰り返し実行してくれる。今回は0.5秒にしている。←1秒より細かくできているか不明

# 導入方法
スクリプトエディタを開く
→デフォルトのAppleScriptからJavascriptに変更
→ソースをコピー
→保存するときに`ファイルフォーマット`をアプリに変更
→オプションの`ハンドラの実行後に終了しない`を選択
→保存

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316390/7ad69c30-6d1b-6ede-f654-53beb6df12c3.png)

これでアプリができるので、クリックすれば起動。終了したいときはDockの右クリックから終了。

# Discussion
仮想デスクトップに関して、フルスクリーンにしたときには反映されない(デスクトップ0になる？)ので注意。これを考慮したものに改良するかもしれない。
また、通知のバナーが多くてうぜぇってなるので、通知のバナーがフェードアウトする時間を（デフォルトでは4~5秒から）1秒とかに変更したくて調べたら、なぜかBig Surだと変更できない。Monterey~~は不明~~（8/25/2022現在、できなかった）。Catalina以前だと
`defaults write com.apple.notificationcenterui bannerTime -int 1 && killall NotificationCenter\ osascript -e 'display notification "test"'`でできるらしい。


# 最後に
実はTL;DRに挿入されたgifを見れば気づくかもしれないが、Menubarに密かに[デスクトップ番号を知らせてくれるメニューバー常駐のアプリを入れている](https://github.com/gechr/WhichSpace) （cf https://github.com/Jaysce/Spaceman）
これで十分なのだが、どうにかAppleScriptまたはJavascriptでできないかなあということで今回やってみた。
`defaults read com.apple.spaces | plutil -convert json - -o - | jq '.SpacesDisplayConfiguration."Management Data".Monitors[0] '` これで、Current Spaceが得られるはずが、仮想デスクトップを追加または削除しないと反映されないという仕様があり、かなり手こずった。
JXAもAppleScripも気軽に定期実行ができるしmenubarをいじれるし、Apple信者、Mac使いにとってはなかなか面白い。