Group
Extension

App-Jiffy/lib/App/Jiffy.pm

package App::Jiffy;

use strict;
use warnings;

use utf8;
use open ':std', ':encoding(utf8)';

use 5.008_005;
our $VERSION = '0.09';

use App::Jiffy::TimeEntry;
use App::Jiffy::View::Timesheet;
use App::Jiffy::Util::Duration qw/round/;

use YAML::Any qw( LoadFile );
use JSON::MaybeXS 'JSON';

use Getopt::Long;
Getopt::Long::Configure("pass_through");

use Moo;

has cfg => (
  is      => 'ro',
  default => sub {
    LoadFile( $ENV{HOME} . '/.jiffy.yml' ) || {};
  },
);

has terminator_regex => (
  is  => 'ro',
  isa => sub {
    die 'terminator_regex must be a regex ref' unless ref $_[0] eq 'Regexp';
  },
  default => sub {
    qr/^end$|
    ^done$|
    ^eod$|
    ^finished$|
    ^\\\(^\s*\.^\s*\)\/$| # This is a smily face with hands raised
    ^✓$|
    ^x$/x;
  },
);

sub remove_terminators {
  my $self = shift;
  return (
    title => {
      '$not' => $self->terminator_regex,
    } );
}

sub add_entry {
  my $self    = shift;
  my $options = shift;
  my $title;
  if ( ref $options ne 'HASH' ) {
    $title = $options;
    undef $options;
  } else {
    $title = shift;
  }

  my $start_time;

  my $LocalTZ = DateTime::TimeZone->new( name => 'local' );    # For caching
  my $now = DateTime->now( time_zone => $LocalTZ );

  if ( $options->{time} ) {
    require DateTime::Format::Strptime;

    # @TODO Figure out something more flexible and powerful to get time

    # First try H:M:S
    my $strp = DateTime::Format::Strptime->new(
      pattern   => '%T',
      time_zone => $LocalTZ,
    );
    $start_time = $strp->parse_datetime( $options->{time} );

    # If no time found try just H:M
    if ( not $start_time ) {
      my $strp = DateTime::Format::Strptime->new(
        pattern   => '%R',
        time_zone => $LocalTZ,
      );
      $start_time = $strp->parse_datetime( $options->{time} );
    }

    # Make sure the date part of the datetime is not set to the
    # beginning of time.
    if ( $start_time and $start_time->year < $now->year ) {
      $start_time->set(
        day   => $now->day,
        month => $now->month,
        year  => $now->year,
      );
    }
  }

  # Create and save Entry
  App::Jiffy::TimeEntry->new(
    title      => $title,
    start_time => $start_time // $now,
    cfg        => $self->cfg,
  )->save;
}

sub current_time {
  my $self = shift;

  my $latest   = App::Jiffy::TimeEntry::last_entry( $self->cfg );
  my $duration = $latest->duration;

  print '"' . $latest->title . '" has been running for';

  my %deltas = $duration->deltas;
  foreach my $unit ( keys %deltas ) {
    next unless $deltas{$unit};
    print " " . $deltas{$unit} . " " . $unit;
  }
  print ".\n";
}

sub time_sheet {
  my $self    = shift;
  my $options = shift;
  my $from;
  if ( ref $options ne 'HASH' ) {
    $from = $options;
    undef $options;
  } else {
    $from = shift;
  }

  my $from_date = DateTime->today( time_zone => 'local' );

  if ( defined $from ) {
    $from_date->subtract( days => $from );
  }

  my @entries = App::Jiffy::TimeEntry::search(
    $self->cfg,
    query => {
      start_time => { '$gt' => $from_date, },
      $self->remove_terminators,
    },
    sort => {
      start_time => 1,
    },
  );

  if ( $options->{round} ) {
    @entries = map { $_->duration( round( $_->duration ) ); $_ } @entries;
  }

  if ( $options->{json} ) {
    my $json = JSON::MaybeXS->new( pretty => 1, convert_blessed => 1 );
    print $json->encode( \@entries );
  } else {
    $options->{from} = $from;
    App::Jiffy::View::Timesheet::render( \@entries, $options );
  }
}

sub search {
  my $self       = shift;
  my $query_text = shift;
  my $options    = shift;
  my $days;
  if ( ref $options ne 'HASH' ) {
    $days = $options;
    undef $options;
  } else {
    $days = shift;
  }

  my $from_date = DateTime->today( time_zone => 'local' );

  if ( defined $days ) {
    $from_date->subtract( days => $days );
  }

  my @entries = App::Jiffy::TimeEntry::search(
    $self->cfg,
    query => {
      start_time => { '$gt' => $from_date, },
      $self->remove_terminators,
      title => qr/$query_text/,
    },
    sort => {
      start_time => 1,
    },
  );

  if ( $options->{round} ) {
    @entries = map { $_->duration( round( $_->duration ) ); $_ } @entries;
  }

  if ( not @entries ) {
    print "No Entries Found\n";
    return;
  }

  if ( $options->{json} ) {
    my $json = JSON::MaybeXS->new( pretty => 1, convert_blessed => 1 );
    print $json->encode( \@entries );
  } else {
    $options->{from} = $days;
    App::Jiffy::View::Timesheet::render( \@entries, $options );
  }
}

sub run {
  my $self = shift;
  my @args = @_;

  if ( $args[0] ) {
    if ( $args[0] eq 'current' ) {
      shift @args;
      return $self->current_time(@args);
    } elsif ( $args[0] eq 'timesheet' ) {
      shift @args;

      my $p = Getopt::Long::Parser->new( config => ['pass_through'], );
      $p->getoptionsfromarray(
        \@args,
        'verbose' => \my $verbose,
        'round'   => \my $round,
        'json'    => \my $json
      );

      return $self->time_sheet( {
          verbose => $verbose,
          round   => $round,
          json    => $json,
        },
        @args
      );
    } elsif ( $args[0] eq 'search' ) {
      shift @args;

      my $p = Getopt::Long::Parser->new( config => ['pass_through'], );
      $p->getoptionsfromarray(
        \@args,
        'verbose' => \my $verbose,
        'round'   => \my $round,
        'json'    => \my $json
      );

      my $query_text = shift @args;

      return $self->search(
        $query_text,
        {
          verbose => $verbose,
          round   => $round,
          json    => $json,
        },
        @args
      );
    }
  }

  my $p = Getopt::Long::Parser->new( config => ['pass_through'], );
  $p->getoptionsfromarray( \@args, 'time=s' => \my $time, );

  return $self->add_entry( {
      time => $time,
    },
    join ' ',
    @args
  );
}

1;
__END__

=encoding utf-8

=head1 NAME

App::Jiffy - A minimalist time tracking app focused on precision and effortlessness.

=head1 SYNOPSIS

  use App::Jiffy;

  # cmd line tool
  jiffy Solving world hunger
  jiffy Cleaning the plasma manifolds
  jiffy current # Returns the elapsed time for the current task

  # Run server
  jiffyd
  curl -d "title=Meeting with Client X" http://localhost:3000/timeentry

=head1 DESCRIPTION

App::Jiffy's philosophy is that you should have to do as little as possible to track your time. Instead you should focus on working. App::Jiffy also focuses on precision. Many times time tracking results in globbing activities together masking the fact that your 5 hours of work on project "X" was actually 3 hours of work with interruptions from your coworker asking about project "Y".

In order to be precise with as little effort as possible, App::Jiffy will be available via a myriad of mediums and devices but will have a central server to combine all the information. Plans currently include the following applications:

=over

=item Command line tool

=item Web app L<App::Jiffyd>

=item iPhone app ( potentially )

=back

=head1 INSTALLATION

  curl -L https://cpanmin.us | perl - git://github.com/lejeunerenard/jiffy

=head1 METHODS

The following are methods available on the C<App::Jiffy> object.

=head2 add_entry

C<add_entry> will create a new TimeEntry with the current time as the entry's start_time.

=head2 current_time

C<current_time> will print out the elapsed time for the current task (AKA the time since the last entry was created).

=head2 time_sheet

C<time_sheet> will print out a time sheet including the time spent for each C<TimeEntry>.

=head2 search( C<$query_text>, C<$days> )

The C<search> subcommand will look for the given C<$query_text> in the past C<$days> number of days. It will treat the C<$query_text> argument as a regex.

=head2 run

C<run> will start an instance of the Jiffy app.

=head1 AUTHOR

Sean Zellmer E<lt>sean@lejeunerenard.comE<gt>

=head1 COPYRIGHT

Copyright 2015- Sean Zellmer

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.