#!/usr/bin/perl use Time::Local; use Digest::MD5 qw(md5_hex); use File::Basename; use POSIX; use strict; my $trk_dir = "$ENV{HOME}/.trk"; if ( $ENV{TRK_DIR} ) { if ( -d $ENV{TRK_DIR} ) { $trk_dir = $ENV{TRK_DIR} if -d $ENV{TRK_DIR}; } else { printf("Environment variable TRK_DIR is not a directory\n"); exit(1); } } use constant { START => 1, TIMEFORMAT => 2, STOP => 3, EDIT => 4, TASK => 5, }; sub help { my $code = shift; printf("It seems you require assistance\n"); if ( $code ) { printf("How to start\n") if $code == START; printf("How to time\n") if $code == TIMEFORMAT; } exit(-1); } sub gen_puuid (;$) { my $id_length = shift; $id_length = 32 if not defined $id_length; my $w = time; for(my $i=0 ; $i<128;) { my $tc = chr(int(rand(127))); if($tc =~ /[a-zA-Z0-9]/) { $w .=$tc; $i++; } } $w = md5_hex( $w ); while ( length($w) < $id_length ) { $w .= gen_puuid( $id_length - length( $w ) ); } $w = substr( $w, 0, $id_length ); return $w; } # Input to parse_time is: # * date -> date-string in the form YYYY-MM-DD # * time -> time-string in the form HH:MM # Return value is a unix timestamp, as returned by time() sub parse_time ($$) { my ( $Y, $M, $D ) = split ("-", shift ); my ( $h, $m ) = split(":", shift ); return timelocal(0, $m, $h, $D, ($M-1), $Y); } sub str2time ($) { my $i = shift; return 0 if not $i =~ m/(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d)/; return parse_time($1, $2); } sub time2str ($) { my $t = shift; return strftime("%Y-%m-%d %H:%M", localtime($t)); } sub parse_arguments ($) { my $step = shift; my $start_time = time; my $title = undef; if (( $#ARGV >= 1) && ( $ARGV[1] eq "at" )) { # Start and Activity require a title to be present. # All other (stop, main...) do not ^^. if ( ($step == START) || ($step == TASK) ) { # TODO: Allow no title! # If no title is given, read ID of previously used track in stead :) help($step) unless $#ARGV > 3; $title = join(" ", @ARGV[4..$#ARGV]); } help(TIMEFORMAT) unless ( $ARGV[2] =~ m/\d\d\d\d-\d\d-\d\d/ && $ARGV[3] =~ m/\d\d:\d\d/); $start_time = parse_time( $ARGV[2], $ARGV[3] ); } elsif ( ($step == START) || ($step == TASK) || ($step == EDIT)) { shift(@ARGV); $title = join(" ", @ARGV); } if ( not defined $title ) { return $start_time; } else { return ( $start_time, $title ); } } sub get_last_id (;$) { my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; return undef if ( ! -f $wrk_dir . "/last" ); open ( CUR, "<" . $wrk_dir . "/last" ) or die ("Unable to read last track file"); my $id = ; chomp($id); close(CUR); return $id; } sub get_current_id (;$) { my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; return undef if ( ! -f $wrk_dir . "/current" ); open ( CUR, "<" . $wrk_dir . "/current" ) or die ("Unable to read current track file"); my $id = ; chomp($id); close(CUR); return $id; } sub set_current_id ($;$) { my $id = shift; my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; return undef if ( -f $wrk_dir . "/current" ); open ( CUR, ">" . $wrk_dir . "/current" ) or die ("Unable to write current track file"); printf(CUR "%s\n", $id ); close(CUR); open ( LAST, ">" . $wrk_dir . "/last" ) or die ("Unable to write last track file"); printf(LAST "%s\n", $id ); close(LAST); } sub get_tracks (;$) { my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; my %tracks; foreach my $d ( <$wrk_dir/*> ) { next if not -d $d; next if not -f $d . "/info"; my $id = basename($d); my $title = get_track_name( $id, $trk_id ); $tracks{$id} = $title unless not defined $title; } return \%tracks; } sub get_track_id ($;$) { my $title = shift; my $trk_id = shift; # Get hash of track-id's and -names from get_tracks my $tracks = get_tracks($trk_id); # Look up name in list foreach my $id ( keys %$tracks ) { # Return ID for name return $id if ( $tracks->{$id} eq $title ) } # If no match, return undef. return undef; } sub get_track_name ($;$) { my $id = shift; my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; open(PRO, "<" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to read track medatata file!"); my $title = undef; while () { next if not $_ =~ /^title:(.*)/; $title = $1; } close(PRO); return $title; } sub create_track ($;$) { my $title = shift; my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; my $id; do { $id = gen_puuid(8); } while ( -d $wrk_dir . "/" . $id ); mkdir ( $wrk_dir . "/" . $id ); open(PRO, ">" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to create track medatata file!"); printf(PRO "title:%s\n", $title); close(PRO); return $id; } sub start_track ($$;$) { my $start_time = shift; my $title = shift; my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; my $current = get_current_id($trk_id); if ( not $current ) { if ( not $title ) { $current = get_last_id( $trk_id ); } else { $current = get_track_id( $title, $trk_id ); if ( not $current ) { printf("No track by that name! Creating a new one.\n"); $current = create_track($title, $trk_id); } } # Break off here if we haven't gotten an ID yet. return undef if not $current; set_current_id($current, $trk_id); # First iteration is VERY naive: simply add the start time to the bottom of the tracking file # Will have to do more logic: if the start point is before one of the times already in the track, # the file will have to be manipulated to get coherent tracking! open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!"); printf(TRACK "[%s]", time2str($start_time)); close (TRACK); return $current; } return undef; } sub close_track ($;$) { my $stop_time = shift; my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; my $current = get_current_id( $trk_id ); die ("Project exists, but tracking file does not!") if ( not -f $wrk_dir . "/" . $current . "/tracking" ); # First iteration is VERY naive: simply add the stop time to the bottom line of the tracking file # Will have to do more logic: if the start point is before one of the times already in the track, # the file will have to be manipulated to get coherent tracking! # In addtion to this: actually do some file sanity checking! open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!"); printf(TRACK " to [%s]\n", time2str($stop_time)); close (TRACK); unlink ( $wrk_dir . "/current" ); } sub report ($;$) { my $current = shift; my $trk_id = shift; my $wrk_dir = $trk_dir; $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id; my $total = 0; my $name = get_track_name( $current, $trk_id ); printf("# Report for '%s':\n\n", $name); open (TRACK, "<" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!"); while ( ) { next if not $_ =~ m/\[(.*)\] to \[(.*)\]/; my $start = $1; my $end = $2; my $t_start = str2time( $start ); my $t_end = str2time( $end ); my $delta = $t_end - $t_start; my $t = $delta; my $hours = $t / 3600; $t = $delta % 3600; my $minutes = $t / 60; printf(" %s to %s => %d hours %d minutes\n", $start, $end, $hours, $minutes); $total += $delta; } close ( TRACK ); my $t = $total; my $hours = $t / 3600; $t = $total % 3600; my $minutes = $t / 60; printf("\nTotal: %d hours %d minutes\n", $hours, $minutes); } ############################################################ if ( ! -d $trk_dir ) { mkdir $trk_dir or die("Unable to create data directory"); } if ( $#ARGV < 0 ) { help(); } my $command = $ARGV[0]; if ( ( $command eq "start") || ($command eq "on" ) ) { if ( $#ARGV < 1) { help(START); } my ( $start_time, $title ) = parse_arguments(START); my $current = get_current_id(); if ( not $current ) { $current = start_track( $start_time, $title ); if ( not $current ) { printf("Something weird happened.\n"); exit(1); } } else { printf("A project is being tracked: %s\n", get_track_name( $current ) ); close_track($start_time); $current = start_track( $start_time, $title ); } printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time); } elsif ( ( $command eq "stop") || ($command eq "off" ) ) { if ( $#ARGV < 0) { help(STOP); } my $stop_time = parse_arguments(STOP); my $current = get_current_id(); if ( not $current ) { printf("No project is currently tracked. To stop, please start first\n"); exit(0); } my $title = get_track_name( $current ); my $activity = get_current_id($current); if ( $activity ) { printf("An active subtask is running: '%s'. Closing it.\n", get_track_name( $activity, $current )); close_track($stop_time, $current); } close_track($stop_time); printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time); } elsif ( ( $command eq "activity") || ($command eq "task" ) ) { if ( $#ARGV < 1) { help(START); } my ( $start_time, $title ) = parse_arguments(START); my $trk_id = get_current_id(); if ( not $trk_id ) { printf("Starting a task/activity requires an active main track.\n"); exit(1); } else { my $current = get_current_id( $trk_id ); if ( not $current ) { $current = start_track( $start_time, $title, $trk_id ); if ( not $current ) { printf("Something weird happened.\n"); exit(1); } } else { printf("A task/activity is being tracked: %s\n", get_track_name( $current, $trk_id ) ); close_track($start_time, $trk_id); $current = start_track( $start_time, $title, $trk_id ); } printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time); } } elsif ( $command eq "main" ) { if ( $#ARGV < 0) { help(STOP); } my $stop_time = parse_arguments(STOP); my $trk_id = get_current_id(); if ( not $trk_id ) { printf("Stopping a task/activity requires an active main track.\n"); exit(1); } else { my $current = get_current_id( $trk_id ); if ( not $current ) { printf("No activity/task is currently tracked. To stop, please start first\n"); exit(0); } my $title = get_track_name( $current, $trk_id ); close_track($stop_time, $trk_id); printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time); } } elsif ( ( $command eq "projects" ) || ( $command eq "list" ) ) { # Todo/future extensions: # TODO: Sort list of names alphabetically # TODO: Get total-hours for projects # TODO: my $tracks = get_tracks(); printf("Currently tracked project names:\n\n"); my $current = get_current_id(); #foreach my $id ( keys %$tracks ) foreach my $id ( sort { $tracks->{$a} cmp $tracks->{$b} || $a cmp $b } keys %$tracks ) { if (( $#ARGV >= 1) && ( $ARGV[1] eq "verbose" )) { printf(" %s %s %s\n", ($id eq $current ? ">" : " " ), $id , $tracks->{$id}); } else { printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} ); } } print("\n"); } elsif ( $command eq "report" ) { my ( undef, $title ) = parse_arguments(START); my $track = undef; if ( $title ) { $track = get_track_id( $title ); } else { $track = get_last_id(); } if ( not $track ) { printf ("Unable to get info for that track\n"); exit(1); } my $activities = get_tracks( $track ); if ( keys %$activities ) { printf("# Reporting for sub-task/activities:\n\n"); foreach my $id ( sort { $activities->{$a} cmp $activities->{$b} || $a cmp $b } keys %$activities ) #foreach my $id ( keys %$activities ) { report( $id, $track ); printf("# --------------------------------------------------------------\n"); print("\n"); } } printf("# Reporting for main track/project/task\n"); report($track); printf("\n# ==============================================================\n\n"); print("# End of report\n"); } elsif ( $command eq "edit" ) { my ( undef, $title ) = parse_arguments(EDIT); my $id = get_last_id(); if ( $title ) { $id = get_track_id($title); if ( not $id ) { printf("No project by that name. Try 'list'\n"); exit(0); } } system( "/usr/bin/editor " . $trk_dir . "/" . $id . "/tracking" ); } else { help(); }