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:
-
Find the icon, and paste it in, like you did for the StopWatch app in that installment above.
-
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
-
Export the CocoaAppletAppDelegate.scpt as run only so it overwrites the original CocoaAppletAppDelegate.scpt
-
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.
- ) 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.