From d4fa7880c3b0bfa66915534941fba898e47c2520 Mon Sep 17 00:00:00 2001 From: Jon Langseth Date: Tue, 30 Apr 2013 20:01:01 +0200 Subject: [PATCH 1/1] A rough starting point --- concept.txt | 102 ++++++++++++++++++ trk | 301 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 concept.txt create mode 100755 trk diff --git a/concept.txt b/concept.txt new file mode 100644 index 0000000..111f65e --- /dev/null +++ b/concept.txt @@ -0,0 +1,102 @@ +trk start (if tracking project: stop-and-start, aka switch) +trk start at +trk on (alias for start) + + Starts tracking of a project. If 'at' is given, start-time + is overridden to the given time. + + Future expansion: + If a project was already tracking, that project will be stopped + at the startingpoint of the new tracking, i.e. implicitly switching. + +trk note Comment will be timestamped + + Adds a comment/note to the tracking. The note will not + affect tracked time, but will be included in detailed reports. + +trk activity +trk activity at + + Starts tracking of an activity within the project; think task + or sub-projects. Activities will be tallied within the project + like the "main project", while time counts to both activity + and project. + + Future: + Only one activity can be ongoing at the same time, + so starting a new activity while one is ongoing stops the current + and starts the new one. + +trk main +trk main at + + Stops tracking ongoing activity, and keeps tracking project. + +trk pause +trk back + + Adds an activity to the project that is not counted on + the project. Pauses will be listed on detailed reports, + but not on terse, standard and verbose reports. + +trk charge + + Will set a customer name as meta-information. Setting the + charged customer will replace any previously set value. + +trk status + + Displays current project, charged customer (if any), + start time for this session, time spent on this session, + time spent on project total, + current activity with start time and time spent this session. + +trk stop +trk stop at +trk off (alias for stop) + + Stops tracking of project, with implicit stopping of any + current activity. + +trk report terse +trk report +trk report verbose +tkr report details + + Gives a tracking report for the given project. + + Terse reports include: + * project name + * charged customer + * total time accumulated + + Standard reports include: + * project name + * charged customer + * session start and end datetimes and time elapsed on session + * total time accumulated + + Verbose reports include: + * project name + * charged customer + * session start and end datetimes and time elapsed on session + * activity start and end datetimes and time elapesd on activity + * note texts w/datetime + * time accumulated per named task + * total time accumulated + + Detailed reports include: + * project name + * charged customer + * session start and end datetimes and time elapsed on session + * activity start and end datetimes and time elapesd on activity + * pauses with start, end, comment and elapsed time + * note texts w/datetime + * time accumulated per named task + * total time accumulated + +trk projects + + Lists all known project names + + diff --git a/trk b/trk new file mode 100755 index 0000000..f52f49a --- /dev/null +++ b/trk @@ -0,0 +1,301 @@ +#!/usr/bin/perl +use Time::Local; +use Digest::MD5 qw(md5_hex); +use File::Basename; + + +my $trk_dir = "$ENV{HOME}/.trk"; + +use constant { + START => 1, + TIMEFORMAT => 2, + STOP => 3, +}; + +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); +} + +# 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 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; +} + +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 project 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) ) + { + shift(@ARGV); + $title = join(" ", @ARGV); + } + + if ( not defined $title ) + { + return $start_time; + } + else + { + return ( $start_time, $title ); + } +} + +sub get_current_project +{ + return undef if ( ! -f $trk_dir . "/current" ); + open ( CUR, "<" . $trk_dir . "/current" ) or die ("Unable to read current project file"); + my $id = ; + chomp($id); + close(CUR); + return $id; +} + +sub set_current_project ($) +{ + my $id = shift; + return undef if ( -f $trk_dir . "/current" ); + open ( CUR, ">" . $trk_dir . "/current" ) or die ("Unable to write current project file"); + printf(CUR "%s\n", $id ); + close(CUR); + + open ( LAST, ">" . $trk_dir . "/last" ) or die ("Unable to write last project file"); + printf(LAST "%s\n", $id ); + close(LAST); +} + +sub current_task +{ + my $project = get_current_project(); + return undef if not $project; + + open ( CUR, "<" . $trk_dir . "/current" ) or die ("Unable to read current project file"); + ; + my $id = ; + chomp($id); + close(CUR); + return $id; + +} + +sub get_projects +{ + my %projects; + + foreach my $d ( <$trk_dir/*> ) + { + next if not -d $d; + next if not -f $d . "/info"; + + my $id = basename($d); + my $title = get_project_name( $id ); + + $projects{$id} = $title unless not defined $title; + } + + return \%projects; + +} + +sub get_project_id ($) +{ + my $title = shift; + + # Get hash of project-id's and -names from get_projects + my $projects = get_projects(); + + # Look up name in list + foreach my $id ( keys $projects ) + { + # Return ID for name + return $id if ( $projects->{$id} eq $title ) + } + + # If no match, return undef. + return undef; +} + +sub get_project_name ($) +{ + my $id = shift; + open(PRO, "<" . $trk_dir . "/" . $id . "/info" ) or die ("Unable to read project medatata file!"); + my $title = undef; + while () + { + next if not $_ =~ /^title:(.*)/; + $title = $1; + } + close(PRO); + return $title; +} + +sub create_project ($) +{ + 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 project medatata file!"); + printf(PRO "title:%s", $title); + close(PRO); + + return $id; +} + +############################################################ + +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_project(); + if ( not $current ) + { + $current = get_project_id( $title ); + if ( not $current ) + { + printf("No project by that name! Creating a new one.\n"); + $current = create_project($title); + } + else + { + printf("Continuing tracking for existing project.\n"); + } + set_current_project($current); + } + else + { + printf("A project is being tracked: %s\n", get_project_name( $current ) ); + printf("Stop current tracking before starting a new one\n"); + exit(0); + } + + # 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]", $start_time); + close (TRACK); + + 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_project(); + if ( not $current ) + { + printf("No project is currently tracked. To stop, please start first\n"); + exit(0); + } + my $title = get_project_name( $current ); + + 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", $stop_time); + close (TRACK); + + unlink ( $trk_dir . "/current" ); + + printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time); +} +else +{ + help(); +} -- 2.39.2