Skip to content

Instantly share code, notes, and snippets.

@akashpal-21
Last active October 20, 2025 17:22
Show Gist options
  • Select an option

  • Save akashpal-21/b9d07fb981dd245b1defd87bb42e4d0d to your computer and use it in GitHub Desktop.

Select an option

Save akashpal-21/b9d07fb981dd245b1defd87bb42e4d0d to your computer and use it in GitHub Desktop.
;; README — org-roam-sequence
;;
;; Helpers
;; -------
;; org-roam-sequence-id-split (id)
;; > split "1a2" -> (1 "a" 2).
;;
;; org-roam-sequence-id-join (parts)
;; > join (1 "a" 2) -> "1a2".
;;
;; org-roam-sequence--alpha-inc (s)
;; > bijective base-26 increment: a->b, z->aa, az->ba.
;;
;; org-roam-sequence--fetch-progeny (prefix expected-parts)
;; > query DB for ids with given prefix.
;; > return only direct children tails (no grandchildren).
;;
;; org-roam-sequence--next-id (parts &optional child)
;; > compute next suffix by scanning existing tails.
;; > numeric -> max+1, alpha -> longest+lexicographic then alpha-inc.
;;
;; Interfaces
;; ----------
;; org-roam-sequence-next-sibling-id (id)
;; > compute next sibling id.
;;
;; org-roam-sequence-next-child-id (id)
;; > compute next child id.
;;
;; org-roam-sequence-add-child
;; org-roam-sequence-add-sibling
;; > interactive commands, prompt for title, capture node.
;;
;; Control flow
;; ------------
;; - Split id into parts.
;; - Decide prefix: parent for sibling, self for child.
;; - Fetch progeny of prefix.
;; - If last part numeric -> find max numeric tail -> +1.
;; - If last part alpha -> find best alpha tail -> alpha-inc.
;; - Join prefix + new suffix.
;; Helpers and API
(defun org-roam-sequence-id-split (id)
"Split alphanumeric ID into alternating parts.
Example: \"1a2\" -> (1 \"a\" 2)."
(let ((i 0) parts)
(while (< i (length id))
(cond
((string-match "\\`[0-9]+" (substring id i))
(push (string-to-number (match-string 0 (substring id i))) parts)
(setq i (+ i (length (match-string 0 (substring id i))))))
((string-match "\\`[a-z]+" (substring id i))
(push (match-string 0 (substring id i)) parts)
(setq i (+ i (length (match-string 0 (substring id i))))))))
(nreverse parts)))
(defun org-roam-sequence-id-join (parts)
"Join PARTS back into an alphanumeric ID."
(mapconcat (lambda (p) (if (numberp p) (number-to-string p) p)) parts ""))
(defun org-roam-sequence--alpha-inc (s)
"Increment bijective base-26 string S: a->b, z->aa, az->ba."
(let* ((chars (string-to-list (downcase s)))
(n (length chars))
(carry 1))
(dotimes (i n)
(let* ((idx (- n i 1))
(c (nth idx chars)))
(if (= carry 1)
(if (= c ?z)
(setf (nth idx chars) ?a
carry 1)
(setf (nth idx chars) (1+ c)
carry 0)))))
(if (= carry 1)
(concat "a" (apply #'string chars))
(apply #'string chars))))
(defun org-roam-sequence--fetch-progeny (prefix expected-parts)
"Return list of tail strings that are direct children of PREFIX.
Drops all instances of grandchildren.
Assumption: No orphan.Ex: !exist 3c3 w/o exist 3c."
(let* ((sql (concat "select id from nodes where id like '\"" prefix "%%\"';"))
(rows (org-roam-db-query sql)))
(delq nil
(mapcar (lambda (row)
(let* ((id (car row))
(parts (mapcar (lambda (p) (if (numberp p) (number-to-string p) p))
(org-roam-sequence-id-split id)))
(tail (car (last parts))))
(when (and (= (length parts) (1+ expected-parts))
(or (string-match-p "\\`[0-9]+\\'" tail)
(string-match-p "\\`[a-z]+\\'" (downcase tail))))
tail)))
rows))))
(defun org-roam-sequence--next-id (parts &optional child)
"Compute next id from PARTS.
If CHILD is non-nil compute next child,
otherwise compute available sibling."
(let* ((last (car (last parts)))
(prefix-parts (if child parts
(butlast parts)))
(expected (length prefix-parts))
(prefix (org-roam-sequence-id-join prefix-parts))
(tails (org-roam-sequence--fetch-progeny prefix expected))
(numericp (numberp last)))
(if numericp
(let ((maxn 0))
(dolist (tail tails)
(when (string-match-p "\\`[0-9]+\\'" tail)
(setq maxn (max maxn (string-to-number tail)))))
(org-roam-sequence-id-join (append prefix-parts (list (number-to-string (1+ maxn))))))
(let ((best nil))
(dolist (tail tails)
(when (and (stringp tail) (string-match-p "\\`[a-z]+\\'" tail))
(when (or (null best)
(> (length tail) (length best))
(and (= (length tail) (length best)) (string> tail best)))
(setq best tail))))
(org-roam-sequence-id-join
(append prefix-parts (list (if best (org-roam-sequence--alpha-inc best) "a"))))))))
(defun org-roam-sequence-next-sibling-id (id)
"Return next available sibling id for ID."
(org-roam-sequence--next-id (org-roam-sequence-id-split id) nil))
(defun org-roam-sequence-next-child-id (id)
"Return next available child id for ID."
(org-roam-sequence--next-id (org-roam-sequence-id-split id) 'child))
;; HHHH------------------------------
;; Interactive fns
(defun org-roam-sequence-add-child ()
"Capture a new child node under the current Org-roam node.
Prompts the user for a title."
(interactive)
(let* ((current-node (org-roam-node-at-point))
(current-id (org-roam-node-id current-node))
(child-id (org-roam-sequence-next-child-id current-id))
(child-title (read-string "Child title: "))
(child-node (org-roam-node-create :id child-id :title child-title)))
(org-roam-capture- :node child-node
:props '(:immediate-finish nil))))
(defun org-roam-sequence-add-sibling ()
"Capture a new sibling node to the current Org-roam node.
Prompts the user for a title."
(interactive)
(let* ((current-node (org-roam-node-at-point))
(current-id (org-roam-node-id current-node))
(sibling-id (org-roam-sequence-next-sibling-id current-id))
(sibling-title (read-string "Sibling title: "))
(sibling-node (org-roam-node-create :id sibling-id :title sibling-title)))
(org-roam-capture- :node sibling-node
:props '(:immediate-finish nil))))
;; HHHH------------------------------
;; Ignore next line unless loading file
(provide 'org-roam-sequence)
;; End of file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment