Omahub
← All plugins
T

Calendar

by tmn73

Your Google Calendar in the bar. Your day at a glance, one click to join a meeting, and events you create and edit without leaving the desktop.

Security review

Potentially dangerous behavior detected · 3 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
15509fc
Scanned
1 week ago

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
15509fc
Reviewed
1 week ago

The plugin is a calendar widget that syncs Google Calendar events via gws or EDS, with optional write support. The systemd timer and service units are a legitimate periodic sync mechanism, and the sudo command in the README is only for installing optional packages for an alternative backend, not part of the plugin's own installation. No obfuscated, destructive, or credential-harvesting code was found in the sampled files.

  • The sync/setup script is not sampled; it automates gcloud and gws setup, which may prompt for user credentials and modify Google Cloud configurations, but it is explicitly user-initiated and documented.
  • The systemd timer runs a sync command every 5 minutes; this is a normal periodic task but adds a persistent service.
  • The README suggests installing evolution-data-server with sudo for an optional backend, which is a package install that the user must explicitly choose.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/tmn73/omarchy-calendar --enable
Widgets #bar #quickshell #system

Calendar for Omarchy

Your Google Calendar, in your Omarchy bar. A month view with your real events on it, a bar that tells you what is coming before it starts, and a form to create or edit events without opening Google.

Not a Google user? It reads a plain JSON file, so khal, vdirsyncer, Nextcloud or an ICS feed work just as well. See Use another source.

Preview

It replaces the built-in clock rather than sitting beside it, so you keep one icon. Left click opens a month calendar with your real events on it. When something is close, the bar itself stops being just a clock and tells you:

The bar announcing the next event

The clock stays. This widget takes the desktop clock's place, so trading the time away for an event title would be a downgrade you pay for all day.

Features

  • Month grid with ISO week numbers, coloured dots per calendar
  • The selected day's agenda under the grid, click any day to see it
  • Today's agenda reads as a timeline: a line at the current time, past events faded, the event in progress highlighted, and a countdown on the next one
  • Create, edit and delete events from the panel (press n), with a form like Google's: date picker, times in 15-minute steps, repeat, guests with suggestions, Google Meet, notifications, colour. Off by default, see Create and edit events
  • The bar label announces what is next, minutes before it starts
  • A Join button on meetings that have a video link, shown only from 15 minutes before the start until 15 minutes after the end
  • Clicking any event opens it in your calendar
  • Per-calendar visibility, week start, countdown lead time and a 24 h or 12 h time format
  • Google's working-location markers hidden by default, declined invitations struck through
  • Everything the built-in Omarchy clock does: label formats, right click to cycle them, the year and life progress bars if you want them back
  • Theme aware, light themes included, because it is a fork of the built-in clock
  • No Google Cloud project needed if you read your calendars through Evolution Data Server (community-maintained), or any other source that writes the events file

Requirements

Omarchy 4 with Quickshell. Google Calendar is optional, see Use another source. A Google Cloud project is optional too, see Sync without a Google Cloud project.

Install

omarchy plugin add https://github.com/tmn73/omarchy-calendar.git --enable

This widget replaces the built-in clock. In ~/.config/omarchy/shell.json, remove the omarchy.clock entry from bar.layout.center and point bar.centerAnchor at tmn73.calendar:

{
  "bar": {
    "centerAnchor": "tmn73.calendar",
    "layout": {
      "center": [
        {
          "id": "tmn73.calendar",
          "format": "dddd HH:mm",
          "eventTimeFormat": "HH:mm"
        }
      ]
    }
  }
}

Then:

omarchy restart shell

Installing is not the whole job. At this point you have a working clock and an empty calendar, because nothing is feeding it yet. Connect Google Calendar below, or point any other source at the file. The widget says as much when you open it, with the command to run.

Sync your Google Calendar

~/.config/omarchy/plugins/tmn73.calendar/sync/setup

Run it in a real terminal. It pauses for input, and four steps have to be done by hand in the Google Cloud Console.

You need your own Google OAuth client. There is no shared one, and that is not laziness. calendar.readonly is a Google sensitive scope, so a publicly distributed client would need Google verification and is capped at 100 users until it gets it. This is exactly why gcalcli's shared token is currently restricted. Every user brings their own credentials.

The script automates what has an API:

  • an isolated gcloud configuration, so your other projects are untouched
  • creating the Google Cloud project
  • enabling the Calendar API
  • installing the downloaded client secret, with the right permissions
  • the scoped login, and verifying the scope was actually granted
  • the systemd timer

It stops and waits for the four things Google exposes no API for: the consent screen, declaring the calendar scope, publishing the app, and creating the Desktop OAuth client. Each one prints the exact URL and the exact values.

Two of those steps are traps, and the script says so at the time:

  • Declaring the scope under Data Access is not optional. A scope that is not declared there is never offered on the consent screen, so there is no box to tick, Google silently grants only your email address, and every sync then fails with 403 insufficient scopes while the login reports success.
  • Publish the app. While it sits in Testing, Google expires refresh tokens after seven days and your calendar quietly stops updating. Unverified production apps show a one time warning screen and then work indefinitely.

When it finishes, events land in ~/.local/state/omarchy/calendar-events.json every five minutes and the widget picks them up without a restart.

Create and edit events

Off by default. Turn it on and the panel gets a + next to the day's agenda (or press n), and a pencil and a trash can when you hover one of your own events.

It needs one more Google scope, calendar.events. That scope can see and edit events. It cannot change sharing or delete a calendar.

New setup: answer yes when sync/setup asks, or run sync/setup --write.

Already set up:

  1. In the Cloud Console, under Data Access, add calendar.events next to calendar.readonly.

  2. Log in again with both scopes. The rm drops the cached token, which gws would keep serving without the new scope:

    rm -f ~/.config/gws-omarchy-calendar/token_cache.json
    GOOGLE_WORKSPACE_CLI_CONFIG_DIR=~/.config/gws-omarchy-calendar gws auth login \
      --scopes https://www.googleapis.com/auth/calendar.readonly,https://www.googleapis.com/auth/calendar.events
    
  3. Set "write": true in ~/.config/omarchy/calendar-sync.json.

Only calendars you own or can edit get the pencil and the trash can. The form follows Google's: start and end dates with a date picker, times in 15-minute steps in your eventTimeFormat, all day, repeat, guests, Google Meet, location and description. Under "More options": notifications, busy or free, visibility, colour and guest permissions.

With guests, the panel asks whether to send invitation emails. For a recurring event, it asks whether the change is for this event or all events. To move an event to another calendar, use Google Calendar. A repeat rule the menu cannot show, or a notification it has no entry for, stays as it is unless you pick another one.

The EDS backend cannot write yet, so the panel shows none of this there.

Sync without a Google Cloud project

Community-maintained. The author does not run Evolution Data Server, so the people who use this backend are the ones who test it. When you open an issue about it, say that you are on the EDS backend.

The setup above needs a Google Cloud project because calendar.readonly is a Google sensitive scope, so a publicly distributed client would need verification. There is a way around that: read the calendars out of Evolution Data Server, which signs in with GNOME's already-verified OAuth client. No project, no consent screen, no scope declaration, no client_secret.json, and no Testing-mode refresh token expiring after seven days. Just a browser sign-in.

The trade is roughly 20 packages, and a GUI is needed once to sign in.

sudo pacman -S evolution-data-server evolution

Describe the account in ~/.config/evolution/sources/google.source:

[Data Source]
DisplayName=Google (you@example.com)
Enabled=true
Parent=

[Collection]
BackendName=google
Identity=you@example.com
CalendarEnabled=true
ContactsEnabled=false
MailEnabled=false

[Authentication]
Method=Google
User=you@example.com
Host=www.google.com
RememberPassword=true

Writing that by hand is not laziness either. Evolution's Collection Account wizard resolves a custom Workspace domain to Google's mail servers and then asks for a password to discover CalDAV, rather than reusing the Google OAuth2 provider it already ships. It offers no calendar at all, and the wizard is a dead end. The file skips it.

Then sign in once:

evolution -c calendar

Its credential prompter opens Google's sign-in, EDS discovers the calendars over CalDAV, and the refresh token goes to your keyring. Evolution is not needed again unless Google later wants a re-auth and needs a window to ask in; the data server keeps running headless.

Finally point the sync at it in ~/.config/omarchy/calendar-sync.json:

{
  "backend": "eds",
  "identity": "you@example.com"
}

identity is the address whose invitation answers count as yours, which is what makes "hide declined events" work. Leave it out and responseStatus is left unset rather than guessed from the first attendee.

The same systemd timer drives it, calendars.include/exclude and window behave identically, and the file written is the same contract, so switching backends changes nothing the widget can see.

One known limit: clicking an event opens nothing on this backend, because CalDAV does not carry Google's link to the event.

Use another source

The widget has no idea Google exists. It reads one file and renders it:

~/.local/state/omarchy/calendar-events.json

Anything that writes that file works: khal, vdirsyncer, Nextcloud, an ICS feed, a shell script, a cron job of your own. No credentials, no network, no gws.

{
  "version": 1,
  "syncedAt": "2026-08-10T16:42:00+00:00",
  "source": "whatever produced this",
  "events": [
    {
      "id": "any-stable-id",
      "calendarId": "work@example.com",
      "calendarName": "Work",
      "color": "#f83a22",
      "dateKey": "2026-08-10",
      "start": "2026-08-10T19:15:00-05:00",
      "end": "2026-08-10T20:15:00-05:00",
      "allDay": false,
      "title": "Tax filing",
      "location": ""
    }
  ]
}

These extra fields are optional. Omit them and everything still works:

Field Effect
meetingUrl Shows the Join button around the event's time. Must be https, anything else is dropped
eventUrl Clicking the row opens this. Must be https
eventType workingLocation is hidden by default, outOfOffice is labelled
responseStatus declined is struck through, and can be hidden entirely

A top-level writableCalendars list (id, name, color) turns on the panel's edit buttons for those calendars. Only the bundled sync should write it: the panel sends its edits to the bundled event command, not to your writer.

Rules a writer has to follow:

  • dateKey is YYYY-MM-DD in local time, and it is what the grid keys on.
  • A multi-day event is emitted once per day it covers, each row with its own dateKey. Those rows share an id, so the unique key for a row is id + dateKey.
  • allDay events are excluded from the countdown, since counting down to midnight tells you nothing.
  • Write the file atomically, temp file then rename. The widget watches it.
  • Unknown fields are ignored, so you can add your own.

tests/fixtures/calendar-events.json is a valid two-event file to start from.

Writers other people have built:

  • Thunderbird by @marijn070. A Nushell script that reads Thunderbird's local calendar, so every source you already aggregate in Thunderbird shows up in the widget.

Settings

Click the clock, then the gear icon in the panel header.

The settings page

Section What it does
Calendars Show or hide each calendar. The list comes from your own events, so it needs no configuration
Week starts on Monday Off starts the week on Sunday
Working location events Google's work-from-home markers. Hidden by default because they are all-day rows describing no commitment
Declined invitations On lists them struck through, off hides them entirely
Year and life progress Brings back the built-in clock's bars, off by default
Bar label How early the bar announces what is next: never, 5, 15, 30 or 60 minutes
Event times Set eventTimeFormat in shell.json to a Qt date-time format such as HH:mm or h:mm AP
Sync Event count, source and last sync time, for diagnosing a quiet calendar

Hiding a calendar is instant and does not change what the sync fetches, so bringing one back does not wait for the next run.

Sync behaviour lives in ~/.config/omarchy/calendar-sync.json:

{
  "profile": "~/.config/gws-omarchy-calendar",
  "gwsPath": "/absolute/path/to/gws",
  "calendars": { "include": [], "exclude": [] },
  "window": { "pastDays": 7, "futureDays": 60 }
}

include: [] means all of them. Names and ids both match. gwsPath has to be absolute: a systemd user service does not inherit your shell's PATH, so a gws installed by bun, cargo or pipx is invisible to it under its bare name.

Troubleshooting

journalctl --user -u omarchy-calendar-sync -f
systemctl --user list-timers omarchy-calendar-sync.timer
Symptom Cause
403 insufficient scopes The calendar scope was never granted. Check gws auth status; if it only lists openid and email, declare the scope under Data Access in the console, then run sync/setup again
401 invalid_grant The refresh token expired. Almost always an app left in Testing, which caps refresh tokens at seven days. Publish it, then log in again
gws is not installed or not on PATH from the timer, but it works in your terminal gwsPath is not absolute. sync/setup writes it for you
"Write access not granted" in the event form The token has no calendar.events scope. See Create and edit events
"Google refused the change: Shared properties can only be changed by the organizer" The event is an invitation. Only its organizer can change its title, time or guests. Your own colour, notification and busy or free still change
cannot parse gws version from the timer, with exec: node: not found gwsPath is absolute but points at an npm wrapper that needs node on your shell PATH. With mise, use its shim: ~/.local/share/mise/shims/gws. sync/setup checks this and records the shim for you
Not in a workspace during setup, or setup says gws is not the Google Workspace CLI Another program named gws comes first on your PATH, for example the git workspace helper. Pass the right one: GWS=/absolute/path/to/gws sync/setup
The panel says "No calendar synced yet" The events file does not exist. The sync has never completed
The panel says the calendar may be out of date The file exists but syncedAt is old. Check the journal above
An event shows up twice Two of your calendars both carry it. Hide one in settings. The sync already drops exact duplicates by iCalUID and start time
The project ID you specified is already in use during setup Fixed in 0.1.1. Google Cloud project ids are unique across all of Google, and older versions hardcoded one. Update the plugin, or pass your own: PROJECT_ID=something-unique sync/setup
Clicking an event opens your calendar but not the event The link resolves only for the Google account the sync authenticated as. If your browser opens it in a profile signed into a different account, Google falls back to the calendar root. Route google.com/calendar to the profile holding that account
The Join button never appears It only shows from 15 minutes before the start until 15 minutes after the end, and only when the event has a video link
Events are off by a day Report it. Timezone handling resolves a named IANA zone precisely to avoid this, and there is a regression test for daylight saving transitions

Uninstall

systemctl --user disable --now omarchy-calendar-sync.timer
rm ~/.config/systemd/user/omarchy-calendar-sync.{service,timer}
systemctl --user daemon-reload
omarchy plugin remove tmn73.calendar

Then put omarchy.clock back in shell.json and omarchy restart shell.

Your Google credentials live in the gws profile directory and are not touched by any of this. Delete that directory to revoke locally, and remove the project from your Google Cloud console to revoke properly.

Development

cd sync && PYTHONPATH=. python3 -m unittest discover -s ../tests -t .. -v
node --test tests/model.test.js

No dependencies, no dev dependencies. The Python sync is standard library only and the QML logic lives in Model.js, which loads under Node precisely so it can be tested.

Panel.qml and BarWidget.qml are not unit tested. Quickshell widgets need a live shell to render, and building that harness would cost more than it catches. Anything worth testing was deliberately pushed down into Model.js.

License

MIT. Derived from Omarchy's built-in clock plugin, whose copyright notice is kept in LICENSE.