初めての Apple Script

カーソルのサイズをトグルするために Apple Script を書いてみた。

以下がこのスクリプトの YouTube の動画へのリンクである。16 秒程度の動画なので、是非見てみてほしい。

[![Apple Script でカーソルサイズをトグルする](http://img.youtube.com/vi/V5Y_UacXEkA/0.jpg)](https://youtu.be/V5Y_UacXEkA)

## Apple Script とは

すごい昔（1993 年）から Apple のコンピュータに搭載されているオブジェクト指向のスクリプト言語。定期的に古いアーキテクチャを捨て、イチから構築する Apple において、唯一受け継がれているテクノロジーでもある（CPU が ARM 化しても生き残りそうだ）。ちょっと昔（2015 年）に JavaScript で同等のことができるようになったが、ドキュメントやサンプルが圧倒的に不足しているため、Apple Script の方が捗る。

Windows では Windows Scripting Host の位置付けに近いような気がするが、WSH よりは Apple Script の方が自動操作に強い。ObjC と組み合わせてより高度なことをしたり、署名して Mac App Store で配布することもできる。

## スクリプトエディタ

![スクリプトエディタ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/3ea72630-2bb8-d5fd-7df3-d6306a1473e5.png)

Launchpad の奥の方にひっそりと存在するアプリ。このアプリで Apple Script および JavaScript の開発ができる。できるが、エディタ機能は最低限、ステップ実行やブレイクポイントは使えないため、自分の好きなエディタで開発したほうが捗る。

なお、スクリプトエディタには記録機能が備わっているが、残念ながら筆者の試した限りどのアプリケーションに対しても機能しなかった。この記録機能を使用するにはどうやらアプリケーション側に特別な対応が必要らしく、その対応が Mac のプリインストールアプリですら対応されていないようだ。

また、スクリプトエディタは、スクリプトを保存するとコンパイルしてバイトコードとして上書き保存してしまう。バイトコードにコンパイルされていると確かに実行時のオーバーヘッドが軽減するが、バイトコードはもはやスクリプトエディタ以外のテキストエディタでは編集ができなくなってしまい、またバージョン管理的にも扱いにくい。バイトコードに保存するのは良いが、別ファイルにして欲しかったものである。

ただし、後述するスクリプトエディタ付属の用語説明は大変役に立つ。

## カーソルサイズをトグルするコード

以下が動画で動かしたスクリプトの全容だ。

```applescript
#!/usr/bin/env osascript

-- システム環境設定が起動しているかどうか。
-- 起動していない場合、処理の最後に起動したシステム環境設定を終了させる
set didRunSystemPreferences to get running of application "System Preferences"

-- システム環境設定を起動し特定の画面を開く。
tell application "System Preferences"
  -- アクセシビリティを開く
  set current pane to pane "com.apple.preference.universalaccess"
  tell current pane
    -- ディスプレイを開く
    reveal anchor "Seeing_Display"
  end tell
end tell

try
  tell application "System Events" to tell tab group 1 of group 1 of window 1 of application process "System Preferences"
    -- ラジオボタン（セグメント）の 2 番目をクリックする。
    click radio button 2

    tell slider 1
      -- 最初のスライダーの値を読み取り、別の値を設定する。
      if value is 1.0 then
        set value to 2.0
      else
        set value to 1.0
      end if
    end tell
  end tell
on error errMsg
  display dialog "Error: " & errMsg
end try

-- システム環境設定を起動したのであれば終了させる。
if not didRunSystemPreferences then
  quit application "System Preferences"
end if
```

このファイルを `toggle-cursor.applescript` という名前で保存し、以下のように実行する。

```shell-session
$ chmod a+x toggle-cursor.applescript
$ ./toggle-cursor.applescript
$
```

## 解説

`"System Preferences"` というのがシステム環境設定のアプリケーション名で、これはスクリプトエディタで調べられる。

![用語説明](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/4eae1ed6-ce77-e875-09df-b438d6e85cf4.png)

ここから目的のシステム環境設定を選択すると、以下のような用語説明を参照することができる。

![System Preference 用語説明](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/dbcab280-440f-46c0-5d18-2a2f50ebe5cb.png)

このウィンドウのタイトル部分がアプリケーション名である。例えばフォントブックなら `"Font Book"`、Google Chrome なら `"Google Chrome"` となる。

Apple Script は自然言語に近い文法で記述できるのが特徴で、例えば:

```applescript
  tell application "System Events" to tell tab group 1 of group 1 of window 1 of application process "System Preferences"
```

の部分は「システム環境設定アプリケーションプロセスのウィンドウ 1 のグループ 1 のタブグループ 1 にシステムイベントを伝える」と読る。また:

```applescript
    click radio button 2

    tell slider 1
```

はより自然言語らしく:

```applescript
    click second radio button

    tell first slider
```

と書くこともできる。なお、インデックスは 1 から始まる。

![System Preference 用語説明](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/dbcab280-440f-46c0-5d18-2a2f50ebe5cb.png)

上の説明書を見ると、変数が `n`、関数が `v` と、自然言語のそれで定義されており、Apple Script のターゲットがプログラマではなく一般ユーザである（あった）ことが伺える。

```applescript
tell application "System Preferences"
  set current pane to pane "com.apple.preference.universalaccess"
  tell current pane
    reveal anchor "Seeing_Display"
  end tell
end tell
```

上記は以下の画面を開く処理だが:

![アクセシビリティ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/04124f6d-2747-f799-9531-44a1e28c9c75.png)

以下の用語説明によると:

![System Preference 用語説明](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/7fb5e844-3b27-c056-7fd9-00cdec5d04ea.png)

システム環境設定には `pane` と `current pane` というものがあり、これが要するに開かれている画面を参照、または操作できる。

`"com.apple.preference.universalaccess"` をどうやって調べるか、については:

```applescript
tell application "System Preferences"
  log id of every pane as list
end tell
```
上記のようにすべての `pane` の `id` をログ出力すると:

```
com.apple.preferences.AppleIDPrefPane,
com.apple.preferences.Bluetooth,
com.apple.preference.dock,
com.apple.preference.expose,
com.apple.preference.speech,
com.apple.preference.spotlight,
com.apple.prefs.backup,
com.apple.preference.universalaccess,
com.apple.preferences.internetaccounts,
com.apple.preference.keyboard,
com.apple.preference.sound,
com.apple.preference.screentime,
com.apple.preference.security,
com.apple.preferences.softwareupdate,
com.apple.preference.displays,
com.apple.preference.desktopscreeneffect,
com.apple.preference.trackpad,
com.apple.preference.network,
com.apple.preferences.FamilySharingPrefPane,
com.apple.preference.printfax,
com.apple.preference.mouse,
com.apple.preferences.users,
com.apple.preference.general,
com.apple.preferences.extensions,
com.apple.preference.startupdisk,
com.apple.preferences.sharing,
com.apple.Localization,
com.apple.preference.energysaver,
com.apple.preference.notifications,
com.apple.preference.datetime
```

のような出力が得られるので（見やすいように改行を追加）、これをそれっぽい名前のものからひとつずつ試して目的の画面の `pane` `id` を調べた。

次は `"Seeing_Display"` の調べ方だが:

```applescript
tell application "System Preferences"
  set current pane to pane "com.apple.preference.universalaccess"
  tell current pane
    log name of every anchor as list
  end tell
end tell
```

という感じで以下のリストが得られる。

```text
Accessibility_Shortcut,
Seeing_Cursor,
Seeing_Zoom,
Keyboard,
Seeing_VoiceOver,
Virtual_Keyboard,
TextToSpeech,
Dwell,
Dictation,
Switch,
Siri,
Alternate_Pointer_Actions,
Mouse,
Captioning,
Media_Descriptions,
Head_Pointer,
General,
Seeing_ColorFilters,
Seeing_Display,
Hearing
```

次は `tell application "System Events" to tell tab group 1 of group 1 of window 1 of application process "System Preferences"` の要素の階層の調べ方だが:

```applescript
tell application "System Events" to tell application process "System Preferences"
  log every UI element as list
  activate
end tell
```

で:

```text
window アクセシビリティ of application process System Preferences,
menu bar 1 of application process System Preferences
```

が得られ:

```applescript
tell application "System Events" to tell window "アクセシビリティ" of application process "System Preferences"
  log every UI element as list
  activate
end tell
```

で:

```text
button 1 of window アクセシビリティ of application process System Preferences,
checkbox メニューバーにアクセシビリティの状況を表示 of window アクセシビリティ of application process System Preferences,
scroll area 1 of window アクセシビリティ of application process System Preferences,
group 1 of window アクセシビリティ of application process System Preferences,
toolbar 1 of window アクセシビリティ of application process System Preferences,
button 2 of window アクセシビリティ of application process System Preferences,
button 3 of window アクセシビリティ of application process System Preferences,
button 4 of window アクセシビリティ of application process System Preferences
```

が得られ:

```applescript
tell application "System Events" to tell group 1 of window "アクセシビリティ" of application process "System Preferences"
  log every UI element as list
  activate
end tell
```

で:

```text
radio button ディスプレイ of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
radio button カーソル of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
radio button カラーフィルタ of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
checkbox カラーを反転 of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
checkbox 視差効果を減らす of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
checkbox コントラストを上げる of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
checkbox 透明度を下げる of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
checkbox カラー以外で区別 of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
static text 最大 of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
static text 通常 of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
slider コントラスト: of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
static text コントラスト: of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences,
checkbox 反転（クラシック） of tab group 1 of group 1 of window アクセシビリティ of application process System Preferences
```

が得られる。

`activate` を入れるか、`quit application "System Preferences"` を無くすことで以下のようにアプリが起動中のままになるので、少しずつ確認しながら要素の階層を調べていく。

![アクセシビリティ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/b3d2567d-98f9-ae84-dec5-8ebf50f0e4cc.png)

この画面で「カーソル」セグメントを表示するには:

```applescript
click radio button "カーソル"
```

とすれば良い。

最終的に以下のようなコードになるが:

```applescript
tell application "System Events" to tell tab group 1 of group 1 of window "アクセシビリティ" of application process "System Preferences"
  click radio button "カーソル"
end tell
```

このままでは日本語環境でしか動作しないため、日本語が含まれている `window "アクセシビリティ"` を `window 1` に、`radio button "カーソル"` を `radio button 2` 変更する。汎用性を取ると可読性が落ちてしまうこのあたりは残念だが仕方がない。

## 参考になったサイト

- [鳶嶋工房 / AppleScript / 入門(Introduction)](http://tonbi.jp/AppleScript/Introduction/)