added Changelog.
[gtdo.git] / gtdo.pl
blobc2cea2eccd33d16550efcabf185d053fc7cb6f0a
1 #!/usr/bin/perl -w
2 use strict;
4 #### Copyright 2008, Devendra Gera <gera@theoldmonk.net>,
5 #### All rights reserved.
6 ####
7 #### This is Free Software, released under the terms of the GNU General Public
8 #### License, version 2. A copy of the license can be obtained by emailing the
9 #### author, or from http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
10 ####
11 #### As noted in the License, this software does not come with any warranty,
12 #### explicit or implied. This program might, and would be buggy. Use it at
13 #### your own risk.
15 use Pod::Usage;
16 use Term::ReadLine;
17 use Fcntl qw(:flock);
19 my $todo_file = "$ENV{HOME}/todo/todo.txt";
20 my $done_file = "$ENV{HOME}/todo/done.txt";
21 my $not_done_file = "$ENV{HOME}/todo/not_done.txt";
23 my %dispatch = (
24 add => \&add_task,
25 ls => \&list_tasks,
26 do => \&do_task,
27 contexts => \&list_contexts,
28 groups => \&list_groups,
29 lsall => \&listall,
30 del => \&delete_task,
31 change => \&change_task,
33 my @tasks;
35 my $command = shift || "ls";
36 pod2usage()
37 unless ( (defined $command) && (exists $dispatch{$command}) );
39 read_tasks();
40 $dispatch{$command}->(@ARGV);
42 exit(0);
44 sub add_task
46 my $task = join (" ", @_);
47 quit("") unless $task;
49 # find out the group of the task
50 my (undef, $group) = ( $task =~ m{(^|\s)(/\S*)(\s|$)} );
51 $group |= "";
52 my ($parent, $number) = ( $group =~ /(.*?)\.(\d+)/ );
54 my $target_offset = @tasks;
56 # insert properly if its a subtask
57 if(defined $parent) {
59 # also remember to strip the number
60 $task =~ s/$parent\.$number/$parent/;
62 my @candidate_ids = grep_pos_in_tasks($parent, ());
63 my $length = @candidate_ids;
65 if( $number <= $length ) {
66 $target_offset = $candidate_ids[$number - 1] - 1;
70 splice @tasks, $target_offset, 0, ($task);
72 write_tasks();
75 sub list_tasks
77 my @patterns = @_;
78 @patterns = ("") unless @patterns;
79 my $is_a_group = 0;
81 my @candidates = ();
82 foreach my $pattern ( @patterns ) {
83 $is_a_group = 1 if( $pattern =~ m{^/} );
84 @candidates = grep_pos_in_tasks( $pattern, @candidates );
87 # list tasks only if they are the first task from their group.
88 my %out;
89 my $i = 0;
90 foreach my $id ( @candidates ) {
91 unless ($is_a_group) {
92 my (undef,$group) = ( $tasks[$id - 1] =~ m{(^|\s)(/\S*)(\s|$)} );
93 if(defined $group) {
94 my $first = (grep_pos_in_tasks( $group, () ))[0];
95 $out{$group} = $id if ($id == $first);
96 next;
99 $out{$i++} = $id;
102 print_tasks( values %out );
105 sub listall
107 my @patterns = @_;
108 return print_tasks( (1 .. scalar(@tasks)) )
109 unless @patterns;
111 my @candidates = ();
112 foreach my $pattern ( @patterns ) {
113 @candidates = grep_pos_in_tasks( $pattern, @candidates );
116 # list only one task per group from these results.
117 my %out;
118 my $i = 0;
119 foreach my $id ( @candidates ) {
120 my (undef,$group) = ( $tasks[$id - 1] =~ m{(^|\s)(/\S*)(\s|$)} );
121 if(defined $group) {
122 $out{$group} = $id if (!exists $out{$group});
123 next;
125 $out{$i++} = $id;
128 print_tasks( values %out );
131 sub move_task
133 my $num = shift;
134 my $file = shift || quit("no file to move to!");
136 quit("invalid task number") unless ((defined $num) && ($num =~ /^\d+$/));
137 quit("") if ($num > @tasks);
139 my $task = splice @tasks, $num - 1, 1;
140 append_to_file($task, $file);
141 write_tasks();
144 sub do_task
146 my $flag = shift;
147 my $num = ($flag =~ /^\d+$/) ? $flag : shift;
149 my $ask = ($flag eq "-i") ? 1 : 0;
150 if($ask) {
151 print "delete task $num <$tasks[$num - 1]> ?";
152 my $resp = <STDIN>;
153 chomp $resp;
154 return if ($resp =~ /^n/i); # return if negative
157 move_task($num, $done_file);
160 sub delete_task
162 my $num = shift;
163 move_task($num, $not_done_file);
166 sub read_tasks
168 open TODO, "<$todo_file" or return;
170 while(<TODO>) {
171 chomp;
172 next unless $_;
173 push @tasks, $_;
176 close TODO;
179 sub write_tasks
181 open LOCK, ">$todo_file.lock" or quit("cannot open lock file : $!");
182 flock LOCK, LOCK_EX;
184 open TODO, ">$todo_file.$$" or quit("cannot open $todo_file.$$ : $!");
185 print TODO join( "\n", @tasks );
186 close TODO;
188 rename( "$todo_file.$$", $todo_file ) or quit("cannot rename : $!");
190 flock LOCK, LOCK_UN;
191 close LOCK;
194 sub grep_pos_in_tasks
196 my $pattern = shift;
197 my @positions = @_;
199 @positions = ( 1 .. scalar(@tasks) ) if( @positions < 1 );
201 my @ret = ();
202 foreach my $pos ( @positions ) {
203 push @ret, $pos if ( $tasks[$pos - 1] =~ /$pattern/ );
206 return @ret;
209 sub append_to_file
211 my $task = shift or quit("no task to append!");
212 my $file = shift or quit("no file to append to!");
213 my ($day, $mon, $year) = (localtime) [3 .. 5];
214 $mon ++;
215 $year += 1900;
217 open LOCK, ">$file.lock" or quit("cannot open lock file : $!");
218 flock LOCK, LOCK_EX;
220 open DONE, ">>$file" or quit("cannot open $file.$$ : $!");
221 print DONE "$year-$mon-$day : $task", "\n";
222 close DONE;
224 flock LOCK, LOCK_UN;
225 close LOCK;
228 sub quit
230 my $msg = shift;
232 die $msg if $msg;
235 sub extract_tokens
237 my $pattern = shift;
238 my %ret;
240 foreach ( @tasks ) {
241 $ret{$2}++ if (/(^|\s)($pattern.*?)(\s|$)/);
244 return keys %ret;
247 sub list_contexts
249 my @contexts = extract_tokens('@');
250 print join( "\n", @contexts, "" );
253 sub list_groups
255 my @groups = extract_tokens('/');
256 print join( "\n", @groups, "" );
259 sub change_task
261 my $num = shift;
262 my $new_task = join(" ", @_);
264 quit("not a valid task number") unless ((defined $num) && ($num =~ /^\d+$/));
265 quit("") if ($num > @tasks);
267 if($new_task =~ /^\s*$/) {
268 # wow - we don't have a task here - so we'll get one.
269 my $term = new Term::ReadLine 'gtdo';
270 $new_task = $term->readline( "new task >", $tasks[$num - 1] );
273 return if($new_task =~ /^\s*$/);
275 $tasks[$num - 1] = $new_task;
276 write_tasks();
279 sub print_tasks
281 my @ids = @_;
283 foreach my $id ( sort { $a <=> $b } @ids ) {
284 print "$id : $tasks[$id - 1]", "\n";