Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Notion Calendar for the Omarchy bar

A bar widget that behaves like the Notion Calendar menu bar item on macOS: the next meeting with a live countdown, a click-through agenda for the day, one-click join for video calls, and a nudge before things start.

󰃭 Design review · 12m

Read this first. This plugin works by parsing Notion Calendar's private, undocumented local database. That is the only way to get at the data — there is no public API — but it means an app update can break it without warning. If you want something that cannot break, use a calendar with a real API. Run bin/notion-cal doctor when it goes quiet.

It reads local files only. No credentials, no API keys, no network calls of any kind — the sole exception being xdg-open handing a meeting URL to your browser when you click Join.

Requirements

  • Omarchy 4.x (Quickshell-based shell)
  • The Notion Calendar Linux app — e.g. notion-calendar-electron-fixed from the AUR
  • Python 3 (standard library only; nothing to pip install)
  • A Nerd Font in the bar, for the glyphs

Install

git clone https://github.com/ngurney/omarchy-notion-calendar \
  ~/.config/omarchy/plugins/ngurney.notion-calendar

The directory name must match the id in manifest.json. Then add it to a bar section in ~/.config/omarchy/shell.json:

{ "id": "ngurney.notion-calendar" }

The shell picks it up on save. If it doesn't appear, see Troubleshooting.

Where the data comes from

Notion Calendar has no public API and no Linux-native data export. What it does have is a local store: the Electron app (notion-calendar-electron-fixed) keeps every synced event in a Chrome IndexedDB database at

~/.config/Notion Calendar/IndexedDB/https_calendar.notion.so_0.indexeddb.leveldb

This plugin reads that store directly. The upside is real: it sees exactly what the app sees, across every linked account and provider. On this machine that is Google and Outlook — pulling from the Google Calendar API alone would silently miss more than half the events.

Getting at it takes three layers, all implemented from scratch in lib/ncread.py with nothing but the Python standard library:

Layer Why it's needed
Snappy decompression LevelDB compresses its data blocks
LevelDB SST + WAL parsing Chrome uses a custom idb_cmp1 key comparator, so stock LevelDB bindings refuse to open the store at all
V8 structured-clone decoding Values are serialized by Blink, not stored as JSON

The store is snapshot-copied to a temp directory before reading, so the app's own database is never touched or locked.

The one real caveat

The store only advances while Notion Calendar is running. Close the app and the data freezes at whatever it last knew.

This is the same bargain macOS makes — the menu bar item exists because the app is running — but on Linux it is easy to have the app closed and not realize it. So the widget never pretends: it tracks how old the data is and says so, in the tooltip and as a banner in the panel, instead of showing a confident countdown to a meeting that may have moved.

To keep it fresh, leave Notion Calendar running. To start it with your session:

-- ~/.config/hypr/autostart.lua
o.exec("notion-calendar")

Interactions

Action Result
Left click Today's agenda panel (next active day if today is done)
Right click Join the next meeting's call, or launch the app if there is none
Middle click Force a refresh
j in panel Join the soonest call
o in panel Open the Notion Calendar app
r in panel Refresh
Esc Close

Clicking a row joins its call if it has one, otherwise opens the event.

Behaviour worth knowing

  • Cancelled and declined events are hidden. Outlook in particular leaves Canceled: ... tombstones in the store; these are dropped.
  • Duplicates are collapsed. One meeting mirrored across linked accounts often shows up several times, usually as opaque Busy blocks on the others. Events are merged only when they share a time slot and either a UID, a title, or one side is a privacy placeholder — two genuinely different meetings at the same time both survive.
  • All-day events are kept out of the bar countdown (they would pin it to "now" all day) but still appear in the panel.
  • Parsing is mtime-gated. A full parse takes ~1.4s, so it only runs when the store actually changes; polling reads a small JSON cache instead.

Configuration

~/.config/notion-cal-bar/config.json — all keys optional:

Key Default Meaning
leadMinutes 5 How far ahead to fire the meeting notification
notify true Desktop notifications on/off
hideDeclined true Drop events you declined
hideCancelled true Drop cancelled events
hideAllDay true Keep all-day events out of the bar label
excludeCalendars ["en.usa#holiday@group.v.calendar.google.com"] Calendar IDs to ignore
excludeSummaries [] Exact titles to ignore
staleWarnMinutes 90 When to start warning that data is old
lookaheadDays 14 How far ahead to keep events cached

Troubleshooting

~/.config/omarchy/plugins/ngurney.notion-calendar/bin/notion-cal doctor

Reports whether the store was found, when it was last written, whether the app is running, how long a parse takes, and the next few meetings.

Other commands: status, agenda, refresh --force, join [id], open [id], launch, notify.

Nothing shows in the bar, or the widget looks stale after an edit

Quickshell caches compiled QML, and that cache is not reliably invalidated when plugin files change. Hot-reload and omarchy-shell shell rescanPlugins will happily keep running the previous version of Widget.qml / Panel.qml — including reporting errors at line numbers that no longer exist. After editing either file:

rm -rf ~/.cache/quickshell/qmlcache && omarchy restart shell

The cache is regenerable; deleting it is safe.

Verifying a panel actually opened

QML bindings that are simply missing produce no warning at all, so a clean log is not proof the UI works. Check the compositor for the panel's layer surface instead:

hyprctl layers -j | grep -c omarchy-keyboard-panel   # 1 while open, 0 while closed

The countdown is stuck or wrong

Almost always means Notion Calendar isn't running — see the caveat above. The widget dims and the panel shows a banner when data is stale.

Fragility

This reads a private, undocumented store. A Notion Calendar update that changes its schema, or a Chrome update that changes the serialization format, can break it. doctor is the first thing to run when it goes quiet — if the store parses but yields no events, the schema moved.

lib/ncread.py is self-contained and has no dependency on the rest of the plugin. If you only want the Chrome IndexedDB reader — raw Snappy, LevelDB SST/WAL, and V8 structured-clone decoding in pure stdlib — take that file.

License

MIT. See LICENSE.

About

Notion Calendar in the Omarchy bar: next meeting, live countdown, today's agenda, one-click join. Reads the app's local store — no API keys, no network.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages