Skip to content

feat: calendar integration (Google Calendar + CalDAV) - #142

Open
flathead wants to merge 5 commits into
Axenide:devfrom
flathead:feature/calendar-integration
Open

feat: calendar integration (Google Calendar + CalDAV)#142
flathead wants to merge 5 commits into
Axenide:devfrom
flathead:feature/calendar-integration

Conversation

@flathead

@flathead flathead commented Apr 1, 2026

Copy link
Copy Markdown

Overview

Adds a full calendar integration to Ambxst — a Python-backed service with QML frontend supporting Google Calendar (OAuth2) and CalDAV accounts.

This is a personal implementation that grew organically, so it is almost certainly not production-ready and likely needs significant rework before it would fit this codebase well. It is submitted as a reference / starting point rather than a finished feature. See the Known Issues section at the bottom.


What's included

Backend — scripts/calendar_service.py

A standalone Python process that communicates with QML over stdin/stdout using a line-delimited JSON protocol. Responsibilities:

  • Accounts: Google Calendar via OAuth2 (google-api-python-client), CalDAV via the caldav library, import of existing gcalcli tokens
  • Sync: background thread, configurable interval, per-calendar enable/disable
  • Notifications: reminders and arrival alerts via notify-send; arrival sound via canberra-gtk-play with paplay / pw-play / aplay fallback chain
  • CRUD: create, update, delete events with cmd_start / cmd_result acknowledgement protocol so the UI can show operation state
  • Persistence: notification deduplication across restarts (~/.cache/ambxst/calendar_notified.json), token files written atomically with 0o600 permissions

QML service — modules/services/CalendarService.qml

Singleton wrapping the Python process. Exposes accounts, calendars, events lists; operation pending/result state; arrival blinking state. All audio is handled by Python — QML plays no sounds.

Bar widget — modules/bar/BarCalendarIndicator.qml

  • Default mode: shows upcoming event count badge; hides when all today's events are past
  • barShowNextEvent: shows next event title and countdown timer
  • barAlwaysShow: always visible, popup splits into Upcoming / Past sections with past events dimmed
  • Blinking icon animation on arrival, dismissed on click or after 30 s

Dashboard — CalendarPanel, EventDetailPanel, EventItem, EventPopup

  • CalendarPanel: account management (Google OAuth flow, CalDAV form, gcalcli import), per-calendar toggle, all settings with pending-state save pattern
  • EventDetailPanel: create/edit form — time spinners (keyboard + mouse wheel), calendar selector, reminder, description, location, Google Meet link toggle (Google accounts only); operation feedback with 15 s timeout; back cancels edit, ✕ always closes
  • EventItem / EventPopup: event list item and quick popup from the calendar grid

Config

New calendar module in Config.qml and config/defaults/calendar.js, following the existing JsonAdapter pattern.

i18n readiness

All user-facing strings in the calendar files are wrapped with a local _t(key, fallback) helper that safely falls back to the hardcoded English string when the I18n singleton is not present. Translation keys (calendar.bar.*, calendar.event.*, calendar.form.*, calendar.settings.*) have been added to feature/i18n (39 keys, en/ru/es).


Dependencies

Package Purpose
google-api-python-client Google Calendar API
google-auth-oauthlib OAuth2 flow
caldav CalDAV protocol
requests CalDAV pre-flight check
canberra-gtk-play (optional) Arrival sound (freedesktop theme)
paplay / pw-play / aplay Fallback audio

Known issues and areas that need proper work

The implementation has several rough edges that a maintainer should address before this would be mergeable:

Architecture

  • _auth_google() and _auth_caldav() run directly on the stdin dispatch thread. During an OAuth browser flow this blocks all other commands indefinitely. Both need to be offloaded to worker threads following the same pattern as _do_sync.
  • self.tokens is accessed from both the main thread and the sync thread without lock protection. In practice the GIL prevents corruption, but concurrent token refresh and account removal could produce a partially-written token file.

Security

  • The Google OAuth client secret is stored in the general config file (~/.config/ambxst/config/calendar.json), which is world-readable by default. The token file is correctly 0o600, but the secret that mints it is not protected.
  • gcalcli token import uses pickle.load() on the gcalcli token file. The risk is documented in the code but not surfaced to the user in the UI.

Validation / consistency

  • EventPopup.qml (the click-on-day path from the calendar grid) saves events without validating title or end > start, while EventDetailPanel.qml (the dashboard path) does both. The validation logic should be shared.
  • EventItem.qml is missing pragma ComponentBehavior: Bound, inconsistent with every other file in the feature.
  • EventPopup.qml strings are not wrapped with _t(), inconsistent with EventDetailPanel.qml.
  • The location button in EventItem.qml is shown for plain-text addresses but only opens URLs starting with http. It should either construct a maps search URL or hide itself for non-URL values.

These are not exhaustive — the code has not been tested against real Google Calendar or CalDAV accounts in a CI environment, and edge cases around timezone handling, recurring events, and multi-day all-day events are likely incomplete.

Screenshots

1 2 3 4 5
2026-04-01T19-11-10-038Z.mp4

Install as a mod

Requires the native manager from PR #230 and the Python imports listed above. Paste the package link into Settings → Mods → Package source:

https://github.com/flathead/ambxst-mods/tree/main/packages/calendar-integration

The package needs community.i18n (PR #134) for its account, settings, and event strings. The details pane marks that requirement as missing, disabled, or ready, and Install required mods fetches i18n, enables it first, and leaves this package for you to enable. The same steps from the CLI:

ambxst mods install https://github.com/flathead/ambxst-mod-i18n.git
ambxst mods enable community.i18n
ambxst mods install https://github.com/flathead/ambxst-mods/tree/main/packages/calendar-integration
ambxst mods enable community.calendar-integration
ambxst reload

The package targets current dev, keeps the existing General config module, and adds Calendar as a separate module.

Merge into Ambxst core

For core review, take this PR's branch: the feature as ordinary Ambxst source on dev.

The package patch is a separate artifact. It targets a tree that already carries the mod manager from PR #230 and the community.i18n package, because that is the only tree a mod can be installed on. To reproduce exactly what the manager composes:

git remote add flathead https://github.com/flathead/Ambxst.git
git fetch flathead feature/mod-manager
git switch -c mods/calendar-integration flathead/feature/mod-manager
git apply --3way ambxst-mod-i18n/patches/feature.patch
git apply --3way ambxst-mods/packages/calendar-integration/patches/feature.patch
go test ./...
go vet ./...

It adds the Python service, QML singleton, bar indicator, account and settings panel, event views, Calendar config module, icons, and Dashboard registration. This PR's branch already calls I18n.t() in five files, so it expects PR #134 too; the package only states that requirement instead of leaving it implied. The architecture and security concerns listed above still apply before a core merge; packaging does not hide or waive them.

Package parity

The package in flathead/ambxst-mods and this branch carry the same feature code. CalendarService.qml, CalendarPanel.qml, calendar_service.py, the bar indicator, the event views, and calendar.js are byte-identical. Config.qml, BarContent.qml, SettingsTab.qml, SettingsIndex.qml, and Calendar.qml differ only in registration and string wrapping, because the package is generated on a tree that also carries the mod manager and community.i18n. In the package the Calendar settings section claims its own id instead of renumbering the existing ones; in core it can take its natural position.

flathead added 3 commits April 1, 2026 22:02
Full calendar integration for the Ambxst shell, including:

Backend (scripts/calendar_service.py):
- Python service communicating with QML via stdin/stdout JSON protocol
- Google Calendar via OAuth2 (google-api-python-client)
- CalDAV support (caldav library)
- Import of existing gcalcli tokens
- Background sync thread with configurable interval
- Notification system: reminders and arrival alerts via notify-send
- Sound on arrival via canberra-gtk-play / paplay / pw-play / aplay fallback chain
- Persisted notification deduplication across restarts (~/.cache/ambxst/calendar_notified.json)
- CRUD operations (create, update, delete) with cmd_start/cmd_result protocol
- Atomic token writes with 0o600 permissions

QML service (modules/services/CalendarService.qml):
- Singleton wrapping the Python process
- Accounts, calendars, events state
- Operation feedback: operationPending, operationResult signal
- Arrival state with iconBlinking and 30s auto-dismiss timer
- All audio handled by Python; QML does not play sounds

Bar widget (modules/bar/BarCalendarIndicator.qml):
- Shows upcoming event count or next event title + time
- barAlwaysShow mode: splits popup into Upcoming / Past sections
- Blinking icon animation on event arrival
- barShowNextEvent mode: shows event title and countdown

Dashboard panels:
- CalendarPanel.qml: accounts management, Google OAuth, CalDAV auth,
  gcalcli import, per-calendar enable/disable, all settings
- EventDetailPanel.qml: full create/edit form with time spinners,
  calendar selector, reminder, description, location, Google Meet toggle,
  operation feedback (saving state, timeout, error display),
  back = cancel edit, close button always exits
- EventItem.qml / EventPopup.qml: event list item and quick popup

Config:
- config/defaults/calendar.js: all defaults
- Config.qml: JsonAdapter with full property set including barAlwaysShow

i18n:
- All user-facing strings in calendar files wrapped with _t(key, fallback)
- Helper is safe without the I18n singleton (try/catch fallback)
- Translation keys added to feature/i18n branch (en/ru/es)
Add _t() helper to EventPopup.qml and wrap all user-visible strings
with translation keys. Wrap all remaining ~30 hardcoded strings in
CalendarPanel.qml (section headers, form labels, buttons, hints).
All strings have English fallbacks for use without i18n module.
@NVitschDEV

Copy link
Copy Markdown

This is fire :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants