Calendar Tasks

unlisted

by Matthew Woo

Tag a line with a date, then see it on a sidebar calendar.

Updated 17d ago0BSD
View on GitHub

Calendar Tasks

An Obsidian plugin. Tag a line with a date, then see that line on a sidebar calendar.

What it does

Type @ on any line and the four commands appear:

CommandWhat it writesWhat it does
@today2026-09-03inserts straight away
@tomorrow2026-09-04inserts straight away
@date2026-09-24opens the calendar to pick a day
@due(by:: 2026-09-24)opens the calendar to pick a deadline

Keep typing to narrow them: @t leaves today and tomorrow, @d leaves date and due. The @ has to start a line or follow a space, so email addresses are left alone.

@today and @tomorrow show the date they will insert, and Enter writes it. @date and @due open a calendar at the cursor: click a day, press a quick button, or use the arrow keys and Enter.

You can also type the day instead of clicking it. Each of these moves the highlight, ready for Enter:

You typeYou get
@date todaytoday's date
@due tomorrowtomorrow's date
@date mondaythe next Monday
@due +3d3 days from today
@date +1w1 week from today
@due 2026-09-24that exact date

Arrow keys move a day left and right, a week up and down. PageUp and PageDown change month. Escape closes the popup.

Every date renders as a chip — a rounded pill, like Confluence. The text underneath never changes, so the date stays plain text you can select and search. Click a chip and the sidebar calendar jumps to that day. In the editor the text cursor still lands where you clicked, so a chip edits like ordinary text.

The sidebar calendar shows a month grid with a dot per item on every day that has them, up to three. A deadline gets a red dot, drawn first, so a busy day still shows it. The selected day drops its dots. Click an item to open that note at that line.

The calendar stays put and only the list below it scrolls, so the month is always in view. Each item is trimmed to a readable summary: inline fields like (product:: …), markdown links, and formatting are stripped, and long lines are clamped to two lines.

Below the grid sits one of two lists:

  • Nothing selected — the week ahead. Today plus the next six days, soonest first, grouped by date. Days with nothing on them are left out. A deadline carries a flag.
  • A day selected — that day only, with deadlines in their own Due section.

Click a day to select it. Click the same day again to clear it and go back to the week ahead.

Settings

A date in an inline field always counts. The other two sources are noisier, so each has its own switch, both off by default.

Count bare dates. Picks up a date written plainly in a line, with no field around it. Off by default, because it also catches every date mentioned in passing and every [[2026-09-15]] link.

Count dates in headings. A dated heading covers its whole section, sub-headings included, and stops at the next heading of the same level or higher. Turn this on when a heading is a release date:

## 2026-09-24

### Native Speech Analyzer

- Speech Analyzer built into the student app (staged:: 2026-08-17)

That item lands on both days: 17 August because it was staged then, and 24 September because that is when it ships. Each date keeps its own meaning, and a heading date the line already names is not repeated.

The catch is that a daily note titled # 2026-09-15 dates everything it holds. Exclude that folder below if you turn headings on.

Excluded folders. A list of folders to leave out of the scan entirely.

Where dates can go

On the line itself. Any list line, bullet or task:

- [ ] Send the invoice 2026-09-10

Under a heading, when Only count dates in inline fields is off. A dated heading then covers the list directly under it, so a daily log needs the date once:

## 2026-09-03

- [ ] task 1
- [ ] task 2

Prose here. The blank line above already ended the block.

- [ ] this one has no date

The block stops at the first blank line after the list. Even so, a daily note titled # 2026-09-15 pulls in the list under its title, which is why headings only count under the looser rule.

Twice on one line. Two dates on a line put it on both days, and the item reads the same on each. If a bare date and a by:: land on the same day, the item appears once, as due.

As a day and a deadline. A bare date means "this is the day". (by:: …) marks a deadline, so a line can hold both:

- [ ] Ship the first build 2026-09-21 (by:: 2026-09-24)

That line appears on both days. The bare date sits with the day's other items, because the day heading already says which day it is. Deadlines get their own Due section, with a flag on the right of each row.

There is no when::. A bare date already says "this is the day", so the field would cost markup and change nothing you can see. Old notes that use it still work: the field is stripped from the text, and the date inside counts as an ordinary date.

Only list lines count — bullets, numbered items, and task checkboxes. A date in a paragraph is ignored, so ordinary prose never clutters the calendar.

Build it

npm run dev is not a server. It is a watcher that compiles the TypeScript in src/ into a single main.js, because Obsidian can only load plain JavaScript.

Install once:

npm install

Start the watcher. Leave it running; it rebuilds on every save:

npm run dev

In Obsidian, choose Open folder as vault and pick test-vault inside this repo. The plugin is already switched on there. After each rebuild, reload with Cmd+R.

Build once, without the watcher:

npm run build

Install it in a real vault

Only three files ship: main.js, manifest.json, and styles.css. No Node, no watcher, no build step on the other machine.

npm run build

Copy those three files into <your vault>/.obsidian/plugins/calendar-task/, then switch the plugin on in Settings → Community plugins.

Publish it

Only three files ship: main.js, manifest.json, and styles.css.

A GitHub Action in .github/workflows/release.yml does the build. Push a tag and it compiles the plugin and opens a draft release with those three files attached:

git tag 0.1.0 && git push origin 0.1.0

The tag must match the version in manifest.json exactly, with no v in front: 0.1.0, not v0.1.0. Publish the draft release on GitHub when you are ready.

Anyone can then install it with the BRAT plugin by pasting the repo name.

Submit it to the community directory

  1. Make the repo public. A private repo cannot be submitted.
  2. Publish a release, as above.
  3. Go to community.obsidian.md, sign in with your Obsidian account, and link your GitHub account.
  4. Add the plugin there. Obsidian reads manifest.json from the default branch.
  5. An automated review runs. Fix anything it flags, then publish a new release with the version bumped, until it passes.

This replaced the old process of opening a pull request against obsidianmd/obsidian-releases.

The test vault

test-vault/ is a throwaway vault for testing only. Its plugin folder is a symlink back to this repo, so a rebuild lands in the vault straight away. Never point this at a real vault.

The vault is not in git, because that symlink points at its own parent. Recreate it after a fresh clone:

mkdir -p test-vault/.obsidian/plugins && ln -sfn ../../.. test-vault/.obsidian/plugins/calendar-task && echo '["calendar-task"]' > test-vault/.obsidian/community-plugins.json

Then open test-vault in Obsidian with Open folder as vault.

Code map

FileJob
src/main.tsEntry point. Wires the pieces and listens for changes
src/parser.tsDate maths and line parsing. Pure functions
src/index.tsScans the vault and keeps the results keyed by date
src/suggest.tsThe @ popup: trigger, state, keyboard
src/popup.tsDraws the command list and the calendar picker
src/commands.tsThe four @ commands
src/chip.tsRenders dates as chips, in the editor and reading view
src/view.tsThe sidebar calendar
src/settings.tsThe settings tab and the excluded-folder list
styles.cssAll styling, using Obsidian's theme variables

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.