Advice for an `org-follow-mode` following org/org-roam links?

Hello all, and first of all thanks to everybody for this amazing tool!

I’m trying to add a minor org-follow-mode somewhere between org-agenda-follow-mode and org-roam-ui-follow-mode.

While this is not strictly an org-roam question, I’d like to see if this could be activated just for org-roam notes (and I’ll contact the org community as a next step).

My intuition so far was to start from a copy org-agenda-follow-mode (and subcommands), adapting it to an org file being the main window.

(defvar org-follow-mode nil)

(defvar org-pre-follow-window-conf nil)

(defun org-marked-tree-to-indirect-buffer (arg)
  ;; TODO needed?
  ;; (interactive "P")
  ;; (org-agenda-check-no-diary)
  (let* ((marker
          ;;(or
           (org-get-at-bol 'org-marker)
	   ;; (org-agenda-error)
           ;; )
         )
	 (buffer (marker-buffer marker))
	 (pos (marker-position marker)))
    (with-current-buffer buffer
      (save-excursion
	(goto-char pos)
	(org-tree-to-indirect-buffer arg))))
  ;; TODO needed?
  ;; (setq org-agenda-last-indirect-buffer org-last-indirect-buffer)
  )

(defcustom org-follow-indirect nil
  "Non-nil means `org-follow-mode' displays only the current item's
tree, in an indirect buffer."
  :group 'org
  :version "29.4"
  :type 'boolean)

(defun org-do-context-action ()
  "Show outline path and, maybe, follow mode window."
  (let ((m (org-get-at-bol 'org-marker)))
    (when (and (markerp m) (marker-buffer m))
      (and org-follow-mode
           (if org-follow-indirect
               (let ((org-indirect-buffer-display 'other-window))
                 ;; TODO or org-marked-tree-to-indirect-buffer from above
                 (org-tree-to-indirect-buffer nil))
             ;; TODO needed?
             ;; (org-agenda-show)
             ;; TODO
             ;; (and org-agenda-show-outline-path
             ;;      (org-with-point-at m (org-display-outline-path org-agenda-show-outline-path)))
             )))))

(defun org-follow-mode ()
  "Toggle follow mode in a buffer."
  (interactive)
  (unless org-follow-mode
    (setq org-pre-follow-window-conf
	  (current-window-configuration)))
  (setq org-follow-mode (not org-follow-mode))
  (unless org-follow-mode
    (set-window-configuration org-pre-follow-window-conf))
  ;; TODO needed?
  ;; (org-set-mode-name)
  (org-do-context-action)
  (message "Follow mode is %s"
	   (if org-follow-mode "on" "off")))

Now this obviously doesn’t work, primarily because I have no org-marker defined as no agenda line is selected.

I then went over the org-roam-ui-follow-mode code, little of which I could use for this as most of the follow-mode looks simpler, directly using the network instead of having to manage windows.

Would anyone have some advice to set this up? How to replace the use of marker and read the current org position instead?

Thanks again for reading, and best to y’all

What do you wish to do as a user, not as a programmer?

I do not use org-agend or org-roam-ui (ORUI), but I will try to guess what you wish to do.

With org-agenda-follow-mode, you can do this, as described in Org manual:

In Follow mode, as you move point through the agenda buffer, the other window always shows the corresponding location in the Org file.

As an illustration, I just produced my Org-agenda buffer (I don’t use it, but just to understand what you want to do). So, as I move along the list of TODO’s in the window above, the window below displays the corresponding location of the originating Org buffer. I imagine you would have TODOs coming from multiple Org-roam nodes.

Here is what I think you want to do:

  1. As I move along my list of TODOs in the agenda, Emacs shows the corresponding location in the Org file (org-agenda-follow-mode)

  2. If the displayed location of Org (the bottom window in my example) happens to be an Org-roam node, I want ORUI to also show the corresponding location in the browser.

  3. I want this to happen automatically without me moving my point to the Org buffer below, so that I can keep moving in the TODO list in the agenda view.

Is this what you wish to do? The direction of the “data” flow is from Emacs to ORUI. I don’t think it would be easy to also control org-agenda-follow-mode from within ORUI’s browser graph display (maybe possible).

Then… It may be much simpler, and would not require a minor mode. Try this. I don’t have org-roam-ui set up in my current Emacs, so I cannot test it. But it looks like this org-roam-ui function does what org-roam-ui-follow-mode does. And org-agenda-afeter-show-hook is run when your point is inside the node.

(add-hook 'org-agenda-after-show-hook #'org-roam-ui--update-current-node)

Thanks for the quick answer.

As a user I’d like to have two org buffers/windows:

  • one in which I move up/down,
  • and the other that shows the content of any org link over which I move in the first buffer

So similar to your screenshot, but the top window would be a regular org file instead of the agenda.

So you have a collection of links to other nodes, like an index file? Can you show us an example?

Also, how are ORUI and agenda related to this?

And… You are saying “any” Org links? So, it is not related to Org-roam, ORUI, or Org-agenda; none of them?

Here’s one: both windows show normal org-roam files, but I use the top one as a list of other org-roam notes. I’d like that when I move around in the top window, the bottom window shows the content of the org-roam note I’m over in the top window.

So in the example, the cursor is at the “Current technology seems incompatible with …” link, and the bottom is the contents of that note.

(And please ignore the content :blush: – or I can explain properly in a couple weeks once this will have turned into a manuscript)

ORUI and agenda both have similar behaviours: one frame (or ORUI) can follow what is at point in another frame. Both are *-follow-mode.

Indeed, it’s not specific to org-roam, as this could also work with a regular (non-roam) org file, showing the contents (in the “follow” window) of any org link at point (in the main window),

Does this make sense?

Didn’t take too long, so sharing the result. Try the code below.

I suggest you manually test the command my/follow-link before adding it to post-command-hook. I believe you know what it does, and what it means to use hook locally (?).

It works on my end with the 3 bullet points with links on the left window. The link target files are visited on the right.

The code does not force a placement of the “other” window nor re-configure the original frame state. If you need these features, please investigate further.

You can define a local minor mode to add/remove the my/follow-link (please rename it) to/from post-command-hook.

For testing, you can call M-x remove-hook and manually remove it to tear down the set-up.


(defvar my/follow-preview-buffer nil)

(defun my/follow-link ()
  "Automatically open the first link in the current line.
      When it is a file to open, the buffer will open in \"other\"
      window."
  (interactive)
  (when (and (member this-command '(previous-line next-line
                                             forward-char backward-char
                                             left-char right-char
                                             org-previous-link org-next-link))
             ;; This second condition may be part of the minor-mode function
             (derived-mode-p 'org-mode))
    (when my/follow-preview-buffer (kill-buffer my/follow-preview-buffer)
          (setq my/follow-preview-buffer nil))
    (save-excursion
      ;; `cl-letf' is meant to ensure `file-file-other-window' to be used for
      ;; opening a file by temporarily changing the value of
      ;; `org-link-frame-setup' during evaluating the body.
      (cl-letf (((alist-get 'file org-link-frame-setup)
                 #'find-file-other-window))
        (let ((buffers-before-follow)
              (win (selected-window))
              (link
               (when
                   (or (org-in-regexp org-link-any-re)
                       (re-search-forward org-link-any-re
                                          (line-end-position) :t))
                 ;; It seems `org-element-link-parser' must be located at
                 ;; the beginning of the link at point.
                 (goto-char (match-beginning 0))
                 (org-element-link-parser))))
          (when link
            (setq buffers-before-follow (buffer-list))
            (org-link-open link)
            ;; Current buffer is the link target
            (unless (member (current-buffer) buffers-before-follow)
              (rename-buffer (concat "preview: " (buffer-name)))
              (setq my/follow-preview-buffer (current-buffer))))
          (select-window win))))))
(add-hook 'post-command-hook #'my/follow-link nil :local)


Edited from the original.

  1. Added a filter so that only cursor movement commands opens the link. Previous, even opening help documentation with “C-h” on the line would call a link
(when (and (member this-command '(previous-line next-line
                                             forward-char backward-char
                                             left-char right-char
                                             org-previous-link org-next-link))
  1. Rename the buffer with prefix "preview: " to the buffer opened newly by this function. If the buffer already exists before following the link, it will be unchanged.
            (unless (member (current-buffer) buffers-before-follow)
              (rename-buffer (concat "preview: " (buffer-name)))
  1. Global variable my/follow-preview-buffer is introduced and kill the preview
    (when my/follow-preview-buffer (kill-buffer my/follow-preview-buffer)
          (setq my/follow-preview-buffer nil))
[...]
            (unless (member (current-buffer) buffers-before-follow)
              (rename-buffer (concat "preview: " (buffer-name)))
              (setq my/follow-preview-buffer (current-buffer))))
1 Like

One more note. The files are visited and thus will remain in the buffer list. So if you have many links and move into 30 of the links, your buffer list may feel cluttered with 30+ buffers. If you want to just “preview” the links and kill the buffer when you move to the next one, you’d need to deal with some complexity. You probably want to keep the buffers already open, but kill those this function newly visits as “previews”. I don’t know how this can be done.

I use midnight-mode to clean unused buffers. By default, after 3 days of inactivity, a buffer is killed (unless its changed have not been saved)

1 Like

A minor mode can be used to add/remove the hook.

2 Likes

Thanks all.

I’d like to keep this open for a few days / up to a week as I try to set up

  1. a filter so the main frame is the only one in which links are followed
  2. different links in the same line can be followed
  3. letting only org links to be followed (e.g. not web links, currently opened)
  4. turning it all into the minor mode mentioned by @dmg

Updated the code. See the revised code above. You can ignore the changes if you do not like any of it. I won’t take it personal or anything.

For your TODOs,

  1. a filter so the main frame is the only one in which links are followed

I don’t quite understand what this is (maybe I don’t need to, and leave it to you), but I intended the following part to ensure that the link only opens within another window in the current frame. I only use one frame, so perhaps you are talking about when you already have an existing buffer as a link target in another frame… Also, the hook is local, so it will not affect any other buffer (let alone frames).

      (cl-letf (((alist-get 'file org-link-frame-setup)
                 #'find-file-other-window))
  1. different links in the same line can be followed

Edit: Sorry, what I said was not accurate. It should work. Try the latest code above with moving the cursor. org-next-link and org-previous-link should also open the preview, too.

  1. letting only org links to be followed (e.g. not web links, currently opened)

Add more condition to the part below. You can check the type property of the link, I think.

;; Only revised code has this when condition to check link being non-nil
 (when link

with something using (org-element-property :type link).

  1. turning it all into the minor mode mentioned by @dmg

Agree.

Thank you. I shutdown my computer after every use, at least daily :).

See the revised code. A version of preview handling is there now — not as sophisticated as how Consult does it but mine seems to work for the purpose here.

Thanks again all of you!

The minor mode was indeed as simple as

(define-minor-mode my/follow-link-minor-mode
  "TODO docs"
  :lighter " mfl" ;; The indicator for the mode line.
  (if my/follow-link-minor-mode
      (add-hook 'post-command-hook #'my/follow-link nil :local)
    (remove-hook 'post-command-hook #'my/follow-link :local)))
1 Like