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