File Coverage

lib/App/TimeTracker.pm
Criterion Covered Total %
statement 109 135 80.7
branch 38 54 70.3
condition 11 19 57.8
subroutine 18 19 94.7
pod 0 6 0.0
total 176 233 75.5


line stmt bran cond sub pod time code
1             package App::TimeTracker;
2              
3             # ABSTRACT: time tracking for impatient and lazy command line lovers
4             our $VERSION = '3.010'; # VERSION
5              
6 13     13   1584700 use strict;
  13         51  
  13         408  
7 13     13   68 use warnings;
  13         28  
  13         385  
8 13     13   266 use 5.010;
  13         48  
9              
10 13     13   1420 use App::TimeTracker::Data::Task;
  13         32  
  13         453  
11 13     13   6380 use DateTime;
  13         3319819  
  13         516  
12 13     13   98 use Moose;
  13         29  
  13         184  
13 13     13   94564 use Moose::Util::TypeConstraints;
  13         36  
  13         140  
14 13     13   33874 use Path::Class qw();
  13         96740  
  13         319  
15 13     13   7867 use Path::Class::Iterator;
  13         119037  
  13         164  
16 13     13   5907 use MooseX::Storage::Format::JSONpm;
  13         865119  
  13         593  
17 13     13   110 use JSON::XS;
  13         30  
  13         5420  
18              
19             our $HOUR_RE = qr/(?<hour>[012]?\d)/;
20             our $MINUTE_RE = qr/(?<minute>[0-5]?\d)/;
21             our $DAY_RE = qr/(?<day>[0123]?\d)/;
22             our $MONTH_RE = qr/(?<month>[01]?\d)/;
23             our $YEAR_RE = qr/(?<year>2\d{3})/;
24              
25             with qw(
26             MooseX::Getopt
27             );
28              
29             subtype 'TT::DateTime' => as class_type('DateTime');
30             subtype 'TT::RT' => as 'Int';
31             subtype 'TT::Duration' => as enum( [qw(day week month year)] );
32              
33             coerce 'TT::RT' => from 'Str' => via {
34             my $raw = $_;
35             $raw =~ s/\D//g;
36             return $raw;
37             };
38              
39             coerce 'TT::DateTime' => from 'Str' => via {
40             my $raw = $_;
41             my $dt = DateTime->now;
42             $dt->set_time_zone('local');
43             $dt->set( second => 0 );
44              
45             if ($raw) {
46             if ( $raw =~ /^ $HOUR_RE : $MINUTE_RE $/x ) { # "13:42"
47 12     12   5583 $dt->set( hour => $+{hour}, minute => $+{minute} );
  12         5483  
  12         3744  
48             }
49             elsif ( $raw =~ /^ $YEAR_RE [-.]? $MONTH_RE [-.]? $DAY_RE $/x )
50             { # "2010-02-26"
51             $dt->set( year => $+{year}, month => $+{month}, day => $+{day} );
52             $dt->truncate( to => 'day' );
53             }
54             elsif ( $raw
55             =~ /^ $YEAR_RE [-.]? $MONTH_RE [-.]? $DAY_RE \s+ $HOUR_RE : $MINUTE_RE $/x
56             )
57             { # "2010-02-26 12:34"
58             $dt->set(
59             year => $+{year},
60             month => $+{month},
61             day => $+{day},
62             hour => $+{hour},
63             minute => $+{minute} );
64             }
65             elsif ( $raw =~ /^ $DAY_RE [-.]? $MONTH_RE [-.]? $YEAR_RE $/x )
66             { # "26-02-2010"
67             $dt->set( year => $+{year}, month => $+{month}, day => $+{day} );
68             $dt->truncate( to => 'day' );
69             }
70             elsif ( $raw
71             =~ /^ $DAY_RE [-.]? $MONTH_RE [-.]? $YEAR_RE \s $HOUR_RE : $MINUTE_RE $/x
72             )
73             { # "26-02-2010 12:34"
74             $dt->set(
75             year => $+{year},
76             month => $+{month},
77             day => $+{day},
78             hour => $+{hour},
79             minute => $+{minute} );
80             }
81             else {
82             confess "Invalid date format '$raw'";
83             }
84             }
85             return $dt;
86             };
87              
88             MooseX::Getopt::OptionTypeMap->add_option_type_to_map( 'TT::DateTime' => '=s',
89             );
90             MooseX::Getopt::OptionTypeMap->add_option_type_to_map( 'TT::RT' => '=i', );
91              
92 13     13   188 no Moose::Util::TypeConstraints;
  13         27  
  13         148  
93              
94             has 'home' => (
95             is => 'ro',
96             isa => 'Path::Class::Dir',
97             traits => ['NoGetopt'],
98             required => 1,
99             );
100             has 'config' => (
101             is => 'ro',
102             isa => 'HashRef',
103             required => 1,
104             traits => ['NoGetopt'],
105             );
106             has '_current_project' => (
107             is => 'ro',
108             isa => 'Str',
109             predicate => 'has_current_project',
110             traits => ['NoGetopt'],
111             );
112              
113             has 'tags' => (
114             isa => 'ArrayRef',
115             is => 'ro',
116             traits => ['Array'],
117             default => sub { [] },
118             handles => {
119             insert_tag => 'unshift',
120             add_tag => 'push',
121             },
122             documentation => 'Tags [Multiple]',
123             );
124              
125             has '_current_command' => (
126             isa => 'Str',
127             is => 'rw',
128             traits => ['NoGetopt'],
129             );
130              
131             has '_current_task' => (
132             isa => 'App::TimeTracker::Data::Task',
133             is => 'rw',
134             traits => ['NoGetopt'],
135             );
136              
137             has '_previous_task' => (
138             isa => 'App::TimeTracker::Data::Task',
139             is => 'rw',
140             traits => ['NoGetopt'],
141             );
142              
143             sub run {
144 1     1 0 18 my $self = shift;
145 1   50     32 my $command = 'cmd_' . ( $self->extra_argv->[0] || 'missing' );
146              
147 1 50       23 $self->cmd_commands()
148             unless $self->can($command);
149 0         0 $self->_current_command($command);
150 0         0 $self->$command;
151             }
152              
153             sub now {
154 2     2 0 6152 my $dt = DateTime->now();
155 2         627 $dt->set_time_zone('local');
156 2         4842 return $dt;
157             }
158              
159             sub beautify_seconds {
160 15     15 0 9868 my ( $self, $s ) = @_;
161 15 100       41 return '0' unless $s;
162 13         33 my ( $m, $h ) = ( 0, 0 );
163              
164 13 100       38 if ( $s >= 60 ) {
165 12         33 $m = int( $s / 60 );
166 12         37 $s = $s - ( $m * 60 );
167             }
168 13 100 100     63 if ( $m && $m >= 60 ) {
169 4         18 $h = int( $m / 60 );
170 4         9 $m = $m - ( $h * 60 );
171             }
172 13         213 return sprintf( "%02d:%02d:%02d", $h, $m, $s );
173             }
174              
175             sub find_task_files {
176 5     5 0 12061 my ( $self, $args ) = @_;
177              
178 5         173 my $root = $self->home;
179 5         14 my ( $cmp_from, $cmp_to );
180              
181 5 100       24 if ( my $from = $args->{from} ) {
182 4   66     38 my $to = $args->{to} || $self->now;
183 4 100       46 $to->set( hour => 23, minute => 59, second => 59 ) unless $to->hour;
184 4         722 $cmp_from = $from->strftime("%Y%m%d%H%M%S");
185 4         431 $cmp_to = $to->strftime("%Y%m%d%H%M%S");
186              
187 4 100       371 if ( $from->year == $to->year ) {
188 3         37 $root = $root->subdir( $from->year );
189 3 50       252 if ( $from->month == $to->month ) {
190 3         36 $root = $root->subdir( $from->strftime("%m") );
191             }
192             }
193             }
194              
195 5         243 my $projects;
196 5 100       20 if ( $args->{projects} ) {
197 3         8 $projects = join( '|', map { s/-/./g; $_ } @{ $args->{projects} } );
  3         9  
  3         14  
  3         11  
198             }
199              
200 5         12 my $children;
201 5 50       18 if ($args->{parent}) {
202 0         0 my @kids = $args->{parent};
203 0         0 $self->all_childs_of($args->{parent},\@kids);
204 0         0 $children = join( '|', map { s/-/./g; $_ } @kids );
  0         0  
  0         0  
205             }
206              
207 5         9 my $tags;
208 5 50       16 if ( $args->{tags} ) {
209 0         0 $tags = join( '|', @{ $args->{tags} } );
  0         0  
210             }
211              
212 5         10 my @found;
213 5         42 my $iterator = Path::Class::Iterator->new( root => $root, );
214 5   66     20833 until ( !$iterator || $iterator->done ) {
215 79         2706 my $file = $iterator->next;
216              
217 79 100       47171 next unless -f $file;
218 75         3376 my $name = $file->basename;
219 75 50       602 next unless $name =~ /\.trc$/;
220              
221 75 100       173 if ($cmp_from) {
222 52         122 $file =~ /(\d{8})-(\d{6})/;
223 52         1714 my $time = $1 . $2;
224 52 100       236 next if $time < $cmp_from;
225 41 100       127 next if $time > $cmp_to;
226             }
227              
228 58 100       130 if ($projects) {
229 49 100       337 next unless ( $name =~ m/$projects/i );
230             }
231              
232 34 50       74 if ($children) {
233 0 0       0 next unless ( $name =~ m/$children/i );
234             }
235              
236 34 50       65 if ($tags) {
237 0         0 my $raw_content = $file->slurp;
238 0 0       0 next unless $raw_content =~ /$tags/i;
239             }
240              
241 34         166 push( @found, $file );
242             }
243 5         201 return sort @found;
244             }
245              
246             sub project_tree {
247 2     2 0 6 my $self = shift;
248 2         64 my $file = $self->home->file('projects.json');
249 2 50 33     181 return unless -e $file && -s $file;
250 2         244 my $decoder = JSON::XS->new->utf8->pretty->relaxed;
251 2         14 my $projects = $decoder->decode( scalar $file->slurp );
252              
253 2         607 my %tree;
254             my $depth;
255 2         16 while ( my ( $project, $location ) = each %$projects ) {
256 16   50     97 $tree{$project} //= { parent => undef, children => {} };
257             # check config file for parent
258 16 100       292 if ( -e $location ) {
259 14         97 my $this_config = $decoder->decode(
260             scalar Path::Class::file($location)->slurp );
261 14 50       4146 if ( my $parent = $this_config->{parent} ) {
262 0         0 $tree{$project}->{parent} = $parent;
263 0         0 $tree{$parent}->{children}{$project} = 1;
264 0         0 next;
265             }
266             }
267             # check path for parent
268 16         62 my @parts = Path::Class::file($location)->parent->parent->dir_list;
269 16         2737 foreach my $dir (@parts) {
270 48 50 33     217 if ( $project ne $dir and my $parent = $projects->{$dir} ) {
271 0         0 $tree{$project}->{parent} = $dir;
272 0         0 $tree{$dir}->{children}{$project} = 1;
273             }
274             }
275             }
276              
277 2         26 return \%tree;
278             }
279              
280             sub all_childs_of {
281 0     0 0 0 my ($self, $parent, $collector) = @_;
282              
283 0         0 my $tree = $self->project_tree;
284 0         0 my $this = $tree->{$parent};
285              
286 0         0 my @kids = keys %{$this->{children}};
  0         0  
287              
288 0 0       0 if (@kids) {
289 0         0 push(@$collector, @kids);
290 0         0 foreach my $kid (@kids) {
291 0         0 $self->all_childs_of($kid, $collector);
292             }
293             }
294             }
295              
296             1;
297              
298             __END__
299              
300             =pod
301              
302             =encoding UTF-8
303              
304             =head1 NAME
305              
306             App::TimeTracker - time tracking for impatient and lazy command line lovers
307              
308             =head1 VERSION
309              
310             version 3.010
311              
312             =head1 SYNOPSIS
313              
314             Backend for the C<tracker> command. See L<tracker> and/or C<perldoc tracker> for details.
315              
316             =head1 INSTALLATION
317              
318             L<App::TimeTracker> is a L<Perl|http://perl.org> application, and thus requires
319             a recent Perl (>= 5.10). It also reuses a lot of code from
320             L<CPAN|http://cpan.org>.
321              
322             =head2 From CPAN
323              
324             The easiest way to install the current stable version of L<App::TimeTracker> is
325             via L<CPAN|http://cpan.org>. There are several different CPAN clients
326             available:
327              
328             =head3 cpanminus
329              
330             ~$ cpanm App::TimeTracker
331             --> Working on App::TimeTracker
332             Fetching http://search.cpan.org/CPAN/authors/id/D/DO/DOMM/App-TimeTracker-2.009.tar.gz ... OK
333             Configuring App-TimeTracker-2.009 ... OK
334             Building and testing App-TimeTracker-2.009 ... OK
335             Successfully installed App-TimeTracker-2.009
336             1 distribution installed
337              
338             If you don't have C<cpanminus> installed yet, L<install it right
339             now|http://search.cpan.org/dist/App-cpanminus/lib/App/cpanminus.pm#INSTALLATION>:
340              
341             ~$ curl -L http://cpanmin.us | perl - --sudo App::cpanminus
342              
343             =head3 CPAN.pm
344              
345             CPAN.pm is available on ancient Perls, and feels a bit ancient, too.
346              
347             cpan App::TimeTracker
348              
349             =head2 From a tarball
350              
351             To install L<App::TimeTracker> from a tarball, do the usual CPAN module
352             install dance:
353              
354             ~/perl/App-TimeTracker$ perl Build.PL
355             ~/perl/App-TimeTracker$ ./Build
356             ~/perl/App-TimeTracker$ ./Build test
357             ~/perl/App-TimeTracker$ ./Build install # might require sudo
358              
359             =head2 From a git checkout
360              
361             Clone the repository if you have not already done so, and enter the
362             C<App-TimeTracker> directory:
363              
364             ~$ git clone git@github.com:domm/App-TimeTracker.git
365             ~$ cd App-TimeTracker
366              
367             C<App-TimeTracker> uses L<Dist::Zilla> to build, test and install the code,
368             hence this must be installed first, e.g. with C<cpanm>:
369              
370             ~/path/to/App-Tracker$ cpanm Dist::Zilla
371              
372             Now install the distribution's dependencies, test and install in the usual
373             manner for C<Dist::Zilla> projects:
374              
375             ~/path/to/App-Tracker$ dzil listdeps --missing | cpanm
376             ~/path/to/App-Tracker$ dzil test
377             ~/path/to/App-Tracker$ dzil install
378              
379             =head1 PLUGINS
380              
381             Custom commands or adaptations to your workflow can be implemented via
382             an "interesting" set of L<Moose>-powered plugins. You can configure
383             different sets of plugins for different jobs or projects.
384              
385             B<Tip:> Use C<tracker plugins> to list all installed plugins. Read more
386             about each plugin in C<App::TimeTracker::Command::PLUGIN-NAME>.
387              
388             =head2 Note about (missing) Plugins
389              
390             Up to version 2.028 a lot of plugins where included in the main distribution
391             C<App-TimeTracker>. To make installation easier and faster, all non-core
392             command plugins have been moved into distinct, standalone distributions.
393              
394             The following plugins are affected:
395              
396             =over
397              
398             =item * App::TimeTracker::Git (which also includes SyncViaGit)
399              
400             =item * App::TimeTracker::RT
401              
402             =item * App::TimeTracker::TellRemote (which was called Post2IRC earlier)
403              
404             =item * App::TimeTracker::Overtime has been removed, while the idea is nice, the API and implementation are not good enough.
405              
406             =item * App::TimeTracker::TextNotify has been removed.
407              
408             =back
409              
410             =head1 SOURCE CODE
411              
412             =head2 git
413              
414             We use C<< git >> for version control and maintain a public repository on
415             L<github|http://github.com>.
416              
417             You can find the latest version of L<App::TimeTracker> here:
418              
419             L<https://github.com/domm/App-TimeTracker>
420              
421             If you want to work on L<App::TimeTracker>, add a feature, add a plugin or fix
422             a bug, please feel free to L<fork|http://help.github.com/fork-a-repo/> the
423             repo and send us L<pull requests|http://help.github.com/send-pull-requests/>
424             to merge your changes.
425              
426             To report a bug, please use the C<< issues >> feature from GitHub.
427              
428             =head2 CPAN
429              
430             L<App::TimeTracker> is distributed via L<CPAN|http://cpan.org/>, the
431             Comprehensive Perl Archive Network. Here are a few different views of
432             CPAN, offering slightly different features:
433              
434             =over
435              
436             =item * L<https://metacpan.org/release/App-TimeTracker/>
437              
438             =item * L<http://search.cpan.org/dist/App-TimeTracker/>
439              
440             =back
441              
442             =head1 Viewing and reporting Bugs
443              
444             Please use this URL to view and report bugs:
445              
446             L<https://github.com/domm/App-TimeTracker/issues>
447              
448             =head1 CONTRIBUTORS
449              
450             Maros Kollar, Klaus Ita, Yanick Champoux, Lukas Rampa, David Schmidt, Michael Kröll, Thomas Sibley, Nelo Onyiah, Jozef Kutej, Roland Lammel, Ruslan Zakirov, Kartik Thakore, Tokuhiro Matsuno, Paul Cochrane, David Provost, Mohammad S Anwar, Håkon Hægland, Thomas Mantl, Zakariyya Mughal
451              
452             =head1 AUTHOR
453              
454             Thomas Klausner <domm@plix.at>
455              
456             =head1 COPYRIGHT AND LICENSE
457              
458             This software is copyright (c) 2011 - 2021 by Thomas Klausner.
459              
460             This is free software; you can redistribute it and/or modify it under
461             the same terms as the Perl 5 programming language system itself.
462              
463             =cut