Obsidian Full Calendar's goal is to give users a robust and feature-ful calendar view into their Obsidian Vault. In addition to displaying and modifying events stored in note frontmatter and daily note bulleted lists, it can also read events from the Internet in CalDAV and ICS format.
Obsidian Full Calendar takes its name from FullCalendar, a "Full-sized drag & drop event calendar in JavaScript." This plugin uses FullCalendar as its calendar view. While the naming can be ambiguous, this document will always refer to the FullCalendar view library without any spaces, or as fullcalendar.io
. The plugin will be referred to either as "the plugin", "Full Calendar" with a space, or "Obsidian Full Calendar".
As of now, the plugin supports events from the following sources and formats:
- Frontmatter of notes in the open Obsidian Vault.
- Bullet list items in Daily Notes generated by the core Daily Notes or Periodic Notes plugins.
- ICS files publicly accessible at a URL.
- CalDAV servers authenticated with HTTP basic authentication.
Much of the code of Full Calendar exists to deal with the normalization of these formats so they can be handled by the view layer without worrying about what sources different events are actually from.
Below is a birds-eye view of the different components of the plugin, and the interactions between them.
┌──────────────┐
│ │
┌────────────┐ │ │ ┌─────────────┐ ┌──────────────┐
│ ├───► ├───────► ├─────► │
│ EventStore │ │ EventCache │ │ view.ts + │ │ FullCalendar │
│ ◄───┤ ◄───────┤ calendar.ts ◄─────┤ View* │
└────────────┘ │ │ │ │ │ │
│ │ └─────────────┘ └──────────────┘
┌──►└──────▲──┬────┘
│ │ │
│ │ │
│ ┌─────┴──▼─────┐
│ │ │
│ │ Calendars │
│ │ │
│ └─▲──┬─────▲───┘
│ │ │ │
┌───────────┴──────┴──▼──┐ │
│ │ ┌┴────────────┐
│Obsidian Vault APIs* │ │ The │
│ │ │ Internet* │
└────────────────────────┘ └─────────────┘
* Components with an asterisk are not part of the plugin's code.
Following the advice in this blog post on architecture docs, the following section will list out the main modules of the code without linking out to specific file locations that may quickly become stale. Make use of code search and the TypeScript Language Server to jump around and explore the code, with this section as your guide.
This module defines some common types used throughout the code. The most prevalent is OFCEvent
, short for Obsidian Full Calendar Event, that specifies the intermediate representation for all events in the plugin. Note that FullCalendar.io uses a different event format called EventInput
, which you can read about in their documentation.
OFCEvent
is derived from a Zod parser that handles parsing/validating JavaScript objects into the expected shape of an event. You can check out the parser in types/schema.ts
.
Translation between OFCEvent
and EventInput
is handled in interop.ts
. Each Calendar
subclass (see below) handles its own translation from its source format into OFCEvent
.
The core
directory consists of two classes, EventStore
and EventCache
. These two classes comprise the plugin's main event-managing logic.
The EventStore
is the source of truth for events in the plugin. Its interface is similar to a simplified database that stores events, calendars and file locations. Files and calendars are one-to-many relationships: every event is related to exactly one calendar and at most one file, but calendars and files can have many events within them. The EventStore
allows for effecient querying of events grouped by calendars and files. Every event in the EventStore
has an ID associated with it. Local events have random IDs that are generated at insert time, but remote events using the iCal spec have UID
s that are plumbed through.
The EventCache
manages the state stored in the EventStore
. Its main job is co-ordinating with both the view layer and the Calendar
s which perform I/O to actually read events from disk or the network. The EventCache
has two main hooks to update the EventStore
:
- Hook (via
MetadataCache.on('update')
) for when a file has changed so that it can tellCalendar
s to re-parse that file. - Hook for when an event with a given ID has been modified from the view.
Other components can subscribe to state updates on the
EventCache
. Right now, the view is the only subscriber, but in the future it may be possible for other plugins to subscribe to updates.
Notably, while the core
components have some knowledge of Obsidian APIs (mostly the TFile
type and the ability to show Notice
toasts to the user), they do not hold references to the App
, Vault
, MetadataCache
or any other API that deals with file I/O. File I/O is handled entirely by the Calendar
subclasses. This simplifies testing dramatically, since the Obsidian API does not need to be mocked out when testing the EventCache
logic.
The plugin has exactly one EventCache
instance at any given time. It is initialized and hooked up to Vault
and MetadataCache
listeners when the plugin is initialized, in main.ts
.
While calendars that store their events on disk are kept up-to-date with listeners from the Obsidian API, remote calendars are managed with the Stale-While-Revalidating (SWR) cache strategy.
Each source of events has its own Calendar
subclass that handles the relevant I/O operations and parses events into the common format. There are two abstract subclasses: RemoteCalendar
and EditableCalendar
. RemoteCalendar
s should cache their responses from the Internet, and have a method to re-fetch their input as part of the SWR cache strategy.
EditableCalendar
s are constructed with references to an ObsidianAdapter
instance that handles all interactions with the Obsidian API. This adapter is useful for testing, since it reduces the surface area of APIs to be mocked from the entire API to a handful of functions that the plugin actually uses. It also allows for useful and safe abstractions on top of the Obsidian API, so that its harder for Calendars to do incorrect things, like write a stale copy of a file back to disk.
While core
and calendars
make up the Model in the MVC
pattern, the Views and Controllers are currently both living in the ui
directory. The view connector to the FullCalendar library lives in calendar.ts
. Most of the controller logic that interfaces with the EventCache
lives, somewhat confusingly, in view.ts
, which also instantiates the Obsidian plugin View. Auxilliary views, like the edit/create modal and settings selectors, are React components that live in their own .tsx
files and are mounted into the DOM when needed.
Architecture Invariant: All interactions with event data should be mediated by the EventCache
. Code in the ui
directory should not reference or call out to the EventStore
, Obsidian Vault APIs, or Calendar
subclasses.