Sun, 12 Feb 2006

Calendars and TODOs

I've thought for quite a while that I wanted to sort out my various TODO lists into something more general, and integrate it with a calendar of some sort.

My requirements didn't seem to extreme:

* access from any machine or some way to synchronise machines
* calendar and TODO list integrated
* let me know whats coming up and what I need to do soon

I looked at Chandler version 0.6.0, which is marked as "experimentally usable" but unfortunately I found it to be unusable. I see 0.6.1 is released now, so that might be better, but I've not tried it. But Chandler seemed to be overkill for what I wanted anyway.

So I started looking at command line applications. I experimented with ccal which seems quite nice. It's written in python and I even started hacking on it to make it do some more of what I wanted and some more of what it should have done, but in the end it just didn't do enough of what I wanted. Here's my patch, anyway:

--- /home/pjcj/utils/	2004-08-29 18:01:59.000000000 +0200
+++ /home/pjcj/utils/ccal	2006-02-12 17:38:08.230850964 +0100
@@ -182,11 +182,17 @@
                print "ccal entries:\n"
                if cal:
                        print "Calendar:\n"
-			for item in self._cal.getItems():
-				if not str(item.__class__)=="__main__.ccalItem":
-					item=ccalItem(item)
-				print item.entry
+			for i in range(999) :
+				viewtime = (datetime.datetime(self._cal.viewtime[0],self._cal.viewtime[1],self._cal.viewtime[2])+datetime.timedelta(days=i)).timetuple()
+				entries=self._cal.getItems(viewtime)
+				dateString = time.strftime(self._cal.dateformat,viewtime)
+				dateString += " ("+self.friendlyDateTimeDelta(datetime.datetime(viewtime[0], viewtime[1], viewtime[2]) - datetime.datetime(self._cal.localtime[0],self._cal.localtime[1],self._cal.localtime[2]))+")"
+				if entries!=None and len(entries) > 0:
+					entries.sort(lambda x, y: cmp(ccalItem(x).entry, ccalItem(y).entry))
+					for item in entries:
+						if not str(item.__class__)=="__main__.ccalItem":
+							item=ccalItem(item)
+                                                print dateString+": "+item.entry
                        print "\n"
                if todo:
                        print "Todo list:\n"
@@ -523,6 +529,7 @@
+				entries.sort(lambda x, y: cmp(ccalItem(x).entry, ccalItem(y).entry))
                                for entry in entries:

Then I started to take a look at some vim plugins hoping for better luck. I found a couple that individually seemed to do part of what I wanted. First there was VimOutliner, which will do nicely for managing my todos. Then there was calendar.vim, which will deal with the calendar part. Now all I needed to do was join them up so that todos with a date went into the calendar. I little bit of Perl sorted that out. So now I have the infrastructure. All I have to do now is use it.


# Copyright 2006, Paul Johnson (

use warnings;
use strict;

use File::Find;

my $dir = <~/g/calendar>;
chdir $dir or die "Can't chdir $dir: $!";

my %todos;

my $outline = "pjcj.otl";

# Read the outline file and note any entries that have associateed dates.
open my $f, "<", $outline or die "Can't open $outline: $!";
while (<$f>)
    # Dates have the format YYYY-MM-DD
    push @{$todos{$2}{$1 eq "X" ? "done" : "open"}}, $3
        if /^\s*(?:\[(.)\])?\s*(?:\d*%)?\s*(20\d\d-[01]\d-[0-3]\d)\s*(.+)/;

my ($y, $m, $d) = (localtime)[5, 4, 3];
my $today = sprintf "%04d-%02d-%02d", $y + 1900, $m + 1, $d;
print "Today is $today\n";

sub write_calendar
    my ($cal) = @_;

    # Get the date from the filename.
    my ($y, $m, $d) = $cal =~ /\d+/g;
    my $date = sprintf "%04d-%02d-%02d", $y, $m, $d;

    my %entries;
    if (-e $cal)
        # Read in the calendar file, ignoring any existing todos.
        open my $f, "<", $cal or die "Can't open $cal: $!";
        my @l = grep /\S/ && !/^(?:TODO|DONE) /, <$f>;
        chomp @l;
        @entries{@l} = ();

        # Delete the file - there may be nothing more to write to it.
        unlink $cal or die "Can't delete $cal: $!";

    # Add the todos from the outline.
    @entries{map "TODO $_", @{$todos{$date}{open}}} = ();
    @entries{map "DONE $_", @{$todos{$date}{done}}} = ();

        # If there's nothing to be done, just get out.
        last unless %entries;

        # Write the new calendar file.
        open my $f, ">", $cal or die "Can't open $cal: $!";
        print $f "$_\n" for sort keys %entries;

        # Print out what's coming up.
        # Don't print if it's in the past and there are no todos,
        # or if it's just todos and they are all done.
        last if $date lt $today && !@{$todos{$date}{open}};
        last if keys %entries eq @{$todos{$date}{done}};

        print "$date\n";
        print "  $_\n" for sort keys %entries;

    # We're finished with this date.
    delete $todos{$date};

sub wanted
    # We're only interested in calendar files.
    return unless -f;
    unlink, return if -z;
    return unless /\.cal$/;

    write_calendar $_;

    no warnings "numeric";

    # Go through the calendar files in chronological order.
            wanted     => \&wanted,
            preprocess => sub { sort { $a <=> $b } @_ },
            no_chdir   => 1
         }, <20??>);

# Now take the remaining todos and turn them to the calendar files.
for my $date (sort keys %todos)
    # Locate the calendar file from the date.
    (my $cal = $date) =~ s|-0?|/|g;
    $cal .= ".cal";

    write_calendar $cal;

[/software] permanent link

November 2022
Sun Mon Tue Wed Thu Fri Sat