Skip to content

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

Open
flathead wants to merge 3 commits intoAxenide:devfrom
flathead:feature/calendar-integration
Open

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

Conversation

@flathead
Copy link
Copy Markdown

@flathead flathead commented Apr 1, 2026

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

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.
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.

1 participant