Adding a Function to Carry Forward an Org-Mode Agenda Item

Delving into org-mode and org-element-map

I spent some time hacking on Emacs. What follows builds from Org Mode Capture Templates and Time Tracking.

My intention in sharing this post is to add to the corpus of examples for Org Element API; to share working with the constraints of org-element-at-point; namely that the element at point does load content of that element. To do so, you must parse the buffer.

Over the last 3 months I’ve been using Org-Mode’s Clocking Commands. I have settled on the following structure:

  • The first three headline level’s come from the datetree target; levels for year, month and day if you will.
  • The fourth level is for projects; these are for clients and we bill at the project level.
  • The fifth heading is a task; I track my time on the tasks.

Headlines deeper than level 5 are for organizational purposes.

I tag the fourth level heading with a :project: tag and the fifth level heading with a :task: tag; but for this post the tags are irrelevant.

Below is an example of my structure.

* 2022
** 2022-10 October
*** 2022-10-23 Sunday
**** Samvera :project:
***** Review pull requests on samvera/hyrax :task:

The Problem Statement

My logbook entries track time to the task level. I don’t always complete a task within a single day. Which means I need to carry forward a task from one day to another.

When I carry forward that task, I want all of the prior context except the logbook. If I bring forward the logbook, this messes up my time reporting process.

Code

The following function moves point to the task level. It recursively walks up to a level 5 headline.

(defun jf/org-agenda-task-at-point ()
  "Find the `org-mode' task at point."
  (let ((element (org-element-at-point)))
    (if (eq 'headline (org-element-type element))
	(pcase (org-element-property :level element)
	  (1 (error "Selected element is a year"))
	  (2 (error "Selected element is a month"))
	  (3 (error "Selected element is a day"))
	  (4 (error "Selected element is a project"))
	  (5 (progn (message "%s" element) element))
	  (_ (progn (org-up-heading-safe) (jf/org-task-at-point))))
      (progn
	(org-back-to-heading)
	(jf/org-task-at-point)))))

Below, the jf/org-agenda-get-day-and-project-and-task-at-point function retrieves the task and it’s associated project and day.

(defun jf/org-agenda-get-day-and-project-and-task-at-point ()
  "Return a plist of :day, :project, and :task for element at point."
  (let* ((task (jf/org-agenda-task-at-point))
	 (project (progn
		    (org-up-heading-safe)
		    (org-element-at-point)))
	 (day (progn
		(org-up-heading-safe)
		(org-element-at-point))))
    (list :project project :task task :day day)))

Now we get to the interactive function jf/org-agenda-carry-forward-task; this does the in buffer adjustments to carry the text forward.

(cl-defun jf/org-agenda-carry-forward-task ()
  "Carry an `org-mode' task node forward."
  (interactive)
  (save-excursion
    (let* ((day-project-task (jf/org-agenda-get-day-and-project-and-task-at-point))
	   (from-project (plist-get day-project-task :project))
	   (from-task (plist-get day-project-task :task)))

      ;; Narrowing the region to perform quicker queries on the element
      (narrow-to-region (org-element-property :begin from-task)
			(org-element-property :end from-task))

      ;; Grab each section for the from-task and convert that into text.
      ;;
      ;; Yes we have the from-task, however, we haven't parsed that entity.
      ;; Without parsing that element, the `org-element-contents' returns nil.
      (let ((content (s-join "\n" (org-element-map (org-element-parse-buffer) 'section
				    (lambda (section)
				      (mapconcat
				       (lambda (element)
					 (pcase (org-element-type element)
					   ;; I want to skip my time entries
					   ('drawer nil)
					   (_ (buffer-substring-no-properties
					       (org-element-property :begin element)
					       (org-element-property :end element)))))
				       (org-element-contents section)
				       "\n"))))))

	;; Capture the following to the “Day with plain entry”.  Because it’s a
	;; plain node, I’m adding the headline level.
	(org-capture-string (format "%s %s :%s:\n\n%s %s %s :%s:\n%s"
				    (s-repeat (org-element-property :level from-project) "*")
				    (org-element-property :title from-project)
				    (s-join ":" (org-element-property :tags from-project))

				    (s-repeat (org-element-property :level from-task) "*")
				    (org-element-property :todo-keyword from-task)
				    (org-element-property :title from-task)
				    (s-join ":" (org-element-property :tags from-task))
				    content)
			    "d")

	;; Widen what we once narrowed
	(widen))
      ;; Now that we've added the content, let's tidy up the from-task.
      (goto-char (org-element-property :begin from-task))
      ;; Prompt for the todo state of the original task.
      (call-interactively 'org-todo))))

And below is one of the entries I added to org-capture-templates. Note, I’m using the plain type.

("d" "Day with plain entry"
 plain (file+olp+datetree jf/primary-agenda-filename-for-machine)
 "%i"
 :empty-lines 1
 :time-prompt t
 :immediate-finish t)

Conclusion

My hope is that this blog post and associated code can add to the shared examples of how to work with the Org Element API.

Going forward, I might look to amend the previous task to indicate that I carried the task forward. I might also add a line item saying, task carried forward from the date.

As with all things Emacs, once you begin you start seeing all kinds of options unfold.