JXA で AXUIElement を使用して UI の情報にアクセスする
macOS の JavaScript for Automation (JXA) で Visual Studio Code の UI の情報にアクセスがしたくなったので調べてみたところ、AXUIElement を使えばできることが分かったのでその方法をメモしておく。ただ先に書いておくと、結構コードがトリッキーでデバッグもなかなか難しいため、素直に Swift を使って実装した方がいいかもしれない。
まず、AXUIElement は Application Services フレームワークに含まれているので、JXA からは Objective-C ブリッジ経由でこれにアクセスする。JXA でフレームワークをインポートすると次のようなコードになる。ここではついでに関連する Cocoa もあわせてインポートしている。
ObjC.import("Cocoa");
ObjC.import("ApplicationServices");
次に CF
で始まる名前の型を NS
で始まる名前の型に変換するために CFMakeCollectable を次のように明示的に bind しておく。これについては自分も完全に理解したわけではないが、JXA から値にアクセスするには NS
で始まる型になっていた方が都合が良いので、その変換を容易にするための Hack だと理解している。
ObjC.bindFunction("CFMakeCollectable", ["id", ["void *"]]);
フレームワークのインポートや関数の bind ができたら、実行中の VSCode のアプリケーションオブジェクトを Bundle Identifier から取得する。$
は Objective-C ブリッジでインポートした各種フレームワークの操作に使用する。
const id = "com.microsoft.VSCode";
const vscode = $.NSRunningApplication.runningApplicationsWithBundleIdentifier(id).firstObject;
次に AXUIElementCreateApplication を呼び出してトップレベルのアクセシビリティ要素を取得する。呼び出し時には実行中の VSCode の PID を指定する。
const app = $.AXUIElementCreateApplication(vscode.processIdentifier);
トップレベルの要素が取得できたので、ここからアプリの UI の各種属性値にアクセスできる。属性値の取得には AXUIElementCopyAttributeValue を使用する。例えばアプリケーションの名前を取得する場合は次のようになる。Objective-C ブリッジ経由で取得した値はそのままだと JavaScript の値としては扱えないため ObjC.unwrap()
や ObjC.deepUnwrap()
などを使う必要があることに注意する。
// 要素の属性名
const attr = $.CFStringCreateWithCString($.kCFAllocatorDefault, "AXTitle", $.kCFStringEncodingUTF8);
// 属性値を受け取るための参照
const valueRef = Ref();
// トップレベル要素からアプリ名を取得
const ret = $.AXUIElementCopyAttributeValue(app, attr, valueRef);
if (ret !== 0) {
throw new Error(`failed to get attribute value: ${ret}`);
}
// アプリ名を NSString に変換してから、JavaScript の文字列型に変換
const title = ObjC.unwrap($.CFMakeCollectable(valueRef[0]));
// アプリ名を表示
console.log(title); // => "Code"
要素から取得可能な属性は AXUIElementCopyAttributeNames を呼び出すと確認できる。なお取得可能な属性は要素の種類によって異なるので注意が必要。
// 属性リストを受け取るための参照
const namesRef = Ref();
// 要素から属性リストを取得
const ret = $.AXUIElementCopyAttributeNames(app, namesRef);
if (ret !== 0) {
throw new Error(`failed to get attribute names: ${ret}`);
}
// 文字列の配列に変換
const names = ObjC.deepUnwrap($.CFMakeCollectable(namesRef[0]));
// 属性リストを表示
console.log(names); // => AXFunctionRowTopLevelElements,AXManualAccessibility, ...
AXUIElement の要素はツリー状になっているので、AXChildren
属性を指定して子要素を取得し、UI の各要素を辿っていくことができる。アプリの要素がどのようなツリー構造になっているかは、XCode に付属する Accessibility Inspector を使えば実際の画面の要素を指定して確認できる。