]> git.defcon.no Git - trk/blob - trk
Added optional arguments, may be generified enough to support a nested info-structure
[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
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 };
16
17 sub help
18 {
19 my $code = shift;
20 printf("It seems you require assistance\n");
21
22 if ( $code )
23 {
24 printf("How to start\n") if $code == START;
25 printf("How to time\n") if $code == TIMEFORMAT;
26 }
27 exit(-1);
28 }
29
30
31 sub gen_puuid (;$)
32 {
33 my $id_length = shift;
34 $id_length = 32 if not defined $id_length;
35
36 my $w = time;
37
38 for(my $i=0 ; $i<128;)
39 {
40 my $tc = chr(int(rand(127)));
41 if($tc =~ /[a-zA-Z0-9]/)
42 {
43 $w .=$tc;
44 $i++;
45 }
46 }
47 $w = md5_hex( $w );
48
49 while ( length($w) < $id_length )
50 {
51 $w .= gen_puuid( $id_length - length( $w ) );
52 }
53
54 $w = substr( $w, 0, $id_length );
55 return $w;
56 }
57
58 # Input to parse_time is:
59 # * date -> date-string in the form YYYY-MM-DD
60 # * time -> time-string in the form HH:MM
61 # Return value is a unix timestamp, as returned by time()
62 sub parse_time ($$)
63 {
64 my ( $Y, $M, $D ) = split ("-", shift );
65 my ( $h, $m ) = split(":", shift );
66 return timelocal(0, $m, $h, $D, ($M-1), $Y);
67 }
68
69 sub str2time ($)
70 {
71 my $i = shift;
72 printf("%s\n", $i);
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 my %tracks;
174
175 foreach my $d ( <$wrk_dir/*> )
176 {
177 next if not -d $d;
178 next if not -f $d . "/info";
179
180 my $id = basename($d);
181 my $title = get_track_name( $id );
182
183 $tracks{$id} = $title unless not defined $title;
184 }
185
186 return \%tracks;
187
188 }
189
190 sub get_track_id ($;$)
191 {
192 my $title = shift;
193 my $trk_id = shift;
194
195 # Get hash of track-id's and -names from get_tracks
196 my $tracks = get_tracks($trk_id);
197
198 # Look up name in list
199 foreach my $id ( keys $tracks )
200 {
201 # Return ID for name
202 return $id if ( $tracks->{$id} eq $title )
203 }
204
205 # If no match, return undef.
206 return undef;
207 }
208
209 sub get_track_name ($;$)
210 {
211 my $id = shift;
212 my $trk_id = shift;
213 my $wrk_dir = $trk_dir;
214 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
215
216 open(PRO, "<" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to read track medatata file!");
217 my $title = undef;
218 while (<PRO>)
219 {
220 next if not $_ =~ /^title:(.*)/;
221 $title = $1;
222 }
223 close(PRO);
224 return $title;
225 }
226
227 sub create_track ($;$)
228 {
229 my $title = shift;
230 my $trk_id = shift;
231 my $wrk_dir = $trk_dir;
232 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
233
234 my $id;
235 do
236 {
237 $id = gen_puuid(8);
238
239 } while ( -d $wrk_dir . "/" . $id );
240 mkdir ( $wrk_dir . "/" . $id );
241
242 open(PRO, ">" . $wrk_dir . "/" . $id . "/info" ) or die ("Unable to create track medatata file!");
243 printf(PRO "title:%s\n", $title);
244 close(PRO);
245
246 return $id;
247 }
248
249 sub start_track ($$;$)
250 {
251 my $start_time = shift;
252 my $title = shift;
253
254 my $trk_id = shift;
255 my $wrk_dir = $trk_dir;
256 $wrk_dir = $trk_dir . "/" . $trk_id if $trk_id;
257
258
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 ( $trk_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 close_track($stop_time);
380
381 printf("Stopped tracking of '%s' at %s\n\n", $title, scalar localtime $stop_time);
382 }
383 elsif ( ( $command eq "projects" ) || ( $command eq "list" ) )
384 {
385 # Todo/future extensions:
386 # TODO: Sort list of names alphabetically
387 # TODO: Get total-hours for projects
388 # TODO:
389 my $tracks = get_tracks();
390 printf("Currently tracked project names:\n\n");
391 my $current = get_current_id();
392
393 foreach my $id ( keys $tracks )
394 {
395 printf(" %s %s\n", ($id eq $current ? ">" : " " ),$tracks->{$id} );
396 }
397 print("\n");
398 }
399 elsif ( $command eq "edit" )
400 {
401
402 my ( undef, $title ) = parse_arguments(EDIT);
403 my $id = get_last_id();
404
405 if ( $title )
406 {
407 $id = get_track_id($title);
408 if ( not $id )
409 {
410 printf("No project by that name. Try 'list'\n");
411 exit(0);
412 }
413 }
414
415 system( "/usr/bin/editor " . $trk_dir . "/" . $id . "/tracking" );
416 }
417 else
418 {
419 help();
420 }