]> git.defcon.no Git - trk/blob - trk
Added listing of tasks/activities
[trk] / trk
1 #!/usr/bin/perl
2 #
3 # Copyright © Jon Langseth
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining a copy
6 # of this software and associated documentation files (the "Software"), to deal
7 # in the Software without restriction, including without limitation the rights
8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the Software is
10 # furnished to do so, subject to the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 # SOFTWARE.
22
23 use Time::Local;
24 use Digest::MD5 qw(md5_hex);
25 use File::Basename;
26 use POSIX;
27 use strict;
28
29 my $trk_dir = "$ENV{HOME}/.trk";
30 if ( $ENV{TRK_DIR} )
31 {
32 if ( -d $ENV{TRK_DIR} ) { $trk_dir = $ENV{TRK_DIR} if -d $ENV{TRK_DIR}; }
33 else { printf("Environment variable TRK_DIR is not a directory\n"); exit(1); }
34 }
35
36 use constant {
37 START => 1,
38 TIMEFORMAT => 2,
39 STOP => 3,
40 EDIT => 4,
41 TASK => 5,
42 };
43
44 sub help
45 {
46 my $code = shift;
47
48 printf("Error in invocation. Syntax summary:\n\n");
49 printf(" %s {start|on} [at yyyy-mm-dd hh:mm] <trackname>\n", $0);
50 printf(" %s {stop|off} [at yyyy-mm-dd hh:mm] \n", $0);
51 printf(" %s {activity|task} [at yyyy-mm-dd hh:mm] <taskname>\n", $0);
52 printf(" %s main [at yyyy-mm-dd hh:mm]\n", $0);
53 printf(" %s {projects|list} [verbose]\n", $0);
54 printf(" %s report [{terse|standard|verbose|details}] [<trackname>]\n", $0);
55 printf(" %s edit <trackname>\n", $0);
56 printf(" %s status\n", $0);
57 printf("\nSee README.txt for more information\n");
58
59 exit(-1);
60 }
61
62
63 sub gen_puuid (;$)
64 {
65 my $id_length = shift;
66 $id_length = 32 if not defined $id_length;
67
68 my $w = time;
69
70 for(my $i=0 ; $i<128;)
71 {
72 my $tc = chr(int(rand(127)));
73 if($tc =~ /[a-zA-Z0-9]/)
74 {
75 $w .=$tc;
76 $i++;
77 }
78 }
79 $w = md5_hex( $w );
80
81 while ( length($w) < $id_length )
82 {
83 $w .= gen_puuid( $id_length - length( $w ) );
84 }
85
86 $w = substr( $w, 0, $id_length );
87 return $w;
88 }
89
90 # Input to parse_time is:
91 # * date -> date-string in the form YYYY-MM-DD
92 # * time -> time-string in the form HH:MM
93 # Return value is a unix timestamp, as returned by time()
94 sub parse_time ($$)
95 {
96 my ( $Y, $M, $D ) = split ("-", shift );
97 my ( $h, $m ) = split(":", shift );
98 return timelocal(0, $m, $h, $D, ($M-1), $Y);
99 }
100
101 sub str2time ($)
102 {
103 my $i = shift;
104 return 0 if not $i =~ m/(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d)/;
105 return parse_time($1, $2);
106 }
107
108 sub time2str ($)
109 {
110 my $t = shift;
111 return strftime("%Y-%m-%d %H:%M", localtime($t));
112 }
113
114 sub delta2str ($)
115 {
116 my $delta = shift;
117 my $t = $delta;
118 my $hours = $t / 3600;
119 $t = $delta % 3600;
120 my $minutes = $t / 60;
121 return sprintf("%d hours %d minutes", $hours, $minutes);
122 }
123
124 sub parse_arguments ($)
125 {
126
127 my $step = shift;
128
129 my $start_time = time;
130 my $title = undef;
131
132 if (( $#ARGV >= 1) && ( $ARGV[1] eq "at" ))
133 {
134 # Start and Activity require a title to be present.
135 # All other (stop, main...) do not ^^.
136 if ( ($step == START) || ($step == TASK) )
137 {
138 # TODO: Allow no title!
139 # If no title is given, read ID of previously used track in stead :)
140 help($step) unless $#ARGV > 3;
141 $title = join(" ", @ARGV[4..$#ARGV]);
142 }
143 help(TIMEFORMAT) unless ( $ARGV[2] =~ m/\d\d\d\d-\d\d-\d\d/ && $ARGV[3] =~ m/\d\d:\d\d/);
144
145 $start_time = parse_time( $ARGV[2], $ARGV[3] );
146 }
147 elsif ( ($step == START) || ($step == TASK) || ($step == EDIT))
148 {
149 shift(@ARGV);
150 $title = join(" ", @ARGV);
151 }
152
153 if ( not defined $title )
154 {
155 return $start_time;
156 }
157 else
158 {
159 return ( $start_time, $title );
160 }
161 }
162
163 sub get_last_id (;$)
164 {
165 my $trk_id = shift;
166 my $wrk_dir = $trk_dir;
167 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
168
169 return undef if ( ! -f $wrk_dir . "/last" );
170 open ( CUR, "<" . $wrk_dir . "/last" ) or die ("Unable to read last track file");
171 my $id = <CUR>;
172 chomp($id);
173 close(CUR);
174 return $id;
175 }
176
177 sub get_current_id (;$)
178 {
179 my $trk_id = shift;
180 my $wrk_dir = $trk_dir;
181 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
182
183 return undef if ( ! -f $wrk_dir . "/current" );
184 open ( CUR, "<" . $wrk_dir . "/current" ) or die ("Unable to read current track file");
185 my $id = <CUR>;
186 chomp($id);
187 close(CUR);
188 return $id;
189 }
190
191 sub set_current_id ($;$)
192 {
193 my $id = shift;
194 my $trk_id = shift;
195 my $wrk_dir = $trk_dir;
196 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
197
198 return undef if ( -f $wrk_dir . "/current" );
199 open ( CUR, ">" . $wrk_dir . "/current" ) or die ("Unable to write current track file");
200 printf(CUR "%s\n", $id );
201 close(CUR);
202
203 open ( LAST, ">" . $wrk_dir . "/last" ) or die ("Unable to write last track file");
204 printf(LAST "%s\n", $id );
205 close(LAST);
206 }
207
208 sub get_tracks (;$)
209 {
210 my $trk_id = shift;
211 my $wrk_dir = $trk_dir;
212 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
213
214
215 my %tracks;
216
217 foreach my $d ( <$wrk_dir/*> )
218 {
219 next if not -d $d;
220 next if not -f $d . "/info";
221
222 my $id = basename($d);
223 my $title = get_track_name( $id, $trk_id );
224
225 $tracks{$id} = $title unless not defined $title;
226 }
227
228 return \%tracks;
229
230 }
231
232 sub get_track_id ($;$)
233 {
234 my $title = shift;
235 my $trk_id = shift;
236
237 # Get hash of track-id's and -names from get_tracks
238 my $tracks = get_tracks($trk_id);
239
240 # Look up name in list
241 foreach my $id ( keys %$tracks )
242 {
243 # Return ID for name
244 return $id if ( $tracks->{$id} eq $title )
245 }
246
247 # If no match, return undef.
248 return undef;
249 }
250
251 sub get_track_name ($;$)
252 {
253 my $id = shift;
254 my $trk_id = shift;
255 my $wrk_dir = $trk_dir;
256 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
257
258 open(PRO, "<" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to read track medatata file!");
259 my $title = undef;
260 while (<PRO>)
261 {
262 next if not $_ =~ /^title:(.*)/;
263 $title = $1;
264 }
265 close(PRO);
266 return $title;
267 }
268
269 sub create_track ($;$)
270 {
271 my $title = shift;
272 my $trk_id = shift;
273 my $wrk_dir = $trk_dir;
274 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
275
276 my $id;
277 do
278 {
279 $id = gen_puuid(8);
280
281 } while ( -d $wrk_dir . "/" . $id );
282 mkdir ( $wrk_dir . "/" . $id );
283
284 open(PRO, ">" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to create track medatata file!");
285 printf(PRO "title:%s\n", $title);
286 close(PRO);
287
288 return $id;
289 }
290
291 sub start_track ($$;$)
292 {
293 my $start_time = shift;
294 my $title = shift;
295
296 my $trk_id = shift;
297 my $wrk_dir = $trk_dir;
298 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
299
300 my $current = get_current_id($trk_id);
301 if ( not $current )
302 {
303 if ( not $title )
304 {
305 $current = get_last_id( $trk_id );
306 }
307 else
308 {
309 $current = get_track_id( $title, $trk_id );
310 if ( not $current )
311 {
312 printf("No track by that name! Creating a new one.\n");
313 $current = create_track($title, $trk_id);
314 }
315 }
316
317 # Break off here if we haven't gotten an ID yet.
318 return undef if not $current;
319
320 set_current_id($current, $trk_id);
321
322 # First iteration is VERY naive: simply add the start time to the bottom of the tracking file
323 # Will have to do more logic: if the start point is before one of the times already in the track,
324 # the file will have to be manipulated to get coherent tracking!
325 open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
326 printf(TRACK "[%s]", time2str($start_time));
327 close (TRACK);
328
329 return $current;
330 }
331
332 return undef;
333 }
334
335 sub close_track ($;$)
336 {
337
338 my $stop_time = shift;
339 my $trk_id = shift;
340 my $wrk_dir = $trk_dir;
341 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
342
343 my $current = get_current_id( $trk_id );
344
345 die ("Project exists, but tracking file does not!") if ( not -f $wrk_dir . "/" . $current . "/tracking" );
346
347 # First iteration is VERY naive: simply add the stop time to the bottom line of the tracking file
348 # Will have to do more logic: if the start point is before one of the times already in the track,
349 # the file will have to be manipulated to get coherent tracking!
350 # In addtion to this: actually do some file sanity checking!
351 open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
352 printf(TRACK " to [%s]\n", time2str($stop_time));
353 close (TRACK);
354
355 unlink ( $wrk_dir . "/current" );
356 }
357
358
359 sub report ($$;$)
360 {
361 my $current = shift;
362 my $silent = shift;
363 my $trk_id = shift;
364 my $wrk_dir = $trk_dir;
365 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
366
367 my $total = 0;
368
369 my $name = get_track_name( $current, $trk_id );
370 printf("# Report for '%s':\n\n", $name) unless $silent;
371
372 my $check = get_current_id( $trk_id );
373
374 open (TRACK, "<" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
375 while ( <TRACK> )
376 {
377 next if not $_ =~ m/\[(.*)\] to \[(.*)\]/;
378 my $start = $1;
379 my $end = $2;
380 my $t_start = str2time( $start );
381 my $t_end = str2time( $end );
382 my $delta = $t_end - $t_start;
383
384 if ( not $silent )
385 {
386 printf(" %s to %s => %s\n", $start, $end, delta2str($delta) );
387 }
388 $total += $delta;
389
390
391 }
392 close ( TRACK );
393
394 if ( $check eq $current )
395 {
396 my $t;
397 if ( $t = current_starttime( $trk_id ) )
398 {
399 my $delta = time - $t;
400 printf(" %s to NOW (active) => %s\n", time2str( $t ), delta2str( $delta ) ) if not $silent;
401 $total += $delta;
402 }
403 }
404
405 return $total;
406
407 }
408
409 sub current_starttime (;$)
410 {
411 my $trk_id = shift;
412 my $wrk_dir = $trk_dir;
413 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
414
415 my $line = undef;
416
417 my $current = get_current_id( $trk_id );
418 return 0 if not $current;
419
420 open (TRACK, "<" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
421 while ( <TRACK> )
422 {
423 $line = $_;
424 }
425 close ( TRACK );
426 return 0 if not $line =~ m/^\[(\d\d\d\d-\d\d-\d\d \d\d:\d\d)\]$/;
427 return str2time($1);
428 }
429
430 ############################################################
431
432 if ( ! -d $trk_dir )
433 {
434 mkdir $trk_dir or die("Unable to create data directory");
435 }
436
437 if ( $#ARGV < 0 )
438 {
439 help();
440 }
441
442 my $command = $ARGV[0];
443
444 if ( ( $command eq "start") || ($command eq "on" ) )
445 {
446 if ( $#ARGV < 1)
447 {
448 help(START);
449 }
450
451 my ( $start_time, $title ) = parse_arguments(START);
452
453 my $current = get_current_id();
454 if ( not $current )
455 {
456 $current = start_track( $start_time, $title );
457
458 if ( not $current )
459 {
460 printf("Something weird happened.\n");
461 exit(1);
462 }
463 }
464 else
465 {
466 printf("A project is being tracked: %s\n", get_track_name( $current ) );
467 close_track($start_time);
468 $current = start_track( $start_time, $title );
469 }
470
471 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
472 }
473 elsif ( ( $command eq "stop") || ($command eq "off" ) )
474 {
475 if ( $#ARGV < 0)
476 {
477 help(STOP);
478 }
479
480
481 my $stop_time = parse_arguments(STOP);
482
483 my $current = get_current_id();
484 if ( not $current )
485 {
486 printf("No project is currently tracked. To stop, please start first\n");
487 exit(0);
488 }
489 my $title = get_track_name( $current );
490
491 my $activity = get_current_id($current);
492 if ( $activity )
493 {
494 printf("An active subtask is running: '%s'. Closing it.\n", get_track_name( $activity, $current ));
495 close_track($stop_time, $current);
496 }
497 close_track($stop_time);
498
499 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
500 }
501 elsif ( ( $command eq "activity") || ($command eq "task" ) )
502 {
503 if ( $#ARGV < 1)
504 {
505 help(START);
506 }
507
508 my ( $start_time, $title ) = parse_arguments(START);
509
510 my $trk_id = get_current_id();
511 if ( not $trk_id )
512 {
513 printf("Starting a task/activity requires an active main track.\n");
514 exit(1);
515 }
516 else
517 {
518
519 my $current = get_current_id( $trk_id );
520 if ( not $current )
521 {
522 $current = start_track( $start_time, $title, $trk_id );
523
524 if ( not $current )
525 {
526 printf("Something weird happened.\n");
527 exit(1);
528 }
529 }
530 else
531 {
532 printf("A task/activity is being tracked: %s\n", get_track_name( $current, $trk_id ) );
533 close_track($start_time, $trk_id);
534 $current = start_track( $start_time, $title, $trk_id );
535 }
536
537 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
538
539 }
540
541 }
542
543 elsif ( $command eq "main" )
544 {
545
546 if ( $#ARGV < 0)
547 {
548 help(STOP);
549 }
550
551
552 my $stop_time = parse_arguments(STOP);
553
554 my $trk_id = get_current_id();
555 if ( not $trk_id )
556 {
557 printf("Stopping a task/activity requires an active main track.\n");
558 exit(1);
559 }
560 else
561 {
562 my $current = get_current_id( $trk_id );
563 if ( not $current )
564 {
565 printf("No activity/task is currently tracked. To stop, please start first\n");
566 exit(0);
567 }
568 my $title = get_track_name( $current, $trk_id );
569 close_track($stop_time, $trk_id);
570
571 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
572 }
573 }
574 #### Mark
575 elsif ( ( $command eq "projects" ) || ( $command eq "list" ) )
576 {
577 # Todo/future extensions:
578 # TODO: Sort list of names alphabetically
579 # TODO: Get total-hours for projects
580 # TODO:
581 my $tracks = get_tracks();
582 printf("Currently tracked project names:\n\n");
583 my $current = get_current_id();
584
585 #foreach my $id ( keys %$tracks )
586 foreach my $id ( sort { $tracks->{$a} cmp $tracks->{$b} || $a cmp $b } keys %$tracks )
587 {
588
589 if (( $#ARGV >= 1) && ( $ARGV[1] eq "verbose" ))
590 {
591 printf(" %s %s %s\n", ($id eq $current ? ">" : " " ), $id , $tracks->{$id});
592 }
593 else
594 {
595 printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} );
596 }
597 }
598 print("\n");
599 }
600 #### Mark
601 elsif ( ( $command eq "activities" ) || ( $command eq "tasks" ) )
602 {
603 # Todo/future extensions:
604 # TODO: Sort list of names alphabetically
605 # TODO: Get total-hours for projects
606 # TODO:
607
608
609 my $trk_id = get_current_id();
610 if ( not $trk_id )
611 {
612 printf("Starting a task/activity requires an active main track.\n");
613 exit(1);
614 }
615
616 my $verbose = (( $#ARGV >= 1) && ( $ARGV[1] eq "verbose" ));
617
618 my $tracks = get_tracks( $trk_id );
619 printf("Current track is '%s'\n", get_track_name( $trk_id ));
620 printf("Track ID for for current track is %s\n", $trk_id) if $verbose;
621 printf("Currently tracked activities/tasks for track '%s':\n\n", get_track_name( $trk_id ));
622 my $current = get_current_id( $trk_id );
623
624 #foreach my $id ( keys %$tracks )
625 foreach my $id ( sort { $tracks->{$a} cmp $tracks->{$b} || $a cmp $b } keys %$tracks )
626 {
627
628 if ( $verbose )
629 {
630 printf(" %s %s %s\n", ($id eq $current ? ">" : " " ), $id , $tracks->{$id});
631 }
632 else
633 {
634 printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} );
635 }
636 }
637 print("\n");
638 }
639 elsif ( $command eq "report" )
640 {
641
642 my $format = "standard";
643 my $output = 0;
644
645 if (( $#ARGV >= 1) &&
646 ( ( $ARGV[1] eq "standard" )
647 || ( $ARGV[1] eq "terse" )
648 || ( $ARGV[1] eq "verbose" )
649 || ( $ARGV[1] eq "details" ) ) )
650 {
651 $format = $ARGV[1];
652 shift @ARGV;
653 }
654
655 $output = 0 if $format eq "terse";
656 $output = 1 if $format eq "standard";
657 $output = 2 if $format eq "verbose";
658 $output = 3 if $format eq "details";
659
660 my ( undef, $title ) = parse_arguments(START);
661
662 printf("Report format: %s\nTitle: %s\n", $format, $title);
663
664 my $track = undef;
665
666 if ( $title )
667 {
668 $track = get_track_id( $title );
669 }
670 else
671 {
672 $track = get_last_id();
673 }
674
675 if ( not $track )
676 {
677 printf ("Unable to get info for that track\n");
678 exit(1);
679 }
680
681 my $total = 0;
682 my $subtotals = 0;
683
684 my $activities = get_tracks( $track );
685 if ( keys %$activities )
686 {
687 printf("# Reporting for sub-task/activities:\n\n") if $output >= 2;
688
689 foreach my $id ( sort { $activities->{$a} cmp $activities->{$b} || $a cmp $b } keys %$activities )
690 #foreach my $id ( keys %$activities )
691 {
692 $subtotals += report( $id, ( $output >= 2 ? 0 : 1 ), $track );
693 printf("# --------------------------------------------------------------\n\n") if $output >= 2;
694 }
695 }
696
697 printf("# Reporting for main track/project/task\n") if $output >= 2;
698 $total += report($track, ( ( $output >= 1 ? 0 : 1 ) ) );
699 printf("\n# ==============================================================\n\n") if $output >= 2;
700 print("\n") if $output >= 1;
701
702
703 printf("Total: %s\n", delta2str($total) );
704
705 if ( $output >= 2 )
706 {
707 printf("Time logged on tasks: %s\n", delta2str($subtotals) );
708 }
709
710 print("# End of report\n") if $output >= 1;
711
712 }
713 elsif ( $command eq "status" )
714 {
715
716 my $trk_id = get_current_id();
717 if ( not $trk_id )
718 {
719 printf("Not currently tracking anything.\n");
720 $trk_id = get_last_id();
721 if ( $trk_id )
722 {
723 printf("Last track was: %s\n", get_track_name( $trk_id ) );
724 }
725 exit(1);
726 }
727 printf("Currently tracking: %s\n", get_track_name( $trk_id ) );
728 my $t = current_starttime();
729 printf("Tracking started at %s\n", scalar localtime $t);
730 printf("Time elapsed since start of session: %s\n", delta2str(time - $t) );
731
732 my $activity = get_current_id( $trk_id );
733 if ( $activity )
734 {
735 printf("\nCurrent sub-task/activity is: %s\n", get_track_name( $activity, $trk_id ) );
736 my $t = current_starttime($trk_id);
737 printf("Activity started at %s\n", scalar localtime $t);
738 printf("Time elapsed since start of activity: %s\n", delta2str(time - $t) );
739 }
740 else
741 {
742 $activity = get_last_id( $trk_id );
743 if ( $activity )
744 {
745 printf("\nLast track was: %s\n", get_track_name( $activity, $trk_id ) );
746 }
747 }
748 }
749 elsif ( $command eq "edit" )
750 {
751
752 my ( undef, $title ) = parse_arguments(EDIT);
753 my $id = get_last_id();
754
755 if ( $title )
756 {
757 $id = get_track_id($title);
758 if ( not $id )
759 {
760 printf("No project by that name. Try 'list'\n");
761 exit(0);
762 }
763 }
764
765 system( "/usr/bin/editor " . $trk_dir . "/" . $id . "/tracking" );
766 }
767 else
768 {
769 help();
770 }