[BACK]Return to Todo.pm CVS log [TXT][DIR] Up to [local] / todotxt / Text-Todo / lib / Text

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>