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