Skip to content

meedstrom/org-node

Repository files navigation

org-node

News [2024-05-07]

  • It was unclear how to ensure that org-id knows about all IDs, so new variable: org-node-extra-id-dirs!

News [2024-05-03]

  • org-node-reset is now async!

News [2024-05-01]

  • It turns out that org-node-reset took dozens of seconds on some people’s configs. I think it’s fixed now.

News [2024-04-30]

  • We no longer depend on a BACKLINKS property! It is still a great way to show backlinks, so it remains as an option.
  • Renamed CACHED_BACKLINKS to just BACKLINKS. One M-x org-node-backlink-fix-all will take care of the rename, or you can remove them all with M-x org-node-backlink-regret.
  • Breaking API change – nodes are objects now, not plists! The software can’t give you a deprecation notice, because plists fail silently (that’s why the change). See updated examples for org-node-filter-fn etc.

What’s all this

I found org-roam too slow, so I made quickroam. And that idea spun off into this package, a standalone thing. I hope it’s also easier to learn.

  • If you were using org-roam, there is nothing to migrate. You can use both packages. It’s exactly the same on-disk format: “notes” are identified by their org-id.

    Thanks to shims, you can even skip syncing the org-roam DB but still use its features, like the rich backlinks buffer and org-roam-capture!

    In pursuit of being “just org-id”, this package has no equivalent setting to org-roam-directory – it just looks up org-id-locations.

  • If you were not using org-roam, maybe think of it as a limited, focused org-ql. If you were the sort of person to prefer ID-links over file links or any other type of link, you’re in the right place! Now you can truly rely on IDs, and forget about filenames and hierarchies and directory structures – as long as you’ve assigned an ID to something, you can find it later.

    And you can still keep doing hierarchies and structures, where that brings you joy, unlike systems that mandate limitations like “one note per file”.

What’s a “node”?

My life can be divided into two periods ”before org-roam” and ”after org-roam”. I crossed a kind of gap once I got a good way to link between my notes. It’s odd to remember when I just relied on browsing hierarchies of subtrees – what a strange way to do things!

At the core, all the “notetaking packages” (orgrr/zk/deft/org-roam/denote/…) try to help you with this: make it easy to link between notes.

Right off the bat, that imposes two requirements: a method to search for notes, since you can’t link to something you can’t search for, and a decision about exactly what kinds of things should turn up as search hits.

Just searching for Org files is too coarse, and just searching for any subtree anywhere brings in too much clutter.

Here’s what org-roam invented. It turns out that if you limit the search hits to just those files and subtrees you’ve deigned to assign an org-id – which roughly maps to everything you’ve ever thought it was worth linking to – it filters out the noise excellently.

Once a subtree has an ID you can link to, it’s a “node” because it has joined the wider graph, the network of linked nodes. I wish the English language had more distinct sounds for the words “node” and “note”, but to clarify, I’ll say “ID-node” when the distinction matters.

Feature matrix

For your pleasure, here’s a comparison of three similar notetaking systems: all permitting org-id as first-class citizen and not locking you into an “one-note-per-file” concept.

org-roamorg-nodeorg-super-links
Backlinksyesyesyes
Node search and insertyesyesno (suggests org-ql)
Node aliasesyesyesnot applicable
Rich backlinks bufferyesyes (org-roam’s)no
Reflinksyesyes (as backlinks)no
Ref searchyesyes (as aliases)no
Extract subtree to new fileyesyesno
Can configure rich completionsyesyesnot applicable
org-roam-capture integrationyesyesno
org-capture integrationnoyesno
Backlinks in same windownoyesyes
Avoid scanning :BACKLINKS: drawersnoyesyes
Node exclusionyeslimitednot applicable
Support roam: linksyesnono
Warn about dead linksnonono
Some query-able cacheorg-roam-dborg-nodesno
Asynchronous cachingnoyesyes (very async ;)
Time to re-cache my 2000 files2m 48s0m 02snot applicable

This isn’t a totally fair comparison; org-roam has an advantage in the details, see Appendix II. With org-node I’ve made what I would’ve liked to hack on, so it’s more simplistic in many aspects.

Setup

Add an init snippet like this (assuming straight.el):

(use-package org-node
  :straight (org-node :type git :host github :repo "meedstrom/org-node")
  :hook (org-mode . org-node-cache-mode))

Quick start

If you’re new to these concepts, fear not. The main things for day-to-day operation are two commands: “find” and “insert-link”.

Pick some good keys and try them out, and you can come back to this README later—or never.

The keys I use:

(global-set-key (kbd "<f2> f") #'org-node-find)
(global-set-key (kbd "<f2> i") #'org-node-insert-link)

(I like F2, but if you don’t, maybe M-s?)

(global-set-key (kbd "M-s f") #'org-node-find)
(global-set-key (kbd "M-s i") #'org-node-insert-link)

To browse config options, type M-x customize-group RET org-node RET.

Final tip for the newbie: there’s no separate command for creating a new node! Reuse one of the commands above, and type the name of a node that doesn’t exist.

Use Org-roam at the same time?

These user options help you feel at home using both packages side-by-side:

(setq org-node-creation-fn #'org-node-new-by-roam-capture)
(setq org-node-slug-fn #'org-node-slugify-like-roam)
(setq org-node-creation-hook nil)

Also, either run M-x org-roam-id-update-id-locations, or add your org-roam-directory to the following variable. It shouldn’t be necessary, but upstream org-id has no convenient setting like this.

(setq org-node-extra-id-dirs '(...))

If you’ve been struggling with slow saving of big files in the past, consider these org-roam settings:

(setq org-roam-db-update-on-save nil) ;; don't update DB on save, not needed
(setq org-roam-link-auto-replace nil) ;; don't look for "roam:" links on save

With that done, try out the commands mentioned in Quick start. There’s more under Toolbox. Enjoy!

I’m guessing you also want the org-roam-buffer. See next section.

Backlink solution 1: org-roam-buffer

Want to keep using M-x org-roam-buffer-toggle?

Option 1A. Keep letting org-roam update its own DB.

If you didn’t have laggy saves, this is fine. In other words, keep org-roam-db-update-on-save at t.

Option 1B. Tell org-node to write to the org-roam DB.

The following hook should keep the database synced.

(add-hook 'org-node-cache-rescan-file-hook #'org-node-feed-file-to-roam-db)

For a full reset, equivalent to C-u M-x org-roam-db-sync, you can type M-x org-node-feed-roam-db. It’s still slow, but interestingly, all the slowness comes from EmacSQL or SQLite. If someone figures out how to optimize that, please let me know!

Option 1C. Cut out the DB altogether.

Do some hax:

(advice-add 'org-roam-backlinks-get :override
            #'org-node--fabricate-roam-backlinks)

(advice-add 'org-roam-reflinks-get :override
            #'org-node--fabricate-roam-reflinks)

Backlink solution 2: inside the file

I love these solutions because I rarely have the screen space to display a backlink buffer.

Option 2A. Let org-node add a :BACKLINKS: property to all nodes.

For a first-time run, type M-x org-node-backlink-fix-all. (Don’t worry, if you change your mind, you can undo with M-x org-node-backlink-regret.)

Then start using the minor mode org-node-backlink-mode, which keeps these properties updated. Init snippet:

(add-hook 'org-mode-hook #'org-node-backlink-mode)

Option 2B. Let org-super-links manage a :BACKLINKS:...:END: drawer.

I think the following should work. Totally untested, let me know!

(add-hook 'org-node-insert-link-hook #'org-node-convert-link-to-super)

Misc

Org-capture

You may have heard that org-roam has its own set of capture templates: the org-roam-capture-templates.

It can make sense, for people who fully understand the magic of capture templates. I didn’t, so I was not confident using a second-order abstraction over an already leaky abstraction.

So can we reproduce the functionality on top of vanilla org-capture? That’d be less scary. The answer is yes!

Here are some example capture templates. The secret sauce is (function org-node-capture-target).

(setq org-capture-templates
      '(("n" "ID node")
        ("nc" "Capture to ID node (maybe creating it)"
         plain (function org-node-capture-target) nil
         :empty-lines-after 1)

        ("nv" "Visit ID node (maybe creating it)"
         plain (function org-node-capture-target) nil
         :jump-to-captured t
         :immediate-finish t)

        ;; Sometimes useful with `org-node-insert-link' to make a stub
        ("ni" "Instantly create ID node without content & without visiting"
         plain (function org-node-capture-target) nil
         :immediate-finish t)))

And if you want the commands org-node-find & org-node-insert-link to likewise outsource to org-capture when creating new nodes:

(setq org-node-creation-fn #'org-capture)

Managing org-id-locations

I find unsatisfactory the config options in org-id (Why? See Taking ownership of org-id), so org-node gives you an additional way to feed data to org-id, making sure we won’t run into “ID not found” situations.

Example setting:

(setq org-node-extra-id-dirs
      '("/home/kept/notes"
        "/home/kept/project1/"
        "/home/kept/project2/")

Rich completions

How to see the headings’ full outline paths while searching for nodes:

;; Prepend completions with the heading's outline path
(setq org-node-format-candidate-fn
      (lambda (node title)
        (if-let ((olp (org-node-get-olp node)))
            (concat (string-join olp " > ") " > " title)
          title)))

When tinkering with this expression, test the result by evalling the form and doing a M-x org-node-reset.

Another variant, that greys out the ancestor headings, and includes the file title (I like this one):

(setopt org-node-format-candidate-fn
        (lambda (node title)
          (if (org-node-get-is-subtree node)
              (let ((ancestors (cons (org-node-get-file-title-or-basename node)
                                     (org-node-get-olp node)))
                    (result nil))
                (dolist (anc ancestors)
                  (push (propertize anc 'face 'shadow) result)
                  (push " > " result))
                (push title result)
                (string-join (nreverse result)))
            title)))

Limitations: excluding notes

The org-node-filter-fn works well for ignoring TODO items that happen to have an ID, and ignoring org-drill items and that sort of thing, but beyond that, it has limited utility because unlike org-roam, child ID nodes of an excluded node are not excluded!

So let’s say you have a big archive file, fulla IDs, and you want to exclude all of it. Putting a :ROAM_EXCLUDE: t at the top won’t do it. As it stands, what I’d suggest is unfortunately, look at the file name.

The point of org-id is to avoid dependence on filenames, but it’s often pragmatic to let up on purism just a bit :-) It works well for me to filter out any file or directory that happens to contain “archive” in the name:

(setq org-node-filter-fn
      (lambda (node)
        (not (or (org-node-get-todo node) ;; Ignore headings with todo state
                 (member "drill" (org-node-get-tags node))
                 (assoc "ROAM_EXCLUDE" (org-node-get-properties node))
                 (string-search "archive" (org-node-get-file-path node))))))

Toolbox

  • org-node-find
    • A counterpart to org-roam-node-find
  • org-node-insert-link
    • A counterpart to org-roam-node-insert
  • org-node-insert-transclusion
  • org-node-insert-transclusion-as-subtree
  • org-node-extract-subtree
    • A bizarro counterpart to org-roam-extract-subtree. Export a subtree at point into a file-level node, leave a link in the source, and make the new node the current buffer.
  • org-node-nodeify-entry
    • (Trivial) Give an ID to the subtree at point (and run org-node-creation-hook)
  • org-node-insert-heading
    • (Trivial) Insert a new heading with an ID (and run org-node-creation-hook)
  • org-node-random
    • Visit a random node
  • org-node-rewrite-links-ask
    • Look for link descriptions that got out of sync with the current node title, then prompt at each link to update it
  • org-node-rename-file-by-title
    • Auto-rename the file based on the current #+title
  • org-node-rename-asset-and-rewrite-links
    • Interactively rename an asset such as an image file and try to update all Org links to them. Requires wgrep.
      • NOTE: For now, it only looks for links inside the root directory that it prompts you for, and sub and sub-subdirectories and so on – but won’t find a link in a completely different place. Like if you have stuff under /stuff linking to stuff in /home, those links won’t be updated.
  • org-node-backlink-fix-all
    • Add BACKLINKS property to every node everywhere that should have one
  • org-node-backlink-regret
    • In case you regret using this package – remove all BACKLINKS

Appendix I: Rosetta stone

API comparison between org-roam and org-node.

Actionorg-roamorg-node
Get ID at point(org-roam-id-at-point)(org-entry-get nil "ID" t)
Get node at point(org-roam-node-at-point)(org-node-at-point)
Get list of files(org-roam-list-files)(org-node-files)
Prompt user to pick a node(org-roam-node-read)(org-node-read)
Get backlink objects(org-roam-backlinks-get NODE)(gethash (org-node id NODE) org-node--links-table)
Get reflink objects(org-roam-reflinks-get NODE)(gethash (org-node id NODE) org-node--reflinks-table)
Get title(org-roam-node-title NODE)(org-node-get-title NODE)
Get title of file where NODE is(org-roam-node-file-title NODE)(org-node-get-file-title NODE)
Get title or name of file where NODE is(org-node-get-file-title-or-basename NODE)
Get ID(org-roam-node-id NODE)(org-node-get-id NODE)
Get filename(org-roam-node-file NODE)(org-node-get-file-path NODE)
Get tags(org-roam-node-tags NODE)(org-node-get-tags NODE), no inherited tags
Get outline level(org-roam-node-level NODE)(org-node-get-level NODE)
Get char position(org-roam-node-point NODE)(org-node-get-pos NODE)
Get properties(org-roam-node-properties NODE)(org-node-get-properties NODE), no inherited properties
Get subtree TODO state(org-roam-node-todo NODE)(org-node-get-todo NODE), only that match global org-todo-keywords
Get subtree SCHEDULED(org-roam-node-scheduled NODE)(org-node-get-scheduled NODE)
Get subtree DEADLINE(org-roam-node-deadline NODE)(org-node-get-deadline NODE)
Get outline-path(org-roam-node-olp NODE)(org-node-get-olp NODE)
Get ROAM_REFS(org-roam-node-refs NODE)(org-node-get-refs NODE)
Get ROAM_ALIASES(org-roam-node-aliases NODE)(org-node-get-aliases NODE)
Get ROAM_EXCLUDE(assoc "ROAM_EXCLUDE" (org-node-get-properties NODE)), doesn’t inherit parent excludes!
Get whether this is a subtree(zerop (org-roam-node-level NODE))(org-node-get-is-subtree NODE)
Get subtree priority(org-roam-node-priority NODE)
Ensure fresh data(org-roam-db-sync)(org-node-cache-ensure)

Appendix II: Pros of org-roam

  1. It is the most general toolkit. Take a function like org-roam-id-at-point. Why does it exist, when you could use (org-id-get nil nil nil t)? Well, the org-roam version ignores those ancestor headings that have an ID but have been marked not to count as “Roam nodes”, so it travels further up the tree until it finds one that is indeed “a Roam node”.
    • This brings good to some users. Complexity is not the enemy. It’s just a bit of a YAML vs TOML situation. Or lsp-mode vs eglot. I prefer to try to be “closer to the metal”, use vanilla org-capture instead of org-roam-capture, look up vanilla org-id-locations instead of org-roam-directory etc. Not have so many wrappers.
  2. Take the variable org-roam-mode-sections. Under any ordinary Emacs Lisp package, this would just be a list of functions. But in fact, you can add to it a cons cell of a function plus the arguments to pass to it. I like programmability, but this is … oriented towards people who aren’t programmers, I think.
    • It does make the org-roam source code a slower read. You scratch your head and ask “Why is it made that way?” Then you see, and you say “Ah, but I don’t need that!” Well, maybe someone does.
  3. Take the variable org-roam-node-display-templates. At least, others may consider this a pro, but for my tastes no. I try to let people customize with little lambdas and provide examples of how they’d get some result or other. This instead has the dream UI where you can just set the variable to a string “${olp} ${tags} ${title}” or some such and be done with it. Problem is it’s a new mini-DSL (domain-specific language), and when you learn it you miss out on an elisp lesson. Convenient for beginners but also keeps them beginners.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published