File Coverage

blib/lib/App/Jiffy.pm
Criterion Covered Total %
statement 35 128 27.3
branch 0 36 0.0
condition 0 6 0.0
subroutine 12 18 66.6
pod 5 6 83.3
total 52 194 26.8


line stmt bran cond sub pod time code
1             package App::Jiffy;
2              
3 1     1   101773 use strict;
  1         11  
  1         30  
4 1     1   5 use warnings;
  1         2  
  1         24  
5              
6 1     1   639 use utf8;
  1         15  
  1         6  
7 1     1   494 use open ':std', ':encoding(utf8)';
  1         1245  
  1         6  
8              
9 1     1   17336 use 5.008_005;
  1         3  
10             our $VERSION = '0.09';
11              
12 1     1   477 use App::Jiffy::TimeEntry;
  1         4  
  1         40  
13 1     1   508 use App::Jiffy::View::Timesheet;
  1         4  
  1         44  
14 1     1   11 use App::Jiffy::Util::Duration qw/round/;
  1         2  
  1         63  
15              
16 1     1   545 use YAML::Any qw( LoadFile );
  1         1330  
  1         6  
17 1     1   3903 use JSON::MaybeXS 'JSON';
  1         4199  
  1         62  
18              
19 1     1   799 use Getopt::Long;
  1         10583  
  1         6  
20             Getopt::Long::Configure("pass_through");
21              
22 1     1   161 use Moo;
  1         3  
  1         7  
23              
24             has cfg => (
25             is => 'ro',
26             default => sub {
27             LoadFile( $ENV{HOME} . '/.jiffy.yml' ) || {};
28             },
29             );
30              
31             has terminator_regex => (
32             is => 'ro',
33             isa => sub {
34             die 'terminator_regex must be a regex ref' unless ref $_[0] eq 'Regexp';
35             },
36             default => sub {
37             qr/^end$|
38             ^done$|
39             ^eod$|
40             ^finished$|
41             ^\\\(^\s*\.^\s*\)\/$| # This is a smily face with hands raised
42             ^✓$|
43             ^x$/x;
44             },
45             );
46              
47             sub remove_terminators {
48 0     0 0   my $self = shift;
49             return (
50 0           title => {
51             '$not' => $self->terminator_regex,
52             } );
53             }
54              
55             sub add_entry {
56 0     0 1   my $self = shift;
57 0           my $options = shift;
58 0           my $title;
59 0 0         if ( ref $options ne 'HASH' ) {
60 0           $title = $options;
61 0           undef $options;
62             } else {
63 0           $title = shift;
64             }
65              
66 0           my $start_time;
67              
68 0           my $LocalTZ = DateTime::TimeZone->new( name => 'local' ); # For caching
69 0           my $now = DateTime->now( time_zone => $LocalTZ );
70              
71 0 0         if ( $options->{time} ) {
72 0           require DateTime::Format::Strptime;
73              
74             # @TODO Figure out something more flexible and powerful to get time
75              
76             # First try H:M:S
77 0           my $strp = DateTime::Format::Strptime->new(
78             pattern => '%T',
79             time_zone => $LocalTZ,
80             );
81 0           $start_time = $strp->parse_datetime( $options->{time} );
82              
83             # If no time found try just H:M
84 0 0         if ( not $start_time ) {
85 0           my $strp = DateTime::Format::Strptime->new(
86             pattern => '%R',
87             time_zone => $LocalTZ,
88             );
89 0           $start_time = $strp->parse_datetime( $options->{time} );
90             }
91              
92             # Make sure the date part of the datetime is not set to the
93             # beginning of time.
94 0 0 0       if ( $start_time and $start_time->year < $now->year ) {
95 0           $start_time->set(
96             day => $now->day,
97             month => $now->month,
98             year => $now->year,
99             );
100             }
101             }
102              
103             # Create and save Entry
104             App::Jiffy::TimeEntry->new(
105 0   0       title => $title,
106             start_time => $start_time // $now,
107             cfg => $self->cfg,
108             )->save;
109             }
110              
111             sub current_time {
112 0     0 1   my $self = shift;
113              
114 0           my $latest = App::Jiffy::TimeEntry::last_entry( $self->cfg );
115 0           my $duration = $latest->duration;
116              
117 0           print '"' . $latest->title . '" has been running for';
118              
119 0           my %deltas = $duration->deltas;
120 0           foreach my $unit ( keys %deltas ) {
121 0 0         next unless $deltas{$unit};
122 0           print " " . $deltas{$unit} . " " . $unit;
123             }
124 0           print ".\n";
125             }
126              
127             sub time_sheet {
128 0     0 1   my $self = shift;
129 0           my $options = shift;
130 0           my $from;
131 0 0         if ( ref $options ne 'HASH' ) {
132 0           $from = $options;
133 0           undef $options;
134             } else {
135 0           $from = shift;
136             }
137              
138 0           my $from_date = DateTime->today( time_zone => 'local' );
139              
140 0 0         if ( defined $from ) {
141 0           $from_date->subtract( days => $from );
142             }
143              
144 0           my @entries = App::Jiffy::TimeEntry::search(
145             $self->cfg,
146             query => {
147             start_time => { '$gt' => $from_date, },
148             $self->remove_terminators,
149             },
150             sort => {
151             start_time => 1,
152             },
153             );
154              
155 0 0         if ( $options->{round} ) {
156 0           @entries = map { $_->duration( round( $_->duration ) ); $_ } @entries;
  0            
  0            
157             }
158              
159 0 0         if ( $options->{json} ) {
160 0           my $json = JSON::MaybeXS->new( pretty => 1, convert_blessed => 1 );
161 0           print $json->encode( \@entries );
162             } else {
163 0           $options->{from} = $from;
164 0           App::Jiffy::View::Timesheet::render( \@entries, $options );
165             }
166             }
167              
168             sub search {
169 0     0 1   my $self = shift;
170 0           my $query_text = shift;
171 0           my $options = shift;
172 0           my $days;
173 0 0         if ( ref $options ne 'HASH' ) {
174 0           $days = $options;
175 0           undef $options;
176             } else {
177 0           $days = shift;
178             }
179              
180 0           my $from_date = DateTime->today( time_zone => 'local' );
181              
182 0 0         if ( defined $days ) {
183 0           $from_date->subtract( days => $days );
184             }
185              
186 0           my @entries = App::Jiffy::TimeEntry::search(
187             $self->cfg,
188             query => {
189             start_time => { '$gt' => $from_date, },
190             $self->remove_terminators,
191             title => qr/$query_text/,
192             },
193             sort => {
194             start_time => 1,
195             },
196             );
197              
198 0 0         if ( $options->{round} ) {
199 0           @entries = map { $_->duration( round( $_->duration ) ); $_ } @entries;
  0            
  0            
200             }
201              
202 0 0         if ( not @entries ) {
203 0           print "No Entries Found\n";
204 0           return;
205             }
206              
207 0 0         if ( $options->{json} ) {
208 0           my $json = JSON::MaybeXS->new( pretty => 1, convert_blessed => 1 );
209 0           print $json->encode( \@entries );
210             } else {
211 0           $options->{from} = $days;
212 0           App::Jiffy::View::Timesheet::render( \@entries, $options );
213             }
214             }
215              
216             sub run {
217 0     0 1   my $self = shift;
218 0           my @args = @_;
219              
220 0 0         if ( $args[0] ) {
221 0 0         if ( $args[0] eq 'current' ) {
    0          
    0          
222 0           shift @args;
223 0           return $self->current_time(@args);
224             } elsif ( $args[0] eq 'timesheet' ) {
225 0           shift @args;
226              
227 0           my $p = Getopt::Long::Parser->new( config => ['pass_through'], );
228 0           $p->getoptionsfromarray(
229             \@args,
230             'verbose' => \my $verbose,
231             'round' => \my $round,
232             'json' => \my $json
233             );
234              
235 0           return $self->time_sheet( {
236             verbose => $verbose,
237             round => $round,
238             json => $json,
239             },
240             @args
241             );
242             } elsif ( $args[0] eq 'search' ) {
243 0           shift @args;
244              
245 0           my $p = Getopt::Long::Parser->new( config => ['pass_through'], );
246 0           $p->getoptionsfromarray(
247             \@args,
248             'verbose' => \my $verbose,
249             'round' => \my $round,
250             'json' => \my $json
251             );
252              
253 0           my $query_text = shift @args;
254              
255 0           return $self->search(
256             $query_text,
257             {
258             verbose => $verbose,
259             round => $round,
260             json => $json,
261             },
262             @args
263             );
264             }
265             }
266              
267 0           my $p = Getopt::Long::Parser->new( config => ['pass_through'], );
268 0           $p->getoptionsfromarray( \@args, 'time=s' => \my $time, );
269              
270 0           return $self->add_entry( {
271             time => $time,
272             },
273             join ' ',
274             @args
275             );
276             }
277              
278             1;
279             __END__
280              
281             =encoding utf-8
282              
283             =head1 NAME
284              
285             App::Jiffy - A minimalist time tracking app focused on precision and effortlessness.
286              
287             =head1 SYNOPSIS
288              
289             use App::Jiffy;
290              
291             # cmd line tool
292             jiffy Solving world hunger
293             jiffy Cleaning the plasma manifolds
294             jiffy current # Returns the elapsed time for the current task
295              
296             # Run server
297             jiffyd
298             curl -d "title=Meeting with Client X" http://localhost:3000/timeentry
299              
300             =head1 DESCRIPTION
301              
302             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".
303              
304             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:
305              
306             =over
307              
308             =item Command line tool
309              
310             =item Web app L<App::Jiffyd>
311              
312             =item iPhone app ( potentially )
313              
314             =back
315              
316             =head1 INSTALLATION
317              
318             curl -L https://cpanmin.us | perl - git://github.com/lejeunerenard/jiffy
319              
320             =head1 METHODS
321              
322             The following are methods available on the C<App::Jiffy> object.
323              
324             =head2 add_entry
325              
326             C<add_entry> will create a new TimeEntry with the current time as the entry's start_time.
327              
328             =head2 current_time
329              
330             C<current_time> will print out the elapsed time for the current task (AKA the time since the last entry was created).
331              
332             =head2 time_sheet
333              
334             C<time_sheet> will print out a time sheet including the time spent for each C<TimeEntry>.
335              
336             =head2 search( C<$query_text>, C<$days> )
337              
338             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.
339              
340             =head2 run
341              
342             C<run> will start an instance of the Jiffy app.
343              
344             =head1 AUTHOR
345              
346             Sean Zellmer E<lt>sean@lejeunerenard.comE<gt>
347              
348             =head1 COPYRIGHT
349              
350             Copyright 2015- Sean Zellmer
351              
352             =head1 LICENSE
353              
354             This library is free software; you can redistribute it and/or modify
355             it under the same terms as Perl itself.
356              
357             =cut