Skip to content

Latest commit

 

History

History
328 lines (236 loc) · 19.8 KB

README.org

File metadata and controls

328 lines (236 loc) · 19.8 KB

org-node

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.

    With optional shims, you can even skip syncing the org-roam DB and continue using its 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 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, 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!

I used to lose track of things I had written, under some forgotten heading in a forgotten file in a forgotten directory. The org-roam method let me find and build on my own work, instead of having repetitive cryptomnesia and staying on square one forever.

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

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 design-philosophy about exactly what kinds of things should turn up as search hits. What’s a “note”?

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

A comparison of three similar 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 insertyesyes– (suggests org-ql)
Node aliasesyesyesnot applicable
Rich backlinks bufferyesyes (org-roam’s)
Reflinksyesyes (as backlinks)
Ref searchyesyes (as aliases)
Extract subtree to new fileyesyes
Can configure rich completionsyesyesnot applicable
org-roam-capture integrationyesyes
org-capture integrationyes
Backlinks in same windowyesyes
Avoid double-counting :BACKLINKS:yesyes
Node exclusionyeslimitednot applicable
Support roam: linksyes
Support roam citations (#6)yes
Some query-able cacheorg-roam-dborg-nodes
Asynchronous cachingyesyes, very async ;)
Time to re-cache my 2000 files2m 48s0m 02snot applicable

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 verbs: “find” and “insert-link”.

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

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

(If you don’t like F2, 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)

Also, either run M-x org-roam-update-org-id-locations, or edit the following option so it includes your org-roam-directory. Ideally org-id would manage itself, but it doesn’t do it super-well, and this is insurance.

(setq org-node-extra-id-dirs '("~/org/")) ;; ... assuming that's your org-roam-directory

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 already mentioned in Quick start. There’s more under Toolbox. Enjoy!

If you also want the org-roam-buffer, see the next section.

Backlink solution 1: borrowing org-roam’s backlink 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.

Yes, it’s possible!

(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: printing inside the file

I like 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)

Alas, this is mainly directed towards people were using org-super-links from the beginning, as there is not yet a bulk command to add drawers to all nodes. (Issue 93)

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 you'll
        ;; fill in later
        ("ni" "Instantly create stub ID node 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.)

A variant I like, that greys out the ancestor headings and includes the file title:

(setq 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)))

Limitation: excluding notes

The option org-node-filter-fn works well for excluding TODO items that happen to have an ID, and excluding 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 them from appearing as search hits. 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.

While the point of org-id is to avoid dependence on filenames, 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, via the last line here:

(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)) ;; Ignore :drill:
                 (assoc "ROAM_EXCLUDE" (org-node-get-properties node))
                 (string-search "archive" (org-node-get-file-path node))))))

Toolbox

Commands:

  • org-node-find
  • org-node-insert-link
  • org-node-insert-transclusion
  • org-node-insert-transclusion-as-subtree
  • org-node-rename-file-by-title
    • Auto-rename the file based on the current #+title
  • 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-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 Org files under /media linking to assets in /home, those links won’t be updated.
  • 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 where it was, and show the new file as the current buffer.
  • org-node-random
    • Visit a random node
  • 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-backlink-fix-all
    • Add BACKLINKS property to all nodes everywhere (takes a while)
  • org-node-backlink-regret
    • In case you regret the BACKLINKS properties – remove them all

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-get-id NODE) org-node--links-table)
Get reflink objects(org-roam-reflinks-get NODE)(gethash (org-node-get-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)