There are several instruments on any good workbench, and we reach for the one that suits a subtask best.
AppleScript has a very mature automation interface, embedded in a small, slow and slightly neglected language with some obvious gaps and stumbling blocks.
JavaScript is a fast and actively updated language, with rich libraries, and a less documented and less polished app Automation interface.
On the one hand we have a good sharp old knife with an awkward handle and a single blade, and on the other a newer penknife, with a wider range of uses, but nothing to match the sharpness of that older blade for one or two things.
( Neither is on the shelf with the power-tools like Haskell and Rust, Lisp and C – this is just scripting, after all – small-scale coffee-break work – all you need is the old Opinel or new Swiss Army knife that you use for picnics … )
The main real gap in Apple’s Automation object for JS is that it can’t set a value across a whole collection in a single Apple Event, and whenever the speed and convenience of doing this are key, it’s probably an Execute AppleScript action that you want to reach for.
A classic example: toggling the expansion of many rows in OmniOutliner:
tell application "OmniOutliner"
if (count of documents) > 0 then
tell front document
set toggle to not expanded of first row
-- SET A VALUE RIGHT ACROSS A COLLECTION
-- (using just one Apple Event)
set expanded of rows to toggle -- (Applescript shines here)
if toggle then
"Expanded"
else
"Collapsed"
end if
end tell
else
"No outlines open in OmniOutliner"
end if
end tell
We can certainly do this in JS,
(() => {
// TOGGLING OO ROWS ------------------------------------------------------
const
rows = Application('OmniOutliner')
.documents[0]
.rows,
toggled = !rows[0].expanded();
return (
rows()
.map(x => x.expanded = toggled),
toggled ? 'Expanded' : 'Collapsed'
);
})();
but with larger outlines it’s noticeably slower (one Apple Event per row), which is distracting.
I personally also still often use a large old AppleScript which relies quite heavily on setting properties quickly across large collections of lines and shapes in OmniGraffle.
(omniJS is now even faster, but that’s another story, and my old Applescript still works, at least on the macOS version of OmniGraffle).
When setting properties over large collections is not an issue, I always reach for JavaScript – which I find more flexible, faster, with fewer gotchas, richer libraries and somehow just a more pleasant experience.
But, occasionally, even within JavaScript, I still reach for AppleScript. There are (real, though rather rare) cases when some part of a particular application’s automation interface just doesn’t work properly with JavaScript.
(pre-omniJS for example, there was an issue in OmniGraffle with automating the creation of parent-child links between shapes. It worked in AppleScript, and somehow choked on a datatype conversion in JavaScript).
One way of doing this is to run an AppleScript snippet from inside an Execute Javascript for Automation with something like an evalASMay function. Here’s an example of calling it, although this is a case in which I would probably just use AppleScript
Using an Applescript snippet from inside an Execute JXA action:
(() => {
'use strict';
// GENERIC FUNCTIONS -----------------------------------------------------
// evalASMay :: String -> Maybe String
const evalASMay = s => {
const
error = $(),
result = $.NSAppleScript.alloc.initWithSource(s)
.executeAndReturnError(error),
e = ObjC.deepUnwrap(error);
return e ? {
nothing: true,
msg: e.NSAppleScriptErrorBriefMessage
} : {
nothing: false,
just: ObjC.unwrap(result.stringValue)
};
};
// intercalate :: String -> [String] -> String
const intercalate = (s, xs) => xs.join(s);
// TOGGLE EXPANSION ACROSS OMNIOUTLINER ROW COLLECTION -------------------
const
ds = Application('OmniOutliner')
.documents,
mbBool = ds.length > 0 ? {
nothing: false,
just: ds[0].rows[0].expanded()
} : {
nothing: true
};
return mbBool.nothing ? (
'No document open in OmniOutliner'
) : (() => {
const strName = evalASMay(
'tell application "OmniOutliner" to tell front document\n\
set expanded of rows to ' + !mbBool.just + '\n\
name\n\
end tell'
)
.just;
// Value -------------------------------------------------------------
return intercalate(
' -> ', [strName, (mbBool.just ? 'collapsed' : 'expanded')]
)
})();
})();
and, of course, if you want to reach for bits of JavaScript libraries from inside an Execute AppleScript action, you can do the same, in the opposite direction.
Using a Javascript snippet from inside an Execute Applescript action:
(evalJSMay()
will only work if you include the line use framework "AppKit"
at the top of your AS code)
use framework "AppKit"
-- Javascript code -> Maybe AppleScript string (or Nothing with error msg)
-- evalJSMay :: String -> Maybe String
on evalJSMay(strCode)
set ca to current application
set oScript to ca's OSAScript's alloc's initWithSource:strCode ¬
|language|:(ca's OSALanguage's languageForName:("JavaScript"))
set {blnCompiled, compileError} to oScript's compileAndReturnError:(reference)
if blnCompiled then
set {descriptor, evalError} to oScript's executeAndReturnError:(reference)
if (evalError is missing value) then
{nothing:false, just:stringValue of descriptor as text}
else
{nothing:true, msg:NSLocalizedDescription of evalError as text}
end if
else
{nothing:true, msg:NSLocalizedDescription of compileError as text}
end if
end evalJSMay
-- TEST ------------------------------------------------------------------
on run
evalJSMay("Math.cos(.5)")
--> {nothing:false, just:"0.8775825618903728"}
evalJSMay("encodeURIComponent('A string of some kind')")
--> {nothing:false, just:"A%20string%20of%20some%20kind"}
end run