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.008'; # VERSION
5              
6 13     13   1639652 use strict;
  13         53  
  13         470  
7 13     13   82 use warnings;
  13         24  
  13         345  
8 13     13   261 use 5.010;
  13         45  
9              
10 13     13   1619 use App::TimeTracker::Data::Task;
  13         69  
  13         510  
11 13     13   7369 use DateTime;
  13         3742603  
  13         566  
12 13     13   125 use Moose;
  13         26  
  13         199  
13 13     13   96259 use Moose::Util::TypeConstraints;
  13         35  
  13         149  
14 13     13   34106 use Path::Class qw();
  13         105472  
  13         395  
15 13     13   8711 use Path::Class::Iterator;
  13         123909  
  13         168  
16 13     13   6304 use MooseX::Storage::Format::JSONpm;
  13         871841  
  13         595  
17 13     13   110 use JSON::XS;
  13         28  
  13         5637  
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   6218 $dt->set( hour => $+{hour}, minute => $+{minute} );
  12         5410  
  12         3726  
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   115 no Moose::Util::TypeConstraints;
  13         47  
  13         162  
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 20 my $self = shift;
145 1   50     31 my $command = 'cmd_' . ( $self->extra_argv->[0] || 'missing' );
146              
147 1 50       27 $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 6285 my $dt = DateTime->now();
155 2         737 $dt->set_time_zone('local');
156 2         5754 return $dt;
157             }
158              
159             sub beautify_seconds {
160 15     15 0 8236 my ( $self, $s ) = @_;
161 15 100       47 return '0' unless $s;
162 13         31 my ( $m, $h ) = ( 0, 0 );
163              
164 13 100       39 if ( $s >= 60 ) {
165 12         30 $m = int( $s / 60 );
166 12         30 $s = $s - ( $m * 60 );
167             }
168 13 100 100     62 if ( $m && $m >= 60 ) {
169 4         8 $h = int( $m / 60 );
170 4         8 $m = $m - ( $h * 60 );
171             }
172 13         205 return sprintf( "%02d:%02d:%02d", $h, $m, $s );
173             }
174              
175             sub find_task_files {
176 5     5 0 12505 my ( $self, $args ) = @_;
177              
178 5         235 my $root = $self->home;
179 5         14 my ( $cmp_from, $cmp_to );
180              
181 5 100       27 if ( my $from = $args->{from} ) {
182 4   66     41 my $to = $args->{to} || $self->now;
183 4 100       36 $to->set( hour => 23, minute => 59, second => 59 ) unless $to->hour;
184 4         826 $cmp_from = $from->strftime("%Y%m%d%H%M%S");
185 4         465 $cmp_to = $to->strftime("%Y%m%d%H%M%S");
186              
187 4 100       268 if ( $from->year == $to->year ) {
188 3         35 $root = $root->subdir( $from->year );
189 3 50       241 if ( $from->month == $to->month ) {
190 3         36 $root = $root->subdir( $from->strftime("%m") );
191             }
192             }
193             }
194              
195 5         234 my $projects;
196 5 100       22 if ( $args->{projects} ) {
197 3         9 $projects = join( '|', map { s/-/./g; $_ } @{ $args->{projects} } );
  3         9  
  3         12  
  3         12  
198             }
199              
200 5         12 my $children;
201 5 50       16 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         12 my $tags;
208 5 50       14 if ( $args->{tags} ) {
209 0         0 $tags = join( '|', @{ $args->{tags} } );
  0         0  
210             }
211              
212 5         10 my @found;
213 5         43 my $iterator = Path::Class::Iterator->new( root => $root, );
214 5   66     22288 until ( !$iterator || $iterator->done ) {
215 79         3011 my $file = $iterator->next;
216              
217 79 100       50646 next unless -f $file;
218 75         3819 my $name = $file->basename;
219 75 50       579 next unless $name =~ /\.trc$/;
220              
221 75 100       175 if ($cmp_from) {
222 52         148 $file =~ /(\d{8})-(\d{6})/;
223 52         1842 my $time = $1 . $2;
224 52 100       242 next if $time < $cmp_from;
225 41 100       129 next if $time > $cmp_to;
226             }
227              
228 58 100       120 if ($projects) {
229 49 100       364 next unless ( $name =~ m/$projects/i );
230             }
231              
232 34 50       70 if ($children) {
233 0 0       0 next unless ( $name =~ m/$children/i );
234             }
235              
236 34 50       88 if ($tags) {
237 0         0 my $raw_content = $file->slurp;
238 0 0       0 next unless $raw_content =~ /$tags/i;
239             }
240              
241 34         241 push( @found, $file );
242             }
243 5         219 return sort @found;
244             }
245              
246             sub project_tree {
247 2     2 0 6 my $self = shift;
248 2         63 my $file = $self->home->file('projects.json');
249 2 50 33     169 return unless -e $file && -s $file;
250 2         234 my $decoder = JSON::XS->new->utf8->pretty->relaxed;
251 2         13 my $projects = $decoder->decode( scalar $file->slurp );
252              
253 2         583 my %tree;
254             my $depth;
255 2         15 while ( my ( $project, $location ) = each %$projects ) {
256 6   50     48 $tree{$project} //= { parent => undef, children => {} };
257             # check config file for parent
258 6 100       102 if ( -e $location ) {
259 4         26 my $this_config = $decoder->decode(
260             scalar Path::Class::file($location)->slurp );
261 4 50       1214 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 6         25 my @parts = Path::Class::file($location)->parent->parent->dir_list;
269 6         1040 foreach my $dir (@parts) {
270 14 50 33     81 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         20 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.008
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 B<do not> use the C<< issues >> feature from github;
427             use RT instead.
428              
429             =head2 CPAN
430              
431             L<App::TimeTracker> is distributed via L<CPAN|http://cpan.org/>, the
432             Comprehensive Perl Archive Network. Here are a few different views of
433             CPAN, offering slightly different features:
434              
435             =over
436              
437             =item * L<https://metacpan.org/release/App-TimeTracker/>
438              
439             =item * L<http://search.cpan.org/dist/App-TimeTracker/>
440              
441             =back
442              
443             =head1 Viewing and reporting Bugs
444              
445             We use L<rt.cpan.org|http://rt.cpan.org> (thank you
446             L<BestPractical|http://rt.bestpractical.com>) for bug reporting. Please do
447             not use the C<issues> feature of github! We pay no attention to those...
448              
449             Please use this URL to view and report bugs:
450              
451             L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-TimeTracker>
452              
453             =head1 CONTRIBUTORS
454              
455             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
456              
457             =head1 AUTHOR
458              
459             Thomas Klausner <domm@plix.at>
460              
461             =head1 COPYRIGHT AND LICENSE
462              
463             This software is copyright (c) 2011 - 2021 by Thomas Klausner.
464              
465             This is free software; you can redistribute it and/or modify it under
466             the same terms as the Perl 5 programming language system itself.
467              
468             =cut