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