Skip to content

Instantly share code, notes, and snippets.

@tommorris
Last active April 30, 2024 12:32
Show Gist options
  • Save tommorris/99bcbbcf445bb6475797 to your computer and use it in GitHub Desktop.
Save tommorris/99bcbbcf445bb6475797 to your computer and use it in GitHub Desktop.
OSA JavaScript recipes

For a while, you've been able to script Mac OS using JavaScript using JavaScript for Open Scripting Architecture (JSOSA). This is a huge improvement on AppleScript. Why? JavaScript isn't a great language, but it broadly follows the conventions that programming languages of the C/ALGOL family have followed for decades. AppleScript is a horrendous mess that should never have come into being.

Let's compare.

set x to 0
set taskName to the name of task
var x = 0;
var taskName = task.name();

Trying to make programming languages follow the semantics of English was a horrendous mistake.

Now we have both JSOSA and the osascript command, scripting Mac applications is a lot easier. You can't directly call JSOSA from Node.js, but you can make a system call to osascript from your favourite language.

You can experiment with JSOSA in a REPL by running: osascript -l JavaScript -i

You can run JSOSA files with osascript -l JavaScript filename.js

Lessons

Some general lessons learned so far:

  • for .. in doesn't tend to work for the collections returned by apps, but forEach/filter-style methods do.
  • Plenty of things behave differently depending on whether you are getting them as a property vs. calling them. In Things.app, for instance, Application("Things").lists() will return a collection of lists, while Application("Things").lists is an object you can call with methods like .byName.
  • It would be nice if Apple provided some decent documentation on how AppleScript maps to JavaScript. But they don't.
  • One interesting use of JSOSA is you can use it as a way to extract data from Mac apps and store it in more open formats. Basically, the idea is you can build up a JSON output and then use JSON.stringify to output it. This is a huge improvement on nasty AppleScript hacks that constructed CSVs by concatenating strings.

CLI arguments (argv)

var args = $.NSProcessInfo.processInfo.arguments

From this section of the excellent JXA Cookbook

Spotify

spotify = Application("Spotify");
spotify.pause();
spotify.play();
spotify.nextTrack();
spotify.previousTrack();
/* ^^ this ACTUALLY does the same as pressing 'prev' in the interface. fire it twice to go the previous track. */

iTunes

itunes = Application("iTunes");
itunes.playpause()
itunes.play();
itunes.pause();
itunes.playerState(); /* paused || stopped || rewinding || fast forwarding */
var things = Application("Things");
/* Exploring areas */
// look through all our areas of responsibility
things.areas().forEach(function (area) {
console.log(area.name());
});
// some are inactive ("suspended")
things.areas().foreach(function (area) {
if (!area.suspended()) {
console.log(area.name());
}
})
/* Tasks */
// now let's look at all the tasks in one area
things.areas.byName("Work").toDos();
things.areas.byName("Work").toDos().length;
var task = things.areas.byName("Work").toDos()[0];
console.log(task.name());
console.log(task.tagNames()); // The tagNames list is a ", " separated list of tags.
/* Logbook */
// Completed tasks are stored in the Logbook
var logbook = things.lists.byName("Logbook").toDos();
// You know they are done, because they have status="completed"
console.log(logbook[0].status());
// The dates are proper JavaScript date objects:
console.log(logbook[0].completionDate().toISOString());
// We can iterate through tasks.
var wr = logbook.filter(function (task) { return task.name() == "Weekly review"; });
var things = Application("Things");
function tasksDoneByDay(logbook, offset) {
var from = new Date(Date.now() + ((-offset) * 24 * 3600 * 1000));
from.setHours(0);
from.setMinutes(0);
from.setSeconds(0);
var to = new Date();
to.setTime(from.getTime());
to.setHours(23);
to.setMinutes(59);
to.setSeconds(59);
return logbook.filter(function (task) {
return task.completionDate() >= from && task.completionDate() < to;
});
}
function main(things) {
var output = {};
output['stats'] = {};
var logbook = things.lists.byName("Logbook").toDos();
var wr = logbook.filter(function (todo) { return todo.name() == "Weekly review"; })
output['stats']['lastWeeklyReview'] = wr[0].completionDate().toISOString();
output['stats']['completed'] = {}
output['stats']['completed']['yesterday'] = tasksDoneByDay(logbook, 1).length;
console.log(JSON.stringify(output, null, ' '));
}
main(things);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment