]> git.defcon.no Git - trk/blob - trk
Added support for tasks/activities, removed some related bugs
[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 printf("%s\n", $i);
74 return 0 if not $i =~ m/(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d)/;
75 return parse_time($1, $2);
76 }
77
78 sub time2str ($)
79 {
80 my $t = shift;
81 return strftime("%Y-%m-%d %H:%M", localtime($t));
82 }
83
84 sub parse_arguments ($)
85 {
86
87 my $step = shift;
88
89 my $start_time = time;
90 my $title = undef;
91
92 if (( $#ARGV >= 1) && ( $ARGV[1] eq "at" ))
93 {
94 # Start and Activity require a title to be present.
95 # All other (stop, main...) do not ^^.
96 if ( ($step == START) || ($step == TASK) )
97 {
98 # TODO: Allow no title!
99 # If no title is given, read ID of previously used track in stead :)
100 help($step) unless $#ARGV > 3;
101 $title = join(" ", @ARGV[4..$#ARGV]);
102 }
103 help(TIMEFORMAT) unless ( $ARGV[2] =~ m/\d\d\d\d-\d\d-\d\d/ && $ARGV[3] =~ m/\d\d:\d\d/);
104
105 $start_time = parse_time( $ARGV[2], $ARGV[3] );
106 }
107 elsif ( ($step == START) || ($step == TASK) || ($step == EDIT))
108 {
109 shift(@ARGV);
110 $title = join(" ", @ARGV);
111 }
112
113 if ( not defined $title )
114 {
115 return $start_time;
116 }
117 else
118 {
119 return ( $start_time, $title );
120 }
121 }
122
123 sub get_last_id (;$)
124 {
125 my $trk_id = shift;
126 my $wrk_dir = $trk_dir;
127 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
128
129 return undef if ( ! -f $wrk_dir . "/last" );
130 open ( CUR, "<" . $wrk_dir . "/last" ) or die ("Unable to read last track file");
131 my $id = <CUR>;
132 chomp($id);
133 close(CUR);
134 return $id;
135 }
136
137 sub get_current_id (;$)
138 {
139 my $trk_id = shift;
140 my $wrk_dir = $trk_dir;
141 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
142
143 return undef if ( ! -f $wrk_dir . "/current" );
144 open ( CUR, "<" . $wrk_dir . "/current" ) or die ("Unable to read current track file");
145 my $id = <CUR>;
146 chomp($id);
147 close(CUR);
148 return $id;
149 }
150
151 sub set_current_id ($;$)
152 {
153 my $id = shift;
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 . "/current" );
159 open ( CUR, ">" . $wrk_dir . "/current" ) or die ("Unable to write current track file");
160 printf(CUR "%s\n", $id );
161 close(CUR);
162
163 open ( LAST, ">" . $wrk_dir . "/last" ) or die ("Unable to write last track file");
164 printf(LAST "%s\n", $id );
165 close(LAST);
166 }
167
168 sub get_tracks (;$)
169 {
170 my $trk_id = shift;
171 my $wrk_dir = $trk_dir;
172 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
173
174
175 my %tracks;
176
177 foreach my $d ( <$wrk_dir/*> )
178 {
179 next if not -d $d;
180 next if not -f $d . "/info";
181
182 my $id = basename($d);
183 my $title = get_track_name( $id, $trk_id );
184
185 $tracks{$id} = $title unless not defined $title;
186 }
187
188 return \%tracks;
189
190 }
191
192 sub get_track_id ($;$)
193 {
194 my $title = shift;
195 my $trk_id = shift;
196
197 # Get hash of track-id's and -names from get_tracks
198 my $tracks = get_tracks($trk_id);
199
200 # Look up name in list
201 foreach my $id ( keys $tracks )
202 {
203 # Return ID for name
204 return $id if ( $tracks->{$id} eq $title )
205 }
206
207 # If no match, return undef.
208 return undef;
209 }
210
211 sub get_track_name ($;$)
212 {
213 my $id = shift;
214 my $trk_id = shift;
215 my $wrk_dir = $trk_dir;
216 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
217
218 open(PRO, "<" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to read track medatata file!");
219 my $title = undef;
220 while (<PRO>)
221 {
222 next if not $_ =~ /^title:(.*)/;
223 $title = $1;
224 }
225 close(PRO);
226 return $title;
227 }
228
229 sub create_track ($;$)
230 {
231 my $title = shift;
232 my $trk_id = shift;
233 my $wrk_dir = $trk_dir;
234 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
235
236 my $id;
237 do
238 {
239 $id = gen_puuid(8);
240
241 } while ( -d $wrk_dir . "/" . $id );
242 mkdir ( $wrk_dir . "/" . $id );
243
244 open(PRO, ">" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to create track medatata file!");
245 printf(PRO "title:%s\n", $title);
246 close(PRO);
247
248 return $id;
249 }
250
251 sub start_track ($$;$)
252 {
253 my $start_time = shift;
254 my $title = shift;
255
256 my $trk_id = shift;
257 my $wrk_dir = $trk_dir;
258 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
259
260 my $current = get_current_id($trk_id);
261 if ( not $current )
262 {
263 if ( not $title )
264 {
265 $current = get_last_id( $trk_id );
266 }
267 else
268 {
269 $current = get_track_id( $title, $trk_id );
270 if ( not $current )
271 {
272 printf("No track by that name! Creating a new one.\n");
273 $current = create_track($title, $trk_id);
274 }
275 }
276
277 # Break off here if we haven't gotten an ID yet.
278 return undef if not $current;
279
280 set_current_id($current, $trk_id);
281
282 # First iteration is VERY naive: simply add the start time to the bottom of the tracking file
283 # Will have to do more logic: if the start point is before one of the times already in the track,
284 # the file will have to be manipulated to get coherent tracking!
285 open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
286 printf(TRACK "[%s]", time2str($start_time));
287 close (TRACK);
288
289 return $current;
290 }
291
292 return undef;
293 }
294
295 sub close_track ($;$)
296 {
297
298 my $stop_time = shift;
299 my $trk_id = shift;
300 my $wrk_dir = $trk_dir;
301 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
302
303 my $current = get_current_id( $trk_id );
304
305 die ("Project exists, but tracking file does not!") if ( not -f $wrk_dir . "/" . $current . "/tracking" );
306
307 # First iteration is VERY naive: simply add the stop time to the bottom line 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 # In addtion to this: actually do some file sanity checking!
311 open (TRACK, ">>" . $wrk_dir . "/" . $current . "/tracking" ) or die ("Unable to open file, $!");
312 printf(TRACK " to [%s]\n", time2str($stop_time));
313 close (TRACK);
314
315 unlink ( $wrk_dir . "/current" );
316 }
317
318 ############################################################
319
320 if ( ! -d $trk_dir )
321 {
322 mkdir $trk_dir or die("Unable to create data directory");
323 }
324
325 if ( $#ARGV < 0 )
326 {
327 help();
328 }
329
330 my $command = $ARGV[0];
331
332 if ( ( $command eq "start") || ($command eq "on" ) )
333 {
334 if ( $#ARGV < 1)
335 {
336 help(START);
337 }
338
339 my ( $start_time, $title ) = parse_arguments(START);
340
341 my $current = get_current_id();
342 if ( not $current )
343 {
344 $current = start_track( $start_time, $title );
345
346 if ( not $current )
347 {
348 printf("Something weird happened.\n");
349 exit(1);
350 }
351 }
352 else
353 {
354 printf("A project is being tracked: %s\n", get_track_name( $current ) );
355 close_track($start_time);
356 $current = start_track( $start_time, $title );
357 }
358
359 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
360 }
361 elsif ( ( $command eq "stop") || ($command eq "off" ) )
362 {
363 if ( $#ARGV < 0)
364 {
365 help(STOP);
366 }
367
368
369 my $stop_time = parse_arguments(STOP);
370
371 my $current = get_current_id();
372 if ( not $current )
373 {
374 printf("No project is currently tracked. To stop, please start first\n");
375 exit(0);
376 }
377 my $title = get_track_name( $current );
378
379 my $activity = get_current_id($current);
380 if ( $activity )
381 {
382 printf("An active subtask is running: '%s'. Closing it.\n", get_track_name( $activity, $current ));
383 close_track($stop_time, $current);
384 }
385 close_track($stop_time);
386
387 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
388 }
389 elsif ( ( $command eq "activity") || ($command eq "task" ) )
390 {
391 if ( $#ARGV < 1)
392 {
393 help(START);
394 }
395
396 my ( $start_time, $title ) = parse_arguments(START);
397
398 my $trk_id = get_current_id();
399 if ( not $trk_id )
400 {
401 printf("Starting a task/activity requires an active main track.\n");
402 exit(1);
403 }
404 else
405 {
406
407 my $current = get_current_id( $trk_id );
408 if ( not $current )
409 {
410 $current = start_track( $start_time, $title, $trk_id );
411
412 if ( not $current )
413 {
414 printf("Something weird happened.\n");
415 exit(1);
416 }
417 }
418 else
419 {
420 printf("A task/activity is being tracked: %s\n", get_track_name( $current, $trk_id ) );
421 close_track($start_time, $trk_id);
422 $current = start_track( $start_time, $title, $trk_id );
423 }
424
425 printf("Started tracking of '%s' at %s\n\n", $title, scalar localtime $start_time);
426
427 }
428
429 }
430
431 elsif ( $command eq "main" )
432 {
433
434 if ( $#ARGV < 0)
435 {
436 help(STOP);
437 }
438
439
440 my $stop_time = parse_arguments(STOP);
441
442 my $trk_id = get_current_id();
443 if ( not $trk_id )
444 {
445 printf("Stopping a task/activity requires an active main track.\n");
446 exit(1);
447 }
448 else
449 {
450 my $current = get_current_id( $trk_id );
451 if ( not $current )
452 {
453 printf("No activity/task is currently tracked. To stop, please start first\n");
454 exit(0);
455 }
456 my $title = get_track_name( $current, $trk_id );
457 close_track($stop_time, $trk_id);
458
459 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
460 }
461 }
462 elsif ( ( $command eq "projects" ) || ( $command eq "list" ) )
463 {
464 # Todo/future extensions:
465 # TODO: Sort list of names alphabetically
466 # TODO: Get total-hours for projects
467 # TODO:
468 my $tracks = get_tracks();
469 printf("Currently tracked project names:\n\n");
470 my $current = get_current_id();
471
472 foreach my $id ( keys $tracks )
473 {
474 printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} );
475 }
476 print("\n");
477 }
478 elsif ( $command eq "edit" )
479 {
480
481 my ( undef, $title ) = parse_arguments(EDIT);
482 my $id = get_last_id();
483
484 if ( $title )
485 {
486 $id = get_track_id($title);
487 if ( not $id )
488 {
489 printf("No project by that name. Try 'list'\n");
490 exit(0);
491 }
492 }
493
494 system( "/usr/bin/editor " . $trk_dir . "/" . $id . "/tracking" );
495 }
496 else
497 {
498 help();
499 }