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