239 lines
8.9 KiB
HTML
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–probably faster–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 ♥ and
|
||
|
<a href="https://emacs.love/weblorg" target="_blank">
|
||
|
weblorg
|
||
|
</a>
|
||
|
</footer>
|
||
|
|
||
|
|
||
|
</body>
|
||
|
</html>
|