website/posts/efficient-org-agenda.html
J S ef43fe3f0a
feat[posts] Added org-agenda post.
I added a post on org-agenda
I renamed zettelkasten-tooling post's slug.
I made the link color slightly brighter.
2023-12-27 23:00:14 -05:00

239 lines
8.9 KiB
HTML

<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<title>
Open Thoughts
</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Judah Sotomayor" />
<meta property="og:title" content="In Pursuit of an Efficient Org-Agenda">
<meta property="og:url" content="https://judah.freedomland.xyz//posts/efficient-org-agenda.html">
<meta name="twitter:card" content="summary_large_image">
<link rel="stylesheet" href="https://judah.freedomland.xyz//static/style.css" type="text/css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lora&family=Space+Mono&display=swap" />
</head>
<body>
<header>
<h1>
<a href="https://judah.freedomland.xyz//">
Open Thoughts
</a>
</h1>
<a href="#main" class="visually-hidden">jump to main content</a>
<nav>
<ul class="menu">
<li><a href="https://judah.freedomland.xyz//about">about</a></li>
<li><a href="https://judah.freedomland.xyz//">blog</a></li>
</ul>
</nav>
</header>
<main id="main">
<article class="post">
<h1 class="post__title">
In Pursuit of an Efficient Org-Agenda
</h1>
<section class="post__meta">
Dec 27, 2023
</section>
<section>
<div id="table-of-contents" role="doc-toc">
<h2>Table of Contents</h2>
<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="#first-iteration">1. First Iteration</a></li>
<li><a href="#second-iteration">2. Second Iteration</a></li>
<li><a href="#finally">3. Final</a></li>
</ul>
</div>
</div>
<p>
After beginning my Emacs and Org-Mode adventures I quickly realized the insufficiency of the standard agenda.
It is wonderful at producing a list, but I am quickly approaching over 1000 notes.
The agenda tooling is incapable of searching this many files efficiently, and it took over thirty seconds to generate an agenda.
</p>
<p>
This was intolerable. I had two options:
</p>
<ol class="org-ol">
<li>Place all my TODO items into a single file.</li>
<li>Narrow the number of files the agenda mechanism needs to search</li>
</ol>
<p>
I find the first option undesireable for reasons I mentioned in my <a href="https://judah.freedomland.xyz//posts/reflections-zettelkasten-tooling.html">post</a> about zettelkasten tools.
I like to have my todo items mixed with the context where they were born.
A student of the <i>Getting Things Done</i> methodology might ask, "Doesn't this violate the central todo-list principle?"
Yes, it does.
Org-agenda allows me to have my cake and eat it too.
I can create a centralized todo-list out of all my todo items, and then immediately jump into the context of my next task.
</p>
<p>
It took several iterations of configuration to reach a seamless workflow.
If you'd like to jump straight to the final setup, <a href="#finally">here's the link.</a>
</p>
<div id="outline-container-first-iteration" class="outline-2">
<h2 id="first-iteration"><span class="section-number-2">1.</span> First Iteration</h2>
<div class="outline-text-2" id="text-1">
<p>
The first piece of tooling I used came from <a href="https://d12frosted.io/posts/2021-01-16-task-management-with-roam-vol5.html">this post</a> and <a href="https://gist.github.com/d12frosted/a60e8ccb9aceba031af243dff0d19b2e">this Gist</a>.
Essentially it creates a function <code>vulpea-project-files</code> that can easily query for a tag, and then sets the org-agenda files to all the files containing this tag.
</p>
<p>
Coupled with a helper-function that adds the tag <code>hastodos</code> to any file containing a TODO entry, it functioned well.
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">vulpea-project-files</span> ()
<span class="org-doc">"Return a list of note files containing '</span><span class="org-doc"><span class="org-constant">hastodos</span></span><span class="org-doc">' tag."</span> <span class="org-comment-delimiter">;</span>
(seq-uniq
(seq-map
#'car
(org-roam-db-query
[<span class="org-builtin">:select</span> [nodes:file]
<span class="org-builtin">:from</span> tags
<span class="org-builtin">:left-join</span> nodes
<span class="org-builtin">:on</span> (= tags:node-id nodes:id)
<span class="org-builtin">:where</span> (like tag (<span class="org-keyword">quote</span> <span class="org-string">"%\"hastodos\"%"</span>))]))))
(<span class="org-keyword">setq</span> org-agenda-files (vulpea-project-files))
</pre>
</div>
<p>
This solution is great because it is pure elisp.
Anywhere you're running Emacs it should function just fine.
It also works well with one of my needs: transparent file encryption.
Emacs has extensions for both <code>gpg</code> and <code>age</code> encryption that allow files to be transparently encrypted and decrypted.
This solution can make use of that where a standard <code>grep</code> could not.
</p>
</div>
</div>
<div id="outline-container-second-iteration" class="outline-2">
<h2 id="second-iteration"><span class="section-number-2">2.</span> Second Iteration</h2>
<div class="outline-text-2" id="text-2">
<p>
Eventually, I grew tired of seeing the entire solution in my config file.
All told, it is about 170 lines.
This is a lot for a small utility!
</p>
<p>
I decided to try using <code>ripgrep</code> to fix this:
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">set-org-agenda-files-ripgrep</span> ()
(<span class="org-keyword">setq</span> org-agenda-files (split-string (shell-command-to-string <span class="org-string">"rg -torg -l TODO /home/user/org"</span>))))
</pre>
</div>
<p>
This worked nicely.
It takes just a few lines, runs just as fast&#x2013;probably faster&#x2013;than pure elisp, and is much easier to read and understand.
</p>
<p>
The only difficulty is encryption.
<code>ripgrep</code> does not operate on open emacs buffers, instead it operates on what is saved to disk.
Naturally, this means that it only sees the encrypted files.
</p>
</div>
</div>
<div id="outline-container-finally" class="outline-2">
<h2 id="finally"><span class="section-number-2">3.</span> Final</h2>
<div class="outline-text-2" id="text-finally">
<p>
The solution to file encryption is ripgrep preprocessing.
I owe this solution to <a href="https://www.reddit.com/r/emacs/comments/q4e2az/full_text_search_of_gpg_encrypted_files/">this reddit post</a>.
</p>
<p>
Ripgrep can run a command (or shell script!) to files before processing.
While the original poster was using <code>gpg</code> to encrypt files, just a few modifications allowed me to use <code>age</code>.
</p>
<div class="org-src-container">
<pre class="src src-zsh">#!/usr/bin/env zsh
case "$1" in
*.age)
# The -s flag ensures that the file is non-empty.
if [ -s "$1" ]; then
exec /usr/bin/age --decrypt -i ~/.age/personal $1
else
exec cat
fi
;;
*)
;;
esac
</pre>
</div>
<p>
This script operates on all <code>.age</code> files, decrypting them to stdout.
When using a preprocessor, ripgrep will simply search the output of the command.
</p>
<p>
I often have a mix of files in my notes directory, so I use filetypes to restrict ripgrep.
This means I have to add a new filetype to allow <code>.age</code> files through the filter.
</p>
<div class="org-src-container">
<pre class="src src-sh">rg --type-add <span class="org-string">'aorg:*.org.age'</span> <span class="org-sh-escaped-newline">\</span>
-torg -taorg <span class="org-sh-escaped-newline">\</span>
--pre ~/age-preprocessor.zsh --pre-glob <span class="org-string">'*.age'</span> -l TODO /home/user/org
</pre>
</div>
<p>
Finally, we have our completed function:
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">set-org-agenda-files-ripgrep</span> ()
(<span class="org-keyword">setq</span> org-agenda-files (split-string (shell-command-to-string <span class="org-string">"rg --type-add </span><span class="org-string"><span class="org-warning">\</span></span><span class="org-string">'aorg:*.org.age</span><span class="org-string"><span class="org-warning">\</span></span><span class="org-string">' -torg -taorg --pre ~/age-preprocessor.zsh --pre-glob </span><span class="org-string"><span class="org-warning">\</span></span><span class="org-string">'*.age</span><span class="org-string"><span class="org-warning">\</span></span><span class="org-string">' -l TODO /home/user/org "</span>))))
</pre>
</div>
<p>
Hope this helps!
</p>
</div>
</div>
</section>
</article>
</main>
<footer>
Made with &#x2665; and&nbsp;
<a href="https://emacs.love/weblorg" target="_blank">
weblorg
</a>
</footer>
</body>
</html>