--- /dev/null
+#!/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])
+
--- /dev/null
+#!/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
+