···
1
+
;;; caledonia.el --- Emacs integration for Caledonia -*- lexical-binding: t -*-
3
+
;; Copyright (C) 2025 Ryan Gibb
5
+
;; Author: Ryan Gibb <ryan@freumh.org>
7
+
;; Package-Requires: ((emacs "27.1"))
8
+
;; Keywords: calendar, caledonia
9
+
;; URL: https://ryan.freumh.org/caledonia.html
11
+
;; This file is not part of GNU Emacs.
15
+
;; This package provides an Emacs interface to the Caledonia calendar CLI.
16
+
;; It communicates with Caledonia using S-expressions for data exchange.
22
+
(require 'pulse nil t)
25
+
(defgroup caledonia nil
26
+
"Interface to Caledonia calendar client."
28
+
:prefix "caledonia-")
30
+
(defcustom caledonia-executable "caled"
31
+
"Path to the Caledonia executable."
35
+
(defface caledonia-calendar-name-face
36
+
'((t :inherit font-lock-function-name-face))
37
+
"Face used for calendar names in the events view."
40
+
(defface caledonia-date-face
41
+
'((t :inherit font-lock-string-face))
42
+
"Face used for dates in the events view."
45
+
(defface caledonia-summary-face
46
+
'((t :inherit default))
47
+
"Face used for event summaries in the events view."
50
+
(defface caledonia-location-face
51
+
'((t :inherit font-lock-comment-face))
52
+
"Face used for event locations in the events view."
55
+
(defcustom caledonia-calendar-column-width 0
56
+
"Column width for the Calendar entry."
59
+
(defcustom caledonia-start-column-width 0
60
+
"Column width for the Start entry."
63
+
(defcustom caledonia-end-column-width 0
64
+
"Column width for the End entry."
67
+
(defcustom caledonia-list-from-date "today"
68
+
"Default start date for calendar list view."
72
+
(defcustom caledonia-list-to-date "+3m"
73
+
"Default end date for calendar list view (3 months from today)."
77
+
(defcustom caledonia-search-from-date nil
78
+
"Default start date for calendar search; nil means no start date limit."
82
+
(defcustom caledonia-search-to-date "+75y"
83
+
"Default end date for calendar search (75 years from today)."
87
+
;; Define histories for input fields
89
+
(defvar caledonia-from-history nil "History for from date inputs.")
90
+
(defvar caledonia-to-history nil "History for to date inputs.")
91
+
(defvar caledonia-timezone-history nil "History for timezone inputs.")
92
+
(defvar caledonia-calendars-history nil "History for calendar inputs.")
93
+
(defvar caledonia-text-history nil "History for search text inputs.")
94
+
(defvar caledonia-search-fields-history nil "History for search fields inputs.")
95
+
(defvar caledonia-id-history nil "History for event ID inputs.")
96
+
(defvar caledonia-limit-history nil "History for limit inputs.")
97
+
(defvar caledonia-search-prompt-history nil "History for search prompt inputs.")
99
+
;; Internal variables
101
+
(defvar caledonia--events-buffer "*Caledonia Events*"
102
+
"Buffer name for displaying Caledonia events.")
103
+
(defvar caledonia--details-buffer "*Caledonia Event Details*"
104
+
"Buffer name for displaying Caledonia event details.")
105
+
(defvar caledonia--server-process nil
106
+
"The persistent Caledonia server process.")
107
+
(defvar caledonia--server-buffer-name "*caledonia-server-io*"
108
+
"Buffer for server process I/O.")
109
+
(defvar caledonia--response-line nil
110
+
"Last response line received.")
111
+
(defvar caledonia--response-flag nil
112
+
"Flag set when response is received.")
113
+
(defvar-local caledonia--current-query nil
114
+
"The current query parameters being displayed in this buffer.")
118
+
(defvar caledonia--server-line-buffer "")
120
+
(defun caledonia--server-filter (process output)
121
+
;; Append to the ongoing buffer for logging/debugging
122
+
(when (buffer-live-p (process-buffer process))
123
+
(with-current-buffer (process-buffer process)
124
+
(goto-char (point-max))
126
+
;; Append new output to line buffer
127
+
(setq caledonia--server-line-buffer (concat caledonia--server-line-buffer output))
128
+
;; Extract full lines
129
+
(let ((lines (split-string caledonia--server-line-buffer "\n")))
130
+
;; Keep the last line (possibly incomplete) for next round
131
+
(setq caledonia--server-line-buffer (car (last lines)))
132
+
;; Process all complete lines
133
+
(dolist (line (butlast lines))
134
+
(when (and (not caledonia--response-flag)
135
+
(not (string-empty-p line)))
136
+
(setq caledonia--response-line line)
137
+
(setq caledonia--response-flag t)))))
139
+
(defun caledonia--server-sentinel (process event)
140
+
(message "Caledonia Server process event: %s (%s)" process event)
141
+
(setq caledonia--server-process nil))
143
+
(defun caledonia--ensure-server-running ()
144
+
(unless (and caledonia--server-process (process-live-p caledonia--server-process))
145
+
(message "Caledonia Starting server...")
146
+
(setq caledonia--server-process
147
+
(start-process "caledonia-server"
148
+
(get-buffer-create caledonia--server-buffer-name)
149
+
caledonia-executable
151
+
(unless (and caledonia--server-process (process-live-p caledonia--server-process))
152
+
(error "Caledonia Failed to start server process."))
153
+
(set-process-filter caledonia--server-process #'caledonia--server-filter)
154
+
(set-process-sentinel caledonia--server-process #'caledonia--server-sentinel)
155
+
(message "Caledonia Server started.")))
157
+
(defun caledonia--send-request (request-str)
158
+
(caledonia--ensure-server-running)
159
+
(setq caledonia--response-line nil)
160
+
(setq caledonia--response-flag nil)
161
+
(process-send-string caledonia--server-process (concat request-str "\n"))
162
+
;; Wait for response
163
+
(let ((start-time (current-time)))
164
+
(while (and (not caledonia--response-flag)
165
+
(< (time-to-seconds (time-since start-time)) 5) ; 5 sec timeout
166
+
(process-live-p caledonia--server-process))
167
+
(accept-process-output caledonia--server-process 0 100000))) ; Wait 100ms
168
+
(unless caledonia--response-flag
169
+
(error "Caledonia Timeout or server died waiting for response."))
170
+
(condition-case err
171
+
(let ((response-sexp (read caledonia--response-line)))
172
+
(unless (and (listp response-sexp) (memq (car response-sexp) '(Ok Error)))
173
+
(error "Caledonia Invalid response format: %S" response-sexp))
174
+
(if (eq (car response-sexp) 'Error)
175
+
(error "Caledonia Server Error: %s" (cadr response-sexp))
176
+
;; Return the (Ok ...) payload
177
+
(cadr response-sexp)))
178
+
(error (error "Caledonia Failed to parse response line: %s"
179
+
caledonia--response-line (error-message-string err)))))
181
+
(defun caledonia--get-events (event-payload)
182
+
"Parse SEXP-STRING of structure (Events (events...))"
183
+
(if (and (listp event-payload) (eq (car event-payload) 'Events))
184
+
(let ((event-list (cadr event-payload)))
187
+
(message "Failed to parse Caledonia output: %s" (error-message-string err))
192
+
(defun caledonia--format-timestamp (iso-string &optional format)
193
+
"Format ISO-8601 time string to human-readable format.
194
+
FORMAT defaults to \"%Y-%m-%d %H:%M\" if not specified."
195
+
(let* ((parsed (parse-time-string iso-string))
196
+
(time (apply #'encode-time
197
+
(append (cl-subseq parsed 0 6) (list nil -1)))))
198
+
(format-time-string (or format "%Y-%m-%d %H:%M") time)))
200
+
(defun caledonia--get-key (key event)
201
+
"Get KEY from EVENT as a string."
202
+
(let ((value (cadr (assoc key event))))
205
+
((stringp value) value)
206
+
((symbolp value) (symbol-name value)))))
208
+
(defun caledonia--tabulated-list-entries (events)
209
+
"Convert EVENTS for a format suitable for showing via a tabulated-list-mode'."
210
+
(let ((max-calendar-width 0)
211
+
(max-start-width 0)
213
+
(tabulated-list-entries nil))
214
+
;; first pass: calculate maximum widths
215
+
(dolist (event events)
216
+
(let* ((calendar (caledonia--get-key 'calendar event))
217
+
(start (caledonia--get-key 'start event))
218
+
(end (caledonia--get-key 'end event))
219
+
(cal-str (if (not calendar) "unkown" calendar))
220
+
(start-str (caledonia--format-timestamp start))
222
+
(caledonia--format-timestamp (format "%s" end)))))
223
+
(setq max-calendar-width (max max-calendar-width (length cal-str)))
224
+
(setq max-start-width (max max-start-width (+ (length start-str) 2)))
225
+
(setq max-end-width (max max-end-width (length end-str)))))
226
+
(setq caledonia-calendar-column-width (max max-calendar-width (length "Calendar")))
227
+
(setq caledonia-start-column-width (max max-start-width (length "Start")))
228
+
(setq caledonia-end-column-width (max max-end-width (length "End")))
229
+
;; second pass: prepare tabulated-list entries with properties
230
+
(setq tabulated-list-entries
231
+
(mapcar (lambda (event)
233
+
(id (caledonia--get-key 'id event))
234
+
(summary (caledonia--get-key 'summary event))
235
+
(start (caledonia--get-key 'start event))
236
+
(end (caledonia--get-key 'end event))
237
+
(location (caledonia--get-key 'location event))
238
+
(calendar (caledonia--get-key 'calendar event))
239
+
(start-str (caledonia--format-timestamp start))
240
+
(end-str (if end (caledonia--format-timestamp (format "%s" end)) ""))
241
+
(start-str (if end (format "%s -" start-str) start-str))
242
+
(location-str (if location (concat " @ " location) ""))
243
+
(cal-prop (propertize calendar 'face 'caledonia-calendar-name-face))
244
+
(start-prop (propertize start-str 'face 'caledonia-date-face))
245
+
(end-prop (propertize end-str 'face 'caledonia-date-face))
246
+
(summary-prop (propertize (concat summary location-str)
247
+
'face 'caledonia-summary-face))
248
+
;; Store the full event data as a text property for retrieval
249
+
(entry-id (propertize (format "%s" id) 'event-data event)))
250
+
(list entry-id (vector cal-prop start-prop end-prop summary-prop))))
252
+
tabulated-list-entries))
254
+
(defun caledonia--sort-calendar (A B)
255
+
"Sort function for calendar column."
256
+
(let ((a (aref (cadr A) 0))
257
+
(b (aref (cadr B) 0)))
260
+
(defun caledonia--sort-start (A B)
261
+
"Sort function for date/time column."
262
+
(let ((a (aref (cadr A) 1))
263
+
(b (aref (cadr B) 1)))
264
+
(time-less-p (date-to-time a) (date-to-time b))))
266
+
(defun caledonia--sort-end (A B)
267
+
"Sort function for date/time column."
268
+
(let ((a (aref (cadr A) 2))
269
+
(b (aref (cadr B) 2)))
270
+
(time-less-p (date-to-time a) (date-to-time b))))
272
+
(defun caledonia--make-query (&optional query)
273
+
"Make a query with the QUERY S-expression.
274
+
If QUERY is nil, use the current query stored in `caledonia--current-query`."
276
+
(let* ((query-to-use (or query caledonia--current-query '())) ;; Use current query if available
277
+
;; Ensure to date is set if not present in query
278
+
(query-to-use (if (assq 'to query-to-use)
280
+
(cons `(to ,caledonia-list-to-date) query-to-use)))
281
+
(request-str (format "(Query %s)" (prin1-to-string query-to-use)))
282
+
(payload (caledonia--send-request request-str))
283
+
(events (caledonia--get-events payload))
284
+
(entries (caledonia--tabulated-list-entries events)))
285
+
;; Save this query for future refreshes if explicitly provided
287
+
(setq-local caledonia--current-query query-to-use))
288
+
(setq tabulated-list-entries entries))
289
+
(setq tabulated-list-format
290
+
`[("Calendar" ,caledonia-calendar-column-width caledonia--sort-calendar)
291
+
("Start" ,caledonia-start-column-width caledonia--sort-start)
292
+
("End" ,caledonia-end-column-width caledonia--sort-end)
294
+
(setq tabulated-list-sort-key (cons "Start" nil))
295
+
(tabulated-list-init-header)
296
+
(tabulated-list-print t))
298
+
(defun caledonia--find-and-highlight-event-in-file (file event-id)
299
+
"Find EVENT-ID in FILE, position cursor, and highlight the event.
300
+
Return non-nil if the event was found."
301
+
(when (and file event-id)
302
+
(let ((id-str (format "%s" event-id))
304
+
;; Try to find and highlight iCalendar VEVENT block
305
+
(goto-char (point-min))
306
+
(when (and (string-match-p "\\.ics$" file)
307
+
(search-forward (format "UID:%s" id-str) nil t))
308
+
;; Found the UID in an ICS file, try to highlight the VEVENT block
309
+
(let ((uid-pos (match-beginning 0))
312
+
;; Find start of the VEVENT block
314
+
(goto-char uid-pos)
315
+
(if (search-backward "BEGIN:VEVENT" nil t)
316
+
(setq vevent-start (match-beginning 0))
317
+
(setq vevent-start uid-pos)))
318
+
;; Find end of the VEVENT block
320
+
(goto-char uid-pos)
321
+
(if (search-forward "END:VEVENT" nil t)
322
+
(setq vevent-end (match-end 0))
323
+
(setq vevent-end (line-end-position))))
324
+
;; Highlight the whole VEVENT block if found
325
+
(when (and vevent-start vevent-end)
326
+
(goto-char vevent-start)
327
+
(caledonia--highlight-region vevent-start vevent-end)
331
+
(message "Event ID not found in file"))
334
+
(defun caledonia--display-event-details (event)
335
+
"Display details for EVENT in a separate buffer."
336
+
(let ((buf (get-buffer-create caledonia--details-buffer)))
337
+
(with-current-buffer buf
338
+
(let ((inhibit-read-only t))
341
+
(let* ((id (caledonia--get-key 'id event))
342
+
(summary (caledonia--get-key 'summary event))
343
+
(description (caledonia--get-key 'description event))
344
+
(start (caledonia--get-key 'start event))
345
+
(end (caledonia--get-key 'end event))
346
+
(location (caledonia--get-key 'location event))
347
+
(calendar (caledonia--get-key 'calendar event))
348
+
(file (caledonia--get-key 'file event))
349
+
(start-str (when start (caledonia--format-timestamp start)))
350
+
(end-str (when end (caledonia--format-timestamp end))))
352
+
(insert (propertize "Summary: " 'face 'bold) summary "\n"))
354
+
(insert (propertize "ID: " 'face 'bold) id "\n"))
356
+
(insert (propertize "Calendar: " 'face 'bold) calendar "\n"))
358
+
(insert (propertize "Start: " 'face 'bold) start-str "\n"))
360
+
(insert (propertize "End: " 'face 'bold) end-str "\n"))
362
+
(insert (propertize "Location: " 'face 'bold) location "\n"))
364
+
(insert (propertize "File: " 'face 'bold)
365
+
(propertize file 'face 'link
366
+
'mouse-face 'highlight
367
+
'help-echo "Click to open file with highlighting"
368
+
'keymap (let ((map (make-sparse-keymap))
369
+
(event-copy event))
370
+
(define-key map [mouse-1]
373
+
(let ((file-path file)
374
+
(id-val (caledonia--get-key 'id event-copy)))
375
+
(find-file file-path)
376
+
(caledonia--find-and-highlight-event-in-file
377
+
file-path id-val))))
378
+
(define-key map (kbd "RET")
381
+
(let ((file-path file)
382
+
(id-val (caledonia--get-key 'id event-copy)))
383
+
(find-file file-path)
384
+
(caledonia--find-and-highlight-event-in-file
385
+
file-path id-val))))
389
+
(insert "\n" (propertize "Description:" 'face 'bold) "\n"
390
+
(propertize "------------" 'face 'bold) "\n"
391
+
description "\n")))))
392
+
(switch-to-buffer-other-window buf)))
394
+
(defun caledonia--highlight-region (start end)
395
+
"Highlight the region between START and END."
396
+
(when (fboundp 'pulse-momentary-highlight-region)
397
+
(pulse-momentary-highlight-region start end))
398
+
;; Fallback for when pulse is not available
399
+
(unless (fboundp 'pulse-momentary-highlight-region)
400
+
(let ((overlay (make-overlay start end)))
401
+
(overlay-put overlay 'face 'highlight)
402
+
(run-with-timer 0.5 nil (lambda () (delete-overlay overlay))))))
404
+
(defun caledonia--read-date-range ()
405
+
"Read a date range from the user with org-mode date picker integration.
406
+
Returns a cons cell (from-date . to-date).
407
+
The from-date can be nil to indicate no start date constraint."
410
+
(if (y-or-n-p "Set a start date? ")
411
+
(org-read-date nil nil nil "From date: " nil nil t)
412
+
; empty string differentiates from nil for optional args later on
414
+
;; Use org-mode's date picker for To date (must have a value)
415
+
(setq to (org-read-date nil nil nil "To date: " nil nil t))
418
+
;; Query parameter modification functions
420
+
(defun caledonia-query-date-range ()
421
+
"Set the date range for the current calendar view."
423
+
(when (eq major-mode 'caledonia-mode)
424
+
(let* ((dates (caledonia--read-date-range
425
+
"From date" "" 'caledonia-from-history
426
+
"To date" "" 'caledonia-to-history))
429
+
(current-query caledonia--current-query)
430
+
(new-query (copy-tree current-query)))
431
+
;; Update the query with the new date range
432
+
(setq new-query (assq-delete-all 'from new-query))
433
+
(setq new-query (assq-delete-all 'to new-query))
434
+
(when (and from (not (string-empty-p from)))
435
+
(push `(from ,from) new-query))
436
+
(when (and to (not (string-empty-p to)))
437
+
(push `(to ,to) new-query))
438
+
;; Execute the updated query
439
+
(caledonia--make-query new-query))))
441
+
(defun caledonia-query-calendars ()
442
+
"Set the calendars to filter by for the current calendar view.
443
+
Fetches available calendars from server to allow selection from a list."
445
+
(when (eq major-mode 'caledonia-mode)
446
+
(let* ((available-calendars
447
+
(caledonia--send-request "ListCalendars"))
449
+
(if (and (listp available-calendars)
450
+
(eq (car available-calendars) 'Calendars))
451
+
(cadr available-calendars)
453
+
(message "Failed to get calendar list from server")
455
+
;; Use completing-read-multiple to select from available calendars
456
+
(selected-calendars
457
+
(completing-read-multiple
458
+
"Select calendars (comma-separated, empty for all): "
459
+
;; Use empty list if no calendars found
460
+
(or calendars-list '())
462
+
(let ((current-calendars (cdr (assq 'calendars caledonia--current-query))))
463
+
(when current-calendars
464
+
(mapconcat #'identity current-calendars ",")))
465
+
'caledonia-calendars-history))
466
+
(calendars (mapcar #'string-trim selected-calendars))
467
+
(current-query caledonia--current-query)
468
+
(new-query (copy-tree current-query)))
469
+
;; Update the query with the new calendars
470
+
(setq new-query (assq-delete-all 'calendars new-query))
471
+
(when (and calendars (not (null calendars)))
472
+
(push `(calendars ,calendars) new-query))
473
+
;; Execute the updated query
474
+
(caledonia--make-query new-query))))
476
+
(defun caledonia-query-text ()
477
+
"Set the search text for the current calendar view."
479
+
(when (eq major-mode 'caledonia-mode)
480
+
(let* ((text (read-string "Search text (leave empty for no text search): "
481
+
nil 'caledonia-text-history))
482
+
(search-in-str (when (and text (not (string-empty-p text)))
483
+
(read-string "Search in (summary,description,location - leave empty for all): "
484
+
nil 'caledonia-search-fields-history)))
485
+
(search-in (when (and search-in-str (not (string-empty-p search-in-str)))
486
+
(mapcar (lambda (field)
487
+
(intern (string-trim field)))
488
+
(split-string search-in-str "," t))))
489
+
(current-query caledonia--current-query)
490
+
(new-query (copy-tree current-query)))
491
+
;; Update the query with the new text search parameters
492
+
(setq new-query (assq-delete-all 'text new-query))
493
+
(setq new-query (assq-delete-all 'search_in new-query))
494
+
(when (and text (not (string-empty-p text)))
495
+
(push `(text ,text) new-query))
497
+
(push `(search_in ,search-in) new-query))
498
+
;; Execute the updated query
499
+
(caledonia--make-query new-query))))
501
+
(defun caledonia-query-id ()
502
+
"Set the event ID to filter by for the current calendar view."
504
+
(when (eq major-mode 'caledonia-mode)
505
+
(let* ((id (read-string "Event ID (leave empty for all events): "
506
+
nil 'caledonia-id-history))
507
+
(current-query caledonia--current-query)
508
+
(new-query (copy-tree current-query)))
509
+
;; Update the query with the new ID
510
+
(setq new-query (assq-delete-all 'id new-query))
511
+
(when (and id (not (string-empty-p id)))
512
+
(push `(id ,id) new-query))
513
+
;; Execute the updated query
514
+
(caledonia--make-query new-query))))
516
+
(defun caledonia-query-recurring ()
517
+
"Set whether to filter by recurring events for the current calendar view."
519
+
(when (eq major-mode 'caledonia-mode)
520
+
(let* ((recurring-str (completing-read "Recurring events (yes/no/all, leave empty for all): "
521
+
'("" "yes" "no") nil nil nil))
522
+
(recurring (cond ((string= recurring-str "yes") t)
523
+
((string= recurring-str "no") nil)
525
+
(current-query caledonia--current-query)
526
+
(new-query (copy-tree current-query)))
527
+
;; Update the query with the recurring filter
528
+
(setq new-query (assq-delete-all 'recurring new-query))
529
+
(when (not (string-empty-p recurring-str))
530
+
(push `(recurring ,(if (string= recurring-str "yes") t nil)) new-query))
531
+
;; Execute the updated query
532
+
(caledonia--make-query new-query))))
534
+
(defun caledonia-query-limit ()
535
+
"Set the maximum number of events to show in the current calendar view."
537
+
(when (eq major-mode 'caledonia-mode)
538
+
(let* ((limit-str (read-string "Maximum events to show (leave empty for no limit): "
539
+
nil 'caledonia-limit-history))
540
+
(limit (when (and limit-str (not (string-empty-p limit-str)))
541
+
(string-to-number limit-str)))
542
+
(current-query caledonia--current-query)
543
+
(new-query (copy-tree current-query)))
544
+
;; Update the query with the new limit
545
+
(setq new-query (assq-delete-all 'limit new-query))
547
+
(push `(limit ,limit) new-query))
548
+
;; Execute the updated query
549
+
(caledonia--make-query new-query))))
551
+
(defun caledonia-query-timezone ()
552
+
"Set the timezone for the current calendar view."
554
+
(when (eq major-mode 'caledonia-mode)
555
+
(let* ((timezone-str (read-string "Timezone (e.g. Europe/London, leave empty for default): "
556
+
nil 'caledonia-timezone-history))
557
+
(timezone (when (not (string-empty-p timezone-str)) timezone-str))
558
+
(current-query caledonia--current-query)
559
+
(new-query (copy-tree current-query)))
560
+
;; Update the query with the new timezone
561
+
(setq new-query (assq-delete-all 'timezone new-query))
563
+
(push `(timezone ,timezone) new-query))
564
+
;; Execute the updated query
565
+
(caledonia--make-query new-query))))
567
+
;; Buffer functions
569
+
(defun caledonia-show-event ()
570
+
"Show details for the event at point in a separate buffer."
572
+
(when (eq major-mode 'caledonia-mode)
573
+
(let* ((id (tabulated-list-get-id))
574
+
(event (when id (get-text-property 0 'event-data id))))
576
+
(caledonia--display-event-details event)
577
+
(message "No event at point")))))
579
+
(defun caledonia-open-event-file ()
580
+
"Open the file associated with the event at point.
581
+
If the file contains the event ID, the cursor will be positioned at that location."
583
+
(when (eq major-mode 'caledonia-mode)
584
+
(let* ((id (tabulated-list-get-id))
585
+
(event (when id (get-text-property 0 'event-data id)))
586
+
(file (when event (caledonia--get-key 'file event)))
587
+
(event-id (when event (caledonia--get-key 'id event))))
590
+
(message "No event at point"))
592
+
(message "No file associated with this event"))
593
+
((not (file-exists-p file))
594
+
(message "File does not exist: %s" file))
597
+
(caledonia--find-and-highlight-event-in-file file event-id))))))
599
+
(defun caledonia-refresh ()
600
+
"Refresh calendar data from disk and update the current view.
601
+
This is useful when calendar files have been modified outside Emacs
602
+
(for example, by a sync program or direct file edits)."
604
+
(when (eq major-mode 'caledonia-mode)
605
+
;; Send a refresh command to clear the server's cache
606
+
(caledonia--send-request "Refresh")
607
+
;; Re-apply the current query to update the view
608
+
(when (string= (buffer-name) caledonia--events-buffer)
609
+
;; Just use caledonia--make-query without args to use the stored query
610
+
(caledonia--make-query))))
614
+
(defun caledonia-query ()
615
+
"Query events with interactive prompts for all filter parameters.
616
+
Opens a series of prompts to build a complete query and then displays the results.
617
+
After the initial query is displayed, you can further refine the results
618
+
using the caledonia-query-* family of functions."
621
+
(dates (caledonia--read-date-range))
624
+
(timezone-str (read-string "Timezone (e.g. Europe/London, leave empty for default): "
625
+
nil 'caledonia-timezone-history))
626
+
(timezone (when (not (string-empty-p timezone-str)) timezone-str))
627
+
(available-calendars
628
+
(caledonia--send-request "ListCalendars"))
630
+
(if (and (listp available-calendars)
631
+
(eq (car available-calendars) 'Calendars))
632
+
(cadr available-calendars)
634
+
(message "Failed to get calendar list from server")
636
+
(selected-calendars
637
+
(completing-read-multiple
638
+
"Select calendars (comma-separated, empty for all): "
639
+
(or calendars-list '()) nil nil nil 'caledonia-calendars-history))
640
+
(calendars (mapcar #'string-trim selected-calendars))
641
+
(text (read-string "Search text (leave empty for no text search): "
642
+
nil 'caledonia-text-history))
643
+
(search-in-str (when (and text (not (string-empty-p text)))
644
+
(read-string "Search in (summary,description,location - leave empty for all): "
645
+
nil 'caledonia-search-fields-history)))
646
+
(search-in (when (and search-in-str (not (string-empty-p search-in-str)))
647
+
(mapcar (lambda (field)
648
+
(intern (string-trim field)))
649
+
(split-string search-in-str "," t))))
650
+
(id (read-string "Event ID (leave empty for all events): "
651
+
nil 'caledonia-id-history))
652
+
(recurring (completing-read "Recurring events (yes/no/all, leave empty for all): "
653
+
'("" "yes" "no") nil nil nil))
654
+
(limit-str (read-string "Maximum events to show (leave empty for no limit): "
655
+
nil 'caledonia-limit-history))
656
+
(limit (when (and limit-str (not (string-empty-p limit-str)))
657
+
(string-to-number limit-str)))
659
+
;; Build query based on parameters
660
+
(when (and from (not (string-empty-p from)))
661
+
(push `(from ,from) query))
662
+
(when (and to (not (string-empty-p to)))
663
+
(push `(to ,to) query))
665
+
(push `(timezone ,timezone) query))
667
+
(push `(calendars ,calendars) query))
668
+
(when (and text (not (string-empty-p text)))
669
+
(push `(text ,text) query))
671
+
(push `(search_in ,search-in) query))
672
+
(when (and id (not (string-empty-p id)))
673
+
(push `(id ,id) query))
674
+
(when (not (string-empty-p recurring))
675
+
(push `(recurring ,(if (string= recurring "yes") t nil)) query))
677
+
(push `(limit ,limit) query))
678
+
;; Create buffer and execute query
679
+
(let ((buffer (get-buffer-create caledonia--events-buffer)))
680
+
(with-current-buffer buffer
681
+
;; Clear the buffer and reset it
682
+
(let ((inhibit-read-only t))
684
+
;; Activate our mode and make the query
686
+
(caledonia--make-query query)
687
+
(switch-to-buffer buffer)))))
689
+
(defun caledonia-list (&optional from-date to-date)
690
+
"List calendar in a new buffer within the default date range.
691
+
FROM-DATE and TO-DATE override the default date range if provided.
692
+
TO-DATE is required and will use a default if not specified.
693
+
With prefix arg (C-u), prompts for the date range with an interactive calendar."
695
+
(when current-prefix-arg
696
+
(let* ((dates (caledonia--read-date-range)))
697
+
(list (car dates) (cdr dates)))))
698
+
(let ((buffer (get-buffer-create caledonia--events-buffer))
699
+
(from (or from-date caledonia-list-from-date))
700
+
;; Ensure to date is always provided
701
+
(to (or (and to-date (not (string-empty-p to-date)) to-date)
702
+
caledonia-list-to-date)))
703
+
(with-current-buffer buffer
704
+
;; Clear the buffer and reset it
705
+
(let ((inhibit-read-only t))
708
+
(let* ((query `((to ,to))))
709
+
;; Add from date only if specified
710
+
(when (and from (not (string-empty-p from)))
711
+
(setq query (append query `((from ,from)))))
712
+
;; Activate our mode and make the query
714
+
(caledonia--make-query query)
715
+
(switch-to-buffer buffer)))))
717
+
(defun caledonia-search (&optional expr from-date to-date)
718
+
"Search for query EXPR with optional FROM-DATE and TO-DATE.
719
+
This is an interactive function which asks user for EXPR if not passed as an argument.
720
+
With prefix arg (C-u), also prompts for date range with an interactive calendar.
721
+
Use this to find events matching specific text across all calendars.
722
+
TO-DATE is required; a default will be used if not provided."
724
+
(let* ((search-text (read-string "Search for: " nil 'caledonia-search-prompt-history))
725
+
(dates (when current-prefix-arg (caledonia--read-date-range))))
727
+
(when current-prefix-arg (car dates))
728
+
(when current-prefix-arg (cdr dates)))))
729
+
(let ((buffer (get-buffer-create caledonia--events-buffer))
730
+
(from (or from-date caledonia-search-from-date))
731
+
(to (or to-date caledonia-search-to-date)))
732
+
(with-current-buffer buffer
733
+
;; Clear the buffer and reset it
734
+
(let ((inhibit-read-only t))
737
+
(let* ((query `((text ,expr)(to ,to))))
738
+
;; Add from date only if specified
739
+
(when (and from (not (string-empty-p from)))
740
+
(setq query (append query `((from ,from)))))
741
+
;; Activate our mode and make the query
743
+
(caledonia--make-query query)
744
+
(switch-to-buffer buffer)))))
749
+
;; Create a filter prefix map for query refinement
750
+
(defvar caledonia-filter-map
751
+
(let ((map (make-sparse-keymap)))
752
+
(define-key map (kbd "d") 'caledonia-query-date-range)
753
+
(define-key map (kbd "c") 'caledonia-query-calendars)
754
+
(define-key map (kbd "t") 'caledonia-query-text)
755
+
(define-key map (kbd "i") 'caledonia-query-id)
756
+
(define-key map (kbd "r") 'caledonia-query-recurring)
757
+
(define-key map (kbd "l") 'caledonia-query-limit)
758
+
(define-key map (kbd "z") 'caledonia-query-timezone)
760
+
"Keymap for filter commands in Caledonia mode.")
762
+
(defvar caledonia-mode-map
763
+
(let ((map (make-sparse-keymap)))
764
+
(set-keymap-parent map tabulated-list-mode-map)
765
+
(define-key map (kbd "RET") 'caledonia-show-event)
766
+
(define-key map (kbd "M-RET") 'caledonia-open-event-file)
767
+
(define-key map (kbd "l") 'caledonia-list)
768
+
(define-key map (kbd "s") 'caledonia-search)
769
+
(define-key map (kbd "r") 'caledonia-refresh)
770
+
(define-key map (kbd "q") 'quit-window)
771
+
;; Individual filter command bindings
772
+
(define-key map (kbd "C-c d") 'caledonia-query-date-range)
773
+
(define-key map (kbd "C-c c") 'caledonia-query-calendars)
774
+
(define-key map (kbd "C-c t") 'caledonia-query-text)
775
+
(define-key map (kbd "C-c i") 'caledonia-query-id)
776
+
(define-key map (kbd "C-c r") 'caledonia-query-recurring)
777
+
(define-key map (kbd "C-c l") 'caledonia-query-limit)
778
+
(define-key map (kbd "C-c z") 'caledonia-query-timezone)
779
+
;; Use f prefix for filter commands
780
+
(define-key map (kbd "C-c f") caledonia-filter-map)
782
+
"Keymap for Caledonia mode.")
784
+
(define-derived-mode caledonia-mode tabulated-list-mode "Caledonia"
785
+
"Major mode for displaying calendar entries in a tabular view.")
787
+
;; Define a prefix map specifically for Evil mode
788
+
(defvar caledonia-evil-filter-map
789
+
(let ((map (make-sparse-keymap)))
790
+
(define-key map "d" 'caledonia-query-date-range)
791
+
(define-key map "c" 'caledonia-query-calendars)
792
+
(define-key map "t" 'caledonia-query-text)
793
+
(define-key map "i" 'caledonia-query-id)
794
+
(define-key map "r" 'caledonia-query-recurring)
795
+
(define-key map "l" 'caledonia-query-limit)
796
+
(define-key map "z" 'caledonia-query-timezone)
798
+
"Evil mode keymap for filter commands in Caledonia mode.")
800
+
(eval-after-load 'evil
802
+
;; Basic navigation and commands
803
+
(evil-define-key 'normal caledonia-mode-map
804
+
(kbd "RET") 'caledonia-show-event
805
+
(kbd "M-RET") 'caledonia-open-event-file
806
+
"l" 'caledonia-list
807
+
"s" 'caledonia-search
808
+
"r" 'caledonia-refresh
810
+
;; Set up a proper Evil prefix key
811
+
(evil-define-key 'normal caledonia-mode-map "f" caledonia-evil-filter-map)))
813
+
(defun caledonia--setup-evil-integration ()
814
+
"Set up Evil integration for Caledonia mode."
815
+
(when (bound-and-true-p evil-mode)
816
+
(evil-make-overriding-map caledonia-mode-map 'normal)
817
+
(evil-normalize-keymaps)))
819
+
(add-hook 'caledonia-mode-hook 'caledonia--setup-evil-integration)
821
+
(provide 'caledonia)
822
+
;;; caledonia.el ends here