calendar
Handles recurring events, timezone-aware occurrence expansion, conflict detection, iCal export/import, and admin UI components for scheduling applications.
Key namespaces
| Namespace | Layer | Responsibility |
|---|---|---|
|
shared |
Malli schemas: |
|
shared |
|
|
core |
|
|
shell |
|
|
core |
RRULE parsing, |
|
core |
|
|
core |
Pure Hiccup: |
|
core |
|
|
shell |
ical4j adapter ( |
|
shell |
Public API: |
Defining event types
(require '[wagoe.calendar.shell.registry :as registry])
(registry/defevent appointment-event
{:id :appointment
:label "Appointment"
:schema [:map
[:patient-id :uuid]
[:room :string]]})
(registry/get-event-type :appointment) ;=> {:id :appointment ...}
(registry/list-event-types) ;=> [:appointment ...]
Recurring events (RRULE)
(require '[wagoe.calendar.core.recurrence :as recurrence])
;; Expand weekly recurring event
(recurrence/occurrences event-data start-dt end-dt)
;; Next occurrence from an explicit reference instant
(recurrence/next-occurrence* event-data after-dt)
;; Full expansion with metadata
(recurrence/expand-event event-data range-start range-end)
Conflict detection
(require '[wagoe.calendar.core.conflict :as conflict])
(conflict/overlaps? event-a event-b) ;=> true/false
(conflict/find-conflicts events window-start window-end)
;=> ({:event-a ... :event-b ... :overlap-start ... :overlap-end ...})
UI components
(require '[wagoe.calendar.core.ui :as ui])
(ui/month-view 2026 3 events {:timezone "Europe/Amsterdam"
:event-href #(str "/appointments/" (:id %))
:t t-fn})
(ui/week-view (java.time.LocalDate/of 2026 3 2) events {:t t-fn})
-
An event’s
:href, or the:event-hrefoption, turns its badge into a link. -
Weekday and month labels are
[:t …]markers. They resolve only throughwagoe.i18n.shell.render; plainhiccup2.core/htmlprints them as<t>tags. Pass:t(key → string) to resolve them when the hiccup is built. -
Multi-day events show on every day they cover, clipped to the visible range; the week view puts those of 24 hours or more in an all-day row. Mark all-day events
:all-day? true: they show no time and keep their dates in the event’s own:timezone.
iCal export / import
The :wagoe/calendar component implements CalendarAdapterProtocol. Take
(ig/ref :wagoe/calendar) and call it through the ports:
(require '[wagoe.calendar.ports :as cal]
'[wagoe.calendar.core.feed :as feed])
;; Export to iCal string
(cal/export-ical calendar events {})
;; Import from iCal string; :timezone places events without a TZID
(cal/import-ical calendar ical-string {:timezone "Europe/Amsterdam"})
;; Ring response for a feed (subscription URL): content type, filename, caching
(feed/ical-response (cal/export-ical calendar events {}) {:filename "appointments.ics"})
An :all-day? true event is written as DTSTART;VALUE=DATE with an exclusive
DTEND, and a VALUE=DATE event is read back :all-day? true. event/all-day?
reads that flag and nothing else.
Import reads a TZID that names an IANA zone, or a Windows one such as
W. Europe Standard Time, with the JVM’s tz rules, whatever the feed’s
VTIMEZONE says, and returns it as :timezone. Any other TZID is read with its
VTIMEZONE, and :timezone is the option’s.
Without Integrant, wagoe.calendar.shell.service offers the same calls.
Dependencies
Uses org.mnode.ical4j/ical4j {:mvn/version "4.3.0"} (note: group ID is org.mnode.ical4j, Java packages are still net.fortuna.ical4j.*).
Testing
clojure -M:test :calendar