RFC: AppleScript integration in actions

Getting around to AppleScript integration in the Mac version. Interested in feedback from those who might make use of this feature. This is not about Drafts itself being scriptable (which is coming as well), but about how a “Run AppleScript” action step should behave when calling AppleScripts. I’m going to provide a few suggested directions I could go with this and would like comments, or suggestions if the technically inclined can think of a better option I’m missing.

A Few Background Details

Drafts is a sandboxed app. As a result, the only external scripts the app is permitted to execute must reside in its designated scripts folder in ~/Library/Application Scripts/. So, to run an AppleScript, it must be in a file in that folder.

By default, a sandboxed app only has permissions to read from it’s own Application Scripts folder, but it can prompt the user for write permissions to that folder like any other folder it it needs to write a file there.

AppleScripts (and other OSA-compliant scripts) are compiled and stored in a binary format, not as plain text scripts. Because of the tooling to compile and test scripts, it makes the most sense for users to edit those scripts in the Script Editor app included with the Mac, not directly in Drafts or another text editor.

Implementation Options

There’s a few different ways I could approach how the “Run AppleScript” action step would run scripts. These are the options I’ve thought of so far and am interested on which sounds most appealing. There are probably some pros and cons of each approach I’ve not yet considered as well.

Option #1: You manage the AppleScript files

A “Run AppleScript” step would just store the relative path of a script file, and when executed, attempt to find the script by that name in the file system and run it.

  • Pros:
    • I don’t have to implement much to support it! :slight_smile:
    • Scripts would be stored compiled, ready to edit in the file system. Nothing is required in Drafts to pickup on changes to a script file.
    • Requires no special permission to write to the Application Scripts directory.
  • Cons:
    • Scripts are not backed up, or synced between Macs. If you run Drafts on multiple Macs (or multiple user accounts on same Mac), you would be responsible for moving around and updating and scripts you need. Same for a re-install of Drafts on a new system.
    • Not portable. An action requiring an AppleScript could not simply be shared via the Action Directory, export, or other means without also directing users how how to acquire and install the scripts it requires.
    • No visibility of script content within Drafts.

Option #2: Drafts stores the scripts in binary format

When editing a a “Run AppleScript” step, the user can import a script file (from anywhere on their system) they have written and compiled in Script Editor. The binary “.scpt” file data would be stored in the action, and when the action is run, Drafts would save that data out to disk in a temp folder in the Application Scripts directory and execute it.

  • Pros:
    • Scripts would be stored compiled and ready to run.
    • Portable. Scripts would be part of the action data, and synced and backed up to iCloud and be shareable via the Action Directory and/or export mechanisms.
  • Cons:
    • Requires special permission to write to the Application Scripts directory.
    • No visibility of script content within Drafts.
    • Changes to the script would require it be re-imported into Drafts.

Option #3: Drafts stores the scripts in plain text format

The plain text version of the AppleScript would be stored in Drafts. It could be previewed and edited in Drafts directly, much like Javascript is now.

  • Pros:
    • Visibility. Users could read/preview script contents in Drafts, in the action directory, etc., before running the action.
    • Portable. Scripts would be part of the action data, and synced and backed up to iCloud and be shareable via the Action Directory and/or export mechanisms.
  • Cons:
    • Requires special permission to write to the Application Scripts directory.
    • Maybe slow(?). Likely caching of compiled versions would be possible, but also might require the script to be compiled before running each time.
    • Drafts action manager is not the idea environment to edit/test scripts.

Conclusion

Thoughts on which of these options seem most attractive? Ideas for other approaches?

On a related note, since it’s likely to get asked…AppleScripts written for Drafts will look something like the below. Drafts will call a subroutine (tentatively using execute for the name of the subroutine, but that could change), passing a record with values from the current draft. If the subroutine returns a value (text, number, list, record, etc.) it will be made available to subsequent script steps in the Action.

on execute(draft)
    tell application "Finder"
        activate
        set msg to content of draft
        display dialog msg buttons {"OK"} default button "OK"
    end tell
    --optionally this could return values to Drafts
    --which would be available to subsequent steps in an action
    --return "Hello Drafts!"
end execute

1 or 2. As I only use Drafts on one Mac and multiple iOS devices (which won’t have AppleScript), I lean towards 1. Sounds easier to implement and easier to make changes in the script.

2 is also nice with the syncing, but the reimporting sounds like a pain if debugging/making changes to a script. Sync is relatively easy through other means.

3 sounds like a lot of work and less than ideal. AppleScript is strange enough to me that I wouldn’t want to do it outside of script editor.

1 Like

I would consider Option 2 to be the most favorable. I narrowed it down to Options 2 and 3 since I would want scripts to be backed up and synced. I selected Option 2 over 3 since I think the script editor is the correct place to develop and edit scripts. Re-importing scripts would not present a significant inconvenience. Thanks for all the work!

1 Like

My vote is for Option 3. It would match up pretty well with the way Keyboard Maestro handles AppleScript steps, and that’s been pretty successful.

To me, the only con of significance is the special permission to write to Application Scripts, and even that doesn’t seem like a big deal if it means what I think. Is it that I would have to grant Drafts permission—once—to write to that directory? People who use automation apps on the Mac are used to things like that.

I really doubt the compilation would be slow. I have shell and Python scripts that use osascript to compile and run AppleScript, and I’ve never noticed them being significantly slower than any regular compiled AppleScript. Do you know if Keyboard Maestro compiles its AppleScript steps on the fly? It never seems slow.

Even though Option 3 would require the AppleScript code to be saved within Drafts, I’d never write it there. Instead, I’d do what I do with AppleScript in Keyboard Maestro: write, edit, and test in Script Editor (or Script Debugger) and then copy into a Drafts action step when I think it’s ready.

3 Likes

I would vote for Option 3. The presumed one-time permission request is not too burdensome and what people are getting used to. So long as it’s well explained why - which I think is entirely feasible/likely.

1 Like

I’d vote for option 3, for much the same reasons Dr. Drang noted. And I would definitely use this feature.

2 Likes

Another vote for Option 3 — I already do my (limited) JavaScripting in a different editor and copy into the action when it’s ready to go.

I’m fine with self-managing (Option 1), but as someone who benefits from others actions a lot when developing my own, losing sharing of scripts is a big con.

If I interpret Option 2 correctly, you could conceivably download a shared AppleScript action and run it without seeing the source, which I think makes it a no-go from a security standpoint.

1 Like

I’ve gotten option 3 running. It does not seem that performance is much of an issue, at least for the small size scripts this will mostly be used to trigger. It is the option I like best as well, and will likely go with it. For those interested, the technical implementation goes more or less like this:

  • The first time an AppleScript step is run on a particular Mac, you will get an “Open” dialog pre-selecting the ~/Library/Application Scripts/com.agiletortoise.Drafts-OSX folder requesting you give write permissions to that folder to Drafts. This will be persistent for the most part…but if those permissions are every lost, you will just be prompted again.
  • When run, Drafts will compile the script text saved in the action to a file on disk using osacompile from a script that ships with Drafts in its app bundle.
  • That compiled script is moved to the folder mentioned above, so that the sandbox will allow it to be executed, then it is run, calling the execute subroutine with a single draft parameter - which is an object which contains keys with information about the current draft (uuid, content, etc.).
  • If the script returns a value which is convertible to JavaScript-friendly objects - basically core data types like text, date, or lists or records containing core data types - those values will be bridged to the javascript context so they can be used in subsequent script steps in the action. So, say you run an AppleScript that gathers information about open tabs in Safari, you can pass back that data to be inserted in a draft. Still sorting out exactly what that API looks like, but it will be something like how Callback URL responses are exposed.
  • Draft then cleans up after itself, deleting the temporary script files.
3 Likes

I’ve written the Javascript integration now as well. Compiling on the fly makes this a lot easier, too. Calling AppleScript from directly in JavaScript will enable more flexibility to call named subroutines, pass any parameters to the AppleScript, and get the results directly in the script. Example syntax:

let method = "execute";
let script = `on execute(bodyHTML)
	tell application "Notes"
		activate
		tell account "iCloud"
			make new note at folder "Testing" with properties {body:bodyHTML}
		end tell
	end tell
end execute`;

let html = draft.processTemplate("%%[[draft]]%%");

let runner = AppleScript.create(script);
if (runner.execute(method, [html])) {
	//
}
else {
	alert(runner.lastError);
}

Should be able to get this stuff in a beta in the next couple of weeks.

2 Likes