Release Notes

This article describes JavaScript for Automation, a new feature in OS X Yosemite.

Contents:

Introduction

JavaScript for Automation is a host environment for JavaScript that adds the following global properties:

OSA Component

The JavaScript OSA component implements JavaScript for Automation. The component can be used from Script Editor, the global Script Menu, in the Run JavaScript Automator Action, applets/droplets, the osascript command-line tool, the NSUserScriptTask API, and everywhere else other OSA components, such as AppleScript, can be used. This includes Mail Rules, Folder Actions, Address Book Plugins, Calendar Alarms, and Message Triggers.

Scripting Dictionaries

Scripting dictionaries detail the object model of applications. Terminology in scripting dictionaries is mapped to valid JavaScript identifiers following a set of conventions. The Script Dictionary viewer in Script Editor has been updated to show terminology in AppleScript, JavaScript, and Objective-C (Scripting Bridge framework) formats. You can open a scripting dictionary in Script Editor by using the File > Open Dictionary command or the Window > Library command.

Object Specifiers

Many of the objects in the JavaScript for Automation host environment refer to external entities, such as other applications, or windows or data inside of those other applications. When you access a JavaScript property of an application object, or of an element of an application object, a new object specifier is returned that refers to the specific property of that object. It is important to note that this object is not the actual value of the external entity's property; it is a reference to the object. To retrieve the value of a referenced property, an additional step is required, described in the Getting and Setting Properties section.

Accessing Applications

Applications can be accessed in these ways:

  1. Name

    Application('Mail')
  2. Bundle ID

    Application('com.apple.mail')
  3. Path

    Application('/Applications/Mail.app')
  4. Process ID

    Application(763)
  5. Getting an application on a remote machine

    Application('eppc://127.0.0.1/Mail')
  6. Getting the application that is running the script

    Application.currentApplication()

Syntax Examples

These are examples of the syntax for interacting with objects:

  1. Accessing properties

    Mail.name
  2. Accessing elements

    Mail.outgoingMessages[0]
  3. Calling commands

    Mail.open(...)
  4. Creating new objects

    Mail.OutgoingMessage(...)

Getting and Setting Properties

Properties of scripting objects are accessed as JavaScript properties using dot notation. As described above, the returned object is an object specifier; a reference to the accessed property, rather than the actual value. To send the "get" event to the external entity and return its value, you call the property as a function:

subject = Mail.inbox.messages[0].subject()

Similarly, setting a property sends the "set" event to the external entity with the data you want to set.

Mail.outgoingMessages[0].subject = 'Hello world'

Here is an example of getting a property from every element of an array (in this case, the subject of every message in Mail's inbox).

subjects = Mail.inbox.messages.subject()

Element Arrays

Elements are accessed by calling specific methods on arrays or using square brackets. The returned values are object specifiers, with their own properties and elements, that refer to the array elements. They can be accessed by:

  1. Index

    window = Mail.windows.at(0)
    window = Mail.windows[0]
  2. Name

    window = Mail.windows.byName('New Message')
    window = Mail.windows['New Message']
  3. ID

    window = Mail.windows.byId(412)

    Note: ID access does not have a square bracket shorthand.

Filtering Arrays

To filter arrays, returning only some of the elements, you can used the special whose method for arrays. Here is the general syntax:

someElementArray.whose({...})

You pass in an object containing the query's criteria. You can check for:

  1. Literal property matches

    { name: 'JavaScript for Automation' }
    { name: { _equals: 'JavaScript for Automation' } }
    { name: { '=': 'JavaScript for Automation' } }
  2. Property matches containing a value

    { name: { _contains: 'script for Auto' } }
  3. Property matches beginning with a value

    { name: { _beginsWith: 'JavaScript' } }
  4. Property matches ending with a value

    { name: { _endsWith: 'Automation' } }
  5. Property matches greater than a value

    { size: { _greaterThan: 20 } }
    { size: { '>': 20 } }
  6. Property matches greater than or equal to a value

    { size: { _greaterThanEquals: 20 } }
    { size: { '>=': 20 } }
  7. Property matches less than a value

    { size: { _lessThan: 20 } }
    { size: { '<': 20 } }
  8. Property matches less than or equal to a value

    { size: { _lessThanEquals: 20 } }
    { size: { '<=': 20 } }
  9. Sub-elements' property matches

    { _match: [ ObjectSpecifier.tabs[0].name, 'Apple' ] }

    Note: The _match key takes an array as its value.

You can combine these to see if multiple checks are all true, or if one or none of many checks is true.

  1. Multiple checks should all be true

    { _and: [
        { name: 'Apple' },
        { size: { '<': 20 } },
    ]}

    Note: An object with multiple properties to match will be treated as a logical and of the property checks.

    Note: The _and array requires at least two elements.

  2. At least one of many checks is true

    { _or: [
        { name: 'Apple' },
        { name: { _contains: 'JavaScript' } },
    ]}

    Note: The _or array requires at least two elements.

  3. None of one or more checks is true

    { _not: [
        { name: 'Apple' },
        { name: { _contains: 'JavaScript' } },
    ]}

    Note: The _not array can have one or more elements.

The _and, _or, and _not keys require arrays as their value.

Here is an example of a whose clause to find all of the messages in your inbox that contain the word "JavaScript" or the word "Automation" in the subject.

Mail = Application('Mail')
Mail.inbox.messages.whose({
    _or: [
        { subject: { _contains: "JavaScript" } }
        { subject: { _contains: "Automation" } }
    ]
})

Calling Commands

Commands are called as functions. Some commands take a direct parameter, which is passed as the first argument to a command. Some commands can take named parameters, accepting an object of keyed values for the named parameters. If the command takes a direct parameter, you pass the object of named parameters as a second argument. If the command has no direct parameter, the object of named parameters are passed as the first and only argument. When the direct parameter if optional, you can pass nothing to the command, or you can pass null as the first parameter if you will pass in named parameters.

  1. Command with no arguments

    message.open()
  2. Command with direct parameter

    Mail.open(message)
  3. Command with named parameters

    response = message.reply({
        replayAll: true,
        openingWindow: false
    })
  4. Command with direct parameter and named parameters

    Safari.doJavaScript('alert("Hello world")', {
        in: Safari.windows[0].tabs[0]
    })

Creating Objects

You can create new objects by calling class constructors as functions. Optionally, you can pass in an object of properties to set on the newly created object, and you can can pass in a second parameter as the data to be used when creating the new object. Once you have made the object, you can either call the make method on the object or push it onto an array in the application for it to be created. Before you call make or push onto an array in the application, the object doesn't exist in the application.

  1. Creating a new object

    message = Mail.OutgoingMessage().make()
  2. Creating a new object with properties

    message = Mail.OutgoingMessage({
        subject: 'Hello world',
        visible: true
    })
    Mail.outgoingMessages.push(message)
  3. Creating a new object with data

    para = TextEdit.Paragraph({}, 'Some text')
    TextEdit.documents[0].paragraphs.push(para)

Once an object has been created in an application (by calling make or pushing onto the appropriate array), it can be interacted with just like any of the already existing objects in the application.

message = Mail.OutgoingMessage().make()
message.subject = 'Hello world'

Note: Class constructors are title cased.

Scripting Additions

Scripting additions (plugins for scripts) can be used to enhance the functionality of applications. The OS has a set of standard scripting additions available. These offer things like speaking text and user interaction dialogs. To use them, an application must explicitly set the includeStandardAdditions flag to true.

app = Application.currentApplication()
app.includeStandardAdditions = true
app.say('Hello world')
app.displayDialog('Please enter your email address', {
    withTitle: 'Email',
    defaultAnswer: 'your_email@site.com'
})

Strict Flags

You can affect how strictly JavaScript for Automation treats Application objects when:

  1. Looking up properties of objects

    app.strictPropertyScope = true

    If the Foo class has a property bar, but the Baz class does not, the following code will only send the proper event to get the property when strictPropertyScope is false

    app.baz[0].bar()
  2. Looking up commands that objects respond to

    app.strictCommandScope = true

    If the application has a command foo, but the Bar class does not say it responds to that command, the following code will only send the proper command event when strictCommandScope is false

    app.bar[0].foo()
  3. Checking the types of parameters sent in commands

    app.strictParameterType = true

    For example, System Events has a keystroke command that accepts text as its first argument. The application's strictParameterType property must be false for you to be able to send a number instead of a string.

By default, applications have strictPropertyScope and strictCommandScope set to false and strictParameterType set to true.

Timeout, Considering, and Ignoring

You can pass event modifiers when getting and setting properties, calling commands on objects, creating new objects in applications, and so on. The event modifiers should be passed as the last argument to whatever function you are calling. Here are some examples:

  1. Calling a command with a timeout

    app = Application.currentApplication()
    app.includeStandardAdditions = true
    app.displayAlert('This will timeout in 30 seconds', { timeout: 30 })
  2. Calling a whose method ignoring case, but considering white space and hypens

    app.documents.whose({
        text: { _contains: 'custom-built automation' }
    }, {
        ignoring: 'case',
        considering: ['white space', 'hyphens']
    })

You can consider or ignore:

  1. case

  2. diacriticals

  3. white space

  4. hypens

  5. punctuation

  6. numeric strings

  7. application responses

ObjectSpecifier

The ObjectSpecifier object can be used to:

  1. Check if an object is an object specifier

    Mail.inbox.messages[0] instanceof ObjectSpecifier == true
  2. Get the class of an object specifier

    Mail = Application('Mail')
    ObjectSpecifier.classOf(Mail.inbox)
  3. Build arbitrary object specifier chains for use in whose clauses

    firstTabsName = ObjectSpecifier.tabs[0].name
    app.windows.whose({
        _match: [firstTabsName, 'Apple']
    })

Paths

When you need to interact with files, such as a document in TextEdit, you will need a path object, not just a string with a path in it. You can use the Path constructor to instantiate paths.

TextEdit = Application('TextEdit')
path = Path('/Users/username/Desktop/foo.rtf')
TextEdit.open(path)

Note: you can get the Path's string value by calling the toString method.

Progress

You can interact with a script's Progress object to affect the UI that details the script's progress. You can set its:

  1. Total unit count

    Progress.totalUnitCount = 10
  2. Completed Unit count

    Progress.completedUnitCount = 1
  3. Description

    Progress.description = 'Processing Files'

    Note: Setting the description of the progress object wil replace the "Running..." text that appears by default.

  4. Additional description

    Progress.additionalDescription = 'This may take a little while'

    Note: Setting the additional description of the progress object means the total unit count and completed unit count will not be shown.

Libraries

You can use scripts as libraries by storing them in ~/Library/Script Libraries/.

If you have a script library "toolbox.scpt":

function log(message) {
    TextEdit = Application('TextEdit')
    doc = TextEdit.documents['Log.rtf']
    doc.text = message
}

a script can use the library like this:

toolbox = Library('toolbox')
toolbox.log('Hello world')

Note: Any code outside of functions in a library will be executed when the library is instantiated.

Applets

You can make standalone, double-clickable applications (called applets) by writing a script in Script Editor and saving it as an application. Applets have a certain set of events that you can create handlers for:

  1. When the applet is run:

    function run() {...}

    Note: Any code outside of functions in an applet will be executed when the applet is run.

  2. When the applet (droplet) is told to open documents (done by dropping documents onto the applet):

    function openDocuments(docs) {...}
  3. When the applet is told to print documents:

    function printDocuments(docs) {...}

    Note: The argument to the openDocuments and printDocuments handler will be an array of strings containing the file paths.

  4. The idle handler allows you to do periodic processing

    function idle() {...}
  5. When the applet is reopened:

    function reopen() {...}
  6. When the applet is quit:

    function quit() {...}

    Note: Returning false from the quit handler is the only way to cause the applet not to quit. Having no quit handler or returning any value other than false (including not returning a value) will cause the applet to quit as it normally would.

UI Automation

You can script the interface of applications by using the System Events application. Explore the scripting dictionary of System Events in Script Editor (specifically the Processes Suite) to see what exactly can be interacted with.

Here is an example of using UI scripting to create a new note in Notes:

Notes = Application('Notes')
Notes.activate()
 
delay(1)
SystemEvents = Application('System Events')
Notes = SystemEvents.processes['Notes']
 
Notes.windows[0].splitterGroups[0].groups[1].groups[0].buttons[0].click()

Objective-C Bridge

JavaScript for Automation has a built-in Objective-C bridge that offers powerful utility such as accessing the file system and building Cocoa applications.

The primary access points for the Objective-C bridge are the global properties ObjC and $.

Frameworks

The symbols from the Foundation framework are available by default. You can make additional frameworks and libraries available using the ObjC.import() method. BridgeSupport files are consulted when importing frameworks. For instance, to use the NSBeep() function, which is not present in Foundation, you can import the Cocoa framework:

ObjC.import('Cocoa')
$.NSBeep()

In addition to system frameworks, functionality from some of the system libraries has been exposed and can be imported by referring to the name of the header file (without the .h) that declares that functionality, such as: arpa/inet, asl, copyfile, dispatch, dyld, errno, getopt, glob, grp, ifaddrs, launch, membership, netdb, netinet/in, notify, objc, paths, pwd, readline, removefile, signal, spawn, sqlite3, stdio, stdlib, string, sys/fcntl, sys/file, sys/ioctl, sys/mount, sys/param, sys/resource, sys/socket, sys/stat, sys/sysctl, sys/time, sys/times, sys/types, sys/wait, sys/xattr, syslog, time, unistd, uuid/uuid, vImage, vecLib, vmnet, xpc, and zlib.

Besides built-in frameworks and libraries, you can also import any framework that has bridge support by passing the full path to the framework. For example, if you have a framework /Library/Frameworks/Awesome.framework with bridge support, you can do this:

ObjC.import('/Library/Frameworks/Awesome.framework')

Data types

The primitive JavaScript data types are mapped to C data types. For instance, a JavaScript string maps to char *, while a JavaScript integer maps to int. When using an ObjC API that returns a char *, you'll get a JS string. Note: Longs will be converted to strings in JavaScript.

Primitive JavaScript data types will be automatically converted to ObjC object types when passed as an argument to an ObjC method that is expecting an object type. For instance, a JS string will be converted to an NSString if that is what the method signature says the argument should be typed as.

Note, however, that ObjC object types that are returned by ObjC methods are never automatically converted to primitive JavaScript data types.

Instantiating classes and invoking methods

All classes are defined as properties of the $ object. Methods of ObjC objects are invoked in one of two ways, depending on whether the method takes arguments.

If the ObjC method does not take arguments, then you invoke it by accessing the JavaScript property with that name. For instance, this instantiates an empty mutable string:

str = $.NSMutableString.alloc.init

If the ObjC method does take arguments, then you invoke it by calling the JavaScript method (function-typed property) named according to the JSExport convention; the letter following each ":" is capitalized, and then the ":"s are removed. For instance, this instantiates an NSString from a JavaScript string and writes it to a file:

str = $.NSString.alloc.initWithUTF8String('foo')
str.writeToFileAtomically('/tmp/foo', true)

If you call a method, such as -intValue, that returns a C data type rather than an object, then you'll get back a primitive JavaScript data type. For instance, the following returns the primitive JavaScript integer, 99.

$.NSNumber.numberWithInt(99).intValue

Accessing ObjC properties

ObjC properties are also accessed through JavaScript properties, similar to how argument-less methods are invoked.

When a property of a bridged object is accessed, the list of ObjC properties is first consulted, and if a property with that name exists, then the appropriate getter or setter selector for that property is used. If an ObjC property with that name does not exist on the class, then the property name is used as the method selector.

If a property is defined using a custom getter name, you can use either the property name, or the getter name, and get the same result:

task = $.NSTask.alloc.init
task.running == task.isRunning

Also, unlike argument-less methods, bridged object properties that map to ObjC properties can also be set to (assuming the property is readwrite). The following two lines are equivalent because launchPath is defined as an ObjC property:

task.launchPath = '/bin/sleep'
task.setLaunchPath('/bin/sleep')

Wrapping and unwrapping

The ObjC.wrap() method can be used convert a primitive JavaScript data type into an ObjC object type, and then wrap that ObjC object in a JavaScript wrapper.

The ObjC.unwrap() method can be used to convert a wrapped ObjC object into a primitive JavaScript data type.

The ObjC.deepUnwrap() method can be used to recursively convert a wrapped ObjC object into a primitive JavaScript data type.

The $() operator is a convenient alias for ObjC.wrap(), and is meant to be similar to the @() boxing operator in ObjC. For instance, $("foo") returns a wrapped NSString instance, and $(1) returns a wrapped NSNumber instance.

The .js property of wrapped ObjC objects is a convenient alias for ObjC.unwrap(). For instance, $("foo").js returns "foo".

Constants and enums

Constants and enums are defined as properties of the $ object:

$.NSNotFound
$.NSFileReadNoSuchFileError
$.NSZeroRect

Functions

Functions are also defined as properties of the $ object:

$.NSBeep()
$.NSMakeSize(33, 55)

Variadics

When using functions and methods that accept variadic arguments, every variable argument passed in must be the same type as the last declared parameter. For example, printf must receive %s arguments after the initial format string.

ObjC.import('stdio')
$.printf('%s %s %s', 'JavaScript', 'for', 'Automation')

NSLog has a final declared parameter type of %@ and must receive %@ arguments after the initial format string.

$.NSLog('%@ %@ %@', $('JavaScript'), $('for'), $('Automation'))

When creating an NSArray using arrayWithObjects, nil termination is handled for you and you shouldn't try to terminate it yourself.

$.NSArray.arrayWithObjects($('JavaScript'), $('for'), $('Automation'))

Structs

Structs are represented as generic JavaScript objects, where the struct field names are the property names:

$.NSMakeRect(0, 0, 1024, 768)

Result: {"origin":{"x":0,"y":0},"size":{"width":1024,"height":768}}

Nil

In ObjC, nil is a valid message target. Sending a message to a nil object returns nil again. This is different from JavaScript, which does not allow methods to be called on undefined.

In order to create a JavaScript object that represents nil, you just call the box function with no arguments:

nil = $()

To determine if a bridged object is actually wrapping a nil pointer, you can call the isNil() method:

if (o.isNil()) { ... }

Implicit pass-by-reference

This boxed nil can be passed as an argument to an ObjC method that expects an argument passed by reference; the runtime will magically replace the inner nil pointer with the pointer returned by reference.

error = $()
fm = $.NSFileManager.defaultManager
fm.attributesOfItemAtPathError('/DOES_NOT_EXIST', error)
if (error.code == $.NSFileReadNoSuchFileError) {
    // ...
}

Explicit pass-by-reference

The Ref class can be used to pass arguments that ObjC expects to be passed by reference. The referenced value can be retrieved by accessing the '0' property of the Ref.

function testExplicitPassByReference(path) {
    ref = Ref()
    fm = $.NSFileManager.defaultManager
    exists = fm.fileExistsAtPathIsDirectory(path, ref)
    if (exists) {
        isDirectory = ref[0]
        return (path + ' is ' + (isDirectory ? '' : 'not ') + 'a directory; ')
    }
    else {
        return (path + ' does not exist\n')
    }
}
 
output = testExplicitPassByReference('/System')
       + testExplicitPassByReference('/mach_kernel')
       + testExplicitPassByReference('/foo')

Result: /System is a directory; /mach_kernel is not a directory; /foo does not exist

Ref

To access the byte values of Ref objects, they can be indexed into like arrays:

ref = Ref('id*')
ref[0] = $('foo')
if (ref[0] == $('foo')) { ... }

When you need to pass an opaque pointer and check whether you got that opaque pointer back, you instantiate a void Ref and use the equals method. For example:

CONTEXT = Ref('void')
 
ObjC.registerSubclass({
    name: 'MyObject',
    properties: { foo: 'id' },
    methods: {
        'startObserving': {
            types: ['void', []],
            implementation: function (obj) {
                this.addObserverForKeyPathOptionsContext(
                    this,
                    'foo',
                    ($.NSKeyValueObservingOptionNew | $.NSKeyValueObservingOptionOld),
                    CONTEXT
                );
            },
        },
        'observeValueForKeyPath:ofObject:change:context:': {
            types: ['void', ['id', 'id', 'id', 'void *']],
            implementation: function(keyPath, observedObject, change, context) {
                $.NSLog(Automation.getDisplayString(Ref.equals(context, CONTEXT)))
            }
        }
    }
})

Function binding

When frameworks are imported, only the C functions specified in the BridgeSupport files are made available. To access other C functions that are linked into the host application, a script may bind them into the JavaScript environment. To do so, you must specify the return and argument types of the function:

ObjC.bindFunction('pow', ['double', ['double', 'double']])
$.pow(2,10)

Result: 1024

Types

Here are some of the allowed strings for specifying types of properties, and method and function arguments and return values: void, bool, float, double, unsigned char, char, unsigned short, short, unsigned int, int, unsigned long, long, long double, class, selector, string, pointer, block, function, and id.

Note: Struct type names, such as NSRect, are allowed, but id must be used for objects, not actual class names.

Subclassing

Subclasses of Objective-C objects can be registered from JavaScript:

ObjC.registerSubclass({
    name: 'AppDelegate',
    superclass: 'NSObject',
    protocols: ['NSApplicationDelegate'],
    properties: {
        window: 'id'
    },
    methods: {
        'applicationDidFinishLaunching:': function (notification) {
            $.NSLog('Application finished launching');
        }
    }
})

If no superclass is given, the object will implicitly subclass NSObject. When you override a method, specifying the method types is optional, but if the types are present, they must match those of the superclass's method.

Here is an example of a custom init method and a custom method with its types declared:

ObjC.registerSubclass({
    name: 'MyClass',
    properties: {
        foo: 'id'
    },
    methods: {
        init: function () {
            var _this = ObjC.super(this).init;
            if (_this != undefined) {
                _this.foo = 'bar'
            }
            return _this
        },
        baz: {
            types: ['void', []],
            implementation: function () {
                this.foo = 'quux'
            }
        }
    }
})

Note: DO NOT assign to the this variable or any of its properties in init methods. Assign to a different variable (_this is used above), use that variable to initialize properties, and then return it. In all other methods, the standard this variable should be used. In JavaScript, you are not allowed to reassign the this variable. Objective-C classes are allowed to return from their init methods a different object than that on which the method was called.

Automation.getDisplayString()

The Automation object has a getDisplayString method that allows you to pass in an object and print it as source text or a human-readable display string

foo = $('bar')
Automation.getDisplayString(foo)

Passing in true as the second parameter to getDisplayString will print the object as a display string instead of source text.

Automation.getDisplayString(foo, true)