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