Hello, The attached 2 patches add support for exporting unscheduled tasks and repeating tasks to iCalendar, respectively.
For patch 1 (unscheduled tasks): Currently, ox-icalendar does not allow creating an iCalendar task without a scheduled start date. If an Org TODO is missing a SCHEDULED timestamp, then ox-icalendar sets today as the scheduled start date for the exported task. Patch 1 changes this by adding a new customization org-icalendar-todo-force-scheduling. When non-nil, the start date is set to today (same as the current behavior). When nil, unscheduled Org TODOs are instead exported without a start date. I also propose the default value to be nil. Note, this is backwards-incompatible with the previous behavior! But I think it should be the default anyways, because IMO it is the more correct and useful behavior. An iCalendar VTODO without a DTSTART property is valid, and has the same meaning as an Org TODO without a SCHEDULED timestamp. Also, all the iCalendar programs I have tried support unscheduled tasks, including Thunderbird, Evolution, Nextcloud, and Tasks.org. For patch 2 (repeating timestamps): I add recurrence rule (RRULE) export for repeating SCHEDULED and DEADLINE timestamps in TODOs, similar to how repeating non-TODO events are currently handled. The main complication here is that iCalendar's RRULE applies to both DTSTART and DUE properties; by contrast, Org's SCHEDULED and DEADLINE timestamps may have different repeaters. I am not sure the best way to handle the case where SCHEDULED and DEADLINE have different repeaters, so in that case I issue a warning and skip the repeater.
>From 1bd268ab260d5077d7456c0d64fea36128772f86 Mon Sep 17 00:00:00 2001 From: Jack Kamm <jackk...@gmail.com> Date: Sun, 26 Mar 2023 07:43:53 -0700 Subject: [PATCH 1/2] ox-icalendar: Allow exporting unscheduled VTODOs * lisp/ox-icalendar.el (org-icalendar-todo-force-scheduling): New option to revert to previous export behavior of unscheduled TODOs. (org-icalendar--vtodo): Don't force unscheduled TODOs to have a scheduled start time of today, unless `org-icalendar-todo-force-scheduling' is set. --- etc/ORG-NEWS | 15 +++++++++++++++ lisp/ox-icalendar.el | 32 +++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index ac233a986..fb4f82b29 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -23,6 +23,15 @@ If you still want to use python-mode with ob-python, you might consider [[https://gitlab.com/jackkamm/ob-python-mode-mode][ob-python-mode-mode]], where the code to support python-mode has been ported to. +*** Icalendar export of TODOs no longer forces a start time + +For TODOs without a scheduled start time, ox-icalendar no longer +forces them to have a scheduled start time of today when exporting. +This makes it possible to create icalendar TODOs without a start time. + +To revert to the old behavior, set the new custom option +~org-icalendar-todo-force-scheduling~ to non-nil. + ** New and changed options *** New ~org-cite-natbib-export-bibliography~ option defining fallback bibliography style @@ -111,6 +120,12 @@ backend used for evaluation of ClojureScript. official [[https://clojure.org/guides/deps_and_cli][Clojure CLI tools]]. The command can be customized with ~ob-clojure-cli-command~. +*** New ~org-icalendar-todo-force-scheduling~ option for old ox-icalendar TODO scheduling behavior + +Set ~org-icalendar-todo-force-scheduling~ to non-nil to revert to the +old ox-icalendar TODO export behavior, that forced all exported TODOs +to have a scheduled start time. + ** New features *** Add support for ~logind~ idle time in ~org-user-idle-seconds~ diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el index 81a77a770..63aefcc84 100644 --- a/lisp/ox-icalendar.el +++ b/lisp/ox-icalendar.el @@ -231,6 +231,12 @@ (defcustom org-icalendar-include-todo nil (repeat :tag "Specific TODO keywords" (string :tag "Keyword")))) +(defcustom org-icalendar-todo-force-scheduling nil + "Non-nil means unscheduled tasks are exported as scheduled. +The current date is used as the scheduled time for such tasks." + :group 'org-export-icalendar + :type 'boolean) + (defcustom org-icalendar-include-bbdb-anniversaries nil "Non-nil means a combined iCalendar file should include anniversaries. The anniversaries are defined in the BBDB database." @@ -776,21 +782,25 @@ (defun org-icalendar--vtodo Return VTODO component as a string." (let ((start (or (and (memq 'todo-start org-icalendar-use-scheduled) (org-element-property :scheduled entry)) - ;; If we can't use a scheduled time for some - ;; reason, start task now. - (let ((now (decode-time))) - (list 'timestamp - (list :type 'active - :minute-start (nth 1 now) - :hour-start (nth 2 now) - :day-start (nth 3 now) - :month-start (nth 4 now) - :year-start (nth 5 now))))))) + (when org-icalendar-todo-force-scheduling + ;; If we can't use a scheduled time for some + ;; reason, start task now. + (let ((now (decode-time))) + (list 'timestamp + (list :type 'active + :minute-start (nth 1 now) + :hour-start (nth 2 now) + :day-start (nth 3 now) + :month-start (nth 4 now) + :year-start (nth 5 now)))))))) (org-icalendar-fold-string (concat "BEGIN:VTODO\n" "UID:TODO-" uid "\n" (org-icalendar-dtstamp) "\n" - (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n" + (when start + (concat (org-icalendar-convert-timestamp + start "DTSTART" nil timezone) + "\n")) (and (memq 'todo-due org-icalendar-use-deadline) (org-element-property :deadline entry) (concat (org-icalendar-convert-timestamp -- 2.39.2
>From 8348f5b8c56087f0fb8cdd775a816f63cb57f38f Mon Sep 17 00:00:00 2001 From: Jack Kamm <jackk...@gmail.com> Date: Sun, 26 Mar 2023 10:37:47 -0700 Subject: [PATCH 2/2] ox-icalendar: Support repeating timestamps in TODOs * lisp/ox-icalendar.el (org-icalendar--rrule): New helper function to generate RRULE. (org-icalendar--vevent): Use `org-icalendar--rrule' instead of generating the RRULE directly. (org-icalendar--vtodo): Generate RRULE for repeating scheduled and deadline timestamps. --- etc/ORG-NEWS | 13 ++++++++++++ lisp/ox-icalendar.el | 50 +++++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index fb4f82b29..3919b240e 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -159,6 +159,19 @@ Running shell blocks with the ~:session~ header freezes Emacs until execution completes. The new ~:async~ header allows users to continue editing with Emacs while a ~:session~ block executes. +*** Add support for repeating tasks in iCalendar export + +Repeating Scheduled and Deadline timestamps in TODOs are now exported +as recurring tasks in iCalendar export. + +Note that in Org-mode, the repeaters for the Scheduled and Deadline +timestamps can be different; whereas in iCalendar, the recurrence rule +applies to both the scheduled start time and the deadline due date. + +In case the timestamp repeaters contradict, the correct export +behavior is not well-defined. Currently, Org-mode will issue a +warning and skip the repeaters in this case. + ** Miscellaneous *** Remove undocumented ~:target~ header parameter in ~ob-clojure~ diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el index 63aefcc84..179795ac9 100644 --- a/lisp/ox-icalendar.el +++ b/lisp/ox-icalendar.el @@ -726,6 +726,13 @@ (defun org-icalendar-entry (entry contents info) ;; Don't forget components from inner entries. contents)))) +(defun org-icalendar--rrule (unit value) + (format "RRULE:FREQ=%s;INTERVAL=%d\n" + (cl-case unit + (hour "HOURLY") (day "DAILY") (week "WEEKLY") + (month "MONTHLY") (year "YEARLY")) + value)) + (defun org-icalendar--vevent (entry timestamp uid summary location description categories timezone class) "Create a VEVENT component. @@ -752,12 +759,9 @@ (\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others (org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n" (org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n" ;; RRULE. - (when (org-element-property :repeater-type timestamp) - (format "RRULE:FREQ=%s;INTERVAL=%d\n" - (cl-case (org-element-property :repeater-unit timestamp) - (hour "HOURLY") (day "DAILY") (week "WEEKLY") - (month "MONTHLY") (year "YEARLY")) - (org-element-property :repeater-value timestamp))) + (org-icalendar--rrule + (org-element-property :repeater-unit timestamp) + (org-element-property :repeater-value timestamp)) "SUMMARY:" summary "\n" (and (org-string-nw-p location) (format "LOCATION:%s\n" location)) (and (org-string-nw-p class) (format "CLASS:%s\n" class)) @@ -792,7 +796,9 @@ (defun org-icalendar--vtodo :hour-start (nth 2 now) :day-start (nth 3 now) :month-start (nth 4 now) - :year-start (nth 5 now)))))))) + :year-start (nth 5 now))))))) + (due (and (memq 'todo-due org-icalendar-use-deadline) + (org-element-property :deadline entry)))) (org-icalendar-fold-string (concat "BEGIN:VTODO\n" "UID:TODO-" uid "\n" @@ -801,11 +807,31 @@ (defun org-icalendar--vtodo (concat (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n")) - (and (memq 'todo-due org-icalendar-use-deadline) - (org-element-property :deadline entry) - (concat (org-icalendar-convert-timestamp - (org-element-property :deadline entry) "DUE" nil timezone) - "\n")) + (when due + (concat (org-icalendar-convert-timestamp + due "DUE" nil timezone) + "\n")) + ;; RRULE + (let ((start-repeater-unit (org-element-property + :repeater-unit start)) + (start-repeater-value (org-element-property + :repeater-value start)) + (due-repeater-unit (org-element-property + :repeater-unit due)) + (due-repeater-value (org-element-property + :repeater-value due))) + (when (or start-repeater-value due-repeater-value) + (if (and start due + (not (and (eql start-repeater-unit + due-repeater-unit) + (eql start-repeater-value + due-repeater-value)))) + (progn (warn "Scheduled and Deadline repeaters are not equal. Skipping repeater export.") + nil) + (org-icalendar--rrule (or start-repeater-unit + due-repeater-unit) + (or start-repeater-value + due-repeater-value))))) "SUMMARY:" summary "\n" (and (org-string-nw-p location) (format "LOCATION:%s\n" location)) (and (org-string-nw-p class) (format "CLASS:%s\n" class)) -- 2.39.2