Marcin Borkowski: Emacs Lock mode
-1:-- Emacs Lock mode (Post Marcin Borkowski)--L0--C0--2026-10-05T19:26:14.000Z
-1:-- Emacs Lock mode (Post Marcin Borkowski)--L0--C0--2026-10-05T19:26:14.000Z
A long-standing passion of mine is listening to and collecting music. I’m always on the lookout for new music and where I discover it is primarily from internet radio stations. That said, listening to music in such a way is largely a passive activity, where your main focus is somewhere else. The better music players that acknowledge this difference in focus provide mechanisms for remote-controlling them from other apps. So it is with the Triode app for macOS, which leverages Cocoa frameworks (App Intents in particular) to support remote control.
triode.el is an Emacs Lisp library which provides a simple remote control interface to the Triode app. Basic support for selecting a radio station, play/stop, and muting are provided. Not every feature in the Triode app is accessible to triode.el though; this is controlled by the App Intents actually implemented by its developer, The Iconfactory.

Interested readers can learn more about triode.el in its user guide.
triode.el is now available on MELPA. This release joins my other packages that support remote control of music apps on macOS from Emacs:
All the above efforts have been an exploration of malleable computing using Emacs as the main orchestration tool. I plan on posting a more technical deep dive reflecting this work in the not too distant future. That said, if you’re a macOS user who uses Emacs and Triode, I encourage you to give triode.el a try. Also, consider this a plug for Triode app, a well-designed app that does a great job of taking advantage of the Cocoa frameworks to provide internet radio across different devices provided by Apple.
-1:-- Announcing triode.el (Post Charles Choi)--L0--C0--2026-10-05T17:50:00.000Z
Chris Maiorana asks a provocative question: What’s the future of Emacs? He asks that question in the context of assuming that Emacs will continue to exist for any timeline that matters to us. But how, he asks, will we use it?
The answer that will occur to most of us is speech mediated input á la Star Trek TNG. We’ll simply tell Emacs what we want it to do and it will do it. But think about that for a moment. Would you really want to dictate the code for your latest program to the computer? Or would you even want to dictate your documentation, story, or letter to your computer?
Sure, you could say something like, “Computer write a program to do such and such.” but then you’re not using Emacs at all. The same for asking the computer to write something for you. That—more or less—is here today but most of us don’t like the results and, again, we’re not really using Emacs in those situations.
For my part, my written thoughts are always much better organized than my spoken ones so even with capable voice input, I’d much prefer to use the keyboard. Maiorana agrees. He points back to a couple of episodes from Tales From the Darkside1 in which people use the keyboard to do magic with the computer. For him, Meta+x is the spell that enables the magic and he’s very attached to it. Like me, he doesn’t anticipate abandoning the keyboard.
What do you think? Will we still use the keyboard or will we move on to something better?
You can find Mookie and Pookie and Word Processor of the Gods in the Internet Archive.
-1:-- The Future Of Emacs (Post Irreal)--L0--C0--2026-10-05T15:11:13.000Z
Version 1.4.0 of the doric-themes, which is currently in
development, will include two new themes that draw inspiration from
natural settings.
The Doric project is all about restraining the use of colour and being deliberate about the application of typographic styles. The result is a minimalist aesthetic that can make it easier to focus on the contents of the Emacs buffer.
Below are samples of the two themes. For accurate results, click to enlarge the image.
The Doric themes use few colours and will appear monochromatic in many contexts. They are my most minimalist themes. Styles involve the careful use of typographic features and subtleties in colour gradients to establish a consistent rhythm.
If you want maximalist themes in terms of colour, check my ef-themes
package. For something in-between, which I would consider the best
“default theme” for a text editor, opt for my modus-themes.
doric-themes-1:-- Emacs: ‘doric-meadow’ and ‘doric-mountain’ coming to the ‘doric-themes’ (Post Protesilaos)--L0--C0--2026-10-05T00:00:00.000Z
Since moving to Zola as my static site generator, I have had a nice setup for my website. However, a recent update to zola (v0.22 -> v0.23) broke my website (it apparently had something to do with the Tera templating engine that zola uses). This forced me to finally make to move to switching to orgmode as my SSG.
My ultimate goal has always been to build this website purely with
emacs and orgmode[1], and I'm glad now that's the case. Full
disclosure however, I did take the help of LLMs to implement this over
the weekend. There are still some rough edges to the site, which is
why I have this build on a different branch (instead of main) on my
website's source repository.
The website is generated via org-publish with the following directory structure for the source code:
.
├── LICENSE
├── Makefile
├── README.org
├── content/
├── lisp/
├── publish.el
├── static/
├── statichost.yml
├── style.css
└── templates/
5 directories, 6 files
The key component is publish.el which exports all of my blogposts (which
are orgmode files now) to html files. I also have the htmlize.el
file hosted on the git repo for syntax highlighting.
Feel free to peruse the source code of this build if you're interested in the technical details. At its current state, I'm quite happy with how it has turned out. A nice little addition (which is the default behaviour of org-publish) is that I now have a footer entry mentioning the emacs and orgmode version used to build this website!
As is the trend to move all of my computing needs to emacs. ↩
-1:-- This website is now built with Org mode (Post Ashish Panigrahi)--L0--C0--2026-10-05T00:00:00.000Z
How will the future affect Emacs? How will it affect the keyboard itself? Will we speak commands to be interpreted by language models? Will we direct a swarm of bots to do our bidding like an over-zealous Howard Hughes with a megaphone? Or, are keyboards and M-x here to stay?
I’ve been thinking back a lot recently on a few of my favorite episodes of Tales From the Darkside, a horror anthology series that ran from 1983 to 1988, a recurring favorite on the Sci-fi channel, later collected on DVD, and probably streaming somewhere today. Two episodes from the first season are particularly memorable for me: “Mookie and Pookie” and “Word Processor of the Gods.” If you’re an Emacs user and haven’t seen these two great episodes of television, you have some homework to do. Google up, or do whatever it is you do when you need to search for something these days, and find these. At the very least, you will recognize the source of the background image I’ve used in countless YouTube video thumbails.
In “Mookie and Pookie” (original air date 11 November 1984) a terminally (pun not lost on anyone) ill young man uploads his consciousness into a computer, leaving behind a special set of printed instructions for his sister, Pookie (Justine Bateman), on how to invoke his spirit in “the network” after he is gone.
Hold that thought in memory for just a moment.
In “Word Processor of the Gods,” (original air date 25 November 1984) based on a short story by Stephen King, a man receives a homebrew computer as a birthday gift from his recently deceased nephew. This special computer gives the user the ability to create, edit, and delete aspects of his own life—basically the power to author reality.

Is any of this reminding you of Emacs yet?
These tales represent the hope and anxiety that people had about computers in the 1980s. Those magical boxes were either angels bringing benefits to mankind or devils that would lead to our doom. With the benefit of hindsight, we can see that computers brought a mixed bag of benefits and drawbacks, but no one wants to go back to a time before we had them.
I feel quite privileged that I got to see the birth of the first person shooter, the Internet, the smartphone, and “AI” (jury still being out on whether or not language models are really “artificial intelligence”), and have lived long enough to tell about it.
But we are drifting here from the thrust of this article, which is basically to pontificate on the question of “How will the future affect Emacs?”
Let’s go back to “Mookie and Pookie” and the image of teenage Justine Bateman typing out programs by hand, on a keyboard, in that episode of Tales From the Darkside. At the start of the episode, the girl who hates computers is nervously typing one command after another, trying to get a feel for this mysterious box. But soon, she’s become a hacker, accessing data banks and interfacing with the soul of her dead brother. We can see she’s not just typing commands but learning magic.

Nothing so closely matches the experience I had learning Emacs. Except, of course, that I was a thirty-year-old man, not a teenage girl.
Nevertheless, “Mookie,” like “Word Processor of The Gods,” is about using the keyboard as a conveyance for invoking magic.
Fast forward to my discovery of Emacs. Using the help system was like opening a book of magic spells. The keyboard was my wand, artist’s brush, sculptor’s trowel, chef’s saute pan, or whatever artistic/magical metaphor you prefer. This magic is achieved by invoking M-x.
Invoking M-x is the most sensuous and gratifying experience in all of computing.
That simple key command opens a whole world of possibilities.
There’s nothing quite like it. Even though I can set a custom key-binding for a frequently used function, I often still prefer not, because M-x is my silver key.
But this is how we use Emacs today. Now, let’s get to the brave new world.
Computing is about choice, especially in our open source communities. That will never change. I don’t know how future developments will affect Emacs, no one does. But I don’t see how it could change much for those who choose to use it over newer and flashier options.
It’s easy to see how a coding agent could nullify a text editor for a segment of users, but that would be applicable to all text editors—not just Emacs.
As any Emacs user can attest, it’s more than a text editor. Learning the ways of Emacs is learning the ways of Lisp which is learning about the nature of computation itself. That journey is worth taking for its own sake. No “agentic” expedient can replace it.
People are still learning and speaking “dead” languages like Latin and Ancient Greek. Because they’re not dead at all. They’re just as vibrant as ever in the places they are spoken (mostly in churches and legal offices) and in the languages that have been built from them.
While I do my best to welcome and understand any new technology, and reserve the right to tinker and test and taste to my heart’s content, I have no immediate plans on giving up my precious M-x. Not for nothing.
The post M-x and the future of Emacs appeared first on Chris Maiorana.
-1:-- M-x and the future of Emacs (Post Chris Maiorana)--L0--C0--2026-10-04T14:59:02.000Z
Just a quick offering today on an informative post from Ankit Gadiya about unwanted nested frames in Emacs. The TL;DR is that Gadiya runs Ghostel as his Emacs shell in TUI and demon mode. That works well but he has a script that calls emacsclient to load a file. There’s nothing special about it; it’s just what you’d expect it to be but when he runs it from with Ghostel, it creates a nested frame in the Ghostel frame rather than replacing the Ghostel frame.
The solution is pretty simple. Gadiya changed his script to check if he was in Ghostel, and if so, calls emacsclient with a parameter to reuse the current frame. You can check Gadiya’s post for the details.
For me, the most interesting aspect was how he checked if he was in Ghostel. It turns out that Emacs sets the environment variable INSIDE_EMACS when it’s running a shell or subprocess. I didn’t know that so I learned something new from Gadiya’s post.
Update : Added link to Gadiya’s post.
-1:-- Nested Emacs Frames (Post Irreal)--L0--C0--2026-10-04T14:58:04.000Z
-1:-- Opening org-capture in a new frame (Post Isa Mert Gurbuz)--L0--C0--2026-10-04T14:02:00.000Z
As we all discover eventually, age is going to be accompanied by declining eyesight. If you make your living writing and coding, this is going to result in some discomfort as it becomes harder and harder to read the text in books, newspapers, or your screen. You can’t do much about printed matter except to get stronger reading glasses but if you’re an Emacs user—or, really, any other editor—it’s easy to increase the text size on your screen. I find myself doing this more often when writing, especially at night when my eyes are tired.
The problem is that the adjustment doesn’t persist across sessions and you have to make the adjustment for every buffer that needs a non-default font size. James Cherti has a solution: persist-text-scale.el.
The TL;DR is that once you set a new font scale for a buffer, it persists across sessions and furthermore you can arrange for all buffers of a certain class to have the same scale settings. That’s perfect. You can, for example, arrange to have all the buffers you use for writing or reading text be in a larger text while keeping other buffers in the default or even smaller text.
Take a look at the GitHub repository linked above for the details. You might also want to check out the comments in Cherti’s Emacs subreddit announcement where he answers some questions on what he’s trying to accomplish with the package. If you’d like to have certain buffers rendered in a larger (or smaller) font, this package may be just what you need.
-1:-- Persisting Text Scale (Post Irreal)--L0--C0--2026-10-03T14:09:37.000Z
Earlier this year I added Ghostel to my Emacs configuration. Ghostel is a terminal emulator for Emacs that uses Ghostty's engine underneath. It is a delight to use especially with the TUI applications.
I use Emacs in daemon
mode,
and then use emacsclient to open new frames from the daemon. To use the same
Emacs daemon when opening files through other programs such as git, sops, I have
a small shell script ee configured as my default EDITOR. It opens a new
Emacs frame in the terminal.
#!/bin/sh
exec emacsclient --create-frame --no-window-system "$@"
When I run this script from Ghostel, it opens a nested Emacs frame in Ghostel's terminal session.
Emacs sets an environment variable INSIDE_EMACS for shells and subprocesses.
The script use this variable to check if it is launched from Ghostel and use the
--reuse-frame flag to open the new buffer in the current frame.
#!/bin/sh
if [ "$INSIDE_EMACS" = "ghostel" ]
then
exec emacsclient --reuse-frame "$@"
fi
exec emacsclient --create-frame --no-window-system "$@"
-1:-- Fix nested Emacs frames (Post Ankit Gadiya)--L0--C0--2026-10-03T00:00:00.000Z
We chatted with Pedro about Org Mode, LaTeX exports and font management including alternatives and things you can and can't do depending on your choices.
Update : Looks like the issue with the hand emoji was that there's a regression in TeXLive: LaTeX 2024 produces the PDF with the right fonts and colours, but LaTeX 2026 doesn't.
Links:
View in the Internet Archive, watch or comment on YouTube, read the transcript online, download the transcript (VTT or TXT), or e-mail me.
Pedro: The things that I have here that might be interesting for people is that another thing that is quite recent for Emacs is that we are starting to have a lot of the configuration variables as local variables. So you don't have to go and get in your init's .el to have anything configured. Why? This is another sort of fight against the windmills I've had. Why? Because with this I can make my Org files more distributable, so I can share them with more people and they don't have to touch anything in their configurations. That's the basic idea. Small little trick that I have is that I'm using fancyvrb for everything instead of listings. And there was somewhere in the documentation, you also have an explanation for how to use fancyvrb to substitute listings and get nice listings in your LaTeX exported by Org. And that's the only thing. Initially, pdflatex, Everything is plain. If I go and just go and export...
Pedro: This is a mini export menu I have myself because I think it's much more handy than the export thing that we have in Org Mode. So I did a small one for me. And so I'm exporting to LaTeX and there you see the...
Pedro: I was very much inspired by what Pandoc is doing. In Pandoc, you can say, "I have my serif font, which is this font. For this font, I want these fallbacks because that's..." Fallback is, first of all, fallback fonts. Fallback is a term used by lualatex to say, if you can't find a character in a font, try to find it in another font. And then, if you provide lualatex with a list of fonts, it's going to be looking up the fonts to get your characters. That's the main idea. Prot: Let me ask something here, because if you notice the output... Of course, it is not what we want, but it is also not just the tofu, you know, like the missing character. If you notice, there is some kind of a symbol there going on. From what I recall, we have characters and then we have something that is called the grapheme or grapheme cluster, something like that, which is basically characters laid on top of each other to produce the emoji. It seems here that Free Font has the underlying characters, but maybe not the composition codes or something like that. Pedro: No. It only has some characters. It only has some some underlying characters. For example it only has the... It has the finger, but it doesn't have all the compositions that you need for that. Prot: Exactly. Sacha: That one in particular, I think, changes the color of it. So that's why. Pedro: Yes. And if you look at the, if we go back here, it's saying there's no character at all for the face. But if you go to the beginning, you will also notice that there was some, where was it? I've seen it. At some point, they are also telling you that the thing... where is it? Okay, fine. It's going to take too long to find. Sacha: It's fine. Besides, you fixed the problem, so you don't have to dig into the old way of doing things. Pedro: Yes, so that's my main problem right now. So I go and I'm going to go to the... I keep trying to use the teleconference, the conference screen, the shared screen instead of the red one. This is why sometimes I sort of get mixed up. We had this. And I'm just going to go and comment this out because I'm going to go for the new one.
Pedro: For the new one, I have a full example here. Here we go with the fontspec configuration, which is at the beginning. You say, what are you doing there, right? I know it might look a bit scary at the beginning but then if you if you're familiar with what Pandoc is doing it's not that different Sacha: Nonsense. It's not scary at all. We'll just copy whatever you post. Prot: Life is simple. Pedro: Yeah, no, but I mean, that's the basic idea. You have something very similar to this in the actual documentation, both in the docstring and in the manual. So all this process that I'm telling you is also part of the manual, step by step. So let's go for this and let's try to... First of all, I'm going to load my new variables. I'm going to refresh the variables. Let's go.
Pedro: If you have noticed before I compile, you see that each font has its own size. That is sometimes sort of not very nice, right? So what I would do... The next is I'm going to add a property to both the sans and the monophon to say, try to scale them so that they match the lowercase of the main font. That sort of makes them more proportional, better look and feel, right?
Pedro: This is the LaTeX that is produced. The important part is here. That's the fallbacks. If you see, I'm only having the Noto Color emoji font being included. Why? Because what I told you before, there is no Han, so there's no character being added there. I don't add the Han font because I don't need it. That's sort of a mini optimization that you may want to have. Next thing...
Pedro: I get my fonts scaled so that they look more or less the same size, which is a nice appearance. I have my listings and my outputs. And I have my emojis. Prot: This looks good. Pedro: This is basically what I need. Prot: Can I ask something? Two questions, actually. One is, I see the props parameter you have for the... if you switch to your Emacs buffer... Pedro: Just a second. I was on the wrong stream. Prot: Okay.
Pedro: And just before we close this, I've been showing what I mostly hear dominate, which is English, Spanish, or Latin alphabet. But with this package, we also have initial support for when you're using xelatex. There's a package which is called xeCJK to provide simplified Chinese, Japanese, and Korean documents. So this, with lualatex, raw, without any Babel or anything else, raw and monolingual, is also supported. What I basically do is I map what is on Overleaf describing how to do this with xelatex and this package which is called xeCJK. I map that with the same kind of header. You can use this kind of variables to do the same for this Chinese, Japanese, and Korean document. Sacha: Would you happen to have an example so that people can see what the markup looks like? Pedro: The markup, I mean, the idea would be instead of using main... I don't have an example here.. But the idea is that instead of calling the fonts main, sans, and mono, you have CJKmain. So you would have something like, the font would be like CJKMain. C, J, K. And the other thing is that you would have to change your compiler to xelatex. That's also very, very well documented in current main Org. So we have it. That would be more or less the effort of the last couple of years and how it has crystallized into things that are already in master. You can e-mail me at sacha@sachachua.com.
-1:-- Emacs Chat 31: Pedro A. Aranda Gutierrez (Post Sacha Chua)--L0--C0--2026-10-02T23:01:46.000Z
There are a couple of threads on the Emacs Devel mailing list that, zombie like, refuse to die. They’re essentially the same. One, entitled "ethically" running proprietary code? is an endless discussion of what Emacs should do if a package can indirectly lead to running non-free code. The other, [NonGNU] new: hermes is the same discussion but is particularized to whether Hermes, which is free software but might indirectly lead to a non-free LLM implementation should be added to the NonGNU repository.
The sane people on the list argue that Emacs can’t possibly control what users do with a particular piece of Emacs software and point to EWW as an example. It’s a core part of Emacs and as free as can be yet users can use it to download non-free software and do other things that the FSF wouldn’t approve of.
The true believers, on the other hand, say, “But but but. Someone, somewhere might use non-free software.” Of course they will. Unless you’re RMS who has taken extraordinary steps to restrict his computer use to a tiny part of what’s available, we all use non-free software of some sort. Even if you’re using Linux and are careful about the software you install, what about the BIOS on your computer?
Most of us support FSF and its ideals. We believe in free software and see it as a net positive for our industry. That’s different from taking an absolutist position—as if we were members of some crazy cult—and insisting that nothing we do should ever lead, even indirectly, to the use of non-free software.
Over at the Emacs subreddit, DevelopmentCool2449 takes note of the conversation and briefly discusses how it has opened a schism between RMS and Emacs developers. The action, as usual, is in the comments. Most commenters seem to side with the developers but some are taking no prisoners and hope that Eli Zaretskii quits. This despite the years of unpaid, exemplary service that Zaretskii has given to the Emacs community.
It’s obvious which side of the debate I’m on but good faith arguments can be made on both sides. Still, I wish everyone could agree to disagree and move on.
-1:-- 🥩 Red Meat Friday: True Believers (Post Irreal)--L0--C0--2026-10-02T15:04:23.000Z
The Emacs function spook is
not what you think it is. It doesn't make creepy noises, draw scary
ASCII art or change the theme to orange on black. In fact it has
nothing at all to do with Halloween, ghosts, monsters, pandemics or
IRS audits.
What it does is insert words and phrases into the current buffer "to help give your message ... attractiveness for automated keyword scanners."
What kind of words or phrases might you expect to see? To find out I
invoked spook while composing this content. Here's what
it added:
TELINT Drug North Korea Security Consulting Virii Abu Ghraib Fort Hancock CUD AMEMB Vaccine MARTA racal E.O.D. Blizzard Nigeria
It's easy1 to configure spook to insert an alternate set
of words and phrases -- just change the file name in the
variable spook-phrases-file from "spook.lines" to the
name of the alternate file. Unfortunately it must reside in the
Emacs data directory; the file's location is fixed. The variable
can be found in the Spook customization group, whose parent group is
Games.
Even with only 72 lines in its source code file,2 spook demonstrates how to insert one or more random lines from a file into the current buffer.
1 It's "easy" if you have Administrator permissions.
2 The 72 lines is the entire spook.el file; the first 48% of the file provides the usual comments that describe the GNU GPL, author, maintainer info as well as documentation.
-1:-- What is spook? (Post Raymond Zeitler)--L0--C0--2026-10-02T02:17:46.693Z
Raw link: https://www.youtube.com/watch?v=gPaaFJQWVj8
This is a summary of comments I made at the ‘Root Access’ podcast. They are on the topic of whether to use Omarchy or not on the basis of how one feels about its author.
I personally prefer the complete episode because clips can distort the vide of the event, such as by making me look angry when in fact we are laughing throughout: https://protesilaos.com/codelog/2026-09-27-emacs-live-root-access-show/.
Root Access is a live conversation with Linkarzu, CEO members, and invited guests about tech, tools, workflows, and what they’re building. Guests can share their projects before we dive into the discussion. Everyone is welcome to watch live and join the chat; the full replay stays public.
-1:-- About Omarchy and reasonableness (Post Protesilaos)--L0--C0--2026-10-02T00:00:00.000Z
Raw link: https://www.youtube.com/watch?v=LCWWdcu8Wx8
This is a summary of comments I made at the ‘Root Access’ podcast. They are on the topic of AI-generated contributions to Emacs and the wider discussion on free software politics.
I personally prefer the complete episode because clips can distort the vide of the event, such as by making me look angry when in fact we are laughing throughout: https://protesilaos.com/codelog/2026-09-27-emacs-live-root-access-show/.
Root Access is a live conversation with Linkarzu, CEO members, and invited guests about tech, tools, workflows, and what they’re building. Guests can share their projects before we dive into the discussion. Everyone is welcome to watch live and join the chat; the full replay stays public.
-1:-- About Emacs, AI contributions, and the politics of copyright (Post Protesilaos)--L0--C0--2026-10-02T00:00:00.000Z
One of the most fun things you can do with Emacs is to build an interface for another piece of software. It is the Lisp version of running Doom on any device. It is also quite useful, because you can connect it to other tools in the gnu's editor and build more pleasant workflows.
The big question: can I chat on WhatsApp from Emacs? Spoiler: I made it, with some limitations.
Context: I work on macOS. It matters, because the solution depends on it.
Meta offers no API for this, and the macOS desktop app has no bridge at all: no open ports, no gRPC, no AppleScript dictionary. Nothing.
There are unofficial clients, like whatsmeow, that talk directly to WhatsApp's servers by pretending to be a device. They work, but your account ends up depending on software Meta does not authorize, and it is one more process to maintain. On top of that, Meta can detect them and ban your number. I am not going to risk my account just to chat from Emacs.
So how do you talk to an app that does not want to talk to you? By reading the same thing it reads.
The WhatsApp app stores every chat in a local SQLite file:
~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite
Emacs 29 ships with SQLite built in, no external libraries. Opening it read-only takes one line:
(sqlite-open file t)
The schema is undocumented, so I had to explore it by hand. Chats live in ZWACHATSESSION, messages in ZWAMESSAGE, and group member names in ZWAGROUPMEMBER and ZWAPROFILEPUSHNAME. That Z at the start of every name gives it away: it is a Core Data database.
And Core Data has a surprise. Dates do not count from 1970, but from January 1, 2001. My first messages showed up dated 1995. The fix: a constant.
(defconst whatsapp--epoch-offset 978307200
"Seconds between the Unix epoch and the Core Data epoch (2001-01-01).")
There were more traps. Pinned chats store a date in the future so they stay on top, and the text of the session's last message is encoded. Better to read both from the message itself.
One more detail: macOS protects that directory. You have to give Emacs Full Disk Access in the privacy settings. I would hand Emacs the keys to my house if it asked.
Reading is easy. What about writing? Editing the database is pointless: the app would not notice and nothing would go out over the network.
Then I remembered that the WhatsApp app provides an action in macOS Shortcuts: "Send Message". And Shortcuts can be run from the terminal:
shortcuts run "Send WhatsApp" --input-path message.json
The problem is that the action expects a contact from your address book, not a number. The trick: build a vCard on the fly with the chat's phone number, which the shortcut turns into a temporary contact. Nothing is saved in your address book.
:quality(85)/https://andros.dev/media/blog/2026/10/whatsapp-shortcut.png)
Emacs writes a JSON file with the vCard and the text, starts the process and waits:
(json-encode
`((vcard . ,(whatsapp--vcard name phone))
(text . ,text)))
The full flow looks like this:
flowchart LR
E[Emacs] -- reads --> DB[(ChatStorage.sqlite)]
E -- JSON with vCard --> S[Send WhatsApp shortcut]
S -- Send Message --> W[Official WhatsApp app]
W -- stores --> DB
I like one detail. While the shortcut runs, the message shows as (sending...). When WhatsApp stores it in its database, Emacs replaces it with the real one. What you see is what WhatsApp actually sent, not what Emacs thinks it sent.
An ERC or rcirc style interface, with a chat list and one buffer per conversation:
--- Wednesday 1 October 2026 ---
[08:15] <Alice> Good morning!
[08:16] <Me> Morning! Are we still on for today?
[08:20] <Alice> Yes, 10:00 at the station
[08:21] <Alice> [image] This is the entrance
Alice> _
This is the most trivial part of the whole thing. Emacs solved this problem decades ago.
The whole client fits in a few lines of Elisp, with no dependencies.
It has its limits, of course.
C-c C-u to fetch new messages.But now I can talk to my family without leaving Emacs. What more could you ask for?
shortcuts command.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-782e8f3b@andros.dev to leave a comment. The subject will be ignored.
-1:-- How I built a WhatsApp client for Emacs without an API (Post Andros Fenollosa)--L0--C0--2026-10-01T07:52:08.000Z
The default keybinding of org-todo is C-c
C-t. Today I learned about another command with a similar
keybinding (C-c C-x C-t) --
org-toggle-timestamp-overlays. Note the C-x
in the middle.
When I pressed that key combination (inadvertently), all the
YYYY-MM-DD timestamps in the buffer were converted to the MM/DD/YYYY
format. This was unacceptable -- I needed to change them back.
However undo did not revert the timestamps; it just undid
the last edit. Furthermore, these undesired timestamps couldn't be
edited and were impervious to org-shiftright. I was
confused yet again.
I didn't even know how this happened. So I
invoked view-lossage as a kind reader once suggested 1
and discovered org-toggle-timestamp-overlays.
The docstring was no help – it's self-evident that it would "Toggle the use of custom time stamp formats." Eager to clock out of my task, I didn't care to learn more. All I wanted to do was undo the effect.
I figured that since it toggles the timestamp, all I had to do was invoke it again to toggle it back. Nope. Well, a nice message appears, saying "Time stamp overlays removed." Yet the timestamps looked the same.
Undaunted, I figured I'd reload the buffer with org-mode-restart.
That had no effect. To finally fix this, I deleted the buffer and
revisited the file.
A command like this surely is in the same category as upcase-region,
which is disabled by default. And so this is why I added
(put 'org-toggle-timestamp-overlays 'disabled t) to my init file.
1 See https://ray-on-emacs.blogspot.com/2026/05/emacs-view-commands.html
-1:-- Now org-toggle-timestamp-overlays is Disabled (Post Raymond Zeitler)--L0--C0--2026-10-01T00:19:28.313Z
Quick, what does this match?
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
// Take
// your
// time...
//
// ...still decoding?
//
// OK, keep reading :)
That's the official regexp from semver.org. It validates version numbers like:
// matches
"1.2.3"
"0.10.0"
"2.0.0-rc.1"
"1.0.0-alpha.1+build.5"
"1.0.0+20260930"
// doesn't match
"01.2.3" // leading zero
"1.2" // missing patch
"v1.2.3" // no "v" prefix allowed
"1.0.0-01" // numeric pre-release with a leading zero
"1.2.3-" // empty pre-release
Don't get me wrong, I love regexps, but in practice you probably spend a bunch of time writing one, testing it against some cases, and moving on, proud of your achievement!
Some time passes and lucky future you (or unlucky someone else) has to change it. Dramatic pause here.
I bet you've been there. Now your options are probably: decode it again from the start, rewrite the whole thing, or, in the age of AI, ask (and hopefully not blindly accept) an LLM for a new recipe.
Emacs has had a nice answer for more readable regexps for a long time:
the rx macro. I started using it all the time in Emacs Lisp, as
reviewers always suggested it to me. Later, I started missing this DSL
in JavaScript and TypeScript, so I wrote a small version of it for my
projects.
So, what about reading that SemVer regexp like semver in the code
below?
const num = or("0", seq(anyOf("1-9"), zeroOrMore(digit)));
const idChar = anyOf(alnum, "-");
const preId = or(num, seq(zeroOrMore(digit), anyOf(alpha, "-"), zeroOrMore(idChar)));
const dotted = (x: Item) => seq(x, zeroOrMore(".", x));
const semver = RX(
start,
named("major", num), ".",
named("minor", num), ".",
named("patch", num),
optional("-", named("pre", dotted(preId))),
optional("+", named("build", dotted(oneOrMore(idChar)))),
end,
);
The same strings match, and you get named groups as a bonus. By the end of this post you'll know every piece of it.
TL;DR: jump straight to the cheat sheet, the side-by-side examples, the full source, or grab the gist to sneak a peek at the result.
NOTE: the
RXhere has nothing to do with RxJS, which is an amazing library for reactive programming with observables.
With rx you describe a regexp as a tree of named forms, and Emacs
turns it into the regexp string for you:
(rx bos (+ digit) eos)
;; => "\\`[[:digit:]]+\\'"
(rx bol "colo" (? "u") "r" eol)
;; => "^colou?r$"
(rx bos "(" (= 3 digit) ")" space (= 3 digit) "-" (= 4 digit) eos)
;; => "\\`([[:digit:]]\\{3\\})[[:space:]][[:digit:]]\\{3\\}-[[:digit:]]\\{4\\}\\'"
A few things to notice:
"(" means a parenthesis. You don't
need to escape anything by hand.seq, even though seq exists.(+ digit) becomes
[[:digit:]]+, not \(?:[[:digit:]]\)+.The proposed JavaScript/TypeScript version in this post reads like this:
const phone = RX(
start, "(", repeat(3, digit), ")", space,
repeat(3, digit), "-", repeat(4, digit), end,
);
// => /^\(\d{3}\)\s\d{3}-\d{4}$/
If you want strings to be literals, you can't represent a regexp piece
as a plain string, otherwise you can't tell "(" (a literal
parenthesis) apart from "(?:...)" (a group you built). So every
piece is a small object:
type Kind = "atom" | "seq" | "alt";
interface RxNode {
readonly src: string;
readonly kind: Kind;
readonly set?: string; // char sets only, see below
readonly neg?: boolean;
}
type Item = string | RxNode;
src is the regexp text. kind records how that text behaves when
you glue it to other things:
atom: a single unit, like a, \d, [a-z] or (...). You can
put a quantifier right after it.seq: safe to concatenate, but a quantifier needs (?:...) around
it. abc is a seq, and so is a+, since a+? would silently
turn into a lazy quantifier.alt: has a | at the top level, so it needs (?:...) almost
everywhere.Plain strings go through literal, which escapes them:
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const literal = (s: string): RxNode => ({
src: esc(s),
kind: s.length === 1 ? "atom" : "seq",
});
const toNode = (x: Item): RxNode => (typeof x === "string" ? literal(x) : x);
With that in place, seq joins nodes and only brackets alternations:
const seq = (...xs: Item[]): RxNode => {
const nodes = xs.map(toNode).filter((n) => n.src !== "");
if (nodes.length === 0) return { src: "", kind: "seq" };
if (nodes.length === 1) return nodes[0];
let src = "";
for (const n of nodes) {
const part = n.kind === "alt" ? `(?:${n.src})` : n.src;
// `\1` followed by a literal `0` would read as `\10`
if (/\\\d+$/.test(src) && /^\d/.test(part)) src += "(?:)";
src += part;
}
return { src, kind: "seq" };
};
(That backreference check is one of those bugs you only find by
writing tests, or when it happens to you in prod. backref(1)
followed by the literal "0" gives you backreference number ten.)
Every quantifier is a seq of its arguments plus a suffix, bracketed
only when the body isn't an atom:
const quantifiable = (n: RxNode) =>
n.kind === "atom" ? n.src : `(?:${n.src})`;
const quantifier =
(suffix: string) =>
(...xs: Item[]): RxNode => ({
src: quantifiable(seq(...xs)) + suffix,
kind: "seq",
});
const zeroOrMore = quantifier("*");
const oneOrMore = quantifier("+");
const optional = quantifier("?");
Because each quantifier calls seq on its arguments, you get the
implicit sequence for free: optional("-", group(x)) becomes
(?:-(x))?.
And finally, the two entry points. As in Emacs, rx returns a
string. RX returns a RegExp you can use right away:
const rx = (...xs: Item[]): string => seq(...xs).src;
function RX(...xs: Item[]): RegExp {
return new RegExp(rx(...xs));
}
RX.flags = (flags: string, ...xs: Item[]): RegExp =>
new RegExp(rx(...xs), flags);
RX.flags exists because Emacs controls case folding through the
case-fold-search variable, and JavaScript puts it on the regexp
itself.
That's the whole engine! Now, let's build our vocabulary.
In Emacs you write (any "a-z" "_"). Inside those strings, a-z is a
range, and a - at either end is a plain dash. I kept the same rule:
const hexDigit = anyOf("0-9a-fA-F");
RX(start, "#", repeat(6, hexDigit), end);
// => /^#[0-9a-fA-F]{6}$/
RX(start, optional(anyOf("+-")), oneOrMore(digit), end);
// => /^[+\-]?\d+$/
The dash comes out escaped because sets can merge. If you combine
anyOf("+-") with anyOf("0-9"), an unescaped - would end up in
the middle and create a range from + to 0. Escaping it costs one
backslash.
And merging is the reason why RxNode has a set field. It is there
to hold the text that goes between [ and ], so anyOf can take
other sets as arguments:
const lower = anyOf("a-z");
const upper = anyOf("A-Z");
const alpha = anyOf(lower, upper); // [a-zA-Z]
const alnum = anyOf(alpha, "0-9"); // [a-zA-Z0-9]
not negates a set, and it knows the shorthand classes:
not(digit); // \D
not(anyOf(space, "@")); // [^\s@]
notChar(","); // [^,] (rx's not-char)
The simple email check, which most of us have written as
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ at some point, becomes:
const part = oneOrMore(not(anyOf(space, "@")));
RX(start, part, "@", part, ".", part, end);
// => /^[^\s@]+@[^\s@]+\.[^\s@]+$/
The rest of the Emacs character classes are there too: digit,
hexDigit, space, blank, wordChar, notWordChar, alpha,
alnum, lower, upper, punct, control, graphic, printing,
ascii and nonascii. One difference: in Emacs they understand
Unicode, and mine are ASCII only. alpha won't match é.
Two more come from rx's symbol list, and people (me, many times) mix them up:
const notNewline: RxNode = { src: ".", kind: "atom" }; // rx: nonl
const anything = set("\\s\\S"); // rx: anything / anychar
In rx, anything really means anything, newlines included. Here is
where the difference shows up:
const code = "a = 1; /* first\n second */ b = 2;";
RX("/*", zeroOrMoreLazy(notNewline), "*/").exec(code);
// => null
RX("/*", zeroOrMoreLazy(anything), "*/").exec(code)?.[0];
// => "/* first\n second */"
or works as you'd expect, and gets bracketed when it lands inside a
sequence:
RX(start, or("cat", "dog", "bird"), end);
// => /^(?:bird|cat|dog)$/
Did you notice the order changed? I copied that behavior from
Emacs. When every branch of an or is a plain string, rx hands them
to regexp-opt, which builds a pattern that prefers the longest
match:
(rx (or "in" "int" "interface"))
;; => "\\(?:in\\(?:t\\(?:erface\\)?\\)?\\)"
JavaScript alternation takes the first branch that matches, going left to right. So the naive regexp for a list of keywords has a 'bug':
/in|int|interface/.exec("interface Foo")?.[0];
// => "in"
RX(or("in", "int", "interface")).exec("interface Foo")?.[0];
// => "interface"
I don't build a trie like regexp-opt does. Sorting the strings by
length, longest first, is enough to get the same behavior:
const or = (...xs: Item[]): RxNode => {
if (xs.length === 0) return unmatchable;
if (xs.length === 1) return toNode(xs[0]);
const branches = xs.every((x) => typeof x === "string")
? [...(xs as string[])].sort((a, b) => b.length - a.length)
: xs;
return { src: branches.map((x) => toNode(x).src).join("|"), kind: "alt" };
};
As in Emacs, or() with no branches returns unmatchable, which is
(?!) here. It's handy when you build the branch list at runtime and
it might come out empty.
Emacs has (= n ...), (>= n ...) and (** n m ...). Here they are
repeat, atLeast and between:
RX(start, between(2, 4, digit), end); // /^\d{2,4}$/
RX(atLeast(3, digit)); // /\d{3,}/
The lazy versions *?, +? and ?? are zeroOrMoreLazy,
oneOrMoreLazy and optionalLazy. The classic HTML tag example:
const html = "<b>bold</b> and <i>italic</i>";
RX("<", oneOrMore(notNewline), ">").exec(html)?.[0];
// => "<b>bold</b> and <i>italic</i>"
RX("<", oneOrMoreLazy(notNewline), ">").exec(html)?.[0];
// => "<b>"
group is a capturing group, and backref points back to it:
RX(start, group(oneOrMore(wordChar)), space, backref(1), end);
// => /^(\w+)\s\1$/ matches "hello hello", not "hello world"
Emacs also has (group-n N ...) to pick the group number. JavaScript
can't do that, but it has named groups, which serve the same purpose
and read better:
const date = RX(
named("y", repeat(4, digit)), "-",
named("m", repeat(2, digit)), "-",
named("d", repeat(2, digit)),
);
date.exec("2026-09-30")?.groups;
// => { y: '2026', m: '09', d: '30' }
backref accepts a name as well:
RX(
"<", named("tag", oneOrMore(wordChar)), ">",
zeroOrMoreLazy(notNewline),
"</", backref("tag"), ">",
);
// => /<(?<tag>\w+)>.*?<\/\k<tag>>/
rx distinguishes the start of the string (bos) from the start of a
line (bol). In JavaScript both are ^, and the m flag decides
which one you get. I kept both names so the intent shows in the code:
const text = "TODO: write post\nDONE: fix rx\nTODO: publish";
const todo = RX.flags(
"gm",
lineStart, "TODO: ", named("task", oneOrMore(notNewline)), lineEnd,
);
[...text.matchAll(todo)].map((m) => m.groups?.task);
// => [ 'write post', 'publish' ]
Why not add start and end automatically? Because you only want
them when validating a whole string. When searching inside a text, as
in split, replace or matchAll, a hidden ^ and $ would break
everything. Emacs agrees: bos and eos are explicit in rx too.
wordBoundary and notWordBoundary map straight to \b and \B.
Emacs also has bow and eow (\< and \>), start and end of a
word. JavaScript lacks those, so I combined \b with a lookaround:
const wordStart = zeroWidth("\\b(?=\\w)");
const wordEnd = zeroWidth("\\b(?<=\\w)");
Plain strings are already literals, but rx has an explicit literal
form for strings computed at runtime, and I kept it. It documents that
the value came from somewhere else:
const userInput = "1+1=2? (maybe)";
new RegExp(userInput).test(userInput); // false, oops
RX(literal(userInput)).test(userInput); // true
The opposite direction is rx's (regexp ...) form, the escape hatch.
Here it's raw, and it receives either a string or an existing
RegExp. It lets you adopt the DSL in a codebase full of old regexps
without rewriting all of them, like:
const legacyZip = /\d{5}(?:-\d{4})?/;
RX(start, repeat(2, upper), " ", raw(legacyZip), end);
// => /^[A-Z]{2} (?:\d{5}(?:-\d{4})?)$/
raw can't see inside the text it gets, so it adds brackets whenever
it's combined with something else. It's an extra (?:), and the
regexp still works.
Back to the regexp from the intro. In Emacs you would give names to
the pieces with rx-define or rx-let. In TypeScript those are just
consts:
const num = or("0", seq(anyOf("1-9"), zeroOrMore(digit)));
const idChar = anyOf(alnum, "-");
const preId = or(num, seq(zeroOrMore(digit), anyOf(alpha, "-"), zeroOrMore(idChar)));
const dotted = (x: Item) => seq(x, zeroOrMore(".", x));
const semver = RX(
start,
named("major", num), ".",
named("minor", num), ".",
named("patch", num),
optional("-", named("pre", dotted(preId))),
optional("+", named("build", dotted(oneOrMore(idChar)))),
end,
);
Now you can read the spec in the code. A numeric identifier is 0, or
a non-zero digit followed by any number of digits. A pre-release is a
dotted list of identifiers, and so is build metadata. dotted is a
plain function returning a node, which is as far as abstraction needs
to go here.
It matches the same strings as the official regexp, and the named groups give you a result like:
semver.exec("1.0.0-alpha.1+build.5")?.groups;
// => {
// major: '1',
// minor: '0',
// patch: '0',
// pre: 'alpha.1',
// build: 'build.5'
// }
Next time the spec changes, you can understand what the current regex does at a glance, instead of fighting an army of punctuation.
I tried to map every rx form, and a few have no JavaScript equivalent:
point: JavaScript regexps don't know about a cursor.symbol-start, symbol-end, syntax, category: these depend on
Emacs syntax tables.intersection: possible with the v flag, but I haven't needed it.minimal-match / maximal-match: these flip the greediness of
everything inside them. Doable, but it would need a separate pass,
and the *Lazy functions cover my use cases.eval: TypeScript already evaluates expressions everywhere, so you
get it for free.And one addition Emacs doesn't need: RX.flags.
| Emacs rx | TypeScript | JS regexp (roughly) |
|---|---|---|
seq, :, and |
seq(...), implicit in every form |
ab |
or, | |
or(...) |
a|b |
any, in, char |
anyOf("a-z", "_", digit) |
[a-z_\d] |
not-char |
notChar(...) |
[^...] |
not |
not(charset) |
\D, [^...] |
*, +, ? |
zeroOrMore, oneOrMore, optional |
x*, x+, x? |
*?, +?, ?? |
zeroOrMoreLazy, oneOrMoreLazy, optionalLazy |
x*?, x+?, x?? |
=, >=, ** |
repeat, atLeast, between |
x{n}, x{n,}, x{n,m} |
group |
group(...) |
(...) |
group-n |
named("name", ...) |
(?<name>...) |
backref |
backref(1), backref("name") |
\1, \k<name> |
literal |
literal(s) |
s, escaped: 1\+1 |
regexp, regex |
raw("..."), raw(/.../) |
(?:...), as-is |
rx-define, rx-let |
const |
(none) |
bos, eos |
start, end |
^, $ |
bol, eol |
lineStart, lineEnd (with the m flag) |
^, $ |
bow, eow |
wordStart, wordEnd |
\b(?=\w), \b(?<=\w) |
word-boundary |
wordBoundary |
\b |
not-word-boundary |
notWordBoundary |
\B |
nonl, not-newline |
notNewline |
. |
anychar, anything |
anything |
[\s\S] |
unmatchable |
unmatchable |
(?!) |
digit |
digit |
\d |
hex-digit, xdigit |
hexDigit |
[0-9a-fA-F] |
space, whitespace |
space |
\s |
blank |
blank |
[ \t] |
word, wordchar |
wordChar |
\w |
not-wordchar |
notWordChar |
\W |
alpha, letter |
alpha |
[a-zA-Z] |
alnum |
alnum |
[a-zA-Z0-9] |
lower, upper |
lower, upper |
[a-z], [A-Z] |
punct, punctuation |
punct |
[!-/:-@[-`{-~] |
cntrl, control |
control |
[\x00-\x1f\x7f] |
graph, graphic |
graphic |
[!-~] |
print, printing |
printing |
[ -~] |
ascii, nonascii |
ascii, nonascii |
[\x00-\x7f], [\u0080-\uffff] |
Each example below shows the goal, the Emacs rx form in a comment,
the regexp you would write by hand, and the RX version. When RX
produces a different regexp text, the // => line shows it. The
results at the bottom come from running both against the same strings.
The whole string is digits.
// Emacs: (rx bos (+ digit) eos)
const regex = /^\d+$/;
const dsl = RX(start, oneOrMore(digit), end);
// "123" -> true
// "12a" -> false
The whole string is ASCII letters.
// Emacs: (rx bos (+ alpha) eos)
const regex = /^[a-zA-Z]+$/;
const dsl = RX(start, oneOrMore(alpha), end);
// "Hello" -> true
// "He11o" -> false
Both spellings, color and colour.
// Emacs: (rx bos "colo" (? "u") "r" eos)
const regex = /^colou?r$/;
const dsl = RX(start, "colo", optional("u"), "r", end);
// "color" -> true
// "colour" -> true
// "colouur" -> false
Two words separated by a space.
// Emacs: (rx bos (+ wordchar) space (+ wordchar) eos)
const regex = /^\w+\s\w+$/;
const word = oneOrMore(wordChar);
const dsl = RX(start, word, space, word, end);
// "hello world" -> true
// "hello" -> false
(123) 456-7890, parentheses and all.
// Emacs: (rx bos "(" (= 3 digit) ")" space (= 3 digit) "-" (= 4 digit) eos)
const regex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
const dsl = RX(
start, "(", repeat(3, digit), ")", space,
repeat(3, digit), "-", repeat(4, digit), end,
);
// "(123) 456-7890" -> true
// "123-456-7890" -> false
#ff00aa-style colors.
// Emacs: (rx bos "#" (= 6 hex-digit) eos)
const regex = /^#[0-9a-fA-F]{6}$/;
const dsl = RX(start, "#", repeat(6, hexDigit), end);
// "#ff00aa" -> true
// "#ff00ag" -> false
An optional sign, then digits.
// Emacs: (rx bos (? (any "+-")) (+ digit) eos)
const regex = /^[+-]?\d+$/;
const dsl = RX(start, optional(anyOf("+-")), oneOrMore(digit), end);
// => /^[+\-]?\d+$/
// "-42" -> true
// "42" -> true
// "*42" -> false
Something@something.something, no spaces.
// Emacs: (rx-let ((part (+ (not (any space "@")))))
// (rx bos part "@" part "." part eos))
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const part = oneOrMore(not(anyOf(space, "@")));
const dsl = RX(start, part, "@", part, ".", part, end);
// "a@b.com" -> true
// "a b@c.com" -> false
// "a@b" -> false
A string without any digit.
// Emacs: (rx bos (+ (not digit)) eos)
const regex = /^[^\d]+$/;
const dsl = RX(start, oneOrMore(not(digit)), end);
// => /^\D+$/
// "abc" -> true
// "a1b" -> false
Exactly three comma-separated fields.
// Emacs: (rx-let ((field (+ (not-char ","))))
// (rx bos field "," field "," field eos))
const regex = /^[^,]+,[^,]+,[^,]+$/;
const field = oneOrMore(notChar(","));
const dsl = RX(start, field, ",", field, ",", field, end);
// "a,b,c" -> true
// "a,b," -> false
A fixed list of words.
// Emacs: (rx bos (or "cat" "dog" "bird") eos)
const regex = /^(?:cat|dog|bird)$/;
const dsl = RX(start, or("cat", "dog", "bird"), end);
// => /^(?:bird|cat|dog)$/
// "cat" -> true
// "bird" -> true
// "cow" -> false
mr or ms, then a name, keeping the title.
// Emacs: (rx bos (group (or "mr" "ms")) space (+ wordchar) eos)
const regex = /^(mr|ms)\s\w+$/;
const dsl = RX(start, group(or("mr", "ms")), space, oneOrMore(wordChar), end);
// "mr john" -> true
// "dr john" -> false
Two to four digits.
// Emacs: (rx bos (** 2 4 digit) eos)
const regex = /^\d{2,4}$/;
const dsl = RX(start, between(2, 4, digit), end);
// "12" -> true
// "12345" -> false
Three or more digits, anywhere.
// Emacs: (rx (>= 3 digit))
const regex = /\d{3,}/;
const dsl = RX(atLeast(3, digit));
// "12" -> false
// "a123" -> true
The same word twice.
// Emacs: (rx bos (group (+ wordchar)) space (backref 1) eos)
const regex = /^(\w+)\s\1$/;
const dsl = RX(start, group(oneOrMore(wordChar)), space, backref(1), end);
// "hello hello" -> true
// "hello world" -> false
An open tag and its own closing tag.
// Emacs: (rx "<" (group-n 1 (+ wordchar)) ">" (*? nonl) "</" (backref 1) ">")
const regex = /<(?<tag>\w+)>.*?<\/\k<tag>>/;
const dsl = RX(
"<", named("tag", oneOrMore(wordChar)), ">",
zeroOrMoreLazy(notNewline),
"</", backref("tag"), ">",
);
// "<b>bold</b>" -> true
// "<b>oops</i>" -> false
cat as a word, not inside another one.
// Emacs: (rx word-boundary "cat" word-boundary)
const regex = /\bcat\b/;
const dsl = RX(wordBoundary, "cat", wordBoundary);
// "the cat sat" -> true
// "concatenate" -> false
hello, in any case.
// Emacs: (let ((case-fold-search t))
// (string-match-p (rx bos "hello" eos) "HeLLo"))
const regex = /^hello$/i;
const dsl = RX.flags("i", start, "hello", end);
// "HeLLo" -> true
// "help" -> false
It's a single file with no dependencies. Copy it into your project and start deleting the forms you don't need, or adding the ones you miss.
You can check the same code, plus all the examples from this post (and a few more), in this gist. If you'd rather not set anything up, paste it into the TypeScript Playground, hit "Run", and check the "Logs" tab.
/* =========================================================
* CORE
* ========================================================= */
// How a node behaves when combined with others:
// atom -> single unit, a quantifier can be glued right after it
// seq -> safe to concatenate, needs (?:) to be quantified
// alt -> has a top-level `|`, needs (?:) almost everywhere
type Kind = "atom" | "seq" | "alt";
interface RxNode {
readonly src: string;
readonly kind: Kind;
// char sets only: the text that goes inside [ ], so sets can merge
readonly set?: string;
readonly neg?: boolean;
}
type Item = string | RxNode;
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escSet = (s: string) => s.replace(/[\]\\^-]/g, "\\$&");
// rx: (literal EXPR) — a string computed at runtime, matched as-is
const literal = (s: string): RxNode => ({
src: esc(s),
kind: s.length === 1 ? "atom" : "seq",
});
const toNode = (x: Item): RxNode => (typeof x === "string" ? literal(x) : x);
// wrap a node so a quantifier applies to all of it
const quantifiable = (n: RxNode) =>
n.kind === "atom" ? n.src : `(?:${n.src})`;
// rx: (regexp EXPR) — escape hatch, trust the regexp as-is
const raw = (re: string | RegExp): RxNode => ({
src: typeof re === "string" ? re : re.source,
kind: "alt",
});
/* =========================================================
* COMPOSITION
* ========================================================= */
const seq = (...xs: Item[]): RxNode => {
const nodes = xs.map(toNode).filter((n) => n.src !== "");
if (nodes.length === 0) return { src: "", kind: "seq" };
if (nodes.length === 1) return nodes[0];
let src = "";
for (const n of nodes) {
const part = n.kind === "alt" ? `(?:${n.src})` : n.src;
// `\1` followed by a literal `0` would read as `\10`
if (/\\\d+$/.test(src) && /^\d/.test(part)) src += "(?:)";
src += part;
}
return { src, kind: "seq" };
};
const unmatchable: RxNode = { src: "(?!)", kind: "atom" };
// Like rx: when every branch is a plain string, try the longest first,
// so or("in", "int") matches "int" instead of stopping at "in".
const or = (...xs: Item[]): RxNode => {
if (xs.length === 0) return unmatchable;
if (xs.length === 1) return toNode(xs[0]);
const branches = xs.every((x) => typeof x === "string")
? [...(xs as string[])].sort((a, b) => b.length - a.length)
: xs;
return { src: branches.map((x) => toNode(x).src).join("|"), kind: "alt" };
};
/* =========================================================
* CHARACTER SETS
* ========================================================= */
const set = (body: string, neg = false): RxNode => ({
src: neg ? `[^${body}]` : `[${body}]`,
kind: "atom",
set: body,
neg,
});
// class escapes are sets too, so they can go inside anyOf(...)
const classEscape = (e: string): RxNode => ({
src: e,
kind: "atom",
set: e,
neg: false,
});
// Same reading as rx: inside a string, "a-z" is a range, while a `-`
// at the start or end is just a dash ("+-" is plus or minus).
const intervals = (s: string): string => {
let body = "";
let i = 0;
while (i < s.length) {
if (i < s.length - 2 && s[i + 1] === "-") {
body += `${escSet(s[i])}-${escSet(s[i + 2])}`;
i += 3;
} else {
body += escSet(s[i]);
i += 1;
}
}
return body;
};
// rx: (any "a-z" "_" digit) — also known as `in` and `char`
const anyOf = (...xs: Item[]): RxNode => {
const body = xs
.map((x) => {
if (typeof x === "string") return intervals(x);
if (x.set === undefined || x.neg)
throw new Error(`anyOf: not a positive char set: ${x.src}`);
return x.set;
})
.join("");
return set(body);
};
// rx: (not charset) — not(digit) -> \D, not(anyOf(",;")) -> [^,;]
const not = (x: Item): RxNode => {
const n = typeof x === "string" ? anyOf(x) : x;
if (n.set === undefined) throw new Error(`not: not a char set: ${n.src}`);
if (n.neg) return set(n.set);
if (/^\\[dswDSW]$/.test(n.src)) {
const c = n.src[1];
const flipped = c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase();
return classEscape(`\\${flipped}`);
}
return set(n.set, true);
};
// rx: (not-char "a-z" ...) — shorthand for (not (any ...))
const notChar = (...xs: Item[]) => not(anyOf(...xs));
// rx char classes, `[[:name:]]` in Emacs
const digit = classEscape("\\d");
const space = classEscape("\\s");
const wordChar = classEscape("\\w");
const notWordChar = not(wordChar);
const lower = anyOf("a-z");
const upper = anyOf("A-Z");
const alpha = anyOf(lower, upper);
const alnum = anyOf(alpha, "0-9");
const hexDigit = anyOf("0-9a-fA-F");
const blank = set(" \\t");
const control = set("\\x00-\\x1f\\x7f");
const punct = anyOf("!-/:-@[-`{-~");
const graphic = anyOf("!-~");
const printing = anyOf(" -~");
const ascii = set("\\x00-\\x7f");
const nonascii = set("\\u0080-\\uffff");
// rx: `nonl` is any char but newline; `anything` really is anything
const notNewline: RxNode = { src: ".", kind: "atom" };
const anything = set("\\s\\S");
/* =========================================================
* ANCHORS (zero-width)
* ========================================================= */
const zeroWidth = (src: string): RxNode => ({ src, kind: "seq" });
// rx: bos / eos
const start = zeroWidth("^");
const end = zeroWidth("$");
// rx: bol / eol — same symbols, only per line with the "m" flag
const lineStart = start;
const lineEnd = end;
const wordBoundary = zeroWidth("\\b");
const notWordBoundary = zeroWidth("\\B");
// rx: bow / eow — JS has no \< \>, so a boundary plus a lookaround
const wordStart = zeroWidth("\\b(?=\\w)");
const wordEnd = zeroWidth("\\b(?<=\\w)");
/* =========================================================
* GROUPS & BACKREFERENCES
* ========================================================= */
const group = (...xs: Item[]): RxNode => ({
src: `(${seq(...xs).src})`,
kind: "atom",
});
// rx has (group-n N ...); JS can't pick group numbers, but it can name them
const named = (name: string, ...xs: Item[]): RxNode => ({
src: `(?<${name}>${seq(...xs).src})`,
kind: "atom",
});
const backref = (ref: number | string): RxNode => ({
src: typeof ref === "number" ? `\\${ref}` : `\\k<${ref}>`,
kind: "atom",
});
/* =========================================================
* QUANTIFIERS
* ========================================================= */
const quantifier =
(suffix: string) =>
(...xs: Item[]): RxNode => ({
src: quantifiable(seq(...xs)) + suffix,
kind: "seq",
});
// greedy — rx: * + ?
const zeroOrMore = quantifier("*");
const oneOrMore = quantifier("+");
const optional = quantifier("?");
// lazy — rx: *? +? ??
const zeroOrMoreLazy = quantifier("*?");
const oneOrMoreLazy = quantifier("+?");
const optionalLazy = quantifier("??");
// rx: (= n ...) (>= n ...) (** n m ...)
const repeat = (n: number, ...xs: Item[]) => quantifier(`{${n}}`)(...xs);
const atLeast = (n: number, ...xs: Item[]) => quantifier(`{${n},}`)(...xs);
const between = (n: number, m: number, ...xs: Item[]) =>
quantifier(`{${n},${m}}`)(...xs);
/* =========================================================
* ENTRY POINTS
* ========================================================= */
// rx(...) -> the regexp source string (like Emacs, rx returns a string)
// RX(...) -> a ready-to-use RegExp, no more `new RegExp(seq(...))`
const rx = (...xs: Item[]): string => seq(...xs).src;
function RX(...xs: Item[]): RegExp {
return new RegExp(rx(...xs));
}
// Emacs uses `case-fold-search` for this; JS puts it on the regexp
RX.flags = (flags: string, ...xs: Item[]): RegExp =>
new RegExp(rx(...xs), flags);
None of this is new. On the Emacs side, as I said before, rx has
shipped for decades, and the Elisp version is more complete than mine.
The idea of describing patterns with a small DSL instead of raw syntax isn't new either. Plenty of people have tried it, each in their own way. One project I like a lot in this space is Zod, which I wrote about in my Zod quick tutorial. It's not a regexp builder: you compose small schema pieces, and Zod gives you back a parser and a TypeScript type from the same construction. It follows the same spirit, though: build big things out of small named pieces you can read.
If you write Elisp and have never tried rx, open *scratch*, type
(rx (+ digit)), and C-x C-e it. If you write JavaScript or
TypeScript, the file above is yours. And if you port it to another
language, send me a link.
-1:-- Readable Regular Expressions for JavaScript/TypeScript, Inspired by Emacs' rx (Post Rahul Juliato)--L0--C0--2026-09-30T23:00:00.000Z
Over at the Emacs subreddit, BBSnek writes about discovering remember mode. It’s been around for a long time. I remember using it before Org mode. Indeed, I still use Ctrl+c r to invoke org-capture because that was what I was used to with remember-mode.
Although I’m sure there are some, I can’t think of any reason to use remember-mode these days. Org mode can do everything it can and more. Org’s templates make capturing data and notes so much easier and, of course, you can bring all the other Org tools to bear on the results.
BBSnek uses it for quick notes—and it’s great for that—but Org can do that too and so much more. The truth is, I’m surprised that anyone is still using it for any other reason than inertia. Still, as usual, Emacs lets us have it our way and for those who prefer the simpler and lighter weight remember-mode, it’s there for them.
-1:-- Remember Remember Mode? (Post Irreal)--L0--C0--2026-09-30T15:15:24.000Z
Last December, I gave a talk at EmacsConf 2025 called “Zettelkasten for Regular Emacs Hackers”. I announced it back then, but never pointed to the recording. Here it is.
The pitch: writing improves the quality and depth of thought, and a Zettelkasten is an environment for thinking through writing, not merely a place to store notes. Nobody can hand you a finished thinking environment the way they can hand you an email client. You have to build your own, and Emacs is the most moldable place to do it.
In 23 minutes, I walk through:
init.el and Denote to create, link, rename, and backlink notes. Try it yourself with the configuration and the notes from the demo below.This is the whole configuration I used in the demo. Save it as init.el in an empty folder and launch Emacs from there:
$ emacs -nw --init-directory . init.el
(load-theme 'modus-vivendi-tinted) ;; Dark theme to match dark slides
(menu-bar-mode -1) ;; Disable topmost menu bar
(package-refresh-contents)
(use-package denote
:ensure t
:hook (dired-mode . denote-dired-mode)
:bind
(("C-c n n" . denote)
("C-c n r" . denote-rename-file)
("C-c n l" . denote-link)
("C-c n c" . denote-link-after-creating)
("C-c n b" . denote-backlinks)
("C-c n d" . denote-dired)
("C-c n g" . denote-grep))
:config
(setq denote-directory (expand-file-name "./notes/"))
;; Automatically rename Denote buffers when opening them so that
;; instead of their long file name they have, for example, a literal
;; "[D]" followed by the file's title. Read the doc string of
;; `denote-rename-buffer-format' for how to modify this.
(denote-rename-buffer-mode 1))
;; For the demo, I forced notes to show in full screen by default.
(setopt display-buffer-alist nil)
(add-to-list 'display-buffer-alist
'("^\\[D\\] " ;; Denote buffer name prefix
(display-buffer-reuse-mode-window display-buffer-full-frame)
))
These are the notes I wrote during the recording. The video runs them at 3 to 4 times the speed, so they’re easier to read here.
#+title: Mechanics and habits to use a Zettelkasten
#+date: [2025-11-21 Fri 20:48]
#+filetags: :zettelkasten:
#+identifier: 20251121T204827
Essential:
- Write: put in effort; [[denote:20251121T205045][GIGO]]
- Connect: Link notes to create trails
- Correct: Improve as you go
Additional:
- Design for use: give things a purpose, e.g. blog about it
- Create structure. [[denote:20251121T205312][Types of structures in a Zettelkasten]]: design new entry points and trails
- Start in the Zettelkasten: use the system to learn, make it grow, get better
- Start with a link: keeps your notes connected. [[denote:20251121T210416][Create notes as link first to avoid orphans]]
Tool doesn't matter as much. Small files are good. [[denote:20251121T210107][Use Denote in Emacs]]
(Christian Tietze: Zettelkasten for Regular Emacs Hackers, EmacsConf 2025)
#+title: Garbage in, garbage out
#+date: [2025-11-21 Fri 20:50]
#+filetags: :programming:quality:
#+identifier: 20251121T205045
The principle known as "Garbage In, Garbage Out" (GIGO) in computer
programming states that for any system, the quality of output is
directly depending on the quality of input.
See: https://en.wikipedia.org/wiki/Garbage_in,_garbage_out
#+title: Types of structures in a Zettelkasten
#+date: [2025-11-21 Fri 20:53]
#+filetags: :structure:zettelkasten:
#+identifier: 20251121T205312
Non-comprehensive list of structures that could be used in a Zettelkasten:
- Opposition pair: 1 note for the pair, 1 note per position/opposition each; pro/contra
- Table of contents: list of topics/headings
- an outline for a writing project
- recreation of a book's contents (so I can write in detail about the book);
- Argument: recreation of a distilled form to get to a conclusion;
list premises and evidence, and how these support the conclusion
- Counter-argument: address parts of the original argument to support another
view and test the argument's strength
- Table of things
- Graphics, like a concept map, Mind-Map, diagram: visually bring
elements into a relation, then write about the relation and the elements
Metaphors:
- Iceberg: visibile tip with hidden depth; someting that appears small
but is large; good metaphor for obstacles
- Black box: focus on inputs and outputs, and ignore the 'how' in the middle
- e.g. most Emacs Lisp functions (hoping the documentation is good)
- [[denote:20251121T205739][Atom, molecule, organism]]: how small parts compose to larger parts,
which compose to even larger pieces
(Christian Tietze: Zettelkasten for Regular Emacs Hackers, EmacsConf 2025)
#+title: Atom, molecule, organism metaphor
#+date: [2025-11-21 Fri 20:57]
#+filetags: :composition:recursion:
#+identifier: 20251121T205739
Atom: smallest part, indivisible elements
Molecule: comprised of atoms; structure added to combine elements
Organism: comprised of molecules; different level of analysis, irreducible to molecules/atoms
This can be used e.g. for programming: functions compose into larger
functions which compose into packages.
Actual biological organisms also work in such a way. Organs are part
of bodily functions (high abstraction), but from a chemical or
physical perspective, you can decompose them into atoms. That doesn't
tell you anything about the organ's function.
#+title: Denote.el for Zettelkasten in Emacs
#+date: [2025-11-21 Fri 21:01]
#+filetags: :denote:emacs:zettelkasten:
#+identifier: 20251121T210107
While not shipping with Emacs out of the box, Denote is easy to install and a great start to manage notes:
- it comes with unified way to create new notes, and hides time-stamped ID's with "[D]" in buffer lists
- it can manage links, and show incoming links to notes ("backlinks")
- it supports quickly adding links, and creating links _first_ out of
the box -- it just creates the note immediately, so you don't notice
- [[denote:20251121T210416][Create notes as link first to avoid orphans]]
* Example configuration
#+BEGIN_SRC elisp
(use-package denote
:ensure t
:hook (dired-mode . denote-dired-mode)
:bind
(("C-c n n" . denote)
("C-c n r" . denote-rename-file)
("C-c n l" . denote-link)
("C-c n c" . denote-link-after-creating)
("C-c n b" . denote-backlinks)
("C-c n d" . denote-dired)
("C-c n g" . denote-grep))
:config
(setq denote-directory (expand-file-name "./notes/"))
;; Automatically rename Denote buffers when opening them so that
;; instead of their long file name they have, for example, a literal
;; "[D]" followed by the file's title. Read the doc string of
;; `denote-rename-buffer-format' for how to modify this.
(denote-rename-buffer-mode 1))
#+END_SRC
#+title: Create notes as link first to avoid orphans
#+date: [2025-11-21 Fri 21:04]
#+filetags: :linking:zettelkasten:
#+identifier: 20251121T210416
Recommended practice by Christian Tietze: start a new note with a link
first from some place, any place, then create the file to match that
link (which would initially lead nowhere).
- Reduces orphans in the Zettelkasten by guaranteeing every note to
have at least one link.
- Supposedly teaches you to think about possible connections early,
which makes the network better.
A very loosey-goosey approach, when taken literally and used
liberally: it doesn't matter where you are, just leave a forward link
to something you wish existed. Even if it doesn't fit the current
note. After all, you came up with the idea just now -- so maybe
there's a connection that you just can't spell out, yet?
If you want the general introduction without Emacs, zettelkasten.de is the place to start.
Get new posts delivered to your inbox — subscribe.
-1:-- Watch My EmacsConf Talk: Zettelkasten for Regular Emacs Hackers (Post Christian Tietze)--L0--C0--2026-09-30T14:00:00.000Z
Charles Choi has been writing a bunch of nice little apps lately—more than I can cover. His latest, Aquí, is sort of a niche application but is potentially useful for more than just its intended use.
The back story is that Choi really likes the sunrise-sunset command in Emacs that tells him the times that the sun rises and sets. The problem is that sunrise-sunset requires that you first specify the latitude and longitude of where you want to know the times. The standard way of doing that is to record them in your init.el, which is what I do. Choi, however, travels with his laptop but still wants to know when the sun is coming up or going down. There’s no automatic way of setting the information so he wrote Aquí to provide it. The only downside is that it works exclusively on the Mac platform.
Once installed, all you need do is call aqui to set all the appropriate information. I hardly ever travel these days so I wasn’t going to bother but it’s easy to use and can give you your current latitude and longitude as well as your address so I thought, “Why not?”. After I installed and ran it, it gave me my exact address as well as my latitude and longitude. That might sometimes vary a bit depending on the vagaries of GPS but it’s certain to be close.
If you decide to try it out, be sure to take a look at the user manual so see what commands are available. Mostly, though, you’ll just need to run aqui. Also be sure to check out the installation section because you need to install a shortcut. That’s just a matter of clicking on a link in the manual but Aquí won’t work until you do.
-1:-- Aquí Location Finder (Post Irreal)--L0--C0--2026-09-29T15:31:05.000Z
A few days ago I announced
that I am working on denote-data. The goal is to provide an opt-in,
in-memory cache for Denote. It can be used to speed up certain
computationally expensive operations. It must be opt-in because there
are many users, myself included, who do not need such functionality (I
program because the challenge is fun, just how I did with denote-sequence, for example).
I have a final update on this that I posted on denote.git issue 724 and am copying below. Any future changes will appear in the release notes of the next version of Denote. In short: it is happening.
Hello again folks!
I have an update on the denote-data project. The code is in this denote.git branch for now: https://github.com/protesilaos/denote/tree/denote-data
What I am doing:
denote-data variants for them.funcall VAR instead of calling the heavy functions directly.denote-data-mode set those variables.This way we can have any extension do what denote-data does. And this way denote-data can be its own package as well.
list-packages and related.filenotify.el. @mentalisttraceur I have noted that your code is okay for copyright purposes, so I can check it out. Or you can send a PR, if you prefer.Otherwise, I think this is almost done.
With an opt-in cache we can support other workflows that people have been asking for, such as in-buffer completion.
-1:-- Emacs: status update on the ‘denote-data’ cache for Denote (Post Protesilaos)--L0--C0--2026-09-29T00:00:00.000Z
Raw link: https://www.youtube.com/watch?v=Y6NdZYNCkdk
UPDATE 2026-10-02 11:22 +0300: A longer clip which better captures the vibe is here: https://protesilaos.com/codelog/2026-10-02-omarchy-reasonableness/.
This is an excerpt from the ‘Root Access’ live stream: https://protesilaos.com/codelog/2026-09-27-emacs-live-root-access-show/.
I was making a broader comment there, but it is only available in the full video. Or it may be shared later as another short if @linkarzu gets to it.
-1:-- Comment on guilt by association about Omarchy and the Modus themes (Post Protesilaos)--L0--C0--2026-09-29T00:00:00.000Z
-1:-- How many screens left (Post Marcin Borkowski)--L0--C0--2026-09-28T18:17:26.000Z
One of my favorite Emacs commands is sunrise-sunset (Sunrise/Sunset (GNU Emacs Manual)) to get the day’s times for sunrise and sunset. For it to work right, you must have Emacs pre-configured with location information. This is done by setting three customizable variables:
calendar-latitudecalendar-longitudecalendar-location-nameIf you run Emacs on a system that is fixed in location, manually setting the above variables is a one time task. However if you do a fair amount of traveling with Emacs on a laptop, a more programmatic solution might be desired using a location service, ideally an OS-native one. If you’re a macOS user, Aquí (aqui.el) does just that by invoking a Shortcut to query the OS-native location service.
Leveraging Shortcuts gives Emacs access to a high-accuracy fused location update without need to build, install, and configure a separate macOS executable.
Once Aquí is installed, updating the location is done via the aqui command.
1 | |
This will update the above mentioned variables and message the current location in the mini-buffer.
For more information, refer to the Aquí User Guide.
-1:-- Announcing Aquí (Post Charles Choi)--L0--C0--2026-09-28T15:25:00.000Z
As I wrote back in July, although I’ve had Embark installed for some time, I’ve only just begun to really use it. Most of my uses involve following a link (of any type) and acting on a group of files, typically to delete several files at once. Of course, those uses are barely scratching the surface of Embark’s capabilities.
Those capabilities are essentially open ended. By that I mean you can define your own Embark actions. Stefano Rodighiero has a nice example of this. It’s a nice example for two reasons. First, it’s simple and easy to follow even though—the second reason—I don’t know anything about what he’s trying to do. The point is that Rodighiero has an obscure—to me—application that he wants to provide an action for but you don’t need to know the details of the application to follow the action.
This is incredibly powerful. Rodighiero let Embark do the hard work of identifying and parsing the target and merely wrote some simple glue to take that target reference and do something useful with it.
If, like me, you’re just beginning to feel your way around Embark, Rodighiero’s post is a useful read. You almost certainly won’t have a need to copy his action but it’s an excellent go by for your own.
-1:-- A Nice Example Of An Embark Action (Post Irreal)--L0--C0--2026-09-28T15:20:10.000Z
There's a nice long video about Org mode with plenty of tips for beginners. Also, it's the end of the month, so check out the submissions for the Emacs Carnival for September ("Games") and consider volunteering to host the next one!
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-09-28 Emacs news (Post Sacha Chua)--L0--C0--2026-09-28T13:31:07.000Z
One of the most common complaints I see from Les Misérables who are stuck in a corporate environment mandating the use of the horrific Outlook for email is that it’s very difficult or impossible to deal with it from within Emacs. For those of us who feel that doing any text oriented task outside of Emacs is an imposition, that’s intolerable.
Patient_Chance_3795 over at the Emacs subreddit has a hint on how to deal with one aspect of the problem: registering an Azure app. Check out his post for a suggestion on how to deal with the issue.
The sad truth is that the main difficulty in dealing with Outlook is not the emails but the other functions such as calendar invites. As usual, Microsoft has made a point of ensuring that you have to use their software if you’re unlucky enough to be find yourself in their ecosphere.
Regardless, every little bit of help in escaping their maw is welcome. If you’d like to at least deal with emails in an Outlook environment, take a look at Patient_Chance_3795’s post.
-1:-- Outlook To Emacs (Post Irreal)--L0--C0--2026-09-27T15:44:56.000Z
I recently built Emacs 31 from source to get the newest Emacs on Debian. Most things worked like they used to in Emacs 30 except the few things I knew about from reading up on Emacs 31, but for some strange reason, when browsing with eww inside Emacs, I could no longer hit & and get the site to open in LibreWolf. These are the relevant eww settings in my init.el:
(use-package eww ; Web browser
:config
(setopt browse-url-browser-function 'eww-browse-url
browse-url-secondary-browser-function 'browse-url-generic
browse-url-generic-program "librewolf"
; ...
I had a look in the manual to see if there was something new about how browse-url-secondary-browser-function should work, but it seems like it should work exactly like before. I then had a look in the Emacs News (C-h C-n), but it did not say anything about this. The documentation for the relevant variables and functions have not changed either.
Since browse-url-secondary-browser-function no longer accepts 'browse-url-generic, I tried giving it a lambda to start-process "librewolf" instead and that worked perfectly.
(use-package eww ; Web browser
:config
(setopt browse-url-browser-function 'eww-browse-url
browse-url-secondary-browser-function (lambda () (start-process "librewolf"))
; ...
Another thing that does work is when I do this:
(use-package eww ; Web browser
:config
(setopt browse-url-browser-function 'eww-browse-url
browse-url-secondary-browser-function (lambda () (browse-url-generic))
browse-url-generic-program "librewolf"
; ...
When I look up what is bound to & in eww with C-h k, I get eww-browse-with-external-browser.
(defun eww-browse-with-external-browser (&optional url)
"Browse the current URL with an external browser.
Use `browse-url-secondary-browser-function' if it is an external
browser, otherwise use `browse-url-with-browser-kind' to open an
external browser."
(interactive nil eww-mode)
(setq url (or url (plist-get eww-data :url)))
(if (eq 'external (browse-url--browser-kind
browse-url-secondary-browser-function url))
(let ((browse-url-browser-function browse-url-secondary-browser-function))
(browse-url url))
(browse-url-with-browser-kind 'external url)))
When I look into that function's source code, it calls browse-url–browser-kind to determine if the function used for browser-url-secondary-browser-function is an external browser or an internal one and if it is external, uses the function to browse the url by setting browse-url-browser-function temporarily and then use browse-url. If browse-url–browser-kind returns something else, it runs browse-url-with-browser-kind 'external on the url.
When I look at the source for browse-url–browser-kind, it determines if the function used is internal or external by looking at properties set with put for the function. When I looked up browse-url-firefox, after the function definition, there is a line that says (function-put 'browse-url-firefox 'browse-url-browser-kind 'external). If I look up browse-url-generic, I find (function-put 'browse-url-generic 'browse-url-browser-kind 'external) there as well.
When I call browse-url-generic interactively, it works perfectly. When I try to run (browse-url--browser-kind 'browse-url-generic "https://mostad.eu/") it returns external which I also get when running (browse-url--browser-kind browse-url-secondary-browser-function "https://mostad.eu/").
Going back to eww-browse-with-external-browser, when I execute
(if (eq 'external (browse-url--browser-kind
browse-url-secondary-browser-function "https://mostad.eu/"))
(let ((browse-url-browser-function browse-url-secondary-browser-function))
(browse-url "https://mostad.eu/"))
(browse-url-with-browser-kind 'external "https://mostad.eu/"))
it launches mostad.eu in eww, not in librewolf. (This is the part of the function that does the real work, but with url replaced with "https://mostad.eu/" so it works outside the function.) I did a C-x C-e after the if statement and it returned t as expected, so the let should be evaluated. I also tried modifying the else clause to message something if it kicked in and it did not happen.
When I tried the below to see if browse-url-browser-function somehow was something else, then the message buffer showed the even further below output, confirming that it was in fact browse-url-generic.
(if (eq 'external (browse-url--browser-kind
browse-url-secondary-browser-function "https://mostad.eu/"))
(let ((browse-url-browser-function
browse-url-secondary-browser-function))
(message "%s" browse-url-browser-function)
(browse-url "https://mostad.eu/"))
(message "neida"))
browse-url-generic Contacting host: mostad.eu:443 #<buffer *http mostad.eu:443*>
At this point I was running out of patience and concentration and needed to eat something. Before eating, I remembered when looking through the source code for browse-url-firefox, that it could also open firefox variants if browse-url-firefox-program was set to a firefox downstream, so I tried setting it to "librewolf" and had a go with & in an eww buffer to see if that would work when using & in eww. It did not work. It just reloaded the site I was in in eww.
(use-package eww ; Web browser
:config
(setopt browse-url-browser-function 'eww-browse-url
browse-url-secondary-browser-function 'browse-url-firefox
browse-url-firefox-program "librewolf"
; ...
I then reset the setting to browse-url-generic. When trying & in eww, it relaunches the same page in eww. If I set browse-url-secondary-browser-function to any function except the obvious ones like 'browse-url-firefox, 'browse-url-generic etc, for example if I set it to (lambda ()), when I use & in eww, it opens the site in LibreWolf. This seems slightly insane. I went down the rabbit hole and found browse-url-default-browser's source code and it seems to use browse-url-firefox which I previously set up to use librewolf, so maybe that is why this works?
Maybe there is logic saying that browse-url-browser-function and browse-url-secondary-browser-function cannot be the same that stops browse-url from opening the site in librewolf when eww-browse-with-external-browser temporarily sets browse-url-browser-function to browse-url-secondary-browser-function like it does in eww-browse-with-external-browser? I did not see anything like that in the functions I read if I understood them correctly, but I only read some functions and my understanding of Emacs Lisp is still at the beginner level.
I think for now, I will use
(use-package eww ; Web browser
:config
(setopt browse-url-browser-function 'eww-browse-url
browse-url-secondary-browser-function (lambda ())
browse-url-generic-program "librewolf"
; ...
which somehow works. I don't know why, but since it works, I'll leave it at that for now.
Is this problem reproducible at all for other people or is it just a problem on my machine? I am on Sway and have set $BROWSER="MOZ_ENABLE_WAYLAND=1 librewolf" in my .bash_profile. I configured Emacs 31 –with-native-compilation=aot –with-cairo –with-harfbuzz –with-gnutls –with-gsettings –with-tree-sitter –with-gif –with-png –with-jpeg –with-rsvg –with-tiff –with-imagemagick –with-pgtk –without-x –with-mailutils
Maybe someone more experienced with Emacs Lisp can look into it further if it is a reproducible problem and clear up why the obvious settings do not work, but other functions works. This could also be user error since I am still learning Emacs Lisp and Emacs in general. I know some things, but there are a lot more things yet for me to learn. Are my settings plain wrong? Have I misunderstood something here?
-1:-- EWW in Emacs 31 would not use browse-url-generic as browse-url-secondary-browser-function (Post Einar Mostad)--L0--C0--2026-09-27T12:00:00.000Z
Raw link: https://www.youtube.com/watch?v=c0qZ6QzpcVc
Tonight I will join the livestream that @linkarzu hosts together with Rumen and Ted. We will talk about Emacs. The discussion is open to the audience, so join us to write your questions and just hang out.
I am looking forward to this!
Root Access is a live conversation with Linkarzu, CEO members, and invited guests about tech, tools, workflows, and what they’re building. Guests can share their projects before we dive into the discussion. Everyone is welcome to watch live and join the chat; the full replay stays public.
-1:-- Emacs live at the ‘Root Access’ stream on 2026-09-27 at 21:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-09-27T00:00:00.000Z
Over at Emacs Dwelling, James Dyer has a nice post on capturing only the visible portion of a folded Org region. When you call kill-ring-save (Meta+w) on a folded region, it will capture everything to the kill ring—even the hidden text. As Dyer says, the folding is just a display hack. All the text is still there.
But sometimes Dyer wants to copy just the visible part. When I first read that, I couldn’t think of how to proceed without a lot of research and probably bit twiddling. That was just me overthinking the problem. It turns out to be pretty easy. The secret is org-copy-visible, which despite its name works outside of Org as well1.
Once you know that, the rest is easy. You need only call org-copy-visible instead of kill-ring-save. Dyer does that using the shortcut Ctrl+u Meta+w when he wants to copy only the visible part.
Take a look at his post for the code if this sort of thing interests you. You can, of course, simply call org-copy-visible explicitly but if you do it a lot, a shortcut such as Dyer’s make sense.
I haven’t verified this but Dyer says he’s used it with Dired
-1:-- Copy Only What You See (Post Irreal)--L0--C0--2026-09-26T15:48:40.000Z
Isee 724 on the denote.git issue tracker prompted me to start an experiment for an optional, in-memory cache for Denote. The cache can help speed up all sorts of operations.
The goal is to explore what can be done and whether it makes sense to
have that as a feature that users can enable, either directly in
denote.el or as a separate package.
For the time being, I have what I call denote-data:
https://github.com/protesilaos/denote-data. This is not for users
and does nothing useful right now. I have not written any
documentation on purpose. I am making it available as a
proof-of-concept in case there are interested parties who want to
contribute to the project.
What I have right now seems solid, though I suspect it will only be
useful at scale if the caching is done asynchronously. I plan to to
read the manual and check functions such as list-packages to
understand how we can do something in the background without blocking
Emacs.
If that is done, then the rest is a matter of (i) extending
denote-data to cover all cases where files need to be indexed and
(ii) plugging it into core Denote functions.
At any rate, this is an experiment. If it proves useful, then I will do what is necessary to make it work with Denote as an opt-in feature.
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.
denote-1:-- Emacs: I am experimenting with a Denote cache (Post Protesilaos)--L0--C0--2026-09-26T00:00:00.000Z
Emacs Asia-Pacific (APAC) virtual meetup will not be happening this month. See you in the next meetup! Stay updated: https://emacs-apac.gitlab.io/#stay-updated.
-1:-- CANCELED Announcing Emacs Asia-Pacific (APAC) virtual meetup, Saturday, September 26, 2026 (Post Emacs APAC)--L0--C0--2026-09-25T20:30:00.000Z
A quick look at a Quick Look for Emacs from Charles Choi. MacOS has the notion of a Quick Look to peek at a file without invoking a heavier weight application such as an editor. It’s more than just dumping text onto the screen. It can deal with text, PDFs, HTML, various video files, ODF, Microsoft file types, and many others.
Choi likes the Quick Look functionality but, of course, would like to have it in Emacs too. It’s not clear how to do this in general but it’s trivially easy to do it for Dired and Ibuffer. Choi solves the problem by binding dired-display-file and ibuffer-visit-buffer-other-window-noselect to the Space key. These commands open the file in another window so they’re more than just open the file as usual in Emacs.
If you like the idea of being able to peek at a file without abandoning your current window, take a look at Choi’s post for the details on how to do it from Dired and Ibuffer windows.
-1:-- Quick Look For Emacs (Post Irreal)--L0--C0--2026-09-25T15:43:33.000Z
Raw link: https://www.youtube.com/watch?v=aEnacESU-DU
[ The event will be recorded. ]
I will join Sacha Chua and Pedro A. Aranda Gutierrez to talk about Emacs. Specifically, Pedro will elaborate on work related to the Org LaTeX exporter. Pedro is doing some interesting work on that front.
-1:-- Emacs live with Sacha Chua and Pedro about Org LaTeX export on 2026-10-02 at 13:00 Europe/Athens (Post Protesilaos)--L0--C0--2026-09-25T00:00:00.000Z
Because Denote files are standard files, we do not provide the
equivalent of a find-file command to directly open a file in the
denote-directory. Though users may still want to have that
functionality, which is easy to implement with what core Denote
already provides:
(defun my-denote-find-file ()
"Open a file in the variable `denote-directory'."
(declare (interactive-only t))
(interactive)
(when-let* ((file (denote-file-prompt)))
(find-file file)))
This can be extended to include commands that only match specific file
types or regular expressions in general. For example, here is how we
prompt only for files that include .pdf in their name and also
modify the minibuffer prompt to indicate what we are doing:
(defun my-denote-find-file-pdf ()
"Prompt for FILE in the variable `denote-directory' and open it."
(declare (interactive-only t))
(interactive)
(when-let* ((file (denote-file-prompt "\\.pdf" "Prompt for PDF file")))
(find-file file)))
To take it a step further, we can define a keymap which we may then bind to a prefix key. This way we can access all the “Denote find file” commands we have defined for ourselves:
;; Define the keymap. Notice the symbol of :prefix, which is used in
;; the `define-key' below.
(defvar-keymap my-denote-find-file-map
:doc "Commands to find a file in the variable `denote-directory'."
:prefix 'my-denote-find
"f" #'my-denote-find-file
"p" #'my-denote-find-file-pdf)
;; Pick a suitable prefix key. Here we use C-c f. So
;; `my-denote-find-file' is this: C-c f f.
(define-key global-map (kbd "C-c f") 'my-denote-find)
Putting it all together:
;; Use this as a template to define as many commands as you need, like
;; with `my-denote-find-file-pdf'.
(defun my-denote-find-file ()
"Open a file in the variable `denote-directory'."
(declare (interactive-only t))
(interactive)
(when-let* ((file (denote-file-prompt)))
(find-file file)))
(defun my-denote-find-file-pdf ()
"Prompt for FILE in the variable `denote-directory' and open it."
(declare (interactive-only t))
(interactive)
(when-let* ((file (denote-file-prompt "\\.pdf" "Find PDF file")))
(find-file file)))
;; Define the keymap. Notice the symbol of :prefix, which is used in
;; the `define-key' below.
(defvar-keymap my-denote-find-file-map
:doc "Commands to find a file in the variable `denote-directory'."
:prefix 'my-denote-find
"f" #'my-denote-find-file
"p" #'my-denote-find-file-pdf)
;; Pick a suitable prefix key. Here we use C-c f. So
;; `my-denote-find-file' is this: C-c f f.
(define-key global-map (kbd "C-c f") 'my-denote-find)
Enjoy!
-1:-- Emacs: find a file in the ‘denote-directory’ (Post Protesilaos)--L0--C0--2026-09-25T00:00:00.000Z
A feature of macOS Finder that I enjoy using is Quick Look, where a preview of a selected file can be displayed by pressing the space bar. While “Quick Look” is not really a thing in Emacs (at least not out of the box), we can quasi-emulate this by taking advantage of commands that can open files in another Emacs window, all the while keeping the current Emacs window selected.
In Dired, I’ve bound SPC to the command dired-display-file.
1 | |
A similar thing can be done for IBuffer:
1 | |
This configuration is definitely a personal preference thing, so YMMV. That said, I’ve found these bindings to be quite useful, so perhaps you may as well.
-1:-- Binding the SPC key in Dired and IBuffer (Post Charles Choi)--L0--C0--2026-09-24T21:05:00.000Z
Select a region in a folded Org buffer, press M-w, and you get more than you bargained for: the kill ring receives the hidden text as well. Folding is only a display trick - the text is still in the buffer, and kill-ring-save dutifully copies every character between the region boundaries, ellipsis and all. Paste that into an email or a chat and the hidden notes come along for the ride.
The fix, in my config, is a prefix argument: C-u M-w copies only the visible text of the region, leaving everything folded or hidden behind. Plain M-w is unchanged.
I have been a fan of org-copy-visible ever since I discovered it works outside Org - back in 2023 I was using it in dired with dired-hide-details-mode to copy just the filenames. The only friction was having to remember M-x org-copy-visible instead of my muscle-memory M-w. So I taught my existing my/kill-ring-save wrapper a another trick: hand it a prefix argument and it copies only the visible text.
M-w has been remapped to a small wrapper for a while, mainly so that pressing it with no active region copies the current line. The new version forwards the prefix argument into a second branch:
(defun my/kill-ring-save (beg end &optional copy-visible)
"Save the region to the kill ring.
With a prefix argument, copy only the visible text between BEG
and END, skipping folded or hidden text, like `org-copy-visible'."
(interactive (if (use-region-p)
(list (region-beginning) (region-end) current-prefix-arg)
(list (line-beginning-position)
(line-beginning-position 2) current-prefix-arg)))
(if copy-visible
(org-copy-visible beg end)
(kill-ring-save beg end)))
(global-set-key [remap kill-ring-save] 'my/kill-ring-save)
[remap kill-ring-save] keeps M-w working everywhere.
So why org-copy-visible?, I could have written my own invisible-text walker, but org-copy-visible already is one and it ships with Emacs!
-1:-- Copy Only What You See: Teaching M-w a Prefix Argument (Post James Dyer)--L0--C0--2026-09-24T12:55:00.000Z
Recently, I needed to test a full build of my C networking library together with the C build toolchain under NetBSD. Everything is fine on GNU/Linux (Ubuntu) and macOS—I just want to confirm it works on NetBSD as well.
Why NetBSD? NetBSD is an open-source Unix-like operating system famous for its outstanding portability. It runs on an exceptionally wide range of hardware—from modern x86-64/aarch64 servers and desktops to embedded systems, handheld devices, vintage machines (such as VAX and m68k), and even some game consoles (see Platforms supported by NetBSD). Its long-standing motto is “Of course it runs NetBSD”. This makes sense if you want maximum portability.
After all, it’s essentially good old 4.3BSD, which adds a nice touch of nostalgia.
My home data center, as I call it, does not have a dedicated NetBSD machine so I settled on using a virtual machine, or rather, the smolBSD microVM, a minimal NetBSD-based BSD UNIX virtual machine that boots and starts a service in a few milliseconds.
My requirements to the microVM are simple.
ksh in NetBSD, by default).gcc, make, ld, etc.)
because I will compile software inside the microVM.(Yes, NetBSD comes with vi—not vim!—version 1.81.6-2013-11-20nb4 pre-installed, but… but I just need Emacs.)
A detailed description of the smolBSD installation process on Debian-based and macOS systems can be found on the smolBSD GitHub page.
.img)To build a raw NetBSD image we will use SMOLerfile, smolBSD’s
configuration file for defining a minimal NetBSD-based microVM
service. It supports the well-known Dockerfile verbs—like FROM,
RUN, CMD etc.—but does not implement the full Dockerfile
reference. This is sufficient for our purposes.
My Dockerfile.devsh SMOLerfile that installs and configures Emacs is shown
below. I put it in the $NETBSD_ROOT/smolerfiles directory on the host;
the NETBSD_ROOT envvar points to the smolBSD installation directory on the host.
# ----------------------------------------------------------
# NetBSD development shell with Emacs included.
# Time-stamp: <Last changed 2026-09-19 16:59:22 by magnolia>
# ----------------------------------------------------------
# Disable: unusual "FROM" naming, multiple RUN don't matter, and non-JSON CMD
# hadolint global ignore=DL3006,DL3025,DL3059
# `base' -- The core system. Contains the essential binaries, shared libraries,
# and utilities required for a minimally functional NetBSD system.
# This set is mandatory.
# `etc' -- System configuration files (primarily under /etc and a few other places).
# Needed for a complete install from scratch;
# provides the default configuration skeleton.
# `comp' -- Compiler/toolchain set. Includes compilers (C/C++),
# header files (/usr/include), static libraries, linker, make,
# and related development tools.
FROM base,etc,comp
# Mandatory -- a name of the service
LABEL smolbsd.service="devsh"
# Shrink the final image to only needed content + extra NNN MB
LABEL smolbsd.minimize=+512
# Extra packages
LABEL smolbsd.addpkgs="pkgin pkg_tarup pkg_install sqlite3 rsync curl"
# Microvm will use a `pty' as console
LABEL smolbsd.use_pty="y"
# 2G disk initially; it will be truncated to minimum + extra NNN MB
LABEL smolbsd.imgsize=2048
ARG HOSTNAME=bsd
ARG USERSHELL=/bin/ksh
ENV USERNAME=magnolia
# Note: INCLUDE is SMOLerfile extension!
# For interactive shell; provided by smolBSD distribution.
INCLUDE smolerfiles/usershell.inc
# ====================================
# EMACS
# ====================================
# Create ~/.emacs with minimal tuning.
RUN <<EOF
cat > ${HOMEDIR}/.emacs << 'EMACS'
;;; Minimal Emacs config for NetBSD
;; Basic settings
(setq
column-number-mode t
ring-bell-function 'ignore
scroll-step 1
scroll-conservatively 100000
scroll-preserve-screen-position 1
scroll-margin 0
mouse-wheel-scroll-amount '(1)
mouse-wheel-progressive-speed nil
global-visual-line-mode nil
show-paren-mode t
tab-always-indent 'indent
tab-width 4
tool-bar-mode nil
truncate-lines nil
word-wrap t
make-backup-files nil
auto-save-default nil
)
;; No line numbers (can be toggled by "C-c l")
(global-display-line-numbers-mode -1)
(setq-default indent-tabs-mode nil)
(global-hl-line-mode 1)
(delete-selection-mode t)
(desktop-save-mode 1)
(savehist-mode 1)
;; No menu
(menu-bar-mode -1)
;; Killing the current Dired buffer upon navigating into a different directory.
(setq dired-kill-when-opening-new-dired-buffer t)
;; Basic C settings
(setq c-default-style "stroustrup")
(setq c-basic-indent 4)
(setq c-basic-offset 4)
(c-set-offset 'innamespace 0)
(c-set-offset 'inextern-lang 0)
;; Some useful keybindings
(define-key global-map (kbd "RET") #'newline-and-indent)
(global-set-key (kbd "M-o") #'other-window)
(global-set-key (kbd "M-a") #'beginning-of-line) ; QEMU intercepts "C-a"
(global-set-key (kbd "M-e") #'end-of-line) ; For consistency
(global-set-key (kbd "C-c l") #'display-line-numbers-mode)
EMACS
chown ${USERNAME} ${HOMEDIR}/.emacs
chmod 600 ${HOMEDIR}/.emacs
EOF
# To use Emacs keys in the shell
RUN echo 'set -o emacs' > /etc/shrc
RUN echo 'alias ll="ls -la"' >> /etc/shrc
# Install Emacs
RUN pkgin up && pkgin -y in emacs-nox11
# Install GNU make; we will use it instead of default `bsdmake'
RUN pkgin -y in gmake
# Login to `ksh', the default shell on NetBSD
CMD login -f -p $USERNAME
As you can see, I added a new key binding—M-a for beginning-of-line.
The point is that QEMU—an open-source machine emulator
and virtualizer available on essentially all major desktop and server
operating systems (Linux, macOS, Windows, BSDs) and the most common
CPU architectures—uses Ctrl-A as its special escape key for the
character-backend multiplexer. That is why pressing Ctrl-A aborts or
interferes with the VM instead of being passed to Emacs.
A quick workaround is to send a real C-a to Emacs by pressing Ctrl-A
twice (Ctrl-A Ctrl-A)—the first Ctrl-A is eaten by QEMU;
the second one is delivered to the guest.
My muscle memory resists using double-pressing C-a so I added M-a,
although it’s not that easy to remember either. M-e is added for consistency.
To simplify the build process, I’ve created a simple script:
#!/bin/bash
# `smolbsd-build.sh SERVICE'
# ==========================
# NETBSD_ROOT envvar should point to the smolBSD directory on the host.
if [ -z "$NETBSD_ROOT" ]; then
echo "Error: NETBSD_ROOT envvar not defined."
exit 2
fi
if [ -z "$1" ]; then
echo "Usage: smolbsd-build.sh SERVICE"
exit 1
fi
pushd .
IMAGE="smolerfiles/Dockerfile.$1"
cd $NETBSD_ROOT
echo "Building $IMAGE ..."
./smoler.sh build $IMAGE
popd
Here is my script to run the NetBSD microVM:
#!/bin/bash
# `smolbsd-run.sh SERVICE'
# ========================
# NETBSD_ROOT envvar should point to the smolBSD directory on the host.
if [ -z "$NETBSD_ROOT" ]; then
echo "Error: NETBSD_ROOT envvar not defined."
exit 2
fi
if [ -z "$1" ]; then
echo "Usage: smolbsd-run.sh SERVICE"
exit 1
fi
pushd .
cd $NETBSD_ROOT
# 1) -f CONFIG VM configuration file; it's created automatically during the build stage.
# 2) -P use PTY terminal
# 3) -n 2 enable stopping VM from ksh (as `su'): . /etc/include/shutdown
# 4) -w PATH mount host's PATH to guest's /mnt/
./startnb.sh -f "etc/$1.conf" -P -n 2 -w ${HOME}/workspace/devel
popd
Now we can run the devsh microVM (bsd) on the host (foxtrot):

As you can see, the NetBSD kernel starts in 26 ms on my old Dell Latitude 7400 laptop. The entire VM initially occupies 180 MB of host memory. Not a bad result.
And then we can run Emacs:

Happy emacsing in VM!
— The Emacs Cat.
-1:-- Running Emacs in microVM (Post The Emacs Cat)--L0--C0--2026-09-23T08:55:56.000Z
This is a major mode for editing sxhkdrc files. SXHKD is the Simple
X Hot Key Daemon which is commonly used in minimalist desktop sessions
on Xorg (I use it with bspwm, herbstluftwm, and i3wm). The sxhkdrc
file configures key chords, binding them to commands. For the
technicalities, read the man page sxhkd(1).
sxhkdrc-modeThis version makes quality-of-life refinements to an already stable package.
Key chord chains are written with the semicolon (;) or colon (:)
characters. The former refers to one-shot operations while the latter
is for continuous ones that exit only with Escape.
The corresponding faces are sxhkdrc-mode-chain-one-off-separator and
sxhkdrc-mode-chain-continuous-separator.
The previous version only covered the ; in front of a command. The
prefixes @ and ~ are now covered as well. They are all using teh
face sxhkdrc-mode-command-prefix, which supersedes the deprecated
sxhkdrc-mode-command-async.
This concerns the backslash character (\) at the end of a line. The
applicable face is sxhkdrc-mode-continuation-line.
The updated manpage of SXHKD, sxhkd(1), mentions all the modifiers
that it supports. I have extended support for all of them in the order
they appear in the manual.
-1:-- Emacs: sxhkdrc-mode version 1.3.0 (Post Protesilaos)--L0--C0--2026-09-23T00:00:00.000Z
Another month, another agent-shell update. If you missed the last post, have a look at the 0.73 update.
As usual, this post showcases highlights, but please check out the full list of changes if you're after the nitty-gritty.
agent-shell?agent-shell is a native Emacs mode to interact with AI agents powered by ACP (Agent Client Protocol).
Two agents join the family, available as usual via M-x agent-shell, or explicitly as follows:
M-x agent-shell-start-antigravity-agent.M-x agent-shell-start-qoder-agent.Likely the most impactful feature in the release. It's highly discoverable and plenty useful, so I'm expecting a fair amount of uptake.
A typical shell experience offers a prompt. Users type and submit their commands and wait for the command to finish before the shell prompt is offered again.
Deriving from comint-mode, agent-shell was no different. That is, until now.
As of v0.78, agent-shell offers a writeable prompt at the end of the buffer at all times. Submit, and the prompt comes right back while the agent is busy handling the turn. Type and submit again and agent-shell will automatically queue your request if necessary.
While queueing itself isn't a new feature, the shell prompt queueing route offers a freebie in terms of cognitive load. Submit prompts as you used to (via RET binding), and let agent-shell decide whether to handle now or queue for later.

The persistent prompt is enabled by default via agent-shell-persistent-prompt-enabled. If this isn't your cup of tea, it can be easily disabled with:
(setopt agent-shell-persistent-prompt-enabled nil)
The viewport's compose buffer is there either way, and submitting from it mid-turn routes through exactly the same logic.
As of this release, agent-shell can also steer turns (provided the agent supports the _session/steering ACP extension). Steering enables you to course correct in-flight prompts without cancelling or waiting for the prompt processing to finish. As of today, I'm aware of Claude and Codex handling ACP steering, but please reach out if you know of others.
Steering an in-flight turn via M-x agent-shell-prompt-steer offers a similar experience to the existing M-x agent-shell-prompt-queue. That is, prompting the user for text in the minibuffer. Having said that, we now have a new and shiny persistent prompt, and as we now know, the RET binding automatically queues if needed. From the same prompt you can now also steer by submitting via the M-RET binding.

Huge thanks to @OSadovy, who took on the legwork for steering in #777.
With RET and M-RET respectively queueing and steering as needed, the default behaviour is configurable via agent-shell-busy-submit-default-function (queues by default) while the M-RET (or C-u RET) route uses agent-shell-busy-submit-override-function (steers by default). Both customizations accept a function, so swapping would offer RET steering and M-RET queueing with something like:
(setopt agent-shell-busy-submit-default-function
#'agent-shell-busy-submit-steer)
(setopt agent-shell-busy-submit-override-function
#'agent-shell-busy-submit-queue)
Both apply wherever a prompt is submitted mid-turn, the shell prompt and the viewport's compose buffer alike. As emacsers, we want all sorts of customizations, so custom functions can be used too, if you'd like something a little different from what's offered.
While we could already C-y paste screenshots from the clipboard, we can now drag and drop files from external file managers onto either shell or viewport buffers. Images get a preview, anything else is attached as an @path link.

Thank you @dustinfarris for #825. While on topic, @dustinfarris fixed file mentions carrying whitespace in paths (#824).
After all this time, I had no idea the temporary thumbnail generated by macOS's screenshot utility is draggable, and so you can now drop it straight into your agent-shell session.

Thanks to @dustinfarris for the tip!
If you'd like to keep an eye on token cost, headers can now show the session's cumulative cost, right after the context usage indicator.
Currently off by default, so opt in with:
(setopt agent-shell-show-cost-indicator t)
Keep in mind cost is displayed for agents reporting cost via ACP. Thank you @mrcnski for #834.
agent-shell folds lots away by default (tool calls, thinking, groups), requiring additional help if we want closer isearch integration. Thanks to @mrcnski's contribution in #832, folded fragments now respect search-invisible.
Searching also folds back what it expanded once you're done, groups included. Also thanks to @Gleek for #827: expanding a fragment no longer clobbers isearch's match data.
Slash command completion is now offered more idiomatically across all three surfaces (shell prompt, viewport compose buffer, minibuffer): it only kicks in when whitespace alone precedes the /. Thank you @izeigerman for #810.
File completion after @ is unchanged.
We now have M-x agent-shell-copy-last-output, which grabs the most recent output regardless of point location, so you can pull the latest response from anywhere in buffer.
Links got a handful of fixes/improvements worth mentioning:
code spans are now linked (#781, #782, thanks @OSadovy).dired.C-u N on next/previous page now moves N interactions instead of one, and a negative prefix pages the other way. Moving forward past the newest interaction restores a parked compose snapshot, so C-u N does what pressing the key N times does, rather than stopping short of your draft. Thank you @liaowang11 for #813.
Following on from last month's table work, plain data rows now get a face of their own, and every table face inherits from one base face. If you'd like to restyle tables wholesale, you now have a single place to do it. Thank you @mrcnski for #822.
agent-shell-session-list-page-limit takes a positive integer.agent-shell-session-idagent-shell-session-id returns the current ACP session ID. On that note, there's also M-x agent-shell-copy-session-id for when you just want it in your kill ring.
agent-shell-opencode-default-model-variant was a bit narrow, so it's been replaced by agent-shell-opencode-default-config-options, an alist of whatever OpenCode advertises under "Available config options" when starting a new shell:
(setopt agent-shell-opencode-default-config-options
'(("model" . "anthropic/claude-opus-4-5")
("effort" . "high")
("mode" . "plan")))
Options are applied in the order listed, and order matters. Thank you @nhojb for #739.
Four more joining the lot:
agent-shell agent, project, and skill definitions in org-roam.agent-shell sessions.agent-shell.With the new renderer offering a richer Markdown experience for some time now, the deprecated markdown-overlays renderer is gone.
If you peeked at the commit logs for the period, you'll see it's been another busy month. Since the last post, 153 commits shipped, 32 issues have been closed and 25 pull requests merged. As of this writing, the backlog sits at 16 open issues and 6 open PRs (versus 11 and 5 last time around).
Zooming out a little, here's how the backlog has tracked since March:

Side note: this chart was generated using the /github-activity skill shared in my emacs-skills repo.
From the graph, it's evident when I became a father, but you can also see I managed to bring things back down, hovering at a fairly stable level since.
All of this requires daily attention 👉 hint hint 👈
These days (especially at the workplace), vendor-neutral tooling matters more than ever, and there are a couple of ways to help keep agent-shell going. Some cost money, others just a click. All are appreciated ;)
agent-shell is built and maintained by me, an indie dev, while the tools it often competes with at the workplace have well-funded teams behind them. Time spent on agent-shell is time away from other work that pays the bills, so if it's useful to you, please consider sponsoring the project. And if your employer benefits from your agent-shell use, nudge them to chip in too, they can typically contribute at a scale individuals can't.
GitHub stars help with exposure, attracting new users and potential sponsors. Starring agent-shell costs nothing and can potentially help bring in more funding, so if you don't mind a couple of clicks, the project can really use another GitHub star.
Thank you to all contributors for these improvements!
comint-highlight-prompt (@OSadovy)agent-shell-antigravity (@tychoish)agent-shell-queue link (@tychoish)Package-Requires floors (@alberti42)agent-shell-org-config to Related projects (@lllShamanlll)search-invisible (@mrcnski)agent-shell-queue-transient link to README (@mrcnski)agent-shellLiking agent-shell? Would like to see it evolve? Consider sponsoring the effort.
-1:-- agent-shell 0.78 updates (Post Alvaro Ramirez)--L0--C0--2026-09-22T00:00:00.000Z
Please note that planet.emacslife.com aggregates blogs, and blog authors might mention or link to nonfree things. To add a feed to this page, please e-mail the RSS or ATOM feed URL to sacha@sachachua.com . Thank you!