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