line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package App::TimelogTxt; |
2
|
|
|
|
|
|
|
|
3
|
1
|
|
|
1
|
|
24276
|
use warnings; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
31
|
|
4
|
1
|
|
|
1
|
|
4
|
use strict; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
23
|
|
5
|
1
|
|
|
1
|
|
16
|
use 5.010; |
|
1
|
|
|
|
|
6
|
|
|
1
|
|
|
|
|
25
|
|
6
|
|
|
|
|
|
|
|
7
|
1
|
|
|
1
|
|
38637
|
use autodie; |
|
1
|
|
|
|
|
28333
|
|
|
1
|
|
|
|
|
4
|
|
8
|
1
|
|
|
1
|
|
5570
|
use App::CmdDispatch; |
|
1
|
|
|
|
|
7265
|
|
|
1
|
|
|
|
|
29
|
|
9
|
1
|
|
|
1
|
|
711
|
use Getopt::Long qw(:config posix_default); |
|
1
|
|
|
|
|
10026
|
|
|
1
|
|
|
|
|
8
|
|
10
|
1
|
|
|
1
|
|
960
|
use App::TimelogTxt::Utils; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
29
|
|
11
|
1
|
|
|
1
|
|
405
|
use App::TimelogTxt::Day; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
24
|
|
12
|
1
|
|
|
1
|
|
338
|
use App::TimelogTxt::File; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
22
|
|
13
|
1
|
|
|
1
|
|
349
|
use App::TimelogTxt::Event; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
271
|
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
our $VERSION = '0.22'; |
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
|
|
6
|
use base 'App::CmdDispatch'; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
2364
|
|
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
|
$summary->update_dur( $last, $end_time ); |
418
|
|
|
|
|
|
|
} |
419
|
|
|
|
|
|
|
else |
420
|
|
|
|
|
|
|
{ |
421
|
0
|
|
0
|
|
|
0
|
my $datestamp = $summary->date_stamp() || $stamp; |
422
|
0
|
0
|
|
|
|
0
|
if( App::TimelogTxt::Utils::is_today( $datestamp ) ) |
423
|
|
|
|
|
|
|
{ |
424
|
0
|
|
|
|
|
0
|
$end_time = time; |
425
|
0
|
|
|
|
|
0
|
$summary->update_dur( $last, $end_time ); |
426
|
|
|
|
|
|
|
} |
427
|
|
|
|
|
|
|
else |
428
|
|
|
|
|
|
|
{ |
429
|
0
|
|
|
|
|
0
|
$summary->close_day( $last ); |
430
|
0
|
|
|
|
|
0
|
$end_time = App::TimelogTxt::Utils::stamp_to_localtime( $datestamp ); |
431
|
|
|
|
|
|
|
} |
432
|
|
|
|
|
|
|
} |
433
|
|
|
|
|
|
|
|
434
|
0
|
0
|
|
|
|
0
|
return if $summary->is_empty; |
435
|
|
|
|
|
|
|
|
436
|
0
|
|
|
|
|
0
|
return _filter_summaries( \@filters, \@summaries ); |
437
|
|
|
|
|
|
|
} |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
sub _filter_summaries |
440
|
|
|
|
|
|
|
{ |
441
|
0
|
|
|
0
|
|
0
|
my ( $filters, $summaries ) = @_; |
442
|
|
|
|
|
|
|
|
443
|
0
|
0
|
|
|
|
0
|
return $summaries unless @{$filters}; |
|
0
|
|
|
|
|
0
|
|
444
|
|
|
|
|
|
|
|
445
|
0
|
|
|
|
|
0
|
my $filter = join( '|', map { "(?:$_)" } @{$filters} ); |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
446
|
0
|
|
|
|
|
0
|
my $filter_re = qr/$filter/; |
447
|
|
|
|
|
|
|
return [ |
448
|
0
|
|
|
|
|
0
|
grep { $_->has_tasks } |
|
0
|
|
|
|
|
0
|
|
449
|
0
|
|
|
|
|
0
|
map { $_->day_filtered_by_project( $filter_re ) } |
450
|
0
|
|
|
|
|
0
|
@{$summaries} |
451
|
|
|
|
|
|
|
]; |
452
|
|
|
|
|
|
|
} |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
sub _process_extraction_args |
455
|
|
|
|
|
|
|
{ |
456
|
0
|
|
|
0
|
|
0
|
my ($day, @args) = @_; |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
# First argument is always the day |
459
|
0
|
|
|
|
|
0
|
my $stamp = App::TimelogTxt::Utils::day_stamp( $day ); |
460
|
0
|
0
|
|
|
|
0
|
die "No day provided.\n" unless defined $stamp; |
461
|
0
|
|
|
|
|
0
|
my $eday = shift @args; |
462
|
0
|
0
|
|
|
|
0
|
my $estamp = App::TimelogTxt::Utils::day_end( $eday ? App::TimelogTxt::Utils::day_stamp( $eday ) : $stamp ); |
463
|
0
|
0
|
|
|
|
0
|
if( !defined $estamp ) |
464
|
|
|
|
|
|
|
{ |
465
|
0
|
|
|
|
|
0
|
$estamp = App::TimelogTxt::Utils::day_end( $stamp ); |
466
|
0
|
0
|
|
|
|
0
|
unshift @args, $eday if $eday; |
467
|
|
|
|
|
|
|
} |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
# I need to start one day before to deal with the possibility that first |
470
|
|
|
|
|
|
|
# task was held over midnight. |
471
|
0
|
|
|
|
|
0
|
my $pstamp = App::TimelogTxt::Utils::prev_stamp( $stamp ); |
472
|
|
|
|
|
|
|
|
473
|
0
|
|
|
|
|
0
|
return ($stamp, $estamp, $pstamp, @args); |
474
|
|
|
|
|
|
|
} |
475
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
# Utility functions |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
# Find user's home directory |
479
|
|
|
|
|
|
|
sub _home |
480
|
|
|
|
|
|
|
{ |
481
|
1
|
50
|
|
1
|
|
6
|
return $ENV{HOME} if defined $ENV{HOME}; |
482
|
0
|
0
|
|
|
|
0
|
if( $^O eq 'MSWin32' ) |
483
|
|
|
|
|
|
|
{ |
484
|
0
|
0
|
|
|
|
0
|
return "$ENV{HOMEDRIVE}$ENV{HOMEPATH}" if defined $ENV{HOMEPATH}; |
485
|
0
|
0
|
|
|
|
0
|
return $ENV{USERPROFILE} if defined $ENV{USERPROFILE}; |
486
|
|
|
|
|
|
|
} |
487
|
0
|
|
|
|
|
0
|
return '/'; |
488
|
|
|
|
|
|
|
} |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
# Resolve ~ notation and convert to an absolute path. |
491
|
|
|
|
|
|
|
sub _normalize_path |
492
|
|
|
|
|
|
|
{ |
493
|
1
|
|
|
1
|
|
1
|
my ($path) = @_; |
494
|
|
|
|
|
|
|
|
495
|
1
|
|
|
|
|
4
|
my $home = _home(); |
496
|
1
|
|
|
|
|
5
|
$path =~ s/~/$home/; |
497
|
|
|
|
|
|
|
|
498
|
1
|
|
|
|
|
2
|
return $path; |
499
|
|
|
|
|
|
|
} |
500
|
|
|
|
|
|
|
|
501
|
|
|
|
|
|
|
sub _each_logline |
502
|
|
|
|
|
|
|
{ |
503
|
0
|
|
|
0
|
|
|
my ( $app, $code ) = @_; |
504
|
0
|
|
|
|
|
|
open my $fh, '<', $app->_logfile; |
505
|
0
|
|
|
|
|
|
$code->() while( <$fh> ); |
506
|
0
|
|
|
|
|
|
return; |
507
|
|
|
|
|
|
|
} |
508
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
sub _stack |
510
|
|
|
|
|
|
|
{ |
511
|
0
|
|
|
0
|
|
|
my ($app) = @_; |
512
|
0
|
|
|
|
|
|
require App::TimelogTxt::Stack; |
513
|
0
|
|
|
|
|
|
return App::TimelogTxt::Stack->new( $app->_stackfile ); |
514
|
|
|
|
|
|
|
} |
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
sub _get_last_full_event |
517
|
|
|
|
|
|
|
{ |
518
|
0
|
|
|
0
|
|
|
my ( $app ) = @_; |
519
|
0
|
|
|
|
|
|
my $event_line; |
520
|
0
|
|
|
0
|
|
|
_each_logline( $app, sub { $event_line = $_; } ); |
|
0
|
|
|
|
|
|
|
521
|
0
|
|
|
|
|
|
return App::TimelogTxt::Event->new_from_line( $event_line ); |
522
|
|
|
|
|
|
|
} |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
sub _get_last_event |
525
|
|
|
|
|
|
|
{ |
526
|
0
|
|
|
0
|
|
|
my $event = _get_last_full_event( @_ ); |
527
|
|
|
|
|
|
|
|
528
|
0
|
|
|
|
|
|
return $event->task; |
529
|
|
|
|
|
|
|
} |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
1; |
532
|
|
|
|
|
|
|
__END__ |