Annotation of todotxt/Text-Todo/lib/Text/Todo.pm, Revision 1.17
1.1 andrew 1: package Text::Todo;
2:
1.17 ! andrew 3: # $AFresh1: Todo.pm,v 1.16 2010/01/12 19:40:52 andrew Exp $
1.1 andrew 4:
5: use warnings;
6: use strict;
7: use Carp;
8:
1.2 andrew 9: use Class::Std::Utils;
10: use Text::Todo::Entry;
1.5 andrew 11: use File::Spec;
12:
1.1 andrew 13: use version; our $VERSION = qv('0.0.1');
14:
1.2 andrew 15: {
16:
1.5 andrew 17: my %path_of;
1.2 andrew 18: my %list_of;
1.8 andrew 19: my %loaded_of;
1.1 andrew 20:
1.2 andrew 21: sub new {
1.5 andrew 22: my ( $class, $options ) = @_;
1.1 andrew 23:
1.2 andrew 24: my $self = bless anon_scalar(), $class;
25: my $ident = ident($self);
26:
1.5 andrew 27: $path_of{$ident} = {
1.14 andrew 28: todo_dir => undef,
29: todo_file => 'todo.txt',
30: done_file => undef,
1.5 andrew 31: };
32:
33: if ($options) {
34: if ( ref $options eq 'HASH' ) {
35: foreach my $opt ( keys %{$options} ) {
36: if ( exists $path_of{$ident}{$opt} ) {
37: $self->_path_to( $opt, $options->{$opt} );
38: }
39: else {
1.14 andrew 40:
1.13 andrew 41: #carp "Invalid option [$opt]";
1.5 andrew 42: }
43: }
44: }
45: else {
46: if ( -d $options ) {
47: $self->_path_to( 'todo_dir', $options );
48: }
49: elsif ( $options =~ /\.txt$/ixms ) {
50: $self->_path_to( 'todo_file', $options );
51: }
52: else {
53: carp "Unknown options [$options]";
54: }
55: }
56: }
57:
58: my $file = $self->_path_to('todo_file');
59: if ( defined $file && -e $file ) {
60: $self->load();
61: }
1.2 andrew 62:
63: return $self;
64: }
65:
1.5 andrew 66: sub _path_to {
67: my ( $self, $type, $path ) = @_;
68: my $ident = ident($self);
69:
70: if ( $type eq 'todo_dir' ) {
71: if ($path) {
72: $path_of{$ident}{$type} = $path;
73: }
74: return $path_of{$ident}{$type};
75: }
76:
77: if ($path) {
78: my ( $volume, $directories, $file )
79: = File::Spec->splitpath($path);
80: $path_of{$ident}{$type} = $file;
81:
82: if ($volume) {
83: $directories = File::Spec->catdir( $volume, $directories );
84: }
85:
86: # XXX Should we save complete paths to each file, mebbe only if
87: # the dirs are different?
88: if ($directories) {
89: $path_of{$ident}{todo_dir} = $directories;
90: }
91: }
92:
93: if ( $type =~ /(todo|done|report)_file/xms ) {
94: if ( my ( $pre, $post )
95: = $path_of{$ident}{$type} =~ /^(.*)$1(.*)\.txt$/ixms )
96: {
97: foreach my $f qw( todo done report ) {
98: if ( !defined $path_of{$ident}{ $f . '_file' } ) {
99: $path_of{$ident}{ $f . '_file' }
100: = $pre . $f . $post . '.txt';
101: }
102: }
103: }
104: }
105:
106: if ( defined $path_of{$ident}{todo_dir} ) {
107: return File::Spec->catfile( $path_of{$ident}{todo_dir},
108: $path_of{$ident}{$type} );
109: }
110:
111: return;
112: }
113:
1.3 andrew 114: sub file {
1.2 andrew 115: my ( $self, $file ) = @_;
116: my $ident = ident($self);
117:
1.5 andrew 118: if ( defined $file && exists $path_of{$ident}{$file} ) {
119: $file = $self->_path_to($file);
120: }
121: else {
122: $file = $self->_path_to( 'todo_file', $file );
1.2 andrew 123: }
124:
1.5 andrew 125: return $file;
1.3 andrew 126: }
127:
128: sub load {
129: my ( $self, $file ) = @_;
130: my $ident = ident($self);
131:
1.8 andrew 132: $loaded_of{$ident} = undef;
133:
1.9 andrew 134: $file = $self->file($file);
135:
1.8 andrew 136: if ( $list_of{$ident} = $self->listfile($file) ) {
137: $loaded_of{$ident} = $file;
138: return 1;
139: }
140:
141: return;
142: }
143:
144: sub listfile {
145: my ( $self, $file ) = @_;
146:
1.5 andrew 147: $file = $self->file($file);
148:
149: if ( !defined $file ) {
1.8 andrew 150: carp q{file can't be found};
151: return;
1.5 andrew 152: }
153:
154: if ( !-e $file ) {
1.8 andrew 155: carp "file [$file] does not exist";
1.5 andrew 156: return;
157: }
1.2 andrew 158:
159: my @list;
160: open my $fh, '<', $file or croak "Couldn't open [$file]: $!";
161: while (<$fh>) {
162: s/\r?\n$//xms;
163: push @list, Text::Todo::Entry->new($_);
164: }
165: close $fh or croak "Couldn't close [$file]: $!";
166:
1.8 andrew 167: return wantarray ? @list : \@list;
1.2 andrew 168: }
169:
170: sub save {
171: my ( $self, $file ) = @_;
172: my $ident = ident($self);
173:
1.5 andrew 174: $file = $self->file($file);
175: if ( !defined $file ) {
1.6 andrew 176: croak q{todo file can't be found};
1.5 andrew 177: }
1.2 andrew 178:
179: open my $fh, '>', $file or croak "Couldn't open [$file]: $!";
180: foreach my $e ( @{ $list_of{$ident} } ) {
1.3 andrew 181: print {$fh} $e->text . "\n"
182: or croak "Couldn't print to [$file]: $!";
1.2 andrew 183: }
184: close $fh or croak "Couldn't close [$file]: $!";
185:
1.9 andrew 186: $loaded_of{$ident} = $file;
187:
1.2 andrew 188: return 1;
189: }
190:
191: sub list {
1.3 andrew 192: my ($self) = @_;
1.2 andrew 193: my $ident = ident($self);
1.6 andrew 194:
1.2 andrew 195: return if !$list_of{$ident};
1.6 andrew 196: return wantarray ? @{ $list_of{$ident} } : $list_of{$ident};
1.5 andrew 197: }
198:
199: sub listpri {
1.14 andrew 200: my ( $self, $pri ) = @_;
1.5 andrew 201:
1.14 andrew 202: my @list;
203: if ($pri) {
204: $pri = uc $pri;
205: if ( $pri !~ /^[A-Z]$/xms ) {
1.17 ! andrew 206: croak 'PRIORITY must a single letter from A to Z.';
1.14 andrew 207: }
208: @list = grep { defined $_->priority && $_->priority eq $pri }
209: $self->list;
210: }
211: else {
212: @list = grep { $_->priority } $self->list;
213: }
1.5 andrew 214:
215: return wantarray ? @list : \@list;
1.2 andrew 216: }
1.1 andrew 217:
1.3 andrew 218: sub add {
219: my ( $self, $entry ) = @_;
220: my $ident = ident($self);
221:
1.5 andrew 222: if ( !ref $entry ) {
223: $entry = Text::Todo::Entry->new($entry);
224: }
225: elsif ( ref $entry ne 'Text::Todo::Entry' ) {
226: croak(
227: 'entry is a ' . ref($entry) . ' not a Text::Todo::Entry!' );
228: }
229:
230: push @{ $list_of{$ident} }, $entry;
231:
232: return $entry;
233: }
234:
1.6 andrew 235: sub del {
1.5 andrew 236: my ( $self, $src ) = @_;
237: my $ident = ident($self);
238:
1.6 andrew 239: my $id = $self->_find_entry_id($src);
1.5 andrew 240:
241: my @list = $self->list;
1.6 andrew 242: my $entry = splice @list, $id, 1;
1.5 andrew 243: $list_of{$ident} = \@list;
244:
245: return $entry;
246: }
247:
248: sub move {
249: my ( $self, $entry, $dst ) = @_;
250: my $ident = ident($self);
251:
252: my $src = $self->_find_entry_id($entry);
253: my @list = $self->list;
254:
1.6 andrew 255: splice @list, $dst, 0, splice @list, $src, 1;
1.5 andrew 256:
257: $list_of{$ident} = \@list;
258:
259: return 1;
260: }
261:
1.6 andrew 262: sub listproj {
1.17 ! andrew 263: my ($self) = @_;
1.14 andrew 264: return $self->listtag('project');
265: }
266:
267: sub listcon {
1.17 ! andrew 268: my ($self) = @_;
1.14 andrew 269: return $self->listtag('context');
270: }
271:
272: sub listtag {
273: my ( $self, $tag ) = @_;
1.5 andrew 274: my $ident = ident($self);
1.17 ! andrew 275:
1.14 andrew 276: my $accessor = $tag . 's';
1.5 andrew 277:
1.14 andrew 278: my %available;
1.6 andrew 279: foreach my $e ( $self->list ) {
1.14 andrew 280: foreach my $p ( $e->$accessor ) {
281: $available{$p} = 1;
1.5 andrew 282: }
283: }
284:
1.14 andrew 285: my @tags = sort keys %available;
1.5 andrew 286:
1.17 ! andrew 287: return wantarray ? @tags : \@tags;
1.5 andrew 288: }
289:
1.9 andrew 290: sub archive {
291: my ($self) = @_;
292: my $ident = ident($self);
293:
294: if ( !defined $loaded_of{$ident}
295: || $loaded_of{$ident} ne $self->file('todo_file') )
296: {
297: carp 'todo_file not loaded';
298: return;
299: }
300:
1.12 andrew 301: my $changed = 0;
1.9 andrew 302: ENTRY: foreach my $e ( $self->list ) {
303: if ( $e->done ) {
304: if ( $self->addto( 'done_file', $e ) && $self->del($e) ) {
1.12 andrew 305: $changed++;
1.9 andrew 306: }
307: else {
308: carp q{Couldn't archive entry [} . $e->text . ']';
309: last ENTRY;
310: }
311: }
1.14 andrew 312: elsif ( $e->text eq q{} ) {
313: if ( $self->del($e) ) {
1.12 andrew 314: $changed++;
315: }
316: else {
317: carp q{Couldn't delete blank entry};
318: last ENTRY;
319: }
320: }
1.9 andrew 321: }
322:
1.12 andrew 323: if ($changed) {
1.9 andrew 324: $self->save;
325: }
326:
1.12 andrew 327: return $changed;
1.9 andrew 328: }
1.8 andrew 329:
330: sub addto {
331: my ( $self, $file, $entry ) = @_;
332: my $ident = ident($self);
333:
334: $file = $self->file($file);
335: if ( !defined $file ) {
336: croak q{file can't be found};
337: }
338:
1.9 andrew 339: if ( ref $entry ) {
340: if ( ref $entry eq 'Text::Todo::Entry' ) {
341: $entry = $entry->text;
342: }
343: else {
344: carp 'Unknown ref [' . ref($entry) . ']';
345: return;
346: }
347: }
348:
1.8 andrew 349: open my $fh, '>>', $file or croak "Couldn't open [$file]: $!";
350: print {$fh} $entry, "\n"
351: or croak "Couldn't print to [$file]: $!";
352: close $fh or croak "Couldn't close [$file]: $!";
353:
354: if ( defined $loaded_of{$ident} && $file eq $loaded_of{$ident} ) {
355: return $self->load($file);
356: }
357:
358: return 1;
359: }
1.5 andrew 360:
361: sub _find_entry_id {
362: my ( $self, $entry ) = @_;
363: my $ident = ident($self);
364:
1.3 andrew 365: if ( ref $entry ) {
366: if ( ref $entry ne 'Text::Todo::Entry' ) {
367: croak( 'entry is a '
368: . ref($entry)
369: . ' not a Text::Todo::Entry!' );
370: }
1.5 andrew 371:
372: my @list = $self->list;
373: foreach my $id ( 0 .. $#list ) {
374: if ( $list[$id] eq $entry ) {
375: return $id;
376: }
377: }
1.3 andrew 378: }
1.5 andrew 379: elsif ( $entry =~ /^\d+$/xms ) {
380: return $entry;
1.3 andrew 381: }
382:
1.5 andrew 383: croak "Invalid entry [$entry]!";
1.3 andrew 384: }
1.2 andrew 385: }
1.1 andrew 386:
1.2 andrew 387: 1; # Magic true value required at end of module
1.1 andrew 388: __END__
389:
390: =head1 NAME
391:
1.4 andrew 392: Text::Todo - Perl interface to todo_txt files
1.1 andrew 393:
1.10 andrew 394:
1.6 andrew 395: =head1 VERSION
396:
1.10 andrew 397: Since the $VERSION can't be automatically included,
398: here is the RCS Id instead, you'll have to look up $VERSION.
1.6 andrew 399:
1.17 ! andrew 400: $Id: Todo.pm,v 1.16 2010/01/12 19:40:52 andrew Exp $
1.1 andrew 401:
402: =head1 SYNOPSIS
403:
404: use Text::Todo;
1.10 andrew 405:
406: my $todo = Text::Todo->new('todo/todo.txt');
407:
408: foreach my $e (sort { lc($_->text) cmp lc($e->text)} $todo->list) {
409: print $e->text, "\n";
410: }
411:
1.1 andrew 412:
413: =head1 DESCRIPTION
414:
1.10 andrew 415: This module is a basic interface to the todo.txt files as described by
416: Lifehacker and extended by members of their community.
417:
1.4 andrew 418: For more information see L<http://todotxt.com>
1.1 andrew 419:
1.10 andrew 420: This module supports the 3 axes of an effective todo list.
421: Priority, Project and Context.
422:
423: It does not support other notations or many of the more advanced features of
424: the todo.sh like plugins.
425:
426: It should be extensible, but and hopefully will be before a 1.0 release.
427:
428:
1.1 andrew 429: =head1 INTERFACE
430:
1.2 andrew 431: =head2 new
432:
1.10 andrew 433: new({
434: [ todo_dir => 'directory', ]
435: [ todo_file => 'filename in todo_dir', ]
436: [ done_file => 'filename in todo_dir', ]
437: [ report_file => 'filename in todo_dir', ]
438: });
439:
440: Allows you to set each item individually. todo_file defaults to todo.txt.
441:
442: new('path/to/todo.txt');
443:
444: Automatically sets todo_dir to 'path/to', todo_file to 'todo.txt'
445:
446: new('path/to')
447:
448: If you pass an existing directory to new, it will set todo_dir.
449:
450:
451: If you what you set matches (.*)todo(.*).txt it will automatically set
452: done_file to $1done$2.txt
453: and
454: report_file to $1report$2.txt.
455:
456: For example, new('todo/todo.shopping.txt') will set
457: todo_dir to 'todo',
458: todo_file to 'todo.shopping.txt',
459: done_file to 'done.shopping.txt',
460: and
461: report_file to 'report.shopping.txt'.
462:
1.9 andrew 463: =head2 file
464:
1.10 andrew 465: Allows you to read the paths to the files in use.
466: If as in the SYNOPSIS above you used $todo = new('todo/todo.txt').
467:
468: $todo_file = $todo->file('todo_file');
469:
470: then, $todo_file eq 'todo/todo.txt'
471:
1.2 andrew 472: =head2 load
1.16 andrew 473: - Reads a list from a file into the current object.
1.2 andrew 474:
1.10 andrew 475: Allows you to load a different file into the object.
476:
477: $todo->load('done_file');
478:
479: This effects the other functions that act on the list.
480:
1.2 andrew 481: =head2 save
1.16 andrew 482: - Writes the list to disk.
1.2 andrew 483:
1.10 andrew 484: $todo->save(['new/path/to/todo']);
485:
1.16 andrew 486: Either writes the current working file or the passed in argument
1.10 andrew 487: that can be recognized by file().
488:
489: If you specify a filename it will save to that file and update the paths.
490: Additional changes to the object work on that file.
491:
1.9 andrew 492: =head2 list
1.16 andrew 493: - get the curently loaded list
1.9 andrew 494:
1.10 andrew 495: my @todo_list = $todo->list;
496:
1.16 andrew 497: In list context returns a list, it scalar context returns an array reference to the list.
498:
1.9 andrew 499: =head2 listpri
1.16 andrew 500: - get the list items that are marked priority
1.3 andrew 501:
1.10 andrew 502: Like list, but only returns entries that have priority set.
503:
504: my @priority_list = $todo->listpri;
505:
1.16 andrew 506: Since this is so easy to write as:
507:
508: my @priority_list = grep { $_->priority } $todo->list;
509:
510: I think it may become depreciated unless there is demand.
511:
512: =head2 listtag
513:
514: Returns tags found in the list sorted by name.
1.3 andrew 515:
1.16 andrew 516: If there were projects +GarageSale and +Shopping then
1.10 andrew 517:
1.16 andrew 518: my @projects = $todo->listtag('project');
1.10 andrew 519:
520: is the same as
521:
522: @projects = ( 'GarageSale', 'Shopping' );
1.16 andrew 523:
524: =head2 listcon
525: - Shortcut to listtag('context')
526:
527: =head2 listproj
528: - Shortcut to listtag('project')
1.10 andrew 529:
1.3 andrew 530: =head2 add
1.9 andrew 531:
1.10 andrew 532: Adds a new entry to the list.
533: Can either be a Text::Todo::Entry object or plain text.
534:
535: $todo->add('new todo entry');
536:
537: It then becomes $todo->list->[-1];
538:
1.9 andrew 539: =head2 del
540:
1.10 andrew 541: Remove an entry from the list, either the reference or by number.
542:
543: $removed_entry = $todo->del($entry);
544:
545: $entry can either be an Text::Todo::Entry in the list or the index of the
546: entry to delete.
547:
548: Note that entries are 0 indexed (as expected in perl) not starting at line 1.
549:
1.9 andrew 550: =head2 move
551:
1.10 andrew 552: $todo->move($entry, $new_pos);
553:
554: $entry can either be the number of the entry or the actual entry.
555: $new_pos is the new position to put it.
556:
557: Note that entries are 0 indexed (as expected in perl) not starting at line 1.
558:
1.9 andrew 559: =head2 archive
560:
1.10 andrew 561: $todo->archive
562:
563: Iterates over the list and for each done entry,
564: addto('done_file')
565: and
566: del($entry).
567: If any were archived it will then
568: save()
569: and
570: load().
571:
1.9 andrew 572: =head2 addto
573:
1.10 andrew 574: $todo->addto($file, $entry);
1.1 andrew 575:
1.10 andrew 576: Appends text to the file.
577: $file can be anyting recognized by file().
578: $entry can either be a Text::Todo::Entry or plain text.
1.1 andrew 579:
1.10 andrew 580: =head2 listfile
1.1 andrew 581:
1.10 andrew 582: @list = $todo->listfile($file);
1.1 andrew 583:
1.10 andrew 584: Read a file and returns a list like $todo->list but does not update the
585: internal list that is being worked with.
586: $file can be anyting recognized by file().
1.1 andrew 587:
588:
1.10 andrew 589: =head1 DIAGNOSTICS
1.1 andrew 590:
1.10 andrew 591: Most methods return undef on failure.
1.1 andrew 592:
1.10 andrew 593: Some more important methods are fatal.
1.1 andrew 594:
595:
596: =head1 CONFIGURATION AND ENVIRONMENT
597:
598: Text::Todo requires no configuration files or environment variables.
599:
1.10 andrew 600: Someday it should be able to read and use the todo.sh config file. This may
601: possibly be better done in a client that would use this module.
1.4 andrew 602:
1.1 andrew 603:
604: =head1 DEPENDENCIES
605:
1.10 andrew 606: Class::Std::Utils
607: File::Spec
608: version
1.1 andrew 609:
610:
611: =head1 INCOMPATIBILITIES
612:
613: None reported.
614:
615:
616: =head1 BUGS AND LIMITATIONS
617:
618: No bugs have been reported.
1.11 andrew 619:
620: Limitations:
621:
622: Currently there isn't an easy way to print out line numbers with the entry.
1.1 andrew 623:
624: Please report any bugs or feature requests to
625: C<bug-text-todo@rt.cpan.org>, or through the web interface at
626: L<http://rt.cpan.org>.
627:
628:
629: =head1 AUTHOR
630:
631: Andrew Fresh C<< <andrew@cpan.org> >>
632:
633:
634: =head1 LICENSE AND COPYRIGHT
635:
636: Copyright (c) 2009, Andrew Fresh C<< <andrew@cpan.org> >>. All rights reserved.
637:
638: This module is free software; you can redistribute it and/or
639: modify it under the same terms as Perl itself. See L<perlartistic>.
640:
641:
642: =head1 DISCLAIMER OF WARRANTY
643:
644: BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
645: FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
646: OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
647: PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
648: EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
649: WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
650: ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
651: YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
652: NECESSARY SERVICING, REPAIR, OR CORRECTION.
653:
654: IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
655: WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
656: REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
657: LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
658: OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
659: THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
660: RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
661: FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
662: SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
663: SUCH DAMAGES.
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>