Updated 2016-11-16 18:07:30 by EMJ

EKB This is a simple task timer I built using clock.tcl (last updated 15 Dec. 2005). Here's what it looks like:

It's set to be always on top, at least on Windows. It combines a clock with a timer. The "copy to clipboard" feature pastes the date, start time, and time elapsed (in hours) to the clipboard, separated by tabs, then resets the timer:
 Tue, 13 Sep 2005        17:30        0.25

The reason for this is that it can then be pasted into a table (e.g., TkTable, Excel, OpenOffice.org Calc) for easy manipulation. It can also be pasted straight into a text file. I then type in the project & task information, and keep going.

I use this to keep track of my billed time. Here's a typical workflow:

  1. Click the "start/pause/stop" button when starting a task.
  2. Click the "start/pause/stop" to pause & restart when taking a break.
  3. When done with the task, click "paste to clipboard" and paste into log.

If there's an error, then click "reset" to reset the timer.

Here's the code:
 ######################################################
 ##
 ## Always on top, no resize
 ##
 ######################################################
 wm attributes . -topmost 1
 wm resizable . 0 0
 
 ######################################################
 ##
 ## Buttons
 ##
 ######################################################

 image create photo appclock16 -data {
   R0lGODlhEAAPAIQAAPwCBARCZKze7Kza5IzK3ITC1KTS5Hy2zGyuxGSmvFyW
   tFyavEyStEyOrESGrDx+pDRynCRqlBxejAAAAAAAAAAAAAAAAAAAAAAAAAAA
   AAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQAA8AAAVOICCKQTCe
   KBAIgpmiwUAM7ksORUHbpHH8LV6AgCgidq/AIcFkImEJhWIxXdROAQWD0eA2
   rqNAw0Emg8OPdPochrgh7HBkHg9LJHVs3R4CACH+aENyZWF0ZWQgYnkgQk1Q
   VG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4g
   QWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29t
   ADs=
 }
 
  image create photo apppause16 -data {
   R0lGODlhDwAPAIMAAPwCBARCZKze7JTO3Hy2zITC1GyqxGSmvFyavGSixEyO
   tESGrDx+pDRynAAAAAAAACH5BAEAAAAALAAAAAAPAA8AAARFEMhJq7016KlD
   FqAXgILHDQQhoioVFIYhwrJLGId4566BICIf0JVQKETFowuxWIiYTpeCwRBN
   qy5GoyHScjMbSQdDLl8iACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZl
   cnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyBy
   ZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=
 }
 
 image create photo record16 -data {
   R0lGODlhEAAQAIUAAIx+bIx6bIR6bIRyZHxyZHxuXHRqXPwCBOTe3Nze3HRm
   XIR2ZPz+/GxiVPz29GRaTGReVFxWTHxuZPzy5Ly2rFxSRPTq3OzexLyulFRK
   RKyqrPTizLyihExKRMTCxGxeVOzWvERCPFxSTOzSrLyabLSytGRaVOTKpEQ+
   NLy+vMS+tMS2nLymhDw2NExGPMSulMSqjDw6NDQ2LAAAAAAAAAAAAAAAAAAA
   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAcALAAAAAAQABAAAAal
   QEBAMCAUDIekUhlAJBJOhWK5XCQYWEbD8aAmB9csBBKJeCVhhmOSoFQeR6Uh
   vLZcMBlNQppUINQTFhsXHB0eDB9cBw0JdYMgHCGHHw8PIh8JgRcXICMkkmsJ
   JRUmCYKcIyckKCmBG3gRKhQrGBy2qyWnLB0iFRkdHSEhKC0tJYMjkV4uGikl
   JajKVBGmj6mrXrErKy8wLCQkMV4VFcDCKCEtMn5BACH+aENyZWF0ZWQgYnkg
   Qk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5
   OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3Iu
   Y29tADs=
 }
 
 ######################################################
 ##
 ## "Minimalist" balloon help from Tcler's Wiki
 ##
 ## With modest change so that there isn't much delay
 ## while browsing over buttons.
 ##
 ######################################################

 namespace eval balloon {
    variable long_delay 750
    variable short_delay 50
    variable delay $long_delay
    variable family {}
 }

 bind . <Enter> {
    if {$balloon::family != ""} {
        if {[lsearch -exact $balloon::family %W] == -1} {
            set balloon::family {}    
            set balloon::delay $balloon::long_delay
        }
    }
 }

 proc set_balloon {w help} {
    bind $w <Enter> "+after \$balloon::delay [list balloon::show %W [list $help]]; set balloon::delay $balloon::short_delay; set balloon::family \[balloon::getwfamily %W\]"
    bind $w <Leave> "+destroy %W.balloon"
 }

 # Add these to the namespace
 proc balloon::getwfamily {w} {
    return [winfo children [winfo parent $w]]
 }

 proc balloon::show {w arg} {
    if {[eval winfo containing  [winfo pointerxy .]]!=$w} {return}
    set top $w.balloon
    catch {destroy $top}
    toplevel $top -bd 1 -bg black
    wm attributes $top -topmost 1
    wm overrideredirect $top 1
    if {$::tcl_platform(platform) == "macintosh"} {
        unsupported1 style $top floating sideTitlebar
    }
    pack [message $top.txt -aspect 10000 -bg lightyellow -padx 1 -pady 0 \
            -text $arg]
    set wmx [expr [winfo rootx $w]+5]
    set wmy [expr [winfo rooty $w]+[winfo height $w]+7]
    wm geometry $top \
      [winfo reqwidth $top.txt]x[winfo reqheight $top.txt]+$wmx+$wmy
    raise $top
 }


 ######################################################
 ##
 ## Timer procs
 ##
 ######################################################
 proc every {ms body} {
    eval $body
    after $ms [list every $ms $body]
 }
 
 proc timeElapsed {sec} {
    set hours [expr {$sec / 3600}]
    set mins [expr {($sec / 60) % 60}]
    return [format "%02d:%02d" $hours $mins]
 }
 
 # Are we timing, or not?
 set timer(state) 0
 # Has the timer been reset?
 set timer(reset) 1
 # Store previous time elapsed
 set timer(prev) 0
 proc toggleTimer {b} {
    if {$::timer(state)} {
        $b config -relief flat
        $b config -image appclock16
        set ::timer(state) 0
        set ::timer(prev) [expr {$::timer(prev) + [clock sec] - $::timer(init)}]
    } else {
        $b config -relief sunken
        $b config -image apppause16
        if {$::timer(reset)} {
            set ::timer(reset) 0
        }
        set ::timer(init) [clock sec]
        set ::timer(state) 1
    }
 }
 proc resetTimer {b} {
    set ::timer(reset) 1
    set ::timer(elapsed) "00:00"
    set ::timer(init) [clock sec]
    set ::timer(prev) 0
 }
 proc recordTime {b} {
    # Stop timing
    if {$::timer(state)} {
        toggleTimer $b
    }
    # Save info
    set date [clock format [clock sec] -format "%a, %d %b %Y"]
    set start "--:--"
    if [info exists ::timer(init)] {
        set start [fmtTime $::timer(init)]
    }
    set elapsed [unfmtTime $::timer(elapsed)]
    # Reset the timer
    resetTimer $b
    
    clipboard clear
    clipboard append -type STRING "$date\t$start\t$elapsed"
 }
 proc fmtTime {sec} {
    return [clock format $sec -format %H:%M]
 }
 
 # unfmtTime: Take H:M and return decimal hours
 proc unfmtTime {s} {
    set t [split $s ":"]
    set hr [string trimleft [lindex $t 0] "0"]
    if {$hr == ""} {set hr 0}
    set min [string trimleft [lindex $t 1] "0"]
    if {$min == ""} {set min 0}
    set hr [expr {0.01 * round(100 * ($hr + (1.0 * $min)/60))}]
    
    return [format "%.2f" $hr]
 }
 proc updateTimer {} {
    set ::timer(now) [fmtTime [clock sec]]
    if {$::timer(state)} {
        set elapsed_sec [expr {$::timer(prev) + [clock sec] - $::timer(init)}]
        set ::timer(elapsed) [timeElapsed $elapsed_sec]
    }
 }
 
 ## -- Top frame
 pack [frame .top] -side top
 pack [label .top.l -textvar timer(now) -font {Tahoma 18}] -side left -padx 6
 
 # Buttons
 pack [frame .top.buttons] -side right -padx 6
 set clockbutton [button .top.buttons.t -image appclock16 -relief flat \
    -width 16]
 pack $clockbutton -side left
 set recordbutton [button .top.buttons.rec -image record16 -relief flat]
 pack $recordbutton -side left
 
 ## -- Bottom frame
 pack [frame .bottom] -side bottom
 pack [label .bottom.l -textvar timer(elapsed) -font {Tahoma 10}]
 
 $clockbutton config -command "toggleTimer $clockbutton"
 set_balloon $clockbutton "Start/stop timer"
 $recordbutton config -command "recordTime $clockbutton"
 set_balloon $recordbutton "Clear and save to clipboard"

 ######################################################
 ##
 ## Start program!
 ##
 ######################################################
 set timer(elapsed) "00:00"
 every 1000 updateTimer

EKB With the magic of Freewrap, this is now available to the world as a Windows executable: [1]

Thanks for writing this app. I've found it really useful. It's just so cool how contributions feed on each other: some simple test code, someone expands it for usefulness, and then releases it to the world.

EKB That's great! Thanks for posting this.

see also multiple timers

[bigote] - 2009-07-07 20:05:33

Hello, I'm quite a beginner in Tcl and it's the first time I write here so please forgive me if I do something wrong :)

With your permission I would like to share my small modification made to the Task Timer. It's a cosmetic change but now it looks like a WindowMaker applet. Well, I suppose if there's something wrong with the font size on your system, it wouldn't, but it could be corrected easily changing font face and/or size in the lines
set cur_timer [label .top.l -textvar timer(now) -font {Arial 8 bold}]
and
pack [label .top.timer.l -textvar timer(elapsed) -font {Tahoma 8}]

I even use this newly baked applet instead of wmcalclock. I just added another tooltip that shows the current date when pointing the mouse at the current time. I also had to adapt the code to Linux environment (added command package require Tk and commented out two identic lines wm attributes . -topmost 1).

So here goes the diff. You can copy and paste it into a file (for instance, patch.diff) and then apply the patch to the source file (let it be tasktimer.tcl) using Unix patch command after placing patch.diff next to it:
patch -p0 < patch.diff

Here you have a Windows version of patch.
--- tasktimer.tcl      2009-07-08 01:43:56.000000000 +0200
+++ ttmr.tcl    2009-07-08 01:47:37.000000000 +0200
@@ -1,9 +1,25 @@
+#!/bin/sh
+# the next line restarts using wish \
+exec wish "$0" "[email protected]" $argv;# -geometry +800+0
+
+#This program is written by EKB http://wiki.tcl.tk/13772
+#Its code can be found at http://wiki.tcl.tk/14707
+
+#This modification is made by Serge Yudin (xmpp:[email protected])
+#and consists of geometry changes (now it looks like a wm-applet).
+#Also a tooltip added that shows the current date when pointing
+#the cursor at the current time. The window size could vary depending 
+#on font size. Please hack it yourself if it's too big or too small.
+
+package require Tk
  ######################################################
  ##
  ## Always on top, no resize
  ##
  ######################################################
- wm attributes . -topmost 1
+#This line is commented out because it gave me an error 
+#when running in Linux. (SY)
+# wm attributes . -topmost 1
  wm resizable . 0 0
 
  ######################################################
@@ -87,7 +103,7 @@
     set top $w.balloon
     catch {destroy $top}
     toplevel $top -bd 1 -bg black
-    wm attributes $top -topmost 1
+#    wm attributes $top -topmost 1
     wm overrideredirect $top 1
     if {$::tcl_platform(platform) == "macintosh"} {
         unsupported1 style $top floating sideTitlebar
@@ -164,7 +180,7 @@
     clipboard append -type STRING "$date\t$start\t$elapsed"
  }
  proc fmtTime {sec} {
-    return [clock format $sec -format %H:%M]
+    return [clock format $sec -format %H:%M:%S]
  }
 
  # unfmtTime: Take H:M and return decimal hours
@@ -188,10 +204,10 @@
 
  ## -- Top frame
  pack [frame .top] -side top
- pack [label .top.l -textvar timer(now) -font {Tahoma 18}] -side left -padx 6
-
+ set cur_timer [label .top.l -textvar timer(now) -font {Arial 8 bold}]
+ pack $cur_timer -side top -padx 2
  # Buttons
- pack [frame .top.buttons] -side right -padx 6
+ pack [frame .top.buttons] -side bottom -padx 6
  set clockbutton [button .top.buttons.t -image appclock16 -relief flat \
     -width 16]
  pack $clockbutton -side left
@@ -199,13 +215,14 @@
  pack $recordbutton -side left
 
  ## -- Bottom frame
- pack [frame .bottom] -side bottom
- pack [label .bottom.l -textvar timer(elapsed) -font {Tahoma 10}]
+ pack [frame .top.timer] -side top
+ pack [label .top.timer.l -textvar timer(elapsed) -font {Tahoma 8}]
 
  $clockbutton config -command "toggleTimer $clockbutton"
  set_balloon $clockbutton "Start/stop timer"
  $recordbutton config -command "recordTime $clockbutton"
  set_balloon $recordbutton "Clear and save to clipboard"
+ set_balloon $cur_timer "[clock format [clock seconds] -format {%A, %d of %B %Y}]"
 
  ######################################################
  ##

LV - 2016-11-16 13:11:44

So I wanted to run the original timer code on this page on a Windows 7 system. I downloaded a recent tclkit from the tclkit building site. I copied the code from the top of this page into a text file. I invoked the tclkit and the text file from within Cygwin on my desktop. A widget appeared. It shows the correct time. But when I press the play button, the numbers where I would expect to see elapsed time does not change. I added a puts statement to output the $::timer(now) and $::timer(elapsed) values, and elapsed never changes even though now does. The timer state array entry changes when the play button is pressed, and the icon goes from flat to sunken. The calculation of $elapsed_sec is occurring as expected. But the surprise, to me, is that the timer only measures hours and minutes. You have to fiddle with the code to add seconds to the elapsed timer formatting in the widget.

Just in case someone tries the code and wonders why it doesn't appear to be doing anything initially.

EMJ 2016-11-16: It's for tracking billable time, seconds don't matter, the value calculated for ::timer(elapsed) rounds down in minutes, so it only changes after a minute has passed. In other words, works as designed.