Boilerplate to Add AppleScript to Your macOS App in 2020
There’s not a lot to do, but the documentation is (1) old, (2) not maintained anymore, (3) very wordy. The steps involved are actually very simple. I’ll go through them and provide detail and links for further reading.
Open the Script Editor app. You’ll want to use that for test-driving your app.
Enable AppleScript in Your App
The steps from the now archived docs are:
- You need an
.sdef
file with the script command definitions, - reference this file in your
Info.plist
, and - enable AppleScript in your
Info.plist
as well.
First, create a script definition file ending in .sdef
. It usually has the same name as your app, but you can pick whatever you want. For TableFlip, this file would be TableFlip.sdef
, for The Archive I picked TheArchive.sdef
.
Xcode won’t help you with a template here, so you might as well use touch
from the Terminal or your favorie text editor and drag the file into your project later.
This is the bare minimum container boilerplate:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="YOUR APP NAME HERE Terminology">
</dictionary>
Update 2023-03-29: The <dictionary>
element used to contain a new namespace, xmlns:xi="http://www.w3.org/2003/XInclude"
, but unless xi:include
or similar are used, this isn’t needed. Thanks to August Mohr for asking what this is for, which prompted me to simplify the code! (The document type definition at /System/Library/DTDs/sdef.dtd
already contains the namespace anyway, if I understand it correctly.)
Replace the title
with the name of your app plus the word “Terminology”. That appears to be a convention.
Add this file to your Xcode project and make sure it’s part of the app target. You may want to double-check that the target’s Build Phases > Copy Bundle Resources now contains your .sdef
file.
Second, open your app target’s Info.plist
in Xcode and add two new rows:
- In the first row, enable AppleScript support. Set the key to
NSAppleScriptEnabled
(will become “Scriptable”) and the value toYES
(it’s a boolean Xcode will recognize). - In the next row, define which
.sdef
file exposes available commands. Set the key toOSAScriptingDefinition
(will become “Scripting definition file name”) and the value to the file name of your.sdef
, e.g. “TableFlip.sdef
”. No need to specify a path.
Now build your project in Xcode.
Reveal the build product in Finder and drag the app bundle onto the Script Editor. When you drag app bundles onto the Script Editor, it’ll show a browsable scripting definition, aka the terminology your app exposes.
Since the boilerplate is empty, this should also be empty. For now.
Add default Cocoa commands to e.g. quit your app
How do you get started with defining script commands? – Thankfully, there’s a default set of commands that is provided as a quick start.
You can open it in Xcode straight from the Terminal:
open /System/Library/ScriptingDefinitions/CocoaStandard.sdef
It looks like this at the moment of writing on my machine:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="Standard Terminology">
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
<command name="open" code="aevtodoc" description="Open a document.">
<direct-parameter description="The file(s) to be opened.">
<type type="file"/>
<type type="file" list="yes"/>
</direct-parameter>
<result description="The opened document(s).">
<type type="document"/>
<type type="document" list="yes"/>
</result>
</command>
... lines 19--263 follow ...
</suite>
</dictionary>
That’s the same structure as the boilerplate from above. You see that inside of a terminology <dictionary>
element, there is a <suite>
element that contains many <command>
elements.
Your .sdef
can only have 1 root level <dictionary>
element, only one!, but that can contain multiple suited to group different purposes.
If you want, copy the whole code from the opening <suite>
until the closing </suite>
into your .sdef
file. Note that you will want to have a look at each command and change it so it fits your app. Not everything from the CocoaStandard.sdef
makes sense.
Let’s look at an example to see how you can customize this.
Customize the standard Quit command
If you look at the definition of the “quit” command in the CocoaStandard.sdef
file, you’ll see this (broken onto multiple lines to preserve space):
<command name="quit" code="aevtquit" description="Quit the application.">
<cocoa class="NSQuitCommand"/>
<parameter name="saving" code="savo"
type="save options" optional="yes"
description="Should changes be saved before quitting?">
<cocoa key="SaveOptions"/>
</parameter>
</command>
In my note-taking app The Archive, there’s no notion of manual saving. Files are saved as you type. Yet the command definition above offers an optional parameter to the “quit” command. I don’t need that, so it has to go. I delete the whole <parameter>...</parameter>
node.
My .sdef
looks like this afterwards:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="YOUR APP NAME HERE Terminology">
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
<command name="quit" code="aevtquit" description="Quit the application.">
<cocoa class="NSQuitCommand"/>
</command>
</suite>
</dictionary>
You can paste this into your .sdef
, too. Save the file, build the project, then drag the app bundle onto Script Editor again. It does not auto-update just because the underlying bundle did change during compilation.
Lo and behold – your first command appears!
Test your commands
With Script Editor still open, create a new document. That’ll be the scratchpad for AppleScript interaction.
Use the power of the new “quit” command to see how things work.
tell application "THE APP NAME" to quit
You have to replace THE APP NAME
with your app’s name. It appears to work well with the product name. In my case, that’ll be The Archive
with a space, or TableFlip
without one.
First, run the app from Xcode so it is open.
Then execute the script.
Did your app close? If so, congratulations!
If not, it could be because you typed
# Warning: will not work!
tell "THE APP NAME" to quit
That’ll send the “quit” message to the string. You want to send it to the application with that name.
Where to go from here
I think it’s a good first step to redact the CocoaStandard.sdef
to fit your app.
Heads up: Removing commands like the “quit” command from your app’s .sdef
will actually not disable them. The app still responds to the standard messages; apparently, these are inherited and the suite with the same name and identifier can be overridden. So the commands just don’t show up in the Script Editor terminology view.
When you have cleaned up the “Standard Suite”, you can start adding your own.
To prototype your app’s scriptability, the archived docs recommend you start with the .sdef
file first and don’t actually implement any functionality. Instead, you write test scripts in Script Editor and hit the “Compile” button. That’ll verify the syntax and show errors if you mistyped the invokation in the script, or botched the definition in you .sdef
file.
I really like the idea of outside-in development of the syntax to see what kind of interactions you will want, and then you figure out how to create the appropriate command implementations.
You will be putting your commands into your own <suite>
…</suite>
node, with a unique name and a unique 8-character identifier. Whatever that is for, and why-ever that is limited to 8 characters. Legacy, I guess. We’ll have a look at custom command creation in another post.
Please note that you need to run your app from Xcode before you execute a Script Editor script. If you don’t, macOS’s launch services may figure that the best fit for an application of that name is in you /Application
folder. Run your app from Xcode to make this debug version occupy the name and receive the AppleScript events.
Further reading:
- Excellent example of a custom script command suite on StackOverflow.
- How to turn on script debugging to show command handling in the Xcode console output.
- Implementing a Scriptable Application from the archived docs.