Working with Apps

Working with Apps

The first step in working with an app is to get an instance of it like so: const app = Application("AppName"). This is the easiest method. You could also use the bundle identifier, the path to the app’s bundle on disk or the process ID of a running app. Most of those are not very practical, so we’ll stick with the method above.

Get an app’s properties and elements #

As said before, the primary source for information on an app’s properties and elements is its function library that you can inspect in the script editor. However, there are some methods that allow you to retrieve very basic information from the application with JavaScript itself. Unfortunately, the usual JavaScript suspects like getOwnProperties do not work.

For Apple’s Finder app, you could for example do this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(() => {
    const app = Application("Finder");
    const elements = app.properties();
      for (e in elements) {
        const v = e ? 
          `${e}: ${Automation.getDisplayString(elements[e])}`
          : "undefined";
        console.log(v);
    }
})()

Note the usage of Automation.getDisplayString() here in line 6. It converts the property values to something that is not only legible but also valuable, because it tells you a lot about the internal workings of JXA (see the complete output of the script below).

Output of properties for Finder app
 class: undefined 
 name: "Finder" 
 clipboard: null 
 visible: true 
 frontmost: false 
 selection: [] 
 insertionLocation: Application("Finder").startupDisk.folders.byName("Users").folders.byName("xxx").folders.byName("xxx").folders.byName("xxx").folders.byName("jxa").folders.byName("content") 
 productVersion: "" 
 version: "11.5" 
 desktop: Application("Finder").desktop 
 desktopPicture: Application("Finder").startupDisk.folders.byName("Users").folders.byName("xxx").folders.byName("Pictures").documentFiles.byName("Fotos-Mediathek.photoslibrary").folders.byName("originals").folders.byName("9").documentFiles.byName("938AFCCA-70EF-4C00-9065-97C553AB460F.heic") 
 finderPreferences: Application("Finder").finderPreferences 
 startupDisk: Application("Finder").startupDisk 
 trash: Application("Finder").trash 
 home: Application("Finder").startupDisk.folders.byName("Users").folders.byName("xxx") 
JXA scripting in Mail is partially broken. Notably, the access to Mail’s properties has been butchered by Apple. Therefore, the examples here use Finder.

Alternatively, you could use propertiesOfClass() like so:

1
2
3
4
5
(() => {
  const app = Application("Finder");
  const elements = app.propertiesOfClass("application");
  elements.forEach( el => console.log(el));
})()

However, getPropertiesOfClass() will only tell you the names of the properties, whereas properties() gives you an object with the property names as attributes and the property values as their values.

A similar function to get at the elements of an application is elementsOfClass. This will tell you about all elements, i.e. child objects. These elements are always lists:

1
2
3
4
5
6
7
(() => {
  const app = Application("Finder");
  const elements = app.elementsOfClass("application");
  elements.sort().forEach( e => {
    console.log(`${e} has ${app[e]().length} elements`);
  })
})()

As shown above, you can also access properties with JavaScript’s usual square bracket syntax like app[property]().

Output of elementsOfClass for Finder
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 aliasFiles has 1 elements 
 applicationFiles has 4 elements 
 clippingWindows has 0 elements 
 clippings has 0 elements 
 containers has 5 elements 
 disks has 22 elements 
 documentFiles has 3 elements 
 files has 8 elements 
 finderWindows has 1 elements 
 folders has 5 elements 
 internetLocationFiles has 1 elements 
 items has 13 elements 
 packages has 4 elements 
 windows has 1 elements 

Getting types of properties and more #

To get the type of a property, use propertyTypeForNameInClass() like so

1
2
3
4
5
6
7
8
(() => {
  const app = Application("Finder");
  const props = app.propertiesOfClass("application");
  props.forEach( p => {
  let t = app.propertyTypeForNameInClass(p);
     console.log(`${p} is of type "${t}"`);
  });
})()

When you run this script, you’ll see that the type of some properties (for example productVersion) is undefined although it obviously is of type text if you look at its value with properties(). This is one of the consequences of Apple’s half-baked implementation of JXA. Most of the times it works, sometimes there a workarounds and sometimes it is just so broken that you can’t do anything about it.

In order to get the parameters of a command (method) and their type, you can use parametersForCommand() and parameterTypeForNameInCommand(). For example, to determine the parameters for the Finder’s make command, you’d use

1
2
3
4
5
6
7
8
(() => {
  const app = Application("Finder");
  const params = app.parameterNamesForCommand("make");
  params.forEach( p => {
    let t = app.parameterTypeForNameInCommand(p);
    console.log(`${p} is of type "${t}"`);
  });
})()

Running this script unfortunately gives only unsatisfying output:

1
2
3
4
at is of type "undefined"
new is of type "undefined"
to is of type "undefined" 
withProperties is of type "undefined"

So it is possible to get some information about the internals of applications, their classes, methods and properties. But this does not work in all cases, and the only sufficiently reliable source of information is the Script Editor.

Creating new objects #

Some applications offer methods to create new objects, e.g. Mail and Notes. The scripting dictionaries for these apps seem to indicate that new objects should be created by calling the make method. However, this does not work in JXA.

To create a new top level document in an app, you have to use the documents class name as a method on the application object and then make() on the returned object. For Apple’s Numbers spreadsheet app, it looks like this:

1
2
3
4
(() => {
  const app = Application("Numbers");
  const doc = app.Document().make();
})()

You can pass parameters to the newly created object by adding them to the Document call like so:

1
2
3
4
5
(() => {
  const app = Application("Numbers");
  const myTemplate = app.templates["My Template"];
  const doc = app.Document({documentTemplate: myTemplate}).make();
})()

This works for top level documents in an app, i.e. Document in Number and Pages, Note in Notes and so on. Adding an object to a top level document does not require a call to make but only to the class-named method. To add a new sheet to the first Numbers document, you’d use:

1
2
3
4
5
6
(() => {
  const app = Application("Numbers");
  const doc = app.documents[0];
  const sheet = app.Sheet({name: "My Sheet"});
  doc.sheets.push(sheet);
})()

Since a Document contains sheets, you push a newly created sheet onto this list with push. Note, however, that many other array methods do not work in this case. So you cannot pop an element from sheets.

About