Skip to content
Pat Gilmour edited this page Jan 26, 2022 · 25 revisions
← previous · · · · · · · · · · · · · · next →

The System Events application lets you automate the GUI in any application. You can see some examples in AppleScript Wikibook.

var se = Application('System Events')

Sending Keystrokes

Before you send the keystroke, you usually need to activate the app in which the keystroke is to be sent to.

Example Keystrokes

se.keystroke('Hello')
se.keystroke('a', { using: 'command down' }) // Press Cmd-A
se.keystroke(' ', { using: [ 'option down', 'command down' ] }) // Press Opt-Cmd-Space
se.keyCode(36); // Press Enter

Example of Sending Copy Command

'use strict';

//--- GET A REF TO CURRENT APP WITH STD ADDITONS ---
var app = Application.currentApplication()
app.includeStandardAdditions = true

var seApp = Application('System Events')

//--- Set the Clipboard so we can test for no selection ---
app.setTheClipboardTo("[NONE]")

//--- Activate the App to COPY the Selection ---
var safariApp = Application("Safari")
safariApp.activate()
delay(0.2)  // adjust the delay as needed

//--- Issue the COPY Command ---
seApp.keystroke('c', { using: 'command down' }) // Press ⌘C 
delay(0.2)  // adjust the delay as needed

//--- Get the Text on the Clipboard ---
var clipStr = app.theClipboard()
console.log(clipStr)

//--- Display Alert if NO Selection was Made ---
if (clipStr === "[NONE]") {
  var msgStr = "NO Selection was made"
  console.log(msgStr)
  app.activate()
  app.displayAlert(msgStr)
}

For a list of key codes to use with .keyCode(), see the Complete list of AppleScript key codes on the Eastman Reference website, or use the free Key Codes application.

Start the Screen Saver

AppleScript Equivalent: tell application "System Events" to start the current screen saver

The verb here is start, and current screen saver is the noun. So converting that to JavaScript-speak makes it look like this:

se.start(se.currentScreenSaver)

Toggle Menu Bar Visibility

Toggle Show/Hide the menu bar.

AppleScript Equivalent:

tell application "System Events"
	tell dock preferences
		set menuBarStatus to autohide menu bar
		if (menuBarStatus is false) then
			set autohide menu bar to true
		else
			set autohide menu bar to false
		end if
	end tell
end tell

Note that in the conversion to JavaScript, in order to get autohideMenuBar property into a value, we call it as a method autohideMenuBar():

var se = new Application("System Events");
var menuHidden = se.dockPreferences.autohideMenuBar();
if (menuHidden == false) {
	se.dockPreferences.autohideMenuBar = true;	
} else {
	seApp.dockPreferences.autohideMenuBar = false;		
}

Network: Connecting to a VPN or Other Network Service

Assuming you have a VPN service called "MyVPN" in the network preferences. You can use JXA to tell your computer to connect to it.

AppleScript Equivalent: tell application "System Events" to disconnect service "MyVPN" of network preferences

se.connect(se.networkPreferences.services["MyVPN"])

Menu items

Security

First you must enable osascript access under Security & Privacy ➡️ Accessibility in Settings. The only way to do this is to use a front-end tool tccutil.py to add the entry (since osascript is not a .app bundle).

Note: as of MacOS Sierra (10.12), tccutil.py can no longer be used to grant access without disabling SIP. See https://github.com/jacobsalmela/tccutil/issues/18.

Download the Python script and execute it like so:

sudo python tccutil.py -i /usr/bin/osascript

Note that you also must add any in-between applications, like tmux or screen if you expect osascript to work from there.

Also back in Settings, be sure to add Terminal.app which you can do normally.

Settings

Clicking menu items

Understand that the top menu can only be grabbed with System Events and you must activate the application for the menu you want before you can click the item. If you are doing GUI interactions, always think of the steps that you would take normally. Additionally, you may want to find the quickest way to do something, such as pressing a keyboard shortcut or clicking a menu item (instead of trying to position the cursor at an offset on the screen and clicking).

Activate the application first. Note that you should never treat these methods as blocking. You may want to add delay() in some cases.

it = Application('iTunes');
it.activate();

You need to get the application via the System Events processes property. You can use the byName() on that collection for convenience.

var its = se.processes.byName('iTunes');

Next, if security settings are set, we should be able to get the menu. Most applications will only have one menuBar however some applications will have more than one, such as a "tray icon". So while in AppleScript you would use menu bar 1, here you iterate from 0.

All menu bars contain a menuBarItems property, which also has a byName() convenience method.

In menuBarItems you would find Apple (which is the Apple menu), an item with the name of the application, and the other menus after (if there are any).

var fileMenu = proc.menuBars[0].menuBarItems.byName('File');

We can .click() this one but it will be so fast we probably will not see anything.

The structure of a menu contains further with menuBarItem objects containing a menus property. This may seem confusing as the name of it will refer to itself, but this is the actual equivalent to NSMenu in AppleScript. It will have the items we are looking for. Usually the first item is all we need.

var editMenu = proc.menuBars[0].menuBarItems.byName('Edit');
editMenu.menus[0].menuItems.byName('Select All');

The structure of .menuItems.byName() to .menus[0] to .menuItems.byName() will continue as the menu has more children. If the name is dynamic, simply grab the menu items and use something like a regular expression:

items = fileMenu.menus[0].menuItems(); // Note ()
for (var item, i = 0, j = items.length; i < j; i++) {
    item = items[i];
    if (item.enabled() &&
        /^Log out myname/.test(item.title()) { // again note () on attributes
        item.click();
    }
}

The major disadvantage to name matching is locale. Nothing here is locale-aware. If you need to support multiple languages, you may want to research into bridging with $.NSLocalizedStringFromTableInBundle() or the NSLocale API.

Performing actions

Actions can be performed on UI elements via OS X's Accessibility API.

AppleScript Equivalent: tell application "System Events" to tell process "Finder" to tell first toolbar of first window to perform action "AXShowMenu"

The JavaScript version is quite different, requiring the action to be selected first, at which time perform() can be called on it.

se.processes['Finder'].windows[0].toolbars[0].actions['AXShowMenu'].perform();

How to show the menu of the first item (uiElements[0]) on the dock:

se.processes['Dock'].lists[0].uiElements[0].actions['AXShowMenu'].perform();

// Now this will have a non-empty list of menu items
se.processes['Dock'].lists[0].uiElements[0].menus[0].menuItems();

// Click 'Hide'
se.processes['Dock'].lists[0].uiElements[0].menus[0].menuItems['Hide'].click();
← previous · · · · · · · · · · · · · · next →