JXA and AppleScript compared via HyperCard
In A HyperCard Time Machine I wrote that I also seriously considered writing the script as a JavaScript (JXA) app. In fact, I did write the script as a JavaScript app. It was a toss-up to the end which version I was going to use, so they’re both fully-working versions.
One advantage the JXA version has is not requiring old-school, BASIC-style manipulation of strings to extract the current card number and the total card count from the window title. Instead of grabbing the location of the final slash, which required serious gymnastics in AppleScript, the JavaScript solution can use a regular expression that is both shorter and more reliable:
[toggle code]
- titleRegex = new RegExp("^(.+[^ ]) *([1-9][0-9]*) *\/ *([1-9][0-9]*)$");
- …
- titleParts = titleRegex.exec(title);
- filename = titleParts[1];
- currentCard = titleParts[2];
- totalCards = titleParts[3];
JXA also doesn’t use tell
blocks. It places the application in a variable, and the things we want to tell it to do can be handled at any point without worrying about how a tell block affects the syntax. At the beginning of the app, I did:
- app = Application.currentApplication();
- app.includeStandardAdditions = true;
- system = Application("System Events");
This gave me the current application and System Events. The current application is necessary for dialog with the user, which is why I’ve also included the Standard Additions on it. The Standard Additions include Display Dialog and Choose Folder.
Because JXA doesn’t use tell
blocks, it is easier to place the request for the destination folder after the check to see if HyperCardPreview is running and has at least one open window.1
Where the JXA solution falls down, and, ultimately why I decided not to use it, is that it contains some very odd behavior. The lesser of them is that there is no equivalent to Quoted Form Of
in JXA. That’s not a killer, because a functional equivalent can easily be added.
[toggle code]
-
function quotedFormOf(string) {
- //Not sure why replacing with multiple apostphrophes is necessary
- //But it matches what is necessary on the command line in Terminal
- return "'" + string.replaceAll("'", "'\\''") + "'";
- }
Someone with more knowledge of the command line can probably tell me why double-apostrophes are necessary to properly escape internal apostrophes.
Interestingly, JXA also doesn’t contain a POSIX path of
equivalent. It doesn’t need it. It appears to use the POSIX path by default. Simply converting a path to a string results in the path being a POSIX path.
- destination = app.chooseFolder({withPrompt:"Where do you want to save the screen shots?"});
- destination = destination.toString();
The above will provide the path to the folder using slashes rather than drive name and colons. This is likely to cause some problems in other cases, but in this case that’s what I need anyway, since the actual screenshot is handled by /usr/sbin/screenshot
.
More weirdly, this fails:
- stackPreview.menuBars[0].menuBarItems.byName('Browse').menuItems.byName("Go to Next Card").click();
That’s even though this does not fail:
- stackPreview.menuBars[0].menuBarItems.byName('Browse').click();
That will successfully cause HyperCardPreview’s Browse menu to drop down. The solution is to use the apparently-not-equivalent syntax of:
- stackPreview.menuBars[0].menus.byName('Browse').menuItems.byName("Go to Next Card").click();
That’s how I referred to the menus anyway in the AppleScript version. This is a weird discrepancy, but not a critical one. The same discrepancy appears in AppleScript. This will pull down the menu:
[toggle code]
-
tell application "System Events"
-
tell application process "HyperCardPreview"
- set frontmost to true
- click menu bar item "Browse" of menu bar 1
- end tell
-
tell application process "HyperCardPreview"
But click menu item "Go to Next Card" of menu bar item "Browse" of menu bar 1
“Can’t get menu item "Go to Next Card"”. The similar syntax click menu item "Go to Next Card" of menu "Browse" of menu bar 1
is required, just as in JavaScript.
I first attempted to do the screenshots using System Events to invoke the screenshot with CMD-SHIFT-4 (or CMD-$). I abandoned this approach because the resulting files didn’t have useful names and because they were all deposited on the Desktop. But the technique does show how sometimes the JavaScript and AppleScript versions coincide nearly exactly.
The AppleScript version was:
[toggle code]
- --take the screenshot
-
tell application "System Events"
- keystroke "$" using {command down}
- delay 0.1
- keystroke " "
- keystroke return
- end tell
It’s simple enough to understand, and the JavaScript version is just as simple:2
- system = Application("System Events");
- …
- //take the screenshot
- system.keystroke("$", {using: ['command down']});
- delay(0.1);
- system.keystroke(" ");
- system.keystroke("\r");
Sending keyCode 13 or keyCode 10 at the end fails, but using the Unix equivalent of AppleScript’s return
variable works fine.
I suspect that some people will see the AppleScript version as cleaner, and some the JavaScript version. If you decide you prefer the JavaScript solution, it, like the AppleScript version, needs to be saved as an application, and for the same reason: you need to grant it permission to control applications on your computer and do screen recording without annoying messages. Every time you update the app, manually remove it from both the Accessibility list and the Screen Recording list under the Privacy pane of the Security & Privacy settings, then re-add it by dragging the app in to each list. See the parent post for more details.
This is probably thinking far too much, for a script that is rarely going to be used—after all, I’m not creating new HyperCard stacks nowadays—about whether or not to switch out the AppleScript version for the JXA version. But it was interesting. There are advantages to each; ultimately I prefer the readability of the AppleScript version, despite the more complicated string manipulation in AppleScript and the use of variables in place of tell blocks in JXA.
The main reason for that is likely that this app is more about communication and less about data manipulation. AppleScript excels at the former.
Here is the full code:
[toggle code]
- // do a screen capture of all cards in a Hypercard Stack
- // Jerry Stratton, astoundingscripts.com
- app = Application.currentApplication();
- app.includeStandardAdditions = true;
- system = Application("System Events");
- titleRegex = new RegExp("^(.+[^ ]) *([1-9][0-9]*) *\/ *([1-9][0-9]*)$");
- //get app by search so that a failure does not stop the script
- stackPreview = system.applicationProcesses.whose({name:"HyperCardPreview"});
-
if (stackPreview().length < 1 || stackPreview[0].windows().length < 1) {
- app.displayDialog("Open and size the stack first.", {buttons: ["Cancel"]});
-
} else {
- //choose folder to save into
- destination = app.chooseFolder({withPrompt:"Where do you want to save the screen shots?"});
- destination = destination.toString();
- stackPreview = stackPreview[0];
- stackPreview.frontmost = true;
- //determine the rect to copy
- stackWindow = stackPreview.windows()[0];
- position = stackWindow.position();
- size = stackWindow.size();
- rectString = position[0] + ',' + position[1] + ',' + size[0] + ',' + size[1]
- command = "/usr/sbin/screencapture -R " + rectString + " ";
- //loop through each card and save as a screenshot
- goToNextCard = stackPreview.menuBars[0].menus.byName('Browse').menuItems.byName("Go to Next Card");
-
while (true) {
- //create filename using the current card number from the window title
- //windows() must be run on each interation, or it fails when going to the next card
- stackWindow = stackPreview.windows()[0];
- title = stackWindow.title();
- titleParts = titleRegex.exec(title);
- filename = titleParts[1];
- currentCard = titleParts[2];
- totalCards = titleParts[3];
- filename = destination + '/' + currentCard + ' ' + filename + '.png';
- app.doShellScript(command + quotedFormOf(filename));
-
if (currentCard == totalCards) {
- break;
- }
- goToNextCard.click();
- }
- }
-
function quotedFormOf(string) {
- //Not sure why replacing with multiple apostphrophes is necessary
- //But it matches what is necessary on the command line in Terminal
- return "'" + string.replaceAll("'", "'\\''") + "'";
- }
In response to A HyperCard Time Machine: Use AppleScript and HyperCardPreview to archive a screenshot of every card in a HyperCard stack.
Since I use this script under HyperCardPreview’s script menu, checking to see if the app is running is mostly superfluous: the script is only available if the app is running. It does help for testing it under Script Editor, and that’s about it.
↑In both versions the script also has to correctly position the mouse; as I mentioned in the previous post, I used cliclick for that. I had to add 4 to both the x and y values of HyperCardPreview’s window position to move it down and over from the top left. Otherwise,
↑screenshot
grabbed whatever was under the HyperCardPreview window. This is likely because windows in macOS have rounded corners.
- cliclick via Homebrew at Homebrew
- “Tool for emulating mouse and keyboard events.”
- HyperCardPreview: Pierre Lorenzi
- “This application displays HyperCard stacks in Mac OS X… it does not edit them, it does not execute them… The look is very close to the original one, with bitmap fonts, old-style scrollers, aliasing. In the Home stacks the look is accurate to the pixel, as in most Apple stacks, but less so if there are colors, and not at all if there are XCMDs.”
More AppleScript
- Find all parent mailboxes in macOS Mail
- The macOS Mail app seems to want to hide the existence of mailboxes and any sense of hierarchical storage. These two AppleScripts will help you find the full path to a selected message and open the message’s mailbox.
- Using version control with AppleScripts
- AppleScripts aren’t stored as text, which makes it impossible to track changes in AppleScript files using version control software such as Mercurial or Git.
- Save clipboard text to the current folder
- Use the Finder toolbar to save text on the current clipboard directly to a file in the folder that Finder window is displaying.
- Adding parenthetical asides to photograph titles on macOS
- Use Applescript to append a parenthetical to the titles of all selected photographs in Photos on macOS.
- AppleScript, variables, and dropped filenames in Automator
- Automator is a simple workflow system for Mac OS X. By its nature it is very procedural: one task follows another; workflows don’t loop and they don’t store variables for later. However, this is possible in Automator and while it adds complexities it can also solve problems such as wanting to save dropped filenames for later use.
- 17 more pages with the topic AppleScript, and other related pages