Skip to content

Instantly share code, notes, and snippets.

@jsntn
Created August 2, 2024 09:34
Show Gist options
  • Save jsntn/542443e8505d62cac635eacd8bce458a to your computer and use it in GitHub Desktop.
Save jsntn/542443e8505d62cac635eacd8bce458a to your computer and use it in GitHub Desktop.
;; START: my :workday: todo task schedule update
(require 'org)
(require 'calendar)
(defun my-is-repeating-workday-task-p ()
"Check if the current TODO task is a repeating workday task.
A repeating workday task is defined as a task that has the tag :workday:
and a SCHEDULED property with a repeater interval of +1d/++1d/.+1d."
(save-excursion
(org-back-to-heading t)
(let ((tags (org-get-tags))
(scheduled (org-entry-get (point) "SCHEDULED")))
(and scheduled
(member "workday" tags)
(or
(string-match-p "\\+1d" scheduled) ; exact 1 day
(string-match-p "\\.+1d" scheduled) ; 1 day from today
(string-match-p "\\++1d" scheduled)) ; 1 day from today
))))
(defun my-is-one-day-repeater-p ()
"Check the repeater for current TASK.
Return t for a 1-day-repeater, nil for at-least-1-day-repeater."
(interactive)
(let* ((element (org-element-at-point))
(schedule (org-element-property :scheduled element))
(repeater-type (and schedule (org-element-property :repeater-type schedule)))
(repeater-value (and schedule (org-element-property :repeater-value schedule)))
(repeater-unit (and schedule (org-element-property :repeater-unit schedule)))
)
;; (message "%s" schedule)
;; (message "%s" repeater-type)
;; (message "%s" repeater-value)
;; (message "%s" repeater-unit)
(when (and (= repeater-value 1)
(string= repeater-unit "day"))
(if (or (string= repeater-type "catch-up") (string= repeater-type "restart"))
nil
t))))
(defun my/schedule-workday-todo ()
"Set the SCHEDULED date of a repeating :workday: TODO to the next workday
if the current SCHEDULED date is Mon-Thu, or to the next Monday if it's Fri-Sun.
Display a colored message in the minibuffer."
(interactive)
(save-excursion
(org-back-to-heading t)
(when (my-is-repeating-workday-task-p)
(if (my-is-one-day-repeater-p)
(let* ((scheduled (org-entry-get (point) "SCHEDULED"))
(date (org-parse-time-string scheduled))
(day-of-week (nth 6 (decode-time (apply 'encode-time date)))))
;; (message "%s" date)
;; (message "%s" day-of-week)
(my-update-schedule-date date day-of-week))
;; Return the current day of the week as a number (1 for Monday, 7 for Sunday)
(let ((date (decode-time (current-time)))
(day-of-week (string-to-number (format-time-string "%u"))))
(if (= day-of-week 7)
0 ;; Convert Sunday from 7 to 0
day-of-week)
;; (message "%s" day-of-week)
(my-update-schedule-date date day-of-week))
))))
(defun my-update-schedule-date (date day-of-week)
(cond
((<= 1 day-of-week 4) ; Monday to Thursday (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
(let* ((new-date (time-add (apply 'encode-time date) (* 86400 1))) ; 86400 seconds in a day
(new-date-string (format-time-string "%Y-%m-%d" new-date)) ; Format without time
(new-date-with-day (format-time-string "%a" new-date))) ; Day of the week (e.g., Mon, Tue)
(org-schedule nil new-date-string)
(my-insert-properties-entry "LAST_REPEAT" (format-time-string "[%Y-%m-%d %a %H:%M]"))
(my-insert-drawer-content "LOGBOOK"
(format "- State \"DONE\" from \"TODO\" [%s]"
(format-time-string "%Y-%m-%d %a %H:%M")))
(message (propertize (format "Scheduled date updated to %s (%s)" new-date-string new-date-with-day)
'face '(:foreground "green")))))
(t ; Friday to Sunday
(let* ((days-to-next-monday (- 8 day-of-week)) ; Calculate days to next Monday
(new-date (time-add (apply 'encode-time date) (* 86400 days-to-next-monday))) ; 86400 seconds in a day
(new-date-string (format-time-string "%Y-%m-%d" new-date)) ; Format without time
(new-date-with-day (format-time-string "%a" new-date))) ; Day of the week (e.g., Mon, Tue)
(org-schedule nil new-date-string)
(my-insert-properties-entry "LAST_REPEAT" (format-time-string "[%Y-%m-%d %a %H:%M]"))
(my-insert-drawer-content "LOGBOOK"
(format "- State \"DONE\" from \"TODO\" [%s]"
(format-time-string "%Y-%m-%d %a %H:%M")))
(message (propertize (format "Scheduled date updated to %s (%s)" new-date-string new-date-with-day)
'face '(:foreground "green")))))))
(defun my-org-entry-has-drawer (drawer)
"Check if the current Org entry has a specific DRAWER as a standalone line."
(save-excursion
(org-back-to-heading t)
(let ((end (save-excursion (outline-next-heading) (point))))
(re-search-forward (format "^:%s:$" drawer) end t))))
(defun my-insert-properties-entry (property value)
"Insert or update PROPERTY with VALUE in the :PROPERTIES: drawer."
(interactive)
(save-excursion
(org-back-to-heading t)
(if (org-entry-get (point) property)
;; if PROPERTY exists
(progn
(org-delete-property property)
(org-set-property property value)
)
;; if PROPERTY does not exist
(org-set-property property value))))
;; (my-insert-properties-entry "LAST_REPEAT" (format-time-string "[%Y-%m-%d %a %H:%M]"))
(defun my-insert-drawer-content (drawer content)
"Insert a LOGBOOK entry below the current task if it doesn't already exist,
or add to the existing LOGBOOK drawer if it does."
(interactive)
(save-excursion
(org-back-to-heading t)
(if (my-org-entry-has-drawer drawer)
;; If DRAWER exists, check if entry already exists
(progn
(re-search-forward (format "^:%s:$" drawer))
(forward-line 1)
(insert (format "%s\n" content)))
;; Otherwise, create DRAWER and insert content
(progn
(org-end-of-meta-data t)
(org-insert-drawer nil drawer)
(insert content)))))
;; (my-insert-drawer-content "LOGBOOK" (format "- State \"DONE\" from \"TODO\" [%s]" (format-time-string "%Y-%m-%d %a %H:%M")))
(defun my-org-todo ()
"Change TODO state and reschedule if necessary."
(interactive)
(if (my-is-repeating-workday-task-p)
(my/schedule-workday-todo)
(org-todo)))
;; Override the key binding for C-c C-t to use my/org-todo-and-reschedule
(define-key org-mode-map (kbd "C-c C-t") 'my-org-todo)
;; END: my :workday: todo task schedule update
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment