Annotation of todotxt/Text-Todo/lib/Text/Todo.pm, Revision 1.10
1.1 andrew 1: package Text::Todo;
2:
1.10 ! andrew 3: # $RedRiver: Todo.pm,v 1.9 2010/01/09 06:54:15 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} = {
28: todo_dir => undef,
29: todo_file => 'todo.txt',
30: done_file => undef,
31: report_file => undef,
32: };
33:
34: if ($options) {
35: if ( ref $options eq 'HASH' ) {
36: foreach my $opt ( keys %{$options} ) {
37: if ( exists $path_of{$ident}{$opt} ) {
38: $self->_path_to( $opt, $options->{$opt} );
39: }
40: else {
41: carp "Invalid option [$opt]";
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;
1.8 andrew 163: next if !length $_;
1.2 andrew 164: push @list, Text::Todo::Entry->new($_);
165: }
166: close $fh or croak "Couldn't close [$file]: $!";
167:
1.8 andrew 168: return wantarray ? @list : \@list;
1.2 andrew 169: }
170:
171: sub save {
172: my ( $self, $file ) = @_;
173: my $ident = ident($self);
174:
1.5 andrew 175: $file = $self->file($file);
176: if ( !defined $file ) {
1.6 andrew 177: croak q{todo file can't be found};
1.5 andrew 178: }
1.2 andrew 179:
180: open my $fh, '>', $file or croak "Couldn't open [$file]: $!";
181: foreach my $e ( @{ $list_of{$ident} } ) {
1.3 andrew 182: print {$fh} $e->text . "\n"
183: or croak "Couldn't print to [$file]: $!";
1.2 andrew 184: }
185: close $fh or croak "Couldn't close [$file]: $!";
186:
1.9 andrew 187: $loaded_of{$ident} = $file;
188:
1.2 andrew 189: return 1;
190: }
191:
192: sub list {
1.3 andrew 193: my ($self) = @_;
1.2 andrew 194: my $ident = ident($self);
1.6 andrew 195:
1.2 andrew 196: return if !$list_of{$ident};
1.6 andrew 197: return wantarray ? @{ $list_of{$ident} } : $list_of{$ident};
1.5 andrew 198: }
199:
200: sub listpri {
201: my ($self) = @_;
202:
203: my @list = grep { $_->priority } $self->list;
204:
205: return wantarray ? @list : \@list;
1.2 andrew 206: }
1.1 andrew 207:
1.3 andrew 208: sub add {
209: my ( $self, $entry ) = @_;
210: my $ident = ident($self);
211:
1.5 andrew 212: if ( !ref $entry ) {
213: $entry = Text::Todo::Entry->new($entry);
214: }
215: elsif ( ref $entry ne 'Text::Todo::Entry' ) {
216: croak(
217: 'entry is a ' . ref($entry) . ' not a Text::Todo::Entry!' );
218: }
219:
220: push @{ $list_of{$ident} }, $entry;
221:
222: return $entry;
223: }
224:
1.6 andrew 225: sub del {
1.5 andrew 226: my ( $self, $src ) = @_;
227: my $ident = ident($self);
228:
1.6 andrew 229: my $id = $self->_find_entry_id($src);
1.5 andrew 230:
231: my @list = $self->list;
1.6 andrew 232: my $entry = splice @list, $id, 1;
1.5 andrew 233: $list_of{$ident} = \@list;
234:
235: return $entry;
236: }
237:
238: sub move {
239: my ( $self, $entry, $dst ) = @_;
240: my $ident = ident($self);
241:
242: my $src = $self->_find_entry_id($entry);
243: my @list = $self->list;
244:
1.6 andrew 245: splice @list, $dst, 0, splice @list, $src, 1;
1.5 andrew 246:
247: $list_of{$ident} = \@list;
248:
249: return 1;
250: }
251:
1.6 andrew 252: sub listproj {
1.5 andrew 253: my ( $self, $entry, $dst ) = @_;
254: my $ident = ident($self);
255:
256: my %available_projects;
1.6 andrew 257: foreach my $e ( $self->list ) {
1.5 andrew 258: foreach my $p ( $e->projects ) {
259: $available_projects{$p} = 1;
260: }
261: }
262:
263: my @projects = sort keys %available_projects;
264:
265: return wantarray ? @projects : \@projects;
266: }
267:
1.9 andrew 268: sub archive {
269: my ($self) = @_;
270: my $ident = ident($self);
271:
272: if ( !defined $loaded_of{$ident}
273: || $loaded_of{$ident} ne $self->file('todo_file') )
274: {
275: carp 'todo_file not loaded';
276: return;
277: }
278:
279: my $archived = 0;
280: ENTRY: foreach my $e ( $self->list ) {
281: if ( $e->done ) {
282: if ( $self->addto( 'done_file', $e ) && $self->del($e) ) {
283: $archived++;
284: }
285: else {
286: carp q{Couldn't archive entry [} . $e->text . ']';
287: last ENTRY;
288: }
289: }
290: }
291:
292: if ($archived) {
293: $self->save;
294: }
295:
296: return $archived;
297: }
1.8 andrew 298:
299: sub addto {
300: my ( $self, $file, $entry ) = @_;
301: my $ident = ident($self);
302:
303: $file = $self->file($file);
304: if ( !defined $file ) {
305: croak q{file can't be found};
306: }
307:
1.9 andrew 308: if ( ref $entry ) {
309: if ( ref $entry eq 'Text::Todo::Entry' ) {
310: $entry = $entry->text;
311: }
312: else {
313: carp 'Unknown ref [' . ref($entry) . ']';
314: return;
315: }
316: }
317:
1.8 andrew 318: open my $fh, '>>', $file or croak "Couldn't open [$file]: $!";
319: print {$fh} $entry, "\n"
320: or croak "Couldn't print to [$file]: $!";
321: close $fh or croak "Couldn't close [$file]: $!";
322:
323: if ( defined $loaded_of{$ident} && $file eq $loaded_of{$ident} ) {
324: return $self->load($file);
325: }
326:
327: return 1;
328: }
1.5 andrew 329:
330: sub _find_entry_id {
331: my ( $self, $entry ) = @_;
332: my $ident = ident($self);
333:
1.3 andrew 334: if ( ref $entry ) {
335: if ( ref $entry ne 'Text::Todo::Entry' ) {
336: croak( 'entry is a '
337: . ref($entry)
338: . ' not a Text::Todo::Entry!' );
339: }
1.5 andrew 340:
341: my @list = $self->list;
342: foreach my $id ( 0 .. $#list ) {
343: if ( $list[$id] eq $entry ) {
344: return $id;
345: }
346: }
1.3 andrew 347: }
1.5 andrew 348: elsif ( $entry =~ /^\d+$/xms ) {
349: return $entry;
1.3 andrew 350: }
351:
1.5 andrew 352: croak "Invalid entry [$entry]!";
1.3 andrew 353: }
1.2 andrew 354: }
1.1 andrew 355:
1.2 andrew 356: 1; # Magic true value required at end of module
1.1 andrew 357: __END__
358:
359: =head1 NAME
360:
1.4 andrew 361: Text::Todo - Perl interface to todo_txt files
1.1 andrew 362:
1.10 ! andrew 363:
1.6 andrew 364: =head1 VERSION
365:
1.10 ! andrew 366: Since the $VERSION can't be automatically included,
! 367: here is the RCS Id instead, you'll have to look up $VERSION.
1.6 andrew 368:
1.10 ! andrew 369: $Id: Todo.pm,v 1.9 2010/01/09 06:54:15 andrew Exp $
1.1 andrew 370:
371: =head1 SYNOPSIS
372:
373: use Text::Todo;
1.10 ! andrew 374:
! 375: my $todo = Text::Todo->new('todo/todo.txt');
! 376:
! 377: foreach my $e (sort { lc($_->text) cmp lc($e->text)} $todo->list) {
! 378: print $e->text, "\n";
! 379: }
! 380:
1.1 andrew 381:
382: =head1 DESCRIPTION
383:
1.10 ! andrew 384: This module is a basic interface to the todo.txt files as described by
! 385: Lifehacker and extended by members of their community.
! 386:
1.4 andrew 387: For more information see L<http://todotxt.com>
1.1 andrew 388:
1.10 ! andrew 389: This module supports the 3 axes of an effective todo list.
! 390: Priority, Project and Context.
! 391:
! 392: It does not support other notations or many of the more advanced features of
! 393: the todo.sh like plugins.
! 394:
! 395: It should be extensible, but and hopefully will be before a 1.0 release.
! 396:
! 397:
1.1 andrew 398: =head1 INTERFACE
399:
1.2 andrew 400: =head2 new
401:
1.10 ! andrew 402: new({
! 403: [ todo_dir => 'directory', ]
! 404: [ todo_file => 'filename in todo_dir', ]
! 405: [ done_file => 'filename in todo_dir', ]
! 406: [ report_file => 'filename in todo_dir', ]
! 407: });
! 408:
! 409: Allows you to set each item individually. todo_file defaults to todo.txt.
! 410:
! 411: new('path/to/todo.txt');
! 412:
! 413: Automatically sets todo_dir to 'path/to', todo_file to 'todo.txt'
! 414:
! 415: new('path/to')
! 416:
! 417: If you pass an existing directory to new, it will set todo_dir.
! 418:
! 419:
! 420: If you what you set matches (.*)todo(.*).txt it will automatically set
! 421: done_file to $1done$2.txt
! 422: and
! 423: report_file to $1report$2.txt.
! 424:
! 425: For example, new('todo/todo.shopping.txt') will set
! 426: todo_dir to 'todo',
! 427: todo_file to 'todo.shopping.txt',
! 428: done_file to 'done.shopping.txt',
! 429: and
! 430: report_file to 'report.shopping.txt'.
! 431:
1.9 andrew 432: =head2 file
433:
1.10 ! andrew 434: Allows you to read the paths to the files in use.
! 435: If as in the SYNOPSIS above you used $todo = new('todo/todo.txt').
! 436:
! 437: $todo_file = $todo->file('todo_file');
! 438:
! 439: then, $todo_file eq 'todo/todo.txt'
! 440:
1.2 andrew 441: =head2 load
442:
1.10 ! andrew 443: Allows you to load a different file into the object.
! 444:
! 445: $todo->load('done_file');
! 446:
! 447: This effects the other functions that act on the list.
! 448:
1.2 andrew 449: =head2 save
450:
1.10 ! andrew 451: $todo->save(['new/path/to/todo']);
! 452:
! 453: Writes the list to the file. Either the current working file or something
! 454: that can be recognized by file().
! 455:
! 456: If you specify a filename it will save to that file and update the paths.
! 457: Additional changes to the object work on that file.
! 458:
1.9 andrew 459: =head2 list
460:
1.10 ! andrew 461: my @todo_list = $todo->list;
! 462:
1.9 andrew 463: =head2 listpri
1.3 andrew 464:
1.10 ! andrew 465: Like list, but only returns entries that have priority set.
! 466:
! 467: my @priority_list = $todo->listpri;
! 468:
1.9 andrew 469: =head2 listproj
1.3 andrew 470:
1.10 ! andrew 471: Returns projects in the list sorted by name.
! 472: If there were projects +GarageSale and +Shopping
! 473:
! 474: my @projects = $todo->listproj;
! 475:
! 476: is the same as
! 477:
! 478: @projects = ( 'GarageSale', 'Shopping' );
! 479:
1.3 andrew 480: =head2 add
1.9 andrew 481:
1.10 ! andrew 482: Adds a new entry to the list.
! 483: Can either be a Text::Todo::Entry object or plain text.
! 484:
! 485: $todo->add('new todo entry');
! 486:
! 487: It then becomes $todo->list->[-1];
! 488:
1.9 andrew 489: =head2 del
490:
1.10 ! andrew 491: Remove an entry from the list, either the reference or by number.
! 492:
! 493: $removed_entry = $todo->del($entry);
! 494:
! 495: $entry can either be an Text::Todo::Entry in the list or the index of the
! 496: entry to delete.
! 497:
! 498: Note that entries are 0 indexed (as expected in perl) not starting at line 1.
! 499:
1.9 andrew 500: =head2 move
501:
1.10 ! andrew 502: $todo->move($entry, $new_pos);
! 503:
! 504: $entry can either be the number of the entry or the actual entry.
! 505: $new_pos is the new position to put it.
! 506:
! 507: Note that entries are 0 indexed (as expected in perl) not starting at line 1.
! 508:
1.9 andrew 509: =head2 archive
510:
1.10 ! andrew 511: $todo->archive
! 512:
! 513: Iterates over the list and for each done entry,
! 514: addto('done_file')
! 515: and
! 516: del($entry).
! 517: If any were archived it will then
! 518: save()
! 519: and
! 520: load().
! 521:
1.9 andrew 522: =head2 addto
523:
1.10 ! andrew 524: $todo->addto($file, $entry);
1.1 andrew 525:
1.10 ! andrew 526: Appends text to the file.
! 527: $file can be anyting recognized by file().
! 528: $entry can either be a Text::Todo::Entry or plain text.
1.1 andrew 529:
1.10 ! andrew 530: =head2 listfile
1.1 andrew 531:
1.10 ! andrew 532: @list = $todo->listfile($file);
1.1 andrew 533:
1.10 ! andrew 534: Read a file and returns a list like $todo->list but does not update the
! 535: internal list that is being worked with.
! 536: $file can be anyting recognized by file().
1.1 andrew 537:
538:
1.10 ! andrew 539: =head1 DIAGNOSTICS
1.1 andrew 540:
1.10 ! andrew 541: Most methods return undef on failure.
1.1 andrew 542:
1.10 ! andrew 543: Some more important methods are fatal.
1.1 andrew 544:
545:
546: =head1 CONFIGURATION AND ENVIRONMENT
547:
548: Text::Todo requires no configuration files or environment variables.
549:
1.10 ! andrew 550: Someday it should be able to read and use the todo.sh config file. This may
! 551: possibly be better done in a client that would use this module.
1.4 andrew 552:
1.1 andrew 553:
554: =head1 DEPENDENCIES
555:
1.10 ! andrew 556: Class::Std::Utils
! 557: File::Spec
! 558: version
1.1 andrew 559:
560:
561: =head1 INCOMPATIBILITIES
562:
563: None reported.
564:
565:
566: =head1 BUGS AND LIMITATIONS
567:
568: No bugs have been reported.
569:
570: Please report any bugs or feature requests to
571: C<bug-text-todo@rt.cpan.org>, or through the web interface at
572: L<http://rt.cpan.org>.
573:
574:
575: =head1 AUTHOR
576:
577: Andrew Fresh C<< <andrew@cpan.org> >>
578:
579:
580: =head1 LICENSE AND COPYRIGHT
581:
582: Copyright (c) 2009, Andrew Fresh C<< <andrew@cpan.org> >>. All rights reserved.
583:
584: This module is free software; you can redistribute it and/or
585: modify it under the same terms as Perl itself. See L<perlartistic>.
586:
587:
588: =head1 DISCLAIMER OF WARRANTY
589:
590: BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
591: FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
592: OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
593: PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
594: EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
595: WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
596: ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
597: YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
598: NECESSARY SERVICING, REPAIR, OR CORRECTION.
599:
600: IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601: WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
602: REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
603: LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
604: OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
605: THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
606: RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
607: FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
608: SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
609: SUCH DAMAGES.
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>