Irreal: Jinx For Spell Correction

Marcin Borkowski (mbork) has a nice post on the Jinx spell corrector. Mbork has tried fly-spell but found it too laggy for his purposes. That was back in the days when he had a Pentium class machine running at 100 Mhz so he thought, in view of his more modern hardware, that it was worth trying out fly-spell again.

He still found it too slow and decided to try Jinx instead. I’ve always found fly-spell snappy and I hadn’t heard of Jinx until I read Prot’s post on writing (and spell correcting) foreign languages in Emacs but mbork wasn’t satisfied until he tried Jinx. He found that it was fast enough for his use cases and decided to embrace it as his spell corrector.

He discovered that Jinx interfered with some of his work flows but it turned out to be pretty easy to fix those. You can check out mbork’s post for the details.

If you are writing in multiple languages with Emacs, then Jinx may be an excellent solution as a spell checker. If, like me, you mostly write in your native tongue, you’ll probably find fly-spell is all you need. As I said above, it’s more than fast enough for me and provides a wonderfully interactive spell correction system.

-1:-- Jinx For Spell Correction (Post Irreal)--L0--C0--2026-07-12T15:18:36.000Z

Chris Maiorana: Org mode as a gantt chart generator

The word Gantt tends to evoke project-management software with bars you can drag and a hefty license fee. The underlying idea, tasks arranged on a horizontal timeline, with owners and effort estimates, is much older and much simpler than the software around it. It is basically a chart. You can draw one with a pencil and a string for a plumb line.

Org mode already stores everything you need for a Gantt chart: a hierarchy of tasks, SCHEDULED and DEADLINE timestamps, properties for Effort and assignments to various agents. What’s missing is the picture. But org can draw the picture too. Let’s take a look and, one hopes, save you a subscription fee.

The minimum data model

For each task you want to track on the chart, add three things:

* Draft chapter 5
SCHEDULED: <2026-02-12 Thu> DEADLINE: <2026-02-26 Thu>
:PROPERTIES:
:AGENT:    Chris
:Effort:   10
:END:

* Edit chapter 4
SCHEDULED: <2026-02-10 Tue> DEADLINE: <2026-02-20 Fri>
:PROPERTIES:
:AGENT:    Chris
:Effort:   6
:END:

* Design cover art
SCHEDULED: <2026-02-15 Sun> DEADLINE: <2026-03-01 Sun>
:PROPERTIES:
:AGENT:    Maya
:Effort:   8
:END:

That is enough for a starting point. SCHEDULED is the start of the bar, DEADLINE is the end, :AGENT: is the swimlane, :Effort: is the height of the bar (or just a hover tooltip). Everything else is presentation.

Rendering the chart

I’ve included an example implementation below on how I render a nice view from this. Here’s the whole renderer, about sixty lines of elisp. Saved as gantt-render.el somewhere on my load path, I can open the project file, and run M-x org-fas-gantt-render-to-html. It walks every heading that has both a SCHEDULED start and a DEADLINE end, lays each one out as a bar scaled to the project’s overall span, and groups the bars into swimlanes by :AGENT:.

(require 'org)
(require 'ox-html)   ; for org-html-encode-plain-text

(defun org-fas-gantt--collect ()
  "Return a list of task plists from the current Org buffer.
Only headings with both a SCHEDULED and a DEADLINE timestamp
are included.  :start and :end are absolute day numbers so
they are cheap to subtract."
  (delq nil
        (org-map-entries
         (lambda ()
           (let ((start (org-get-scheduled-time (point)))
                 (end   (org-get-deadline-time (point))))
             (when (and start end)
               (list :title  (org-get-heading t t t t)
                     :agent  (or (org-entry-get (point) "AGENT") "Unassigned")
                     :effort (org-entry-get (point) "Effort")
                     :start  (time-to-days start)
                     :end    (time-to-days end))))))))

(defun org-fas-gantt--bar (task day0 span)
  "Return one HTML row for TASK, scaled against DAY0 across SPAN days."
  (let* ((left  (* 100.0 (/ (float (- (plist-get task :start) day0)) span)))
         (width (max 1.0 (* 100.0 (/ (float (- (plist-get task :end)
                                               (plist-get task :start)))
                                     span))))
         (effort (plist-get task :effort)))
    (format (concat "<div class=\"row\"><span class=\"label\">%s</span>"
                    "<span class=\"track\"><span class=\"bar\" "
                    "style=\"left:%.1f%%;width:%.1f%%\" title=\"%s\"></span>"
                    "</span></div>")
            (org-html-encode-plain-text (plist-get task :title))
            left width
            (if effort (format "Effort: %s" effort) ""))))

(defun org-fas-gantt-render-to-html (&optional file)
  "Render the current buffer's Gantt tasks to FILE as HTML.
Interactively prompts for the output file.  Returns the file name."
  (interactive)
  (let* ((tasks (org-fas-gantt--collect))
         (file  (or file (read-file-name "Write Gantt HTML to: " nil "gantt.html"))))
    (unless tasks
      (user-error "No tasks with both SCHEDULED and DEADLINE found"))
    (let* ((day0 (apply #'min (mapcar (lambda (tk) (plist-get tk :start)) tasks)))
           (dayN (apply #'max (mapcar (lambda (tk) (plist-get tk :end)) tasks)))
           (span (max 1 (- dayN day0)))
           (agents (cl-remove-duplicates
                    (mapcar (lambda (tk) (plist-get tk :agent)) tasks)
                    :test #'equal :from-end t)))
      (with-temp-file file
        (insert "<!DOCTYPE html><html><head><meta charset=\"utf-8\">\n"
                "<style>\n"
                "body{font-family:sans-serif;margin:2rem;color:#222;}\n"
                "h2{border-bottom:1px solid #ccc;margin-top:1.5rem;}\n"
                ".row{display:flex;align-items:center;margin:.25rem 0;}\n"
                ".label{width:16rem;font-size:.9rem;padding-right:.5rem;}\n"
                ".track{position:relative;flex:1;height:1.2rem;background:#f0f0f0;border-radius:3px;}\n"
                ".bar{position:absolute;top:0;height:100%;background:#4a7;border-radius:3px;}\n"
                "</style></head><body>\n"
                "<h1>Project timeline</h1>\n")
        (dolist (agent agents)
          (insert (format "<h2>%s</h2>\n" (org-html-encode-plain-text agent)))
          (dolist (task tasks)
            (when (equal (plist-get task :agent) agent)
              (insert (org-fas-gantt--bar task day0 span) "\n"))))
        (insert "</body></html>\n"))
      (message "Wrote %s (%d task%s)" file (length tasks)
               (if (= (length tasks) 1) "" "s"))
      file)))

It reads your timestamps, does a little arithmetic to turn dates into percentages, and writes plain HTML and CSS. You could extend it to color bars by status or add a “today” line in an afternoon.

You might be wondering why HTML. I love HTML and CSS. It’s simple, and anyone can read it. You can easily throw it up to a server and share it with anyone capable of opening a web browser.

You may prefer a different delivery format. Or, importantly, your stakeholders and team might prefer a different format. That’s fine. The point here is that from org mode you can generate any format you want.

The renderer doesn’t matter as much; the point is that the source of truth is still the .org file. You can change the renderer next year and your project data is pristine.

Here’s a picture of my Gantt chart for a fake video game release. This was totally built from org mode and then exported to HTML. What you see is the browser rendering:

Now that’s a nice visual indicator of progress that costs you and your team nothing, but looks really fancy.

Keeping the team on schedule

If you are coordinating with people who don’t use Emacs, which is almost everyone, the workflow is:

  1. You maintain the org file as the planning document.
  2. You export to HTML (or PDF, or a quick PNG) and share it to a central location.
  3. Updates come back as comments or from a meeting; you transcribe them into the org file.

It may sound inefficient, but in practice it’s the same number of steps as maintaining a separate Asana board, minus the SaaS subscription, plus the fact that your project plan now lives next to your notes and your agenda, and you’re not locked into a proprietary format.

If this post saved you a SaaS subscription or two, consider tossing a few dollars in the tip jar. It goes straight back into writing more of these.

The post Org mode as a gantt chart generator appeared first on Chris Maiorana.

-1:-- Org mode as a gantt chart generator (Post Chris Maiorana)--L0--C0--2026-07-12T15:01:11.000Z

Srijan Choudhary: 2026-07-12-001

#TIL that #Emacs also has a Global Mark Ring:

the global mark ring records a sequence of buffers that you have been in, and, for each buffer, a place where you set the mark

For programming buffers / projects, I've been using the xref stack to go back/forward when jumping around. But the global mark ring is super useful as a general purpose tracker of my context jumps.

By default, C-x C-<SPC> jumps back. There is no forward like xref-go-forward, but it' a ring so it's possible to go around. Or there's always consult-global-mark to show the marks in a list and select from it.

Other helpful links:

-1:-- 2026-07-12-001 (Post Srijan Choudhary)--L0--C0--2026-07-12T07:50:00.000Z

Protesilaos: Emacs: doric-themes version 1.2.0

These are my minimalist themes. They use few colours and will appear mostly monochromatic in many contexts. Styles involve the careful use of typography, such as italics and bold italics.

If you want maximalist themes in terms of colour, check my ef-themes package. For something in-between, which I would consider the best “default theme” for a text editor, opt for my modus-themes.

Below are the release notes.


Version 1.2.0 on 2026-07-12

Two new themes

doric-tiger is a light theme, while doric-lion is dark. Both have warm colours.

Org agenda events are easier to spot

Events are entries which have an active timestamp but not a SCHEDULED or DEADLINE keyword associated with it. Those are now rendered in italics in addition to the faint foreground they already had (the faint foreground is there because an event is not as important as a

Support for vc-dir-key-binding-hint-label (Emacs 32)

This concerns a new option for VC Dir buffers to display their available key bindings. The face applies to the additional headings. They should now look like all the other headings in those buffers in the interest of stylistic consistency.

Some package.el faces stand out more

Those are present in the buffer that M-x list-packages produces. They concern certain status indicators and fit in better with the rest of the design.

The nobreak-space face is now underlined

This is one way to make that character visible. It is useful to know that a space is not the regular space. For example, in French orthography we are expected to include non-breaking spaces between the quotes and the words like « Protesilaos ». Whether you actually follow that guideline is another discussion—I happily ignore it.

-1:-- Emacs: doric-themes version 1.2.0 (Post Protesilaos)--L0--C0--2026-07-12T00:00:00.000Z

Irreal: Highest Org Priority

If you’re an Org user, you probably know that you can assign a priority to TODO items. By default, the priorities are A, B, and C but with a bit of trickery you can extend the ranges to 0–9 and A–Z.

I never use priorities but Raymond Zeitler does and after figuring out the above trickery he thought it would be useful to have an ultimate priority a sort of “drop everything and do this immediately” priority. He decided to make this priority 100 times the highest priority and to designate it with !.

The solution wasn’t straightforward but it wasn’t too difficult either. The thing about Emacs is that it’s easy to check the source code to see how things are done. It turns out that Org has a org-priority-get-priority-function variable that contains a function that Org calls to get the priority from the priority cookie. Zeitler wrote his own version to check for a ! and return the high priority if it’s found. If not, then it calls a slightly modified version of the normal function.

As I say, I don’t make use of the priority mechanism and have no idea how many people do but if you’re one of them and are looking for a way to specify an extraordinary priority, take a look at Zeitler’s post. You can cut and paste his code if you’re interested.

-1:-- Highest Org Priority (Post Irreal)--L0--C0--2026-07-11T15:10:09.000Z

Marcin Borkowski: Fast spell-checking in Emacs with Jinx

Emacs has had a spell-checker for a very long time. In fact, I’ve been using ispell-word for at least several years now, as I have mentioned for example here. However, I’ve only used Flyspell mode once, over two decades ago, when I tried it out on a machine with maybe 8MB RAM and a Pentium-class processor clocked at about 100MHz and decided that it’s too slow to be really useful. Well, things have changed a bit since then, haven’t they?
-1:-- Fast spell-checking in Emacs with Jinx (Post Marcin Borkowski)--L0--C0--2026-07-11T06:52:02.000Z

Aimé Bertrand: Preserving email dates when moving messages in Mu4e

The problem

My email setup is the usual text based machinery I keep writing about: Mu4e with mu, isync (mbsync) and msmtp. It works beautifully – until one day it did not.

When I moved emails around in Emacs – with mu4e-headers-mark-for-move, mu4e-headers-mark-for-refile or other command – the date of the moved email changed. A mail from last March would suddenly float to the top of the mailbox as if it had just arrived.

The diagnosis

The Date: header inside the message is never touched – that is why Mu4e always looks correct, it shows the date parsed from that header.

What changes is the arrival date: the IMAP “internal date” the server stores separately from the body. That is what Mail.app shows and sorts by (“Date Received”).

The culprit is this setting, which I need to avoid duplicate-UID errors with mbsync:

(setq mu4e-change-filenames-when-moving t)

With it on, Mu4e does not just move the file on refile – it rewrites the message as a brand-new Maildir file, as if freshly delivered, resetting its arrival time to now. mbsync then pushes that up and the server stamps the new internal date. Mail.app is exempt because it moves server-side via IMAP MOVE, which preserves the internal date.

First fix: CopyArrivalDate (necessary, not sufficient)

The fix lives in mbsync, not Emacs. In ~/.maildir/mbsyncrc, once at the top so it applies to every channel:

CopyArrivalDate yes

This propagates the arrival time with the message instead of letting the server stamp a fresh one. But on its own it was still wrong – because mbsync reads that arrival time from the file’s modification time (mtime), and Mu4e already reset the mtime to now when it rewrote the file. So CopyArrivalDate faithfully copies the wrong value.

The real fix: restore the mtimes before syncing

I keep mu4e-change-filenames-when-moving at t and, right before each sync, walk the Maildir, read each message’s Date: header, and set the file’s mtime to match. Then CopyArrivalDate yes carries the correct value up.

Thank god for Claude Code for the help with the Python script.

~/.maildir/fix-maildir-mtimes.py:

#!/usr/bin/env python3

"""Restore Maildir mtimes from each message's Date: header, so that
mbsync's CopyArrivalDate propagates the correct received date."""

import os
import sys
import time

from pathlib import Path
from email.parser import BytesParser
from email.utils import parsedate_to_datetime

maildir = Path(os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else "~/.maildir"))
window  = float(sys.argv[2]) if len(sys.argv) > 2 else 2 * 86400
cutoff  = time.time() - window if window else 0

for f in maildir.rglob("cur/*"):
    try:
        st = f.stat()
        if window and st.st_mtime < cutoff:
            continue
        with open(f, "rb") as fh:
            msg = BytesParser().parse(fh, headersonly=True)
        dt = parsedate_to_datetime(msg.get("Date"))
        if dt is None:
            continue
        ts = dt.timestamp()
        if abs(st.st_mtime - ts) > 1:
            os.utime(f, (ts, ts))
    except Exception:
        continue

Wrapping it in front of mbsync

Rather than hooking “into” mbsync, I put a wrapper in front of it. This is ~/.maildir/sync-mail.py (yes, the .py on a bash script is a small lie – it started life in Python):

#!/usr/bin/env bash

~/.maildir/fix-maildir-mtimes.py "$HOME/.maildir"

exec mbsync "$@"

The fixer runs against the whole ~/.maildir root, covering every account in one pass. $@ is forwarded straight to mbsync, so I can pass a single channel name or --all. Make both files executable:

chmod +x ~/.maildir/fix-maildir-mtimes.py ~/.maildir/sync-mail.py

Hooking it into Mu4e

Mu4e simply runs whatever is in mu4e-get-mail-command, so I point that at the wrapper. By default it syncs everything:

(customize-set-variable 'mu4e-get-mail-command
                        (concat (executable-find "~/.maildir/sync-mail.py") " --all"))

And for the times I want to sync a single account, timu-mu4e-get-mail lets me pick it first:

(defun timu-mu4e-get-mail ()
  "Select the Account before syncing.
This makes the syncing of mails more flexible."
  (interactive)
  (let ((mu4e-get-mail-command
         (concat
          "~/.maildir/sync-mail.py "
          (completing-read
           "Which Account: "
           '("icloud" "aimebertrand" "moclub" "--all")))))
    (mu4e-update-mail-and-index t)))

(keymap-set mu4e-headers-mode-map "M-r" #'timu-mu4e-get-mail)
(keymap-set mu4e-main-mode-map    "M-r" #'timu-mu4e-get-mail)
(keymap-set mu4e-view-mode-map    "M-r" #'timu-mu4e-get-mail)

Because sync-mail.py runs the fixer before exec mbsync, every sync I trigger with M-r repairs the dates first – asynchronously, so Emacs never blocks. If you also sync outside Emacs (cron, launchd, a terminal), point those at sync-mail.py too, or a background sync could re-clobber a freshly moved message.

Conclusion

Three moving parts: CopyArrivalDate yes in mbsyncrc, fix-maildir-mtimes.py to restore the mtimes, and sync-mail.py wrapping both as the single entry point behind mu4e-get-mail-command.

Two honest caveats: it only fixes messages going forward – already-moved mails keep their overwritten server date – and some servers may stamp their own date on upload regardless. For my mailboxes it works wonderfully though. Yeah!!!

-1:-- Preserving email dates when moving messages in Mu4e (Post Aimé Bertrand)--L0--C0--2026-07-10T22:00:00.000Z

Raymond Zeitler: One Hundred Times the Highest Priority

In the previous post,1 I was disappointed to learn that valid priority characters are limited to 0–9 and A–Z. I thought it would be neat if the exclamation mark ("!") could represent the ultimate priority, a sort of "drop everything and do this immediately" kind of priority.

It turns out that you can use "!" for this purpose, regardless of which priority characters you've configured. The trick is to write a function and assign it to org-priority-get-priority-function.

The function (which I named org-get-cust-priority) simply looks for "!" in the priority cookie and returns one hundred times the value that would be returned by the highest valid priority. If it doesn't find "!" it simply calls a modified version of the usual org-get-property function,2 which I named org-get-std-priority. The two functions are shown below.3

(defun org-get-cust-priority (s)
  "Return 100 times the highest priority when S contains
a priority cookie with `!'.  Otherwise call the usual
`org-get-property' function. Intended to be assigned to
`org-priority-get-priority-function'."
  (interactive)
  (if (not (functionp org-priority-get-priority-function))
      (org-get-std-priority s)
    (string-match ".*?\\(\\[#\\(!\\)\\] ?\\)" s)
    (if (string-equal (match-string 2 s) "!")
        (* 100000 (abs (- org-priority-lowest org-priority-highest)))
      (org-get-std-priority s))))

(defun org-get-std-priority (s)
  "Find priority cookie and return priority.
S is a string against which you can match `org-priority-regexp'.
Same function as `org-get-priority' sans test for a custom
function in `org-priority-get-priority-function'."
  (save-match-data
    (if (not (string-match org-priority-regexp s))
        (* 1000 (- org-priority-lowest org-priority-default))
      (* 1000 (- org-priority-lowest
                 (org-priority-to-value (match-string 2 s)))))))

The number that Org uses to sort on priority depends on the values of org-priority-lowest and org-priority-highest,4 which are "C" and "A" by default. In the default configuration, the highest priority has a numeric value of 2000, while the lowest has a value of 0. If the highest and lowest priority characters are 1 and 9, respectively, the highest priority has a value of 8000.

You know you need this if you have about a dozen items on your agenda and half of them are prioritized A.


1 https://ray-on-emacs.blogspot.com/2026/07/numeric-priorities-in-org-mode.html
2 Modified just to prevent recursion.
3 I know this code isn't particuarly elegant. I could have made custom variables for "!" and the priority multiplier, for example. I'd like to use it for a while before deciding whether it's worth investing any more effort. I do welcome suggestions on how to improve.
4 Or rather, the difference between the highest and lowest priorities.

-1:-- One Hundred Times the Highest Priority (Post Raymond Zeitler)--L0--C0--2026-07-10T18:45:12.923Z

Charlie Holland: A Tree and a Server Walk Into a Core&#x2026;

1. TLDR   tldr

july-emacs-carnival-banner.jpeg

Two technologies have endowed every editor with IDE superpowers this past decade: tree-sitter, a generic incremental parser that gives your editor a live syntax tree of your code, and the Language Server Protocol (LSP), a generic client-server protocol that gives your editor code intelligence features like linting, type-checking, auto-completion, and code navigation. Each one replaces an M×N fan-out of per-language, per-editor hacks with a single, language- and editor-agnostic protocol. Emacs has merged support for both into its core, and shipped them in Emacs 29.1 (three years ago this month!). This post explains what each one does, visualizes a real-life tree-sitter parse tree, and recounts how both technologies made their way upstream into the glorious Emacs core.

An interactive visualization of the syntax tree tree-sitter builds from a small Python module — hover any node for details, click to zoom into a subtree. Two more of these live further down the post.

2. About   emacs carnival programming

This is my entry for July's Emacs Carnival, hosted by Andy over at Plain DrOps. The theme is Programming, i.e. Emacs as a programming environment. I've done these before (May was about deep Emacs patterns, June about Emacs teaching you Emacs), and this month I want to push back on my least favorite meme: that Emacs is arcane; rusty and dusty; old news; the "Editor for Middle Aged Computer Scientists".

Emacs has been under active development by a growing number of contributors, and is surprisingly modern. As two language-agnostic protocols for IDE features arose (tree-sitter for syntax and LSP for semantics), Emacs developers quickly merged support for both into core. More interestingly, they made them usable in the editor by integrating them into the Emacs machinery that predates them by decades.

The first suggested topic in the carnival is "Language Specific Setups" and, given the utility of tree-sitter and LSP, I couldn't help but misinterpret this suggestion intentionally and create a post on my "Language Generic Setup".

3. The Combinatorial Woes of M×N   leverage architecture

Traditionally, in most IDEs "support for language X" meant one implementation per editor, per language.

Syntax highlighting was a mixed bag of regular expressions that approximated the language's grammar.

Navigation ("jump to the function I'm in") was basically regex guesswork. Completion and go-to-definition, if they were even available, came from language-specific plugins of wildly varying quality.

With M editors and N languages, the world had to write (and maintain, and debug) M×N of these plugins. The modern fix is the classic programmer's reflex of establishing a generic layer, or protocol, in the middle, and paying a much more affordable M+N tax instead (the framing the LSP community itself uses).

This reflex is older than any editor in the diagram below, by the way. It's the UNCOL argument from 1958, which suggested putting one universal intermediate language between M source languages and N machines, and M×N compilers become M+N. UNCOL itself never shipped, but its argument became the standard justification for compiler intermediate representations, and the argument is just as valid for editor tooling.

graph TB
    subgraph before["Before: every editor reimplements every language (M × N)"]
        direction LR
        A1[Emacs] --- B1[Python]
        A1 --- B2[Rust]
        A1 --- B3[TypeScript]
        A2[Vim] --- B1
        A2 --- B2
        A2 --- B3
        A3[VS Code] --- B1
        A3 --- B2
        A3 --- B3
    end
    subgraph after["After: one generic layer in the middle (M + N)"]
        direction LR
        C1[Emacs] --> MID(("tree-sitter<br/>+ LSP"))
        C2[Vim] --> MID
        C3[VS Code] --> MID
        MID --> D1[Python grammar & server]
        MID --> D2[Rust grammar & server]
        MID --> D3[TypeScript grammar & server]
    end
    before ~~~ after

Tree-sitter provides the answer to "what is this text, structurally?" — it can tell the editor that characters 4 through 7 are a function name. LSP provides the answer to "what does this text mean, in this project?" — it knows that the function is defined in another file, is called from twelve places, and is missing an argument, etc…. These two layers solve "syntax and semantics" in a graceful, reusable way.

4. Tree-sitter: a Fresh Parse Tree on Every Keystroke   treesitter parsing

Tree-sitter, started by Max Brunsfeld while working on Atom at GitHub, is a parser generator plus an incremental parsing library. Rather than implementing a boat-load of regular expressions, you provide tree-sitter with a language's grammar, tree-sitter compiles it into a small C library, and any tool can then parse that language. Each grammar is its own small repository. The canonical ones live under the tree-sitter GitHub org (e.g. tree-sitter-python, the grammar behind every tree in this post), and the community maintains a list of hundreds more.

Compilers typically wouldn't work well for providing live feedback in the text-editing use case, but there are two properties that make tree-sitter performant and robust enough to be editor-grade:

  1. It's incremental. When you type a character, tree-sitter doesn't re-parse the file, but rather patches the existing tree in microseconds. The result is that you have an up-to-date parse tree, immediately, on every keystroke.
  2. It's error-tolerant. Code being edited is malformed almost all of the time, because you are mid-keystroke more often than not. Tree-sitter handles this gracefully and keeps the rest of the tree intact instead of failing at the first syntax error.

These aren't new ideas so much as ideas finally made practical. The figure below comes from the 1998 Berkeley dissertation that inspired tree-sitter's development, and it shows the incremental trick. As an edit arrives, the parser restores a consistent tree by creating just two new nodes (black) and adjusting a handful of others (gray). Every other node in the tree is reused as-is. This is a phenomenal paper, by the way, I highly recommend you read it.

wagner-incremental-parsing.png

Figure 1: From Tim Wagner's 1998 Berkeley dissertation, Practical Algorithms for Incremental Software Development Environments (his Figure 4.3), the work tree-sitter's incremental parsing builds on. Dashed lines are the paths walked to reach the edit site.

4.1. Looking at Concrete Trees

You'll most often hear these trees called Abstract Syntax Trees (ASTs). Tree-sitter's own docs use the term, but strictly speaking, tree-sitter produces a concrete syntax tree (CST). While an AST discards everything that stops mattering once the structure is known (keywords, parentheses, punctuation), a CST keeps every last token of the source. Editors need the concrete version, because you can't highlight a def you've thrown away. You'll see this in the charts below, where the keyword and punctuation tokens appear in the tree, in gray.

Here I show a few examples, in order of increasing complexity, of the syntax trees that tree-sitter actually produces from Python code.

There are many ways to visualize these trees. Mine are drawn with plotly as an icicle chart. Each tile is a node in the tree. A tile's width is proportional to the amount of source code the node covers, and its children sit beneath it. Color encodes the node's role in the code. These charts are interactive and fun to play with (which is why I created three examples!). You can hover over any tile to see the node type and the exact source bytes it covers, and you can click a tile to zoom into its subtree (the breadcrumb bar that appears on top zooms you back out).

4.1.1. Fibona-tree

Here's everyone's favorite, flogged-to-death fibanacci function:

def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

Here is the tree that tree-sitter produces.

Notice how the tree covers the text completely. Even def and each ( get a node — remember, that's the concrete in "concrete syntax tree". Any syntactical question or request the editor has ("am I inside a function?", "select the whole if statement", "highlight this as a parameter") can be answered by querying this tree.

Something I love about Emacs's support for this is that you don't need my chart to see these trees concretely. In Emacs 29+, you can open any *-ts-mode buffer and run M-x treesit-explore-mode. Then Emacs shows you this same live tree and highlights the node at point as you move. In this way, and very much in the spirit of last month's post on discoverability and introspection, Emacs can show you its own parse trees.

4.1.2. Scaling Up: quicksort

Here's a more complicated tree for a quicksort algorithm, implemented using list comprehensions, which are not as intuitively tree-like:

def quicksort(xs):
    if len(xs) <= 1:
        return xs
    pivot, rest = xs[0], xs[1:]
    lo = [x for x in rest if x < pivot]
    hi = [x for x in rest if x >= pivot]
    return quicksort(lo) + [pivot] + quicksort(hi)

There are two things worth noticing as you play with this tree. First, each list_comprehension is a single expression node that contains an entire clause structure (a for_in_clause and an if_clause). In this way, the editor's request to "select the whole comprehension" is one node-hop, rather than a text scan for matching brackets. Second, notice the tree got wider but not much deeper than fib's. Expression-heavy Python fans out horizontally.

4.1.3. Zooming Out: a Whole Module

Here's the typical form of an entire (albeit short) module in Python.

import math
from dataclasses import dataclass


@dataclass
class Point:
    """A point in the plane."""

    x: float
    y: float

    def distance_to(self, other):
        return math.hypot(self.x - other.x, self.y - other.y)


def centroid(points):
    n = len(points)
    return Point(
        sum(p.x for p in points) / n,
        sum(p.y for p in points) / n,
    )


def nearest(points, target):
    return min(points, key=target.distance_to)

Look at the second row and you will see import_statement, import_from_statement, decorated_definition, function_definition, function_definition. That row is your file outline, and it's what Emacs tools like imenu, which-function-mode, and code folding query from the tree. Every structural feature your editor offers over this file starts from some node in this picture. When your editor "expands the selection to the enclosing expression", it is literally walking one row up this diagram.

This brings us to the tools that do exactly that.

4.2. What Emacs Does with the Tree   fontlock navigation

With a live tree in hand, the classic per-language hacks become generic one-liners over nodes:

  • Highlighting (font-lock) becomes precise. Regexes were always fine when the answer sits inside the match (def foo is a definition, the def is right there), but they fail when it doesn't. Highlighting the uses of a parameter inside a function body, or distinguishing a C declaration from a call, requires structure and scope, and matching nested structure is provably beyond regular expressions. Classic highlighters approximated this detection with stacks of context rules, but with a tree, those distinctions are just node types.
  • Navigation and selection by type: you can easily jump to the enclosing function, next class, or previous statement, because the tree knows the exact bounds of everything, whether you're reading finished code or still typing it out.
  • Indentation, folding, imenu all subsist on the same structure, the syntax tree.

The showcase for navigation is Mickey Petersen's Combobulate, built on Emacs's built-in treesit library. It gives you structural editing across Python, TypeScript, Go, YAML, and more. It ships with commands for expanding the selection node-by-node, dragging whole siblings up and down, hopping between parameters or list elements, placing cursors on every matching node, and many more. It also has wonderfully guiding transient menus, making Combobulate a phenomenal way to learn a language's structure too.

Combobulate: expand the selection node-by-node

Expand the selection node-by-node — one row up the icicle charts above

Combobulate: navigate between sibling nodes in JSX

Hop between sibling nodes (here in JSX)

Combobulate: drag whole sibling nodes up and down

Drag whole siblings up and down

Combobulate: clone the node at point

Clone the node at point

Combobulate: place cursors on every matching node

Place cursors on every matching node

Combobulate: splice a node up into its parent

Splice a node up into its parent

Figure 2: Combobulate in action (gifs from the Combobulate repo). Click any gif for a full-screen view.

5. LSP: Tapping a Compiler's Brain   lsp architecture

Tree-sitter knows your file's structure, but it doesn't know your project. "Where is this function defined?", "where is it called?", "what's this variable's type?". Answering those questions requires language analysis, the front half of a compiler (front half meaning the parsing, name resolution, and type checking, without the code generation).

The Language Server Protocol, published by Microsoft in 2016 for VS Code, solves this with the same M+N trick, outside of the editor. The language and project intelligence lives in a standalone server process (pyright, rust-analyzer, clangd, gopls, …), and the editor talks to it over JSON-RPC. Your editor is a client of the language server.

sequenceDiagram
    participant E as Emacs (eglot)
    participant S as Language server (e.g. pyright)
    E->>S: initialize (what can you do?)
    S-->>E: capabilities (completion, definitions, rename, ...)
    E->>S: textDocument/didChange (user typed something)
    S-->>E: textDocument/publishDiagnostics (line 12: type error)
    E->>S: textDocument/definition (M-. on a symbol)
    S-->>E: Location (utils.py, line 48)

Any editor that can speak this protocol (the Language Server Protocol) is provided with completion, go-to-definition, find-references, rename, hover documentation, and on-the-fly diagnostics for every language that has a server. Because the language servers follow a protocol, integrating these intelligence features across languages is easy. The server is usually written by the people who know the language best, often the compiler team itself.

5.1. Eglot: LSP, the Emacs way   eglot builtins

Emacs's built-in client is João Távora's Eglot ("Emacs polyGLOT", started in 2018). What makes Eglot special is how it uses Emacs machinery that has already existed for decades to integrate with these language servers.

LSP capability Served via
Go to definition xref
Hover docs eldoc
Diagnostics flymake
Completion completion-at-point
Symbol outline imenu
Project boundaries project.el

M-x eglot in a programming buffer is genuinely all it takes to start using a language server, and these Emacs tools you've probably already used will work, but with richer information provided by the language servers.

The coolest part of this integration story is that Emacs honored a modern protocol by adopting it, and Eglot honored Emacs by expressing that protocol through Emacs's native idioms. Adopting the power of LSP didn't mutate Emacs in any significant way. It just added one package integrating LSPs with the existing Emacs machinery.

6. The Road Into Core   history upstream

Both tree-sitter and LSP support followed a similar path into Emacs, where external experimentation proved the concept and value, and then the core maintainers did the hard work of making it native.

timeline
    1998 : Wagner's Berkeley dissertation works out incremental parsing for editors
    2013-2017 : Max Brunsfeld develops tree-sitter at GitHub for Atom
              : Microsoft publishes LSP (2016) — lsp-mode brings it to Emacs (2017)
    2018-2020 : Eglot appears as an external package (João Távora)
              : elisp-tree-sitter binds tree-sitter to Emacs via dynamic modules (Tuấn-Anh Nguyễn)
    2022 : October — Eglot merged into Emacs core
         : November — Yuan Fu's native tree-sitter integration (treesit.el) merged
    2023 : July — Emacs 29.1 ships both, plus python-ts-mode, c-ts-mode, and friends

Tree-sitter merged in November 2022, weeks after Eglot's October merge, and Emacs 29.1 (July 2023) shipped them together with a family of *-ts-modes and M-x treesit-install-language-grammar. Emacs 29.1 was a landmark release because it delivered modern, language-agnostic syntax highlighting and code semantics in one fell swoop.

7. Try It   handson

On Emacs 29 or later, no packages required:

;; fetch + compile the grammar
M-x treesit-install-language-grammar RET python RET
;; tree-sitter highlighting
M-x python-ts-mode
;; watch the live parse tree
M-x treesit-explore-mode
;; connect to a language server
M-x eglot

(For eglot you'll need a server on your PATH, e.g. pip install pyright. To make the ts modes the default, see major-mode-remap-alist.)

treesit-install-language-grammar clones a grammar repository and compiles it for you. If the language isn't already listed in treesit-language-source-alist, Emacs prompts for the repository and pre-fills the canonical guess, https://github.com/tree-sitter/tree-sitter-<language>. For anything more exotic, pick a repository from the community's list of parsers.

-1:-- A Tree and a Server Walk Into a Core&#x2026; (Post Charlie Holland)--L0--C0--2026-07-10T18:36:07.000Z

Charles Choi: In Emacs, Everything Looks Like a Service

A common refrain is that Emacs is an operating system (OS). This isn’t true, but what invites comparison to an OS is its ability to orchestrate applications and utilities above the OS kernel level. The diagram below suggests a truer picture of how Emacs’ relates to an OS and its capabilities.

img

Emacs’ built-in access to OS system services (file system, network, etc.) coupled with the ability to run other programs makes it routine to improvise client behavior within it. Because of this, Emacs users are able to accomplish many of their computing needs from the different client modes that have been made for it. This gives credence to the notion of “living only in Emacs.”

In this post, we’ll examine some of the ways Emacs lets you build a client. By the end of this post, you’ll hopefully be convinced that from within Emacs, everything looks like a service.

Client-Server Model

Let’s first provide some definitions.

The Client–Server model is a common computer interaction pattern where a task is partitioned between the provider of a resource (the service) and the requester of that resource (the client). The client issues a request to the server, and the server in turn returns a response as shown in the diagram below.

img

Depending on the implementation, the transaction (request + response) can occur over a network or be local to a system. Client-server models using a network has been most elaborated upon with REST-style software architectures. Shown in the sequence diagram below is a common implementation pattern for REST-style client server architecture.

img

Emacs as a Client

From the diagram above, there are three concerns the client is typically responsible for:

  • UI: User interface (if any).
  • Client Edge: Sub-system concerned with communication with the service. For networked clients, this is the network sub-system.
  • Local Database: Representation of data that is exchanged or synchronized with the server. How this data is managed is up to the implementation requirements.

For the above concerns, Emacs provides numerous libraries both built-in and third-party which can implement a client. Listed below are some built-in libraries with their respective links for further reading:

Requirements dictate the amount of complexity required to implement the Emacs client. If there is an existing command line utility that can do the “heavy lifting”, said utility can be reframed as a “service” that can be accessed via a shell call.

img

Elisp

All the libraries mentioned above are accessed through the Emacs Lisp (Elisp) programming language. Elisp is a dynamic programming language which allows for a high degree of improvisation during run-time. This capability allows for complex orchestration of any behavior that is available to Emacs, from Elisp functions to shell commands.

Example wttr.in client

wttr.in is a console-oriented weather forecast web-service. It supports JSON output so we can build an Emacs wttr command which will prompt for a location, make the HTTP request, process the JSON response and display the result in the mini-buffer.

The top-level command wttr is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defun wttr (location)
  "Show weather conditions for LOCATION from `https://wttr.in' in mini-buffer.

Result is also stored in `kill-ring'."
  (interactive "sWhere (default: local): ")

  (condition-case err
      (let* ((url (wttr--request-url location))
             (jsondb (fetch-json-as-hash-table url))
             (msg (wttr--report-message jsondb)))
        (kill-new msg)
        (message "%s" msg))

    (error (message "ERROR: %s" (cdr err)))))

The wttr.in URL is constructed by the function wttr--request-url shown below.

1
2
3
4
5
6
7
(defun wttr--request-url (location)
  "Construct wttr.in URL with LOCATION."
  (let* ((base-url (url-generic-parse-url "https://wttr.in"))
         (encoded-location (string-replace " " "+" location))
         (query (format "/%s?0&format=j1" encoded-location))
         (_dummy (setf (url-filename base-url) query)))
    (url-recreate-url base-url)))

We can subsequently pass that URL into fetch-json-as-hash-table which does the heavy lifting of retrieving the URL and parsing the JSON response into an Elisp hash-table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(defun fetch-json-as-hash-table (url)
  "Fetch URL with expected JSON response and return a `hash-table'."
  (let ((data-buffer (url-retrieve-synchronously url)))
    (if (not data-buffer)
        (error "Failed to fetch data from %s" url)
      (unwind-protect
          (with-current-buffer data-buffer
            ;; Move point past the HTTP metadata headers
            (goto-char url-http-end-of-headers)
            ;; Parse the remaining JSON buffer into a hash-table
            (json-parse-buffer :object-type 'hash-table))
        ;; Always kill the downloaded network buffer to prevent memory leaks
        (kill-buffer data-buffer)))))

Finally we can extract the desired values from the JSON response (jsondb) to populate the message that will sent to the mini-buffer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(defun wttr--report-message (jsondb)
  "Generate weather report message from JSONDB."
  (let* ((area-buflist ())
         (nearest-area
          (wttr--get-first jsondb "nearest_area"))
         (area-name
          (map-elt (wttr--get-first nearest-area "areaName") "value"))
         (region
          (map-elt (wttr--get-first nearest-area "region") "value"))
         (country
          (map-elt (wttr--get-first nearest-area "country") "value"))

         (current-condition (wttr--get-first jsondb "current_condition"))
         (temp_c (map-elt current-condition "temp_C"))
         (temp_f (map-elt current-condition "temp_F"))

         (weather-description
          (map-elt
           (wttr--get-first current-condition "weatherDesc") "value")))

    (mapc (lambda (x)
            (if (and x (not (string-equal x "")))
                (push x area-buflist)))
          (list area-name region country))

    (format "%s: %s°C, %s°F %s"
            (string-join (reverse area-buflist) ", ")
            temp_c
            temp_f
            weather-description)))

wttr.el source

Closing Thoughts

At this point, hopefully you are convinced of the title assertion that from Emacs, everything looks like a service. Furthermore, many of the APIs offered by Emacs work at a high-level of abstraction. Consider that the lines of code for wttr.el weighs in at 67. (Result using the cloc utility.)

If that’s too much, then imagine an alternate implementation where the actual network request and JSON processing is done in a Python script called weather. Then the Elisp command to invoke it is just the code shown below.

1
2
3
4
5
6
7
8
9
(defun weather (location)
  "Call weather script with LOCATION and show result in minibuffer."
  (interactive "sWhere (default: local): ")

  (let* ((weather-cmd "weather")
         (cmd (if location (format "%s %s" weather-cmd location) weather-cmd))
         (result (shell-command-to-string cmd)))
    (kill-new result)
    (message result)))

With the above implementation, the shell command becomes effectively the “service” to make a request to.

As Elisp is a dynamic programming language, it can allow for integration of Elisp libraries with command line utilities in an improvised fashion.

This capability is compelling to users who recognize the opportunities it can offer.

-1:-- In Emacs, Everything Looks Like a Service (Post Charles Choi)--L0--C0--2026-07-10T06:40:00.000Z

Raymond Zeitler: Numeric Priorities in Org Mode

Org mode is configured "out-of-the-box" with these three levels of priority: A, B, C, where A is the highest priority. However, this can be changed. You can use numbers to indicate priority, such as 1 (highest) to 10 (lowest) as shown in the documentation.1

If you specify priorities from 65 to 90, Org will interpret the numbers as ordinal values of ASCII characters and give you "A" to "Z." If you try to be clever and specify priorities as 65 to 91 (91 is the decimal ASCII code for left square bracket character) you'll be disappointed that when you lower the priority from "Z," you don't get "[." (I was disappointed, anyway.)

You can define priorities from 10 to 99, for example. But if you try to get to a priority higher than 10 using org-priority-up, Org will clear the priority cookie. However, you can manually enter a priority of 0 to ensure the item appears above all other items. (Please see Figure 1, below.) Thus, a priority value of 0 can be your "stat" item. It would be super neat if Org could understand that ASCII characters below 0 (such as the exclamation mark) are an even higher priority than 0. Unfortunately, it doesn't even recognize [#!] as a priority cookie.

A large priority span seems like it would be useful. For example, you might have all utility bills set from 50 to 59, and shopping for birthday or anniversary gifts from 10 to 19. However, in order to go from a priority of 50 to 10, you'd have to press the Up Arrow key forty times. If you think you can invoke M-4 0 M-x org-priority-up to go quickly from 50 to 10, you'll be disappointed (again). The function org-priority-up doesn't accept a prefix argument.2

How do you use priority in Org?


1 Org Manual -- Priorities

2 But you can record a macro that consists of a single S-<up> key press and then play it back with a prefix argument of 39.

-1:-- Numeric Priorities in Org Mode (Post Raymond Zeitler)--L0--C0--2026-07-09T23:48:51.151Z

Amit Patel: Emacs: marking text

Although I primarily use Emacs, I love the idea of vim text objects, and wanted to incorporate them into my editing. The Kakoune introduction explains that vim uses verb first, noun second for its commands, whereas Kakoune uses noun first, verb second. I wrote a blog post about why I prefer noun-verb over verb-noun, not only in text editors but also in games and other applications.

I started cobbling together some commands in a hydra, trying to match vim keys when I could:

(defhydra hydra-mark (:body-pre (set-mark-command nil) :color red)
  "
_w_, _,w_: word, symbol    _i'_, _a'_: string  _i)_, _a)_: pair
_t_, _f_: to char (exclude/include)   _0_, _$_: begin/end of line
_;_: comment   _u_: url    _e_: email      _>_: web-mode block or tag
_S_: sexp      _d_: defun  _p_: paragraph  _s_: sentence
_h_, _j_, _k_, _l_: move   _H-._: off
  "
  ("t" mark-to-char-exclusive)
  ("f" mark-to-char-inclusive)
  ("0" move-beginning-of-line)
  ("$" move-end-of-line)
  ("w" er/mark-word)
  (",w" er/mark-symbol)
  ("i'" er/mark-inside-quotes)
  ("a'" er/mark-outside-quotes)
  ("i)" er/mark-inside-pairs)
  ("a)" er/mark-outside-pairs)
  ("i]" er/mark-inside-pairs)
  ("a]" er/mark-outside-pairs)
  ("j" next-line)
  ("k" previous-line)
  ("h" left-char)
  ("l" right-char)
  (";" er/mark-comment)
  ("u" er/mark-url)
  ("e" er/mark-email)
  ("d" er/mark-defun)
  ("S" mark-sexp)
  ("s" mark-end-of-sentence)
  ("p" mark-paragraph)
  (">" web-mode-mark-and-expand)
  ("H-." deactivate-mark :exit t)
   )
(defun my/hydra-mark ()
  (interactive)
  (set-mark-command nil)
  (hydra-mark/body))

(bind-key "H-." #'my/hydra-mark)
And some helper functions:
(defun move-to-char (arg char)
  (interactive (list (prefix-numeric-value current-prefix-arg)
                     (read-char "Move to char: " t)))
  (search-forward (char-to-string char) nil nil arg))

(defun mark-to-char-exclusive (arg char)
  "Mark up to but not including ARGth occurrence of CHAR."
  (interactive (list (prefix-numeric-value current-prefix-arg)
                     (read-char "Mark to char: " t)))
  (set-mark
   (save-excursion
     (move-to-char arg char)
     (backward-char)
     (point))))

(defun mark-to-char-inclusive (arg char)
  "Mark up to and including ARGth occurrence of CHAR."
  (interactive (list (prefix-numeric-value current-prefix-arg)
                     (read-char "Mark to char: " t)))
  (set-mark
   (save-excursion
     (move-to-char arg char)
     (point))))

(use-package expand-region) ;; for the er/* commands

I've been using this since 2017. It's now 2022. How did it go?

Old habits are hard to break. I used this for urls, words, strings, almost none of the others. It's easier for me to move the cursor manually than to learn the specific commands, unless the command is something I use often.

So I'm going to call this experiment complete. I learned that it it's not going to work for me. That's ok. I try lots of things and most don't work. Some do, which is why I keep trying.

I still think the idea is good but I have 30 years of emacs muscle memory to fight.

I considered switching to one of these:

  • mark-thing-at lets you define keys for marking each type of thin
  • objed lets you work on text objects, inspired by vim and kakoune
  • expand-region will guess the object instead of making me choose

I decided I'll remove my experiment code and try expand-region next. [update 2025-05: gave up on expand-region in general, as I only use mark word or url, so I wrote something specific to that.]

-1:-- Emacs: marking text (Post Amit Patel)--L0--C0--2026-07-09T23:09:59.982Z

Irreal: Globally Overriding Keys

Protesilaos Stavrou (prot) has an excellent video that addresses a problem I often see people asking about. The problem is how to globally set a key binding so that it won’t be overridden. He uses setting Meta+o to other-window as an example to explore the problem.

Prot begins by defining Meta+o in the global map in the usual way:

(define-key global-map (kbd "M-o") 'other-window )

That works well until he tries it in an HTML buffer. In the HTML buffer, Meta+o turns out to be a prefix key for inserting an HTML face command. The problem is that Emacs will use the most specific bindings first. Global bindings are the most general, followed by major mode bindings, and ending with minor mode bindings as the most specific.

Since HTML is a major mode, its keybinding take precedence over the global binding that we set. The answer is to get more specific yet by defining our binding in a minor mode so that it will always have precedence.

Prot shows how to define a minor where we can define our binding and activate it globally so that it’s always available. Of course, once we have this mechanism set up, we can add any number of bindings to our minor mode. Prot demonstrates this by adding a binding that Org uses.

Take a look at Prot’s video for all the details. He’s also included a copy of the file he was working with so you can see everything without worrying about copying it down from the video. The video is 12 minutes, 45 seconds so it should be easy to fit in.

-1:-- Globally Overriding Keys (Post Irreal)--L0--C0--2026-07-09T14:57:14.000Z

Sacha Chua: Chatting with John Wiegley about personal information management and Karthik Chikmagalur

Karthik Chikmagalur suggested that we chat with John Wiegley to find out about the interesting things he's been doing with Org Mode. It was a really cool look at a heavy-duty workflow that dealt with thousands of open items. Since John was sharing his actual Org files instead of a simplified toy example, there was a fair bit of redaction to do. I got all the way to redacting these screenshots, but my brain didn't want to get around to figuring out how to redact the information in a live video where John was scrolling around and stuff. Instead of waiting for either me or Karthik to figure that part out someday, we figured we'd just post the audio, the transcript, and the screenshots. Here you go!

Chapters

  • 0:00 Getting set up with Zoom, chatting
  • 2:34 Increasing font size globally with C-x C-M-=
  • 3:47 Redacting information when sharing Org demos
  • 4:41 Daily Org Mode workflow for handling 40,000 entries
  • 6:24 Outlines for Focus and Home
  • 7:34 Column view
  • 8:44 Home is a collection of links
  • 9:22 C-c C-o opens one or all links in the Org Mode entry
  • 9:38 Example: Claude settings.json on multiple machines
  • 11:41 org-super-agenda is divided into topical categories
  • 12:54 Org-ql filters tasks
  • 13:20 Other agenda reports help review tasks
  • 13:29 Custom reports identify things that need to be filed
  • 13:37 I keep files in a flat directory and use search instead of categorizing files
  • 14:14 Make meaningful distinctions
  • 16:08 Color indicates agenda category
  • 17:00 Simplifying meaningful distinctions
  • 18:01 Capturing a task
  • 21:02 Task metadata is mostly automatic
  • 21:55 Task hash detects modifications
  • 22:24 Categorizing tasks
  • 22:56 Daily reports should be short; reschedule or unschedule aggressively
  • 25:41 Note from Karthik: John is an old-school Org user
  • 27:04 Continuing with the lifecycle: capture, file, schedule, review
  • 27:48 Habits
  • 28:09 org-review and items needing review; randomization
  • 29:49 org-review task properties
  • 31:48 It's all just plain text
  • 33:18 Capturing to the current point with M-0
  • 33:59 A different set of Org Roam capture templates
  • 34:18 Capturing by name
  • 34:38 An after-save hook automatically renames the files
  • 34:54 org-ql in the template provides a meeting agenda
  • 35:49 The "Review by" argument for the column view block filters the tasks
  • 36:36 Copying from Slack to paste into tasks
  • 38:13 Using gptel and large language models to generate titles and identify tasks
  • 40:03 after-save-hook adds a TODO file tag; the TODO file tag adds it to the list of org-agenda files
  • 40:33 Reviewing the task
  • 42:04 Karthik: "Things I capture in Org never get done."
  • 42:40 Grazing through tasks
  • 43:36 This is an advanced Org workflow
  • 45:11 Drafts: write down the text and then decide what to do with it
  • 46:47 My lab notebook collects notes and ideas throughout the day
  • 49:17 org-jw: normalizing structured data
  • 51:17 Copying structured Org data into a PostgreSQL database
  • 53:12 OpenClaw enables conversations with the data
  • 56:49 Avoid drift by using only one TODO system
  • 58:59 Capturing with Drafts on the Mac or Apple Watch
  • 1:01:01 A foot pedal makes speech-to-text even more convenient; whisperflow, handy
  • 1:06:39 Looking to the future
  • 1:07:07 John is running local models on a Mac Studio
  • 1:08:12 Using AI to facilitate getting data into PostgreSQL; structural limitations?
  • 1:11:04 Habits are better than goals
  • 1:11:24 Breaking down tasks with a large language model
  • 1:12:13 Inferring tasks with a large language model
  • 1:15:35 johnw/prompt-deploy has the prompts
  • 1:17:02 Summarizing our conversation so far
  • 1:19:13 Karthik's completing-read version of C-c C-o
  • 1:19:56 Karthik's thoughts on the demo so far
  • 1:21:29 Categories are subject areas
  • 1:22:58 TODO keywords use a fixed vocabulary
  • 1:24:48 Tags are context: person, place, or thing needed for the task
  • 1:26:14 Priority: A means must do, C means optional
  • 1:27:31 Properties are mostly automatically assigned metadata
  • 1:28:09 The hash
  • 1:28:51 Assigning the tags when refiling
  • 1:30:47 Per-file tags and tag validation
  • 1:31:29 Starting tasks with a restricted set of verbs and validating them
  • 1:33:08 Searching items: ripgrep, vector embedding, openclaw
  • 1:35:36 Linting and normalizing data in a pre-commit hook using Lefthook
  • 1:36:40 LLM-generated RFC-style specifications of the format
  • 1:37:17 The heading grammar
  • 1:37:52 A TODO example with everything
  • 1:38:27 Locating objects in the physical world with {text}
  • 1:40:37 Managing attachments: DEVONthink for long-lived files, org-attach for temporary ones
  • 1:41:45 Tags hint at where you can find more information
  • 1:42:31 :LINK: tags indicate that there is a link; LINK state says this note is only a link
  • 1:43:30 Some other code turns Firefox bookmarks into an Org Mode file
  • 1:44:06 The format specifications are LLM-generated
  • 1:45:30 Quick recap
  • 1:46:28 A quick look at Github repositories: org2jsonl
  • 1:47:16 org-gptel: chat with AI within Org Mode instead of having to leave it
  • 1:47:35 obr: fork of Rust port of Beads to use Org Mode for issue lists
  • 1:47:45 org-context saves metadata when refiling to allow restoring the task to its original location afterwards
  • 1:49:13 org-context use case: moving packing lists to the phone and back
  • 1:51:22 org-agenda-overlay
  • 1:53:05 john-wiegley-theme defines a palette
  • 1:56:16 Some numbers: 83 packages just related to Org Mode
  • 1:58:05 Org Mode inspiration
  • 1:58:51 Statistics
  • 2:00:18 Most-common properties: ID, CREATED
  • 2:00:33 Log entries are useful for notes by date
  • 2:01:48 Transcripts are great too

Transcript

Expand this for the transcript and screenshots

0:00 Getting set up with Zoom, chatting

Sacha: Recorded. There you go. Fantastic. Karthik was just telling me that you and Karthik have been having cozy chats on Discord about all the cool things you've been doing on the road.

John: That's right.

Sacha: Here we are.

Karthik: Okay. Hi, John.

John: Hey there, Karthik.

Sacha: Okay, I'm going to figure this out. There's probably a gallery view that I can use to show everybody on screen. Sorry, Zoom is—

John: That little nine-boxes icon in the upper right should give you that option.

Sacha: Nine boxes. I'm not seeing it. Nine boxes.

John: Oh, you might be on a Linux machine. I don't know how it works.

Karthik: It says "view." For me, it's a button on the top right that says "view."

Sacha: I'm just on the little screen and I'm in the web browser, so I don't see it. But I'm going to leave and I'll come right back.

John: Okay. It's been a while since we've seen each other in person, Karthik. How's the new job?

Karthik: Oh, it's busy. It's getting busier and busier. Just managing this two hours in the afternoon today was quite a deal.

John: Oh, I'm sorry to have been late. I did not mean to.

Karthik: Oh, no, no, it's fine. I'm glad we can finally capture your amazeballs Org setup.

John: We'll see what we can cover.

Karthik: So yeah, roughly speaking, I want to spend half an hour where you just show the various things you do with Org and how you use it to track your life. The other half hour, I'm going to badger you with philosophical questions about what it means to track your life.

John: Okay, that sounds fair.

Karthik: Right? Where Org will be a participant but not the focus. That's kind of my plan, but I'm going to let Sacha take it away because she's the consummate interviewer here. I will pipe in with questions. That's the plan. Okay, can we check if you can share your screen, your Emacs window, because I imagine we'll be seeing that a lot. Let me see here. My Emacs window. Okay.

2:34 Increasing font size globally with C-x C-M-=

Karthik: John, could you increase the font size by a little bit? Because I'm imagining people watching this on YouTube. I'm going to end up having to do that a lot.

John: I don't know how to do it globally.

Karthik: I can tell you. Get your fingers warmed up because it's C-x C-M-=.

John: Yeah.

Sacha: And then—okay. Yes, all right.

John: Wow, look at that.

Karthik: And then you can just keep pressing equals to keep increasing it.

John: So which size? Is this size good?

Karthik: Yeah, this size. Sacha, do you think this is big enough?

Sacha: It looks great. It looks fine to me. Let's give it a try.

John: It's going to be a lot of text to blur out, Karthik.

Sacha: Do you want it bigger?

Karthik: I can draw big windows in the video editor.

3:47 Redacting information when sharing Org demos

Sacha: I guess someday we will fix this with some kind of redaction global minor mode that just replaces everything with plausible-sounding text. I have something like that locally—it just replaces email addresses and phone numbers and stuff. But yes.

John: I wonder if Sora could do that actually to the whole video.

Karthik: You would be sharing all your data.

John: Oh, that's true. Never mind.

Sacha: One thing that we were talking about before is you could also consider sharing the transcript and then either screenshots or short clips, because that way we can just focus on the parts that demonstrate the workflow that you've developed without necessarily having to edit every single frame to make sure that everything is always covered.

John: Sure. Yes.

4:41 Daily Org Mode workflow for handling 40,000 entries

Sacha: So thank you for joining me and Karthik. I'm looking forward to finding out about all the cool things that you've been developing in your Org-mode workflow over the past two years since our last conversation about Org and personal information management. So what does your daily Org workflow look like now?

John: I've actually evolved several different tools on top of Org-mode itself. Now that we're in 2026, I have just under 40,000 Org-mode entries in my system. About 9,700 of those are tasks, and right now there are something like 1,200 of those tasks that are open. So my whole system is meant now to help me pay attention to what I need to pay attention to, because there's too much information. I could never, even if I wanted to, review it all or even just browse through it all. So Org-mode has to be the one to really condense everything into lists that are appropriate for when I'm working and what I'm working on. That's kind of the object of all the different things that I have: how do I make use of this sea of data? And I'm sure there's a whole bunch of stuff in my life that isn't even captured yet. That's the real rub here— despite this complexity, it's not that there's too much complexity, it's that there's not even enough yet. There are still things that live only in my mind, and as much as I can, I try to move them from my mind into Org-mode. But then I need to have Org-mode show it to me when I need to see it. Otherwise, it's no better than my memory.

6:24 Outlines for Focus and Home

image from video 00:06:23.160John: When I first go into Emacs, I always see these two pages. I use Org-roam on top of Org-mode in order to move tasks and notes into separate Org-mode files and have those be interlinked and organized well. I interlink everything by ID, which is sort of an Org-roam philosophy, and I've taken that on. I come into the focus page on the left, which is all of the stuff that I want to focus on project-wise right now. Every heading should either be a link to the project that the focus is about, or if I go ahead and look at the body of one of these entries, it will be a column view that pulls in a report— basically an embedded report for that category. I use categories a lot, and I use projects a lot, where projects are hierarchies that contain tasks, and categories are just names that might cut across many, many tasks. I also use tags, but tags are a whole separate thing that doesn't have to do with this.

7:34 Column view

image from video 00:07:37.400John: So anyway, you can see here a column view— this is a special version of column view. It's based on org-ql in order for it to be fast enough, but this column view doesn't exist out in the wild. This is in my own private dot-emacs repository. I wrote an org-ql column view function. Of course, it's very customized to my data format, but I have a :who field, and if you put a word here, that means if this shows up as either a category or a tag, then include that item in the report. Then I want it to sort by column three, and column three here ends up being the tags. That way I can see things sorted by tag. Anyway, each of these is linked to the corresponding issue by identity. So if I just do C-c C-o, then it will take me over to that item in that Org-mode file—that Org-roam file, sorry. So that's the purpose of the focus file: I can have a 10,000-foot overview of what I'm currently working on and what I want to focus on.

8:44 Home is a collection of links

image from video 00:08:45.680John: Then on the right is the homepage. This homepage basically is a collection of links, kind of like Linux utilities such as Glances or Cockpit— something that allows you to have one page that you jump to all kinds of different things from. This is my jumping-off page to a bunch of other pages that themselves serve as indexes within my Org-mode/Org-roam repository. This way I don't have to remember, "Oh yeah, how did I get to such-and-such a project?" I can just look in—it's a bookmarks list, but it's a meta-bookmarks list.

9:22 C-c C-o opens one or all links in the Org Mode entry

image from video 00:09:32.720John: The other nice thing about Org-mode is that if you're on an entry that has links inside the entry like this one does, and you do C-c C-o, it'll show you all of the links, and then you can hit return to open them all.

9:38 Example: Claude settings.json on multiple machines

image from video 00:09:44.800John: This way, what I do in the homepage is, I have a whole bunch of settings files for Claude because I use Claude in a lot of different places. I have different accounts and I have different machines and different accounts on those machines. Sometimes I need to make an edit to every single one of those files but I don't want to have to remember how to open up each one. Even an Emacs bookmark wouldn't be quick enough, but if I have the whole list of links as I do here in my Org-mode file, then I can just C-c C-o RET and it will open all of these in my browser at once. I do this a lot to open sets of pages when I need to do bulk editing in the browser or even in Emacs because, as you know, with the right add-ons, Org-mode links can be empowered to open all kinds of different things. I have Org-mode links that jump to Magit status pages for different projects in Git. I have ones that open dired buffers. I have ones that open Gnus, all kinds of different apps. I navigate to these through Org-mode links. In this way, Org-mode becomes the master dashboard of my information ecology. Yeah, so... go ahead.

Sacha: No, no. So to recapitulate: instead of using a gazillion agendas where you have to remember the keystrokes to open each custom agenda, you use Org-mode outlines in your focus and your homepage that have links to or reports for the different things that you're focusing on.

John: Yes, but not instead of. I also use tons of Org-mode agenda links as well. But Org-mode agenda links have a very specific focus. Usually when I start the day, when I start Emacs, I come to these two pages. I've set it up so that after startup, it always shows me these two pages. But usually the first thing I do is hit C-c a a to go to my agenda for today.

11:41 org-super-agenda is divided into topical categories

image from video 00:11:40.720John: I use org-super-agenda to divide this into categories— not category categories, but topical categories— so that I can see things segregated by which are the high-priority items, which are the things that are currently in progress, and then if they have a context where they need to happen (phone calls, errands, blah blah blah), then I see them all. This is just one of the agenda reports I use; this is the daily report. If I look at my agenda here, you can see I have the standard agenda reports, but then I have subcategories. I have a whole set of org-ql-powered agenda queries. If I look at these, I can look at all my open-source tasks, all my work tasks.

Sacha: Your screen is shifted sideways for me. I'm not really sure.

Karthik: Same here. So it's a little bit on the left. You may need to reshare your screen.

John: That's weird. I've never seen that happen before.

Sacha: So you've got your agenda.

John: I'm sharing a process window.

Sacha: Okay, here we are. Yes, I see it now. Okay, so these are the other things you've got.

12:54 Org-ql filters tasks

image from video 00:12:55.920John: These are basically queries that I have created for org-ql, and then the tasks-for that mirrors that org-ql column view "who tag" where I can give something that will match either a category or a tag,

image from video 00:13:12.240John: and it will show me here in the org-agenda report the same information it would have shown in that org-ql column view report.

13:20 Other agenda reports help review tasks

image from video 00:13:26.760John: I also have different reports in the agenda for reviewing my tasks, and I'll have to come back to that because that's a big part of this whole process as well.

13:29 Custom reports identify things that need to be filed

image from video 00:13:29.200John: Then at the bottom, you can see I have other custom reports for seeing things that haven't been filed— they're in my inbox, they need to be filed away.

13:37 I keep files in a flat directory and use search instead of categorizing files

John: I do like to file tasks in the file system. I've lost the battle of introducing any kind of structure because I just have too many files. My database on functional programming alone now has over 10,000 PDFs in it. I just can't categorize those. So they're all in one flat directory, and I search for them by using AI and by keyword search, because that's all I really can do. But for Org-mode, I haven't given up on having a hierarchy to my data. That's still helpful.

14:14 Make meaningful distinctions

John: By the way, we're going to talk philosophy in a little bit, but I will say that all the decisions I make about which reports to create, which tags to create, which categories to create, are driven by a philosophical principle which I call meaningful distinctions. We all interact with basically an uncountably large sea of information, and part of our job as knowledge workers is to impose criteria on that information so that we can make distinctions and say, "Okay, this relates to this, this relates to this." But there's all kinds of stuff we could be doing in making those distinctions, and not all of them are meaningful. Sometimes we spend energy— and all maintaining distinctions takes energy, because first you have to do it and then you have to maintain it. So it's very, very important to economize the work you spend making distinctions. I economize by trying to answer the question: is this distinction meaningful? What makes a distinction meaningful to me is that I use it in some way. It either has to help me in my job of maintaining focus on the appropriate information, or it has to help me with finding information. If it's not doing one of those two things, there's no reason to have the distinction. Even if a distinction screams out loud, "Hey, there's a distinction between your wife and your mother-in-law!"— does it matter that I draw that distinction? Probably not in most cases. So I have a "family" group rather than distinguishing those two people. Do I need to distinguish between all my co-workers? Probably—those are all distinctions that have some kind of meaning, but they're not meaningful to the purpose for which I use Org-mode, which is to assist my focus. So it's really important to know what you want Org-mode to do for you in order to make good use of Org-mode, especially at scale.

16:08 Color indicates agenda category

image from video 00:16:18.040Sacha: I noticed one of the things that's changed since I last saw your agenda is now you're using color to make a lot of things more salient. So how do the colors in your agenda kind of build on the distinctions that you're making?

John: The colors here are based on category. I also show the category in words on the left because many categories share colors. Everything that's personal is one color, everything that's work-related is another color, everything that's family, faith, open-source-related— they each have their own color. That way, when I look at the agenda, I see whether a color is dominating, or it draws my attention. If I see colors that I know are work, I will see if those need rescheduling into the next day first.

17:00 Simplifying meaningful distinctions

Sacha: All right. And it looks like the rest of your agenda is fairly light in terms of showing those meaningful distinctions. So I guess you're just using the filtering of the different agendas and other blocks in your outline to do that kind of filtering of things based on the distinctions you've already made.

John: Yeah. I do still have lots of distinctions, but I have now narrowed it down. I used to have more, and then when I realized this concept, I started getting rid of as many of them as I could possibly get rid of. Otherwise it's just too—if you have to sit down and spend hours combing your data, it's just never going to happen. It's going to end up piling up on you and you'll never get to it. So this has to be a manageable amount of distinction, and every single one of those distinctions has to have precious value in helping me manage my focus.

18:01 Capturing a task

Sacha: Is it possible for you to walk us through an example of when you're capturing something and any structure you have to support making those distinctions?

John: Usually I'd capture everything into drafts. So if I want to create an item, I use org-capture. Actually, I capture it in two ways, so I'll show you both of them.

John: For capturing a task—

Karthik: Sorry, John, before that, Sacha, do you see the window having shifted again?

Sacha: It's doing the thing. I don't like seeing the thing.

John: It's not moving at all over here. So this has to be something wrong happening with the Zoom client.

Sacha: I don't know.

John: We're just going to have to keep resharing it at intervals. Because nothing is moving here.

Sacha: What if we don't make it full screen and we just make it slightly... Are you sharing the window or are you sharing the screen?

John: I'm sharing the window.

Sacha: Okay. You might consider sharing the whole screen if that's not too weird.

John: I prefer not to, only because lots of notifications pop up.

Sacha: Oh, yes. Okay, that makes sense too.

John: And then we would have to blur those out as well.

Sacha: Okay, then. All right.

John: What I can try to do is share the window as a full-screen window.

Sacha: Oh yeah, okay, well, we can work with that. Probably in post we can just—oh yeah, oh look at that, it's zooming in.

Karthik: Okay. This is perfect.

Sacha: We were talking about how...

Sacha: Let's say you're creating a new task. Do you just type in the text and all that stuff by hand? Or do you have something to help you make those distinctions?

image from video 00:19:48.920John: What I do is hit M-m, because that wasn't being used for anything else, and that pops up a capture template for what I want to add. So let's add a TODO. I'm going to say "send an email to Sacha."

image from video 00:19:58.280John: I try not to do any of that organization of the task at capture time. The idea with capture is that it gets onto paper as quickly as humanly possible. So I just hit C-c C-c, and it writes it out.

image from video 00:20:21.800John: What will happen, because this is now in my drafts, is that it will appear at the top of my items needing review, and it will be in this very bright fuchsia-color background, which is extremely loud and always says to me, "You have items in your drafts that need reviewing."

image from video 00:20:41.520John: So I go to the item—here I'm in my drafts.org file, which has an inbox and a collection of drafts. (That's the other thing I'm going to tell you about.) Now I look at, "Oh, send..." I don't like using filler words. I actually have a report that finds entries that have filler words. So I always get rid of "a" and "the," stuff like that. So: "Send email to Sacha."

21:02 Task metadata is mostly automatic

image from video 00:21:03.920John: It comes with a lot of metadata. All of my tasks get a lot of metadata to start out. They get a notion of when I need to next review them. They always get a unique identifier— everything has a unique identifier, every entry, every file. They get the created timestamp, the GPS location of where I was when I created the task. The task itself is hashed so that I can know in the future when it changes, for a reason I will also explain shortly. The modified date is the last time the hash was updated, and that way I know...

Sacha: I'm curious, what do you use GPS for?

John: It's just information. Why not collect it? Any information I can collect that requires zero energy from me, I collect.

Sacha: Okay, fair, fair. All right. So you're saying hash. Okay, you got a hash.

21:55 Task hash detects modifications

John: So I have a hash of the content, which includes all the property values other than the hash, of course, and the title and the body and all that kind of stuff. It just lets me know when the item has changed. I have a procedure in my Git pre-commit hook which will ensure that the hash is correct, and the hash updates whenever the file is saved. This is all being done by a module I wrote called org-hash, and org-hash will do this all for you.

22:24 Categorizing tasks

image from video 00:22:32.200John: So now what I'll do is—this doesn't really need much categorization. I will just put it under "friends," so I just file it under friends. I didn't give it a scheduled date, so that'll appear in my review list rather than any daily agenda. This one is a reminder to me to download an AI model once the quantizations are available for my machine, so I'll just put that into the AI category.

22:56 Daily reports should be short; reschedule or unschedule aggressively

image from video 00:22:59.720John: Now my inbox is empty and I can go back to my daily report. This daily report, by the way, is too long. In general, daily reports don't work unless they're less than ten items long. I haven't done it yet today, but I try to aggressively reschedule or unschedule things that don't fit within ten items in a day, because I just will never do more than ten items in a day. Like, I talked with my wife about this item, so I'm just going to move that to the next day so I can revisit it. These work items are going to have to go to Monday now. I'll just get them out. I reschedule or unschedule pretty aggressively.

Sacha: Okay, so what I'm hearing is: you capture it very quickly, every so often you look over the bright fuchsia items in your review inbox, and say, "Okay, let me refile that to the categories"— the files, basically, or actually the places in your outline where they make sense. Then you have this list shown to you properly categorized now, but then you reschedule things until things look more manageable.

John: Yeah, the ideal scenario is that on a given day, Org-mode is showing me ten or fewer items, and those ten items are the things I should be thinking about and doing on that day. That is not the case right now, which means I need to curate this. But that is the objective function for this entire system. If the system can do that, it is succeeding. So I just work as hard as I can to get the system to approximate that behavior.

Sacha: Last time we talked, in 2024, you waxed nostalgic about LlamaGraphics Life Balance and some kind of automatic prioritization. You also briefly mentioned that you use AI sometimes to do sorting and reviewing. Is your workflow for prioritization still manual, or have you found something that works to help you?

John: I did start writing an org-balance thing— you and I talked about it a while back— I have not continued that. I don't try to do automated balancing. I just look at the daily list and pick and choose. That generally ends up being what happens. As far as the task management in an AI-friendly way, we'll have to come back to that because that's another thing I want to show you. I don't think, Karthik, that 30 minutes is going to be anywhere near enough to even just show you the outline of how it is a system.

Sacha: Yeah. That's why we have ongoing conversations.

John: Yeah, this will just be the 10,000-foot overview for today.

25:41 Note from Karthik: John is an old-school Org user

Karthik: If I may jump in, I'm thinking about this from the perspective of someone who knows what Org-mode is and maybe has used it once or twice and is now watching this on a video hosting platform and is going, "What? What's happening here?" So can we zoom out a bit? Maybe I should start by saying that John is an old-school Org user. I mean, he's also new-school as we'll see when we get to the AI stuff. But John is a veteran Org user who's been using it for 20-plus years, if I'm not wrong. There are functions in Emacs Org-mode that are named after John, or at least mention John in their docstring as the reason for their existence. This is what you're looking at, and what you were probably confused by, dear viewer, is the result of 20 years of using Org and organizing your life with Org and then optimizing and improving the workflow incrementally. This is kind of where you end up. Okay, maybe that should go in the intro, but now it's fine.

27:04 Continuing with the lifecycle: capture, file, schedule, review

image from video 00:27:44.520Karthik: Maybe we can continue with the capture example that you were showing—the typical lifecycle of a piece of information, right? It starts as a capture and then shows up in the agenda. Well, it shows up in the agenda very brightly saying "pay attention to me," and then eventually gets filed somewhere. Let's continue from there. What happens next? What happens to the email that you haven't written to Sacha?

John: Well, either I schedule the task at that time and then it's going to appear in one of these daily agendas, or— as I'm looking at my agenda here, you'll see I have my items for today all grouped out by org-super-agenda.

27:48 Habits

image from video 00:27:48.680John: Then I have habits, which is a different type of task where I'm trying to track consistency rather than completion. I use habits a lot, especially because I really love the book Atomic Habits. It makes some really good arguments about the importance of behaviors and processes to getting things really done.

28:09 org-review and items needing review; randomization

image from video 00:28:09.160John: Then at the bottom of every agenda view is this section, "items needing review." I have close to a thousand items currently needing review. I can't review them— that's too many to review. Even if I just looked at the work ones, it would still be too many to review. So what I choose to do instead is that every time I refresh my agenda view, it picks 38 unreviewed or unscheduled items at random and puts them here in this list. What I do is I just sort of scan it to say, "Is there anything in this list that really I should be doing? Should I be scheduling something?" I don't even try to read the whole list because that is too much. I just cherry-pick. I say, "Oh, is there anything? Oh, look at this— 'set up active voice chat with OpenClaw.' Well, that's going to be priority C, that's not very important." Or this one, "front braking on my bike is not very responsive." You know what? I don't really care about that right now, so I'm going to tell it to show it to me for review later in the summer. I have a bunch of key bindings behind the key r, which allow me to change when it will be reviewed by.

Karthik: Okay, so to clarify, this idea of a review is different from anything that Org provides you out of the box, right? This is not scheduling the task to be completed at a certain time. This is not a timestamp in the sense of an event in a calendar. This is not a deadline. This is like a different concept that means show this to me after this time. Is that correct?

29:49 org-review task properties

image from video 00:29:55.000John: Yes. If we look at the task itself, we will see in the properties for that task it has these three properties: LAST​_​REVIEW, NEXT​_​REVIEW, and REVIEWS. These are added by the org-review package, which is an add-on to Org-mode that's not part of the stock distribution. LAST_REVIEW is the last time it was reviewed, of course. NEXT_REVIEW is the next time I want to see it in the reports that gather tasks to be reviewed. That's the only time that date ever comes into play. If that date NEXT_REVIEW is in the future, it won't show up in those 38 items that are randomly selected underneath my tasks. So if I've reviewed every task and they're all in the future, then that list at the bottom of my to-do list would be empty. It has been empty sometimes when I'm on vacation and I have the time to actually review tasks. REVIEWS is something I added to keep track of how many times I have pushed that into the future, because then I can create an agenda report for the redheaded stepchildren in my Org-mode database— who are the people that are just keep getting pushed and pushed and pushed and never getting any attention?

Karthik: Okay, so this is like a whole subsystem that you added to Org to handle the kinds of tasks that are maybe important but not urgent?

John: Right. Because there has to be a midway between "these are the focus tasks for today" and "these are all the guys that aren't scheduled." There has to be something in between those two, because the gap is too large.

Sacha: All right, so instead of just relying on scheduling, you use reviews to give you that extra level of "I want this to come back on my radar every so often." You will actually have a randomized subset of these things to come back on your radar every so often, but you're not necessarily scheduling it for that day, so that your scheduled tasks still focus on your priorities. Right.

31:48 It's all just plain text

John: Now, at the end of the day, Org-mode files are just text files. That's one of the beauties of the whole system: they're really nothing special. Because it's Org-roam on top of Org-mode, I have many files. I have hundreds and hundreds of Org-mode files. Many of them contain tasks, many of them do not. I use some advice from the internet on how to optimize the collection of tasks for the agenda so that it only opens the files that have tasks in them. That way it stays nice and responsive, even though the number of files is constantly growing. The reason I use separate Org-mode files is, yes, I do separate by topic sometimes— I have a file for work and a file for personal— but I also like to have an individual file for every meeting. At work, if I have a one-on-one meeting, a team meeting, a project meeting, an all-hands meeting, I will create a file for that meeting. I will put into it who the attendees are and what was the agenda. I will have an AI note-taker (usually Gemini or Fireflies or something like that) on during that meeting, so that I can include the summary, the action items, and the transcript from the AI note-taker inside that file when the meeting is done. I also have a notes section where I collect all of the Org-mode tasks and notes. I don't use the drafts mechanism to capture in those instances; I capture them directly into the file.

33:18 Capturing to the current point with M-0

image from video 00:33:21.560John: One of the nice things about Org-mode is that when I hit M-m to select the capture template, that's going to go into my drafts file. But if I hit M-0 M-m, then it's going to capture it where I currently am. So I could say M-0 M-m a,

image from video 00:33:35.640John: and then I could say "send email to Sacha" again— you're going to get lots of emails, Sacha—

image from video 00:33:42.040John: and as you can see, it's right here in the file where I was a moment ago. That's what I use to capture stuff into the one-on-one files. So I use M-m if I'm using the Org-mode capture, which is for individual items that either are in my current location or in the drafts.

33:59 A different set of Org Roam capture templates

image from video 00:34:01.640John: Then I use C-c M-m for a different set of capture templates, which are for Org-roam files. If I do C-c M-m, then I can go to my work templates, my Bahá'í templates. I could capture a note, which is an independent empty file. I could capture a blog for one of my two blogs.

34:18 Capturing by name

image from video 00:34:20.880John: So let's do work. I'll say "work," I'll say "O" for one-on-ones,

image from video 00:34:24.640John: I'll say "D" for names that begin with D,

image from video 00:34:27.680John: and I'll say "W" and it will be my manager. What that will do is now it pops me into an Org-roam file where all the metadata for this file has been set up: the right category, the right file tags, creation date, everything.

34:38 An after-save hook automatically renames the files

image from video 00:34:42.600John: I like to add the meeting time to the date. When I save the file, then there is an after-save hook that will automatically rename the file so it has the correct date and time in the filename.

34:54 org-ql in the template provides a meeting agenda

image from video 00:34:57.200John: Then you can see that it has pre-populated an empty org-ql column view,

image from video 00:34:59.920John: which I then hit C-c C-c on. So now, if the person I'm meeting with has not provided me with an agenda, I have an automatically constructed agenda for that person based on what I know I have to do for them in my Org-mode data. When I was a manager for several employees and would use Fireflies to capture all the action items from all of our meetings, this was how I followed up on all those action items with all the people. I had one-on-ones with every direct every week, and I would auto-populate agendas for those meetings from Org-mode. Then I would just go through the agenda with them and say, "What's the status? What's the status?" and just keep that moving forward. That ended up being a really nice system for making sure that all the action items we committed to were being completed.

35:49 The "Review by" argument for the column view block filters the tasks

image from video 00:35:52.000John: You can see here on this org-ql column view I have this :REVIEW_BY tag. That says, "If the NEXT_REVIEW date for the item is beyond this date, don't put it here. It doesn't belong in this agenda." That way, if a person says, "Oh, I'm working on this, but it's going to take me two months," I'll say, "Well, I'm going to check back with you in two months." Then I'll just set the NEXT_REVIEW for that item to be two months into the future.

Sacha: And then for the ones that are there that you're reviewing, as you're sitting in the meeting with them, you're opening up the other tasks in another window and updating your status?

John: Usually, yeah, I'll do this.

image from video 00:36:25.720John: I'll open it up and see whether there's any supporting information. There may be links to files, links to URLs, a description.

36:36 Copying from Slack to paste into tasks

John: I like to copy and paste from Slack into tasks. In fact, I have a whole pipeline for that. I use Slack through Firefox because Firefox has a "Copy as Org-mode" plugin. So what I do is I drag and drop over the entire conversation that I want to make a task out of. I hit Home on my keyboard, which uses Keyboard Maestro on my Mac to copy that as Org-mode, switch to the Emacs application, capture a task, insert the Org-mode Slack text that it just captured, and then run an Emacs Lisp function called org-fixup-slack, which will rewrite all of the links and all of the markup to match what I want in my Org-mode files. Then I ask Claude to review that content and synthesize a title for that to-do item for me. Then I will tag it with the person I need to work on this with— or I'll switch it from TODO to TASK, which means it's delegated to them, and then I'll tag it with their name, which means they're the ones working on it. I have to check in with them on our next one-on-one to see whether this was completed.

Sacha: All right, so that's how you get external information such as Slack conversations into your notes, formatted the way that you like to format them, and into your whole agenda review meeting process.

38:13 Using gptel and large language models to generate titles and identify tasks

John: Right. And Karthik is the author of the wonderful gptel Emacs interface— that's the interface whose API I use to do the title synthesizing. I do a lot of title synthesizing. I do not write Org-mode titles. I just paste in lots of information and then ask Claude to create the title for me, because I've taught it what kind of titles I like. In fact, I could do that right now. I could go into notes, I could create a TODO here. That didn't work.

image from video 00:38:46.320John: I could create a TODO— I'm using yasnippet to create the TODO. I have to fix my org-review today, so one second here while I make a task to fix that.

Sacha: Of course.

John: And that will be scheduled for today.

Sacha: That sounds like a good opportunity to copy the error message, paste it in, and demonstrate how Claude will do the thing for us.

John: Yeah, well, it might. I could do it this way.

image from video 00:39:12.640John: I type C-x c t — C-x c is my prefix for all things AI, and T is "make an Org-mode title." Actually, it's not using Claude, it's using my local AI. This is going to take a little bit longer. So while it does that, I'll tell you about other things. It can do it asynchronously, so I can come back here and tell you about this. So I'm here. What was I saying? What was I going to do in this file? I think I was just going to show you the creation of a TODO here.

Sacha: Yeah, we were talking about copying the information from somewhere else. You've got this big paste, and then you're getting the AI to give you a title that summarizes the action item.

40:03 after-save-hook adds a TODO file tag; the TODO file tag adds it to the list of org-agenda files

image from video 00:40:18.040John: Since I've created a TODO, and since I saved the file (which is what caused it to then create the hash), you'll see that it has added "TODO" to the file tags. This is how the system knows to add it to the set of org-agenda files, so that the org-agenda will now get the TODOs from this file as well.

Sacha: Okay, adding to the agenda. Gotcha. All right. Now, you mentioned the hash a few times. Go ahead.

40:33 Reviewing the task

Karthik: If I may jump in briefly, I feel like I'm trying to catch up to an F1 car on a bike. I want to go back to the flow of that one task about writing an email to Sacha very briefly. You said you filed it and then it shows up in the list of things to review, right? What happens from there is that either you schedule it, or you just do it at some point, or you push it forward into the future. Is that right?

John: Say that one more time?

Karthik: The task about writing an email to Sacha that you captured and then moved into the friends category— I want to know how the task actually gets done. How are you reminded of it two weeks from now, let's say?

John: Well, it'll have to show up down here in the tasks to be reviewed.

Karthik: Uh-huh.

John: But randomly.

Sacha: Let's pretend you're scheduling it for today, because it's very important you send me an email today. So if you want to refile it to find it again, we can schedule that and you can send it in. Because I think Karthik just really wants to see the task marked done.

Karthik: No, you don't even have to write an email to Sacha—

Sacha: Oh, you can.

Karthik: I mean, you can if you really want to.

42:04 Karthik: "Things I capture in Org never get done."

Karthik: So okay, I'll tell you the reason why I keep focusing on this. It's because everyone knows how to capture in Org, right? And then everyone also knows how to get a list of all TODOs. But things I capture in Org never get done, right? So I want to know what the mechanism is to force your hand into actually sitting down and writing this email. Of course, if it's urgent and there's a deadline, it gets done, right? But it's the things like this where, oh, it would be nice to write this email and then follow up on this.

Sacha: I think the problem, as you said earlier, the problem is the human.

42:40 Grazing through tasks

image from video 00:42:39.640John: Yeah, the truth is it may never happen, Karthik, but it will be here as an open task. As I have time in my life, I do graze through my tasks. I cut it in different ways to try to refresh myself on what hasn't been done, and there will come a day when I will see this task again.

Sacha: Okay, yeah. Anything that's got a time on it goes into the priority queue, and anything that would be nice to have goes into— someday it will show up in John's lottery of random tasks and then he'll be like, "You know, I'd rather do that task than all the other tasks. Let's do that one instead." Just a quick question, because we're coming up...

John: Oh, we haven't even really scratched the surface.

Sacha: I know.

John: There are major, major things underneath this that I haven't even mentioned yet.

Sacha: I can keep going if you can keep going. So up to you.

43:36 This is an advanced Org workflow

Karthik: Also, I know more than I'm letting on about John's Org workflow, right? I'm just trying to ease the viewer into it gently because— what's the goal here? I don't think that this presentation is for people newly introduced to Org-mode.

John: I just don't think you can. I don't think they want my system. This is too complex. What I'm hoping is that people who already use Org-mode will cherry-pick out ideas that could advance their system a tiny bit. But nobody should do everything that I do, because I can barely survive it. It's too much information.

Karthik: There's no way that I was expecting this to be a tutorial. Not at all. It's just that I want the viewer to come away from this knowing what's possible and understanding what you're actually doing. Right.

Sacha: This is definitely aspirational. This is the thing that helps people think, "Oh, you know what? That's possible. Now I can go look up org-review or think about writing validation functions for my Org-mode, before-save hooks or actually after-save, that rename my files." So they can look through it. They're not going to be able to just pull the whole thing from your repository and plop it into their system right off the bat, but they can look at it for ideas, seeing how it all comes together.

45:11 Drafts: write down the text and then decide what to do with it

image from video 00:45:15.480John: I mentioned that I was going to show in the drafts how I have an inbox

image from video 00:45:16.520John: and a draft section. I've just recently added a new thing to my workflow, which is, instead of collecting drafts here... So what is a draft? A lot of times when I'm going to capture,

image from video 00:45:28.320John: I hit M-m and I pick from the options of what I want to capture. That creates a template for that type of item. But sometimes that's too much thinking up front. I don't want to have to distinguish between a TODO or a note or a link or anything else. What I want is to just get the text out of my mind absolutely as fast as possible. For this purpose, I wrote org-drafts. I named it after a Mac application that I've been using for years that does exactly the same thing. Drafts lets you write first, act on the information later. In Drafts, the Mac app, you can write some text and then decide you want to send it as an email, send it as a message, send it as a WhatsApp—you know, but you shouldn't have to be thinking about the mechanism of action when you first just want to think about the creative act. I have changed it so that, instead of just M-m,

image from video 00:46:32.160John: if I do M-S-m, it creates a draft. It used to create the draft using the org-capture interface, and then it would go into that drafts subheading in my drafts.org file. But now what it does is it goes into my lab notebook.

46:47 My lab notebook collects notes and ideas throughout the day

John: So the lab notebook is where I want to collect notes and ideas throughout the day. I don't know that I want it to be in the lab notebook— the point is, I don't want to think about it. So when I do M-S-m, then what it does is it creates a draft instantly at the bottom of my lab notebook, and it puts me in the body of that draft to write it. So I could then say, "Send an email to Sacha." That's all I have to do. I don't have to hit C-c C-c. I don't even have to save the file. I could just go straight on to what I was doing. But the idea with org-drafts is that, once you have written a draft, now or at any time in the future, you're going to want to act on that draft in some way.

image from video 00:47:32.680John: So if I hit C-c C-c, it pops up a little submenu of things I can do with this draft.

image from video 00:47:41.800John: If I hit t, it'll take the first line of the body of the draft and turn it into a TODO with that line as the title of the TODO.

image from video 00:47:50.080John: If I hit C-c C-c c, it will copy the body of the draft onto my clipboard and then change the keyword of the draft to SCRAP. SCRAP is just a keyword that I use to identify drafts that I don't need to act on anymore but I like to keep the information. Maybe I want the information in the future.

image from video 00:48:11.920John: Another thing I can do with it: I could do C-S-c, and what that will do—you don't see it because it didn't show up there— but it starts up a webpage into Claude.ai and inserts the body of the draft as the text to submit for the prompt to that webpage. This is extensible. You can add new actions to this. I can draft an email with this. I don't have it set up to send Apple Messages, but I could. I could set up that interaction as well. This is now a preferable way for me to collect ideas and thoughts and notes throughout the day. So when I'm using Claude Code, which is what I'm generally using, and I write a really long prompt and I think, "I may want to use that prompt again in the future," I will copy and paste it and then make it a draft, and not do anything with it. Actually, I just turn it into SCRAP right away with another keybinding, but I want to retain it within my lab notebook so that I can use other forms of search, which I'm going to talk about in a moment, to try and recover that information and find it later.

49:17 org-jw: normalizing structured data

John: I said that Org-mode is text, and that is a strength of Org-mode. It is also a weakness of Org-mode, because really Org-mode is a database— but it is a super-unstructured database. I have spent more than a year now layering a system on top of Org-mode I call org-jw, simply because it is so specific to my workflow that I didn't think it would ever be of general use. What org-jw is, is a separate Haskell application that parses Org-mode files into an internal representation in memory of those files that's entirely complete. When I say "entirely complete," I mean that if I take that in-memory representation and write it back out as an Org-mode file, it is byte-for-byte identical to the Org-mode file that went in. It checks this property by doing a round trip. If the file fails the round trip, it raises an error saying your data is not conformant. I make this a pre-commit hook before anything can be committed to Git as a change to my Org-mode files, to ensure that if it's in Git, it is normalized. This fully normalized data now is accessible in new ways. I wrote a whole linting process that checks hundreds of different properties of my Org-mode data to make sure that those properties are always maintained. For example, if there is a URL property in an entry, it must have a "link" tag, and vice versa. Those two things have to go together, because the link tag is there to be a reminder to me that there is a URL and it would be meaningful to use C-c C-o on that entry. Likewise, I don't want it to have a link tag but not have a URL. The two always have to come together. There are a whole bunch of linting rules that I have, but they're all specific to me and how I like to use Org-mode.

51:17 Copying structured Org data into a PostgreSQL database

John: Another thing org-jw does, now that it has this internal memory representation, is it can write all of that data out to a highly relational database. So it's, I think, a third-normal-form or fourth-normal-form Postgres database that collects all of the information. When I say third normal form, I mean that tags are in their own table, categories are in their own table—every little piece of information is in its own table. Then there are correlating tables. So each entry is identified by its UUID, and then the tag knows what entry it goes with by basically referencing that UUID. All the data is interlinked like this. In addition to storing the data in this database—and this is the text of every property, the text of every note, the text of all the body paragraphs, everything is in there—because all of that data is now so nicely structured within the database, Postgres has the ability to do vector search. Anyone that's been playing in the AI space knows about vector search. When I use org-jw to reflect all entries that have recently changed back into the Postgres database, I also use an AI model to calculate a vector embedding for every piece of text that's related to every entry. These get stored as vectors in Postgres, so that later I can ask the database, "Show me entries that are related to a leak that I'm searching for in my pool." It will search for that semantically within the database, and it will find me everything that is in the area of pools and leaks and all of that kind of thing. This is very effective. I can do it on the command line with org-db-search—that’s the name of the command that I run to do this.

53:12 OpenClaw enables conversations with the data

John: But I also taught OpenClaw how to use this tool. So OpenClaw is an AI agent that will do work on your behalf. I have it linked up to a local LLM that I run, and I get to talk to it over Discord. It's really locked down— I have it all bound within its own Linux microVM so that it can't really do anything but the few capabilities I've given it. But what it means is that, when I'm on the airplane, I can use my phone and my WhatsApp or my Discord app, and I can say, "What tasks do I have coming up in the next week that involve calling somebody?" Then it can do both a full-text search, a structural search for tasks, and also a vector embedding search to find similarity. So I could ask OpenClaw, "Do I have anything coming up having to do with Christmas ornaments?" and it will find for me the items that deal with Christmas ornaments. What this has all done, by starting with Org-mode and then creating basically what I call "strict Org-mode" or "structured Org-mode," from that creating a database, from that creating these vector embeddings, and then from that tying it into an AI agent, I now have the ability to dialogue with my data. I can ask my whole entire Org-mode sea of data questions like, "What problems at work have I not been paying much attention to lately?" and it can give me an answer. Then I can query that answer and refine it further. I can say, "Well, do any of those have to do with this project?" or, "Do they seem like they're very high priority?" I can keep going until I get to a more refined set of items that represent a focus for the day, as an alternative to using the scheduled dates and the org-agenda reports. But I also have an org-agenda report which does a vector query. They all tie together, so I can do it from whatever direction. If I say, here, "org-semantic-search," and then say "go out for a coffee with Karthik," what it will do is run the org-db-search command-line tool and then feed the data into an org-agenda query.

Sacha: So I'm hearing: you've got your Org plain-text data, you make sure that it's all formatted nicely using your normalization functions, you take that and put it into a structured PostgreSQL database, which then allows you to do a full-text search as well as vector search. Then you take that and you hook an AI into it through something like OpenClaw, so then you can have conversations. Even when you're away from your computer, you can query it for stuff and do something with the results. Even just the semantic search is something I'm very interested in because it's hard to find things if you need exact word match, so this is great for being able to find stuff that's related to something without having to worry about making sure you've got the right words in it.

John: Right.

Karthik: This is like five levels of crazy, each building on another.

John: Yeah, it's all about making this data work for me. I've spent all this time collecting it—how can I make this data work for me? I don't know why the CLI search is not working. It might be some problem with the local model. So many changes. So many. Yeah, it's the demo phenomenon. Also, so many things change in my environment so fast that, unless I use things constantly, they easily break.

56:49 Avoid drift by using only one TODO system

image from video 00:56:54.200Sacha: Which actually is an interesting thing that touches on the conversation that Karthik and I were having before you joined. How do you keep the ideal of your tasks or whatever synchronized with the reality of your current focus, or the things that have broken or changed?

Karthik: Yeah, it's the drift. I call it the drift problem, where the state of your life—well, the portion of your life that you want to capture in this case in Org—has drifted from what's actually happening. Then when you look at your agenda, you go, "No, this is not relevant to me anymore. The things that are bothering me right now are not here." So it's like the Venn diagrams—I mean, the two blobs—the overlap is getting smaller and smaller. So is that a problem you have, and if you do, how do you deal with it?

John: I deal with it by not using any other systems but Org-mode. So you have to always be in Org-mode, seeing that data somehow. You have to just be interacting with it to combat that drift constantly. You can't have even two to-do lists. For me, even just one other to-do list makes the drift unmanageable. So if somebody puts something for me on another to-do list, I'll make an Org-mode task and I'll put a link to that to-do list. It has to be in Org-mode.

Sacha: Do you sometimes find yourself— go ahead.

Karthik: What about the to-do list in your mind?

John: I don't keep a to-do list in my mind.

Karthik: You don't have a to-do list in your mind.

Sacha: I was just saying, as we saw earlier, if you do come across—if there's a thought and it's not in your current review list and it's not in your current agenda, your semantic search and your org-ql will help you find that thing relatively quickly, I think.

John: Yeah, if it's in the Org-mode file anywhere, a combination of either ripgrep or Postgres full-text search or semantic search, there will be a way to find it. Then if it's not in there, I just use the capture interface to put it in there.

58:59 Capturing with Drafts on the Mac or Apple Watch

John: Oh, by the way, I also should mention how I capture. I've been showing you Org-mode commands to capture, but that's not actually how I typically capture tasks. I use the Drafts app on macOS, as I mentioned, and I have it set up so that in the Drafts app, if I hit C-c, it will write the item that I captured in Drafts to a file that Org-mode will turn into a task the next time I run any agenda command. So I have something that's the after-agenda, the before-agenda setup hook. Let me do that. I'll go over to my Mac and I'll say, "Send an email to Sacha." I'll then submit it. It goes to iCloud Drive, gets synced to all my machines.

image from video 00:59:45.080John: Now I'll regenerate my Org-mode agenda. And now here it is—this fuchsia guy just came in from the Mac Drafts app. The Drafts app on the Mac runs on my phone and it runs on my watch. So what I do to create tasks is I tell my watch— I just talk to Siri and I say, "Remind me to do such-and-such." Then the Drafts app knows to automatically suck in anything that's on my Reminders list, and that ends up becoming something that's auto-sucked into Org-mode. This way I use voice to capture things as they happen wherever I am. I don't have to have my computer. I don't even have to have my phone. I just need my watch at the very least, and I'm capturing tasks by voice like this, dozens of them a day. This is probably the most frequent way that I do it. Oh, and also, I taught OpenClaw how to add stuff to Drafts. So I can also ask OpenClaw via Discord, "Hey, remind me to do such-and-such." But why do that? I just did it as an alternate way. I prefer to do it with my watch.

Sacha: Yeah, just because you can.

1:01:01 A foot pedal makes speech-to-text even more convenient; whisperflow, handy

Sacha: As I was mentioning, voice computing—just speech-to-text— is very interesting because it allows you to capture your thoughts so much faster than typing. So I was wondering if you're using any of that in your drafts, not just for quick tasks that you're capturing from your phone, but when you're at your computer, are you using that at all yet?

John: Yes. In fact, I am using it so much that I bought a foot pedal for myself.

Sacha: Yeah?

John: Yeah. I use two different apps. I use one that's commercial called WhisperFlow that sends your voice out to the internet and uses their AI models. First they transcribe it, and then they process it— they remove all the uhs and the ums and they rewrite it into correct grammar and all that kind of stuff. That's extremely fast, so usually when I'm talking to Claude and prompting it, I'm prompting it by voice using my foot pedal. That's the right pedal. The left pedal uses a free open-source app called Handy. Handy uses a local transcription model, which I use Whisper Transcribe for, because it's super fast and super accurate. Then I use Qwen 3.5 9-billion-parameter as my local LLM model to post-process the transcription, so that it does the same things that WhisperFlow does. It can accomplish everything WhisperFlow does; it just takes about ten times as long to do it. Sometimes I don't like that delay in the flow. But it's also entirely open source and entirely local, so I know that whatever I'm transcribing, none of that data is going out to any third-party services.

Sacha: Fantastic. Okay, so that gets you— when you're brain-dumping a whole bunch of things into a prompt for an AI or into your notes before a meeting or whatever, you can just blurt a lot of information and the AI will clean it up nicely and make it more presentable.

John: Let's do this. "Send an email to Sacha." Now, it just finished transcribing. Transcribing only takes like half a second, but now it's processing. It may take longer the first time because that 9-billion-parameter model may not be currently loaded—I had to reboot my machine recently.

Karthik: Sacha, you're going to get so many emails.

Sacha: I don't know, it's fine. It's fine. I get emails sometimes. Go ahead.

John: Oh, there we go. So it said, "Send an email to Sacha." It nicely added a period at the end of it.

Sacha: Yeah.

John: Let's see. Now that it's warmed up, let's do a longer one. "This email should say something about Org-mode and it should be related to the conversation that we all had on our Zoom call together." You could hear that I had— I don't know if you heard me, but I had some uhs and ums in that.

image from video 01:03:54.960John: But this is what it came out with. The only thing it didn't know how to do was put a hyphen between "Org" and "mode." I have complete control with Handy over the post-processing prompt. In the post-processing prompt, I can give it an Emacs section and tell it about vocabulary that's special to Emacs that I want it to be aware of. I've done that for all of my work topics, but I haven't done it for the Emacs topics.

Sacha: I have something similar where I do the post-processing of the speech-recognition output in Emacs, so I can just run a list of functions on it, including fixing common errors and processing speech commands. So I'm gradually playing around with that too.

John: It's invaluable. The other nice thing about voice transcription is that you end up saying more than you type, because I think we just naturally economize— typing takes more energy. When I talk, I'm just a blabbermouth and I end up giving all this information that ends up being better for AI. In fact, they've shown that if you just add 40 more periods at the end of your sentence, that improves the quality of your response, because all of those additional tokens cause attention to be recomputed for the existing tokens many, many more times, and that computation helps it find its way through the feed-forward networks more accurately. So more text is actually better, especially when you're only talking about maybe 20% longer due to voice transcription. So I prefer to talk to AIs through voice. What I do is I like to create—let's see here— I like to create the body of the TODO using voice transcription, and then after that I like to create the title using AI as well. Now that it's warmed up, it should not take quite so long.

image from video 01:05:42.360John: There we go. We will just say to Qwen— that was Qwen 9B giving me the transcription— now we're going to use Qwen 27B to give me the title. It may not be warmed up as well. I try to keep four different models always loaded so that they'll be quick to respond, but as I said, I had to reboot the machine recently.

Sacha: All right. So you've got your outline for focus and your home, they have the blocks in it, you can use that to drill down into whatever subset of your tasks. You also have your agenda with the reviews. You can go into any of those tasks, use AI to fill out the text based on your speech recognition, or you can send it to Claude or other AI for executing the task, for all that stuff.

1:06:39 Looking to the future

image from video 01:06:53.281Sacha: If you're looking three to six months out, your workflow is pretty polished, but where do you want to take it? What are your future needs that you'd like to work towards?

John: I just need to be able to get these big old long lists shorter. That's all I want to say.

Sacha: Yeah, that is a getting-things-done thing, not a tooling thing, right?

John:

Yeah. Atomic Habits, I think, helped me more

than anything else in the way that I break up tasks.

1:07:07 John is running local models on a Mac Studio

John: I'll give you another AI example here. For some reason, Qwen is not working. Let me check my OMLX— I use OMLX these days for running.

Karthik: So you're running all these local models on a Mac Studio?

John: Yes.

Karthik: Okay.

John: Oh, it looks like OMLX might not be running. No, I have it.

Karthik: Okay. In the meantime, I had a question about the whole database mirror of your corpus.

John: Okay, sorry, one sec. The reason it wasn't working is because I'm also working on another AI-powered app for doing stock analysis, and it was taking over the port. So what was your question?

1:08:12 Using AI to facilitate getting data into PostgreSQL; structural limitations?

Karthik: Yeah, so the question was: you have a mirror of your Org corpus in Postgres, right? I was thinking, one of the reasons that people like Org-mode is because it's easier to work with as text, but also because it's flexible. So you can set up whatever system you want corresponding to whatever schema in the database. You can also change it easily because it's just text, right? But your Org subset specification, by your own admission, is rigid, right? You've settled on it for now, and that's why you can have a database that's like this with a one-to-one connection. What happens if you change something? Like if you change what a category means or if you add more things, you have to then update all the tables in the database and update the schema. You have to do a migration of the database.

John: Yeah, but I just have AI do that for me. I didn't create a schema. I didn't even create the code that did the Postgres migration. That was all written for me.

Karthik: I suspected that, yeah, that's probably what you do. You don't worry about it. That is neat, the idea that you can have all the flexibility of Org-mode—flexibility to change things, change the system as your needs change—and then also have the database do all the things it does, like very fast search, semantic search. You get all of that for free, if you have someone who can go and change the database when you change your Org system, if you have someone who can do that for you.

Sacha: It actually prompts me to think of this question I had about stuffing the Org entries into the database. Let's say, for example, you have an Org tree and you have sub-entries within it that you also want to be able to work with on a more granular basis. If you have subheadings, how do you like to put it in your database? Do you have a copy of everything, and then each sub-entry has its own entry as well, and then each subheading under that has its own row in the database? Or are you only working with the most granular ones?

John: I don't even know, actually. how the AI decided to organize it.

Sacha: Okay, yeah. So basically, when you do a search, for example, can you find something where the keywords are in multiple subtrees of a larger entry?

John: I could do a join and find that information, because every entry knows what its parent entry is, and it could find out what the keywords for those parents and for any siblings would be as well.

Sacha: Interesting.

1:11:04 Habits are better than goals

John: All the data is there. I was going to mention, by the way, that I was talking about Atomic Habits and how much that has affected the way I break tasks down. I try to get them to be smaller, and I also try to rely more on habits than on goals to accomplish things.

1:11:24 Breaking down tasks with a large language model

image from video 01:11:33.000John: So another thing that I built up in Org-mode using gptel is— let me find an entry that's long enough for this to work. Okay, so here we have a relatively long entry. It's a note here, and it's about bridging non-switching ports on an OPNsense router. Let's say that this was something I actually had to do now.

image from video 01:11:46.240John: I have this C-x c prefix that I use for talking to AI. One of the things that it can do is called a "task breakdown." So I do C-x c, and there are two of them: capital B and capital T. Capital B is designed to take something that is already a task and identify what the component tasks would be, probably, that need to be done and in what order to get this thing done.

1:12:13 Inferring tasks with a large language model

John: Capital T is "you've just got a sea of text— infer the tasks that are implied by that text." I wrote that one because sometimes when you use these voice transcriptions of meetings, all I have is the transcript, and I want to know, well, what were the action items? So I wrote "infer tasks" to get the action items. Let's infer the tasks out of this OPNsense router. So I will run that. I'm using now the Claude Sonnet model— I think I'm using the Claude Sonnet model. We're about to find out. Let me actually see which model that uses.

image from video 01:12:52.480John: So that uses the "infer tasks" preset. The infer tasks—oh yeah, it uses the Sonnet model. I do not know—oh, maybe I have to block out the text. I mean, select the region. There we go. So what that did—you know, for some reason Claude was changed recently and it does not work for these types of tasks anymore.

image from video 01:13:31.680John: "No actionable tasks identified in this text." Let's go somewhere else. Let's go to a work meeting. This is my directory that has all the meetings in it. Let's find one that has a transcript.

image from video 01:13:49.841John: This one has a transcript. So we're just going to select the whole transcript, and the transcript got sent to Claude. We'll see what it comes up with.

Sacha: All right. So AI help for identifying the tasks and also asking you questions, I guess, to break down tasks.

John: It doesn't ask me questions.

image from video 01:14:17.200John: So these are the seven tasks that it inferred from the body of the transcript. It even identified that two of them were tasks to be done by the person that I met with, and the other five are the ones to be done by me. They have descriptive text, possibly, showing the context that it used to determine that that text should be there.

image from video 01:14:41.480John: It even has the time codes as a property, so I can go back to where in the transcript it got that task from. That ends up being extremely valuable, especially when I'm dealing with— like I said—a sea of information, and I want to pick from that sea which fish I intend to catch. I've been using AI with gptel and Org-mode more and more connectedly to try and help me manage—not just manage that sea of information, not just search it and categorize it, but also refine it, break things down into more manageable pieces, help me find which things I should work on today. All of that is coming together rather nicely to help combat the drift problem that Karthik was talking about.

Sacha: And it's so easy to work with because it's all text, and you can just give it that along with your prompts. Do you share your prompts anywhere?

1:15:35 johnw/prompt-deploy has the prompts

John: My prompts are all on GitHub. There's a project that I wrote called Prompt Deploy, and you'll find all of my prompts in there. Prompt Deploy is both a set of prompts, agents, skills, MCP tools, and models, and then a Python script that will deploy it to all of your agentic frameworks. It knows about Factories, Droid, OpenCode, Claude Code, Codex, Gemini—and it knows how to rewrite that information so that the prompt is usable in every one of these for every machine that you use all of these things on. When I do a Prompt Deploy, it writes out 896 different files to all of these different tools in all of their different locations. Because I only want to define the prompt once.

Sacha: I'm looking forward to having a good poke around. Now, interestingly, in our last conversation, I think one of the ideas was to then take the transcript of that conversation and see if a meeting summarizer could do something with it. I don't think we actually went through with that last time in 2024, but maybe this year it'll do a better job of pulling out the key topics. I will still probably edit the transcript manually, because transcripts are fun. Yeah. But okay—

1:17:02 Summarizing our conversation so far

Sacha: Today, the conversation was mainly about your kind of Org task lifecycle, including using AI to help you refine the tasks further. Karthik, do you have anything that you want to dig into a little bit more?

Karthik: I don't think we'll have time to discuss philosophy today—the philosophy of task management. But I do want to say, I think we covered how things go into the system and then how things surface, right? Things go into the system using manual captures, using voice or whatever, or they're created by LLMs. Then things come out of the system or are surfaced using the various agenda views, right? I hope that that's a somewhat complete picture of just the ins and outs—tasks go in and then they show up in various ways. There's a lot to take in. I knew some of this, but this is still too much. I've been using Org for nearly 20 years and I can't even wrap my head around it. It's easy to think of these as cool things that might be possible, but to actually see them being used— and usefully—there's a huge gap between the two. The fact that John actually uses this productively is pretty crazy to me.

Sacha: I think we can break it down into a couple of practices that people might be interested in experimenting with. Like, for example, that idea of using those kinds of focus or home dashboards—an Org file where everything is organized by your hierarchy of importance, and includes the dynamic blocks to list the actual tasks for that section, or the links that you can open with C-c C-o.

1:19:13 Karthik's completing-read version of C-c C-o

Sacha: Karthik, did you know that C-c C-o could open all the links, Because I did not.

Karthik: Yeah.

Sacha: So let's go.

Karthik: I knew that, and I also wrote— I didn't like the way Org does it, so I wrote a completing-read version of that.

Sacha: Oh, very cool.

Karthik: That's what I use, and I use it from the agenda. So if I do C-c C-o in the agenda, it shows me a completing-read interface with all the links in that entry. The reason I did this is because now I can do other things with the links. I can use Embark. I can export the links, just the links. You're not limited to just opening them.

1:19:56 Karthik's thoughts on the demo so far

Karthik: But anyway, it's just difficult for me to wrap my head around the fact that all of this comes together in a way where it shows you what you need to see when you need to see it. Yeah, it's like magic to me. I don't know how else to describe it. As proofs of concept, all of this makes sense— every individual feature. But the idea that it actually works—and then when you see it, you're not like, "Oh, I see what you're showing me, but that's not what I want to think about right now." The fact that that doesn't happen, or if it does happen, it's in a minimal way—it's pretty impressive.

Sacha: It's something that people will have to ease into as they try out different parts of this workflow. Things like using the org-capture, even just with the menu that you have, where it breaks it down into lots of levels—names starting from A to D, that sort of thing. Yeah, it's a lot of cool stuff, which we will try to write about and index, and maybe use screenshots for, because otherwise I think Karthik goes slightly crazy with the blurring.

Karthik: No, for the most part... The only challenging moments are when you were scrolling line by line, because that gets... But most of the time it was static, so it's easy to draw rectangles.

1:21:29 Categories are subject areas

Karthik: There's one other thing I wanted to ask you, which is how you break down... So Org has a few affordances. It has TODO keywords, categories, properties, and tags, right? I just wanted to know what meanings—you're free to assign whatever meanings you want to all of these, right? There are different ways to break this down. I just wanted to know, for the record, how do you think about each of these things?

John: For me, categories are subject areas. So if I want to see everything that I currently have in my TODOs related to AI— because those are usually fun tasks— then anything that is in the AI category, or under the AI category (one of the subcategories), I can just do a category report for AI and I will see all of those tasks. Sometimes when I have an evening and I just want to play, that's the report I'll do.

Karthik: But sorry, categories are mutually exclusive though, right? When you assign a category, it owns that task.

John: AI is a child of the Computer category, which is a child of the Personal category. So they're hierarchical.

Karthik: Oh, categories can have children?

John: Yes.

Sacha: Because you can specify it as a property.

John: So an entry has multiple categories— it's every parent that it has up the hierarchy.

1:22:58 TODO keywords use a fixed vocabulary

image from video 01:23:25.080Karthik: Okay. The next is TODO keywords. I saw even HABIT was a TODO keyword for you, so I was wondering...

John: Oh yeah, I have a very fixed vocabulary of TODO keywords. If I share my screen again, the way that I manage my TODO keywords is that I have a dot-file. This dot-file defines my keyword hierarchy and how they're all related. The Org Haskell utility that I wrote that does the data normalization—this is how I tell it what my keywords are. I don't yet have Elisp code that also populates Emacs's Org-mode definitions based on this file, but I do it by hand— I guess because that data was older and it was already mostly there.

Karthik: And these are global across your whole corpus?

John: Yes, they're global. I don't ever use keywords outside of this set.

Karthik: Per-file keywords?

John: No, I don't use per-file keywords.

Sacha: Because you have your validation smacking it if it gets mistyped, for example.

John: Right.

Karthik: So to be clear, your TODO keywords represent the state of a task, right? Like, what state is it in? Hence the state machine right here, right?

John: Exactly.

Karthik: Okay. And the states include things like SCRAP or LINK. Why is LINK a state?

John: LINK is because it's a different kind of a note. A LINK is a note that only has a URL.

Sacha: So it doesn't require—yeah, your action is just look at the thing or do something with it.

John: Yeah, it has no body.

1:24:48 Tags are context: person, place, or thing needed for the task

image from video 01:25:17.160Karthik: The next question is tags. How do you think of tags in Org?

John: Tags for me are context. They indicate the person, place, or thing that has to be present in order for me to be able to make progress on that task.

Karthik: And how is that different from— oh, okay, I can see how it's different from the category, I guess. So what are some examples of tags you use? I understand people's names are tags, but what else do you have there?

John: I have "call" as a tag, so it has to be during the times of the day when I could make a phone call. I have "errand" as a tag—it has to be during a time of the day that things would be open and I could drive out of the house. I have tags for individual people, so that I have to be able to be in contact with them. Org-mode has a nice feature: if I hit \ RET, it will examine my environment based on the name-mask-list-at Emacs Lisp function that I wrote, and it will auto-filter out the tags that don't currently apply. So if I have a tag based on "call" and it's outside of the time of when it could happen—like at 7 AM— then it will filter all the call items out of my daily agenda. They're still in the daily agenda; they just won't appear until later when it is time to make calls.

Karthik: Ooh. Okay.

1:26:14 Priority: A means must do, C means optional

Karthik: So the next affordance is priority. I guess you use priority the normal way— just A, B, C—for importance.

John: Well, I only use A and C. I don't use B.

Karthik: Okay, so you only need two, I guess. So what does it mean if it doesn't have a priority? Is that the lowest?

John: No. If it doesn't have a priority, that's just... So to me it's "must," "should," and "optional"—those are the meanings of the three priorities. "Must" means there is a consequence if you don't do it. "Should" means maybe you're missing out on an opportunity if you don't do it. But really, it's up to you. "Optional" means there's really no good or bad here; it's just an item you may want to do.

Karthik: So if it doesn't have an explicit priority, is it optional?

John: If it has no explicit priority, it's a "should."

Karthik: Okay.

John: I assume that if I made a TODO entry, it's something I should do. That's the assumption.

1:27:31 Properties are mostly automatically assigned metadata

image from video 01:27:49.120Sacha: Do you use properties for anything? Aside from the review property, of course, do you have any other custom properties you like?

John: Just the ones I showed after we did the capture, like the ID, the location, when it was created, when it was modified.

Sacha: Yeah, so those seem to be more automatically assigned.

John: I very rarely use manually assigned properties.

Sacha: Okay.

John: To me, properties are metadata, so I'd rather just never look at them and have the system automatically manage them.

Sacha: I don't think we actually touched on what you do with a hash. Did we talk about that one yet?

1:28:09 The hash

image from video 01:28:13.160John: The hash is how the database knows that an entry needs to be fully updated. If the hash has changed—the database has the hashes in it as well, and it has the last modification dates as well— first I do a search for all items whose modification date is different from the modification date in the Org-mode file, and then I check the hash values, because maybe I changed an item but I changed it back, and now the database doesn't need to be updated for that.

Sacha: Very cool. All right, so that's all the different Org features and how they support your workflow.

John: Yeah.

1:28:51 Assigning the tags when refiling

Karthik: Yeah, the one that's most interesting to me is tags, because I understand in the abstract what you mean by "a tag is a context or a resource that you need." But somehow I can never seem to implement it. It's not that I end up with a pile of tags that don't mean anything; it's that entries just don't get the tags that they need, so they don't show up later. Is there some way you ensure—when you capture, I know that you don't assign tags, you just want to get stuff in— at what point does it get assigned the right tags, and how?

John: Usually when I'm refiling it from the inbox into the appropriate category, into the appropriate heading.

Karthik: So do you do that by hand?

John: Yes, I always do that by hand. That's my main way of interacting with the task. I go to the inbox and then I give it the tags or the properties I want it to have.

Sacha: Are you using the standard Org completion for tags, or typing it in manually, or doing something to help you select from the gazillions of tags you probably have?

John: Yeah, I use—let's see, where is it in my Org-mode file here? I use the Org-mode feature... where are tags? Let me see where tags get defined, because I know I have the shortcut defined somewhere here in my Org-mode file. It says that "call" is the letter C.

Sacha: Oh, yeah.

John: I forget how Org defines that.

Sacha: That looks like that org-tag-persistent-alist there.

Karthik: That's C, yeah.

John: Yeah, but that's only a couple of tags.

1:30:47 Per-file tags and tag validation

image from video 01:31:01.040John: Tags are one thing where I do tend to use in-file. If I want to have shortcuts for tags, I tend to use—so I have some tags that are truly global, and then I have per-file tags because I want the tags to be related to the categories that I'm tagging. My org-jw Haskell utility recognizes a file property called tags-all. If that file property is set, it will mandate that the tag has to be within that vocabulary.

1:31:29 Starting tasks with a restricted set of verbs and validating them

image from video 01:31:23.540John: The same with verb-all. Verbs are words that can be at the beginning of an entry title that are followed by a colon.

image from video 01:31:51.760John: So I might have, instead of "send an email to Sacha," I might say "Reply: Sacha's email." Instead of saying "reply to," I just make it a command verb. That verb has to be within that constrained vocabulary.

Sacha: That's interesting. Do you use those verbs for further filtering?

John: Yeah, because then you can search. They stand out a little bit better because they represent actions that need to be taken, and it's very clear what the action is. You could also query for all of the items that have a certain verb type. Like if I'm sitting down to write a bunch of emails, I don't have a tag for writing email and I don't have a category for writing email, but if I were to search for all items that have a "Reply" verb, I would find all of the emails that I need to write today.

Karthik: Is that just a text search, or do you search for...?

John: It's just a text search. Yeah, because colons don't get used in any other way. That's also a constraint of the system: where other people would use colons, I just use em dashes.

Karthik: Okay. And if I understand your system right, you wouldn't tag this entry with "Sacha," right? Because you don't need Sacha for this task.

John: That's correct. Unless Sacha would be the one writing the email, I don't need Sacha to write the email.

Sacha: Okay. Very interesting.

1:33:08 Searching items: ripgrep, vector embedding, openclaw

Karthik: So if you want to get a list of all your interactions with Sacha that have been captured in your Org database, would you just use full-text search with a name, or a semantic search with a name?

John: That's a really good question.

Sacha: You just removed two of the tasks, so we can put some back so that there's something to search.

image from video 01:33:32.640John: Well, I would first use ripgrep, because that's the fastest.

image from video 01:33:38.120John: But it all really depends on what it is that I'm looking for, because I have lots of—Sacha occurs a lot of times in my database. I don't have any way of saying, "What are all of my tasks related to Sacha?" unless she were a category. If she were a category, I could do it. But I don't have categories for every single person that I know.

Karthik: Okay.

Sacha: Okay, cool.

Karthik: So even with this extensive system, there are views that are not readily available?

John: I mean, I would probably at this point ask OpenClaw, "What are all my tasks related to Sacha?"

Karthik: Yeah. That's the semantic search doing its job.

John: Semantic plus full-text.

Karthik: Yeah.

John: Actually, let's see what it says. You know what? Actually, I shouldn't just have semantic search doing vector embedding here. I should actually have this able to engage a local LLM from Emacs, instead of just from OpenClaw.

Sacha: We see a TODO appearing real-time as John thinks, "Okay, I've got to add this to the system."

John: Yeah, I'll wait until the need increases.

Sacha: Yeah, yeah. But yeah, once you have the data, especially if you've got it indexed in something like Postgres that can do things a little bit faster, then yeah.

image from video 01:35:07.600John: So this was its list of all tasks related to Sacha Chua. It didn't do a very good job.

Sacha: Because semantic—you know, vector search and names doesn't make sense. But if you're doing something more concept-related, that might work.

John: Yeah.

Sacha: Full text would be great. I think org-ql is something that people often use for that sort of thing anyway.

1:35:36 Linting and normalizing data in a pre-commit hook using Lefthook

image from video 01:35:56.760John: Then finally, I use Lefthook, which is a Git hooks manager, so that when I type "changes"— which is going to make a commit whose subject line is just "changes" (I don't try to give descriptive commits for my thing)— it will use Lefthook to run the pre-commit hook, which is going to use the linter and use the org-jw Haskell. So it's now doing full round-trip lint on just the files that changed, by the way, not on all of them.

image from video 01:36:10.280John: That succeeded, so it made the commit. Now that it's made the commit, it's going to store all of the changed entries in the database. Then it's going to do vector embeddings on all of the text regions that need up-to-date embeddings.

Sacha: I think this whole normalization piece is something that I would love to see if we can get into a form that somebody other than you can run, because it's one of the things I've envied about your system for a while.

1:36:40 LLM-generated RFC-style specifications of the format

image from video 01:36:49.000John: I did finally write it all. I had AI help me create an RFC-format document for the entire Org-mode format. So this documents as a standard the Org-mode text format. Then this document is an RFC-format document which is the delta—everything that I do that is different or adds on to the Org-mode format is in this document. We could see, like, keywords. Not keywords, what do I call them... Verbs.

1:37:17 The heading grammar

image from video 01:37:18.160John: So like here, this is the extended heading grammar. This shows exactly what can be in the heading of an Org-mode entry in what order. There is no syntax for headings in the Org-mode standard, but there is one in the JW extensions. So my org-jw Haskell project really is just an implementation of this RFC document.

Sacha: I see, because you've constrained your vocabulary so that, for example, your headings are verb-colon-whatever, and that can be verified by your Haskell program.

1:37:52 A TODO example with everything

image from video 01:38:11.040John: Exactly. You can see down here an example that uses every feature. It's got the keyword, the priority. It's got the context, which is a special type of context—usually relating to accounts or things like that. If it's related to a bank, or actually if Sacha were the one who asked me to write the email, I might make Sacha be the context of the task. Then there's the verb, then there's the title, then there's the locator.

1:38:27 Locating objects in the physical world with {text}

John: The reason I use a locator is that in my closet I have a hanging files folder, and every file has a letter of the alphabet on it, A through Z. So if the task has—let's see here—if the task was "send email to Sacha" like this, this is an indicator to me that there is information related to this task that's in folder A of the hanging files folder. So sometimes if I have to send my property taxes into the county— well, they sent me a form to fill out and post with my check, so that form has to live somewhere while the task is open. Closed tasks, it's okay that their locator becomes invalid, because then I take the item out of the hanging folder and I shred it. But while the task is open, the locator locates related information, even if that information is physical.

Sacha: I'm always very interested in the interaction between digital and physical systems.

image from video 01:39:40.320John: Yeah, I might say "garage" to locate something in my house, that there's an item in the garage.

Karthik: The typical approach to this would be to use an Org property that says "location" or "physical location," but I guess because you require properties to be managed...

John: Yeah, I don't see properties. To me, they can't hold actionable information.

Sacha: So what I'm taking away from this is "stuff more useful metadata into titles."

John: Or put a tag in the title that tells me that there is useful metadata, like the link tag.

Sacha: Yeah, yeah.

John: I could have a "phys" tag, which says that there's a locator to a physical something in the property. Or what's another one I use? It just occurred to me a second ago, but now I've forgotten it. Whatever. But yes, I could use that more too.

1:40:37 Managing attachments: DEVONthink for long-lived files, org-attach for temporary ones

John: Oh, attachments. I also use Org attachments. If somebody sends me a PDF, if that PDF is long-lived, I'll put it into a file-retrieval database called DEVONthink. Then I have a package for Org-mode, of course, called org-devonthink. If I just have the item selected in that database, I can hit a key which will associate a link that goes to that unique entry within the database. But if it's short-lived, meaning that I only need the PDF as long as I need the task to be open—kind of like with locators— then I will put it into an Org attachment, and that lives within a git-annex-controlled Git data directory within my Org-mode, sea of Org-mode data files. It gets a tag, which is capital FILE instead of capital LINK, which tells me if I open the attachments directory for this task, I will see something related to the task.

Sacha: Interesting.

1:41:45 Tags hint at where you can find more information

Sacha: So the tags also include hints about where you can go for more information.

John: Yeah, and those tend to be all uppercase— those sort of metadata-awareness tags— whereas the "call," "errand," blah blah blah are all capitalized words, as are all of the ones for people.

Karthik: About the link tag, sorry—you have LINK as the TODO keyword. You also have a link tag. Yes. Do they mean different things?

John: The LINK keyword means this is equivalent to a note that has a link tag but no body.

Karthik: Sorry, what am I looking at on your screen?

John: Oh, you're not looking. I'm not showing you anything.

1:42:31 :LINK: tags indicate that there is a link; LINK state says this note is only a link

image from video 01:42:45.120John: So if I have a note that has a body and it has a URL, then it has to have a link tag.

Karthik: Oh, okay. This is just putting it into practice.

image from video 01:42:49.040John: If there is no body, then it's this. It's just a contraction.

Sacha: It says, "Don't bother looking in here for a body."

John: Yeah, right. There's no supporting information here. This is a bookmark link. But if it's a note, then it's a note with a link attached. It's a question of: is the link the principal piece of information, or is the link the ancillary information?

Sacha: So the link tag says you can use C-c C-o on this and have something useful happen. The LINK type, or the LINK TODO state, says, "I'm just the link, I don't have any other notes."

1:43:30 Some other code turns Firefox bookmarks into an Org Mode file

John: Right. I have some other code— I believe it's written in Python— that will turn my Firefox bookmarks file into an Org-mode file with a bunch of link entries. It does this so it keeps that file in sync. That way I can use Firefox as a bookmark manager if I want.

Sacha: Cool.

1:44:06 The format specifications are LLM-generated

Karthik: So I looked at the RFC and it's 3,600 lines. That's the delta. How long did that take you?

John: It didn't take me any time at all. I just asked Claude to write it.

Karthik: Okay. But then how do you know it got it right?

John: I don't know that. Even if I wrote it, I wouldn't know that I'd gotten it right.

Karthik: That's true. Yeah. But I was wondering, is that intended for humans?

John: Probably not. It's intended for people who are writing such a process for themselves, and they would only be reading one specific section of interest. There's no human being that would ever read that document front to back. Or you could feed it to an AI and have it create the same thing for you in Python or Rust or whatever language you prefer.

Sacha: It's a specification.

John: Yeah, and AIs do very well with specifications. One thing that I could do that would be interesting is to use that specification to create a compliance test, and then any application which is able to pass the compliance test would be an implementation of the spec.

Sacha: It also helps with your validation code, probably. Yeah, that makes sense.

1:45:30 Quick recap

Sacha: Cool, cool. Okay, so a whole bunch of things here that I am definitely looking forward to digging into further. I particularly like that idea of a draft—you know, kind of just capture the text and then have a menu of actions that you can do with the text, such as turning it into an email message or copying to your clipboard while saving a copy of it. That sounds very interesting. And then I can dig into all the other cool things that you do with breaking down tasks and identifying tasks out of transcripts. Many, many things to explore. Karthik, is your brain full too?

Karthik: Yes, my brain overflowed with both information and ideas. I knew about the Drafts thing because John told me last year, but I didn't get it. The demo really helped with that in particular. Yeah, this was a whirlwind tour of...

1:46:28 A quick look at Github repositories: org2jsonl

image from video 01:46:45.160John: So in my GitHub, you will find—if you look for "org-this" and other utilities that I use for managing Org-mode— there's another one I wrote called org2jsonl, which will take any Org-mode file and turn it into a list of JSON objects. That's because there are certain utilities like Beads for managing tasks for AIs, and this allows you to use Org-mode as the data format for that. I have a Rust implementation of Beads that I cloned from somebody else that I modified to use org2jsonl, so it manages the tasks database there. We talked about org-hash, org-drafts.

1:47:16 org-gptel: chat with AI within Org Mode instead of having to leave it

image from video 01:47:16.840John: I use ob-gptel so that in my lab notebook especially, I can dialogue with AI within Org-mode, not having to leave Org-mode and go to some other client. I have a now a running dialogue with the AI in my Org-mode file.

1:47:35 obr: fork of Rust port of Beads to use Org Mode for issue lists

image from video 01:47:35.360John: obr is the task management tool that uses Org-mode as the storage format. We talked about org-devonthink.

1:47:45 org-context saves metadata when refiling to allow restoring the task to its original location afterwards

John: Oh yes, I use org-context a lot as well. I never mentioned that one. This is another way that I use metadata. If I'm on a task, I can be on the stars of that task and type a short key. So w will refile a task somewhere else into another Org-mode file. When you refile a task in my system, it will supply it with a rich set of metadata identifying where the task came from. That's what org-context does. It doesn't do that for items that you refile out of the drafts inbox, because I assume that everybody came from the drafts inbox. But if it goes from the inbox to another file and then to another file, it'll know it came from that previous file. This can happen, for example, if a work task becomes an open-source task. Using org-context, this process is reversible. You can go to a task and say, "You know what? Unrefile yourself back to where you were." It uses that metadata to put itself back.

Karthik: Does this also work when you archive entries?

John: org-context advises on top of the archive mechanism to do the same thing for archiving that it does for refiling, by thinking of archiving as a form of refiling. So it allows you to unarchive a task back to where you archived it from.

Karthik: Yeah, because that's...

1:49:13 org-context use case: moving packing lists to the phone and back

John: The main reason I wrote it and that I use this system is that I use the iPhone app Plain Org for maintaining a list of items that I want to have access to as Org entries on my phone. So I have a special file called mobile.org that has its own little tiny hierarchy of just the items I want to see on my phone. But an Org entry can't live in two places, and it doesn't make sense for the phone version to be a link to the computer version because it doesn't have access to the computer. So what I do is, for the duration of time that I want it on the phone— and usually this is packing lists, actually, because I want to do the packing list on my phone— I will refile it into mobile.org. Then when I mark it done, I will unrefile it back to where it came from, which is typically a trip. One of the capture templates I have is for trips, where it will include "get a hotel," "get a flight," "do this," "do that"— it just is like a stock body of ten tasks that are associated with every trip that I ever take. Every time I do a trip, one of those tasks is a packing list task with a huge checklist underneath it. I will file that into mobile.org and unrefile it back.

Karthik: I see what you meant by "don't ever have only one system, only ever use Org, put everything in Org." You can't have a packing list somewhere else that's full of things to do.

John: I wouldn't have two. It has to live in this.

Karthik: Yeah, yeah, I know. I mean, that's one of the—I mean, that is a tooling problem. So there's a psychological problem, I think, with me trying to do things with Org, where present-me and future-me don't agree on what needs to be done and when, right? Org can't help with that. Then there's a tooling problem, which is just how do I see the things I need to see on my phone when I need to see them? At least as far as the tooling problem is concerned, you've got it all sorted.

John: I'm getting there! Little bit by little bit.

1:51:22 org-agenda-overlay

image from video 01:51:24.240John: Then lastly, org-agenda-overlay. This is what causes the background colors to differ among my org-agenda items. org-agenda-overlay allows you—as a property, or as a file property, or as a defined Org-mode variable— to associate categories. I don't... maybe I even can associate tags. It might be arbitrary items, but you can associate it with a face property. Then you can specify in the face property any modifications to the Emacs face that you want to make. I have it set so that—I think I do this by... yeah, I do it by... Are you seeing my Emacs at the moment?

Karthik: Not yet.

John: Let me show you the Emacs. So I do it with org-todo-keyword-faces. Then I also do it with—oh, I think that's the only one I do it for. But you can see here—no, that's the colors of the keywords. Sorry, let me go here. It's under, of course, my org-agenda-overlay use-package declaration.

image from video 01:52:26.960John: So I have org-agenda-overlay-by-file-tag. If the file has this file tag, then it will apply this background and foreground to any task from that file.

image from video 01:52:43.400John: Then this is overlay-by-olp, which is the outline-level path. If it's in a heading called "inbox" (and of course this supports slashes so that you can be more specific), then alter the face. But you could do this with properties on the individual entries as well, if you wanted to.

1:53:05 john-wiegley-theme defines a palette

image from video 01:53:15.041John: I created—let me see, what did I call it?—oh, john-wiegley-theme. I love to use rainbow-mode to help me edit this file. I wanted to create a consistent color vocabulary for everything that is in Org-mode. So all Org-mode tags, keywords, background colors, everything is harmonized to use these sets of colors. I have dark, darker, and darkest variants because of my black background.

Sacha: Interesting.

John: Yeah, I used a Mac app called Paletton to help me create the harmonious color wheel that these are chosen from.

Sacha: Okay, just a quick check. In terms of getting stuff out of this conversation in two forms that other people can learn from— if we're thinking of the audio recording and the transcript—we didn't talk about anything really weird. So is that reasonably good to go, or do you want to review it first?

John: No, I would ask the AI to let me know whether there was anything in the transcript that I wouldn't want to become public.

Sacha: All right. So basically, we do the transcript, we break it up into chapters so that there are timestamps, you have your AI say whether we should need to take stuff out. I was thinking maybe we do the transcript, possibly the audio recording, because it's fun to hear people be excited about stuff—and then you can imagine your cadence as you're talking about things. Then we can progressively enhance it with screenshots or clips or whatever Karthik has patience for. I don't know if we're going to do the full whole length video, but definitely bits of it.

Karthik: I'm going to start, and then I'm going to see how it's going. If it looks like I just have to draw rectangles over things—let me do like ten minutes of the video by hand and see if it took a reasonable amount of time. If it did, then I can extend that to the whole video. Otherwise, we will probably have to downgrade to screenshots or something where it's safe and easy to redact stuff.

Sacha: And then we can send— Karthik will coordinate with John about whatever else needs to be removed.

Karthik: Yeah. So John, I'm going to ping you and ask you, "Is this okay to include? Is this okay to include?" I'll try to batch these queries so you can give me—I'll try to be conservative, but yeah, let's see. In any case, I will share the video with you. No one is publishing anything until you give me the okay. So I think the transcript and audio stuff, Sacha, that's on you.

Sacha: And the text. Yes.

Karthik: And the video thing—drawing rectangles and adding blur filters— I will get started on that.

1:56:16 Some numbers: 83 packages just related to Org Mode

John: I just looked at my Emacs init file, and I realized that I have 83 different packages being configured that are related just to Org-mode. There's a lot being layered on to Org-mode. I didn't talk about org-contacts, I didn't talk about org-edna, I didn't talk about vcard or inline-task—all of these are things that I use very heavily. Like org-noter—org-noter is my primary way of taking notes on PDF files.

Sacha: You know, if we spend like five minutes where you just rattle off this list of modules you have, with like a one-line description—beyond the package description— of how it fits into your workflow...

John: I don't even remember what they all do.

Sacha: That's true. Yeah.

John: org-transcription I use, like, oh my gosh.

Sacha: A topic for another conversation.

John: Yeah, I think it's another—

Sacha: I think that's one of the fun things that we had back when we did the first Emacs Chat: just going through the configuration. Because when you're looking at it, you're like, "Oh yeah, that package, this is how I use it," or, "Oh yeah, I totally forgot about that one." So at some point we can do a walkthrough that's got config so you can say, "Oh yeah, this is what I use for this."

Karthik: Just use-package block after use-package block.

John: Yes, that's right. One after the other.

Sacha: But it doesn't have to be today.

Karthik: Yeah, definitely not. Yeah. I think Sacha and I have our hands full trying to get this video ready to be published.

John: Yeah. Well, at least I think we scratched the surface. I think we covered some of the big, broad strokes.

Sacha: Yeah.

Karthik: Yeah, I would say so.

Sacha: So this was a great idea. And thank you for taking the time to do it.

1:58:05 Org Mode inspiration

Karthik: I hope people get an idea of what's possible with Org-mode, because the common problem is that people say, "You can do everything, you can manage your life in Org-mode," and then when someone asks you to show how, you see some very anodyne, pedestrian things like to-do lists, and that's not what we're talking about. That's not the scale of things. Yeah, yeah, yeah. "And don't forget the milk," and this and that. No, that's not...

Sacha: Yeah, no, that's why we should talk about... 40,000 entries—what were you saying... 9,700 tasks and 1,200 of them are open. Yeah, that's the kind of scale we're looking at.

Karthik: And it's been maintained for 20 years at least.

1:58:51 Statistics

John: Actually, let's see, talking about— Let me share my terminal because I haven't looked at this in a little bit.

John: So if we go into my Org and I type make stats.

image from video 01:59:05.480John: So this is 3,170 files, 40 megabytes of data. Oh, it is over 40,000 entries now.

Sacha: Yeah, yeah, yeah.

John: 28,000 are TODO items.

Sacha: Okay, okay. Well, let's update that.

John: 818 are open. So more than half of my Org-mode entries are TODO items.

Sacha: Very cool. And somehow the combination of org-ql and the Postgres database makes it easy to just fly through all of those files looking for the ones that you need.

John: I mean, "easy" is a very charitable word.

image from video 01:59:49.480Sacha: "Fast," at least.

John: Oh wow, I have three items that actually have B priorities. I wonder what those mean.

Sacha: I'm surprised that your validation and normalization functions let those slip past you.

John: I don't think I disallow B. I think I had B have a specific meaning at one point, because B stands out so much. I had ascribed it to some special meaning.

Sacha: I like your stats. They're fun.

2:00:18 Most-common properties: ID, CREATED

image from video 02:00:21.240John: Yeah. Look at that. My most commonly occurring properties are ID and CREATED. That's right, that's what it should be. Oh, but not every entry has a hash, clearly. I wonder how that happened.

2:00:33 Log entries are useful for notes by date

John: And we didn't talk about how useful log entries are. That's actually— I did try to veer away from Org-mode in like 2010, and the reason I couldn't was because log entries are too useful.

Sacha: Oh, now I'm really curious.

John: Yeah. Just the ability—while you're working on it— you know, some tasks live a long time. There was a time when Aetna wouldn't honor one of my insurance claims and it took me nine months to resolve it with them. But every single time I talked to them on the phone or by email, I would log an Org-mode note of what we talked about and who I talked to. So as time went on and I would get on the phone with a new person, I would be like, "Well, on this date I talked to this person and they said this. Then on this date, this person said this." I would overwhelm them with so much concrete information about how much I had done to resolve this, that they would be like, "Okay, okay, I get it."

Sacha: This person keeps receipts.

Karthik: How did you get that log into Org-mode? Were you on your computer while you were on the phone with them?

John: Yes, I was on the computer while I was on the phone.

Karthik: Okay. I'm just wondering, that's the kind of thing I could never manage, because I don't know where I'll be on the phone. I won't be near Org-mode.

2:01:48 Transcripts are great too

John: Well, nowadays the iPhone lets you record your phone conversations. So you can ask them if it's okay to record, and then—hey, AI is your friend.

Karthik: Okay.

John: I love transcripts now. I use them for all kinds of things.

Sacha: I agree. Transcripts are wonderful.

John: Yeah. And I record them all. Every meeting file, if I can, I just put the whole transcript in the file, because it helps with searching in the future.

Sacha: Yeah. All right. And on that note, we are going to turn this into a transcript and possibly [video].

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- Chatting with John Wiegley about personal information management and Karthik Chikmagalur (Post Sacha Chua)--L0--C0--2026-07-09T00:30:29.000Z

Protesilaos: Emacs live with @linkarzu on Org basics on 2026-07-12 at 22:00 Europe/Athens

Raw link: https://www.youtube.com/watch?v=Crp2e5D_Q78

This Sunday I am meeting again with Christian Arzu from the @linkarzu YouTube channel. We will look into the basics of Org mode. How to use it to write documents and organise your life.

Our previous meeting about getting started with Emacs is here: https://protesilaos.com/codelog/2026-07-05-emacs-live-with-linkarzu/.

-1:-- Emacs live with @linkarzu on Org basics on 2026-07-12 at 22:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-07-09T00:00:00.000Z

Raymond Zeitler: Configure Web Browser to be More Emacs-like

Emacs, LibreOffice Writer and Vivaldi are the productivity tools I use most often. So I like to incorporate a feature from one into the others. One example is the ability to initiate a search from selected text. I added this feature to LibreOffice as described here. Another nice feature is the ability to switch between the current buffer and the next (or previous) buffer with C-<TAB> and C-S-<TAB> that I added to Emacs1.

But I've also configured Vivaldi with a useful Emacs feature. Now it responds to C-S-k by invoking "Close tabs to the Right." Emacs devotees know that C-k is bound to kill-line, which "Kill[s] the rest of the current line...."2 If you imagine that the row of tabs is a line of text, it's natural to expect C-k (or C-S-k) would remove all the tabs to the right of the current tab.

Why did I choose the key combination C-S-k instead of C-k? I think at the time C-k performed another function. Or perhaps I felt that it was more appropriate to use two modifier keys for such a significant operation. I rarely need to delete tabs to the left; if I did, I'd bind C-M-S-k to "Close tabs to the Left."

You might utter the following complaint, "But C-S-k doesn't work for Gmail; it brings up a virtual keyboard instead!" Correct. So while we're on the topic of Vivaldi key configurations, I'd like to point out that it's possible to tell Vivaldi to ignore a website's key assignment, allowing the key to pass to Vivaldi. Just search for the setting "Browser Priority Shortcuts," click on one of the existing key combinations and press C-S-k to add it to the others.

I rely on the web browser's tab history a great deal, too. I'm working on implementing something vaguely similar for Emacs."


1 To get the C-<TAB> and C-S-<TAB> to switch buffers, just add this to your init file:

  (keymap-global-set "C-<tab>" 'next-buffer)
  (keymap-global-set "C-S-<tab>" 'previous-buffer)

2 https://www.gnu.org/software/emacs/manual/html_node/emacs/Killing-by-Lines.html

-1:-- Configure Web Browser to be More Emacs-like (Post Raymond Zeitler)--L0--C0--2026-07-08T20:35:47.102Z

Irreal: Insert Mode

My pal Watts Martin has a post that explores an artifact from the past: the insert key. Martin, who’s been a Mac head much longer than I have, remembers when Mac keyboards still had an Insert. They no longer do, of course, but my Unicomp Model M clone does as do several other PC keyboards. The key was used to toggle between inserting text and overwriting it. Emacs calls the latter overwrite-mode.

The idea of insert mode is that text in front of the cursor is pushed ahead as new text is added behind it. When insert mode is off—that is, when overwrite-mode is active—the text in front of cursor is overwritten. Overwriting text used to be more common but these days everyone uses insert mode and you rarely see it.

Still, as Martin says, overwriting can sometimes be useful and if you have an Insert key there’s no reason not to use it in Emacs. That’s a one liner and easy do in your init.el. On the Mac, the Insert key is usually mapped to Help but on other systems it probably has a different mapping so you may have to experiment to discover what it is.

Finally, Martin wanted a visual clue as to which mode he was in so he changed the shape of the cursor to reflect the mode. Again, that’s easy to do. He uses overwrite-mode-hook to toggle the cursor shape.

While I agree that overwrite-mode can occasionally be useful I don’t—and have never—used it enough to dedicate an actual key to it. I’m perfectly happy to simply call overwrite-mode to turn it on and off.

-1:-- Insert Mode (Post Irreal)--L0--C0--2026-07-08T14:57:58.000Z

Anand Tamariya: Plan 9: Dynamic HTML

 

Render Dynamic HTML using plot and qjs.

 

Linkedom Fixes

  • Use lowerCaseAttributeNames: isHTML to ignore case for attribute names.
 

Lessons Learnt

  1. Mouse click generates two events like a keypress. If you are doing a zero byte read, you're likely to miss an event as each read pops the event queue.
  2. Screen, keyboard and mouse are separate devices - obviously. However, only Plan 9 allows these to be interacted with via separate programs. 

 

References

  1. HTML rendering in Emacs 
  2. HTML rendering via SVG 
  3. Browser in Javascript 
  4. Plan 9: Tiled Map 
-1:-- Plan 9: Dynamic HTML (Post Anand Tamariya)--L0--C0--2026-07-08T09:55:21.000Z

Kristoffer Balintona: emacs-guix-starter: I’m glad this exists

Recently Hilton Chain[1] put together emacs-guix-starter, a starter Emacs configuration that uses Guix and is set up to Guix. What it is: a regular Emacs init.el (with basic configuration) alongside a companion Guix packages manifest (packages.scm) to install those Emacs packages and all their system dependencies. The init.el even provides configuration to edit Guile!

Beginner-oriented configurations are sorely lacking in Guix. In the Emacs world, there are many personal configurations, configuration frameworks (Doom, Spacemacs, Prelude), and starter configs (minimal-emacs.d, emacs-bedrock). These are great references, if not just to peruse. This is especially true for newcomers, users who haven’t yet developed a second-nature for how to organize files, idiomatic coding patterns, nor familiarized themselves with the standard library.

On the other hand, in the Guix world, aside from emacs-guix-starter, there’s guix-studio[2], whose last commit was 5+ years ago.

With Emacs, learning feels “bootstrapped” because once one learns about Help buffers and navigating Info manuals, it becomes much easier to both discover questions and their answers (by navigating manuals, jumping through source code, and reading docstrings). On the other hand, it’s been my experience that Guix doesn’t become that much easier to pick up once you start—though, granted, I’ve spent much less time trying to figure out Guile and Guix than I have Elisp and Emacs.

emacs-guix-starter shows what it should look like…

  • to use Guix as one’s Emacs package manager,
  • what an Emacs configuration to develop in Guile looks like,
  • as well as how to compose several Guix CLI commands.

Other users can use it as a model or base for their own configuration. Just seeing these types of projects around goes a long way to demystifying confusions and spreading knowledge throughout the community.

-1:-- emacs-guix-starter: I’m glad this exists (Post Kristoffer Balintona)--L0--C0--2026-07-08T00:55:00.000Z

Protesilaos: Emacs: global keybinding overrides

Raw link: https://www.youtube.com/watch?v=D99GB591Vgo

In this video I demonstrate how to define key bindings that have a global overriding effect so that major modes cannot change them. This is important if you want to redefine keys that are not meant to be configured by the user.

Below is the code I showed in the video:

(setq custom-file (locate-user-emacs-file "custom.el"))
(load custom-file t)

(load-theme 'modus-operandi-tinted)

(use-package keycast
  :ensure t
  :config
  (keycast-mode-line-mode 1))

(set-face-attribute 'default nil :height 180)

;; Your only safe keys are documented here:
;; (info "(elisp) Key Binding Conventions")
(define-key global-map (kbd "M-o") 'other-window)

;; (define-key MAP KEY nil)

;; There is also `defvar-keymap'...
(defvar prot-overrides-mode-map (make-sparse-keymap)
  "Keymap for the `prot-overrides-mode'.")

(define-minor-mode prot-overrides-mode
  "Activate the `prot-overrides-mode-map'."
  :global t
  :init-value nil
  :keymap prot-overrides-mode-map)

(define-key prot-overrides-mode-map (kbd "M-o") 'other-window)
(define-key prot-overrides-mode-map (kbd "C-<return>") 'find-file)

(prot-overrides-mode 1)
-1:-- Emacs: global keybinding overrides (Post Protesilaos)--L0--C0--2026-07-08T00:00:00.000Z

Randy Ridenour: Beamer-Style Typst Slides Using Org Mode

You should note one thing before investing too much time reading what follows: I could not have done this in the time I had available without using an AI tool. So, if you are averse to anything tainted by such things, you’re welcome to close the window at any time.

For years, almost everything that I have written was done with LaTeX. After using several editors, I eventually settled on Emacs. I’ve found nothing better than Emacs plus Auctex for editing LaTeX files. The basics of LaTeX are not difficult, so long as you don’t need to get into the weeds of the actual TeX code. Still, though, there is a kind of complexity to LaTeX files that would be nice to avoid. For example, a simple enumerated list looks like this:

\begin{enumerate}
  \item One
  \item Two
  \item Three
\end{enumerate}

The combination of Auctex and Yasnippet makes writing this code simple, but it gets very busy very quickly. If we wanted to add some sub-points to the first item, we’d have to do this:

\begin{enumerate}
  \item One
  \begin{enumerate}
    \item One
    \item Two
  \end{enumerate}
  \item Two
  \item Three
\end{enumerate}

Then, I discovered Org mode and life changed for the better. The same list was simply:

1. One
     1. One
     2. Two
2. Two
3. Three

Emacs handles all the numbering automatically. With Org mode, it became possible to write very simple documents, export to LaTeX, and compile the PDF without even having to touch the LaTeX file. Even better, I now had source documents that could become anything I needed, LaTeX, HTML, or even, God forbid, DOCX.

Org Mode’s LaTeX Beamer support made producing lecture slides incredibly quick and easy. Eventually, I developed a workflow by which I could produce Beamer slides and a handout that contained more detailed explanations not found on the slides all from the same source document. I wrote my own Beamer theme called basic, replicating the Keynote Basic White theme. It’s a very simple theme that has white, gray, and black variations and a title slide with room for the university’s logo. It works well, and gives me everything I need. The only downside is compilation time. A large presentation that I used this week compiled in twelve seconds for the slides, another seventeen seconds for the handout. Then, as it often goes, I immediately notice a mistake, fix, recompile, repeat….

I played with Typst when it first came out. I was impressed, but it wasn’t able to do then what I needed. It’s now become a robust publishing system with a package system that provides powerful functionality, including a very good presentation package, Touying. Typst text markup syntax is similar to Org syntax in complexity, and It compiles even complex documents instantly. So, I wondered if I could replicate my slide/handout workflow in Typst.

With much help from Claude code, I replicated my basic theme and worked out a way to generate the handouts and slides from the same source files. I also made sure that the theme could easily be able to produce the kinds of slides I occasionally used: two-column slides, full-frame image slides with no titles, and slides with a main point centered in the slide, bold-face, with a large type size. Everything worked well, but, in the end, I concluded that I really didn’t want to write in Typst, I wanted to write in Org mode.

Emacs does have an Org mode exporter for Typst, ox-typst, which works beautifully for articles, but not so much for my slides. So I concluded that it was a fun exercise for the past few days, but I would just continue making slides using Beamer and Org Mode in my usual way.

Then I started wondering how difficult it would be to make an exporter for Org to Typst that would do what I needed. It turned out to be, once again with the help of Claude, not so difficult. Here is the resulting workflow, from beginning to end.

The Pieces

Three things work together:

basic-theme
a theme for Touying that replicates my Beamer theme. It handles the styling of the title and section slides and provides some white, black, and gray color variants.
rlr-touying-scaffold.el
this provides an Emacs command that scaffolds the required files in a new folder. The include an Org file, a config file, and the two Typst files that are compiled to produce the slide deck and the handout.
ox-touying.el
a custom Org export backend. It turns the Org file into the Typst source that is read by the slide deck and handout files. With this, I never need to hand-write Typst markup for ordinary content.

To summarize, a lecture lives in one Org file. Compiling that file produces two documents, slides to present from, and a handout for students to read.

Creating a New Lecture

M-x rlr/new-touying-presentation asks for a title and a directory, then creates a subdirectory containing:

  • config.typ: theme setup and metadata (title, author, date, institution, logo).
  • talk.org: where the actual content goes. This starts out as just a #+TITLE: line and a comment to remind me of the conventions below.
  • <slug>-slides.typ and <slug>-handout.typ: the two compile targets. The file names are created from the lecture title, making it easier to find them with a search.
  • content.typ: generated from talk.org automatically. I never touch this file directly.

From then on the loop is: edit talk.org, run M-x rlr/org-export-to-touying-content to regenerate content.typ, then typst compile whichever target I want to look at. Of course, this being Emacs, I wrote a function that executes both the export, the compilation, and opens the resulting PDF’s with a single command.

Two Configuration Options

config.typ’s project() function takes two options.

variant picks the color scheme: "white" (black on white, the default), "black" (white on black, good for a dim room), or "gray" (black on light gray). Modifying this requires changing one line in config.typ:

#show: project.with(variant: "black")

slide-level decides how many heading levels a talk uses. Most of my talks are flat require no subsections, so the default is 2: a top-level heading is a section, the next level down is a frame. Talks that need a subsection layer in-between require slide-level: 3 instead:

#let project(variant: "white", slide-level: 3, body) = { ... }

With slide-level: 2, * is a section and ** is a frame. With slide-level: 3, * is a section, ** is a subsection, and *** is the frame. Whichever level is “the frame” is the one whose heading shows up as the bold title at the top of the slide; the levels above it get their own dedicated section/subsection slides instead.

Writing Content

Inside talk.org, Org mode headings provide the section, subsection, and slide structure. Plain prose under a frame heading needs no special markup at all. it just becomes the body of the slide:

* Hellenistic Philosophy

** The Cyrenaics

*** Cyrenaic Hedonism

- Aristippus @@typst:#pause@@
- Pleasure is the only good @@typst:#pause@@
- Goal: maximize pleasure in every moment

The @@typst:#pause@@ line is Org’s own “export snippet” syntax, which passes raw target-format text straight through untouched. It provides the reveal content function It’s equivalent to the Beamer \pause command.

Speaker notes (shown only to the speaker, via Touying’s second-screen view) and handout notes (reader-only context that never appears on the live slides, only in the printed handout) are each their own block:

  #+begin_speakernote
  The goal is maximizing pleasure across a lifetime by maximizing pleasure in every single moement through self-indulgence.
  #+end_speakernote

  #+begin_handoutnote
The goal is maximizing pleasure across a lifetime by maximizing pleasure in every single moement through self-indulgence. This is a naive hedonism, especially compared to the Epicureans. For example, consider a person who spends any money they have as soon as they get it vs. a person who saves her money for a year to get something she really wants. Who will have the greatest pleasure over the course of their life?
  #+end_handoutnote

Both do not compile to the side on which they don’t belong. i.e., speaker notes never reach the handout and handout notes never reach the slide deck.

Layout Blocks

Most slides are bulleted or numbered lists, but I do sometimes need some layouts that plain prose can’t quite capture. There are three more #+begin_...#+end blocks to cover those cases.

Two columns, side by side:

* Stoicism
** Major Figures

#+begin_columns
#+begin_column
- Classical
    - Zeno of Citium
    - Cleanthes
    - Chrysippus
#+end_column
#+begin_column
- Roman
    - Marcus Aurelius
    - Seneca
    - Epictetus
#+end_column
#+end_columns

A full-frame image, no title, no margins:

** The School of Athens

#+begin_fullslide
file:school-athens.jpg
#+end_fullslide

A lone image inside #+begin_fullslide is automatically sized to cover the whole slide. That’s fine for an image that is close to the 16:9 landscape slide format. If a specific size is desired instead, an #+ATTR_TOUYING: line right before the image link overrides that:

#+begin_fullslide
#+ATTR_TOUYING: :width 60% :height 300pt :fit "contain"
file:school-athens.jpg
#+end_fullslide

The same #+ATTR_TOUYING: mechanism works on any image, full-bleed or not; :width, :height, and :fit pass straight through to Typst’s image(), and :align (e.g. center, center + horizon) wraps the image so it’s centered in its container, since alignment belongs to the surrounding box in Typst, not to the image itself:

#+ATTR_TOUYING: :width 40% :align center
file:diagram.png

And last, a big centered “statement” slide – the kind of slide that’s just one line, large, in the middle of an otherwise empty frame, for the one claim in a lecture I want to land without any visual clutter around it:

** The Thesis

#+begin_fullslide
#+ATTR_TOUYING: :size 2.5em
#+begin_statement
Virtue is the only good.
#+end_statement
#+end_fullslide

Nested inside #+begin_fullslide like that, the frame has no title at all – just the sentence, centered both horizontally and vertically, at whatever size :size asks for (2em if omitted). Left outside a fullslide, the same block still centers its text but leaves the frame’s title showing above it.

Compiling

Once talk.org says what I want, M-x rlr/org-export-to-touying-content regenerates content.typ, and then it’s ordinary Typst:

typst compile my-talk-slides.typ    # what I present from
typst compile my-talk-handout.typ   # what students get afterward

I use this Fish function to compile both and open the respective PDF files without Emacs losing focus:

function compile-touying-deck --description 'typst compile the *slides.typ and *handout.typ files in a directory'
    set -l dir .
    if test (count $argv) -gt 0
        set dir $argv[1]
    end

    if not test -d $dir
        echo "compile-touying-deck: not a directory: $dir" >&2
        return 1
    end

    set -l files (find $dir -maxdepth 1 -type f \( -name '*slides.typ' -o -name '*handout.typ' \) | sort)

    if test (count $files) -eq 0
        echo "compile-touying-deck: no *slides.typ or *handout.typ files found in $dir" >&2
        return 1
    end

    set -l failed 0
    for file in $files
        echo "Compiling $file..."
        if not typst compile $file
            set failed 1
        end
    end
    wait
    open -g *.pdf

    return $failed
end

There is, of course, no need to leave Emacs for the shell.

(defun compile-typst-lecture ()
  "Compiles the slides.typ and handout.typ files in the directory."
  (interactive)
    (shell-command "compile-touying-deck"))

Even better is to export, compile, and open the PDF files with a single command:

(defun rlr/org-mktouying ()
    (interactive)
(rlr/org-export-to-touying-content)
(compile-typst-lecture))

The slide deck paginates normally, respects the progressive reveals, and shows speaker notes on a second screen. The handout collapses the whole thing into one flowing document – no pagination, headings numbered (1, 1.1, 1.2, …), and a plain line above each handout note marking where the text stops being something the audience actually saw and starts being added context for the reader.

Why bother

None of this is strictly necessary – Touying is perfectly usable directly, and I could just write Typst by hand. What Org buys me is the same thing it buys me everywhere else — ease of writing and portability of content. In the end, producing the slides in Typst is no harder at all than it is with Beamer, with the added benefit of instant compilation and more accessible final documents.1

The code and a sample presentation is on Github.

Tagged: Emacs Org Typst

Footnotes

1

Accessibility has, for very good reasons, become very important. We obviously want all of our products to be as accessible as possible to all of our students. Typst has an excellent accessibility guide to help in that endeavor.

-1:-- Beamer-Style Typst Slides Using Org Mode (Post Randy Ridenour)--L0--C0--2026-07-07T14:56:00.000Z

James Dyer: VC Shuttle: Advice-Based Git Sync for Air-Gapped Emacs

I work on an air-gapped VM, which is to say, a virtual machine with no network access at all. You copy code in via USB or a shared folder, develop, and then you need to get that code back out to the host machine. Git is running inside the VM, of course, it's the sensible thing to do for version control, but git push wants a remote, and there is no remote. What you actually need is a file copy operation that shuttles the repo data to a shared folder, where the host can pick it up.

20260514140413-emacs--VC-Shuttle-Advice-Based-Git-Sync-for-Air-Gapped-Emacs.jpg

So I have a pair of bash scripts, unimaginatively called in and out. out copies the source tree to the shared folder so the host sees my commits, and in pulls changes back from the host into the VM. Works fine from the terminal, but I live in Emacs, and I want to trigger this from within Emacs rather than jumping to a shell.

The obvious answer is to bind a key to async-shell-command and call it done. But then I remembered, Emacs already has this infrastructure - the VC mode's push command. When you press P in a vc-dir buffer, or invoke vc-push from the mode-line popup, Emacs runs vc-git-push which calls git push. I don't want that, I want my out script. The pull side is the same problem. So the question becomes: can I make Emacs's VC commands do something else?

Yes, obviously, this is Emacs. advice-add lets you replace any function, and vc-git-push is just a function. The :override strategy means my function runs entirely in place of the original, which is what I want - there's no useful fallback to git push on this machine, it would just fail.

These two functions live in my coding.el starter snippet, here's the push side:

(defun my/vc-git-push-shuttle (_file-list &rest _args)
  "Override vc-push to run a local sync script instead of git push."
  (interactive)
  (let ((script-path "/home/jdyer/bin/out"))
    (if (file-executable-p script-path)
        (progn
          (message "Syncing source TO shared folder...")
          (async-shell-command script-path "*out*"))
      (error "Sync script not found or not executable at %s"
             script-path))))

The _file-list and _rest _args soak up whatever VC passes to the push function, I don't care about the arguments since my script doesn't need them. The function checks the script is executable, shows a message, and runs it asynchronously into the *out* buffer so I can inspect the output if something goes wrong. Pull is the same thing pointing at /home/jdyer/bin/in into *in*, so:

(defun my/vc-git-pull-shuttle (_file-list &rest _args)
  "Override vc-pull to run a local sync script instead of git pull."
  (interactive)
  (let ((script-path "/home/jdyer/bin/in"))
    (if (file-executable-p script-path)
        (progn
          (message "Syncing source FROM shared folder...")
          (async-shell-command script-path "*in*"))
      (error "Sync script not found or not executable at %s"
             script-path))))

Then the advice:

(advice-add 'vc-git-push :override #'my/vc-git-push-shuttle)
(advice-add 'vc-git-pull :override #'my/vc-git-pull-shuttle)

And that's it. Now every Emacs command that would have called git push or git pull runs my shuttle scripts instead. When I press P in vc-dir the code ends up in the shared folder ready for the host to collect. No context switch to a terminal.

The same pattern would work for any situation where you want VC's push/pull to do something domain-specific - copying to a local archive, rsyncing to a staging server, or in my case, bridging an air gap.

-1:-- VC Shuttle: Advice-Based Git Sync for Air-Gapped Emacs (Post James Dyer)--L0--C0--2026-07-07T14:30:00.000Z

Irreal: Problems With Org Mode Code Blocks

Raymond Zeitler has an interesting post on an aspect of using Org Mode code blocks that I didn’t know about. Zeitler says that he prefers to write his code in separate code files rather than in Org mode code blocks and provides an example of why that is.

The TL;DR is that in certain edge cases, things like differing syntax tables can make code behave differently in Org mode than it does directly in Emacs. Take a look at his post for the details.

I have to say that I’ve never run into this problem. Perhaps that’s because I usually use Org code blocks only for code that will be executed in the Org environment. The few times that I have run the code outside of Org, I haven’t run into the problems that Zeitler describes.

Your mileage may vary, of course, but it’s good to be aware that there are some edge cases that can reach out and bite you if you’re not careful. Most of you, I’m sure, will have my experience but it pays to be aware of what can go wrong.

-1:-- Problems With Org Mode Code Blocks (Post Irreal)--L0--C0--2026-07-07T14:14:47.000Z

Charlie Holland: HyWiki: Zero Markup Hypertext

1. TLDR

hyperbole-hywiki-banner.webp

A key feature is missing from most Personal Knowledge Management Systems (PKMSs): define a concept by pressing a keybinding on a word, and from then on all occurrences of that word become live, actionable links everywhere. This capability would transform your knowledge base from a siloed graph into an omniscient second brain. That is the unique feature of the HyWiki part of the GNU Hyperbole package for Emacs, a wiki whose only syntax is the HyWikiWord: a PascalCased word like Emacs or EmacsCompletion, without the link delimiters ([[ ]]) other PKMSs require.

Because a WikiWord is a Hyperbole implicit button, once defined it is highlighted and actionable in every text and programming buffer, not just inside a siloed notes vault. This lets you keep the messy majority of your notes as frictionless, markup-free text, promoting only the few broader concepts into formal nodes. The reduced friction HyWiki provides leads to more and better writing, and deeper understanding. This post also dispels the myth that Hyperbole is too deep to learn. In actual fact, it is useful the moment you install it, and Implicit Buttons and HyWiki are the two best places to start.

2. About   emacs hyperbole hywiki knowledgeManagement

A plethora of PKMSs have come online over the last few years. Most of them give you a siloed container within which you author your notes with hardcoded links. These solutions ignore the critical reality that your entire file system is already a container of knowledge-bearing texts that naturally possess organic connective tissue in the form of implicit and explicit relationships.

Hyperbole's approach honors this reality and facilitates your knowledge traversal, annotation, and synthesis across the interconnected web of information that is already on your machine.

This is the first post in a series on Hyperbole, a package I think is immensely useful and often misunderstood. The misunderstanding is usually about depth. Hyperbole is extensive and turns your Emacs into a vastly capable personalized information environment (PIE)1. People assume Hyperbole's vastness means it is demanding, but I've found it has almost no adoption burden relative to other Emacs packages.

Like many of the best Emacs packages, it is useful the second you install it and asks almost nothing of you to begin using it effectively. You do not need to grok the whole Hyperbole system, and instead you can adopt it incrementally, one capability at a time.

The two best on-ramps, and the subjects of this post, are Implicit Buttons and HyWiki.

3. Implicit Buttons: Emacs as a Hyperverse   hyperbole implicitButtons

Before HyWiki, we need to lay the groundwork with Hyperbole's most important concept: Implicit Buttons.

Hyperbole author Bob Weiner describes implicit buttons as "pattern recognizers across large text corpuses," and that phrase is worth analyzing, because it is the magic trick that transforms your Emacs from a big bag of text into an interconnected and navigable hyperverse.

An ordinary hyperlink is explicit. You must explicitly add markup, wrapping some text in specialized syntax (such as an <a href> or [[Org Link]]) so that some software knows it is actionable (or clickable). In contrast, an implicit button is a natural part of text with minimal syntax to type or distract your eye while reading. Any piece of text, marked up or not, can be an implicit button.

How? When you press the Action Key (M-RET by default), Hyperbole runs a cascade of context-sensitive recognizers over the plain text around point (your cursor) and asks: Does this look actionable? Things like a file path, a URL, a bug reference like fixes #43, a man-page name, a file-and-line-number, a WikiWord, or a programming symbol are all patterns, and each pattern carries an associated action. The combination of pattern and action creates an implicit button type that automatically recognizes matching buttons throughout your Emacs buffers.

So, the Action Key activates any implicit button at point and Hyperbole infers, from context, what the text is and what to do with it. This is similar to double-clicking a GUI button, but more powerful in concept because any text (and everything in Emacs is text) can be an implicit button. Ramin Honary, who gave a fantastic EmacsConf talk introducing Hyperbole2, drew an analogy I keep coming back to:

When you use a well-designed GUI, you can perform all sorts of different actions with just the left mouse click. […] every graphical element that you can click on in a GUI has its own executable command attached to it. There are always visual cues that indicate what behavior to expect when you click. This is the fundamental principle behind Hyperbole, but applied to textual elements, rather than graphical elements. The big difference is that a GUI uses geometric information about a 2D scene to decide what action a click will trigger, whereas Hyperbole uses textual pattern matching.

— Ramin Honary, Introduction to Hyperbole

A HyWikiWord, as it turns out, is just one of the implicit buttons that surfaces from this "textual pattern matching", which is why (once you let it loose with hywiki-mode set to :all), HyWiki turns your Emacs into a zero markup hyperverse. You simply write or download information and all your specified concepts are hyperlinked for free.

hywiki-fig1-hyperverse.webp

Figure 1: The zero-markup hyperverse: with ~/hywiki/ listed at left, the same HyWikiWords (Emacs, GNU, Lisp, and more) light up and become clickable across an Org notes buffer, the hywiki.el source, and a Wikipedia page rendered in EWW

Ramin clarifies these concepts in his post:

  • First, "no markup language is necessary to create actionable text with Hyperbole". That's what I mean by zero markup.
  • Second, because the recognizers run within Hyperbole's global minor mode, the behavior is available "everywhere, regardless of whatever other major or minor modes are active". That's what I mean by hyperverse.

4. Minimum-Viable Syntax   hywiki knowledgeManagement

HyWiki is Hyperbole's wiki. A HyWiki page is an Org file living under hywiki-directory (~/hywiki by default), and a HyWikiWord is that page's name written in any buffer as a PascalCased word: Emacs, OrgMode, EmacsCompletion. This PascalCasing is the only syntax, so no hardcoded links, tags, ids, database indices, or anything else is required.

hywiki-fig2-directory.webp

Figure 2: A HyWiki is just a directory of Org files. Left: ~/hywiki/ in Dired, one page per concept. Right: the Emacs.org page open, displaying live HyWikiWords like GNU and Emacs Lisp that jump to their own pages.

Once a HyWikiWord has a wiki page, any time you type it, it is immediately highlighted and active as a link to that page. You can even link to page sections by simply adding a # character, like Emacs#Description. More advanced users will find that HyWikiWords can even perform arbitrary actions, rather than linking to a page, just as all Hyperbole implicit button types can.

But how does this make HyWiki better than other PKMSs? It helps to see what HyWiki is not doing. In Obsidian, Logseq, and Org Roam, a link is [[Some Page][Visible Description]]. This can be distracting to read and it requires remembering the specific delimiter syntax that only resolves inside that tool. Worst of all, the overhead of manually linking your knowledge adds up with these other tools. With HyWiki, on the other hand, the PascalCase convention3 does all the work for you.

hywiki-fig3-zero-markup.webp

Figure 3: The bare HyWikiWord Emacs (top) is the same live link as the verbose Org syntax [[./Emacs.org][Emacs]] (bottom), minus the delimiters!

John Wiegley, the longtime maintainer of GNU Emacs, made this comparison in a 2019 note that Bob reposted to the Hyperbole list4:

Remember what Wikis did for plain text on a website? They were revolutionary at the time because they reduced the cost of associating information. Creating a link to a new page was as simple as ChangingCase, turning that word into a button you could click on to either visit the page, or create it if it didn't exist. So simple, yet it solved an important problem […]

— John Wiegley

This is exactly how Hyperbole's HyWiki works.

I love that the second I recognize something worth curating in my knowledge graph, a single keystroke promotes a word into a concept, so I can stay in the flow of typing and add associated notes later. Promotion is the right word to me, because the node's link syntax is the node's name, and so anywhere that word already shows up, it becomes highlighted and actionable, behaving similarly to a URL, but without the syntax.

Concretely, if I type Hyperverse and press M-RET, a fresh Hyperverse.org is created. From then on, wherever Hyperverse appears in Emacs, whether I type it into a buffer or whether it appears in a webpage, elfeed article, or email, it shows up as a live, actionable link.

5. The Omniscient Second Brain   hyperbole hypertext

Once a WikiWord is defined, every occurrence of that word becomes highlighted and actionable. HyWiki propagates your knowledge curation in this way. You don't have to do the bookkeeping of linking those occurrences! Past, present, and future, your glorious new proper noun lights up everywhere. The HyWiki documentation explains:

HyWikiWords are also recognized in text buffers after the global minor mode, `hywiki-mode' is enabled via {M-x hywiki-mode RET}. To create or jump to a HyWiki page, simply type out a potential HyWikiWord or move point onto one and press the Action Key {M-RET}. This will create the associated page if it does not exist. This also highlights any other instances of HyWikiWords across all visible Emacs windows. HyWiki is built for scalability and has been tested to be performant with 10,000 HyWikiWords.

— Hyperbole documentation

This is the beating heart of Hyperbole's philosophy, and the source of the "hyperverse" framing.

To be fair to the competition, the notes themselves are not the problem. Obsidian is plain Markdown, and Org Roam and Denote are plain-text Emacs packages, so your files are yours in all of them. But two things stay locked inside each tool. The first is the link, because a wikilink or a node id only resolves inside that tool's own index and graph. The second is reach, because while those tools can surface unlinked mentions of a note, they can only do so in a sidebar, and only inside their vault. Hyperbole instead makes the concept itself live everywhere. This is one of the many ways that Hyperbole "brings your text to life".

Hyperbole's stroke of genius is that it treats all the information Emacs presides over as one arbitrarily interconnected space, and offers you a laborless mechanism to traverse the connective tissue that is already there. If other PKMSs offer you a 'second brain', HyWiki offers you an omniscient second brain.

I really like the way John Wiegley describes it:

Hyperbole […] should be thought of as an extensible "information enabler," automatically turning inert documents into active ones […] With every new recognizer and action you add, the more interactive all your information becomes. It's a multiplying effect, turning inert, standalone documents into more interactive, virtual semi-networks.

— John Wiegley

That multiplying effect John points out is the point of Hyperbole. A WikiWord you define while reading email is the same live link anywhere it later appears. Hyperlinking your world, to borrow one of Hyperbole's catch phrases, is not some sloganish metaphor, but rather a fulfilled promise; Hyperbole's literal behavior.

6. Your File System is the Knowledge Base   knowledgeManagement hypertext

This is something I didn't fully register until talking to Bob, and is an important perspective shift for those who are used to other PKMSs.

If WikiWords are actionable everywhere, then what does everywhere become? Everywhere becomes your knowledge base, or at least, the knowledge-bearing information that will hydrate your curated wiki. This defies the common wisdom of the unfortunately siloed PKMSs.

  • Capture from wherever you read. Once you notice Emacs mentioned in an elfeed article, a mu4e message, or an EWW page, the Action Key promotes that into a curated WikiWord.
  • Connective tissue that spans applications. The same Emacs link is visible and actionable in your prose, your code comments, Org agenda, mail, and (this is Emacs) anything you can imagine or build.
  • Bring Your Own Notes (BYON). Open your Obsidian or Logseq Markdown vault in Emacs with hywiki-mode set to :all, and your WikiWords highlight there too, without the dreaded migration you would need to undertake to convert or integrate those with any other knowledge tool. HyWiki can function standalone or as a sidecar, riding alongside whatever you already use, and hywiki-directory can sit wherever it suits you (even within your own knowledge base).

The sidecar arrangement is so fascinating to me, because it demonstrates that Hyperbole can act as a wrapper around other tools. Unlike other standalone PKMSs, Hyperbole functions as a global hypermedia mechanism for managing your knowledge.

While other PKMSs formalize everything in your notes repository as a node that adheres to their (very specific) schema, HyWiki lets you bring formalized nodes to your existing notes, leaving the notes themselves PKMS-naive, which is a beautiful outcome in the service of surfacing and managing knowledge.

Note that the HyWiki-as-a-sidecar usage pattern is the one that fits my workflow, but HyWiki is flexible about where it resides. The hywiki-directory can even be your siloed PKMS if you choose to operate that way. The value proposition of Hyperbole is the flexibility.

7. HyWiki Makes Writing More Valuable   writing learning

There is a deeper reason I care about keeping the notes themselves frictionless, and it is about the more important writing process, over the less important bookkeeping or filing process that modern PKMSs espouse. After all, writing is a tool for thought.

Paul Graham puts the mechanism plainly: "writing about something, even something you know well, usually shows you that you didn't know it as well as you thought," and, more strikingly, "half the ideas that end up in an essay will be ones you thought of while you were writing it."5

Writing is not the transcription of finished thought, but rather where the thought gets finished. That is the constructivist view of learning. Knowledge is built and curated, not received, and we build it especially well while producing an external artifact6. And generating the words yourself, rather than merely reading them, is also what makes them stick7.

Using HyWiki together with (or instead of) the other PKMSs means the messy stuff gets to stay messy. This is important to me because oftentimes, the learning (or synthesis) doesn't happen when I take notes, but rather when my neurons re-wire. This happens passively during sleep, showers, breakfast, and actively in the curation of my chicken-scratch notes. HyWiki lets the stream of my consciousness flow freely onto the page, and then facilitates the curation of my notes when higher level concepts finally crystallize.

The friction a tool puts between you and the page is harmful. It is a diversion from the activity that actually produces intelligence and knowledge. Most PKMSs are surprisingly distracting here because, before you can write, you are asked to make the note a structured, first-class graph node. And if you want a backlink to show up elsewhere, you have to subsequently wire its connections by hand with link syntax. These context-switches to bookkeeping are cognitively expensive and a disservice to the writing process.

HyWiki removes that friction because it lets you write anywhere in plain text. When a real concept surfaces, you promote it to a curated concept (a WikiWord) with a single keypress, and then Hyperbole maintains the connections for you, forever, no markup required.

In my review of the many contemporary PKMSs, Hyperbole's HyWiki is the lowest-friction way to bootstrap your own intelligence through the precious and indispensable process of writing. It's interesting how HyWiki not only acts as a second brain, but also, by facilitating the process of writing, serves your primary brain as well.

8. Getting Started   hyperbole

Running this code will install and configure Hyperbole and HyWiki. I recommend you add this to your init file.

;; add melpa to get the latest Hyperbole
(add-to-list 
 'package-archives 
 '("melpa" . "https://snapshots.melpa.org/packages/") t)

;; install Hyperbole from GNU ELPA-Devel
(use-package hyperbole
  :config
  ;; enable hyperbole-mode
  (hyperbole-mode 1)

  ;; enable hywiki-mode and make HyWikiWords appear everywhere
  (hywiki-mode :all)

  ;; For contexts where Hyperbole's {M-RET} Action Key is active,
  ;; make it override org-mode's built-in key binding
  (customize-save-variable 'hsys-org-enable-smart-keys t))

Then the Action Key (M-RET) immediately works on implicit buttons like HyWikiWords. Create your first HyWiki page by typing a capitalized word and pressing the Action Key on it.

Read the Hyperbole README and the HyWiki Manual for more information!

9. Further Reading

Hyperbole, originally created by Bob Weiner in 1989, predates the web, and was part of his Master's Thesis on Personalized Information Environments (PIEs) at Brown University.1

Also check out John Wiegley's Using Hyperbole: A Motivation and Ramin Honary's Introduction to Hyperbole, both quoted throughout this post.

Footnotes:

1

Robert Weiner, "PIEmail: A Personalized Information Environment Mail Tool", https://cdn.jsdelivr.net/gh/rswgnu/rsw_publications@a9813b7a/PIEs/PIEmail.pdf.

2

Ramin Honary, "Introduction to Hyperbole", tilde.town/~ramin_hal9001/articles/intro-to-hyperbole.html.

3

The convention predates the web's bracketed link syntaxes. The first wiki, Ward Cunningham's WikiWikiWeb, went public on 25 March 1995 and automatically turned any PascalCase word into a link to a page of that name, creating the page if it did not yet exist: "beginning with WikiWikiWeb in 1995, most wikis used pascal case to name pages." See Pascal case and History of wikis (Wikipedia).

4

John Wiegley, "Using Hyperbole: A Motivation", lists.gnu.org/archive/html/hyperbole-users/2019-01/msg00037.html. Wiegley is a longtime maintainer of GNU Emacs.

5

Paul Graham, "Putting Ideas Into Words", February 2022: paulgraham.com/words.html.

6

Constructivism: the view that learners actively build knowledge rather than receive it, traces to Jean Piaget. Seymour Papert's constructionism sharpens it to the claim that we learn especially well while building a sharable external artifact (Papert, Mindstorms, 1980). Writing is the most portable such artifact.

7

The generation effect describes that information you generate yourself is remembered better than the same information merely read. The classic delineation is Norman J. Slamecka and Peter Graf, "The generation effect: Delineation of a phenomenon," Journal of Experimental Psychology: Human Learning and Memory 4(6), 1978, 592–604.

-1:-- HyWiki: Zero Markup Hypertext (Post Charlie Holland)--L0--C0--2026-07-07T11:41:35.000Z

Andros Fenollosa: An alternative to Webmentions for the Emacs Carnival

The Emacs Carnival collects submissions by hand: you write your post, then notify the host by comment, DM (Reddit, Mastodon...) or email. The IndieWeb solves this with Webmentions, but that requires your blog to send them and the host's blog to receive them, which most static Emacs blogs don't support. Org Social can do better: it is a federated social network, and it can be used to run a carnival without any extra software.

Publish or announce

Add a post to your social.org with the agreed tag emacs-carnival.

** 2026-08-05T10:00:00+0200
:PROPERTIES:
:TAGS: emacs-carnival
:END:

Check out [[https://my-blog.example/carnival-post/][my entry for this month's Emacs Carnival]].

That's it. No endpoint, no microformats, no notification step: one tagged post.

Read the submissions

Client

The first option is a client: org-social.el, or any other client, can filter the timeline by tag. Browse emacs-carnival and you are reading the carnival, no extra tooling involved.

JSON

For scripts and roundups, you can use the Org Social Relay API:

curl "https://relay.org-social.org/search/?tag=emacs-carnival"

It returns a paginated JSON list with the permalink of every submission.

RSS feed

You don't need an Org Social account to follow along. Subscribe from any feed reader:

https://relay.org-social.org/rss.xml?tag=emacs-carnival

Each item includes the author, the content and the permalink.

Conclusion

If you use a static blog and want to participate in the Emacs Carnival, consider using Org Social. It allows you to publish your submission with a simple tag, and others can easily read and follow the submissions through clients, the JSON API, or RSS feeds. This approach simplifies the process and makes it more accessible for everyone involved.

-1:-- An alternative to Webmentions for the Emacs Carnival (Post Andros Fenollosa)--L0--C0--2026-07-07T09:09:53.000Z

Raymond Zeitler: Comment on Org Source Blocks

Yesterday I wrote that I prefer putting each code snippet into its own dedicated file rather than into an org source block because "I don't want the code to execute with org-mode active."

What was I thinking; what does it mean?

Here's a scenario. I'm developing functions to create a document in some markup language. The colorizer function applies a color tag to the first word in a given string.

It works great in my Org source block. But when I add it to the lisp file and invoke it, it doesn't do exactly what I expect.

In Org mode, the single quote is a word constituent, which is fine for my purpose. However, in emacs-lisp-mode, single quote is a separator. The effect is demonstrated below. Note the code in the testre.el file tags only "Sophia," not "Sophia's," while the source block applies the tag as intended.

(defun colorizer (s)
  "Add blue color tag to first word in S."
  (interactive)
  (replace-regexp-in-string "\\(\\w+\\)\\(.+\\)" "blue{\\1}\\2" s t))

(defun testre ()
  "Test colorizer."
  (interactive)
  (message
   (concat
    (colorizer "Blanche lives in Miami.")
    " "
    (colorizer "Sofia's sweater is yellow."))))

(testre)

#+RESULTS:
: blue{Blanche} lives in Miami. blue{Sofia’s} sweater is yellow.

Here's what the code in the testre.el file produces:

  blue{Blanche} lives in Miami. blue{Sofia}’s sweater is yellow.

Another problem with having Org mode active in a lisp source block is that indenting doesn't work as expected. Pressing C-M-\ does nothing (if I'm lucky) or something undesirable. Once I activate emacs-lisp-mode, C-M-\ makes the code look nice again. (If I re-activate Org mode the buffer reloads, and I lose my place in the document.) This makes sense when you recall that C-M-\ is bound to indent-according-to-mode.

I'm sure I'm doing something wrong, or I have something misconfigured.

-1:-- Comment on Org Source Blocks (Post Raymond Zeitler)--L0--C0--2026-07-06T23:14:36.828Z

Sacha Chua: 2026-07-06 Emacs news

Wow, the June Emacs Carnival gathered 22 entries on the topic of Underappreciated Emacs built-ins. Looking for something to write about next? "Programming" is the Emacs Carnival theme for July. Thanks to Ross for hosting in June and Andy for hosting in July. Also, there was a fair bit of discussion about The GNU Emacs Architecture : Unlocking the Core. Have fun!

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- 2026-07-06 Emacs news (Post Sacha Chua)--L0--C0--2026-07-06T16:20:27.000Z

Raymond Zeitler: Emacs Carnival July 2026 -- Programming

I don't think of Emacs as an IDE or even a programming environment unless I'm writing Lisp to automate Emacs. Instead, I consider Emacs to be a text editor with some useful extras.

Syntax highlighting (font-lock-mode) is a nice extra. It might seem strange that I consider syntax highlighting to be an extra. If you've used a text editor 40 years ago, you'll understand why -- text editors didn't have the feature. Syntax highlighting was the purview of the compiler tool. My first experience with syntax highlighting was with Borland Turbo Pascal; I used Borland Turbo C soon after. (I think Borland's Turbo Assembler, TASM, had the feature, too.)

According to packages-selected-packages, these are the programming modes I have configured:

  • basic-mode
  • csv-mode
  • gnuplot
  • matlab-mode
  • powershell
  • python and python-mode
  • Swift mode

I don't program in Swift. Perhaps the package is a dependency for another mode. Or maybe I was ambitious one day and thought I'd try another programming language.

Is csv a programming language? If you automate the generation of spreadsheets. it's nice to have some help. But to be honest, I've not used it very often.

Easy access to shell is another nice extra. I have two functions to make use of it when I write batch files. They execute either the current line (exeln) or the entire buffer (shellfn) with some sort of shell command; they are shown below. The Windows binary uses cmdproxy.exe from MinGW -- it works out-of-the-box. But I use the Windows start program for shellfn because it invokes the correct executable based on the file's extension. Thus, Python runs a .py file; cmd, a .bat; Octave, a .m file.

;; This is bound to C-c x
;; Many thanks to Sebastián Monía for suggesting many improvements
(defun exeln (arg)
  "Execute  current line as a shell command.
With prefix ARG, run asynchronously."
  (interactive "P")
  (funcall (if arg
               #'async-shell-command
             #'shell-command)
           (thing-at-point 'line t)))

;; This is bound to C-c s
(defun shellfn ()
  "Invokes the shell using the current buffer file name as a parameter."
  (interactive)
  (shell-command (concat "start " (buffer-file-name))))

I've tried to use source blocks in Org, but I prefer putting each code snippet into its own dedicated file. Why? One reason is because I don't want the code to execute with org-mode active. Another reason is that indentation doesn't work properly. Switching modes fixes both issues, but it gets annoying very quickly. This is a topic for another post.

I look forward to seeing how other folks use Emacs for programming.

-1:-- Emacs Carnival July 2026 -- Programming (Post Raymond Zeitler)--L0--C0--2026-07-06T02:22:09.964Z

Irreal: Using Emacs Input Methods For Foreign Languages

Protesilaos Stavrou (Prot) has a very nice video on writing foreign languages with Emacs. The main problem for Latin languages are the diacritical marks. Other languages, such as Greek, Russian, and Chinese, have completely different alphabets. In either case, Emacs has us covered.

Prot writes Greek, English, French, and (possibly) Spanish so being able to handle all these languages easily is important to him. His video shows us how he does it. The video is in two parts:

  1. Using the Emacs input method mechanism and
  2. Multilingual spellchecking.

In the first part, Prot demonstrates how to switch between the normal input method—that is, the characters your keyboard normally sends—and another input method that maps to characters from another language.

He uses French as an example and shows one way (the postfix method) to add the diacritical marks that French uses. I use the same method for Spanish and like it. It’s easy and natural. The only thing that takes a bit of getting used to is using an accent character in a non-diacritical way. For example, when writing with the spanish-postfix method, e' results in é but if you want to write something like “he’s” you have to input he''s .

The second part of the video considers multilingual spellchecking. For that, Prot uses jinx, which is pretty much like the normal spell checking in Emacs except you can check multiple languages. It will underline spelling errors as usual and you can have it offer suggestions for correcting those errors.

I wish I’d seen Prot’s video before I started studying Spanish. It would have saved me a lot of time figuring things out on my own. If you have a need to use Emacs to write in another language, you should definitely take a look at Prot’s video.

-1:-- Using Emacs Input Methods For Foreign Languages (Post Irreal)--L0--C0--2026-07-05T14:25:03.000Z

Sacha Chua: Une navigation simplifiée sur mon blog grâce à EWW (et Emacs !)

Quick English translation: You can now navigate my blog with n and p (eww-next-url, eww-previous-url) in the EWW browser in Emacs.

(C'est aussi une traduction en tagalog après la version française pour Amin, qui est en train de l'apprend󠆻re !)

Pour le carnaval d'Emacs en mai, Omar Antolin a écrit un article qui recommande vivement le navigateur EWW sous Emacs. Grâce au commentaire de technomancy concernant celui-ci, j'ai appris qu'il peut naviguer vers la page suivante et la page précédente avec les raccourcis clavier « n » (eww-next-url) et « p » (eww-previous-url) si la page inclut les liens dans son en-tête comme ça :

<link rel="next" href="https://sachachua.com/blog/2026/06/2026-06-29-emacs-news/">
<link rel="prev" href="https://sachachua.com/blog/2026/07/semaine-du-22-au-28-juin/">

J'ai donc ajouté cette fonctionnalité à mon site en modifiant ma configuration du générateur de site statique 11ty. D'abord, j'ai ajouté les données à tous les articles dans mon eleventy.config.js.

  eleventyConfig.addCollection('_posts', function(collectionApi) {
    const posts = collectionApi.getFilteredByTag('_posts');
    for (let i = 0; i < posts.length; i++) {
      const next = i > 0 ? posts[i - 1] : null;
      const prev = i < posts.length - 1 ? posts[i + 1] : null;
      posts[i].data.navLinks = {
        prev: { url: prev?.url, title: prev?.data?.title },
        next: { url: next?.url, title: next?.data?.title }
      };
    }
    return posts;
  });

Ensuite j'ai modifié l'etiquette d'en-tête.

const navLinks = data?.page?.url && data?.navLinks ? data?.navLinks : {
  next: { url: data.pagination && data.pagination.pageNumber < data.pagination.hrefs.length - 1 && data.pagination.hrefs[data.pagination.pageNumber + 1] },
  prev: { url: data.pagination && data.pagination.pageNumber > 0 && data.pagination.hrefs[data.pagination.pageNumber - 1] }
};
const nextLink = navLinks?.next?.url ? `<link rel="next" href="${navLinks?.next?.url}" />` : '';
const prevLink = navLinks?.prev?.url ? `<link rel="prev" href="${navLinks?.prev?.url}" />` : '';

J'avais déjà les liens vers l'article suivant et l'article précédent, il m'a juste suffi de les ajouter à l'en-tête. Si vous lisez un article spécifique sur mon blog, vous pouvez y naviguer de cette façon. Voilà !

output-2026-07-05-07:40:52.gif
Figure 1: Navigating with n (eww-next-url) and p (eww-previous-url) in eww
in Tagalog

Para sa Carnival ng Emacs noong Mayo, sumulat si Omar Antolin ng isang artikulo tungkol sa EWW browser na kasama sa Emacs. Dahil sa komento ni technomancy, nalaman ko na pwede palang mag-navigate sa susunod at nakaraang pahina gamit ang mga keyboard shortcut na "n" (eww-next-url) at "p" (eww-previous-url) kung kasama sa header ng pahina ang mga link katulad nito:

<link rel="next" href="https://sachachua.com/blog/2026/06/2026-06-29-emacs-news/">
<link rel="prev" href="https://sachachua.com/blog/2026/07/semaine-du-22-au-28-juin/">

Gusto kong idagdag 'yung feature na 'to sa site ko. Gumagamit ako ng static site generator (11ty) para gawin yung site ko, kaya dinagdag ko 'to sa kanyang configuration:

  eleventyConfig.addCollection('_posts', function(collectionApi) {
    const posts = collectionApi.getFilteredByTag('_posts');
    for (let i = 0; i < posts.length; i++) {
      const next = i > 0 ? posts[i - 1] : null;
      const prev = i < posts.length - 1 ? posts[i + 1] : null;
      posts[i].data.navLinks = {
        prev: { url: prev?.url, title: prev?.data?.title },
        next: { url: next?.url, title: next?.data?.title }
      };
    }
    return posts;
  });

Pagkatapos noon, pinalitan ko yung header tag ko.

const navLinks = data?.page?.url && data?.navLinks ? data?.navLinks : {
  next: { url: data.pagination && data.pagination.pageNumber < data.pagination.hrefs.length - 1 && data.pagination.hrefs[data.pagination.pageNumber + 1] },
  prev: { url: data.pagination && data.pagination.pageNumber > 0 && data.pagination.hrefs[data.pagination.pageNumber - 1] }
};
const nextLink = navLinks?.next?.url ? `<link rel="next" href="${navLinks?.next?.url}" />` : '';
const prevLink = navLinks?.prev?.url ? `<link rel="prev" href="${navLinks?.prev?.url}" />` : '';

Mayroon na 'kong mga link sa susunod at nakaraang artikulo. Kailangan ko lang silang isama sa header. Kung nagbabasa ka ng isang artikulo sa blog ko, kaya mo nang mag-navigate sa ganoong paraan. Ayan!

output-2026-07-05-07:40:52.gif
Figure 2: Navigating with n (eww-next-url) and p (eww-previous-url) in eww
View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- Une navigation simplifiée sur mon blog grâce à EWW (et Emacs !) (Post Sacha Chua)--L0--C0--2026-07-05T12:56:17.000Z

Meta Redux: Clojurists Together Update: May and June 2026

Some of you might know that Clojurists Together are supporting my work on nREPL, CIDER and friends this year. Normally I send them a bi-monthly progress report, but I saw some other people who got funding for their OSS work publish those reports as blog posts for the broader public and I thought to try this for a change.

The past two months were super productive. I had a lot of inspiration during this period and I managed to tackle a lot of long-standing ideas and issues across the entire nREPL/CIDER ecosystem. Funnily enough, I also managed to grow the ecosystem with a couple of brand new projects, but more about those later.

The big highlights from my perspective:

  • CIDER 1.22 is out
  • CIDER 2.0 is essentially ready and needs more user testing
  • Sayid is reborn
  • Two brand new projects saw the light of day: port and neat
  • Piggieback 0.7.0 is out (and Weasel got modernized while I was in the area)
  • clj-refactor and refactor-nrepl got some love as well

Below you’ll find more details about the work I did, project by project.

CIDER

CIDER 1.22 (“São Miguel”) landed in mid-June, wrapping up the 1.x series. Its main features:

  • a registry for jack-in tools, so third parties can plug new build tools and Clojure dialects into cider-jack-in
  • a “default session” escape hatch from sesman’s project-based dispatch
  • keyword-argument versions of the low-level request APIs, alongside a proper decoupling of the nREPL client layer from CIDER’s UI

It also fixed a long list of small annoyances: severe editor lag in unlinked buffers, several TRAMP and SSH tunnel problems, request id leaks, and a bunch of broken menu entries.

Right after that I switched the development version to 2.0 and most of the planned work is already done. The headline items so far:

That last one deserves a special mention: evaluation results that are images now render inline out of the box, and file/URL results offer their content on demand, six years after the feature had to be disabled over its safety problems. There was also a big cleanup pass: consolidated configuration options, the REPL history browser renamed to cider-history to end a long-standing naming clash, theme-aware faces instead of hardcoded colors, refreshed docs and a regenerated refcard. CIDER 2.0 is available from MELPA snapshots and I’d love for more people to take it for a spin before the final release.

cider-nrepl

Lots of cider-nrepl releases, driving the CIDER work above:

  • cider-nrepl 0.60.0 added the ops backing the new protocol exploration commands (cider/who-implements, cider/type-protocols, cider/protocols-with-method).
  • cider-nrepl 0.61.0 brought ClojureScript test support, a ClojureScript macroexpansion fix, formatting that honors the project’s cljfmt configuration, and a pprint backed by orchard.pp.
  • cider-nrepl 0.62.0-alpha1 and 0.62.0-alpha2 hardened the content-type and slurp middleware (URL scheme allowlist, size caps, graceful fetch errors) and cleaned up the response protocol, which is what made it safe to turn rich content on by default in CIDER 2.0.

Along the way the project’s build was migrated from Leiningen to tools.deps, which required a new MrAnderson release (see the blog posts below).

Orchard

Orchard, the library that powers much of cider-nrepl’s functionality, kept pace:

  • Orchard 0.42.0 and Orchard 0.43.0 continued the inspector polish, added symbol classification to orchard.meta, a programmatic listener API for the tracer, and protocol/multimethod introspection in orchard.xref. The project also moved to tools.deps and its CI now covers JDK 26.

Sayid

Sayid, the omniscient Clojure debugger, had been dormant for years and I finally gave it the revival it deserved:

  • Sayid 0.2.0 was the big modernization pass: new mx.cider/sayid coordinates, a documented nREPL middleware API, a consolidated op surface (37 ops down to 26) and fixes for the most annoying Emacs client breakages.
  • Sayid 0.3.0 followed with usability work: no more frozen Emacs during the reload workflow, simpler query commands and help buffers generated from the keymaps.

port

port is a brand new project I started in May: a minimalist Clojure interactive programming environment for Emacs, built on prepl instead of nREPL. It went from nothing to three releases in the course of the month:

  • port 0.1.0
  • port 0.2.0
  • port 0.3.0, which added eldoc with active argument highlighting, a wire-level message log for debugging and a roughly 10x speedup in handling large prepl responses.

I don’t have any particular plans for the future of this project - it was just something that I wanted to experiment with for a while. I see it as an interesting option for people looking for some middle ground between inf-clojure and CIDER.

neat

neat is the other new arrival: a small, language-agnostic nREPL client for Emacs. neat 0.1.0 has the essentials in place: a pure-elisp bencode codec, a comint-based REPL, and a source-buffer minor mode with eval, completion, eldoc, xref and doc lookup, tested against Clojure, Babashka and Basilisp. It’s early days, but it’s a nice testbed for exercising the nREPL protocol outside CIDER.

This project also means I’ve dropped any plans to try to make CIDER a language-agnostic development environment. Going forward CIDER will focus only on Clojure-like languages, and everything else will be covered by neat.

Piggieback and Weasel

The nREPL org saw some ClojureScript-flavored action:

  • Piggieback 0.6.2 and Piggieback 0.7.0. The 0.7.0 release makes load-file evaluate the editor’s buffer contents instead of re-reading from disk, tears down ClojureScript REPLs when their sessions close (no more leaked Node processes) and surfaces ClojureScript status in the describe response.
  • Weasel 0.8.0 modernized the WebSocket REPL: the client now uses the platform’s native WebSocket, so it runs in any modern JavaScript runtime (browsers, Node 22+, Deno, Bun, workers), and the minimum requirements moved to Clojure/ClojureScript 1.12.

I also backfilled proper GitHub releases for the historic tags of both projects, so their release history is finally browsable.

Improving the ClojureScript support in CIDER has long been a major objective for me, and these small changes were some initial steps in that direction.

refactor-nrepl and clj-refactor

refactor-nrepl got three releases: 3.12.0, 3.13.0 and 3.14.0, the last one making the AST-based indexing much faster and more reliable. clj-refactor.el received a round of maintenance on master as well, and will get a new release after I wrap up the work on CIDER 2.0.

I’m still pondering the future of both projects, as I plan to move the most useful refactor-nrepl features (those that don’t carry a lot of complexity) to CIDER and cider-nrepl eventually, and I’m not sure that the flagship AST-powered refactorings are very competitive these days (compared to clojure-lsp and static project-wide analysis a la clj-kondo in general).

I’ll write a bit more about this and I’d certainly appreciate more feedback from the users of clj-refactor on the subject. It’s funny that I’ve been maintaining the project for ages, but I’ve never really used it (mostly due to its brittleness in the past). I think I managed to address some of the biggest problems recently, but perhaps this happened too late and the project has lost its relevance by now.

Blog posts

I wrote a few articles related to the work above:

Wrapping up

Big thanks to Clojurists Together, Nubank and the other organizations and people supporting my Clojure OSS work! I love you and none of this would have happened without you. Sadly, the amount of financial support my projects receive has eroded massively over the past 4 years and I’ve kind of lost hope that this negative trend will eventually be reversed. It was never easy to maintain many popular OSS projects, but the job certainly hasn’t got any easier or more rewarding in recent years…

Overall, a super productive two months. Hopefully the next two are going to be just as productive, although I have to admit I’ve plucked most of the low-hanging fruit already. Then again, I’ve said this many times in the past, so one never knows…

-1:-- Clojurists Together Update: May and June 2026 (Post Meta Redux)--L0--C0--2026-07-05T08:57:30.000Z

Bicycle for Your Mind: Kosshi Improves

KosshiKosshi

Product: Kosshi - macOS/iOS Outliner
Price: $24.99 from App Store

The developer of Kosshi has been busy. He is working on fixing bugs and incorporating some of the suggestions that are coming from the users of the program. These are seven which got my attention.

Move Content

move comtentmove comtent

You can move content around the program with a keyboard command (⇧⌘M). That is useful because you don’t have to use the mouse/trackpad and drag and drop content to places. Press the keyboard command for Move and choose from a drop down menu of places you can move the content to. Useful when you are writing and the structure needs changing.

Navigate to a Section

navigate to sectionnavigate to section

You can obviously move to a section in the outline you are working in or a section in some other outline by clicking on the sidebar. But there is an easier way now. Press (⇧⌘O) and you get a drop-down list of sections in Kosshi. It also has a preview in the drop-down menu. Type part of the name and the program narrows the options. Select, hit return and you are there. Much easier than monkeying with the mouse/trackpad.1

Update: The default command is ⇧⌘F. I changed it to fit what I am used to. Thanks to Junichi Sato, the developer, for pointing this out.

Command Palette

command palettecommand palette

Kosshi has a command palette (⇧⌘A). Gives you instant access to any command you want with the keyboard command also displayed along with the command. Helps you learn the command and not worry about the command palette. This makes the task of getting familiar with the program easier and also enhances access to the commands you need.

Custom Keyboard Commands

keyboard commands customizedkeyboard commands customized

The keyboard commands are customizable. You can set them to be anything you want them to be. This lets you repeat the commands you are used to from other outlining programs and makes the learning curve in Kosshi smoother. I love it.

Screen Width and the Effect on Full Screen

screen width and full screenscreen width and full screen

Kosshi lets you set the width of the line in your outlines and so now you can specify the size of the Kosshi window to be quite large and have the Editing window be in the middle of the screen with a lot of real estate around it. It is a look I am used to from Ulysses and Emacs. I love it. This also means that the full-screen view for people with larger screens is usable. The lines don’t cover the whole screen and the full-screen view is perfectly usable.

Line Above

line aboveline above

You can now add a line above the line you are on by pressing a keyboard command.

One of the many things I am liking about Kosshi is the ability to interact and work with my outlines without accessing the mouse/trackpad. Makes the act of writing in it so much easier that I can replicate the keyboard commands I am used to from other programs. Like (⌥↩︎) for the new line above which I also have in OmniOutliner.

Move to the Parent of the Row You Are On

move to parent rowmove to parent row

When you are on a row and you want to go to the parent row, you press a keyboard command (⌥⌘↑) to get to it. When you are working on large outlines this command is an useful addition.

Conclusion

It is always a pleasure to see a program evolve, and Kosshi is evolving. The developer, Junichi Sato, is working to improve the product, by both fixing bugs and adding usability features. It is coming along nicely.

I am thrilled with the changes that are happening with Kosshi. It is now the default outliner in my workflow. I love being in it and working with it.

Kosshi is heartily recommended.

macosxguru at the gmail thingie.


  1. As you have probably noticed. I hate monkeying with the mouse/trackpad.↩︎

-1:-- Kosshi Improves (Post Bicycle for Your Mind)--L0--C0--2026-07-05T07:00:00.000Z

Protesilaos: Emacs beginner live stream with @linkarzu on 2026-07-05 20:00 Europe/Athens

Raw link: https://www.youtube.com/watch?v=btAOBkcLEkg

I will do a live stream together with Christian Arzu, a NeoVim user, who is now trying out Emacs. In this meeting we will go over the basics. The idea is that I will do some handholding at this early stage to set Christian up with a basic configuration. We will also take any comments from the chat.

For some background, read my comments on an article that Christian posted the other day about his expectations: https://protesilaos.com/codelog/2026-07-04-emacs-for-beginners-with-linkarzu/. My article also includes commentary on some of the feedback Christian gets for his premium offerings.

-1:-- Emacs beginner live stream with @linkarzu on 2026-07-05 20:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-07-05T00:00:00.000Z

Irreal: Breaking Version Changes Coming To MELPA

Jonas Bernoulli (tarsius) has a long post announcing new channels and versioning on MELPA. Why a long post? And, really, why should we care? It turns out that the process is much more complicated than you’d think it would be and will, eventually, require nontrivial action on the part of MELPA users.

The first question is, Why is MELPA introducing new channels and why are they changing the versioning? The new (experimental) channels are a vehicle for testing the new versioning. The two new channels are Snapshots—that corresponds to the current Regular releases—and Releases—that corresponds to the current Stable releases. The plan is for these new channels to replace the Regular and Stable channels.

The reasons for changing the versioning are technical and you can read about them in Tarsius’ post but the reason we care is that when the official switchover happens, MELPA users are going to have to take certain actions to ensure their ELPA distributions are consistent with the new versioning. Otherwise, the new versioning could make a new version of a package look older than previous versions. Doubtless, when the time comes the MELPA team will provide a script to automate the changeover.

As I said at the beginning, there’s a lot of complicated details involved so you should definitely read Tarsius’ post. He recommends that you get in front of things by changing to the new channels now. His post explains everything you have to do. The official changeover probably won’t be for another year so you have time to prepare.

Update [2026-07-04 Sat 13:33]: Fixed title typo.

-1:-- Breaking Version Changes Coming To MELPA (Post Irreal)--L0--C0--2026-07-04T13:56:12.000Z

Meta Redux: Projectile 3.1

Hot on the heels of Projectile 3.0 comes Projectile 3.1!

Three days apart, yes. There’s a story there. A big chunk of what’s in 3.1 was originally meant for 3.0, but 3.0 was already turning into a monster of a release and I decided to cut it into two, so I’d actually be able to reason about each of them. So think of 3.1 less as “the next release” and more as “the second half of 3.0 that I was too scared to ship all at once”.

There was also a bit of calendar mischief involved. I really wanted a release on the 1st of July - that’s July Morning, which is a bit of a thing here in Bulgaria1 - and then another one on the 4th of July. Partly because it’s the 250th US Independence Day, and partly because it happens to be my wedding anniversary. When else am I going to get a round number like that to line up? So here we are.

Unlike 3.0, 3.1 has no breaking changes. Nothing was removed, no command or option changed its name. What it does instead is knock out a pile of long-standing ideas and feature requests, most of which pull in the same direction: making Projectile leaner under the hood and a lot more extensible from your own config.

Projectile learns what you actually work on

projectile-find-file now ranks the files you visit most often and most recently at the top of the completion list. It’s the kind of thing you don’t notice until you go back to a Projectile without it and suddenly your muscle memory is off. It works with any completion UI (Vertico, the default one, whatever) and under every indexing method, and it’s on by default. If it’s not your thing, set projectile-enable-frecency to nil.

Named project tasks

The six lifecycle commands (compile, test, run, …) were never enough for real projects, which tend to accumulate a dozen little “run this incantation” commands. So there’s projectile-tasks now - an arbitrary set of named commands you can attach to a project. The nice part is you can put them in .dir-locals.el and share them with your team through the repo:

((nil . ((projectile-tasks . (("lint"   . "make lint")
                              ("deploy" . "make deploy STAGE=prod"))))))

Then s-p c x (projectile-run-task) prompts you for a task and runs it, with a prefix argument if you want to tweak the command first. This one subsumes about four separate feature requests I’d been staring at for years.

Finding files by kind

This is my favourite one. Lots of frameworks organize files into well-known kinds - Rails has models, controllers, views and helpers; Django has models, views and urls; and so on. Projectile can now describe those kinds declaratively, and it gives you two commands for free: s-p j to jump to a file of a particular kind, and s-p J to hop between related files (from a model to its controller to its views and back).

The best part is that it’s not hardcoded. Rails and Django ship out of the box, but the whole thing is driven by a :file-kinds entry on the project type, so you can teach Projectile about your own framework’s layout in a few lines. This is the “more extensible” theme in a nutshell - I’d much rather ship a mechanism than a hardcoded list.

Running the test at point

If you’re on Emacs 29+ with tree-sitter, s-p c . runs just the test your cursor is in, rather than the whole suite. It figures out the enclosing test from the parse tree and builds the right command. pytest, go test and jest are supported out of the box, and - you guessed it - you can register rules for other test runners yourself.

Other-window and other-frame, the Emacs way

Instead of a small army of -other-window/-other-frame command variants, there are now s-p 4 4 and s-p 5 5 prefixes, modeled exactly on Emacs’s own C-x 4 4 / C-x 5 5. Press s-p 4 4 and the next Projectile command opens its buffer in another window - even commands that never had a dedicated variant. The old commands still work, of course.

More solid under the hood

A few things got quietly sturdier:

  • projectile-invalidate-cache has always been Projectile’s most notorious footgun. If you opt into projectile-auto-update-cache-with-watches, Projectile watches your project and keeps its file cache in sync as files come and go, so you rarely have to think about invalidation at all. It’s off by default and deliberately conservative - when in doubt it just rebuilds.
  • The glob patterns in your dirconfig now follow .gitignore-style rules, and they behave identically under native and hybrid indexing. This kills a whole family of “why is this file showing up” bug reports that go back the better part of a decade.
  • Project and project-type detection now probe marker files with a single directory listing instead of a stat per marker, which turns dozens of sequential round-trips into one over TRAMP.

I should have done all of those a long time ago, but better late then never, right?

Wrapping up

There are no breaking changes, but a handful of defaults and behaviors did shift (auto-discovery is on by default now, for one), so give the Upgrading to Projectile 3.1 guide a quick read before you upgrade. The full list of changes lives in the changelog, as always.

With 3.1 out the door, Projectile is basically where I always wanted it to be. I’ve got a few ideas for follow-up releases, but the pressure is off - this is the release where the big items I’d been carrying around for years finally got done. Another win for my burst-driven approach to maintaining my projects: nothing happens for a while, and then a whole lot happens at once.

Keep hacking!

  1. July Morning is a Bulgarian tradition where people head to the Black Sea coast to greet the sunrise on the 1st of July. It started as a hippie / rock-and-roll thing in the 80s and stuck around. Highly recommended, if you ever get the chance. 

-1:-- Projectile 3.1 (Post Meta Redux)--L0--C0--2026-07-04T05:30:00.000Z

Wai Hon: One-Line-Per-Day Diary in Org-mode

Recently, I came across these plain text systems and concepts:

They resonated me because they match very closely with how I keep my daily diary in Org-mode!

The One-Line-Per-Day Format

I keep my diary in a one-line-per-day format. Under this system, each day is represented by a single line using Org-mode’s description list syntax:

  • Level-1 heading: The Year (e.g., * 2026)
  • Level-2 heading: The Week, containing the week number, date range, and a brief weekly highlight (e.g., ** W25 (06-15 - 06-21) - Weekly highlight)
  • List items: One line per day, starting with an inactive timestamp and a description separator (::) followed by the day’s events.

Here is an example illustrating a complete recent week:

* 2026

** W26 (06-22 - 06-28) - Run 3 times. Mele X1! CPU PL1/PL2/Multpilers. TPM: LUKS + SSH.

- [2026-06-22 Mon] :: Team dinner.
- [2026-06-23 Tue] :: Running. Daycare fire false alarm. Primes day Mini PC research.
- [2026-06-24 Wed] :: Buy Mele X1.
- [2026-06-25 Thu] :: Running. Benchmark Mele X1. Good single core performance. Emacs startup time ~0.4s.
- [2026-06-26 Fri] :: Bike to pick up. Dinner: YAYOI.
- [2026-06-27 Sat] :: TIL: Performance Copilot (pcp). Arch: migrate to systemd + sd-encrypt. TPM: LUKS unlock + SSH Agent.
- [2026-06-28 Sun] :: Running. Order Square Paper. Termux workaround with non-local network.

Daily Entries

At the end of every day, I go to the top of diary.org, add a line for today with the things that worth remembering.

To make writing daily entries frictionless, I use a simple Emacs Lisp helper. I use the following function to insert today’s date formatted as an inactive timestamp:

(defun my/insert-current-date ()
 "Insert today's date in Org inactive format."
 (interactive)
 (insert (format-time-string "[%Y-%m-%d %a]")))

I bind this command to a key combination for quick access:

(keymap-global-set "C-c s d" #'my/insert-current-date)

With this helper, starting a daily log is as simple as typing - (or using M-RET to start a list item), calling the function, typing ::, and entering the daily highlight.

Weekly Highlight

At the end of each week (Sunday), I pick the things worth highlighting from my daily entries and put them in the weekly heading. This allows me to look back at my past weeks, know what happened, and feel like my days count.

Why This Works Well

This format has greatly improved both writing and searching:

  • Self-Contained Search Results: Since the date and the content of the entry reside on the same line, searching with consult-line or ripgrep returns the full context directly. A search for a keyword like “sourdough” immediately displays the date it happened without needing to check parent headings.
  • Compact File Structure: The outline remains clean and fast to fold and navigate, with only weeks and years as headings.
  • Lower Friction: Writing a single line per day lowers the cognitive barrier to journaling. Even on busy days, it is easy to jot down a quick summary.

I am also using a diary-work.org to track my daily work similarly.

Conclusion

This layout is a lightweight and sustainable way to keep a daily log. By using Org-mode’s description lists and automating timestamp insertions, the diary remains clean, highly readable, and easily searchable for years to come.

Appendix: Screenshot

This is how the diary.org looks like:

-1:-- One-Line-Per-Day Diary in Org-mode (Post Wai Hon)--L0--C0--2026-07-04T03:00:00.000Z

Protesilaos: Emacs for beginners with Christian Arzu (@linkarzu on YouTube)

This Sunday I will do a live stream with Christian Arzu. The details are not finalised yet, so I will make the announcement separately.

Christian is a NeoVim user who is Emacs-curious. He is trying out Emacs and needs someone to guide him at this early stage, which I am happy to do.

Last year I had a ~2.5-hour interview with Christian Arzu from the @linkarzu YouTube channel: https://protesilaos.com/codelog/2025-08-01-linkarzu-chat-emacs-neovim-philosophy/.

What follows are some comments on Christian’s article Expectations from Prot’s Emacs Coaching Sessions (2026-07-03).

Install Emacs and what version to use on the MacOS

The premise of our meeting is that I will do the handholding. You will thus install the emacs-plus package version 30. There are always a zillion excellent options, though handholding demands that we pick one.

Emacs 30 is the latest stable version of Emacs. Emacs 31 will be released in the near future, at which point you can upgrade.

In general, Emacs is a very conservative project (which I think is the right approach): there are no major breaking changes between versions, so even an older stable version will most probably be perfectly fine.

Vanilla Emacs or Doom Emacs?

Vanilla 100%, which is why there is no “Prot Emacs” distro. You have to try this in earnest for at least one week.

The disruption vanilla Emacs creates is good for you long-term:

  • it forces you to slow down;
  • it makes it clear that your old habits do not apply;
  • now you have to gradually learn from the ground up.

I come from a Neovim background, so is Doom Emacs a good choice or not and why.

Doom Emacs is fine in its own right: it takes care of everything. With vanilla Emacs you have to piece together a system that works for you, so you will better understand what each part does and why it is there.

This will, in turn, empower you to make more informed decisions as you continue in your Emacs journey. Plus, you will be exposed to more Emacs Lisp, which is what unlocks the superpower of Emacs’ extensibility.

Tutorial and basic keys

So should I do the whole thing or should I start with a few basics and go from there. Like a cheat-sheet or something?

The tutorial is mandatory. Do it again if you have to.

Other than that, you are on a custom keyboard which has the potential to make things much easier for you.

  • I assume you already have the arrow keys available at some convenient spot (mine are LAYER + h, j, k, l (qwerty layout)).
  • Bonus points is you also have Home, PgDn, PgUp, End bound somewhere (mine are LAYER + y, u, i, o). Those are enough for basic motions.

Additionally, make sure you can access the Ctrl and Alt (Meta) keys. Emacs relies on them for practically everything.

What do I want Emacs for?

I still don’t know what I want, but I think that I want to use the life organizing abilities that Emacs provides through org.

I think Org is a good reason to start with Emacs.

Writing your tasks and notes in Org will also help you practice general skills, such as to open a file, create a bookmark, switch to a buffer, move around to edit text, search/grep for something.

Beside that, you want some core functionality that improves every aspect of Emacs. This concerns the minibuffer, which is the interface where you can provide input such as to choose a command to run, open a new file from anywhere in the filesystem, switch to a specific buffer, and more.

If you commit to using Emacs for Org, you will find other interesting uses, such as to have your RSS reader in Emacs. When you read an RSS entry, you can link to it with Org, so you can create a task/note about it. Same idea for eventually integrating your email in Emacs: you use Org to connect messages you receive to appointments, for example.

And if you have done all that, well, you will be searching for reasons to bring your programming and virtually everything text-related into Emacs. You will understand why it makes sense to do things this way.

You’re a fraud, why do you ask for money, isn’t YouTube Ads enough?

This is about Christian’s premium offerings for which he got backlash:

I do 100% agree that I owe these interviews to my guests, I can’t thank them enough. Prot and many of the other guests have dedicated years of their life into researching and figuring stuff out, to then share it with all of us in an easy way to understand. That is just priceless, all of that work and dedication, that is usually tied to paid education and years of experience, just being shared with us. So I admire and have so much respect for the guests.

Christian does a lot of decent work with those videos. If you have watched any of my videos, you know that their production quality is poor, the audio is awkward, and even my hairstyle is unpredictable! 🙃 Christian’s videos are high quality. The guests, where relevant, are also carefully chosen, making for insightful conversations.

For example, in my interview with Christian I had the opportunity to cover many topics that I would otherwise not mention the way I did. This is no mere repackaging of information: Christian helped me share something new in a format that is fun and approachable.

Even without guests though, these are publications about tools we care about. It is a nice, casual experience. We have something to discuss and we share our interests together.

In short, Christian makes a positive contribution to the community.

I am in favour of Christian asking for donations and even paywalling some of his contributions. Same for everybody else. Nobody is entitled to content and the free stuff out there is already more than enough.

Emacs is a great piece of software. Though what makes it even better is the community around it. Through the community we preserve knowledge and get extensions for Emacs that make our computing life a bit better.

I personally dislike the snake oil merchant, the one who promises magical solutions to fix your life. But even then, the real work is to encourage people to be prudent by applying judgement. Denouncing someone is not constructive long-term: they do what they can while we focus on doing the work we care about.

That granted, learn to ignore negative people (which can also be the naysaying inner voice). They will complain if you are arrogant. They will complain if you are modest. They will ridicule you if you are creative. They will ridicule you if you are conventional. They will call you names if they find you pretty. They will call you names if they find you ugly. They have nothing better to do, whereas you have plenty of things to contribute. The best middle finger is consistent, high quality work—and this is, paradoxically, how the negative person provides the impetus for something decent to happen.

-1:-- Emacs for beginners with Christian Arzu (@linkarzu on YouTube) (Post Protesilaos)--L0--C0--2026-07-04T00:00:00.000Z

Vineet Naik: Controlling a YouTube video from emacs

Last week I shipped a YouTube section looper in Captrice, the guitar practice app I'm building. It's meant for learning songs by ear — you mark a section of the video to loop, play/pause, rewind as well as adjust playback speed. Besides being a looper, it's also a keyboard-driven controller for the YouTube video player embedded in the app so that you can operate it easily while playing guitar. Here's a short preview:

That's not what this post is about though. Building this functionality in Captrice made me wonder if it'd be similarly possible to control a YouTube video playing in a browser from inside emacs using keybindings. I have a simple use case — I use org-roam to take notes while learning. If the learning material is a YouTube video, I split the screen with the browser on one side and emacs on the other. The problem is that pausing or rewinding the video involves switching to the browser window, interacting with the player, and then switching back (and the same steps in reverse to resume). On MacOS the media controls on the touch bar come in handy for this use case. It's the one thing I've genuinely miss since moving to Linux.

Now if you know emacs, you can bet someone must have managed to get YouTube videos playing inside the editor. And sure enough, there are packages that turn emacs into a media player. But getting them to play YouTube videos requires additional dependencies (youtube-dl, mpv etc.) that I'd like to avoid if possible. I'm happy with the video playing in the browser as long as it can be controlled from emacs.

Turns out, it's quite trivial to achieve this. Most modern browsers on Linux implement the MPRIS2 D-Bus Interface out of the box. MPRIS2 stands for Media Player Remote Interfacing Specification 2.0. To put it simply, it's a standard Linux D-Bus interface through which a process can control an MPRIS-compatible media player running in another process.

When a web browser plays media, it registers itself as an MPRIS2 player on D-Bus and exposes its controls. The player can now be controlled from another process by sending messages over D-Bus. playerctl is a command line controller that makes this straightforward. For e.g. the following command will pause the video in the browser (if it's playing) and resume (if it's paused).

1
playerctl play-pause

Here's another handy command to rewind the video by 5 seconds

1
playerctl position 5-

There's however a bug (actually in YouTube's media player and not playerctl) that causes it to disregard the offset and always seek (ahead or behind depending on the sign) by 5s. This means the following command will also seek behind by 5s despite 15- being specified as the offset

1
playerctl position 15-

Thankfully, the position command accepts an absolute position (without the sign) too as input, so it's easy to work around this bug by doing the calculation ourselves.

1
playerctl position $(echo $(playerctl position) | awk '{v=int($1)-15; print (v<0)?0:v}')

playerctl position without any further argument returns the current position as a float. The awk command converts it to integer, subtracts 15 from it and clamps the result to 0 in case it's negative.

Coming back to emacs, all that's left to do now is to implement interactive functions that shell-out to playerctl and define keybindings for the same. We could also implement a MRPIS2 controller in elisp but it seems like an overkill.

My emacs config looks something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(defun my/youtube-play-pause ()
  (interactive)
  (shell-command "playerctl play-pause"))

(defun my/youtube-rewind ()
  (interactive)
  (shell-command "playerctl pause && playerctl position $(echo $(playerctl position) | awk '{v=int($1)-15; print (v<0)?0:v}') && playerctl play"))

(define-prefix-command 'my/media-map)
(global-set-key (kbd "C-c m") 'my/media-map)
(define-key my/media-map (kbd "p") 'my/youtube-play-pause)
(define-key my/media-map (kbd "b") 'my/youtube-rewind)

Notice that the playerctl position command is wrapped in pause && … && play. I've observed that it works more reliably this way.

The youtube-rewind function can be parameterized to take the offset as input. But I have kept it simple for now and may add support for more control later if needed.

Here's a quick demo:

Due to the absence of audio, it's not immediately clear that the YouTube video on the left is indeed playing in the beginning. But you can follow the keybindings that appear in the right-bottom part of the screen as I type them. Also notice that emacs is always the active window for the entire duration which means I never switch to the browser window.

And thus, it's so much easier to take notes while learning from a video without interrupting the flow.

-1:-- Controlling a YouTube video from emacs (Post Vineet Naik)--L0--C0--2026-07-03T18:30:00.000Z

Irreal: Automatically Signing And Encrypting Email With Mu4e

Nicolas Cavigneaux has an excellent post on signing and optionally encrypting emails sent with mu4e. The idea is to sign each email you send and if you have a GPG key for every recipient, encrypt it was well. As Cavigneaux says, it’s pretty easy to sign and/or encrypt messages with mu4e but you have to remember to do it and most people, including him, don’t always remember.

The solution, of course, is to automate the process so that you don’t have to remember. That turns out to be pretty easy to do. Cavigneaux has the necessary code in his post. The basic flow is to check if you have everyone’s encryption key and, if so, encrypt the message. Regardless of whether or not everyone’s encryption key is available sign the message. There are calls available for both the signing and encryption. The process is kicked off with the send-message-hook every time you send a message.

There was a time when encrypting all your messages was the thing to do but difficulties with key distribution have pretty much put an end to the encrypt all emails movement. Most people don’t have keys and those who do will probably be annoyed to receive mundane emails that are encrypted. For those sensitive emails being sent to people you know have keys, you pretty much automatically remember to encrypt them.

That leaves signing. It mostly doesn’t hurt anything1 so it probably doesn’t matter if you always sign your messages, especially if you have reasons to suspect that their contents might come into dispute. I’ve stopped doing even that because, as I say, no one has any idea what it means.

So my takeaway is that if you’re paranoid about certifying the content of your emails or you regularly deal with people who want to ensure themselves that they’re communication with you, it may make sense to automatically sign your emails. Except in special circumstances, I don’t see any reason to encrypt all your emails.

Footnotes:

1

Although I do remember being queried about those unreadable binary blobs at the end of my emails.

-1:-- Automatically Signing And Encrypting Email With Mu4e (Post Irreal)--L0--C0--2026-07-03T15:06:33.000Z

Sacha Chua: Re: React to Sacha and Prot Newbies and Starter Kits Emacs Video - linkarzu

: All right, quick video form of this post is at Yay Emacs 35: Reacting to Linkarzu's reaction to my video with Prot about newbies and starter kits - YouTube in case anyone wants. That's how it works, right? =)

Hey hey hey, now I'm linkarzu-famous. =) Linkarzu (Christian Arzu) posted a reaction video to the first part of YE24: Sacha and Prot Talk Emacs - Newbies/Starter Kits. Here's his vid:

YouTube might be holding my comment for moderation because I tried to add too many links to it. I also realized my timestamps were off in my YT comment, so here it is along with other stuff I've just added.

On my Newbies/Starter Kits chat with Prot

It's definitely more of a meta-discussion (how can we make the newcomer experience better?) than something directly focused on helping newbies, but I hope you're getting something out of it. Think of it like a live coaching session for me so that I can figure out what to prioritize on my TODO list to make the newcomer experience better, with some ideas and questions thrown out there in case other people want to work on things too.

Learning Emacs in order to organize your life

02:20:13 What do I want to do with Emacs? I think, like I've said it before, organize my life a little bit better. Try Org, pretty much. I don't care about Emacs for editing Markdown files. That wouldn't make sense, you know, because I can do that in Neovim quite well. And I don't want to replicate that in Emacs. That's just going to be a waste of time. Something that I don't do in Neovim. This is something that the professor said as well: Just use Emacs for something that you don't do in Neovim right now.

If you want to learn Emacs so you can use it to organize your life, there are more direct paths than my video about newbies/starter kits. You might be just fine with the basic tutorial (Ctrl+h t within Emacs), the Org Mode compact guide, maybe another Org Mode tutorial that matches the way you think, and some time experimenting with the basics until you figure out the kinds of things you'd like to improve. The idea is to quickly get to the point where this is useful, and then start using some of the time/energy saved to learn more. A simple progression might start with something like this:

  • Opening: Use Emacs to open and close your todo.org. No keyboard shortcuts needed, just open the file, type, and use the toolbar or menu bar to save. If you're in a console Emacs and you don't want to use the mouse, you can use F10 to open the menu.
  • Leaving Emacs open: Realize you can save time by just leaving Emacs open with the file instead of opening/closing it all the time. This is probably more of a mindset change for Vim users. Set up your window manager so that you can switch to Emacs with a convenient keyboard shortcut. I use super+1.
  • Themes? If the default theme gets on your nerves, figure out how to change it. M-x customize-themes is a good starting point. If you're not sure what that means, go through the tutorial (Help - Emacs Tutorial).
  • Keyboard shortcuts: Get annoyed with using the toolbar or menu bar to save. Get the hang of C-x C-s (save-buffer). Start to get your mind used to the idea of keyboard shortcuts being different in different apps. Try not to give in to the temptation to make this C-s like in other apps. C-s is isearch-forward, which you will probably eventually find really useful, and if you move that you will end up needing to move whatever you are moving it to. Use sticky notes to remind yourself of the handful of keyboard shortcuts you're learning.
  • Basic Org Mode tutorial: Read an Org Mode tutorial, maybe this one. Start with * TODO ... headings. You can manually type them. Change TODO to DONE. Again, this can be pretty manual.
  • Org markup: Learn how to open links, make subheadings (**, ***), etc.
  • Shift: Get annoyed with manually typing TODO keywords. Experiment how to use shift left and shift right. (Might not work on console Emacs, depending on what keyboard shortcuts your terminal supports.)

Feel free to switch steps around depending on where the friction is. Depending on what you want to do from here, you might want to learn about scheduling things and displaying an agenda or setting up capture (which gets even more useful as you do more things within Emacs, since it can automatically pick up links to whatever you're looking at).

Also, along the way, it could be worth flipping through the StarterKits page on the EmacsWiki to see if one of those options matches the way you think (totally optional), or maybe chat with Prot so he can help translate what you want into the keywords you can use to find stuff or the priority to learn things in. Meetups are great too.

Learning Emacs with people

  • 00:33:20 I need a daddy that holds my hand and guides me through the process. I'm lost. And chat is even way looser. Even more lost, you know, because they're like, try Doom, try Evil and try this and try the other one, you know, and run Neovim inside Kitty and no, run Emacs inside Kitty or no, use the GUI. No. So it's pretty confusing.
  • 01:28:45 You know what we should do, okay? We should pay Prot for his coaching sessions. He's in Greece, right? He's in Cyprus. To give something back to him. But I don't know if he's okay in transmitting this live because all of these cheap ass MFers watching that would be watching this live stream will not pay Prot. So that's bad business, brother, because the live streams are just going to stay there. But if I do it for myself, if I pay to him, we have the one-on-ones and I don't post them and I learn Emacs, you know, that is not… That's going to leave you guys behind.

Yup, mentorship/coaching is totally a great way to learn Emacs. Prot is okay with people livestreaming or posting a recording of the coaching session. This livestream is actually one of those instances - I set it up as a coaching session with him! =) Amin Bandali has also posted some of his sessions with Prot (FFS code review and Emacs extensibility with Protesilaos, FFS code review with Protesilaos - bandali). I love your recent livestreams about exploring Emacs. Learning out loud is fantastic. It lets other people help out, and you help lots of people along the way. If you're comfortable with the idea, I think livestreaming or posting a recording of a coaching session with Prot would be wonderful. You've mentioned wanting to use Emacs to organize your life, so it's of course totally okay to chat privately. That way you don't have to worry about leaking any private information. Either way works!

This thing about balancing learning from resources and learning from people is an interesting one to think about. On one hand, we don't want a flood of generic requests from help from people who haven't bothered to look things up for themselves. On the other hand, because Emacs is so large and so many things are possible (and also oddly-named), it really helps to be able to talk to people. It's like the way you could learn how to play the piano or speak a different language by yourself, but a piano teacher could help you pick the right pieces for your level, and a tutor can help you with the nuances and pronunciation feedback that a dictionary or a textbook can't. I think learning how to learn from both resources and people is definitely a good skill worth working on during the early days, which could include:

  • taking notes and sharing them - great way to solidify your knowledge and pay it forward
  • learning how to skim tutorials and references to pick up ideas and terminology without feeling like you're progressing too slowly
  • learning how to break the things you want into bite-sized chunks so that they can actually fit into your brain; use sticky notes and text files to help you
  • connecting with people, learning how to ask questions

Reddit links

00:35:32 I'm not an Emacs user. I'm an Emacs, I don't know, tester or trier, whatever. If I come to the Emacs subreddit, for me, it's just a waste of time because I will not be able to find anything. I don't know where to find stuff. Maybe if I go to the about page, but who does that? Okay. Who in their sane mind comes here to the about page?

… Getting started, maybe here. Get Emacs. Emacs resources. Okay, but if I go to the EmacsWiki… How to edit Elisp area. Like there's a thousand links here and that is just the first link, brother. Then I have this other one. WikEmacs. OK, I trust it, so I'm just going to hit continue. WikEmacs.org. That one doesn't even load. Emacs reference. Not found. OK, so you see what I'm saying? This one has a thousand links and I just by looking at the amount of them, I'm like, nope, don't want to look at that. Maybe this one. The book, OK, the book, this is the book that Aaron recommends.

… So I think the suggestion that Prot is sharing there is quite useful, to be honest. Have them pinned here. I have something like that in my subreddit, you know, things that I want people to see when they get there. So when they get to the server, the first thing that I want them to see is this: The Discord information that the podcast has been moved to a different YouTube channel. This is useful as well. … So yeah, this is actual information that I want people to know. If those would be updated on the Emacs subreddit, I think it would be a good idea.

Oooh, good catch. I've messaged the r/emacs mods about the dead links, suggested direct links to Emacs Newbie and Starter Kits on the EmacsWiki, and suggested adding Doom Emacs and Spacemacs if Better Defaults is listed. The sidebar isn't very visible (most people miss it, especially on mobile), but every little bit helps. Usually what happens is a newbie posts about their question on r/emacs and someone replies with some helpful resources. An Automod or a sticky might help. I sent your video + timestamp to the r/emacs mods.

Timestamps

  • 44:01 So how does she add those timestamps? … Oh, she typed some magic there. She typed something and then a timestamp was added.
  • 01:30:25 We could see what she typed there. Let's see. Is she in normal mode? Does she use modal navigation or something? Let's see. OT. She typed OT and then she… OT and then she presses a key, which probably expands the snippet or something.

I have an abbreviation "ot" that expands to the timestamp after I press space, comma, other punctuation, tab, whatever. This is convenient for me to type because it's home-row on Dvorak. Here's the relevant part of my config:

(setq-default abbrev-mode 1)
(define-abbrev global-abbrev-table "ot" ""
  (lambda () (insert (format-time-string "[%Y-%m-%d %a %H:%M]"))))

Big picture: I added this abbreviation for timestamps because I wanted a quick way to keep track of highlights, things to clip, possible chapter markers, etc. I could calculate it as a relative time using org-timer (there's a built-in feature), but wall-clock time is easier to use in calculations in case I want to adjust it later on. So, for example, I now have a little bit of code (sacha-stream-org-convert-timestamps-to-youtube-offsets in my config) that replaces all the timestamps in a selected region with the offsets based on the start time of the livestream that includes those timestamps. I can export the selection into a plain-text format that I can paste into the YouTube video description for quick chapter markers. Then I can bulk-add comments with those timestamps into the VTT transcription produced by WhisperX (subed-vtt-insert-chapter-comments in subed), move them earlier or later to match the actual times, copy the corrected chapter markers into YouTube (subed-section-comments-as-chapters), and use those chapter markers when publishing the transcript (using Org Mode and a custom link type). This is because I don't usually have the patience to listen to my whole video again and I don't expect people to have the patience to listen to my whole video either, so I want people to be able to quickly jump to the parts that might be interesting for them. =) I'm not sure this is a workflow you can easily pick up if you're starting from scratch (… haven't confirmed that it actually works for anyone other than me…), but I'm mentioning it to give kind of the big picture of why I have that snippet and what else it enables. Because Emacs!

Timestamps are very handy. I even have some code that schedules a YouTube livestream for the Org timestamp at point (sacha-stream-org-schedule-livestream-for-entry-at-point), using the title and body of the Org subtree and uploading the thumbnail from the Org entry :THUMBNAIL: property (or a default property). It inserts the YT embed. I have another function for setting up a Google Calendar entry so I can invite the guest (sacha-emacs-chat-schedule). I mess up times and timezones all the time, so the less I have to manually click on stuff, the better.

My evil plan

1:31:36 Now why is she interested in doing all this, brother? This is a pretty good person, actually. Why is she so concerned about the experience for newcomers in Emacs? On the Neovim side of things, it's like, brother, you're just on your own. "F* yourself, go and watch some videos, and if you get it, awesome." Now, there's really amazing people as well on the Neovim side of things. I'm just talking shit, but I'm honestly curious, like… She's really concerned about new people joining into the Emacs church. Is this a church really? Like, okay, do we have to pay after once we're part of the church? Like, do we need to give like 10% of our income to the Emacs church?

Hah, it's all part of my Evil Plan. (Not to be confused with evil-mode.) Sure, Emacs isn't a good fit for everyone. I think the people who seem to really click with it and with other people who use it are the ones who enjoy tinkering and who can (mostly) find the balance between getting stuff done and tweaking their setup. =) If this might be your jam, I hope you can get past the initial hump and get to the point where it gets to be fun and useful! Sometimes it takes several tries for it to stick. We have lots of stories of people who didn't get Emacs the first time around, but who eventually figured it out later. I love that there are so many people who've used Emacs to make a TODO system that actually works for them. (It's usually Org Mode, but sometimes it's something else, that's all cool.) I love that people can do little tweaks to remove friction or make new things possible step by step.

So, the evil plan:

  1. If people learning Emacs can connect with resources and people who can help them enjoy figuring things out, then…
  2. they'll get to the point where they can come up with ideas and make things better for themselves.
  3. This often turns out to be useful for other people too,
  4. and then people can bounce ideas around and make things even better.
  5. So, years down the line, I'll want to do something crazy with Emacs and someone will already have written a function for doing it. ;)

See? I'm just planning ahead. Bwahaha! Also, I love seeing the kinds of cool things people come up with and share, even if I might not personally need it (yet). It's fun. I hope you get the hang of it. I think that could lead to lots of interesting conversations. Even if you decide to use something else, that's cool too. The important thing is that you're figuring out stuff that works for you! =)

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- Re: React to Sacha and Prot Newbies and Starter Kits Emacs Video - linkarzu (Post Sacha Chua)--L0--C0--2026-07-03T12:24:23.000Z

Emacs Redux: Automating Emacs Screenshots

I maintain a bunch of theme ports these days: Zenburn, Solarized, Tokyo Night, and most recently Batppuccin. Every one of them wants screenshots in the README, ideally one per variant, so people can compare the flavors at a glance.

I’d been putting this off forever, because taking the screenshots by hand is such a chore. Load a theme, resize the frame, arrange a nice-looking buffer, take a screenshot, crop it, then repeat for every single flavor. And the results are never quite consistent: the font is a little different, the window is a slightly different size, the crop is off by a few pixels. Multiply that by four flavors across several themes and you can see why I kept finding better things to do.

So recently I finally did what I should have done from the start and taught Emacs to take the screenshots for me. What started as a five-minute hack turned into a surprisingly deep little rabbit hole, so I figured it was worth a write-up.

The core idea is simple: spin up a throwaway emacs -Q, load the theme, show a sample buffer in a frame of a fixed size, and capture just that window. Because every step is scripted, the screenshots come out identical in layout, and regenerating the whole set after a color tweak is a single command.

Four Batppuccin flavors, generated automatically

A clean, disposable frame

The first half of the job is pure Emacs Lisp. I want a frame with no distractions (no tool bar, menu bar, or scroll bars), a nice font, a fixed size, and of course the theme loaded:

(setq inhibit-startup-screen t)
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq-default cursor-type nil)          ; hide the cursor for a clean shot
(set-face-attribute 'default nil :family "Fira Code" :height 150)

(add-to-list 'custom-theme-load-path "/path/to/theme")
(load-theme 'batppuccin-mocha t)

(set-frame-size (selected-frame) 92 36)
(find-file "sample.el")

I load all of this into a throwaway Emacs:

emacs -Q --eval '(load "setup.el")'

A couple of things bit me here that are worth calling out:

  • emacs -L some/dir puts a directory on the load-path, but load-theme searches custom-theme-load-path. Those are two different lists, so remember to add the theme’s directory to the latter.
  • If your sample file lives in a project with a .dir-locals.el, opening it can pop up an “unsafe local variables” prompt that blocks the whole script. Setting enable-local-variables and enable-dir-local-variables to nil sidesteps that.

I use an Emacs Lisp file as the sample, by the way. It highlights nicely and, being the mother tongue, needs no third-party major mode, which keeps the whole setup dependency-free.

Now there’s a pretty Emacs frame on screen. The other half of the problem is turning it into a PNG.

Option 1: let Emacs export itself

The cleanest approach doesn’t involve a screenshot at all. If your Emacs is built with Cairo (as the GTK build on Linux typically is), it has a wonderful function called x-export-frames that renders a frame straight to an image:

(with-temp-file "shot.svg"
  (insert (x-export-frames nil 'svg)))

It can emit svg, pdf, postscript, or png. No external tools, no window-manager wrangling, no “don’t touch the mouse while it runs”. Emacs simply hands you the picture. If you’re on a Cairo build, stop reading and use this.

Alas, I’m on macOS, where Emacs uses the NS toolkit and x-export-frames isn’t available (you get a friendly void-function). So I had to go the screenshot route.

Option 2: screenshot the window

macOS ships with screencapture, which can grab a rectangular region of the screen. The trick is knowing exactly where the Emacs frame is. Conveniently, Emacs knows: frame-edges reports the outer pixel coordinates of the frame:

(let ((e (frame-edges nil 'outer-edges)))
  (with-temp-file "/tmp/geom"
    (insert (format "%d %d %d %d" (nth 0 e) (nth 1 e) (nth 2 e) (nth 3 e)))))

The shell side reads those coordinates and captures the rectangle:

read L T R B < /tmp/geom
screencapture -x -R "$L,$T,$((R - L)),$((B - T))" out.png

And that’s basically it. Except it isn’t.

The part nobody warns you about

Here’s the catch with region capture: screencapture -R grabs whatever happens to be on screen at those coordinates. If any other window is sitting on top of the Emacs frame, you’ll cheerfully capture that instead. And since I like to keep working while the script churns through a dozen flavors, this happened all the time. I’d end up with a screenshot of my terminal, my browser, or my other Emacs.

I ended up stacking a few tricks. First I float the frame above everything else with the z-group frame parameter, (set-frame-parameter nil 'z-group 'above). Then, since an Emacs launched from a background shell isn’t the active application and starts out buried behind the other windows, I pull it forward with (ns-hide-emacs 'activate).

Neither of those is bulletproof on its own, so the real safety net is checking the result. After each capture I sample a pixel from a corner that should be the theme’s background and compare it to the color Emacs reports for the default face. If they don’t match, I know I grabbed the wrong window, so I wait a moment and try again. That one check is what makes the whole thing safe to run while I keep working. Asking ImageMagick for a single pixel is enough:

magick out.png -crop '1x1+24+420' +repage -format '%[pixel:p{0,0}]' info:

Capturing a specific window by its ID would sidestep the stacking problem entirely, but on recent macOS enumerating window IDs requires Screen Recording permission for whatever process asks, and I couldn’t get that from a plain script. Floating the frame and verifying the result turned out to be simpler and more reliable.

A few finishing touches

A couple of smaller tweaks made the output look properly polished. GUI Emacs on macOS colors the native title bar according to the frame’s ns-appearance parameter, so I set it to light for light themes and dark for dark ones, and add ns-transparent-titlebar so the bar blends into the buffer background. It’s a tiny detail, but it makes the whole window feel like one piece. (It’s also the same trick that fixes an unreadable title bar under light themes, but that’s a story for another day.)

The other tweak was really a bugfix. Every so often a shot came out with a stray glyph or two at the top of the buffer, some redisplay artifact I never bothered to fully diagnose. Forcing a (redraw-display) right before I read out the frame geometry made it go away.

Other roads not taken

If you want to run this on a server or in CI, the story gets even better on Linux. You can start a virtual X display with Xvfb, run the Cairo Emacs build against it, and use x-export-frames: fully headless, perfectly reproducible, and with none of the “is the right window on top?” nonsense. That’s the setup I’d reach for if I ever wanted to generate these in a GitHub Action.

There are also plenty of other capture tools depending on your platform: ImageMagick’s import and the venerable scrot on X11, grim on Wayland, gnome-screenshot, and so on. Most of them can target a specific window, which is handy. One thing that won’t work is emacs --batch: batch mode has no GUI frame, so there’s nothing to photograph. You genuinely need a real, on-screen frame (or Cairo’s off-screen rendering).

Where this is headed

For now this lives as a small tools/ script inside my Batppuccin repo: a sample.el to display, an Emacs Lisp file to set up the frame, and a shell script to drive the capture and verification. It has already saved me a ton of tedium. Regenerating four flavors’ worth of screenshots is one command, and they come out identical every time.

But all of my theme ports have the same need, and the logic is almost entirely theme-agnostic. So I suspect I’ll eventually pull it out into a little standalone project: point it at a theme directory, hand it a sample file, and let it produce a consistent gallery for any theme. If that sounds useful to you as well, let me know. It might just nudge me into actually doing it.

Until then, I hope some of these ideas save you a bit of manual cropping. Automating the boring parts is what Emacs is all about.

-1:-- Automating Emacs Screenshots (Post Emacs Redux)--L0--C0--2026-07-03T07:00:00.000Z

Joar von Arndt: Automatically Find PGP Keys in Mu4e


I have found a peculiar delight in encryption, and particularly in using pgp-based asymmetric cryptography. This is especially useful for securing email communications, but doing so requires remembering to run M-x mml-secure-sign or mml-secure-encrypt in Mail Utilities For Emacs (Mu4e) before sending the message. For this reason I have been wanting to write a quick method that automatically adds signing capabilities and encryption only when the recipient has a public key. Thankfully, I did not have to do so because of Nicolas Cavigneaux’s recent post Sign Always, Encrypt When Possible where he showcases his functions for doing exactly that. It prompted me to tweak it slightly, and to add the functionality to automatically discover public keys that I have yet to import.

Cavigneaux’s post sets up an elegant arrangement: email should always be signed with your pgp key, and you should “upgrade” from there to encryption only if your correspondent can handle encrypted mail and has shared their public key with you. This is done by using Emacs’ built-in Easy Privacy Guard (epg) tooling to compare the owners of the imported public keys on the machine with the list of recipients in an email.1 If you have the public key for all recipients, the message will be automatically encrypted — otherwise it will merely be signed to prove that you are the author.

This was exactly what I had wanted, and I quickly incorporated this functionality into my own configuration. Testing it out, I felt a bit uneasy sending encrypted mail automatically. Because it is not the default, I felt that fully automated encryption made me unsure whether the mail has been encrypted or not — even when I know I have the recipient’s public key. I thus made the slight modification of prompting for both encryption and signing:

(defun kudu/message-sign-encrypt-if-all-keys-available ()
    "Add MML tag to encrypt message when there is a key for each recipient,
sign it otherwise."
    (if (bounga/message-all-epg-keys-available-p)
        (if (y-or-n-p "Encrypt? ")
            (mml-secure-message-sign-encrypt)
          (when (y-or-n-p "Sign? ")
            (mml-secure-message-sign)))
      (when (y-or-n-p "Sign? ")
        (mml-secure-message-sign))))

In practice I will probably always be pressing Y when prompted, but this adds a level of certainty that the code is running correctly. It reminds me of an advertising gimmick for housewives in the 1950s: when cake mix first became available, it was not appreciated because it was perceived as being “too easy” — it did not feel like baking a cake yourself. And so the recipe was tweaked to simply require an extra egg to be added. In practice this was a trivial change, but it meant that you felt more responsible for the finished work.

But there is an additional step I want to automate that Cavigneaux alluded to, but did not implement. He writes:

[T]he policy is only as good as my keyring. Encryption depends on having imported the right public keys; nothing here fetches them for me.

I have recently been having more correspondence with people using the Swiss-based email provider Protonmail. Proton provides support for the Web Key Directory (wkd) method of providing keys, where the dns settings of a domain point to a file containing a public key for a specific user. This allows the email itself to “provide” its own public key for encryption. The public key of any @protonmail.com or @pm.me address can be quickly imported in a second using gpg --locate-external-keys email@pm.me. Proton will similarly use wkd to get your public key, and so you will receive encrypted mail as well. This is still easy to set up for any other email provider, as long as you can edit the domain settings.2

Sadly, epg does not come with any options for Gnupg’s --locate-keys or --locate-external-keys flags. This might be a good addition. In the meantime, we can write a quick and dirty function to do the job for us in the background when opening any new email:

(defun kudu/message-locate-keys ()
  "Tries to find the public keys of 'bounga/message-recipients' via WKD through --locate-keys."
  (dolist (recipient (string-to-list (bounga/message-recipients)))
    (let ((recipient-email (cadr recipient))
          (proc (make-process
                 :name "gpg-locate-keys"
                 :command (list epg-gpg-program "--no-tty" "--locate-keys" (cadr recipient))
                 :connection-type 'pipe
                 :filter (lambda (proc string)
                           (process-put proc 'output
                                        (concat (or (process-get proc 'output) "") string)))
                 :sentinel (lambda (proc event)
                             (when (eq (process-status proc) 'exit)
                               (let ((output (process-get proc 'output))
                                     (email (process-get proc 'email)))
                                 (if (and output (string-match-p "imported: [1-9]" output))
                                     (message "Public key imported for %s" email))))))))
      (process-put proc 'email recipient-email))))

We use --locate-keys because the stricter --locate-external-keys will check the domain even if a key already exists in the local keyring. --locate-keys will instead instantly find local keys and finish early, while only missing keys will be checked. This is done asynchronously through make-process so that it does not cause the ui to freeze as we wait for network requests to finish. gpg seems to check very quickly, so it is not a large performance hit regardless.

We want to have imported any new keys before sending our email because Cavigneaux’s functions are run on 'message-send-hook. We therefore run kudu/message-locate-keys much earlier:

(add-hook 'mu4e-view-rendered-hook 'kudu/message-locate-keys)

This adds any correspondents’ public keys on any opened email, and so does not have to go through your entire contacts list. A downside is that it does not check for a public key when you are the person sending an unsolicited email, but it will when you get their reply.

This is not the world’s most elegant function, and if someone more familiar with epg has any comments regarding ways to improve it I will happily edit this post for improvements. Just send me a (pgp encrypted!) email. Run echo 'am9hcnhwYWJsb0B2b25hcm5kdC5zZQ' | base64 --decode | xargs gpg --locate-external-keys to import my key. If you have wkd set up, I will automatically fetch your public key. If not, this is a great opportunity to fix that!

I will also note how wonderful Emacs and epg make working with encrypted files, messages, and/or text more generally. I must admit that I used a plain-text .authinfo for an embarrassingly long time to store some of my secrets, thinking it would be bothersome to use the encrypted .authinfo.gpg for auth-sources. If you are like me, do not worry — the peace of mind from being able to write down personal information and simply leave it scattered on your filesystem is well worth just having to type your password. ❦

Footnotes:

1

From Cavigneaux’s post, the function to get all message recipients. Returns a list formatted like (("First recipient" "first@domain.tld") ("Second recpient" "second@domain.tld") ...):

(defun bounga/message-recipients ()
  "Return a list of all recipients in the message, looking at TO, CC and BCC.
Each recipient is in the format of `mail-extract-address-components'."
  (mapcan (lambda (header)
            (let ((header-value (message-fetch-field header)))
              (and
               header-value
               (mail-extract-address-components header-value t))))
          '("To" "Cc" "Bcc")))
2

So no @gmail.com, @outlook.com, or @yahoo.com addresses. You can still use these to service your mail; wkd does not interact with any email-related dns records.

-1:-- Automatically Find PGP Keys in Mu4e (Post Joar von Arndt)--L0--C0--2026-07-02T22:00:00.000Z

Irreal: Magit 4.6 Is Released

Jonas Bernoulli is announcing the release of Magit 4.6. It includes, he says, over 300 commits since the last release 6 months ago. You can check Bernoulli’s post for the details but an overview is that the main changes were in

  • Blob handling. Working with blobs should be easier and make more sense with this release.
  • Experimental syntax highlighting in diffs.
  • Improved hook handling that allows for the calling of Elisp hooks from Git hooks.
  • Improved commit message processing. List of changed definitions in a diff are now available for composing a git commit message.

As I’ve said before, Magit and Org Mode are responsible for luring many new users into the Emacs event horizon so these improvements are important. Moreover, they raise again the need for supporting this effort. Bernoulli spends full time on Magit on a few other open source projects and depends on contributors for his livelihood Take a look at his support page to see how you can help out.

-1:-- Magit 4.6 Is Released (Post Irreal)--L0--C0--2026-07-02T14:46:33.000Z

Protesilaos: Emacs: write with input method (e.g. French) and Jinx for spelling

Raw link: https://www.youtube.com/watch?v=UjCbNOqUgTs

In this video demonstration I show the tools I use to write in French using Emacs. One is the built-in framework for input methods, which allows us to compose characters to express the full range of the language we are typing in (e.g. French or Greek). The other is a spell-checking package called jinx, which is developed by Daniel Mendler.

Below is the code I showed in the video:

(use-package emacs
  :demand t
  :bind
  ( :map global-map
    ;; The `toggle-input-method' sets the latest selected input
    ;; method, or the one defined in `default-input-method'.  Once an
    ;; input method is set, `toggle-input-method' will switch back to
    ;; the standard Emacs input.
    ("<f2>" . toggle-input-method))
  :config
  ;; Call the command `describe-input-method' to get a description of
  ;; what the given input method supports in terms of key sequences as
  ;; well as the key layout it has.
  (setq default-input-method "french-postfix"))



;; You need to have libenchant available on your system. For example,
;; Debian provides the package `libenchant-2-dev'.
(use-package jinx
  :ensure t
  :demand t
  :bind
  ( :map global-map
    ("M-$" . jinx-correct) ; or bind `jinx-correct-all'
    ("C-M-$" . jinx-languages))
  :config
  ;; Here you can specify a string with space-separated dictionaries.
  ;; I install the aspell dictionaries, such as the Debian package
  ;; `aspell-fr' for French and `aspell-el' for Greek (Éllinika).
  ;; With `aspell' installed on the system, do `aspell dicts' on the
  ;; command-line to get a list of available dictionaries.
  (setq jinx-languages "en fr el es")

  ;; I want to have Jinx in programming modes but I do not want it to
  ;; check anything that is a comment or string, because then it
  ;; underlines too many things which are not useful. We can do the
  ;; same for other modes, though I think this is fine.
  (setq jinx-exclude-faces
        '((prog-mode font-lock-comment-face font-lock-string-face)))

  (global-jinx-mode 1))
-1:-- Emacs: write with input method (e.g. French) and Jinx for spelling (Post Protesilaos)--L0--C0--2026-07-02T00:00:00.000Z

Nicolas Cavigneaux: emacs-mailto: Open macOS mailto: Links in Emacs

What it is

emacs-mailto makes macOS mailto: links open in Emacs, using whatever mail-user-agent you already have configured: mu4e, notmuch, Gnus, Rmail, or plain message-mode. Click a mailto: link in your browser, and Emacs comes to the front with a compose buffer already addressed.

The project lives at sr.ht/~bounga/emacs-mailto, where you’ll find the source and the ticket tracker.

The problem it solves

The GNU Emacs NS port (as shipped by emacs-plus, emacs-mac, or the official build) does not handle mailto: URLs.

When Emacs is already running, clicking a mailto: link activates Emacs but the URL never arrives: macOS brings the app to the front and drops the URL Apple-event before it ever reaches Emacs Lisp. So nothing happens: no compose buffer, and no way to fix it from your config, because the event never gets there in the first place.

That’s the whole frustration emacs-mailto exists to remove.

How it works

emacs-mailto sidesteps the dropped Apple-event with a tiny relay app registered as your default mail application. On a mailto: click, it brings Emacs to the front and hands the URL to emacsclient:

open -a Emacs
emacsclient -n -e "(run-at-time 0 nil (lambda () (browse-url-mail \"<url>\")))"

browse-url-mail parses the mailto: URL (recipient, subject, body) and opens a compose buffer through your mail-user-agent. The run-at-time wrapper lets emacsclient return immediately instead of blocking on an interactive prompt, such as a From-address picker.

A few details make it painless in practice:

  • emacsclient is located automatically at runtime (Homebrew on Apple Silicon or Intel, MacPorts, ~/.local/bin, or /usr/bin), so no path is hard-coded.
  • The app is built and ad-hoc signed locally by the installer, so there is no Gatekeeper quarantine and no pre-built binary to trust.

Requirements

  • macOS
  • Emacs running with a server (M-x server-start, or the Doom/Spacemacs defaults), so emacsclient can reach it
  • A configured mail-user-agent (this is what actually opens the compose buffer)

Installation

Clone the repository and run the installer:

git clone https://git.sr.ht/~bounga/emacs-mailto
cd emacs-mailto
./install.sh

This compiles Emacs Mailto.app into ~/Applications, registers it, and sets it as the default mailto: handler.

Test it:

open "mailto:test@example.com?subject=Hi"

Emacs should come to the front with a compose buffer addressed to test@example.com.

If your Emacs app has a different name, or you want a custom bundle identifier, override them with environment variables:

EMACS_APP="Emacs" BUNDLE_ID="org.nongnu.emacs-mailto" ./install.sh

A caveat about the default mail app

The relay runs as a background agent (LSUIElement), so it does not show up in Mail → Settings → General → “Default email application”. That menu may keep displaying another app; ignore it. To check the real handler, ask LaunchServices directly:

swift -e 'import AppKit; print(NSWorkspace.shared.urlForApplication(toOpen: URL(string:"mailto:x@y.z")!)!.path)'

It should print .../Emacs Mailto.app.

Uninstall

./uninstall.sh

This removes the app and resets the mailto: handler to Apple Mail.

Wrapping up

The whole thing is one small relay that removes a long-standing papercut of running Emacs as a mail client on macOS. It’s MIT licensed. If you hit a rough edge with your particular Emacs build or macOS version, the tracker is on the project page at sr.ht/~bounga/emacs-mailto.

-1:-- emacs-mailto: Open macOS mailto: Links in Emacs (Post Nicolas Cavigneaux)--L0--C0--2026-07-01T22:00:00.000Z

Nicolas Cavigneaux: shpaste: a paste.sr.ht Client for Emacs

What it is

shpaste is a small Emacs client for paste.sr.ht, the pastebin service from SourceHut. It lets me share a snippet without leaving the editor: select some text, run a command, and the paste URL is already in my kill-ring, ready to paste into a chat or an email.

It is built on the SourceHut GraphQL API and does just enough to be useful: create pastes, list them, open them, delete them. Nothing more.

The project lives at sr.ht/~bounga/shpaste, where you’ll find the source, the ticket tracker, and the mailing lists.

What it does

  • Create a paste from the region or from the whole buffer. The resulting URL is pushed to the kill-ring and shown in the echo area.
  • Browse your pastes in a dedicated list buffer, sorted and tabulated.
  • From that buffer: open a paste, copy its URL, delete it, or refresh the list.
  • Choose a visibility per paste: public, unlisted, or private.
  • Point it at a self-hosted SourceHut instance if you don’t use the public one.

The list buffer looks like this:

The shpaste-list buffer

Requirements

  • Emacs 29.1 or later
  • plz (GNU ELPA) and curl

Installation

shpaste is not on MELPA yet; the package is only a few weeks old. MELPA Stable support will come later, built from the version tags. In the meantime, install it straight from the repository.

On vanilla Emacs 29.1+, package-vc-install clones and builds it from source:

(package-vc-install "https://git.sr.ht/~bounga/shpaste")

On Emacs 30+, you can let use-package do the same with :vc:

(use-package shpaste
  :vc (:url "https://git.sr.ht/~bounga/shpaste" :rev :newest))

In Doom Emacs, declare the recipe in packages.el:

(package! shpaste
  :recipe (:type git
           :host nil
           :repo "https://git.sr.ht/~bounga/shpaste"
           :files ("*.el")))

Then configure it in config.el. This is the setup I use, with a paste leader menu and the list-buffer bindings:

(use-package! shpaste
  :commands (shpaste-list
             shpaste-create-from-region
             shpaste-create-from-buffer)
  :init
  (setq shpaste-default-visibility 'unlisted)
  (map! :leader
        (:prefix ("P" . "paste")
         :desc "List my pastes"      "l" #'shpaste-list
         :desc "Paste buffer"        "p" #'shpaste-create-from-buffer
         :desc "Paste region"        "r" #'shpaste-create-from-region))
  :config
  (map! :map shpaste-list-mode-map
        :n "RET" #'shpaste-list-open
        :n "w"   #'shpaste-list-copy-url
        :n "d"   #'shpaste-list-delete
        :n "gr"  #'shpaste-list-refresh))

Getting a token

shpaste authenticates with a SourceHut OAuth2 personal access token. Generate one at meta.sr.ht/oauth2 with the PASTES grant in read-write mode, then store it in auth-source with the host set to your instance — for example in ~/.authinfo.gpg:

machine paste.sr.ht password <YOUR_TOKEN>

There is no shpaste-token variable: the token is only ever read from auth-source. If you want the details of how that lookup works and why I chose it, I wrote a whole post about it: Using auth-source in a Real Emacs Package.

Usage

Command Action
shpaste-create-from-region Create a paste from the region; URL to kill-ring
shpaste-create-from-buffer Create a paste from the whole buffer
shpaste-list Browse your pastes in a list buffer

Inside the shpaste-list buffer:

Key Action
RET Open the paste
w Copy its URL
d Delete it
g Refresh the list

Configuration

Two options, both optional:

  • shpaste-instance (default "paste.sr.ht") — set it to a self-hosted host. It is used both to build the API endpoint and as the auth-source lookup key, so the machine field above must match it.
  • shpaste-default-visibility (default unlisted) — one of public, unlisted, or private.

Wrapping up

That’s the whole tool. shpaste is young (currently 0.1.0) and MIT licensed. If you live in Emacs and use SourceHut, give it a try — and if you hit a rough edge or have an idea, the project page is at sr.ht/~bounga/shpaste.

-1:-- shpaste: a paste.sr.ht Client for Emacs (Post Nicolas Cavigneaux)--L0--C0--2026-07-01T22:00:00.000Z

Nicolas Cavigneaux: Sign Always, Encrypt When Possible: Automatic GPG in mu4e

Introduction

GPG is only useful for the emails you actually remember to protect.

In mu4e, signing and encrypting a message is a manual gesture. Before sending, I hit C-c C-m s p to sign, or C-c C-m c p to sign and encrypt. It works, but it relies on me thinking about it every single time.

I don’t. Nobody does.

So in practice, the manual approach means most messages go out unsigned, and encryption only happens on the rare occasion I stop to remember it exists.

I wanted the opposite default. Something with no friction, that follows a clear policy:

  • Sign every message I send. Always.
  • Encrypt a message when, and only when, I have a public key for every recipient.

The second rule matters. Encryption is all-or-nothing: if a single recipient is missing from my keyring, encrypting would lock them out of their own email. So encryption has to be opportunistic — on when it can be, silently off when it can’t.

Here is how I wired that into mu4e.

The Building Blocks

Three pieces do the work.

MML secure tags. When you compose a message, Emacs doesn’t encrypt anything itself. It inserts an MML tag that tells the message layer what to do at send time. Two functions add those tags for me: mml-secure-message-sign and mml-secure-message-sign-encrypt.

message-send-hook. This hook runs right before a message is sent, when the headers are filled in and the recipients are known. That’s the perfect moment to decide whether to sign or to sign and encrypt.

epg. The Emacs interface to GnuPG. I use it to ask a simple question: do I have a public key for this address?

Two settings configure the signing context:

(setq mml-secure-openpgp-sign-with-sender t
      mml-secure-openpgp-encrypt-to-self t)

sign-with-sender tells the OpenPGP layer to pick the signing key based on the From address, which is exactly what I want when juggling several accounts.

encrypt-to-self is the one that saves you from a classic mistake. When you encrypt a message to someone else’s key, you can no longer read it — not even in your own Sent folder. Encrypting to yourself as well keeps your sent copies readable.

Collecting the Recipients

To decide whether encryption is possible, I first need the full list of people the message is going to.

(defun bounga/message-recipients ()
  "Return a list of all recipients in the message, looking at TO, CC and BCC.
Each recipient is in the format of `mail-extract-address-components'."
  (mapcan (lambda (header)
            (let ((header-value (message-fetch-field header)))
              (and
               header-value
               (mail-extract-address-components header-value t))))
          '("To" "Cc" "Bcc")))

The bounga/ prefix is my personal namespace. Emacs Lisp has a single global namespace, so prefixing your own functions is the convention that keeps them from colliding with built-ins or package code — never define a bare message-recipients that looks like it belongs to message.el. Pick whatever prefix you like; just be consistent.

I walk the To, Cc and Bcc headers, read each one with message-fetch-field, and parse it with mail-extract-address-components. The trailing t asks for all addresses in a header, not just the first, so a To line with five people returns five entries.

Bcc is in the list on purpose. A blind-carbon recipient still has to be able to read the message — if I can’t encrypt to them, I can’t encrypt at all.

mapcan concatenates the per-header results into a single flat list.

Do I Have Everyone’s Key?

Now the actual question: is there a public key in my keyring for every recipient?

(defun bounga/message-all-epg-keys-available-p ()
  "Return non-nil if the pgp keyring has a public key for each recipient."
  (require 'epa)
  (let ((context (epg-make-context epa-protocol)))
    (catch 'break
      (dolist (recipient (bounga/message-recipients))
        (let ((recipient-email (cadr recipient)))
          (when (and recipient-email (not (epg-list-keys context recipient-email)))
            (throw 'break nil))))
      t)))

I create an epg context, then loop over the recipients. For each one I ask epg-list-keys whether the keyring holds a key for that address. The email is the second element of each parsed recipient, hence cadr.

The logic is deliberately strict. The moment I find a single recipient with no key, I throw out of the loop and return nil: encryption is off the table. Only if the loop completes — every recipient covered — does the function return t.

The catch/throw pair is just an early exit. There is no point checking the remaining addresses once one is missing.

Tying It to Send

The decision function is tiny, because the two helpers did the hard part:

(defun bounga/message-sign-encrypt-if-all-keys-available ()
  "Add MML tag to encrypt message when there is a key for each recipient,
sign it otherwise.
Consider adding this function to `message-send-hook' to
systematically send signed / encrypted emails when possible."
  (if (bounga/message-all-epg-keys-available-p)
      (mml-secure-message-sign-encrypt)
    (mml-secure-message-sign)))

If I have everyone’s key, sign and encrypt. Otherwise, just sign. Either way, the message is signed — that part is never optional.

The last step is to run this before every send:

(add-hook 'message-send-hook 'bounga/message-sign-encrypt-if-all-keys-available)

message-send-hook fires once the message is complete and about to leave. By then the recipients are settled, so the keyring check reflects exactly who will receive the message.

The Result

With those few lines in place, I no longer have to remember GPG at all.

Every email I send is signed. Whenever I happen to have public keys for all the recipients (which, over time, covers more and more of my correspondence), the message is encrypted too, without a single keystroke on my part.

A couple of honest caveats.

Signing everything means every recipient can tell I use GPG, and gets a signature attachment whether they care about it or not. For me that’s a feature, not a cost.

And the policy is only as good as my keyring. Encryption depends on having imported the right public keys; nothing here fetches them for me. When a key is missing, the message silently falls back to signing only — which is the safe failure mode, but a silent one. If you’d rather be told, this is the spot to add a notice.

None of that changes the day-to-day feel, though: I write email, I send it, and the right cryptography happens on its own.

BTW, my public PGP key is available here: DB19B66E.

-1:-- Sign Always, Encrypt When Possible: Automatic GPG in mu4e (Post Nicolas Cavigneaux)--L0--C0--2026-07-01T22:00:00.000Z

Meta Redux: Projectile 3.0

Projectile 3.0 is finally out, and it’s a big one - easily the biggest release in years. There’s a nice reason for that: this year Projectile turns 15.1 It all started back in the summer of 2011, simply because I was frustrated that find-file-in-project didn’t work on Windows, and somehow that little anniversary made me want to shake things up and finally tackle a whole pile of changes I’d been putting off for ages. Some new features, some long-overdue spring cleaning, and a few things I’d wanted to remove for the better part of a decade.

What’s dead can’t die

There’s a running joke that Projectile is obsolete now that Emacs ships project.el out of the box. I’ve heard it many times, and I even wrote about it when Projectile turned 10 - it’s obviously hard to compete with a built-in package, as it has a home-field advantage you can never match.

But here’s the thing. If it’s really true that Projectile is dead and everyone has moved on to project.el, then that’s oddly liberating. What’s dead can’t die, right? If there are no users left to upset, I can finally go wild with my wildest ideas and stop worrying about breaking the workflows of Projectile’s (non-existent) users. And that’s more or less the spirit in which I approached 3.0 - I stopped being quite so precious about backwards compatibility and just did what I felt was right for the project.

(Turns out there might still be a user or two out there after all, so do skim the upgrade notes below. Sorry in advance!)

The highlights

There’s a lot in this release - well over a hundred entries in the changelog - but here are the highlights I’m most excited about:

  • projectile-dispatch (bound to s-p m) is a new transient menu that mirrors the command map, so you no longer have to remember every keybinding. It also exposes command modifiers as switches, so you can toggle a regexp search, start a fresh shell, invalidate the cache, or open a result in another window/frame right from the menu.
  • Projectile can now index a project asynchronously, in a background process, so a cold projectile-find-file no longer freezes Emacs while it walks a huge tree. The building blocks are public, so external tools can drive Projectile’s indexing asynchronously as well.
  • There’s a new (optional) projectile-consult integration built on those async data sources. Its finder streams candidates into the minibuffer as the project gets indexed, instead of blocking until indexing is done, while still honouring your VCS and indexing configuration.
  • The various search commands got folded into a single projectile-search, backed by a small, extensible backend registry. It ships grep, ripgrep and ag backends out of the box, and you can register your own (deadgrep, consult-ripgrep, whatever you fancy) in a few lines.
  • The shell, REPL and terminal commands got the same treatment - one projectile-run with shell, eshell, ielm, term, vterm, eat and ghostel backends, plus a seam to plug in your own.
  • The indexers got a thorough going-over - batched stat calls, single directory listings instead of dozens of file-exists-p probes, smarter caching. It all adds up, and it really shows over TRAMP, where every needless round-trip used to hurt. I think that 15 years later I’ve finally solved Projectile’s infamous performance issues over TRAMP!2
  • I finally removed a pile of features that had outlived their usefulness - the single-key Commander (superseded by the dispatch menu), the idle timer, projectile-browse-dirty-projects, and all of the built-in tags support (xref and LSP do this far better nowadays).
  • The minimum supported Emacs is now 28.1. That let me drop a heap of compatibility shims and finally make transient a proper dependency instead of an optional extra.
  • Projectile is a better project.el citizen now - it implements more of the protocol (project name, buffers, ignores), so tools built on top of project.el behave correctly in Projectile-managed projects.
  • I also dropped the legacy ido/ivy/helm completion systems - these days everything rides on the standard completing-read, so Vertico, Consult and friends just work. ido fans are the one group that has to lift a finger: install ido-completing-read+ (the package formerly known as ido-ubiquitous) and turn on ido-ubiquitous-mode, and ido will drive Projectile’s prompts just like before.
  • And there’s a pile of smaller quality-of-life additions - subproject compile/test for multi-module projects (c m c / c m t), a %p placeholder for the project name in command strings, projectile-add-and-switch-project, jumping back to the most recent project, and more.

Upgrading

3.0 is a major release for a reason - there are breaking changes. The minimum Emacs version is now 28.1, several long-deprecated commands and options are gone, and a couple of keybindings moved around. I’ve written a dedicated Upgrading to Projectile 3.0 guide that walks through everything and how to adapt your config. Most setups will keep working untouched, but please give it a read before you upgrade.

Fifteen years later

As I said back when Projectile turned 10 - it’s always hard to compete with built-in packages. Still, I’m genuinely proud that 15 years in, Projectile is still here and, hopefully, still relevant to at least some of you. It remains one of my favourite projects and the one I reach for every single day.

Huge thanks to everyone who has contributed code, reported bugs, written extensions, or simply used Projectile over the years - none of this would exist without you. I really hope 3.0 isn’t the end of the innovation for Projectile, and that it’ll keep surprising and delighting its users for years to come.

Keep hacking!

  1. Give or take - I started Emacs Prelude the same year, so it’s always been a little hard to say which of my open-source projects was truly first. In my mind it’s always been Projectile. 

  2. Famous last words… I know… 

-1:-- Projectile 3.0 (Post Meta Redux)--L0--C0--2026-07-01T17:21:00.000Z

Irreal: Deleting To Trash Redux

Over at Emacs Dwelling there’s a nice post on why you should set Emacs to delete files by moving them to trash. That’s easy to do by setting delete-by-moving-to-trash to t. The post has a story illustrating why you should do that: it’s very easy to accidentally delete a file with Dired and if you do it to a file with changes that haven’t been committed to version control you can lose a few hours work. If you don’t use version control, the story is even worse.

The post seemed to me to be a worthy cautionary tale and I thought it would make a good Irreal post but when I began the writing process by entering a file name for the post, delete-to-trash.org, I discovered that such a file already existed from 2022. Even worse, for my blogging plans, it said everything I was planning to say in this post. Nevertheless, that was 4 years ago and the lesson, it seems, is evergreen so it’s worth making the point again. It costs you nothing and in those rare occasions where errant muscle memory causes you to delete a file, it can save the day. So rather than repeat the story I’ll just suggest that you check out the Emacs Dwelling post or my previous effort.

-1:-- Deleting To Trash Redux (Post Irreal)--L0--C0--2026-07-01T15:13:19.000Z

Meta Redux: Sayid Redux

I have a soft spot for Sayid - it’s one of the most ingenious Clojure tools ever built, and also one of the most neglected. It’s an omniscient debugger: instead of stopping your program at a breakpoint, it quietly records every call to the functions you’ve traced and lets you rummage through the recording afterwards. It’s the kind of thing you demo to people and watch their jaw drop. And it had been sitting practically unmaintained for the better part of a decade.

Here’s the awkward part: that neglect is largely on me. Bill Piel, Sayid’s original author, handed me the keys ages ago, and I’ve been… let’s say a less than exemplary steward. I’d merge the occasional patch to keep the lights on, but until very recently I’d done precious little to actually move the project forward.

The best time to maintain your open-source project was six years ago. The second best time is now.

– Ancient proverb, lightly adapted

So what finally lit a fire under me? Blame CIDER 2.0. I’ve been reworking CIDER’s debugging and tracing story, and at some point I sat down to make the built-in tracer smarter. A few hours in it hit me: nothing I could realistically bolt onto the built-in tracing would come anywhere close to what Sayid already does. So instead of building a worse Sayid, I dusted off the real one, gave it a good scrub, and here we are - Sayid 0.4.

What is Sayid, anyway?

For the uninitiated: Sayid records the arguments, return value, timing and full call tree of the functions you trace, so you can go back and inspect exactly what happened - no breakpoints, no println, no re-running the thing five times.

While we’re on the subject of embarrassing confessions: I’ve been the maintainer of this thing for years and I still have no idea what “Sayid” actually means or what it’s a reference to. If you happen to know, please, put me out of my misery - I’d love to finally get the joke.

Trace a namespace, run your code, and pop open the workspace with C-c s w:

▾ (demo.coins/can-afford? [:quarter :dime :nickel :penny] 45) => true
    (demo.coins/total-cents [:quarter :dime :nickel :penny]) => 45

can-afford? says true, but four coins worth 41 cents shouldn’t cover a 45-cent tab. Something’s off, and the bug lives inside total-cents. This is where Sayid really shines - flip on an inner trace and it records every expression inside the function:

▾ (demo.coins/can-afford? [:quarter :dime :nickel :penny] 45) => true
  ▾ (demo.coins/total-cents [:quarter :dime :nickel :penny]) => 45
    ▾ (apply + (map coin-values coins)) => 45
        (map coin-values coins) => (25 10 5 5)

There it is, staring right at us: (25 10 5 5), when the last value should be 1. A penny is worth five cents in our coin map. We never wrote a single println, and we never had to guess where to put a breakpoint - we just looked at what actually happened.

That’s the pitch, and it’s a great one. So why was such a cool tool gathering dust?

Why an omniscient debugger, in 2026?

Honestly, part of it is that Clojure folks are spoiled. We have the REPL, so the reflex for most of us is to just re-run a form with some tap> or println sprinkled in and eyeball the output. That works right up until it doesn’t - until the bug is three layers deep in a map over a lazy seq, or only shows up on the 400th call, or lives in code you didn’t write and don’t feel like instrumenting by hand.

A traditional stepping debugger stops the world and makes you drive. Sayid does the opposite: it lets the program run to completion and then hands you the entire execution history as something you can navigate at your own pace. tools.trace is in the same spirit, but it dumps text to stdout - Sayid keeps structured data and gives you a query DSL to slice it. That’s a much better fit for how we actually work in Clojure: run it, capture everything, explore the data.

And here’s the thing that made me want to revive it rather than reinvent it - “capture everything as data and explore it later” is exactly the workflow that’s becoming more relevant, not less. Structured execution traces are gold, whether the thing doing the exploring is you, a data-inspection tool like Portal, or an AI assistant trying to understand why your code misbehaved.

Out with the old coordinates

First order of business was dragging the project into the present.

Sayid used to live under com.billpiel/sayid on Clojars, with namespaces like com.billpiel.sayid.core. The new home is clojure-emacs, so the artifact is now published as mx.cider/sayid:

{:user {:plugins [[mx.cider/sayid "0.4.0"]]}}

I also dropped the personal-domain prefix from every namespace - it’s plain sayid.core, sayid.trace, sayid.nrepl-middleware and so on now. The old com.billpiel/sayid coordinates still get the same releases for the time being, so nobody’s dependency breaks overnight, but the future is mx.cider.

Data first, strings later

Here’s the change I’m most excited about. Sayid’s nREPL middleware used to hand the Emacs client a pre-rendered blob of text plus a pile of text properties for colouring. In other words, the server did all the rendering and the client was a dumb terminal. That single decision is a big part of why there was exactly one Sayid client.

So the middleware now speaks data. There’s a family of new ops - sayid-get-workspace-data, sayid-query-data and friends - that return the recorded call tree as honest, navigable data instead of a wall of text:

{"id"       "4793"
 "name"     "demo.coins/can-afford?"
 "args"     ["[:quarter :dime :nickel :penny]" "45"]
 "return"   "true"
 "file"     "demo/coins.clj"
 "line"     12
 "children" [...]}

The structural bits (ids, names, timings, source location, the tree shape) come across as real nested maps and lists you can walk by key. The captured values are pr-str‘d, since an arbitrary Clojure value can’t always round-trip over the wire - but that’s the only place strings sneak in, and it’s exactly where you’d expect them.

The upshot: any editor or tool that speaks nREPL can now fetch a workspace and render it however it likes, and a REPL one-liner or a Portal tap gets you the same data with zero Sayid-specific machinery. The whole thing is written up in doc/nrepl-api.md, so you don’t have to reverse-engineer the wire format from the Emacs client the way you would have before.

A UI that doesn’t feel like 2015

With the data ops in place, I rebuilt the Emacs UI on top of them. The workspace and the “what’s traced” views are now proper foldable trees, built on CIDER’s new cider-tree-view. You get real folding (TAB), navigation (n/p), jump-to-source (RET), and - my favourite - c i hands the actual captured value straight to CIDER’s inspector, so you can drill into an argument or a return value as a live, navigable object rather than squinting at its printed form.

There’s also a query layer wired into the tree: f narrows to every recorded call of the function at point, i focuses a single call and its subtree. On a big trace that’s the difference between “wall of text” and “actually finding the thing”.

The best part is that the client is now smaller, not bigger - all the tree rendering, folding and value inspection are handled by mature components instead of bespoke code painting text properties by hand. That’s the payoff of moving rendering to the client: the server ships data, and the client is free to be as fancy or as plain as it wants.

Wait, you broke everything?

Backwards compatibility is a promise you make to the users you have. Sayid had exactly one, and reader, I am that user.

– Me, rationalising

Yeah, I did. I went pretty wild with the breaking changes this time around - new artifact coordinates, new namespaces, a reworked nREPL API, a bumped minimum CIDER version. Normally I’d bend over backwards to keep old clients working, but here I made a deliberate call: as far as I can tell, the bundled Emacs client was the only client Sayid ever had. Dancing around imaginary third parties to preserve compatibility nobody was relying on would have just made the project harder to adopt and harder to maintain.

So I opted for sweeping changes that leave Sayid in a much better place to build on, rather than a museum of backwards-compatible cruft. If it turns out I was wrong and you were quietly depending on the old coordinates - I’m sorry, and do let me know, because that’s genuinely useful information.

Take it for a spin

That’s the gist of it. Sayid is alive again, it’s leaner, it speaks data, and it has a UI I’m not embarrassed to demo. What I’d love now is for more people to actually use it and tell me whether the new direction resonates.

So please - [mx.cider/sayid "0.4.0"], trace something gnarly, and pop open the tree. Then head over to the issue tracker and tell me what you think: what feels great, what feels rough, what’s missing. I have ideas for where to take it next (bounding the recording so you can safely trace a whole namespace under a test suite is high on the list), but I’d rather steer by what people actually want out of it.

Big thanks to Bill Piel for building such a wonderful tool in the first place - I’m merely standing on the shoulders of a giant here. And thanks in advance to everyone willing to kick the tyres on the revival!

That’s all from me for now. Keep hacking!

P.S. One more thing…

Well, that didn’t take long. Remember that “high on the list” bit a few paragraphs up - bounding the recording so you can safely trace a whole namespace under a test suite? Turns out I couldn’t leave it alone. A short burst of small improvements after this post went up, and it’s done: Sayid 0.5.

That was the thing that made Sayid feel like a toy - point it at a real workload and it would cheerfully eat your whole heap and fall over. The recording now has a set of tunable bounds instead: a cap on how many top-level calls it keeps, a depth limit, 1-in-N sampling for hot paths, a per-function cap, and a keep-only-the-last-N mode for when what you care about is whatever happened right before things went sideways. Fat and infinite values no longer hang the data ops either, and the traced-functions view got its enable/disable/remove actions back.

The upshot for you: you can finally point Sayid at real code under real load without babysitting it. Same ask as before - give it a spin and tell me how it feels.

-1:-- Sayid Redux (Post Meta Redux)--L0--C0--2026-07-01T13:01:00.000Z

Chris Maiorana: God mode notes with Denote and Consult Notes

Consult notes is a package that hooks into your note-taking system. It works particularly well with Denote, picking up your notes directory variable automatically.

My interest in having a note system comes down to whether it helps my writing. A writer needs different kinds of notes: reading notes, notes on craft, notes on etymology, and random ideas. You’ll notice I’ve built that into the config below.

I’ve touched on the topic of note-taking in Emacs a few times, in posts on “making everything a note” and on the “undergrad” style of note-taking. I’ve often argued that a full dedicated notes package was unnecessary when you could simply create files and link them together.

But this “consult notes” package made me want to give Denote another try and see how it feels. So far, I like it. I’m going to test drive this config for a few weeks and report back on how it’s going.

Denote config

(use-package denote
  :ensure t
  :bind (("C-c n n" . denote)                ; new note
         ("C-c n f" . denote-open-or-create) ; find/create by title
         ("C-c n l" . denote-link)           ; insert a link to another note
         ("C-c n b" . denote-backlinks))     ; show what links here
  :custom
  ;; Names the destination for notes in the file system
  (denote-directory (expand-file-name "~/notes/"))
  ;; Default to Org type of files
  (denote-file-type 'org)
  ;; Keywords you reach for more often
  (denote-known-keywords '("craft" "etymology" "draft" "idea"))
  (denote-prompts '(title keywords))
  :config
  (denote-rename-buffer-mode 1))            ; show note title in the buffer name, nice

Consult

(use-package consult
  :ensure t
  :bind (("M-s l" . consult-line)            ; search within current draft
         ("M-s L" . consult-line-multi)      ; search across all open buffers
         ("M-s r" . consult-ripgrep)         ; search the whole writing directory
         ("M-s o" . consult-outline)         ; jump between org/markdown headings
         ("M-s b" . consult-buffer)
         ("M-s k" . consult-keep-lines)      ; isolate lines matching a pattern
         ("M-s f" . consult-flush-lines))    ; strip lines matching a pattern
  :custom
  ;; Small delay before previewing (keeps a big file from feeling jumpy)
  (consult-preview-key '(:debounce 0.2 any)))

Consult notes

(use-package consult-notes
  :ensure t
  :commands (consult-notes consult-notes-search-in-all-notes)
  :bind (("M-s n" . consult-notes)                       ; search notes by title
         ("M-s N" . consult-notes-search-in-all-notes))  ; full-text across notes
  :config
  ;; Reads denote-directory automatically (no second path to maintain)
  (consult-notes-denote-mode))

The post God mode notes with Denote and Consult Notes appeared first on Chris Maiorana.

-1:-- God mode notes with Denote and Consult Notes (Post Chris Maiorana)--L0--C0--2026-07-01T01:00:28.000Z

Raymond Zeitler: Create Date Books, Planners and Calendars with Emacs

Diary was my pick for this month's Emacs Carnival. But in fact it's part of a larger set of functions under the calendar group. Another set of functions allows you to create pocket date books, desk planners and wall calendars if you have access to a printer. The function names begin with cal-tex-cursor and cal-html-cursor, and they can be found in cal-tex.el and cal-html.el, respectively.

The "tex" and "html" in the function names describe the output formats, LaTeX and, well, HTML; "cursor" describes one way of defining their inputs (from the position of the cursor in the calendar buffer). They're further subdivided into layouts such as day, week, month and year. My favorite is cal-tex-cursor-week-iso.

Open the calendar (M-x calendar). Observe that the cursor is at today. Now invoke cal-html-cursor-month and accept the default at the prompt. The function will create a file named YYYY-MM.html. The variable cal-html-directory provides the cal-html functions with the path name. The HTML file is placed in a subdirectory named YYYY. In my case, cal-html-directory is set to "~/public_html." As I invoke it today, the subdirectory / filename is 2026/2026-06.html.

The calendar can pull information from your diary file(s), the holidays defined in holidays.el and in the holiday-other-holidays variable. If you have %%(diary-sunrise-sunset) and %%(diary-lunar-phases) you'll get the times of sunrise and sunset at your coordinates, and the dates and times of the four lunar phases, assuming that cal-tex-diary is non-nil. (I also have cal-tex-24 non-nil.)

I suggested cal-html-cursor-month because its output is easily viewed (rendered) with a web browser. But I prefer the weekly calendar produced by cal-tex-cursor-week-iso, which outputs in LaTeX.

I've invoked cal-tex-cursor-week-iso with the cursor on June 16. The output is shown in Figure 1 after rendering to PDF with pdflatex. (Note that the screenshot is closely cropped.) There's a lot included here.

The Rendered LaTeX for Week 25 of 2026

First, note that our Third Tuesday of the Month Club meeting (used as an example in a previous post) is shown. This comes from a diary entry; unfortunately org file events are ignored. (The agenda for this week is shown in Figure 2; note the event in red does not appear in the weekly calendar. This is another reason I favor recording appointments in the diary.)

Two other diary entries also are included: a record of when the kitchen cabinets were painted, plus a breakfast meeting with Howard.

Then you'll find two holidays, Islamic New Year and Father's Day. And because I have diary-lunar-phases in my diary, a moon phase also shows up. (I commented out the diary-sunrise-sunset statement because it adds the information to every day of the week, which makes the weekly planner look too cluttered.) Last but not least the Summer Solstice is included as a Sunday holiday along with the time it occurs.

The first line of each day block also shows (right justified) the day number (of the year) and the number of days remaining in the year. The Sunday block is quite crowded; the first line is wrapped, so the day number / days remaining field appears on the next line.

Sadly, any links in the diary that worked in Org Agenda do not work in the LaTeX output. You'll see instead the full syntax of the link, [URL][description] ].

I created an improved version of cal-tex-cursor-week-iso. Here are some of the changes:

  • inhibit sunrise/sunset data for all but the first day
  • switch to sans-serif font
  • color birthdays and anniversaries in blue
  • replace the non-ASCII characters (used for Bahai holidays)
  • offset left or right to allow for the binding of a year's worth of pages
  • more?

I uploaded it to Codeberg, but I need to clean it up a bit before I feel comfortable promoting it.

If you position the cursor on the last day of the year and invoke cal-tex-cursor-week-iso with a prefix argument of 53, you can create the pages for a personalized weekly planner for next year! Try it out!

-1:-- Create Date Books, Planners and Calendars with Emacs (Post Raymond Zeitler)--L0--C0--2026-06-30T21:05:58.956Z

James Dyer: That Moment Dired Eats Your File and why Trash Saves You

I was working on my diff-minimap package, deep in the weeds of fringe indicators and search highlights, and I did the thing. You know the thing. I was in Dired, accidentally, for some reason hit D to delete a filename (probably some malfunctioning muscle memory) and it was gone!

20260515110240-emacs--That-Moment-Dired-Eats-Your-File-and-why-trash-saves-you.jpg

Except it was my own source file. diff-minimap.el. The one I had been hacking on for the last hour. The one with all those uncommitted changes.

Now, in my head, I have a trash setup. I am sure I configured that somewhere, probably years ago, in some init file on some machine. But the machine I was sitting at? Nope. Emacs just did a straight delete, no safety net, no way back.

The file was tracked in git, so I could git checkout HEAD -- diff-minimap.el to get the committed version back. But the uncommitted work - the inline diff preview, the vc-diff positioning, the side-window hunk viewer - that was gone.

Emacs has a built-in variable, delete-by-moving-to-trash. When it is set to t, Dired (and other delete operations) move files to the system trash instead of permanently deleting them. On Linux that is ~/.local/share/Trash/files/, on macOS it is ~/.Trash/, on Windows it is the Recycle Bin.

It is one of those settings that seems obvious in hindsight but is easy to overlook because Emacs defaults it to nil. The reasoning is probably that Emacs is a serious tool for serious people who know what they are doing, so deleting means deleting?!

So I added it to my config:

(setq delete-by-moving-to-trash t)

That is it. One line. From now on, D in Dired moves to trash instead of deleting permanently and I will not have that sinking feeling again!

Anyway, back to diff-minimap.

-1:-- That Moment Dired Eats Your File and why Trash Saves You (Post James Dyer)--L0--C0--2026-06-30T09:17:00.000Z

Meta Redux: CIDER 2.0 is Brewing…

Normally I write these posts after a CIDER release is out, with the smug satisfaction of someone who has already tagged the version and updated the changelog. Today I’m flipping the script a little: the release isn’t out yet, but I’m too excited to keep quiet about it.

Here’s the thing. The next CIDER release was supposed to be 1.23. But as I kept piling change upon change, it slowly dawned on me that calling this “1.23” would be doing it a disservice. So I’m now strongly considering shipping it as CIDER 2.0 instead.

A different kind of release

The last few CIDER releases were on the conservative side - lots of important internal work, but relatively little that you’d actually notice day to day. This one is the exact opposite. It’s stuffed with user-visible changes, many of which have been sitting on the issue tracker and in the back of my mind for literally years.

Why now? Two reasons.

First, all that boring groundwork paid off. Some of you noticed I spent a lot of time recently backfilling tests and generally enriching the test suite. That wasn’t busywork - it was me building a safety net, so that finally making sweeping changes would be safe and pleasant instead of terrifying. Mission accomplished: I’ve been refactoring with abandon and sleeping just fine.

Second - and I know this is the controversial part - AI agents have been a genuinely great help. Not for writing CIDER for me (the design taste is still very much mine, thank you), but for quickly prototyping my half-formed ideas in different ways so I can see and feel them before committing to the approach I like best. Turns out “show me, don’t tell me” works wonders when the thing showing you can knock out a prototype in minutes. Boo me if you must, but when you’re the maintainer of so many projects and you have only so little time to spend on all of them that really makes a difference.

There’s a third ingredient worth mentioning: I’d been hammocking on most of this for months, quietly turning the designs over in the back of my mind while I was wrapping up CIDER 1.22. So while 1.22 itself took the better part of four months (and still shipped later than I’d have liked), the release right after it has been snapping together in a couple of weeks - the code was the easy part once the thinking was done. It helps that a lot of these changes close issues that had been gathering dust for years; the ClojureScript macroexpansion bug (#2099) was filed all the way back in 2017. Nothing beats hammock-driven development - even if it’s decidedly not the fastest way to ship anything.

OK, enough preamble. Let’s look at the goodies.

Keymaps you can actually discover

CIDER has a lot of commands, and historically the only way to find them was to memorize cryptic key chords or grep the manual. No more. Every command prefix now pops a transient menu (the same UI magit made famous), and there’s a top-level cider-menu that ties them all together:

Evaluate             Code              REPL & session           Diagnostics
 e Eval...            t Test...         j Insert into REPL...    r Trace...
 m Macroexpand...     n Namespace...    x Jack-in / connect...   p Profile...
 d Documentation...   w References...                            l Log...

Crucially, this isn’t a modal speed bump - your old muscle memory still fires instantly. C-c C-d C-d runs cider-doc as fast as ever; the menu only shows up if you pause after the prefix, wondering “wait, what else is in here?”

One tree view to rule them all

A bunch of CIDER’s browsers grew up independently and looked it. They now share a single, foldable tree-view widget - the namespace browser, the spec browser, and the new call-graph browsers all behave consistently (TAB to fold, n/p to move around). For example, exploring who calls a function:

who-calls clojure.core/reduce
  ▾ my.app.core/sum
      ▸ my.app.api/handler
  ▸ my.app.util/total

Expand a node and CIDER lazily fetches the next level. It’s a much nicer way to spelunk through a codebase than a flat list of strings.

Find usages, batteries included

Speaking of call graphs - CIDER now ships with genuinely capable cross-reference functionality out of the box. cider-who-calls/cider-who-is-called for the call graph, cider-who-implements for protocols and multimethods, plus xref-find-references (M-?) that searches your source and therefore finds usages even in code you haven’t loaded into the REPL yet.

The practical upshot: if you were reaching for clj-refactor.el or clojure-lsp mostly to find usages, you probably don’t need them anymore. (If you were using them for other things, carry on - we’re friends, not rivals.)

Much of the inspiration here came straight from swank-clojure and SLIME itself, which have offered this kind of cross-referencing for ages. Sometimes nothing beats revisiting the classics.

The debugging toolbox got a serious polish

This is the part I’m most happy about. Pretty much every “what is my code actually doing?” tool got some love.

The macro tooling is the headliner. The macroexpansion buffer finally grew a header line that shows the active expander and options, cycles namespace display and metadata in place, pulses the freshly-expanded form, and - at long last - says something useful when you point it at a special form or an unresolved symbol, instead of silently shrugging. Better still, there’s a brand new inline stepper (cider-macrostep, a Clojure spin on the venerable macrostep package) that expands macros right where they sit, one step at a time. Expandable sub-forms get underlined so you can hop between them with n/p, and every gensym is painted its own color, so you can finally follow where that g__1234 ends up. I did not expect macro debugging to be fun, and yet here we are.

Tracing got the same treatment. Traced calls no longer smear themselves all over your REPL output; they stream into a dedicated, foldable *cider-trace* buffer instead:

*cider-trace*
▾ (my.app/process {:id 7})
    ▸ (my.app/validate {:id 7})  => true
    ▸ (my.app/persist {:id 7})   => {:id 7, :saved? true}
  => {:id 7, :saved? true}

And because “wait, what did I even trace?” is a question I ask myself constantly, cider-list-traced and cider-untrace-all are now a keystroke away.

Enlighten (you remember it, right?) picked up some manners too. You can light up a single form with cider-enlighten-defun-at-point without flipping the global mode, and cider-enlighten-stop makes the whole thing vanish at once, instead of making you re-evaluate everything in penance. And for the tap> crowd, cider-tap opens a buffer that streams whatever you send to tap> and lets you crack any value open in the inspector with RET. It’s println debugging, minus the println guilt.

ClojureScript gets some love too

ClojureScript is always the trickier sibling, but it got meaningful improvements this round:

  • You can now run ClojureScript tests with the regular test commands (including async cljs.test tests) instead of CIDER refusing to play along.
  • Macroexpansion of user-defined cljs macros finally works (it used to silently echo the form back unexpanded - a bug that had been open since forever).
  • And when you invoke a Clojure-only command under a cljs REPL, CIDER now tells you so clearly, rather than failing in some confusing way.

This was another area where AI tooling was quite helpful to me, as I’ve rarely used ClojureScript in the past, but I still managed to figure out how to finally solve those problems that have been pain points for CIDER’s users for as long as we’ve had ClojureScript support.

On a related note, I’ve also been chasing down a few small bugs in Piggieback (the middleware that powers most of the cljs REPLs CIDER talks to) - have a look at the recent releases if you’re curious. And I’ve been idly pondering some form of “native” shadow-cljs support down the road. That last one is very much TBD, so don’t hold me to it - but the ClojureScript story keeps inching forward.

A pile of quality-of-life touches

It wouldn’t be a CIDER release without a long tail of small comforts. A few that I keep bumping into and grinning at:

  • Eldoc is asynchronous now, so dragging the cursor around no longer blocks Emacs on an nREPL round-trip. Buttery smooth.
  • A new cider-mode lighter (with an optional fringe marker) flags when the current buffer’s namespace isn’t loaded into the REPL, or has gone stale because you edited an already-evaluated form. That retires a whole genre of “why isn’t my change taking effect?” head-scratching.
  • cider-doc can pull ClojureDocs examples right into the doc buffer.
  • Sending a form into your namespace’s (comment ...) block is a one-keystroke affair now (cider-send-to-comment), and cider-jump-to-comment teleports you back there.
  • Stuck in .cljc land? You can pin where a buffer’s evaluations go - clj, cljs, or both - with cider-set-eval-destination and friends.
  • The REPL banner is slimmer and far less shouty; the getting-started spiel now lives in a summonable reference card (C-c C-h).
  • The cheatsheet finally learned about all the functions Clojure has grown since 1.11.

I’m probably still forgetting a dozen things - the changelog is genuinely enormous this time around.

Please go play with it!

Unfortunately no one can be told what CIDER 2.0 is – you have to experience it yourselves…

– Clorpheus

Here’s where you come in. All of this is already available in the snapshot release of CIDER on MELPA. I’d love for you to install it, kick the tires, and - especially - tell me how the various UX changes feel. Discoverable? Annoying? Joyful? I genuinely want to know before I carve any of it in stone.

If no serious problems surface, I plan to cut the real release fairly soon - think a week or two. So now’s the perfect time to influence it.

Epilogue

I keep hearing that the Clojure community isn’t innovating much these days. I hope CIDER 2.0 goes a small way towards convincing the doubters that we’re not quite done yet. The best is always yet to come - for both Clojure and Emacs.

Now let’s go forth and brew some (magic) CIDER together! Keep brewing!

-1:-- CIDER 2.0 is Brewing… (Post Meta Redux)--L0--C0--2026-06-30T09:00:00.000Z

tusharhero: Underappreciated builtin: Grand Unified Debugger

~1089 words. ~5 minutes.

Or as I would like to call it, GLORIOUS Unified Debugger.

This is my submission for June's Emacs Carnival, Underappreciated Emacs built-ins.

You can find detailed information in the excellent documentation at (info "(emacs) Debuggers").

Multi debugger support

I mostly use its GDB Graphical Interface. But it supports multiple debuggers such as,

  • lldb (LLVM debugger)
  • perldb (Perl debugger)
  • jdb (Java debugger)
  • pdb (Python debugger)
  • guiler (Guile!)
  • dbx (Debugger with support for C, C++, and so.)
  • xdb (Debugger for MS Windows (?))
  • sdb (System Debugger)

The GDB interface

The manual has a entire section of the special GDB GUI interface. Which I will illustrate now.

This is our demo source file: fibo.c,

 #include  

 int
 fibo ( int  n)
{
   if (n < 2)
     return 1;
   else
     return fibo (n - 1) + fibo (n - 2);
}

 int
 main ( void)
{
   for ( int  i = 0; i < 5; i++)
    printf ( "%d: %d\n", i, fibo(i));
}

Firstly, we compile via,

gcc -g fibo.c -o fibo

(The -g flag is necessary for debugging with GDB.)

Now that we have a compiled program, we can try executing it.

./fibo
0: 1
1: 1
2: 2
3: 3
4: 5

Now we can proceed with starting gdb gud in Emacs. To do so, we type M-x gdb RET fibo RET, and this creates a buffer called *gud-fibo*.

Current directory is /home/tusharhero/Documents/c-scratch/
GNU gdb (GDB) 17.2
Copyright (C) 2025 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
    .

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from fibo...
 (gdb) 

Though we won't be directly using this CLI interface, you can use it if you want to.

Let's set some breakpoints.

GDB breakpoints by clicking on the fringe.

Yup, you can just click on the fringe (the place left to the text), and automatically set up the breakpoints, which show up like so in *gud-fibo*.

Breakpoint 1 at 0x401138: file fibo.c, line 7.
Breakpoint 2 at 0x40113f: file fibo.c, line 9.
Breakpoint 3 at 0x401174: file fibo.c, line 16.
 (gdb)  

Now, we will start debugging by running the program. tool-bar-run-button.jpg

These buttons: Run, Next Line, Step Line, Up Stack, and Down Stack are part of the tool bar.

And immediately it stops at the first breakpoint it encounters, (breakpoint 3). The white arrow represents the current line that is being executed.

fringe-current-line-arrow.jpg

Now we can step into fibo function by using Step Line.

tool-bar-step-line.jpg

Notice that we also have a new button for Continue now, which continues until the next break point is hit. So, let us turn off the breakpoint in the for loop. For this we will use the dedicated breakpoints management window.

Now let's just spam the continue button a bunch of times.

And since we are calling printf in the for loop, there is a pop out dedicated window for I/O. (Notice that it is not just for output).

Well what if I want to see what the value of n is currently? There are actually a bunch of ways to look at that.

Here you just kinda put your cursor over the thing you want to get the value of … and you just get it? :)

The other way to use to the locals window,

But then who wants to keep track of this manually? Let's just watch the variable, so that we get update whenever it changes.

Closing

I would like to cover many other features, but I unfortunately don't have the time to get through all of them, maybe I will write a sort-of sequel covering more features and other debuggers supported by GUD.

-1:-- Underappreciated builtin: Grand Unified Debugger (Post tusharhero)--L0--C0--2026-06-29T18:30:00.000Z

Sacha Chua: 2026-06-29 Emacs news

This week, lots of people were talking about FSF's policy of not accepting LLM contributions to Emacs core (see the last two items in the AI category). Comments seem generally supportive of FSF's caution.

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View Org source for this post

You can e-mail me at sacha@sachachua.com.

-1:-- 2026-06-29 Emacs news (Post Sacha Chua)--L0--C0--2026-06-29T17:04:18.000Z

Raymond Zeitler: Emacs disabled Commands

It's possible to configure Emacs to prompt the user before calling a function, thereby "disabling" a function. The function isn't totally inaccessible -- Emacs will let you run it if you want it to. I chose to do this with scroll-left, scroll-right, scroll-up-command and scroll-down-command as explained in the previous post.

Most keypresses call self-insert-command, which is itself a function. So what would happen if self-insert-command were disabled?

It turns out to be not as debilitating as you'd think -- only the alpha-numeric and punctuation keys are affected. When a modifier is used with a key (M-x, for example), there is no interruption. Likewise, the TAB key is allowed to pass, which makes completion easier.

When Emacs inhibits self-insert-command, it prevents the keypress from having its intended effect. It will say that the command is disabled because many users find it confusing. Then you'll have the options of:

  • (n) Not running the command
  • (y) Run the command and don't ask again
  • SPC Run the command just one time to try it
  • (!) Enable the command and all other disabled commands

The n, y, SPC and ! keys are not inhibited at this point, so the user doesn't get trapped in a set of recursive prompts.

So to invoke org-agenda, I'd press M-x o. Then I'd press SPC to enable the self-insert-command for "o." Then I'd press r followed by SPC, and so on. My keypresses are M-x o r g - a g TAB ENTER.

Thus, adding (put 'self-insert-command 'disabled t) to a cube-mate's init file is a harmless prank, something to try on April Fool's Day.

-1:-- Emacs disabled Commands (Post Raymond Zeitler)--L0--C0--2026-06-29T02:40:43.489Z

Protesilaos: Emacs: fontaine version 3.1.0

Fontaine allows the user to define detailed font configurations and set them on demand. For example, one can have a regular-editing preset and another for presentation-mode (these are arbitrary, user-defined symbols): the former uses small fonts which are optimised for writing, while the latter applies typefaces that are pleasant to read at comfortable point sizes.

Below are the release notes.


Version 3.1.0 on 2026-06-29

This is a small release that includes internal refinements as well as two user-facing changes:

  1. The user option fontaine-presets accepts an optional :line-spacing entry, which corresponds to the line-spacing variable. As of Emacs version 31, line-spacing can be bound to a cons cell to set the space above and below. Fontaine now handles this as intended.

  2. By default, changing the font size has the effect of resizing the frame. This is because of the original value of the variable frame-inhibit-implied-resize. Fontaine is now designed to always inhibit frame resizing, regardless of frame-inhibit-implied-resize.

-1:-- Emacs: fontaine version 3.1.0 (Post Protesilaos)--L0--C0--2026-06-29T00:00:00.000Z

Monadic Sheep: Canvas patch: we need testers!

by Tushar
2026 June 29

The Canvas patch is almost done. Except that we need more testing, specifically for its MS Windows port. (Though testers on other operating systems like GNU/Linux and MacOS are also welcome.)

Since we don't have any computers running MS Windows, we are not sure if the code is actually correct.

Please follow the following instructions for testing it on MS Windows. You can discuss the results or ask us questions at MonadicSheep Emacs Fork's Issue tracker, #phi-mu-lambda on Libera IRC (open webchat in browser), or on the fediverse (tag the post with #emacs).

1. Building GNU Emacs on MS Windows

More detailed information is available here.

  1. Install MSYS2 by following the official instructions mentioned there.
  2. Open a MSYS2 UCRT64 session terminal.
  3. In the bash prompt, run the following commands to get the canvas patch source code.

    pacman -Sy git
    git clone https://codeberg.org/MonadicSheep/emacs
    
  4. Now, change directory to emacs source checkout, by running the following command.

    cd emacs/
    
  5. Install dependencies,

    pacman -Sy --needed base-devel autoconf \
    mingw-w64-ucrt-x86_64-toolchain \
    mingw-w64-ucrt-x86_64-xpm-nox \
    mingw-w64-ucrt-x86_64-gmp \
    mingw-w64-ucrt-x86_64-gnutls \
    mingw-w64-ucrt-x86_64-libtiff \
    mingw-w64-ucrt-x86_64-giflib \
    mingw-w64-ucrt-x86_64-libpng \
    mingw-w64-ucrt-x86_64-libjpeg-turbo \
    mingw-w64-ucrt-x86_64-librsvg \
    mingw-w64-ucrt-x86_64-libwebp \
    mingw-w64-ucrt-x86_64-lcms2 \
    mingw-w64-ucrt-x86_64-libxml2 \
    mingw-w64-ucrt-x86_64-zlib \
    mingw-w64-ucrt-x86_64-harfbuzz \
    mingw-w64-ucrt-x86_64-libgccjit \
    mingw-w64-ucrt-x86_64-sqlite3 \
    mingw-w64-ucrt-x86_64-libtree-sitter
    
  6. Now, run the following command to build Emacs.

    make
    

    This may take a while depending on your hardware.

  7. Once that is done, you should be able to start emacs with the following command.

    ./src/emacs
    

2. Testing

Evaluate the following in *scratch* (Using C-c C-e keybinding).

(defun make-rect (width height pixel)
  (make-vector (* width height) pixel))

(setq rect-canvas-vec (make-rect 250 250 #xFFFF0000))
(setq rect-canvas `(image :type canvas
                          :data-width 250
                          :data-height 250
                          :data ,rect-canvas-vec))
(insert (propertize "#" 'display rect-canvas))

(defvar rect-canvas-timer nil)
(let ((i 0))
  (setq rect-canvas-timer
        (run-with-timer
         0 0.016
         (lambda ()
           (if (< i (* 20 250))
               (progn
                 (aset rect-canvas-vec (+ (* 115 250) i) #xFF0000FF)
                 (canvas-refresh rect-canvas t)
                 (setq i (1+ i)))
             (cancel-timer rect-canvas-timer))))))

You should be able to see something like this.

-1:-- Canvas patch: we need testers! (Post Monadic Sheep)--L0--C0--2026-06-29T00:00:00.000Z

Please note that planet.emacslife.com aggregates blogs, and blog authors might mention or link to nonfree things. To add a feed to this page, please e-mail the RSS or ATOM feed URL to sacha@sachachua.com . Thank you!