Created
August 2, 2024 09:34
-
-
Save jsntn/542443e8505d62cac635eacd8bce458a to your computer and use it in GitHub Desktop.
Repeat tasks M-F but not weekends on Emacs org-mode, see https://stackoverflow.com/questions/28369833/emacs-org-mode-repeat-tasks-m-f-but-not-weekends
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;; 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