Idle Thoughts

Idle Thoughts

A stay-open AppleScript application containing an idle handler receives periodic idle events from the system, even if the application is running in the background. The idle handler is automatically called for the first time as soon as the application’s run or open handler terminates (no explicit call to the idle handler is required). The idle handler is run again every thirty seconds thereafter by default. It can be made to run at different intervals by causing the idle handler to return a positive integer number of seconds (or, starting with Mac OS X 10.3 (Panther), a positive real number) as the desired “sleep” period until the next idle event. Any other return value, such as 0, causes it to repeat at the previously established interval. It can be made to run at random intervals, for example, by calling the Random Number command in the Standard Additions scripting addition as the last statement in the idle handler. The idle handler can also be run on demand by calling it explicitly.

Note carefully that this all works only if you save your script as an application with the Stay Open option enabled.

The key to effective use of the idle handler is to think of a script application’s run or open handler as an initialization routine, and to build the main functionality of the application into its idle handler. Since the idle handler is called by the system at intervals, it can easily be coded to do simple, unchanging tasks at every call, such as beeping periodically forever. A more useful application must perform one or more tests in the idle handler to ascertain what to do every time the system calls it. For example, using variables that track the current state of the application, the idle handler can perform a series of tasks in turn, waiting until each task is completed before performing the next. After the final task is completed, the stay-open application can quit, or it can repeat the whole cycle indefinitely.

In effect, the system’s periodic calls to the idle handler can function as a traditional Macintosh event loop. In older versions of the Mac OS, the minimum interval between system calls to the idle handler was one full second, making it impractical to use this technique for applications that require quick responsiveness. The minimum interval changed in Mac OS X 10.3 (Panther), when intervals expressed as real numbers were first allowed. Now, for example, you can trigger the idle handler every tenth of a second, or even more frequently.)

A common mistake is to code the idle handler as a repeat loop. A repeat loop is not needed, because the system itself calls the idle handler repeatedly.

Here are some strategies for using the idle handler:

  1. To repeat a fixed routine at regular intervals, simply code it in the idle handler and set the desired repetition interval. The idle handler will automatically start firing as soon as the run handler terminates, usually after running some initialization routines written into the run handler, and it will continue to fire periodically until the user quits the application. A common example is an “alarm clock” script that checks the current time periodically, watching for the alarm time, then beeps or plays an iTunes song when the set time arrives. This example is a very slow “metronome,” beeping once every five seconds.

global numberOfBeeps

on idle
   beep numberOfBeeps
   return 5
end idle

on run
   set numberOfBeeps to 1
end run

  1. To pause and resume idle-time processing, set and reset a Boolean flag or a counter variable under appropriate conditions. The idle handler should first test the flag, then its idle-time code will either lie dormant or run depending on the state of the flag. The following example emits two quick beeps while counting down from 3 to 1 at five-second intervals, but it skips the second cycle.

global numberOfBeeps, beepCycle

on idle
   if beepCycle is greater than 0 then
      if beepCycle is not 2 then beep numberOfBeeps --skip second cycle
      set beepCycle to beepCycle - 1
   end if
   return 5
end idle

on run
   set numberOfBeeps to 2
   set beepCycle to 3
end run

An alternative technique is to set the idle interval to a very long time – effectively forever – as soon as idle handling is to be paused. Then the idle handler will not even be called by the system until other code – perhaps an external AppleScript application – explicitly calls the idle handler again, at which time the idle interval can be reset to a shorter time to “unpause” it.

  1. To stop the idle handler from running altogether, use one of several techniques: (1) quit immediately from within the idle handler or a handler that is called from the idle handler; (2) code a flag in the idle handler that causes it to do nothing thereafter; or (3) set the idle interval to a very long time, such as a day or a week (if the computer is not left running that long). The next example modifies the previous example by quitting once the countdown ends.

global numberOfBeeps, beepCycle

on idle
   if beepCycle is greater than 0 then
      if beepCycle is not 2 then beep numberOfBeeps --skip second cycle
      set beepCycle to beepCycle - 1
   else
      quit
   end if
   return 5
end idle

on run
   set numberOfBeeps to 2
   set beepCycle to 3
end run

  1. To call the idle handler off-cycle in the middle of a script’s logical flow, put the initialization routines and the first part of the program in the run handler, then call the idle handler explicitly. Place test routines in the idle handler and, when the desired condition arises, run the rest of the program’s logic from the idle handler until done.

global numberOfBeeps, beepCycle

on goodbye()
   quit
end goodbye

on idle
   if beepCycle is greater than 0 then
      if beepCycle is not 2 then beep numberOfBeeps --skip second cycle
      set beepCycle to beepCycle - 1
   else
      goodbye()
   end if
   return 5
end idle

on run
   set numberOfBeeps to 2
   set beepCycle to 4
   idle
   --more code here, if needed
end run

  1. It may sometimes be convenient to call a script application’s run handler from the idle handler. The run handler would first run its initialization code, then the idle handler would automatically be called by the system (or you could call it explicitly). The idle handler would simply call the run handler again. A flag must be used to tell the run handler to skip the initialization code after it is run the first time (this works only if the flag is declared as a pre-initialized property; don’t forget to reset it again in a quit handler if this is needed to ensure that the application will function correctly the next time it is launched).

property initialized : false

on idle
   run
   return 3
end idle

on run
   if initialized then
      beep
   else
      set initialized to true
   end if
end run

on quit
   set initialized to false
   continue quit
end quit

  1. An efficient way to code a “wait loop” in AppleScript is to use the idle handler. This is useful, for example, when launching an application that takes a while to become ready to handle Apple events, or when opening a PPP connection via modem that takes some time to open. The CPU is released to run other processes between calls to the idle handler. Even if the interval between calls to the idle handler is as short as one second, the CPU can accomplish a lot.

A common mistake is to overlook the idle handler and instead code a simple repeat loop that counts to a fixed number, testing for the desired condition at each iteration. The CPU then spends most of its time counting and testing, and the other processes will take much longer to complete – some, such as modem connections, may not work at all because of sensitive timing requirements. The speedup in other processes that is accomplished by using the idle handler to construct a wait loop can be astonishing.

In recent versions of Mac OS X, you can use a repeat loop instead of an idle handler if you include a delay command in the repeat loop. A statement like delay 1 gives the CPU plenty of time to do other work.


on idle
   tell application "Finder"
      if process "TextEdit" exists then
         beep
         activate application "TextEdit"
         quit me
      end if
   end tell
   return 1
end idle

on run
   launch application "TextEdit"
end run

  1. To set up a more complex application, code a series of if/else if/else tests in the idle handler to select among multiple triggering conditions. This strategy can be used for a progressive process that proceeds in stages depending upon completion of preceding stages and ending with a quit command. Alternatively, it can be used for an unordered, continuous “event loop” that runs different routines when different events are detected. An example of a progressive process is an internet connection script that tells Internet Connect to connect and waits for the connection to succeed, then launches Mail and waits for it to start running, then tells Mail to get the mail and waits until all the mail is retrieved, then processes the mail and tells itself to quit. This example cycles through three stages, then quits itself.

global stage1, stage2

on idle
   if stage1 then
      beep 1
      --place stage 1 code here
      set stage1 to false
      set stage2 to true
   else if stage2 then
      beep 2
      --place stage 2 code here
      set stage2 to false
   else
      beep 3
      quit
   end if
   return 3
end idle

on run
   set stage1 to true
end run

  1. A particularly powerful technique is to assign various script objects in turn to a single variable. The idle handler can simply run the variable, which will cause the script object that is currently assigned to the variable to run. The identity of the currently-installed script object can be tested to help control the substitution of new script objects in stages. This example cycles through the three stage objects endlessly until you quit it.

global theScript

script Stage1
   beep 1
end script

script Stage2
   beep 2
end script

script Stage3
   beep 3
end script

on idle
   run script theScript
   if theScript is Stage1 then
      set theScript to Stage2
   else if theScript is Stage2 then
      set theScript to Stage3
   else
      set theScript to Stage1
   end if
   return 3
end idle

on run
   set theScript to Stage1
end run