Annotation of todotxt/Text-Todo/bin/todo.pl, Revision 1.15
1.1 andrew 1: #!/usr/bin/perl
1.15 ! andrew 2: # $AFresh1: todo.pl,v 1.14 2010/01/11 19:52:06 andrew Exp $
1.1 andrew 3: ########################################################################
4: # todo.pl *** a perl version of todo.sh. Uses Text::Todo.
5: #
6: # 2010.01.07 #*#*# andrew fresh <andrew@cpan.org>
7: ########################################################################
8: # Copyright 2010 Andrew Fresh, all rights reserved
9: #
10: # This program is free software; you can redistribute it and/or modify
11: # it under the same terms as Perl itself.
12: ########################################################################
13: use strict;
14: use warnings;
15:
16: use Data::Dumper;
1.2 andrew 17:
18: use Getopt::Std;
1.1 andrew 19: use Text::Todo;
20:
21: use version; our $VERSION = qv('0.0.1');
22:
1.2 andrew 23: # option defaults
24: my $config_file = $ENV{HOME} . '/todo.cfg';
1.3 andrew 25: CONFIG: foreach my $f ( $config_file, $ENV{HOME} . '/.todo.cfg', ) {
26: if ( -e $f ) {
27: $config_file = $f;
28: last CONFIG;
29: }
30: }
1.2 andrew 31:
32: my %actions = (
1.3 andrew 33: add => \&add,
34: addto => \&addto,
35: append => \&append,
36: archive => \&archive,
37: command => \&command,
38: del => \&del,
39: depri => \&depri,
40: do => \&mark_done,
41: help => \&help,
1.2 andrew 42: list => \&list,
1.3 andrew 43: listall => \&listall,
44: listcon => \&listcon,
45: listfile => \&listfile,
46: listpri => \&listpri,
47: listproj => \&listproj,
48: move => \&move,
49: prepend => \&prepend,
50: pri => \&pri,
51: replace => \&replace,
52: report => \&report,
1.2 andrew 53: );
54:
55: my %aliases = (
56: a => 'add',
57: app => 'append',
58: rm => 'del',
59: dp => 'depri',
60: ls => 'list',
61: lsa => 'listall',
62: lsc => 'listcon',
63: lf => 'listfile',
1.3 andrew 64: lsp => 'listpri',
1.2 andrew 65: lsprj => 'listproj',
66: mv => 'move',
67: prep => 'prepend',
68: p => 'pri',
69: );
70:
71: my %opts;
1.15 ! andrew 72: getopts( q{+d:fhpPntvV@}, \%opts );
1.2 andrew 73:
74: my $action = shift @ARGV;
75: if ( $action && $action eq 'command' ) {
76:
77: # We don't support action scripts so . . .
78: $action = shift @ARGV;
79: }
80: if ( $action && exists $aliases{$action} ) {
81: $action = $aliases{$action};
82: }
83:
84: if ( $opts{h} || !$action ) {
85: usage( $opts{h} );
86: }
87:
1.6 andrew 88: my @unsupported = grep { defined $opts{$_} } qw( @ + f h p P t v V );
1.2 andrew 89: if (@unsupported) {
1.3 andrew 90: warn 'Unsupported options: ' . ( join q{, }, @unsupported ) . "\n";
1.2 andrew 91: }
92:
93: if ( $opts{d} ) {
94: $config_file = $opts{d};
95: }
96:
97: if ( exists $actions{$action} ) {
98: my $config = read_config($config_file);
99: my $action = $actions{$action}->( $config, @ARGV );
100: }
101: else {
102: usage();
103: }
104:
1.3 andrew 105: sub add {
1.7 andrew 106: my ( $config, @entry ) = @_;
107: if ( !@entry ) {
1.3 andrew 108: die "usage: todo.pl add 'item'\n";
109: }
110:
1.7 andrew 111: my $entry = join q{ }, @entry;
112:
1.3 andrew 113: my $todo = Text::Todo->new($config);
114: if ( $todo->add($entry) ) {
115: my @list = $todo->list;
116: my $lines = scalar @list;
117:
118: print "TODO: '$entry' added on line $lines\n";
119:
120: return $lines;
121: }
122: die "Unable to add [$entry]\n";
123: }
124:
125: sub addto {
1.7 andrew 126: my ( $config, $file, @entry ) = @_;
127: if ( !( $file && @entry ) ) {
1.3 andrew 128: die "usage: todo.pl addto DEST 'TODO ITEM'\n";
129: }
130:
1.7 andrew 131: my $entry = join q{ }, @entry;
132:
1.3 andrew 133: my $todo = Text::Todo->new($config);
134:
135: $file = $todo->file($file);
136: if ( $todo->addto( $file, $entry ) ) {
137: my @list = $todo->listfile($file);
138: my $lines = scalar @list;
139:
140: print "TODO: '$entry' added to $file on line $lines\n";
141:
142: return $lines;
143: }
144: die "Unable to add [$entry]\n";
145: }
146:
1.4 andrew 147: sub append {
1.9 andrew 148: my ( $config, $line, @text ) = @_;
1.7 andrew 149: if ( !( $line && @text && $line =~ /^\d+$/xms ) ) {
1.4 andrew 150: die 'usage: todo.pl append ITEM# "TEXT TO APPEND"' . "\n";
151: }
1.7 andrew 152:
153: my $text = join q{ }, @text;
1.4 andrew 154:
155: my $todo = Text::Todo->new($config);
156: my $entry = $todo->list->[ $line - 1 ];
157:
158: if ( $entry->append($text) && $todo->save ) {
159: return printf "%02d: %s\n", $line, $entry->text;
160: }
161: die "Unable to append\n";
162: }
163:
1.9 andrew 164: sub archive {
165: my ($config) = @_;
1.5 andrew 166: my $todo = Text::Todo->new($config);
1.9 andrew 167:
1.5 andrew 168: my $file = $todo->file;
169:
170: my $archived = $todo->archive;
1.9 andrew 171: if ( defined $archived ) {
1.5 andrew 172: return print "TODO: $file archived.\n";
173: }
174: die "Unable to archive $file\n";
175: }
176:
1.15 ! andrew 177: ## no critic 'sigal'
1.9 andrew 178: sub command { return &unsupported }
1.15 ! andrew 179: ## use critic
1.6 andrew 180:
1.9 andrew 181: sub del {
1.6 andrew 182: my ( $config, $line ) = @_;
183: if ( !( $line && $line =~ /^\d+$/xms ) ) {
184: die 'usage: todo.pl del ITEM#' . "\n";
185: }
186: my $todo = Text::Todo->new($config);
1.9 andrew 187:
1.10 andrew 188: my $entry = $todo->list->[ $line - 1 ];
1.15 ! andrew 189: print 'Delete \'', $entry->text . "'? (y/n)\n";
1.6 andrew 190: warn "XXX No delete confirmation currently!\n";
191:
1.9 andrew 192: if ( $opts{n} ) {
193: if ( $todo->del($entry) && $todo->save ) {
1.6 andrew 194: return print 'TODO: \'', $entry->text, "' deleted.\n";
195: }
196: }
197: else {
198: my $text = $entry->text;
1.9 andrew 199: if ( $entry->replace(q{}) && $todo->save ) {
1.6 andrew 200: return print 'TODO: \'', $text, "' deleted.\n";
201: }
202: }
203:
204: die "Unable to delete entry\n";
205: }
206:
1.9 andrew 207: sub depri {
208: my ( $config, $line ) = @_;
209: if ( !( $line && $line =~ /^\d+$/xms ) ) {
1.11 andrew 210: die 'usage: todo.pl depri ITEM#' . "\n";
1.9 andrew 211: }
212: my $todo = Text::Todo->new($config);
213:
214: my $entry = $todo->list->[ $line - 1 ];
215: if ( $entry->depri && $todo->save ) {
216: return print $line, ': ', $entry->text, "\n",
217: 'TODO: ', $line, " deprioritized.\n";
218: }
219: die "Unable to deprioritize entry\n";
220: }
221:
1.12 andrew 222: # since "do" is reserved
1.15 ! andrew 223: sub mark_done {
1.12 andrew 224: my ( $config, $line ) = @_;
225: if ( !( $line && $line =~ /^\d+$/xms ) ) {
226: die 'usage: todo.pl del ITEM#' . "\n";
227: }
228: my $todo = Text::Todo->new($config);
229:
230: my $entry = $todo->list->[ $line - 1 ];
231:
232: if ( $entry->do && $todo->save ) {
233: my $status = print $line, ': ', $entry->text, "\n",
234: 'TODO: ', $line, " marked as done.\n";
1.15 ! andrew 235: if ( !$opts{a} ) {
1.12 andrew 236: return archive($config);
237: }
238: return $status;
239: }
240: die "Unable to mark as done\n";
241: }
242:
1.15 ! andrew 243: ## no critic 'sigal'
! 244: sub help { return &unsupported }
! 245: ## use critic
1.3 andrew 246:
1.2 andrew 247: sub list {
1.3 andrew 248: my ( $config, $term ) = @_;
249: my $todo = Text::Todo->new($config);
250:
251: my @list = _number_list( $todo->list );
252: my $shown = _show_sorted_list( $term, @list );
253:
254: return _show_list_footer( $shown, scalar @list, $config->{todo_file} );
255: }
256:
257: sub listall {
258: my ( $config, $term ) = @_;
259: my $todo = Text::Todo->new($config);
260:
261: my @list = _number_list(
262: $todo->listfile('todo_file'),
263: $todo->listfile('done_file'),
264: );
265: my $shown = _show_sorted_list( $term, @list );
266:
267: return _show_list_footer( $shown, scalar @list, $config->{'todo_dir'} );
268: }
269:
270: sub listcon {
1.2 andrew 271: my ($config) = @_;
272: my $todo = Text::Todo->new($config);
1.3 andrew 273: return print map {"\@$_\n"} $todo->listcon;
274: }
1.2 andrew 275:
1.3 andrew 276: sub listfile {
277: my ( $config, $file, $term ) = @_;
278: if ( !$file ) {
279: die "usage: todo.pl listfile SRC [TERM]\n";
1.2 andrew 280: }
1.3 andrew 281: my $todo = Text::Todo->new($config);
282:
283: my @list = _number_list( $todo->listfile($file) );
284: my $shown = _show_sorted_list( $term, @list );
285:
286: return _show_list_footer( $shown, scalar @list, $file );
287: }
288:
289: sub listpri {
290: my ( $config, $pri ) = @_;
291:
292: my $todo = Text::Todo->new($config);
293:
294: my @list = _number_list( $todo->listfile('todo_file') );
295: my @pri_list;
296: if ($pri) {
297: $pri = uc $pri;
298: if ( $pri !~ /^[A-Z]$/xms ) {
299: die "usage: todo.pl listpri PRIORITY\n",
300: "note: PRIORITY must a single letter from A to Z.\n";
301: }
302: @pri_list = grep {
303: defined $_->{entry}->priority
304: && $_->{entry}->priority eq $pri
305: } @list;
306: }
307: else {
308: @pri_list = grep { $_->{entry}->priority } @list;
309: }
310:
311: my $shown = _show_sorted_list( undef, @pri_list );
312:
313: return _show_list_footer( $shown, scalar @list, $config->{todo_file} );
314: }
315:
316: sub listproj {
317: my ($config) = @_;
318: my $todo = Text::Todo->new($config);
319: return print map {"\+$_\n"} $todo->listproj;
320: }
321:
1.15 ! andrew 322: ## no critic 'sigal'
1.10 andrew 323: sub move { return &unsupported }
1.15 ! andrew 324: ## use critic
1.8 andrew 325:
326: sub prepend {
1.10 andrew 327: my ( $config, $line, @text ) = @_;
1.8 andrew 328: if ( !( $line && @text && $line =~ /^\d+$/xms ) ) {
1.9 andrew 329: die 'usage: todo.pl prepend ITEM# "TEXT TO PREPEND"' . "\n";
1.8 andrew 330: }
331:
332: my $text = join q{ }, @text;
333:
334: my $todo = Text::Todo->new($config);
335: my $entry = $todo->list->[ $line - 1 ];
336:
337: if ( $entry->prepend($text) && $todo->save ) {
338: return printf "%02d: %s\n", $line, $entry->text;
339: }
1.9 andrew 340: die "Unable to prepend\n";
1.8 andrew 341: }
342:
1.11 andrew 343: sub pri {
344: my ( $config, $line, $priority ) = @_;
345: my $error = 'usage: todo.pl pri ITEM# PRIORITY';
346: if ( !( $line && $line =~ /^\d+$/xms && $priority ) ) {
1.15 ! andrew 347: die "$error\n";
1.11 andrew 348: }
1.15 ! andrew 349: elsif ( $priority !~ /^[A-Z]$/xms ) {
! 350: $error .= "\n" . 'note: PRIORITY must a single letter from A to Z.';
! 351: die "$error\n";
1.11 andrew 352: }
353:
354: my $todo = Text::Todo->new($config);
355:
356: my $entry = $todo->list->[ $line - 1 ];
357: if ( $entry->pri($priority) && $todo->save ) {
358: return print $line, ': ', $entry->text, "\n",
359: 'TODO: ', $line, ' prioritized (', $entry->priority, ").\n";
360: }
361: die "Unable to prioritize entry\n";
362: }
363:
1.15 ! andrew 364: ## no critic 'sigal'
1.3 andrew 365: sub replace { return &unsupported }
366: sub report { return &unsupported }
1.15 ! andrew 367: ## use critic
1.3 andrew 368:
369: sub _number_list {
370: my (@list) = @_;
371:
372: my $line = 1;
373: return map { { line => $line++, entry => $_, } } @list;
374: }
375:
376: sub _show_sorted_list {
377: my ( $term, @list ) = @_;
1.15 ! andrew 378: $term = defined $term ? quotemeta($term) : q{};
1.3 andrew 379:
380: my $shown = 0;
1.15 ! andrew 381: my @sorted = map { sprintf '%02d %s', $_->{line}, $_->{entry}->text }
1.4 andrew 382: sort { lc $a->{entry}->text cmp lc $b->{entry}->text } @list;
383:
384: foreach my $line ( grep {/$term/xms} @sorted ) {
1.15 ! andrew 385: print "$line\n";
1.3 andrew 386: $shown++;
387: }
388:
389: return $shown;
390: }
391:
392: sub _show_list_footer {
393: my ( $shown, $total, $file ) = @_;
394:
395: $shown ||= 0;
396: $total ||= 0;
397:
398: print "-- \n";
399: print "TODO: $shown of $total tasks shown from $file\n";
400:
401: return 1;
1.2 andrew 402: }
403:
404: sub unsupported { die "Unsupported action\n" }
405:
406: sub usage {
407: my ($long) = @_;
408:
409: print <<'EOL';
410: * command list taken from todo.sh for compatibility
411: Usage: todo.pl [-fhpantvV] [-d todo_config] action
412: EOL
413:
414: if ($long) {
415: print <<'EOL';
1.3 andrew 416:
1.2 andrew 417: Actions:
418: add|a "THING I NEED TO DO +project @context"
419: addto DEST "TEXT TO ADD"
420: append|app NUMBER "TEXT TO APPEND"
421: archive
422: command [ACTIONS]
423: del|rm NUMBER [TERM]
424: dp|depri NUMBER
425: do NUMBER
426: help
427: list|ls [TERM...]
428: listall|lsa [TERM...]
429: listcon|lsc
430: listfile|lf SRC [TERM...]
431: listpri|lsp [PRIORITY]
432: listproj|lsprj
433: move|mv NUMBER DEST [SRC]
434: prepend|prep NUMBER "TEXT TO PREPEND"
435: pri|p NUMBER PRIORITY
436: replace NUMBER "UPDATED TODO"
437: report
438: EOL
439: }
440: else {
441: print <<'EOL';
442: Try 'todo.pl -h' for more information.
443: EOL
444: }
445:
446: exit;
447: }
448:
449: sub read_config {
450: my ($file) = @_;
451:
452: my %config;
1.15 ! andrew 453: open my $fh, '<', $file or die "Unable to open [$file] : $!\n";
1.2 andrew 454: LINE: while (<$fh>) {
1.15 ! andrew 455: _parse_line( $_, \%config );
1.2 andrew 456: }
1.15 ! andrew 457: close $fh or die "Unable to close [$file]: $!\n";
1.1 andrew 458:
1.2 andrew 459: my %lc_config;
460: foreach my $k ( keys %config ) {
1.15 ! andrew 461: $lc_config{ lc $k } = $config{$k};
1.2 andrew 462: }
1.1 andrew 463:
1.2 andrew 464: return \%lc_config;
1.1 andrew 465: }
1.13 andrew 466:
1.15 ! andrew 467: sub _parse_line {
! 468: my ( $line, $config ) = @_;
! 469:
! 470: $line =~ s/\r?\n$//xms;
! 471: $line =~ s/\s*\#.*$//xms;
! 472: return if !$line;
! 473:
! 474: if (s/^\s*export\s+//xms) {
! 475: my ( $key, $value ) = /^([^=]+)\s*=\s*"?(.*?)"?\s*$/xms;
! 476: if ($key) {
! 477: foreach my $k ( keys %config ) {
! 478: $value =~ s/\$\Q$k\E/$config{$k}/gxms;
! 479: $value =~ s/\${\Q$k\E}/$config{$k}/gxms;
! 480: }
! 481: foreach my $k ( keys %ENV ) {
! 482: $value =~ s/\$\Q$k\E/$ENV{$k}/gxms;
! 483: $value =~ s/\${\Q$k\E}/$ENV{$k}/gxms;
! 484: }
! 485: $value =~ s/\$\w+//gxms;
! 486: $value =~ s/\${\w+}//gxms;
! 487:
! 488: $config->{$key} = $value;
! 489: }
! 490: }
! 491:
! 492: return 1;
! 493: }
! 494:
1.13 andrew 495: __END__
496:
497: =head1 NAME
498:
499: todo.pl - a perl replacement for todo.sh
500:
501:
502: =head1 VERSION
503:
504: Since the $VERSION can't be automatically included,
505: here is the RCS Id instead, you'll have to look up $VERSION.
506:
1.15 ! andrew 507: $Id: todo.pl,v 1.14 2010/01/11 19:52:06 andrew Exp $
1.13 andrew 508:
509:
510: =head1 SYNOPSIS
511:
512: todo.pl list
513:
514: todo.pl -h
515:
516: =head1 DESCRIPTION
517:
518: Mostly compatible with todo.sh but not completely.
519: Any differences are either noted under limitations is a bug.
1.14 andrew 520:
521: Ideally todo.pl should pass all the todo.sh tests.
1.13 andrew 522:
523: This is a proof of concept to get the Text::Todo modules used.
524:
525: The modules are there to give more access to my todo.txt file from more
526: places. My goal is a web API for a web interface and then a WebOS version for
527: my Palm Pre.
528:
529: For more information see L<http://todotxt.com>
530:
1.15 ! andrew 531: =head1 USAGE
! 532:
! 533: See todo.pl -h
! 534:
! 535: =head1 OPTIONS
! 536:
! 537: See todo.pl -h
! 538:
! 539: =head1 REQUIRED ARGUMENTS
! 540:
! 541: See todo.pl -h
1.13 andrew 542:
543: =head1 CONFIGURATION AND ENVIRONMENT
544:
545: todo.pl should read the todo.cfg file that todo.sh uses. It is a very
546: simplistic reader and would probably be easy to break.
547:
548: It only uses TODO_DIR, TODO_FILE and DONE_DIR
549:
550: It does not currently support any of the environment variables that todo.sh
551: uses.
552:
1.15 ! andrew 553: =head1 DIAGNOSTICS
1.13 andrew 554:
555: =head1 DEPENDENCIES
556:
557: Perl Modules:
558:
559: =over
560:
561: =item Text::Todo
562:
563: =item version
564:
565: =back
566:
567:
568: =head1 INCOMPATIBILITIES
569:
570: Text::Todo::Entry actually checks if the entry is done before marking it
571: complete again.
572:
573: Text::Todo::Entry will keep the completed marker and then the priority at the
574: beginning of the line in that order.
575:
576:
577: =head1 BUGS AND LIMITATIONS
578:
579: No bugs have been reported.
580:
581: Known limitations:
582:
583: Does not support some command line arguments.
584: @, +, f, h, p, P, t, v or V.
585:
586: Does not yet support some actions. Specifically, command, help and report.
587:
588: Does not colorize output.
589:
590:
591: =head1 AUTHOR
592:
593: Andrew Fresh C<< <andrew@cpan.org> >>
594:
595:
596: =head1 LICENSE AND COPYRIGHT
597:
598: Copyright (c) 2009, Andrew Fresh C<< <andrew@cpan.org> >>. All rights reserved.
599:
600: This module is free software; you can redistribute it and/or
601: modify it under the same terms as Perl itself. See L<perlartistic>.
602:
603:
604: =head1 DISCLAIMER OF WARRANTY
605:
606: BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
607: FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
608: OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
609: PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
610: EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
611: WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
612: ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
613: YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
614: NECESSARY SERVICING, REPAIR, OR CORRECTION.
615:
616: IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
617: WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
618: REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
619: LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
620: OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
621: THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
622: RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
623: FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
624: SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
625: SUCH DAMAGES.
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>