Architecture notes for contributors and reviewers.
README | Configuration | Integrations | Troubleshooting | Testing
Overview
Chime is a small event pipeline around org-agenda. It periodically asks org for upcoming entries, converts those entries into serializable event alists, then uses that event list for notifications, modeline text, tooltip content, and jump-to-source actions.
The central design constraint is responsiveness: agenda scanning can be slow for large org collections, so Chime does that work in an async Emacs subprocess and keeps the interactive Emacs session limited to scheduling, callbacks, rendering, and notification dispatch.
Runtime Flow
chime-mode
|
v
chime--start
|
v
timer -> chime-check
|
v
chime--maybe-validate
|
v
chime--fetch-and-process
|
v
async child process
|
v
chime--retrieve-events
|
v
org-agenda-list -> markers -> filters -> chime--gather-info
|
v
event alists returned to parent process
|
v
chime--handle-async-success
|
v
callback
| |
v v
chime--process-events chime--update-modeline
| |
v v
chime--notify chime-modeline-string
Manual refresh uses the same validation and async retrieval path, but supplies a modeline-only callback so it does not send notifications:
M-x chime-refresh-modeline
-> chime--maybe-validate
-> chime--fetch-and-process
-> chime--update-modeline
Major Components
Lifecycle
chime-mode controls the package lifecycle. Enabling the mode calls chime--start, which installs a timer. Disabling the mode calls chime--stop, which cancels the timer, interrupts any active async process, and resets validation state.
Key state:
chime--timer— active polling timerchime--process— currently running async process, if anychime--last-check-time— last event check timechime--validation-done— whether startup validation has succeededchime--validation-retry-count— retry counter for startup validation
Validation
chime--maybe-validate gates event retrieval. It gives startup configuration a chance to populate org-agenda-files before Chime starts checking.
chime-validate-configuration is also interactive. Interactively, it prints a checklist to *Messages*. Programmatically, it returns issue pairs so callers can decide whether to continue.
Async Retrieval
chime--retrieve-events returns the child-process form used by async.el. The child process:
- Receives selected parent variables through
async-inject-variables. - Initializes package.el.
- Requires
chime. - Runs
org-agenda-listfor the required lookahead span. - Extracts org markers from agenda line text properties.
- Applies include/exclude filters.
- Converts markers to event alists with
chime--gather-info.
The parent process receives only plain Lisp data. This is why event source identity is stored as file path and buffer position rather than as live marker objects.
Event Processing
chime--process-events handles notification dispatch. It combines:
- timed notifications from
chime--check-event - day-wide notifications from
chime--day-wide-notifications
Timed notifications compare event timestamps against current time plus each configured alert interval. Day-wide notifications are handled separately because all-day timestamps have no clock component.
Modeline and Tooltip
chime--update-modeline computes two related views:
chime-modeline-string— rendered text for the soonest timed event insidechime-modeline-lookahead-minuteschime--upcoming-events— sorted tooltip tuples insidechime-tooltip-lookahead-hours
All-day events are never shown in the modeline itself because the modeline is clock-relative. They can appear in the tooltip and in day-wide notifications.
The tooltip uses chime--upcoming-events, grouping entries by day and limiting display with chime-modeline-tooltip-max-events.
Data Structures
Event Alist
The internal event shape is an alist created by chime--make-event and validated by chime--valid-event-p:
((times . (("<2026-05-10 Sun 09:30>" . (26760 32460))))
(title . "Planning")
(intervals . ((10 . medium) (0 . high)))
(marker-file . "/path/to/agenda.org")
(marker-pos . 1234))
Keys:
times— list of timestamp entriestitle— sanitized display titleintervals— alert intervals fromchime-alert-intervalsmarker-file— source org file path, or nil for synthesized test eventsmarker-pos— source buffer position, or nil for synthesized test events
Use the accessors instead of open-coded assoc in production code:
chime--event-timeschime--event-titlechime--event-intervalschime--event-marker-filechime--event-marker-pos
Time Entry
Each entry in times is:
(TIMESTAMP-STRING . PARSED-TIME)
TIMESTAMP-STRING is the original org timestamp string. PARSED-TIME is a serialized Emacs time value for timed events, or nil for all-day events.
Examples:
("<2026-05-10 Sun 09:30>" . (26760 32460))
("<2026-05-10 Sun>" . nil)
Upcoming Event Tuple
Tooltip state uses tuples derived from event alists:
(EVENT TIME-INFO MINUTES-UNTIL)
Example:
(((times . ...)
(title . "Planning")
(intervals . ...)
(marker-file . "/path/to/agenda.org")
(marker-pos . 1234))
("<2026-05-10 Sun 09:30>" . (26760 32460))
15.0)
These tuples are stored in chime--upcoming-events and are also passed through tooltip grouping and deduplication helpers.
Filtering
Filtering happens in the async child before event alists are created:
markers
-> chime--apply-include-filters
-> chime--apply-exclude-filters
-> chime--gather-info
Filters operate on org markers, not event alists, so predicates can inspect org properties, tags, TODO state, and source files directly.
chime-include-filters and chime-exclude-filters share the same shape:
((keywords . ("TODO" "NEXT"))
(tags . ("work"))
(predicates . (my-marker-predicate)))
If include filters are configured, a marker must match at least one include predicate. Exclude filters then remove any matching marker.
org-gcal Handling
Regular org entries use SCHEDULED and DEADLINE timestamps first, then plain timestamps in the entry body.
org-gcal entries are different: Chime detects them by the entry-id property and extracts timestamps only from the :org-gcal: drawer. That drawer is the authoritative source after calendar syncs; planning lines or body timestamps can lag behind after remote edits.
Error Handling
Async failures are tracked with chime--consecutive-async-failures. Repeated failures can trigger a warning controlled by chime-max-consecutive-failures.
chime--fetch-and-process records failures from two places:
- errors returned by the async process
- errors raised while the parent callback handles returned data
Successful async processing resets the consecutive failure counter.
Navigation
Modeline right-click uses the first item in chime--upcoming-events. Chime reconstructs source location from marker-file and marker-pos:
chime--jump-to-first-event
-> chime--jump-to-event
-> find-file
-> goto-char
-> org-fold-show-entry / org-show-entry
This works across the async boundary because the child returns file path and numeric position, not marker objects.
Testing Notes
Tests build event alists through shared helpers in tests/testutil-events.el so fixtures stay aligned with the production event contract.
Useful focused test files:
tests/test-chime-event-contract.eltests/test-chime-gather-info.eltests/test-chime-check-event.eltests/test-chime-update-modeline.eltests/test-chime-update-modeline-helpers.eltests/test-chime-process-notifications.el
Run the full suite with:
make test