Org Mode requests: [RFC] Font control as of today
-1:-- [RFC] Font control as of today (Post Org Mode requests)--L0--C0--2026-07-18T17:10:24.000Z
-1:-- [RFC] Font control as of today (Post Org Mode requests)--L0--C0--2026-07-18T17:10:24.000Z
All my notes live in Denote, the fantastic Emacs package by Protesilaos Stavrou. The system is perfect while I am at the computer... but I wanted to read and edit my notes from my phone. I did not find anything that respected the Denote convention, so I built it myself: Dotdenote, an iOS app that reads, edits and creates notes following the Denote naming scheme over WebDAV. It is open source, you can browse the code at git.andros.dev/andros/dotdenote.
:quality(85)/https://andros.dev/media/blog/2026/07/dotdenote-note-list.png)
:quality(85)/https://andros.dev/media/blog/2026/07/dotdenote-note-detail.png)
:quality(85)/https://andros.dev/media/blog/2026/07/dotdenote-settings.png)
The app does not lock your notes anywhere: it talks to any WebDAV server, so they remain plain text files under your control. In this mini tutorial we will set up a WebDAV server with Docker, put an Nginx in front of it, and connect the app to your existing Denote notes. In less than 10 minutes you will be browsing your Zettelkasten from your phone.
.org, .md or .txt files).We will use hacdias/webdav, a small and solid WebDAV server, behind an Nginx reverse proxy. Both run as containers.
Create a folder for the stack and copy your notes inside data/notes/:
mkdir -p denote-server/data/notes
cd denote-server
cp -r ~/Documents/notes/* data/notes/
The notes subfolder is important: the app uses /notes/ as its default notes path, so this layout works out of the box.
Create config.yaml with the server settings and your credentials:
address: 0.0.0.0
port: 6065
directory: /data
permissions: CRUD
users:
- username: denote
password: change-me-please
A couple of notes:
directory is the key used since v5 of hacdias/webdav (older tutorials mention scope, which no longer works).permissions: CRUD gives the user full read and write access, which is what the app needs to create and edit notes.Create nginx.conf. WebDAV uses HTTP methods and headers that a default proxy config does not forward, so we pass them explicitly:
server {
listen 80;
client_max_body_size 1g;
location / {
proxy_pass http://webdav:6065;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# WebDAV specific headers. Destination arrives as an absolute URL,
# rewrite it to a path so the backend accepts it.
set $destination $http_destination;
if ($destination ~ ^https?://[^/]+(/.*)$) {
set $destination $1;
}
proxy_set_header Destination $destination;
proxy_set_header Depth $http_depth;
proxy_set_header Overwrite $http_overwrite;
}
}
Create compose.yaml gluing everything together:
services:
webdav:
image: hacdias/webdav:latest
restart: unless-stopped
volumes:
- ./config.yaml:/config.yaml:ro
- ./data:/data
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- webdav
Your folder should now look like this:
denote-server/
├── compose.yaml
├── config.yaml
├── nginx.conf
└── data/
└── notes/
├── 20240322T131856--my-note-title__emacs_notes.org
└── ...
Start it:
docker compose up -d
The app checks the connection with a PROPFIND request, so let's do exactly the same with curl (replace the IP with the address of your server):
curl -u denote:change-me-please -X PROPFIND -H 'Depth: 1' http://192.168.1.50:8080/notes/
If you get an XML response listing your notes, the server is ready. If you get a 401, check the credentials in config.yaml; if you get a 404, check that your notes live inside data/notes/.
Download Dotdenote from the App Store: https://apps.apple.com/us/app/dotdenote/id6773266542
Open the app and fill in the setup form:
| Field | Value |
|---|---|
| URL | http://192.168.1.50:8080 |
| NOTES PATH | /notes/ (the default) |
| USERNAME | denote |
| PASSWORD | change-me-please |
Tap [ test connection ]. When you see the check mark, save, and your Denote notes will appear in the list: you can search them, open them, edit them, and create new ones. Every note the app creates follows the Denote convention (IDENTIFIER--title__keywords.ext) with the proper front matter for Org, Markdown or plain text, so everything stays perfectly compatible with Emacs.
The password is stored in the iOS Keychain, and the notes never touch any third-party service: the app only talks to your server.
The setup above is fine inside your home network or over a VPN like WireGuard or Tailscale. If you want to reach your notes from anywhere, do not expose plain HTTP: put a domain in front of the server and add TLS. The quickest path is to run Nginx on the host (or use a proxy like Caddy or Traefik) with a Let's Encrypt certificate from Certbot, pointing to 127.0.0.1:8080. Then use https://notes.example.com as the URL in the app, everything else stays the same.
Enjoy it!
-1:-- Your Denote notes on the iPhone (Post Andros Fenollosa)--L0--C0--2026-07-18T15:27:20.000Z
Some time ago, I installed Embark but I never got around to learning it and it has sat unused ever since. Then I saw this post by Bozhidar Batsov and decided to take another look. The problem with Embark is that it’s hard to understand and the documentation is long and complex. Too long, I discovered, to read and understand in one sitting.
I approached the problem by reading a section at a time and trying out examples in my Emacs. Even so, it still took some experimentation to completely figure things out. The process is ongoing but I’m making progress.
All of the above sounds pretty negative but I actually think Embark is a real win. Once you figure things out it’s really useful. For example, one of the things I’ve never been able to get working reliably is following a link in an arbitrary buffer to its target. Embark does that easily and if you use embark-dwim it’s a single keystroke. I use Hyper+. for embark-act and Hyper+, for embark-dwim. I have Hyper bound to the right ⌘ Cmd key so it’s very easy to invoke either of those commands.
Batsov is all in on Embark. He and I often share preferences and this is an example of that. The more I find out about Embark, the more I like it. The commands that work on a group of files can be difficult to grok but once you do they’re very powerful. I’ve resigned myself that learning Embark is an ongoing process but I think it’s worth it.
-1:-- Embark (Post Irreal)--L0--C0--2026-07-18T15:10:57.000Z
Nous allons parler en direct avec Richard Bonichon (Github) d'Emacs, de ses flux de travail, de sa configuration, et d'autres sujets en français. Je suis un peu intimidée, mais si vous posez vos questions dans le chat, je pense que nous pouvons survivre à mon faible niveau de français. =)
Notes : https://sachachua.com/blog/2026/08/le-6-aout-emacs-chat-richard-bonichon/
La diffusion en direct aura lieu le dans le fuseau horaire America/Toronto, soit 0730H EDT / 0630H CDT / 0530H MDT / 0430H PDT / 1130H UTC / 1330H CEST / 1430H EEST / 1700H IST / 1930H +08 / 2030H JST.
Si vous voulez parler d'Emacs avec Prot et moi en français, n'hésitez pas à m'envoyer un courriel !
You can e-mail me at sacha@sachachua.com.
-1:-- 6 aôut: Emacs Chat: Richard Bonichon (Post Sacha Chua)--L0--C0--2026-07-18T13:53:46.000Z
Nous allons parler en direct avec Abdallah Maouche (heiwiper) (Gitlab, Github) d'Emacs, de ses flux de travail, de sa configuration, et d'autres sujets en français. Je suis un peu intimidée, mais si vous posez vos questions dans le chat, je pense que nous pouvons survivre à mon faible niveau de français. =)
La diffusion en direct aura lieu le dans le fuseau horaire America/Toronto, soit 0730H EDT / 0630H CDT / 0530H MDT / 0430H PDT / 1130H UTC / 1330H CEST / 1430H EEST / 1700H IST / 1930H +08 / 2030H JST
Si vous voulez parler d'Emacs avec Prot et moi en français, n'hésitez pas à m'envoyer un courriel !
You can e-mail me at sacha@sachachua.com.
-1:-- 20 aôut: Emacs Chat: Abdallah Maouche (Post Sacha Chua)--L0--C0--2026-07-18T13:53:00.000Z
which-key is one of those packages I recommend to pretty much everyone -
newcomers and grizzled Emacs veterans alike. You start a key sequence like
C-x, pause for a moment, and it pops up a handy cheat sheet of every key that
can follow. I’ve had it in my config for ages, and I suspect a lot of you have
as well.
Here’s the good news - as of Emacs 30 you don’t need to install it anymore.
which-key is now a built-in package, so there’s one less thing to fetch
from MELPA when you set up Emacs on a fresh machine.
Being built-in doesn’t mean it’s enabled by default, though. You still have to turn it on explicitly:
(which-key-mode 1)
That’s the whole setup for most people. Now start any prefix key - C-x,
C-c, C-h - wait a moment, and a popup appears at the bottom of the frame
listing every binding under that prefix.
Here’s what I get when I press C-c p, the prefix for
projectile’s command map:

Instead of straining to remember whether it’s C-c p f or C-c p s s, I just
pause and Emacs reminds me. Projectile has a lot of commands, so which-key
even paginates them - notice the 1 of 3 in the bottom corner.
The defaults are sensible, but here are the knobs I reach for most often:
;; show the popup faster (the default is a full second)
(setq which-key-idle-delay 0.5)
;; where the popup shows up
;; options: side-window (default), minibuffer, frame, custom
(setq which-key-popup-type 'side-window)
;; and where that side window sits
;; options: bottom (default), top, left, right
(setq which-key-side-window-location 'bottom)
I like a slightly snappier delay, but otherwise the stock configuration serves me just fine.
People sometimes ask me how which-key relates to
Transient - the menu library that powers
Magit’s famous popups. The two look superficially similar (both drop a bunch of
keys and descriptions at the bottom of your frame), but they solve rather
different problems.
which-key is a passive, automatic reminder. It doesn’t really know anything
about your commands - it simply reads whatever keymap you happen to be in and
shows you what’s there. You get it for free, across every prefix in Emacs, with
zero per-keymap work.
Transient is an interactive command builder. You design a transient deliberately, for a particular workflow, and unlike which-key it can hold state
Projectile happens to ship both, which makes for a nice side-by-side. The
C-c p keymap you saw above is a plain keymap - which-key displays it and then
gets out of the way. But Projectile 3.0 also added projectile-dispatch (bound
to C-c p m), a Transient menu that mirrors the very same command map:

Notice the Modifiers section at the top - those are switches you flip before
picking a command. Want your next search to be case-sensitive, or your next
file lookup to invalidate the cache first? Toggle -c or -i, then press the
command key. That’s something a plain keymap (and therefore which-key) simply
can’t do.
So the way I see it - reach for which-key as an always-on safety net for the thousands of bindings scattered across Emacs, and reach for Transient when you’re building a polished, self-contained command center for a specific task. They’re complementary, and there’s no reason not to run both. In fact, if you use Magit or Projectile (or a dozen other modern packages), you’re already using Transient whether you realized it or not.
Long-time Emacs users might point out that you’ve always been able to press
C-h after a prefix key to see the available bindings. That’s true! The
difference is that which-key does it automatically, no extra keypress
required - which makes it far more discoverable, especially for newcomers.
And the two don’t step on each other. C-h after a prefix still works and
gives you the traditional describe-bindings buffer, which you can scroll and
search to your heart’s content. Think of which-key as the quick glance and
C-h as the full reference.
I’ve been a happy which-key user for years, and having it in the box means one
less package to install and keep updated. If you’ve somehow never tried it, do
yourself a favor and flip it on - it’s the kind of small quality-of-life feature
you stop noticing precisely because it’s always quietly helping you.
Do you keep which-key on all the time, or do you prefer to lean on C-h? And
have you started building your own Transient menus yet? I’d love to hear about
it in the comments.
That’s all I have for you today. Keep hacking (and let Emacs remember the keys for you)!
-1:-- which-key is Now Built-in (Post Emacs Redux)--L0--C0--2026-07-18T07:00:00.000Z
Sayid 0.8 is out! It’s the third release since I brought Sayid back from the dead a couple of weeks ago, and it has a clear theme: making the tool easy to pick up. The revival releases were mostly about the engine - bounding the recording, consolidating the API, getting the data out. This one is about the experience. If you’ve ever bounced off Sayid because you couldn’t figure out what to press, or what it was trying to tell you, 0.8 is for you.
Shortly after the revival post, someone reported that pressing c i in the
workspace view - “inspect this captured value” - printed Def'd as $s/* and
then… nothing. The fix turned out to be a one-liner: Sayid was calling a
CIDER function whose signature changed years ago, and nobody had noticed since.
Which tells you everything about how many people were actually using that
command.1
The one-liner was easy, but the report got under my skin. If the inspector integration could sit broken for years, what else about Sayid was quietly hostile to anyone trying it for the first time? So I sat down and did a proper UX audit of the Emacs client, wrote down everything that made me wince, and 0.8 is the result.
Here’s the current state of affairs in one take - trace, run, explore:

Before we get to the UX work, a quick recap of the two releases I never got around to announcing (the revival post covered things up to 0.5).
Sayid 0.6 rebuilt inner tracing - the mode that records every
intermediate expression inside a function - on top of tools.analyzer.jvm.
The old implementation re-read your source and rewrote raw forms, with special
cases for individual macros; the new one works off the analyzed AST. That
killed the long-standing bug where an inner-traced try/catch would swallow
exceptions, along with the per-macro special-casing that made the old
instrumenter so fragile.
Sayid 0.7 made the trace itself data. sayid.data/trace-data
returns the recorded call tree as plain Clojure data with the live captured
values, and tap-trace! sends it to tap>, so you can explore a recording in
Portal or your data tool of choice. It also added sayid.golden - capture
a run’s call tree as a baseline, then assert future runs still match it, which
is a surprisingly pleasant way to pin down the behavior of gnarly legacy code
before refactoring it.
Now, the 0.8 goodies. The full list is in the changelog, but here’s what I’m most excited about:
C-c s pops up a transient menu
(sayid-menu) that groups the commands along Sayid’s core loop - trace
something, run your code, explore the recording - and shows you how much is
traced and recorded right now, or what’s missing (a REPL, the middleware) when
you’re not connected. The menu uses the same key sequences as the classic
prefix map, so your muscle memory keeps working, and sayid-use-menu brings
the plain keymap back if popups aren’t your thing.
sayid-trace-fn (C-c s t t) is the new “just trace this” command. You no
longer need to know what inner and outer traces are before you can trace your
first function - the default does the right thing, and you can graduate to
the fancier variants later.C-c s w shows what was recorded”), and trying to enable a trace
that doesn’t exist tells you the actual problem instead of pretending
everything went fine.c i - yes, it works now),
def it to a var for REPL poking (c d), pretty-print it (c p), or copy an
expression that reproduces the call (c r). C-c s f - show the recorded
calls of the form at point - renders there too.g refreshes the Sayid buffers like every other
Emacs buffer, and resetting the workspace asks before irreversibly dropping
your traces and recording.deps.edn
pins an older Sayid jar than your Emacs package expects, things degrade
politely instead of erroring in strange ways.cider-tree-view throughout.Nothing here should break a working setup, but two things are worth knowing.
The Emacs package now requires CIDER 2.0 (released earlier this week), so
they’ll need to be upgraded together. And C-c s now opens the menu instead of
acting as a bare prefix - every old key sequence still works exactly as typed,
but if you prefer the old silent prefix, set sayid-use-menu to nil. A few
commands got more consistent names (sayid-trace-fn-outer and friends); the
old names live on as obsolete aliases.
When I wrote the revival post I asked people to kick the tyres and tell me what feels rough. This release is what acting on that feedback looks like, and it’s exactly the kind of contribution I need more of - a two-line bug report turned into the biggest usability overhaul in the project’s history. So thanks to everyone who has been trying out the revived Sayid, and please keep the reports coming on the issue tracker.
[mx.cider/sayid "0.8.0"] is on Clojars, the Emacs package is on MELPA, and
tracing your first function is now a single C-c s t t away. Give it a spin!
Keep hacking!
In fairness, it also tells you something about me - the bug shipped with my own resurrection releases. Reviving a decade-old codebase means inheriting a decade of API drift, and some of it only surfaces when a real user presses a real key. ↩
-1:-- Sayid 0.8 (Post Meta Redux)--L0--C0--2026-07-18T07:00:00.000Z
Projectile 3.2 is out!1 That’s the third Projectile release this month, and by now you probably see the pattern - a whole lot of nothing for a couple of years, then everything at once. Where 3.0 was the big cleanup and 3.1 the pile of long-standing feature requests, 3.2 is a focused release with one clear theme: search and replace. Plus one bonus feature I’ve wanted for ages, but more on that below.
projectile-replace has always been one of those commands that technically
worked, but that I never quite trusted. It walks the project file by file with
query-replace, asking you about every match with no way to see the big
picture, and once you start there’s no graceful way out. I’ve been promising
myself a better story there for years (that’s #1924, if you’re keeping
score at home).
3.2 finally delivers it. projectile-replace-review (s-p R) prompts for a
search string and a replacement, gathers every match across the project into a
*projectile-replace* buffer, and shows you a per-match preview of exactly
what’s about to change. Every match can be toggled on or off, and ! applies
just the enabled ones - in any order, whenever you’re ready. Here’s the whole
flow, start to finish:

The matches are gathered in Emacs Lisp rather than by shelling out to grep, so
projectile-replace-regexp-review honours full Emacs regexp syntax (yes,
including \_<foo\_> and capture groups in the replacement), and the preview
reflects the exact text that will be edited - including unsaved changes in
buffers you already have open. The apply step is equally paranoid: files are
edited bottom-up so earlier edits can’t shift later matches, open buffers get a
single undo step, and buffers modified since the scan are skipped rather than
clobbered.
The results buffer is a little workbench of its own - you can toggle case
sensitivity (c), flip between literal and regexp matching (x), keep or
flush matches by line or by file (k/d/K/D), and export everything to a
grep-mode buffer (e) if you’d rather finish the job with wgrep. And both
reviewers scan the project asynchronously, so a big search no longer freezes
Emacs - matches stream in as they’re found and you can bail out at any point.
Once the replace reviewer existed, the obvious next question was “can I have
this without the replacing part?”. You can. projectile-search-review
(s-p s R) collects every match for a term into a read-only
*projectile-search* buffer, grouped by file, with the matched spans
highlighted:

It shares the replace reviewer’s navigation and filtering, and pressing r
hands the current search over to the replace reviewer, carrying along the term
and settings and prompting only for the replacement. So “look first, replace
later” is now a single keystroke, not a re-do.
My favourite touch: when rg is installed, a literal search rides ripgrep and
streams matches in near-instantly even on huge projects (customizable via
projectile-search-use-ripgrep). The regexp variant and the replace reviewer
always use the portable Emacs Lisp scan, so the preview you act on is always
the real thing.
The bonus feature: projectile-session-mode, a global minor mode that gives
each project its own tab-bar tab. Switch to a project and Projectile either
selects its existing tab (with the window layout you left there) or opens a
fresh one named after the project. Sessions can also persist across restarts -
s-p w s saves a project’s layout and buffers, switching to the project can
restore it automatically, and projectile-session-restore-all brings your
whole working set back after a restart, each project in its own tab.
I’ve long envied the “workspace per project” workflows people build with
tab-bar and a handful of hooks, and now Projectile just does it out of the
box. It’s opt-in, so nothing changes unless you enable the mode.
A few smaller things that shipped alongside the headliners:
w in the results buffer, or default it with projectile-search-whole-word.projectile-dispatch picked up matching switches - --word and
--case-sensitive seed the reviewable search/replace, the same way
--regexp already did for the other search commands.marginalia annotates candidates properly (buffers get buffer
annotations, projects get file ones) and embark can act on them. With
vertico and marginalia the built-in commands look downright fancy now:
No breaking changes this time, and nothing to migrate - the full list of changes is in the changelog, as always.
More importantly, 3.2 completes - for now - the arc I embarked on with 3.0. Looking back at the whole 3.x series, the mission boils down to three things:
All three boxes feel properly ticked at this point, so the summer release burst is probably winding down.2 Projectile is in the best shape it’s been in years, and I’m having more fun working on it than I’ve had in a long time. Huge thanks to everyone who reported issues and tested the new reviewers while they were taking shape!
Keep hacking!
-1:-- Projectile 3.2 (Post Meta Redux)--L0--C0--2026-07-18T06:15:00.000Z
This month I'd like to host the Emacs Carnival! I've been an enthusiastic participant for a while now. I wrote entries for May, June, and July (that last one hosted by Andy over at Plain DrOps). Now it's my turn to hold the torch.
Reading other people's carnival posts has consistently been the highlight of my month, and I find I learn a lot through this structured prompting for insight from other Emacs users.
This month's topic is "The Search for Knowledge". This combines a couple of concepts that I think are near and dear to the hearts of many Emacsapiens, and have seen a recent surge in attention:
I'm hoping to assay the community for solutions to these complex endeavors. Emacs, in my opinion, is the world's most capable text processing environment, and so it only makes sense that it would have best-in-class solutions and workflows for managing complex information and synthesizing precious knowledge from it.
There are so many solutions to these problems, and we see a veritable embarrassment of riches in Emacs along our intrepid search for knowledge. I'm hoping this carnival can crystallize some of the best solutions and workflows.
Some topic ideas, but feel free to bring your own!
See the wikipage on the topic here: Emacs Carnival.
This is our first Emacs Carnival, so you may not know the format. A blog carnival is a fun way to tie together a community with shared writing prompts, and marvel at all the creative interpretations of the topic of the month. I’ve provided a couple of interpretations above, but you may think of something else entirely. That’s amazing, roll with it, that’s what makes this fun!
— Christian Tietze
I want to emphasize that a carnival is supposed to be fun. And I'd also like to say that if you've ever considered writing a post but haven't found the time or space, the Emacs carnival is the best way to give it a crack!
Getting involved is easy:
#EmacsCarnival so the wider community sees it too. You can also DM me on reddit at u/misterchiply, I hang out there a lot!Don't overthink it! Even a few honest paragraphs about how you find things in your notes make a great entry.
I'll keep this list updated throughout August as entries come in. Check back through the month, and look out for the round-up once the carnival closes.
No submissions yet — yours could be the first!
-1:-- The Search for Knowledge: Emacs Carnival August 2026 (Post Charlie Holland)--L0--C0--2026-07-17T15:42:51.000Z
When you write a tree-sitter major mode, it’s tempting to think the
parser handles everything. It doesn’t. Tree-sitter drives font-lock,
indentation, and structural navigation, but a whole layer of everyday
Emacs behavior still runs on the humble syntax table: forward-sexp
and friends, electric-pair-mode, delete-pair, forward-word, and
anything built on syntax-ppss. (I wrote about that structural layer in
Essential Structured Navigation and Editing Commands.)
I got a sharp reminder of this while building
neocaml, my tree-sitter mode for
OCaml. The grammar parsed everything beautifully, yet delete-pair and
sexp motion kept misbehaving around a couple of OCaml constructs. The
fix turned out to be a classic tool that long predates tree-sitter:
syntax-propertize-function. Here’s the story - it tripped me up for a
good while, so maybe it’ll spare you some head-scratching.
A syntax table assigns each character a single class: this one opens a string, that one is a comment starter, this is a word constituent. That works until a character means different things in different places. OCaml has two such troublemakers.
Character literals. 'a' is a character, and the single quotes
delimit it. But the same ' also starts a type variable ('a in
'a list), and can appear inside identifiers as a prime (x'). So you
can’t just declare ' a string delimiter. And if you leave it as a
symbol constituent (the usual choice), a character literal whose contents
happen to be a delimiter wreaks havoc:
let x = '"' (* the " opens a string, as far as the syntax table knows *)
let y = '(' (* the ( is an unbalanced open paren *)
Quoted string literals. OCaml’s {|...|} (and tagged {foo|...|foo})
raw strings can contain anything - including ", (*, and friends -
which the syntax table reads as real string/comment delimiters:
let z = {|a "b" (* not a comment *)|}
You can see the damage with syntax-ppss. With nothing but a static
table, point at the end of let x = '"' reports that you’re inside a
string:
(nth 3 (syntax-ppss)) ;; => 34 (i.e. inside a string opened by ")
And every command that consults the syntax table inherits the confusion:
C-M-f walks off into nonsense, delete-pair grabs the wrong delimiter,
electric-pair-mode autopairs incorrectly.
Note that font-lock looks fine here - tree-sitter fontifies these constructs correctly from the parse tree. That’s exactly what makes the bug sneaky: the buffer looks right, but the syntactic layer underneath is lying.
syntax-propertizeThe escape hatch is
syntax-propertize-function:
a buffer-local function that runs lazily over regions of the buffer and
applies syntax-table text properties to override the static table
where context demands it. Because the properties are attached to specific
positions, ' can be a string delimiter in 'a' and an ordinary symbol
character in 'a list - in the same buffer.
The usual way to write one is with syntax-propertize-rules, which maps
regexps to the syntax classes to apply to their capture groups. Here’s
the heart of neocaml’s:
(defun neocaml--syntax-propertize (start end)
(goto-char start)
(funcall
(syntax-propertize-rules
;; Character literals: 'a', '\n', '"', '(', ...
;; Mark both quotes as string delimiters so the contents are inert.
;; A closing quote is required, so type variables ('a) are untouched.
("\\_<\\('\\)\\(?:[^'\\\n]\\|\\\\.[^\\'\n \")]*\\)\\('\\)"
(1 "\"") (2 "\""))
;; Quoted strings {tag|...|tag}: fence both ends so the body is inert.
("\\({\\)\\([[:lower:]_]*\\)|"
(1 (let* ((tag (match-string 2))
(close (save-excursion
(when (search-forward (concat "|" tag "}") end t)
(point)))))
(when close
(put-text-property (1- close) close
'syntax-table (string-to-syntax "|")))
(string-to-syntax "|")))))
(point) end))
and you install it in the mode body:
(setq-local syntax-propertize-function #'neocaml--syntax-propertize)
Two kinds of rules are at work:
'...' and gives both
quotes string-quote syntax ("). The contents then sit inside a
string as far as the scanner is concerned, so an inner " or ( is
inert. Crucially, the regexp requires a closing quote, so a bare
'a type variable never matches and keeps its symbol syntax.{tag|, searches for the
matching |tag}, and marks both the opening and closing delimiters with
generic string fence syntax (|). Everything between two fences is a
string, so embedded quotes and comment starters are neutralized.With that in place, syntax-ppss tells the truth again, and C-M-f,
delete-pair, and electric-pair-mode all behave - including for the
delete-pair corner case I wrote about earlier.
A few things worth keeping in mind, whether or not you touch OCaml:
syntax-propertize whenever a character’s role depends on
context - raw strings, here-docs, regex literals, character literals,
JSX, anything a single syntax class can’t capture. It’s exactly what
rust-ts-mode and c-ts-mode reach for, too.syntax-ppss from inside your syntax-propertize-function.
It’s re-entrant (syntax-propertize is itself driven by syntax-ppss)
and a fragile source of subtle bugs. Match constructs whole from their
opening delimiter instead, as the quoted-string rule above does, rather
than asking “am I currently inside a string?”." (string quote) is delimited by the
same character, while class | (generic string fence) pairs up
independently of the specific character. The latter is handy for
multi-character or asymmetric delimiters like {tag| … |tag}.None of this is new - syntax-propertize has been the right answer since
Emacs 24. But it’s easy to forget it exists when you’re deep in
tree-sitter land, and the symptoms (movement and pairing going subtly
wrong while the colors look perfect) are puzzling until you remember which
layer owns what.
Have you run into this while writing your own tree-sitter modes? I’d love to hear how you tamed the syntax table in the comments.
That’s all I have for you today. Keep those parens balanced!
-1:-- Tree-sitter Modes Still Need a Syntax Table (Post Emacs Redux)--L0--C0--2026-07-17T09:15:00.000Z
As part of the ongoing overhaul of my Emacs setup
I’ve been trying to make the most of the built-in functionality that recent Emacs
releases keep quietly shipping. My in-buffer completion setup is
based on corfu and cape
these days, but it turns out I had overlooked a nice Emacs 30 addition
in the same area - completion-preview-mode.
It gives you inline completion suggestions - the “ghost text” UI that GitHub
Copilot and friends made famous1 - except here it’s powered by plain old
Emacs completions.
As you type, completion-preview-mode shows a grayed-out preview of the top
completion candidate right after point - inline, in the buffer itself. No popup,
no minibuffer, no fuss. If the suggestion is what you want, press TAB to accept
it. If not, just keep typing and the preview updates (or goes away).

The part I like best - the candidates come from completion-at-point-functions,
which means the preview is fed by the exact same backends as corfu and
company. If you’ve already set up cape-dabbrev, cape-file or eglot,
all of them power the ghost text automatically. Batteries very much included.
You can try it out in the current buffer with M-x completion-preview-mode.
If you like what you see, you can enable it everywhere:
(global-completion-preview-mode 1)
Here’s the relevant bit of my config:
(use-package completion-preview
:ensure nil ; built-in
:config
;; cycle through the other candidates with M-n/M-p (those two
;; commands have no default bindings)
(define-key completion-preview-active-mode-map (kbd "M-n") #'completion-preview-next-candidate)
(define-key completion-preview-active-mode-map (kbd "M-p") #'completion-preview-prev-candidate)
(global-completion-preview-mode +1))
While a preview is visible you can make use of the following keybindings:
TAB (completion-preview-insert) - accept the suggested completionM-i (completion-preview-complete) - insert only the longest common prefix
of all the candidates (the preview underlines it, so you know in advance what
you’ll get) and let you keep typing from thereM-n / M-p (completion-preview-next-candidate / completion-preview-prev-candidate) -
cycle through the other candidates; those are not bound by default, but the
docs themselves suggest M-n and M-p (see my config above)Here’s M-i in action. I had typed my-pro, and pressing M-i filled in
ject-find-, stopping exactly where the two candidates (my-project-find-file
and my-project-find-dir) diverge:

And when you cycle with M-n, Emacs even tells you where you are in the
candidate list in the echo area:

There’s basic mouse support as well - clicking the ghost text with mouse-1
inserts it, and scrolling the mouse wheel over it cycles through the candidates.
(I doubt I’ll ever use that, but it’s kind of cute)
A few user options to adjust the behavior:2
;; show the preview only after typing at least 3 characters (the default)
(setopt completion-preview-minimum-symbol-length 3)
;; wait a bit before showing the preview (by default it shows up instantly)
(setopt completion-preview-idle-delay 0.2)
;; show a preview only when there's exactly one candidate
(setopt completion-preview-exact-match-only t)
There’s also completion-preview-commands - the list of commands after which
the preview appears (things like self-insert-command). You’ll rarely need to
touch it, but it’s good to know it’s there if some command you use doesn’t
trigger the preview.
When I first read about completion-preview-mode I assumed it was competing
with corfu and company, but I’ve come to think that’s the wrong way to look
at it. They draw candidates from the same source and simply present them
differently - corfu gives you the full candidate list with annotations and
documentation, while the preview gives you the single most likely candidate
with zero visual ceremony. These days I run both: the ghost text handles the
“obviously I meant that symbol” cases with a single TAB, and corfu’s popup
kicks in when I actually need to browse.
Admittedly, two completion UIs at once won’t be everyone’s cup of tea. If you
find it too busy, a nice middle ground is to enable completion-preview-mode
only where a popup feels out of place - e.g. in eshell-mode or comint-mode
buffers.
Emacs 30 keeps chipping away at my list of third-party packages - which-key
and EditorConfig support are now built-in, and completion-preview-mode
covers a niche I didn’t even know I wanted covered. Not bad for a “boring”
stable release!
Have you tried completion-preview-mode already? Are you a ghost text person
or a popup person? (or both, like me) I’d love to hear your thoughts in the
comments!
That’s all I have for you today. Keep completing!
Unlike the AI assistants, the suggestions here come straight from your
buffers and your completion-at-point-functions. No subscription needed
and no hallucinations included. ↩
If you’re wondering what’s this setopt thing - check out
this article. ↩
-1:-- Inline Completion with completion-preview-mode (Post Emacs Redux)--L0--C0--2026-07-17T08:45:00.000Z
This is another installment in the series of articles about the notable changes in CIDER 2.0. Today’s topic is one of those “ambitious ideas that lay dormant for ages” I keep mentioning: proper interactive macro stepping.
CIDER has had macroexpansion support practically forever - C-c C-m expands
the form before point into a dedicated buffer, a feature we inherited
spiritually from SLIME. It works, but it has always felt a bit… detached. The
expansion lives in another buffer, divorced from the code you’re reading, and
for deeply nested macros you end up bouncing between buffers trying to keep
your bearings.
Emacs Lisp hackers have long had something nicer: macrostep, a brilliant little package that expands macros in place - right where they sit in your code - one step at a time, and collapses them back when you’re done. I’ve wanted a Clojure version of this for years. CIDER 2.0 finally ships one.
I’m hardly the first person to want this. The idea of bridging CIDER and
macrostep goes back to at least
2016, where a proof of
concept wired up macrostep’s extension hooks to CIDER by injecting a couple of
helper functions into your REPL and shuttling forms back and forth as strings.
Later, macrostep-geiser - a
Scheme-oriented macrostep backend - grew CIDER support as well, and SLIME
itself ships a slime-macrostep contrib built on the same extension API. So
the hooks were there, the hacks existed, and I could have blessed one of them
and called it a day.
I opted for a clean, from-scratch implementation in CIDER instead - mostly to provide the best possible experience for Clojure programmers, without the compromises the bridges had to make. macrostep’s extension API was designed around Emacs Lisp’s happy circumstances, and Clojure violates most of them:
macrostep-expand-1-function must be synchronous. That’s fine when
expansion is an elisp function call; it’s less fine when it’s a network
round-trip to an nREPL server. (CIDER has been busy removing exactly this
kind of blocking call lately - asynchronous eldoc being the poster child.)#() lambdas and namespaced maps have no
faithful elisp representation, so every bridge ends up round-tripping code
through strings and hoping the quoting survives. The 2016 proof of concept
literally did (format "(expand-once '%s)" form), which should make anyone
a little nervous.macrostep-print-function expects the client to print and fontify the
expansion. In CIDER the server does the printing - that’s how we get
namespace tidying (when instead of clojure.core/when), print-option
handling and metadata display for free. A client-side printer would have to
reimplement all of that, badly.There’s also a less technical reason: lately I’ve grown rather averse to adding third-party dependencies to CIDER. A dependency is a bet on someone else’s continued enthusiasm, and such bets sometimes go bad - tellingly, macrostep itself spent a stretch unmaintained and now lives in a GitHub org literally named “emacsorphanage” (it has since found a new maintainer, but the point stands). CIDER is 14 years old and intends to stick around; over that kind of horizon, owning ~600 lines of overlay code is cheaper than adopting someone else’s semi-abandoned package. So we kept macrostep’s brilliant UX ideas and its familiar keybindings, and left the coupling behind.
The entry point is cider-macrostep-expand (C-c M-m e). Put point after a
macro form, invoke it, and the form is replaced inline with its one-step
expansion, highlighted so you can tell what’s expansion and what’s your code:
(when-let [x (fetch-thing)]
(process x))
;; C-c M-m e =>
(let [temp__5825__auto__ (fetch-thing)]
(when temp__5825__auto__
(let [x temp__5825__auto__]
(process x))))
From there you’re in cider-macrostep-mode, where the further-expandable
sub-forms are underlined, n/p hop between them, e (or RET) steps into
one, and c/q collapse one level or everything back to the original code.
Your buffer is never
actually modified in a way that sticks - collapse everything and it’s exactly as
it was.
Here’s the whole flow in motion - expand, step into a nested macro, collapse back to the original code:

A couple of touches I’m particularly fond of:
temp__5825__auto__ flows through the expanded code. Once
you’ve seen a for expansion with colorized gensyms, you won’t want to go
back:
E (cider-macrostep-expand-all) fully expands the form in one step, for
when you don’t care about the journey.b (cider-macrostep-expand-in-buffer) runs the same stepping session in a
dedicated popup, leaving the source buffer untouched - handy when you’re in
someone else’s code and feel uneasy about inline rewrites, however temporary.The traditional macroexpansion buffer wasn’t neglected either: it grew a header
line showing the active expander and display options, n and t cycle
namespace display and metadata in place, g re-expands with the latest macro
definition, and freshly expanded forms pulse briefly so your eye lands in the
right place.
The expansion commands also got more talkative: pointing them at an unresolved symbol now tells you whether the namespace simply isn’t loaded yet (evaluate the buffer!) or you’ve got a typo, instead of silently doing nothing.1
Macros are the part of Clojure people are most likely to describe as “magic”,
and the standard advice - “just macroexpand it” - has always carried a hidden
tax: the expansion of any non-trivial macro is a wall of gensyms and nested
let*s that’s genuinely hard to read cold. Stepping through the expansion one
level at a time, in place, with the gensyms color-coded, turns that wall into
something you can actually follow. I did not expect macro debugging to become
fun, and yet here we are.
All the details are in the macroexpansion docs. Give it a try the next time a macro surprises you - and keep hacking!
Amusingly, we initially overdid these diagnostics - the guard refused to
expand let and fn (which are macros wearing special-form badges) and
broke a beloved trick of using macroexpansion to normalize reader syntax
like ::auto/keywords. See
#4111 - fixed right
after 2.0. Even diagnostics need diagnostics. ↩
-1:-- Stepping Through Macros in CIDER (Post Meta Redux)--L0--C0--2026-07-17T05:15:00.000Z
Hot on the heels of CIDER 2.0, clj-refactor.el 4.0 is out! It’s the first major release of the project in almost five years, and this time around the version bump is not just ceremonial - 4.0 is the biggest batch of user-facing improvements clj-refactor has seen in ages, plus a healthy dose of long-overdue spring cleaning.
When I announced clj-refactor.el 3.0
back in 2021, I opened with a confession - we had failed to deliver the main
objective of that release, namely restoring the dependency hot-loading that
JDK 9’s module system broke. Well, better late than never:
cljr-hotload-dependency is back in 4.0, reimplemented on top of
clojure.tools.deps, and it even accepts deps.edn-style coordinate maps
alongside the classic Leiningen vectors.1
I’ll also be honest about the bigger picture, as I was back then:
clj-refactor spent most of the intervening years in maintenance mode, while
clojure-lsp became the default refactoring engine for much of the community.
That hasn’t discouraged me, though - it clarified what clj-refactor should
be. The project now has a proper roadmap that spells out where
things are headed (spoiler: a static analysis index and closer ties with
CIDER), and 4.0 is the first big step in that direction.
The changelog for 4.0 has over 40 entries, but here’s what I’m most excited about:
clj-refactor-menu - a transient menu of every command,
grouped by category (bound to hh under your prefix, e.g. C-c C-m hh).
It mirrors the classic two-letter mnemonics, so it doubles as a way to
finally learn them. It replaces the old hydra-powered menus, which allowed
us to drop the hydra dependency.cljr-rename-symbol,
cljr-change-function-signature and cljr-inline-symbol now show you their
edits as a diff and touch nothing until you confirm, and the new
cljr-undo-last-refactoring (ur) reverts the last applied refactoring
across every affected file in one step.cljr-change-function-signature grew up: it can add and remove parameters
now (not just reorder and rename them), it understands multi-arity
functions, and a long-standing parsing bug that broke it against recent
CIDER releases is fixed.cljr-slash (the magic requires feature) learned a neat trick: entries in
cljr-magic-require-namespaces can carry an :artifact coordinate, and
when the namespace isn’t on your classpath, typing json/ will offer to add
the library to your project and hotload it on the spot.cljr-slash and
cljr-add-missing-libspec fall back to the static alias table,
cljr-clean-ns falls back to sorting the ns form, and cljr-remove-let and
the #(...) -> (fn ...) promotion are handled purely on the Emacs side.
Refactoring your requires before the REPL is up is no longer a paper cut.cljr-slash fires on every /
keystroke), and the startup cache warming now actually populates the cache
the interactive commands read from.refactor/clean-ns and friends), with a graceful fallback for older
middleware versions. The companion refactor-nrepl 3.14.0
release also hardened the AST analysis and made find-usages noticeably
faster on projects with many macros.multiple-cursors, hydra and inflections
are gone, along with a pile of aliases that had been deprecated since 2.3.0.
clj-refactor is a much leaner package than it used to be.cljr-describe-refactoring now
opens the wiki page in your browser instead of scraping HTML,
the dependency commands give clear errors instead of raw search failures,
and clojure-ts-mode users get a working offline ns sort and proper setup
docs.4.0 is a major release and it does come with breaking changes: the minimum
requirements are now Emacs 28.1 and CIDER 2.0, the long-deprecated 2.3.0-era
command aliases are gone (use the clojure-* equivalents that live in
clojure-mode these days), and the legacy namespace-aliases code path in
cljr-slash was removed along with its three configuration options. On the
middleware side you don’t need to do anything if you use cider-jack-in -
refactor-nrepl 3.14.0 gets injected automatically, and older versions keep
working thanks to the op-name fallback. Most setups should upgrade without
touching a thing, but do skim the changelog if you have a
heavily customized configuration.
Reading the 3.0 announcement again, I wrote back then that we wanted to show “we haven’t reached the limit of what one can do with a purely (mostly?) REPL-powered approach to refactoring”. I still believe that, and 4.0 is hopefully some evidence. The roadmap has the rest of the plan - including making the analysis fast and robust enough to erase the reasons people reach for other tools - and with CIDER 2.0 out the door I finally have some cycles to spend here.
Huge thanks to everyone who contributed to clj-refactor and refactor-nrepl over the years, to Clojurists Together for supporting the broader CIDER ecosystem, and to everyone who kept filing issues during the quiet years - they shaped most of what shipped in this release.
In the REPL we trust!
Only five years late. In my defense, nobody said when it would be restored. ↩
-1:-- clj-refactor.el 4.0 (Post Meta Redux)--L0--C0--2026-07-16T15:57:00.000Z
I feel like a bad Emacs user because, while I've been using it for a long time, it's only been in the past few months that I have embraced Orgmode as my task manager. Sometime I will probably write about my journey to this point, but for now I want to talk about a small annoyance I had in org-agenda.
I use org-agenda many times a day. I used to use the Week view (C-c a a) because it gives me an overview of what I finished and what is coming up. The problem is that a week is a fixed box. On Monday I saw a full week ahead of me. By Friday that list was mostly useless because all I saw were the tasks I'd finished earlier in the week and the few I had left, with next week's work still invisible – it won't show up until next week.
But that's not how I work. By Thursday I don't generally care what I finished on Monday, but I'd love a peek at what's coming. What I wanted wasn't a calendar week at all – I wanted a window that follows me, always showing a little behind and a little ahead, with today sitting near the middle.
In typical Emacs fashion, this was easy to do – it was three lines:
(setq org-agenda-span 6
org-agenda-start-day "-2d"
org-agenda-start-on-weekday nil)
The first two options make a lot of sense:
org-agenda-span 6: show six days instead of a rigid calendar week.org-agenda-start-day "-2d": start two days before today. Org accepts relative date strings like -2d or +1w here, which makes this easy to tweak.The third line is seemingly innocent but actually it's the one that matters the most. If you set only the first two, nothing changes. You'll tweak start-day, reload the agenda, and watch it snap right back to Monday, and you'll wonder if you fat-fingered a variable name.
You see, by default org-agenda-start-on-weekday is set to a weekday, and when it's set, Org rounds your start day back to that week boundary – silently overriding start-day. Setting it to nil tells Org to stop snapping to the week and honor the start day you actually asked for. Of course, its docstring explains it if you already know to go looking, but nothing points you there when your config silently fails.
Once I had that, I went looking for how to zoom in and out from my rolling agenda and found the view shortcuts:
d - day view
w - week view (so 7 full days)
vt - fortnight (14 days)
vm - month
y - year
To be more specific, v gives you a menu of options to choose from – everything that I listed here and more.
None of this is strange – it's three variables that ship with Org. But org-agenda-start-on-weekday is a good reminder that in Emacs, the config item you want usually already exists, and so your reasoning should be less of "there's no setting for that" and more about "what setting am I missing?".
-1:-- Why Org's agenda ignores your start day (Post Mike Hostetler)--L0--C0--2026-07-16T05:00:00.000Z
I have been working on two new themes for my minimalist doric-themes
package. The original plan was to publish them together as part of the
version 1.2.0 from a few days ago (the version that also brought
doric-tiger and doric-lion), but then I decided to refine them
further.
doric-lilac is a light theme, while doric-borage is dark. I am
still testing them, though in the screenshots below you can get a
first look.
Borage, by the way, occurs naturally in the mountains and there even is some in my land: all sorts of bees love it. I have seen lilac around, so I will need to transplant some of it here.
The new themes will be available in version 1.3.0. They already have
the character I want from them, though I may still tweak some colour
values.
The Doric themes use few colours and will appear monochromatic in many contexts. They are my most minimalist themes. Styles involve the careful use of typographic features and subtleties in colour gradients to establish a consistent rhythm.
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.
doric-themes-1:-- Emacs: ‘doric-lilac’ and ‘doric-borage’ coming to the ‘doric-themes’ (Post Protesilaos)--L0--C0--2026-07-16T00:00:00.000Z
My Emacs file-finding habits go back a long way. I lived with vanilla find-file until I discovered ido-mode, and that was a big step up in UX. Later I moved most completion to helm — but C-x C-f stayed on ido the whole time: when helm-mode takes over the standard completion dialogs, helm-completing-read-handlers-alist lets you add per-command exceptions, and mine said (find-file . ido). Last December I finally rewrote my config around the modern stack — vertico, orderless, consult — and the vertico wiki even has a recipe for making it behave more like ido. Most of my muscle memory survived the move.
Except backspace.
Here is the difference. In ido-find-file, M-p/M-n move you between the directories you previously opened files in: you land in a directory, and from there each backspace takes you one level up. It is navigation, not text editing. Vertico’s M-p instead recalls the full path of a previously opened file, inserted into the minibuffer as ordinary editable text — which I actually like more: the file I want is often one I already opened. And when it is not, a recalled path is still my starting point. In a project I know well, project-find-file is only half of how I move around; just as often I navigate relative to a file I touched recently — a habit formed years before project.el or projectile existed: recall it, delete its name, go a level or two up or into a sibling directory. Which means pressing backspace — and this is where it breaks. Ordinary text gets ordinary editing: backspace deletes one character, except right after a slash, where vertico-directory-delete-char removes a whole path component. So I hold backspace to erase the long file name, it eats the name character by character — and the moment it crosses the slash, the still-held key starts deleting whole directories. Release a beat too late and half the path is gone.
The fix is a small replacement for vertico-directory-delete-char: when the point is at the end of the minibuffer and the input as a whole is an existing file path, treat backspace as “delete the last component” — exactly what my fingers expect after ido.
(defun my/-vertico-directory-delete-char (n)
"Delete N directories or characters before point.
Like `vertico-directory-delete-char', but when point is at the end
of the minibuffer and the input is an existing file path, delete the
entire path component instead of a single character."
(interactive "p")
(cond
;; Active region: let `delete-backward-char' handle it.
((and (use-region-p) delete-active-region (= n 1))
(with-no-warnings (delete-backward-char n)))
;; At a directory boundary, try to go up a directory. Also at end of
;; input, if the whole path is an existing local file, treat DEL as
;; directory-up (delete the last path component). Skip remote paths to
;; avoid blocking on network round-trips.
((and (or (eq (char-before) ?/)
(and (eobp)
(let ((mb (minibuffer-contents-no-properties)))
(and (not (file-remote-p mb))
(file-exists-p mb)))))
(vertico-directory-up n)))
;; Otherwise, delete a character.
(t
(with-no-warnings (delete-backward-char n)))))
And the binding, replacing the stock one:
(keymap-set vertico-map "DEL" #'my/-vertico-directory-delete-char)
The file-exists-p check is the heart of it, not a safety net. It is what distinguishes a path you just recalled from history (it exists — backspace removes the whole name) from a name you are typing right now (it does not exist yet — backspace fixes the typo, character by character). Filtering input like init compl for orderless is likewise untouched. Two more details: remote paths are skipped on purpose, because file-exists-p on a TRAMP path is a network round-trip, and backspace is the last key that should ever block on the network; and mid-path behavior is stock — after a slash, backspace deletes a whole path component; inside a name, a single character.
One honest limitation: while erasing a typed name character by character, you can land on an existing prefix (deleting Makefile2 and reaching Makefile), and the next backspace jumps a whole component. In practice, I have yet to be bitten by it.
The result: recall a path from history, press backspace once — the file name is gone; every further press climbs one directory, deliberately. No more holding the key down, no more runaway deletion, and years of finger habits work again.
History recall is where the old behavior hurt the most, but the same rule pays off in ordinary navigation too: while the name you are typing matches no existing file, backspace edits it character by character; the moment the input is a complete file name — typed to the end or completed with TAB — a single backspace removes it whole. Funnily enough, ido is worse here: whenever there is text in the minibuffer, it deletes character by character. My ido habits, it turns out, were really about directory-oriented history — and where the two behaviors differ, I like the new one more.
Here is the commit in my config, with the use-package wiring around it.
-1:-- Making backspace in vertico behave like ido (Post Vadzim Shender)--L0--C0--2026-07-16T00:00:00.000Z
Yesterday I wrote that I had reconsidered my stance on Jinx and decided to give it a serious try. The only problem I had was that picking the correct spelling wasn’t working as advertised. I speculated that this was probably the result of some interaction between Jinx and Ivy and this, indeed, turned out to be the case.
Embarrassingly, mbork had already pointed this out in his post about Jinx that I linked to but since I wasn’t planning on installing Jinx, I hadn’t paid it much attention. His solution was simple: he merely disabled Ivy for Jinx by advising jinx-correct. That works fine but I’m not sure that I like that behavior either. You have to press Tab to get the list corrections and then it isn’t any easier choose the right one.
I’m sure there’s something simple that I’m missing so I’ll continue experimenting or perhaps one of Irreal’s more knowledgeable readers will tell me I’m just being stupid. In any event, I’m going back to the original behavior until I can figure out the correct way of fixing things.
-1:-- Update To Jinx Reconsidered (Post Irreal)--L0--C0--2026-07-15T15:15:37.000Z
Two weeks ago I wrote that CIDER 2.0 was brewing. Today the brew is ready - CIDER 2.0 (“Terceira”)1 is officially out! I promised the release would follow the preview within a week or two if nothing serious surfaced, and for once in my life I’m actually on schedule.
The preview post covered the big themes in detail - the
transient menus, the
call-graph browsers,
cider-macrostep,
the revamped tracing and
enlighten, the
ClojureScript improvements - so I won’t rehash all of that here. Instead I’ll
focus on what changed between the preview and the release, and on the bigger
picture of what CIDER 2.0 is actually about.
Looking back at the (enormous) changelog, the release boils down to four themes:
Notice what’s not on that list - a pile of shiny new features. There are a few genuinely new things in 2.0, of course, but the heart of this release is that most of CIDER’s important features got overhauled (tastefully, I hope) or made more robust and faster. After 14 years you accumulate a lot of good ideas with rough edges; 2.0 is me going over them with fine-grit sandpaper.
Quite a lot, as it turns out - the last two weeks were busy. The headliners:
[show content] button that fetches it only when you press it.
HTML renders as formatted text, URLs are clickable. This works for regular
C-x C-e-style evaluations too, not just in the REPL (configurable via
cider-eval-rich-content-destination). Fun fact: content-type support was
added way back in 0.17, disabled in 0.25
after it got a bit overzealous with the fetching, and the interactive-eval
part was requested in 2018.
Better late than never, right?? and m respectively), and many menus grew argument
flags - pick a pretty-printer per invocation, set test selectors once and
reuse them across runs, toggle the refresh modes, pass aliases at jack-in.
As before, your muscle memory is safe - the menus only help when you pause.cider-doctor
command that checks your Emacs setup and your
active session for common problems (version mismatches, stale byte-code,
leftover obsolete config) and produces a copy-pasteable report. My hope is
that it will make “CIDER doesn’t work” bug reports a thing of the past - or
at least give us something to look at when they arrive.cider-nrepl 0.62 fixed a batch of
instrumentation bugs (records surviving instrumentation, clear errors for
forms too big to instrument, and a few crashes).C-c C-d sends EOF
for code that reads until end of input.clojure.core/fn - a bug
from 2020 - and
ClojureScript frames render their ns/fn properly.cider-history, the inline-result options became a coherent
cider-eval-result-* family, and the six per-buffer auto-select options
collapsed into a single cider-auto-select-buffer. Every old name keeps
working as an obsolete alias, so nothing breaks.cider-classpath on Windows, formatting no longer
corrupting multi-line strings, theme-aware colors for the nREPL message log,
and plenty more of the same ilk.The documentation also got restructured to be more approachable - there’s a
proper quickstart now, a
keybindings reference page,
dedicated pages on using CIDER alongside
clojure-lsp and
clojure-ts-mode,
and a guide for
full-stack Clojure + ClojureScript projects.
The manual has grown organically for over a decade, and it showed; hopefully
finding things is much easier now.
Despite the big scary version number, upgrading should be uneventful. All the
renames ship with obsolete aliases, the transient menus preserve the classic
keybindings, and the only removals are commands that had been no-ops for years.
The one bit of muscle memory you may need to adjust: cider-macroexpand-all
moved from C-c M-m to C-c M-m a, as C-c M-m is now a prefix for all the
macroexpansion commands. If anything feels off after the upgrade, M-x
cider-doctor is your friend.
CIDER 0.1 (well, nrepl.el 0.1) was released on July 10th, 2012 - fourteen
years (and five days) ago.2 I’ve been reflecting on this a lot lately.
Fourteen years is an eternity in our line of work - entire ecosystems have come
and gone in that time - and yet here we are, still innovating, still improving,
still moving forward. I dare say CIDER 2.0 is the strongest release in the
project’s history, and it’s certainly the one I’ve enjoyed working on the most.3
None of this would have been possible without the people and organizations who have supported the project over the years - everyone who contributed code, reported issues, wrote about CIDER, answered questions, or backed the project financially. A special thanks to Clojurists Together for their long-standing support, and to everyone who took the snapshot for a spin after the preview post and shared feedback - several rough edges got filed down because of you.
So, go play with CIDER 2.0! Kick the tires, explore the menus, crack open some values in the inspector, step through a macro or two. And if CIDER makes your work a little nicer every day, consider supporting its future development
Where to from here? The sky is the limit. The REPL is the inspiration. The best is always yet to come…
Keep hacking!
Continuing the Azores naming streak started by 1.22 (“São Miguel”). “Terceira” literally means “the third” in Portuguese, which is a slightly confusing name for a 2.0 release, but naming things has never been my strong suit. ↩
The full origin story is in CIDER Turns 10, if you’re curious how a prototype hacked on a flight to San Francisco ended up here. ↩
That I can remember. My memory is not what it used to be. ↩
-1:-- CIDER 2.0: Sky is the Limit (Post Meta Redux)--L0--C0--2026-07-15T13:40:00.000Z
mojopad
Product: MojoPad — a thinking space for your Mac · second brain & PKM
Price: $59
Some of you might remember VoodooPad. It was a product marketed with the tag line “VoodooPad - You put your brain in it”
I used it a long time ago. After version 5, the product got sold to a new developer who promised version 6. Users of VoodooPad have been waiting for a long time for that. We are still waiting.
Paul Korn in the Miss the old VooDooPad? · Outliner Software post described MojoPad as “VooDooPad definitely was wonderful – and it’s wonderful to see a sort of tribute-band modern version.”
I don’t remember everything VoodooPad used to do or how it did it. In this article I am going to cover what MojoPad does and not worry about comparing it to VoodooPad.
I would describe MojoPad as VoodooPad remixed for the modern age. Yes, the product is inspired by VoodooPad, but it does so much more that the comparison is not fair to it.
TL;DR: If you are looking for a PKM application which is innovative, comprehensive, and one you can live in, check out MojoPad.
Caveats: MojoPad is an Electron application. If you have a visceral negative reaction to all Electron applications, you are better off avoiding reading further. If you can see past that, you might want to read on and try out a product which could become the centerpiece of your writing life.
MojoPad comes with a great manual. Go read it first. MojoPad is a deep product and you are going to read the manual a few times. Go through it once. You are not going to remember all of it. It is large. But you are going to have an idea of what the toolset of MojoPad is. That will guide what you use it for. You can find the manual in the Help menu. It is a MojoPad document.
pretty
The first thing that strikes you when you launch MojoPad is that it is pretty. One could never accuse VoodooPad of that. It comes with a collection of themes and is nice to look at.
Marketed as “A Digital Playground For The Mind” MojoPad is a personal wiki. We have all become used to wikis in the context of “knowledge-management” programs like Obsidian. MojoPad is that. You surround a selection of words with two square brackets, that becomes a link. You click on it and the program takes you to a new page. You can link both to a new page or to an existing page. Conceptually it is a website on your local machine containing links to and from parts of the document you are working on. It is a powerful metaphor for managing your notes, projects, and writing.
themes
MojoPad has a nice collection of themes.
more customization
Some more choices to make the program look pretty and customize it to your taste.
links
You get to open the links in the same window you are on or in a new tab. I love the preview bubble. Probably not useful, but it is an interesting look. You hover over a link and a bubble showing the first few lines pop up. Pretty.
editing
You get to choose the default font for the editor, the font for the headings. Interestingly, you can specify a font for the Markdown source view. Love Lexend — Change the way the world reads. and am using it almost everywhere I can. Zed Mono, a forked Iosevka, is the default Markdown font for me. You have some of the usual typographic controls.
shortcuts
This is the feature which makes me love MojoPad. Every keyboard command is customizable. You can change it all. Use the ones you are used to. If you are used to outliners or text editors, you have favorites merged into your muscle memory. Use them in MojoPad. Makes using it easy.
web clipper
MojoPad has a good web clipper. It is an extension for Chrome and a bookmarklet for Safari.
backup
There is a backup function and you can specify the location of the backup. Gives me peace of mind.
location, date, and time
You get to specify the date and time formats and your location in the application.
MojoPad lets you choose your document format. You can work in rich text, plain text, or Markdown. Of course, I choose to work in Markdown.
It has a full-featured outliner mode (more on that later).
external editor
You get to assign an external editor to the program. If you prefer writing in another program, you can assign an editor as your external editor and use that to write. MojoPad is not bad as an editor, but I prefer Emacs or iA Writer, so I use this function a lot.
A keyboard command switches the present page to the external editor. I work on it there, save it and come back to MojoPad.
Writing in MojoPad is a good experience. You get to choose your own fonts and it has typewriter scrolling with nice themes. You have great typographic control and can control spaces and distance between lines and paragraphs.
For me, the usefulness of an editor is a function of its support for keyboard commands. I don’t like mousing around and MojoPad’s support for keyboard commands and the ability to customize them to my liking makes it a great environment to write in.
A few words about formats. MojoPad supports rich text, plain text and Markdown. I don’t care about rich text, so I have no input on that. The Markdown support is excellent. It doesn’t deal with Org-mode in the sense that it doesn’t do any syntax highlighting of Org-mode files, but it treats them as plain text files and that is enough for my use.
MojoPad has an outlining mode. A document can be treated as an outline document and it supports all of the features that make a good outliner. I have been using it for a while and haven’t found anything that I am missing. It is well implemented. The interesting thing about MojoPad is lot of the features of an outline are shared by Markdown mode. Same keyboard commands, same general structure and the same ease of dealing with documents. At this stage, MojoPad is a great outliner along with its other features.
There are small touches in MojoPad which make writing in it simple and efficient. Some of the ones I am fond of:
You can fold/unfold headings and reduce distractions while you are writing.
The command Open Folder as Longform… makes it possible to view multiple pages as a stream. You can choose the pages or it can follow the folder structure, gives you a sense of how the document will flow if presented in a particular sequence. Useful.
Another command which I use all the time is Open Side by Side… This lets me get two views into the same document or a view into a different document while working. I use this feature a lot.
MojoPad has a an implementation of tasks and associated projects. I have not explored this much. My tasks and projects are maintained in Emacs through Org-mode. But I have rudimentary task lists in MojoPad and they work great giving me the opportunity to break up a big task into its component pieces and providing a running tally of how many of the sub-tasks have been completed out of the total number of sub-tasks. Another useful touch.
There is a Kanban board, and a Canvas implementation in MojoPad. I have not explored those. Nor have I explored the Mermaid diagrams implementation. I suggest that you read the manual of MojoPad. That is where all of the features are detailed and it is an invaluable resource if you are going to be using this program. You will discover many goodies hidden there. The documentation is excellent and it is free. Use it.
Mark Busse is the developer of MojoPad. I have never interacted with a developer like Mark. He is on the ball. I find a bug, or a feature request, and send it to him. It almost seems like the fixes/additions make it to the program before the send is complete. He is on it. I suspect he is a super-spy who keeps a close watch on my brain waves to monitor what he can find out about my usage of MojoPad. Seriously, he is responsive and absolutely phenomenal when it comes to fixing bugs and adding features. It is an unqualified pleasure dealing with a developer who is that responsive.
I am impressed and I would use MojoPad only because of his attitude and talent. The product now has a blog at Blog — MojoPad and a community discourse channel at MojoPad. Join up, ask questions, opine, and contribute to the future direction of MojoPad.
I cannot think of anything I would suggest and that is extremely rare for me. OK, maybe one thing. I know it is an Electron app and it chews up memory. I would like an attempt to control the amount of memory the application eats up. The speed of the application is not a problem, the memory usage is. A focus on reducing that would be a welcome addition.
You can live in MojoPad. This can be the central node of your writing life.
I recommend MojoPad heartily.
Thanks: Thanks to the developer who provided me with a review license for the product when I asked for one.
-1:-- MojoPad is a Wicked Wiki (Post Bicycle for Your Mind)--L0--C0--2026-07-15T07:00:00.000Z
Still working with Claude on my Emacs settings as a reference. Need to start a new session, so asked it to write a summary with instructions on how I like it to help me. It wrote this:
I know it’s complementing me on purpose, but that emphasized why? That’s why my blog is called this way 😉
-1:-- (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-07-14T22:20:58.000Z
: J'ai ajouté des bénévoles. J'ai hâte de discuter avec eux !
C'est merveilleux ! Prot a répondu à ma vidéo et il adhère à mon idée folle d'essayer des conversations sur Emacs avec d'autres personnes en français, malgré mon niveau débutant. Prot est aussi en train de pratiquer son expression orale, donc c'est parfait. Comme Prot l'a dit, je dois parler sans peur. Tout le monde est patient, et si on n'est pas patient, on peut partir. ;)
Voulez-vous parler d'Emacs avec Prot et moi ? Je pense que ce sera comme mes conversations Emacs Chat en anglais. L'objectif est principalement le partage de chouettes idées sur votre flux de travail et votre configuration, au lieu de se focaliser sur moi. Je devrais donc pouvoir survivre avec mon français. Peut-être que le chat web me sauvera si je suis bloquée. Je peux probablement aller loin avec quelques expressions comme:
Je pense que ma fille se lève plus tard pendant l'été. Si je planifie des sessions tôt (pour moi), nous pourrons probablement avoir une conversation avant que je doive m'occuper de mes activités de maman. Par conséquent, j'espère être disponible (America/Toronto; UTC-4 à ce moment; si vous avez Javascript et si vous lisez ceci sur mon site, vous pouvez cliquer sur l'heure pour convertir à votre fuseau horaire):
Si vous souhaitez me rejoindre pour une des séances, s'il vous plaît, envoyez un courriel à sacha@sachachua.com avec vos dates préférées, puis je vais m'organiser avec Prot. Si aucun de ces créneaux horaires ne vous convient, veuillez nous en suggérer d'autres et nous verrons si c'est possible.
Si vous incluez des liens vers votre configuration ou votre site, je pourrai préparer des notes et chercher des mots dans le dictionnaire. =)
( Et si mes tentatives inspirent d'autres personnes à partager leurs apprentissages malgré leur envie de toujours peaufiner, ce sera incroyable. )
D'autres sujets dans ma vidéo pour Prot :
Faire du shadowing consiste à écouter un exemple court et à le répéter immédiatement (peut-être plus lentement ou à la même vitesse) pour imiter la prononciation et le rythme. Si je suis sur mon ordinateur, c'est facile d'utiliser les sous-titres pour répéter une phrase, mais jusqu'à présent, je n'ai pas de flux de travail similaire sur mon téléphone sauf pour faire du shadowing sur des phrases avec la synthèse vocale dans Chrome. Je suis sûre qu'il y a beaucoup d'applications qui demandent un abonnement pour offrir cette fonctionnalité, mais comme vous le savez, je veux exploiter Emacs (ou mes propres logiciels) au maximum. Un jour, peut-être !
You can e-mail me at sacha@sachachua.com.
-1:-- Voulez-vous parler d'Emacs avec Prot et moi ? (Post Sacha Chua)--L0--C0--2026-07-14T18:22:03.000Z
In a stunning case of the Baader–Meinhof phenomenon, I’ve been running across the Jinx spelling corrector everywhere. I had never heard of it until last week when I watched and wrote about Prot’s video on writing in foreign languages with Emacs. Then Marcin Borkowski wrote about it and I posted about that. In all those instances, I concluded that Jinx was interesting but mainly of use for those who regularly wrote in more than one language. Since I seldom do that, I decided I was going to stick with flyspell.
Then Omar commented on my Borkowski post saying that Jinx was a better solution than flyspell even if you used it with only one language. I thought that perhaps I should reconsider my decision about sticking with flyspell. Finally, I saw this post by Bozhidar Batsov on why he has recently switched from flyspell to Jinx and became convinced that I should give it a try.
I installed it without difficulty and am writing this post with it as a sort of maiden voyage. So far, everything seems fine. By default, jinx-correct is bound to Meta+$, which I find a little clumsy so I changed that to Ctrl+;, which is where my muscle memory thinks it should be.
The only problem I’ve had is quickly choosing the correction. The documentation says that you can do that put typing the number of the correction but that simply exits the correction leaving the old spelling in place. The problem may be some interaction with Ivy, which I’ll investigate as soon as I get some time.
-1:-- Jinx Reconsidered (Post Irreal)--L0--C0--2026-07-14T16:02:46.000Z
Yesterday I published a post about
Jinx, where I
mentioned in passing that dropping Flyspell freed up C-. and C-; for
embark, and that this would be a topic
for another article. Well, here it is!
Embark has been around for quite a few years, and pretty much everyone in the
vertico/consult crowd swears by it. Somehow I never got around to adopting
it until my recent config overhaul, and
now I keep asking myself why I waited so long.
Embark, written by Omar Antolín Camarena, is often described as a keyboard-driven
right-click menu for Emacs, and I think that’s a pretty apt description. You
point it at something - a minibuffer candidate, a file name, a URL, a symbol -
press C-. (embark-act) and you get a menu of actions that make sense for
that particular thing. A few examples:
C-x b? You can kill it, rename it, or diff it against its
file without leaving the minibuffer.C-x C-f? You can delete it, rename it, copy its path, or open
it in another window.The beauty of it is that you don’t have to abort the minibuffer session,
fiddle with dired or remember some obscure command name - the action comes to
you, right where you are.
Here’s the action menu you get when you press C-. with point on a URL:

There’s also embark-dwim (C-; here), which skips the menu and runs the
default action directly - it opens the URL at point, visits the file at point
and so on. I find myself using it all the time for the common cases and
reaching for the full embark-act menu when I need something more exotic.
Acting on a single thing is nice, but embark-export is where Embark really
shines. It takes the entire set of minibuffer candidates and dumps it into a
buffer of the appropriate major mode:
consult-ripgrep/consult-grep results become a grep-mode bufferconsult-line results become an occur bufferdired bufferibuffer bufferYou trigger the export from within the action menu - e.g. C-. E while in the
middle of a consult-ripgrep session.
In other words - you start a quick search in the minibuffer, and if it turns
out to be something bigger, you promote it to a proper buffer you can navigate,
save for later, or (my favorite) edit in place with
wgrep (C-c C-p in the exported
grep buffer) and apply the changes across all the matched files. Once this
workflow clicks, there’s no going back.
A few more things I picked up in my first days with Embark that you’ll probably want to know about:
C-h right after C-. to get a searchable completing-read menu of
every applicable action, and run the one you pick. This is both the best way
to discover what Embark can do and a lifesaver when you’ve forgotten a key.C-. i inserts the current candidate in the buffer you came from and C-. w
copies it to the kill-ring. Sounds mundane, but grabbing a file path or a
variable name from the minibuffer this way quickly becomes second nature.C-. A (embark-act-all) acts on all the candidates at once - think
killing every buffer matching your current input in one go.C-. B (embark-become) replaces the current minibuffer command while
keeping the input you’ve typed. Started C-x C-f, but realized you actually
need consult-buffer? No need to abort and start over - just “become” the
other command.embark-act with
a prefix argument (C-u C-.) to keep the session alive - handy when you want
to, say, delete a few files in a row. (see embark-quit-after-action if you
prefer to flip the default)Being the Projectile maintainer, I
obviously had to check how well Embark plays with it. The answer is - pretty
well, actually! Projectile advertises the completion category of its
candidates, and Embark knows how to resolve a project-relative path like
lisp/init-git.el to a full path before acting on it.1 This means the
whole file action arsenal works in projectile-find-file:
C-. w - copy the full path of a project file without visiting itC-. r - rename a file (no dired detour needed)C-. d - delete a file you just realized shouldn’t existC-. j - jump to the file’s location in diredprojectile-switch-project is even more fun, as there the candidates are the
project directories themselves:
C-. j - browse a project in dired without actually switching to itC-. $ - open eshell directly in some project’s root (Embark is smart
enough to run shell actions in the target’s directory)C-. w - copy a project’s pathC-. E - export the entire list of your known projects to a dired bufferThat last one makes for a pretty nice project dashboard, if you ask me.
You can also teach embark-become about Projectile. Out of the box its
file/buffer map offers p for the built-in project-find-file, but we can
easily add a Projectile equivalent:
(with-eval-after-load 'embark
(keymap-set embark-become-file+buffer-map "P" #'projectile-find-file))
Now when you start a plain C-x C-f and realize the file is somewhere deep in
the current project, C-. B P will re-run your input through
projectile-find-file instead. No retyping needed.
Note: One small asymmetry - C-. E in projectile-find-file currently
produces an Embark collect buffer rather than a dired buffer, as the
exporters operate on the raw completion category. Still useful (you can keep
acting on the candidates from there), just don’t expect wdired magic.
Note: The exporters for the various consult commands live in the
embark-consult package, so you’ll want that one too if you’re a consult
user.
Here’s the relevant bit of my config:
(use-package embark
:ensure t
:bind (("C-." . embark-act)
("C-;" . embark-dwim)
("C-h B" . embark-bindings))
:init
(setq prefix-help-command #'embark-prefix-help-command))
(use-package embark-consult
:ensure t
:hook (embark-collect-mode . consult-preview-at-point-mode))
C-. and C-; are the bindings suggested by Embark’s README and they are
quite comfortable, provided nothing else is squatting on them.2 The
prefix-help-command bit is a small gem on its own - press C-h after any
prefix (say C-x) and instead of the regular help buffer you get a
completing-read over all the bindings in it, which you can narrow and execute
directly.
Embark doesn’t really care which minibuffer completion UI you’re using - it
works fine with vertico, icomplete and even the default completion setup.
Embark is one of those packages that are a bit hard to explain, but make total
sense the moment you try them - the “aha” moment for me came the first time I
exported a consult-ripgrep session. It has certainly earned its place in the
small set of packages I’d install on day one.
Are you using Embark already? What are your favorite actions? I’d love to hear about them in the comments!
That’s all I have for you today. Keep acting (on all the things)!
Under the hood the path resolution goes through project.el, so this
works in any VC-backed project - which covers pretty much every Projectile
project out there. ↩
In my case Flyspell was hogging both of them, which is partially what prompted its replacement. ↩
-1:-- All Aboard Embark! (Post Emacs Redux)--L0--C0--2026-07-14T08:45:00.000Z
-1:-- [RFC] Rename options controlling recursive statistics (Post Org Mode requests)--L0--C0--2026-07-14T07:31:23.000Z
Just a few days ago, Human Emacs was published: a declaration signed by more than 50 people from the community, led by technomancy, announcing that if GNU ends up accepting LLM-generated contributions in Emacs, they are prepared to maintain a fork free of AI-generated code. They have also opened a mailing list to discuss the topic and a chat channel on Libera Chat. Naturally, debates have appeared elsewhere too, such as Hacker News, Lobsters and Reddit. The debate is on.
I notice a lot of tension in the community. Every time I leave a comment in a thread supporting some point contrary to Human Emacs, I receive downvotes and flags for off-topic, spam, troll and unkind. That is why I thought it appropriate to develop my opinion on my blog, where there is room for nuance without the pressure of votes and labels.
That said, it is worth separating two debates that are being mixed together. The first one belongs to GNU and the FSF, the legal matter: if LLM-generated code cannot be copyrighted, it could weaken copyleft.
The licensing arguments seem legitimate to me, but easy to get around. Today you can work with a free LLM, with open weights, running on your own machine. If the purity of the tool really mattered, we should not accept patches typed on Windows with a proprietary IDE. If code is the heart of free software, we should only care about it being useful, maintainable and of good quality, regardless of its origin. And if what matters is the person behind it, we are entering territory that has already been settled: contributions developed by private companies are accepted.
The second one belongs to the Human Emacs signatories, and it is purely ethical. They make it clear themselves in their "Not Under Discussion" section:
We are not here to discuss whether LLMs are effective at what they are claimed to be able to do; their effectiveness is not at all relevant to the question of whether their use can be part of a principled software movement dedicated to user empowerment.
This stance has the virtue of being honest: they are not discussing whether LLMs work, they are discussing whether they fit their principles. And there is no possible debate against someone's principles. I will only say that I do not share the premise that LLMs are incompatible with user empowerment: for someone who cannot program, being able to add a complex feature to their editor is precisely empowerment.
I fully understand the right to fork, and I defend it: I maintain emacs-gpu myself. Forking is the essence of free software, and having an Emacs without AI code for those who want it seems healthy to me. What I find harder is the practical side: there is no reliable way to detect LLM-generated code, so the policy can only rest on trust and each contributor's voluntary disclosure. And we already know where that leads: a few weeks ago a 92-line performance patch was rejected on emacs-devel precisely because its author disclosed that it had been drafted by an open-weights model, even though they reviewed it, modified it, tested it and took legal responsibility for it themselves. If they had kept quiet, nobody would have known. A policy that punishes honesty only produces less honest contributors.
Personally, I believe LLMs will improve Emacs in areas where it was not possible before. It is already happening with very complex bugs and with abstraction layers that required an enormous amount of human work that nobody was willing to do. And although I understand that LLM-written code in the core is not the same as human-written packages that use LLMs, such as gptel or agent-shell.el, I struggle to see the coherence between rejecting the former outright and celebrating that the latter is bringing fresh air into the ecosystem. I believe that, deep down, there is more human fear than real problem: perhaps the root lies neither in the LLMs nor in the FSF, but in the uncertainty that such a big change stirs in us. And that fear runs both ways: fear that LLMs will degrade what we love, and fear of falling behind by not using them. The Emacs community has always been very open to new ideas, and I would like it to stay that way.
-1:-- Human fear in Emacs (Post Andros Fenollosa)--L0--C0--2026-07-14T06:06:15.000Z
Say you go jump to a header in org-mode somewhere with C-c C-o. Did you know that you can go back with C-c &? And that it has history, so you can keep going back??
Years of isearch-backwards… wasted. Mind blown. 🤯
-1:-- (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-07-13T22:21:20.000Z
There's some more buzz about spell-checking with Jinx (Bozhidar Batsov, Marcin Borkowski). Also, quite a few people have signed up for the Human Emacs initiative.
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!
You can comment on Mastodon or e-mail me at sacha@sachachua.com.
-1:-- 2026-07-13 Emacs news (Post Sacha Chua)--L0--C0--2026-07-13T17:45:19.000Z
As I mentioned yesterday, I started organizing (and cleaning) my Emacs config. What may have seemed like an annoying and boring task has proven to be fun and addicting (reminds me of this, which is part of this video 😂).
I’ve already discovered a couple of new improvements to my current config as I was researching individual settings and why I have them, and as I said I do plan to share it. For now though, one important section I added to my config is a quick org-mode links reference. This is actually the first time I figured out the difference between radio links and “regular” plain links and how to use them.
Here it is, slightly modified:
We know about links to other files using org-id: You give a header an :ID: property (this is generated automatically1 when you link to a header) and you get a slug, usually a UUID (for me it’s something slightly else, more on that soon below). This stores the links in org-id-locations, which in turn points to a local file with all of these unique IDs. This is good for linking between headers in different files.
We can set a :CUSTOM_ID: property for a header, and it’s only active in that file. Kind of like a table of contents in a book, with chapters (= headers) listed. If you don’t have the book, you don’t have its table of contents, so you know nothing about it. To point to a header that has a custom ID: [[#custom-id-value][description]].
These are buffer-specific only (no cross-reference between files). In this case, the links are tied to words (anchors), not headers. There are two kinds.
The regular plain target link works when we set the anchor first with <<an anchor>>, then call it again with [[an anchor]].
Its more “magical” brother is a radio target link, where we set an anchor like so <<<very special anchor>>> (three angle brackets) and then all we need to do is type very special anchor (no brackets this time), and it works. Note though that when you create a radio target for the first time, you have to revert the buffer or M-x org-update-radio-target-regexp for it to take effect.
1 : this can be done with a function, org-id-get-create. For a deeper dive into how and why I set my org-id with a date format (not UUID), look here.
-1:-- A Quick org-mode Internal Links Reference (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-07-13T12:55:12.000Z
I’ve been using corfu for in-buffer completion for a while now and I’m quite happy with it. There was one thing that kept bugging me, though - the completion popup would only ever show whatever the current major mode’s completion function had to offer. No plain word completion, no file name completion - if the major mode (or your LSP server) didn’t know about something, neither did the popup.
Turns out I was simply missing one piece of the puzzle, namely cape.
A bit of background first. Modern Emacs completion is built around
completion-at-point-functions (capfs for short) - each major mode registers a
function that knows how to complete things in its buffers, and UIs like
corfu (or the built-in completion-at-point) simply display the results.
The catch is that most major modes register only their own capf. That’s why
the trusty old dabbrev-style completion (complete any word that appears in
your buffers) is nowhere to be found in the popup, even though most of us have
been relying on it for decades via M-/.
Cape (Completion At Point Extensions) is another package by the prolific Daniel Mendler and it does exactly what its name suggests - it provides a bunch of extra capfs that you can mix into any buffer:
cape-dabbrev - complete words from the current and other buffers (the star
of the show, in my opinion)cape-file - complete file pathscape-keyword - complete programming language keywordscape-elisp-symbol - complete Elisp symbols anywhere (e.g. in comments or docstrings)cape-dict - complete words from a dictionary filecape-line - complete entire lines from the buffercape-emoji - complete emoji, if that’s your thing (Emacs 29+)My setup is deliberately minimal - buffer words and file names everywhere, on top of whatever the major mode provides:
(use-package cape
:ensure t
:init
(add-hook 'completion-at-point-functions #'cape-dabbrev)
(add-hook 'completion-at-point-functions #'cape-file))
That’s it. Now when I type a few characters, corfu shows the major mode’s
candidates and falls back to buffer words when there’s nothing smarter to
offer. Typing a path like ~/proj offers to complete it as a file name. It’s
one of those small quality of life improvements you stop noticing after a day,
because it feels like it has always been there.
Note: Every cape capf is also an interactive command, so you can invoke
them on demand - e.g. M-x cape-file to complete a file name regardless of
your setup. Cape’s README suggests putting them on the C-c p prefix, but for
me (and my fellow Projectile users)
that’s a no-go, as C-c p is Projectile territory. Pick your own prefix if
you want quick access to them.
A few practical things worth knowing:
file: anywhere and cape-file kicks in for the text right after it,
even in places where a path wouldn’t normally be recognized (say, in the
middle of a comment). The prefix is customizable via cape-file-prefix.cape-dabbrev gathers its candidates from buffers with the same major mode
by default, so your Elisp buffers won’t pollute the completions of your
Clojure buffers. If you want different behavior, that’s
cape-dabbrev-buffer-function.tab-always-indent to complete (as I do), TAB becomes the
perfect trigger for the whole completion stack - it indents the line if
needed and otherwise summons corfu with all your capfs, cape ones included:;; smart tab behavior - indent or complete
(setq tab-always-indent 'complete)
cape-dict reads from /usr/share/dict/words by default, which is fine on
macOS and most Linux distros, but you can point cape-dict-file to any word
list - e.g. one for your native language.Cape also ships a few combinators that are worth knowing about:
cape-capf-super merges several capfs into one, so their candidates show up
in a single unified popup. (e.g. you can blend eglot’s completion with
cape-dabbrev)cape-company-to-capf converts company-mode backends into capfs, which is
super handy if there’s a company backend for your favorite tool but no capf
in sight.I haven’t needed either of them yet, but it’s good to know they are there.
If you’re already on the vertico/consult/corfu bandwagon, cape is
pretty much a mandatory addition - corfu deliberately stays small and
focused, and cape is the intended way to extend what it can complete.1
Funny enough, I went years without it simply because I never stopped to ask
why file names wouldn’t complete in the popup. Sometimes you don’t know
something is missing until you try it.
What’s in your completion-at-point-functions? Any cape goodies I’ve
overlooked? Let me know in the comments!
That’s all I have for you today. Keep completing (at point)!
Also, let’s be honest - corfu wearing a cape is a pretty great mental image. ↩
-1:-- Cape: Corfu’s Best Friend (Post Emacs Redux)--L0--C0--2026-07-13T06:00:00.000Z
I’ve been using flyspell-mode for the better part of two decades, and I’ve
written about it a couple of times
before. It gets the job
done, but it has always felt a bit creaky to me - it checks words one at a time
as you type, flyspell-buffer is painfully slow in big buffers, and you have to
remember to enable flyspell-prog-mode in your programming modes, so it would
check only comments and strings there.
Recently, as part of the ongoing overhaul of my personal Emacs config, I finally replaced it with jinx and I can already tell you that I’m not going back.
Jinx is a modern spell-checker by Daniel Mendler (of vertico, consult and corfu fame), built on top of libenchant.1 Quite a few things make it a great alternative to Flyspell:
flyspell-buffer - misspellings simply get highlighted as they scroll into
view.global-jinx-mode replaces both flyspell-mode and
flyspell-prog-mode. Jinx decides what to check based on faces, so in
programming modes it automatically limits itself to comments and strings.jinx-languages to something
like "en_US bg" and Jinx will check both English and Bulgarian in the same
buffer. As someone who writes in two languages every day, this alone would
have sold me on it.M-$ (jinx-correct) pops up a
completing-read menu (lovely if you’re using vertico) with the
suggestions, plus entries for saving the word. More on that in a bit.Here’s Jinx flagging a few typos for me:

Here’s the relevant bit of my config:
(use-package jinx
:ensure t
:hook (emacs-startup . global-jinx-mode)
:bind (("M-$" . jinx-correct)
("C-M-$" . jinx-languages)))
M-$ is bound to ispell-word by default, so rebinding it to jinx-correct
feels quite natural. jinx-languages allows you to switch the active languages
for the current buffer on the fly.
Note that those bindings are not optional - Jinx doesn’t bind any keys globally
and jinx-mode-map is empty, so out of the box there’s no way to even trigger
a correction! The package does come with a few handy bindings of its own, but
they live on the misspelled words themselves (via overlay keymaps) and are
active only while point is on one of them. More on that in the next section.
One thing to keep in mind - Jinx uses a small native module to talk to
libenchant, and this module gets compiled automatically the first time you
enable the mode. In other words - you’ll need libenchant (plus pkgconf) and
a C compiler on your system:
# macOS
brew install enchant
# Debian/Ubuntu
sudo apt install libenchant-2-dev pkgconf
Admittedly, that’s a bit more setup than Flyspell, which is built-in and only
needs an external aspell/hunspell binary. Flyspell will probably remain the
path of least resistance, but in my opinion the small extra effort pays for
itself many times over.
In the spirit of Essential Flyspell,
let’s go over the keybindings you actually need. The nice thing about Jinx is
that almost everything hangs off M-$:
M-$ (jinx-correct) - correct the nearest misspelled wordC-u M-$ - correct all the misspellings in the buffer, one after anotherC-u C-u M-$ (jinx-correct-word) - correct the word before point, even if
Jinx doesn’t consider it misspelledC-M-$ (jinx-languages) - switch the languages for the current bufferWhen point is on a misspelled word a few extra bindings light up (those are the overlay-map bindings I mentioned earlier):
M-n (jinx-next) and M-p (jinx-previous) - jump to the next/previous
misspelling. They also set up a repeat map (provided you’ve enabled the
built-in repeat-mode), so after the first jump you can keep going with
just n and p, and press $ to correct the word you’ve landed on.mouse-3 (a.k.a. right click) - pops up a menu with the corrections.
(Flyspell used mouse-2 for this, which always clashed with the default
mouse yank)The correction minibuffer has a few tricks of its own:
1 to 9 select the corresponding suggestion right awayM-n and M-p move to the next/previous misspelled word without leaving
the minibuffer - super handy in combination with C-u M-$@ (personal dictionary), * (file-local), / (directory-local) and +
(just for the current session)This is how correcting a word looks with vertico (note the numbered quick
select keys and the squiggly underlines on the remaining typos):

If your Flyspell muscle memory is strong, here’s how the old bindings map to the new ones:
M-$ (ispell-word) is still M-$ - that was the whole point of the
binding in my setupC-, (flyspell-goto-next-error) becomes M-nC-c $ (flyspell-correct-word-before-point) is also covered by M-$, as
correcting a word and saving it to your dictionary live in the same menuC-. and C-; (the auto-correct commands) have no direct equivalent -
Jinx always takes you through the correction minibuffer, although the
numeric quick-select keys make that almost as fastNote: Dropping Flyspell also frees up C-. and C-;, which happen to be
prime keybinding real estate. In my setup they are now bound to
embark’s embark-act and embark-dwim,
but that’s a topic for another article.
Funny enough, I knew about Jinx for quite a while, but I kept using Flyspell mostly because of inertia. I guess old habits die hard! Now that I’ve finally made the switch, I can’t help but wonder what took me so long.
Have you tried Jinx already? Are you still happily using Flyspell (or something else entirely)? I’d love to hear your thoughts in the comments!
That’s all I have for you today. Keep fixing those typos!
I guess the name makes sense - what do you get from an enchantment gone wrong? A jinx! ↩
-1:-- Replacing Flyspell with Jinx (Post Emacs Redux)--L0--C0--2026-07-13T05:26:00.000Z
Thought my Emacs init file needs a bit of cleaning - it contains configs from maybe 5 and 6 years ago. It’s a nostalgic learning experience. More fun than I thought. More to come soon 🤓
-1:-- (Post TAONAW - Emacs and Org Mode)--L0--C0--2026-07-13T00:14:31.000Z
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
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.
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.
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.
If you are coordinating with people who don’t use Emacs, which is almost everyone, the workflow is:
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
#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
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.
doric-themesBelow are the release notes.
doric-tiger is a light theme, while doric-lion is dark. Both have
warm colours.
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
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.
package.el faces stand out moreThose 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.
nobreak-space face is now underlinedThis 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
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
-1:-- [FR] org-inside: New feature to help with hidden markers (Post Org Mode requests)--L0--C0--2026-07-11T09:33:13.000Z
-1:-- Fast spell-checking in Emacs with Jinx (Post Marcin Borkowski)--L0--C0--2026-07-11T06:52:02.000Z
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 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.
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.
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
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
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.
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
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
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.
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".
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.
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:
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.
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.
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).
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.
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.
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.
With a live tree in hand, the classic per-language hacks become generic one-liners over nodes:
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.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.
Expand the selection node-by-node — one row up the icicle charts above
Hop between sibling nodes (here in JSX)
Drag whole siblings up and down
Clone the node at point
Place cursors on every matching node
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.
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.
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.
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.
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… (Post Charlie Holland)--L0--C0--2026-07-10T18:36:07.000Z
-1:-- [RFC] Help with PDF/UA2 accessibility tests for ox-latex (Post Org Mode requests)--L0--C0--2026-07-10T17:14:57.000Z
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.
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.
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.
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.
From the diagram above, there are three concerns the client is typically responsible for:
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:
UI
Client Edge
Local Database
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.
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.
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 | |
The wttr.in URL is constructed by the function wttr--request-url shown below.
1 2 3 4 5 6 7 | |
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 | |
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 | |
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 | |
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
It's been a while since I wrote for the Emacs Blog Carnival but I have missed it. I decided to dip my toes back in when I saw July 2026's theme - Programming.
I started thinking about how my Emacs configuration for development isn't that special and then I realized that the centerpiece of my environment, direnv and Nix, is something that is worth talking about. I have a whole post on setting them up so this will continue with what that post glossed over: How does this work with Emacs?
The idea started off with the fact that I don't want separate configurations for my CLI and Emacs. I want to just configure it once and things will just work in both. Since getting my shell to see my Emacs configuration seems like a waste of time, I chose to let Emacs read what the shell was doing. What I ended up with is my system-wide Emacs reading the same configuration that my shell does, and then by default, Emacs uses the same paths and interpreters. I don't need a .dir-locals in any project folders to set special paths. It just works.
The center of this is the envrc package. It runs direnv when each buffer is opened so you have the right configuration when you need it. I don't think the envrc maintainers sought to support Nix in their package, but because it supports everything direnv does seamlessly, it supports Nix by default.
The envrc configuration in Emacs is easy:
(use-package envrc
:hook (after-init . envrc-global-mode))
Then I make sure I set up my .envrc and shell.nix files like I did in my old post and then make sure I run direnv allow and… that is it. Whenever I open up a file in that project after that inside Emacs, envrc will setup the environment automatically. It finds the right interpreter, LSP engine, etc. I don't have to do anything.
This is especially powerful for LSP. This is an actual shell.nix file for an old Python project I maintain:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.python311
pkgs.poetry
pkgs.basedpyright
];
}
I install the LSP server – Basedpyright in this case – within the project instead of globally. That way I know that it has the right version of Python and can get its dependencies and then Eglot starts up just fine. When I need to work on this project, I just cd ~/Projects/project-a and then my shortcut for helm-projectile-switch-project and choose the project folder in Emacs.
For completion's sake, here is the .envrc for the same project:
use nix
The magic is that this is all local to the buffers. I can have two projects open at once, each pinned to a different Python, and Emacs just does the right thing in each.
So in project-a it looks like:
;; a buffer under ~/Projects/project-a
M-! which python
/nix/store/j0r98nqnq891c08q2dns661qnz42fz49-python3-3.11.15-env/bin/python
Then I switch to project-b
;; a buffer under ~/Projects/project-b
M-! which python
/nix/store/jn7s7larin2h9yz8fn9g0zkz5asg8i4d-python3-3.13.12-env/bin/python
Same Emacs, same command, two different interpreters because I'm in buffers in two different projects. I never touched exec-path or dropped a .dir-locals.el in either project. Each buffer is running under its own project's .envrc. Of course if I add environment variables to my .envrc file or change that in any way, I have to do another direnv allow in that folder, or in Emacs run envrc-allow in a buffer in that project.
On the other hand, I can change the shell.nix file as much as I want and it will be automatic when I reload the buffer.
Both my shell and Emacs use the same configuration, and I don't even think about it anymore. It just is so seamless and automatic.
-1:-- How Emacs Uses Nix (Post Mike Hostetler)--L0--C0--2026-07-10T05:00:00.000Z
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?
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
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)
(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:
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
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
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!
John: 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.
John: 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.
John: 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.
John: 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.
John: 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.
John: 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.
John: 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,
John: 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.
John: 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.
John: 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.
Sacha: 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.
John: 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."
John: 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.
John: 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."
John: 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."
John: 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.
John: 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.
John: 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.
Karthik: 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.
John: 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.
John: 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?
John: 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.
John: 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,
John: and then I could say "send email to Sacha" again— you're going to get lots of emails, Sacha—
John: 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.
John: 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.
John: So let's do work. I'll say "work," I'll say "O" for one-on-ones,
John: I'll say "D" for names that begin with D,
John: 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.
John: 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.
John: Then you can see that it has pre-populated an empty org-ql column view,
John: 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.
John: 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.
John: I'll open it up and see whether there's any supporting information. There may be links to files, links to URLs, a description.
John: 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.
John: 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.
John: 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.
John: 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.
John: I mentioned that I was going to show in the drafts how I have an inbox
John: 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,
John: 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,
John: 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.
John: So if I hit C-c C-c, it pops up a little submenu of things I can do with this draft.
John: 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.
John: 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.
John: 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. org-db-search—that’s the name of the command that I run to do this.
Sacha: 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.
John: 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.
John: 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.
John: 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.
Sacha: 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.
John: 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.
John: 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.
John: 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.
John: "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.
John: 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.
John: 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.
John: 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?
Karthik: 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.
Karthik: 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.
Sacha: 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?
John: 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. org-tag-persistent-alist there. Karthik: That's C, yeah. John: Yeah, but that's only a couple of tags.
John: 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.
John: The same with verb-all. Verbs are words that can be at the beginning of an entry title that are followed by a colon.
John: 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.
John: Well, I would first use ripgrep, because that's the fastest.
John: 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.
John: 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.
John: 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.
John: 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.
John: 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.
John: 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.
John: 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.
John: 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. 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.
John: 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.
John: 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."
John: 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.
John: 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.
John: obr is the task management tool that uses Org-mode as the storage format. We talked about org-devonthink.
John: 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.
John: 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.
John: 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.
John: 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. make stats.
John: 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.
Sacha: "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.
John: 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. You can e-mail me at sacha@sachachua.com.
-1:-- Chatted with John Wiegley and Karthik Chikmagalur about personal information management (Post Sacha Chua)--L0--C0--2026-07-09T00:30:29.000Z
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
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
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
Render Dynamic HTML using plot and qjs.
-1:-- Plan 9: Dynamic HTML (Post Anand Tamariya)--L0--C0--2026-07-08T09:55:21.000Z
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…
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
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
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.
Three things work together:
basic-themerlr-touying-scaffold.elox-touying.elTo 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.
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.
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.
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.
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
- Classical
- Zeno of Citium
- Cleanthes
- Chrysippus
#+end
#+begin
- Roman
- Marcus Aurelius
- Seneca
- Epictetus
#+end
#+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: :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:
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: :size 2.5em
#+begin
Virtue is the only good.
#+end
#+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.
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.
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.
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
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.
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
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
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.
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.
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.
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:
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.
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.
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.
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.
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.
Emacs mentioned in an elfeed article, a mu4e message, or an EWW page, the Action Key promotes that into a curated WikiWord.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.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.
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.
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!
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.
Robert Weiner, "PIEmail: A Personalized Information Environment Mail Tool", https://cdn.jsdelivr.net/gh/rswgnu/rsw_publications@a9813b7a/PIEs/PIEmail.pdf.
Ramin Honary, "Introduction to Hyperbole", tilde.town/~ramin_hal9001/articles/intro-to-hyperbole.html.
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).
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.
Paul Graham, "Putting Ideas Into Words", February 2022: paulgraham.com/words.html.
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.
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
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.
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.
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.
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.
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.
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
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
-1:-- [FR] Re: [DISCUSSION] Restructuring LaTeX output: the bigger picture => a) ox-beamer. (Post Org Mode requests)--L0--C0--2026-07-06T20:14:48.000Z
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!
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
If you’ve ever written some Emacs Lisp that pokes around in a buffer, chances are you’ve needed to answer a deceptively simple question at some point - is point inside a string or a comment right now? It comes up constantly: when you’re font-locking something, when you’re computing completion candidates, when you’re navigating code and want to skip over the bits that aren’t actually code.
The workhorse behind all of this is syntax-ppss. The name is a bit of a
mouthful of abbreviations - it’s short for “syntax parse-partial-sexp”, and that
ppss tail stands for “parse-partial-sexp state”, which is the value you get
back from Emacs’s low-level parse-partial-sexp (itself short for “parse partial
s-expression”). Say that three times fast. Anyway, you hand syntax-ppss a
position (or just call it at point), it parses the buffer up to there and returns
that parser state - how deeply nested in parens you are, whether you’re in a
string, where the enclosing list started, and so on.1
There’s just one catch. That parser state is a plain list of eleven elements, and for the longest time the only way to get anything out of it was to reach in by position:
;; are we inside a string?
(nth 3 (syntax-ppss))
;; ... or a comment?
(nth 4 (syntax-ppss))
;; where did that string or comment start?
(nth 8 (syntax-ppss))
I don’t know about you, but I can never remember what lives at which index.
nth 3 tells you precisely nothing about what you’re checking. Every time I ran
into code like this I’d have to pull up C-h f parse-partial-sexp RET and count
elements in the docstring. Magic numbers scattered all over the place, and not
the fun kind.
Turns out Emacs 26.1 fixed this years ago and I somehow missed the memo. It
ships a ppss struct (yes, that abbreviation again) with proper named accessors
for every field. And here’s
the neat part - the struct is defined with :type list, which means it’s
literally the same list syntax-ppss already returns. No wrapping, no
conversion. You just call the accessor on the value you already have:
(ppss-string-terminator (syntax-ppss)) ; inside a string?
(ppss-comment-depth (syntax-ppss)) ; inside a comment?
(ppss-comment-or-string-start (syntax-ppss)) ; where it started
Now the code says what it means. These are the accessors I reach for the most, though there’s one for every slot in the list:
ppss-string-terminator is non-nil when point sits inside a string (nth 3).ppss-comment-depth is non-nil inside a comment (nth 4).ppss-comment-or-string-start gives you the position where the current string or comment began, or nil if you’re in neither (nth 8).ppss-innermost-start is the start of the innermost list containing point (nth 1), which is handy when you want to jump out to the enclosing form.ppss-depth is how many levels of parens deep you are (nth 0).If you’re worried about the cost of a function call on a hot path like
font-locking, don’t be. The accessors come with compiler macros, so
(ppss-string-terminator state) compiles down to the exact same
(nth 3 state) you’d have typed by hand. You get the readable name at read time
and pay nothing at run time. And since parse-partial-sexp returns the same kind
of list, all of these work on its result too.
There’s another helper worth knowing about for the most common check of all -
am I in a string or a comment, never mind which one. That’s syntax-ppss-context,
which returns the symbol string, the symbol comment, or nil:
(if (syntax-ppss-context (syntax-ppss))
(message "not in code")
(message "somewhere in the code"))
A single call in place of the (or (nth 3 state) (nth 4 state)) dance you see
all over the place. There’s also syntax-ppss-toplevel-pos for getting back to
the enclosing top-level form. And plenty of major modes still bundle their own
little foo-in-string-p predicates - now you know what they’re doing under the
hood.
The usual caveat about built-in libraries applies: the ppss accessors arrived
in Emacs 26.1, so if for some reason you’re still targeting something older
you’ll have to live with the nth calls. In 2026 that’s unlikely to trouble
anyone.
I went on a little cleanup spree in CIDER’s
codebase recently and swapped every one of these positional lookups for the named
accessors. It’s the kind of change
that touches a lot of lines but risks nothing, and the diff ends up reading like
documentation. If you’ve got a stray nth 3 (syntax-ppss) lurking in your own
config or packages, it’s a five-minute upgrade well worth making.
That’s all I have for you today. Keep hacking!
syntax-ppss is really just parse-partial-sexp run from the start of the top-level form down to your position, with a cache in front of it so that repeated calls at nearby positions stay cheap. That caching is why you should prefer it over calling parse-partial-sexp yourself most of the time. ↩
-1:-- Reading syntax-ppss State Without the Magic Numbers (Post Emacs Redux)--L0--C0--2026-07-06T07:00:00.000Z
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:
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
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:
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
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à !
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!
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
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:
Below you’ll find more details about the work I did, project by project.
CIDER 1.22 (“São Miguel”) landed in mid-June, wrapping up the 1.x series. Its main features:
cider-jack-inIt 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:
tap> buffer and a dedicated trace bufferThat 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.
Lots of cider-nrepl releases, driving the CIDER work above:
cider/who-implements, cider/type-protocols, cider/protocols-with-method).pprint backed by orchard.pp.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, the library that powers much of cider-nrepl’s functionality, kept pace:
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, the omniscient Clojure debugger, had been dormant for years and I finally gave it the revival it deserved:
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.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:
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 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.
The nREPL org saw some ClojureScript-flavored action:
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.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 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.
I wrote a few articles related to the work above:
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
Kosshi
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 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 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
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.
keyboard 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 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
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 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.
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.
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
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
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!