+++ title = "Neat trees from org-ql to org-mode" lastmod = 2025-04-21T15:16:56+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)) ``` RESULTS: - Blog - Tech - Neat trees from org-ql to org-mode :@emacs:orgmode:elisp: - I really, really like Emacs :@emacs:orgmode:@tech: - A new look: ox-tufte :@emacs:orgmode:web:css:tufte:@tech: - Moving to Hugo :hugo:web:orgmode:css:tufte:@emacs: - Some improvements for my ox-hugo set-up :hugo:web:orgmode:@emacs: 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.