Org Mode requests: [FR] LaTeX preview support Emacs without SVG support
-1:-- [FR] LaTeX preview support Emacs without SVG support (Post Org Mode requests)--L0--C0--2026-11-06T19:20:22.000Z
-1:-- [FR] LaTeX preview support Emacs without SVG support (Post Org Mode requests)--L0--C0--2026-11-06T19:20:22.000Z
Over at And now it’s all this, Dr Drang has a post on using TextEdit versus using Pages. He’s a Mac user, obviously, but that doesn’t matter so much. The real argument is over whether one should use a word processor or some sort of text based editor.
There can be no doubt on where I stand on the issue. The only time I can remember using a “word processor” of any type in the last 3 decades was when I had to collaborate on another author’s file as a technical editor1. As I’ve said before, whenever I write in anything other than Emacs, I feel like I’m in Bizarro World where nothing works as it should. That includes even things like iMessage which is why I tend to use emacs-everywhere for it and most other things.
Dr Drang is nominally the same. He says he almost never uses a Word Processor but sometimes for a one-off he thinks it makes sense to use a word processor rather than one of his LaTeX or Markdown templates. That seems backwards to me. Sure, maybe you don’t want to use a template for a technical report to write a quick note to your baby sitter but Pages or any other word processor is not the simple alternative. I’d simply use Org and export it to whatever format seems appropriate. Even if Dr Drang is not an Emacs user, he is a Markdown user so surely the easy thing for him to do is to dash off his quick note in Markdown.
Really, unless you have collaboration issues, there’s no reason to subject yourself to the horrors of a word processor.
That post was so long ago that I was still using Groff instead of Org to typeset documents. I’ve done technical editing since then that also required a Word clone and the experience was no better.
-1:-- Why Use A Word Processor When You Have Org? (Post Irreal)--L0--C0--2026-11-06T15:59:52.000Z
We have a recording session for the EmacsConf 2026 panel on community tomorrow. I want to get my thoughts together so that I can answer concisely, saving the details for this post so that people can dig into it before or after the conversation. So here are some rough notes:
If you want to reflect on the questions, please feel free to do so and to share your responses with me or on your blog. (You don't have to be one of the panelists. =) )
You can e-mail me at sacha@sachachua.com.
-1:-- EmacsConf: Thinking about the community panel (Post Sacha Chua)--L0--C0--2026-11-06T15:50:05.000Z
Time for another bi-monthly update on the work that Clojurists Together are funding this year - my maintenance of nREPL, CIDER and friends. You can find the previous one here.
For a change, most of my focus in September and October was on nREPL itself, not on CIDER. The big themes were the nREPL protocol spec and how well the servers and clients out there actually stick to it. For years building an nREPL server or client that plays well with the rest of the ecosystem came down to trial and error, and this time I finally did something about it. It paid off much sooner than I expected.
The big highlights from my perspective:
ns form, backed by new cider-nrepl and Orchard releasesBelow you’ll find the details, project by project.
proof is the big new project this time around, and the one I’m most excited about. I’ve been talking about some automated way to check nREPL compatibility for ages (it made my list of things people hate about nREPL back in 2023), so it feels really great to finally see it become a reality. Good things take a while to happen, they said… Does your server work with CIDER? Until now the only way to find out was to implement the protocol, connect CIDER (or Calva, or whatever your users run) and see what breaks. Now you can point proof at your server and it’ll tell you. It’s a single binary that runs about 80 checks against a server. A check fails only when a real client would break (with a link to the client code that depends on the behavior), and everything else is just a warning. proof also comes with client profiles that send exactly what CIDER, Calva, Conjure, vim-fireplace, REPLy and neorepl send, profiles for the servers I know of, and a GitHub Action for running it in a server’s CI.
Once the server side was in place, I started on the clients as well. proof proxy sits between a client and a server and checks what the client sends,
and proof serve is a small server for the test suites of clients, which can mimic the quirks of nREPL, Babashka, jank and others (proof serve -like jank).
I wrote about proof in a lot more detail in proof: How Strong Is Your nREPL?.
proof goes hand in hand with the nREPL spec, the draft Phil Hagelberg wrote earlier this year. When the spec and the existing clients disagree, proof goes with the clients (a spec that breaks CIDER isn’t much use to anyone), and every such conflict, along with the gaps proof ran into, ends up on a list of spec changes. The first few of them are already PRs against the spec: how replies relate to their requests, the status values, how interrupts are answered and the fields of lookup replies.
What I like the most about proof is how quickly it started paying off:
done of an eval interrupted from another connection to the wrong connection (fixed on master), drops the evals waiting behind an interrupted one (#503) and can’t interrupt an eval that hasn’t started yet (#504). Not great!I was also super happy to see other people using proof already. Tom Waddington has been running it against his nREPL implementations
for Janet and Steel Scheme since early October (nrepl-janet even tests its client with proof serve),
Jeaye Wilkerson suggested using it for jank’s nREPL server, and Kyle Cesare is already fixing what it found there.
That’s exactly what I was hoping for!
neorepl is the other brand new project, and it grew up together with proof. It’s a terminal REPL for Clojure (and anything else that runs an nREPL server), and the best way I can describe it is “CIDER in a terminal”: it uses cider-nrepl when it’s around, the same way CIDER does, and falls back to nREPL’s built-in ops when it isn’t. Every lookup the REPL can do is also a command with structured output, and there’s an MCP server and a skill for coding agents, so it’s “CIDER for agents” as well. Five releases in three weeks:
neorepl mcp, a skill that teaches agents to check, load, test and debug Clojure code with the CLI, and neorepl check --fix, which fixes mismatched delimiters the way parinfer does.proof checks every server with the exact requests neorepl sends, and neorepl went through proof proxy and every scenario of proof serve before 0.1 was out.
Its commands are separate processes sharing a session, so neorepl hits nREPL’s interrupt bugs harder than most clients,
and making it work as well on Babashka’s server as with cider-nrepl is where most of the Babashka reports came from.
Even JLine got a few bug reports along the way.
By the time I got around to writing this update, neorepl 0.5 was out as well, with a much smarter line editor, a namespace browser, automatic reloading and a CLI that editors can use without a plugin. I wrote about it in neorepl 0.5: CIDER in a Terminal.
nREPL 1.8.0 came out on October 2 with the URL-based connections and the TLS improvements I mentioned last time,
plus a few session fixes. Sessions survive evaluated code closing *in* or *out*, a race that could split responses between concurrent reads in nrepl.core/client is gone,
and Sashko made persistent sessions re-create their thread when a faulty exec thunk kills it. He also moved the Unix sockets to the SocketChannel API.
Master moved on quickly after the release, much of it because of proof and neorepl:
done of an eval interrupted from another connection goes to the eval’s own connection, where the client that sent the eval never got itTCP_NODELAY on accepted connections, so on Linux an eval’s done no longer waits up to 40ms behind its value:out-limit with separate :out-quota and :err-quota limits, made the bencode transport turn empty lists into nil for ?-ending keys and made building the middleware stack fasterThe interrupt bugs that are still open are next on my list.
load-file failing with “No such namespace” when the file requires a foreign lib (e.g. a cljsjs package).:cljs/quit on nREPL 1.3+, where the session stayed in ClojureScript mode, and made evaluation use the same dynamic bindings as cljs.repl. That means the cljs-repl options (e.g. :warnings) and the bindings tools like figwheel-main set up around it finally take effect, which closes an issue from 2018. It also picked up :as-alias keywords and the :cljs branch of data_readers.cljc.nrepl.org got a facelift at the end of October. The manual moved to docs.nrepl.org, and nrepl.org itself is now a landing page that explains what nREPL is and how its protocol works, with real exchanges between a client and a server next to every feature. It even has a “Try it” section with an nREPL server running right in your browser, which turned out to be a story of its own. That playground is how nrepl-sci, a small nREPL server for SCI that runs entirely in a web page, came to be, and it even led me to take a closer look at clojure-mode for CodeMirror, which powers the playground’s editor.
CIDER got a landing page of its own at cider.mx, made to look like an Emacs frame (mode line, M-x and all), with mini screencasts of CIDER in action.
I had a lot of fun working on both sites!
My toy nREPL implementations were the first things I ran proof against, and all of them got a round of fixes in early October:
close with session-closed, echo the op in unknown-op replies and return a value for every Erlang form in an evalerr, handles unknown sessions and stdin EOF, and checks describe before using completions, lookup and interruptneat’s own integration suite now tests just the client and leaves the servers to proof. None of the three has had a release yet, though.
No CIDER release this time (2.1 is still brewing), but master kept moving:
cider-test-stream-results, on by default): the echo area keeps a running count, and failing tests land in the report as soon as they finish. This closes an issue from 2014! cider-test-fail-fast is now off by default to go with it.ns form: cider-ns-add-require, cider-ns-add-import, cider-ns-add-missing (adds what the symbol at point needs), magic requires (typing alias/ adds the require the alias needs) and cider-ns-clean. These are some of clj-refactor’s most popular features, and they’re backed by new cider-nrepl ops that read your code instead of loading it.cider-connected-hook and friends never ran. proof found this one.nil value are left out of nREPL requests instead of being sent as empty lists, which closes an issue from 2020.cider-nrepl releases:
orchard.test and streams their progress to the client, which is what CIDER’s test streaming builds on.cider/resolve-missing, cider/resolve-alias and cider/clean-ns ops behind CIDER’s ns commands (after three alphas), made the test ops always answer and send what tests print with their own responses on nREPL 1.3+, and fixed the inspector and ClojureScript tests on shadow-cljs.Orchard releases:
orchard.test, the clojure.test runner that used to live in cider-nrepl and can now report progress as it goes, plus Sashko’s string analytics in the inspector and his fix for the stack overflows when printing recursive collections.orchard.ns-form, which takes apart an ns form and tells what it requires, refers and imports that the code doesn’t use, all without loading anything, and fixed some Java doc rendering problems.str) from overflowing the stack.Sashko also added JDK 27 to the CI of nREPL, cider-nrepl and Orchard. Thanks, Sashko!
refactor-nrepl 3.15.0 documented the limitations of its AST-based analysis
and deprecated the ops cider-nrepl has taken over. Back in the spring I said I’d like to move the most useful refactor-nrepl features
to CIDER and cider-nrepl eventually, and the ns editing commands above are the first step in that direction.
No release, but quite a few fixes on main. I fixed clojure-ts-mode on Emacs 31 (e.g. the ClojureScript mode asking for the jsdoc grammar, M-q indenting only the innermost form)
and a pile of refactoring commands that went wrong around metadata, #_ forms and reader conditionals.
Jason Jackson sent a round of indentation and alignment fixes (threading macros, reader prefixes, metadata and comments before arguments),
and Kyle Cesare kept up with jank’s syntax. Thanks, Jason and Kyle!
MrAnderson 0.7.2 stopped bundling pomegranate’s Maven classes in its jar, where they clashed with any other resolver on the classpath and broke cider-nrepl’s build once tools.build pulled in a newer tools.deps. The artifact went from 8 MB to about 200 KB in the process.
There was also a lot of release and CI maintenance across nREPL, cider-nrepl, Orchard, Piggieback and friends (e.g. release notes taken from the changelogs, the docs of a release published by its release workflow), and I revived the CI of parseclj and parseedn.
Big thanks to Clojurists Together, Nubank and the other organizations and people supporting my Clojure OSS work! None of this would have happened without you. You rock!
As for what’s next - last time I said CIDER 2.1 was the obvious next milestone. It still is, but it got delayed, as most of my time went into nREPL instead. I told you I’m bad at predictions! nREPL 1.8 shipped as planned, and mezcaml and the BEAM servers got a lot more reliable thanks to proof, even if neither got a release. While we’ve made solid progress on CIDER and friends as well, my main focus until the end of the year will likely be the broader nREPL ecosystem: fixing the interrupt bugs proof found in nREPL, turning the list of spec changes into PRs against the spec, and getting more servers and clients to run proof.
Keep hacking!
-1:-- Clojurists Together Update: September and October 2026 (Post Meta Redux)--L0--C0--2026-11-06T09:00:00.000Z
For as long as I’ve been involved with nREPL, the question I’ve heard most often from people building servers and clients has been some variation of “Will this work with CIDER?”. And for just as long the honest answer was “Try it and see”. Today I have a better answer for you - proof, a compatibility suite for nREPL servers (and clients). I’ve been working on it since the end of September and proof 0.2 came out a couple of days ago, so I guess it’s high time I told you a bit about it.
The nREPL protocol is tiny. A handful of ops, requests and responses that are
simple maps, bencode on the wire. You can write a basic nREPL server in a
couple of hours, and lots of people have done exactly that. Making it play nice with
CIDER, Calva, Conjure and the rest of the clients out there is a very
different matter, though. What clients actually expect from a server is
spread across the reference implementation, a draft spec, many years of
client code and lots of GitHub issues. Which status values mean what? Is
ops in describe a map or a list? Where does the end of an interrupted
evaluation go? What happens to a session when its client goes away in the
middle of an eval? Server authors usually find out one bug report at a time.
Client authors don’t have it any easier, as every server has its own quirks, and for many years the only way to find them was to try your client with every server you could get your hands on. I’ve been on both sides of this conversation for well over a decade (CIDER is a client, nREPL is a server), so believe me - I know the pain.
Why bother? Because I’ve always seen nREPL as something bigger than Clojure.
Back in 2019 I wrote that nREPL is similar in nature to
LSP, only much simpler, and I
still believe that. For any language that can evaluate code at runtime, nREPL
is a much simpler alternative to LSP. Implement eval, interrupt and
load-file, throw in completions and lookups if you feel like it, and every
nREPL client out there becomes an editor integration for your language.
That’s not just a dream anymore. These days there are nREPL servers for Clojure dialects running on Python (Basilisp), C++ (jank), .NET (ClojureCLR), Go (let-go) and Node.js (nbb), for Janet and Steel Scheme, and even for Erlang and Elixir (more on those later). Still, “language-agnostic” doesn’t mean much if everyone who builds a server has to check it against CIDER by hand.
The idea of a compatibility suite is not new. When nREPL 1.0 came out in 2022 I wrote that improving the protocol spec and the ability to test against it was where our focus should go next, and a year later the lack of a compatibility test suite made my list of things people hate about nREPL. I even asked if one of you was looking for a fun small OSS project. Nobody volunteered, and the idea spent a few more years on the backburner. This year, thanks to the support of Clojurists Together, I finally made a very focused push on it.
It started with the spec. Late last year Phil Hagelberg wrote a draft of the nREPL protocol spec, which landed in May. A spec is great, but you can’t run a spec against your server, so I started proof. The two feed each other - every time proof finds a place where the spec and the clients disagree, or something the spec doesn’t cover at all, it ends up in a running list, and from there in the spec itself. (A few of those are already pull requests against it.)
When the spec and the clients disagree, proof sides with the clients. For
instance, the spec recommends returning the ops in describe as a list, but
CIDER and Calva both expect a map. A server that follows the spec to the
letter, but that CIDER can’t connect to, is not much use to anyone. That’s
also why proof is a compatibility suite and not a conformance suite. You
can only conform to an official spec, and nREPL doesn’t have one yet.
Checking servers is proof’s main job. It’s a single binary1 that talks to your server over a socket, the same way any client would. It knows nothing about the server’s internals and it doesn’t care what language the server is written in. What it does need is a profile - a small TOML file that tells proof how to start your server and gives it a few snippets of code in the server’s language (an idea I borrowed from jupyter_kernel_test):
name = "My server"
[launch]
command = ["my-server", "--port", "0"]
port-pattern = 'nREPL server started on port (\d+)'
[snippets.value]
code = "(+ 1 2)"
value = "3"
Then you install proof and run it:
$ go install github.com/nrepl/proof/cmd/proof@latest
$ proof run my-server.toml
proof starts your server, runs about 80 checks against it and shuts it down
afterwards. (You can also grab a binary from the
releases, if you don’t have Go
around.) Profiles for nREPL, Babashka, nbb, Basilisp, ClojureCLR, jank,
let-go, dialtone and repartee come with proof, so proof run babashka just
works if you have Babashka installed.
The most important decision in proof’s design is how it grades the checks. A check fails only when some client won’t work properly with your server (or when users will lose data), and the report tells you which client it is and links to the client code in question, pinned to a specific commit. If your server simply does something differently from the reference implementation, you get a warning instead. Here’s a failure from a run against Babashka 1.13.226:
FAIL interrupt.other-connection An interrupted eval ends on its own connection
the interrupted eval's done went to the connection the interrupt came from
why: neorepl interrupt stops an eval that another neorepl process
started, and that process waits for the eval's done on its own
connection forever if it goes anywhere else.
see: neorepl interrupt stops an eval from another process
https://github.com/nrepl/neorepl/blob/462a182c.../src/neorepl/main.clj#L374-L385
see: nrepl/nrepl#498 https://github.com/nrepl/nrepl/issues/498
The idea is that a failure should always be worth fixing and never be a matter of taste. If you think some failure isn’t justified, the links show you exactly what you need to argue with.
Some checks go a step further and send exactly what a particular client
sends, the way it sends it. cider.connect sends the requests CIDER sends
while connecting, calva.eval evaluates code the way Calva does, and so on
for Conjure, vim-fireplace, REPLy, neorepl, clojure-cli.repl and
bb repl --connect. That way the report tells you directly whether those clients will
work with your server.
Once your server passes everything it can pass, you can list the failures you can’t fix yet as expected failures in its profile and run proof in your CI with its GitHub Action. When an expected failure starts passing, proof fails the build and asks you to remove it from the list, so the list can’t go stale.
Last, but not least, the bundled profiles run every day and their reports end up in a compatibility matrix, which shows where the different servers agree, where they don’t and where the spec needs some work.
Servers were always proof’s primary focus, but once the foundation was in place people started asking me how they could check their clients with it. That made a lot of sense to me - after all, the same knowledge cuts both ways. proof has two tools for client authors.
proof proxy sits between a client and a server, passes everything along as
is and records it on the way. You use your client for a while (or run its
test suite through the proxy), press Ctrl-C and proof grades every request
the client sent. Here’s a quick neorepl eval going through it:
$ proof proxy -listen 127.0.0.1:7888 babashka
$ neorepl eval --port 7888 '(println "hi") (+ 1 2)'
client
PASS client.bencode Every frame is valid bencode
PASS client.dict Every request is a dict
PASS client.canonical Frames use canonical bencode
PASS client.id Every request has an id
PASS client.active-id An id isn't reused while its request is active
PASS client.field-types Request fields have the right types
PASS client.required-fields Requests carry the fields their op needs
PASS client.need-input need-input is answered with stdin in the same session
WARN client.close Sessions are closed before disconnecting
PASS client.unknown-op Requests use ops the server supports
9 passed, 0 failed, 1 warnings, 0 skipped
The grading rule is the same, just turned around. A client check fails only
when some server breaks, and it links to the server code in question. (That
warning is fine, by the way. neorepl keeps its session around on purpose, so
the next neorepl eval can use it.)
A proxy sees only what goes over the wire, though, so it can’t tell you how
your client copes with the replies of different servers. That’s what
proof serve is for. It’s a small nREPL server for client test suites, which
behaves like nREPL unless you ask it to behave like some other server in some
particular way - send only the value of the last form (like Basilisp, jank and
dialtone), drop what the code prints to stderr, have no interrupt op,
deliver its replies a byte at a time, and so on. None of those scenarios are
made up. Every one of them is something a real server does (or something TCP
can do to any server’s replies). -like turns on all the quirks of a server
at once, so you can run your client’s tests against “jank” without installing
jank:
$ proof serve -listen 127.0.0.1:7888 -like jank
So far, so good. But does it actually find anything? You bet! Here are some of the things proof turned up in the past couple of months:
done after its client goes away in
the middle of an eval (#509).
Oops!eval-error status, sessions sharing *1 and
the current namespace, and a request that throws taking its whole connection
down.Michiel Borkent fixed the Babashka and nbb issues the same day I reported them (or the next one), Conjure fixed its encoder the next day and Calva within a week. That’s the Clojure community for you!
I was also super happy to see other people picking up proof before I even got
around to announcing it. Tom Waddington has been running it against his nREPL
implementations for Janet and
Steel Scheme since early October
(nrepl-janet even tests its client with proof serve), Jeaye Wilkerson
suggested using it for
jank’s nREPL server, and Kyle Cesare is already
fixing what it found there.
That’s exactly what I was hoping for!
The projects that benefited from proof the most were my own, though. This year I built a few toy nREPL implementations, mostly for fun and to see how well the protocol holds up outside of Clojure:
close with session-closed, didn’t echo unknown ops and returned only the
last value of several forms, and fixing those uncovered a race after close
and temporary sessions that never stopped.err and unknown sessions wrong.And then there’s
neorepl,
the CIDER-inspired terminal REPL I’ve been working on lately, which is just as
happy to be used by coding agents as by humans. neorepl and proof pretty much
grew up together. I ran neorepl through proof proxy and every scenario of
proof serve, which caught a few bugs before neorepl 0.1 was out, and proof
checks every server with the exact requests neorepl sends. It’s also how I
found the interrupt bugs in nREPL above, which hit neorepl harder than most
clients. Its commands are separate processes sharing a session, so
neorepl interrupt stops an eval that some neorepl eval started, and that
neorepl eval used to wait for its done forever.
I guess I should spell out who proof is for. If you’re using nREPL through CIDER or Calva you’ll probably never run proof yourself (although hopefully you’ll feel its effects). proof is a tool for tool authors - people writing nREPL servers, clients and middleware.
I really hope that it will make it a lot easier to grow the nREPL ecosystem. These days anyone can put together an nREPL server or client in no time with the help of tools like Claude. Writing the code is no longer the hard part - knowing whether the result actually works with CIDER, Calva and friends is. proof gives you (and your coding agent) a fast feedback loop and failures that explain themselves: what went wrong, why it matters and which client code depends on it. That’s usually all an agent needs to fix the problem.
Naming is hard! Luckily, the nREPL world has a tradition that makes it a bit easier, as many nREPL clients are named after drinks (CIDER, neat, mezcaml, chaser and so on). proof is not a client, so it bends the rule a bit, but it still belongs at the bar. The proof of a spirit is a measure of its strength, which is more or less what this tool measures for nREPL servers. And running it gives you proof that your server works with the existing clients (or a list of the reasons why it doesn’t). Two meanings for the price of one!2
proof is still in its early days, but it already covers the core of the protocol, the exact requests of the most popular clients and the server quirks clients have to deal with. Now it needs more users.
So, if you’re working on an nREPL server or client, please take proof out for a spin and let me know how it went! I’d love to see your server in the compatibility matrix, and if you think some failure isn’t fair, open an issue and let’s argue about it. And if you’re a casual Clojure hacker who’d just like a nice REPL in the terminal, give neorepl a try:
$ bbin install io.github.nrepl/neorepl
Big thanks to Clojurists Together for supporting my work on nREPL, to Phil Hagelberg for getting the spec going, and to the maintainers of all the servers and clients who were so quick to fix the problems proof found. You rock! In the (n)REPL we trust!
That’s all I have for you today. Keep hacking!
Written in Go, of all things. A Clojure tool would have been the obvious choice, but proof shouldn’t favour any of the languages it checks, and a single binary without a runtime is easy to drop into any CI. ↩
“hangover” would have been a good name for what you get when a server forgets to send done, but it’s a hard sell for a project. ↩
-1:-- proof: How Strong Is Your nREPL? (Post Meta Redux)--L0--C0--2026-11-05T21:00:00.000Z
Here's my contribution to this month's Emacs Carnival, which explores how we discover Emacs features.
I'm adding bookmarks into my workflow. I tend to get lost among
multiple windows in which several Org files reside, as well as windows
that browse the Internet (*eww*), or house *Help*, *Apropos*,
*Messages*, *Customize* (or TBD). It's important that I can jump back
to my main task. I'm tired of flailing away
with other-window and next-buffer . I suppose I could globally
bind C-c C-x C-j to org-clock-go -- that certainly would help.
But I may need to access a window of secondary importance. Also, some
windows have nothing to clock into, so I'd still need a mechanism that
works outside of Org.1
Bookmark keybindings share a prefix with rectangle commands, namely
C-x r . But since I use rectangle functions, too, I
might devise a new set of keystrokes to divorce the bookmark commands
from rectangle commands. This would make the C-x r C-h
help window easier to understand. It makes sense that the rectangle
editing commands are grouped under
C-x r, but why are bookmarks shoved in there, too?
Perhaps it's because C-x b already was dedicated to
the switch-to-buffer function, so it had to go
somewhere.2
How often do I adopt a new feature? Based on the versioning of my init file, I make 0.65 changes on average every day. But perhaps 75% of the changes are minor adjustments due to preference or to not having grepped a new feature correctly. So let's say 0.16 changes per day, or about 5 every month.3 It would be higher, but I have to eat, too!
When I was getting paid for my productivity, I belonged to the Set and Forget team. I would devise and validate workflows, trading that effort for the real work I was supposed to do. Other software programs forced updates on users. This annoyed me, especially when a feature was removed or a keystroke changed. I greatly appreciated Emacs for not doing that. I'd install a new Emacs version only when provided with a new computer.
Now that I answer to no one4 exploring Emacs features has become my past time.
I learn about Emacs from many sources. The following list of examples is not all-inclusive:
When you're committed to self-improvement and life-long learning as I am, discovering Emacs features becomes an obsession!
1 I've also started to use the mark-ring, popping mark to navigate within a buffer.
2 Buffer functions came first. I infer this from the initial copyright years of window.el and bookmark.el, which are 1985 and 1993, respectively. window.el provides the functions.
3 Here are the dates of my first and latest init file updates since I started the current versioning system:
4 I answer to no one except for my wife.
-1:-- Emacs Carnival November 2026 (Post Raymond Zeitler)--L0--C0--2026-11-05T19:31:34.544Z
-1:-- Tracking issues with Emacs and Org mode dynamic blocks (Post Isa Mert Gurbuz)--L0--C0--2026-11-05T16:47:00.000Z
neorepl 0.5 is out! When I announced neorepl last month, I wrote that I’d always wanted a terminal REPL that felt like CIDER, without the Emacs part. A few releases later I think neorepl is mostly there, so this one went out with “CIDER in a terminal” for a tagline. It also has everything from the “What’s next” list in the 0.4 post, which almost never happens with my plans.

That’s where you spend most of your time in a REPL, so that’s where most of the work went:
defn lines up with doc-string?. The lookups happen in the background
now, so typing never waits on the server.$EDITOR, as it does in bash, for
when a form outgrows the line editor.set editing-mode vi in ~/.inputrc gets you vi’s keys. neorepl takes
the readline settings JLine knows from there, and what’s under
$if neorepl is for neorepl alone.Not everyone likes that much going on around the cursor, so ,set lists
the REPL’s settings and turns them on and off, C-x C-h, C-x C-p and C-x C-q
toggle eldoc, pretty printing and quiet mode, and
~/.config/neorepl/config.edn says what they start as:
{:eldoc false
:print-length 100
:autoreload true}

,ns without a name opens a full-screen browser of the project’s
namespaces, or every loaded one after you press a. Type / and part of a
name to narrow them down, → to see a namespace’s public vars with their
docs, Enter to switch to it and e to open it in your editor. ,spec does
the same for the spec registry: a spec’s form, the specs it names one key
away, and x for an example generated from it, as long as test.check is on
the server’s classpath. (With cider-nrepl, x only works since
0.5.1, which came
out today as well. I found that bug recording the GIF above.)
,refs lists the functions that use a var, and ,deps the ones a function
uses, courtesy of cider-nrepl’s fn-refs and fn-deps. They come in handy
before you change a function’s arguments, and even more so for coding
agents, which get them as neorepl refs and the MCP refs tool. They only
look through the project’s code that’s loaded, mind you.

,autoreload has the REPL reload the namespaces whose files changed before
each evaluation, and before ,test, ,inspect and ,ns, so you’re never
calling yesterday’s version of a function. It’s
clj-reload under cider-nrepl, which
reloads the namespaces that depend on the changed ones too, in the right
order, and keeps what defonce defined. When a reload fails you get its
error, rather than an evaluation against half-reloaded code.
Coding agents need this even more than people do, as they edit files all
day long and forget to load them about as often. neorepl eval --reload and
the reload option of the MCP eval tool take care of that.

A few more things CIDER users will recognize:
tap> show up above the prompt, and ,tap opens the last
one in the inspector, with cider-nrepl 0.64 or newer.,profile times every call of the functions you name, or of a whole
namespace, and without a name shows what it measured: how many calls each
function had, and how long they took, on average, in all, at the shortest
and the longest. neorepl profile does the same from the shell.The CLI was meant for scripts from day one, and it turns out an editor
doesn’t need a plugin to use it. neorepl eval -f FILE --line N:COL -
evaluates the top-level form at the cursor in the file’s namespace, with
the buffer’s text on standard input, so unsaved changes count too, and
neorepl eval --ns NS - does the same for a selection.
Helix can’t send a buffer to a command, so there
the keys save the file first, and that’s all it takes:
[keys.normal."+"]
e = [":write", ":sh neorepl eval -s editor -f %{buffer_name} --line %{cursor_line}:%{cursor_column}"]
l = [":write", ":sh neorepl load %{buffer_name}"]
doc/editors.md
has complete setups for Neovim, Emacs and Helix. (Yes, Emacs. Please don’t
tell anyone.) The REPL sees whatever the editor evaluates, as they share a
server, so with the REPL in a split next to your editor you get a pretty
decent setup out of a few lines of config. The setups evaluate in a session
of their own, called editor, so ,sessions editor takes the REPL there,
and ,pst shows the last error your editor ran into.
neorepl’s tests run against fakes that behave like cider-nrepl, which says
nothing about the next cider-nrepl, or the next babashka. So now CI also
runs neorepl against the real thing: nREPL with cider-nrepl, started by the
Clojure CLI and by Leiningen, babashka’s server, ClojureScript on Node
through piggieback and through shadow-cljs, nbb and Basilisp. That turned
up a fair share of bugs on both sides of the wire. Running a ClojureScript
namespace’s tests through piggieback failed with a circular dependency, a
ClojureScript test’s error came with the line NaN, code that threw on nbb
didn’t fail neorepl eval, and source, macroexpand and apropos didn’t
work on Basilisp. All fixed now.
neorepl needs babashka 1.13.227 or newer these days, as that’s the first release with all the JLine and socket classes it used to work around.
in-ns, and Michiel fixed them within a day or two.
They’ll be in the next nbb release. Thanks again, Michiel! On the
babashka side, the test output going to the wrong message and info
knowing nothing about namespaces are fixed on master.0.6 is about getting neorepl to more people:
After that comes 1.0, where the CLI’s exit codes, its edn and JSON output and the MCP tools become a contract I promise to keep.
$ bbin install io.github.nrepl/neorepl
$ cd my-project
$ neorepl
The CHANGELOG has everything I skipped here, and the README has the details. Please keep the issues and ideas coming!
Keep hacking!
-1:-- neorepl 0.5: CIDER in a Terminal (Post Meta Redux)--L0--C0--2026-11-05T15:34:00.000Z
I’m happy to announce that neocaml 0.11 is out! I never got around
to writing about 0.10, which was mostly about neocaml-utop (a utop
backend that talks to utop via its native editor protocol), but 0.11 is a
different story. Its changelog is twice as long as that of any release before
it, and by now I’ve stopped pretending that a grand 1.0 is just around the
corner.1 There’s a lot in there, but three things stand out for me - OxCaml
support, indentation styles, and much better support for documentation. Here’s
why…
OxCaml is Jane Street’s set of extensions to OCaml - modes and
modalities, kinds, unboxed types, stack allocation and so on. You can use them
today with an OxCaml compiler switch, and Jane Street’s libraries use them all
over the place. A lot of the heavy lifting happened upstream, as
tree-sitter-ocaml 0.26 learned to parse OxCaml, and
Tim McGilchrist bumped neocaml’s grammars to it. Pieter
Goetschalckx (the tree-sitter-ocaml maintainer) once again cut an
ABI 14 build of the release, so we didn’t have to break anyone’s
Emacs. (see the 0.9 post
for the whole ABI saga) Huge thanks to both of them!
A grammar that parses the code is only half of the job, though. Here’s a bit of OxCaml, the way neocaml shows it:

neocaml now highlights and indents all of this. The new keywords (stack_,
exclave_, local_, global_, borrow_ and kind_), modes and modalities,
kinds, unboxed types and literals (float#, #1.0, #(...)), immutable
arrays ([: ... :]) and comprehensions are all covered. The indentation
follows the way ocamlformat lays out Jane Street’s own OxCaml
libraries (e.g. portable, unique and parallel), which is why the body of
an exclave_ isn’t indented, while those of stack_ and local_ are.2
Kind definitions also show up in imenu and work with the defun navigation
commands.
If you don’t write OxCaml, nothing changes for you - plain OCaml looks exactly
the way it did before. If you do, there’s nothing to turn on in neocaml. Just
make sure Emacs sees your OxCaml switch and you’ll get its ocamllsp
and ocamlformat as well. The docs have all the details.
Here’s a confession - neocaml’s indentation used to be a bit of a mix. Some
layouts followed ocamlformat’s conventional profile, some followed
janestreet, and a few followed neither. That’s a problem, as OCaml doesn’t
have one indentation style everyone follows. ocamlformat ships several
profiles, Jane Street’s code doesn’t look like the compiler’s, and plenty of
projects never run a formatter at all.
So 0.11 lets you pick one of three styles via neocaml-indent-style:
ocp-indent, which indents the way ocp-indent does out of the boxconventional, after ocamlformat’s default profile (used by ocaml.org, ppxlib and Irmin)janestreet, after ocamlformat’s janestreet profile (used by Jane Street’s libraries, dune and ocaml-lsp)They agree on most things and differ on a handful of layouts, e.g. how far the body of a match case goes:
1
2
3
4
5
6
7
8
9
10
11
(* ocp-indent and janestreet *)
match value with
| Some payload ->
finish payload
| None -> 0
(* conventional *)
match value with
| Some payload ->
finish payload
| None -> 0
The best part is that most of the time you won’t have to pick a style yourself.
By default neocaml looks for the project’s .ocamlformat and picks the style
that matches its profile (it also respects cases-exp-indent). Without an
.ocamlformat it checks for an .ocp-indent file, and without either you get
the ocp-indent style, as that’s what most projects that don’t use
ocamlformat indent with. (e.g. opam, Rocq, Frama-C, Eio and the
compiler itself) You can
also set the style per project in .dir-locals.el, or switch it for the
current buffer with M-x neocaml-set-indent-style.
I also wanted to know how well all of this works on real code, instead of
guessing. So I wrote a couple of small scripts (they live in dev/) that format
a bunch of OCaml files with each ocamlformat profile and count the lines
neocaml would indent differently. I ran them on the almost 300 files of the
opam packages in my default switch (mostly Jane Street’s base), and 0.10
didn’t look great. About one line in ten had no indentation rule at all, so
TAB simply left it wherever you typed it, and only 81.9% of the lines landed
where ocamlformat’s conventional profile puts them (77.8% for janestreet).
Ouch! With the matching style, 0.11 gets 97.5% and 98% of them right.3 Not
perfect, but pretty close!
Keep in mind that indentation only places the lines you’ve broken, it doesn’t
break them for you. If you run ocamlformat on save it has the final word
anyway, but with the matching style less of what you type moves around when
you save.
The third big theme is documentation. A few months ago Matthieu Lemerre
suggested (#60) that neocaml should render the ocamldoc markup in
comments, instead of showing it as a soup of braces. I loved the idea, and in
0.11 doc comments finally look like documentation - {b bold} is bold, {i
italic} is italic, [code], {[ ... ]} and {@ocaml[ ... ]} are shown as
code, and {!references} and tags like @param get faces of their own. This
is the new doc-markup font-lock feature, and it’s enabled at the default
font-lock level. Here’s how the doc comments in an interface file look now:

Tim also contributed neocaml-odoc-mode (#46) for odoc’s
standalone .mld documentation pages, built on Tim’s own Tree-sitter grammar
for odoc. You get font-lock for headings, inline markup,
references, links, tags, lists and tables, plus indentation and imenu. On
Emacs 30+ the code in {@ocaml[...]}, {@dune[...]} and {@opam[...]} blocks
is highlighted as well, using the grammars for those languages:

Run M-x neocaml-odoc-install-grammar to get started, and check out the
docs for more.
While I was at it, I also fixed M-q and M-j in comments. Embarrassingly,
M-q used to always fill the first paragraph of a comment, no matter where you
were in it, and it merged code blocks into the text around them. M-j now
continues the * of comments that start each line with one.
Those are just the highlights, of course. Here are a few more things that I think you’ll like:
neocaml-mlx-mode for .mlx files (OCaml with JSX, as used by
Melange and React), contributed by Akira Komamuradune’s
promotions (C-c C-o T, C-c C-o = and C-c C-o P), with their own
bindings in neocaml-cram-modedune project in a monorepo gets its own REPLneocaml-format-buffer replaces only the text that changed, so point and
the window positions stay putHere’s the REPL highlighting in action:

Most setups will keep working just fine, but a few changes in this release require some action on your part:
tree-sitter-ocaml 0.26 now, so reinstall the OCaml grammars
with C-u M-x neocaml-install-grammars. The dune grammar moved to its new
home, ocaml-dune/tree-sitter-dune, so reinstall it as well
with C-u M-x neocaml-dune-install-grammar. neocaml will warn you if your
grammars are too old.ocaml-eglot and
merlin, so they had to move. In
the REPL minor modes sending the phrase at point is now C-c C-e, sending it
and moving to the next one C-c C-s, loading the file C-c C-u and
interrupting the toplevel C-c C-q. The dune commands moved from C-c C-d
to C-c C-o.ocamlformat
nor ocp-indent a few layouts will move, so set neocaml-indent-style if the
ocp-indent style is not your cup of tea.With OxCaml, mlx and odoc on board, neocaml now covers pretty much every kind
of OCaml file I can think of. Famous last words, right?
So, what’s next? I don’t really have any big plans for 0.12. I’d like to switch
neocaml-mlx-mode to the dedicated grammar from
tree-sitter-mlx once it has an ABI 14 release, and there’s
the usual pile of smaller ideas in the roadmap. No promises, though.
Big thanks to Tim McGilchrist, who did a ton of work on the grammars
(OCaml, dune, opam and menhir) and the odoc support! Thanks
to Akira Komamura for bringing mlx to neocaml, and to Alessio
Duè for fixing a sexp navigation bug! And thanks to everyone who reported problems
and shared ideas - many of the improvements in 0.11 started as an issue on
GitHub.
As usual - update from MELPA, play with it, and let me know how it goes. The full list of changes is in the release notes.
That’s all I have for you today. Keep hacking!
-1:-- Neocaml 0.11: OxCaml, Indentation Styles, and odoc (Post Bozhidar Batsov)--L0--C0--2026-11-05T05:30:00.000Z
A comment from Dave on Emacs salmagundi, 2026-10-11:
I preload compile-history in some directory trees via .dir-locals.el, and then I can just type M-x compile and choose something from the history. I document this at https://davemq.github.io/2026/05/12/remote-linux-dev-with-emacs.html.
That's a very nice tip!
He continues
This multi-compile package looks interesting. But neither your article or the README for the package have examples of running it. Do I type M-x multi-compile?
No, the command is actually M-x multi-compile-run. I probably should have
mentioned that at least once when writing about it.
In the upstream repo the information is located at the very end of the README. It probably should be mentioned much earlier.
-1:-- Follow-up: Emacs salmagundi, 2026-10-11 (Post Magnus)--L0--C0--2026-11-04T19:09:00.000Z
A long while ago (in February this year) I got nice comment from Mike on my switch to project.el
That said, I was surprised to see your comment about having to set
vc-handled-backendstonilbecause otherwisevcinterferes withmagit, as I have not seen any such conflicts myself, and I made the transition toproject.elseveral years ago.Maybe it's not worth it to get into it: you obviously found a straightforward workaround, and perhaps there's additional background that you simply didn't include, or I'm over-interpreting "interfere" when you just mean some quirk that I just no longer notice having lived with it for long enough—but it seemed worth it to mention that this may not be universal, and thus might be worth looking into?
I did a search through my commit history and found that I disabled it in August 2024:
Disable VC completely
Apparently VC was activated when loading the config as all of it is in git repos (thanks to straight). This resulted in a question about following a symbolic link in a git repo (thanks to 'vs-follow-links being set to 'ask). I could have turned off the asking, but since I prefer to use magit anyway and I don't use any other VCS I decided to turn VC off completely for now.
Yes, there's a type in there, vs-follow-links should be vc-follow-symlinks I
believe.
I reverted about two years later when I switched to elpaca:
Remove the config for VC
It looks like the need for disabling VC disappeared with the move from straight. Now only .projectile is an extra marker for projects.
I would be very surprised if my workaround is still needed when using straight.
-1:-- Follow-up: Switching to project.el (Post Magnus)--L0--C0--2026-11-04T18:59:00.000Z
If you’ve been around Irreal for as little as a year and a half, you know that my most important and most used iOS app is Journelly. I use it as my memo book to record my daily activities and random things that I want to remember. For example, if I see some article or ad in my browser, I can ask Safari to share it with Journelly and a Journelly entry is automatically made complete with a link to whatever it was I wanted to capture. Each entry contains a date/time stamp as well as the location of where the entry was made.
Best of all, Journelly keeps its data as an Org file so I can query or edit it in Emacs. Of course, Journelly has a search function but sometimes I want to do a more complicated search or even to just count the number of entries in the database (4361 as of right now). Having the database as an Org file means that my data is available on all my devices, even if that device is a Mac.
I’ve been using Journelly since March 2025, ever since it was in beta, and have never looked back. Lately, however, it’s been getting a bit laggy, something I put down to my large database. I was on the verge of truncating my active data to the most recent entries in order to improve performance when Álvaro Ramírez pushed a new beta—v1.5(4)—that completely solved the problem. Now Journelly performs just like it did when I started.
If you are an Emacs user and are using an iPhone/iPad, I can’t think of any reason why you wouldn’t want to use Journelly. It allows you to integrate your day-to-day note taking with the power of Org mode. Even if you aren’t an Emacs user, you’ll probably find Journelly useful. It can also save its data as a markdown file so you can still get all the benefits of an easily accessible and editable database even if you prefer another editor.
-1:-- A New Journelly Beta (Post Irreal)--L0--C0--2026-11-04T15:47:04.000Z
This is in response to issue 12 in my dotfiles.git. I am posting it here since (i) some of the information is useful in general and (ii) it is easier for me to find it here.
The specifics of my Emacs configuration:
What is the motivation to divide between what seems to be like e.g. prot-lisp consisting of what seems to be just functions (library) and prot-emacs-modules which seems to be just package configuration (like what you’d fine in the body of a use-package declaration)?
I keep prot-emacs-modules and prot-lisp separate because they have
a different role. The latter consists of what effectively are Emacs
Lisp “packages”. I write them in the same style as actual packages
that I maintain (check https://protesilaos.com/emacs).
The modules are the specifics of my configuration. They have the exact preferences I rely on in my current session. A package may define options. The module picks one among them.
This is exactly what we all do when we configure a package from GNU
ELPA, for example: the package defines options and in our init.el we
set it up as we see fit.
Strictly speaking, this distinction is not necessary for a personal Emacs configuration. All the code is mine. I could easily hardcode values, not provide abstractions, and not bother with clear points of entry. Doing so would work well for my use-case, which is the main goal of all this code, anyway. But that would be a counterproductive habit; “counterproductive” because I want the code I write to be easy for others to copy and adapt. It also conditions me to be more thoughtful with my programs and more organised with my writings.
There are some exceptions where a function is defined in the modules. This is either because the function really is about my configuration or it is part of some experiment I am doing.
Also, is there anything you’d suggest to make it easier to try out e.g. your modeline from the config and and tweak from it into my own? Copying the contents of those two files and trying to evaluate it is not enough as it depends on other functions. I guess the only way is to take your whole config and play with it in a separate emacs –init-directory and dig around for relevant parts, right?
It is probably better for you long-term to follow the video I did about writing a custom modeline and tweaking its colours:
By understanding those concepts, you will be prepared to copy only the
parts you need from prot-modeline.el and tweak them accordingly.
That granted, here is how you can get my code into your setup (and
anything from prot-lisp/ for that matter). The use-package for
prot-modeline is the equivalent of what I have in my modules right
now.
(use-package prot-common
:vc ( :url "https://github.com/protesilaos/dotfiles"
:lisp-dir "emacs/.emacs.d/prot-lisp"
:main-file "prot-common.el"
:rev :newest))
(use-package prot-modeline
:vc ( :url "https://github.com/protesilaos/dotfiles"
:lisp-dir "emacs/.emacs.d/prot-lisp"
:main-file "prot-modeline.el"
:rev :newest)
:config
(setq mode-line-compact nil) ; Emacs 28
(setq mode-line-right-align-edge 'right-margin) ; Emacs 30
(setq prot-modeline-show-frame-name (alist-get 'undecorated initial-frame-alist))
(setq-default mode-line-format
'("%e"
prot-modeline-kbd-macro
prot-modeline-narrow
prot-modeline-buffer-status
prot-modeline-window-dedicated-status
prot-modeline-input-method
" "
prot-modeline-buffer-identification
" "
prot-modeline-major-mode
prot-modeline-process
" "
prot-modeline-frame-name
" "
prot-modeline-vc-branch
" "
prot-modeline-eglot
" "
prot-modeline-flymake
" "
mode-line-format-right-align ; Emacs 30
prot-modeline-which-function-indicator
prot-modeline-notmuch-indicator
" "
prot-modeline-misc-info)))
use-packageIt seems you don’t use use-package (I see
(require ...in the modules) anymore? Curious why.
I consider this “sand in the gears”. It forces those who read my configuration to slow down and be careful about what they are copying. If they just paste my Elisp into their setup, they will get an error. Slowing down gives them the chance to read the code and think about what they are working towards.
Yes, this sounds like I am doing them a disservice. But I think they will benefit from this practice long-term. Copying willy-nilly is how you never learn.
Concretely about the use of require, in the modules I use my own
macros. Every require should be wrapped in a prot-emacs-configure.
Not all my modules rely on require though. I have some packages that
are loaded when a certain condition is met, typically via a hook.
In prot-lisp/ the use of require is standard: this is how packages
are supposed to do it.
By the way, writing my own configuration macros has helped me understand things at a deeper level. It also gives me maximum control over my setup. But the real reason is the sand in the gears that I alluded to.
Is there any reason to autoload features? I’ve come across some guides on using Emacs and it seems people tend to prefer to autoload everything as much as possible but I feel like it only makes sense to autoload only features you might not use during a session. The time to load on startup when you front load instead of autoload seems less important than the time it takes to load when you actually use a feature during a session. Especially if running Emacs as a server or using a single long-running instance.
With use-package you get deferred loading when you use keywords such
as :bind and :hook. Internally, this depends on the autoload
mechanism that Emacs has. Autoloads are hints that inform Emacs about
the existence of some function or variable in a certain file. When
that symbol is called for the first time before its file is loaded,
Emacs goes to the corresponding file, evaluates everything, and then
runs the original code. For example:
(autoload 'prot-test-hello "prot-test")
If we call prot-test-hello, Emacs evaluates prot-test.el, and runs
prot-test-hello. This works only if the file is in the load-path.
Users generally do not need to understand the technicalities. When
installing a package, Emacs generates an index of all the autoloads
automatically. If you are curious, go to the elpa/ subdirectory of
your Emacs directory, find a package, and read the autolaods file
(e.g. denote-autoloads.el).
Strictly speaking, deferred loading is not necessary. If you are going run Emacs for several hours non-stop, then the ~60 seconds it takes to load an average configuration do not matter. Even ten minutes are not be important in such a scenario: you can brew a coffee in the meantime.
Deferred loading is still beneficial though:
Autoloads help you discover new features. For example, when you press
M-x in a generic Emacs you are presented with thousands of options.
Most of those are not loaded yet. Emacs just knows the symbols and
will load their files when you make the selection. A package developer
can leverage this feature to provide functionality that may be useful
only under certain circumstances. That is a helpful way for the user
to notice the before and after states. It also gives the user maximum
control of what to load and when, which improves confidence in one’s
Emacs configuration.
Do you ever run Emacs on the terminal or GUI only and do you always have a single long-running instance or do you sometimes use multiple instances?
I always use the GUI. It can combine different font families and font sizes and it can also display images—how else am I supposed to check my selfies? The terminal is fine in its own right and is especially useful for remote connections. But I do not have that use-case.
My terminal emulator is xterm, by the way. It works well, though I
do not rely on it. Everything that could be done in a terminal is
handled by Emacs. I also am on Debian stable, so trying the newest
programs is neither a straightforward option nor a priority: I have a
stable setup and focus on getting work done.
I use a single instance of Emacs. In my configuration I also have
server-start. This gives me the option to evaluate Emacs Lisp code
even when Emacs is not focused, such as when I switch the theme of my
entire desktop environment or window manager.
I do not run Emacs for long hours. When I am done with whatever I am working on, I switch off the computer. The typical workflow is for 2-3 hours maximum. Then I will do some outdoor activity.
Any thoughts on using Evil?
It is fine and I am indifferent to it. What makes Emacs so nice is that you can evaluate Emacs Lisp live and work with the new state. The choice of keybindings is a personal choice. There is no better or worse.
People will, of course, argue about which paradigm is more efficient. This is the kind of knowledge that seldom translates into real-world productivity. The bottleneck is how fast and how well you can think. Then what matters are the qualities of your output. Moving text around quickly does not make you productive per se or, indeed, a decent writer/programmer.
This is a matter of comfort though, which can indirectly influence your productivity simply by improving your mood and willingness to write. I put colours and fonts in that same category. If it makes you feel better, then use it.
That granted, to configure evil-mode properly you need to have a
solid understanding of Emacs and be willing to control every part of
your setup. Otherwise you will run into cases where something does not
work as expected and you will not know how exactly to change it.
It seems like you’re willing to embrace Emacs defaults–is there anything you’ve still maintained Emacs to be more Vim-like, e.g. modal and arguably more ergnomic/mnemonic?
The general scheme of Emacs default keybindings is good enough for me.
Though I change plenty of keybindings to do what I want. Actually, I
plan to rebind basics such as C-n, but I have not found the time to
do it thoroughly. There are many default keybindings that I do not
like.
I do not think Vim is ergonomic by default. Impulsively reaching out to the Escape key is not helpful, especially if you are twisting the wrist in the process. Typing Shift with the relatively weak small fingers is not beneficial either. Yes, you can circumvent those, but I am referring to the out-of-the-box experience, because if you are thinking about making changes then you might as well do the same for the Emacs-style keys.
Emacs keys are not ergonomic either. Ergonomics is a matter of the entire office setup, from the keyboard, to the desk, the placement of the monitor, the typing style. I have an article on the matter: https://protesilaos.com/codelog/2026-05-04-emacs-keyboard-ergonomics/.
My custom keyboard has hjkl mapped to the arrow keys on a so-called
“layer” that I can activate with my thumbs. The yuio keys above
correspond to Home, PageDn, PageUp, End, respectively. Those
are the bigger motion counterparts of the cardinal directions. Extend
this to Ctrl and one of those keys to perform a different motion,
like C-<home> to go to the beginning of the buffer, C-<right> to
move forward by a word. This works everywhere, by the way.
I actually have plenty of articles on the topic of keyboards, if you are so inclined: https://protesilaos.com/keeb.
I may have missed this, but how do you approach persistent session (if at all)?
I do not. This is not necessary in my workflow. I have bookmarks for the buffers/files I visit frequently. Then I have Git projects that I always keep in a clean state with a history that is meaningful. When I need to work on something, I switch to the bookmark/project and start from where I left it.
I like that if I return a project I’ve worked on a few weeks ago, restoring these aspects is a good way to suggest where I last left off instead of starting from scratch (little things like window layout or set of open buffers is suggestive of progress of the project).
This is a matter of how one’s brain works. Mine is more abstract, so
having things in a specific spatial arrangement does not add much
value. Configuring Emacs to take care of all relevant cases is thus
not appealing to me. I will commit my changes and leave everything in
a condition that makes the next step obvious. I am happy to write
TODO and FIXME comments in my code and/or have a relevant task in
my Org agenda. This is a matter of logical sequencing. It is easier
for me to reason about it.
I used to load the built-in desktop.el. Then I realised that loading
hundreds of buffers for something that I might not need is
counter-productive. Furthermore, storing a specific state for a
potential future occasion requires that I remember to load the right
file, manage all such files, and so on, which is cognitively
burdensome and ultimately no more efficient than working with Git.
🔷 Consider making a donation.
-1:-- Reply to Emacs questions about configuration, Evil, and more (Post Protesilaos)--L0--C0--2026-11-04T00:00:00.000Z
Today is election day in the United States, and if you’re eligible to vote and have already done so, thank you. While we await the results over the next several days, here’s something to help pass the time, particularly if you use Org mode HTML export in Emacs.
As with many tools that export HTML, Org mode leaves most aesthetic decisions like CSS layout and typography to the user to define. Org mode’s default HTML export output is serviceable, but why settle for that? In particular, my expectations for HTML export in 2026 are this:
CSS has long supported both of the above with media queries. More recently, CSS has a light-dark() function for handling OS appearance changes.
Org mode has numerous ways to customize how CSS is used in export (see org#CSS support). For my purposes, setting the customizable variable org-html-head-extra serves my needs as it will be the default behavior for exporting Org to HTML. For any special cases, I can locally override that variable.
The Elisp I use in my Emacs initialization file is as follows:
1 2 3 | |
Using the function insert-file-contents lets me specify the CSS in a separate file. For the above example, I’ve defined my CSS in the file cc-org-html.css. Readers are welcome to use or modify cc-org-html.css to their satisfaction.
Adaptive Dark Mode
Just as I was finalizing this post, I saw this WebKit post (Introducing Adaptive Dark Mode | WebKit) pop up in my feed today: apparently Safari 27.3 beta includes support to automatically adapt a website that does not support OS appearance changes. This is an early and browser-specific feature, so I still think specifying light and dark behavior is worthwhile, especially since the light-dark() function makes this so easy to do and is widespread.
Addendum Nov 6, 2025
At the date of posting, the file cc-org-html.css had errors in the call to light-dark() which are now corrected. The above link to this file has been amended to point to the corrected file.
-1:-- Beautifying Org HTML Export (Post Charles Choi)--L0--C0--2026-11-03T21:45:00.000Z
Randy Ridenour has taken another step in the journey so many of are on: moving everything inside Emacs. Regular Irreal readers will recall that Ridenour is a Philosophy professor so he doesn’t have much need for high powered math tools but he does have to deal with grade calculations and various budget items. He had been using Soulver for this but, of course, this meant leaving Emacs, something all we Emacers hate.
Then he stumbled on literate-calc-mode. It did everything that Soulver did and Ridenour didn’t have to leave Emacs to use it. It’s been a win for him and he’s happy with it.
When I read Ridenour’s post, something seemed familiar. It turns out that I’d written about literate-calc-mode six years ago. It had just been released and no one had any experience with it yet. I meant to follow up but, of course, life intervened and I never did. I’m glad to see that it’s still around and has lived up to its potential.
I’m tempted to install it but I’m reasonably proficient with Calc itself so I probably wouldn’t use it enough to make it worthwhile. Still, if Reverse Polish Notation is not your thing and you’d rather deal with normal infix notation, literate-calc-mode may be a good fit. It’s easy to try out so give it a spin if you’re interested.
-1:-- Ridenour On Literate Calc Mode (Post Irreal)--L0--C0--2026-11-03T15:34:56.000Z
You can set up a capture template that creates a new Org heading. If
you define jump-to-captured to be
non-nil, org-capture will jump to that new heading. It
will also create a bookmark named org-capture-last-stored
at that line.
Bookmarks can help you find your way back to a line of code or a line
in an essay, and you don't have to be using Org. Before you navigate away to look something up, invoke
bookmark-set ( C-x r m ) to save your place.
This will place a character-sized image in the left fringe at the
line. Invoke bookmark-jump ( C-x r b ) to
return to your work.
If you have nothing better to do, you can change the shape of the
bookmark.1 First, design it with define-fringe-bitmap.
Then set the variable bookmark-fringe-mark to your new
bitmap using customize.
The Lisp expression in the following source block defines a Z-shaped
pattern that's 8 pixels wide and 8 pixels tall and names it
bookmark-zeit. Figure 1 shows the default bookmark and
this new bookmark for comparison.
(define-fringe-bitmap 'bookmark-zeit
[#b11111111
#b01111111
#b00001111
#b00011100
#b00111000
#b11110000
#b11111110
#b11111111])
![]() |
| Figure 1 -- Above, default bookmark; below, custom, Z-shaped bookmark |
The pattern can be a different size, but I'd recommend making it the same width as the fringe, which is 8 pixels by default. But you could make it taller. Figure 2 shows a Z-shaped pattern that's 12 pixels tall; note how well it stands out!
![]() |
Figure 2 -- A Z-shaped bookmark 12 pixels tall |
Recently I wrote about how to make the active window stand out. Originally, I thought to change the color of the left fringe to something much brighter. Now it occurs to me that one might leverage the bookmark system to fill the fringe with a pattern that's 64 pixels tall. Unfortunately it seems Emacs renders only the first 16 rows. In this case, adding eight 16-pixel-tall bookmarks on successive lines can fill up a 64-pixel vertical portion of the fringe. This is shown in Figure 3.
(define-fringe-bitmap 'bookmark-16
[#b01111110
#b01111110
#b01111110
#b00011000
#b00011000
#b01111110
#b01111110
#b01111110
#b01111110
#b01111110
#b01111110
#b00011000
#b00011000
#b01111110
#b01111110
#b01111110])
Figure 3 -- Eight 16-pixel-tall bookmarks on consective lines |
One important caveat to all this is that this re-defining of bookmarks affects all bookmarks everywhere, not just the latest one or the next one. At least that's true of Emacs version 30. Who knows, maybe we'll see a fringe-decor package eventually. As if there's isn't enough to play with now ....
1 But if you do have better things to do and want to
change fringe bitmaps, you could check out the
packages fringe-helper or modern-fringes
on melpa.
-1:-- Emacs Bookmark -- Just a Bitmap in the Fringe (Post Raymond Zeitler)--L0--C0--2026-11-02T22:37:19.150Z
Belated happy 50th birthday, Emacs!
In other news, you can now reach Emacs News at https://emacs.news. Enjoy!
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-11-02 Emacs news (Post Sacha Chua)--L0--C0--2026-11-02T14:20:32.000Z
-1:-- My blogging setup in 2026: reducing friction (Post Diego Zamboni)--L0--C0--2026-11-02T13:16:00.000Z
My calculation needs are usually fairly simple — adding up exam scores, solving a few basic probability problems, etc. Most of the time, Emacs Calc is more than enough. There are a few times, though, when it’s useful to have the calculation embedded in a page of notes. In these instances, I’ve always used Soulver on the Mac.
I recently stumbled on Literate Calc Mode by Robin1 that does everything I was using Soulver for, and I now have one more reason to not leave Emacs. It’s in Melpa, so installing it is simple. Once required, it provides both literate-calc-mode and literate-calc-minor-mode. I find the minor mode particularly useful for Org mode scratch buffers. For example, I used it recently to sketch out some details for a conference funding request. In an Org buffer, I entered:
Expenses for February 2027 Baylor Trip
Hotel = 500
Parking = 10
Registration = 295
Mileage = 300
Meals = 200
Total = Hotel + Parking + Registration + Mileage + Meals
Then, M-x literate-calc-minor-mode instantly changed it to this:
Expenses for February 2027 Baylor Trip
Hotel = 500 => Hotel: 500
Parking = 10 => Parking: 10
Registration = 295 => Registration: 295
Mileage = 300 => Mileage: 300
Meals = 200 => Meals: 200
Total = Hotel + Parking + Registration + Mileage + Meals => Total: 1,305
The part of each line beginning with => is not editable and displayed in a lighter shade. Changing one of the variable values results in an instant update of the total. It uses Calc, so it is able to do almost anything that Calc can do. So far, I’ve used it to plan travel expenses and to build material lists for woodworking projects. It works flawlessly and I’ve found it surprisingly useful to have a scratchpad that can automatically do the math.
Tagged: Emacs
I couldn’t find a last name on the site.
-1:-- Emacs Literate Calc Mode (Post Randy Ridenour)--L0--C0--2026-11-01T22:56:00.000Z
The ability to mark items in GNU Emacs Dired allows for bulk operations on both files and directories. With marking, onerous tasks can be turned into a trivial ones.
While default Dired has many bulk operation commands, not everything is covered. That is the motivation for mdired, an Elisp package which extends Dired with some SIMD-style commands.
Using these commands fall into two steps:
mdired supports the following four commands:
mdired-copy: copy a single file to multiple directories.
mdired-do-command: run an Elisp command or named keyboard macro on multiple files.
mdired-compile: run compile on multiple directories.
mdired-byte-recompile: byte-compile selected files or directories.
Refer to the mdired User Guide for more details on usage.
Bulk operation commands are chainsaws. Use them with caution. That said, they are great for improvised workflows:
As mdired is new, I’m open to feedback on it. Let me know what you think.
-1:-- Announcing mdired, or adding a chainsaw to Dired (Post Charles Choi)--L0--C0--2026-11-01T22:25:00.000Z
TL;DR: LaTeX in Org mode now go whoosh, all pretty like. Install the development branch of Org mode to use it.
After grumbling about the inadequacy of LaTeX previews in Emacs – and specifically Org mode – for over a decade, Tecosaur and I decided to take the leap and rewrite the feature from the ground up. The result is a preview system for LaTeX math that can provide near-WYSIWYG feedback but retains the properties we expect in the Emacs environment: text-first, extensible and composable.
It’s not much code, but it contains many moving pieces and makes contact with several corners of Emacs. Now that the feature is part of Org mode, I thought I would take this opportunity to document how it works from the ground up, and use it as a launchpad to explain relevant aspects of Emacs’ event loop and its unofficial package API Emacs doesn’t really have a “package API”, since you build Emacs packages from the same elements as Emacs itself. .
This article is not an exhaustive demo of all the shiny new features, although there are a few videos peppered throughout. It’s also not the user manual (not yet updated on orgmode.org) or a tutorial (see Tecosaur’s This Month in Org writeup).
Instead, it aims to provide an overview of the goals and design of the library, along with some explainers of relevant aspects of the Emacs API and tips to write your own features. This write-up is for you if you are interested in
org-latex-preview?This is a feature included with Org mode to preview LaTeX math snippets as images in Emacs buffers. The contents of LaTeX fragments and environments in Org mode are replaced with rendered images.
It has existed for decades at this point, but in a form that I consider barely usable; this rewrite overhauls the code base and adds a number of quality-of-life features. There is a 13 minute demo that can give you a good idea of the new capabilities of Org mode:
The bullet points, if you prefer:
And a couple more quick demos:
The rest of this article is mostly about how it works, and briefly why. As such, it has a “fractal” level of detail. There is information intended
org-latex-preview library.To that end, many details are tucked away in blocks like the following. You can safely ignore them in favor of the top-level headings, images and video demos.
Blocks like this describe the concrete implementation of aspects of the code.
This is probably only interesting if you are looking to dive into the code itself, or if you are looking to write a similar package.
Blocks like this are for additional notes on aspects of Emacs’ API, including the odd code snippets.
Warning: Here be idiosyncratic takes.
Blocks like this describe other Org features relevant to LaTeX preview generation.
Command output
Blocks like this describe CLI commands associated with this process, or their output.
Indeed, this feature
LaTeX was not built with the aim of minimizing latency. Having to start multiple new processes even for a one-character change places a hard cap on what’s possible in terms of real-time feedback. Implementing an equivalent preview feature for a system designed for modern computers, like Typst, is trivial in comparison.
Moreover, as popular as Org mode is, it’s a small player in the space of markup languages. The fraction of Org and LaTeX users who would prefer to use Org mode to write LaTeX is smaller still. And while Org attempts to integrate well with LaTeX, navigating the mishmash of Org and LaTeX syntax presents an extra learning curve on top of learning to use the two markup languages individually.
But it also has a few things going for it.
First, LaTeX is ubiquitous, and Org mode is a far more convenient format for writing than raw LaTeX. There is significantly less visual noise (think Markdown vs HTML), and Org provides an escape hatch for adding LaTeX verbatim. Org’s simple interface for shuffling text around, monitoring task progress and executing code blocks makes it easy for eventual LaTeX documents to begin as Org files. Since everything in Emacs composes with everything else, your system for (say) fast LaTeX input transfers over to Org mode seamlessly.
Second, the fact that Org is versatile as a publishing tool implies that some benefits of better LaTeX support transfer over to other formats too, like equations in HTML or ODT exports.
There is a wide swath of applications built on Org mode that benefit from LaTeX math input, such as Org Roam and Org Noter. For these applications converting Org documents to LaTeX is not the goal, as they are not primarily publication tools: they stick around as private digital archives, and better LaTeX support is a bonus to the user’s expressivity.
Real-time feedback when editing LaTeX snippets is a categorical improvement to the user’s experience. I did not realize this when I started work on org-latex-preview, but now I wouldn’t want to LaTeX without it.
It baffles me that web LaTeX editors (Overleaf etc) don’t provide this out of the box, especially as they target novice LaTeX users.
.
Finally, we can keep Org mode out of the picture: org-latex-preview can technically work anywhere in Emacs, and in every major-mode. it’s just not there yet
(See below)
.
But perhaps the most germane reason is that Emacs users don’t shy away from settling comfortably into tiny, cramped and poorly insulated niches. Comes with the territory!
Emacs users are closely acquainted with its frequent unresponsiveness. You’re never more than one minor-mode or mode line indicator away from causing Emacs to block or lag. While it’s not a big ask to wait a second for the result of an action you initiate, the unresponsiveness often manifests as short random freezes, which is unacceptable. But there’s more. Beyond the annoyance and inconvenience, poor responsiveness denies us an entire class of uses: real-time feedback is everything for interactive applications.
Owing to both its design and age, Emacs is not very well suited for asynchronous applications. Certain patterns of asynchronous usage are viable (see below), but concurrency support is limited and parallelism is absent: no worker threads, channels, async-io, or anything of the like.
Generator-based async in Emacs lispI lied. In addition to Elisp threads, there exist prototype libraries for async-io (Chris Wellons) and channels/CSP (Andrey Listopadov) based on Emacs’ generators library, which in turn is implemented using continuation passing. But generators as provided by this library do not have anywhere near the level of robustness or support you need to build actual applications: no introspection, error handling or debugging support, poor performance, and just overall jankiness. I’ve tried and lived to regret it – Elfeed Tube is a real mess.
The Futur package is another promising swing at an async-io style interface, but it’s new and I haven’t been able to try it yet.
In any event, a more responsive org-latex-preview would require parallelism and not just concurrency. There is John Wiegley’s async library (and packages inspired by it, like session-async) that spawn a child Emacs that runs in parallel, but communication is limited and the overhead is sizeable, so this is a bit of a bludgeon.
The upshot is this: we want org-latex-preview to be as asynchronous as possible within Emacs’ effectively single-threaded event loop model. The goal is to minimize how long the user has to wait, in terms of both the overall run time and the fraction of that for which Emacs is unresponsive. We end up having to trade these off against each other.
A close second concern is preview generation speed. This is desirable both in the large and the small. Previewing upwards of a hundred LaTeX math fragments in a math-heavy Org document used to be an exercise in patience. Now it takes about one second, and can be much faster with some trade-offs.
| LaTeX Fragments | Old preview system (s) | org-latex-preview (s) |
Speed up |
|---|---|---|---|
| 35 | 12.96 | 0.43 | 30x |
| 70 | 22.59 | 0.51 | 44x |
| 160 | 33.99 | 0.68 | 50x |
| 330 | 84.95 | 1.32 | 64x |
| 660 | 139.73 | 1.96 | 71x |
| 1000 | 194.16 | 3.24 | 60x |
Figure 1: LaTeX preview generation times under the slowest settings. The vertical axis is time, and along each vertical line the red chunks correspond to intervals for which Emacs is blocked. We still manage to process a file with 250 LaTeX fragments in about a second with minimal impact on Emacs’ responsiveness.
Speeding up bulk generation is at most a boost in convenience. You don’t regenerate a large buffer’s worth of previews more than once every few sessions, and the resulting images are cached anyway. The more interesting aspect is that speed enables a close to real-time feedback loop that wasn’t available before. This is especially useful when you’re deep inside a messy structure like an array:
No more counting field separators. No more exiting, regenerating and re-examining snippets. It’s the best parts of WYSIWYG and visible markup, together.
Even in the niche of academic types who use Org mode with LaTeX, there are very different orientations. There’s Org the transient format, where an Org document starts out as a rough sketch, a thought bubble translated to the screen, and soon transitions into a TeX file that becomes the source of truth. There’s Org the intermediate publication format, where the document lives as an Org file, but its value is in the web page or PDF document it eventually becomes. For the adherents of this style the appearance of the Org document is irrelevant.
Finally, there’s the class of uses described above, where the desired output is the Org document itself, or a cross-linked collection of them. These are folks who care about the aesthetics of Org documents and often suffer a thousand cuts in trying to make Emacs look “modern”.
One of the objectives of org-latex-preview is to ease their self-imposed burden somewhat: LaTeX previews should blend in perfectly with the surrounding text. This means
Getting the fonts to match is possible too, but requires some degree of compromise on the part of the user You could start by using the variable-pitch Latin Modern font in your Org documents. .
Each of these is a long-standing issue with LaTeX previews in Org, and Stack Overflow and the usual haunts have decades of discussions on how to fix them, with band-aid after band-aid proposed as solutions. We took an elephant gun to this problem, starting by throwing out the giant mess that was Org’s accreted LaTeX preview code of the past two decades.
Here’s a demo of theme-aware LaTeX preview colors:
Finally, there are a few features we picked up along the way because they came free.
Recording compilation errors was a simple matter since we scan the LaTeX output anyway.
Consistent equation numbering was expensive in the past because numbering changes can propagate across the document, and parsing the Org document and LaTeX fragment compilation were both slow. Recent changes to Org have made the former a non-issue (see the parsing section below), and the performance scaling of our bulk LaTeX runs has fixed the latter.
There are several more minor features along for the ride, such as improvements to editing LaTeX with org-edit-special (C-c ').
As mentioned in the introduction, the LaTeX preview system draws on a few different facets of the Emacs API: mainly async process interaction (the “backend”) and overlays (the UI or “frontend”). The live-updating behavior leans heavily on hooks and timers, and we join these pieces together with very simple Lispy data structures, like alists and plists.
We also (naturally) use a few Org mode specific APIs, including ones developed by us for this purpose. We cover these further below as we encounter them.
If you are designing an Emacs application that interacts with the outside world, it helps to start with a mental picture of Emacs’ event loop. To a first approximation, the main loop does only four things:
Together these two connect the user and Emacs. If the main loop did only these two things Emacs would still qualify for the title of “Lisp evaluator masquerading as a text editor”, but it wouldn’t be nearly as useful. For that we need it to
The last event loop function combines the above three, but across time:
This design explains why timers in Emacs are not reliable in the absolute sense. The timer runs not when it’s ripe, but when it’s ripe and when the main loop gets around to it. Nevertheless, scheduling code provides a much-needed degree of flexibility to a rigidly ordered task list.
The way these four tasks are actually organized in the main loop code is less pleasing:
Also, Emacs is obviously doing things outside of the event loop as well. Font caches are compacted, Emacs is responding to the compositor and some OS signals, interacting with dynamic modules, and so on. In the currently experimental MPS branch, some garbage collection activities also occur in the background. These are invisible to the Lisp evaluator and thus out of scope when learning to build an Elisp package.
This design suggests that Emacs can only do one thing at a time As of Emacs 31 , so that designing a responsive application is a steep ask. But the situation is more nuanced:
Yes, the Lisp evaluator holds a global interpreter lock. At any point in time Emacs is doing exactly one of the following: painting the screen (redisplay), updating syntax highlighting (fontification), running hook functions or timers, responding to window changes, handling keyboard input, and – crucially for us – interacting with subprocesses. Every action is thus blocking.
The only asynchronous interaction possible is by starting an OS-level subprocess
Another lie, since we have dynamic modules, covered below.
. Emacs continues to run independently from the subprocess, but it still has to handle its STDOUT and STDERR streams and respond to process signals. When it is doing this, it cannot pay attention to your input or anything else. Again, Emacs can only do one thing at a time.
There are multiple commands to start an asynchronous subprocess from Emacs (see M-x shortdoc process), but start-process covers most common use cases.
(setq my-process-object
(start-process "name" "buffer-name" "program" "--arg1" "--arg2" "..."))
Processes are (optionally) associated with a buffer where process output is inserted. To send a subprocess some input, there’s process-send-string and process-send-region. Responding to process output and signals is the responsibility of process filters and sentinels (next block). kill-process does what you’d expect. All of these functions take the process object that start-process returns as an argument.
Except for some fiddly details, that’s essentially all there is to the process interaction API. There are several variants of start-process available – the synchronous call-process, the shell versions start-process-shell-command and call-process-shell-command, and so on. The only one that does something different is the primitive make-process, which is a more extensive version that can (additionally) create network processes, send data through TCP or Unix sockets and so on.
Emacs will try to prioritize user input events. To do this it can delay tasks like reading from subprcesses if necessary. Idle timers are another example of this. Some actions are thus delayable, but they block Emacs when they (eventually) run. There might be a penalty to pay afterwards, for instance when there’s more subprocess output to process as a result of delaying the task.
By default, Emacs inserts the output from a subprocess into the process buffer, or ignores it if there is no process buffer. (Crucially, it still has to read the output from the process pipe.) This is good enough for simple tasks. To use or transform subprocess output in more specialized ways, you can write an elisp function that is used as a process filter. It is called whenever Emacs reads new process output. In the language of shell commands, I think of process filters like this:
$ start-process "some-process" 2>&1 | process-filter
Process filters can be tricky to write if you want to parse structured process output, because the amount of data that Emacs reads from the pipe can be arbitrary. Unless latency is a concern, it’s often preferable to work with process sentinels for this reason. Sentinels are the second tool Emacs makes available for reacting to processes. A process sentinel is an elisp function (callback) called whenever the process changes state or raises a signal, including when it terminates. Practically speaking, I think of them like this:
$ start-process "some-process" 2>&1 > process-buffer && \
process-sentinel < process-buffer
They are typically used for cleaning up after the process, and updating Emacs’ state depending on how the subprocess ends. But you can use them to access the process buffer containing all the process output, and thus do whatever you would in the filter.
Filters and sentinels can be attached to a subprocess object created with start-process using set-process-filter and set-process-sentinel respectively.
Many blocking operations can be interrupted by signaling a quit: this is what pressing C-g does, with some variations, in most contexts. Crucially for us, however, quitting is suppressed when Emacs is handling subprocess output or signals
This is to keep your C-g from doing something unpredictable to “background” code that you didn’t initiate.
. If Emacs is blocked because it’s reading a large data dump… well, tough luck.
keyboard-quit (C-g) is a specific input that interrupts any code, with a few exceptions.
The obverse of this is to wrap code in a while-no-input clause – now any input will interrupt the processing of this specific block. This can be used to do background processing in small chunks without making Emacs unresponsive, but there are subtleties involved that I’m not familiar with.
Despite these carveouts, a package that interacts heavily with external processes faces a latency-throughput tradeoff. The more time Emacs spends starting processes and managing their output, the less attention it can pay to your input. Designing Org’s LaTeX preview thus involves finding an acceptable balance.

Figure 2: (Top) Emacs dealing with a “fat” high-throughput process, where it spends a chunk of time reading subprocess output. The time between reading your input and running the resulting command is thus longer than (bottom) the “thin” low-throughput case.
It is significantly easier to write a large Emacs library than an equally versatile plugin for most plugin systems built around multi-threaded GUI applications. I say this as a non-programmer who has only succeeded at the former. We lean on the interpreter lock in many ways, choosing simple data structures and control flow that wouldn’t work if Emacs was multi-threaded. But most of Emacs wouldn’t work if Emacs was multi-threaded, anyway. In for a penny…
Overlays are a way to alter the appearance or behavior of regions of buffer text without modifying the text itself. Displaying images is one of their most common uses: LaTeX preview images are overlaid on top of the source fragments.
Once you have the preview images in hand, placing them in overlays and managing their state is essentially all the LaTeX preview code does.
In fact, overlays account for much of the display trickery in Emacs: Images over text? Overlays. “Virtual text” like in whitespace-mode? Overlays. Inline results of code evaluation: overlays. Multiple cursors? Those are overlays. Emacs has only one cursor.
For a quick look into the overlay API, run M-x shortdoc overlay.
The even shorter version: We can create an overlay ov from buffer position BEG to END with
(setq ov (make-overlay BEG END))
and move it around with
(move-overlay ov NEW-BEG NEW-END)
To avoid creating garbage, it is preferable to reuse overlays when possible instead of throwing them away with (delete-overlay ov) and starting over. move-overlay can move them to other buffers too.
The only other tricky question is what happens to the overlay when text is inserted at its left or right boundaries. Does it expand to contain the text? This is left as an exercise for the reader.
These alterations are specified by adding or modifying the overlay’s “properties”. Like symbol properties, overlay properties are a way to store arbitrary information along with the overlay, and org-latex-preview uses them to keep track of state, such as whether the preview image is out of date because the LaTeX fragment has been edited, and to store LaTeX compilation errors for the corresponding fragment.
To set and retrieve overlay properties, we can use
(overlay-put ov prop value)
(overlay-get ov prop)
where the property prop is any elisp symbol. There is also an overlay-properties function to access the full list. Once again, M-x shortdoc overlay has us covered.
Some properties have special meanings as interpreted by Emacs’ display engine. display, the most common property, changes how the overlay region appears on screen, and this is typically set to the image we want to see in place of the text.
But several more interesting modifications are possible with overlays, such as changing the face (font+style) of the underlying text, making it invisible, making it intangible to the cursor, or specifying keymaps that are activated when the cursor is within the bounds of the overlay.
The only other detail we need is the list of overlay properties that Emacs considers special. The display property can be used to show an image instead of the text “below” (or “under”) the overlay, for example:
(let ((img (create-image "/path/to/image.png")))
(overlay-put ov 'display img))
The face property can change the display of the text. Here we dim the text the overlay covers:
(overlay-put ov 'face 'shadow)
Here are a few more interesting uses of other special overlay properties.
Send a message if the text under the overlay changes:
(defun notify-on-ov-change (ov after-p beg end &optional _)
(when after-p
(message "Text changed: %s"
(buffer-substring-no-properties beg end))))
(overlay-put ov 'modification-hooks (list #'notify-on-ov-change))
You can use this to set up a “watcher” – perhaps run a process when a region inside a buffer changes
If you want to set up a watcher on the contents of the entire buffer as opposed to a piece of it, you should avoid overlays and look at the hook after-change-functions instead. Be warned: it’s easy to slow down Emacs to a crawl this way if you’re not careful.
! This is not hypothetical: preview-latex does this to re-dump the LaTeX preamble of TeX files (in the background) as you edit them.
Add a highlight and tooltip (“hover text”) when the mouse is on the overlay:
(overlay-put ov 'mouse-face 'highlight)
(overlay-put ov 'help-echo "You're on an overlay, pal.")
Open the displayed image externally when pressing RET with the cursor on it:
(overlay-put
ov 'keymap
(define-keymap "RET"
(lambda () (interactive)
(when-let* ((img (get-char-property (point) 'display))
((imagep img))
(file (image-property img :file)))
(start-process "view-image" nil
"xdg-open" file)))))
This works by defining a keymap overlay property with the required function. (On non-Linux systems, you will want to change "xdg-open" to an equivalent command-line utility.)
Center an image in a window:
(let* ((img (image-create "/path/to/image.png"))
(spacing (propertize
" " 'face 'default
'display
`(space :align-to (- center (0.5 . ,img))))))
(overlay-put ov 'display img)
(overlay-put ov 'before-string spacing))
This image stays centered as you resize the window:
This one needs a little explanation: The before-string overlay property displays an object before the overlay itself. The object we choose to display is some propertized text with a display text-property. The display text-property provides the means to insert spaces of specified widths, and a symbolic way to refer to the text margins, window center and horizontal window boundary positions. Combining the two gives us (consistently) centered images in Emacs.
In its basic form, the LaTeX preview feature involves no reactive behavior: there is one command to overlay preview images on LaTeX fragments, another to clear those overlays, and nothing else.
But this is a far cry from our goal of real-time feedback. For LaTeX previews to be automatically generated and updated as we type, we need both timers and hooks.
Hooks are variables provided by Emacs where we can register code to run at certain points. In this case, we want to react primarily to two kinds of user inputs:
In turn, org-latex-preview exposes hooks of its own for consumers to run additional processing. These fire when LaTeX subprocesses finish, when overlays are created, and so on.
Timers are complementary to hooks, used to space out or limit the consequences of hooks firing. They are used
org-latex-previewBesides basic Elisp data structures, that’s everything we need from Emacs to implement non-blocking, fast LaTeX previews. Assuming you have a handle on what org-latex-preview does, we can start peering into the how.
Here is a high-level overview of the control flow of LaTeX preview generation:
Figure 3: Control flow for Org mode’s LaTeX preview system.
This is a rough picture of how LaTeX previews are generated in Org mode, subdivided into tasks The made-up term “task” corresponds to a handful of functions devoted, in our heads, to one purpose. . Each action belongs to one of three categories:
When a LaTeX preview command is issued, the first chunk of processing (the PREPARE block)
It then starts a two step process chain (the RUN block) that runs asynchronously. The end result of a successful run is a preview image or series of images.
The PROCESS FILTERS block: From this point on until either process ends, Emacs checks in periodically through the associated process filter, acting on the STDOUT to capture various bits of preview metadata. It also keeps track of images that have finished processing, and (synchronously) caches them and adds them to the overlays in the buffer for these fragments. This continuous update is doubly useful:
The POSTPROCESS block: After the subprocesses have finished, Emacs runs their process sentinels, which clean up the detritus of this process chain, or reset the buffer if the preview run failed.
And speaking of detritus…
This figure shows the same process but with an emphasis on disk I/O. The primary chain is TeX file → DVI/PDF output file → SVG/PNG preview images.
Of the file categories involved in the process, we care about two: the “format dump file” produced by precompilation, and the preview images. Both are cached and persisted across Emacs sessions. All other produced files are deleted by the POSTPROCESS block.
Displaying previews that are color-, alignment- and size-aware, along with associated information like errors, requires a considerable amount of bookkeeping. Since this information arrives piecemeal and processing happens asynchronously in chunks, we also need some way of storing and passing along this context. There are perhaps clever ways to do this, but we use a straightforward solution – a (dynamic) global plist that associates each active process chain with its data (see org-async below).
All of the data generated and required for the many tasks in our process chain are captured in a single mutable container, referred to here – for convenience – as “Extended Info” (extended-info). This data includes
and more besides.
extended-info: implementation “Extended Info” is implemented as a plist (property list). Actually, almost every compound data structure org-latex-preview uses is either an alist or a plist, which is just a linked-list under the hood. extended-info is also a tree since a few of its members are plists themselves (see below).
We used property lists at first since they’re mutable and schema-less, and the design of the library was very fluid. Unfortunately we are now well past the point where we could switch to something more structured like cl-defstruct, so plists it is.
If it’s any consolation, most of Org works with large property lists too. Org’s export system passes around a big plist bucket for communication in much the same way we do here. Access can get expensive as they grow, but at least passing them around is cheap. It’s… not much of a consolation.
apply yourself: property lists as arguments plists have one other coincidental(?) advantage: You can splice them in as arguments to a cl-defun.
Consider:
(cl-defun foo (&key (buf (current-buffer)) (valid-p t) (pred #'ignore)
&allow-other-keys)
;; body of foo
)
This function has the signature
(foo :buf some-buffer :valid-p t :pred #'always)
which is just a list with the name of the function followed by… a plist. So we can build up the plist programmatically and use it directly as foo’s argument list:
(let ((arglist `(:buf ,some-buffer :valid-p t :pred ,#'always :unknown-key nil)))
(apply #'foo arglist))
’tis a silly thing but oh, so satisfying. In fact, this is a convenient way to feed a job spec to org-async (see below).
extended-info is populatedThe setup functions in the PREPARE block initialize this bucket with data from parsing the buffer – i.e., LaTeX fragment locations, equation numbers, etc. From then on, each task updates extended-info with progressively more information. The structure of extended-info and this flow of information into it is visualized in this “data flow” diagram:
Contained in “Extended Info”: “Processing Info” contains information about and relevant to the external process (LaTeX and the image converter). “Fragment” is a list of metadata for fragments to be previewed, including the overlays that will eventually display the preview images. All of this preview metadata is obtained from the outputs of the latex and image converter runs via the process filters.
Note that Emacs’ interpreter lock precludes data races and other concurrency issues, since only one function modifies extended-info at a time.
extended-info is accessedThe above chart only captures the flow of data in one direction: from the sources of computation into the extended-info container. Tasks that don’t modify extended-info still need to read it to do their jobs. For example,
Most functions in the process chain are passed extended-info, or a subset thereof, as an argument.
Once again, we get away with it because there are no concurrency problems when there is no concurrency. This design pattern of passing around a single heap-allocated container across setup functions and callbacks works for all kinds of applications in Emacs. This is exactly how Org’s (synchronous) export system manages information as well.
org-latex-preview Pieces and the Org APIThis is where we get into the weeds. If you’re here for the bird’s eye view, you can skip ahead to Live previews now.
This is the Collect LaTeX Fragments task in the control flow diagram.
Org’s syntax is extensive, and parsing Org files used to involve a giant mess of regular expressions. Thankfully the mess is now handled by the org-element library, which provides a simple and powerful API for querying or acting on Org’s AST. A fair amount of work has gone into making it perform well under the constraints of Emacs’ single-threaded event loop
Besides caching its state, it uses timers to schedule parsing work outside of the times the user is interacting with Emacs.
.
Crucially for us, Org has first-class support for LaTeX environments and fragments, and this greatly simplifies parsing and metadata handling. This support is what makes using LaTeX in Org a pleasant experience in the first place.
org-element library
Added in late 2021, the org-element library greatly simplifies the process of working with Org documents.
I have a minimal working knowledge of the library, so this block covers only the aspects of org-element that we use in org-latex-preview.
Org’s syntax distinguishes between “elements” and “objects”, where anything “larger” than a paragraph is an element, and any syntax that could be contained within a paragraph is an object
Incidentally, the default mark-paragraph binding runs org-mark-element in Org mode, which selects the smallest logical element is at point instead. Try M-h at different locations in an Org buffer to see what element you’re on!
.
This distinction matters to us because LaTeX text to be previewed can be either. As you might expect, inline math ($...$ or \(...\)) is an object, and LaTeX environments (\begin{foo}...\end{foo}) are elements, and they differ in the way we must display the preview images.
Every node in the AST as parsed by org-element is made available as a plist whose keys are properties of the element. These properties include the start and end positions of the element and its contents (:begin, :end and :contents), as well as metadata like properties and keywords. For example, every element can be named with a #+name: keyword.
These are the only things we need from org-element as far as parsing goes.
The objective of the parsing step is to find the bounds of LaTeX fragments in the buffer (or in some region). In principle, we could do this with org-element:
(org-element-map
(org-element-parse-buffer)
'(latex-environment latex-fragment)
#'identity)
But this would be too slow, and would allocate far more memory than we need. Instead, we use a simple regexp scan of the buffer to find the bounds of LaTeX fragments and environments, and org-element only to gather specific metadata about these elements.
This is the Create preamble task in the control flow diagram.
Many editor plugins (and web apps) implement LaTeX previews, usually via MathJax or KaTeX. KaTeX is fast, and both function well for web pages with limited needs. But they implement ersatz support for a limited number of LaTeX packages, and cannot consume an actual “full-fat” LaTeX preamble of the kind you might use when writing a research paper. There is no \usepackage{} macro or TikZ support, for example.
By comparison, org-latex-preview is backed by a full LaTeX engine and LaTeX distribution, so nothing is off the table. Of course, this means we need a way to construct the preamble and feed it to the LaTeX engine.
In addition to Emacs-wide settings (like org-latex-preview-preamble and org-latex-packages-alist), document-specific LaTeX headers can be specified with the #+latex_header: directive:
#+latex_header: \newcommand{\sOmega}{{\scriptstyle \Omega}}
#+latex_header: \DeclareMathOperator{\diag}{diag}
#+latex_header: \DeclareMathOperator{\toep}{toep}
#+latex_header: \usepackage{calc,cancel}
#+latex_header: \usepackage{rotating}
#+latex_header: \usepackage{tikz}
The combined preamble must be constructed when exporting the Org document to LaTeX, and this machinery lives in the ox-latex library. org-latex-preview simply calls into it for the preamble. Among other benefits, this means the preamble used for the preview run is identical to the one used for LaTeX exports
unless you want them to differ, because of course Org provides ways to do that
, so these truly are previews.
org-export (ox) library The ox library and its children, like ox-latex provide an interface to convert Org documents to any other format, like Pandoc does.
While the specifics (like collating a LaTeX preamble) are output-format dependent, the basic method is uniform and simple. An exporter maps each Org syntax element and object to a function that creates the equivalent in the output format, like image links to HTML img tags, or #+begin_quote ... #+end_quote blocks to HTML blockquotes elements. The fact that the Org syntax supports LaTeX environments natively helps with LaTeX exports.
Of course, when a clean mapping like this is not possible, you can work on the buffer directly and rip out and replace raw text. Emacs, huh.
This does make ox-latex one of the few dependencies of org-latex-preview, which is regrettable given the larger aim of providing LaTeX previews across Emacs.
This is the Precompile LaTeX preamble task in the control flow diagram.
To process a preamble, the LaTeX engine needs to tokenize and execute tens of thousands of lines of macro source. For a complex preamble, the kind you would use for a book, this can take several seconds on a modern processor.
Since org-latex-preview aims to provide close to real-time feedback as you type, we resort to a trick from the 1990s: we serialize the entire engine state – the string pool, macro bodies as token lists, font data, engine execution parameters and so on – into a single binary memory image. This process, called format dumping or precompilation, was automated for document preambles by David Carlisle’s mylatex.ltx, modernized as the mylatexformat package by Florent Chervet.
The upshot is that precompilation saves a significant amount of LaTeX startup time, reducing and (almost) flattening it across all preamble sizes.
Precompilation relies on a number of hacks and only works because pdftex has changed little for decades. Unfortunately, precompilation is not supported at all by xelatex and only partially by lualatex
preambles that load OpenType fonts or Lua code are unsupported
, so org-latex-preview is slow with the Unicode engines.
Under a different LaTeX preview architecture, none of this would be needed. But it’s what we have so far: a very 90s solution for a very 90s editor.
And speaking of being stuck with an old architecture, depending on pdflatex implies that we have to create and feed it a TeX file for each batch of previews pdflatex and friends do accept input via STDIN, but this did not work out for finicky reasons. .
This is the Create TeX file task in the control flow diagram.
As you might expect, the generated TeX file contains a reference to the precompiled dump file and a listing of all the LaTeX fragments to be previewed. Instead of placing them into the document separated by something like \pagebreak, we place them inside \begin{preview} and \end{preview} blocks provided by the preview.sty CTAN package.
preview.sty is authored by David Kastrup, who also wrote the Emacs-side preview.el package that’s part of AUCTeX. preview.el was the inspiration for org-latex-preview – it was clear at least that previews in Org mode could be much, much faster.
preview.sty does three critical things for us:
\begin{preview} and \end{preview} blocks) on a separate page in the LaTeX output;These modifications were technically doable through a combination of other packages (Org’s old implementation used the tightpage option of the preview machinery), but in cumbersome and error-prone ways.
This makes preview.sty an external dependency of org-latex-preview. Fortunately this package is included by default even with minimal installs of all common LaTeX distributions.
The pipeline from the LaTeX fragment text in the TeX file to images suitable for preview consists of two steps. A LaTeX engine (pdflatex, xelatex or lualatex) produces DVI, XDV or PDF output, which is fed to an image converter (dvisvgm, dvipng or imagemagick) to produce PNG or SVG images. This is the RUN block of the control flow diagram.
This involves a fair bit of bookkeeping, and Emacs’ callback-based async process management (via “process sentinels” and “process filters”) isn’t the most ergonomic tool for this. In the absence of reliable async-io support, we use org-async, a declarative process chain manager now bundled with Org mode.
The
org-async library Because we need to run a chain of processes with different callback behaviors depending on the exit statuses, org-latex-preview uses org-async, a process queue manager that wraps Emacs’ start-process, process filter and process sentinel API. It handles a lot of the bookkeeping for us, providing for
extended-info bucket in our case) and passing it to the filter and sentinel,The global variable that keeps track of the context data between async calls is managed by org-async.
The following code might be out of date, please refer to the Org manual.
An example of an org-async job spec might look like the following (simplified for clarity):
(setq task-spec
(list 'org-async-task ; <-- LaTeX compilation task
'("latex" "-interaction" "nonstopmode" "texfile.tex")
:buffer t
:info extended-info
:filter #'latex-preview-filter
:failure #'latex-failure-callback
:success (list 'org-async-task ; <-- Image conversion task
'("dvisvgm" "--page=1-" "-o out-%p.svg" "texfile.dvi")
:buffer t
:info extended-info
:filter #'dvisvgm-place-previews-filter
:failure (list #'dvisvgm-failure-callback
#'log-errors-callback
#'cleanup-callback)
:success (list #'check-fragments
#'cleanup-callabck))))
Starting this process chain is simple:
(org-async-call task-spec)
org-async-call also provides other conveniences like timeouts, retries and concurrent subprocess limits.
An example of this bookkeeping is that the pipeline isn’t always serial. When generating PNG images with dvipng, we start LaTeX and dvipng in parallel. Dvipng processes pages of the DVI file as they appear, while the LaTeX process is still churning them out. (Generating PNG preview images for a large Org buffer is much faster than the other routes.)
On the other side of the process invocation are the “process filters”, which are callbacks called by the event loop whenever Emacs reads chunks of subprocess output. This is the PROCESS FILTERS block in the control flow diagram.
A shell metaphor for process filtersFollowing the shell metaphor for process filters, a simple way to think of a process filter for an Emacs subprocess is as a sink at the end of a Unix pipe:
some-process 2>&1 | process-filter
In the LaTeX preview case, the picture is a little more complex as there are multiple processes:
latex -interaction nonstopmode -output-directory /tmp preview-file.tex | latex-process-filter; \
dvisvgm --page=1- --bbox=preview -o image-file.svg preview-file.dvi | image-converter-process-filter
The analogy is different for each of the image converters. Since dvipng runs in parallel with LaTeX, for example:
{ latex -interaction nonstopmode -output-directory /tmp preview-file.tex | latex-process-filter; } & \
{ dvipng --follow -T tight --depth --height -o image-%09d.png preview-file.dvi | image-converter-process-filter; }
In org-latex-preview’s process filters we look for the output (DVI or image) file paths, but also a variety of metadata that preview.sty and dvisvgm emit. This includes geometry information like the width, height and baseline alignment of the image, and any compilation errors.
preview.sty
Preview: Fontsize 10pt
! Preview: Snippet 1 started.
<-><->
l.9 \begin{preview}
Preview: Tightpage -32891 -32891 32891 32891
! Preview: Snippet 1 ended.(973936+0x22609920).
<-><->
l.14 \end{preview}
The printed information is cryptic. Here’s what the numbers mean.
Margins:
left bottom right top
Preview: Tightpage -32891 -32891 32891 32891
height depth width
! Preview: Snippet 1 ended.(973936 + 0 x 22609920).
The preview.sty manual provides more details and documents other options.
pre-processing DVI file (format version 2)
processing page 1
applying bounding box set by preview package (version 13.1)
width=346.003754pt, height=15.362961pt, depth=.501877pt
graphic size: 346.003754pt x 15.864838pt (121.606411mm x 5.575853mm)
output written to /tmp/org-tex-ang3Se-000000001.svg
1 of 1 page converted in 0.16035 seconds
[1 (preview-latex version 13.1) (preview-latex tightpage option detected, will use its bounding box) depth=1 height=29]
Fine-tuning Emacs’ process interaction parameters
“Whenever Emacs reads chunks of subprocess output…”
This phrasing omits some important but fiddly details about how often and how much output is read at a time, in much the same way as the Unix pipe | symbol does.
While the event loop is switching between its four main tasks continuously, there are (only) a couple of knobs available at the Elisp level to manage how Emacs reads subprocess output.
read-process-output-max limits how much it reads in one event loop turn. Setting it to a high value means output from chatty processes will be read sooner, but at the expense of blocking user input. This is, essentially, Emacs’ latency vs throughput curse in a nutshell.process-adaptive-read-buffering tries to delay reading from subprocesses that produce very small chunks of data at a time, as this is inefficient. In practice turning this on has tended to make things worse instead of better when running most subprocesses.Trying to optimize this for org-latex-preview has proved to be… difficult. The rates and sizes of output emitted varies significantly between different LaTeX engines and image converters:
Figure 4: Intervals for which Emacs is blocked when previewing using SVG and PNG images. The vertical axis is time, and the solid chunks are periods where Emacs is unresponsive.
They differ even between different versions of the same program across Texlive versions:
Figure 5: A time-aligned flamegraph profile showing process filter calls for the LaTeX and image converter processes when using Texlive 2022 and 2023. In the aggregate, the LaTeX and dvisvgm binaries shipped with Texlive 2023 aren’t faster or slower than the former. They just have a different profile across time to which it is difficult to adjust.
org-latex-preview settles for a broadly acceptable set of defaults in this regard.
But the main reason we use process filters here is that we can place images in the buffer as they are generated (and read in from the process STDOUT by the event loop), and don’t have to wait for the LaTeX or image converter processes to end.
The unfortunate result of being forced to do file I/O at each step of the pipeline is that we are left with a trail of intermediate files that need to be swept away Instead of, say, a block of shared memory that can simply be freed. . This is the POSTPROCESS block in the control flow diagram, accomplished in the “process sentinels”.
A process sentinel is a callback invoked by the event loop when the process changes status. In principle this includes any status change (exit, signal, stop/continue), but in practice we only care about it after the process exits.
Clearing the intermediate detritus of the compilation is not an urgent task, so org-latex-preview’s process sentinels actually schedule this on an idle timer. Their other major function in this design is to diagnose process failures and restart the job when the cause looks transient.
This is the Cache images and metadata task in the control flow diagram.
LaTeX preview generation is an order of magnitude (or two) faster than what Org did before, so caching preview images might seem like a secondary concern. But org-latex-preview still caches images, for a few reasons:
org-latex-preview is responsible for generating the images to be bundled with the web page or document.In addition to the text of the LaTeX fragment itself, preview images are specific to the LaTeX preview pipeline configuration, the preamble, the Emacs theme and even the fragment’s position in the buffer when equation numbering is turned on. To cache previews reproducibly, all of these inputs are hashed together as the key to the cached image.
org-persist library
While you can specify where the preview images should be cached relative to the document, org-latex-preview defaults to using org-persist, a general-purpose caching library that is part of Org mode. org-persist is used for various caching duties by Org mode, such as resources fetched from the Internet and the partial syntax trees of parsed Org buffers.
Caching LaTeX preview images actually involves more than copying a file somewhere and serializing a hash table to disk. We also need to store, as Elisp data, the image display geometry information, including the baseline alignment height. The org-persist API supports caching different container kinds for exactly this purpose.
Finally, with sufficient time (or a large enough corpus) the cache of LaTeX images can grow to gigabytes in size, so cache entries are set to expire after a month, another function served by org-persist’s built-in garbage collection.
This covers the in-buffer UI and affordances of org-latex-preview, indicated by the Create overlays and Place LaTeX previews blocks in the control flow diagram.
Beyond what is covered in displaying images over text, displaying the image itself is simple: the image just needs to be resized and aligned according to the recorded geometry. The overlays themselves are set up before starting the LaTeX run, and a series of overlay properties are set or unset over the course of the process.
currentColor attribute, we also color the overlay image red to indicate that there was an error.These are basic but (hopefully) intuitive indicators of the state of the preview.
The breakdown provided so far does not explain the claims we made in the objectives section, like real-time (or at least fast) feedback as you type. As seen in the demos above, there is one more element to making the previews appear “live” and reactive to cursor movement or buffer edits.
For the seamless hide/show behavior:
For real-time feedback:
org-edit-special, C-c ') should cause the preview to be updated as you type.All of these are achieved using hooks, which is ultimately the only mechanism we have to trigger reactive behavior in Emacs. The hide/show behavior involves tracking the cursor position via pre-command-hook and post-command-hook (made buffer-local). Buffer edits are tracked via after-change-functions, but with several fast-fail heuristics to avoid slowing down Emacs
The difficulty of identifying, in real time, $ and $$ as LaTeX delimiter starts, delimiter ends, or as actual dollar signs is why live previews require the unambiguous \(...\) and \[...\] delimiters, even though the dollars are supported in non-live usage.
. The list goes on, since we need to track Org’s folding states, as well as changes in other buffers when using org-edit-special.
The other strategy we’re forced to employ, given the event loop’s limitations, is to throttle and debounce pipeline calls, batching edits and avoiding race conditions between in-flight LaTeX runs – the interpreter lock protects Elisp state, but independent subprocesses will happily interleave and clobber each other’s output.
Finally, this feature is the source of dozens of edge cases, such as editing fragments inside folded regions (during global text replacement, say), or the many invalid intermediate states of a LaTeX fragment causing the preview to fail repeatedly. Knocking down these bugs while keeping this path fast was a substantial portion of the effort over the years this library was developed Now that it’s part of Org mode and seeing more widespread use, there are no doubt several more edge cases waiting to be reported! .
A proof of concept: the design of org-latex-preview makes it possible to preview LaTeX outside of Org mode. Here’s an example previewing LaTeX-formatted comments in a prog buffer (Clojure, here):
In fact, you can use the same code to preview LaTeX fragments literally anywhere in Emacs, including, delightfully, the Emacs calculator. This (18 minute) demo runs through a bunch of contexts outside of Org mode showcasing live LaTeX previews:
While this feature exists only as a prototype, it’s simple to implement and extend. To see why, let’s look again at the preview process breakdown. All tasks here are agnostic of Org mode save for two:
org-element,org-export (ox-latex) library.To use LaTeX previews outside of Org mode, we only need providers for these two tasks:
A parser that can identify the boundaries of LaTeX fragments in the buffer
Technically you could reuse org-element in non-Org buffers for this, but there will be many false negatives and occasional false positives.
. This does not need to be a full-fledged parser – just a regexp scan of the buffer looking for \begin{ or \[ can suffice. This is how the above demos were created.
A LaTeX preamble generator appropriate for the buffer. This can just be a constant string (instead of a function) that includes a reasonable set of LaTeX packages. I used Org’s default LaTeX preamble string everywhere in the above demos.
org-latex-preview This description might be out of date, please refer to the Org manual.
org-latex-preview tries to separate these independent concerns by providing the function org-latex-preview-place, that generates previews anywhere in Emacs given the above information. Previewing LaTeX fragments is as simple as:
(require 'org-latex-preview)
(org-latex-preview-place 'dvisvgm entries nil preamble)
where entries encodes the start and end positions of the fragments, and preamble is the LaTeX preamble to use.
;; entries is a list of lists
;; Each entry is (start end),
;; or (start end contents)
((1342 1388)
(1911 2104)
(2273 2295 "\\( \\epsilon + 3 \\)"))
;; preamble is a preamble string
;; See `org-latex-preview-preamble'
"\\documentclass{article}
[DEFAULT-PACKAGES]
[PACKAGES]
\\usepackage{xcolor}"
The positions in entries would be obtained from your parser for the major mode. For preamble you can just reuse or tweak org-latex-preview-preamble in a pinch. See the function documentation for more details.
Requiring the org-latex-preview library will in turn load a few Org libraries (org-element, org-persist, ox and org-macs), but it does not load all of Org. We tried hard to keep from loading the 16,000-line behemoth that is org.el.
As of late 2026, abstracting the library from its major-mode adapters remains on the TODO list.
When we started org-latex-preview in 2021, live-updating previews were not available anywhere except one LaTeX formula-editing website and, to a lesser degree, LyX. We often wondered why, since preview.sty did most of the work for you and the editor side was pretty simple to implement. If org-latex-preview were a thrown-together external Emacs package providing many of the same features (sans polish), it could have been written over a couple of weekends. I know this because I wrote it. Twice.
Over the years, however, this feature has grown in ubiquity and there are plugins for most note-taking (Obsidian) and editing software (Neovim, VS Code) that provide this. Emacs itself has at least four active implementations of live LaTeX previews now, and a frontier LLM can probably implement a basic version for your preferred platform with one prompt.
Even so, Emacs remains the oldest platform to implement this feature, and does so in the most complete and mature form As of 2026. . Considering I spent a lot of this write-up complaining about the limitations of the Elisp runtime, this is remarkable.
But creating temporary files after every buffer change and spawning separate LaTeX processes remains a terrible way to do real-time updates. Precompilation is another hack, unsupported by LaTeX engines besides pdflatex. While org-latex-preview is a massive improvement over the previous LaTeX preview implementation for Org mode, it’s still the Elisp equivalent of a Bash script that got out of hand. Quite simply, this is the wrong level at which to address the problem. A better way to implement continuous previews would be to talk to a managed process over a pipe or socket. The right approach here is to work at the level of the TeX engine, as TeXPresso does.
But it would be even better to avoid inter-process communication entirely and use an Emacs dynamic module, linking with a modern LaTeX library that does incremental (instead of batch) compilation. “Lightweight” alternatives like KaTeX or MathJaX don’t cut it, since we need a full-fat LaTeX compiler, macros and all. One possible candidate for this is realtime-tex, the rtex library, which embeds LuaLaTeX (the most modern of the LaTeX engines) and compiles incrementally and effectively instantaneously
Although I would be inclined to wait for the project to mature.
. This would be a quality improvement almost as drastic as the one org-latex-preview makes over its predecessor.
Of course, a dynamic module linking an incremental compiler is a completely different architecture from what we have now, and would share at most the image display and UI layer. So projects like this and the inevitable official Typst preview package for Org are tabled for another day. Maybe you’ll be the one to implement them?
-1:-- Understanding Org's LaTeX previews (Post Karthik Chikmagalur)--L0--C0--2026-11-01T15:00:00.000Z
I've updated the transcript below, but it might take Linkarzu a little while to apply it to the YouTube video, so it might not be in sync yet. Enjoy!
Download the transcript or read it online (with screenshots)
Yeah, 100%.
And I have a question for you. Sacha: Yes.
Sacha: So here's your waveform for it. If I play it back, which is of course the Spaceball reference, it will show me then this thing. I can copy the text if I wanted to, or I can extract this into its own video.
Sacha: Of course I do my editing within Emacs. This is what my video editing looks like within Emacs. I can just copy and paste the subtitle cues that I want. I tell it where I want the output to be. I tell it what audio that I want to use in case I've replaced it. Here, for example, I'm using the same audio as well as the video from a previous live stream recorded session. and then I can just reorder things, clip things, change the timestamps here. If I want to cut from there onwards, sample bits of it and so on, it's something I can do. Linkarzu: Can you remove words as well or just entire sentences? Sacha: One way to do that is to split at that point. So for example, if I want to remove definitely, let's load this one so we can see where I am. I can follow along here. So this is my livestream from yesterday. "My bottleneck is definitely thinking speed." So here's definitely, right? This is the start of "definitely thinking speed". If I split here, and I split at the end of "thinking speed", I can say let's skip this. And I think, assuming I haven't broken... "My bottleneck is thinking speed." Linkarzu: Okay. Yeah. Just a little tail there, but I get it. Sacha: Yeah. It's just a matter of choosing the right timestamp. "Thinking speed." Linkarzu: So that is pretty interesting and based. So you do all of your videos in Emacs, edit all of them there. Sacha: Yeah, pretty much. Well, it started because in 2020, I think, I wanted to make a presentation for EmacsConf. And I wanted to record my audio and then just add the visuals on top of it. But I don't like clicking the mouse because I'm on a Lenovo ThinkPad. The tiny little pointer is handy, but... if I use a separate mouse, it's annoying. Anyway. Clicking is annoying, especially since I'm so used to being able to automate everything with keyboard shortcuts. So I said, why don't I use Emacs for video editing and audio editing? So what I can do then is I can take my audio recording... Or actually, I can record from Emacs itself and then I can edit it to remove the oopses and the false starts and change all the things that I want to. Then I can add the images or videos that I want for each clip and it will automatically speed up or slow down the video to fit the duration of the audio. So if I'm saying, you know, this is a quick preview of the conference schedule, et cetera, then it speeds up the scrolling automatically so I don't have to worry about doing it fast enough if I want to. I can also tell it to play this original speed. That has just grown since then. Every time I want to do something else... Like now I'm thinking, you know what would be really cool is if I could add a title card very easily, right? So let me go to, let me start a new thing here. WebVTT. So if I want to say, I want to add a title card that says "Hello world", right? Oh, I need to actually save it. Let's save it as, hang on a sec. Breathe, Sacha. It's okay. test.vtt. Yeah, that one. Okay. All right. So now I have a test.
Sacha: If I say Hello World, then it just calls ImageMagick to create this file here. If I open that file, it's super simple.
Sacha: It's just white text and a black background using my custom font, because I went to the trouble of figuring out how to make a font with my handwriting and then kern it using Org Mode. Because why not? Linkarzu: Okay. Sacha: That's ready. Then I can pop that in and I can say, you know, this is a test here. Let’s do a subed-record here. "This is a test here." And then that audio here, it becomes a whole thing. If I compile this, I think it will just make a thing. Let's put it into #+OUTPUT: test.webm, sure. All right. mpv test.webm "This is a test here." Linkarzu: I see. Pretty cool. Have you seen...
Linkarzu: How do you do your drawings, by the way? Like, do you have like a tablet or something? Sacha: Oh yeah, yeah. I use Noteful on an iPad. I used to draw using Krita on a Lenovo X230 tablet PC, but at the moment I can just use the iPad with Noteful. Currently I'm drawing in dark mode because you have a dark mode channel, so I have things like my sketches.
Sacha: But I can press C-; and it uses substitute. "Orgzly Revived" here. What that does, in addition to the substitutions that happen throughout the buffer, so for example, if I'm adding a transcript, it will change that all throughout the transcript. I've also added some code (because this is Emacs, I can just do that) that saves this in common errors or commonly misrecognized words. And then when I recognize text, whether it's before a voice command or if I'm just doing this or whatever, it will apply all of those common fixes, common edits. Here we go.
Sacha: So it automatically adds it to this thing here which gets saved automatically for me. And that's the change that I've just added. Linkarzu: Interesting. Pretty cool. Sacha: It's really nice to have that kind of [dictionary] shortcut built into the way that I edit things. All I know is I just hit C-; and put in what it's supposed to be. Then it gets not only fixed in my current file, but in the future as well. Linkarzu: Yeah, I need to work on my transcript game. Local models? Sacha: Oh, yeah, it's so cool. C-. b here. Was it this one? Okay, so that turns into this kind of blog post. Right? You have the embedded video. You have the chapters. Clicking on the chapter link will advance to that.
Sacha: And then you have the whole transcript right here. with screenshots. So then people who don't want to listen through an entire hour-long video can just jump to the interesting parts themselves. I think these ones also go back to the video up there, which someday I'm going to figure out how to make a nice sticky display for. Clearly I need to fix that because it's not actually on that thing over there. Theoretically, if I'm publishing it right, this time I think I've been trying to host the videos on the Internet Archive so people you have to watch on YouTube if they don't want to. But yes, so the idea there is the video gives you so much text that you can then share. Linkarzu: Yeah. Or edit a video way faster. Before I came up with this automated thing for creating my clips, I used to do that in DaVinci, you know, because DaVinci has like a transcript feature and you can... but I'd just rather not touch it at all.
Sacha: So this is a doctor. You can say things like, I'm feeling happy today, as I said, right? And then it uses that to generate its response. But I have this conversation mode. Yes, especially when I get to tinker with Emacs. And then... What happened when you get to tinker with Emacs? Does the TTS come through? Yeah, it does. The reason I did this was because after I showed off my vocal commands to Emacs Paris, they were like, oh, you know, what if you can have a conversation? So I was like, okay, let's come back full circle to the thing that got me curious about Emacs in the first place and voice enable that. And then, of course, this is a very old AI.
Sacha: And then on the computer, if I need to capture something quickly, ooh, I can show you this one. Let's see. "Remind me to put together a quick description of my to-do workflow." It will take that note and it will stick it into an inbox. Here I'm in live mode so my public tasks don't get mixed in with my private tasks. If I do an agenda, it should just be limited to the public stuff. Anyway, so the "Remind me" created the note for me. It includes the audio in case the speech recognition didn't get anything right. It includes a screenshot so that in case I'm doing this from a different application, because my keyboard shortcut is global, I can still see the context and go find that sort of stuff. Linkarzu: Quite interesting, quite advanced, quite complex. It must have taken you months, years to set all of this up. Sacha: As I said, I can only work in like 30 minutes or so. So this one, I think was a 10, 15 minute hack. Little bits of time. This link here to whatever I was looking at, you got that free from Org Mode. This thing here with the recording, I got that because I already wanted to save my recordings anyway. So it was just a matter of adding a link to the file name that I already had saved. This screenshot, you know, another five minutes because I already had Spectacle capturing the screenshot of both screens. So it's fairly simple. It will even work for things like, remind me next month to follow up with Linkarzu to see whether he's switched to Emacs. C-x b and switch to another application just like I switch to other buffers. Linkarzu: Yep. Moving on to other topics.
Linkarzu: There's this dotfyle thing that came up like some years ago, but I think it's just mainly an automated thing that pulls up GitHub repos. I don't think it has like an actual person that is behind it, pulling up news and aggregating all the stuff. There needs to be a human that does this, that adds the touch and adds the love that you do on the Emacs side of things. I'm not willing to take that job. I don't know if someone on the Neovim community is willing to do it, because, yeah.
Linkarzu: Yeah, yep. I do kind of something similar. This is the way that I schedule live streams now, you know? I have a snippet, Neovim. So I just type something here, live stream with a guest, open terminal, or if it's a solo live stream, that's going to add my snippets. And I just fill it up and I have a key map that I execute. It schedules the live stream, sends emails to the guests, if there are guests, you know? Sacha: Yeah, I got that too. Linkarzu: You have that? So it's one of the things that I'm... Sacha: Do you have it also update your YouTube stream description through API? Linkarzu: Yeah, the title, the description is right here. Sacha: Yeah. What else do I have in mine? Mine, I have it do the time zone conversions because I really suck at time zone conversions. Linkarzu: I choose UTC and I tell guests, you know? UTC it is. Sacha: But I also like including the gazillion time zone translations in the description so that people don't have to do the UTC conversion themselves. So based on the timestamp for the event, I have this thing that allows me to pick up the time zone translations, because I use that for EmacsConf and then I just generalize it so I can use it for everything. That can go into the YouTube description. Then I have the same entry get turned into the blog post afterwards because I can publish that with a transcript and the screenshots as I showed you before. Linkarzu: Okay. Automation. It's all about automation. Sacha: It is. It absolutely is. Oh, and the thing that I have which also lets me copy the chat and then if I paste it in, then I can have it reformatted so that it looks nicer because otherwise when you paste it in, there's lots of spaces and stuff. But that's a fairly easy text transformation thing, so you should be able to do something like that just fine. Related:
You can comment on Mastodon or e-mail me at sacha@sachachua.com.
-1:-- Transcript for my chat with Linkarzu about the Neovim and Emacs communities, voice control, and other topics (Post Sacha Chua)--L0--C0--2026-11-01T14:03:33.000Z
I usually stick with some favorite font for a while, then, for some reason, the bug bites: some new activity, different hardware or work, and I want to change the default font.
I have this old file with content like this:
(set-face-attribute 'default nil :family "3270 Nerd Font Mono")
(set-face-attribute 'default nil :family "GoMono Nerd Font")
(set-face-attribute 'default nil :family "Hack Nerd Font Mono")
(set-face-attribute 'default nil :family "Iosevka Nerd Font Mono")
(set-face-attribute 'default nil :family "JetBrainsMono Nerd Font")
;; ... and a bunch more
What I do (shame on me) is put the cursor at the end of a line, C-x C-e, switch back to the buffer or tab I was working on, look around
for a while, switch back to the file, move down one line, C-x C-e
again... You get the idea. It works, but it's clunky, and changing the
size means editing the line and adding :height to it.
I wanted a way to scrub through my font list and increase or decrease the font height using only the arrow keys, while I check live what feels right.
That's a perfect job for a transient menu, which has shipped with Emacs since version 28, written by the one and only Jonas Bernoulli, author of nothing less than Magit.
This post is a walkthrough of how you can implement such functionality yourself, and hopefully a guide along your Elisp learning journey.
TL;DR: jump straight to the full source, put it in your init file, edit the font list, and run
M-x font-cycle.
First, we build a list of font families:
(defvar font-cycle-families
'("3270 Nerd Font Mono"
"GoMono Nerd Font"
"Hack Nerd Font Mono"
"Hurmit Nerd Font Mono"
"Inconsolata LGC Nerd Font"
"Iosevka Nerd Font Mono"
"IosevkaTerm Nerd Font Mono"
"JetBrainsMono Nerd Font"
"Lekton Nerd Font Mono"
"M+CodeLat50 Nerd Font"
"Maple Mono NF"
"MartianMono Nerd Font"
"SauceCodePro Nerd Font"
"UbuntuMono Nerd Font Mono"
"VictorMono Nerd Font Mono"
"ZedMono Nerd Font Mono")
"Font families to cycle through with `font-cycle'.")
If you don't know the exact family name of a font you have installed,
and how it is "perceived" by Emacs, you can use M-x describe-font
with completion, or M-: (font-family-list).
If a family on that list is not installed (a typo, another machine, a
font you uninstalled), set-face-attribute won't complain. Emacs just
falls back to some other font, and you end up judging the wrong
thing. So, before cycling, we keep only the families Emacs can
actually find:
(defun font-cycle--installed ()
"Return the members of `font-cycle-families' installed on this system."
(seq-filter (lambda (family) (find-font (font-spec :family family)))
font-cycle-families))
Cycling is an index and a mod. The mod is what makes it wrap
around: going right from the last font takes you to the first one,
and going left from the first takes you to the last.
(defvar font-cycle--index 0
"Position of the current font in the installed families.")
(defun font-cycle--select (index)
"Use the installed family at INDEX, wrapping around both ends."
(let ((fonts (font-cycle--installed)))
(unless fonts
(user-error "None of `font-cycle-families' is installed"))
(setq font-cycle--index (mod index (length fonts)))
(set-face-attribute 'default nil
:family (nth font-cycle--index fonts))))
(defun font-cycle-next ()
"Switch to the next font."
(interactive)
(font-cycle--select (1+ font-cycle--index)))
(defun font-cycle-previous ()
"Switch to the previous font."
(interactive)
(font-cycle--select (1- font-cycle--index)))
Note that mod in Elisp follows the sign of the divisor, so (mod -1 16) is 15, not -1. This is what we want for going left.
For the size, we change the :height of the default face. Its unit
is 1/10 of a point, so a step of 10 is one point:
(defun font-cycle--resize (delta)
"Change the default face height by DELTA (1/10 pt)."
(set-face-attribute 'default nil
:height (+ (face-attribute 'default :height) delta)))
(defun font-cycle-bigger ()
"Increase the font size by 1pt."
(interactive)
(font-cycle--resize 10))
(defun font-cycle-smaller ()
"Decrease the font size by 1pt."
(interactive)
(font-cycle--resize -10))
When I finally find a combination I like, I don't want to remember and retype it. So one more command builds the exact line for my init file and puts it in the kill ring for later yanking:
(defun font-cycle--form ()
"Return the `set-face-attribute' call for the current font."
(format "(set-face-attribute 'default nil :family %S :height %d)"
(face-attribute 'default :family)
(face-attribute 'default :height)))
(defun font-cycle-copy ()
"Copy the current font setting as Elisp to the kill ring."
(interactive)
(kill-new (font-cycle--form))
(message "Copied: %s" (current-kill 0)))
Then it's just a C-y in init.el.
Now let's glue everything together. Each suffix marked with
:transient t runs its command and keeps the menu open, so you can
press the arrows as many times as you want. y and q don't have it,
so they close the menu.
(transient-define-prefix font-cycle ()
"Cycle through `font-cycle-families' and adjust the font size."
[:description font-cycle--description
["Font"
("<right>" "next" font-cycle-next :transient t)
("<left>" "previous" font-cycle-previous :transient t)]
["Size"
("<up>" "bigger" font-cycle-bigger :transient t)
("<down>" "smaller" font-cycle-smaller :transient t)]
["Done"
("y" "copy as elisp" font-cycle-copy)
("q" "quit" transient-quit-one)]])
The :description of the outer group is a function, and transient
calls it again every time the menu is redrawn. That gives us a header
that always tells which font is on screen, its size, and where we are
in the list:
(defun font-cycle--description ()
"Header showing the current font, its size and position in the list."
(format "%s %dpt [%d/%d]"
(propertize (face-attribute 'default :family) 'face 'bold)
(/ (face-attribute 'default :height) 10)
(1+ font-cycle--index)
(length (font-cycle--installed))))
Run M-x font-cycle and you get something like this at the bottom of
the frame:
IosevkaTerm Nerd Font Mono 16pt [7/16]
Font Size Done
<right> next <up> bigger y copy as elisp
<left> previous <down> smaller q quitSince the menu lives at the bottom, everything above it is your real
work: the buffers you were editing, your theme, your splits. Tap
right, look, tap right again. If you like it, bump it up a point or
two. When you're done, y and paste.
If you want it one key away:
(keymap-global-set "C-c f" #'font-cycle)
;;; font-cycle.el --- Test-drive fonts with a transient menu -*- lexical-binding: t; -*-
(require 'transient)
(defvar font-cycle-families
'("3270 Nerd Font Mono"
"GoMono Nerd Font"
"Hack Nerd Font Mono"
"Hurmit Nerd Font Mono"
"Inconsolata LGC Nerd Font"
"Iosevka Nerd Font Mono"
"IosevkaTerm Nerd Font Mono"
"JetBrainsMono Nerd Font"
"Lekton Nerd Font Mono"
"M+CodeLat50 Nerd Font"
"Maple Mono NF"
"MartianMono Nerd Font"
"SauceCodePro Nerd Font"
"UbuntuMono Nerd Font Mono"
"VictorMono Nerd Font Mono"
"ZedMono Nerd Font Mono")
"Font families to cycle through with `font-cycle'.")
(defvar font-cycle--index 0
"Position of the current font in the installed families.")
(defun font-cycle--installed ()
"Return the members of `font-cycle-families' installed on this system."
(seq-filter (lambda (family) (find-font (font-spec :family family)))
font-cycle-families))
(defun font-cycle--select (index)
"Use the installed family at INDEX, wrapping around both ends."
(let ((fonts (font-cycle--installed)))
(unless fonts
(user-error "None of `font-cycle-families' is installed"))
(setq font-cycle--index (mod index (length fonts)))
(set-face-attribute 'default nil
:family (nth font-cycle--index fonts))))
(defun font-cycle-next ()
"Switch to the next font."
(interactive)
(font-cycle--select (1+ font-cycle--index)))
(defun font-cycle-previous ()
"Switch to the previous font."
(interactive)
(font-cycle--select (1- font-cycle--index)))
(defun font-cycle--resize (delta)
"Change the default face height by DELTA (1/10 pt)."
(set-face-attribute 'default nil
:height (+ (face-attribute 'default :height) delta)))
(defun font-cycle-bigger ()
"Increase the font size by 1pt."
(interactive)
(font-cycle--resize 10))
(defun font-cycle-smaller ()
"Decrease the font size by 1pt."
(interactive)
(font-cycle--resize -10))
(defun font-cycle--form ()
"Return the `set-face-attribute' call for the current font."
(format "(set-face-attribute 'default nil :family %S :height %d)"
(face-attribute 'default :family)
(face-attribute 'default :height)))
(defun font-cycle-copy ()
"Copy the current font setting as Elisp to the kill ring."
(interactive)
(kill-new (font-cycle--form))
(message "Copied: %s" (current-kill 0)))
(defun font-cycle--description ()
"Header showing the current font, its size and position in the list."
(format "%s %dpt [%d/%d]"
(propertize (face-attribute 'default :family) 'face 'bold)
(/ (face-attribute 'default :height) 10)
(1+ font-cycle--index)
(length (font-cycle--installed))))
(transient-define-prefix font-cycle ()
"Cycle through `font-cycle-families' and adjust the font size."
[:description font-cycle--description
["Font"
("<right>" "next" font-cycle-next :transient t)
("<left>" "previous" font-cycle-previous :transient t)]
["Size"
("<up>" "bigger" font-cycle-bigger :transient t)
("<down>" "smaller" font-cycle-smaller :transient t)]
["Done"
("y" "copy as elisp" font-cycle-copy)
("q" "quit" transient-quit-one)]])
(provide 'font-cycle)
;;; font-cycle.el ends here
A few extras, if you want to practice some Elisp:
mod with Emacs' built-in ring.el.variable-pitch face, the
same way.defcustoms, an autoload,
proper headers, and a clean checkdoc and byte-compile.Font picking seems to be in the air lately. If you'd rather use something ready-made, or just want to see other takes on the idea, check these out:
show-font-tabulated lists every font rendered in its
own typeface.There's nothing specifically clever here: a list, an index, a mod,
and a transient menu holding it all together.
I hope this educational post helps you with the basics of using transient, and maybe teaches you something new about Emacs Lisp along the way.
As for me, I'm currently on font 7 of 16. Ask me again next month.
-1:-- Test-Driving Fonts in Emacs with a Transient Menu (Post Rahul Juliato)--L0--C0--2026-10-31T23:00:00.000Z
I first tried mise while setting up Arch Linux on my Dell XPS 13.
I was bringing over my Doom Emacs configuration, which used asdf.el, and
considering how mise would fit into the same environment.
Colleagues had already spoken well of it. What interested me was the possibility of simplifying runtime integration in both Fish and Emacs. If that worked on the Dell, I was considering the same change on my Mac.
The useful test was inside the editor. I set up separate directories for Ruby 3.4 and 3.3, opened their Ruby files in Doom, and checked the version from each buffer. Each one used the Ruby selected for its directory. I also confirmed that Doom could find Node and npm, and that the Elixir tools were available.
That gave me a working setup on Arch before moving my main Mac environment away from asdf. The Mac work then extended into Fish configuration and the tools Emacs launches. The migration described here took place in October 2026; the Mac checks used mise 2026.10.21.
The starting point on the Mac was a set of projects with existing version files. Replacing the manager did not require changing every project’s Ruby or converting all those files to a new format.
Mise reads .tool-versions, the format I was already
using with asdf. For ecosystem-specific files such as .ruby-version and
.nvmrc, I enabled the corresponding tools in the global settings:
[settings]
idiomatic_version_file_enable_tools = ["ruby", "node"]
experimental = true
The experimental setting is used by the installed mise.el, which
checks it before enabling the integration.
My personal defaults now live in ~/.config/mise/config.toml. Its current
tool selections include changes made during the wider cleanup:
[tools]
elixir = "1.20.2-otp-29"
go = "1.27"
ruby = "4.0.9"
rust = "1.99.1"
Those are personal defaults. A project’s files can select another version.
During the migration checks in the blog repository, mise current ruby
reported 3.4.6. The project’s .tool-versions selected that version even
though its .ruby-version said 3.4.5.
The configuration rules determine how global and project
files combine. I checked the effective selection rather than assuming
that finding a familiar version file was enough. mise config ls lists
the configuration files being read, and mise current ruby reports the
selected Ruby.
I manage my dotfiles with GNU Stow. During the transition, the package
still named asdf retained .tool-versions, .default-gems and
.default-mix-commands. I later moved those files into the mise package
and recreated their Stow links. The move left the effective runtime
selections unchanged. That was configuration housekeeping, separate from
moving or installing runtimes.
The default-installation files were retained, but I have not validated that a fresh Ruby or Elixir installation consumes them automatically.
On Arch, the first mise doctor check reported that mise was not activated.
After setting up Fish activation, it reported no problem. Installing the
executable had been one step; arranging the shell environment was another.
On the Mac, mise is installed through Homebrew. I did not need to add an
activation line to my own config.fish: this installation supplies
/opt/homebrew/share/fish/vendor_conf.d/mise-activate.fish:
if [ "$MISE_FISH_AUTO_ACTIVATE" != "0" ]
/opt/homebrew/opt/mise/bin/mise activate fish | source
end
The Fish setup documentation describes this package-provided activation. I kept that mechanism instead of adding another copy to my personal configuration.
The surrounding Fish changes made the command paths explicit.
Personal paths moved into conf.d/00-environment.fish, which normalizes
the inherited PATH, removes duplicates and filters known obsolete
entries, including the old asdf shims. Asdf and older installations were
cleaned up as part of that work.
The prompt also needed to follow the new manager. My old override parsed
.tool-versions itself. Its replacement asks mise current once per render
and displays the languages relevant to the directory. Ruby, Node and
Elixir were joined by Rust, detected through files such as Cargo.toml.
The prompt now uses mise’s selection rather than maintaining its own
version-file parser.
Rustup remains in this setup: the mise Rust entry links to the existing
~/.cargo/bin directory. I added Rust to the same prompt segment rather
than adding another standalone version check.
The wider Fish review also removed the old Oh My Fish Emacs plugin and
kept the command helpers I use as autoloaded functions. That removed an
emacs --version call from shell initialization. I did not
measure a before-and-after startup time for it.
The Doom change had two parts. I replaced the asdf.el package declaration
with the recipe for mise.el in packages.el:
(package! mise :recipe (:host github :repo "eki3z/mise.el"))
In config.org, I replaced the asdf-enable setup with:
(use-package! mise
:config
(global-mise-mode +1))
On Arch, after synchronizing Doom, I confirmed that the mode was active and that Emacs could find the Ruby shim. I checked Ruby LSP and ElixirLS in that setup.
The installed revision of mise.el makes the mechanism visible.
It calls mise env --json, merges the result into a buffer-local
process-environment, and derives a buffer-local exec-path from the
resulting PATH.
Emacs uses exec-path to find a program named without an absolute path.
The process it starts inherits process-environment, including the PATH
used to find its own child commands. The Emacs process reference
describes both roles. That is why checking Ruby in a terminal alone could
not validate what a formatter or language server launched by Emacs used.
The package uses inheritenv around selected commands, including
shell-command-to-string, so they can carry the editing buffer’s
environment into temporary buffers.
Doom has its own environment snapshot as well. In this revision,
doom sync regenerates an existing snapshot;
doom sync --env also creates one if absent. The Emacs command
doom/reload-env only loads the saved file into the
running session. On Arch, doom doctor continued to warn that my PATH
had changed even while the runtime checks worked. I did not establish the
cause of that warning.
On the Mac, my Ruby formatter configuration also follows the project:
RuboCop when a .rubocop.yml is present, otherwise Standard. RuboCop uses
bundle exec when there is a Gemfile. Both commands clear inherited
Bundler-related variables before starting, and Standard uses the gems
available to the selected Ruby. The full configuration
contains the details; the important connection here is that a formatter
needs the project’s environment as well as its configuration file.
Some settings outside the runtime manager still named the old locations. My Arduino configuration was one of them:
(defvar bounga/arduino-language-server
- (expand-file-name "~/go/bin/arduino-language-server"))
+ (executable-find "arduino-language-server"))
The defvar initializer does not repeat the lookup each time I open an
Arduino buffer. Resolving it to a mise shim gives that saved value a stable
path while the executable lives under a particular Go installation.
Checking the running Emacs confirmed the shim path, and the server answered
its help command.
An installation problem with arduino-language-server and dockfmt led me
to check Go. Mise had Go 1.27.3 installed, but it was inactive in the directory
where I was working, so ordinary commands still found Homebrew’s Go 1.26.6.
mise exec go@1.27.3 -- go version used the intended installation, and both
tools built with it in a temporary directory. I then selected Go globally
and installed them.
mise install does not record a version selection;
mise use records one and installs the runtime if needed. I did not reproduce
the original installation error, so the inactive Go is an observed problem,
not a demonstrated cause of that failure.
This setup puts Go-installed executables in the selected Go version’s
bin directory through GOBIN. It changes where the tools live.
After adding an executable there, mise reshim makes it
available through the shim directory.
That mattered for dockerfmt, which Doom actually uses for Dockerfiles.
I had a copy installed with Homebrew, and Emacs kept finding it after I
installed dockerfmt with the mise Go. After mise reshim, executable-find
resolved to the mise shim. The earlier dockfmt build concerned a different
formatter. Doom’s Docker configuration uses dockerfmt,
although its doctor check still looks for dockfmt.
I made a separate storage change during the cleanup: removing the global
Bundler path setting that sent gems into vendor/bundle. Projects using
the same Ruby installation can now share installed gems.
Mise selects Ruby and, in this setup, its RubyGems directory. Each project’s
Gemfile and lockfile still select its dependencies. With Ruby 3.4.6,
Gem.dir matches Bundler.bundle_path under both Bundler 2.3.17 for the
blog and 4.0.17 for ctf_tracker.
That follows the Bundler policy, not a mise feature. The
configuration reference specifies Gem.dir as the default
before Bundler 5, and a repository-relative .bundle directory starting with
Bundler 5. A project can override path.
Seven active projects passed dependency and principal-library load checks
against the shared directories, with their Gemfiles, lockfiles and Ruby
selection files unchanged. After cleanup, ctf_tracker also passed its 86
examples. The global automatic-installation and cleanup settings were
removed, along with a production exclusion that excluded no dependencies
in those projects. My .gemrc now only disables documentation
and backtraces, with its Stow link preserved.
The 45 removed vendor/bundle trees had an allocated size of about 10.46 GiB
in the retained du -sk inventories. Gems were also installed in the shared
directories, so this is not a measured net saving. With those trees removed
from the checkouts, I re-enabled LSP file watchers. After restarting Emacs,
a large Rails project opened without a warning or noticeable slowness, an
observation limited to that project.
The two Ruby buffers on Arch were an early check that per-directory selection could work inside the editor. On the Mac, getting there also meant replacing old paths and checking the tools launched by the editor. The project dependency files could stay as they were while I changed the runtime manager and the environment around them.
-1:-- Leaving asdf for mise (Post Nicolas Cavigneaux)--L0--C0--2026-10-31T23:00:00.000Z
I recently wrote about revisiting my Doom configuration. I also got Emacs Everywhere working on my Mac: select text in a browser field, edit it in an Emacs frame, then send the result back.
A macOS setup shared in the project’s issue tracker uses Hammerspoon. I used Apple’s built-in Shortcuts app for the launcher and keyboard binding. My successful test was a GitHub comment field in Safari: the edited text returned to the field, without submitting the comment.
This is my tested Doom setup with Homebrew Emacs Plus 31, specifically Emacs 31.0.91. The launcher was small; getting it to find my running server and giving Emacs the right permission took more attention.
In ~/.config/doom/init.el, enable everywhere under :app in the existing
doom! block. Then synchronize Doom and restart Emacs:
~/.config/emacs/bin/doom sync
Emacs must already be running with its server active. Use
M-x server-start if needed. This launcher does not start Emacs for you.
The Doom module documentation describes the integration;
in the version I tested, doom +everywhere calls
emacsclient --eval '(emacs-everywhere)'.
Open Shortcuts, create a shortcut named Emacs Everywhere, and add a Run Shell Script action. Choose bash, leave the action’s input variable unset, and keep Run as Administrator off. With no input, the Pass Input setting does not matter.
Paste this script, adapting the absolute Doom path on the last line:
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export EMACS_SOCKET_NAME="$(/usr/bin/getconf DARWIN_USER_TEMP_DIR)emacs$(/usr/bin/id -u)/server"
/Users/nico/.config/emacs/bin/doom +everywhere
The locale exports addressed a warning I encountered with en_FR.UTF-8.
The socket export addressed a more confusing failure: the command worked
in Terminal, but Shortcuts reported that emacsclient could not find the
socket even though my server was running.
getconf DARWIN_USER_TEMP_DIR obtains my account’s temporary directory;
id -u supplies its numeric user ID. Together they locate the default
macOS socket without hardcoding a /var/folders/... path.
EMACS_SOCKET_NAME tells the client which socket to use.
If you customize server-name or server-socket-dir, adapt the expression.
I reproduced the lookup failure with a different TMPDIR and confirmed
that the explicit socket fixed it. I did not inspect Shortcuts’ own
environment, so that test does not establish its exact difference from
Terminal.
Open the shortcut’s Details panel (ⓘ), choose Add Keyboard Shortcut,
and press Control+Option+Command+E (⌃⌥⌘E). This combination worked in
my setup; check for conflicts in yours. Apple’s keyboard shortcut
instructions describe the same binding mechanism.

My macOS interface is in French. “Exécuter le script Shell” is Run Shell Script, and “Détails” is Details. The shortcut receives Quick Action input, but the shell action has no input variable: Emacs Everywhere handles the selected text itself.
In System Settings → Privacy & Security → Accessibility, add and enable the Emacs application you actually run. This is the macOS permission for controlling other applications.

Allow Emacs to control other applications in Privacy & Security → Accessibility. The screenshot shows the enabled entry in my French UI.
An enabled entry was not enough in my case. I removed it, added the current
Homebrew application again, and fully restarted Emacs. The tested app was
/opt/homebrew/Cellar/emacs-plus@31/31.0.91/Emacs.app.
Before that, I saw errors with codes -25211 and -1719, meaning that
osascript was not allowed assistive access. Re-adding Emacs resolved the
permission problem; I did not establish why the earlier entry failed.
A Homebrew upgrade may require granting this permission again. On my Mac,
/Applications/Emacs.app already links through
/opt/homebrew/opt/emacs-plus@31/Emacs.app, but Finder resolves it to the
versioned application when I add it. I would not rely on that symlink to
preserve permission. I upgrade Emacs infrequently and chose to re-add it
when necessary.
This was the missing step in my first attempt: select the source text. Putting the caret in the field was not enough. Without a selection, my installation imported the existing clipboard contents, including a path I had copied earlier.
For a first test:
Use C-c C-k to cancel without pasting back. In the installed version, an unchanged buffer is also treated as an aborted session, so opening and immediately closing it does not test the return path.
I also encountered this where the application or window name should have appeared:
script error -1758. Couldn't get error text because of error -1700.
The installed package compiled its helper scripts with
osacompile -r scpt:128 -t osas, producing legacy resource-fork scripts.
osadecompile rejected those files with
errOSADataFormatObsolete (-1758). Newly compiled data-fork scripts were
readable. Rewriting the existing file’s data fork alone did not repair it:
the obsolete resource fork remained. Replacing the file did.
The GitHub source checked on 1 November 2026 still uses that compilation command. Check your installed version before adding this workaround; it is not a prerequisite for every installation.
The local workaround in my Doom configurationI keep this in an emacs-lisp source block with :tangle yes in
~/.config/doom/config.org, which generates config.el. If you use a
literate configuration too, edit the Org source rather than the generated
file.
(after! emacs-everywhere
;; macOS rejects the legacy resource-fork AppleScripts generated upstream.
(defun +everywhere-modern-applescript-a (fn &rest args)
(let ((default-directory emacs-everywhere--dir))
(apply fn args)
(dolist (script '("app-name" "window-title" "window-geometry"))
(unless (zerop (call-process "osadecompile" nil nil nil script))
(let ((compiled (make-temp-file "everywhere-")))
(unwind-protect
(progn
(unless (zerop (call-process "osacompile" nil
"*everywhere compilation*" nil
"-d" "-o" compiled
(concat script ".applescript")))
(error "Could not compile Emacs Everywhere script: %s" script))
(delete-file script)
(rename-file compiled script))
(when (file-exists-p compiled)
(delete-file compiled))))))))
(advice-add 'emacs-everywhere--ensure-oscascript-compiled :around
#'+everywhere-modern-applescript-a))
The wrapper runs the package’s compilation function from its actual
directory, checks each helper with osadecompile, and replaces unreadable
files with freshly compiled data-fork versions. It also covers later
regeneration. The private function name is upstream’s spelling, including
oscascript; this advice may need adapting after a package update.
Forced regeneration and decompilation of all three repaired helpers worked in my tests. After fixing Accessibility permission, application and window queries worked too.
The useful check was the complete round trip in Safari. Once I selected
the field’s contents before launching, I could edit them in Emacs and
return them with C-c C-c. That is the sequence I would test first after
changing this setup.
-1:-- Emacs Everywhere on macOS with Shortcuts (Post Nicolas Cavigneaux)--L0--C0--2026-10-31T23:00:00.000Z
Three years ago, I wrote about marking the active window (update here ) so that it’s easy to pick it out from the other, inactive windows. I did that by making the mode line of the active window a distinct color instead of the slightly different shade of gray from the other windows. That’s worked out really well for me and I’ve never looked back.
Now Ray Zeitler over at Ray on Emacs takes another look at the problem and found a really nice solution. It’s a bit more in your face than mine so if you’re looking for something that makes the active window really stand out, you should take a look at his post. The TL;DR is that instead of changing the active window, he changes the background color of all the other windows.
Zeitler found his solution on a 10 year old Stack Exchange post so it’s pretty certain to work no matter what version of Emacs you’re using. The code is very simple and you can simply copy it to your init.el if you’re interested.
As I said, this solution seems a little aggressive to me so I doubt I’ll be implementing it but others may disagree. One thing for sure, it’s going to make your active window stand out. It’s a small enough change that you can try it out—say by copying it to your scratch buffer and executing it—to see if it’s something you’d like.
-1:-- Highlighting The Active Window (Post Irreal)--L0--C0--2026-10-31T14:42:26.000Z
Now you can use emacs.news to get to Emacs News. That should save some clicking/typing. It redirects to my blog category at the moment, so the URL doesn't show emacs.news after you get there, but I've added it to the note at the top of that category page to help people find out about the shortcut.
Thanks to @xenodium for the nudge!
You can e-mail me at sacha@sachachua.com.
-1:-- emacs.news (Post Sacha Chua)--L0--C0--2026-10-31T13:46:49.000Z
I stumbled across Andros Fenollosa's blog a while ago while thinking about doing more of my web browsing with EWW. He has written a post where he defines a standard for websites that read in the terminal he calls Termweb. It's well thought through and makes a lot of sense.
Eww isn't always a terminal browser, although it can be very useful in a TTY on a server to read up on the documentation while also using Emacs as a terminal multiplexer with config files you edit in another window and eshell, shell or ansi-term in a third window within the TTY. When I am in a graphical Wayland seesion, I use EWW in a GUI Emacsclient frame which means I can get different fonts and pictures as well as just text which you would get in EWW in Emacs in a terminal. (Of course, Lynx can show pictures in a graphical terminal as well through sixel and it is probably possible to hack EWW to use sixel like Lynx or use the abilities of newer terminals like Alacritty and Kitty to show pictures, but GUI Emacs has a few advantages more around keybindings, toolbars and the modeline as well as just pictures and fonts which is why most people prefer it when in a graphical session over terminal Emacs.)
I use the Emacs package org-static-blog to make my blog from org files. I also made a function to push changes to Codeberg pages via git. Org-static-blog already follows most of Fenollosa's advice. I additionally made a CSS stylesheet for graphical browsers when I started using it. I set a dark background, text that would contrast, and a few settings for the heading, footer and code blocks to stand out while still staying within a mostly monochromatic look. I think a coherent style, ie less CSS applied to more tags, IDs and classes, is better than tweaking everything individually.
Org-static-blog already makes HTML that renders well in a text-based browser and I chose to have my header on top, not on the side, when I made the CSS to make it look as similar as possible in a text-based browser as it looks in a graphical browser. Today I had a look at it again in EWW and I realised that the header and footer (or preamble and postamble as org-static-blog calls it) blends to much into the rest of the content of the page when read in a text based browser, so I added a horizontal line after the header and before the footer to separate it out. It looks good in EWW, but a bit unnecessary in a graphical browser. The obvious solution was to tweak the CSS to not show hr-tags inside the preamble and postamble. Then the header and footer look like they used to on graphical browsers, but they look better than before on text based browsers. After looking further at the header and footer, I felt that it did actually look quite stylish with the hr visible in my graphical browser as well, so I removed the extra CSS to remove it again.
My goal isn't to make my blog look good in text based browsers or graphical browsers, but both. Using a simple structure and following most of the rules by Andros Fenollosa helps to achieve both goals at once. If you have not had a look at your own site in EWW or Lynx and you have a bit of time to tweak some HTML or CSS, I recommend reading Fenollosa's post and have a go at tweaking things to follow most of his advice. Every reader you have that use EWW or a terminal text browser will thank you for making your site more readable.
-1:-- Adapting to text-based browser (Post Einar Mostad)--L0--C0--2026-10-31T11:22:00.000Z
This is my contribution to the October 2026 edition of the Emacs Blog Carnival. The theme is “Where and how will you be using emacs in the next year, two years, five years?”.
To step back and siuatate what we are talking about: Last year, I described Emacs as an Emancipatory Computing System: Emacs is a (1) uniform UI, (2) a transparent+accessible program and (3) a collaborative “community”. For me, these three attributes constitute the attractive and worthwhile aspects of Emacs. The fact that we got here is not a natural consequence of technical decisions. In fact, I’d argue that left to anarchy, Emacs would naturally devolve, but it is only due to the concious and intelligent mantainance of the core that the entropy of a system like Emacs can be counterbalanced.
Presently, I can see all these points being under threat in the coming years:
The uniform UI, meaning text-based UI that can be navigated and manupulated with the same commands, or at least custom commands that ensure intuitive consistency (i.e. C-k might not kill a physical line in a special-mode-derived buffer, but k still “kills” a logical entry) has been suffering for the past few years with custom UIs like transient and I remain weary of what the new Canvas functionality in the upcoming Emacs 32 release will spur people to implement. With the exception of edge-cases like games (which just like with the built-in games in Emacs are more demonstrations than actual features) all feature that were to use a Canvas image for user input will have to re-implement navigation and input in a intuitive way while dealing with the issue that “inuitive” means different thing to differnt people, depending on what subset of navigation and manipulation keybindings you are used to.
The transparency of Emacs has also been strained in the past few years: Initiatives such as Tree-Sitter in the core make it more difficult to make quick-paced changes to a language (which is the price you pay for improved and faster parsing) and even if you do change and re-build the grammar it is to my knowledge not possible to hot-swap a grammar without restarting the Emacs process since the grammar is packaged as shared objects.
Outside of the core there are some projects that again that also try to speed up Emacs by implementing functionality outside of Emacs. One such example is lsp-bridge or Emacs application framework where the main logic of the program is written in a seperate Python programs.
Of course it has always been the case that Emacs, as a kind of shell, integrated the surrounding system and invites the user to invoke shell commands both to interface with the outside system but also to manipulate the data within Emacs. I’d argue the difference with the previous examples is that we are not talking about already-existing generic and abstract tooling of our system like tr or make, but bespoke software that morally constitutes the same program but doesn’t enjoy the transparancy of both reading and changing the source code of the program as is the case with a traditional Elisp script.
Regarding collaborative community, it is worth pointing out that this is not necessarily just the online community consisnting mostly of a few dozen to perhaps a few hundred people. It might as well be a network of friends or colleges where knowledge and worked is shared. In fact I have more hope for the latter, as the former is strained both by the conflict of pro-LLM vs anti-LLM people (leaving many by the wayside) but also the annoyance of figures in the public space that are clearly motivated by the need to constantly self-advertise.
Specifically regarding LLMs, I think it is conservative to say that while LLMs can deal with the works on humans, it feels off for humans to deal with the output of an LLM. Their priorities, style and attention is plainly atypical for humans as at least currently it seems they share less of a need for abstraction and conciseness (which makes sense to me, as the current economic model of AI services is to sell tokens). This is all fine for a no-coder who generates scripts to implement features they desire, but for shared code where you cannot expect that no human won’t want to read and modify it themselves it becomes tricky – one option is not to care, but not caring about others is the detriment of a community.
So as for myself, and my relation to Emacs, it is of course difficult for me to imagine computing without it. In fact I get annoyed by a lot of other software, especially web services and mobile applications, due to my expectations. At the same time, I am aware that the compfort of Emacs has held me back from trying out software that lacked good support within Emacs — on some level I would be better off ignorant. Especially nowadays I find it more difficult than ever to predict the future of computing. I can anticipate that at my day-job (context: I am a PhD student in PL research working with Rocq) I will be continuting to use Emacs along with Proof General for the forseable future. But on a much larger and high-level scheme, the question of how we use computers in general and what role traditioanl software will play remains open to me.
(As an aside: The (software) complexity crisis has been worsening outside of specific, mostly autarchic circles (Emacs, while not immute, is has arguably stood it’s ground better) and the solution has never been to write more code faster — but let us also not forget that specifically with the LLM situation, code generation isn’t and shouldn’t be the end-game, instead I still belive that it is a marketing approach to sell the usage of generative AI in situation that can be well trained, so as to fund the continuted research and development — while also making a lot of money (the two are the cynical flipsides of each-other). Nevertheless I am entierly willing to concede that we might be dealing with a non-backwards compatible paradigm shift on how we compute, and that if we overcome the “LLM-generating-code” phase we could live in a future where the CPU becomes the co-processor to a LLM-centric user interface where something like Emacs wouldn’t be relevant anymore.)
If a space will continue to exist where something like Emacs is an adequate and useful tool to interface with computers, I am confinent that Emacs will continue to exist. But beyond that I hope that as both local communities and the global community we can come together in a shared understanding of what it is that we value and ensure that we do not undermine it or better yet forster them.
-1:-- On Whether Emacs Is Here To Stay? (Post Philip Kaludercic)--L0--C0--2026-10-31T00:44:03.000Z
Raw link: https://www.youtube.com/watch?v=_sUctSSXBhU
[ The stream will be recorded. You can watch it later. ]
This is a spontaneous live stream. I plan to develop at least one new theme for my ‘ef-themes’ package which will be in the spirit of Halloween. Depending on my progress, I will do two themes, actually.
I will keep the chat open, in case there are any questions related to this or anything else—I always answer everything.
ef-halloween-dark prototypeI made some more tweaks and published the dark theme. There is more to do, but this is a good start. The light theme needs more work. Maybe during another live stream.
commit 3a9898e23da62d14b57faeae1af9ae05bb3c59f3
Author: Protesilaos <info@protesilaos.com>
Date: Sat Oct 31 21:07:01 2026 +0200
Add PROTOTYPE of ef-halloween-dark
I did most of this during the live stream earlier today:
<https://protesilaos.com/codelog/2026-10-31-emacs-live-ef-themes-halloween/>.
ef-halloween-dark-theme.el | 242 +++++++++++++++++++++++++++++++++++++++++++++
ef-themes.el | 1 +
2 files changed, 243 insertions(+)
ef-halloween-light prototypeAnd this is from the day after.
commit dd0c2ee019e79d5541e1a3e563cd27c8a744c1ae
Author: Protesilaos <info@protesilaos.com>
Date: Sun Nov 1 07:51:01 2026 +0200
Add ef-halloween-light PROTOTYPE
contrasts.org | 36 +++++++
ef-halloween-light-theme.el | 242 ++++++++++++++++++++++++++++++++++++++++++++
ef-themes.el | 1 +
3 files changed, 279 insertions(+)
ef-themesThe ef-themes are a collection of light and dark themes for GNU
Emacs that provide colourful (“pretty”) yet legible options for users
who want something with a bit more flair than the modus-themes (also
designed by me).
Starting with version 2.0.0, the ef-themes are built on top of the
modus-themes. This means that all customisation options of the Modus
themes apply to the Ef themes. Same for all Modus commands that load a
theme. Enable ef-themes-take-over-modus-themes-mode to set up this
arrangement (or enable modus-themes-include-derivatives-mode instead
to blend Ef and Modus into one collection).
ef-themes🔷 Consider making a donation.
-1:-- Emacs live new Halloween theme for ‘ef-themes’ at 2026-10-31 14:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-10-31T00:00:00.000Z
I designed the basis of this theme during a live stream earlier today: https://protesilaos.com/codelog/2026-10-31-emacs-live-ef-themes-halloween/. Then I followed it up with refinements to colour values and palette mappings. The light counterpart of this theme will be the subject of further development work, maybe tomorrow or next week.
When I include “light” or “dark” in the theme’s name, it means that I have envisaged it as part of a pair. Most themes are not designed as pairs though, as certain colours cannot work in both cases.
Always click to enlarge the image for best results.
UPDATE 2026-11-02 09:42 +0200: Added ef-halloween-light as well.
I may still tweak it further, but the character is already set. It captures the style I had in mind and I am happy with the result.
Remember that since version 2.0.0, the ef-themes are built on top
of my modus-themes. This means that they are highly customisable,
support a wide range of packages and face groups, and are extensively
tested down to the finest details.
Enjoy!
The ef-themes are a collection of light and dark themes for GNU
Emacs that provide colourful (“pretty”) yet legible options for users
who want something with a bit more flair than the modus-themes (also
designed by me).
ef-themes🔷 Consider making a donation.
-1:-- Emacs: ef-halloween-dark is in ef-themes.git (Post Protesilaos)--L0--C0--2026-10-31T00:00:00.000Z
My Emacs printed this after starting:
Doom loaded 286 packages across 71 modules in 1.713s
Another machine running Arch, apparently less powerful, started Emacs in under a second. I wondered whether macOS was responsible.
I had no controlled comparison between the machines. Different builds, packages and caches could all matter. But I did have a configuration I could inspect, and it turned out to contain work I was asking Emacs to do before I needed it.
The startup investigation became part of a wider review of my
Doom configuration. I keep its source in config.org;
config.el is generated from it. Reading the prose beside each
customization made the review more useful: I could compare what I said I
wanted with what the installed code actually did.
The checks described below used Emacs 31.0.91. The source links point to the installed package revisions, rather than assuming a newer checkout would behave the same way.
The first useful finding was in Doom’s own reporting code. Its package count comes from this expression:
(- (length load-path) (length (get 'load-path 'initial-value)))
That counts added search-path entries. It does not mean that 286 packages
had been fully loaded. The time is Doom’s doom-init-time, measured from
Emacs’s before-init-time, rather than an external stopwatch covering the
whole application launch. The implementation makes both
definitions explicit.
All times below use Doom’s timer. Temporary graphical instances ran my configuration with instrumentation in a temporary directory; my main session stayed open. Four dedicated reference starts reported 1.489, 1.397, 1.303 and 1.264 seconds, in that order. Another start of the original configuration, recorded while setting up the functional check, reported 1.618 seconds. These repeats did not reproduce the initial 1.713-second observation; I did not establish why that launch was slower.
Native compilation was available. The recorded reference starts had no garbage collections during the measured interval, and zero recorded GC time. Doom already defers collection during interactive startup, so raising a GC threshold again would not address the work I was seeing.
To locate that work, I timed load, require and process calls in
separate instrumented runs. Those timings add overhead, so I used them
to identify candidates, keeping the simpler reference runs for the
startup comparisons.
One require stood out: claude-code-ide, the package connecting Claude
Code to Emacs. One instrumented run attributed about 467 ms to it,
including its dependencies.
The package entry point requires its MCP integration.
Following that into the handlers reaches diagnostics,
which in turn attempts to load Flycheck. My timings
showed those loads happening during startup. The time attributed to
claude-code-ide already included the nested work; adding each dependency’s
time to it would count the same work more than once.
I wanted the integration available from my leader menu. I did not need it loaded while Emacs was opening an unrelated buffer.
The durable change was one line:
(use-package! claude-code-ide
+ :defer t
:init
With :defer t, use-package skips adding the package to its implicit
:load list, so the declaration no longer requires it
at startup. That does not, by itself, create a way to load it later.
Here, all nine commands I had bound had autoloads, including the menu
command. Calling one could load the library containing its definition.
The :defer handler distinguishes this from a numeric
:defer, which schedules a load after an idle delay.
I kept the keybindings and my Ghostel backend selection in :init.
They need to exist before the package loads. The call to
claude-code-ide-emacs-tools-setup stayed in :config, to register the
Emacs tools after loading.
That arrangement needed checking in a fresh instance. A package already loaded in my main session could hide a missing autoload. The check confirmed that the library was absent at startup, all nine bindings were available, and invoking the menu loaded the package. The Emacs MCP tools were registered and the backend was still Ghostel. This did not exercise a complete conversation with the Claude CLI.
The startup measurements were encouraging:
| Configuration | Starts | Doom-reported time |
|---|---|---|
| Original, dedicated reference runs | 4 | 1.264 to 1.489 s |
| Original, functional-check setup | 1 | 1.618 s |
| Temporary variant deferring Claude | 2 | 0.863 and 0.903 s |
| Real configuration, menu probes | 2 | 0.767 and 0.799 s |
| Real configuration, final checks | 2 | 0.768 and 0.873 s |
The menu probes failed their functional assertions while the check was
being developed. Their startup times still belong in the record:
doom-init-time was set before those assertions ran. The later successful
check establishes the behavior described above; a short startup time on
its own would not establish that the integration still worked.
These are local observations from the review on 30 October 2026, with a small number of trials and no controlled cold-cache protocol. The table is not a promise that this edit always saves the difference between the fastest and slowest entries. It was enough evidence to keep the change, alongside the functional check.
The cost has also moved: the first use of the integration now has loading work to do. I did not measure that first-use latency separately.
Mise was another visible source of startup work, with 16 to 23 synchronous calls in the instrumented runs. Disabling it on its own produced one measurement of 1.267 seconds, within the dedicated reference range. Combining that with deferred Claude loading produced 0.781 seconds, also within the range reached by the deferral alone. Neither single trial established a repeatable benefit from disabling mise. I kept it enabled; only Claude’s loading was durably deferred in this startup experiment.
My configuration could now start in under a second on this Mac. That answered the practical question that sent me looking, even though the comparison with Arch remained uncontrolled.
Startup had an obvious number attached to it. Some other customizations needed a different check: opening the kind of buffer I actually use.
In 2020, I wrote about binding M-RET to insert an Elixir pipe.
I still want that shortcut. The function moves to the end of the line,
inserts a new line and adds |> . But my customization targeted
elixir-mode, while my enabled Doom module also uses elixir-ts-mode.
The current hook covers both:
(add-hook! '(elixir-mode-hook elixir-ts-mode-hook)
(local-set-key
(kbd "M-RET")
#'bounga/insert-elixir-pipe-operator))
I checked it in an Elixir Tree-sitter buffer by invoking the binding:
foo became foo followed by a newline and |> . The Elixir docset was
also available there. On the Ruby side, checks in both ruby-mode and
ruby-ts-mode confirmed the Ruby/Rails docsets and subword-mode;
M-f stopped after My in MyLongClass.
Finding the hook names in the file would not have told me that. The customization can look perfectly familiar while missing the buffer where I expect to use it.
I also removed an explicit elixir-format hook before saving. My Doom
configuration enables (format +onsave +lsp). The installed
format module enables Apheleia on first-file use, and
Apheleia maps both Elixir modes to mix-format, with
Doom also supporting formatting through an available LSP formatter.
The old hook was a separate formatting path that I no longer needed.
In an isolated directory with Elixir 1.20.2 and OTP 29, saving a buffer
containing foo( 1,2 ) through Apheleia produced foo(1, 2) in both modes,
in the buffer and on disk. No LSP server was involved, so this exercised
Apheleia’s mix-format path.
A smaller naming error showed the same need to inspect the actual
integration. I had written after! tramp-mode. Doom’s after! waits for
a feature to load; the relevant feature is tramp. Changing it to
after! tramp made the remote-path customization run in the targeted
check. A familiar mode name was the wrong name for that job.
The mail changes deserve more care because I had already published my automatic signing and encryption setup. That article described the policy I wanted, but the implementation needed correction.
The old send hook chose between signing and signing plus encryption without first checking for an existing MML security instruction. It could replace a choice I had made explicitly while composing the message.
The current policy is:
| Message state | Automatic action |
|---|---|
| An explicit MML security instruction is present | Leave it alone |
| No explicit instruction, and every recipient has a usable encryption key | Request signing and encryption |
| No explicit instruction, and the key check fails | Request signing only |
The hook now looks for an existing secure tag in the message body before applying its default. Tests reproduce the earlier replacement of explicit signing, encryption and combined instructions, then confirm that the new hook preserves each one.
Reading Emacs’s own message helpers exposed another
duplication. The body of my recipient parser matched
message-all-recipients. My availability predicate followed the built-in
message-all-epg-keys-available-p, including its epg-list-keys check.
Giving those functions my own namespace had not given me different
behavior.
Emacs also provides message-sign-encrypt-if-all-keys-available, but it
does nothing when the key check fails. My policy includes signing in
that case. I kept my own hook for that fallback and for preserving an
explicit choice, replaced the parser with message-all-recipients, and
switched the key check to mml-secure-find-usable-keys for encryption.
Having a key in a search result is insufficient. The MML implementation checks the relevant user ID and encryption capability, excluding expired or revoked candidates and disabled keys. An empty recipient list now fails my predicate as well.
This still does not establish that a key belongs to the person I mean, or replace the trust policy used when encrypting. Availability, usability and trust are separate questions. The hook requests an action; it cannot guarantee that signing or encryption will succeed when the message is sent.
The checks used simulated keys, covering To, Cc and Bcc, missing
keys and unusable candidates. Across the two targeted suites, ten of
fifteen tests failed against the earlier functions; all fifteen passed
against the current definitions extracted from config.org. No real
keyring was read and no mail was sent. The suites test the decisions in
my code; they do not exercise an end-to-end encrypted exchange.
The terminal setting needed no replacement. I removed the old vterm-shell
assignment after moving to Ghostel. I did not replace it with an explicit
ghostel-shell: its default reads SHELL, with /bin/sh
as a fallback, and selects the shell I want.
I also made the fullscreen request explicit with fullboth instead of
toggling it. I had not reproduced a fullscreen failure; the edit made the
requested state explicit. It would be misleading to count it alongside
the mail bug as one more thing that had been broken.
The distinction matters to how I read the resulting diff. Some lines fix observed behavior. Others follow a change in what I run, such as replacing vterm with Ghostel, or state a preference more clearly. Doom was doing useful work, including deferring GC and arranging formatting, that I needed to understand before adding more.
I can explain why Claude waits until first use, and I have checks for the editing and mail behavior I mean to keep. Those checks are what I want to rerun when the next package update makes a familiar customization worth revisiting.
-1:-- Revisiting My Doom Emacs Configuration (Post Nicolas Cavigneaux)--L0--C0--2026-10-30T23:00:00.000Z
-1:-- terminal-colors.el - Make your Emacs terminal colors look like a real terminal (vterm, eat, term, and ansi-color) (Post James Cherti)--L0--C0--2026-10-30T19:34:15.000Z
When I'm "on the road," Vakana1 becomes my human interface to Emacs.
I use Vakana because its presence on my Android device affords me these three conveniences:
My computer is portable, and it does have a microphone. But its ability to recognize speech is poor despite multiple training sessions.2 As a workaround, I'd dictate an email message on my cellphone, save it as a draft, and then access it from my computer.
As speech interfaces, my Android email clients are clunky. But the real issue with them is that they distract me with unread messages. It's like working in the office on a Monday morning where chatty coworkers talk about how well their team played, or the latest attempt at a ceasefire, or the ever-increasing price of … everything. I needed a tool devoted to ideation. So I tried Vakana.
After a one-time setup with a "blog notes" thread, I simply create a new note and start dictating right away. It's like working late on a Friday evening with only unobtrusive air handlers as coworkers.
Dictation isn't perfect -- I resist the urge to edit except in cases where I misspeak and want to choose a different word or phrase. I focus primarily on capturing ideas; creating content is secondary.
When I'm ready, I'll export either to HTML or Org. Either way, all the thread's notes are combined into a single file. The HTML export produces a single HTML file with bead images included with an SVG tag. The Org export creates a ZIP file with several Org files, one for each note, plus SVG files. Then I'll attach the file to an email message, which I save as draft and open on my computer.
With the exception of Org export, this has nothing to do with Emacs.
When I wonder how I might operate Emacs with voice, I vaguely envision "voice-bindings" -- a software layer that converts a spoken command such as "Visit File" into its associated function. The subsequent input at the Find file prompt would be managed with a voice-controlled dired package, perhaps called diredv. And, by-the-way, it doesn't need to be a monotonic voice; it can be a melody where pitch might fill in for the value of a prefix argument. Rhythm might denote yet another aspect of a command or input. I laugh when I think of what might happen to someone with hiccups.3
Pursuit of this is not on my agenda.
I imagine that evolution of enhanced Emacs input would be driven by our community's most brilliant contributors. I'm thinking of the ones who use elaborate hardware devices tailored to their anatomy and workflow. Their devices resemble split keyboards. Sometimes modifier keys are positioned under the thumbs to minimize pinkie strain. You might've seen examples if you read r/emacs.4 The evolution from that might be gloves used in conjunction with a virtual keyboard.
Personally, I distrust specialized hardware. I've had many peripherals become obsolete due to the lack of an OS-compatible driver. As well, I don't over-customize Emacs. I think it's important that I can use an Emacs -Q invocation if necessary.
I'm sure that the FOSS community would ensure that drivers for a specialized Emacs input device would be free, even if only as source code. However, the real impediment would be getting employers to allow such specialized and low-level code to be installed on their systems. And I can't imagine anyone investing in a solution that can't be used for most of the day.
1 Learn about Vakana here.
2 The speech recognition training probably requires a registered, logged in account, which I refuse to create.
3 This reminds me of the motion-activated radio on board the Heart of Gold. You don't need to get up to change the channels or mute it. On the other hand, you have to sit awfully still if you don't want to change it inadvertently. Obviously the engineer who came up with that idea wasn't too fond of dancing.
4 The Emacs subreddit can be found here: https://www.reddit.com/r/emacs/
• This is my contribution to the October 2026 Emacs Carnival.
• One might jokingly refer to "The Future of Human Interface for Emacs" with the acronym "FUHIE," self-deprecatingly pronounced "Phooey."
-1:-- The Future of Human Interface for Emacs (Post Raymond Zeitler)--L0--C0--2026-10-30T18:22:22.798Z
I like forums. Debates, questions, answers, opinions, etc. That feeling of an asynchronous community, where answers are thought through and content lasts, is what makes the internet an interesting place. That is why I like reading them and taking part in them. But I had never designed or built one.
Maybe you have come across BBJ at some point, a bulletin board for the terminal built for small communities (tildes, pubnixes, club servers, etc.). I love its spirit. However, its foundations, architecture-wise, could be better. What would a modern bulletin board look like? In my spare time I started sketching one, and I ended up building my humble headless, federated forum, with simple, modern technologies and open standards.
As always, what matters is what you learn along the way. A forum looks simple, but it hides the classic problems of any server. Who can do what? How do you tell a moderator from a regular user? What happens when you have to ban someone? How do you stop someone from stealing an account? Roles, permissions, bans and security. I had to solve them one by one.
Let me tell you how I made Tyrtulia, but what I really want to explain are my decisions.
Of course, before starting I looked at what was already out there. There are very complete free software forums:
Or the good old classic: phpBB.
They are all fantastic, but they are designed to be used from a web browser. They also have an ambitious scope. That is ideal for large communities, but too much for a small, light forum.
The first decision was to build no interface. The server only offers a solid, documented API. It is REST with hypermedia: the client only knows one route, GET /, and discovers everything else by following links. And I banned myself from inventing my own formats. Underneath there are only some very boring standards:
ETag and If-Match so edits don't overwrite each other.PATCH.There is no web interface, no forms, no advanced authentication systems, no file handling (images, attachments, etc.), and nothing that isn't strictly needed to read and write messages. Everything I left out was code I would never have to maintain.
That is why it is so light. It uses between 60 and 70 MB of RAM and about 50 MB of disk. It only needs Python and SQLite to run.
How does a client know what it can do? The server tells it with links. Look at the same request, GET /, made by a guest (simplified):
{
"name": "Forum of example.org",
"_links": {
"self": {"href": "/", "method": "GET"},
"latest": {"href": "/threads", "method": "GET"},
"search": {"href": "/search{?q,author,in}", "method": "GET", "templated": true},
"login": {"href": "/sessions", "method": "POST", "schema": "/schemas/session"}
}
}
And now by alice, logged in:
{
"name": "Forum of example.org",
"_links": {
"self": {"href": "/", "method": "GET"},
"latest": {"href": "/threads", "method": "GET"},
"search": {"href": "/search{?q,author,in}", "method": "GET", "templated": true},
"createThread": {"href": "/threads", "method": "POST", "schema": "/schemas/thread"},
"notifications": {"href": "/notifications", "method": "GET"},
"events": {"href": "/events", "method": "GET", "type": "text/event-stream"}
}
}
If I change a route tomorrow, no client breaks, because none of them knew it in advance. They just follow the links the server gives them.
Along the way, this solved one of the problems I mentioned at the start: permissions. The client never asks whether you are a moderator. If you can edit, an edit link appears. If the thread is closed, reply disappears. And so I wouldn't cheat myself, Tyrtulia Web has a test that fails if I write an API route in the code.
No OAuth, no OpenID, no sign-up forms. There are two ways to authenticate:
There is no registration over HTTP. If you have a Unix user on the server, you already have an account: it is created the first time you connect through the socket. From there you can add your SSH keys to log in from outside. If you don't have a user on the machine, the administrator creates an account for you with your public key from the command line. On a tilde, having a Unix user is already the invitation.
A security detail you might find interesting: the challenge.
What would happen if the client signed the challenge and nothing else? Imagine you use the same SSH key on two forums, A and B, and A is malicious. A asks B for a challenge, pretending to be you, and sends it to you as if it were its own. You sign it without suspecting anything, and A uses your signature to log in to B with your account. It's a classic relay attack.
That is why the forum doesn't send the bare challenge, but a message that also carries its address and your username:
tyrtulia login
origin: https://forum.example.org
username: alice
challenge: 3q2-7wXbG0yTuFq8aN1sKkz9Jm5vQe4Lr6Hc2Pd0Tbw
Before signing, the client checks that origin is the forum it is talking to. If it doesn't match, it doesn't sign. That way a signature is only valid for one forum, one user and one challenge.
It's the same idea behind passkeys (WebAuthn): the signature is bound to the domain, so it's useless anywhere else.
And if someone has to be banned, their sessions are revoked and their keys are blocked.
Easy to use, secure, and no passwords to steal.
Before writing any code, I studied BBJ in depth. I wanted to know what to keep and what to change. Some things struck me. The password travels on every request as an unsalted SHA-256 hash, so in practice the hash is the password. And notifications have been pending since 2017.
First, the foundations.
| BBJ | Tyrtulia | |
|---|---|---|
| API style | RPC: everything is POST /api/<endpoint> |
REST with hypermedia (HAL): only GET / is known |
| Permissions | The client asks (is_admin, edit_query) |
Links that appear or disappear |
| Authentication | Unsalted SHA-256 hash of the password on every request | No passwords: Unix socket or SSH-signed challenge |
| Errors | 6 custom numeric codes | RFC 9457 with a catalog of types |
| Pagination | Everything at once | Cursor based |
| Concurrency | The last edit wins, silently | ETag and If-Match |
| Text format | Custom language ([bold: ...], [red: ...]) |
Standard Markdown |
| Validation | A db_validate endpoint |
JSON Schema linked from each form |
| Configuration | config.json |
TOML |
Then, the features I missed.
| BBJ | Tyrtulia | |
|---|---|---|
| Reply to a specific post | >>N quotes in the text |
Linked replies, with a tree view |
| Mentions | No | Yes |
| Notifications | No (pending since 2017) | Inbox with replies, mentions and alerts |
| Real time | No, you have to poll | Server-Sent Events, with resume |
| Edit history | No, editing overwrites | Full, with diffs and revert |
| Deletion | Deletes the row | Tombstone: numbering is preserved |
| Search | Each client searches what it has downloaded | Server-side full text, accent and case insensitive |
| Feeds | No | Atom for the forum and each thread |
| Moderation | Admin or not | Roles, hidden posts, reports and a public log |
| Rate limiting | No | Per user and per IP |
And what I left untouched, because it is what makes BBJ special:
A headless forum is useless without clients, so I had to write them.
The first and most natural one was for the terminal.
Tyrtulia TUI is the terminal client, built with Textual.
:quality(85)/https://andros.dev/media/blog/2026/10/tyrtulia-tui-thread.png)
If you know me a little, you know this package was a must: tyrtulia.el.
:quality(85)/https://andros.dev/media/blog/2026/10/tyrtulia-el-thread.png)
Tyrtulia Web is more a mirror of the content than a client. Its goal is to let search engines index the forum and let anyone read it from a browser.
:quality(85)/https://andros.dev/media/blog/2026/10/tyrtulia-web-thread.png)
It has an unstyled theme and another one that mimics my website.
You can connect to one forum or set up several. Federation happens in the clients: servers don't talk to each other or copy messages between them. Each forum is independent, with its own users, rules and moderators.
Instead of splitting them into tabs or accounts, the clients mix the threads of all of them into a single list.
:quality(85)/https://andros.dev/media/blog/2026/10/tyrtulia-el-forums.png)
Of course, you can only write in the forums where you have an account. In the rest you can only read.
I think it's a pretty cool feature, because it keeps the API code much simpler and lets you explore other forums at zero cost.
In short, it has been fun.
I also found out that it works for more than a forum. A thread can be a wiki page with its history, a pinned notice for every user on the server or an article in a group blog. And since a cron script can post through the socket with no credentials, you get a wall that doesn't get lost and can be searched.
Of course there are more complete forums, more powerful federation systems, and even decentralized systems that leave Tyrtulia in the dust (Aether, Nostr...). But its niche is very specific: a light, closed-access, federated, indexable, secure forum with text clients. For everything else, there are better things out there.
If you feel like trying it, the installation guide is in the Tyrtulia repository.
I look forward to your feedback.
Help me keep writing Every coffee gives me a push toward the next article. Sure, it's on me!
Send an email to comment+article-b5c8c6aa@andros.dev to leave a comment. The subject will be ignored.
-1:-- I built a forum with no interface, no passwords, made for the terminal (Post Andros Fenollosa)--L0--C0--2026-10-30T11:26:35.000Z
Raw link: https://www.youtube.com/watch?v=VgL7usTlpsc
[ The stream will be recorded. You can watch it later. ]
This is a spontaneous live stream that will start in ~35 minutes. I am
going to work on my denote package for Emacs. The plan is to review
some functionality and improve certain edge cases where things can go
wrong. This is the sort of work that improves the quality of a
package: it does not introduce anything with a “wow” factor, but it
contributes to a nicer day-to-day experience.
I will keep the chat open, in case there are any questions related to this or anything else—I always answer everything.
Denote is a simple note-taking tool for Emacs. It is based on the idea that notes should follow a predictable and descriptive file-naming scheme. The file name must offer a clear indication of what the note is about, without reference to any other metadata. Denote basically streamlines the creation of such files while providing facilities to link between them.
Denote’s file-naming scheme is not limited to “notes”. It can be used for all types of file, including those that are not editable in Emacs, such as videos. Naming files in a consistent way makes their filtering and retrieval considerably easier. Denote provides relevant facilities to rename files, regardless of file type.
[ Further down on this list I include more of my Denote-related packages. ]
denote🔷 Consider making a donation.
-1:-- Emacs live refinements for Denote at 2026-10-30 14:45 Europe/Athens (Post Protesilaos)--L0--C0--2026-10-30T00:00:00.000Z
In https://emacs.stackexchange.com/a/36240/47342, CodyChan provides
a few options "to better distinguish between focused window." But
the one I like best is highlight-selected-window, which, rather than
highlighting the selected window, changes the background color of
all the unselected windows. The effect is that the active window
stands out in comparison to the others.
The especially neat thing about this solution is that when the minibuffer is active, the background of all the windows is altered. So it's obvious even when Emacs is expecting input -- something I often miss.
I've changed the color of the buffer-face-set background from "#111"
(sienna4) to "antiquewhite2" because I still need to read the
content of inactive windows. The original color is too dark.
I've also adopted flash-active-buffer, which sets the
background color of the active window temporarily. I changed the
color to palegreen just because. I use s-? as a
keybinding. It's a bit awkward, but it's as if I use "?" to ask,
"Where am I?" The function highlight-selected-window
works so well that I don't think I'll need flash-active-buffer.
The color names that Emacs understands are stored in
color-name-rgb-alist.
See also https://www.color-hex.com/color-names.html, but note that Emacs does not support every color name listed here.
-1:-- Highlight Active Emacs Window (Post Raymond Zeitler)--L0--C0--2026-10-29T22:03:48.513Z
-1:-- proxymacs.el - An Emacs package for configuring HTTP, HTTPS, and SOCKS proxies with optional authentication (Post James Cherti)--L0--C0--2026-10-29T20:26:41.000Z
linkarzu wins the clip-off thanks to his fancy AI-powered clipping process! Here's a snippet from our conversation today. He'll be posting more clips, and I'm also going to edit the transcript of the conversation. Fun fun fun!
Additional notes:
(add-hook 'substitute-post-replace-functions #'sacha-substitute-save-common-edits) to save the changes. sacha-substitute-save-common-edits
Edit text has functions like sacha-subed-fix-common-errors-in-text that apply those changes to my speech recognition results and sacha-subed-fix-common-errors-from-start that apply them to already-prepared transcripts. I really like how I can mark a region and use that to define the substitution, like changing "tug a" to "toggle".
Sacha: It says Orgsley Revived. What is that even? But I can press Ctrl+; and it uses substitute.el.
Sacha: Orgzly Revived here. And what that does, in addition to the substitutions that happen throughout the buffer, so for example, if I'm adding a transcript, it will change that all throughout the transcript. I've also added some code because this is Emacs, I can just do that. that saves this in common errors or commonly misrecognized words. And then when I recognize text, whether it's before a voice command or if I'm just doing this or whatever, it will apply all of those common fixes. Common edits.
Sacha: Here we go. So it automatically adds it to this thing here, which gets saved automatically for me. And that's the change that I've just added. Linkarzu: Interesting. Pretty cool. Sacha: So it's really nice to have that kind of [dictionary] shortcut built into the way that I edit things. All I know is I just hit Ctrl+; and put in what it's supposed to be. And then it gets not only fixed in my current file, but in the future as well. Linkarzu: Yeah, I need to work on my transcript game.
Linkarzu: What other tricks do you want to show us there? What other things do you want to share about, brag about your Emacs workflow? Sacha: It's really hard to say. I told you how M-x doctor was the beginning of my experience, right? Linkarzu: Your journey, yep. Sacha: Yes, yes. So this is the doctor. You can say things like, "I'm feeling happy today," as I said, right? And then it uses that to generate its response. But I have this conversation mode. "Yes, especially when I get to tinker with Emacs." And then, uh... "What happened when you get to tinker with Emacs?" Did that...? Does the TTS come through? Yeah, it does. And the reason I did this was because after I showed off my vocal commands to Emacs Paris, they were like, oh, you know, what if you can have a conversation? So I was like, OK, let's come back full circle to the thing that got me curious about Emacs in the first place, and voice enable that. And then, of course, this is a very old AI. It's much more interesting if you apply it to the new models. So if I want to, for example, practice French. This is one of my voice commands here. Then I can... Doctor: "What would happen? I would appreciate it if you would continue." Sacha: Okay, here we are. Then I might say, "Je parle avec linkarzu." And then I think... There you go. It will eventually... Maybe? Thank you.
Gemini: "Ah, c'est super de discuter avec lui ! C'est toujours enrichissant de partager (to share) ses passions en direct. Quel sujet abordez-vous ensemble aujourd'hui?" Sacha: Alright, so this now uses a large language model. This one uses Gemini because gptel makes it super easy to plug in different models. So if I wanted to practice French, but I don't necessarily want to bug people because I take a while to think about my words and sometimes I'm looking stuff up, then at least I can simulate that kind of conversation. And there are a gazillion apps that will sell you a subscription to do this. But because it's here and it's plain text and I can control it, then I can more easily go through it afterwards and analyze the vocabulary or correct the things that it didn't get right, which I know is because of the speech recognition and not because of me or whatever, right? So it's so much fun to be able to tinker with Emacs to do what I wanted to do.
Sacha: Here I'm in live mode so my public tasks don't get mixed in with my private tasks. If I do an agenda, it should just be limited to the public stuff. Anyway, so the remind me created the note for me. It includes the audio in case the speech recognition didn't get anything right. And it includes a screenshot so that in case I'm doing this from a different application because my keyboard shortcut is global, I can still see the context and find that sort of stuff. So yeah.
Sacha: So this one here, for example, it says, remind me next month, et cetera, et cetera. And it has scheduled it for next month. Linkarzu: That's pretty cool. Sacha: I'm not actually going to pressure you to switch the Emacs. You can keep using Neovim and just borrow all the interesting ideas. org.nvim is there, I can adapt it to do this, you know?He hasn't released a clip of this one yet, but this exchange was one of my favourite parts of the main conversation:
[Linkarzu]: I don't know if like that's a particularity of Neovim users, we're all toxic and we just hate each other.
[Sacha]: Yeah, maybe you should just come over to the Emacs community. We have hot chocolate.
[Linkarzu]: If you go to the Emacs subreddit, it's like rainbows and butterflies. There's not a lot of hate. I always wonder why does that happen? It's not moderators, you know, it's just people. Is it just because it's old people? Like the professor, he doesn't want to fight any more. He's old enough. He retired already. He's just ready to pass to the next life. So what, what?
[Sacha]: Hypothesis number two, maybe your editor makes you grumpy.
That's at about 01:30:06 in this unlisted embed of the full conversation:
You can e-mail me at sacha@sachachua.com.
-1:-- Clip: Emacs voice control - describing my setup during Linkarzu's livestream (Post Sacha Chua)--L0--C0--2026-10-29T19:53:50.000Z
Over at the MIT Technology Review there’s a nice article on Emacs turning 50 this year. It gives a short history of Emacs starting from its TECO days. Most of that history is pretty well known to seasoned Emacs users but it’s still interesting.
What the article doesn’t give is the exact day of Emacs’ 50th birthday. That’s not surprising. The early history of Emacs involved the researchers at the MIT Artificial Intelligence Laboratory agreeing on a standard set of TECO macros in order to improve collaboration. That was undoubtedly an ongoing process that didn’t have a well defined beginning or end so it’s easy to understand why the exact birthday of Emacs is a little murky.
Regardless, we can all celebrate Emacs’ 50th year. It’s remarkable when you think about it. Very few programs last that long. There’s FORTRAN and Lisp, of course, but I can’t think of any other piece of software with that sort of longevity. There is, to be sure, a Ship of Theseus aspect to that longevity but it’s one shared by FORTRAN and Lisp as well.
Who knows what things will look like in another 50 years but if we still have editors, I’m putting my money on Emacs still being around. Wouldn’t that be something?
-1:-- Emacs Turns 50 (Post Irreal)--L0--C0--2026-10-29T15:19:40.000Z
Raw link: https://www.youtube.com/watch?v=JnDcXPzFsfA
[ The stream will be recorded. You can watch it later. ]
This is a spontaneous live stream that starts in ~10 minutes. I will
do some work on my ef-themes package for Emacs. I want to tweak the
color values of all themes in order to achieve inter-theme consistency
as I noticed some cases that are problematic.
I will keep the chat open, in case there are any questions related to this or anything else—I always answer everything.
(defun prot/ef-themes-match-p (file)
(string-match-p "ef-.*-theme\\.el" file))
(defun prot/ef-themes-get-theme-files ()
(thread-last
(directory-files "/home/prot/Git/Projects/ef-themes/" :full)
(seq-filter #'prot/ef-themes-match-p)))
(defun prot/ef-themes-add-bg-diff-fringe (files)
"Add bg-DIFF-fringe to each theme."
(dolist (file files)
(with-current-buffer (find-file-noselect file)
(goto-char (point-min))
(dolist (scope '("added" "changed" "removed"))
(when (re-search-forward (format "\\(?:(bg-%s-refine[\s\t]+\\)\"\\(#[A-Fa-f0-9]+\\)\"" scope) nil t)
(let* ((color (match-string-no-properties 1))
(percentage (if (modus-themes-color-dark-p color)
10
-10))
(fringe-value (modus-themes-adjust-value color percentage)))
(goto-char (line-end-position))
(insert (format "\n(bg-%s-fringe %S)" scope fringe-value)))))
(indent-region (point-min) (point-max))
(save-buffer))))
(prot/ef-themes-add-bg-diff-fringe (prot/ef-themes-get-theme-files))
(defun prot/ef-themes-get-name-from-file (file)
(thread-last
(file-name-nondirectory file)
(replace-regexp-in-string "-theme\\.el" "")))
(defun prot/ef-themes-add-diff-contrast-tables (files)
(dolist (file files)
(let ((theme-name (prot/ef-themes-get-name-from-file file)))
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(let ((foreground-colour nil)
(tables nil))
(dolist (scope '("added" "changed" "removed"))
(let ((background-colours nil))
(dolist (specifier '("" "-faint" "-refine"))
(when (re-search-forward (format "\\(?:(bg-%s%s[\s\t]+\\)\"\\(#[A-Fa-f0-9]+\\)\"" scope specifier) nil t)
(push (match-string-no-properties 1) background-colours)))
(setq background-colours (nreverse background-colours))
(when (re-search-forward (format "\\(?:(fg-%s[\s\t]+\\)\"\\(#[A-Fa-f0-9]+\\)\"" scope) nil t)
(setq foreground-colour (match-string-no-properties 1)))
(push
(concat (format "| | %s | %s | %s |"
(nth 0 background-colours)
(nth 1 background-colours)
(nth 2 background-colours))
"\n"
"|-"
"\n"
(format "| %s |" foreground-colour)
"\n"
"#+TBLFM: $2='(Λ $1 @1);%.2f :: $3='(Λ $1 @1);%.2f :: $4='(Λ $1 @1);%.2f"
"\n")
tables)))
(setq table (nreverse tables))
(with-current-buffer (find-file-noselect "/home/prot/Git/Projects/ef-themes/contrast-ratios.org")
(goto-char (point-min))
(when (re-search-forward (format "^\\* %s" theme-name))
(call-interactively #'org-narrow-to-subtree)
(goto-char (point-max))
(insert "** Diff colours")
(insert "\n\n")
(dolist (table tables)
(insert table)
(insert "\n"))
(widen))))))))
(prot/ef-themes-add-diff-contrast-tables
(prot/ef-themes-get-theme-files))
ef-themesThe ef-themes are a collection of light and dark themes for GNU
Emacs that provide colourful (“pretty”) yet legible options for users
who want something with a bit more flair than the modus-themes (also
designed by me).
Starting with version 2.0.0, the ef-themes are built on top of the
modus-themes. This means that all customisation options of the Modus
themes apply to the Ef themes. Same for all Modus commands that load a
theme. Enable ef-themes-take-over-modus-themes-mode to set up this
arrangement (or enable modus-themes-include-derivatives-mode instead
to blend Ef and Modus into one collection).
ef-themes🔷 Consider making a donation.
-1:-- Emacs live refining ‘ef-themes’ Diff/Magit colours at 2026-10-29 18:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-10-29T00:00:00.000Z
Marcin Borkowski (mbork) has an interesting post on Unicode punctuation in Emacs. His post is mainly concerned with the vagaries of dealing with English and Polish punctuation in the same document but a lot of the heavy lifting is accomplished with Electric Quote Mode.
The main problem is handling single and double quotes ‘like this’ and “like this”. Because I do almost all my writing in Org mode, I use the ':t option to turn the ASCII double quote (") into their curved typeset equivalents on export. That’s supposed to do the same with single quotes but I could never get it to work for me and always had to add then manually.
Fortunately, it’s easy to make everything work right in the buffer as you type. You merely turn electric-quote-mode on and it takes care of everything for you. By default it acts the way TeX used to by requiring “ for an opening double quote and ” for the closing double quote and similarly for single quotes. You can make it accept ” and ‘ instead by setting electric-quote-replace-double and electric-quote-context-sensitive to t.
Electric Quote Mode is global so turning it on enables it for all your buffers. You can use electric-quote-local-mode if you want it only in the current buffer. You can toggle either of them on or off by simply calling Meta+x electric-quote....
If you have to deal different quote notation for two or more languages, take a look at mbork’s post for a nifty solution.
-1:-- Electric Quote Mode (Post Irreal)--L0--C0--2026-10-28T14:50:34.000Z
I make lots of little EmacsConf schedule updates based on the speakers' feedback.
I needed a way to keep track of the schedule that I've just sent the speaker so that I could calculate the differences and notify them again the next time I change the schedule for their talk.
I like the way that keeping track of this lets me include a summary of the change in the subject and in the body of the message. For example, one of the emails I sent today had the subject:
This blog post is about updating the EMAILED_SCHEDULE entry property in my Org Mode file that keeps track of this year's conference talks so that I can save the schedule that I've emailed. I want to set it only after I've successfully sent the email, because I might change my mind or forget to send the e-mail, and I want to make sure the entry property is based on what I've e-mailed.
EmacsConf 2026: Schedule update as of 2026-10-28: svg: Sun ~2:20 PM EST (40 min earlier)
I send the messages within Emacs, of course. This lets me use emacsconf-mail.el to mail-merge using Emacs Lisp. I can then use message-sent-hook to run buffer-specific code once I've sent the message. This lets me set it up so that each message updates its own talk once I've used C-c C-c to send it.
First, I need a function that adds to a talk's logbook.
This is not specifically needed for the message-sent-hook, but it's useful for keeping track of the fact that I've sent this kind of email to that speaker on this date.
It's very helpful to be able to see who I've already sent the backstage information to or when was the last time I sent them a schedule update.
I can also filter the list of talks by the text in the logs so that if I partially send a mail merge and then cancel the rest because I want to tweak the code, I can send the new mail merge only to the remaining speakers.
This is in emacsconf.el.
emacsconf-add-to-talk-logbook: Add NOTE as a logbook entry for TALK.
(defun emacsconf-add-to-talk-logbook (talk note) "Add NOTE as a logbook entry for TALK." (interactive (list (emacsconf-complete-talk) (read-string "Note: "))) (save-window-excursion (save-excursion (emacsconf-with-talk-heading talk (emacsconf-add-to-logbook (if (functionp note) (funcall note) note)) (bury-buffer (current-buffer))))))
This function is the one that gets added to the message sent hook. It's in emacsconf-mail.el:
emacsconf-mail-add-to-logbook: Add to logbook for all matching talks from this speaker.
(defun emacsconf-mail-add-to-logbook (email note) "Add to logbook for all matching talks from this speaker." (interactive (let* ((email (mail-strip-quoted-names (plist-get (plist-get (notmuch-show-get-message-properties) :headers) :From))) (talks (emacsconf-mail-talks email))) (list email (read-string (format "Note for %s: " (mapconcat (lambda (o) (plist-get o :slug)) talks", ")))))) (condition-case e (save-window-excursion (mapc (lambda (talk) (emacsconf-add-to-talk-logbook talk note)) (emacsconf-mail-talks email))) (error (message "Error %s" e))))
And this function actually sets up the message-sent-hook:
emacsconf-mail-log-message-when-sent
(defun emacsconf-mail-log-message-when-sent (o message) (add-hook 'message-sent-hook `(lambda () (save-window-excursion (emacsconf-add-to-talk-logbook ,(plist-get o :slug) ,message)) (when (match-buffers "*unsent") (switch-to-buffer (car (match-buffers "*unsent"))))) nil t))
Which gets called by this part of emacsconf-mail-prepare:
(when (plist-get template :log-note)
(mapc (lambda (talk)
(emacsconf-mail-log-message-when-sent talk (plist-get template :log-note)))
(emacsconf-mail-talks email)))
Then my emacsconf-mail-interim-schedule-update can specify the :log-note like this:
:log-note
(lambda ()
(dolist (talk (cdr group))
(emacsconf-with-talk-heading talk
(emacsconf-schedule-save-emailed-time)))
(concat "emacsconf-mail-interim-schedule-update"
(if log-note (concat ": " log-note) "") " - " summary))
This lets me run that code only when the mail gets sent.
Next automation idea: Someday I may even figure out how to attach an iCalendar file (including updates to events) so that people can add it to their calendars.
If you find yourself wanting to run code when a message gets sent, then definitely take a look at message-sent-hook.
You can add a buffer-local hook with the fourth argument to add-hook (ex: (add-hook 'message-sent-hook (lambda () ...) nil t)) so that you can take actions specific to that message.
Have fun!
You can e-mail me at sacha@sachachua.com.
-1:-- EmacsConf: Using message-sent-hook to run buffer-specific code to keep track of e-mailed schedules (Post Sacha Chua)--L0--C0--2026-10-28T14:13:35.000Z
Today I learned a new Emacs trick from Emacs Dwelling. Well, new to me. It turns out it’s been in Emacs since 28.1 so for about four and a half years.
Here’s the problem. When you call isearch it starts searching at the current point location to the bottom of the file. You can cause it to wrap around and continue searching at the top of the file but this isn’t always convenient. As a result, many people always start their search by moving to the top of the buffer first. But this isn’t always convenient either. For example, if you quit the search you go back to the top of the file, not where you started.
Sometimes you want a bit more control: perhaps you want to start at the first match or the last match and if you quit, you’d like to be back where you started. Well this is Emacs so of course you can do that. The nice thing is that it’s already built in. There are no packages to install, you just enable it my setting isearch-allow-motion to t.
You can start the search at any of:
Take a look at the Emacs Dwelling post to see the details and how to invoke the extra behavior. I don’t know why this hasn’t become the default. If you don’t use the extra capabilities, everything works exactly as before.
-1:-- Controlling Where Isearch Starts Searching (Post Irreal)--L0--C0--2026-10-27T14:28:18.000Z
-1:-- Unicode punctuation in Emacs (Post Marcin Borkowski)--L0--C0--2026-10-26T19:12:43.000Z
Sacha Chua has a nice post that describes something that I really like: taking some data that she’s collected and analysing it to extract useful information. For some reason I really like that sort of thing and am always doing it myself with my own data.
In Chua’s case, she wanted to know how many Emacs News entries she’s written and plot them over time. It’s a little more difficult than you might think. First of all, she didn’t have the complete Emacs News index so she had to recreate the missing parts by converting the HTML entries into Org mode. She did that with a standard Unix pipeline using Pandoc.
Once she had everything in Org mode, she wrote some Elisp to extract the needed information and put it in a table. From there, it was easy to produce a graph showing how may entries she wrote by year. You can even see the years her daughter was a toddler because the number of entries went down for a couple of years. As Chua says, “Babies sleep a lot; toddlers, not so much.”
Take a look at Chua’s post to see all the code and the final graph. The data is mostly useful only for Chua just as mine is useful mostly for me but the way she goes about analyzing the data is useful to anyone that needs to take some data and massage it.
-1:-- Analyzing Emacs News (Post Irreal)--L0--C0--2026-10-26T14:39:49.000Z
The coolest thing I saw this week is Clerkenwell, a handwritten Clojure notebook (Github).
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-10-26 Emacs news (Post Sacha Chua)--L0--C0--2026-10-26T13:26:36.000Z
I have a small ritual when I need to find something in a file: jump to the top with M-<, start C-s, and type the search string. It works, but it is a little fiddly. The top of the buffer is not where I am, and most of the time I do not actually need to start there. I just want the first match in the file, or I want to search forward from wherever point happens to be.
The built-in isearch-allow-motion option is kinda useful. It lets the ordinary motion keys M-< and M-> work as match navigation while an isearch is active. Type C-s, type the search string, and then press M-< to jump to the first occurrence in the buffer or M-> to jump to the last. The search stays active, so C-s and C-r keep working from the new position. No trip to the top, no restarting the search.
With isearch-allow-motion set to non-nil, isearch intercepts four motion commands and gives them a search-specific meaning:
| Key | Command | Default during isearch | With isearch-allow-motion |
|---|---|---|---|
M-< |
beginning-of-buffer |
exits isearch, goes to buffer start | jumps to first match in buffer |
M-> |
end-of-buffer |
exits isearch, goes to buffer end | jumps to last match in buffer |
C-v |
scroll-up-command |
exits isearch, scrolls forward | jumps to first match after window |
M-v |
scroll-down-command |
exits isearch, scrolls backward | jumps to last match before window |
The search string is not changed, and you are still in isearch after the jump. That is the whole trick: the motion command has been repurposed from "move the buffer" to "move through the matches", but only while you are searching.
The option has been in Emacs since 28.1, so I would say generally there is probably a high chance you already have it.
Add one line in your init file:
(setq isearch-allow-motion t)
The top-of-buffer ritual was not a huge cost, but it was a repeated one. I do this dozens of times a day, and every time I did it I was leaving the place I wanted to search from, just to make the first match the first match. isearch-allow-motion lets me keep my starting point and still get the first or last match when I need it.
It is a small, built-in option, not a new package or a new search interface. It just teaches a few familiar keys to do the obvious thing while isearch is active. If you are on Emacs 28.1 or later, give it a try!
-1:-- Stop Going to the Top: isearch-allow-motion in Emacs (Post James Dyer)--L0--C0--2026-10-26T13:25:00.000Z
-1:-- Emacs Carnival: Are keyboards and M-x here to stay? (Post Eric MacAdie)--L0--C0--2026-10-26T03:17:55.000Z
I used to work as a freelancer. And of course I did my bookkeeping in Emacs. One task was noting down the time I spent on each project each day.
I can already hear you saying: "Sure thing, use org-clock!" But org-clock was more machinery than I needed. Especially because I wasn't always in Emacs (yes, sad, but true) when I worked for my clients. Sometimes I was running workshops or coaching sessions away from the keyboard.
I just had an org-file with a simple list like this:
+ [ ] [2026-10-21 Wed] project A; 0,5 h; status meeting + [ ] [2026-10-22 Thu] project B; 1,5 h; coaching
At the end of each month, I would turn this list into invoices for my different clients. And since every client had their own specific template and invoicing requirements, I didn't bother to set up an automated workflow in Emacs. I just had this list and a little macro that converted it into CSV and I handled the rest manually. (Maybe today, with the power of LLMs, I would try to vibe-code some automation.)
Now, to prepare an invoice, I only needed the lines for that particular client. I did know about the wonderful function flush-lines. It just removes lines matching a regexp. I was already wondering how to negate my regexp used for flush-lines … then I thought: "Maybe there's a sibling function for this purpose?" And behold, Emacs had me covered: M-x keep-lines exists and does exactly what I wanted.
This might sound too small to bother mentioning. And maybe my workflow makes you cringe. But that's that two things that I love about Emacs: You can do things your very own way. And sometimes, improving your workflow doesn't take a package or a custom function. Just finding the command that's already there.
And that is so satisfying about living in Emacs!
Contact: emacs@tutanota.de
Text and images: © 2026 Badore, CC BY-SA 4.0. Code examples written for this blog: © 2026 Badore, GPL-3.0-or-later. Code from third-party projects remains under its original license and is attributed in the relevant post.
-1:-- M-x keep-lines (Post Badore)--L0--C0--2026-10-25T23:00:00.000Z
Martin Schell has an excellent post on building and using Org capture templates. What’s interesting about his approach is that his template prompts for certain information—customer ID and site location in his example—captures that information in some local variables, and uses those variables to customize the capture for the particular customer.
His two main tools for doing this are:
%^{PROMPT|A|B|...} placeholder to prompt for data.%(EXP) placeholder to execute the Elisp expression EXP.
He wrote some helper functions to deal with setting and accessing local variables. The first, ms/set-var, prompts the user for the value of a variable, creates the variable, sets its value, and returns the value. This is different from the %^{...} placeholder in that it captures the value of the user’s answer for later use. The other helper functions make use of the captured variables.
Take a look at Schell’s post to see the functions and how the whole thing hangs together. His needs may be more complicated than normal but the ideas he uses to solve them are handy and applicable to many—even simpler—situations.
I’ve used all the placeholders that uses in my own templates but the idea of capturing a variable for later reuse in the template is new to me. It’s a neat idea and definitely worth knowing.
-1:-- Variables In Org Capture (Post Irreal)--L0--C0--2026-10-25T14:39:20.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 consider the best “default
theme” for a text editor, opt for my modus-themes.
doric-themesBelow are the release notes.
doric-meadow and doric-mountain themesdoric-meadow is a light theme with yellow and green colours, while
doric-mountain has a similar albeit dark palette. They expand the
collection of the Doric themes to cover even more styles and
preferences.
I improved the inter-theme consistency of all colours that apply to
diffs, such as those shown in diff-mode, ediff, and magit buffers.
Before, some themes were needlessly more subtle than others while
certain colours could be hard to discern in some situations. For
example, if the diff-hl package was in use and the fringe was set to
a thin line.
Other packages also benefit from this review, such as my pulsar and
lin.
The highlight of matching parentheses produced by show-paren-mode is
now distinct from the active region. The same is true for the current
line, per hl-line-mode.
doric-themes-define-theme should not fail in certain casesI fixed an issue that was reported to me by Amin Bandali where the
doric-themes-define-theme was failing to instantiate a theme in the
macroexpanded code.
bg-accent and fg-accent palette entries are renamedThis is relevant for those who rely on doric-themes-with-colors. The
new names are bg-accent-primary and fg-accent-primary. The old
ones will not longer work.
🔷 Consider making a donation.
-1:-- Emacs: doric-themes version 1.4.0 (Post Protesilaos)--L0--C0--2026-10-25T00:00:00.000Z
Hello everyone,
I’m Badore, and I’ve been living in Emacs for over 25 years. That does sound like a long time, but there have been plenty of twists and turns along the way, and the learning never stops.
A few years ago, though, I started reading Planet Emacslife, and it feels like someone hit the turbo button on my Emacs journey. So many brilliant ideas and solutions land in my feed that I find myself increasingly humbled. And I wonder how much I really know about Emacs after all. A huge thank you to Sacha Chua for making this possible—and to everyone who so generously shares their knowledge and experience!
Now I’ve decided to share some of what I’ve learned and perhaps give something back. I’ll start by describing some of the parts of my setup that I find most useful, along with a few little tips and tricks that surprised me by actually working. But this won’t be a book or a lecture about Emacs. Just a few ideas here and there, and stories about my daily life in Emacs from time to time. I hope you’ll find it useful!
Contact: emacs@tutanota.de
Text and images: © 2026 Badore, CC BY-SA 4.0. Code examples written for this blog: © 2026 Badore, GPL-3.0-or-later. Code from third-party projects remains under its original license and is attributed in the relevant post.
-1:-- (message "hello world") (Post Badore)--L0--C0--2026-10-24T22:00:00.000Z
When you hover the mouse pointer over a link in Org, Emacs displays a tooltip that shows the link's URL.0 This mimics the behavior of a web browser that displays the URL in the status bar.
Within Emacs, I try to avoid the mouse as much as possible. I already configured Emacs so that pressing ENTER on a link follows the link.1 But this method doesn't provide a way to preview the link before following it. I wondered, "What if Emacs displayed a tooltip whenever point is on a link?"
The answer came to me immediately; "It would be darn annoying."
So I decided that the user should invoke a function to "display tooltip at point." It didn't take long for me to find a solution on Stack Exchange, "farynaio/org-link-copy."2 The function's primary purpose is to add the URL to the kill-ring. But it also shows the URL in a message that indicates the URL was copied. It works perfectly.
However, the tricky part was binding a key to the function. It was
suggested that one could bind C-x C-l3 to the
function with this statement:
(define-key org-mode-map (kbd "C-x C-l") 'farynaio/org-link-copy)
Unfortunately, when I included it in my init file, it generated the error "Symbol's value as variable is void: org-mode-map" even while it worked after Org was enabled.
Eventually, after trying (unsuccessfully) to add the assignment to
org-mode-hook, I found it easy to include this statement above the
define-key statement: (require 'org-keys).
This provides a definition of org-mode-map, which is provided
in org-keys.el.
As Emacs is very keyboard-oriented, this little addition allowed my fingers (and thumbs) to stay on the keys.
Recall that my original objective was to display a tooltip, not a message. However, a tooltip would appear at the location of the mouse pointer, not at point, even if the mouse is hovering over another program's window. Just try it yourself:
(tooltip-show-help "MSG")
But this disadvantageous behavior of tooltip-show-help
could be useful if you wanted to locate the mouse pointer or signal
the status of some long-running asynchronous Emacs process. Setting
the message
to "****\n****\n****\n****\n****\n****\n****\n****" might
make the mouse obvious enough to notice. Feel free to play with that!
Trying to locate the mouse pointer leads me to another issue: how to locate point. So far, I've been using my overwrite-mode indicator to show me point.4 But I hope to devise a more effective solution soon. Perhaps.
0 If you don't see this, you might need to enable tooltip-mode.
1 Setting the variable org-return-follows-link to non-nil achieves
this.
2
See https://emacs.stackexchange.com/a/60555. The function is org-link-copy.
3 I chose to bind the function to "s-SPC".
-1:-- A Point-Based Tooltip for Emacs (Post Raymond Zeitler)--L0--C0--2026-10-24T21:28:21.479Z
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!