Scripting System Preferences
The Mac is widely regarded as the computer of choice for creativity. It is often at the centre of a wide variety of pursuits, from music to movies, from illustration to photography or from design to desktop publishing. And at the heart of the Mac is Mac OS X, helping us to be more productive by making the creative process more effortless, enjoyable and effective.
To allow us work and play in our own, individual way, the Mac OS has long allowed extensive customisation. While such features were originally accessed through a collection of control panels in the control panels folder, Mac OS X introduced a single application to bring together most of the system customisation tools: System Preferences.
The aim of this article is to explore some of the ways in which we might use AppleScript to change certain system settings.
About System Preferences
System Preferences offers a wide range of options to control the system. Even when a Mac is shared by several users, each person can customise his or her experience and maintain most settings separately. The controls are grouped together quite logically and presented in a clear, graphical form so that even a relative novice should have little difficulty understanding how to use them.
System Preferencesâ preference panes include:
* .Mac used to set preferences for the users .Mac account and iDisk
* Accounts user creation/deletion, administrator privileges and user limitations are set here
* Appearance changes the general color of the OS as well as placement of Scroll arrows and Font Smoothing
* Bluetooth pair Bluetooth devices, edit Bluetooth settings
* CDs & DVDs used to set default settings upon inserting blank CD/DVDs, as well as music CDs, Picture CDs and Video DVDs
* Classic used to activate the Classic environment as well as set up settings for the Classic environment
* Date & Time used to set the Date & Time of the computer, as well as how the clock appears on the menu bar
* Desktop & Screensaver used to set the Desktop Picture as well as the screensaver, and there settings
* Displays set screen resolution and Color settings here
* Dock adjust the dock size as well as magnification and position on screen
* Energy Saver optimize energy settings as well as sleep times and processor usage
* Expos set Active Screen Corners and Keyboard and Mouse settings to activate Expos
* Ink set handwriting recognition settings
* International set the default OS language as well as numerical formats
* Keyboard & Mouse set keyboard settings, as well as change keyboard shortcuts, and mouse settings
* Network set Ethernet, AirPort, Modem and VPN Settings
* Print & Fax set the default Printer as well as fax settings
* QuickTime set Network speeds, plug-in settings, update, and register Quicktime here
* Security set FileVault settings, set up a Master password and set Account Security Settings
* Share set firewall and computer services preferences
* Software Update set default times to check for updates, and view updates already installed
* Sound set Alert Sound, Volume and Input/Output option
* Speech set the computers Default Voice, set up Speech Recognition, and other Speech Settings
* Startup Disk set the default disk, Hard Drive or Otherwise, for the computer to boot into
* Universal Access make your computer more accessible for those with, sight, hearing and other impairments
* Other panes itâs also possible to add preference panes to System Preferences with third-party software
Accessing System Settings with AppleScript
From a scripting point of view, System Preferencesâ bias towards the graphic user interface (GUI) seems to have been somewhat at the expense of direct scriptability. While it would be helpful to have more scripting support added in future, we have to do what we can with whatâs currently available.
Although there are a number of ways to access system defaults, each is limited to certain individual settings. Until relatively recently, there has been no way to access system settings in a more general way.
Fortunately another feature, introduced in Mac OS X 10.3, includes support for the control of the GUI via AppleScript by means of an enhanced version of the System Events application. The GUI Scripting architecture is based upon the Mac OS X accessibility frameworks, and provides alternative methods of querying and controlling the interfaces of many applications and of the Mac OS itself. This means that AppleScript scripts can select menu items, push buttons, enter text into text fields, and generally control the interfaces of most non-Classic applications.
So the main methods of getting and changing system settings now include:
* using a shell script to access specific defaults
* extracting and modifying data in a property list file
* using third-party software
* GUI scripting of the System Preferences application
Avoiding Potential Conflicts Between Access Methods
In spite of the availability of GUI scripting, itâs often preferable to modify settings quietly in the background, using an alternative approach (where one exists).
When using other methods, particularly if directly modifying data in property list files, itâs generally advisable to avoid potential conflicts by making sure the application normally used to access those files is not currently open while changes are made. Otherwise, thereâs a risk that the application could ignore (or even overwrite) any settings modified by other means.
In a number of cases, System Preferences may be left open and will simply reflect the changes being made (as it does, for example, with the techniques for Changing Power Management Settings suggested below). In other situations, a dialog sheet might be displayed to alert the user that some settings âhave been changed by another applicationâ (as with the method proposed for Changing Current Location, below if System Preferencesâ Network pane is selected). In addition, any attempts to change settings normally accessed through a dialog sheet (such as the âScheduleâ options for Energy Saver) are likely to fail if that sheet is displayed at the time.
One way around such issues is to quit the application, if itâs running. Itâs better to do this without a direct application tell statement which may actually cause an unlaunched application to open. Try using an indirect quit statement instead, such as:
quit application "System Preferences"
However, this doesnât consider that the user may currently be using the application involved. A more user-friendly approach might be to quit the application, make any changes using the alternative method chosen and then re-launch it (preferably reinstating previous window settings, etc).
Hereâs an example of such a routine for System Preferences:
to |get SysPrefs status|()
tell application "System Events"
if not (exists application process "System Preferences") then return {false, false, false, false, false, false, false}
tell button "OK" of sheet 1 of front window of application process "System Preferences" to if exists then click
set {class xpsa:show_all, class xpcp:selected_pane, class xppw:{bounds:b, miniaturized:m}} to properties of application "System Preferences"
set {frontmost:f, visible:v} to application process "System Preferences"
quit application "System Preferences"
repeat while exists application process "System Preferences"
delay 0.2
end repeat
end tell
{true, not show_all, selected_pane, b, m, f, v}
end |get SysPrefs status|
to |restore SysPrefs| to {app_open, pane_selected, selected_pane, b, m, f, v}
if app_open then tell (a reference to application "System Preferences")
launch
if pane_selected then set its class xpcp to selected_pane
tell its class xppw to set {bounds, miniaturized, visible} to {b, m, not m}
tell application "System Events" to tell application process "System Preferences"
set frontmost to f
if not v then set visible to v
end tell
end tell
end |restore SysPrefs|
set SysPrefs_status to |get SysPrefs status|()
(* set defaults/settings using an appropriate non-System Preferences method *)
|restore SysPrefs| to SysPrefs_status
Using a Shell Script to Access Specific Defaults
Some shell commands provide direct access to certain system settings. Using a shell in this way can be an effective method, since the effect is the same as changing the setting in System Preferences but without the application intruding on the user.
Since this isnât a shell tutorial, we canât go into too much detail here. However, Iâve included a couple of shell examples along with a brief description of how we might find and implement an appropriate shell tool to achieve our aim.
Finding and Using a Suitable Shell Command
For this, letâs assume that we want to change the current location in our network preferences.
Our first task is to carry out a search, to see if we can locate a suitable tool. To do this, we could use the apropos command to search the âwhatisâ database for possibly relevant strings. So letâs launch Terminal and, in a new Terminal window, type (or paste) something like: apropos system location then press the enter or return key.
Whoa! While some searches may return an unhopeful nothing appropriate, others (like this one) can present a welter of information. However, letâs remain undaunted and wade through whatâs there. About halfway down the mass of data, we should see an entry that looks like:
Might this do the trick? Letâs find out by reading the relevant man (on-line manual) pageâŠ
Back in Terminal, type or paste (in a new window, if preferred) the words: man scselect, and press enter or return. This should give us some useful information about the command including a synopsis (outlining forms of syntax) and description (which should tell us if weâre on the right track). From the description shown here, I think weâve found something that might do the trickâŠ
One final point on this. To avoid potential confusion over the file location of the tool to which weâre referring, itâs a good idea to include the file path in our shell script. We can usually determine this with the which command either in Terminal, or using a script:
do shell script "which scselect"
--> "/usr/sbin/scselect"
Changing Current Location
As described above, instead of opening the Network pane in System Preferences to change the current location (system configuration set), we could simply use scselect.
do shell script "/usr/sbin/scselect 'required-location-name'"
Accessing Energy Saver Settings
Another useful shell command in this context is pmset, which can read and set power management settings (normally accessed manually through System Preferencesâ Energy Saver pane). Either method reads or writes settings stored in /Library/Preferences/SystemConfiguration/com.apple.PowerManagement.plist apart from scheduled power on/off events, which are stored in /Library/Preferences/SystemConfiguration/com.apple.AutoWake.plist.
The following guide to corresponding controls may help:
Table 1. Power Management Settings
The displaysleep and disksleep arguments were introduced in Mac OS 10.4 to replace dim and spindown respectively. Use the latter if you need compatibility with an earlier system. (While deprecated in Tiger, the older arguments will continue to work there when setting values.)
The slider-type values in pmset generally range from 1 (minute) to 180 (3 hours), with 0 representing âneverâ. Checkbox-type values are usually either â0â (off/unchecked) or â1â (on/checked). A notable exception is the System Preferencesâ checkbox marked âPut the hard disk(s) to sleep when possibleâ. When checked, this sets the disksleep/spindown value to 10 (minutes); unchecking it will set a value of 180 (3 hours). Any disksleep value can be set using pmset (although subsequently checking/unchecking the checkbox in System Preferences will revert to the 10/180 defaults).
Getting Power Management Settings
To get the current settings, use pmsetâs -g (get) flag:
do shell script "/usr/bin/pmset -g"
To extract an individual setting, we could use grep:
last word of (do shell script "/usr/bin/pmset -g | grep -w sleep") as integer
Note that grepâs -w (word-regexp) option is used here, to avoid any confusion with other arguments that could include the word âsleepâ such as âdisksleepâ or âdisplaysleepâ.
Getting Scheduled Events
To determine any currently scheduled startup/wake and shutdown/sleep events:
do shell script "/usr/bin/pmset -g sched"
Changing Power Management Settings
To change power management settings, pmset must be run as root. In AppleScript terms, this means running it with administrator privileges, which would normally also require an administrator password either entered by the user or provided by the script itself. Where the current user is not an administrator, an administrator account name should also be included.
This, for example, should ask the user for an administrator password (if required), and set the computer to sleep when it is inactive for 30 minutes:
do shell script "pmset sleep 30" with administrator privileges
This should require no user input, and set the display to sleep when the computer is inactive for 20 minutes:
set admin_name to "adminName" (* modify as appropriate *)
set admin_password to "adminPassword" (* modify as appropriate *)
do shell script "pmset displaysleep 20" user name admin_name password admin_password with administrator privileges
Scheduling Repeating Power Events
Clicking the âScheduleâ button in System Preferences Energy Saver calls up a dialog sheet in which startup/wake and shutdown/sleep events can be scheduled. These events are repeated at a specified time according to the selected repeat option which could be one of the following:
* Weekdays
* Weekends
* Every Day
* A particular day of the week
These options can also be set using pmset. As with System Preferences, pmset supports only one pair of repeating events at any time; a âpower onâ event (poweron, wake or wakeorpoweron) and a âpower offâ event (sleep or shutdown).
Date and time formats in pmset are MM/dd/yy HH:mm:ss (24 hour format). Weekdays are designated by a subset of MTWRFSU with each character representing a day of the week, Monday (M) thru Sunday (U).
The following examples assume that the appropriate administrator name and password have been set as indicated above.
Start up or wake Weekdays at 08:30
do shell script "pmset repeat wakeorpoweron MTWRF 08:30:00" user name admin_name password admin_password with administrator privileges
Start up or wake Weekends at 10:45
do shell script "pmset repeat wakeorpoweron SU 10:45:00" user name admin_name password admin_password with administrator privileges
Sleep Every Day at 21:00
do shell script "pmset repeat sleep MTWRFSU 21:00:00" user name admin_name password admin_password with administrator privileges
Shutdown Sunday at 12:15
do shell script "pmset repeat shutdown U 12:15:00" user name admin_name password admin_password with administrator privileges
The options available from pmset are slightly more flexible than those offered by System Preferences. However, while a non-standard combination of days can be selected using pmset, System Preferences will be unable to display such choices; the corresponding pop up button will appear to be blank.
Start up or wake Monday, Wednesday and Friday at 09:00
do shell script "pmset repeat wakeorpoweron MWF 09:00:00" user name admin_name password admin_password with administrator privileges
Scheduling Individual Power Events
In addition, pmset provides a way to schedule individual (non-repeating) events which, again, System Preferences will be unable to reflect in the GUI.
Start up or wake Sunday, December 31, 2006 at 23:59
do shell script "pmset schedule wakeorpoweron '12/31/06 23:59:00'" user name admin_name password admin_password with administrator privileges
Cancelling Scheduled Power Events
To cancel all repeating events:
do shell script "pmset repeat cancel" user name admin_name password admin_password with administrator privileges
To cancel an individual event (in this case, the one we scheduled earlier):
do shell script "pmset schedule cancel wakeorpoweron '12/31/06 23:59:00'" user name admin_name password admin_password with administrator privileges
Combining Power Management Commands
A range of values can be set with a single shell script, as demonstrated by this composite example:
set admin_name to "adminName" (* modify as appropriate *)
set admin_password to "adminPassword" (* modify as appropriate *)
do shell script "pmset sleep 30 displaysleep 20 disksleep 10 ring 1 womp 1 halfdim 1 autorestart 0 reduce 0 repeat wakeorpoweron MTWRF 09:00:00 sleep MTWRF 17:30:00" user name admin_name password admin_password with administrator privileges
Further Information
For more details, type man pmset in a Terminal window and press the enter or return key or see below for details of how to create a PDF file.
Creating Unix PDF Manuals
While the formatting preferences in Terminal can be adjusted, you may still wish to enhance the way in which man pages are presented. To improve the general readability of such documents, you might want to consider creating dedicated PDF files.
Something like this might help:
property man_folder : path to desktop (* modify alias path as required *)
property dflt_doc : "man"
to |open pdf manual| for shell_command
if (count shell_command) is 0 then return beep
set pdf_doc to shell_command & ".pdf"
tell application "Finder" to tell folder man_folder's file pdf_doc
if not (exists) then tell me to try
do shell script "/usr/bin/man -t " & quoted form of shell_command & " | /usr/bin/pstopdf -i -o " & quoted form of (POSIX path of man_folder & pdf_doc)
on error
return beep
end try
if exists then open
end tell
set dflt_doc to shell_command
end |open pdf manual|
|open pdf manual| for text returned of (display dialog "Open which Unix PDF manual?" default answer dflt_doc)
Extracting and Modifying Data in a Property List File
Extracting Data
Many system settings can be extracted from property list files, either using the defaults shell command, or by scripting with System Eventsâ Property List Suite.
If, for example, we wanted to find out the current animation effect when minimizing windows, we could extract it from the Dockâs plist file (~/Library/Preferences/com.apple.dock.plist).
The shell script method might look like this:
set min_effect to last word of (do shell script "defaults read com.apple.dock | grep mineffect")
--> "genie" or "scale"
An equivalent approach using System Events would look more like:
tell application "System Events" to set min_effect to value of property list item "mineffect" of property list file (preferences folder's path & "com.apple.dock.plist")
--> "genie" or "scale"
Many folks will, of course, be attracted by the brevity of a shell script for such an operation. However, before deciding on the most appropriate method for a given situation, remember that a more verbose routine using System Events can often be considerably faster than invoking a shell.
Modifying Data
In many cases, data in a plist file can also be modified although doing so may not actually change the current setting. This is mainly because a number of the settings stored in plist files are loaded into memory at startup/login. Since the version in memory is usually that which is subsequently used, changing the plist file will often have little or no effect.
Nevertheless, the technique can sometimes work so, before resorting to GUI scripting, it might be worth a try. In addition, not all settings are âregisteredâ in the way described and are picked up by certain applications when they launch.
Screen Saver settings, for instance, are checked by the ScreenSaverEngine application whenever it launches. So, to change the preferred Screen Saver module, we could run something like:
quit application "System Preferences"
set text item delimiters to {""}
tell application "System Events" to tell property list file (preferences folder's path & "ByHost:com.apple.screensaver." & words of (system info)'s primary Ethernet address & ".plist")
set value of property list item "modulePath" to "/System/Library/Screen Savers/Beach.slideSaver"
set value of property list item "moduleName" to "Beach"
end tell
Other applications, such as the Dock and SystemUIServer (which deals with any Apple menu items that appear to the right of the menu bar), might stay open after launch and wouldnât therefore be aware of a changed plist file setting. Nevertheless, if youâre not averse to being a bit heavy-handed, you could always force the application to quit so that, when it re-launches (usually automatically), the new settings should take effect. (When trying this trick with the Dock, bear in mind that any minimized windows that it may contain will become maximized.)
Hereâs a short script, based on the earlier data extraction example, which toggles the window minimization effect:
quit application "System Preferences"
tell application "System Events" to tell property list file ((preferences folder's path) & "com.apple.dock.plist")'s property list item "mineffect"
if value is "Genie" then
set value to "scale"
else
set value to "Genie"
end if
end tell
quit application "Dock"
Using Third-Party Software
Sometimes, a third-party application or scripting addition (often referred to as an osax after its filename extension) can be useful for changing certain settings.
However, if you wish to run this kind of solution on any other machine, that will also need the same third-party software installed since it wonât have been included as part of the standard install. (In other words, such an approach canât be described as âvanillaâ.) Remember, too, that scripting additions should be installed in one of the Scripting Additions folders: /Library/ScriptingAdditions/ or ~/Library/ScriptingAdditions/.
For these examples, letâs assume that we want to change the sound output volumeâŠ
Requires Jonâs Commands:
set sound volume to 200
Requires Extra Suites
tell application "Extra Suites" to ES set volume 100
Both of these utilities, along with many more, are available from scripting additions.
GUI Scripting of the System Preferences Application
As a relatively new technology, GUI scripting is by no means comprehensive or consistent in every circumstance but it can be invaluable for controlling applications that either do not have AppleScript support or are only partially scriptable.
Enabling GUI Scripting
By default, the accessibility frameworks are disabled. An administrative user can enable them by clicking the checkbox labelled Enable access for assistive devices, in the Universal Access System Preference pane, and entering their password in the resulting authentication dialog. Alternatively, in Mac OS X version 10.4 or later, access can be enabled using the AppleScript Utility (located in /Applications/AppleScript/).
In addition, if we have a script that relies on GUI scripting being enabled, we could even insert a handler like this:
to |enable GUI scripting|()
if (system attribute "sysv") < 4138 then display dialog "This script requires the installation of Mac OS X 10.3 or higher." buttons {"Cancel"} default button 1 with icon 2
tell application "System Events" to if not UI elements enabled then
tell me to display dialog "This script requires the built-in Graphic User Interface Scripting architecture of Mac OS X, which is currently disabled." & return & return & "Enable GUI Scripting now? (You may be asked to enter your password.)" buttons {"Cancel", "Enable"} default button 2 with icon 2
set UI elements enabled to true
if not UI elements enabled then error number -128
end if
end |enable GUI scripting|
|enable GUI scripting|()
Having made sure that itâs enabled, we can now explore the delights of GUI scriptingâŠ
Some Drawbacks of GUI Scripting
Actually, before we do go on, perhaps I should mention that scripting via the GUI is not generally considered an ideal method. It has a number of potential disadvantages, and can be:
* slow itâs sometimes necessary to wait for certain UI elements to appear, before theyâre accessible
* intrusive it may get in the way of whatever the user might be trying to do at any given moment
* susceptible the user could, by changing object focus, get in the scriptâs way at any given moment
* unpredictable the UI structure of an object can change substantially between different software versions
* local-dependent variations in language, keyboard layout and certain other local settings may need to be considered
* complex identifying a specific target element, particularly one thatâs deeply nested, can be a tortuous process
As a last resort, though, GUI scripting can be remarkably effective. So if I havenât put you off completely by now, letâs explore it a little furtherâŠ
Opening the Required System Preferences Pane & Tab
Our first job is to open System Preferences, get to the required pane and, if it contains more than one tab, select the relevant one. For the sake of an example, letâs say that we wanted to access the Output tab of the Sound preferences pane.
One way to achieve this might go something like:
activate application "System Preferences"
tell application "System Events"
tell application process "System Preferences"
tell window 1
click button "Show All" of group 1 of group 2 of tool bar 1 (* in case another pane is already open *)
tell button "Sound" of scroll area 1
repeat until exists (* wait until the object is accessible *)
delay 0.2
end repeat
click
end tell
tell radio button "Output" of tab group 1
repeat until exists (* wait until the object is accessible *)
delay 0.2
end repeat
click
end tell
end tell
end tell
end tell
Phew. And thatâs before we even begin to change any settings! (The script may also need considerable adjustment before it will work on a machine where English is not the primary language.)
However, in spite of its currently limited scripting support, this is at least one area in which System Preferences can help substantially. Compare the above routine with this much simpler approach:
tell application "System Preferences" to activate (reveal anchor "output" of pane id "com.apple.preference.sound")
Letâs take a look at how that actually worksâŠ
In this context, the term anchor is an element of a pane within a System Preferences window. It has only one (read only) property: name. This is used to identify it when using the reveal command. The anchor element and the reveal command are both referred to in System Preferencesâ AS dictionary.
Some panes (such as those whose id is âcom.apple.preference.generalâ, âcom.apple.preference.digihub.discsâ, âcom.apple.preference.exposeâ, âcom.apple.preference.dockâ, âcom.apple.preference.securityâ or âcom.apple.preference.startupdiskâ) might have only a single anchor usually named âmainâ.
So something likeâŠ
tell application "System Preferences" to activate (reveal anchor "main" of pane id "com.apple.preference.general")
⊠will have pretty much the same effect as:
tell application "System Preferences"
set current pane to pane id "com.apple.preference.general"
activate
end tell
Other panes might include a âmainâ anchor along with several others. For example, pane id âcom.apple.preference.soundâ contains 4 anchors: âmainâ, âoutputâ, âinputâ and âeffectsâ. Revealing the âmainâ anchor will simply show the sound pane that was chosen previously. (This could be used if access to only the output volume slider was required since thatâs generally available in all variants of the sound pane.) Revealing the output, input and effects anchors will access the balance, input level and alert volume sliders, respectively.
In many cases, revealing an anchor can perform the equivalent function of selecting a tab. However, thatâs not always the case.
The following, for instance, effectively emulates the clicking of the âHot CornersâŠâ button of âDesktop & Screen Saverâ. So it will display the âActive Screen Cornersâ sheet.
tell application "System Preferences" to activate (reveal anchor "ScreenSaverPref_HotCorners" of pane id "com.apple.preference.desktopscreeneffect")
Accessing Items that Require Authentication
In other situations, revealing an anchor is about the only way to access certain elements. Anyone who has ever tried, via GUI scripting, to get into the Accounts/Login Options of System Preferences will know what a headache that can be.
But something like this (perhaps with more appropriate password precautions, if required) should do the trick:
property authentication_data : missing value
to get_data(d)
repeat
tell text returned of (display dialog "Enter the required " & d & ":" default answer "" with hidden answer) to if (count) > 0 then return it
end repeat
end get_data
to authenticate_changes() (* may need a few moments to execute *)
tell application "System Events" to tell window "Authenticate" of process "SecurityAgent"
tell group 1 to repeat with i from 1 to 2
set value of text field i to authentication_data's item i
end repeat
click button "OK" of group 2
end tell
end authenticate_changes
if authentication_data is missing value then set authentication_data to {get_data("name"), get_data("password")}
tell application "System Preferences" to set current pane to pane id "com.apple.preferences.users"
tell application "System Events" to tell button "Click the lock to make changes." of window 1 of process "System Preferences" to if exists then
click
my authenticate_changes()
end if
tell application "System Preferences" to activate (reveal anchor "loginOptionsPref" of pane id "com.apple.preferences.users")
Identifying Panes and Anchors
âAh yes, thatâs all very well,â I can hear you say. âBut how do I identify preferences panes and anchors in the first place?â
Good question, although itâs one that System Preferences can answer quite easily as demonstrated by this short example:
tell application "System Preferences"
activate
tell current pane to if it exists then
set pane_name to localized name
else
set pane_name to missing value
end if
set pane_name to (choose from list (get localized name of panes)
with prompt "Choose a pane:" default items pane_name)
if pane_name is false then error number -128
set pane_id to id of (first pane whose localized name is pane_name's beginning)
set anchor_name to (choose from list (get name of pane pane_id's anchors)
with prompt "Choose an anchor from " & pane_name & ":")
if anchor_name is false then error number -128
end tell
tell application "Script Editor"
activate
make document with properties {text:"tell application \"System Preferences\"
activate
reveal anchor \"" & anchor_name & "\" of pane id \"" & pane_id & "\"
end"}
end tell
Some GUI Elements Whose Values Donât Stick
In general, changing the values of system settings via GUI Scripting can work quite well. However, itâs worth noting that some UI elements (notably certain sliders) appear to be Teflon-coated. Since their values are not entirely persistent, these are best set using an alternative method. They include:
Table 2. UI Elements Whose Values Are Not Persistent
General GUI Scripting Utilities
At this point, I might have attempted to go into some detail about the finer points of scripting the GUI. However, since thatâs a little beyond the scope of this piece, perhaps we can leave it to another time. All the same, since Iâm aware of how tricky it can sometimes be to identify UI elements, Iâll mention one or two products that might help just in case youâre not already aware of them.
First, there are a couple of Apple applications that can make exploring a GUI objectâs structure a little easier: âAccessibility Inspectorâ and âAccessibility Verifierâ. If you have Appleâs Developer Tools installed, youâll find them both in the folder /Developer/Applications/Utilities/Accessibility Tools/. If not, an older version of Accessibility Inspector, still available at the Apple Developer web-site, was released as sample code under the name UIElementInspector.
Then again, if youâre serious about GUI scripting, you might want to consider PreFab UI Browser, which will not only help you navigate the user interface hierarchy, but can also generate useful AppleScript statements with a single click.
SysPref Scripter
Iâve also done a little work on developing a script to assist in this particular area of scripting. This was originally a collection of smaller scripts, for my own use and these have now been combined as a single script, which might hopefully be of some help to you.
Its aim is to identify the relevant property list files (for plist scripting) or UI elements (for GUI scripting) relating specifically to System Preferences and then to draft an initial script, which can be modified further as required.
For the most part, a series of dialogs should help to guide you through the various choices and stages of analysis. The following notes may also help:
General Principles:
* It makes sense to consider scripting methods in a particular order, usually starting with the least intrusive.
* If a suitable Unix command exists, such as scselect or pmset, it could be the best option.
* After that, experiment with plist scripting. If that works, stick with it.
* Finally, if all else fails, try GUI scripting. This should work for most of the remaining settings.
Initial Use of SysPref Scripter:
* Read each dialog carefully before you dismiss it since it will often explain what action you should take next.
* To identify a UI element for GUI scripting, simply hold the mouse pointer over the target. Donât click it.
* Conversely, to identify property list files/items for plist scripting, make sure you click, drag or select an item to change its value.
* Thereâs a two-stage identification process for plist scripting: 1) to identify the plist file, 2) to identify any modified items in the file.
* Did I mention about reading each dialog carefully?
Other SysPref Scripter Considerations:
* Bear in mind that changing a value in System Preferences may not always result in the identification of a property list file or item.
* In the event of a failure, try again once or twice in case there was a temporary glitch. Then, if no dice, move on.
* Even if a file and item is identified, attempts to change it using a script may not work.
* Attempts at plist scripting may also fail if date/time settings are changed during the run.
* Occasionally (including on the first run), an extra dialog will explain that file modification dates may need adjusting.
Third-Party Software:
* For GUI scripting operations, the script requires requires the scripting addition Xtool.
* However, other operations should still be possible without the need for Xtool - and a dialog should notify when it is required.
Requires Xtool:
-- SysPref Scripter --
-- Version 1.0.2 --
-- 2007.03.25 --
-- Kai Edwards --
-- known issues: if date/time settings are changed in System Preferences while running this script, it may not work correctly with plist files
-- timing properties -- (adjust only if necessary)
property dlog_timeout : 5 * minutes -- [integer] seconds before a dialog/'choose from list' times out
property mouse_timeout : 30 -- [integer] seconds after which mouse position monitoring is cancelled
property pre_move_delay : 0.3 -- [real] initial delay in seconds before the mouse position is monitored
property post_move_delay : 0.7 -- [real] seconds the mouse must remain static before its final position is recorded
property general_delay : 0.5 -- [real] seconds before script proceeds - to allow time for, say, an object to appear
property mod_timeout : 15 -- [integer] seconds before plist file change detector times out
-- copy/paste properties --
property default_app : "System Preferences"
property default_option : "Copy only"
property copy_button : missing value
property paste_script : false
-- other script properties --
property app_timeout : dlog_timeout + minutes
property check_rate : post_move_delay div 0.1
property anchor_name : "main"
property script_target : "the clipboard"
property osax_name : "XTool.osax"
property osax_url : "http://osaxen.com/files/xtool1.1.html"
property type_list : {"property", "attribute", "action"}
property enter_key : ASCII character 3
property default_property : missing value
property default_attribute : missing value
property default_action : missing value
property GUI_option_list : missing value
property script_option_list : missing value
property curr_script_option : missing value
property current_GMT : missing value
property back_button : missing value
property pane_name : missing value
property last_login : missing value
property login_anchor : false
-- global variables --
global target_string, element_list, pane_id, popup_text
-- shared handlers --
to |copy script|(script_list)
set tid to text item delimiters
set text item delimiters to return
set script_string to script_list as Unicode text
set text item delimiters to tid
activate application default_app
tell application default_app to if paste_script then
if default_app starts with "Script Editor" then
make document with properties {text:script_string}
else if default_app starts with "Smile" then
tell (make class sctx)
set its text to script_string
event SATICKSN
end tell
else
set clip_store to the clipboard
set the clipboard to script_string
tell application "System Events"
keystroke "nv" using command down
keystroke enter_key
end tell
delay general_delay
set the clipboard to clip_store
end if
else
set the clipboard to script_string
display dialog "The script is ready to paste." buttons "OK" default button 1 with icon 1 giving up after 1
end if
error number -128
end |copy script|
on dlog(msg, btns, btn, icn)
tell application "System Preferences" to with timeout of app_timeout seconds
activate
tell (display dialog msg buttons {"Cancel"} & btns default button btn with icon icn giving up after dlog_timeout)
if gave up then error number -128
button returned
end tell
end timeout
end dlog
to |choose item|(item_type, item_list, default_item)
if (count item_list) > 1 then with timeout of dlog_timeout seconds
if default_item is not in item_list then set default_item to item_list's beginning
tell application "System Preferences" to try
activate
set item_list to (choose from list item_list with prompt "Choose the required " & item_type & ":" default items default_item)
on error number -1712 (* errAETimeout *)
tell application "System Events" to tell button "Cancel" of application process "System Preferences"'s front window to if exists then click
error number -128
end try
end timeout
if item_list is false then error number -128
item_list's beginning
end |choose item|
on |object text| for o
try
{o}'s t
on error t
end try
set tid to text item delimiters
set text item delimiters to "{"
set t to t's text from text item 2 to end
set text item delimiters to "}"
set t to t's text beginning thru text item -2
set text item delimiters to tid
t
end |object text|
to |set copy options|()
set app_list to {}
set copy_list to {"Copy only"}
set paste_list to {}
repeat with i in {"com.latenightsw.scriptdebugger", "com.apple.scripteditor2", "com.satimage.smile"}
tell application "Finder" to try
tell (get application file id i's displayed name)
set app_list's end to it
set copy_list's end to "Copy and activate " & it
set paste_list's end to "Paste in new " & it & " document"
end tell
end try
end repeat
tell |choose item|("script copy/paste option", copy_list & paste_list & back_button, default_option) to if it is not back_button and it is not default_option then
set default_option to it
set paste_script to it is in paste_list
if it is "Copy only" then
set default_app to "System Preferences"
else
repeat with i in app_list
if i is in it then exit repeat
end repeat
set default_app to i's contents
end if
end if
|choose script option|()
end |set copy options|
-- GUI routine handlers --
on |mouse position|()
delay pre_move_delay
set cancel_time to (current date) + mouse_timeout
repeat while (current date) comes before cancel_time
set start_position to mouse location
repeat check_rate times
delay 0.1
if (mouse location) is not start_position then exit repeat
end repeat
tell (mouse location) to if it is start_position then return it
end repeat
error number -128
end |mouse position|
on |repeat loop|()
ignoring white space
tell application "System Events" to tell button "OK" of sheet 1 of window 1 of application process "System Preferences" to if exists then
click
if element_list's beginning ends with "sheet 1" then return "repeat until exists" & return & "delay 0.2" & return & "end repeat" & return
end if
end ignoring
""
end |repeat loop|
on |property text keys| for chosen_target
set l to chosen_target's properties
set t to (|object text| for l)'s text 2 thru -2 & ","
set tid to text item delimiters
repeat with i in l as list
set text item delimiters to ":" & (|object text| for i's contents) & ","
set t to t's text items
set text item delimiters to return
set t to t as Unicode text
end repeat
set text item delimiters to tid
t's paragraphs 1 thru -2
end |property text keys|
to |choose target|(target_type, target_list, current_target)
tell |choose item|(target_type, target_list & back_button, current_target)
if it is back_button then return my |GUI routine|()
it
end tell
end |choose target|
to |choose property| for chosen_element
if curr_script_option starts with "Get" then -- get property value
set default_property to |choose target|("property", (|property text keys| for chosen_element) & " *every property", default_property)
if default_property is " *every property" then
set target_string to "properties"
else
set target_string to default_property
end if
else -- set property value
set default_property to |choose target|("property", (|property text keys| for chosen_element), default_property)
set target_string to "set " & default_property & " to " & (|object text| for run script "tell application \"System Events\" to " & default_property & " of " & (|object text| for chosen_element))
end if
end |choose property|
to |choose attribute| for chosen_element
tell application "System Events" to if curr_script_option starts with "Get" then -- get attribute value
set default_attribute to my |choose target|("attribute", name of chosen_element's attributes & " *every attribute", default_attribute)
if default_attribute is " *every attribute" then
set target_string to "value of attributes"
else
set target_string to "value of attribute \"" & default_attribute & "\""
end if
else -- set attribute value
tell chosen_element
set settable_count to count (attributes where it is settable)
if settable_count is 0 then
my dlog("The selected element has no attributes that can be set.", back_button, 2, 2)
return my |GUI routine|()
else if settable_count is 1 then
tell (first attribute where it is settable) to set target_string to "set value of attribute \"" & name & "\" to " & my (|object text| for value)
else
set default_attribute to my |choose target|("attribute", name of chosen_element's attributes where it is settable, default_attribute)
set target_string to "set value of attribute \"" & default_attribute & "\" to " & my (|object text| for value of first attribute whose name is default_attribute)
end if
end tell
end if
end |choose attribute|
to |get menu items| for popup_button
tell application "System Events" to tell popup_button
set curr_value to value
click
tell menu 1 to if (exists) and (count menu items) > 0 then
set menu_list to every Unicode text of (get name of menu items) & {" *pop up button only", back_button}
perform action "AXCancel"
tell my |choose item|("item to select", menu_list, curr_value)
if it is back_button then return my (|choose action| for popup_button)
if it is not " *pop up button only" then set popup_text to return & "tell menu 1's menu item \"" & it & "\"" & return & "perform action \"AXPress\"" & return & "end tell"
end tell
end if
end tell
end |get menu items|
to |choose action| for chosen_element
tell application "System Events"
set action_count to count chosen_element's actions
if action_count is 0 then
my dlog("The selected element has no actions to perform.", back_button, 2, 2)
return my |GUI routine|()
else if action_count is 1 then
set target_string to "perform action \"" & name of chosen_element's first action & "\""
else
set default_action to my |choose target|("action", name of chosen_element's actions, default_action)
set target_string to "perform action \"" & default_action & "\""
end if
end tell
if (|object text| for chosen_element) starts with "pop up button" and target_string ends with "\"AXPress\"" then |get menu items| for chosen_element
end |choose action|
to |make UI script| for chosen_element
set popup_text to ""
tell curr_script_option's last word to if it is "property" then
my (|choose property| for chosen_element)
else if it is "attribute" then
my (|choose attribute| for chosen_element)
else
my (|choose action| for chosen_element)
end if
end |make UI script|
to |close sheet|()
tell application "System Events" to tell button "OK" of sheet 1 of window 1 of application process "System Preferences" to if exists then click
end |close sheet|
to |access login options|()
tell application "System Events"
tell window 1 of application process "System Preferences"
if enabled of list 1's group -1 then return
click button 4
end tell
repeat while exists window 1 of application process "SecurityAgent"
delay general_delay
end repeat
if not enabled of group -1 of list 1 of window 1 of application process "System Preferences" then error number -128
end tell
end |access login options|
to |identify element| from object_ref at {x, y}
tell application "System Events" to tell object_ref's UI elements to set {e, s, p} to {it, size, position}
repeat with i from (count e) to 1 by -1
set {h, v} to p's item i
tell s's item i to if h Ăąâ°Â€ x and v Ăąâ°Â€ y and h + beginning Ăąâ°Â„ x and v + end Ăąâ°Â„ y then return my (|identify element| from e's item i at {x, y})
end repeat
object_ref
end |identify element|
to |choose element|()
tell application "System Events" to set SysPrefs_window to front window of application process "System Preferences"
set chosen_element to |identify element| from SysPrefs_window at |mouse position|()
if chosen_element is SysPrefs_window then
set element_list to {"", ""}
else
tell (|object text| for chosen_element) to set element_list to {"tell " & text 1 thru ((my (offset of " of window " in it)) - 1) & return, return & "end tell"}
end if
|make UI script| for chosen_element
end |choose element|
on |GUI routine|()
if dlog("Click the 'Identify Element' button, then position the mouse pointer over the required System Preferences UI element." & return & return & "Leave it in that position until the next dialog appears.", {back_button, "Identify Element"}, 3, 1) is back_button then return |choose script option|()
|choose element|()
tell {{"tell application \"System Preferences\"", "activate"}, {"reveal anchor \"" & anchor_name & "\" of pane id \"" & pane_id & "\"", "end tell", "", "tell application \"System Events\" to tell front window of application process \"System Preferences\"", element_list's beginning & |repeat loop|() & target_string & popup_text & element_list's end, "end tell"}} to if login_anchor then
my |copy script|({"to access_login_options()", "tell application \"System Events\"", "tell window 1 of application process \"System Preferences\"", "if enabled of list 1's group -1 then return", "click button 4", "end tell", "repeat while exists window 1 of application process \"SecurityAgent\"", "delay " & general_delay, "end repeat", "if not enabled of group -1 of list 1 of window 1 of application process \"System Preferences\" then error number -128", "end tell", "end access_login_options", ""} & beginning & "my access_login_options()" & end)
else
my |copy script|(it)
end if
end |GUI routine|
-- plist routine handlers --
on |modified items| from orig against dupe
set mod_list to {}
tell application "System Events" to repeat with n from 1 to count orig's property list items
tell orig's property list item n
tell name to if exists then set n to it (* sequence of named keys may differ between files *)
if value is not value of dupe's property list item n then
set item_count to count property list items
if item_count > 0 and item_count is (count property list items of dupe's property list item n) then
set mod_list to mod_list & my (|modified items| from it against dupe's property list item n)
else
set mod_list's end to it
end if
end if
end tell
end repeat
mod_list
end |modified items|
to |make plist script| from orig_path against dupe_path
tell application "System Events" to set mod_list to my (|modified items| from property list file orig_path against property list file dupe_path)
set mod_count to count mod_list
if mod_count is 0 then
dlog("No property list item changes were detected in \"" & orig_path & "\". To try again, click the '" & back_button & "' button." & return & return & "Make sure the value of the same object is changed. If necessary, check that the currently selected property list file seems appropriate.", {back_button}, 2, 2)
return |identify modified plist item| from orig_path
end if
set script_start to {}
set script_end to {}
set tid to text item delimiters
set text item delimiters to " of contents of property list file \""
set path_text to last text item of (|object text| for mod_list's beginning)
set text item delimiters to "\""
set path_text to path_text's first text item & "\")"
set script_text to "preferences folder's path & \""
set text item delimiters to path to preferences as Unicode text
if (count path_text's text items) is 1 then
set script_text to "local domain's " & script_text
set text item delimiters to path to preferences from local domain as Unicode text
end if
set path_text to path_text's last text item
if path_text starts with "ByHost:" then
set text item delimiters to {""}
set text item delimiters to words of (system info)'s primary Ethernet address as Unicode text
tell path_text's text items to if (count) is 2 then
set script_start to {"set text item delimiters to {\"\"}"}
set path_text to beginning & "\" & words of (system info)'s primary Ethernet address & \"" & end
end if
end if
set script_text to "tell application \"System Events\" to tell property list file (" & script_text & path_text
set text item delimiters to " of contents of property list file \""
if curr_script_option starts with "Set" then -- set plist value(s)
set script_start's beginning to "quit application \"System Preferences\""
tell application "System Events" to repeat with i in mod_list
set i's contents to "set value of " & first text item of my (|object text| for i's contents) & " to " & my (|object text| for i's value)
end repeat
if path_text contains "com.apple.dock." then
set script_end to {"quit application \"Dock\"", ""}
else if path_text contains "com.apple.systemuiserver.plist" then
set script_end to {"do shell script \"killall SystemUIServer\"", ""}
end if
else -- get plist value(s)
repeat with i in mod_list
set i's contents to "value of " & first text item of (|object text| for i's contents)
end repeat
set text item delimiters to ", "
set mod_list to mod_list as Unicode text
if mod_count > 1 then set mod_list to "{" & mod_list & "}"
end if
set text item delimiters to tid
|copy script|(script_start & {script_text, mod_list, "end tell", script_end})
end |make plist script|
to |identify modified plist item| from plist_path
set temp_path to POSIX path of (path to temporary items)
do shell script "cp -f " & plist_path's quoted form & space & temp_path's quoted form
if dlog("Now click the 'Identify Plist Item' button, and change the value of the same object again." & return & return & "Wait once more for the change to register.", {back_button, "Identify Plist Item"}, 3, 1) is back_button then return |plist routine|()
set current_timeout to (current date) + mod_timeout - (time to GMT)
tell application "System Events" to tell file plist_path
set {name:file_name, modification date:mod_date} to it
repeat while ((current date) - (time to GMT)) < current_timeout
if modification date is not mod_date then return my (|make plist script| from plist_path against (temp_path & file_name))
delay 0.2
end repeat
end tell
dlog("No property list item changes were detected in \"" & file_name & "\". To try again, click the '" & back_button & "' button." & return & return & "Make sure the value of the same object is changed. If necessary, check that the currently selected property list file seems appropriate.", {back_button}, 2, 2)
return |identify modified plist item| from plist_path
end |identify modified plist item|
to |identify modified plist file| from start_date
set current_timeout to start_date + mod_timeout
tell application "System Events" to repeat while (current date) - (time to GMT) < current_timeout
repeat with current_domain in {user domain, local domain} -- the additional loops and date comparisons below are due to a System Events filter bug
tell current_domain's preferences folder
repeat with f in (get files whose modification date > start_date + (time to GMT) and name extension is "plist" and name is not "com.apple.recentitems.plist" and name does not start with "com.apple.iCal")
tell f to if modification date > start_date + (time to GMT) then return my (|identify modified plist item| from POSIX path)
end repeat
repeat with c in (get folders whose modification date > start_date + (time to GMT))
tell c to if modification date > start_date + (time to GMT) then repeat with f in (get files whose modification date > start_date + (time to GMT) and name extension is "plist")
tell f to if modification date > start_date + (time to GMT) then return my (|identify modified plist item| from POSIX path)
end repeat
end repeat
end tell
end repeat
delay general_delay
end repeat
dlog("No file changes were detected. To try again, click the '" & back_button & "' button." & return & return & "If attempts fail repeatedly, the target value may not be stored in the usual locations (in which case, GUI scripting may be worth considering).", back_button, 2, 2)
return |plist routine|()
end |identify modified plist file|
to |limit mod dates| for target_folder to target_date
tell application "System Events" to tell target_folder
set modification date of (files whose (name starts with "com.apple" or name ends with "references.plist") and modification date > target_date) to target_date
if modification date > target_date then set modification date to target_date
end tell
end |limit mod dates|
to |limit plist mod dates| to target_date
tell (time to GMT)
if it is current_GMT then return
my dlog("Some property list file modification dates may need to be adjusted to reflect possible recent time zone changes." & return & return & "This may take a few moments.", "OK", 2, 1)
set current_GMT to it
end tell
tell application "System Events"
tell user domain's preferences folder
tell folder "ByHost" to if exists then my (|limit mod dates| for it to target_date)
my (|limit mod dates| for it to target_date)
end tell
tell local domain's preferences folder
tell folder "SystemConfiguration" to if exists then my (|limit mod dates| for it to target_date)
my (|limit mod dates| for it to target_date)
end tell
end tell
end |limit plist mod dates|
on |plist routine|()
|limit plist mod dates| to current date
if dlog("Click the 'Identify Plist File' button, then change the value of an object in the current pane (confirming the change if necessary)." & return & return & "Wait a few moments for the change to register.", {back_button, "Identify Plist File"}, 3, 1) is back_button then return |choose script option|()
|identify modified plist file| from (current date) - (time to GMT)
end |plist routine|
-- pane/anchor/script selection handlers --
to |choose script option|()
set chosen_option to |choose item|("script option", script_option_list, curr_script_option)
if chosen_option is copy_button then
return |set copy options|()
else if chosen_option is back_button then
return |choose anchor| with going_back
else if chosen_option is "Enable GUI options" then
return |get osax|()
end if
set curr_script_option to chosen_option
if curr_script_option is in GUI_option_list then
|GUI routine|()
else
|plist routine|()
end if
end |choose script option|
to |choose anchor| given going_back:going_back
|close sheet|()
tell application "System Preferences" to tell current pane
set pane_id to id
set anchor_list to name of anchors
if (count anchor_list) is 1 then
if going_back then return my |choose pane|()
set anchor_name to anchor_list's beginning
reveal anchor anchor_name
return my |choose script option|()
end if
tell my |choose item|("anchor", anchor_list & back_button, anchor_name)
if it is back_button then return my |choose pane|()
set anchor_name to it
end tell
set login_anchor to anchor_name is "loginOptionsPref"
if login_anchor then my |access login options|()
reveal anchor anchor_name
end tell
delay general_delay
tell application "System Events" to if exists image 1 of sheet 1 of window 1 of application process "System Preferences" then
my dlog("That option does not appear to be available.", back_button, 2, 2)
return my (|choose anchor| with going_back)
else
my |choose script option|()
end if
end |choose anchor|
to |choose pane|()
activate application "System Preferences"
|close sheet|()
tell application "System Preferences"
set pane_list to localized name of panes
if not show all then set pane_name to current pane's localized name
set pane_name to my |choose item|("preference pane", pane_list, pane_name)
set current pane to first pane whose localized name is pane_name
end tell
|choose anchor| without going_back
end |choose pane|
-- initial setup handlers --
to |get osax|()
if dlog("To enable the GUI options of this script, you need to install the scripting addition \"" & osax_name & "\" in your scripting additions folder." & return & return & "Go to the download site now?", {back_button, "Download Site"}, 3, 1) is back_button then return |choose script option|()
open location osax_url
error number -128
end |get osax|
to |check GUI|()
tell application "System Events" to if UI elements enabled then return
dlog("This script requires the built-in Graphic User Interface Scripting architecture of Mac OS X, which is currently disabled." & return & return & "Enable GUI Scripting now?", "Enable", 2, 2)
tell application "System Events"
set UI elements enabled to true
if not UI elements enabled then error number -128
end tell
delay 1
end |check GUI|
to |set script options|()
if osax_name is in (list folder (path to scripting additions) without invisibles) or osax_name is in (list folder (path to scripting additions from user domain) without invisibles) then
set GUI_op