Progress Bar, Desktop, and Split/Pause Timers and Tests of Accuracy.

Progress Bar, Desktop, and Split/Pause Timers and Tests of Accuracy.

I apologize for the length of this post. It is primarily intended for members who have an interest in AppleScript timers, assessments of their accuracy, and strategies for eliminating timer error. For members only interested in the Progress Bar Timer I developed to step the time in 1-second increments and which corrects for timer lag, scroll down to (or do a text search for) the following line:

:alarm_clock: A lag-adjusted progress bar timer.

Even if you have no interest in tests of timer accuracy or an overview of Progress Bar, Desktop, and Split/Pause timers, you might want to scan the animated gif images scattered throughout the post to see examples of AppleScript timers. Or simply click this link to view the graphics used in the post: https://tinyurl.com/MacScripter-graphics

Since I was unaware that the width of the preview field shown when creating a post is wider than that of the post after submitting it, three of my jpg images are truncated on the right side. I note this below the image and suggest a solution: drag the image to the desktop and use QuickLook or Preview to view the entire image. Alternatively, control-click the image and open it in a new tab.

Nine topics are covered below, each marked by a “➤” preceding the topic heading:

  1. Timing with single-line vs repeat-loop timers.
  2. Accuracy of single-line and repeat-loop timers.
  3. Progress bar timers.
  4. A lag-adjusted progress bar timer.
  5. Desktop timers.
  6. Creating desktop timer folders with a custom icon.
  7. Split/Pause timers.
  8. Assessments of CPU usage by the timers.
  9. Comparative strengths of the timers.

A note on convention: in this post code segments that cannot execute as stand-alone AppleScripts when copied to the Script Editor are set off in bold, whereas executable scripts are set off in standard AppleScript format, as in this example:

-- a simple script to countdown from 3 to 0
say "Counting down from"
repeat with i from 0 to 3
	say 3 - i -- countdown from 3 to 0
end repeat

If a script requires an external subroutine library, this is noted where applicable.

Timing with single-line vs repeat-loop code.

Depending on the specific need, timer routines usually employ one of two strategies. To simply have the script pause for a specified period of time, the following single-line statement can be used:

delay delaySeconds

Assuming delaySeconds has been assigned a value, say 10 seconds, this statement will pause script execution for that many seconds. If it is necessary to perform a specific action at particular intervals, then a repeat-loop is generally used:

repeat with i from 1 to delaySeconds
delay 1
< action(s) to be performed each second >
end repeat

This loop executes the required action every second for delaySeconds. For timer scripts, the action is usually to display the elapsed time, either in seconds or in HMS (hh:mm:ss) format. The repeat loop is easily modified to step at larger intervals where that is desired.

Accuracy of single-line and repeat-loop timers.

For either the single-line or repeat-loop timer, we can ask just how accurate the timer is over varying lengths of time. If the timer is completely accurate, the elapsed time of the timer should be delaySeconds. For repeat-loop timers, this assumes that the action(s) performed in each iteration of the loop require less than one second (or whatever the delay interval is) to execute.

If there is error then the actual elapsed time will differ to some extent from the nominal elapsed time of delaySeconds. To get the actual elapsed time for the single-line and repeat-loop examples above, the code can be modified as follows:

set startTime to (current date)
delay delaySeconds
set endTime to (current date)

and

set startTime to (current date)
repeat with i from 1 to delaySeconds
delay 1
end repeat
set endTime to (current date)

In either case, comparing delaySeconds to (endTime - startTime) provides an index of the error in timing, if any. To this end, I wrote a script to determine the actual elapsed times for nominal times of 0 to 10 hours in duration. Not knowing the specifics of how AppleScript implements the timing for the delay command, I had no intuition as to the how much error might arise, except for this: it seemed likely that the repeat-loop error would be greater than the single-line error simply because the opportunity for cumulative error over delaySeconds executions of delay 1 is greater than that which would arise from one execution of delay delaySeconds.

Here is the script for a repeat-loop timing test of 1 hour duration:

-- script for testing timer accuracy: actual vs nominal time
set HMS_Time to "01:00:00" -- for testing the actual vs nominal time of 1 hr
set delaySeconds to getTimeConversion(HMS_Time)
set startTime to (current date)
repeat with i from 1 to delaySeconds
	delay 1
end repeat
set endTime to (current date)
set HMS_Elapsed to TimetoText(endTime - startTime)
set dialogText to "Elapsed Time: " & return & ¬
	"Nominal	 = " & HMS_Time & return & ¬
	"Actual	 = " & HMS_Elapsed
tell me to activate
display dialog dialogText with title "Timer Result"
return dialogText

-- getTimeConversion converts a time in HMS format (hh:mm:ss) to a time in seconds
on getTimeConversion(HMS_Time)
	set HMSlist to the words of HMS_Time -- get {hh, mm, ss} from HMS time (p. 158 Rosenthal)
	set theHours to item 1 of HMSlist
	set theMinutes to item 2 of HMSlist
	set theSeconds to item 3 of HMSlist
	return round (theHours * (60 ^ 2) + theMinutes * 60 + theSeconds)
end getTimeConversion

-- TimetoText converts a time in seconds to a time in HMS format (hh:mm:ss)
on TimetoText(theTime)
	-- parameters - TheTime [integer]: the time in seconds
	-- returns [text]: the time in the format hh:mm:ss
	-- Nigel Garvey points out this script is only valid for parameter values up to 86399 seconds (23:59:59) and offers a solution for longer times here: https://macscripter.net/viewtopic.php?pid=134656#p134656 
	if (class of theTime) as text is "integer" then
		set TimeString to 1000000 + 10000 * (theTime mod days div hours) -- hours
		set TimeString to TimeString + 100 * (theTime mod hours div minutes) -- minutes
		set TimeString to (TimeString + (theTime mod minutes)) as text -- seconds
		tell TimeString to set theTime to (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
	end if
	return theTime
end TimetoText

The single-line timer script is exactly the same except that the repeat-loop block is replaced by delay delaySeconds. The durations tested were 0, 1, 2, 3, 4, 5, 6, and 10 hrs. If you wish to do a quick 10-second run of the script, change HMS_Time to “00:00:10” in the first line of the script. Note that the getTimeConversion and TimetoText subroutines convert HMS times to time-in-seconds and time-in-seconds to HMS times, respectively. (Timing is done in seconds, but the input and display of times are done in HMS format.)

The results for the single-line timer can be summarized quickly: there was no difference between actual and nominal time for any duration tested. The results for the repeat-loop timer were another matter, as shown in the table below:

The difference between the actual and nominal times increases in direct proportion to duration: the “lag” grows from 4 seconds after 1 hour to 35 seconds after 10 hours. Over this range, the average lag in seconds is about 6% of the nominal time in seconds, which is a surprisingly large amount of error for a timer. Note that the 0 hour interval is not included in this average, because there can’t be a difference between the single-line and repeat-loop timers in this case. The 0 hr duration is desired however for the regression analysis summarized in the bottom of the table and plotted in the graph below:

These results show that the line of best fit, Y’ = 3.44X + .42, provides an accurate description of the relationship between timer lag and timer duration, as the value of r-square (.996) is very nearly 1. The slope of the function indicates that the repeat-loop timer lags 3.44 seconds behind actual time for every hour that passes.

Progress bar timers.

The timer test results indicate that if you want to have a timer script that uses a repeat-loop strategy, then you must find a way to eliminate the timer lag inherent in it. I first encountered this problem in one of my AppleScripts that used a progress bar to show the passage of time visually and to display the countup and countdown times dynamically. This is a brief example of it timing for 10-seconds:

You can see that the timer steps the progress bar one second each time through the repeat loop for a total of 10 seconds. Synchronous with the stepping, the countup time increments from 0 to 10 and the countdown time decrements from 10 to 0. Hence the progress bar timer functions as both a countup and a countdown timer. Since countdown timing is for a pre-determined duration, one simply sets the time to the precise duration required. For countup timing, on the other hand, the time would be set to some arbitrarily long value (e.g., the maximum possible value of 23:59:59), the timer started, and when some external event or process finishes one simply notes the countup time on the progress bar at the moment of completion, and then closes the timer by clicking the Stop button.

Progress bars normally disappear on script termination, but I elected to display a dialog at the end of the script in order to keep the progress bar on the screen. For purposes of debugging I had the dialog display the actual and nominal times as well, and that is when I discovered they didn’t always agree. After verifying the discrepancies were due to timer lag rather than programming error, I set about conducting the timing tests reviewed above to get a better handle on what was going on. Having quantified the lag-induced timer error, the next step was to find some way to remove it.

Lag-adjusted progress bar timers.

Interestingly, the very code I used to keep track of the timer lag suggested the solution. If I monitored the actual elapsed time each time through the loop, I could compute a lag adjustment to subtract from the delay time for that cycle of the loop:

set startTime to (current date)
set lagAdjust to 0
repeat with i from 0 to delaySeconds – begin at i = 0 to start the timer at 00:00:00
set elapsedTime to (current date) - startTime – get actual elapsed time for adjusting delay
set lagAdjust to elapsedTime - i – compute lag adjustment
delay 1 - lagAdjust – delay 1 second minus any cumulative lag that needs removing
end repeat

Whenever the elapsed time and the loop index agree, the lag adjustment (lagAdjust) is elapsedTime - i = 0 and no adjustment in the delay of 1 second is applied. But if timer lag has caused i (the nominal elapsed time) to fall behind the actual elapsed time, then the lag adjustment will be greater than 0 and some adjustment is applied. Subtracting the lag adjustment from the 1 second delay serves to resynchronize the actual and nominal time, and if it doesn’t, the process continues in subsequent iterations until it does. This approach makes the timer self-correcting “on the fly.”

Does the correction work? Recall that for the unadjusted repeat-loop timer tested above, nominal time lagged 35 seconds behind actual time after 10 hours. Here are the results for the lag-adjusted timer at the conclusion of 10-hours:

The nominal and actual elapsed times now agree, and the lag-adjusted repeat-loop timer is just as accurate as the single-line timer we discussed at the very beginning. An example of the full script for a lag-adjusted progress bar timer is:

:alarm_clock: A lag-adjusted progress bar timer.

-- Progress Bar Coffee Timer for boiling water to make coffee

progress_timer("00:03:20", "Coffee") -- call the progress timer with an HMS time and timer label
return result

------------------------------------------
-- subroutines in alphabetical order --
------------------------------------------

-- getTimeConversion converts a time in HMS format (hh:mm:ss) to a time in seconds
on getTimeConversion(HMS_Time)
	set HMSlist to the words of HMS_Time -- get {hh, mm, ss} from HMS time (p. 158 Rosenthal)
	set theHours to item 1 of HMSlist
	set theMinutes to item 2 of HMSlist
	set theSeconds to item 3 of HMSlist
	return round (theHours * (60 ^ 2) + theMinutes * 60 + theSeconds)
end getTimeConversion

-- progress_timer displays the elapsed time in a progress bar. For information on progress bars see: https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/DisplayProgress.html
on progress_timer(HMS_Time, timerLabel)
	set theTimeSec to getTimeConversion(HMS_Time) -- convert the HMS format to seconds
	set progress total steps to theTimeSec
	set progress completed steps to 0
	set progress description to "Timing for " & HMS_Time
	set startTime to (current date)
	repeat with i from 0 to theTimeSec -- begin at i = 0 to start the timer at 00:00:00
		set HMS_SoFar to TimetoText(i) -- convert the seconds so far to HMS format for display
		set HMS_ToGo to TimetoText(theTimeSec - i) -- convert the seconds to go to HMS format for display
		set progress additional description to ¬
			"Counting Up:		" & HMS_SoFar & return & ¬
			"Counting Down:		" & HMS_ToGo
		set progress completed steps to i
		set elapsedTime to (current date) - startTime -- get actual elapsed time for adjusting delay
		set lagAdjust to elapsedTime - i -- compute lag adjustment
		delay 1 - lagAdjust -- delay 1 second minus any cumulative lag that needs removing
	end repeat
	say timerLabel & " time is up"
	set HMS_Elapsed to TimetoText(elapsedTime) -- convert elapsedTime back to HMS format for display
	set dialogText to "Elapsed Time: " & return & ¬
		"Nominal	 = " & HMS_Time & return & ¬
		"Actual	 = " & HMS_Elapsed
	tell me to activate
	display dialog dialogText with title timerLabel & " Timer"
	return dialogText
end progress_timer

-- TimetoText converts a time in seconds to a time in HMS format (hh:mm:ss)
on TimetoText(theTime)
	-- parameters - TheTime [integer]: the time in seconds
	-- returns [text]: the time in the format hh:mm:ss
	-- Nigel Garvey points out this script is only valid for parameter values up to 86399 seconds (23:59:59) and offers a solution for longer times here: https://macscripter.net/viewtopic.php?pid=134656#p134656 
	if (class of theTime) as text is "integer" then
		set TimeString to 1000000 + 10000 * (theTime mod days div hours) -- hours
		set TimeString to TimeString + 100 * (theTime mod hours div minutes) -- minutes
		set TimeString to (TimeString + (theTime mod minutes)) as text -- seconds
		tell TimeString to set theTime to (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
	end if
	return theTime
end TimetoText

This specific application is for timing how long it takes to boil water for making coffee, but the code is identical to the 10-hr test shown above, except that the HMS time and timer label passed to subroutine progress_timer are “00:03:20” and “Coffee” rather than “10:00:00” and “10-hr”. The getTimeConversion and TimetoText subroutines do the usual conversions between HMS time and time in seconds.

Here is what the coffee progress bar timer looks like stepping up through the 10 seconds from 00:01:00 to 00:01:10 (the countup time) and simultaneously stepping down from 00:02:10 to 00:01:10 (the countdown time):

Since the overall duration (3:20) is larger than for the 10-second timer, the step size is smaller. For long durations, as in the 10-hr timing test reported above, the movement of the progress bar at each step is barely noticeable, as shown in the progress bar of this graphic discussed below: https://tinyurl.com/Desktop-Coffee-Freezer-Timers. Nevertheless, the countup and countdown times shown in the progress bar “tick” in synchrony with the system clock of your menu bar, second by second. As far as I can determine, the only exception to this would be the occasional skipping of a countup/countdown 1-second update caused by the need to adjust for lag in the timer: e.g., lag might cause an HMS time of “02:45:03” to jump to “02:45:05” rather than “02:45:04” in order to synchronize nominal and actual elapsed time. The regression results reviewed earlier showed a lag of 3.44 seconds per hour, so we would expect 3 or 4 such “jumps” per hour (or per 3600 seconds). This translates into a lag correction of roughly 1 in every 1000 updates.

You may wonder why I don’t have the script input the HMS time from the user at runtime, and do the timing based on that. This would make the timer more versatile by not restricting it to a specific timer application, as the coffee timer does. Originally I did just that, but I quickly tired of dealing with the additional step of selecting a timer preset (or entering a custom time) each time I ran the script. Also, I wanted to be able to time several different events simultaneously and this requires independent script applications. The solution is simple. Create a very simple script customized for the specific timer application, and move all the supporting subroutines to an external script stored in the scripts folder. The specialized script can then call the timer subroutines without having to store them in the script itself. To do that, follow these steps:

  1. Open a new script window (⌘-n) in the Script Editor.
  2. Copy the three subroutines in the script above to the script window, compile it, and save the file as “Utility.scpt” to your Scripts Folder.
  3. Use the sample script below to create your custom timer script. Simply edit the HMS time and timer label as needed, and save the script with an appropriate name: for the progress bar coffee timer it was saved as “Progress Coffee Timer.scpt”.
  4. Finally, export a version with a file format of Application and an option of Run-only, and save it in your Scripts Folder. Saving the script as an application is important, because the progress bar only displays when the script is run as an application.

Each timer script you create will have a very short calling routine that specifies a time and a label, and by giving each script and application its own name, you can run as many at once as you like. The abbreviated coffee timer script which uses the external script library Utility.scpt is:

------------------------------------------------------------------------------------------------------------------------
-- Sample script if the subroutines are stored in the scripts folder in an external script called "Utility.scpt" --
------------------------------------------------------------------------------------------------------------------------

-- Progress Bar Coffee Timer for boiling water to make coffee

-- load the external subroutine library Utility
set pathname to ((path to scripts folder as text)) & "Utility.scpt" -- get pathname to Utility
set Utility to (load script file pathname) -- load Utility

-- call the progress timer with an HMS time and timer label
tell Utility to progress_timer("00:03:20", "Coffee")
return result

The two SET commands in the script define (1) the path to the external subroutine library “Utility.Scpt” so the needed subroutines can be accessed when the script is compiled, and (2) a variable called “Utility” which points to the subroutine library when invoked in a tell statement. In this case, the tell statement is:

tell Utility to progress_timer(“00:03:20”, “Coffee”)

This statement executes the subroutine progress_timer in script library “Utility.scpt” using parameter values of “00:03:20” for the time and “Coffee” for the label. The original coffee timer script now reduces to a single line (excluding the Utility SET and the return statements, which are the same in every script). Once you discover the power of external subroutine scripts, you will create your own to add to the library. I now have 75 in my Utility.scpt file for executing various tasks in the Finder, Safari, iTunes, and for text editing, timing, and selecting menu items for applications.

If you have the Script Editor configured to show in your menu bar, then any of the script applications you create will be available for execution by simply clicking on the Script Editor icon and selecting the specific timer application to run, as illustrated here:

The menu bar selection executes the abbreviated coffee timer application. (By default, the progress bar timer is displayed in the middle of the screen, so I had to drag it to area being recorded for it to be visible.) The menu bar drop-down shows some other single-line timer scripts I have created:

tell Utility to progress_timer(“00:45:00”, “Laundry”) — times when the washing machine is done
tell Utility to progress_timer(“01:00:00”, “Dryer”) — times when the dryer is done
tell Utility to progress_timer(“02:00:00”, “Cleaning”) — monitors the time left for the cleaning lady to complete tasks
tell Utility to progress_timer(“10:00:00”, “10-hr”) — for conducting a 10-hr timer test of the lag-adjusted timer
tell Utility to progress_timer_halfway(“01:30:00”, “Workout”) — times when a workout session is done
tell Utility to progress_timer_halfway(“02:30:00”, “Freezer”) — times when an item chilling in the freezer is done

The two calls to progress_timer_halfway reference a modified progress timer which provides an auditory alert at the halfway point of the time interval, useful for checking that things are on pace (when freezing something, working out, etc).

I have one general purpose timer which has preset times (like those above) and a custom time option where the user specifies the HMS time for the timer. So if you don’t like creating multiple short scripts, as above, it might be preferable. The call is:

tell Utility to progress_timer(getChoice(), “Choice”) – start the timer with the user’s time choice

Invoking getChoice in the progress_timer call produces the following dialog:


[image truncated: drag to desktop and use QuickLook or Preview to see entire image]

The presets are shown on the left, and the dialog provided by selecting “Custom Time” is shown on the right. The preset or custom time chosen by the user is then passed to progress_timer and the progress bar timer starts as usual. (It is easy to add additional presets to the selection provided, and/or to delete any of those present.) This animated gif shows a user choosing the Custom Time default option of 5-seconds (note that the “jumpy” appearance of the expanding windows is an artifact of the video to gif conversion):

For the getChoice and the progress_timer_halfway calls, you would need to add those subroutines to the Utility.scpt external subroutine library, as well as any subroutines they call in my Utility.scpt file, and any routines those routines call, etc. Rather than sort all that out, I would just sent my entire Utility.scpt file to anyone requesting it.

Desktop timers.

Another example of an AppleScript timer is the desktop folder timer. The idea comes from a kitchen timer script on pp. 108-109 of Hanaan Rosenthal’s book AppleScript (2006). To get around the limitations of AppleScript, which make it displaying a dialog with a dynamically changing time value difficult, Rosenthal adopted a clever strategy: have the script create a folder on the desktop, and in the timer’s repeat loop tell the Finder to change the folder name every second so as to correspond to the current time value. The timer folder is created with this statement:

tell application “Finder” to set folder_alias to (make new folder at desktop with properties {name:displayTime}) as alias

The displayTime is the initial name of the folder. For a countup timer this would be “00hrs 00min 00sec” but for a countdown timer it would be the countdown duration: e.g. “00hrs 03min 20sec” for the coffee timer. Note that the standard HMS format of “00:00:00” or "“00:03:20"” can’t be employed for a folder name, because the colon is reserved by the OS.

Updating the folder name is accomplished by this statement contained in the timer repeat loop:

tell application “Finder” to set name of folder_alias to displayTime

where displayTime is a string containing the HMS time for iteration i.

Rosenthal’s script wasn’t useful to me in the form presented, so I created my own using just the basic idea of communicating a time value via a folder name. Here is what my desktop folder timer looks like for the 2:38-2:28 portion of the coffee countdown timer, which updates every 2 seconds for reasons explained below:

Since there is no way to tell what the timer is timing, I modified my script to use an icon for the folder that makes it obvious. Here is what the desktop folders look like for a coffee countdown timer and a freezer countup timer, along with a 10-hr progress bar timer for comparison:

Note that the three different timers are running simultaneously, and that the folder timers update every 2-seconds whereas the progress bar timer updates every second. The 2-second interval for the desktop timers is necessary because 1-second intervals produce erratic updates for the folder names whenever the OS can’t finish changing the name of one folder before it is required to update the next. Depending on system load, the desktop timers may skip 1 or more updates or the updates may come closely paired in time, with gaps in between, rather than being equally spaced. The progress bar timer, on the other hand, “ticks” accurately regardless of the number of timers running. That is the reason for my preference for the progress bar solution. However, I do like the fact that the desktop folder takes up less real estate than the progress bar timer. In addition, using transparent png images for the folder icons allows even more of the desktop background to show, making the desktop timer less “intrusive” than the other timers reviewed in this post.

The script for the coffee desktop timer is:

-- Desktop Countdown Timer for timing when coffee water is about to boil

countdown_desktop_timer("00:03:20", "Coffee")
return result

------------------------------------------
-- subroutines in alphabetical order --
------------------------------------------

-- converts a path to an alias
on convertPathToAlias(thePath)
	-- source: "Converting a Path to an Aliases", Listing 15-9:
	-- https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/ReferenceFilesandFolders.html
	tell application "System Events"
		try
			return (path of disk item (thePath as string)) as alias
		on error
			return (path of disk item (path of thePath) as string) as alias
		end try
	end tell
end convertPathToAlias

--- deletes an old countdown file so the present countdown ends on "00:00:00" rather than 00:00:02 (to avoid overwriting)
on countdown_delete_ifExists()
	set theItem to (path to desktop) & "00hrs 00min 00sec" as string -- name of leftover countdown timer
	tell application "Finder"
		if (exists theItem) then
			delete theItem -- delete it so the current timer stops at 00:00:00, not 00:00:02 (to avoid overwriting)
		end if
	end tell
end countdown_delete_ifExists

-- this is a desktop countdown timer with a preset time and custom icon
on countdown_desktop_timer(HMS_Time, timerLabel)
	
	-- Desktop Countdown Timer that displays the elapsed time in a folder on the desktop in hh:mm:ss format
	-- source: inspired by a kitchen timer script on p. 108 of Applescript (2006), by Hanaan Rosenthal
	-- Note: Rosenthal's code was subject to timer lags developing over time - the following code avoids that
	
	-- ⚠️ to terminate this script in the Script Editor simply press ⌘. or click the stop button 
	-- to quit from the runtime application, click on the timer folder and command-DELETE to put it in the trash,
	-- or do a forced quit using the Activity Monitor (search for "timer")
	
	-- if an old countdown file exists, delete it so this countdown ends on "00:00:00" not 00:00:02 (to avoid overwriting)
	countdown_delete_ifExists()
	
	-- get the HMS countdown time to use as the initial timer folder name
	set HMSlist to the words of HMS_Time -- get the list {hh, mm, ss} from the time tokens from HMS time
	set displayTime to getDisplayTime(HMSlist) -- get the initial folder name for the timer folder (hrs min secs)
	
	-- copy an iconized timer folder to desktop if one exists, or a standard OS folder if not; return the alias of the created folder
	create_desktopFolder_and_getAlias(displayTime, timerLabel)
	set folder_alias to result
	
	-- for timing, we need to convert the HMS time to a time in seconds, and store it in timeLeft; we also set the delay time between updates (secDelay)
	set secDelay to 2 -- this is the delay in seconds between folder name updates 
	set startTime to (current date)
	set theTimesec to getTimeConversion(HMS_Time) -- convert the hh:mm:ss countdown time to time in seconds
	set timeLeft to theTimesec -- initialize timeLeft: theTimesec wil remain constant and timeLeft will decrement by secDelay with each repeat until it is ≤0
	
	-- this repeat loop executes the countdown timing, delaying secDelay each time through, updating the timeLeft accordingly, having the Finder update the timer folder name, checking for a "put-in-trash" operation to quit the timer immediately, and trapping various errors which may arise in the attempt to do so 
	repeat while timeLeft > 0
		set currentTime to (current date)
		set timeLeft to theTimesec - (currentTime - startTime) -- get the time remaining in the count down
		set HMSTime to TimetoText(timeLeft) -- convert timeleft to hh:mm:ss format for display
		set HMSlist to the words of HMSTime -- get {hh, mm, ss} from HMS time (p. 158 Rosenthal)
		set displayTime to getDisplayTime(HMSlist) -- update the timer folder name to the current time left
		if item 1 of HMSlist = "99" then set displayTime to "00hrs 00min 00sec" -- wrapped past 0 on odd sec end
		-- try to update the timer folder name, and if an error occurs, process it
		try
			tell application "Finder" to set name of folder_alias to displayTime -- update the timer
		on error errorMessage number errorNum
			if errorNum = -48 then -- the folder exists from a previous countup timer run
				-- so skip this timer folder name update and continue with the execution of the script
			else
				display dialog "Error in script Desktop Countdown Timer: " & errorMessage & return & errorNum
			end if
		end try
		-- check to see if timer folder has been put in the trash (command-delete), and if so quit
		if (offset of "Trash" in the (POSIX path of (folder_alias) as text)) > 0 then
			return "Application quit at:  Countdown Time = " & displayTime
		end if
		delay secDelay
	end repeat
	
	-- when the countdown timer reaches 00:00:00, provide a verbal notification and stop
	say timerLabel & "time is up"
	return "Timer stopped at:  " & displayTime
end countdown_desktop_timer

-- this is a desktop countup timer with a custom icon
on countup_desktop_timer(timerLabel)
	
	-- Desktop Countup Timer that displays the elapsed time in a folder on the desktop in hh:mm:ss format
	-- source: inspired by a kitchen timer script on p. 108 of Applescript (2006), by Hanaan Rosenthal
	-- Note: Rosenthal's code was subject to timer lags developing over time - the following code avoids that
	
	-- ⚠️ to terminate this script in the Script Editor simply press ⌘. or click the stop button 
	-- to quit from the runtime application, click on the timer folder and command-DELETE to put it in the trash,
	-- or do a forced quit using the Activity Monitor (search for "timer")
	
	-- if an old countdown file exists, delete it
	countdown_delete_ifExists()
	
	-- copy an iconized timer folder to desktop if one exists, or a standard OS folder if not; return the alias of the created folder
	create_desktopFolder_and_getAlias("00hrs 00min 00sec", timerLabel)
	set folder_alias to result
	
	set secDelay to 2 -- this is the delay in seconds between folder name/time updates
	set startTime to (current date)
	
	-- this repeat loop executes the countup timing, delaying secDelay each time through, updating the countupTime accordingly, having the Finder update the timer folder name, checking for a "put-in-trash" operation to quit the timer immediately, and trapping various errors which may arise in the attempt to do so 
	repeat
		set currentTime to (current date)
		set countupTime to currentTime - startTime -- get the time since app started running
		-- get and display the countup folder name/time
		set HMSTime to TimetoText(countupTime) -- convert countup time to hh:mm:ss
		set HMSlist to the words of HMSTime -- get {hh, mm, ss} from HMS time (p. 158 Rosenthal)
		set displayTime to getDisplayTime(HMSlist) -- get the folder name for the current elapsed time
		try
			tell application "Finder" to set name of folder_alias to displayTime -- update the timer folder name
		on error errorMessage number errorNum
			if errorNum = -48 then -- the folder exists from a previous countup timer run
				-- so skip this timer folder name update and continue with the execution of the script
			else
				display dialog "Error in script Desktop Countup Timer: " & errorMessage & return & errorNum
			end if
		end try
		if (offset of "Trash" in the (POSIX path of (folder_alias) as text)) > 0 then
			return "Application quit at:  Elapsed Time = " & displayTime -- quit if timer folder is in trash
		end if
		delay secDelay
	end repeat
	
end countup_desktop_timer

-- check if an iconized folder exists, and if so copy it from the Script Resouces to the desktop, otherwise create a OS standard folder; return an alias for the created folder
on create_desktopFolder_and_getAlias(displayTime, timerLabel)
	set pathString to (path to scripts folder) & "Script Resources:" & timerLabel as string
	set flagExists to itemExists(pathString)
	if flagExists then -- an iconized folder exists so copy it to the desktop and get an alias for it
		set pathAlias to alias (pathString)
		tell application "Finder" to copy folder pathAlias to the desktop
		tell application "Finder" to set folder_path to ((path to desktop) & timerLabel as string) -- get path to desktop folder
		set folder_alias to convertPathToAlias(folder_path)
	else -- an iconized folder doesn't exist so make a standard folder on the desktop and get an alias for it
		tell application "Finder" to set folder_alias to (make new folder at desktop with properties {name:displayTime}) as alias
	end if
	return folder_alias
end create_desktopFolder_and_getAlias

-- format HMS format for displaying as a foldername that doesn't include the ":" delimiter
on getDisplayTime(HMSlist)
	set hrsText to item 1 of HMSlist
	set minText to item 2 of HMSlist
	set secText to item 3 of HMSlist
	return hrsText & "hrs " & minText & "min " & secText & "sec"
end getDisplayTime

-- getTimeConversion converts a time in HMS format (hh:mm:ss) to a time in seconds
on getTimeConversion(HMS_Time)
	set HMSlist to the words of HMS_Time -- get {hh, mm, ss} from HMS time (p. 158 Rosenthal)
	set theHours to item 1 of HMSlist
	set theMinutes to item 2 of HMSlist
	set theSeconds to item 3 of HMSlist
	return round (theHours * (60 ^ 2) + theMinutes * 60 + theSeconds)
end getTimeConversion

-- checks to see if the file/folder exists
on itemExists(theItem)
	tell application "Finder"
		if (exists theItem) then
			return true
		else
			return false
		end if
	end tell
end itemExists

-- TimetoText converts a time in seconds to a time in HMS format (hh:mm:ss)
on TimetoText(theTime)
	-- parameters - TheTime [integer]: the time in seconds
	-- returns [text]: the time in the format hh:mm:ss
	-- Nigel Garvey points out this script is only valid for parameter values up to 86399 seconds (23:59:59) and offers a solution for longer times here: https://macscripter.net/viewtopic.php?pid=134656#p134656 
	if (class of theTime) as text is "integer" then
		set TimeString to 1000000 + 10000 * (theTime mod days div hours) -- hours
		set TimeString to TimeString + 100 * (theTime mod hours div minutes) -- minutes
		set TimeString to (TimeString + (theTime mod minutes)) as text -- seconds
		tell TimeString to set theTime to (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
	end if
	return theTime
end TimetoText

The heart of this script is the countdown_desktop_timer subroutine, which manages the countdown timing. To adapt the script for some other use, just change the values passed for the countdown time (HMS_Time) and label (timerLabel). For example,

countdown_desktop_timer(“02:30:00”, “Freezer”)

would be used to countdown for 2.5 hrs for an item in the freezer.

In the subroutines listed in the desktop coffee timer script I include one named countup_desktop_timer. It is not called by the coffee timer, but I include it so you can create countup desktop timer scripts. To time how long an item has been chilling in the freezer, use this call:

countup_desktop_timer(“Freezer”)

Note that since the countup timer runs until you put it in the trash, there is no need for a countdown time (HMS_Time) to be provided in the call. Both the countdown and countup scripts for “Freezer” use the same icon, but you can distinguish which is which by simply noting how the HMS times are changing in the folder names (going down for the countdown timer, and up for the countup timer).

I have one other routine that is a combination countup/countdown timer (not included above) which uses this call:

countup_countdown_desktop_timer(“02:30:00”, “Freezer”)

The parameter values passed are the same as for the countdown timer. However, the timer differs in that it counts up rather than down, but it provides an auditory alert at the countdown time of 2.5 hrs, and continues countup timing after the alert. This is useful if you happen to miss the auditory alert and want to know by how much you have overshot the countdown time.

As with the progress bar timers, you can copy the required subroutines to the external script file “Utility.scpt” stored in your Scripts Folder, and the abbreviated desktop coffee timer becomes:

------------------------------------------------------------------------------------------------------------------------
-- Sample script if the subroutines are stored in the scripts folder in an external script called "Utility.scpt" --
------------------------------------------------------------------------------------------------------------------------

-- load the external subroutine library Utility
set pathname to ((path to scripts folder as text)) & "Utility.scpt" -- get pathname to Utility
set Utility to (load script file pathname) -- load Utility

tell Utility to countdown_desktop_timer("00:03:20", "Coffee") -- call the desktop folder timer with an HMS time and timer label
return result

This approach greatly simplifies the creation of additional desktop timer scripts, as all you have to do is edit the parameter values in the tell Utility statement and save the script with the name of the new timer. You can export each of your desktop timer scripts as run-only applications, and have them available for execution through the Script Editor menu icon.

Creating desktop timer folders with a custom icon.

To give a timer folder a custom icon, install a Script Resources folder in your Scripts folder and store any iconized folders there that are required by your timer scripts. To iconize a folder, follow these steps:

  1. create a folder with an appropriate name (e.g. “Coffee” or “Freezer” in this case)
  2. open the png image you wish to use as your folder icon with Preview
  3. select the entire image (command-a)
  4. copy it to the clipboard (command-c)
  5. open the folder’s information box (command-i)
  6. click on the folder icon at the upper left (or press tab)
  7. paste the png image onto it (command-v)

I’ve made a 36-second video showing these steps. It was too long to be a gif, but you can watch it here:

https://tinyurl.com/Iconized-Desktop-Folder

To watch it simply position your cursor anywhere on the image and it will start. However, if you want to pause the video and/or reposition the video bug, then click on the image to open the video player. If the video doesn’t start on its own, click the play button. Use the cursor to make the video controller appear/disappear, and to reposition the bug.

For experimenting, download the png icon images for the coffee, freezer, and workout scripts here:

Coffee: https://tinyurl.com/Coffee-icon
Freezer: https://tinyurl.com/Freezer-icon
Workout: https://tinyurl.com/Workout-icon

Incidentally, the downloaded icon won’t look transparent on your desktop unless you open it with Preview, select and copy the image to the clipboard, and then paste it on the default icon in the png’s information box, just as you do to create a transparent folder icon. Try creating an iconized folder with one of the png images above and see how it works. Just remember to store it in a folder called “Script Resources” in your Scripts folder.

Here is a useful source for additional 3D icons: https://tinyurl.com/3D-icons. This search engine allows you to filter the search by color — picking the “transparent” option will limit the displayed icons to transparent PNGs, but this doesn’t always work. Also, some the best icons are not transparent and you will miss those if you restrict the search. However, you can make then so with a little trial-and-error using a PNG editing tool such as: https://tinyurl.com/Make-PNG-Transparent. This tool was successful in making the freezer icon transparent.

If none of the preceding resources allow you to find an icon for your desktop timer folder, you can employ the alarm clock (:alarm_clock:) as a general purpose timer icon:

Timer: https://tinyurl.com/Alarm-clock-icon

As above, you have to make an iconized folder called “Timer” from the downloaded png file, and the folder has to be stored in Script Resources. Lastly, for the iconized timer folder to load you need to use “Timer” as the parameter value for timerLabel in the call.

Note that all the desktop timer subroutines check to see if an iconized folder exists in Script Resources. If it doesn’t, a standard OS folder icon is displayed instead. So it is not necessary to use icons at all, although if you are running multiple timers, icons provide an easy way to know what each of the timers are timing.

Split/Pause timers.

The last AppleScript timer to demonstrate is the split/pause (countup) timer. In contrast to the progress bar and desktop timers, which provide continual updates of the elapsed time, the split/pause timer only displays the elapsed time when the user clicks a “Split Time” button. Clicking “Pause” puts the timer in pause mode, and a “Resume” button starts the timer up again. When timing is done the user clicks “Stop Timer” and a summary of both the total elapsed time and the pause-adjusted elapsed time is provided. This is an animated gif of a sample session showing successive dialogs being invoked by using the various features of the split/pause timer:

While this illustrates how the successive dialogs appear on the screen when using the timer, it will be easier to discuss the timer’s features using a graphic that consists a sequence of static snapshots which aren’t constantly moving:


[image truncated: drag to desktop and use QuickLook or Preview to see entire image]

The snapshots are in order from left to right and top to bottom. The upper left panel shows the the dialog window at the start of the timing session. Note that the timer starts as soon as the script is run and the dialog shows the initial split time at startup, which is “00:00:00”. The second panel (to the right) shows the result produced by clicking the “Split Time” button after 59 seconds. The third panel shows the result of clicking the “Pause Timer button” at 1:41 after startup, and the fourth panel shows the result after clicking the “Resume Timer” button. The timer resumes timing where it left off, namely 1:41. Finally, the last panel shows the result of clicking the “Stop Timer” button. A summary dialog indicates that the total elapsed time was 2:53 and the pause-adjusted elapsed time was 2:10. To avoid confusion, the current state of the timer (running/paused/summary) is indicated in the title of each dialog.

To say to the timer is “running” is misleading in one sense, because elapsed times are computed and displayed only when the user clicks the Split Time button. This is done by comparing the current date/time to the date/time at startup (adjusted for pauses) and displaying the result in a new dialog window. But after the new dialog is up, the script isn’t timing anything — it is just waiting for the user to make the next selection. And since it spends most of its time just waiting, its demands on the CPU are minimal. This will provide an explanation for why it is the most efficient timer based on CPU usage, as shown below.

The primary virtues of the split/pause timer are (1) no overhead is wasted on constant updating of the elapsed time every 1 or 2 seconds, and (2) the timer can be paused if necessary. The second feature can very useful. If you are timing how long it takes to finish a cross-word puzzle or similar task and the phone rings, you can pause the timer to answer the call, and restart it afterward. In some cases a combination of timers to time the same event is a good strategy. For instance, suppose there is a target time for finishing the crossword puzzle that represents “success” — say 45 minutes. You could start a progress bar timer to time for 00:45:00 and this would provide a constant visual representation of how close you are getting to the end. Simultaneously, you could run the split/pause timer to keep track of the actual time it takes to complete the puzzle in the event you need more time than set by the target goal. This is necessary because the progress bar timer stops at 00:45:00 whereas the split/pause timer doesn’t. Also, if the phone rings you can pause the split/pause timer, restart it afterward, and when you complete the puzzle click on “Stop Timer” to see if the pause-adjusted elapsed time is under 00:45:00. So one timer provides a continuous visual representation of the time used up relative to the target — information that can be taken in at a glance — and the other provides timing beyond the target and allows you to compensate for interruptions so as to still determine if the goal was met or not.

Here is the code for the split/pause timer:

-- Source: JayBird in https://macscripter.net/viewtopic.php?pid=134653#p134653
-- Adak47 modifications: changed button definitions, titles, and provided for a split timer and a paused timer. The default mode of operation is split timer mode, starting at 00:00:00, and updating whenever the Split Time button is pressed. Pressing the Pause button pauses the timer and a Resume button starts the timer again. Finally, the Stop Timer button displays the final elapsed and pause adjusted elapsed times. Current limitations: for timing events up to (but not including) 24 hrs duration.

try
	-- initialize text and numeric variables
	set timerMode to "Split Time" -- set the default timer mode
	set PauseTotal to 0 -- PauseTotal is the total time spent pausing
	set Time1 to 0 -- Time1 is the time at the beginning of a pause
	set Time2 to 0 -- Time2 is the time at the end of a pause
	-- record the startTime and start the timer
	set startTime to (current date)
	repeat
		set EndTime to (current date)
		set AdjustedTime to EndTime - PauseTotal -- adjust EndTime time for the total pause delays
		if timerMode = "Split Time" then -- display and process the Split Time dialog
			set timerMode to the button returned of ¬
				(display dialog ¬
					"Start:	" & (startTime as text) & return & "End:		" & (EndTime as text) & return & return & ¬
					"Split Time: " & TimeToText(AdjustedTime - startTime) ¬
					buttons {"Split Time", "Pause Timer", "Stop Timer"} ¬
					default button "Stop Timer" with title "Countup Timer is Running")
		else if timerMode = "Pause Timer" then -- display and process the Pause Timer dialog
			set Time1 to (current date) -- time at the beginning of the pause
			set timerMode to the button returned of (display dialog ¬
				"Start:	" & (startTime as text) & return & "End:		" & (EndTime as text) & return & return & ¬
				"Paused Time: " & TimeToText(AdjustedTime - startTime) ¬
				buttons {"Exit", "Resume Timer"} ¬
				default button "Resume Timer" with title "Countup Timer is Paused")
			if timerMode = "Exit" then return -- exit script in Pause mode without showing Stop Timer dialog
			set Time2 to (current date) -- time at the end of the pause
			set PauseTotal to PauseTotal + (Time2 - Time1) -- update the total pause time
			set timerMode to "Split Time" -- resume timing from the end of pause
		else if timerMode = "Stop Timer" then -- display and process the Stop Timer dialog
			set PauseAdjusted to return & "Pause Adjusted Time:	" & TimeToText(AdjustedTime - startTime) -- adjusted time
			if PauseTotal = 0 then set PauseAdjusted to "" -- don't display adjusted time if it is the same as the unadjusted 
			display dialog ¬
				"Start:	" & (startTime as text) & return & "End:		" & (EndTime as text) & return & return & ¬
				"Elapsed Time:		" & TimeToText(EndTime - startTime) & PauseAdjusted ¬
				buttons {"OK"} default button "OK" with title "Countup Timer Summary"
			if PauseAdjusted = "" then
				return "Start:	" & (startTime as text) & return & "End:		" & (EndTime as text) & return & ¬
					"Total Elapsed Time:	" & TimeToText(EndTime - startTime)
			else
				return "Start:	" & (startTime as text) & return & "End:		" & (EndTime as text) & return & ¬
					"Pause Adjusted Time:	" & TimeToText(AdjustedTime - startTime)
			end if
		end if
	end repeat
on error errorMessage number errorNum
	display dialog "Error in script Countup Timer: " & errorMessage & return & errorNum
end try

on TimeToText(TheTime) -- converts a time in seconds to a time in HMS format (hh:mm:ss)
	-- parameters - TheTime [integer]: the time in seconds
	-- returns [text]: the time in the format hh:mm:ss
	-- Nigel Garvey points out this script is only valid for parameter values up to 86399 seconds (23:59:59) and offers a solution for longer times here: https://macscripter.net/viewtopic.php?pid=134656#p134656 
	if (class of theTime) as text is "integer" then
		set TimeString to 1000000 + 10000 * (theTime mod days div hours) -- hours
		set TimeString to TimeString + 100 * (theTime mod hours div minutes) -- minutes
		set TimeString to (TimeString + (theTime mod minutes)) as text -- seconds
		tell TimeString to set theTime to (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
	end if
	return theTime
end TimetoText

Note that since the elapsed times displayed by the script are based on the start and end times returned by the date function, the repeat-loop timer lag discussed at the beginning of this post does not occur.

Assessments of CPU usage by the timers.

To assess the impact of the timers on the CPU of my iMac, I set up a fairly stringent test by running 7 timers at once. The following animated gif shows the timers grouped at the upper right of my desktop image (https://tinyurl.com/F4U-Corsair-of-Ira-Kepford):

The following timers were run:

  1. a progress bar 10-hr timer: “10-hr”
  2. a progress bar 1-hr timer: “Dryer”
  3. a desktop countup timer: “Freezer”
  4. a desktop countdown timer: “Dryer”
  5. a desktop countdown timer: “Workout”
  6. a desktop countup timer: “Timer”
  7. a split/pause (countup) timer

The windows of the progress bar and split/pause timers form a column on the left, and the iconized desktop timers form a column on the right. Inspecting how the times update in the seconds field reveals the occasional erratic nature of desktop timer updates. In the freezer timer for example, the update between “00hrs 13 min 14sec” and “00hrs 13 min 16sec” seems too long, and the subsequent update between “00hrs 13 min 16sec” and “00hrs 13 min 18sec” too short. This would be explained by the Finder being too busy to update the folder name at precisely the 16 second mark, and the resulting delay caused the first interval to be too long, and the second too short. If a Finder update delay exceeds the 2-second interval, the folder name update may be skipped entirely, producing a 4 second interval between updates. The progress bar timers on the other hand, display their countup/countdown times without such erratic behavior. The issue doesn’t arise for the split/pause timer because it doesn’t do dynamic updating. Instead, time updates only occur when the user clicks on the Split Time button. The animated gif shows that the last split/pause update occurred at 00:06:50 after startup.

While these timers were running, I used the activity monitor to determine their impact on the CPU. There is, of course, variation in CPU impact for each timer over time, but representative values are shown in this animated gif:

None of the timers have a dramatic impact on CPU usage. The progress bar timers have the greatest impact, coming in at ≈.3%. Next in line, the desktop timers each use about ≈.2% and the split/pause timer only ≈0%. These differences make sense given what the three types of timers are doing. The progress bar timers are not only displaying the countup/countdown times dynamically, they are stepping the progress bar in very small steps (1-sec at a time out of 10-hrs or 1-hr) to produce the advancing progress bar. This overhead is not present in the desktop timers, which is why their CPU impact is less. On the other hand, they suffer from the need to constantly update the timer folder names with the current time. This requires the Finder to perform that action every 2 seconds, and in this case to do so for 4 desktop timers at once. From my tests, I learned that the .2% impact is fairly consistent, regardless of system load and the number of timers. But what you notice when load or number of timers is high is that folder name updates may be delayed, so the times displayed get bunched up or stretched out. And, as noted, in some cases an update may be skipped entirely.

The split/pause timer has 0% impact except when the user is interacting with it. Clicking on the Split Time button to obtain the current countup time will produce a quick burst of activity that raises CPU impact to 1-2%, which is quickly followed by a return to 0%. The results are explained by the fact that the split/pause timer only does work when it is asked to update the split time value, to pause or resume the timer, or to stop the timer. The rest of the time a static dialog is waiting for the user to make a selection, and no iterations of the repeat-loop or code execution can occur until that selection is made. An interesting exception is when the split/pause dialog is made the active window: a quick bump in CPU usage to ≈.5% occurs followed by a return to 0%, assuming the mouse is not being moved. As soon as the mouse is moved, there is a bump up to 2% if you move it continuously, which again drops to 0% when you stop.

Comparative strengths of the timers.

The table below summarizes the features possessed by each of the timers. If a box is checked, it indicates the timer has that particular feature or strength. Each timer is rated as to whether it has ±1-second accuracy, dynamic updating, 1-second updating, avoids erratic updating, provides a visual analog, is iconized, is unobtrusive, can be paused, and is highly efficient. For completeness, I include the single-line and repeat loop timers that I illustrated at the very beginning, and also an unadjusted progress bar timer. But my comments below focuses on the features of the (lag-adjusted) progress bar, desktop, and split/pause timers in the last three rows of the table.


[image truncated: drag to desktop and use QuickLook or Preview to see entire image]

Of all the timers tested, the lag-adjusted progress bar timer has the greatest number of strengths. On the downside it is also the least efficient timer, and obscures more of the desktop than the desktop timer, especially when multiple timers are in use. The desktop timers also provide support for icons that makes multiple timers easy to distinguish. These positive features must be weighed against the features that are lacking. While the desktop timer could support 1-second updating, in practice this does not work well because of erratic updating, which can occur even with the 2-second update interval used in the script I provide, and the problem of erratic updating is compounded when many desktop timers are run at once.

Relative to the progress bar and desktop timers, the split/pause timer has only two features in its favor. First, it is the most efficient of all the timers tested (see CPU results in the last column). Second, it is the only timer that can be paused, which is important in certain timing scenarios. While it is conceivable that the progress bar and desktop timer scripts could be modified to allow for pausing, they do not lend themselves as naturally to doing this as the split/pause timer. Since those timers do not present a dialog with a pause option, some event must be monitored that can signal the progress bar or desktop timer to pause and resume. Using a function key to toggle the pause and resume states might work, but I have not succeeded in coming up with a “clean” solution. But if this can be done, then the only advantage the split/pause timer would have is efficiency.

Concluding comments.

I hope this overview of AppleScript timers has been of some interest. I invite members to provide comments, constructive criticism, and suggestions for improvements to the various timer scripts. Once the progress bar, desktop folder, and split/pause timer scripts have been vetted by more experienced members I will make any necessary changes and post the solutions to the Code Exchange, as requested in Proffer Your Project of the posting guidelines (https://macscripter.net/guidelines.php).

Finally, I invite members to provide links to other AppleScript timer solutions than those reviewed above, and to comment on their relative advantages and disadvantages.

Model: iMac running OS 10.13.6
AppleScript: 2.7
Browser: Safari 12.0
Operating System: macOS 10.13

Thanks Adam for this interesting article and examples.

ldicroce – thanks, I appreciate the feedback. If you try any of the scripts, let me know which you like best.