Drafts: Methods for Running AppleScript

Drafts is utilitarian text app available on iOS, iPadOS, and macOS that I use frequently. To say I’m a daily user would not do justice to the app. As the tag line says this is the app where text starts, and that’s certainly true of my use cases. It provides a quick point of capture and powerful tools to let me do more with the text to prepare and send it to its final destination than any other app possible could; and that says a lot when you look at apps like Keyboard Maestro, Text Soap and Sublime Text!

In the latest release (version 19) of the app on macOS, developer Greg Pierce (aka AgileTortoise) has significantly extended the automation and processing scope through the addition of AppleScript, and in this post I’m going to walk you through some example set-ups related to this.

Scripting in Drafts Version 19

This latest version of Drafts includes several key new features, of which AppleScript support is just one. In this post I’m going to specifically cover the triggering of AppleScript from within the Drafts actions framework.

But in this release, Greg also included limited AppleScript support for Drafts itself. Thus allowing you to control Drafts from AppleScript outside of the app. As noted, it is limited right now, only allowing you to create drafts, and it isn’t something I’m going to cover in this post.

The release also includes support for running shell scripts so you can utilise Zsh, Bash, Perl, Ruby, Python, etc. from within Drafts. I’ll probably cover more on this in a future post, but I will touch on this later as it also opens up a useful opportunity with AppleScript.

As a note, not of warning, but more for awareness, you will notice that when you run your first local script-related action, you are prompted to permit access for Drafts to its script directory (at ~/Library/Application Scripts/com.agiletortoise.Drafts-OSX). This is normal and you should just be prompted once. This is just to comply with the macOS security requirements and is necessary because to run any of the local scripts, Drafts will need to temporarily create script files on the file system to enable them to be run.

AppleScript as a Native Action Step

Probably the simplest way to add AppleScript to your Drafts actions is via the new AppleScript action. You can simply add A new AppleScript action step and then add the AppleScript content to the action.

While it’s even easier to add AppleScript than it is to add JavaScript to the action (one click and field entry vs. two clicks and a new window), it doesn’t have any syntax highlighting, validation, etc. and so the general recommendation, that I fully support, is to write your AppleScript in another editor and then paste it in.


If you don’t have Late Night Software’s Script Debugger app for AppleScript, go and get it right now, then come straight back. You can thank me later!


The script is based around a subroutine called execute, and so the step should always contain a subroutine of that name. Since it will always be passed the draft object, you should always include this too.

on execute(draft)
	-- Your main script goes here
end execute

You can also return a result from the AppleScript allowing you to process the draft contents outside of Drafts and return a result. This result is available within subsequent (JavaScript) Script action steps as an array, accessed using context.appleScriptResponses.

Throughout this post I’m going to use an example script as the basis for showing you how this works. In each case the AppleScript will translate the content it is passed to NATO phonetic alphabet. In each case, the translated content will be returned to Drafts. In some cases, it will also do something that’s very easy in AppleScript, but not so much in JavaScript; but the main point is that I wanted to use something that was relatively easy to follow, and was not dependent upon any additional third party apps.

Example Action - Embedded Action

Download: AS Embedded Action

This action has two steps. The first step runs an AppleScript step that converts the contents of the current draft into phonetic words. Should the draft happen to be empty, it will prompt you to enter text to be converted.

The second step takes the resulting array return, and appends it to the end of the current draft.

STEP: Run AppleScript

on execute(draft)
	if content of the draft is "" then
		set strOriginal to text returned of (display dialog "Enter string to convert" default answer "")
	else
		set strOriginal to content of the draft
	end if
	
	return processString(strOriginal)
end execute


on processString(p_strInput)
	set strConverted to listToString(stringToPhonetic(p_strInput, ""), " ")
	-- Because we're passing a whole draft, let's forego speaking the conversion
	-- say strConverted
	return strConverted
end processString

on stringToPhonetic(p_strInput)
	--Initialise alphabets and output
	set strBase to "abcdefghijklmnopqrstuvwxyz0123456789"
	set listPhonetic to {"alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliett", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey", "x-ray", "yankee", "zulu", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}
	set strOutput to {}
	
	set listInput to stringToList(p_strInput, "")
	
	--For every character in the text to be converted, check if it is in the base alphabet string.
	repeat with i from 1 to length of listInput
		if strBase contains item i of listInput then
			--If it is, get the position, then get the item at the equivalent position in the NATO phonetic alphabet list
			set intPosition to offset of (item i of listInput as string) in strBase
			set end of strOutput to item intPosition of listPhonetic
		else
			--If it is not, just put the original untransformed character in
			set end of strOutput to item i of listInput
		end if
		
	end repeat
	return strOutput
end stringToPhonetic


on stringToList(p_strInput, p_strTempDelimiter)
	--Save and switch delimiter
	set strOriginalDelimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to p_strTempDelimiter
	
	--Convert string to list
	set listOutput to (every text item of p_strInput)
	
	--Switch delimiter back and return new list
	set AppleScript's text item delimiters to strOriginalDelimiter
	return listOutput
end stringToList

on listToString(p_listInput, p_strTempDelimiter)
	--Save and switch delimiter
	set strOriginalDelimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to p_strTempDelimiter
	
	--Convert list to string
	set listOutput to p_listInput as string
	
	--Switch delimiter back and return new string
	set AppleScript's text item delimiters to strOriginalDelimiter
	return listOutput
end listToString

STEP: Script

draft.content = draft.content + "\n" + context.appleScriptResponses[0];
draft.update();

The big advantages of this script step are that it is really easy to utilise, and if you are sharing the action, the AppleScript will be shared with it.

This is great if you want to trigger some AppleScript independent of Drafts (e.g. to open an SFTP app and upload a file having exported it from Drafts), or if you want to work with a whole draft (e.g. parse a standard format draft and input the data into another app). But what if you just wanted to do something with a part of the draft? For example, the current text selection.

Since this action step always runs on the current draft, the script isn’t aware of anything else about the Drafts app unless you pass that in via the draft, or via another channel (clipboard, information file the script can read in, etc.) Fortunately the next method allows us to address this limitation.

AppleScript as JavaScript

As well as the Run AppleScript action step, Drafts also provides an AppleScript Object for JavaScript. This provides some additional flexibility, including the ability to pass in whatever you like to the script.

You can create an instance of an AppleScript object to work with by using the create method passing it the AppleScript to execute. The execute method can then be used to run the script, passing in the name of the subroutine to execute.

A simple “Hello World” script utilising AppleScript can be created like this.

let strAS = `
on hw()
	display dialog "Hello World"
end hw
`;

let asRunScript = AppleScript.create(strAS);
asRunScript.execute("hw")

Because we can now dynamically build up the AppleScript to run, and we have access to the Drafts object model from JavaScript, we are no longer restricted to just working with the content of the current draft.

You can also define the JavaScript and AppleScript object once, but choose to run different subroutines on different execution calls.

Also we can still share this easily with others as the script is still embedded in the action. But again, writing and debugging the script within the JavaScript is only suitable for simpler scripts and the over confident. Do consider writing your scripts outside of Drafts and then copying them in.

A “Hello World” example is all well and good, but lets take a look at how we can implement the NATO phonetic alphabet example to return the content and also how we can use the AppleScript object’s error handling.

Example Action - JavaScripted Action

Download: AS JavaScripted Action

This action has just a single script step. Here, rather than passing in the entire draft, the action will utilise the current text selection. If nothing is selected, then the action will prompt you to enter text to be converted.

If successful, the converted text is once again appended to the end of the current draft, but this time, because we almost certainly have a much shorter piece of text than the entire content of the draft, I’ve enabled an additional line of code that speaks the translation. So fair warning, don’t select a lot of text when you’re testing this one out.

STEP: Script

let strAS = `
on execute(p_strInput)
	if p_strInput is "" then
		set strOriginal to text returned of (display dialog "Enter string to convert" default answer "")
	else
		set strOriginal to p_strInput
	end if
		
	return processString(strOriginal)
end execute


on processString(p_strInput)
	set strConverted to listToString(stringToPhonetic(p_strInput, ""), " ")
	-- Because we're passing in a selection, let's speak it too
	say strConverted
	return strConverted
end processString

on stringToPhonetic(p_strInput)
	--Initialise alphabets and output
	set strBase to "abcdefghijklmnopqrstuvwxyz0123456789"
	set listPhonetic to {"alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliett", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey", "x-ray", "yankee", "zulu", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}
	set strOutput to {}
	
	set listInput to stringToList(p_strInput, "")
	
	--For every character in the text to be converted, check if it is in the base alphabet string.
	repeat with i from 1 to length of listInput
		if strBase contains item i of listInput then
			--If it is, get the position, then get the item at the equivalent position in the NATO phonetic alphabet list
			set intPosition to offset of (item i of listInput as string) in strBase
			set end of strOutput to item intPosition of listPhonetic
		else
			--If it is not, just put the original untransformed character in
			set end of strOutput to item i of listInput
		end if
		
	end repeat
	return strOutput
end stringToPhonetic

on stringToList(p_strInput, p_strTempDelimiter)
	--Save and switch delimiter
	set strOriginalDelimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to p_strTempDelimiter
	
	--Convert string to list
	set listOutput to (every text item of p_strInput)
	
	--Switch delimiter back and return new list
	set AppleScript's text item delimiters to strOriginalDelimiter
	return listOutput
end stringToList

on listToString(p_listInput, p_strTempDelimiter)
	--Save and switch delimiter
	set strOriginalDelimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to p_strTempDelimiter
	
	--Convert list to string
	set listOutput to p_listInput as string
	
	--Switch delimiter back and return new string
	set AppleScript's text item delimiters to strOriginalDelimiter
	return listOutput
end listToString
`;

let asRunScript = AppleScript.create(strAS);

if (asRunScript.execute("execute", [editor.getSelectedText()]))
{
	draft.content = draft.content + "\n" + asRunScript.lastResult;
	draft.update();
}
else
{
	console.log(asRunScript.lastError);
	context.fail(asRunScript.lastError);
}

Brilliant. We now have two AppleScript methods we can use. They vary a little in complexity, but not too much, and we can use the returned values, and share the scripts with other people relatively easily.

Unfortunately, I had a third use case that neither of these inbuilt approaches covered. I already have AppleScripts that I have in place and trigger through other methods, but that would be nice to trigger at the end of actions. Consider the SFTP example I mentioned above. Maybe I have several apps that could produce an update file and Drafts is just one of them. Wouldn’t it be nice to keep a single source AppleScript and have all apps, including Drafts, be able to run that script?

Well, time for method three.

AppleScript as a Script

First of all I’m going to start with highlighting that there’s probably more than one way to resolve this issue.

  • I could use a tool like Keyboard Maestro to control Drafts to run an action, and then run the external AppleScript.
  • I could use AppleScript and URL schemes to perhaps trigger a Drafts action, wait a short period, and then run the other AppleScript.

I’m sure there are many other control mechanisms outside of Drafts that could facilitate this. But reliable control of timing is often an issue with these sorts of things. Not necessarily insurmountable, but typically inefficient, and almost always rather fragile. Therefore, having a method I can utilise and control from within Drafts seemed like it would be a better approach.

This is where we return to Drafts’ new functionality to run shell scripts.

Unlike AppleScript, this isn’t (currently) available as an action step, but in the same way that Drafts has been given an AppleScript object for use in its Script step, there’s now also a ShellScript object for running shell scripts.

The structure for running a shell script is very similar to way an AppleScript is run. The object is created and a string defining the script is passed to an execution method, but this time there’s no subroutine to specify.

Now, one of the things you can do from a shell script is run another script. This includes running an AppleScript script by use of the osascript command (see SS64 reference for details). We can therefore use this to run an AppleScript that’s held somewhere out in the file system.

That’s right. I’m running an AppleScript script, from a command in a shell script, that’s embedded in a JavaScript-based, Script step in a Drafts action. Let’s take a look at the example.

Example Action - External Call Action

Download: AS External Call Action

This action once again has just a single script step. Like the previous example, rather than passing in the entire draft, the action will use only the current text selection. If nothing is selected, then the AppleScript’s logic will prompt you to enter text to be converted.

If successful, the converted text is, as ever, appended to the end of the current draft, and once again, in the AppleScript I’ve used, I’ve enabled speaking of the translation.

Because everything is not embedded in the action, it requires more set-up, so pay close attention.

STEP: Script Here’s the script step that is contained in the action. The bulk of the script is a function to run an external AppleScript. The idea was to make the bulk of the code reusable. It accepts a path to an AppleScript script, and a text string of arguments.

Below, you can see the runExternalAppleScript function is passed the path to my AppleScript (nato.scpt), which is held in an iCloud folder, and the current text selection. Once again, because we’re running this via JavaScript, we get access to everything in the object model for Drafts that’s available to JavaScripting in the app.

function runExternalAppleScript(p_strASPath, p_strArgument)
{
	// Initialise the script
	let runScriptContents = `#!/bin/zsh	
osascript "${p_strASPath}" "${p_strArgument}"
`;
	let runScript = ShellScript.create(runScriptContents);
	// Run the script and if necessary deal with any errors
	if(!runScript.execute())
	{
		context.fail(`Running "${p_strASPath}" failed.`);
		console.log("STDERR: " + runScript.standardError);
	}
	// Always log and return the standard output on a success or failure
	console.log("STDOUT: " + runScript.standardOutput);
	return runScript.standardOutput;
}

draft.content = draft.content + "\n" + runExternalAppleScript("/Users/stephen/Library/Mobile Documents/com~apple~CloudDocs/AppleScripts/nato.scpt", editor.getSelectedText());
draft.update();

Here’s a copy of the AppleScript that is held in that nato.scpt file. If you are reproducing this, please remember to change the path used in the action Script step (above) to point to your location of this file. It is of course pretty similar to the AppleScript used previously.

--NATO Phonetic Alphabet

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

on run argv
	if (count argv) is 0 then
		set strOriginal to text returned of (display dialog "Enter string to convert" default answer "")
	else
		set strOriginal to item 1 of argv
	end if
	
	return processString(strOriginal)
end run

on processString(p_strInput)
	set strConverted to listToString(stringToPhonetic(p_strInput, ""), " ")
	say strConverted
	return strConverted
end processString

on stringToPhonetic(p_strInput)
	--Initialise alphabets and output
	set strBase to "abcdefghijklmnopqrstuvwxyz0123456789"
	set listPhonetic to {"alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliett", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey", "x-ray", "yankee", "zulu", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}
	set strOutput to {}
	
	set listInput to stringToList(p_strInput, "")
	
	--For every character in the text to be converted, check if it is in the base alphabet string.
	repeat with i from 1 to length of listInput
		if strBase contains item i of listInput then
			--If it is, get the position, then get the item at the equivalent position in the NATO phonetic alphabet list
			set intPosition to offset of (item i of listInput as string) in strBase
			set end of strOutput to item intPosition of listPhonetic
		else
			--If it is not, just put the original untransformed character in
			set end of strOutput to item i of listInput
		end if
		
	end repeat
	return strOutput
end stringToPhonetic

on stringToList(p_strInput, p_strTempDelimiter)
	--Save and switch delimiter
	set strOriginalDelimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to p_strTempDelimiter
	
	--Convert string to list
	set listOutput to (every text item of p_strInput)
	
	--Switch delimiter back and return new list
	set AppleScript's text item delimiters to strOriginalDelimiter
	return listOutput
end stringToList

on listToString(p_listInput, p_strTempDelimiter)
	--Save and switch delimiter
	set strOriginalDelimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to p_strTempDelimiter
	
	--Convert list to string
	set listOutput to p_listInput as string
	
	--Switch delimiter back and return new string
	set AppleScript's text item delimiters to strOriginalDelimiter
	return listOutput
end listToString

Conclusion

I think there’s a place for each of these methods, and hopefully the following table below help clarify the points above and help you in deciding what to use when. Just make sure you check it out - this update to Drafts allows you to tap into the phenomenal power that underpins all of the apps on your Mac, and opens up a world of intercommunication between apps as well. Good luck with your scripting endeavours, and I’m looking forward to seeing how everyone really pushes the use of scripting over on the official Drafts forum, where you might even find me hanging out under the handle @sylumer.

  Pros Cons
Embedded AppleScript Step
  • Couldn't really be any easier to use.
  • To share, you just need to share the action.
  • Unable to incorporate more than just the contents of a draft into the script.
  • Have to copy the code into Drafts.
  • Have to maintain a separate copy and copy back, or potentially copy out/in for updates.
JavaScripted AppleScript
  • To share, you just need to share the action.
  • Able to incorporate more than just the contents of a draft into the script.
  • Slightly more complicated to use when compared to the Run AppleScript step.
  • Have to copy the code into Drafts.
  • Have to maintain a separate copy and copy back, or potentially copy out/in for updates.
External AppleScript
  • Able to incorporate more than just the contents of a draft into the script.
  • Able to maintain a single shared AppleScript script across apps/OS.
  • AppleScript code does not need to be copied into, or out of, Drafts.
  • Slightly more complicated to use when compared to the Run AppleScript step.
  • To share, you need to share the action, and the script, and the other person needs to modify the action to point to the script.
Author: Stephen Millard
Tags: | drafts | applescript |

Buy me a coffeeBuy me a coffee



Related posts that you may also like to read