--- /dev/null
+trk start <project> (if tracking project: stop-and-start, aka switch)
+trk start at <YYYY-MM-DD hh:mm> <project>
+trk on <project> (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 <task>
+trk activity at <YYYY-MM-DD hh:mm> <task>
+
+ 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 <YYYY-MM-DD hh:mm>
+
+ Stops tracking ongoing activity, and keeps tracking project.
+
+trk pause <optional description>
+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 <customer>
+
+ 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 <YYYY-MM-DD hh:mm>
+trk off (alias for stop)
+
+ Stops tracking of project, with implicit stopping of any
+ current activity.
+
+trk report terse <project>
+trk report <project>
+trk report verbose <project>
+tkr report details <project>
+
+ 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
+
+
--- /dev/null
+#!/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 = <CUR>;
+ 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");
+ <CUR>;
+ my $id = <CUR>;
+ 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 (<PRO>)
+ {
+ 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();
+}