+++
title = "Neat trees from org-ql to org-mode"
lastmod = 2025-04-21T14:19:29+02:00
tags = ["orgmode", "elisp"]
categories = ["tech", "emacs"]
draft = false
meta = true
type = "list"
[menu]
  [menu.posts]
    weight = 3001
    identifier = "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:

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

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.

<!--listend-->

```elisp
(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.

<!--listend-->

```elisp
(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:

```elisp
(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))
```

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.