Adding an iCalendar source to Calfw (feat. Doom Emacs)

/img/calendarScreenshot.png
Here's my calendar in Emacs. I use both an org-agenda and an iCalendar source.

Introduction

This will be a short post on how I use custom sources for the Doom Emacs calendar module; in particular, a iCalendar/ics source.

Whenever I mention 'Doom', I'm referring to Doom Emacs.

Prerequisites

A basic understanding of how to use Doom Emacs, and some familiarity with Lisp-like languages.

Getting the Calendar

Firstly, you'll want to activate the calendar module in your init.el by uncommenting it. It's on line 176 in init.el at the time of writing.

:app
calendar

Once you :wq! and doom sync, you should have access to a calendar by running M-x =calendar. This calendar syncs with the org agenda by default, but we want an iCalendar source.

Adding our own sources

Unfortunately, it's not quite as simple as setting a variable. We're going to have to write our own interactive Elisp function. (You can add everything to your private config, config.el.)

The doom docs suggest a function like this.

(defun calendar-helper ()
  (interactive) ;; Allows us to call the function with =M-x [functionName]=
  (cfw:open-calendar-buffer
   :contents-sources
   (list
    ;; the below function loads our org agenda, you can remove it if you wish
    (cfw:org-create-source "Purple")
    ;; the below function takes a name, URL and a color.
    (cfw:ical-create-source "YourIcalName" "https://icalsource.com/ics/example.ics" "Blue"))))

This sort of works, but it's not ideal.

It opens a buffer with the calendar and loads the source(s). The problem is that Doom works best with workspaces: the usual calendar does this, which means we can conveniently switch between our code workspace and calendar workspace. So let's try to implement that with our calendar function.

I've taken and modified the original calendar function. This is the main function we would call with M-x =my-calendar, I've renamed it so the old calendar M-x =calendar is still there.

(defun =my-calendar ()
  "Activate (or switch to) *my* `calendar' in its workspace."
  (interactive)
  (if (featurep! :ui workspaces) ;; if we've enabled workspaces
      (progn
        (+workspace-switch "Calendar" t)
        (doom/switch-to-scratch-buffer)
        (calendar-init) ;; this is another function we'll write
        (+workspace/display))
    (setq +calendar--wconf (current-window-configuration))
    (delete-other-windows)
    (switch-to-buffer (doom-fallback-buffer))
    (calendar-init)))

The above mostly uses existing Elisp functions, except for calendar-init, which is below:

(defun calendar-init ()
  ;; switch to existing calendar buffer if applicable
  (if-let (win (cl-find-if (lambda (b) (string-match-p "^\\*cfw:" (buffer-name b)))
                           (doom-visible-windows)
                           :key #'window-buffer))
      (select-window win)
    (calendar-helper))) ;; this is the first function we wrote

We don't want to create a new buffer if it already exists.

Finally, here's all of the code together which you can paste into your config.el. Remember to add your own iCalendar name/link/colour.

(defun calendar-helper () ;; doesn't have to be interactive
  (cfw:open-calendar-buffer
   :contents-sources
   (list
    (cfw:org-create-source "Purple")
    (cfw:ical-create-source "YourIcalName" "https://icalsource.com/ics/example.ics" "Blue"))))
(defun calendar-init ()
  ;; switch to existing calendar buffer if applicable
  (if-let (win (cl-find-if (lambda (b) (string-match-p "^\\*cfw:" (buffer-name b)))
                           (doom-visible-windows)
                           :key #'window-buffer))
      (select-window win)
    (calendar-helper)))
(defun =my-calendar ()
  "Activate (or switch to) *my* `calendar' in its workspace."
  (interactive)
  (if (featurep! :ui workspaces) ;; create workspace (if enabled)
      (progn
        (+workspace-switch "Calendar" t)
        (doom/switch-to-scratch-buffer)
        (calendar-init)
        (+workspace/display))
    (setq +calendar--wconf (current-window-configuration))
    (delete-other-windows)
    (switch-to-buffer (doom-fallback-buffer))
    (calendar-init)))