Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d1a1c4d
feat(calendar): add reminder config and event lead times
yevhen-sychov Aug 24, 2026
830c04f
feat(calendar): add reminder planner
yevhen-sychov Aug 24, 2026
d14d175
feat(calendar): read VALARM triggers from iCalendar feeds
yevhen-sychov Aug 24, 2026
9380832
feat(calendar): read reminders from the Google Calendar API
yevhen-sychov Aug 24, 2026
e04a6f8
feat(calendar): persist reminder lead times in the event cache
yevhen-sychov Aug 24, 2026
15b7585
feat(notification): let internal notifications opt into history and a…
yevhen-sychov Aug 24, 2026
7316b4c
feat(calendar): notify before events start
yevhen-sychov Aug 24, 2026
dc4a372
Merge branch 'noctalia-dev:main' into feat/calendar-reminders
yevhen-sychov Aug 27, 2026
59843f5
fix(notification): keep persisted notifications in history when dismi…
yevhen-sychov Aug 27, 2026
224d446
Merge remote-tracking branch 'upstream/main' into feat/calendar-remin…
yevhen-sychov Aug 27, 2026
1b84f00
Merge remote-tracking branch 'upstream/main' into feat/calendar-remin…
yevhen-sychov Aug 27, 2026
51798e1
fix(calendar): round a reminder's lead up instead of truncating it
yevhen-sychov Aug 28, 2026
b03d68e
feat(calendar): count reminders down until their event starts
yevhen-sychov Aug 28, 2026
4efa0f2
Merge remote-tracking branch 'upstream/main' into feat/calendar-remin…
yevhen-sychov Sep 1, 2026
e200284
Merge remote-tracking branch 'upstream/main' into feat/calendar-remin…
yevhen-sychov Sep 2, 2026
e6e4ecd
Merge remote-tracking branch 'upstream/main' into feat/calendar-remin…
yevhen-sychov Sep 3, 2026
e77b1bf
docs(calendar): document event reminders
yevhen-sychov Sep 3, 2026
d15e1e6
Merge remote-tracking branch 'origin/main' into feat/calendar-reminders
yevhen-sychov Sep 4, 2026
6bc7999
Merge remote-tracking branch 'upstream/main' into feat/calendar-remin…
yevhen-sychov Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,8 @@
"calendar": "Calendar",
"calendar-credential-locked-body": "Unlock the keyring used by Secret Service, then retry in Settings. With GNOME Keyring, make an automatically unlocked keyring such as Login the default.",
"calendar-credential-locked-title": "Calendar keyring is locked",
"calendar-digest-more": "+{count} more",
"calendar-digest-title": "All-day today",
"calendar-google-connect-browser-failed": "No browser opener could be launched for Google authorization. Check your default browser or xdg-open setup.",
"calendar-google-connect-credential-store-failed": "Authorization completed, but the refresh token could not be saved. Unlock or start Secret Service, then connect again.",
"calendar-google-connect-expired": "Google authorization expired before sign-in finished. Start the connection again.",
Expand All @@ -766,6 +768,13 @@
"calendar-google-connect-start-failed": "Could not start Google authorization. Check your network connection and try again.",
"calendar-google-connect-start-http": "Could not start Google authorization. The OAuth broker returned HTTP {status}.",
"calendar-google-connect-timeout": "Google authorization timed out. Start the connection again when you are ready to sign in.",
"calendar-reminder-catchup-body": "{count} more events are starting soon.",
"calendar-reminder-catchup-title": "More upcoming events",
"calendar-reminder-in": "Starts in {minutes} min · {time}",
"calendar-reminder-now": "Starts now · {time}",
"calendar-reminder-open": "Open",
"calendar-reminder-untitled": "Untitled event",
"calendar-reminder-with-location": "{when}\n{location}",
"config-migration-body": "Update {path} in the source file. Noctalia is applying this replacement only temporarily. Run noctalia config validate to list every required change.",
"config-migration-title": "Configuration update needed",
"dbus-disabled": "DBus notifications disabled",
Expand Down Expand Up @@ -2577,6 +2586,10 @@
"description-unavailable": "No Secret Service provider is available in this session",
"label": "Calendar Credential Storage"
},
"calendar-digest-time": {
"description": "Time of day for a single notification listing all-day events. Leave empty to disable.",
"label": "All-day digest time"
},
"calendar-edit": {
"button": "Edit",
"button-reconnect": "Reconnect",
Expand All @@ -2597,6 +2610,18 @@
"description": "How often to sync calendar events (minutes)",
"label": "Refresh Interval"
},
"calendar-reminder-lead": {
"description": "How far ahead to notify for events that set no reminder of their own.",
"label": "Default reminder lead"
},
"calendar-reminder-use-event": {
"description": "Honor the reminder set on each event. When off, every event uses the default lead time.",
"label": "Use event reminders"
},
"calendar-reminders": {
"description": "Notify before an event starts.",
"label": "Event reminders"
},
"calendar-storage": {
"button": "Retry",
"description-cancelled": "The encrypted storage prompt was cancelled; calendar events remain available for this session only",
Expand Down
28 changes: 26 additions & 2 deletions docs/user/services/calendar.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: Calendar
description: Calendar account setup for CalDAV, iCloud, Google Calendar, ICS, and local vdir directories.
description: Calendar account setup for CalDAV, iCloud, Google Calendar, ICS, and local vdir directories, plus event reminders.
sidebar:
order: 4
---

The calendar service syncs events from your online accounts or local calendar directories (such as vdirsyncer) and shows them in the control center calendar. CalDAV providers (Nextcloud, iCloud, Fastmail, Radicale, mailbox.org, …) are talked to directly; Google Calendar is connected through a browser authorization flow; local vdir directories are watched with inotify. Events are read-only.

`[calendar]` covers account sync and shared event date and time formats. Calendar-tab-only presentation options, such as the events card and ISO week number column, live under [`[control_center.calendar]`](/noctalia/control-center/#calendar).
`[calendar]` covers account sync, shared event date and time formats, and [event reminders](#reminders). Calendar-tab-only presentation options, such as the events card and ISO week number column, live under [`[control_center.calendar]`](/noctalia/control-center/#calendar).

## Settings setup

Expand Down Expand Up @@ -79,6 +79,30 @@ color = "#FF0000"

**Credentials are never stored in your config.** The `password_file` setting contains only a path; the file contents hold the CalDAV password.

## Reminders

Noctalia can notify you before an event starts. Reminders are read from the calendar itself — `VALARM` triggers on CalDAV, ICS, and local vdir events, and reminder overrides or calendar defaults on Google — with a configurable fallback for events that carry none.

```toml
[calendar.reminders]
enabled = true
use_event_reminders = true # honor per-event reminders; off = always use the default lead below
default_lead_minutes = 10 # used for events with no reminder of their own; 0 = at event start
all_day_digest_time = "09:00" # one notification listing today's all-day events; "" disables it
```

The same four options are in **Settings → Services → Calendar**. Reminders require `[calendar]` itself to be enabled.

The notification body counts down while the event approaches and settles on "Starts now" when it begins. Clicking a reminder opens the event's link when it has one.

All-day events do not notify individually. They are collected into one digest at `all_day_digest_time` local time, listing today's all-day events. The digest fires once per day; starting the shell after that time still shows the day's digest, and a calendar that syncs later in the morning is not skipped.

A reminder whose time passed while the shell was not running still notifies at startup, as long as the event has not started yet. Reminders for events that already began are dropped rather than shown late.

Reminders are kept in notification history, so one missed while you were away is still in the control center. They follow your Do Not Disturb setting.

Reminder times come from the encrypted event cache, so they work offline and across restarts without a sync.

## Credential storage

Each CalDAV account has exactly one credential source:
Expand Down
2 changes: 2 additions & 0 deletions docs/user/services/notifications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ internal shell/plugin toasts.
This affects new notifications only. Use `noctalia msg notification-clear-history` to remove entries already stored in
history.

Shell notifications are toast-only and are not kept in history, with one exception: [calendar reminders](/noctalia/services/calendar/#reminders) are retained, because a reminder missed while you were away has no other place to surface. They appear under the app name `Calendar`, so a filter matching that name controls their toast, sound, and history like any other notification.

## Related IPC

For Do Not Disturb status/toggle commands and notification clear actions, see
Expand Down
8 changes: 8 additions & 0 deletions example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,14 @@ refresh_minutes = 15
event_date_format = "%A %e %B" # date format used for events
event_time_format = "%H:%M" # time format used for events

# Notifications for upcoming events.

[calendar.reminders]
enabled = true
use_event_reminders = true # honor per-event reminders; off = always use the default lead below
default_lead_minutes = 10 # used for events with no reminder of their own; 0 = at event start
all_day_digest_time = "09:00" # one notification listing today's all-day events; "" disables it

# Calendar tab of the control center.

[control_center.calendar]
Expand Down
6 changes: 6 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ _noctalia_sources = files(
'src/calendar/calendar_cache.cpp',
'src/calendar/calendar_credential_store.cpp',
'src/calendar/calendar_discovery_state.cpp',
'src/calendar/calendar_reminder_monitor.cpp',
'src/calendar/calendar_reminders.cpp',
'src/calendar/calendar_service.cpp',
'src/calendar/event_link.cpp',
'src/calendar/google_client.cpp',
Expand Down Expand Up @@ -1138,6 +1140,8 @@ if build_tests
'calendar_cache_permissions',
'calendar_credential_store',
'calendar_discovery_state',
'calendar_reminder_monitor',
'calendar_reminders',
'calendar_view_passed_event',
'capsule_group_lane_override',
'capsule_group_reconcile',
Expand Down Expand Up @@ -1165,6 +1169,7 @@ if build_tests
'fcitx_status',
'freedesktop_key_file',
'google_client_calendar_list',
'google_client_reminders',
'greeter_appearance_sync',
'grid_item_cursor',
'hook_manager',
Expand Down Expand Up @@ -1196,6 +1201,7 @@ if build_tests
'notification_filter',
'notification_history_dismiss',
'notification_history_utf8',
'notification_internal_history',
'path_browse',
'plugin_bindings',
'plugin_catalog',
Expand Down
4 changes: 4 additions & 0 deletions src/app/application.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include "app/deferred_call_poll_source.h"
#include "app/timer_poll_source.h"
#include "calendar/calendar_poll_source.h"
#include "calendar/calendar_reminder_monitor.h"
#include "calendar/calendar_reminder_poll_source.h"
#include "calendar/calendar_service.h"
#include "capture/screenshot_service.h"
#include "compositors/compositor_platform.h"
Expand Down Expand Up @@ -244,6 +246,7 @@ class Application {
LockKeysService m_lockKeysService;
NotificationManager m_notificationManager;
CalendarService m_calendarService;
CalendarReminderMonitor m_calendarReminderMonitor{m_configService, m_notificationManager};
std::unique_ptr<SessionBus> m_bus;
std::unique_ptr<SystemBus> m_systemBus;
std::unique_ptr<LogindService> m_logindService;
Expand Down Expand Up @@ -374,6 +377,7 @@ class Application {
LocationPollSource m_locationPollSource{m_locationService};
WeatherPollSource m_weatherPollSource{m_weatherService};
CalendarPollSource m_calendarPollSource{m_calendarService};
CalendarReminderPollSource m_calendarReminderPollSource{m_calendarReminderMonitor};
Timer m_trayInitTimer;
Timer m_polkitInitTimer;
Timer m_polkitIdleCloseTimer;
Expand Down
1 change: 1 addition & 0 deletions src/app/application_poll_sources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ std::vector<PollSource*> Application::currentPollSources() {
sources.push_back(&m_locationPollSource);
sources.push_back(&m_weatherPollSource);
sources.push_back(&m_calendarPollSource);
sources.push_back(&m_calendarReminderPollSource);
sources.push_back(&m_thumbnailService);
sources.push_back(&m_wallpaperScanner);
sources.push_back(&m_asyncTextureCache);
Expand Down
17 changes: 17 additions & 0 deletions src/app/application_services.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,23 @@ void Application::initSessionBusServices() {
m_weatherService.initialize();
m_calendarService.initialize();

// Load the persisted fired set before the first evaluation, or a restart would re-notify.
m_calendarReminderMonitor.initialize();
(void)m_calendarService.addChangeCallback([this]() {
m_calendarReminderMonitor.onSnapshotChanged(m_calendarService.snapshot());
});
// initialize() already loaded the encrypted cache, so the snapshot can be valid before the first
// network sync — seed from it so missed reminders fire at startup rather than after a refresh.
m_calendarReminderMonitor.onSnapshotChanged(m_calendarService.snapshot());
m_configService.addReloadCallback(
[this]() {
if (m_configService.lastChange().calendar) {
m_calendarReminderMonitor.onConfigReload();
}
},
"calendar-reminders"
);

// LocationService is the single source of "where am I": push its resolved coordinates to the
// weather service, night light, and theme auto mode. Manual latitude/longitude and fixed
// sunrise/sunset live in [location] and reach night light/theme through their config reloads.
Expand Down
Loading