Add time tracking scripts.
authorDan White <dan@whiteaudio.com>
Wed, 2 Feb 2011 17:18:06 +0000 (11:18 -0600)
committerDan White <dan@whiteaudio.com>
Wed, 2 Feb 2011 17:18:06 +0000 (11:18 -0600)
TimeSampleHistogram [new file with mode: 0755]
timeSampler [new file with mode: 0755]

diff --git a/TimeSampleHistogram b/TimeSampleHistogram
new file mode 100755 (executable)
index 0000000..dfb5702
--- /dev/null
@@ -0,0 +1,264 @@
+#!/usr/bin/env python
+
+# Present histogram reports from timeSampler data.  See "$0 -h" for more
+#
+# (C) Dan White <dan@whiteaudio.com>
+
+# TODO add in-file and cmd line opt for sample spacing (10min vs min)
+
+import datetime as dt
+import optparse
+import os
+import sys
+import re
+
+from collections import defaultdict
+from math import floor, log10
+
+
+#import pylab
+
+MAX_NAME_LEN = 15
+PRINT_WIDTH = 80
+
+RE_SAMPLE = re.compile(r'^(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d) (?P<hour>\d\d):(?P<minute>\d\d)\s+(?P<text>.*)$')
+
+RE_M = re.compile(
+    r'^([a-z]{2})(\d+|[A-Z][a-z]+)(\d+|[A-Z][a-z]+)(\d+|[A-Z][a-z]+)')
+
+parser = optparse.OptionParser()
+parser.add_option('-s', '--start', dest='startdate',
+                  help='include dates starting with YYYY-MM-DD', metavar='DATE')
+parser.add_option('-e', '--end', dest='enddate',
+                  help='include dates thru YYYY-MM-DD', metavar='DATE')
+parser.add_option('-d', '--days', dest='days',
+    help='include NUM days after --start, or if negative, NUM before --end',
+                  metavar='NUM', type='int', default=0)
+parser.add_option('-t', '--today', dest='today', action='store_true',
+                  help='show today only', default=False)
+parser.add_option('-y', '--yesterday', dest='yesterday', action='store_true',
+                  help='show yesterday only', default=False)
+parser.add_option('-w', '--width', dest='width', type='int',
+                  help='width of report', metavar='W', default=PRINT_WIDTH)
+parser.add_option('-n', '--numer', dest='numsort', action='store_true',
+                  help='sort by ascending number of occurences', default=False)
+parser.add_option('-r', '--reverse', dest='reverse', action='store_true',
+                  help='reverse sort', default=False)
+parser.add_option('-g', '--gtd', dest='gtd', action='store_true',
+                  help='show GTD category counts', default=False)
+parser.add_option('-b', '--bare', dest='bare', action='store_true',
+                  help='show histogram only', default=False)
+parser.add_option('-W', '--isoweek', dest='isoweek',
+                  help='only show ISO [YYYY-]WK', default=None)
+(opt, args) = parser.parse_args()
+
+#testing
+#opt.startdate = '2010-01-01'
+#opt.enddate = '2010-01-15'
+#opt.numsort = True
+#opt.reverse = True
+#opt.gtd = True
+#opt.yesterday = True
+#opt.today = True
+#testing
+
+# these are from
+# http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar/1700069#1700069
+def iso_year_start(iso_year):
+    "The gregorian calendar date of the first day of the given ISO year"
+    fourth_jan = dt.date(iso_year, 1, 4)
+    delta = dt.timedelta(fourth_jan.isoweekday()-1)
+    return fourth_jan - delta 
+
+def iso_to_gregorian(iso_year, iso_week, iso_day):
+    "Gregorian calendar date for the given ISO year, week and day"
+    year_start = iso_year_start(iso_year)
+    return year_start + dt.timedelta(iso_day-1, 0, 0, 0, 0, 0, iso_week-1)
+
+#
+# Date range filter
+#
+LONG_AGO = dt.datetime(1900, 1, 1)
+if not opt.startdate:
+    startdate = LONG_AGO
+else:
+    #convert from YYYY-MM-DD
+    startdate = dt.date(*map(int, opt.startdate.split('-')))
+
+if not opt.enddate:
+    enddate = dt.date.today()
+else:
+    #convert from YYYY-MM-DD
+    enddate = dt.date(*map(int, opt.enddate.split('-')))
+
+if opt.today and opt.yesterday:
+    startdate = dt.date.today() - dt.timedelta(1)
+    enddate = dt.date.today()
+elif opt.today:
+    startdate = enddate = dt.date.today()
+elif opt.yesterday:
+    startdate = enddate = dt.date.today() - dt.timedelta(1)
+
+# handle -d, --days option
+# if supplied, modify either startdate or enddate accordingly
+if opt.days > 0:
+    enddate = startdate + dt.timedelta(opt.days)
+elif opt.days < 0:
+    startdate = enddate + dt.timedelta(opt.days)
+
+if opt.isoweek:
+    yw = opt.isoweek.split('-')
+    if len(yw) == 1:
+        year = dt.datetime.now().year
+        week = int(yw[0])
+    if len(yw) > 1:
+        year = int(yw[0])
+        week = int(yw[1])
+
+    startdate = iso_to_gregorian(year, week, 1)
+    enddate = iso_to_gregorian(year, week, 7)
+
+
+
+# modify to include all of enddate instead of start-of-day
+startdate = dt.datetime.combine(startdate, dt.time.min)
+enddate = dt.datetime.combine(enddate, dt.time.max)
+
+#
+# Sorting options
+#
+if opt.numsort:
+    def sorter(x, y, reverse=opt.reverse):
+        m = -1 if reverse else 1
+        return m * cmp(x[1], y[1])
+else:
+    # alphabetical on key
+    def sorter(x, y, reverse=opt.reverse):
+        m = -1 if reverse else 1
+        return m * cmp(x[0].lower(), y[0].lower())
+
+
+
+#
+# read and parse timeSampler files
+#
+files = [f for f in os.listdir(os.environ['HOME'])
+         if f.startswith('.timeSampler') and not f.endswith('.gz')]
+
+hist = defaultdict(int(1))
+histHier = defaultdict(int(1))
+histGtd = defaultdict(int(1))
+histOther = defaultdict(int(1))
+for file in files:
+    f = open(os.environ['HOME'] + '/' + file, 'r')
+    for line in f.readlines():
+        if line.startswith('#'):
+            #comment, skip
+            continue
+
+        m = RE_SAMPLE.match(line)
+        try:
+            r = m.groups()[:-1]
+        except:
+            print line
+            raise
+        ri = [int(i) for i in r]
+        d = dt.datetime(*ri)
+        t = m.group('text')
+
+        if not (d >= startdate and d <= enddate):
+            continue
+        if t == 'null':
+            continue
+
+        hist[t] += 1
+
+        #task hierarchy
+        h = RE_M.search(t)
+        if h:
+            pass
+            #print h.groups()
+
+        # segregate non-gtd-project items
+        if len(t) < 3 or t[2] != '.':
+        #if t.lower() == t and t[0:2] != 'ed' or t == 'Tasks':
+            histOther[t] += 1
+        else:
+            histGtd[t[0:2]] += 1
+
+if hist:
+    maxlen = max(map(len, hist.iterkeys()))
+    maxnum = max(hist.itervalues())
+else:
+    maxlen = maxnum = 1
+
+name_len = min(maxlen, MAX_NAME_LEN)
+count_len = floor(log10(maxnum) + 1)
+
+# Tic-per-count or scale all tics proportionally
+maxtics = opt.width - name_len - count_len - 3 #magic num from line format 'ps'
+if maxnum > maxtics:
+    def tics(n):
+        return int(maxtics*(float(v)/maxnum))
+else:
+    def tics(n):
+        return n
+
+#format
+ps = '%%-%is (%%%ii)%%s' % (name_len, count_len)
+
+#
+# Show date range
+#
+if not opt.bare:
+    if opt.today and opt.yesterday:
+        print 'Today (%s) and yesterday' % startdate.strftime('%Y-%m-%d')
+
+    elif opt.yesterday:
+        if opt.days:
+            print 'From:', startdate.strftime('%Y-%m-%d'), ' Thru',
+
+        print 'Yesterday:', startdate.strftime('%Y-%m-%d')
+
+    elif opt.today:
+        if opt.days:
+            print 'From:', startdate.strftime('%Y-%m-%d'), ' Thru',
+
+        print 'Today:', startdate.strftime('%Y-%m-%d')
+
+    elif startdate > LONG_AGO:
+        print 'From:', startdate.strftime('%Y-%m-%d'), ' Thru:', enddate.strftime('%Y-%m-%d')
+
+    elif enddate:
+        print 'Thru:', enddate.strftime('%Y-%m-%d')
+
+    print '-' * opt.width
+
+
+#
+# Display the histogram
+#
+for k,v in sorted(hist.iteritems(), cmp=sorter):
+    if v < 1: continue
+
+    if len(k) > MAX_NAME_LEN:
+        k = k[:MAX_NAME_LEN-1] + '~'
+
+    print ps % (k, v, '+'*tics(v))
+
+if not opt.bare:
+    print ('%%%is' % (name_len + count_len + 2)) % sum(hist.values())
+    
+
+if opt.gtd:
+    if not opt.bare:
+        print
+        print 'Counts for GTD categories:'
+        print '--------------------------'
+    for k in sorted(histGtd.keys()):
+        print '%-5s %4i' % (k, histGtd[k])
+    print 'other %4i' % (sum(hist.values()) - sum(histGtd.values()))
+
+#for k in sorted(histOther.keys()):
+    #print '%s: %3i' % (k, histOther[k])
+
diff --git a/timeSampler b/timeSampler
new file mode 100755 (executable)
index 0000000..7ddb201
--- /dev/null
@@ -0,0 +1,77 @@
+#!/bin/bash
+
+# fire off subshells which periodically ask for what I am
+# currently doing.  Subshells keep the periodicity independent
+# of response time to the script
+#
+# (C) 2010 Dan White <dan@whiteaudio.com> under GPL
+
+# idea from: http://wiki.43folders.com/index.php/Time_sampling
+
+BEEP="beep -f707 -n -f500"
+#BEEP="beep -f707 -n -f500 -n -f707"
+#BEEP="dcop knotify default notify notify Me notext KDE_Vox_Ahem.ogg nofile 1 0"
+MINUTES=6 #to give 10/hour
+#MAXJOBS=$((1*60/$MINUTES))
+#MAXJOBS=8
+MAXJOBS=17
+MAXJOBS=$(($MAXJOBS-1))
+
+LOGFILE="$HOME/.timeSampler.$HOSTNAME"
+
+#put something in the buffer to start
+for i in $(seq 0 $MAXJOBS); do
+    buffer[$i]="$i"
+done
+
+function lastActivity {
+    python -c "print ' '.join(open('$LOGFILE').readlines()[-1].split()[2:])"
+}
+
+# popup without blocking main script
+function sampleTask {
+    DATE=$(date +"%F %R")
+    ACTIVITY=$(zenity --entry \
+              --title="Task sample" \
+              --text="Current activity: $DATE" \
+              --entry-text="$(lastActivity)")
+
+#    ACTIVITY=`gxmessage -entry \
+#                        -center \
+#                        -buttons "GTK_STOCK_OK:0" \
+#                        -title "Task sample" \
+#                        -timeout 2 \
+#                        "Current activity: $DATE"`
+
+    if [ -z "$ACTIVITY" ]; then
+        ACTIVITY="null"
+    fi
+
+    echo "$DATE $ACTIVITY" >> $LOGFILE
+}
+
+
+while true; do
+    [[ $(lastActivity) != "null" ]] && $BEEP && sleep 0.8
+    (sampleTask) &
+    pid=$!
+
+    #FIFO of subshell PIDs
+    # or i+=1, and buffer[i % $MAXJOBS]
+    oldest=${buffer[0]}
+    for i in $(seq 0 $(( ${#buffer[@]}-1 )) ); do
+        buffer[$i]=${buffer[$(($i+1))]}
+    done
+    buffer[$MAXJOBS]=$pid
+
+    for j in `jobs -p`; do
+        if [ "$j" == "$oldest" ]; then
+            for zenitypid in $(ps --no-headers --ppid $oldest -o pid); do
+                kill -9 $zenitypid
+            done
+        fi
+    done
+
+    sleep $(($MINUTES*60 - 1))
+done
+