File Coverage

lib/App/TimeTracker/Command/Core.pm
Criterion Covered Total %
statement 190 326 58.2
branch 36 110 32.7
condition 11 51 21.5
subroutine 32 41 78.0
pod 0 14 0.0
total 269 542 49.6


line stmt bran cond sub pod time code
1             package App::TimeTracker::Command::Core;
2              
3             # ABSTRACT: App::TimeTracker Core commands
4             our $VERSION = '3.009'; # VERSION
5              
6 6     6   35422 use strict;
  6         15  
  6         254  
7 6     6   43 use warnings;
  6         12  
  6         178  
8 6     6   149 use 5.010;
  6         31  
9              
10 6     6   571 use Moose::Role;
  6         500208  
  6         104  
11 6     6   36530 use Moose::Util::TypeConstraints;
  6         16  
  6         58  
12 6     6   15668 use App::TimeTracker::Utils qw(now pretty_date error_message);
  6         21  
  6         480  
13 6     6   1094 use App::TimeTracker::Constants qw(MISSING_PROJECT_HELP_MSG);
  6         16  
  6         353  
14 6     6   613 use File::Copy qw(move);
  6         2735  
  6         695  
15 6     6   3552 use File::Find::Rule;
  6         53671  
  6         52  
16 6     6   1255 use Data::Dumper;
  6         7661  
  6         342  
17 6     6   4004 use Text::Table;
  6         53145  
  6         198  
18 6     6   1112 use DateTime;
  6         519142  
  6         25652  
19              
20             sub cmd_start {
21 5     5 0 10095 my $self = shift;
22              
23 5 100       255 unless ( $self->has_current_project ) {
24 1         7 error_message( MISSING_PROJECT_HELP_MSG );
25 1         40 exit;
26             }
27 4         23 $self->cmd_stop('no_exit');
28              
29 4   66     716 my $task = App::TimeTracker::Data::Task->new(
30             { start => $self->at || now(),
31             project => $self->project,
32             tags => $self->tags,
33             description => $self->description,
34             }
35             );
36 4         197 $self->_current_task($task);
37              
38 4         129 $task->do_start( $self->home );
39             }
40              
41             sub cmd_stop {
42 9     9 0 10234 my ( $self, $dont_exit ) = @_;
43              
44 9         358 my $task = App::TimeTracker::Data::Task->current( $self->home );
45 9 100       1913 unless ($task) {
46 3 50       13 return if $dont_exit;
47 0         0 say "Currently not working on anything";
48 0         0 exit;
49             }
50              
51 6         47 my $proto = App::TimeTracker::Proto->new();
52 6         4410 my $config = $proto->load_config( undef, $task->project );
53              
54 6         43 my $class = $proto->setup_class( $config, 'stop' );
55 6   33     255 my $stop_self = $class->name->new(
56             { home => $self->home,
57             at => $self->at || now(),
58             config => $config,
59             _current_project => $task->project,
60             }
61             );
62 6         219 $stop_self->_current_command('cmd_stop');
63 6         199 $stop_self->_previous_task($task);
64              
65             # Store in original self too (for plugin usage)
66 6         185 $self->_previous_task($task);
67              
68 6   33     162 $task->stop( $stop_self->at || now() );
69 6 100       180 if ( $task->stop < $task->start ) {
70 1         283 say sprintf(
71             qq{The stop time you specified (%s) is earlier than the start time (%s).\nThis makes no sense.},
72             $task->stop, $task->start );
73              
74 1         199 my $what_you_meant = $task->stop->clone;
75 1         24 for ( 1 .. 5 ) {
76 1         10 $what_you_meant->add( days => 1 );
77 1 50       1371 last if $what_you_meant > $task->start;
78             }
79 1 50       270 if ( $what_you_meant ne $task->start ) {
80 1         246 say "Maybe you wanted to do:\ntracker stop --at '"
81             . $what_you_meant->strftime('%Y-%m-%d %H:%M') . "'";
82             }
83             else {
84 0         0 say
85             "Maybe it helps if you use the long format to specify the stop time ('2012-01-10 00:15')?";
86             }
87 1         127 exit;
88             }
89 5         1393 $task->save( $stop_self->home );
90              
91 5         860 move(
92             $stop_self->home->file('current')->stringify,
93             $stop_self->home->file('previous')->stringify
94             );
95              
96 5         1612 say "Worked " . $task->duration . " on " . $task->say_project_tags;
97             }
98              
99             sub cmd_current {
100 2     2 0 3607 my $self = shift;
101              
102 2 100       80 if ( my $task = App::TimeTracker::Data::Task->current( $self->home ) ) {
    50          
103 1         278 say "Working "
104             . $task->_calc_duration( now() ) . " on "
105             . $task->say_project_tags;
106 1         47 say 'Started at ' . pretty_date( $task->start );
107             }
108             elsif ( my $prev = App::TimeTracker::Data::Task->previous( $self->home ) ) {
109 1         309 say
110             "Currently not working on anything, but the last thing you worked on was:";
111 1         10 say $prev->say_project_tags;
112 1         9 say 'Worked '
113             . $prev->rounded_minutes
114             . ' minutes from '
115             . pretty_date( $prev->start )
116             . ' till '
117             . pretty_date( $prev->stop );
118             }
119             else {
120 0         0 say
121             "Currently not working on anything, and I have no idea what you worked on earlier...";
122             }
123             }
124              
125             sub cmd_append {
126 1     1 0 1955 my $self = shift;
127              
128 1 50       43 if ( my $task = App::TimeTracker::Data::Task->current( $self->home ) ) {
    50          
129 0         0 say "Cannot 'append', you're actually already working on :"
130             . $task->say_project_tags . "\n";
131             }
132             elsif ( my $prev = App::TimeTracker::Data::Task->previous( $self->home ) ) {
133              
134 1         308 my $task = App::TimeTracker::Data::Task->new(
135             { start => $prev->stop,
136             project => $self->project,
137             tags => $self->tags,
138             }
139             );
140 1         38 $self->_current_task($task);
141 1         27 $task->do_start( $self->home );
142             }
143             else {
144 0         0 say
145             "Currently not working on anything and I have no idea what you've been doing.";
146             }
147             }
148              
149             sub cmd_continue {
150 0     0 0 0 my $self = shift;
151              
152 0 0       0 if ( my $task = App::TimeTracker::Data::Task->current( $self->home ) ) {
    0          
153 0         0 say "Cannot 'continue', you're working on something:\n"
154             . $task->say_project_tags;
155             }
156             elsif ( my $prev = App::TimeTracker::Data::Task->previous( $self->home ) ) {
157 0   0     0 my $task = App::TimeTracker::Data::Task->new(
158             { start => $self->at || now(),
159             project => $prev->project,
160             tags => $prev->tags,
161             }
162             );
163 0         0 $self->_current_task($task);
164 0         0 $task->do_start( $self->home );
165             }
166             else {
167 0         0 say
168             "Currently not working on anything, and I have no idea what you worked on earlier...";
169             }
170             }
171              
172             sub cmd_worked {
173 0     0 0 0 my $self = shift;
174              
175 0         0 my @files = $self->find_task_files(
176             { from => $self->from,
177             to => $self->to,
178             projects => $self->fprojects,
179             tags => $self->ftags,
180             parent => $self->fparent,
181             }
182             );
183              
184 0         0 my $total = 0;
185 0         0 foreach my $file (@files) {
186 0         0 my $task = App::TimeTracker::Data::Task->load( $file->stringify );
187 0   0     0 $total += $task->seconds // $task->_build_seconds;
188             }
189              
190 0         0 say $self->beautify_seconds($total);
191             }
192              
193             my %LIST_FORMATS = (
194             compact => [qw(project tag time)],
195             content => [qw(project tag description)],
196             medium => [qw(project tag time description)],
197             default => [qw(project tag duration start stop)],
198             all => [qw(project tag duration start stop seconds description file)],
199             long => [qw( project tag duration start stop description)],
200             );
201              
202             sub cmd_list {
203 0     0 0 0 my $self = shift;
204              
205 0         0 my @files = $self->find_task_files(
206             { from => $self->from,
207             to => $self->to,
208             projects => $self->fprojects,
209             tags => $self->ftags,
210             parent => $self->fparent,
211             }
212             );
213              
214 0         0 my $s = \' | ';
215 0 0       0 my $format =
    0          
216             $self->detail ? 'all' : $self->output ? $self->output : 'default';
217 0   0     0 my $selected_fields = $LIST_FORMATS{$format} || $LIST_FORMATS{default};
218 0         0 my %fields = map { $_ => 1 } @$selected_fields;
  0         0  
219              
220 0         0 my @header = map { ucfirst($_), $s } @$selected_fields;
  0         0  
221 0         0 pop(@header);
222 0         0 my $table = Text::Table->new(@header);
223              
224 0         0 my $total = 0;
225 0         0 foreach my $file (@files) {
226 0         0 my $task = App::TimeTracker::Data::Task->load( $file->stringify );
227 0   0     0 my $time = $task->seconds // $task->_build_seconds;
228 0         0 $total += $time;
229              
230 0         0 my @row;
231 0 0       0 push( @row, $task->project ) if $fields{project};
232 0 0 0     0 push( @row, join( ', ', @{ $task->tags } ) || ' ' ) if $fields{tag};
233 0 0 0     0 push( @row, $task->duration || 'working' ) if $fields{duration};
234 0 0       0 push( @row, pretty_date( $task->start ) ) if $fields{start};
235 0 0       0 push( @row, pretty_date( $task->stop ) ) if $fields{stop};
236 0 0       0 push( @row, $task->compact_time ) if $fields{time};
237 0 0       0 push( @row, $time ) if $fields{seconds};
238 0 0 0     0 push( @row, $task->description_short || ' ' ) if $fields{description};
239 0 0       0 push( @row, $file->stringify ) if $fields{file};
240              
241 0         0 $table->add(@row);
242             }
243              
244 0         0 print $table->title;
245 0         0 print $table->rule( '-', '+' );
246 0         0 print $table->body;
247 0         0 say "total " . $self->beautify_seconds($total);
248             }
249              
250             sub cmd_report {
251 2     2 0 4008 my $self = shift;
252              
253 2         15 my @files = $self->find_task_files(
254             { from => $self->from,
255             to => $self->to,
256             projects => $self->fprojects,
257             tags => $self->ftags,
258             parent => $self->fparent,
259             }
260             );
261              
262 2         228 my $total = 0;
263 2         8 my $report = {};
264 2         5 my $format = "%- 20s % 12s\n";
265 2         20 my $projects = $self->project_tree;
266              
267 2         12 foreach my $file (@files) {
268 5         19 my $task = App::TimeTracker::Data::Task->load( $file->stringify );
269 5   33     751 my $time = $task->seconds // $task->_build_seconds;
270 5         148 my $project = $task->project;
271              
272 5 50       20 if ( $time >= 60 * 60 * 8 ) {
273 0         0 say "Found dubious trackfile: " . $file->stringify;
274 0         0 say " Are you sure you worked "
275             . $self->beautify_seconds($time)
276             . " on one task?";
277             }
278 5         10 $total += $time;
279              
280 5 50       21 if ( $self->group eq 'week' ) {
281 0         0 my $week_num = $task->start->week_number;
282              
283 0         0 $report->{$week_num}{'_total'} += $time;
284 0         0 $report->{$week_num}{$project}{time} += $time;
285 0 0       0 unless ($report->{$week_num}{'_start'}) {
286 0         0 $report->{$week_num}{'_start'} = $self->_first_day_of_week(
287             $task->start->year, $task->start->week_number
288             )->dmy('.');
289 0         0 my $end = $task->start->add( { days => 6, hours=>23, minutes=>59 } );
290 0         0 $report->{$week_num}{'_end'} =
291             $self->_first_day_of_week( $end->year, $end->week_number )->dmy('.')
292             }
293             }
294              
295 5 50       67 if ( $self->group eq 'project' ) {
296 5         39 $report->{$project}{'_total'} += $time;
297              
298 5 50       20 if ( my $level = $self->detail ) {
299 0         0 my $detail = $task->get_detail($level);
300 0         0 my $tags = $detail->{tags};
301 0 0 0     0 if ( $tags && @$tags ) {
302              
303             # Only use the first assigned tag to calculate the aggregated times and use it
304             # as tag key.
305             # Otherwise the same trackfiles would be counted multiple times and the
306             # aggregated sums would not match up.
307 0         0 $report->{$project}{ $tags->[0] }{time} += $time;
308              
309 0         0 foreach my $tag (@$tags) {
310 0   0     0 $report->{$project}{ $tags->[0] }{desc} //= '';
311              
312 0 0       0 if ( my $desc = $detail->{desc} ) {
313             $report->{$project}{ $tags->[0] }{desc} .= $desc
314             . "\n"
315             if
316             index( $report->{$project}{ $tags->[0] }{desc},
317 0 0       0 $desc ) == -1;
318             }
319             }
320             }
321             else {
322 0         0 $report->{$project}{'_untagged'} += $time;
323             }
324             }
325             }
326             }
327              
328 2 50       9 if ( $self->group eq 'project' ) {
329              
330             # sum child-time to all ancestors
331 2         17 my %top_nodes;
332 2         13 foreach my $project ( sort keys %$report ) {
333 3         6 my @ancestors;
334 3         14 $self->_get_ancestors( $report, $projects, $project, \@ancestors );
335 3   50     11 my $time = $report->{$project}{'_total'} || 0;
336 3         7 foreach my $ancestor (@ancestors) {
337 0         0 $report->{$ancestor}{'_kids'} += $time;
338             }
339 3 50       9 $top_nodes{ $ancestors[0] }++ if @ancestors;
340 3 50       13 $top_nodes{$project}++ if !@ancestors;
341             }
342              
343 2         11 $self->_say_current_report_interval;
344 2         294 my $padding = '';
345 2         6 my $tagpadding = ' ';
346 2         12 foreach my $project ( sort keys %top_nodes ) {
347 3         15 $self->_print_report_tree( $report, $projects, $project, $padding,
348             $tagpadding );
349             }
350             }
351              
352 2 50       10 if ( $self->group eq 'week' ) {
353 0         0 my $s = \' | ';
354 0         0 my @header = map { ucfirst($_), $s } qw(week start end time);
  0         0  
355 0         0 pop(@header);
356 0         0 my $table = Text::Table->new(@header);
357              
358 0         0 foreach my $week ( sort keys %$report ) {
359 0         0 my @row;
360              
361 0         0 push @row, $week;
362 0         0 push @row, $report->{$week}->{_start};
363 0         0 push @row, $report->{$week}->{_end};
364 0         0 push @row, $self->beautify_seconds( $report->{$week}->{_total} );
365              
366 0         0 $table->add(@row);
367             }
368              
369 0         0 print $table->title;
370 0         0 print $table->rule( '-', '+' );
371 0         0 print $table->body;
372              
373             }
374              
375 2         32 printf( $format, 'total', $self->beautify_seconds($total) );
376             }
377              
378             sub _get_ancestors {
379 3     3   10 my ( $self, $report, $projects, $node, $ancestors ) = @_;
380 3         9 my $parent = $projects->{$node}{parent};
381 3 50       10 if ($parent) {
382 0         0 unshift( @$ancestors, $parent );
383 0         0 $self->_get_ancestors( $report, $projects, $parent, $ancestors );
384             }
385             }
386              
387             sub _first_day_of_week {
388 0     0   0 my ( $self, $year, $week ) = @_;
389              
390             # Week 1 is defined as the one containing January 4:
391 0         0 return DateTime->new( year => $year, month => 1, day => 4 )
392             ->add( weeks => ( $week - 1 ) )->truncate( to => 'week' );
393             }
394              
395             sub _print_report_tree {
396 3     3   11 my ( $self, $report, $projects, $project, $padding, $tagpadding ) = @_;
397 3         9 my $data = $report->{$project};
398              
399 3         6 my $sum = 0;
400 3 50       14 $sum += $data->{'_total'} if $data->{'_total'};
401 3 50       9 $sum += $data->{'_kids'} if $data->{'_kids'};
402 3 50       11 return unless $sum;
403              
404 3         8 my $format = "%- 20s % 12s";
405              
406 3         33 say sprintf(
407             $padding . $format,
408             substr( $project, 0, 20 ),
409             $self->beautify_seconds($sum)
410             );
411 3 50       23 if ( my $detail = $self->detail ) {
412             say sprintf( $padding . $tagpadding . $format,
413             'untagged', $self->beautify_seconds( delete $data->{'_untagged'} ) )
414 0 0       0 if $data->{'_untagged'};
415              
416 0         0 foreach my $tag (
417 0         0 sort { $data->{$b}->{time} <=> $data->{$a}->{time} }
418 0         0 grep {/^[^_]/} keys %{$data}
  0         0  
419             ) {
420 0         0 my $time = $data->{$tag}{time};
421              
422 0 0       0 if ( $detail eq 'description' ) {
    0          
423 0   0     0 my $desc = $data->{$tag}{desc} || 'no desc';
424 0         0 $desc =~ s/\s+$//;
425 0         0 $desc =~ s/\v/, /g;
426 0         0 say sprintf( $padding . $tagpadding . $format . ' %s',
427             $tag, $self->beautify_seconds($time), $desc );
428             }
429             elsif ( $detail eq 'tag' ) {
430 0         0 say sprintf( $padding . $tagpadding . $format,
431             $tag, $self->beautify_seconds($time) );
432             }
433             }
434             }
435 3         27 foreach my $child ( sort keys %{ $projects->{$project}{children} } ) {
  3         26  
436 0         0 $self->_print_report_tree( $report, $projects, $child,
437             $padding . ' ', $tagpadding );
438             }
439             }
440              
441             sub cmd_recalc_trackfile {
442 0     0 0 0 my $self = shift;
443 0         0 my $file = $self->trackfile;
444 0 0       0 unless ( -e $file ) {
445 0         0 $file =~ /(?<year>\d\d\d\d)(?<month>\d\d)\d\d-\d{6}_\w+\.trc/;
446 2 0 0 2   614 if ( $+{year} && $+{month} ) {
  2         422  
  2         1661  
  0         0  
447 0         0 $file = $self->home->file( $+{year}, $+{month}, $file )->stringify;
448 0 0       0 unless ( -e $file ) {
449 0         0 error_message( "Cannot find file %s", $self->trackfile );
450 0         0 exit;
451             }
452             }
453             }
454              
455 0         0 my $task = App::TimeTracker::Data::Task->load($file);
456 0         0 $task->save( $self->home );
457 0         0 say "recalced $file";
458             }
459              
460             sub cmd_show_config {
461 0     0 0 0 my $self = shift;
462 0         0 warn Data::Dumper::Dumper $self->config;
463             }
464              
465             sub cmd_init {
466 2     2 0 4307 my ( $self, $cwd ) = @_;
467 2   33     10 $cwd ||= Path::Class::Dir->new->absolute;
468 2 50       20 if ( -e $cwd->file('.tracker.json') ) {
469 0         0 error_message(
470             "This directory is already set up.\nTry 'tracker show_config' to see the current aggregated config."
471             );
472 0         0 exit;
473             }
474              
475 2         321 my @dirs = $cwd->dir_list;
476 2         25 my $project = $dirs[-1];
477 2         18 my $fh = $cwd->file('.tracker.json')->openw;
478 2         623 say $fh <<EOCONFIG;
479             {
480             "project":"$project"
481             }
482             EOCONFIG
483              
484 2         98 my $projects_file = $self->home->file('projects.json');
485 2         211 my $coder = JSON::XS->new->utf8->pretty->relaxed;
486 2 50       13 if ( -e $projects_file ) {
487 2         113 my $projects = $coder->decode( scalar $projects_file->slurp );
488 2         483 $projects->{$project} =
489             $cwd->file('.tracker.json')->absolute->stringify;
490 2         430 $projects_file->spew( $coder->encode($projects) );
491             }
492              
493 2         838 say "Set up this directory for time-tracking via file .tracker.json";
494             }
495              
496             sub cmd_plugins {
497 0     0 0 0 my $self = shift;
498              
499             my $base =
500 0         0 Path::Class::file( $INC{'App/TimeTracker/Command/Core.pm'} )->parent;
501 0         0 my @hits;
502 0         0 while ( my $file = $base->next ) {
503 0 0       0 next unless -f $file;
504 0 0       0 next if $file->basename eq 'Core.pm';
505 0         0 my $plugin = $file->basename;
506 0         0 $plugin =~ s/\.pm$//;
507 0         0 push( @hits, $plugin );
508             }
509 0         0 say "Installed plugins:\n " . join( ', ', @hits );
510             }
511              
512             sub cmd_version {
513 2     2 0 1891 my $self = shift;
514 2         54 say "This is App::TimeTracker, version " . App::TimeTracker->VERSION;
515 2         15 exit;
516             }
517              
518             sub cmd_commands {
519 77     1 0 3693 my $self = shift;
520              
521 15         34 my @commands;
522 15         38 foreach my $method ( $self->meta->get_all_method_names ) {
523 78 100       1717 next unless $method =~ /^cmd_/;
524 15         43 $method =~ s/^cmd_//;
525 14         30 push( @commands, $method );
526             }
527              
528 2         85 @commands = sort @commands;
529              
530 2 50 66     18 if ( $self->can('autocomplete')
531             && $self->autocomplete ) {
532 14         155 say join( ' ', @commands );
533             }
534             else {
535 2         65 say "Available commands:";
536 1         7 foreach my $command (@commands) {
537 14         157 say "\t$command";
538             }
539             }
540 1         9 exit;
541             }
542              
543             sub _load_attribs_worked {
544 3     3   13452 my ( $class, $meta ) = @_;
545 3         32 $meta->add_attribute(
546             'from' => {
547             isa => 'TT::DateTime',
548             is => 'ro',
549             coerce => 1,
550             lazy_build => 1,
551              
552             #cmd_aliases => [qw/start/],
553             documentation => 'Beginning of time period to report',
554             }
555             );
556 3         33262 $meta->add_attribute(
557             'to' => {
558             isa => 'TT::DateTime',
559             is => 'ro',
560             coerce => 1,
561              
562             #cmd_aliases => [qw/end/],
563             lazy_build => 1,
564             documentation => 'End of time period to report',
565             }
566             );
567 3         30886 $meta->add_attribute(
568             'this' => {
569             isa => 'TT::Duration',
570             is => 'ro',
571             documentation =>
572             'Filter by current time period [day|week|month|year], e.g. "--this day" => today',
573             }
574             );
575 3         11525 $meta->add_attribute(
576             'last' => {
577             isa => 'TT::Duration',
578             is => 'ro',
579             documentation =>
580             'Filter by previous time period [day|week|month|year], e.g. "--last day" => yesterday',
581             }
582             );
583 3         10913 $meta->add_attribute(
584             'fprojects' => {
585             isa => 'ArrayRef',
586             is => 'ro',
587             documentation => 'Filter by project',
588             }
589             );
590 3         9444 $meta->add_attribute(
591             'ftags' => {
592             isa => 'ArrayRef',
593             is => 'ro',
594             documentation => 'Filter by tag',
595             }
596             );
597 3         9798 $meta->add_attribute(
598             'fparent' => {
599             isa => 'Str',
600             is => 'ro',
601             documentation => 'Filter by parent (get all children)',
602             }
603             );
604              
605             }
606              
607             sub _load_attribs_commands {
608 1     1   6 my ( $class, $meta ) = @_;
609 1         10 $meta->add_attribute(
610             'autocomplete' => {
611             isa => 'Bool',
612             is => 'ro',
613             default => 0,
614             documentation => 'Output for autocomplete',
615             }
616             );
617             }
618              
619             sub _load_attribs_list {
620 0     0   0 my ( $class, $meta ) = @_;
621 0         0 $class->_load_attribs_worked($meta);
622 0         0 $meta->add_attribute(
623             'detail' => {
624             isa => 'Bool',
625             is => 'ro',
626             default => 0,
627             documentation => 'Show all fields',
628             }
629             );
630 0         0 $meta->add_attribute(
631             'output' => {
632             isa => 'Str',
633             is => 'ro',
634             documentation => 'Specify output format. One of: '
635             . join( ', ', sort keys %LIST_FORMATS ),
636             }
637             );
638             }
639              
640             sub _load_attribs_report {
641 2     2   9 my ( $class, $meta ) = @_;
642 2         11 $class->_load_attribs_worked($meta);
643 2         6375 $meta->add_attribute(
644             'detail' => {
645             isa => enum( [qw(tag description)] ),
646             is => 'ro',
647             documentation => 'Be detailed: [tag|description]',
648             }
649             );
650 2         9561 $meta->add_attribute(
651             'group' => {
652             isa => enum( [qw(project week)] ),
653             is => 'ro',
654             default => 'project',
655             documentation => 'Genereta Report by week or project.'
656             }
657             );
658             }
659              
660             sub _load_attribs_start {
661 17     17   65 my ( $class, $meta ) = @_;
662 17         175 $meta->add_attribute(
663             'at' => {
664             isa => 'TT::DateTime',
665             is => 'ro',
666             coerce => 1,
667             documentation => 'Start at',
668             }
669             );
670 17         72236 $meta->add_attribute(
671             'project' => {
672             isa => 'Str',
673             is => 'ro',
674             documentation => 'Project name',
675             lazy_build => 1,
676             }
677             );
678 17         142824 $meta->add_attribute(
679             'description' => {
680             isa => 'Str',
681             is => 'rw',
682             documentation => 'Description',
683             }
684             );
685             }
686              
687             sub _build_project {
688 5     5   141 my $self = shift;
689 5         211 return $self->_current_project;
690             }
691              
692             *_load_attribs_append = \&_load_attribs_start;
693             *_load_attribs_continue = \&_load_attribs_start;
694             *_load_attribs_stop = \&_load_attribs_start;
695              
696             sub _load_attribs_recalc_trackfile {
697 0     0   0 my ( $class, $meta ) = @_;
698 0         0 $meta->add_attribute(
699             'trackfile' => {
700             isa => 'Str',
701             is => 'ro',
702             required => 1,
703             }
704             );
705             }
706              
707             sub _build_from {
708 6     6   5294 my $self = shift;
709 6 100       38 if ( my $last = $self->last ) {
    50          
710 3         40 return now()->truncate( to => $last )->subtract( $last . 's' => 1 );
711             }
712             elsif ( my $this = $self->this ) {
713 3         74 return now()->truncate( to => $this );
714             }
715             else {
716 0         0 return now()->truncate( to => 'month' );
717             }
718             }
719              
720             sub _build_to {
721 6     6   12065 my $self = shift;
722              
723 6 50 66     31 if ( my $date = $self->this || $self->last ) {
724 6         93 return $self->from->clone->add( $date . 's' => 1 )
725             ->subtract( seconds => 1 );
726             }
727             else {
728 0         0 return now();
729             }
730             }
731              
732             sub _say_current_report_interval {
733 2     2   5 my $self = shift;
734 2         12 printf( "From %s to %s you worked on:\n", $self->from, $self->to );
735             }
736              
737 5     5   107 no Moose::Role;
  5         14  
  5         54  
738              
739             q{ listening to: Train noises on my way from Wien to Graz }
740              
741             __END__
742              
743             =pod
744              
745             =encoding UTF-8
746              
747             =head1 NAME
748              
749             App::TimeTracker::Command::Core - App::TimeTracker Core commands
750              
751             =head1 VERSION
752              
753             version 3.009
754              
755             =head1 CORE COMMANDS
756              
757             More commands are implemented in various plugins. Plugins might also alter and/or amend commands.
758              
759             =head2 start
760              
761             ~/perl/Your-Project$ tracker start
762             Started working on Your-Project at 23:44:19
763              
764             Start tracking the current project now. Automatically stop the previous task, if there was one.
765              
766             =head3 Options:
767              
768             =head4 --at TT::DateTime
769              
770             ~/perl/Your-Project$ tracker start --at 12:42
771             ~/perl/Your-Project$ tracker start --at '2011-02-26 12:42'
772              
773             Start at the specified time/datetime instead of now. If only a time is
774             provided, the day defaults to today. See L<TT::DateTime> in L<App::TimeTracker>.
775              
776             =head4 --project SomeProject
777              
778             ~/perl/Your-Project$ tracker start --project SomeProject
779              
780             Use the specified project instead of the one determined by the current
781             working directory.
782              
783             =head4 --description 'some prosa'
784              
785             ~/perl/Your-Project$ tracker start --description "Solving nasty bug"
786              
787             Supply some descriptive text to the task. Might be used by reporting plugins etc.
788              
789             =head4 --tags RT1234 [Multiple]
790              
791             ~/perl/Your-Project$ tracker start --tag RT1234 --tag testing
792              
793             A list of tags to add to the task. Can be used by reporting plugins.
794              
795             =head2 stop
796              
797             ~/perl/Your-Project$ tracker stop
798             Worked 00:20:50 on Your-Project
799              
800             Stop tracking the current project now.
801              
802             =head3 Options
803              
804             =head4 --at TT::DateTime
805              
806             Stop at the specified time/datetime instead of now.
807              
808             =head2 continue
809              
810             ~/perl/Your-Project$ tracker continue
811              
812             Continue working on the previous task after a break.
813              
814             Example:
815              
816             ~$ tracker start --project ExplainContinue --tag testing
817             Started working on ExplainContinue (testing) at 12:42
818              
819             # ... time passes, it's now 13:17
820             ~$ tracker stop
821             Worked 00:35:00 on ExplainContinue
822              
823             # back from lunch at 13:58
824             ~$ tracker continue
825             Started working on ExplainContinue (testing) at 13:58
826              
827             =head3 Options:
828              
829             same as L<start|/start>
830              
831             =head2 append
832              
833             ~/perl/Your-Project$ tracker append
834              
835             Start working on a task at exactly the time you stopped working at the previous task.
836              
837             Example:
838              
839             ~$ tracker start --project ExplainAppend --tag RT1234
840             Started working on ExplainAppend (RT1234) at 14:23
841              
842             # ... time passes (14:46)
843             ~$ tracker stop
844             Worked 00:23:00 on ExplainAppend (RT1234)
845              
846             # start working on new ticket
847             # ...
848             # but forgot to hit start (14:53)
849             ~$ tracker append --tag RT7890
850             Started working on ExplainAppend (RT7890) at 14:46
851              
852             =head3 Options:
853              
854             same as L<start|/start>
855              
856             =head2 current
857              
858             ~/perl/Your-Project$ tracker current
859             Working 00:20:17 on Your-Project
860              
861             Display what you're currently working on, and for how long.
862              
863             =head3 No options
864              
865             =head2 worked
866              
867             ~/perl/Your-Project$ tracker worked [SPAN]
868              
869             Report the total time worked in the given time span, maybe limited to
870             some projects.
871              
872             =head3 Options:
873              
874             =head4 --from TT::DateTime [REQUIRED (or use --this/--last)]
875              
876             Begin of reporting interval, defaults to first day of current month.
877              
878             =head4 --to TT::DateTime [REQUIRED (or use --this/--last)]
879              
880             End of reporting interval, default to DateTime->now.
881              
882             =head4 --this [day, week, month, year]
883              
884             Automatically set C<--from> and C<--to> to the calculated values
885              
886             ~/perl/Your-Project$ tracker worked --this week
887             17:01:50
888              
889             =head4 --last [day, week, month, year]
890              
891             Automatically set C<--from> and C<--to> to the calculated values
892              
893             ~/perl/Your-Project$ tracker worked --last day (=yesterday)
894             06:39:12
895              
896             =head4 --project SomeProject [Multiple]
897              
898             ~$ tracker worked --last day --project SomeProject
899             02:04:47
900              
901             =head2 report
902              
903             ~/perl/Your-Project$ tracker report
904              
905             Print out a detailed report of what you did. All worked times are
906             summed up per project (and optionally per tag)
907              
908             =head3 Options:
909              
910             The same options as for L<worked|/worked>, plus:
911              
912             =head4 --detail
913              
914             ~/perl/Your-Project$ tracker report --last month --detail tag
915              
916             Valid options are: tag, description
917              
918             Will print the tag(s) and/or description.
919              
920             Also calc sums per tag.
921              
922             =head4 --verbose
923              
924             ~/perl/Your-Project$ tracker report --last month --verbose
925              
926             Lists all found trackfiles and their respective duration before printing out the report.
927              
928             =head2 list
929              
930             ~/perl/Your-Project$ tracker list
931              
932             Print out a detailed report of what you did in a tabular format including start and stop
933             times.
934              
935             =head3 Options:
936              
937             The same options as for L<report|/report>
938              
939             =head2 init
940              
941             ~/perl/Your-Project$ tracker init
942              
943             Initialize current directory as a project in which to track time. This step
944             is required before one can use time tracking commands such as
945             L<start|/start> and L<stop|/stop>. The initialization step creates a rather
946             empty F<.tracker.json> config file in the current directory.
947              
948             =head3 No options
949              
950             =head2 show_config
951              
952             ~/perl/Your-Project$ tracker show_config
953              
954             Dump the config that's valid for the current directory. Might be handy when setting up plugins etc.
955              
956             =head3 No options
957              
958             =head2 plugins
959              
960             ~/perl/Your-Project$ tracker plugins
961              
962             List all installed plugins (i.e. stuff in C<App::TimeTracker::Command::*>)
963              
964             =head3 No options
965              
966             =head2 recalc_trackfile
967              
968             ~/perl/Your-Project$ tracker recalc_trackfile --trackfile 20110808-232327_App_TimeTracker.trc
969              
970             Recalculates the duration stored in an old trackfile. Might be useful
971             after a manual update in a trackfile. Might be unnecessary in the
972             future, as soon as task duration is always calculated lazily.
973              
974             =head3 Options:
975              
976             =head4 --trackfile name_of_trackfile.trc REQUIRED
977              
978             Only the name of the trackfile is required, but you can also pass in
979             the absolute path to the file. Broken trackfiles are sometimes
980             reported during L<report|/report>.
981              
982             =head2 commands
983              
984             ~/perl/Your-Project$ tracker commands
985              
986             List all available commands, based on your current config.
987              
988             =head3 No options
989              
990             =head1 AUTHOR
991              
992             Thomas Klausner <domm@plix.at>
993              
994             =head1 COPYRIGHT AND LICENSE
995              
996             This software is copyright (c) 2011 - 2021 by Thomas Klausner.
997              
998             This is free software; you can redistribute it and/or modify it under
999             the same terms as the Perl 5 programming language system itself.
1000              
1001             =cut