Automating Org Mode Tasks with Emacs Lisp

News

  • Reminder about the EmacsConf 2024 Call for Participation:

    The conference will be December 7 and 8 this year:

    https://emacsconf.org/2024/cfp/

    If you have an Emacs-related topic you’re excited about, consider submitting a proposal!

Let’s Hack on Org Mode Files!

This week I made a post on socials asking Emacs users how much their life would be improved by understanding how to write Emacs Lisp code at an intermediate level:

https://fosstodon.org/@daviwil/112756516386132987

This post generated quite a lot of responses and discussion! One recurring theme I saw was the desire to automate Org Mode tasks with Emacs Lisp.

Let’s learn a bit about how we might do that by hacking on a few useful tasks:

  • Looping over all TODO items in a file
  • Refiling all DONE tasks to another file
  • Sorting all TODO items under a heading based on their task state
  • Scraping Org Agenda items to extract details
  • Exporting clocked task data to another format
  • What else?

Useful Functions

  • org-map-entries: Loops over all headings in an org document, including filtering, etc
  • org-entry-get: Gets the value of a property of a given entry

Existing Code for Agenda Scraping

(defun dw/get-schedule-entries ()
  "Get all daily agenda entries with the category 'Schedule'."
  (let ((entries '()))
    (save-window-excursion
      (org-agenda nil "a")
      (goto-char (point-min))
      (while (org-agenda-next-item 1)
        (when (string= (get-text-property (point) 'org-category)
                       "Schedule")
          (push entry entries))))
      entries))

Extracting details about the agenda item at point:

(get-text-property (point) 'time) ;; 14:00-16:00
(get-text-property (point) 'time-of-day) ;; 1400
(get-text-property (point) 'duration) ;; 120.0 or nil if no range

(let* ((time-of-day (get-text-property (point) 'time-of-day))
       (hour (/ time-of-day 100))
       (min (% time-of-day 100))
       (current-time (decode-time))
       (current-hour (nth 2 current-time))
       (current-min (nth 1 current-time)))
  ;; do comparison here
  )

The final code

;; -*- lexical-binding: t; -*-

;; (with-current-buffer "Tasks.org"
;;   (org-map-entries (lambda ()
;;                      (org-entry-get nil "TODO"))
;;                    "+TODO=\"DONE\""))

(defun my/refile-heading-to-file-heading (file heading)
  (let ((pos (save-excursion
               (find-file-noselect file)
               (org-find-exact-headline-in-buffer heading))))
    (org-refile nil nil (list heading file nil pos))))

(defun my/refile-done-tasks-to-archive ()
  (interactive)
  (let ((archive-file-name
         (format "%s_archive.org"
                 (file-name-sans-extension (buffer-file-name)))))
    (org-map-entries (lambda ()
                       (my/refile-heading-to-file-heading
                        archive-file-name
                        "Archived Tasks"))
                     "+TODO=\"DONE\"")))

(defun my/org-move-done-tasks-to-bottom ()
  "Sort all tasks in the topmost heading by TODO state."
  (interactive)
  (save-excursion
    (while (org-up-heading-safe))
    (org-sort-entries nil ?o))

  ;; Reset the view of TODO items
  (org-overview)
  (org-show-entry)
  (org-show-children))
Subscribe to the System Crafters Newsletter!
Stay up to date with the latest System Crafters news and updates! Read the Newsletter page for more information.
Name (optional)
Email Address