]> git.defcon.no Git - trk/blob - trk
Fixed up reporting to be more useful
[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 printf("It seems you require assistance\n");
48
49 if ( $code )
50 {
51 printf("How to start\n") if $code == START;
52 printf("How to time\n") if $code == TIMEFORMAT;
53 }
54 exit(-1);
55 }
56
57
58 sub gen_puuid (;$)
59 {
60 my $id_length = shift;
61 $id_length = 32 if not defined $id_length;
62
63 my $w = time;
64
65 for(my $i=0 ; $i<128;)
66 {
67 my $tc = chr(int(rand(127)));
68 if($tc =~ /[a-zA-Z0-9]/)
69 {
70 $w .=$tc;
71 $i++;
72 }
73 }
74 $w = md5_hex( $w );
75
76 while ( length($w) < $id_length )
77 {
78 $w .= gen_puuid( $id_length - length( $w ) );
79 }
80
81 $w = substr( $w, 0, $id_length );
82 return $w;
83 }
84
85 # Input to parse_time is:
86 # * date -> date-string in the form YYYY-MM-DD
87 # * time -> time-string in the form HH:MM
88 # Return value is a unix timestamp, as returned by time()
89 sub parse_time ($$)
90 {
91 my ( $Y, $M, $D ) = split ("-", shift );
92 my ( $h, $m ) = split(":", shift );
93 return timelocal(0, $m, $h, $D, ($M-1), $Y);
94 }
95
96 sub str2time ($)
97 {
98 my $i = shift;
99 return 0 if not $i =~ m/(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d)/;
100 return parse_time($1, $2);
101 }
102
103 sub time2str ($)
104 {
105 my $t = shift;
106 return strftime("%Y-%m-%d %H:%M", localtime($t));
107 }
108
109 sub parse_arguments ($)
110 {
111
112 my $step = shift;
113
114 my $start_time = time;
115 my $title = undef;
116
117 if (( $#ARGV >= 1) && ( $ARGV[1] eq "at" ))
118 {
119 # Start and Activity require a title to be present.
120 # All other (stop, main...) do not ^^.
121 if ( ($step == START) || ($step == TASK) )
122 {
123 # TODO: Allow no title!
124 # If no title is given, read ID of previously used track in stead :)
125 help($step) unless $#ARGV > 3;
126 $title = join(" ", @ARGV[4..$#ARGV]);
127 }
128 help(TIMEFORMAT) unless ( $ARGV[2] =~ m/\d\d\d\d-\d\d-\d\d/ && $ARGV[3] =~ m/\d\d:\d\d/);
129
130 $start_time = parse_time( $ARGV[2], $ARGV[3] );
131 }
132 elsif ( ($step == START) || ($step == TASK) || ($step == EDIT))
133 {
134 shift(@ARGV);
135 $title = join(" ", @ARGV);
136 }
137
138 if ( not defined $title )
139 {
140 return $start_time;
141 }
142 else
143 {
144 return ( $start_time, $title );
145 }
146 }
147
148 sub get_last_id (;$)
149 {
150 my $trk_id = shift;
151 my $wrk_dir = $trk_dir;
152 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
153
154 return undef if ( ! -f $wrk_dir . "/last" );
155 open ( CUR, "<" . $wrk_dir . "/last" ) or die ("Unable to read last track file");
156 my $id = <CUR>;
157 chomp($id);
158 close(CUR);
159 return $id;
160 }
161
162 sub get_current_id (;$)
163 {
164 my $trk_id = shift;
165 my $wrk_dir = $trk_dir;
166 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
167
168 return undef if ( ! -f $wrk_dir . "/current" );
169 open ( CUR, "<" . $wrk_dir . "/current" ) or die ("Unable to read current track file");
170 my $id = <CUR>;
171 chomp($id);
172 close(CUR);
173 return $id;
174 }
175
176 sub set_current_id ($;$)
177 {
178 my $id = shift;
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 write current track file");
185 printf(CUR "%s\n", $id );
186 close(CUR);
187
188 open ( LAST, ">" . $wrk_dir . "/last" ) or die ("Unable to write last track file");
189 printf(LAST "%s\n", $id );
190 close(LAST);
191 }
192
193 sub get_tracks (;$)
194 {
195 my $trk_id = shift;
196 my $wrk_dir = $trk_dir;
197 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
198
199
200 my %tracks;
201
202 foreach my $d ( <$wrk_dir/*> )
203 {
204 next if not -d $d;
205 next if not -f $d . "/info";
206
207 my $id = basename($d);
208 my $title = get_track_name( $id, $trk_id );
209
210 $tracks{$id} = $title unless not defined $title;
211 }
212
213 return \%tracks;
214
215 }
216
217 sub get_track_id ($;$)
218 {
219 my $title = shift;
220 my $trk_id = shift;
221
222 # Get hash of track-id's and -names from get_tracks
223 my $tracks = get_tracks($trk_id);
224
225 # Look up name in list
226 foreach my $id ( keys %$tracks )
227 {
228 # Return ID for name
229 return $id if ( $tracks->{$id} eq $title )
230 }
231
232 # If no match, return undef.
233 return undef;
234 }
235
236 sub get_track_name ($;$)
237 {
238 my $id = shift;
239 my $trk_id = shift;
240 my $wrk_dir = $trk_dir;
241 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
242
243 open(PRO, "<" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to read track medatata file!");
244 my $title = undef;
245 while (<PRO>)
246 {
247 next if not $_ =~ /^title:(.*)/;
248 $title = $1;
249 }
250 close(PRO);
251 return $title;
252 }
253
254 sub create_track ($;$)
255 {
256 my $title = shift;
257 my $trk_id = shift;
258 my $wrk_dir = $trk_dir;
259 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
260
261 my $id;
262 do
263 {
264 $id = gen_puuid(8);
265
266 } while ( -d $wrk_dir . "/" . $id );
267 mkdir ( $wrk_dir . "/" . $id );
268
269 open(PRO, ">" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to create track medatata file!");
270 printf(PRO "title:%s\n", $title);
271 close(PRO);
272
273 return $id;
274 }
275
276 sub start_track ($$;$)
277 {
278 my $start_time = shift;
279 my $title = shift;
280
281 my $trk_id = shift;
282 my $wrk_dir = $trk_dir;
283 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
284
285 my $current = get_current_id($trk_id);
286 if ( not $current )
287 {
288 if ( not $title )
289 {
290 $current = get_last_id( $trk_id );
291 }
292 else
293 {
294 $current = get_track_id( $title, $trk_id );
295 if ( not $current )
296 {
297 printf("No track by that name! Creating a new one.\n");
298 $current = create_track($title, $trk_id);
299 }
300 }
301
302 # Break off here if we haven't gotten an ID yet.
303 return undef if not $current;
304
305 set_current_id($current, $trk_id);
306
307 # First iteration is VERY naive: simply add the start time to the bottom of the tracking file
308 # Will have to do more logic: if the start point is before one of the times already in the track,
309 # the file will have to be manipulated to get coherent tracking!
310 open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
311 printf(TRACK "[%s]", time2str($start_time));
312 close (TRACK);
313
314 return $current;
315 }
316
317 return undef;
318 }
319
320 sub close_track ($;$)
321 {
322
323 my $stop_time = shift;
324 my $trk_id = shift;
325 my $wrk_dir = $trk_dir;
326 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
327
328 my $current = get_current_id( $trk_id );
329
330 die ("Project exists, but tracking file does not!") if ( not -f $wrk_dir . "/" . $current . "/tracking" );
331
332 # First iteration is VERY naive: simply add the stop time to the bottom line of the tracking file
333 # Will have to do more logic: if the start point is before one of the times already in the track,
334 # the file will have to be manipulated to get coherent tracking!
335 # In addtion to this: actually do some file sanity checking!
336 open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
337 printf(TRACK " to [%s]\n", time2str($stop_time));
338 close (TRACK);
339
340 unlink ( $wrk_dir . "/current" );
341 }
342
343
344 sub report ($$;$)
345 {
346 my $current = shift;
347 my $silent = shift;
348 my $trk_id = shift;
349 my $wrk_dir = $trk_dir;
350 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
351
352 my $total = 0;
353
354 my $name = get_track_name( $current, $trk_id );
355 printf("# Report for '%s':\n\n", $name) unless $silent;
356
357 open (TRACK, "<" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
358 while ( <TRACK> )
359 {
360 next if not $_ =~ m/\[(.*)\] to \[(.*)\]/;
361 my $start = $1;
362 my $end = $2;
363 my $t_start = str2time( $start );
364 my $t_end = str2time( $end );
365 my $delta = $t_end - $t_start;
366
367 if ( not $silent )
368 {
369 my $t = $delta;
370 my $hours = $t / 3600;
371 $t = $delta % 3600;
372 my $minutes = $t / 60;
373 printf(" %s to %s => %d hours %d minutes\n", $start, $end, $hours, $minutes);
374 }
375 $total += $delta;
376
377 }
378 close ( TRACK );
379 return $total;
380
381 }
382
383 ############################################################
384
385 if ( ! -d $trk_dir )
386 {
387 mkdir $trk_dir or die("Unable to create data directory");
388 }
389
390 if ( $#ARGV < 0 )
391 {
392 help();
393 }
394
395 my $command = $ARGV[0];
396
397 if ( ( $command eq "start") || ($command eq "on" ) )
398 {
399 if ( $#ARGV < 1)
400 {
401 help(START);
402 }
403
404 my ( $start_time, $title ) = parse_arguments(START);
405
406 my $current = get_current_id();
407 if ( not $current )
408 {
409 $current = start_track( $start_time, $title );
410
411 if ( not $current )
412 {
413 printf("Something weird happened.\n");
414 exit(1);
415 }
416 }
417 else
418 {
419 printf("A project is being tracked: %s\n", get_track_name( $current ) );
420 close_track($start_time);
421 $current = start_track( $start_time, $title );
422 }
423
424 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
425 }
426 elsif ( ( $command eq "stop") || ($command eq "off" ) )
427 {
428 if ( $#ARGV < 0)
429 {
430 help(STOP);
431 }
432
433
434 my $stop_time = parse_arguments(STOP);
435
436 my $current = get_current_id();
437 if ( not $current )
438 {
439 printf("No project is currently tracked. To stop, please start first\n");
440 exit(0);
441 }
442 my $title = get_track_name( $current );
443
444 my $activity = get_current_id($current);
445 if ( $activity )
446 {
447 printf("An active subtask is running: '%s'. Closing it.\n", get_track_name( $activity, $current ));
448 close_track($stop_time, $current);
449 }
450 close_track($stop_time);
451
452 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
453 }
454 elsif ( ( $command eq "activity") || ($command eq "task" ) )
455 {
456 if ( $#ARGV < 1)
457 {
458 help(START);
459 }
460
461 my ( $start_time, $title ) = parse_arguments(START);
462
463 my $trk_id = get_current_id();
464 if ( not $trk_id )
465 {
466 printf("Starting a task/activity requires an active main track.\n");
467 exit(1);
468 }
469 else
470 {
471
472 my $current = get_current_id( $trk_id );
473 if ( not $current )
474 {
475 $current = start_track( $start_time, $title, $trk_id );
476
477 if ( not $current )
478 {
479 printf("Something weird happened.\n");
480 exit(1);
481 }
482 }
483 else
484 {
485 printf("A task/activity is being tracked: %s\n", get_track_name( $current, $trk_id ) );
486 close_track($start_time, $trk_id);
487 $current = start_track( $start_time, $title, $trk_id );
488 }
489
490 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
491
492 }
493
494 }
495
496 elsif ( $command eq "main" )
497 {
498
499 if ( $#ARGV < 0)
500 {
501 help(STOP);
502 }
503
504
505 my $stop_time = parse_arguments(STOP);
506
507 my $trk_id = get_current_id();
508 if ( not $trk_id )
509 {
510 printf("Stopping a task/activity requires an active main track.\n");
511 exit(1);
512 }
513 else
514 {
515 my $current = get_current_id( $trk_id );
516 if ( not $current )
517 {
518 printf("No activity/task is currently tracked. To stop, please start first\n");
519 exit(0);
520 }
521 my $title = get_track_name( $current, $trk_id );
522 close_track($stop_time, $trk_id);
523
524 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
525 }
526 }
527 elsif ( ( $command eq "projects" ) || ( $command eq "list" ) )
528 {
529 # Todo/future extensions:
530 # TODO: Sort list of names alphabetically
531 # TODO: Get total-hours for projects
532 # TODO:
533 my $tracks = get_tracks();
534 printf("Currently tracked project names:\n\n");
535 my $current = get_current_id();
536
537 #foreach my $id ( keys %$tracks )
538 foreach my $id ( sort { $tracks->{$a} cmp $tracks->{$b} || $a cmp $b } keys %$tracks )
539 {
540 printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} );
541 }
542 print("\n");
543 }
544 elsif ( $command eq "report" )
545 {
546
547 my $format = "standard";
548 my $output = 0;
549
550 if (( $#ARGV >= 1) &&
551 ( ( $ARGV[1] eq "standard" )
552 || ( $ARGV[1] eq "terse" )
553 || ( $ARGV[1] eq "verbose" )
554 || ( $ARGV[1] eq "details" ) ) )
555 {
556 $format = $ARGV[1];
557 shift @ARGV;
558 }
559
560 $output = 0 if $format eq "terse";
561 $output = 1 if $format eq "standard";
562 $output = 2 if $format eq "verbose";
563 $output = 3 if $format eq "details";
564
565 my ( undef, $title ) = parse_arguments(START);
566
567 printf("Report format: %s\nTitle: %s\n", $format, $title);
568
569 my $track = undef;
570
571 if ( $title )
572 {
573 $track = get_track_id( $title );
574 }
575 else
576 {
577 $track = get_last_id();
578 }
579
580 if ( not $track )
581 {
582 printf ("Unable to get info for that track\n");
583 exit(1);
584 }
585
586 my $total = 0;
587 my $subtotals = 0;
588
589 my $activities = get_tracks( $track );
590 if ( keys %$activities )
591 {
592 printf("# Reporting for sub-task/activities:\n\n") if $output >= 2;
593
594 foreach my $id ( sort { $activities->{$a} cmp $activities->{$b} || $a cmp $b } keys %$activities )
595 #foreach my $id ( keys %$activities )
596 {
597 $subtotals += report( $id, ( $output >= 2 ? 0 : 1 ), $track );
598 printf("# --------------------------------------------------------------\n\n") if $output >= 2;
599 }
600 }
601
602 printf("# Reporting for main track/project/task\n") if $output >= 2;
603 $total += report($track, ( ( $output >= 1 ? 0 : 1 ) ) );
604 printf("\n# ==============================================================\n\n") if $output >= 2;
605 print("\n") if $output >= 1;
606
607
608 my $t = $total;
609 my $hours = $t / 3600;
610 $t = $total % 3600;
611 my $minutes = $t / 60;
612
613 printf("Total: %d hours %d minutes\n", $hours, $minutes);
614 if ( $output >= 2 )
615 {
616 my $t = $subtotals;
617 my $hours = $t / 3600;
618 $t = $subtotals % 3600;
619 my $minutes = $t / 60;
620 printf("Time logged on tasks: %d hours %d minutes\n", $hours, $minutes);
621 }
622
623 print("# End of report\n") if $output >= 1;
624
625 }
626
627 elsif ( $command eq "edit" )
628 {
629
630 my ( undef, $title ) = parse_arguments(EDIT);
631 my $id = get_last_id();
632
633 if ( $title )
634 {
635 $id = get_track_id($title);
636 if ( not $id )
637 {
638 printf("No project by that name. Try 'list'\n");
639 exit(0);
640 }
641 }
642
643 system( "/usr/bin/editor " . $trk_dir . "/" . $id . "/tracking" );
644 }
645 else
646 {
647 help();
648 }