- SPEC.md: retitled from 'v1 Draft' to a living-document status line; added missing Schema Reference sections for gtd-kanban (2.3) and gtd-matrix (2.4, both modes) — previously undocumented outside the roadmap; added unscheduled-todos/unscheduled-notes to the calendar table; rewrote Rendering (§4) and Architecture (§5) to reflect the three-block pipeline, shared collectors, and tag-fetch machinery; corrected v0.2.0 roadmap heading (recurrence shipped, strip horizon was not built — previously overclaimed) - New CHANGELOG.md: one entry per shipped version, 0.1.0 through 0.5.0 - README.md: fixed stale test count (47 -> 88), linked CHANGELOG, added a 'Cutting a release' checklist including the npm whoami / E404 auth-expiry gotcha hit across several publishes - No source changes; 88 tests still passing
27 KiB
GTD Calendar — Project Specification
Status: living document, current through v0.5.0. Sections 1–5 describe the shipped design; §9 tracks what's still ahead.
A fork of WeMakeMachines/joplin-plugin-event-calendar (MIT) that inverts the plugin's data model: instead of events living as YAML inside the calendar note, the plugin's views are populated by real Joplin notes and todos in the surrounding folder tree, with clickable drilldown to the source note. What started as a single calendar view has grown into three composable views — calendar, kanban, and a priorities matrix — sharing one data layer and one tag/date vocabulary.
Package name: joplin-plugin-gtd-calendar (npm)
Manifest id: com.victorwiebe.joplin.plugin.gtd-calendar
1. Concept
A note anywhere in a folder contains a ```gtd-calendar fenced block. That block renders as a Day/Week/Month calendar populated by notes and todos in scope. Items opt in (by default) via a ```gtd fenced block in their body. Clicking a calendar event opens the underlying note.
Dateless items are not lost — they collect in an Unscheduled section below the calendar grid, functioning as a GTD next-actions / someday-maybe bucket.
2. Schema Reference
2.1 The gtd-calendar block (lives in the calendar note)
```gtd-calendar
view: month # day | week | month | month-grid (default: day)
title: Editorial Schedule
scope: this-folder # this-folder | children | <integer depth> (default: this-folder)
notes: gtd-only # gtd-only | all | none (default: gtd-only)
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort: asc # asc | desc (default: asc)
sort-type: title # title | modified_date (default: title)
| Key | Values | Default | Notes |
|---|---|---|---|
view |
day, week, month (also d/w/m), month-grid (also grid) |
day |
Grouping granularity. day/week/month render the compact tile strip spanning all events; month-grid renders a classic 7-column grid of the entire current calendar month with weekday headers, dimmed adjacent-month days, today highlighted, and clickable event chips in each day cell. Events outside the displayed month are counted in a footnote. |
title |
string | none | Rendered as a heading above the calendar. |
scope |
this-folder, children, or an integer |
this-folder |
children = unlimited recursion into subfolders. An integer n = this folder plus n levels of descendants (0 ≡ this-folder). Documentation will caution that broad scopes scan every note body in the tree. |
notes |
gtd-only, all, none |
gtd-only |
Which plain notes participate. all includes notes without a gtd block (they receive default styling and are Unscheduled unless... see §3.2). |
todos |
gtd-only, all, none |
gtd-only |
Same semantics for todos. |
sort |
asc, desc |
asc |
Order of events within each day/week/month grouping and within Unscheduled. Groupings themselves always run chronologically. |
sort-type |
title, modified_date |
title |
Field used by sort. modified_date maps to Joplin's updated_time. |
unscheduled-todos |
yes/no (also true/false, show/hide) |
yes |
Whether the Unscheduled to-dos sub-section is shown. |
unscheduled-notes |
yes/no (also true/false, show/hide) |
yes |
Whether the Unscheduled notes sub-section is shown. Independent of unscheduled-todos — lets a dashboard split the two lists across stacked calendar blocks instead of duplicating both. |
Unscheduled items (no resolvable date) are split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or toggled off.
Unknown keys are ignored with a warning. A malformed block renders an inline warning above the calendar rather than failing silently.
2.2 The gtd block (lives in participating notes/todos)
```gtd
date: 2026-07-04 # required for notes to be scheduled; optional override for todos
bg-colour: teal # any CSS color value
fg-colour: white
icon: 🦴
title: Override title # optional; defaults to the note's own title
text: Hover text # optional; tooltip detail, as upstream
| Key | Applies to | Behavior |
|---|---|---|
date |
notes | Required for the item to appear in the grid; without it the note goes to Unscheduled. Accepts yyyy-mm-dd (and mm-dd-yyyy for upstream parity). |
date |
todos | Optional. If present, overrides todo_due (explicit beats implicit). |
bg-colour / bgColor |
both | Event tile background. Both spellings accepted; bg-colour is canonical for this fork. |
fg-colour |
both | Event tile text color (new vs. upstream). |
icon |
both | Emoji/short string, as upstream. Defaults: type indicator (see §4). |
title |
both | Overrides the note title on the tile. |
text |
both | Tooltip body. |
An empty ```gtd block is valid and meaningful: it is the opt-in signal with all defaults.
2.3 The gtd-kanban block (lives in the board note)
```gtd-kanban
title: Editorial Board
scope: this-folder # same semantics as gtd-calendar
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort-type: due-date # due-date | title | modified-date (default: due-date)
sort: asc # asc | desc (default: asc)
in-progress-tag: in-progress # default: "in-progress"
card-detail: hover # hover | always | none (default: hover)
done-window: 7 # days, or "all" (default: 7)
Three columns, always shown: Backlog, In Progress, Done. To-dos only — plain notes never appear. Bucketing: todo_completed > 0 → Done (wins regardless of tags); else the in-progress-tag → In Progress; else → Backlog. Backlog shows everything uncompleted and untagged, dated or not. Done is filtered to to-dos completed within done-window days (all for full history), preventing unbounded growth. Sorting is per-column; under due-date, cards without a due date sort after all dated ones regardless of direction. Read-only in the current version — reflects to-do state, does not change it.
2.4 The gtd-matrix block (lives in the matrix note)
```gtd-matrix
mode: skeleton # skeleton | eisenhower (default: skeleton)
title: Priorities
scope: this-folder
todos: gtd-only
sort-type: due-date
sort: asc
urgent-tag: urgent # default: "urgent"
important-tag: important # Eisenhower mode only; default: "important"
in-progress-tag: in-progress # Skeleton mode only; default: "in-progress"
urgent-window: 3 # Skeleton mode only, days; default: 3
card-detail: hover
A 2×2 grid with labelled axes. To-dos only; completed to-dos are excluded entirely in both modes (a prioritisation view, not a tracking view — the kanban's Done column is where completions live). The board is internally positional (top-left / top-right / bottom-left / bottom-right); each mode supplies its own axis and quadrant labels.
mode: skeleton (default) — rows = active (in-progress-tag, the same tag the kanban uses); columns = due soon (due date within urgent-window days, overdue included, or the urgent-tag as a manual override for dateless items). Quadrants: Do Next (active + due soon), Scheduled (active, not due soon — includes dateless in-progress work), On Deck (not active + due soon — the early-warning quadrant), Backlog (neither). Designed to complement the calendar and kanban directly: same in-progress tag, dates drive urgency instead of a second opinion-tag, and nothing is ever condemned as "Eliminate" — a Backlog item is simply low priority. In-progress + urgent-tag + no date resolves to Do Next (the urgent tag is an explicit override, so it wins over the absence of a date).
mode: eisenhower — the classic: rows = important-tag; columns = urgent-tag. Quadrants: Do First (both), Schedule (important only), Delegate (urgent only), Eliminate (neither).
A warning is raised if urgent-tag and important-tag resolve to the same tag (Eisenhower mode only — the axes would collapse). Read-only.
3. Data Model Rules
3.1 Date resolution
- Todos:
todo_duefrom the Joplin API. Adate:in thegtdblock overrides it. - Notes:
date:from thegtdblock only. No fallback to created/updated time — those are not event dates and would produce misleading calendars. - No resolvable date → item is rendered in the Unscheduled section below the grid.
3.2 Inclusion matrix
An item appears on the calendar iff its type's mode admits it:
| Mode | Has gtd block |
No gtd block |
|---|---|---|
gtd-only (default) |
✅ included | ❌ excluded |
all |
✅ included | ✅ included (default styling) |
none |
❌ excluded | ❌ excluded |
The calendar note itself is always excluded from results, as is any note whose only gtd-calendar block makes it a calendar (a note may, however, be both an event and contain a calendar if it has both block types — edge case, supported, documented).
3.3 Completed todos
v1 decision: completed todos (todo_completed > 0) render with strikethrough + checked indicator, and a future completed: show | hide calendar option is reserved. (Logged as decision D3 below; cheap to change.)
4. Rendering
- Calendar reuses upstream's grouping/renderer architecture (
DayGrouping/WeekGrouping/MonthGrouping+ DOM renderers + CSS asset), including the empty-grouping placeholders and current-date highlight, plus the fork's ownmonth-grid(a 7-column grid of the entire current month). - Fixed upstream bug:
Calendar/index.tsimportedWeekGroupingfromMonth/MonthGrouping, so the week view silently grouped by month. Corrected in the fork. - Type distinction: todos render with a checkbox glyph (☐ / ☑ when completed); notes render with a document glyph. Glyphs are suppressed if the item supplies its own
icon. Recurring to-dos (per therecurringtag) get a ↻ suffix to the right of any icon, across every view. - Unscheduled section: below the calendar grid, split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or switched off (
unscheduled-todos/unscheduled-notes). - Kanban renders three fixed columns (Backlog / In Progress / Done) of compact cards; a "hover" mode shows due date and hover text only on mouseover (
card-detail). - Matrix renders a 2×2 grid with axis and quadrant labels supplied per-mode by the plugin process; cards are visually and behaviourally identical to kanban cards.
- Drilldown: every tile, card, hover-card row, and chip across all three block types is clickable and opens the source note.
5. Architecture
Upstream is a single synchronous markdown-it content script with no data API access. The fork is a three-part plugin, now serving three fence types (gtd-calendar, gtd-kanban, gtd-matrix) through the same pipeline:
┌─────────────────────────────┐
│ 1. Markdown-it content │ Intercepts ```gtd-calendar / ```gtd-kanban /
│ script (renderer) │ ```gtd-matrix fences. Parses YAML config,
│ │ emits a placeholder <div> tagged with
│ │ data-block-type and the config as a
│ │ data attribute.
└──────────────┬──────────────┘
│ (rendered HTML)
┌──────────────▼──────────────┐
│ 2. Webview asset script │ Finds placeholders, calls
│ (runs in rendered note) │ webviewApi.postMessage(config) with
│ │ getEvents / getKanban / getMatrix
│ │ depending on block type. Builds the
│ │ calendar / board / matrix DOM from the
│ │ returned payload. Click → postMessage
│ │ ({type: "openNote", noteId}).
└──────────────┬──────────────┘
│ postMessage / onMessage
┌──────────────▼──────────────┐
│ 3. Main plugin process │ joplin.contentScripts.onMessage routes
│ (index.ts) │ by message type to a handler per block
│ │ type, each of which:
│ │ • resolves current note's folder
│ │ (joplin.workspace.selectedNote)
│ │ • builds folder tree, applies scope
│ │ (src/Gtd/folderScope.ts)
│ │ • fetches notes/todos, paginated
│ │ • parses ```gtd blocks from bodies
│ │ (src/Gtd/gtdBlock.ts)
│ │ • fetches tag titles per to-do
│ │ (getNoteTagTitles) for recurrence /
│ │ in-progress / urgent / important
│ │ detection
│ │ • applies the inclusion matrix, date
│ │ resolution, and view-specific
│ │ bucketing (collectEvents.ts /
│ │ collectKanban.ts / collectMatrix.ts)
│ │ • handles openNote via
│ │ joplin.commands.execute
└─────────────────────────────┘
All three collectors (collectEvents, collectKanban, collectMatrix) share folderScope.ts, gtdBlock.ts, and resolveDate.ts — the inclusion-matrix and date-resolution logic is written once and reused by every view.
Performance notes
- Folder tree and note list fetched per render request; paginated API calls (
page/has_more). - Body scanning is limited to items in scope. Kanban and matrix additionally fetch tag titles per to-do (one call each) for recurrence/state detection. For large scopes, pre-filter with Joplin's search API before fetching full bodies; cache by
updated_timeif needed — not yet required at observed real-world scales (largest tested: ~285 notes, single folder scope), but worth revisiting if achildren-scope board on a large tree feels slow, especially with Persistent Layout keeping several boards rendered at once. - Refresh model: re-render occurs on note switch (Joplin behavior); no live refresh on background data changes. Fine for a read-only plugin; becomes a sharper question once the authoring axis (§9) lands, where a write on one rendered board should ideally refresh others.
Platform caveats
- Desktop is the target. Mobile plugin support is partial and this plugin's core mechanisms (custom fence rendering, webview↔plugin messaging) are not expected to work there; documented explicitly in the README. Underlying to-dos still sync and appear in Joplin's native mobile views. The "Embed Search" plugin pairing remains a documented pattern for cross-scope aggregation generally, not specifically a mobile workaround.
6. Implementation Phases
- Phase 0 — Scaffolding. Fork, rename (manifest id, plugin name, file names), build pipeline verified (
npm install && npm run dist), upstream week-view bug fixed, upstream tests green. - Phase 1 — Plumbing. Placeholder rendering for
gtd-calendarfences; webview ⇄ plugin message round-trip proven with a hardcoded payload. - Phase 2 — Data layer. Folder tree + scope resolution; note/todo fetching;
gtdblock parsing; inclusion matrix; date resolution; unit tests for all of it (the upstream Jest setup carries over). - Phase 3 — Rendering. Wire real data into the reused renderers; Unscheduled section; type glyphs; fg-colour support; sort options.
- Phase 4 — Drilldown & polish. Click-to-open; error boxes for malformed YAML; docs/README; scope-performance warnings.
7. Decision Log / Open Items
| # | Decision | Status |
|---|---|---|
| D1 | Defaults: notes: gtd-only, todos: gtd-only |
⚠️ Confirm — earlier discussion said todos are always included (i.e. todos: all default); the final message specified gtd-only as default for both. Spec follows the final message. |
| D2 | gtd date: overrides todo_due (vs. emitting a warning) |
Adopted; revisit if confusing in practice. |
| D3 | Completed todos: shown struck-through in v1; completed: option reserved |
Proposed, unconfirmed. |
| D4 | sort/sort-type act within groupings only; grid is always chronological |
Confirmed. |
| D5 | Colour key spelling: bg-colour/fg-colour canonical, bgColor accepted for upstream parity |
Proposed. |
| D6 | Future options parked: completed:, tag-based filtering, week-start day, live refresh |
Backlog. |
8. Licensing & Attribution
Upstream is MIT (© Franco Speziali / WeMakeMachines). The fork retains the MIT license, preserves the original copyright notice, and credits upstream in the README per standard practice.
9. Roadmap (post-1.0 / post-launch)
Recorded direction, not commitments. Each gets a focused design pass before work begins; real-world use of the shipped plugin may reorder this.
v0.2.0 — Repeating to-dos (visualisation) — SHIPPED · strip horizon — NOT YET BUILT
Recurrence itself is delegated to an existing community plugin (open-environment approach; we don't reimplement it). Candidates evaluated: BeatLink's original Repeating To-Dos (unmaintained), the [Revived] fork (maintained, GitHub issues only), and Repeating To-dos v2 by TheScriptingGuy (com.github.TheScriptingGuy.joplin-plugin-repeating-todos, best-documented). All share the same mechanic: on completion the to-do's alarm date is reset to the next occurrence and the to-do is unmarked — so at any moment a recurring to-do is a single to-do at its next due date, with the recurrence rule stored in the note body.
- Option A (shipped in 0.2.0): detect recurrence and mark those events with a ↻ glyph. Implementation note: v2 of the plugin stores recurrence in Joplin's
userData(not the note body) and maintains arecurringtag as its query index — so detection is by tag presence on to-dos (constantRECURRING_TAG), not body-scanning. Cleaner than the originally-planned body parse, and reuses the tag-fetching the kanban work will need. No series projection — the to-do shows at its next due date. - Option B (in pocket): parse the recurrence rule and project all future occurrences across the visible window. Deferred because the body format has proven to be a moving target across forks; promote to a 0.2.x only if Option A's single-occurrence view proves insufficient, and target a specific fork's format at that time.
- README: recommend the maintained fork (v2 first, Revived as fallback); warn about the known "recurrence block injected into all notes" failure mode; note coexistence (their plugin moves the anchor, ours draws the marker).
Configurable strip horizon. day/week/month strip views currently span first-event to last-event (plus today). Add a horizon option to gtd-calendar (working name weeks: or horizon:, default ~8 weeks forward) controlling how far ahead the strip extends — relevant both generally and for how many future occurrences Option B would draw. month-grid is explicitly excluded: it always shows the entire current calendar month, by design, and that is not configurable.
Note: multiple gtd-calendar blocks in one note already work as of 0.1.0 (each fence renders independently) — e.g. a "week ahead" strip plus a "month grid" in a single dashboard note. No work required; document as a recommended pattern.
v0.3.0 — Kanban board (read-only) — SHIPPED
Decision: a second block type within this plugin, not a separate plugin. Rationale: a kanban needs the entire existing data layer (folderScope, collectEvents inclusion matrix, getNoteTagTitles, config parsing, recurrence detection, webview messaging); a separate plugin would duplicate all of it or require a shared-library split whose overhead isn't justified for "three tag-driven columns." One plugin also serves the intended use — a calendar and a kanban in one dashboard note — with a single install and a unified config/tag vocabulary.
The gtd-kanban block. Parallel to gtd-calendar; lives in its own note or stacked beneath a calendar (multiple blocks already render independently).
```gtd-kanban
title: Editorial Board
scope: this-folder # this-folder | children | N — same semantics as gtd-calendar
todos: gtd-only # gtd-only (default) | all | none
sort-type: due-date # due-date (default) | title | modified-date
sort: asc # asc | desc
in-progress-tag: in-progress # configurable; default "in-progress"
card-detail: hover # hover (default) | always | none
done-window: 7 # days; integer or "all"; default 7
Columns (always three, always shown even when empty): Backlog · In Progress · Done.
Bucketing rules:
- Done wins. Any to-do with
todo_completed > 0→ Done, regardless of tags. - Else, carrying the in-progress tag → In Progress.
- Else → Backlog.
- Backlog shows everything uncompleted and untagged — dated or not, near or far. (Acknowledged: can grow long; compact cards mitigate.)
Sorting (within each column):
sort-type: due-date | title | modified-date, withsort: asc | desc.- Under
due-date, to-dos without a due date sort after all dated ones.
Done column window: shows only to-dos completed within done-window days (default 7); all shows the full history. Prevents unbounded growth.
Cards: compact by default — title, due date, ↻ if recurring, click-to-open. card-detail controls the hover panel: hover (default), always (details inline), none (no panel). Notes are excluded entirely (to-dos only).
Read-only in 0.3.0: reflects state, never mutates it. Proves columns/bucketing cheaply before write-back.
Architecture: reuses the calendar's pipeline. New gtd-kanban markdown-it fence → placeholder → webview requests data → plugin runs scope + inclusion + tag fetch (already built) + bucketing → returns three columns → webview renders. IN_PROGRESS_TAG constant mirrors RECURRING_TAG, exposed via in-progress-tag: config from the start.
sort-type vocabulary reconciliation: the calendar block's sort-type (currently title | modified_date) is unified toward due-date | title | modified-date across both blocks so they read consistently. For the calendar, due-date is accepted but falls back to chronological where less meaningful. (Note the spelling shift modified_date → modified-date for cross-block consistency; the old spelling stays accepted as an alias to avoid breaking existing calendars.)
v0.4.0 — Eisenhower Matrix (gtd-matrix block) — SHIPPED
v0.5.0 — Skeleton Matrix (mode: on gtd-matrix) — SHIPPED
Real-world feedback: Eisenhower's opinion axes (and "Eliminate" in particular) fit triage, not a vault of already-committed work. The Skeleton Matrix replaces them with two derived axes the system already maintains: rows = active (in-progress-tag, shared with the kanban); columns = due soon (urgent-window days, default 3, overdue included; urgent tag as manual override — in-progress + urgent + dateless is Do Next by design). Quadrants: Do Next / Scheduled / On Deck / Backlog; dateless active work → Scheduled; nothing is ever "Eliminate". Implemented as mode: skeleton | eisenhower on the same block — positional MatrixBoard (topLeft…bottomRight), labels supplied per mode via payload, shared rendering. Skeleton is the default (it matches the plugin's own tag/date vocabulary); classic Eisenhower preserved via mode: eisenhower.
A fourth display block: a 2×2 prioritisation matrix, tag-driven like the kanban. Chosen ahead of drag-and-drop deliberately: it is on the views axis (another rendering of tagged to-dos, reusing the whole data layer, read-only) rather than the authoring axis (vault mutation), so it deepens what the plugin already is without committing to write-back.
- Quadrants from two configurable tags (
urgent-tag:defaulturgent;important-tag:defaultimportant):- urgent + important → Do First
- important only → Schedule
- urgent only → Delegate
- neither → Eliminate (the default bucket for untagged to-dos — a deliberately pointed default)
- Completed to-dos are excluded entirely — the matrix is a prioritisation view, not a tracking view; kanban's Done column is where completions live.
- To-dos only; same
scope/todos/sort-type/sort/card-detailoptions as the kanban; same compact clickable cards with ↻. - Own block (
gtd-matrix), not a kanban mode — different layout, different tag semantics.
v0.5.0+ — The authoring axis (drag-and-drop + create-from-view) — one deliberate epic
Clustered because they share write-back machinery, and adopting any of them commits the plugin to mutating the vault:
Drag-and-drop kanban (write-back). The first feature making the plugin a controller of data rather than a viewer — warrants its own design pass.
- Dragging a card writes back to Joplin via the existing webview→plugin channel:
- → In Progress: add the
in-progresstag (create the tag if absent; resolve its id; associate). - → Done: set
todo_completed. - → Backlog: remove the tag / clear completion.
- → In Progress: add the
- Open design questions for that pass: optimistic UI vs. wait-for-confirmation; behaviour on write failure; whether drops need undo; re-query/refresh of kanban and any affected calendars after a successful write; the trust implications of a rendered view mutating other notes.
- Feasibility: confirmed possible (we already execute
openNotefrom the webview; data writes use the same channel with more responsibility). Complexity lives in tag management and the write/refresh cycle, not the dragging. - Multi-board refresh: with Persistent Layout keeping several dashboards rendered, a write from one board leaves others stale until re-render — the refresh question is sharper in the fractal-dashboard architecture than for single-board use.
Create-to-do from kanban. A "+" affordance per column creates a to-do with that column's state (In Progress column → in-progress tag; Backlog → no tag). Easier than create-from-calendar: the column is the metadata, no date geometry. Open: target notebook (leaf dashboards obvious, cockpit murky), trigger reliability in the rendered viewer.
Create-to-do from calendar. Click/affordance on a day cell creates a to-do due that day. Open questions: trigger mechanism in the read-only viewer (double-click/right-click may be claimed by Joplin; per-cell "+" affordance likelier), target notebook rule, whether created to-dos get an empty gtd block so they appear under todos: gtd-only. Prototype the trigger before designing the rest.