bhankas

Taming org-agenda indirect buffer size

Update: Changed title to a more suitable one, corrected grammar and added more optimized final code.

Update 2: Thanks to @viz on Emacs matrix room, added even better code.


Few weeks ago I discovered org-agenda-follow-mode and org-agenda-follow-indirect variable. Its combination automatically shows a little preview of tasks in org-agenda in separate pop-up window. It is very useful to glance at more details of a task on the fly, and I like it.

But, the pop-up buffer automatically takes as much space as necessary by default. This is a problem, because for repeated tasks (e.g. watering plants) the LOGBOOK property drawer can be hundreds of lines, and the pop-up occupies nearly entire display, sort-of defeating the purpose.

So, I went about trying to find what the org-agenda keybindings are doing. This is where the rabbit hole leads:

1helpful-key
2-> org-agenda-next-line
3-> org-agenda-do-context-action
4-> org-agenda-tree-indirect-buffer
5-> (fit-window-to-buffer indirect-window)

Bingo! The last line is responsible for resizing the popup, and unlike the usual ones, it does not obey doom's set-popup-rule! setting.

Function definition of fit-window-to-buffer function is fairly straightforward:

1(fit-window-to-buffer &optional WINDOW MAX-HEIGHT MIN-HEIGHT MAX-WIDTH MIN-WIDTH PRESERVE-SIZE)

An extra argument for MAX-HEIGHT and it will do the job, easy.

But wait! Turns out, this fit-window-to-buffer function is called quite often, most notably by the awesome which-key. So I want to modify the argument list, but only when it is called by the aforementioned org-agenda-tree-indirect-buffer.

Emacs-lisp has some nice functionality built-int to walk the stack-trace (or in elisp ling, a backtrace). So I cooked up a function that walks the backtrace to determine if fit-window-to-buffer was called by org-agenda-tree-indirect-buffer and append MAX-HEIGHT to arguments list if it is:

 1(defun bhankas-org-agenda-limit-indirect-buffer (og-fun &rest args)
 2  (let ((new-args args))
 3    (when (cl-some
 4           (lambda (lst)
 5             (and (listp lst) (member 'org-agenda-tree-to-indirect-buffer lst)))
 6           (backtrace-frames))
 7      (setq new-args (append args '(20))))
 8    (apply og-fun new-args)))
 9
10(advice-add 'fit-window-to-buffer :around #'bhankas-org-agenda-limit-indirect-buffer)

And this works. The only problem is, walking the entire backtrace for every single invocation of function, just to update args during very small minority number of invocations that match the criteria is computationally expensive. fit-window-to-buffer being called on every which-key invocation makes this particularly awful solution because it is invoked very very often in my workflow, sometimes multiple times per second.1

So, I needed a better, more performant solution. And then the bulb lit! Instead of one advice, I could use two advices, and use some global state as caller indicator :). Here it is:

 1(defun bhankas-org-agenda-limit-indirect-max (og-fun &rest args)
 2  (dlet ((bhankas-fit-buffer-limit-max t))
 3    (apply og-fun args)))
 4
 5(defun bhankas-org-agenda-limit-indirect-buffer (og-fun &rest args)
 6  (when (boundp 'bhankas-fit-buffer-limit-max)
 7    (setq args (append args '(20))))
 8  (apply og-fun args))
 9
10(advice-add 'fit-window-to-buffer :around #'bhankas-org-agenda-limit-indirect-buffer)
11(advice-add 'org-agenda-tree-to-indirect-buffer :around #'bhankas-org-agenda-limit-indirect-max)

1

I set which-key-idle-delay to 0.1, because I like it that way.