Simple Text Agenda

Yesterday, I got tired of not having a simple way of seeing an agenda for the upcoming days of all my calendars. Between work using hosted Exchange, using a joint iCloud calendar, and still using Google Calendar quite often, getting all of the information in one place is a pain.

I tried starting out with Google Calendar. Unfortunately, I can't add iCloud shared calendars because the iCloud servers have a robots.txt that disallow everything to everyone, and Google Calendar for some reason respects that.

iCloud doesn't let you subscribe to third party calendars. So it's out as a combined solution.

Hosted Exchange makes subscribing to third party calendars so difficult as to be impossible.

So like any good programmer, I decided I needed to write my own.

use strict;
use warnings;
use Data::Dumper;
use HTML::Entities;
use iCal::Parser;
use LWP::UserAgent::Cached;
use Text::Table;
my $cache_dir = '/tmp/lwp-cache';
my @calendars = qw|
my $now = DateTime->now( time_zone => 'America/New_York' );
my $tb = Text::Table->new({ }, { align => "right" });
my $parser = iCal::Parser->new();
foreach my $calendar_url (@calendars) {
my $combined = $parser->calendar;
my @events_list;
my $events = $combined->{events};
my %summaries;
my %summaries_seen;
foreach my $year (keys %{$events}) {
	foreach my $month (keys %{$events->{$year}}) {
		foreach my $day (keys %{$events->{$year}{$month}}) {
			foreach my $uuid (keys %{$events->{$year}{$month}{$day}}) {
				my $event = $events->{$year}{$month}{$day}{$uuid};
				my $summary = $event->{SUMMARY};
				my $start = $event->{DTSTART};
				my $end = $event->{DTEND};
				my $delta = $start->delta_days(DateTime->today());
				next if (DateTime->compare($start, DateTime->today()) == -1);
				next if ($delta->in_units("days") > 14);
				push(@events_list, [ $summary, $start, $end ]);
foreach my $event (sort { DateTime->compare($a->[1], $b->[1]) } @events_list) {
	next if ($summaries_seen{$event->[0]});
	my $summary = $event->[0];
	if ($summaries{$summary} > 1) {
		$summary .= ' (multiple)';
	my $delta = $event->[1]->subtract_datetime($now);
	my ($days, $hours, $minutes) = $delta->in_units('days', 'hours', 'minutes');
	my $delta_text = 'd';
	if ($days == 0) {
		$delta = $delta->in_units('hours');
		$delta_text = 'h';
	} else {
		$delta = $delta->in_units('days');
	$tb->load([ decode_entities($summary), $delta . $delta_text ]);
print $tb;
sub get {
	my ($url) = @_;
	mkdir($cache_dir) unless -d $cache_dir;
	my $ua = new LWP::UserAgent::Cached(cache_dir => $cache_dir);
	my $response = $ua->get($url);
	if ($response->is_success()) {
		return $response->content();
	} else {
		warn $response->status_line;

Above is the source for the script. Simply, it fetches the calendar URLs provided in @calendars, parses them, and ignores anything from the past, and anything more than 14 days out. Then it generates a nice text table showing the events happening.

If you find this useful, please let me know.