A journaling stop watch for mundane tasks and time slips

About

StopWatch is a simple time tracker, that writes the timeslips [1] into a human readable journal. The journal can be used as a foundation for billing, or for:
Figuring out how much time you have spent on some tasks, -how you actually spend your time.

If you have your journal open in TextEdit, then the journal will be updated in TextEdit, and optionally brought to front.[2]

You can at any time, run the script, read off the dialog over how much time has passed, and then cancel the dialog again.

       [img]https://dl.dropboxusercontent.com/u/6829111/StopWatch.jpeg[/img]

It is very easy to use in your workflow, since there is three main ways to invoke it:

StopWatch comes with an Automator Service, which you can assign a short cut to in the keyboard preferences.

The script also comes with an Applet that you can put onto your Dock, so you don’t have to use the keyboard to reach it, when you are currently operating your mouse.

  • The script itself should be stored somewhere logical to you on your ScriptMenu.

StopWatch doesn’t tax you, or your system.

No processor time nor battery is used when you aren’t interacting by the script in one of several ways. It is also very unintrusive, and can be configured to not disturb your current App/Window setup at all.

[1]
The “timeslip” lines consists of the start date,a description of what you are tracking, a start point in time, end point time, effective time used, and slack - paused time. (optional), as comma separated values.

[2]
The script itself, doesn’t even run in the background, so you use 0% of processor time and battery when you don’t actually interact with the script.
This also means, to the greatest extent, that you can turn off your computer, go out and do a task you have started tracking, get back in when you are finished, turn on the computer again, and stop tracking the task.

Assumptions about Your System

I assume you have “full keyboard access” enabled, and “use function keys” (you have to press fn-F5 to dim the screen brightness), those settings can be set in the keyboard preferences pane of System Preferences.

Installation of the Script

1.) Compile the script, and save it somewhere accessible and logical in your ScriptMenu.

2.) Try running it three or more times, but be sure to “stop” it from Script Editor.

3.) Inspect that you got contents into the “~/Desktop/Journal.txt” file.

4.) Close it in the Script Editor, and try to run it again from the Script Menu,
and verify that you got a timeslip from that session too in your file.


# Copyright © 2015 McUsr, you may not post this as a work of your own on some webpage, or in a book.
# http://macscripter.net/viewtopic.php?pid=180130#p180130
(*
 Thanks to kel for coming with the great idea of having an inspection script that can show
 the current state. (Not done yet.)
 Now considers daylightsavings time regardless of locale
 This works as follows: if the time to gmt has increased, from when we started timing, then we must 
 subtract the difference, if the time has decreased, then we increase the time of the current lap
 likewise.
 Version 2:
 Modularized so you can make it fit your own needs.
 Version 2.1
 Numerous small bug fixes, added updating of journal if the journal was open in TextEdit
 
 Added a journal file, and a choice to open the journal file when the timing/journaling is done.
 Version 2.2:
 Rearranged the output of the find dialog. Resized the journal window to a fitting width. This is done by a property, windowWidth.
 
 Version 2.3
 Changed the global clockIcon into a property, in order for Stopwatch to be runnable from other scripts.
 NB! This means that a script must be opened, recompiled and saved on other machines, you can't just share
 a script by an email.
 
 Modifiying it to suit your own needs: The Journaling part is mostly hooked up in the stopTiming()
 handler.
 
 The formatting of time is handled by the stopClocking of the stopClockwork handler.
 
 Version 2.4
 
 Harnessed the dialogues. (For Workflow-runner.)
 Revealing the end of the Journal, in TextEdit.
 Changed  the calculation of the adjusted endtime. 
 (Only takes effect when you have set the property
 JournalSlack to false.)
 
 Version 2.5
 
 Removed two bugs, regarding recording of elapsed time and slack,
 which would happen if the user cancelled a "pauseOrStopDialog" or a "startOrStopDialog"-
 
 Version 2.6
 
 Modularized the code further, now the clockWork is an encapsulated object
 containing what it should. Numerous issues. (I assume you'll never leave any dialog. so it times
 out after two minutes!)
 
 Version 2.7
 
 Implemented the wantsNotofications property, harnessed the appendToFile handler.
 -- Thanks to DJ Bazzie Wazzie.
 Also removed a redundandt notification, and implemented the property  for turning
 Notifications off in the code.
 
 Version 2.8
 Implemented changes suggested by Nigel Garvey and StefanK to further simplify the
 appendToFile. Thanks a lot guys!
 Added the start time to the dialogs, so you can always see when a task started.
 Added the option of opening the journal file from the first dialog, if you want 
 quick access to the journal file, (or have forgotten where you stored it, and loathe
 to have to open the script to see where that was.)
 Removed some redundant code from displayJournalFile at the same time.
 
 Version 2.9
 The refreshJournal handler is more "silent" when TextEdit isn't visible.
 The dialogs are more robust with timeouts of 5 minutes, giving up a second before.
 The script is reset, when this happens, since we won't save the script in an undefined
 state. (The script is saved "manually" from the Service, and the Applet.)
 
 Version 3.0
 Removed the emergency reset, as I figured that the best thing was to 
 return buttons with value of cancel where that made sense and Ok
 when that made sense.
 
 Version 3.1
 Declared the variable gaveUp up front in the dialog handlers, 
 so that Automator didn't bark.
 
 Version 3.1.1
 I have changed the "Stop" into "Stop."to reflect that pressing "Stop." takes you to 
 another dialog.
 
 Version 3.2
 Asserted that time strings will allways show up in 24h format, and added some 
 formatting to the dialogs as well as factoring out the actual dialog (3button).
 I have also changed the wording of the dialogs, with the hope that the new wording is better.
 
 Version 3.2.1
 Fixed a bug in the dialog3button handler
 
Version 3.2.2
Fixed a bug in the fmt24HTime handler
*)
property parent : AppleScript
property revision : "3.2.1"
property scriptTitle : "Stop Watch"
-- user definable properties:
property wantsNotifications : false
-- set the above to false if you feel the dialogs are enough, when it comes to regular notifications.
property journalFile : "~/Desktop/journal.txt"
-- Set it to point to whatever text file you want to keep your journal in, but it should be a posix path!
property journalSlack : true
-- Set the property above to false if you want the slack to be subtracted from the end time.
property JournalToFrontUnsolicited : false
-- Set the property above to false, if you rather not have the journal brought to front.

property JournalHeading : "Date                    Task description      Started   Ended     Time  Slack
=============================================================================="

-- adjust the heading above to suit your needs.
-- properties for TextEdit follows, below, change them to your taste
property fontName : "Menlo-Regular"
property fontSize : 12.0
property windowWidth : 935

-- Properties that are used by the "state-machine" You *really* shouldn't alter them, unless you are rebuilding the stop watch.
property state : "Stopped"
property origPrompt : "Period/task to track duration of:"
property observation : origPrompt
property startTime : 0
property clockIcon : (path to library folder from system domain as text) & "CoreServices:CoreTypes.bundle:Contents:Resources:Clock.icns"

script clockWork
	(*
	This script-object  is a kind of stopwatch, for tracking how long a task takes, and the sum of how long
	the tasks have been paused from start to end.
	
	*)
	property start_time : 0
	property end_time : 0
	-- The "Physical" start and end time, thatis the unadjusted end time.
	
	property t0 : 0
	property t0_ToGMT : 0
	property Te_ToGMT : 0
	property elapsed : 0
	property slack : 0
	property slackStart : 0
	
	on formatTime(someSecs)
		-- the numbers we format in here, can still be calculated with, 
		-- -so its part of the model, not the view.
		if someSecs ≥ 3600 then -- we have to consider hours
			set tHours to someSecs div 3600
			if tHours < 10 then
				set tHours to "0" & tHours & ":"
			else
				set tHours to "" & tHours & ":"
			end if
			set someSecs to someSecs mod 3600
		else
			set tHours to "00:"
		end if
		set tMinutes to (text -2 thru -1 of ("0" & (someSecs div 60))) & ":"
		set someSecs to someSecs mod 60
		set tSecs to text -2 thru -1 of ("0" & someSecs)
		return tHours & tMinutes & tSecs
	end formatTime
	
	-- High level Actions, fired by user initated Events, as the state of the StopWatch changes.
	on start_clocking()
		--As restart_clocking but also sets the absolute start_time
		-- which will be represented in a 24 clock format.
		set {t0, t0_ToGMT} to {(current date), (time to GMT)}
		copy t0 to start_time
		return start_time
	end start_clocking
	
	on pause_clocking()
		-- See: restart_clock, but think "pause"
		set {slackStart, t0_ToGMT} to {(current date), (time to GMT)}
	end pause_clocking
	
	on restart_clocking()
		-- Sets a new T0 when we restart, so we can record "this lap" correcly.
		-- We also record time to gmt, For the case that we have changed timezone to or 
		-- from daylightsavings time, so the recorded time can be adjusted accordingly.
		set {t0, t0_ToGMT} to {(current date), (time to GMT)}
		return formatTime(elapsed)
	end restart_clocking
	
	on view_elapsed_untilNow()
		-- Gives intermediary elapsed-time to display in the dialog
		-- without changing any variables, so the clock is still running!
		set imed_time to elapsed + ((current date) - t0) + (t0_ToGMT - (time to GMT))
		return {formatTime(imed_time), start_time}
	end view_elapsed_untilNow
	
	-- the idea behind having the two handlers shown in the "finite-state" machine, 
	-- is to not not repeat code, but to make the code easier to grasp.
	on record_lap()
		-- Is called to book-keep the slack, when we end a 'lap'
		-- the reason being that we either, 'pause', or 'stop/resets' the timer.
		set Te_ToGMT to (time to GMT)
		set elapsed to elapsed + ((current date) - t0) + (t0_ToGMT - Te_ToGMT)
		return (formatTime(elapsed))
	end record_lap
	
	on view_paused_untilNow()
		-- Gives intermediary pause-time to display in the dialog
		-- without changing any variables, so the pause is still tracked!
		set imed_pause to slack + ((current date) - slackStart) + (t0_ToGMT - (time to GMT))
		return {formatTime(imed_pause), start_time}
	end view_paused_untilNow
	
	on record_pause()
		-- Is called to book-keep the slack, when we end a pause.
		set Te_ToGMT to (time to GMT)
		set slack to slack + ((current date) - slackStart) + (t0_ToGMT - Te_ToGMT)
		return (formatTime(slack))
	end record_pause
	
	on stop_clocking()
		-- Stops clocking this time slip, resets the timer, and return the results.
		
		-- we save values up front, we are wiping them out before stop_clocking returns.
		copy start_time to startTime
		set time_spent to elapsed
		set pause_time to slack
		set adj_endtime to start_time + time_spent + Te_ToGMT - t0_ToGMT
		-- A date object, containing the adjusted end time when we remove the slack
		
		set {t0, t0_ToGMT, start_time, Te_ToGMT, elapsed, slack, slackStart} to {0, 0, 0, 0, 0, 0, 0}
		-- We wipe out the date here, so we'll start with a clean slate next time!
		
		return {startTime, formatTime(time_spent), formatTime(pause_time), (current date), adj_endtime}
		-- Last value, is for the case that someone wants to adjust a journal entry
		--  -so the slack time goes unnoticed, but can be  subtracted from the end time
		-- Next to last value is the end_date, that we have no reason for storing in the clockWork.
	end stop_clocking
end script

on run
	local elapsed, slack, timeSlip, datum, btn
	
	if state = "Stopped" then
		set {btn, observation} to startDialog()
		if btn is "Cancel" then
			-- The user aborted
			return
		else if btn is "Open Journal" then
			displayJournal()
			return
		end if
		
		set state to "Running"
		set datum to fmt24HTime(clockWork's start_clocking())
		-- Changes the state to running so we don't enter this block before this clocking is stopped (reset).
		if wantsNotifications then display notification "Clocking started at : " & datum & "." with title scriptTitle subtitle observation
	else if state = "Running" then
		-- Clock is ticking, do we want to pause or stop the tracking of the observation?
		-- -Or just view intermediary results, time spent so far, user does this by hitting "Cancel"
		-- We can come back to the state "Running" from the state "Paused", 
		-- or when we have started afresh again from the state "Stopped"
		
		set {elapsed, datum} to clockWork's view_elapsed_untilNow()
		set btn to pauseOrStopDialog(observation, elapsed, datum)
		
		if btn is not "Cancel" then
			-- Recording "lap", time spent on observation, so far.
			set elapsed to clockWork's record_lap()
			if btn = "Pause" then
				set state to "Paused"
				clockWork's pause_clocking()
				if wantsNotifications then display notification "Paused after " & elapsed & " , at: " & fmt24HTime(current date) & "." with title scriptTitle subtitle observation
			else if btn = "Stop." then
				set state to "Stopped"
				set timeSlip to clockWork's stop_clocking()
				journalAndDisplay(observation, timeSlip)
				set observation to origPrompt
				-- There is no way out of the "Stopped" state, timer is reset, so we'll start afresh next time!
			end if
		end if
		
	else if state is "Paused" then
		-- We can only come back to the state "Paused from the state "Running", that is;
		--If we don't stop the script from this "Paused" state.
		
		set {slack, datum} to clockWork's view_paused_untilNow() -- user may hit cancel, to just see intermediary results.
		set btn to stopOrStartDialog(observation, slack, datum)
		
		if btn is not "Cancel" then
			set slack to clockWork's record_pause()
			if btn = "Start" then
				set state to "Running"
				set elapsed to clockWork's restart_clocking()
				if wantsNotifications then display notification "Continued at " & fmt24HTime(current date) & ". Time so far: " & elapsed & "." with title scriptTitle subtitle observation
			else if btn = "Stop." then
				set state to "Stopped"
				set timeSlip to clockWork's stop_clocking()
				journalAndDisplay(observation, timeSlip)
				set observation to origPrompt
				-- There is no way out of the "Stopped" state, timer is reset, so we'll start afresh next time!
			end if
		end if
	end if
end run

on startDialog()
	set {gaveUp, btn, theText} to {false, "Cancel", observation}
	
	set introString to "Time is now: " & fmt24HTime(current date) & "

Start StopWatch or Open Journal?"
	
	with timeout of 320 seconds
		tell application (path to frontmost application as text)
			
			try
				set {gave up:gaveUp, button returned:btn, text returned:theText} to (display dialog introString default answer observation with title my scriptTitle buttons {"Cancel", "Open Journal", "Start"} cancel button 1 default button 3 with icon file (my clockIcon) giving up after 319)
			on error e number n
				if n ≠ -128 then
					error e number n
				end if
			end try
		end tell
	end timeout
	if gaveUp then set {btn, theText} to {"Cancel", observation}
	-- We never pass this point if the user hits "Cancel" then the script effectively dies.
	return {btn, theText}
end startDialog

on dialogWith3Buttons(displayString, buttonList, defaultNr, gaveUpButton)
	--	3 buttons, defaultnr changes, and so does the button that should
	-- be returned if the dialog times out (gaveUpButton)
	set {gaveUp, btn} to {false, gaveUpButton}
	with timeout of 320 seconds
		tell application (path to frontmost application as text)
			try
				if gaveUpButton = "Cancel" then
					set {button returned:btn, gave up:gaveUp} to (display dialog displayString with title my scriptTitle buttons buttonList cancel button 1 default button defaultNr with icon file (my clockIcon) giving up after 319)
				else
					set {button returned:btn, gave up:gaveUp} to (display dialog displayString with title my scriptTitle buttons buttonList default button defaultNr with icon file (my clockIcon) giving up after 319)
				end if
			end try
		end tell
	end timeout
	if gaveUp then set btn to gaveUpButton
	return btn
	-- the button is cascaded upwards to the main run handler, where it governs
	-- what action to take
end dialogWith3Buttons

on pauseOrStopDialog(obsDescription, elapsed, datum)
	
	set resultString to makeIngress("Currently Tracking:", obsDescription) & "

Started:  " & fmt24HTime(datum) & "    Time Now: " & fmt24HTime(current date) & "
Elapsed: " & elapsed
	return dialogWith3Buttons(resultString, {"Cancel", "Pause", "Stop."}, 2, "Cancel")
end pauseOrStopDialog

on stopOrStartDialog(obsDescription, slack, datum)
	
	set resultString to makeIngress("Currently Pausing:", obsDescription) & "
				
Started: " & fmt24HTime(datum) & " Time Now: " & fmt24HTime(current date) & "
Paused: " & slack
	return dialogWith3Buttons(resultString, {"Cancel", "Start", "Stop."}, 2, "Cancel")
end stopOrStartDialog

on endDialog(obsDescription, startTime, obsTime, slack, JournalEntry)
	
	set resultString to makeIngress("Final Time Slip for:", obsDescription) & "

Started:  " & fmt24HTime(startTime) & "    Ended:  " & fmt24HTime(current date) & "
Elapsed: " & obsTime & "    Paused: " & slack
	return dialogWith3Buttons(resultString, {"Open Journal", "Clipboard", "Ok"}, 3, "Ok")
end endDialog

on journalAndDisplay(observation, L)
	-- This is a main handler that writes journal entry, calls the end dialog, and resets variables
	-- See: clockWork's stop_clocking() handler.
	
	set {startTime, formattedElapsedTime, FormattedTimePaused, endTime, adj_endtime} to L
	set JournalEntry to makeJournalEntry(observation, startTime, formattedElapsedTime, FormattedTimePaused, endTime, adj_endtime)
	
	set btn to endDialog(observation, startTime, formattedElapsedTime, FormattedTimePaused, JournalEntry)
	
	if btn is "Clipboard" then
		set the clipboard to JournalEntry
	else if btn is "Open Journal" then
		displayJournal()
	else if running of application id "ttxt" then
		refreshJournal()
		-- we only refresh the journal if it was open in TextEdit
		-- and only brings the journal to front if JournalToFrontUnsolicited is true
	end if
end journalAndDisplay


(* ====== Journaling subsystem ===== *)


on makeJournalEntry(observation, startTime, formattedElapsedTime, FormattedTimePaused, endTime, adj_endtime)
	-- This is really the place to start modifying if you want the script to use CSV
	-- or talk directly to Excel/Numbers/FileMaker 
	-- You may need to cange the output format of the StopwWatch itself too: 
	-- -have a look at the stopClocking of the clockWork.
	-- ----------
	-- journalSlack and scriptTitle are global properties
	makeJournalFileIfNeedBe(journalFile, JournalHeading)
	set delim to ", "
	-- Date of timing, what was timed, start_time, end_time, used time, slack, 
	if journalSlack then
		set JournalEntry to linefeed & startTime's date string & delim & observation & delim & fmt24HTime(startTime) & delim & fmt24HTime(endTime) & delim & formattedElapsedTime & delim & FormattedTimePaused
		-- Alternatively: without any slack, the end time has the slack subtracted
	else
		set JournalEntry to linefeed & startTime's date string & delim & observation & delim & fmt24HTime(startTime) & delim & fmt24HTime(adj_endtime) & delim & formattedElapsedTime
	end if
	set success to appendToFile(JournalEntry, journalFile)
	if not success then
		tell application (path to frontmost application as text)
			display alert my scriptTitle message "An error occured while trying to write a journal entry to: " & journalFile
		end tell
	end if
	return JournalEntry
end makeJournalEntry

on makeJournalFileIfNeedBe(theFile, theHeading)
	if theFile starts with "~/" then
		set theFile to POSIX path of (path to home folder as text) & text 3 thru -1 of theFile
	end if
	set hasJournal to false
	tell application id "sevs" to if exists item theFile then set hasJournal to true
	if not hasJournal then
		set success to appendToFile(theHeading, theFile)
		if not success then
			tell application (path to frontmost application as text)
				display alert my scriptTitle message "An error occured while trying to write a journal entry to: " & journalFile
				error number -128 -- halts the script
			end tell
		end if
	end if
end makeJournalFileIfNeedBe

on displayJournal()
	
	set {fileToOpen, journalStemName} to tildeExpandedPosixPathAndDeriveStemName(journalFile)
	tell application id "sevs"
		if exists item fileToOpen then
			set fileExists to true
		else
			set fileExists to false
		end if
	end tell
	if fileExists then
		closeAnyOpenDoc(journalStemName)
		do shell script "open -b \"com.apple.TextEdit\" " & quoted form of fileToOpen & " >/dev/null 2>&1 &"
		preparateDocWindow(fileToOpen, fontName, fontSize, windowWidth, true)
	else
		tell application (path to frontmost application as text)
			display alert my scriptTitle message "displayJournal: The Journal file:
" & fileToOpen & "
Doesn't exist!
Hopefully this is your first run of the script since no Journal file exists before you have actually journalled something."
		end tell
	end if
end displayJournal


on refreshJournal()
	-- We refresh a journal, if TextEdit was running, and if the journal
	-- were among TextEdit's open documents, we only bring it to front if
	-- JournalToFrontUnsolicited is true. NB! if JournalToFrontUnsolicited is false
	-- then we will rely on TextEdit's autosave feature to save the journal.
	set {fileToOpen, journalStemName} to tildeExpandedPosixPathAndDeriveStemName(journalFile)
	
	set wasOpen to closeIfOpenDoc(journalStemName)
	
	
	if wasOpen then
		
		tell application id "sevs" to set wasVisible to visible of process "TextEdit"
		
		-- It is only that if the document was open, we are going to refresh it.
		try
			if JournalToFrontUnsolicited then
				if wasVisible then
					do shell script "open -b \"com.apple.TextEdit\" " & quoted form of fileToOpen & " >/dev/null 2>&1 &"
				else
					do shell script "open -ge  " & quoted form of fileToOpen & " >/dev/null 2>&1 &"
				end if
			else
				do shell script "open -ge  " & quoted form of fileToOpen & " >/dev/null 2>&1 &"
			end if
		on error e number n
			tell application (path to frontmost application as text)
				display alert my scriptTitle message "refreshJournal: Error when trying to open the Journal file:
" & fileToOpen & "
" & e & n
			end tell
		end try
		if wasVisible then
			preparateDocWindow(journalStemName, fontName, fontSize, windowWidth, JournalToFrontUnsolicited)
		else
			preparateDocWindow(journalStemName, fontName, fontSize, windowWidth, false)
			if not JournalToFrontUnsolicited then tell application id "sevs" to set visible of process "TextEdit" to false
		end if
		
		if not JournalToFrontUnsolicited or not wasVisible then
			-- cant see the update so we tell about it.
			display notification "Updating the open journal file: " & journalStemName & "." with title scriptTitle
		end if
		
	else
		-- cant see the update so we tell about it.
		display notification "Updating the open journal file: " & journalStemName & "." with title scriptTitle
	end if
end refreshJournal

(* ======= Handlers of General Utility ======== *)

on fmt24HTime(aDate)
	tell aDate
		set h to hours of it
		set m to minutes of it
		set s to seconds of it
	end tell
	if h < 10 then
		set h to "0" & h
	else
		set h to "" & h
	end if
	if m < 10 then set m to "0" & m
	if s < 10 then set s to "0" & s
	return (h & ":" & m & ":" & s)
end fmt24HTime

on makeIngress(leading, observation)
	-- avoids along line in a dialog box that flows to the line below
	-- by inserting a newline.
	set ingressLen to (length of leading) + (length of observation)
	if ingressLen ≤ 33 then
		set ingress to leading & " \"" & observation & "\""
	else
		set ingress to leading & linefeed & linefeed & " \"" & observation & "\""
	end if
	return ingress
end makeIngress

to appendToFile(theData, theFile)
	(*
       For adding text to the end of a file, which is created if it didn't exist.
       returns true upon success
   *)
	if theFile starts with "~/" then
		set theFile to POSIX path of (path to home folder as text) & text 3 thru -1 of theFile
	end if
	-- Stolen from Chris Stone, as I have stolen the omission of file specifier
	-- or alias from the open for access command.
	try
		-- open the file, or create it if it doesn't exist
		set fRef to open for access theFile with write permission
		-- Simplified: Thanks to Nigel Garvey
		-- Append contents to file
		write theData to fRef starting at eof as «class utf8»
		-- Close it
		close access fRef
		return true
	on error
		try -- harnessing, thanks to DJ. Bazzie Wazzie
			-- simplifying thanks to StefanK
			close access theFile
		end try
		return false
	end try
end appendToFile

on baseNameOfPxPath for pxPath
	local tids, basename
	set {tids, text item delimiters} to {text item delimiters, "/"}
	set {basename, text item delimiters} to {text item -1 of pxPath, tids}
	return basename
end baseNameOfPxPath

on tildeExpandedPosixPathAndDeriveStemName(posixPath)
	if posixPath starts with "~/" then
		set expandedPath to POSIX path of (path to home folder as text) & text 3 thru -1 of posixPath
	else
		set expandedPath to posixPath
	end if
	set stemName to baseNameOfPxPath for expandedPath
	return {expandedPath, stemName}
end tildeExpandedPosixPathAndDeriveStemName

on closeAnyOpenDoc(docStemName)
	-- uses this from displayJournal, where we are going to display the document unequivocally.
	tell application id "ttxt"
		try
			tell document docStemName to close saving no
		end try
	end tell
end closeAnyOpenDoc

on closeIfOpenDoc(docStemName)
	-- uses this from refreshJournal, where are only going to update the journal if 
	-- was already open.
	tell application id "ttxt"
		if exists document docStemName then
			try
				tell document docStemName to close saving no
				set success to true
			on error
				set success to false
			end try
		else
			set success to false
		end if
	end tell
	
	return success
end closeIfOpenDoc



on preparateDocWindow(docStemName, fontName, fontSize, windowWidth, isSentToFront)
	(*
	preparates a document window, containing a document 
	that is guarranteed to be open, by setting
	correct font, size, and window width, then saving the window, and 
	scrolling to the end of, this handler was made for displaying the 
	last journal entry, but may be used, whenever the end of some
	document ist the most interesting to view.
	
	*)
	tell application id "ttxt"
		try
			tell document docStemName
				set font of it to fontName
				set size of it to fontSize
			end tell
		end try
		tell front document
			repeat while name of it is not in docStemName
				delay 0.2
			end repeat
		end tell
		set {bounds:wbnd} to window 1 of it
		set item 3 of wbnd to (item 1 of wbnd) + windowWidth
		set bounds of window 1 of it to wbnd
		
	end tell
	if isSentToFront then
		tell application id "sevs" to tell application process "TextEdit"
			keystroke "s" using command down
			delay 0.2
			key code 125 using command down
		end tell
	end if
end preparateDocWindow

Installation of the applet.

1.) Activate Script Editor and create a new script.

2.) Paste the code below the Installation instructions into it.

3.) Enter the correct posix path to the script.

You can easily get the correct posix path if you opt-click the folder where you stored the StopWatch script, and just drag the script from the Finder window, into the Script Editor window that contains the applet-code.)

4.) Compile the script, and check that it runs correctly.

5.) Export it as an applet, and give it the name StopWatch.app.

6.) From the applet sidebar in the Script Editor window’s sidebar, choose the gear icon and reveal your applet in finder, then choose to show package contents.

7.) Set up the Applet as an Agent, so it doesn’t mess up the screen.

If you have Xcode or some Property List editor, then add a key to the info.plist file. The human readable key is “Application is Agent (UIElement)” and it is of type boolean, and should be checked off.

If you don’t have Xcode or a plist editor, then the key is in Xml form below, and you can paste that into the info.plist file with in with an editor, preferably TextWrangler, but I guess you can do it with TextEdit too, but see to that you have set the file encoding in its preferences to utf-8 before you do.

8.) Save and close info.plist

9.) move back in the finder window, so you can see the applet again, now it should run without presenting any menu.

10.) Give the applet an icon you’d like to see in the Dock.

I recommend you use a particular icon from the /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Clock.icns

10.1) You must open the icns file in the coreservices/resources folder in Preview.

10.2) You must then find the icon with the “right size” and copy it to the clipboard, (the icon with index number 7 in the sidebar of the Preview window suited me fine).

10.3) Open the info panel of the applet, click the applet icon so it turns “bluish”, and paste.

10.4) Drag the applet onto the Dock

10.5) Verify that it works by clicking on it.

10.6) Close the window in Script Editor, as we are all done.


property parent : AppleScript
script theStopWatch
   property parent : AppleScript
   on run
       do shell script "/usr/bin/osascript /Path/to/StopWatch.scpt >/dev/null 2>&1 &"
   end run
end script
tell theStopWatch to run

Installation - creation of the Automator Service.

Thanks to Mark Hunte I have found a solution, that doesn’t involve an Automator Service, to create a dedicated StopWatchService with AppleScript.

This is not so complex, yet not totally straight forward, if you are a novice on the subject.

1.) Open AppleScript Editor, or Script Editor.

2.) We are going to make a new Cocoa-AppleScript Applet, (File->New From Template)

3.) Select everything in it, and paste in the code below, overwriting everything that was there.
(Change the path to were StopWatch.scpt resides, and any spaces in the filename should be quoted with a ''. -You can easily get the correct posix path if you opt-click the folder where you stored the StopWatch script, and just drag the script from the Finder window, into the Script Editor window that contains the applet.)


-- main.scpt
-- Cocoa-AppleScript Applet
--
-- 2015 McUsr. 
-- Big thanks to Mark Hunte, it is totally stolen from him. Any faults are mine.

property NSWorkspace : class "NSWorkspace"

tell current application's NSApp to setServicesProvider:me
NSUpdateDynamicServices()

my runAService()
on runAService()
		tell me
		do shell script "/usr/bin/osascript /Path/to/StopWatch.scpt >/dev/null 2>&1 &"
		quit
	end tell
end runAService

4.) Compile, then save it as StopWatchService.

5.) Open the sidebar pane, and give it a bundle identifier of

6.) Save again.

7.) Click on the gear icon on the side bar and select “Reveal in Finder”.

8.) Launch Terminal

9.) Enter [b]defaults write /b then drag the info.plist file into the terminal window, so the posix path of it follows what you just typed.

10.) Delete the .plist part of the filename, with a space and write: NSServices -array-add ‘{NSMenuItem={default=“Launch StopWatch”;}; NSMessage=“runAService”; NSPortName=“StopWatchService”; NSSendTypes=();}’, then hit enter.

The whole line should look something like this before you hit enter:

  1. Find the icon, and paste it in, like you did for the StopWatch app in that installment above.

  2. Open the CocoaAppletAppDelegate.scpt of the Contents/Resources folder of the service, and replace it with the following code, ( you may wish to save this somewhere on disk as well, as a back up).

Warning:
Your script editor may most probably hang of this step, I think it usually goes well, that all files get to be restored after you have force quit Script Editor, but then again, it may not, so be sure to have saved any unsaved files, before you try to overwrite the existing CocoaAppleAppDelegate.


script CocoaAppletAppDelegate
	property parent : class "NSObject"
	property mainScript : missing value -- the applet's main.scpt
	property isQuitting : false -- re-entrancy guard: true = in the process of quitting
	
	on applicationWillFinishLaunching:aNotification
		-- Insert code here to initialize your application before any files are opened 
		
		-- Emulate an OSA Applet: Load the main script from the Scripts resource folder.
		try
			set my mainScript to load script (path to resource "main.scpt" in directory "Scripts")
		on error errMsg number errNum
			-- Perhaps this should silently fail if it can't load the script; that way, a Cocoa applet
			-- can just have Cocoa classes and no main.scpt.
			display alert "Could not load main.scpt" message errMsg & " (" & errNum & ")" as critical
		end try
	end applicationWillFinishLaunching:
	
	on applicationDidFinishLaunching:aNotification
		-- Insert code here to do startup actions after your application has initialized
		
		if mainScript is missing value then return
		
		-- Emulate an OSA Applet: Invoke the "run" handler.
		
		-- If we have already opened files during startup, don't invoke the run handler.
		
		try
			tell mainScript to run
		on error errMsg number errNum
			if errNum is not -128 then
				display alert "An error occurred while running" message errMsg & " (" & errNum & ")" as critical
			end if
		end try
		
		-- TODO: Read the applet's "stay open" flag and quit if it's false or unspecified.
		-- For now, all Cocoa Applets stay open and require the run handler to explicitly quit,
		-- which is arguably more correct for a Cocoa application, anyway.
		(* if not shouldStayOpen then
			quit
		end if *)
	end applicationDidFinishLaunching:
	
	on applicationShouldTerminate:sender
		-- Insert code here to do any housekeeping before your application quits 
		
		-- Guard against re-entrancy.
		if not isQuitting and mainScript is not missing value then
			set isQuitting to true
			
			-- Emulate an OSA Applet: Invoke the "quit" handler; if the handler returns, it has fully
			-- handled the quit message and we should not quit, otherwise, it calls "continue quit",
			-- which returns error -10000.
			try
				tell mainScript to quit
				set isQuitting to false
				
				return current application's NSTerminateCancel
			on error errMsg number errNum
				-- -128 means there is no quit handler
				-- -10000 means the handler did "continue quit"
				if errNum is not -128 and errNum is not -10000 then
					display alert "An error occurred while quitting" message errMsg & " (" & errNum & ")" as critical
				end if
			end try
			
			set isQuitting to false
		end if
		
		return current application's NSTerminateNow
	end applicationShouldTerminate:
	
end script

  1. Export the CocoaAppletAppDelegate.scpt as run only so it overwrites the original CocoaAppletAppDelegate.scpt

  2. Save a text copy of the main.scpt somwhere, keep main.scpt open in applescript Editor, and export it as well as run only, so it overwrites the Contents/Resources/Scripts/man.scpt.

15.) Activate the Finder window, press cmd up arrow twice, so you can see the StopWatchService.app. Then execute it once, by doubleclicking, nothing should happen.

  1. ) Open the Services tab of the shortcuts tab of the keyboard preferences pane. Somewhere there, you should now see an item named Launch StopWatch. Assign this service to a shortcut. I recommend cmd-F4 as the shortcut for the Automator Service if you have checked off for “use function keys” in the keyboard preferences pane of System Preferences.

17.) Try the Shortcut, it should bring the Journal to front in TextEdit, unless you already have altered the configurable properties. If it now works, Congratulations.

18.) Feel free to comment on anything unclear about this procedure, or if it didn’t work for you.

Marks Huntes original post resides here in case you want to see the ‘gold standard’, and he has provided nice screen shots. I have added some extra steps and keys, in order to make it as fast as possible, all faults are mine.

Configuration of the StopWatch script

Stopwatch is configurable:

The script is modelled like a finite-state machine, so it keeps it states (and data for the current timeslip) internally between runs. This also means that you will ruin any states if you open and edit the script while you are currently tracking an event or task.

  • You can specify the path to your journal file.

  • Whether you should get notifications after you have made choices thru the dialog-boxes.

  • Whether the journal file, should be brought to front usolicited, when it is already present in TextEdit.

  • Whether pauses are to be recorded in the journal entry, or if the end time, is to be silently skewed towards the start time.

To make StopWatch as silent as possible

Set the property wantsNotifications to false

set the property JournalToFrontUnsolicited

Now, if it is open, but not the frontmost, then it will update the document, so that what you see it is what is on disk. You will also be sent a notification, you can’t turn off, without modding the code.

To make StopWatch silently subtract the slack from the end-time in the journal

Set journalSlack to false.

Change Journal Heading

Edit the JournalHeading property.

Some notes on how to usage:

It is really just to run it a couple of times to you get the hang of it.

Basically first time you run the script you start it, next time, you’ll either pause or stop it, if you stop it, then you are shown a new dialog, with a “Report”, if you pause the stop watch, then it will show a dialog asking you whether you want to restart the timer, or stop it.

If you choose “Journal” from the last dialog when you have stopped the stopwatch - the end dialog, then the journal is shown to you by TextEdit. If it was open, and you have configured it to show journals unsolicitied when the journal is already open in text edit,
then it is brought forward automatically.

The dialogs contains useful info, that are replicated by notifications, which serve a second purpose, to make it totally clear to you which choice you really did select, so there should never be any doubt. This option can be turned off, and then a notification will only be shown when the journal is updated.

How to not make it interfere much with your current workflow:

How to use StopWatch in your current workflow, to make it unintrusive: When the journal pops up in TextEdit, and that is the only document you have open in TextEdit, or you aren’t working in any documents of TextEdit, then I assume you just press cmd-H, or clicks hide TextEdit from its Application menu. Otherwise, I surmise you press cmd-M “minimize”, or click “Minimize” from TextEdit’s Window menu, to get the window out of your current window setup.

Caveat:
The time is ticking , (or pause) until you do a selection in one of the dialog, If a dialog times out, because you left the dialog open for some reason, then the time just continues ticking, like if you invoked the script, and just hit cancel for seeing how long time that either was lapsed or paused.

b[/b]Leveraging upon the keyboard:
I assume you have “full keyboard access” enabled, and “use function keys” (you have to press fn-F5 to dim the screen brightness). Those settings can be set in the keyboard preferences panel of the System Preferences. Not only lets this you navigate dialogs easily, but now it is also much easier to use the the keystrokes ctrl-F4 and shift-ctrl-F4, that lets you 'cycle through windows. That is, move between windows in the order they were visited, not all apps supports cycle through windows though, then hiding the app you want to move away from is a good second method, -at least this works for me.

Other remarks

A very few apps doesn’t show focus rings around the active button of dialog.

Some apps, like TextWrangler, doesn’t show the focus rings around the active button of a dialog when you try to tab between them, however, you can see it when you tab several times, because you’ll notice when the default button is active. Using that it is really easy to figure out how many times you have to tab to select your choice, by pressing space. (There is really nothing I can do about that.) The other alternative, is to “grab” the mouse, and click the button instead. Cancel is always easy to select, because you can use either cmd-. (command-period), or Esc. (cmd-. is the only thing that works with input dialogs.)

HelpViewer or any other faceless background application with a window, “hides” the dialog

You’ll will not see the dialog, after having invoked it if HelpViewer is frontmost, because HelpViewer is not considered to be an application, but if you change application by hitting command tab once, so you switch to an app, and then hits command-tab to switch app once again, , then you are on the app, that the system deeemed the frontmost when HelpViewer had the front window, and you should see the dialog right in front of you.

This is a kluge, but I haven’t found any way around this, because HelpViewer, is a background application, with no support for UI scripting, which makes it really hard to detect. At least I haven’t found a way to detect it, maybe someone else has.

Intention of posting the code

It is my intent, that besides, having something working to start with, that you can also use this as a basis for your own private solution, serving your needs for a time tracking utility. The code is well commented and should be easy to evolve. (You will have to evolve it a little, in order to painlessly import the file as csv to Filemaker or Excel for instance.

Some words about the code:

First of all there is the finite state machine, which acts like a controller, but the view is also in baked in it, through the different dialoges.

Here is a state diagram, that shows how the different states lead to another, (the script starts in the state “Stopped” that is, “Not Running”.

      [img]https://dl.dropboxusercontent.com/u/6829111/StopWatchStateDiagram.jpeg[/img]

The model is represented first of all by the clockwork, which now has all the computation of time built into it. But the journaling system is also an important part.

I have tried to keep it all as simple as possible, while getting a simple journaling system out of it.

I have commented the code, so it hopefully is easy to modify it, if someone wants to intergrate the journal into a Spreadsheet or database.

I have consciously tested the whole thing, but should someone find an issue, please don’t hesitate, to post the issue, that is very much appreciated.

Thanks and enjoy. :slight_smile:

A small sidenote. When opening a file with write permission the returned file descriptor can be wrong (like when a file can’t be opened due to previous run). So in the error block it’s better to try closing the file by it’s complete path than file descriptor to avoid to have a file opened forever, the whole point of the try block.

That is a very good point!

Thanks DJ! :slight_smile:

Hello.

Implemented the wantsNotifications property to enable disable properties, harnessed the append_to_file handler, Thanks to DJ Bazzie Wazzie. The one play you really or at least I really don’t want something to err, is in a “on error” block! :slight_smile:

Also removed a redundandt notification, and rewrote parts of the “preparateDocWindow” handler.

This works perfectly for me without any notification, as I can just invoke the StopWatch, see how much time has passed, and then hit Esc or command period to cancel the dialog from the keyboard.

Also, the File Read/Write commands can use a parameter constant called ‘eof’, which refers to the insertion point at the end of the file (ie. not to the last byte). So instead of .

-- Get the file length
set flen to (get eof fRef)

-- Append contents to file
write theData to fRef starting at (flen + 1) as «class utf8»

. you could write:

-- Append contents to file
write theData to fRef starting at eof as «class utf8»

Similarly, a command like .

read fRef from 1 to eof as «class utf8»

. would read from the first byte to the end of the file.

Hi,

as theFile is an expanded POSIX path anyway, this is sufficient


	on error
		try
			close access theFile
		end try
		return false
	end try

Hello.

Thanks to Nigel Garvey and StefanK.

I have implemented your changes, I didn’t see the changes Nigel suggested, and Stefan’s I didn’t want to try, I take his word for it to work.

Thanks a lot guys, and I hope you like it! :slight_smile:

I have posted the updated version, (2.8 by now!) along with some other improvements/changes:

Added the start time to the dialogs, so you can always see when a task started (more informative).

Added the option of opening the journal file from the first dialog, if you want quick access to the journal file, (or have forgotten where you stored it, and lothe to have to open the script to see where that was. I hope I did succeed to make an easygoing error message for the case that the journal file doesn’t exist, and the user starts off by clicking the “Open Journal” button. I removed some redundant code from displayJournalFile at the same time.

Hello.

Some very last additions:

1.) I have made the handler refresehJournal more “rational” in order to not disturb the Ui more than necessary, when TextEdit have been hidden.

If TextEdit isn’t visible when you have set JournalToFrontUnsolicited to false, then the journal stays invisible with any other documents, (after that it has been updated). (But, if JournalToFrontUnsolicited is set to true, then that trumps the visisbility of TextEdit.)

The rules that governs the notification that displays when a document is updated is now changed, it now only triggers when we don’t see the updated document: that is, when either TextEdit is hidden, or JournalToFrontUnsolicited is false.

2.) I am a bit paranoid, about saving the script manually after a dialog has timed out from the Service, or the Applet, so I have set a timeout of 320 seconds, and giving up after 319 seconds, if the script sees that the user gave up, then the scripts properties are reset, and we quit.

Hello.

I removed the emergency reset, and the try block from the run handler, as I figured that the best thing was to return buttons with value of cancel where that made sense and Ok when that made sense.

So, if the startDialog times out, then the timing is just cancelled, the two dialogs that can shift state from either running to pause or stop, or from pause to running and stop, just continues in the state they were in, like if the user just hit cancel to get an intermediary result. The endDialog returns Ok, which is fine, and then the journal displays, in this case everything is reset already anyway.

I have also removed some debris.

Hello.

I had to declare the variable gaveUp, up front in the dialog handlers, so that Automator don’t bark, when the script is run from the Service.

Hello.

Thought I’d just mention that in order to get the good looking icon on the StopWatch.app in the Dock:

  • You must open the icns file in the coreservices/resources folder in Preview.
    (The icns file I have in mind is specified in the StopWatch.scpt also in post #1).

  • You must then find the icon with the “right size” and copy it to the clipboard.

  • Open the info panel of the applet, click the applet icon so it turns “bluish”, and paste.

If it doesn’t update in the Dock, if you have put there already, then please remove the applet from the Dock and
then drag the applet with the now good-looking icon onto the Dock.

That’s all.

Thanks. :slight_smile:

Stop Watch Service

Hello

The Automator service I have provided so far, appears to never quit properly, that means, that some of the time, but not all of the time, the worflow service doesn’t quit, you may wish to enter a ‘w’ in the search field of Activity Monitor.

Thanks to Mark Hunte I have found a solution, that doesn’t involve an Automator Service, to create a dedicated StopWatchService with AppleScript.

This is not so complex, yet not totally straight forward, if you are a novice on the subject.

Prequisites: You need to have the StopWatch.app from post #1 on your disk.

1.) Open AppleScript Editor, or Script Editor.

2.) We are going to make a new Cocoa-AppleScript Applet, (File->New From Template)

3.) Select everything in it, and paste in the code below, overwriting everything that was there.
(Change the path to were StopWatch.scpt resides, and any spaces in the filename should be quoted with a ''


-- main.scpt
-- Cocoa-AppleScript Applet
--
-- 2015 McUsr. 
-- Big thanks to Mark Hunte, it is totally stolen from him. Any faults are mine.

property NSWorkspace : class "NSWorkspace"

tell current application's NSApp to setServicesProvider:me
NSUpdateDynamicServices()

my runAService()
on runAService()
		tell me
		do shell script "/usr/bin/osascript /Path/to/StopWatch.scpt >/dev/null 2>&1 &"
		quit
	end tell
end runAService

4.) Compile, then save it as StopWatchService.

5.) Open the sidebar pane, and give it a bundle identifier of

6.) Save again.

7.) Click on the gear icon on the side bar and select “Reveal in Finder”.

8.) Launch Terminal

9.) Enter [b]defaults write /b then drag the info.plist file into the terminal window, so the posix path of it follows what you just typed.

10.) replace the .plist part of the filename, with a space and write: NSServices -array-add ‘{NSMenuItem={default=“Launch StopWatch”;}; NSMessage=“runAService”; NSSendTypes=();}’, then hit enter.

The whole line should look something like this before you hit enter:

11.) Activate the Finder window, press cmd up arrow twice, so you can see the StopWatchService.app. Then execute it once, by doubleclicking, nothing should happen.

  1. ) Open the Services tab of the shortcuts tab of the keyboard preferences pane. Somewhere there, you should now see an item named StopWatchService. Assign this service to a shortcut.

13.) Try the Shortcut.

14.) Feel free to comment on anything unclear about this procedure, or if it didn’t work for you.

Marks Huntes original post resides here in case you want to look it up, and it has nice pictures. :slight_smile:

Hello.

I have corrected a typo, the name of the service should be Launch StopWatch, and not Launch Terminal.

I have also changed the contents of the service, for the hope of gaining some of the speed back, when we lost the memory leakage. I now use an osascript to run the script from the service, and this seems to speed up things a little.

I doesn’t match the WorkflowRunner, but the advantage of not leaking memory, by having many WorkflowServices zombies in the memory trumps that in my opinion.

Hello.

I have updated the applet in post #1, the new applet uses osascript to execute the shell script much like the StopWatchService does. The reason for this is, that the applet hangs if the helpviewer was the most frontmost app.

As it is now, you’ll still not see the dialog if HelpViewer is frontmost, but if you shift application by hitting command tab once, to switch app, and then hits command-tab to switch app once again, you’ll have it right in front of you.

This is a kluge, but I haven’t found any way around this, because HelpViewer, is a background application, with no support for UI scripting, which makes it really hard to detect. At least I haven’t found a way to detect it, maybe someone else has.

I have also bumped the version of the script to “3.1.1”, but all I have done, has really been adding elipsis (.) where the “Stop” button will invoke a new dialog, -just to be conformant. :slight_smile:

Hello.

I have updated the StopWatch script in post #1.

I have asserted that time strings will allways show up in 24h format, which is logical when we deal with stopwatches really, and besides that, makes the code simpler, with respect to formatting, since the length of time strings varies. I have added some other formatting to the dialogs as well, with the hope that they are more informative, besides prettier.
I have also changed the wording of the dialogs, with the hope that the new wording is better.

Enjoy.

Hello.

I had forgotton that there weren’t any cancel dialog in the endbutton, when I made and implemented the dialog3button handler…

That is now fixed.

Hello.

I have completely rewritten the documentation, after a couple of days, I am going to delete this whole thread, and post the first post in new and pristine conditions. :slight_smile:

Hello.

I have tried to make the StopWatchService as wee bit snappier. First of all I have added an item specifiying a port name, that you may wish to paste into the Services array of the info.plist file of StopWatchService, with TextWrangler:

If you have the tools for it, then the the human readable form of the name is “Incoming service port name”, and it should have a value of StopWatchService.

The next step is to replace the CocoaAppletAppDelegate script in the Resouces folder of StopWatchService with the code below, and then make it run only ( Export as Run-Only from the File menu of Script Editor).

Warning:
Your script editor may most probably hang of this step, I think it usually goes well, that all files get to be restored after you have force quit Script Editor, but then again, it may not, so be sure to have saved any unsaved files, before you try to overwrite the existing CocoaAppleAppDelegate.


script CocoaAppletAppDelegate
	property parent : class "NSObject"
	property mainScript : missing value -- the applet's main.scpt
	property isQuitting : false -- re-entrancy guard: true = in the process of quitting
	
	on applicationWillFinishLaunching:aNotification
		-- Insert code here to initialize your application before any files are opened 
		
		-- Emulate an OSA Applet: Load the main script from the Scripts resource folder.
		try
			set my mainScript to load script (path to resource "main.scpt" in directory "Scripts")
		on error errMsg number errNum
			-- Perhaps this should silently fail if it can't load the script; that way, a Cocoa applet
			-- can just have Cocoa classes and no main.scpt.
			display alert "Could not load main.scpt" message errMsg & " (" & errNum & ")" as critical
		end try
	end applicationWillFinishLaunching:
	
	on applicationDidFinishLaunching:aNotification
		-- Insert code here to do startup actions after your application has initialized
		
		if mainScript is missing value then return
		
		-- Emulate an OSA Applet: Invoke the "run" handler.
		
		-- If we have already opened files during startup, don't invoke the run handler.
		
		try
			tell mainScript to run
		on error errMsg number errNum
			if errNum is not -128 then
				display alert "An error occurred while running" message errMsg & " (" & errNum & ")" as critical
			end if
		end try
		
		-- TODO: Read the applet's "stay open" flag and quit if it's false or unspecified.
		-- For now, all Cocoa Applets stay open and require the run handler to explicitly quit,
		-- which is arguably more correct for a Cocoa application, anyway.
		(* if not shouldStayOpen then
			quit
		end if *)
	end applicationDidFinishLaunching:
	
	on applicationShouldTerminate:sender
		-- Insert code here to do any housekeeping before your application quits 
		
		-- Guard against re-entrancy.
		if not isQuitting and mainScript is not missing value then
			set isQuitting to true
			
			-- Emulate an OSA Applet: Invoke the "quit" handler; if the handler returns, it has fully
			-- handled the quit message and we should not quit, otherwise, it calls "continue quit",
			-- which returns error -10000.
			try
				tell mainScript to quit
				set isQuitting to false
				
				return current application's NSTerminateCancel
			on error errMsg number errNum
				-- -128 means there is no quit handler
				-- -10000 means the handler did "continue quit"
				if errNum is not -128 and errNum is not -10000 then
					display alert "An error occurred while quitting" message errMsg & " (" & errNum & ")" as critical
				end if
			end try
			
			set isQuitting to false
		end if
		
		return current application's NSTerminateNow
	end applicationShouldTerminate:
	
end script

The last step is to have the main script open in script editor, and export that too, (from the File menu) so that it overwrites the main.scpt that resides in StopWatchService/Contents/Resources/Scripts.

I am really sorry that that I came up with this so late, but it was only later that I realized I had to make it a tad snappier. I really recommend you do the modding, because the snappiness, also decreases the chance of a port time out, if the app that is in foreground are under a heavy load, when you hit the short cut key.
(Services are routed through the foreground app.)

And I am even more sorry for having forgotten to warn you about a hanging ScriptEditor, when you save the CocoAplletAppDelegate script. I have added a warning about that in the post above.

Hello.

I have removed a small bug in the fmt24HTime handler: I tested for hours less than 12, I should of course have tested for hours less than 10. I am sorry for the inconvenience.