All AppleScript, All the Time

Every once in a while, you want stuff to happen automatically at certain times on certain days, or you want a particular event to precipitate some kind of action. Mail makes this easy with the Rules system, iCal has the Run script alarm option, and you can achieve some measure of success with Folder Actions as well. If you want to dig into a little UNIX, launchd is very versatile, especially on an Intel Mac, as is cron (simplified with Cronnix).

These are all fabulous background solutions, and go a long way to helping those of us who enjoy automation to customize our machines the way we desire. Another alternative to consider is the Stay Open Application option using AppleScript. Let’s say you simply must hear a particular song in your iTunes library at noon every day, sort of a Pavlovian signal to lunch. The basic script is simple:


set work_Days to {Monday, Tuesday, Wednesday, Thursday, Friday}
if work_Days contains ((current date)'s weekday) and ((current date)'s time = 43200) then
	tell application "iTunes" to play track 1 of playlist "BVD"
end if

As you can see, this is dandy as long as you run the script precisely at noon. (Remember that time is kept as seconds since the previous midnight, so 12:00 noon would be 60 seconds X 60 minutes X 12 hours to get 43200.) You could also use the eternal repeat:


repeat
	set work_Days to {Monday, Tuesday, Wednesday, Thursday, Friday}
	if work_Days contains ((current date)'s weekday) and ((current date)'s time = 43200) then
		tell application "iTunes" to play track 1 of playlist "BVD"
	end if
end repeat

And although this works, it really taxes your CPU; just activate Activity Monitor (located in your Applications/Utilities folder) and run that script to see how much of a burden it really is to keep it going all the time.

To make this little project function as a Stay Open Application and achieve the same results (without the CPU load), you would enclose the script inside of an on idle handler, like this:


on idle
	set work_Days to {Monday, Tuesday, Wednesday, Thursday, Friday}
	if work_Days contains ((current date)'s weekday) and ((current date)'s time = 43200) then
		tell application "iTunes" to play track 1 of playlist "BVD"
	end if
	return 1
end idle

The on idle handler functions as an automatically repeating set of instructions. Notice how I have added the command return 1 just before the end idle. This instructs the handler to repeat this script every second. As written, this script will not run in Script Editor; to test it out, you need to save it as a Stay Open Application and run the application. First, however, it is always advisable to save the script as a .scpt file, then do your Save As. Here is the Save As window you will see:

Figure 1: Save Dialog for Saving a Script as a Stay-Open Script Application.

You just choose application in the File Format list, and be sure that the Stay Open box is checked. When you run that new application, you will see a new script-like icon in your dock, and it just might work. As I have played around with this, I have noticed that it sometimes skips when asked to hit the correct second, even with the return 1 command, so I modified the test line to open up a 5 second window:

if work_Days contains ((current date)'s weekday) and ((current date)'s time > 43200) and ((current date)'s time < 43205) then

If you still have your Activity Monitor running, you can see that the load on the CPU is practically zero with this script running all the time.

You can also get fancy with these things. Imagine that you have to fill out a spreadsheet on Thursday of every week and you also need to fill out an evaluation form every Monday for your subordinates. You think it would be cool for those documents to automatically open for you on those days at 10:00 am:


property boss_Sheet : Thursday
property sub_Form : Monday

on idle
	if ((current date)'s weekday) = boss_Sheet and ((current date)'s time > 36000) and ((current date)'s time < 36015) then
		run script "TyrannoDrive:Users:praxisii:Library:Scripts:OpenBossExcel.scpt"
	else if ((current date)'s weekday) = sub_Form and ((current date)'s time > 36000) and ((current date)'s time < 36015) then
		my GetSubForm()
	end if
	return 13
end idle

to GetSubForm()
	tell application "Microsoft Word"
		if (get name of every document) does not contain "Subordinate Evaluation" then
			open document "path:theDoc"
		end if
	end tell
end GetSubForm

Just as in any other script, you can declare properties (and the values will be retained between launches, so long as you do not re-compile), global variables, and use different handlers. The only thing you must include is the [i]idle handler[/i] with the [i]return[/i] command, and that return can be extremely versatile, depending on how often you want to call the script inside of the idle handler.

There are two other handlers that although optional, can be helpful, depending on how complicated your application becomes and how many different actions you want it to perform.

on run
This handler runs when the application is started to set things up. It is not necessary to include this, but if you need to generate any user input for properties or global variables, this is a very handy method to use. It will only be called one time, when the application is initialized, and the on idle handler runs immediately following the run handler. Be sure to use end run at the conclusion of your list of startup items.

on quit
I admit that I have never used this handler, so here is an explanation by Adam Bell:

“Use this if you want to clean things up, save preferences or a log file, change property values or simply ask if the user really wants to quit before your application actually quits. This section runs only if the application encounters a ‘quit’ in the script itself, or is quit from the dock, or some other application. If you do include an ‘on quit’ handler, then continue quit must be the last statement in the handler, or the script will not stop! The presence of an on quit handler ‘grabs’ the system command to stop the process, so it will not complete the quit process until it is restarted; continue quit restarts the system’s quit process.”
Here are two of Adam’s examples:


on quit -- save, print, log, etc. A final process to complete.
	set B to button returned of (display dialog "Would you like to save changes to the log file?" buttons {"Quit Without Saving", "Save"} default button 2)
	if B is "Save" then
		-- append log to the file, write to prefs, print a file, note time stopped, etc.
		continue quit
	else
		continue quit -- just quit without doing anything else.
	end if
end quit 

-- or this:

on quit -- warn the user.
	set B to button returned of (display dialog "Are you sure you want to quit?" & return & "Your logging will stop." buttons {"Oops, NO!", "Yes, Please"} default button 1)
	if B contains "Yes" then
		continue quit
	else
		return -- idler keeps running, quit process not re-engaged.
	end if
end quit

If you don’t like the 3-D script icon that is automatically associated with your Stay Open Application, you can use a slightly different Save As technique to assign your own icon to your creation. When you have your script the way you want it, and have clicked Save As, choose the [i]File Format: application bundle[/i], and don’t forget to check the Stay Open box:

Figure 2: Save Dialog for Saving a Script as a Stay-Open Application Bundle.

After you have saved it, take a look at your Script Editor window:

Figure 3: Script Editor Window with “Bundle Contents” Toolbar Button Now Appearing.

You now have another accessible option in your Script Editor toolbar, entitled Bundle Contents. When you click there, a drawer will open and you will see a file called applet.icns, which is the little icon file that is native to AS applications. There are a couple of steps necessary to successfully change that to a different icon. First, locate a legitimate .icns file that you would like to use. Drag that file into the open Bundle Contents drawer, and then select the applet.icns file, right-click, and choose Delete. Now, select the new .icns file, right-click, and choose Rename. Name that file applet.icns and close the Bundle Contents drawer. Save your application, and here is where it gets weird. You will have the new icon in your dock as soon as you activate your application, but to get the icon to change in the folder wherein the application resides, sometimes you either need to quit SE and re-open the application file, and sometimes you need to change the name of application. I don’t really understand this part, but those are the two methods that I have found to work.

If you absolutely despise an extra icon of any sort cluttering up your nice, neat dock, take a look at James Sentman’s Drop Script Backgrounder. I have not used it personally (I have no problem with a messy dock), but it looks very cool.

Activity Monitor is a great application to keep an eye on your system resources, and it has some nice customization features as well, but I prefer iSlayer’s iStat Menus because you can have graphical CPU and memory usage information right there on your menu bar. Although it is freeware, donations are recommended and I certainly encourage you to do so. (Disclosure - I have no relationship at all with anyone at iSlayer; they do not know I am making this recommendation.)

I use a single Stay Open Application on my old dual G4 PowerMac:


property first_Tuesday : date "Tuesday, September 18, 2007 12:00:00 AM" --The initial property value is the first known tuesday for running payroll.  As long as the script is never re-compiled, the property changes every 2 weeks to next pay day, so that the current property is the FUTURE date.
property scr_path : (path to scripts folder as Unicode text)
property log_Path : ((path to documents folder as Unicode text) & "Cathouse:Payroll:StayOpenLog.txt")
property Open_Payroll_Script : scr_path & "TCH:OpenPayroll.scpt"
property ret_num : 3600 --1 hour  21600 --6 hours --This is the return value for the on idle handler (in seconds)

on idle
	if ((current date)'s short date string) = (first_Tuesday's short date string) then --This will check to see if we are on the correct day.
		set right_now to (current date)'s time --Get the time...
		if right_now > 32400 and right_now < 36000 then -- If the time is between 9 and 10 AM
			run script file Open_Payroll_Script -- Should open up Payroll Excel sheet
			set first_Tuesday to (first_Tuesday + (14 * days)) -- Should set the property 2 weeks into the future
			my WriteLog("Payroll script called.")
			tell application "TextEdit" to make new document with properties {text:"The next payroll is :" & (first_Tuesday as string)} -- Just for fun, make a TextEdit document announcing that the new property has been created
			my WriteLog("New Property: " & (first_Tuesday as string))
		else
			set ret_num to (35100 - right_now) --Time difference between now and 9:45 AM in seconds
		end if
	else if first_Tuesday < (current date) then --this will fire if the script has been recompiled and the property needs to be set into the future
		set old_date to (first_Tuesday's date string) --save the value for log purposes
		repeat until first_Tuesday > (current date)
			set first_Tuesday to first_Tuesday + (14 * days) --Set it to the proper 2 weeks in advance value
		end repeat
		my WriteLog("Tuesday property changed from " & old_date & " to " & (first_Tuesday's date string)) -- write the log
		set ret_num to 100 --try again in 100 seconds, just in case
	else
		my WriteLog("Payroll script not called.")
		if ret_num ≠ 3600 then set ret_num to 3600 --If the number is not 3600 (1 hour), set it to 1 hour
	end if
	return ret_num -- This should make the script cycle depending on the need
end idle

to WriteLog(txt)
	set wrtlog to open for access file log_Path with write permission
	write ((current date) as string) & " -- " & txt & return to wrtlog starting at eof
	close access wrtlog
end WriteLog

Although a bit sloppy, it works great, and uses no significant resources at all. I set the return to 3600 (1 hour) and I have it keep a log in a .txt file. Its only function is to track every other Tuesday, and then open up an Excel document that I have to fill in and email to my accountant. It is very handy, and my accountant appreciates not having to call and remind me to do it every two weeks.

As with most tasks utilizing AS, you are only limited by your own imagination when thinking of uses for this. There is a great thread on the OS X board from Summer 2007 where we helped a poster devise a Stay Open Application to automatically download, verify, and erase camera cards while he shot sporting events. If you are interested in this topic at all, take a look at that thread sometime.

This unique feature of AS offers a quick and simple solution to automation and customization, and I hope you enjoy playing with it.