#!/usr/bin/perl use Time::Local; use Digest::MD5 qw(md5_hex); use File::Basename; use POSIX; my $trk_dir = "$ENV{HOME}/.trk"; use constant { START => 1, TIMEFORMAT => 2, STOP => 3, EDIT => 4, }; 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; printf("%s\n", $i); 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 { return undef if ( ! -f $trk_dir . "/last" ); open ( CUR, "<" . $trk_dir . "/last" ) or die ("Unable to read last track file"); my $id = ; chomp($id); close(CUR); return $id; } sub get_current_id { return undef if ( ! -f $trk_dir . "/current" ); open ( CUR, "<" . $trk_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; return undef if ( -f $trk_dir . "/current" ); open ( CUR, ">" . $trk_dir . "/current" ) or die ("Unable to write current track file"); printf(CUR "%s\n", $id ); close(CUR); open ( LAST, ">" . $trk_dir . "/last" ) or die ("Unable to write last track file"); printf(LAST "%s\n", $id ); close(LAST); } sub get_tracks { my %tracks; foreach my $d ( <$trk_dir/*> ) { next if not -d $d; next if not -f $d . "/info"; my $id = basename($d); my $title = get_track_name( $id ); $tracks{$id} = $title unless not defined $title; } return \%tracks; } sub get_track_id ($) { my $title = shift; # Get hash of track-id's and -names from get_tracks my $tracks = get_tracks(); # 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; open(PRO, "<" . $trk_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 $id; do { $id = gen_puuid(8); } while ( -d $trk_dir . "/" . $id ); mkdir ( $trk_dir . "/" . $id ); open(PRO, ">" . $trk_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 $current = get_current_id(); if ( not $current ) { if ( not $title ) { $current = get_last( ); } else { $current = get_track_id( $title ); if ( not $current ) { printf("No track by that name! Creating a new one.\n"); $current = create_track($title); } } # Break off here if we haven't gotten an ID yet. return undef if not $current; set_current_id($current); # 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, ">>" . $trk_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 $current = get_current_id(); die ("Project exists, but tracking file does not!") if ( not -f $trk_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, ">>" . $trk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!"); printf(TRACK " to [%s]\n", time2str($stop_time)); close (TRACK); unlink ( $trk_dir . "/current" ); } ############################################################ 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 ); close_track($stop_time); 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 ) { printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} ); } print("\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(); }