Friday, November 17, 2017

#1 2015-04-13 07:01:53 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

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.

           StopWatch.jpeg

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.

Applescript:


# Copyright © 2015 McUsr, you may not post this as a work of your own on some webpage, or in a book.
# [url]http://macscripter.net/viewtopic.php?pid=180130#p180130[/url]
(*
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.

<key>LSUIElement</key>
    <true/>


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.

Applescript:


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.)

Applescript:


-- 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

net.mcusr.StopWatch.StopWatchService


6.) Save again.

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

8.) Launch Terminal

9.) Enter defaults write (space) 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:

/usr/bin/defaults write /Users/you/Applications/Somewhere/StopWatchService.app/Contents/Info NSServices -array-add '{NSMenuItem={default="Launch StopWatch";}; NSMessage="runAService";  NSPortName="StopWatchService"; NSSendTypes=();}'


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

12) 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.

Applescript:


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

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

14) 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.

16. ) 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.


(i)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".

          StopWatchStateDiagram.jpeg

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. smile

Last edited by McUsrII (2015-04-20 05:08:06 am)


Filed under: StopWatch

Offline

 

#2 2015-04-14 02:41:36 am

DJ Bazzie Wazzie
Member
From:: the Netherlands
Registered: 2004-10-20
Posts: 2724
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#3 2015-04-14 04:29:36 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

DJ Bazzie Wazzie wrote:

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! smile

Offline

 

#4 2015-04-14 05:23:12 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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! 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.

Last edited by McUsrII (2015-04-14 05:24:40 am)

Offline

 

#5 2015-04-14 05:49:09 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4420

Re: A journaling stop watch for mundane tasks and time slips

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 …

Applescript:

-- 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:

Applescript:

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

Similarly, a command like …

Applescript:

read fRef from 1 to eof as «class utf8»

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


NG

Offline

 

#6 2015-04-14 05:58:06 am

StefanK
Member
From:: St. Gallen, Switzerland
Registered: 2006-10-21
Posts: 11482
Website

Re: A journaling stop watch for mundane tasks and time slips

McUsrII wrote:

Applescript:


…
   on error
       try -- harnessing, thanks to DJ. Bazzie Wazzie
           if theFile starts with "/" then
               close access POSIX file theFile
           else
               close access file theFile
           end if
       end try
       return false
   end try
…


Hi,

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

Applescript:


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


regards

Stefan

Offline

 

#7 2015-04-14 08:16:52 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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! 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.

Last edited by McUsrII (2015-04-14 08:18:25 am)

Offline

 

#8 2015-04-14 01:29:14 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#9 2015-04-14 01:48:09 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Last edited by McUsrII (2015-04-14 01:48:31 pm)

Offline

 

#10 2015-04-14 01:57:06 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#11 2015-04-15 04:38:14 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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. smile

Offline

 

#12 2015-04-15 08:47:45 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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 '\'

Applescript:


-- 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

net.mcusr.StopWatch.StopWatchService


6.) Save again.

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

8.) Launch Terminal

9.) Enter defaults write (space) 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:

/usr/bin/defaults write /Users/you/Applications/Somewhere/StopWatchService.app/Contents/Info NSServices -array-add '{NSMenuItem={default="Launch StopWatch";}; NSMessage="runAService"; NSSendTypes=();}'


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.

12. ) 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. smile

Last edited by McUsrII (2015-04-16 12:38:19 pm)

Offline

 

#13 2015-04-15 01:42:23 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#14 2015-04-15 04:32:49 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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. smile

Last edited by McUsrII (2015-04-15 04:35:04 pm)

Offline

 

#15 2015-04-16 10:40:50 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#16 2015-04-16 12:36:02 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Last edited by McUsrII (2015-04-16 12:36:26 pm)

Offline

 

#17 2015-04-17 08:56:38 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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. smile

Offline

 

#18 2015-04-19 03:02:20 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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:

<key>NSPortName</key>
            <string>StopWatchService</string>


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.

Applescript:


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.)

Last edited by McUsrII (2015-04-19 11:15:47 am)

Offline

 

#19 2015-04-19 11:15:32 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#20 2015-04-20 05:09:31 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

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.

Offline

 

#21 2015-04-20 05:59:37 am

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

Re: A journaling stop watch for mundane tasks and time slips

Hello.

I have written a script to configure the info.plist file of an app, in order to configure it as a service, I have added both! properties for making it into a background only app, and a user agent. This for the hope that at least one of them, sees to, that launchd skips the part of the launch process that loads UI.

Other than that: You can only run this script once per propertylist file. And you should really duplicate the info.plist file first, so you have a backup, should something go wrong. (just duplicate it in Finder. Of course have read Mark Hunte's post about this, -which I refer to in post nr 1.

Install

This script must be installed into the script menu, and you should change the name to "Script Editor" if on Yosemite.

Usage

See to that the application that you want to configure is the frontmost, and then run this script, after you have configured it with the values you want to use.

smile

Applescript:

-- Copyright 2015 © McUsr
-- Copyright 2015 © McUsr
# You may not post, or print this somewhere else, but refer to the link where you found it.
property scriptTitle : "Configure Applet as Service"
-- This script is, as it stands just able to be run on once, it doesn't change any already
-- existing values, it probably will ruin the whole plist file, so it is a good thing to have
-- made a copy up front.

property serviceName : "Launch Mail"
-- Above is the name of the service as it should turn up in any services menu.
property serviceMessage : "runAService"
-- Above is the name of the handler in your applet that is the services run-handler.
property servicePortName : "LaunchMail"
-- above is the name of the service portname the service should be called with.
-- I think it wise to call it the same as the Application
property dryRun : false


tell application "AppleScript Editor"
   if serviceName = "Launch Console" and servicePortName = "LaunchConsole" then
       display dialog "It appears that you haven't edited the original values of the script, are you sure you want to enter those values into an info.plist of an applet you intend to use as a service?" with title my scriptTitle with icon 2
   end if
   
   try
       -- Check to see if the front document has been saved as a bundle application
       try
           set the doc_properties to the (properties of the front document)
           set the script_path to the path of doc_properties
           set appletName to name of doc_properties
           set the plist_path to script_path & "/Contents/Info.plist"
           set the plist_file to plist_path as POSIX file as alias
       on error
           error "The front script document has not be saved as a Script bundle or as an Application bundle."
       end try
       
       
       -- Query the user
       
       set the dialog_message to "This scripts sets necessary for an application based on the cocoa-applet to be configured as a Service.
You have service name to : -> "
& my serviceName & " <-,
the service message to: -> "
& my serviceMessage & " <-,
the service port name to : -> "
& my servicePortName & " <-" & return & "
Are those the properterties you want to use on: -> "
& appletName & " <- , for it to become a Service?"
       set {button returned:btn} to (display dialog dialog_message with title my scriptTitle buttons {"No", "Yes"} cancel button 1 default button 2 with icon 2)
       
       if btn = "Yes" then
           tell application "System Events"
               -- create an empty property list dictionary item
               set the parent_dictionary to make new property list item with properties {kind:record}
               -- create new property list file using the empty dictionary list item as contents
               if my dryRun then
                   set the plistfile_path to "~/Desktop/ServiceExample.plist"
                   
                   set this_plistfile to ¬
                       make new property list file with properties {contents:parent_dictionary, name:plistfile_path}
               else
                   set this_plistfile to property list file plist_path
               end if
               if (exists property list item "NSServices" of contents of this_plistfile) then
                   display alert my scriptTitle message "The property list already contains an NSServices item, I quit" buttons {"Cancel"} cancel button 1
               end if
               make new property list item at end of property list items of contents of this_plistfile ¬
                   with properties {kind:boolean, name:"LSUIElement", value:true}
               
               make new property list item at end of property list items of contents of this_plistfile ¬
                   with properties {kind:string, name:"LSBackgroundOnly", value:"1"}
               
               
               set servicesArray to make new property list item at end of property list items of contents of this_plistfile ¬
                   with properties {kind:list, name:"NSServices"}
               set servicesDict to make new property list item at end of property list items of contents of servicesArray ¬
                   with properties {kind:record}
               set menuItemDict to make new property list item at end of property list items of contents of servicesDict ¬
                   with properties {kind:record, name:"NSMenuItem"}
               make new property list item at end of property list items of contents of menuItemDict ¬
                   with properties {kind:string, value:my serviceName, name:"default"}
               
               -- Inserting into Services dict
               make new property list item at end of property list items of contents of servicesDict ¬
                   with properties {kind:string, value:my serviceMessage, name:"NSMessage"}
               make new property list item at end of property list items of contents of servicesDict ¬
                   with properties {kind:string, value:my servicePortName, name:"NSPortName"}
               -- creating the empty array:
               make new property list item at end of property list items of contents of servicesDict ¬
                   with properties {kind:list, name:"NSSendTypes"}
               
           end tell
       end if
       
       display notification appletName & " configured to be a Service." with title scriptTitle
       
   on error error_message number error_number
       if the error_number is not -128 then
           display dialog error_message with title scriptTitle buttons {"Cancel"} default button 1 with icon 2
       end if
   end try
end tell

Last edited by McUsrII (2015-04-20 06:11:52 am)

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)