Bringing Google Calendar to My Desktop

ScreenshotFor a long time, I’ve wanted fast access to Google Calendar’s agenda view. Essentially, I don’t like waiting for the page to load every time I have to remind myself of the time of some appointment in the next day or two. Feel free to read my solution and skip my disclaimers and ramblings.

The major challenges for me have been (1) that I don’t care to open iCal, since I never use it otherwise, (2) that Google presents all its data on one line (making it a bit more complicated to use Unix commands that rely on line endings), and (3) that I don’t want to take up a whole lot of bandwidth or system resources.

I think I’ve finally got a solution, and though it’s a bit complex, it’s worth sharing here. I should mention now that it is Mac-only, though I’m sure capable Windows or Linux users could adapt it readily enough.

My system involves a number of components, some non-standard (in the Mac OS), some optional. But here they are, just so you have a sense up front of where we’re headed:

  • A curl command to retrieve data from my calendar’s RSS feed
  • A launchd agent to run the curl command periodically
  • Lingon to set up the launchd agent
  • A Perl script to parse the XML from the calendar feed
  • A few Perl modules to support the script
  • Geektool to send the output of the script to my desktop periodically

What I will discuss here is how I implemented a system that is perfect for my needs. I won’t really suggest modifications or alternatives, though I welcome such thoughts in the comments.

Still, a few limitations are worth noting:

  1. As it stands, this doesn’t address multiple calendars; I have only one that I check with any frequency
  2. I use 24-hour time, because I don’t care enough to convert to 12-hour; if you do, please post your changes.
  3. At this point, the launchd command fails if I go without an Internet connection for more than 10 minutes or so (which causes the curl command to fail). I can restart it easily enough, and I usually have a connection. But, consider this fair warning, and give us a comment if you have any ideas.
  4. As I’m not an expert in any of this, really, I don’t know how portable my solution is, nor how efficiently my code is written. Comments are welcome.

Otherwise, happy reading.


Using curl to Get the Data

First, create a plain text file in your user directory. Open a new terminal window, and type
pico .gcalfeed.xml

Then type Ctrl-O, then the enter key. Finally, type Ctrl-X to exit pico.
I use the following curl command to retrieve my calendar’s private feed from Google. Note that the lines are broken artificially; otherwise you’d have a wicked horizontal scroll bar right now.

/usr/bin/curl -f -s
http://www.google.com/calendar/feeds
/[your.gmail.username]%40gmail.com/
[your-private-calendar-feed]/full
-o /Users/[your-OS-X-user-directory]/.gcalfeed.xml

First, note that I specify the full path to curl, which will become necessary once we’re using launchd to run it. The -f flag makes the command fail silently (for example, if I have no Internet connection). The -s flag keeps curl’s bulky status meters out of my system logs.

The next three lines are the URL, which you can retrieve in its entirety by going your calendar’s details page in Google Calendar. To grab the XML link for your Private Address, click the XML button in the Private Address section and copy the URL. Be sure you change the end of the URL from “/basic” to “/full” before you use it.

The last flag above, -o, tells curl to save the output to a file. Mine is in my OS X user directory to avoid permissions issues; it’s got a period at the front of its name so I don’t have to look at it all the time.

It’s worth trying all this from the OS X Terminal with your own information till you’re certain it works for you.

Using launchd to Run the curl command

I used Lingon to set this up, and that’s probably the easiest way for you, too. In Lingon, click the New button. Leave the “My Agents” radio button selected, and click the Create button. Click go to the Expert panel, select all, and paste in the XML below. (Don’t worry if the indentation doesn’t show up.)

UPDATE: Newer versions of Lingon don’t have the Expert panel. In that case, you can also create a text document with this XML at ~/Library/LaunchAgents/ (create the LaunchAgents folder if it doesn’t exist). Follow the com.devan.gcalfeed naming convention, but append “.plist” to the filename. Once you’re done, you’ll have to log out and back in, or restart.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

<key>Label</key>
<string>com.devan.gcalfeed</string>
<key>ProgramArguments</key>
<array>

<string>/usr/bin/curl</string>
<string>-f</string>
<string>-s</string>
<string>http://www.google.com/calendar/feeds
/[your.gmail.username]%40gmail.com/
[your-private-calendar-feed]/full</string>
<string>-o</string>
<string>/path/to/.gcalfeed.xml</string>

</array>
<key>RunAtLoad</key>
<true/>
<key>ServiceDescription</key>
<string>DownloadGCalFeed</string>
<key>StartInterval</key>
<integer>300</integer>

</dict>
</plist>

First, where I have com.devan.gcalfeed and DownloadGCalFeed (under the Label and ServiceDescription keys), you can have whatever you want. It is worth using the reverse naming convention for the former, as it will make the process readily identifiable in case you run into trouble.

Next, both the URL string and the string containing the path to your XML file should be on a single line, but once again lines have been artificially broken here.

Finally, theRunAtLoad key tells launchd to run this process once when it is loaded (which happens, among other times, when you log in), and the StartInterval key tells launchd how many seconds should pass between invocations of the process.

Once you’ve pasted this in and included your own information, click Lingon’s Save & Load button.

Using Perl to parse the XML

Download this Perl script, remove the .txt extension, and add a period to make the title just “gcal.pl”. Place the script somewhere on your system; I chose /usr/local/bin, but do what you like. Also, on line 9, you’ve got to include the path to your own XML file.

For the script to work, you’ll need the following Perl modules installed. I use CPAN for installation, which in this case is a good idea because some of the modules have dependencies on others.

  • XML::Simple
  • Data::Dumper
  • Date::Parse

I’ve got a lot of commenting in there, but in brief, the script grabs non-all-day events from today, tomorrow, and the next day, and returns the title and date and time information for each one. Then, it grabs all-day events whose date span includes today’s date, and returns their information.

(In fact, it doesn’t grab them in that order; the last few lines do the sorting to get them that way. All the same.)

Typical output looks like this:

Today, 12:30-14:00: Meet with Andrés
Tomorrow, 08:00-09:00: Call with client
Tomorrow, 19:00-20:00: Dinner at Casbah
Through 09/14: Off from work

Using Geektool to View the Data

This is the easy part. Install Geektool, if you haven’t already, and set up a new entry. Make it a Shell command, and enter the following:

/path/to/your/gcal.pl

I have GeekTool run the script every 300 seconds.

37 Responses to “Bringing Google Calendar to My Desktop”

  1. Mac Tip: Embed Google Calendar Agenda into the Desktop | Redslush.com Says:

    [...] you should be all set. Here’s how to embed the GCal web page onto your Windows desktop. Bringing Google Calendar to My Desktop [Devan Being [...]

  2. Mac Tip: Embed Google Calendar Agenda into the Desktop · TechBlogger Says:

    [...] you should be all set. Here’s how to embed the GCal web page onto your Windows desktop. Bringing Google Calendar to My Desktop [Devan Being [...]

  3. Brandon Says:

    Can someone post a screenshot?

  4. devan Says:

    Hi, Brandon,

    I’ve added a thumbnailed screenshot at the top of the post. Click for full size.

    Devan

  5. Macoholic - Notizen aus dem Leben Says:

    [...] Wer sich und seinem Mac das zutraut, der “curlt” sich die Daten mal eben auf den Schirm. Wie das geht erfahrt ihr hier. [...]

  6. Dom Barnes Says:

    Hey Devan. Great work
    I’ve followed the instructions but Geektool doesn’t seem to produce any output. My console output says Permission denied. Do I need to chmod a file or something?

  7. Byron Says:

    you can simplify this. you don’t need the launchd/Lingon; you can do their work in the perl script that you’re invoking anyway.

    add the following code to your gcal.pl file, after line 5.
    system "curl -f -s http://www.google.com/calendar/feeds/your.gmail.username%40gmail.com/your-private-calendar-feed/full -o /path/to/.gcalfeed.xml”
    this calls the curl command every time that gcal.pl is run; which is however often you have geektool refresh.

  8. devan Says:

    Thanks, Dom,

    You might be right. You could try:

    chmod 755 /path/to/gcal.pl

    I also had worse luck when I was trying to store the xml file outside my home directory (in /usr or /tmp or something). Not sure, but one or both of those should help.

    Devan

  9. devan Says:

    Thanks, Byron,

    You’re quite right, of course — especially since I have both the launchd agent and the GeekTool item set to run every 300 seconds.

    The only issue for me when I had the curl command in the perl script would came when the curl command would fail (e.g., if I had no Internet access). In that case, the script would quit, and I’d be left with no agenda items on the desktop. (Perhaps there’s a workaround here? I am an amateur.)

    For me, an outdated agenda is better than none, because my event times don’t usually change once they’re recorded; for others, it might be better not to have any events than inaccurate ones. In that case, your solution would be ideal.

  10. Jeff Says:

    This is pretty remarkable stuff.

    Anyone have an idea why it might be giving me a “‘Use of uninitialized value in numeric le (

  11. Jeff Says:

    Anyone know what might be causing a

    Use of uninitialized value in numeric le (<=) at ./gcal.pl line 73.

    It outputs that message a dozen or so times, and then no output.

    By screenshot, this thing looks incredibly slick and I’d love to get mine running.

    Amazingly creative stuff.

  12. Dom Barnes Says:

    Thanks for the help. Its working now.
    I moved the gcal.pl script to my home directory. Also turns out XML::Simple didn’t install properly so i did sudo cpan before going to install.
    Thanks all.

  13. Byron Says:

    really? when i’m offline i still get my agenda showing up, even using this method.

    i mean, logically, if the curl command fails, the .gcalfeed.xml file won’t be overridden. if it’s not overridden, the perl script should use whatever values are in it (”outdated” agenda).

  14. devan Says:

    Interesting — I was using an earlier version of the script, so maybe it was a different problem. I’ll check it out. Thanks again.

  15. devan Says:

    Hi, Jeff,

    Interesting question. Use of uninitialized value” usually means some variable hasn’t been declared, but the only two in the less-than-or-equal-to phrase (the “numeric le,” if you like) are $bday and $tday, both of which have been initialized earlier in the script.

    Two things to try: First, it looks like when you’re calling the script, you’re calling it with a relative path (./gcal.pl). Try calling it with an absolute path (e.g., /Users/you/Documents/gcal.pl).

    Second, try commenting out the line that reads

    use strict;

    by adding a hash mark:

    # use strict;

    Without that instruction, Perl might be a bit more flexible about uninitialized variables.

  16. Chris Says:

    As a note, in general you can run things from cron on a macosX box (laptop say) so doing things with launchd which (to me) seems very messy isn’t required, just drop a simple crontab entry like:

    */10 * * * * /usr/bin/curl -f -s {private xml url} -o {destination for .gcalfeed.xml} > /tmp/gcal-feed.log 2>&1

    This will execute the curl command every 10 minutes (*/5 for every 5 minutes if you prefer, but how often do you update your calendar?) and drop the XML file into the proper place for the script+geektool to do their magics.

    Use cron, it’s built-in…

  17. devan Says:

    Launchd is indeed a bit more complicated (though Lingon helps greatly when you’re creating a new agent). But cron is deprecated in OS X, with version 10.4. Someday, it may go away completely…

  18. flipdoubt Says:

    I know this is a little off topic, but could you share your Battery, Hard Drive, and Wireless scripts?

  19. devan Says:

    No problem. These may not be the most efficient, or anything, but they’re easy.

    For the battery, I have the following in GeekTool:

    echo "BATTERY";
    system_profiler SPPowerDataType | grep "Remaining" | cut -c 7-9,31-40;
    system_profiler SPPowerDataType | grep "Full" | cut -c 7-10,33-40

    For the Hard Drive:

    echo "HARD DRIVE";
    df -m | grep "disk0s2\|Capacity" | cut -c 41-54

    For the Wireless info:

    echo "WIRELESS";
    airport -I | grep -w SSID | cut -c 12-50;
    airport -I | grep avgSignalLevel | cut -c 5-50;
    airport -I | grep avgNoiseLevel | cut -c 6-50;
    ifconfig en1 | grep inet | grep -v inet6 | cut -d " " -f 2

    Hope these help.

  20. ltj Says:

    and how to get(feed) and show my repeated events?

  21. devan Says:

    That’s gonna be a tough one. My script relies on the element in the XML, and recurring events don’t even have that one; all the timing is handled with the element, and NOT with XML structures (which makes parsing difficult).

    I’ll have to look into this one at greater length, but in case somebody else will just get the solution, here’s a sample of the relevant content:


    DTSTART;TZID=America/New_York:20070905T113000
    DTEND;TZID=America/New_York:20070905T123000
    RRULE:FREQ=DAILY;UNTIL=20070906T153000Z;WKST=MO
    BEGIN:VTIMEZONE
    TZID:America/New_York
    X-LIC-LOCATION:America/New_York
    BEGIN:DAYLIGHT
    TZOFFSETFROM:-0500
    TZOFFSETTO:-0400
    TZNAME:EDT
    DTSTART:19700308T020000
    RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
    END:DAYLIGHT
    BEGIN:STANDARD
    TZOFFSETFROM:-0400
    TZOFFSETTO:-0500
    TZNAME:EST
    DTSTART:19701101T020000
    RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
    END:STANDARD
    END:VTIMEZONE

  22. devan Says:

    Hi again, Jeff,

    I think this is caused by repeated events. They have no element, so the script is breaking on them. I’m looking into repeat handling now, but it may be a while… Sorry.

    Devan

  23. pizen Says:

    I added the following to the perl script

    use Date::Format;

    my $startmin = time2str("%Y-%m-%d", time);
    my $startmax = time2str("%Y-%m-%d", time+345600);

    then I appended ?singleevents=true&start-min=$startmin&start-max=$startmax&orderby=starttime&sortorder=a to the GCal XML url.

    There’s more detailed info at the Google Calendar Data API Reference.

  24. devan Says:

    Great work, pizen. I’ll try this later today. ltj, pizen’s solution should work for you… May take some tinkering with the script.

  25. ltj Says:

    it works, thanks a lot

  26. Desktop and Laptop Computers Says:

    Desktop and Laptop Computers…

    I couldn’t understand some parts of this article, but it sounds interesting…

  27. Dennis Walker Says:

    Wow, this is not easy. Would it be possible to see a version of someone’s pl script that has the curl and Date::Format rolled in? It seems that putting the System curl call is definitely the way to go. It also seems that everything after “system” is supposed to in quotes. Currently I’m hung up on this error, “Use of uninitialized value in numeric le (

  28. devan Says:

    Hi, Dennis,

    I think the error you’re getting is caused by my failure to account for repeated events. See pizen’s comment above, where he adds parameters to the query string. Should work like a charm.

    As for rolling curl into the script, I’ve tried both ways, and I still prefer to have curl run as a launchd agent.

    When I have curl in the perl script, two things happen:

    1. If the script fails (e.g., because I’m offline), my agenda doesn’t show up. This should not happen, as Byron points out above, but it does.

    2. While the script is running, I can’t see any events. So, for about 10 seconds every 5 minutes, I’m in the dark. No big deal, but I like it the other way better.

    All that said, getting curl in the script could look like this:

    my @curlargs = ("/usr/bin/curl", "-s", "-f",
    "[URL string]“, “-o”, “/path/to/.gcalfeed.xml”);

    system(@curlargs) == 0
    or die "system @curlargs failed: $?";

    As usual, I’ve broken one of these lines artificially; there’s a line break between "-f", and "[URL string]“ that you should remove.

  29. chris Says:

    go away completely? ugh :( also, the google-apps hosted calendars don’t seem to be functional with this method… you don’t get a ‘private xml url’ the url’s they pass out are only ‘public’ versions which will only work if your calendar is shared to all the world.

    I do hope that they don’t deprecate cron…

  30. jan Says:

    I’m using 10.5.2 with Lingon 2.0.2 and I don’t see the “Expert” tab anywhere - what am I doing wrong?

    Any help is greatly appreciated.

  31. devan Says:

    You’re quite right—Lingon has undergone a significant change since my post. See my update above, near the discussion of the XML.

  32. Atakpa Livingstone Says:

    I want to see google on my desktop even if I am offline

  33. devan Says:

    Hi, Atakpa,

    This should do just what you’re asking, but since it doesn’t, try this:

    Skip all the work with Lingon, and just include the curl command in your perl script, using this syntax:


    my @curlargs = (”/usr/bin/curl”, “-s”, “-f”,
    “[URL string]“, “-o”, “/path/to/.gcalfeed.xml”);

    system(@curlargs);

    Be sure you remove the line break I’ve inserted after “-f”.

  34. name Says:

    Hi!,

  35. ashok patnaik Says:

    i feel verry lucky

  36. fnurl Says:

    Hi I needed some timezone stuff so I added the following stuff:

    First I need two variables to keep track of my time zone and the difference from the one in the google calendar:

    (specified after my $data ….)
    my $tzone = 2;
    my $zoneDiff = 0;

    Then I need to modify the start and end time (around line 38):

    if (defined($bzone)) {
    # time zone into account, $bzone is in seconds
    $bzone = $bzone/3600;
    $zoneDiff = $tzone-$bzone;
    $bhh = $bhh+$zoneDiff;
    $ehh = $ehh+$zoneDiff;
    }

  37. devan Says:

    Thanks, fnurl—I’m sure that’ll be helpful.

Leave a Reply