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