| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package App::TimelogTxt; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 1 |  |  | 1 |  | 23348 | use warnings; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 22 |  | 
| 4 | 1 |  |  | 1 |  | 4 | use strict; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 19 |  | 
| 5 | 1 |  |  | 1 |  | 13 | use 5.010; | 
|  | 1 |  |  |  |  | 5 |  | 
|  | 1 |  |  |  |  | 21 |  | 
| 6 |  |  |  |  |  |  |  | 
| 7 | 1 |  |  | 1 |  | 392 | use autodie; | 
|  | 1 |  |  |  |  | 11337 |  | 
|  | 1 |  |  |  |  | 4 |  | 
| 8 | 1 |  |  | 1 |  | 4394 | use App::CmdDispatch; | 
|  | 1 |  |  |  |  | 6218 |  | 
|  | 1 |  |  |  |  | 23 |  | 
| 9 | 1 |  |  | 1 |  | 594 | use Getopt::Long qw(:config posix_default); | 
|  | 1 |  |  |  |  | 7396 |  | 
|  | 1 |  |  |  |  | 5 |  | 
| 10 | 1 |  |  | 1 |  | 524 | use App::TimelogTxt::Utils; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 22 |  | 
| 11 | 1 |  |  | 1 |  | 306 | use App::TimelogTxt::Day; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 20 |  | 
| 12 | 1 |  |  | 1 |  | 282 | use App::TimelogTxt::File; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 20 |  | 
| 13 | 1 |  |  | 1 |  | 274 | use App::TimelogTxt::Event; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 235 |  | 
| 14 |  |  |  |  |  |  |  | 
| 15 |  |  |  |  |  |  | our $VERSION = '0.20'; | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | # Initial configuration information. | 
| 18 |  |  |  |  |  |  | my %config = ( | 
| 19 |  |  |  |  |  |  | editor => '', | 
| 20 |  |  |  |  |  |  | dir    => '', | 
| 21 |  |  |  |  |  |  | defcmd => '', | 
| 22 |  |  |  |  |  |  | ); | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | my $config_file =  _normalize_path( '~/.timelogrc' ); | 
| 25 |  |  |  |  |  |  |  | 
| 26 |  |  |  |  |  |  | # Dispatch table for commands | 
| 27 |  |  |  |  |  |  | my %commands = ( | 
| 28 |  |  |  |  |  |  | 'start' => { | 
| 29 |  |  |  |  |  |  | code     => \&start_event, | 
| 30 |  |  |  |  |  |  | clue     => 'start {event description}', | 
| 31 |  |  |  |  |  |  | abstract => 'Start timing a new event.', | 
| 32 |  |  |  |  |  |  | help     => 'Stop the current event and start timing a new event.', | 
| 33 |  |  |  |  |  |  | }, | 
| 34 |  |  |  |  |  |  | App::TimelogTxt::Utils::STOP_CMD() => { | 
| 35 |  |  |  |  |  |  | code => sub { my $app = shift; log_event( $app, App::TimelogTxt::Utils::STOP_CMD() ); }, | 
| 36 |  |  |  |  |  |  | clue => App::TimelogTxt::Utils::STOP_CMD(), | 
| 37 |  |  |  |  |  |  | abstract => 'Stop timing the current event.', | 
| 38 |  |  |  |  |  |  | help     => 'Stop timing the current event.', | 
| 39 |  |  |  |  |  |  | }, | 
| 40 |  |  |  |  |  |  | 'init' => { | 
| 41 |  |  |  |  |  |  | code     => \&init_timelog, | 
| 42 |  |  |  |  |  |  | clue     => 'init [directory]', | 
| 43 |  |  |  |  |  |  | abstract => 'Create the timelog directory and configuration.', | 
| 44 |  |  |  |  |  |  | help     => 'Create the directory and configuration file used by timelog | 
| 45 |  |  |  |  |  |  | if they do not already exist.', | 
| 46 |  |  |  |  |  |  | }, | 
| 47 |  |  |  |  |  |  | 'push' => { | 
| 48 |  |  |  |  |  |  | code     => \&push_event, | 
| 49 |  |  |  |  |  |  | clue     => 'push {event description}', | 
| 50 |  |  |  |  |  |  | abstract => 'Save the current event and start timing new.', | 
| 51 |  |  |  |  |  |  | help     => 'Save the current event on stack and start timing new event.', | 
| 52 |  |  |  |  |  |  | }, | 
| 53 |  |  |  |  |  |  | 'pop' => { | 
| 54 |  |  |  |  |  |  | code     => \&pop_event, | 
| 55 |  |  |  |  |  |  | clue     => 'pop', | 
| 56 |  |  |  |  |  |  | abstract => 'Return to last pushed event.', | 
| 57 |  |  |  |  |  |  | help     => 'Stop last event and restart top event on stack.', | 
| 58 |  |  |  |  |  |  | }, | 
| 59 |  |  |  |  |  |  | 'drop' => { | 
| 60 |  |  |  |  |  |  | code     => \&drop_event, | 
| 61 |  |  |  |  |  |  | clue     => 'drop [all|{n}]', | 
| 62 |  |  |  |  |  |  | abstract => 'Drop items from stack.', | 
| 63 |  |  |  |  |  |  | help     => 'Drop one or more events from top of event stack, or all | 
| 64 |  |  |  |  |  |  | if argument supplied.', | 
| 65 |  |  |  |  |  |  | }, | 
| 66 |  |  |  |  |  |  | 'ls' => { | 
| 67 |  |  |  |  |  |  | code     => \&list_events, | 
| 68 |  |  |  |  |  |  | clue     => 'ls [date]', | 
| 69 |  |  |  |  |  |  | abstract => 'List events.', | 
| 70 |  |  |  |  |  |  | help     => 'List events for the specified day. Default to today.', | 
| 71 |  |  |  |  |  |  | }, | 
| 72 |  |  |  |  |  |  | 'lsproj' => { | 
| 73 |  |  |  |  |  |  | code     => \&list_projects, | 
| 74 |  |  |  |  |  |  | clue     => 'lsproj', | 
| 75 |  |  |  |  |  |  | abstract => 'List known projects.', | 
| 76 |  |  |  |  |  |  | help     => 'List known projects.', | 
| 77 |  |  |  |  |  |  | }, | 
| 78 |  |  |  |  |  |  | 'lstk' => { | 
| 79 |  |  |  |  |  |  | code     => \&list_stack, | 
| 80 |  |  |  |  |  |  | clue     => 'lstk', | 
| 81 |  |  |  |  |  |  | abstract => 'Display items on the stack.', | 
| 82 |  |  |  |  |  |  | help     => 'Display items on the stack.', | 
| 83 |  |  |  |  |  |  | }, | 
| 84 |  |  |  |  |  |  | 'edit' => { | 
| 85 |  |  |  |  |  |  | code     => \&edit_logfile, | 
| 86 |  |  |  |  |  |  | clue     => 'edit', | 
| 87 |  |  |  |  |  |  | abstract => 'Edit the timelog file.', | 
| 88 |  |  |  |  |  |  | help     => 'Open the timelog file in the current editor', | 
| 89 |  |  |  |  |  |  | }, | 
| 90 |  |  |  |  |  |  | 'report' => { | 
| 91 |  |  |  |  |  |  | code     => \&daily_report, | 
| 92 |  |  |  |  |  |  | clue     => 'report [date [end date]] [project regexes]', | 
| 93 |  |  |  |  |  |  | abstract => 'Task report.', | 
| 94 |  |  |  |  |  |  | help     => 'Display a report for the specified days and projects.', | 
| 95 |  |  |  |  |  |  | }, | 
| 96 |  |  |  |  |  |  | 'summary' => { | 
| 97 |  |  |  |  |  |  | code     => \&daily_summary, | 
| 98 |  |  |  |  |  |  | clue     => 'summary [date [end date]] [project regexes]', | 
| 99 |  |  |  |  |  |  | abstract => 'Short summary report.', | 
| 100 |  |  |  |  |  |  | help     => q{Display a summary of the appropriate days' projects.}, | 
| 101 |  |  |  |  |  |  | }, | 
| 102 |  |  |  |  |  |  | 'hours' => { | 
| 103 |  |  |  |  |  |  | code     => \&report_hours, | 
| 104 |  |  |  |  |  |  | clue     => 'hours [date [end date]] [project regexes]', | 
| 105 |  |  |  |  |  |  | abstract => 'Hours report.', | 
| 106 |  |  |  |  |  |  | help     => q{Display the hours worked for each of the appropriate days | 
| 107 |  |  |  |  |  |  | and projects.}, | 
| 108 |  |  |  |  |  |  | }, | 
| 109 |  |  |  |  |  |  | 'curr' => { | 
| 110 |  |  |  |  |  |  | code     => \¤t_event, | 
| 111 |  |  |  |  |  |  | clue     => 'curr', | 
| 112 |  |  |  |  |  |  | abstract => 'Display current event.', | 
| 113 |  |  |  |  |  |  | help     => q{Display the current event (if any) and the time since the | 
| 114 |  |  |  |  |  |  | event started.}, | 
| 115 |  |  |  |  |  |  | }, | 
| 116 |  |  |  |  |  |  | ); | 
| 117 |  |  |  |  |  |  |  | 
| 118 |  |  |  |  |  |  | # Sub class of App::CmdDispatch that initializes configuration information | 
| 119 |  |  |  |  |  |  | # specific to this program. It also provides access to that configuration. | 
| 120 |  |  |  |  |  |  | { | 
| 121 |  |  |  |  |  |  | package Timelog::CmdDispatch; | 
| 122 | 1 |  |  | 1 |  | 3 | use base 'App::CmdDispatch'; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 1991 |  | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | sub new | 
| 125 |  |  |  |  |  |  | { | 
| 126 | 0 |  |  | 0 |  | 0 | my $self = App::CmdDispatch::new( @_ ); | 
| 127 | 0 |  |  |  |  | 0 | $self->init(); | 
| 128 | 0 |  |  |  |  | 0 | $self->{_timelog_out_fh} = \*STDOUT; | 
| 129 | 0 |  |  |  |  | 0 | return $self; | 
| 130 |  |  |  |  |  |  | } | 
| 131 |  |  |  |  |  |  |  | 
| 132 | 0 |  |  | 0 |  | 0 | sub _logfile   { return $_[0]->get_config()->{'logfile'}; } | 
| 133 | 0 |  |  | 0 |  | 0 | sub _stackfile { return $_[0]->get_config()->{'stackfile'}; } | 
| 134 | 0 |  |  | 0 |  | 0 | sub _out_fh    { return $_[0]->{_timelog_out_fh}; } | 
| 135 |  |  |  |  |  |  |  | 
| 136 |  |  |  |  |  |  | # Support injection of an output filehandle for testing. | 
| 137 | 0 |  |  | 0 |  | 0 | sub _set_out_fh { return $_[0]->{_timelog_out_fh} = $_[1]; } | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | sub init | 
| 140 |  |  |  |  |  |  | { | 
| 141 | 0 |  |  | 0 |  | 0 | my ($self) = @_; | 
| 142 | 0 |  |  |  |  | 0 | my $config = $self->get_config(); | 
| 143 |  |  |  |  |  |  |  | 
| 144 | 0 |  | 0 |  |  | 0 | $config->{editor} ||= $config{editor} || $ENV{'VISUAL'} || $ENV{'EDITOR'} || 'vim'; | 
|  |  |  | 0 |  |  |  |  | 
| 145 | 0 |  | 0 |  |  | 0 | $config->{dir}    ||= $config{dir} || App::TimelogTxt::_normalize_path( '~/timelog' ); | 
|  |  |  | 0 |  |  |  |  | 
| 146 | 0 |  | 0 |  |  | 0 | $config->{defcmd} ||= $config{defcmd} || App::TimelogTxt::Utils::STOP_CMD(); | 
|  |  |  | 0 |  |  |  |  | 
| 147 | 0 |  |  |  |  | 0 | $config->{dir} = App::TimelogTxt::_normalize_path( $config->{'dir'} ); | 
| 148 | 0 |  |  |  |  | 0 | foreach my $d ( [qw/logfile timelog.txt/], [qw/stackfile stack.txt/] ) | 
| 149 |  |  |  |  |  |  | { | 
| 150 | 0 |  |  |  |  | 0 | $config->{ $d->[0] } = "$config->{'dir'}/$d->[1]"; | 
| 151 |  |  |  |  |  |  | } | 
| 152 | 0 |  |  |  |  | 0 | return; | 
| 153 |  |  |  |  |  |  | } | 
| 154 |  |  |  |  |  |  |  | 
| 155 |  |  |  |  |  |  | } | 
| 156 |  |  |  |  |  |  |  | 
| 157 |  |  |  |  |  |  | sub run | 
| 158 |  |  |  |  |  |  | { | 
| 159 | 0 |  |  | 0 | 1 | 0 | GetOptions( | 
| 160 |  |  |  |  |  |  | "dir=s"    => \$config{'dir'}, | 
| 161 |  |  |  |  |  |  | "editor=s" => \$config{'editor'}, | 
| 162 |  |  |  |  |  |  | "conf=s"   => \$config_file, | 
| 163 |  |  |  |  |  |  | ); | 
| 164 |  |  |  |  |  |  |  | 
| 165 | 0 | 0 |  |  |  | 0 | my $options = { | 
| 166 |  |  |  |  |  |  | config           => (-f $config_file ? $config_file : undef), | 
| 167 |  |  |  |  |  |  | default_commands => 'help shell', | 
| 168 |  |  |  |  |  |  | 'help:post_hint' => | 
| 169 |  |  |  |  |  |  | "\nwhere [date] is an optional string specifying a date of the form YYYY-MM-DD | 
| 170 |  |  |  |  |  |  | or a day name: yesterday, today, or sunday .. saturday and [project regexes] | 
| 171 |  |  |  |  |  |  | is a list of strings of regular expressions matching project names.\n", | 
| 172 |  |  |  |  |  |  | 'help:post_help' => | 
| 173 |  |  |  |  |  |  | "\nwhere [date] is an optional string specifying a date of the form YYYY-MM-DD | 
| 174 |  |  |  |  |  |  | or a day name: yesterday, today, or sunday .. saturday and [project regexes] | 
| 175 |  |  |  |  |  |  | is a list of strings of regular expressions matching project names.\n", | 
| 176 |  |  |  |  |  |  | }; | 
| 177 | 0 |  |  |  |  | 0 | my $app = Timelog::CmdDispatch->new( \%commands, $options ); | 
| 178 |  |  |  |  |  |  |  | 
| 179 |  |  |  |  |  |  | # Handle default command if none specified | 
| 180 | 0 | 0 |  |  |  | 0 | @ARGV = split / /, $app->get_config()->{'defcmd'} unless @ARGV; | 
| 181 |  |  |  |  |  |  |  | 
| 182 | 0 |  |  |  |  | 0 | $app->run( @ARGV ); | 
| 183 |  |  |  |  |  |  |  | 
| 184 | 0 |  |  |  |  | 0 | return; | 
| 185 |  |  |  |  |  |  | } | 
| 186 |  |  |  |  |  |  |  | 
| 187 |  |  |  |  |  |  | # Command handlers | 
| 188 |  |  |  |  |  |  |  | 
| 189 |  |  |  |  |  |  | sub init_timelog | 
| 190 |  |  |  |  |  |  | { | 
| 191 | 0 |  |  | 0 | 1 | 0 | my ($app, $dir) = @_; | 
| 192 | 0 |  |  |  |  | 0 | require File::Path; | 
| 193 | 0 |  |  |  |  | 0 | my $config = $app->get_config(); | 
| 194 | 0 |  | 0 |  |  | 0 | $dir //= $config->{'dir'}; | 
| 195 | 0 |  |  |  |  | 0 | $dir = _normalize_path( $dir ); | 
| 196 | 0 | 0 |  |  |  | 0 | File::Path::mkpath( $dir ) unless -d $dir; | 
| 197 | 0 | 0 |  |  |  | 0 | unless( -f $config_file ) | 
| 198 |  |  |  |  |  |  | { | 
| 199 | 0 |  |  |  |  | 0 | open my $fh, '>', $config_file; | 
| 200 |  |  |  |  |  |  | # don't supply the editor value, default to the environment | 
| 201 | 0 |  |  |  |  | 0 | print {$fh} <<"EOF"; | 
|  | 0 |  |  |  |  | 0 |  | 
| 202 |  |  |  |  |  |  | dir=$dir | 
| 203 |  |  |  |  |  |  | defcmd=$config->{defcmd} | 
| 204 |  |  |  |  |  |  |  | 
| 205 |  |  |  |  |  |  | [alias] | 
| 206 |  |  |  |  |  |  | EOF | 
| 207 |  |  |  |  |  |  | } | 
| 208 | 0 |  |  |  |  | 0 | print "timelog initialized\n"; | 
| 209 | 0 |  |  |  |  | 0 | return; | 
| 210 |  |  |  |  |  |  | } | 
| 211 |  |  |  |  |  |  |  | 
| 212 |  |  |  |  |  |  | sub log_event | 
| 213 |  |  |  |  |  |  | { | 
| 214 | 0 |  |  | 0 | 1 | 0 | my $app    = shift; | 
| 215 | 0 |  |  |  |  | 0 | my $task = "@_"; | 
| 216 | 0 | 0 | 0 |  |  | 0 | if( App::TimelogTxt::Utils::is_stop_cmd( $task ) || App::TimelogTxt::Utils::has_project( $task ) ) | 
| 217 |  |  |  |  |  |  | { | 
| 218 | 0 |  |  |  |  | 0 | open my $fh, '>>', $app->_logfile; | 
| 219 | 0 |  |  |  |  | 0 | my $event = App::TimelogTxt::Event->new( $task, time ); | 
| 220 | 0 |  |  |  |  | 0 | print {$fh} $event->to_string, "\n"; | 
|  | 0 |  |  |  |  | 0 |  | 
| 221 |  |  |  |  |  |  | } | 
| 222 |  |  |  |  |  |  | else | 
| 223 |  |  |  |  |  |  | { | 
| 224 | 0 |  |  |  |  | 0 | die "Event has no project.\n"; | 
| 225 |  |  |  |  |  |  | } | 
| 226 | 0 |  |  |  |  | 0 | return; | 
| 227 |  |  |  |  |  |  | } | 
| 228 |  |  |  |  |  |  |  | 
| 229 |  |  |  |  |  |  | sub edit_logfile | 
| 230 |  |  |  |  |  |  | { | 
| 231 | 0 |  |  | 0 | 1 | 0 | my ( $app ) = @_; | 
| 232 | 0 |  |  |  |  | 0 | system $app->get_config()->{'editor'}, $app->_logfile; | 
| 233 | 0 |  |  |  |  | 0 | return; | 
| 234 |  |  |  |  |  |  | } | 
| 235 |  |  |  |  |  |  |  | 
| 236 |  |  |  |  |  |  | sub list_events | 
| 237 |  |  |  |  |  |  | { | 
| 238 | 0 |  |  | 0 | 1 | 0 | my ( $app, $day ) = @_; | 
| 239 | 0 |  |  |  |  | 0 | my $stamp = App::TimelogTxt::Utils::day_stamp( $day ); | 
| 240 |  |  |  |  |  |  |  | 
| 241 | 0 | 0 |  | 0 |  | 0 | _each_logline( $app, sub { print if 0 == index $_, $stamp; } ); | 
|  | 0 |  |  |  |  | 0 |  | 
| 242 | 0 |  |  |  |  | 0 | return; | 
| 243 |  |  |  |  |  |  | } | 
| 244 |  |  |  |  |  |  |  | 
| 245 |  |  |  |  |  |  | sub list_projects | 
| 246 |  |  |  |  |  |  | { | 
| 247 | 0 |  |  | 0 | 1 | 0 | my ( $app ) = @_; | 
| 248 | 0 |  |  |  |  | 0 | my %projects; | 
| 249 |  |  |  |  |  |  | _each_logline( | 
| 250 |  |  |  |  |  |  | $app, | 
| 251 |  |  |  |  |  |  | sub { | 
| 252 | 0 |  |  | 0 |  | 0 | my ( @projs ) = m/\+(\S+)/g; | 
| 253 | 0 | 0 |  |  |  | 0 | @projects{@projs} = ( 1 ) x @projs if @projs; | 
| 254 |  |  |  |  |  |  | } | 
| 255 | 0 |  |  |  |  | 0 | ); | 
| 256 | 0 |  |  |  |  | 0 | print "$_\n" foreach sort keys %projects; | 
| 257 | 0 |  |  |  |  | 0 | return; | 
| 258 |  |  |  |  |  |  | } | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | sub daily_report | 
| 261 |  |  |  |  |  |  | { | 
| 262 | 0 |  |  | 0 | 1 | 0 | my ( $app, @filters ) = @_; | 
| 263 |  |  |  |  |  |  |  | 
| 264 | 0 |  |  |  |  | 0 | my $summaries = extract_day_tasks( $app, @filters ); | 
| 265 |  |  |  |  |  |  |  | 
| 266 | 0 |  |  |  |  | 0 | foreach my $summary ( @{$summaries} ) | 
|  | 0 |  |  |  |  | 0 |  | 
| 267 |  |  |  |  |  |  | { | 
| 268 | 0 |  |  |  |  | 0 | $summary->print_day_detail( $app->_out_fh ); | 
| 269 |  |  |  |  |  |  | } | 
| 270 | 0 |  |  |  |  | 0 | return; | 
| 271 |  |  |  |  |  |  | } | 
| 272 |  |  |  |  |  |  |  | 
| 273 |  |  |  |  |  |  | sub daily_summary | 
| 274 |  |  |  |  |  |  | { | 
| 275 | 0 |  |  | 0 | 1 | 0 | my ( $app, @filters ) = @_; | 
| 276 |  |  |  |  |  |  |  | 
| 277 | 0 |  |  |  |  | 0 | my $summaries = extract_day_tasks( $app, @filters ); | 
| 278 |  |  |  |  |  |  |  | 
| 279 | 0 |  |  |  |  | 0 | foreach my $summary ( @{$summaries} ) | 
|  | 0 |  |  |  |  | 0 |  | 
| 280 |  |  |  |  |  |  | { | 
| 281 | 0 |  |  |  |  | 0 | $summary->print_day_summary( $app->_out_fh ); | 
| 282 |  |  |  |  |  |  | } | 
| 283 | 0 |  |  |  |  | 0 | return; | 
| 284 |  |  |  |  |  |  | } | 
| 285 |  |  |  |  |  |  |  | 
| 286 |  |  |  |  |  |  | sub report_hours | 
| 287 |  |  |  |  |  |  | { | 
| 288 | 0 |  |  | 0 | 1 | 0 | my ( $app, @filters ) = @_; | 
| 289 |  |  |  |  |  |  |  | 
| 290 | 0 |  |  |  |  | 0 | my $summaries = extract_day_tasks( $app, @filters ); | 
| 291 |  |  |  |  |  |  |  | 
| 292 | 0 |  |  |  |  | 0 | foreach my $summary ( @{$summaries} ) | 
|  | 0 |  |  |  |  | 0 |  | 
| 293 |  |  |  |  |  |  | { | 
| 294 | 0 |  |  |  |  | 0 | $summary->print_hours( $app->_out_fh ); | 
| 295 |  |  |  |  |  |  | } | 
| 296 | 0 |  |  |  |  | 0 | return; | 
| 297 |  |  |  |  |  |  | } | 
| 298 |  |  |  |  |  |  |  | 
| 299 |  |  |  |  |  |  | sub start_event | 
| 300 |  |  |  |  |  |  | { | 
| 301 | 0 |  |  | 0 | 1 | 0 | my ( $app, @event ) = @_; | 
| 302 | 0 |  |  |  |  | 0 | log_event( $app, @event ); | 
| 303 | 0 |  |  |  |  | 0 | return; | 
| 304 |  |  |  |  |  |  | } | 
| 305 |  |  |  |  |  |  |  | 
| 306 |  |  |  |  |  |  | sub push_event | 
| 307 |  |  |  |  |  |  | { | 
| 308 | 0 |  |  | 0 | 1 | 0 | my ( $app, @event ) = @_; | 
| 309 | 0 |  |  |  |  | 0 | my $stack = _stack( $app ); | 
| 310 | 0 |  |  |  |  | 0 | $stack->push( _get_last_event( $app ) ); | 
| 311 | 0 |  |  |  |  | 0 | log_event( $app, @event ); | 
| 312 | 0 |  |  |  |  | 0 | return; | 
| 313 |  |  |  |  |  |  | } | 
| 314 |  |  |  |  |  |  |  | 
| 315 |  |  |  |  |  |  | sub pop_event | 
| 316 |  |  |  |  |  |  | { | 
| 317 | 0 |  |  | 0 | 1 | 0 | my ( $app ) = @_; | 
| 318 | 0 | 0 |  |  |  | 0 | return unless -f $app->_stackfile; | 
| 319 | 0 |  |  |  |  | 0 | my $stack = _stack( $app ); | 
| 320 | 0 |  |  |  |  | 0 | my $event = $stack->pop; | 
| 321 | 0 | 0 |  |  |  | 0 | die "Event stack is empty.\n" unless $event; | 
| 322 | 0 |  |  |  |  | 0 | log_event( $app, $event ); | 
| 323 | 0 |  |  |  |  | 0 | return; | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  |  | 
| 326 |  |  |  |  |  |  | sub drop_event | 
| 327 |  |  |  |  |  |  | { | 
| 328 | 0 |  |  | 0 | 1 | 0 | my ( $app, $arg ) = @_; | 
| 329 | 0 | 0 |  |  |  | 0 | return unless -f $app->_stackfile; | 
| 330 | 0 |  |  |  |  | 0 | my $stack = _stack( $app ); | 
| 331 | 0 |  |  |  |  | 0 | $stack->drop( $arg ); | 
| 332 | 0 |  |  |  |  | 0 | return; | 
| 333 |  |  |  |  |  |  | } | 
| 334 |  |  |  |  |  |  |  | 
| 335 |  |  |  |  |  |  | sub list_stack | 
| 336 |  |  |  |  |  |  | { | 
| 337 | 0 |  |  | 0 | 1 | 0 | my ($app) = @_; | 
| 338 | 0 | 0 |  |  |  | 0 | return unless -f $app->_stackfile; | 
| 339 | 0 |  |  |  |  | 0 | my $stack = _stack( $app ); | 
| 340 | 0 |  |  |  |  | 0 | $stack->list( $app->_out_fh ); | 
| 341 | 0 |  |  |  |  | 0 | return; | 
| 342 |  |  |  |  |  |  | } | 
| 343 |  |  |  |  |  |  |  | 
| 344 |  |  |  |  |  |  | sub current_event | 
| 345 |  |  |  |  |  |  | { | 
| 346 | 0 |  |  | 0 | 1 | 0 | my ($app) = @_; | 
| 347 | 0 |  |  |  |  | 0 | my $fh = $app->_out_fh; | 
| 348 |  |  |  |  |  |  |  | 
| 349 | 0 |  |  |  |  | 0 | my $event = _get_last_full_event( $app ); | 
| 350 | 0 | 0 |  |  |  | 0 | if( $event->is_stop ) | 
| 351 |  |  |  |  |  |  | { | 
| 352 | 0 |  |  |  |  | 0 | print {$fh} "Not in event.\n"; | 
|  | 0 |  |  |  |  | 0 |  | 
| 353 | 0 |  |  |  |  | 0 | return; | 
| 354 |  |  |  |  |  |  | } | 
| 355 |  |  |  |  |  |  |  | 
| 356 |  |  |  |  |  |  | # Use day summary object to get duration and to report. | 
| 357 | 0 |  |  |  |  | 0 | my $summary = App::TimelogTxt::Day->new( $event->stamp ); | 
| 358 | 0 |  |  |  |  | 0 | $summary->start_task( $event ); | 
| 359 | 0 |  |  |  |  | 0 | $summary->update_dur( $event, time ); | 
| 360 |  |  |  |  |  |  |  | 
| 361 | 0 |  |  |  |  | 0 | print {$fh} $event->to_string, "\nDuration: "; | 
|  | 0 |  |  |  |  | 0 |  | 
| 362 | 0 |  |  |  |  | 0 | $summary->print_duration( $fh ); | 
| 363 |  |  |  |  |  |  |  | 
| 364 | 0 |  |  |  |  | 0 | return; | 
| 365 |  |  |  |  |  |  | } | 
| 366 |  |  |  |  |  |  |  | 
| 367 |  |  |  |  |  |  | # Extract the daily events from the timelog.txt file and generate the list of Day | 
| 368 |  |  |  |  |  |  | # objects that encapsulates them. | 
| 369 |  |  |  |  |  |  |  | 
| 370 |  |  |  |  |  |  | sub extract_day_tasks | 
| 371 |  |  |  |  |  |  | { | 
| 372 | 0 |  |  | 0 | 1 | 0 | my ( $app, @args ) = @_; | 
| 373 |  |  |  |  |  |  |  | 
| 374 | 0 |  |  |  |  | 0 | my ($stamp, $estamp, $pstamp, @filters) = _process_extraction_args( @args ); | 
| 375 | 0 |  |  |  |  | 0 | my ( $summary, $last, @summaries ); | 
| 376 | 0 |  |  |  |  | 0 | my $prev_stamp = ''; | 
| 377 |  |  |  |  |  |  |  | 
| 378 | 0 |  |  |  |  | 0 | open my $fh, '<', $app->_logfile; | 
| 379 | 0 |  |  |  |  | 0 | my $file = App::TimelogTxt::File->new( $fh, $pstamp, $estamp ); | 
| 380 |  |  |  |  |  |  |  | 
| 381 | 0 |  |  |  |  | 0 | while( defined( my $line = $file->readline ) ) | 
| 382 |  |  |  |  |  |  | { | 
| 383 | 0 |  |  |  |  | 0 | my $event; | 
| 384 | 0 | 0 |  |  |  | 0 | eval { | 
| 385 | 0 |  |  |  |  | 0 | $event = App::TimelogTxt::Event->new_from_line( $line ); | 
| 386 |  |  |  |  |  |  | } or next; | 
| 387 | 0 | 0 |  |  |  | 0 | if( $prev_stamp ne $event->stamp ) | 
| 388 |  |  |  |  |  |  | { | 
| 389 | 0 |  |  |  |  | 0 | my $new_stamp = $event->stamp; | 
| 390 | 0 |  |  |  |  | 0 | my $new_summary = App::TimelogTxt::Day->new( $new_stamp ); | 
| 391 | 0 | 0 | 0 |  |  | 0 | if( $summary and !$summary->is_complete() ) | 
| 392 |  |  |  |  |  |  | { | 
| 393 | 0 |  |  |  |  | 0 | $summary->close_day( $last ); | 
| 394 |  |  |  |  |  |  | # Need to build a new last item beginning at midnight for the | 
| 395 |  |  |  |  |  |  | # Previous event. | 
| 396 | 0 |  |  |  |  | 0 | my $start = $summary->day_end()+1; | 
| 397 | 0 |  |  |  |  | 0 | $last = App::TimelogTxt::Event->new( $last->task(), $start ); | 
| 398 | 0 |  |  |  |  | 0 | $new_summary->start_task( $last ); | 
| 399 |  |  |  |  |  |  | } | 
| 400 | 0 |  |  |  |  | 0 | $summary = $new_summary; | 
| 401 | 0 |  |  |  |  | 0 | push @summaries, $summary; | 
| 402 | 0 |  |  |  |  | 0 | $prev_stamp = $new_stamp; | 
| 403 |  |  |  |  |  |  | } | 
| 404 | 0 |  |  |  |  | 0 | $summary->update_dur( $last, $event->epoch ); | 
| 405 | 0 |  |  |  |  | 0 | $summary->start_task( $event ); | 
| 406 | 0 | 0 |  |  |  | 0 | $last = ($event->is_stop() ? undef : $event ); | 
| 407 |  |  |  |  |  |  | } | 
| 408 |  |  |  |  |  |  |  | 
| 409 |  |  |  |  |  |  | # If the first summary is the day before we were supposed to report, | 
| 410 |  |  |  |  |  |  | #   drop it. | 
| 411 | 0 | 0 | 0 |  |  | 0 | shift @summaries if @summaries && $summaries[0]->date_stamp() eq $pstamp; | 
| 412 |  |  |  |  |  |  |  | 
| 413 | 0 | 0 |  |  |  | 0 | return [] unless $summary; | 
| 414 | 0 |  |  |  |  | 0 | my $end_time; | 
| 415 | 0 | 0 |  |  |  | 0 | if( !$summary->is_complete() ) | 
| 416 |  |  |  |  |  |  | { | 
| 417 | 0 |  | 0 |  |  | 0 | my $datestamp = $summary->date_stamp() || $stamp; | 
| 418 | 0 | 0 |  |  |  | 0 | if( App::TimelogTxt::Utils::is_today( $datestamp ) ) | 
| 419 |  |  |  |  |  |  | { | 
| 420 | 0 |  |  |  |  | 0 | $end_time = time; | 
| 421 |  |  |  |  |  |  | } | 
| 422 |  |  |  |  |  |  | else | 
| 423 |  |  |  |  |  |  | { | 
| 424 | 0 |  |  |  |  | 0 | $summary->close_day( $last ); | 
| 425 | 0 |  |  |  |  | 0 | $end_time = App::TimelogTxt::Utils::stamp_to_localtime( $datestamp ); | 
| 426 |  |  |  |  |  |  | } | 
| 427 |  |  |  |  |  |  | } | 
| 428 |  |  |  |  |  |  |  | 
| 429 | 0 |  |  |  |  | 0 | $summary->update_dur( $last, $end_time ); | 
| 430 |  |  |  |  |  |  |  | 
| 431 | 0 | 0 |  |  |  | 0 | return if $summary->is_empty; | 
| 432 |  |  |  |  |  |  |  | 
| 433 | 0 |  |  |  |  | 0 | return _filter_summaries( \@filters, \@summaries ); | 
| 434 |  |  |  |  |  |  | } | 
| 435 |  |  |  |  |  |  |  | 
| 436 |  |  |  |  |  |  | sub _filter_summaries | 
| 437 |  |  |  |  |  |  | { | 
| 438 | 0 |  |  | 0 |  | 0 | my ( $filters, $summaries ) = @_; | 
| 439 |  |  |  |  |  |  |  | 
| 440 | 0 | 0 |  |  |  | 0 | return $summaries unless @{$filters}; | 
|  | 0 |  |  |  |  | 0 |  | 
| 441 |  |  |  |  |  |  |  | 
| 442 | 0 |  |  |  |  | 0 | my $filter = join( '|', map { "(?:$_)" } @{$filters} ); | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 443 | 0 |  |  |  |  | 0 | my $filter_re = qr/$filter/; | 
| 444 |  |  |  |  |  |  | return [ | 
| 445 | 0 |  |  |  |  | 0 | grep { $_->has_tasks } | 
|  | 0 |  |  |  |  | 0 |  | 
| 446 | 0 |  |  |  |  | 0 | map { $_->day_filtered_by_project( $filter_re ) } | 
| 447 | 0 |  |  |  |  | 0 | @{$summaries} | 
| 448 |  |  |  |  |  |  | ]; | 
| 449 |  |  |  |  |  |  | } | 
| 450 |  |  |  |  |  |  |  | 
| 451 |  |  |  |  |  |  | sub _process_extraction_args | 
| 452 |  |  |  |  |  |  | { | 
| 453 | 0 |  |  | 0 |  | 0 | my ($day, @args) = @_; | 
| 454 |  |  |  |  |  |  |  | 
| 455 |  |  |  |  |  |  | # First argument is always the day | 
| 456 | 0 |  |  |  |  | 0 | my $stamp = App::TimelogTxt::Utils::day_stamp( $day ); | 
| 457 | 0 | 0 |  |  |  | 0 | die "No day provided.\n" unless defined $stamp; | 
| 458 | 0 |  |  |  |  | 0 | my $eday = shift @args; | 
| 459 | 0 | 0 |  |  |  | 0 | my $estamp = App::TimelogTxt::Utils::day_end( $eday ? App::TimelogTxt::Utils::day_stamp( $eday ) : $stamp ); | 
| 460 | 0 | 0 |  |  |  | 0 | if( !defined $estamp ) | 
| 461 |  |  |  |  |  |  | { | 
| 462 | 0 |  |  |  |  | 0 | $estamp = App::TimelogTxt::Utils::day_end( $stamp ); | 
| 463 | 0 | 0 |  |  |  | 0 | unshift @args, $eday if $eday; | 
| 464 |  |  |  |  |  |  | } | 
| 465 |  |  |  |  |  |  |  | 
| 466 |  |  |  |  |  |  | # I need to start one day before to deal with the possibility that first | 
| 467 |  |  |  |  |  |  | #   task was held over midnight. | 
| 468 | 0 |  |  |  |  | 0 | my $pstamp = App::TimelogTxt::Utils::prev_stamp( $stamp ); | 
| 469 |  |  |  |  |  |  |  | 
| 470 | 0 |  |  |  |  | 0 | return ($stamp, $estamp, $pstamp, @args); | 
| 471 |  |  |  |  |  |  | } | 
| 472 |  |  |  |  |  |  |  | 
| 473 |  |  |  |  |  |  | # Utility functions | 
| 474 |  |  |  |  |  |  |  | 
| 475 |  |  |  |  |  |  | # Find user's home directory | 
| 476 |  |  |  |  |  |  | sub _home | 
| 477 |  |  |  |  |  |  | { | 
| 478 | 1 | 50 |  | 1 |  | 5 | return $ENV{HOME} if defined $ENV{HOME}; | 
| 479 | 0 | 0 |  |  |  | 0 | if( $^O eq 'MSWin32' ) | 
| 480 |  |  |  |  |  |  | { | 
| 481 | 0 | 0 |  |  |  | 0 | return "$ENV{HOMEDRIVE}$ENV{HOMEPATH}" if defined $ENV{HOMEPATH}; | 
| 482 | 0 | 0 |  |  |  | 0 | return $ENV{USERPROFILE} if defined $ENV{USERPROFILE}; | 
| 483 |  |  |  |  |  |  | } | 
| 484 | 0 |  |  |  |  | 0 | return '/'; | 
| 485 |  |  |  |  |  |  | } | 
| 486 |  |  |  |  |  |  |  | 
| 487 |  |  |  |  |  |  | # Resolve ~ notation and convert to an absolute path. | 
| 488 |  |  |  |  |  |  | sub _normalize_path | 
| 489 |  |  |  |  |  |  | { | 
| 490 | 1 |  |  | 1 |  | 1 | my ($path) = @_; | 
| 491 |  |  |  |  |  |  |  | 
| 492 | 1 |  |  |  |  | 3 | my $home = _home(); | 
| 493 | 1 |  |  |  |  | 3 | $path =~ s/~/$home/; | 
| 494 |  |  |  |  |  |  |  | 
| 495 | 1 |  |  |  |  | 2 | return $path; | 
| 496 |  |  |  |  |  |  | } | 
| 497 |  |  |  |  |  |  |  | 
| 498 |  |  |  |  |  |  | sub _each_logline | 
| 499 |  |  |  |  |  |  | { | 
| 500 | 0 |  |  | 0 |  |  | my ( $app, $code ) = @_; | 
| 501 | 0 |  |  |  |  |  | open my $fh, '<', $app->_logfile; | 
| 502 | 0 |  |  |  |  |  | $code->() while( <$fh> ); | 
| 503 | 0 |  |  |  |  |  | return; | 
| 504 |  |  |  |  |  |  | } | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | sub _stack | 
| 507 |  |  |  |  |  |  | { | 
| 508 | 0 |  |  | 0 |  |  | my ($app) = @_; | 
| 509 | 0 |  |  |  |  |  | require App::TimelogTxt::Stack; | 
| 510 | 0 |  |  |  |  |  | return App::TimelogTxt::Stack->new( $app->_stackfile ); | 
| 511 |  |  |  |  |  |  | } | 
| 512 |  |  |  |  |  |  |  | 
| 513 |  |  |  |  |  |  | sub _get_last_full_event | 
| 514 |  |  |  |  |  |  | { | 
| 515 | 0 |  |  | 0 |  |  | my ( $app ) = @_; | 
| 516 | 0 |  |  |  |  |  | my $event_line; | 
| 517 | 0 |  |  | 0 |  |  | _each_logline( $app, sub { $event_line = $_; } ); | 
|  | 0 |  |  |  |  |  |  | 
| 518 | 0 |  |  |  |  |  | return App::TimelogTxt::Event->new_from_line( $event_line ); | 
| 519 |  |  |  |  |  |  | } | 
| 520 |  |  |  |  |  |  |  | 
| 521 |  |  |  |  |  |  | sub _get_last_event | 
| 522 |  |  |  |  |  |  | { | 
| 523 | 0 |  |  | 0 |  |  | my $event = _get_last_full_event( @_ ); | 
| 524 |  |  |  |  |  |  |  | 
| 525 | 0 |  |  |  |  |  | return $event->task; | 
| 526 |  |  |  |  |  |  | } | 
| 527 |  |  |  |  |  |  |  | 
| 528 |  |  |  |  |  |  | 1; | 
| 529 |  |  |  |  |  |  | __END__ |