From: Dan White Date: Wed, 2 Feb 2011 17:18:06 +0000 (-0600) Subject: Add time tracking scripts. X-Git-Url: http://git.whiteaudio.com/gitweb/?a=commitdiff_plain;h=978cd7410f4b29b03bbde4e99e58a15c1d10a4c7;p=pubbin.git Add time tracking scripts. --- 978cd7410f4b29b03bbde4e99e58a15c1d10a4c7 diff --git a/TimeSampleHistogram b/TimeSampleHistogram new file mode 100755 index 0000000..dfb5702 --- /dev/null +++ b/TimeSampleHistogram @@ -0,0 +1,264 @@ +#!/usr/bin/env python + +# Present histogram reports from timeSampler data. See "$0 -h" for more +# +# (C) Dan White + +# 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\d\d\d\d)-(?P\d\d)-(?P\d\d) (?P\d\d):(?P\d\d)\s+(?P.*)$') + +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 index 0000000..7ddb201 --- /dev/null +++ b/timeSampler @@ -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 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 +