App-TimeTracker
view release on metacpan or search on metacpan
lib/App/TimeTracker/Command/Core.pm view on Meta::CPAN
package App::TimeTracker::Command::Core;
# ABSTRACT: App::TimeTracker Core commands
our $VERSION = '3.010'; # VERSION
use strict;
use warnings;
use 5.010;
use Moose::Role;
use Moose::Util::TypeConstraints;
use App::TimeTracker::Utils qw(now pretty_date error_message);
use App::TimeTracker::Constants qw(MISSING_PROJECT_HELP_MSG);
use File::Copy qw(move);
use File::Find::Rule;
use Data::Dumper;
use Text::Table;
use DateTime;
sub cmd_start {
my $self = shift;
unless ( $self->has_current_project ) {
error_message( MISSING_PROJECT_HELP_MSG );
exit;
}
$self->cmd_stop('no_exit');
my $task = App::TimeTracker::Data::Task->new(
{ start => $self->at || now(),
project => $self->project,
tags => $self->tags,
description => $self->description,
}
);
$self->_current_task($task);
$task->do_start( $self->home );
}
sub cmd_stop {
my ( $self, $dont_exit ) = @_;
my $task = App::TimeTracker::Data::Task->current( $self->home );
unless ($task) {
return if $dont_exit;
say "Currently not working on anything";
exit;
}
my $proto = App::TimeTracker::Proto->new();
my $config = $proto->load_config( undef, $task->project );
my $class = $proto->setup_class( $config, 'stop' );
my $stop_self = $class->name->new(
{ home => $self->home,
at => $self->at || now(),
config => $config,
_current_project => $task->project,
}
);
$stop_self->_current_command('cmd_stop');
$stop_self->_previous_task($task);
# Store in original self too (for plugin usage)
$self->_previous_task($task);
$task->stop( $stop_self->at || now() );
if ( $task->stop < $task->start ) {
say sprintf(
qq{The stop time you specified (%s) is earlier than the start time (%s).\nThis makes no sense.},
$task->stop, $task->start );
my $what_you_meant = $task->stop->clone;
for ( 1 .. 5 ) {
$what_you_meant->add( days => 1 );
last if $what_you_meant > $task->start;
}
lib/App/TimeTracker/Command/Core.pm view on Meta::CPAN
my %top_nodes;
foreach my $project ( sort keys %$report ) {
my @ancestors;
$self->_get_ancestors( $report, $projects, $project, \@ancestors );
my $time = $report->{$project}{'_total'} || 0;
foreach my $ancestor (@ancestors) {
$report->{$ancestor}{'_kids'} += $time;
}
$top_nodes{ $ancestors[0] }++ if @ancestors;
$top_nodes{$project}++ if !@ancestors;
}
$self->_say_current_report_interval;
my $padding = '';
my $tagpadding = ' ';
foreach my $project ( sort keys %top_nodes ) {
$self->_print_report_tree( $report, $projects, $project, $padding,
$tagpadding );
}
}
if ( $self->group eq 'week' ) {
my $s = \' | ';
my @header = map { ucfirst($_), $s } qw(week start end time);
pop(@header);
my $table = Text::Table->new(@header);
foreach my $week ( sort keys %$report ) {
my @row;
push @row, $week;
push @row, $report->{$week}->{_start};
push @row, $report->{$week}->{_end};
push @row, $self->beautify_seconds( $report->{$week}->{_total} );
$table->add(@row);
}
print $table->title;
print $table->rule( '-', '+' );
print $table->body;
}
printf( $format, 'total', $self->beautify_seconds($total) );
}
sub _get_ancestors {
my ( $self, $report, $projects, $node, $ancestors ) = @_;
my $parent = $projects->{$node}{parent};
if ($parent) {
unshift( @$ancestors, $parent );
$self->_get_ancestors( $report, $projects, $parent, $ancestors );
}
}
sub _first_day_of_week {
my ( $self, $year, $week ) = @_;
# Week 1 is defined as the one containing January 4:
return DateTime->new( year => $year, month => 1, day => 4 )
->add( weeks => ( $week - 1 ) )->truncate( to => 'week' );
}
sub _print_report_tree {
my ( $self, $report, $projects, $project, $padding, $tagpadding ) = @_;
my $data = $report->{$project};
my $sum = 0;
$sum += $data->{'_total'} if $data->{'_total'};
$sum += $data->{'_kids'} if $data->{'_kids'};
return unless $sum;
my $format = "%- 20s % 12s";
say sprintf(
$padding . $format,
substr( $project, 0, 20 ),
$self->beautify_seconds($sum)
);
if ( my $detail = $self->detail ) {
say sprintf( $padding . $tagpadding . $format,
'untagged', $self->beautify_seconds( delete $data->{'_untagged'} ) )
if $data->{'_untagged'};
foreach my $tag (
sort { $data->{$b}->{time} <=> $data->{$a}->{time} }
grep {/^[^_]/} keys %{$data}
) {
my $time = $data->{$tag}{time};
if ( $detail eq 'description' ) {
my $desc = $data->{$tag}{desc} || 'no desc';
$desc =~ s/\s+$//;
$desc =~ s/\v/, /g;
say sprintf( $padding . $tagpadding . $format . ' %s',
$tag, $self->beautify_seconds($time), $desc );
}
elsif ( $detail eq 'tag' ) {
say sprintf( $padding . $tagpadding . $format,
$tag, $self->beautify_seconds($time) );
}
}
}
foreach my $child ( sort keys %{ $projects->{$project}{children} } ) {
$self->_print_report_tree( $report, $projects, $child,
$padding . ' ', $tagpadding );
}
}
sub cmd_recalc_trackfile {
my $self = shift;
my $file = $self->trackfile;
unless ( -e $file ) {
$file =~ /(?<year>\d\d\d\d)(?<month>\d\d)\d\d-\d{6}_\w+\.trc/;
if ( $+{year} && $+{month} ) {
$file = $self->home->file( $+{year}, $+{month}, $file )->stringify;
unless ( -e $file ) {
error_message( "Cannot find file %s", $self->trackfile );
exit;
}
lib/App/TimeTracker/Command/Core.pm view on Meta::CPAN
my $projects = $coder->decode( scalar $projects_file->slurp );
$projects->{$project} =
$cwd->file('.tracker.json')->absolute->stringify;
$projects_file->spew( $coder->encode($projects) );
}
say "Set up this directory for time-tracking via file .tracker.json";
}
sub cmd_plugins {
my $self = shift;
my $base =
Path::Class::file( $INC{'App/TimeTracker/Command/Core.pm'} )->parent;
my @hits;
while ( my $file = $base->next ) {
next unless -f $file;
next if $file->basename eq 'Core.pm';
my $plugin = $file->basename;
$plugin =~ s/\.pm$//;
push( @hits, $plugin );
}
say "Installed plugins:\n " . join( ', ', @hits );
}
sub cmd_version {
my $self = shift;
say "This is App::TimeTracker, version " . App::TimeTracker->VERSION;
exit;
}
sub cmd_commands {
my $self = shift;
my @commands;
foreach my $method ( $self->meta->get_all_method_names ) {
next unless $method =~ /^cmd_/;
$method =~ s/^cmd_//;
push( @commands, $method );
}
@commands = sort @commands;
if ( $self->can('autocomplete')
&& $self->autocomplete ) {
say join( ' ', @commands );
}
else {
say "Available commands:";
foreach my $command (@commands) {
say "\t$command";
}
}
exit;
}
sub _load_attribs_worked {
my ( $class, $meta ) = @_;
$meta->add_attribute(
'from' => {
isa => 'TT::DateTime',
is => 'ro',
coerce => 1,
lazy_build => 1,
#cmd_aliases => [qw/start/],
documentation => 'Beginning of time period to report',
}
);
$meta->add_attribute(
'to' => {
isa => 'TT::DateTime',
is => 'ro',
coerce => 1,
#cmd_aliases => [qw/end/],
lazy_build => 1,
documentation => 'End of time period to report',
}
);
$meta->add_attribute(
'this' => {
isa => 'TT::Duration',
is => 'ro',
documentation =>
'Filter by current time period [day|week|month|year], e.g. "--this day" => today',
}
);
$meta->add_attribute(
'last' => {
isa => 'TT::Duration',
is => 'ro',
documentation =>
'Filter by previous time period [day|week|month|year], e.g. "--last day" => yesterday',
}
);
$meta->add_attribute(
'fprojects' => {
isa => 'ArrayRef',
is => 'ro',
documentation => 'Filter by project',
}
);
$meta->add_attribute(
'ftags' => {
isa => 'ArrayRef',
is => 'ro',
documentation => 'Filter by tag',
}
);
$meta->add_attribute(
'fparent' => {
isa => 'Str',
is => 'ro',
documentation => 'Filter by parent (get all children)',
}
);
}
sub _load_attribs_commands {
my ( $class, $meta ) = @_;
$meta->add_attribute(
'autocomplete' => {
isa => 'Bool',
is => 'ro',
default => 0,
documentation => 'Output for autocomplete',
}
);
}
sub _load_attribs_list {
my ( $class, $meta ) = @_;
$class->_load_attribs_worked($meta);
$meta->add_attribute(
'detail' => {
isa => 'Bool',
is => 'ro',
default => 0,
documentation => 'Show all fields',
}
);
$meta->add_attribute(
'output' => {
isa => 'Str',
is => 'ro',
documentation => 'Specify output format. One of: '
. join( ', ', sort keys %LIST_FORMATS ),
}
);
}
sub _load_attribs_report {
my ( $class, $meta ) = @_;
$class->_load_attribs_worked($meta);
$meta->add_attribute(
'detail' => {
isa => enum( [qw(tag description)] ),
is => 'ro',
documentation => 'Be detailed: [tag|description]',
}
);
$meta->add_attribute(
'group' => {
isa => enum( [qw(project week)] ),
is => 'ro',
default => 'project',
documentation => 'Genereta Report by week or project.'
}
);
}
sub _load_attribs_start {
my ( $class, $meta ) = @_;
$meta->add_attribute(
'at' => {
isa => 'TT::DateTime',
is => 'ro',
coerce => 1,
documentation => 'Start at',
}
);
$meta->add_attribute(
'project' => {
isa => 'Str',
is => 'ro',
documentation => 'Project name',
lazy_build => 1,
}
);
$meta->add_attribute(
'description' => {
isa => 'Str',
is => 'rw',
documentation => 'Description',
}
);
}
sub _build_project {
my $self = shift;
return $self->_current_project;
}
*_load_attribs_append = \&_load_attribs_start;
*_load_attribs_continue = \&_load_attribs_start;
*_load_attribs_stop = \&_load_attribs_start;
sub _load_attribs_recalc_trackfile {
my ( $class, $meta ) = @_;
$meta->add_attribute(
'trackfile' => {
isa => 'Str',
is => 'ro',
required => 1,
}
);
}
sub _load_attribs_init {
my ( $class, $meta ) = @_;
$meta->add_attribute(
'project' => {
isa => 'Str',
is => 'ro',
documentation => 'Project name to initialize',
lazy_build => 1,
}
);
}
sub _build_from {
my $self = shift;
if ( my $last = $self->last ) {
return now()->truncate( to => $last )->subtract( $last . 's' => 1 );
}
elsif ( my $this = $self->this ) {
return now()->truncate( to => $this );
}
else {
return now()->truncate( to => 'month' );
}
}
sub _build_to {
my $self = shift;
if ( my $date = $self->this || $self->last ) {
return $self->from->clone->add( $date . 's' => 1 )
->subtract( seconds => 1 );
}
else {
return now();
}
}
sub _say_current_report_interval {
my $self = shift;
printf( "From %s to %s you worked on:\n", $self->from, $self->to );
}
no Moose::Role;
q{ listening to: Train noises on my way from Wien to Graz }
__END__
=pod
=encoding UTF-8
=head1 NAME
App::TimeTracker::Command::Core - App::TimeTracker Core commands
=head1 VERSION
version 3.010
=head1 CORE COMMANDS
More commands are implemented in various plugins. Plugins might also alter and/or amend commands.
=head2 start
~/perl/Your-Project$ tracker start
Started working on Your-Project at 23:44:19
Start tracking the current project now. Automatically stop the previous task, if there was one.
=head3 Options:
=head4 --at TT::DateTime
~/perl/Your-Project$ tracker start --at 12:42
~/perl/Your-Project$ tracker start --at '2011-02-26 12:42'
Start at the specified time/datetime instead of now. If only a time is
provided, the day defaults to today. See L<TT::DateTime> in L<App::TimeTracker>.
=head4 --project SomeProject
~/perl/Your-Project$ tracker start --project SomeProject
Use the specified project instead of the one determined by the current
working directory.
=head4 --description 'some prosa'
~/perl/Your-Project$ tracker start --description "Solving nasty bug"
Supply some descriptive text to the task. Might be used by reporting plugins etc.
=head4 --tags RT1234 [Multiple]
~/perl/Your-Project$ tracker start --tag RT1234 --tag testing
A list of tags to add to the task. Can be used by reporting plugins.
=head2 stop
~/perl/Your-Project$ tracker stop
Worked 00:20:50 on Your-Project
Stop tracking the current project now.
=head3 Options
=head4 --at TT::DateTime
Stop at the specified time/datetime instead of now.
=head2 continue
~/perl/Your-Project$ tracker continue
Continue working on the previous task after a break.
Example:
~$ tracker start --project ExplainContinue --tag testing
Started working on ExplainContinue (testing) at 12:42
# ... time passes, it's now 13:17
~$ tracker stop
Worked 00:35:00 on ExplainContinue
# back from lunch at 13:58
~$ tracker continue
Started working on ExplainContinue (testing) at 13:58
=head3 Options:
same as L<start|/start>
=head2 append
~/perl/Your-Project$ tracker append
Start working on a task at exactly the time you stopped working at the previous task.
Example:
~$ tracker start --project ExplainAppend --tag RT1234
Started working on ExplainAppend (RT1234) at 14:23
# ... time passes (14:46)
~$ tracker stop
Worked 00:23:00 on ExplainAppend (RT1234)
# start working on new ticket
# ...
# but forgot to hit start (14:53)
~$ tracker append --tag RT7890
Started working on ExplainAppend (RT7890) at 14:46
=head3 Options:
same as L<start|/start>
=head2 current
~/perl/Your-Project$ tracker current
Working 00:20:17 on Your-Project
Display what you're currently working on, and for how long.
=head3 No options
=head2 worked
~/perl/Your-Project$ tracker worked [SPAN]
Report the total time worked in the given time span, maybe limited to
some projects.
=head3 Options:
=head4 --from TT::DateTime [REQUIRED (or use --this/--last)]
Begin of reporting interval, defaults to first day of current month.
=head4 --to TT::DateTime [REQUIRED (or use --this/--last)]
End of reporting interval, default to DateTime->now.
=head4 --this [day, week, month, year]
Automatically set C<--from> and C<--to> to the calculated values
~/perl/Your-Project$ tracker worked --this week
17:01:50
=head4 --last [day, week, month, year]
Automatically set C<--from> and C<--to> to the calculated values
~/perl/Your-Project$ tracker worked --last day (=yesterday)
06:39:12
=head4 --project SomeProject [Multiple]
~$ tracker worked --last day --project SomeProject
02:04:47
=head2 report
~/perl/Your-Project$ tracker report
Print out a detailed report of what you did. All worked times are
summed up per project (and optionally per tag)
=head3 Options:
The same options as for L<worked|/worked>, plus:
=head4 --detail
~/perl/Your-Project$ tracker report --last month --detail tag
Valid options are: tag, description
Will print the tag(s) and/or description.
Also calc sums per tag.
=head4 --verbose
~/perl/Your-Project$ tracker report --last month --verbose
Lists all found trackfiles and their respective duration before printing out the report.
=head2 list
~/perl/Your-Project$ tracker list
Print out a detailed report of what you did in a tabular format including start and stop
times.
=head3 Options:
The same options as for L<report|/report>
=head2 init
( run in 1.015 second using v1.01-cache-2.11-cpan-39bf76dae61 )