如何让 Cocoa App 支持 AppleScript

2016年12月29日 · 7 years ago

一、AppleScript

AppleScript 是 Apple 自己开发和维护的一种脚本语言,最早在 Mac OS 7 时代(1993 年)就已经支持了。熟悉 macOS 系统的读者应该都听说过,它相当于应用对外的接口,其他人可以通过 AppleScript 调用这些接口,实现对应用的编程。

所有的 Cocoa Apps 都自带一套支持基础能力的 Standard Suite,支持诸如启动、退出、保存等标准动作。所以即使开发者不做任何事,你也可以通过 AppleScript 实现绝大多数 Apps 的启动和关闭操作,这对于高级用户来说简直是居家旅行必备利器。

二、让一个标准的 Cocoa App 支持 AppleScript

1. Alfred Workflow

Alfred.app 是 AppleScript 应用场景的一个极好的例子。

上图这个 Workflow 就是基于 Evernote 提供的 AppleScript 接口实现的。

2016-12-28 at 17.38.png2016-12-28 at 17.39.png

使用 Script Editor.app 的 Dictionary 功能可以看到 Evernote.app 提供的所有接口。

2. Info.plist 声明支持

要让你的 App 支持 Apple Script,首先要在 Info.plist 文件中声明 NSAppleScriptEnabled (Scriptable) 为 YES. 同时声明 .sdef 文件的名字,这里以 Just Focus for Mac 为例:

3. 编写 sdef (Scripting Definition File) 文件

.sdef 文件是支持 AppleScript 中最重要的一步,它向 macOS 系统声明了这个 App 支持的 AppleScript 接口,其形式是一个 XML,结构如下:

文件头两行是固定不变的,第一行声明 XML 版本,第二行指定对应这个 XML 的 DTD。这个我们可以看做是早期标记语言比如 SGML 的遗留产物,DTD 是规定了当前文档的合法结构。DTD 相关的内容可以自行参阅维基百科DTDW3schools。这里我们只要 copy and paste 就好了。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">

接着是 XML 的主要内容,根节点为 dictionary,可以有多个 suite,每个 suite 支持多个 command

<dictionary title="JustFocus Scripting Terminology">
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">

<!-- …省略一万字… -->

<command name="close" code="coreclos" description="Close a window.">
<cocoa class="NSCloseCommand"/>
<direct-parameter type="specifier" description="the window(s) to close."/>
</command>

</suite>

<suite name="JustFocus Suite" code="JTFC" description="JustFocus Application Suite">
<cocoa name="JustFocus"/>

<command name="start pomodoro" code="pomodoro" description="Start a Pomodoro timer.">
<cocoa class="JustFocus.JFPomodoroCommand"/>
</command>

<command name="short break" code="cmdshtbr" description="Take a short break.">
<cocoa class="JustFocus.JFShortBreakCommand"/>
</command>

<command name="long break" code="cmdlngbr" description="Take a long break.">
<cocoa class="JustFocus.JFLongBreakCommand"/>
</command>

<command name="stop" code="cmdstopt" description="Stop current timer.">
<cocoa class="JustFocus.JFStopTimerCommand"/>
</command>

</suite>

</dictionary>

Standard Suite 是可以省略的,JustFocus Suite 才是我们要关注的。这里定义了几个 command,可以开始蕃茄钟,开始休息和停止计时。这几个命令都不带参数。

4. 实现 NSScriptCommand

接下来就可以使用 AppleScript 愉快地调用我们定义好的接口了:

tell application "JustFocus"
launch
start pomodoro
end tell

launch 是自带的实现我们就不管了,我们以 start pomodoro 为例:

<command name="start pomodoro" code="pomodoro" description="Start a Pomodoro timer.">
<cocoa class="JustFocus.JFPomodoroCommand"/>
</command>

JustFocus 是工程名,JFPomodoroCommand 是对应的处理的类名,这个类继承自 NSScriptCommand。这里有两点需要特别留意:

  1. 对于 ObjC 的实现 class 只要写 JFPomodoroCommand 就好了,但是 Swift 的实现需要写完整 Project.CommandClass,可以自己在 Swift 类里打印确认一下。

class func getClassName() {
return NSStringFromClass(self)
}

  1. sdef 文件里一个 command 的 code 属性,必须是 8 个字母组成。如果不是 8 个字符,在使用 Script Editor 调试的时候会报如下错误:

The application has a corrupted dictionary.

这点可把我害惨了,虽说我是没仔细看文档就上手了,但是这个奇怪的设定本身就是个坑T^T。详情可以参考文档:Preparing a Scripting Definition File,截取重点如下:

Note: The code for a command totals eight characters in length. Although the code can in fact be an arbitrary value, the first half (aevt in this case) has historically represented the suite (the Standard suite, formerly called the Core suite) and the second half (quit) the command. See Code Constants Used in Scriptability Information for more information on using codes.

接下来实现 JFPomodoroCommand 这个类就万事大吉了:

// MARK: - Pomodoro
class JFPomodoroCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
// Do something…

return nil
}
}

这样当有人用 AppleScript 调用了 start pomodoro 方法的时候,这个类的 performDefaultImplementation() 方法就会被调用,在这里实现你的逻辑就行了。

5. 带参数的命令

<command name="test parameter" code="jftestpa" description="Test parameter API">
<cocoa class="JustFocus.JFTestParameterCommand"/>
<direct-parameter type="text" optional="yes" description="Just for test"/>
</command>

这个命令带有一个 direct-parameter,类型为 text,可选。AppleScript 的用法如下:

tell application "JustFocus"
test parameter "test blablabla"
end tell

// MARK: - Test
class JFTestParameterCommand: JFASCommand {
override func performDefaultImplementation() -> Any? {
JF.log.debug("\(directParameter)")

return nil
}
}

参数通过 directParameter 获取,这里打印的结果如下:

Optional(test blablabla)

三、尾声

Cocoa App 支持 AppleScript 的整个过程还是比较直观简洁的,就分为定义部分和实现部分,就像写 ObjC 的头文件 .h 和实现文件 .m 一样。

但是,XML 的定义写法罗里吧嗦简直想死,本文仅仅介绍了最基础的 command 和 direct-parameter,除此之外,你还可以使用 class, parameter, property, enumeration 等多种特性,但是写起来繁琐得很。

有兴趣的读者可以参考官方文档:Preparing a Scripting Definition File。最近在实现 Just Focus 的 AppleScript 支持,踩了一点坑,想起其实去年就已经在另外一个 App 做过一样的事情,但是总记不住,相关的教程也不多,故写此文以作记录。希望能对需要的人有一点帮助。

16.12.28/夜