phil@bajsicki:~$


Neat trees from org-ql to org-mode

Over the past few weeks I’ve been working on a rather large inventory of items stored and held in org-mode.

For the purposes of this example, let’s use the current buffer, bajsicki.com.org

I started using org-ql to grab the data I need, in the form of:

(let ((org-ql-cache (make-hash-table)))
(org-ql-select "bajsicki.com.org"
  '(tags-local "@emacs")
  :action (lambda () (org-get-outline-path t))))

RESULTS:

(("Blog" "Tech" "Neat trees from org-ql to org-mode") ("Blog" "Tech" "I really, really like Emacs") ("Blog" "Tech" "A new look: ox-tufte") ("Blog" "Tech" "Moving to Hugo") ("Blog" "Tech" "Some improvements for my ox-hugo set-up"))

So, this is neat. But notice how it’s a list of lists, and there’s a lot of repetition.

Suppose you wanted to see this in a more pleasant form, maybe even one that you can interact with in org-mode directly.

To that end, I ended up writing two functions.

  1. Turn the list of lists we get from org-ql into a tree.
(defun phil/make-tree-from-nested-lists (lists)
  (if (not lists) ;; been an issue, isn't any more
      nil
    (let ((grouped-lists
	   (seq-group-by #'car
			 (cl-remove-if nil lists))))
      ;;would occasionally return (nil nil) conses, so we filter them out
      (mapcar (lambda (group)
                (let ((key (car group))
                      (sublists (mapcar #'cdr (cdr group))))
                  (cons key (if (not (every #'null sublists))
                                (phil/make-tree-from-nested-lists sublists)
                              sublists))))
              grouped-lists))))
  1. Format and output that tree into org-mode.
(defun phil/org-list-from-tree (tree &optional indent)
  (let ((indent (or indent "")))
    (mapconcat (lambda (item) ;; actual org-mode indentation on output? In MY Emacs?
		 (unless (eq item (cons nil nil))
		   (when (consp item)
		     (format "%s- %s\n%s" indent (car item)
			     (phil/org-list-from-tree (cdr item) (concat indent "  ")))
		     (format "%s- %s\n" indent item))))
    tree "")))

Example:

(let ((tree (phil/make-tree-from-nested-lists
	     (let ((org-ql-cache (make-hash-table)))
	       (org-ql-select (get-buffer "bajsicki.com.org") ;;the file I write my blog in
		 '(and (tags-local "@emacs"))
		 :action (lambda ()
			   (-snoc ;;this gets the tag list as well, on top of the heading text itself
			    (org-get-outline-path)
			    (replace-regexp-in-string
			     "^*+\s" "" ;; remove initial asterisks, since we're already indenting
			     (buffer-substring-no-properties
			      (line-beginning-position)
			      (line-end-position))))))))))

  (phil/org-list-from-tree tree))

RESULTS:

Pretty neat.

Now, this isn’t ideal or even close to good. Here’s known issues:

  1. It’s jank. I’m sure there’s a cleaner way of doing this.
  2. It gets tags. If you don’t want them, replace the entire dash.el -snoc form with just (org-get-outline-path t).
  3. Sometimes nil would slip in, and so I’m just removing all nils to start with. This may cause unintended issues.
  4. This hasn’t been extensively tested, and I only tested it with the tags-local org-ql predicate.

So yeah. There. I may end up wrapping these in a function of some sort, but for the time being this is entirely sufficient for my purposes. We’ll see.