]> git.defcon.no Git - trk/blob - trk
Added CURRENT track and task to reports. Made current_starttime safer
[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 if ( $check eq $current )
391 {
392 my $t;
393 if ( $t = current_starttime( $trk_id ) )
394 {
395 my $delta = time - $t;
396 printf(" %s to NOW (active) => %s\n", time2str( $t ), delta2str( $delta ) ) if not $silent;
397 $total += $delta;
398 }
399 }
400
401 }
402 close ( TRACK );
403 return $total;
404
405 }
406
407 sub current_starttime (;$)
408 {
409 my $trk_id = shift;
410 my $wrk_dir = $trk_dir;
411 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
412
413 my $line = undef;
414
415 my $current = get_current_id( $trk_id );
416 return 0 if not $current;
417
418 open (TRACK, "<" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
419 while ( <TRACK> )
420 {
421 $line = $_;
422 }
423 close ( TRACK );
424 return 0 if not $line =~ m/^\[(\d\d\d\d-\d\d-\d\d \d\d:\d\d)\]$/;
425 return str2time($1);
426 }
427
428 ############################################################
429
430 if ( ! -d $trk_dir )
431 {
432 mkdir $trk_dir or die("Unable to create data directory");
433 }
434
435 if ( $#ARGV < 0 )
436 {
437 help();
438 }
439
440 my $command = $ARGV[0];
441
442 if ( ( $command eq "start") || ($command eq "on" ) )
443 {
444 if ( $#ARGV < 1)
445 {
446 help(START);
447 }
448
449 my ( $start_time, $title ) = parse_arguments(START);
450
451 my $current = get_current_id();
452 if ( not $current )
453 {
454 $current = start_track( $start_time, $title );
455
456 if ( not $current )
457 {
458 printf("Something weird happened.\n");
459 exit(1);
460 }
461 }
462 else
463 {
464 printf("A project is being tracked: %s\n", get_track_name( $current ) );
465 close_track($start_time);
466 $current = start_track( $start_time, $title );
467 }
468
469 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
470 }
471 elsif ( ( $command eq "stop") || ($command eq "off" ) )
472 {
473 if ( $#ARGV < 0)
474 {
475 help(STOP);
476 }
477
478
479 my $stop_time = parse_arguments(STOP);
480
481 my $current = get_current_id();
482 if ( not $current )
483 {
484 printf("No project is currently tracked. To stop, please start first\n");
485 exit(0);
486 }
487 my $title = get_track_name( $current );
488
489 my $activity = get_current_id($current);
490 if ( $activity )
491 {
492 printf("An active subtask is running: '%s'. Closing it.\n", get_track_name( $activity, $current ));
493 close_track($stop_time, $current);
494 }
495 close_track($stop_time);
496
497 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
498 }
499 elsif ( ( $command eq "activity") || ($command eq "task" ) )
500 {
501 if ( $#ARGV < 1)
502 {
503 help(START);
504 }
505
506 my ( $start_time, $title ) = parse_arguments(START);
507
508 my $trk_id = get_current_id();
509 if ( not $trk_id )
510 {
511 printf("Starting a task/activity requires an active main track.\n");
512 exit(1);
513 }
514 else
515 {
516
517 my $current = get_current_id( $trk_id );
518 if ( not $current )
519 {
520 $current = start_track( $start_time, $title, $trk_id );
521
522 if ( not $current )
523 {
524 printf("Something weird happened.\n");
525 exit(1);
526 }
527 }
528 else
529 {
530 printf("A task/activity is being tracked: %s\n", get_track_name( $current, $trk_id ) );
531 close_track($start_time, $trk_id);
532 $current = start_track( $start_time, $title, $trk_id );
533 }
534
535 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
536
537 }
538
539 }
540
541 elsif ( $command eq "main" )
542 {
543
544 if ( $#ARGV < 0)
545 {
546 help(STOP);
547 }
548
549
550 my $stop_time = parse_arguments(STOP);
551
552 my $trk_id = get_current_id();
553 if ( not $trk_id )
554 {
555 printf("Stopping a task/activity requires an active main track.\n");
556 exit(1);
557 }
558 else
559 {
560 my $current = get_current_id( $trk_id );
561 if ( not $current )
562 {
563 printf("No activity/task is currently tracked. To stop, please start first\n");
564 exit(0);
565 }
566 my $title = get_track_name( $current, $trk_id );
567 close_track($stop_time, $trk_id);
568
569 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
570 }
571 }
572 elsif ( ( $command eq "projects" ) || ( $command eq "list" ) )
573 {
574 # Todo/future extensions:
575 # TODO: Sort list of names alphabetically
576 # TODO: Get total-hours for projects
577 # TODO:
578 my $tracks = get_tracks();
579 printf("Currently tracked project names:\n\n");
580 my $current = get_current_id();
581
582 #foreach my $id ( keys %$tracks )
583 foreach my $id ( sort { $tracks->{$a} cmp $tracks->{$b} || $a cmp $b } keys %$tracks )
584 {
585 printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} );
586 }
587 print("\n");
588 }
589 elsif ( $command eq "report" )
590 {
591
592 my $format = "standard";
593 my $output = 0;
594
595 if (( $#ARGV >= 1) &&
596 ( ( $ARGV[1] eq "standard" )
597 || ( $ARGV[1] eq "terse" )
598 || ( $ARGV[1] eq "verbose" )
599 || ( $ARGV[1] eq "details" ) ) )
600 {
601 $format = $ARGV[1];
602 shift @ARGV;
603 }
604
605 $output = 0 if $format eq "terse";
606 $output = 1 if $format eq "standard";
607 $output = 2 if $format eq "verbose";
608 $output = 3 if $format eq "details";
609
610 my ( undef, $title ) = parse_arguments(START);
611
612 printf("Report format: %s\nTitle: %s\n", $format, $title);
613
614 my $track = undef;
615
616 if ( $title )
617 {
618 $track = get_track_id( $title );
619 }
620 else
621 {
622 $track = get_last_id();
623 }
624
625 if ( not $track )
626 {
627 printf ("Unable to get info for that track\n");
628 exit(1);
629 }
630
631 my $total = 0;
632 my $subtotals = 0;
633
634 my $activities = get_tracks( $track );
635 if ( keys %$activities )
636 {
637 printf("# Reporting for sub-task/activities:\n\n") if $output >= 2;
638
639 foreach my $id ( sort { $activities->{$a} cmp $activities->{$b} || $a cmp $b } keys %$activities )
640 #foreach my $id ( keys %$activities )
641 {
642 $subtotals += report( $id, ( $output >= 2 ? 0 : 1 ), $track );
643 printf("# --------------------------------------------------------------\n\n") if $output >= 2;
644 }
645 }
646
647 printf("# Reporting for main track/project/task\n") if $output >= 2;
648 $total += report($track, ( ( $output >= 1 ? 0 : 1 ) ) );
649 printf("\n# ==============================================================\n\n") if $output >= 2;
650 print("\n") if $output >= 1;
651
652
653 printf("Total: %s\n", delta2str($total) );
654
655 if ( $output >= 2 )
656 {
657 printf("Time logged on tasks: %s\n", delta2str($subtotals) );
658 }
659
660 print("# End of report\n") if $output >= 1;
661
662 }
663 elsif ( $command eq "status" )
664 {
665
666 my $trk_id = get_current_id();
667 if ( not $trk_id )
668 {
669 printf("Not currently tracking anything.\n");
670 $trk_id = get_last_id();
671 if ( $trk_id )
672 {
673 printf("Last track was: %s\n", get_track_name( $trk_id ) );
674 }
675 exit(1);
676 }
677 printf("Currently tracking: %s\n", get_track_name( $trk_id ) );
678 my $t = current_starttime();
679 printf("Tracking started at %s\n", scalar localtime $t);
680 printf("Time elapsed since start of session: %s\n", delta2str(time - $t) );
681
682 my $activity = get_current_id( $trk_id );
683 if ( $activity )
684 {
685 printf("\nCurrent sub-task/activity is: %s\n", get_track_name( $activity, $trk_id ) );
686 my $t = current_starttime($trk_id);
687 printf("Activity started at %s\n", scalar localtime $t);
688 printf("Time elapsed since start of activity: %s\n", delta2str(time - $t) );
689 }
690 else
691 {
692 $activity = get_last_id( $trk_id );
693 if ( $activity )
694 {
695 printf("\nLast track was: %s\n", get_track_name( $activity, $trk_id ) );
696 }
697 }
698 }
699 elsif ( $command eq "edit" )
700 {
701
702 my ( undef, $title ) = parse_arguments(EDIT);
703 my $id = get_last_id();
704
705 if ( $title )
706 {
707 $id = get_track_id($title);
708 if ( not $id )
709 {
710 printf("No project by that name. Try 'list'\n");
711 exit(0);
712 }
713 }
714
715 system( "/usr/bin/editor " . $trk_dir . "/" . $id . "/tracking" );
716 }
717 else
718 {
719 help();
720 }