URI:
       ZIPPY ORG-AGENDA BUFFER BUILDING
       
       - 
       - 
       
       
       Preamble
       ------------------------------------------------------------
       `org-agenda', if you don't know about the Emacs package,
       builds views out of `org-mode' notes. Usually the views will
       show the current day or week, showing all notes with
       timestamps landing within the date range. But you can also
       see notes with todo keywords, specific tags, or other
       properties if you want. It's pleasantly accommodating to a
       variety of workflows.
       
       I use `org-agenda' to show a chronological view into my note
       taking. I like being able to look back on an arbitrary
       period of time, like last week or last month, to see what
       I've written. I find the act of review to build context and
       confidence in my life.
       
       ,----
       |   Week-agenda (W24):
       |   Monday     10 June 2024 W24
       |   Tuesday    11 June 2024
       |     koreader:                         19:42...... Developing
       |     github_cheatcodes:                20:33...... How to squash many patches
       |   Wednesday  12 June 2024
       |   Thursday   13 June 2024
       |   Friday     14 June 2024
       |     php:                               8:14...... Setup on OpenBSD
       |     dsc_pc1555:                       16:34...... Connecting
       |   Saturday   15 June 2024
       |   Sunday     16 June 2024
       `----
       
       Building and rebuilding my `org-agenda' is usually very
       slow. I have 474 notes at the time of this writing. Each of
       these notes needs to be opened and parsed to determine its
       eligibility for inclusion in the agenda. It takes /minutes/
       to build an agenda buffer, which is a huge deterrent on me
       actually using the feature.
       
       Many times I've searched the WWW for ways to speed up my
       `org-agenda' views. There are some packages that purport to
       speed things up, but I found their mechanisms difficult to
       understand, and their integration into my existing
       `org-mode' config excessive. (I like to maintain a pretty
       simple set of configuration files for Emacs, about 400
       lines.) So I decided to embark on the task of finding my own
       way to faster `org-agenda' views. The remainder of this
       writing traces my thoughts, development notes, and outcome.
       
       
       Development notes
       ------------------------------------------------------------
       To start, I ran the Emacs profiler while building a
       `org-agenda' buffer build. I observed that the
       `org-agenda-get-timestamps' function accounts for 33% of CPU
       usage and 2682 samples. I figured the high samples count is
       probably owed to the many agenda files; again, 474 such
       notes. So with some confidence I assumed that `org-agenda'
       buffers could be built faster if the agenda had fewer files
       to look through. Searching the web, I found lots of ad-hoc
       advice from other individuals facing a similar problem. I
       read about hacks to dynamically populate the
       `org-agenda-files' variable. Nobody's specific approach
       appealed to me, but I got some good ideas. In short, I
       decided I'd use `grep' to pre-sift my `org-mode' notes for
       inclusion in my agenda.
       
       I was concerned that my approach would get messy. Modifying
       a variable crucial to building the agenda could break lots
       of stuff? But because the damn thing was so slow and I
       hardly used it anymore, better to restore one very-used
       feature even if it risks breaking some amount of less-used
       features.
       
       Next, I looked into the advising functions of Emacs---a very
       powerful feature! I had great success using such a function
       before to fix a bug with `org-download'. For this task, I
       imagined a similar approach: evaluating my function before
       the `org-agenda' view forms evaluate. But I'd first need to
       find the right function to advise. I experimented with a few
       that seemed fortuitous.
       
       ,----
       |   (advice-add 'org-agenda-list :before #'roygbyte/org-agenda-list-before)
       |   (advice-add 'org-agenda-list :around #'roygbyte/org-agenda-list-around)
       |   (advice-add 'org-agenda-list :filter-args #'roygbyte/org-agenda-list-filter-args)
       |   (advice-add 'org-agenda-prepare :before #'roygbyte/org-agenda-prepare-before)
       `----
       
       I found that advising my function `:before'
       `org-agenda-list' or `org-agenda-prepare' successful. At
       either point, I could grab information related to the
       generating agenda buffer. That information was contained in
       some of the following variables:
       
       ,----
       |   (print org-agenda-redo-command)
       |   (print org-agenda-start-day)
       |   (print org-starting-day)
       |   (print org-agenda-span)
       |   (print org-agenda-overriding-arguments)
       |   (print org-agenda-buffer-name)
       |   (print org-keys)
       `----
       
       I observed the values of these variables in different agenda
       contexts, like a week view, custom command, or existing view
       rebuild. Trial and error led me to observe the variables
       that would be helpful for my operation: `org-agenda-span'
       and `org-agenda-overriding-arguments'. (By the way, I found
       all candidates for observation by reading through the
       `org-agenda-list' function, as well as the `defvar' and
       `defcustom' variables declared throughout `org-agenda.el'.)
       With this, I now had the key parts and could proceed to
       writing the code.
       
       I split the logic of the operation into two main parts:
       identifying paths to the `org-mode' files of interest for
       the given agenda; and, modifying `org-agenda-files' to only
       contain files associated with the search query for a given
       agenda. I began by building first part, which would in
       effect be my invocation of `grep' from Emacs.
       
       
       grep search for relevant files
       ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
       To invoke `grep' from Emacs I use a subprocess. Subprocesses
       can be invoked synchronously or asynchronously. I chose the
       former, since it's easier to program and better accords with
       the situation. I built and tested my `grep' command and its
       argument in the command line, then translated it into
       ELisp. Note that there are many ways to define a
       subprocesses' command, and my method is maybe not the best.
       
       ,----
       |   (apply 'call-process "grep" nil t nil "-soE" regex-pattern files-to-search)
       `----
       
       I use `apply' to expand the list `files-to-search' into in
       individual arguments that are passed to `call-process'. I
       provide the files as individual arguments (and not just
       their enclosing directory) because it offers more certainty
       over how `grep' is invoked. Passing a directory and the `-R'
       flag would be the alternative to this approach. But in my
       trials recursing through a directory opened a host of
       problems I decided to avoid.
       
       ,----
       |   (with-temp-buffer
       |     (setq files-to-search (directory-files
       |                            directory t "\\.org\\'"))
       |     (apply 'call-process "grep" nil t nil "-soE" regex-pattern files-to-search)
       |     (goto-char (point-min))
       |     ;; Clean up buffer
       |     (while (search-forward ":" nil t)
       |       (delete-region (- (point) 1) (line-end-position)))
       |     ;; Translate buffer into list
       |     (seq-filter (lambda (a)
       |                   (not (string-empty-p a)))
       |                 (split-string (buffer-string) "\n")))
       `----
       
       The `grep' subprocess is invoked inside a temporary
       buffer. When the subprocess finishes, that buffer contains
       the results. Unfortunately, my distros version of `grep'
       doesn't support only returning matching filenames, so I have
       to clean up the buffer. Finally, the buffer is split by
       newline into a list and completes by evaluating to that list
       of org files matching the initial regular expression.
       
       
       Ad-hoc modification of org-agenda-files
       ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
       The second part of my operation is building the regular
       expression. In practice, this function is evaluated first. I
       only wrote it second because it seemed the more difficult
       bit of code to write. Indeed, it was.
       
       Two values are crucial to building any agenda: start date
       and span. The start date corresponds to when the agenda view
       begins. The challenge is that `org-agenda' stores these
       values in different variables depending on how the agenda is
       being built. For instance, if the agenda is being built
       "fresh", then `org-agenda-overriding-arguments' will be
       `nil'. In that case, the start day is stored in
       `org-agenda-start-day', unless that variable is `nil', in
       which case the start day will be `(org-today)'. However, if
       the agenda already exists and is being refreshed or moved to
       the previous or next week then the values will all be in
       `org-agenda-overriding-arguments'.
       
       I also got tripped up on how `org-mode' represents
       dates. After some digging, I learn that `org' represents
       this value as "the number of days elapsed since the
       imaginary Gregorian date Sunday, December 31, 1 BC." I find
       this by tracing `(org-today)' back to `(date-to-day)'.
       
       Another important detail to building an `org-agenda' view is
       using the right start day. I wrote a small function to
       translate a given day to the start of the week. For
       instance: Thursday, October, 3, becomes Sunday, September
       30. This function must also accommodate the possibility that
       `org-agenda-start-on-weekday' is set to Monday, which it
       could be for strange people who don't start their week on
       Sunday.
       
       
       Code
       ------------------------------------------------------------
       Below is the finished code. By my eyes, it's not the best
       looking Lisp. I still struggle with choosing the best place
       for line breaks and indentation. It also seems really long
       and complicated for such simple operations. About that:
       there's a lot of logic required for ensuring dates are in
       the correct format, and that edge cases are accommodated. I
       don't know if it can be simplified? Maybe. Whatever.
       
       A caveat: my extension doesn't handle non-standard todo
       keywords or even tags. I don't use those much, if at all. I
       provided a minimal amount of logic for the standard `TODO'
       keyword, and that's it. I think extending the function to
       support other stuff is easy, but I have no use for it right
       now.
       
       ,----
       |     (advice-add 'org-agenda-list :before #'roygbyte/org-agenda-list-before)
       |   (defun roygbyte/org-agenda-list-before (&rest r)
       |     "Speed up agenda buffer generation by dynamically modifying
       |   `org-agenda-files`. variable before `org-agenda-list` function
       |   is evaluated. Performance gains for buffers are largely
       |   accomplished by using grep to search for org notes containing
       |   timestamps (or TODOs!) corresponding to the desired time range."
       |     (cond
       |      ((or (string-equal org-keys "t")
       |           (string-equal (car org-agenda-redo-command) "org-todo-list"))
       |       (setq org-agenda-files
       |             (delete-dups (roygbyte/files-in-directory-matching-pattern
       |                           (file-truename
       Happy helping ☃ here: You tried to output a spurious TAB character. This will break gopher. Please review your scripts. Have a nice day!
       |      (t (let ((start-day nil)
       |               (end-day nil)
       |               (span nil)
       |               (regex nil))
       |           (when org-agenda-overriding-arguments
       |             (setq start-day (roygbyte/day-to-start-of-week
       |                              (nth 1 org-agenda-overriding-arguments)))
       |             (setq span (let ((span-ambiguous
       |                               (nth 2 org-agenda-overriding-arguments)))
       |                          (if (symbolp span-ambiguous)
       |                              (org-agenda-span-to-ndays span-ambiguous)
       |                            span-ambiguous)))
       |             (setq end-day (+ start-day span)))
       |           (when (not start-day)
       |             (setq start-day
       |                   (cond
       |                    (org-agenda-start-day
       |                     (time-to-days (org-read-date nil t org-agenda-start-day)))
       |                    (org-starting-day org-starting-day)
       |                    (t (roygbyte/day-to-start-of-week (org-today))))))
       |           (when (not span)
       |             (setq span (if (numberp org-agenda-span)
       |                            org-agenda-span
       |                          (org-agenda-span-to-ndays org-agenda-span))))
       |           (when (not end-day)
       |             (setq end-day (+ start-day span)))
       |           (setq regex
       |                 (string-join
       |                  (cl-map 'list (lambda (day-number)
       |                                  (let ((l (calendar-gregorian-from-absolute day-number)))
       |                                    (format "%d-%02d-%02d"
       |                                            (nth 2 l) (nth 0 l) (nth 1 l))))
       |                          (number-sequence start-day end-day)) "|"))
       |           (setq org-agenda-files
       |                 (delete-dups (roygbyte/files-in-directory-matching-pattern
       |                               (file-truename
       |                                (concat org-roam-directory "/pages/")) regex)))))))
       | 
       |   (defun roygbyte/day-to-start-of-week (day)
       |     "Take a DAY, represented by number of days since December 31, 1 BC, and translate
       |   value so it becomes the start of week."
       |     (let* ((day-of-week (calendar-day-of-week
       |                          (calendar-gregorian-from-absolute day)))
       |            (weekday-start org-agenda-start-on-weekday))
       |       (- day (abs (- weekday-start day-of-week)))))
       | 
       |   (defun roygbyte/files-in-directory-matching-pattern (directory regex-pattern)
       |     "Evaluates to a list of absolute file paths in DIRECTORY whose
       |    contents match REGEX-PATTERN."
       |     (with-temp-buffer
       |       (setq files-to-search (directory-files
       |                              directory t "\\.org\\'"))
       |       (apply 'call-process
       |              "grep" nil t nil "-soE" regex-pattern files-to-search)
       |       (goto-char (point-min))
       |       (while (search-forward ":" nil t)
       |         (delete-region (- (point) 1) (line-end-position)))
       |       (seq-filter (lambda (a)
       |                     (not (string-empty-p a)))
       |                   (split-string (buffer-string) "\n"))))
       `----
       
       
       Footnotes & references
       ------------------------------------------------------------
       - `org-roam' was once the cause for a brief crisis in my
         life. One day, I rebooted my computer and setup my
         environment in the usual way, by running `startx' in my
         login shell after login. The computer appeared to "hang". My
         i3 status bar and other window manager features did not
         appear. I was terrified I broke my indispensible device!
         Well, after a bit of investigation I uncovered the cause:
         invokation of `emacs --daemon' in my Bash RC file, which
         hanged /forever/ while `org-roam' attempted to sync files to
         its database.
       
       -