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))

Enjoyed this stream? Explore our hands-on courses for deeper, structured learning on Guile Scheme and more.

Get the System Crafters Newsletter
Updates on open source tools, tutorials, and community projects. We'll also occasionally let you know about new courses and resources.
Name (optional)
Email Address