First Experience with AppleScript

✍🏼 Written on May 3, 2022    💡 Updated on Jan 5, 2023
❗️ Note: it has been days since this article was written, please be aware of its timeliness
🖥  Note:I briefly experimented with AppleScript and introduced some common user interaction methods—it feels incredibly powerful.
📚  Also published on Craft: https://www.craft.do/s/RsgimVt9VBxAmg

Preface

Recently, I purchased a new Mac Studio equipped with an M1 Max chip and 64GB of RAM, delivering exceptional performance. Meanwhile, in my previous blog processing workflow], I frequently encountered slow network speeds when uploading images pulled from Craft (this step is fast since both are overseas) to Tencent Cloud COS using GitHub Actions.

SCR-20220502-q47

After some research, I discovered that GitHub does not support setting the region for Action servers]. Given this limitation, I decided to move the publishing workflow locally to speed up the build process, hence this article. Below, AppleScript will be abbreviated as AS.

Why Use AppleScript Instead of Other Options?

First and foremost, I wanted to minimize the operational steps and complexity, so I defined the workflow as follows:

  1. Click the publish button in Craft.
  2. Perform a local build while displaying the build log in the terminal.
  3. Push the build results to GitHub.

Since this requires one app to invoke another via a link, AppleScript is the only viable option. However, although AS is used, it essentially executes the existing JavaScript files—just moving the workflow tasks from craft_publish_ci] to local execution.

Introduction to AppleScript

Conceptual diagram:

execute_script_2x

What AppleScript Can Do

  1. Interact with users, such as responding to input or sending notifications via Notification Center.
  2. Control other apps—either directly if the app provides AS interfaces or indirectly by manipulating UI elements like buttons if no interface is exposed.
  3. Automate tasks like sending emails, running scheduled scripts, opening music players, locking the screen, or executing scripts in response to custom URL Schemes.

Basic Syntax

  • Prefix a line with --- (three hyphens) or # to comment it out.
  • Use (*``*xxx**``) (parentheses + asterisk) to wrap xxx for block comments.
  • The rest is similar to spoken language, but if you’re unsure about English grammar (e.g., the nuances of “besides,” “against,” or “over”), stick to basic syntax.

Forms of AppleScript

AppleScript commonly comes in two forms: Script and Application. Here, I’ll only cover these two, as I haven’t used Script Bundle or Text.

SCR-20220502-qqc

Note: Even with identical code, the interface differs when saved as a script versus an application.

Script

A script is an executable file, similar to JS, but AS runs in the macOS desktop environment while JS runs in Chrome. Double-click to execute. Below is the progress display of a script process, launched by clicking the “Run” button in the script’s top-right corner:

SCR-20220502-r3e

Application

An AS saved as an Application has the same suffix as regular apps (e.g., Safari, Chrome, WeChat) and ends with .app. You can also inspect its package contents. It supports multiple launch methods: URL Scheme invocation, double-clicking, or dragging content onto the app icon. You’ll need to write event handlers to respond to user actions. Below is the progress display of an application process, launched by double-clicking “xxx.app”:

SCR-20220502-r33

Others

Beyond these two, other forms are less common. However, if you need to save a script as a Service to invoke it from the menu bar or right-click context menus, you’ll need a different format:

SCR-20220502-rcr

Saving the script as a system Service displays the progress bar in the top menu bar:

scriptmenu_progress_2x

Common Operation Code

The following operations are based on the “Application” type of AS. The syntax can be quickly tested in the terminal using osascript -e '语法内容'. Note that multiline expressions, like progress bars, cannot be executed via osascript. While you can chain multiple -e parameters for multiline commands like tell (e.g., osascript -e ‘tell application “Finder”’ -e ‘end tell’), this doesn’t work for progress bars.

User Interaction

Display Messages in Notification Center

1
display notification "通知内容" with title "通知 title" subtitle "通知副标题"

SCR-20220502-sfl

Pop-up Dialogs

1
display dialog "这是个通知"

Image

Get User Input

1
display dialog "What's your name?" default answer "" with icon note buttons {"取消", "确认"} default button "确认"

Image

Note: AS cannot generate form-like components, only dialog boxes like the one above, similar to prompts in web pages.

Play Given Text

1
say "What is your name?" using "Alex" speaking rate 140 pitch 42 modulation 60

Others

Allowing users to select folders, files, colors, or choose an item from a list is temporarily omitted.

Execute Command Line

1
do shell script "echo $PATH"

Note: The PATH environment variable in the command line is /usr/bin:/bin:/usr/sbin:/sbin, so it cannot execute commands like node or nvm that were installed later. These commands require manually specifying their locations when executed.

Execute a Node script:

1
2
3
set node to "/Users/x/.nvm/versions/node/v14.19.1/bin/node"
set appPath to "/Applications/Xhelper.app/Contents/Resources/Scripts/"
do shell script node & " " & appPath & "index.js"

Retrieve Output from the Previous Statement

Directly use result immediately after the previous statement to represent the result:

1
2
do shell script "echo $PATH"
display dialog result with title "通知"

Image

Display Debug Information

Simply use the log statement, then retrieve the output in the Replies section below Script Editor. Here, you can see the content output to the console during the execution of the shell script command, such as console.log when executing a JS script.

1
log do shell script "echo $PATH"

SCR-20220502-sov

Show Progress

Displaying progress uses progress, and the syntax is relatively complex because it needs to show different statuses, requiring multiple lines:

1
2
3
4
5
6
7
8
9
set progress total steps to 3
set progress completed steps to 0
set progress description to "处理中..."
delay 1
set progress completed steps to 1
delay 1
set progress completed steps to 2
delay 1
set progress completed steps to 3

Related screenshots can be seen above.

Execute After a Delay

1
delay n

n represents the number of seconds, supporting decimals. This is generally used to simulate user operations with delays, preventing issues where scripts run too fast before the page loads. Some odd problems can also be resolved by delayed execution. This statement can be likened to the setTimeout technique in js.

Call Other Applications

Note: This will trigger a system warning requiring your confirmation to execute:

Image

After confirmation, you can verify in 设置-安全与隐私-隐私-自动化:

Image

You can call other applications to perform operations. Below is an example of launching the Terminal app, activating it, and executing a command:

1
2
3
4
5
tell application "Terminal"
if not (exists window 1) then reopen
activate
do script "echo $PATH" in window 1
end tell

This statement includes a check: if the first opened window exists, it activates it and executes the command (do script) inside; if not, it creates a new window and executes the command there. Since each do script opens a new Terminal, this check ensures window reuse. However, note that if window 1 already has an existing process (e.g., an unstopped server), the command will not execute and requires additional checks, which are not elaborated here.

Image

Respond to URL Scheme

Sometimes, you don’t want to first locate the AS script and double-click to run it, or you need to trigger your AS app from another application. This is where URL Scheme comes in. There’s an introduction on ](https://example.com) you can check out.

First, you need to modify the Info.plist file. You can find it in Finder by right-clicking “Show in Finder” in Script Editor’s sidebar:

Image

Navigate up one level to see the file:

Image

The content is XML-like. Edit it and add the following field:

1
2
3
4
5
6
7
8
9
10
<array>
<dict>
<key>CFBundleURLName</key>
<string>Open File</string>
<key>CFBundleURLSchemes</key>
<array>
<string>xhelper</string>
</array>
</dict>
</array>

Then verify by opening the file in Xcode to see a similar entry:

Image

This indicates success.

This operation means the AS app will respond to URLs starting with xhelper (you can modify this to any prefix you want, as long as it doesn’t conflict with existing ones).

After setting this up, you need to save it in Script Editor, or cmd + L it, or run it once (or do all three) to recompile and make it effective.

To make AS respond to the URL, you need to set up an event listener:

1
2
3
on open location this_URL
display dialog this_URL
end open location

In the code above, this_URL is the link opened with your xhelper.

Now, test a link in a browser (the screenshot shows Chrome):

Image

The AS app will respond and display a popup showing xhelper://你好!. Note that no URL encoding is required here.

My Usage

Here, I share my Craft build code, which uses the URL Scheme mentioned above. The Node-executed JS scripts are adapted from previous workflows ](https://example.com), with no new logic. Additionally, AS syntax is quite colloquial, so each line’s meaning is self-explanatory, and I won’t explain them one by one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
on open location this_URL
try
set startOffset to offset of "://" in this_URL
set the content to text from (startOffset + 3) to -1 of this_URL
set node to "/Users/x/.nvm/versions/node/v14.19.1/bin/node"
set appPath to "/Applications/Xhelper.app/Contents/Resources/Scripts/"
--- 如果有两个参数表示需要发布到 wechat,如果只有一个参数则参数即为 base64 编码的内容
set AppleScript's text item delimiters to "&"
set arguments to every text item of the content
set AppleScript's text item delimiters to ""
set listLength to the length of arguments
if listLength = 2 then
set realContent to item 2 of arguments
do shell script "echo " & realContent & " > " & appPath & "content.base64.txt"
tell application "Terminal"
if not (exists window 1) then reopen
activate
set alive to do script node & " " & appPath & "index.js" in window 1
--- 等待上一个脚本执行完毕后再执行下一个脚本
repeat
delay 0.1
if not busy of alive then exit repeat
end repeat
beep
do script node & " " & appPath & "wechat.js" in window 1
end tell
else
do shell script "echo " & content & " > " & appPath & "content.base64.txt"
tell application "Terminal"
if not (exists window 1) then reopen
activate
do script node & " " & appPath & "index.js" in window 1
end tell
end if
on error error_message
display dialog error_message buttons {"Cancel"} default button 1
end try

end open location

Result:

Image

- EOF -
Originally published at: First Experience with AppleScript - Xheldon Blog