# Horizon Calendar for your Obsidian vault: daily and periodic notes, tasks, and dated notes in one place. Part of the marioverse Obsidian plugin suite.
The month grid, with daily notes and open, done and overdue tasks shown right on their date.
## Views - **Sidebar mini calendar** — month at a glance with content dots: daily note (accent), due tasks (orange), overdue (red), scheduled (cyan), done (green), dated notes (gray). ISO week numbers open weekly notes. - **Calendar tab** (ribbon icon or `Horizon: Apri il calendario`) with three modes: - **Mese** — 7-column grid with content chips per day; "+N altri" jumps to the week view - **Settimana** — hour grid for the week: an all-day lane per day (all-day events, tasks, untimed notes) above timed events and timed notes placed by time, with a current-time line - **Giorno** — the same grid for a single day - **Agenda** — chronological list of the upcoming days that have content - **Diario** — a text-first feed of existing daily notes, newest first, with expandable previews ## Data sources 1. **Daily / weekly / monthly / yearly notes** — existence resolved live from the per-period folder + filename format configured in settings (monthly/yearly are off by default) 2. **Tasks** with obsidian-tasks-plugin emoji dates: 📅 due, ⏳ scheduled, ✅ done. Cancelled tasks (status `-`) are hidden 3. **Notes with a `date` frontmatter property** (periodic notes are excluded to avoid duplicates) 4. **Google Calendar events** (read-only, optional) — see below ## Google Calendar Horizon reads your Google calendars and shows their events in every view. It never writes to Google. Setup (once, on desktop): 1. In Google Cloud Console create a project, enable the **Google Calendar API**, configure the OAuth consent screen (External, add yourself as a test user, then **publish** it: tokens of an app left in Testing expire after 7 days). 2. Create an OAuth client of type **Desktop app**. 3. In Horizon settings → Google Calendar paste the client ID, store the client secret as an Obsidian secret, and press **Connect**. Sign-in happens in the browser and returns to a one-shot local port. The refresh token lives in Obsidian's secret storage on that device, never in the vault. Calendars follow your Google Calendar selection; switch single ones off in settings. Events refresh every 5 minutes, or with *Refresh Google Calendar*. Click an event for: **Join call** (Meet, Zoom or Teams link), **Open/Create meeting note**, **Open daily note**, **Open in Google Calendar**. A meeting note gets `date`, `gcal_event` and a `daily` link; a Granola note with the same title on the same day is reused instead of creating a new one. On phones events show only after connecting there, which needs the desktop sign-in flow, so mobile stays without events for now. ## Rich mini-cards Notes in Agenda, Week, the day popover, and Bases views render as **mini-cards**: title (with time), a clean two-line excerpt, and the note's cover image (frontmatter `cover`/`image`/`thumbnail` → first embed → first external image). Hydration is async and LRU-cached. In the compact Month grid, hovering a note chip shows a floating **hover-card** with image and a longer excerpt. Settings: card length slider and a `richCards` toggle to fall back to compact chips. ## Overdue triage Open overdue tasks roll up to a pinned **"In ritardo"** section on today in the Agenda (with a batch "Porta tutto a oggi"), and today's cell shows a red `↩ N` badge in every view. Right-click any task chip for **snooze presets** (Oggi / Domani / Lunedì prossimo / +1 settimana). Every reschedule Notice carries a 10-second **Annulla** that re-applies the old date through the same guarded writes. ## Bases view Horizon registers as a **Bases view**: any `.base` gains a month calendar. The Base decides *which* notes (filters); the view option `dateProperty` decides *when* (default `date` — set it to `created` for Granola meeting notes). Timestamps with an explicit offset (Granola writes UTC `Z`) are converted to the local day and time. ## Template tokens Beyond `{{title}}` / `{{date:FMT}}` / `{{time:FMT}}`, periodic-note templates can use: - `{{agenda}}` — the target day's calendar events and meetings (with times) and open tasks, as plain bullets with source links (never `- [ ]` lines: no task duplication) - `{{week-digest}}` — pre-compiled weekly review: *Fatto* (done per day), *Meeting e note*, *In arrivo* (due next week). ## Agents `(app.plugins.getPlugin('horizon') as HorizonPlugin).api` exposes: `getAgenda(from, to)`, `getOverdue()`, `rescheduleTask(ref, kind, day)`, `toggleTaskDone(ref)`, `exportAgenda()`, `propose(proposal)`. Reads come from the live index; writes go through the guarded line-edit path. - **Agenda export** — disabled by default; when enabled, writes to the configurable `.horizon/agenda.json` path at most every 5 minutes of activity. - **Ghost proposals** — agents append to the configurable `.horizon/proposals.json` path (`kind: 'reschedule' | 'new-task'`, `targetKey`, optional `reason`). Horizon renders dashed ✦ ghost chips on the target days; ✓ accepts, ✕ dismisses. ## Interactions - Click a day (or its number in the tab view) → open the daily note, creating it from the template when missing ({{title}}, {{date:FMT}}, {{time:FMT}} tokens fill against the target day). Mod-click opens in a new tab - Click a week number → open/create the weekly note - Hover a day with a note → native page preview - **Checkbox on a task chip** → toggle done with a `✅` date, Tasks-plugin compatible. Recurring (🔁) tasks open at the line instead — completing them needs the Tasks rrule engine - **Drag a task chip onto another day** (any view, including the sidebar) → its date field is rewritten in the source file. Writes are guarded: exact text at the expected line, unique-match fallback, abort on ambiguity ## Notes - Replaces the community **Calendar** plugin — disable it manually to avoid two sidebar calendars - The stale Periodic Notes config is ignored on purpose; Horizon keeps its own per-period settings (first launch seeds the daily period from `daily-notes.json`) - Templater syntax in templates passes through unexecuted (core-Templates tokens only) ## Mobile **Verified** — `isDesktopOnly: false` in `manifest.json`; `styles.css` ships a `pointer: coarse` media query with 44px hit areas plus a `max-width: 480px` responsive rule for compact layouts. ## Development ```bash pnpm i pnpm dev # esbuild watch pnpm build # typecheck + production build pnpm test # node native test runner (74+ unit tests, no Obsidian needed) pnpm lint ``` Architecture: a pure per-day index (`src/index/`) fed by `metadataCache` events, consumed by two `ItemView`s (`src/ui/`). All date math is UTC-immune (`src/dates.ts`); file writes go through guarded line edits (`src/edits/`). Pure modules never import the Obsidian runtime, so the whole read/write logic tests headless. ## Try it See it running in the [Obsidianverse sample vault](https://github.com/mariomile/obsidianverse-sample-vault), a small, fictional vault with the whole plugin suite pre-configured.