File Coverage

blib/lib/Text/Todo.pm
Criterion Covered Total %
statement 32 43 74.4
branch 5 14 35.7
condition 0 6 0.0
subroutine 8 8 100.0
pod n/a
total 45 71 63.3


line stmt bran cond sub pod time code
1             package Text::Todo;
2              
3             # $AFresh1: Todo.pm,v 1.27 2010/02/16 01:13:12 andrew Exp $
4              
5 3     3   197805 use warnings;
  3         9  
  3         120  
6 3     3   18 use strict;
  3         5  
  3         104  
7 3     3   18 use Carp;
  3         9  
  3         236  
8              
9 3     3   3741 use Class::Std::Utils;
  3         13615  
  3         25  
10 3     3   2486 use Text::Todo::Entry;
  3         9  
  3         104  
11 3     3   21 use File::Spec;
  3         20  
  3         84  
12              
13 3     3   15 use version; our $VERSION = qv('0.2.0');
  3         5  
  3         14  
14              
15             {
16              
17             my @attr_refs = \(
18             my %path_of,
19              
20             my %list_of,
21             my %loaded_of,
22             my %known_tags_of,
23             );
24              
25             sub new {
26 2     2   1334 my ( $class, $options ) = @_;
27              
28 2         12 my $self = bless anon_scalar(), $class;
29 2         19 my $ident = ident($self);
30              
31 2         14 $path_of{$ident} = {
32             todo_dir => undef,
33             todo_file => 'todo.txt',
34             done_file => undef,
35             };
36              
37 2         10 my %tags = (
38             context => q{@},
39             project => q{+},
40             );
41              
42 2 100       8 if ($options) {
43 1 50       4 if ( ref $options eq 'HASH' ) {
44 0         0 foreach my $opt ( keys %{$options} ) {
  0         0  
45 0 0 0     0 if ( exists $path_of{$ident}{$opt} ) {
    0          
46 0         0 $self->_path_to( $opt, $options->{$opt} );
47             }
48             elsif ( $opt eq 'tags'
49             && ref $options->{$opt} eq 'HASH' )
50             {
51 0         0 %tags = ( %tags, %{ $options->{$opt} } );
  0         0  
52             }
53             else {
54              
55             #carp "Invalid option [$opt]";
56             }
57             }
58             }
59             else {
60 1 50       37 if ( -d $options ) {
    50          
61 0         0 $self->_path_to( 'todo_dir', $options );
62             }
63             elsif ( $options =~ /\.txt$/ixms ) {
64 1         23 $self->_path_to( 'todo_file', $options );
65             }
66             else {
67 0         0 carp "Unknown options [$options]";
68             }
69             }
70             }
71              
72 1         2 $known_tags_of{$ident} = \%tags;
73              
74 1         21 my $file = $self->_path_to('todo_file');
75 0 0 0       if ( defined $file && -e $file ) {
76 0           $self->load();
77             }
78              
79 0           return $self;
80             }
81              
82             sub _path_to {
83             my ( $self, $type, $path ) = @_;
84             my $ident = ident($self);
85              
86             if ( $type eq 'todo_dir' ) {
87             if ($path) {
88             $path_of{$ident}{$type} = $path;
89             }
90             return $path_of{$ident}{$type};
91             }
92              
93             if ($path) {
94             my ( $volume, $directories, $file )
95             = File::Spec->splitpath($path);
96             $path_of{$ident}{$type} = $file;
97              
98             if ($volume) {
99             $directories = File::Spec->catdir( $volume, $directories );
100             }
101              
102             # XXX Should we save complete paths to each file, mebbe only if
103             # the dirs are different?
104             if ($directories) {
105             $path_of{$ident}{todo_dir} = $directories;
106             }
107             }
108              
109             if ( $type =~ /(todo|done|report)_file/xms ) {
110             if ( my ( $pre, $post )
111             = $path_of{$ident}{$type} =~ /^(.*)$1(.*)\.txt$/ixms )
112             {
113             foreach my $f qw( todo done report ) {
114             if ( !defined $path_of{$ident}{ $f . '_file' } ) {
115             $path_of{$ident}{ $f . '_file' }
116             = $pre . $f . $post . '.txt';
117             }
118             }
119             }
120             }
121              
122             if ( defined $path_of{$ident}{todo_dir} ) {
123             return File::Spec->catfile( $path_of{$ident}{todo_dir},
124             $path_of{$ident}{$type} );
125             }
126              
127             return;
128             }
129              
130             sub file {
131             my ( $self, $file ) = @_;
132             my $ident = ident($self);
133              
134             if ( defined $file && exists $path_of{$ident}{$file} ) {
135             $file = $self->_path_to($file);
136             }
137             else {
138             $file = $self->_path_to( 'todo_file', $file );
139             }
140              
141             return $file;
142             }
143              
144             sub load {
145             my ( $self, $file ) = @_;
146             my $ident = ident($self);
147              
148             $loaded_of{$ident} = undef;
149              
150             $file = $self->file($file);
151              
152             if ( $list_of{$ident} = $self->listfile($file) ) {
153             $self->known_tags;
154             $loaded_of{$ident} = $file;
155             return 1;
156             }
157              
158             return;
159             }
160              
161             sub listfile {
162             my ( $self, $file ) = @_;
163              
164             $file = $self->file($file);
165              
166             if ( !defined $file ) {
167             carp q{file can't be found};
168             return;
169             }
170              
171             if ( !-e $file ) {
172             carp "file [$file] does not exist";
173             return;
174             }
175              
176             my @list;
177             open my $fh, '<', $file or croak "Couldn't open [$file]: $!";
178             while (<$fh>) {
179             s/\r?\n$//xms;
180             push @list, Text::Todo::Entry->new($_);
181             }
182             close $fh or croak "Couldn't close [$file]: $!";
183              
184             return wantarray ? @list : \@list;
185             }
186              
187             sub save {
188             my ( $self, $file ) = @_;
189             my $ident = ident($self);
190              
191             $file = $self->file($file);
192             if ( !defined $file ) {
193             croak q{todo file can't be found};
194             }
195              
196             open my $fh, '>', $file or croak "Couldn't open [$file]: $!";
197             foreach my $e ( @{ $list_of{$ident} } ) {
198             print {$fh} $e->text . "\n"
199             or croak "Couldn't print to [$file]: $!";
200             }
201             close $fh or croak "Couldn't close [$file]: $!";
202              
203             $loaded_of{$ident} = $file;
204              
205             return 1;
206             }
207              
208             sub list {
209             my ($self) = @_;
210             my $ident = ident($self);
211              
212             return if !$list_of{$ident};
213             return wantarray ? @{ $list_of{$ident} } : $list_of{$ident};
214             }
215              
216             sub listpri {
217             my ( $self, $pri ) = @_;
218              
219             my @list;
220             if ($pri) {
221             $pri = uc $pri;
222             if ( $pri !~ /^[A-Z]$/xms ) {
223             croak 'PRIORITY must a single letter from A to Z.';
224             }
225             @list = grep { defined $_->priority && $_->priority eq $pri }
226             $self->list;
227             }
228             else {
229             @list = grep { $_->priority } $self->list;
230             }
231              
232             return wantarray ? @list : \@list;
233             }
234              
235             sub add {
236             my ( $self, $entry ) = @_;
237             my $ident = ident($self);
238              
239             if ( !ref $entry ) {
240             $entry = Text::Todo::Entry->new(
241             { text => $entry,
242             tags => $known_tags_of{$ident},
243             }
244             );
245             }
246             elsif ( ref $entry ne 'Text::Todo::Entry' ) {
247             croak(
248             'entry is a ' . ref($entry) . ' not a Text::Todo::Entry!' );
249             }
250              
251             push @{ $list_of{$ident} }, $entry;
252              
253             $self->known_tags;
254              
255             return $entry;
256             }
257              
258             sub del {
259             my ( $self, $src ) = @_;
260             my $ident = ident($self);
261              
262             my $id = $self->_find_entry_id($src);
263              
264             my @list = $self->list;
265             my $entry = splice @list, $id, 1;
266             $list_of{$ident} = \@list;
267              
268             return $entry;
269             }
270              
271             sub move {
272             my ( $self, $entry, $dst ) = @_;
273             my $ident = ident($self);
274              
275             my $src = $self->_find_entry_id($entry);
276             my @list = $self->list;
277              
278             splice @list, $dst, 0, splice @list, $src, 1;
279              
280             $list_of{$ident} = \@list;
281              
282             return 1;
283             }
284              
285             sub listproj {
286             my ($self) = @_;
287             return $self->listtag('project');
288             }
289              
290             sub listcon {
291             my ($self) = @_;
292             return $self->listtag('context');
293             }
294              
295             sub listtag {
296             my ( $self, $tag ) = @_;
297             my $ident = ident($self);
298              
299             my $accessor = $tag . 's';
300              
301             my %available;
302             foreach my $e ( $self->list ) {
303             foreach my $p ( $e->$accessor ) {
304             $available{$p} = 1;
305             }
306             }
307              
308             my @tags = sort keys %available;
309              
310             return wantarray ? @tags : \@tags;
311             }
312              
313             sub learn_tag {
314             my ( $self, $tag, $sigal ) = @_;
315              
316             $known_tags_of{ ident $self}{$tag} = $sigal;
317             $self->known_tags;
318              
319             return 1;
320             }
321              
322             sub known_tags {
323             my ($self) = @_;
324             my $ident = ident($self);
325              
326             my @list = $self->list;
327             my %tags = %{ $known_tags_of{$ident} };
328              
329             foreach my $e (@list) {
330             my $kt = $e->known_tags;
331             foreach my $t ( keys %{$kt} ) {
332             if ( !exists $tags{$t} ) {
333             $tags{$t} = $kt->{$t};
334             }
335             }
336             }
337              
338             foreach my $e (@list) {
339             my $kt = $e->known_tags;
340             foreach my $t ( keys %tags ) {
341             if ( !exists $kt->{$t} || $tags{$t} ne $kt->{$t} ) {
342             $e->learn_tag( $t, $tags{$t} );
343             }
344             }
345             }
346              
347             $known_tags_of{$ident} = \%tags;
348              
349             return $known_tags_of{$ident};
350             }
351              
352             sub archive {
353             my ($self) = @_;
354             my $ident = ident($self);
355              
356             if ( !defined $loaded_of{$ident}
357             || $loaded_of{$ident} ne $self->file('todo_file') )
358             {
359             carp 'todo_file not loaded';
360             return;
361             }
362              
363             my $changed = 0;
364             ENTRY: foreach my $e ( $self->list ) {
365             if ( $e->done ) {
366             if ( $self->addto( 'done_file', $e ) && $self->del($e) ) {
367             $changed++;
368             }
369             else {
370             carp q{Couldn't archive entry [} . $e->text . ']';
371             last ENTRY;
372             }
373             }
374             elsif ( $e->text eq q{} ) {
375             if ( $self->del($e) ) {
376             $changed++;
377             }
378             else {
379             carp q{Couldn't delete blank entry};
380             last ENTRY;
381             }
382             }
383             }
384              
385             if ($changed) {
386             $self->save;
387             }
388              
389             return $changed;
390             }
391              
392             sub addto {
393             my ( $self, $file, $entry ) = @_;
394             my $ident = ident($self);
395              
396             $file = $self->file($file);
397             if ( !defined $file ) {
398             croak q{file can't be found};
399             }
400              
401             if ( ref $entry ) {
402             if ( ref $entry eq 'Text::Todo::Entry' ) {
403             $entry = $entry->text;
404             }
405             else {
406             carp 'Unknown ref [' . ref($entry) . ']';
407             return;
408             }
409             }
410              
411             open my $fh, '>>', $file or croak "Couldn't open [$file]: $!";
412             print {$fh} $entry, "\n"
413             or croak "Couldn't print to [$file]: $!";
414             close $fh or croak "Couldn't close [$file]: $!";
415              
416             if ( defined $loaded_of{$ident} && $file eq $loaded_of{$ident} ) {
417             return $self->load($file);
418             }
419              
420             return 1;
421             }
422              
423             sub _find_entry_id {
424             my ( $self, $entry ) = @_;
425             my $ident = ident($self);
426              
427             if ( ref $entry ) {
428             if ( ref $entry ne 'Text::Todo::Entry' ) {
429             croak( 'entry is a '
430             . ref($entry)
431             . ' not a Text::Todo::Entry!' );
432             }
433              
434             my @list = $self->list;
435             foreach my $id ( 0 .. $#list ) {
436             if ( $list[$id] eq $entry ) {
437             return $id;
438             }
439             }
440             }
441             elsif ( $entry =~ /^\d+$/xms ) {
442             return $entry;
443             }
444              
445             croak "Invalid entry [$entry]!";
446             }
447              
448             sub DESTROY {
449             my ($self) = @_;
450             my $ident = ident $self;
451              
452             foreach my $attr_ref (@attr_refs) {
453             delete $attr_ref->{$ident};
454             }
455              
456             return;
457             }
458             }
459              
460             1; # Magic true value required at end of module
461             __END__