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